q5 3.4.0 → 3.5.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 +3 -3
  3. package/q5.d.ts +431 -384
  4. package/q5.js +163 -89
  5. package/q5.min.js +2 -2
package/q5.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 3.4
3
+ * @version 3.5
4
4
  * @author quinton-ashley
5
5
  * @contributors evanalulu, Tezumie, ormaq, Dukemz, LingDong-
6
6
  * @license LGPL-3.0
@@ -426,12 +426,14 @@ Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.preloadMethods[n] = fn[n]);
426
426
  function createCanvas(w, h, opt) {
427
427
  if (Q5._hasGlobal) return;
428
428
 
429
- if (w == 'webgpu' || h == 'webgpu' || opt == 'webgpu' || opt?.renderer == 'webgpu') {
430
- return Q5.WebGPU().then((q) => q.createCanvas(w, h, opt));
431
- } else {
429
+ let useC2D = w == 'c2d' || h == 'c2d' || opt == 'c2d' || opt?.renderer == 'c2d' || !Q5._esm;
430
+
431
+ if (useC2D) {
432
432
  let q = new Q5();
433
433
  let c = q.createCanvas(w, h, opt);
434
434
  return q.ready.then(() => c);
435
+ } else {
436
+ return Q5.WebGPU().then((q) => q.createCanvas(w, h, opt));
435
437
  }
436
438
  }
437
439
 
@@ -440,14 +442,21 @@ if (Q5._server) global.p5 ??= global.Q5 = Q5;
440
442
  if (typeof window == 'object') {
441
443
  window.p5 ??= window.Q5 = Q5;
442
444
  window.createCanvas = createCanvas;
443
- window.GPU = window.WEBGPU = 'webgpu';
445
+ window.C2D = 'c2d';
446
+ window.WEBGPU = 'webgpu';
444
447
  } else global.window = 0;
445
448
 
446
- Q5.version = Q5.VERSION = '3.4';
449
+ Q5.version = Q5.VERSION = '3.5';
447
450
 
448
451
  if (typeof document == 'object') {
449
452
  document.addEventListener('DOMContentLoaded', () => {
450
- if (!Q5._hasGlobal) new Q5('auto');
453
+ if (!Q5._hasGlobal) {
454
+ if (Q5.setup || Q5.update || Q5.draw) {
455
+ Q5.WebGPU();
456
+ } else {
457
+ new Q5('auto');
458
+ }
459
+ }
451
460
  });
452
461
  }
453
462
  Q5.modules.canvas = ($, q) => {
@@ -2286,6 +2295,7 @@ Q5.modules.color = ($, q) => {
2286
2295
  pink: [255, 192, 203],
2287
2296
  purple: [128, 0, 128],
2288
2297
  red: [255, 0, 0],
2298
+ silver: [192, 192, 192],
2289
2299
  skyblue: [135, 206, 235],
2290
2300
  tan: [210, 180, 140],
2291
2301
  turquoise: [64, 224, 208],
@@ -3094,8 +3104,8 @@ Q5.modules.dom = ($, q) => {
3094
3104
  el.type = 'range';
3095
3105
  el.min = min;
3096
3106
  el.max = max;
3097
- el.value = value;
3098
3107
  el.step = step;
3108
+ el.value = value;
3099
3109
  el.val = () => parseFloat(el.value);
3100
3110
  return el;
3101
3111
  };
@@ -3194,6 +3204,7 @@ Q5.modules.fes = ($) => {
3194
3204
 
3195
3205
  let errFile = stackLines[idx].split(sep).at(-1);
3196
3206
  if (errFile.startsWith('blob:')) errFile = errFile.slice(5);
3207
+ errFile = errFile.split(')')[0];
3197
3208
  let parts = errFile.split(':');
3198
3209
  let lineNum = parseInt(parts.at(-2));
3199
3210
  parts[parts.length - 1] = parts.at(-1).split(')')[0];
@@ -3246,24 +3257,24 @@ Q5.modules.fes = ($) => {
3246
3257
  });
3247
3258
  }
3248
3259
  }
3249
- };
3250
3260
 
