animot-presenter 0.2.4 → 0.2.6

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/README.md CHANGED
@@ -472,7 +472,7 @@ animot-presenter .animot-controls { display: none; }
472
472
 
473
473
  ## Features
474
474
 
475
- - **Morphing animations** — Elements with the same ID across slides smoothly morph position, size, rotation, color, opacity, and border radius
475
+ - **Morphing animations** — Elements with the same ID across slides smoothly morph position, size, rotation, color, opacity, border radius, and CSS filters (blur, brightness, contrast, saturate, grayscale)
476
476
  - **Code highlighting** — Syntax highlighting via Shiki with typewriter, highlight-changes, and instant animation modes
477
477
  - **Shape morphing** — Rectangles, circles, triangles, stars, hexagons with smooth transitions
478
478
  - **Charts** — Animated bar, line, area, pie, and donut charts
@@ -483,7 +483,8 @@ animot-presenter .animot-controls { display: none; }
483
483
  - **Transitions** — Fade, slide, zoom, flip, and morphing (none) transition types
484
484
  - **Responsive** — Automatically scales to fit any container size
485
485
  - **Keyboard navigation** — Arrow keys, spacebar, Home/End
486
- - **Property sequencing** — Fine-grained control over which properties animate first
486
+ - **CSS filters** — Blur, brightness, contrast, saturate, and grayscale on any element, animated across slides
487
+ - **Property sequencing** — Fine-grained control over which properties animate first (including filters via the `blur` property sequence)
487
488
 
488
489
  ## JSON Schema
489
490
 
@@ -517,7 +518,12 @@ Animot JSON files follow this structure:
517
518
  "color": "#ffffff",
518
519
  "rotation": 0,
519
520
  "visible": true,
520
- "zIndex": 1
521
+ "zIndex": 1,
522
+ "blur": 0,
523
+ "brightness": 100,
524
+ "contrast": 100,
525
+ "saturate": 100,
526
+ "grayscale": 0
521
527
  }
522
528
  ]
523
529
  },
@@ -563,6 +569,32 @@ You can replace the base64 data URL with a remote or local URL:
563
569
 
564
570
  This lets you keep your JSON files lightweight by hosting images separately instead of embedding them as base64. The same applies to text elements with `backgroundImage` and background `image` fields.
565
571
 
572
+ ### CSS Filters
573
+
574
+ All element types support CSS filter properties that animate smoothly between slides:
575
+
576
+ ```json
577
+ {
578
+ "id": "hero-image",
579
+ "type": "image",
580
+ "blur": 5,
581
+ "brightness": 120,
582
+ "contrast": 110,
583
+ "saturate": 80,
584
+ "grayscale": 0
585
+ }
586
+ ```
587
+
588
+ | Property | Range | Default | Description |
589
+ |--------------|---------|---------|----------------------------------|
590
+ | `blur` | 0–20 | 0 | Gaussian blur in pixels |
591
+ | `brightness` | 0–200 | 100 | Brightness percentage (100 = normal) |
592
+ | `contrast` | 0–200 | 100 | Contrast percentage (100 = normal) |
593
+ | `saturate` | 0–200 | 100 | Saturation percentage (100 = normal) |
594
+ | `grayscale` | 0–100 | 0 | Grayscale percentage (0 = none) |
595
+
596
+ Set different filter values on the same element across slides to create smooth animated transitions (e.g., blur 10 on slide 1 to blur 0 on slide 2 for a reveal effect). Use the `blur` property sequence in `animationConfig.propertySequences` to control filter animation timing independently.
597
+
566
598
  > **Note:** Remote URLs must allow cross-origin requests (CORS) if served from a different domain.
567
599
 
568
600
  ## Bundle Size
@@ -30,6 +30,11 @@
30
30
  strokeWidth: TweenValue | null;
31
31
  shapeMorph: TweenValue | null;
32
32
  motionPathProgress: TweenValue | null;
33
+ blur: TweenValue;
34
+ brightness: TweenValue;
35
+ contrast: TweenValue;
36
+ saturate: TweenValue;
37
+ grayscale: TweenValue;
33
38
  }
34
39
 
35
40
  interface ShapeMorphState { fromType: string; toType: string; }
@@ -106,6 +111,22 @@
106
111
  return { x: points[0].x, y: points[0].y, angle: 0 };
107
112
  }
108
113
 
114
+ function computeMotionPathPosition(
115
+ mpPoint: { x: number; y: number; angle: number },
116
+ startPoint: { x: number; y: number; angle: number },
117
+ animX: number, animY: number, animW: number, animH: number
118
+ ): { x: number; y: number } {
119
+ const offsetX = (animX + animW / 2) - startPoint.x;
120
+ const offsetY = (animY + animH / 2) - startPoint.y;
121
+ const angleDelta = (mpPoint.angle - startPoint.angle) * Math.PI / 180;
122
+ const cos = Math.cos(angleDelta);
123
+ const sin = Math.sin(angleDelta);
124
+ return {
125
+ x: mpPoint.x + offsetX * cos - offsetY * sin - animW / 2,
126
+ y: mpPoint.y + offsetX * sin + offsetY * cos - animH / 2
127
+ };
128
+ }
129
+
109
130
  let {
110
131
  src, data, autoplay = false, loop = false, controls = true, arrows = false,
111
132
  progress: showProgress = true, keyboard = true, duration: durationOverride,
@@ -205,8 +226,26 @@
205
226
  textTypewriterState = new Map(textTypewriterState);
206
227
  }
207
228
 
