@twick/2d 0.14.0 → 1.14.3

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.
Files changed (186) hide show
  1. package/LICENSE +21 -21
  2. package/editor/editor/tsconfig.build.tsbuildinfo +1 -1
  3. package/lib/components/Audio.d.ts.map +1 -1
  4. package/lib/components/Audio.js +33 -3
  5. package/lib/components/CodeBlock.d.ts +1 -1
  6. package/lib/components/Img.js +23 -23
  7. package/lib/components/Line.js +31 -31
  8. package/lib/components/Media.d.ts +6 -0
  9. package/lib/components/Media.d.ts.map +1 -1
  10. package/lib/components/Media.js +277 -61
  11. package/lib/components/Node.d.ts +1 -1
  12. package/lib/components/Path.d.ts +1 -1
  13. package/lib/components/SVG.d.ts +1 -1
  14. package/lib/components/Shape.d.ts +1 -1
  15. package/lib/components/Spline.js +25 -25
  16. package/lib/components/Video.d.ts +0 -1
  17. package/lib/components/Video.d.ts.map +1 -1
  18. package/lib/components/Video.js +70 -65
  19. package/lib/tsconfig.build.tsbuildinfo +1 -1
  20. package/package.json +4 -5
  21. package/src/editor/NodeInspectorConfig.tsx +76 -76
  22. package/src/editor/PreviewOverlayConfig.tsx +67 -67
  23. package/src/editor/Provider.tsx +93 -93
  24. package/src/editor/SceneGraphTabConfig.tsx +81 -81
  25. package/src/editor/icons/CircleIcon.tsx +7 -7
  26. package/src/editor/icons/CodeBlockIcon.tsx +8 -8
  27. package/src/editor/icons/CurveIcon.tsx +7 -7
  28. package/src/editor/icons/GridIcon.tsx +7 -7
  29. package/src/editor/icons/IconMap.ts +35 -35
  30. package/src/editor/icons/ImgIcon.tsx +8 -8
  31. package/src/editor/icons/LayoutIcon.tsx +9 -9
  32. package/src/editor/icons/LineIcon.tsx +7 -7
  33. package/src/editor/icons/NodeIcon.tsx +7 -7
  34. package/src/editor/icons/RayIcon.tsx +7 -7
  35. package/src/editor/icons/RectIcon.tsx +7 -7
  36. package/src/editor/icons/ShapeIcon.tsx +7 -7
  37. package/src/editor/icons/TxtIcon.tsx +8 -8
  38. package/src/editor/icons/VideoIcon.tsx +7 -7
  39. package/src/editor/icons/View2DIcon.tsx +10 -10
  40. package/src/editor/index.ts +17 -17
  41. package/src/editor/tree/DetachedRoot.tsx +23 -23
  42. package/src/editor/tree/NodeElement.tsx +74 -74
  43. package/src/editor/tree/TreeElement.tsx +72 -72
  44. package/src/editor/tree/TreeRoot.tsx +10 -10
  45. package/src/editor/tree/ViewRoot.tsx +20 -20
  46. package/src/editor/tree/index.module.scss +38 -38
  47. package/src/editor/tree/index.ts +3 -3
  48. package/src/editor/tsconfig.build.json +5 -5
  49. package/src/editor/tsconfig.json +12 -12
  50. package/src/editor/tsdoc.json +4 -4
  51. package/src/editor/vite-env.d.ts +1 -1
  52. package/src/lib/code/CodeCursor.ts +445 -445
  53. package/src/lib/code/CodeDiffer.ts +78 -78
  54. package/src/lib/code/CodeFragment.ts +97 -97
  55. package/src/lib/code/CodeHighlighter.ts +75 -75
  56. package/src/lib/code/CodeMetrics.ts +47 -47
  57. package/src/lib/code/CodeRange.test.ts +74 -74
  58. package/src/lib/code/CodeRange.ts +216 -216
  59. package/src/lib/code/CodeScope.ts +101 -101
  60. package/src/lib/code/CodeSelection.ts +24 -24
  61. package/src/lib/code/CodeSignal.ts +327 -327
  62. package/src/lib/code/CodeTokenizer.ts +54 -54
  63. package/src/lib/code/DefaultHighlightStyle.ts +98 -98
  64. package/src/lib/code/LezerHighlighter.ts +113 -113
  65. package/src/lib/code/diff.test.ts +311 -311
  66. package/src/lib/code/diff.ts +319 -319
  67. package/src/lib/code/extractRange.ts +126 -126
  68. package/src/lib/code/index.ts +13 -13
  69. package/src/lib/components/Audio.ts +168 -131
  70. package/src/lib/components/Bezier.ts +105 -105
  71. package/src/lib/components/Circle.ts +266 -266
  72. package/src/lib/components/Code.ts +526 -526
  73. package/src/lib/components/CodeBlock.ts +576 -576
  74. package/src/lib/components/CubicBezier.ts +112 -112
  75. package/src/lib/components/Curve.ts +455 -455
  76. package/src/lib/components/Grid.ts +135 -135
  77. package/src/lib/components/Icon.ts +96 -96
  78. package/src/lib/components/Img.ts +319 -319
  79. package/src/lib/components/Knot.ts +157 -157
  80. package/src/lib/components/Latex.ts +122 -122
  81. package/src/lib/components/Layout.ts +1092 -1092
  82. package/src/lib/components/Line.ts +429 -429
  83. package/src/lib/components/Media.ts +576 -346
  84. package/src/lib/components/Node.ts +1940 -1940
  85. package/src/lib/components/Path.ts +137 -137
  86. package/src/lib/components/Polygon.ts +171 -171
  87. package/src/lib/components/QuadBezier.ts +100 -100
  88. package/src/lib/components/Ray.ts +125 -125
  89. package/src/lib/components/Rect.ts +187 -187
  90. package/src/lib/components/Rive.ts +156 -156
  91. package/src/lib/components/SVG.ts +797 -797
  92. package/src/lib/components/Shape.ts +143 -143
  93. package/src/lib/components/Spline.ts +344 -344
  94. package/src/lib/components/Txt.test.tsx +81 -81
  95. package/src/lib/components/Txt.ts +203 -203
  96. package/src/lib/components/TxtLeaf.ts +205 -205
  97. package/src/lib/components/Video.ts +461 -462
  98. package/src/lib/components/View2D.ts +98 -98
  99. package/src/lib/components/__tests__/children.test.tsx +142 -142
  100. package/src/lib/components/__tests__/clone.test.tsx +126 -126
  101. package/src/lib/components/__tests__/generatorTest.ts +28 -28
  102. package/src/lib/components/__tests__/mockScene2D.ts +45 -45
  103. package/src/lib/components/__tests__/query.test.tsx +122 -122
  104. package/src/lib/components/__tests__/state.test.tsx +60 -60
  105. package/src/lib/components/index.ts +28 -28
  106. package/src/lib/components/types.ts +35 -35
  107. package/src/lib/curves/ArcSegment.ts +159 -159
  108. package/src/lib/curves/CircleSegment.ts +77 -77
  109. package/src/lib/curves/CubicBezierSegment.ts +78 -78
  110. package/src/lib/curves/CurveDrawingInfo.ts +11 -11
  111. package/src/lib/curves/CurvePoint.ts +15 -15
  112. package/src/lib/curves/CurveProfile.ts +7 -7
  113. package/src/lib/curves/KnotInfo.ts +10 -10
  114. package/src/lib/curves/LineSegment.ts +62 -62
  115. package/src/lib/curves/Polynomial.ts +355 -355
  116. package/src/lib/curves/Polynomial2D.ts +62 -62
  117. package/src/lib/curves/PolynomialSegment.ts +124 -124
  118. package/src/lib/curves/QuadBezierSegment.ts +64 -64
  119. package/src/lib/curves/Segment.ts +17 -17
  120. package/src/lib/curves/UniformPolynomialCurveSampler.ts +94 -94
  121. package/src/lib/curves/createCurveProfileLerp.ts +471 -471
  122. package/src/lib/curves/getBezierSplineProfile.ts +223 -223
  123. package/src/lib/curves/getCircleProfile.ts +86 -86
  124. package/src/lib/curves/getPathProfile.ts +178 -178
  125. package/src/lib/curves/getPointAtDistance.ts +21 -21
  126. package/src/lib/curves/getPolylineProfile.test.ts +21 -21
  127. package/src/lib/curves/getPolylineProfile.ts +89 -89
  128. package/src/lib/curves/getRectProfile.ts +139 -139
  129. package/src/lib/curves/index.ts +16 -16
  130. package/src/lib/decorators/canvasStyleSignal.ts +16 -16
  131. package/src/lib/decorators/colorSignal.ts +9 -9
  132. package/src/lib/decorators/compound.ts +72 -72
  133. package/src/lib/decorators/computed.ts +18 -18
  134. package/src/lib/decorators/defaultStyle.ts +18 -18
  135. package/src/lib/decorators/filtersSignal.ts +136 -136
  136. package/src/lib/decorators/index.ts +10 -10
  137. package/src/lib/decorators/initializers.ts +32 -32
  138. package/src/lib/decorators/nodeName.ts +13 -13
  139. package/src/lib/decorators/signal.test.ts +90 -90
  140. package/src/lib/decorators/signal.ts +345 -345
  141. package/src/lib/decorators/spacingSignal.ts +15 -15
  142. package/src/lib/decorators/vector2Signal.ts +30 -30
  143. package/src/lib/globals.d.ts +2 -2
  144. package/src/lib/index.ts +8 -8
  145. package/src/lib/jsx-dev-runtime.ts +2 -2
  146. package/src/lib/jsx-runtime.ts +46 -46
  147. package/src/lib/parse-svg-path.d.ts +14 -14
  148. package/src/lib/partials/Filter.ts +180 -180
  149. package/src/lib/partials/Gradient.ts +102 -102
  150. package/src/lib/partials/Pattern.ts +34 -34
  151. package/src/lib/partials/ShaderConfig.ts +117 -117
  152. package/src/lib/partials/index.ts +4 -4
  153. package/src/lib/partials/types.ts +58 -58
  154. package/src/lib/scenes/Scene2D.ts +242 -242
  155. package/src/lib/scenes/index.ts +3 -3
  156. package/src/lib/scenes/makeScene2D.ts +16 -16
  157. package/src/lib/scenes/useScene2D.ts +6 -6
  158. package/src/lib/tsconfig.build.json +5 -5
  159. package/src/lib/tsconfig.json +10 -10
  160. package/src/lib/tsdoc.json +4 -4
  161. package/src/lib/utils/CanvasUtils.ts +306 -306
  162. package/src/lib/utils/diff.test.ts +453 -453
  163. package/src/lib/utils/diff.ts +148 -148
  164. package/src/lib/utils/index.ts +2 -2
  165. package/src/lib/utils/is.ts +11 -11
  166. package/src/lib/utils/makeSignalExtensions.ts +30 -30
  167. package/src/lib/utils/video/declarations.d.ts +1 -1
  168. package/src/lib/utils/video/ffmpeg-client.ts +50 -50
  169. package/src/lib/utils/video/mp4-parser-manager.ts +72 -72
  170. package/src/lib/utils/video/parser/index.ts +1 -1
  171. package/src/lib/utils/video/parser/parser.ts +257 -257
  172. package/src/lib/utils/video/parser/sampler.ts +72 -72
  173. package/src/lib/utils/video/parser/segment.ts +302 -302
  174. package/src/lib/utils/video/parser/sink.ts +29 -29
  175. package/src/lib/utils/video/parser/utils.ts +31 -31
  176. package/src/tsconfig.base.json +19 -19
  177. package/src/tsconfig.build.json +8 -8
  178. package/src/tsconfig.json +5 -5
  179. package/tsconfig.project.json +7 -7
  180. package/lib/components/utils/waitUntil.d.ts +0 -7
  181. package/lib/components/utils/waitUntil.d.ts.map +0 -1
  182. package/lib/components/utils/waitUntil.js +0 -15
  183. package/lib/utils/waitUntil.d.ts +0 -7
  184. package/lib/utils/waitUntil.d.ts.map +0 -1
  185. package/lib/utils/waitUntil.js +0 -15
  186. package/src/lib/utils/waitUntil.ts +0 -18
