q5 4.5.0 → 4.5.3

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/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q5/q5",
3
- "version": "4.5.0",
3
+ "version": "4.5.3",
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.3",
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);
@@ -450,7 +453,7 @@ Q5.preloadMethods = {};
450
453
  Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.preloadMethods[n] = fn[n]);
451
454
 
452
455
  function Canvas(w, h, opt) {
453
- if (Q5._hasGlobal) return;
456
+ if (Q5._hasGlobal) return Promise.resolve(Q5.instances[0].canvas);
454
457
 
455
458
  let useC2D = w == 'c2d' || h == 'c2d' || opt == 'c2d' || opt?.renderer == 'c2d' || !Q5._esm;
456
459
 
@@ -584,7 +587,7 @@ Q5.modules.canvas = ($, q) => {
584
587
  if (!el) {
585
588
  // reattach canvas to the DOM
586
589
  document.getElementById(c.id)?.remove();
587
- addCanvas();
590
+ $._addCanvas();
588
591
  }
589
592
 
590
593
  if (window.IntersectionObserver) {
@@ -715,7 +718,7 @@ Q5.modules.canvas = ($, q) => {
715
718
  }
716
719
  };
717
720
 
718
- function addCanvas() {
721
+ $._addCanvas = () => {
719
722
  let el = $._parent;
720
723
  el ??= document.getElementsByTagName('main')[0];
721
724
  if (!el) {
@@ -730,8 +733,8 @@ Q5.modules.canvas = ($, q) => {
730
733
  if (document.body) document.body.appendChild(el);
731
734
  });
732
735
  }
733
- }
734
- addCanvas();
736
+ };
737
+ $._addCanvas();
735
738
  }
736
739
 
