@glissade/core 0.13.0-pre.0 → 0.13.0-pre.2

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/clips.d.ts CHANGED
@@ -88,7 +88,7 @@ interface DurationOpts {
88
88
  /** Arriving ease of the motion. */
89
89
  ease?: EaseSpec;
90
90
  }
91
- /** Entrance: opacity 0→1 and scale 0.8→1 (a "pop" in). */
91
+ /** Entrance: opacity 0→1 and scale [0.8,0.8][1,1] (a "pop" in). */
92
92
  declare function popIn(opts?: DurationOpts): Clip;
93
93
  type SlideEdge = 'left' | 'right' | 'top' | 'bottom';
94
94
  /**
package/dist/clips.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as targetNodeId, i as resolveTweenTarget, p as track, s as key, t as TARGET_PATH, x as inferValueType } from "./targetRef.js";
1
+ import { i as resolveTweenTarget, p as track, s as key, t as TARGET_PATH, x as inferValueType } from "./targetRef.js";
2
2
  //#region src/clip.ts
3
3
  /**
4
4
  * Motion clips (DESIGN.md §2 build-time sugar): `clip()` captures a relative-time
@@ -168,7 +168,7 @@ function clipList(c, targets, startSec, opts) {
168
168
  * Loop clips (`pulse`, `driftLoop`) author first-key-value == last-key-value, so
169
169
  * tiling them under `clipList` (or repeating the clip) reads seamlessly.
170
170
  */
171
- /** Entrance: opacity 0→1 and scale 0.8→1 (a "pop" in). */
171
+ /** Entrance: opacity 0→1 and scale [0.8,0.8][1,1] (a "pop" in). */
172
172
  function popIn(opts) {
173
173
  const d = opts?.duration ?? .3;
174
174
  const ease = opts?.ease ?? "easeOutCubic";
@@ -179,7 +179,7 @@ function popIn(opts) {
179
179
  },
180
180
  scale: {
181
181
  path: "scale",
182
- keys: [key(0, .8), key(d, 1, ease)]
182
+ keys: [key(0, [.8, .8]), key(d, [1, 1], ease)]
183
183
  }
184
184
  } });
185
185
  }
@@ -212,9 +212,9 @@ function pulse(opts) {
212
212
  return clip({ channels: { scale: {
213
213
  path: "scale",
214
214
  keys: [
215
- key(0, 1),
216
- key(d / 2, peak, ease),
217
- key(d, 1, ease)
215
+ key(0, [1, 1]),
216
+ key(d / 2, [peak, peak], ease),
217
+ key(d, [1, 1], ease)
218
218
  ]
219
219
  } } });
220
220
  }
