forked from nikita/muzika-gromche
106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
import { formatTime } from "@/lib/AudioTrack";
|
|
import { rangeInclusive } from "@/lib/iter";
|
|
import {
|
|
useOptimalBeatTickInterval,
|
|
useOptimalTickInterval,
|
|
useTicksBounds,
|
|
} from "@/lib/TinelineTicks";
|
|
import { usePx } from "@/lib/vue";
|
|
import { useTimelineStore } from "@/store/TimelineStore";
|
|
import { storeToRefs } from "pinia";
|
|
import {
|
|
computed,
|
|
type ComputedRef,
|
|
type MaybeRefOrGetter,
|
|
toValue,
|
|
} from "vue";
|
|
import type { AnyTime, Beats, Px, Seconds } from "./units";
|
|
|
|
export interface Ticks<T extends AnyTime> {
|
|
tickIn: ComputedRef<T>;
|
|
tickOut: ComputedRef<T>;
|
|
interval: ComputedRef<T>;
|
|
/** An inclusive range from tickIn to tickOut, with `interval` step. */
|
|
ticks: ComputedRef<T[]>;
|
|
width: ComputedRef<Px>;
|
|
widthPx: ComputedRef<string>;
|
|
left: (tickIn: T) => ComputedRef<Px>;
|
|
label: (tickIn: T) => ComputedRef<string>;
|
|
}
|
|
|
|
export function useTimelineTicks<T extends AnyTime>(
|
|
viewportIn: MaybeRefOrGetter<T>,
|
|
viewportOut: MaybeRefOrGetter<T>,
|
|
interval: ComputedRef<T>,
|
|
intervalToPixels: (interval: T) => Px,
|
|
positionToPixels: (position: T) => Px,
|
|
positionToLabel: (position: T) => string,
|
|
): Ticks<T> {
|
|
const ticksBounds = useTicksBounds(interval, viewportIn, viewportOut);
|
|
|
|
const tickIn = computed(() => ticksBounds.value.tickIn);
|
|
const tickOut = computed(() => ticksBounds.value.tickOut);
|
|
const ticks = computed(() =>
|
|
rangeInclusive(tickIn.value, tickOut.value, toValue(interval)) as T[]
|
|
);
|
|
const width = computed(() => intervalToPixels(toValue(interval)));
|
|
const widthPx = usePx(width);
|
|
|
|
return {
|
|
tickIn,
|
|
tickOut,
|
|
interval,
|
|
ticks,
|
|
width,
|
|
widthPx,
|
|
left: (tickIn) => computed(() => positionToPixels(tickIn)),
|
|
label: (tickIn) => computed(() => positionToLabel(tickIn)),
|
|
};
|
|
}
|
|
|
|
// TODO: cache / singletone / store?
|
|
|
|
export function useTimelineTicksSeconds(): Ticks<Seconds> {
|
|
const timeline = useTimelineStore();
|
|
const {
|
|
contentWidthIncludingEmptySpace,
|
|
durationIncludingEmptySpace,
|
|
viewportInSeconds,
|
|
viewportOutSeconds,
|
|
} = storeToRefs(timeline);
|
|
|
|
return useTimelineTicks<Seconds>(
|
|
viewportInSeconds,
|
|
viewportOutSeconds,
|
|
useOptimalTickInterval(
|
|
contentWidthIncludingEmptySpace,
|
|
durationIncludingEmptySpace,
|
|
),
|
|
(interval) => timeline.secondsToPixels(interval),
|
|
(position) => timeline.secondsToPixels(position),
|
|
(position) => formatTime(position, 2),
|
|
);
|
|
}
|
|
|
|
export function useTimelineTicksBeats(): Ticks<Beats> {
|
|
const timeline = useTimelineStore();
|
|
const {
|
|
contentWidthIncludingEmptySpace,
|
|
durationBeatsIncludingEmptySpace,
|
|
viewportInLoopOffsetBeats,
|
|
viewportOutLoopOffsetBeats,
|
|
} = storeToRefs(timeline);
|
|
|
|
return useTimelineTicks<Beats>(
|
|
viewportInLoopOffsetBeats,
|
|
viewportOutLoopOffsetBeats,
|
|
useOptimalBeatTickInterval(
|
|
contentWidthIncludingEmptySpace,
|
|
durationBeatsIncludingEmptySpace,
|
|
),
|
|
(interval) => timeline.beatsToPixels(interval),
|
|
(position) => timeline.loopOffsetBeatsToPixels(position),
|
|
(position) => position.toFixed(),
|
|
);
|
|
}
|