dialkit 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +653 -6
- package/dist/dropdown-position.d.ts +15 -0
- package/dist/dropdown-position.js +22 -0
- package/dist/dropdown-position.js.map +1 -0
- package/dist/icons.d.ts +4 -1
- package/dist/icons.js +13 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.cjs +3796 -768
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +315 -10
- package/dist/index.d.ts +315 -10
- package/dist/index.js +3733 -711
- package/dist/index.js.map +1 -1
- package/dist/panel-drag.d.ts +19 -0
- package/dist/panel-drag.js +72 -0
- package/dist/panel-drag.js.map +1 -0
- package/dist/solid/index.d.ts +264 -5
- package/dist/solid/index.js +5277 -1632
- package/dist/solid/index.js.map +1 -1
- package/dist/store/index.cjs +329 -57
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +43 -4
- package/dist/store/index.d.ts +43 -4
- package/dist/store/index.js +321 -56
- package/dist/store/index.js.map +1 -1
- package/dist/styles.css +809 -7
- package/dist/svelte/components/ControlRenderer.svelte +5 -2
- package/dist/svelte/components/ControlRenderer.svelte.d.ts +2 -0
- package/dist/svelte/components/ControlRenderer.svelte.d.ts.map +1 -1
- package/dist/svelte/components/DialRoot.svelte +218 -16
- package/dist/svelte/components/DialRoot.svelte.d.ts +1 -0
- package/dist/svelte/components/DialRoot.svelte.d.ts.map +1 -1
- package/dist/svelte/components/Folder.svelte +24 -13
- package/dist/svelte/components/Folder.svelte.d.ts +1 -0
- package/dist/svelte/components/Folder.svelte.d.ts.map +1 -1
- package/dist/svelte/components/Panel.svelte +104 -71
- package/dist/svelte/components/Panel.svelte.d.ts +4 -0
- package/dist/svelte/components/Panel.svelte.d.ts.map +1 -1
- package/dist/svelte/components/PresetManager.svelte +5 -5
- package/dist/svelte/components/PresetManager.svelte.d.ts.map +1 -1
- package/dist/svelte/components/SelectControl.svelte +6 -14
- package/dist/svelte/components/SelectControl.svelte.d.ts.map +1 -1
- package/dist/svelte/components/Timeline/ClipPopover.svelte +206 -0
- package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts +26 -0
- package/dist/svelte/components/Timeline/ClipPopover.svelte.d.ts.map +1 -0
- package/dist/svelte/components/Timeline/DialTimeline.svelte +76 -0
- package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts +13 -0
- package/dist/svelte/components/Timeline/DialTimeline.svelte.d.ts.map +1 -0
- package/dist/svelte/components/Timeline/TimelineClip.svelte +233 -0
- package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts +24 -0
- package/dist/svelte/components/Timeline/TimelineClip.svelte.d.ts.map +1 -0
- package/dist/svelte/components/Timeline/TimelineSection.svelte +756 -0
- package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts +12 -0
- package/dist/svelte/components/Timeline/TimelineSection.svelte.d.ts.map +1 -0
- package/dist/svelte/components/Timeline/TimelineToggleButton.svelte +25 -0
- package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts +4 -0
- package/dist/svelte/components/Timeline/TimelineToggleButton.svelte.d.ts.map +1 -0
- package/dist/svelte/components/TransitionControl.svelte +26 -11
- package/dist/svelte/components/TransitionControl.svelte.d.ts +9 -0
- package/dist/svelte/components/TransitionControl.svelte.d.ts.map +1 -1
- package/dist/svelte/createDialKit.svelte.d.ts +11 -1
- package/dist/svelte/createDialKit.svelte.d.ts.map +1 -1
- package/dist/svelte/createDialKit.svelte.js +61 -34
- package/dist/svelte/createDialTimeline.svelte.d.ts +4 -0
- package/dist/svelte/createDialTimeline.svelte.d.ts.map +1 -0
- package/dist/svelte/createDialTimeline.svelte.js +73 -0
- package/dist/svelte/index.d.ts +7 -3
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +4 -1
- package/dist/svelte/theme-css.d.ts +1 -1
- package/dist/svelte/theme-css.d.ts.map +1 -1
- package/dist/svelte/theme-css.js +809 -7
- package/dist/timeline/index.cjs +1288 -0
- package/dist/timeline/index.cjs.map +1 -0
- package/dist/timeline/index.d.cts +443 -0
- package/dist/timeline/index.d.ts +443 -0
- package/dist/timeline/index.js +1233 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/vue/index.d.ts +325 -12
- package/dist/vue/index.js +3226 -280
- package/dist/vue/index.js.map +1 -1
- package/package.json +23 -13
- package/dist/solid/index.cjs +0 -3079
- package/dist/solid/index.cjs.map +0 -1
- package/dist/solid/index.d.cts +0 -258
- package/dist/vue/index.cjs +0 -3056
- package/dist/vue/index.cjs.map +0 -1
- package/dist/vue/index.d.cts +0 -675
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { DialStore, formatLabel } from 'dialkit/store';
|
|
4
|
+
import type { DialValue } from 'dialkit/store';
|
|
5
|
+
import {
|
|
6
|
+
TimelineStore,
|
|
7
|
+
buildCopyInstruction,
|
|
8
|
+
clamp,
|
|
9
|
+
computeClipStaticFromValues,
|
|
10
|
+
formatClock,
|
|
11
|
+
normalizeTimelineValuesForCopy,
|
|
12
|
+
} from 'dialkit/timeline';
|
|
13
|
+
import type {
|
|
14
|
+
TimelineClipMeta,
|
|
15
|
+
TimelineClipStatic,
|
|
16
|
+
TimelineMeta,
|
|
17
|
+
TimelineTrackStatic,
|
|
18
|
+
TimelineTransport,
|
|
19
|
+
} from 'dialkit/timeline';
|
|
20
|
+
import {
|
|
21
|
+
ICON_ADD_PRESET,
|
|
22
|
+
ICON_CHEVRON,
|
|
23
|
+
ICON_CHECK,
|
|
24
|
+
ICON_CLIPBOARD,
|
|
25
|
+
ICON_PAUSE,
|
|
26
|
+
ICON_PLAY,
|
|
27
|
+
} from '../../../icons';
|
|
28
|
+
import { findControl } from '../../../shortcut-utils';
|
|
29
|
+
import PresetManager from '../PresetManager.svelte';
|
|
30
|
+
import type { DialTheme } from '../DialRoot.svelte';
|
|
31
|
+
import ClipPopover from './ClipPopover.svelte';
|
|
32
|
+
import type { PopoverState } from './ClipPopover.svelte';
|
|
33
|
+
import TimelineClip from './TimelineClip.svelte';
|
|
34
|
+
|
|
35
|
+
const DRAG_THRESHOLD_PX = 3;
|
|
36
|
+
const MAJOR_TICK_TARGET_PX = 140;
|
|
37
|
+
const MILLISECOND_STEP = 0.001;
|
|
38
|
+
const SECOND_TICK_STEPS = [
|
|
39
|
+
0.001, 0.002, 0.005,
|
|
40
|
+
0.01, 0.02, 0.05,
|
|
41
|
+
0.1, 0.2, 0.5,
|
|
42
|
+
1, 2, 5, 10, 15, 30, 60, 120, 300, 600,
|
|
43
|
+
];
|
|
44
|
+
const MIN_TIMELINE_MAX_ZOOM = 8;
|
|
45
|
+
const PLAYHEAD_FLAG_WIDTH = 52;
|
|
46
|
+
const ZOOM_DRAG_DISTANCE = 180;
|
|
47
|
+
|
|
48
|
+
type ZoomDragState = {
|
|
49
|
+
pointerX: number;
|
|
50
|
+
rect: DOMRect;
|
|
51
|
+
zoom: number;
|
|
52
|
+
viewStart: number;
|
|
53
|
+
anchorRatio: number;
|
|
54
|
+
anchorTime: number;
|
|
55
|
+
moved: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type ScrubState = {
|
|
59
|
+
wasPlaying: boolean;
|
|
60
|
+
rect: DOMRect;
|
|
61
|
+
viewStart: number;
|
|
62
|
+
visibleDuration: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type GroupRow = {
|
|
66
|
+
kind: 'group';
|
|
67
|
+
key: string;
|
|
68
|
+
group: string;
|
|
69
|
+
collapsed: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type ClipRow = {
|
|
73
|
+
kind: 'clip';
|
|
74
|
+
key: string;
|
|
75
|
+
clip: TimelineClipMeta;
|
|
76
|
+
stat: TimelineClipStatic;
|
|
77
|
+
tracksOpen: boolean;
|
|
78
|
+
selected: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type TrackRow = {
|
|
82
|
+
kind: 'track';
|
|
83
|
+
key: string;
|
|
84
|
+
parent: TimelineClipMeta;
|
|
85
|
+
clip: TimelineClipMeta;
|
|
86
|
+
stat: TimelineClipStatic;
|
|
87
|
+
track: TimelineTrackStatic;
|
|
88
|
+
selected: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
let { meta, defaultOpen, theme, dockVisible } = $props<{
|
|
92
|
+
meta: TimelineMeta;
|
|
93
|
+
defaultOpen: boolean;
|
|
94
|
+
theme: DialTheme;
|
|
95
|
+
dockVisible: boolean;
|
|
96
|
+
}>();
|
|
97
|
+
|
|
98
|
+
let open = $state(untrack(() => defaultOpen));
|
|
99
|
+
let copied = $state(false);
|
|
100
|
+
let popover = $state<PopoverState | null>(null);
|
|
101
|
+
let collapsedGroups = $state(new Set<string>());
|
|
102
|
+
let expandedTracks = $state(new Set<string>());
|
|
103
|
+
let zoom = $state(1);
|
|
104
|
+
let viewStart = $state(0);
|
|
105
|
+
let values = $state<Record<string, DialValue>>(DialStore.getValues(untrack(() => meta.id)));
|
|
106
|
+
let transport = $state<TimelineTransport>(TimelineStore.getTransport(untrack(() => meta.id)));
|
|
107
|
+
let laneArea = $state<HTMLDivElement>();
|
|
108
|
+
let titleElement = $state<HTMLSpanElement>();
|
|
109
|
+
let actionsElement = $state<HTMLDivElement>();
|
|
110
|
+
let laneWidth = $state(0);
|
|
111
|
+
let flagClearRange = $state({ start: 0, end: 0 });
|
|
112
|
+
|
|
113
|
+
let zoomDrag: ZoomDragState | null = null;
|
|
114
|
+
let rulerScrub: ScrubState | null = null;
|
|
115
|
+
let trackScrub: ScrubState | null = null;
|
|
116
|
+
let overviewScrub: { wasPlaying: boolean; rect: DOMRect } | null = null;
|
|
117
|
+
let playheadScrub: ScrubState | null = null;
|
|
118
|
+
|
|
119
|
+
const presets = $derived.by(() => {
|
|
120
|
+
values;
|
|
121
|
+
return DialStore.getPresets(meta.id);
|
|
122
|
+
});
|
|
123
|
+
const activePresetId = $derived.by(() => {
|
|
124
|
+
values;
|
|
125
|
+
return DialStore.getActivePresetId(meta.id);
|
|
126
|
+
});
|
|
127
|
+
const visibleDuration = $derived(meta.duration > 0 ? meta.duration / zoom : meta.duration);
|
|
128
|
+
const safeViewStart = $derived(clampViewStart(viewStart, meta.duration, visibleDuration));
|
|
129
|
+
const viewEnd = $derived(safeViewStart + visibleDuration);
|
|
130
|
+
const pxPerSecond = $derived(visibleDuration > 0 && laneWidth > 0 ? laneWidth / visibleDuration : 0);
|
|
131
|
+
const maxZoom = $derived(Math.max(
|
|
132
|
+
MIN_TIMELINE_MAX_ZOOM,
|
|
133
|
+
laneWidth > 0 && meta.duration > 0
|
|
134
|
+
? (MAJOR_TICK_TARGET_PX * meta.duration) / (MILLISECOND_STEP * 10 * laneWidth)
|
|
135
|
+
: MIN_TIMELINE_MAX_ZOOM
|
|
136
|
+
));
|
|
137
|
+
const playheadX = $derived(clamp((transport.time - safeViewStart) * pxPerSecond, 0, laneWidth));
|
|
138
|
+
const playheadVisible = $derived(
|
|
139
|
+
transport.time >= safeViewStart && transport.time <= viewEnd && laneWidth > 0
|
|
140
|
+
);
|
|
141
|
+
const playheadPlacement = $derived.by(() => {
|
|
142
|
+
const left = playheadX - PLAYHEAD_FLAG_WIDTH / 2;
|
|
143
|
+
return left >= flagClearRange.start && left + PLAYHEAD_FLAG_WIDTH <= flagClearRange.end
|
|
144
|
+
? 'raised'
|
|
145
|
+
: 'lowered';
|
|
146
|
+
});
|
|
147
|
+
const overviewViewportWidth = $derived(
|
|
148
|
+
meta.duration > 0 ? ((viewEnd - safeViewStart) / meta.duration) * 100 : 100
|
|
149
|
+
);
|
|
150
|
+
const overviewPlayheadPercent = $derived(
|
|
151
|
+
meta.duration > 0 ? (transport.time / meta.duration) * 100 : 0
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const ticks = $derived.by(() => {
|
|
155
|
+
const rawStep = pxPerSecond > 0 ? MAJOR_TICK_TARGET_PX / pxPerSecond : 1;
|
|
156
|
+
const adaptive = SECOND_TICK_STEPS.find((step) => step >= rawStep)
|
|
157
|
+
?? SECOND_TICK_STEPS[SECOND_TICK_STEPS.length - 1];
|
|
158
|
+
const majorStep = zoom < 1.5 && meta.duration >= 1 ? Math.max(1, adaptive) : adaptive;
|
|
159
|
+
const fineStep = majorStep / 10;
|
|
160
|
+
const major: number[] = [];
|
|
161
|
+
const medium: number[] = [];
|
|
162
|
+
const fine: number[] = [];
|
|
163
|
+
const firstMajor = Math.ceil((safeViewStart - 1e-6) / majorStep) * majorStep;
|
|
164
|
+
for (let time = firstMajor; time <= viewEnd + 1e-6; time += majorStep) {
|
|
165
|
+
major.push(Number(time.toFixed(4)));
|
|
166
|
+
}
|
|
167
|
+
const firstFine = Math.ceil((safeViewStart - 1e-6) / fineStep);
|
|
168
|
+
const lastFine = Math.floor((viewEnd + 1e-6) / fineStep);
|
|
169
|
+
for (let index = firstFine; index <= lastFine; index++) {
|
|
170
|
+
if (index % 10 === 0) continue;
|
|
171
|
+
const tick = Number((index * fineStep).toFixed(6));
|
|
172
|
+
if (index % 5 === 0) medium.push(tick);
|
|
173
|
+
else fine.push(tick);
|
|
174
|
+
}
|
|
175
|
+
return { major, medium, fine, majorStep };
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const rows = $derived.by(() => {
|
|
179
|
+
const result: Array<GroupRow | ClipRow | TrackRow> = [];
|
|
180
|
+
let lastGroup: string | undefined;
|
|
181
|
+
for (const clip of meta.clips) {
|
|
182
|
+
if (clip.group !== lastGroup) {
|
|
183
|
+
lastGroup = clip.group;
|
|
184
|
+
if (clip.group) {
|
|
185
|
+
result.push({
|
|
186
|
+
kind: 'group',
|
|
187
|
+
key: `group:${clip.group}`,
|
|
188
|
+
group: clip.group,
|
|
189
|
+
collapsed: collapsedGroups.has(clip.group),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (clip.group && collapsedGroups.has(clip.group)) continue;
|
|
194
|
+
const tracksOpen = Boolean(clip.tracks?.length) && expandedTracks.has(clip.key);
|
|
195
|
+
const stat = computeClipStaticFromValues(values, clip, meta.duration);
|
|
196
|
+
result.push({
|
|
197
|
+
kind: 'clip',
|
|
198
|
+
key: clip.key,
|
|
199
|
+
clip,
|
|
200
|
+
stat,
|
|
201
|
+
tracksOpen,
|
|
202
|
+
selected: popover?.clip.key === clip.key,
|
|
203
|
+
});
|
|
204
|
+
if (!tracksOpen) continue;
|
|
205
|
+
for (const trackRef of clip.tracks ?? []) {
|
|
206
|
+
const track = stat.tracks.find((candidate) => candidate.prop === trackRef.prop);
|
|
207
|
+
if (!track) continue;
|
|
208
|
+
const key = `${clip.key}.${trackRef.prop}`;
|
|
209
|
+
result.push({
|
|
210
|
+
kind: 'track',
|
|
211
|
+
key,
|
|
212
|
+
parent: clip,
|
|
213
|
+
clip: {
|
|
214
|
+
key,
|
|
215
|
+
label: `${clip.label} · ${formatLabel(trackRef.prop)}`,
|
|
216
|
+
color: clip.color,
|
|
217
|
+
loop: clip.loop,
|
|
218
|
+
group: clip.group,
|
|
219
|
+
stepKeys: trackRef.stepKeys,
|
|
220
|
+
},
|
|
221
|
+
stat,
|
|
222
|
+
track,
|
|
223
|
+
selected: popover?.clip.key === key,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
$effect(() => {
|
|
231
|
+
values = DialStore.getValues(meta.id);
|
|
232
|
+
transport = TimelineStore.getTransport(meta.id);
|
|
233
|
+
const unsubscribeValues = DialStore.subscribe(meta.id, () => {
|
|
234
|
+
values = DialStore.getValues(meta.id);
|
|
235
|
+
});
|
|
236
|
+
const unsubscribeTransport = TimelineStore.subscribe(meta.id, () => {
|
|
237
|
+
transport = TimelineStore.getTransport(meta.id);
|
|
238
|
+
});
|
|
239
|
+
return () => {
|
|
240
|
+
unsubscribeValues();
|
|
241
|
+
unsubscribeTransport();
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
$effect(() => {
|
|
246
|
+
if (!open || !laneArea || !titleElement || !actionsElement) return;
|
|
247
|
+
const measure = () => {
|
|
248
|
+
if (!laneArea || !titleElement || !actionsElement) return;
|
|
249
|
+
const rulerRect = laneArea.getBoundingClientRect();
|
|
250
|
+
const titleRect = titleElement.getBoundingClientRect();
|
|
251
|
+
const actionsRect = actionsElement.getBoundingClientRect();
|
|
252
|
+
laneWidth = rulerRect.width;
|
|
253
|
+
flagClearRange = {
|
|
254
|
+
start: Math.round(titleRect.right + 10 - rulerRect.left),
|
|
255
|
+
end: Math.round(actionsRect.left - 10 - rulerRect.left),
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
measure();
|
|
259
|
+
const observer = new ResizeObserver(measure);
|
|
260
|
+
observer.observe(laneArea);
|
|
261
|
+
observer.observe(titleElement);
|
|
262
|
+
observer.observe(actionsElement);
|
|
263
|
+
return () => observer.disconnect();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
$effect(() => {
|
|
267
|
+
const nextZoom = clamp(zoom, 1, maxZoom);
|
|
268
|
+
if (nextZoom !== zoom) zoom = nextZoom;
|
|
269
|
+
const nextStart = clampViewStart(viewStart, meta.duration, meta.duration / zoom);
|
|
270
|
+
if (nextStart !== viewStart) viewStart = nextStart;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
$effect(() => {
|
|
274
|
+
if (!dockVisible) popover = null;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
function centerViewAt(time: number) {
|
|
278
|
+
if (zoom <= 1 || meta.duration <= 0) return;
|
|
279
|
+
const duration = meta.duration / zoom;
|
|
280
|
+
viewStart = clampViewStart(time - duration / 2, meta.duration, duration);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resetView() {
|
|
284
|
+
zoom = 1;
|
|
285
|
+
viewStart = 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function seek(scrub: ScrubState | null, clientX: number) {
|
|
289
|
+
if (!scrub || scrub.rect.width <= 0) return;
|
|
290
|
+
TimelineStore.seek(meta.id, clamp(
|
|
291
|
+
scrub.viewStart + ((clientX - scrub.rect.left) / scrub.rect.width) * scrub.visibleDuration,
|
|
292
|
+
scrub.viewStart,
|
|
293
|
+
scrub.viewStart + scrub.visibleDuration
|
|
294
|
+
));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function handleRulerPointerDown(event: PointerEvent) {
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
event.stopPropagation();
|
|
300
|
+
const target = event.currentTarget as HTMLDivElement;
|
|
301
|
+
const rect = target.getBoundingClientRect();
|
|
302
|
+
if (rect.width <= 0) return;
|
|
303
|
+
target.setPointerCapture(event.pointerId);
|
|
304
|
+
if (!event.altKey) {
|
|
305
|
+
const reset = event.shiftKey;
|
|
306
|
+
rulerScrub = {
|
|
307
|
+
wasPlaying: TimelineStore.getTransport(meta.id).playing,
|
|
308
|
+
rect,
|
|
309
|
+
viewStart: reset ? 0 : safeViewStart,
|
|
310
|
+
visibleDuration: reset ? meta.duration : visibleDuration,
|
|
311
|
+
};
|
|
312
|
+
if (reset) resetView();
|
|
313
|
+
TimelineStore.pause(meta.id);
|
|
314
|
+
seek(rulerScrub, event.clientX);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const ratio = clamp((event.clientX - rect.left) / rect.width, 0, 1);
|
|
318
|
+
zoomDrag = {
|
|
319
|
+
pointerX: event.clientX,
|
|
320
|
+
rect,
|
|
321
|
+
zoom,
|
|
322
|
+
viewStart: safeViewStart,
|
|
323
|
+
anchorRatio: ratio,
|
|
324
|
+
anchorTime: safeViewStart + ratio * visibleDuration,
|
|
325
|
+
moved: false,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function handleRulerPointerMove(event: PointerEvent) {
|
|
330
|
+
if (rulerScrub) {
|
|
331
|
+
seek(rulerScrub, event.clientX);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (!zoomDrag || meta.duration <= 0) return;
|
|
335
|
+
const dx = event.clientX - zoomDrag.pointerX;
|
|
336
|
+
if (!zoomDrag.moved && Math.abs(dx) <= DRAG_THRESHOLD_PX) return;
|
|
337
|
+
zoomDrag.moved = true;
|
|
338
|
+
const nextZoom = clamp(zoomDrag.zoom * Math.exp(dx / ZOOM_DRAG_DISTANCE), 1, maxZoom);
|
|
339
|
+
const nextDuration = meta.duration / nextZoom;
|
|
340
|
+
zoom = nextZoom;
|
|
341
|
+
viewStart = clampViewStart(
|
|
342
|
+
zoomDrag.anchorTime - zoomDrag.anchorRatio * nextDuration,
|
|
343
|
+
meta.duration,
|
|
344
|
+
nextDuration
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function finishRuler() {
|
|
349
|
+
if (rulerScrub?.wasPlaying) TimelineStore.play(meta.id);
|
|
350
|
+
rulerScrub = null;
|
|
351
|
+
zoomDrag = null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function handleTrackPointerDown(event: PointerEvent) {
|
|
355
|
+
const target = event.target as HTMLElement;
|
|
356
|
+
if (target.closest('.dialkit-timeline-label, button')) return;
|
|
357
|
+
if (!event.shiftKey && target.closest('.dialkit-timeline-clip')) return;
|
|
358
|
+
const rect = laneArea?.getBoundingClientRect();
|
|
359
|
+
if (!rect) return;
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
|
362
|
+
const reset = event.shiftKey;
|
|
363
|
+
trackScrub = {
|
|
364
|
+
wasPlaying: TimelineStore.getTransport(meta.id).playing,
|
|
365
|
+
rect,
|
|
366
|
+
viewStart: reset ? 0 : safeViewStart,
|
|
367
|
+
visibleDuration: reset ? meta.duration : visibleDuration,
|
|
368
|
+
};
|
|
369
|
+
if (reset) resetView();
|
|
370
|
+
popover = null;
|
|
371
|
+
TimelineStore.pause(meta.id);
|
|
372
|
+
seek(trackScrub, event.clientX);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function finishTrack() {
|
|
376
|
+
if (trackScrub?.wasPlaying) TimelineStore.play(meta.id);
|
|
377
|
+
trackScrub = null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function handleOverviewPointerDown(event: PointerEvent) {
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
const target = event.currentTarget as HTMLElement;
|
|
383
|
+
target.setPointerCapture(event.pointerId);
|
|
384
|
+
overviewScrub = {
|
|
385
|
+
wasPlaying: TimelineStore.getTransport(meta.id).playing,
|
|
386
|
+
rect: target.getBoundingClientRect(),
|
|
387
|
+
};
|
|
388
|
+
TimelineStore.pause(meta.id);
|
|
389
|
+
seekOverview(event.clientX);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function seekOverview(clientX: number) {
|
|
393
|
+
if (!overviewScrub || overviewScrub.rect.width <= 0 || meta.duration <= 0) return;
|
|
394
|
+
const next = clamp(
|
|
395
|
+
((clientX - overviewScrub.rect.left) / overviewScrub.rect.width) * meta.duration,
|
|
396
|
+
0,
|
|
397
|
+
meta.duration
|
|
398
|
+
);
|
|
399
|
+
TimelineStore.seek(meta.id, next);
|
|
400
|
+
centerViewAt(next);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function finishOverview() {
|
|
404
|
+
if (overviewScrub?.wasPlaying) TimelineStore.play(meta.id);
|
|
405
|
+
overviewScrub = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function handlePlayheadPointerDown(event: PointerEvent) {
|
|
409
|
+
const rect = laneArea?.getBoundingClientRect();
|
|
410
|
+
if (!rect) return;
|
|
411
|
+
event.preventDefault();
|
|
412
|
+
event.stopPropagation();
|
|
413
|
+
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
|
414
|
+
const reset = event.shiftKey;
|
|
415
|
+
playheadScrub = {
|
|
416
|
+
wasPlaying: TimelineStore.getTransport(meta.id).playing,
|
|
417
|
+
rect,
|
|
418
|
+
viewStart: reset ? 0 : safeViewStart,
|
|
419
|
+
visibleDuration: reset ? meta.duration : visibleDuration,
|
|
420
|
+
};
|
|
421
|
+
if (reset) resetView();
|
|
422
|
+
TimelineStore.pause(meta.id);
|
|
423
|
+
seek(playheadScrub, event.clientX);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function finishPlayhead() {
|
|
427
|
+
if (playheadScrub?.wasPlaying) TimelineStore.play(meta.id);
|
|
428
|
+
playheadScrub = null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function handleCopy() {
|
|
432
|
+
const normalized = normalizeTimelineValuesForCopy(DialStore.getValues(meta.id), meta.clips);
|
|
433
|
+
void navigator.clipboard.writeText(buildCopyInstruction('createDialTimeline', meta.name, normalized));
|
|
434
|
+
copied = true;
|
|
435
|
+
window.setTimeout(() => { copied = false; }, 1500);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function handleAddPreset() {
|
|
439
|
+
DialStore.savePreset(meta.id, `Version ${presets.length + 2}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function toggleSet(current: Set<string>, key: string): Set<string> {
|
|
443
|
+
const next = new Set(current);
|
|
444
|
+
if (next.has(key)) next.delete(key);
|
|
445
|
+
else next.add(key);
|
|
446
|
+
return next;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function toggleTracks(key: string) {
|
|
450
|
+
expandedTracks = toggleSet(expandedTracks, key);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function toggleGroup(key: string) {
|
|
454
|
+
collapsedGroups = toggleSet(collapsedGroups, key);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function clipPopoverExclusions(clip: TimelineClipMeta): Set<string> {
|
|
458
|
+
return new Set([...(clip.stepKeys ?? []), ...(clip.tracks?.map((track) => track.prop) ?? [])]);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function getClipControls(path: string, exclusions?: Set<string>) {
|
|
462
|
+
const panel = DialStore.getPanel(meta.id);
|
|
463
|
+
const folder = panel ? findControl(panel.controls, path) : null;
|
|
464
|
+
if (!folder?.children) return [];
|
|
465
|
+
return folder.children.filter((control) => {
|
|
466
|
+
const key = control.path.slice(path.length + 1);
|
|
467
|
+
return key !== 'at' && key !== 'duration' && !exclusions?.has(key);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function openClipPopover(clip: TimelineClipMeta, rect: DOMRect, stepKey?: string) {
|
|
472
|
+
const targetPath = stepKey ? `${clip.key}.${stepKey}` : clip.key;
|
|
473
|
+
if (getClipControls(targetPath, stepKey ? undefined : clipPopoverExclusions(clip)).length === 0) return;
|
|
474
|
+
popover = popover?.clip.key === clip.key && popover.stepKey === stepKey
|
|
475
|
+
? null
|
|
476
|
+
: {
|
|
477
|
+
clip,
|
|
478
|
+
stepKey,
|
|
479
|
+
anchor: {
|
|
480
|
+
left: rect.left,
|
|
481
|
+
top: rect.top,
|
|
482
|
+
right: rect.right,
|
|
483
|
+
bottom: rect.bottom,
|
|
484
|
+
width: rect.width,
|
|
485
|
+
height: rect.height,
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function handleBarClick(clip: TimelineClipMeta, rect: DOMRect, stepKey?: string) {
|
|
491
|
+
if (!stepKey && clip.tracks?.length) toggleTracks(clip.key);
|
|
492
|
+
else openClipPopover(clip, rect, stepKey);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function formatRulerSeconds(time: number, step: number): string {
|
|
496
|
+
if (step >= 1 && Number.isInteger(time)) return formatClock(time);
|
|
497
|
+
const decimals = Math.min(3, Math.max(1, Math.ceil(-Math.log10(step))));
|
|
498
|
+
return `${time.toFixed(decimals)}s`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function clampViewStart(start: number, duration: number, shownDuration: number): number {
|
|
502
|
+
return clamp(start, 0, Math.max(0, duration - shownDuration));
|
|
503
|
+
}
|
|
504
|
+
</script>
|
|
505
|
+
|
|
506
|
+
<div class="dialkit-timeline-section">
|
|
507
|
+
<div class="dialkit-timeline-header" data-open={open || undefined}>
|
|
508
|
+
<div class="dialkit-timeline-identity">
|
|
509
|
+
<span bind:this={titleElement} class="dialkit-timeline-title">{meta.name}</span>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
{#if !open}
|
|
513
|
+
<div
|
|
514
|
+
class="dialkit-timeline-overview"
|
|
515
|
+
onpointerdown={handleOverviewPointerDown}
|
|
516
|
+
onpointermove={(event) => overviewScrub && seekOverview(event.clientX)}
|
|
517
|
+
onpointerup={finishOverview}
|
|
518
|
+
onpointercancel={finishOverview}
|
|
519
|
+
onlostpointercapture={finishOverview}
|
|
520
|
+
title="Drag to scrub the full timeline"
|
|
521
|
+
role="slider"
|
|
522
|
+
tabindex="0"
|
|
523
|
+
aria-label="Timeline overview"
|
|
524
|
+
aria-valuemin={0}
|
|
525
|
+
aria-valuemax={meta.duration}
|
|
526
|
+
aria-valuenow={transport.time}
|
|
527
|
+
>
|
|
528
|
+
<div
|
|
529
|
+
class="dialkit-timeline-overview-viewport"
|
|
530
|
+
data-zoomed={overviewViewportWidth < 99.999 || undefined}
|
|
531
|
+
style:left={`${meta.duration > 0 ? (safeViewStart / meta.duration) * 100 : 0}%`}
|
|
532
|
+
style:width={`${overviewViewportWidth}%`}
|
|
533
|
+
></div>
|
|
534
|
+
<div class="dialkit-timeline-overview-progress" style:width={`${overviewPlayheadPercent}%`}></div>
|
|
535
|
+
<div class="dialkit-timeline-overview-playhead" style:left={`${overviewPlayheadPercent}%`}></div>
|
|
536
|
+
</div>
|
|
537
|
+
{/if}
|
|
538
|
+
|
|
539
|
+
<div bind:this={actionsElement} class="dialkit-timeline-actions">
|
|
540
|
+
<button
|
|
541
|
+
class="dialkit-toolbar-add"
|
|
542
|
+
onclick={() => transport.playing ? TimelineStore.pause(meta.id) : TimelineStore.play(meta.id)}
|
|
543
|
+
title={transport.playing ? 'Pause' : 'Play'}
|
|
544
|
+
aria-label={transport.playing ? 'Pause' : 'Play'}
|
|
545
|
+
>
|
|
546
|
+
<span style="position:relative;width:16px;height:16px;">
|
|
547
|
+
{#if transport.playing}
|
|
548
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" style="position:absolute;inset:0;width:16px;height:16px;color:var(--dial-text-label);">
|
|
549
|
+
{#each ICON_PAUSE as path}<path d={path} fill="currentColor" />{/each}
|
|
550
|
+
</svg>
|
|
551
|
+
{:else}
|
|
552
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" style="position:absolute;inset:0;width:16px;height:16px;color:var(--dial-text-label);">
|
|
553
|
+
<path d={ICON_PLAY} fill="currentColor" />
|
|
554
|
+
</svg>
|
|
555
|
+
{/if}
|
|
556
|
+
</span>
|
|
557
|
+
</button>
|
|
558
|
+
<button class="dialkit-toolbar-add" onclick={handleAddPreset} title="Add timeline version" aria-label="Add timeline version">
|
|
559
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
560
|
+
{#each ICON_ADD_PRESET as path}<path d={path} />{/each}
|
|
561
|
+
</svg>
|
|
562
|
+
</button>
|
|
563
|
+
<PresetManager panelId={meta.id} {presets} {activePresetId} />
|
|
564
|
+
<button
|
|
565
|
+
class="dialkit-toolbar-add"
|
|
566
|
+
onclick={handleCopy}
|
|
567
|
+
title="Copy parameters"
|
|
568
|
+
aria-label={copied ? 'Copied parameters' : 'Copy parameters'}
|
|
569
|
+
>
|
|
570
|
+
<span style="position:relative;width:16px;height:16px;">
|
|
571
|
+
{#if copied}
|
|
572
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="position:absolute;inset:0;width:16px;height:16px;color:var(--dial-text-label);">
|
|
573
|
+
<path d={ICON_CHECK} />
|
|
574
|
+
</svg>
|
|
575
|
+
{:else}
|
|
576
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" style="position:absolute;inset:0;width:16px;height:16px;color:var(--dial-text-label);">
|
|
577
|
+
<path d={ICON_CLIPBOARD.board} stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
|
|
578
|
+
<path d={ICON_CLIPBOARD.sparkle} fill="currentColor" />
|
|
579
|
+
<path d={ICON_CLIPBOARD.body} stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
580
|
+
</svg>
|
|
581
|
+
{/if}
|
|
582
|
+
</span>
|
|
583
|
+
</button>
|
|
584
|
+
<button
|
|
585
|
+
class="dialkit-timeline-chevron"
|
|
586
|
+
data-open={open}
|
|
587
|
+
aria-expanded={open}
|
|
588
|
+
onclick={() => { open = !open; }}
|
|
589
|
+
title={open ? 'Collapse timeline' : 'Expand timeline'}
|
|
590
|
+
>
|
|
591
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
592
|
+
<path d={ICON_CHEVRON} />
|
|
593
|
+
</svg>
|
|
594
|
+
</button>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
{#if open}
|
|
599
|
+
<div
|
|
600
|
+
class="dialkit-timeline-body"
|
|
601
|
+
onpointerdown={handleTrackPointerDown}
|
|
602
|
+
onpointermove={(event) => trackScrub && seek(trackScrub, event.clientX)}
|
|
603
|
+
onpointerup={finishTrack}
|
|
604
|
+
onpointercancel={finishTrack}
|
|
605
|
+
onlostpointercapture={finishTrack}
|
|
606
|
+
role="presentation"
|
|
607
|
+
>
|
|
608
|
+
<div class="dialkit-timeline-grid">
|
|
609
|
+
<div class="dialkit-timeline-row dialkit-timeline-ruler-row">
|
|
610
|
+
<div class="dialkit-timeline-label"></div>
|
|
611
|
+
<div
|
|
612
|
+
bind:this={laneArea}
|
|
613
|
+
class="dialkit-timeline-ruler"
|
|
614
|
+
onpointerdown={handleRulerPointerDown}
|
|
615
|
+
onpointermove={handleRulerPointerMove}
|
|
616
|
+
onpointerup={finishRuler}
|
|
617
|
+
onpointercancel={finishRuler}
|
|
618
|
+
onlostpointercapture={finishRuler}
|
|
619
|
+
title="Drag to seek · Option-drag to zoom · Shift-drag to reset zoom"
|
|
620
|
+
role="slider"
|
|
621
|
+
tabindex="0"
|
|
622
|
+
aria-label="Timeline ruler"
|
|
623
|
+
aria-valuemin={safeViewStart}
|
|
624
|
+
aria-valuemax={viewEnd}
|
|
625
|
+
aria-valuenow={transport.time}
|
|
626
|
+
>
|
|
627
|
+
{#each ticks.fine as time}
|
|
628
|
+
<div class="dialkit-timeline-tick dialkit-timeline-tick-fine" style:left={`${(time - safeViewStart) * pxPerSecond}px`}></div>
|
|
629
|
+
{/each}
|
|
630
|
+
{#each ticks.medium as time}
|
|
631
|
+
<div class="dialkit-timeline-tick dialkit-timeline-tick-medium" style:left={`${(time - safeViewStart) * pxPerSecond}px`}></div>
|
|
632
|
+
{/each}
|
|
633
|
+
{#each ticks.major as time}
|
|
634
|
+
<div class="dialkit-timeline-tick" style:left={`${(time - safeViewStart) * pxPerSecond}px`}>
|
|
635
|
+
<span class="dialkit-timeline-tick-label">{formatRulerSeconds(time, ticks.majorStep)}</span>
|
|
636
|
+
</div>
|
|
637
|
+
{/each}
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
|
|
641
|
+
{#each rows as row (row.key)}
|
|
642
|
+
{#if row.kind === 'group'}
|
|
643
|
+
<div class="dialkit-timeline-row dialkit-timeline-group-row">
|
|
644
|
+
<div class="dialkit-timeline-label">
|
|
645
|
+
<button
|
|
646
|
+
class="dialkit-timeline-group-toggle"
|
|
647
|
+
data-open={!row.collapsed}
|
|
648
|
+
onclick={() => toggleGroup(row.group)}
|
|
649
|
+
title={row.collapsed ? 'Expand layer' : 'Collapse layer'}
|
|
650
|
+
>
|
|
651
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
652
|
+
<path d={ICON_CHEVRON} />
|
|
653
|
+
</svg>
|
|
654
|
+
</button>
|
|
655
|
+
<span>{formatLabel(row.group)}</span>
|
|
656
|
+
</div>
|
|
657
|
+
<div class="dialkit-timeline-lane"></div>
|
|
658
|
+
</div>
|
|
659
|
+
{:else if row.kind === 'clip'}
|
|
660
|
+
<div class="dialkit-timeline-row" data-grouped={row.clip.group ? '' : undefined}>
|
|
661
|
+
<div class="dialkit-timeline-label">
|
|
662
|
+
{#if row.clip.tracks?.length}
|
|
663
|
+
<button
|
|
664
|
+
class="dialkit-timeline-group-toggle"
|
|
665
|
+
data-open={row.tracksOpen}
|
|
666
|
+
onclick={(event) => {
|
|
667
|
+
event.stopPropagation();
|
|
668
|
+
toggleTracks(row.clip.key);
|
|
669
|
+
}}
|
|
670
|
+
title={row.tracksOpen ? 'Collapse properties' : 'Expand properties'}
|
|
671
|
+
>
|
|
672
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
673
|
+
<path d={ICON_CHEVRON} />
|
|
674
|
+
</svg>
|
|
675
|
+
</button>
|
|
676
|
+
{/if}
|
|
677
|
+
{row.clip.label}
|
|
678
|
+
</div>
|
|
679
|
+
<div class="dialkit-timeline-lane">
|
|
680
|
+
<TimelineClip
|
|
681
|
+
timelineId={meta.id}
|
|
682
|
+
clip={row.clip}
|
|
683
|
+
at={row.stat.at}
|
|
684
|
+
duration={row.stat.duration}
|
|
685
|
+
loop={row.stat.loop}
|
|
686
|
+
steps={row.clip.stepKeys?.length ? row.stat.tracks[0]?.steps : undefined}
|
|
687
|
+
fixedDuration={row.clip.tracks?.length ? true : row.stat.isPhysics}
|
|
688
|
+
composite={Boolean(row.clip.tracks?.length)}
|
|
689
|
+
{pxPerSecond}
|
|
690
|
+
viewStart={safeViewStart}
|
|
691
|
+
timelineDuration={meta.duration}
|
|
692
|
+
selected={row.selected}
|
|
693
|
+
selectedStepKey={row.selected ? popover?.stepKey : undefined}
|
|
694
|
+
onClick={handleBarClick}
|
|
695
|
+
onDrag={() => { popover = null; }}
|
|
696
|
+
/>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
{:else}
|
|
700
|
+
<div class="dialkit-timeline-row dialkit-timeline-track-row" data-grouped={row.parent.group ? '' : undefined}>
|
|
701
|
+
<div class="dialkit-timeline-label">{formatLabel(row.track.prop ?? '')}</div>
|
|
702
|
+
<div class="dialkit-timeline-lane">
|
|
703
|
+
<TimelineClip
|
|
704
|
+
timelineId={meta.id}
|
|
705
|
+
clip={row.clip}
|
|
706
|
+
at={row.stat.at + row.track.delay}
|
|
707
|
+
duration={row.track.duration}
|
|
708
|
+
loop={row.stat.loop}
|
|
709
|
+
steps={row.clip.stepKeys?.length ? row.track.steps : undefined}
|
|
710
|
+
fixedDuration={!row.clip.stepKeys?.length && row.track.steps[0]?.isPhysics === true}
|
|
711
|
+
baseAt={row.stat.at}
|
|
712
|
+
delayMode={true}
|
|
713
|
+
{pxPerSecond}
|
|
714
|
+
viewStart={safeViewStart}
|
|
715
|
+
timelineDuration={meta.duration}
|
|
716
|
+
selected={row.selected}
|
|
717
|
+
selectedStepKey={row.selected ? popover?.stepKey : undefined}
|
|
718
|
+
onClick={openClipPopover}
|
|
719
|
+
onDrag={() => { popover = null; }}
|
|
720
|
+
/>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
{/if}
|
|
724
|
+
{/each}
|
|
725
|
+
|
|
726
|
+
{#if pxPerSecond > 0 && playheadVisible}
|
|
727
|
+
<div
|
|
728
|
+
class="dialkit-timeline-playhead-control"
|
|
729
|
+
data-edge="center"
|
|
730
|
+
data-placement={playheadPlacement}
|
|
731
|
+
style:left={`calc(var(--dial-timeline-label-w) + ${playheadX}px)`}
|
|
732
|
+
onpointerdown={handlePlayheadPointerDown}
|
|
733
|
+
onpointermove={(event) => playheadScrub && seek(playheadScrub, event.clientX)}
|
|
734
|
+
onpointerup={finishPlayhead}
|
|
735
|
+
onpointercancel={finishPlayhead}
|
|
736
|
+
onlostpointercapture={finishPlayhead}
|
|
737
|
+
role="slider"
|
|
738
|
+
tabindex="0"
|
|
739
|
+
aria-label="Timeline current time"
|
|
740
|
+
aria-valuemin={0}
|
|
741
|
+
aria-valuemax={meta.duration}
|
|
742
|
+
aria-valuenow={transport.time}
|
|
743
|
+
title="Drag to scrub the timeline"
|
|
744
|
+
>
|
|
745
|
+
<div class="dialkit-timeline-playhead-flag">{transport.time.toFixed(2)}</div>
|
|
746
|
+
<div class="dialkit-timeline-playhead-stem"></div>
|
|
747
|
+
</div>
|
|
748
|
+
{/if}
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
{/if}
|
|
752
|
+
|
|
753
|
+
{#if popover}
|
|
754
|
+
<ClipPopover panelId={meta.id} {popover} {values} {theme} onClose={() => { popover = null; }} />
|
|
755
|
+
{/if}
|
|
756
|
+
</div>
|