animot-presenter 0.5.22 → 0.5.24
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/dist/AnimotPresenter.svelte +216 -62
- package/dist/cdn/animot-presenter.esm.js +4811 -4729
- package/dist/cdn/animot-presenter.min.js +8 -8
- package/package.json +2 -1
|
@@ -29,6 +29,42 @@
|
|
|
29
29
|
|
|
30
30
|
type TweenValue = ReturnType<typeof tween<number>>;
|
|
31
31
|
|
|
32
|
+
// Svelte's underlying Tween.set() leaks: when retargeted before completion,
|
|
33
|
+
// the previous rAF task is aborted but its promise is NEVER fulfilled
|
|
34
|
+
// (see svelte/src/internal/client/loop.js — abort() only deletes the task,
|
|
35
|
+
// doesn't call fulfill). The presenter calls .to() on 15+ tween properties
|
|
36
|
+
// per element per slide transition, so a deck running in a loop accumulates
|
|
37
|
+
// hundreds of thousands of orphaned promises plus the async-function
|
|
38
|
+
// Contexts of any awaiter that's stuck on them. wrapTween() patches each
|
|
39
|
+
// tween instance so:
|
|
40
|
+
// 1) .to() on a value the tween is already at is a no-op (Promise.resolve)
|
|
41
|
+
// 2) Each .to() resolves the previous wrapper promise immediately on
|
|
42
|
+
// retarget, so any await chain unwinds and releases its Context even
|
|
43
|
+
// if the underlying Svelte promise is left dangling.
|
|
44
|
+
function wrapTween<TV extends { current: unknown; to: (v: never, o?: unknown) => Promise<void> }>(tv: TV): TV {
|
|
45
|
+
const origTo = tv.to.bind(tv);
|
|
46
|
+
let prevResolve: (() => void) | null = null;
|
|
47
|
+
(tv as unknown as { to: (v: unknown, o?: unknown) => Promise<void> }).to = (value, options) => {
|
|
48
|
+
if (tv.current === value) {
|
|
49
|
+
if (prevResolve) { const r = prevResolve; prevResolve = null; r(); }
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
}
|
|
52
|
+
if (prevResolve) { const r = prevResolve; prevResolve = null; r(); }
|
|
53
|
+
const inner = origTo(value as never, options);
|
|
54
|
+
return new Promise<void>((resolve) => {
|
|
55
|
+
prevResolve = resolve;
|
|
56
|
+
const done = () => {
|
|
57
|
+
if (prevResolve === resolve) { prevResolve = null; resolve(); }
|
|
58
|
+
};
|
|
59
|
+
inner.then(done, done);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
return tv;
|
|
63
|
+
}
|
|
64
|
+
function mkTween<T>(value: T, options?: Parameters<typeof tween<T>>[1]): ReturnType<typeof tween<T>> {
|
|
65
|
+
return wrapTween(tween(value, options)) as ReturnType<typeof tween<T>>;
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
interface AnimatedElement {
|
|
33
69
|
x: TweenValue; y: TweenValue; width: TweenValue; height: TweenValue;
|
|
34
70
|
rotation: TweenValue; skewX: TweenValue; skewY: TweenValue;
|
|
@@ -49,34 +85,76 @@
|
|
|
49
85
|
|
|
50
86
|
interface ShapeMorphState { fromType: string; toType: string; }
|
|
51
87
|
|
|
88
|
+
// Race a promise against an AbortSignal so awaits unwind the instant a
|
|
89
|
+
// loop is cancelled — otherwise tween.to() / setTimeout promises keep
|
|
90
|
+
// pending and pin their async-function Context to the heap. Long-running
|
|
91
|
+
// loops without this leak millions of closure contexts (see commit notes).
|
|
92
|
+
function abortable<T>(p: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
93
|
+
if (signal.aborted) return Promise.reject(new DOMException('aborted', 'AbortError'));
|
|
94
|
+
return new Promise<T>((resolve, reject) => {
|
|
95
|
+
const onAbort = () => reject(new DOMException('aborted', 'AbortError'));
|
|
96
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
97
|
+
p.then(
|
|
98
|
+
(v) => { signal.removeEventListener('abort', onAbort); resolve(v); },
|
|
99
|
+
(e) => { signal.removeEventListener('abort', onAbort); reject(e); }
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
104
|
+
if (signal.aborted) return Promise.reject(new DOMException('aborted', 'AbortError'));
|
|
105
|
+
return new Promise<void>((resolve, reject) => {
|
|
106
|
+
const id = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
107
|
+
const onAbort = () => { clearTimeout(id); reject(new DOMException('aborted', 'AbortError')); };
|
|
108
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function isAbortError(e: unknown): boolean {
|
|
112
|
+
return !!(e && typeof e === 'object' && (e as { name?: string }).name === 'AbortError');
|
|
113
|
+
}
|
|
114
|
+
|
|
52
115
|
// Active motion path loop cancellation tokens
|
|
53
116
|
let motionPathLoopAbort: AbortController | null = null;
|
|
54
117
|
function cancelMotionPathLoops() {
|
|
55
118
|
if (motionPathLoopAbort) { motionPathLoopAbort.abort(); motionPathLoopAbort = null; }
|
|
56
119
|
}
|
|
57
120
|
|
|
58
|
-
// Keyframe schedule loop
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// crowd out the existing tween reactivity in 0.5.20.
|
|
121
|
+
// Keyframe schedule loop. Plain (non-reactive) module-scope state — adding
|
|
122
|
+
// $state for keyframe overrides in 0.5.20 broke reactivity for the existing
|
|
123
|
+
// tween-driven render. This implementation only retargets existing tweens
|
|
124
|
+
// via setTimeout; nothing here is reactive, nothing is read from render.
|
|
63
125
|
let keyframeLoopAbort: AbortController | null = null;
|
|
126
|
+
// Scheduled-but-not-yet-fired keyframe timeouts. We track these so cancel
|
|
127
|
+
// also clears them — otherwise their closures (which capture `signal`,
|
|
128
|
+
// `animated`, etc.) survive until natural firing time and pin a Context.
|
|
129
|
+
let keyframeTimeouts: ReturnType<typeof setTimeout>[] = [];
|
|
130
|
+
// Per-element overrides for keyframe-driven props that aren't tweened
|
|
131
|
+
// (backgroundColor, text color). The schedule writes these at each
|
|
132
|
+
// keyframe boundary; liveProps reads them at render time.
|
|
133
|
+
let keyframeOverrides = $state<Map<string, Record<string, any>>>(new Map());
|
|
64
134
|
function cancelKeyframeLoops() {
|
|
65
135
|
if (keyframeLoopAbort) { keyframeLoopAbort.abort(); keyframeLoopAbort = null; }
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
case 'linear': case 'ease-in': case 'ease-out': case 'ease-in-out': return name;
|
|
70
|
-
case 'spring': return 'ease-out';
|
|
71
|
-
case 'ease': default: return 'ease-out';
|
|
136
|
+
if (keyframeTimeouts.length) {
|
|
137
|
+
for (const id of keyframeTimeouts) clearTimeout(id);
|
|
138
|
+
keyframeTimeouts = [];
|
|
72
139
|
}
|
|
73
140
|
}
|
|
141
|
+
function setKeyframeOverride(elementId: string, prop: string, value: any) {
|
|
142
|
+
const cur = keyframeOverrides.get(elementId) ?? {};
|
|
143
|
+
keyframeOverrides.set(elementId, { ...cur, [prop]: value });
|
|
144
|
+
keyframeOverrides = new Map(keyframeOverrides);
|
|
145
|
+
}
|
|
146
|
+
// Tweens take an easing FUNCTION (t→number), not a CSS keyword. Returning
|
|
147
|
+
// a string here was the cause of "r is not a function" thrown deep in the
|
|
148
|
+
// tween animation loop — `r(t)` crashed because `r` was the literal string
|
|
149
|
+
// passed in. Reuse the engine's `getEasingFn` so keyframe easing matches
|
|
150
|
+
// the slide-morph engine's vocabulary.
|
|
151
|
+
function easingForTween(name: string | undefined): (t: number) => number {
|
|
152
|
+
return getEasingFn(name ?? 'ease-out');
|
|
153
|
+
}
|
|
74
154
|
function animateKeyframes(slide: Slide) {
|
|
75
155
|
cancelKeyframeLoops();
|
|
76
|
-
// Bail if nothing on the slide actually has keyframes — avoids
|
|
77
|
-
// allocating an AbortController + iterating elements for the common
|
|
78
|
-
// case (no keyframes anywhere).
|
|
79
156
|
const hasAnyKeyframes = slide.canvas.elements.some((el) => el.keyframes && el.keyframes.length > 0);
|
|
157
|
+
if (keyframeOverrides.size > 0) keyframeOverrides = new Map();
|
|
80
158
|
if (!hasAnyKeyframes) return;
|
|
81
159
|
keyframeLoopAbort = new AbortController();
|
|
82
160
|
const signal = keyframeLoopAbort.signal;
|
|
@@ -86,6 +164,7 @@
|
|
|
86
164
|
if (!animated) continue;
|
|
87
165
|
const sorted = [...element.keyframes].sort((a, b) => a.time - b.time);
|
|
88
166
|
const first = sorted[0];
|
|
167
|
+
// Snap tweens to KF1 instantly so the slide-displayed pose IS the start.
|
|
89
168
|
if (first.position) { animated.x?.to(first.position.x, { duration: 0 }); animated.y?.to(first.position.y, { duration: 0 }); }
|
|
90
169
|
if (first.size) { animated.width?.to(first.size.width, { duration: 0 }); animated.height?.to(first.size.height, { duration: 0 }); }
|
|
91
170
|
if (first.rotation !== undefined) animated.rotation?.to(first.rotation, { duration: 0 });
|
|
@@ -104,8 +183,10 @@
|
|
|
104
183
|
if (first.contrast !== undefined) animated.contrast?.to(first.contrast, { duration: 0 });
|
|
105
184
|
if (first.saturate !== undefined) animated.saturate?.to(first.saturate, { duration: 0 });
|
|
106
185
|
if (first.grayscale !== undefined) animated.grayscale?.to(first.grayscale, { duration: 0 });
|
|
186
|
+
if (first.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', first.backgroundColor);
|
|
187
|
+
if (first.color !== undefined) setKeyframeOverride(element.id, 'color', first.color);
|
|
107
188
|
for (const kf of sorted.slice(1)) {
|
|
108
|
-
setTimeout(() => {
|
|
189
|
+
const tid = setTimeout(() => {
|
|
109
190
|
if (signal.aborted) return;
|
|
110
191
|
const easing = easingForTween(kf.easing);
|
|
111
192
|
const idx = sorted.indexOf(kf);
|
|
@@ -129,7 +210,10 @@
|
|
|
129
210
|
if (kf.contrast !== undefined) animated.contrast?.to(kf.contrast, { duration: span, easing });
|
|
130
211
|
if (kf.saturate !== undefined) animated.saturate?.to(kf.saturate, { duration: span, easing });
|
|
131
212
|
if (kf.grayscale !== undefined) animated.grayscale?.to(kf.grayscale, { duration: span, easing });
|
|
213
|
+
if (kf.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', kf.backgroundColor);
|
|
214
|
+
if (kf.color !== undefined) setKeyframeOverride(element.id, 'color', kf.color);
|
|
132
215
|
}, Math.max(0, kf.time));
|
|
216
|
+
keyframeTimeouts.push(tid);
|
|
133
217
|
}
|
|
134
218
|
}
|
|
135
219
|
}
|
|
@@ -347,6 +431,35 @@
|
|
|
347
431
|
return slide?.canvas.elements.find(el => el.id === elementId);
|
|
348
432
|
}
|
|
349
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Overlay tween-driven values + non-tweened keyframe overrides onto the
|
|
436
|
+
* element used in render. Gated: returns the element unchanged when it
|
|
437
|
+
* has no keyframes AND no active override — that's the 99% case and
|
|
438
|
+
* keeps the existing slide-morph render pipeline allocation-free.
|
|
439
|
+
*/
|
|
440
|
+
function liveProps<T extends CanvasElement>(element: T): T {
|
|
441
|
+
const hasKeyframes = !!element.keyframes && element.keyframes.length > 0;
|
|
442
|
+
const overrides = keyframeOverrides.get(element.id);
|
|
443
|
+
if (!hasKeyframes && !overrides) return element;
|
|
444
|
+
const a = animatedElements.get(element.id) as any;
|
|
445
|
+
const e = element as any;
|
|
446
|
+
const out: any = { ...element };
|
|
447
|
+
if (a && hasKeyframes) {
|
|
448
|
+
if (a.borderRadius && e.borderRadius !== undefined) out.borderRadius = a.borderRadius.current;
|
|
449
|
+
if (a.fontSize && e.fontSize !== undefined) out.fontSize = a.fontSize.current;
|
|
450
|
+
if (a.fillColor && e.fillColor !== undefined) out.fillColor = a.fillColor.current;
|
|
451
|
+
if (a.strokeColor && e.strokeColor !== undefined) out.strokeColor = a.strokeColor.current;
|
|
452
|
+
if (a.strokeWidth && e.strokeWidth !== undefined) out.strokeWidth = a.strokeWidth.current;
|
|
453
|
+
if (a.blur && e.blur !== undefined) out.blur = a.blur.current;
|
|
454
|
+
if (a.brightness && e.brightness !== undefined) out.brightness = a.brightness.current;
|
|
455
|
+
if (a.contrast && e.contrast !== undefined) out.contrast = a.contrast.current;
|
|
456
|
+
if (a.saturate && e.saturate !== undefined) out.saturate = a.saturate.current;
|
|
457
|
+
if (a.grayscale && e.grayscale !== undefined) out.grayscale = a.grayscale.current;
|
|
458
|
+
}
|
|
459
|
+
if (overrides) Object.assign(out, overrides);
|
|
460
|
+
return out as T;
|
|
461
|
+
}
|
|
462
|
+
|
|
350
463
|
// Typewriter
|
|
351
464
|
function startTypewriterAnimation(elementId: string, fullText: string, speed: number) {
|
|
352
465
|
const existing = typewriterIntervals.get(elementId);
|
|
@@ -487,36 +600,71 @@
|
|
|
487
600
|
for (const element of slide.canvas.elements) {
|
|
488
601
|
if (!animatedElements.has(element.id)) {
|
|
489
602
|
const inCurrent = getElementInSlide(currentSlide, element.id);
|
|
490
|
-
|
|
603
|
+
let startOpacity = inCurrent ? ((inCurrent as any).opacity ?? 1) : 0;
|
|
491
604
|
const br = (element as any).borderRadius ?? 0;
|
|
492
605
|
const isShape = element.type === 'shape';
|
|
493
606
|
const shapeEl = isShape ? element as ShapeElement : null;
|
|
494
607
|
const isText = element.type === 'text';
|
|
495
608
|
const textEl = isText ? element as TextElement : null;
|
|
609
|
+
|
|
610
|
+
// If the element has keyframes, seed each tween with the FIRST
|
|
611
|
+
// keyframe's value instead of the persisted (last-captured)
|
|
612
|
+
// state. The persisted state is whatever was on the canvas
|
|
613
|
+
// at the moment the user captured their final keyframe; using
|
|
614
|
+
// it as the tween's starting value would cause the slide to
|
|
615
|
+
// flash at the "wrong" pose before the keyframe schedule
|
|
616
|
+
// snaps it to KF1. Doing it here also avoids depending on a
|
|
617
|
+
// duration:0 snap that the tween library may not apply
|
|
618
|
+
// synchronously.
|
|
619
|
+
const sortedKfs = element.keyframes && element.keyframes.length > 0
|
|
620
|
+
? [...element.keyframes].sort((a, b) => a.time - b.time)
|
|
621
|
+
: null;
|
|
622
|
+
const k0 = sortedKfs ? sortedKfs[0] : null;
|
|
623
|
+
const seedX = k0?.position ? k0.position.x : element.position.x;
|
|
624
|
+
const seedY = k0?.position ? k0.position.y : element.position.y;
|
|
625
|
+
const seedW = k0?.size ? k0.size.width : element.size.width;
|
|
626
|
+
const seedH = k0?.size ? k0.size.height : element.size.height;
|
|
627
|
+
const seedRot = k0?.rotation !== undefined ? k0.rotation : element.rotation;
|
|
628
|
+
const seedSkewX = k0?.skewX !== undefined ? k0.skewX : (element.skewX ?? 0);
|
|
629
|
+
const seedSkewY = k0?.skewY !== undefined ? k0.skewY : (element.skewY ?? 0);
|
|
630
|
+
const seedTiltX = k0?.tiltX !== undefined ? k0.tiltX : (element.tiltX ?? 0);
|
|
631
|
+
const seedTiltY = k0?.tiltY !== undefined ? k0.tiltY : (element.tiltY ?? 0);
|
|
632
|
+
if (k0?.opacity !== undefined) startOpacity = k0.opacity;
|
|
633
|
+
const seedBR = k0?.borderRadius !== undefined ? k0.borderRadius : br;
|
|
634
|
+
const seedFontSize = k0?.fontSize !== undefined ? k0.fontSize : (textEl ? textEl.fontSize : 0);
|
|
635
|
+
const seedFill = k0?.fillColor !== undefined ? k0.fillColor : (shapeEl ? shapeEl.fillColor : '');
|
|
636
|
+
const seedStroke = k0?.strokeColor !== undefined ? k0.strokeColor : (shapeEl ? shapeEl.strokeColor : '');
|
|
637
|
+
const seedStrokeW = k0?.strokeWidth !== undefined ? k0.strokeWidth : (shapeEl ? shapeEl.strokeWidth : 0);
|
|
638
|
+
const seedBlur = k0?.blur !== undefined ? k0.blur : (element.blur ?? 0);
|
|
639
|
+
const seedBright = k0?.brightness !== undefined ? k0.brightness : (element.brightness ?? 100);
|
|
640
|
+
const seedContrast = k0?.contrast !== undefined ? k0.contrast : (element.contrast ?? 100);
|
|
641
|
+
const seedSat = k0?.saturate !== undefined ? k0.saturate : (element.saturate ?? 100);
|
|
642
|
+
const seedGray = k0?.grayscale !== undefined ? k0.grayscale : (element.grayscale ?? 0);
|
|
643
|
+
|
|
496
644
|
animatedElements.set(element.id, {
|
|
497
|
-
x:
|
|
498
|
-
y:
|
|
499
|
-
width:
|
|
500
|
-
height:
|
|
501
|
-
rotation:
|
|
502
|
-
skewX:
|
|
503
|
-
skewY:
|
|
504
|
-
tiltX:
|
|
505
|
-
tiltY:
|
|
506
|
-
perspective:
|
|
507
|
-
opacity:
|
|
508
|
-
borderRadius:
|
|
509
|
-
fontSize: textEl ?
|
|
510
|
-
fillColor: shapeEl ?
|
|
511
|
-
strokeColor: shapeEl ?
|
|
512
|
-
strokeWidth: shapeEl ?
|
|
513
|
-
shapeMorph: shapeEl ?
|
|
514
|
-
motionPathProgress: element.motionPathConfig ?
|
|
515
|
-
blur:
|
|
516
|
-
brightness:
|
|
517
|
-
contrast:
|
|
518
|
-
saturate:
|
|
519
|
-
grayscale:
|
|
645
|
+
x: mkTween(seedX, { duration: 500 }),
|
|
646
|
+
y: mkTween(seedY, { duration: 500 }),
|
|
647
|
+
width: mkTween(seedW, { duration: 500 }),
|
|
648
|
+
height: mkTween(seedH, { duration: 500 }),
|
|
649
|
+
rotation: mkTween(seedRot, { duration: 500 }),
|
|
650
|
+
skewX: mkTween(seedSkewX, { duration: 500 }),
|
|
651
|
+
skewY: mkTween(seedSkewY, { duration: 500 }),
|
|
652
|
+
tiltX: mkTween(seedTiltX, { duration: 500 }),
|
|
653
|
+
tiltY: mkTween(seedTiltY, { duration: 500 }),
|
|
654
|
+
perspective: mkTween(element.perspective ?? 1000, { duration: 500 }),
|
|
655
|
+
opacity: mkTween(startOpacity, { duration: 300 }),
|
|
656
|
+
borderRadius: mkTween(seedBR, { duration: 500 }),
|
|
657
|
+
fontSize: textEl ? mkTween(seedFontSize, { duration: 500 }) : null,
|
|
658
|
+
fillColor: shapeEl ? mkTween(seedFill, { duration: 500 }) : null,
|
|
659
|
+
strokeColor: shapeEl ? mkTween(seedStroke, { duration: 500 }) : null,
|
|
660
|
+
strokeWidth: shapeEl ? mkTween(seedStrokeW, { duration: 500 }) : null,
|
|
661
|
+
shapeMorph: shapeEl ? mkTween(1, { duration: 500 }) : null,
|
|
662
|
+
motionPathProgress: element.motionPathConfig ? mkTween(0, { duration: 500 }) : null,
|
|
663
|
+
blur: mkTween(seedBlur, { duration: 500 }),
|
|
664
|
+
brightness: mkTween(seedBright, { duration: 500 }),
|
|
665
|
+
contrast: mkTween(seedContrast, { duration: 500 }),
|
|
666
|
+
saturate: mkTween(seedSat, { duration: 500 }),
|
|
667
|
+
grayscale: mkTween(seedGray, { duration: 500 })
|
|
520
668
|
});
|
|
521
669
|
const currentSlideEl = getElementInSlide(currentSlide, element.id);
|
|
522
670
|
elementContent.set(element.id, JSON.parse(JSON.stringify(currentSlideEl || element)));
|
|
@@ -555,16 +703,20 @@
|
|
|
555
703
|
if (shouldLoop) {
|
|
556
704
|
const laps = element.motionPathConfig.laps ?? 0;
|
|
557
705
|
(async () => {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
706
|
+
try {
|
|
707
|
+
let lap = 0;
|
|
708
|
+
while (!signal.aborted && (laps === 0 || lap < laps)) {
|
|
709
|
+
await abortable(animated.motionPathProgress!.to(0, { duration: 0 }), signal);
|
|
710
|
+
await abortable(animated.motionPathProgress!.to(1, { duration, easing }), signal);
|
|
711
|
+
lap++;
|
|
712
|
+
if (!signal.aborted && (laps === 0 || lap < laps)) await abortableSleep(50, signal);
|
|
713
|
+
}
|
|
714
|
+
} catch (e) {
|
|
715
|
+
if (!isAbortError(e)) throw e;
|
|
564
716
|
}
|
|
565
717
|
})();
|
|
566
718
|
} else {
|
|
567
|
-
animated.motionPathProgress.to(1, { duration, easing });
|
|
719
|
+
animated.motionPathProgress.to(1, { duration, easing }).catch(() => {});
|
|
568
720
|
}
|
|
569
721
|
}
|
|
570
722
|
}
|
|
@@ -1384,7 +1536,7 @@
|
|
|
1384
1536
|
use:decorations={{ config: element.decorations, slideDuration: currentSlide?.duration, shape: element.type === 'shape' ? { type: (element as any).shapeType, borderRadius: (element as any).borderRadius } : undefined, key: `${currentSlideIndex}-${JSON.stringify(element.decorations ?? null)}-${element.type === 'shape' ? (element as any).shapeType + ':' + ((element as any).borderRadius ?? 0) : ''}` }}
|
|
1385
1537
|
>
|
|
1386
1538
|
{#if element.type === 'code'}
|
|
1387
|
-
{@const codeEl = element as CodeElement}
|
|
1539
|
+
{@const codeEl = liveProps(element) as CodeElement}
|
|
1388
1540
|
{@const morphState = codeMorphState.get(codeEl.id)}
|
|
1389
1541
|
<div class="animot-code-block" class:transparent-bg={codeEl.transparentBackground} style:font-size="{codeEl.fontSize}px" style:font-weight={codeEl.fontWeight || 400} style:padding="{codeEl.padding}px" style:border-radius="{animated.borderRadius.current}px" style:background={codeEl.bgColor ?? '#0d1117'}>
|
|
1390
1542
|
{#if codeEl.showHeader}
|
|
@@ -1441,7 +1593,7 @@
|
|
|
1441
1593
|
</div>
|
|
1442
1594
|
</div>
|
|
1443
1595
|
{:else if element.type === 'text'}
|
|
1444
|
-
{@const textEl = element as TextElement}
|
|
1596
|
+
{@const textEl = liveProps(element) as TextElement}
|
|
1445
1597
|
{@const animFontSize = animated.fontSize?.current ?? textEl.fontSize}
|
|
1446
1598
|
{@const typewriterState = textTypewriterState.get(element.id)}
|
|
1447
1599
|
{@const displayText = typewriterState?.isAnimating ? typewriterState.fullText.slice(0, typewriterState.displayedChars) : textEl.content}
|
|
@@ -1472,7 +1624,7 @@
|
|
|
1472
1624
|
{#if !isActionTextMode}{displayText}{#if typewriterState?.isAnimating}<span class="animot-typewriter-cursor">|</span>{/if}{/if}
|
|
1473
1625
|
</div>
|
|
1474
1626
|
{:else if element.type === 'arrow'}
|
|
1475
|
-
{@const arrowEl = element as ArrowElement}
|
|
1627
|
+
{@const arrowEl = liveProps(element) as ArrowElement}
|
|
1476
1628
|
{@const cp = arrowEl.controlPoints || []}
|
|
1477
1629
|
{@const pathD = cp.length === 0 ? `M ${arrowEl.startPoint.x} ${arrowEl.startPoint.y} L ${arrowEl.endPoint.x} ${arrowEl.endPoint.y}` : cp.length === 1 ? `M ${arrowEl.startPoint.x} ${arrowEl.startPoint.y} Q ${cp[0].x} ${cp[0].y} ${arrowEl.endPoint.x} ${arrowEl.endPoint.y}` : cp.length === 2 ? `M ${arrowEl.startPoint.x} ${arrowEl.startPoint.y} C ${cp[0].x} ${cp[0].y} ${cp[1].x} ${cp[1].y} ${arrowEl.endPoint.x} ${arrowEl.endPoint.y}` : buildCatmullRomPath(arrowEl.startPoint, cp, arrowEl.endPoint)}
|
|
1478
1630
|
{@const lastCp = cp.length > 0 ? cp[cp.length - 1] : arrowEl.startPoint}
|
|
@@ -1495,11 +1647,11 @@
|
|
|
1495
1647
|
{/if}
|
|
1496
1648
|
</svg>
|
|
1497
1649
|
{:else if element.type === 'image'}
|
|
1498
|
-
{@const imgEl = element as ImageElement}
|
|
1650
|
+
{@const imgEl = liveProps(element) as ImageElement}
|
|
1499
1651
|
{@const clipPath = imgEl.clipMask?.enabled ? (imgEl.clipMask.shapeType === 'circle' ? 'circle(50% at 50% 50%)' : imgEl.clipMask.shapeType === 'ellipse' ? 'ellipse(50% 50% at 50% 50%)' : imgEl.clipMask.shapeType === 'triangle' ? 'polygon(50% 0%, 0% 100%, 100% 100%)' : imgEl.clipMask.shapeType === 'star' ? 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)' : imgEl.clipMask.shapeType === 'hexagon' ? 'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)' : (imgEl.clipMask.borderRadius ?? 0) > 0 ? `inset(0 round ${imgEl.clipMask.borderRadius}px)` : 'none') : 'none'}
|
|
1500
1652
|
<img class="animot-image-element" src={imgEl.src} alt="" style:object-fit={imgEl.objectFit} style:border-radius="{imgEl.clipMask?.enabled ? 0 : imgEl.borderRadius}px" style:clip-path={clipPath} style:background-color={imgEl.backgroundColor ?? 'transparent'} />
|
|
1501
1653
|
{:else if element.type === 'video'}
|
|
1502
|
-
{@const videoEl = element as VideoElement}
|
|
1654
|
+
{@const videoEl = liveProps(element) as VideoElement}
|
|
1503
1655
|
{@const videoEmbed = parseEmbedUrl(videoEl.src)}
|
|
1504
1656
|
{#if videoEmbed}
|
|
1505
1657
|
<div class="animot-video-element animot-embed-wrap" style:border-radius="{videoEl.borderRadius}px" style:opacity={videoEl.opacity}>
|
|
@@ -1509,7 +1661,7 @@
|
|
|
1509
1661
|
<video class="animot-video-element" src={videoEl.src} poster={videoEl.posterImage} autoplay={videoEl.autoplay} loop={videoEl.loop} muted={videoEl.muted} controls={!!videoEl.showControls} playsinline preload="auto" style:object-fit={videoEl.objectFit} style:border-radius="{videoEl.borderRadius}px" style:opacity={videoEl.opacity}></video>
|
|
1510
1662
|
{/if}
|
|
1511
1663
|
{:else if element.type === 'shape'}
|
|
1512
|
-
{@const shapeEl = element as ShapeElement}
|
|
1664
|
+
{@const shapeEl = liveProps(element) as ShapeElement}
|
|
1513
1665
|
{@const animFill = animated.fillColor?.current ?? shapeEl.fillColor}
|
|
1514
1666
|
{@const animStroke = animated.strokeColor?.current ?? shapeEl.strokeColor}
|
|
1515
1667
|
{@const animStrokeWidth = animated.strokeWidth?.current ?? shapeEl.strokeWidth}
|
|
@@ -1517,15 +1669,15 @@
|
|
|
1517
1669
|
{@const morphProgress = animated.shapeMorph?.current ?? 1}
|
|
1518
1670
|
{@const effectiveShapeType = mState ? (morphProgress >= 1 ? mState.toType : (morphProgress <= 0 ? mState.fromType : null)) : shapeEl.shapeType}
|
|
1519
1671
|
{@const isMorphing = mState && morphProgress > 0 && morphProgress < 1}
|
|
1520
|
-
<svg class="animot-shape-element" viewBox="0 0 {animated.width.current} {animated.height.current}" fill-opacity={shapeEl.fillOpacity ?? 1} stroke-opacity={shapeEl.strokeOpacity ?? 1} style:filter={shapeEl.boxShadow?.enabled ? `drop-shadow(${shapeEl.boxShadow.offsetX}px ${shapeEl.boxShadow.offsetY}px ${shapeEl.boxShadow.blur}px ${shapeEl.boxShadow.color})` : 'none'}>
|
|
1672
|
+
<svg class="animot-shape-element" viewBox="0 0 {Math.max(0, animated.width.current)} {Math.max(0, animated.height.current)}" fill-opacity={shapeEl.fillOpacity ?? 1} stroke-opacity={shapeEl.strokeOpacity ?? 1} style:filter={shapeEl.boxShadow?.enabled ? `drop-shadow(${shapeEl.boxShadow.offsetX}px ${shapeEl.boxShadow.offsetY}px ${shapeEl.boxShadow.blur}px ${shapeEl.boxShadow.color})` : 'none'}>
|
|
1521
1673
|
{#if isMorphing}
|
|
1522
|
-
{@const w = animated.width.current}
|
|
1523
|
-
{@const h = animated.height.current}
|
|
1674
|
+
{@const w = Math.max(0, animated.width.current)}
|
|
1675
|
+
{@const h = Math.max(0, animated.height.current)}
|
|
1524
1676
|
{@const sw = animStrokeWidth}
|
|
1525
1677
|
<g style:opacity={1 - morphProgress}>{@html renderShape(mState!.fromType, w, h, animated.borderRadius.current, animFill, animStroke, sw, shapeEl.strokeStyle, shapeEl.strokeDashGap)}</g>
|
|
1526
1678
|
<g style:opacity={morphProgress}>{@html renderShape(mState!.toType, w, h, animated.borderRadius.current, animFill, animStroke, sw, shapeEl.strokeStyle, shapeEl.strokeDashGap)}</g>
|
|
1527
1679
|
{:else}
|
|
1528
|
-
{@html renderShape(effectiveShapeType ?? shapeEl.shapeType, animated.width.current, animated.height.current, animated.borderRadius.current, animFill, animStroke, animStrokeWidth, shapeEl.strokeStyle, shapeEl.strokeDashGap)}
|
|
1680
|
+
{@html renderShape(effectiveShapeType ?? shapeEl.shapeType, Math.max(0, animated.width.current), Math.max(0, animated.height.current), animated.borderRadius.current, animFill, animStroke, animStrokeWidth, shapeEl.strokeStyle, shapeEl.strokeDashGap)}
|
|
1529
1681
|
{/if}
|
|
1530
1682
|
</svg>
|
|
1531
1683
|
{:else if element.type === 'counter'}
|
|
@@ -1574,7 +1726,7 @@
|
|
|
1574
1726
|
{:else if element.type === 'motionPath'}
|
|
1575
1727
|
{@const mpEl = element as MotionPathElement}
|
|
1576
1728
|
{#if mpEl.showInPresentation}
|
|
1577
|
-
<svg width="100%" height="100%" viewBox="0 0 {animated.width.current} {animated.height.current}" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;">
|
|
1729
|
+
<svg width="100%" height="100%" viewBox="0 0 {Math.max(0, animated.width.current)} {Math.max(0, animated.height.current)}" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;">
|
|
1578
1730
|
<path d={buildPresenterPathD(mpEl.points, mpEl.closed)} stroke={mpEl.pathColor} stroke-width={mpEl.pathWidth} fill="none" stroke-dasharray="8 4" />
|
|
1579
1731
|
</svg>
|
|
1580
1732
|
{/if}
|
|
@@ -1643,6 +1795,8 @@
|
|
|
1643
1795
|
}
|
|
1644
1796
|
|
|
1645
1797
|
function renderShape(type: string, w: number, h: number, br: number, fill: string, stroke: string, sw: number, strokeStyle?: string, strokeDashGap?: number): string {
|
|
1798
|
+
const nn = (v: number) => (v > 0 ? v : 0);
|
|
1799
|
+
w = nn(w); h = nn(h); br = nn(br); sw = nn(sw);
|
|
1646
1800
|
let dashAttr = '';
|
|
1647
1801
|
if (strokeStyle && strokeStyle !== 'solid') {
|
|
1648
1802
|
const s = sw || 1;
|
|
@@ -1656,17 +1810,17 @@
|
|
|
1656
1810
|
return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr} stroke-linejoin="round"/>`;
|
|
1657
1811
|
};
|
|
1658
1812
|
switch (type) {
|
|
1659
|
-
case 'rectangle': return `<rect x="${sw/2}" y="${sw/2}" width="${w-sw}" height="${h-sw}" rx="${br}" ry="${br}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1660
|
-
case 'circle': return `<circle cx="${w/2}" cy="${h/2}" r="${Math.min(w,h)/2-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1661
|
-
case 'ellipse': return `<ellipse cx="${w/2}" cy="${h/2}" rx="${w/2-sw/2}" ry="${h/2-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1813
|
+
case 'rectangle': return `<rect x="${sw/2}" y="${sw/2}" width="${nn(w-sw)}" height="${nn(h-sw)}" rx="${nn(Math.min(br, (w-sw)/2, (h-sw)/2))}" ry="${nn(Math.min(br, (w-sw)/2, (h-sw)/2))}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1814
|
+
case 'circle': return `<circle cx="${w/2}" cy="${h/2}" r="${nn(Math.min(w,h)/2-sw/2)}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1815
|
+
case 'ellipse': return `<ellipse cx="${w/2}" cy="${h/2}" rx="${nn(w/2-sw/2)}" ry="${nn(h/2-sw/2)}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1662
1816
|
case 'triangle': return polyOrPath(`${w/2},${sw/2} ${sw/2},${h-sw/2} ${w-sw/2},${h-sw/2}`);
|
|
1663
1817
|
case 'star': {
|
|
1664
|
-
const cx = w/2, cy = h/2, outerR = Math.min(w,h)/2-sw/2, innerR = outerR*0.4;
|
|
1818
|
+
const cx = w/2, cy = h/2, outerR = nn(Math.min(w,h)/2-sw/2), innerR = outerR*0.4;
|
|
1665
1819
|
const pts = Array.from({length:10},(_,i)=>{const a=(i*Math.PI/5)-Math.PI/2;const r=i%2===0?outerR:innerR;return`${cx+r*Math.cos(a)},${cy+r*Math.sin(a)}`;}).join(' ');
|
|
1666
1820
|
return polyOrPath(pts);
|
|
1667
1821
|
}
|
|
1668
1822
|
case 'hexagon': {
|
|
1669
|
-
const cx = w/2, cy = h/2, r = Math.min(w,h)/2-sw/2;
|
|
1823
|
+
const cx = w/2, cy = h/2, r = nn(Math.min(w,h)/2-sw/2);
|
|
1670
1824
|
const pts = Array.from({length:6},(_,i)=>{const a=(i*Math.PI/3)-Math.PI/2;return`${cx+r*Math.cos(a)},${cy+r*Math.sin(a)}`;}).join(' ');
|
|
1671
1825
|
return polyOrPath(pts);
|
|
1672
1826
|
}
|