@@ -1,1940 +1,1940 @@
1
- import type {
2
- ColorSignal,
3
- PossibleColor,
4
- PossibleSpacing,
5
- PossibleVector2,
6
- Promisable,
7
- ReferenceReceiver,
8
- Signal,
9
- SignalValue,
10
- SimpleSignal,
11
- SimpleVector2Signal,
12
- SpacingSignal,
13
- ThreadGenerator,
14
- TimingFunction,
15
- Vector2Signal,
16
- } from '@twick/core';
17
- import {
18
- BBox,
19
- DependencyContext,
20
- UNIFORM_DESTINATION_MATRIX,
21
- UNIFORM_SOURCE_MATRIX,
22
- UNIFORM_TIME,
23
- Vector2,
24
- all,
25
- clamp,
26
- createSignal,
27
- easeInOutCubic,
28
- isReactive,
29
- modify,
30
- threadable,
31
- transformAngle,
32
- transformScalar,
33
- transformVector,
34
- transformVectorAsPoint,
35
- unwrap,
36
- useLogger,
37
- } from '@twick/core';
38
- import {
39
- NODE_NAME,
40
- cloneable,
41
- colorSignal,
42
- computed,
43
- getPropertiesOf,
44
- initial,
45
- initializeSignals,
46
- inspectable,
47
- nodeName,
48
- parser,
49
- signal,
50
- vector2Signal,
51
- wrapper,
52
- } from '../decorators';
53
- import type {FiltersSignal} from '../decorators/filtersSignal';
54
- import {filtersSignal} from '../decorators/filtersSignal';
55
- import {spacingSignal} from '../decorators/spacingSignal';
56
- import type {Filter} from '../partials';
57
- import type {
58
- PossibleShaderConfig,
59
- ShaderConfig,
60
- } from '../partials/ShaderConfig';
61
- import {parseShader} from '../partials/ShaderConfig';
62
- import {useScene2D} from '../scenes/useScene2D';
63
- import {drawLine} from '../utils';
64
- import type {View2D} from './View2D';
65
- import type {ComponentChild, ComponentChildren, NodeConstructor} from './types';
66
-
67
- export type NodeState = NodeProps & Record<string, any>;
68
-
69
- export interface NodeProps {
70
- ref?: ReferenceReceiver<any>;
71
- children?: SignalValue<ComponentChildren>;
72
- /**
73
- * @deprecated Use {@link children} instead.
74
- */
75
- spawner?: SignalValue<ComponentChildren>;
76
- key?: string;
77
-
78
- x?: SignalValue<number>;
79
- y?: SignalValue<number>;
80
- position?: SignalValue<PossibleVector2>;
81
- rotation?: SignalValue<number>;
82
- scaleX?: SignalValue<number>;
83
- scaleY?: SignalValue<number>;
84
- scale?: SignalValue<PossibleVector2>;
85
- skewX?: SignalValue<number>;
86
- skewY?: SignalValue<number>;
87
- skew?: SignalValue<PossibleVector2>;
88
- zIndex?: SignalValue<number>;
89
-
90
- opacity?: SignalValue<number>;
91
- filters?: SignalValue<Filter[]>;
92
-
93
- shadowColor?: SignalValue<PossibleColor>;
94
- shadowBlur?: SignalValue<number>;
95
- shadowOffsetX?: SignalValue<number>;
96
- shadowOffsetY?: SignalValue<number>;
97
- shadowOffset?: SignalValue<PossibleVector2>;
98
-
99
- cache?: SignalValue<boolean>;
100
- /**
101
- * {@inheritDoc Node.cachePadding}
102
- */
103
- cachePaddingTop?: SignalValue<number>;
104
- /**
105
- * {@inheritDoc Node.cachePadding}
106
- */
107
- cachePaddingBottom?: SignalValue<number>;
108
- /**
109
- * {@inheritDoc Node.cachePadding}
110
- */
111
- cachePaddingLeft?: SignalValue<number>;
112
- /**
113
- * {@inheritDoc Node.cachePadding}
114
- */
115
- cachePaddingRight?: SignalValue<number>;
116
- /**
117
- * {@inheritDoc Node.cachePadding}
118
- */
119
- cachePadding?: SignalValue<PossibleSpacing>;
120
-
121
- composite?: SignalValue<boolean>;
122
- compositeOperation?: SignalValue<GlobalCompositeOperation>;
123
- /**
124
- * @experimental
125
- */
126
- shaders?: PossibleShaderConfig;
127
- }
128
-
129
- @nodeName('Node')
130
- export class Node implements Promisable<Node> {
131
- /**
132
- * @internal
133
- */
134
- public declare readonly [NODE_NAME]: string;
135
- public declare isClass: boolean;
136
-
137
- /**
138
- * Represents the position of this node in local space of its parent.
139
- *
140
- * @example
141
- * Initializing the position:
142
- * ```tsx
143
- * // with a possible vector:
144
- * <Node position={[1, 2]} />
145
- * // with individual components:
146
- * <Node x={1} y={2} />
147
- * ```
148
- *
149
- * Accessing the position:
150
- * ```tsx
151
- * // retrieving the vector:
152
- * const position = node.position();
153
- * // retrieving an individual component:
154
- * const x = node.position.x();
155
- * ```
156
- *
157
- * Setting the position:
158
- * ```tsx
159
- * // with a possible vector:
160
- * node.position([1, 2]);
161
- * node.position(() => [1, 2]);
162
- * // with individual components:
163
- * node.position.x(1);
164
- * node.position.x(() => 1);
165
- * ```
166
- */
167
- @vector2Signal()
168
- public declare readonly position: Vector2Signal<this>;
169
-
170
- public get x() {
171
- return this.position.x as SimpleSignal<number, this>;
172
- }
173
- public get y() {
174
- return this.position.y as SimpleSignal<number, this>;
175
- }
176
-
177
- /**
178
- * A helper signal for operating on the position in world space.
179
- *
180
- * @remarks
181
- * Retrieving the position using this signal returns the position in world
182
- * space. Similarly, setting the position using this signal transforms the
183
- * new value to local space.
184
- *
185
- * If the new value is a function, the position of this node will be
186
- * continuously updated to always match the position returned by the function.
187
- * This can be useful to "pin" the node in a specific place or to make it
188
- * follow another node's position.
189
- *
190
- * Unlike {@link position}, this signal is not compound - it doesn't contain
191
- * separate signals for the `x` and `y` components.
192
- */
193
- @wrapper(Vector2)
194
- @cloneable(false)
195
- @signal()
196
- public declare readonly absolutePosition: SimpleVector2Signal<this>;
197
-
198
- protected getAbsolutePosition(): Vector2 {
199
- const matrix = this.localToWorld();
200
- return new Vector2(matrix.m41, matrix.m42);
201
- }
202
-
203
- protected setAbsolutePosition(value: SignalValue<PossibleVector2>) {
204
- this.position(
205
- modify(value, unwrapped =>
206
- transformVectorAsPoint(new Vector2(unwrapped), this.worldToParent()),
207
- ),
208
- );
209
- }
210
-
211
- /**
212
- * Represents the rotation (in degrees) of this node relative to its parent.
213
- */
214
- @initial(0)
215
- @signal()
216
- public declare readonly rotation: SimpleSignal<number, this>;
217
-
218
- /**
219
- * A helper signal for operating on the rotation in world space.
220
- *
221
- * @remarks
222
- * Retrieving the rotation using this signal returns the rotation in world
223
- * space. Similarly, setting the rotation using this signal transforms the
224
- * new value to local space.
225
- *
226
- * If the new value is a function, the rotation of this node will be
227
- * continuously updated to always match the rotation returned by the function.
228
- */
229
- @cloneable(false)
230
- @signal()
231
- public declare readonly absoluteRotation: SimpleSignal<number, this>;
232
-
233
- protected getAbsoluteRotation() {
234
- const matrix = this.localToWorld();
235
- return Vector2.degrees(matrix.m11, matrix.m12);
236
- }
237
-
238
- protected setAbsoluteRotation(value: SignalValue<number>) {
239
- this.rotation(
240
- modify(value, unwrapped =>
241
- transformAngle(unwrapped, this.worldToParent()),
242
- ),
243
- );
244
- }
245
-
246
- /**
247
- * Represents the scale of this node in local space of its parent.
248
- *
249
- * @example
250
- * Initializing the scale:
251
- * ```tsx
252
- * // with a possible vector:
253
- * <Node scale={[1, 2]} />
254
- * // with individual components:
255
- * <Node scaleX={1} scaleY={2} />
256
- * ```
257
- *
258
- * Accessing the scale:
259
- * ```tsx
260
- * // retrieving the vector:
261
- * const scale = node.scale();
262
- * // retrieving an individual component:
263
- * const scaleX = node.scale.x();
264
- * ```
265
- *
266
- * Setting the scale:
267
- * ```tsx
268
- * // with a possible vector:
269
- * node.scale([1, 2]);
270
- * node.scale(() => [1, 2]);
271
- * // with individual components:
272
- * node.scale.x(1);
273
- * node.scale.x(() => 1);
274
- * ```
275
- */
276
- @initial(Vector2.one)
277
- @vector2Signal('scale')
278
- public declare readonly scale: Vector2Signal<this>;
279
-
280
- /**
281
- * Represents the skew of this node in local space of its parent.
282
- *
283
- * @example
284
- * Initializing the skew:
285
- * ```tsx
286
- * // with a possible vector:
287
- * <Node skew={[40, 20]} />
288
- * // with individual components:
289
- * <Node skewX={40} skewY={20} />
290
- * ```
291
- *
292
- * Accessing the skew:
293
- * ```tsx
294
- * // retrieving the vector:
295
- * const skew = node.skew();
296
- * // retrieving an individual component:
297
- * const skewX = node.skew.x();
298
- * ```
299
- *
300
- * Setting the skew:
301
- * ```tsx
302
- * // with a possible vector:
303
- * node.skew([40, 20]);
304
- * node.skew(() => [40, 20]);
305
- * // with individual components:
306
- * node.skew.x(40);
307
- * node.skew.x(() => 40);
308
- * ```
309
- */
310
- @initial(Vector2.zero)
311
- @vector2Signal('skew')
312
- public declare readonly skew: Vector2Signal<this>;
313
-
314
- /**
315
- * A helper signal for operating on the scale in world space.
316
- *
317
- * @remarks
318
- * Retrieving the scale using this signal returns the scale in world space.
319
- * Similarly, setting the scale using this signal transforms the new value to
320
- * local space.
321
- *
322
- * If the new value is a function, the scale of this node will be continuously
323
- * updated to always match the position returned by the function.
324
- *
325
- * Unlike {@link scale}, this signal is not compound - it doesn't contain
326
- * separate signals for the `x` and `y` components.
327
- */
328
- @wrapper(Vector2)
329
- @cloneable(false)
330
- @signal()
331
- public declare readonly absoluteScale: SimpleVector2Signal<this>;
332
-
333
- protected getAbsoluteScale(): Vector2 {
334
- const matrix = this.localToWorld();
335
- return new Vector2(
336
- Vector2.magnitude(matrix.m11, matrix.m12),
337
- Vector2.magnitude(matrix.m21, matrix.m22),
338
- );
339
- }
340
-
341
- protected setAbsoluteScale(value: SignalValue<PossibleVector2>) {
342
- this.scale(
343
- modify(value, unwrapped => this.getRelativeScale(new Vector2(unwrapped))),
344
- );
345
- }
346
-
347
- private getRelativeScale(scale: Vector2): Vector2 {
348
- const parentScale = this.parent()?.absoluteScale() ?? Vector2.one;
349
- return scale.div(parentScale);
350
- }
351
-
352
- @initial(0)
353
- @signal()
354
- public declare readonly zIndex: SimpleSignal<number, this>;
355
-
356
- @initial(false)
357
- @signal()
358
- public declare readonly cache: SimpleSignal<boolean, this>;
359
-
360
- /**
361
- * Controls the padding of the cached canvas used by this node.
362
- *
363
- * @remarks
364
- * By default, the size of the cache is determined based on the bounding box
365
- * of the node and its children. That includes effects such as stroke or
366
- * shadow. This property can be used to expand the cache area further.
367
- * Usually used to account for custom effects created by {@link shaders}.
368
- */
369
- @spacingSignal('cachePadding')
370
- public declare readonly cachePadding: SpacingSignal<this>;
371
-
372
- @initial(false)
373
- @signal()
374
- public declare readonly composite: SimpleSignal<boolean, this>;
375
-
376
- @initial('source-over')
377
- @signal()
378
- public declare readonly compositeOperation: SimpleSignal<
379
- GlobalCompositeOperation,
380
- this
381
- >;
382
-
383
- private readonly compositeOverride = createSignal(0);
384
-
385
- @threadable()
386
- protected *tweenCompositeOperation(
387
- value: SignalValue<GlobalCompositeOperation>,
388
- time: number,
389
- timingFunction: TimingFunction,
390
- ) {
391
- const nextValue = unwrap(value);
392
- if (nextValue === 'source-over') {
393
- yield* this.compositeOverride(1, time, timingFunction);
394
- this.compositeOverride(0);
395
- this.compositeOperation(nextValue);
396
- } else {
397
- this.compositeOperation(nextValue);
398
- this.compositeOverride(1);
399
- yield* this.compositeOverride(0, time, timingFunction);
400
- }
401
- }
402
-
403
- /**
404
- * Represents the opacity of this node in the range 0-1.
405
- *
406
- * @remarks
407
- * The value is clamped to the range 0-1.
408
- */
409
- @initial(1)
410
- @parser((value: number) => clamp(0, 1, value))
411
- @signal()
412
- public declare readonly opacity: SimpleSignal<number, this>;
413
-
414
- @computed()
415
- public absoluteOpacity(): number {
416
- return (this.parent()?.absoluteOpacity() ?? 1) * this.opacity();
417
- }
418
-
419
- @filtersSignal()
420
- public declare readonly filters: FiltersSignal<this>;
421
-
422
- @initial('#0000')
423
- @colorSignal()
424
- public declare readonly shadowColor: ColorSignal<this>;
425
-
426
- @initial(0)
427
- @signal()
428
- public declare readonly shadowBlur: SimpleSignal<number, this>;
429
-
430
- @vector2Signal('shadowOffset')
431
- public declare readonly shadowOffset: Vector2Signal<this>;
432
-
433
- /**
434
- * @experimental
435
- */
436
- @initial([])
437
- @parser(parseShader)
438
- @signal()
439
- public declare readonly shaders: Signal<
440
- PossibleShaderConfig,
441
- ShaderConfig[],
442
- this
443
- >;
444
-
445
- @computed()
446
- protected hasFilters(): boolean {
447
- return !!this.filters().find(filter => filter.isActive());
448
- }
449
-
450
- @computed()
451
- protected hasShadow() {
452
- return (
453
- !!this.shadowColor() &&
454
- (this.shadowBlur() > 0 ||
455
- this.shadowOffset.x() !== 0 ||
456
- this.shadowOffset.y() !== 0)
457
- );
458
- }
459
-
460
- @computed()
461
- protected filterString(): string {
462
- let filters = '';
463
- const matrix = this.compositeToWorld();
464
- for (const filter of this.filters()) {
465
- if (filter.isActive()) {
466
- filters += ' ' + filter.serialize(matrix);
467
- }
468
- }
469
-
470
- return filters;
471
- }
472
-
473
- /**
474
- * @deprecated Use {@link children} instead.
475
- */
476
- @inspectable(false)
477
- @cloneable(false)
478
- @signal()
479
- protected declare readonly spawner: SimpleSignal<ComponentChildren, this>;
480
- protected getSpawner(): ComponentChildren {
481
- return this.children();
482
- }
483
- protected setSpawner(value: SignalValue<ComponentChildren>) {
484
- this.children(value);
485
- }
486
-
487
- @inspectable(false)
488
- @cloneable(false)
489
- @signal()
490
- public declare readonly children: Signal<ComponentChildren, Node[], this>;
491
- protected setChildren(value: SignalValue<ComponentChildren>) {
492
- if (this.children.context.raw() === value) {
493
- return;
494
- }
495
-
496
- this.children.context.setter(value);
497
- if (!isReactive(value)) {
498
- this.spawnChildren(false, value);
499
- } else if (!this.hasSpawnedChildren) {
500
- for (const oldChild of this.realChildren) {
501
- oldChild.parent(null);
502
- }
503
- }
504
- }
505
- protected getChildren(): Node[] {
506
- this.children.context.getter();
507
- return this.spawnedChildren();
508
- }
509
-
510
- @computed()
511
- protected spawnedChildren(): Node[] {
512
- const children = this.children.context.getter();
513
- if (isReactive(this.children.context.raw())) {
514
- this.spawnChildren(true, children);
515
- }
516
- return this.realChildren;
517
- }
518
-
519
- @computed()
520
- protected sortedChildren(): Node[] {
521
- return [...this.children()].sort((a, b) =>
522
- Math.sign(a.zIndex() - b.zIndex()),
523
- );
524
- }
525
-
526
- protected view2D: View2D;
527
- private stateStack: NodeState[] = [];
528
- protected realChildren: Node[] = [];
529
- protected hasSpawnedChildren = false;
530
- private unregister: () => void;
531
- public readonly parent = createSignal<Node | null>(null);
532
- public readonly properties = getPropertiesOf(this);
533
- public readonly key: string;
534
- public readonly creationStack?: string;
535
-
536
- public constructor({children, spawner, key, ...rest}: NodeProps) {
537
- const scene = useScene2D();
538
- [this.key, this.unregister] = scene.registerNode(this, key);
539
- this.view2D = scene.getView();
540
- this.creationStack = new Error().stack;
541
- initializeSignals(this, rest);
542
- if (spawner) {
543
- useLogger().warn({
544
- message: 'Node.spawner() has been deprecated.',
545
- remarks: 'Use <code>Node.children()</code> instead.',
546
- inspect: this.key,
547
- stack: new Error().stack,
548
- });
549
- }
550
- this.children(spawner ?? children);
551
- }
552
-
553
- /**
554
- * Get the local-to-world matrix for this node.
555
- *
556
- * @remarks
557
- * This matrix transforms vectors from local space of this node to world
558
- * space.
559
- *
560
- * @example
561
- * Calculate the absolute position of a point located 200 pixels to the right
562
- * of the node:
563
- * ```ts
564
- * const local = new Vector2(0, 200);
565
- * const world = transformVectorAsPoint(local, node.localToWorld());
566
- * ```
567
- */
568
- @computed()
569
- public localToWorld(): DOMMatrix {
570
- const parent = this.parent();
571
- return parent
572
- ? parent.localToWorld().multiply(this.localToParent())
573
- : this.localToParent();
574
- }
575
-
576
- /**
577
- * Get the world-to-local matrix for this node.
578
- *
579
- * @remarks
580
- * This matrix transforms vectors from world space to local space of this
581
- * node.
582
- *
583
- * @example
584
- * Calculate the position relative to this node for a point located in the
585
- * top-left corner of the screen:
586
- * ```ts
587
- * const world = new Vector2(0, 0);
588
- * const local = transformVectorAsPoint(world, node.worldToLocal());
589
- * ```
590
- */
591
- @computed()
592
- public worldToLocal() {
593
- return this.localToWorld().inverse();
594
- }
595
-
596
- /**
597
- * Get the world-to-parent matrix for this node.
598
- *
599
- * @remarks
600
- * This matrix transforms vectors from world space to local space of this
601
- * node's parent.
602
- */
603
- @computed()
604
- public worldToParent(): DOMMatrix {
605
- return this.parent()?.worldToLocal() ?? new DOMMatrix();
606
- }
607
-
608
- /**
609
- * Get the local-to-parent matrix for this node.
610
- *
611
- * @remarks
612
- * This matrix transforms vectors from local space of this node to local space
613
- * of this node's parent.
614
- */
615
- @computed()
616
- public localToParent(): DOMMatrix {
617
- const matrix = new DOMMatrix();
618
- matrix.translateSelf(this.x(), this.y());
619
- matrix.rotateSelf(0, 0, this.rotation());
620
- matrix.scaleSelf(this.scale.x(), this.scale.y());
621
- matrix.skewXSelf(this.skew.x());
622
- matrix.skewYSelf(this.skew.y());
623
-
624
- return matrix;
625
- }
626
-
627
- /**
628
- * A matrix mapping composite space to world space.
629
- *
630
- * @remarks
631
- * Certain effects such as blur and shadows ignore the current transformation.
632
- * This matrix can be used to transform their parameters so that the effect
633
- * appears relative to the closest composite root.
634
- */
635
- @computed()
636
- public compositeToWorld(): DOMMatrix {
637
- return this.compositeRoot()?.localToWorld() ?? new DOMMatrix();
638
- }
639
-
640
- @computed()
641
- protected compositeRoot(): Node | null {
642
- if (this.composite()) {
643
- return this;
644
- }
645
-
646
- return this.parent()?.compositeRoot() ?? null;
647
- }
648
-
649
- @computed()
650
- public compositeToLocal() {
651
- const root = this.compositeRoot();
652
- if (root) {
653
- const worldToLocal = this.worldToLocal();
654
- worldToLocal.m44 = 1;
655
- return root.localToWorld().multiply(worldToLocal);
656
- }
657
- return new DOMMatrix();
658
- }
659
-
660
- public view(): View2D {
661
- return this.view2D;
662
- }
663
-
664
- /**
665
- * Add the given node(s) as the children of this node.
666
- *
667
- * @remarks
668
- * The nodes will be appended at the end of the children list.
669
- *
670
- * @example
671
- * ```tsx
672
- * const node = <Layout />;
673
- * node.add(<Rect />);
674
- * node.add(<Circle />);
675
- * ```
676
- * Result:
677
- * ```mermaid
678
- * graph TD;
679
- * layout([Layout])
680
- * circle([Circle])
681
- * rect([Rect])
682
- * layout-->rect;
683
- * layout-->circle;
684
- * ```
685
- *
686
- * @param node - A node or an array of nodes to append.
687
- */
688
- public add(node: ComponentChildren): this {
689
- return this.insert(node, Infinity);
690
- }
691
-
692
- /**
693
- * Insert the given node(s) at the specified index in the children list.
694
- *
695
- * @example
696
- * ```tsx
697
- * const node = (
698
- * <Layout>
699
- * <Rect />
700
- * <Circle />
701
- * </Layout>
702
- * );
703
- *
704
- * node.insert(<Txt />, 1);
705
- * ```
706
- *
707
- * Result:
708
- * ```mermaid
709
- * graph TD;
710
- * layout([Layout])
711
- * circle([Circle])
712
- * text([Text])
713
- * rect([Rect])
714
- * layout-->rect;
715
- * layout-->text;
716
- * layout-->circle;
717
- * ```
718
- *
719
- * @param node - A node or an array of nodes to insert.
720
- * @param index - An index at which to insert the node(s).
721
- */
722
- public insert(node: ComponentChildren, index = 0): this {
723
- const array: ComponentChild[] = Array.isArray(node) ? node : [node];
724
- if (array.length === 0) {
725
- return this;
726
- }
727
-
728
- const children = this.children();
729
- const newChildren = children.slice(0, index);
730
-
731
- for (const node of array) {
732
- if (node instanceof Node) {
733
- newChildren.push(node);
734
- node.remove();
735
- node.parent(this);
736
- }
737
- }
738
-
739
- newChildren.push(...children.slice(index));
740
- this.setParsedChildren(newChildren);
741
-
742
- return this;
743
- }
744
-
745
- /**
746
- * Remove this node from the tree.
747
- */
748
- public remove(): this {
749
- const current = this.parent();
750
- if (current === null) {
751
- return this;
752
- }
753
-
754
- current.removeChild(this);
755
- this.parent(null);
756
- return this;
757
- }
758
-
759
- /**
760
- * Rearrange this node in relation to its siblings.
761
- *
762
- * @remarks
763
- * Children are rendered starting from the beginning of the children list.
764
- * We can change the rendering order by rearranging said list.
765
- *
766
- * A positive `by` arguments move the node up (it will be rendered on top of
767
- * the elements it has passed). Negative values move it down.
768
- *
769
- * @param by - Number of places by which the node should be moved.
770
- */
771
- public move(by = 1): this {
772
- const parent = this.parent();
773
- if (by === 0 || !parent) {
774
- return this;
775
- }
776
-
777
- const children = parent.children();
778
- const newChildren: Node[] = [];
779
-
780
- if (by > 0) {
781
- for (let i = 0; i < children.length; i++) {
782
- const child = children[i];
783
- if (child === this) {
784
- const target = i + by;
785
- for (; i < target && i + 1 < children.length; i++) {
786
- newChildren[i] = children[i + 1];
787
- }
788
- }
789
- newChildren[i] = child;
790
- }
791
- } else {
792
- for (let i = children.length - 1; i >= 0; i--) {
793
- const child = children[i];
794
- if (child === this) {
795
- const target = i + by;
796
- for (; i > target && i > 0; i--) {
797
- newChildren[i] = children[i - 1];
798
- }
799
- }
800
- newChildren[i] = child;
801
- }
802
- }
803
-
804
- parent.setParsedChildren(newChildren);
805
-
806
- return this;
807
- }
808
-
809
- /**
810
- * Move the node up in relation to its siblings.
811
- *
812
- * @remarks
813
- * The node will exchange places with the sibling right above it (if any) and
814
- * from then on will be rendered on top of it.
815
- */
816
- public moveUp(): this {
817
- return this.move(1);
818
- }
819
-
820
- /**
821
- * Move the node down in relation to its siblings.
822
- *
823
- * @remarks
824
- * The node will exchange places with the sibling right below it (if any) and
825
- * from then on will be rendered under it.
826
- */
827
- public moveDown(): this {
828
- return this.move(-1);
829
- }
830
-
831
- /**
832
- * Move the node to the top in relation to its siblings.
833
- *
834
- * @remarks
835
- * The node will be placed at the end of the children list and from then on
836
- * will be rendered on top of all of its siblings.
837
- */
838
- public moveToTop(): this {
839
- return this.move(Infinity);
840
- }
841
-
842
- /**
843
- * Move the node to the bottom in relation to its siblings.
844
- *
845
- * @remarks
846
- * The node will be placed at the beginning of the children list and from then
847
- * on will be rendered below all of its siblings.
848
- */
849
- public moveToBottom(): this {
850
- return this.move(-Infinity);
851
- }
852
-
853
- /**
854
- * Move the node to the provided position relative to its siblings.
855
- *
856
- * @remarks
857
- * If the node is getting moved to a lower position, it will be placed below
858
- * the sibling that's currently at the provided index (if any).
859
- * If the node is getting moved to a higher position, it will be placed above
860
- * the sibling that's currently at the provided index (if any).
861
- *
862
- * @param index - The index to move the node to.
863
- */
864
- public moveTo(index: number): this {
865
- const parent = this.parent();
866
- if (!parent) {
867
- return this;
868
- }
869
-
870
- const currentIndex = parent.children().indexOf(this);
871
- const by = index - currentIndex;
872
-
873
- return this.move(by);
874
- }
875
-
876
- /**
877
- * Move the node below the provided node in the parent's layout.
878
- *
879
- * @remarks
880
- * The node will be moved below the provided node and from then on will be
881
- * rendered below it. By default, if the node is already positioned lower than
882
- * the sibling node, it will not get moved.
883
- *
884
- * @param node - The sibling node below which to move.
885
- * @param directlyBelow - Whether the node should be positioned directly below
886
- * the sibling. When true, will move the node even if
887
- * it is already positioned below the sibling.
888
- */
889
- public moveBelow(node: Node, directlyBelow = false): this {
890
- const parent = this.parent();
891
- if (!parent) {
892
- return this;
893
- }
894
-
895
- if (node.parent() !== parent) {
896
- useLogger().error(
897
- "Cannot position nodes relative to each other if they don't belong to the same parent.",
898
- );
899
- return this;
900
- }
901
-
902
- const children = parent.children();
903
- const ownIndex = children.indexOf(this);
904
- const otherIndex = children.indexOf(node);
905
-
906
- if (!directlyBelow && ownIndex < otherIndex) {
907
- // Nothing to do if the node is already positioned below the target node.
908
- // We could move the node so it's directly below the sibling node, but
909
- // that might suddenly move it on top of other nodes. This is likely
910
- // not what the user wanted to happen when calling this method.
911
- return this;
912
- }
913
-
914
- const by = otherIndex - ownIndex - 1;
915
-
916
- return this.move(by);
917
- }
918
-
919
- /**
920
- * Move the node above the provided node in the parent's layout.
921
- *
922
- * @remarks
923
- * The node will be moved above the provided node and from then on will be
924
- * rendered on top of it. By default, if the node is already positioned
925
- * higher than the sibling node, it will not get moved.
926
- *
927
- * @param node - The sibling node below which to move.
928
- * @param directlyAbove - Whether the node should be positioned directly above the
929
- * sibling. When true, will move the node even if it is
930
- * already positioned above the sibling.
931
- */
932
- public moveAbove(node: Node, directlyAbove = false): this {
933
- const parent = this.parent();
934
- if (!parent) {
935
- return this;
936
- }
937
-
938
- if (node.parent() !== parent) {
939
- useLogger().error(
940
- "Cannot position nodes relative to each other if they don't belong to the same parent.",
941
- );
942
- return this;
943
- }
944
-
945
- const children = parent.children();
946
- const ownIndex = children.indexOf(this);
947
- const otherIndex = children.indexOf(node);
948
-
949
- if (!directlyAbove && ownIndex > otherIndex) {
950
- // Nothing to do if the node is already positioned above the target node.
951
- // We could move the node so it's directly above the sibling node, but
952
- // that might suddenly move it below other nodes. This is likely not what
953
- // the user wanted to happen when calling this method.
954
- return this;
955
- }
956
-
957
- const by = otherIndex - ownIndex + 1;
958
-
959
- return this.move(by);
960
- }
961
-
962
- /**
963
- * Change the parent of this node while keeping the absolute transform.
964
- *
965
- * @remarks
966
- * After performing this operation, the node will stay in the same place
967
- * visually, but its parent will be changed.
968
- *
969
- * @param newParent - The new parent of this node.
970
- */
971
- public reparent(newParent: Node) {
972
- const position = this.absolutePosition();
973
- const rotation = this.absoluteRotation();
974
- const scale = this.absoluteScale();
975
- newParent.add(this);
976
- this.absolutePosition(position);
977
- this.absoluteRotation(rotation);
978
- this.absoluteScale(scale);
979
- }
980
-
981
- /**
982
- * Remove all children of this node.
983
- */
984
- public removeChildren() {
985
- for (const oldChild of this.realChildren) {
986
- oldChild.parent(null);
987
- }
988
- this.setParsedChildren([]);
989
- }
990
-
991
- /**
992
- * Get the current children of this node.
993
- *
994
- * @remarks
995
- * Unlike {@link children}, this method does not have any side effects.
996
- * It does not register the `children` signal as a dependency, and it does not
997
- * spawn any children. It can be used to safely retrieve the current state of
998
- * the scene graph for debugging purposes.
999
- */
1000
- public peekChildren(): readonly Node[] {
1001
- return this.realChildren;
1002
- }
1003
-
1004
- /**
1005
- * Find all descendants of this node that match the given predicate.
1006
- *
1007
- * @param predicate - A function that returns true if the node matches.
1008
- */
1009
- public findAll<T extends Node>(predicate: (node: any) => node is T): T[];
1010
- /**
1011
- * Find all descendants of this node that match the given predicate.
1012
- *
1013
- * @param predicate - A function that returns true if the node matches.
1014
- */
1015
- public findAll<T extends Node = Node>(predicate: (node: any) => boolean): T[];
1016
- public findAll<T extends Node>(predicate: (node: any) => node is T): T[] {
1017
- const result: T[] = [];
1018
- const queue = this.reversedChildren();
1019
- while (queue.length > 0) {
1020
- const node = queue.pop()!;
1021
- if (predicate(node)) {
1022
- result.push(node);
1023
- }
1024
- const children = node.children();
1025
- for (let i = children.length - 1; i >= 0; i--) {
1026
- queue.push(children[i]);
1027
- }
1028
- }
1029
-
1030
- return result;
1031
- }
1032
-
1033
- /**
1034
- * Find the first descendant of this node that matches the given predicate.
1035
- *
1036
- * @param predicate - A function that returns true if the node matches.
1037
- */
1038
- public findFirst<T extends Node>(
1039
- predicate: (node: Node) => node is T,
1040
- ): T | null;
1041
- /**
1042
- * Find the first descendant of this node that matches the given predicate.
1043
- *
1044
- * @param predicate - A function that returns true if the node matches.
1045
- */
1046
- public findFirst<T extends Node = Node>(
1047
- predicate: (node: Node) => boolean,
1048
- ): T | null;
1049
- public findFirst<T extends Node>(
1050
- predicate: (node: Node) => node is T,
1051
- ): T | null {
1052
- const queue = this.reversedChildren();
1053
- while (queue.length > 0) {
1054
- const node = queue.pop()!;
1055
- if (predicate(node)) {
1056
- return node;
1057
- }
1058
- const children = node.children();
1059
- for (let i = children.length - 1; i >= 0; i--) {
1060
- queue.push(children[i]);
1061
- }
1062
- }
1063
-
1064
- return null;
1065
- }
1066
-
1067
- /**
1068
- * Find the last descendant of this node that matches the given predicate.
1069
- *
1070
- * @param predicate - A function that returns true if the node matches.
1071
- */
1072
- public findLast<T extends Node>(
1073
- predicate: (node: Node) => node is T,
1074
- ): T | null;
1075
- /**
1076
- * Find the last descendant of this node that matches the given predicate.
1077
- *
1078
- * @param predicate - A function that returns true if the node matches.
1079
- */
1080
- public findLast<T extends Node = Node>(
1081
- predicate: (node: Node) => boolean,
1082
- ): T | null;
1083
- public findLast<T extends Node>(
1084
- predicate: (node: Node) => node is T,
1085
- ): T | null {
1086
- const search: Node[] = [];
1087
- const queue = this.reversedChildren();
1088
-
1089
- while (queue.length > 0) {
1090
- const node = queue.pop()!;
1091
- search.push(node);
1092
- const children = node.children();
1093
- for (let i = children.length - 1; i >= 0; i--) {
1094
- queue.push(children[i]);
1095
- }
1096
- }
1097
-
1098
- while (search.length > 0) {
1099
- const node = search.pop()!;
1100
- if (predicate(node)) {
1101
- return node;
1102
- }
1103
- }
1104
-
1105
- return null;
1106
- }
1107
-
1108
- /**
1109
- * Find the closest ancestor of this node that matches the given predicate.
1110
- *
1111
- * @param predicate - A function that returns true if the node matches.
1112
- */
1113
- public findAncestor<T extends Node>(
1114
- predicate: (node: Node) => node is T,
1115
- ): T | null;
1116
- /**
1117
- * Find the closest ancestor of this node that matches the given predicate.
1118
- *
1119
- * @param predicate - A function that returns true if the node matches.
1120
- */
1121
- public findAncestor<T extends Node = Node>(
1122
- predicate: (node: Node) => boolean,
1123
- ): T | null;
1124
- public findAncestor<T extends Node>(
1125
- predicate: (node: Node) => node is T,
1126
- ): T | null {
1127
- let parent: Node | null = this.parent();
1128
- while (parent) {
1129
- if (predicate(parent)) {
1130
- return parent;
1131
- }
1132
- parent = parent.parent();
1133
- }
1134
-
1135
- return null;
1136
- }
1137
-
1138
- /**
1139
- * Get the nth children cast to the specified type.
1140
- *
1141
- * @param index - The index of the child to retrieve.
1142
- */
1143
- public childAs<T extends Node = Node>(index: number): T | null {
1144
- return (this.children()[index] as T) ?? null;
1145
- }
1146
-
1147
- /**
1148
- * Get the children array cast to the specified type.
1149
- */
1150
- public childrenAs<T extends Node = Node>(): T[] {
1151
- return this.children() as T[];
1152
- }
1153
-
1154
- /**
1155
- * Get the parent cast to the specified type.
1156
- */
1157
- public parentAs<T extends Node = Node>(): T | null {
1158
- return (this.parent() as T) ?? null;
1159
- }
1160
-
1161
- /**
1162
- * Prepare this node to be disposed of.
1163
- *
1164
- * @remarks
1165
- * This method is called automatically when a scene is refreshed. It will
1166
- * be called even if the node is not currently attached to the tree.
1167
- *
1168
- * The goal of this method is to clean any external references to allow the
1169
- * node to be garbage collected.
1170
- */
1171
- public dispose() {
1172
- if (!this.unregister) {
1173
- return;
1174
- }
1175
-
1176
- this.stateStack = [];
1177
- this.unregister();
1178
- this.unregister = null!;
1179
- for (const {signal} of this) {
1180
- signal?.context.dispose();
1181
- }
1182
- for (const child of this.realChildren) {
1183
- child.dispose();
1184
- }
1185
- }
1186
-
1187
- /**
1188
- * Create a copy of this node.
1189
- *
1190
- * @param customProps - Properties to override.
1191
- */
1192
- public clone(customProps: NodeState = {}): this {
1193
- const props = {...customProps};
1194
- if (isReactive(this.children.context.raw())) {
1195
- props.children ??= this.children.context.raw();
1196
- } else if (this.children().length > 0) {
1197
- props.children ??= this.children().map(child => child.clone());
1198
- }
1199
-
1200
- for (const {key, meta, signal} of this) {
1201
- if (!meta.cloneable || key in props) continue;
1202
- if (meta.compound) {
1203
- for (const [key, property] of meta.compoundEntries) {
1204
- if (property in props) continue;
1205
- const component = (<Record<string, SimpleSignal<any>>>(
1206
- (<unknown>signal)
1207
- ))[key];
1208
- if (!component.context.isInitial()) {
1209
- props[property] = component.context.raw();
1210
- }
1211
- }
1212
- } else if (!signal.context.isInitial()) {
1213
- props[key] = signal.context.raw();
1214
- }
1215
- }
1216
-
1217
- return this.instantiate(props);
1218
- }
1219
-
1220
- /**
1221
- * Create a copy of this node.
1222
- *
1223
- * @remarks
1224
- * Unlike {@link clone}, a snapshot clone calculates any reactive properties
1225
- * at the moment of cloning and passes the raw values to the copy.
1226
- *
1227
- * @param customProps - Properties to override.
1228
- */
1229
- public snapshotClone(customProps: NodeState = {}): this {
1230
- const props = {
1231
- ...this.getState(),
1232
- ...customProps,
1233
- };
1234
-
1235
- if (this.children().length > 0) {
1236
- props.children ??= this.children().map(child => child.snapshotClone());
1237
- }
1238
-
1239
- return this.instantiate(props);
1240
- }
1241
-
1242
- /**
1243
- * Create a reactive copy of this node.
1244
- *
1245
- * @remarks
1246
- * A reactive copy has all its properties dynamically updated to match the
1247
- * source node.
1248
- *
1249
- * @param customProps - Properties to override.
1250
- */
1251
- public reactiveClone(customProps: NodeState = {}): this {
1252
- const props = {...customProps};
1253
- if (this.children().length > 0) {
1254
- props.children ??= this.children().map(child => child.reactiveClone());
1255
- }
1256
-
1257
- for (const {key, meta, signal} of this) {
1258
- if (!meta.cloneable || key in props) continue;
1259
- props[key] = () => signal();
1260
- }
1261
-
1262
- return this.instantiate(props);
1263
- }
1264
-
1265
- /**
1266
- * Create an instance of this node's class.
1267
- *
1268
- * @param props - Properties to pass to the constructor.
1269
- */
1270
- public instantiate(props: NodeProps = {}): this {
1271
- return new (<NodeConstructor<NodeProps, this>>this.constructor)(props);
1272
- }
1273
-
1274
- /**
1275
- * Set the children without parsing them.
1276
- *
1277
- * @remarks
1278
- * This method assumes that the caller took care of parsing the children and
1279
- * updating the hierarchy.
1280
- *
1281
- * @param value - The children to set.
1282
- */
1283
- protected setParsedChildren(value: Node[]) {
1284
- this.children.context.setter(value);
1285
- this.realChildren = value;
1286
- }
1287
-
1288
- protected spawnChildren(reactive: boolean, children: ComponentChildren) {
1289
- const parsedChildren = this.parseChildren(children);
1290
-
1291
- const keep = new Set<string>();
1292
- for (const newChild of parsedChildren) {
1293
- const current = newChild.parent.context.raw() as Node | null;
1294
- if (current && current !== this) {
1295
- current.removeChild(newChild);
1296
- }
1297
- keep.add(newChild.key);
1298
- newChild.parent(this);
1299
- }
1300
-
1301
- for (const oldChild of this.realChildren) {
1302
- if (!keep.has(oldChild.key)) {
1303
- oldChild.parent(null);
1304
- }
1305
- }
1306
-
1307
- this.hasSpawnedChildren = reactive;
1308
- this.realChildren = parsedChildren;
1309
- }
1310
-
1311
- /**
1312
- * Parse any `ComponentChildren` into an array of nodes.
1313
- *
1314
- * @param children - The children to parse.
1315
- */
1316
- protected parseChildren(children: ComponentChildren): Node[] {
1317
- const result: Node[] = [];
1318
- const array = Array.isArray(children) ? children : [children];
1319
- for (const child of array) {
1320
- if (child instanceof Node) {
1321
- result.push(child);
1322
- }
1323
- }
1324
-
1325
- return result;
1326
- }
1327
-
1328
- /**
1329
- * Remove the given child.
1330
- */
1331
- protected removeChild(child: Node) {
1332
- this.setParsedChildren(this.children().filter(node => node !== child));
1333
- }
1334
-
1335
- /**
1336
- * Whether this node should be cached or not.
1337
- */
1338
- protected requiresCache(): boolean {
1339
- return (
1340
- this.cache() ||
1341
- this.opacity() < 1 ||
1342
- this.compositeOperation() !== 'source-over' ||
1343
- this.hasFilters() ||
1344
- this.hasShadow() ||
1345
- this.shaders().length > 0
1346
- );
1347
- }
1348
-
1349
- @computed()
1350
- protected cacheCanvas(): CanvasRenderingContext2D {
1351
- const canvas = document.createElement('canvas').getContext('2d');
1352
- if (!canvas) {
1353
- throw new Error('Could not create a cache canvas');
1354
- }
1355
-
1356
- return canvas;
1357
- }
1358
-
1359
- /**
1360
- * Get a cache canvas with the contents of this node rendered onto it.
1361
- */
1362
- @computed()
1363
- protected async cachedCanvas() {
1364
- const context = this.cacheCanvas();
1365
- const cache = this.worldSpaceCacheBBox();
1366
- const matrix = this.localToWorld();
1367
-
1368
- context.canvas.width = cache.width;
1369
- context.canvas.height = cache.height;
1370
-
1371
- context.setTransform(
1372
- matrix.a,
1373
- matrix.b,
1374
- matrix.c,
1375
- matrix.d,
1376
- matrix.e - cache.x,
1377
- matrix.f - cache.y,
1378
- );
1379
- await this.draw(context);
1380
-
1381
- return context;
1382
- }
1383
-
1384
- /**
1385
- * Get a bounding box for the contents rendered by this node.
1386
- *
1387
- * @remarks
1388
- * The returned bounding box should be in local space.
1389
- */
1390
- protected getCacheBBox(): BBox {
1391
- return new BBox();
1392
- }
1393
-
1394
- /**
1395
- * Get a bounding box for the contents rendered by this node as well
1396
- * as its children.
1397
- */
1398
- @computed()
1399
- public cacheBBox(): BBox {
1400
- const cache = this.getCacheBBox();
1401
- const children = this.children();
1402
- const padding = this.cachePadding();
1403
- if (children.length === 0) {
1404
- return cache.addSpacing(padding);
1405
- }
1406
-
1407
- const points: Vector2[] = cache.corners;
1408
- for (const child of children) {
1409
- const childCache = child.fullCacheBBox();
1410
- const childMatrix = child.localToParent();
1411
- points.push(
1412
- ...childCache.corners.map(r => transformVectorAsPoint(r, childMatrix)),
1413
- );
1414
- }
1415
-
1416
- const bbox = BBox.fromPoints(...points);
1417
- return bbox.addSpacing(padding);
1418
- }
1419
-
1420
- /**
1421
- * Get a bounding box for the contents rendered by this node (including
1422
- * effects applied after caching).
1423
- *
1424
- * @remarks
1425
- * The returned bounding box should be in local space.
1426
- */
1427
- @computed()
1428
- protected fullCacheBBox(): BBox {
1429
- const matrix = this.compositeToLocal();
1430
- const shadowOffset = transformVector(this.shadowOffset(), matrix);
1431
- const shadowBlur = transformScalar(this.shadowBlur(), matrix);
1432
-
1433
- const result = this.cacheBBox().expand(
1434
- this.filters.blur() * 2 + shadowBlur,
1435
- );
1436
-
1437
- if (shadowOffset.x < 0) {
1438
- result.x += shadowOffset.x;
1439
- result.width -= shadowOffset.x;
1440
- } else {
1441
- result.width += shadowOffset.x;
1442
- }
1443
-
1444
- if (shadowOffset.y < 0) {
1445
- result.y += shadowOffset.y;
1446
- result.height -= shadowOffset.y;
1447
- } else {
1448
- result.height += shadowOffset.y;
1449
- }
1450
-
1451
- return result;
1452
- }
1453
-
1454
- /**
1455
- * Get a bounding box in world space for the contents rendered by this node as
1456
- * well as its children.
1457
- *
1458
- * @remarks
1459
- * This is the same the bounding box returned by {@link cacheBBox} only
1460
- * transformed to world space.
1461
- */
1462
- @computed()
1463
- protected worldSpaceCacheBBox(): BBox {
1464
- const viewBBox = BBox.fromSizeCentered(this.view().size());
1465
- const canvasBBox = BBox.fromPoints(
1466
- ...viewBBox.transformCorners(this.view().localToWorld()),
1467
- );
1468
- const cacheBBox = BBox.fromPoints(
1469
- ...this.cacheBBox().transformCorners(this.localToWorld()),
1470
- ).pixelPerfect.expand(2);
1471
-
1472
- return canvasBBox.intersection(cacheBBox);
1473
- }
1474
-
1475
- @computed()
1476
- protected parentWorldSpaceCacheBBox(): BBox {
1477
- return (
1478
- this.findAncestor(node => node.requiresCache())?.worldSpaceCacheBBox() ??
1479
- new BBox(Vector2.zero, useScene2D().getSize())
1480
- );
1481
- }
1482
-
1483
- /**
1484
- * Prepare the given context for drawing a cached node onto it.
1485
- *
1486
- * @remarks
1487
- * This method is called before the contents of the cache canvas are drawn
1488
- * on the screen. It can be used to apply effects to the entire node together
1489
- * with its children, instead of applying them individually.
1490
- * Effects such as transparency, shadows, and filters use this technique.
1491
- *
1492
- * Whether the node is cached is decided by the {@link requiresCache} method.
1493
- *
1494
- * @param context - The context using which the cache will be drawn.
1495
- */
1496
- protected setupDrawFromCache(context: CanvasRenderingContext2D) {
1497
- context.globalCompositeOperation = this.compositeOperation();
1498
- context.globalAlpha *= this.opacity();
1499
- if (this.hasFilters()) {
1500
- context.filter = this.filterString();
1501
- }
1502
- if (this.hasShadow()) {
1503
- const matrix = this.compositeToWorld();
1504
- const offset = transformVector(this.shadowOffset(), matrix);
1505
- const blur = transformScalar(this.shadowBlur(), matrix);
1506
-
1507
- context.shadowColor = this.shadowColor().serialize();
1508
- context.shadowBlur = blur;
1509
- context.shadowOffsetX = offset.x;
1510
- context.shadowOffsetY = offset.y;
1511
- }
1512
-
1513
- const matrix = this.worldToLocal();
1514
- context.transform(
1515
- matrix.a,
1516
- matrix.b,
1517
- matrix.c,
1518
- matrix.d,
1519
- matrix.e,
1520
- matrix.f,
1521
- );
1522
- }
1523
-
1524
- protected renderFromSource(
1525
- context: CanvasRenderingContext2D,
1526
- source: CanvasImageSource,
1527
- x: number,
1528
- y: number,
1529
- ) {
1530
- this.setupDrawFromCache(context);
1531
-
1532
- const compositeOverride = this.compositeOverride();
1533
- context.drawImage(source, x, y);
1534
- if (compositeOverride > 0) {
1535
- context.save();
1536
- context.globalAlpha *= compositeOverride;
1537
- context.globalCompositeOperation = 'source-over';
1538
- context.drawImage(source, x, y);
1539
- context.restore();
1540
- }
1541
- }
1542
-
1543
- private shaderCanvas(destination: TexImageSource, source: TexImageSource) {
1544
- const shaders = this.shaders();
1545
- if (shaders.length === 0) {
1546
- return null;
1547
- }
1548
-
1549
- const scene = useScene2D();
1550
- const size = scene.getRealSize();
1551
- const parentCacheRect = this.parentWorldSpaceCacheBBox();
1552
- const cameraToWorld = new DOMMatrix()
1553
- .scaleSelf(
1554
- size.width / parentCacheRect.width,
1555
- size.height / -parentCacheRect.height,
1556
- )
1557
- .translateSelf(
1558
- parentCacheRect.x / -size.width,
1559
- parentCacheRect.y / size.height - 1,
1560
- );
1561
-
1562
- const cacheRect = this.worldSpaceCacheBBox();
1563
- const cameraToCache = new DOMMatrix()
1564
- .scaleSelf(size.width / cacheRect.width, size.height / -cacheRect.height)
1565
- .translateSelf(cacheRect.x / -size.width, cacheRect.y / size.height - 1)
1566
- .invertSelf();
1567
-
1568
- const gl = scene.shaders.getGL();
1569
- scene.shaders.copyTextures(destination, source);
1570
- scene.shaders.clear();
1571
-
1572
- for (const shader of shaders) {
1573
- const program = scene.shaders.getProgram(shader.fragment);
1574
- if (!program) {
1575
- continue;
1576
- }
1577
-
1578
- if (shader.uniforms) {
1579
- for (const [name, uniform] of Object.entries(shader.uniforms)) {
1580
- const location = gl.getUniformLocation(program, name);
1581
- if (location === null) {
1582
- continue;
1583
- }
1584
-
1585
- const value = unwrap(uniform);
1586
- if (typeof value === 'number') {
1587
- gl.uniform1f(location, value);
1588
- } else if ('toUniform' in value) {
1589
- value.toUniform(gl, location);
1590
- } else if (value.length === 1) {
1591
- gl.uniform1f(location, value[0]);
1592
- } else if (value.length === 2) {
1593
- gl.uniform2f(location, value[0], value[1]);
1594
- } else if (value.length === 3) {
1595
- gl.uniform3f(location, value[0], value[1], value[2]);
1596
- } else if (value.length === 4) {
1597
- gl.uniform4f(location, value[0], value[1], value[2], value[3]);
1598
- }
1599
- }
1600
- }
1601
-
1602
- gl.uniform1f(
1603
- gl.getUniformLocation(program, UNIFORM_TIME),
1604
- this.view2D.globalTime(),
1605
- );
1606
-
1607
- gl.uniform1i(
1608
- gl.getUniformLocation(program, UNIFORM_TIME),
1609
- scene.playback.frame,
1610
- );
1611
-
1612
- gl.uniformMatrix4fv(
1613
- gl.getUniformLocation(program, UNIFORM_SOURCE_MATRIX),
1614
- false,
1615
- cameraToCache.toFloat32Array(),
1616
- );
1617
-
1618
- gl.uniformMatrix4fv(
1619
- gl.getUniformLocation(program, UNIFORM_DESTINATION_MATRIX),
1620
- false,
1621
- cameraToWorld.toFloat32Array(),
1622
- );
1623
-
1624
- shader.setup?.(gl, program);
1625
- scene.shaders.render();
1626
- shader.teardown?.(gl, program);
1627
- }
1628
-
1629
- return gl.canvas;
1630
- }
1631
-
1632
- /**
1633
- * Render this node onto the given canvas.
1634
- *
1635
- * @param context - The context to draw with.
1636
- */
1637
- public async render(context: CanvasRenderingContext2D) {
1638
- if (this.absoluteOpacity() <= 0) {
1639
- return;
1640
- }
1641
-
1642
- context.save();
1643
- this.transformContext(context);
1644
-
1645
- if (this.requiresCache()) {
1646
- const cacheRect = this.worldSpaceCacheBBox();
1647
- if (cacheRect.width !== 0 && cacheRect.height !== 0) {
1648
- const cache = (await this.cachedCanvas()).canvas;
1649
- const source = this.shaderCanvas(context.canvas, cache);
1650
- if (source) {
1651
- this.renderFromSource(context, source, 0, 0);
1652
- } else {
1653
- this.renderFromSource(
1654
- context,
1655
- cache,
1656
- cacheRect.position.x,
1657
- cacheRect.position.y,
1658
- );
1659
- }
1660
- }
1661
- } else {
1662
- await this.draw(context);
1663
- }
1664
-
1665
- context.restore();
1666
- }
1667
-
1668
- /**
1669
- * Draw this node onto the canvas.
1670
- *
1671
- * @remarks
1672
- * This method is used when drawing directly onto the screen as well as onto
1673
- * the cache canvas.
1674
- * It assumes that the context have already been transformed to local space.
1675
- *
1676
- * @param context - The context to draw with.
1677
- */
1678
- protected async draw(context: CanvasRenderingContext2D) {
1679
- await this.drawChildren(context);
1680
- }
1681
-
1682
- protected async drawChildren(context: CanvasRenderingContext2D) {
1683
- for (const child of this.sortedChildren()) {
1684
- await child.render(context);
1685
- }
1686
- }
1687
-
1688
- /**
1689
- * Draw an overlay for this node.
1690
- *
1691
- * @remarks
1692
- * The overlay for the currently inspected node is displayed on top of the
1693
- * canvas.
1694
- *
1695
- * The provided context is in screen space. The local-to-screen matrix can be
1696
- * used to transform all shapes that need to be displayed.
1697
- * This approach allows to keep the line widths and gizmo sizes consistent,
1698
- * no matter how zoomed-in the view is.
1699
- *
1700
- * @param context - The context to draw with.
1701
- * @param matrix - A local-to-screen matrix.
1702
- */
1703
- public drawOverlay(context: CanvasRenderingContext2D, matrix: DOMMatrix) {
1704
- const box = this.cacheBBox().transformCorners(matrix);
1705
- const cache = this.getCacheBBox().transformCorners(matrix);
1706
- context.strokeStyle = 'white';
1707
- context.lineWidth = 1;
1708
- context.beginPath();
1709
- drawLine(context, box);
1710
- context.closePath();
1711
- context.stroke();
1712
-
1713
- context.strokeStyle = 'blue';
1714
- context.beginPath();
1715
- drawLine(context, cache);
1716
- context.closePath();
1717
- context.stroke();
1718
- }
1719
-
1720
- protected transformContext(context: CanvasRenderingContext2D) {
1721
- const matrix = this.localToParent();
1722
- context.transform(
1723
- matrix.a,
1724
- matrix.b,
1725
- matrix.c,
1726
- matrix.d,
1727
- matrix.e,
1728
- matrix.f,
1729
- );
1730
- }
1731
-
1732
- /**
1733
- * Try to find a node intersecting the given position.
1734
- *
1735
- * @param position - The searched position.
1736
- */
1737
- public hit(position: Vector2): Node | null {
1738
- let hit: Node | null = null;
1739
- const local = transformVectorAsPoint(
1740
- position,
1741
- this.localToParent().inverse(),
1742
- );
1743
- const children = this.children();
1744
- for (let i = children.length - 1; i >= 0; i--) {
1745
- hit = children[i].hit(local);
1746
- if (hit) {
1747
- break;
1748
- }
1749
- }
1750
-
1751
- return hit;
1752
- }
1753
-
1754
- /**
1755
- * Collect all asynchronous resources used by this node.
1756
- */
1757
- protected collectAsyncResources() {
1758
- for (const child of this.children()) {
1759
- child.collectAsyncResources();
1760
- }
1761
- }
1762
-
1763
- /**
1764
- * Wait for any asynchronous resources that this node or its children have.
1765
- *
1766
- * @remarks
1767
- * Certain resources like images are always loaded asynchronously.
1768
- * Awaiting this method makes sure that all such resources are done loading
1769
- * before continuing the animation.
1770
- */
1771
- public async toPromise(): Promise<this> {
1772
- do {
1773
- await DependencyContext.consumePromises();
1774
- this.collectAsyncResources();
1775
- } while (DependencyContext.hasPromises());
1776
- return this;
1777
- }
1778
-
1779
- /**
1780
- * Return a snapshot of the node's current signal values.
1781
- *
1782
- * @remarks
1783
- * This method will calculate the values of any reactive properties of the
1784
- * node at the time the method is called.
1785
- */
1786
- public getState(): NodeState {
1787
- const state: NodeState = {};
1788
- for (const {key, meta, signal} of this) {
1789
- if (!meta.cloneable || key in state) continue;
1790
- state[key] = signal();
1791
- }
1792
- return state;
1793
- }
1794
-
1795
- /**
1796
- * Apply the given state to the node, setting all matching signal values to
1797
- * the provided values.
1798
- *
1799
- * @param state - The state to apply to the node.
1800
- */
1801
- public applyState(state: NodeState): void;
1802
- /**
1803
- * Smoothly transition between the current state of the node and the given
1804
- * state.
1805
- *
1806
- * @param state - The state to transition to.
1807
- * @param duration - The duration of the transition.
1808
- * @param timing - The timing function to use for the transition.
1809
- */
1810
- public applyState(
1811
- state: NodeState,
1812
- duration: number,
1813
- timing?: TimingFunction,
1814
- ): ThreadGenerator;
1815
- public applyState(
1816
- state: NodeState,
1817
- duration?: number,
1818
- timing: TimingFunction = easeInOutCubic,
1819
- ): ThreadGenerator | void {
1820
- if (duration === undefined) {
1821
- for (const key in state) {
1822
- const signal = this.signalByKey(key);
1823
- if (signal) {
1824
- signal(state[key]);
1825
- }
1826
- }
1827
- }
1828
-
1829
- const tasks: ThreadGenerator[] = [];
1830
- for (const key in state) {
1831
- const signal = this.signalByKey(key);
1832
- if (state[key] !== signal.context.raw()) {
1833
- tasks.push(signal(state[key], duration!, timing));
1834
- }
1835
- }
1836
-
1837
- return all(...tasks);
1838
- }
1839
-
1840
- /**
1841
- * Push a snapshot of the node's current state onto the node's state stack.
1842
- *
1843
- * @remarks
1844
- * This method can be used together with the {@link restore} method to save a
1845
- * node's current state and later restore it. It is possible to store more
1846
- * than one state by calling `save` method multiple times.
1847
- */
1848
- public save(): void {
1849
- this.stateStack.push(this.getState());
1850
- }
1851
-
1852
- /**
1853
- * Restore the node to its last saved state.
1854
- *
1855
- * @remarks
1856
- * This method can be used together with the {@link save} method to restore a
1857
- * node to a previously saved state. Restoring a node to a previous state
1858
- * removes that state from the state stack.
1859
- *
1860
- * @example
1861
- * ```tsx
1862
- * const node = <Circle width={100} height={100} fill={"lightseagreen"} />
1863
- *
1864
- * view.add(node);
1865
- *
1866
- * // Save the node's current state
1867
- * node.save();
1868
- *
1869
- * // Modify some of the node's properties
1870
- * yield* node.scale(2, 1);
1871
- * yield* node.fill('hotpink', 1);
1872
- *
1873
- * // Restore the node to its saved state
1874
- * node.restore();
1875
- * ```
1876
- */
1877
- public restore(): void;
1878
- /**
1879
- * Tween the node to its last saved state.
1880
- *
1881
- * @remarks
1882
- * This method can be used together with the {@link save} method to restore a
1883
- * node to a previously saved state. Restoring a node to a previous state
1884
- * removes that state from the state stack.
1885
- *
1886
- * @example
1887
- * ```tsx
1888
- * const node = <Circle width={100} height={100} fill={"lightseagreen"} />
1889
- *
1890
- * view.add(node);
1891
- *
1892
- * // Save the node's current state
1893
- * node.save();
1894
- *
1895
- * // Modify some of the node's properties
1896
- * yield* node.scale(2, 1);
1897
- * yield* node.fill('hotpink', 1);
1898
- *
1899
- * // Tween the node to its saved state over 1 second
1900
- * yield* node.restore(1);
1901
- * ```
1902
- *
1903
- * @param duration - The duration of the transition.
1904
- * @param timing - The timing function to use for the transition.
1905
- */
1906
- public restore(duration: number, timing?: TimingFunction): ThreadGenerator;
1907
- public restore(
1908
- duration?: number,
1909
- timing: TimingFunction = easeInOutCubic,
1910
- ): ThreadGenerator | void {
1911
- const state = this.stateStack.pop();
1912
-
1913
- if (state !== undefined) {
1914
- return this.applyState(state, duration!, timing);
1915
- }
1916
- }
1917
-
1918
- public *[Symbol.iterator]() {
1919
- for (const key in this.properties) {
1920
- const meta = this.properties[key];
1921
- const signal = this.signalByKey(key);
1922
- yield {meta, signal, key};
1923
- }
1924
- }
1925
-
1926
- private signalByKey(key: string): SimpleSignal<any> {
1927
- return (<Record<string, SimpleSignal<any>>>(<unknown>this))[key];
1928
- }
1929
-
1930
- private reversedChildren() {
1931
- const children = this.children();
1932
- const result: Node[] = [];
1933
- for (let i = children.length - 1; i >= 0; i--) {
1934
- result.push(children[i]);
1935
- }
1936
- return result;
1937
- }
1938
- }
1939
-
1940
- Node.prototype.isClass = true;
1
+ import type {
2
+ ColorSignal,
3
+ PossibleColor,
4
+ PossibleSpacing,
5
+ PossibleVector2,
6
+ Promisable,
7
+ ReferenceReceiver,
8
+ Signal,
9
+ SignalValue,
10
+ SimpleSignal,
11
+ SimpleVector2Signal,
12
+ SpacingSignal,
13
+ ThreadGenerator,
14
+ TimingFunction,
15
+ Vector2Signal,
16
+ } from '@twick/core';
17
+ import {
18
+ BBox,
19
+ DependencyContext,
20
+ UNIFORM_DESTINATION_MATRIX,
21
+ UNIFORM_SOURCE_MATRIX,
22
+ UNIFORM_TIME,
23
+ Vector2,
24
+ all,
25
+ clamp,
26
+ createSignal,
27
+ easeInOutCubic,
28
+ isReactive,
29
+ modify,
30
+ threadable,
31
+ transformAngle,
32
+ transformScalar,
33
+ transformVector,
34
+ transformVectorAsPoint,
35
+ unwrap,
36
+ useLogger,
37
+ } from '@twick/core';
38
+ import {
39
+ NODE_NAME,
40
+ cloneable,
41
+ colorSignal,
42
+ computed,
43
+ getPropertiesOf,
44
+ initial,
45
+ initializeSignals,
46
+ inspectable,
47
+ nodeName,
48
+ parser,
49
+ signal,
50
+ vector2Signal,
51
+ wrapper,
52
+ } from '../decorators';
53
+ import type {FiltersSignal} from '../decorators/filtersSignal';
54
+ import {filtersSignal} from '../decorators/filtersSignal';
55
+ import {spacingSignal} from '../decorators/spacingSignal';
56
+ import type {Filter} from '../partials';
57
+ import type {
58
+ PossibleShaderConfig,
59
+ ShaderConfig,
60
+ } from '../partials/ShaderConfig';
61
+ import {parseShader} from '../partials/ShaderConfig';
62
+ import {useScene2D} from '../scenes/useScene2D';
63
+ import {drawLine} from '../utils';
64
+ import type {View2D} from './View2D';
65
+ import type {ComponentChild, ComponentChildren, NodeConstructor} from './types';
66
+
67
+ export type NodeState = NodeProps & Record<string, any>;
68
+
69
+ export interface NodeProps {
70
+ ref?: ReferenceReceiver<any>;
71
+ children?: SignalValue<ComponentChildren>;
72
+ /**
73
+ * @deprecated Use {@link children} instead.
74
+ */
75
+ spawner?: SignalValue<ComponentChildren>;
76
+ key?: string;
77
+
78
+ x?: SignalValue<number>;
79
+ y?: SignalValue<number>;
80
+ position?: SignalValue<PossibleVector2>;
81
+ rotation?: SignalValue<number>;
82
+ scaleX?: SignalValue<number>;
83
+ scaleY?: SignalValue<number>;
84
+ scale?: SignalValue<PossibleVector2>;
85
+ skewX?: SignalValue<number>;
86
+ skewY?: SignalValue<number>;
87
+ skew?: SignalValue<PossibleVector2>;
88
+ zIndex?: SignalValue<number>;
89
+
90
+ opacity?: SignalValue<number>;
91
+ filters?: SignalValue<Filter[]>;
92
+
93
+ shadowColor?: SignalValue<PossibleColor>;
94
+ shadowBlur?: SignalValue<number>;
95
+ shadowOffsetX?: SignalValue<number>;
96
+ shadowOffsetY?: SignalValue<number>;
97
+ shadowOffset?: SignalValue<PossibleVector2>;
98
+
99
+ cache?: SignalValue<boolean>;
100
+ /**
101
+ * {@inheritDoc Node.cachePadding}
102
+ */
103
+ cachePaddingTop?: SignalValue<number>;
104
+ /**
105
+ * {@inheritDoc Node.cachePadding}
106
+ */
107
+ cachePaddingBottom?: SignalValue<number>;
108
+ /**
109
+ * {@inheritDoc Node.cachePadding}
110
+ */
111
+ cachePaddingLeft?: SignalValue<number>;
112
+ /**
113
+ * {@inheritDoc Node.cachePadding}
114
+ */
115
+ cachePaddingRight?: SignalValue<number>;
116
+ /**
117
+ * {@inheritDoc Node.cachePadding}
118
+ */
119
+ cachePadding?: SignalValue<PossibleSpacing>;
120
+
121
+ composite?: SignalValue<boolean>;
122
+ compositeOperation?: SignalValue<GlobalCompositeOperation>;
123
+ /**
124
+ * @experimental
125
+ */
126
+ shaders?: PossibleShaderConfig;
127
+ }
128
+
129
+ @nodeName('Node')
130
+ export class Node implements Promisable<Node> {
131
+ /**
132
+ * @internal
133
+ */
134
+ public declare readonly [NODE_NAME]: string;
135
+ public declare isClass: boolean;
136
+
137
+ /**
138
+ * Represents the position of this node in local space of its parent.
139
+ *
140
+ * @example
141
+ * Initializing the position:
142
+ * ```tsx
143
+ * // with a possible vector:
144
+ * <Node position={[1, 2]} />
145
+ * // with individual components:
146
+ * <Node x={1} y={2} />
147
+ * ```
148
+ *
149
+ * Accessing the position:
150
+ * ```tsx
151
+ * // retrieving the vector:
152
+ * const position = node.position();
153
+ * // retrieving an individual component:
154
+ * const x = node.position.x();
155
+ * ```
156
+ *
157
+ * Setting the position:
158
+ * ```tsx
159
+ * // with a possible vector:
160
+ * node.position([1, 2]);
161
+ * node.position(() => [1, 2]);
162
+ * // with individual components:
163
+ * node.position.x(1);
164
+ * node.position.x(() => 1);
165
+ * ```
166
+ */
167
+ @vector2Signal()
168
+ public declare readonly position: Vector2Signal<this>;
169
+
170
+ public get x() {
171
+ return this.position.x as SimpleSignal<number, this>;
172
+ }
173
+ public get y() {
174
+ return this.position.y as SimpleSignal<number, this>;
175
+ }
176
+
177
+ /**
178
+ * A helper signal for operating on the position in world space.
179
+ *
180
+ * @remarks
181
+ * Retrieving the position using this signal returns the position in world
182
+ * space. Similarly, setting the position using this signal transforms the
183
+ * new value to local space.
184
+ *
185
+ * If the new value is a function, the position of this node will be
186
+ * continuously updated to always match the position returned by the function.
187
+ * This can be useful to "pin" the node in a specific place or to make it
188
+ * follow another node's position.
189
+ *
190
+ * Unlike {@link position}, this signal is not compound - it doesn't contain
191
+ * separate signals for the `x` and `y` components.
192
+ */
193
+ @wrapper(Vector2)
194
+ @cloneable(false)
195
+ @signal()
196
+ public declare readonly absolutePosition: SimpleVector2Signal<this>;
197
+
198
+ protected getAbsolutePosition(): Vector2 {
199
+ const matrix = this.localToWorld();
200
+ return new Vector2(matrix.m41, matrix.m42);
201
+ }
202
+
203
+ protected setAbsolutePosition(value: SignalValue<PossibleVector2>) {
204
+ this.position(
205
+ modify(value, unwrapped =>
206
+ transformVectorAsPoint(new Vector2(unwrapped), this.worldToParent()),
207
+ ),
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Represents the rotation (in degrees) of this node relative to its parent.
213
+ */
214
+ @initial(0)
215
+ @signal()
216
+ public declare readonly rotation: SimpleSignal<number, this>;
217
+
218
+ /**
219
+ * A helper signal for operating on the rotation in world space.
220
+ *
221
+ * @remarks
222
+ * Retrieving the rotation using this signal returns the rotation in world
223
+ * space. Similarly, setting the rotation using this signal transforms the
224
+ * new value to local space.
225
+ *
226
+ * If the new value is a function, the rotation of this node will be
227
+ * continuously updated to always match the rotation returned by the function.
228
+ */
229
+ @cloneable(false)
230
+ @signal()
231
+ public declare readonly absoluteRotation: SimpleSignal<number, this>;
232
+
233
+ protected getAbsoluteRotation() {
234
+ const matrix = this.localToWorld();
235
+ return Vector2.degrees(matrix.m11, matrix.m12);
236
+ }
237
+
238
+ protected setAbsoluteRotation(value: SignalValue<number>) {
239
+ this.rotation(
240
+ modify(value, unwrapped =>
241
+ transformAngle(unwrapped, this.worldToParent()),
242
+ ),
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Represents the scale of this node in local space of its parent.
248
+ *
249
+ * @example
250
+ * Initializing the scale:
251
+ * ```tsx
252
+ * // with a possible vector:
253
+ * <Node scale={[1, 2]} />
254
+ * // with individual components:
255
+ * <Node scaleX={1} scaleY={2} />
256
+ * ```
257
+ *
258
+ * Accessing the scale:
259
+ * ```tsx
260
+ * // retrieving the vector:
261
+ * const scale = node.scale();
262
+ * // retrieving an individual component:
263
+ * const scaleX = node.scale.x();
264
+ * ```
265
+ *
266
+ * Setting the scale:
267
+ * ```tsx
268
+ * // with a possible vector:
269
+ * node.scale([1, 2]);
270
+ * node.scale(() => [1, 2]);
271
+ * // with individual components:
272
+ * node.scale.x(1);
273
+ * node.scale.x(() => 1);
274
+ * ```
275
+ */
276
+ @initial(Vector2.one)
277
+ @vector2Signal('scale')
278
+ public declare readonly scale: Vector2Signal<this>;
279
+
280
+ /**
281
+ * Represents the skew of this node in local space of its parent.
282
+ *
283
+ * @example
284
+ * Initializing the skew:
285
+ * ```tsx
286
+ * // with a possible vector:
287
+ * <Node skew={[40, 20]} />
288
+ * // with individual components:
289
+ * <Node skewX={40} skewY={20} />
290
+ * ```
291
+ *
292
+ * Accessing the skew:
293
+ * ```tsx
294
+ * // retrieving the vector:
295
+ * const skew = node.skew();
296
+ * // retrieving an individual component:
297
+ * const skewX = node.skew.x();
298
+ * ```
299
+ *
300
+ * Setting the skew:
301
+ * ```tsx
302
+ * // with a possible vector:
303
+ * node.skew([40, 20]);
304
+ * node.skew(() => [40, 20]);
305
+ * // with individual components:
306
+ * node.skew.x(40);
307
+ * node.skew.x(() => 40);
308
+ * ```
309
+ */
310
+ @initial(Vector2.zero)
311
+ @vector2Signal('skew')
312
+ public declare readonly skew: Vector2Signal<this>;
313
+
314
+ /**
315
+ * A helper signal for operating on the scale in world space.
316
+ *
317
+ * @remarks
318
+ * Retrieving the scale using this signal returns the scale in world space.
319
+ * Similarly, setting the scale using this signal transforms the new value to
320
+ * local space.
321
+ *
322
+ * If the new value is a function, the scale of this node will be continuously
323
+ * updated to always match the position returned by the function.
324
+ *
325
+ * Unlike {@link scale}, this signal is not compound - it doesn't contain
326
+ * separate signals for the `x` and `y` components.
327
+ */
328
+ @wrapper(Vector2)
329
+ @cloneable(false)
330
+ @signal()
331
+ public declare readonly absoluteScale: SimpleVector2Signal<this>;
332
+
333
+ protected getAbsoluteScale(): Vector2 {
334
+ const matrix = this.localToWorld();
335
+ return new Vector2(
336
+ Vector2.magnitude(matrix.m11, matrix.m12),
337
+ Vector2.magnitude(matrix.m21, matrix.m22),
338
+ );
339
+ }
340
+
341
+ protected setAbsoluteScale(value: SignalValue<PossibleVector2>) {
342
+ this.scale(
343
+ modify(value, unwrapped => this.getRelativeScale(new Vector2(unwrapped))),
344
+ );
345
+ }
346
+
347
+ private getRelativeScale(scale: Vector2): Vector2 {
348
+ const parentScale = this.parent()?.absoluteScale() ?? Vector2.one;
349
+ return scale.div(parentScale);
350
+ }
351
+
352
+ @initial(0)
353
+ @signal()
354
+ public declare readonly zIndex: SimpleSignal<number, this>;
355
+
356
+ @initial(false)
357
+ @signal()
358
+ public declare readonly cache: SimpleSignal<boolean, this>;
359
+
360
+ /**
361
+ * Controls the padding of the cached canvas used by this node.
362
+ *
363
+ * @remarks
364
+ * By default, the size of the cache is determined based on the bounding box
365
+ * of the node and its children. That includes effects such as stroke or
366
+ * shadow. This property can be used to expand the cache area further.
367
+ * Usually used to account for custom effects created by {@link shaders}.
368
+ */
369
+ @spacingSignal('cachePadding')
370
+ public declare readonly cachePadding: SpacingSignal<this>;
371
+
372
+ @initial(false)
373
+ @signal()
374
+ public declare readonly composite: SimpleSignal<boolean, this>;
375
+
376
+ @initial('source-over')
377
+ @signal()
378
+ public declare readonly compositeOperation: SimpleSignal<
379
+ GlobalCompositeOperation,
380
+ this
381
+ >;
382
+
383
+ private readonly compositeOverride = createSignal(0);
384
+
385
+ @threadable()
386
+ protected *tweenCompositeOperation(
387
+ value: SignalValue<GlobalCompositeOperation>,
388
+ time: number,
389
+ timingFunction: TimingFunction,
390
+ ) {
391
+ const nextValue = unwrap(value);
392
+ if (nextValue === 'source-over') {
393
+ yield* this.compositeOverride(1, time, timingFunction);
394
+ this.compositeOverride(0);
395
+ this.compositeOperation(nextValue);
396
+ } else {
397
+ this.compositeOperation(nextValue);
398
+ this.compositeOverride(1);
399
+ yield* this.compositeOverride(0, time, timingFunction);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Represents the opacity of this node in the range 0-1.
405
+ *
406
+ * @remarks
407
+ * The value is clamped to the range 0-1.
408
+ */
409
+ @initial(1)
410
+ @parser((value: number) => clamp(0, 1, value))
411
+ @signal()
412
+ public declare readonly opacity: SimpleSignal<number, this>;
413
+
414
+ @computed()
415
+ public absoluteOpacity(): number {
416
+ return (this.parent()?.absoluteOpacity() ?? 1) * this.opacity();
417
+ }
418
+
419
+ @filtersSignal()
420
+ public declare readonly filters: FiltersSignal<this>;
421
+
422
+ @initial('#0000')
423
+ @colorSignal()
424
+ public declare readonly shadowColor: ColorSignal<this>;
425
+
426
+ @initial(0)
427
+ @signal()
428
+ public declare readonly shadowBlur: SimpleSignal<number, this>;
429
+
430
+ @vector2Signal('shadowOffset')
431
+ public declare readonly shadowOffset: Vector2Signal<this>;
432
+
433
+ /**
434
+ * @experimental
435
+ */
436
+ @initial([])
437
+ @parser(parseShader)
438
+ @signal()
439
+ public declare readonly shaders: Signal<
440
+ PossibleShaderConfig,
441
+ ShaderConfig[],
442
+ this
443
+ >;
444
+
445
+ @computed()
446
+ protected hasFilters(): boolean {
447
+ return !!this.filters().find(filter => filter.isActive());
448
+ }
449
+
450
+ @computed()
451
+ protected hasShadow() {
452
+ return (
453
+ !!this.shadowColor() &&
454
+ (this.shadowBlur() > 0 ||
455
+ this.shadowOffset.x() !== 0 ||
456
+ this.shadowOffset.y() !== 0)
457
+ );
458
+ }
459
+
460
+ @computed()
461
+ protected filterString(): string {
462
+ let filters = '';
463
+ const matrix = this.compositeToWorld();
464
+ for (const filter of this.filters()) {
465
+ if (filter.isActive()) {
466
+ filters += ' ' + filter.serialize(matrix);
467
+ }
468
+ }
469
+
470
+ return filters;
471
+ }
472
+
473
+ /**
474
+ * @deprecated Use {@link children} instead.
475
+ */
476
+ @inspectable(false)
477
+ @cloneable(false)
478
+ @signal()
479
+ protected declare readonly spawner: SimpleSignal<ComponentChildren, this>;
480
+ protected getSpawner(): ComponentChildren {
481
+ return this.children();
482
+ }
483
+ protected setSpawner(value: SignalValue<ComponentChildren>) {
484
+ this.children(value);
485
+ }
486
+
487
+ @inspectable(false)
488
+ @cloneable(false)
489
+ @signal()
490
+ public declare readonly children: Signal<ComponentChildren, Node[], this>;
491
+ protected setChildren(value: SignalValue<ComponentChildren>) {
492
+ if (this.children.context.raw() === value) {
493
+ return;
494
+ }
495
+
496
+ this.children.context.setter(value);
497
+ if (!isReactive(value)) {
498
+ this.spawnChildren(false, value);
499
+ } else if (!this.hasSpawnedChildren) {
500
+ for (const oldChild of this.realChildren) {
501
+ oldChild.parent(null);
502
+ }
503
+ }
504
+ }
505
+ protected getChildren(): Node[] {
506
+ this.children.context.getter();
507
+ return this.spawnedChildren();
508
+ }
509
+
510
+ @computed()
511
+ protected spawnedChildren(): Node[] {
512
+ const children = this.children.context.getter();
513
+ if (isReactive(this.children.context.raw())) {
514
+ this.spawnChildren(true, children);
515
+ }
516
+ return this.realChildren;
517
+ }
518
+
519
+ @computed()
520
+ protected sortedChildren(): Node[] {
521
+ return [...this.children()].sort((a, b) =>
522
+ Math.sign(a.zIndex() - b.zIndex()),
523
+ );
524
+ }
525
+
526
+ protected view2D: View2D;
527
+ private stateStack: NodeState[] = [];
528
+ protected realChildren: Node[] = [];
529
+ protected hasSpawnedChildren = false;
530
+ private unregister: () => void;
531
+ public readonly parent = createSignal<Node | null>(null);
532
+ public readonly properties = getPropertiesOf(this);
533
+ public readonly key: string;
534
+ public readonly creationStack?: string;
535
+
536
+ public constructor({children, spawner, key, ...rest}: NodeProps) {
537
+ const scene = useScene2D();
538
+ [this.key, this.unregister] = scene.registerNode(this, key);
539
+ this.view2D = scene.getView();
540
+ this.creationStack = new Error().stack;
541
+ initializeSignals(this, rest);
542
+ if (spawner) {
543
+ useLogger().warn({
544
+ message: 'Node.spawner() has been deprecated.',
545
+ remarks: 'Use <code>Node.children()</code> instead.',
546
+ inspect: this.key,
547
+ stack: new Error().stack,
548
+ });
549
+ }
550
+ this.children(spawner ?? children);
551
+ }
552
+
553
+ /**
554
+ * Get the local-to-world matrix for this node.
555
+ *
556
+ * @remarks
557
+ * This matrix transforms vectors from local space of this node to world
558
+ * space.
559
+ *
560
+ * @example
561
+ * Calculate the absolute position of a point located 200 pixels to the right
562
+ * of the node:
563
+ * ```ts
564
+ * const local = new Vector2(0, 200);
565
+ * const world = transformVectorAsPoint(local, node.localToWorld());
566
+ * ```
567
+ */
568
+ @computed()
569
+ public localToWorld(): DOMMatrix {
570
+ const parent = this.parent();
571
+ return parent
572
+ ? parent.localToWorld().multiply(this.localToParent())
573
+ : this.localToParent();
574
+ }
575
+
576
+ /**
577
+ * Get the world-to-local matrix for this node.
578
+ *
579
+ * @remarks
580
+ * This matrix transforms vectors from world space to local space of this
581
+ * node.
582
+ *
583
+ * @example
584
+ * Calculate the position relative to this node for a point located in the
585
+ * top-left corner of the screen:
586
+ * ```ts
587
+ * const world = new Vector2(0, 0);
588
+ * const local = transformVectorAsPoint(world, node.worldToLocal());
589
+ * ```
590
+ */
591
+ @computed()
592
+ public worldToLocal() {
593
+ return this.localToWorld().inverse();
594
+ }
595
+
596
+ /**
597
+ * Get the world-to-parent matrix for this node.
598
+ *
599
+ * @remarks
600
+ * This matrix transforms vectors from world space to local space of this
601
+ * node's parent.
602
+ */
603
+ @computed()
604
+ public worldToParent(): DOMMatrix {
605
+ return this.parent()?.worldToLocal() ?? new DOMMatrix();
606
+ }
607
+
608
+ /**
609
+ * Get the local-to-parent matrix for this node.
610
+ *
611
+ * @remarks
612
+ * This matrix transforms vectors from local space of this node to local space
613
+ * of this node's parent.
614
+ */
615
+ @computed()
616
+ public localToParent(): DOMMatrix {
617
+ const matrix = new DOMMatrix();
618
+ matrix.translateSelf(this.x(), this.y());
619
+ matrix.rotateSelf(0, 0, this.rotation());
620
+ matrix.scaleSelf(this.scale.x(), this.scale.y());
621
+ matrix.skewXSelf(this.skew.x());
622
+ matrix.skewYSelf(this.skew.y());
623
+
624
+ return matrix;
625
+ }
626
+
627
+ /**
628
+ * A matrix mapping composite space to world space.
629
+ *
630
+ * @remarks
631
+ * Certain effects such as blur and shadows ignore the current transformation.
632
+ * This matrix can be used to transform their parameters so that the effect
633
+ * appears relative to the closest composite root.
634
+ */
635
+ @computed()
636
+ public compositeToWorld(): DOMMatrix {
637
+ return this.compositeRoot()?.localToWorld() ?? new DOMMatrix();
638
+ }
639
+
640
+ @computed()
641
+ protected compositeRoot(): Node | null {
642
+ if (this.composite()) {
643
+ return this;
644
+ }
645
+
646
+ return this.parent()?.compositeRoot() ?? null;
647
+ }
648
+
649
+ @computed()
650
+ public compositeToLocal() {
651
+ const root = this.compositeRoot();
652
+ if (root) {
653
+ const worldToLocal = this.worldToLocal();
654
+ worldToLocal.m44 = 1;
655
+ return root.localToWorld().multiply(worldToLocal);
656
+ }
657
+ return new DOMMatrix();
658
+ }
659
+
660
+ public view(): View2D {
661
+ return this.view2D;
662
+ }
663
+
664
+ /**
665
+ * Add the given node(s) as the children of this node.
666
+ *
667
+ * @remarks
668
+ * The nodes will be appended at the end of the children list.
669
+ *
670
+ * @example
671
+ * ```tsx
672
+ * const node = <Layout />;
673
+ * node.add(<Rect />);
674
+ * node.add(<Circle />);
675
+ * ```
676
+ * Result:
677
+ * ```mermaid
678
+ * graph TD;
679
+ * layout([Layout])
680
+ * circle([Circle])
681
+ * rect([Rect])
682
+ * layout-->rect;
683
+ * layout-->circle;
684
+ * ```
685
+ *
686
+ * @param node - A node or an array of nodes to append.
687
+ */
688
+ public add(node: ComponentChildren): this {
689
+ return this.insert(node, Infinity);
690
+ }
691
+
692
+ /**
693
+ * Insert the given node(s) at the specified index in the children list.
694
+ *
695
+ * @example
696
+ * ```tsx
697
+ * const node = (
698
+ * <Layout>
699
+ * <Rect />
700
+ * <Circle />
701
+ * </Layout>
702
+ * );
703
+ *
704
+ * node.insert(<Txt />, 1);
705
+ * ```
706
+ *
707
+ * Result:
708
+ * ```mermaid
709
+ * graph TD;
710
+ * layout([Layout])
711
+ * circle([Circle])
712
+ * text([Text])
713
+ * rect([Rect])
714
+ * layout-->rect;
715
+ * layout-->text;
716
+ * layout-->circle;
717
+ * ```
718
+ *
719
+ * @param node - A node or an array of nodes to insert.
720
+ * @param index - An index at which to insert the node(s).
721
+ */
722
+ public insert(node: ComponentChildren, index = 0): this {
723
+ const array: ComponentChild[] = Array.isArray(node) ? node : [node];
724
+ if (array.length === 0) {
725
+ return this;
726
+ }
727
+
728
+ const children = this.children();
729
+ const newChildren = children.slice(0, index);
730
+
731
+ for (const node of array) {
732
+ if (node instanceof Node) {
733
+ newChildren.push(node);
734
+ node.remove();
735
+ node.parent(this);
736
+ }
737
+ }
738
+
739
+ newChildren.push(...children.slice(index));
740
+ this.setParsedChildren(newChildren);
741
+
742
+ return this;
743
+ }
744
+
745
+ /**
746
+ * Remove this node from the tree.
747
+ */
748
+ public remove(): this {
749
+ const current = this.parent();
750
+ if (current === null) {
751
+ return this;
752
+ }
753
+
754
+ current.removeChild(this);
755
+ this.parent(null);
756
+ return this;
757
+ }
758
+
759
+ /**
760
+ * Rearrange this node in relation to its siblings.
761
+ *
762
+ * @remarks
763
+ * Children are rendered starting from the beginning of the children list.
764
+ * We can change the rendering order by rearranging said list.
765
+ *
766
+ * A positive `by` arguments move the node up (it will be rendered on top of
767
+ * the elements it has passed). Negative values move it down.
768
+ *
769
+ * @param by - Number of places by which the node should be moved.
770
+ */
771
+ public move(by = 1): this {
772
+ const parent = this.parent();
773
+ if (by === 0 || !parent) {
774
+ return this;
775
+ }
776
+
777
+ const children = parent.children();
778
+ const newChildren: Node[] = [];
779
+
780
+ if (by > 0) {
781
+ for (let i = 0; i < children.length; i++) {
782
+ const child = children[i];
783
+ if (child === this) {
784
+ const target = i + by;
785
+ for (; i < target && i + 1 < children.length; i++) {
786
+ newChildren[i] = children[i + 1];
787
+ }
788
+ }
789
+ newChildren[i] = child;
790
+ }
791
+ } else {
792
+ for (let i = children.length - 1; i >= 0; i--) {
793
+ const child = children[i];
794
+ if (child === this) {
795
+ const target = i + by;
796
+ for (; i > target && i > 0; i--) {
797
+ newChildren[i] = children[i - 1];
798
+ }
799
+ }
800
+ newChildren[i] = child;
801
+ }
802
+ }
803
+
804
+ parent.setParsedChildren(newChildren);
805
+
806
+ return this;
807
+ }
808
+
809
+ /**
810
+ * Move the node up in relation to its siblings.
811
+ *
812
+ * @remarks
813
+ * The node will exchange places with the sibling right above it (if any) and
814
+ * from then on will be rendered on top of it.
815
+ */
816
+ public moveUp(): this {
817
+ return this.move(1);
818
+ }
819
+
820
+ /**
821
+ * Move the node down in relation to its siblings.
822
+ *
823
+ * @remarks
824
+ * The node will exchange places with the sibling right below it (if any) and
825
+ * from then on will be rendered under it.
826
+ */
827
+ public moveDown(): this {
828
+ return this.move(-1);
829
+ }
830
+
831
+ /**
832
+ * Move the node to the top in relation to its siblings.
833
+ *
834
+ * @remarks
835
+ * The node will be placed at the end of the children list and from then on
836
+ * will be rendered on top of all of its siblings.
837
+ */
838
+ public moveToTop(): this {
839
+ return this.move(Infinity);
840
+ }
841
+
842
+ /**
843
+ * Move the node to the bottom in relation to its siblings.
844
+ *
845
+ * @remarks
846
+ * The node will be placed at the beginning of the children list and from then
847
+ * on will be rendered below all of its siblings.
848
+ */
849
+ public moveToBottom(): this {
850
+ return this.move(-Infinity);
851
+ }
852
+
853
+ /**
854
+ * Move the node to the provided position relative to its siblings.
855
+ *
856
+ * @remarks
857
+ * If the node is getting moved to a lower position, it will be placed below
858
+ * the sibling that's currently at the provided index (if any).
859
+ * If the node is getting moved to a higher position, it will be placed above
860
+ * the sibling that's currently at the provided index (if any).
861
+ *
862
+ * @param index - The index to move the node to.
863
+ */
864
+ public moveTo(index: number): this {
865
+ const parent = this.parent();
866
+ if (!parent) {
867
+ return this;
868
+ }
869
+
870
+ const currentIndex = parent.children().indexOf(this);
871
+ const by = index - currentIndex;
872
+
873
+ return this.move(by);
874
+ }
875
+
876
+ /**
877
+ * Move the node below the provided node in the parent's layout.
878
+ *
879
+ * @remarks
880
+ * The node will be moved below the provided node and from then on will be
881
+ * rendered below it. By default, if the node is already positioned lower than
882
+ * the sibling node, it will not get moved.
883
+ *
884
+ * @param node - The sibling node below which to move.
885
+ * @param directlyBelow - Whether the node should be positioned directly below
886
+ * the sibling. When true, will move the node even if
887
+ * it is already positioned below the sibling.
888
+ */
889
+ public moveBelow(node: Node, directlyBelow = false): this {
890
+ const parent = this.parent();
891
+ if (!parent) {
892
+ return this;
893
+ }
894
+
895
+ if (node.parent() !== parent) {
896
+ useLogger().error(
897
+ "Cannot position nodes relative to each other if they don't belong to the same parent.",
898
+ );
899
+ return this;
900
+ }
901
+
902
+ const children = parent.children();
903
+ const ownIndex = children.indexOf(this);
904
+ const otherIndex = children.indexOf(node);
905
+
906
+ if (!directlyBelow && ownIndex < otherIndex) {
907
+ // Nothing to do if the node is already positioned below the target node.
908
+ // We could move the node so it's directly below the sibling node, but
909
+ // that might suddenly move it on top of other nodes. This is likely
910
+ // not what the user wanted to happen when calling this method.
911
+ return this;
912
+ }
913
+
914
+ const by = otherIndex - ownIndex - 1;
915
+
916
+ return this.move(by);
917
+ }
918
+
919
+ /**
920
+ * Move the node above the provided node in the parent's layout.
921
+ *
922
+ * @remarks
923
+ * The node will be moved above the provided node and from then on will be
924
+ * rendered on top of it. By default, if the node is already positioned
925
+ * higher than the sibling node, it will not get moved.
926
+ *
927
+ * @param node - The sibling node below which to move.
928
+ * @param directlyAbove - Whether the node should be positioned directly above the
929
+ * sibling. When true, will move the node even if it is
930
+ * already positioned above the sibling.
931
+ */
932
+ public moveAbove(node: Node, directlyAbove = false): this {
933
+ const parent = this.parent();
934
+ if (!parent) {
935
+ return this;
936
+ }
937
+
938
+ if (node.parent() !== parent) {
939
+ useLogger().error(
940
+ "Cannot position nodes relative to each other if they don't belong to the same parent.",
941
+ );
942
+ return this;
943
+ }
944
+
945
+ const children = parent.children();
946
+ const ownIndex = children.indexOf(this);
947
+ const otherIndex = children.indexOf(node);
948
+
949
+ if (!directlyAbove && ownIndex > otherIndex) {
950
+ // Nothing to do if the node is already positioned above the target node.
951
+ // We could move the node so it's directly above the sibling node, but
952
+ // that might suddenly move it below other nodes. This is likely not what
953
+ // the user wanted to happen when calling this method.
954
+ return this;
955
+ }
956
+
957
+ const by = otherIndex - ownIndex + 1;
958
+
959
+ return this.move(by);
960
+ }
961
+
962
+ /**
963
+ * Change the parent of this node while keeping the absolute transform.
964
+ *
965
+ * @remarks
966
+ * After performing this operation, the node will stay in the same place
967
+ * visually, but its parent will be changed.
968
+ *
969
+ * @param newParent - The new parent of this node.
970
+ */
971
+ public reparent(newParent: Node) {
972
+ const position = this.absolutePosition();
973
+ const rotation = this.absoluteRotation();
974
+ const scale = this.absoluteScale();
975
+ newParent.add(this);
976
+ this.absolutePosition(position);
977
+ this.absoluteRotation(rotation);
978
+ this.absoluteScale(scale);
979
+ }
980
+
981
+ /**
982
+ * Remove all children of this node.
983
+ */
984
+ public removeChildren() {
985
+ for (const oldChild of this.realChildren) {
986
+ oldChild.parent(null);
987
+ }
988
+ this.setParsedChildren([]);
989
+ }
990
+
991
+ /**
992
+ * Get the current children of this node.
993
+ *
994
+ * @remarks
995
+ * Unlike {@link children}, this method does not have any side effects.
996
+ * It does not register the `children` signal as a dependency, and it does not
997
+ * spawn any children. It can be used to safely retrieve the current state of
998
+ * the scene graph for debugging purposes.
999
+ */
1000
+ public peekChildren(): readonly Node[] {
1001
+ return this.realChildren;
1002
+ }
1003
+
1004
+ /**
1005
+ * Find all descendants of this node that match the given predicate.
1006
+ *
1007
+ * @param predicate - A function that returns true if the node matches.
1008
+ */
1009
+ public findAll<T extends Node>(predicate: (node: any) => node is T): T[];
1010
+ /**
1011
+ * Find all descendants of this node that match the given predicate.
1012
+ *
1013
+ * @param predicate - A function that returns true if the node matches.
1014
+ */
1015
+ public findAll<T extends Node = Node>(predicate: (node: any) => boolean): T[];
1016
+ public findAll<T extends Node>(predicate: (node: any) => node is T): T[] {
1017
+ const result: T[] = [];
1018
+ const queue = this.reversedChildren();
1019
+ while (queue.length > 0) {
1020
+ const node = queue.pop()!;
1021
+ if (predicate(node)) {
1022
+ result.push(node);
1023
+ }
1024
+ const children = node.children();
1025
+ for (let i = children.length - 1; i >= 0; i--) {
1026
+ queue.push(children[i]);
1027
+ }
1028
+ }
1029
+
1030
+ return result;
1031
+ }
1032
+
1033
+ /**
1034
+ * Find the first descendant of this node that matches the given predicate.
1035
+ *
1036
+ * @param predicate - A function that returns true if the node matches.
1037
+ */
1038
+ public findFirst<T extends Node>(
1039
+ predicate: (node: Node) => node is T,
1040
+ ): T | null;
1041
+ /**
1042
+ * Find the first descendant of this node that matches the given predicate.
1043
+ *
1044
+ * @param predicate - A function that returns true if the node matches.
1045
+ */
1046
+ public findFirst<T extends Node = Node>(
1047
+ predicate: (node: Node) => boolean,
1048
+ ): T | null;
1049
+ public findFirst<T extends Node>(
1050
+ predicate: (node: Node) => node is T,
1051
+ ): T | null {
1052
+ const queue = this.reversedChildren();
1053
+ while (queue.length > 0) {
1054
+ const node = queue.pop()!;
1055
+ if (predicate(node)) {
1056
+ return node;
1057
+ }
1058
+ const children = node.children();
1059
+ for (let i = children.length - 1; i >= 0; i--) {
1060
+ queue.push(children[i]);
1061
+ }
1062
+ }
1063
+
1064
+ return null;
1065
+ }
1066
+
1067
+ /**
1068
+ * Find the last descendant of this node that matches the given predicate.
1069
+ *
1070
+ * @param predicate - A function that returns true if the node matches.
1071
+ */
1072
+ public findLast<T extends Node>(
1073
+ predicate: (node: Node) => node is T,
1074
+ ): T | null;
1075
+ /**
1076
+ * Find the last descendant of this node that matches the given predicate.
1077
+ *
1078
+ * @param predicate - A function that returns true if the node matches.
1079
+ */
1080
+ public findLast<T extends Node = Node>(
1081
+ predicate: (node: Node) => boolean,
1082
+ ): T | null;
1083
+ public findLast<T extends Node>(
1084
+ predicate: (node: Node) => node is T,
1085
+ ): T | null {
1086
+ const search: Node[] = [];
1087
+ const queue = this.reversedChildren();
1088
+
1089
+ while (queue.length > 0) {
1090
+ const node = queue.pop()!;
1091
+ search.push(node);
1092
+ const children = node.children();
1093
+ for (let i = children.length - 1; i >= 0; i--) {
1094
+ queue.push(children[i]);
1095
+ }
1096
+ }
1097
+
1098
+ while (search.length > 0) {
1099
+ const node = search.pop()!;
1100
+ if (predicate(node)) {
1101
+ return node;
1102
+ }
1103
+ }
1104
+
1105
+ return null;
1106
+ }
1107
+
1108
+ /**
1109
+ * Find the closest ancestor of this node that matches the given predicate.
1110
+ *
1111
+ * @param predicate - A function that returns true if the node matches.
1112
+ */
1113
+ public findAncestor<T extends Node>(
1114
+ predicate: (node: Node) => node is T,
1115
+ ): T | null;
1116
+ /**
1117
+ * Find the closest ancestor of this node that matches the given predicate.
1118
+ *
1119
+ * @param predicate - A function that returns true if the node matches.
1120
+ */
1121
+ public findAncestor<T extends Node = Node>(
1122
+ predicate: (node: Node) => boolean,
1123
+ ): T | null;
1124
+ public findAncestor<T extends Node>(
1125
+ predicate: (node: Node) => node is T,
1126
+ ): T | null {
1127
+ let parent: Node | null = this.parent();
1128
+ while (parent) {
1129
+ if (predicate(parent)) {
1130
+ return parent;
1131
+ }
1132
+ parent = parent.parent();
1133
+ }
1134
+
1135
+ return null;
1136
+ }
1137
+
1138
+ /**
1139
+ * Get the nth children cast to the specified type.
1140
+ *
1141
+ * @param index - The index of the child to retrieve.
1142
+ */
1143
+ public childAs<T extends Node = Node>(index: number): T | null {
1144
+ return (this.children()[index] as T) ?? null;
1145
+ }
1146
+
1147
+ /**
1148
+ * Get the children array cast to the specified type.
1149
+ */
1150
+ public childrenAs<T extends Node = Node>(): T[] {
1151
+ return this.children() as T[];
1152
+ }
1153
+
1154
+ /**
1155
+ * Get the parent cast to the specified type.
1156
+ */
1157
+ public parentAs<T extends Node = Node>(): T | null {
1158
+ return (this.parent() as T) ?? null;
1159
+ }
1160
+
1161
+ /**
1162
+ * Prepare this node to be disposed of.
1163
+ *
1164
+ * @remarks
1165
+ * This method is called automatically when a scene is refreshed. It will
1166
+ * be called even if the node is not currently attached to the tree.
1167
+ *
1168
+ * The goal of this method is to clean any external references to allow the
1169
+ * node to be garbage collected.
1170
+ */
1171
+ public dispose() {
1172
+ if (!this.unregister) {
1173
+ return;
1174
+ }
1175
+
1176
+ this.stateStack = [];
1177
+ this.unregister();
1178
+ this.unregister = null!;
1179
+ for (const {signal} of this) {
1180
+ signal?.context.dispose();
1181
+ }
1182
+ for (const child of this.realChildren) {
1183
+ child.dispose();
1184
+ }
1185
+ }
1186
+
1187
+ /**
1188
+ * Create a copy of this node.
1189
+ *
1190
+ * @param customProps - Properties to override.
1191
+ */
1192
+ public clone(customProps: NodeState = {}): this {
1193
+ const props = {...customProps};
1194
+ if (isReactive(this.children.context.raw())) {
1195
+ props.children ??= this.children.context.raw();
1196
+ } else if (this.children().length > 0) {
1197
+ props.children ??= this.children().map(child => child.clone());
1198
+ }
1199
+
1200
+ for (const {key, meta, signal} of this) {
1201
+ if (!meta.cloneable || key in props) continue;
1202
+ if (meta.compound) {
1203
+ for (const [key, property] of meta.compoundEntries) {
1204
+ if (property in props) continue;
1205
+ const component = (<Record<string, SimpleSignal<any>>>(
1206
+ (<unknown>signal)
1207
+ ))[key];
1208
+ if (!component.context.isInitial()) {
1209
+ props[property] = component.context.raw();
1210
+ }
1211
+ }
1212
+ } else if (!signal.context.isInitial()) {
1213
+ props[key] = signal.context.raw();
1214
+ }
1215
+ }
1216
+
1217
+ return this.instantiate(props);
1218
+ }
1219
+
1220
+ /**
1221
+ * Create a copy of this node.
1222
+ *
1223
+ * @remarks
1224
+ * Unlike {@link clone}, a snapshot clone calculates any reactive properties
1225
+ * at the moment of cloning and passes the raw values to the copy.
1226
+ *
1227
+ * @param customProps - Properties to override.
1228
+ */
1229
+ public snapshotClone(customProps: NodeState = {}): this {
1230
+ const props = {
1231
+ ...this.getState(),
1232
+ ...customProps,
1233
+ };
1234
+
1235
+ if (this.children().length > 0) {
1236
+ props.children ??= this.children().map(child => child.snapshotClone());
1237
+ }
1238
+
1239
+ return this.instantiate(props);
1240
+ }
1241
+
1242
+ /**
1243
+ * Create a reactive copy of this node.
1244
+ *
1245
+ * @remarks
1246
+ * A reactive copy has all its properties dynamically updated to match the
1247
+ * source node.
1248
+ *
1249
+ * @param customProps - Properties to override.
1250
+ */
1251
+ public reactiveClone(customProps: NodeState = {}): this {
1252
+ const props = {...customProps};
1253
+ if (this.children().length > 0) {
1254
+ props.children ??= this.children().map(child => child.reactiveClone());
1255
+ }
1256
+
1257
+ for (const {key, meta, signal} of this) {
1258
+ if (!meta.cloneable || key in props) continue;
1259
+ props[key] = () => signal();
1260
+ }
1261
+
1262
+ return this.instantiate(props);
1263
+ }
1264
+
1265
+ /**
1266
+ * Create an instance of this node's class.
1267
+ *
1268
+ * @param props - Properties to pass to the constructor.
1269
+ */
1270
+ public instantiate(props: NodeProps = {}): this {
1271
+ return new (<NodeConstructor<NodeProps, this>>this.constructor)(props);
1272
+ }
1273
+
1274
+ /**
1275
+ * Set the children without parsing them.
1276
+ *
1277
+ * @remarks
1278
+ * This method assumes that the caller took care of parsing the children and
1279
+ * updating the hierarchy.
1280
+ *
1281
+ * @param value - The children to set.
1282
+ */
1283
+ protected setParsedChildren(value: Node[]) {
1284
+ this.children.context.setter(value);
1285
+ this.realChildren = value;
1286
+ }
1287
+
1288
+ protected spawnChildren(reactive: boolean, children: ComponentChildren) {
1289
+ const parsedChildren = this.parseChildren(children);
1290
+
1291
+ const keep = new Set<string>();
1292
+ for (const newChild of parsedChildren) {
1293
+ const current = newChild.parent.context.raw() as Node | null;
1294
+ if (current && current !== this) {
1295
+ current.removeChild(newChild);
1296
+ }
1297
+ keep.add(newChild.key);
1298
+ newChild.parent(this);
1299
+ }
1300
+
1301
+ for (const oldChild of this.realChildren) {
1302
+ if (!keep.has(oldChild.key)) {
1303
+ oldChild.parent(null);
1304
+ }
1305
+ }
1306
+
1307
+ this.hasSpawnedChildren = reactive;
1308
+ this.realChildren = parsedChildren;
1309
+ }
1310
+
1311
+ /**
1312
+ * Parse any `ComponentChildren` into an array of nodes.
1313
+ *
1314
+ * @param children - The children to parse.
1315
+ */
1316
+ protected parseChildren(children: ComponentChildren): Node[] {
1317
+ const result: Node[] = [];
1318
+ const array = Array.isArray(children) ? children : [children];
1319
+ for (const child of array) {
1320
+ if (child instanceof Node) {
1321
+ result.push(child);
1322
+ }
1323
+ }
1324
+
1325
+ return result;
1326
+ }
1327
+
1328
+ /**
1329
+ * Remove the given child.
1330
+ */
1331
+ protected removeChild(child: Node) {
1332
+ this.setParsedChildren(this.children().filter(node => node !== child));
1333
+ }
1334
+
1335
+ /**
1336
+ * Whether this node should be cached or not.
1337
+ */
1338
+ protected requiresCache(): boolean {
1339
+ return (
1340
+ this.cache() ||
1341
+ this.opacity() < 1 ||
1342
+ this.compositeOperation() !== 'source-over' ||
1343
+ this.hasFilters() ||
1344
+ this.hasShadow() ||
1345
+ this.shaders().length > 0
1346
+ );
1347
+ }
1348
+
1349
+ @computed()
1350
+ protected cacheCanvas(): CanvasRenderingContext2D {
1351
+ const canvas = document.createElement('canvas').getContext('2d');
1352
+ if (!canvas) {
1353
+ throw new Error('Could not create a cache canvas');
1354
+ }
1355
+
1356
+ return canvas;
1357
+ }
1358
+
1359
+ /**
1360
+ * Get a cache canvas with the contents of this node rendered onto it.
1361
+ */
1362
+ @computed()
1363
+ protected async cachedCanvas() {
1364
+ const context = this.cacheCanvas();
1365
+ const cache = this.worldSpaceCacheBBox();
1366
+ const matrix = this.localToWorld();
1367
+
1368
+ context.canvas.width = cache.width;
1369
+ context.canvas.height = cache.height;
1370
+
1371
+ context.setTransform(
1372
+ matrix.a,
1373
+ matrix.b,
1374
+ matrix.c,
1375
+ matrix.d,
1376
+ matrix.e - cache.x,
1377
+ matrix.f - cache.y,
1378
+ );
1379
+ await this.draw(context);
1380
+
1381
+ return context;
1382
+ }
1383
+
1384
+ /**
1385
+ * Get a bounding box for the contents rendered by this node.
1386
+ *
1387
+ * @remarks
1388
+ * The returned bounding box should be in local space.
1389
+ */
1390
+ protected getCacheBBox(): BBox {
1391
+ return new BBox();
1392
+ }
1393
+
1394
+ /**
1395
+ * Get a bounding box for the contents rendered by this node as well
1396
+ * as its children.
1397
+ */
1398
+ @computed()
1399
+ public cacheBBox(): BBox {
1400
+ const cache = this.getCacheBBox();
1401
+ const children = this.children();
1402
+ const padding = this.cachePadding();
1403
+ if (children.length === 0) {
1404
+ return cache.addSpacing(padding);
1405
+ }
1406
+
1407
+ const points: Vector2[] = cache.corners;
1408
+ for (const child of children) {
1409
+ const childCache = child.fullCacheBBox();
1410
+ const childMatrix = child.localToParent();
1411
+ points.push(
1412
+ ...childCache.corners.map(r => transformVectorAsPoint(r, childMatrix)),
1413
+ );
1414
+ }
1415
+
1416
+ const bbox = BBox.fromPoints(...points);
1417
+ return bbox.addSpacing(padding);
1418
+ }
1419
+
1420
+ /**
1421
+ * Get a bounding box for the contents rendered by this node (including
1422
+ * effects applied after caching).
1423
+ *
1424
+ * @remarks
1425
+ * The returned bounding box should be in local space.
1426
+ */
1427
+ @computed()
1428
+ protected fullCacheBBox(): BBox {
1429
+ const matrix = this.compositeToLocal();
1430
+ const shadowOffset = transformVector(this.shadowOffset(), matrix);
1431
+ const shadowBlur = transformScalar(this.shadowBlur(), matrix);
1432
+
1433
+ const result = this.cacheBBox().expand(
1434
+ this.filters.blur() * 2 + shadowBlur,
1435
+ );
1436
+
1437
+ if (shadowOffset.x < 0) {
1438
+ result.x += shadowOffset.x;
1439
+ result.width -= shadowOffset.x;
1440
+ } else {
1441
+ result.width += shadowOffset.x;
1442
+ }
1443
+
1444
+ if (shadowOffset.y < 0) {
1445
+ result.y += shadowOffset.y;
1446
+ result.height -= shadowOffset.y;
1447
+ } else {
1448
+ result.height += shadowOffset.y;
1449
+ }
1450
+
1451
+ return result;
1452
+ }
1453
+
1454
+ /**
1455
+ * Get a bounding box in world space for the contents rendered by this node as
1456
+ * well as its children.
1457
+ *
1458
+ * @remarks
1459
+ * This is the same the bounding box returned by {@link cacheBBox} only
1460
+ * transformed to world space.
1461
+ */
1462
+ @computed()
1463
+ protected worldSpaceCacheBBox(): BBox {
1464
+ const viewBBox = BBox.fromSizeCentered(this.view().size());
1465
+ const canvasBBox = BBox.fromPoints(
1466
+ ...viewBBox.transformCorners(this.view().localToWorld()),
1467
+ );
1468
+ const cacheBBox = BBox.fromPoints(
1469
+ ...this.cacheBBox().transformCorners(this.localToWorld()),
1470
+ ).pixelPerfect.expand(2);
1471
+
1472
+ return canvasBBox.intersection(cacheBBox);
1473
+ }
1474
+
1475
+ @computed()
1476
+ protected parentWorldSpaceCacheBBox(): BBox {
1477
+ return (
1478
+ this.findAncestor(node => node.requiresCache())?.worldSpaceCacheBBox() ??
1479
+ new BBox(Vector2.zero, useScene2D().getSize())
1480
+ );
1481
+ }
1482
+
1483
+ /**
1484
+ * Prepare the given context for drawing a cached node onto it.
1485
+ *
1486
+ * @remarks
1487
+ * This method is called before the contents of the cache canvas are drawn
1488
+ * on the screen. It can be used to apply effects to the entire node together
1489
+ * with its children, instead of applying them individually.
1490
+ * Effects such as transparency, shadows, and filters use this technique.
1491
+ *
1492
+ * Whether the node is cached is decided by the {@link requiresCache} method.
1493
+ *
1494
+ * @param context - The context using which the cache will be drawn.
1495
+ */
1496
+ protected setupDrawFromCache(context: CanvasRenderingContext2D) {
1497
+ context.globalCompositeOperation = this.compositeOperation();
1498
+ context.globalAlpha *= this.opacity();
1499
+ if (this.hasFilters()) {
1500
+ context.filter = this.filterString();
1501
+ }
1502
+ if (this.hasShadow()) {
1503
+ const matrix = this.compositeToWorld();
1504
+ const offset = transformVector(this.shadowOffset(), matrix);
1505
+ const blur = transformScalar(this.shadowBlur(), matrix);
1506
+
1507
+ context.shadowColor = this.shadowColor().serialize();
1508
+ context.shadowBlur = blur;
1509
+ context.shadowOffsetX = offset.x;
1510
+ context.shadowOffsetY = offset.y;
1511
+ }
1512
+
1513
+ const matrix = this.worldToLocal();
1514
+ context.transform(
1515
+ matrix.a,
1516
+ matrix.b,
1517
+ matrix.c,
1518
+ matrix.d,
1519
+ matrix.e,
1520
+ matrix.f,
1521
+ );
1522
+ }
1523
+
1524
+ protected renderFromSource(
1525
+ context: CanvasRenderingContext2D,
1526
+ source: CanvasImageSource,
1527
+ x: number,
1528
+ y: number,
1529
+ ) {
1530
+ this.setupDrawFromCache(context);
1531
+
1532
+ const compositeOverride = this.compositeOverride();
1533
+ context.drawImage(source, x, y);
1534
+ if (compositeOverride > 0) {
1535
+ context.save();
1536
+ context.globalAlpha *= compositeOverride;
1537
+ context.globalCompositeOperation = 'source-over';
1538
+ context.drawImage(source, x, y);
1539
+ context.restore();
1540
+ }
1541
+ }
1542
+
1543
+ private shaderCanvas(destination: TexImageSource, source: TexImageSource) {
1544
+ const shaders = this.shaders();
1545
+ if (shaders.length === 0) {
1546
+ return null;
1547
+ }
1548
+
1549
+ const scene = useScene2D();
1550
+ const size = scene.getRealSize();
1551
+ const parentCacheRect = this.parentWorldSpaceCacheBBox();
1552
+ const cameraToWorld = new DOMMatrix()
1553
+ .scaleSelf(
1554
+ size.width / parentCacheRect.width,
1555
+ size.height / -parentCacheRect.height,
1556
+ )
1557
+ .translateSelf(
1558
+ parentCacheRect.x / -size.width,
1559
+ parentCacheRect.y / size.height - 1,
1560
+ );
1561
+
1562
+ const cacheRect = this.worldSpaceCacheBBox();
1563
+ const cameraToCache = new DOMMatrix()
1564
+ .scaleSelf(size.width / cacheRect.width, size.height / -cacheRect.height)
1565
+ .translateSelf(cacheRect.x / -size.width, cacheRect.y / size.height - 1)
1566
+ .invertSelf();
1567
+
1568
+ const gl = scene.shaders.getGL();
1569
+ scene.shaders.copyTextures(destination, source);
1570
+ scene.shaders.clear();
1571
+
1572
+ for (const shader of shaders) {
1573
+ const program = scene.shaders.getProgram(shader.fragment);
1574
+ if (!program) {
1575
+ continue;
1576
+ }
1577
+
1578
+ if (shader.uniforms) {
1579
+ for (const [name, uniform] of Object.entries(shader.uniforms)) {
1580
+ const location = gl.getUniformLocation(program, name);
1581
+ if (location === null) {
1582
+ continue;
1583
+ }
1584
+
1585
+ const value = unwrap(uniform);
1586
+ if (typeof value === 'number') {
1587
+ gl.uniform1f(location, value);
1588
+ } else if ('toUniform' in value) {
1589
+ value.toUniform(gl, location);
1590
+ } else if (value.length === 1) {
1591
+ gl.uniform1f(location, value[0]);
1592
+ } else if (value.length === 2) {
1593
+ gl.uniform2f(location, value[0], value[1]);
1594
+ } else if (value.length === 3) {
1595
+ gl.uniform3f(location, value[0], value[1], value[2]);
1596
+ } else if (value.length === 4) {
1597
+ gl.uniform4f(location, value[0], value[1], value[2], value[3]);
1598
+ }
1599
+ }
1600
+ }
1601
+
1602
+ gl.uniform1f(
1603
+ gl.getUniformLocation(program, UNIFORM_TIME),
1604
+ this.view2D.globalTime(),
1605
+ );
1606
+
1607
+ gl.uniform1i(
1608
+ gl.getUniformLocation(program, UNIFORM_TIME),
1609
+ scene.playback.frame,
1610
+ );
1611
+
1612
+ gl.uniformMatrix4fv(
1613
+ gl.getUniformLocation(program, UNIFORM_SOURCE_MATRIX),
1614
+ false,
1615
+ cameraToCache.toFloat32Array(),
1616
+ );
1617
+
1618
+ gl.uniformMatrix4fv(
1619
+ gl.getUniformLocation(program, UNIFORM_DESTINATION_MATRIX),
1620
+ false,
1621
+ cameraToWorld.toFloat32Array(),
1622
+ );
1623
+
1624
+ shader.setup?.(gl, program);
1625
+ scene.shaders.render();
1626
+ shader.teardown?.(gl, program);
1627
+ }
1628
+
1629
+ return gl.canvas;
1630
+ }
1631
+
1632
+ /**
1633
+ * Render this node onto the given canvas.
1634
+ *
1635
+ * @param context - The context to draw with.
1636
+ */
1637
+ public async render(context: CanvasRenderingContext2D) {
1638
+ if (this.absoluteOpacity() <= 0) {
1639
+ return;
1640
+ }
1641
+
1642
+ context.save();
1643
+ this.transformContext(context);
1644
+
1645
+ if (this.requiresCache()) {
1646
+ const cacheRect = this.worldSpaceCacheBBox();
1647
+ if (cacheRect.width !== 0 && cacheRect.height !== 0) {
1648
+ const cache = (await this.cachedCanvas()).canvas;
1649
+ const source = this.shaderCanvas(context.canvas, cache);
1650
+ if (source) {
1651
+ this.renderFromSource(context, source, 0, 0);
1652
+ } else {
1653
+ this.renderFromSource(
1654
+ context,
1655
+ cache,
1656
+ cacheRect.position.x,
1657
+ cacheRect.position.y,
1658
+ );
1659
+ }
1660
+ }
1661
+ } else {
1662
+ await this.draw(context);
1663
+ }
1664
+
1665
+ context.restore();
1666
+ }
1667
+
1668
+ /**
1669
+ * Draw this node onto the canvas.
1670
+ *
1671
+ * @remarks
1672
+ * This method is used when drawing directly onto the screen as well as onto
1673
+ * the cache canvas.
1674
+ * It assumes that the context have already been transformed to local space.
1675
+ *
1676
+ * @param context - The context to draw with.
1677
+ */
1678
+ protected async draw(context: CanvasRenderingContext2D) {
1679
+ await this.drawChildren(context);
1680
+ }
1681
+
1682
+ protected async drawChildren(context: CanvasRenderingContext2D) {
1683
+ for (const child of this.sortedChildren()) {
1684
+ await child.render(context);
1685
+ }
1686
+ }
1687
+
1688
+ /**
1689
+ * Draw an overlay for this node.
1690
+ *
1691
+ * @remarks
1692
+ * The overlay for the currently inspected node is displayed on top of the
1693
+ * canvas.
1694
+ *
1695
+ * The provided context is in screen space. The local-to-screen matrix can be
1696
+ * used to transform all shapes that need to be displayed.
1697
+ * This approach allows to keep the line widths and gizmo sizes consistent,
1698
+ * no matter how zoomed-in the view is.
1699
+ *
1700
+ * @param context - The context to draw with.
1701
+ * @param matrix - A local-to-screen matrix.
1702
+ */
1703
+ public drawOverlay(context: CanvasRenderingContext2D, matrix: DOMMatrix) {
1704
+ const box = this.cacheBBox().transformCorners(matrix);
1705
+ const cache = this.getCacheBBox().transformCorners(matrix);
1706
+ context.strokeStyle = 'white';
1707
+ context.lineWidth = 1;
1708
+ context.beginPath();
1709
+ drawLine(context, box);
1710
+ context.closePath();
1711
+ context.stroke();
1712
+
1713
+ context.strokeStyle = 'blue';
1714
+ context.beginPath();
1715
+ drawLine(context, cache);
1716
+ context.closePath();
1717
+ context.stroke();
1718
+ }
1719
+
1720
+ protected transformContext(context: CanvasRenderingContext2D) {
1721
+ const matrix = this.localToParent();
1722
+ context.transform(
1723
+ matrix.a,
1724
+ matrix.b,
1725
+ matrix.c,
1726
+ matrix.d,
1727
+ matrix.e,
1728
+ matrix.f,
1729
+ );
1730
+ }
1731
+
1732
+ /**
1733
+ * Try to find a node intersecting the given position.
1734
+ *
1735
+ * @param position - The searched position.
1736
+ */
1737
+ public hit(position: Vector2): Node | null {
1738
+ let hit: Node | null = null;
1739
+ const local = transformVectorAsPoint(
1740
+ position,
1741
+ this.localToParent().inverse(),
1742
+ );
1743
+ const children = this.children();
1744
+ for (let i = children.length - 1; i >= 0; i--) {
1745
+ hit = children[i].hit(local);
1746
+ if (hit) {
1747
+ break;
1748
+ }
1749
+ }
1750
+
1751
+ return hit;
1752
+ }
1753
+
1754
+ /**
1755
+ * Collect all asynchronous resources used by this node.
1756
+ */
1757
+ protected collectAsyncResources() {
1758
+ for (const child of this.children()) {
1759
+ child.collectAsyncResources();
1760
+ }
1761
+ }
1762
+
1763
+ /**
1764
+ * Wait for any asynchronous resources that this node or its children have.
1765
+ *
1766
+ * @remarks
1767
+ * Certain resources like images are always loaded asynchronously.
1768
+ * Awaiting this method makes sure that all such resources are done loading
1769
+ * before continuing the animation.
1770
+ */
1771
+ public async toPromise(): Promise<this> {
1772
+ do {
1773
+ await DependencyContext.consumePromises();
1774
+ this.collectAsyncResources();
1775
+ } while (DependencyContext.hasPromises());
1776
+ return this;
1777
+ }
1778
+
1779
+ /**
1780
+ * Return a snapshot of the node's current signal values.
1781
+ *
1782
+ * @remarks
1783
+ * This method will calculate the values of any reactive properties of the
1784
+ * node at the time the method is called.
1785
+ */
1786
+ public getState(): NodeState {
1787
+ const state: NodeState = {};
1788
+ for (const {key, meta, signal} of this) {
1789
+ if (!meta.cloneable || key in state) continue;
1790
+ state[key] = signal();
1791
+ }
1792
+ return state;
1793
+ }
1794
+
1795
+ /**
1796
+ * Apply the given state to the node, setting all matching signal values to
1797
+ * the provided values.
1798
+ *
1799
+ * @param state - The state to apply to the node.
1800
+ */
1801
+ public applyState(state: NodeState): void;
1802
+ /**
1803
+ * Smoothly transition between the current state of the node and the given
1804
+ * state.
1805
+ *
1806
+ * @param state - The state to transition to.
1807
+ * @param duration - The duration of the transition.
1808
+ * @param timing - The timing function to use for the transition.
1809
+ */
1810
+ public applyState(
1811
+ state: NodeState,
1812
+ duration: number,
1813
+ timing?: TimingFunction,
1814
+ ): ThreadGenerator;
1815
+ public applyState(
1816
+ state: NodeState,
1817
+ duration?: number,
1818
+ timing: TimingFunction = easeInOutCubic,
1819
+ ): ThreadGenerator | void {
1820
+ if (duration === undefined) {
1821
+ for (const key in state) {
1822
+ const signal = this.signalByKey(key);
1823
+ if (signal) {
1824
+ signal(state[key]);
1825
+ }
1826
+ }
1827
+ }
1828
+
1829
+ const tasks: ThreadGenerator[] = [];
1830
+ for (const key in state) {
1831
+ const signal = this.signalByKey(key);
1832
+ if (state[key] !== signal.context.raw()) {
1833
+ tasks.push(signal(state[key], duration!, timing));
1834
+ }
1835
+ }
1836
+
1837
+ return all(...tasks);
1838
+ }
1839
+
1840
+ /**
1841
+ * Push a snapshot of the node's current state onto the node's state stack.
1842
+ *
1843
+ * @remarks
1844
+ * This method can be used together with the {@link restore} method to save a
1845
+ * node's current state and later restore it. It is possible to store more
1846
+ * than one state by calling `save` method multiple times.
1847
+ */
1848
+ public save(): void {
1849
+ this.stateStack.push(this.getState());
1850
+ }
1851
+
1852
+ /**
1853
+ * Restore the node to its last saved state.
1854
+ *
1855
+ * @remarks
1856
+ * This method can be used together with the {@link save} method to restore a
1857
+ * node to a previously saved state. Restoring a node to a previous state
1858
+ * removes that state from the state stack.
1859
+ *
1860
+ * @example
1861
+ * ```tsx
1862
+ * const node = <Circle width={100} height={100} fill={"lightseagreen"} />
1863
+ *
1864
+ * view.add(node);
1865
+ *
1866
+ * // Save the node's current state
1867
+ * node.save();
1868
+ *
1869
+ * // Modify some of the node's properties
1870
+ * yield* node.scale(2, 1);
1871
+ * yield* node.fill('hotpink', 1);
1872
+ *
1873
+ * // Restore the node to its saved state
1874
+ * node.restore();
1875
+ * ```
1876
+ */
1877
+ public restore(): void;
1878
+ /**
1879
+ * Tween the node to its last saved state.
1880
+ *
1881
+ * @remarks
1882
+ * This method can be used together with the {@link save} method to restore a
1883
+ * node to a previously saved state. Restoring a node to a previous state
1884
+ * removes that state from the state stack.
1885
+ *
1886
+ * @example
1887
+ * ```tsx
1888
+ * const node = <Circle width={100} height={100} fill={"lightseagreen"} />
1889
+ *
1890
+ * view.add(node);
1891
+ *
1892
+ * // Save the node's current state
1893
+ * node.save();
1894
+ *
1895
+ * // Modify some of the node's properties
1896
+ * yield* node.scale(2, 1);
1897
+ * yield* node.fill('hotpink', 1);
1898
+ *
1899
+ * // Tween the node to its saved state over 1 second
1900
+ * yield* node.restore(1);
1901
+ * ```
1902
+ *
1903
+ * @param duration - The duration of the transition.
1904
+ * @param timing - The timing function to use for the transition.
1905
+ */
1906
+ public restore(duration: number, timing?: TimingFunction): ThreadGenerator;
1907
+ public restore(
1908
+ duration?: number,
1909
+ timing: TimingFunction = easeInOutCubic,
1910
+ ): ThreadGenerator | void {
1911
+ const state = this.stateStack.pop();
1912
+
1913
+ if (state !== undefined) {
1914
+ return this.applyState(state, duration!, timing);
1915
+ }
1916
+ }
1917
+
1918
+ public *[Symbol.iterator]() {
1919
+ for (const key in this.properties) {
1920
+ const meta = this.properties[key];
1921
+ const signal = this.signalByKey(key);
1922
+ yield {meta, signal, key};
1923
+ }
1924
+ }
1925
+
1926
+ private signalByKey(key: string): SimpleSignal<any> {
1927
+ return (<Record<string, SimpleSignal<any>>>(<unknown>this))[key];
1928
+ }
1929
+
1930
+ private reversedChildren() {
1931
+ const children = this.children();
1932
+ const result: Node[] = [];
1933
+ for (let i = children.length - 1; i >= 0; i--) {
1934
+ result.push(children[i]);
1935
+ }
1936
+ return result;
1937
+ }
1938
+ }
1939
+
1940
+ Node.prototype.isClass = true;