@strategicprojects/rpic 0.7.1 → 0.8.1
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/index.js +204 -11
- package/package.json +1 -1
- package/pkg/rpic_wasm_bg.wasm +0 -0
- package/pkg/rpic_wasm_math_bg.wasm +0 -0
package/index.js
CHANGED
|
@@ -63,7 +63,9 @@ function ensure() {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Compile pic source into `{ svg, animations, diagnostics, warnings, objects }`
|
|
66
|
+
* Compile pic source into `{ svg, animations, diagnostics, warnings, objects }`
|
|
67
|
+
* (throws on a pic error). A top-level `scroll: true` is present when the source
|
|
68
|
+
* used `animate scroll` — a hint to scrub the timeline on scroll (see `animate`).
|
|
67
69
|
* @param {string} src
|
|
68
70
|
* @param {{circuits?: boolean, texlabels?: boolean}} [opts]
|
|
69
71
|
*/
|
|
@@ -93,12 +95,17 @@ export function renderSvg(src, opts) {
|
|
|
93
95
|
* Build a GSAP timeline from a drawing's animation manifest and play it on the
|
|
94
96
|
* SVG inside `root`. Browser-only (needs the DOM and a GSAP instance).
|
|
95
97
|
* @param {Element} root container holding the injected SVG
|
|
96
|
-
* @param {Array<{id:string,effect:string,start:number,duration:number}>} animations
|
|
97
|
-
* @param {*} gsap the GSAP instance
|
|
98
|
+
* @param {Array<{id:string,effect:string,start:number,duration:number,repeat?:number,yoyo?:boolean,ease?:string,path?:string,color?:string,out?:boolean,from?:string,morph?:string}>} animations
|
|
99
|
+
* @param {*} gsap the GSAP instance (register MotionPathPlugin for `move`, MorphSVGPlugin for `morph`)
|
|
98
100
|
* @returns the GSAP timeline
|
|
99
101
|
*/
|
|
100
102
|
export function animate(root, animations, gsap) {
|
|
101
103
|
const tl = gsap.timeline();
|
|
104
|
+
// GSAP's MotionPath/MorphSVG plugins need real <path> elements, but rpic
|
|
105
|
+
// emits <line>/<rect>/<circle>/<polygon>. Convert the shapes those effects
|
|
106
|
+
// reference to <path> up front — before any tween captures element refs, so
|
|
107
|
+
// a `draw` on the same shape traces the path instead of a detached primitive.
|
|
108
|
+
preconvertGeometry(root, animations);
|
|
102
109
|
for (const a of animations) {
|
|
103
110
|
const sel =
|
|
104
111
|
typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.id) : `[id="${a.id}"]`;
|
|
@@ -106,25 +113,209 @@ export function animate(root, animations, gsap) {
|
|
|
106
113
|
if (!el) continue;
|
|
107
114
|
switch (a.effect) {
|
|
108
115
|
case 'fade':
|
|
109
|
-
tl
|
|
116
|
+
enterExit(tl, el, withOverrides({ opacity: 0, duration: a.duration, ease: 'power1.out' }, a), a);
|
|
110
117
|
break;
|
|
111
118
|
case 'pop':
|
|
112
|
-
|
|
119
|
+
enterExit(
|
|
120
|
+
tl,
|
|
113
121
|
el,
|
|
114
|
-
{ scale: 0, transformOrigin: '50% 50%', duration: a.duration, ease: 'back.out(1.7)' },
|
|
115
|
-
a
|
|
122
|
+
withOverrides({ scale: 0, transformOrigin: '50% 50%', duration: a.duration, ease: 'back.out(1.7)' }, a),
|
|
123
|
+
a
|
|
116
124
|
);
|
|
117
125
|
break;
|
|
126
|
+
case 'slide':
|
|
127
|
+
slideIn(tl, el, a);
|
|
128
|
+
break;
|
|
118
129
|
case 'draw':
|
|
119
130
|
drawOn(el, a, tl);
|
|
120
131
|
break;
|
|
132
|
+
case 'move':
|
|
133
|
+
moveAlong(root, el, a, tl);
|
|
134
|
+
break;
|
|
135
|
+
case 'morph':
|
|
136
|
+
morphInto(root, el, a, tl);
|
|
137
|
+
break;
|
|
138
|
+
case 'highlight':
|
|
139
|
+
highlightWith(el, a, tl);
|
|
140
|
+
break;
|
|
121
141
|
default:
|
|
122
|
-
tl
|
|
142
|
+
enterExit(tl, el, withOverrides({ opacity: 0, duration: a.duration }, a), a);
|
|
123
143
|
}
|
|
124
144
|
}
|
|
125
145
|
return tl;
|
|
126
146
|
}
|
|
127
147
|
|
|
148
|
+
// Entrances tween FROM the hidden `vars` to the natural state; `out` reverses
|
|
149
|
+
// it into an exit (TO the hidden state). Shared by fade/pop/slide.
|
|
150
|
+
function enterExit(tl, el, vars, a) {
|
|
151
|
+
return a.out ? tl.to(el, vars, a.start) : tl.from(el, vars, a.start);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The `slide` effect: enter (or, with `out`, leave) by translating from a
|
|
155
|
+
// compass direction, offset by the element's own extent so it clears its slot.
|
|
156
|
+
function slideIn(tl, el, a) {
|
|
157
|
+
let bb = { width: 0, height: 0 };
|
|
158
|
+
try {
|
|
159
|
+
bb = el.getBBox();
|
|
160
|
+
} catch {
|
|
161
|
+
/* not yet laid out */
|
|
162
|
+
}
|
|
163
|
+
const off = {
|
|
164
|
+
left: { x: -(bb.width * 1.5 || 40) },
|
|
165
|
+
right: { x: bb.width * 1.5 || 40 },
|
|
166
|
+
up: { y: -(bb.height * 1.5 || 40) },
|
|
167
|
+
down: { y: bb.height * 1.5 || 40 },
|
|
168
|
+
}[a.from || 'left'];
|
|
169
|
+
enterExit(tl, el, withOverrides({ ...off, opacity: 0, duration: a.duration, ease: 'power2.out' }, a), a);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fold the optional GSAP overrides (repeat/yoyo/ease) into a tween's vars.
|
|
173
|
+
// `ease` replaces the effect's default easing; repeat/yoyo loop the tween.
|
|
174
|
+
function withOverrides(vars, a) {
|
|
175
|
+
if (a.repeat) vars.repeat = a.repeat;
|
|
176
|
+
if (a.yoyo) vars.yoyo = true;
|
|
177
|
+
if (a.ease) vars.ease = a.ease;
|
|
178
|
+
return vars;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
182
|
+
|
|
183
|
+
// Build SVG path data (`d`) for the primitives rpic emits. GSAP's MotionPath
|
|
184
|
+
// and MorphSVG need a real <path>; passing a raw <line>/<rect>/<circle> throws
|
|
185
|
+
// "Expecting a <path> element or an SVG path data string".
|
|
186
|
+
function pathDataOf(el) {
|
|
187
|
+
const n = el.tagName.toLowerCase();
|
|
188
|
+
const f = (a) => parseFloat(el.getAttribute(a)) || 0;
|
|
189
|
+
if (n === 'path') return el.getAttribute('d');
|
|
190
|
+
if (n === 'line') return `M${f('x1')},${f('y1')} L${f('x2')},${f('y2')}`;
|
|
191
|
+
if (n === 'polyline' || n === 'polygon') {
|
|
192
|
+
const nums = (el.getAttribute('points') || '').trim().split(/[\s,]+/).map(Number);
|
|
193
|
+
if (nums.length < 4) return null;
|
|
194
|
+
let d = '';
|
|
195
|
+
for (let i = 0; i + 1 < nums.length; i += 2) d += `${i ? 'L' : 'M'}${nums[i]},${nums[i + 1]} `;
|
|
196
|
+
return n === 'polygon' ? d + 'Z' : d.trim();
|
|
197
|
+
}
|
|
198
|
+
if (n === 'rect') return `M${f('x')},${f('y')} h${f('width')} v${f('height')} h${-f('width')} Z`;
|
|
199
|
+
if (n === 'circle') {
|
|
200
|
+
const r = f('r'), cx = f('cx'), cy = f('cy');
|
|
201
|
+
return `M${cx - r},${cy} a${r},${r} 0 1,0 ${2 * r},0 a${r},${r} 0 1,0 ${-2 * r},0 Z`;
|
|
202
|
+
}
|
|
203
|
+
if (n === 'ellipse') {
|
|
204
|
+
const rx = f('rx'), ry = f('ry'), cx = f('cx'), cy = f('cy');
|
|
205
|
+
return `M${cx - rx},${cy} a${rx},${ry} 0 1,0 ${2 * rx},0 a${rx},${ry} 0 1,0 ${-2 * rx},0 Z`;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Replace a primitive SVG element with an equivalent <path> (preserving paint),
|
|
211
|
+
// in place. Idempotent: a <path> is returned untouched.
|
|
212
|
+
function convertToPath(el) {
|
|
213
|
+
if (!el || el.tagName.toLowerCase() === 'path') return el;
|
|
214
|
+
const d = pathDataOf(el);
|
|
215
|
+
if (!d || !el.parentNode || typeof document === 'undefined') return el;
|
|
216
|
+
const p = document.createElementNS(SVG_NS, 'path');
|
|
217
|
+
p.setAttribute('d', d);
|
|
218
|
+
for (const attr of el.attributes) {
|
|
219
|
+
if (!/^(x1|y1|x2|y2|points|x|y|width|height|cx|cy|r|rx|ry)$/.test(attr.name)) {
|
|
220
|
+
p.setAttribute(attr.name, attr.value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
el.parentNode.replaceChild(p, el);
|
|
224
|
+
return p;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Convert every shape a `move`/`morph` references to a <path> before any tween
|
|
228
|
+
// captures element references (so a concurrent `draw` traces the path, not a
|
|
229
|
+
// now-detached primitive).
|
|
230
|
+
function preconvertGeometry(root, animations) {
|
|
231
|
+
const ALL = 'path, polyline, line, rect, circle, ellipse, polygon';
|
|
232
|
+
const pick = (id, sel) => {
|
|
233
|
+
if (!id) return;
|
|
234
|
+
const g = root.querySelector(`[id="${id}"]`);
|
|
235
|
+
const prim = g && g.querySelector(sel);
|
|
236
|
+
if (prim) convertToPath(prim);
|
|
237
|
+
};
|
|
238
|
+
for (const a of animations || []) {
|
|
239
|
+
if (a.effect === 'move') pick(a.path, 'path, polyline, line');
|
|
240
|
+
if (a.effect === 'morph') {
|
|
241
|
+
pick(a.id, ALL);
|
|
242
|
+
pick(a.morph, ALL);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The `highlight` effect: emphasise `el`. With a target colour, recolour and
|
|
248
|
+
// thicken its outline AND give the whole object a small scale pulse, so the
|
|
249
|
+
// emphasis reads at a glance rather than a bare colour swap; without a colour,
|
|
250
|
+
// a colour-free scale pulse. One-directional `.to()` — pair with `repeat 1
|
|
251
|
+
// yoyo` for a flash-and-return, or `repeat -1 yoyo` for a continuous pulse.
|
|
252
|
+
function highlightWith(el, a, tl) {
|
|
253
|
+
if (a.color) {
|
|
254
|
+
const shapes = el.querySelectorAll('path, polyline, line, rect, circle, ellipse, polygon');
|
|
255
|
+
tl.to(
|
|
256
|
+
shapes,
|
|
257
|
+
withOverrides({ stroke: a.color, strokeWidth: '+=2', duration: a.duration, ease: 'power1.inOut' }, a),
|
|
258
|
+
a.start
|
|
259
|
+
);
|
|
260
|
+
tl.to(
|
|
261
|
+
el,
|
|
262
|
+
withOverrides({ scale: 1.1, transformOrigin: '50% 50%', duration: a.duration, ease: 'power1.inOut' }, a),
|
|
263
|
+
a.start
|
|
264
|
+
);
|
|
265
|
+
} else {
|
|
266
|
+
tl.to(
|
|
267
|
+
el,
|
|
268
|
+
withOverrides({ scale: 1.12, transformOrigin: '50% 50%', duration: a.duration, ease: 'power1.inOut' }, a),
|
|
269
|
+
a.start
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// The `morph` effect: tween `el`'s outline into the shape of the object
|
|
275
|
+
// `a.morph` references, via GSAP's MorphSVGPlugin. The consumer must have
|
|
276
|
+
// registered it (`gsap.registerPlugin(MorphSVGPlugin)`); without it GSAP
|
|
277
|
+
// no-ops the morphSVG and the shape stays put. Both source and target were
|
|
278
|
+
// converted to <path> by preconvertGeometry.
|
|
279
|
+
function morphInto(root, el, a, tl) {
|
|
280
|
+
if (!a.morph) return;
|
|
281
|
+
const tsel =
|
|
282
|
+
typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.morph) : `[id="${a.morph}"]`;
|
|
283
|
+
const target = root.querySelector(tsel);
|
|
284
|
+
const src = el.querySelector('path');
|
|
285
|
+
const dst = target && target.querySelector('path');
|
|
286
|
+
if (!src || !dst) return;
|
|
287
|
+
tl.to(src, withOverrides({ morphSVG: dst, duration: a.duration, ease: 'power1.inOut' }, a), a.start);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// The `move` effect: travel `el` along the geometry of the object `a.path`
|
|
291
|
+
// references, via GSAP's MotionPathPlugin. The consumer must have registered
|
|
292
|
+
// it (`gsap.registerPlugin(MotionPathPlugin)`); without it GSAP no-ops the
|
|
293
|
+
// motionPath and the object simply stays put.
|
|
294
|
+
function moveAlong(root, el, a, tl) {
|
|
295
|
+
if (!a.path) return;
|
|
296
|
+
const psel =
|
|
297
|
+
typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.path) : `[id="${a.path}"]`;
|
|
298
|
+
const group = root.querySelector(psel);
|
|
299
|
+
// The shaft was converted to a <path> by preconvertGeometry; skip the
|
|
300
|
+
// arrowhead <polygon> — we want the line the traveller rides, not the tip.
|
|
301
|
+
const pathEl = group && group.querySelector('path');
|
|
302
|
+
if (!pathEl) return;
|
|
303
|
+
// `align` maps the path into the traveller's coordinate space; a `move`
|
|
304
|
+
// tween is a `.to()` (current position → along the path), not a `.from()`.
|
|
305
|
+
tl.to(
|
|
306
|
+
el,
|
|
307
|
+
withOverrides(
|
|
308
|
+
{
|
|
309
|
+
motionPath: { path: pathEl, align: pathEl, alignOrigin: [0.5, 0.5], autoRotate: false },
|
|
310
|
+
duration: a.duration,
|
|
311
|
+
ease: 'none',
|
|
312
|
+
},
|
|
313
|
+
a
|
|
314
|
+
),
|
|
315
|
+
a.start
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
128
319
|
function drawOn(group, a, tl) {
|
|
129
320
|
const els = group.querySelectorAll('path, polyline, line, rect, circle, ellipse, polygon');
|
|
130
321
|
els.forEach((el) => {
|
|
@@ -146,14 +337,16 @@ function drawOn(group, a, tl) {
|
|
|
146
337
|
len = 0;
|
|
147
338
|
}
|
|
148
339
|
if (len > 0) {
|
|
340
|
+
// `out` reverses the trace: the stroke retracts (dashoffset 0 → len).
|
|
341
|
+
const [begin, end] = a.out ? [0, len] : [len, 0];
|
|
149
342
|
tl.fromTo(
|
|
150
343
|
el,
|
|
151
|
-
{ strokeDasharray: len, strokeDashoffset:
|
|
152
|
-
{ strokeDashoffset:
|
|
344
|
+
{ strokeDasharray: len, strokeDashoffset: begin },
|
|
345
|
+
withOverrides({ strokeDashoffset: end, duration: a.duration, ease: 'none' }, a),
|
|
153
346
|
a.start
|
|
154
347
|
);
|
|
155
348
|
} else {
|
|
156
|
-
tl
|
|
349
|
+
enterExit(tl, el, withOverrides({ opacity: 0, duration: a.duration }, a), a);
|
|
157
350
|
}
|
|
158
351
|
});
|
|
159
352
|
const texts = group.querySelectorAll('text');
|
package/package.json
CHANGED
package/pkg/rpic_wasm_bg.wasm
CHANGED
|
Binary file
|
|
Binary file
|