animot-presenter 0.6.2 → 0.6.4
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/AnimotPresenter.svelte +428 -58
- package/dist/cdn/animot-presenter.css +1 -1
- package/dist/cdn/animot-presenter.esm.js +9856 -7624
- package/dist/cdn/animot-presenter.min.js +14 -10
- package/dist/styles/presenter.css +38 -0
- package/dist/types.d.ts +33 -2
- package/dist/utils/freehand.d.ts +26 -0
- package/dist/utils/freehand.js +70 -0
- package/dist/utils/path-morph.d.ts +89 -0
- package/dist/utils/path-morph.js +420 -0
- package/dist/utils/svg-path-edit.d.ts +37 -0
- package/dist/utils/svg-path-edit.js +279 -0
- package/package.json +3 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse/serialize an SVG path `d` into an editable cubic-Bézier anchor model
|
|
3
|
+
* used by the node editor. Every segment is normalized to a cubic so each
|
|
4
|
+
* anchor has at most an incoming + outgoing control handle — the same mental
|
|
5
|
+
* model as the motion-path editor.
|
|
6
|
+
*
|
|
7
|
+
* Supported input commands: M m L l H h V v C c S s Q q T t Z z.
|
|
8
|
+
* Arcs (A/a) are not supported; `parsePath` returns null so the caller can
|
|
9
|
+
* keep node-editing disabled for those paths.
|
|
10
|
+
*/
|
|
11
|
+
const TOKEN = /([MmLlHhVvCcSsQqTtZzAa])|(-?\d*\.?\d+(?:e[-+]?\d+)?)/gi;
|
|
12
|
+
export function parsePath(d) {
|
|
13
|
+
const tokens = [];
|
|
14
|
+
let m;
|
|
15
|
+
while ((m = TOKEN.exec(d)) !== null) {
|
|
16
|
+
if (m[1])
|
|
17
|
+
tokens.push(m[1]);
|
|
18
|
+
else
|
|
19
|
+
tokens.push(parseFloat(m[2]));
|
|
20
|
+
}
|
|
21
|
+
const subs = [];
|
|
22
|
+
let cur = null;
|
|
23
|
+
let px = 0, py = 0; // current point
|
|
24
|
+
let startX = 0, startY = 0; // subpath start
|
|
25
|
+
let prevCubicC2 = null; // for S
|
|
26
|
+
let prevQuadC = null; // for T
|
|
27
|
+
let i = 0;
|
|
28
|
+
let cmd = '';
|
|
29
|
+
const num = () => tokens[i++];
|
|
30
|
+
const isCmd = (t) => typeof t === 'string';
|
|
31
|
+
const pushAnchor = (x, y) => {
|
|
32
|
+
cur.anchors.push({ x, y });
|
|
33
|
+
};
|
|
34
|
+
while (i < tokens.length) {
|
|
35
|
+
if (isCmd(tokens[i])) {
|
|
36
|
+
cmd = tokens[i];
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
const rel = cmd === cmd.toLowerCase();
|
|
40
|
+
const c = cmd.toUpperCase();
|
|
41
|
+
if (c === 'M') {
|
|
42
|
+
const x = num() + (rel ? px : 0);
|
|
43
|
+
const y = num() + (rel ? py : 0);
|
|
44
|
+
cur = { anchors: [], closed: false };
|
|
45
|
+
subs.push(cur);
|
|
46
|
+
pushAnchor(x, y);
|
|
47
|
+
px = startX = x;
|
|
48
|
+
py = startY = y;
|
|
49
|
+
prevCubicC2 = prevQuadC = null;
|
|
50
|
+
cmd = rel ? 'l' : 'L'; // subsequent pairs are implicit lineto
|
|
51
|
+
}
|
|
52
|
+
else if (c === 'L') {
|
|
53
|
+
const x = num() + (rel ? px : 0);
|
|
54
|
+
const y = num() + (rel ? py : 0);
|
|
55
|
+
pushAnchor(x, y);
|
|
56
|
+
px = x;
|
|
57
|
+
py = y;
|
|
58
|
+
prevCubicC2 = prevQuadC = null;
|
|
59
|
+
}
|
|
60
|
+
else if (c === 'H') {
|
|
61
|
+
const x = num() + (rel ? px : 0);
|
|
62
|
+
pushAnchor(x, py);
|
|
63
|
+
px = x;
|
|
64
|
+
prevCubicC2 = prevQuadC = null;
|
|
65
|
+
}
|
|
66
|
+
else if (c === 'V') {
|
|
67
|
+
const y = num() + (rel ? py : 0);
|
|
68
|
+
pushAnchor(px, y);
|
|
69
|
+
py = y;
|
|
70
|
+
prevCubicC2 = prevQuadC = null;
|
|
71
|
+
}
|
|
72
|
+
else if (c === 'C' || c === 'S') {
|
|
73
|
+
let c1x, c1y;
|
|
74
|
+
if (c === 'C') {
|
|
75
|
+
c1x = num() + (rel ? px : 0);
|
|
76
|
+
c1y = num() + (rel ? py : 0);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// S: first control = reflection of previous cubic's 2nd control.
|
|
80
|
+
c1x = prevCubicC2 ? 2 * px - prevCubicC2.x : px;
|
|
81
|
+
c1y = prevCubicC2 ? 2 * py - prevCubicC2.y : py;
|
|
82
|
+
}
|
|
83
|
+
const c2x = num() + (rel ? px : 0), c2y = num() + (rel ? py : 0);
|
|
84
|
+
const x = num() + (rel ? px : 0), y = num() + (rel ? py : 0);
|
|
85
|
+
if (cur.anchors.length)
|
|
86
|
+
cur.anchors[cur.anchors.length - 1].cout = { x: c1x, y: c1y };
|
|
87
|
+
cur.anchors.push({ x, y, cin: { x: c2x, y: c2y } });
|
|
88
|
+
prevCubicC2 = { x: c2x, y: c2y };
|
|
89
|
+
prevQuadC = null;
|
|
90
|
+
px = x;
|
|
91
|
+
py = y;
|
|
92
|
+
}
|
|
93
|
+
else if (c === 'Q' || c === 'T') {
|
|
94
|
+
let qx, qy;
|
|
95
|
+
if (c === 'Q') {
|
|
96
|
+
qx = num() + (rel ? px : 0);
|
|
97
|
+
qy = num() + (rel ? py : 0);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
qx = prevQuadC ? 2 * px - prevQuadC.x : px;
|
|
101
|
+
qy = prevQuadC ? 2 * py - prevQuadC.y : py;
|
|
102
|
+
}
|
|
103
|
+
const x = num() + (rel ? px : 0), y = num() + (rel ? py : 0);
|
|
104
|
+
// Quadratic → cubic control points.
|
|
105
|
+
const c1x = px + (2 / 3) * (qx - px), c1y = py + (2 / 3) * (qy - py);
|
|
106
|
+
const c2x = x + (2 / 3) * (qx - x), c2y = y + (2 / 3) * (qy - y);
|
|
107
|
+
if (cur.anchors.length)
|
|
108
|
+
cur.anchors[cur.anchors.length - 1].cout = { x: c1x, y: c1y };
|
|
109
|
+
cur.anchors.push({ x, y, cin: { x: c2x, y: c2y } });
|
|
110
|
+
prevQuadC = { x: qx, y: qy };
|
|
111
|
+
prevCubicC2 = null;
|
|
112
|
+
px = x;
|
|
113
|
+
py = y;
|
|
114
|
+
}
|
|
115
|
+
else if (c === 'A') {
|
|
116
|
+
// Elliptical arc → one or more cubic Béziers (so the rest of the
|
|
117
|
+
// editor only ever deals with anchors + cubic handles).
|
|
118
|
+
const rx = num(), ry = num(), xrot = num(), large = num(), sweep = num();
|
|
119
|
+
const ex = num() + (rel ? px : 0), ey = num() + (rel ? py : 0);
|
|
120
|
+
const cubics = arcToCubics(px, py, rx, ry, xrot, large, sweep, ex, ey);
|
|
121
|
+
for (const seg of cubics) {
|
|
122
|
+
if (cur.anchors.length)
|
|
123
|
+
cur.anchors[cur.anchors.length - 1].cout = { x: seg.c1x, y: seg.c1y };
|
|
124
|
+
cur.anchors.push({ x: seg.x, y: seg.y, cin: { x: seg.c2x, y: seg.c2y } });
|
|
125
|
+
}
|
|
126
|
+
px = ex;
|
|
127
|
+
py = ey;
|
|
128
|
+
prevCubicC2 = prevQuadC = null;
|
|
129
|
+
}
|
|
130
|
+
else if (c === 'Z') {
|
|
131
|
+
if (cur)
|
|
132
|
+
cur.closed = true;
|
|
133
|
+
px = startX;
|
|
134
|
+
py = startY;
|
|
135
|
+
prevCubicC2 = prevQuadC = null;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Unknown — bail.
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return subs.filter((s) => s.anchors.length > 0);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Reduce anchor count via Ramer–Douglas–Peucker on anchor positions. Used to
|
|
146
|
+
* make a dense freehand outline node-editable (otherwise it has hundreds of
|
|
147
|
+
* handles). Kept anchors retain their Bézier handles; dropped ones become
|
|
148
|
+
* straight. `tolerance` is in the path's own coordinate units.
|
|
149
|
+
*/
|
|
150
|
+
export function simplifyAnchors(subs, tolerance) {
|
|
151
|
+
const rdp = (pts) => {
|
|
152
|
+
if (pts.length < 3)
|
|
153
|
+
return pts;
|
|
154
|
+
let maxD = 0, idx = 0;
|
|
155
|
+
const a = pts[0], b = pts[pts.length - 1];
|
|
156
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
157
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
158
|
+
for (let i = 1; i < pts.length - 1; i++) {
|
|
159
|
+
const d = Math.abs((pts[i].x - a.x) * dy - (pts[i].y - a.y) * dx) / len;
|
|
160
|
+
if (d > maxD) {
|
|
161
|
+
maxD = d;
|
|
162
|
+
idx = i;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (maxD > tolerance) {
|
|
166
|
+
const left = rdp(pts.slice(0, idx + 1));
|
|
167
|
+
const right = rdp(pts.slice(idx));
|
|
168
|
+
return left.slice(0, -1).concat(right);
|
|
169
|
+
}
|
|
170
|
+
return [a, b];
|
|
171
|
+
};
|
|
172
|
+
return subs.map((s) => ({ closed: s.closed, anchors: rdp(s.anchors) }));
|
|
173
|
+
}
|
|
174
|
+
/** Convert an SVG elliptical arc to a list of cubic Bézier segments. */
|
|
175
|
+
function arcToCubics(x1, y1, rx, ry, xRotDeg, large, sweep, x2, y2) {
|
|
176
|
+
if (rx === 0 || ry === 0)
|
|
177
|
+
return [{ c1x: x1, c1y: y1, c2x: x2, c2y: y2, x: x2, y: y2 }];
|
|
178
|
+
rx = Math.abs(rx);
|
|
179
|
+
ry = Math.abs(ry);
|
|
180
|
+
const phi = (xRotDeg * Math.PI) / 180;
|
|
181
|
+
const cosP = Math.cos(phi), sinP = Math.sin(phi);
|
|
182
|
+
// Step 1: transform to ellipse-centered coords.
|
|
183
|
+
const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
|
|
184
|
+
const x1p = cosP * dx + sinP * dy;
|
|
185
|
+
const y1p = -sinP * dx + cosP * dy;
|
|
186
|
+
// Correct out-of-range radii.
|
|
187
|
+
let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
188
|
+
if (lambda > 1) {
|
|
189
|
+
const s = Math.sqrt(lambda);
|
|
190
|
+
rx *= s;
|
|
191
|
+
ry *= s;
|
|
192
|
+
}
|
|
193
|
+
let sign = large === sweep ? -1 : 1;
|
|
194
|
+
let num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
195
|
+
let den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
196
|
+
let co = sign * Math.sqrt(Math.max(0, num / den));
|
|
197
|
+
const cxp = (co * rx * y1p) / ry;
|
|
198
|
+
const cyp = (-co * ry * x1p) / rx;
|
|
199
|
+
const cx = cosP * cxp - sinP * cyp + (x1 + x2) / 2;
|
|
200
|
+
const cy = sinP * cxp + cosP * cyp + (y1 + y2) / 2;
|
|
201
|
+
const ang = (ux, uy, vx, vy) => {
|
|
202
|
+
const dot = ux * vx + uy * vy;
|
|
203
|
+
const len = Math.hypot(ux, uy) * Math.hypot(vx, vy);
|
|
204
|
+
let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
|
|
205
|
+
if (ux * vy - uy * vx < 0)
|
|
206
|
+
a = -a;
|
|
207
|
+
return a;
|
|
208
|
+
};
|
|
209
|
+
let theta1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
210
|
+
let dTheta = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
211
|
+
if (!sweep && dTheta > 0)
|
|
212
|
+
dTheta -= 2 * Math.PI;
|
|
213
|
+
if (sweep && dTheta < 0)
|
|
214
|
+
dTheta += 2 * Math.PI;
|
|
215
|
+
const segCount = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 2)));
|
|
216
|
+
const delta = dTheta / segCount;
|
|
217
|
+
const t = (4 / 3) * Math.tan(delta / 4);
|
|
218
|
+
const out = [];
|
|
219
|
+
let th = theta1;
|
|
220
|
+
let curX = x1, curY = y1;
|
|
221
|
+
for (let i = 0; i < segCount; i++) {
|
|
222
|
+
const th2 = th + delta;
|
|
223
|
+
const cosTh = Math.cos(th), sinTh = Math.sin(th);
|
|
224
|
+
const cosTh2 = Math.cos(th2), sinTh2 = Math.sin(th2);
|
|
225
|
+
// Derivatives in ellipse space → control points, rotated back.
|
|
226
|
+
const ep = (ca, sa) => ({
|
|
227
|
+
x: cx + rx * cosP * ca - ry * sinP * sa,
|
|
228
|
+
y: cy + rx * sinP * ca + ry * cosP * sa
|
|
229
|
+
});
|
|
230
|
+
const d1 = { x: -rx * cosP * sinTh - ry * sinP * cosTh, y: -rx * sinP * sinTh + ry * cosP * cosTh };
|
|
231
|
+
const d2 = { x: -rx * cosP * sinTh2 - ry * sinP * cosTh2, y: -rx * sinP * sinTh2 + ry * cosP * cosTh2 };
|
|
232
|
+
const p2 = ep(cosTh2, sinTh2);
|
|
233
|
+
out.push({
|
|
234
|
+
c1x: curX + t * d1.x, c1y: curY + t * d1.y,
|
|
235
|
+
c2x: p2.x - t * d2.x, c2y: p2.y - t * d2.y,
|
|
236
|
+
x: p2.x, y: p2.y
|
|
237
|
+
});
|
|
238
|
+
curX = p2.x;
|
|
239
|
+
curY = p2.y;
|
|
240
|
+
th = th2;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
function n(v) {
|
|
245
|
+
return Number.isInteger(v) ? String(v) : v.toFixed(2);
|
|
246
|
+
}
|
|
247
|
+
export function serializePath(subs) {
|
|
248
|
+
const parts = [];
|
|
249
|
+
for (const sub of subs) {
|
|
250
|
+
const a = sub.anchors;
|
|
251
|
+
if (a.length === 0)
|
|
252
|
+
continue;
|
|
253
|
+
parts.push(`M${n(a[0].x)},${n(a[0].y)}`);
|
|
254
|
+
for (let k = 1; k < a.length; k++) {
|
|
255
|
+
const prev = a[k - 1];
|
|
256
|
+
const cur = a[k];
|
|
257
|
+
if (prev.cout || cur.cin) {
|
|
258
|
+
const c1 = prev.cout ?? { x: prev.x, y: prev.y };
|
|
259
|
+
const c2 = cur.cin ?? { x: cur.x, y: cur.y };
|
|
260
|
+
parts.push(`C${n(c1.x)},${n(c1.y)} ${n(c2.x)},${n(c2.y)} ${n(cur.x)},${n(cur.y)}`);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
parts.push(`L${n(cur.x)},${n(cur.y)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (sub.closed) {
|
|
267
|
+
// Curve back to start if the closing edge had handles.
|
|
268
|
+
const last = a[a.length - 1];
|
|
269
|
+
const first = a[0];
|
|
270
|
+
if (last.cout || first.cin) {
|
|
271
|
+
const c1 = last.cout ?? { x: last.x, y: last.y };
|
|
272
|
+
const c2 = first.cin ?? { x: first.x, y: first.y };
|
|
273
|
+
parts.push(`C${n(c1.x)},${n(c1.y)} ${n(c2.x)},${n(c2.y)} ${n(first.x)},${n(first.y)}`);
|
|
274
|
+
}
|
|
275
|
+
parts.push('Z');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return parts.join(' ');
|
|
279
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "animot-presenter",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@animotion/motion": "^2.0.1",
|
|
61
61
|
"canvas-confetti": "^1.9.4",
|
|
62
|
+
"flubber": "^0.4.2",
|
|
63
|
+
"perfect-freehand": "^1.2.3",
|
|
62
64
|
"shiki": "^1.0.0"
|
|
63
65
|
},
|
|
64
66
|
"keywords": [
|