animot-presenter 0.5.21 → 0.5.22

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.
@@ -56,18 +56,15 @@
56
56
  }
57
57
 
58
58
  // Keyframe schedule loop — independent from motion-path / morph engines.
59
+ // Plain (non-reactive) state: no $state for the abort controller because
60
+ // the keyframe loop only retargets existing tweens and doesn't need to
61
+ // drive any reactive UI of its own. Adding reactive state here proved to
62
+ // crowd out the existing tween reactivity in 0.5.20.
59
63
  let keyframeLoopAbort: AbortController | null = null;
60
- let keyframeOverrides = $state<Map<string, Record<string, any>>>(new Map());
61
64
  function cancelKeyframeLoops() {
62
65
  if (keyframeLoopAbort) { keyframeLoopAbort.abort(); keyframeLoopAbort = null; }
63
66
  }
64
- function setKeyframeOverride(elementId: string, prop: string, value: any) {
65
- const cur = keyframeOverrides.get(elementId) ?? {};
66
- keyframeOverrides.set(elementId, { ...cur, [prop]: value });
67
- keyframeOverrides = new Map(keyframeOverrides);
68
- }
69
67
  function easingForTween(name: string | undefined): string {
70
- // Animotion tween easing accepts strings; pass-through with default.
71
68
  switch (name) {
72
69
  case 'linear': case 'ease-in': case 'ease-out': case 'ease-in-out': return name;
73
70
  case 'spring': return 'ease-out';
@@ -76,17 +73,19 @@
76
73
  }
77
74
  function animateKeyframes(slide: Slide) {
78
75
  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
+ const hasAnyKeyframes = slide.canvas.elements.some((el) => el.keyframes && el.keyframes.length > 0);
80
+ if (!hasAnyKeyframes) return;
79
81
  keyframeLoopAbort = new AbortController();
80
82
  const signal = keyframeLoopAbort.signal;
81
- if (keyframeOverrides.size > 0) keyframeOverrides = new Map();
82
83
  for (const element of slide.canvas.elements) {
83
84
  if (!element.keyframes || element.keyframes.length === 0) continue;
84
85
  const animated = animatedElements.get(element.id) as any;
85
86
  if (!animated) continue;
86
87
  const sorted = [...element.keyframes].sort((a, b) => a.time - b.time);
87
88
  const first = sorted[0];
88
-
89
- // Snap to KF1 instantly so the slide-displayed pose IS the start.
90
89
  if (first.position) { animated.x?.to(first.position.x, { duration: 0 }); animated.y?.to(first.position.y, { duration: 0 }); }
91
90
  if (first.size) { animated.width?.to(first.size.width, { duration: 0 }); animated.height?.to(first.size.height, { duration: 0 }); }
92
91
  if (first.rotation !== undefined) animated.rotation?.to(first.rotation, { duration: 0 });
@@ -105,11 +104,7 @@
105
104
  if (first.contrast !== undefined) animated.contrast?.to(first.contrast, { duration: 0 });
106
105
  if (first.saturate !== undefined) animated.saturate?.to(first.saturate, { duration: 0 });
107
106
  if (first.grayscale !== undefined) animated.grayscale?.to(first.grayscale, { duration: 0 });
108
- if (first.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', first.backgroundColor);
109
- if (first.color !== undefined) setKeyframeOverride(element.id, 'color', first.color);
110
-
111
107
  for (const kf of sorted.slice(1)) {
112
- const delay = Math.max(0, kf.time);
113
108
  setTimeout(() => {
114
109
  if (signal.aborted) return;
115
110
  const easing = easingForTween(kf.easing);
@@ -134,9 +129,7 @@
134
129
  if (kf.contrast !== undefined) animated.contrast?.to(kf.contrast, { duration: span, easing });
135
130
  if (kf.saturate !== undefined) animated.saturate?.to(kf.saturate, { duration: span, easing });
136
131
  if (kf.grayscale !== undefined) animated.grayscale?.to(kf.grayscale, { duration: span, easing });
137
- if (kf.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', kf.backgroundColor);
138
- if (kf.color !== undefined) setKeyframeOverride(element.id, 'color', kf.color);
139
- }, delay);
132
+ }, Math.max(0, kf.time));
140
133
  }
141
134
  }
142
135
  }
@@ -350,76 +343,8 @@
350
343
  return elements.map(e => e.id);
351
344
  });
352
345
 
