diff --git a/package.json b/package.json index 0192588..cfadbc2 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@typescript-eslint/parser": "^6.19.0", "autoprefixer": "^10.4.16", "conventional-changelog-eslint": "^5.0.0", + "dayjs": "^1.11.10", "dequal": "^2.0.3", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95c7ff3..92bbe35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ devDependencies: conventional-changelog-eslint: specifier: ^5.0.0 version: 5.0.0 + dayjs: + specifier: ^1.11.10 + version: 1.11.10 dequal: specifier: ^2.0.3 version: 2.0.3 @@ -1598,6 +1601,10 @@ packages: hasBin: true dev: true + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: true + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} diff --git a/src/lib/Block.ts b/src/lib/Block.ts index 4da1eb9..59d97e9 100644 --- a/src/lib/Block.ts +++ b/src/lib/Block.ts @@ -17,6 +17,17 @@ const DEFAULT_VALIDATION_OPTIONS: Required = { } }; +type ISetTimeOptions = { + maintainDuration?: boolean; + snap?: boolean; + snapTimes?: number[]; + snapThreshold?: number; +}; + +const DEFAULT_SET_TIME_OPTIONS = { + snapThreshold: 150 +}; + export class Block implements ISequenceChild { layers: Layer[]; index: number; @@ -26,7 +37,7 @@ export class Block implements ISequenceChild { data?: { [key: string]: unknown; }; - markers: { time: number; label: string }[] = []; + markers: { time: number; title?: string }[] = []; errors: { type: string; message: string }[] = []; private _inTime?: number; @@ -194,18 +205,12 @@ export class Block implements ISequenceChild { this.setOutTime(this._outTime as number); } - public setInTime( - value: number, - options: { maintainDuration?: boolean; snap?: boolean; snapTimes?: number[] } = {} - ) { + public setInTime(value: number, options: ISetTimeOptions = DEFAULT_SET_TIME_OPTIONS) { const res = this.setTimeCommon(value, tHandles.inTime, options); return res.apply(); } - public setOutTime( - value: number, - options: { maintainDuration?: boolean; snap?: boolean; snapTimes?: number[] } = {} - ) { + public setOutTime(value: number, options: ISetTimeOptions = DEFAULT_SET_TIME_OPTIONS) { const res = this.setTimeCommon(value, tHandles.outTime, options); return res.apply(); } @@ -223,11 +228,18 @@ export class Block implements ISequenceChild { protected setTimeCommon( inputValue: number, prop: tHandles, - options: { maintainDuration?: boolean; snap?: boolean; snapTimes?: number[] } = {}, + options: ISetTimeOptions = DEFAULT_SET_TIME_OPTIONS, depth = 0 ) { depth++; + const { + maintainDuration, + snap, + snapTimes, + snapThreshold = DEFAULT_SET_TIME_OPTIONS.snapThreshold + } = options; + const value = this.roundTime(inputValue); const propValidation = this.validations[prop]; @@ -278,16 +290,15 @@ export class Block implements ISequenceChild { // if value is within a certain threshold of a value in snapTimes // snap to that value // TODO: parse in value bases on ui pixels - const snapTimeThreshold = 150; - if (options.snapTimes) { - const snaps = options.snapTimes + if (snapTimes) { + const snaps = snapTimes .map((snapTime) => { // make relative return snapTime - this.parent.getAbsoluteInTime(); }) .filter((snapTime) => { - return Math.abs(setT - snapTime) < snapTimeThreshold; + return Math.abs(setT - snapTime) < snapThreshold; }) .sort((a, b) => { return Math.abs(setT - a) - Math.abs(setT - b); @@ -319,7 +330,7 @@ export class Block implements ISequenceChild { const expanding = (fwd && prop == 'outTime') || (!fwd && prop == 'inTime'); - if (options?.maintainDuration) { + if (maintainDuration) { //console.debug(debugPrefix, 'set opposing to maintain duration'); const res = setOp(opC + diff, { maintainDuration: false, snapTimes: [] }); @@ -357,15 +368,10 @@ export class Block implements ISequenceChild { if ((isIn && setT < adj[opProp]) || (!isIn && setT > adj[opProp])) { //console.debug(debugPrefix, 'hits adjacent block'); - if (options.snap) { + if (snap) { setT = adj[opProp]; } else { - const res = adj.setTimeCommon( - setT, - opProp, - { maintainDuration: options.maintainDuration }, - depth - ); + const res = adj.setTimeCommon(setT, opProp, { maintainDuration }, depth); res.apply(); setT = res.v1; } @@ -429,7 +435,7 @@ export class Block implements ISequenceChild { // const lastChild = layer.blocks[layer.blocks.length - 1]; - if (!isIn && setT - this.inTime < lastChild.outTime && !options.maintainDuration) { + if (!isIn && setT - this.inTime < lastChild.outTime && !maintainDuration) { const res = lastChild.setTimeCommon( setT - this.inTime, tHandles.outTime, @@ -440,7 +446,7 @@ export class Block implements ISequenceChild { setT = this.inTime + res.v1; return res; - } else if (isIn && !options.maintainDuration) { + } else if (isIn && !maintainDuration) { if (this.outTime - setT < lastChild.outTime) { const res = lastChild.setTimeCommon( this.outTime - setT, @@ -461,7 +467,10 @@ export class Block implements ISequenceChild { return set(setT); } - public move(delta: number, options: { snap?: boolean; snapTimes?: number[] } = {}) { + public move( + delta: number, + options: Omit = DEFAULT_SET_TIME_OPTIONS + ) { if (delta == 0) return; const res = this.setTimeCommon( diff --git a/src/lib/components/Block.svelte b/src/lib/components/Block.svelte index 4d17639..57d7704 100644 --- a/src/lib/components/Block.svelte +++ b/src/lib/components/Block.svelte @@ -120,11 +120,13 @@ } // if user has cursor over a marker or handle on another block then snap to its time if within a certain threshold - let res; + const snapThreshold = 10 * ($duration / $width); // snap to marker if within 10 pixels regardless of screen width and duration + + const opts = { snap, snapTimes: $snapTimes, snapThreshold }; if (handle == 'block') { - res = block.move(accDeltaTime, { snap: snap, snapTimes: $snapTimes }); + res = block.move(accDeltaTime, opts); time.set(block.absoluteInTime); } else if (handle == 'inTime') { /*const snapInDelta = $snapValue && $snapValue - (block.inTime + accDeltaTime); @@ -139,12 +141,12 @@ //snap = true; } else {*/ - res = block.setInTime(block.inTime + accDeltaTime, { snap: snap, snapTimes: $snapTimes }); + res = block.setInTime(block.inTime + accDeltaTime, opts); //} time.set(block.absoluteInTime); } else if (handle == 'outTime') { - res = block.setOutTime(block.outTime + accDeltaTime, { snap: snap, snapTimes: $snapTimes }); + res = block.setOutTime(block.outTime + accDeltaTime, opts); time.set(block.absoluteOutTime); } @@ -250,7 +252,12 @@ {#if markers.length > 0}
{#each markers as marker, index} - {/each}
diff --git a/src/lib/components/BlockMarker.svelte b/src/lib/components/BlockMarker.svelte index f96ec8b..6d43b39 100644 --- a/src/lib/components/BlockMarker.svelte +++ b/src/lib/components/BlockMarker.svelte @@ -4,13 +4,20 @@ export let time: number; export let index: number; + export let title = `Marker #${index + 1}`; export let disableSnapping = false; export let block: Block; export let tag = 'div'; - const { duration, width, scrubOverride, time: playheadTime } = getSequenceContext(); + const { duration, width, scrubOverride, time: playheadTime, formatTimeFn } = getSequenceContext(); + + //export let format = (value: number) => `${Math.round(value)}`; + + export let formatTitle = () => { + return `${title} (+${formatTimeFn(time)})`; + }; $: timeToPixel = (1 / $duration) * $width; $: absoluteTime = time + block.absoluteInTime; @@ -25,7 +32,7 @@ >
`${Math.round(value)}`; + setSequenceContext({ time, duration, @@ -29,7 +31,8 @@ width, snapTimes, selectedHandle, - scrubOverride + scrubOverride, + formatTimeFn }); $: currentTime = $time; @@ -84,7 +87,7 @@ > - + diff --git a/src/lib/components/SequenceContext.ts b/src/lib/components/SequenceContext.ts index 9740562..034e22d 100644 --- a/src/lib/components/SequenceContext.ts +++ b/src/lib/components/SequenceContext.ts @@ -12,6 +12,7 @@ export type SequenceContext = { snapTimes: Writable; scrubOverride: Writable; sequence: Writable; + formatTimeFn: (time: number) => string; }; export const key = Symbol(); diff --git a/src/lib/components/Timebar.svelte b/src/lib/components/Timebar.svelte index 5b2511d..d7b0948 100644 --- a/src/lib/components/Timebar.svelte +++ b/src/lib/components/Timebar.svelte @@ -4,9 +4,15 @@ import TimebarLabel from './TimebarLabel.svelte'; - const { time, duration, scrubOverride, selectedHandle } = getSequenceContext(); - - export let formatTimeFn = (value: number) => `${Math.round(value)}`; + const { + time, + duration, + scrubOverride, + selectedHandle, + formatTimeFn: sequenceFormatTimeFn + } = getSequenceContext(); + + export let formatTimeFn = sequenceFormatTimeFn; // We could instead have a store for timebarLabels that we loop over to allow showing n number of relevant times and control through context let extraTime: number | null = null; diff --git a/src/lib/components/TimebarLabel.svelte b/src/lib/components/TimebarLabel.svelte index 0b46b31..958a448 100644 --- a/src/lib/components/TimebarLabel.svelte +++ b/src/lib/components/TimebarLabel.svelte @@ -2,9 +2,9 @@ import { uniqueClasses } from '../utils'; import { getSequenceContext } from './SequenceContext'; - const { duration, width } = getSequenceContext(); + const { duration, width, formatTimeFn } = getSequenceContext(); - export let formatFn = (value: number) => `${Math.round(value)}`; + export let formatFn = formatTimeFn; export let time: number; let pos: number; diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index f8e5d52..3a3b3f0 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -46,7 +46,7 @@ export type TSequenceBlockOptions = ISequenceCommonOptions & { outTime?: number; // Initial outTime as absolute milliseconds validations?: TValidationOptions; layers?: Array; - markers?: Array<{ time: number; label: string }>; + markers?: Array<{ time: number; title?: string }>; }; export interface ISequenceCommon { diff --git a/src/routes/examples/markers/+page.svelte b/src/routes/examples/markers/+page.svelte index e1176e2..e99a5a0 100644 --- a/src/routes/examples/markers/+page.svelte +++ b/src/routes/examples/markers/+page.svelte @@ -5,6 +5,10 @@ import CustomLayer from './CustomLayer.svelte'; import { Label, Input } from 'flowbite-svelte'; + import dayjs from 'dayjs'; + import dayjsDuration from 'dayjs/plugin/duration'; + dayjs.extend(dayjsDuration); + const PromoterBlockTemplate = { key: 'promoter', type: 'promoter', @@ -32,43 +36,40 @@ markers: [ { time: 1000, - label: 'scene 1' + title: 'scene 1' }, { time: 1050, - label: 'scene 2' + title: 'scene 2' }, { - time: 300, - label: 'scene 3' + time: 300 }, { - time: 4000, - label: 'scene 4' + time: 4000 }, { - time: 5000, - label: 'scene 5' + time: 5000 }, { time: 6000, - label: 'scene 6' + title: 'scene 6' }, { time: 7000, - label: 'scene 7' + title: 'scene 7' }, { time: 8000, - label: 'scene 8' + title: 'scene 8' }, { time: 9000, - label: 'scene 9' + title: 'scene 9' }, { time: 10000, - label: 'scene 10' + title: 'scene 10' } ] } @@ -181,6 +182,43 @@ /* TODO: toggle snapping */ + + const millisInSecond = 1000; + const millisInFrame = (framerate: number) => { + return (1 / framerate) * millisInSecond; + }; + + type formatTimeOptions = { + framerate?: number; + format?: string; + }; + const formatTimeFn = (time: number, options?: formatTimeOptions) => { + if (time === undefined || time === null) { + return ''; + } + + time = Math.floor(time); + + let format = options?.format ?? 'HH:mm:ss.SSS'; + const framerate = options?.framerate ?? 25; + + const duration = dayjs.duration(time, 'milliseconds'); + + if (format.includes('FF')) { + // calculate remaining frames after smallest unit in format string + + const millis = duration.milliseconds(); + const frames = Math.floor(millis / millisInFrame(framerate)); + + format = format.replace('FF', `${frames}`.padStart(2, '0')); + } + + if (format.includes('R')) { + format = format.replace('R', `${framerate}`); + } + + return `${duration.format(format)}`; + };
@@ -219,13 +257,10 @@ - `${value}`} - class="dark:bg-gray-900" - /> +