q5 3.5.0 → 3.5.2

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 +18 -71
  4. package/q5.js +166 -101
  5. package/q5.min.js +1 -1
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q5/q5",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "license": "LGPL-3.0",
5
5
  "description": "Beginner friendly graphics powered by WebGPU and optimized for interactive art!",
6
6
  "author": "quinton-ashley",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "q5",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "Beginner friendly graphics powered by WebGPU and optimized for interactive art!",
5
5
  "author": "quinton-ashley",
6
6
  "contributors": [
package/q5.d.ts CHANGED
@@ -458,10 +458,6 @@ createCanvas(200);
458
458
  createCanvas(200, 100);
459
459
  circle(100, 50, 80);
460
460
  * @example
461
- await Q5.WebGPU();
462
- createCanvas(200, 100);
463
- circle(0, 0, 80);
464
- * @example
465
461
  createCanvas(200, 200, { alpha: true });
466
462
 
467
463
  function draw() {
@@ -971,15 +967,15 @@ function draw() {
971
967
  c.g = (c.g + 1) % 256;
972
968
  }
973
969
  * @example
974
- let q = await Q5.WebGPU();
970
+ createCanvas(200);
975
971
 
976
- // (r, g, b, a)
977
- let c = color(0, 1, 1, 0.2);
972
+ // (r, g, b, a)
973
+ let c = color(0, 255, 255, 50);
978
974
 
979
- q.draw = () => {
975
+ function draw() {
980
976
  fill(c);
981
977
  circle(mouseX, mouseY, 50);
982
- };
978
+ }
983
979
  */
984
980
  function color(c0: string | number | Color | number[], c1?: number, c2?: number, c3?: number): Color;
985
981
 
@@ -1274,12 +1270,10 @@ circle(25, 12.5, 16);
1274
1270
  createCanvas(200, 100);
1275
1271
  background('crimson');
1276
1272
  * @example
1277
- let q = await Q5.WebGPU();
1278
-
1279
- q.draw = () => {
1280
- background(0.5, 0.4);
1273
+ function draw() {
1274
+ background(128, 110);
1281
1275
  circle(mouseX, mouseY, 20);
1282
- };
1276
+ }
1283
1277
  */
1284
1278
  function background(filler: Color | Q5.Image): void;
1285
1279
 
@@ -1395,12 +1389,10 @@ background(200);
1395
1389
  strokeWeight(5);
1396
1390
  capsule(40, 40, 160, 60, 10);
1397
1391
  * @example
1398
- let q = await Q5.WebGPU();
1399
-
1400
- q.draw = () => {
1401
- background(0.8);
1392
+ function draw() {
1393
+ background(200);
1402
1394
  strokeWeight(10);
1403
- capsule(0, 0, mouseX, mouseY, 20);
1395
+ capsule(100, 100, mouseX, mouseY, 20);
1404
1396
  }
1405
1397
  */
1406
1398
  function capsule(x1: number, y1: number, x2: number, y2: number, r: number): void;
@@ -1553,15 +1545,6 @@ ellipse(50, 25, 150, 75);
1553
1545
  *
1554
1546
  * Only takes effect in q5 WebGPU.
1555
1547
  * @param {number} val curve detail level, default is 20
1556
- * @example
1557
- await Q5.WebGPU();
1558
-
1559
- curveDetail(4);
1560
-
1561
- strokeWeight(10);
1562
- stroke(0, 1, 1);
1563
- noFill();
1564
- curve(-100, -200, -50, 0, 50, 0, 100, -200);
1565
1548
  */
1566
1549
  function curveDetail(val: number): void;
1567
1550
 
@@ -1719,15 +1702,6 @@ let logo = loadImage('/q5js_logo.avif');
1719
1702
  function draw() {
1720
1703
  background(logo);
1721
1704
  }
1722
- * @example
1723
- let q = await Q5.WebGPU();
1724
- createCanvas(200);
1725
-
1726
- let logo = loadImage('/q5js_logo.avif');
1727
-
1728
- q.draw = () => {
1729
- background(logo);
1730
- };
1731
1705
  */
1732
1706
  function loadImage(url: string): Q5.Image | Promise<Q5.Image>;
1733
1707
 
@@ -2212,13 +2186,12 @@ textFont('serif');
2212
2186
  textSize(32);
2213
2187
  text('Hello, world!', 15, 90);
2214
2188
  * @example
2215
- let q = await Q5.WebGPU();
2216
2189
  createCanvas(200);
2217
- background(0.8);
2190
+ background(200);
2218
2191
 
2219
2192
  textFont('monospace');
2220
2193
 
2221
- q.setup = () => {
2194
+ function setup() {
2222
2195
  text('Hello, world!', -65, 0);
2223
2196
  }
2224
2197
  */
@@ -2385,23 +2358,21 @@ function draw() {
2385
2358
  * @param {number} x x-coordinate where the image should be placed
2386
2359
  * @param {number} y y-coordinate where the image should be placed
2387
2360
  * @example
2388
- let q = await Q5.WebGPU();
2389
2361
  createCanvas(200);
2390
- background(0.8);
2362
+ background(200);
2391
2363
  textSize(96);
2392
2364
  textAlign(CENTER, CENTER);
2393
2365
 
2394
- textImage('🐶', 0, 0);
2366
+ textImage('🐶', 100,100);
2395
2367
  * @example
2396
- let q = await Q5.WebGPU();
2397
2368
  createCanvas(200);
2398
2369
 
2399
2370
  loadFont('/assets/Robotica.ttf');
2400
2371
 
2401
- q.setup = () => {
2402
- background(0.8);
2372
+ function setup() {
2373
+ background(200);
2403
2374
  textSize(66);
2404
- textImage('Hello!', -100, 100);
2375
+ textImage('Hello!', 0, 0);
2405
2376
  };
2406
2377
  */
2407
2378
  function textImage(img: Q5.Image | String, x: number, y: number): void;
@@ -2896,19 +2867,6 @@ function draw() {
2896
2867
  circle(x, 100, n * 40);
2897
2868
  }
2898
2869
  }
2899
- * @example
2900
- let q = await Q5.WebGPU();
2901
-
2902
- q.draw = () => {
2903
- noStroke();
2904
- let t = millis() * 0.002;
2905
- for (let x = -100; x < 100; x += 5) {
2906
- for (let y = -100; y < 100; y += 5) {
2907
- fill(noise(t, (mouseX + x) * .05, y * .05));
2908
- square(x, y, 5);
2909
- }
2910
- }
2911
- };
2912
2870
  */
2913
2871
  function noise(x?: number, y?: number, z?: number): number;
2914
2872
 
@@ -3764,17 +3722,6 @@ await load('/assets/Robotica.ttf');
3764
3722
  background(255);
3765
3723
  textSize(24);
3766
3724
  text('Hello, world!', 16, 100);
3767
- * @example
3768
- let q = await Q5.WebGPU();
3769
- createCanvas(200);
3770
-
3771
- let [jump, retro] = await load(
3772
- '/assets/jump.wav', '/assets/retro.flac'
3773
- );
3774
-
3775
- q.mousePressed = () => {
3776
- mouseButton == 'left' ? jump.play() : retro.play();
3777
- };
3778
3725
  */
3779
3726
  function load(...urls: string[]): Promise<any[]>;
3780
3727
 
package/q5.js CHANGED
@@ -3104,8 +3104,8 @@ Q5.modules.dom = ($, q) => {
3104
3104
  el.type = 'range';
3105
3105
  el.min = min;
3106
3106
  el.max = max;
3107
- el.value = value;
3108
3107
  el.step = step;
3108
+ el.value = value;
3109
3109
  el.val = () => parseFloat(el.value);
3110
3110
  return el;
3111
3111
  };
@@ -3204,6 +3204,7 @@ Q5.modules.fes = ($) => {
3204
3204
 
3205
3205
  let errFile = stackLines[idx].split(sep).at(-1);
3206
3206
  if (errFile.startsWith('blob:')) errFile = errFile.slice(5);
3207
+ errFile = errFile.split(')')[0];
3207
3208
  let parts = errFile.split(':');
3208
3209
  let lineNum = parseInt(parts.at(-2));
3209
3210
  parts[parts.length - 1] = parts.at(-1).split(')')[0];
@@ -3256,24 +3257,24 @@ Q5.modules.fes = ($) => {
3256
3257
  });
3257
3258
  }
3258
3259
  }
3259
- };
3260
3260
 
3261
- if (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
- }
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
+ }
3274
3274
 
3275
- checkLatestVersion();
3276
- }
3275
+ checkLatestVersion();
3276
+ }
3277
+ };
3277
3278
  Q5.modules.input = ($, q) => {
3278
3279
  if ($._isGraphics) return;
3279
3280
 
@@ -4581,7 +4582,7 @@ Q5.modules.util = ($, q) => {
4581
4582
  return res.text();
4582
4583
  })
