animot-presenter 0.1.0

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.
@@ -0,0 +1,979 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { tween } from '@animotion/motion';
4
+ import { highlightCode } from './highlight/highlighter';
5
+ import CodeMorph from './highlight/CodeMorph.svelte';
6
+ import ParticlesBackground from './effects/ParticlesBackground.svelte';
7
+ import ConfettiEffect from './effects/ConfettiEffect.svelte';
8
+ import CounterRenderer from './renderers/CounterRenderer.svelte';
9
+ import ChartRenderer from './renderers/ChartRenderer.svelte';
10
+ import IconRenderer from './renderers/IconRenderer.svelte';
11
+ import { easeInOutCubic, getBackgroundStyle, hashFraction, getFloatAnimName, computeFloatAmp, computeFloatSpeed } from './engine/utils';
12
+ import type {
13
+ AnimotProject, AnimotPresenterProps, CanvasElement, CodeElement, TextElement,
14
+ ArrowElement, ImageElement, ShapeElement, CounterElement, ChartElement, IconElement,
15
+ Slide, CodeAnimationMode, AnimatableProperty
16
+ } from './types';
17
+ import './styles/presenter.css';
18
+
19
+ type TweenValue = ReturnType<typeof tween<number>>;
20
+
21
+ interface AnimatedElement {
22
+ x: TweenValue; y: TweenValue; width: TweenValue; height: TweenValue;
23
+ rotation: TweenValue; skewX: TweenValue; skewY: TweenValue;
24
+ tiltX: TweenValue; tiltY: TweenValue; opacity: TweenValue; borderRadius: TweenValue;
25
+ fontSize: TweenValue | null;
26
+ fillColor: ReturnType<typeof tween<string>> | null;
27
+ strokeColor: ReturnType<typeof tween<string>> | null;
28
+ strokeWidth: TweenValue | null;
29
+ shapeMorph: TweenValue | null;
30
+ }
31
+
32
+ interface ShapeMorphState { fromType: string; toType: string; }
33
+
34
+ let {
35
+ src, data, autoplay = false, loop = false, controls = true, arrows = false,
36
+ progress: showProgress = true, keyboard = true, duration: durationOverride,
37
+ startSlide = 0, class: className = '', onslidechange, oncomplete
38
+ }: AnimotPresenterProps = $props();
39
+
40
+ // State
41
+ let project = $state<AnimotProject | null>(null);
42
+ let loading = $state(true);
43
+ let error = $state<string | null>(null);
44
+ let currentSlideIndex = $state(0);
45
+ let isTransitioning = $state(false);
46
+ let isAutoplay = $state(false);
47
+ let transitionClass = $state('');
48
+ let transitionDirection = $state<'forward' | 'backward'>('forward');
49
+ let transitionDurationMs = $state(500);
50
+ let containerEl: HTMLElement;
51
+ let containerWidth = $state(0);
52
+ let containerHeight = $state(0);
53
+
54
+ let animatedElements = $state<Map<string, AnimatedElement>>(new Map());
55
+ let codeHighlights = $state<Map<string, string>>(new Map());
56
+ let elementContent = $state<Map<string, CanvasElement>>(new Map());
57
+ let previousCodeContent = $state<Map<string, string>>(new Map());
58
+ let codeMorphState = $state<Map<string, {oldCode: string, newCode: string, mode: CodeAnimationMode, speed: number, highlightColor: string}>>(new Map());
59
+ let textTypewriterState = $state<Map<string, {fullText: string, displayedChars: number, isAnimating: boolean}>>(new Map());
60
+ let typewriterIntervals = new Map<string, ReturnType<typeof setInterval>>();
61
+ let shapeMorphStates = $state<Map<string, ShapeMorphState>>(new Map());
62
+ let autoplayTimer: ReturnType<typeof setTimeout> | null = null;
63
+ let menuVisible = $state(true);
64
+ let mouseIdleTimer: ReturnType<typeof setTimeout> | null = null;
65
+
66
+ const slides = $derived(project?.slides ?? []);
67
+ const currentSlide = $derived(slides[currentSlideIndex]);
68
+ const canvasWidth = $derived(currentSlide?.canvas.width ?? 800);
69
+ const canvasHeight = $derived(currentSlide?.canvas.height ?? 600);
70
+
71
+ const presentationScale = $derived.by(() => {
72
+ if (!containerWidth || !containerHeight) return 1;
73
+ const scaleX = containerWidth / canvasWidth;
74
+ const scaleY = containerHeight / canvasHeight;
75
+ return Math.min(scaleX, scaleY);
76
+ });
77
+
78
+ const backgroundStyle = $derived.by(() => {
79
+ if (!currentSlide) return 'background: transparent';
80
+ return getBackgroundStyle(currentSlide.canvas.background);
81
+ });
82
+
83
+ const allElementIds = $derived.by(() => {
84
+ const ids = new Set<string>();
85
+ slides.forEach(slide => slide.canvas.elements.forEach(el => ids.add(el.id)));
86
+ return ids;
87
+ });
88
+
89
+ const sortedElementIds = $derived.by(() => {
90
+ const elements: Array<{id: string, zIndex: number}> = [];
91
+ for (const id of allElementIds) {
92
+ const el = elementContent.get(id);
93
+ if (el) elements.push({ id, zIndex: el.zIndex ?? 0 });
94
+ }
95
+ elements.sort((a, b) => a.zIndex - b.zIndex);
96
+ return elements.map(e => e.id);
97
+ });
98
+
99
+ function getElementInSlide(slide: Slide | null, elementId: string): CanvasElement | undefined {
100
+ return slide?.canvas.elements.find(el => el.id === elementId);
101
+ }
102
+
103
+ // Typewriter
104
+ function startTypewriterAnimation(elementId: string, fullText: string, speed: number) {
105
+ const existing = typewriterIntervals.get(elementId);
106
+ if (existing) { clearInterval(existing); typewriterIntervals.delete(elementId); }
107
+ textTypewriterState.set(elementId, { fullText, displayedChars: 0, isAnimating: true });
108
+ textTypewriterState = new Map(textTypewriterState);
109
+ const intervalMs = 1000 / speed;
110
+ const interval = setInterval(() => {
111
+ const state = textTypewriterState.get(elementId);
112
+ if (state && state.isAnimating) {
113
+ if (state.displayedChars < state.fullText.length) {
114
+ textTypewriterState.set(elementId, { ...state, displayedChars: state.displayedChars + 1 });
115
+ textTypewriterState = new Map(textTypewriterState);
116
+ } else {
117
+ clearInterval(interval); typewriterIntervals.delete(elementId);
118
+ textTypewriterState.set(elementId, { ...state, isAnimating: false });
119
+ textTypewriterState = new Map(textTypewriterState);
120
+ }
121
+ } else { clearInterval(interval); typewriterIntervals.delete(elementId); }
122
+ }, intervalMs);
123
+ typewriterIntervals.set(elementId, interval);
124
+ }
125
+
126
+ function clearAllTypewriterAnimations() {
127
+ for (const [, interval] of typewriterIntervals) clearInterval(interval);
128
+ typewriterIntervals.clear();
129
+ textTypewriterState.clear();
130
+ textTypewriterState = new Map(textTypewriterState);
131
+ }
132
+
133
+ // Arrow draw animation action
134
+ function animateStyledArrowDraw(node: SVGPathElement, params: { enabled: boolean; duration: number; dashPattern: string; startX: number; endX: number; slideIndex: number }) {
135
+ let lastSlideIndex = params.slideIndex;
136
+ let animationId: number | null = null;
137
+ function runAnimation() {
138
+ if (!params.enabled) return;
139
+ if (animationId) cancelAnimationFrame(animationId);
140
+ const svg = node.closest('svg') as SVGSVGElement | null;
141
+ if (!svg) return;
142
+ const goesLeftToRight = params.endX >= params.startX;
143
+ svg.style.clipPath = goesLeftToRight ? 'inset(0 100% 0 0)' : 'inset(0 0 0 100%)';
144
+ const startTime = performance.now();
145
+ const dur = params.duration;
146
+ function animate(currentTime: number) {
147
+ const elapsed = currentTime - startTime;
148
+ const progress = Math.min(elapsed / dur, 1);
149
+ const eased = 1 - Math.pow(1 - progress, 3);
150
+ const inset = 100 * (1 - eased);
151
+ svg!.style.clipPath = goesLeftToRight ? `inset(0 ${inset}% 0 0)` : `inset(0 0 0 ${inset}%)`;
152
+ if (progress < 1) animationId = requestAnimationFrame(animate);
153
+ else { svg!.style.clipPath = 'none'; animationId = null; }
154
+ }
155
+ animationId = requestAnimationFrame(animate);
156
+ }
157
+ runAnimation();
158
+ return {
159
+ update(newParams: typeof params) {
160
+ if (newParams.slideIndex !== lastSlideIndex) { lastSlideIndex = newParams.slideIndex; params = newParams; runAnimation(); }
161
+ },
162
+ destroy() { if (animationId) cancelAnimationFrame(animationId); }
163
+ };
164
+ }
165
+
166
+ // Init animated elements
167
+ function initAllAnimatedElements() {
168
+ const firstSlide = slides[0];
169
+ if (firstSlide) {
170
+ for (const element of firstSlide.canvas.elements) {
171
+ if (element.type === 'code') previousCodeContent.set(element.id, (element as CodeElement).code);
172
+ if (element.type === 'text') {
173
+ const textEl = element as TextElement;
174
+ if (textEl.animation?.mode === 'typewriter') startTypewriterAnimation(element.id, textEl.content, textEl.animation.typewriterSpeed || 50);
175
+ }
176
+ }
177
+ }
178
+ for (const slide of slides) {
179
+ for (const element of slide.canvas.elements) {
180
+ if (!animatedElements.has(element.id)) {
181
+ const inCurrent = getElementInSlide(currentSlide, element.id);
182
+ const startOpacity = inCurrent ? 1 : 0;
183
+ const br = (element as any).borderRadius ?? 0;
184
+ const isShape = element.type === 'shape';
185
+ const shapeEl = isShape ? element as ShapeElement : null;
186
+ const isText = element.type === 'text';
187
+ const textEl = isText ? element as TextElement : null;
188
+ animatedElements.set(element.id, {
189
+ x: tween(element.position.x, { duration: 500 }),
190
+ y: tween(element.position.y, { duration: 500 }),
191
+ width: tween(element.size.width, { duration: 500 }),
192
+ height: tween(element.size.height, { duration: 500 }),
193
+ rotation: tween(element.rotation, { duration: 500 }),
194
+ skewX: tween(element.skewX ?? 0, { duration: 500 }),
195
+ skewY: tween(element.skewY ?? 0, { duration: 500 }),
196
+ tiltX: tween(element.tiltX ?? 0, { duration: 500 }),
197
+ tiltY: tween(element.tiltY ?? 0, { duration: 500 }),
198
+ opacity: tween(startOpacity, { duration: 300 }),
199
+ borderRadius: tween(br, { duration: 500 }),
200
+ fontSize: textEl ? tween(textEl.fontSize, { duration: 500 }) : null,
201
+ fillColor: shapeEl ? tween(shapeEl.fillColor, { duration: 500 }) : null,
202
+ strokeColor: shapeEl ? tween(shapeEl.strokeColor, { duration: 500 }) : null,
203
+ strokeWidth: shapeEl ? tween(shapeEl.strokeWidth, { duration: 500 }) : null,
204
+ shapeMorph: shapeEl ? tween(1, { duration: 500 }) : null
205
+ });
206
+ const currentSlideEl = getElementInSlide(currentSlide, element.id);
207
+ elementContent.set(element.id, JSON.parse(JSON.stringify(currentSlideEl || element)));
208
+ }
209
+ }
210
+ }
211
+ animatedElements = new Map(animatedElements);
212
+ elementContent = new Map(elementContent);
213
+ previousCodeContent = new Map(previousCodeContent);
214
+ }
215
+
216
+ // Animate to slide
217
+ async function animateToSlide(targetIndex: number) {
218
+ if (isTransitioning || targetIndex < 0 || targetIndex >= slides.length) return;
219
+ if (targetIndex === currentSlideIndex) return;
220
+ isTransitioning = true;
221
+ transitionDirection = targetIndex > currentSlideIndex ? 'forward' : 'backward';
222
+ const targetSlide = slides[targetIndex];
223
+ clearAllTypewriterAnimations();
224
+ const transition = targetSlide.transition;
225
+ const duration = durationOverride ?? transition.duration;
226
+ transitionDurationMs = duration;
227
+ const hasSlideTransition = transition.type !== 'none';
228
+
229
+ if (hasSlideTransition) {
230
+ transitionClass = `transition-${transition.type}-out`;
231
+ await new Promise(r => setTimeout(r, duration * 0.4));
232
+ const newElementContent = new Map(elementContent);
233
+ const newCodeMorphState = new Map(codeMorphState);
234
+ const newPreviousCodeContent = new Map(previousCodeContent);
235
+ for (const elementId of allElementIds) {
236
+ const targetEl = getElementInSlide(targetSlide, elementId);
237
+ const animated = animatedElements.get(elementId);
238
+ if (targetEl) {
239
+ if (targetEl.type === 'code') {
240
+ const codeEl = targetEl as CodeElement;
241
+ const prevCode = newPreviousCodeContent.get(elementId) || '';
242
+ newCodeMorphState.set(elementId, { oldCode: prevCode, newCode: codeEl.code, mode: codeEl.animation?.mode || 'highlight-changes', speed: codeEl.animation?.typewriterSpeed || 50, highlightColor: codeEl.animation?.highlightColor || '#fef08a' });
243
+ newPreviousCodeContent.set(elementId, codeEl.code);
244
+ }
245
+ newElementContent.set(elementId, JSON.parse(JSON.stringify(targetEl)));
246
+ if (animated) {
247
+ animated.x.to(targetEl.position.x, { duration: 0 }); animated.y.to(targetEl.position.y, { duration: 0 });
248
+ animated.width.to(targetEl.size.width, { duration: 0 }); animated.height.to(targetEl.size.height, { duration: 0 });
249
+ animated.rotation.to(targetEl.rotation, { duration: 0 });
250
+ animated.skewX.to(targetEl.skewX ?? 0, { duration: 0 }); animated.skewY.to(targetEl.skewY ?? 0, { duration: 0 });
251
+ animated.tiltX.to(targetEl.tiltX ?? 0, { duration: 0 }); animated.tiltY.to(targetEl.tiltY ?? 0, { duration: 0 });
252
+ animated.opacity.to(1, { duration: 0 });
253
+ animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: 0 });
254
+ if (targetEl.type === 'text') animated.fontSize?.to((targetEl as TextElement).fontSize, { duration: 0 });
255
+ if (targetEl.type === 'shape') {
256
+ const s = targetEl as ShapeElement;
257
+ animated.fillColor?.to(s.fillColor, { duration: 0 });
258
+ animated.strokeColor?.to(s.strokeColor, { duration: 0 });
259
+ animated.strokeWidth?.to(s.strokeWidth, { duration: 0 });
260
+ }
261
+ }
262
+ } else if (animated) { animated.opacity.to(0, { duration: 0 }); }
263
+ }
264
+ for (const [, element] of newElementContent) {
265
+ if (element.type === 'code') {
266
+ const codeEl = element as CodeElement;
267
+ const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
268
+ if (!codeHighlights.has(key)) {
269
+ const html = await highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers });
270
+ codeHighlights.set(key, html);
271
+ }
272
+ }
273
+ }
274
+ codeHighlights = new Map(codeHighlights);
275
+ shapeMorphStates = new Map();
276
+ codeMorphState = newCodeMorphState;
277
+ previousCodeContent = newPreviousCodeContent;
278
+ elementContent = newElementContent;
279
+ animatedElements = new Map(animatedElements);
280
+ currentSlideIndex = targetIndex;
281
+ for (const elementId of allElementIds) {
282
+ const targetEl = getElementInSlide(targetSlide, elementId);
283
+ if (targetEl?.type === 'text') {
284
+ const textEl = targetEl as TextElement;
285
+ if (textEl.animation?.mode === 'typewriter') startTypewriterAnimation(elementId, textEl.content, textEl.animation.typewriterSpeed || 50);
286
+ }
287
+ }
288
+ transitionClass = `transition-${transition.type}-in`;
289
+ await new Promise(r => setTimeout(r, duration * 0.6));
290
+ transitionClass = '';
291
+ isTransitioning = false;
292
+ onslidechange?.(targetIndex, slides.length);
293
+ if (targetIndex === slides.length - 1 && !loop) oncomplete?.();
294
+ return;
295
+ }
296
+
297
+ // Per-element morphing (transition type = 'none')
298
+ const animations: Promise<void>[] = [];
299
+ for (const elementId of allElementIds) {
300
+ const currentEl = getElementInSlide(currentSlide, elementId);
301
+ const animated = animatedElements.get(elementId);
302
+ if (!animated) continue;
303
+ if (currentEl) {
304
+ await animated.x.to(currentEl.position.x, { duration: 0 });
305
+ await animated.y.to(currentEl.position.y, { duration: 0 });
306
+ await animated.width.to(currentEl.size.width, { duration: 0 });
307
+ await animated.height.to(currentEl.size.height, { duration: 0 });
308
+ await animated.rotation.to(currentEl.rotation, { duration: 0 });
309
+ await animated.skewX.to(currentEl.skewX ?? 0, { duration: 0 });
310
+ await animated.skewY.to(currentEl.skewY ?? 0, { duration: 0 });
311
+ await animated.tiltX.to(currentEl.tiltX ?? 0, { duration: 0 });
312
+ await animated.tiltY.to(currentEl.tiltY ?? 0, { duration: 0 });
313
+ await animated.borderRadius.to((currentEl as any).borderRadius ?? 0, { duration: 0 });
314
+ await animated.opacity.to(1, { duration: 0 });
315
+ if (currentEl.type === 'text' && animated.fontSize) await animated.fontSize.to((currentEl as TextElement).fontSize, { duration: 0 });
316
+ if (currentEl.type === 'shape') {
317
+ const s = currentEl as ShapeElement;
318
+ if (animated.fillColor) await animated.fillColor.to(s.fillColor, { duration: 0 });
319
+ if (animated.strokeColor) await animated.strokeColor.to(s.strokeColor, { duration: 0 });
320
+ if (animated.strokeWidth) await animated.strokeWidth.to(s.strokeWidth, { duration: 0 });
321
+ }
322
+ }
323
+ }
324
+
325
+ interface AnimationTask { elementId: string; order: number; delay: number; elementDuration: number; run: () => Promise<void>[]; }
326
+ const animationTasks: AnimationTask[] = [];
327
+
328
+ for (const elementId of allElementIds) {
329
+ const currentEl = getElementInSlide(currentSlide, elementId);
330
+ const targetEl = getElementInSlide(targetSlide, elementId);
331
+ const animated = animatedElements.get(elementId);
332
+ if (!animated) continue;
333
+ const animConfig = targetEl?.animationConfig || currentEl?.animationConfig;
334
+ const order = animConfig?.order ?? 0;
335
+ const delay = animConfig?.delay ?? 0;
336
+ const elementDuration = animConfig?.duration ?? duration;
337
+
338
+ if (targetEl) {
339
+ const easing = easeInOutCubic;
340
+ const propertySequences = targetEl.animationConfig?.propertySequences;
341
+ if (targetEl.type === 'text') {
342
+ const textEl = targetEl as TextElement;
343
+ if (textEl.animation?.mode === 'typewriter') startTypewriterAnimation(elementId, textEl.content, textEl.animation.typewriterSpeed || 50);
344
+ }
345
+
346
+ const getSeqTiming = (prop: AnimatableProperty) => {
347
+ if (!propertySequences?.length) return { duration: elementDuration, delay: 0, order: 0 };
348
+ const seq = propertySequences.find(s => s.property === prop);
349
+ return seq ? { duration: seq.duration, delay: seq.delay, order: seq.order } : { duration: elementDuration, delay: 0, order: 99 };
350
+ };
351
+
352
+ animationTasks.push({
353
+ elementId, order, delay, elementDuration,
354
+ run: () => {
355
+ const anims: Promise<void>[] = [];
356
+ if (propertySequences?.length) {
357
+ const sequencedProps = new Set(propertySequences.map(s => s.property));
358
+ if (!sequencedProps.has('position')) { anims.push(animated.x.to(targetEl.position.x, { duration: elementDuration, easing })); anims.push(animated.y.to(targetEl.position.y, { duration: elementDuration, easing })); }
359
+ if (!sequencedProps.has('rotation')) anims.push(animated.rotation.to(targetEl.rotation, { duration: elementDuration, easing }));
360
+ if (!sequencedProps.has('tilt')) { anims.push(animated.tiltX.to(targetEl.tiltX ?? 0, { duration: elementDuration, easing })); anims.push(animated.tiltY.to(targetEl.tiltY ?? 0, { duration: elementDuration, easing })); }
361
+ if (!sequencedProps.has('skew')) { anims.push(animated.skewX.to(targetEl.skewX ?? 0, { duration: elementDuration, easing })); anims.push(animated.skewY.to(targetEl.skewY ?? 0, { duration: elementDuration, easing })); }
362
+ if (!sequencedProps.has('size')) { anims.push(animated.width.to(targetEl.size.width, { duration: elementDuration, easing })); anims.push(animated.height.to(targetEl.size.height, { duration: elementDuration, easing })); }
363
+ if (!sequencedProps.has('borderRadius')) anims.push(animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: elementDuration, easing }));
364
+ const sortedSeqs = [...propertySequences].sort((a, b) => a.order - b.order);
365
+ let cumulativeDelay = 0;
366
+ for (const seq of sortedSeqs) {
367
+ const seqDelay = cumulativeDelay + seq.delay;
368
+ const seqDuration = seq.duration;
369
+ setTimeout(() => {
370
+ if (seq.property === 'position') { animated.x.to(targetEl.position.x, { duration: seqDuration, easing }); animated.y.to(targetEl.position.y, { duration: seqDuration, easing }); }
371
+ else if (seq.property === 'rotation') animated.rotation.to(targetEl.rotation, { duration: seqDuration, easing });
372
+ else if (seq.property === 'tilt') { animated.tiltX.to(targetEl.tiltX ?? 0, { duration: seqDuration, easing }); animated.tiltY.to(targetEl.tiltY ?? 0, { duration: seqDuration, easing }); }
373
+ else if (seq.property === 'skew') { animated.skewX.to(targetEl.skewX ?? 0, { duration: seqDuration, easing }); animated.skewY.to(targetEl.skewY ?? 0, { duration: seqDuration, easing }); }
374
+ else if (seq.property === 'size') { animated.width.to(targetEl.size.width, { duration: seqDuration, easing }); animated.height.to(targetEl.size.height, { duration: seqDuration, easing }); }
375
+ else if (seq.property === 'borderRadius') animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: seqDuration, easing });
376
+ else if (seq.property === 'color' && targetEl.type === 'shape') {
377
+ const s = targetEl as ShapeElement;
378
+ animated.fillColor?.to(s.fillColor, { duration: seqDuration, easing });
379
+ animated.strokeColor?.to(s.strokeColor, { duration: seqDuration, easing });
380
+ animated.strokeWidth?.to(s.strokeWidth, { duration: seqDuration, easing });
381
+ }
382
+ }, seqDelay);
383
+ cumulativeDelay = seqDelay + seqDuration;
384
+ }
385
+ anims.push(new Promise(r => setTimeout(r, cumulativeDelay)));
386
+ } else {
387
+ anims.push(animated.x.to(targetEl.position.x, { duration: elementDuration, easing }));
388
+ anims.push(animated.y.to(targetEl.position.y, { duration: elementDuration, easing }));
389
+ anims.push(animated.width.to(targetEl.size.width, { duration: elementDuration, easing }));
390
+ anims.push(animated.height.to(targetEl.size.height, { duration: elementDuration, easing }));
391
+ anims.push(animated.rotation.to(targetEl.rotation, { duration: elementDuration, easing }));
392
+ anims.push(animated.skewX.to(targetEl.skewX ?? 0, { duration: elementDuration, easing }));
393
+ anims.push(animated.skewY.to(targetEl.skewY ?? 0, { duration: elementDuration, easing }));
394
+ anims.push(animated.tiltX.to(targetEl.tiltX ?? 0, { duration: elementDuration, easing }));
395
+ anims.push(animated.tiltY.to(targetEl.tiltY ?? 0, { duration: elementDuration, easing }));
396
+ anims.push(animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: elementDuration, easing }));
397
+ }
398
+ if (targetEl.type === 'text' && animated.fontSize) anims.push(animated.fontSize.to((targetEl as TextElement).fontSize, { duration: elementDuration, easing }));
399
+ if (targetEl.type === 'shape' && currentEl?.type === 'shape') {
400
+ const ts = targetEl as ShapeElement;
401
+ const cs = currentEl as ShapeElement;
402
+ if (!propertySequences?.length) {
403
+ if (animated.fillColor) anims.push(animated.fillColor.to(ts.fillColor, { duration: elementDuration, easing }));
404
+ if (animated.strokeColor) anims.push(animated.strokeColor.to(ts.strokeColor, { duration: elementDuration, easing }));
405
+ if (animated.strokeWidth) anims.push(animated.strokeWidth.to(ts.strokeWidth, { duration: elementDuration, easing }));
406
+ }
407
+ if (cs.shapeType !== ts.shapeType && animated.shapeMorph) {
408
+ shapeMorphStates.set(elementId, { fromType: cs.shapeType, toType: ts.shapeType });
409
+ shapeMorphStates = new Map(shapeMorphStates);
410
+ anims.push(animated.shapeMorph.to(0, { duration: 0 }));
411
+ anims.push(animated.shapeMorph.to(1, { duration: elementDuration, easing }));
412
+ }
413
+ } else if (targetEl.type === 'shape' && !propertySequences?.length) {
414
+ const s = targetEl as ShapeElement;
415
+ if (animated.fillColor) anims.push(animated.fillColor.to(s.fillColor, { duration: elementDuration, easing }));
416
+ if (animated.strokeColor) anims.push(animated.strokeColor.to(s.strokeColor, { duration: elementDuration, easing }));
417
+ if (animated.strokeWidth) anims.push(animated.strokeWidth.to(s.strokeWidth, { duration: elementDuration, easing }));
418
+ }
419
+ if (!currentEl) anims.push(animated.opacity.to(1, { duration: elementDuration / 2, easing }));
420
+ return anims;
421
+ }
422
+ });
423
+ } else if (currentEl) {
424
+ animationTasks.push({ elementId, order, delay, elementDuration, run: () => [animated.opacity.to(0, { duration: elementDuration / 2, easing: easeInOutCubic })] });
425
+ }
426
+ }
427
+
428
+ animationTasks.sort((a, b) => a.order - b.order);
429
+ const orderGroups = new Map<number, AnimationTask[]>();
430
+ for (const task of animationTasks) {
431
+ if (!orderGroups.has(task.order)) orderGroups.set(task.order, []);
432
+ orderGroups.get(task.order)!.push(task);
433
+ }
434
+ const sortedOrders = [...orderGroups.keys()].sort((a, b) => a - b);
435
+ for (let orderIdx = 0; orderIdx < sortedOrders.length; orderIdx++) {
436
+ const order = sortedOrders[orderIdx];
437
+ const tasks = orderGroups.get(order)!;
438
+ const groupAnimations: Promise<void>[] = [];
439
+ for (const task of tasks) {
440
+ if (task.delay > 0) setTimeout(() => { task.run().forEach(p => animations.push(p)); }, task.delay);
441
+ else groupAnimations.push(...task.run());
442
+ }
443
+ animations.push(...groupAnimations);
444
+ if (orderIdx < sortedOrders.length - 1) {
445
+ const maxDur = Math.max(...tasks.map(t => t.elementDuration));
446
+ await new Promise(r => setTimeout(r, maxDur * 0.3));
447
+ }
448
+ }
449
+
450
+ const newElementContent = new Map(elementContent);
451
+ const newCodeMorphState = new Map(codeMorphState);
452
+ const newPreviousCodeContent = new Map(previousCodeContent);
453
+ for (const elementId of allElementIds) {
454
+ const targetEl = getElementInSlide(targetSlide, elementId);
455
+ if (targetEl) {
456
+ if (targetEl.type === 'code') {
457
+ const codeEl = targetEl as CodeElement;
458
+ const prevCode = newPreviousCodeContent.get(elementId) || '';
459
+ newCodeMorphState.set(elementId, { oldCode: prevCode, newCode: codeEl.code, mode: codeEl.animation?.mode || 'highlight-changes', speed: codeEl.animation?.typewriterSpeed || 50, highlightColor: codeEl.animation?.highlightColor || '#fef08a' });
460
+ newPreviousCodeContent.set(elementId, codeEl.code);
461
+ }
462
+ newElementContent.set(elementId, JSON.parse(JSON.stringify(targetEl)));
463
+ }
464
+ }
465
+ for (const [, element] of newElementContent) {
466
+ if (element.type === 'code') {
467
+ const codeEl = element as CodeElement;
468
+ const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
469
+ if (!codeHighlights.has(key)) {
470
+ const html = await highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers });
471
+ codeHighlights.set(key, html);
472
+ }
473
+ }
474
+ }
475
+ codeHighlights = new Map(codeHighlights);
476
+ shapeMorphStates = new Map();
477
+ codeMorphState = newCodeMorphState;
478
+ previousCodeContent = newPreviousCodeContent;
479
+ elementContent = newElementContent;
480
+ currentSlideIndex = targetIndex;
481
+ isTransitioning = false;
482
+ onslidechange?.(targetIndex, slides.length);
483
+ if (targetIndex === slides.length - 1 && !loop) oncomplete?.();
484
+ }
485
+
486
+ // Autoplay
487
+ function clearAutoplayTimer() { if (autoplayTimer) { clearTimeout(autoplayTimer); autoplayTimer = null; } }
488
+ function scheduleNextSlide() {
489
+ clearAutoplayTimer();
490
+ if (!isAutoplay) return;
491
+ const slideDuration = durationOverride ?? currentSlide?.duration ?? 3000;
492
+ autoplayTimer = setTimeout(() => {
493
+ if (currentSlideIndex < slides.length - 1) animateToSlide(currentSlideIndex + 1);
494
+ else if (loop) animateToSlide(0);
495
+ else isAutoplay = false;
496
+ }, slideDuration);
497
+ }
498
+ $effect(() => { if (isAutoplay && !isTransitioning) scheduleNextSlide(); });
499
+ $effect(() => () => clearAutoplayTimer());
500
+
501
+ // Keyboard
502
+ function handleKeyDown(e: KeyboardEvent) {
503
+ if (!keyboard) return;
504
+ if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'Enter') { e.preventDefault(); animateToSlide(currentSlideIndex + 1); }
505
+ else if (e.key === 'ArrowLeft' || e.key === 'Backspace') { e.preventDefault(); animateToSlide(currentSlideIndex - 1); }
506
+ else if (e.key === 'Home') animateToSlide(0);
507
+ else if (e.key === 'End') animateToSlide(slides.length - 1);
508
+ else if (e.key === 'p' || e.key === 'P') { isAutoplay = !isAutoplay; if (!isAutoplay) clearAutoplayTimer(); }
509
+ }
510
+
511
+ function resetMouseIdleTimer() {
512
+ menuVisible = true;
513
+ if (mouseIdleTimer) clearTimeout(mouseIdleTimer);
514
+ mouseIdleTimer = setTimeout(() => { menuVisible = false; }, 3000);
515
+ }
516
+
517
+ // Code highlight helpers
518
+ async function loadCodeHighlights() {
519
+ for (const slide of slides) {
520
+ for (const element of slide.canvas.elements) {
521
+ if (element.type === 'code') {
522
+ const codeEl = element as CodeElement;
523
+ const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
524
+ if (!codeHighlights.has(key)) {
525
+ const html = await highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers });
526
+ codeHighlights.set(key, html);
527
+ }
528
+ }
529
+ }
530
+ }
531
+ codeHighlights = new Map(codeHighlights);
532
+ }
533
+
534
+ function getCodeHighlight(elementId: string): string {
535
+ const slideElement = getElementInSlide(currentSlide, elementId);
536
+ if (!slideElement || slideElement.type !== 'code') return '';
537
+ const codeEl = slideElement as CodeElement;
538
+ const key = `${codeEl.id}-${codeEl.code}-${codeEl.language}-${codeEl.showLineNumbers}`;
539
+ const cached = codeHighlights.get(key);
540
+ if (cached) return cached;
541
+ highlightCode(codeEl.code, codeEl.language, codeEl.theme, { showLineNumbers: codeEl.showLineNumbers }).then(html => {
542
+ codeHighlights.set(key, html);
543
+ codeHighlights = new Map(codeHighlights);
544
+ });
545
+ return '';
546
+ }
547
+
548
+ // Public API (exposed via bind:this)
549
+ export async function goto(slideIndex: number) { await animateToSlide(slideIndex); }
550
+ export async function next() { await animateToSlide(currentSlideIndex + 1); }
551
+ export async function prev() { await animateToSlide(currentSlideIndex - 1); }
552
+ export function play() { isAutoplay = true; }
553
+ export function pause() { isAutoplay = false; clearAutoplayTimer(); }
554
+ export function getCurrentSlide() { return currentSlideIndex; }
555
+ export function getTotalSlides() { return slides.length; }
556
+ export function getIsPlaying() { return isAutoplay; }
557
+
558
+ // Load data
559
+ async function loadProject() {
560
+ loading = true; error = null;
561
+ try {
562
+ if (data) { project = data; }
563
+ else if (src) {
564
+ const res = await fetch(src);
565
+ if (!res.ok) throw new Error(`Failed to load: ${res.status}`);
566
+ project = await res.json();
567
+ } else { throw new Error('Either src or data prop is required'); }
568
+ currentSlideIndex = startSlide;
569
+ await new Promise(r => setTimeout(r, 10));
570
+ initAllAnimatedElements();
571
+ await loadCodeHighlights();
572
+ loading = false;
573
+ if (autoplay) isAutoplay = true;
574
+ } catch (e: any) { error = e.message; loading = false; }
575
+ }
576
+
577
+ // ResizeObserver
578
+ let resizeObserver: ResizeObserver;
579
+
580
+ onMount(() => {
581
+ loadProject();
582
+ resizeObserver = new ResizeObserver(entries => {
583
+ for (const entry of entries) {
584
+ containerWidth = entry.contentRect.width;
585
+ containerHeight = entry.contentRect.height;
586
+ }
587
+ });
588
+ if (containerEl) resizeObserver.observe(containerEl);
589
+ resetMouseIdleTimer();
590
+ return () => { resizeObserver?.disconnect(); clearAutoplayTimer(); clearAllTypewriterAnimations(); if (mouseIdleTimer) clearTimeout(mouseIdleTimer); };
591
+ });
592
+
593
+ // Watch for prop changes
594
+ $effect(() => { if (data) { project = data; } });
595
+ </script>
596
+
597
+ <svelte:window onkeydown={handleKeyDown} />
598
+
599
+ <div
600
+ class="animot-presenter {className}"
601
+ class:animot-menu-visible={menuVisible}
602
+ bind:this={containerEl}
603
+ onmousemove={resetMouseIdleTimer}
604
+ role="region"
605
+ aria-label="Animot Presentation"
606
+ >
607
+ {#if loading}
608
+ <div class="animot-loading"><div class="animot-spinner"></div></div>
609
+ {:else if error}
610
+ <div class="animot-error">{error}</div>
611
+ {:else if project && currentSlide}
612
+ <div class="animot-canvas-wrapper" style:transform="scale({presentationScale})">
613
+ <div
614
+ class="animot-canvas {transitionClass}"
615
+ class:forward={transitionDirection === 'forward'}
616
+ class:backward={transitionDirection === 'backward'}
617
+ style:width="{canvasWidth}px"
618
+ style:height="{canvasHeight}px"
619
+ style:--transition-duration="{transitionDurationMs}ms"
620
+ style={backgroundStyle}
621
+ >
622
+ {#if currentSlide.canvas.background.particles?.enabled}
623
+ <ParticlesBackground config={currentSlide.canvas.background.particles} width={canvasWidth} height={canvasHeight} />
624
+ {/if}
625
+ {#if currentSlide.canvas.background.confetti?.enabled}
626
+ <ConfettiEffect config={currentSlide.canvas.background.confetti} width={canvasWidth} height={canvasHeight} />
627
+ {/if}
628
+
629
+ {#each sortedElementIds as elementId}
630
+ {@const element = elementContent.get(elementId)}
631
+ {@const animated = animatedElements.get(elementId)}
632
+ {@const floatCfg = element?.floatingAnimation}
633
+ {@const hasFloat = floatCfg?.enabled}
634
+ {@const floatGroupId = element?.groupId}
635
+ {#if element && animated && animated.opacity.current > 0.01 && element.visible !== false}
636
+ <div
637
+ class="animot-element"
638
+ class:floating={hasFloat}
639
+ style:left="{animated.x.current}px"
640
+ style:top="{animated.y.current}px"
641
+ style:width="{animated.width.current}px"
642
+ style:height="{animated.height.current}px"
643
+ style:opacity={animated.opacity.current}
644
+ style:transform="perspective(1000px) rotateX({animated.tiltX.current}deg) rotateY({animated.tiltY.current}deg) rotate({animated.rotation.current}deg) skewX({animated.skewX.current}deg) skewY({animated.skewY.current}deg)"
645
+ style:transform-origin={element.tiltOrigin ?? 'center'}
646
+ style:z-index={element.zIndex}
647
+ style:--float-amp="{hasFloat ? computeFloatAmp(floatCfg, floatGroupId || elementId) : 10}px"
648
+ style:--float-speed="{hasFloat ? computeFloatSpeed(floatCfg, floatGroupId || elementId) : 3}s"
649
+ style:--float-delay="{hashFraction(floatGroupId || elementId, 3) * 2}s"
650
+ style:animation-name={hasFloat ? getFloatAnimName(floatCfg!.direction, floatGroupId || elementId) : 'none'}
651
+ >
652
+ {#if element.type === 'code'}
653
+ {@const codeEl = element as CodeElement}
654
+ {@const morphState = codeMorphState.get(codeEl.id)}
655
+ <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">
656
+ {#if codeEl.showHeader}
657
+ <div class="animot-code-header" class:macos={codeEl.headerStyle === 'macos'}>
658
+ {#if codeEl.headerStyle === 'macos'}
659
+ <div class="animot-window-controls">
660
+ <span class="animot-control close"></span>
661
+ <span class="animot-control minimize"></span>
662
+ <span class="animot-control maximize"></span>
663
+ </div>
664
+ {/if}
665
+ <span class="animot-filename">{codeEl.filename}</span>
666
+ </div>
667
+ {/if}
668
+ <div class="animot-code-content">
669
+ <div class="animot-highlighted-code">
670
+ {#if morphState}
671
+ {#if morphState.oldCode !== morphState.newCode && morphState.mode !== 'instant'}
672
+ {#key currentSlideIndex}
673
+ <CodeMorph oldCode={morphState.oldCode} newCode={morphState.newCode} language={codeEl.language} theme={codeEl.theme} mode={morphState.mode} speed={morphState.speed} highlightColor={morphState.highlightColor} highlightDuration={codeEl.animation?.highlightDuration || 1000} showLineNumbers={(getElementInSlide(currentSlide, codeEl.id) as CodeElement | undefined)?.showLineNumbers ?? false} />
674
+ {/key}
675
+ {:else}
676
+ {@html getCodeHighlight(codeEl.id)}
677
+ {/if}
678
+ {:else}
679
+ {@html getCodeHighlight(codeEl.id)}
680
+ {/if}
681
+ </div>
682
+ </div>
683
+ </div>
684
+ {:else if element.type === 'text'}
685
+ {@const textEl = element as TextElement}
686
+ {@const animFontSize = animated.fontSize?.current ?? textEl.fontSize}
687
+ {@const typewriterState = textTypewriterState.get(element.id)}
688
+ {@const displayText = typewriterState?.isAnimating ? typewriterState.fullText.slice(0, typewriterState.displayedChars) : textEl.content}
689
+ <div
690
+ class="animot-text-element"
691
+ style:font-size="{animFontSize}px"
692
+ style:font-weight={textEl.fontWeight}
693
+ style:font-family="'{textEl.fontFamily}', sans-serif"
694
+ style:font-style={textEl.fontStyle ?? 'normal'}
695
+ style:text-decoration={textEl.textDecoration ?? 'none'}
696
+ style:color={textEl.backgroundImage ? 'transparent' : (textEl.hollow && textEl.textStroke?.enabled ? 'transparent' : textEl.color)}
697
+ style:background-color={textEl.backgroundImage ? 'transparent' : textEl.backgroundColor}
698
+ style:background-image={textEl.backgroundImage ? `url(${textEl.backgroundImage})` : 'none'}
699
+ style:background-size={textEl.backgroundImage ? `${textEl.backgroundScale ?? 100}%` : 'cover'}
700
+ style:background-position={textEl.backgroundImage ? `${textEl.backgroundPositionX ?? 50}% ${textEl.backgroundPositionY ?? 50}%` : 'center'}
701
+ style:-webkit-background-clip={textEl.backgroundImage ? 'text' : 'border-box'}
702
+ style:background-clip={textEl.backgroundImage ? 'text' : 'border-box'}
703
+ style:padding="{textEl.padding}px"
704
+ style:border-radius="{textEl.borderRadius}px"
705
+ style:text-align={textEl.textAlign}
706
+ style:justify-content={textEl.textAlign === 'center' ? 'center' : textEl.textAlign === 'right' ? 'flex-end' : 'flex-start'}
707
+ style:opacity={textEl.opacity ?? 1}
708
+ style:-webkit-text-stroke={textEl.textStroke?.enabled ? `${textEl.textStroke.width}px ${textEl.textStroke.color}` : '0'}
709
+ style:text-shadow={textEl.textShadow?.enabled ? `${textEl.textShadow.offsetX}px ${textEl.textShadow.offsetY}px ${textEl.textShadow.blur}px ${textEl.textShadow.color}` : 'none'}
710
+ >
711
+ {displayText}{#if typewriterState?.isAnimating}<span class="animot-typewriter-cursor">|</span>{/if}
712
+ </div>
713
+ {:else if element.type === 'arrow'}
714
+ {@const arrowEl = element as ArrowElement}
715
+ {@const cp = arrowEl.controlPoints || []}
716
+ {@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}` : `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}`}
717
+ {@const endAngle = cp.length === 0 ? Math.atan2(arrowEl.endPoint.y - arrowEl.startPoint.y, arrowEl.endPoint.x - arrowEl.startPoint.x) : cp.length === 1 ? Math.atan2(arrowEl.endPoint.y - cp[0].y, arrowEl.endPoint.x - cp[0].x) : Math.atan2(arrowEl.endPoint.y - cp[1].y, arrowEl.endPoint.x - cp[1].x)}
718
+ {@const headAngle = Math.PI / 6}
719
+ {@const headSize = arrowEl.headSize}
720
+ {@const arrowHeadPath = `M ${arrowEl.endPoint.x - headSize * Math.cos(endAngle - headAngle)} ${arrowEl.endPoint.y - headSize * Math.sin(endAngle - headAngle)} L ${arrowEl.endPoint.x} ${arrowEl.endPoint.y} L ${arrowEl.endPoint.x - headSize * Math.cos(endAngle + headAngle)} ${arrowEl.endPoint.y - headSize * Math.sin(endAngle + headAngle)}`}
721
+ {@const arrowAnimMode = arrowEl.animation?.mode ?? 'none'}
722
+ {@const arrowAnimDuration = arrowEl.animation?.duration ?? 500}
723
+ {@const isStyledArrow = arrowEl.style !== 'solid'}
724
+ {@const baseDashArray = arrowEl.style === 'dashed' ? '10,5' : arrowEl.style === 'dotted' ? '2,5' : 'none'}
725
+ <svg class="animot-arrow-element" class:arrow-animate-draw={arrowAnimMode === 'draw' && !isStyledArrow} class:arrow-animate-grow={arrowAnimMode === 'grow'} viewBox="0 0 {arrowEl.size.width} {arrowEl.size.height}" preserveAspectRatio="none" style="overflow: visible; --arrow-anim-duration: {arrowAnimDuration}ms;">
726
+ <path class="arrow-path" d={pathD} fill="none" stroke={arrowEl.color} stroke-width={arrowEl.strokeWidth} stroke-dasharray={baseDashArray} stroke-linecap="round" stroke-linejoin="round" use:animateStyledArrowDraw={{ enabled: arrowAnimMode === 'draw' && isStyledArrow, duration: arrowAnimDuration, dashPattern: baseDashArray, startX: arrowEl.startPoint.x, endX: arrowEl.endPoint.x, slideIndex: currentSlideIndex }} />
727
+ {#if arrowEl.showHead !== false}
728
+ <path class="arrow-head" class:arrow-head-styled-draw={arrowAnimMode === 'draw' && isStyledArrow} d={arrowHeadPath} fill="none" stroke={arrowEl.color} stroke-width={arrowEl.strokeWidth} stroke-linecap="round" stroke-linejoin="round" style={arrowAnimMode === 'draw' && isStyledArrow ? `--arrow-anim-duration: ${arrowAnimDuration}ms;` : ''} />
729
+ {/if}
730
+ </svg>
731
+ {:else if element.type === 'image'}
732
+ {@const imgEl = element as ImageElement}
733
+ {@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'}
734
+ <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:opacity={imgEl.opacity} style:filter={imgEl.blur ? `blur(${imgEl.blur}px)` : 'none'} style:clip-path={clipPath} />
735
+ {:else if element.type === 'shape'}
736
+ {@const shapeEl = element as ShapeElement}
737
+ {@const animFill = animated.fillColor?.current ?? shapeEl.fillColor}
738
+ {@const animStroke = animated.strokeColor?.current ?? shapeEl.strokeColor}
739
+ {@const animStrokeWidth = animated.strokeWidth?.current ?? shapeEl.strokeWidth}
740
+ {@const mState = shapeMorphStates.get(element.id)}
741
+ {@const morphProgress = animated.shapeMorph?.current ?? 1}
742
+ {@const effectiveShapeType = mState ? (morphProgress >= 1 ? mState.toType : (morphProgress <= 0 ? mState.fromType : null)) : shapeEl.shapeType}
743
+ {@const isMorphing = mState && morphProgress > 0 && morphProgress < 1}
744
+ <svg class="animot-shape-element" viewBox="0 0 {animated.width.current} {animated.height.current}" style:opacity={shapeEl.opacity} style:filter={shapeEl.boxShadow?.enabled ? `drop-shadow(${shapeEl.boxShadow.offsetX}px ${shapeEl.boxShadow.offsetY}px ${shapeEl.boxShadow.blur}px ${shapeEl.boxShadow.color})` : 'none'}>
745
+ {#if isMorphing}
746
+ {@const w = animated.width.current}
747
+ {@const h = animated.height.current}
748
+ {@const sw = animStrokeWidth}
749
+ <g style:opacity={1 - morphProgress}>{@html renderShape(mState!.fromType, w, h, animated.borderRadius.current, animFill, animStroke, sw)}</g>
750
+ <g style:opacity={morphProgress}>{@html renderShape(mState!.toType, w, h, animated.borderRadius.current, animFill, animStroke, sw)}</g>
751
+ {:else}
752
+ {@html renderShape(effectiveShapeType ?? shapeEl.shapeType, animated.width.current, animated.height.current, animated.borderRadius.current, animFill, animStroke, animStrokeWidth)}
753
+ {/if}
754
+ </svg>
755
+ {:else if element.type === 'counter'}
756
+ <CounterRenderer element={element as CounterElement} slideId={currentSlide?.id ?? ''} />
757
+ {:else if element.type === 'chart'}
758
+ <ChartRenderer element={element as ChartElement} slideId={currentSlide?.id ?? ''} />
759
+ {:else if element.type === 'icon'}
760
+ <IconRenderer element={element as IconElement} />
761
+ {/if}
762
+ </div>
763
+ {/if}
764
+ {/each}
765
+ </div>
766
+ </div>
767
+
768
+ {#if arrows}
769
+ <button class="animot-arrow animot-arrow-left" onclick={() => animateToSlide(currentSlideIndex - 1)} disabled={currentSlideIndex === 0 || isTransitioning} aria-label="Previous slide">
770
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
771
+ </button>
772
+ <button class="animot-arrow animot-arrow-right" onclick={() => animateToSlide(currentSlideIndex + 1)} disabled={currentSlideIndex === slides.length - 1 || isTransitioning} aria-label="Next slide">
773
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
774
+ </button>
775
+ {/if}
776
+
777
+ {#if controls}
778
+ <div class="animot-controls">
779
+ <button onclick={() => animateToSlide(currentSlideIndex - 1)} disabled={currentSlideIndex === 0 || isTransitioning} aria-label="Previous">
780
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
781
+ </button>
782
+ <span class="animot-slide-indicator">{currentSlideIndex + 1} / {slides.length}</span>
783
+ <button onclick={() => animateToSlide(currentSlideIndex + 1)} disabled={currentSlideIndex === slides.length - 1 || isTransitioning} aria-label="Next">
784
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
785
+ </button>
786
+ <button onclick={() => { isAutoplay = !isAutoplay; if (!isAutoplay) clearAutoplayTimer(); }} class:active={isAutoplay} aria-label={isAutoplay ? 'Pause' : 'Play'}>
787
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
788
+ {#if isAutoplay}<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>{:else}<polygon points="5 3 19 12 5 21 5 3"/>{/if}
789
+ </svg>
790
+ </button>
791
+ </div>
792
+ {/if}
793
+
794
+ {#if showProgress}
795
+ <div class="animot-progress-bar">
796
+ <div class="animot-progress-fill" style:width="{((currentSlideIndex + 1) / slides.length) * 100}%"></div>
797
+ </div>
798
+ {/if}
799
+ {/if}
800
+ </div>
801
+
802
+ <script module lang="ts">
803
+ function renderShape(type: string, w: number, h: number, br: number, fill: string, stroke: string, sw: number): string {
804
+ switch (type) {
805
+ 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}"/>`;
806
+ 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}"/>`;
807
+ 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}"/>`;
808
+ case 'triangle': return `<polygon points="${w/2},${sw/2} ${sw/2},${h-sw/2} ${w-sw/2},${h-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" stroke-linejoin="round"/>`;
809
+ case 'star': {
810
+ const cx = w/2, cy = h/2, outerR = Math.min(w,h)/2-sw/2, innerR = outerR*0.4;
811
+ 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(' ');
812
+ return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" stroke-linejoin="round"/>`;
813
+ }
814
+ case 'hexagon': {
815
+ const cx = w/2, cy = h/2, r = Math.min(w,h)/2-sw/2;
816
+ 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(' ');
817
+ return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" stroke-linejoin="round"/>`;
818
+ }
819
+ default: return '';
820
+ }
821
+ }
822
+ </script>
823
+
824
+ <style>
825
+ .animot-presenter {
826
+ position: relative;
827
+ width: 100%;
828
+ height: 100%;
829
+ display: flex;
830
+ align-items: center;
831
+ justify-content: center;
832
+ overflow: hidden;
833
+ background: transparent;
834
+ }
835
+
836
+ .animot-canvas-wrapper {
837
+ display: flex;
838
+ align-items: center;
839
+ justify-content: center;
840
+ }
841
+
842
+ .animot-canvas {
843
+ position: relative;
844
+ overflow: hidden;
845
+ }
846
+
847
+ .animot-element {
848
+ position: absolute;
849
+ box-sizing: border-box;
850
+ will-change: transform, opacity, left, top, width, height;
851
+ }
852
+
853
+ .animot-element.floating {
854
+ animation-duration: var(--float-speed, 3s);
855
+ animation-timing-function: ease-in-out;
856
+ animation-iteration-count: infinite;
857
+ animation-delay: var(--float-delay, 0s);
858
+ }
859
+
860
+ /* Code */
861
+ .animot-code-block {
862
+ width: 100%; height: 100%; background: #0d1117; overflow: hidden;
863
+ display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.4);
864
+ margin: 0; box-sizing: border-box;
865
+ }
866
+ .animot-code-block.transparent-bg { background: transparent; box-shadow: none; }
867
+ .animot-code-block.transparent-bg .animot-code-header { background: transparent; border-bottom-color: rgba(255,255,255,0.1); }
868
+ .animot-code-header { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; }
869
+ .animot-window-controls { display: flex; gap: 8px; }
870
+ .animot-control { width: 12px; height: 12px; border-radius: 50%; display: block; }
871
+ .macos .animot-control.close { background: #ff5f57; }
872
+ .macos .animot-control.minimize { background: #febc2e; }
873
+ .macos .animot-control.maximize { background: #28c840; }
874
+ .animot-filename { color: #8b949e; font-size: 14px; flex: 1; }
875
+ .animot-code-content { flex: 1; overflow: hidden; position: relative; }
876
+ .animot-highlighted-code { width: 100%; height: 100%; }
877
+ .animot-code-content :global(pre), .animot-highlighted-code :global(pre) { margin: 0; padding: 16px; background: transparent !important; line-height: 1.6; font-size: inherit; overflow: visible; }
878
+ .animot-highlighted-code :global(code) { font-family: inherit; font-size: inherit; font-weight: inherit; }
879
+ .animot-highlighted-code :global(.line-number) { display: inline-block; width: 2.5em; margin-right: 1em; text-align: right; color: #6e7681; user-select: none; opacity: 0.6; }
880
+
881
+ /* Text */
882
+ .animot-text-element { width: 100%; height: 100%; display: flex; align-items: center; white-space: pre-wrap; word-wrap: break-word; }
883
+ .animot-typewriter-cursor { animation: animot-blink 0.7s infinite; font-weight: 100; }
884
+ @keyframes animot-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
885
+
886
+ /* Arrow */
887
+ .animot-arrow-element { width: 100%; height: 100%; }
888
+ .arrow-animate-draw .arrow-path { stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: animot-arrow-draw var(--arrow-anim-duration, 500ms) ease-out forwards; }
889
+ .arrow-head-styled-draw { opacity: 0; animation: animot-arrow-head-appear var(--arrow-anim-duration, 500ms) ease-out forwards; animation-delay: calc(var(--arrow-anim-duration, 500ms) * 0.7); }
890
+ .arrow-animate-draw .arrow-head { opacity: 0; animation: animot-arrow-head-appear var(--arrow-anim-duration, 500ms) ease-out forwards; animation-delay: calc(var(--arrow-anim-duration, 500ms) * 0.7); }
891
+ .arrow-animate-grow { transform-origin: left center; animation: animot-arrow-grow var(--arrow-anim-duration, 500ms) ease-out forwards; }
892
+ @keyframes animot-arrow-draw { to { stroke-dashoffset: 0; } }
893
+ @keyframes animot-arrow-head-appear { from { opacity: 0; } to { opacity: 1; } }
894
+ @keyframes animot-arrow-grow { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }
895
+
896
+ /* Image */
897
+ .animot-image-element { width: 100%; height: 100%; display: block; }
898
+
899
+ /* Shape */
900
+ .animot-shape-element { width: 100%; height: 100%; display: block; overflow: visible; }
901
+
902
+ /* Transitions */
903
+ .animot-canvas { --transition-duration: 500ms; transition: transform calc(var(--transition-duration) * 0.4) ease, opacity calc(var(--transition-duration) * 0.4) ease; }
904
+ .animot-canvas.transition-fade-out { opacity: 0; }
905
+ .animot-canvas.transition-fade-in { animation: animot-fadeIn calc(var(--transition-duration) * 0.6) ease forwards; }
906
+ .animot-canvas.transition-slide-left-out.forward { transform: translateX(-100%); opacity: 0; }
907
+ .animot-canvas.transition-slide-left-in.forward { animation: animot-slideInFromRight calc(var(--transition-duration) * 0.6) ease forwards; }
908
+ .animot-canvas.transition-slide-left-out.backward { transform: translateX(100%); opacity: 0; }
909
+ .animot-canvas.transition-slide-left-in.backward { animation: animot-slideInFromLeft calc(var(--transition-duration) * 0.6) ease forwards; }
910
+ .animot-canvas.transition-slide-right-out.forward { transform: translateX(100%); opacity: 0; }
911
+ .animot-canvas.transition-slide-right-in.forward { animation: animot-slideInFromLeft calc(var(--transition-duration) * 0.6) ease forwards; }
912
+ .animot-canvas.transition-slide-up-out { transform: translateY(-100%); opacity: 0; }
913
+ .animot-canvas.transition-slide-up-in { animation: animot-slideInFromBottom calc(var(--transition-duration) * 0.6) ease forwards; }
914
+ .animot-canvas.transition-slide-down-out { transform: translateY(100%); opacity: 0; }
915
+ .animot-canvas.transition-slide-down-in { animation: animot-slideInFromTop calc(var(--transition-duration) * 0.6) ease forwards; }
916
+ .animot-canvas.transition-zoom-in-out { transform: scale(0.5); opacity: 0; }
917
+ .animot-canvas.transition-zoom-in-in { animation: animot-zoomIn calc(var(--transition-duration) * 0.6) ease forwards; }
918
+ .animot-canvas.transition-zoom-out-out { transform: scale(1.5); opacity: 0; }
919
+ .animot-canvas.transition-zoom-out-in { animation: animot-zoomOut calc(var(--transition-duration) * 0.6) ease forwards; }
920
+ .animot-canvas.transition-flip-out { transform: perspective(1000px) rotateY(90deg); opacity: 0; }
921
+ .animot-canvas.transition-flip-in { animation: animot-flipIn calc(var(--transition-duration) * 0.6) ease forwards; }
922
+
923
+ @keyframes animot-fadeIn { from { opacity: 0; } to { opacity: 1; } }
924
+ @keyframes animot-slideInFromRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
925
+ @keyframes animot-slideInFromLeft { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
926
+ @keyframes animot-slideInFromBottom { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
927
+ @keyframes animot-slideInFromTop { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
928
+ @keyframes animot-zoomIn { from { transform: scale(0.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
929
+ @keyframes animot-zoomOut { from { transform: scale(1.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
930
+ @keyframes animot-flipIn { from { transform: perspective(1000px) rotateY(-90deg); opacity: 0; } to { transform: perspective(1000px) rotateY(0); opacity: 1; } }
931
+
932
+ /* Controls */
933
+ .animot-controls {
934
+ position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
935
+ display: flex; align-items: center; gap: 8px; padding: 8px 16px;
936
+ background: rgba(0,0,0,0.7); backdrop-filter: blur(10px); border-radius: 10px;
937
+ opacity: 0; transition: opacity 0.3s ease; z-index: 100;
938
+ }
939
+ .animot-presenter:hover .animot-controls, .animot-menu-visible .animot-controls { opacity: 1; }
940
+ .animot-controls button {
941
+ display: flex; align-items: center; justify-content: center;
942
+ width: 32px; height: 32px; border-radius: 6px; border: none; cursor: pointer;
943
+ background: rgba(255,255,255,0.1); color: white; transition: background 0.2s;
944
+ }
945
+ .animot-controls button:hover:not(:disabled) { background: rgba(255,255,255,0.2); }
946
+ .animot-controls button:disabled { opacity: 0.3; cursor: not-allowed; }
947
+ .animot-controls button.active { background: rgba(99,102,241,0.6); }
948
+ .animot-controls button svg { width: 16px; height: 16px; }
949
+ .animot-slide-indicator { font-size: 12px; color: white; min-width: 50px; text-align: center; font-family: system-ui, sans-serif; }
950
+
951
+ /* Arrows */
952
+ .animot-arrow {
953
+ position: absolute; top: 50%; transform: translateY(-50%);
954
+ width: 40px; height: 40px; border-radius: 50%; border: none; cursor: pointer;
955
+ background: rgba(0,0,0,0.5); color: white; display: flex; align-items: center; justify-content: center;
956
+ opacity: 0; transition: opacity 0.3s; z-index: 100;
957
+ }
958
+ .animot-presenter:hover .animot-arrow { opacity: 1; }
959
+ .animot-arrow:hover:not(:disabled) { background: rgba(0,0,0,0.7); }
960
+ .animot-arrow:disabled { opacity: 0.2; cursor: not-allowed; }
961
+ .animot-arrow svg { width: 20px; height: 20px; }
962
+ .animot-arrow-left { left: 8px; }
963
+ .animot-arrow-right { right: 8px; }
964
+
965
+ /* Progress bar */
966
+ .animot-progress-bar {
967
+ position: absolute; bottom: 0; left: 0; right: 0; height: 3px;
968
+ background: rgba(255,255,255,0.1); z-index: 100;
969
+ opacity: 0; transition: opacity 0.3s;
970
+ }
971
+ .animot-presenter:hover .animot-progress-bar { opacity: 1; }
972
+ .animot-progress-fill { height: 100%; background: linear-gradient(135deg, #7c3aed, #ec4899); transition: width 0.6s ease; }
973
+
974
+ /* Loading / Error */
975
+ .animot-loading { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
976
+ .animot-spinner { width: 32px; height: 32px; border: 3px solid rgba(255,255,255,0.2); border-top-color: #7c3aed; border-radius: 50%; animation: animot-spin 0.8s linear infinite; }
977
+ @keyframes animot-spin { to { transform: rotate(360deg); } }
978
+ .animot-error { color: #ef4444; padding: 20px; text-align: center; font-family: system-ui, sans-serif; }
979
+ </style>