208
- // Arrow draw animation action
209
- function animateStyledArrowDraw(node: SVGPathElement, params: { enabled: boolean; duration: number; dashPattern: string; startX: number; endX: number; slideIndex: number }) {
229
+ // Build SVG path for 3+ control points using Catmull-Rom spline
230
+ function buildCatmullRomPath(start: {x:number,y:number}, cps: {x:number,y:number}[], end: {x:number,y:number}): string {
231
+ const pts = [start, ...cps, end];
232
+ let d = `M ${pts[0].x} ${pts[0].y}`;
233
+ for (let i = 0; i < pts.length - 1; i++) {
234
+ const p0 = pts[i === 0 ? 0 : i - 1];
235
+ const p1 = pts[i];
236
+ const p2 = pts[i + 1];
237
+ const p3 = pts[i + 2 < pts.length ? i + 2 : pts.length - 1];
238
+ const c1x = p1.x + (p2.x - p0.x) / 6;
239
+ const c1y = p1.y + (p2.y - p0.y) / 6;
240
+ const c2x = p2.x - (p3.x - p1.x) / 6;
241
+ const c2y = p2.y - (p3.y - p1.y) / 6;
242
+ d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${p2.x} ${p2.y}`;
243
+ }
244
+ return d;
245
+ }
246
+
247
+ // Arrow draw/undraw/draw-undraw animation action
248
+ function animateStyledArrowDraw(node: SVGPathElement, params: { enabled: boolean; mode: string; duration: number; dashPattern: string; startX: number; endX: number; slideIndex: number }) {
210
249
  let lastSlideIndex = params.slideIndex;
211
250
  let animationId: number | null = null;
212
251
  function runAnimation() {
@@ -215,24 +254,63 @@
215
254
  const svg = node.closest('svg') as SVGSVGElement | null;
216
255
  if (!svg) return;
217
256
  const goesLeftToRight = params.endX >= params.startX;
218
- svg.style.clipPath = goesLeftToRight ? 'inset(0 100% 0 0)' : 'inset(0 0 0 100%)';
219
- const startTime = performance.now();
257
+ const mode = params.mode;
220
258
  const dur = params.duration;
259
+ const startTime = performance.now();
260
+ if (mode === 'draw' || mode === 'draw-undraw') {
261
+ svg.style.clipPath = goesLeftToRight ? 'inset(0 100% 0 0)' : 'inset(0 0 0 100%)';
262
+ } else if (mode === 'undraw') {
263
+ svg.style.clipPath = 'none';
264
+ }
221
265
  function animate(currentTime: number) {
222
266
  const elapsed = currentTime - startTime;
223
- const progress = Math.min(elapsed / dur, 1);
224
- const eased = 1 - Math.pow(1 - progress, 3);
225
- const inset = 100 * (1 - eased);
226
- svg!.style.clipPath = goesLeftToRight ? `inset(0 ${inset}% 0 0)` : `inset(0 0 0 ${inset}%)`;
227
- if (progress < 1) animationId = requestAnimationFrame(animate);
228
- else { svg!.style.clipPath = 'none'; animationId = null; }
267
+ if (mode === 'draw') {
268
+ const progress = Math.min(elapsed / dur, 1);
269
+ const eased = 1 - Math.pow(1 - progress, 3);
270
+ const inset = 100 * (1 - eased);
271
+ svg!.style.clipPath = goesLeftToRight ? `inset(0 ${inset}% 0 0)` : `inset(0 0 0 ${inset}%)`;
272
+ if (progress < 1) { animationId = requestAnimationFrame(animate); }
273
+ else { svg!.style.clipPath = 'none'; animationId = null; }
274
+ } else if (mode === 'undraw') {
275
+ const progress = Math.min(elapsed / dur, 1);
276
+ const eased = 1 - Math.pow(1 - progress, 3);
277
+ const inset = 100 * eased;
278
+ svg!.style.clipPath = goesLeftToRight ? `inset(0 0 0 ${inset}%)` : `inset(0 ${inset}% 0 0)`;
279
+ if (progress < 1) { animationId = requestAnimationFrame(animate); }
280
+ else { svg!.style.clipPath = 'inset(0 0 0 100%)'; animationId = null; }
281
+ } else if (mode === 'draw-undraw') {
282
+ const halfDur = dur / 2;
283
+ if (elapsed < halfDur) {
284
+ const progress = Math.min(elapsed / halfDur, 1);
285
+ const eased = 1 - Math.pow(1 - progress, 3);
286
+ const inset = 100 * (1 - eased);
287
+ svg!.style.clipPath = goesLeftToRight ? `inset(0 ${inset}% 0 0)` : `inset(0 0 0 ${inset}%)`;
288
+ animationId = requestAnimationFrame(animate);
289
+ } else {
290
+ const progress = Math.min((elapsed - halfDur) / halfDur, 1);
291
+ const eased = 1 - Math.pow(1 - progress, 3);
292
+ const inset = 100 * eased;
293
+ svg!.style.clipPath = goesLeftToRight ? `inset(0 0 0 ${inset}%)` : `inset(0 ${inset}% 0 0)`;
294
+ if (progress < 1) { animationId = requestAnimationFrame(animate); }
295
+ else { svg!.style.clipPath = 'inset(0 0 0 100%)'; animationId = null; }
296
+ }
297
+ }
229
298
  }
230
299
  animationId = requestAnimationFrame(animate);
231
300
  }
232
301
  runAnimation();
233
302
  return {
234
303
  update(newParams: typeof params) {
235
- if (newParams.slideIndex !== lastSlideIndex) { lastSlideIndex = newParams.slideIndex; params = newParams; runAnimation(); }
304
+ const slideChanged = newParams.slideIndex !== lastSlideIndex;
305
+ params = newParams;
306
+ if (!params.enabled) {
307
+ if (animationId) { cancelAnimationFrame(animationId); animationId = null; }
308
+ const svg = node.closest('svg') as SVGSVGElement | null;
309
+ if (svg) svg.style.clipPath = '';
310
+ lastSlideIndex = newParams.slideIndex;
311
+ return;
312
+ }
313
+ if (slideChanged) { lastSlideIndex = newParams.slideIndex; runAnimation(); }
236
314
  },
237
315
  destroy() { if (animationId) cancelAnimationFrame(animationId); }
238
316
  };
@@ -254,7 +332,7 @@
254
332
  for (const element of slide.canvas.elements) {
255
333
  if (!animatedElements.has(element.id)) {
256
334
  const inCurrent = getElementInSlide(currentSlide, element.id);
257
- const startOpacity = inCurrent ? 1 : 0;
335
+ const startOpacity = inCurrent ? ((inCurrent as any).opacity ?? 1) : 0;
258
336
  const br = (element as any).borderRadius ?? 0;
259
337
  const isShape = element.type === 'shape';
260
338
  const shapeEl = isShape ? element as ShapeElement : null;
@@ -278,7 +356,12 @@
278
356
  strokeColor: shapeEl ? tween(shapeEl.strokeColor, { duration: 500 }) : null,
279
357
  strokeWidth: shapeEl ? tween(shapeEl.strokeWidth, { duration: 500 }) : null,
280
358
  shapeMorph: shapeEl ? tween(1, { duration: 500 }) : null,
281
- motionPathProgress: element.motionPathConfig ? tween(0, { duration: 500 }) : null
359
+ motionPathProgress: element.motionPathConfig ? tween(0, { duration: 500 }) : null,
360
+ blur: tween(element.blur ?? 0, { duration: 500 }),
361
+ brightness: tween(element.brightness ?? 100, { duration: 500 }),
362
+ contrast: tween(element.contrast ?? 100, { duration: 500 }),
363
+ saturate: tween(element.saturate ?? 100, { duration: 500 }),
364
+ grayscale: tween(element.grayscale ?? 0, { duration: 500 })
282
365
  });
283
366
  const currentSlideEl = getElementInSlide(currentSlide, element.id);
284
367
  elementContent.set(element.id, JSON.parse(JSON.stringify(currentSlideEl || element)));
@@ -315,11 +398,14 @@
315
398
  const shouldLoop = element.motionPathConfig.loop;
316
399
 
317
400
  if (shouldLoop) {
401
+ const laps = element.motionPathConfig.laps ?? 0;
318
402
  (async () => {
319
- while (!signal.aborted) {
403
+ let lap = 0;
404
+ while (!signal.aborted && (laps === 0 || lap < laps)) {
320
405
  await animated.motionPathProgress!.to(0, { duration: 0 });
321
406
  await animated.motionPathProgress!.to(1, { duration, easing });
322
- if (!signal.aborted) await new Promise(r => setTimeout(r, 50));
407
+ lap++;
408
+ if (!signal.aborted && (laps === 0 || lap < laps)) await new Promise(r => setTimeout(r, 50));
323
409
  }
324
410
  })();
325
411
  } else {
@@ -368,8 +454,13 @@
368
454
  animated.skewX.to(targetEl.skewX ?? 0, { duration: 0 }); animated.skewY.to(targetEl.skewY ?? 0, { duration: 0 });
369
455
  animated.tiltX.to(targetEl.tiltX ?? 0, { duration: 0 }); animated.tiltY.to(targetEl.tiltY ?? 0, { duration: 0 });
370
456
  animated.perspective.to(targetEl.perspective ?? 1000, { duration: 0 });
371
- animated.opacity.to(1, { duration: 0 });
457
+ animated.opacity.to((targetEl as any).opacity ?? 1, { duration: 0 });
372
458
  animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: 0 });
459
+ animated.blur.to(targetEl.blur ?? 0, { duration: 0 });
460
+ animated.brightness.to(targetEl.brightness ?? 100, { duration: 0 });
461
+ animated.contrast.to(targetEl.contrast ?? 100, { duration: 0 });
462
+ animated.saturate.to(targetEl.saturate ?? 100, { duration: 0 });
463
+ animated.grayscale.to(targetEl.grayscale ?? 0, { duration: 0 });
373
464
  if (targetEl.type === 'text') animated.fontSize?.to((targetEl as TextElement).fontSize, { duration: 0 });
374
465
  if (targetEl.type === 'shape') {
375
466
  const s = targetEl as ShapeElement;
@@ -433,7 +524,12 @@
433
524
  await animated.tiltY.to(currentEl.tiltY ?? 0, { duration: 0 });
434
525
  await animated.perspective.to(currentEl.perspective ?? 1000, { duration: 0 });
435
526
  await animated.borderRadius.to((currentEl as any).borderRadius ?? 0, { duration: 0 });
436
- await animated.opacity.to(1, { duration: 0 });
527
+ await animated.blur.to(currentEl.blur ?? 0, { duration: 0 });
528
+ await animated.brightness.to(currentEl.brightness ?? 100, { duration: 0 });
529
+ await animated.contrast.to(currentEl.contrast ?? 100, { duration: 0 });
530
+ await animated.saturate.to(currentEl.saturate ?? 100, { duration: 0 });
531
+ await animated.grayscale.to(currentEl.grayscale ?? 0, { duration: 0 });
532
+ await animated.opacity.to((currentEl as any).opacity ?? 1, { duration: 0 });
437
533
  if (currentEl.type === 'text' && animated.fontSize) await animated.fontSize.to((currentEl as TextElement).fontSize, { duration: 0 });
438
534
  if (currentEl.type === 'shape') {
439
535
  const s = currentEl as ShapeElement;
@@ -493,7 +589,18 @@
493
589
  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 })); }
494
590
  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 })); }
495
591
  if (!sequencedProps.has('borderRadius')) anims.push(animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: elementDuration, easing }));
592
+ if (!sequencedProps.has('blur')) {
593
+ animated.blur.to(targetEl.blur ?? 0, { duration: elementDuration, easing });
594
+ animated.brightness.to(targetEl.brightness ?? 100, { duration: elementDuration, easing });
595
+ animated.contrast.to(targetEl.contrast ?? 100, { duration: elementDuration, easing });
596
+ animated.saturate.to(targetEl.saturate ?? 100, { duration: elementDuration, easing });
597
+ animated.grayscale.to(targetEl.grayscale ?? 0, { duration: elementDuration, easing });
598
+ }
496
599
  if (!sequencedProps.has('perspective')) anims.push(animated.perspective.to(targetEl.perspective ?? 1000, { duration: elementDuration, easing }));
600
+ if (!sequencedProps.has('opacity')) {
601
+ const targetOpacity = (targetEl as any).opacity ?? 1;
602
+ if (animated.opacity.current !== targetOpacity) anims.push(animated.opacity.to(targetOpacity, { duration: elementDuration, easing }));
603
+ }
497
604
  const sortedSeqs = [...propertySequences].sort((a, b) => a.order - b.order);
498
605
  let cumulativeDelay = 0;
499
606
  for (const seq of sortedSeqs) {
@@ -506,6 +613,13 @@
506
613
  else if (seq.property === 'skew') { animated.skewX.to(targetEl.skewX ?? 0, { duration: seqDuration, easing }); animated.skewY.to(targetEl.skewY ?? 0, { duration: seqDuration, easing }); }
507
614
  else if (seq.property === 'size') { animated.width.to(targetEl.size.width, { duration: seqDuration, easing }); animated.height.to(targetEl.size.height, { duration: seqDuration, easing }); }
508
615
  else if (seq.property === 'borderRadius') animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: seqDuration, easing });
616
+ else if (seq.property === 'blur') {
617
+ animated.blur.to(targetEl.blur ?? 0, { duration: seqDuration, easing });
618
+ animated.brightness.to(targetEl.brightness ?? 100, { duration: seqDuration, easing });
619
+ animated.contrast.to(targetEl.contrast ?? 100, { duration: seqDuration, easing });
620
+ animated.saturate.to(targetEl.saturate ?? 100, { duration: seqDuration, easing });
621
+ animated.grayscale.to(targetEl.grayscale ?? 0, { duration: seqDuration, easing });
622
+ }
509
623
  else if (seq.property === 'color' && targetEl.type === 'shape') {
510
624
  const s = targetEl as ShapeElement;
511
625
  animated.fillColor?.to(s.fillColor, { duration: seqDuration, easing });
@@ -513,6 +627,7 @@
513
627
  animated.strokeWidth?.to(s.strokeWidth, { duration: seqDuration, easing });
514
628
  }
515
629
  else if (seq.property === 'perspective') animated.perspective.to(targetEl.perspective ?? 1000, { duration: seqDuration, easing });
630
+ else if (seq.property === 'opacity') animated.opacity.to((targetEl as any).opacity ?? 1, { duration: seqDuration, easing });
516
631
  }, seqDelay);
517
632
  cumulativeDelay = seqDelay + seqDuration;
518
633
  }
@@ -529,6 +644,17 @@
529
644
  anims.push(animated.tiltY.to(targetEl.tiltY ?? 0, { duration: elementDuration, easing }));
530
645
  anims.push(animated.perspective.to(targetEl.perspective ?? 1000, { duration: elementDuration, easing }));
531
646
  anims.push(animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: elementDuration, easing }));
647
+ anims.push(animated.blur.to(targetEl.blur ?? 0, { duration: elementDuration, easing }));
648
+ anims.push(animated.brightness.to(targetEl.brightness ?? 100, { duration: elementDuration, easing }));
649
+ anims.push(animated.contrast.to(targetEl.contrast ?? 100, { duration: elementDuration, easing }));
650
+ anims.push(animated.saturate.to(targetEl.saturate ?? 100, { duration: elementDuration, easing }));
651
+ anims.push(animated.grayscale.to(targetEl.grayscale ?? 0, { duration: elementDuration, easing }));
652
+ // Opacity interpolation for morphing elements
653
+ const currOpacity = animated.opacity.current;
654
+ const targetOpacity = (targetEl as any).opacity ?? 1;
655
+ if (currOpacity !== targetOpacity) {
656
+ anims.push(animated.opacity.to(targetOpacity, { duration: elementDuration, easing }));
657
+ }
532
658
  }
533
659
  // Motion path progress — await reset, then animate forward
534
660
  if (animated.motionPathProgress && targetEl.motionPathConfig) {
@@ -575,6 +701,11 @@
575
701
  anims.push(animated.tiltY.to(targetEl.tiltY ?? 0, { duration: 0 }));
576
702
  anims.push(animated.perspective.to(targetEl.perspective ?? 1000, { duration: 0 }));
577
703
  anims.push(animated.borderRadius.to((targetEl as any).borderRadius ?? 0, { duration: 0 }));
704
+ anims.push(animated.blur.to(targetEl.blur ?? 0, { duration: 0 }));
705
+ anims.push(animated.brightness.to(targetEl.brightness ?? 100, { duration: 0 }));
706
+ anims.push(animated.contrast.to(targetEl.contrast ?? 100, { duration: 0 }));
707
+ anims.push(animated.saturate.to(targetEl.saturate ?? 100, { duration: 0 }));
708
+ anims.push(animated.grayscale.to(targetEl.grayscale ?? 0, { duration: 0 }));
578
709
  if (targetEl.type === 'text' && animated.fontSize) {
579
710
  anims.push(animated.fontSize.to((targetEl as TextElement).fontSize, { duration: 0 }));
580
711
  }
@@ -585,10 +716,11 @@
585
716
  if (animated.strokeWidth) anims.push(animated.strokeWidth.to(s.strokeWidth, { duration: 0 }));
586
717
  }
587
718
  const entrance = targetEl.animationConfig?.entrance ?? 'fade';
719
+ const targetOpacity = (targetEl as any).opacity ?? 1;
588
720
  if (entrance === 'fade') {
589
- anims.push(animated.opacity.to(1, { duration: elementDuration / 2, easing }));
721
+ anims.push(animated.opacity.to(targetOpacity, { duration: elementDuration / 2, easing }));
590
722
  } else {
591
- anims.push(animated.opacity.to(1, { duration: 0 }));
723
+ anims.push(animated.opacity.to(targetOpacity, { duration: 0 }));
592
724
  }
593
725
  }
594
726
  return anims;
@@ -659,6 +791,13 @@
659
791
  elementContent = newElementContent;
660
792
  currentSlideIndex = targetIndex;
661
793
  isTransitioning = false;
794
+ // Ensure elements not on the new slide are fully hidden
795
+ const newSlide = slides[targetIndex];
796
+ for (const elementId of allElementIds) {
797
+ const onSlide = getElementInSlide(newSlide, elementId);
798
+ const animated = animatedElements.get(elementId);
799
+ if (!onSlide && animated) { animated.opacity.to(0, { duration: 0 }); }
800
+ }
662
801
  animateMotionPaths(slides[targetIndex]);
663
802
  onslidechange?.(targetIndex, slides.length);
664
803
  if (targetIndex === slides.length - 1 && !loop) oncomplete?.();
@@ -877,8 +1016,14 @@
877
1016
  {@const mpElement = mpConfig ? currentSlide?.canvas.elements.find(el => el.id === mpConfig.motionPathId) as MotionPathElement | undefined : undefined}
878
1017
  {@const mpProgress = animated?.motionPathProgress?.current ?? 0}
879
1018
  {@const mpPoint = mpElement && mpConfig ? getPresenterPointOnPath(mpElement.points, mpElement.closed, (mpConfig.startPercent + (mpConfig.endPercent - mpConfig.startPercent) * mpProgress) / 100) : null}
880
- {@const elemX = mpPoint ? mpPoint.x - (animated?.width.current ?? 0) / 2 : animated?.x.current ?? 0}
881
- {@const elemY = mpPoint ? mpPoint.y - (animated?.height.current ?? 0) / 2 : animated?.y.current ?? 0}
1019
+ {@const mpStartPoint = mpElement && mpConfig ? getPresenterPointOnPath(mpElement.points, mpElement.closed, mpConfig.startPercent / 100) : null}
1020
+ {@const mpPos = mpPoint && mpStartPoint && animated
1021
+ ? computeMotionPathPosition(mpPoint, mpStartPoint,
1022
+ animated.x.current, animated.y.current,
1023
+ animated.width.current, animated.height.current)
1024
+ : null}
1025
+ {@const elemX = mpPos ? mpPos.x : (animated?.x.current ?? 0)}
1026
+ {@const elemY = mpPos ? mpPos.y : (animated?.y.current ?? 0)}
882
1027
  {@const mpRotation = mpPoint && mpConfig?.autoRotate
883
1028
  ? mpPoint.angle + (mpConfig.orientationOffset ?? 0)
884
1029
  : null}
@@ -899,22 +1044,52 @@
899
1044
  style:--float-speed="{hasFloat ? computeFloatSpeed(floatCfg, floatGroupId || elementId) : 3}s"
900
1045
  style:--float-delay="{hashFraction(floatGroupId || elementId, 3) * 2}s"
901
1046
  style:animation-name={hasFloat ? getFloatAnimName(floatCfg!.direction, floatGroupId || elementId) : 'none'}
1047
+ style:filter={(() => { const parts: string[] = []; const b = animated.blur.current; const br2 = animated.brightness.current; const c = animated.contrast.current; const s = animated.saturate.current; const g = animated.grayscale.current; if (b) parts.push(`blur(${b}px)`); if (br2 !== 100) parts.push(`brightness(${br2}%)`); if (c !== 100) parts.push(`contrast(${c}%)`); if (s !== 100) parts.push(`saturate(${s}%)`); if (g) parts.push(`grayscale(${g}%)`); return parts.length ? parts.join(' ') : 'none'; })()}
902
1048
  >
903
1049
  {#if element.type === 'code'}
904
1050
  {@const codeEl = element as CodeElement}
905
1051
  {@const morphState = codeMorphState.get(codeEl.id)}
906
- <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">
1052
+ <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'}>
907
1053
  {#if codeEl.showHeader}
908
- <div class="animot-code-header" class:macos={codeEl.headerStyle === 'macos'}>
1054
+ <div class="animot-code-header" class:macos={codeEl.headerStyle === 'macos'} class:windows={codeEl.headerStyle === 'windows'} style:border-radius="{codeEl.headerRadius ?? animated.borderRadius.current}px {codeEl.headerRadius ?? animated.borderRadius.current}px 0 0">
909
1055
  {#if codeEl.headerStyle === 'macos'}
910
1056
  <div class="animot-window-controls">
911
1057
  <span class="animot-control close"></span>
912
1058
  <span class="animot-control minimize"></span>
913
1059
  <span class="animot-control maximize"></span>
914
1060
  </div>
1061
+ {:else if codeEl.headerStyle === 'windows'}
1062
+ <div class="animot-window-controls">
1063
+ <span class="animot-control win-minimize">
1064
+ <svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 5h6" stroke="currentColor" stroke-width="1.2"/></svg>
1065
+ </span>
1066
+ <span class="animot-control win-maximize">
1067
+ <svg width="10" height="10" viewBox="0 0 10 10"><rect x="1.5" y="1.5" width="7" height="7" rx="0.5" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>
1068
+ </span>
1069
+ <span class="animot-control win-close">
1070
+ <svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.2"/></svg>
1071
+ </span>
1072
+ </div>
915
1073
  {/if}
916
- <span class="animot-filename">{codeEl.filename}</span>
1074
+ <div class="animot-filename-tab" style:border-radius="{codeEl.tabRadius ?? 6}px">
1075
+ <svg class="animot-file-icon" width="14" height="14" viewBox="0 0 16 16" fill="none">
1076
+ <path d="M4 1h5.5L13 4.5V14a1 1 0 01-1 1H4a1 1 0 01-1-1V2a1 1 0 011-1z" stroke="currentColor" stroke-width="1.2" opacity="0.5"/>
1077
+ <path d="M9.5 1v3.5H13" stroke="currentColor" stroke-width="1.2" opacity="0.5"/>
1078
+ </svg>
1079
+ <span class="animot-filename">{codeEl.filename}</span>
1080
+ </div>
1081
+ <button class="animot-copy-code-btn" onclick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(codeEl.code); const btn = e.currentTarget as HTMLElement; btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500); }}>
1082
+ <span class="animot-copy-label">Copy</span><span class="animot-copied-label">Copied!</span>
1083
+ <svg class="animot-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
1084
+ <svg class="animot-check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
1085
+ </button>
917
1086
  </div>
1087
+ {:else}
1088
+ <button class="animot-copy-code-btn animot-floating" onclick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(codeEl.code); const btn = e.currentTarget as HTMLElement; btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500); }}>
1089
+ <span class="animot-copy-label">Copy</span><span class="animot-copied-label">Copied!</span>
1090
+ <svg class="animot-copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
1091
+ <svg class="animot-check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
1092
+ </button>
918
1093
  {/if}
919
1094
  <div class="animot-code-content">
920
1095
  <div class="animot-highlighted-code">
@@ -951,7 +1126,6 @@
951
1126
  style:border-radius="{textEl.borderRadius}px"
952
1127
  style:text-align={textEl.textAlign}
953
1128
  style:justify-content={textEl.textAlign === 'center' ? 'center' : textEl.textAlign === 'right' ? 'flex-end' : 'flex-start'}
954
- style:opacity={textEl.opacity ?? 1}
955
1129
  style:-webkit-text-stroke={textEl.textStroke?.enabled ? `${textEl.textStroke.width}px ${textEl.textStroke.color}` : '0'}
956
1130
  style:text-shadow={textEl.textShadow?.enabled ? `${textEl.textShadow.offsetX}px ${textEl.textShadow.offsetY}px ${textEl.textShadow.blur}px ${textEl.textShadow.color}` : 'none'}
957
1131
  >
@@ -960,25 +1134,27 @@
960
1134
  {:else if element.type === 'arrow'}
961
1135
  {@const arrowEl = element as ArrowElement}
962
1136
  {@const cp = arrowEl.controlPoints || []}
963
- {@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}`}
964
- {@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)}
1137
+ {@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)}
1138
+ {@const lastCp = cp.length > 0 ? cp[cp.length - 1] : arrowEl.startPoint}
1139
+ {@const endAngle = Math.atan2(arrowEl.endPoint.y - lastCp.y, arrowEl.endPoint.x - lastCp.x)}
965
1140
  {@const headAngle = Math.PI / 6}
