animot-presenter 0.2.7 → 0.2.9

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.
@@ -100,10 +100,23 @@
100
100
  for (const seg of segs) {
101
101
  if (accum + seg.length >= targetLength || seg === segs[segs.length - 1]) {
102
102
  const t = Math.max(0, Math.min(1, seg.length > 0 ? (targetLength - accum) / seg.length : 0));
103
+ let dx = cubicBezDeriv(seg.p0x, seg.p1x, seg.p2x, seg.p3x, t);
104
+ let dy = cubicBezDeriv(seg.p0y, seg.p1y, seg.p2y, seg.p3y, t);
105
+ // Degenerate tangent at endpoints (no Bezier handles) — sample nearby
106
+ if (Math.abs(dx) < 0.001 && Math.abs(dy) < 0.001) {
107
+ const epsilon = t < 0.5 ? 0.01 : -0.01;
108
+ dx = cubicBezDeriv(seg.p0x, seg.p1x, seg.p2x, seg.p3x, t + epsilon);
109
+ dy = cubicBezDeriv(seg.p0y, seg.p1y, seg.p2y, seg.p3y, t + epsilon);
110
+ }
111
+ // Still zero (fully degenerate segment) — use chord direction
112
+ if (Math.abs(dx) < 0.001 && Math.abs(dy) < 0.001) {
113
+ dx = seg.p3x - seg.p0x;
114
+ dy = seg.p3y - seg.p0y;
115
+ }
103
116
  return {
104
117
  x: cubicBez(seg.p0x, seg.p1x, seg.p2x, seg.p3x, t),
105
118
  y: cubicBez(seg.p0y, seg.p1y, seg.p2y, seg.p3y, t),
106
- angle: Math.atan2(cubicBezDeriv(seg.p0y, seg.p1y, seg.p2y, seg.p3y, t), cubicBezDeriv(seg.p0x, seg.p1x, seg.p2x, seg.p3x, t)) * (180 / Math.PI)
119
+ angle: Math.atan2(dy, dx) * (180 / Math.PI)
107
120
  };
108
121
  }
109
122
  accum += seg.length;
@@ -114,8 +127,12 @@
114
127
  function computeMotionPathPosition(
115
128
  mpPoint: { x: number; y: number; angle: number },
116
129
  startPoint: { x: number; y: number; angle: number },
117
- animX: number, animY: number, animW: number, animH: number
130
+ animX: number, animY: number, animW: number, animH: number,
131
+ closed: boolean
118
132
  ): { x: number; y: number } {
133
+ if (!closed) {
134
+ return { x: mpPoint.x - animW / 2, y: mpPoint.y - animH / 2 };
135
+ }
119
136
  const offsetX = (animX + animW / 2) - startPoint.x;
120
137
  const offsetY = (animY + animH / 2) - startPoint.y;
121
138
  const angleDelta = (mpPoint.angle - startPoint.angle) * Math.PI / 180;
@@ -416,6 +433,86 @@
416
433
  }
417
434
  }
418
435
 
