tela.js 1.1.8 → 1.1.10

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
@@ -16,6 +16,8 @@ Playground usage:
16
16
 
17
17
  - [Quick start](#quick-start)
18
18
  - [Dependencies](#dependencies)
19
+ - [Acknowledgements](#acknowledgements)
20
+ - [TODOs](#todos)
19
21
 
20
22
  # Quick start
21
23
 
@@ -36,7 +38,7 @@ Playground usage:
36
38
 
37
39
  const width = 640;
38
40
  const height = 480;
39
- const canvas = Canvas.ofSize(640, 480);
41
+ const canvas = Canvas.ofSize(width, height);
40
42
  loop(({ time, dt }) => {
41
43
  document.title = `FPS: ${(Math.floor(1 / dt))}`;
42
44
  canvas.map((x, y) => {
@@ -60,7 +62,7 @@ import { loop, Color, Window } from "tela.js/src/index.node.js";
60
62
 
61
63
  const width = 640;
62
64
  const height = 480;
63
- const window = Window.ofSize(640, 480);
65
+ const window = Window.ofSize(width, height);
64
66
  loop(({ time, dt }) => {
65
67
  window.setTitle(`FPS: ${Math.floor(1 / dt)}`);
66
68
  window.map((x, y) => {
@@ -111,9 +113,6 @@ video(
111
113
 
112
114
  And run it: `node index.mjs` / `bun index.js`
113
115
 
114
- ### Bun install issue
115
-
116
- Bun install is currently not working properly on these project. Although you can run examples with bun.
117
116
 
118
117
  ### Note on generating videos
119
118
 
@@ -139,8 +138,14 @@ You can find more examples of usage in:
139
138
  - [`ffmpeg`][ffmpeg]
140
139
  - [`node-sdl`][sdl]
141
140
 
142
- [Node][node] is preferred when running the demos (it is faster, [opened a bug in bun](https://github.com/oven-sh/bun/issues/9218)), [bun][bun] is needed to build the library.
141
+ [Node][node] is preferred when running the demos (it is faster, [opened a bug in bun](https://github.com/oven-sh/bun/issues/9218)).
142
+
143
+
144
+ # Acknowledgements
143
145
 
146
+ - [Keenan's 3D Model Repository](https://www.cs.cmu.edu/~kmcrane/Projects/ModelRepository/)
147
+ - [The models resource](https://www.models-resource.com/)
148
+ - [otaviogood fonts](https://github.com/otaviogood/shader_fontgen)
144
149
 
145
150
  # TODOs
146
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tela.js",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "author": "Pedroth",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,7 +4,7 @@ import { getRayTracer } from "./rayTrace.js";
4
4
  import { rasterGraphics } from "./raster.js";
5
5
  import { sdfTrace } from "./sdf.js";
6
6
  import { normalTrace } from "./normal.js";
7
- import { parallelWorkers } from "./parallel.js";
7
+ import { rayMapWorkers, rayTraceWorkers } from "./parallel.js";
8
8
 
9
9
  export default class Camera {
10
10
  constructor(props = {}) {
@@ -107,6 +107,18 @@ export default class Camera {
107
107
  }
108
108
  }
109
109
 
110
+ rayMapParallel(lambdaWithRays, dependencies) {
111
+ return {
112
+ to: (canvas, { scene, ...vars }) => {
113
+ return Promise
114
+ .allSettled(rayMapWorkers(this, scene, canvas, lambdaWithRays, vars, dependencies))
115
+ .then(() => {
116
+ return canvas.paint();
117
+ })
118
+ }
119
+ }
120
+ }
121
+
110
122
  sceneShot(scene, params) {
111
123
  return this.rayMap(getRayTracer(scene, params));
112
124
  }
@@ -129,7 +141,7 @@ export default class Camera {
129
141
  return {
130
142
  to: canvas => {
131
143
  return Promise
132
- .all(parallelWorkers(this, scene, canvas, params))
144
+ .allSettled(rayTraceWorkers(this, scene, canvas, params))
133
145
  .then(() => {
134
146
  return canvas.paint();
135
147
  })
@@ -1,6 +1,5 @@
1
- import { IS_NODE, NUMBER_OF_CORES } from "../Utils/Constants.js";
1
+ import { CHANNELS, IS_NODE, NUMBER_OF_CORES } from "../Utils/Constants.js";
2
2
  import Color from "../Color/Color.js";
3
- import { CHANNELS } from "../Tela/Tela.js";
4
3
 
5
4
  //========================================================================================
6
5
  /* *
@@ -23,8 +22,14 @@ class MyWorker {
23
22
  if (IS_NODE) {
24
23
  this.worker.removeAllListeners('message');
25
24
  this.worker.on("message", lambda);
25
+ this.worker.on("error", e => console.log("Caught error on worker", e))
26
26
  } else {
27
- this.worker.onmessage = message => lambda(message.data);
27
+ if (this.__lambda) {
28
+ this.worker.removeEventListener('message', this.__lambda);
29
+ }
30
+ this.__lambda = message => lambda(message.data);
31
+ this.worker.addEventListener("message", this.__lambda);
32
+ this.worker.addEventListener("error", e => console.log("Caught error on worker", e));
28
33
  }
29
34
  }
30
35
 
@@ -33,29 +38,39 @@ class MyWorker {
33
38
  }
34
39
  }
35
40
 
36
- let WORKERS = [];
41
+ let RAY_TRACE_WORKERS = [];
42
+ let RAY_MAP_WORKERS = [];
37
43
  let prevSceneHash = undefined;
44
+ let isFirstTimeCounter = NUMBER_OF_CORES;
45
+ let serializedScene = undefined;
38
46
 
47
+ const MAGIC_SETUP_TIME = 800;
39
48
  //========================================================================================
40
49
  /* *
41
50
  * MAIN *
42
51
  * */
43
52
  //========================================================================================
44
53
 
45
- export function parallelWorkers(camera, scene, canvas, params = {}) {
54
+ export function rayTraceWorkers(camera, scene, canvas, params = {}) {
46
55
  // lazy loading workers
47
- if (WORKERS.length === 0) {
56
+ if (RAY_TRACE_WORKERS.length === 0) {
48
57
  // needs to be here...
49
58
  const isGithub = typeof window !== "undefined" && (window.location.host || window.LOCATION_HOST) === "pedroth.github.io";
50
59
  const SOURCE = isGithub ? "/tela.js" : ""
51
- WORKERS = [...Array(NUMBER_OF_CORES)]
60
+ RAY_TRACE_WORKERS = [...Array(NUMBER_OF_CORES)]
52
61
  .map(() => new MyWorker(`${IS_NODE ? "." : SOURCE}/src/Camera/rayTraceWorker.js`));
53
62
  }
54
63
  const w = canvas.width;
55
64
  const h = canvas.height;
56
- const isNewScene = prevSceneHash !== scene.hash;
57
- if (isNewScene) prevSceneHash = scene.hash;
58
- return WORKERS.map((worker, k) => {
65
+ const newHash = scene?.getHash();
66
+ const isNewScene = prevSceneHash !== newHash;
67
+ if (isNewScene) {
68
+ prevSceneHash = newHash;
69
+ serializedScene = scene.serialize()
70
+ } else {
71
+ serializedScene = undefined;
72
+ }
73
+ return RAY_TRACE_WORKERS.map((worker, k) => {
59
74
  return new Promise((resolve) => {
60
75
  worker.onMessage(message => {
61
76
  const { image, startRow, endRow, } = message;
@@ -67,18 +82,80 @@ export function parallelWorkers(camera, scene, canvas, params = {}) {
67
82
  }
68
83
  resolve();
69
84
  })
70
- const ratio = Math.floor(h / WORKERS.length);
85
+ const ratio = Math.floor(h / RAY_TRACE_WORKERS.length);
86
+
87
+ const message = {
88
+ width: w,
89
+ height: h,
90
+ vars: params,
91
+ startRow: k * ratio,
92
+ endRow: Math.min(h, (k + 1) * ratio),
93
+ camera: camera.serialize(),
94
+ scene: serializedScene
95
+ };
96
+ if (isFirstTimeCounter > 0 && !IS_NODE) {
97
+ // hack to work in the browser, don't know why it works
98
+ isFirstTimeCounter--;
99
+ setTimeout(() => worker.postMessage(message), MAGIC_SETUP_TIME);
100
+ } else {
101
+ worker.postMessage(message)
102
+ }
103
+ });
104
+ })
105
+ }
71
106
 
107
+ export function rayMapWorkers(camera, scene, canvas, lambda, vars = [], dependencies = []) {
108
+ // lazy loading workers
109
+ if (RAY_MAP_WORKERS.length === 0) {
110
+ // needs to be here...
111
+ const isGithub = typeof window !== "undefined" && (window.location.host || window.LOCATION_HOST) === "pedroth.github.io";
112
+ const SOURCE = isGithub ? "/tela.js" : ""
113
+ RAY_MAP_WORKERS = [...Array(NUMBER_OF_CORES)]
114
+ .map(() => new MyWorker(`${IS_NODE ? "." : SOURCE}/src/Camera/rayMapWorker.js`));
115
+ }
116
+ const w = canvas.width;
117
+ const h = canvas.height;
118
+ const newHash = scene?.getHash();
119
+ const isNewScene = prevSceneHash !== newHash;
120
+ if (isNewScene) {
121
+ prevSceneHash = newHash;
122
+ serializedScene = scene.serialize()
123
+ } else {
124
+ serializedScene = undefined;
125
+ }
126
+ return RAY_MAP_WORKERS.map((worker, k) => {
127
+ return new Promise((resolve) => {
128
+ worker.onMessage(message => {
129
+ const { image, startRow, endRow, } = message;
130
+ let index = 0;
131
+ const startIndex = CHANNELS * w * startRow;
132
+ const endIndex = CHANNELS * w * endRow;
133
+ for (let i = startIndex; i < endIndex; i += CHANNELS) {
134
+ canvas.setPxlData(i, Color.ofRGB(image[index++], image[index++], image[index++], image[index++]));
135
+ }
136
+ resolve();
137
+ })
138
+ const ratio = Math.floor(h / RAY_MAP_WORKERS.length);
139
+
72
140
  const message = {
73
141
  width: w,
74
142
  height: h,
75
- params: params,
143
+ vars: vars,
144
+ lambda: lambda.toString(),
76
145
  startRow: k * ratio,
77
146
  endRow: Math.min(h, (k + 1) * ratio),
78
147
  camera: camera.serialize(),
79
- scene: isNewScene ? scene.serialize() : undefined
148
+ dependencies: dependencies.map(d => d.toString()),
149
+ scene: serializedScene
80
150
  };
81
- worker.postMessage(message);
151
+ if (isFirstTimeCounter > 0 && !IS_NODE) {
152
+ // hack to work in the browser, don't know why it works
153
+ isFirstTimeCounter--;
154
+ setTimeout(() => worker.postMessage(message), MAGIC_SETUP_TIME);
155
+ } else {
156
+ worker.postMessage(message)
157
+ }
158
+
82
159
  });
83
160
  })
84
- }
161
+ }
@@ -0,0 +1,81 @@
1
+ import Camera from "./Camera.js";
2
+ import { memoize } from "../Utils/Utils.js";
3
+ import { CHANNELS, IS_NODE } from "../Utils/Constants.js";
4
+ import { deserializeScene } from "../Scene/utils.js";
5
+ import Color from "../Color/Color.js";
6
+ import Box from "../Geometry/Box.js";
7
+ import Vec, { Vec2, Vec3 } from "../Vector/Vector.js";
8
+ import Ray from "../Ray/Ray.js";
9
+
10
+ const parentPort = IS_NODE ? (await import("node:worker_threads")).parentPort : undefined;
11
+
12
+ let scene = undefined;
13
+
14
+ function getScene(serializedScene) {
15
+ return deserializeScene(serializedScene).then(s => s.rebuild());
16
+ }
17
+
18
+ async function main(inputs) {
19
+ const {
20
+ startRow,
21
+ endRow,
22
+ width,
23
+ height,
24
+ vars,
25
+ dependencies,
26
+ lambda,
27
+ scene: serializedScene,
28
+ camera: serializedCamera,
29
+ } = inputs;
30
+
31
+ scene = serializedScene ? (await getScene(serializedScene)).rebuild() : scene;
32
+ const camera = Camera.deserialize(serializedCamera);
33
+ const rayGen = camera.rayFromImage(width, height);
34
+ const __lambda = getLambda(lambda, dependencies);
35
+ const bufferSize = width * (endRow - startRow + 1) * CHANNELS;
36
+ const image = new Float32Array(bufferSize);
37
+ let index = 0;
38
+ // the order does matter
39
+ for (let y = startRow; y < endRow; y++) {
40
+ for (let x = 0; x < width; x++) {
41
+ const ray = rayGen(x, height - 1 - y);
42
+ const color = __lambda(ray, { ...vars, scene });
43
+ if(!color) continue;
44
+ image[index++] = color.red;
45
+ image[index++] = color.green;
46
+ image[index++] = color.blue;
47
+ image[index++] = color.alpha;
48
+ }
49
+ }
50
+ return { image, startRow, endRow };
51
+ }
52
+
53
+ const getLambda = memoize((lambda, dependencies) => {
54
+ const __lambda = eval(`
55
+ ${dependencies.map(d => d.toString()).join("\n")}
56
+ const __lambda = ${lambda};
57
+ __lambda;
58
+ `)
59
+ return __lambda;
60
+ });
61
+
62
+
63
+ if (IS_NODE) {
64
+ parentPort.on("message", async message => {
65
+ const input = message;
66
+ const output = await main(input);
67
+ parentPort.postMessage(output);
68
+ });
69
+ } else {
70
+ self.onmessage = async message => {
71
+ const input = message.data;
72
+ const output = await main(input);
73
+ postMessage(output);
74
+ };
75
+
76
+ self.onerror = e => {
77
+ console.log(`Caught error inside ray map worker ${e}`)
78
+ };
79
+
80
+
81
+ }
@@ -4,7 +4,7 @@ import Ray from "../Ray/Ray.js";
4
4
  import Vec from "../Vector/Vector.js";
5
5
  import { getBiLinearTexColor, getDefaultTexColor, getTexColor } from "./common.js";
6
6
 
7
- export function rayTrace(ray, scene, params) {
7
+ export function rayTrace(ray, scene, params = {}) {
8
8
  let { samplesPerPxl, bounces, variance, gamma, bilinearTexture } = params;
9
9
  bounces = bounces ?? 10;
10
10
  variance = variance ?? 0.001;
@@ -1,7 +1,6 @@
1
1
  import Camera from "./Camera.js";
2
2
  import { rayTrace } from "./rayTrace.js";
3
- import { CHANNELS } from "../Tela/Tela.js";
4
- import { IS_NODE } from "../Utils/Constants.js";
3
+ import { CHANNELS, IS_NODE } from "../Utils/Constants.js";
5
4
  import { deserializeScene } from "../Scene/utils.js";
6
5
  import Color from "../Color/Color.js";
7
6
 
@@ -56,4 +55,6 @@ if (IS_NODE) {
56
55
  const output = await main(input);
57
56
  postMessage(output);
58
57
  };
58
+
59
+ onerror = e => console.log("Caught error on rayTrace worker", e);
59
60
  }
@@ -142,11 +142,15 @@ export default class Box {
142
142
  }
143
143
 
144
144
  serialize() {
145
- // TODO
145
+ return {
146
+ type: Box.name,
147
+ min: this.min.toArray(),
148
+ max: this.max.toArray()
149
+ }
146
150
  }
147
151
 
148
- deserialize() {
149
- // TODO
152
+ static async deserialize(json) {
153
+ return new Box(Vec.fromArray(json.min), Vec.fromArray(json.max));
150
154
  }
151
155
 
152
156
  static EMPTY = new Box();
@@ -77,7 +77,17 @@ export default class Line {
77
77
  }
78
78
 
79
79
  serialize() {
80
- //TODO's
80
+ return {
81
+ type: Line.name,
82
+ name: this.name,
83
+ radius: this.radius,
84
+ emissive: this.emissive,
85
+ colors: this.colors.map(x => x.toArray()),
86
+ texCoords: this.texCoords.map(x => x.toArray()),
87
+ positions: this.positions.map(x => x.toArray()),
88
+ texture: this.texture ? this.texture.serialize() : undefined,
89
+ material: { type: this.material.type, args: this.material.args }
90
+ }
81
91
  }
82
92
 
83
93
  static deserialize(json) {
package/src/IO/IO.js CHANGED
@@ -7,7 +7,7 @@ export function saveImageToFile(fileAddress, image) {
7
7
  const ppmName = `${fileName}.ppm`;
8
8
  writeFileSync(ppmName, createPPMFromImage(image));
9
9
  if (extension !== "ppm") {
10
- execSync(`ffmpeg -i ${ppmName} ${fileName}.${extension}`);
10
+ execSync(`ffmpeg -i ${ppmName} -y ${fileName}.${extension}`);
11
11
  unlinkSync(ppmName)
12
12
  }
13
13
  }
@@ -89,7 +89,7 @@ export function saveImageStreamToVideo(fileAddress, streamWithImages, { imageGet
89
89
  s = await s.tail;
90
90
  }
91
91
  if (!fps) fps = ite / time;
92
- execSync(`ffmpeg -framerate ${fps} -i ${fileName}_%d.ppm ${fileName}.${extension}`);
92
+ execSync(`ffmpeg -framerate ${fps} -i ${fileName}_%d.ppm -y ${fileName}.${extension}`);
93
93
  for (let i = 0; i < ite; i++) {
94
94
  unlinkSync(`${fileName}_${i}.ppm`);
95
95
  }
@@ -106,8 +106,8 @@ export function saveParallelImageStreamToVideo(fileAddress, parallelStreamOfImag
106
106
  const promises = inputParamsPartitions.map((inputParams, i) => {
107
107
  const spawnFile = "IO_parallel" + i + ".js";
108
108
  writeFileSync(spawnFile, `
109
- import * as _module from "./src/index.node.js"
110
109
  import fs from "node:fs";
110
+ import * as _module from "./src/index.node.js"
111
111
  const {
112
112
  Box,
113
113
  Vec,
@@ -105,7 +105,7 @@ export default class BScene extends NaiveScene {
105
105
  }
106
106
 
107
107
  serialize() {
108
- const json = this.super.serialize();
108
+ const json = super.serialize();
109
109
  json.type = BScene.name;
110
110
  return json;
111
111
  }
@@ -9,8 +9,15 @@ export default class NaiveScene {
9
9
  this.sceneElements = [];
10
10
  }
11
11
 
12
- get hash() {
13
- return this.getElements().reduce((e, x) => e ^ hashStr(x.name), 0);
12
+ getHash() {
13
+ const elements = this.getElements();
14
+ let combinedHash = 0;
15
+ const prime = 31; // A prime number, typically used in hash functions
16
+ for (let i = 0; i < elements.length; i++) {
17
+ const hash = hashStr(elements[i].name);
18
+ combinedHash = (combinedHash * prime) ^ hash;
19
+ }
20
+ return combinedHash >>> 0; // unsigned shift operator, converts combinedHash to unsigned number
14
21
  }
15
22
 
16
23
  add(...elements) {
@@ -1,3 +1,4 @@
1
+ import Line from "../Geometry/Line.js";
1
2
  import Sphere from "../Geometry/Sphere.js";
2
3
  import Triangle from "../Geometry/Triangle.js";
3
4
  import BScene from "./BScene.js";
@@ -21,6 +22,7 @@ export async function deserializeScene(sceneJson) {
21
22
  const serializedElement = sceneData[i];
22
23
  if (serializedElement.type === Triangle.name) sceneElements.push(await Triangle.deserialize(serializedElement, artifacts));
23
24
  if (serializedElement.type === Sphere.name) sceneElements.push(await Sphere.deserialize(serializedElement, artifacts));
25
+ if (serializedElement.type === Line.name) sceneElements.push(await Line.deserialize(serializedElement, artifacts));
24
26
  }
25
27
  return new SceneClass(...params)
26
28
  .addList(sceneElements);
@@ -1,7 +1,6 @@
1
1
  import Color from "../Color/Color.js";
2
- import { MAX_8BIT } from "../Utils/Constants.js";
3
- import { memoize } from "../Utils/Utils.js";
4
- import Tela, { CHANNELS } from "./Tela.js";
2
+ import { CHANNELS, MAX_8BIT } from "../Utils/Constants.js";
3
+ import Tela from "./Tela.js";
5
4
 
6
5
  export default class Canvas extends Tela {
7
6
 
@@ -26,66 +25,6 @@ export default class Canvas extends Tela {
26
25
  return this;
27
26
  }
28
27
 
29
- mapParallel = memoize((lambda, dependencies = []) => {
30
- const N = navigator.hardwareConcurrency;
31
- const w = this.width;
32
- const h = this.height;
33
- const fun = ({ _start_row, _end_row, _width_, _height_, _worker_id_, _vars_ }) => {
34
- const image = new Float32Array(CHANNELS * _width_ * (_end_row - _start_row));
35
- const startIndex = CHANNELS * _width_ * _start_row;
36
- const endIndex = CHANNELS * _width_ * _end_row;
37
- let index = 0;
38
- for (let k = startIndex; k < endIndex; k += CHANNELS) {
39
- const i = Math.floor(k / (CHANNELS * _width_));
40
- const j = Math.floor((k / CHANNELS) % _width_);
41
- const x = j;
42
- const y = _height_ - 1 - i;
43
- const color = lambda(x, y, { ..._vars_ });
44
- if (!color) return;
45
- image[index] = color.red;
46
- image[index + 1] = color.green;
47
- image[index + 2] = color.blue;
48
- image[index + 3] = color.alpha;
49
- index += CHANNELS;
50
- }
51
- return { image, _start_row, _end_row, _worker_id_ };
52
- }
53
- const workers = [...Array(N)].map(() => createWorker(fun, lambda, dependencies));
54
- return {
55
- run: (vars = {}) => {
56
- // works better than Promise.all solution
57
- return new Promise((resolve) => {
58
- const allWorkersDone = [...Array(N)].fill(false);
59
- workers.forEach((worker, k) => {
60
- worker.onmessage = (event) => {
61
- const { image, _start_row, _end_row, _worker_id_ } = event.data;
62
- let index = 0;
63
- const startIndex = CHANNELS * w * _start_row;
64
- const endIndex = CHANNELS * w * _end_row;
65
- for (let i = startIndex; i < endIndex; i++) {
66
- this.image[i] = image[index];
67
- index++;
68
- }
69
- allWorkersDone[_worker_id_] = true;
70
- if (allWorkersDone.every(x => x)) {
71
- return resolve(this.paint());
72
- }
73
- };
74
- const ratio = Math.floor(h / N);
75
- worker.postMessage({
76
- _start_row: k * ratio,
77
- _end_row: Math.min(h - 1, (k + 1) * ratio),
78
- _width_: w,
79
- _height_: h,
80
- _worker_id_: k,
81
- _vars_: vars
82
- });
83
- })
84
- })
85
- }
86
- }
87
- });
88
-
89
28
  onMouseDown(lambda) {
90
29
  this.canvas.addEventListener("mousedown", handleMouse(this, lambda), false);
91
30
  this.canvas.addEventListener("touchstart", handleMouse(this, lambda), false);
@@ -218,20 +157,4 @@ function handleMouse(canvas, lambda) {
218
157
  const y = Math.floor(h - 1 - my * h);
219
158
  return lambda(x, y, event);
220
159
  }
221
- }
222
-
223
- const createWorker = (main, lambda, dependencies) => {
224
- const workerFile = `
225
- const CHANNELS = ${CHANNELS};
226
- ${Color.toString()}
227
- ${dependencies.map(d => d.toString()).join("\n")}
228
- const lambda = ${lambda.toString()};
229
- const __main__ = ${main.toString()};
230
- onmessage = e => {
231
- const input = e.data
232
- const output = __main__(input);
233
- self.postMessage(output);
234
- };
235
- `;
236
- return new Worker(URL.createObjectURL(new Blob([workerFile])));
237
- };
160
+ }
package/src/Tela/Image.js CHANGED
@@ -1,71 +1,9 @@
1
+ import Tela from "./Tela.js";
1
2
  import Color from "../Color/Color.js";
2
3
  import { readImageFrom } from "../IO/IO.js";
3
- import { memoize } from "../Utils/Utils.js";
4
- import Tela, { CHANNELS } from "./Tela.js";
5
- import { Worker } from "node:worker_threads";
6
- import os from "node:os";
7
4
 
8
5
  export default class Image extends Tela {
9
6
 
10
- mapParallel = memoize((lambda, dependencies = []) => {
11
- const N = os.cpus().length;
12
- const w = this.width;
13
- const h = this.height;
14
- const fun = ({ _start_row, _end_row, _width_, _height_, _worker_id_, _vars_ }) => {
15
- const image = new Float32Array(CHANNELS * _width_ * (_end_row - _start_row));
16
- const startIndex = CHANNELS * _width_ * _start_row;
17
- const endIndex = CHANNELS * _width_ * _end_row;
18
- let index = 0;
19
- for (let k = startIndex; k < endIndex; k += CHANNELS) {
20
- const i = Math.floor(k / (CHANNELS * _width_));
21
- const j = Math.floor((k / CHANNELS) % _width_);
22
- const x = j;
23
- const y = _height_ - 1 - i;
24
- const color = lambda(x, y, { ..._vars_ });
25
- if (!color) continue;
26
- image[index] = color.red;
27
- image[index + 1] = color.green;
28
- image[index + 2] = color.blue;
29
- image[index + 3] = 1;
30
- index += CHANNELS;
31
- }
32
- return { image, _start_row, _end_row, _worker_id_ };
33
- }
34
- const workers = [...Array(N)].map(() => createWorker(fun, lambda, dependencies));
35
- return {
36
- run: (vars = {}) => {
37
- // in node promise.all is faster than the canvas method
38
- return Promise
39
- .all(workers.map((worker, k) => {
40
- return new Promise((resolve) => {
41
- worker.removeAllListeners('message');
42
- worker.on("message", (message) => {
43
- const { image, _start_row, _end_row } = message;
44
- let index = 0;
45
- const startIndex = CHANNELS * w * _start_row;
46
- const endIndex = CHANNELS * w * _end_row;
47
- for (let i = startIndex; i < endIndex; i++) {
48
- this.image[i] = image[index];
49
- index++;
50
- }
51
- return resolve();
52
- });
53
- const ratio = Math.floor(h / N);
54
- worker.postMessage({
55
- _start_row: k * ratio,
56
- _end_row: Math.min(h - 1, (k + 1) * ratio),
57
- _width_: w,
58
- _height_: h,
59
- _worker_id_: k,
60
- _vars_: vars
61
- });
62
- })
63
- }))
64
- .then(() => this.paint());
65
- }
66
- }
67
- });
68
-
69
7
  serialize() {
70
8
  return { type: Image.name, url: this.url };
71
9
  }
@@ -104,27 +42,4 @@ export default class Image extends Tela {
104
42
  return image.getPxl(x, y);
105
43
  })
106
44
  }
107
- }
108
-
109
- //========================================================================================
110
- /* *
111
- * Private *
112
- * */
113
- //========================================================================================
114
-
115
-
116
- const createWorker = (main, lambda, dependencies) => {
117
- const workerFile = `
118
- const { parentPort } = require("node:worker_threads");
119
- const CHANNELS = ${CHANNELS};
120
- ${dependencies.concat([Color]).map(d => d.toString()).join("\n")}
121
- const lambda = ${lambda.toString()};
122
- const __main__ = ${main.toString()};
123
- parentPort.on("message", message => {
124
- const output = __main__(message);
125
- parentPort.postMessage(output);
126
- });
127
- `;
128
- const worker = new Worker(workerFile, { eval: true });
129
- return worker;
130
- };
45
+ }