4583
4584
  .then((f) => {
4584
- if (type == 'csv') f = $.CSV.parse(f);
4585
+ if (type == 'csv') f = Q5.CSV.parse(f);
4585
4586
  if (typeof f == 'string') ret.text = f;
4586
4587
  else Object.assign(ret, f);
4587
4588
  delete ret.promise;
@@ -4687,21 +4688,6 @@ Q5.modules.util = ($, q) => {
4687
4688
  } else saveFile(a);
4688
4689
  };
4689
4690
 
4690
- $.CSV = {};
4691
- $.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
4692
- if (!csv.length) return [];
4693
- let a = [],
4694
- lns = csv.split(lineSep),
4695
- headers = lns[0].split(sep).map((h) => h.replaceAll('"', ''));
4696
- for (let i = 1; i < lns.length; i++) {
4697
- let o = {},
4698
- ln = lns[i].split(sep);
4699
- headers.forEach((h, i) => (o[h] = JSON.parse(ln[i])));
4700
- a.push(o);
4701
- }
4702
- return a;
4703
- };
4704
-
4705
4691
  if ($.canvas && !Q5._createServerCanvas) {
4706
4692
  $.canvas.save = $.saveCanvas = $.save;
4707
4693
  }
@@ -4738,6 +4724,21 @@ Q5.modules.util = ($, q) => {
4738
4724
  return a;
4739
4725
  };