436
+ // Reset presentation to first slide (snap all elements back to initial state)
437
+ async function resetToFirstSlide() {
438
+ if (isTransitioning) return;
439
+ isTransitioning = true;
440
+ clearAllTypewriterAnimations();
441
+ cancelMotionPathLoops();
442
+ const firstSlide = slides[0];
443
+ if (!firstSlide) { isTransitioning = false; return; }
444
+
445
+ for (const elementId of allElementIds) {
446
+ const targetEl = getElementInSlide(firstSlide, elementId);
447
+ const animated = animatedElements.get(elementId);
448
+ if (!animated) continue;
449
+ if (targetEl) {
450
+ animated.x.to(targetEl.position.x, { duration: 0 }); animated.y.to(targetEl.position.y, { duration: 0 });
451
+ animated.width.to(targetEl.size.width, { duration: 0 }); animated.height.to(targetEl.size.height, { duration: 0 });
452
+ animated.rotation.to(targetEl.rotation, { duration: 0 });
453
+ animated.skewX.to(targetEl.skewX ?? 0, { duration: 0 }); animated.skewY.to(targetEl.skewY ?? 0, { duration: 0 });
454
+ animated.tiltX.to(targetEl.tiltX ?? 0, { duration: 0 }); animated.tiltY.to(targetEl.tiltY ?? 0, { duration: 0 });
455
+ animated.perspective.to(targetEl.perspective ?? 1000, { duration: 0 });
456
+ animated.opacity.to((targetEl as any).opacity ?? 1, { duration: 0 });
457
+ animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: 0 });
458
+ animated.blur.to(targetEl.blur ?? 0, { duration: 0 });
459
+ animated.brightness.to(targetEl.brightness ?? 100, { duration: 0 });
460
+ animated.contrast.to(targetEl.contrast ?? 100, { duration: 0 });
461
+ animated.saturate.to(targetEl.saturate ?? 100, { duration: 0 });
462
+ animated.grayscale.to(targetEl.grayscale ?? 0, { duration: 0 });
463
+ if (targetEl.type === 'text' && animated.fontSize) animated.fontSize.to((targetEl as TextElement).fontSize, { duration: 0 });
464
+ if (targetEl.type === 'shape') {
465
+ const s = targetEl as ShapeElement;
466
+ if (animated.fillColor) animated.fillColor.to(s.fillColor, { duration: 0 });
467
+ if (animated.strokeColor) animated.strokeColor.to(s.strokeColor, { duration: 0 });
468
+ if (animated.strokeWidth) animated.strokeWidth.to(s.strokeWidth, { duration: 0 });
469
+ }
470
+ if (animated.motionPathProgress) animated.motionPathProgress.to(0, { duration: 0 });
471
+ } else {
472
+ animated.opacity.to(0, { duration: 0 });
473
+ }
474
+ }
475
+
476
+ for (const elementId of allElementIds) {
477
+ const targetEl = getElementInSlide(firstSlide, elementId);
478
+ if (targetEl) elementContent.set(elementId, JSON.parse(JSON.stringify(targetEl)));
479
+ }
480
+
481
+ const newPreviousCodeContent = new Map<string, string>();
482
+ for (const element of firstSlide.canvas.elements) {
483
+ if (element.type === 'code') newPreviousCodeContent.set(element.id, (element as CodeElement).code);
484
+ }
485
+
486
+ for (const element of firstSlide.canvas.elements) {
487
+ if (element.type === 'code') {
488
+ const codeEl = element as CodeElement;
489
+ const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
490
+ if (!codeHighlights.has(key)) {
491
+ const html = await highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers });
492
+ codeHighlights.set(key, html);
493
+ }
494
+ }
495
+ }
496
+ codeHighlights = new Map(codeHighlights);
497
+
498
+ codeMorphState = new Map();
499
+ previousCodeContent = newPreviousCodeContent;
500
+ shapeMorphStates = new Map();
501
+ elementContent = new Map(elementContent);
502
+ currentSlideIndex = 0;
503
+ isTransitioning = false;
504
+
505
+ for (const element of firstSlide.canvas.elements) {
506
+ if (element.type === 'text') {
507
+ const textEl = element as TextElement;
508
+ if (textEl.animation?.mode === 'typewriter') startTypewriterAnimation(element.id, textEl.content, textEl.animation.typewriterSpeed || 50);
509
+ }
510
+ }
511
+
512
+ animateMotionPaths(firstSlide);
513
+ onslidechange?.(0, slides.length);
514
+ }
515
+
419
516
  // Animate to slide
420
517
  async function animateToSlide(targetIndex: number) {
421
518
  if (isTransitioning || targetIndex < 0 || targetIndex >= slides.length) return;
@@ -811,17 +908,34 @@
811
908
  const slideDuration = durationOverride ?? currentSlide?.duration ?? 3000;
812
909
  autoplayTimer = setTimeout(() => {
813
910
  if (currentSlideIndex < slides.length - 1) animateToSlide(currentSlideIndex + 1);
814
- else if (loop) animateToSlide(0);
911
+ else if (loop) {
912
+ const loopMode = project?.settings?.loopMode ?? 'reset';
913
+ if (loopMode === 'transition') animateToSlide(0);
914
+ else resetToFirstSlide();
915
+ }
815
916
  else isAutoplay = false;
816
917
  }, slideDuration);
817
918
  }
818
919
  $effect(() => { if (isAutoplay && !isTransitioning) scheduleNextSlide(); });
819
920
  $effect(() => () => clearAutoplayTimer());
820
921
 
