q5 2.18.3 → 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.
Files changed (5) hide show
  1. package/deno.json +1 -1
  2. package/package.json +1 -1
  3. package/q5.d.ts +195 -49
  4. package/q5.js +279 -124
  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;
@@ -1324,10 +1281,7 @@ Q5.renderers.c2d.image = ($, q) => {
1324
1281
  opt ??= {};
1325
1282
  opt.alpha ??= true;
1326
1283
  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;
1284
+ return new Q5.Image(w, h, opt);
1331
1285
  };
1332
1286
 
1333
1287
  $.loadImage = function (url, cb, opt) {
@@ -1483,7 +1437,7 @@ Q5.renderers.c2d.image = ($, q) => {
1483
1437
  $.ctx.globalCompositeOperation = 'source-over';
1484
1438
  $.ctx.drawImage($.canvas, 0, 0, $.canvas.w, $.canvas.h);
1485
1439
  $.ctx.restore();
1486
- $._retint = true;
1440
+ $.modified = $._retint = true;
1487
1441
  };
1488
1442
 
1489
1443
  if ($._scope == 'image') {
@@ -1501,7 +1455,7 @@ Q5.renderers.c2d.image = ($, q) => {
1501
1455
  $.ctx.clearRect(0, 0, c.width, c.height);
1502
1456
  $.ctx.drawImage(o, 0, 0, c.width, c.height);
1503
1457
 
1504
- $._retint = true;
1458
+ $.modified = $._retint = true;
1505
1459
  };
1506
1460
  }
1507
1461
 
@@ -1548,17 +1502,25 @@ Q5.renderers.c2d.image = ($, q) => {
1548
1502
  $.ctx.globalCompositeOperation = old;
1549
1503
  $.ctx.restore();
1550
1504
 
1551
- $._retint = true;
1505
+ $.modified = $._retint = true;
1552
1506
  };
1553
1507
 
1554
1508
  $.inset = (x, y, w, h, dx, dy, dw, dh) => {
1555
1509
  let pd = $._pixelDensity || 1;
1556
1510
  $.ctx.drawImage($.canvas, x * pd, y * pd, w * pd, h * pd, dx, dy, dw, dh);
1557
1511
 
1558
- $._retint = true;
1512
+ $.modified = $._retint = true;
1559
1513
  };
1560
1514
 
1561
- $.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
+ };
1562
1524
 
1563
1525
  $.get = (x, y, w, h) => {
1564
1526
  let pd = $._pixelDensity || 1;
@@ -1568,22 +1530,19 @@ Q5.renderers.c2d.image = ($, q) => {
1568
1530
  }
1569
1531
  x = Math.floor(x || 0) * pd;
1570
1532
  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;
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;
1580
1539
  return img;
1581
1540
  };
1582
1541
 