4740
4726
  };
4727
+
4728
+ Q5.CSV = {};
4729
+ Q5.CSV.parse = (csv, sep = ',', lineSep = '\n') => {
4730
+ if (!csv.length) return [];
4731
+ let a = [],
4732
+ lns = csv.split(lineSep),
4733
+ headers = lns[0].split(sep).map((h) => h.replaceAll('"', ''));
4734
+ for (let i = 1; i < lns.length; i++) {
4735
+ let o = {},
4736
+ ln = lns[i].split(sep);
4737
+ headers.forEach((h, i) => (o[h] = JSON.parse(ln[i])));
4738
+ a.push(o);
4739
+ }
4740
+ return a;
4741
+ };
4741
4742
  Q5.modules.vector = ($) => {
4742
4743
  $.Vector = Q5.Vector;
4743
4744
  $.createVector = (x, y, z) => new $.Vector(x, y, z, $);
@@ -5081,6 +5082,8 @@ struct Q5 {
5081
5082
  frameLayout,
5082
5083
  frameSampler,
5083
5084
  frameBindGroup,
5085
+ frameBindGroupA,
5086
+ frameBindGroupB,
5084
5087
  colorIndex = 2,
5085
5088
  colorStackIndex = 12,
5086
5089
  prevFramePL = 0,
@@ -5238,6 +5241,25 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5238
5241
 
5239
5242
  // Create a pipeline for rendering frames
5240
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
+ });
5241
5263
  };
5242
5264
 
5243
5265
  $._createCanvas = (w, h, opt) => {
@@ -5618,6 +5640,9 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5618
5640
  $.popStyles();
5619
5641
  };
5620
5642
 
5643
+ // Reusable array for calcBox to avoid GC
5644
+ let boxCache = [0, 0, 0, 0];
5645
+
5621
5646
  const calcBox = (x, y, w, h, mode) => {
5622
5647
  // left, right, top, bottom
5623
5648
  let l, r, t, b;
@@ -5641,7 +5666,11 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5641
5666
  b = h;
5642
5667
  }
5643
5668
 
5644
- return [l, r, t, b];
5669
+ boxCache[0] = l;
5670
+ boxCache[1] = r;
5671
+ boxCache[2] = t;
5672
+ boxCache[3] = b;
5673
+ return boxCache;
5645
5674
  };
5646
5675
 
5647
5676
  // prettier-ignore
@@ -5735,11 +5764,15 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5735
5764
  };
5736
5765
 