922
+ function handleNextSlide() {
923
+ if (currentSlideIndex < slides.length - 1) {
924
+ animateToSlide(currentSlideIndex + 1);
925
+ } else if (loop) {
926
+ const loopMode = project?.settings?.loopMode ?? 'reset';
927
+ if (loopMode === 'transition') {
928
+ animateToSlide(0);
929
+ } else {
930
+ resetToFirstSlide();
931
+ }
932
+ }
933
+ }
934
+
821
935
  // Keyboard
822
936
  function handleKeyDown(e: KeyboardEvent) {
823
937
  if (!keyboard) return;
824
- if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'Enter') { e.preventDefault(); animateToSlide(currentSlideIndex + 1); }
938
+ if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'Enter') { e.preventDefault(); handleNextSlide(); }
825
939
  else if (e.key === 'ArrowLeft' || e.key === 'Backspace') { e.preventDefault(); animateToSlide(currentSlideIndex - 1); }
826
940
  else if (e.key === 'Home') animateToSlide(0);
827
941
  else if (e.key === 'End') animateToSlide(slides.length - 1);
@@ -867,7 +981,7 @@
867
981
 
868
982
  // Public API (exposed via bind:this)
869
983
  export async function goto(slideIndex: number) { await animateToSlide(slideIndex); }
870
- export async function next() { await animateToSlide(currentSlideIndex + 1); }
984
+ export async function next() { handleNextSlide(); }
871
985
  export async function prev() { await animateToSlide(currentSlideIndex - 1); }
872
986
  export function play() { isAutoplay = true; }
873
987
  export function pause() { isAutoplay = false; clearAutoplayTimer(); }
@@ -1017,10 +1131,11 @@
1017
1131
  {@const mpProgress = animated?.motionPathProgress?.current ?? 0}
1018
1132
  {@const mpPoint = mpElement && mpConfig ? getPresenterPointOnPath(mpElement.points, mpElement.closed, (mpConfig.startPercent + (mpConfig.endPercent - mpConfig.startPercent) * mpProgress) / 100) : null}
1019
1133
  {@const mpStartPoint = mpElement && mpConfig ? getPresenterPointOnPath(mpElement.points, mpElement.closed, mpConfig.startPercent / 100) : null}
1020
- {@const mpPos = mpPoint && mpStartPoint && animated
1134
+ {@const mpPos = mpPoint && mpStartPoint && animated && mpElement
1021
1135
  ? computeMotionPathPosition(mpPoint, mpStartPoint,
1022
1136
  animated.x.current, animated.y.current,
1023
- animated.width.current, animated.height.current)
1137
+ animated.width.current, animated.height.current,
1138
+ mpElement.closed)
1024
1139
  : null}
1025
1140
  {@const elemX = mpPos ? mpPos.x : (animated?.x.current ?? 0)}
1026
1141
  {@const elemY = mpPos ? mpPos.y : (animated?.y.current ?? 0)}
@@ -1209,7 +1324,7 @@
1209
1324
  <button class="animot-arrow animot-arrow-left" onclick={() => animateToSlide(currentSlideIndex - 1)} disabled={currentSlideIndex === 0 || isTransitioning} aria-label="Previous slide">
1210
1325
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
1211
1326
  </button>
1212
- <button class="animot-arrow animot-arrow-right" onclick={() => animateToSlide(currentSlideIndex + 1)} disabled={currentSlideIndex === slides.length - 1 || isTransitioning} aria-label="Next slide">
1327
+ <button class="animot-arrow animot-arrow-right" onclick={() => handleNextSlide()} disabled={(!loop && currentSlideIndex === slides.length - 1) || isTransitioning} aria-label="Next slide">
1213
1328
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
1214
1329
  </button>
1215
1330
  {/if}
@@ -1220,7 +1335,7 @@
1220
1335
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
1221
1336
  </button>
1222
1337
  <span class="animot-slide-indicator">{currentSlideIndex + 1} / {slides.length}</span>
1223
- <button onclick={() => animateToSlide(currentSlideIndex + 1)} disabled={currentSlideIndex === slides.length - 1 || isTransitioning} aria-label="Next">
1338
+ <button onclick={() => handleNextSlide()} disabled={(!loop && currentSlideIndex === slides.length - 1) || isTransitioning} aria-label="Next">
1224
1339
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
1225
1340
  </button>
1226
1341
  <button onclick={() => { isAutoplay = !isAutoplay; if (!isAutoplay) clearAutoplayTimer(); }} class:active={isAutoplay} aria-label={isAutoplay ? 'Pause' : 'Play'}>