@mirage-engine/core 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module "*?raw" {
2
+ const content: string;
3
+ export default content;
4
+ }
@@ -10,6 +10,8 @@ import {
10
10
  travelerClipArea,
11
11
  } from "../types";
12
12
  import { Painter } from "@mirage-engine/painter";
13
+ import { MeshRegistry } from "../store/MeshRegistry";
14
+ import { TextureLifecycleManager } from "../store/TextureLifecycleManager";
13
15
 
14
16
  export class Renderer {
15
17
  public readonly canvas: HTMLCanvasElement;
@@ -24,17 +26,31 @@ export class Renderer {
24
26
 
25
27
  private target: HTMLElement;
26
28
  private mountContainer: HTMLElement;
29
+ private registry: MeshRegistry;
27
30
  private targetRect: DOMRect;
28
31
 
29
- private meshMap: Map<HTMLElement, THREE.Mesh> = new Map();
32
+ private travelers: Set<THREE.Mesh> = new Set();
33
+ private textureManager: TextureLifecycleManager;
34
+ // private meshMap: Map<HTMLElement, THREE.Mesh> = new Map();
35
+
36
+ public readonly fixedMeshes: Set<THREE.Mesh> = new Set();
30
37
 
31
38
  constructor(
32
39
  target: HTMLElement,
33
40
  config: CoreConfig,
34
41
  mountContainer: HTMLElement,
42
+ registry: MeshRegistry,
35
43
  ) {
36
44
  this.target = target;
37
45
  this.mountContainer = mountContainer;
46
+ this.registry = registry;
47
+
48
+ this.textureManager = new TextureLifecycleManager((el, texture) => {
49
+ const mesh = this.registry.get(el);
50
+ if (mesh && mesh.material instanceof THREE.ShaderMaterial) {
51
+ Painter.forceUpdateUniforms(mesh.material, { texture: texture });
52
+ }
53
+ });
38
54
 
39
55
  this.mode = config.mode ?? "overlay";
40
56
  this.clipArea = config.travelerClipArea ?? 1;
@@ -60,8 +76,6 @@ export class Renderer {
60
76
  this.camera.position.z = 100;
61
77
  this.camera.layers.set(0);
62
78
 
63
- // [new]
64
- // THREE.ColorManagement.enabled = false;
65
79
 
66
80
  this.renderer = new THREE.WebGLRenderer({
67
81
  canvas: this.canvas,
@@ -71,6 +85,11 @@ export class Renderer {
71
85
  // premultipliedAlpha: true
72
86
  });
73
87
 
88
+
89
+ THREE.ColorManagement.enabled = false;
90
+ this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
91
+
92
+
74
93
  this.renderer.setPixelRatio(window.devicePixelRatio);
75
94
  this.renderer.setSize(width, height);
76
95
 
@@ -86,7 +105,7 @@ export class Renderer {
86
105
  format: THREE.RGBAFormat,
87
106
  stencilBuffer: false,
88
107
  depthBuffer: true,
89
- samples: 4,
108
+ // samples: 0.7,
90
109
  },
91
110
  );
92
111
  }
@@ -138,6 +157,7 @@ export class Renderer {
138
157
  public dispose() {
139
158
  this.renderer.dispose();
140
159
  this.canvas.remove();
160
+ this.textureManager.disposeAll();
141
161
  // TODO: Scene 내부 Mesh들도 순회하며 dispose
142
162
  }
143
163
 
@@ -156,7 +176,7 @@ export class Renderer {
156
176
  this.camera.updateProjectionMatrix();
157
177
  }
158
178
 
159
- public syncScene(graphNode: SceneNode) {
179
+ public syncScene(graphNode: SceneNode, pendingDeletions: Set<HTMLElement>) {
160
180
  const newRect = this.target.getBoundingClientRect();
161
181
 
162
182
  const isResized =
@@ -182,28 +202,59 @@ export class Renderer {
182
202
 
183
203
  this.renderOrder = 0;
184
204
 
185
- const activeElements = new Set<HTMLElement>();
186
-
187
- this.reconcileNode(graphNode, activeElements);
188
-
189
- for (const [el, mesh] of this.meshMap.entries()) {
190
- if (!activeElements.has(el)) {
191
- this.scene.remove(mesh);
192
-
193
- mesh.geometry.dispose();
194
- if (mesh.material instanceof THREE.Material) mesh.material.dispose();
195
- this.meshMap.delete(el);
196
- }
205
+ // const activeElements = new Set<HTMLElement>();
206
+
207
+ // this.reconcileNode(graphNode, activeElements);
208
+ this.reconcileNode(graphNode);
209
+
210
+ if (pendingDeletions.size > 0) {
211
+ pendingDeletions.forEach((el) => {
212
+ const meshToDestroy = this.registry.get(el) as THREE.Mesh | undefined;
213
+ if (meshToDestroy) {
214
+ this.scene.remove(meshToDestroy);
215
+ this.travelers.delete(meshToDestroy);
216
+ this.fixedMeshes.delete(meshToDestroy);
217
+ meshToDestroy.geometry.dispose();
218
+ meshToDestroy.traverse((child) => {
219
+ if (child instanceof THREE.Mesh) {
220
+ if (child.geometry) child.geometry.dispose();
221
+ if (child.material) {
222
+ if (Array.isArray(child.material)) {
223
+ child.material.forEach((mat) => mat.dispose());
224
+ } else {
225
+ child.material.dispose();
226
+ }
227
+ }
228
+ }
229
+ });
230
+ this.registry.remove(el);
231
+ this.textureManager.unregister(el);
232
+ }
233
+ });
197
234
  }
235
+
236
+ // mesh.geometry.dispose();
237
+ // if (mesh.material instanceof THREE.Material) mesh.material.dispose();
238
+ // this.meshMap.delete(el);
239
+ // }
240
+ // }
198
241
  }
199
242
 
200
- private reconcileNode(node: SceneNode, activeElements: Set<HTMLElement>) {
201
- activeElements.add(node.element);
243
+ // 탐색 후 완성된 Scene node 이용하여 mesh를 만들거나 조정
244
+ // => 이 과정에서 scene node에 있는 ele를 넣은 hash => activeElements
245
+ // => 이후 activeElements를 이용하여 mesh를 정리!!!+ map에서도 삭제
202
246
 
203
- let mesh = this.meshMap.get(node.element);
247
+ // private reconcileNode(node: SceneNode, activeElements: Set<HTMLElement>) {
248
+ private reconcileNode(node: SceneNode) {
249
+ // activeElements.add(node.element);
250
+
251
+ // let mesh = this.meshMap.get(node.element);
252
+ let mesh = this.registry.get(node.element) as THREE.Mesh | undefined;
204
253
  if (!mesh) {
205
254
  const geometry = new THREE.PlaneGeometry(1, 1);
206
255
  let material: THREE.MeshBasicMaterial | THREE.Material;
256
+ const initialTexture = node.isTraveler ? this.renderTarget?.texture : this.textureManager.get(node.element);
257
+
207
258
  material = Painter.create(
208
259
  "BOX",
209
260
  node.styles,
@@ -211,24 +262,43 @@ export class Renderer {
211
262
  node.rect.width,
212
263
  node.rect.height,
213
264
  this.qualityFactor,
214
- node.isTraveler ? this.renderTarget?.texture : undefined,
215
- node.shaderHooks
265
+ initialTexture,
266
+ node.shaderHooks,
216
267
  );
217
268
  mesh = new THREE.Mesh(geometry, material);
218
269
  if (node.type === "TEXT") mesh.name = "BG_MESH";
219
270
  this.scene.add(mesh);
220
- this.meshMap.set(node.element, mesh);
271
+ this.registry.register(node.element, mesh);
221
272
  }
222
273
 
274
+ // [Important] use whene mesh animating with js
223
275
  mesh.userData.domRect = node.rect;
224
276
 
225
277
  this.updateMeshProperties(mesh, node);
226
278
  this.updateMeshLayers(mesh, node);
227
- if (node.isTraveler) mesh.layers.enable(28);
279
+ if (node.isTraveler) {
280
+ mesh.layers.enable(28);
281
+ this.travelers.add(mesh);
282
+ } else {
283
+ mesh.layers.disable(28);
284
+ this.travelers.delete(mesh);
285
+ }
286
+
287
+ if (node.isFixed) {
288
+ this.fixedMeshes.add(mesh);
289
+ } else {
290
+ this.fixedMeshes.delete(mesh);
291
+ }
292
+
293
+ if (node.styles.imageSrc) {
294
+ this.textureManager.register(node.element, node.styles.imageSrc);
295
+ } else {
296
+ this.textureManager.unregister(node.element);
297
+ }
228
298
 
229
299
  if (node.type === "BOX") {
230
300
  for (const child of node.children) {
231
- this.reconcileNode(child, activeElements);
301
+ this.reconcileNode(child);
232
302
  }
233
303
  } else if (node.type === "TEXT") {
234
304
  this.reconcileTextChild(mesh, node);
@@ -236,55 +306,66 @@ export class Renderer {
236
306
  }
237
307
 
238
308
  private reconcileTextChild(parentMesh: THREE.Mesh, node: SceneNode) {
239
- let textMesh = parentMesh.children.find(
240
- (c) => c.name === "TEXT_CHILD",
241
- ) as THREE.Mesh;
242
-
243
- const currentStyleHash = JSON.stringify(node.textStyles);
244
- const cachedStyleHash = textMesh?.userData?.styleHash;
309
+ const lines = node.textLines || [{ text: node.textContent || "", rect: node.rect }];
310
+ const currentStyleHash = JSON.stringify(node.textStyles) + node.textContent + lines.map(l => l.text).join("|");
311
+ const cachedStyleHash = parentMesh.userData?.textChildStyleHash;
245
312
  const isDirty =
246
- !textMesh ||
247
313
  node.dirtyMask & DIRTY_CONTENT ||
248
314
  currentStyleHash !== cachedStyleHash;
249
315
 
250
316
  if (isDirty) {
251
- if (textMesh) {
317
+ // Remove all existing TEXT_CHILD meshes
318
+ const existingChildren = parentMesh.children.filter(c => c.name.startsWith("TEXT_CHILD"));
319
+ existingChildren.forEach(child => {
320
+ const textMesh = child as THREE.Mesh;
252
321
  (textMesh.material as THREE.MeshBasicMaterial).map?.dispose();
253
322
  textMesh.geometry.dispose();
254
323
  parentMesh.remove(textMesh);
255
- }
256
-
257
- const material = Painter.create(
258
- "TEXT",
259
- node.textStyles!,
260
- node.textContent || "",
261
- node.rect.width,
262
- node.rect.height,
263
- this.qualityFactor,
264
- );
324
+ });
265
325
 
266
- const geometry = new THREE.PlaneGeometry(1, 1);
267
- textMesh = new THREE.Mesh(geometry, material);
268
-
269
- textMesh.name = "TEXT_CHILD";
270
- textMesh.userData = { styleHash: currentStyleHash };
271
- this.updateMeshLayers(textMesh, node);
272
- textMesh.position.z = 0.005;
273
- parentMesh.add(textMesh);
274
- }
275
-
276
- if (textMesh) {
277
- const parentRect = parentMesh.userData.domRect;
326
+ const parentRect = node.rect;
278
327
  const parentCenterX = parentRect.x + parentRect.width / 2;
279
328
  const parentCenterY = parentRect.y + parentRect.height / 2;
280
329
 
281
- const textCenterX = node.rect.x + node.rect.width / 2;
282
- const textCenterY = node.rect.y + node.rect.height / 2;
283
-
284
- const offsetX = textCenterX - parentCenterX;
285
- const offsetY = -(textCenterY - parentCenterY);
286
-
287
- textMesh.position.set(offsetX, offsetY, 0.005);
330
+ lines.forEach((line, index) => {
331
+ const material = Painter.create(
332
+ "TEXT",
333
+ node.textStyles!,
334
+ line.text,
335
+ line.rect.width,
336
+ line.rect.height,
337
+ this.qualityFactor,
338
+ );
339
+
340
+ const geometry = new THREE.PlaneGeometry(1, 1);
341
+ const textMesh = new THREE.Mesh(geometry, material);
342
+
343
+ textMesh.name = `TEXT_CHILD_${index}`;
344
+ this.updateMeshLayers(textMesh, node);
345
+
346
+ // Parent is already scaled to node.rect.width/height.
347
+ // We counter-scale the child so its absolute size is exactly line.rect.width/height.
348
+ const scaleX = node.rect.width === 0 ? 1 : line.rect.width / node.rect.width;
349
+ const scaleY = node.rect.height === 0 ? 1 : line.rect.height / node.rect.height;
350
+ textMesh.scale.set(scaleX, scaleY, 1);
351
+
352
+ const textCenterX = line.rect.x + line.rect.width / 2;
353
+ const textCenterY = line.rect.y + line.rect.height / 2;
354
+
355
+ const offsetX = textCenterX - parentCenterX;
356
+ const offsetY = -(textCenterY - parentCenterY);
357
+
358
+ // Position must also be counter-scaled relative to parent's scale
359
+ textMesh.position.set(
360
+ node.rect.width === 0 ? 0 : offsetX / node.rect.width,
361
+ node.rect.height === 0 ? 0 : offsetY / node.rect.height,
362
+ 0.005
363
+ );
364
+
365
+ parentMesh.add(textMesh);
366
+ });
367
+
368
+ parentMesh.userData.textChildStyleHash = currentStyleHash;
288
369
  }
289
370
  }
290
371
 
@@ -306,11 +387,44 @@ export class Renderer {
306
387
  const localX = rect.x - targetPageX;
307
388
  const localY = rect.y - targetPageY;
308
389
 
390
+ const baseX = localX - canvasWidth / 2 + rect.width / 2;
391
+ const baseY = -localY + canvasHeight / 2 - rect.height / 2;
392
+
309
393
  mesh.position.set(
310
- localX - canvasWidth / 2 + rect.width / 2,
311
- -localY + canvasHeight / 2 - rect.height / 2,
394
+ baseX,
395
+ baseY,
312
396
  styles.zIndex + this.renderOrder * Z_MICRO_OFFSET,
313
397
  );
398
+ const pureDOM_X = rect.x; // 트랜스폼 오염 전 순수 좌표 (가정)
399
+ const pureDOM_Y = rect.y;
400
+
401
+ mesh.userData.basePosition = { x: baseX, y: baseY };
402
+ mesh.userData.originalBasePosition = { x: baseX, y: baseY };
403
+ mesh.userData.baseSize = { width: rect.width, height: rect.height };
404
+ // ✨ 추가: 역산을 위한 순수 DOM 초기 좌표 저장
405
+ mesh.userData.baseDOM = { x: pureDOM_X, y: pureDOM_Y };
406
+ mesh.userData.isFixed = node.isFixed;
407
+ mesh.userData.initialScroll = { x: window.scrollX, y: window.scrollY };
408
+
409
+ // ✨ 추가: 초기 transform 상태를 저장하여 애니메이션 시 delta 값만 적용하도록 함 (이중 적용 방지)
410
+ const targetEl = node.element.nodeType === Node.TEXT_NODE ? node.element.parentElement! : node.element;
411
+ const computed = window.getComputedStyle(targetEl);
412
+ let baseTx = 0, baseTy = 0;
413
+ if (computed.transform && computed.transform !== "none") {
414
+ const matrix = new DOMMatrix(computed.transform);
415
+ baseTx = matrix.m41;
416
+ baseTy = matrix.m42;
417
+ }
418
+ mesh.userData.baseTransform = { x: baseTx, y: baseTy };
419
+
420
+ // ✨ 추가: 애니메이션이 끝나고 씬이 갱신되면 비율 캐시 초기화!
421
+ delete mesh.userData.originRatioX;
422
+ delete mesh.userData.originRatioY;
423
+ // mesh.position.set(
424
+ // localX - canvasWidth / 2 + rect.width / 2,
425
+ // -localY + canvasHeight / 2 - rect.height / 2,
426
+ // styles.zIndex + this.renderOrder * Z_MICRO_OFFSET,
427
+ // );
314
428
  Painter.update(
315
429
  mesh.material as THREE.Material,
316
430
  "BOX",
@@ -319,7 +433,7 @@ export class Renderer {
319
433
  node.rect.width,
320
434
  node.rect.height,
321
435
  this.qualityFactor,
322
- node.isTraveler ? this.renderTarget?.texture : undefined,
436
+ node.isTraveler ? this.renderTarget?.texture : this.textureManager.get(node.element),
323
437
  );
324
438
  }
325
439
 
@@ -330,13 +444,11 @@ export class Renderer {
330
444
  }
331
445
 
332
446
  private captureRenderTarget() {
333
- const travelers: THREE.Mesh[] = [];
334
- for (const mesh of this.meshMap.values()) {
335
- if ((mesh.layers.mask & (1 << 28)) !== 0) {
336
- travelers.push(mesh);
337
- }
338
- }
339
- if (travelers.length === 0) return;
447
+ // [Problem] this method called on requestAnimationFrame
448
+ // => this logic travers all meshes every frame, need optimization
449
+
450
+ // if (travelers.length === 0) return;
451
+ if (this.travelers.size === 0) return;
340
452
 
341
453
  const oldClearColor = new THREE.Color();
342
454
  const oldClearAlpha = this.renderer.getClearAlpha();
@@ -357,7 +469,7 @@ export class Renderer {
357
469
 
358
470
  const pixelRatio = this.renderer.getPixelRatio();
359
471
 
360
- for (const traveler of travelers) {
472
+ for (const traveler of this.travelers) {
361
473
  vector.setFromMatrixPosition(traveler.matrixWorld);
362
474
  vector.project(this.camera);
363
475
 
@@ -392,7 +504,7 @@ export class Renderer {
392
504
  this.renderer.setScissorTest(false);
393
505
  this.renderer.autoClear = true;
394
506
  this.renderer.setRenderTarget(null);
395
- this.camera.layers.set(28);
507
+ this.camera.layers.set(0);
396
508
  this.renderer.setClearColor(oldClearColor, oldClearAlpha);
397
509
  }
398
510
 
@@ -0,0 +1,32 @@
1
+ import * as THREE from "three";
2
+
3
+ export class MeshRegistry {
4
+ private store: WeakMap<HTMLElement, THREE.Mesh>;
5
+
6
+ constructor() {
7
+ this.store = new WeakMap();
8
+ }
9
+ public register(element: HTMLElement, mesh: THREE.Mesh): void {
10
+ this.store.set(element, mesh);
11
+ }
12
+
13
+ public get(element: HTMLElement): THREE.Mesh | undefined {
14
+ return this.store.get(element);
15
+ }
16
+ // public register(element: HTMLElement, meshGroup: THREE.Group): void {
17
+ // this.store.set(element, meshGroup);
18
+ // }
19
+
20
+ // public get(element: HTMLElement): THREE.Group | undefined {
21
+ // return this.store.get(element);
22
+ // }
23
+
24
+ public has(element: HTMLElement): boolean {
25
+ return this.store.has(element);
26
+ }
27
+
28
+ // Optional
29
+ public remove(element: HTMLElement): void {
30
+ this.store.delete(element);
31
+ }
32
+ }
@@ -0,0 +1,111 @@
1
+ import * as THREE from "three";
2
+
3
+ export type TextureReadyCallback = (
4
+ element: HTMLElement,
5
+ texture: THREE.Texture | null,
6
+ ) => void;
7
+
8
+ export class TextureLifecycleManager {
9
+ private observer: IntersectionObserver;
10
+ private textures: WeakMap<HTMLElement, THREE.Texture> = new WeakMap();
11
+ private loadStatus: WeakMap<HTMLElement, boolean> = new WeakMap();
12
+ private elementUrls: WeakMap<HTMLElement, string> = new WeakMap();
13
+ private onUpdate: TextureReadyCallback;
14
+
15
+ constructor(onUpdate: TextureReadyCallback) {
16
+ this.onUpdate = onUpdate;
17
+ this.observer = new IntersectionObserver(
18
+ (entries) => {
19
+ for (const entry of entries) {
20
+ const el = entry.target as HTMLElement;
21
+ if (entry.isIntersecting) {
22
+ this.loadTexture(el);
23
+ } else {
24
+ this.disposeTexture(el);
25
+ }
26
+ }
27
+ },
28
+ { rootMargin: "300px" },
29
+ );
30
+ }
31
+
32
+ public register(element: HTMLElement, url: string) {
33
+ if (element.nodeType !== Node.ELEMENT_NODE) return;
34
+ const currentUrl = this.elementUrls.get(element);
35
+ if (currentUrl !== url) {
36
+ this.elementUrls.set(element, url);
37
+ // Restart observation to trigger intersecting check
38
+ this.observer.unobserve(element);
39
+ this.observer.observe(element);
40
+ }
41
+ }
42
+
43
+ public unregister(element: HTMLElement) {
44
+ if (element.nodeType === Node.ELEMENT_NODE) {
45
+ this.observer.unobserve(element);
46
+ }
47
+ this.disposeTexture(element);
48
+ this.elementUrls.delete(element);
49
+ this.loadStatus.delete(element);
50
+ }
51
+
52
+ private async loadTexture(element: HTMLElement) {
53
+ if (this.loadStatus.get(element) || this.textures.has(element)) return;
54
+
55
+ const url = this.elementUrls.get(element);
56
+ if (!url) return;
57
+
58
+ this.loadStatus.set(element, true);
59
+
60
+ try {
61
+ const response = await fetch(url);
62
+ const blob = await response.blob();
63
+ const bitmap = await createImageBitmap(blob, { imageOrientation: "flipY" });
64
+
65
+ // Check if URL changed or element was unregistered while decoding
66
+ if (this.elementUrls.get(element) !== url) {
67
+ bitmap.close();
68
+ return;
69
+ }
70
+
71
+ // Check if element is no longer intersecting (debounced unobserve might have fired)
72
+ // Actually we just check if it's still observed and has the URL.
73
+ // A more robust check for intersection state might be needed, but for now we rely on disposeTexture.
74
+
75
+ const texture = new THREE.CanvasTexture(bitmap);
76
+ texture.needsUpdate = true;
77
+ this.textures.set(element, texture);
78
+ this.onUpdate(element, texture);
79
+ } catch (e) {
80
+ console.warn("[MirageEngine] Failed to load texture:", url, e);
81
+ } finally {
82
+ // Re-check url, if it changed, we should probably trigger loadTexture again,
83
+ // but IntersectionObserver will handle it if it's still intersecting.
84
+ if (this.elementUrls.has(element)) {
85
+ this.loadStatus.set(element, false);
86
+ }
87
+ }
88
+ }
89
+
90
+ private disposeTexture(element: HTMLElement) {
91
+ const texture = this.textures.get(element);
92
+ if (texture) {
93
+ texture.dispose();
94
+ this.textures.delete(element);
95
+ this.onUpdate(element, null);
96
+ }
97
+ // Also cancel loading state if we had one
98
+ this.loadStatus.set(element, false);
99
+ }
100
+
101
+ public get(element: HTMLElement) {
102
+ return this.textures.get(element);
103
+ }
104
+
105
+ public disposeAll() {
106
+ this.observer.disconnect();
107
+ // Assuming WeakMap handles GC of remaining textures, but in WebGL we must explicitly delete.
108
+ // However, WeakMap is not iterable, so we can't easily iterate and delete.
109
+ // The renderer should unregister nodes upon pendingDeletions.
110
+ }
111
+ }
@@ -0,0 +1,26 @@
1
+ export interface StyleData {
2
+ // 1. Transform
3
+ x?: number;
4
+ y?: number;
5
+ z?: number;
6
+ scaleX?: number;
7
+ scaleY?: number;
8
+ scaleZ?: number;
9
+ rotateX?: number;
10
+ rotateY?: number;
11
+ rotateZ?: number;
12
+
13
+ // for future use
14
+ matrix?: Float32Array;
15
+
16
+ // 2. Material / Appearance
17
+ opacity?: number;
18
+ backgroundColor?: [number, number, number];
19
+ backgroundImage?: string;
20
+ borderRadius?: number | [number, number, number, number];
21
+
22
+ // 3. Layout / Depth
23
+ width?: number;
24
+ height?: number;
25
+ zIndex?: number;
26
+ }
@@ -20,12 +20,14 @@ export interface SceneNode {
20
20
 
21
21
  textContent?: string;
22
22
  textStyles?: TextStyles;
23
+ textLines?: { text: string; rect: NodeRect }[];
23
24
 
24
25
  dirtyMask: number;
25
26
  // TODO: SceneNode의 sort 로직 추가(아마도)
26
27
 
27
28
  visibility: Visibility;
28
29
  isTraveler: boolean;
30
+ isFixed: boolean;
29
31
  shaderHooks?: ShaderHooks;
30
32
 
31
33
  children: SceneNode[];
@@ -1,3 +1,4 @@
1
1
  export * from './config';
2
2
  export * from './common';
3
- export * from './flags';
3
+ export * from './flags';
4
+ export * from './animate';