@@ -271,11 +271,16 @@ function assertFiniteBox(label, b) {
271
271
  * `'<nodeId>/<prop>'` string the clip map form resolves (which re-runs the
272
272
  * structural/anonymous-id rejection via resolveTweenTarget). A string target is
273
273
  * a bare node id; a property-signal carrier exposes its node via TARGET_PATH.
274
+ *
275
+ * The node id may itself carry slashes (an each() clone like 'card/3'); DO NOT
276
+ * re-split it on the FIRST slash — APPEND the prop and trust the caller. The
277
+ * scene's longest-registered-prefix resolver disambiguates node id vs prop path
278
+ * at bind time, so 'card/3' targets the clone, not the wrapping 'card' Group.
274
279
  */
275
280
  function nodeTarget(target, prop) {
276
281
  const id = typeof target === "string" ? target : target[TARGET_PATH];
277
282
  if (typeof id !== "string" || id.length === 0) return `${String(id)}/${prop}`;
278
- return `${targetNodeId(id)}/${prop}`;
283
+ return `${typeof target === "string" ? id : id.slice(0, Math.max(0, id.lastIndexOf("/")))}/${prop}`;
279
284
  }
280
285
  /**
281
286
  * Compile a shared-element box-FLIP morph into hand-authored-equivalent tracks.
@@ -426,17 +431,16 @@ function partitionOpacity(tracks, opacityTarget) {
426
431
  */
427
432
  function presence(nodeId, opts) {
428
433
  const { show, hide } = opts;
429
- const baseTarget = resolveTweenTarget(typeof nodeId === "string" ? `${nodeId}/opacity` : nodeId);
430
- const slash = baseTarget.indexOf("/");
431
- const nodeIdStr = slash < 0 ? baseTarget : baseTarget.slice(0, slash);
432
- const opacityTarget = `${nodeIdStr}/opacity`;
434
+ const opacityTarget = resolveTweenTarget(typeof nodeId === "string" ? `${nodeId}/opacity` : nodeId);
435
+ const lastSlash = opacityTarget.lastIndexOf("/");
436
+ const nodeIdStr = lastSlash < 0 ? opacityTarget : opacityTarget.slice(0, lastSlash);
433
437
  const enter = opts.enter ?? defaultEnter();
434
438
  const exit = opts.exit ?? defaultExit();
435
439
  const enterRes = enter.apply(nodeIdStr, show, opts.enterOpts);
436
440
  const enterEnd = show + effectiveDuration(enter, opts.enterOpts);
437
441
  const exitDur = effectiveDuration(exit, opts.exitOpts);
438
442
  const exitStart = hide - exitDur;
439
- if (exitStart < show) throw new PresenceError(`presence('${nodeIdStr}') exit needs ${exitDur.toFixed(3)}s but only ${(hide - show).toFixed(3)}s between show (${show}) and hide (${hide}) — widen the window or shorten/speed up the exit`);
443
+ if (exitStart <= show) throw new PresenceError(`presence('${nodeIdStr}') exit needs ${exitDur.toFixed(3)}s but only ${(hide - show).toFixed(3)}s between show (${show}) and hide (${hide}) — widen the window or shorten/speed up the exit`);
440
444
  const exitRes = exit.apply(nodeIdStr, exitStart, opts.exitOpts);
441
445
  const { opacityKeys: enterOpacity, rest: enterRest } = partitionOpacity(enterRes.tracks, opacityTarget);
442
446
  const { opacityKeys: exitOpacity, rest: exitRest } = partitionOpacity(exitRes.tracks, opacityTarget);
@@ -446,7 +450,13 @@ function presence(nodeId, opts) {
446
450
  key(hide, 0, { interp: "hold" })
447
451
  ];
448
452
  const overlay = [];
449
- if (enterOpacity.length > 0) for (const k of enterOpacity) overlay.push(k);
453
+ if (enterOpacity.length > 0) enterOpacity.forEach((k, i) => {
454
+ if (i === 0 && k.value !== 0 && k.interp !== "hold") overlay.push({
455
+ ...k,
456
+ interp: "hold"
457
+ });
458
+ else overlay.push(k);
459
+ });
450
460
  else overlay.push(key(show, 0), key(enterEnd, 1));
451
461
  if (exitOpacity.length > 0) for (const k of exitOpacity) overlay.push(k);
452
462
  else overlay.push(key(exitStart, 1), key(hide, 0));
@@ -104,6 +104,17 @@ async function loadSubsetFont() {
104
104
  });
105
105
  return subsetFontPromise;
106
106
  }
