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 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.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
  },
@@ -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 isFirstTimeCounter = NUMBER_OF_CORES;
15
+ let prevScene = undefined;
15
16
  let serializedScene = undefined;
17
+ let isFirstTimeCounter = NUMBER_OF_CORES;
16
18
 
17
- const MAGIC_SETUP_TIME = 800;
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(`Camera/rayTraceWorker.js`));
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.serialize()
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(() => worker.postMessage(message), MAGIC_SETUP_TIME);
67
- } else {
68
- worker.postMessage(message)
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(`Camera/rayMapWorker.js`));
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.serialize()
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(() => worker.postMessage(message), MAGIC_SETUP_TIME);
119
- } else {
120
- worker.postMessage(message)
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
  }
@@ -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 k = -intRadius; k < intRadius; k++) {
84
- for (let l = -intRadius; l < intRadius; l++) {
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
+ }
@@ -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
- return Vec.fromArray(grad).scale(Math.sign(d)).normalize();
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 == Box.EMPTY) return true;
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,
@@ -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());
@@ -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 (!radius) return this;
122
+ if (radius === undefined) return this;
122
123
  this._radius = radius;
123
124
  return this;
124
125
  }
@@ -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
- this.faceNormal = u.cross(v).normalize();
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,
@@ -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
- let distance = Number.MAX_VALUE;
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() : normal;
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 Number.MAX_VALUE;
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);
@@ -9,6 +9,10 @@ export default class NaiveScene {
9
9
  this.sceneElements = [];
10
10
  }
11
11
 
12
+ get(id) {
13
+ return this.id2ElemMap(id);
14
+ }
15
+
12
16
  getHash() {
13
17
  const elements = this.getElements();
14
18
  let combinedHash = 0;
@@ -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
- for (let i = 0; i < elements.length; i++) {
58
- const elem = elements[i];
59
- normal = normal.add(elem.normalToPoint(p));
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() : normal;
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);
@@ -114,7 +114,8 @@ export default class Window extends Tela {
114
114
  //========================================================================================
115
115
 
116
116
  function handleMouse(canvas, lambda) {
117
- return ({ x, y }) => {
118
- return lambda(x, canvas.height - 1 - y);
117
+ return (e) => {
118
+ const { x, y } = e;
119
+ return lambda(x, canvas.height - 1 - y, e);
119
120
  }
120
121
  }
@@ -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
- const MAGIC_SETUP_TIME = 700;
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(`Tela/telaWorker.js`));
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(() => worker.postMessage(message), MAGIC_SETUP_TIME);
53
- } else {
54
- worker.postMessage(message)
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
+
@@ -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
- export function parseSvgPath(svgPath) {
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 params = attributeValue.match(/-?\d+\.?\d*/g).map(Number);
746
- if (attributeValue.includes("matrix")) {
747
- transform = dot(transform, transformBuilder(...params));
748
- }
749
- if (attributeValue.includes("translate")) {
750
- transform = dot(transform, transformBuilder(1, 0, 0, 1, ...params))
751
- }
752
- if (attributeValue.includes("scale")) {
753
- transform = dot(transform, transformBuilder(params[0], 0, 0, params[1] ?? params[0], 0, 0))
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
+ }
@@ -1,4 +1,4 @@
1
- import { IS_NODE, SOURCE } from "./Constants.js";
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);
@@ -29,7 +29,7 @@ export function videoAsync(file, lambda, { width = 640, height = 480, FPS = 25 }
29
29
  async ({ time, image }) => {
30
30
  return {
31
31
  time: time + dt,
32
- image: await lambda({ time, image })
32
+ image: await lambda({ time, image, dt })
33
33
  }
34
34
  }
35
35
  );
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 VoxelScene from "./Scene/VoxelScene.js"
13
+ import Path from "./Geometry/Path.js"
14
+ import parseSVG from "./Utils/SVG.js"
10
15
  import RandomScene from "./Scene/RandomScene.js"
11
- import Vec, { Vec2, Vec3 } from "./Vector/Vector.js"
12
- import Box from "./Geometry/Box.js"
16
+ import Ray from "./Ray/Ray.js"
13
17
  import Sphere from "./Geometry/Sphere.js"
14
- import Line from "./Geometry/Line.js"
15
- import Path from "./Geometry/Path.js"
18
+ import Stream from "./Utils/Stream.js"
16
19
  import Triangle from "./Geometry/Triangle.js"
17
- import Mesh from "./Geometry/Mesh.js"
18
- import Ray from "./Ray/Ray.js"
19
- import parseSVG, {parseSvgPath} from "./Utils/SVG.js"
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"