3251
- if (typeof navigator != undefined && navigator.onLine) {
3252
- async function checkLatestVersion() {
3253
- try {
3254
- let response = await fetch('https://data.jsdelivr.com/v1/package/npm/q5');
3255
- if (!response.ok) return;
3256
- let data = await response.json();
3257
- let l = data.tags.latest;
3258
- l = l.slice(0, l.lastIndexOf('.'));
3259
- if (l != Q5.version) {
3260
- console.warn(`q5.js v${l} is now available! Consider updating from v${Q5.version}.`);
3261
- }
3262
- } catch (e) {}
3263
- }
3261
+ if (Q5.online != false && typeof navigator != undefined && navigator.onLine) {
3262
+ async function checkLatestVersion() {
3263
+ try {
3264
+ let response = await fetch('https://data.jsdelivr.com/v1/package/npm/q5');
3265
+ if (!response.ok) return;
3266
+ let data = await response.json();
3267
+ let l = data.tags.latest;
3268
+ l = l.slice(0, l.lastIndexOf('.'));
3269
+ if (l != Q5.version) {
3270
+ console.warn(`q5.js v${l} is now available! Consider updating from v${Q5.version}.`);
3271
+ }
3272
+ } catch (e) {}
3273
+ }
3264
3274
 
3265
- checkLatestVersion();
3266
- }
3275
+ checkLatestVersion();
3276
+ }
3277
+ };
3267
3278
  Q5.modules.input = ($, q) => {
3268
3279
  if ($._isGraphics) return;
3269
3280
 
@@ -5071,6 +5082,8 @@ struct Q5 {
5071
5082
  frameLayout,
5072
5083
  frameSampler,
5073
5084
  frameBindGroup,
5085
+ frameBindGroupA,
5086
+ frameBindGroupB,
5074
5087
  colorIndex = 2,
5075
5088
  colorStackIndex = 12,
5076
5089
  prevFramePL = 0,
@@ -5228,6 +5241,25 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5228
5241
 
5229
5242
  // Create a pipeline for rendering frames
5230
5243
  $._pipelines[0] = Q5.device.createRenderPipeline($._pipelineConfigs[0]);
5244
+
5245
+ // Create persistent bind groups for both frame buffers
5246
+ frameBindGroupA = Q5.device.createBindGroup({
5247
+ layout: frameLayout,
5248
+ entries: [
5249
+ { binding: 0, resource: { buffer: uniformBuffer } },
5250
+ { binding: 1, resource: frameSampler },
5251
+ { binding: 2, resource: frameA.createView() }
5252
+ ]
5253
+ });
5254
+
5255
+ frameBindGroupB = Q5.device.createBindGroup({
5256
+ layout: frameLayout,
5257
+ entries: [
5258
+ { binding: 0, resource: { buffer: uniformBuffer } },
5259
+ { binding: 1, resource: frameSampler },
5260
+ { binding: 2, resource: frameB.createView() }
5261
+ ]
5262
+ });
5231
5263
  };
5232
5264
 
5233
5265
  $._createCanvas = (w, h, opt) => {
@@ -5608,6 +5640,9 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5608
5640
  $.popStyles();
5609
5641
  };
5610
5642
 
5643
+ // Reusable array for calcBox to avoid GC
5644
+ let boxCache = [0, 0, 0, 0];
5645
+
5611
5646
  const calcBox = (x, y, w, h, mode) => {
5612
5647
  // left, right, top, bottom
5613
5648
  let l, r, t, b;
@@ -5631,7 +5666,11 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5631
5666
  b = h;
5632
5667
  }
5633
5668
 
5634
- return [l, r, t, b];
5669
+ boxCache[0] = l;
5670
+ boxCache[1] = r;
5671
+ boxCache[2] = t;
5672
+ boxCache[3] = b;
5673
+ return boxCache;
5635
5674
  };
