@glissade/scene 0.57.0-pre.0 → 0.57.1-pre.0

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/describe.js CHANGED
@@ -23,7 +23,7 @@ import { easings, listValueTypes } from "@glissade/core";
23
23
  * never pulled onto the base embed path — a scene that never calls `describe()`
24
24
  * pays zero bytes for it.
25
25
  */
26
- const RAW_VERSION = "0.57.0-pre.0";
26
+ const RAW_VERSION = "0.57.1-pre.0";
27
27
  const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
28
28
  /**
29
29
  * Parse the documented positional-arg count from a helper `usage` string — the
@@ -586,13 +586,13 @@ const HELPERS = [
586
586
  name: "particles",
587
587
  summary: "A small SEEDED, BAKED particle emitter (FACTORY, no `new`): composes each() (count fixed slot nodes at `${id}/${i}`) + bake() (seeded physics → position/opacity/scale/rotation tracks on those SAME ids). Every slot is a real node with real tracks → a real exportable Lottie layer, faithful BY CONSTRUCTION (no render-only/custom-draw path). `count` is the MAX-CONCURRENT ring-buffer pool (bounded 200 — over THROWS, never clamps), NOT total emitted; opacity-0-for-the-whole-window slots are pruned so the layer count stays proportional. Seed defaults to hashStr(id); byte-identical run-to-run, a different seed varies. ESCAPE HATCH: `appearance` (any Node/glyph template), `step` (raw per-particle sim), `...` velocity/forces/lifetime. Tree-shakeable (@glissade/scene/motion).",
588
588
  import: "@glissade/scene/motion",
589
- usage: "particles(spec: { id, count, box: {w,h}, duration, fps, origin: [fx,fy], lifetime: number | [min,max], velocity: { speed:[min,max], angle:[min,max] (deg) }, appearance: (i, ctx) => Node | { node, opacityOverLife?, scaleOverLife? }, rate?, burst?: number | {at,n}[], seed?, area?, forces?: { gravity?, drag?, wind? }, spin?, opacityOverLife?, scaleOverLife?, step?: (p, dt, rng) => void }): { node: Group, tracks: Track[], end } — supply rate and/or burst; count > 200 throws."
589
+ usage: "particles(spec: { id, count, box: {w,h}, duration, fps, origin: [fx,fy], lifetime: number | [min,max], velocity: { speed:[min,max], angle:[min,max] (deg) }, appearance: (i, ctx) => Node | { node, opacityOverLife?, scaleOverLife? }, rate?, burst?: number | {at,n}[], seed?, area?, safeBottom? (relative [0,1] safe-area clamp — no spawn below this Y), forces?: { gravity?, drag?, wind? }, spin?, opacityOverLife?, scaleOverLife?, step?: (p, dt, rng) => void }): { node: Group, tracks: Track[], end } — supply rate and/or burst; count > 200 throws; safeBottom out-of-[0,1] or above the spawn-band top throws."
590
590
  },
591
591
  {
592
592
  name: "drift",
593
- summary: "Particles preset: ambient low-opacity motes floating gently up (a bokeh companion). Continuous low-rate; DEFAULTS to a small max-concurrent count (24) so the exported layer count stays proportional, NOT 200 near-empty layers. `appearance` is the primary control (a themed dot); `...rest` forwards to particles() (velocity/forces/lifetime/step). Factory (no `new`). Tree-shakeable (@glissade/scene/motion).",
593
+ summary: "Particles preset: ambient low-opacity motes floating gently up (a bokeh companion). Continuous low-rate; DEFAULTS to a small max-concurrent count (24) so the exported layer count stays proportional, NOT 200 near-empty layers. SAFE-AREA (0.57.1): the DEFAULT spawn band is centered + shallow (bottom ~0.68H) so bare drift() clears a standard lower-third caption safe-area by itself; pass `safeBottom` (relative [0,1]) to pin a consumer's exact captionTop, or override `area`/`origin` for a custom spawn region. `appearance` is the primary control (a themed dot); `...rest` forwards to particles() (velocity/forces/lifetime/area/safeBottom/step). Factory (no `new`). Tree-shakeable (@glissade/scene/motion).",
594
594
  import: "@glissade/scene/motion",
595
- usage: "drift(opts: { box: {w,h}, duration, fps, count?, rate?, origin?, color?, radius?, seed?, id?, ...rest (lifetime/velocity/forces/appearance/step) }): { node: Group, tracks: Track[], end }"
595
+ usage: "drift(opts: { box: {w,h}, duration, fps, count?, rate?, origin?, color?, radius?, seed?, id?, area?, safeBottom? (relative [0,1] — no motes below this Y, e.g. just above captionTop), ...rest (lifetime/velocity/forces/appearance/step) }): { node: Group, tracks: Track[], end }"
596
596
  },
597
597
  {
598
598
  name: "sparks",
package/dist/motion.d.ts CHANGED
@@ -305,6 +305,15 @@ interface ParticleSpec {
305
305
  origin: Place;
306
306
  /** Optional spread around the origin (px). */
307
307
  area?: AreaSpec;
308
+ /**
309
+ * Safe-area clamp (0.57.1): no particle spawns BELOW this RELATIVE Y (`safeBottom *
310
+ * box.h`), so ambient motes never drift into a lower-third caption band. Relative
311
+ * [0,1] — NOT a pixel Y. Must sit at/below the spawn band's top (a `safeBottom` above
312
+ * the band top leaves no valid spawn region → throws). The framework can't know a
313
+ * consumer's captionTop, so this is the opt-in PRECISE clamp; the `drift` preset also
314
+ * ships a conservative DEFAULT band that clears a standard lower-third by itself.
315
+ */
316
+ safeBottom?: number;
308
317
  /** Polar initial velocity — `speed` px/s, `angle` degrees (0 = +x / right). */
