q5 4.5.0 → 4.5.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
@@ -47,7 +47,9 @@ p5.js is licensed under the LGPLv2, small sections of p5' code directly copied i
47
47
 
48
48
  q5 was inspired by the incredible work of [Ben Fry](https://benfry.com) and [Casey Reas](https://x.com/REAS) on Java [Processing](https://processingfoundation.org/) from 2001 to 2023, [Lauren McCarthy](http://lauren-mccarthy.com)'s work on [p5.js](https://p5js.org) from 2013 to 2019, and all contributors to these projects.
49
49
 
50
- ## Code Excerpt Sources
50
+ Huge thanks to all the [q5 contributors](https://github.com/q5js/q5.js/graphs/contributors)!
51
+
52
+ @evanalulu, @Tezumie, @keturn, @ormaq, @bertubi, @RedWilly, @Dukemz, @LingDong-
51
53
 
52
54
  WebGPU MSDF text rendering:
53
55
  https://webgpu.github.io/webgpu-samples/?sample=textRenderingMsdf
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q5/q5",
3
- "version": "4.5.0",
3
+ "version": "4.5.4",
4
4
  "license": "LGPL-3.0-only",
5
5
  "description": "Beginner friendly graphics powered by WebGPU, optimized for interactive art!",
6
6
  "author": "quinton-ashley",
@@ -24,6 +24,7 @@
24
24
  "home",
25
25
  "lang",
26
26
  "learn",
27
+ "teach",
27
28
  "test",
28
29
  ".npmignore",
29
30
  ".prettierignore",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "q5",
3
- "version": "4.5.0",
3
+ "version": "4.5.4",
4
4
  "description": "Beginner friendly graphics powered by WebGPU, optimized for interactive art!",
5
5
  "author": "quinton-ashley",
6
6
  "contributors": [
@@ -12,8 +12,27 @@
12
12
  ],
13
13
  "license": "LGPL-3.0-only",
14
14
  "homepage": "https://q5js.org/home",
15
- "main": "q5-server.js",
16
15
  "types": "q5.d.ts",
16
+ "browser": {
17
+ ".": "./q5.js",
18
+ "node:process": false
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./q5.d.ts",
23
+ "browser": "./q5.js",
24
+ "deno": "./q5-deno-server.js",
25
+ "node": "./q5-server.js",
26
+ "default": "./q5.js"
27
+ },
28
+ "./q5.min.js": "./q5.min.js",
29
+ "./q5.js": "./q5.js",
30
+ "./q5-server.js": {
31
+ "browser": null,
32
+ "deno": "./q5-deno-server.js",
33
+ "default": "./q5-server.js"
34
+ }
35
+ },
17
36
  "funding": [
18
37
  {
19
38
  "type": "patreon",
@@ -30,7 +49,7 @@
30
49
  ],
31
50
  "scripts": {
32
51
  "bundle": "cat src/q5-core.js src/q5-canvas.js src/q5-c2d-canvas.js src/q5-c2d-shapes.js src/q5-c2d-image.js src/q5-c2d-soft-filters.js src/q5-c2d-text.js src/q5-color.js src/q5-display.js src/q5-dom.js src/q5-fes.js src/q5-input.js src/q5-math.js src/q5-record.js src/q5-sound.js src/q5-util.js src/q5-vector.js src/q5-webgpu.js src/q5-lang.js src/q5-python.js > q5.js",
33
- "min": "terser q5.js --compress ecma=2025 --mangle > q5.min.js",
52
+ "min": "terser q5.js --compress ecma=2026 --mangle --output q5.min.js --source-map url=q5.min.js.map",
34
53
  "dist": "bun bundle && bun min",
35
54
  "tests": "jest test",
36
55
  "bld": "node lang/build.js",
@@ -61,6 +80,10 @@
61
80
  "jest-cli": "^29.7.0",
62
81
  "jsdom": "^25.0.1",
63
82
  "json2csv": "^6.0.0-alpha.2",
64
- "skia-canvas": "^1.0.2"
65
- }
83
+ "skia-canvas": "^1.0.2",
84
+ "terser": "^5.46.1"
85
+ },
86
+ "trustedDependencies": [
87
+ "skia-canvas"
88
+ ]
66
89
  }