5636
5675
 
5637
5676
  // prettier-ignore
@@ -5725,11 +5764,15 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5725
5764
  };
5726
5765
 
5727
5766
  $._beginRender = () => {
5728
- // swap the frame textures
5767
+ // swap the frame textures and bind groups
5729
5768
  const temp = frameA;
5730
5769
  frameA = frameB;
5731
5770
  frameB = temp;
5732
5771
 
5772
+ const tempBindGroup = frameBindGroupA;
5773
+ frameBindGroupA = frameBindGroupB;
5774
+ frameBindGroupB = tempBindGroup;
5775
+
5733
5776
  encoder = Q5.device.createCommandEncoder();
5734
5777
 
5735
5778
  $._pass = pass = encoder.beginRenderPass({
@@ -5745,14 +5788,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5745
5788
  ]
5746
5789
  });
5747
5790
 
5748
- frameBindGroup = Q5.device.createBindGroup({
5749
- layout: frameLayout,
5750
- entries: [
5751
- { binding: 0, resource: { buffer: uniformBuffer } },
5752
- { binding: 1, resource: frameSampler },
5753
- { binding: 2, resource: frameB.createView() }
5754
- ]
5755
- });
5791
+ // Use pre-created bind group instead of creating new one
5792
+ frameBindGroup = frameBindGroupB;
5756
5793
 
5757
5794
  if (!shouldClear) {
5758
5795
  pass.setPipeline($._pipelines[prevFramePL]);
@@ -5763,6 +5800,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5763
5800
  };
5764
5801
 
5765
5802
  let transformsBuffer, colorsBuffer, shapesVertBuff, imgVertBuff, charBuffer, textBuffer;
5803
+ let mainBindGroup, lastTransformsBuffer, lastColorsBuffer;
5766
5804
 
5767
5805
  $._render = () => {
5768
5806
  let transformsSize = matrices.length * MATRIX_SIZE * 4; // 4 bytes per float
@@ -5787,32 +5825,36 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5787
5825
 
5788
5826
  Q5.device.queue.writeBuffer(colorsBuffer, 0, colorStack.subarray(0, colorStackIndex));
5789
5827
 
5790
- $._uniforms = [
5791
- $.width,
5792
- $.height,
5793
- $.halfWidth,
5794
- $.halfHeight,
5795
- $._pixelDensity,
5796
- $.frameCount,
5797
- performance.now(),
5798
- $.deltaTime,
5799
- $.mouseX,
5800
- $.mouseY,
5801
- $.mouseIsPressed ? 1 : 0,
5802
- $.keyCode,
5803
- $.keyIsPressed ? 1 : 0
5804
- ];
5805
-
5806
- Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array($._uniforms));
5807
-
5808
- let mainBindGroup = Q5.device.createBindGroup({
5809
- layout: mainLayout,
5810
- entries: [
5811
- { binding: 0, resource: { buffer: uniformBuffer } },
5812
- { binding: 1, resource: { buffer: transformsBuffer } },
5813
- { binding: 2, resource: { buffer: colorsBuffer } }
5814
- ]
5815
- });
5828
+ // Reuse uniform array instead of creating new one each frame
5829
+ $._uniforms[0] = $.width;
5830
+ $._uniforms[1] = $.height;
5831
+ $._uniforms[2] = $.halfWidth;
5832
+ $._uniforms[3] = $.halfHeight;
5833
+ $._uniforms[4] = $._pixelDensity;
5834
+ $._uniforms[5] = $.frameCount;
5835
+ $._uniforms[6] = performance.now();
5836
+ $._uniforms[7] = $.deltaTime;
5837
+ $._uniforms[8] = $.mouseX;
5838
+ $._uniforms[9] = $.mouseY;
5839
+ $._uniforms[10] = $.mouseIsPressed ? 1 : 0;
5840
+ $._uniforms[11] = $.keyCode;
5841
+ $._uniforms[12] = $.keyIsPressed ? 1 : 0;
5842
+
5843
+ Q5.device.queue.writeBuffer(uniformBuffer, 0, $._uniforms);
5844
+
5845
+ // Only recreate bind group if buffers changed
5846
+ if (!mainBindGroup || lastTransformsBuffer !== transformsBuffer || lastColorsBuffer !== colorsBuffer) {
5847
+ mainBindGroup = Q5.device.createBindGroup({
5848
+ layout: mainLayout,
5849
+ entries: [
5850
+ { binding: 0, resource: { buffer: uniformBuffer } },
5851
+ { binding: 1, resource: { buffer: transformsBuffer } },
5852
+ { binding: 2, resource: { buffer: colorsBuffer } }
5853
+ ]
5854
+ });
5855
+ lastTransformsBuffer = transformsBuffer;
5856
+ lastColorsBuffer = colorsBuffer;
5857
+ }
5816
5858
 
