@glissade/scene 0.5.0-pre.5 → 0.5.0-pre.6

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
@@ -1,4 +1,4 @@
1
- import { $ as Resource, A as HitArea, B as segmentGraphemes, C as VideoProps, D as AnchorSpec, E as roundedRectSegs, F as TextMeasurer, G as DisplayListBuilder, H as setDefaultMeasurer, I as TextMetricsLite, J as FilterValidationError, K as DrawCommand, L as breakLines, M as NodeProps, N as PropInit, O as BindablePropTarget, P as resolveAnchor, Q as Rect$1, R as estimatingMeasurer, S as Video, T as revealSchedule, U as BlendMode, V as segmentWords, W as DisplayList, X as Paint, Y as FontSpec, Z as PathSeg, _ as Rect, a as LayoutEngineMissingError, at as glow, b as Text, c as requireLayoutEngine, ct as Mat2x3, d as Group, dt as invert, et as ResourceId, f as ImageNode, ft as matEquals, g as PathProps, h as Path, i as LayoutEngine, it as filtersToCanvasFilter, j as Node, k as EvalContext, l as setLayoutEngine, lt as applyToPoint, m as LineBox, n as LayoutChildSpec, nt as StrokeStyle, ot as validateFilters, p as ImageProps, pt as multiply, q as FilterSpec, r as LayoutContainerSpec, rt as createDisplayListBuilder, s as getLayoutEngine, st as IDENTITY, t as LayoutBox, tt as ShaderRef, u as Circle, ut as fromTRS, v as RevealMark, w as WordBox, x as TextProps, y as ShapeProps, z as quantize } from "./layoutEngine.js";
1
+ import { $ as BlendMode, A as SketchValidationError, B as HitArea, C as VideoProps, Ct as multiply, D as Polyline, E as roundedRectSegs, F as sketchStrokes, G as TextMeasurer, H as NodeProps, I as validateSketch, J as estimatingMeasurer, K as TextMetricsLite, L as AnchorSpec, M as flatten, N as resolveSketch, O as ResolvedSketch, P as roughen, Q as setDefaultMeasurer, R as BindablePropTarget, S as Video, St as matEquals, T as revealSchedule, U as PropInit, V as Node, W as resolveAnchor, X as segmentGraphemes, Y as quantize, Z as segmentWords, _ as Rect, _t as IDENTITY, a as LayoutEngineMissingError, at as FontSpec, b as Text, bt as fromTRS, c as requireLayoutEngine, ct as Rect$1, d as Group, dt as ShaderRef, et as DisplayList, f as ImageNode, ft as StrokeStyle, g as PathProps, gt as validateFilters, h as Path, ht as glow, i as LayoutEngine, it as FilterValidationError, j as arcLength, k as SketchStyle, l as setLayoutEngine, lt as Resource, m as LineBox, mt as filtersToCanvasFilter, n as LayoutChildSpec, nt as DrawCommand, ot as Paint, p as ImageProps, pt as createDisplayListBuilder, q as breakLines, r as LayoutContainerSpec, rt as FilterSpec, s as getLayoutEngine, st as PathSeg, t as LayoutBox, tt as DisplayListBuilder, u as Circle, ut as ResourceId, v as RevealMark, vt as Mat2x3, w as WordBox, x as TextProps, xt as invert, y as ShapeProps, yt as applyToPoint, z as EvalContext } from "./layoutEngine.js";
2
2
  import { BindableSignal, BoundTimeline, CompiledTimeline, PathValue, Playhead, Timeline, Track, Vec2 } from "@glissade/core";
3
3
 
4
4
  //#region src/highlight.d.ts
