@strategicprojects/rpic 0.8.0 → 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 CHANGED
@@ -101,6 +101,11 @@ export function renderSvg(src, opts) {
101
101
  */
102
102
  export function animate(root, animations, gsap) {
103
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);
104
109
  for (const a of animations) {
105
110
  const sel =
106
111
  typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.id) : `[id="${a.id}"]`;
@@ -173,14 +178,90 @@ function withOverrides(vars, a) {
173
178
  return vars;
174
179
  }
175
180
 
176
- // The `highlight` effect: emphasise `el`. With a target colour, tween the
177
- // stroke of its geometry to that colour; without one, a colour-free scale
178
- // pulse. It's a one-directional `.to()` pair with `repeat 1 yoyo` for a
179
- // flash-and-return, or `repeat -1 yoyo` for a continuous pulse.
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.
180
252
  function highlightWith(el, a, tl) {
181
253
  if (a.color) {
182
254
  const shapes = el.querySelectorAll('path, polyline, line, rect, circle, ellipse, polygon');
183
- tl.to(shapes, withOverrides({ stroke: a.color, duration: a.duration, ease: 'power1.inOut' }, a), a.start);
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
+ );
184
265
  } else {
185
266
  tl.to(
186
267
  el,
@@ -193,15 +274,15 @@ function highlightWith(el, a, tl) {
193
274
  // The `morph` effect: tween `el`'s outline into the shape of the object
194
275
  // `a.morph` references, via GSAP's MorphSVGPlugin. The consumer must have
195
276
  // registered it (`gsap.registerPlugin(MorphSVGPlugin)`); without it GSAP
196
- // no-ops the morphSVG and the shape stays put.
277
+ // no-ops the morphSVG and the shape stays put. Both source and target were
278
+ // converted to <path> by preconvertGeometry.
197
279
  function morphInto(root, el, a, tl) {
198
280
  if (!a.morph) return;
199
281
  const tsel =
200
282
  typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.morph) : `[id="${a.morph}"]`;
201
283
  const target = root.querySelector(tsel);
202
- const sel = 'path, polyline, line, rect, circle, ellipse, polygon';
203
- const src = el.querySelector(sel);
204
- const dst = target && target.querySelector(sel);
284
+ const src = el.querySelector('path');
285
+ const dst = target && target.querySelector('path');
205
286
  if (!src || !dst) return;
206
287
  tl.to(src, withOverrides({ morphSVG: dst, duration: a.duration, ease: 'power1.inOut' }, a), a.start);
207
288
  }
@@ -215,7 +296,9 @@ function moveAlong(root, el, a, tl) {
215
296
  const psel =
216
297
  typeof CSS !== 'undefined' && CSS.escape ? '#' + CSS.escape(a.path) : `[id="${a.path}"]`;
217
298
  const group = root.querySelector(psel);
218
- const pathEl = group && group.querySelector('path, polyline, line, polygon');
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');
219
302
  if (!pathEl) return;
220
303
  // `align` maps the path into the traveller's coordinate space; a `move`
221
304
  // tween is a `.to()` (current position → along the path), not a `.from()`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strategicprojects/rpic",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "The pic graphics language compiled to SVG with animation manifests, via WebAssembly. Browser + Node.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
Binary file
Binary file