5817
5859
  pass.setBindGroup(0, mainBindGroup);
5818
5860
 
@@ -5860,11 +5902,13 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5860
5902
  // prepare to render text
5861
5903
 
5862
5904
  if (charStack.length) {
5863
- // Calculate total buffer size for text data
5864
- let totalTextSize = 0;
5905
+ // Flatten char data into reusable buffer instead of creating new array
5906
+ let charOffset = 0;
5865
5907
  for (let charsData of charStack) {
5866
- totalTextSize += charsData.length * 4;
5908
+ charDataBuffer.set(charsData, charOffset);
5909
+ charOffset += charsData.length;
5867
5910
  }
5911
+ let totalTextSize = charOffset * 4;
5868
5912
 
5869
5913
  if (!charBuffer || charBuffer.size < totalTextSize) {
5870
5914
  if (charBuffer) charBuffer.destroy();
@@ -5874,10 +5918,16 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5874
5918
  });
5875
5919
  }
5876
5920
 
5877
- Q5.device.queue.writeBuffer(charBuffer, 0, new Float32Array(charStack.flat()));
5921
+ Q5.device.queue.writeBuffer(charBuffer, 0, charDataBuffer.buffer, 0, totalTextSize);
5922
+
5923
+ // Flatten text metadata into reusable buffer
5924
+ let textOffset = 0;
5925
+ for (let textData of textStack) {
5926
+ textDataBuffer.set(textData, textOffset);
5927
+ textOffset += textData.length;
5928
+ }
5929
+ let totalMetadataSize = textOffset * 4;
5878
5930
 
5879
- // Calculate total buffer size for metadata
5880
- let totalMetadataSize = textStack.length * 8 * 4;
5881
5931
  if (!textBuffer || textBuffer.size < totalMetadataSize) {
5882
5932
  if (textBuffer) textBuffer.destroy();
5883
5933
  textBuffer = Q5.device.createBuffer({
@@ -5887,7 +5937,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5887
5937
  });
5888
5938
  }
5889
5939
 
5890
- Q5.device.queue.writeBuffer(textBuffer, 0, new Float32Array(textStack.flat()));
5940
+ Q5.device.queue.writeBuffer(textBuffer, 0, textDataBuffer.buffer, 0, totalMetadataSize);
5891
5941
 
5892
5942
  // create a single bind group for the text buffer and metadata buffer
5893
5943
  $._textBindGroup = Q5.device.createBindGroup({
@@ -6005,14 +6055,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
6005
6055
  ]
6006
6056
  });
6007
6057
 