309
318
  velocity: {
310
319
  speed: readonly [number, number];
@@ -347,6 +356,8 @@ interface ParticlePresetRest {
347
356
  forces?: ParticleForces;
348
357
  spin?: readonly [number, number];
349
358
  area?: AreaSpec;
359
+ /** Safe-area clamp (relative [0,1]) — no spawn below this Y. See ParticleSpec.safeBottom. */
360
+ safeBottom?: number;
350
361
  opacityOverLife?: OverLife;
351
362
  scaleOverLife?: OverLife;
352
363
  appearance?: (i: number, ctx: ParticleAppearanceContext) => Node | ParticleAppearance;
@@ -363,7 +374,7 @@ interface DriftOptions extends ParticlePresetRest {
363
374
  count?: number;
364
375
  /** Continuous emission rate, particles/sec (default 8). */
365
376
  rate?: number;
366
- /** Spawn point, relative viewport coords (default centered-lower [0.5,0.6]). */
377
+ /** Spawn point, relative viewport coords (default centered [0.5,0.5] — the conservative caption-safe band). */
367
378
  origin?: Place;
368
379
  /** Themed mote color (default a soft blue). */
369
380
  color?: string;
package/dist/motion.js CHANGED
@@ -403,6 +403,13 @@ function particles(spec) {
403
403
  assertFiniteNum(spec.area.w, "area.w");
404
404
  assertFiniteNum(spec.area.h, "area.h");
405
405
  } else if (spec.area?.kind === "disc") assertFiniteNum(spec.area.radius, "area.radius");
406
+ if (spec.safeBottom !== void 0) {
407
+ const sb = assertFiniteNum(spec.safeBottom, "safeBottom");
408
+ if (sb < 0 || sb > 1) throw new ParticleError(`particles(): safeBottom must be a RELATIVE fraction in [0,1] (got ${sb}) — it is safeBottom*box.h, not a pixel Y (did you pass a captionTop in px?).`);
409
+ const areaHalfHRel = spec.area?.kind === "box" ? spec.area.h / 2 / spec.box.h : spec.area?.kind === "disc" ? spec.area.radius / spec.box.h : 0;
410
+ const bandTopRel = spec.origin[1] - areaHalfHRel;
411
+ if (sb < bandTopRel) throw new ParticleError(`particles(): safeBottom ${sb} is above the spawn band top (${bandTopRel.toFixed(3)}) — no valid spawn region (raise safeBottom, or lower the origin/shrink the area).`);
412
+ }
406
413
  const emitTimes = buildEmitTimes(spec);
407
414
  const opacityCurves = new Array(count);
408
415
  const scaleCurves = new Array(count);
@@ -434,6 +441,7 @@ function particles(spec) {
434
441
  const ox = spec.origin[0] * spec.box.w;
435
442
  const oy = spec.origin[1] * spec.box.h;
436
443
  const area = spec.area;
444
+ const safeBottomPx = spec.safeBottom !== void 0 ? spec.safeBottom * spec.box.h : void 0;
437
445
  const emitDue = (w, rng) => {
438
446
  const t = w.frame / spec.fps;
439
447
  while (w.next < emitTimes.length && emitTimes[w.next] <= t + 1e-9) {
@@ -455,6 +463,7 @@ function particles(spec) {
455
463
  px += Math.cos(th) * rr;
456
464
  py += Math.sin(th) * rr;
457
465
  }
466
+ if (safeBottomPx !== void 0 && py > safeBottomPx) py = safeBottomPx;
458
467
  s.x = px;
459
468
  s.y = py;
460
469
  s.rot = 0;
@@ -556,6 +565,7 @@ function mergeSpec(base, rest) {
556
565
  ...rest.forces !== void 0 ? { forces: rest.forces } : {},
557
566
  ...rest.spin !== void 0 ? { spin: rest.spin } : {},
558
567
  ...rest.area !== void 0 ? { area: rest.area } : {},
568
+ ...rest.safeBottom !== void 0 ? { safeBottom: rest.safeBottom } : {},
559
569
  ...rest.opacityOverLife !== void 0 ? { opacityOverLife: rest.opacityOverLife } : {},
560
570
  ...rest.scaleOverLife !== void 0 ? { scaleOverLife: rest.scaleOverLife } : {},
561
571
  ...rest.appearance !== void 0 ? { appearance: rest.appearance } : {},
@@ -584,7 +594,7 @@ function drift(opts) {
584
594
  duration: opts.duration,
585
595
  fps: opts.fps,
586
596
  rate: opts.rate ?? 8,
587
- origin: opts.origin ?? [.5, .6],
597
+ origin: opts.origin ?? [.5, .5],
588
598
  lifetime: [3, 6],
589
599
  velocity: {
590
600
  speed: [4, 14],
@@ -594,7 +604,7 @@ function drift(opts) {
594
604
  area: {
595
605
  kind: "box",
596
606
  w: opts.box.w * .8,
597
- h: opts.box.h * .5
607
+ h: opts.box.h * .36
598
608
  },
599
609
  opacityOverLife: fadeCurve(.5, .2, .35),
600
610
  appearance: dotAppearance(color, radius)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.57.0-pre.0",
3
+ "version": "0.57.1-pre.0",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -77,7 +77,7 @@
77
77
  ],
78
78
  "dependencies": {
79
79
  "yoga-layout": "^3.2.1",
80
- "@glissade/core": "0.57.0-pre.0"
80
+ "@glissade/core": "0.57.1-pre.0"
81
81
  },
82
82
  "repository": {
83
83
  "type": "git",