@@ -116,6 +116,7 @@ interface TypewriterResult {
116
116
  declare function typewriter(target: string, edits: readonly TypeEdit[], opts?: {
117
117
  start?: number;
118
118
  perChar?: number;
119
+ gap?: number;
119
120
  }): TypewriterResult;
120
121
  //#endregion
121
122
  //#region src/motionPath.d.ts
@@ -306,6 +307,7 @@ interface Ctx2DLike<TPath, TDrawable> {
306
307
  drawImage(image: TDrawable, x: number, y: number, w?: number, h?: number): void;
307
308
  drawImage(image: TDrawable, sx: number, sy: number, sw: number, sh: number, x: number, y: number, w: number, h: number): void;
308
309
  setLineDash(segments: number[]): void;
310
+ lineDashOffset: number;
309
311
  fillStyle: unknown;
310
312
  strokeStyle: unknown;
311
313
  lineWidth: number;
@@ -415,4 +417,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
415
417
  */
416
418
  declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
417
419
  //#endregion
418
- export { type AnchorSpec, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EditMark, type EvalContext, type FilterSpec, FilterValidationError, FollowPath, type FollowPathProps, type FontSpec, Group, Highlight, type HighlightProps, type HitArea, IDENTITY, type ImageHandle, ImageNode, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, type Mat2x3, Node, type NodeProps, type Paint, Path, type PathLike, type PathProps, type PathSampler, type PathSeg, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type StepMark, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, type TypeEdit, type TypewriterResult, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, followPath, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, typewriter, validateFilters };
420
+ export { type AnchorSpec, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EditMark, type EvalContext, type FilterSpec, FilterValidationError, FollowPath, type FollowPathProps, type FontSpec, Group, Highlight, type HighlightProps, type HitArea, IDENTITY, type ImageHandle, ImageNode, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, type Mat2x3, Node, type NodeProps, type Paint, Path, type PathLike, type PathProps, type PathSampler, type PathSeg, type Polyline, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type ResolvedSketch, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type SketchStyle, SketchValidationError, type StepMark, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, type TypeEdit, type TypewriterResult, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, arcLength, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateSketch };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as fromTRS, C as FilterValidationError, D as validateFilters, E as glow, M as matEquals, N as multiply, O as IDENTITY, S as setDefaultMeasurer, T as filtersToCanvasFilter, _ as estimatingMeasurer, a as Circle, b as segmentGraphemes, c as Path, d as Video, f as revealSchedule, g as breakLines, h as resolveAnchor, i as setLayoutEngine, j as invert, k as applyToPoint, l as Rect, m as Node, n as getLayoutEngine, o as Group, p as roundedRectSegs, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as fallbackMeasurer, w as createDisplayListBuilder, x as segmentWords, y as quantize } from "./layoutEngine.js";
1
+ import { A as FilterValidationError, B as multiply, C as breakLines, D as segmentGraphemes, E as quantize, F as IDENTITY, I as applyToPoint, L as fromTRS, M as filtersToCanvasFilter, N as glow, O as segmentWords, P as validateFilters, R as invert, S as resolveAnchor, T as fallbackMeasurer, _ as resolveSketch, a as Circle, b as validateSketch, c as Path, d as Video, f as revealSchedule, g as flatten, h as arcLength, i as setLayoutEngine, j as createDisplayListBuilder, k as setDefaultMeasurer, l as Rect, m as SketchValidationError, n as getLayoutEngine, o as Group, p as roundedRectSegs, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as roughen, w as estimatingMeasurer, x as Node, y as sketchStrokes, z as matEquals } from "./layoutEngine.js";
2
2
  import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, signal, track, vec2Signal } from "@glissade/core";
3
3
  //#region src/highlight.ts