5737
5766
  $._beginRender = () => {
5738
- // swap the frame textures
5767
+ // swap the frame textures and bind groups
5739
5768
  const temp = frameA;
5740
5769
  frameA = frameB;
5741
5770
  frameB = temp;
5742
5771
 
5772
+ const tempBindGroup = frameBindGroupA;
5773
+ frameBindGroupA = frameBindGroupB;
5774
+ frameBindGroupB = tempBindGroup;
5775
+
5743
5776
  encoder = Q5.device.createCommandEncoder();
5744
5777
 
5745
5778
  $._pass = pass = encoder.beginRenderPass({
@@ -5755,14 +5788,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5755
5788
  ]
5756
5789
  });
5757
5790
 
5758
- frameBindGroup = Q5.device.createBindGroup({
5759
- layout: frameLayout,
5760
- entries: [
5761
- { binding: 0, resource: { buffer: uniformBuffer } },
5762
- { binding: 1, resource: frameSampler },
5763
- { binding: 2, resource: frameB.createView() }
5764
- ]
5765
- });
5791
+ // Use pre-created bind group instead of creating new one
5792
+ frameBindGroup = frameBindGroupB;
5766
5793
 
5767
5794
  if (!shouldClear) {
5768
5795
  pass.setPipeline($._pipelines[prevFramePL]);
@@ -5773,6 +5800,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5773
5800
  };
5774
5801
 
5775
5802
  let transformsBuffer, colorsBuffer, shapesVertBuff, imgVertBuff, charBuffer, textBuffer;
5803
+ let mainBindGroup, lastTransformsBuffer, lastColorsBuffer;
5776
5804
 
5777
5805
  $._render = () => {
5778
5806
  let transformsSize = matrices.length * MATRIX_SIZE * 4; // 4 bytes per float
@@ -5797,32 +5825,36 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5797
5825
 
5798
5826
  Q5.device.queue.writeBuffer(colorsBuffer, 0, colorStack.subarray(0, colorStackIndex));
5799
5827
 
5800
- $._uniforms = [
5801
- $.width,
5802
- $.height,
5803
- $.halfWidth,
5804
- $.halfHeight,
5805
- $._pixelDensity,
5806
- $.frameCount,
5807
- performance.now(),
5808
- $.deltaTime,
5809
- $.mouseX,
5810
- $.mouseY,
5811
- $.mouseIsPressed ? 1 : 0,
5812
- $.keyCode,
5813
- $.keyIsPressed ? 1 : 0
5814
- ];
5815
-
5816
- Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array($._uniforms));
5817
-
5818
- let mainBindGroup = Q5.device.createBindGroup({
5819
- layout: mainLayout,
5820
- entries: [
5821
- { binding: 0, resource: { buffer: uniformBuffer } },
5822
- { binding: 1, resource: { buffer: transformsBuffer } },
5823
- { binding: 2, resource: { buffer: colorsBuffer } }
5824
- ]
5825
- });
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
+ }
5826
5858
 
5827
5859
  pass.setBindGroup(0, mainBindGroup);
5828
5860
 
@@ -5870,11 +5902,13 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5870
5902
  // prepare to render text
5871
5903
 
5872
5904
  if (charStack.length) {
5873
- // Calculate total buffer size for text data
5874
- let totalTextSize = 0;
5905
+ // Flatten char data into reusable buffer instead of creating new array
5906
+ let charOffset = 0;
5875
5907
  for (let charsData of charStack) {
5876
- totalTextSize += charsData.length * 4;
5908
+ charDataBuffer.set(charsData, charOffset);
5909
+ charOffset += charsData.length;
5877
5910
  }
5911
+ let totalTextSize = charOffset * 4;
5878
5912
 
5879
5913
  if (!charBuffer || charBuffer.size < totalTextSize) {
5880
5914
  if (charBuffer) charBuffer.destroy();
@@ -5884,10 +5918,16 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5884
5918
  });
5885
5919
  }
5886
5920
 
5887
- 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;
5888
5930
 
5889
- // Calculate total buffer size for metadata
5890
- let totalMetadataSize = textStack.length * 8 * 4;
5891
5931
  if (!textBuffer || textBuffer.size < totalMetadataSize) {
5892
5932
  if (textBuffer) textBuffer.destroy();
5893
5933
  textBuffer = Q5.device.createBuffer({
@@ -5897,7 +5937,7 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
5897
5937
  });
5898
5938
  }
5899
5939
 
5900
- Q5.device.queue.writeBuffer(textBuffer, 0, new Float32Array(textStack.flat()));
5940
+ Q5.device.queue.writeBuffer(textBuffer, 0, textDataBuffer.buffer, 0, totalMetadataSize);
5901
5941
 