107
+ let fontverterPromise;
108
+ async function loadFontverter() {
109
+ fontverterPromise ??= import("fontverter").then((m) => {
110
+ const convert = (m.default ?? m).convert;
111
+ if (typeof convert !== "function") throw new FontIngestError(`'fontverter' loaded but exposes no convert() — incompatible version`);
112
+ return convert;
113
+ }, (err) => {
114
+ throw new FontIngestError(`font ingestion needs the optional 'fontverter' dependency for woff/woff2 decode — install it. (${String(err?.message ?? err)})`);
115
+ });
116
+ return fontverterPromise;
117
+ }
107
118
  /**
108
119
  * Build the "retain every code point" string from a font's own cmap — used to
109
120
  * run hb-subset purely as a woff2 DECODER / variable-axis INSTANCER without
@@ -132,10 +143,10 @@ async function ingestFont(init) {
132
143
  let bytes;
133
144
  if (!needsDecode && !needsInstance) bytes = input;
134
145
  else {
135
- const subsetFont = await loadSubsetFont();
136
146
  let decoded = input;
137
- if (needsDecode) decoded = await subsetFont(input, retainAllText(parseCmap(input)), { targetFormat: "sfnt" });
147
+ if (needsDecode) decoded = await (await loadFontverter())(input, "truetype");
138
148
  if (needsInstance) {
149
+ const subsetFont = await loadSubsetFont();
139
150
  const coverage = parseCmap(decoded);
140
151
  decoded = await subsetFont(decoded, retainAllText(coverage), {
141
152
  targetFormat: "sfnt",
package/dist/targetRef.js CHANGED
@@ -603,12 +603,31 @@ const pathType = {
603
603
  const lerpN = (a, b, t) => a + (b - a) * t;
604
604
  const lerpPt = (a, b, t) => [lerpN(a[0], b[0], t), lerpN(a[1], b[1], t)];
605
605
  /** Same hue at alpha 0 — a transparent stand-in so an appearing/disappearing
606
- * `bg` (mesh baseline) fades through alpha instead of popping at the boundary. */
606
+ * `bg` (mesh baseline) fades through alpha instead of popping at the boundary.
607
+ * `parseColor` THROWS on a non-hex/non-canonical CSS color (hsl/named/oklch);
608
+ * since this runs inside `lerp` during evaluate() it must NEVER throw — fall
609
+ * back to holding the PRESENT color so the bg snaps in/out instead of crashing
610
+ * the whole frame. */
607
611
  function transparentOf(color) {
608
- return formatColor({
609
- ...parseColor(color),
610
- a: 0
611
- });
612
+ try {
613
+ return formatColor({
614
+ ...parseColor(color),
615
+ a: 0
616
+ });
617
+ } catch {
618
+ return color;
619
+ }
620
+ }
621
+ /** `lerpColor` that NEVER throws inside `lerp` (evaluate()): on an unparseable
622
+ * color (hsl/named/oklch — `parseColor` only knows hex/rgb) it SNAPS (holds `a`,
623
+ * then `b` at t≥1) instead of crashing the frame. Byte-identical to `lerpColor`
624
+ * for parseable colors, so it does not move any golden. */
625
+ function safeLerpColor(a, b, t) {
626
+ try {
627
+ return lerpColor(a, b, t);
628
+ } catch {
629
+ return t >= 1 ? b : a;
630
+ }
612
631
  }
613
632
  /** Lift a solid color to a uniform gradient matching `shape` (every stop = color),
614
633
  * so a color↔gradient tween fades smoothly instead of snapping. */
@@ -665,7 +684,7 @@ const paintType = {
665
684
  color: lerpColor(pa.color, pb.color, t)
666
685
  };
667
686
  });
668
- const bg = a.bg !== void 0 || b.bg !== void 0 ? { bg: lerpColor(a.bg ?? transparentOf(b.bg), b.bg ?? transparentOf(a.bg), t) } : {};
687
+ const bg = a.bg !== void 0 || b.bg !== void 0 ? { bg: safeLerpColor(a.bg ?? transparentOf(b.bg), b.bg ?? transparentOf(a.bg), t) } : {};
669
688
  return {
670
689
  kind: "mesh",
671
690
  points,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/core",
3
- "version": "0.13.0-pre.0",
3
+ "version": "0.13.0-pre.2",
4
4
  "description": "glissade core: signals, tracks, timeline document, evaluation, easing, springs, seeded RNG. Zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -35,6 +35,7 @@
35
35
  "directory": "packages/core"
36
36
  },
37
37
  "optionalDependencies": {
38
+ "fontverter": "^2.0.0",
38
39
  "subset-font": "^2.5.0"
39
40
  },
40
41
  "scripts": {