353
- /**
354
- * Resolve an element's pose on a slide, taking per-element keyframes
355
- * into account. Keyframes turn the element's "effective state" into
356
- * something time-dependent — `pose='enter'` returns the FIRST keyframe's
357
- * state (used as morph TARGET / initial-tween-seed), `pose='exit'` the
358
- * LAST keyframe's (used as morph SOURCE on slide leave).
359
- */
360
- function getElementInSlide(slide: Slide | null, elementId: string, pose: 'enter' | 'exit' = 'enter'): CanvasElement | undefined {
361
- const el = slide?.canvas.elements.find(el => el.id === elementId);
362
- if (!el) return el;
363
- if (!el.keyframes || el.keyframes.length === 0) return el;
364
- const sorted = [...el.keyframes].sort((a, b) => a.time - b.time);
365
- const k = pose === 'exit' ? sorted[sorted.length - 1] : sorted[0];
366
- const out: any = { ...el };
367
- if (k.position) out.position = k.position;
368
- if (k.size) out.size = k.size;
369
- if (k.rotation !== undefined) out.rotation = k.rotation;
370
- if (k.opacity !== undefined) out.opacity = k.opacity;
371
- if (k.skewX !== undefined) out.skewX = k.skewX;
372
- if (k.skewY !== undefined) out.skewY = k.skewY;
373
- if (k.tiltX !== undefined) out.tiltX = k.tiltX;
374
- if (k.tiltY !== undefined) out.tiltY = k.tiltY;
375
- if (k.borderRadius !== undefined) out.borderRadius = k.borderRadius;
376
- if (k.fontSize !== undefined) out.fontSize = k.fontSize;
377
- if (k.fillColor !== undefined) out.fillColor = k.fillColor;
378
- if (k.strokeColor !== undefined) out.strokeColor = k.strokeColor;
379
- if (k.strokeWidth !== undefined) out.strokeWidth = k.strokeWidth;
380
- if (k.blur !== undefined) out.blur = k.blur;
381
- if (k.brightness !== undefined) out.brightness = k.brightness;
382
- if (k.contrast !== undefined) out.contrast = k.contrast;
383
- if (k.saturate !== undefined) out.saturate = k.saturate;
384
- if (k.grayscale !== undefined) out.grayscale = k.grayscale;
385
- return out as CanvasElement;
386
- }
387
-
388
- /**
389
- * Overlay the current LIVE tween values for properties that the renderer
390
- * reads directly from element data. Lets per-element keyframes drive
391
- * borderRadius / fontSize / shape colors / stroke width / CSS filters
392
- * at render time, plus non-tweened overrides (backgroundColor, color).
393
- */
394
- /**
395
- * Overlay tween-driven values for properties that the renderer reads
396
- * directly from element data (instead of via tween.current). Only
397
- * applies when this element has keyframes OR has an active override —
398
- * for everything else this is a passthrough so the existing slide-morph
399
- * render pipeline is left exactly as it was. Without this gate, every
400
- * render frame allocated a new copy and crowded out the tween reads.
401
- */
402
- function liveProps<T extends CanvasElement>(element: T): T {
403
- const hasKeyframes = !!element.keyframes && element.keyframes.length > 0;
404
- const overrides = keyframeOverrides.get(element.id);
405
- if (!hasKeyframes && !overrides) return element;
406
- const a = animatedElements.get(element.id) as any;
407
- const e = element as any;
408
- const out: any = { ...element };
409
- if (a && hasKeyframes) {
410
- if (a.borderRadius && e.borderRadius !== undefined) out.borderRadius = a.borderRadius.current;
411
- if (a.fontSize && e.fontSize !== undefined) out.fontSize = a.fontSize.current;
412
- if (a.fillColor && e.fillColor !== undefined) out.fillColor = a.fillColor.current;
413
- if (a.strokeColor && e.strokeColor !== undefined) out.strokeColor = a.strokeColor.current;
414
- if (a.strokeWidth && e.strokeWidth !== undefined) out.strokeWidth = a.strokeWidth.current;
415
- if (a.blur && e.blur !== undefined) out.blur = a.blur.current;
416
- if (a.brightness && e.brightness !== undefined) out.brightness = a.brightness.current;
417
- if (a.contrast && e.contrast !== undefined) out.contrast = a.contrast.current;
418
- if (a.saturate && e.saturate !== undefined) out.saturate = a.saturate.current;
419
- if (a.grayscale && e.grayscale !== undefined) out.grayscale = a.grayscale.current;
420
- }
421
- if (overrides) Object.assign(out, overrides);
422
- return out as T;
346
+ function getElementInSlide(slide: Slide | null, elementId: string): CanvasElement | undefined {
347
+ return slide?.canvas.elements.find(el => el.id === elementId);
423
348
  }
424
349
 
425
350
  // Typewriter
@@ -853,9 +778,7 @@
853
778
  // Per-element morphing (transition type = 'none')
854
779
  const animations: Promise<void>[] = [];
855
780
  for (const elementId of allElementIds) {
856
- // FROM-sync: use the EXIT pose (last keyframe) so morph starts
857
- // from where the keyframe loop left the element on screen.
858
- const currentEl = getElementInSlide(currentSlide, elementId, 'exit');
781
+ const currentEl = getElementInSlide(currentSlide, elementId);
859
782
  const animated = animatedElements.get(elementId);
860
783
  if (!animated) continue;
861
784
  if (currentEl) {
@@ -916,8 +839,8 @@
916
839
  const animationTasks: AnimationTask[] = [];
917
840
 
918
841
  for (const elementId of allElementIds) {
919
- const currentEl = getElementInSlide(currentSlide, elementId, 'exit');
920
- const targetEl = getElementInSlide(targetSlide, elementId, 'enter');
842
+ const currentEl = getElementInSlide(currentSlide, elementId);
843
+ const targetEl = getElementInSlide(targetSlide, elementId);
921
844
  const animated = animatedElements.get(elementId);
922
845
  if (!animated) continue;
923
846
  const animConfig = targetEl?.animationConfig || currentEl?.animationConfig;
@@ -1461,7 +1384,7 @@
1461
1384
  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) : ''}` }}
1462
1385
  >
1463
1386
  {#if element.type === 'code'}
1464
- {@const codeEl = liveProps(element) as CodeElement}
1387
+ {@const codeEl = element as CodeElement}
1465
1388
  {@const morphState = codeMorphState.get(codeEl.id)}
1466
1389
  <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'}>
1467
1390
  {#if codeEl.showHeader}
@@ -1518,7 +1441,7 @@
1518
1441
  </div>
1519
1442
  </div>
1520
1443
  {:else if element.type === 'text'}
1521
- {@const textEl = liveProps(element) as TextElement}
1444
+ {@const textEl = element as TextElement}
1522
1445
  {@const animFontSize = animated.fontSize?.current ?? textEl.fontSize}
1523
1446
  {@const typewriterState = textTypewriterState.get(element.id)}
1524
1447
  {@const displayText = typewriterState?.isAnimating ? typewriterState.fullText.slice(0, typewriterState.displayedChars) : textEl.content}
@@ -1549,7 +1472,7 @@
1549
1472
  {#if !isActionTextMode}{displayText}{#if typewriterState?.isAnimating}<span class="animot-typewriter-cursor">|</span>{/if}{/if}
1550
1473
  </div>
1551
1474
  {:else if element.type === 'arrow'}
1552
- {@const arrowEl = liveProps(element) as ArrowElement}
1475
+ {@const arrowEl = element as ArrowElement}
1553
1476
  {@const cp = arrowEl.controlPoints || []}
1554
1477
  {@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)}
1555
1478
  {@const lastCp = cp.length > 0 ? cp[cp.length - 1] : arrowEl.startPoint}
@@ -1572,11 +1495,11 @@
1572
1495
  {/if}
1573
1496
  </svg>
1574
1497
  {:else if element.type === 'image'}
1575
- {@const imgEl = liveProps(element) as ImageElement}
1498
+ {@const imgEl = element as ImageElement}
1576
1499
  {@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'}
1577
1500
  <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'} />
1578
1501
  {:else if element.type === 'video'}
1579
- {@const videoEl = liveProps(element) as VideoElement}
1502
+ {@const videoEl = element as VideoElement}
1580
1503
  {@const videoEmbed = parseEmbedUrl(videoEl.src)}
1581
1504
  {#if videoEmbed}
1582
1505
  <div class="animot-video-element animot-embed-wrap" style:border-radius="{videoEl.borderRadius}px" style:opacity={videoEl.opacity}>
@@ -1586,7 +1509,7 @@
1586
1509
  <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>
1587
1510
  {/if}
1588
1511
  {:else if element.type === 'shape'}
1589
- {@const shapeEl = liveProps(element) as ShapeElement}
1512
+ {@const shapeEl = element as ShapeElement}
1590
1513
  {@const animFill = animated.fillColor?.current ?? shapeEl.fillColor}
1591
1514
  {@const animStroke = animated.strokeColor?.current ?? shapeEl.strokeColor}
1592
1515
  {@const animStrokeWidth = animated.strokeWidth?.current ?? shapeEl.strokeWidth}