q5 2.18.3 → 2.19.1

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.
Files changed (5) hide show
  1. package/deno.json +1 -1
  2. package/package.json +1 -1
  3. package/q5.d.ts +194 -49
  4. package/q5.js +284 -128
  5. package/q5.min.js +2 -2
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;
@@ -313,7 +313,7 @@ function createCanvas(w, h, opt) {
313
313
  }
314
314
  }
315
315
 
316
- Q5.version = Q5.VERSION = '2.18';
316
+ Q5.version = Q5.VERSION = '2.19';
317
317
 
318
318
  if (typeof document == 'object') {
319
319
  document.addEventListener('DOMContentLoaded', () => {
@@ -422,49 +422,6 @@ Q5.modules.canvas = ($, q) => {
422
422
  return g;
423
423
  };
424
424
 
425
- async function saveFile(data, name, ext) {
426
- name = name || 'untitled';
427
- ext = ext || 'png';
428
- if (ext == 'jpg' || ext == 'png' || ext == 'webp') {
429
- if (data instanceof OffscreenCanvas) {
430
- const blob = await data.convertToBlob({ type: 'image/' + ext });
431
- data = await new Promise((resolve) => {
432
- const reader = new FileReader();
433
- reader.onloadend = () => resolve(reader.result);
434
- reader.readAsDataURL(blob);
435
- });
436
- } else {
437
- data = data.toDataURL('image/' + ext);
438
- }
439
- } else {
440
- let type = 'text/plain';
441
- if (ext == 'json') {
442
- if (typeof data != 'string') data = JSON.stringify(data);
443
- type = 'text/json';
444
- }
445
- data = new Blob([data], { type });
446
- data = URL.createObjectURL(data);
447
- }
448
- let a = document.createElement('a');
449
- a.href = data;
450
- a.download = name + '.' + ext;
451
- a.click();
452
- URL.revokeObjectURL(a.href);
453
- }
454
-
455
- $.save = (a, b, c) => {
456
- if (!a || (typeof a == 'string' && (!b || (!c && b.length < 5)))) {
457
- c = b;
458
- b = a;
459
- a = $.canvas;
460
- }
461
- if (c) return saveFile(a, b, c);
462
- if (b) {
463
- b = b.split('.');
464
- saveFile(a, b[0], b.at(-1));
465
- } else saveFile(a);
466
- };
467
-
468
425
  $._setCanvasSize = (w, h) => {
469
426
  if (!w) h ??= window.innerHeight;
470
427
  else h ??= w;
@@ -549,10 +506,7 @@ Q5.modules.canvas = ($, q) => {
549
506
  $._resizeCanvas(w, h);
550
507
  };
551
508
 
552
- if (c && !Q5._createServerCanvas) {
553
- c.resize = $.resizeCanvas;
554
- c.save = $.saveCanvas = $.save;
555
- }
509
+ if (c && !Q5._createServerCanvas) c.resize = $.resizeCanvas;
556
510
 
557
511
  $.pixelDensity = (v) => {
558
512
  if (!v || v == $._pixelDensity) return $._pixelDensity;
@@ -1324,10 +1278,7 @@ Q5.renderers.c2d.image = ($, q) => {
1324
1278
  opt ??= {};
1325
1279
  opt.alpha ??= true;
1326
1280
  opt.colorSpace ??= $.canvas.colorSpace || Q5.canvasOptions.colorSpace;
1327
- let img = new Q5.Image(w, h, opt);
1328
- img.defaultWidth = w * $._defaultImageScale;
1329
- img.defaultHeight = h * $._defaultImageScale;
1330
- return img;
1281
+ return new Q5.Image(w, h, opt);
1331
1282
  };
1332
1283
 
1333
1284
  $.loadImage = function (url, cb, opt) {
@@ -1483,7 +1434,7 @@ Q5.renderers.c2d.image = ($, q) => {
1483
1434
  $.ctx.globalCompositeOperation = 'source-over';
1484
1435
  $.ctx.drawImage($.canvas, 0, 0, $.canvas.w, $.canvas.h);
1485
1436
  $.ctx.restore();
1486
- $._retint = true;
1437
+ $.modified = $._retint = true;
1487
1438
  };
1488
1439
 
1489
1440
  if ($._scope == 'image') {
@@ -1501,7 +1452,7 @@ Q5.renderers.c2d.image = ($, q) => {
1501
1452
  $.ctx.clearRect(0, 0, c.width, c.height);
1502
1453
  $.ctx.drawImage(o, 0, 0, c.width, c.height);
1503
1454
 
1504
- $._retint = true;
1455
+ $.modified = $._retint = true;
1505
1456
  };
1506
1457
  }
1507
1458
 
@@ -1548,17 +1499,25 @@ Q5.renderers.c2d.image = ($, q) => {
1548
1499
  $.ctx.globalCompositeOperation = old;
1549
1500
  $.ctx.restore();
1550
1501
 
1551
- $._retint = true;
1502
+ $.modified = $._retint = true;
1552
1503
  };
1553
1504
 
1554
1505
  $.inset = (x, y, w, h, dx, dy, dw, dh) => {
1555
1506
  let pd = $._pixelDensity || 1;
1556
1507
  $.ctx.drawImage($.canvas, x * pd, y * pd, w * pd, h * pd, dx, dy, dw, dh);
1557
1508
 
1558
- $._retint = true;
1509
+ $.modified = $._retint = true;
1559
1510
  };
1560
1511
 
1561
- $.copy = () => $.get();
1512
+ $.copy = () => {
1513
+ let img = $.get();
1514
+ for (let prop in $) {
1515
+ if (typeof $[prop] != 'function' && !/(canvas|ctx|texture|textureIndex)/.test(prop)) {
1516
+ img[prop] = $[prop];
1517
+ }
1518
+ }
1519
+ return img;
1520
+ };
1562
1521
 
1563
1522
  $.get = (x, y, w, h) => {
1564
1523
  let pd = $._pixelDensity || 1;
@@ -1568,22 +1527,19 @@ Q5.renderers.c2d.image = ($, q) => {
1568
1527
  }
1569
1528
  x = Math.floor(x || 0) * pd;
1570
1529
  y = Math.floor(y || 0) * pd;
1571
- let _w = (w = w || $.width);
1572
- let _h = (h = h || $.height);
1573
- w *= pd;
1574
- h *= pd;
1575
- let img = $.createImage(w, h);
1576
- img.ctx.drawImage($.canvas, x, y, w, h, 0, 0, w, h);
1577
- img._pixelDensity = pd;
1578
- img.width = _w;
1579
- img.height = _h;
1530
+ w ??= $.width;
1531
+ h ??= $.height;
1532
+ let img = $.createImage(w, h, { pixelDensity: pd });
1533
+ img.ctx.drawImage($.canvas, x, y, w * pd, h * pd, 0, 0, w, h);
1534
+ img.width = w;
1535
+ img.height = h;
1580
1536
  return img;
1581
1537
  };
1582
1538
 
1583
1539
  $.set = (x, y, c) => {
1584
1540
  x = Math.floor(x);
1585
1541
  y = Math.floor(y);
1586
- $._retint = true;
1542
+ $.modified = $._retint = true;
1587
1543
  if (c.canvas) {
1588
1544
  let old = $._tint;
1589
1545
  $._tint = null;
@@ -1611,7 +1567,7 @@ Q5.renderers.c2d.image = ($, q) => {
1611
1567
  $.updatePixels = () => {
1612
1568
  if (imgData != null) {
1613
1569
  $.ctx.putImageData(imgData, 0, 0);
1614
- $._retint = true;
1570
+ $.modified = $._retint = true;
1615
1571
  }
1616
1572
  };
1617
1573
 
@@ -1620,6 +1576,20 @@ Q5.renderers.c2d.image = ($, q) => {
1620
1576
 
1621
1577
  if ($._scope == 'image') return;
1622
1578
 
1579
+ $._saveCanvas = async (data, ext) => {
1580
+ data = data.canvas || data;
1581
+ if (data instanceof OffscreenCanvas) {
1582
+ const blob = await data.convertToBlob({ type: 'image/' + ext });
1583
+
1584
+ return await new Promise((resolve) => {
1585
+ const reader = new FileReader();
1586
+ reader.onloadend = () => resolve(reader.result);
1587
+ reader.readAsDataURL(blob);
1588
+ });
1589
+ }
1590
+ return data.toDataURL('image/' + ext);
1591
+ };
1592
+
1623
1593
  $.tint = function (c) {
1624
1594
  $._tint = (c._q5Color ? c : $.color(...arguments)).toString();
1625
1595
  };
@@ -1963,10 +1933,15 @@ Q5.renderers.c2d.text = ($, q) => {
1963
1933
  tY = leading * lines.length;
1964
1934
 
1965
1935
  if (!img) {
1936
+ let ogBaseline = $.ctx.textBaseline;
1937
+ $.ctx.textBaseline = 'alphabetic';
1938
+
1966
1939
  let measure = ctx.measureText(' ');
1967
1940
  let ascent = measure.fontBoundingBoxAscent;
1968
1941
  let descent = measure.fontBoundingBoxDescent;
1969
1942
 
1943
+ $.ctx.textBaseline = ogBaseline;
1944
+
1970
1945
  img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(tY + descent), {
1971
1946
  pixelDensity: $._pixelDensity
1972
1947
  });
@@ -1977,12 +1952,13 @@ Q5.renderers.c2d.text = ($, q) => {
1977
1952
  img._middle = img._top + ascent * 0.5;
1978
1953
  img._bottom = img._top + ascent;
1979
1954
  img._leading = leading;
1955
+ } else {
1956
+ img.modified = true;
1980
1957
  }
1981
1958
 
1982
1959
  img._fill = $._fill;
1983
1960
  img._stroke = $._stroke;
1984
1961
  img._strokeWeight = $._strokeWeight;
1985
- img.modified = true;
1986
1962
 
1987
1963
  ctx = img.ctx;
1988
1964
 
@@ -2913,7 +2889,7 @@ Q5.modules.input = ($, q) => {
2913
2889
  $._onwheel = (e) => {
2914
2890
  $._updateMouse(e);
2915
2891
  e.delta = e.deltaY;
2916
- if ($.mouseWheel(e) == false) e.preventDefault();
2892
+ if ($.mouseWheel(e) == false || $._noScroll) e.preventDefault();
2917
2893
  };
2918
2894
 
2919
2895
  $.cursor = (name, x, y) => {
@@ -2928,9 +2904,8 @@ Q5.modules.input = ($, q) => {
2928
2904
  $.canvas.style.cursor = name + pfx;
2929
2905
  };
2930
2906
 
2931
- $.noCursor = () => {
2932
- $.canvas.style.cursor = 'none';
2933
- };
2907
+ $.noCursor = () => ($.canvas.style.cursor = 'none');
2908
+ $.noScroll = () => ($._noScroll = true);
2934
2909
 
2935
2910
  if (window) {
2936
2911
  $.lockMouse = document.body?.requestPointerLock;
@@ -4044,6 +4019,10 @@ Q5.modules.util = ($, q) => {
4044
4019
  $.loadJSON = (url, cb) => $._loadFile(url, cb, 'json');
4045
4020
  $.loadCSV = (url, cb) => $._loadFile(url, cb, 'csv');
4046
4021
 
4022
+ const imgRegex = /(jpe?g|png|gif|webp|avif|svg)/,
4023
+ fontRegex = /(ttf|otf|woff2?|eot|json)/,
4024
+ audioRegex = /(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/;
4025
+
4047
4026
  $.load = function (...urls) {
4048
4027
  if (Array.isArray(urls[0])) urls = urls[0];
4049
4028
 
@@ -4057,11 +4036,11 @@ Q5.modules.util = ($, q) => {
4057
4036
  obj = $.loadJSON(url);
4058
4037
  } else if (ext == 'csv') {
4059
4038
  obj = $.loadCSV(url);
4060
- } else if (/(jpe?g|png|gif|webp|avif|svg)/.test(ext)) {
4039
+ } else if (imgRegex.test(ext)) {
4061
4040
  obj = $.loadImage(url);
4062
- } else if (/(ttf|otf|woff2?|eot|json)/i.test(ext)) {
4041
+ } else if (fontRegex.test(ext)) {
4063
4042
  obj = $.loadFont(url);
4064
- } else if (/(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/.test(ext)) {
4043
+ } else if (audioRegex.test(ext)) {
4065
4044
  obj = $.loadSound(url);
4066
4045
  } else {
4067
4046
  obj = $.loadText(url);
@@ -4073,6 +4052,40 @@ Q5.modules.util = ($, q) => {
4073
4052
  return Promise.all(loaders);
4074
4053
  };
4075
4054
 
4055
+ async function saveFile(data, name, ext) {
4056
+ name = name || 'untitled';
4057
+ ext = ext || 'png';
4058
+ if (imgRegex.test(ext)) {
4059
+ data = await $._saveCanvas(data, ext);
4060
+ } else {
4061
+ let type = 'text/plain';
4062
+ if (ext == 'json') {
4063
+ if (typeof data != 'string') data = JSON.stringify(data);
4064
+ type = 'text/json';
4065
+ }
4066
+ data = new Blob([data], { type });
4067
+ data = URL.createObjectURL(data);
4068
+ }
4069
+ let a = document.createElement('a');
4070
+ a.href = data;
4071
+ a.download = name + '.' + ext;
4072
+ a.click();
4073
+ setTimeout(() => URL.revokeObjectURL(a.href), 1000);
4074
+ }
4075
+
4076
+ $.save = (a, b, c) => {
4077
+ if (!a || (typeof a == 'string' && (!b || (!c && b.length < 5)))) {
4078
+ c = b;
4079
+ b = a;
4080
+ a = $.canvas;
4081
+ }
4082
+ if (c) saveFile(a, b, c);
4083
+ else if (b) {
4084
+ let lastDot = b.lastIndexOf('.');
4085
+ saveFile(a, b.slice(0, lastDot), b.slice(lastDot + 1));
4086
+ } else saveFile(a);
4087
+ };
4088
+
4076
4089
  $.CSV = {};
4077
4090
  $.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
4078
4091
  if (!csv.length) return [];
@@ -4088,6 +4101,10 @@ Q5.modules.util = ($, q) => {
4088
4101
  return a;
4089
4102
  };
4090
4103
 
4104
+ if ($.canvas && !Q5._createServerCanvas) {
4105
+ $.canvas.save = $.saveCanvas = $.save;
4106
+ }
4107
+
4091
4108
  if (typeof localStorage == 'object') {
4092
4109
  $.storeItem = localStorage.setItem;
4093
4110
  $.getItem = localStorage.getItem;
@@ -4406,11 +4423,6 @@ Q5.Vector.sub = (v, u) => v.copy().sub(u);
4406
4423
  for (let k of ['fromAngle', 'fromAngles', 'random2D', 'random3D']) {
4407
4424
  Q5.Vector[k] = (u, v, t) => new Q5.Vector()[k](u, v, t);
4408
4425
  }
4409
- /**
4410
- * q5-webgpu
4411
- *
4412
- * EXPERIMENTAL, for developer testing only!
4413
- */
4414
4426
  Q5.renderers.webgpu = {};
4415
4427
 
4416
4428
  Q5.renderers.webgpu.canvas = ($, q) => {
@@ -4426,6 +4438,11 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4426
4438
 
4427
4439
  let pass,
4428
4440
  mainView,
4441
+ frameTextureA,
4442
+ frameTextureB,
4443
+ frameSampler,
4444
+ framePipeline,
4445
+ frameBindGroup,
4429
4446
  colorIndex = 1,
4430
4447
  colorStackIndex = 8;
4431
4448
 
@@ -4474,14 +4491,60 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4474
4491
  });
4475
4492
 
4476
4493
  let createMainView = () => {
4494
+ let w = $.canvas.width,
4495
+ h = $.canvas.height,
4496
+ size = [w, h],
4497
+ format = 'bgra8unorm';
4498
+
4477
4499
  mainView = Q5.device
4478
4500
  .createTexture({
4479
- size: [$.canvas.width, $.canvas.height],
4501
+ size,
4480
4502
  sampleCount: 4,
4481
- format: 'bgra8unorm',
4503
+ format,
4482
4504
  usage: GPUTextureUsage.RENDER_ATTACHMENT
4483
4505
  })
4484
4506
  .createView();
4507
+
4508
+ let usage =
4509
+ GPUTextureUsage.COPY_SRC |
4510
+ GPUTextureUsage.COPY_DST |
4511
+ GPUTextureUsage.TEXTURE_BINDING |
4512
+ GPUTextureUsage.RENDER_ATTACHMENT;
4513
+
4514
+ frameTextureA = Q5.device.createTexture({ size, format, usage });
4515
+ frameTextureB = Q5.device.createTexture({ size, format, usage });
4516
+
4517
+ let finalShader = Q5.device.createShaderModule({
4518
+ code: `
4519
+ @vertex fn v(@builtin(vertex_index)i:u32)->@builtin(position)vec4<f32>{
4520
+ const pos=array(vec2(-1f,-1f),vec2(1f,-1f),vec2(-1f,1f),vec2(1f,1f));
4521
+ return vec4(pos[i],0f,1f);
4522
+ }
4523
+ @group(0) @binding(0) var s: sampler;
4524
+ @group(0) @binding(1) var t: texture_2d<f32>;
4525
+ @fragment fn f(@builtin(position)c:vec4<f32>)->@location(0)vec4<f32>{
4526
+ let uv=c.xy/vec2(${w}, ${h});
4527
+ return textureSample(t,s,uv);
4528
+ }`
4529
+ });
4530
+
4531
+ frameSampler = Q5.device.createSampler({
4532
+ magFilter: 'linear',
4533
+ minFilter: 'linear'
4534
+ });
4535
+
4536
+ // Create a pipeline for rendering
4537
+ framePipeline = Q5.device.createRenderPipeline({
4538
+ layout: 'auto',
4539
+ vertex: { module: finalShader, entryPoint: 'v' },
4540
+ fragment: {
4541
+ module: finalShader,
4542
+ entryPoint: 'f',
4543
+ targets: [{ format, writeMask: GPUColorWrite.ALL }]
4544
+ },
4545
+ primitive: { topology: 'triangle-strip' },
4546
+ multisample: { count: 4 }
4547
+ });
4485
4548
  };
4486
4549
 
4487
4550
  $._createCanvas = (w, h, opt) => {
@@ -4847,23 +4910,52 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4847
4910
  }
4848
4911
  };
4849
4912
 
4850
- $.clear = () => {};
4913
+ let shouldClear = false;
4914
+ $.clear = () => {
4915
+ shouldClear = true;
4916
+ };
4917
+
4918
+ const _drawFrame = () => {
4919
+ pass.setPipeline(framePipeline);
4920
+ pass.setBindGroup(0, frameBindGroup);
4921
+ pass.draw(4);
4922
+ };
4851
4923
 
4852
4924
  $._beginRender = () => {
4925
+ // swap the frame textures
4926
+ const temp = frameTextureA;
4927
+ frameTextureA = frameTextureB;
4928
+ frameTextureB = temp;
4929
+ $.canvas.texture = frameTextureA;
4930
+
4853
4931
  $.encoder = Q5.device.createCommandEncoder();
4854
4932
 
4933
+ let target = shouldClear ? $.ctx.getCurrentTexture().createView() : frameTextureA.createView();
4934
+
4855
4935
  pass = q.pass = $.encoder.beginRenderPass({
4856
4936
  label: 'q5-webgpu',
4857
4937
  colorAttachments: [
4858
4938
  {
4859
4939
  view: mainView,
4860
- resolveTarget: $.ctx.getCurrentTexture().createView(),
4940
+ resolveTarget: target,
4861
4941
  loadOp: 'clear',
4862
4942
  storeOp: 'store',
4863
4943
  clearValue: [0, 0, 0, 0]
4864
4944
  }
4865
4945
  ]
4866
4946
  });
4947
+
4948
+ if (!shouldClear) {
4949
+ frameBindGroup = Q5.device.createBindGroup({
4950
+ layout: framePipeline.getBindGroupLayout(0),
4951
+ entries: [
4952
+ { binding: 0, resource: frameSampler },
4953
+ { binding: 1, resource: frameTextureB.createView() }
4954
+ ]
4955
+ });
4956
+
4957
+ _drawFrame();
4958
+ }
4867
4959
  };
4868
4960
 
4869
4961
  $._render = () => {
@@ -4938,8 +5030,33 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4938
5030
 
4939
5031
  $._finishRender = () => {
4940
5032
  pass.end();
4941
- let commandBuffer = $.encoder.finish();
4942
- Q5.device.queue.submit([commandBuffer]);
5033
+
5034
+ if (!shouldClear) {
5035
+ pass = $.encoder.beginRenderPass({
5036
+ colorAttachments: [
5037
+ {
5038
+ view: mainView,
5039
+ resolveTarget: $.ctx.getCurrentTexture().createView(),
5040
+ loadOp: 'clear',
5041
+ storeOp: 'store',
5042
+ clearValue: [0, 0, 0, 0]
5043
+ }
5044
+ ]
5045
+ });
5046
+
5047
+ frameBindGroup = Q5.device.createBindGroup({
5048
+ layout: framePipeline.getBindGroupLayout(0),
5049
+ entries: [
5050
+ { binding: 0, resource: frameSampler },
5051
+ { binding: 1, resource: frameTextureA.createView() }
5052
+ ]
5053
+ });
5054
+ _drawFrame();
5055
+ pass.end();
5056
+ shouldClear = false;
5057
+ }
5058
+
5059
+ Q5.device.queue.submit([$.encoder.finish()]);
4943
5060
 
4944
5061
  q.pass = $.encoder = null;
4945
5062
 
@@ -5734,12 +5851,10 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5734
5851
 
5735
5852
  $.smooth();
5736
5853
 
5737
- let MAX_TEXTURES = 12000;
5738
-
5739
- $._textures = [];
5740
5854
  let tIdx = 0;
5741
5855
 
5742
5856
  $._createTexture = (img) => {
5857
+ let g = img;
5743
5858
  if (img.canvas) img = img.canvas;
5744
5859
 
5745
5860
  let textureSize = [img.width, img.height, 1];
@@ -5759,26 +5874,18 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5759
5874
  textureSize
5760
5875
  );
5761
5876
 
5762
- $._textures[tIdx] = texture;
5763
- img.textureIndex = tIdx;
5877
+ g.texture = texture;
5878
+ g.textureIndex = tIdx;
5764
5879
 
5765
- const textureBindGroup = Q5.device.createBindGroup({
5880
+ $._textureBindGroups[tIdx] = Q5.device.createBindGroup({
5766
5881
  layout: textureLayout,
5767
5882
  entries: [
5768
5883
  { binding: 0, resource: sampler },
5769
5884
  { binding: 1, resource: texture.createView() }
5770
5885
  ]
5771
5886
  });
5772
- $._textureBindGroups[tIdx] = textureBindGroup;
5773
-
5774
- tIdx = (tIdx + 1) % MAX_TEXTURES;
5775
5887
 
5776
- // if the texture array is full, destroy the oldest texture
5777
- if ($._textures[tIdx]) {
5778
- $._textures[tIdx].destroy();
5779
- delete $._textures[tIdx];
5780
- delete $._textureBindGroups[tIdx];
5781
- }
5888
+ tIdx++;
5782
5889
  };
5783
5890
 
5784
5891
  $.loadImage = (src, cb) => {
@@ -5809,23 +5916,31 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5809
5916
  };
5810
5917
 
5811
5918
  $.image = (img, dx = 0, dy = 0, dw, dh, sx = 0, sy = 0, sw, sh) => {
5812
- let g = img;
5813
- if (img.canvas) img = img.canvas;
5814
5919
  if (img.textureIndex == undefined) return;
5815
5920
 
5921
+ let cnv = img.canvas || img;
5922
+
5816
5923
  if ($._matrixDirty) $._saveMatrix();
5817
5924
 
5818
- let w = img.width,
5819
- h = img.height,
5820
- pd = g._pixelDensity || 1;
5925
+ let w = cnv.width,
5926
+ h = cnv.height,
5927
+ pd = img._pixelDensity || 1;
5821
5928
 
5822
- dw ??= g.defaultWidth;
5823
- dh ??= g.defaultHeight;
5929
+ if (img.modified) {
5930
+ Q5.device.queue.copyExternalImageToTexture(
5931
+ { source: cnv },
5932
+ { texture: img.texture, colorSpace: $.canvas.colorSpace },
5933
+ [w, h, 1]
5934
+ );
5935
+ img.modified = false;
5936
+ }
5937
+
5938
+ dw ??= img.defaultWidth;
5939
+ dh ??= img.defaultHeight;
5824
5940
  sw ??= w;
5825
5941
  sh ??= h;
5826
-
5827
- dw *= pd;
5828
- dh *= pd;
5942
+ sx *= pd;
5943
+ sy *= pd;
5829
5944
 
5830
5945
  let [l, r, t, b] = $._calcBox(dx, dy, dw, dh, $._imageMode);
5831
5946
 
@@ -5845,8 +5960,55 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5845
5960
  $.drawStack.push(1, img.textureIndex);
5846
5961
  };
5847
5962
 
5963
+ $._saveCanvas = async (data, ext) => {
5964
+ let texture = data.texture,
5965
+ w = texture.width,
5966
+ h = texture.height,
5967
+ bytesPerRow = Math.ceil((w * 4) / 256) * 256;
5968
+
5969
+ let buffer = Q5.device.createBuffer({
5970
+ size: bytesPerRow * h,
5971
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
5972
+ });
5973
+
5974
+ let en = Q5.device.createCommandEncoder();
5975
+
5976
+ en.copyTextureToBuffer({ texture }, { buffer, bytesPerRow, rowsPerImage: h }, { width: w, height: h });
5977
+
5978
+ Q5.device.queue.submit([en.finish()]);
5979
+
5980
+ await buffer.mapAsync(GPUMapMode.READ);
5981
+
5982
+ let pad = new Uint8Array(buffer.getMappedRange());
5983
+ data = new Uint8Array(w * h * 4); // unpadded data
5984
+
5985
+ // Remove padding from each row
5986
+ for (let y = 0; y < h; y++) {
5987
+ const p = y * bytesPerRow; // padded row offset
5988
+ const u = y * w * 4; // unpadded row offset
5989
+ data.set(pad.subarray(p, p + w * 4), u);
5990
+ }
5991
+
5992
+ buffer.unmap();
5993
+
5994
+ let colorSpace = $.canvas.colorSpace;
5995
+ data = new Uint8ClampedArray(data.buffer);
5996
+ data = new ImageData(data, w, h, { colorSpace });
5997
+ let cnv = new OffscreenCanvas(w, h);
5998
+ let ctx = cnv.getContext('2d', { colorSpace });
5999
+ ctx.putImageData(data, 0, 0);
6000
+
6001
+ // Convert to blob then data URL
6002
+ let blob = await cnv.convertToBlob({ type: 'image/' + ext });
6003
+ return await new Promise((resolve) => {
6004
+ let r = new FileReader();
6005
+ r.onloadend = () => resolve(r.result);
6006
+ r.readAsDataURL(blob);
6007
+ });
6008
+ };
6009
+
5848
6010
  $._hooks.preRender.push(() => {
5849
- if (!$._textureBindGroups.length) return;
6011
+ if (!vertIndex) return;
5850
6012
 
5851
6013
  // Switch to image pipeline
5852
6014
  $.pass.setPipeline($._pipelines[1]);
@@ -6387,20 +6549,14 @@ fn fragmentMain(f : FragmentParams) -> @location(0) vec4f {
6387
6549
  }
6388
6550
 
6389
6551
  let img = $._g.createTextImage(str, w, h);
6390
-
6391
- if (img.canvas.textureIndex == undefined) {
6552
+ if (img.textureIndex == undefined) {
6392
6553
  $._createTexture(img);
6393
- } else if (img.modified) {
6394
- let cnv = img.canvas;
6395
- let textureSize = [cnv.width, cnv.height, 1];
6396
- let texture = $._textures[cnv.textureIndex];
6397
-
6398
- Q5.device.queue.copyExternalImageToTexture(
6399
- { source: cnv },
6400
- { texture, colorSpace: $.canvas.colorSpace },
6401
- textureSize
6402
- );
6403
- img.modified = false;
6554
+ let _copy = img.copy;
6555
+ img.copy = function () {
6556
+ let copy = _copy();
6557
+ $._createTexture(copy);
6558
+ return copy;
6559
+ };
6404
6560
  }
6405
6561
  return img;
6406
6562
  };