@mirage-engine/core 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,9 @@
1
1
  export type Quality = "low" | "medium" | "high" | number;
2
+ export type LayerTarget = "base" | "selected";
2
3
  export type MirageMode = "overlay" | "duplicate";
3
4
  export type PxUnit = `${number}px`;
4
5
  export type PercentUnit = `${number}%`;
5
6
  export type travelerClipArea = PxUnit | PercentUnit | number;
6
- export interface FilterConfig {
7
- includeTree?: string[];
8
- excludeTree?: string[];
9
- includeSelf?: string[];
10
- excludeSelf?: string[];
11
- end?: string[];
12
- }
13
7
  export interface ResizeConfig {
14
8
  delay?: number;
15
9
  onStart?: () => void;
@@ -17,11 +11,11 @@ export interface ResizeConfig {
17
11
  }
18
12
  interface BaseConfig {
19
13
  debug?: boolean;
14
+ layer?: number | LayerTarget;
20
15
  quality?: Quality;
21
16
  style?: {
22
17
  zIndex?: string;
23
18
  };
24
- filter?: FilterConfig;
25
19
  resizeDebounce?: boolean | ResizeConfig;
26
20
  travelerClipArea?: travelerClipArea;
27
21
  }
@@ -1,11 +1,12 @@
1
- export declare const DIRTY_NONE = 0;
2
- export declare const DIRTY_RECT: number;
3
- export declare const DIRTY_STYLE: number;
4
- export declare const DIRTY_ZINDEX: number;
5
- export declare const DIRTY_STRUCTURE: number;
6
- export declare const DIRTY_CONTENT: number;
1
+ export { DIRTY_NONE, DIRTY_RECT, DIRTY_STYLE, DIRTY_ZINDEX, DIRTY_STRUCTURE, DIRTY_CONTENT, } from '@mirage-engine/dom-tracker';
7
2
  export declare const USER_LAYER: number;
8
- export declare const SYSTEM_LAYER: number;
3
+ export declare const SELECT_LAYER: number;
9
4
  export declare const EXCLUDED = 0;
5
+ export declare const THREE_LAYERS: {
6
+ readonly BASE: 0;
7
+ readonly SELECTED: 1;
8
+ readonly getCaptureLayer: (layerNum: number) => number;
9
+ readonly HIDDEN: 31;
10
+ };
10
11
  export type Visibility = 0 | 1 | 2 | 3;
11
12
  export declare const ALLOWED_FILTERS: string[];
@@ -1,3 +1,5 @@
1
1
  export * from './config';
2
2
  export * from './common';
3
+ export * from './attributes';
3
4
  export * from './flags';
5
+ export * from '@mirage-engine/dom-tracker';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirage-engine/core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "peerDependencies": {
@@ -13,7 +13,8 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@types/three": "^0.181.0",
16
- "@mirage-engine/painter": "0.4.0"
16
+ "@mirage-engine/painter": "1.0.1",
17
+ "@mirage-engine/dom-tracker": "0.3.0"
17
18
  },
18
19
  "private": false,
19
20
  "publishConfig": {
@@ -0,0 +1,108 @@
1
+ import { StyleData } from "@mirage-engine/dom-tracker";
2
+ import { MeshRegistry } from "../store/MeshRegistry";
3
+ import { Painter } 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
+ if (
25
+ styleData.width !== undefined &&
26
+ mesh.userData.originRatioX === undefined &&
27
+ Math.abs(widthDiff) > 0.1
28
+ ) {
29
+ const rect = element.getBoundingClientRect();
30
+ const tx = styleData.x ?? 0;
31
+ const baseTx = mesh.userData.baseTransform?.x ?? 0;
32
+ const scrollOffsetX = mesh.userData.isFixed ? 0 : window.scrollX;
33
+ const currentPageX = rect.left + scrollOffsetX - tx + baseTx;
34
+ const deltaX = currentPageX - mesh.userData.baseDOM.x;
35
+
36
+ let ratioX = -deltaX / widthDiff;
37
+ if (Math.abs(ratioX) < 0.05) ratioX = 0.0;
38
+ else if (Math.abs(ratioX - 1.0) < 0.05) ratioX = 1.0;
39
+ else if (Math.abs(ratioX - 0.5) < 0.05) ratioX = 0.5;
40
+
41
+ mesh.userData.originRatioX = ratioX;
42
+ }
43
+
44
+ if (
45
+ styleData.height !== undefined &&
46
+ mesh.userData.originRatioY === undefined &&
47
+ Math.abs(heightDiff) > 0.1
48
+ ) {
49
+ const rect = element.getBoundingClientRect();
50
+ const ty = styleData.y ?? 0;
51
+ const baseTy = mesh.userData.baseTransform?.y ?? 0;
52
+ const scrollOffsetY = mesh.userData.isFixed ? 0 : window.scrollY;
53
+ const currentPageY = rect.top + scrollOffsetY - ty + baseTy;
54
+ const deltaY = currentPageY - mesh.userData.baseDOM.y;
55
+
56
+ let ratioY = -deltaY / heightDiff;
57
+ if (Math.abs(ratioY) < 0.05) ratioY = 0.0;
58
+ else if (Math.abs(ratioY - 1.0) < 0.05) ratioY = 1.0;
59
+ else if (Math.abs(ratioY - 0.5) < 0.05) ratioY = 0.5;
60
+
61
+ mesh.userData.originRatioY = ratioY;
62
+ }
63
+
64
+ const ratioX = mesh.userData.originRatioX ?? 0.5;
65
+ const ratioY = mesh.userData.originRatioY ?? 0.5;
66
+
67
+ const moveX = widthDiff * (0.5 - ratioX);
68
+ const moveY = heightDiff * (0.5 - ratioY);
69
+
70
+ const baseTx = mesh.userData.baseTransform?.x ?? 0;
71
+ const baseTy = mesh.userData.baseTransform?.y ?? 0;
72
+
73
+ const deltaTx = (styleData.x ?? baseTx) - baseTx;
74
+ const deltaTy = (styleData.y ?? baseTy) - baseTy;
75
+
76
+ mesh.position.setX(baseX + moveX + deltaTx);
77
+ mesh.position.setY(baseY - (moveY + deltaTy));
78
+ mesh.scale.set(currentW, currentH, 1);
79
+
80
+ Painter.forceUpdateUniforms(mesh.material as THREE.ShaderMaterial, {
81
+ backgroundColor: styleData.backgroundColor,
82
+ backgroundImage: styleData.backgroundImage,
83
+ opacity: styleData.opacity,
84
+ borderRadius: styleData.borderRadius ?? mesh.userData.baseStyles?.borderRadius,
85
+ width: currentW,
86
+ height: currentH,
87
+ });
88
+ });
89
+ }
90
+
91
+ export function updateFixedMeshesScroll(
92
+ fixedMeshes: Set<THREE.Mesh>,
93
+ currentScrollX: number,
94
+ currentScrollY: number,
95
+ ) {
96
+ fixedMeshes.forEach((mesh) => {
97
+ if (mesh.userData.isFixed && mesh.userData.initialScroll && mesh.userData.originalBasePosition) {
98
+ const offsetX = currentScrollX - mesh.userData.initialScroll.x;
99
+ const offsetY = currentScrollY - mesh.userData.initialScroll.y;
100
+
101
+ mesh.userData.basePosition.x = mesh.userData.originalBasePosition.x + offsetX;
102
+ mesh.userData.basePosition.y = mesh.userData.originalBasePosition.y - offsetY;
103
+
104
+ mesh.position.x = mesh.userData.basePosition.x;
105
+ mesh.position.y = mesh.userData.basePosition.y;
106
+ }
107
+ });
108
+ }
@@ -1,14 +1,28 @@
1
- import { CoreConfig } from "../types";
1
+ import { CoreConfig, ATTR_DOM } 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
+ [${ATTR_DOM.NAME}="${ATTR_DOM.VALUES.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,51 @@ export class Engine {
41
58
  this.syncer.stop();
42
59
  this.renderer.dispose();
43
60
  }