966
1141
  {@const headSize = arrowEl.headSize}
967
1142
  {@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)}`}
968
1143
  {@const arrowAnimMode = arrowEl.animation?.mode ?? 'none'}
969
1144
  {@const arrowAnimDuration = arrowEl.animation?.duration ?? 500}
970
1145
  {@const isStyledArrow = arrowEl.style !== 'solid'}
1146
+ {@const isDrawType = arrowAnimMode === 'draw' || arrowAnimMode === 'undraw' || arrowAnimMode === 'draw-undraw'}
971
1147
  {@const baseDashArray = arrowEl.style === 'dashed' ? '10,5' : arrowEl.style === 'dotted' ? '2,5' : 'none'}
972
- <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="--arrow-anim-duration: {arrowAnimDuration}ms;">
973
- <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 }} />
1148
+ <svg class="animot-arrow-element" class:arrow-animate-draw={arrowAnimMode === 'draw' && !isStyledArrow} class:arrow-animate-undraw={arrowAnimMode === 'undraw' && !isStyledArrow} class:arrow-animate-draw-undraw={arrowAnimMode === 'draw-undraw' && !isStyledArrow} class:arrow-animate-grow={arrowAnimMode === 'grow'} viewBox="0 0 {arrowEl.size.width} {arrowEl.size.height}" preserveAspectRatio="none" style="--arrow-anim-duration: {arrowAnimDuration}ms;">
1149
+ <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: isDrawType && isStyledArrow, mode: arrowAnimMode, duration: arrowAnimDuration, dashPattern: baseDashArray, startX: arrowEl.startPoint.x, endX: arrowEl.endPoint.x, slideIndex: currentSlideIndex }} />
974
1150
  {#if arrowEl.showHead !== false}
975
- <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;` : ''} />
1151
+ <path class="arrow-head" class:arrow-head-styled-draw={isDrawType && isStyledArrow} class:arrow-head-undraw={arrowAnimMode === 'undraw'} class:arrow-head-draw-undraw={arrowAnimMode === 'draw-undraw'} d={arrowHeadPath} fill="none" stroke={arrowEl.color} stroke-width={arrowEl.strokeWidth} stroke-linecap="round" stroke-linejoin="round" style={isDrawType && isStyledArrow ? `--arrow-anim-duration: ${arrowAnimDuration}ms;` : ''} />
976
1152
  {/if}