737
740
  $.resizeCanvas = (w, h) => {
@@ -3456,16 +3459,12 @@ Q5.modules.fes = ($) => {
3456
3459
  try {
3457
3460
  let res = await (await fetch(fileUrl)).text(),
3458
3461
  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}`;
3462
+ errLine = lines[lineNum - 1]?.trim();
3464
3463
 
3465
- if (inIframe) $.log(prefix + errorMsg);
3466
- else {
3467
- $.log(`%c${prefix}%c${errorMsg}`, 'background: #b7ebff; color: #000;', '');
3468
- }
3464
+ let type = '';
3465
+ if (e instanceof SyntaxError || e.name === 'SyntaxError') type = 'syntax';
3466
+
3467
+ Q5.friendlyError(fileBase, lineNum, errLine, type);
3469
3468
  } catch (err) {}
3470
3469
  };
3471
3470
 
@@ -3478,7 +3477,7 @@ Q5.modules.fes = ($) => {
3478
3477
  let match = line.match(/(https?:\/\/[^\s)]+\.js|\b\/[^\s)]+\.js)/);
3479
3478
  if (match) {
3480
3479
  let file = match[1];
3481
- if (!/q5|p5play/i.test(file)) {
3480
+ if (!/q5|p5play|q5play|brython/i.test(file)) {
3482
3481
  $._sketchFile = file;
3483
3482
  break;
3484
3483
  }
@@ -3517,6 +3516,19 @@ Q5.modules.fes = ($) => {
3517
3516
  checkLatestVersion();
3518
3517
  }
3519
3518
  };
3519
+
3520
+ Q5.friendlyError = (file, lineNum, detail) => {
3521
+ let bug = ['🐛', '🐞', '🐜', '🦗', '🦋', '🪲'][Math.floor(Math.random() * 6)],
3522
+ inIframe = window.self !== window.top,
3523
+ prefix = `q5 ${bug}`,
3524
+ msg = `Error in ${file} on line ${lineNum}`;
3525
+
3526
+ if (detail) msg += ':\n\n' + detail;
3527
+
3528
+ if (inIframe) return console.log(prefix + msg);
3529
+
3530
+ console.log(`%c${prefix}%c ${msg}`, 'background: #b7ebff; color: #000;', '');
3531
+ };
3520
3532
  Q5.modules.input = ($, q) => {
3521
3533
  if ($._isGraphics) return;
3522
3534
 
@@ -9349,17 +9361,17 @@ const userLangs = `
9349
9361
  update -> es:actualizar
9350
9362
  draw -> es:dibujar
9351
9363
  postProcess -> es:postProcesar
9352
- mousePressed -> es:alPresionarRatón
9353
- mouseReleased -> es:alSoltarRatón
9354
- mouseMoved -> es:alMoverRatón
9355
- mouseDragged -> es:alArrastrarRatón
9364
+ mousePressed -> es:alPresionarRaton
9365
+ mouseReleased -> es:alSoltarRaton
9366
+ mouseMoved -> es:alMoverRaton
9367
+ mouseDragged -> es:alArrastrarRaton
9356
9368
  doubleClicked -> es:dobleClic
9357
9369
  keyPressed -> es:alPresionarTecla
9358
9370
  keyReleased -> es:alSoltarTecla
9359
9371
  touchStarted -> es:alEmpezarToque
9360
9372
  touchEnded -> es:alTerminarToque
9361
9373
  touchMoved -> es:alMoverToque
9362
- mouseWheel -> es:ruedaRatón
9374
+ mouseWheel -> es:ruedaRaton
9363
9375
  `;
9364
9376
 
9365
9377
  const classLangs = {
@@ -9440,20 +9452,19 @@ Object.defineProperty(Q5, 'lang', {
9440
9452
 
9441
9453
  for (let className in classLangs) {
9442
9454
  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
- }
9455
+ if (!target) continue;
9456
+ let map = parseLangs(classLangs[className], val);
9457
+ for (let name in map) {
9458
+ let translatedName = map[name];
9459
+ if (target.hasOwnProperty(translatedName)) continue;
9460
+ Object.defineProperty(target, translatedName, {
9461
+ get: function () {
9462
+ return this[name];
9463
+ },
9464
+ set: function (v) {
9465
+ this[name] = v;
9466
+ }
9467
+ });
9457
9468
  }
9458
9469
  }
9459
9470
 
@@ -9512,6 +9523,13 @@ Q5.addHook('init', (q) => {
9512
9523
  for (let name in m) {
9513
9524
  let translatedName = m[name];
9514
9525
  q[translatedName] = q[name];
9526
+
9527
+ if (Q5._lang == 'es') {
9528
+ let unaccentedName = translatedName.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
9529
+ if (unaccentedName != translatedName) {
9530
+ q[unaccentedName] = q[name];
9531
+ }
9532
+ }
9515
9533
  }
9516
9534
  });
9517
9535
 
@@ -9524,6 +9542,8 @@ Q5.addHook('predraw', (q) => {
9524
9542
  'frameCount',
9525
9543
  'mouseX',
9526
9544
  'mouseY',
9545
+ 'pmouseX',
9546
+ 'pmouseY',
9527
9547
  'movedX',
9528
9548
  'movedY',
9529
9549
  'mouseIsPressed',
@@ -9536,7 +9556,14 @@ Q5.addHook('predraw', (q) => {
9536
9556
 
9537
9557
  // sync properties
9538
9558
  for (let p of props) {
9539
- if (m[p]) q[m[p]] = q[p];
9559
+ if (!m[p]) continue;
9560
+ q[m[p]] = q[p];
9561
+ if (Q5._lang == 'es') {
9562
+ let unaccentedName = m[p].normalize('NFD').replace(/[\u0300-\u036f]/g, '');
9563
+ if (unaccentedName != m[p]) {
9564
+ q[unaccentedName] = q[p];
9565
+ }
9566
+ }
9540
9567
  }
9541
9568
  });
9542
9569
  const runPython = async function () {
@@ -9554,35 +9581,10 @@ const runPython = async function () {
9554
9581
  document.head.appendChild(script);
9555
9582
  });
9556
9583
 
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');
9584
+ await loadScript('https://cdn.jsdelivr.net/npm/brython@3.14.0/brython.min.js');
9585
+ await loadScript('https://cdn.jsdelivr.net/npm/brython@3.14.0/brython_stdlib.min.js');
9559
9586
  }
9560
9587
 
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
9588
  let code = '';
9587
9589
  for (const script of scripts) {
9588
9590
  code += script.src ? await (await fetch(script.src)).text() : script.innerText;
@@ -9591,30 +9593,111 @@ window._runQ5PY = runQ5PY
9591
9593
  const useWebGPU = !code.slice(0, code.indexOf('\n')).includes('C2D'),
9592
9594
  q5py = useWebGPU ? await Q5.WebGPU() : new Q5();
9593
9595
 
9596
+ // `window.Canvas` returns a promise that resolves when Q5 is ready
9597
+ // but `q5py.Canvas` returns the renderer synchronously
9598
+ // so to make Brython happy with `await Canvas()` we need to make it async
9599
+ const Canvas = q5py.Canvas;
9600
+ q5py.Canvas = async (...a) => Canvas(...a);
9601
+
9594
9602
  code = code.replaceAll('\n', '\n\t');
9595
9603
 
9596
9604
  code = `
9597
- async def __run_code():
9598
- pass
9599
-
9605
+ async def __run():
9600
9606
  ${code}
9601
9607
 
9602
- q5_state_vars = ["mouseX", "mouseY", "pmouseX", "pmouseY", "width", "height", "frameCount", "deltaTime", "mouseIsPressed", "mouseButton", "keyIsPressed", "key", "keyCode", "touches", "movedX", "movedY"]
9608
+ _q5_state_vars = ["mouseX", "mouseY", "pmouseX", "pmouseY", "width", "height", "frameCount", "deltaTime", "mouseIsPressed", "mouseButton", "keyIsPressed", "key", "keyCode", "touches", "movedX", "movedY"]
9603
9609
 
9604
9610
  def _sync_and_call(fn):
9605
9611
  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)
9612
+ try:
9613
+ for var in _q5_state_vars:
9614
+ if hasattr(q5py, var):
9615
+ ns[var] = getattr(q5py, var)
9616
+ return fn(*args)
9617
+ except Exception as e:
9618
+ window._pyErr(_err())
9619
+ raise e
9610
9620
  return _wrapper
9611
9621
 
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]))
9622
+ for fn_name in ["update", "draw", "mousePressed", "mouseReleased", "mouseMoved", "mouseDragged", "mouseClicked", "doubleClicked", "mouseWheel", "keyPressed", "keyReleased", "keyTyped", "touchStarted", "touchMoved", "touchEnded", "windowResized"]:
9623
+ if fn_name in locals():
9624
+ setattr(window, fn_name, _sync_and_call(locals()[fn_name]))
9615
9625
  `;
9616
9626
 
9617
- await window._runQ5PY(code, q5py);
9627
+ window._pyErr = (err, lineNum) => {
9628
+ if (typeof err === 'string' && err.includes('Traceback')) {
9629
+ let lines = err.split('\n');
9630
+ for (let i = 0; i < lines.length; i++) {
9631
+ const match = lines[i].match(/File "<string>", line (\d+)/);
9632
+ if (match) {
9633
+ lineNum = parseInt(match[1]);
9634
+ lines = lines.slice(i + 1);
9635
+ // de-indent the first two lines based on the first line's indentation
9636
+ const indentMatch = lines[0].match(/^\s+/);
9637
+ if (indentMatch) {
9638
+ const indent = indentMatch[0];
9639
+ for (let j = 0; j < Math.min(2, lines.length); j++) {
9640
+ lines[j] = lines[j].slice(indent.length);
9641
+ }
9642
+ }
9643
+ err = lines.join('\n');
9644
+ break;
9645
+ }
9646
+ }
9647
+ }
9648
+
9649
+ let file = scripts[0].src.split('/').at(-1);
9650
+ lineNum -= 2; // adjust for the wrapper code lines
9651
+ if (Q5.friendlyError) Q5.friendlyError(file, lineNum, err);
9652
+ else console.error(`Error in ${file} on line ${lineNum}:\n\n${err}`);
9653
+ };
9654
+
9655
+ brython();
9656
+
9657
+ // hide brython's internal logs by temporarily overriding console.log
9658
+ let log = console.log;
9659
+ console.log = function () {};
9660
+
9661
+ __BRYTHON__.runPythonSource(`
9662
+ from browser import window, aio
9663
+ import traceback
9664
+ import io
9665
+
9666
+ def _err():
9667
+ f = io.StringIO()
9668
+ traceback.print_exc(file=f)
9669
+ return f.getvalue()
9670
+
9671
+ async def _run_py(q5py, code):
9672
+ ns = globals().copy()
9673
+ ns['ns'] = ns
9674
+ ns['q5py'] = q5py
9675
+
9676
+ for attr in dir(q5py):
9677
+ if not attr.startswith('_'):
9678
+ try:
9679
+ ns[attr] = getattr(q5py, attr)
9680
+ except Exception:
9681
+ pass
9682
+
9683
+ try:
9684
+ exec(code, ns)
9685
+ except SyntaxError as e:
9686
+ return window._pyErr(_err(), e.lineno)
9687
+ except Exception as e:
9688
+ return window._pyErr(_err())
9689
+
9690
+ try:
9691
+ await ns["__run"]()
9692
+ except Exception as e:
9693
+ window._pyErr(_err())
9694
+
9695
+ window._runPy = _run_py
9696
+ `);
9697
+
9698
+ console.log = log;
9699
+
9700
+ await window._runPy(q5py, code);
9618
9701
  };
9619
9702
 
9620
9703
  if (typeof document == 'object') {