tela.js 1.1.8 → 1.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tela.js",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "author": "Pedroth",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
  /* *
@@ -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
 
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,
@@ -1,7 +1,7 @@
1
1
  import Color from "../Color/Color.js";
2
- import { MAX_8BIT } from "../Utils/Constants.js";
2
+ import { CHANNELS, MAX_8BIT } from "../Utils/Constants.js";
3
3
  import { memoize } from "../Utils/Utils.js";
4
- import Tela, { CHANNELS } from "./Tela.js";
4
+ import Tela from "./Tela.js";
5
5
 
6
6
  export default class Canvas extends Tela {
7
7
 
@@ -228,10 +228,10 @@ const createWorker = (main, lambda, dependencies) => {
228
228
  const lambda = ${lambda.toString()};
229
229
  const __main__ = ${main.toString()};
230
230
  onmessage = e => {
231
- const input = e.data
231
+ const input = e.data;
232
232
  const output = __main__(input);
233
233
  self.postMessage(output);
234
234
  };
235
235
  `;
236
- return new Worker(URL.createObjectURL(new Blob([workerFile])));
236
+ return new Worker(URL.createObjectURL(new Blob([workerFile], { type: 'application/javascript' })), { type: "module" });
237
237
  };
package/src/Tela/Image.js CHANGED
@@ -1,7 +1,8 @@
1
+ import Tela from "./Tela.js";
1
2
  import Color from "../Color/Color.js";
2
3
  import { readImageFrom } from "../IO/IO.js";
3
4
  import { memoize } from "../Utils/Utils.js";
4
- import Tela, { CHANNELS } from "./Tela.js";
5
+ import { CHANNELS } from "../Utils/Constants.js";
5
6
  import { Worker } from "node:worker_threads";
6
7
  import os from "node:os";
7
8
 
@@ -116,14 +117,31 @@ export default class Image extends Tela {
116
117
  const createWorker = (main, lambda, dependencies) => {
117
118
  const workerFile = `
118
119
  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
- });
120
+ import("./src/index.js").then(_module => {
121
+ const {
122
+ Box,
123
+ Vec,
124
+ Vec2,
125
+ Vec3,
126
+ Mesh,
127
+ Color,
128
+ Image,
129
+ BScene,
130
+ Camera,
131
+ KScene,
132
+ Sphere,
133
+ CHANNELS
134
+ MAX_8BIT,
135
+ NaiveScene,
136
+ } = _module;
137
+ ${dependencies.map(d => d.toString()).join("\n")}
138
+ const lambda = ${lambda.toString()};
139
+ const __main__ = ${main.toString()};
140
+ parentPort.on("message", message => {
141
+ const output = __main__(message);
142
+ parentPort.postMessage(output);
143
+ });
144
+ })
127
145
  `;
128
146
  const worker = new Worker(workerFile, { eval: true });
129
147
  return worker;
package/src/Tela/Tela.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import Color from "../Color/Color.js";
2
2
  import Box from "../Geometry/Box.js";
3
+ import { CHANNELS } from "../Utils/Constants.js";
3
4
  import { mod } from "../Utils/Math.js";
4
5
  import { Vec2 } from "../Vector/Vector.js";
5
6
 
6
- export const CHANNELS = 4;
7
-
8
7
  // Abstract Image
9
8
  export default class Tela {
10
9
  constructor(width, height) {
@@ -42,6 +41,19 @@ export default class Tela {
42
41
  return this.paint();
43
42
  }
44
43
 
44
+ mapBox = (lambda, box) => {
45
+ const init = box.min;
46
+ const end = box.max;
47
+ for (let x = init.x; x < end.x; x++) {
48
+ for (let y = init.y; y < end.y; y++) {
49
+ const color = lambda(x - init.x, y - init.y);
50
+ if (!color) continue;
51
+ this.setPxl(x, y, color);
52
+ }
53
+ }
54
+ return this;
55
+ }
56
+
45
57
  fill(color) {
46
58
  if (!color) return;
47
59
  const n = this.image.length;
@@ -1,11 +1,11 @@
1
1
  import { memoize } from "../Utils/Utils.js";
2
2
  import Color from "../Color/Color.js";
3
- import { MAX_8BIT } from "../Utils/Constants.js";
3
+ import { CHANNELS, MAX_8BIT } from "../Utils/Constants.js";
4
4
  import { clamp } from "../Utils/Math.js";
5
5
  import sdl from "@kmamal/sdl";
6
6
  import os from 'node:os';
7
7
  import { Worker } from "node:worker_threads";
8
- import Tela, { CHANNELS } from "./Tela.js";
8
+ import Tela from "./Tela.js";
9
9
  import { Buffer } from "node:buffer";
10
10
 
11
11
 
@@ -127,6 +127,11 @@ export default class Window extends Tela {
127
127
  return this;
128
128
  }
129
129
 
130
+ onResizeWindow(lambda) {
131
+ this.window.on("resize", (e) => lambda(e));
132
+ return this;
133
+ }
134
+
130
135
  setWindowSize(w, h) {
131
136
  this.window.setSize(w, h);
132
137
  return this;
@@ -1,4 +1,5 @@
1
1
  export const MAX_8BIT = 255;
2
+ export const CHANNELS = 4;
2
3
  export const RAD2DEG = 180 / Math.PI;
3
4
  export const IS_NODE = typeof process !== 'undefined' && process.versions && process.versions.node;
4
5
  export const NUMBER_OF_CORES = IS_NODE ?
package/src/Utils/Math.js CHANGED
@@ -19,6 +19,23 @@ export function lerp(a, b) {
19
19
  return t => a.scale(1 - t).add(b.scale(t));
20
20
  }
21
21
 
22
+ export function qBezier(p1, p2, p3) {
23
+ const q1 = lerp(p1, p2);
24
+ const q2 = lerp(p2, p3);
25
+ return t => lerp(q1(t), q2(t))(t);
26
+ }
27
+
28
+ export function cBezier(p1, p2, p3, p4) {
29
+ const b1 = lerp(p1, p2);
30
+ const b2 = lerp(p2, p3);
31
+ const b3 = lerp(p3, p4);
32
+ return t => {
33
+ const c1 = lerp(b1(t), b2(t));
34
+ const c2 = lerp(b2(t), b3(t));
35
+ return lerp(c1(t), c2(t))(t);
36
+ }
37
+ }
38
+
22
39
  export function randomPointInSphere(dim) {
23
40
  let randomInSphere;
24
41
  while (true) {
package/src/Utils/SVG.js CHANGED
@@ -1,21 +1,13 @@
1
- /*
2
- SVG -> StartTag InnerSVG EndTag / EmptyTag / CommentTag
3
- InnerSVG -> SVGTypes InnerSVG / ε
4
- SVGTypes -> SVG / Value
5
- Value -> AnyBut(<)
6
- StartTag -> < (" ")* AlphaNumName (" " || "\n")* Attrs (" " || "\n")*>
7
- EmptyTag -> <(" ")* AlphaNumName (" " || "\n")* Attrs (" " || "\n")* />
8
- Attrs -> Attr (" " || "\n")* Attrs / ε
9
- Attr -> AlphaNumName="AnyBut(")" / AlphaNumName='AnyBut(')'
10
- EndTag -> </(" ")*AlphaNumName(" ")*>
11
- AlphaNumName -> [a-zA-z][a-zA-Z0-9]*
12
- */
1
+ import { Vec2 } from "../Vector/Vector.js";
2
+ import { cBezier, qBezier } from "./Math.js";
3
+
13
4
  export default function parse(text) {
14
- const { left: SVG } = parseSVG(eatSpacesTabsAndNewLines(tokens(stream(text))));
15
- // TODO create triangulation
16
- return SVG;
5
+ const tokensStream = tokens(stream(text));
6
+ const { left: SVG } = parseSVG(eatAllSpacesChars(tokensStream));
7
+ return readSVGNode(SVG);
17
8
  }
