q5 4.4.5 → 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.4.5",
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.4.5",
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",
@@ -29,8 +48,8 @@
29
48
  }
30
49
  ],
31
50
  "scripts": {
32
- "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 > q5.js",
33
- "min": "terser q5.js --compress ecma=2025 --mangle > q5.min.js",
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",
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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 4.4
3
+ * @version 4.5
4
4
  * @author quinton-ashley
5
5
  * @contributors evanalulu, Tezumie, ormaq, Dukemz, LingDong-
6
6
  * @license LGPL-3.0
@@ -298,6 +298,7 @@ function Q5(scope, parent, renderer) {
298
298
  if (Q5[name]) $[name] = Q5[name];
299
299
  else {
300
300
  Object.defineProperty(Q5, name, {
301
+ configurable: true,
301
302
  get: () => $[name],
302
303
  set: (fn) => ($[name] = fn)
303
304
  });
@@ -307,9 +308,9 @@ function Q5(scope, parent, renderer) {
307
308
 
308
309
  function wrapWithFES(name) {
309
310
  const fn = t[name] || $[name];
310
- $[name] = (event) => {
311
+ $[name] = function (...args) {
311
312
  try {
312
- return fn(event);
313
+ return fn.apply(this, args);
313
314
  } catch (e) {
314
315
  if ($._fes) $._fes(e);
315
316
  throw e;
@@ -321,6 +322,7 @@ function Q5(scope, parent, renderer) {
321
322
  await runHooks('presetup');
322
323
 
323
324
  readyResolve();
325
+ if ($._removed) return;
324
326
 
325
327
  if (t.preload || $.preload) {
326
328
  wrapWithFES('preload');
@@ -352,9 +354,9 @@ function Q5(scope, parent, renderer) {
352
354
  })
353
355
  ]);
354
356
 
355
- if (!$._disablePreload) {
356
- await $.loadAll();
357
- }
357
+ if ($._removed) return;
358
+ if (!$._disablePreload) await $.loadAll();
359
+ if ($._removed) return;
358
360
 
359
361
  $.setup ??= t.setup || (() => {});
360
362
  wrapWithFES('setup');
@@ -366,10 +368,12 @@ function Q5(scope, parent, renderer) {
366
368
  millisStart = performance.now();
367
369
  await $.setup();
368
370
  $._setupDone = true;
371
+
372
+ if ($._removed) return;
369
373
  if ($.ctx === null) $.createCanvas(200, 200);
370
374
  await runHooks('postsetup');
371
375
 
372
- if ($.frameCount) return;
376
+ if ($.frameCount || $._removed) return;
373
377
 
374
378
  $._lastFrameTime = performance.now() - 15;
375
379
  raf(_draw);
@@ -449,7 +453,7 @@ Q5.preloadMethods = {};
449
453
  Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.preloadMethods[n] = fn[n]);
450
454
 
451
455
  function Canvas(w, h, opt) {
452
- if (Q5._hasGlobal) return;
456
+ if (Q5._hasGlobal) return Promise.resolve(Q5.instances[0].canvas);
453
457
 
454
458
  let useC2D = w == 'c2d' || h == 'c2d' || opt == 'c2d' || opt?.renderer == 'c2d' || !Q5._esm;
455
459
 
@@ -491,7 +495,7 @@ if (typeof window == 'object') {
491
495
  window.addEventListener('pagehide', cleanup);
492
496
  } else global.window = 0;
493
497
 
494
- Q5.version = Q5.VERSION = '4.4';
498
+ Q5.version = Q5.VERSION = '4.5';
495
499
 
496
500
  if (typeof document == 'object') {
497
501
  document.addEventListener('DOMContentLoaded', () => {
@@ -583,7 +587,7 @@ Q5.modules.canvas = ($, q) => {
583
587
  if (!el) {
584
588
  // reattach canvas to the DOM
585
589
  document.getElementById(c.id)?.remove();
586
- addCanvas();
590
+ $._addCanvas();
587
591
  }
588
592
 
589
593
  if (window.IntersectionObserver) {
@@ -714,7 +718,7 @@ Q5.modules.canvas = ($, q) => {
714
718
  }
715
719
  };
716
720
 
717
- function addCanvas() {
721
+ $._addCanvas = () => {
718
722
  let el = $._parent;
719
723
  el ??= document.getElementsByTagName('main')[0];
720
724
  if (!el) {
@@ -729,8 +733,8 @@ Q5.modules.canvas = ($, q) => {
729
733
  if (document.body) document.body.appendChild(el);
730
734
  });
731
735
  }
732
- }
733
- addCanvas();
736
+ };
737
+ $._addCanvas();
734
738
  }
735
739
 
736
740
  $.resizeCanvas = (w, h) => {
@@ -3455,16 +3459,12 @@ Q5.modules.fes = ($) => {
3455
3459
  try {
3456
3460
  let res = await (await fetch(fileUrl)).text(),
3457
3461
  lines = res.split('\n'),
3458
- errLine = lines[lineNum - 1]?.trim() ?? '',
3459
- bug = ['🐛', '🐞', '🐜', '🦗', '🦋', '🪲'][Math.floor(Math.random() * 6)],
3460
- inIframe = window.self !== window.top,
3461
- prefix = `q5.js ${bug}`,
3462
- errorMsg = ` Error in ${fileBase} on line ${lineNum}:\n\n${errLine}`;
3462
+ errLine = lines[lineNum - 1]?.trim();
3463
3463
 
3464
- if (inIframe) $.log(prefix + errorMsg);
3465
- else {
3466
- $.log(`%c${prefix}%c${errorMsg}`, 'background: #b7ebff; color: #000;', '');
3467
- }
3464
+ let type = '';
3465
+ if (e instanceof SyntaxError || e.name === 'SyntaxError') type = 'syntax';
3466
+
3467
+ Q5.friendlyError(fileBase, lineNum, errLine, type);
3468
3468
  } catch (err) {}
3469
3469
  };
3470
3470
 
@@ -3477,7 +3477,7 @@ Q5.modules.fes = ($) => {
3477
3477
  let match = line.match(/(https?:\/\/[^\s)]+\.js|\b\/[^\s)]+\.js)/);
3478
3478
  if (match) {
3479
3479
  let file = match[1];
3480
- if (!/q5|p5play/i.test(file)) {
3480
+ if (!/q5|p5play|q5play|brython/i.test(file)) {
3481
3481
  $._sketchFile = file;
3482
3482
  break;
3483
3483
  }
@@ -3516,6 +3516,19 @@ Q5.modules.fes = ($) => {
3516
3516
  checkLatestVersion();
3517
3517
  }
3518
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
+ };
3519
3532
  Q5.modules.input = ($, q) => {
3520
3533
  if ($._isGraphics) return;
3521
3534
 
@@ -9348,17 +9361,17 @@ const userLangs = `
9348
9361
  update -> es:actualizar
9349
9362
  draw -> es:dibujar
9350
9363
  postProcess -> es:postProcesar
9351
- mousePressed -> es:alPresionarRatón
9352
- mouseReleased -> es:alSoltarRatón
9353
- mouseMoved -> es:alMoverRatón
9354
- mouseDragged -> es:alArrastrarRatón
9364
+ mousePressed -> es:alPresionarRaton
9365
+ mouseReleased -> es:alSoltarRaton
9366
+ mouseMoved -> es:alMoverRaton
9367
+ mouseDragged -> es:alArrastrarRaton
9355
9368
  doubleClicked -> es:dobleClic
9356
9369
  keyPressed -> es:alPresionarTecla
9357
9370
  keyReleased -> es:alSoltarTecla
9358
9371
  touchStarted -> es:alEmpezarToque
9359
9372
  touchEnded -> es:alTerminarToque
9360
9373
  touchMoved -> es:alMoverToque
9361
- mouseWheel -> es:ruedaRatón
9374
+ mouseWheel -> es:ruedaRaton
9362
9375
  `;
9363
9376
 
9364
9377
  const classLangs = {
@@ -9439,20 +9452,19 @@ Object.defineProperty(Q5, 'lang', {
9439
9452
 
9440
9453
  for (let className in classLangs) {
9441
9454
  let target = className == 'Q5' ? Q5 : Q5[className] ? Q5[className].prototype : null;
9442
- if (target) {
9443
- let map = parseLangs(classLangs[className], val);
9444
- for (let name in map) {
9445
- let translatedName = map[name];
9446
- if (target.hasOwnProperty(translatedName)) continue;
9447
- Object.defineProperty(target, translatedName, {
9448
- get: function () {
9449
- return this[name];
9450
- },
9451
- set: function (v) {
9452
- this[name] = v;
9453
- }
9454
- });
9455
- }
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
+ });
9456
9468
  }
9457
9469
  }
9458
9470
 
@@ -9511,6 +9523,13 @@ Q5.addHook('init', (q) => {
9511
9523
  for (let name in m) {
9512
9524
  let translatedName = m[name];
9513
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
+ }
9514
9533
  }
9515
9534
  });
9516
9535
 
@@ -9523,6 +9542,8 @@ Q5.addHook('predraw', (q) => {
9523
9542
  'frameCount',
9524
9543
  'mouseX',
9525
9544
  'mouseY',
9545
+ 'pmouseX',
9546
+ 'pmouseY',
9526
9547
  'movedX',
9527
9548
  'movedY',
9528
9549
  'mouseIsPressed',
@@ -9535,6 +9556,152 @@ Q5.addHook('predraw', (q) => {
9535
9556
 
9536
9557
  // sync properties
9537
9558
  for (let p of props) {
9538
- 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
+ }
9539
9567
  }
9540
9568
  });
9569
+ const runPython = async function () {
9570
+ let scripts = [...document.getElementsByTagName('script')];
9571
+ scripts = scripts.filter((s) => s.type == 'q5-python' || s.type == 'text/q5-python');
9572
+ if (!scripts.length) return;
9573
+
9574
+ if (!window.brython) {
9575
+ const loadScript = (src) =>
9576
+ new Promise((resolve, reject) => {
9577
+ const script = document.createElement('script');
9578
+ script.src = src;
9579
+ script.onload = resolve;
9580
+ script.onerror = reject;
9581
+ document.head.appendChild(script);
9582
+ });
9583
+
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');
9586
+ }
9587
+
9588
+ let code = '';
9589
+ for (const script of scripts) {
9590
+ code += script.src ? await (await fetch(script.src)).text() : script.innerText;
9591
+ }
9592
+
9593
+ const useWebGPU = !code.slice(0, code.indexOf('\n')).includes('C2D'),
9594
+ q5py = useWebGPU ? await Q5.WebGPU() : new Q5();
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
+
9602
+ code = code.replaceAll('\n', '\n\t');
9603
+
9604
+ code = `
9605
+ async def __run():
9606
+ ${code}
9607
+
9608
+ _q5_state_vars = ["mouseX", "mouseY", "pmouseX", "pmouseY", "width", "height", "frameCount", "deltaTime", "mouseIsPressed", "mouseButton", "keyIsPressed", "key", "keyCode", "touches", "movedX", "movedY"]
9609
+
9610
+ def _sync_and_call(fn):
9611
+ def _wrapper(*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
9620
+ return _wrapper
9621
+
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]))
9625
+ `;
9626
+
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);
9701
+ };
9702
+
9703
+ if (typeof document == 'object') {
9704
+ if (document.readyState == 'loading') {
9705
+ document.addEventListener('DOMContentLoaded', runPython);
9706
+ } else runPython();
9707
+ }