@r2digisolutions/components 0.2.0 → 0.3.1
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 +10 -0
- package/dist/components/atoms/Playhead/Playhead.svelte +44 -5
- package/dist/components/atoms/Playhead/Playhead.svelte.d.ts +8 -0
- package/dist/components/atoms/TimeRuler/TimeRuler.svelte +75 -16
- package/dist/components/atoms/TimeRuler/TimeRuler.svelte.d.ts +7 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.stories.js +8 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte +39 -0
- package/dist/components/atoms/TimelineGrid/TimelineGrid.svelte.d.ts +11 -0
- package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte +7 -0
- package/dist/components/atoms/TimelineGrid/TimelineGridStory.svelte.d.ts +18 -0
- package/dist/components/molecules/ColorPicker/ColorPicker.svelte +45 -4
- package/dist/components/molecules/ColorPicker/ColorPicker.svelte.d.ts +2 -0
- package/dist/components/molecules/ContextMenu/ContextMenu.svelte +38 -11
- package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte +17 -2
- package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte.d.ts +5 -0
- package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte +362 -52
- package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte.d.ts +22 -2
- package/dist/components/molecules/PdfViewer/PdfViewer.svelte.d.ts +1 -1
- package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte +751 -29
- package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte.d.ts +14 -1
- package/dist/components/molecules/SplitPane/SplitPane.svelte +2 -2
- package/dist/components/molecules/TimelineClip/TimelineClip.svelte +95 -18
- package/dist/components/molecules/TimelineClip/TimelineClip.svelte.d.ts +3 -0
- package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte +82 -12
- package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte.d.ts +13 -2
- package/dist/components/molecules/TransportControls/TransportControls.svelte +84 -2
- package/dist/components/molecules/TransportControls/TransportControls.svelte.d.ts +13 -1
- package/dist/components/molecules/TransportControls/TransportControlsStory.svelte +33 -11
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte +213 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanel.svelte.d.ts +22 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoAnimationsPanel/VideoAnimationsPanelStory.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte +524 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanel.svelte.d.ts +32 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoComponentsPanel/VideoComponentsPanelStory.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte +85 -32
- package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte.d.ts +6 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.stories.js +8 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte +113 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanel.svelte.d.ts +18 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte +7 -0
- package/dist/components/molecules/VideoTransitionsPanel/VideoTransitionsPanelStory.svelte.d.ts +18 -0
- package/dist/components/organisms/AudioEditor/AudioEditor.svelte +52 -1
- package/dist/components/organisms/EditorShell/EditorShell.svelte +102 -92
- package/dist/components/organisms/FileUploader/FileUploader.stories.d.ts +10 -0
- package/dist/components/organisms/FileUploader/FileUploader.stories.js +21 -0
- package/dist/components/organisms/FileUploader/FileUploader.svelte +61 -21
- package/dist/components/organisms/FileUploader/FileUploader.svelte.d.ts +9 -0
- package/dist/components/organisms/FileUploader/FileUploaderStory.svelte +7 -1
- package/dist/components/organisms/FileUploader/FileUploaderStory.svelte.d.ts +3 -0
- package/dist/components/organisms/VideoEditor/VideoEditor.svelte +1457 -78
- package/dist/components/organisms/VideoEditor/VideoEditor.svelte.d.ts +2 -1
- package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte +3 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -2
- package/dist/styles.css +237 -0
- package/dist/utils/mediaTime.d.ts +14 -4
- package/dist/utils/mediaTime.js +27 -8
- package/dist/utils/mediaTracks.d.ts +154 -0
- package/dist/utils/mediaTracks.js +697 -1
- package/dist/utils/videoSequence.js +35 -4
- package/package.json +4 -2
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { MediaClip, MediaTrack } from '../../../utils/mediaTracks.js';
|
|
3
|
-
import {
|
|
2
|
+
import type { MediaClip, MediaClipRect, MediaTrack } from '../../../utils/mediaTracks.js';
|
|
3
|
+
import {
|
|
4
|
+
findClipAt,
|
|
5
|
+
findTransitionAt,
|
|
6
|
+
isClipActiveAt,
|
|
7
|
+
resolveClipRect,
|
|
8
|
+
resolvePreviewOpacity,
|
|
9
|
+
transitionDipOverlay,
|
|
10
|
+
transitionLayerStyle,
|
|
11
|
+
type TransitionLayerStyle
|
|
12
|
+
} from '../../../utils/mediaTracks.js';
|
|
13
|
+
import { formatTimecode } from '../../../utils/mediaTime.js';
|
|
14
|
+
import MarqueeRect from '../../atoms/MarqueeRect/MarqueeRect.svelte';
|
|
15
|
+
import {
|
|
16
|
+
attachMarqueeSelect,
|
|
17
|
+
collectMarqueeItems,
|
|
18
|
+
defaultMarqueeIgnore,
|
|
19
|
+
resolveMarqueeSelection,
|
|
20
|
+
type MarqueeRect as MarqueeBox
|
|
21
|
+
} from '../../../utils/marqueeSelect.js';
|
|
22
|
+
import { onDestroy } from 'svelte';
|
|
23
|
+
|
|
24
|
+
type ResizeEdge = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
|
4
25
|
|
|
5
26
|
interface ProgramMonitorProps {
|
|
6
27
|
width?: number;
|
|
7
28
|
height?: number;
|
|
8
29
|
background?: string;
|
|
9
30
|
currentTimeMs?: number;
|
|
31
|
+
playing?: boolean;
|
|
32
|
+
playbackRate?: number;
|
|
33
|
+
muted?: boolean;
|
|
34
|
+
volume?: number;
|
|
10
35
|
tracks?: MediaTrack[];
|
|
36
|
+
selectedClipIds?: string[];
|
|
37
|
+
/** When true, selected overlays can be dragged / resized on the frame */
|
|
38
|
+
editable?: boolean;
|
|
11
39
|
class?: string;
|
|
40
|
+
onselectclip?: (clipId: string, e: MouseEvent) => void;
|
|
41
|
+
onchangeclip?: (clip: MediaClip) => void;
|
|
42
|
+
/** Batch update (multi-drag). Falls back to repeated onchangeclip. */
|
|
43
|
+
onchangeclips?: (clips: MediaClip[]) => void;
|
|
44
|
+
onemptyclick?: () => void;
|
|
45
|
+
onselectionchange?: (clipIds: string[]) => void;
|
|
12
46
|
}
|
|
13
47
|
|
|
14
48
|
let {
|
|
@@ -16,57 +50,745 @@
|
|
|
16
50
|
height = 720,
|
|
17
51
|
background = '#0a0a0a',
|
|
18
52
|
currentTimeMs = 0,
|
|
53
|
+
playing = false,
|
|
54
|
+
playbackRate = 1,
|
|
55
|
+
muted = false,
|
|
56
|
+
volume = 1,
|
|
19
57
|
tracks = [],
|
|
20
|
-
|
|
58
|
+
selectedClipIds = [],
|
|
59
|
+
editable = true,
|
|
60
|
+
class: className = '',
|
|
61
|
+
onselectclip,
|
|
62
|
+
onchangeclip,
|
|
63
|
+
onchangeclips,
|
|
64
|
+
onemptyclick,
|
|
65
|
+
onselectionchange
|
|
21
66
|
}: ProgramMonitorProps = $props();
|
|
22
67
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
68
|
+
const frame = $derived({ width, height });
|
|
69
|
+
const selectedSet = $derived(new Set(selectedClipIds));
|
|
70
|
+
|
|
71
|
+
function isOverlayClip(clip: MediaClip): boolean {
|
|
72
|
+
if (clip.kind === 'text') return true;
|
|
73
|
+
if (clip.kind === 'image' && !clip.src) return true;
|
|
74
|
+
if (clip.rect && clip.kind !== 'video' && clip.kind !== 'audio') return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const activeVideo = $derived.by(() => {
|
|
79
|
+
const clip =
|
|
80
|
+
findClipAt(tracks, currentTimeMs, 'video') ?? findClipAt(tracks, currentTimeMs, 'image');
|
|
81
|
+
if (!clip || clip.kind === 'text' || isOverlayClip(clip)) return null;
|
|
82
|
+
return clip;
|
|
83
|
+
});
|
|
84
|
+
|
|
26
85
|
const activeAudio = $derived(findClipAt(tracks, currentTimeMs, 'audio'));
|
|
27
86
|
|
|
87
|
+
const transition = $derived(findTransitionAt(tracks, currentTimeMs));
|
|
88
|
+
|
|
89
|
+
function isProgramMedia(clip: MediaClip | null | undefined): clip is MediaClip {
|
|
90
|
+
return !!clip && clip.kind !== 'text' && clip.kind !== 'audio' && !isOverlayClip(clip);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Outgoing full-frame layer during a transition (kept even past the cut). */
|
|
94
|
+
const programOutgoing = $derived.by(() => {
|
|
95
|
+
if (transition) {
|
|
96
|
+
if (isProgramMedia(transition.from)) return transition.from;
|
|
97
|
+
// From is an overlay — don't also treat the incoming program clip as "outgoing"
|
|
98
|
+
if (activeVideo && activeVideo.id !== transition.to?.id) return activeVideo;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return activeVideo;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const programIncoming = $derived.by(() => {
|
|
105
|
+
if (!transition?.to || !isProgramMedia(transition.to)) return null;
|
|
106
|
+
return transition.to;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const overlays = $derived.by(() => {
|
|
110
|
+
const list: MediaClip[] = [];
|
|
111
|
+
const seen = new Set<string>();
|
|
112
|
+
for (const t of tracks) {
|
|
113
|
+
if (t.muted && !t.solo) continue;
|
|
114
|
+
for (const c of t.clips) {
|
|
115
|
+
if (!isOverlayClip(c)) continue;
|
|
116
|
+
const active = isClipActiveAt(c, currentTimeMs);
|
|
117
|
+
const inTransition =
|
|
118
|
+
!!transition &&
|
|
119
|
+
(c.id === transition.from.id || (!!transition.to && c.id === transition.to.id));
|
|
120
|
+
if (!active && !inTransition) continue;
|
|
121
|
+
if (seen.has(c.id)) continue;
|
|
122
|
+
seen.add(c.id);
|
|
123
|
+
list.push(c);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return list;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const dip = $derived(
|
|
130
|
+
transition ? transitionDipOverlay(transition.type, transition.progress) : null
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
let stageEl = $state<HTMLDivElement | null>(null);
|
|
134
|
+
let videoEl = $state<HTMLVideoElement | null>(null);
|
|
135
|
+
let incomingVideoEl = $state<HTMLVideoElement | null>(null);
|
|
136
|
+
let audioEl = $state<HTMLAudioElement | null>(null);
|
|
137
|
+
let marquee = $state<MarqueeBox | null>(null);
|
|
138
|
+
let disposeMarquee: (() => void) | null = null;
|
|
139
|
+
|
|
140
|
+
const SCRUB_DRIFT = 0.04;
|
|
141
|
+
const PLAY_DRIFT = 0.45;
|
|
142
|
+
|
|
143
|
+
let lastVideoClipId: string | null = null;
|
|
144
|
+
let lastIncomingClipId: string | null = null;
|
|
145
|
+
let lastAudioClipId: string | null = null;
|
|
146
|
+
let wasPlayingVideo = false;
|
|
147
|
+
let wasPlayingIncoming = false;
|
|
148
|
+
let wasPlayingAudio = false;
|
|
149
|
+
|
|
150
|
+
type DragMode = 'move' | 'resize';
|
|
151
|
+
let drag = $state<{
|
|
152
|
+
clipId: string;
|
|
153
|
+
mode: DragMode;
|
|
154
|
+
edge?: ResizeEdge;
|
|
155
|
+
startX: number;
|
|
156
|
+
startY: number;
|
|
157
|
+
origins: Record<string, MediaClipRect>;
|
|
158
|
+
} | null>(null);
|
|
159
|
+
|
|
160
|
+
$effect(() => {
|
|
161
|
+
disposeMarquee?.();
|
|
162
|
+
disposeMarquee = null;
|
|
163
|
+
const el = stageEl;
|
|
164
|
+
if (!el || !editable || playing || !onselectionchange) return;
|
|
165
|
+
disposeMarquee = attachMarqueeSelect(el, {
|
|
166
|
+
getItems: () => collectMarqueeItems(el),
|
|
167
|
+
onRect: (r) => (marquee = r),
|
|
168
|
+
shouldIgnore: (target) =>
|
|
169
|
+
defaultMarqueeIgnore(target) ||
|
|
170
|
+
(target instanceof Element && Boolean(target.closest('[data-marquee-id]'))),
|
|
171
|
+
onSelect: ({ ids, modifier }) => {
|
|
172
|
+
onselectionchange(resolveMarqueeSelection(ids, selectedClipIds, modifier));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
onDestroy(() => {
|
|
178
|
+
disposeMarquee?.();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const canvaHandles: { edge: ResizeEdge; class: string }[] = [
|
|
182
|
+
{ edge: 'n', class: 'left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 cursor-n-resize' },
|
|
183
|
+
{ edge: 's', class: 'left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2 cursor-s-resize' },
|
|
184
|
+
{ edge: 'e', class: 'top-1/2 right-0 translate-x-1/2 -translate-y-1/2 cursor-e-resize' },
|
|
185
|
+
{ edge: 'w', class: 'top-1/2 left-0 -translate-x-1/2 -translate-y-1/2 cursor-w-resize' },
|
|
186
|
+
{ edge: 'nw', class: 'left-0 top-0 -translate-x-1/2 -translate-y-1/2 cursor-nw-resize' },
|
|
187
|
+
{ edge: 'ne', class: 'right-0 top-0 translate-x-1/2 -translate-y-1/2 cursor-ne-resize' },
|
|
188
|
+
{ edge: 'sw', class: 'bottom-0 left-0 -translate-x-1/2 translate-y-1/2 cursor-sw-resize' },
|
|
189
|
+
{ edge: 'se', class: 'bottom-0 right-0 translate-x-1/2 translate-y-1/2 cursor-se-resize' }
|
|
190
|
+
];
|
|
191
|
+
|
|
28
192
|
function sourceTime(clip: MediaClip): number {
|
|
29
193
|
const trim = clip.trimInMs ?? 0;
|
|
194
|
+
if (currentTimeMs < clip.startMs) return trim / 1000;
|
|
30
195
|
return Math.max(0, currentTimeMs - clip.startMs + trim) / 1000;
|
|
31
196
|
}
|
|
197
|
+
|
|
198
|
+
function clipOpacity(clip: MediaClip): number {
|
|
199
|
+
const t = currentTimeMs;
|
|
200
|
+
const inAnim = clip.animationIn;
|
|
201
|
+
const outAnim = clip.animationOut;
|
|
202
|
+
let opacity = resolvePreviewOpacity(clip);
|
|
203
|
+
|
|
204
|
+
const fadeTypes = new Set([
|
|
205
|
+
'fade',
|
|
206
|
+
'pop',
|
|
207
|
+
'bounce',
|
|
208
|
+
'blur',
|
|
209
|
+
'glow',
|
|
210
|
+
'drop',
|
|
211
|
+
'swing',
|
|
212
|
+
'spin',
|
|
213
|
+
'flip-x',
|
|
214
|
+
'flip-y'
|
|
215
|
+
]);
|
|
216
|
+
if (inAnim && fadeTypes.has(inAnim.type) && inAnim.durationMs > 0) {
|
|
217
|
+
const local = t - clip.startMs;
|
|
218
|
+
if (local < inAnim.durationMs) {
|
|
219
|
+
const p = local / inAnim.durationMs;
|
|
220
|
+
const eased = inAnim.type === 'bounce' ? bounceEase(p) : p;
|
|
221
|
+
opacity = Math.min(opacity, eased);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (outAnim && fadeTypes.has(outAnim.type) && outAnim.durationMs > 0) {
|
|
225
|
+
const remaining = clip.endMs - t;
|
|
226
|
+
if (remaining < outAnim.durationMs) {
|
|
227
|
+
const p = remaining / outAnim.durationMs;
|
|
228
|
+
opacity = Math.min(opacity, p);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return Math.max(0, Math.min(1, opacity));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function bounceEase(p: number) {
|
|
235
|
+
return Math.min(1, 1 - Math.abs(Math.sin((1 - p) * Math.PI * 1.2)) * (1 - p));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function clipFilter(clip: MediaClip): string | undefined {
|
|
239
|
+
const t = currentTimeMs;
|
|
240
|
+
const parts: string[] = [];
|
|
241
|
+
|
|
242
|
+
function apply(type: string, durationMs: number, entering: boolean) {
|
|
243
|
+
const local = entering ? t - clip.startMs : clip.endMs - t;
|
|
244
|
+
if (local < 0 || local >= durationMs) return;
|
|
245
|
+
const p = entering ? 1 - local / durationMs : 1 - local / durationMs;
|
|
246
|
+
if (type === 'blur') parts.push(`blur(${p * 10}px)`);
|
|
247
|
+
if (type === 'glow') parts.push(`brightness(${1 + p * 0.8})`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (clip.animationIn?.durationMs) apply(clip.animationIn.type, clip.animationIn.durationMs, true);
|
|
251
|
+
if (clip.animationOut?.durationMs) apply(clip.animationOut.type, clip.animationOut.durationMs, false);
|
|
252
|
+
return parts.length ? parts.join(' ') : undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function clipTransform(clip: MediaClip): string {
|
|
256
|
+
const t = currentTimeMs;
|
|
257
|
+
const inAnim = clip.animationIn;
|
|
258
|
+
const outAnim = clip.animationOut;
|
|
259
|
+
const parts: string[] = [];
|
|
260
|
+
|
|
261
|
+
function applyIn(type: string, durationMs: number) {
|
|
262
|
+
const local = t - clip.startMs;
|
|
263
|
+
if (local < 0 || local >= durationMs) return;
|
|
264
|
+
const p = 1 - local / durationMs; // 1 → 0 as it settles
|
|
265
|
+
const reveal = 1 - p; // 0 → 1
|
|
266
|
+
if (type === 'slide' || type === 'slide-up') parts.push(`translateY(${p * 48}px)`);
|
|
267
|
+
else if (type === 'slide-down') parts.push(`translateY(${-p * 48}px)`);
|
|
268
|
+
else if (type === 'slide-left') parts.push(`translateX(${p * 64}px)`);
|
|
269
|
+
else if (type === 'slide-right') parts.push(`translateX(${-p * 64}px)`);
|
|
270
|
+
else if (type === 'scale' || type === 'pop' || type === 'grow')
|
|
271
|
+
parts.push(`scale(${0.55 + 0.45 * reveal})`);
|
|
272
|
+
else if (type === 'shrink') parts.push(`scale(${1.35 - 0.35 * reveal})`);
|
|
273
|
+
else if (type === 'rotate' || type === 'spin')
|
|
274
|
+
parts.push(`rotate(${p * -28}deg) scale(${0.85 + 0.15 * reveal})`);
|
|
275
|
+
else if (type === 'bounce') parts.push(`translateY(${Math.sin(p * Math.PI) * 22}px)`);
|
|
276
|
+
else if (type === 'drop') parts.push(`translateY(${-p * 80}px)`);
|
|
277
|
+
else if (type === 'swing') parts.push(`rotate(${Math.sin(p * Math.PI) * -18}deg)`);
|
|
278
|
+
else if (type === 'flip-x') parts.push(`perspective(700px) rotateY(${p * 90}deg)`);
|
|
279
|
+
else if (type === 'flip-y') parts.push(`perspective(700px) rotateX(${p * 90}deg)`);
|
|
280
|
+
else if (type === 'skew') parts.push(`skewX(${p * 18}deg)`);
|
|
281
|
+
else if (type === 'expand-h') parts.push(`scaleX(${0.15 + 0.85 * reveal})`);
|
|
282
|
+
else if (type === 'expand-v') parts.push(`scaleY(${0.15 + 0.85 * reveal})`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function applyOut(type: string, durationMs: number) {
|
|
286
|
+
const remaining = clip.endMs - t;
|
|
287
|
+
if (remaining < 0 || remaining >= durationMs) return;
|
|
288
|
+
const p = 1 - remaining / durationMs; // 0 → 1 as it exits
|
|
289
|
+
if (type === 'slide' || type === 'slide-up') parts.push(`translateY(${-p * 48}px)`);
|
|
290
|
+
else if (type === 'slide-down') parts.push(`translateY(${p * 48}px)`);
|
|
291
|
+
else if (type === 'slide-left') parts.push(`translateX(${-p * 64}px)`);
|
|
292
|
+
else if (type === 'slide-right') parts.push(`translateX(${p * 64}px)`);
|
|
293
|
+
else if (type === 'scale' || type === 'pop' || type === 'shrink')
|
|
294
|
+
parts.push(`scale(${1 - 0.45 * p})`);
|
|
295
|
+
else if (type === 'grow') parts.push(`scale(${1 + 0.35 * p})`);
|
|
296
|
+
else if (type === 'rotate' || type === 'spin')
|
|
297
|
+
parts.push(`rotate(${p * 28}deg) scale(${1 - 0.15 * p})`);
|
|
298
|
+
else if (type === 'bounce') parts.push(`translateY(${-Math.sin(p * Math.PI) * 22}px)`);
|
|
299
|
+
else if (type === 'drop') parts.push(`translateY(${p * 80}px)`);
|
|
300
|
+
else if (type === 'swing') parts.push(`rotate(${Math.sin(p * Math.PI) * 18}deg)`);
|
|
301
|
+
else if (type === 'flip-x') parts.push(`perspective(700px) rotateY(${p * -90}deg)`);
|
|
302
|
+
else if (type === 'flip-y') parts.push(`perspective(700px) rotateX(${p * -90}deg)`);
|
|
303
|
+
else if (type === 'skew') parts.push(`skewX(${-p * 18}deg)`);
|
|
304
|
+
else if (type === 'expand-h') parts.push(`scaleX(${1 - 0.85 * p})`);
|
|
305
|
+
else if (type === 'expand-v') parts.push(`scaleY(${1 - 0.85 * p})`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (inAnim?.durationMs) applyIn(inAnim.type, inAnim.durationMs);
|
|
309
|
+
if (outAnim?.durationMs) applyOut(outAnim.type, outAnim.durationMs);
|
|
310
|
+
|
|
311
|
+
return parts.length ? parts.join(' ') : 'none';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function ensureSrc(el: HTMLMediaElement, src: string) {
|
|
315
|
+
if (el.dataset.mediaSrc === src) return;
|
|
316
|
+
el.dataset.mediaSrc = src;
|
|
317
|
+
el.src = src;
|
|
318
|
+
el.load();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function syncMedia(
|
|
322
|
+
el: HTMLMediaElement | null,
|
|
323
|
+
clip: MediaClip | null,
|
|
324
|
+
shouldPlay: boolean,
|
|
325
|
+
opts: {
|
|
326
|
+
lastClipId: string | null;
|
|
327
|
+
wasPlaying: boolean;
|
|
328
|
+
applyAudio: boolean;
|
|
329
|
+
}
|
|
330
|
+
): { clipId: string | null; wasPlaying: boolean } {
|
|
331
|
+
if (!el || !clip?.src) {
|
|
332
|
+
if (el && !el.paused) el.pause();
|
|
333
|
+
return { clipId: null, wasPlaying: false };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
ensureSrc(el, clip.src);
|
|
337
|
+
el.playbackRate = playbackRate;
|
|
338
|
+
|
|
339
|
+
if (opts.applyAudio) {
|
|
340
|
+
const clipVol = clip.volume ?? 1;
|
|
341
|
+
const nextVol = muted ? 0 : Math.max(0, Math.min(1, clipVol * volume));
|
|
342
|
+
el.muted = muted || nextVol === 0;
|
|
343
|
+
el.volume = nextVol;
|
|
344
|
+
} else {
|
|
345
|
+
el.muted = true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const target = sourceTime(clip);
|
|
349
|
+
const clipChanged = clip.id !== opts.lastClipId;
|
|
350
|
+
const playStarted = shouldPlay && !opts.wasPlaying;
|
|
351
|
+
const drift = Math.abs(el.currentTime - target);
|
|
352
|
+
const threshold = shouldPlay ? PLAY_DRIFT * Math.max(1, playbackRate) : SCRUB_DRIFT;
|
|
353
|
+
|
|
354
|
+
if (clipChanged || playStarted || drift > threshold) {
|
|
355
|
+
try {
|
|
356
|
+
el.currentTime = target;
|
|
357
|
+
} catch {
|
|
358
|
+
/* ignore seek before loaded */
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (shouldPlay) {
|
|
363
|
+
if (el.paused) void el.play().catch(() => {});
|
|
364
|
+
} else if (!el.paused) {
|
|
365
|
+
el.pause();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { clipId: clip.id, wasPlaying: shouldPlay };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
$effect(() => {
|
|
372
|
+
const videoClip = programOutgoing?.kind === 'video' ? programOutgoing : null;
|
|
373
|
+
const next = syncMedia(videoEl, videoClip, playing, {
|
|
374
|
+
lastClipId: lastVideoClipId,
|
|
375
|
+
wasPlaying: wasPlayingVideo,
|
|
376
|
+
applyAudio: false
|
|
377
|
+
});
|
|
378
|
+
lastVideoClipId = next.clipId;
|
|
379
|
+
wasPlayingVideo = next.wasPlaying;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
$effect(() => {
|
|
383
|
+
const videoClip = programIncoming?.kind === 'video' ? programIncoming : null;
|
|
384
|
+
const next = syncMedia(incomingVideoEl, videoClip, playing, {
|
|
385
|
+
lastClipId: lastIncomingClipId,
|
|
386
|
+
wasPlaying: wasPlayingIncoming,
|
|
387
|
+
applyAudio: false
|
|
388
|
+
});
|
|
389
|
+
lastIncomingClipId = next.clipId;
|
|
390
|
+
wasPlayingIncoming = next.wasPlaying;
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
$effect(() => {
|
|
394
|
+
const next = syncMedia(audioEl, activeAudio, playing, {
|
|
395
|
+
lastClipId: lastAudioClipId,
|
|
396
|
+
wasPlaying: wasPlayingAudio,
|
|
397
|
+
applyAudio: true
|
|
398
|
+
});
|
|
399
|
+
lastAudioClipId = next.clipId;
|
|
400
|
+
wasPlayingAudio = next.wasPlaying;
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
function overlayVisual(clip: MediaClip): TransitionLayerStyle {
|
|
404
|
+
const baseOp = clipOpacity(clip);
|
|
405
|
+
const baseTf = clipTransform(clip);
|
|
406
|
+
const baseFilter = clipFilter(clip);
|
|
407
|
+
if (
|
|
408
|
+
!transition ||
|
|
409
|
+
(clip.id !== transition.from.id && clip.id !== transition.to?.id)
|
|
410
|
+
) {
|
|
411
|
+
return {
|
|
412
|
+
opacity: baseOp,
|
|
413
|
+
transform: baseTf === 'none' ? 'none' : baseTf,
|
|
414
|
+
filter: baseFilter
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const role = clip.id === transition.from.id ? 'from' : 'to';
|
|
418
|
+
const layer = transitionLayerStyle(role, transition.type, transition.progress);
|
|
419
|
+
const parts = [baseTf !== 'none' ? baseTf : '', layer.transform !== 'none' ? layer.transform : '']
|
|
420
|
+
.filter(Boolean)
|
|
421
|
+
.join(' ');
|
|
422
|
+
const filters = [baseFilter, layer.filter].filter(Boolean).join(' ');
|
|
423
|
+
return {
|
|
424
|
+
opacity: baseOp * layer.opacity,
|
|
425
|
+
transform: parts || 'none',
|
|
426
|
+
filter: filters || undefined,
|
|
427
|
+
transformOrigin: layer.transformOrigin,
|
|
428
|
+
maskImage: layer.maskImage,
|
|
429
|
+
wipe: layer.wipe,
|
|
430
|
+
clipPath: layer.clipPath
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function programLayerStyle(role: 'from' | 'to', clip: MediaClip): TransitionLayerStyle {
|
|
435
|
+
if (transition && (clip.id === transition.from.id || clip.id === transition.to?.id)) {
|
|
436
|
+
const r = clip.id === transition.from.id ? 'from' : 'to';
|
|
437
|
+
return transitionLayerStyle(r, transition.type, transition.progress);
|
|
438
|
+
}
|
|
439
|
+
if (role === 'from') {
|
|
440
|
+
return {
|
|
441
|
+
opacity: clipOpacity(clip),
|
|
442
|
+
transform: clipTransform(clip),
|
|
443
|
+
filter: clipFilter(clip)
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return { opacity: 1, transform: 'none' };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function layerInlineStyle(s: TransitionLayerStyle): string {
|
|
450
|
+
const parts = [
|
|
451
|
+
`opacity:${s.opacity}`,
|
|
452
|
+
`transform:${s.transform || 'none'}`,
|
|
453
|
+
`transform-origin:${s.transformOrigin ?? 'center'}`
|
|
454
|
+
];
|
|
455
|
+
if (s.filter) parts.push(`filter:${s.filter}`);
|
|
456
|
+
if (s.clipPath) parts.push(`clip-path:${s.clipPath}`, `-webkit-clip-path:${s.clipPath}`);
|
|
457
|
+
if (s.maskImage) {
|
|
458
|
+
parts.push(
|
|
459
|
+
`mask-image:${s.maskImage}`,
|
|
460
|
+
`-webkit-mask-image:${s.maskImage}`,
|
|
461
|
+
'mask-size:100% 100%',
|
|
462
|
+
'-webkit-mask-size:100% 100%',
|
|
463
|
+
'mask-repeat:no-repeat',
|
|
464
|
+
'-webkit-mask-repeat:no-repeat'
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
return parts.join(';');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function wipeBoxStyle(box: { left: string; top: string; width: string; height: string }): string {
|
|
471
|
+
return `left:${box.left};top:${box.top};width:${box.width};height:${box.height};`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function beginInteract(clip: MediaClip, mode: DragMode, e: PointerEvent, edge?: ResizeEdge) {
|
|
475
|
+
if (!editable || playing) return;
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
e.stopPropagation();
|
|
478
|
+
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
|
|
479
|
+
onselectclip?.(clip.id, e as unknown as MouseEvent);
|
|
480
|
+
const moveIds =
|
|
481
|
+
mode === 'move' && selectedSet.has(clip.id) && selectedClipIds.length > 1
|
|
482
|
+
? selectedClipIds
|
|
483
|
+
: [clip.id];
|
|
484
|
+
const origins: Record<string, MediaClipRect> = {};
|
|
485
|
+
for (const id of moveIds) {
|
|
486
|
+
const c = overlays.find((o) => o.id === id) ?? (id === clip.id ? clip : null);
|
|
487
|
+
if (c) origins[id] = { ...resolveClipRect(c, frame) };
|
|
488
|
+
}
|
|
489
|
+
if (!origins[clip.id]) origins[clip.id] = { ...resolveClipRect(clip, frame) };
|
|
490
|
+
drag = {
|
|
491
|
+
clipId: clip.id,
|
|
492
|
+
mode,
|
|
493
|
+
edge,
|
|
494
|
+
startX: e.clientX,
|
|
495
|
+
startY: e.clientY,
|
|
496
|
+
origins
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function applyResize(o: MediaClipRect, edge: ResizeEdge, dx: number, dy: number): MediaClipRect {
|
|
501
|
+
const minW = 40;
|
|
502
|
+
const minH = 24;
|
|
503
|
+
let { x, y, w, h } = o;
|
|
504
|
+
|
|
505
|
+
if (edge.includes('e')) w = Math.max(minW, o.w + dx);
|
|
506
|
+
if (edge.includes('s')) h = Math.max(minH, o.h + dy);
|
|
507
|
+
if (edge.includes('w')) {
|
|
508
|
+
const nextW = Math.max(minW, o.w - dx);
|
|
509
|
+
x = o.x + (o.w - nextW);
|
|
510
|
+
w = nextW;
|
|
511
|
+
}
|
|
512
|
+
if (edge.includes('n')) {
|
|
513
|
+
const nextH = Math.max(minH, o.h - dy);
|
|
514
|
+
y = o.y + (o.h - nextH);
|
|
515
|
+
h = nextH;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
x = Math.min(Math.max(0, x), width - w);
|
|
519
|
+
y = Math.min(Math.max(0, y), height - h);
|
|
520
|
+
w = Math.min(w, width - x);
|
|
521
|
+
h = Math.min(h, height - y);
|
|
522
|
+
|
|
523
|
+
return { x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h) };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function onPointerMove(e: PointerEvent) {
|
|
527
|
+
if (!drag || !stageEl) return;
|
|
528
|
+
const r = stageEl.getBoundingClientRect();
|
|
529
|
+
const sx = r.width > 0 ? width / r.width : 1;
|
|
530
|
+
const sy = r.height > 0 ? height / r.height : 1;
|
|
531
|
+
const dx = (e.clientX - drag.startX) * sx;
|
|
532
|
+
const dy = (e.clientY - drag.startY) * sy;
|
|
533
|
+
|
|
534
|
+
if (drag.mode === 'move') {
|
|
535
|
+
const updates: MediaClip[] = [];
|
|
536
|
+
for (const [id, o] of Object.entries(drag.origins)) {
|
|
537
|
+
const next = {
|
|
538
|
+
x: Math.round(Math.min(Math.max(0, o.x + dx), width - o.w)),
|
|
539
|
+
y: Math.round(Math.min(Math.max(0, o.y + dy), height - o.h)),
|
|
540
|
+
w: o.w,
|
|
541
|
+
h: o.h
|
|
542
|
+
};
|
|
543
|
+
const clip = overlays.find((c) => c.id === id);
|
|
544
|
+
if (clip) updates.push({ ...clip, rect: next });
|
|
545
|
+
}
|
|
546
|
+
if (updates.length) {
|
|
547
|
+
if (onchangeclips) onchangeclips(updates);
|
|
548
|
+
else updates.forEach((c) => onchangeclip?.(c));
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const o = drag.origins[drag.clipId];
|
|
554
|
+
if (!o) return;
|
|
555
|
+
const next = applyResize(o, drag.edge ?? 'se', dx, dy);
|
|
556
|
+
const clip = overlays.find((c) => c.id === drag!.clipId);
|
|
557
|
+
if (clip) onchangeclip?.({ ...clip, rect: next });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function endInteract() {
|
|
561
|
+
drag = null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function styleForRect(rect: MediaClipRect): string {
|
|
565
|
+
return `left:${(rect.x / width) * 100}%;top:${(rect.y / height) * 100}%;width:${(rect.w / width) * 100}%;height:${(rect.h / height) * 100}%;`;
|
|
566
|
+
}
|
|
32
567
|
</script>
|
|
33
568
|
|
|
569
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
34
570
|
<div
|
|
571
|
+
bind:this={stageEl}
|
|
35
572
|
class={[
|
|
36
573
|
'relative flex aspect-video w-full items-center justify-center overflow-hidden rounded-lg border border-border',
|
|
37
574
|
className
|
|
38
575
|
]}
|
|
39
576
|
style:background
|
|
40
577
|
style:aspect-ratio={`${width} / ${height}`}
|
|
578
|
+
onpointermove={onPointerMove}
|
|
579
|
+
onpointerup={endInteract}
|
|
580
|
+
onpointercancel={endInteract}
|
|
581
|
+
onclick={(e) => {
|
|
582
|
+
if (e.target === e.currentTarget) onemptyclick?.();
|
|
583
|
+
}}
|
|
41
584
|
>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
585
|
+
<!-- Program / full-frame media (+ transition pair) -->
|
|
586
|
+
<div class="absolute inset-0 overflow-hidden">
|
|
587
|
+
{#if programOutgoing?.src && (programOutgoing.kind === 'video' || programOutgoing.kind === 'image')}
|
|
588
|
+
{@const fromStyle = programLayerStyle('from', programOutgoing)}
|
|
589
|
+
{#if fromStyle.wipe}
|
|
590
|
+
<div class="absolute overflow-hidden" style={wipeBoxStyle(fromStyle.wipe.box)}>
|
|
591
|
+
<div
|
|
592
|
+
class="absolute flex items-center justify-center"
|
|
593
|
+
style={`${wipeBoxStyle(fromStyle.wipe.content)}${layerInlineStyle(fromStyle)}`}
|
|
594
|
+
>
|
|
595
|
+
{#if programOutgoing.kind === 'video'}
|
|
596
|
+
<video
|
|
597
|
+
bind:this={videoEl}
|
|
598
|
+
class="h-full w-full object-contain"
|
|
599
|
+
muted
|
|
600
|
+
playsinline
|
|
601
|
+
preload="auto"
|
|
602
|
+
></video>
|
|
603
|
+
{:else}
|
|
604
|
+
<img
|
|
605
|
+
src={programOutgoing.src}
|
|
606
|
+
alt={programOutgoing.name}
|
|
607
|
+
class="h-full w-full object-contain"
|
|
608
|
+
/>
|
|
609
|
+
{/if}
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
{:else}
|
|
613
|
+
<div class="absolute inset-0 flex items-center justify-center" style={layerInlineStyle(fromStyle)}>
|
|
614
|
+
{#if programOutgoing.kind === 'video'}
|
|
615
|
+
<video
|
|
616
|
+
bind:this={videoEl}
|
|
617
|
+
class="h-full w-full object-contain"
|
|
618
|
+
muted
|
|
619
|
+
playsinline
|
|
620
|
+
preload="auto"
|
|
621
|
+
></video>
|
|
622
|
+
{:else}
|
|
623
|
+
<img
|
|
624
|
+
src={programOutgoing.src}
|
|
625
|
+
alt={programOutgoing.name}
|
|
626
|
+
class="h-full w-full object-contain"
|
|
627
|
+
/>
|
|
628
|
+
{/if}
|
|
629
|
+
</div>
|
|
630
|
+
{/if}
|
|
631
|
+
{/if}
|
|
632
|
+
|
|
633
|
+
{#if programIncoming?.src}
|
|
634
|
+
{@const toStyle = programLayerStyle('to', programIncoming)}
|
|
635
|
+
{#if toStyle.wipe}
|
|
636
|
+
<div class="absolute overflow-hidden" style={wipeBoxStyle(toStyle.wipe.box)}>
|
|
637
|
+
<div
|
|
638
|
+
class="absolute flex items-center justify-center"
|
|
639
|
+
style={`${wipeBoxStyle(toStyle.wipe.content)}${layerInlineStyle(toStyle)}`}
|
|
640
|
+
>
|
|
641
|
+
{#if programIncoming.kind === 'video'}
|
|
642
|
+
<video
|
|
643
|
+
bind:this={incomingVideoEl}
|
|
644
|
+
class="h-full w-full object-contain"
|
|
645
|
+
muted
|
|
646
|
+
playsinline
|
|
647
|
+
preload="auto"
|
|
648
|
+
></video>
|
|
649
|
+
{:else}
|
|
650
|
+
<img
|
|
651
|
+
src={programIncoming.src}
|
|
652
|
+
alt={programIncoming.name}
|
|
653
|
+
class="h-full w-full object-contain"
|
|
654
|
+
/>
|
|
655
|
+
{/if}
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
{:else}
|
|
659
|
+
<div class="absolute inset-0 flex items-center justify-center" style={layerInlineStyle(toStyle)}>
|
|
660
|
+
{#if programIncoming.kind === 'video'}
|
|
661
|
+
<video
|
|
662
|
+
bind:this={incomingVideoEl}
|
|
663
|
+
class="h-full w-full object-contain"
|
|
664
|
+
muted
|
|
665
|
+
playsinline
|
|
666
|
+
preload="auto"
|
|
667
|
+
></video>
|
|
668
|
+
{:else}
|
|
669
|
+
<img
|
|
670
|
+
src={programIncoming.src}
|
|
671
|
+
alt={programIncoming.name}
|
|
672
|
+
class="h-full w-full object-contain"
|
|
673
|
+
/>
|
|
674
|
+
{/if}
|
|
675
|
+
</div>
|
|
676
|
+
{/if}
|
|
677
|
+
{/if}
|
|
678
|
+
|
|
679
|
+
{#if dip}
|
|
680
|
+
<div
|
|
681
|
+
class="pointer-events-none absolute inset-0"
|
|
682
|
+
style:background={dip.color}
|
|
683
|
+
style:opacity={dip.opacity}
|
|
684
|
+
></div>
|
|
685
|
+
{/if}
|
|
686
|
+
|
|
687
|
+
{#if !programOutgoing?.src && overlays.length === 0 && !programIncoming}
|
|
688
|
+
<div class="flex h-full items-center justify-center">
|
|
689
|
+
<span class="text-sm text-muted">No clip at playhead</span>
|
|
690
|
+
</div>
|
|
691
|
+
{/if}
|
|
692
|
+
</div>
|
|
693
|
+
|
|
694
|
+
{#each overlays as clip (clip.id)}
|
|
695
|
+
{@const rect = resolveClipRect(clip, frame)}
|
|
696
|
+
{@const selected = selectedSet.has(clip.id)}
|
|
697
|
+
{@const showChrome = selected && editable && !playing}
|
|
698
|
+
{@const visual = overlayVisual(clip)}
|
|
699
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
700
|
+
<div
|
|
701
|
+
class={[
|
|
702
|
+
'group/overlay absolute box-border overflow-hidden',
|
|
703
|
+
editable && !playing ? 'cursor-move' : 'pointer-events-none',
|
|
704
|
+
showChrome && 'outline outline-2 outline-[#3b82f6]'
|
|
705
|
+
]}
|
|
706
|
+
style={styleForRect(rect)}
|
|
707
|
+
data-marquee-id={clip.id}
|
|
708
|
+
onpointerdown={(e) => {
|
|
709
|
+
const t = e.target as HTMLElement;
|
|
710
|
+
if (t?.closest('[data-resize-handle]')) return;
|
|
711
|
+
beginInteract(clip, 'move', e);
|
|
712
|
+
}}
|
|
713
|
+
onclick={(e) => {
|
|
714
|
+
e.stopPropagation();
|
|
715
|
+
onselectclip?.(clip.id, e);
|
|
716
|
+
}}
|
|
717
|
+
>
|
|
718
|
+
{#if visual.wipe}
|
|
719
|
+
<div class="absolute overflow-hidden" style={wipeBoxStyle(visual.wipe.box)}>
|
|
720
|
+
<div
|
|
721
|
+
class="absolute flex items-center justify-center"
|
|
722
|
+
style={`${wipeBoxStyle(visual.wipe.content)}${layerInlineStyle(visual)};background:${clip.kind === 'image' && !clip.src ? (clip.color ?? '#000') : 'transparent'}`}
|
|
723
|
+
>
|
|
724
|
+
{#if clip.kind === 'text'}
|
|
725
|
+
<span
|
|
726
|
+
class="max-h-full max-w-full overflow-hidden whitespace-pre-line px-2 py-1 text-center leading-tight"
|
|
727
|
+
style:color={clip.textColor ?? '#ffffff'}
|
|
728
|
+
style:font-size={`${Math.max(10, (clip.fontSize ?? 24) * (rect.h / 120))}px`}
|
|
729
|
+
style:font-weight={clip.fontWeight ?? 600}
|
|
730
|
+
style:font-family={clip.fontFamily}
|
|
731
|
+
style:text-align={clip.textAlign ?? 'center'}
|
|
732
|
+
style:text-shadow="0 1px 3px rgba(0,0,0,0.55)"
|
|
733
|
+
>
|
|
734
|
+
{clip.text ?? clip.name}
|
|
735
|
+
</span>
|
|
736
|
+
{:else if clip.src}
|
|
737
|
+
<img src={clip.src} alt={clip.name} class="h-full w-full object-cover" />
|
|
738
|
+
{/if}
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
{:else}
|
|
742
|
+
<div
|
|
743
|
+
class="flex h-full w-full items-center justify-center overflow-hidden"
|
|
744
|
+
style={`${layerInlineStyle(visual)};background:${clip.kind === 'image' && !clip.src ? (clip.color ?? '#000') : 'transparent'}`}
|
|
745
|
+
>
|
|
746
|
+
{#if clip.kind === 'text'}
|
|
747
|
+
<span
|
|
748
|
+
class="max-h-full max-w-full overflow-hidden whitespace-pre-line px-2 py-1 text-center leading-tight"
|
|
749
|
+
style:color={clip.textColor ?? '#ffffff'}
|
|
750
|
+
style:font-size={`${Math.max(10, (clip.fontSize ?? 24) * (rect.h / 120))}px`}
|
|
751
|
+
style:font-weight={clip.fontWeight ?? 600}
|
|
752
|
+
style:font-family={clip.fontFamily}
|
|
753
|
+
style:text-align={clip.textAlign ?? 'center'}
|
|
754
|
+
style:text-shadow="0 1px 3px rgba(0,0,0,0.55)"
|
|
755
|
+
>
|
|
756
|
+
{clip.text ?? clip.name}
|
|
757
|
+
</span>
|
|
758
|
+
{:else if clip.src}
|
|
759
|
+
<img src={clip.src} alt={clip.name} class="h-full w-full object-cover" />
|
|
760
|
+
{/if}
|
|
761
|
+
</div>
|
|
762
|
+
{/if}
|
|
763
|
+
|
|
764
|
+
{#if showChrome}
|
|
765
|
+
{#each canvaHandles as h (h.edge)}
|
|
766
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
767
|
+
<div
|
|
768
|
+
data-resize-handle
|
|
769
|
+
role="separator"
|
|
770
|
+
aria-label={`Resize ${h.edge}`}
|
|
771
|
+
class={[
|
|
772
|
+
'absolute z-20 h-3 w-3 touch-none rounded-full border-2 border-[#3b82f6] bg-white shadow-sm',
|
|
773
|
+
h.class
|
|
774
|
+
]}
|
|
775
|
+
onpointerdown={(e) => beginInteract(clip, 'resize', e, h.edge)}
|
|
776
|
+
></div>
|
|
777
|
+
{/each}
|
|
778
|
+
{/if}
|
|
58
779
|
</div>
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
{
|
|
780
|
+
{/each}
|
|
781
|
+
|
|
782
|
+
<audio bind:this={audioEl} class="hidden" preload="auto"></audio>
|
|
62
783
|
|
|
63
|
-
{
|
|
64
|
-
<audio src={activeAudio.src} currentTime={sourceTime(activeAudio)} class="hidden"></audio>
|
|
65
|
-
{/if}
|
|
784
|
+
<MarqueeRect rect={marquee} />
|
|
66
785
|
|
|
67
786
|
<div
|
|
68
787
|
class="pointer-events-none absolute bottom-2 left-2 rounded bg-black/50 px-2 py-0.5 font-mono text-[10px] text-white"
|
|
69
788
|
>
|
|
70
|
-
{
|
|
789
|
+
{formatTimecode(currentTimeMs)}
|
|
790
|
+
{#if transition}
|
|
791
|
+
<span class="ml-2 text-fuchsia-300">· {transition.type}</span>
|
|
792
|
+
{/if}
|
|
71
793
|
</div>
|
|
72
794
|
</div>
|