@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.
- package/LICENSE +202 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +102 -0
- package/dist/layout.d.ts +46 -0
- package/dist/layout.js +168 -0
- package/dist/layoutEngine.d.ts +395 -0
- package/dist/layoutEngine.js +655 -0
- package/package.json +34 -0
|
@@ -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 };
|