animot-presenter 0.5.22 → 0.5.23
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 +116 -40
- package/dist/cdn/animot-presenter.esm.js +4476 -4471
- package/dist/cdn/animot-presenter.min.js +8 -8
- package/package.json +2 -1
|
@@ -55,28 +55,35 @@
|
|
|
55
55
|
if (motionPathLoopAbort) { motionPathLoopAbort.abort(); motionPathLoopAbort = null; }
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Keyframe schedule loop
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// crowd out the existing tween reactivity in 0.5.20.
|
|
58
|
+
// Keyframe schedule loop. Plain (non-reactive) module-scope state — adding
|
|
59
|
+
// $state for keyframe overrides in 0.5.20 broke reactivity for the existing
|
|
60
|
+
// tween-driven render. This implementation only retargets existing tweens
|
|
61
|
+
// via setTimeout; nothing here is reactive, nothing is read from render.
|
|
63
62
|
let keyframeLoopAbort: AbortController | null = null;
|
|
63
|
+
// Per-element overrides for keyframe-driven props that aren't tweened
|
|
64
|
+
// (backgroundColor, text color). The schedule writes these at each
|
|
65
|
+
// keyframe boundary; liveProps reads them at render time.
|
|
66
|
+
let keyframeOverrides = $state<Map<string, Record<string, any>>>(new Map());
|
|
64
67
|
function cancelKeyframeLoops() {
|
|
65
68
|
if (keyframeLoopAbort) { keyframeLoopAbort.abort(); keyframeLoopAbort = null; }
|
|
66
69
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
function setKeyframeOverride(elementId: string, prop: string, value: any) {
|
|
71
|
+
const cur = keyframeOverrides.get(elementId) ?? {};
|
|
72
|
+
keyframeOverrides.set(elementId, { ...cur, [prop]: value });
|
|
73
|
+
keyframeOverrides = new Map(keyframeOverrides);
|
|
74
|
+
}
|
|
75
|
+
// Tweens take an easing FUNCTION (t→number), not a CSS keyword. Returning
|
|
76
|
+
// a string here was the cause of "r is not a function" thrown deep in the
|
|
77
|
+
// tween animation loop — `r(t)` crashed because `r` was the literal string
|
|
78
|
+
// passed in. Reuse the engine's `getEasingFn` so keyframe easing matches
|
|
79
|
+
// the slide-morph engine's vocabulary.
|
|
80
|
+
function easingForTween(name: string | undefined): (t: number) => number {
|
|
81
|
+
return getEasingFn(name ?? 'ease-out');
|
|
73
82
|
}
|
|
74
83
|
function animateKeyframes(slide: Slide) {
|
|
75
84
|
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
85
|
const hasAnyKeyframes = slide.canvas.elements.some((el) => el.keyframes && el.keyframes.length > 0);
|
|
86
|
+
if (keyframeOverrides.size > 0) keyframeOverrides = new Map();
|
|
80
87
|
if (!hasAnyKeyframes) return;
|
|
81
88
|
keyframeLoopAbort = new AbortController();
|
|
82
89
|
const signal = keyframeLoopAbort.signal;
|
|
@@ -86,6 +93,7 @@
|
|
|
86
93
|
if (!animated) continue;
|
|
87
94
|
const sorted = [...element.keyframes].sort((a, b) => a.time - b.time);
|
|
88
95
|
const first = sorted[0];
|
|
96
|
+
// Snap tweens to KF1 instantly so the slide-displayed pose IS the start.
|
|
89
97
|
if (first.position) { animated.x?.to(first.position.x, { duration: 0 }); animated.y?.to(first.position.y, { duration: 0 }); }
|
|
90
98
|
if (first.size) { animated.width?.to(first.size.width, { duration: 0 }); animated.height?.to(first.size.height, { duration: 0 }); }
|
|
91
99
|
if (first.rotation !== undefined) animated.rotation?.to(first.rotation, { duration: 0 });
|
|
@@ -104,6 +112,8 @@
|
|
|
104
112
|
if (first.contrast !== undefined) animated.contrast?.to(first.contrast, { duration: 0 });
|
|
105
113
|
if (first.saturate !== undefined) animated.saturate?.to(first.saturate, { duration: 0 });
|
|
106
114
|
if (first.grayscale !== undefined) animated.grayscale?.to(first.grayscale, { duration: 0 });
|
|
115
|
+
if (first.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', first.backgroundColor);
|
|
116
|
+
if (first.color !== undefined) setKeyframeOverride(element.id, 'color', first.color);
|
|
107
117
|
for (const kf of sorted.slice(1)) {
|
|
108
118
|
setTimeout(() => {
|
|
109
119
|
if (signal.aborted) return;
|
|
@@ -129,6 +139,8 @@
|
|
|
129
139
|
if (kf.contrast !== undefined) animated.contrast?.to(kf.contrast, { duration: span, easing });
|
|
130
140
|
if (kf.saturate !== undefined) animated.saturate?.to(kf.saturate, { duration: span, easing });
|
|
131
141
|
if (kf.grayscale !== undefined) animated.grayscale?.to(kf.grayscale, { duration: span, easing });
|
|
142
|
+
if (kf.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', kf.backgroundColor);
|
|
143
|
+
if (kf.color !== undefined) setKeyframeOverride(element.id, 'color', kf.color);
|
|
132
144
|
}, Math.max(0, kf.time));
|
|
133
145
|
}
|
|
134
146
|
}
|
|
@@ -347,6 +359,35 @@
|
|
|
347
359
|
return slide?.canvas.elements.find(el => el.id === elementId);
|
|
348
360
|
}
|
|
349
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Overlay tween-driven values + non-tweened keyframe overrides onto the
|
|
364
|
+
* element used in render. Gated: returns the element unchanged when it
|
|
365
|
+
* has no keyframes AND no active override — that's the 99% case and
|
|
366
|
+
* keeps the existing slide-morph render pipeline allocation-free.
|
|
367
|
+
*/
|
|
368
|
+
function liveProps<T extends CanvasElement>(element: T): T {
|
|
369
|
+
const hasKeyframes = !!element.keyframes && element.keyframes.length > 0;
|
|
370
|
+
const overrides = keyframeOverrides.get(element.id);
|
|
371
|
+
if (!hasKeyframes && !overrides) return element;
|
|
372
|
+
const a = animatedElements.get(element.id) as any;
|
|
373
|
+
const e = element as any;
|
|
374
|
+
const out: any = { ...element };
|
|
375
|
+
if (a && hasKeyframes) {
|
|
376
|
+
if (a.borderRadius && e.borderRadius !== undefined) out.borderRadius = a.borderRadius.current;
|
|
377
|
+
if (a.fontSize && e.fontSize !== undefined) out.fontSize = a.fontSize.current;
|
|
378
|
+
if (a.fillColor && e.fillColor !== undefined) out.fillColor = a.fillColor.current;
|
|
379
|
+
if (a.strokeColor && e.strokeColor !== undefined) out.strokeColor = a.strokeColor.current;
|
|
380
|
+
if (a.strokeWidth && e.strokeWidth !== undefined) out.strokeWidth = a.strokeWidth.current;
|
|
381
|
+
if (a.blur && e.blur !== undefined) out.blur = a.blur.current;
|
|
382
|
+
if (a.brightness && e.brightness !== undefined) out.brightness = a.brightness.current;
|
|
383
|
+
if (a.contrast && e.contrast !== undefined) out.contrast = a.contrast.current;
|
|
384
|
+
if (a.saturate && e.saturate !== undefined) out.saturate = a.saturate.current;
|
|
385
|
+
if (a.grayscale && e.grayscale !== undefined) out.grayscale = a.grayscale.current;
|
|
386
|
+
}
|
|
387
|
+
if (overrides) Object.assign(out, overrides);
|
|
388
|
+
return out as T;
|
|
389
|
+
}
|
|
390
|
+
|
|
350
391
|
// Typewriter
|
|
351
392
|
function startTypewriterAnimation(elementId: string, fullText: string, speed: number) {
|
|
352
393
|
const existing = typewriterIntervals.get(elementId);
|
|
@@ -487,36 +528,71 @@
|
|
|
487
528
|
for (const element of slide.canvas.elements) {
|
|
488
529
|
if (!animatedElements.has(element.id)) {
|
|
489
530
|
const inCurrent = getElementInSlide(currentSlide, element.id);
|
|
490
|
-
|
|
531
|
+
let startOpacity = inCurrent ? ((inCurrent as any).opacity ?? 1) : 0;
|
|
491
532
|
const br = (element as any).borderRadius ?? 0;
|
|
492
533
|
const isShape = element.type === 'shape';
|
|
493
534
|
const shapeEl = isShape ? element as ShapeElement : null;
|
|
494
535
|
const isText = element.type === 'text';
|
|
495
536
|
const textEl = isText ? element as TextElement : null;
|
|
537
|
+
|
|
538
|
+
// If the element has keyframes, seed each tween with the FIRST
|
|
539
|
+
// keyframe's value instead of the persisted (last-captured)
|
|
540
|
+
// state. The persisted state is whatever was on the canvas
|
|
541
|
+
// at the moment the user captured their final keyframe; using
|
|
542
|
+
// it as the tween's starting value would cause the slide to
|
|
543
|
+
// flash at the "wrong" pose before the keyframe schedule
|
|
544
|
+
// snaps it to KF1. Doing it here also avoids depending on a
|
|
545
|
+
// duration:0 snap that the tween library may not apply
|
|
546
|
+
// synchronously.
|
|
547
|
+
const sortedKfs = element.keyframes && element.keyframes.length > 0
|
|
548
|
+
? [...element.keyframes].sort((a, b) => a.time - b.time)
|
|
549
|
+
: null;
|
|
550
|
+
const k0 = sortedKfs ? sortedKfs[0] : null;
|
|
551
|
+
const seedX = k0?.position ? k0.position.x : element.position.x;
|
|
552
|
+
const seedY = k0?.position ? k0.position.y : element.position.y;
|
|
553
|
+
const seedW = k0?.size ? k0.size.width : element.size.width;
|
|
554
|
+
const seedH = k0?.size ? k0.size.height : element.size.height;
|
|
555
|
+
const seedRot = k0?.rotation !== undefined ? k0.rotation : element.rotation;
|
|
556
|
+
const seedSkewX = k0?.skewX !== undefined ? k0.skewX : (element.skewX ?? 0);
|
|
557
|
+
const seedSkewY = k0?.skewY !== undefined ? k0.skewY : (element.skewY ?? 0);
|
|
558
|
+
const seedTiltX = k0?.tiltX !== undefined ? k0.tiltX : (element.tiltX ?? 0);
|
|
559
|
+
const seedTiltY = k0?.tiltY !== undefined ? k0.tiltY : (element.tiltY ?? 0);
|
|
560
|
+
if (k0?.opacity !== undefined) startOpacity = k0.opacity;
|
|
561
|
+
const seedBR = k0?.borderRadius !== undefined ? k0.borderRadius : br;
|
|
562
|
+
const seedFontSize = k0?.fontSize !== undefined ? k0.fontSize : (textEl ? textEl.fontSize : 0);
|
|
563
|
+
const seedFill = k0?.fillColor !== undefined ? k0.fillColor : (shapeEl ? shapeEl.fillColor : '');
|
|
564
|
+
const seedStroke = k0?.strokeColor !== undefined ? k0.strokeColor : (shapeEl ? shapeEl.strokeColor : '');
|
|
565
|
+
const seedStrokeW = k0?.strokeWidth !== undefined ? k0.strokeWidth : (shapeEl ? shapeEl.strokeWidth : 0);
|
|
566
|
+
const seedBlur = k0?.blur !== undefined ? k0.blur : (element.blur ?? 0);
|
|
567
|
+
const seedBright = k0?.brightness !== undefined ? k0.brightness : (element.brightness ?? 100);
|
|
568
|
+
const seedContrast = k0?.contrast !== undefined ? k0.contrast : (element.contrast ?? 100);
|
|
569
|
+
const seedSat = k0?.saturate !== undefined ? k0.saturate : (element.saturate ?? 100);
|
|
570
|
+
const seedGray = k0?.grayscale !== undefined ? k0.grayscale : (element.grayscale ?? 0);
|
|
571
|
+
|
|
496
572
|
animatedElements.set(element.id, {
|
|
497
|
-
x: tween(
|
|
498
|
-
y: tween(
|
|
499
|
-
width: tween(
|
|
500
|
-
height: tween(
|
|
501
|
-
rotation: tween(
|
|
502
|
-
skewX: tween(
|
|
503
|
-
skewY: tween(
|
|
504
|
-
tiltX: tween(
|
|
505
|
-
tiltY: tween(
|
|
573
|
+
x: tween(seedX, { duration: 500 }),
|
|
574
|
+
y: tween(seedY, { duration: 500 }),
|
|
575
|
+
width: tween(seedW, { duration: 500 }),
|
|
576
|
+
height: tween(seedH, { duration: 500 }),
|
|
577
|
+
rotation: tween(seedRot, { duration: 500 }),
|
|
578
|
+
skewX: tween(seedSkewX, { duration: 500 }),
|
|
579
|
+
skewY: tween(seedSkewY, { duration: 500 }),
|
|
580
|
+
tiltX: tween(seedTiltX, { duration: 500 }),
|
|
581
|
+
tiltY: tween(seedTiltY, { duration: 500 }),
|
|
506
582
|
perspective: tween(element.perspective ?? 1000, { duration: 500 }),
|
|
507
583
|
opacity: tween(startOpacity, { duration: 300 }),
|
|
508
|
-
borderRadius: tween(
|
|
509
|
-
fontSize: textEl ? tween(
|
|
510
|
-
fillColor: shapeEl ? tween(
|
|
511
|
-
strokeColor: shapeEl ? tween(
|
|
512
|
-
strokeWidth: shapeEl ? tween(
|
|
584
|
+
borderRadius: tween(seedBR, { duration: 500 }),
|
|
585
|
+
fontSize: textEl ? tween(seedFontSize, { duration: 500 }) : null,
|
|
586
|
+
fillColor: shapeEl ? tween(seedFill, { duration: 500 }) : null,
|
|
587
|
+
strokeColor: shapeEl ? tween(seedStroke, { duration: 500 }) : null,
|
|
588
|
+
strokeWidth: shapeEl ? tween(seedStrokeW, { duration: 500 }) : null,
|
|
513
589
|
shapeMorph: shapeEl ? tween(1, { duration: 500 }) : null,
|
|
514
590
|
motionPathProgress: element.motionPathConfig ? tween(0, { duration: 500 }) : null,
|
|
515
|
-
blur: tween(
|
|
516
|
-
brightness: tween(
|
|
517
|
-
contrast: tween(
|
|
518
|
-
saturate: tween(
|
|
519
|
-
grayscale: tween(
|
|
591
|
+
blur: tween(seedBlur, { duration: 500 }),
|
|
592
|
+
brightness: tween(seedBright, { duration: 500 }),
|
|
593
|
+
contrast: tween(seedContrast, { duration: 500 }),
|
|
594
|
+
saturate: tween(seedSat, { duration: 500 }),
|
|
595
|
+
grayscale: tween(seedGray, { duration: 500 })
|
|
520
596
|
});
|
|
521
597
|
const currentSlideEl = getElementInSlide(currentSlide, element.id);
|
|
522
598
|
elementContent.set(element.id, JSON.parse(JSON.stringify(currentSlideEl || element)));
|
|
@@ -1384,7 +1460,7 @@
|
|
|
1384
1460
|
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
1461
|
>
|
|
1386
1462
|
{#if element.type === 'code'}
|
|
1387
|
-
{@const codeEl = element as CodeElement}
|
|
1463
|
+
{@const codeEl = liveProps(element) as CodeElement}
|
|
1388
1464
|
{@const morphState = codeMorphState.get(codeEl.id)}
|
|
1389
1465
|
<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
1466
|
{#if codeEl.showHeader}
|
|
@@ -1441,7 +1517,7 @@
|
|
|
1441
1517
|
</div>
|
|
1442
1518
|
</div>
|
|
1443
1519
|
{:else if element.type === 'text'}
|
|
1444
|
-
{@const textEl = element as TextElement}
|
|
1520
|
+
{@const textEl = liveProps(element) as TextElement}
|
|
1445
1521
|
{@const animFontSize = animated.fontSize?.current ?? textEl.fontSize}
|
|
1446
1522
|
{@const typewriterState = textTypewriterState.get(element.id)}
|
|
1447
1523
|
{@const displayText = typewriterState?.isAnimating ? typewriterState.fullText.slice(0, typewriterState.displayedChars) : textEl.content}
|
|
@@ -1472,7 +1548,7 @@
|
|
|
1472
1548
|
{#if !isActionTextMode}{displayText}{#if typewriterState?.isAnimating}<span class="animot-typewriter-cursor">|</span>{/if}{/if}
|
|
1473
1549
|
</div>
|
|
1474
1550
|
{:else if element.type === 'arrow'}
|
|
1475
|
-
{@const arrowEl = element as ArrowElement}
|
|
1551
|
+
{@const arrowEl = liveProps(element) as ArrowElement}
|
|
1476
1552
|
{@const cp = arrowEl.controlPoints || []}
|
|
1477
1553
|
{@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
1554
|
{@const lastCp = cp.length > 0 ? cp[cp.length - 1] : arrowEl.startPoint}
|
|
@@ -1495,11 +1571,11 @@
|
|
|
1495
1571
|
{/if}
|
|
1496
1572
|
</svg>
|
|
1497
1573
|
{:else if element.type === 'image'}
|
|
1498
|
-
{@const imgEl = element as ImageElement}
|
|
1574
|
+
{@const imgEl = liveProps(element) as ImageElement}
|
|
1499
1575
|
{@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
1576
|
<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
1577
|
{:else if element.type === 'video'}
|
|
1502
|
-
{@const videoEl = element as VideoElement}
|
|
1578
|
+
{@const videoEl = liveProps(element) as VideoElement}
|
|
1503
1579
|
{@const videoEmbed = parseEmbedUrl(videoEl.src)}
|
|
1504
1580
|
{#if videoEmbed}
|
|
1505
1581
|
<div class="animot-video-element animot-embed-wrap" style:border-radius="{videoEl.borderRadius}px" style:opacity={videoEl.opacity}>
|
|
@@ -1509,7 +1585,7 @@
|
|
|
1509
1585
|
<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
1586
|
{/if}
|
|
1511
1587
|
{:else if element.type === 'shape'}
|
|
1512
|
-
{@const shapeEl = element as ShapeElement}
|
|
1588
|
+
{@const shapeEl = liveProps(element) as ShapeElement}
|
|
1513
1589
|
{@const animFill = animated.fillColor?.current ?? shapeEl.fillColor}
|
|
1514
1590
|
{@const animStroke = animated.strokeColor?.current ?? shapeEl.strokeColor}
|
|
1515
1591
|
{@const animStrokeWidth = animated.strokeWidth?.current ?? shapeEl.strokeWidth}
|