4
4
  /**
@@ -181,6 +181,7 @@ const DEFAULT_PER_CHAR = .06;
181
181
  function typewriter(target, edits, opts = {}) {
182
182
  const start = opts.start ?? 0;
183
183
  const globalPer = opts.perChar ?? DEFAULT_PER_CHAR;
184
+ const gap = opts.gap ?? 0;
184
185
  let t = start;
185
186
  const shown = [];
186
187
  const keys = [key(start, "", { interp: "hold" })];
@@ -221,6 +222,7 @@ function typewriter(target, edits, opts = {}) {
221
222
  end: t,
222
223
  value: shown.join("")
223
224
  });
225
+ if (gap > 0 && ei < edits.length - 1) t += gap;
224
226
  }
225
227
  return {
226
228
  track: track(target, "string", keys),
@@ -859,9 +861,15 @@ var Raster2D = class {
859
861
  ctx.lineWidth = cmd.stroke.width;
860
862
  ctx.lineCap = cmd.stroke.cap ?? "butt";
861
863
  ctx.lineJoin = cmd.stroke.join ?? "miter";
862
- if (cmd.stroke.dash) ctx.setLineDash(cmd.stroke.dash);
864
+ if (cmd.stroke.dash) {
865
+ ctx.setLineDash(cmd.stroke.dash);
866
+ ctx.lineDashOffset = cmd.stroke.dashOffset ?? 0;
867
+ }
863
868
  ctx.stroke(this.path(list.resources, cmd.path));
864
- if (cmd.stroke.dash) ctx.setLineDash([]);
869
+ if (cmd.stroke.dash) {
870
+ ctx.setLineDash([]);
871
+ ctx.lineDashOffset = 0;
872
+ }
865
873
  const b = this.pathBounds(list.resources, cmd.path);
866
874
  if (b) {
867
875
  const o = cmd.stroke.width * ((cmd.stroke.join ?? "miter") === "miter" ? 5 : 1);
@@ -1071,4 +1079,4 @@ function evaluate(scene, doc, t) {
1071
1079
  });
1072
1080
  }
1073
1081
  //#endregion
1074
- export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, FollowPath, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, followPath, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, typewriter, validateFilters };
1082
+ export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, FollowPath, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, SketchValidationError, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, arcLength, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateSketch };
package/dist/layout.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as TextMeasurer, G as DisplayListBuilder, M as NodeProps, N as PropInit, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, j as Node, k as EvalContext, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
1
+ import { G as TextMeasurer, H as NodeProps, U as PropInit, V as Node, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox, tt as DisplayListBuilder, z as EvalContext } from "./layoutEngine.js";
2
2
  import { BindableSignal } from "@glissade/core";
3
3
 
4
4
  //#region src/layout.d.ts
package/dist/layout.js CHANGED
@@ -1,4 +1,4 @@
1
- import { i as setLayoutEngine, n as getLayoutEngine, o as Group, r as requireLayoutEngine, t as LayoutEngineMissingError, v as fallbackMeasurer } from "./layoutEngine.js";
1
+ import { T as fallbackMeasurer, i as setLayoutEngine, n as getLayoutEngine, o as Group, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
2
2
  import { signal } from "@glissade/core";
3
3
  //#region src/layout.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { BindableSignal, PathValue, ReadonlySignal, Track, Vec2, Vec2Signal } from "@glissade/core";
1
+ import { BindableSignal, PathValue, ReadonlySignal, Rng, Track, Vec2, Vec2Signal } from "@glissade/core";
2
2
 
3
3
  //#region src/matrix.d.ts
4
4
 
@@ -351,6 +351,72 @@ declare abstract class Node {
351
351
  emit(out: DisplayListBuilder, ctx: EvalContext): void;
352
352
  }
353
353
  //#endregion
354
+ //#region src/sketch.d.ts
355
+ /** The closed set of hand-drawn looks. Mirrors FilterSpec's discipline. */
356
+ type SketchStyle = {
357
+ kind: 'marker';
358
+ width?: number;
359
+ roughness?: number;
360
+ } | {
361
+ kind: 'crayon';
362
+ width?: number;
363
+ roughness?: number;
364
+ passes?: number;
365
+ } | {
366
+ kind: 'pencil';
367
+ width?: number;
368
+ roughness?: number;
369
+ passes?: number;
370
+ } | {
371
+ kind: 'ink';
372
+ width?: number;
373
+ roughness?: number;
374
+ } | {
375
+ kind: 'chalk';
376
+ width?: number;
377
+ roughness?: number;
378
+ dash?: number[];
379
+ };
380
+ declare class SketchValidationError extends Error {
381
+ constructor(message: string);
382
+ }
383
+ /** Reject unknown kinds / out-of-range params at construction (like validateFilters). */
384
+ declare function validateSketch(s: SketchStyle): void;
385
+ interface ResolvedSketch {
386
+ width: number;
387
+ roughness: number;
388
+ passes: number;
389
+ dash?: number[];
390
+ }
391
+ /** Per-kind defaults — the character of each look. */
392
+ declare function resolveSketch(s: SketchStyle): ResolvedSketch;
393
+ interface Polyline {
394
+ points: [number, number][];
395
+ closed: boolean;
396
+ }
397
+ /**
398
+ * Flatten a path to polylines — de Casteljau for C/Q, arc sampling for E
399
+ * (Circle and rounded-rect corners are 'E' segments, so this MUST handle them
400
+ * or those shapes roughen wrong). `steps` is the samples per curved segment.
401
+ */
402
+ declare function flatten(segs: readonly PathSeg[], steps?: number): Polyline[];
403
+ /** Total length of a flattened polyline (for draw-on dashing). */
404
+ declare function arcLength(poly: Polyline): number;
405
+ /**
406
+ * Roughen a path into hand-drawn stroke passes. Each segment becomes a bowed,
407
+ * jittered quadratic; `passes` overlay slightly different jitters for the
408
+ * built-up look. `rng` must be a freshly seeded generator (the caller reseeds
409
+ * per draw from a stable seed, so evaluate() stays pure).
410
+ */
411
+ declare function roughen(segs: readonly PathSeg[], style: SketchStyle, rng: Rng): {
412
+ strokes: PathSeg[][];
413
+ resolved: ResolvedSketch;
414
+ };
415
+ /** FNV-1a 32-bit — a stable per-shape sketch seed from its id. */
416
+
417
+ /** Convenience: the rough stroke passes for a path at a given seed. */
418
+ declare function sketchStrokes(segs: readonly PathSeg[], style: SketchStyle, seed: number): PathSeg[][];
419
+ //#endregion
354
420
  //#region src/nodes.d.ts