1583
1542
  $.set = (x, y, c) => {
1584
1543
  x = Math.floor(x);
1585
1544
  y = Math.floor(y);
1586
- $._retint = true;
1545
+ $.modified = $._retint = true;
1587
1546
  if (c.canvas) {
1588
1547
  let old = $._tint;
1589
1548
  $._tint = null;
@@ -1611,7 +1570,7 @@ Q5.renderers.c2d.image = ($, q) => {
1611
1570
  $.updatePixels = () => {
1612
1571
  if (imgData != null) {
1613
1572
  $.ctx.putImageData(imgData, 0, 0);
1614
- $._retint = true;
1573
+ $.modified = $._retint = true;
1615
1574
  }
1616
1575
  };
1617
1576
 
@@ -1620,6 +1579,20 @@ Q5.renderers.c2d.image = ($, q) => {
1620
1579
 
1621
1580
  if ($._scope == 'image') return;
1622
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
+
1623
1596
  $.tint = function (c) {
1624
1597
  $._tint = (c._q5Color ? c : $.color(...arguments)).toString();
1625
1598
  };
@@ -1963,10 +1936,15 @@ Q5.renderers.c2d.text = ($, q) => {
1963
1936
  tY = leading * lines.length;
1964
1937
 
1965
1938
  if (!img) {
1939
+ let ogBaseline = $.ctx.textBaseline;
1940
+ $.ctx.textBaseline = 'alphabetic';
1941
+
1966
1942
  let measure = ctx.measureText(' ');
1967
1943
  let ascent = measure.fontBoundingBoxAscent;
1968
1944
  let descent = measure.fontBoundingBoxDescent;
1969
1945
 
1946
+ $.ctx.textBaseline = ogBaseline;
1947
+
1970
1948
  img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(tY + descent), {
1971
1949
  pixelDensity: $._pixelDensity
1972
1950
  });
@@ -1977,12 +1955,13 @@ Q5.renderers.c2d.text = ($, q) => {
1977
1955
  img._middle = img._top + ascent * 0.5;
1978
1956
  img._bottom = img._top + ascent;
1979
1957
  img._leading = leading;
1958
+ } else {
1959
+ img.modified = true;
1980
1960
  }
1981
1961
 
1982
1962
  img._fill = $._fill;
1983
1963
  img._stroke = $._stroke;
1984
1964
  img._strokeWeight = $._strokeWeight;
1985
- img.modified = true;
1986
1965
 
1987
1966
  ctx = img.ctx;
1988
1967
 
@@ -2913,7 +2892,7 @@ Q5.modules.input = ($, q) => {
2913
2892
  $._onwheel = (e) => {
2914
2893
  $._updateMouse(e);
2915
2894
  e.delta = e.deltaY;
2916
- if ($.mouseWheel(e) == false) e.preventDefault();
2895
+ if ($.mouseWheel(e) == false || $._noScroll) e.preventDefault();
2917
2896
  };
2918
2897
 
2919
2898
  $.cursor = (name, x, y) => {
@@ -2928,9 +2907,8 @@ Q5.modules.input = ($, q) => {
2928
2907
  $.canvas.style.cursor = name + pfx;
2929
2908
  };
2930
2909
 
2931
- $.noCursor = () => {
2932
- $.canvas.style.cursor = 'none';
2933
- };
2910
+ $.noCursor = () => ($.canvas.style.cursor = 'none');
2911
+ $.noScroll = () => ($._noScroll = true);
2934
2912
 
2935
2913
  if (window) {
2936
2914
  $.lockMouse = document.body?.requestPointerLock;
@@ -4044,6 +4022,10 @@ Q5.modules.util = ($, q) => {
4044
4022
  $.loadJSON = (url, cb) => $._loadFile(url, cb, 'json');
4045
4023
  $.loadCSV = (url, cb) => $._loadFile(url, cb, 'csv');
4046
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
+
4047
4029
  $.load = function (...urls) {
4048
4030
  if (Array.isArray(urls[0])) urls = urls[0];
4049
4031
 
@@ -4057,11 +4039,11 @@ Q5.modules.util = ($, q) => {
4057
4039
  obj = $.loadJSON(url);
4058
4040
  } else if (ext == 'csv') {
4059
4041
  obj = $.loadCSV(url);
4060
- } else if (/(jpe?g|png|gif|webp|avif|svg)/.test(ext)) {
4042
+ } else if (imgRegex.test(ext)) {
4061
4043
  obj = $.loadImage(url);
4062
- } else if (/(ttf|otf|woff2?|eot|json)/i.test(ext)) {
4044
+ } else if (fontRegex.test(ext)) {
4063
4045
  obj = $.loadFont(url);
4064
- } else if (/(wav|flac|mp3|ogg|m4a|aac|aiff|weba)/.test(ext)) {
4046
+ } else if (audioRegex.test(ext)) {
4065
4047
  obj = $.loadSound(url);
4066
4048
  } else {
4067
4049
  obj = $.loadText(url);
@@ -4073,6 +4055,40 @@ Q5.modules.util = ($, q) => {
4073
4055
  return Promise.all(loaders);
4074
4056
  };
4075
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
+
4076
4092
  $.CSV = {};
4077
4093
  $.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
4078
4094
  if (!csv.length) return [];
@@ -4406,11 +4422,6 @@ Q5.Vector.sub = (v, u) => v.copy().sub(u);
4406
4422
  for (let k of ['fromAngle', 'fromAngles', 'random2D', 'random3D']) {
4407
4423
  Q5.Vector[k] = (u, v, t) => new Q5.Vector()[k](u, v, t);
4408
4424
  }
4409
- /**
4410
- * q5-webgpu
4411
- *
4412
- * EXPERIMENTAL, for developer testing only!
4413
- */
4414
4425
  Q5.renderers.webgpu = {};
4415
4426
 
4416
4427
  Q5.renderers.webgpu.canvas = ($, q) => {
@@ -4426,6 +4437,11 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4426
4437
 
4427
4438
  let pass,
4428
4439
  mainView,
4440
+ frameTextureA,
4441
+ frameTextureB,
4442
+ frameSampler,
4443
+ framePipeline,
4444
+ frameBindGroup,
4429
4445
  colorIndex = 1,
4430
4446
  colorStackIndex = 8;
4431
4447
 
@@ -4474,14 +4490,60 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4474
4490
  });
4475
4491
 
4476
4492
  let createMainView = () => {
4493
+ let w = $.canvas.width,
4494
+ h = $.canvas.height,
4495
+ size = [w, h],
4496
+ format = 'bgra8unorm';
4497
+
4477
4498
  mainView = Q5.device
4478
4499
  .createTexture({
4479
- size: [$.canvas.width, $.canvas.height],
4500
+ size,
4480
4501
  sampleCount: 4,
4481
- format: 'bgra8unorm',
4502
+ format,
4482
4503
  usage: GPUTextureUsage.RENDER_ATTACHMENT
4483
4504
  })
4484
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
+ });
4485
4547
  };
4486
4548
 
4487
4549
  $._createCanvas = (w, h, opt) => {
@@ -4847,23 +4909,52 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4847
4909
  }
4848
4910
  };
4849
4911
 
4850
- $.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
+ };
4851
4922
 
4852
4923
  $._beginRender = () => {
4924
+ // swap the frame textures
4925
+ const temp = frameTextureA;
4926
+ frameTextureA = frameTextureB;
4927
+ frameTextureB = temp;
4928
+ $.canvas.texture = frameTextureA;
4929
+
4853
4930
  $.encoder = Q5.device.createCommandEncoder();
4854
4931
 
4932
+ let target = shouldClear ? $.ctx.getCurrentTexture().createView() : frameTextureA.createView();
4933
+
4855
4934
  pass = q.pass = $.encoder.beginRenderPass({
4856
4935
  label: 'q5-webgpu',
4857
4936
  colorAttachments: [
4858
4937
  {
4859
4938
  view: mainView,
4860
- resolveTarget: $.ctx.getCurrentTexture().createView(),
4939
+ resolveTarget: target,
4861
4940
  loadOp: 'clear',
4862
4941
  storeOp: 'store',
4863
4942
  clearValue: [0, 0, 0, 0]
4864
4943
  }
4865
4944
  ]
4866
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
+ }
4867
4958
  };
4868
4959
 
4869
4960
  $._render = () => {
@@ -4938,8 +5029,33 @@ Q5.renderers.webgpu.canvas = ($, q) => {
4938
5029
 
4939
5030
  $._finishRender = () => {
4940
5031
  pass.end();
4941
- let commandBuffer = $.encoder.finish();
4942
- 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()]);
4943
5059
 
4944
5060
  q.pass = $.encoder = null;
4945
5061
 
@@ -5734,12 +5850,10 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5734
5850
 
5735
5851
  $.smooth();
5736
5852
 
5737
- let MAX_TEXTURES = 12000;
5738
-
5739
- $._textures = [];
5740
5853
  let tIdx = 0;
5741
5854
 
5742
5855
  $._createTexture = (img) => {
5856
+ let g = img;
5743
5857
  if (img.canvas) img = img.canvas;
5744
5858
 
5745
5859
  let textureSize = [img.width, img.height, 1];
@@ -5759,26 +5873,18 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5759
5873
  textureSize
5760
5874
  );
5761
5875
 
5762
- $._textures[tIdx] = texture;
5763
- img.textureIndex = tIdx;
5876
+ g.texture = texture;
5877
+ g.textureIndex = tIdx;
5764
5878
 
5765
- const textureBindGroup = Q5.device.createBindGroup({
5879
+ $._textureBindGroups[tIdx] = Q5.device.createBindGroup({
5766
5880
  layout: textureLayout,
5767
5881
  entries: [
5768
5882
  { binding: 0, resource: sampler },
5769
5883
  { binding: 1, resource: texture.createView() }
5770
5884
  ]
5771
5885
  });
5772
- $._textureBindGroups[tIdx] = textureBindGroup;
5773
5886
 
5774
- tIdx = (tIdx + 1) % MAX_TEXTURES;
5775
-
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
- }
5887
+ tIdx++;
5782
5888
  };
5783
5889
 
5784
5890
  $.loadImage = (src, cb) => {
@@ -5809,23 +5915,31 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5809
5915
  };
5810
5916
 
5811
5917
  $.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
5918
  if (img.textureIndex == undefined) return;
5815
5919
 
5920
+ let cnv = img.canvas || img;
5921
+
5816
5922
  if ($._matrixDirty) $._saveMatrix();
5817
5923
 
5818
- let w = img.width,
5819
- h = img.height,
5820
- pd = g._pixelDensity || 1;
5924
+ let w = cnv.width,
5925
+ h = cnv.height,
5926
+ pd = img._pixelDensity || 1;
5821
5927
 
5822
- dw ??= g.defaultWidth;
5823
- 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;
5824
5939
  sw ??= w;
5825
5940
  sh ??= h;
5826
-
5827
- dw *= pd;
5828
- dh *= pd;
5941
+ sx *= pd;
5942
+ sy *= pd;
5829
5943
 
5830
5944
  let [l, r, t, b] = $._calcBox(dx, dy, dw, dh, $._imageMode);
5831
5945
 
@@ -5845,8 +5959,55 @@ fn fragmentMain(f: FragmentParams) -> @location(0) vec4f {
5845
5959
  $.drawStack.push(1, img.textureIndex);
5846
5960
  };
5847
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
+
5848
6009
  $._hooks.preRender.push(() => {
5849
- if (!$._textureBindGroups.length) return;
6010
+ if (!vertIndex) return;
5850
6011
 
5851
6012
  // Switch to image pipeline
5852
6013
  $.pass.setPipeline($._pipelines[1]);
@@ -6387,20 +6548,14 @@ fn fragmentMain(f : FragmentParams) -> @location(0) vec4f {
6387
6548
  }
6388
6549
 
6389
6550
  let img = $._g.createTextImage(str, w, h);
6390
-
6391
- if (img.canvas.textureIndex == undefined) {
6551
+ if (img.textureIndex == undefined) {
6392
6552
  $._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;
6553
+ let _copy = img.copy;
6554
+ img.copy = function () {
6555
+ let copy = _copy();
6556
+ $._createTexture(copy);
6557
+ return copy;
6558
+ };
6404
6559
  }
6405
6560
  return img;
6406
6561
  };