5902
5942
  // create a single bind group for the text buffer and metadata buffer
5903
5943
  $._textBindGroup = Q5.device.createBindGroup({
@@ -6015,14 +6055,8 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
6015
6055
  ]
6016
6056
  });
6017
6057
 
6018
- frameBindGroup = Q5.device.createBindGroup({
6019
- layout: frameLayout,
6020
- entries: [
6021
- { binding: 0, resource: { buffer: uniformBuffer } },
6022
- { binding: 1, resource: frameSampler },
6023
- { binding: 2, resource: frameA.createView() }
6024
- ]
6025
- });
6058
+ // Use pre-created bind group instead of creating new one
6059
+ frameBindGroup = frameBindGroupA;
6026
6060
 
6027
6061
  pass.setPipeline($._pipelines[framePL]);
6028
6062
  pass.setBindGroup(0, frameBindGroup);
@@ -6034,11 +6068,11 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
6034
6068
  $._pass = pass = encoder = null;
6035
6069
 
6036
6070
  // clear the stacks for the next frame
6037
- drawStack.splice(0, drawStack.length);
6071
+ drawStack.length = 0; // faster than splice for clearing
6038
6072
  colorIndex = 2;
6039
6073
  colorStackIndex = 12;
6040
- matrices = [matrices[0]];
6041
- matricesIdxStack = [];
6074
+ matrices.length = 1; // keep first matrix, clear rest
6075
+ matricesIdxStack.length = 0;
6042
6076
 
6043
6077
  // frameA can now be saved when saveCanvas is run
6044
6078
  $._texture = frameA;
@@ -6046,13 +6080,15 @@ fn fragMain(f: FragParams ) -> @location(0) vec4f {
6046
6080
  // reset
6047
6081
  shapesVertIdx = 0;
6048
6082
  imgVertIdx = 0;
6049
- $._textureBindGroups.splice(tIdx, vidFrames);
6083
+ // Remove video frames without creating new array
6084
+ if (vidFrames > 0) {
6085
+ $._textureBindGroups.length = tIdx;
6086
+ }
6050
6087
  vidFrames = 0;
6051
- charStack = [];
6052
- textStack = [];
6053
- 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
6054
6091
  rectStackIdx = 0;
6055
- ellipseStack = new Float32Array(Q5.MAX_ELLIPSES * 16);
6056
6092
  ellipseStackIdx = 0;
6057
6093
 
6058
6094
  // destroy buffers
@@ -6624,6 +6660,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6624
6660
  $.rectMode = (x) => (_rectMode = x);
6625
6661
  $._getRectMode = () => _rectMode;
6626
6662
 
6663
+ // Reusable array for rect mode calculations
6664
+ let rectModeCache = [0, 0, 0, 0];
6665
+
6627
6666
  function applyRectMode(x, y, w, h) {
6628
6667
  let hw = w / 2,
6629
6668
  hh = h / 2;
@@ -6641,7 +6680,11 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6641
6680
  y += hh;
6642
6681
  }
6643
6682
  }
6644
- return [x, y, hw, hh];
6683
+ rectModeCache[0] = x;
6684
+ rectModeCache[1] = y;
6685
+ rectModeCache[2] = hw;
6686
+ rectModeCache[3] = hh;
6687
+ return rectModeCache;
6645
6688
  }
6646
6689
 
6647
6690
  $.rect = (x, y, w, h, rr = 0) => {
@@ -6919,6 +6962,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6919
6962
  $.ellipseMode = (x) => (_ellipseMode = x);
6920
6963
  $._getEllipseMode = () => _ellipseMode;
6921
6964
 
6965
+ // Reusable array for ellipse mode calculations
6966
+ let ellipseModeCache = [0, 0, 0, 0];
6967
+
6922
6968
  function applyEllipseMode(x, y, w, h) {
6923
6969
  h ??= w;
6924
6970
  let a, b;
@@ -6939,7 +6985,11 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
6939
6985
  a = (w - x) / 2;
6940
6986
  b = (h - y) / 2;
6941
6987
  }
6942
- return [x, y, a, b];
6988
+ ellipseModeCache[0] = x;
6989
+ ellipseModeCache[1] = y;
6990
+ ellipseModeCache[2] = a;
6991
+ ellipseModeCache[3] = b;
6992
+ return ellipseModeCache;
6943
6993
  }
