@weasel-js/core 1.4.4 → 1.5.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/CHANGELOG.md +937 -2202
- package/README.md +118 -75
- package/dist/{autoPoseDescriptor-DF1SnnSx.d.ts → autoPoseDescriptor-CvjflWJK.d.ts} +29 -26
- package/dist/{chunk-2VXGHUVL.js → chunk-BDWAA634.js} +4 -22
- package/dist/chunk-BDWAA634.js.map +1 -0
- package/dist/{chunk-R3AWPTLZ.js → chunk-MG7OXCAI.js} +2760 -4491
- package/dist/chunk-MG7OXCAI.js.map +1 -0
- package/dist/{chunk-PRGBGMH3.js → chunk-MQI4PIX3.js} +3 -3
- package/dist/chunk-MQI4PIX3.js.map +1 -0
- package/dist/{chunk-WPM42WJP.js → chunk-UCPV7JXC.js} +201 -256
- package/dist/chunk-UCPV7JXC.js.map +1 -0
- package/dist/clipboard.d.ts +2 -3
- package/dist/clone.d.ts +3 -2
- package/dist/depSchema-nMqj_qTM.d.ts +3490 -0
- package/dist/{grid-0Pbn5B2C.d.ts → grid-BrIa38gG.d.ts} +7 -10
- package/dist/index.d.ts +1996 -1229
- package/dist/index.js +4 -5
- package/dist/insert.d.ts +4 -4
- package/dist/insert.js +1 -1
- package/dist/move.d.ts +5 -6
- package/dist/move.js +3 -6
- package/dist/move.js.map +1 -1
- package/dist/{options-DbYLImvq.d.ts → options-BDyCnrp8.d.ts} +3 -2
- package/dist/poseDescriptor-CGOgIgf8.d.ts +134 -0
- package/dist/renderer.d.ts +10 -4
- package/dist/renderer.js +4 -5
- package/dist/resize.d.ts +10 -12
- package/dist/resize.js +2 -2
- package/dist/routing.d.ts +1 -142
- package/dist/routing.js +1 -1
- package/dist/routing.js.map +1 -1
- package/dist/{types-ei3UMl9R.d.ts → types-DMyo7dnM.d.ts} +12 -41
- package/dist/{types-DEALFt5F.d.ts → types-DtjCJA5r.d.ts} +9 -3
- package/package.json +13 -10
- package/dist/DrawCommand-CD-ug3d9.d.ts +0 -332
- package/dist/builtins-BXFBXegF.d.ts +0 -840
- package/dist/chunk-2VXGHUVL.js.map +0 -1
- package/dist/chunk-BL65SHCX.js +0 -573
- package/dist/chunk-BL65SHCX.js.map +0 -1
- package/dist/chunk-PRGBGMH3.js.map +0 -1
- package/dist/chunk-R3AWPTLZ.js.map +0 -1
- package/dist/chunk-WPM42WJP.js.map +0 -1
- package/dist/geometry-6fCNhAux.d.ts +0 -114
- package/dist/path-JEV2c5If.d.ts +0 -48
- package/dist/registry-BY-wI9gm.d.ts +0 -4003
- package/dist/types-BHK2dkMu.d.ts +0 -172
- package/dist/types-bcc7jcUy.d.ts +0 -594
- package/dist/view-DSQgxBJB.d.ts +0 -63
|
@@ -0,0 +1,3490 @@
|
|
|
1
|
+
import { GradStop, TextureHandle, FillStyle, Stroke } from '@weasel-js/paint';
|
|
2
|
+
import { a as Path, P as PoseDescriptor } from './poseDescriptor-CGOgIgf8.js';
|
|
3
|
+
import { ResolvedRun, TextStyle, TextVerticalAlign } from '@weasel-js/text';
|
|
4
|
+
import * as _weasel_js_history from '@weasel-js/history';
|
|
5
|
+
import { Op, History, SerializedHistory } from '@weasel-js/history';
|
|
6
|
+
import { NodeId, Bounds, View as View$1, SelectionApi, ActionDeps, IngestItem, DragSample, Point2 } from '@weasel-js/routing';
|
|
7
|
+
import { a as SceneAdapter, L as LayoutStrategy, I as InsertAdapter } from './types-DtjCJA5r.js';
|
|
8
|
+
import { MutableRefObject, ReactNode } from 'react';
|
|
9
|
+
import { ActiveToolContextValue } from '@weasel-js/routing/react';
|
|
10
|
+
import { B as BoundsConstraint, P as PointSnapBehavior } from './types-DMyo7dnM.js';
|
|
11
|
+
import { Mat3 as Mat3$1 } from '@weasel-js/geom';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Axis-aligned rectangle pose with optional rotation. The canonical pose
|
|
15
|
+
* shape used by `composeRectPose` and the `unionBounds` helper. Rotation is
|
|
16
|
+
* in radians, pivoted on the unrotated AABB center; absent === 0. Kit-side
|
|
17
|
+
* code that consumes rotation already reads `pose.rotation ?? 0`
|
|
18
|
+
* (`wrapNodeOutput`, `rotate/handle.ts`, `pathInWorld.ts`), so
|
|
19
|
+
* the slot exists on every default scene whether or not the consumer
|
|
20
|
+
* populates it.
|
|
21
|
+
*/
|
|
22
|
+
interface RectPose {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
/** Rotation in radians around the unrotated AABB center. Absent === 0. */
|
|
28
|
+
rotation?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Brand a string as a NodeId. */
|
|
32
|
+
declare const asNodeId: (s: string) => NodeId;
|
|
33
|
+
/**
|
|
34
|
+
* One dependency, as a derivation reads it: the node itself and the pose it is
|
|
35
|
+
* painted at.
|
|
36
|
+
*
|
|
37
|
+
* The node comes along because a connector legitimately reads more than a box
|
|
38
|
+
* — an edge that thickens with its endpoint's weight, or routes only to nodes
|
|
39
|
+
* on a given layer, is answering off `data` and `layer`. The scene already
|
|
40
|
+
* invalidates dependents on `kit:setData` and `kit:setLayer` for exactly that,
|
|
41
|
+
* so handing over only the pose made the invalidation pay for a read nothing
|
|
42
|
+
* could perform.
|
|
43
|
+
*/
|
|
44
|
+
interface DerivedDep<TPose> {
|
|
45
|
+
node: Node<unknown, string, TPose>;
|
|
46
|
+
/** Its override when it has one, else its own derived pose, else the pose
|
|
47
|
+
* the document stores — `effectivePose`, the same answer the renderer uses. */
|
|
48
|
+
pose: TPose;
|
|
49
|
+
/** The path it derives, or `null` when it derives none. Read it to place
|
|
50
|
+
* something *along* a dependency rather than beside it — a label on a
|
|
51
|
+
* routed edge, a tick on a curve.
|
|
52
|
+
*
|
|
53
|
+
* Resolved on first read and memoized on the dependency, so a route costs
|
|
54
|
+
* the same whether one node reads it or five, and a dependency nobody asks
|
|
55
|
+
* about costs nothing. */
|
|
56
|
+
readonly path: Path | null;
|
|
57
|
+
}
|
|
58
|
+
interface NodeBase<TData, TLayer extends string, TPose> {
|
|
59
|
+
id: NodeId;
|
|
60
|
+
layer: TLayer;
|
|
61
|
+
pose: TPose;
|
|
62
|
+
data: TData;
|
|
63
|
+
parent: NodeId | null;
|
|
64
|
+
/** Nodes whose poses this node's geometry is computed from. Fixed at add
|
|
65
|
+
* time. Absent or empty means the node's geometry is authored, which is the
|
|
66
|
+
* normal case.
|
|
67
|
+
*
|
|
68
|
+
* `'children'` means "my own children, in child order" — a container that
|
|
69
|
+
* hugs its contents, which a fixed id list cannot express because
|
|
70
|
+
* reparenting would have to maintain it. The two forms differ in lifetime
|
|
71
|
+
* as well as in membership: deleting a node deletes everything that names
|
|
72
|
+
* it in `dependsOn`, but a container outlives the children it derives
|
|
73
|
+
* from — an emptied group is still a group. */
|
|
74
|
+
dependsOn?: readonly NodeId[] | 'children';
|
|
75
|
+
/** Whether the hit-test walk can return this node. Default `true`; `false`
|
|
76
|
+
* makes it transparent to picking, so a click on it lands on whatever is
|
|
77
|
+
* behind — normally its own container.
|
|
78
|
+
*
|
|
79
|
+
* For content a container owns rather than content the user selects on its
|
|
80
|
+
* own: the label inside a box, a body's rows. Without it the innermost hit
|
|
81
|
+
* wins, and dragging a labeled box moves the label out of the box. It does
|
|
82
|
+
* not hide the node from anything else — it still paints, still clips, and
|
|
83
|
+
* a consumer can still select it by id. */
|
|
84
|
+
pickable?: boolean;
|
|
85
|
+
/** Computes this node's path from its dependencies, in `dependsOn` order —
|
|
86
|
+
* each one the node and the pose it is painted at.
|
|
87
|
+
* A dependency that has been removed arrives as `undefined`.
|
|
88
|
+
* Returning `null` means "nothing to draw right now". Re-evaluated when a
|
|
89
|
+
* dependency's world pose changes, never authored. Absolute-pose `Scene`
|
|
90
|
+
* makes that the dependency's own pose, and an ancestor's move reaches it as
|
|
91
|
+
* a `setPose` of its own from the container cascade.
|
|
92
|
+
* `node` is deliberately widened: naming `TData`/`TLayer` here puts them in
|
|
93
|
+
* a contravariant position, making `Scene` invariant in both and breaking
|
|
94
|
+
* assignment kit-wide. The cost is that a `derivePath` casts to read `node.data`. */
|
|
95
|
+
derivePath?: (node: Node<unknown, string, TPose>, deps: readonly (DerivedDep<TPose> | undefined)[]) => Path | null;
|
|
96
|
+
/** Computes this node's pose from its dependencies' poses, the same way
|
|
97
|
+
* `derivePath` computes its path — same `dependsOn` list, same widened
|
|
98
|
+
* `node`, same registry-keyed serialization.
|
|
99
|
+
*
|
|
100
|
+
* Where a derived path is resolved at paint time and reaches only the
|
|
101
|
+
* painter, a derived pose is what the node *is* at: it feeds bounds,
|
|
102
|
+
* hit-testing, selection chrome, snapping and layout, and every reader
|
|
103
|
+
* gets it through `effectivePose`. A pose override still wins over it —
|
|
104
|
+
* that is what lets a drag preview a node the document says is elsewhere.
|
|
105
|
+
*
|
|
106
|
+
* Returning `null` means "I have nothing to derive from right now", and
|
|
107
|
+
* the node falls back to its authored `pose`. `setPose` on a derived node
|
|
108
|
+
* still writes that authored pose; it is simply not what anything reads. */
|
|
109
|
+
derivePose?: (node: Node<unknown, string, TPose>, deps: readonly (DerivedDep<TPose> | undefined)[]) => TPose | null;
|
|
110
|
+
}
|
|
111
|
+
/** A node with no children — a shape, a label, an image. */
|
|
112
|
+
interface LeafNode<TData, TLayer extends string, TPose = RectPose> extends NodeBase<TData, TLayer, TPose> {
|
|
113
|
+
kind: 'leaf';
|
|
114
|
+
}
|
|
115
|
+
/** A node with an ordered list of children. This is the real group: what
|
|
116
|
+
* Cmd+G creates, what SVG `<g>` round-trips to. A container has its own pose,
|
|
117
|
+
* which its children's poses are relative to, and may optionally clip them. */
|
|
118
|
+
interface ContainerNode<TData, TLayer extends string, TPose = RectPose> extends NodeBase<TData, TLayer, TPose> {
|
|
119
|
+
kind: 'container';
|
|
120
|
+
children: NodeId[];
|
|
121
|
+
/** Optional clip-path source. Re-evaluated each render. Returning `null`
|
|
122
|
+
* means "no clip for this container right now"; an empty / zero-area path
|
|
123
|
+
* means "clip everything out" (children render nowhere). When set, the
|
|
124
|
+
* renderer rasterizes the returned path into the stencil buffer and
|
|
125
|
+
* paints descendants only where it covers. */
|
|
126
|
+
clipFromPose?: (pose: TPose) => Path | null;
|
|
127
|
+
}
|
|
128
|
+
/** A node in the scene tree: either a leaf or a container. Re-exported
|
|
129
|
+
* publicly as `SceneNode`, to avoid colliding with the DOM's `Node`. */
|
|
130
|
+
type Node<TData, TLayer extends string, TPose = RectPose> = LeafNode<TData, TLayer, TPose> | ContainerNode<TData, TLayer, TPose>;
|
|
131
|
+
interface LayerRecordBase<TLayer extends string> {
|
|
132
|
+
id: TLayer;
|
|
133
|
+
visible: boolean;
|
|
134
|
+
locked: boolean;
|
|
135
|
+
}
|
|
136
|
+
/** A layer declared when the scene was created. Fixed set, no display name —
|
|
137
|
+
* these are the kit's own render bands, not something a user manages. */
|
|
138
|
+
interface SystemLayerRecord<TLayer extends string> extends LayerRecordBase<TLayer> {
|
|
139
|
+
kind: 'system';
|
|
140
|
+
}
|
|
141
|
+
/** A layer the user created and can rename, reorder or delete. */
|
|
142
|
+
interface UserLayerRecord<TLayer extends string> extends LayerRecordBase<TLayer> {
|
|
143
|
+
kind: 'user';
|
|
144
|
+
name: string;
|
|
145
|
+
}
|
|
146
|
+
/** Per-layer metadata held by the scene: whether it is visible and locked,
|
|
147
|
+
* and where it sits in the render stack. Distinct from a node's `layer` tag,
|
|
148
|
+
* which merely names one of these. */
|
|
149
|
+
type LayerRecord<TLayer extends string> = SystemLayerRecord<TLayer> | UserLayerRecord<TLayer>;
|
|
150
|
+
/** What `Scene.add` needs to mint a node. Everything except the id is
|
|
151
|
+
* required; the id is generated unless one is supplied. */
|
|
152
|
+
interface AddNodeSpec<TData, TLayer extends string, TPose = RectPose> {
|
|
153
|
+
kind: 'leaf' | 'container';
|
|
154
|
+
layer: TLayer;
|
|
155
|
+
pose: TPose;
|
|
156
|
+
data: TData;
|
|
157
|
+
parent?: NodeId | null;
|
|
158
|
+
index?: number;
|
|
159
|
+
/** Explicit id wins over the Scene's `generateId` and the kit default. */
|
|
160
|
+
id?: NodeId;
|
|
161
|
+
/** Mirrors `SceneNode.pickable`. Omit for the default, which is pickable. */
|
|
162
|
+
pickable?: boolean;
|
|
163
|
+
/** Only meaningful when `kind === 'container'`. Attach a clip-path function
|
|
164
|
+
* to the node; ignored for leaves. Mirrors `ContainerNode.clipFromPose`. */
|
|
165
|
+
clipFromPose?: (pose: TPose) => Path | null;
|
|
166
|
+
/** Mirrors `SceneNode.dependsOn`. */
|
|
167
|
+
dependsOn?: readonly NodeId[] | 'children';
|
|
168
|
+
/** Mirrors `SceneNode.derivePath`. Taken as a live function; its registry key is
|
|
169
|
+
* looked up from it, never passed in. */
|
|
170
|
+
derivePath?: (node: Node<unknown, string, TPose>, deps: readonly (DerivedDep<TPose> | undefined)[]) => Path | null;
|
|
171
|
+
/** Mirrors `SceneNode.derivePose`, on the same terms as `derivePath`. */
|
|
172
|
+
derivePose?: (node: Node<unknown, string, TPose>, deps: readonly (DerivedDep<TPose> | undefined)[]) => TPose | null;
|
|
173
|
+
}
|
|
174
|
+
/** A custom scene mutation registered with `Scene.registerOp`: how to apply
|
|
175
|
+
* it and how to undo it. The pair is what makes it participate in history. */
|
|
176
|
+
interface RegisteredOp<P> {
|
|
177
|
+
apply: (payload: P) => void;
|
|
178
|
+
revert: (payload: P) => void;
|
|
179
|
+
}
|
|
180
|
+
/** One of the layers a scene is created with. */
|
|
181
|
+
interface SystemLayerSpec<TLayer extends string> {
|
|
182
|
+
id: TLayer;
|
|
183
|
+
visible?: boolean;
|
|
184
|
+
locked?: boolean;
|
|
185
|
+
}
|
|
186
|
+
/** Argument to `Scene.addLayer`. Always produces a `UserLayerRecord`
|
|
187
|
+
* (`kind: 'user'`). */
|
|
188
|
+
interface AddLayerSpec<TLayer extends string> {
|
|
189
|
+
id: TLayer;
|
|
190
|
+
name: string;
|
|
191
|
+
/** Default `true`. */
|
|
192
|
+
visible?: boolean;
|
|
193
|
+
/** Default `false`. */
|
|
194
|
+
locked?: boolean;
|
|
195
|
+
/** Render-stack position. Default: top of stack (highest render index). */
|
|
196
|
+
index?: number;
|
|
197
|
+
}
|
|
198
|
+
/** One layer as it appears in a snapshot. A snapshot written before layer
|
|
199
|
+
* kind was serialized carries neither field, and loads as a system layer —
|
|
200
|
+
* which is what every layer in such a snapshot was. */
|
|
201
|
+
interface SerializedLayer<TLayer extends string> extends SystemLayerSpec<TLayer> {
|
|
202
|
+
kind?: 'system' | 'user';
|
|
203
|
+
/** Present on a user layer, absent on a system one. */
|
|
204
|
+
name?: string;
|
|
205
|
+
}
|
|
206
|
+
/** JSON-serializable shape of a Scene's current state. Produced by
|
|
207
|
+
* `scene.toJSON()`; consumed by `sceneFromJSON()`. Function fields
|
|
208
|
+
* (e.g., `clipFromPose`) appear as string keys (`clipFromPoseKey`) and
|
|
209
|
+
* are resolved through `SceneRegistry` at load time. */
|
|
210
|
+
interface SerializedScene<TData, TLayer extends string, TPose> {
|
|
211
|
+
version: 1;
|
|
212
|
+
/** The whole layer stack, system and user alike — the name predates user
|
|
213
|
+
* layers and is kept so existing snapshots still parse. */
|
|
214
|
+
systemLayers: readonly SerializedLayer<TLayer>[];
|
|
215
|
+
nodes: readonly SerializedNode<TData, TLayer, TPose>[];
|
|
216
|
+
}
|
|
217
|
+
/** JSON-serializable shape of a single node. Mirrors `AddNodeSpec` but
|
|
218
|
+
* with function fields replaced by registry keys. */
|
|
219
|
+
interface SerializedNode<TData, TLayer extends string, TPose> {
|
|
220
|
+
id: string;
|
|
221
|
+
kind: 'leaf' | 'container';
|
|
222
|
+
layer: TLayer;
|
|
223
|
+
pose: TPose;
|
|
224
|
+
data: TData;
|
|
225
|
+
/** Parent id; omitted for roots. */
|
|
226
|
+
parent?: string;
|
|
227
|
+
/** Registry key for the container's clip-path factory.
|
|
228
|
+
* Containers only; omitted when the container has no clip. */
|
|
229
|
+
clipFromPoseKey?: string;
|
|
230
|
+
/** Ids this node's geometry derives from, or `'children'`. Omitted when it
|
|
231
|
+
* derives from nothing. */
|
|
232
|
+
dependsOn?: readonly string[] | 'children';
|
|
233
|
+
/** Mirrors `SceneNode.pickable`. Omitted when the node is pickable. */
|
|
234
|
+
pickable?: boolean;
|
|
235
|
+
/** Registry key for the node's `derivePath` function. Omitted when it has none. */
|
|
236
|
+
derivePathKey?: string;
|
|
237
|
+
/** Registry key for the node's `derivePose` function. Omitted when it has none. */
|
|
238
|
+
derivePoseKey?: string;
|
|
239
|
+
}
|
|
240
|
+
/** Per-scene registry mapping string keys to live function references.
|
|
241
|
+
* Passed to `createScene({ ..., registry })` and `sceneFromJSON(json, { registry })`.
|
|
242
|
+
* Each function-field type has its own keyed map. */
|
|
243
|
+
interface SceneRegistry<TPose> {
|
|
244
|
+
/** Maps registry keys to `clipFromPose` factory functions for container nodes. */
|
|
245
|
+
clipFromPose?: Readonly<Record<string, (pose: TPose) => Path | null>>;
|
|
246
|
+
/** Maps registry keys to `derivePath` functions for nodes with `dependsOn`. */
|
|
247
|
+
derivePath?: Readonly<Record<string, (node: Node<unknown, string, TPose>, deps: readonly (DerivedDep<TPose> | undefined)[]) => Path | null>>;
|
|
248
|
+
/** Maps registry keys to `derivePose` functions for nodes with `dependsOn`. */
|
|
249
|
+
derivePose?: Readonly<Record<string, (node: Node<unknown, string, TPose>, deps: readonly (DerivedDep<TPose> | undefined)[]) => TPose | null>>;
|
|
250
|
+
}
|
|
251
|
+
/** Options for `useScene` — the layers the scene has, what it starts out
|
|
252
|
+
* holding, and how its history behaves. */
|
|
253
|
+
interface UseSceneOptions<TData, TLayer extends string, TPose = RectPose> {
|
|
254
|
+
systemLayers: readonly SystemLayerSpec<TLayer>[];
|
|
255
|
+
initial?: readonly AddNodeSpec<TData, TLayer, TPose>[];
|
|
256
|
+
ops?: Readonly<Record<string, RegisteredOp<unknown>>>;
|
|
257
|
+
historyLimit?: number;
|
|
258
|
+
/** Window (ms) within which consecutive same-shaped mutations merge into
|
|
259
|
+
* the previous undo entry (matching per-op coalesce keys — e.g. repeated
|
|
260
|
+
* `setPose` on the same node, or repeated `applyBatch` calls whose ops
|
|
261
|
+
* carry matching `coalesceKey` multisets). `0` (default) disables
|
|
262
|
+
* coalescing: every mutation is a discrete undo entry. Undo of a
|
|
263
|
+
* coalesced entry returns to the state before the first merged mutation;
|
|
264
|
+
* redo restores the latest. `scene.batch` entries never coalesce. */
|
|
265
|
+
coalesceWindowMs?: number;
|
|
266
|
+
generateId?: () => NodeId;
|
|
267
|
+
/** Per-scene registry for non-serializable function fields (clipFromPose, etc.).
|
|
268
|
+
* Required only when serializing/deserializing scenes that use function fields. */
|
|
269
|
+
registry?: SceneRegistry<TPose>;
|
|
270
|
+
/** When supplied, `scene.applyOps(ops, label)` consults this on every call.
|
|
271
|
+
* If it returns a non-null `Journal`, ops are routed to the journal's
|
|
272
|
+
* `applyBatch` instead of recording a new parent-history entry. The journal
|
|
273
|
+
* drives adapter mutation internally (via `op.apply(adapter)`), so the
|
|
274
|
+
* scene state changes as normal; only the history tracking differs.
|
|
275
|
+
*
|
|
276
|
+
* The accessor is called on every `applyOps` invocation so the caller can
|
|
277
|
+
* swap the active journal in and out by updating the closure's reference
|
|
278
|
+
* (e.g., an app's mode machine holds `let activeJournal: Journal | null`
|
|
279
|
+
* and the accessor reads that variable).
|
|
280
|
+
*
|
|
281
|
+
* When the accessor isn't known at scene-construction time (typical for
|
|
282
|
+
* mode machines that depend on `scene.history`), pass nothing here and
|
|
283
|
+
* wire it after construction via `scene.setActiveJournalAccessor(fn)`. */
|
|
284
|
+
getActiveJournal?: () => _weasel_js_history.Journal | null;
|
|
285
|
+
/** Re-render the host on every scene mutation. Default `true`. Set `false`
|
|
286
|
+
* when the scene is read by a frame loop rather than by a render — a game
|
|
287
|
+
* loop, a simulation — and nothing in the host's DOM derives from it.
|
|
288
|
+
* Read by `useScene`; `createScene` ignores it. */
|
|
289
|
+
subscribe?: boolean;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* One node's ephemeral presentation override — what a frame loop wants to say
|
|
293
|
+
* about a node without saying it about the document.
|
|
294
|
+
*
|
|
295
|
+
* Not document content: never recorded in history, never in `toJSON`, and
|
|
296
|
+
* writing one does not bump `Scene.getVersion()`. Hoist one entry per node and
|
|
297
|
+
* mutate it in place on a frame loop; `PoseOverrides.commit()` is what makes a
|
|
298
|
+
* mutation visible.
|
|
299
|
+
*/
|
|
300
|
+
interface PoseOverride<TPose> {
|
|
301
|
+
/** Replaces the node's document pose everywhere the render and hit-test
|
|
302
|
+
* paths read one, including the clip a container derives from its pose, and
|
|
303
|
+
* winning over a `derivePose`. Resolved by `effectivePose` — reading
|
|
304
|
+
* `node.pose` directly is how those paths came to disagree about where a
|
|
305
|
+
* node is. */
|
|
306
|
+
pose?: TPose;
|
|
307
|
+
/** Multiplied into the node's painted alpha, on top of any `alphaFor`. */
|
|
308
|
+
alpha?: number;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The scene's ephemeral per-node overrides — see {@link PoseOverride}.
|
|
312
|
+
*
|
|
313
|
+
* The intended shape of a frame is: `set` each node once, mutate the entries
|
|
314
|
+
* in place per frame, `commit()` once. `commit` is not optional bookkeeping —
|
|
315
|
+
* the painter memo keys on pose *reference*, so a mutation without a commit
|
|
316
|
+
* paints the previous frame with no error.
|
|
317
|
+
*
|
|
318
|
+
* To promote a frame to document state (dropping a drag, baking an animation),
|
|
319
|
+
* write it once through `Scene.setPose` and `clear` the override.
|
|
320
|
+
*/
|
|
321
|
+
interface PoseOverrides<TPose> {
|
|
322
|
+
/** Store `entry` for `id` **by reference**; the caller keeps mutating it. */
|
|
323
|
+
set(id: NodeId, entry: PoseOverride<TPose>): void;
|
|
324
|
+
get(id: NodeId): PoseOverride<TPose> | undefined;
|
|
325
|
+
has(id: NodeId): boolean;
|
|
326
|
+
/** The overridden ids, as a snapshot array. */
|
|
327
|
+
ids(): readonly NodeId[];
|
|
328
|
+
clear(id: NodeId): void;
|
|
329
|
+
clearAll(): void;
|
|
330
|
+
/** Publish this frame's in-place mutations: invalidate the painter memo for
|
|
331
|
+
* every overridden node, then notify subscribers. */
|
|
332
|
+
commit(): void;
|
|
333
|
+
/** Notified after every write. The canvas uses this to repaint without a
|
|
334
|
+
* scene version bump. */
|
|
335
|
+
subscribe(fn: () => void): () => void;
|
|
336
|
+
/** Monotonic write counter. A snapshot for observers that poll. */
|
|
337
|
+
getGeneration(): number;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The kit-owned scene tree: nodes, layers, and the undo history over both.
|
|
341
|
+
*
|
|
342
|
+
* A scene is logical, not visual — it says what exists and where, and nothing
|
|
343
|
+
* about how it is painted. Every mutating method is undoable, and reads are
|
|
344
|
+
* snapshots rather than live views. Nodes are addressed by `NodeId`; hold ids
|
|
345
|
+
* across mutations, not node objects.
|
|
346
|
+
*
|
|
347
|
+
* Three type parameters keep it domain-agnostic: `TData` is the app's payload,
|
|
348
|
+
* which the kit never inspects; `TPose` is the transform shape, `RectPose` by
|
|
349
|
+
* default; `TLayer` is the union of layer names.
|
|
350
|
+
*/
|
|
351
|
+
interface Scene<TData, TLayer extends string, TPose = RectPose> {
|
|
352
|
+
readonly nodes: ReadonlyMap<NodeId, Node<TData, TLayer, TPose>>;
|
|
353
|
+
readonly roots: readonly NodeId[];
|
|
354
|
+
readonly layers: readonly LayerRecord<TLayer>[];
|
|
355
|
+
/** The registry this scene resolves node functions against — the consumer's
|
|
356
|
+
* entries over the kit's. */
|
|
357
|
+
readonly registry: SceneRegistry<TPose>;
|
|
358
|
+
get(id: NodeId): Node<TData, TLayer, TPose> | undefined;
|
|
359
|
+
childrenOf(id: NodeId): readonly NodeId[];
|
|
360
|
+
ancestorsOf(id: NodeId): readonly NodeId[];
|
|
361
|
+
renderOrder(): Iterable<NodeId>;
|
|
362
|
+
/** The same layer-major sequence as {@link Scene.renderOrder}, as the nodes
|
|
363
|
+
* themselves. Prefer this wherever the ids are only going to be resolved
|
|
364
|
+
* back to nodes: the traversal already holds them, and re-looking each one
|
|
365
|
+
* up was ~40% of the area hit-test's per-node cost. Cached until a
|
|
366
|
+
* structural edit, so repeat calls hand back the same array — a snapshot,
|
|
367
|
+
* not a live view, and not yours to mutate. */
|
|
368
|
+
renderOrderNodes(): readonly Node<TData, TLayer, TPose>[];
|
|
369
|
+
add(spec: AddNodeSpec<TData, TLayer, TPose>): NodeId;
|
|
370
|
+
/** Delete `id`, its **entire subtree**, and **everything that derives from**
|
|
371
|
+
* any of those nodes — a node listing one of them in `dependsOn` goes too,
|
|
372
|
+
* along with its own subtree, transitively. A dependent can live anywhere in
|
|
373
|
+
* the tree, so this deletes nodes the caller never named and may unlink
|
|
374
|
+
* several disjoint subtrees at once. Recorded as one undoable step; `undo()`
|
|
375
|
+
* restores every one of them where it was, child order intact. */
|
|
376
|
+
remove(id: NodeId): void;
|
|
377
|
+
/** {@link remove} over several roots at once, as a **single** undoable step.
|
|
378
|
+
* Ids resolve against the tree as it stands at the call, so an id that
|
|
379
|
+
* another one would cascade away is absorbed rather than removed twice —
|
|
380
|
+
* which is what makes it safe to pass a whole selection. Throws if any id is
|
|
381
|
+
* not in the scene; an empty list does nothing and records no step. */
|
|
382
|
+
removeMany(ids: readonly NodeId[]): void;
|
|
383
|
+
/** Every node {@link removeMany} would take if given `ids`: each id, its
|
|
384
|
+
* subtree, everything deriving from any of those, and those nodes' subtrees
|
|
385
|
+
* in turn. The authoritative answer — the cascade relations live here, and
|
|
386
|
+
* a caller reconstructing them from `dependsOn` and `children` drifts the
|
|
387
|
+
* moment a third one is added. Ids not in the scene come back unchanged.
|
|
388
|
+
*
|
|
389
|
+
* A set, not a restore order: a dependent can be reached before the parent
|
|
390
|
+
* it sits under, which is also in the closure. */
|
|
391
|
+
removalClosure(ids: readonly NodeId[]): readonly NodeId[];
|
|
392
|
+
update(id: NodeId, patch: {
|
|
393
|
+
data: TData;
|
|
394
|
+
}): void;
|
|
395
|
+
setPose(id: NodeId, pose: TPose): void;
|
|
396
|
+
/** Retag `id` to `layer`. On a **container this cascades**: every descendant
|
|
397
|
+
* is moved to the same layer, recorded as a **single** undo step.
|
|
398
|
+
*
|
|
399
|
+
* Invariants:
|
|
400
|
+
* - **Layer floor** — a child may not render below its parent, so retagging
|
|
401
|
+
* to a layer *below* the node's parent throws. Retagging to the parent's
|
|
402
|
+
* layer or any higher one is allowed; a node with no parent is
|
|
403
|
+
* unconstrained.
|
|
404
|
+
* - **No-op elision** — setting the layer a node already has does nothing
|
|
405
|
+
* and pushes **no** history entry. */
|
|
406
|
+
setLayer(id: NodeId, layer: TLayer): void;
|
|
407
|
+
/** Point `id` at a different set of dependencies — retargeting an edge's end
|
|
408
|
+
* onto another node, or switching a container between an id list and
|
|
409
|
+
* `'children'`. Recorded as one undoable step.
|
|
410
|
+
*
|
|
411
|
+
* Order is significant: a derivation reads its dependencies positionally,
|
|
412
|
+
* so `[b, a]` is not `[a, b]`. Ids need not be in the scene — a declared
|
|
413
|
+
* dependency that appears later invalidates the node when it does, the same
|
|
414
|
+
* as one declared at `add`. Passing `undefined` drops the declaration, after
|
|
415
|
+
* which nothing cascades the node away.
|
|
416
|
+
*
|
|
417
|
+
* **No-op elision** — declaring what the node already declares does nothing
|
|
418
|
+
* and pushes no history entry. */
|
|
419
|
+
setDependsOn(id: NodeId, dependsOn: readonly NodeId[] | 'children' | undefined): void;
|
|
420
|
+
/** Reparent `id` under `parent` (or to a root when `parent` is `null`) at
|
|
421
|
+
* `index` within the new sibling list, appending when `index` is omitted.
|
|
422
|
+
* Siblings are reindexed. Recorded as one undoable step.
|
|
423
|
+
*
|
|
424
|
+
* Rejected (throws) when:
|
|
425
|
+
* - `parent` exists but is a **leaf**, not a container;
|
|
426
|
+
* - the move would form a **cycle** — `parent` is `id` itself or one of
|
|
427
|
+
* `id`'s own descendants;
|
|
428
|
+
* - it would drop `id` **below its new parent's layer** (child may not
|
|
429
|
+
* render below its parent).
|
|
430
|
+
*
|
|
431
|
+
* `move(id, null)` — detaching to a root — is always allowed regardless of
|
|
432
|
+
* layer, since a root has no parent to render beneath. */
|
|
433
|
+
move(id: NodeId, parent: NodeId | null, index?: number): void;
|
|
434
|
+
/** Shift `id` to `index` within its **current** parent's child list. Unlike
|
|
435
|
+
* {@link move}, the parent never changes — only sibling order. */
|
|
436
|
+
reorder(id: NodeId, index: number): void;
|
|
437
|
+
setLayerVisible(layer: TLayer, visible: boolean): void;
|
|
438
|
+
/** Lock or unlock a layer. A locked layer still paints, but its nodes are
|
|
439
|
+
* out of reach: picking and area selection pass over them, the selection
|
|
440
|
+
* drops them, and every node mutation on them throws. Locking is never
|
|
441
|
+
* blocked by the lock, and neither are the other layer operations. */
|
|
442
|
+
setLayerLocked(layer: TLayer, locked: boolean): void;
|
|
443
|
+
/** Whether `id` sits on a locked layer or under a container that does.
|
|
444
|
+
* A container's lock covers its whole subtree, whatever layers the
|
|
445
|
+
* descendants are tagged to. An id not in the scene is not locked. */
|
|
446
|
+
isLocked(id: NodeId): boolean;
|
|
447
|
+
/** Run `fn` with the lock guard lifted, for a programmatic edit that has to
|
|
448
|
+
* reach a locked node. Undo and redo never need it. */
|
|
449
|
+
unlocked<T>(fn: () => T): T;
|
|
450
|
+
addLayer(spec: AddLayerSpec<TLayer>): void;
|
|
451
|
+
/** Drop a user layer and every node tagged to it, as one undoable step.
|
|
452
|
+
* Removal cascades, so this also deletes nodes **on other layers** that
|
|
453
|
+
* derive from a node on this one. */
|
|
454
|
+
removeLayer(layer: TLayer): void;
|
|
455
|
+
renameLayer(layer: TLayer, name: string): void;
|
|
456
|
+
moveLayer(layer: TLayer, index: number): void;
|
|
457
|
+
registerOp<P>(kind: string, handler: RegisteredOp<P>): void;
|
|
458
|
+
recordOp<P>(op: {
|
|
459
|
+
kind: string;
|
|
460
|
+
payload: P;
|
|
461
|
+
}): void;
|
|
462
|
+
/** Install (or clear) the active-journal accessor after scene construction.
|
|
463
|
+
* Useful when the journal source (typically a mode machine) is built
|
|
464
|
+
* with `scene.history` as a dependency — a chicken-and-egg situation
|
|
465
|
+
* where the accessor can't be passed in via `UseSceneOptions`.
|
|
466
|
+
*
|
|
467
|
+
* Pass `null` to detach. Overrides any `getActiveJournal` set in
|
|
468
|
+
* `UseSceneOptions`. */
|
|
469
|
+
setActiveJournalAccessor(fn: (() => _weasel_js_history.Journal | null) | null): void;
|
|
470
|
+
/** Apply a batch of ops with journal-aware routing.
|
|
471
|
+
*
|
|
472
|
+
* - **Without active journal** (or no `getActiveJournal` in options):
|
|
473
|
+
* the ops themselves are recorded as one undo entry on the scene's own
|
|
474
|
+
* history, rebound to `adapter` — undo replays each op's `invert()`
|
|
475
|
+
* against that same adapter. Consecutive `applyBatch` entries can
|
|
476
|
+
* coalesce via matching op `coalesceKey`s when the scene opts into
|
|
477
|
+
* `coalesceWindowMs`.
|
|
478
|
+
* - **With active journal**: routes ops to `journal.applyBatch(ops, label)`.
|
|
479
|
+
* The scene's history recording is suppressed for the duration so the
|
|
480
|
+
* journal's inner history — not the scene's undo stack — tracks the batch.
|
|
481
|
+
* Mutations still happen on `adapter` / scene state.
|
|
482
|
+
*
|
|
483
|
+
* `adapter` must be the same adapter the ops expect (typically a
|
|
484
|
+
* `SceneCanvasAdapter`). Pass `this` from `sceneToAdapter` or a compatible
|
|
485
|
+
* adapter. */
|
|
486
|
+
applyBatch(ops: Op[], label: string, adapter: unknown): void;
|
|
487
|
+
/** The transient set of active ids — "operate on these N as a unit".
|
|
488
|
+
* Shared by every view over this scene unless a view supplies its own
|
|
489
|
+
* (see `CanvasView.selection`). Not document content: it never appears
|
|
490
|
+
* in `toJSON`. It does ride on history entries, so undo and redo put
|
|
491
|
+
* back the selection an edit was made under; changing it is never an
|
|
492
|
+
* undo step of its own. */
|
|
493
|
+
getSelection(): readonly NodeId[];
|
|
494
|
+
setSelection(ids: readonly NodeId[]): void;
|
|
495
|
+
/** Per-node pose / alpha overrides the render and hit-test paths read
|
|
496
|
+
* through. Like {@link Scene.getSelection} this is not document content:
|
|
497
|
+
* writes are never recorded, never serialized, and do not bump
|
|
498
|
+
* {@link Scene.getVersion}. See {@link PoseOverrides}. */
|
|
499
|
+
readonly overrides: PoseOverrides<TPose>;
|
|
500
|
+
/** The scene's undo/redo engine, in the shape `@weasel-js/history` defines —
|
|
501
|
+
* what the kit's `undo` / `redo` actions resolve their `history` dep to, and
|
|
502
|
+
* what a mode machine opens journals against. Mutating members route through
|
|
503
|
+
* the scene's own wrappers, so an undo driven through this bumps
|
|
504
|
+
* {@link Scene.getVersion} and notifies subscribers exactly as
|
|
505
|
+
* {@link Scene.undo} does; `getVersion` / `subscribe` on it report the
|
|
506
|
+
* engine's history version, not the scene's. */
|
|
507
|
+
readonly history: History;
|
|
508
|
+
undo(): boolean;
|
|
509
|
+
redo(): boolean;
|
|
510
|
+
canUndo(): boolean;
|
|
511
|
+
canRedo(): boolean;
|
|
512
|
+
batch<T>(label: string, fn: () => T): T;
|
|
513
|
+
/** Read-only snapshot of every history entry currently reachable from
|
|
514
|
+
* the present state. Oldest applied first, then redoable entries in
|
|
515
|
+
* the order they'd be re-applied. Each entry id is stable. */
|
|
516
|
+
historyEntries(): readonly {
|
|
517
|
+
id: string;
|
|
518
|
+
label: string;
|
|
519
|
+
}[];
|
|
520
|
+
/** Index of the "current state". Equals the count of applied entries;
|
|
521
|
+
* `0` means "nothing applied" (initial). */
|
|
522
|
+
historyIndex(): number;
|
|
523
|
+
/** Jump to the given history index by calling undo/redo repeatedly.
|
|
524
|
+
* Clamps to [0, total]. Returns true if any movement occurred. */
|
|
525
|
+
jumpToHistoryIndex(index: number): boolean;
|
|
526
|
+
/** Snapshot the undo/redo history in a JSON-serializable form (the
|
|
527
|
+
* engine's `SerializedHistory`). Entries containing any nameless op are
|
|
528
|
+
* dropped (hand-rolled anonymous ops passed to `applyBatch`); kit and
|
|
529
|
+
* consumer-registered ops always carry names. Payload JSON-safety
|
|
530
|
+
* (e.g. typed arrays inside poses) is the caller's concern. Do not call
|
|
531
|
+
* mid-`batch` — the open batch's ops are not yet recorded. */
|
|
532
|
+
serializeHistory(): SerializedHistory;
|
|
533
|
+
/** Replace the undo/redo history from a `serializeHistory()` snapshot.
|
|
534
|
+
* Call on a scene whose node/layer state already matches the snapshot's
|
|
535
|
+
* head state (i.e. right after `loadState` from the paired scene
|
|
536
|
+
* snapshot); node state is NOT mutated. Ops re-registered via
|
|
537
|
+
* `registerOp` before this call round-trip; unknown kinds become no-op
|
|
538
|
+
* placeholders; external ops rebuild via the global op-factory registry
|
|
539
|
+
* and replay against the `setHistoryAdapter` accessor. Restored entries
|
|
540
|
+
* never coalesce with new ones. Notifies once. Do not call mid-`batch`:
|
|
541
|
+
* the stacks are replaced underneath the open batch, whose eventual
|
|
542
|
+
* flush would graft onto (and evict against) the restored stacks. */
|
|
543
|
+
restoreHistory(snapshot: SerializedHistory): void;
|
|
544
|
+
/** Install (or clear with `null`) the accessor for the adapter that
|
|
545
|
+
* RESTORED external ops (recorded via `applyBatch`, rebuilt from a
|
|
546
|
+
* `restoreHistory` snapshot) apply against on undo/redo. Resolved lazily
|
|
547
|
+
* at each apply, so wiring order relative to `restoreHistory` doesn't
|
|
548
|
+
* matter. Live `applyBatch` entries are unaffected (they bind their
|
|
549
|
+
* call-site adapter). If unset when a restored op applies, the op is a
|
|
550
|
+
* debug-warned no-op. */
|
|
551
|
+
setHistoryAdapter(fn: (() => unknown) | null): void;
|
|
552
|
+
/** Snapshot the current scene state to a JSON-serializable shape.
|
|
553
|
+
* History (undo/redo stacks) is NOT captured. Function fields like
|
|
554
|
+
* `ContainerNode.clipFromPose` are translated to string keys via the
|
|
555
|
+
* scene's registry; throws if any function field has no matching key. */
|
|
556
|
+
toJSON(): SerializedScene<TData, TLayer, TPose>;
|
|
557
|
+
/** Replace this scene's entire node + layer state in place from a snapshot
|
|
558
|
+
* produced by `toJSON()`. Unlike `sceneFromJSON`, the existing Scene
|
|
559
|
+
* instance is preserved — holders such as `<SceneCanvas>` keep their
|
|
560
|
+
* reference. History (undo/redo) is cleared, matching `sceneFromJSON`.
|
|
561
|
+
* Bumps `getVersion()` and notifies subscribers exactly once.
|
|
562
|
+
*
|
|
563
|
+
* Throws on an unsupported version or unknown registry/layer ids; on a
|
|
564
|
+
* malformed snapshot the scene is left empty or partially populated (callers should treat a
|
|
565
|
+
* `loadState` throw as fatal and reload). Snapshots from `toJSON()` are
|
|
566
|
+
* always well-formed. */
|
|
567
|
+
loadState(json: SerializedScene<TData, TLayer, TPose>): void;
|
|
568
|
+
subscribe(listener: () => void): () => void;
|
|
569
|
+
/** Monotonically increasing version. Snapshot for `useSyncExternalStore`. */
|
|
570
|
+
getVersion(): number;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Pixel dimensions of the canvas viewport. */
|
|
574
|
+
interface ViewportDims {
|
|
575
|
+
width: number;
|
|
576
|
+
height: number;
|
|
577
|
+
}
|
|
578
|
+
/** Options for {@link fitViewToBounds}. */
|
|
579
|
+
interface FitViewToBoundsOptions {
|
|
580
|
+
/** Inset (in CSS px) around the bounds. Default 16. */
|
|
581
|
+
padding?: number;
|
|
582
|
+
/**
|
|
583
|
+
* Override the kit's system max scale. Defaults to the kit-wide max zoom
|
|
584
|
+
* used by `computeWheelAction` (`DEFAULT_MAX_ZOOM`).
|
|
585
|
+
*/
|
|
586
|
+
maxScale?: number;
|
|
587
|
+
/**
|
|
588
|
+
* Override the kit's system min scale. Defaults to the kit-wide min zoom
|
|
589
|
+
* used by `computeWheelAction` (`DEFAULT_MIN_ZOOM`).
|
|
590
|
+
*/
|
|
591
|
+
minScale?: number;
|
|
592
|
+
/**
|
|
593
|
+
* Fit mode:
|
|
594
|
+
* - `'contain'` (default): uniform scale = `min(availW/w, availH/h)`. Bounds fit entirely; one axis has letterboxing.
|
|
595
|
+
* - `'fill'`: uniform scale = `max(...)`. Bounds overflow viewport on one axis.
|
|
596
|
+
* - `'stretch'`: per-axis scale. Bounds match viewport exactly; scale is non-uniform.
|
|
597
|
+
*/
|
|
598
|
+
mode?: 'contain' | 'fill' | 'stretch';
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Compute the target {@link View} that fits `bounds` (world space) inside
|
|
602
|
+
* `viewportDims` (CSS px). The bounds are centered in the viewport with
|
|
603
|
+
* uniform padding on all sides; the resulting scale is clamped to the
|
|
604
|
+
* configured min/max.
|
|
605
|
+
*
|
|
606
|
+
* View semantics: `view.x` / `view.y` is the world point currently drawn at
|
|
607
|
+
* the canvas top-left, and `view.scale.{x,y}` is pixels per world unit on
|
|
608
|
+
* each axis (see `view.ts`). So:
|
|
609
|
+
* screenX = (worldX - view.x) * view.scale.x
|
|
610
|
+
* screenY = (worldY - view.y) * view.scale.y
|
|
611
|
+
*
|
|
612
|
+
* Empty / zero-sized bounds or viewports cannot be fit — in that case this
|
|
613
|
+
* returns `currentView` unchanged and logs a `console.warn`. Callers that
|
|
614
|
+
* want to no-op silently can detect zero area themselves before calling.
|
|
615
|
+
*/
|
|
616
|
+
declare function fitViewToBounds(bounds: Bounds, viewportDims: ViewportDims, currentView: View$1, opts?: FitViewToBoundsOptions): View$1;
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Minimal compile/link/lookup wrapper for a GL program. Throws
|
|
620
|
+
* `ShaderCompileError` on compile or link failure with the GL info log
|
|
621
|
+
* embedded in the error message — never returns a half-initialized program.
|
|
622
|
+
*
|
|
623
|
+
* Designed so test code can stub `getShaderParameter` / `getProgramParameter`
|
|
624
|
+
* via the recorder Proxy without further special-casing.
|
|
625
|
+
*/
|
|
626
|
+
type Stage = 'vertex' | 'fragment' | 'link';
|
|
627
|
+
/** Thrown when a shader fails to compile or link, carrying which stage failed
|
|
628
|
+
* and the driver's log. */
|
|
629
|
+
declare class ShaderCompileError extends Error {
|
|
630
|
+
readonly stage: Stage;
|
|
631
|
+
readonly log: string;
|
|
632
|
+
constructor(stage: Stage, log: string);
|
|
633
|
+
}
|
|
634
|
+
declare class ShaderProgram {
|
|
635
|
+
private readonly gl;
|
|
636
|
+
readonly handle: WebGLProgram;
|
|
637
|
+
private readonly uniforms;
|
|
638
|
+
private readonly attributes;
|
|
639
|
+
constructor(gl: WebGL2RenderingContext, vertSrc: string, fragSrc: string);
|
|
640
|
+
private compile;
|
|
641
|
+
lookupUniforms(names: readonly string[]): void;
|
|
642
|
+
lookupAttributes(names: readonly string[]): void;
|
|
643
|
+
uniform(name: string): WebGLUniformLocation | undefined;
|
|
644
|
+
attribute(name: string): number | undefined;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* A tessellated representation of a Path, ready to upload to GL.
|
|
649
|
+
*
|
|
650
|
+
* - `vertices` is interleaved x,y in path-local coordinates (`Float32Array`
|
|
651
|
+
* of length `2 * vertexCount`).
|
|
652
|
+
* - `indices` are triangle indices into `vertices` (`Uint32Array`, length
|
|
653
|
+
* `3 * triangleCount`).
|
|
654
|
+
* - `requiresStencil` is set for paths whose fillRule is `'evenodd'` and
|
|
655
|
+
* whose triangulation is a *naive* per-contour fan rather than a clean
|
|
656
|
+
* inside/outside triangulation. The renderer must use a stencil
|
|
657
|
+
* two-pass when this flag is true. Single-contour paths and `'nonzero'`
|
|
658
|
+
* multi-contour paths leave it false.
|
|
659
|
+
* - `anchorA` / `anchorB` / `anchorT` parameterize each mesh vertex by
|
|
660
|
+
* the two consecutive path anchors it lies between and the arc-length
|
|
661
|
+
* fraction along that segment (0 = at A, 1 = at B). Vertices that fall
|
|
662
|
+
* exactly on an anchor set A === B and t = 0. Used at draw time when
|
|
663
|
+
* the DrawCommand supplies per-anchor colors so the renderer can lerp
|
|
664
|
+
* per mesh vertex. Optional — emitted by the path tessellators; absent
|
|
665
|
+
* on meshes built by other paths (e.g. text glyphs).
|
|
666
|
+
*/
|
|
667
|
+
interface Mesh {
|
|
668
|
+
readonly vertices: Float32Array;
|
|
669
|
+
readonly indices: Uint32Array;
|
|
670
|
+
readonly requiresStencil?: boolean;
|
|
671
|
+
readonly anchorA?: Uint32Array;
|
|
672
|
+
readonly anchorB?: Uint32Array;
|
|
673
|
+
readonly anchorT?: Float32Array;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
interface GLMeshHandle {
|
|
677
|
+
readonly vao: WebGLVertexArrayObject;
|
|
678
|
+
readonly indexCount: number;
|
|
679
|
+
readonly requiresStencil: boolean;
|
|
680
|
+
readonly anchorA?: Uint32Array;
|
|
681
|
+
readonly anchorB?: Uint32Array;
|
|
682
|
+
readonly anchorT?: Float32Array;
|
|
683
|
+
}
|
|
684
|
+
/** Resources to release when a Mesh is reclaimed by GC. */
|
|
685
|
+
interface MeshResources {
|
|
686
|
+
vao: WebGLVertexArrayObject;
|
|
687
|
+
vbo: WebGLBuffer;
|
|
688
|
+
ibo: WebGLBuffer;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Caches GL-side buffers + VAO per `Mesh` identity. Upload happens lazily
|
|
692
|
+
* on first `handleFor(mesh)` call.
|
|
693
|
+
*
|
|
694
|
+
* **GC-aware cleanup (deferred-delete queue):** when a Mesh becomes
|
|
695
|
+
* unreachable, the WeakMap entry is dropped automatically — but the
|
|
696
|
+
* underlying GL resources (VAO, VBO, IBO) would leak forever without an
|
|
697
|
+
* explicit `gl.delete*` call. We register each Mesh with a
|
|
698
|
+
* `FinalizationRegistry`; when the Mesh is reclaimed, the finalizer pushes
|
|
699
|
+
* the resources onto an internal queue. The renderer drains this queue at
|
|
700
|
+
* the start of each `render()` call (via `drainPendingDeletes()`), when GL
|
|
701
|
+
* is in a known state — no VAO bound, no draw mid-flight. Deleting from the
|
|
702
|
+
* finalizer directly was racy (it could fire between two `gl.bindVertexArray`
|
|
703
|
+
* calls inside `dispatch()`, sometimes corrupting the next draw); deferring
|
|
704
|
+
* the actual delete to a known-safe point keeps the use-after-free risk to
|
|
705
|
+
* zero.
|
|
706
|
+
*
|
|
707
|
+
* Caveats:
|
|
708
|
+
* - Finalizer timing is non-deterministic; resources may live for a frame
|
|
709
|
+
* or two beyond their Mesh, but they will be reclaimed.
|
|
710
|
+
* - If the GL context is lost between finalizer and drain, the drain
|
|
711
|
+
* no-ops safely (`gl.isContextLost()` makes deletes no-ops, and
|
|
712
|
+
* `gl.deleteBuffer(null)` is allowed).
|
|
713
|
+
*
|
|
714
|
+
* The cache is GL-context-bound; if the context is lost and re-created, the
|
|
715
|
+
* renderer should construct a new GLMeshCache. (Context loss handling lives
|
|
716
|
+
* in `WeaselRenderer`.)
|
|
717
|
+
*/
|
|
718
|
+
declare class GLMeshCache {
|
|
719
|
+
private readonly gl;
|
|
720
|
+
private readonly aPositionLoc;
|
|
721
|
+
private readonly map;
|
|
722
|
+
/** Meshes this context has drawn at least once; see `uploadRecurring`. */
|
|
723
|
+
private readonly seen;
|
|
724
|
+
private readonly finalizer;
|
|
725
|
+
private readonly pendingDeletes;
|
|
726
|
+
/** Transient resources allocated this frame; freed at end of render(). */
|
|
727
|
+
private readonly transientThisFrame;
|
|
728
|
+
constructor(gl: WebGL2RenderingContext, aPositionLoc: number);
|
|
729
|
+
handleFor(mesh: Mesh): GLMeshHandle;
|
|
730
|
+
/**
|
|
731
|
+
* Upload a Mesh that the caller knows is single-use (e.g. the per-frame
|
|
732
|
+
* stroke ribbon from `tessellateStroke`). Returns a handle backed by fresh
|
|
733
|
+
* GL resources that the renderer will free deterministically at the end
|
|
734
|
+
* of the current frame via `freeTransient()`. Bypasses the WeakMap cache
|
|
735
|
+
* and the FinalizationRegistry, so transient meshes never wait on GC.
|
|
736
|
+
*/
|
|
737
|
+
uploadTransient(mesh: Mesh): GLMeshHandle;
|
|
738
|
+
/**
|
|
739
|
+
* Upload a Mesh that may or may not recur across frames. Its first sight in
|
|
740
|
+
* *this* context takes `uploadTransient`; every later one takes the
|
|
741
|
+
* persistent handle. One transient upload to find out is cheaper than
|
|
742
|
+
* stranding a persistent VAO on a mesh that never returns, whose release
|
|
743
|
+
* would wait on GC. The transient upload does not populate the persistent
|
|
744
|
+
* map, so steady-state reuse begins on the third frame.
|
|
745
|
+
*/
|
|
746
|
+
uploadRecurring(mesh: Mesh): GLMeshHandle;
|
|
747
|
+
/**
|
|
748
|
+
* Free all transient resources allocated since the last call. Called by
|
|
749
|
+
* the renderer at the end of each `render()`. Safe under context loss.
|
|
750
|
+
*/
|
|
751
|
+
freeTransient(): void;
|
|
752
|
+
/** @internal — for tests asserting the transient-list size. */
|
|
753
|
+
_transientCount(): number;
|
|
754
|
+
/**
|
|
755
|
+
* Free GL resources whose Mesh has been GC'd since the last drain.
|
|
756
|
+
* Called by the renderer at the top of each `render()`, before any draws.
|
|
757
|
+
*/
|
|
758
|
+
drainPendingDeletes(): void;
|
|
759
|
+
/** @internal — for tests asserting the queue size. */
|
|
760
|
+
_pendingDeleteCount(): number;
|
|
761
|
+
/** @internal — for tests that need to simulate the finalizer firing. */
|
|
762
|
+
_enqueueDeleteForTest(resources: MeshResources): void;
|
|
763
|
+
private upload;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* GL texture upload + cache for MSDF font atlases (and future images).
|
|
768
|
+
*
|
|
769
|
+
* Textures are keyed by a string id (the font family name or image id).
|
|
770
|
+
* Pixels are uploaded once per id; a later upload of the same id reuses the
|
|
771
|
+
* texture and only re-applies its wrap mode when that changed.
|
|
772
|
+
* The cache is GL-context-bound; discard and create a new one on context loss.
|
|
773
|
+
*
|
|
774
|
+
* Atlas format: RGBA UNSIGNED_BYTE, linear filtering, no mipmaps.
|
|
775
|
+
* Mipmap generation is deliberately skipped — MSDF works correctly with
|
|
776
|
+
* linear filtering, and mipmap resampling corrupts the multi-channel SDF signal.
|
|
777
|
+
*/
|
|
778
|
+
type TexSource$1 = HTMLImageElement | ImageBitmap | ImageData | HTMLCanvasElement;
|
|
779
|
+
/** How a texture samples outside `0..1`. Pattern tiles need `'repeat'`;
|
|
780
|
+
* everything else clamps. WebGL2 allows `REPEAT` on NPOT textures, so a
|
|
781
|
+
* tile of any size tiles correctly. */
|
|
782
|
+
type TextureWrap = 'clamp' | 'repeat';
|
|
783
|
+
declare class GLTextureCache {
|
|
784
|
+
private readonly gl;
|
|
785
|
+
private readonly map;
|
|
786
|
+
private readonly wraps;
|
|
787
|
+
constructor(gl: WebGL2RenderingContext);
|
|
788
|
+
has(id: string): boolean;
|
|
789
|
+
/** Uploads the pixels once per id. `wrap` is re-applied whenever it differs
|
|
790
|
+
* from what the id was last uploaded under: the same registered image can
|
|
791
|
+
* be a clamped shader texture in one draw and a repeating pattern tile in
|
|
792
|
+
* the next, and first-upload-wins gave the second one the first one's mode. */
|
|
793
|
+
upload(id: string, source: TexSource$1, wrap?: TextureWrap): string;
|
|
794
|
+
private applyWrap;
|
|
795
|
+
/** Create a single-channel R8 texture from raw bytes (full upload).
|
|
796
|
+
* No-op if `id` already exists. Same LINEAR/CLAMP params as `upload`;
|
|
797
|
+
* UNPACK_ALIGNMENT dropped to 1 for non-4-aligned row widths. */
|
|
798
|
+
uploadR8(id: string, width: number, height: number, data: Uint8Array): void;
|
|
799
|
+
/** Patch a rect of an existing R8 texture with tightly-packed w×h bytes. */
|
|
800
|
+
subImageR8(id: string, x: number, y: number, w: number, h: number, data: Uint8Array): void;
|
|
801
|
+
bind(id: string, unit: number): void;
|
|
802
|
+
/**
|
|
803
|
+
* Delete every uploaded GL texture and clear the map. Called by
|
|
804
|
+
* `WeaselRenderer.dispose()`. The cache is unusable but refillable
|
|
805
|
+
* afterward — a subsequent `upload()` for a previously-seen id re-creates
|
|
806
|
+
* the texture rather than restoring the deleted one.
|
|
807
|
+
*/
|
|
808
|
+
free(): void;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* GL texture upload cache for ImageBitmap objects.
|
|
813
|
+
*
|
|
814
|
+
* Key: ImageBitmap (or pattern source object) identity (WeakMap) — lets GC
|
|
815
|
+
* reclaim unreferenced bitmaps. The GL textures are NOT freed when the source
|
|
816
|
+
* is gc'd; deferred to v2.
|
|
817
|
+
*
|
|
818
|
+
* Wrapping is set once at upload time per the `repetition` parameter:
|
|
819
|
+
* - undefined / 'no-repeat' → CLAMP_TO_EDGE
|
|
820
|
+
* - 'repeat' → REPEAT (both axes)
|
|
821
|
+
* - 'repeat-x' → REPEAT on S, CLAMP on T
|
|
822
|
+
* - 'repeat-y' → CLAMP on S, REPEAT on T
|
|
823
|
+
*
|
|
824
|
+
* Convention §2: texels stored straight; shader premultiplies.
|
|
825
|
+
*/
|
|
826
|
+
type PatternRepetition = 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat';
|
|
827
|
+
/** MIN_FILTER strategy for uploaded textures. */
|
|
828
|
+
type ImageMinification = 'linear' | 'mipmap';
|
|
829
|
+
type TexSource = ImageBitmap | ImageData | HTMLCanvasElement | HTMLImageElement;
|
|
830
|
+
declare class GLImageCache {
|
|
831
|
+
private readonly gl;
|
|
832
|
+
private readonly minification;
|
|
833
|
+
private readonly map;
|
|
834
|
+
/**
|
|
835
|
+
* MAG_FILTER each texture currently carries, so a redundant write can be
|
|
836
|
+
* skipped.
|
|
837
|
+
*
|
|
838
|
+
* The batch sets this per flush — the same bitmap can be drawn at both
|
|
839
|
+
* filters in one frame, and the value has to be right at the draw rather than
|
|
840
|
+
* at upload. Filtering is state on the *texture object*, though, not on the
|
|
841
|
+
* unit, so re-asserting a value it already has is a write to a live texture
|
|
842
|
+
* for no reason. On a large sheet that is not free: a consumer's wall samples
|
|
843
|
+
* a 5652px-square atlas, 122MB resident, and measured `sampling: 'nearest'`
|
|
844
|
+
* costing up to 8x `'linear'` there — where the linear pass re-asserts the
|
|
845
|
+
* upload default and the nearest pass changes state on every draw.
|
|
846
|
+
*
|
|
847
|
+
* Whether that is the cause is unproven (see `docs/TODO.md`), but the write
|
|
848
|
+
* was redundant either way.
|
|
849
|
+
*/
|
|
850
|
+
private readonly magFilters;
|
|
851
|
+
/** `minification` selects the MIN_FILTER strategy for uploaded textures.
|
|
852
|
+
* `'linear'` (default) is the screen path's existing behavior. `'mipmap'`
|
|
853
|
+
* generates mipmaps and filters LINEAR_MIPMAP_LINEAR — required for
|
|
854
|
+
* quality minification when a large source bitmap is drawn small (the
|
|
855
|
+
* headless print/export path); bilinear-only minification undersamples
|
|
856
|
+
* and produces moiré. */
|
|
857
|
+
constructor(gl: WebGL2RenderingContext, minification?: ImageMinification);
|
|
858
|
+
upload(key: object, source: TexSource, repetition?: PatternRepetition): WebGLTexture;
|
|
859
|
+
bind(key: object, unit: number): void;
|
|
860
|
+
/** Set `key`'s MAG_FILTER, skipping the call when it already holds that
|
|
861
|
+
* value. Its texture must be bound to the active unit — callers pair this
|
|
862
|
+
* with `bind`. */
|
|
863
|
+
setMagFilter(key: object, filter: number): void;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* CPU gradient-ramp builder + the GL texture every baked ramp lives in.
|
|
868
|
+
*
|
|
869
|
+
* Each unique stop list is baked once into a 256-texel strip and written to its
|
|
870
|
+
* own **row** of one RGBA texture, keyed by `JSON.stringify(stops)`. One
|
|
871
|
+
* texture rather than one per ramp is what lets a gradient take a batch texture
|
|
872
|
+
* slot the way a bitmap or a font atlas does: every gradient in a frame samples
|
|
873
|
+
* the same unit, so a run does not break per gradient. The atlas is
|
|
874
|
+
* GL-context-bound; discard and recreate on context loss.
|
|
875
|
+
*
|
|
876
|
+
* Output convention §2: texels are stored as straight RGBA. The fragment
|
|
877
|
+
* shader applies premultiplication before writing outColor.
|
|
878
|
+
*/
|
|
879
|
+
|
|
880
|
+
/** Bake gradient stops into a 256-entry RGBA lookup strip, which the shader
|
|
881
|
+
* samples instead of evaluating stops per fragment. */
|
|
882
|
+
declare function buildGradientRamp(stops: GradStop[]): Uint8ClampedArray;
|
|
883
|
+
declare class GradientRampAtlas {
|
|
884
|
+
private readonly gl;
|
|
885
|
+
/** Row per stop list, in least-recently-used order: a hit re-inserts, so the
|
|
886
|
+
* first entry is the row a full atlas recycles. */
|
|
887
|
+
private readonly rowByKey;
|
|
888
|
+
private texture;
|
|
889
|
+
private rows;
|
|
890
|
+
/** Mirror of the texture, so growth can respecify it without re-baking every
|
|
891
|
+
* ramp it already holds. */
|
|
892
|
+
private pixels;
|
|
893
|
+
private totalQueries;
|
|
894
|
+
private cacheHits;
|
|
895
|
+
constructor(gl: WebGL2RenderingContext);
|
|
896
|
+
/** Rows the atlas currently holds — its texture height. */
|
|
897
|
+
get height(): number;
|
|
898
|
+
/**
|
|
899
|
+
* Bake `stops` into a row and return it, reusing the row an identical stop
|
|
900
|
+
* list already holds.
|
|
901
|
+
*
|
|
902
|
+
* **A returned row outlives the frame only while the atlas has room.** Past
|
|
903
|
+
* `RAMP_ATLAS_MAX_ROWS` an upload recycles the least recently used row, which
|
|
904
|
+
* rewrites texels a row handed out earlier still names. Nothing that defers
|
|
905
|
+
* its draw past this call may hold a row across another `upload` without
|
|
906
|
+
* arranging to be flushed first.
|
|
907
|
+
*/
|
|
908
|
+
upload(stops: GradStop[]): number;
|
|
909
|
+
/**
|
|
910
|
+
* Whether uploading `stops` would move where existing rows sit — by growing
|
|
911
|
+
* the atlas, which changes every row's `v`, or by recycling one, which
|
|
912
|
+
* rewrites its texels.
|
|
913
|
+
*
|
|
914
|
+
* Anything holding a row past this call asks first and gets itself out of
|
|
915
|
+
* the way, because neither can be undone once it has happened.
|
|
916
|
+
*/
|
|
917
|
+
wouldReshape(stops: GradStop[]): boolean;
|
|
918
|
+
/**
|
|
919
|
+
* The `v` a row is sampled at — its center.
|
|
920
|
+
*
|
|
921
|
+
* The center is load-bearing: the atlas filters LINEAR, so a `v` anywhere
|
|
922
|
+
* else blends the ramp beside it into this one. At the center the neighbor's
|
|
923
|
+
* weight is exactly zero.
|
|
924
|
+
*/
|
|
925
|
+
rowV(row: number): number;
|
|
926
|
+
bind(unit: number): void;
|
|
927
|
+
hitRate(): number;
|
|
928
|
+
resetStats(): void;
|
|
929
|
+
/**
|
|
930
|
+
* Delete the atlas texture and forget every row in it. Called by
|
|
931
|
+
* `WeaselRenderer.dispose()`. The CPU mirror goes with it, so the atlas is
|
|
932
|
+
* unusable but refillable afterward (stats are left as-is; call
|
|
933
|
+
* `resetStats()` separately if desired).
|
|
934
|
+
*/
|
|
935
|
+
free(): void;
|
|
936
|
+
/** The row the next ramp is written to: a free one, a taller atlas, or the
|
|
937
|
+
* least recently used row of a full one. */
|
|
938
|
+
private claimRow;
|
|
939
|
+
/** Grow to `rows`, keeping every row at the index it already had — a row
|
|
940
|
+
* index is a stable name, and callers hold them. */
|
|
941
|
+
private resize;
|
|
942
|
+
private writeRow;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* 2D affine matrix utilities. Column-major 9-element Float32Array, matching
|
|
947
|
+
* `WebGL2RenderingContext.uniformMatrix3fv` byte order so we can pass the
|
|
948
|
+
* array directly without a transpose flag.
|
|
949
|
+
*
|
|
950
|
+
* Layout (column-major):
|
|
951
|
+
* [m00, m10, 0,
|
|
952
|
+
* m01, m11, 0,
|
|
953
|
+
* tx, ty, 1]
|
|
954
|
+
*
|
|
955
|
+
* `apply(m, x, y)` returns `[m * (x, y, 1)] = [m00*x + m01*y + tx,
|
|
956
|
+
* m10*x + m11*y + ty]`.
|
|
957
|
+
*/
|
|
958
|
+
type Mat3 = Float32Array;
|
|
959
|
+
declare function identity(): Mat3;
|
|
960
|
+
declare function multiply(out: Mat3, m: Mat3): Mat3;
|
|
961
|
+
declare function translate(m: Mat3, tx: number, ty: number): Mat3;
|
|
962
|
+
declare function scale(m: Mat3, sx: number, sy: number): Mat3;
|
|
963
|
+
/** Inverse of an affine matrix, or `null` when `@weasel-js/geom`'s `invert`
|
|
964
|
+
* finds it singular — the same rule, read through this layout. */
|
|
965
|
+
declare function invert(m: Mat3): Mat3 | null;
|
|
966
|
+
declare function apply(m: Mat3, x: number, y: number): [number, number];
|
|
967
|
+
/**
|
|
968
|
+
* Map screen pixel coords (0..width on X, 0..height on Y, top-left origin)
|
|
969
|
+
* into clip space (-1..1 on X, 1..-1 on Y — note Y flip so screen-down
|
|
970
|
+
* matches clip-down).
|
|
971
|
+
*/
|
|
972
|
+
declare function screenToClip(width: number, height: number): Mat3;
|
|
973
|
+
/**
|
|
974
|
+
* Uniform-equivalent scale factor: the square root of the absolute
|
|
975
|
+
* determinant of the linear part, i.e. the geometric mean of the two axis
|
|
976
|
+
* scales. Rotation-invariant. Under non-uniform scale it is between the two
|
|
977
|
+
* axes and exact on neither — the same compromise `meanScale` documents.
|
|
978
|
+
*/
|
|
979
|
+
declare function meanScaleOf(m: Mat3): number;
|
|
980
|
+
/** The renderer's 3x3 matrix operations, as one namespace. These work on the
|
|
981
|
+
* 9-element `Float32Array` form the GL uniform upload wants — distinct from
|
|
982
|
+
* `@weasel-js/geom`'s 6-element affine `Mat3`, though the logical element
|
|
983
|
+
* order is the same. */
|
|
984
|
+
declare const mat3: {
|
|
985
|
+
identity: typeof identity;
|
|
986
|
+
multiply: typeof multiply;
|
|
987
|
+
translate: typeof translate;
|
|
988
|
+
scale: typeof scale;
|
|
989
|
+
invert: typeof invert;
|
|
990
|
+
apply: typeof apply;
|
|
991
|
+
screenToClip: typeof screenToClip;
|
|
992
|
+
meanScaleOf: typeof meanScaleOf;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
/** Row-major 4×5 color matrix identity. */
|
|
996
|
+
declare const IDENTITY_COLOR_MATRIX: Float32Array<ArrayBuffer>;
|
|
997
|
+
interface GroupFrame {
|
|
998
|
+
transform?: Mat3;
|
|
999
|
+
alpha?: number;
|
|
1000
|
+
/** Row-major 4×5 color matrix (20 floats). Absent leaves the stack unchanged. */
|
|
1001
|
+
colorMatrix?: Float32Array | number[];
|
|
1002
|
+
}
|
|
1003
|
+
declare class GroupState {
|
|
1004
|
+
private transformStack;
|
|
1005
|
+
private alphaStack;
|
|
1006
|
+
private colorMatrixStack;
|
|
1007
|
+
get transform(): Mat3;
|
|
1008
|
+
get alpha(): number;
|
|
1009
|
+
get colorMatrix(): Float32Array;
|
|
1010
|
+
push(frame: GroupFrame): void;
|
|
1011
|
+
/**
|
|
1012
|
+
* Push a frame whose children paint into a surface of their own.
|
|
1013
|
+
*
|
|
1014
|
+
* The transform still accumulates — the children draw where they would have
|
|
1015
|
+
* drawn. Alpha and colour do not: they describe how the finished surface
|
|
1016
|
+
* joins the frame, and applying them on the way in as well would fade the
|
|
1017
|
+
* pixels an effect is about to read, then fade them again on the way out.
|
|
1018
|
+
*
|
|
1019
|
+
* Returns the pair the caller must apply at composite time — what `push`
|
|
1020
|
+
* would have left on the stack — so the composition rules live here and not
|
|
1021
|
+
* in the dispatcher.
|
|
1022
|
+
*/
|
|
1023
|
+
pushIsolated(frame: GroupFrame): {
|
|
1024
|
+
alpha: number;
|
|
1025
|
+
colorMatrix: Float32Array;
|
|
1026
|
+
};
|
|
1027
|
+
/** Drop every pushed frame, leaving the root. A frame that throws part-way
|
|
1028
|
+
* down the tree never pops, and the next frame would draw under leftovers. */
|
|
1029
|
+
reset(): void;
|
|
1030
|
+
pop(): void;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* registerProgram — public API for registering custom shader programs.
|
|
1035
|
+
*
|
|
1036
|
+
* Stores raw GLSL source strings in a module-level registry. GL compilation
|
|
1037
|
+
* happens on each WeaselRenderer via WeaselRenderer.registerProgram(), which
|
|
1038
|
+
* calls getProgramSource() and compiles the result. This keeps registerProgram
|
|
1039
|
+
* GL-context-agnostic — identical pattern to registerFont storing ImageBitmap.
|
|
1040
|
+
*
|
|
1041
|
+
* Module-level state = source strings only; compiled GL
|
|
1042
|
+
* programs live on each renderer's programRegistry (Map<id, ShaderProgram>).
|
|
1043
|
+
*
|
|
1044
|
+
* Lifecycle: program sources live for the module lifetime. No unregister in v1.
|
|
1045
|
+
*/
|
|
1046
|
+
|
|
1047
|
+
/** Opaque handle to a compiled custom shader program. */
|
|
1048
|
+
interface ShaderProgramHandle {
|
|
1049
|
+
readonly id: string;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Scalar and vector uniform types accepted by the custom shader uniform binder.
|
|
1053
|
+
*
|
|
1054
|
+
* | TS type | GL call |
|
|
1055
|
+
* |-------------------------|--------------------------------------|
|
|
1056
|
+
* | number | uniform1f |
|
|
1057
|
+
* | [n, n] | uniform2fv |
|
|
1058
|
+
* | [n, n, n] | uniform3fv |
|
|
1059
|
+
* | [n, n, n, n] | uniform4fv |
|
|
1060
|
+
* | Float32Array length 9 | uniformMatrix3fv (column-major) |
|
|
1061
|
+
* | Float32Array length 16 | uniformMatrix4fv (column-major) |
|
|
1062
|
+
* | TextureHandle | bind to next tex unit + uniform1i |
|
|
1063
|
+
*/
|
|
1064
|
+
type ShaderUniform = number | [number, number] | [number, number, number] | [number, number, number, number] | Float32Array | TextureHandle;
|
|
1065
|
+
/**
|
|
1066
|
+
* Register a custom shader program by id.
|
|
1067
|
+
*
|
|
1068
|
+
* Pass an empty string for `vert` to use the kit's default vertex shader
|
|
1069
|
+
* (recommended). The kit's vertex shader exposes `v_uv`, `v_screen`, and
|
|
1070
|
+
* `v_world` varyings plus `u_bounds` and `u_view` uniforms.
|
|
1071
|
+
*
|
|
1072
|
+
* **IMPORTANT — Premultiplied alpha:**
|
|
1073
|
+
* Your fragment shader MUST output premultiplied alpha:
|
|
1074
|
+
* `outColor = vec4(rgb * a, a);` ← correct
|
|
1075
|
+
* `outColor = vec4(rgb, a);` ← WRONG — over-brightens translucent regions
|
|
1076
|
+
*
|
|
1077
|
+
* The renderer uses `gl.blendFunc(ONE, ONE_MINUS_SRC_ALPHA)` to match.
|
|
1078
|
+
* Opaque fragments (a=1) are unaffected; only fragments with a < 1 differ.
|
|
1079
|
+
*
|
|
1080
|
+
* **Re-registration behavior:**
|
|
1081
|
+
* - Dev mode (`NODE_ENV !== 'production'`): calling with an existing id replaces
|
|
1082
|
+
* the source (hot-reload). Each renderer must call `WeaselRenderer.registerProgram(handle)`
|
|
1083
|
+
* again to pick up the new source.
|
|
1084
|
+
* - Prod mode: calling with an existing id throws.
|
|
1085
|
+
*
|
|
1086
|
+
* Actual GL compilation and `ShaderCompileError` throwing happen in
|
|
1087
|
+
* `WeaselRenderer.registerProgram()`, not here.
|
|
1088
|
+
*
|
|
1089
|
+
* @experimental API may break before v2.
|
|
1090
|
+
*/
|
|
1091
|
+
declare function registerProgram(id: string, vert: string, frag: string): ShaderProgramHandle;
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* One full-screen pass over what a group has already drawn.
|
|
1095
|
+
*
|
|
1096
|
+
* The renderer runs a group's effects in order, each reading the previous
|
|
1097
|
+
* one's output through `u_source` and writing a whole new buffer — so an
|
|
1098
|
+
* effect is free to read neighbouring pixels, which is the entire point and
|
|
1099
|
+
* the one thing `colorMatrix` can never do.
|
|
1100
|
+
*
|
|
1101
|
+
* `uniforms` are the effect's own; `u_source`, `u_resolution` and `u_texel`
|
|
1102
|
+
* come from the renderer and must not be passed here.
|
|
1103
|
+
*/
|
|
1104
|
+
interface Effect {
|
|
1105
|
+
program: ShaderProgramHandle;
|
|
1106
|
+
uniforms?: Record<string, ShaderUniform>;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Register a fragment shader as an effect. Sugar over `registerProgram` with
|
|
1110
|
+
* the effect vertex shader, and the reason a consumer never imports the
|
|
1111
|
+
* prelude: an effect that registers with the *custom-shader* vertex shader
|
|
1112
|
+
* compiles, runs, and samples its source upside down.
|
|
1113
|
+
*
|
|
1114
|
+
* The fragment shader reads `v_uv` and `u_source`, and may declare
|
|
1115
|
+
* `u_resolution` / `u_texel`. See `effectPrelude.ts` for the full contract,
|
|
1116
|
+
* including the premultiplied-alpha requirement.
|
|
1117
|
+
*
|
|
1118
|
+
* @experimental
|
|
1119
|
+
*/
|
|
1120
|
+
declare function registerEffect(id: string, frag: string): ShaderProgramHandle;
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Solid-fill paint variant (subset of the full `FillStyle` union from
|
|
1124
|
+
* `@weasel-js/core`). Kept for back-compat with step-1/2 consumers and
|
|
1125
|
+
* because some code reads `fill.color` directly. Through step 4, fills can
|
|
1126
|
+
* be any `FillStyle` variant — solid, pattern, or gradient.
|
|
1127
|
+
*/
|
|
1128
|
+
interface SolidPaint {
|
|
1129
|
+
fill?: 'solid';
|
|
1130
|
+
/** Any CSS color string accepted by `parseColor`: hex, `rgb()`/`rgba()`, `hsl()`/`hsla()`, named, or `transparent`. */
|
|
1131
|
+
color: string;
|
|
1132
|
+
opacity?: number;
|
|
1133
|
+
}
|
|
1134
|
+
/** DrawCommand variants implemented through step 6. */
|
|
1135
|
+
type DrawCommand = PathDrawCommand | GroupDrawCommand | TextDrawCommand | ImageDrawCommand | SpritesDrawCommand | ShaderDrawCommand;
|
|
1136
|
+
/** Draw a path, filled and/or stroked. The workhorse command: every shape the
|
|
1137
|
+
* kit draws that is not text, an image, or a custom shader is one of these. */
|
|
1138
|
+
interface PathDrawCommand {
|
|
1139
|
+
kind: 'path';
|
|
1140
|
+
path: Path;
|
|
1141
|
+
/** Any `FillStyle` variant: solid, pattern, or gradient (linear/radial/conic). */
|
|
1142
|
+
fill?: FillStyle;
|
|
1143
|
+
/** Stroke spec. Only solid `paint` supported through step 4. */
|
|
1144
|
+
stroke?: Stroke;
|
|
1145
|
+
/**
|
|
1146
|
+
* Optional flat RGBA-per-path-anchor color array (length =
|
|
1147
|
+
* `4 × countPathAnchors(path)`, floats in 0..1). The renderer
|
|
1148
|
+
* arc-length-interpolates these per-anchor colors across the
|
|
1149
|
+
* flattened/triangulated mesh between consecutive anchors using the
|
|
1150
|
+
* mesh's `anchorA` / `anchorB` / `anchorT` parameterization.
|
|
1151
|
+
*
|
|
1152
|
+
* **`fill` must also be set when using `vertexColors`.** The renderer
|
|
1153
|
+
* only enters the per-vertex shader path when the command has a fill
|
|
1154
|
+
* (the fill provides the opacity uniform; the vertex colors override
|
|
1155
|
+
* the fill's color). Pass any solid `fill` (e.g. `{ color: '#fff' }`)
|
|
1156
|
+
* as the placeholder; the per-vertex colors win in the shader.
|
|
1157
|
+
*/
|
|
1158
|
+
vertexColors?: number[];
|
|
1159
|
+
}
|
|
1160
|
+
/** Draw a list of commands under a shared transform, opacity, color matrix
|
|
1161
|
+
* and clip. Groups nest, and their effects accumulate down the stack — this
|
|
1162
|
+
* is how a container node's transform reaches its descendants. */
|
|
1163
|
+
interface GroupDrawCommand {
|
|
1164
|
+
kind: 'group';
|
|
1165
|
+
transform?: Mat3;
|
|
1166
|
+
alpha?: number;
|
|
1167
|
+
/**
|
|
1168
|
+
* Optional 4×5 color matrix (row-major, 20 numbers) — `out = M₄ₓ₄ * in + bias`.
|
|
1169
|
+
* Accumulated multiplicatively down the group stack. Defaults to identity.
|
|
1170
|
+
*/
|
|
1171
|
+
colorMatrix?: number[];
|
|
1172
|
+
/** Optional clip path. When set, the renderer rasterizes this path into
|
|
1173
|
+
* the stencil buffer before drawing `children`; the children paint only
|
|
1174
|
+
* where the clip covers. Nested groups with clips intersect — a child
|
|
1175
|
+
* cannot escape an ancestor's clip. Max 7 nesting levels; the renderer
|
|
1176
|
+
* throws if exceeded. */
|
|
1177
|
+
clip?: Path;
|
|
1178
|
+
/**
|
|
1179
|
+
* Full-screen passes run over this group's own pixels, in order, before it
|
|
1180
|
+
* is composited into its parent.
|
|
1181
|
+
*
|
|
1182
|
+
* Unlike every other field here, this does not accumulate down the group
|
|
1183
|
+
* stack — it is a render-target boundary. The children draw into a buffer of
|
|
1184
|
+
* their own, each effect reads the previous one's output, and the result is
|
|
1185
|
+
* composited back under this group's `transform`, `alpha`, `colorMatrix` and
|
|
1186
|
+
* whatever clip encloses it. So a blur here blurs this group and nothing
|
|
1187
|
+
* around it, which is what a CSS `filter` on the canvas cannot do.
|
|
1188
|
+
*
|
|
1189
|
+
* An empty or absent list costs nothing: no buffer is allocated until a
|
|
1190
|
+
* group asks for one.
|
|
1191
|
+
*/
|
|
1192
|
+
effects?: readonly Effect[];
|
|
1193
|
+
children: DrawCommand[];
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Text draw command. Renders one or more runs at (`x`, `y`) in screen
|
|
1197
|
+
* space, optionally word-wrapping at `maxWidth`. The renderer resolves
|
|
1198
|
+
* each run's `(fontFamily, fontWeight, fontStyle)` to an MSDF atlas via
|
|
1199
|
+
* `resolveFontVariant` and bucket-draws by atlas + color group.
|
|
1200
|
+
*
|
|
1201
|
+
* `style` carries node-level defaults (`lineHeight`, anti-alias width)
|
|
1202
|
+
* that don't belong on individual runs.
|
|
1203
|
+
*/
|
|
1204
|
+
interface TextDrawCommand {
|
|
1205
|
+
kind: 'text';
|
|
1206
|
+
x: number;
|
|
1207
|
+
y: number;
|
|
1208
|
+
runs: ResolvedRun[];
|
|
1209
|
+
/** Wrap width. Absent never wraps. */
|
|
1210
|
+
maxWidth?: number;
|
|
1211
|
+
align?: 'left' | 'center' | 'right';
|
|
1212
|
+
/** Box width `align` resolves within, from `x`. Default `maxWidth`; with
|
|
1213
|
+
* neither, `x` is the line's left edge, midpoint or right edge. */
|
|
1214
|
+
width?: number;
|
|
1215
|
+
style: TextStyle;
|
|
1216
|
+
/** Box height for vertical alignment. When set with `verticalAlign`,
|
|
1217
|
+
* the laid-out block shifts within `[y, y+height]`. */
|
|
1218
|
+
height?: number;
|
|
1219
|
+
/** Default 'top' — the legacy top-anchored behavior. */
|
|
1220
|
+
verticalAlign?: TextVerticalAlign;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Image draw command — renders `image` at screen-space rect (x, y, w, h).
|
|
1224
|
+
* The image is stretched to fit; no tiling. Use a pattern FillStyle on a path
|
|
1225
|
+
* for tiling.
|
|
1226
|
+
*/
|
|
1227
|
+
interface ImageDrawCommand {
|
|
1228
|
+
kind: 'image';
|
|
1229
|
+
image: ImageBitmap;
|
|
1230
|
+
x: number;
|
|
1231
|
+
y: number;
|
|
1232
|
+
w: number;
|
|
1233
|
+
h: number;
|
|
1234
|
+
opacity?: number;
|
|
1235
|
+
/** Magnification filter. `'linear'` (default) smooths; `'nearest'` shows
|
|
1236
|
+
* device pixels as hard squares — required by anything magnifying a
|
|
1237
|
+
* framebuffer readback, where blur destroys the point of the readback. */
|
|
1238
|
+
sampling?: 'linear' | 'nearest';
|
|
1239
|
+
/** Sub-rectangle of `image` to draw, in bitmap pixels from the top-left.
|
|
1240
|
+
* Omitted draws the whole bitmap. Not range-checked: a rect past the edge
|
|
1241
|
+
* samples outside [0..1], which CLAMP_TO_EDGE smears. With
|
|
1242
|
+
* `sampling: 'linear'` the filter reaches half a texel beyond `source`, so
|
|
1243
|
+
* atlas frames need a gutter (see `SpriteSheet.spacing`) or `'nearest'`. */
|
|
1244
|
+
source?: {
|
|
1245
|
+
x: number;
|
|
1246
|
+
y: number;
|
|
1247
|
+
w: number;
|
|
1248
|
+
h: number;
|
|
1249
|
+
};
|
|
1250
|
+
/** Mirror the sampled region within the destination rect. The quad does not
|
|
1251
|
+
* move — a flipped draw covers exactly the pixels an unflipped one does. */
|
|
1252
|
+
flipX?: boolean;
|
|
1253
|
+
flipY?: boolean;
|
|
1254
|
+
}
|
|
1255
|
+
/** Floats per sprite in `SpritesDrawCommand.sprites`. */
|
|
1256
|
+
declare const SPRITE_STRIDE = 9;
|
|
1257
|
+
/**
|
|
1258
|
+
* Draw many quads sampling one bitmap — an atlas, a sprite sheet, a wall of
|
|
1259
|
+
* thumbnails. The same picture as a run of `ImageDrawCommand`s the renderer
|
|
1260
|
+
* would coalesce anyway, handed over already packed so it never walks a
|
|
1261
|
+
* command object per quad.
|
|
1262
|
+
*
|
|
1263
|
+
* Reach for it past a few thousand sprites. Below that a plain run of image
|
|
1264
|
+
* commands merges into the same single draw and reads better; the packed form
|
|
1265
|
+
* exists because at 20,000 the object walk is about half the frame.
|
|
1266
|
+
*
|
|
1267
|
+
* The sprites are one run: they share a texture, a filter, and whatever group
|
|
1268
|
+
* transform, alpha, color matrix and clip are live, exactly as a merged run of
|
|
1269
|
+
* image commands would. Anything varying per sprite is in the array.
|
|
1270
|
+
*/
|
|
1271
|
+
interface SpritesDrawCommand {
|
|
1272
|
+
kind: 'sprites';
|
|
1273
|
+
image: ImageBitmap;
|
|
1274
|
+
/** Magnification filter for the whole run, as `ImageDrawCommand.sampling`. */
|
|
1275
|
+
sampling?: 'linear' | 'nearest';
|
|
1276
|
+
/**
|
|
1277
|
+
* `SPRITE_STRIDE` floats per sprite:
|
|
1278
|
+
* `dx, dy, dw, dh, sx, sy, sw, sh, opacity`.
|
|
1279
|
+
*
|
|
1280
|
+
* Destination is in the group's coordinates; source is in bitmap pixels,
|
|
1281
|
+
* like `ImageDrawCommand.source`. A negative `sw` or `sh` mirrors that axis
|
|
1282
|
+
* within the source rect, which is what `flipX` / `flipY` do. A trailing
|
|
1283
|
+
* partial sprite is ignored.
|
|
1284
|
+
*/
|
|
1285
|
+
sprites: Float32Array;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Custom shader draw command. The renderer generates a quad over `bounds`
|
|
1289
|
+
* and dispatches the consumer's fragment shader with the kit's vertex prelude.
|
|
1290
|
+
*
|
|
1291
|
+
* `uniforms` keys must match names declared in the consumer's fragment shader.
|
|
1292
|
+
* The kit automatically sets `u_bounds`, `u_view`, and `u_proj` — do not
|
|
1293
|
+
* declare those in `uniforms`.
|
|
1294
|
+
*
|
|
1295
|
+
* @experimental API may change before v2.
|
|
1296
|
+
*/
|
|
1297
|
+
interface ShaderDrawCommand {
|
|
1298
|
+
kind: 'shader';
|
|
1299
|
+
program: ShaderProgramHandle;
|
|
1300
|
+
uniforms: Record<string, ShaderUniform>;
|
|
1301
|
+
/** Screen-space bounding rect in CSS pixels. */
|
|
1302
|
+
bounds: {
|
|
1303
|
+
x: number;
|
|
1304
|
+
y: number;
|
|
1305
|
+
w: number;
|
|
1306
|
+
h: number;
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Growable vertex staging for a run of solid-fill geometry, image quads and
|
|
1312
|
+
* glyphs.
|
|
1313
|
+
*
|
|
1314
|
+
* Geometry only: `draw.ts` owns when a run starts, what breaks it, and the
|
|
1315
|
+
* uniforms the flush draws under.
|
|
1316
|
+
*
|
|
1317
|
+
* **One batch for all three, because a page interleaves them.** A grid of
|
|
1318
|
+
* thumbnails is a ground rect under an atlas quad under a caption, per cell,
|
|
1319
|
+
* and while each kind staged separately every one had to drain the others
|
|
1320
|
+
* before it could stage — so a shape that batches perfectly in any one half
|
|
1321
|
+
* alone paid a flush per command. Everything a vertex needs to say which it is
|
|
1322
|
+
* fits in the same vertex: solids carry the UV of a 1x1 white texel and are
|
|
1323
|
+
* their own color, quads carry their atlas UV and a white color, glyphs carry
|
|
1324
|
+
* a font atlas UV, their text color, and a paint mode saying the texel is a
|
|
1325
|
+
* distance field rather than a color. Gradients join as a fourth off the ramp
|
|
1326
|
+
* atlas — a linear one without a mode of its own, since (ramp position, row) is
|
|
1327
|
+
* what the plain mode already samples, and a radial or conic one with a mode
|
|
1328
|
+
* that says its UV is a gradient-space coordinate to take a `length` or an
|
|
1329
|
+
* `atan` of. See `shaders/batchFill.ts`.
|
|
1330
|
+
*
|
|
1331
|
+
* Colors ride the vertices because shapes in a run differ in color and a merged
|
|
1332
|
+
* draw has one set of uniforms — and so does the model transform, applied here
|
|
1333
|
+
* rather than uploaded, so shapes under different transforms still share a
|
|
1334
|
+
* draw. The texture rides them too, as a slot index into the units the flush
|
|
1335
|
+
* binds: slot 0 is the white texel every solid samples, and `draw.ts` hands out
|
|
1336
|
+
* the rest per bitmap. A run holds as many bitmaps as there are slots, so an
|
|
1337
|
+
* atlas is what makes a wall of one sheet coalesce and a handful of loose
|
|
1338
|
+
* bitmaps no longer breaks a run per command.
|
|
1339
|
+
*/
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* A gradient vertex's `a_uv`, each channel affine in the coordinates the
|
|
1343
|
+
* geometry arrives in: `u = ux*x + uy*y + u0`, and the same for `v`.
|
|
1344
|
+
*
|
|
1345
|
+
* One shape for all three gradients. A linear one's `v` row is the constant
|
|
1346
|
+
* atlas row and its `u` row is the ramp position; a radial or conic one's two
|
|
1347
|
+
* rows are the gradient-space coordinate, and the row rides `a_post` instead.
|
|
1348
|
+
*/
|
|
1349
|
+
interface GradientUV {
|
|
1350
|
+
ux: number;
|
|
1351
|
+
uy: number;
|
|
1352
|
+
u0: number;
|
|
1353
|
+
vx: number;
|
|
1354
|
+
vy: number;
|
|
1355
|
+
v0: number;
|
|
1356
|
+
}
|
|
1357
|
+
declare class DrawBatch {
|
|
1358
|
+
private readonly gl;
|
|
1359
|
+
private readonly aPos;
|
|
1360
|
+
private readonly aColor;
|
|
1361
|
+
private readonly aUv;
|
|
1362
|
+
private readonly aPost;
|
|
1363
|
+
private readonly aSlot;
|
|
1364
|
+
/** One ring per tier, cycled per flush. Slots are created on first use, so a
|
|
1365
|
+
* renderer that flushes rarely allocates as few as it flushes. */
|
|
1366
|
+
private readonly rings;
|
|
1367
|
+
private readonly nextInRing;
|
|
1368
|
+
/** The same, for flushes past the largest tier; these sets grow to fit. */
|
|
1369
|
+
private readonly largeRing;
|
|
1370
|
+
private nextLarge;
|
|
1371
|
+
private verts;
|
|
1372
|
+
private idx;
|
|
1373
|
+
private nVerts;
|
|
1374
|
+
private nIdx;
|
|
1375
|
+
/** Whether the staged run is quads alone, so its indices are the canonical
|
|
1376
|
+
* pattern and a slot already holding that pattern needs no upload. */
|
|
1377
|
+
private pureRects;
|
|
1378
|
+
constructor(gl: WebGL2RenderingContext, prog: ShaderProgram);
|
|
1379
|
+
get length(): number;
|
|
1380
|
+
/** Whether staging `vertices` more would put the run past the per-flush cap. */
|
|
1381
|
+
wouldOverflow(vertices: number): boolean;
|
|
1382
|
+
/**
|
|
1383
|
+
* Append one rect's four corners through `m`, all carrying `rgba` (straight
|
|
1384
|
+
* alpha). An affine maps a rect to a parallelogram, so two triangles still
|
|
1385
|
+
* cover it and the batch draws at `u_model` identity.
|
|
1386
|
+
*/
|
|
1387
|
+
pushRect(x: number, y: number, w: number, h: number, m: Mat3, r: number, g: number, b: number, a: number): void;
|
|
1388
|
+
/**
|
|
1389
|
+
* Append one image quad: the destination rect `(x, y, w, h)` mapped through
|
|
1390
|
+
* `m`, sampling `(u0, v0)`-`(u1, v1)`, every corner carrying `post` as the
|
|
1391
|
+
* after-the-color-matrix alpha factor.
|
|
1392
|
+
*
|
|
1393
|
+
* Corners wind top-left, top-right, bottom-right, bottom-left with UVs
|
|
1394
|
+
* following — the same winding `pushRect` uses, which is what lets a run of
|
|
1395
|
+
* mixed rects and quads keep the canonical index pattern. The flips a command
|
|
1396
|
+
* asks for are already in the `u` / `v` the caller passes.
|
|
1397
|
+
*
|
|
1398
|
+
* `slot` is the texture unit the corners sample, which `draw.ts` assigns per
|
|
1399
|
+
* bitmap within the run.
|
|
1400
|
+
*/
|
|
1401
|
+
pushQuad(x: number, y: number, w: number, h: number, m: Mat3, u0: number, v0: number, u1: number, v1: number, post: number, slot: number): void;
|
|
1402
|
+
/**
|
|
1403
|
+
* Append one glyph quad: the box `(x0, y0)`-`(x1, y1)` mapped through `m`,
|
|
1404
|
+
* sampling `(u0, v0)`-`(u1, v1)` of the font atlas at `slot`, painted in
|
|
1405
|
+
* `rgba`.
|
|
1406
|
+
*
|
|
1407
|
+
* `mode` says which channels of that atlas carry the distance field, and
|
|
1408
|
+
* rides the vertices packed into the slot — so a run off a baked MSDF atlas
|
|
1409
|
+
* and one off the runtime canvas bake still share a draw. The synthetic-bold
|
|
1410
|
+
* threshold does not: it is `u_synthBold`, and `draw.ts` breaks the run when
|
|
1411
|
+
* it changes.
|
|
1412
|
+
*
|
|
1413
|
+
* **A synthetic oblique is sheared here rather than in the shader.** The old
|
|
1414
|
+
* text program carried the baseline per vertex and skewed against
|
|
1415
|
+
* `u_synthItalic`; the batch places its own corners, so the shear is one
|
|
1416
|
+
* multiply while they are being placed, and `tanItalic` is 0 for an upright
|
|
1417
|
+
* face. It has to happen before `m`, which is where the shader had it too.
|
|
1418
|
+
*
|
|
1419
|
+
* Corners wind top-left, top-right, bottom-right, bottom-left — `pushQuad`'s
|
|
1420
|
+
* winding, which is what lets a run of mixed rects, image quads and glyphs
|
|
1421
|
+
* keep the canonical index pattern.
|
|
1422
|
+
*/
|
|
1423
|
+
pushGlyph(x0: number, y0: number, x1: number, y1: number, baselineY: number, tanItalic: number, m: Mat3, u0: number, v0: number, u1: number, v1: number, r: number, g: number, b: number, a: number, slot: number, mode: number): void;
|
|
1424
|
+
/**
|
|
1425
|
+
* Append one rect filled by a gradient: the corners of `(x, y, w, h)` mapped
|
|
1426
|
+
* through `m`, sampling the ramp atlas at `slot`.
|
|
1427
|
+
*
|
|
1428
|
+
* `uv` gives each channel of `a_uv` as an affine function of the coordinates
|
|
1429
|
+
* the corners arrive in, which is what lets a gradient ride the vertices at
|
|
1430
|
+
* all: a linear one's ramp position is affine in position outright, and a
|
|
1431
|
+
* radial or conic one's gradient-space *coordinate* is, even though the ramp
|
|
1432
|
+
* position it yields is not. Either way the rasterizer's interpolation across
|
|
1433
|
+
* a triangle is exact. `mode` says which of the two the fragment shader is
|
|
1434
|
+
* looking at, and `post` carries the atlas row for the modes that read it —
|
|
1435
|
+
* see `shaders/batchFill.ts`.
|
|
1436
|
+
*
|
|
1437
|
+
* Values outside 0..1 are the sampler's business; the atlas clamps to the
|
|
1438
|
+
* edge texel, which is what the gradient shader's own `clamp` did.
|
|
1439
|
+
*/
|
|
1440
|
+
pushGradientRect(x: number, y: number, w: number, h: number, m: Mat3, uv: GradientUV, post: number, slot: number, mode: number, r: number, g: number, b: number, a: number): void;
|
|
1441
|
+
/** `pushMesh` for a mesh filled by a gradient — see `pushGradientRect` for
|
|
1442
|
+
* what `uv`, `post` and `mode` carry. */
|
|
1443
|
+
pushGradientMesh(mesh: Mesh, m: Mat3, uv: GradientUV, post: number, slot: number, mode: number, r: number, g: number, b: number, a: number): void;
|
|
1444
|
+
/**
|
|
1445
|
+
* Append a tessellated mesh through `m`, all vertices carrying `rgba`. The
|
|
1446
|
+
* mesh's own indices are rebased onto the staged vertices, which is why the
|
|
1447
|
+
* index buffer is uploaded per flush rather than written once.
|
|
1448
|
+
*/
|
|
1449
|
+
pushMesh(mesh: Mesh, m: Mat3, r: number, g: number, b: number, a: number): void;
|
|
1450
|
+
/** Upload the staged geometry into the next set of buffers and bind its VAO.
|
|
1451
|
+
* Returns the index count for the caller's `drawElements`. */
|
|
1452
|
+
uploadAndBind(): number;
|
|
1453
|
+
reset(): void;
|
|
1454
|
+
dispose(): void;
|
|
1455
|
+
private writeVertex;
|
|
1456
|
+
/** Two triangles over the four corners just written. */
|
|
1457
|
+
private pushQuadIndices;
|
|
1458
|
+
/** Grow the CPU arrays so `vertices` / `indices` more fit. */
|
|
1459
|
+
private reserve;
|
|
1460
|
+
private nextRingSlot;
|
|
1461
|
+
private nextLargeSlot;
|
|
1462
|
+
private createSet;
|
|
1463
|
+
private deleteSet;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/** How to construct a `WeaselRenderer`: the GL context or canvas to draw
|
|
1467
|
+
* into, the output size, and the quality knobs that separate screen
|
|
1468
|
+
* rendering from print. */
|
|
1469
|
+
interface WeaselRendererOptions {
|
|
1470
|
+
gl?: WebGL2RenderingContext;
|
|
1471
|
+
canvas?: HTMLCanvasElement;
|
|
1472
|
+
width: number;
|
|
1473
|
+
height: number;
|
|
1474
|
+
dpr: number;
|
|
1475
|
+
/** MIN_FILTER strategy for image/pattern textures (`GLImageCache`).
|
|
1476
|
+
* Default `'linear'` — the existing screen behavior. The headless
|
|
1477
|
+
* `renderSceneToPixels` path passes `'mipmap'` for print-quality
|
|
1478
|
+
* minification. Explicitly passing `'linear'` is always valid. */
|
|
1479
|
+
imageMinification?: ImageMinification;
|
|
1480
|
+
/** Flatness tolerance for curve tessellation, in WORLD units (see
|
|
1481
|
+
* `TessellateOptions.flattenTolerance`). When set, path fills are
|
|
1482
|
+
* tessellated fresh at this tolerance per frame (transient pool) instead
|
|
1483
|
+
* of served from the Path-identity mesh cache — the cache key does not
|
|
1484
|
+
* include tolerance. Default: unset — the existing cached behavior at
|
|
1485
|
+
* `DEFAULT_FLATTEN_TOLERANCE`. The headless path derives this from the
|
|
1486
|
+
* requested output scale; screen callers normally leave it unset. */
|
|
1487
|
+
flattenTolerance?: number;
|
|
1488
|
+
/** Per-render synchronous bake budget for dynamic canvas-SDF glyphs.
|
|
1489
|
+
* Default DEFAULT_BAKE_BUDGET (16). The headless renderSceneToPixels
|
|
1490
|
+
* path passes Infinity so print never defers a glyph. */
|
|
1491
|
+
bakeBudget?: number;
|
|
1492
|
+
/** On-screen glyph size, in CSS pixels, at or above which text renders from
|
|
1493
|
+
* tessellated font outlines instead of a distance field — see
|
|
1494
|
+
* `OUTLINE_MIN_SCREEN_PX`. Only faces registered with
|
|
1495
|
+
* `registerFontOutlines` are affected; everything else keeps its SDF tier
|
|
1496
|
+
* whatever this says. Pass `Infinity` to disable the tier outright, or 0
|
|
1497
|
+
* to use outlines wherever they exist (what the headless path does, since
|
|
1498
|
+
* print has no reason to sample a field it could evaluate exactly). */
|
|
1499
|
+
textOutlineMinScreenSize?: number;
|
|
1500
|
+
}
|
|
1501
|
+
/** Where a renderer draws inside a buffer it does not own.
|
|
1502
|
+
*
|
|
1503
|
+
* The rect's SIZE is the renderer's own `width`/`height` — `resize()` owns
|
|
1504
|
+
* that, and a second copy here could disagree with the one `DrawContext`
|
|
1505
|
+
* reports to screen-space layers. */
|
|
1506
|
+
interface RenderTarget {
|
|
1507
|
+
/** Top-left of this renderer's output within the drawing buffer, in CSS
|
|
1508
|
+
* pixels, with the origin at the buffer's top-left. GL's bottom-left origin
|
|
1509
|
+
* is handled internally. */
|
|
1510
|
+
origin: {
|
|
1511
|
+
x: number;
|
|
1512
|
+
y: number;
|
|
1513
|
+
};
|
|
1514
|
+
/** Clear colour and stencil within the rect before drawing. Default true.
|
|
1515
|
+
* Pass false only when the caller clears the whole buffer itself on behalf
|
|
1516
|
+
* of every co-tenant — a frame that clears neither inherits its neighbour's
|
|
1517
|
+
* stencil bits, and even-odd fills then fill their holes. */
|
|
1518
|
+
clear?: boolean;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* The WebGL2 renderer: takes a list of draw commands and paints them.
|
|
1522
|
+
*
|
|
1523
|
+
* It knows nothing about the scene — commands are the whole interface, which
|
|
1524
|
+
* is what lets layers, HUD widgets and overlays all draw through the same
|
|
1525
|
+
* pipeline. GPU resources (meshes, textures, gradient ramps) are cached across
|
|
1526
|
+
* frames and keyed by identity, so re-issuing the same command is cheap.
|
|
1527
|
+
*/
|
|
1528
|
+
declare class WeaselRenderer {
|
|
1529
|
+
private readonly gl;
|
|
1530
|
+
private pathFill;
|
|
1531
|
+
private pathFillVColor;
|
|
1532
|
+
private imageFill;
|
|
1533
|
+
private batchFill;
|
|
1534
|
+
private gradFill;
|
|
1535
|
+
private patternFill;
|
|
1536
|
+
private meshCache;
|
|
1537
|
+
private textureCache;
|
|
1538
|
+
private imageCache;
|
|
1539
|
+
private gradRamps;
|
|
1540
|
+
private programRegistry;
|
|
1541
|
+
private quadVbo;
|
|
1542
|
+
private quadIbo;
|
|
1543
|
+
private drawBatch;
|
|
1544
|
+
/** 1x1 white, so a batch flush sampling no image still samples something —
|
|
1545
|
+
* see `shaders/batchFill.ts`. */
|
|
1546
|
+
private whiteTexture;
|
|
1547
|
+
private readonly groupState;
|
|
1548
|
+
private widthCss;
|
|
1549
|
+
private heightCss;
|
|
1550
|
+
private dpr;
|
|
1551
|
+
private canvas;
|
|
1552
|
+
private target;
|
|
1553
|
+
/** Offscreen buffers for group effects. Allocates nothing until a group
|
|
1554
|
+
* with effects asks, so a canvas without them pays no memory. */
|
|
1555
|
+
private readonly effectTargets;
|
|
1556
|
+
private readonly imageMinification;
|
|
1557
|
+
private readonly flattenTolerance?;
|
|
1558
|
+
private readonly bakeBudget;
|
|
1559
|
+
private readonly textOutlineMinScreenSize;
|
|
1560
|
+
private contextLost;
|
|
1561
|
+
private boundOnLost;
|
|
1562
|
+
private boundOnRestored;
|
|
1563
|
+
/** True after `dispose()`. A disposed renderer ignores further `render()`
|
|
1564
|
+
* calls and `registerProgram()` calls. */
|
|
1565
|
+
private disposed;
|
|
1566
|
+
constructor(opts: WeaselRendererOptions);
|
|
1567
|
+
private uploadQuadGeometry;
|
|
1568
|
+
/**
|
|
1569
|
+
* Compile a consumer-registered shader program against this renderer's GL context.
|
|
1570
|
+
*
|
|
1571
|
+
* Call once per renderer after the module-level `registerProgram()`.
|
|
1572
|
+
* Throws `ShaderCompileError` if compilation fails.
|
|
1573
|
+
*
|
|
1574
|
+
* In dev mode, calling again with the same handle replaces the compiled program.
|
|
1575
|
+
*
|
|
1576
|
+
* @experimental
|
|
1577
|
+
*/
|
|
1578
|
+
registerProgram(handle: ShaderProgramHandle): void;
|
|
1579
|
+
/**
|
|
1580
|
+
* The compiled program for `id`, compiling it against this context on first
|
|
1581
|
+
* use. Unlike {@link registerProgram} this neither throws on a missing
|
|
1582
|
+
* source nor on a compile failure: a paint kind asking for a program it
|
|
1583
|
+
* never registered must decline the paint, not take down the frame.
|
|
1584
|
+
*/
|
|
1585
|
+
private ensureProgram;
|
|
1586
|
+
/** The GL state every frame assumes. Applied per `render()` rather than once
|
|
1587
|
+
* at construction because a co-tenant sharing this context moves all of it
|
|
1588
|
+
* between our frames. */
|
|
1589
|
+
private applyGlState;
|
|
1590
|
+
/** Viewport and scissor for this frame. Re-applied inside `render()` because
|
|
1591
|
+
* a co-tenant on the same context moves both between our frames. */
|
|
1592
|
+
private applyTarget;
|
|
1593
|
+
/** Confine this renderer to a rect of its drawing buffer, or pass null to
|
|
1594
|
+
* give it the whole buffer back. Takes effect from the next `render()`. */
|
|
1595
|
+
setTarget(target: RenderTarget | null): void;
|
|
1596
|
+
getTarget(): RenderTarget | null;
|
|
1597
|
+
isContextLost(): boolean;
|
|
1598
|
+
private onContextLost;
|
|
1599
|
+
private onContextRestored;
|
|
1600
|
+
/** Free the GL resources this renderer itself owns and detach context-loss
|
|
1601
|
+
* listeners when a canvas was supplied. Idempotent.
|
|
1602
|
+
*
|
|
1603
|
+
* Scope: built-in shader programs, any consumer-registered programs, the
|
|
1604
|
+
* shared quad/rect geometry, any in-flight transient meshes, and the
|
|
1605
|
+
* enumerable Map-keyed caches (`GLTextureCache` atlas/image textures,
|
|
1606
|
+
* `GradientRampAtlas`'s ramp texture) ARE freed.
|
|
1607
|
+
*
|
|
1608
|
+
* NOT freed: `GLImageCache` (bitmap/pattern textures) and `GLMeshCache`'s
|
|
1609
|
+
* persistent per-Path mesh cache are keyed by `WeakMap`, not enumerable,
|
|
1610
|
+
* and are only reclaimed when the GL context itself goes away (cf.
|
|
1611
|
+
* `GLImageCache`'s own "deferred to v2" note). On a caller-owned
|
|
1612
|
+
* long-lived context — the headless render-to-pixels path hands the same
|
|
1613
|
+
* `gl` to many short-lived renderers — the image/pattern textures and
|
|
1614
|
+
* persistent Path meshes each renderer uploads DO accumulate across
|
|
1615
|
+
* renderer instances until the caller recycles the context.
|
|
1616
|
+
*
|
|
1617
|
+
* Also: a Mesh that gets GC'd after `dispose()` still lands in
|
|
1618
|
+
* `GLMeshCache`'s `pendingDeletes` queue via its `FinalizationRegistry`,
|
|
1619
|
+
* but nothing drains that queue post-dispose (only `render()` does) —
|
|
1620
|
+
* those GL resources leak until the context goes away too.
|
|
1621
|
+
*
|
|
1622
|
+
* A disposed renderer ignores further `render()` and `registerProgram()`
|
|
1623
|
+
* calls. */
|
|
1624
|
+
dispose(): void;
|
|
1625
|
+
/**
|
|
1626
|
+
* Draw one frame.
|
|
1627
|
+
*
|
|
1628
|
+
* `viewMatrix` is the frame's world→screen transform. It is only consulted
|
|
1629
|
+
* by `units: 'world'` gradients, which fall back to screen space without
|
|
1630
|
+
* it — every other command carries its own transform in the stream, so
|
|
1631
|
+
* callers with no view concept can keep calling `render(commands)`.
|
|
1632
|
+
*/
|
|
1633
|
+
render(commands: DrawCommand[], viewMatrix?: Mat3): void;
|
|
1634
|
+
resize(dims: {
|
|
1635
|
+
width: number;
|
|
1636
|
+
height: number;
|
|
1637
|
+
dpr: number;
|
|
1638
|
+
}): void;
|
|
1639
|
+
/** @internal */ _gl(): WebGL2RenderingContext;
|
|
1640
|
+
/** @internal */ _pathFill(): ShaderProgram;
|
|
1641
|
+
/** @internal */ _pathFillVColor(): ShaderProgram;
|
|
1642
|
+
/** @internal */ _drawBatch(): DrawBatch;
|
|
1643
|
+
/** @internal */ _imageFill(): ShaderProgram;
|
|
1644
|
+
/** @internal */ _batchFill(): ShaderProgram;
|
|
1645
|
+
/** @internal */ _gradFill(): ShaderProgram;
|
|
1646
|
+
/** @internal */ _patternFill(): ShaderProgram;
|
|
1647
|
+
/** @internal */ _meshCache(): GLMeshCache;
|
|
1648
|
+
/** @internal */ _textureCache(): GLTextureCache;
|
|
1649
|
+
/** @internal */ _imageCache(): GLImageCache;
|
|
1650
|
+
/** @internal */ _gradRamps(): GradientRampAtlas;
|
|
1651
|
+
/** @internal */ _groupState(): GroupState;
|
|
1652
|
+
/** @internal */ _widthCss(): number;
|
|
1653
|
+
/** @internal */ _heightCss(): number;
|
|
1654
|
+
/** @internal */ _dpr(): number;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Uniform-grid sprite sheet layout — frame index to the source rect an
|
|
1659
|
+
* `ImageDrawCommand` samples. Pure arithmetic over the grid description; it
|
|
1660
|
+
* never touches the bitmap.
|
|
1661
|
+
*/
|
|
1662
|
+
/** A sprite sheet's grid. `margin` and `spacing` follow the Tiled / Aseprite
|
|
1663
|
+
* tileset convention, so a sheet exported from either describes itself here. */
|
|
1664
|
+
interface SpriteSheet {
|
|
1665
|
+
frameWidth: number;
|
|
1666
|
+
frameHeight: number;
|
|
1667
|
+
columns: number;
|
|
1668
|
+
/** Empty pixels around the whole grid. Default 0. */
|
|
1669
|
+
margin?: number;
|
|
1670
|
+
/** Empty pixels between adjacent cells — the gutter that keeps linear
|
|
1671
|
+
* sampling off the neighboring frame. Default 0. */
|
|
1672
|
+
spacing?: number;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Row-major source rect for frame `index`, counting from 0 at the top-left.
|
|
1676
|
+
*
|
|
1677
|
+
* `index` is not range-checked and does not wrap: past the last cell this
|
|
1678
|
+
* returns a rect below the sheet, which draws as an edge smear. Wrapping is
|
|
1679
|
+
* the animation's business (`frameRect(sheet, tick % count)`) — a sheet does
|
|
1680
|
+
* not know how many of its cells are filled.
|
|
1681
|
+
*/
|
|
1682
|
+
declare function frameRect(sheet: SpriteSheet, index: number): {
|
|
1683
|
+
x: number;
|
|
1684
|
+
y: number;
|
|
1685
|
+
w: number;
|
|
1686
|
+
h: number;
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* View → Mat3 helper for layer `draw` implementations on world-space layers.
|
|
1691
|
+
*
|
|
1692
|
+
* The kit's main package ships a `View` type at
|
|
1693
|
+
* `src/core/viewport/view.ts`; we re-declare a structurally compatible
|
|
1694
|
+
* shape here to avoid a runtime cross-package import. Exported as
|
|
1695
|
+
* `ViewLike` from the package barrel.
|
|
1696
|
+
*/
|
|
1697
|
+
|
|
1698
|
+
/** A weasel View — `{x, y, scale: {x, y}}`. Local type to avoid a cross-package import. */
|
|
1699
|
+
interface View {
|
|
1700
|
+
x: number;
|
|
1701
|
+
y: number;
|
|
1702
|
+
scale: {
|
|
1703
|
+
x: number;
|
|
1704
|
+
y: number;
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Build the world→screen transform matrix for a `View`.
|
|
1709
|
+
* Use as the `transform` field of a `kind: 'group'` DrawCommand to wrap
|
|
1710
|
+
* world-space content emitted from a layer `draw` implementation.
|
|
1711
|
+
*
|
|
1712
|
+
* Mapping: `screen = (world − {view.x, view.y}) × {view.scale.x, view.scale.y}`.
|
|
1713
|
+
*
|
|
1714
|
+
* Column-major layout (matches `mat3.identity()`):
|
|
1715
|
+
* `[scale.x, 0, 0, 0, scale.y, 0, -view.x*scale.x, -view.y*scale.y, 1]`.
|
|
1716
|
+
*/
|
|
1717
|
+
declare function viewToMat3(view: View): Mat3;
|
|
1718
|
+
|
|
1719
|
+
/** Options for stroke tessellation. */
|
|
1720
|
+
interface StrokeOptions {
|
|
1721
|
+
flattenTolerance?: number;
|
|
1722
|
+
/**
|
|
1723
|
+
* Shorten each open subpath by this much at its start / end, in the same
|
|
1724
|
+
* world units as the path. Stroke markers use this so a filled head is not
|
|
1725
|
+
* speared by its own line; the caller resolves the distance, because this
|
|
1726
|
+
* layer knows nothing about the marker registry.
|
|
1727
|
+
*/
|
|
1728
|
+
startInset?: number;
|
|
1729
|
+
endInset?: number;
|
|
1730
|
+
}
|
|
1731
|
+
/** Resolve a stroke width to world units. A number is already world units;
|
|
1732
|
+
* `{ px }` is screen pixels divided by the accumulated scale, so it holds its
|
|
1733
|
+
* on-screen thickness as the view zooms. */
|
|
1734
|
+
declare function resolveStrokeWidth(width: number | {
|
|
1735
|
+
px: number;
|
|
1736
|
+
}, scale: number): number;
|
|
1737
|
+
/**
|
|
1738
|
+
* Build a triangle-mesh ribbon from a stroked Path.
|
|
1739
|
+
*
|
|
1740
|
+
* Supports:
|
|
1741
|
+
* - cap: 'butt' | 'round' | 'square'
|
|
1742
|
+
* - join: 'miter' | 'round' | 'bevel'
|
|
1743
|
+
* - center alignment directly; RectPath inner/outer alignment via a
|
|
1744
|
+
* pre-shifted rect; PolygonPath inner/outer alignment via stencil at the
|
|
1745
|
+
* renderer level
|
|
1746
|
+
* - dash splitting
|
|
1747
|
+
*/
|
|
1748
|
+
declare function tessellateStroke(path: Path, stroke: Stroke, opts?: StrokeOptions): Mesh;
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Effects that ship with the kit.
|
|
1752
|
+
*
|
|
1753
|
+
* Each is a function returning `Effect[]`, not a single `Effect`, because a
|
|
1754
|
+
* separable kernel is genuinely two passes and hiding that behind one entry
|
|
1755
|
+
* would make the cost invisible at the callsite:
|
|
1756
|
+
*
|
|
1757
|
+
* effects={[...blur({ radius: 4 }), ...vignette({ amount: 0.6 })]}
|
|
1758
|
+
*/
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Gaussian blur, as a horizontal pass followed by a vertical one.
|
|
1762
|
+
*
|
|
1763
|
+
* `radius` is in device pixels and is the distance of the outermost tap, not a
|
|
1764
|
+
* standard deviation — 0 is a copy, and the falloff is the same nine-tap
|
|
1765
|
+
* kernel at every radius, so a large one is a wide blur rather than a better
|
|
1766
|
+
* one. Two passes at O(9) beat one at O(81) and look the same.
|
|
1767
|
+
*/
|
|
1768
|
+
declare function blur({ radius }: {
|
|
1769
|
+
radius: number;
|
|
1770
|
+
}): Effect[];
|
|
1771
|
+
/**
|
|
1772
|
+
* Darken toward the corners. `amount` is how dark the corner gets (0..1) and
|
|
1773
|
+
* `feather` how much of the radius the falloff occupies.
|
|
1774
|
+
*/
|
|
1775
|
+
declare function vignette({ amount, feather }: {
|
|
1776
|
+
amount: number;
|
|
1777
|
+
feather?: number;
|
|
1778
|
+
}): Effect[];
|
|
1779
|
+
|
|
1780
|
+
/** Per-axis limits on a view's position. Any side may be left open. */
|
|
1781
|
+
interface PanBounds {
|
|
1782
|
+
minX?: number;
|
|
1783
|
+
maxX?: number;
|
|
1784
|
+
minY?: number;
|
|
1785
|
+
maxY?: number;
|
|
1786
|
+
}
|
|
1787
|
+
/** Momentum settings for a pan: how quickly a flung view slows, when it stops,
|
|
1788
|
+
* and what happens at the pan limits. The `DecayLoopConfig` fields a caller
|
|
1789
|
+
* chooses up front, without the per-gesture `velocity` / `onTick`. */
|
|
1790
|
+
interface InertiaConfig {
|
|
1791
|
+
friction?: number;
|
|
1792
|
+
minSpeed?: number;
|
|
1793
|
+
/** What to do when inertial pan reaches `bounds`. Default: no clamping. */
|
|
1794
|
+
boundary?: 'stop' | 'bounce' | 'spring';
|
|
1795
|
+
/** View-coordinate limits for boundary clamping. Requires `boundary` to take effect. */
|
|
1796
|
+
bounds?: PanBounds;
|
|
1797
|
+
}
|
|
1798
|
+
/** How a decay should run: its starting velocity, how fast it slows, and what
|
|
1799
|
+
* happens if it reaches the pan limits. */
|
|
1800
|
+
interface DecayLoopConfig {
|
|
1801
|
+
velocity: {
|
|
1802
|
+
vx: number;
|
|
1803
|
+
vy: number;
|
|
1804
|
+
};
|
|
1805
|
+
friction?: number;
|
|
1806
|
+
minSpeed?: number;
|
|
1807
|
+
/** Bounds for boundary clamping. Requires `boundary` to take effect. */
|
|
1808
|
+
viewBounds?: PanBounds;
|
|
1809
|
+
/**
|
|
1810
|
+
* What to do when the accumulated position hits `viewBounds`. Default: no clamping.
|
|
1811
|
+
* - `'stop'`: clamp at boundary, kill velocity component.
|
|
1812
|
+
* - `'bounce'`: linear reflection — flip velocity sign, magnitude preserved.
|
|
1813
|
+
* - `'spring'`: damped reflection — flip velocity sign and shrink magnitude
|
|
1814
|
+
* by `SPRING_DAMPING` per bounce so the motion settles naturally.
|
|
1815
|
+
*/
|
|
1816
|
+
boundary?: 'stop' | 'bounce' | 'spring';
|
|
1817
|
+
/** Starting position for internal boundary tracking. Required when `viewBounds` is set. */
|
|
1818
|
+
initialPosition?: {
|
|
1819
|
+
x: number;
|
|
1820
|
+
y: number;
|
|
1821
|
+
};
|
|
1822
|
+
onTick: (dx: number, dy: number) => void;
|
|
1823
|
+
onEnd?: () => void;
|
|
1824
|
+
}
|
|
1825
|
+
/** A rAF loop that coasts a value to a stop under friction, reporting the
|
|
1826
|
+
* per-frame delta. What turns a released pan drag into momentum scrolling. */
|
|
1827
|
+
declare function useDecayLoop(): {
|
|
1828
|
+
start: (config: DecayLoopConfig) => void;
|
|
1829
|
+
cancel: () => void;
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
/** Which of a node's two per-anchor color arrays an override applies to. */
|
|
1833
|
+
type VertexColorChannel = 'fill' | 'stroke';
|
|
1834
|
+
/** Function-form override: receives the consumer-supplied base color
|
|
1835
|
+
* array and the current animation timestamp (ms, from the animator's
|
|
1836
|
+
* clock). Returns a flat RGBA float array (values in 0..1, matching
|
|
1837
|
+
* the renderer's `stroke.vertexColors` / `PathDrawCommand.vertexColors`
|
|
1838
|
+
* color space) of the same length as `base`. */
|
|
1839
|
+
type ColorOverrideFn = (base: readonly number[], tMs: number) => number[];
|
|
1840
|
+
/** Either a static per-anchor RGBA float array (0..1) or a function-form
|
|
1841
|
+
* override (see {@link ColorOverrideFn}). */
|
|
1842
|
+
type ColorOverride = readonly number[] | ColorOverrideFn;
|
|
1843
|
+
/** Per-node, per-channel store of color overrides. Attached to `useAnimator` as
|
|
1844
|
+
* `animator.colorOverrides`; painted onto scene nodes by `<SceneCanvas animator>`
|
|
1845
|
+
* and onto a `createPathLayer`'s nodes by its `colorOverrides` option. */
|
|
1846
|
+
declare class ColorOverrideRegistry {
|
|
1847
|
+
private readonly map;
|
|
1848
|
+
private _version;
|
|
1849
|
+
set(id: string, channel: VertexColorChannel, override: ColorOverride): void;
|
|
1850
|
+
clear(id: string, channel: VertexColorChannel): void;
|
|
1851
|
+
clearAll(): void;
|
|
1852
|
+
get(id: string, channel: VertexColorChannel): ColorOverride | undefined;
|
|
1853
|
+
/** Whether anything overrides `id`, on either channel. */
|
|
1854
|
+
has(id: string): boolean;
|
|
1855
|
+
/**
|
|
1856
|
+
* The colors to paint on `id`'s `channel`, given the colors the painter would
|
|
1857
|
+
* otherwise paint and the frame's timestamp. A function override needs that
|
|
1858
|
+
* `base` and must return an array of its length; failing either, `base`
|
|
1859
|
+
* stands.
|
|
1860
|
+
*/
|
|
1861
|
+
resolve(id: string, channel: VertexColorChannel, base: readonly number[] | undefined, tMs: number): readonly number[] | undefined;
|
|
1862
|
+
version(): number;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/** One keyframe. `easing` shapes the approach INTO this key from the previous
|
|
1866
|
+
* one, so the first key's easing is never consulted. */
|
|
1867
|
+
interface Keyframe<T> {
|
|
1868
|
+
/** Time within the track's timeline, in ms. */
|
|
1869
|
+
t: number;
|
|
1870
|
+
value: T;
|
|
1871
|
+
/** A function, the name of a built-in, or cubic-bezier control points. */
|
|
1872
|
+
easing?: EasingSpec;
|
|
1873
|
+
}
|
|
1874
|
+
/** A track sampled as a pure function of the playhead. Scrubbing one is free
|
|
1875
|
+
* and order-independent. */
|
|
1876
|
+
interface SampledTrack<T> {
|
|
1877
|
+
kind: 'sampled';
|
|
1878
|
+
label?: string;
|
|
1879
|
+
/** Sorted ascending by `t`. `sampleTrack` assumes this and does not sort. */
|
|
1880
|
+
keys: Keyframe<T>[];
|
|
1881
|
+
/** Required when T is not `number`; defaults to numeric lerp otherwise. */
|
|
1882
|
+
interpolate?: Interpolate<T>;
|
|
1883
|
+
/** Built once per segment and cached. Takes precedence over `interpolate`. */
|
|
1884
|
+
interpolator?: InterpolatorFactory<T>;
|
|
1885
|
+
onTick: (value: T) => void;
|
|
1886
|
+
}
|
|
1887
|
+
/** A clock that events are booked against, reading in ms. An `AudioEngine`
|
|
1888
|
+
* from `@weasel-js/audio` is one. */
|
|
1889
|
+
interface TimelineClock {
|
|
1890
|
+
now(): number;
|
|
1891
|
+
}
|
|
1892
|
+
/** What `book` hands back so the booking can be retracted. A `VoiceHandle` from
|
|
1893
|
+
* `@weasel-js/audio` is one. */
|
|
1894
|
+
interface EventBookingHandle {
|
|
1895
|
+
stop(): void;
|
|
1896
|
+
}
|
|
1897
|
+
interface TimelineEvent {
|
|
1898
|
+
t: number;
|
|
1899
|
+
/** Runs on the frame that crosses the edge, told how far behind the frame it
|
|
1900
|
+
* was crossed, in ms — never negative, and measured against `duration` on
|
|
1901
|
+
* the loop seam, where the outgoing lap's tail fires after the wrap. */
|
|
1902
|
+
fire?: (lateBy: number) => void;
|
|
1903
|
+
/** Runs up to `booking.lookahead` before the edge on a timeline with a
|
|
1904
|
+
* `booking`, once per crossing, with the `booking.clock` time the edge lands
|
|
1905
|
+
* at — never earlier than that clock's `now()`. A returned handle is stopped
|
|
1906
|
+
* if a pause, seek, rate change, loop change, edit or cancel invalidates the
|
|
1907
|
+
* booking before the clock reaches `when`; the event is then booked again
|
|
1908
|
+
* wherever playback next reaches it. A booking with no handle stands. */
|
|
1909
|
+
book?: (when: number) => EventBookingHandle | void;
|
|
1910
|
+
}
|
|
1911
|
+
/** A track of edge crossings, in forward playback only — a `seek` neither
|
|
1912
|
+
* fires nor books the span it skips. */
|
|
1913
|
+
interface EventTrack {
|
|
1914
|
+
kind: 'event';
|
|
1915
|
+
label?: string;
|
|
1916
|
+
/** Sorted ascending by `t`. */
|
|
1917
|
+
events: TimelineEvent[];
|
|
1918
|
+
}
|
|
1919
|
+
interface EventBooking {
|
|
1920
|
+
clock: TimelineClock;
|
|
1921
|
+
/** How far ahead of the playhead to book, in clock ms. Default 100. An event
|
|
1922
|
+
* first reached later than this — after a frame longer than it — books late. */
|
|
1923
|
+
lookahead?: number;
|
|
1924
|
+
/** An event first reached more than this many clock ms after its edge is
|
|
1925
|
+
* skipped instead of booked late. Default `Infinity`. */
|
|
1926
|
+
maxLate?: number;
|
|
1927
|
+
}
|
|
1928
|
+
/** A nested timeline, evaluated at `playhead - at`. Children are NOT registered
|
|
1929
|
+
* with the animator separately; the parent evaluates them. */
|
|
1930
|
+
interface TimelineTrack {
|
|
1931
|
+
kind: 'timeline';
|
|
1932
|
+
label?: string;
|
|
1933
|
+
at: number;
|
|
1934
|
+
timeline: NestedTimeline;
|
|
1935
|
+
}
|
|
1936
|
+
type Track = SampledTrack<any> | EventTrack | TimelineTrack;
|
|
1937
|
+
/** What a child timeline may declare. The parent owns playback, so `loop`,
|
|
1938
|
+
* `autoplay`, `onDone` and `cancelKey` have no meaning below the root. */
|
|
1939
|
+
interface NestedTimeline {
|
|
1940
|
+
tracks: Track[];
|
|
1941
|
+
/** Defaults to the largest end time across `tracks`. */
|
|
1942
|
+
duration?: number;
|
|
1943
|
+
}
|
|
1944
|
+
interface TimelineOptions extends NestedTimeline {
|
|
1945
|
+
/** `true` loops forever, `n` loops n additional times. Default false. */
|
|
1946
|
+
loop?: boolean | number;
|
|
1947
|
+
/** Default true. When false the timeline registers but holds at t=0 until resumed. */
|
|
1948
|
+
autoplay?: boolean;
|
|
1949
|
+
onDone?: () => void;
|
|
1950
|
+
cancelKey?: string;
|
|
1951
|
+
/** Books each event's `book` against a clock the timeline does not own, so a
|
|
1952
|
+
* consumer on that clock lands it at its true sub-frame time. The frame
|
|
1953
|
+
* clock's mapping onto it is smoothed each frame and resynced on a jump. */
|
|
1954
|
+
booking?: EventBooking;
|
|
1955
|
+
}
|
|
1956
|
+
interface TimelineHandle extends AnimationHandle {
|
|
1957
|
+
/** Move the playhead. Never fires event tracks, at any depth. */
|
|
1958
|
+
seek(t: number): void;
|
|
1959
|
+
/** Change the loop policy. `true` loops forever, `n` allows n more laps,
|
|
1960
|
+
* `false` stops at `duration`. Sets policy only — a timeline already parked
|
|
1961
|
+
* at `duration` does not restart, because `rearm` declines to revive one.
|
|
1962
|
+
* Rewind it with `seek(0)` and `resume()` to play it again. */
|
|
1963
|
+
setLoop(loop: boolean | number): void;
|
|
1964
|
+
/** The loop policy as it now stands: `true` endless, `false` stopping at
|
|
1965
|
+
* `duration`, `n` for n laps still allowed. A finite count falls as laps
|
|
1966
|
+
* are consumed, matching what `setLoop` takes. */
|
|
1967
|
+
loop(): boolean | number;
|
|
1968
|
+
/** Current playhead in ms. */
|
|
1969
|
+
time(): number;
|
|
1970
|
+
duration(): number;
|
|
1971
|
+
tracks(): readonly Track[];
|
|
1972
|
+
/** Run `fn`, then recompute duration, drop cached interpolators, and notify.
|
|
1973
|
+
* Every mutation must go through this — an edited keyframe otherwise keeps
|
|
1974
|
+
* interpolating toward its old value with no visible error. */
|
|
1975
|
+
edit(fn: () => void): void;
|
|
1976
|
+
/** Notified after each `edit`. Returns an unsubscribe. */
|
|
1977
|
+
subscribe(cb: () => void): () => void;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
/** No easing: constant rate from start to finish. */
|
|
1981
|
+
declare const linear: EasingFn;
|
|
1982
|
+
/** Accelerates from a standstill, gently. */
|
|
1983
|
+
declare const easeInQuad: EasingFn;
|
|
1984
|
+
/** Decelerates to a stop, gently. The safe default for UI motion. */
|
|
1985
|
+
declare const easeOutQuad: EasingFn;
|
|
1986
|
+
/** Accelerates then decelerates, gently. */
|
|
1987
|
+
declare const easeInOutQuad: EasingFn;
|
|
1988
|
+
/** Accelerates from a standstill, moderately. */
|
|
1989
|
+
declare const easeInCubic: EasingFn;
|
|
1990
|
+
/** Decelerates to a stop, moderately. */
|
|
1991
|
+
declare const easeOutCubic: EasingFn;
|
|
1992
|
+
/** Accelerates then decelerates, moderately. */
|
|
1993
|
+
declare const easeInOutCubic: EasingFn;
|
|
1994
|
+
/** Accelerates from a standstill, sharply. */
|
|
1995
|
+
declare const easeInQuart: EasingFn;
|
|
1996
|
+
/** Decelerates to a stop, sharply. */
|
|
1997
|
+
declare const easeOutQuart: EasingFn;
|
|
1998
|
+
/** Accelerates then decelerates, sharply. */
|
|
1999
|
+
declare const easeInOutQuart: EasingFn;
|
|
2000
|
+
/** Accelerates from a standstill, very sharply. */
|
|
2001
|
+
declare const easeInQuint: EasingFn;
|
|
2002
|
+
/** Decelerates to a stop, very sharply. */
|
|
2003
|
+
declare const easeOutQuint: EasingFn;
|
|
2004
|
+
/** Accelerates then decelerates, very sharply. */
|
|
2005
|
+
declare const easeInOutQuint: EasingFn;
|
|
2006
|
+
/** Accelerates from a standstill along a sine curve — the mildest
|
|
2007
|
+
* acceleration of the built-ins. */
|
|
2008
|
+
declare const easeInSine: EasingFn;
|
|
2009
|
+
/** Decelerates to a stop along a sine curve — the mildest
|
|
2010
|
+
* deceleration of the built-ins. */
|
|
2011
|
+
declare const easeOutSine: EasingFn;
|
|
2012
|
+
/** Accelerates then decelerates along a sine curve. */
|
|
2013
|
+
declare const easeInOutSine: EasingFn;
|
|
2014
|
+
/** Accelerates exponentially: barely moves at first, then rushes. */
|
|
2015
|
+
declare const easeInExpo: EasingFn;
|
|
2016
|
+
/** Decelerates exponentially: leaps away, then creeps in. */
|
|
2017
|
+
declare const easeOutExpo: EasingFn;
|
|
2018
|
+
/** Exponential at both ends — a very fast middle between two
|
|
2019
|
+
* near-still extremes. */
|
|
2020
|
+
declare const easeInOutExpo: EasingFn;
|
|
2021
|
+
/** Accelerates along a circular arc: slow start, abrupt arrival. */
|
|
2022
|
+
declare const easeInCirc: EasingFn;
|
|
2023
|
+
/** Decelerates along a circular arc: abrupt start, slow arrival. */
|
|
2024
|
+
declare const easeOutCirc: EasingFn;
|
|
2025
|
+
/** Circular arcs at both ends. */
|
|
2026
|
+
declare const easeInOutCirc: EasingFn;
|
|
2027
|
+
/** Pulls back past the start before moving forward. Overshoots below 0. */
|
|
2028
|
+
declare const easeInBack: EasingFn;
|
|
2029
|
+
/** Overshoots the target, then settles back onto it. Exceeds 1. */
|
|
2030
|
+
declare const easeOutBack: EasingFn;
|
|
2031
|
+
/** Overshoots at both ends. Leaves the 0–1 range on each side. */
|
|
2032
|
+
declare const easeInOutBack: EasingFn;
|
|
2033
|
+
/** Oscillates around the start with growing amplitude, then snaps away. */
|
|
2034
|
+
declare const easeInElastic: EasingFn;
|
|
2035
|
+
/** Springs past the target and wobbles into it. Exceeds 1. */
|
|
2036
|
+
declare const easeOutElastic: EasingFn;
|
|
2037
|
+
/** Wobbles at both ends. Leaves the 0–1 range on each side. */
|
|
2038
|
+
declare const easeInOutElastic: EasingFn;
|
|
2039
|
+
/** Lands on the target and bounces, in hops of decreasing height. */
|
|
2040
|
+
declare const easeOutBounce: EasingFn;
|
|
2041
|
+
/** Bounces up to the start before departing — `easeOutBounce` reversed. */
|
|
2042
|
+
declare const easeInBounce: EasingFn;
|
|
2043
|
+
/** Bounces at both ends. */
|
|
2044
|
+
declare const easeInOutBounce: EasingFn;
|
|
2045
|
+
/** Alias for `easeInQuad`, kept for call sites that predate the
|
|
2046
|
+
* named-curve library. */
|
|
2047
|
+
declare const easeIn: EasingFn;
|
|
2048
|
+
/** Alias for `easeOutQuad`, kept for call sites that predate the
|
|
2049
|
+
* named-curve library. */
|
|
2050
|
+
declare const easeOut: EasingFn;
|
|
2051
|
+
/** Alias for `easeInOutQuad`, kept for call sites that predate the
|
|
2052
|
+
* named-curve library. */
|
|
2053
|
+
declare const easeInOut: EasingFn;
|
|
2054
|
+
/** All easings in one bag — useful for demos / pickers. */
|
|
2055
|
+
declare const EASINGS: {
|
|
2056
|
+
readonly linear: EasingFn;
|
|
2057
|
+
readonly easeInQuad: EasingFn;
|
|
2058
|
+
readonly easeOutQuad: EasingFn;
|
|
2059
|
+
readonly easeInOutQuad: EasingFn;
|
|
2060
|
+
readonly easeInCubic: EasingFn;
|
|
2061
|
+
readonly easeOutCubic: EasingFn;
|
|
2062
|
+
readonly easeInOutCubic: EasingFn;
|
|
2063
|
+
readonly easeInQuart: EasingFn;
|
|
2064
|
+
readonly easeOutQuart: EasingFn;
|
|
2065
|
+
readonly easeInOutQuart: EasingFn;
|
|
2066
|
+
readonly easeInQuint: EasingFn;
|
|
2067
|
+
readonly easeOutQuint: EasingFn;
|
|
2068
|
+
readonly easeInOutQuint: EasingFn;
|
|
2069
|
+
readonly easeInSine: EasingFn;
|
|
2070
|
+
readonly easeOutSine: EasingFn;
|
|
2071
|
+
readonly easeInOutSine: EasingFn;
|
|
2072
|
+
readonly easeInExpo: EasingFn;
|
|
2073
|
+
readonly easeOutExpo: EasingFn;
|
|
2074
|
+
readonly easeInOutExpo: EasingFn;
|
|
2075
|
+
readonly easeInCirc: EasingFn;
|
|
2076
|
+
readonly easeOutCirc: EasingFn;
|
|
2077
|
+
readonly easeInOutCirc: EasingFn;
|
|
2078
|
+
readonly easeInBack: EasingFn;
|
|
2079
|
+
readonly easeOutBack: EasingFn;
|
|
2080
|
+
readonly easeInOutBack: EasingFn;
|
|
2081
|
+
readonly easeInElastic: EasingFn;
|
|
2082
|
+
readonly easeOutElastic: EasingFn;
|
|
2083
|
+
readonly easeInOutElastic: EasingFn;
|
|
2084
|
+
readonly easeInBounce: EasingFn;
|
|
2085
|
+
readonly easeOutBounce: EasingFn;
|
|
2086
|
+
readonly easeInOutBounce: EasingFn;
|
|
2087
|
+
};
|
|
2088
|
+
/** The name of one of the built-in easing curves. */
|
|
2089
|
+
type EasingName = keyof typeof EASINGS;
|
|
2090
|
+
/** Named spring tunings, from softest to firmest. Springs settle on a target
|
|
2091
|
+
* rather than running for a fixed duration, so these are an alternative to an
|
|
2092
|
+
* easing curve, not a modifier on one. */
|
|
2093
|
+
declare const SPRING_PRESETS: Record<SpringPresetName, SpringPreset>;
|
|
2094
|
+
|
|
2095
|
+
/** Cubic-bezier control points, CSS `cubic-bezier()` order. The curve's two
|
|
2096
|
+
* endpoints are implicit at (0,0) and (1,1). */
|
|
2097
|
+
interface BezierEasing {
|
|
2098
|
+
/** `readonly` so an `as const` preset is assignable; nothing ever writes it. */
|
|
2099
|
+
bezier: readonly [number, number, number, number];
|
|
2100
|
+
}
|
|
2101
|
+
/** An easing curve as a value: a function, the name of a built-in, or control
|
|
2102
|
+
* points. Anything an editor has to name, show or serialize must not be a bare
|
|
2103
|
+
* function, which is why the union exists. */
|
|
2104
|
+
type EasingSpec = EasingFn | EasingName | BezierEasing;
|
|
2105
|
+
/** Build the easing curve for four cubic-bezier control points. `x1`/`x2` are
|
|
2106
|
+
* clamped to [0,1] — CSS `cubic-bezier()`'s constraint for a monotone x(t),
|
|
2107
|
+
* which both `solveForX` root-finders assume. `y1`/`y2` are unclamped: an
|
|
2108
|
+
* overshoot easing (back, elastic) needs them outside 0..1. */
|
|
2109
|
+
declare function cubicBezierEasing(x1: number, y1: number, x2: number, y2: number): EasingFn;
|
|
2110
|
+
/** Resolve a spec to the function that shapes progress. `undefined` is linear. */
|
|
2111
|
+
declare function resolveEasing(spec?: EasingSpec): EasingFn;
|
|
2112
|
+
|
|
2113
|
+
/** An easing curve: maps normalized progress `t ∈ [0, 1]` to eased progress.
|
|
2114
|
+
* Curves may leave the 0–1 range in the middle (back, elastic) but should
|
|
2115
|
+
* pass through 0 at 0 and 1 at 1. */
|
|
2116
|
+
type EasingFn = (t: number) => number;
|
|
2117
|
+
|
|
2118
|
+
/** Blends two `T` values at eased progress `t`. Called once per frame; see
|
|
2119
|
+
* {@link InterpolatorFactory} when the blend has setup worth hoisting. */
|
|
2120
|
+
type Interpolate<T> = (from: T, to: T, t: number) => T;
|
|
2121
|
+
/** Factory interpolator: built ONCE at tween start with (from, to), the returned
|
|
2122
|
+
* function is called with `t ∈ [0, 1]` each frame. Use for interpolators with
|
|
2123
|
+
* expensive setup (color-space conversion, path-string parsing) — d3-interpolate's
|
|
2124
|
+
* shape exactly. For cheap interpolations the per-tick `Interpolate<T>` form is
|
|
2125
|
+
* fine; this is the escape hatch when setup-per-tick is wasteful. */
|
|
2126
|
+
type InterpolatorFactory<T> = (from: T, to: T) => (t: number) => T;
|
|
2127
|
+
/** A spring's physical parameters. Higher stiffness settles faster, higher
|
|
2128
|
+
* damping overshoots less, higher mass makes both sluggish. */
|
|
2129
|
+
interface SpringPreset {
|
|
2130
|
+
stiffness: number;
|
|
2131
|
+
damping: number;
|
|
2132
|
+
mass: number;
|
|
2133
|
+
}
|
|
2134
|
+
/** One of the tunings in `SPRING_PRESETS`. */
|
|
2135
|
+
type SpringPresetName = 'gentle' | 'wobbly' | 'stiff' | 'slow';
|
|
2136
|
+
/** A running animation. Cancel it, or bend its time — pausing and time-scaling
|
|
2137
|
+
* act on this animation's own virtual clock, independent of the animator's. */
|
|
2138
|
+
interface AnimationHandle {
|
|
2139
|
+
/** Monotonic id assigned by the animator. */
|
|
2140
|
+
id: number;
|
|
2141
|
+
/** Cancel this animation. Idempotent — no-op once already finished/canceled. */
|
|
2142
|
+
cancel(): void;
|
|
2143
|
+
/** Freeze this animation's virtual clock. Idempotent. */
|
|
2144
|
+
pause(): void;
|
|
2145
|
+
/** Resume this animation's virtual clock. Idempotent. */
|
|
2146
|
+
resume(): void;
|
|
2147
|
+
/** Multiply this animation's virtual-clock rate by `scale`. 1 = normal. */
|
|
2148
|
+
setTimeScale(scale: number): void;
|
|
2149
|
+
/** This animation's own time scale. 1 once it has finished, since the
|
|
2150
|
+
* animator no longer holds an entry to read. */
|
|
2151
|
+
timeScale(): number;
|
|
2152
|
+
/** True iff this handle is currently paused. */
|
|
2153
|
+
isPaused(): boolean;
|
|
2154
|
+
}
|
|
2155
|
+
/** A duration-based animation from `from` to `to` over `ms`, shaped by an
|
|
2156
|
+
* easing curve. Reach for a spring instead when the motion should respond to
|
|
2157
|
+
* where the value already is rather than restart from a fixed duration. */
|
|
2158
|
+
interface TweenOptions<T> {
|
|
2159
|
+
from: T;
|
|
2160
|
+
to: T;
|
|
2161
|
+
ms: number;
|
|
2162
|
+
easing?: EasingSpec;
|
|
2163
|
+
/** Required when T is not `number`. For T = number, defaults to linear numeric lerp.
|
|
2164
|
+
* Called per-tick with `(from, to, t)`. For interpolators with expensive setup,
|
|
2165
|
+
* prefer `interpolator` which is built once at tween start. */
|
|
2166
|
+
interpolate?: Interpolate<T>;
|
|
2167
|
+
/** Factory interpolator built once at tween start. Takes precedence over
|
|
2168
|
+
* `interpolate` when both are provided. Use this for d3-interpolate or any
|
|
2169
|
+
* `(from, to) => (t) => v` shape. */
|
|
2170
|
+
interpolator?: InterpolatorFactory<T>;
|
|
2171
|
+
onTick: (value: T) => void;
|
|
2172
|
+
onDone?: () => void;
|
|
2173
|
+
/** Any new animation passed the same cancelKey cancels the prior one in flight. */
|
|
2174
|
+
cancelKey?: string;
|
|
2175
|
+
}
|
|
2176
|
+
/** A spring animation: runs until the value settles on `to` rather than for a
|
|
2177
|
+
* set duration, so it absorbs an initial velocity naturally. Non-numeric `T`
|
|
2178
|
+
* needs the four vector helpers. */
|
|
2179
|
+
interface SpringOptions<T> {
|
|
2180
|
+
from: T;
|
|
2181
|
+
to: T;
|
|
2182
|
+
/** Initial velocity in T-units per second. Default: zero (T-shape-aware). */
|
|
2183
|
+
velocity?: T;
|
|
2184
|
+
preset?: SpringPresetName;
|
|
2185
|
+
stiffness?: number;
|
|
2186
|
+
damping?: number;
|
|
2187
|
+
mass?: number;
|
|
2188
|
+
interpolate?: Interpolate<T>;
|
|
2189
|
+
/** Vector helpers — required for non-numeric T. */
|
|
2190
|
+
add?: (a: T, b: T) => T;
|
|
2191
|
+
subtract?: (a: T, b: T) => T;
|
|
2192
|
+
scale?: (v: T, k: number) => T;
|
|
2193
|
+
magnitude?: (v: T) => number;
|
|
2194
|
+
/** Velocity magnitude below which the spring is considered settled. Default 0.01. */
|
|
2195
|
+
restThreshold?: number;
|
|
2196
|
+
onTick: (value: T) => void;
|
|
2197
|
+
onDone?: () => void;
|
|
2198
|
+
cancelKey?: string;
|
|
2199
|
+
}
|
|
2200
|
+
/** Spring and decay as one animation. With a `to`, a spring pulls toward it;
|
|
2201
|
+
* with `to: null`, the value coasts on its velocity. Either can become the
|
|
2202
|
+
* other mid-flight through the handle. */
|
|
2203
|
+
interface PhysicsOptions<T> {
|
|
2204
|
+
from: T;
|
|
2205
|
+
/** Target. `null` ⇒ no spring force (decay-mode). */
|
|
2206
|
+
to?: T | null;
|
|
2207
|
+
/** Initial velocity in T-units per second. */
|
|
2208
|
+
velocity?: T;
|
|
2209
|
+
preset?: SpringPresetName;
|
|
2210
|
+
stiffness?: number;
|
|
2211
|
+
damping?: number;
|
|
2212
|
+
mass?: number;
|
|
2213
|
+
restThreshold?: number;
|
|
2214
|
+
/** Vector helpers — required for non-numeric T. */
|
|
2215
|
+
add?: (a: T, b: T) => T;
|
|
2216
|
+
subtract?: (a: T, b: T) => T;
|
|
2217
|
+
scale?: (v: T, k: number) => T;
|
|
2218
|
+
magnitude?: (v: T) => number;
|
|
2219
|
+
onTick: (value: T) => void;
|
|
2220
|
+
onDone?: () => void;
|
|
2221
|
+
cancelKey?: string;
|
|
2222
|
+
}
|
|
2223
|
+
/** An `AnimationHandle` that can also be steered while it runs — the point of
|
|
2224
|
+
* the physics primitive. */
|
|
2225
|
+
interface PhysicsHandle<T = unknown> extends AnimationHandle {
|
|
2226
|
+
/** Retarget mid-flight. `null` ⇒ switch to decay-mode (no spring force). */
|
|
2227
|
+
setTarget(to: T | null): void;
|
|
2228
|
+
/** Replace the current velocity in T-units per second. */
|
|
2229
|
+
setVelocity(v: T): void;
|
|
2230
|
+
}
|
|
2231
|
+
/** Momentum: coast from `from` at `velocity`, slowing by `friction` each
|
|
2232
|
+
* second until below `threshold`. What a flick-to-pan leaves behind. */
|
|
2233
|
+
interface DecayOptions<T> {
|
|
2234
|
+
from: T;
|
|
2235
|
+
velocity: T;
|
|
2236
|
+
/** Per-second velocity multiplier in (0, 1). Default 0.95. */
|
|
2237
|
+
friction?: number;
|
|
2238
|
+
/** Velocity magnitude below which decay stops. Default 0.5. */
|
|
2239
|
+
threshold?: number;
|
|
2240
|
+
add: (a: T, b: T) => T;
|
|
2241
|
+
scale: (v: T, k: number) => T;
|
|
2242
|
+
magnitude: (v: T) => number;
|
|
2243
|
+
onTick: (value: T) => void;
|
|
2244
|
+
onDone?: () => void;
|
|
2245
|
+
cancelKey?: string;
|
|
2246
|
+
}
|
|
2247
|
+
/** Options for `useAnimator`. Everything here is an injection seam for tests;
|
|
2248
|
+
* the defaults are the real clock, rAF, and `setTimeout`. */
|
|
2249
|
+
interface UseAnimatorOptions {
|
|
2250
|
+
/** Optional clock injection for tests. Returns ms since some epoch. */
|
|
2251
|
+
now?: () => number;
|
|
2252
|
+
/** Optional rAF / cAF injection for tests. Defaults to window.requestAnimationFrame. */
|
|
2253
|
+
requestFrame?: (cb: (t: number) => void) => number;
|
|
2254
|
+
cancelFrame?: (handle: number) => void;
|
|
2255
|
+
/** Optional `setTimeout` injection used by `stagger` for per-item delays.
|
|
2256
|
+
* Defaults to the global `setTimeout`. Tests inject a virtual scheduler. */
|
|
2257
|
+
setTimer?: (cb: () => void, ms: number) => unknown;
|
|
2258
|
+
/** Companion to `setTimer`. Defaults to global `clearTimeout`. */
|
|
2259
|
+
clearTimer?: (handle: unknown) => void;
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* Owns every running animation on a canvas and drives them from one rAF loop.
|
|
2263
|
+
* Beyond the primitives (`tween`, `spring`, `decay`, `physics`) it offers
|
|
2264
|
+
* composition — `loop`, `stagger` — and bulk control by handle, by cancel-key,
|
|
2265
|
+
* or over everything at once.
|
|
2266
|
+
*
|
|
2267
|
+
* An animator does not know about the scene: animations report values through
|
|
2268
|
+
* `onTick` and the caller decides what to do with them.
|
|
2269
|
+
*/
|
|
2270
|
+
interface Animator {
|
|
2271
|
+
tween<T>(opts: TweenOptions<T>): AnimationHandle;
|
|
2272
|
+
spring<T>(opts: SpringOptions<T>): AnimationHandle;
|
|
2273
|
+
decay<T>(opts: DecayOptions<T>): AnimationHandle;
|
|
2274
|
+
/** Unified spring/decay primitive. With `to` set, behaves as a spring;
|
|
2275
|
+
* with `to: null`, behaves as a velocity-driven decay. Supports
|
|
2276
|
+
* mid-flight retargeting via the returned handle's `setTarget`. */
|
|
2277
|
+
physics<T>(opts: PhysicsOptions<T>): PhysicsHandle<T>;
|
|
2278
|
+
/** Cancel a specific animation by handle. Pose stays at current value (no jump). */
|
|
2279
|
+
cancel(handle: AnimationHandle): void;
|
|
2280
|
+
/** Cancel every animation currently active under `key`. */
|
|
2281
|
+
cancelKey(key: string): void;
|
|
2282
|
+
/** Cancel everything. Useful from a destructor or "reset scene" path. */
|
|
2283
|
+
cancelAll(): void;
|
|
2284
|
+
/** True iff at least one animation is active. With `key`, scoped to that cancelKey. */
|
|
2285
|
+
isActive(key?: string): boolean;
|
|
2286
|
+
/**
|
|
2287
|
+
* True while the animator is currently executing an animation tick. Useful
|
|
2288
|
+
* for adapter wrappers (e.g. `animateOnSetPose`) that need to detect
|
|
2289
|
+
* "this `setPose` was called from inside another animation's onTick"
|
|
2290
|
+
* (momentum decay, in-flight tween, spring) and avoid recursively
|
|
2291
|
+
* scheduling a new wrap-animation that would fight the caller.
|
|
2292
|
+
*/
|
|
2293
|
+
isTicking(): boolean;
|
|
2294
|
+
/** Freeze every animation managed by this animator. */
|
|
2295
|
+
pause(): void;
|
|
2296
|
+
/** Resume every animation managed by this animator. */
|
|
2297
|
+
resume(): void;
|
|
2298
|
+
/** True iff the animator is currently globally paused. */
|
|
2299
|
+
isPaused(): boolean;
|
|
2300
|
+
/** Multiply every animation's virtual-clock rate by `scale`. 1 = normal. */
|
|
2301
|
+
setTimeScale(scale: number): void;
|
|
2302
|
+
/** The global time scale. Per-animation scales multiply on top of it. */
|
|
2303
|
+
timeScale(): number;
|
|
2304
|
+
/** Freeze every animation whose `cancelKey` matches. */
|
|
2305
|
+
pauseKey(key: string): void;
|
|
2306
|
+
/** Resume every animation whose `cancelKey` matches. */
|
|
2307
|
+
resumeKey(key: string): void;
|
|
2308
|
+
/** Set per-animation timeScale for every animation whose `cancelKey` matches. */
|
|
2309
|
+
setTimeScaleByKey(key: string, scale: number): void;
|
|
2310
|
+
/**
|
|
2311
|
+
* Loop primitive: repeatedly invoke `factory` to produce a child animation.
|
|
2312
|
+
* The factory must wire its returned handle's `onDone` to call `next` so
|
|
2313
|
+
* the loop advances. Returns a handle whose pause/resume/setTimeScale/cancel
|
|
2314
|
+
* delegate to the current in-flight child (and prevent future iterations
|
|
2315
|
+
* on cancel).
|
|
2316
|
+
*
|
|
2317
|
+
* The loop is registered with the animator under a supervisor entry so
|
|
2318
|
+
* `animator.cancel(handle)`, `animator.cancelKey(opts.cancelKey)`, and
|
|
2319
|
+
* `animator.isActive(opts.cancelKey)` all work for it.
|
|
2320
|
+
*/
|
|
2321
|
+
loop(factory: LoopFactory, opts?: LoopOptions): AnimationHandle;
|
|
2322
|
+
/** Sugar over `loop` for the common case of looping a tween between two
|
|
2323
|
+
* values with optional direction handling (`restart` | `reverse` |
|
|
2324
|
+
* `alternate`). Registered with the animator like `loop`. */
|
|
2325
|
+
tweenLoop<T>(opts: TweenLoopOptions<T>): AnimationHandle;
|
|
2326
|
+
/**
|
|
2327
|
+
* Stagger primitive: schedule a per-item animation, offset by `delay` ms
|
|
2328
|
+
* per index (or a custom function of the index). Two forms:
|
|
2329
|
+
* - Factory form: pass `factory` directly, returns a composite
|
|
2330
|
+
* `AnimationHandle`.
|
|
2331
|
+
* - Builder form: omit `factory`, get a `StaggerBuilder` for fluent
|
|
2332
|
+
* `.each` / `.tween` / `.springPose` calls.
|
|
2333
|
+
*
|
|
2334
|
+
* The composite handle's `cancel` cancels pending timers AND in-flight
|
|
2335
|
+
* children. `pause` / `resume` / `setTimeScale` propagate to in-flight
|
|
2336
|
+
* children; `pause`/`resume` also freeze and thaw pending per-item timers
|
|
2337
|
+
* (the remaining time before each pending fire is preserved across the
|
|
2338
|
+
* pause).
|
|
2339
|
+
*
|
|
2340
|
+
* The stagger is registered with the animator under a supervisor entry so
|
|
2341
|
+
* `animator.cancel(handle)`, `animator.cancelKey(opts.cancelKey)`, and
|
|
2342
|
+
* `animator.isActive(opts.cancelKey)` all work for it.
|
|
2343
|
+
*/
|
|
2344
|
+
stagger<TItem>(items: readonly TItem[], delay: StaggerDelay): StaggerBuilder<TItem>;
|
|
2345
|
+
stagger<TItem>(items: readonly TItem[], delay: StaggerDelay, factory: StaggerFactory<TItem>, opts?: StaggerOptions): AnimationHandle;
|
|
2346
|
+
/**
|
|
2347
|
+
* Keyframe timeline. Registered like any other animation, so its playhead
|
|
2348
|
+
* responds to `pause`, `setTimeScale` and `cancelKey`. Sampled tracks are a
|
|
2349
|
+
* pure function of the playhead; event tracks fire only on forward playback.
|
|
2350
|
+
*/
|
|
2351
|
+
timeline(opts: TimelineOptions): TimelineHandle;
|
|
2352
|
+
/** Per-node, per-channel color override registry, painted onto scene nodes
|
|
2353
|
+
* by `<SceneCanvas animator>` and read by `createPathLayer`. Used by `tweenVertexColors`,
|
|
2354
|
+
* `springVertexColors`, `cycleVertexColors`, `staggerVertexColors`. Cleared
|
|
2355
|
+
* automatically on animator unmount. */
|
|
2356
|
+
colorOverrides: ColorOverrideRegistry;
|
|
2357
|
+
/**
|
|
2358
|
+
* Subscribe to a callback fired once per RAF frame while any animation is
|
|
2359
|
+
* active. Returns an unsubscribe function. Used by consumers (typically
|
|
2360
|
+
* `<SceneCanvas>`) that need to repaint when an animation's side-effect
|
|
2361
|
+
* is read from a non-scene channel (e.g. `colorOverrides` consulted from
|
|
2362
|
+
* a custom `drawOne`) — scene mutations naturally trigger a repaint, but
|
|
2363
|
+
* `colorOverrides` writes do not.
|
|
2364
|
+
*
|
|
2365
|
+
* The callback fires AFTER the per-frame tick of each registered
|
|
2366
|
+
* animation, so by the time it runs `colorOverrides.get(...)` returns
|
|
2367
|
+
* the latest values. If no animations are active, no tick fires.
|
|
2368
|
+
*/
|
|
2369
|
+
onTick(cb: () => void): () => void;
|
|
2370
|
+
/**
|
|
2371
|
+
* Keep the animator's RAF loop running until the returned cancel
|
|
2372
|
+
* function is called. Use for animations whose effect is read on every
|
|
2373
|
+
* frame but which don't have a natural progress state (e.g.
|
|
2374
|
+
* `cycleVertexColors`, which expresses its current value as a function
|
|
2375
|
+
* of `performance.now()` rather than as a tween from `from` to `to`).
|
|
2376
|
+
* Without a keep-alive entry the loop would idle and `onTick` would
|
|
2377
|
+
* stop firing even though the override is still installed.
|
|
2378
|
+
*/
|
|
2379
|
+
keepAlive(): () => void;
|
|
2380
|
+
}
|
|
2381
|
+
/** Options for `Animator.loop`. */
|
|
2382
|
+
interface LoopOptions {
|
|
2383
|
+
/** Maximum number of iterations. Default Infinity. */
|
|
2384
|
+
count?: number;
|
|
2385
|
+
/** Invoked when the loop reaches `count` iterations naturally (not on cancel). */
|
|
2386
|
+
onDone?: () => void;
|
|
2387
|
+
/** Any new animation passed the same cancelKey cancels the prior one in flight.
|
|
2388
|
+
* Also enables `animator.cancelKey` / `animator.isActive(key)` for this loop. */
|
|
2389
|
+
cancelKey?: string;
|
|
2390
|
+
}
|
|
2391
|
+
/** Options for the top-level `Animator.stagger` factory form (third overload). */
|
|
2392
|
+
interface StaggerOptions {
|
|
2393
|
+
/** Cancel-key for the supervising registration. `animator.cancelKey(key)`
|
|
2394
|
+
* cancels the whole stagger; `animator.isActive(key)` returns true while
|
|
2395
|
+
* any timer or child is alive. */
|
|
2396
|
+
cancelKey?: string;
|
|
2397
|
+
}
|
|
2398
|
+
/** Produces one iteration of a loop. Must arrange for `next` to be called when
|
|
2399
|
+
* the animation it returns finishes, or the loop stalls after one pass. */
|
|
2400
|
+
type LoopFactory = (iteration: number, next: () => void) => AnimationHandle;
|
|
2401
|
+
/** Per-index delay schedule. Number ⇒ `index * delay` ms. Function ⇒ caller
|
|
2402
|
+
* decides the absolute delay for each index (e.g. `i => i * i * 30`). */
|
|
2403
|
+
type StaggerDelay = number | ((index: number) => number);
|
|
2404
|
+
/** Produces the animation for one staggered item. */
|
|
2405
|
+
type StaggerFactory<TItem> = (item: TItem, index: number) => AnimationHandle;
|
|
2406
|
+
/** A `T` value or a function that derives one from the per-item context. Used
|
|
2407
|
+
* by the fluent builder methods (`.tween`, `.springPose`) so each item can
|
|
2408
|
+
* vary an option (e.g. `to: (_item, i) => (i + 1) * 10`). */
|
|
2409
|
+
type StaggerPerItem<T, TItem> = T | ((item: TItem, index: number) => T);
|
|
2410
|
+
/** Options for the stagger builder's `.tween`: a tween per item, where
|
|
2411
|
+
* `from`, `to` and `ms` may each vary by item. */
|
|
2412
|
+
interface StaggerTweenOptions<T, TItem> {
|
|
2413
|
+
from: StaggerPerItem<T, TItem>;
|
|
2414
|
+
to: StaggerPerItem<T, TItem>;
|
|
2415
|
+
ms: StaggerPerItem<number, TItem>;
|
|
2416
|
+
easing?: EasingSpec;
|
|
2417
|
+
interpolate?: Interpolate<T>;
|
|
2418
|
+
onTick: (value: T, item: TItem, index: number) => void;
|
|
2419
|
+
onDone?: (item: TItem, index: number) => void;
|
|
2420
|
+
}
|
|
2421
|
+
/** Options for the stagger builder's `.springPose`: the spring tuning, and
|
|
2422
|
+
* whether each item's settle is recorded as an undoable op. */
|
|
2423
|
+
interface StaggerSpringPoseOptions<TPose> {
|
|
2424
|
+
preset?: SpringPresetName;
|
|
2425
|
+
stiffness?: number;
|
|
2426
|
+
damping?: number;
|
|
2427
|
+
mass?: number;
|
|
2428
|
+
geometry?: PoseDescriptor<TPose>;
|
|
2429
|
+
recordOp?: boolean;
|
|
2430
|
+
opLabel?: string;
|
|
2431
|
+
}
|
|
2432
|
+
/** Fluent form of `Animator.stagger`: pick what to run per item after the
|
|
2433
|
+
* items and the delay schedule are already fixed. */
|
|
2434
|
+
interface StaggerBuilder<TItem> {
|
|
2435
|
+
/** Run an arbitrary per-item factory. */
|
|
2436
|
+
each(factory: StaggerFactory<TItem>): AnimationHandle;
|
|
2437
|
+
/** Sugar: per-item `animator.tween` with per-item-varying options. */
|
|
2438
|
+
tween<T>(opts: StaggerTweenOptions<T, TItem>): AnimationHandle;
|
|
2439
|
+
/** Sugar: per-item `springPose` against an adapter. `poseFn` returns the
|
|
2440
|
+
* target pose for each item. Each item must either be a primitive
|
|
2441
|
+
* (string/number) or expose a string `id` field — otherwise pose ids
|
|
2442
|
+
* would collide on `"[object Object]"` and successive tweens would
|
|
2443
|
+
* cancel each other. Throws on items that satisfy neither. */
|
|
2444
|
+
springPose<TPose>(adapter: SceneAdapter<{
|
|
2445
|
+
id: string;
|
|
2446
|
+
}, TPose>, poseFn: (item: TItem, index: number) => TPose, opts?: StaggerSpringPoseOptions<TPose>): AnimationHandle;
|
|
2447
|
+
}
|
|
2448
|
+
/** Options for `Animator.tweenLoop` — a tween's options plus how each
|
|
2449
|
+
* iteration relates to the last. */
|
|
2450
|
+
interface TweenLoopOptions<T> {
|
|
2451
|
+
from: T;
|
|
2452
|
+
to: T;
|
|
2453
|
+
ms: number;
|
|
2454
|
+
easing?: EasingSpec;
|
|
2455
|
+
/** `restart` (default): from→to every iteration.
|
|
2456
|
+
* `reverse`: to→from every iteration.
|
|
2457
|
+
* `alternate`: even iterations from→to, odd iterations to→from. */
|
|
2458
|
+
direction?: 'restart' | 'reverse' | 'alternate';
|
|
2459
|
+
count?: number;
|
|
2460
|
+
interpolate?: Interpolate<T>;
|
|
2461
|
+
onTick: (value: T) => void;
|
|
2462
|
+
onDone?: () => void;
|
|
2463
|
+
cancelKey?: string;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/** Cancel-key prefix. Each hook instance appends its own id, so two runners
|
|
2467
|
+
* sharing an animator do not cancel each other. */
|
|
2468
|
+
declare const VIEW_ANIMATION_KEY = "view";
|
|
2469
|
+
/** How the camera should move. */
|
|
2470
|
+
interface ViewAnimationOptions {
|
|
2471
|
+
/** Duration in ms. Default 250. */
|
|
2472
|
+
ms?: number;
|
|
2473
|
+
/** Easing curve. Default `easeOutCubic`. */
|
|
2474
|
+
easing?: EasingSpec;
|
|
2475
|
+
/** Replace the kit's log-scale / fixed-anchor curve. */
|
|
2476
|
+
interpolator?: InterpolatorFactory<View$1>;
|
|
2477
|
+
/** Fires when the target is reached. Not called on cancel. */
|
|
2478
|
+
onDone?: () => void;
|
|
2479
|
+
}
|
|
2480
|
+
/** Options accepted by {@link ViewAnimationApi.animateToBounds}. */
|
|
2481
|
+
interface AnimateToBoundsOptions extends FitViewToBoundsOptions, ViewAnimationOptions {
|
|
2482
|
+
}
|
|
2483
|
+
/** What the runner reads and writes. On `<SceneCanvas>` this is the same
|
|
2484
|
+
* channel `view.set` uses, so a camera animation on an uncontrolled canvas
|
|
2485
|
+
* costs no React render. */
|
|
2486
|
+
interface ViewChannel {
|
|
2487
|
+
get(): View$1;
|
|
2488
|
+
set(v: View$1): void;
|
|
2489
|
+
}
|
|
2490
|
+
/** The camera animation surface. One animation at a time. */
|
|
2491
|
+
interface ViewAnimationApi {
|
|
2492
|
+
/** Glide from the live view to `to`. A thunk receives the pending target when
|
|
2493
|
+
* one is in flight, so successive discrete steps compound. */
|
|
2494
|
+
animate(to: View$1 | ((base: View$1) => View$1), opts?: ViewAnimationOptions): void;
|
|
2495
|
+
/** `fitViewToBounds` composed with `animate`. */
|
|
2496
|
+
animateToBounds(bounds: Bounds, dims: ViewportDims, opts?: AnimateToBoundsOptions): void;
|
|
2497
|
+
/** Cancel. The view stays where it is — no jump to the target. */
|
|
2498
|
+
stop(): void;
|
|
2499
|
+
isAnimating(): boolean;
|
|
2500
|
+
/** Where the in-flight animation is heading, or null when none is. */
|
|
2501
|
+
target(): View$1 | null;
|
|
2502
|
+
/** Cancel unless the write that prompted this came from the runner's own
|
|
2503
|
+
* per-frame write. Feed it from every channel that can move the camera. */
|
|
2504
|
+
stopIfExternal(): void;
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* Animate the viewport `View`. Runs on the kit's {@link Animator} — pass one to
|
|
2508
|
+
* share a canvas's animator, or omit it and the hook makes its own.
|
|
2509
|
+
*
|
|
2510
|
+
* Every animation from one instance registers under that instance's cancel key,
|
|
2511
|
+
* so starting one cancels whatever *it* had in flight, and each starts from the
|
|
2512
|
+
* *live* view rather than a captured value — an interrupted camera never jumps.
|
|
2513
|
+
* Two instances on one animator are independent.
|
|
2514
|
+
*/
|
|
2515
|
+
declare function useViewAnimation(view: ViewChannel, animator?: Animator): ViewAnimationApi;
|
|
2516
|
+
|
|
2517
|
+
/**
|
|
2518
|
+
* Pose composition for hierarchical scene graphs.
|
|
2519
|
+
*
|
|
2520
|
+
* As of the nesting change, `getPose(id)` on adapters returns the
|
|
2521
|
+
* **local** pose — relative to the object's direct parent. Anything in the
|
|
2522
|
+
* kit that needs to draw, hit-test, snap, or otherwise reason about world
|
|
2523
|
+
* coordinates routes through `composeWorldPose`, which walks the parent
|
|
2524
|
+
* chain and folds local poses together via a consumer-supplied `compose`.
|
|
2525
|
+
*
|
|
2526
|
+
* Pose shape is generic, so the compose strategy is too. For the common
|
|
2527
|
+
* `{x, y, width, height}` axis-aligned rect, use `composeRectPose` —
|
|
2528
|
+
* translation only, child dimensions preserved. Custom pose shapes (paths,
|
|
2529
|
+
* matrix transforms) supply their own.
|
|
2530
|
+
*
|
|
2531
|
+
* The inverse — `rebaseLocalPose` — converts a world-space pose into a
|
|
2532
|
+
* local pose under a target parent. Used when reparenting so the visual
|
|
2533
|
+
* world position of a child is preserved across the parent change.
|
|
2534
|
+
*/
|
|
2535
|
+
/** Re-exported; the declaration lives in `core/scene/types.ts`, which names
|
|
2536
|
+
* it and may not import from features. */
|
|
2537
|
+
|
|
2538
|
+
/** Minimal adapter needed by `composeWorldPose` and friends — pose lookup plus parent walk. */
|
|
2539
|
+
interface PoseAdapter<TPose> {
|
|
2540
|
+
getPose(id: string): TPose;
|
|
2541
|
+
getParent(id: string): string | null;
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* The transforms a `compose` represents **exactly**. A parent transform wider
|
|
2545
|
+
* than its strategy's closure is rounded to the nearest pose — for `'rigid'`
|
|
2546
|
+
* that means an anisotropically scaled parent turns a rotated child into a
|
|
2547
|
+
* parallelogram, which `{x, y, width, height, rotation}` cannot hold, so the
|
|
2548
|
+
* shear is dropped.
|
|
2549
|
+
*
|
|
2550
|
+
* Measured over 50,000 random parent/child pairs: rotation and translation
|
|
2551
|
+
* compose to within 5.7e-13, anisotropic scale to a right-angle error of 0.99.
|
|
2552
|
+
* See `docs/superpowers/specs/2026-09-10-group-as-frame-design.md`.
|
|
2553
|
+
*/
|
|
2554
|
+
type PoseClosure = 'identity' | 'translation' | 'rigid';
|
|
2555
|
+
/** Consumer's pose-composition strategy for hierarchical scenes. `compose`
|
|
2556
|
+
* folds a child's pose (in parent's frame) up to the next frame; `decompose`
|
|
2557
|
+
* is its inverse. Default is IDENTITY — an absolute-pose scene where every
|
|
2558
|
+
* node already stores world coords (parent is grouping-only, no transform). */
|
|
2559
|
+
interface PoseComposition<TPose> {
|
|
2560
|
+
compose: (parent: TPose, child: TPose) => TPose;
|
|
2561
|
+
decompose: (parent: TPose, world: TPose) => TPose;
|
|
2562
|
+
closure: PoseClosure;
|
|
2563
|
+
}
|
|
2564
|
+
/** Default pose-composition strategy: IDENTITY. Both `compose` and
|
|
2565
|
+
* `decompose` return the child/world pose unchanged, modeling an
|
|
2566
|
+
* absolute-pose scene where every node stores world coords and parents are
|
|
2567
|
+
* grouping-only (no transform). With this strategy `composeWorldPose`
|
|
2568
|
+
* returns a node's own raw pose and `rebaseLocalPose` is a no-op. */
|
|
2569
|
+
declare const IDENTITY_POSE_COMPOSITION: PoseComposition<unknown>;
|
|
2570
|
+
/**
|
|
2571
|
+
* Walk `id`'s parent chain (root first to id last) and fold local poses into
|
|
2572
|
+
* a world pose via `compose`. Returns the world pose for `id`. Cycle-safe:
|
|
2573
|
+
* a visited-set guard breaks if the chain ever loops back to itself.
|
|
2574
|
+
*
|
|
2575
|
+
* `compose(parent, child)` interprets `child` as expressed *in `parent`'s
|
|
2576
|
+
* local frame* and returns the equivalent pose in the next frame up. For a
|
|
2577
|
+
* standard translation-only rect: `world = { x: p.x + c.x, y: p.y + c.y,
|
|
2578
|
+
* width: c.width, height: c.height }`.
|
|
2579
|
+
*/
|
|
2580
|
+
declare function composeWorldPose<TPose>(adapter: PoseAdapter<TPose>, id: string, compose: (parent: TPose, child: TPose) => TPose): TPose;
|
|
2581
|
+
/**
|
|
2582
|
+
* Default `compose` for axis-aligned rectangles. Adds translation; preserves
|
|
2583
|
+
* child width/height. Treat as the canonical compose for any
|
|
2584
|
+
* `{x, y, width, height}` pose under a translation-only hierarchy.
|
|
2585
|
+
*
|
|
2586
|
+
* Generic over the concrete pose type so callers with a wider pose
|
|
2587
|
+
* (e.g. `RectPose & { rotation }`) can pass it through; the extra fields
|
|
2588
|
+
* are taken from the child unchanged.
|
|
2589
|
+
*/
|
|
2590
|
+
declare function composeRectPose<TPose extends RectPose>(parent: TPose, child: TPose): TPose;
|
|
2591
|
+
/**
|
|
2592
|
+
* Translate a `RectPose`-shaped pose by `(dx, dy)`. Suitable as the default
|
|
2593
|
+
* `translatePose` for `useMove` when poses carry top-level `x`/`y`. Other
|
|
2594
|
+
* fields (width/height, plus any extra props on `TPose`) are preserved.
|
|
2595
|
+
*/
|
|
2596
|
+
declare function translateRectPose<TPose extends RectPose>(pose: TPose, dx: number, dy: number): TPose;
|
|
2597
|
+
/**
|
|
2598
|
+
* Convert `worldPose` into a local pose expressed under `newParentId`'s
|
|
2599
|
+
* frame. Used when reparenting so the child's visual world position is
|
|
2600
|
+
* preserved despite the change of frame. Inverse of one `compose` step.
|
|
2601
|
+
*
|
|
2602
|
+
* `decompose(parent, world)` returns the local pose `child` such that
|
|
2603
|
+
* `compose(parent, child) === world`. For axis-aligned rects:
|
|
2604
|
+
* `child = { ...world, x: world.x - parent.x, y: world.y - parent.y }`.
|
|
2605
|
+
*
|
|
2606
|
+
* Pass `newParentId === null` for the root frame; the function returns
|
|
2607
|
+
* `worldPose` unchanged.
|
|
2608
|
+
*/
|
|
2609
|
+
declare function rebaseLocalPose<TPose>(adapter: PoseAdapter<TPose>, worldPose: TPose, newParentId: string | null, compose: (parent: TPose, child: TPose) => TPose, decompose: (parent: TPose, world: TPose) => TPose): TPose;
|
|
2610
|
+
/** Inverse of `composeRectPose` — subtracts parent translation. */
|
|
2611
|
+
declare function decomposeRectPose<TPose extends RectPose>(parent: TPose, world: TPose): TPose;
|
|
2612
|
+
/**
|
|
2613
|
+
* `compose` for a container whose pose defines a **frame**: the child's local
|
|
2614
|
+
* pose is offset into the parent's unrotated box, then the whole thing is
|
|
2615
|
+
* turned about the parent's center. Rotations add; the child keeps its size.
|
|
2616
|
+
*
|
|
2617
|
+
* Reduces to `composeRectPose` when the parent is upright, so a scene that
|
|
2618
|
+
* never rotates a container behaves identically under either strategy.
|
|
2619
|
+
*
|
|
2620
|
+
* Exact for translation and rotation. A parent carrying scale is outside what
|
|
2621
|
+
* this can express — see `PoseClosure`.
|
|
2622
|
+
*/
|
|
2623
|
+
declare function composeRigidPose<TPose extends RectPose>(parent: TPose, child: TPose): TPose;
|
|
2624
|
+
/** Inverse of `composeRigidPose` — un-turns about the parent's center, then
|
|
2625
|
+
* subtracts the parent's offset. */
|
|
2626
|
+
declare function decomposeRigidPose<TPose extends RectPose>(parent: TPose, world: TPose): TPose;
|
|
2627
|
+
/** Translation-only composition over `RectPose`. What a scene wants when a
|
|
2628
|
+
* container groups its children but never turns them. */
|
|
2629
|
+
declare const RECT_POSE_COMPOSITION: PoseComposition<RectPose>;
|
|
2630
|
+
/** Composition over `RectPose` where a container's pose is a frame: rotating
|
|
2631
|
+
* the container rotates its contents. */
|
|
2632
|
+
declare const RIGID_POSE_COMPOSITION: PoseComposition<RectPose>;
|
|
2633
|
+
/**
|
|
2634
|
+
* Build a `(id) => world pose | null` callback over a `PoseAdapter`.
|
|
2635
|
+
* Convenience for RenderLayers that take a `getPose` callback (selection
|
|
2636
|
+
* overlays, debug layers, etc.) so consumers don't hand-write a
|
|
2637
|
+
* `composeWorldPose` call per layer.
|
|
2638
|
+
*
|
|
2639
|
+
* Returns `null` when `adapter.getPose` or `adapter.getParent` throws — the
|
|
2640
|
+
* common case is an id removed mid-render between selection state and the
|
|
2641
|
+
* next paint. Layers should treat `null` as "skip this id."
|
|
2642
|
+
*/
|
|
2643
|
+
declare function worldPoseLookup<TPose>(adapter: PoseAdapter<TPose>, compose: (parent: TPose, child: TPose) => TPose): (id: string) => TPose | null;
|
|
2644
|
+
|
|
2645
|
+
/** Boolean op identifiers — five Pathfinder primaries plus Crop. */
|
|
2646
|
+
type BooleanOp = 'union' | 'intersect' | 'subtract' | 'exclude' | 'divide' | 'crop';
|
|
2647
|
+
/**
|
|
2648
|
+
* z-position descriptor for a path node. `parentId` is the direct parent
|
|
2649
|
+
* (or `null` for a top-level node); `index` is the position within that
|
|
2650
|
+
* parent's child order. Used by the optional `getZOrder` hook below to
|
|
2651
|
+
* reposition the result of a boolean op at the topmost source's slot.
|
|
2652
|
+
*/
|
|
2653
|
+
/** @internal */
|
|
2654
|
+
interface BooleanZOrder {
|
|
2655
|
+
parentId: string | null;
|
|
2656
|
+
index: number;
|
|
2657
|
+
}
|
|
2658
|
+
/** Adapter the hook and the pure core both consume. */
|
|
2659
|
+
interface BooleansAdapter {
|
|
2660
|
+
getSelection(): NodeId[];
|
|
2661
|
+
getWorldPath(id: NodeId): Path | undefined;
|
|
2662
|
+
compareZ(a: NodeId, b: NodeId): number;
|
|
2663
|
+
/**
|
|
2664
|
+
* Mint a new node from a boolean-op result `Path`. `producedBy` names the
|
|
2665
|
+
* op that synthesized it — adapters that store provenance (e.g. for a
|
|
2666
|
+
* layer-panel icon) record it; others ignore the arg.
|
|
2667
|
+
*/
|
|
2668
|
+
createPathNode(path: Path, producedBy: BooleanOp): {
|
|
2669
|
+
id: string;
|
|
2670
|
+
};
|
|
2671
|
+
/**
|
|
2672
|
+
* Optional: return the full object for an id, used by the delete ops so
|
|
2673
|
+
* their `invert` (an insert) can restore the complete object on undo.
|
|
2674
|
+
* If omitted, a `{ id }` stub is captured — undo will reinstate the id
|
|
2675
|
+
* but consumers reading other fields (path, fill, etc.) will see them as
|
|
2676
|
+
* undefined. Mirrors `DeleteAdapter.getNode`; should be provided whenever
|
|
2677
|
+
* undo over boolean ops is expected to be lossless.
|
|
2678
|
+
*/
|
|
2679
|
+
getNode?(id: NodeId): {
|
|
2680
|
+
id: string;
|
|
2681
|
+
} | undefined | null;
|
|
2682
|
+
/**
|
|
2683
|
+
* Optional: return the parent + child-index of `id` so the result of a
|
|
2684
|
+
* boolean op can be placed in the topmost source's z-slot. Adapters that
|
|
2685
|
+
* also expose `getChildren`/`setChildOrder` (the `ReorderAdapter`
|
|
2686
|
+
* contract) will have the kit emit a `createMoveToIndexOp` after the
|
|
2687
|
+
* inserts. Adapters that omit this method get v1 behavior — the result
|
|
2688
|
+
* lands wherever the adapter's plain `insertNode` defaults to.
|
|
2689
|
+
*/
|
|
2690
|
+
getZOrder?(id: NodeId): BooleanZOrder | undefined;
|
|
2691
|
+
applyOps?(ops: Op[], label?: string): void;
|
|
2692
|
+
setSelection?(ids: NodeId[]): void;
|
|
2693
|
+
insertNode?(node: {
|
|
2694
|
+
id: string;
|
|
2695
|
+
}): void;
|
|
2696
|
+
removeNode?(id: string): void;
|
|
2697
|
+
}
|
|
2698
|
+
/** Outcome reported back to callers (lets the hook surface no-op signals). */
|
|
2699
|
+
type BooleanOpResult = {
|
|
2700
|
+
kind: 'applied';
|
|
2701
|
+
resultIds: string[];
|
|
2702
|
+
} | {
|
|
2703
|
+
kind: 'noop';
|
|
2704
|
+
reason: 'no-paths' | 'too-few-for-subtract' | 'empty-result';
|
|
2705
|
+
};
|
|
2706
|
+
/**
|
|
2707
|
+
* Run one Boolean operation over the selected paths and commit the result as a
|
|
2708
|
+
* single undoable batch.
|
|
2709
|
+
*
|
|
2710
|
+
* Operands are ordered back-to-front, which is what makes `subtract` mean
|
|
2711
|
+
* "everything in front removed from the backmost shape". Returns without
|
|
2712
|
+
* mutating anything when the selection holds no paths, or too few for the
|
|
2713
|
+
* requested operation.
|
|
2714
|
+
*/
|
|
2715
|
+
declare function applyBooleanOp(adapter: BooleansAdapter, op: BooleanOp): BooleanOpResult;
|
|
2716
|
+
|
|
2717
|
+
/** Context handed to every content handler for one ingest event. */
|
|
2718
|
+
interface IngestCtx {
|
|
2719
|
+
/** World-space arrival point (drop / pointed imperative ingest); `null`
|
|
2720
|
+
* for paste and point-less calls — handlers pick their own policy
|
|
2721
|
+
* (the kit image handler centers on the viewport). */
|
|
2722
|
+
point: {
|
|
2723
|
+
x: number;
|
|
2724
|
+
y: number;
|
|
2725
|
+
} | null;
|
|
2726
|
+
/** Visible canvas area in world coordinates. */
|
|
2727
|
+
viewportWorldRect(): {
|
|
2728
|
+
x: number;
|
|
2729
|
+
y: number;
|
|
2730
|
+
width: number;
|
|
2731
|
+
height: number;
|
|
2732
|
+
};
|
|
2733
|
+
/** The kit insert dep — id/layer/undoable-op supplied; the canonical way
|
|
2734
|
+
* for a handler to mint a node (`insert.commit(bounds, { kind, ... })`). */
|
|
2735
|
+
insert: InsertDep;
|
|
2736
|
+
/** Raw op commit for handlers that build their own ops. */
|
|
2737
|
+
applyOps(ops: Op[], label?: string): void;
|
|
2738
|
+
scene: Scene<unknown, string, unknown>;
|
|
2739
|
+
selection: SelectionApi;
|
|
2740
|
+
/** Consumer file→src resolver (SceneCanvas `ingestion.resolveSrc`).
|
|
2741
|
+
* When absent, the kit image handler embeds as a `data:` URI. */
|
|
2742
|
+
resolveSrc?: (file: File) => Promise<string>;
|
|
2743
|
+
/** Kit SVG-handler options (SceneCanvas `ingestion.svg`) — e.g.
|
|
2744
|
+
* `{ unpack: unpackSvgFiles }` (from `@weasel-js/svg`) to parse SVG files
|
|
2745
|
+
* into scene nodes. */
|
|
2746
|
+
svg?: SvgIngestOptions;
|
|
2747
|
+
/** Clipboard-paste seam — present when the hosting `SceneCanvas` supplied
|
|
2748
|
+
* an adapter with `commitPaste`. `reviver` comes from
|
|
2749
|
+
* `SceneCanvasProps.ingestion.clipboard`. Absent ⇒ the kit weasel-JSON
|
|
2750
|
+
* handler declines inert (dwarn, nothing ingested) — its matched items
|
|
2751
|
+
* were already consumed at match time, so they do NOT fall through;
|
|
2752
|
+
* only match-level misses flow on to other handlers. */
|
|
2753
|
+
clipboard?: ClipboardIngestCtx;
|
|
2754
|
+
/** Set to `true` by the kit weasel-JSON handler when it successfully
|
|
2755
|
+
* pastes a payload in this event. The `ctx` object is shared across all
|
|
2756
|
+
* handlers in one `runIngest` call, and higher-priority handlers' `handle`
|
|
2757
|
+
* bodies run (synchronously) before lower ones — so `kit:svg`'s
|
|
2758
|
+
* `text/plain` SVG fallback reads this to decline the SVG flavor of a copy
|
|
2759
|
+
* whose canonical weasel-JSON flavor already ingested (avoids a
|
|
2760
|
+
* double-paste when both flavors ride one clipboard event). */
|
|
2761
|
+
consumedWeaselPayload?: boolean;
|
|
2762
|
+
/** Full action-deps bag, for consumer handlers that need more. */
|
|
2763
|
+
deps: ActionDeps;
|
|
2764
|
+
}
|
|
2765
|
+
/** A handler for content arriving by paste, drop or file picker. Handlers are
|
|
2766
|
+
* matched by MIME glob or predicate and run highest-priority first; the kit's
|
|
2767
|
+
* own register at a low priority so a consumer's handler wins by default. */
|
|
2768
|
+
interface ContentHandlerEntry {
|
|
2769
|
+
/** Stable identifier — used for unregistration and debugging
|
|
2770
|
+
* (`'kit:image'`, `'app:csv'`). */
|
|
2771
|
+
id: string;
|
|
2772
|
+
/** MIME glob(s) (`'image/*'`, `'text/csv'`) or an item predicate. */
|
|
2773
|
+
match: string | string[] | ((item: IngestItem) => boolean);
|
|
2774
|
+
/** Higher runs earlier. Kit defaults register at -100 so any consumer
|
|
2775
|
+
* handler (default 0) beats them. */
|
|
2776
|
+
priority?: number;
|
|
2777
|
+
handle(items: IngestItem[], ctx: IngestCtx): void | Promise<void>;
|
|
2778
|
+
}
|
|
2779
|
+
/** Register a content handler. Returns a disposer that removes it. */
|
|
2780
|
+
declare function registerContentHandler(entry: ContentHandlerEntry): () => void;
|
|
2781
|
+
|
|
2782
|
+
/**
|
|
2783
|
+
* @experimental
|
|
2784
|
+
* PointerContext — a tiny ambient context that publishes the world-space
|
|
2785
|
+
* position of the canvas pointer, refreshed on every `pointermove` over
|
|
2786
|
+
* the canvas. Cleared (set to `null`) on `pointerleave`.
|
|
2787
|
+
*
|
|
2788
|
+
* Why ref-based and not state-based: cursor moves fire dozens of times per
|
|
2789
|
+
* second; routing those through React state would re-render every consumer
|
|
2790
|
+
* in the tree. The context exposes a stable `pointerRef` whose `.current`
|
|
2791
|
+
* is mutated directly by the publisher, plus a thunk `getDropPoint()` that
|
|
2792
|
+
* reads it on demand. Consumers (e.g. `useClipboard`) pull via the thunk
|
|
2793
|
+
* inside their callbacks — no subscription, no re-render.
|
|
2794
|
+
*
|
|
2795
|
+
* `<SceneCanvas>` publishes automatically. `useClipboardOps` consumes when
|
|
2796
|
+
* the caller didn't pass an explicit `getDropPoint` option. Other future
|
|
2797
|
+
* hit-on-cursor consumers (drop-zone hover, context-menu anchor) can reuse
|
|
2798
|
+
* the same context.
|
|
2799
|
+
*/
|
|
2800
|
+
|
|
2801
|
+
/** @experimental World-space pointer position, or `null` when the pointer
|
|
2802
|
+
* isn't over the publishing canvas. */
|
|
2803
|
+
type PointerWorldPos = {
|
|
2804
|
+
worldX: number;
|
|
2805
|
+
worldY: number;
|
|
2806
|
+
} | null;
|
|
2807
|
+
/** @experimental */
|
|
2808
|
+
interface PointerContextValue {
|
|
2809
|
+
/** Live ref — mutate to publish, read for the latest snapshot. The
|
|
2810
|
+
* identity is stable for the lifetime of the provider. */
|
|
2811
|
+
readonly pointerRef: MutableRefObject<PointerWorldPos>;
|
|
2812
|
+
/** Convenience thunk equivalent to `() => pointerRef.current`. Stable
|
|
2813
|
+
* identity for the lifetime of the provider; safe to pass to hooks. */
|
|
2814
|
+
readonly getDropPoint: () => PointerWorldPos;
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* @experimental
|
|
2818
|
+
* Wrap the part of the React tree that should share a pointer-position
|
|
2819
|
+
* context. Usually placed at the demo / app root, alongside
|
|
2820
|
+
* `<ActionsProvider>` and `<SelectionContextProvider>`.
|
|
2821
|
+
*
|
|
2822
|
+
* Most consumers don't need to mount this directly — `<SceneCanvas>` mounts
|
|
2823
|
+
* an internal provider when no parent provider is in scope, so child hooks
|
|
2824
|
+
* (`useClipboard` without an explicit `getDropPoint`) read the canvas's
|
|
2825
|
+
* tracked pointer for free.
|
|
2826
|
+
*/
|
|
2827
|
+
declare function PointerContextProvider({ children }: {
|
|
2828
|
+
children: ReactNode;
|
|
2829
|
+
}): ReactNode;
|
|
2830
|
+
/** @experimental Read the surrounding pointer-context value, or `null` when
|
|
2831
|
+
* no provider is in scope. */
|
|
2832
|
+
declare function usePointerContext(): PointerContextValue | null;
|
|
2833
|
+
|
|
2834
|
+
/** Optional consumer seam: given a node and the affine `m` that a pose-transform
|
|
2835
|
+
* action applied to the node's POSE, return updated `data` with the node's
|
|
2836
|
+
* data-held geometry transformed by `m`, or `null` if this node has no
|
|
2837
|
+
* data-held geometry (the kit leaves `data` alone). */
|
|
2838
|
+
interface GeometryProjection {
|
|
2839
|
+
transform(node: {
|
|
2840
|
+
id?: string;
|
|
2841
|
+
data: unknown;
|
|
2842
|
+
pose: unknown;
|
|
2843
|
+
}, m: Mat3$1): unknown | null;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
/** Minimal view API the action layer consumes. */
|
|
2847
|
+
interface ViewApi {
|
|
2848
|
+
get(): View$1;
|
|
2849
|
+
set(v: View$1): void;
|
|
2850
|
+
/** Optional recenter callback. When wired, `viewportZoomAction`'s `reset`
|
|
2851
|
+
* branch (Cmd-0) calls this instead of resetting to identity — letting
|
|
2852
|
+
* consumers re-fit the page (or other reference bounds) into the workspace.
|
|
2853
|
+
* Return the target `View` to let the action animate there; return nothing
|
|
2854
|
+
* to keep dispatching the view yourself. */
|
|
2855
|
+
recenter?(): View$1 | void;
|
|
2856
|
+
/** Optional canvas-local host dimensions (CSS px). When wired,
|
|
2857
|
+
* `viewportZoomAction`'s keyboard branches (Cmd+= / Cmd+-) anchor at the
|
|
2858
|
+
* host center instead of the top-left origin. Null when the host isn't
|
|
2859
|
+
* measurable (unmounted). */
|
|
2860
|
+
hostSize?(): {
|
|
2861
|
+
width: number;
|
|
2862
|
+
height: number;
|
|
2863
|
+
} | null;
|
|
2864
|
+
/** Optional camera animation. `<SceneCanvas>` wires these three; a consumer
|
|
2865
|
+
* publishing their own `view` dep need not, and actions fall back to `set`. */
|
|
2866
|
+
animate?(to: View$1, opts?: ViewAnimationOptions): void;
|
|
2867
|
+
stopAnimation?(): void;
|
|
2868
|
+
/** Where an in-flight camera animation is heading, or null. Compute the next
|
|
2869
|
+
* discrete step from this so repeated presses compound. */
|
|
2870
|
+
animationTarget?(): View$1 | null;
|
|
2871
|
+
/** Optional momentum decay. `<SceneCanvas>` wires this from `useDecayLoop`;
|
|
2872
|
+
* a consumer publishing their own `view` dep need not, and `viewport.dragPan`
|
|
2873
|
+
* simply lands the pan without coasting. */
|
|
2874
|
+
decay?(config: DecayLoopConfig): void;
|
|
2875
|
+
stopDecay?(): void;
|
|
2876
|
+
/** Whether a scene layer reaches the screen in this view — its own
|
|
2877
|
+
* `layerVisibility` / `layerOrder`, on top of the scene's `visible` flag.
|
|
2878
|
+
* Absent: this view paints every layer the scene shows. Selecting actions
|
|
2879
|
+
* pass over what the asking view does not paint. */
|
|
2880
|
+
layerIsPainted?(layerId: string): boolean;
|
|
2881
|
+
}
|
|
2882
|
+
/** The part of the asking view a region hit-test consults. */
|
|
2883
|
+
type HitTestView = Pick<ViewApi, 'layerIsPainted'>;
|
|
2884
|
+
/**
|
|
2885
|
+
* Adapter dep for `areaSelectAction`.
|
|
2886
|
+
*
|
|
2887
|
+
* Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>` via AABB
|
|
2888
|
+
* overlap over scene nodes. Consumers with custom hit-testing override this
|
|
2889
|
+
* dep entry in their own registrar.
|
|
2890
|
+
*/
|
|
2891
|
+
/**
|
|
2892
|
+
* Topmost-node-at-world-point dep, consumed by `moveAction` for
|
|
2893
|
+
* reparent-on-drop and available to any action that needs a single-best
|
|
2894
|
+
* pick. Mirrors the same hit-test plumbing `<SceneCanvas>` feeds to the
|
|
2895
|
+
* tool dispatcher; consumers with custom hit-testing override here.
|
|
2896
|
+
*
|
|
2897
|
+
* `exclude` is iterated once per call and treated as a set membership
|
|
2898
|
+
* test — the dep walks hits front-to-back and returns the first id not
|
|
2899
|
+
* in the exclude set. Pass moving-node roots + their descendants when
|
|
2900
|
+
* the caller wants to ignore the nodes it's manipulating.
|
|
2901
|
+
*/
|
|
2902
|
+
type NodeAtPointDep = (point: {
|
|
2903
|
+
x: number;
|
|
2904
|
+
y: number;
|
|
2905
|
+
}, exclude?: Iterable<NodeId>) => NodeId | null;
|
|
2906
|
+
/** What an area-selecting action needs: a way to ask what a region covers,
|
|
2907
|
+
* and a way to read and replace the selection. */
|
|
2908
|
+
interface AreaSelectDep {
|
|
2909
|
+
/** Return ids of all scene nodes whose AABB overlaps `bounds`, skipping any
|
|
2910
|
+
* on a layer `view` — the view the gesture ran in — does not paint. */
|
|
2911
|
+
hitTestArea(bounds: {
|
|
2912
|
+
x: number;
|
|
2913
|
+
y: number;
|
|
2914
|
+
width: number;
|
|
2915
|
+
height: number;
|
|
2916
|
+
}, view?: HitTestView): NodeId[];
|
|
2917
|
+
/** Return the current selection id list. */
|
|
2918
|
+
getSelection(): NodeId[];
|
|
2919
|
+
/** Replace the current selection. */
|
|
2920
|
+
setSelection(ids: NodeId[]): void;
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Adapter dep for `editAnchorsAction`.
|
|
2924
|
+
*
|
|
2925
|
+
* Provides narrow read/write access to the editable polygon for a single
|
|
2926
|
+
* node. Consumers register this dep so anchor-edit actions can read/write
|
|
2927
|
+
* the polygon WITHOUT knowing whether it lives directly on the node's
|
|
2928
|
+
* pose (`pose.kind === 'polygon'`) or on `node.data.path` (the kit's
|
|
2929
|
+
* built-in pen-tool default, also WeaselDraw's shape).
|
|
2930
|
+
*
|
|
2931
|
+
* Note on live previews: in-flight edit state is surfaced through the
|
|
2932
|
+
* dispatcher's standard `OngoingHandle.previewIds/previewPose/previewData`
|
|
2933
|
+
* triple (not this dep), so chrome and preview-ghost stay in lock-step
|
|
2934
|
+
* via one source of truth.
|
|
2935
|
+
*/
|
|
2936
|
+
interface EditAnchorsDep {
|
|
2937
|
+
/** Id of the node currently being edited. Empty string means no node is
|
|
2938
|
+
* currently in edit mode — the chrome and gesture both opt out. */
|
|
2939
|
+
editingId: string;
|
|
2940
|
+
/** Enter/exit edit mode for a specific node. Pass `null` (or an empty
|
|
2941
|
+
* string) to exit. `enterPathEditAction` and `exitPathEditAction` call
|
|
2942
|
+
* this; consumers can call it directly to drive edit mode programmatically. */
|
|
2943
|
+
setEditingId(id: string | null): void;
|
|
2944
|
+
/** Returns the COMMITTED editable polygon in world coordinates, or
|
|
2945
|
+
* null if this node has no editable polygon. Does NOT consult in-
|
|
2946
|
+
* flight previews — callers that need live state read the dispatcher's
|
|
2947
|
+
* in-flight handles. */
|
|
2948
|
+
getEditablePath(id: string): unknown;
|
|
2949
|
+
/** Returns where the polygon is stored — `'pose'` when `node.pose`
|
|
2950
|
+
* IS the polygon, `'data'` when it lives on `node.data.path` with a
|
|
2951
|
+
* rect pose, or `null` when the node has no editable polygon. The
|
|
2952
|
+
* action uses this to know which preview-ghost axis to populate
|
|
2953
|
+
* (`previewPose` only / `previewData` + `previewPose` for data.path). */
|
|
2954
|
+
getStorageKind(id: string): 'pose' | 'data' | null;
|
|
2955
|
+
/** Returns the node's raw `pose` and `data` so storage-aware actions
|
|
2956
|
+
* can capture origin state at gesture-start and synthesize a matching
|
|
2957
|
+
* `previewPose` / `previewData` during `onMove`. Used by
|
|
2958
|
+
* `editAnchorsAction` for the data.path branch (rect pose + data
|
|
2959
|
+
* carrying extra fields like fill / stroke that must be preserved
|
|
2960
|
+
* through the preview). Returns null when the node is gone. */
|
|
2961
|
+
getNodeShape(id: string): {
|
|
2962
|
+
pose: unknown;
|
|
2963
|
+
data: unknown;
|
|
2964
|
+
} | null;
|
|
2965
|
+
/** Commit `worldPath` as the new value for `id`. Implementation routes
|
|
2966
|
+
* to setPose (when pose IS the polygon) or batched setPose+update
|
|
2967
|
+
* (when the polygon lives on data.path). Records one history entry
|
|
2968
|
+
* labelled `label`. */
|
|
2969
|
+
applyEdit(id: string, worldPath: unknown, label: string): void;
|
|
2970
|
+
/**
|
|
2971
|
+
* Anchors currently selected within the edited path, as **flat anchor
|
|
2972
|
+
* indices** — the same numbering `enumerateAnchors` produces and the
|
|
2973
|
+
* `anchor:N` affordance kinds carry.
|
|
2974
|
+
*
|
|
2975
|
+
* Selection is transient UI state, deliberately not part of the scene:
|
|
2976
|
+
* it is cleared whenever `editingId` changes, and any edit that
|
|
2977
|
+
* renumbers anchors (insert, delete) is responsible for leaving it
|
|
2978
|
+
* coherent. Empty means "no anchor selected" — the keyboard actions
|
|
2979
|
+
* (nudge, delete) no-op rather than acting on all anchors, matching
|
|
2980
|
+
* Illustrator.
|
|
2981
|
+
*/
|
|
2982
|
+
selectedAnchors: ReadonlySet<number>;
|
|
2983
|
+
/** Replace the anchor selection. Pass an empty iterable to clear. */
|
|
2984
|
+
setSelectedAnchors(next: Iterable<number>): void;
|
|
2985
|
+
/**
|
|
2986
|
+
* In-flight anchor-marquee rect in world coords, or null when no
|
|
2987
|
+
* marquee drag is active. Written by `marqueeAnchorsAction` and read by
|
|
2988
|
+
* the path-editing overlay — the same "ongoing action owns the preview,
|
|
2989
|
+
* chrome just draws it" split the move/resize ghosts use.
|
|
2990
|
+
*/
|
|
2991
|
+
marquee: {
|
|
2992
|
+
x: number;
|
|
2993
|
+
y: number;
|
|
2994
|
+
width: number;
|
|
2995
|
+
height: number;
|
|
2996
|
+
} | null;
|
|
2997
|
+
/** Set or clear the in-flight marquee rect. */
|
|
2998
|
+
setMarquee(rect: {
|
|
2999
|
+
x: number;
|
|
3000
|
+
y: number;
|
|
3001
|
+
width: number;
|
|
3002
|
+
height: number;
|
|
3003
|
+
} | null): void;
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Adapter dep for `lassoSelectAction`.
|
|
3007
|
+
*
|
|
3008
|
+
* Provides polygon-lasso hit-testing + selection read/write.
|
|
3009
|
+
* Consumers that don't implement `hitTestLasso` can omit it; the action
|
|
3010
|
+
* falls back to a bounding-box AABB test via `hitTestArea`.
|
|
3011
|
+
*/
|
|
3012
|
+
interface LassoSelectDep {
|
|
3013
|
+
/**
|
|
3014
|
+
* Hit-test against a closed polygon (vertex order CW or CCW; last→first
|
|
3015
|
+
* closing edge is implicit). Returns matching node ids.
|
|
3016
|
+
* Optional — when absent, `lassoSelectAction` falls back to AABB via
|
|
3017
|
+
* `hitTestArea`.
|
|
3018
|
+
*/
|
|
3019
|
+
hitTestLasso?(polygon: ReadonlyArray<{
|
|
3020
|
+
x: number;
|
|
3021
|
+
y: number;
|
|
3022
|
+
}>, mode: 'centers' | 'intersect' | 'enclosed', view?: HitTestView): string[];
|
|
3023
|
+
/** Return ids of nodes whose AABB overlaps the given rect (fallback). */
|
|
3024
|
+
hitTestArea(bounds: {
|
|
3025
|
+
x: number;
|
|
3026
|
+
y: number;
|
|
3027
|
+
width: number;
|
|
3028
|
+
height: number;
|
|
3029
|
+
}, view?: HitTestView): string[];
|
|
3030
|
+
/** Return the current selection id list. */
|
|
3031
|
+
getSelection(): string[];
|
|
3032
|
+
/** Replace the current selection. */
|
|
3033
|
+
setSelection(ids: string[]): void;
|
|
3034
|
+
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Options for the kit `image/svg+xml` content handler, threaded from
|
|
3037
|
+
* SceneCanvas's `ingestion={{ svg }}` prop.
|
|
3038
|
+
*/
|
|
3039
|
+
interface SvgIngestOptions {
|
|
3040
|
+
/** Parse dropped/pasted/picked SVG files into native scene nodes (path /
|
|
3041
|
+
* text leaves under containers mirroring the source `<g>` structure)
|
|
3042
|
+
* instead of the default single embedded-image node.
|
|
3043
|
+
*
|
|
3044
|
+
* Pass `unpackSvgFiles` from `@weasel-js/svg`:
|
|
3045
|
+
*
|
|
3046
|
+
* ```ts
|
|
3047
|
+
* import { unpackSvgFiles } from '@weasel-js/svg';
|
|
3048
|
+
* <SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }} />
|
|
3049
|
+
* ```
|
|
3050
|
+
*
|
|
3051
|
+
* It is injected rather than flagged on with `true` because the SVG parser
|
|
3052
|
+
* lives in `@weasel-js/svg`, which depends on this package — core importing
|
|
3053
|
+
* it back would make the two mutually dependent and unpublishable
|
|
3054
|
+
* separately. Passing the function keeps the parser out of core's bundle
|
|
3055
|
+
* for consumers who never unpack. */
|
|
3056
|
+
unpack?: SvgUnpacker;
|
|
3057
|
+
}
|
|
3058
|
+
/** Parses SVG files and inserts the resulting nodes into `ctx.scene`, as one
|
|
3059
|
+
* `applyOps` batch per file. Implemented by `unpackSvgFiles` in
|
|
3060
|
+
* `@weasel-js/svg`; see {@link SvgIngestOptions.unpack}. */
|
|
3061
|
+
type SvgUnpacker = (files: File[], ctx: IngestCtx) => Promise<void>;
|
|
3062
|
+
/**
|
|
3063
|
+
* Clipboard-paste seam consumed by the kit weasel-JSON content handler
|
|
3064
|
+
* (`IngestCtx.clipboard`). Built by `<SceneCanvas>` from its own synthesized
|
|
3065
|
+
* adapter + the `ingestion.clipboard` prop; absent when the consumer set
|
|
3066
|
+
* `ingestion.clipboard.enabled === false` or the adapter lacks `commitPaste`.
|
|
3067
|
+
* Absence makes the handler decline inert (dwarn, nothing ingested) — its
|
|
3068
|
+
* matched items were already consumed at match time and do not fall through
|
|
3069
|
+
* to other handlers.
|
|
3070
|
+
*/
|
|
3071
|
+
interface ClipboardIngestCtx {
|
|
3072
|
+
/** The hosting canvas's adapter — `commitPaste` materializes the pasted
|
|
3073
|
+
* nodes (fresh ids, offset applied); insertion still goes through ops. */
|
|
3074
|
+
adapter: InsertAdapter<{
|
|
3075
|
+
id: string;
|
|
3076
|
+
}>;
|
|
3077
|
+
/** JSON reviver for the weasel wire payload (typed arrays etc.) — from
|
|
3078
|
+
* `SceneCanvasProps.ingestion.clipboard.reviver`. */
|
|
3079
|
+
reviver?: (key: string, value: unknown) => unknown;
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Dep for the `ingest` action (external-content ingestion).
|
|
3083
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
|
|
3084
|
+
* `useIngestionDepSource` — canvas rect + current view.
|
|
3085
|
+
*/
|
|
3086
|
+
interface IngestionDep {
|
|
3087
|
+
/** Visible canvas area in world coordinates. */
|
|
3088
|
+
viewportWorldRect(): {
|
|
3089
|
+
x: number;
|
|
3090
|
+
y: number;
|
|
3091
|
+
width: number;
|
|
3092
|
+
height: number;
|
|
3093
|
+
};
|
|
3094
|
+
/** Consumer file→src resolver (from SceneCanvas's `ingestion` prop).
|
|
3095
|
+
* Live accessor — read it at use time. Destructuring (or copying the
|
|
3096
|
+
* property early) snapshots the current value and won't track later
|
|
3097
|
+
* prop changes across an `await`. */
|
|
3098
|
+
resolveSrc?: (file: File) => Promise<string>;
|
|
3099
|
+
/** Kit SVG-handler options (from SceneCanvas's `ingestion` prop).
|
|
3100
|
+
* Live accessor, same caveat as `resolveSrc`. */
|
|
3101
|
+
svg?: SvgIngestOptions;
|
|
3102
|
+
/** Clipboard-paste seam for the kit weasel-JSON handler.
|
|
3103
|
+
* Live accessor, same caveat as `resolveSrc`. */
|
|
3104
|
+
clipboard?: ClipboardIngestCtx;
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Per-kind extra geometry passed to `InsertDep.commit`.
|
|
3108
|
+
*
|
|
3109
|
+
* Built-in tools populate a typed variant so the kit's default factory can
|
|
3110
|
+
* render the true tool params (line endpoints, polygon side count, star
|
|
3111
|
+
* geometry, pencil sample list). Consumer-defined tools may pass any
|
|
3112
|
+
* `{ kind: string; ... }` payload; the kit's factory falls back to AABB
|
|
3113
|
+
* inscription for unknown kinds.
|
|
3114
|
+
*
|
|
3115
|
+
* `bounds` is still passed alongside as a useful AABB pose hint — factories
|
|
3116
|
+
* may use it as the node's pose even when richer geometry is available.
|
|
3117
|
+
*/
|
|
3118
|
+
type InsertExtras = {
|
|
3119
|
+
kind: 'rect';
|
|
3120
|
+
} | {
|
|
3121
|
+
kind: 'ellipse';
|
|
3122
|
+
} | {
|
|
3123
|
+
kind: 'line';
|
|
3124
|
+
a: {
|
|
3125
|
+
x: number;
|
|
3126
|
+
y: number;
|
|
3127
|
+
};
|
|
3128
|
+
b: {
|
|
3129
|
+
x: number;
|
|
3130
|
+
y: number;
|
|
3131
|
+
};
|
|
3132
|
+
} | {
|
|
3133
|
+
kind: 'polygon';
|
|
3134
|
+
sides: number;
|
|
3135
|
+
rotation: number;
|
|
3136
|
+
center?: {
|
|
3137
|
+
x: number;
|
|
3138
|
+
y: number;
|
|
3139
|
+
};
|
|
3140
|
+
radius?: number;
|
|
3141
|
+
} | {
|
|
3142
|
+
kind: 'star';
|
|
3143
|
+
points: number;
|
|
3144
|
+
innerRadiusRatio: number;
|
|
3145
|
+
rotation: number;
|
|
3146
|
+
center?: {
|
|
3147
|
+
x: number;
|
|
3148
|
+
y: number;
|
|
3149
|
+
};
|
|
3150
|
+
outerRadius?: number;
|
|
3151
|
+
} | {
|
|
3152
|
+
kind: 'pencil';
|
|
3153
|
+
samples: ReadonlyArray<DragSample>;
|
|
3154
|
+
} | {
|
|
3155
|
+
kind: 'text';
|
|
3156
|
+
text?: string;
|
|
3157
|
+
} | {
|
|
3158
|
+
kind: 'image';
|
|
3159
|
+
src?: string;
|
|
3160
|
+
opacity?: number;
|
|
3161
|
+
/** Chrome-only: what the in-flight drag paints. Read by the overlay
|
|
3162
|
+
* layer, ignored by the insert dep. */
|
|
3163
|
+
preview?: 'bitmap' | 'outline';
|
|
3164
|
+
} | {
|
|
3165
|
+
kind: string;
|
|
3166
|
+
[extra: string]: unknown;
|
|
3167
|
+
};
|
|
3168
|
+
/**
|
|
3169
|
+
* World-space point snapping — grid, guides, or any consumer rule.
|
|
3170
|
+
*
|
|
3171
|
+
* Sourced by `<SceneCanvas>` from its `toolOptions.snapPoint`. Actions apply
|
|
3172
|
+
* it to the coords they ingest so the live preview and the committed
|
|
3173
|
+
* geometry agree; `insertAction` snaps the drag's start and current point.
|
|
3174
|
+
*
|
|
3175
|
+
* Optional: when the dep is absent, actions treat it as identity.
|
|
3176
|
+
*/
|
|
3177
|
+
interface SnapDep {
|
|
3178
|
+
/** Snap a world-space point. Return `p` unchanged to opt out. */
|
|
3179
|
+
point(p: {
|
|
3180
|
+
x: number;
|
|
3181
|
+
y: number;
|
|
3182
|
+
}): {
|
|
3183
|
+
x: number;
|
|
3184
|
+
y: number;
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
/**
|
|
3188
|
+
* Adapter dep for `insertAction`.
|
|
3189
|
+
*
|
|
3190
|
+
* Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>`. The `extras`
|
|
3191
|
+
* carry the active tool's kind + per-kind geometry. Callers
|
|
3192
|
+
* that need typed data must supply a richer `insert` dep.
|
|
3193
|
+
*/
|
|
3194
|
+
interface InsertDep {
|
|
3195
|
+
/**
|
|
3196
|
+
* Materialise a new node from the given drag-rect bounds and typed
|
|
3197
|
+
* per-kind extras. Returns the new node's id, or `null` if the consumer
|
|
3198
|
+
* rejected the insert (e.g. sub-threshold bounds, unknown kind).
|
|
3199
|
+
*/
|
|
3200
|
+
commit(bounds: {
|
|
3201
|
+
x: number;
|
|
3202
|
+
y: number;
|
|
3203
|
+
width: number;
|
|
3204
|
+
height: number;
|
|
3205
|
+
}, extras: InsertExtras): NodeId | null;
|
|
3206
|
+
}
|
|
3207
|
+
/**
|
|
3208
|
+
* Adapter dep for `resizeAction`.
|
|
3209
|
+
*
|
|
3210
|
+
* Carries the behavior-shaping options the legacy `useResize` hook
|
|
3211
|
+
* exposed through `UseResizeOptions`: bounds-frame behaviors (e.g.
|
|
3212
|
+
* `lockAspectWithModifier`), world-space anchor-point snap behaviors (e.g.
|
|
3213
|
+
* `pointSnapToGrid`), and group-expansion (`expandIds`).
|
|
3214
|
+
*
|
|
3215
|
+
* Optional in `DepSchema`: when absent, `resizeAction` falls back to
|
|
3216
|
+
* identity defaults (no behaviors, identity expandIds). Consumers wire the
|
|
3217
|
+
* dep via `useDepSource('resizePolicy', ...)` from any descendant of
|
|
3218
|
+
* `<DepRegistryProvider>` / `<SceneCanvas>`.
|
|
3219
|
+
*
|
|
3220
|
+
* The generic is erased to `unknown` at the schema entry; consumers cast at
|
|
3221
|
+
* the call site (mirrors the `scene` entry's convention).
|
|
3222
|
+
*/
|
|
3223
|
+
interface ResizePolicy<TPose> {
|
|
3224
|
+
/** Bounds-frame constraints. Constrained to `TPose extends Bounds` since
|
|
3225
|
+
* constraints read/write `{x,y,width,height}`. For non-rect TPose pass `[]`. */
|
|
3226
|
+
constraints: TPose extends Bounds ? BoundsConstraint<TPose>[] : never[];
|
|
3227
|
+
/** World-space anchor-point snap behaviors. Same TPose constraint as
|
|
3228
|
+
* `constraints`. */
|
|
3229
|
+
pointSnap: TPose extends Bounds ? PointSnapBehavior<TPose>[] : never[];
|
|
3230
|
+
/** Group-expansion at gesture start. Identity (`ids => ids`) when group
|
|
3231
|
+
* resize isn't wanted. */
|
|
3232
|
+
expandIds: (ids: string[]) => string[];
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Layout-strategy lookup by container id, consumed by `moveAction` to run
|
|
3236
|
+
* the drag-time reflow pass. Sourced by `<SceneCanvas>` from its `layouts`
|
|
3237
|
+
* prop. Optional: `getLayout` returns null for any container when no layout
|
|
3238
|
+
* is configured, so the reflow pass is a no-op then.
|
|
3239
|
+
*/
|
|
3240
|
+
interface LayoutDep {
|
|
3241
|
+
getLayout(containerId: string): LayoutStrategy<unknown> | null;
|
|
3242
|
+
}
|
|
3243
|
+
/**
|
|
3244
|
+
* Dep for `enterTextEditAction`.
|
|
3245
|
+
*
|
|
3246
|
+
* Wrap the return value of `useTextEdit` / `useSceneTextEdit` to source this
|
|
3247
|
+
* dep. The `isTextNode` predicate is optional — when absent the action fires
|
|
3248
|
+
* unconditionally (the binding spec acts as the gate).
|
|
3249
|
+
*
|
|
3250
|
+
* @example
|
|
3251
|
+
* ```ts
|
|
3252
|
+
* const textEdit = useSceneTextEdit({ scene, container });
|
|
3253
|
+
* useDepSource('textEdit', () => ({
|
|
3254
|
+
* startEdit: textEdit.startEdit,
|
|
3255
|
+
* isTextNode: (id) => scene.get(id as NodeId)?.data?.kind === 'text',
|
|
3256
|
+
* }));
|
|
3257
|
+
* ```
|
|
3258
|
+
*/
|
|
3259
|
+
interface TextEditDep {
|
|
3260
|
+
/**
|
|
3261
|
+
* Begin editing the node with `id`. Activates the contenteditable overlay
|
|
3262
|
+
* managed by `useTextEdit` / `useSceneTextEdit`.
|
|
3263
|
+
*/
|
|
3264
|
+
startEdit(id: string, opts?: {
|
|
3265
|
+
caret?: number | 'all';
|
|
3266
|
+
}): void;
|
|
3267
|
+
/**
|
|
3268
|
+
* Optional predicate: returns `true` when the node with `id` is a text node.
|
|
3269
|
+
* When absent the action fires on any selected node (binding spec is the gate).
|
|
3270
|
+
* When present and returning `false`, the invocation is a no-op.
|
|
3271
|
+
*/
|
|
3272
|
+
isTextNode?(id: string): boolean;
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* Clipboard dep — the imperative surface `useClipboardOps` returns.
|
|
3276
|
+
*
|
|
3277
|
+
* Consumers publish their live clipboard through `useDepSource('clipboard',
|
|
3278
|
+
* …)` from inside the `<DepRegistryProvider>` (i.e. under `<SceneCanvas>`).
|
|
3279
|
+
* The kit deliberately does not build one for them: `useClipboardOps` needs
|
|
3280
|
+
* an adapter and a selection reader that only the consumer can supply.
|
|
3281
|
+
*/
|
|
3282
|
+
interface ClipboardDep {
|
|
3283
|
+
copy(): void;
|
|
3284
|
+
paste(): void;
|
|
3285
|
+
isEmpty(): boolean;
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Consumer-supplied commit for the Slice action. `commit` receives the finite
|
|
3289
|
+
* slice segment (world coords); the consumer scans the scene, splits crossed
|
|
3290
|
+
* paths via `splitPathByLine`, and applies the result as one undoable batch.
|
|
3291
|
+
*/
|
|
3292
|
+
interface SliceDep {
|
|
3293
|
+
commit(a: Point2, b: Point2): void;
|
|
3294
|
+
}
|
|
3295
|
+
/**
|
|
3296
|
+
* The names an action may declare in `requires`, and what each resolves to.
|
|
3297
|
+
*
|
|
3298
|
+
* This is the whole vocabulary of things an action can reach — selection,
|
|
3299
|
+
* scene, view, history, and the rest. The interface itself is declared empty
|
|
3300
|
+
* in `@weasel-js/routing`, which knows that an action names its dependencies
|
|
3301
|
+
* but not what any of them are; these 24 entries are the kit's, merged in from
|
|
3302
|
+
* outside exactly the way a consumer merges its own
|
|
3303
|
+
* (`declare module '@weasel-js/core'`).
|
|
3304
|
+
*/
|
|
3305
|
+
declare module '@weasel-js/routing' {
|
|
3306
|
+
interface DepSchema {
|
|
3307
|
+
/** Kit selection state — ids of currently selected nodes. */
|
|
3308
|
+
selection: SelectionApi;
|
|
3309
|
+
/** Current viewport — camera position + scale. */
|
|
3310
|
+
view: ViewApi;
|
|
3311
|
+
/**
|
|
3312
|
+
* Scene tree — structural reads + undoable mutations.
|
|
3313
|
+
*
|
|
3314
|
+
* The entry uses the fully-erased form `Scene<unknown, string, unknown>`
|
|
3315
|
+
* because `DepSchema` must be concrete. Actions that need a typed scene
|
|
3316
|
+
* should cast: `deps.scene as Scene<MyData, MyLayer, MyPose>`.
|
|
3317
|
+
*/
|
|
3318
|
+
scene: Scene<unknown, string, unknown>;
|
|
3319
|
+
/** Undo/redo history bound to the current scene. */
|
|
3320
|
+
history: History;
|
|
3321
|
+
/**
|
|
3322
|
+
* Canvas pointer position in world space.
|
|
3323
|
+
*
|
|
3324
|
+
* Exposes `pointerRef` (mutable live ref) and `getDropPoint()` thunk.
|
|
3325
|
+
* Marked `@experimental` in the source.
|
|
3326
|
+
*/
|
|
3327
|
+
pointer: PointerContextValue;
|
|
3328
|
+
/** Currently active tool id + hotkey-hold stack. */
|
|
3329
|
+
activeTool: ActiveToolContextValue;
|
|
3330
|
+
/**
|
|
3331
|
+
* Area-select dep — AABB hit-test + selection read/write.
|
|
3332
|
+
*
|
|
3333
|
+
* Sourced from `<SceneCanvas>` via AABB overlap over all scene
|
|
3334
|
+
* nodes. Override per-consumer for custom hit-testing (e.g. contain-mode,
|
|
3335
|
+
* lock-aware filtering).
|
|
3336
|
+
*/
|
|
3337
|
+
areaSelect: AreaSelectDep;
|
|
3338
|
+
/**
|
|
3339
|
+
* Topmost node at a world-space point. Sourced by `<SceneCanvas>` from
|
|
3340
|
+
* the same picker that feeds the tool dispatcher's `getNodeAtPoint`.
|
|
3341
|
+
* Optional: actions that read this (e.g. `moveAction` reparent-on-drop)
|
|
3342
|
+
* fall back to a no-op when the dep isn't registered.
|
|
3343
|
+
*/
|
|
3344
|
+
nodeAtPoint?: NodeAtPointDep;
|
|
3345
|
+
/**
|
|
3346
|
+
* Insert dep — node factory for drag-to-insert.
|
|
3347
|
+
*
|
|
3348
|
+
* Sourced from `<SceneCanvas>`. The `kind` param comes from
|
|
3349
|
+
* the active binding's `opts.params.kind`. Override per-consumer to
|
|
3350
|
+
* provide a typed node factory (e.g. with custom data payloads).
|
|
3351
|
+
*/
|
|
3352
|
+
insert: InsertDep;
|
|
3353
|
+
/**
|
|
3354
|
+
* Snap dep — world-space point snapping (grid / guides).
|
|
3355
|
+
*
|
|
3356
|
+
* Sourced by `<SceneCanvas>` from `toolOptions.snapPoint`. Optional:
|
|
3357
|
+
* absent means no snapping (identity).
|
|
3358
|
+
*/
|
|
3359
|
+
snap?: SnapDep;
|
|
3360
|
+
/**
|
|
3361
|
+
* Lasso-select dep — polygon hit-test + selection read/write.
|
|
3362
|
+
*
|
|
3363
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>`.
|
|
3364
|
+
* Falls back to AABB hit-test when `hitTestLasso` is absent.
|
|
3365
|
+
*/
|
|
3366
|
+
lassoSelect: LassoSelectDep;
|
|
3367
|
+
/**
|
|
3368
|
+
* Edit-anchors dep — narrow read/write of one polygon's path pose.
|
|
3369
|
+
*
|
|
3370
|
+
* Sourced from consumer. Wraps `getPose`/`setPose`/`applyOps`
|
|
3371
|
+
* for the currently-being-edited polygon node.
|
|
3372
|
+
*
|
|
3373
|
+
* The `editAnchorsAction` requires this dep to be registered when anchor
|
|
3374
|
+
* editing is active. If absent, `start` returns an empty handle (no-op).
|
|
3375
|
+
*/
|
|
3376
|
+
editAnchors: EditAnchorsDep;
|
|
3377
|
+
/**
|
|
3378
|
+
* Text-edit dep — activates the in-place text editing overlay.
|
|
3379
|
+
*
|
|
3380
|
+
* Sourced from consumer via `useTextEdit` / `useSceneTextEdit`.
|
|
3381
|
+
* The `enterTextEditAction` requires this dep to be registered by the text
|
|
3382
|
+
* tool when text editing is available.
|
|
3383
|
+
*
|
|
3384
|
+
* The optional `isTextNode` predicate guards against entering edit mode on
|
|
3385
|
+
* non-text nodes. A binding can pre-filter instead with a
|
|
3386
|
+
* `target: 'kind:text:selected'` spec; the guard remains for consumers who
|
|
3387
|
+
* bind the broader `'selected-body'` target or opted out of routing.
|
|
3388
|
+
*/
|
|
3389
|
+
textEdit: TextEditDep;
|
|
3390
|
+
/**
|
|
3391
|
+
* Resize-policy dep — bounds constraints, point-snap behaviors and
|
|
3392
|
+
* group expansion for `resizeAction`.
|
|
3393
|
+
*
|
|
3394
|
+
* Optional: when omitted, `resizeAction` falls back to identity defaults
|
|
3395
|
+
* (no constraints, no snap, identity expandIds).
|
|
3396
|
+
* Consumers wire via `useDepSource('resizePolicy', ...)` or the
|
|
3397
|
+
* `useResizePolicy` helper.
|
|
3398
|
+
*/
|
|
3399
|
+
resizePolicy?: ResizePolicy<unknown>;
|
|
3400
|
+
/**
|
|
3401
|
+
* How to read and rewrite a pose — bounds, translate, remap, rotation. Every
|
|
3402
|
+
* built-in action that touches a pose reads it. Sourced by `<SceneCanvas>`
|
|
3403
|
+
* from its `poseDescriptor` prop; `AUTO_POSE_DESCRIPTOR` when absent.
|
|
3404
|
+
*/
|
|
3405
|
+
poseDescriptor?: PoseDescriptor<unknown>;
|
|
3406
|
+
/**
|
|
3407
|
+
* Booleans adapter — read selection ids, fetch world-space `Path`s,
|
|
3408
|
+
* compare z-order, and mint result nodes for Pathfinder ops.
|
|
3409
|
+
*
|
|
3410
|
+
* Consumers wire via `useBooleansAdapter(adapter)` (a thin wrapper
|
|
3411
|
+
* around `useDepSource('booleansAdapter', ...)`). The descriptor's
|
|
3412
|
+
* `enabled` predicate reads `deps.selection` for the count check; the
|
|
3413
|
+
* invoker reads `deps.booleansAdapter` to execute the op.
|
|
3414
|
+
*/
|
|
3415
|
+
booleansAdapter?: BooleansAdapter;
|
|
3416
|
+
/**
|
|
3417
|
+
* Gesture dispatcher control surface — exposes `cancelAll(reason)` so
|
|
3418
|
+
* actions that need to abort an in-flight handle (Escape cancels a
|
|
3419
|
+
* drag, etc.) can do so. Sourced by `<SceneCanvas>` from the
|
|
3420
|
+
* dispatcher instance it already owns.
|
|
3421
|
+
*/
|
|
3422
|
+
dispatcher?: {
|
|
3423
|
+
cancelAll(reason: 'commit' | 'cancel'): void;
|
|
3424
|
+
};
|
|
3425
|
+
/**
|
|
3426
|
+
* Layout-strategy lookup. Sourced by `<SceneCanvas>` from `layouts`.
|
|
3427
|
+
* Optional: absent (or all-null) → `moveAction` skips reflow.
|
|
3428
|
+
*/
|
|
3429
|
+
layout?: LayoutDep;
|
|
3430
|
+
/**
|
|
3431
|
+
* Slice dep — consumer-supplied commit for the Slice action.
|
|
3432
|
+
*
|
|
3433
|
+
* Receives the finite slice segment in world coordinates; the consumer
|
|
3434
|
+
* scans the scene, splits crossed paths via `splitPathByLine`, and
|
|
3435
|
+
* applies the result as one undoable batch.
|
|
3436
|
+
*
|
|
3437
|
+
* Optional: when absent, `sliceAction` is a no-op.
|
|
3438
|
+
*/
|
|
3439
|
+
slice?: SliceDep;
|
|
3440
|
+
/**
|
|
3441
|
+
* Clipboard dep — the imperative surface `useClipboardOps` returns.
|
|
3442
|
+
*
|
|
3443
|
+
* Published by the consumer (`useDepSource('clipboard', …)` from under
|
|
3444
|
+
* `<SceneCanvas>`), because `useClipboardOps` needs an adapter and a
|
|
3445
|
+
* selection reader only the consumer has. Feeds `clipboard.copy` /
|
|
3446
|
+
* `clipboard.cut`; both no-op when the dep is absent.
|
|
3447
|
+
*/
|
|
3448
|
+
clipboard?: ClipboardDep;
|
|
3449
|
+
/**
|
|
3450
|
+
* Optional consumer commit hook. When present, `moveAction` (and other
|
|
3451
|
+
* default actions) submit their committed ops through it instead of
|
|
3452
|
+
* `scene.applyBatch`, so apps with their own history integration
|
|
3453
|
+
* (checkpoint + push entry) capture the gesture as one undo entry.
|
|
3454
|
+
* When absent, commits fall back to `scene.applyBatch`.
|
|
3455
|
+
*/
|
|
3456
|
+
applyOps?: (ops: Op[], label: string) => void;
|
|
3457
|
+
/** Optional pose-composition strategy for hierarchical (local-pose) scenes.
|
|
3458
|
+
* When absent, defaults to IDENTITY (absolute-pose: nodes store world
|
|
3459
|
+
* coords). Local-pose consumers supply { compose: composeRectPose,
|
|
3460
|
+
* decompose: decomposeRectPose } (or their pose shape's equivalent). */
|
|
3461
|
+
poseComposition?: PoseComposition<unknown>;
|
|
3462
|
+
/**
|
|
3463
|
+
* Ingestion dep — canvas viewport rect + consumer file→src resolver.
|
|
3464
|
+
*
|
|
3465
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
|
|
3466
|
+
* `useIngestionDepSource`. Feeds `ingestAction` with the world-space
|
|
3467
|
+
* viewport rect for paste-placement and image fit-clamping, and forwards
|
|
3468
|
+
* the consumer's optional `resolveSrc` seam.
|
|
3469
|
+
*
|
|
3470
|
+
* Optional: when absent, the `ingest` action no-ops (there is no
|
|
3471
|
+
* placement geometry to work with).
|
|
3472
|
+
*/
|
|
3473
|
+
ingestion?: IngestionDep;
|
|
3474
|
+
/**
|
|
3475
|
+
* Optional consumer seam for the eager-sync layer: lets pose-transform
|
|
3476
|
+
* actions (resize/move/nudge/flip — NOT rotate) ALSO rewrite a node's
|
|
3477
|
+
* data-held geometry. Given a node and the affine `m` applied to its pose,
|
|
3478
|
+
* `transform(node, m)` returns updated `data` (geometry mapped by `m`) or
|
|
3479
|
+
* `null` for nodes with no data-held geometry.
|
|
3480
|
+
*
|
|
3481
|
+
* Strictly opt-in: when absent (or when `transform` returns null), the kit
|
|
3482
|
+
* emits only the pose op and leaves `data` untouched. apps/draw wires this
|
|
3483
|
+
* to mirror `data.path` through `transformPath`. Rotate intentionally never
|
|
3484
|
+
* consults this seam (rotation lives on the pose, baked at render).
|
|
3485
|
+
*/
|
|
3486
|
+
geometryProjection?: GeometryProjection;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
export { type ViewportDims as $, type SliceDep as A, type VertexColorChannel as B, type InsertExtras as C, type DrawCommand as D, type Effect as E, type SerializedScene as F, type GroupDrawCommand as G, type GeometryProjection as H, IDENTITY_COLOR_MATRIX as I, type ContentHandlerEntry as J, type SvgIngestOptions as K, type PanBounds as L, type Mesh as M, type Node as N, type Animator as O, type PathDrawCommand as P, ColorOverrideRegistry as Q, type RenderTarget as R, SPRITE_STRIDE as S, type TextDrawCommand as T, type UseSceneOptions as U, type View as V, WeaselRenderer as W, type SceneRegistry as X, type RegisteredOp as Y, type PoseOverrides as Z, type DerivedDep as _, type ImageDrawCommand as a, type StaggerOptions as a$, ShaderProgram as a0, type BooleansAdapter as a1, type UseAnimatorOptions as a2, type SpringPresetName as a3, type EasingSpec as a4, type AnimationHandle as a5, type SampledTrack as a6, type AddLayerSpec as a7, type AddNodeSpec as a8, type AnimateToBoundsOptions as a9, type LassoSelectDep as aA, type LayerRecord as aB, type LayoutDep as aC, type LeafNode as aD, type LoopFactory as aE, type LoopOptions as aF, type NestedTimeline as aG, type NodeAtPointDep as aH, type PhysicsHandle as aI, type PhysicsOptions as aJ, PointerContextProvider as aK, type PointerContextValue as aL, type PointerWorldPos as aM, type PoseAdapter as aN, type PoseClosure as aO, type PoseOverride as aP, RECT_POSE_COMPOSITION as aQ, RIGID_POSE_COMPOSITION as aR, type ResizePolicy as aS, SPRING_PRESETS as aT, type SerializedNode as aU, type SnapDep as aV, type SpringOptions as aW, type SpringPreset as aX, type StaggerBuilder as aY, type StaggerDelay as aZ, type StaggerFactory as a_, type AreaSelectDep as aa, type BezierEasing as ab, type BooleanOp as ac, type BooleanOpResult as ad, type ClipboardDep as ae, type ClipboardIngestCtx as af, type ColorOverride as ag, type ColorOverrideFn as ah, type ContainerNode as ai, type DecayLoopConfig as aj, type DecayOptions as ak, EASINGS as al, type EasingFn as am, type EasingName as an, type EditAnchorsDep as ao, type EventBooking as ap, type EventBookingHandle as aq, type EventTrack as ar, type FitViewToBoundsOptions as as, IDENTITY_POSE_COMPOSITION as at, type IngestCtx as au, type IngestionDep as av, type InsertDep as aw, type Interpolate as ax, type InterpolatorFactory as ay, type Keyframe as az, type ImageMinification as b, rebaseLocalPose as b$, type StaggerPerItem as b0, type StaggerSpringPoseOptions as b1, type StaggerTweenOptions as b2, type SvgUnpacker as b3, type SystemLayerRecord as b4, type SystemLayerSpec as b5, type TextEditDep as b6, type TimelineClock as b7, type TimelineEvent as b8, type TimelineHandle as b9, easeInOutBack as bA, easeInOutBounce as bB, easeInOutCirc as bC, easeInOutCubic as bD, easeInOutElastic as bE, easeInOutExpo as bF, easeInOutQuad as bG, easeInOutQuart as bH, easeInOutQuint as bI, easeInOutSine as bJ, easeInQuad as bK, easeInQuart as bL, easeInQuint as bM, easeInSine as bN, easeOut as bO, easeOutBack as bP, easeOutBounce as bQ, easeOutCirc as bR, easeOutCubic as bS, easeOutElastic as bT, easeOutExpo as bU, easeOutQuad as bV, easeOutQuart as bW, easeOutQuint as bX, easeOutSine as bY, fitViewToBounds as bZ, linear as b_, type TimelineOptions as ba, type TimelineTrack as bb, type Track as bc, type TweenLoopOptions as bd, type TweenOptions as be, type UserLayerRecord as bf, VIEW_ANIMATION_KEY as bg, type ViewAnimationApi as bh, type ViewApi as bi, type ViewChannel as bj, applyBooleanOp as bk, asNodeId as bl, composeRectPose as bm, composeRigidPose as bn, composeWorldPose as bo, cubicBezierEasing as bp, decomposeRectPose as bq, decomposeRigidPose as br, easeIn as bs, easeInBack as bt, easeInBounce as bu, easeInCirc as bv, easeInCubic as bw, easeInElastic as bx, easeInExpo as by, easeInOut as bz, type Mat3 as c, registerContentHandler as c0, resolveEasing as c1, resolveStrokeWidth as c2, translateRectPose as c3, useDecayLoop as c4, usePointerContext as c5, useViewAnimation as c6, worldPoseLookup as c7, ShaderCompileError as d, type ShaderDrawCommand as e, type ShaderProgramHandle as f, type ShaderUniform as g, type SolidPaint as h, type SpriteSheet as i, type SpritesDrawCommand as j, type StrokeOptions as k, type WeaselRendererOptions as l, blur as m, buildGradientRamp as n, frameRect as o, mat3 as p, registerProgram as q, registerEffect as r, vignette as s, tessellateStroke as t, type Scene as u, viewToMat3 as v, type RectPose as w, type InertiaConfig as x, type ViewAnimationOptions as y, type PoseComposition as z };
|