6008
- frameBindGroup = Q5.device.createBindGroup({
6009
- layout: frameLayout,
6010
- entries: [
6011
- { binding: 0, resource: { buffer: uniformBuffer } },
6012
- { binding: 1, resource: frameSampler },
6013
- { binding: 2, resource: frameA.createView() }
6014
- ]
6015
- });
6058
+ // Use pre-created bind group instead of creating new one
6059
+ frameBindGroup = frameBindGroupA;
6016
6060
 
6017
6061
  pass.setPipeline($._pipelines[framePL]);
6018
6062
  pass.setBindGroup(0, frameBindGroup);
@@ -6024,11 +6068,11 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
6024
6068
  $._pass = pass = encoder = null;
6025
6069
 
6026
6070
  // clear the stacks for the next frame
6027
- drawStack.splice(0, drawStack.length);
6071
+ drawStack.length = 0; // faster than splice for clearing
6028
6072
  colorIndex = 2;
6029
6073
  colorStackIndex = 12;
6030
- matrices = [matrices[0]];
6031
- matricesIdxStack = [];
6074
+ matrices.length = 1; // keep first matrix, clear rest
6075
+ matricesIdxStack.length = 0;
6032
6076
 
6033
6077
  // frameA can now be saved when saveCanvas is run
6034
6078
  $._texture = frameA;
@@ -6036,13 +6080,15 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
6036
6080
  // reset
6037
6081
  shapesVertIdx = 0;
6038
6082
  imgVertIdx = 0;
6039
- $._textureBindGroups.splice(tIdx, vidFrames);
6083
+ // Remove video frames without creating new array
6084
+ if (vidFrames > 0) {
6085
+ $._textureBindGroups.length = tIdx;
6086
+ }
6040
6087
  vidFrames = 0;
6041
- charStack = [];
6042
- textStack = [];
6043
- rectStack = new Float32Array(Q5.MAX_RECTS * 16);
6088
+ charStack.length = 0;
6089
+ textStack.length = 0;
6090
+ // Don't create new typed arrays - just reset index
6044
6091
  rectStackIdx = 0;
6045
- ellipseStack = new Float32Array(Q5.MAX_ELLIPSES * 16);
6046
6092
  ellipseStackIdx = 0;
6047
6093
 
6048
6094
  // destroy buffers
@@ -6614,6 +6660,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6614
6660
  $.rectMode = (x) => (_rectMode = x);
6615
6661
  $._getRectMode = () => _rectMode;
6616
6662
 
6663
+ // Reusable array for rect mode calculations
6664
+ let rectModeCache = [0, 0, 0, 0];
6665
+
6617
6666
  function applyRectMode(x, y, w, h) {
6618
6667
  let hw = w / 2,
6619
6668
  hh = h / 2;
@@ -6631,7 +6680,11 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6631
6680
  y += hh;
6632
6681
  }
6633
6682
  }
6634
- return [x, y, hw, hh];
6683
+ rectModeCache[0] = x;
6684
+ rectModeCache[1] = y;
6685
+ rectModeCache[2] = hw;
6686
+ rectModeCache[3] = hh;
6687
+ return rectModeCache;
6635
6688
  }
6636
6689
 
6637
6690
  $.rect = (x, y, w, h, rr = 0) => {
@@ -6909,6 +6962,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6909
6962
  $.ellipseMode = (x) => (_ellipseMode = x);
6910
6963
  $._getEllipseMode = () => _ellipseMode;
6911
6964
 
6965
+ // Reusable array for ellipse mode calculations
6966
+ let ellipseModeCache = [0, 0, 0, 0];
6967
+
6912
6968
  function applyEllipseMode(x, y, w, h) {
6913
6969
  h ??= w;
6914
6970
  let a, b;
@@ -6929,7 +6985,11 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6929
6985
  a = (w - x) / 2;
6930
6986
  b = (h - y) / 2;
6931
6987
  }
6932
- return [x, y, a, b];
6988
+ ellipseModeCache[0] = x;
6989
+ ellipseModeCache[1] = y;
6990
+ ellipseModeCache[2] = a;
6991
+ ellipseModeCache[3] = b;
6992
+ return ellipseModeCache;
6933
6993
  }
