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