q5 2.18.1 → 2.19.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,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 2.18
3
+ * @version 2.19
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -104,9 +104,9 @@ function Q5(scope, parent, renderer) {
104
104
  throw e;
105
105
  }
106
106
  for (let m of Q5.methods.post) m.call($);
107
+ $.postProcess();
107
108
  if ($._render) $._render();
108
109
  if ($._finishRender) $._finishRender();
109
- $.postProcess();
110
110
  q.pmouseX = $.mouseX;
111
111
  q.pmouseY = $.mouseY;
112
112
  q.moveX = q.moveY = 0;
@@ -223,6 +223,8 @@ function Q5(scope, parent, renderer) {
223
223
  $.postProcess ??= () => {};
224
224
 
225
225
  let userFns = [
226
+ 'setup',
227
+ 'postProcess',
226
228
  'mouseMoved',
227
229
  'mousePressed',
228
230
  'mouseReleased',
@@ -311,7 +313,7 @@ function createCanvas(w, h, opt) {
311
313
  }
312
314
  }
313
315
 
314
- Q5.version = Q5.VERSION = '2.18';
316
+ Q5.version = Q5.VERSION = '2.19';
315
317
 
316
318
  if (typeof document == 'object') {
317
319
  document.addEventListener('DOMContentLoaded', () => {
@@ -420,49 +422,6 @@ Q5.modules.canvas = ($, q) => {
420
422
  return g;
421
423
  };
422
424
 
423
- async function saveFile(data, name, ext) {
424
- name = name || 'untitled';
425
- ext = ext || 'png';
426
- if (ext == 'jpg' || ext == 'png' || ext == 'webp') {
427
- if (data instanceof OffscreenCanvas) {
428
- const blob = await data.convertToBlob({ type: 'image/' + ext });
429
- data = await new Promise((resolve) => {
430
- const reader = new FileReader();
431
- reader.onloadend = () => resolve(reader.result);
432
- reader.readAsDataURL(blob);
433
- });
434
- } else {
435
- data = data.toDataURL('image/' + ext);
436
- }
437
- } else {
438
- let type = 'text/plain';
439
- if (ext == 'json') {
440
- if (typeof data != 'string') data = JSON.stringify(data);
441
- type = 'text/json';
442
- }
443
- data = new Blob([data], { type });
444
- data = URL.createObjectURL(data);
445
- }
446
- let a = document.createElement('a');
447
- a.href = data;
448
- a.download = name + '.' + ext;
449
- a.click();
450
- URL.revokeObjectURL(a.href);
451
- }
452
-
453
- $.save = (a, b, c) => {
454
- if (!a || (typeof a == 'string' && (!b || (!c && b.length < 5)))) {
455
- c = b;
456
- b = a;
457
- a = $.canvas;
458
- }
459
- if (c) return saveFile(a, b, c);
460
- if (b) {
461
- b = b.split('.');
462
- saveFile(a, b[0], b.at(-1));
463
- } else saveFile(a);
464
- };
465
-
466
425
  $._setCanvasSize = (w, h) => {
467
426
  if (!w) h ??= window.innerHeight;
468
427
  else h ??= w;
@@ -1322,10 +1281,7 @@ Q5.renderers.c2d.image = ($, q) => {
1322
1281
  opt ??= {};
1323
1282
  opt.alpha ??= true;
1324
1283
  opt.colorSpace ??= $.canvas.colorSpace || Q5.canvasOptions.colorSpace;
1325
- let img = new Q5.Image(w, h, opt);
1326
- img.defaultWidth = w * $._defaultImageScale;
1327
- img.defaultHeight = h * $._defaultImageScale;
1328
- return img;
1284
+ return new Q5.Image(w, h, opt);
1329
1285
  };
1330
1286
 
1331
1287
  $.loadImage = function (url, cb, opt) {
@@ -1481,7 +1437,7 @@ Q5.renderers.c2d.image = ($, q) => {
1481
1437
  $.ctx.globalCompositeOperation = 'source-over';
1482
1438
  $.ctx.drawImage($.canvas, 0, 0, $.canvas.w, $.canvas.h);
1483
1439
  $.ctx.restore();
1484
- $._retint = true;
1440
+ $.modified = $._retint = true;
1485
1441
  };
1486
1442
 
1487
1443
  if ($._scope == 'image') {
@@ -1499,7 +1455,7 @@ Q5.renderers.c2d.image = ($, q) => {
1499
1455
  $.ctx.clearRect(0, 0, c.width, c.height);
1500
1456
  $.ctx.drawImage(o, 0, 0, c.width, c.height);
1501
1457
 
1502
- $._retint = true;
1458
+ $.modified = $._retint = true;
1503
1459
  };
1504
1460
  }
1505
1461
 
@@ -1546,17 +1502,25 @@ Q5.renderers.c2d.image = ($, q) => {
1546
1502
  $.ctx.globalCompositeOperation = old;
1547
1503
  $.ctx.restore();
1548
1504
 
1549
- $._retint = true;
1505
+ $.modified = $._retint = true;
1550
1506
  };
1551
1507
 
1552
1508
  $.inset = (x, y, w, h, dx, dy, dw, dh) => {
1553
1509
  let pd = $._pixelDensity || 1;
1554
1510
  $.ctx.drawImage($.canvas, x * pd, y * pd, w * pd, h * pd, dx, dy, dw, dh);
1555
1511
 
1556
- $._retint = true;
1512
+ $.modified = $._retint = true;
1557
1513
  };
1558
1514
 
1559
- $.copy = () => $.get();
1515
+ $.copy = () => {
1516
+ let img = $.get();
1517
+ for (let prop in $) {
1518
+ if (typeof $[prop] != 'function' && !/(canvas|ctx|texture|textureIndex)/.test(prop)) {
1519
+ img[prop] = $[prop];
1520
+ }
1521
+ }
1522
+ return img;
1523
+ };
1560
1524
 
1561
1525
  $.get = (x, y, w, h) => {
1562
1526
  let pd = $._pixelDensity || 1;
@@ -1566,22 +1530,19 @@ Q5.renderers.c2d.image = ($, q) => {
1566
1530
  }
1567
1531
  x = Math.floor(x || 0) * pd;
1568
1532
  y = Math.floor(y || 0) * pd;
1569
- let _w = (w = w || $.width);
1570
- let _h = (h = h || $.height);
1571
- w *= pd;
1572
- h *= pd;
1573
- let img = $.createImage(w, h);
1574
- img.ctx.drawImage($.canvas, x, y, w, h, 0, 0, w, h);
1575
- img._pixelDensity = pd;
1576
- img.width = _w;
1577
- img.height = _h;
1533
+ w ??= $.width;
1534
+ h ??= $.height;
1535
+ let img = $.createImage(w, h, { pixelDensity: pd });
1536
+ img.ctx.drawImage($.canvas, x, y, w * pd, h * pd, 0, 0, w, h);
1537
+ img.width = w;
1538
+ img.height = h;
1578
1539
  return img;
1579
1540
  };
1580
1541
 
1581
1542
  $.set = (x, y, c) => {
1582
1543
  x = Math.floor(x);
1583
1544
  y = Math.floor(y);
1584
- $._retint = true;
1545
+ $.modified = $._retint = true;
1585
1546
  if (c.canvas) {
1586
1547
  let old = $._tint;
1587
1548
  $._tint = null;
@@ -1609,7 +1570,7 @@ Q5.renderers.c2d.image = ($, q) => {
1609
1570
  $.updatePixels = () => {
1610
1571
  if (imgData != null) {
1611
1572
  $.ctx.putImageData(imgData, 0, 0);
1612
- $._retint = true;
1573
+ $.modified = $._retint = true;
1613
1574
  }
1614
1575
  };
1615
1576
 
@@ -1618,6 +1579,20 @@ Q5.renderers.c2d.image = ($, q) => {
1618
1579
 
1619
1580
  if ($._scope == 'image') return;
1620
1581
 
1582
+ $._saveCanvas = async (data, ext) => {
1583
+ data = data.canvas || data;
1584
+ if (data instanceof OffscreenCanvas) {
1585
+ const blob = await data.convertToBlob({ type: 'image/' + ext });
1586
+
1587
+ return await new Promise((resolve) => {
1588
+ const reader = new FileReader();
1589
+ reader.onloadend = () => resolve(reader.result);
1590
+ reader.readAsDataURL(blob);
1591
+ });
1592
+ }
1593
+ return data.toDataURL('image/' + ext);
1594
+ };
1595
+
1621
1596
  $.tint = function (c) {
1622
1597
  $._tint = (c._q5Color ? c : $.color(...arguments)).toString();
1623
1598
  };
@@ -1961,10 +1936,15 @@ Q5.renderers.c2d.text = ($, q) => {
1961
1936
  tY = leading * lines.length;
1962
1937
 
1963
1938
  if (!img) {
1939
+ let ogBaseline = $.ctx.textBaseline;
1940
+ $.ctx.textBaseline = 'alphabetic';
1941
+
1964
1942
  let measure = ctx.measureText(' ');
1965
1943
  let ascent = measure.fontBoundingBoxAscent;
1966
1944
  let descent = measure.fontBoundingBoxDescent;
1967
1945
 
1946
+ $.ctx.textBaseline = ogBaseline;
1947
+
1968
1948
  img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(tY + descent), {
1969
1949
  pixelDensity: $._pixelDensity
1970
1950
  });
@@ -1975,12 +1955,13 @@ Q5.renderers.c2d.text = ($, q) => {
1975
1955
  img._middle = img._top + ascent * 0.5;
1976
1956
  img._bottom = img._top + ascent;
1977
1957
  img._leading = leading;
1958
+ } else {
1959
+ img.modified = true;
1978
1960
  }
1979
1961
 
1980
1962
  img._fill = $._fill;
1981
1963
  img._stroke = $._stroke;
1982
1964
  img._strokeWeight = $._strokeWeight;
1983
- img.modified = true;
1984
1965
 
1985
1966
  ctx = img.ctx;
1986
1967
 
@@ -2082,8 +2063,8 @@ Q5.modules.ai = ($) => {
2082
2063
  let parts = errFile.split(':');
2083
2064
  let lineNum = parseInt(parts.at(-2));
2084
2065
  if (askAI) lineNum++;
2085
- parts[3] = parts[3].split(')')[0];
2086
- let fileUrl = parts.slice(0, 2).join(':');
2066
+ parts[parts.length - 1] = parts.at(-1).split(')')[0];
2067
+ let fileUrl = parts.slice(0, -2).join(':');
2087
2068
  let fileBase = fileUrl.split('/').at(-1);
2088
2069
 
2089
2070
  try {
@@ -2107,7 +2088,7 @@ Q5.modules.ai = ($) => {
2107
2088
  askAI && e.message.length > 10 ? e.message.slice(10) : 'Whats+wrong+with+this+line%3F+short+answer';
2108
2089
 
2109
2090
  let url =
2110
- 'https://chatgpt.com/?q=q5.js+' +
2091
+ 'https://chatgpt.com/?q=using+q5.js+not+p5.js+' +
2111
2092
  question +
2112
2093
  (askAI ? '' : '%0A%0A' + encodeURIComponent(e.name + ': ' + e.message)) +
2113
2094
  '%0A%0ALine%3A+' +
@@ -2911,7 +2892,7 @@ Q5.modules.input = ($, q) => {
2911
2892
  $._onwheel = (e) => {
2912
2893
  $._updateMouse(e);
2913
2894
  e.delta = e.deltaY;
2914
- if ($.mouseWheel(e) == false) e.preventDefault();
2895
+ if ($.mouseWheel(e) == false || $._noScroll) e.preventDefault();
2915
2896
  };
2916
2897
 
2917
2898
  $.cursor = (name, x, y) => {
@@ -2926,9 +2907,8 @@ Q5.modules.input = ($, q) => {
2926
2907
  $.canvas.style.cursor = name + pfx;
2927
2908
  };
2928
2909
 
2929
- $.noCursor = () => {
2930
- $.canvas.style.cursor = 'none';
2931
- };
2910
+ $.noCursor = () => ($.canvas.style.cursor = 'none');
2911
+ $.noScroll = () => ($._noScroll = true);
2932
2912
 
2933
2913
  if (window) {
2934
2914
  $.lockMouse = document.body?.requestPointerLock;
@@ -3167,6 +3147,14 @@ Q5.modules.math = ($, q) => {
3167
3147
  }
3168
3148
  };
3169
3149
 
3150
+ if ($._renderer == 'c2d' && !$._webgpuFallback) {
3151
+ $.randomX = (v = 0) => $.random(-v, $.canvas.w + v);
3152
+ $.randomY = (v = 0) => $.random(-v, $.canvas.h + v);
3153
+ } else {
3154
+ $.randomX = (v = 0) => $.random(-$.canvas.hw - v, $.canvas.hw + v);
3155
+ $.randomY = (v = 0) => $.random(-$.canvas.hh - v, $.canvas.hh + v);
3156
+ }
3157
+
3170
3158
  $.randomGenerator = (method) => {
3171
3159
  if (method == $.LCG) rng1 = lcg();
3172
3160
  else if (method == $.SHR3) rng1 = shr3();
@@ -3455,7 +3443,7 @@ Q5.PerlinNoise = class extends Q5.Noise {
3455
3443
  return (total / maxAmp + 1) / 2;
3456
3444
  }
3457
3445
  };
3458
- Q5.modules.record = ($) => {
3446
+ Q5.modules.record = ($, q) => {
3459
3447
  let rec, btn0, btn1, timer, formatSelect, qualitySelect;
3460
3448
 
3461
3449
  $.recording = false;
@@ -3549,26 +3537,27 @@ Q5.modules.record = ($) => {
3549
3537
  }
3550
3538
  }
3551
3539
 
3552
- formatSelect = $.createSelect();
3540
+ formatSelect = $.createSelect('format');
3553
3541
  for (const name in rec.formats) {
3554
3542
  formatSelect.option(name, rec.formats[name]);
3555
3543
  }
3544
+ formatSelect.title = 'Video Format';
3556
3545
  rec.append(formatSelect);
3557
3546
 
3558
- // prettier-ignore
3559
- rec.qualityPresets = {
3560
- SD: 10000000, // 10 Mbps
3561
- HD: 16000000, // 16 Mbps
3562
- FHD: 22000000, // 22 Mbps
3563
- QHD: 28000000, // 28 Mbps
3564
- '4K': 48000000, // 48 Mbps
3565
- '8K': 75000000 // 75 Mbps
3547
+ let qMult = {
3548
+ min: 0.1,
3549
+ low: 0.25,
3550
+ mid: 0.5,
3551
+ high: 0.75,
3552
+ ultra: 0.9,
3553
+ max: 1
3566
3554
  };
3567
3555
 
3568
- qualitySelect = $.createSelect();
3569
- for (let name in rec.qualityPresets) {
3570
- qualitySelect.option(name);
3556
+ qualitySelect = $.createSelect('quality');
3557
+ for (let name in qMult) {
3558
+ qualitySelect.option(name, qMult[name]);
3571
3559
  }
3560
+ qualitySelect.title = 'Video Quality';
3572
3561
  rec.append(qualitySelect);
3573
3562
 
3574
3563
  rec.encoderSettings = {};
@@ -3578,7 +3567,7 @@ Q5.modules.record = ($) => {
3578
3567
  }
3579
3568
 
3580
3569
  function changeQuality() {
3581
- rec.encoderSettings.videoBitsPerSecond = rec.qualityPresets[qualitySelect.value];
3570
+ rec.encoderSettings.videoBitsPerSecond = maxVideoBitRate * qualitySelect.value;
3582
3571
  }
3583
3572
 
3584
3573
  formatSelect.addEventListener('change', changeFormat);
@@ -3587,8 +3576,8 @@ Q5.modules.record = ($) => {
3587
3576
  Object.defineProperty(rec, 'quality', {
3588
3577
  get: () => qualitySelect.selected,
3589
3578
  set: (v) => {
3590
- v = v.toUpperCase();
3591
- if (rec.qualityPresets[v]) {
3579
+ v = v.toLowerCase();
3580
+ if (qMult[v]) {
3592
3581
  qualitySelect.selected = v;
3593
3582
  changeQuality();
3594
3583
  }
@@ -3607,11 +3596,15 @@ Q5.modules.record = ($) => {
3607
3596
  });
3608
3597
 
3609
3598
  let h = $.canvas.height;
3610
- rec.quality = h >= 4320 ? '8K' : h >= 2160 ? '4K' : h >= 1440 ? 'QHD' : h >= 1080 ? 'FHD' : h >= 720 ? 'HD' : 'SD';
3611
3599
 
3612
3600
  if (h >= 1440 && rec.formats.VP9) rec.format = 'VP9';
3613
3601
  else rec.format = 'H.264';
3614
3602
 
3603
+ let maxVideoBitRate =
3604
+ (h >= 4320 ? 128 : h >= 2160 ? 75 : h >= 1440 ? 36 : h >= 1080 ? 28 : h >= 720 ? 22 : 16) * 1000000;
3605
+
3606
+ rec.quality = 'high';
3607
+
3615
3608
  btn0.addEventListener('click', () => {
3616
3609
  if (!$.recording) start();
3617
3610
  else if (!rec.paused) $.pauseRecording();
@@ -3652,7 +3645,7 @@ Q5.modules.record = ($) => {
3652
3645
  });
3653
3646
 
3654
3647
  rec.mediaRecorder.start();
3655
- $.recording = true;
3648
+ q.recording = true;
3656
3649
  rec.paused = false;
3657
3650
  rec.classList.add('recording');
3658
3651
 
@@ -3673,7 +3666,7 @@ Q5.modules.record = ($) => {
3673
3666
 
3674
3667
  rec.resetTimer();
3675
3668
  rec.mediaRecorder.stop();
3676
- $.recording = false;
3669
+ q.recording = false;
3677
3670
  rec.paused = false;
3678
3671
  rec.classList.remove('recording');
3679
3672
  }
@@ -3745,7 +3738,7 @@ Q5.modules.record = ($) => {
3745
3738
  $.deleteRecording = () => {
3746
3739
  stop();
3747
3740
  resetUI();
3748
- $.recording = false;
3741
+ q.recording = false;
3749
3742
  };
3750
3743
 
3751
3744
  $.saveRecording = async (fileName) => {
@@ -3770,7 +3763,8 @@ Q5.modules.record = ($) => {
3770
3763
  a.target = iframe.name;
3771
3764
  a.href = dataUrl;
3772
3765
  fileName ??=
3773
- 'recording ' +
3766
+ document.title +
3767
+ ' ' +
3774
3768
  new Date()
3775
3769
  .toLocaleString(undefined, { hour12: false })
3776
3770
  .replace(',', ' at')
@@ -3788,7 +3782,7 @@ Q5.modules.record = ($) => {
3788
3782
 
3789
3783
  setTimeout(() => URL.revokeObjectURL(dataUrl), 1000);
3790
3784
  resetUI();
3791
- $.recording = false;
3785
+ q.recording = false;
3792
3786
  };
3793
3787
  };
3794
3788
  Q5.modules.sound = ($, q) => {
@@ -4028,6 +4022,10 @@ Q5.modules.util = ($, q) => {
4028
4022
  $.loadJSON = (url, cb) => $._loadFile(url, cb, 'json');
4029
4023
  $.loadCSV = (url, cb) => $._loadFile(url, cb, 'csv');
4030
4024
 
4025
+ const imgRegex = /(jpe?g|png|gif|webp|avif|svg)/,
4026
+ fontRegex = /(ttf|otf|woff2?|eot|json)/,
4027
+ audioRegex = /(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/;
4028
+
4031
4029
  $.load = function (...urls) {
4032
4030
  if (Array.isArray(urls[0])) urls = urls[0];
4033
4031
 
@@ -4041,11 +4039,11 @@ Q5.modules.util = ($, q) => {
4041
4039
  obj = $.loadJSON(url);
4042
4040
  } else if (ext == 'csv') {
4043
4041
  obj = $.loadCSV(url);
4044
- } else if (/(jpe?g|png|gif|webp|avif|svg)/.test(ext)) {
4042
+ } else if (imgRegex.test(ext)) {
4045
4043
  obj = $.loadImage(url);
4046
- } else if (/(ttf|otf|woff2?|eot|json)/i.test(ext)) {
4044
+ } else if (fontRegex.test(ext)) {
4047
4045
  obj = $.loadFont(url);
4048
- } else if (/(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/.test(ext)) {
4046
+ } else if (audioRegex.test(ext)) {
4049
4047
  obj = $.loadSound(url);
4050
4048
  } else {
4051
4049
  obj = $.loadText(url);
@@ -4057,6 +4055,40 @@ Q5.modules.util = ($, q) => {
4057
4055
  return Promise.all(loaders);
4058
4056
  };
4059
4057
 
4058
+ async function saveFile(data, name, ext) {
4059
+ name = name || 'untitled';
4060
+ ext = ext || 'png';
4061
+ if (imgRegex.test(ext)) {
4062
+ data = await $._saveCanvas(data, ext);
4063
+ } else {
4064
+ let type = 'text/plain';
4065
+ if (ext == 'json') {
4066
+ if (typeof data != 'string') data = JSON.stringify(data);
4067
+ type = 'text/json';
4068
+ }
4069
+ data = new Blob([data], { type });
4070
+ data = URL.createObjectURL(data);
4071
+ }
4072
+ let a = document.createElement('a');
4073
+ a.href = data;
4074
+ a.download = name + '.' + ext;
4075
+ a.click();
4076
+ setTimeout(() => URL.revokeObjectURL(a.href), 1000);
4077
+ }
4078
+
4079
+ $.save = (a, b, c) => {
4080
+ if (!a || (typeof a == 'string' && (!b || (!c && b.length < 5)))) {
4081
+ c = b;
4082
+ b = a;
4083
+ a = $.canvas;
4084
+ }
4085
+ if (c) saveFile(a, b, c);
4086
+ else if (b) {
4087
+ let lastDot = b.lastIndexOf('.');
4088
+ saveFile(a, b.slice(0, lastDot), b.slice(lastDot + 1));
4089
+ } else saveFile(a);
4090
+ };
4091
+
4060
4092
  $.CSV = {};
4061
4093
  $.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
4062
4094
  if (!csv.length) return [];
@@ -4390,11 +4422,6 @@ Q5.Vector.sub = (v, u) => v.copy().sub(u);
4390
4422
  for (let k of ['fromAngle', 'fromAngles', 'random2D', 'random3D']) {
4391
4423
  Q5.Vector[k] = (u, v, t) => new Q5.Vector()[k](u, v, t);
4392
4424
  }
4393
- /**
4394
- * q5-webgpu
4395
- *
4396
- * EXPERIMENTAL, for developer testing only!
4397
- */
4398
4425
  Q5.renderers.webgpu = {};
4399
4426
 
4400
4427
  Q5.renderers.webgpu.canvas = ($, q) => {
@@ -4410,6 +4437,11 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4410
4437
 
4411
4438
  let pass,
4412
4439
  mainView,
4440
+ frameTextureA,
4441
+ frameTextureB,
4442
+ frameSampler,
4443
+ framePipeline,
4444
+ frameBindGroup,
4413
4445
  colorIndex = 1,
4414
4446
  colorStackIndex = 8;
4415
4447
 
@@ -4458,14 +4490,60 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4458
4490
  });
4459
4491
 
4460
4492
  let createMainView = () => {
4493
+ let w = $.canvas.width,
4494
+ h = $.canvas.height,
4495
+ size = [w, h],
4496
+ format = 'bgra8unorm';
4497
+
4461
4498
  mainView = Q5.device
4462
4499
  .createTexture({
4463
- size: [$.canvas.width, $.canvas.height],
4500
+ size,
4464
4501
  sampleCount: 4,
4465
- format: 'bgra8unorm',
4502
+ format,
4466
4503
  usage: GPUTextureUsage.RENDER_ATTACHMENT
4467
4504
  })
4468
4505
  .createView();
4506
+
4507
+ let usage =
4508
+ GPUTextureUsage.COPY_SRC |
4509
+ GPUTextureUsage.COPY_DST |
4510
+ GPUTextureUsage.TEXTURE_BINDING |
4511
+ GPUTextureUsage.RENDER_ATTACHMENT;
4512
+
4513
+ frameTextureA = Q5.device.createTexture({ size, format, usage });
4514
+ frameTextureB = Q5.device.createTexture({ size, format, usage });
4515
+
4516
+ let finalShader = Q5.device.createShaderModule({
4517
+ code: `
4518
+ @vertex fn v(@builtin(vertex_index)i:u32)->@builtin(position)vec4<f32>{
4519
+ const pos=array(vec2(-1f,-1f),vec2(1f,-1f),vec2(-1f,1f),vec2(1f,1f));
4520
+ return vec4(pos[i],0f,1f);
4521
+ }
4522
+ @group(0) @binding(0) var s: sampler;
4523
+ @group(0) @binding(1) var t: texture_2d<f32>;
4524
+ @fragment fn f(@builtin(position)c:vec4<f32>)->@location(0)vec4<f32>{
4525
+ let uv=c.xy/vec2(${w}, ${h});
4526
+ return textureSample(t,s,uv);
4527
+ }`
4528
+ });
4529
+
4530
+ frameSampler = Q5.device.createSampler({
4531
+ magFilter: 'linear',
4532
+ minFilter: 'linear'
4533
+ });
4534
+
4535
+ // Create a pipeline for rendering
4536
+ framePipeline = Q5.device.createRenderPipeline({
4537
+ layout: 'auto',
4538
+ vertex: { module: finalShader, entryPoint: 'v' },
4539
+ fragment: {
4540
+ module: finalShader,
4541
+ entryPoint: 'f',
4542
+ targets: [{ format, writeMask: GPUColorWrite.ALL }]
4543
+ },
4544
+ primitive: { topology: 'triangle-strip' },
4545
+ multisample: { count: 4 }
4546
+ });
4469
4547
  };
4470
4548
 
4471
4549
  $._createCanvas = (w, h, opt) => {
@@ -4831,23 +4909,52 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4831
4909
  }
4832
4910
  };
4833
4911
 
4834
- $.clear = () => {};
4912
+ let shouldClear = false;
4913
+ $.clear = () => {
4914
+ shouldClear = true;
4915
+ };
4916
+
4917
+ const _drawFrame = () => {
4918
+ pass.setPipeline(framePipeline);
4919
+ pass.setBindGroup(0, frameBindGroup);
4920
+ pass.draw(4);
4921
+ };
4835
4922
 
4836
4923
  $._beginRender = () => {
4924
+ // swap the frame textures
4925
+ const temp = frameTextureA;
4926
+ frameTextureA = frameTextureB;
4927
+ frameTextureB = temp;
4928
+ $.canvas.texture = frameTextureA;
4929
+
4837
4930
  $.encoder = Q5.device.createCommandEncoder();
4838
4931
 
4932
+ let target = shouldClear ? $.ctx.getCurrentTexture().createView() : frameTextureA.createView();
4933
+
4839
4934
  pass = q.pass = $.encoder.beginRenderPass({
4840
4935
  label: 'q5-webgpu',
4841
4936
  colorAttachments: [
4842
4937
  {
4843
4938
  view: mainView,
4844
- resolveTarget: $.ctx.getCurrentTexture().createView(),
4939
+ resolveTarget: target,
4845
4940
  loadOp: 'clear',
4846
4941
  storeOp: 'store',
4847
4942
  clearValue: [0, 0, 0, 0]
4848
4943
  }
4849
4944
  ]
4850
4945
  });
4946
+
4947
+ if (!shouldClear) {
4948
+ frameBindGroup = Q5.device.createBindGroup({
4949
+ layout: framePipeline.getBindGroupLayout(0),
4950
+ entries: [
4951
+ { binding: 0, resource: frameSampler },
4952
+ { binding: 1, resource: frameTextureB.createView() }
4953
+ ]
4954
+ });
4955
+
4956
+ _drawFrame();
4957
+ }
4851
4958
  };
4852
4959
 
4853
4960
  $._render = () => {
@@ -4922,8 +5029,33 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4922
5029
 
4923
5030
  $._finishRender = () => {
4924
5031
  pass.end();
4925
- let commandBuffer = $.encoder.finish();
4926
- Q5.device.queue.submit([commandBuffer]);
5032
+
5033
+ if (!shouldClear) {
5034
+ pass = $.encoder.beginRenderPass({
5035
+ colorAttachments: [
5036
+ {
5037
+ view: mainView,
5038
+ resolveTarget: $.ctx.getCurrentTexture().createView(),
5039
+ loadOp: 'clear',
5040
+ storeOp: 'store',
5041
+ clearValue: [0, 0, 0, 0]
5042
+ }
5043
+ ]
5044
+ });
5045
+
5046
+ frameBindGroup = Q5.device.createBindGroup({
5047
+ layout: framePipeline.getBindGroupLayout(0),
5048
+ entries: [
5049
+ { binding: 0, resource: frameSampler },
5050
+ { binding: 1, resource: frameTextureA.createView() }
5051
+ ]
5052
+ });
5053
+ _drawFrame();
5054
+ pass.end();
5055
+ shouldClear = false;
5056
+ }
5057
+
5058
+ Q5.device.queue.submit([$.encoder.finish()]);
4927
5059
 
4928
5060
  q.pass = $.encoder = null;
4929
5061
 
@@ -5718,12 +5850,10 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5718
5850
 
5719
5851
  $.smooth();
5720
5852
 
5721
- let MAX_TEXTURES = 12000;
5722
-
5723
- $._textures = [];
5724
5853
  let tIdx = 0;
5725
5854
 
5726
5855
  $._createTexture = (img) => {
5856
+ let g = img;
5727
5857
  if (img.canvas) img = img.canvas;
5728
5858
 
5729
5859
  let textureSize = [img.width, img.height, 1];
@@ -5743,26 +5873,18 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5743
5873
  textureSize
5744
5874
  );
5745
5875
 
5746
- $._textures[tIdx] = texture;
5747
- img.textureIndex = tIdx;
5876
+ g.texture = texture;
5877
+ g.textureIndex = tIdx;
5748
5878
 
5749
- const textureBindGroup = Q5.device.createBindGroup({
5879
+ $._textureBindGroups[tIdx] = Q5.device.createBindGroup({
5750
5880
  layout: textureLayout,
5751
5881
  entries: [
5752
5882
  { binding: 0, resource: sampler },
5753
5883
  { binding: 1, resource: texture.createView() }
5754
5884
  ]
5755
5885
  });
5756
- $._textureBindGroups[tIdx] = textureBindGroup;
5757
-
5758
- tIdx = (tIdx + 1) % MAX_TEXTURES;
5759
5886
 
5760
- // if the texture array is full, destroy the oldest texture
5761
- if ($._textures[tIdx]) {
5762
- $._textures[tIdx].destroy();
5763
- delete $._textures[tIdx];
5764
- delete $._textureBindGroups[tIdx];
5765
- }
5887
+ tIdx++;
5766
5888
  };
5767
5889
 
5768
5890
  $.loadImage = (src, cb) => {
@@ -5793,23 +5915,31 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5793
5915
  };
5794
5916
 
5795
5917
  $.image = (img, dx = 0, dy = 0, dw, dh, sx = 0, sy = 0, sw, sh) => {
5796
- let g = img;
5797
- if (img.canvas) img = img.canvas;
5798
5918
  if (img.textureIndex == undefined) return;
5799
5919
 
5920
+ let cnv = img.canvas || img;
5921
+
5800
5922
  if ($._matrixDirty) $._saveMatrix();
5801
5923
 
5802
- let w = img.width,
5803
- h = img.height,
5804
- pd = g._pixelDensity || 1;
5924
+ let w = cnv.width,
5925
+ h = cnv.height,
5926
+ pd = img._pixelDensity || 1;
5805
5927
 
5806
- dw ??= g.defaultWidth;
5807
- dh ??= g.defaultHeight;
5928
+ if (img.modified) {
5929
+ Q5.device.queue.copyExternalImageToTexture(
5930
+ { source: cnv },
5931
+ { texture: img.texture, colorSpace: $.canvas.colorSpace },
5932
+ [w, h, 1]
5933
+ );
5934
+ img.modified = false;
5935
+ }
5936
+
5937
+ dw ??= img.defaultWidth;
5938
+ dh ??= img.defaultHeight;
5808
5939
  sw ??= w;
5809
5940
  sh ??= h;
5810
-
5811
- dw *= pd;
5812
- dh *= pd;
5941
+ sx *= pd;
5942
+ sy *= pd;
5813
5943
 
5814
5944
  let [l, r, t, b] = $._calcBox(dx, dy, dw, dh, $._imageMode);
5815
5945
 
@@ -5829,8 +5959,55 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5829
5959
  $.drawStack.push(1, img.textureIndex);
5830
5960
  };
5831
5961
 
5962
+ $._saveCanvas = async (data, ext) => {
5963
+ let texture = data.texture,
5964
+ w = texture.width,
5965
+ h = texture.height,
5966
+ bytesPerRow = Math.ceil((w * 4) / 256) * 256;
5967
+
5968
+ let buffer = Q5.device.createBuffer({
5969
+ size: bytesPerRow * h,
5970
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
5971
+ });
5972
+
5973
+ let en = Q5.device.createCommandEncoder();
5974
+
5975
+ en.copyTextureToBuffer({ texture }, { buffer, bytesPerRow, rowsPerImage: h }, { width: w, height: h });
5976
+
5977
+ Q5.device.queue.submit([en.finish()]);
5978
+
5979
+ await buffer.mapAsync(GPUMapMode.READ);
5980
+
5981
+ let pad = new Uint8Array(buffer.getMappedRange());
5982
+ data = new Uint8Array(w * h * 4); // unpadded data
5983
+
5984
+ // Remove padding from each row
5985
+ for (let y = 0; y < h; y++) {
5986
+ const p = y * bytesPerRow; // padded row offset
5987
+ const u = y * w * 4; // unpadded row offset
5988
+ data.set(pad.subarray(p, p + w * 4), u);
5989
+ }
5990
+
5991
+ buffer.unmap();
5992
+
5993
+ let colorSpace = $.canvas.colorSpace;
5994
+ data = new Uint8ClampedArray(data.buffer);
5995
+ data = new ImageData(data, w, h, { colorSpace });
5996
+ let cnv = new OffscreenCanvas(w, h);
5997
+ let ctx = cnv.getContext('2d', { colorSpace });
5998
+ ctx.putImageData(data, 0, 0);
5999
+
6000
+ // Convert to blob then data URL
6001
+ let blob = await cnv.convertToBlob({ type: 'image/' + ext });
6002
+ return await new Promise((resolve) => {
6003
+ let r = new FileReader();
6004
+ r.onloadend = () => resolve(r.result);
6005
+ r.readAsDataURL(blob);
6006
+ });
6007
+ };
6008
+
5832
6009
  $._hooks.preRender.push(() => {
5833
- if (!$._textureBindGroups.length) return;
6010
+ if (!vertIndex) return;
5834
6011
 
5835
6012
  // Switch to image pipeline
5836
6013
  $.pass.setPipeline($._pipelines[1]);
@@ -6371,20 +6548,14 @@ fn fragmentMain(f : FragmentParams) -> @location(0) vec4f {
6371
6548
  }
6372
6549
 
6373
6550
  let img = $._g.createTextImage(str, w, h);
6374
-
6375
- if (img.canvas.textureIndex == undefined) {
6551
+ if (img.textureIndex == undefined) {
6376
6552
  $._createTexture(img);
6377
- } else if (img.modified) {
6378
- let cnv = img.canvas;
6379
- let textureSize = [cnv.width, cnv.height, 1];
6380
- let texture = $._textures[cnv.textureIndex];
6381
-
6382
- Q5.device.queue.copyExternalImageToTexture(
6383
- { source: cnv },
6384
- { texture, colorSpace: $.canvas.colorSpace },
6385
- textureSize
6386
- );
6387
- img.modified = false;
6553
+ let _copy = img.copy;
6554
+ img.copy = function () {
6555
+ let copy = _copy();
6556
+ $._createTexture(copy);
6557
+ return copy;
6558
+ };
6388
6559
  }
6389
6560
  return img;
6390
6561
  };