animot-presenter 0.6.5 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.ts CHANGED
@@ -432,6 +432,7 @@ export interface DrawElement extends BaseElement {
432
432
  streamline?: number;
433
433
  taperStart?: boolean;
434
434
  taperEnd?: boolean;
435
+ animation?: ArrowAnimationConfig;
435
436
  }
436
437
  export interface StickyElement extends BaseElement {
437
438
  type: 'sticky';
@@ -0,0 +1,19 @@
1
+ import type { DrawElement } from '../types';
2
+ type DrawStrokeEl = Pick<DrawElement, 'points' | 'brush' | 'strokeWidth' | 'thinning' | 'smoothing' | 'streamline' | 'taperStart' | 'taperEnd'>;
3
+ export interface FreehandDrawRevealParams {
4
+ enabled: boolean;
5
+ mode: 'draw' | 'undraw' | 'draw-undraw' | 'none' | string;
6
+ duration: number;
7
+ el: DrawStrokeEl;
8
+ loop?: boolean;
9
+ reverse?: boolean;
10
+ /** Slide loop duration in ms — when looping, the effective duration is
11
+ * rounded so an integer number of cycles fits (seamless GIF loop). */
12
+ slideDuration?: number;
13
+ key?: unknown;
14
+ }
15
+ export declare function freehandDrawReveal(node: SVGPathElement, params: FreehandDrawRevealParams): {
16
+ update(p: FreehandDrawRevealParams): void;
17
+ destroy(): void;
18
+ };
19
+ export {};
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Svelte action for freehand stroke-reveal (draw / undraw / draw-undraw).
3
+ *
4
+ * Unlike arrows (a thin stroked line revealed via stroke-dashoffset), a freehand
5
+ * element is a FILLED brush outline. Revealing it by masking leaks the future
6
+ * segment at self-crossings (the thick already-drawn mask overlaps wherever a
7
+ * later part of the loop passes through). So instead we rebuild the filled
8
+ * outline from the points up to the current arc-length each frame — the
9
+ * not-yet-drawn part simply doesn't exist yet, so loops draw cleanly.
10
+ *
11
+ * The action owns the host <path>'s `d` attribute (don't bind `d=` on it).
12
+ */
13
+ import { drawOutlineAtProgress } from './freehand';
14
+ export function freehandDrawReveal(node, params) {
15
+ let raf = 0;
16
+ const setShown = (shown) => node.setAttribute('d', drawOutlineAtProgress(params.el, shown, !!params.reverse));
17
+ const full = () => node.setAttribute('d', drawOutlineAtProgress(params.el, 1, !!params.reverse));
18
+ function reset() {
19
+ if (raf)
20
+ cancelAnimationFrame(raf);
21
+ raf = 0;
22
+ }
23
+ function run() {
24
+ reset();
25
+ if (!params.enabled || params.mode === 'none') {
26
+ full();
27
+ return;
28
+ }
29
+ const requested = Math.max(50, params.duration);
30
+ const dur = params.slideDuration && params.slideDuration > 0 && params.loop
31
+ ? params.slideDuration / Math.max(1, Math.round(params.slideDuration / requested))
32
+ : requested;
33
+ const start = performance.now();
34
+ const m = params.mode;
35
+ const cubicOut = (t) => 1 - Math.pow(1 - t, 3);
36
+ // Initial frame.
37
+ setShown(m === 'undraw' ? 1 : 0);
38
+ function step(now) {
39
+ const elapsed = now - start;
40
+ if (m === 'draw') {
41
+ const t = Math.min(elapsed / dur, 1);
42
+ setShown(cubicOut(t));
43
+ if (t < 1)
44
+ raf = requestAnimationFrame(step);
45
+ else if (params.loop)
46
+ run();
47
+ else {
48
+ full();
49
+ raf = 0;
50
+ }
51
+ }
52
+ else if (m === 'undraw') {
53
+ const t = Math.min(elapsed / dur, 1);
54
+ setShown(1 - cubicOut(t));
55
+ if (t < 1)
56
+ raf = requestAnimationFrame(step);
57
+ else if (params.loop)
58
+ run();
59
+ else {
60
+ setShown(0);
61
+ raf = 0;
62
+ }
63
+ }
64
+ else if (m === 'draw-undraw') {
65
+ const half = dur / 2;
66
+ if (elapsed < half) {
67
+ setShown(cubicOut(Math.min(elapsed / half, 1)));
68
+ raf = requestAnimationFrame(step);
69
+ }
70
+ else {
71
+ const t = Math.min((elapsed - half) / half, 1);
72
+ setShown(1 - cubicOut(t));
73
+ if (t < 1)
74
+ raf = requestAnimationFrame(step);
75
+ else if (params.loop)
76
+ run();
77
+ else {
78
+ setShown(0);
79
+ raf = 0;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ raf = requestAnimationFrame(step);
85
+ }
86
+ queueMicrotask(run);
87
+ // Let the video-export pipeline restart this under the virtual clock.
88
+ if (typeof window !== 'undefined') {
89
+ const reg = (window.__svgAnimRestart ||= []);
90
+ reg.push(run);
91
+ node.__svgAnimRestart = run;
92
+ }
93
+ let lastKey = params.key;
94
+ return {
95
+ update(p) {
96
+ const keyChanged = p.key !== lastKey;
97
+ params = p;
98
+ if (keyChanged) {
99
+ lastKey = p.key;
100
+ queueMicrotask(run);
101
+ }
102
+ },
103
+ destroy() {
104
+ reset();
105
+ if (typeof window !== 'undefined') {
106
+ const reg = window.__svgAnimRestart;
107
+ const fn = node.__svgAnimRestart;
108
+ if (reg && fn) {
109
+ const idx = reg.indexOf(fn);
110
+ if (idx >= 0)
111
+ reg.splice(idx, 1);
112
+ }
113
+ }
114
+ }
115
+ };
116
+ }
@@ -8,6 +8,15 @@ export interface StrokeOptions {
8
8
  taperEnd: boolean;
9
9
  }
10
10
  export declare function resolveStrokeOptions(el: Pick<DrawElement, 'brush' | 'strokeWidth' | 'thinning' | 'smoothing' | 'streamline' | 'taperStart' | 'taperEnd'>): StrokeOptions;
11
+ type DrawStrokeEl = Pick<DrawElement, 'points' | 'brush' | 'strokeWidth' | 'thinning' | 'smoothing' | 'streamline' | 'taperStart' | 'taperEnd'>;
12
+ /**
13
+ * Filled-outline `d` for the stroke revealed up to `fraction` (0..1) of its arc
14
+ * length. Rebuilds the geometry from the partial point list each call — so at a
15
+ * self-crossing the not-yet-drawn segment simply isn't there (a mask reveal
16
+ * leaks the future segment wherever the thick already-drawn mask overlaps it).
17
+ * The growing tip is left capped (a pen nib); the real taper applies at full.
18
+ */
19
+ export declare function drawOutlineAtProgress(el: DrawStrokeEl, fraction: number, reverse?: boolean): string;
11
20
  /** Compute the axis-aligned bounding box of raw input points. */
12
21
  export declare function pointsBounds(points: number[][]): {
13
22
  minX: number;
@@ -24,3 +33,4 @@ export declare function drawElementToPath(el: Pick<DrawElement, 'points' | 'brus
24
33
  d: string;
25
34
  viewBox: string;
26
35
  };
36
+ export {};
@@ -30,6 +30,72 @@ function outlineToPath(points) {
30
30
  d.push('Z');
31
31
  return d.join(' ');
32
32
  }
33
+ /** Slice the input points to the first `fraction` (0..1) of total arc length,
34
+ * interpolating the final partial segment. `reverse` slices from the far end
35
+ * so the reveal can run end→start. */
36
+ function pointsUpToFraction(points, fraction, reverse) {
37
+ let pts = reverse ? [...points].reverse() : points;
38
+ const n = pts.length;
39
+ if (n === 0)
40
+ return [];
41
+ if (fraction >= 1)
42
+ return pts;
43
+ let total = 0;
44
+ const seg = [];
45
+ for (let i = 1; i < n; i++) {
46
+ const l = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
47
+ seg.push(l);
48
+ total += l;
49
+ }
50
+ if (total === 0)
51
+ return [pts[0]];
52
+ const target = fraction * total;
53
+ const out = [pts[0]];
54
+ let acc = 0;
55
+ for (let i = 1; i < n; i++) {
56
+ const l = seg[i - 1];
57
+ if (acc + l >= target) {
58
+ const t = l > 0 ? (target - acc) / l : 0;
59
+ const x = pts[i - 1][0] + (pts[i][0] - pts[i - 1][0]) * t;
60
+ const y = pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * t;
61
+ const p0 = pts[i - 1][2], p1 = pts[i][2];
62
+ out.push(p0 !== undefined && p1 !== undefined ? [x, y, p0 + (p1 - p0) * t] : [x, y]);
63
+ break;
64
+ }
65
+ acc += l;
66
+ out.push(pts[i]);
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Filled-outline `d` for the stroke revealed up to `fraction` (0..1) of its arc
72
+ * length. Rebuilds the geometry from the partial point list each call — so at a
73
+ * self-crossing the not-yet-drawn segment simply isn't there (a mask reveal
74
+ * leaks the future segment wherever the thick already-drawn mask overlaps it).
75
+ * The growing tip is left capped (a pen nib); the real taper applies at full.
76
+ */
77
+ export function drawOutlineAtProgress(el, fraction, reverse = false) {
78
+ const f = Math.max(0, Math.min(1, fraction));
79
+ if (f <= 0)
80
+ return '';
81
+ if (f >= 1)
82
+ return drawElementToPath(el).d;
83
+ const opts = resolveStrokeOptions(el);
84
+ const sliced = pointsUpToFraction(el.points, f, reverse);
85
+ if (sliced.length < 2) {
86
+ const [x, y] = sliced[0] ?? [0, 0];
87
+ sliced.push([x + 0.01, y]);
88
+ }
89
+ const stroke = getStroke(sliced, {
90
+ size: opts.size,
91
+ thinning: opts.thinning,
92
+ smoothing: opts.smoothing,
93
+ streamline: opts.streamline,
94
+ start: { taper: opts.taperStart ? opts.size * 4 : 0, cap: !opts.taperStart },
95
+ end: { taper: 0, cap: true }
96
+ });
97
+ return outlineToPath(stroke);
98
+ }
33
99
  /** Compute the axis-aligned bounding box of raw input points. */
34
100
  export function pointsBounds(points) {
35
101
  if (points.length === 0)
@@ -49,18 +49,31 @@ export function shapeToPath(type, w, h, borderRadius = 0) {
49
49
  return `M0,0 L${w},0 L${w},${h} L0,${h} Z`;
50
50
  return `M${r},0 L${w - r},0 Q${w},0 ${w},${r} L${w},${h - r} Q${w},${h} ${w - r},${h} L${r},${h} Q0,${h} 0,${h - r} L0,${r} Q0,0 ${r},0 Z`;
51
51
  }
52
- case 'circle':
52
+ case 'circle': {
53
+ // A `circle` shape renders as a TRUE circle (radius = min side / 2,
54
+ // centered) — see Shape.svelte. Match that here so booleans / morphs /
55
+ // masks of a circle in a non-square box stay round, not oval.
56
+ const cr = Math.min(w, h) / 2;
57
+ return ellipsePath(w / 2, h / 2, cr, cr);
58
+ }
53
59
  case 'ellipse':
54
60
  return ellipsePath(w / 2, h / 2, w / 2, h / 2);
55
61
  case 'triangle':
56
62
  return `M${w / 2},0 L${w},${h} L0,${h} Z`;
57
63
  case 'hexagon': {
58
- // Pointy-left/right flat hexagon fitted to the box.
59
- const q = w * 0.25;
60
- return `M${q},0 L${w - q},0 L${w},${h / 2} L${w - q},${h} L${q},${h} L0,${h / 2} Z`;
64
+ // Regular pointy-top hexagon (radius = min side / 2, centered) — matches
65
+ // Shape.svelte so booleans / morphs / masks line up with the render.
66
+ const hcx = w / 2, hcy = h / 2, hr = Math.min(w, h) / 2;
67
+ let hd = '';
68
+ for (let i = 0; i < 6; i++) {
69
+ const ang = -Math.PI / 2 + (i * Math.PI) / 3;
70
+ hd += (i === 0 ? 'M' : 'L') + (hcx + hr * Math.cos(ang)).toFixed(2) + ',' + (hcy + hr * Math.sin(ang)).toFixed(2) + ' ';
71
+ }
72
+ return hd + 'Z';
61
73
  }
62
74
  case 'star': {
63
- const cx = w / 2, cy = h / 2, outer = Math.min(w, h) / 2, inner = outer * 0.5;
75
+ // Inner radius 0.4×outer to match Shape.svelte (was 0.5).
76
+ const cx = w / 2, cy = h / 2, outer = Math.min(w, h) / 2, inner = outer * 0.4;
64
77
  let d = '';
65
78
  for (let i = 0; i < 10; i++) {
66
79
  const rad = i % 2 === 0 ? outer : inner;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "animot-presenter",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Embed animated presentations anywhere. Works with vanilla JS, React, Vue, Angular, Svelte, and any frontend framework. Morphing animations, code highlighting, charts, particles, and more.",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",