6934
6994
 
6935
6995
  $.ellipse = (x, y, w, h) => {
@@ -7326,6 +7386,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7326
7386
  $.imageMode = (x) => (_imageMode = x);
7327
7387
  $._getImageMode = () => _imageMode;
7328
7388
 
7389
+ // Reusable uniform buffer array to avoid GC
7390
+ $._uniforms = new Float32Array(13);
7391
+
7329
7392
  const addImgVert = (x, y, u, v, ci, ti, ia) => {
7330
7393
  let s = imgVertStack,
7331
7394
  i = imgVertIdx;
@@ -7340,6 +7403,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7340
7403
  };
7341
7404
 
7342
7405
  $.image = (img, dx = 0, dy = 0, dw, dh, sx = 0, sy = 0, sw, sh) => {
7406
+ if (!img) return;
7343
7407
  let isVideo;
7344
7408
  if (img._texture == undefined) {
7345
7409
  isVideo = img.tagName == 'VIDEO';
@@ -7754,7 +7818,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7754
7818
  $._loadDefaultFont = (fontName, cb) => {
7755
7819
  fonts[fontName] = null;
7756
7820
  let url = `https://q5js.org/fonts/${fontName}-msdf.json`;
7757
- if (!navigator.onLine) {
7821
+ if (Q5.online == false || !navigator.onLine) {
7758
7822
  url = `/node_modules/q5/builtinFonts/${fontName}-msdf.json`;
7759
7823
  }
7760
7824
  return $.loadFont(url, cb);
@@ -7827,13 +7891,20 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7827
7891
  let charStack = [],
7828
7892
  textStack = [];
7829
7893
 
7894
+ // Reusable array for line widths to avoid GC
7895
+ let lineWidthsCache = new Array(100);
7896
+
7897
+ // Reusable buffers for text data to avoid creating new arrays
7898
+ let charDataBuffer = new Float32Array(100000); // reusable buffer for char data
7899
+ let textDataBuffer = new Float32Array(10000); // reusable buffer for text metadata
7900
+
7830
7901
  let measureText = (font, text, charCallback) => {
7831
7902
  let maxWidth = 0,
7832
7903
  offsetX = 0,
7833
7904
  offsetY = 0,
7834
7905
  line = 0,
7835
7906
  printedCharCount = 0,
7836
- lineWidths = [],
7907
+ lineWidths = lineWidthsCache, // reuse array
7837
7908
  nextCharCode = text.charCodeAt(0);
7838
7909
 
7839
7910
  for (let i = 0; i < text.length; ++i) {
@@ -7864,12 +7935,14 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7864
7935
  printedCharCount++;
7865
7936
  }
7866
7937
  }
7867
- lineWidths.push(offsetX);
7938
+ lineWidths[line] = offsetX;
7868
7939
  maxWidth = Math.max(maxWidth, offsetX);
7940
+ let lineCount = line + 1;
7869
7941
  return {
7870
7942
  width: maxWidth,
7871
- height: lineWidths.length * font.lineHeight * leadPercent,
7943
+ height: lineCount * font.lineHeight * leadPercent,
7872
7944
  lineWidths,
7945
+ lineCount,
7873
7946
  printedCharCount
7874
7947
  };
7875
7948
  };
@@ -8128,6 +8201,7 @@ Q5.initWebGPU = async () => {
8128
8201
  return false;
8129
8202
  }
8130
8203
  if (!Q5.requestedGPU) {
8204
+ Q5.requestedGPU = true;
8131
8205
  let adapter = await navigator.gpu.requestAdapter();
8132
8206
  if (!adapter) {
8133
8207
  console.warn('q5 WebGPU could not start! No appropriate GPUAdapter found, Vulkan may need to be enabled.');