977
1153
  </svg>
978
1154
  {:else if element.type === 'image'}
979
1155
  {@const imgEl = element as ImageElement}
980
1156
  {@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'}
981
- <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} style:background-color={imgEl.backgroundColor ?? 'transparent'} />
1157
+ <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'} />
982
1158
  {:else if element.type === 'shape'}
983
1159
  {@const shapeEl = element as ShapeElement}
984
1160
  {@const animFill = animated.fillColor?.current ?? shapeEl.fillColor}
@@ -988,15 +1164,15 @@
988
1164
  {@const morphProgress = animated.shapeMorph?.current ?? 1}
989
1165
  {@const effectiveShapeType = mState ? (morphProgress >= 1 ? mState.toType : (morphProgress <= 0 ? mState.fromType : null)) : shapeEl.shapeType}
990
1166
  {@const isMorphing = mState && morphProgress > 0 && morphProgress < 1}
991
- <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'}>
1167
+ <svg class="animot-shape-element" viewBox="0 0 {animated.width.current} {animated.height.current}" style:filter={shapeEl.boxShadow?.enabled ? `drop-shadow(${shapeEl.boxShadow.offsetX}px ${shapeEl.boxShadow.offsetY}px ${shapeEl.boxShadow.blur}px ${shapeEl.boxShadow.color})` : 'none'}>
992
1168
  {#if isMorphing}
993
1169
  {@const w = animated.width.current}
994
1170
  {@const h = animated.height.current}
995
1171
  {@const sw = animStrokeWidth}
996
- <g style:opacity={1 - morphProgress}>{@html renderShape(mState!.fromType, w, h, animated.borderRadius.current, animFill, animStroke, sw)}</g>
997
- <g style:opacity={morphProgress}>{@html renderShape(mState!.toType, w, h, animated.borderRadius.current, animFill, animStroke, sw)}</g>
1172
+ <g style:opacity={1 - morphProgress}>{@html renderShape(mState!.fromType, w, h, animated.borderRadius.current, animFill, animStroke, sw, shapeEl.strokeStyle, shapeEl.strokeDashGap)}</g>
1173
+ <g style:opacity={morphProgress}>{@html renderShape(mState!.toType, w, h, animated.borderRadius.current, animFill, animStroke, sw, shapeEl.strokeStyle, shapeEl.strokeDashGap)}</g>
998
1174
  {:else}
999
- {@html renderShape(effectiveShapeType ?? shapeEl.shapeType, animated.width.current, animated.height.current, animated.borderRadius.current, animFill, animStroke, animStrokeWidth)}
1175
+ {@html renderShape(effectiveShapeType ?? shapeEl.shapeType, animated.width.current, animated.height.current, animated.borderRadius.current, animFill, animStroke, animStrokeWidth, shapeEl.strokeStyle, shapeEl.strokeDashGap)}
1000
1176
  {/if}
1001
1177
  </svg>
1002
1178
  {:else if element.type === 'counter'}
@@ -1008,7 +1184,7 @@
1008
1184
  {:else if element.type === 'svg'}
1009
1185
  {@const svgEl = element as SvgElement}
1010
1186
  {@const svgParsed = (() => { const m = svgEl.svgContent.trim().match(/^<svg([^>]*)>([\s\S]*)<\/svg>$/i); if (m) { const vb = m[1].match(/viewBox=["']([^"']+)["']/i); return { inner: m[2], viewBox: vb ? vb[1] : null }; } return { inner: svgEl.svgContent, viewBox: null }; })()}
1011
- <div class="animot-svg-element" style:opacity={svgEl.opacity}>
1187
+ <div class="animot-svg-element">
1012
1188
  <svg width="100%" height="100%" viewBox={svgEl.viewBox ?? svgParsed.viewBox ?? `0 0 ${svgEl.size.width} ${svgEl.size.height}`} preserveAspectRatio={svgEl.preserveAspectRatio} xmlns="http://www.w3.org/2000/svg">
1013
1189
  <g style={svgEl.color ? `fill:${svgEl.color};stroke:${svgEl.color}` : ''}>
1014
1190
  {@html svgParsed.inner}
@@ -1018,7 +1194,7 @@
1018
1194
  {:else if element.type === 'motionPath'}
1019
1195
  {@const mpEl = element as MotionPathElement}
1020
1196
  {#if mpEl.showInPresentation}
1021
- <svg width="100%" height="100%" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;">
1197
+ <svg width="100%" height="100%" viewBox="0 0 {animated.width.current} {animated.height.current}" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;">
1022
1198
  <path d={buildPresenterPathD(mpEl.points, mpEl.closed)} stroke={mpEl.pathColor} stroke-width={mpEl.pathWidth} fill="none" stroke-dasharray="8 4" />
1023
1199
  </svg>
1024
1200
  {/if}
@@ -1064,21 +1240,29 @@
1064
1240
  </div>
1065
1241
 
1066
1242
  <script module lang="ts">
1067
- function renderShape(type: string, w: number, h: number, br: number, fill: string, stroke: string, sw: number): string {
1243
+ function renderShape(type: string, w: number, h: number, br: number, fill: string, stroke: string, sw: number, strokeStyle?: string, strokeDashGap?: number): string {
1244
+ let dashAttr = '';
1245
+ if (strokeStyle && strokeStyle !== 'solid') {
1246
+ const s = sw || 1;
1247
+ const gap = strokeDashGap ?? (strokeStyle === 'dashed' ? s * 3 : s * 2);
1248
+ const da = strokeStyle === 'dashed' ? `${s * 3},${gap}` : `${s * 0.1},${gap}`;
1249
+ const lc = strokeStyle === 'dotted' ? 'round' : 'butt';
1250
+ dashAttr = ` stroke-dasharray="${da}" stroke-linecap="${lc}"`;
1251
+ }
1068
1252
  switch (type) {
1069
- 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}"/>`;
1070
- 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}"/>`;
1071
- 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}"/>`;
1072
- 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"/>`;
1253
+ case 'rectangle': return `<rect x="${sw/2}" y="${sw/2}" width="${w-sw}" height="${h-sw}" rx="${br}" ry="${br}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
1254
+ case 'circle': return `<circle cx="${w/2}" cy="${h/2}" r="${Math.min(w,h)/2-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
1255
+ case 'ellipse': return `<ellipse cx="${w/2}" cy="${h/2}" rx="${w/2-sw/2}" ry="${h/2-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
1256
+ 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}"${dashAttr} stroke-linejoin="round"/>`;
1073
1257
  case 'star': {
1074
1258
  const cx = w/2, cy = h/2, outerR = Math.min(w,h)/2-sw/2, innerR = outerR*0.4;
1075
1259
  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(' ');
1076
- return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" stroke-linejoin="round"/>`;
1260
+ return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr} stroke-linejoin="round"/>`;
1077
1261
  }
1078
1262
  case 'hexagon': {
1079
1263
  const cx = w/2, cy = h/2, r = Math.min(w,h)/2-sw/2;
1080
1264
  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(' ');
1081
- return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" stroke-linejoin="round"/>`;
1265
+ return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr} stroke-linejoin="round"/>`;
1082
1266
  }
1083
1267
  default: return '';
1084
1268
  }
@@ -1143,19 +1327,34 @@
1143
1327
 
1144
1328
  /* Code */
1145
1329
  .animot-code-block {
1146
- width: 100%; height: 100%; background: #0d1117; overflow: hidden;
1330
+ width: 100%; height: 100%; overflow: hidden;
1147
1331
  display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.4);
1148
1332
  margin: 0; box-sizing: border-box;
1149
1333
  }
1150
- .animot-code-block.transparent-bg { background: transparent; box-shadow: none; }
1151
- .animot-code-block.transparent-bg .animot-code-header { background: transparent; border-bottom-color: rgba(255,255,255,0.1); }
1152
- .animot-code-header { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; }
1153
- .animot-window-controls { display: flex; gap: 8px; }
1154
- .animot-control { width: 12px; height: 12px; border-radius: 50%; display: block; }
1334
+ .animot-code-block.transparent-bg { background: transparent !important; box-shadow: none; }
1335
+ .animot-code-block.transparent-bg .animot-code-header { background: transparent; border-bottom-color: rgba(255,255,255,0.06); }
1336
+ .animot-code-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.2); border-bottom: 1px solid rgba(255, 255, 255, 0.06); flex-shrink: 0; min-height: 40px; }
1337
+ .animot-window-controls { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
1338
+ .macos .animot-control { width: 12px; height: 12px; border-radius: 50%; display: block; }
1155
1339
  .macos .animot-control.close { background: #ff5f57; }
1156
1340
  .macos .animot-control.minimize { background: #febc2e; }
1157
1341
  .macos .animot-control.maximize { background: #28c840; }
1158
- .animot-filename { color: #8b949e; font-size: 14px; flex: 1; }
1342
+ .windows .animot-window-controls { order: 99; margin-left: auto; gap: 0; }
1343
+ .windows .animot-control { display: flex; align-items: center; justify-content: center; width: 28px; height: 24px; border-radius: 4px; color: rgba(255,255,255,0.45); }
1344
+ .animot-filename-tab { display: flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; padding: 4px 10px; max-width: 220px; color: rgba(255,255,255,0.4); }
1345
+ .animot-file-icon { flex-shrink: 0; }
1346
+ .animot-filename { color: rgba(255,255,255,0.55); font-size: 12px; line-height: 18px; }
1347
+ .animot-copy-code-btn { display: flex; align-items: center; gap: 5px; height: 28px; padding: 0 8px; margin-left: auto; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: rgba(255,255,255,0.4); cursor: pointer; opacity: 0; transition: opacity 0.2s, background 0.15s, color 0.15s; flex-shrink: 0; font-size: 12px; font-family: inherit; white-space: nowrap; }
1348
+ .animot-copy-code-btn:hover { background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.8); }
1349
+ .animot-copy-code-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
1350
+ .animot-copy-code-btn .animot-check-icon { display: none; }
1351
+ .animot-copy-code-btn .animot-copied-label { display: none; }
1352
+ .animot-copy-code-btn.copied .animot-copy-icon { display: none; }
1353
+ .animot-copy-code-btn.copied .animot-copy-label { display: none; }
1354
+ .animot-copy-code-btn.copied .animot-check-icon { display: block; color: #4ade80; }
1355
+ .animot-copy-code-btn.copied .animot-copied-label { display: inline; color: #4ade80; }
1356
+ .animot-copy-code-btn.animot-floating { position: absolute; top: 8px; right: 8px; z-index: 2; }
1357
+ .animot-code-block:hover .animot-copy-code-btn { opacity: 1; }
1159
1358
  .animot-code-content { flex: 1; overflow: hidden; position: relative; }
1160
1359
  .animot-highlighted-code { width: 100%; height: 100%; }
1161
1360
  .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; }
@@ -1170,11 +1369,19 @@
1170
1369
  /* Arrow */
1171
1370
  .animot-arrow-element { width: 100%; height: 100%; }
1172
1371
  .arrow-animate-draw .arrow-path { stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: animot-arrow-draw var(--arrow-anim-duration, 500ms) ease-out forwards; }
1372
+ .arrow-animate-undraw .arrow-path { stroke-dasharray: 1000; stroke-dashoffset: 0; animation: animot-arrow-undraw var(--arrow-anim-duration, 500ms) ease-out forwards; }
1373
+ .arrow-animate-draw-undraw .arrow-path { stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: animot-arrow-draw-undraw var(--arrow-anim-duration, 500ms) ease-out forwards; }
1173
1374
  .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); }
1174
1375
  .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); }
1376
+ .arrow-animate-undraw .arrow-head, .arrow-head-undraw { opacity: 1; animation: animot-arrow-head-disappear var(--arrow-anim-duration, 500ms) ease-out forwards; }
1377
+ .arrow-animate-draw-undraw .arrow-head, .arrow-head-draw-undraw { opacity: 0; animation: animot-arrow-head-draw-undraw var(--arrow-anim-duration, 500ms) ease-out forwards; }
1175
1378
  .arrow-animate-grow { transform-origin: left center; animation: animot-arrow-grow var(--arrow-anim-duration, 500ms) ease-out forwards; }
1176
1379
  @keyframes animot-arrow-draw { to { stroke-dashoffset: 0; } }
1380
+ @keyframes animot-arrow-undraw { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 1000; } }
1381
+ @keyframes animot-arrow-draw-undraw { 0% { stroke-dashoffset: 1000; } 50% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 1000; } }
1177
1382
  @keyframes animot-arrow-head-appear { from { opacity: 0; } to { opacity: 1; } }
1383
+ @keyframes animot-arrow-head-disappear { 0% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; } }
1384
+ @keyframes animot-arrow-head-draw-undraw { 0% { opacity: 0; } 35% { opacity: 1; } 65% { opacity: 1; } 100% { opacity: 0; } }
1178
1385
  @keyframes animot-arrow-grow { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }
1179
1386
 
1180
1387
  /* Image */