355
421
  /** Rounded-rect path segments — Rect's outline, shared with Highlight. */
356
422
  declare function roundedRectSegs(x: number, y: number, w: number, h: number, r: number): PathSeg[];
@@ -366,14 +432,28 @@ interface ShapeProps extends NodeProps {
366
432
  fill?: PropInit<string>;
367
433
  stroke?: PropInit<string>;
368
434
  strokeWidth?: PropInit<number>;
435
+ /** hand-drawn look: the outline is geometrically roughened (see sketch.ts) */
436
+ sketch?: SketchStyle;
437
+ /** seed for the roughening; default a stable hash of the node id */
438
+ sketchSeed?: number;
439
+ /** draw-on for a sketched shape: 0..1 of the outline drawn (default 1 = whole).
440
+ * Track `<id>/reveal`. Precise for single-contour shapes; multi-contour ones
441
+ * reveal each contour in parallel. */
442
+ reveal?: PropInit<number>;
369
443
  }
370
444
  declare abstract class Shape extends Node {
371
445
  readonly fill: BindableSignal<string>;
372
446
  readonly stroke: BindableSignal<string>;
373
447
  readonly strokeWidth: BindableSignal<number>;
448
+ readonly sketch: SketchStyle | undefined;
449
+ readonly sketchSeed: number;
450
+ readonly reveal: BindableSignal<number>;
374
451
  constructor(props?: ShapeProps);
375
452
  protected abstract pathSegs(): PathSeg[];
376
453
  protected draw(out: DisplayListBuilder): void;
454
+ /** Hand-drawn render: solid fill (if any) under roughened, multi-pass strokes.
455
+ * The seed is consumed fresh each draw, so re-evaluation is byte-identical. */
456
+ private drawSketch;
377
457
  }
