tela.js 1.2.2 → 1.2.4
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/README.md +8 -0
- package/package.json +4 -1
- package/src/Camera/parallel.js +33 -17
- package/src/Camera/raster.js +2 -3
- package/src/Camera/rayMapWorker.js +2 -8
- package/src/Camera/rayTraceWorker.js +2 -2
- package/src/Camera2D/Camera2D.js +114 -0
- package/src/Geometry/Box.js +14 -2
- package/src/Geometry/Line.js +2 -1
- package/src/Geometry/Sphere.js +3 -2
- package/src/Geometry/Triangle.js +4 -2
- package/src/IO/IO.js +2 -1
- package/src/Scene/KScene.js +5 -9
- package/src/Scene/NaiveScene.js +4 -0
- package/src/Scene/VoxelScene.js +8 -4
- package/src/Tela/Tela.js +1 -1
- package/src/Tela/Window.js +3 -2
- package/src/Tela/parallel.js +11 -5
- package/src/Utils/Anima.js +55 -0
- package/src/Utils/Constants.js +0 -3
- package/src/Utils/Math.js +14 -0
- package/src/Utils/SVG.js +80 -15
- package/src/Utils/Triangulate.js +134 -0
- package/src/Utils/Utils.js +7 -1
- package/src/Utils/Video.js +1 -1
- package/src/index.js +19 -13
package/README.md
CHANGED
|
@@ -150,6 +150,14 @@ You can find more examples of usage in:
|
|
|
150
150
|
|
|
151
151
|
- Serialize meshes not only triangles
|
|
152
152
|
- Optimize data serialization in parallel ray tracer
|
|
153
|
+
- Refactor geometric objects to have shader function
|
|
154
|
+
- Refactor parallel raytracing to parallel canvas map
|
|
155
|
+
|
|
156
|
+
- Add lorentz attractors demo
|
|
157
|
+
- Add Iterated map fractals demo
|
|
158
|
+
- Add Volumetric fluid sim
|
|
159
|
+
- Megaman rag doll physics
|
|
160
|
+
- Black hole demo
|
|
153
161
|
|
|
154
162
|
|
|
155
163
|
[ffmpeg]: https://ffmpeg.org/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tela.js",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"author": "Pedroth",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"trustedDependencies": [
|
|
24
24
|
"@kmamal/sdl"
|
|
25
25
|
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@kmamal/sdl": "^0.10.2"
|
|
28
|
+
},
|
|
26
29
|
"bugs": {
|
|
27
30
|
"url": "https://github.com/pedroth/tela.js/issues"
|
|
28
31
|
},
|
package/src/Camera/parallel.js
CHANGED
|
@@ -10,11 +10,13 @@ import { MyWorker } from "../Utils/Utils.js";
|
|
|
10
10
|
|
|
11
11
|
let RAY_TRACE_WORKERS = [];
|
|
12
12
|
let RAY_MAP_WORKERS = [];
|
|
13
|
+
|
|
13
14
|
let prevSceneHash = undefined;
|
|
14
|
-
let
|
|
15
|
+
let prevScene = undefined;
|
|
15
16
|
let serializedScene = undefined;
|
|
17
|
+
let isFirstTimeCounter = NUMBER_OF_CORES;
|
|
16
18
|
|
|
17
|
-
const
|
|
19
|
+
const ERROR_MSG_TIMEOUT = 1000;
|
|
18
20
|
//========================================================================================
|
|
19
21
|
/* *
|
|
20
22
|
* MAIN *
|
|
@@ -25,7 +27,7 @@ export function rayTraceWorkers(camera, scene, canvas, params = {}) {
|
|
|
25
27
|
// lazy loading workers
|
|
26
28
|
if (RAY_TRACE_WORKERS.length === 0) {
|
|
27
29
|
RAY_TRACE_WORKERS = [...Array(NUMBER_OF_CORES)]
|
|
28
|
-
.map(() => new MyWorker(
|
|
30
|
+
.map(() => new MyWorker(`./Camera/rayTraceWorker.js`));
|
|
29
31
|
}
|
|
30
32
|
const w = canvas.width;
|
|
31
33
|
const h = canvas.height;
|
|
@@ -33,14 +35,18 @@ export function rayTraceWorkers(camera, scene, canvas, params = {}) {
|
|
|
33
35
|
const isNewScene = prevSceneHash !== newHash;
|
|
34
36
|
if (isNewScene) {
|
|
35
37
|
prevSceneHash = newHash;
|
|
36
|
-
serializedScene = scene
|
|
38
|
+
serializedScene = scene?.serialize()
|
|
39
|
+
prevScene = serializedScene;
|
|
37
40
|
} else {
|
|
38
41
|
serializedScene = undefined;
|
|
39
42
|
}
|
|
40
43
|
return RAY_TRACE_WORKERS.map((worker, k) => {
|
|
44
|
+
let timerId = undefined;
|
|
41
45
|
return new Promise((resolve) => {
|
|
42
46
|
worker.onMessage(message => {
|
|
43
|
-
const { image, startRow, endRow, } = message;
|
|
47
|
+
const { image, startRow, endRow, hasScene } = message;
|
|
48
|
+
prevScene = hasScene ? undefined : prevScene;
|
|
49
|
+
if (!IS_NODE) clearTimeout(timerId);
|
|
44
50
|
let index = 0;
|
|
45
51
|
const startIndex = CHANNELS * w * startRow;
|
|
46
52
|
const endIndex = CHANNELS * w * endRow;
|
|
@@ -58,24 +64,28 @@ export function rayTraceWorkers(camera, scene, canvas, params = {}) {
|
|
|
58
64
|
startRow: k * ratio,
|
|
59
65
|
endRow: Math.min(h, (k + 1) * ratio),
|
|
60
66
|
camera: camera.serialize(),
|
|
61
|
-
scene: serializedScene
|
|
67
|
+
scene: isNewScene ? serializedScene : prevScene !== undefined ? prevScene : undefined
|
|
62
68
|
};
|
|
69
|
+
worker.postMessage(message);
|
|
63
70
|
if (isFirstTimeCounter > 0 && !IS_NODE) {
|
|
64
71
|
// hack to work in the browser, don't know why it works
|
|
65
72
|
isFirstTimeCounter--;
|
|
66
|
-
setTimeout(() =>
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
timerId = setTimeout(() => {
|
|
74
|
+
console.log("TIMEOUT!!")
|
|
75
|
+
// doesn't block promise
|
|
76
|
+
resolve();
|
|
77
|
+
}, ERROR_MSG_TIMEOUT);
|
|
69
78
|
}
|
|
70
79
|
});
|
|
71
80
|
})
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
|
|
74
84
|
export function rayMapWorkers(camera, scene, canvas, lambda, vars = [], dependencies = []) {
|
|
75
85
|
// lazy loading workers
|
|
76
86
|
if (RAY_MAP_WORKERS.length === 0) {
|
|
77
87
|
RAY_MAP_WORKERS = [...Array(NUMBER_OF_CORES)]
|
|
78
|
-
.map(() => new MyWorker(
|
|
88
|
+
.map(() => new MyWorker(`./Camera/rayMapWorker.js`));
|
|
79
89
|
}
|
|
80
90
|
const w = canvas.width;
|
|
81
91
|
const h = canvas.height;
|
|
@@ -83,14 +93,18 @@ export function rayMapWorkers(camera, scene, canvas, lambda, vars = [], dependen
|
|
|
83
93
|
const isNewScene = prevSceneHash !== newHash;
|
|
84
94
|
if (isNewScene) {
|
|
85
95
|
prevSceneHash = newHash;
|
|
86
|
-
serializedScene = scene
|
|
96
|
+
serializedScene = scene?.serialize()
|
|
97
|
+
prevScene = serializedScene;
|
|
87
98
|
} else {
|
|
88
99
|
serializedScene = undefined;
|
|
89
100
|
}
|
|
90
101
|
return RAY_MAP_WORKERS.map((worker, k) => {
|
|
91
102
|
return new Promise((resolve) => {
|
|
103
|
+
let timerId = undefined;
|
|
92
104
|
worker.onMessage(message => {
|
|
93
|
-
const { image, startRow, endRow, } = message;
|
|
105
|
+
const { image, startRow, endRow, hasScene } = message;
|
|
106
|
+
prevScene = hasScene ? undefined : prevScene;
|
|
107
|
+
if (!IS_NODE) clearTimeout(timerId);
|
|
94
108
|
let index = 0;
|
|
95
109
|
const startIndex = CHANNELS * w * startRow;
|
|
96
110
|
const endIndex = CHANNELS * w * endRow;
|
|
@@ -110,16 +124,18 @@ export function rayMapWorkers(camera, scene, canvas, lambda, vars = [], dependen
|
|
|
110
124
|
endRow: Math.min(h, (k + 1) * ratio),
|
|
111
125
|
camera: camera.serialize(),
|
|
112
126
|
dependencies: dependencies.map(d => d.toString()),
|
|
113
|
-
scene: serializedScene
|
|
127
|
+
scene: isNewScene ? serializedScene : prevScene !== undefined ? prevScene : undefined
|
|
114
128
|
};
|
|
129
|
+
worker.postMessage(message);
|
|
115
130
|
if (isFirstTimeCounter > 0 && !IS_NODE) {
|
|
116
131
|
// hack to work in the browser, don't know why it works
|
|
117
132
|
isFirstTimeCounter--;
|
|
118
|
-
setTimeout(() =>
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
timerId = setTimeout(() => {
|
|
134
|
+
console.log("TIMEOUT!!")
|
|
135
|
+
// doesn't block promise
|
|
136
|
+
resolve();
|
|
137
|
+
}, ERROR_MSG_TIMEOUT);
|
|
121
138
|
}
|
|
122
|
-
|
|
123
139
|
});
|
|
124
140
|
})
|
|
125
141
|
}
|
package/src/Camera/raster.js
CHANGED
|
@@ -46,7 +46,6 @@ export function rasterGraphics(scene, camera, params = {}) {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
canvas.paint();
|
|
50
49
|
return canvas;
|
|
51
50
|
}
|
|
52
51
|
}
|
|
@@ -80,8 +79,8 @@ function rasterSphere({ canvas, camera, elem, w, h, zBuffer }) {
|
|
|
80
79
|
const texColor = getTexColor(texCoord, texture);
|
|
81
80
|
finalColor = finalColor.add(texColor).scale(1 / 2);
|
|
82
81
|
}
|
|
83
|
-
for (let
|
|
84
|
-
for (let
|
|
82
|
+
for (let l = -intRadius; l < intRadius; l++) {
|
|
83
|
+
for (let k = -intRadius; k < intRadius; k++) {
|
|
85
84
|
const xl = Math.max(0, Math.min(w - 1, x + k));
|
|
86
85
|
const yl = Math.floor(y + l);
|
|
87
86
|
const squareLength = k * k + l * l;
|
|
@@ -64,18 +64,12 @@ if (IS_NODE) {
|
|
|
64
64
|
parentPort.on("message", async message => {
|
|
65
65
|
const input = message;
|
|
66
66
|
const output = await main(input);
|
|
67
|
-
parentPort.postMessage(output);
|
|
67
|
+
parentPort.postMessage({...output, hasScene: scene !== undefined});
|
|
68
68
|
});
|
|
69
69
|
} else {
|
|
70
70
|
self.onmessage = async message => {
|
|
71
71
|
const input = message.data;
|
|
72
72
|
const output = await main(input);
|
|
73
|
-
postMessage(output);
|
|
73
|
+
postMessage({...output, hasScene: scene !== undefined});
|
|
74
74
|
};
|
|
75
|
-
|
|
76
|
-
self.onerror = e => {
|
|
77
|
-
console.log(`Caught error inside ray map worker ${e}`)
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
|
|
81
75
|
}
|
|
@@ -47,13 +47,13 @@ if (IS_NODE) {
|
|
|
47
47
|
parentPort.on("message", async message => {
|
|
48
48
|
const input = message;
|
|
49
49
|
const output = await main(input);
|
|
50
|
-
parentPort.postMessage(output);
|
|
50
|
+
parentPort.postMessage({ ...output, hasScene: scene !== undefined });
|
|
51
51
|
});
|
|
52
52
|
} else {
|
|
53
53
|
onmessage = async message => {
|
|
54
54
|
const input = message.data;
|
|
55
55
|
const output = await main(input);
|
|
56
|
-
postMessage(output);
|
|
56
|
+
postMessage({ ...output, hasScene: scene !== undefined });
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
onerror = e => console.log("Caught error on rayTrace worker", e);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Vec2 } from "../Vector/Vector.js"
|
|
2
|
+
import Box from "../Geometry/Box.js";
|
|
3
|
+
import Sphere from "../Geometry/Sphere.js";
|
|
4
|
+
import Line from "../Geometry/Line.js";
|
|
5
|
+
import Triangle from "../Geometry/Triangle.js";
|
|
6
|
+
|
|
7
|
+
export default class Camera2D {
|
|
8
|
+
constructor(box = new Box(Vec2(), Vec2(1, 1))) {
|
|
9
|
+
this.box = box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
map(lambda) {
|
|
13
|
+
return {
|
|
14
|
+
to: canvas => {
|
|
15
|
+
const w = canvas.width;
|
|
16
|
+
const invW = 1 / w;
|
|
17
|
+
const h = canvas.height;
|
|
18
|
+
const invH = 1 / h;
|
|
19
|
+
const ans = canvas.map((x, y) => {
|
|
20
|
+
let p = Vec2(x, y).mul(Vec2(invW, invH));
|
|
21
|
+
p = p.mul(this.box.diagonal).add(this.box.min);
|
|
22
|
+
return lambda(p);
|
|
23
|
+
});
|
|
24
|
+
return ans;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
mapBox(lambda, boxInWorld) {
|
|
30
|
+
return {
|
|
31
|
+
to: canvas => {
|
|
32
|
+
const cameraBoxInCanvasCoords = new Box(this.toCanvasCoord(boxInWorld.min, canvas), this.toCanvasCoord(boxInWorld.max, canvas));
|
|
33
|
+
return canvas.mapBox((x, y) => {
|
|
34
|
+
// (x,y) \in [0,cameraBox.width] x [0, cameraBox.height]
|
|
35
|
+
let p = Vec2(x, y).div(cameraBoxInCanvasCoords.diagonal).mul(boxInWorld.diagonal).add(boxInWorld.min);
|
|
36
|
+
return lambda(p);
|
|
37
|
+
}, cameraBoxInCanvasCoords);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
raster(scene) {
|
|
43
|
+
const type2render = {
|
|
44
|
+
[Sphere.name]: rasterCircle,
|
|
45
|
+
[Line.name]: rasterLine,
|
|
46
|
+
[Triangle.name]: rasterTriangle,
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
to: tela => {
|
|
50
|
+
const elements = scene.getElements();
|
|
51
|
+
for (let i = 0; i < elements.length; i++) {
|
|
52
|
+
const element = elements[i];
|
|
53
|
+
const rasterizer = type2render[element.constructor.name];
|
|
54
|
+
if (rasterizer) {
|
|
55
|
+
rasterizer(element, this, tela);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return tela;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
serialize() {
|
|
64
|
+
return {
|
|
65
|
+
box: this.box.serialize(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toCanvasCoord(p, canvas) {
|
|
70
|
+
return p.sub(this.box.min).div(this.box.diagonal).mul(Vec2(canvas.width, canvas.height));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toWorldCoord(x, canvas) {
|
|
74
|
+
const size = Vec2(canvas.width, canvas.height);
|
|
75
|
+
return x.div(size).mul(this.box.diagonal)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static deserialize(json) {
|
|
79
|
+
return new Camera2D(Box.deserialize(json.box));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
function rasterCircle(circle, camera, canvas) {
|
|
85
|
+
const centerInCanvas = camera.toCanvasCoord(circle.position, canvas);
|
|
86
|
+
const radius = camera.toCanvasCoord(circle.position.add(Vec2(circle.radius, 0)), canvas).x - centerInCanvas.x;
|
|
87
|
+
return canvas.drawCircle(centerInCanvas, radius, () => {
|
|
88
|
+
return circle.color;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function rasterLine(line, camera, canvas) {
|
|
93
|
+
const positionsInCanvas = line.positions.map(p => camera.toCanvasCoord(p, canvas));
|
|
94
|
+
return canvas.drawLine(
|
|
95
|
+
positionsInCanvas[0],
|
|
96
|
+
positionsInCanvas[1],
|
|
97
|
+
() => {
|
|
98
|
+
return line.colors[0]
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rasterTriangle(triangle, camera, canvas) {
|
|
104
|
+
const positionsInCanvas = triangle.positions.map(p => camera.toCanvasCoord(p, canvas).map(Math.floor));
|
|
105
|
+
return canvas.drawTriangle(
|
|
106
|
+
positionsInCanvas[0],
|
|
107
|
+
positionsInCanvas[1],
|
|
108
|
+
positionsInCanvas[2],
|
|
109
|
+
() => {
|
|
110
|
+
return triangle.colors[0];
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
}
|
package/src/Geometry/Box.js
CHANGED
|
@@ -31,7 +31,9 @@ export default class Box {
|
|
|
31
31
|
for (let i = 0; i < n; i++) {
|
|
32
32
|
grad.push(this.distanceToPoint(pointVec.add(Vec.e(n)(i).scale(epsilon))) - d)
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
let sign = Math.sign(d);
|
|
35
|
+
sign = sign === 0 ? 1 : sign;
|
|
36
|
+
return Vec.fromArray(grad).scale(sign).normalize();
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
interceptWithRay(ray) {
|
|
@@ -107,7 +109,7 @@ export default class Box {
|
|
|
107
109
|
|
|
108
110
|
equals(box) {
|
|
109
111
|
if (!(box instanceof Box)) return false;
|
|
110
|
-
if (this
|
|
112
|
+
if (this.isEmpty !== box.isEmpty) return false;
|
|
111
113
|
return this.min.equals(box.min) && this.max.equals(box.max);
|
|
112
114
|
}
|
|
113
115
|
|
|
@@ -130,6 +132,11 @@ export default class Box {
|
|
|
130
132
|
return false;
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
contains(box, precision = 1e-6) {
|
|
136
|
+
const v = box.volume();
|
|
137
|
+
return Math.abs(v - this.sub(box).volume()) < precision;
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
toString() {
|
|
134
141
|
return `{
|
|
135
142
|
min:${this.min.toString()},
|
|
@@ -141,6 +148,11 @@ export default class Box {
|
|
|
141
148
|
return this.min.add(Vec.RANDOM(this.dim).mul(this.diagonal));
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
volume() {
|
|
152
|
+
if(this.isEmpty) return 0;
|
|
153
|
+
return this.diagonal.fold((e, x) => e * x, 1);
|
|
154
|
+
}
|
|
155
|
+
|
|
144
156
|
serialize() {
|
|
145
157
|
return {
|
|
146
158
|
type: Box.name,
|
package/src/Geometry/Line.js
CHANGED
|
@@ -3,6 +3,7 @@ import Color from "../Color/Color.js";
|
|
|
3
3
|
import { Diffuse, MATERIALS } from "../Material/Material.js";
|
|
4
4
|
import { clamp } from "../Utils/Math.js";
|
|
5
5
|
import Vec, { Vec2, Vec3 } from "../Vector/Vector.js";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
6
7
|
|
|
7
8
|
export default class Line {
|
|
8
9
|
constructor({ name, positions, colors, texCoords, normals, texture, radius, emissive, material }) {
|
|
@@ -111,7 +112,7 @@ export default class Line {
|
|
|
111
112
|
const indx = [1, 2];
|
|
112
113
|
class LineBuilder {
|
|
113
114
|
constructor() {
|
|
114
|
-
this._name;
|
|
115
|
+
this._name = randomUUID();
|
|
115
116
|
this._texture;
|
|
116
117
|
this._radius = 1;
|
|
117
118
|
this._normals = indx.map(() => Vec3());
|
package/src/Geometry/Sphere.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Diffuse, MATERIALS } from "../Material/Material.js";
|
|
|
4
4
|
import { randomPointInSphere } from "../Utils/Math.js";
|
|
5
5
|
import Vec, { Vec2, Vec3 } from "../Vector/Vector.js";
|
|
6
6
|
import { deserialize as deserializeImage } from "../Tela/utils.js";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
7
8
|
|
|
8
9
|
class Sphere {
|
|
9
10
|
constructor({ name, position, color, texCoord, normal, radius, texture, emissive, material }) {
|
|
@@ -89,7 +90,7 @@ class Sphere {
|
|
|
89
90
|
|
|
90
91
|
class SphereBuilder {
|
|
91
92
|
constructor() {
|
|
92
|
-
this._name;
|
|
93
|
+
this._name = randomUUID();
|
|
93
94
|
this._texture;
|
|
94
95
|
this._radius = 1;
|
|
95
96
|
this._normal = Vec3();
|
|
@@ -118,7 +119,7 @@ class SphereBuilder {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
radius(radius) {
|
|
121
|
-
if (
|
|
122
|
+
if (radius === undefined) return this;
|
|
122
123
|
this._radius = radius;
|
|
123
124
|
return this;
|
|
124
125
|
}
|
package/src/Geometry/Triangle.js
CHANGED
|
@@ -3,6 +3,7 @@ import Color from "../Color/Color.js";
|
|
|
3
3
|
import { Diffuse, MATERIALS } from "../Material/Material.js";
|
|
4
4
|
import Vec, { Vec2, Vec3 } from "../Vector/Vector.js";
|
|
5
5
|
import { deserialize as deserializeImage } from "../Tela/utils.js";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
6
7
|
|
|
7
8
|
export default class Triangle {
|
|
8
9
|
constructor({ name, positions, colors, texCoords, normals, texture, emissive, material }) {
|
|
@@ -22,7 +23,8 @@ export default class Triangle {
|
|
|
22
23
|
this.tangents = [this.edges[0], this.edges.at(-1).scale(-1)];
|
|
23
24
|
const u = this.tangents[0];
|
|
24
25
|
const v = this.tangents[1];
|
|
25
|
-
|
|
26
|
+
const cross = u.cross(v);
|
|
27
|
+
this.faceNormal = Number.isFinite(cross) ? Vec3(0, 0, cross) : cross.normalize();
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
getBoundingBox() {
|
|
@@ -104,7 +106,7 @@ export default class Triangle {
|
|
|
104
106
|
const indx = [1, 2, 3];
|
|
105
107
|
class TriangleBuilder {
|
|
106
108
|
constructor() {
|
|
107
|
-
this._name;
|
|
109
|
+
this._name = randomUUID();
|
|
108
110
|
this._texture;
|
|
109
111
|
this._normals = indx.map(() => Vec3());
|
|
110
112
|
this._colors = indx.map(() => Color.BLACK);
|
package/src/IO/IO.js
CHANGED
|
@@ -90,7 +90,7 @@ export function saveImageStreamToVideo(fileAddress, streamWithImages, { imageGet
|
|
|
90
90
|
s = await s.tail;
|
|
91
91
|
}
|
|
92
92
|
if (!fps) fps = ite / time;
|
|
93
|
-
execSync(`ffmpeg -framerate ${fps} -i ${fileName}_%d.ppm -y ${fileName}.${extension}`);
|
|
93
|
+
execSync(`ffmpeg -framerate ${fps} -i ${fileName}_%d.ppm -y -c:v libx264 -crf 20 -preset medium -profile:v baseline -level 3.0 -pix_fmt yuv420p -movflags +faststart ${fileName}.${extension}`);
|
|
94
94
|
for (let i = 0; i < ite; i++) {
|
|
95
95
|
unlinkSync(`${fileName}_${i}.ppm`);
|
|
96
96
|
}
|
|
@@ -114,6 +114,7 @@ export function saveParallelImageStreamToVideo(fileAddress, parallelStreamOfImag
|
|
|
114
114
|
Vec,
|
|
115
115
|
Vec2,
|
|
116
116
|
Vec3,
|
|
117
|
+
Ray,
|
|
117
118
|
Mesh,
|
|
118
119
|
Color,
|
|
119
120
|
Image,
|
package/src/Scene/KScene.js
CHANGED
|
@@ -32,14 +32,9 @@ export default class KScene extends NaiveScene {
|
|
|
32
32
|
this.boundingBoxScene = new Node(this.k);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
distanceToPoint(p) {
|
|
35
|
+
distanceToPoint(p, combineLeafs = Math.min) {
|
|
36
36
|
if (this.boundingBoxScene.leafs.length > 0) {
|
|
37
|
-
|
|
38
|
-
const leafs = this.boundingBoxScene.leafs
|
|
39
|
-
for (let i = 0; i < leafs.length; i++) {
|
|
40
|
-
distance = Math.min(distance, leafs[i].element.distanceToPoint(p));
|
|
41
|
-
}
|
|
42
|
-
return distance;
|
|
37
|
+
return distanceFromLeafs(this.boundingBoxScene.leafs, p, combineLeafs);
|
|
43
38
|
}
|
|
44
39
|
return this.getElementNear(p).distanceToPoint(p);
|
|
45
40
|
}
|
|
@@ -57,7 +52,7 @@ export default class KScene extends NaiveScene {
|
|
|
57
52
|
normal = normal.add(n.scale(d));
|
|
58
53
|
weight += d;
|
|
59
54
|
}
|
|
60
|
-
return normal.length() > 0 ? normal.scale(1 / weight).normalize() :
|
|
55
|
+
return normal.length() > 0 ? normal.scale(1 / weight).normalize() : super.normalToPoint(p);
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
interceptWithRay(ray) {
|
|
@@ -225,6 +220,7 @@ class Node {
|
|
|
225
220
|
}
|
|
226
221
|
|
|
227
222
|
distanceToPoint(p) {
|
|
223
|
+
if(!this.left && !this.right) return Number.MAX_VALUE;
|
|
228
224
|
return this.getElementNear(p).distanceToPoint(p);
|
|
229
225
|
}
|
|
230
226
|
|
|
@@ -234,7 +230,7 @@ class Node {
|
|
|
234
230
|
}
|
|
235
231
|
const leftT = this.left?.box?.interceptWithRay(ray)?.[0] ?? Number.MAX_VALUE;
|
|
236
232
|
const rightT = this.right?.box?.interceptWithRay(ray)?.[0] ?? Number.MAX_VALUE;
|
|
237
|
-
if (leftT === Number.MAX_VALUE && rightT === Number.MAX_VALUE) return
|
|
233
|
+
if (leftT === Number.MAX_VALUE && rightT === Number.MAX_VALUE) return this.distanceToPoint(ray.init);
|
|
238
234
|
const first = leftT <= rightT ? this.left : this.right;
|
|
239
235
|
const second = leftT > rightT ? this.left : this.right;
|
|
240
236
|
const firstT = Math.min(leftT, rightT);
|
package/src/Scene/NaiveScene.js
CHANGED
package/src/Scene/VoxelScene.js
CHANGED
|
@@ -52,13 +52,17 @@ export default class VoxelScene extends NaiveScene {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
normalToPoint(p) {
|
|
55
|
+
let weight = 0;
|
|
55
56
|
let normal = Vec3();
|
|
56
57
|
const elements = Object.values(this.gridMap[hash(p, this.gridSpace)] || {});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const size = elements.length;
|
|
59
|
+
for (let i = 0; i < size; i++) {
|
|
60
|
+
const n = elements[i].normalToPoint(p);
|
|
61
|
+
const d = 1 / elements[i].distanceToPoint(p);
|
|
62
|
+
normal = normal.add(n.scale(d));
|
|
63
|
+
weight += d;
|
|
60
64
|
}
|
|
61
|
-
return normal.length() > 0 ? normal.normalize() :
|
|
65
|
+
return normal.length() > 0 ? normal.scale(1 / weight).normalize() : super.normalToPoint(p);
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
interceptWithRay(ray) {
|
package/src/Tela/Tela.js
CHANGED
|
@@ -118,7 +118,7 @@ export default class Tela {
|
|
|
118
118
|
if (line.length <= 1) return;
|
|
119
119
|
const [pi, pf] = line;
|
|
120
120
|
const v = pf.sub(pi);
|
|
121
|
-
const n = v.map(Math.abs).fold((e, x) => e + x);
|
|
121
|
+
const n = v.map(Math.abs).fold((e, x) => e + x) + 5;
|
|
122
122
|
for (let k = 0; k < n; k++) {
|
|
123
123
|
const s = k / n;
|
|
124
124
|
const lineP = pi.add(v.scale(s)).map(Math.floor);
|
package/src/Tela/Window.js
CHANGED
|
@@ -114,7 +114,8 @@ export default class Window extends Tela {
|
|
|
114
114
|
//========================================================================================
|
|
115
115
|
|
|
116
116
|
function handleMouse(canvas, lambda) {
|
|
117
|
-
return (
|
|
118
|
-
|
|
117
|
+
return (e) => {
|
|
118
|
+
const { x, y } = e;
|
|
119
|
+
return lambda(x, canvas.height - 1 - y, e);
|
|
119
120
|
}
|
|
120
121
|
}
|
package/src/Tela/parallel.js
CHANGED
|
@@ -8,8 +8,9 @@ import { MyWorker } from "../Utils/Utils.js";
|
|
|
8
8
|
//========================================================================================
|
|
9
9
|
|
|
10
10
|
let WORKERS = [];
|
|
11
|
+
const ERROR_MSG_TIMEOUT = 1000;
|
|
11
12
|
let isFirstTimeCounter = NUMBER_OF_CORES;
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
//========================================================================================
|
|
14
15
|
/* *
|
|
15
16
|
* MAIN *
|
|
@@ -20,14 +21,16 @@ export function parallelWorkers(tela, lambda, dependencies = [], vars = []) {
|
|
|
20
21
|
// lazy loading workers
|
|
21
22
|
if (WORKERS.length === 0) {
|
|
22
23
|
WORKERS = [...Array(NUMBER_OF_CORES)]
|
|
23
|
-
.map(() => new MyWorker(
|
|
24
|
+
.map(() => new MyWorker(`./Tela/telaWorker.js`));
|
|
24
25
|
}
|
|
25
26
|
const w = tela.width;
|
|
26
27
|
const h = tela.height;
|
|
27
28
|
return WORKERS.map((worker, k) => {
|
|
29
|
+
let timerId = undefined;
|
|
28
30
|
return new Promise((resolve) => {
|
|
29
31
|
worker.onMessage(message => {
|
|
30
32
|
const { image, startRow, endRow, } = message;
|
|
33
|
+
if (!IS_NODE) clearTimeout(timerId);
|
|
31
34
|
let index = 0;
|
|
32
35
|
const startIndex = CHANNELS * w * startRow;
|
|
33
36
|
const endIndex = CHANNELS * w * endRow;
|
|
@@ -46,12 +49,15 @@ export function parallelWorkers(tela, lambda, dependencies = [], vars = []) {
|
|
|
46
49
|
__endRow: Math.min(h, (k + 1) * ratio),
|
|
47
50
|
__dependencies: dependencies.map(d => d.toString()),
|
|
48
51
|
};
|
|
52
|
+
worker.postMessage(message);
|
|
49
53
|
if (isFirstTimeCounter > 0 && !IS_NODE) {
|
|
50
54
|
// hack to work in the browser, don't know why it works
|
|
51
55
|
isFirstTimeCounter--;
|
|
52
|
-
setTimeout(() =>
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
timerId = setTimeout(() => {
|
|
57
|
+
console.log("TIMEOUT!!")
|
|
58
|
+
// doesn't block promise
|
|
59
|
+
resolve();
|
|
60
|
+
}, ERROR_MSG_TIMEOUT);
|
|
55
61
|
}
|
|
56
62
|
});
|
|
57
63
|
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export default class Anima {
|
|
2
|
+
constructor(sequenceOfBehaviors = []) {
|
|
3
|
+
let acc = 0;
|
|
4
|
+
this.sequence = sequenceOfBehaviors.map(b => {
|
|
5
|
+
const ans = { ...b, start: acc, end: acc + b.duration };
|
|
6
|
+
acc = ans.end;
|
|
7
|
+
return ans;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
anime(t, dt) {
|
|
12
|
+
let s = 0;
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (s < t && i < this.sequence.length) {
|
|
15
|
+
s += this.sequence[i].duration;
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
const index = Math.max(0, i - 1);
|
|
19
|
+
const prevBehavior = this.sequence[index];
|
|
20
|
+
let tau = t - prevBehavior.start;
|
|
21
|
+
if (i >= this.sequence.length) tau = Math.min(tau, prevBehavior.duration); // end animation
|
|
22
|
+
if (Math.abs(tau - prevBehavior.duration) < dt) {
|
|
23
|
+
tau = prevBehavior.duration
|
|
24
|
+
}
|
|
25
|
+
return prevBehavior.behavior(tau, dt);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
loop(t, dt) {
|
|
29
|
+
if (this.sequence.length === 0) return;
|
|
30
|
+
const maxT = this.sequence.at(-1).end;
|
|
31
|
+
return this.anime(t % maxT, dt);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
animeTime() {
|
|
35
|
+
return this.sequence.at(-1).end;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
*
|
|
40
|
+
* @param {(t, dt) => any} lambda
|
|
41
|
+
* @param {time in seconds} duration
|
|
42
|
+
*/
|
|
43
|
+
static behavior(lambda, duration) {
|
|
44
|
+
return { behavior: lambda, duration };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static wait(duration) {
|
|
48
|
+
return { behavior: () => { }, duration };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static list(...args) {
|
|
52
|
+
return new Anima(args);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
package/src/Utils/Constants.js
CHANGED
|
@@ -5,6 +5,3 @@ export const IS_NODE = typeof process !== 'undefined' && Boolean(process.version
|
|
|
5
5
|
export const NUMBER_OF_CORES = IS_NODE ?
|
|
6
6
|
(await import("node:os")).cpus().length :
|
|
7
7
|
navigator.hardwareConcurrency;
|
|
8
|
-
|
|
9
|
-
export const IS_GITHUB = typeof window !== "undefined" && (window.location.host || window.LOCATION_HOST) === "pedroth.github.io";
|
|
10
|
-
export const SOURCE = IS_GITHUB ? "/tela.js" : "";
|
package/src/Utils/Math.js
CHANGED
|
@@ -45,4 +45,18 @@ export function randomPointInSphere(dim) {
|
|
|
45
45
|
break;
|
|
46
46
|
}
|
|
47
47
|
return randomInSphere;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
export function orthoBasisFrom(...vectors) {
|
|
52
|
+
const basis = [];
|
|
53
|
+
const n = vectors.length;
|
|
54
|
+
for (let i = 0; i < n; i++) {
|
|
55
|
+
let v = vectors[i];
|
|
56
|
+
for (let j = 0; j < basis.length; j++) {
|
|
57
|
+
v = v.sub(basis[j].scale(v.dot(basis[j])))
|
|
58
|
+
}
|
|
59
|
+
basis.push(v.normalize());
|
|
60
|
+
}
|
|
61
|
+
return basis;
|
|
48
62
|
}
|
package/src/Utils/SVG.js
CHANGED
|
@@ -334,7 +334,7 @@ const eatAllSpacesChars = eatWhile(p => p.type === " " || p.type === "\t" || p.t
|
|
|
334
334
|
* number -> -D.D / D.D / -D / D
|
|
335
335
|
* D -> [0-9]D / ε
|
|
336
336
|
*/
|
|
337
|
-
|
|
337
|
+
function parseSvgPath(svgPath) {
|
|
338
338
|
const { left: path, } = parsePath(stream(svgPath));
|
|
339
339
|
return path;
|
|
340
340
|
}
|
|
@@ -471,8 +471,8 @@ function finishPath(keyPointPath, svg, id, path) {
|
|
|
471
471
|
svg.defPaths[id] = [];
|
|
472
472
|
svg.defKeyPointPaths[id] = [];
|
|
473
473
|
}
|
|
474
|
-
svg.defPaths[id].push(path);
|
|
475
|
-
svg.defKeyPointPaths[id].push(keyPointPath);
|
|
474
|
+
svg.defPaths[id].push(cleanPath(path));
|
|
475
|
+
svg.defKeyPointPaths[id].push(cleanPath(keyPointPath));
|
|
476
476
|
path = [];
|
|
477
477
|
keyPointPath = [];
|
|
478
478
|
}
|
|
@@ -728,11 +728,33 @@ function readPath(svg, tagNode) {
|
|
|
728
728
|
svg.defPaths[id] = [];
|
|
729
729
|
svg.defKeyPointPaths[id] = [];
|
|
730
730
|
}
|
|
731
|
-
svg.defPaths[id].push(path);
|
|
732
|
-
svg.defKeyPointPaths[id].push(keyPointPath);
|
|
731
|
+
svg.defPaths[id].push(cleanPath(path));
|
|
732
|
+
svg.defKeyPointPaths[id].push(cleanPath(keyPointPath));
|
|
733
733
|
}
|
|
734
734
|
}
|
|
735
735
|
|
|
736
|
+
function readRect(svg, tagNode, transform = transformBuilder()) {
|
|
737
|
+
const attrs = tagNode?.Attrs?.attributes;
|
|
738
|
+
const [idObj] = attrs.filter(a => a.attributeName === "id");
|
|
739
|
+
const id = idObj?.attributeValue ?? generateUniqueID(5);
|
|
740
|
+
let width = undefined;
|
|
741
|
+
let height = undefined;
|
|
742
|
+
let x = undefined;
|
|
743
|
+
let y = undefined;
|
|
744
|
+
attrs.forEach(({ attributeName, attributeValue }) => {
|
|
745
|
+
if ("width" === attributeName) width = Number.parseFloat(attributeValue)
|
|
746
|
+
if ("height" === attributeName) height = Number.parseFloat(attributeValue)
|
|
747
|
+
if ("x" === attributeName) x = Number.parseFloat(attributeValue)
|
|
748
|
+
if ("y" === attributeName) y = Number.parseFloat(attributeValue)
|
|
749
|
+
})
|
|
750
|
+
const p = Vec2(x, y)
|
|
751
|
+
let path = [p, p.add(Vec2(width, 0)), p.add(Vec2(width, height)), p.add(Vec2(0, height)), p];
|
|
752
|
+
path = path.map(transform)
|
|
753
|
+
finishPath(path, svg, id, path)
|
|
754
|
+
svg.paths.push([path])
|
|
755
|
+
svg.keyPointPaths.push([path])
|
|
756
|
+
}
|
|
757
|
+
|
|
736
758
|
const transformBuilder = (a = 1, b = 0, c = 0, d = 1, e = 0, f = 0) => x => Vec2(a, b).scale(x.x).add(Vec2(c, d).scale(x.y)).add(Vec2(e, f));
|
|
737
759
|
const dot = (f, g) => x => f(g(x));
|
|
738
760
|
|
|
@@ -742,16 +764,19 @@ function readTransform(svg, transformNode, transform = transformBuilder()) {
|
|
|
742
764
|
.attributes
|
|
743
765
|
.filter(x => x.attributeName === "transform")
|
|
744
766
|
.forEach(({ attributeValue }) => {
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
767
|
+
const multiTransforms = attributeValue.includes("matrix") ? [attributeValue] : attributeValue.split(" ");
|
|
768
|
+
multiTransforms.forEach(T => {
|
|
769
|
+
const params = T.match(/-?\d+\.?\d*/g).map(Number);
|
|
770
|
+
if (T.includes("matrix")) {
|
|
771
|
+
transform = dot(transform, transformBuilder(...params));
|
|
772
|
+
}
|
|
773
|
+
if (T.includes("scale")) {
|
|
774
|
+
transform = dot(transform, transformBuilder(params[0], 0, 0, (params[1] ?? params[0]), 0, 0))
|
|
775
|
+
}
|
|
776
|
+
if (T.includes("translate")) {
|
|
777
|
+
transform = dot(transform, transformBuilder(1, 0, 0, 1, ...params))
|
|
778
|
+
}
|
|
779
|
+
})
|
|
755
780
|
})
|
|
756
781
|
const nodeStack = [...(transformNode?.InnerSVG?.innerSvgs?.map(x => x.SVG) ?? [])];
|
|
757
782
|
while (nodeStack.length > 0) {
|
|
@@ -795,6 +820,12 @@ function readTransform(svg, transformNode, transform = transformBuilder()) {
|
|
|
795
820
|
svg.keyPointPaths.push(svg.defKeyPointPaths[useParams.id].map(paths => paths.map(useParams.transform)));
|
|
796
821
|
}
|
|
797
822
|
}
|
|
823
|
+
if (tag === "path") {
|
|
824
|
+
readPath(svg, currentNode.EmptyTag ?? currentNode.StartTag, transform);
|
|
825
|
+
}
|
|
826
|
+
if (tag === "rect") {
|
|
827
|
+
readRect(svg, currentNode.EmptyTag ?? currentNode.StartTag, transform)
|
|
828
|
+
}
|
|
798
829
|
nodeStack.push(...(currentNode?.InnerSVG?.innerSvgs?.map(x => x.SVG) ?? []));
|
|
799
830
|
}
|
|
800
831
|
|
|
@@ -850,15 +881,49 @@ function readSVGNode(svgNode) {
|
|
|
850
881
|
if (tag === "path") {
|
|
851
882
|
readPath(svg, currentNode.EmptyTag ?? currentNode.StartTag);
|
|
852
883
|
}
|
|
884
|
+
if (tag === "rect") {
|
|
885
|
+
readRect(svg, currentNode.EmptyTag ?? currentNode.StartTag)
|
|
886
|
+
}
|
|
853
887
|
nodeStack.push(...(currentNode?.InnerSVG?.innerSvgs?.map(x => x.SVG) ?? []));
|
|
854
888
|
}
|
|
855
889
|
if (svg.paths.length === 0) {
|
|
856
890
|
Object.values(svg.defPaths).forEach(paths => svg.paths.push(paths));
|
|
857
891
|
Object.values(svg.defKeyPointPaths).forEach(paths => svg.keyPointPaths.push(paths));
|
|
858
892
|
}
|
|
893
|
+
|
|
894
|
+
function normalize(path) {
|
|
895
|
+
return path.map(x => {
|
|
896
|
+
const { min, max } = svg.viewBox;
|
|
897
|
+
const diagonal = max.sub(min);
|
|
898
|
+
let p = x.sub(min).div(diagonal);
|
|
899
|
+
p = Vec2(p.x, -p.y).add(Vec2(0, 1));
|
|
900
|
+
return p;
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
svg.normalize = () => {
|
|
904
|
+
const ans = {
|
|
905
|
+
width: svg.width,
|
|
906
|
+
height: svg.height,
|
|
907
|
+
viewBox: { min: Vec2(), max: Vec2(1, 1) },
|
|
908
|
+
paths: svg.paths.map(paths => paths.map(path => normalize(path))),
|
|
909
|
+
keyPointPaths: svg.keyPointPaths.map(paths => paths.map(path => normalize(path)))
|
|
910
|
+
}
|
|
911
|
+
return ans;
|
|
912
|
+
}
|
|
859
913
|
return svg;
|
|
860
914
|
}
|
|
861
915
|
|
|
916
|
+
function cleanPath(path) {
|
|
917
|
+
const epsilon = 1e-6;
|
|
918
|
+
const cleanPath = [];
|
|
919
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
920
|
+
if (!cleanPath.some(x => x.sub(path[i]).length() < epsilon)) {
|
|
921
|
+
cleanPath.push(path[i]);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
cleanPath.push(path.at(-1));
|
|
925
|
+
return cleanPath;
|
|
926
|
+
}
|
|
862
927
|
|
|
863
928
|
//========================================================================================
|
|
864
929
|
/* *
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import Box from "../Geometry/Box.js";
|
|
2
|
+
|
|
3
|
+
export function triangulate(paths) {
|
|
4
|
+
if (paths.length === 0) return [];
|
|
5
|
+
const boxes = paths.map(path => path.reduce((e, x) => e.add(new Box(x, x)), new Box()));
|
|
6
|
+
const boxTrees = createBoxHierarchy(boxes, paths);
|
|
7
|
+
const triangles = []
|
|
8
|
+
boxTrees.forEach((boxTree) => {
|
|
9
|
+
triangles.push(...triangulateBoxTree(boxTree));
|
|
10
|
+
})
|
|
11
|
+
return triangles;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* boxes and paths must have same size
|
|
16
|
+
*/
|
|
17
|
+
function createBoxHierarchy(boxes, paths) {
|
|
18
|
+
const trees = boxes.map((b, i) => ({ box: b, path: paths[i], children: [] }));
|
|
19
|
+
const ans = [];
|
|
20
|
+
for (let i = 0; i < trees.length; i++) {
|
|
21
|
+
let left = trees[i];
|
|
22
|
+
for (let j = i + 1; j < trees.length; j++) {
|
|
23
|
+
const right = trees[j];
|
|
24
|
+
const intersect = left.box.intersection(right.box);
|
|
25
|
+
if (intersect.equals(right.box)) {
|
|
26
|
+
left.children.push(right);
|
|
27
|
+
}
|
|
28
|
+
if (intersect.equals(left.box)) {
|
|
29
|
+
right.children.push(left)
|
|
30
|
+
left = right;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!ans.some(x => x.box.contains(left.box))) ans.push(left);
|
|
34
|
+
}
|
|
35
|
+
if (ans.length === 0) ans.push(trees[0])
|
|
36
|
+
return ans;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function triangulateBoxTree(boxTree) {
|
|
40
|
+
const triangles = [];
|
|
41
|
+
let path = [...joinBoxTreePaths(boxTree)];
|
|
42
|
+
let i = 0;
|
|
43
|
+
let samePath = 1e6;
|
|
44
|
+
while (path.length > 3 && samePath > 0) {
|
|
45
|
+
i = Math.floor(Math.random() * path.length);
|
|
46
|
+
const prevIndex = mod(i - 1, path.length);
|
|
47
|
+
const nextIndex = mod(i + 1, path.length);
|
|
48
|
+
const prev = path[prevIndex];
|
|
49
|
+
const next = path[nextIndex];
|
|
50
|
+
const c = path[i];
|
|
51
|
+
const u = next.sub(c);
|
|
52
|
+
const v = prev.sub(c);
|
|
53
|
+
const uWedgeV = u.cross(v);
|
|
54
|
+
if (uWedgeV > 0) {
|
|
55
|
+
samePath--;
|
|
56
|
+
// i = (i + 1) % path.length;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let havePointsInside = false;
|
|
61
|
+
for (let j = 0; j < path.length; j++) {
|
|
62
|
+
if (j === i || j === prevIndex || j === nextIndex) continue;
|
|
63
|
+
const p = path[j].sub(c);
|
|
64
|
+
const pWedgeV = p.cross(v);
|
|
65
|
+
const uWedgeP = u.cross(p);
|
|
66
|
+
if(uWedgeV === 0) continue;
|
|
67
|
+
const alpha = pWedgeV / uWedgeV;
|
|
68
|
+
const beta = uWedgeP / uWedgeV;
|
|
69
|
+
if (alpha < 1 && alpha > 0 && beta < 1 && beta > 0) {
|
|
70
|
+
havePointsInside = true;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!havePointsInside) {
|
|
75
|
+
if (uWedgeV > 0) triangles.push([prev, c, next])
|
|
76
|
+
if (uWedgeV < 0) triangles.push([c, prev, next])
|
|
77
|
+
path.splice(i, 1);
|
|
78
|
+
}
|
|
79
|
+
// i = (i + 1) % path.length;
|
|
80
|
+
}
|
|
81
|
+
const prev = path[2];
|
|
82
|
+
const next = path[1];
|
|
83
|
+
const c = path[0];
|
|
84
|
+
const u = next.sub(c);
|
|
85
|
+
const v = prev.sub(c);
|
|
86
|
+
const uWedgeV = u.cross(v);
|
|
87
|
+
if (uWedgeV > 0) triangles.push([path[2], path[0], path[1]])
|
|
88
|
+
if (uWedgeV < 0) triangles.push([path[0], path[2], path[1]])
|
|
89
|
+
return triangles;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function joinBoxTreePaths(boxTree) {
|
|
93
|
+
const childPaths = boxTree.children.map(bT => bT.path);
|
|
94
|
+
let grandPath = [...boxTree.path];
|
|
95
|
+
childPaths.forEach(childPath => {
|
|
96
|
+
let min = Number.MAX_VALUE;
|
|
97
|
+
let minIndexI = -1;
|
|
98
|
+
let minIndexJ = -1;
|
|
99
|
+
for (let i = 0; i < grandPath.length; i++) {
|
|
100
|
+
for (let j = 0; j < childPath.length; j++) {
|
|
101
|
+
const d = childPath[j].sub(grandPath[i]).squareLength();
|
|
102
|
+
// some deterministic salt in cost function
|
|
103
|
+
if (d + j < min) {
|
|
104
|
+
min = d;
|
|
105
|
+
minIndexI = i;
|
|
106
|
+
minIndexJ = j;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const gradLeft = grandPath.slice(0, minIndexI + 1);
|
|
111
|
+
const gradRight = grandPath.slice(minIndexI);
|
|
112
|
+
const innerLeft = childPath.slice(0, minIndexJ + 1);
|
|
113
|
+
const innerRight = childPath.slice(minIndexJ);
|
|
114
|
+
grandPath = gradLeft.concat(cleanPath(innerRight.concat(innerLeft))).concat(gradRight);
|
|
115
|
+
})
|
|
116
|
+
return grandPath;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
function mod(a, b) {
|
|
121
|
+
return ((a % b) + b) % b;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cleanPath(path) {
|
|
125
|
+
const epsilon = 1e-6;
|
|
126
|
+
const cleanPath = [];
|
|
127
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
128
|
+
if (!cleanPath.some(x => x.sub(path[i]).length() < epsilon)) {
|
|
129
|
+
cleanPath.push(path[i]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
cleanPath.push(path.at(-1));
|
|
133
|
+
return cleanPath;
|
|
134
|
+
}
|
package/src/Utils/Utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IS_NODE
|
|
1
|
+
import { IS_NODE } from "./Constants.js";
|
|
2
2
|
|
|
3
3
|
export async function measureTime(lambda) {
|
|
4
4
|
const t = performance.now();
|
|
@@ -94,6 +94,8 @@ export function hashStr(string) {
|
|
|
94
94
|
const __Worker = IS_NODE ? (await import("node:worker_threads")).Worker : Worker;
|
|
95
95
|
export class MyWorker {
|
|
96
96
|
constructor(path) {
|
|
97
|
+
const IS_GITHUB = typeof window !== "undefined" && (window.location.host || window.LOCATION_HOST) === "pedroth.github.io";
|
|
98
|
+
const SOURCE = IS_GITHUB ? "/tela.js" : "";
|
|
97
99
|
try {
|
|
98
100
|
if (IS_NODE) {
|
|
99
101
|
let workerPath = "/" + (import.meta.dirname).split('/').slice(1, -1).join('/');
|
|
@@ -102,6 +104,10 @@ export class MyWorker {
|
|
|
102
104
|
} else {
|
|
103
105
|
const workerPath = `${SOURCE}/src/${path}`;
|
|
104
106
|
this.worker = new __Worker(`${workerPath}`, { type: "module" });
|
|
107
|
+
this.worker.onerror = () => {
|
|
108
|
+
this.worker = new __Worker(`/node_modules/tela.js/src/${path}`, { type: "module" });
|
|
109
|
+
console.log(`Caught error while import from ${SOURCE} web, trying node_modules`);
|
|
110
|
+
}
|
|
105
111
|
}
|
|
106
112
|
} catch (e) {
|
|
107
113
|
console.log("Caught error while importing worker", e);
|
package/src/Utils/Video.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
+
import Anima from "./Utils/Anima.js"
|
|
2
|
+
import BScene from "./Scene/BScene.js"
|
|
3
|
+
import Box from "./Geometry/Box.js"
|
|
4
|
+
import Camera from "./Camera/Camera.js"
|
|
5
|
+
import Camera2D from "./Camera2D/Camera2D.js"
|
|
1
6
|
import Canvas from "./Tela/Canvas.js"
|
|
2
7
|
import Color from "./Color/Color.js"
|
|
3
8
|
import DOM from "./Utils/DomBuilder.js"
|
|
4
|
-
import Stream from "./Utils/Stream.js"
|
|
5
|
-
import Camera from "./Camera/Camera.js"
|
|
6
|
-
import BScene from "./Scene/BScene.js"
|
|
7
9
|
import KScene from "./Scene/KScene.js"
|
|
10
|
+
import Line from "./Geometry/Line.js"
|
|
11
|
+
import Mesh from "./Geometry/Mesh.js"
|
|
8
12
|
import NaiveScene from "./Scene/NaiveScene.js"
|
|
9
|
-
import
|
|
13
|
+
import Path from "./Geometry/Path.js"
|
|
14
|
+
import parseSVG from "./Utils/SVG.js"
|
|
10
15
|
import RandomScene from "./Scene/RandomScene.js"
|
|
11
|
-
import
|
|
12
|
-
import Box from "./Geometry/Box.js"
|
|
16
|
+
import Ray from "./Ray/Ray.js"
|
|
13
17
|
import Sphere from "./Geometry/Sphere.js"
|
|
14
|
-
import
|
|
15
|
-
import Path from "./Geometry/Path.js"
|
|
18
|
+
import Stream from "./Utils/Stream.js"
|
|
16
19
|
import Triangle from "./Geometry/Triangle.js"
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
+
import Vec, { Vec2, Vec3 } from "./Vector/Vector.js"
|
|
21
|
+
import VoxelScene from "./Scene/VoxelScene.js"
|
|
22
|
+
import PQueue from "./Utils/PQueue.js"
|
|
20
23
|
|
|
21
24
|
export {
|
|
22
25
|
Box,
|
|
@@ -28,19 +31,21 @@ export {
|
|
|
28
31
|
Path,
|
|
29
32
|
Vec2,
|
|
30
33
|
Vec3,
|
|
34
|
+
Anima,
|
|
31
35
|
Color,
|
|
32
36
|
BScene,
|
|
33
37
|
Canvas,
|
|
34
38
|
Camera,
|
|
35
39
|
KScene,
|
|
40
|
+
PQueue,
|
|
36
41
|
Sphere,
|
|
37
42
|
Stream,
|
|
43
|
+
Camera2D,
|
|
38
44
|
parseSVG,
|
|
39
45
|
Triangle,
|
|
40
46
|
NaiveScene,
|
|
41
47
|
VoxelScene,
|
|
42
48
|
RandomScene,
|
|
43
|
-
parseSvgPath,
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
export * from "./Utils/Math.js"
|
|
@@ -48,4 +53,5 @@ export * from "./Material/Material.js";
|
|
|
48
53
|
export * from "./Utils/Utils.js";
|
|
49
54
|
export * from "./Utils/Monads.js";
|
|
50
55
|
export * from "./Utils/Constants.js"
|
|
51
|
-
export * from "./Utils/Fonts.js"
|
|
56
|
+
export * from "./Utils/Fonts.js"
|
|
57
|
+
export * from "./Utils/Triangulate.js"
|