@mirage-engine/core 0.0.2 → 0.2.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.
@@ -2,9 +2,11 @@ import * as THREE from "three";
2
2
  import {
3
3
  SceneNode,
4
4
  DIRTY_CONTENT,
5
- TextQuality,
5
+ Quality,
6
6
  CoreConfig,
7
7
  MirageMode,
8
+ USER_LAYER,
9
+ SYSTEM_LAYER,
8
10
  } from "../types";
9
11
  import { Painter } from "@mirage-engine/painter";
10
12
 
@@ -13,10 +15,10 @@ export class Renderer {
13
15
  private readonly scene: THREE.Scene;
14
16
  private readonly camera: THREE.OrthographicCamera;
15
17
  private readonly renderer: THREE.WebGLRenderer;
18
+ private renderTarget: THREE.WebGLRenderTarget | null = null;
16
19
  private renderOrder: number = 0;
17
- private textQualityFactor: number = 2;
20
+ private qualityFactor: number = 2;
18
21
  private mode: MirageMode = "overlay";
19
- private customZIndex: string = "9999";
20
22
 
21
23
  private target: HTMLElement;
22
24
  private mountContainer: HTMLElement;
@@ -34,10 +36,6 @@ export class Renderer {
34
36
 
35
37
  this.mode = config.mode ?? "overlay";
36
38
 
37
- if (config.style?.zIndex) {
38
- this.customZIndex = config.style.zIndex;
39
- }
40
-
41
39
  this.canvas = document.createElement("canvas");
42
40
  this.scene = new THREE.Scene();
43
41
 
@@ -58,42 +56,61 @@ export class Renderer {
58
56
  1000,
59
57
  );
60
58
  this.camera.position.z = 100;
59
+ this.camera.layers.set(0);
60
+
61
+ // [new]
62
+ // THREE.ColorManagement.enabled = false;
61
63
 
62
64
  this.renderer = new THREE.WebGLRenderer({
63
65
  canvas: this.canvas,
64
66
  alpha: true,
65
67
  antialias: true,
68
+ // [new]
69
+ // premultipliedAlpha: true
66
70
  });
67
71
 
68
72
  this.renderer.setPixelRatio(window.devicePixelRatio);
69
73
  this.renderer.setSize(width, height);
70
74
 
71
- this.applyTextQuality(config.textQuality ?? "medium");
75
+ this.applyTextQuality(config.quality ?? "medium");
76
+ }
77
+ public createRenderTarget() {
78
+ this.renderTarget = new THREE.WebGLRenderTarget(
79
+ this.targetRect.width * this.qualityFactor,
80
+ this.targetRect.height * this.qualityFactor,
81
+ {
82
+ minFilter: THREE.LinearFilter,
83
+ magFilter: THREE.LinearFilter,
84
+ format: THREE.RGBAFormat,
85
+ stencilBuffer: false,
86
+ depthBuffer: true,
87
+ samples: 4,
88
+ },
89
+ );
72
90
  }
73
91
 
74
- private applyTextQuality(quality: TextQuality) {
92
+ private applyTextQuality(quality: Quality) {
75
93
  if (typeof quality === "number") {
76
- this.textQualityFactor = Math.max(0.1, quality);
94
+ this.qualityFactor = Math.max(0.1, quality);
77
95
  return;
78
96
  }
79
97
  switch (quality) {
80
98
  case "low":
81
- this.textQualityFactor = 1;
99
+ this.qualityFactor = 1;
82
100
  break;
83
101
  case "high":
84
- this.textQualityFactor = 4;
102
+ this.qualityFactor = 4;
85
103
  break;
86
104
  case "medium":
87
105
  default:
88
- this.textQualityFactor = 2;
106
+ this.qualityFactor = 2;
89
107
  break;
90
108
  }
91
109
  }
92
110
 
93
111
  public mount() {
94
- this.mountContainer.appendChild(this.canvas);
112
+ this.mountContainer.prepend(this.canvas);
95
113
 
96
- this.canvas.style.zIndex = this.customZIndex;
97
114
  this.canvas.style.pointerEvents = this.mode === "overlay" ? "none" : "auto";
98
115
 
99
116
  this.updateCanvasLayout();
@@ -107,7 +124,6 @@ export class Renderer {
107
124
  this.canvas.style.position = "";
108
125
  this.canvas.style.top = "";
109
126
  this.canvas.style.left = "";
110
-
111
127
  this.canvas.style.display = "block";
112
128
  } else {
113
129
  this.canvas.style.position = "absolute";
@@ -125,12 +141,16 @@ export class Renderer {
125
141
 
126
142
  public setSize(width: number, height: number) {
127
143
  this.renderer.setSize(width, height);
128
-
144
+ if (this.renderTarget) {
145
+ this.renderTarget.setSize(
146
+ width * this.qualityFactor,
147
+ height * this.qualityFactor,
148
+ );
149
+ }
129
150
  this.camera.left = width / -2;
130
151
  this.camera.right = width / 2;
131
152
  this.camera.top = height / 2;
132
153
  this.camera.bottom = height / -2;
133
-
134
154
  this.camera.updateProjectionMatrix();
135
155
  }
136
156
 
@@ -148,13 +168,7 @@ export class Renderer {
148
168
 
149
169
  if (isResized) {
150
170
  this.targetRect = newRect;
151
- this.renderer.setSize(this.targetRect.width, this.targetRect.height);
152
-
153
- this.camera.left = this.targetRect.width / -2;
154
- this.camera.right = this.targetRect.width / 2;
155
- this.camera.top = this.targetRect.height / 2;
156
- this.camera.bottom = this.targetRect.height / -2;
157
- this.camera.updateProjectionMatrix();
171
+ this.setSize(this.targetRect.width, this.targetRect.height);
158
172
 
159
173
  this.updateCanvasLayout();
160
174
  } else if (isMoved) {
@@ -174,7 +188,6 @@ export class Renderer {
174
188
  if (!activeElements.has(el)) {
175
189
  this.scene.remove(mesh);
176
190
 
177
- // Garbage Collection
178
191
  mesh.geometry.dispose();
179
192
  if (mesh.material instanceof THREE.Material) mesh.material.dispose();
180
193
  this.meshMap.delete(el);
@@ -188,17 +201,18 @@ export class Renderer {
188
201
  let mesh = this.meshMap.get(node.element);
189
202
  if (!mesh) {
190
203
  const geometry = new THREE.PlaneGeometry(1, 1);
191
- const material = Painter.create(
204
+ let material: THREE.MeshBasicMaterial | THREE.Material;
205
+ material = Painter.create(
192
206
  "BOX",
193
207
  node.styles,
194
208
  "",
195
209
  node.rect.width,
196
210
  node.rect.height,
211
+ this.qualityFactor,
212
+ node.isTraveler ? this.renderTarget?.texture : undefined,
197
213
  );
198
-
199
214
  mesh = new THREE.Mesh(geometry, material);
200
215
  if (node.type === "TEXT") mesh.name = "BG_MESH";
201
-
202
216
  this.scene.add(mesh);
203
217
  this.meshMap.set(node.element, mesh);
204
218
  }
@@ -206,6 +220,8 @@ export class Renderer {
206
220
  mesh.userData.domRect = node.rect;
207
221
 
208
222
  this.updateMeshProperties(mesh, node);
223
+ this.updateMeshLayers(mesh, node);
224
+ if (node.isTraveler) mesh.layers.enable(28);
209
225
 
210
226
  if (node.type === "BOX") {
211
227
  for (const child of node.children) {
@@ -241,7 +257,7 @@ export class Renderer {
241
257
  node.textContent || "",
242
258
  node.rect.width,
243
259
  node.rect.height,
244
- this.textQualityFactor,
260
+ this.qualityFactor,
245
261
  );
246
262
 
247
263
  const geometry = new THREE.PlaneGeometry(1, 1);
@@ -249,7 +265,7 @@ export class Renderer {
249
265
 
250
266
  textMesh.name = "TEXT_CHILD";
251
267
  textMesh.userData = { styleHash: currentStyleHash };
252
-
268
+ this.updateMeshLayers(textMesh, node);
253
269
  textMesh.position.z = 0.005;
254
270
  parentMesh.add(textMesh);
255
271
  }
@@ -299,10 +315,98 @@ export class Renderer {
299
315
  "",
300
316
  node.rect.width,
301
317
  node.rect.height,
318
+ this.qualityFactor,
319
+ node.isTraveler ? this.renderTarget?.texture : undefined,
302
320
  );
303
321
  }
304
322
 
323
+ private updateMeshLayers(mesh: THREE.Mesh, node: SceneNode) {
324
+ const layerNum = (1 - (node.visibility & USER_LAYER)) * 30;
325
+ mesh.layers.set(layerNum);
326
+ if (node.visibility === (USER_LAYER | SYSTEM_LAYER)) mesh.layers.enable(29);
327
+ }
328
+
329
+ private captureRenderTarget() {
330
+ const travelers: THREE.Mesh[] = [];
331
+ for (const mesh of this.meshMap.values()) {
332
+ if ((mesh.layers.mask & (1 << 28)) !== 0) {
333
+ travelers.push(mesh);
334
+ }
335
+ }
336
+ if (travelers.length === 0) return;
337
+
338
+ const oldClearColor = new THREE.Color();
339
+ const oldClearAlpha = this.renderer.getClearAlpha();
340
+ this.renderer.getClearColor(oldClearColor);
341
+
342
+ this.renderer.setClearColor(0x000000, 0);
343
+ this.renderer.setRenderTarget(this.renderTarget);
344
+
345
+ this.renderer.clear();
346
+
347
+ this.renderer.autoClear = false;
348
+ this.renderer.setScissorTest(true);
349
+ this.camera.layers.set(29);
350
+
351
+ const vector = new THREE.Vector3();
352
+ const canvasWidth = this.targetRect.width;
353
+ const canvasHeight = this.targetRect.height;
354
+
355
+ const pixelRatio = this.renderer.getPixelRatio();
356
+
357
+ for (const traveler of travelers) {
358
+ vector.setFromMatrixPosition(traveler.matrixWorld);
359
+ vector.project(this.camera);
360
+
361
+ const centerX = ((vector.x + 1) / 2) * canvasWidth;
362
+ const centerY = ((vector.y + 1) / 2) * canvasHeight;
363
+
364
+ const width = traveler.scale.x;
365
+ const height = traveler.scale.y;
366
+
367
+ const localX = centerX - width / 2;
368
+ const localY = centerY - height / 2;
369
+
370
+ const scissorX = (localX * this.qualityFactor) / pixelRatio;
371
+ const scissorY = (localY * this.qualityFactor) / pixelRatio;
372
+ const scissorW = (width * this.qualityFactor) / pixelRatio;
373
+ const scissorH = (height * this.qualityFactor) / pixelRatio;
374
+
375
+ this.renderer.setScissor(scissorX, scissorY, scissorW, scissorH);
376
+ this.renderer.render(this.scene, this.camera);
377
+ }
378
+ this.renderer.setScissorTest(false);
379
+ this.renderer.autoClear = true;
380
+ this.renderer.setRenderTarget(null);
381
+ this.camera.layers.set(28);
382
+ this.renderer.setClearColor(oldClearColor, oldClearAlpha);
383
+ }
384
+
305
385
  public render() {
386
+ if (this.renderTarget) this.captureRenderTarget();
306
387
  this.renderer.render(this.scene, this.camera);
307
388
  }
389
+
390
+ // for debugging
391
+ public showScissoredRenderTarget() {
392
+ if (!this.renderTarget) return;
393
+
394
+ const w = this.targetRect.width;
395
+ const h = this.targetRect.height;
396
+
397
+ const geometry = new THREE.PlaneGeometry(w, h);
398
+ const material = new THREE.MeshBasicMaterial({
399
+ map: this.renderTarget.texture,
400
+ side: THREE.DoubleSide,
401
+ transparent: true,
402
+ opacity: 0.8,
403
+ });
404
+
405
+ const debugMesh = new THREE.Mesh(geometry, material);
406
+
407
+ debugMesh.position.set(0, 0, 90);
408
+ debugMesh.layers.set(28);
409
+
410
+ this.scene.add(debugMesh);
411
+ }
308
412
  }
@@ -1,6 +1,7 @@
1
1
  import { TextStyles, BoxStyles } from "@mirage-engine/painter";
2
+ import { Visibility } from "./flags";
2
3
 
3
- export type NodeType = "BOX" | "TEXT"
4
+ export type NodeType = "BOX" | "TEXT";
4
5
 
5
6
  export interface NodeRect {
6
7
  x: number;
@@ -9,13 +10,11 @@ export interface NodeRect {
9
10
  height: number;
10
11
  }
11
12
 
12
-
13
-
14
13
  export interface SceneNode {
15
14
  id: string;
16
15
  type: NodeType;
17
16
  element: HTMLElement;
18
-
17
+
19
18
  rect: NodeRect;
20
19
  styles: BoxStyles;
21
20
 
@@ -23,6 +22,10 @@ export interface SceneNode {
23
22
  textStyles?: TextStyles;
24
23
 
25
24
  dirtyMask: number;
25
+ // TODO: SceneNode의 sort 로직 추가(아마도)
26
+
27
+ visibility: Visibility;
28
+ isTraveler: boolean;
29
+
26
30
  children: SceneNode[];
27
31
  }
28
-
@@ -1,21 +1,36 @@
1
- export type TextQuality = "low" | "medium" | "high" | number;
1
+ export type Quality = "low" | "medium" | "high" | number;
2
2
  export type MirageMode = "overlay" | "duplicate";
3
+ export interface FilterConfig {
4
+ includeTree?: string[];
5
+ excludeTree?: string[];
6
+ includeSelf?: string[];
7
+ excludeSelf?: string[];
8
+ end?: string[];
9
+ }
10
+
11
+ export interface ResizeConfig {
12
+ delay?: number;
13
+ onStart?: () => void;
14
+ onEnd?: () => void;
15
+ }
3
16
 
4
17
  interface BaseConfig {
5
18
  debug?: boolean;
6
- textQuality?: TextQuality;
19
+ quality?: Quality;
7
20
  style?: {
8
21
  zIndex?: string;
9
22
  };
23
+ filter?: FilterConfig;
24
+ resizeDebounce?: boolean | ResizeConfig;
10
25
  }
11
26
 
12
27
  export interface OverlayConfig extends BaseConfig {
13
- mode?: "overlay";
28
+ mode?: "overlay";
14
29
  }
15
30
 
16
31
  export interface DuplicateConfig extends BaseConfig {
17
- mode: "duplicate";
18
- container?: HTMLElement;
32
+ mode: "duplicate";
33
+ container?: HTMLElement;
19
34
  }
20
35
 
21
- export type CoreConfig = OverlayConfig | DuplicateConfig;
36
+ export type CoreConfig = OverlayConfig | DuplicateConfig;
@@ -4,4 +4,19 @@ export const DIRTY_RECT = 1 << 0; // 1: x, y, width, height (00000001)
4
4
  export const DIRTY_STYLE = 1 << 1; // 2: style (00000010)
5
5
  export const DIRTY_ZINDEX = 1 << 2; // 4: zIndex (00000100)
6
6
  export const DIRTY_STRUCTURE = 1 << 3; // 8: childNode add / delete (00001000)
7
- export const DIRTY_CONTENT = 1 << 4; // 16: text content changed (00010000)
7
+ export const DIRTY_CONTENT = 1 << 4; // 16: text content changed (00010000)
8
+
9
+ // Filtering Flag
10
+ export const USER_LAYER = 1 << 0;
11
+ export const SYSTEM_LAYER = 1 << 1;
12
+ export const EXCLUDED = 0;
13
+
14
+ export type Visibility = 0 | 1 | 2 | 3;
15
+
16
+ export const ALLOWED_FILTERS = [
17
+ "include-tree",
18
+ "exclude-tree",
19
+ "include-self",
20
+ "exclude-self",
21
+ "end",
22
+ ];