@glissade/scene 0.10.0 → 0.10.1-pre.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/dist/index.d.ts CHANGED
@@ -440,6 +440,10 @@ declare class ShaderEffect extends Group {
440
440
  }
441
441
  //#endregion
442
442
  //#region src/raster2d.d.ts
443
+ /** A backend gradient handle (DOM CanvasGradient and @napi-rs CanvasGradient both satisfy it). */
444
+ interface CanvasGradientLike {
445
+ addColorStop(offset: number, color: string): void;
446
+ }
443
447
  /** The structural path surface buildPath drives — DOM Path2D and @napi-rs Path2D both satisfy it. */
444
448
  interface PathLike {
445
449
  moveTo(x: number, y: number): void;
@@ -468,6 +472,8 @@ interface Ctx2DLike<TPath, TDrawable> {
468
472
  drawImage(image: TDrawable, x: number, y: number, w?: number, h?: number): void;
469
473
  drawImage(image: TDrawable, sx: number, sy: number, sw: number, sh: number, x: number, y: number, w: number, h: number): void;
470
474
  setLineDash(segments: number[]): void;
475
+ createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradientLike;
476
+ createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradientLike;
471
477
  lineDashOffset: number;
472
478
  fillStyle: unknown;
473
479
  strokeStyle: unknown;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { A as quantize, B as applyToPoint, C as validateSketch, D as breakLines, E as MEASURE_QUANTUM_PX, F as createDisplayListBuilder, H as invert, I as filtersToCanvasFilter, L as glow, M as segmentWords, N as setDefaultMeasurer, O as estimatingMeasurer, P as FilterValidationError, R as validateFilters, S as validateHachure, T as resolveAnchor, U as matEquals, V as fromTRS, W as multiply, _ as flatten, a as Circle, b as roughen, c as Path, d as Video, f as pathFromSegs, g as arcLength, h as SketchValidationError, i as setLayoutEngine, j as segmentGraphemes, k as fallbackMeasurer, l as Rect, m as roundedRectSegs, n as getLayoutEngine, o as Group, p as revealSchedule, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as hachureLines, w as Node, x as sketchStrokes, y as resolveSketch, z as IDENTITY } from "./layoutEngine.js";
2
- import { bindTimeline, buildFontRegistry, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, parseCmap, signal, stagger, track, validateFonts, vec2Signal } from "@glissade/core";
2
+ import { bindTimeline, buildFontRegistry, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, lerpColor, parseCmap, signal, stagger, track, validateFonts, vec2Signal } from "@glissade/core";
3
3
  //#region src/highlight.ts
4
4
  /**
5
5
  * Marker-style text highlight: per-line rounded rects behind a Text node's
@@ -958,6 +958,38 @@ var ShaderEffect = class extends Group {
958
958
  };
959
959
  }
960
960
  };
961
+ const smoothstep = (u) => u * u * (3 - 2 * u);
962
+ const GAUSS_K = 2.4;
963
+ const GAUSS_NORM = 1 - Math.exp(-5.76 / 2);
964
+ const gaussianEase = (u) => (1 - Math.exp(-((u * GAUSS_K) ** 2) / 2)) / GAUSS_NORM;
965
+ /**
966
+ * Densify `stops` into a `smooth`/`gaussian` oklab ramp. The output spans the
967
+ * input's offset range with GRADIENT_RAMP_STEPS uniformly-spaced stops; each
968
+ * point eases its blend within the authored segment it falls in. Returns the
969
+ * input unchanged for `linear` (or a single stop) — the canvas-native path.
970
+ */
971
+ function densifyStops(stops, mode) {
972
+ if (mode === "linear" || stops.length < 2) return stops;
973
+ const ease = mode === "gaussian" ? gaussianEase : smoothstep;
974
+ const o0 = stops[0].offset;
975
+ const span = stops[stops.length - 1].offset - o0;
976
+ if (span <= 0) return stops;
977
+ const out = [];
978
+ let seg = 0;
979
+ for (let i = 0; i < 64; i++) {
980
+ const offset = o0 + span * (i / 63);
981
+ while (seg < stops.length - 2 && offset > stops[seg + 1].offset) seg++;
982
+ const a = stops[seg];
983
+ const b = stops[seg + 1];
984
+ const w = b.offset - a.offset;
985
+ const u = w > 0 ? Math.min(1, Math.max(0, (offset - a.offset) / w)) : 0;
986
+ out.push({
987
+ offset,
988
+ color: lerpColor(a.color, b.color, ease(u))
989
+ });
990
+ }
991
+ return out;
992
+ }
961
993
  //#endregion
962
994
  //#region src/raster2d.ts
963
995
  /**
@@ -967,6 +999,25 @@ var ShaderEffect = class extends Group {
967
999
  * four-line adapters, so the twin rasterizers structurally cannot drift —
968
1000
  * the golden + SSIM suites verify the refactor preserved every byte.
969
1001
  */
1002
+ function resolveFill(ctx, paint, bounds) {
1003
+ if (paint.kind === "color") return paint.color;
1004
+ let g;
1005
+ if (paint.kind === "radial") {
1006
+ const cx = paint.center ? paint.center[0] : bounds ? (bounds.minX + bounds.maxX) / 2 : 0;
1007
+ const cy = paint.center ? paint.center[1] : bounds ? (bounds.minY + bounds.maxY) / 2 : 0;
1008
+ const r = paint.radius !== void 0 ? paint.radius : bounds ? Math.hypot(bounds.maxX - bounds.minX, bounds.maxY - bounds.minY) / 2 : 0;
1009
+ g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
1010
+ } else {
1011
+ const fx = paint.from ? paint.from[0] : bounds ? (bounds.minX + bounds.maxX) / 2 : 0;
1012
+ const fy = paint.from ? paint.from[1] : bounds ? bounds.minY : 0;
1013
+ const tx = paint.to ? paint.to[0] : bounds ? (bounds.minX + bounds.maxX) / 2 : 0;
1014
+ const ty = paint.to ? paint.to[1] : bounds ? bounds.maxY : 0;
1015
+ g = ctx.createLinearGradient(fx, fy, tx, ty);
1016
+ }
1017
+ const stops = paint.interpolation ? densifyStops(paint.stops, paint.interpolation) : paint.stops;
1018
+ for (const s of stops) g.addColorStop(s.offset, s.color);
1019
+ return g;
1020
+ }
970
1021
  function fontString(font) {
971
1022
  return `${font.style === "italic" ? "italic " : ""}${font.weight !== void 0 && font.weight !== 400 ? `${font.weight} ` : ""}${font.size}px ${font.family}`;
972
1023
  }
@@ -1260,15 +1311,16 @@ var Raster2D = class {
1260
1311
  break;
1261
1312
  case "fillPath": {
1262
1313
  const ctx = ctxOf();
1263
- ctx.fillStyle = cmd.paint.color;
1264
- ctx.fill(this.path(list.resources, cmd.path));
1265
1314
  const b = this.pathBounds(list.resources, cmd.path);
1315
+ ctx.fillStyle = resolveFill(ctx, cmd.paint, b);
1316
+ ctx.fill(this.path(list.resources, cmd.path));
1266
1317
  if (b) accumulateRect(top(), mat, b.minX, b.minY, b.maxX, b.maxY);
1267
1318
  break;
1268
1319
  }
1269
1320
  case "strokePath": {
1270
1321
  const ctx = ctxOf();
1271
- ctx.strokeStyle = cmd.paint.color;
1322
+ const sb = this.pathBounds(list.resources, cmd.path);
1323
+ ctx.strokeStyle = resolveFill(ctx, cmd.paint, sb);
1272
1324
  ctx.lineWidth = cmd.stroke.width;
1273
1325
  ctx.lineCap = cmd.stroke.cap ?? "butt";
1274
1326
  ctx.lineJoin = cmd.stroke.join ?? "miter";
@@ -1281,17 +1333,16 @@ var Raster2D = class {
1281
1333
  ctx.setLineDash([]);
1282
1334
  ctx.lineDashOffset = 0;
1283
1335
  }
1284
- const b = this.pathBounds(list.resources, cmd.path);
1285
- if (b) {
1336
+ if (sb) {
1286
1337
  const o = cmd.stroke.width * ((cmd.stroke.join ?? "miter") === "miter" ? 5 : 1);
1287
- accumulateRect(top(), mat, b.minX - o, b.minY - o, b.maxX + o, b.maxY + o);
1338
+ accumulateRect(top(), mat, sb.minX - o, sb.minY - o, sb.maxX + o, sb.maxY + o);
1288
1339
  }
1289
1340
  break;
1290
1341
  }
1291
1342
  case "fillText": {
1292
1343
  const ctx = ctxOf();
1293
1344
  ctx.font = fontString(cmd.font);
1294
- ctx.fillStyle = cmd.paint.color;
1345
+ ctx.fillStyle = resolveFill(ctx, cmd.paint, null);
1295
1346
  ctx.textBaseline = "alphabetic";
1296
1347
  ctx.textAlign = cmd.align ?? "left";
1297
1348
  ctx.fillText(cmd.text, cmd.x, cmd.y);
@@ -1,4 +1,4 @@
1
- import { BindableSignal, PathValue, ReadonlySignal, Rng, Track, Vec2, Vec2Signal } from "@glissade/core";
1
+ import { BindableSignal, Paint, PathValue, ReadonlySignal, Rng, Track, Vec2, Vec2Signal } from "@glissade/core";
2
2
 
3
3
  //#region src/matrix.d.ts
4
4
 
@@ -33,11 +33,6 @@ type Resource = {
33
33
  mediaT: number;
34
34
  };
35
35
  type BlendMode = 'source-over' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten';
36
- /** M1: solid colors. Gradients/patterns are additive later — backends switch on kind. */
37
- type Paint = {
38
- kind: 'color';
39
- color: string;
40
- };
41
36
  interface StrokeStyle {
42
37
  width: number;
43
38
  cap?: 'butt' | 'round' | 'square';
@@ -498,8 +493,12 @@ declare class Group extends Node {
498
493
  remove(child: Node): this;
499
494
  protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
500
495
  }
496
+ /** A color string is sugar for a solid `color` Paint; a Paint passes through. */
497
+
501
498
  interface ShapeProps extends NodeProps {
502
- fill?: PropInit<string>;
499
+ /** A CSS color string, or a `Paint` (e.g. a `radial` gradient — soft-light
500
+ * fills with no blur filter; center/radius default to the shape bounds). */
501
+ fill?: PropInit<string | Paint>;
503
502
  stroke?: PropInit<string>;
504
503
  strokeWidth?: PropInit<number>;
505
504
  /** hand-drawn look: the outline is geometrically roughened (see sketch.ts) */
@@ -515,7 +514,7 @@ interface ShapeProps extends NodeProps {
515
514
  sketchFill?: HachureSpec;
516
515
  }
517
516
  declare abstract class Shape extends Node {
518
- readonly fill: BindableSignal<string>;
517
+ readonly fill: BindableSignal<string | Paint>;
519
518
  readonly stroke: BindableSignal<string>;
520
519
  readonly strokeWidth: BindableSignal<number>;
521
520
  readonly sketch: SketchStyle | undefined;
@@ -948,6 +948,13 @@ var Group = class extends Node {
948
948
  for (const child of sorted) child.emit(out, ctx);
949
949
  }
950
950
  };
951
+ /** A color string is sugar for a solid `color` Paint; a Paint passes through. */
952
+ function toPaint(fill) {
953
+ return typeof fill === "string" ? {
954
+ kind: "color",
955
+ color: fill
956
+ } : fill;
957
+ }
951
958
  var Shape = class extends Node {
952
959
  fill;
953
960
  stroke;
@@ -984,10 +991,7 @@ var Shape = class extends Node {
984
991
  if (fill) out.push({
985
992
  op: "fillPath",
986
993
  path,
987
- paint: {
988
- kind: "color",
989
- color: fill
990
- }
994
+ paint: toPaint(fill)
991
995
  });
992
996
  const stroke = this.stroke();
993
997
  const width = this.strokeWidth();
@@ -1021,14 +1025,11 @@ var Shape = class extends Node {
1021
1025
  out.push({
1022
1026
  op: "fillPath",
1023
1027
  path,
1024
- paint: {
1025
- kind: "color",
1026
- color: fill
1027
- }
1028
+ paint: toPaint(fill)
1028
1029
  });
1029
1030
  }
1030
1031
  const { strokes, resolved } = roughen(segs, this.sketch, rng);
1031
- const ink = this.stroke() || fill || "#000000";
1032
+ const ink = this.stroke() || (typeof fill === "string" ? fill : "") || "#000000";
1032
1033
  if (this.sketchFill) {
1033
1034
  const clipPath = out.resource({
1034
1035
  kind: "path",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.10.0",
3
+ "version": "0.10.1-pre.1",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "yoga-layout": "^3.2.1",
23
- "@glissade/core": "0.10.0"
23
+ "@glissade/core": "0.10.1-pre.1"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",