@mirage-engine/core 0.2.0 → 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.
@@ -0,0 +1,163 @@
1
+ import { StyleData } from "../types";
2
+ import { MeshRegistry } from "../store/MeshRegistry";
3
+ import { Painter, parseColor } from "@mirage-engine/painter";
4
+ import * as THREE from "three";
5
+
6
+ export function animateMeshByData(
7
+ registry: MeshRegistry,
8
+ data: Map<HTMLElement, StyleData>,
9
+ ) {
10
+ if (data.size === 0) return;
11
+
12
+ data.forEach((styleData, element) => {
13
+ const mesh = registry.get(element);
14
+ if (!mesh || !mesh.userData.basePosition) return;
15
+
16
+ let { x: baseX, y: baseY } = mesh.userData.basePosition;
17
+ const { width: baseW, height: baseH } = mesh.userData.baseSize;
18
+
19
+ const currentW = styleData.width ?? baseW;
20
+ const currentH = styleData.height ?? baseH;
21
+ const widthDiff = currentW - baseW;
22
+ const heightDiff = currentH - baseH;
23
+
24
+ // --- Capture and Lock Transform Origin (Performance & Accuracy) ---
25
+ // We capture the origin on the very first frame of change (> 0.1px)
26
+ // to ensure perfect sync from the start of the first animation.
27
+ if (
28
+ styleData.width !== undefined &&
29
+ mesh.userData.originRatioX === undefined &&
30
+ Math.abs(widthDiff) > 0.1
31
+ ) {
32
+ const rect = element.getBoundingClientRect();
33
+ const tx = styleData.x ?? 0;
34
+ const baseTx = mesh.userData.baseTransform?.x ?? 0;
35
+ const scrollOffsetX = mesh.userData.isFixed ? 0 : window.scrollX;
36
+ // rect.left already includes GSAP transform (tx).
37
+ // We subtract tx to strip the animation, then add baseTx to match baseDOM's initial state.
38
+ const currentPageX = rect.left + scrollOffsetX - tx + baseTx;
39
+ const deltaX = currentPageX - mesh.userData.baseDOM.x;
40
+
41
+ let ratioX = -deltaX / widthDiff;
42
+ if (Math.abs(ratioX) < 0.05) ratioX = 0.0;
43
+ else if (Math.abs(ratioX - 1.0) < 0.05) ratioX = 1.0;
44
+ else if (Math.abs(ratioX - 0.5) < 0.05) ratioX = 0.5;
45
+
46
+ mesh.userData.originRatioX = ratioX;
47
+ }
48
+
49
+ if (
50
+ styleData.height !== undefined &&
51
+ mesh.userData.originRatioY === undefined &&
52
+ Math.abs(heightDiff) > 0.1
53
+ ) {
54
+ const rect = element.getBoundingClientRect();
55
+ const ty = styleData.y ?? 0;
56
+ const baseTy = mesh.userData.baseTransform?.y ?? 0;
57
+ const scrollOffsetY = mesh.userData.isFixed ? 0 : window.scrollY;
58
+ const currentPageY = rect.top + scrollOffsetY - ty + baseTy;
59
+ const deltaY = currentPageY - mesh.userData.baseDOM.y;
60
+
61
+ let ratioY = -deltaY / heightDiff;
62
+ if (Math.abs(ratioY) < 0.05) ratioY = 0.0;
63
+ else if (Math.abs(ratioY - 1.0) < 0.05) ratioY = 1.0;
64
+ else if (Math.abs(ratioY - 0.5) < 0.05) ratioY = 0.5;
65
+
66
+ mesh.userData.originRatioY = ratioY;
67
+ }
68
+
69
+ // --- Calculate Mesh Position using Measured Origin ---
70
+ const ratioX = mesh.userData.originRatioX ?? 0.5;
71
+ const ratioY = mesh.userData.originRatioY ?? 0.5;
72
+
73
+ // Since Three.js meshes expand from center, we offset the center based on the locked origin ratio
74
+ const moveX = widthDiff * (0.5 - ratioX);
75
+ const moveY = heightDiff * (0.5 - ratioY);
76
+
77
+ // --- Calculate Delta Movement ---
78
+ // Extract baseTransform stored during syncScene
79
+ const baseTx = mesh.userData.baseTransform?.x ?? 0;
80
+ const baseTy = mesh.userData.baseTransform?.y ?? 0;
81
+
82
+ // We only want to apply the DIFFERENCE in transform since the base extraction.
83
+ const deltaTx = (styleData.x ?? baseTx) - baseTx;
84
+ const deltaTy = (styleData.y ?? baseTy) - baseTy;
85
+
86
+ mesh.position.setX(baseX + moveX + deltaTx);
87
+ mesh.position.setY(baseY - (moveY + deltaTy));
88
+ mesh.scale.set(currentW, currentH, 1);
89
+
90
+ Painter.forceUpdateUniforms(mesh.material as THREE.ShaderMaterial, {
91
+ backgroundColor: styleData.backgroundColor,
92
+ backgroundImage: styleData.backgroundImage,
93
+ opacity: styleData.opacity,
94
+ borderRadius: styleData.borderRadius ?? mesh.userData.baseStyles?.borderRadius,
95
+ width: currentW,
96
+ height: currentH,
97
+ });
98
+ });
99
+ }
100
+ export function animateMeshByAttribute(
101
+ _target: HTMLElement,
102
+ _options: { duration: number; easing?: string },
103
+ ) {}
104
+
105
+ export function updateFixedMeshesScroll(
106
+ fixedMeshes: Set<THREE.Mesh>,
107
+ currentScrollX: number,
108
+ currentScrollY: number,
109
+ ) {
110
+ fixedMeshes.forEach((mesh) => {
111
+ if (mesh.userData.isFixed && mesh.userData.initialScroll && mesh.userData.originalBasePosition) {
112
+ const offsetX = currentScrollX - mesh.userData.initialScroll.x;
113
+ const offsetY = currentScrollY - mesh.userData.initialScroll.y;
114
+
115
+ mesh.userData.basePosition.x = mesh.userData.originalBasePosition.x + offsetX;
116
+ mesh.userData.basePosition.y = mesh.userData.originalBasePosition.y - offsetY;
117
+
118
+ mesh.position.x = mesh.userData.basePosition.x;
119
+ mesh.position.y = mesh.userData.basePosition.y;
120
+ }
121
+ });
122
+ }
123
+
124
+ export function extractFromStyle(style: CSSStyleDeclaration): StyleData {
125
+ const styleObject: StyleData = {};
126
+
127
+ if (style.opacity) {
128
+ styleObject.opacity = parseFloat(style.opacity);
129
+ }
130
+
131
+ if (style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)") {
132
+ const parsed = parseColor(style.backgroundColor);
133
+ styleObject.backgroundColor = [parsed.color.r, parsed.color.g, parsed.color.b];
134
+ }
135
+
136
+ if (style.backgroundImage) {
137
+ styleObject.backgroundImage = style.backgroundImage;
138
+ } else if (style.background) {
139
+ styleObject.backgroundImage = style.background;
140
+ }
141
+
142
+ if (style.borderRadius) {
143
+ styleObject.borderRadius = parseFloat(style.borderRadius);
144
+ }
145
+ if (style.width) {
146
+ styleObject.width = parseFloat(style.width);
147
+ }
148
+ if (style.height) {
149
+ styleObject.height = parseFloat(style.height);
150
+ }
151
+ if (style.transform && style.transform !== "none") {
152
+ const matrix = new DOMMatrix(style.transform);
153
+
154
+ styleObject.x = matrix.m41;
155
+ styleObject.y = matrix.m42;
156
+ styleObject.z = matrix.m43;
157
+
158
+ // [TODO] direct matrix data pipeline to webGL
159
+ // styleObject.matrix = matrix.toFloat32Array();
160
+ }
161
+
162
+ return styleObject;
163
+ }
@@ -1,14 +1,28 @@
1
1
  import { CoreConfig } from "../types";
