@mirage-engine/core 0.1.0 → 0.2.1

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,12 @@ 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,
10
+ travelerClipArea,
8
11
  } from "../types";
9
12
  import { Painter } from "@mirage-engine/painter";
10
13
 
@@ -13,9 +16,11 @@ export class Renderer {
13
16
  private readonly scene: THREE.Scene;
14
17
  private readonly camera: THREE.OrthographicCamera;
15
18
  private readonly renderer: THREE.WebGLRenderer;
19
+ private renderTarget: THREE.WebGLRenderTarget | null = null;
16
20
  private renderOrder: number = 0;
17
- private textQualityFactor: number = 2;
21
+ private qualityFactor: number = 2;
18
22
  private mode: MirageMode = "overlay";
23
+ private clipArea: travelerClipArea = 1;
19
24
 
20
25
  private target: HTMLElement;
21
26
  private mountContainer: HTMLElement;
@@ -32,7 +37,7 @@ export class Renderer {
32
37
  this.mountContainer = mountContainer;
33
38
 
34
39
  this.mode = config.mode ?? "overlay";
35
-
40
+ this.clipArea = config.travelerClipArea ?? 1;
36
41
  this.canvas = document.createElement("canvas");
37
42
  this.scene = new THREE.Scene();
38
43
 
@@ -53,6 +58,7 @@ export class Renderer {
53
58
  1000,
54
59
  );
55
60
  this.camera.position.z = 100;
61
+ this.camera.layers.set(0);
56
62
 
57
63
  // [new]
58
64
  // THREE.ColorManagement.enabled = false;
@@ -68,24 +74,38 @@ export class Renderer {
68
74
  this.renderer.setPixelRatio(window.devicePixelRatio);
69
75
  this.renderer.setSize(width, height);
70
76
 
71
- this.applyTextQuality(config.textQuality ?? "medium");
77
+ this.applyTextQuality(config.quality ?? "medium");
78
+ }
79
+ public createRenderTarget() {
80
+ this.renderTarget = new THREE.WebGLRenderTarget(
81
+ this.targetRect.width * this.qualityFactor,
82
+ this.targetRect.height * this.qualityFactor,
83
+ {
84
+ minFilter: THREE.LinearFilter,
85
+ magFilter: THREE.LinearFilter,
86
+ format: THREE.RGBAFormat,
87
+ stencilBuffer: false,
88
+ depthBuffer: true,
89
+ samples: 4,
90
+ },
91
+ );
72
92
  }
73
93
 
74
- private applyTextQuality(quality: TextQuality) {
94
+ private applyTextQuality(quality: Quality) {
75
95
  if (typeof quality === "number") {
76
- this.textQualityFactor = Math.max(0.1, quality);
96
+ this.qualityFactor = Math.max(0.1, quality);
77
97
  return;
78
98
  }
79
99
  switch (quality) {
80
100
  case "low":
81
- this.textQualityFactor = 1;
101
+ this.qualityFactor = 1;
82
102
  break;
83
103
  case "high":
84
- this.textQualityFactor = 4;
104
+ this.qualityFactor = 4;
85
105
  break;
86
106
  case "medium":
87
107
  default:
88
- this.textQualityFactor = 2;
108
+ this.qualityFactor = 2;
89
109
  break;
90
110
  }
91
111
  }
@@ -123,12 +143,16 @@ export class Renderer {
123
143
 
124
144
  public setSize(width: number, height: number) {
125
145
  this.renderer.setSize(width, height);
126
-
146
+ if (this.renderTarget) {
147
+ this.renderTarget.setSize(
148
+ width * this.qualityFactor,
149
+ height * this.qualityFactor,
150
+ );
151
+ }
127
152
  this.camera.left = width / -2;
128
153
  this.camera.right = width / 2;
129
154
  this.camera.top = height / 2;
130
155
  this.camera.bottom = height / -2;
131
-
132
156
  this.camera.updateProjectionMatrix();
133
157
  }
134
158
 
@@ -146,14 +170,7 @@ export class Renderer {
146
170
 
147
171
  if (isResized) {
148
172
  this.targetRect = newRect;
149
- this.renderer.setSize(this.targetRect.width, this.targetRect.height);
150
-
151
- this.camera.left = this.targetRect.width / -2;
152
- this.camera.right = this.targetRect.width / 2;
153
- this.camera.top = this.targetRect.height / 2;
154
- this.camera.bottom = this.targetRect.height / -2;
155
- this.camera.layers.set(0);
156
- this.camera.updateProjectionMatrix();
173
+ this.setSize(this.targetRect.width, this.targetRect.height);
157
174
 
158
175
  this.updateCanvasLayout();
159
176
  } else if (isMoved) {
@@ -186,16 +203,18 @@ export class Renderer {
186
203
  let mesh = this.meshMap.get(node.element);
187
204
  if (!mesh) {
188
205
  const geometry = new THREE.PlaneGeometry(1, 1);
189
- const material = Painter.create(
206
+ let material: THREE.MeshBasicMaterial | THREE.Material;
207
+ material = Painter.create(
190
208
  "BOX",
191
209
  node.styles,
192
210
  "",
193
211
  node.rect.width,
194
212
  node.rect.height,
213
+ this.qualityFactor,
214
+ node.isTraveler ? this.renderTarget?.texture : undefined,
215
+ node.shaderHooks
195
216
  );
196
-
197
217
  mesh = new THREE.Mesh(geometry, material);
198
- mesh.layers.set(node.visibility);
199
218
  if (node.type === "TEXT") mesh.name = "BG_MESH";
200
219
  this.scene.add(mesh);
201
220
  this.meshMap.set(node.element, mesh);
@@ -204,6 +223,8 @@ export class Renderer {
204
223
  mesh.userData.domRect = node.rect;
205
224
 
206
225
  this.updateMeshProperties(mesh, node);
226
+ this.updateMeshLayers(mesh, node);
227
+ if (node.isTraveler) mesh.layers.enable(28);
207
228
 
208
229
  if (node.type === "BOX") {
209
230
  for (const child of node.children) {
@@ -239,7 +260,7 @@ export class Renderer {
239
260
  node.textContent || "",
240
261
  node.rect.width,
241
262
  node.rect.height,
242
- this.textQualityFactor,
263
+ this.qualityFactor,
243
264
  );
244
265
 
245
266
  const geometry = new THREE.PlaneGeometry(1, 1);
@@ -247,7 +268,7 @@ export class Renderer {
247
268
 
248
269
  textMesh.name = "TEXT_CHILD";
249
270
  textMesh.userData = { styleHash: currentStyleHash };
250
- textMesh.layers.set(node.visibility);
271
+ this.updateMeshLayers(textMesh, node);
251
272
  textMesh.position.z = 0.005;
252
273
  parentMesh.add(textMesh);
253
274
  }
@@ -297,10 +318,109 @@ export class Renderer {
297
318
  "",
298
319
  node.rect.width,
299
320
  node.rect.height,
321
+ this.qualityFactor,
322
+ node.isTraveler ? this.renderTarget?.texture : undefined,
300
323
  );
301
324
  }
302
325
 
326
+ private updateMeshLayers(mesh: THREE.Mesh, node: SceneNode) {
327
+ const layerNum = (1 - (node.visibility & USER_LAYER)) * 30;
328
+ mesh.layers.set(layerNum);
329
+ if (node.visibility === (USER_LAYER | SYSTEM_LAYER)) mesh.layers.enable(29);
330
+ }
331
+
332
+ 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;
340
+
341
+ const oldClearColor = new THREE.Color();
342
+ const oldClearAlpha = this.renderer.getClearAlpha();
343
+ this.renderer.getClearColor(oldClearColor);
344
+
345
+ this.renderer.setClearColor(0x000000, 0);
346
+ this.renderer.setRenderTarget(this.renderTarget);
347
+
348
+ this.renderer.clear();
349
+
350
+ this.renderer.autoClear = false;
351
+ this.renderer.setScissorTest(true);
352
+ this.camera.layers.set(29);
353
+
354
+ const vector = new THREE.Vector3();
355
+ const canvasWidth = this.targetRect.width;
356
+ const canvasHeight = this.targetRect.height;
357
+
358
+ const pixelRatio = this.renderer.getPixelRatio();
359
+
360
+ for (const traveler of travelers) {
361
+ vector.setFromMatrixPosition(traveler.matrixWorld);
362
+ vector.project(this.camera);
363
+
364
+ const centerX = ((vector.x + 1) / 2) * canvasWidth;
365
+ const centerY = ((vector.y + 1) / 2) * canvasHeight;
366
+
367
+ let clipDiff = 0;
368
+ let clipRadito = 1;
369
+ if (typeof this.clipArea === "number") {
370
+ clipRadito = this.clipArea;
371
+ } else if (this.clipArea.endsWith("%")) {
372
+ clipRadito = parseFloat(this.clipArea) / 100;
373
+ } else if (this.clipArea.endsWith("px")) {
374
+ clipDiff = parseFloat(this.clipArea);
375
+ }
376
+
377
+ const width = traveler.scale.x * clipRadito + 0.5;
378
+ const height = traveler.scale.y * clipRadito + 0.5;
379
+
380
+ const localX = centerX - width / 2;
381
+ const localY = centerY - height / 2;
382
+
383
+ const scissorX = (localX * this.qualityFactor - clipDiff) / pixelRatio;
384
+ const scissorY = (localY * this.qualityFactor - clipDiff) / pixelRatio;
385
+ const scissorW = (width * this.qualityFactor + clipDiff * 2) / pixelRatio;
386
+ const scissorH =
387
+ (height * this.qualityFactor + clipDiff * 2) / pixelRatio;
388
+
389
+ this.renderer.setScissor(scissorX, scissorY, scissorW, scissorH);
390
+ this.renderer.render(this.scene, this.camera);
391
+ }
392
+ this.renderer.setScissorTest(false);
393
+ this.renderer.autoClear = true;
394
+ this.renderer.setRenderTarget(null);
395
+ this.camera.layers.set(28);
396
+ this.renderer.setClearColor(oldClearColor, oldClearAlpha);
397
+ }
398
+
303
399
  public render() {
400
+ if (this.renderTarget) this.captureRenderTarget();
304
401
  this.renderer.render(this.scene, this.camera);
305
402
  }
403
+
404
+ // for debugging
405
+ public showScissoredRenderTarget() {
406
+ if (!this.renderTarget) return;
407
+
408
+ const w = this.targetRect.width;
409
+ const h = this.targetRect.height;
410
+
411
+ const geometry = new THREE.PlaneGeometry(w, h);
412
+ const material = new THREE.MeshBasicMaterial({
413
+ map: this.renderTarget.texture,
414
+ side: THREE.DoubleSide,
415
+ transparent: true,
416
+ opacity: 0.8,
417
+ });
418
+
419
+ const debugMesh = new THREE.Mesh(geometry, material);
420
+
421
+ debugMesh.position.set(0, 0, 90);
422
+ debugMesh.layers.set(28);
423
+
424
+ this.scene.add(debugMesh);
425
+ }
306
426
  }
@@ -1,7 +1,7 @@
1
- import { TextStyles, BoxStyles } from "@mirage-engine/painter";
1
+ import { TextStyles, BoxStyles, ShaderHooks } from "@mirage-engine/painter";
2
2
  import { Visibility } from "./flags";
3
3
 
4
- export type NodeType = "BOX" | "TEXT"
4
+ export type NodeType = "BOX" | "TEXT";
5
5
 
6
6
  export interface NodeRect {
7
7
  x: number;
@@ -14,7 +14,7 @@ export interface SceneNode {
14
14
  id: string;
15
15
  type: NodeType;
16
16
  element: HTMLElement;
17
-
17
+
18
18
  rect: NodeRect;
19
19
  styles: BoxStyles;
20
20
 
@@ -25,7 +25,8 @@ export interface SceneNode {
25
25
  // TODO: SceneNode의 sort 로직 추가(아마도)
26
26
 
27
27
  visibility: Visibility;
28
-
28
+ isTraveler: boolean;
29
+ shaderHooks?: ShaderHooks;
30
+
29
31
  children: SceneNode[];
30
32
  }
31
-
@@ -1,6 +1,8 @@
1
- export type TextQuality = "low" | "medium" | "high" | number;
1
+ export type Quality = "low" | "medium" | "high" | number;
2
2
  export type MirageMode = "overlay" | "duplicate";
3
-
3
+ export type PxUnit = `${number}px`;
4
+ export type PercentUnit = `${number}%`;
5
+ export type travelerClipArea = PxUnit | PercentUnit | number;
4
6
  export interface FilterConfig {
5
7
  includeTree?: string[];
6
8
  excludeTree?: string[];
@@ -9,13 +11,21 @@ export interface FilterConfig {
9
11
  end?: string[];
10
12
  }
11
13
 
14
+ export interface ResizeConfig {
15
+ delay?: number;
16
+ onStart?: () => void;
17
+ onEnd?: () => void;
18
+ }
19
+
12
20
  interface BaseConfig {
13
21
  debug?: boolean;
14
- textQuality?: TextQuality;
22
+ quality?: Quality;
15
23
  style?: {
16
24
  zIndex?: string;
17
25
  };
18
26
  filter?: FilterConfig;
27
+ resizeDebounce?: boolean | ResizeConfig;
28
+ travelerClipArea? : travelerClipArea;
19
29
  }
20
30
 
21
31
  export interface OverlayConfig extends BaseConfig {
@@ -7,12 +7,11 @@ export const DIRTY_STRUCTURE = 1 << 3; // 8: childNode add / delete (00001000)
7
7
  export const DIRTY_CONTENT = 1 << 4; // 16: text content changed (00010000)
8
8
 
9
9
  // Filtering Flag
10
- export const INCLUDED = 0;
11
- export const SYSTEM = 29;
12
- export const EXCLUDED = 30;
10
+ export const USER_LAYER = 1 << 0;
11
+ export const SYSTEM_LAYER = 1 << 1;
12
+ export const EXCLUDED = 0;
13
13
 
14
- // 다른 파일에서 파라미터 타입 지정용으로 사용
15
- export type Visibility = typeof EXCLUDED | typeof SYSTEM | typeof INCLUDED;
14
+ export type Visibility = 0 | 1 | 2 | 3;
16
15
 
17
16
  export const ALLOWED_FILTERS = [
18
17
  "include-tree",