q5 2.0.17 → 2.1.0

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/q5.js CHANGED
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 2.0
3
+ * @version 2.1
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
7
7
  */
8
- function Q5(scope, parent) {
8
+ function Q5(scope, parent, renderer) {
9
9
  let $ = this;
10
10
  $._q5 = true;
11
- $._scope = scope;
12
11
  $._parent = parent;
12
+ $._renderer = renderer || 'q2d';
13
13
  $._preloadCount = 0;
14
14
 
15
15
  scope ??= 'global';
@@ -17,13 +17,14 @@ function Q5(scope, parent) {
17
17
  if (!(window.setup || window.draw)) return;
18
18
  scope = 'global';
19
19
  }
20
+ $._scope = scope;
20
21
  let globalScope;
21
22
  if (scope == 'global') {
22
23
  Q5._hasGlobal = $._isGlobal = true;
23
24
  globalScope = !Q5._nodejs ? window : global;
24
25
  }
25
26
 
26
- let p = new Proxy($, {
27
+ let q = new Proxy($, {
27
28
  set: (t, p, v) => {
28
29
  $[p] = v;
29
30
  if ($._isGlobal) globalScope[p] = v;
@@ -41,6 +42,10 @@ function Q5(scope, parent) {
41
42
  $._targetFrameDuration = 16.666666666666668;
42
43
  $._frameRate = $._fps = 60;
43
44
  $._loop = true;
45
+ $._hooks = {
46
+ postCanvas: [],
47
+ preRender: []
48
+ };
44
49
 
45
50
  let millisStart = 0;
46
51
  $.millis = () => performance.now() - millisStart;
@@ -48,7 +53,7 @@ function Q5(scope, parent) {
48
53
  $.noCanvas = () => {
49
54
  if ($.canvas?.remove) $.canvas.remove();
50
55
  $.canvas = 0;
51
- p.ctx = p.drawingContext = 0;
56
+ q.ctx = q.drawingContext = 0;
52
57
  };
53
58
 
54
59
  if (window) {
@@ -57,10 +62,10 @@ function Q5(scope, parent) {
57
62
  $.deviceOrientation = window.screen?.orientation?.type;
58
63
  }
59
64
 
60
- $._incrementPreload = () => p._preloadCount++;
61
- $._decrementPreload = () => p._preloadCount--;
65
+ $._incrementPreload = () => q._preloadCount++;
66
+ $._decrementPreload = () => q._preloadCount--;
62
67
 
63
- function _draw(timestamp) {
68
+ $._draw = (timestamp) => {
64
69
  let ts = timestamp || performance.now();
65
70
  $._lastFrameTime ??= ts - $._targetFrameDuration;
66
71
 
@@ -69,44 +74,43 @@ function Q5(scope, parent) {
69
74
  $._shouldResize = false;
70
75
  }
71
76
 
72
- if ($._loop) looper = raf(_draw);
77
+ if ($._loop) looper = raf($._draw);
73
78
  else if ($.frameCount && !$._redraw) return;
74
79
 
75
80
  if (looper && $.frameCount) {
76
81
  let time_since_last = ts - $._lastFrameTime;
77
- if (time_since_last < $._targetFrameDuration - 1) return;
82
+ if (time_since_last < $._targetFrameDuration - 4) return;
78
83
  }
79
- p.deltaTime = ts - $._lastFrameTime;
84
+ q.deltaTime = ts - $._lastFrameTime;
80
85
  $._frameRate = 1000 / $.deltaTime;
81
- p.frameCount++;
86
+ q.frameCount++;
82
87
  let pre = performance.now();
83
- for (let m of Q5.prototype._methods.pre) m.call($);
84
- if ($.ctx) $.ctx.save();
88
+ if ($._beginRender) $._beginRender();
89
+ if ($.ctx) $.resetMatrix();
90
+ for (let m of Q5.methods.pre) m.call($);
85
91
  $.draw();
86
- for (let m of Q5.prototype._methods.post) m.call($);
87
- if ($.ctx) {
88
- $.ctx.restore();
89
- $.resetMatrix();
90
- }
91
- p.pmouseX = $.mouseX;
92
- p.pmouseY = $.mouseY;
92
+ if ($._render) $._render();
93
+ for (let m of Q5.methods.post) m.call($);
94
+ if ($._finishRender) $._finishRender();
95
+ q.pmouseX = $.mouseX;
96
+ q.pmouseY = $.mouseY;
93
97
  $._lastFrameTime = ts;
94
98
  let post = performance.now();
95
99
  $._fps = Math.round(1000 / (post - pre));
96
- }
100
+ };
97
101
  $.noLoop = () => {
98
102
  $._loop = false;
99
103
  looper = null;
100
104
  };
101
105
  $.loop = () => {
102
106
  $._loop = true;
103
- if (looper == null) _draw();
107
+ if (looper == null) $._draw();
104
108
  };
105
109
  $.isLooping = () => $._loop;
106
110
  $.redraw = (n = 1) => {
107
111
  $._redraw = true;
108
112
  for (let i = 0; i < n; i++) {
109
- _draw();
113
+ $._draw();
110
114
  }
111
115
  $._redraw = false;
112
116
  };
@@ -136,7 +140,12 @@ function Q5(scope, parent) {
136
140
  $.describe = () => {};
137
141
 
138
142
  for (let m in Q5.modules) {
139
- Q5.modules[m]($, p);
143
+ Q5.modules[m]($, q);
144
+ }
145
+
146
+ let r = Q5.renderers[$._renderer];
147
+ for (let m in r) {
148
+ r[m]($, q);
140
149
  }
141
150
 
142
151
  // INIT
@@ -147,12 +156,14 @@ function Q5(scope, parent) {
147
156
  }
148
157
  }
149
158
 
159
+ if (scope == 'graphics') return;
160
+
150
161
  if (scope == 'global') {
151
162
  Object.assign(Q5, $);
152
163
  delete Q5.Q5;
153
164
  }
154
165
 
155
- for (let m of Q5.prototype._methods.init) {
166
+ for (let m of Q5.methods.init) {
156
167
  m.call($);
157
168
  }
158
169
 
@@ -169,8 +180,6 @@ function Q5(scope, parent) {
169
180
 
170
181
  if (typeof scope == 'function') scope($);
171
182
 
172
- if (scope == 'graphics') return;
173
-
174
183
  Q5._instanceCount++;
175
184
 
176
185
  let raf =
@@ -218,8 +227,6 @@ function Q5(scope, parent) {
218
227
 
219
228
  if (!($.setup || $.draw)) return;
220
229
 
221
- $._startDone = false;
222
-
223
230
  async function _start() {
224
231
  $._startDone = true;
225
232
  if ($._preloadCount > 0) return raf(_start);
@@ -229,7 +236,7 @@ function Q5(scope, parent) {
229
236
  if ($.ctx === null) $.createCanvas(100, 100);
230
237
  $._setupDone = true;
231
238
  if ($.ctx) $.resetMatrix();
232
- raf(_draw);
239
+ raf($._draw);
233
240
  }
234
241
 
235
242
  if ((arguments.length && scope != 'namespace') || preloadDefined) {
@@ -243,6 +250,7 @@ function Q5(scope, parent) {
243
250
  }
244
251
  }
245
252
 
253
+ Q5.renderers = {};
246
254
  Q5.modules = {};
247
255
 
248
256
  Q5._nodejs = typeof process == 'object';
@@ -253,13 +261,13 @@ Q5._friendlyError = (msg, func) => {
253
261
  };
254
262
  Q5._validateParameters = () => true;
255
263
 
256
- Q5.prototype._methods = {
264
+ Q5.methods = {
257
265
  init: [],
258
266
  pre: [],
259
267
  post: [],
260
268
  remove: []
261
269
  };
262
- Q5.prototype.registerMethod = (m, fn) => Q5.prototype._methods[m].push(fn);
270
+ Q5.prototype.registerMethod = (m, fn) => Q5.methods[m].push(fn);
263
271
  Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.prototype[n] = fn[n]);
264
272
 
265
273
  if (Q5._nodejs) global.p5 ??= global.Q5 = Q5;
@@ -271,7 +279,7 @@ if (typeof document == 'object') {
271
279
  if (!Q5._hasGlobal) new Q5('auto');
272
280
  });
273
281
  }
274
- Q5.modules.q2d_canvas = ($, p) => {
282
+ Q5.modules.canvas = ($, q) => {
275
283
  $._OffscreenCanvas =
276
284
  window.OffscreenCanvas ||
277
285
  function () {
@@ -280,59 +288,28 @@ Q5.modules.q2d_canvas = ($, p) => {
280
288
 
281
289
  if (Q5._nodejs) {
282
290
  if (Q5._createNodeJSCanvas) {
283
- p.canvas = Q5._createNodeJSCanvas(100, 100);
291
+ q.canvas = Q5._createNodeJSCanvas(100, 100);
284
292
  }
285
293
  } else if ($._scope == 'image' || $._scope == 'graphics') {
286
- p.canvas = new $._OffscreenCanvas(100, 100);
294
+ q.canvas = new $._OffscreenCanvas(100, 100);
287
295
  }
296
+
288
297
  if (!$.canvas) {
289
298
  if (typeof document == 'object') {
290
- p.canvas = document.createElement('canvas');
299
+ q.canvas = document.createElement('canvas');
291
300
  $.canvas.id = 'q5Canvas' + Q5._instanceCount;
292
301
  $.canvas.classList.add('q5Canvas');
293
302
  } else $.noCanvas();
294
303
  }
295
304
 
296
305
  let c = $.canvas;
297
-
298
306
  c.width = $.width = 100;
299
307
  c.height = $.height = 100;
300
-
301
- if (c && $._scope != 'graphics' && $._scope != 'image') {
302
- $._setupDone = false;
303
- let parent = $._parent;
304
- if (parent && typeof parent == 'string') {
305
- parent = document.getElementById(parent);
306
- }
307
- c.parent = (el) => {
308
- if (typeof el == 'string') el = document.getElementById(el);
309
- el.append(c);
310
-
311
- function parentResized() {
312
- if ($.frameCount > 1) {
313
- $._shouldResize = true;
314
- $._adjustDisplay();
315
- }
316
- }
317
- if (typeof ResizeObserver == 'function') {
318
- if ($._ro) $._ro.disconnect();
319
- $._ro = new ResizeObserver(parentResized);
320
- $._ro.observe(parent);
321
- } else if (!$.frameCount) {
322
- window.addEventListener('resize', parentResized);
323
- }
324
- };
325
- function appendCanvas() {
326
- parent ??= document.getElementsByTagName('main')[0];
327
- if (!parent) {
328
- parent = document.createElement('main');
329
- document.body.append(parent);
330
- }
331
- c.parent(parent);
332
- }
333
- if (document.body) appendCanvas();
334
- else document.addEventListener('DOMContentLoaded', appendCanvas);
308
+ if ($._scope != 'image') {
309
+ c.renderer = $._renderer;
310
+ c[$._renderer] = true;
335
311
  }
312
+ $._pixelDensity = 1;
336
313
 
337
314
  $._adjustDisplay = () => {
338
315
  if (c.style) {
@@ -341,25 +318,12 @@ Q5.modules.q2d_canvas = ($, p) => {
341
318
  }
342
319
  };
343
320
 
344
- $.createCanvas = function (w, h, renderer, options) {
345
- if (renderer == 'webgl') throw Error(`webgl renderer is not supported in q5, use '2d'`);
346
- if (typeof renderer == 'object') options = renderer;
347
- p.width = c.width = c.w = w || window.innerWidth;
348
- p.height = c.height = c.h = h || window.innerHeight;
349
- c.hw = w / 2;
350
- c.hh = h / 2;
351
- c.renderer = '2d';
321
+ $.createCanvas = function (w, h, options) {
322
+ options ??= arguments[3];
323
+
352
324
  let opt = Object.assign({}, Q5.canvasOptions);
353
- if (options) Object.assign(opt, options);
325
+ if (typeof options == 'object') Object.assign(opt, options);
354
326
 
355
- p.ctx = p.drawingContext = c.getContext('2d', opt);
356
- Object.assign(c, opt);
357
- if ($._colorMode == 'rgb') $.colorMode('rgb');
358
- if ($._scope != 'image') {
359
- $._defaultStyle();
360
- $._da = 0;
361
- }
362
- $.ctx.save();
363
327
  if ($._scope != 'image') {
364
328
  let pd = $.displayDensity();
365
329
  if ($._scope == 'graphics') pd = this._pixelDensity;
@@ -368,96 +332,225 @@ Q5.modules.q2d_canvas = ($, p) => {
368
332
  c.visible = e[0].isIntersecting;
369
333
  }).observe(c);
370
334
  }
371
- $.pixelDensity(Math.ceil(pd));
372
- } else this._pixelDensity = 1;
335
+ $._pixelDensity = Math.ceil(pd);
336
+ }
373
337
 
374
- if ($.displayMode) $.displayMode();
375
- else $._adjustDisplay();
376
- return c;
377
- };
378
- $._createCanvas = $.createCanvas;
338
+ $._setCanvasSize(w, h);
379
339
 
380
- if ($._scope == 'image') return;
340
+ Object.assign(c, opt);
341
+ let rend = $._createCanvas(c.w, c.h, opt);
381
342
 
382
- $._defaultStyle = () => {
383
- $.ctx.fillStyle = 'white';
384
- $.ctx.strokeStyle = 'black';
385
- $.ctx.lineCap = 'round';
386
- $.ctx.lineJoin = 'miter';
387
- $.ctx.textAlign = 'left';
343
+ if ($._hooks) {
344
+ for (let m of $._hooks.postCanvas) m();
345
+ }
346
+ return rend;
388
347
  };
389
348
 
390
- function cloneCtx() {
391
- let t = {};
392
- for (let prop in $.ctx) {
393
- if (typeof $.ctx[prop] != 'function') t[prop] = $.ctx[prop];
349
+ $._save = async (data, name, ext) => {
350
+ name = name || 'untitled';
351
+ ext = ext || 'png';
352
+ if (ext == 'jpg' || ext == 'png' || ext == 'webp') {
353
+ if (data instanceof OffscreenCanvas) {
354
+ const blob = await data.convertToBlob({ type: 'image/' + ext });
355
+ data = await new Promise((resolve) => {
356
+ const reader = new FileReader();
357
+ reader.onloadend = () => resolve(reader.result);
358
+ reader.readAsDataURL(blob);
359
+ });
360
+ } else {
361
+ data = data.toDataURL('image/' + ext);
362
+ }
363
+ } else {
364
+ let type = 'text/plain';
365
+ if (ext == 'json') {
366
+ if (typeof data != 'string') data = JSON.stringify(data);
367
+ type = 'text/json';
368
+ }
369
+ data = new Blob([data], { type });
370
+ data = URL.createObjectURL(data);
394
371
  }
395
- delete t.canvas;
396
- return t;
397
- }
372
+ let a = document.createElement('a');
373
+ a.href = data;
374
+ a.download = name + '.' + ext;
375
+ a.click();
376
+ URL.revokeObjectURL(a.href);
377
+ };
378
+ $.save = (a, b, c) => {
379
+ if (!a || (typeof a == 'string' && (!b || (!c && b.length < 5)))) {
380
+ c = b;
381
+ b = a;
382
+ a = $.canvas;
383
+ }
384
+ if (c) return $._save(a, b, c);
385
+ if (b) {
386
+ b = b.split('.');
387
+ $._save(a, b[0], b.at(-1));
388
+ } else $._save(a);
389
+ };
398
390
 
399
- function _resizeCanvas(w, h) {
391
+ $._setCanvasSize = (w, h) => {
400
392
  w ??= window.innerWidth;
401
393
  h ??= window.innerHeight;
402
-
403
- let t = cloneCtx();
404
- let o;
405
- if ($.frameCount) {
406
- o = new $._OffscreenCanvas(c.width, c.height);
407
- o.w = c.w;
408
- o.h = c.h;
409
- let oCtx = o.getContext('2d');
410
- oCtx.drawImage(c, 0, 0);
411
- }
412
-
413
- c.width = Math.ceil(w * $._pixelDensity);
414
- c.height = Math.ceil(h * $._pixelDensity);
415
- c.w = w;
416
- c.h = h;
394
+ c.w = w = Math.ceil(w);
395
+ c.h = h = Math.ceil(h);
417
396
  c.hw = w / 2;
418
397
  c.hh = h / 2;
419
- for (let prop in t) $.ctx[prop] = t[prop];
420
- $.ctx.scale($._pixelDensity, $._pixelDensity);
421
-
422
- if ($.frameCount) $.ctx.drawImage(o, 0, 0, o.w, o.h);
398
+ c.width = Math.ceil(w * $._pixelDensity);
399
+ c.height = Math.ceil(h * $._pixelDensity);
423
400
 
424
401
  if (!$._da) {
425
- p.width = w;
426
- p.height = h;
402
+ q.width = w;
403
+ q.height = h;
427
404
  } else $.flexibleCanvas($._dau);
428
405
 
429
- if ($.frameCount != 0) $._adjustDisplay();
406
+ if ($.displayMode && !c.displayMode) $.displayMode();
407
+ else $._adjustDisplay();
408
+ };
409
+
410
+ if ($._scope == 'image') return;
411
+
412
+ if (c && $._scope != 'graphics') {
413
+ c.parent = (el) => {
414
+ if (c.parentElement) c.parentElement.removeChild(c);
415
+
416
+ if (typeof el == 'string') el = document.getElementById(el);
417
+ el.append(c);
418
+
419
+ function parentResized() {
420
+ if ($.frameCount > 1) {
421
+ $._shouldResize = true;
422
+ $._adjustDisplay();
423
+ }
424
+ }
425
+ if (typeof ResizeObserver == 'function') {
426
+ if ($._ro) $._ro.disconnect();
427
+ $._ro = new ResizeObserver(parentResized);
428
+ $._ro.observe(el);
429
+ } else if (!$.frameCount) {
430
+ window.addEventListener('resize', parentResized);
431
+ }
432
+ };
433
+
434
+ function addCanvas() {
435
+ let el = $._parent;
436
+ el ??= document.getElementsByTagName('main')[0];
437
+ if (!el) {
438
+ el = document.createElement('main');
439
+ document.body.append(el);
440
+ }
441
+ c.parent(el);
442
+ }
443
+ if (document.body) addCanvas();
444
+ else document.addEventListener('DOMContentLoaded', addCanvas);
430
445
  }
431
446
 
432
447
  $.resizeCanvas = (w, h) => {
448
+ if (!$.ctx) return $.createCanvas(w, h);
433
449
  if (w == c.w && h == c.h) return;
434
- _resizeCanvas(w, h);
450
+
451
+ $._resizeCanvas(w, h);
435
452
  };
436
453
 
437
- $._pixelDensity = 1;
454
+ $.canvas.resize = $.resizeCanvas;
455
+ $.canvas.save = $.saveCanvas = $.save;
456
+
438
457
  $.displayDensity = () => window.devicePixelRatio;
439
458
  $.pixelDensity = (v) => {
440
459
  if (!v || v == $._pixelDensity) return $._pixelDensity;
441
460
  $._pixelDensity = v;
442
- _resizeCanvas(c.w, c.h);
461
+ $._setCanvasSize(c.w, c.h);
443
462
  return v;
444
463
  };
445
464
 
446
- if ($._scope == 'image') return;
447
-
448
- $.fullscreen = (v) => {
449
- if (v === undefined) return document.fullscreenElement;
450
- if (v) document.body.requestFullscreen();
451
- else document.body.exitFullscreen();
452
- };
453
-
454
465
  $.flexibleCanvas = (unit = 400) => {
455
466
  if (unit) {
456
467
  $._da = c.width / (unit * $._pixelDensity);
457
- p.width = $._dau = unit;
458
- p.height = (c.h / c.w) * unit;
468
+ q.width = $._dau = unit;
469
+ q.height = (c.h / c.w) * unit;
459
470
  } else $._da = 0;
460
471
  };
472
+ };
473
+
474
+ Q5.canvasOptions = {
475
+ alpha: false,
476
+ colorSpace: 'display-p3'
477
+ };
478
+
479
+ if (!window.matchMedia || !matchMedia('(dynamic-range: high) and (color-gamut: p3)').matches) {
480
+ Q5.canvasOptions.colorSpace = 'srgb';
481
+ } else Q5.supportsHDR = true;
482
+ Q5.renderers.q2d = {};
483
+
484
+ Q5.renderers.q2d.canvas = ($, q) => {
485
+ let c = $.canvas;
486
+
487
+ if ($.colorMode) $.colorMode('rgb', 'integer');
488
+
489
+ $._createCanvas = function (w, h, options) {
490
+ q.ctx = q.drawingContext = c.getContext('2d', options);
491
+
492
+ if ($._scope != 'image') {
493
+ // default styles
494
+ $.ctx.fillStyle = 'white';
495
+ $.ctx.strokeStyle = 'black';
496
+ $.ctx.lineCap = 'round';
497
+ $.ctx.lineJoin = 'miter';
498
+ $.ctx.textAlign = 'left';
499
+ }
500
+ $.ctx.scale($._pixelDensity, $._pixelDensity);
501
+ $.ctx.save();
502
+ return c;
503
+ };
504
+
505
+ if ($._scope == 'image') return;
506
+
507
+ $._resizeCanvas = (w, h) => {
508
+ let t = {};
509
+ for (let prop in $.ctx) {
510
+ if (typeof $.ctx[prop] != 'function') t[prop] = $.ctx[prop];
511
+ }
512
+ delete t.canvas;
513
+
514
+ let o = new $._OffscreenCanvas(c.width, c.height);
515
+ o.w = c.w;
516
+ o.h = c.h;
517
+ let oCtx = o.getContext('2d');
518
+ oCtx.drawImage(c, 0, 0);
519
+
520
+ $._setCanvasSize(w, h);
521
+
522
+ for (let prop in t) $.ctx[prop] = t[prop];
523
+ $.scale($._pixelDensity);
524
+ $.ctx.drawImage(o, 0, 0, o.w, o.h);
525
+ };
526
+
527
+ $.strokeWeight = (n) => {
528
+ if (!n) $._doStroke = false;
529
+ if ($._da) n *= $._da;
530
+ $.ctx.lineWidth = n || 0.0001;
531
+ };
532
+ $.stroke = function (c) {
533
+ $._doStroke = true;
534
+ $._strokeSet = true;
535
+ if (Q5.Color) {
536
+ if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
537
+ else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
538
+ if (c.a <= 0) return ($._doStroke = false);
539
+ }
540
+ $.ctx.strokeStyle = c.toString();
541
+ };
542
+ $.noStroke = () => ($._doStroke = false);
543
+ $.fill = function (c) {
544
+ $._doFill = true;
545
+ $._fillSet = true;
546
+ if (Q5.Color) {
547
+ if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
548
+ else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
549
+ if (c.a <= 0) return ($._doFill = false);
550
+ }
551
+ $.ctx.fillStyle = c.toString();
552
+ };
553
+ $.noFill = () => ($._doFill = false);
461
554
 
462
555
  // DRAWING MATRIX
463
556
 
@@ -482,7 +575,7 @@ Q5.modules.q2d_canvas = ($, p) => {
482
575
  $.shearY = (ang) => $.ctx.transform(1, $.tan(ang), 0, 1, 0, 0);
483
576
  $.resetMatrix = () => {
484
577
  $.ctx.resetTransform();
485
- $.ctx.scale($._pixelDensity, $._pixelDensity);
578
+ $.scale($._pixelDensity);
486
579
  };
487
580
 
488
581
  $._styleNames = [
@@ -536,29 +629,20 @@ Q5.modules.q2d_canvas = ($, p) => {
536
629
  opt ??= {};
537
630
  opt.alpha ??= true;
538
631
  opt.colorSpace ??= $.canvas.colorSpace;
539
- g._createCanvas.call($, w, h, opt);
632
+ g.createCanvas.call($, w, h, opt);
540
633
  return g;
541
634
  };
542
635
 
543
636
  if (window && $._scope != 'graphics') {
544
637
  window.addEventListener('resize', () => {
545
638
  $._shouldResize = true;
546
- p.windowWidth = window.innerWidth;
547
- p.windowHeight = window.innerHeight;
548
- p.deviceOrientation = window.screen?.orientation?.type;
639
+ q.windowWidth = window.innerWidth;
640
+ q.windowHeight = window.innerHeight;
641
+ q.deviceOrientation = window.screen?.orientation?.type;
549
642
  });
550
643
  }
551
644
  };
552
-
553
- Q5.canvasOptions = {
554
- alpha: false,
555
- colorSpace: 'display-p3'
556
- };
557
-
558
- if (!window.matchMedia || !matchMedia('(dynamic-range: high) and (color-gamut: p3)').matches) {
559
- Q5.canvasOptions.colorSpace = 'srgb';
560
- } else Q5.supportsHDR = true;
561
- Q5.modules.q2d_drawing = ($) => {
645
+ Q5.renderers.q2d.drawing = ($) => {
562
646
  $.CHORD = 0;
563
647
  $.PIE = 1;
564
648
  $.OPEN = 2;
@@ -620,33 +704,6 @@ Q5.modules.q2d_drawing = ($) => {
620
704
 
621
705
  // DRAWING SETTINGS
622
706
 
623
- $.strokeWeight = (n) => {
624
- if (!n) $._doStroke = false;
625
- if ($._da) n *= $._da;
626
- $.ctx.lineWidth = n || 0.0001;
627
- };
628
- $.stroke = function (c) {
629
- $._doStroke = true;
630
- $._strokeSet = true;
631
- if (Q5.Color) {
632
- if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
633
- else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
634
- if (c.a <= 0) return ($._doStroke = false);
635
- }
636
- $.ctx.strokeStyle = c.toString();
637
- };
638
- $.noStroke = () => ($._doStroke = false);
639
- $.fill = function (c) {
640
- $._doFill = true;
641
- $._fillSet = true;
642
- if (Q5.Color) {
643
- if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
644
- else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
645
- if (c.a <= 0) return ($._doFill = false);
646
- }
647
- $.ctx.fillStyle = c.toString();
648
- };
649
- $.noFill = () => ($._doFill = false);
650
707
  $.blendMode = (x) => ($.ctx.globalCompositeOperation = x);
651
708
  $.strokeCap = (x) => ($.ctx.lineCap = x);
652
709
  $.strokeJoin = (x) => ($.ctx.lineJoin = x);
@@ -849,22 +906,18 @@ Q5.modules.q2d_drawing = ($) => {
849
906
  return $.rect(x, y, s, s, tl, tr, br, bl);
850
907
  };
851
908
 
852
- function clearBuff() {
853
- curveBuff = [];
854
- }
855
-
856
909
  $.beginShape = () => {
857
- clearBuff();
910
+ curveBuff = [];
858
911
  $.ctx.beginPath();
859
912
  firstVertex = true;
860
913
  };
861
914
  $.beginContour = () => {
862
915
  $.ctx.closePath();
863
- clearBuff();
916
+ curveBuff = [];
864
917
  firstVertex = true;
865
918
  };
866
919
  $.endContour = () => {
867
- clearBuff();
920
+ curveBuff = [];
868
921
  firstVertex = true;
869
922
  };
870
923
  $.vertex = (x, y) => {
@@ -872,7 +925,7 @@ Q5.modules.q2d_drawing = ($) => {
872
925
  x *= $._da;
873
926
  y *= $._da;
874
927
  }
875
- clearBuff();
928
+ curveBuff = [];
876
929
  if (firstVertex) {
877
930
  $.ctx.moveTo(x, y);
878
931
  } else {
@@ -889,7 +942,7 @@ Q5.modules.q2d_drawing = ($) => {
889
942
  x *= $._da;
890
943
  y *= $._da;
891
944
  }
892
- clearBuff();
945
+ curveBuff = [];
893
946
  $.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
894
947
  };
895
948
  $.quadraticVertex = (cp1x, cp1y, x, y) => {
@@ -899,7 +952,7 @@ Q5.modules.q2d_drawing = ($) => {
899
952
  x *= $._da;
900
953
  y *= $._da;
901
954
  }
902
- clearBuff();
955
+ curveBuff = [];
903
956
  $.ctx.quadraticCurveTo(cp1x, cp1y, x, y);
904
957
  };
905
958
  $.bezier = (x1, y1, x2, y2, x3, y3, x4, y4) => {
@@ -924,7 +977,7 @@ Q5.modules.q2d_drawing = ($) => {
924
977
  $.endShape($.CLOSE);
925
978
  };
926
979
  $.endShape = (close) => {
927
- clearBuff();
980
+ curveBuff = [];
928
981
  if (close) $.ctx.closePath();
929
982
  ink();
930
983
  };
@@ -1076,7 +1129,32 @@ Q5.modules.q2d_drawing = ($) => {
1076
1129
  return $.ctx.isPointInStroke(x * pd, y * pd);
1077
1130
  };
1078
1131
  };
1079
- Q5.modules.q2d_image = ($, p) => {
1132
+ Q5.renderers.q2d.image = ($, q) => {
1133
+ class Q5Image {
1134
+ constructor(w, h, opt) {
1135
+ let $ = this;
1136
+ $._scope = 'image';
1137
+ $.canvas = $.ctx = $.drawingContext = null;
1138
+ $.pixels = [];
1139
+ Q5.modules.canvas($, $);
1140
+ let r = Q5.renderers.q2d;
1141
+ for (let m of ['canvas', 'image', 'soft_filters']) {
1142
+ if (r[m]) r[m]($, $);
1143
+ }
1144
+ $.createCanvas(w, h, opt);
1145
+ delete $.createCanvas;
1146
+ $._loop = false;
1147
+ }
1148
+ get w() {
1149
+ return this.width;
1150
+ }
1151
+ get h() {
1152
+ return this.height;
1153
+ }
1154
+ }
1155
+
1156
+ Q5.Image ??= Q5Image;
1157
+
1080
1158
  $.createImage = (w, h, opt) => {
1081
1159
  opt ??= {};
1082
1160
  opt.alpha ??= true;
@@ -1084,115 +1162,145 @@ Q5.modules.q2d_image = ($, p) => {
1084
1162
  return new Q5.Image(w, h, opt);
1085
1163
  };
1086
1164
 
1087
- $._tint = null;
1088
- let imgData = null;
1089
- let tmpCtx = null;
1090
- let tmpCt2 = null;
1091
-
1092
- function makeTmpCtx(w, h) {
1093
- h ??= w || $.canvas.height;
1094
- w ??= $.canvas.width;
1095
- if (tmpCtx == null) {
1096
- tmpCtx = new $._OffscreenCanvas(w, h).getContext('2d', {
1097
- colorSpace: $.canvas.colorSpace
1098
- });
1165
+ $.loadImage = function (url, cb, opt) {
1166
+ if (url.canvas) return url;
1167
+ if (url.slice(-3).toLowerCase() == 'gif') {
1168
+ throw new Error(`q5 doesn't support GIFs due to their impact on performance. Use a video or animation instead.`);
1099
1169
  }
1100
- if (tmpCtx.canvas.width != w || tmpCtx.canvas.height != h) {
1101
- tmpCtx.canvas.width = w;
1102
- tmpCtx.canvas.height = h;
1170
+ q._preloadCount++;
1171
+ let last = [...arguments].at(-1);
1172
+ opt = typeof last == 'object' ? last : null;
1173
+
1174
+ let g = $.createImage(1, 1, opt);
1175
+
1176
+ function loaded(img) {
1177
+ g.resize(img.naturalWidth || img.width, img.naturalHeight || img.height);
1178
+ g.ctx.drawImage(img, 0, 0);
1179
+ q._preloadCount--;
1180
+ if (cb) cb(g);
1103
1181
  }
1104
- }
1105
1182
 
1106
- function makeTmpCt2(w, h) {
1107
- h ??= w || $.canvas.height;
1108
- w ??= $.canvas.width;
1109
- if (tmpCt2 == null) {
1110
- tmpCt2 = new $._OffscreenCanvas(w, h).getContext('2d', {
1111
- colorSpace: $.canvas.colorSpace
1112
- });
1183
+ if (Q5._nodejs && global.CairoCanvas) {
1184
+ global.CairoCanvas.loadImage(url)
1185
+ .then(loaded)
1186
+ .catch((e) => {
1187
+ q._preloadCount--;
1188
+ throw e;
1189
+ });
1190
+ } else {
1191
+ let img = new window.Image();
1192
+ img.src = url;
1193
+ img.crossOrigin = 'Anonymous';
1194
+ img._pixelDensity = 1;
1195
+ img.onload = () => loaded(img);
1196
+ img.onerror = (e) => {
1197
+ q._preloadCount--;
1198
+ throw e;
1199
+ };
1113
1200
  }
1114
- if (tmpCt2.canvas.width != w || tmpCt2.canvas.height != h) {
1115
- tmpCt2.canvas.width = w;
1116
- tmpCt2.canvas.height = h;
1201
+ return g;
1202
+ };
1203
+
1204
+ $.imageMode = (mode) => ($._imageMode = mode);
1205
+ $.image = (img, dx, dy, dw, dh, sx = 0, sy = 0, sw, sh) => {
1206
+ let drawable = img.canvas || img;
1207
+ if (Q5._createNodeJSCanvas) {
1208
+ drawable = drawable.context.canvas;
1117
1209
  }
1118
- }
1210
+ dw ??= img.width || img.videoWidth;
1211
+ dh ??= img.height || img.videoHeight;
1212
+ if ($._imageMode == 'center') {
1213
+ dx -= dw * 0.5;
1214
+ dy -= dh * 0.5;
1215
+ }
1216
+ if ($._da) {
1217
+ dx *= $._da;
1218
+ dy *= $._da;
1219
+ dw *= $._da;
1220
+ dh *= $._da;
1221
+ sx *= $._da;
1222
+ sy *= $._da;
1223
+ sw *= $._da;
1224
+ sh *= $._da;
1225
+ }
1226
+ let pd = img._pixelDensity || 1;
1227
+ if (!sw) {
1228
+ sw = drawable.width || drawable.videoWidth;
1229
+ } else sw *= pd;
1230
+ if (!sh) {
1231
+ sh = drawable.height || drawable.videoHeight;
1232
+ } else sh *= pd;
1233
+ $.ctx.drawImage(drawable, sx * pd, sy * pd, sw, sh, dx, dy, dw, dh);
1234
+
1235
+ if ($._tint) {
1236
+ $.ctx.globalCompositeOperation = 'multiply';
1237
+ $.ctx.fillStyle = $._tint.toString();
1238
+ $.ctx.fillRect(dx, dy, dw, dh);
1239
+ $.ctx.globalCompositeOperation = 'source-over';
1240
+ }
1241
+ };
1242
+
1243
+ $._tint = null;
1244
+ let imgData = null;
1119
1245
 
1120
1246
  $._softFilter = () => {
1121
- throw 'Load q5-2d-soft-filters.js to use software filters.';
1247
+ throw new Error('Load q5-2d-soft-filters.js to use software filters.');
1122
1248
  };
1123
1249
 
1124
- function nativeFilter(filt) {
1125
- tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
1126
- tmpCtx.filter = filt;
1127
- tmpCtx.drawImage($.canvas, 0, 0);
1128
- $.ctx.save();
1129
- $.ctx.resetTransform();
1130
- $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
1131
- $.ctx.drawImage(tmpCtx.canvas, 0, 0);
1132
- $.ctx.restore();
1133
- }
1250
+ $.filter = (type, x) => {
1251
+ if (!$.ctx.filter) return $._softFilter(type, x);
1134
1252
 
1135
- $.filter = (typ, x) => {
1136
- if (!$.ctx.filter) return $._softFilter(typ, x);
1137
- makeTmpCtx();
1138
- if (typeof typ == 'string') {
1139
- nativeFilter(typ);
1140
- } else if (typ == Q5.THRESHOLD) {
1253
+ if (typeof type == 'string') f = type;
1254
+ else if (type == Q5.GRAY) f = `saturate(0%)`;
1255
+ else if (type == Q5.INVERT) f = `invert(100%)`;
1256
+ else if (type == Q5.BLUR) {
1257
+ let r = Math.ceil(x * $._pixelDensity) || 1;
1258
+ f = `blur(${r}px)`;
1259
+ } else if (type == Q5.THRESHOLD) {
1141
1260
  x ??= 0.5;
1142
- x = Math.max(x, 0.00001);
1143
- let b = Math.floor((0.5 / x) * 100);
1144
- nativeFilter(`saturate(0%) brightness(${b}%) contrast(1000000%)`);
1145
- } else if (typ == Q5.GRAY) {
1146
- nativeFilter(`saturate(0%)`);
1147
- } else if (typ == Q5.OPAQUE) {
1148
- tmpCtx.fillStyle = 'black';
1149
- tmpCtx.fillRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
1150
- tmpCtx.drawImage($.canvas, 0, 0);
1151
- $.ctx.save();
1152
- $.ctx.resetTransform();
1153
- $.ctx.drawImage(tmpCtx.canvas, 0, 0);
1154
- $.ctx.restore();
1155
- } else if (typ == Q5.INVERT) {
1156
- nativeFilter(`invert(100%)`);
1157
- } else if (typ == Q5.BLUR) {
1158
- nativeFilter(`blur(${Math.ceil((x * $._pixelDensity) / 1) || 1}px)`);
1159
- } else {
1160
- $._softFilter(typ, x);
1161
- }
1162
- };
1261
+ let b = Math.floor((0.5 / Math.max(x, 0.00001)) * 100);
1262
+ f = `saturate(0%) brightness(${b}%) contrast(1000000%)`;
1263
+ } else return $._softFilter(type, x);
1163
1264
 
1164
- $.resize = (w, h) => {
1165
- makeTmpCtx();
1166
- tmpCtx.drawImage($.canvas, 0, 0);
1167
- p.width = w;
1168
- p.height = h;
1169
- $.canvas.width = w * $._pixelDensity;
1170
- $.canvas.height = h * $._pixelDensity;
1171
- $.ctx.save();
1172
- $.ctx.resetTransform();
1173
- $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
1174
- $.ctx.drawImage(tmpCtx.canvas, 0, 0, $.canvas.width, $.canvas.height);
1175
- $.ctx.restore();
1265
+ $.ctx.filter = f;
1266
+ $.ctx.drawImage($.canvas, 0, 0, $.canvas.w, $.canvas.h);
1267
+ $.ctx.filter = 'none';
1176
1268
  };
1177
1269
 
1270
+ if ($._scope == 'image') {
1271
+ $.resize = (w, h) => {
1272
+ let o = new $._OffscreenCanvas($.canvas.width, $.canvas.height);
1273
+ let tmpCtx = o.getContext('2d', {
1274
+ colorSpace: $.canvas.colorSpace
1275
+ });
1276
+ tmpCtx.drawImage($.canvas, 0, 0);
1277
+ $._setCanvasSize(w, h);
1278
+
1279
+ $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
1280
+ $.ctx.drawImage(o, 0, 0, $.canvas.width, $.canvas.height);
1281
+ };
1282
+ }
1283
+
1178
1284
  $.trim = () => {
1179
1285
  let pd = $._pixelDensity || 1;
1180
- let imgData = $.ctx.getImageData(0, 0, $.width * pd, $.height * pd);
1181
- let data = imgData.data;
1182
- let left = $.width,
1286
+ let w = $.canvas.width;
1287
+ let h = $.canvas.height;
1288
+ let data = $.ctx.getImageData(0, 0, w, h).data;
1289
+ let left = w,
1183
1290
  right = 0,
1184
- top = $.height,
1291
+ top = h,
1185
1292
  bottom = 0;
1186
1293
 
1187
- for (let y = 0; y < $.height * pd; y++) {
1188
- for (let x = 0; x < $.width * pd; x++) {
1189
- let index = (y * $.width * pd + x) * 4;
1190
- if (data[index + 3] !== 0) {
1294
+ let i = 3;
1295
+ for (let y = 0; y < h; y++) {
1296
+ for (let x = 0; x < w; x++) {
1297
+ if (data[i] !== 0) {
1191
1298
  if (x < left) left = x;
1192
1299
  if (x > right) right = x;
1193
1300
  if (y < top) top = y;
1194
1301
  if (y > bottom) bottom = y;
1195
1302
  }
1303
+ i += 4;
1196
1304
  }
1197
1305
  }
1198
1306
  top = Math.floor(top / pd);
@@ -1213,48 +1321,6 @@ Q5.modules.q2d_image = ($, p) => {
1213
1321
  $.ctx.restore();
1214
1322
  };
1215
1323
 
1216
- $._save = async (data, name, ext) => {
1217
- name = name || 'untitled';
1218
- ext = ext || 'png';
1219
- if (ext == 'jpg' || ext == 'png' || ext == 'webp') {
1220
- if (data instanceof OffscreenCanvas) {
1221
- const blob = await data.convertToBlob({ type: 'image/' + ext });
1222
- data = await new Promise((resolve) => {
1223
- const reader = new FileReader();
1224
- reader.onloadend = () => resolve(reader.result);
1225
- reader.readAsDataURL(blob);
1226
- });
1227
- } else {
1228
- data = data.toDataURL('image/' + ext);
1229
- }
1230
- } else {
1231
- let type = 'text/plain';
1232
- if (ext == 'json') {
1233
- if (typeof data != 'string') data = JSON.stringify(data);
1234
- type = 'text/json';
1235
- }
1236
- data = new Blob([data], { type });
1237
- data = URL.createObjectURL(data);
1238
- }
1239
- let a = document.createElement('a');
1240
- a.href = data;
1241
- a.download = name + '.' + ext;
1242
- a.click();
1243
- URL.revokeObjectURL(a.href);
1244
- };
1245
- $.save = (a, b, c) => {
1246
- if (!a || (typeof a == 'string' && (!b || (!c && b.length < 5)))) {
1247
- c = b;
1248
- b = a;
1249
- a = $.canvas;
1250
- }
1251
- if (c) return $._save(a, b, c);
1252
- if (b) {
1253
- b = b.split('.');
1254
- $._save(a, b[0], b.at(-1));
1255
- } else $._save(a);
1256
- };
1257
-
1258
1324
  $.get = (x, y, w, h) => {
1259
1325
  let pd = $._pixelDensity || 1;
1260
1326
  if (x !== undefined && w === undefined) {
@@ -1299,174 +1365,23 @@ Q5.modules.q2d_image = ($, p) => {
1299
1365
 
1300
1366
  $.loadPixels = () => {
1301
1367
  imgData = $.ctx.getImageData(0, 0, $.canvas.width, $.canvas.height);
1302
- p.pixels = imgData.data;
1368
+ q.pixels = imgData.data;
1303
1369
  };
1304
1370
  $.updatePixels = () => {
1305
1371
  if (imgData != null) $.ctx.putImageData(imgData, 0, 0);
1306
1372
  };
1307
1373
 
1308
- $._tinted = function (col) {
1309
- let alpha = col.a;
1310
- col.a = 255;
1311
- makeTmpCtx();
1312
- tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
1313
- tmpCtx.fillStyle = col.toString();
1314
- tmpCtx.fillRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
1315
- tmpCtx.globalCompositeOperation = 'multiply';
1316
- tmpCtx.drawImage($.ctx.canvas, 0, 0);
1317
- tmpCtx.globalCompositeOperation = 'source-over';
1318
-
1319
- $.ctx.save();
1320
- $.ctx.resetTransform();
1321
- let old = $.ctx.globalCompositeOperation;
1322
- $.ctx.globalCompositeOperation = 'source-in';
1323
- $.ctx.drawImage(tmpCtx.canvas, 0, 0);
1324
- $.ctx.globalCompositeOperation = old;
1325
- $.ctx.restore();
1326
-
1327
- tmpCtx.globalAlpha = alpha / 255;
1328
- tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
1329
- tmpCtx.drawImage($.ctx.canvas, 0, 0);
1330
- tmpCtx.globalAlpha = 1;
1331
-
1332
- $.ctx.save();
1333
- $.ctx.resetTransform();
1334
- $.ctx.clearRect(0, 0, $.ctx.canvas.width, $.ctx.canvas.height);
1335
- $.ctx.drawImage(tmpCtx.canvas, 0, 0);
1336
- $.ctx.restore();
1337
- };
1338
-
1339
1374
  $.smooth = () => ($.ctx.imageSmoothingEnabled = true);
1340
1375
  $.noSmooth = () => ($.ctx.imageSmoothingEnabled = false);
1341
1376
 
1342
1377
  if ($._scope == 'image') return;
1343
1378
 
1344
- $.saveCanvas = $.canvas.save = $.save;
1345
-
1346
1379
  $.tint = function (c) {
1347
1380
  $._tint = c._q5Color ? c : $.color(...arguments);
1348
1381
  };
1349
1382
  $.noTint = () => ($._tint = null);
1350
-
1351
- // IMAGING
1352
-
1353
- $.imageMode = (mode) => ($._imageMode = mode);
1354
- $.image = (img, dx, dy, dWidth, dHeight, sx = 0, sy = 0, sWidth, sHeight) => {
1355
- if ($._da) {
1356
- dx *= $._da;
1357
- dy *= $._da;
1358
- dWidth *= $._da;
1359
- dHeight *= $._da;
1360
- sx *= $._da;
1361
- sy *= $._da;
1362
- sWidth *= $._da;
1363
- sHeight *= $._da;
1364
- }
1365
- let drawable = img.canvas || img;
1366
- if (Q5._createNodeJSCanvas) {
1367
- drawable = drawable.context.canvas;
1368
- }
1369
- function reset() {
1370
- if (!img._q5 || !$._tint) return;
1371
- let c = img.ctx;
1372
- c.save();
1373
- c.resetTransform();
1374
- c.clearRect(0, 0, c.canvas.width, c.canvas.height);
1375
- c.drawImage(tmpCt2.canvas, 0, 0);
1376
- c.restore();
1377
- }
1378
- if (img.canvas && $._tint != null) {
1379
- makeTmpCt2(img.canvas.width, img.canvas.height);
1380
- tmpCt2.drawImage(img.canvas, 0, 0);
1381
- img._tinted($._tint);
1382
- }
1383
- dWidth ??= img.width || img.videoWidth;
1384
- dHeight ??= img.height || img.videoHeight;
1385
- if ($._imageMode == 'center') {
1386
- dx -= dWidth * 0.5;
1387
- dy -= dHeight * 0.5;
1388
- }
1389
- let pd = img._pixelDensity || 1;
1390
- if (!sWidth) {
1391
- sWidth = drawable.width || drawable.videoWidth;
1392
- } else sWidth *= pd;
1393
- if (!sHeight) {
1394
- sHeight = drawable.height || drawable.videoHeight;
1395
- } else sHeight *= pd;
1396
- $.ctx.drawImage(drawable, sx * pd, sy * pd, sWidth, sHeight, dx, dy, dWidth, dHeight);
1397
- reset();
1398
- };
1399
-
1400
- $.loadImage = function (url, cb, opt) {
1401
- if (url.canvas) return url;
1402
- if (url.slice(-3).toLowerCase() == 'gif') {
1403
- throw new Error(`q5 doesn't support GIFs due to their impact on performance. Use a video or animation instead.`);
1404
- }
1405
- p._preloadCount++;
1406
- let last = [...arguments].at(-1);
1407
- opt = typeof last == 'object' ? last : null;
1408
-
1409
- let g = $.createImage(1, 1, opt);
1410
-
1411
- function loaded(img) {
1412
- let c = g.ctx;
1413
- g.width = c.canvas.width = img.naturalWidth || img.width;
1414
- g.height = c.canvas.height = img.naturalHeight || img.height;
1415
- c.drawImage(img, 0, 0);
1416
- p._preloadCount--;
1417
- if (cb) cb(g);
1418
- }
1419
-
1420
- if (Q5._nodejs && global.CairoCanvas) {
1421
- global.CairoCanvas.loadImage(url)
1422
- .then(loaded)
1423
- .catch((e) => {
1424
- p._preloadCount--;
1425
- throw e;
1426
- });
1427
- } else {
1428
- let img = new window.Image();
1429
- img.src = url;
1430
- img.crossOrigin = 'Anonymous';
1431
- img._pixelDensity = 1;
1432
- img.onload = () => loaded(img);
1433
- img.onerror = (e) => {
1434
- p._preloadCount--;
1435
- throw e;
1436
- };
1437
- }
1438
- return g;
1439
- };
1440
1383
  };
1441
1384
 
1442
- // IMAGE CLASS
1443
-
1444
- Q5.imageModules = ['q2d_canvas', 'q2d_image'];
1445
-
1446
- class _Q5Image {
1447
- constructor(w, h, opt) {
1448
- let $ = this;
1449
- $._scope = 'image';
1450
- $.canvas = $.ctx = $.drawingContext = null;
1451
- $.pixels = [];
1452
- for (let m of Q5.imageModules) {
1453
- Q5.modules[m]($, $);
1454
- }
1455
- delete this.createCanvas;
1456
-
1457
- this._createCanvas(w, h, '2d', opt);
1458
- this._loop = false;
1459
- }
1460
- get w() {
1461
- return this.width;
1462
- }
1463
- get h() {
1464
- return this.height;
1465
- }
1466
- }
1467
-
1468
- Q5.Image ??= _Q5Image;
1469
-
1470
1385
  Q5.THRESHOLD = 1;
1471
1386
  Q5.GRAY = 2;
1472
1387
  Q5.OPAQUE = 3;
@@ -1476,12 +1391,12 @@ Q5.DILATE = 6;
1476
1391
  Q5.ERODE = 7;
1477
1392
  Q5.BLUR = 8;
1478
1393
  /* software implementation of image filters */
1479
- Q5.modules.q2d_soft_filters = ($) => {
1394
+ Q5.renderers.q2d.soft_filters = ($) => {
1480
1395
  let tmpBuf = null;
1481
1396
 
1482
- function makeTmpBuf() {
1397
+ function ensureTmpBuf() {
1483
1398
  let l = $.canvas.width * $.canvas.height * 4;
1484
- if (!tmpBuf || l != tmpBuf.length) {
1399
+ if (!tmpBuf || tmpBuf.length != l) {
1485
1400
  tmpBuf = new Uint8ClampedArray(l);
1486
1401
  }
1487
1402
  }
@@ -1523,7 +1438,7 @@ Q5.modules.q2d_soft_filters = ($) => {
1523
1438
  }
1524
1439
  };
1525
1440
  $._filters[Q5.DILATE] = (data) => {
1526
- makeTmpBuf();
1441
+ ensureTmpBuf();
1527
1442
  tmpBuf.set(data);
1528
1443
  let [w, h] = [$.canvas.width, $.canvas.height];
1529
1444
  for (let i = 0; i < h; i++) {
@@ -1550,7 +1465,7 @@ Q5.modules.q2d_soft_filters = ($) => {
1550
1465
  }
1551
1466
  };
1552
1467
  $._filters[Q5.ERODE] = (data) => {
1553
- makeTmpBuf();
1468
+ ensureTmpBuf();
1554
1469
  tmpBuf.set(data);
1555
1470
  let [w, h] = [$.canvas.width, $.canvas.height];
1556
1471
  for (let i = 0; i < h; i++) {
@@ -1579,7 +1494,7 @@ Q5.modules.q2d_soft_filters = ($) => {
1579
1494
  $._filters[Q5.BLUR] = (data, rad) => {
1580
1495
  rad = rad || 1;
1581
1496
  rad = Math.floor(rad * $._pixelDensity);
1582
- makeTmpBuf();
1497
+ ensureTmpBuf();
1583
1498
  tmpBuf.set(data);
1584
1499
 
1585
1500
  let ksize = rad * 2 + 1;
@@ -1651,7 +1566,7 @@ Q5.modules.q2d_soft_filters = ($) => {
1651
1566
  $.ctx.putImageData(imgData, 0, 0);
1652
1567
  };
1653
1568
  };
1654
- Q5.modules.q2d_text = ($, p) => {
1569
+ Q5.renderers.q2d.text = ($, q) => {
1655
1570
  $.NORMAL = 'normal';
1656
1571
  $.ITALIC = 'italic';
1657
1572
  $.BOLD = 'bold';
@@ -1671,12 +1586,12 @@ Q5.modules.q2d_text = ($, p) => {
1671
1586
  $._textStyle = 'normal';
1672
1587
 
1673
1588
  $.loadFont = (url, cb) => {
1674
- p._preloadCount++;
1589
+ q._preloadCount++;
1675
1590
  let name = url.split('/').pop().split('.')[0].replace(' ', '');
1676
1591
  let f = new FontFace(name, `url(${url})`);
1677
1592
  document.fonts.add(f);
1678
1593
  f.load().then(() => {
1679
- p._preloadCount--;
1594
+ q._preloadCount--;
1680
1595
  if (cb) cb(name);
1681
1596
  });
1682
1597
  return name;
@@ -1865,8 +1780,8 @@ Q5.modules.q2d_text = ($, p) => {
1865
1780
  };
1866
1781
  };
1867
1782
  Q5.modules.ai = ($) => {
1868
- $.askAI = (q = '') => {
1869
- throw Error('Ask AI ✨ ' + q);
1783
+ $.askAI = (question = '') => {
1784
+ throw Error('Ask AI ✨ ' + question);
1870
1785
  };
1871
1786
 
1872
1787
  $._aiErrorAssistance = async (e) => {
@@ -1929,22 +1844,24 @@ Q5.modules.ai = ($) => {
1929
1844
  } catch (err) {}
1930
1845
  };
1931
1846
  };
1932
- Q5.modules.color = ($, p) => {
1847
+ Q5.modules.color = ($, q) => {
1933
1848
  $.RGB = $.RGBA = $._colorMode = 'rgb';
1934
1849
  $.OKLCH = 'oklch';
1935
1850
 
1936
- if (Q5.supportsHDR) $.Color = Q5.ColorRGBA_P3;
1937
- else $.Color = Q5.ColorRGBA;
1938
-
1939
- $.colorMode = (mode) => {
1851
+ $.colorMode = (mode, format) => {
1940
1852
  $._colorMode = mode;
1853
+ let srgb = $.canvas.colorSpace == 'srgb' || mode == 'srgb';
1854
+ format ??= srgb ? 'integer' : 'float';
1855
+ $._colorFormat = format;
1941
1856
  if (mode == 'oklch') {
1942
- p.Color = Q5.ColorOKLCH;
1943
- } else if (mode == 'rgb') {
1944
- if ($.canvas.colorSpace == 'srgb') p.Color = Q5.ColorRGBA;
1945
- else p.Color = Q5.ColorRGBA_P3;
1946
- } else if (mode == 'srgb') {
1947
- p.Color = Q5.ColorRGBA;
1857
+ q.Color = Q5.ColorOKLCH;
1858
+ } else {
1859
+ let srgb = $.canvas.colorSpace == 'srgb';
1860
+ if ($._colorFormat == 'integer') {
1861
+ q.Color = srgb ? Q5.ColorRGBA_8 : Q5.ColorRGBA_P3_8;
1862
+ } else {
1863
+ q.Color = srgb ? Q5.ColorRGBA : Q5.ColorRGBA_P3;
1864
+ }
1948
1865
  $._colorMode = 'rgb';
1949
1866
  }
1950
1867
  };
@@ -1989,27 +1906,33 @@ Q5.modules.color = ($, p) => {
1989
1906
  let args = arguments;
1990
1907
  if (args.length == 1) {
1991
1908
  if (typeof c0 == 'string') {
1909
+ let r, g, b, a;
1992
1910
  if (c0[0] == '#') {
1993
1911
  if (c0.length <= 5) {
1994
- return new C(
1995
- parseInt(c0[1] + c0[1], 16),
1996
- parseInt(c0[2] + c0[2], 16),
1997
- parseInt(c0[3] + c0[3], 16),
1998
- c0.length == 4 ? null : parseInt(c0[4] + c0[4], 16)
1999
- );
1912
+ r = parseInt(c0[1] + c0[1], 16);
1913
+ g = parseInt(c0[2] + c0[2], 16);
1914
+ b = parseInt(c0[3] + c0[3], 16);
1915
+ a = c0.length == 4 ? null : parseInt(c0[4] + c0[4], 16);
1916
+ } else {
1917
+ r = parseInt(c0.slice(1, 3), 16);
1918
+ g = parseInt(c0.slice(3, 5), 16);
1919
+ b = parseInt(c0.slice(5, 7), 16);
1920
+ a = c0.length == 7 ? null : parseInt(c0.slice(7, 9), 16);
2000
1921
  }
2001
- return new C(
2002
- parseInt(c0.slice(1, 3), 16),
2003
- parseInt(c0.slice(3, 5), 16),
2004
- parseInt(c0.slice(5, 7), 16),
2005
- c0.length == 7 ? null : parseInt(c0.slice(7, 9), 16)
1922
+ } else if ($._namedColors[c0]) [r, g, b] = $._namedColors[c0];
1923
+ else {
1924
+ console.error(
1925
+ "q5 can't parse color: " + c0 + '\nOnly numeric input, hex, and common named colors are supported.'
2006
1926
  );
1927
+ return new C(0, 0, 0);
2007
1928
  }
2008
- if ($._namedColors[c0]) return new C(...$._namedColors[c0]);
2009
- console.error(
2010
- "q5 can't parse color: " + c0 + '\nOnly numeric input, hex, and common named colors are supported.'
2011
- );
2012
- return new C(0, 0, 0);
1929
+ if ($._colorFormat != 'integer') {
1930
+ r /= 255;
1931
+ g /= 255;
1932
+ b /= 255;
1933
+ if (a != null) a /= 255;
1934
+ }
1935
+ return new C(r, g, b, a);
2013
1936
  }
2014
1937
  if (Array.isArray(c0)) return new C(...c0);
2015
1938
  }
@@ -2079,7 +2002,24 @@ Q5.ColorRGBA = class extends Q5.Color {
2079
2002
  this.r = r;
2080
2003
  this.g = g;
2081
2004
  this.b = b;
2082
- this.a = a ?? 255;
2005
+ this.a = a ?? 1;
2006
+ }
2007
+ get levels() {
2008
+ return [this.r, this.g, this.b, this.a];
2009
+ }
2010
+ toString() {
2011
+ return `color(srgb ${this.r} ${this.g} ${this.b} / ${this.a})`;
2012
+ }
2013
+ };
2014
+ Q5.ColorRGBA_P3 = class extends Q5.ColorRGBA {
2015
+ toString() {
2016
+ return `color(display-p3 ${this.r} ${this.g} ${this.b} / ${this.a})`;
2017
+ }
2018
+ };
2019
+ // legacy 8-bit (0-255) integer color format
2020
+ Q5.ColorRGBA_8 = class extends Q5.ColorRGBA {
2021
+ constructor(r, g, b, a) {
2022
+ super(r, g, b, a ?? 255);
2083
2023
  }
2084
2024
  setRed(v) {
2085
2025
  this.r = v;
@@ -2100,9 +2040,10 @@ Q5.ColorRGBA = class extends Q5.Color {
2100
2040
  return `rgb(${this.r} ${this.g} ${this.b} / ${this.a / 255})`;
2101
2041
  }
2102
2042
  };
2103
- Q5.ColorRGBA_P3 = class extends Q5.ColorRGBA {
2043
+ // p3 10-bit color in integer color format, for backwards compatibility
2044
+ Q5.ColorRGBA_P3_8 = class extends Q5.ColorRGBA {
2104
2045
  constructor(r, g, b, a) {
2105
- super(r, g, b, a);
2046
+ super(r, g, b, a ?? 255);
2106
2047
  this._edited = true;
2107
2048
  }
2108
2049
  get r() {
@@ -2231,8 +2172,14 @@ main {
2231
2172
  Object.assign(c, { displayMode, renderQuality, displayScale });
2232
2173
  $._adjustDisplay();
2233
2174
  };
2175
+
2176
+ $.fullscreen = (v) => {
2177
+ if (v === undefined) return document.fullscreenElement;
2178
+ if (v) document.body.requestFullscreen();
2179
+ else document.body.exitFullscreen();
2180
+ };
2234
2181
  };
2235
- Q5.modules.input = ($, p) => {
2182
+ Q5.modules.input = ($, q) => {
2236
2183
  if ($._scope == 'graphics') return;
2237
2184
 
2238
2185
  $.mouseX = 0;
@@ -2276,17 +2223,26 @@ Q5.modules.input = ($, p) => {
2276
2223
 
2277
2224
  $._updateMouse = (e) => {
2278
2225
  if (e.changedTouches) return;
2279
- let rect = $.canvas.getBoundingClientRect();
2280
- let sx = $.canvas.scrollWidth / $.width || 1;
2281
- let sy = $.canvas.scrollHeight / $.height || 1;
2282
- p.mouseX = (e.clientX - rect.left) / sx;
2283
- p.mouseY = (e.clientY - rect.top) / sy;
2226
+ if (c) {
2227
+ let rect = c.getBoundingClientRect();
2228
+ let sx = c.scrollWidth / $.width || 1;
2229
+ let sy = c.scrollHeight / $.height || 1;
2230
+ q.mouseX = (e.clientX - rect.left) / sx;
2231
+ q.mouseY = (e.clientY - rect.top) / sy;
2232
+ if (c.renderer == 'webgpu') {
2233
+ q.mouseX -= c.hw;
2234
+ q.mouseY -= c.hh;
2235
+ }
2236
+ } else {
2237
+ q.mouseX = e.clientX;
2238
+ q.mouseY = e.clientY;
2239
+ }
2284
2240
  };
2285
2241
  $._onmousedown = (e) => {
2286
2242
  $._startAudio();
2287
2243
  $._updateMouse(e);
2288
- p.mouseIsPressed = true;
2289
- p.mouseButton = mouseBtns[e.button];
2244
+ q.mouseIsPressed = true;
2245
+ q.mouseButton = mouseBtns[e.button];
2290
2246
  $.mousePressed(e);
2291
2247
  };
2292
2248
  $._onmousemove = (e) => {
@@ -2296,18 +2252,15 @@ Q5.modules.input = ($, p) => {
2296
2252
  };
2297
2253
  $._onmouseup = (e) => {
2298
2254
  $._updateMouse(e);
2299
- p.mouseIsPressed = false;
2255
+ q.mouseIsPressed = false;
2300
2256
  $.mouseReleased(e);
2301
2257
  };
2302
2258
  $._onclick = (e) => {
2303
2259
  $._updateMouse(e);
2304
- p.mouseIsPressed = true;
2260
+ q.mouseIsPressed = true;
2305
2261
  $.mouseClicked(e);
2306
- p.mouseIsPressed = false;
2262
+ q.mouseIsPressed = false;
2307
2263
  };
2308
- c.addEventListener('mousedown', (e) => $._onmousedown(e));
2309
- c.addEventListener('mouseup', (e) => $._onmouseup(e));
2310
- c.addEventListener('click', (e) => $._onclick(e));
2311
2264
 
2312
2265
  $.cursor = (name, x, y) => {
2313
2266
  let pfx = '';
@@ -2329,17 +2282,17 @@ Q5.modules.input = ($, p) => {
2329
2282
  $._onkeydown = (e) => {
2330
2283
  if (e.repeat) return;
2331
2284
  $._startAudio();
2332
- p.keyIsPressed = true;
2333
- p.key = e.key;
2334
- p.keyCode = e.keyCode;
2285
+ q.keyIsPressed = true;
2286
+ q.key = e.key;
2287
+ q.keyCode = e.keyCode;
2335
2288
  keysHeld[$.keyCode] = keysHeld[$.key.toLowerCase()] = true;
2336
2289
  $.keyPressed(e);
2337
2290
  if (e.key.length == 1) $.keyTyped(e);
2338
2291
  };
2339
2292
  $._onkeyup = (e) => {
2340
- p.keyIsPressed = false;
2341
- p.key = e.key;
2342
- p.keyCode = e.keyCode;
2293
+ q.keyIsPressed = false;
2294
+ q.key = e.key;
2295
+ q.keyCode = e.keyCode;
2343
2296
  keysHeld[$.keyCode] = keysHeld[$.key.toLowerCase()] = false;
2344
2297
  $.keyReleased(e);
2345
2298
  };
@@ -2357,37 +2310,43 @@ Q5.modules.input = ($, p) => {
2357
2310
  }
2358
2311
  $._ontouchstart = (e) => {
2359
2312
  $._startAudio();
2360
- p.touches = [...e.touches].map(getTouchInfo);
2313
+ q.touches = [...e.touches].map(getTouchInfo);
2361
2314
  if (!$._isTouchAware) {
2362
- p.mouseX = $.touches[0].x;
2363
- p.mouseY = $.touches[0].y;
2364
- p.mouseIsPressed = true;
2365
- p.mouseButton = $.LEFT;
2315
+ q.mouseX = $.touches[0].x;
2316
+ q.mouseY = $.touches[0].y;
2317
+ q.mouseIsPressed = true;
2318
+ q.mouseButton = $.LEFT;
2366
2319
  if (!$.mousePressed(e)) e.preventDefault();
2367
2320
  }
2368
2321
  if (!$.touchStarted(e)) e.preventDefault();
2369
2322
  };
2370
2323
  $._ontouchmove = (e) => {
2371
- p.touches = [...e.touches].map(getTouchInfo);
2324
+ q.touches = [...e.touches].map(getTouchInfo);
2372
2325
  if (!$._isTouchAware) {
2373
- p.mouseX = $.touches[0].x;
2374
- p.mouseY = $.touches[0].y;
2326
+ q.mouseX = $.touches[0].x;
2327
+ q.mouseY = $.touches[0].y;
2375
2328
  if (!$.mouseDragged(e)) e.preventDefault();
2376
2329
  }
2377
2330
  if (!$.touchMoved(e)) e.preventDefault();
2378
2331
  };
2379
2332
  $._ontouchend = (e) => {
2380
- p.touches = [...e.touches].map(getTouchInfo);
2333
+ q.touches = [...e.touches].map(getTouchInfo);
2381
2334
  if (!$._isTouchAware && !$.touches.length) {
2382
- p.mouseIsPressed = false;
2335
+ q.mouseIsPressed = false;
2383
2336
  if (!$.mouseReleased(e)) e.preventDefault();
2384
2337
  }
2385
2338
  if (!$.touchEnded(e)) e.preventDefault();
2386
2339
  };
2387
- c.addEventListener('touchstart', (e) => $._ontouchstart(e));
2388
- c.addEventListener('touchmove', (e) => $._ontouchmove(e));
2389
- c.addEventListener('touchcancel', (e) => $._ontouchend(e));
2390
- c.addEventListener('touchend', (e) => $._ontouchend(e));
2340
+
2341
+ if (c) {
2342
+ c.addEventListener('mousedown', (e) => $._onmousedown(e));
2343
+ c.addEventListener('mouseup', (e) => $._onmouseup(e));
2344
+ c.addEventListener('click', (e) => $._onclick(e));
2345
+ c.addEventListener('touchstart', (e) => $._ontouchstart(e));
2346
+ c.addEventListener('touchmove', (e) => $._ontouchmove(e));
2347
+ c.addEventListener('touchcancel', (e) => $._ontouchend(e));
2348
+ c.addEventListener('touchend', (e) => $._ontouchend(e));
2349
+ }
2391
2350
 
2392
2351
  if (window) {
2393
2352
  let l = window.addEventListener;
@@ -2396,7 +2355,7 @@ Q5.modules.input = ($, p) => {
2396
2355
  l('keyup', (e) => $._onkeyup(e), false);
2397
2356
  }
2398
2357
  };
2399
- Q5.modules.math = ($, p) => {
2358
+ Q5.modules.math = ($, q) => {
2400
2359
  $.DEGREES = 'degrees';
2401
2360
  $.RADIANS = 'radians';
2402
2361
 
@@ -2425,7 +2384,7 @@ Q5.modules.math = ($, p) => {
2425
2384
  $.degrees = (x) => x * $._RADTODEG;
2426
2385
  $.radians = (x) => x * $._DEGTORAD;
2427
2386
 
2428
- $.map = (value, istart, istop, ostart, ostop, clamp) => {
2387
+ $.map = Q5.prototype.map = (value, istart, istop, ostart, ostop, clamp) => {
2429
2388
  let val = ostart + (ostop - ostart) * (((value - istart) * 1.0) / (istop - istart));
2430
2389
  if (!clamp) {
2431
2390
  return val;
@@ -2683,7 +2642,7 @@ Q5.modules.math = ($, p) => {
2683
2642
  let _noise;
2684
2643
 
2685
2644
  $.noiseMode = (mode) => {
2686
- p.Noise = Q5[mode[0].toUpperCase() + mode.slice(1) + 'Noise'];
2645
+ q.Noise = Q5[mode[0].toUpperCase() + mode.slice(1) + 'Noise'];
2687
2646
  _noise = null;
2688
2647
  };
2689
2648
  $.noiseSeed = (seed) => {
@@ -2816,14 +2775,15 @@ Q5.PerlinNoise = class extends Q5.Noise {
2816
2775
  return (total / maxAmp + 1) / 2;
2817
2776
  }
2818
2777
  };
2819
- Q5.modules.sound = ($, p) => {
2778
+ Q5.modules.sound = ($, q) => {
2820
2779
  $.Sound = Q5.Sound;
2821
2780
  $.loadSound = (path, cb) => {
2822
- p._preloadCount++;
2781
+ q._preloadCount++;
2823
2782
  Q5.aud ??= new window.AudioContext();
2824
2783
  let a = new Q5.Sound(path, cb);
2825
2784
  a.addEventListener('canplaythrough', () => {
2826
- p._preloadCount--;
2785
+ q._preloadCount--;
2786
+ a.loaded = true;
2827
2787
  if (cb) cb(a);
2828
2788
  });
2829
2789
  return a;
@@ -2855,10 +2815,16 @@ Q5.Sound = class extends Audio {
2855
2815
  setPan(value) {
2856
2816
  this.pan = value;
2857
2817
  }
2818
+ isLoaded() {
2819
+ return this.loaded;
2820
+ }
2821
+ isPlaying() {
2822
+ return !this.paused;
2823
+ }
2858
2824
  };
2859
- Q5.modules.util = ($, p) => {
2825
+ Q5.modules.util = ($, q) => {
2860
2826
  $._loadFile = (path, cb, type) => {
2861
- p._preloadCount++;
2827
+ q._preloadCount++;
2862
2828
  let ret = {};
2863
2829
  fetch(path)
2864
2830
  .then((r) => {
@@ -2866,7 +2832,7 @@ Q5.modules.util = ($, p) => {
2866
2832
  if (type == 'text') return r.text();
2867
2833
  })
2868
2834
  .then((r) => {
2869
- p._preloadCount--;
2835
+ q._preloadCount--;
2870
2836
  Object.assign(ret, r);
2871
2837
  if (cb) cb(r);
2872
2838
  });
@@ -2911,7 +2877,7 @@ Q5.Vector = class {
2911
2877
  return new Q5.Vector(this.x, this.y, this.z);
2912
2878
  }
2913
2879
  _arg2v(x, y, z) {
2914
- if (x.x !== undefined) return x;
2880
+ if (x?.x !== undefined) return x;
2915
2881
  if (y !== undefined) {
2916
2882
  return { x, y, z: z || 0 };
2917
2883
  }