6944
6994
 
6945
6995
  $.ellipse = (x, y, w, h) => {
@@ -7336,6 +7386,9 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7336
7386
  $.imageMode = (x) => (_imageMode = x);
7337
7387
  $._getImageMode = () => _imageMode;
7338
7388
 
7389
+ // Reusable uniform buffer array to avoid GC
7390
+ $._uniforms = new Float32Array(13);
7391
+
7339
7392
  const addImgVert = (x, y, u, v, ci, ti, ia) => {
7340
7393
  let s = imgVertStack,
7341
7394
  i = imgVertIdx;
@@ -7350,6 +7403,7 @@ fn fragMain(f: FragParams) -> @location(0) vec4f {
7350
7403
  };
7351
7404
 
7352
7405
  $.image = (img, dx = 0, dy = 0, dw, dh, sx = 0, sy = 0, sw, sh) => {
7406
+ if (!img) return;
7353
7407
  let isVideo;
7354
7408
  if (img._texture == undefined) {
7355
7409
  isVideo = img.tagName == 'VIDEO';
@@ -7679,8 +7733,8 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7679
7733
  // chars and kernings can be stored as csv strings, making the file
7680
7734
  // size smaller, but they need to be parsed into arrays of objects
7681
7735
  if (typeof atlas.chars == 'string') {
7682
- atlas.chars = $.CSV.parse(atlas.chars, ' ');
7683
- atlas.kernings = $.CSV.parse(atlas.kernings, ' ');
7736
+ atlas.chars = Q5.CSV.parse(atlas.chars, ' ');
7737
+ atlas.kernings = Q5.CSV.parse(atlas.kernings, ' ');
7684
7738
  }
7685
7739
 
7686
7740
  let charCount = atlas.chars.length;
@@ -7764,7 +7818,7 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7764
7818
  $._loadDefaultFont = (fontName, cb) => {
7765
7819
  fonts[fontName] = null;
7766
7820
  let url = `https://q5js.org/fonts/${fontName}-msdf.json`;
7767
- if (!navigator.onLine) {
7821
+ if (Q5.online == false || !navigator.onLine) {
7768
7822
  url = `/node_modules/q5/builtinFonts/${fontName}-msdf.json`;
7769
7823
  }
7770
7824
  return $.loadFont(url, cb);
@@ -7837,13 +7891,20 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7837
7891
  let charStack = [],
7838
7892
  textStack = [];
7839
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
+
7840
7901
  let measureText = (font, text, charCallback) => {
7841
7902
  let maxWidth = 0,
7842
7903
  offsetX = 0,
7843
7904
  offsetY = 0,
7844
7905
  line = 0,
7845
7906
  printedCharCount = 0,
7846
- lineWidths = [],
7907
+ lineWidths = lineWidthsCache, // reuse array
7847
7908
  nextCharCode = text.charCodeAt(0);
7848
7909
 
7849
7910
  for (let i = 0; i < text.length; ++i) {
@@ -7874,12 +7935,14 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7874
7935
  printedCharCount++;
7875
7936
  }
7876
7937
  }
7877
- lineWidths.push(offsetX);
7938
+ lineWidths[line] = offsetX;
7878
7939
  maxWidth = Math.max(maxWidth, offsetX);
7940
+ let lineCount = line + 1;
7879
7941
  return {
7880
7942
  width: maxWidth,
7881
- height: lineWidths.length * font.lineHeight * leadPercent,
7943
+ height: lineCount * font.lineHeight * leadPercent,
7882
7944
  lineWidths,
7945
+ lineCount,
7883
7946
  printedCharCount
7884
7947
  };
7885
7948
  };
@@ -7888,9 +7951,11 @@ fn fragMain(f : FragParams) -> @location(0) vec4f {
7888
7951
  if (!$._font) {
7889
7952
  // if the default font hasn't been loaded yet, try to load it
7890
7953
  if ($._font !== null) $.textFont('sans-serif');
7891
- return;
7954
+ if (_textSize >= 1) return $.textImage(str, x, y, w, h);
7892
7955
  }
7893
7956
 
7957
+ if (_textSize < 1) return;
7958
+
7894
7959
  let type = typeof str;
7895
7960
  if (type != 'string') {
7896
7961
  if (type == 'object') str = str.toString();