@glissade/scene 0.1.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.
@@ -0,0 +1,395 @@
1
+ import { BindableSignal, ReadonlySignal, Vec2, Vec2Signal } from "@glissade/core";
2
+
3
+ //#region src/matrix.d.ts
4
+
5
+ type Mat2x3 = readonly [number, number, number, number, number, number];
6
+ declare const IDENTITY: Mat2x3;
7
+ declare function multiply(m1: Mat2x3, m2: Mat2x3): Mat2x3;
8
+ /** Compose translate × rotate × scale (rotation in degrees). */
9
+ declare function fromTRS(position: Vec2, rotationDeg: number, scale: Vec2): Mat2x3;
10
+ declare function applyToPoint(m: Mat2x3, p: Vec2): Vec2;
11
+ declare function matEquals(a: Mat2x3, b: Mat2x3): boolean;
12
+ //#endregion
13
+ //#region src/displayList.d.ts
14
+ type ResourceId = number;
15
+ /**
16
+ * Path data as plain segments (JSON-serializable; backends build Path2D/SkPath):
17
+ * M/L: point; C: cubic; Q: quadratic; Z: close
18
+ * E: ellipse arc — cx, cy, rx, ry, rotationRad, startAngleRad, endAngleRad
19
+ */
20
+ type PathSeg = ['M', number, number] | ['L', number, number] | ['C', number, number, number, number, number, number] | ['Q', number, number, number, number] | ['E', number, number, number, number, number, number, number] | ['Z'];
21
+ type Resource = {
22
+ kind: 'path';
23
+ segs: PathSeg[];
24
+ } | {
25
+ kind: 'image';
26
+ assetId: string;
27
+ }
28
+ /** One source-grid video frame: backends resolve via their VideoFrameSource registry (§3.8). */ | {
29
+ kind: 'videoFrame';
30
+ assetId: string;
31
+ mediaT: number;
32
+ };
33
+ type BlendMode = 'source-over' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten';
34
+ /** M1: solid colors. Gradients/patterns are additive later — backends switch on kind. */
35
+ type Paint = {
36
+ kind: 'color';
37
+ color: string;
38
+ };
39
+ interface StrokeStyle {
40
+ width: number;
41
+ cap?: 'butt' | 'round' | 'square';
42
+ join?: 'miter' | 'round' | 'bevel';
43
+ miterLimit?: number;
44
+ dash?: number[];
45
+ dashOffset?: number;
46
+ }
47
+ interface FontSpec {
48
+ family: string;
49
+ size: number;
50
+ weight?: number;
51
+ style?: 'normal' | 'italic';
52
+ }
53
+ /** Enumerated at M2 (§3.4); reserved in the IR now. */
54
+ interface FilterSpec {
55
+ kind: string;
56
+ [k: string]: unknown;
57
+ }
58
+ interface Rect$1 {
59
+ x: number;
60
+ y: number;
61
+ w: number;
62
+ h: number;
63
+ }
64
+ type DrawCommand = {
65
+ op: 'save';
66
+ } | {
67
+ op: 'restore';
68
+ } | {
69
+ op: 'transform';
70
+ m: Mat2x3;
71
+ } | {
72
+ op: 'clip';
73
+ path: ResourceId;
74
+ rule?: 'nonzero' | 'evenodd';
75
+ } | {
76
+ op: 'fillPath';
77
+ path: ResourceId;
78
+ paint: Paint;
79
+ } | {
80
+ op: 'strokePath';
81
+ path: ResourceId;
82
+ paint: Paint;
83
+ stroke: StrokeStyle;
84
+ } | {
85
+ op: 'fillText';
86
+ text: string;
87
+ font: FontSpec;
88
+ paint: Paint;
89
+ x: number;
90
+ y: number;
91
+ align?: 'left' | 'center' | 'right';
92
+ } | {
93
+ op: 'drawImage';
94
+ image: ResourceId;
95
+ src?: Rect$1;
96
+ dst: Rect$1;
97
+ smoothing?: boolean;
98
+ } | {
99
+ op: 'pushGroup';
100
+ opacity: number;
101
+ blend: BlendMode;
102
+ filters: FilterSpec[];
103
+ cacheKey?: string;
104
+ } | {
105
+ op: 'popGroup';
106
+ };
107
+ interface DisplayList {
108
+ commands: DrawCommand[];
109
+ resources: Resource[];
110
+ size: {
111
+ w: number;
112
+ h: number;
113
+ };
114
+ }
115
+ interface DisplayListBuilder {
116
+ push(cmd: DrawCommand): void;
117
+ resource(res: Resource): ResourceId;
118
+ }
119
+ declare function createDisplayListBuilder(size: {
120
+ w: number;
121
+ h: number;
122
+ }): DisplayListBuilder & {
123
+ finish(): DisplayList;
124
+ };
125
+ //#endregion
126
+ //#region src/text.d.ts
127
+ interface TextMetricsLite {
128
+ width: number;
129
+ ascent: number;
130
+ descent: number;
131
+ }
132
+ interface TextMeasurer {
133
+ measureText(text: string, font: FontSpec): TextMetricsLite;
134
+ }
135
+ /** §3.6 measurement quantum. */
136
+ declare function quantize(v: number): number;
137
+ /**
138
+ * Estimating fallback measurer — used only when no backend has been injected
139
+ * (e.g. evaluating for IR-level tests). Deterministic but not metrically
140
+ * faithful; mount(), the CLI, and exporters always inject the real one.
141
+ */
142
+ declare const estimatingMeasurer: TextMeasurer;
143
+ /**
144
+ * Greedy line breaking: explicit '\n' always breaks; otherwise word segments
145
+ * flow until maxWidth is exceeded (Intl.Segmenter boundaries, so CJK wraps
146
+ * without spaces). A segment wider than maxWidth gets its own line (no
147
+ * intra-word breaking in v1).
148
+ */
149
+ declare function breakLines(text: string, font: FontSpec, maxWidth: number | undefined, measurer: TextMeasurer): string[];
150
+ //#endregion
151
+ //#region src/node.d.ts
152
+ interface EvalContext {
153
+ /** The playhead value at evaluate() entry — the only time channel (§3.1). */
154
+ readonly time: number;
155
+ /** Derived: round(time * fps) when the timeline carries an fps advisory; -1 otherwise. */
156
+ readonly frame: number;
157
+ /** Injected by mount()/CLI/exporters (§3.2): the active backend's measurer. */
158
+ readonly measurer: TextMeasurer;
159
+ }
160
+ /** A property initializer: a value, or a computed source (§2.1). */
161
+ type PropInit<T> = T | (() => T);
162
+ interface NodeProps {
163
+ id?: string;
164
+ position?: PropInit<Vec2>;
165
+ rotation?: PropInit<number>;
166
+ scale?: PropInit<Vec2>;
167
+ opacity?: PropInit<number>;
168
+ blend?: PropInit<BlendMode>;
169
+ zIndex?: PropInit<number>;
170
+ }
171
+ interface BindablePropTarget {
172
+ bindSource(fn: () => unknown): void;
173
+ unbindSource(): void;
174
+ }
175
+ declare abstract class Node {
176
+ readonly id: string | undefined;
177
+ readonly position: Vec2Signal;
178
+ readonly rotation: BindableSignal<number>;
179
+ readonly scale: Vec2Signal;
180
+ readonly opacity: BindableSignal<number>;
181
+ readonly blend: BindableSignal<BlendMode>;
182
+ readonly zIndex: BindableSignal<number>;
183
+ readonly filters: BindableSignal<FilterSpec[]>;
184
+ parent: Node | null;
185
+ readonly localMatrix: ReadonlySignal<Mat2x3>;
186
+ readonly worldMatrix: ReadonlySignal<Mat2x3>;
187
+ /** Track-target paths → bindable signals; subclasses register their own props. */
188
+ protected readonly targets: Map<string, BindablePropTarget>;
189
+ constructor(props?: NodeProps);
190
+ protected registerTarget(path: string, sig: BindablePropTarget): void;
191
+ resolveTarget(path: string): BindablePropTarget | undefined;
192
+ /** Subclass drawing: emit own commands (and children for containers). */
193
+ protected abstract draw(out: DisplayListBuilder, ctx: EvalContext): void;
194
+ /**
195
+ * Natural size for flex flow (§3.2); null = not flowable (a Layout parent
196
+ * emits such children absolutely, untouched).
197
+ */
198
+ intrinsicSize(measurer: TextMeasurer): {
199
+ w: number;
200
+ h: number;
201
+ } | null;
202
+ /**
203
+ * Vector from the node origin to its intrinsic box's TOP-LEFT, so Layout
204
+ * can place any anchor correctly. Default: center-anchored (every shape).
205
+ * Text overrides — it draws from a left/center/right baseline origin.
206
+ */
207
+ flowOffset(measurer: TextMeasurer): {
208
+ x: number;
209
+ y: number;
210
+ };
211
+ /** §3.5 predicate: composite-as-a-unit when opacity/blend demand it. */
212
+ protected requiresGroup(): boolean;
213
+ emit(out: DisplayListBuilder, ctx: EvalContext): void;
214
+ }
215
+ //#endregion
216
+ //#region src/nodes.d.ts
217
+ declare class Group extends Node {
218
+ readonly children: Node[];
219
+ constructor(props?: NodeProps & {
220
+ children?: Node[];
221
+ });
222
+ add(child: Node): this;
223
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
224
+ }
225
+ interface ShapeProps extends NodeProps {
226
+ fill?: PropInit<string>;
227
+ stroke?: PropInit<string>;
228
+ strokeWidth?: PropInit<number>;
229
+ }
230
+ declare abstract class Shape extends Node {
231
+ readonly fill: BindableSignal<string>;
232
+ readonly stroke: BindableSignal<string>;
233
+ readonly strokeWidth: BindableSignal<number>;
234
+ constructor(props?: ShapeProps);
235
+ protected abstract pathSegs(): PathSeg[];
236
+ protected draw(out: DisplayListBuilder): void;
237
+ }
238
+ declare class Rect extends Shape {
239
+ readonly width: BindableSignal<number>;
240
+ readonly height: BindableSignal<number>;
241
+ /** Corner radius; clamped to half the smaller dimension. radius = h/2 makes a pill. */
242
+ readonly cornerRadius: BindableSignal<number>;
243
+ constructor(props?: ShapeProps & {
244
+ width?: PropInit<number>;
245
+ height?: PropInit<number>;
246
+ cornerRadius?: PropInit<number>;
247
+ });
248
+ intrinsicSize(): {
249
+ w: number;
250
+ h: number;
251
+ };
252
+ protected pathSegs(): PathSeg[];
253
+ }
254
+ declare class Circle extends Shape {
255
+ readonly radius: BindableSignal<number>;
256
+ constructor(props?: ShapeProps & {
257
+ radius?: PropInit<number>;
258
+ });
259
+ intrinsicSize(): {
260
+ w: number;
261
+ h: number;
262
+ };
263
+ protected pathSegs(): PathSeg[];
264
+ }
265
+ interface ImageProps extends NodeProps {
266
+ /** Asset id from the Timeline manifest (§2.3). */
267
+ assetId: string;
268
+ width?: PropInit<number>;
269
+ height?: PropInit<number>;
270
+ }
271
+ declare class ImageNode extends Node {
272
+ readonly assetId: string;
273
+ readonly width: BindableSignal<number>;
274
+ readonly height: BindableSignal<number>;
275
+ constructor(props: ImageProps);
276
+ intrinsicSize(): {
277
+ w: number;
278
+ h: number;
279
+ };
280
+ protected draw(out: DisplayListBuilder): void;
281
+ }
282
+ interface VideoProps extends NodeProps {
283
+ /** Asset id from the Timeline manifest (kind 'video'). */
284
+ assetId: string;
285
+ /** Timeline second at which the clip starts (§3.8). */
286
+ at?: number;
287
+ /** Seconds into the source where playback begins. */
288
+ trimStart?: number;
289
+ playbackRate?: number;
290
+ /** Clip length on the timeline (seconds); defaults to rest-of-source. */
291
+ clipDuration?: number;
292
+ /**
293
+ * Source frame rate; when set, mediaT is quantized to the source grid in
294
+ * the IR itself (§3.8) so equal-frame times emit identical DisplayLists.
295
+ * Unset: backends quantize at resolve time (pixels identical, IR not).
296
+ */
297
+ sourceFps?: number;
298
+ width?: PropInit<number>;
299
+ height?: PropInit<number>;
300
+ }
301
+ /**
302
+ * Pure given a warmed VideoFrameSource (§3.8): emit() does only the
303
+ * frame-indexed media-time arithmetic — mediaT = trimStart + (t - at) * rate —
304
+ * and references the exact source-grid frame; backends resolve it.
305
+ */
306
+ declare class Video extends Node {
307
+ readonly assetId: string;
308
+ readonly at: number;
309
+ readonly trimStart: number;
310
+ readonly playbackRate: number;
311
+ readonly clipDuration: number | undefined;
312
+ readonly sourceFps: number | undefined;
313
+ readonly width: BindableSignal<number>;
314
+ readonly height: BindableSignal<number>;
315
+ constructor(props: VideoProps);
316
+ /** Frame-indexed media time for timeline time t; null when outside the clip. */
317
+ mediaTime(t: number): number | null;
318
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
319
+ }
320
+ interface TextProps extends NodeProps {
321
+ text?: PropInit<string>;
322
+ fill?: PropInit<string>;
323
+ fontFamily?: string;
324
+ fontSize?: PropInit<number>;
325
+ fontWeight?: number;
326
+ /** Horizontal alignment about the node position; default 'left'. */
327
+ align?: 'left' | 'center' | 'right';
328
+ /** Wrap width in px; unset = no wrapping (explicit \n still breaks). */
329
+ width?: PropInit<number>;
330
+ /** Line height as a multiple of fontSize; default 1.25. */
331
+ lineHeight?: number;
332
+ }
333
+ declare class Text extends Node {
334
+ readonly text: BindableSignal<string>;
335
+ readonly fill: BindableSignal<string>;
336
+ readonly fontSize: BindableSignal<number>;
337
+ readonly fontFamily: string;
338
+ readonly fontWeight: number;
339
+ readonly align: 'left' | 'center' | 'right';
340
+ readonly width: BindableSignal<number>;
341
+ readonly lineHeight: number;
342
+ constructor(props?: TextProps);
343
+ intrinsicSize(measurer: TextMeasurer): {
344
+ w: number;
345
+ h: number;
346
+ };
347
+ /** Text draws from a baseline origin at its align edge, not a center (§3.6). */
348
+ flowOffset(measurer: TextMeasurer): {
349
+ x: number;
350
+ y: number;
351
+ };
352
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
353
+ }
354
+ //#endregion
355
+ //#region src/layoutEngine.d.ts
356
+ /**
357
+ * The LayoutEngine seam (DESIGN.md §3.2): determinism demands the SAME layout
358
+ * engine in browser preview and headless export, so layout never delegates to
359
+ * the platform. Yoga implements this interface in the separately-budgeted
360
+ * '@glissade/scene/layout' entry; the seam keeps Taffy adoptable later and
361
+ * the base embed path free of wasm.
362
+ */
363
+ interface LayoutBox {
364
+ x: number;
365
+ y: number;
366
+ width: number;
367
+ height: number;
368
+ }
369
+ interface LayoutChildSpec {
370
+ width: number;
371
+ height: number;
372
+ grow?: number;
373
+ margin?: number;
374
+ }
375
+ interface LayoutContainerSpec {
376
+ width: number;
377
+ height: number;
378
+ direction: 'row' | 'column';
379
+ gap: number;
380
+ padding: number;
381
+ justify: 'start' | 'center' | 'end' | 'space-between' | 'space-around';
382
+ align: 'start' | 'center' | 'end' | 'stretch';
383
+ }
384
+ interface LayoutEngine {
385
+ /** Pure: child boxes (top-left relative to the container's top-left). */
386
+ compute(container: LayoutContainerSpec, children: LayoutChildSpec[]): LayoutBox[];
387
+ }
388
+ declare class LayoutEngineMissingError extends Error {
389
+ constructor();
390
+ }
391
+ declare function setLayoutEngine(e: LayoutEngine): void;
392
+ declare function getLayoutEngine(): LayoutEngine | null;
393
+ declare function requireLayoutEngine(): LayoutEngine;
394
+ //#endregion
395
+ export { DisplayList as A, StrokeStyle as B, PropInit as C, estimatingMeasurer as D, breakLines as E, Paint as F, fromTRS as G, IDENTITY as H, PathSeg as I, matEquals as K, Rect$1 as L, DrawCommand as M, FilterSpec as N, quantize as O, FontSpec as P, Resource as R, NodeProps as S, TextMetricsLite as T, Mat2x3 as U, createDisplayListBuilder as V, applyToPoint as W, Video as _, LayoutEngineMissingError as a, EvalContext as b, setLayoutEngine as c, ImageNode as d, ImageProps as f, TextProps as g, Text as h, LayoutEngine as i, DisplayListBuilder as j, BlendMode as k, Circle as l, ShapeProps as m, LayoutChildSpec as n, getLayoutEngine as o, Rect as p, multiply as q, LayoutContainerSpec as r, requireLayoutEngine as s, LayoutBox as t, Group as u, VideoProps as v, TextMeasurer as w, Node as x, BindablePropTarget as y, ResourceId as z };