61
+
62
+ public getTracker() {
63
+ return this.syncer.tracker;
64
+ }
65
+
66
+ public updateUniforms(element: HTMLElement, uniforms: Record<string, any>) {
67
+ this.renderer.updateUniforms(element, uniforms);
68
+ }
69
+
70
+ public getCanvas() {
71
+ return this.renderer.canvas;
72
+ }
73
+
74
+ public test() {
75
+ const boxMesh = this.registry.get(
76
+ document.querySelector("#box2") as HTMLElement,
77
+ );
78
+
79
+ if (!boxMesh) return;
80
+
81
+ const keys: { [key: string]: boolean } = {
82
+ ArrowRight: false,
83
+ ArrowLeft: false,
84
+ ArrowUp: false,
85
+ ArrowDown: false,
86
+ };
87
+
88
+ window.addEventListener("keydown", (e) => {
89
+ if (keys[e.key] !== undefined) keys[e.key] = true;
90
+ });
91
+ window.addEventListener("keyup", (e) => {
92
+ if (keys[e.key] !== undefined) keys[e.key] = false;
93
+ });
94
+
95
+ const moveStep = 2;
96
+
97
+ const animate = () => {
98
+ requestAnimationFrame(animate);
99
+
100
+ if (keys.ArrowRight) boxMesh.position.setX(boxMesh.position.x + moveStep);
101
+ if (keys.ArrowLeft) boxMesh.position.setX(boxMesh.position.x - moveStep);
102
+ if (keys.ArrowUp) boxMesh.position.setY(boxMesh.position.y + moveStep);
103
+ if (keys.ArrowDown) boxMesh.position.setY(boxMesh.position.y - moveStep);
104
+ };
105
+
106
+ animate();
107
+ }
44
108
  }