18
9
 
10
+
19
11
  //========================================================================================
20
12
  /* *
21
13
  * TOKENIZER *
@@ -25,8 +17,11 @@ const TOKEN_SYMBOLS = [
25
17
  "<!--",
26
18
  "-->",
27
19
  "\n",
20
+ "\r",
28
21
  "\t",
29
22
  " ",
23
+ "<?",
24
+ "?>",
30
25
  "</",
31
26
  "/>",
32
27
  "<",
@@ -97,13 +92,26 @@ function symbolParser(symbol) {
97
92
  * */
98
93
  //========================================================================================
99
94
 
100
-
95
+ /*
96
+ SVG -> StartTag InnerSVG EndTag / EmptyTag / CommentTag (" " || "\n")* SVG / XMLTag (" " || "\n")* SVG
97
+ InnerSVG -> SVGTypes InnerSVG / ε
98
+ SVGTypes -> SVG / CommentTag / Value
99
+ Value -> AnyBut(<)
100
+ StartTag -> < (" ")* AlphaNumName (" " || "\n")* Attrs (" " || "\n")*>
101
+ EmptyTag -> <(" ")* AlphaNumName (" " || "\n")* Attrs (" " || "\n")* />
102
+ Attrs -> Attr (" " || "\n")* Attrs / ε
103
+ Attr -> AlphaNumName="AnyBut(")" / AlphaNumName='AnyBut(')'
104
+ EndTag -> </(" ")*AlphaNumName(" ")*>
105
+ AlphaNumName -> [a-zA-z][a-zA-Z0-9]*
106
+ CommentTag -> <!--AnyBut("-->")-->
107
+ XMLTag -> <?AnyBut("?>")?>
108
+ */
101
109
  function parseSVG(stream) {
102
110
  return or(
103
111
  () => {
104
112
  const { left: StartTag, right: nextStream1 } = parseStartTag(stream);
105
113
  const { left: InnerSVG, right: nextStream2 } = parseInnerSVG(nextStream1);
106
- const { left: EndTag, right: nextStream3 } = parseEndTag(eatSpacesTabsAndNewLines(nextStream2));
114
+ const { left: EndTag, right: nextStream3 } = parseEndTag(eatAllSpacesChars(nextStream2));
107
115
  return pair({ type: "svg", StartTag, InnerSVG, EndTag }, nextStream3);
108
116
  },
109
117
  () => {
@@ -111,24 +119,35 @@ function parseSVG(stream) {
111
119
  return pair({ type: "svg", EmptyTag }, nextStream);
112
120
  },
113
121
  () => {
114
- const { left: CommentTag, right: nextStream } = parseCommentTag(stream);
115
- return pair({ type: "svg", CommentTag }, nextStream);
122
+ const { right: nextStream } = parseCommentTag(stream);
123
+ const { left: SVG, right: nextStream1 } = parseSVG(eatAllSpacesChars(nextStream));
124
+ return pair({ type: "svg", ...SVG }, nextStream1);
125
+ },
126
+ () => {
127
+ const { right: nextStream } = parseXMLTag(stream);
128
+ const { left: SVG, right: nextStream1 } = parseSVG(eatAllSpacesChars(nextStream));
129
+ return pair({ type: "svg", ...SVG }, nextStream1);
116
130
  }
117
131
  );
118
132
  }
119
133
 
120
134
  function parseValue(stream) {
121
- const { left: AnyBut, right: nextStream } = parseAnyBut(t => t.type === "<" || t.type === "</")(eatSpacesTabsAndNewLines(stream));
135
+ const { left: AnyBut, right: nextStream } = parseAnyBut(t => t.type === "<" || t.type === "</")(eatAllSpacesChars(stream));
122
136
  return pair({ type: "value", text: AnyBut.text }, nextStream);
123
137
  }
124
138
 
125
139
  function parseSVGTypes(stream) {
126
140
  return or(
127
141
  () => {
128
- const cleanStream = eatSpacesTabsAndNewLines(stream);
142
+ const cleanStream = eatAllSpacesChars(stream);
129
143
  const { left: SVG, right: nextStream } = parseSVG(cleanStream);
130
144
  return pair({ type: "svgTypes", SVG }, nextStream);
131
145
  },
146
+ () => {
147
+ const cleanStream = eatAllSpacesChars(stream);
148
+ const { left: CommentTag, right: nextStream } = parseCommentTag(cleanStream);
149
+ return pair({ type: "svgTypes", CommentTag }, nextStream);
150
+ },
132
151
  () => {
133
152
  const { left: Value, right: nextStream } = parseValue(stream);
134
153
  if (Value.text === "") throw Error("Fail to parse SVGType")
@@ -172,7 +191,7 @@ function parseAnyBut(tokenPredicate) {
172
191
  }
173
192
 
174
193
  function parseEndTag(stream) {
175
- const filteredStream = eatSpacesTabsAndNewLines(stream);
194
+ const filteredStream = eatAllSpacesChars(stream);
176
195
  const token = filteredStream.head();
177
196
  if ("</" === token.type) {
178
197
  const nextStream1 = eatSpaces(filteredStream.tail());
@@ -190,9 +209,9 @@ function parseEmptyTag(stream) {
190
209
  if ("<" === token.type) {
191
210
  const nextStream1 = eatSpaces(stream.tail());
192
211
  const { left: tagName, right: nextStream2 } = parseAlphaNumName(nextStream1)
193
- const nextStream3 = eatSpacesTabsAndNewLines(nextStream2);
212
+ const nextStream3 = eatAllSpacesChars(nextStream2);
194
213
  const { left: Attrs, right: nextStream4 } = parseAttrs(nextStream3);
195
- const nextStream5 = eatSpacesTabsAndNewLines(nextStream4);
214
+ const nextStream5 = eatAllSpacesChars(nextStream4);
196
215
  if ("/>" === nextStream5.head().type) {
197
216
  return pair({ type: "emptyTag", tag: tagName.text, Attrs }, nextStream5.tail());
198
217
  }
@@ -209,14 +228,23 @@ function parseCommentTag(stream) {
209
228
  throw new Error("Fail to parse CommentTag")
210
229
  }
211
230
 
231
+ function parseXMLTag(stream) {
232
+ if ("<?" === stream.head().type) {
233
+ const nextStream = stream.tail();
234
+ const { right: nextStream1 } = parseAnyBut(token => '?>' === token.type)(nextStream);
235
+ return pair({ type: "xmlTag" }, eatAllSpacesChars(nextStream1.tail()));
236
+ }
237
+ throw new Error("Fail to parse XMLTag")
238
+ }
239
+
212
240
  function parseStartTag(stream) {
213
241
  const token = stream.head();
214
242
  if ("<" === token.type) {
215
243
  const nextStream1 = eatSpaces(stream.tail());
216
244
  const { left: tagName, right: nextStream2 } = parseAlphaNumName(nextStream1)
217
- const nextStream3 = eatSpacesTabsAndNewLines(nextStream2);
245
+ const nextStream3 = eatAllSpacesChars(nextStream2);
218
246
  const { left: Attrs, right: nextStream4 } = parseAttrs(nextStream3);
219
- const nextStream5 = eatSpacesTabsAndNewLines(nextStream4);
247
+ const nextStream5 = eatAllSpacesChars(nextStream4);
220
248
  if (">" === nextStream5.head().type) {
221
249
  return pair({ type: "startTag", tag: tagName.text, Attrs }, nextStream5.tail());
222
250
  }
@@ -271,7 +299,7 @@ function parseAttrs(stream) {
271
299
  return or(
272
300
  () => {
273
301
  const { left: Attr, right: nextStream } = parseAttr(stream);
274
- const nextStreamNoSpaces = eatSpacesTabsAndNewLines(nextStream);
302
+ const nextStreamNoSpaces = eatAllSpacesChars(nextStream);
275
303
  const { left: Attrs, right: nextStream1 } = parseAttrs(nextStreamNoSpaces);
276
304
  return pair({
277
305
  type: "attrs",
@@ -287,24 +315,496 @@ function parseAttrs(stream) {
287
315
  )
288
316
  }
289
317
 
290
- function eatSpaces(stream) {
291
- let s = stream;
292
- while (!s.isEmpty()) {
293
- if (s.head().type !== " ") break;
294
- s = s.tail();
318
+ const eatSpaces = eatWhile(p => p.type === " ");
319
+ const eatAllSpacesChars = eatWhile(p => p.type === " " || p.type === "\t" || p.type === "\r" || p.type === "\n")
320
+
321
+ //========================================================================================
322
+ /* *
323
+ * PARSE SVG PATH *
324
+ * */
325
+ //========================================================================================
326
+
327
+
328
+ /**
329
+ *
330
+ * path -> action path/ ε
331
+ * action -> (" "* | "\n"* )letter(" "* | "\n"*)numbers / letter
332
+ * letter -> [a...zA...Z]
333
+ * numbers -> number(" "* | "\n"* | ",")numbers / ε
334
+ * number -> -D.D / D.D / -D / D
335
+ * D -> [0-9]D / ε
336
+ */
337
+ export function parseSvgPath(svgPath) {
338
+ const { left: path, } = parsePath(stream(svgPath));
339
+ return path;
340
+ }
341
+
342
+ function parsePath(iStream) {
343
+ return or(
344
+ () => {
345
+ const { left: action, right: nStream } = parseAction(iStream);
346
+ const { left: path, right: nStream2 } = parsePath(nStream);
347
+ return pair({ type: "path", actions: [{ ...action }, ...path.actions] }, nStream2);
348
+ },
349
+ () => {
350
+ return pair({ type: "path", actions: [] }, iStream)
351
+ }
352
+ )
353
+ }
354
+
355
+ function parseAction(iStream) {
356
+ const nStream0 = eatWhile(p => p === " " || p === "\n")(iStream);
357
+ const { left: letter, right: nStream } = parseLetter(nStream0);
358
+ const nStream1 = eatWhile(p => p === " " || p === "\n")(nStream);
359
+ const { left: numbers, right: nStream2 } = parseNumbers(nStream1);
360
+ return pair({ type: "action", letter: letter.letter, numbers: numbers.numbers }, nStream2);
361
+ }
362
+
363
+
364
+ function parseLetter(iStream) {
365
+ const char = iStream.head();
366
+ if (/^[a-zA-Z]$/.test(char)) {
367
+ return pair({ type: "letter", letter: char }, iStream.tail())
295
368
  }
296
- return s;
369
+ throw new Error("Caught exception in parsing letter");
297
370
  }
298
- function eatSpacesTabsAndNewLines(stream) {
299
- let s = stream;
300
- while (!s.isEmpty()) {
301
- const symbol = s.head().type;
302
- if (symbol !== " " && symbol !== "\t" && symbol !== "\n") break;
303
- s = s.tail();
371
+
372
+ function parseNumbers(iStream) {
373
+ return or(
374
+ () => {
375
+ const { left: number, right: nStream } = parseNumber(iStream);
376
+ const nStream2 = eatWhile(p => p === " " || p === "\n" || p === ",")(nStream);
377
+ const { left: numbers, right: nStream3 } = parseNumbers(nStream2);
378
+ return pair({ type: "numbers", numbers: [number.number, ...numbers.numbers] }, nStream3);
379
+ },
380
+ () => {
381
+ return pair({ type: "numbers", numbers: [] }, iStream);
382
+ }
383
+ )
384
+
385
+ }
386
+
387
+ function parseNumber(iStream) {
388
+ return or(
389
+ () => {
390
+ let nStream = iStream;
391
+ if (nStream.head() === "-") {
392
+ nStream = nStream.tail();
393
+ const { left: D, right: nStream2 } = parseD(nStream);
394
+ if (nStream2.head() === ".") {
395
+ const { left: D2, right: nStream3 } = parseD(nStream2.tail()); // remove .
396
+ return pair({ type: "number", number: Number.parseFloat(`-${D.d}.${D2.d}`) }, nStream3);
397
+ }
398
+ }
399
+ throw new Error("fail to parse -D.D");
400
+ },
401
+ () => {
402
+ const { left: D, right: nStream } = parseD(iStream);
403
+ if (nStream.head() === ".") {
404
+ const { left: D2, right: nStream2 } = parseD(nStream.tail()); // remove .
405
+ return pair({ type: "number", number: Number.parseFloat(`${D.d}.${D2.d}`) }, nStream2);
406
+ }
407
+ throw new Error("fail to parse D.D");
408
+ },
409
+ () => {
410
+ let nStream = iStream;
411
+ if (nStream.head() === "-") {
412
+ nStream = nStream.tail();
413
+ const { left: D, right: nStream2 } = parseD(nStream);
414
+ return pair({ type: "number", number: Number.parseFloat(`-${D.d}`) }, nStream2);
415
+ }
416
+ throw new Error("fail to parse -D");
417
+ },
418
+ () => {
419
+ const { left: D, right: nStream } = parseD(iStream);
420
+ if (D.d === "") throw new Error("fail to parse D");
421
+ return pair({ type: "number", number: Number.parseFloat(`${D.d}`) }, nStream)
422
+ }
423
+ )
424
+ }
425
+
426
+ function parseD(iStream) {
427
+ return or(
428
+ () => {
429
+ const char = iStream.head();
430
+ if (/^[0-9]$/.test(char)) {
431
+ const nStream = iStream.tail();
432
+ const { left: D, right: nStream2 } = parseD(nStream);
433
+ return pair({ type: "D", d: `${char}${D.d}` }, nStream2);
434
+ }
435
+ throw new Error("Caught exception in parsing D");
436
+ },
437
+ () => {
438
+ return pair({ type: "D", d: "" }, iStream);
439
+ }
440
+ )
441
+
442
+ }
443
+
444
+ function eatWhile(predicate) {
445
+ return iStream => {
446
+ let s = iStream;
447
+ while (!s.isEmpty() && predicate(s.head())) {
448
+ s = s.tail();
449
+ }
450
+ return s;
304
451
  }
305
- return s;
306
452
  }
307
453
 
454
+ //========================================================================================
455
+ /* *
456
+ * SVG READER *
457
+ * */
458
+ //========================================================================================
459
+
460
+ function addFirstPointIfNeeded(currentPos, path, keyPointPath) {
461
+ if (keyPointPath.length === 0) {
462
+ path.push(currentPos);
463
+ keyPointPath.push(currentPos);
464
+ }
465
+ }
466
+
467
+ function readPath(svg, tagNode) {
468
+ let path = [];
469
+ let keyPointPath = [];
470
+ const samples = 10;
471
+ let currentPos = Vec2();
472
+ const [svgPath] = tagNode.Attrs.attributes.filter(a => a.attributeName === "d");
473
+ const [idObj] = tagNode.Attrs.attributes.filter(a => a.attributeName === "id");
474
+ const id = idObj?.attributeValue ?? generateUniqueID(5);
475
+ const letter2action = {
476
+ "M": (vecs) => {
477
+ const [p] = vecs;
478
+ currentPos = p;
479
+ },
480
+ "m": (vecs) => {
481
+ const [p] = vecs;
482
+ currentPos = currentPos.add(p);
483
+ },
484
+ "L": (vecs) => {
485
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
486
+ for (let j = 0; j < vecs.length; j += 1) {
487
+ path.push(vecs[j]);
488
+ keyPointPath.push(vecs[j]);
489
+ currentPos = path.at(-1);
490
+ }
491
+ },
492
+ "l": (vecs) => {
493
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
494
+ for (let j = 0; j < vecs.length; j += 1) {
495
+ path.push(currentPos.add(vecs[j]));
496
+ keyPointPath.push(currentPos.add(vecs[j]));
497
+ currentPos = path.at(-1);
498
+ }
499
+ },
500
+ "V": (vecs) => {
501
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
502
+ for (let j = 0; j < vecs.length; j += 1) {
503
+ const newP = Vec2(currentPos.x, vecs[j].y);
504
+ path.push(newP);
505
+ keyPointPath.push(newP);
506
+ currentPos = path.at(-1);
507
+ }
508
+ },
509
+ "v": (vecs) => {
510
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
511
+ for (let j = 0; j < vecs.length; j += 1) {
512
+ path.push(currentPos.add(vecs[j]));
513
+ keyPointPath.push(currentPos.add(vecs[j]));
514
+ currentPos = path.at(-1);
515
+ }
516
+ },
517
+ "H": (vecs) => {
518
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
519
+ for (let j = 0; j < vecs.length; j += 1) {
520
+ const newP = Vec2(vecs[j].x, currentPos.y);
521
+ path.push(newP);
522
+ keyPointPath.push(newP);
523
+ currentPos = path.at(-1);
524
+ }
525
+ },
526
+ "h": (vecs) => {
527
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
528
+ for (let j = 0; j < vecs.length; j += 1) {
529
+ path.push(currentPos.add(vecs[j]));
530
+ keyPointPath.push(currentPos.add(vecs[j]));
531
+ currentPos = path.at(-1);
532
+ }
533
+ },
534
+ "Q": (vecs) => {
535
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
536
+ for (let j = 0; j < vecs.length; j += 2) {
537
+ const qb = qBezier(currentPos, vecs[j], vecs[j + 1]);
538
+ for (let i = 0; i < samples; i++) {
539
+ path.push(qb(i / (samples - 1)));
540
+ }
541
+ keyPointPath.push(currentPos, vecs[j], vecs[j + 1]);
542
+ currentPos = path.at(-1);
543
+ }
544
+ },
545
+ "q": (vecs) => {
546
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
547
+ for (let j = 0; j < vecs.length; j += 2) {
548
+ const qb = qBezier(currentPos, currentPos.add(vecs[j]), currentPos.add(vecs[j + 1]));
549
+ for (let i = 0; i < samples; i++) {
550
+ path.push(qb(i / (samples - 1)));
551
+ }
552
+ keyPointPath.push(currentPos, currentPos.add(vecs[j]), currentPos.add(vecs[j + 1]));
553
+ currentPos = path.at(-1);
554
+ }
555
+ },
556
+ "T": (vecs) => {
557
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
558
+ const [end] = vecs;
559
+ const prevKeyPoint = keyPointPath.at(-2) ? keyPointPath.at(-2) : keyPointPath.at(-1);
560
+ const control = currentPos.scale(2).sub(prevKeyPoint); // reflection bla bla
561
+ const qb = qBezier(currentPos, control, end);
562
+ for (let i = 0; i < samples; i++) {
563
+ path.push(qb(i / (samples - 1)));
564
+ }
565
+ keyPointPath.push(
566
+ currentPos,
567
+ control,
568
+ end
569
+ )
570
+ currentPos = path.at(-1);
571
+ },
572
+ "t": (vecs) => {
573
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
574
+ const [end] = vecs;
575
+ const prevKeyPoint = keyPointPath.at(-2) ? keyPointPath.at(-2) : keyPointPath.at(-1);
576
+ const control = currentPos.scale(2).sub(prevKeyPoint); // reflection bla bla
577
+ const qb = qBezier(currentPos, control, currentPos.add(end));
578
+ for (let i = 0; i < samples; i++) {
579
+ path.push(qb(i / (samples - 1)));
580
+ }
581
+ keyPointPath.push(
582
+ currentPos,
583
+ control,
584
+ currentPos.add(end)
585
+ )
586
+ currentPos = path.at(-1);
587
+ },
588
+ "C": (vecs) => {
589
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
590
+ for (let j = 0; j < vecs.length; j += 3) {
591
+ const cb = cBezier(
592
+ currentPos,
593
+ vecs[j],
594
+ vecs[j + 1],
595
+ vecs[j + 2]
596
+ );
597
+ for (let i = 0; i < samples; i++) {
598
+ path.push(cb(i / (samples - 1)));
599
+ }
600
+ keyPointPath.push(
601
+ currentPos,
602
+ vecs[j],
603
+ vecs[j + 1],
604
+ vecs[j + 2]
605
+ )
606
+ currentPos = path.at(-1);
607
+ }
608
+ },
609
+ "c": (vecs) => {
610
+ addFirstPointIfNeeded(currentPos, path, keyPointPath);
611
+ for (let j = 0; j < vecs.length; j += 3) {
612
+ const cb = cBezier(
613
+ currentPos,
614
+ currentPos.add(vecs[j]),
615
+ currentPos.add(vecs[j + 1]),
616
+ currentPos.add(vecs[j + 2])
617
+ );
618
+ for (let i = 0; i < samples; i++) {
619
+ path.push(cb(i / (samples - 1)));
620
+ }
621
+ keyPointPath.push(
622
+ currentPos,
623
+ currentPos.add(vecs[j]),
624
+ currentPos.add(vecs[j + 1]),
625
+ currentPos.add(vecs[j + 2])
626
+ )
627
+ currentPos = path.at(-1);
628
+ }
629
+ },
630
+ "Z": () => {
631
+ if (keyPointPath.length === 0) return;
632
+ path.push(keyPointPath[0]);
633
+ keyPointPath.push(keyPointPath[0]);
634
+ if (!svg.defPaths[id]) {
635
+ svg.defPaths[id] = [];
636
+ svg.defKeyPointPaths[id] = [];
637
+ }
638
+ svg.defPaths[id].push(path);
639
+ svg.defKeyPointPaths[id].push(keyPointPath);
640
+ path = [];
641
+ keyPointPath = [];
642
+ },
643
+ "z": () => {
644
+ if (keyPointPath.length === 0) return;
645
+ path.push(keyPointPath[0]);
646
+ keyPointPath.push(keyPointPath[0]);
647
+ if (!svg.defPaths[id]) {
648
+ svg.defPaths[id] = [];
649
+ svg.defKeyPointPaths[id] = [];
650
+ }
651
+ svg.defPaths[id].push(path);
652
+ svg.defKeyPointPaths[id].push(keyPointPath);
653
+ path = [];
654
+ keyPointPath = [];
655
+ }
656
+ }
657
+ const { actions } = parseSvgPath(svgPath.attributeValue);
658
+ const vectorizedActions = actions
659
+ .map(({ letter, numbers }) => {
660
+ const vectors = [];
661
+ const l = letter.toLowerCase();
662
+ if (l === "v" || l === "h") {
663
+ for (let i = 0; i < numbers.length; i += 1) {
664
+ if (l === "v") vectors.push(Vec2(0, numbers[i]));
665
+ if (l === "h") vectors.push(Vec2(numbers[i], 0));
666
+ }
667
+ } else {
668
+ for (let i = 0; i < numbers.length; i += 2) {
669
+ vectors.push(Vec2(numbers[i], numbers[i + 1]));
670
+ }
671
+ }
672
+ return { letter, vectors };
673
+ })
674
+ vectorizedActions.forEach(({ letter, vectors }) => {
675
+ return (letter2action?.[letter] ?? (() => { }))(vectors);
676
+ });
677
+ if (path.length > 0) {
678
+ if (!svg.defPaths[id]) {
679
+ svg.defPaths[id] = [];
680
+ svg.defKeyPointPaths[id] = [];
681
+ }
682
+ svg.defPaths[id].push(path);
683
+ svg.defKeyPointPaths[id].push(keyPointPath);
684
+ }
685
+ }
686
+
687
+ 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));
688
+ const dot = (f, g) => x => f(g(x));
689
+
690
+ function readTransform(svg, transformNode, transform = transformBuilder()) {
691
+ (transformNode?.StartTag ?? transformNode?.EmptyTag)
692
+ ?.Attrs
693
+ .attributes
694
+ .filter(x => x.attributeName === "transform")
695
+ .forEach(({ attributeValue }) => {
696
+ const params = attributeValue.match(/-?\d+\.?\d*/g).map(Number);
697
+ if (attributeValue.includes("matrix")) {
698
+ transform = dot(transform, transformBuilder(...params));
699
+ }
700
+ if (attributeValue.includes("translate")) {
701
+ transform = dot(transform, transformBuilder(1, 0, 0, 1, ...params))
702
+ }
703
+ if (attributeValue.includes("scale")) {
704
+ transform = dot(transform, transformBuilder(params[0], 0, params[1], 0, 0, 0))
705
+ }
706
+ })
707
+ const nodeStack = [...(transformNode?.InnerSVG?.innerSvgs?.map(x => x.SVG) ?? [])];
708
+ while (nodeStack.length > 0) {
709
+ const currentNode = nodeStack.shift(); // dequeue
710
+ const node = currentNode?.StartTag ?? currentNode?.EmptyTag;
711
+ const tag = node?.tag;
712
+ if (tag === "g") {
713
+ readTransform(svg, currentNode, transform);
714
+ continue;
715
+ }
716
+ if (tag === "use") {
717
+ const useParams = {
718
+ id: undefined,
719
+ transform
720
+ }
721
+ node.Attrs?.attributes.forEach(({ attributeName, attributeValue }) => {
722
+ if (attributeName === "xlink:href") {
723
+ useParams.id = attributeValue.slice(1);
724
+ }
725
+ if (attributeName === "transform") {
726
+ const params = attributeValue.match(/-?\d+\.?\d*/g).map(Number);
727
+ if (attributeValue.includes("scale")) {
728
+ useParams.transform = dot(useParams.transform, transformBuilder(params[0], 0, 0, params[0], 0, 0));
729
+ }
730
+ }
731
+ if (attributeName === "x") {
732
+ useParams.transform = dot(useParams.transform, transformBuilder(1, 0, 0, 1, Number(attributeValue), 0));
733
+ }
734
+ if (attributeName === "y") {
735
+ useParams.transform = dot(useParams.transform, transformBuilder(1, 0, 0, 1, 0, Number(attributeValue)));
736
+ }
737
+ });
738
+ if (useParams.id && svg.defPaths[useParams.id]) {
739
+ svg.paths.push(svg.defPaths[useParams.id].map(paths => paths.map(useParams.transform)));
740
+ svg.keyPointPaths.push(svg.defKeyPointPaths[useParams.id].map(paths => paths.map(useParams.transform)));
741
+ }
742
+ }
743
+ nodeStack.push(...(currentNode?.InnerSVG?.innerSvgs?.map(x => x.SVG) ?? []));
744
+ }
745
+
746
+ }
747
+
748
+ function readSVGNode(svgNode) {
749
+ const svg = {
750
+ width: undefined,
751
+ height: undefined,
752
+ viewBox: {},
753
+ defPaths: {},
754
+ defKeyPointPaths: {},
755
+ paths: [],
756
+ keyPointPaths: []
757
+ }
758
+ const nodeStack = [svgNode];
759
+ while (nodeStack.length > 0) {
760
+ const currentNode = nodeStack.shift(); // dequeue
761
+ const tag = currentNode?.StartTag?.tag ?? currentNode?.EmptyTag?.tag;
762
+ if (tag === "svg") {
763
+ currentNode
764
+ .StartTag
765
+ .Attrs
766
+ .attributes
767
+ .forEach(attr => {
768
+ if (attr.attributeName === "viewBox") {
769
+ const vb = attr.attributeValue;
770
+ const [x, y, w, h] = vb.split(" ").map(x => Number.parseFloat(x));
771
+ svg.viewBox.min = Vec2(x, y);
772
+ svg.viewBox.max = Vec2(x + w, y + h);
773
+ }
774
+ if (attr.attributeName === "width") {
775
+ svg.width = Number.parseInt(attr.attributeValue);
776
+ }
777
+ if (attr.attributeName === "height") {
778
+ svg.height = Number.parseInt(attr.attributeValue);
779
+ }
780
+ })
781
+ }
782
+ if (tag === "defs") {
783
+ const defNodes = (currentNode?.InnerSVG?.innerSvgs?.map(x => x.SVG) ?? []);
784
+ defNodes.forEach(defNode => {
785
+ const defTag = defNode?.StartTag?.tag ?? currentNode?.EmptyTag?.tag;
786
+ if (defTag !== "path") return;
787
+ readPath(svg, defNode.EmptyTag ?? defNode.StartTag);
788
+ })
789
+ continue;
790
+ }
791
+ if (tag === "g") {
792
+ readTransform(svg, currentNode);
793
+ continue;
794
+ }
795
+ if (tag === "path") {
796
+ readPath(svg, currentNode.EmptyTag ?? currentNode.StartTag);
797
+ }
798
+ nodeStack.push(...(currentNode?.InnerSVG?.innerSvgs?.map(x => x.SVG) ?? []));
799
+ }
800
+ if(svg.paths.length === 0) {
801
+ Object.values(svg.defPaths).forEach(paths => svg.paths.push(paths));
802
+ Object.values(svg.defKeyPointPaths).forEach(paths => svg.keyPointPaths.push(paths));
803
+ }
804
+ return svg;
805
+ }
806
+
807
+
308
808
  //========================================================================================
309
809
  /* *
310
810
  * UTILS *
@@ -348,3 +848,16 @@ function stream(stringOrArray) {
348
848
  }
349
849
  };
350
850
  }
851
+
852
+ function generateUniqueID(length) {
853
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
854
+ const charactersLength = characters.length;
855
+
856
+ let randomID = '';
857
+
858
+ for (let i = 0; i < length; i++) {
859
+ randomID += characters[Math.floor(Math.random() * charactersLength)];
860
+ }
861
+
862
+ return randomID;
863
+ }
@@ -57,26 +57,27 @@ const setTimeOut = typeof window === "undefined" ? setTimeout : requestAnimation
57
57
  if(typeof window !== "undefined") window.globalAnimationIDs = [];
58
58
  export function loop(lambda) {
59
59
  let isFinished = false;
60
+ const loopControl = {
61
+ stop: () => {
62
+ isFinished = true;
63
+ },
64
+ play: () => play({ oldT: new Date().getTime(), time: 0 })
65
+ };
60
66
  const play = async ({ time, oldT }) => {
61
67
  const newT = new Date().getTime();
62
68
  const dt = (newT - oldT) * 1e-3;
63
69
 
64
70
  await lambda({ time, dt });
65
71
 
66
- if (isFinished) return;
72
+ if (isFinished) return loopControl;
67
73
  const id = setTimeOut(() => play({
68
74
  oldT: newT,
69
75
  time: time + dt,
70
76
  }));
71
77
  if(typeof window !== "undefined") window.globalAnimationIDs.push(id);
72
- }
73
- const loopControl = {
74
- stop: () => {
75
- isFinished = true;
76
- },
77
- play: () => play({ oldT: new Date().getTime(), time: 0 })
78
- };
79
78
 
79
+ return loopControl;
80
+ }
80
81
  return loopControl;
81
82
  }
82
83
 
package/src/index.js CHANGED
@@ -16,7 +16,7 @@ import Path from "./Geometry/Path.js"
16
16
  import Triangle from "./Geometry/Triangle.js"
17
17
  import Mesh from "./Geometry/Mesh.js"
18
18
  import Ray from "./Ray/Ray.js"
19
- import parseSVG from "./Utils/SVG.js"
19
+ import parseSVG, {parseSvgPath} from "./Utils/SVG.js"
20
20
 
21
21
  export {
22
22
  Box,
@@ -40,6 +40,7 @@ export {
40
40
  NaiveScene,
41
41
  VoxelScene,
42
42
  RandomScene,
43
+ parseSvgPath,
43
44
  }
44
45
 
45
46
  export * from "./Utils/Math.js"