378
458
  declare class Rect extends Shape {
379
459
  readonly width: BindableSignal<number>;
@@ -673,4 +753,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
673
753
  declare function getLayoutEngine(): LayoutEngine | null;
674
754
  declare function requireLayoutEngine(): LayoutEngine;
675
755
  //#endregion
676
- export { Resource as $, HitArea as A, segmentGraphemes as B, VideoProps as C, AnchorSpec as D, roundedRectSegs as E, TextMeasurer as F, DisplayListBuilder as G, setDefaultMeasurer as H, TextMetricsLite as I, FilterValidationError as J, DrawCommand as K, breakLines as L, NodeProps as M, PropInit as N, BindablePropTarget as O, resolveAnchor as P, Rect$1 as Q, estimatingMeasurer as R, Video as S, revealSchedule as T, BlendMode as U, segmentWords as V, DisplayList as W, Paint as X, FontSpec as Y, PathSeg as Z, Rect as _, LayoutEngineMissingError as a, glow as at, Text as b, requireLayoutEngine as c, Mat2x3 as ct, Group as d, invert as dt, ResourceId as et, ImageNode as f, matEquals as ft, PathProps as g, Path as h, LayoutEngine as i, filtersToCanvasFilter as it, Node as j, EvalContext as k, setLayoutEngine as l, applyToPoint as lt, LineBox as m, LayoutChildSpec as n, StrokeStyle as nt, LayoutResult as o, validateFilters as ot, ImageProps as p, multiply as pt, FilterSpec as q, LayoutContainerSpec as r, createDisplayListBuilder as rt, getLayoutEngine as s, IDENTITY as st, LayoutBox as t, ShaderRef as tt, Circle as u, fromTRS as ut, RevealMark as v, WordBox as w, TextProps as x, ShapeProps as y, quantize as z };
756
+ export { BlendMode as $, SketchValidationError as A, HitArea as B, VideoProps as C, multiply as Ct, Polyline as D, roundedRectSegs as E, sketchStrokes as F, TextMeasurer as G, NodeProps as H, validateSketch as I, estimatingMeasurer as J, TextMetricsLite as K, AnchorSpec as L, flatten as M, resolveSketch as N, ResolvedSketch as O, roughen as P, setDefaultMeasurer as Q, BindablePropTarget as R, Video as S, matEquals as St, revealSchedule as T, PropInit as U, Node as V, resolveAnchor as W, segmentGraphemes as X, quantize as Y, segmentWords as Z, Rect as _, IDENTITY as _t, LayoutEngineMissingError as a, FontSpec as at, Text as b, fromTRS as bt, requireLayoutEngine as c, Rect$1 as ct, Group as d, ShaderRef as dt, DisplayList as et, ImageNode as f, StrokeStyle as ft, PathProps as g, validateFilters as gt, Path as h, glow as ht, LayoutEngine as i, FilterValidationError as it, arcLength as j, SketchStyle as k, setLayoutEngine as l, Resource as lt, LineBox as m, filtersToCanvasFilter as mt, LayoutChildSpec as n, DrawCommand as nt, LayoutResult as o, Paint as ot, ImageProps as p, createDisplayListBuilder as pt, breakLines as q, LayoutContainerSpec as r, FilterSpec as rt, getLayoutEngine as s, PathSeg as st, LayoutBox as t, DisplayListBuilder as tt, Circle as u, ResourceId as ut, RevealMark as v, Mat2x3 as vt, WordBox as w, TextProps as x, invert as xt, ShapeProps as y, applyToPoint as yt, EvalContext as z };
@@ -1,4 +1,4 @@
1
- import { TARGET_PATH, computed, emitDevWarning, signal, vec2Signal } from "@glissade/core";
1
+ import { TARGET_PATH, computed, emitDevWarning, random, signal, vec2Signal } from "@glissade/core";
2
2
  //#region src/matrix.ts
3
3
  const IDENTITY = [
4
4
  1,
@@ -437,6 +437,230 @@ var Node = class {
437
437
  }
438
438
  };
439
439
  //#endregion
440
+ //#region src/sketch.ts
441
+ /**
442
+ * Hand-drawn stroke styles via GEOMETRIC roughening — not raster textures. A
443
+ * shape's outline is flattened to polylines, then each segment is redrawn as a
444
+ * slightly jittered, bowed stroke, overlaid in a few passes. Because it's pure
445
+ * path math seeded by a stable per-shape seed, the result is byte-identical on
446
+ * both backends and re-evaluates deterministically (the seed is consumed fresh
447
+ * each draw, never as a shared stateful stream).
448
+ */
449
+ const KINDS = [
450
+ "marker",
451
+ "crayon",
452
+ "pencil",
453
+ "ink",
454
+ "chalk"
455
+ ];
456
+ var SketchValidationError = class extends Error {
457
+ constructor(message) {
458
+ super(message);
459
+ this.name = "SketchValidationError";
460
+ }
461
+ };
462
+ /** Reject unknown kinds / out-of-range params at construction (like validateFilters). */
463
+ function validateSketch(s) {
464
+ if (!KINDS.includes(s.kind)) throw new SketchValidationError(`unknown sketch kind '${String(s.kind)}' (have: ${KINDS.join(", ")})`);
465
+ if (s.width !== void 0 && !(s.width > 0)) throw new SketchValidationError(`sketch width must be > 0, got ${String(s.width)}`);
466
+ if (s.roughness !== void 0 && !(s.roughness >= 0)) throw new SketchValidationError(`sketch roughness must be ≥ 0, got ${String(s.roughness)}`);
467
+ if ((s.kind === "crayon" || s.kind === "pencil") && s.passes !== void 0 && !(s.passes >= 1)) throw new SketchValidationError(`sketch passes must be ≥ 1, got ${String(s.passes)}`);
468
+ if (s.kind === "chalk" && s.dash !== void 0 && (!Array.isArray(s.dash) || s.dash.some((d) => !(d >= 0)))) throw new SketchValidationError("sketch chalk dash must be an array of non-negative numbers");
469
+ }
470
+ /** Per-kind defaults — the character of each look. */
471
+ function resolveSketch(s) {
472
+ switch (s.kind) {
473
+ case "marker": return {
474
+ width: s.width ?? 8,
475
+ roughness: s.roughness ?? 1.2,
476
+ passes: 2
477
+ };
478
+ case "crayon": return {
479
+ width: s.width ?? 4,
480
+ roughness: s.roughness ?? 2.4,
481
+ passes: s.passes ?? 3
482
+ };
483
+ case "pencil": return {
484
+ width: s.width ?? 1.5,
485
+ roughness: s.roughness ?? 1,
486
+ passes: s.passes ?? 2
487
+ };
488
+ case "ink": return {
489
+ width: s.width ?? 2.5,
490
+ roughness: s.roughness ?? .8,
491
+ passes: 1
492
+ };
493
+ case "chalk": return {
494
+ width: s.width ?? 3,
495
+ roughness: s.roughness ?? 1.6,
496
+ passes: 1,
497
+ dash: s.dash ?? [6, 5]
498
+ };
499
+ }
500
+ }
501
+ const cubic = (p0, c1, c2, p1, t) => {
502
+ const mt = 1 - t;
503
+ const a = mt * mt * mt;
504
+ const b = 3 * mt * mt * t;
505
+ const c = 3 * mt * t * t;
506
+ const d = t * t * t;
507
+ return [a * p0[0] + b * c1[0] + c * c2[0] + d * p1[0], a * p0[1] + b * c1[1] + c * c2[1] + d * p1[1]];
508
+ };
509
+ const quad = (p0, c, p1, t) => {
510
+ const mt = 1 - t;
511
+ return [mt * mt * p0[0] + 2 * mt * t * c[0] + t * t * p1[0], mt * mt * p0[1] + 2 * mt * t * c[1] + t * t * p1[1]];
512
+ };
513
+ const ellipse = (cx, cy, rx, ry, rot, ang) => {
514
+ const ex = rx * Math.cos(ang);
515
+ const ey = ry * Math.sin(ang);
516
+ const cos = Math.cos(rot);
517
+ const sin = Math.sin(rot);
518
+ return [cx + ex * cos - ey * sin, cy + ex * sin + ey * cos];
519
+ };
520
+ /**
521
+ * Flatten a path to polylines — de Casteljau for C/Q, arc sampling for E
522
+ * (Circle and rounded-rect corners are 'E' segments, so this MUST handle them
523
+ * or those shapes roughen wrong). `steps` is the samples per curved segment.
524
+ */
525
+ function flatten(segs, steps = 16) {
526
+ const polys = [];
527
+ let cur = null;
528
+ let px = 0;
529
+ let py = 0;
530
+ let sx = 0;
531
+ let sy = 0;
532
+ const ensure = (x, y) => {
533
+ if (!cur) {
534
+ cur = {
535
+ points: [[x, y]],
536
+ closed: false
537
+ };
538
+ polys.push(cur);
539
+ sx = x;
540
+ sy = y;
541
+ }
542
+ return cur;
543
+ };
544
+ for (const s of segs) switch (s[0]) {
545
+ case "M":
546
+ cur = {
547
+ points: [[s[1], s[2]]],
548
+ closed: false
549
+ };
550
+ polys.push(cur);
551
+ px = sx = s[1];
552
+ py = sy = s[2];
553
+ break;
554
+ case "L":
555
+ ensure(px, py).points.push([s[1], s[2]]);
556
+ px = s[1];
557
+ py = s[2];
558
+ break;
559
+ case "C": {
560
+ const c = ensure(px, py);
561
+ for (let k = 1; k <= steps; k++) c.points.push(cubic([px, py], [s[1], s[2]], [s[3], s[4]], [s[5], s[6]], k / steps));
562
+ px = s[5];
563
+ py = s[6];
564
+ break;
565
+ }
566
+ case "Q": {
567
+ const c = ensure(px, py);
568
+ for (let k = 1; k <= steps; k++) c.points.push(quad([px, py], [s[1], s[2]], [s[3], s[4]], k / steps));
569
+ px = s[3];
570
+ py = s[4];
571
+ break;
572
+ }
573
+ case "E": {
574
+ const [, cx, cy, rx, ry, rot, a0, a1] = s;
575
+ const begin = ellipse(cx, cy, rx, ry, rot, a0);
576
+ const c = ensure(begin[0], begin[1]);
577
+ for (let k = 1; k <= steps; k++) c.points.push(ellipse(cx, cy, rx, ry, rot, a0 + (a1 - a0) * (k / steps)));
578
+ const end = ellipse(cx, cy, rx, ry, rot, a1);
579
+ px = end[0];
580
+ py = end[1];
581
+ break;
582
+ }
583
+ case "Z":
584
+ if (cur) {
585
+ cur.points.push([sx, sy]);
586
+ cur.closed = true;
587
+ px = sx;
588
+ py = sy;
589
+ }
590
+ break;
591
+ }
592
+ return polys.filter((p) => p.points.length > 0);
593
+ }
594
+ /** Total length of a flattened polyline (for draw-on dashing). */
595
+ function arcLength(poly) {
596
+ let len = 0;
597
+ for (let i = 1; i < poly.points.length; i++) len += Math.hypot(poly.points[i][0] - poly.points[i - 1][0], poly.points[i][1] - poly.points[i - 1][1]);
598
+ return len;
599
+ }
600
+ /**
601
+ * Roughen a path into hand-drawn stroke passes. Each segment becomes a bowed,
602
+ * jittered quadratic; `passes` overlay slightly different jitters for the
603
+ * built-up look. `rng` must be a freshly seeded generator (the caller reseeds
604
+ * per draw from a stable seed, so evaluate() stays pure).
605
+ */
606
+ function roughen(segs, style, rng) {
607
+ const resolved = resolveSketch(style);
608
+ const polys = flatten(segs);
609
+ const jit = () => (rng() * 2 - 1) * resolved.roughness;
610
+ const strokes = [];
611
+ for (let pass = 0; pass < resolved.passes; pass++) {
612
+ const out = [];
613
+ for (const poly of polys) {
614
+ const pts = poly.points;
615
+ if (pts.length < 2) continue;
616
+ let ax = pts[0][0] + jit();
617
+ let ay = pts[0][1] + jit();
618
+ out.push([
619
+ "M",
620
+ ax,
621
+ ay
622
+ ]);
623
+ for (let i = 1; i < pts.length; i++) {
624
+ const bx = pts[i][0] + jit();
625
+ const by = pts[i][1] + jit();
626
+ const dx = bx - ax;
627
+ const dy = by - ay;
628
+ const len = Math.hypot(dx, dy) || 1;
629
+ const bow = jit() * .5;
630
+ const mx = (ax + bx) / 2 + -dy / len * bow;
631
+ const my = (ay + by) / 2 + dx / len * bow;
632
+ out.push([
633
+ "Q",
634
+ mx,
635
+ my,
636
+ bx,
637
+ by
638
+ ]);
639
+ ax = bx;
640
+ ay = by;
641
+ }
642
+ }
643
+ strokes.push(out);
644
+ }
645
+ return {
646
+ strokes,
647
+ resolved
648
+ };
649
+ }
650
+ /** FNV-1a 32-bit — a stable per-shape sketch seed from its id. */
651
+ function hashStr(s) {
652
+ let h = 2166136261;
653
+ for (let i = 0; i < s.length; i++) {
654
+ h ^= s.charCodeAt(i);
655
+ h = Math.imul(h, 16777619);
656
+ }
657
+ return h >>> 0;
658
+ }
659
+ /** Convenience: the rough stroke passes for a path at a given seed. */
660
+ function sketchStrokes(segs, style, seed) {
661
+ return roughen(segs, style, random(seed >>> 0)).strokes;
662
+ }
663
+ //#endregion
440
664
  //#region src/nodes.ts
441
665
  /**
442
666
  * Built-in nodes for M1 (DESIGN.md §3.1): Group, Rect, Circle, Text.
@@ -561,17 +785,26 @@ var Shape = class extends Node {
561
785
  fill;
562
786
  stroke;
563
787
  strokeWidth;
788
+ sketch;
789
+ sketchSeed;
790
+ reveal;
564
791
  constructor(props = {}) {
565
792
  super(props);
566
793
  this.fill = initProp(signal(""), props.fill);
567
794
  this.stroke = initProp(signal(""), props.stroke);
568
795
  this.strokeWidth = initProp(signal(0), props.strokeWidth);
796
+ this.reveal = initProp(signal(1), props.reveal);
569
797
  this.registerTarget("fill", this.fill);
570
798
  this.registerTarget("stroke", this.stroke);
571
799
  this.registerTarget("strokeWidth", this.strokeWidth);
800
+ this.registerTarget("reveal", this.reveal);
801
+ if (props.sketch) validateSketch(props.sketch);
802
+ this.sketch = props.sketch;
803
+ this.sketchSeed = props.sketchSeed ?? (this.id !== void 0 ? hashStr(this.id) : 0);
572
804
  }
573
805
  draw(out) {
574
806
  const segs = this.pathSegs();
807
+ if (this.sketch) return this.drawSketch(out, segs);
575
808
  const path = out.resource({
576
809
  kind: "path",
577
810
  segs
@@ -597,12 +830,91 @@ var Shape = class extends Node {
597
830
  stroke: { width }
598
831
  });
599
832
  }
833
+ /** Hand-drawn render: solid fill (if any) under roughened, multi-pass strokes.
834
+ * The seed is consumed fresh each draw, so re-evaluation is byte-identical. */
835
+ drawSketch(out, segs) {
836
+ const fill = this.fill();
837
+ if (fill) {
838
+ const path = out.resource({
839
+ kind: "path",
840
+ segs
841
+ });
842
+ out.push({
843
+ op: "fillPath",
844
+ path,
845
+ paint: {
846
+ kind: "color",
847
+ color: fill
848
+ }
849
+ });
850
+ }
851
+ const { strokes, resolved } = roughen(segs, this.sketch, random(this.sketchSeed >>> 0));
852
+ const ink = this.stroke() || fill || "#000000";
853
+ const reveal = this.reveal();
854
+ const drawOn = reveal < 1;
855
+ for (const passSegs of strokes) {
856
+ if (passSegs.length === 0) continue;
857
+ if (drawOn) {
858
+ for (const contour of splitContours(passSegs)) {
859
+ const len = flatten(contour).reduce((s, p) => s + arcLength(p), 0);
860
+ const path = out.resource({
861
+ kind: "path",
862
+ segs: contour
863
+ });
864
+ out.push({
865
+ op: "strokePath",
866
+ path,
867
+ paint: {
868
+ kind: "color",
869
+ color: ink
870
+ },
871
+ stroke: {
872
+ width: resolved.width,
873
+ cap: "round",
874
+ join: "round",
875
+ dash: [len, len],
876
+ dashOffset: len * (1 - reveal)
877
+ }
878
+ });
879
+ }
880
+ continue;
881
+ }
882
+ const path = out.resource({
883
+ kind: "path",
884
+ segs: passSegs
885
+ });
886
+ out.push({
887
+ op: "strokePath",
888
+ path,
889
+ paint: {
890
+ kind: "color",
891
+ color: ink
892
+ },
893
+ stroke: {
894
+ width: resolved.width,
895
+ cap: "round",
896
+ join: "round",
897
+ ...resolved.dash ? { dash: resolved.dash } : {}
898
+ }
899
+ });
900
+ }
901
+ }
600
902
  };