2
2
  import { Renderer } from "../renderer/Renderer";
3
3
  import { Syncer } from "./Syncer";
4
+ import { MeshRegistry } from "../store/MeshRegistry";
4
5
 
5
6
  export class Engine {
6
7
  private renderer: Renderer;
7
8
  private syncer: Syncer;
8
9
  private target: HTMLElement;
10
+ private registry: MeshRegistry;
9
11
 
10
12
  constructor(target: HTMLElement, config: CoreConfig) {
11
13
  this.target = target;
14
+ this.registry = new MeshRegistry();
15
+
16
+ if (!document.getElementById("mirage-engine-styles")) {
17
+ const style = document.createElement("style");
18
+ style.id = "mirage-engine-styles";
19
+ style.textContent = `
20
+ [data-mirage-dom="hide"] {
21
+ opacity: 0 !important;
22
+ }
23
+ `;
24
+ document.head.appendChild(style);
25
+ }
12
26
 
13
27
  let mountContainer: HTMLElement | undefined;
14
28
 
@@ -23,11 +37,14 @@ export class Engine {
23
37
  throw new Error("[Mirage] Cannot find a container (parent or option).");
24
38
  }
25
39
 
26
- this.renderer = new Renderer(this.target, config, mountContainer);
27
-
40
+ this.renderer = new Renderer(
41
+ this.target,
42
+ config,
43
+ mountContainer,
44
+ this.registry,
45
+ );
28
46
  this.renderer.mount();
29
-
30
- this.syncer = new Syncer(this.target, this.renderer, config);
47
+ this.syncer = new Syncer(this.target, this.renderer, this.registry, config);
31
48
  }
32
49
 
33
50
  public start() {
@@ -41,4 +58,39 @@ export class Engine {
41
58
  this.syncer.stop();
42
59
  this.renderer.dispose();
43
60
  }
61
+
62
+ public test() {
63
+ const boxMesh = this.registry.get(
64
+ document.querySelector("#box2") as HTMLElement,
65
+ );
66
+
67
+ if (!boxMesh) return;
68
+
69
+ const keys: { [key: string]: boolean } = {
70
+ ArrowRight: false,
71
+ ArrowLeft: false,
72
+ ArrowUp: false,
73
+ ArrowDown: false,
74
+ };
75
+
76
+ window.addEventListener("keydown", (e) => {
77
+ if (keys[e.key] !== undefined) keys[e.key] = true;
78
+ });
79
+ window.addEventListener("keyup", (e) => {
80
+ if (keys[e.key] !== undefined) keys[e.key] = false;
81
+ });
82
+
83
+ const moveStep = 2;
84
+
85
+ const animate = () => {
86
+ requestAnimationFrame(animate);
87
+
88
+ if (keys.ArrowRight) boxMesh.position.setX(boxMesh.position.x + moveStep);
89
+ if (keys.ArrowLeft) boxMesh.position.setX(boxMesh.position.x - moveStep);
90
+ if (keys.ArrowUp) boxMesh.position.setY(boxMesh.position.y + moveStep);
91
+ if (keys.ArrowDown) boxMesh.position.setY(boxMesh.position.y - moveStep);
92
+ };
93
+
94
+ animate();
95
+ }
44
96
  }
@@ -1,15 +1,20 @@
1
1
  import { CoreConfig } from "../types/config";
2
2
  import { Renderer } from "../renderer/Renderer";
3
+ import { MeshRegistry } from "../store/MeshRegistry";
3
4
  import { extractSceneGraph } from "../dom/Extractor";
4
5
  import {
5
6
  DIRTY_NONE,
6
7
  DIRTY_RECT,
7
8
  DIRTY_STRUCTURE,
9
+ DIRTY_CONTENT
10
+ ,
8
11
  DIRTY_STYLE,
9
12
  USER_LAYER,
10
13
  SYSTEM_LAYER,
11
14
  Visibility,
15
+ StyleData,
12
16
  } from "../types";
17
+ import { extractFromStyle, animateMeshByData, updateFixedMeshesScroll } from "../animation/Animator";
13
18
 
14
19
  interface InternalResizeConfig {
15
20
  enabled: boolean;
@@ -21,9 +26,12 @@ interface InternalResizeConfig {
21
26
  export class Syncer {
22
27
  private target: HTMLElement;
23
28
  private renderer: Renderer;
29
+ private registry: MeshRegistry;
24
30
  private filter: CoreConfig["filter"];
25
31
 
26
32
  private observer: MutationObserver;
33
+ private pendingDeletions: Set<HTMLElement> = new Set();
34
+ private pendingStyles: Map<HTMLElement, StyleData> = new Map();
27
35
 
28
36
  private isDomDirty: boolean = false;
29
37
  private isRunning: boolean = false;
@@ -38,9 +46,19 @@ export class Syncer {
38
46
  private resizeTimer: number | null = null;
39
47
  private isResizing: boolean = false;
40
48
 
41
- constructor(target: HTMLElement, renderer: Renderer, config: CoreConfig) {
49
+ private lastScrollX: number = 0;
50
+ private lastScrollY: number = 0;
51
+ private scrollTimer: number | null = null;
52
+
53
+ constructor(
54
+ target: HTMLElement,
55
+ renderer: Renderer,
56
+ registry: MeshRegistry,
57
+ config: CoreConfig,
58
+ ) {
42
59
  this.target = target;
43
60
  this.renderer = renderer;
61
+ this.registry = registry;
44
62
  this.filter = config.filter;
45
63
 
46
64
  // Resize Debounce Configuration
@@ -64,13 +82,28 @@ export class Syncer {
64
82
  for (const mutation of mutations) {
65
83
  if (mutation.type === "childList") {
66
84
  currentMask |= DIRTY_STRUCTURE;
85
+ if (mutation.removedNodes.length > 0) {
86
+ mutation.removedNodes.forEach((node) => {
87
+ if (node instanceof HTMLElement) {
88
+ this.pendingDeletions.add(node);
89
+ }
90
+ });
91
+ }
67
92
  } else if (mutation.type === "attributes") {
68
- if (
69
- mutation.attributeName === "style" ||
70
- mutation.attributeName === "class"
71
- ) {
93
+ if (mutation.attributeName === "style") {
94
+ currentMask |= DIRTY_RECT | DIRTY_STYLE;
95
+ const target = mutation.target as HTMLElement;
96
+
97
+ const extractedStyleData = extractFromStyle(target.style);
98
+ // console.log(extractedStyleData);
99
+ this.pendingStyles.set(target, extractedStyleData);
100
+ } else if (mutation.attributeName === "class") {
72
101
  currentMask |= DIRTY_RECT | DIRTY_STYLE;
73
102
  }
103
+ // else if (mutation.attributeName === "data-mirage") {
104
+ // }
105
+ } else if (mutation.type === "characterData") {
106
+ currentMask |= DIRTY_CONTENT | DIRTY_RECT;
74
107
  }
75
108
  }
76
109
 
@@ -80,7 +113,6 @@ export class Syncer {
80
113
  // Structural Change detected
81
114
  if (currentMask & DIRTY_STRUCTURE) {
82
115
  this.clearTimers();
83
- console.log("Structural Change detected");
84
116
  this.isDomDirty = true;
85
117
  return;
86
118
  }
@@ -109,7 +141,6 @@ export class Syncer {
109
141
 
110
142
  this.target.addEventListener("transitionend", this.onTransitionFinished);
111
143
  this.target.addEventListener("animationend", this.onTransitionFinished);
112
-
113
144
  window.addEventListener("resize", this.onWindowResize);
114
145
 
115
146
  this.forceUpdateScene();
@@ -138,17 +169,18 @@ export class Syncer {
138
169
  clearTimeout(this.cssTimer);
139
170
  this.cssTimer = null;
140
171
  }
172
+ if (this.scrollTimer) {
173
+ clearTimeout(this.scrollTimer);
174
+ this.scrollTimer = null;
175
+ }
141
176
  }
142
177
 
143
178
  private onTransitionFinished = (e: Event) => {
144
179
  if (!this.target.contains(e.target as Node)) return;
145
-
146
180
  if (this.mutationTimer !== null) return;
147
-
148
181
  if (this.cssTimer) clearTimeout(this.cssTimer);
149
-
182
+ if (this.pendingStyles.size != 0) return;
150
183
  this.pendingMask |= DIRTY_RECT | DIRTY_STYLE;
151
-
152
184
  this.cssTimer = window.setTimeout(() => {
153
185
  this.isDomDirty = true;
154
186
  this.cssTimer = null;
@@ -160,17 +192,13 @@ export class Syncer {
160
192
  this.isDomDirty = true;
161
193
  return;
162
194
  }
163
-
164
195
  if (!this.isResizing) {
165
196
  this.isResizing = true;
166
197
  if (this.resizeConfig.onStart) this.resizeConfig.onStart();
167
198
  }
168
-
169
199
  if (this.resizeTimer) clearTimeout(this.resizeTimer);
170
-
171
200
  this.resizeTimer = window.setTimeout(() => {
172
201
  this.isDomDirty = true;
173
-
174
202
  if (this.resizeConfig.onEnd) this.resizeConfig.onEnd();
175
203
  this.isResizing = false;
176
204
  this.resizeTimer = null;
@@ -179,6 +207,9 @@ export class Syncer {
179
207
 
180
208
  private forceUpdateScene() {
181
209
  this.isDomDirty = false;
210
+
211
+ this.lastScrollX = window.scrollX;
212
+ this.lastScrollY = window.scrollY;
182
213
 
183
214
  const discoveredTraveler =
184
215
  document.querySelector("[data-mirage-travel='traveler']") !== null;
@@ -197,7 +228,8 @@ export class Syncer {
197
228
  );
198
229
 
199
230
  if (sceneGraph) {
200
- this.renderer.syncScene(sceneGraph);
231
+ this.renderer.syncScene(sceneGraph, this.pendingDeletions);
232
+ this.pendingDeletions.clear();
201
233
  }
202
234
 
203
235
  this.pendingMask = DIRTY_NONE;
@@ -210,7 +242,30 @@ export class Syncer {
210
242
  this.forceUpdateScene();
211
243
  }
212
244
 
245
+ const currentScrollX = window.scrollX;
246
+ const currentScrollY = window.scrollY;
247
+ if (currentScrollX !== this.lastScrollX || currentScrollY !== this.lastScrollY) {
248
+ updateFixedMeshesScroll(this.renderer.fixedMeshes, currentScrollX, currentScrollY);
249
+
250
+ this.lastScrollX = currentScrollX;
251
+ this.lastScrollY = currentScrollY;
252
+
253
+ if (this.scrollTimer) clearTimeout(this.scrollTimer);
254
+ this.scrollTimer = window.setTimeout(() => {
255
+ this.pendingMask |= DIRTY_RECT;
256
+ this.isDomDirty = true;
257
+ this.scrollTimer = null;
258
+ }, 150);
259
+ }
260
+
261
+ if (this.pendingStyles.size > 0) {
262
+ animateMeshByData(this.registry, this.pendingStyles);
263
+ this.pendingStyles.clear();
264
+ }
265
+
213
266
  this.renderer.render();
214
267
  requestAnimationFrame(this.renderLoop);
215
268
  };
269
+
270
+ // [ThinkPoint] change call back pattern when after
216
271
  }