package/q5.d.ts CHANGED
@@ -2500,6 +2500,8 @@ declare global {
2500
2500
  * - If one numerical input is provided, returns a number between 0 and the provided value.
2501
2501
  * - If two numerical inputs are provided, returns a number between the two values.
2502
2502
  * - If an array is provided, returns a random element from the array.
2503
+ *
2504
+ * Return value can be the lower bound but can never exactly be the upper bound.
2503
2505
  * @param {number | any[]} [low] lower bound (inclusive) or an array
2504
2506
  * @param {number} [high] upper bound (exclusive)
2505
2507
  * @returns {number | any} a random number or element
@@ -4035,7 +4037,7 @@ declare global {
4035
4037
  *
4036
4038
  * q5.draw = function () {
4037
4039
  * shader(stripes);
4038
- * background(0);
4040
+ * plane(0, 0, width, height);
4039
4041
  *
4040
4042
  * resetShader();
4041
4043
  * triangle(-50, -50, 0, 50, 50, -50);
package/q5.js CHANGED
@@ -308,9 +308,9 @@ function Q5(scope, parent, renderer) {
308
308
 
309
309
  function wrapWithFES(name) {
310
310
  const fn = t[name] || $[name];
311
- $[name] = (event) => {
311
+ $[name] = function (...args) {
312
312
  try {
313
- return fn(event);
313
+ return fn.apply(this, args);
314
314
  } catch (e) {
315
315
  if ($._fes) $._fes(e);
316
316
  throw e;
@@ -322,6 +322,7 @@ function Q5(scope, parent, renderer) {
322
322
  await runHooks('presetup');
323
323
 
324
324
  readyResolve();
325
+ if ($._removed) return;
325
326
 
326
327
  if (t.preload || $.preload) {
327
328
  wrapWithFES('preload');
@@ -353,9 +354,9 @@ function Q5(scope, parent, renderer) {
353
354
  })
354
355
  ]);
355
356
 
356
- if (!$._disablePreload) {
357
- await $.loadAll();
358
- }
357
+ if ($._removed) return;
358
+ if (!$._disablePreload) await $.loadAll();
359
+ if ($._removed) return;
359
360
 
360
361
  $.setup ??= t.setup || (() => {});
361
362
  wrapWithFES('setup');
@@ -367,10 +368,12 @@ function Q5(scope, parent, renderer) {
367
368
  millisStart = performance.now();
368
369
  await $.setup();
369
370
  $._setupDone = true;
371
+
372
+ if ($._removed) return;
370
373
  if ($.ctx === null) $.createCanvas(200, 200);
371
374
  await runHooks('postsetup');
372
375
 
373
- if ($.frameCount) return;
376
+ if ($.frameCount || $._removed) return;
374
377
 
375
378
  $._lastFrameTime = performance.now() - 15;
376
379
  raf(_draw);
@@ -390,6 +393,7 @@ Q5._esm = this === undefined;
390
393
 
391
394
  Q5._instanceCount = 0;
392
395
  Q5.instances = [];
396
+ Q5.errorTolerant = false;
393
397
  Q5._friendlyError = (msg, func) => {
394
398
  if (!Q5.disableFriendlyErrors) console.error(func + ': ' + msg);
395
399
  };
@@ -450,7 +454,7 @@ Q5.preloadMethods = {};
450
454
  Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.preloadMethods[n] = fn[n]);
451
455
 
452
456
  function Canvas(w, h, opt) {
453
- if (Q5._hasGlobal) return;
457
+ if (Q5._hasGlobal) return Promise.resolve(Q5.instances[0].canvas);
454
458
 
455
459
  let useC2D = w == 'c2d' || h == 'c2d' || opt == 'c2d' || opt?.renderer == 'c2d' || !Q5._esm;
456
460
 
@@ -584,7 +588,7 @@ Q5.modules.canvas = ($, q) => {
584
588
  if (!el) {
585
589
  // reattach canvas to the DOM
586
590
  document.getElementById(c.id)?.remove();
587
- addCanvas();
591
+ $._addCanvas();
588
592
  }
589
593
 
590
594
  if (window.IntersectionObserver) {
@@ -715,7 +719,7 @@ Q5.modules.canvas = ($, q) => {
715
719
  }
716
720
  };
717
721
 
718
- function addCanvas() {
722
+ $._addCanvas = () => {
719
723
  let el = $._parent;
720
724
  el ??= document.getElementsByTagName('main')[0];
721
725
  if (!el) {
@@ -730,8 +734,8 @@ Q5.modules.canvas = ($, q) => {
730
734
  if (document.body) document.body.appendChild(el);
731
735
  });
732
736
  }
733
- }
734
- addCanvas();
737
+ };
738
+ $._addCanvas();
735
739
  }
736
740
 
737
741
  $.resizeCanvas = (w, h) => {
@@ -3456,16 +3460,12 @@ Q5.modules.fes = ($) => {
3456
3460
  try {
3457
3461
  let res = await (await fetch(fileUrl)).text(),
3458
3462
  lines = res.split('\n'),
3459
- errLine = lines[lineNum - 1]?.trim() ?? '',
3460
- bug = ['🐛', '🐞', '🐜', '🦗', '🦋', '🪲'][Math.floor(Math.random() * 6)],
3461
- inIframe = window.self !== window.top,
3462
- prefix = `q5.js ${bug}`,
3463
- errorMsg = ` Error in ${fileBase} on line ${lineNum}:\n\n${errLine}`;
3463
+ errLine = lines[lineNum - 1]?.trim();
3464
3464
 
3465
- if (inIframe) $.log(prefix + errorMsg);
3466
- else {
3467
- $.log(`%c${prefix}%c${errorMsg}`, 'background: #b7ebff; color: #000;', '');
3468
- }
3465
+ let type = '';
3466
+ if (e instanceof SyntaxError || e.name === 'SyntaxError') type = 'syntax';
3467
+
3468
+ Q5.friendlyError(fileBase, lineNum, errLine, type);
3469
3469
  } catch (err) {}
3470
3470
  };
3471
3471
 
@@ -3478,7 +3478,7 @@ Q5.modules.fes = ($) => {
3478
3478
  let match = line.match(/(https?:\/\/[^\s)]+\.js|\b\/[^\s)]+\.js)/);
3479
3479
  if (match) {
3480
3480
  let file = match[1];
3481
- if (!/q5|p5play/i.test(file)) {
3481
+ if (!/q5|p5play|q5play|brython/i.test(file)) {
3482
3482
  $._sketchFile = file;
3483
3483
  break;
3484
3484
  }
@@ -3517,6 +3517,19 @@ Q5.modules.fes = ($) => {
3517
3517
  checkLatestVersion();
3518
3518
  }
3519
3519
  };
3520
+
3521
+ Q5.friendlyError = (file, lineNum, detail) => {
3522
+ let bug = ['🐛', '🐞', '🐜', '🦗', '🦋', '🪲'][Math.floor(Math.random() * 6)],
3523
+ inIframe = window.self !== window.top,
3524
+ prefix = `q5 ${bug}`,
3525
+ msg = `Error in ${file} on line ${lineNum}`;
3526
+
3527
+ if (detail) msg += ':\n\n' + detail;
3528
+
3529
+ if (inIframe) return console.log(prefix + msg);
3530
+
3531
+ console.log(`%c${prefix}%c ${msg}`, 'background: #b7ebff; color: #000;', '');
3532
+ };
3520
3533
  Q5.modules.input = ($, q) => {
3521
3534
  if ($._isGraphics) return;
3522
3535
 
@@ -9349,17 +9362,17 @@ const userLangs = `
9349
9362
  update -> es:actualizar
9350
9363
  draw -> es:dibujar
9351
9364
  postProcess -> es:postProcesar
9352
- mousePressed -> es:alPresionarRatón
9353
- mouseReleased -> es:alSoltarRatón
9354
- mouseMoved -> es:alMoverRatón
9355
- mouseDragged -> es:alArrastrarRatón
9365
+ mousePressed -> es:alPresionarRaton
9366
+ mouseReleased -> es:alSoltarRaton
9367
+ mouseMoved -> es:alMoverRaton
9368
+ mouseDragged -> es:alArrastrarRaton
9356
9369
  doubleClicked -> es:dobleClic
9357
9370
  keyPressed -> es:alPresionarTecla
9358
9371
  keyReleased -> es:alSoltarTecla
9359
9372
  touchStarted -> es:alEmpezarToque
9360
9373
  touchEnded -> es:alTerminarToque
9361
9374
  touchMoved -> es:alMoverToque
9362
- mouseWheel -> es:ruedaRatón
9375
+ mouseWheel -> es:ruedaRaton
9363
9376
  `;
9364
9377
 
9365
9378
  const classLangs = {
@@ -9440,20 +9453,19 @@ Object.defineProperty(Q5, 'lang', {
9440
9453
 
9441
9454
  for (let className in classLangs) {
9442
9455
  let target = className == 'Q5' ? Q5 : Q5[className] ? Q5[className].prototype : null;
9443
- if (target) {
9444
- let map = parseLangs(classLangs[className], val);
9445
- for (let name in map) {
9446
- let translatedName = map[name];
9447
- if (target.hasOwnProperty(translatedName)) continue;
9448
- Object.defineProperty(target, translatedName, {
9449
- get: function () {
9450
- return this[name];
9451
- },
9452
- set: function (v) {
9453
- this[name] = v;
9454
- }
9455
- });
9456
- }
9456
+ if (!target) continue;
9457
+ let map = parseLangs(classLangs[className], val);
9458
+ for (let name in map) {
9459
+ let translatedName = map[name];
9460
+ if (target.hasOwnProperty(translatedName)) continue;
9461
+ Object.defineProperty(target, translatedName, {
9462
+ get: function () {
9463
+ return this[name];
9464
+ },
9465
+ set: function (v) {
9466
+ this[name] = v;
9467
+ }
9468
+ });
9457
9469
  }
9458
9470
  }
9459
9471
 
@@ -9512,6 +9524,13 @@ Q5.addHook('init', (q) => {
9512
9524
  for (let name in m) {
9513
9525
  let translatedName = m[name];
9514
9526
  q[translatedName] = q[name];
9527
+
9528
+ if (Q5._lang == 'es') {
9529
+ let unaccentedName = translatedName.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
9530
+ if (unaccentedName != translatedName) {
9531
+ q[unaccentedName] = q[name];
9532
+ }
9533
+ }
9515
9534
  }
9516
9535
  });
9517
9536
 
@@ -9524,6 +9543,8 @@ Q5.addHook('predraw', (q) => {
9524
9543
  'frameCount',
9525
9544
  'mouseX',
9526
9545
  'mouseY',
9546
+ 'pmouseX',
9547
+ 'pmouseY',
9527
9548
  'movedX',
9528
9549
  'movedY',
9529
9550
  'mouseIsPressed',
@@ -9536,7 +9557,14 @@ Q5.addHook('predraw', (q) => {
9536
9557
 
9537
9558
  // sync properties
9538
9559
  for (let p of props) {
9539
- if (m[p]) q[m[p]] = q[p];
9560
+ if (!m[p]) continue;
9561
+ q[m[p]] = q[p];
9562
+ if (Q5._lang == 'es') {
9563
+ let unaccentedName = m[p].normalize('NFD').replace(/[\u0300-\u036f]/g, '');
9564
+ if (unaccentedName != m[p]) {
9565
+ q[unaccentedName] = q[p];
9566
+ }
9567
+ }
9540
9568
  }
9541
9569
  });
9542
9570
  const runPython = async function () {
@@ -9554,67 +9582,135 @@ const runPython = async function () {
9554
9582
  document.head.appendChild(script);
9555
9583
  });
9556
9584
 
9557
- await loadScript('https://cdn.jsdelivr.net/npm/brython@3.12.0/brython.js');
9558
- await loadScript('https://cdn.jsdelivr.net/npm/brython@3.12.0/brython_stdlib.min.js');
9585
+ await loadScript('https://cdn.jsdelivr.net/npm/brython@3.14.0/brython.min.js');
9586
+ await loadScript('https://cdn.jsdelivr.net/npm/brython@3.14.0/brython_stdlib.min.js');
9559
9587
  }
9560
9588
 
9561
- brython();
9562
-
9563
- __BRYTHON__.runPythonSource(`
9564
- from browser import window, aio
9565
-
9566
- async def runQ5PY(code, q5py):
9567
- ns = globals().copy()
9568
- ns['ns'] = ns
9569
- ns['q5py'] = q5py
9570
-
9571
- for attr in dir(q5py):
9572
- if not attr.startswith('_'):
9573
- try:
9574
- ns[attr] = getattr(q5py, attr)
9575
- except Exception:
9576
- pass
9577
-
9578
- exec(code, ns)
9579
-
9580
- if "__run_code" in ns:
9581
- await ns["__run_code"]()
9582
-
9583
- window._runQ5PY = runQ5PY
9584
- `);
9585
-
9586
9589
  let code = '';
9587
9590
  for (const script of scripts) {
9588
9591
  code += script.src ? await (await fetch(script.src)).text() : script.innerText;
9589
9592
  }
9590
9593
 
9591
9594
  const useWebGPU = !code.slice(0, code.indexOf('\n')).includes('C2D'),
9592
- q5py = useWebGPU ? await Q5.WebGPU() : new Q5();
9595
+ q = useWebGPU ? await Q5.WebGPU() : new Q5();
9593
9596
 
9594
- code = code.replaceAll('\n', '\n\t');
9597
+ // `window.Canvas` returns a promise that resolves when Q5 is ready
9598
+ // but `q5py.Canvas` returns the renderer synchronously
9599
+ // so to make Brython happy with `await Canvas()` we need to make it async
9600
+ const Canvas = q.Canvas;
9601
+ q.Canvas = async (...a) => Canvas(...a);
9595
9602
 
9596
- code = `
9597
- async def __run_code():
9598
- pass
9603
+ // add a tab before each line of code to nest it inside the __run function
9604
+ // but not within triple-quoted strings
9605
+ code = code
9606
+ .split(/(\"\"\"[\s\S]*?\"\"\"|\'\'\'[\s\S]*?\'\'\')/g)
9607
+ .map((part, i) => (i % 2 === 0 ? part.replaceAll('\n', '\n\t') : part))
9608
+ .join('');
9599
9609
 
9610
+ code = `
9611
+ async def __run(q):
9600
9612
  ${code}
9601
9613
 
9602
- q5_state_vars = ["mouseX", "mouseY", "pmouseX", "pmouseY", "width", "height", "frameCount", "deltaTime", "mouseIsPressed", "mouseButton", "keyIsPressed", "key", "keyCode", "touches", "movedX", "movedY"]
9614
+ _state_vars = ["mouseX", "mouseY", "pmouseX", "pmouseY", "width", "height", "frameCount", "deltaTime", "mouseIsPressed", "mouseButton", "keyIsPressed", "key", "keyCode", "touches", "movedX", "movedY"]
9615
+
9616
+ _usr_fns = ["update", "draw", "postProcess", "mousePressed", "mouseReleased", "mouseMoved", "mouseDragged", "mouseClicked", "doubleClicked", "mouseWheel", "keyPressed", "keyReleased", "keyTyped", "touchStarted", "touchMoved", "touchEnded", "windowResized"]
9603
9617
 
9604
9618
  def _sync_and_call(fn):
9605
9619
  def _wrapper(*args):
9606
- for _var in q5_state_vars:
9607
- if hasattr(q5py, _var):
9608
- ns[_var] = getattr(q5py, _var)
9609
- return fn(*args)
9620
+ try:
9621
+ for var in _state_vars:
9622
+ if hasattr(q, var):
9623
+ ns[var] = getattr(q, var)
9624
+ return fn(*args)
9625
+ except Exception as e:
9626
+ window._pyErr(_err())
9627
+ if not window.Q5.errorTolerant: noLoop()
9610
9628
  return _wrapper
9611
9629
 
9612
- for _fn_name in ["update", "draw", "mousePressed", "mouseReleased", "mouseMoved", "mouseDragged", "mouseClicked", "doubleClicked", "mouseWheel", "keyPressed", "keyReleased", "keyTyped", "touchStarted", "touchMoved", "touchEnded", "windowResized"]:
9613
- if _fn_name in locals():
9614
- setattr(window, _fn_name, _sync_and_call(locals()[_fn_name]))
9630
+ for fn_name in _usr_fns:
9631
+ if fn_name in locals():
9632
+ setattr(window, fn_name, _sync_and_call(locals()[fn_name]))
9615
9633
  `;
9616
9634
 
9617
- await window._runQ5PY(code, q5py);
9635
+ window._pyErr = (err, lineNum) => {
9636
+ if (typeof err === 'string' && err.includes('Traceback')) {
9637
+ let lines = err.split('\n');
9638
+ for (let i = lines.length - 1; i > 0; i--) {
9639
+ const match = lines[i].match(/File "<string>", line (\d+)/);
9640
+ if (match) {
9641
+ lineNum = parseInt(match[1]);
9642
+ lines = lines.slice(i + 1);
9643
+ // de-indent the first two lines based on the first line's indentation
9644
+ const indentMatch = lines[0].match(/^\s+/);
9645
+ if (indentMatch) {
9646
+ const indent = indentMatch[0];
9647
+ for (let j = 0; j < Math.min(2, lines.length); j++) {
9648
+ lines[j] = lines[j].slice(indent.length);
9649
+ }
9650
+ } else {
9651
+ let line = code.split('\n')[lineNum - 1].trim();
9652
+ lines.unshift(line, '');
9653
+ }
9654
+ err = lines.join('\n');
9655
+ break;
9656
+ }
9657
+ }
9658
+ }
9659
+
9660
+ let file = scripts[0].src || scripts[0]['data-filename'] || 'sketch.py';
9661
+ file = file.split('/').at(-1);
9662
+
9663
+ lineNum -= 2; // adjust for the wrapper code lines
9664
+ if (Q5.friendlyError) Q5.friendlyError(file, lineNum, err);
9665
+ else console.error(`Error in ${file} on line ${lineNum}:\n\n${err}`);
9666
+ };
9667
+
9668
+ brython();
9669
+
9670
+ // hide brython's internal logs by temporarily overriding console.log
9671
+ let log = console.log;
9672
+ console.log = function () {};
9673
+
9674
+ __BRYTHON__.runPythonSource(`
9675
+ from browser import window, aio
9676
+ import traceback
9677
+ import io
9678
+
9679
+ def _err():
9680
+ f = io.StringIO()
9681
+ traceback.print_exc(file=f)
9682
+ return f.getvalue()
9683
+
9684
+ async def _run_py(q, code):
9685
+ ns = globals().copy()
9686
+ ns['ns'] = ns
9687
+ ns['Q5'] = window.Q5
9688
+
9689
+ for attr in dir(q):
9690
+ if not attr.startswith('_'):
9691
+ try:
9692
+ ns[attr] = getattr(q, attr)
9693
+ except Exception:
9694
+ pass
9695
+
9696
+ try:
9697
+ exec(code, ns)
9698
+ except SyntaxError as e:
9699
+ return window._pyErr(_err(), e.lineno)
9700
+ except Exception as e:
9701
+ return window._pyErr(_err())
9702
+
9703
+ try:
9704
+ await ns["__run"](q)
9705
+ except Exception as e:
9706
+ window._pyErr(_err())
9707
+
9708
+ window._runPy = _run_py
9709
+ `);
9710
+
9711
+ console.log = log;
9712
+
9713
+ await window._runPy(q, code);
9618
9714
  };
9619
9715
 
9620
9716
  if (typeof document == 'object') {