@@ -1,216 +1,75 @@
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
- import {
5
- DIRTY_NONE,
6
- DIRTY_RECT,
7
- DIRTY_STRUCTURE,
8
- DIRTY_STYLE,
9
- USER_LAYER,
10
- SYSTEM_LAYER,
11
- Visibility,
12
- } from "../types";
13
-
14
- interface InternalResizeConfig {
15
- enabled: boolean;
16
- delay: number;
17
- onStart?: () => void;
18
- onEnd?: () => void;
19
- }
5
+ import { Visibility, USER_LAYER, ATTR_TRAVEL } from "../types";
6
+ import { animateMeshByData, updateFixedMeshesScroll } from "../animation/Animator";
7
+ import { Tracker } from "@mirage-engine/dom-tracker";
20
8
 
21
9
  export class Syncer {
22
10
  private target: HTMLElement;
23
11
  private renderer: Renderer;
24
- private filter: CoreConfig["filter"];
25
-
26
- private observer: MutationObserver;
27
-
28
- private isDomDirty: boolean = false;
29
- private isRunning: boolean = false;
12
+ private registry: MeshRegistry;
30
13
  private isTravelEnabled: boolean = false;
31
-
32
- private pendingMask: number = DIRTY_NONE;
33
-
34
- private mutationTimer: number | null = null;
35
- private cssTimer: number | null = null;
36
-
37
- private resizeConfig: InternalResizeConfig;
38
- private resizeTimer: number | null = null;
39
- private isResizing: boolean = false;
40
-
41
- constructor(target: HTMLElement, renderer: Renderer, config: CoreConfig) {
14
+
15
+ public tracker: Tracker;
16
+
17
+ constructor(
18
+ target: HTMLElement,
19
+ renderer: Renderer,
20
+ registry: MeshRegistry,
21
+ config: CoreConfig,
22
+ ) {
42
23
  this.target = target;
43
24
  this.renderer = renderer;
44
- this.filter = config.filter;
45
-
46
- // Resize Debounce Configuration
47
- const debounceOpt = config.resizeDebounce ?? true;
48
- if (debounceOpt === false) {
49
- this.resizeConfig = { enabled: false, delay: 0 };
50
- } else if (debounceOpt === true) {
51
- this.resizeConfig = { enabled: true, delay: 150 };
52
- } else {
53
- this.resizeConfig = {
54
- enabled: true,
55
- delay: debounceOpt.delay ?? 150,
56
- onStart: debounceOpt.onStart,
57
- onEnd: debounceOpt.onEnd,
58
- };
59
- }
25
+ this.registry = registry;
60
26
 
61
- this.observer = new MutationObserver((mutations) => {
62
- let currentMask = DIRTY_NONE;
27
+ // Use Tracker from dom-tracker
28
+ this.tracker = new Tracker(target, {
29
+ resizeDebounce: config.resizeDebounce,
30
+ });
63
31
 
64
- for (const mutation of mutations) {
65
- if (mutation.type === "childList") {
66
- currentMask |= DIRTY_STRUCTURE;
67
- } else if (mutation.type === "attributes") {
68
- if (
69
- mutation.attributeName === "style" ||
70
- mutation.attributeName === "class"
71
- ) {
72
- currentMask |= DIRTY_RECT | DIRTY_STYLE;
73
- }
74
- }
32
+ // Wire up the hooks
33
+ this.tracker.onLayoutChange.add((pendingMask, pendingDeletions) => {
34
+ const discoveredTraveler =
35
+ document.querySelector(`[${ATTR_TRAVEL.NAME}~='${ATTR_TRAVEL.VALUES.TRAVELER}']`) !== null;
36
+ if (discoveredTraveler && !this.isTravelEnabled) {
37
+ this.isTravelEnabled = true;
38
+ this.renderer.createRenderTarget();
75
39
  }
76
40
 
77
- if (currentMask !== DIRTY_NONE) {
78
- this.pendingMask |= currentMask;
79
-
80
- // Structural Change detected
81
- if (currentMask & DIRTY_STRUCTURE) {
82
- this.clearTimers();
83
- console.log("Structural Change detected");
84
- this.isDomDirty = true;
85
- return;
86
- }
87
- if (this.mutationTimer) {
88
- clearTimeout(this.mutationTimer);
89
- }
90
- this.mutationTimer = window.setTimeout(() => {
91
- this.mutationTimer = null;
92
- this.isDomDirty = true;
93
- }, 200);
41
+ const sceneGraph = extractSceneGraph(
42
+ this.target,
43
+ pendingMask,
44
+ USER_LAYER as Visibility,
45
+ 1,
46
+ 0,
47
+ this.renderer.qualityFactor
48
+ );
49
+
50
+ if (sceneGraph) {
51
+ this.renderer.syncScene(sceneGraph, pendingDeletions);
94
52
  }
95
53
  });
96
- }
97
-
98
- public start() {
99
- if (this.isRunning) return;
100
- this.isRunning = true;
101
54
 
102
- //DOM observing start
103
- this.observer.observe(this.target, {
104
- childList: true,
105
- subtree: true,
106
- attributes: true,
107
- characterData: true,
55
+ this.tracker.onScrollChange.add((scrollX, scrollY) => {
56
+ updateFixedMeshesScroll(this.renderer.fixedMeshes, scrollX, scrollY);
108
57
  });
109
58
 
110
- this.target.addEventListener("transitionend", this.onTransitionFinished);
111
- this.target.addEventListener("animationend", this.onTransitionFinished);
112
-
113
- window.addEventListener("resize", this.onWindowResize);
114
-
115
- this.forceUpdateScene();
116
- this.renderLoop();
117
- // for debugging
118
- // this.renderer.showScissoredRenderTarget();
119
- }
120
-
121
- public stop() {
122
- this.isRunning = false;
123
- this.observer.disconnect();
124
-
125
- this.clearTimers();
59
+ this.tracker.onStyleChange.add((pendingStyles) => {
60
+ animateMeshByData(this.registry, pendingStyles);
61
+ });
126
62
 
127
- this.target.removeEventListener("transitionend", this.onTransitionFinished);
128
- this.target.removeEventListener("animationend", this.onTransitionFinished);
129
- window.removeEventListener("resize", this.onWindowResize);
63
+ this.tracker.onRender.add(() => {
64
+ this.renderer.render();
65
+ });
130
66
  }
131
67
 
132
- private clearTimers() {
133
- if (this.mutationTimer) {
134
- clearTimeout(this.mutationTimer);
135
- this.mutationTimer = null;
136
- }
137
- if (this.cssTimer) {
138
- clearTimeout(this.cssTimer);
139
- this.cssTimer = null;
140
- }
68
+ public start() {
69
+ this.tracker.start();
141
70
  }
142
71
 
143
- private onTransitionFinished = (e: Event) => {
144
- if (!this.target.contains(e.target as Node)) return;
145
-
146
- if (this.mutationTimer !== null) return;
147
-
148
- if (this.cssTimer) clearTimeout(this.cssTimer);
149
-
150
- this.pendingMask |= DIRTY_RECT | DIRTY_STYLE;
151
-
152
- this.cssTimer = window.setTimeout(() => {
153
- this.isDomDirty = true;
154
- this.cssTimer = null;
155
- }, 50);
156
- };
157
-
158
- private onWindowResize = () => {
159
- if (!this.resizeConfig.enabled) {
160
- this.isDomDirty = true;
161
- return;
162
- }
163
-
164
- if (!this.isResizing) {
165
- this.isResizing = true;
166
- if (this.resizeConfig.onStart) this.resizeConfig.onStart();
167
- }
168
-
169
- if (this.resizeTimer) clearTimeout(this.resizeTimer);
170
-
171
- this.resizeTimer = window.setTimeout(() => {
172
- this.isDomDirty = true;
173
-
174
- if (this.resizeConfig.onEnd) this.resizeConfig.onEnd();
175
- this.isResizing = false;
176
- this.resizeTimer = null;
177
- }, this.resizeConfig.delay);
178
- };
179
-
180
- private forceUpdateScene() {
181
- this.isDomDirty = false;
182
-
183
- const discoveredTraveler =
184
- document.querySelector("[data-mirage-travel='traveler']") !== null;
185
- if (discoveredTraveler && !this.isTravelEnabled) {
186
- this.isTravelEnabled = true;
187
- this.renderer.createRenderTarget();
188
- }
189
-
190
- const sceneGraph = extractSceneGraph(
191
- this.target,
192
- this.pendingMask,
193
- (discoveredTraveler
194
- ? USER_LAYER | SYSTEM_LAYER
195
- : USER_LAYER) as Visibility,
196
- this.filter,
197
- );
198
-
199
- if (sceneGraph) {
200
- this.renderer.syncScene(sceneGraph);
201
- }
202
-
203
- this.pendingMask = DIRTY_NONE;
72
+ public stop() {
73
+ this.tracker.stop();
204
74
  }
205
-
206
- private renderLoop = () => {
207
- if (!this.isRunning) return;
208
-
209
- if (this.isDomDirty) {
210
- this.forceUpdateScene();
211
- }
212
-
213
- this.renderer.render();
214
- requestAnimationFrame(this.renderLoop);
215
- };
216
75
  }