601
903
  function initProp(sig, init) {
602
904
  if (typeof init === "function") sig.bindSource(init);
603
905
  else if (init !== void 0) sig.set(init);
604
906
  return sig;
605
907
  }
908
+ /** Split a stroke path into its subpaths (each starting at an 'M'). */
909
+ function splitContours(segs) {
910
+ const out = [];
911
+ let cur = null;
912
+ for (const s of segs) if (s[0] === "M") {
913
+ cur = [s];
914
+ out.push(cur);
915
+ } else if (cur) cur.push(s);
916
+ return out;
917
+ }
606
918
  var Rect = class extends Shape {
607
919
  width;
608
920
  height;
@@ -1224,4 +1536,4 @@ function requireLayoutEngine() {
1224
1536
  return engine;
1225
1537
  }
1226
1538
  //#endregion
1227
- export { fromTRS as A, FilterValidationError as C, validateFilters as D, glow as E, matEquals as M, multiply as N, IDENTITY as O, setDefaultMeasurer as S, filtersToCanvasFilter as T, estimatingMeasurer as _, Circle as a, segmentGraphemes as b, Path as c, Video as d, revealSchedule as f, breakLines as g, resolveAnchor as h, setLayoutEngine as i, invert as j, applyToPoint as k, Rect as l, Node as m, getLayoutEngine as n, Group as o, roundedRectSegs as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, fallbackMeasurer as v, createDisplayListBuilder as w, segmentWords as x, quantize as y };
1539
+ export { FilterValidationError as A, multiply as B, breakLines as C, segmentGraphemes as D, quantize as E, IDENTITY as F, applyToPoint as I, fromTRS as L, filtersToCanvasFilter as M, glow as N, segmentWords as O, validateFilters as P, invert as R, resolveAnchor as S, fallbackMeasurer as T, resolveSketch as _, Circle as a, validateSketch as b, Path as c, Video as d, revealSchedule as f, flatten as g, arcLength as h, setLayoutEngine as i, createDisplayListBuilder as j, setDefaultMeasurer as k, Rect as l, SketchValidationError as m, getLayoutEngine as n, Group as o, roundedRectSegs as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, roughen as v, estimatingMeasurer as w, Node as x, sketchStrokes as y, matEquals as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.5.0-pre.5",
3
+ "version": "0.5.0-pre.6",
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.5.0-pre.5"
23
+ "@glissade/core": "0.5.0-pre.6"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",