q5 2.2.2 → 2.2.4

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/README.md CHANGED
@@ -339,13 +339,13 @@ Unminified:
339
339
 
340
340
  - p5.js **5112kb** ⚠️
341
341
  - p5.sound.js 488kb
342
- - q5.js 83kb
342
+ - q5.js 90kb
343
343
 
344
344
  Minified:
345
345
 
346
346
  - p5.min.js 1034kb ⚠️
347
347
  - p5.sound.min.js 200kb
348
- - q5.min.js **54kb** 🎉
348
+ - q5.min.js **58kb** 🎉
349
349
 
350
350
  ## Benchmarks
351
351
 
@@ -410,6 +410,9 @@ https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline
410
410
  ziggurat:
411
411
  http://ziggurat.glitch.me/
412
412
 
413
+ Vector.slerp:
414
+ https://github.com/processing/p5.js/blob/v1.10.0/src/math/p5.Vector.js#L2803
415
+
413
416
  random:
414
417
  https://github.com/processing/p5.js/blob/1.1.9/src/math/noise.js
415
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "q5",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "A sequel to p5.js that's smaller and faster",
5
5
  "author": "quinton-ashley",
6
6
  "contributors": [
@@ -12,9 +12,8 @@
12
12
  "main": "q5-server.js",
13
13
  "scripts": {
14
14
  "bundle": "cat src/q5-core.js src/q5-canvas.js src/q5-2d-canvas.js src/q5-2d-drawing.js src/q5-2d-image.js src/q5-2d-text.js src/q5-ai.js src/q5-color.js src/q5-display.js src/q5-input.js src/q5-math.js src/q5-sound.js src/q5-util.js src/q5-vector.js src/q5-webgpu-canvas.js src/q5-webgpu-drawing.js src/q5-webgpu-image.js src/q5-webgpu-text.js > q5.js",
15
- "bundle-q2d": "cat src/q5-core.js src/q5-canvas.js src/q5-2d-canvas.js src/q5-2d-drawing.js src/q5-2d-image.js src/q5-2d-soft-filters.js src/q5-2d-text.js src/q5-ai.js src/q5-color.js src/q5-display.js src/q5-input.js src/q5-math.js src/q5-sound.js src/q5-util.js src/q5-vector.js > q5-q2d.js",
16
- "min": "terser q5.js --compress ecma=2024 --mangle > q5.min.js && terser q5-q2d.js --compress ecma=2024 --mangle > q5-q2d.min.js",
17
- "dist": "bun bundle && bun bundle-q2d && bun min",
15
+ "min": "terser q5.js --compress ecma=2024 --mangle > q5.min.js",
16
+ "dist": "bun bundle && bun min",
18
17
  "dist-p5play": "bun dist && cp q5.js ../../web/p5play-web/v3/q5.js && cp q5.min.js ../../web/p5play-web/v3/q5.min.js",
19
18
  "v": "npm version patch --force",
20
19
  "V": "npm version minor --force",
package/q5.js CHANGED
@@ -219,7 +219,7 @@ function Q5(scope, parent, renderer) {
219
219
  return t[k]();
220
220
  } catch (e) {
221
221
  if ($._aiErrorAssistance) $._aiErrorAssistance(e);
222
- else console.error(e);
222
+ throw e;
223
223
  }
224
224
  };
225
225
  }
@@ -551,7 +551,12 @@ Q5.renderers.q2d.canvas = ($, q) => {
551
551
  $.ctx.lineWidth = n || 0.0001;
552
552
  };
553
553
  $.noStroke = () => ($._doStroke = false);
554
- $.clear = () => $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
554
+ $.clear = () => {
555
+ $.ctx.save();
556
+ $.ctx.resetTransform();
557
+ $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
558
+ $.ctx.restore();
559
+ };
555
560
 
556
561
  // DRAWING MATRIX
557
562
 
@@ -717,15 +722,17 @@ Q5.renderers.q2d.drawing = ($) => {
717
722
  // DRAWING
718
723
 
719
724
  $.background = function (c) {
720
- if (c.canvas) return $.image(c, 0, 0, $.width, $.height);
721
725
  $.ctx.save();
722
726
  $.ctx.resetTransform();
723
- if (Q5.Color) {
724
- if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
725
- else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
727
+ if (c.canvas) $.image(c, 0, 0, $.width, $.height);
728
+ else {
729
+ if (Q5.Color) {
730
+ if (!c._q5Color && typeof c != 'string') c = $.color(...arguments);
731
+ else if ($._namedColors[c]) c = $.color(...$._namedColors[c]);
732
+ }
733
+ $.ctx.fillStyle = c.toString();
734
+ $.ctx.fillRect(0, 0, $.canvas.width, $.canvas.height);
726
735
  }
727
- $.ctx.fillStyle = c.toString();
728
- $.ctx.fillRect(0, 0, $.canvas.width, $.canvas.height);
729
736
  $.ctx.restore();
730
737
  };
731
738
 
@@ -1596,8 +1603,7 @@ Q5.modules.ai = ($) => {
1596
1603
 
1597
1604
  $._aiErrorAssistance = async (e) => {
1598
1605
  let askAI = e.message?.includes('Ask AI ✨');
1599
- if (!askAI) console.error(e);
1600
- if (Q5.disableFriendlyErrors) return;
1606
+ if (Q5.disableFriendlyErrors && !askAI) return;
1601
1607
  if (askAI || !Q5.errorTolerant) $.noLoop();
1602
1608
  let stackLines = e.stack?.split('\n');
1603
1609
  if (!e.stack || stackLines.length <= 1) return;
@@ -1608,7 +1614,7 @@ Q5.modules.ai = ($) => {
1608
1614
  idx = 0;
1609
1615
  sep = '@';
1610
1616
  }
1611
- while (stackLines[idx].indexOf('q5.js:') >= 0) idx++;
1617
+ while (stackLines[idx].indexOf('q5') >= 0) idx++;
1612
1618
 
1613
1619
  let parts = stackLines[idx].split(sep).at(-1);
1614
1620
  parts = parts.split(':');
@@ -1646,11 +1652,11 @@ Q5.modules.ai = ($) => {
1646
1652
  '%0A%0AExcerpt+for+context%3A%0A%0A' +
1647
1653
  encodeURIComponent(context);
1648
1654
 
1649
- if (!askAI) console.log('Error in ' + fileBase + ' on line ' + lineNum + ':\n\n' + errLine);
1655
+ console.warn('Error in ' + fileBase + ' on line ' + lineNum + ':\n\n' + errLine);
1650
1656
 
1651
1657
  console.warn('Ask AI ✨ ' + url);
1652
1658
 
1653
- if (askAI) window.open(url, '_blank');
1659
+ if (askAI) return window.open(url, '_blank');
1654
1660
  } catch (err) {}
1655
1661
  };
1656
1662
  };
@@ -1728,27 +1734,22 @@ Q5.modules.color = ($, q) => {
1728
1734
  c0 = parseInt(c0.slice(1, 3), 16);
1729
1735
  }
1730
1736
  } else if ($._namedColors[c0]) {
1731
- c0 = $._namedColors[c0];
1737
+ [c0, c1, c2, c3] = $._namedColors[c0];
1732
1738
  } else {
1733
1739
  console.error(
1734
1740
  "q5 can't parse color: " + c0 + '\nOnly numeric input, hex, and common named colors are supported.'
1735
1741
  );
1736
1742
  return new C(0, 0, 0);
1737
1743
  }
1738
- }
1739
- if (Array.isArray(c0)) {
1740
- c1 = c0[1];
1741
- c2 = c0[2];
1742
- c3 = c0[3];
1743
- c0 = c0[0];
1744
- }
1745
- }
1746
1744
 
1747
- if ($._colorFormat == 1) {
1748
- c0 /= 255;
1749
- if (c1) c1 /= 255;
1750
- if (c2) c2 /= 255;
1751
- if (c3) c3 /= 255;
1745
+ if ($._colorFormat == 1) {
1746
+ c0 /= 255;
1747
+ if (c1) c1 /= 255;
1748
+ if (c2) c2 /= 255;
1749
+ if (c3) c3 /= 255;
1750
+ }
1751
+ }
1752
+ if (Array.isArray(c0)) [c0, c1, c2, c3] = c0;
1752
1753
  }
1753
1754
 
1754
1755
  if (c2 == undefined) return new C(c0, c0, c0, c1);
@@ -1760,11 +1761,11 @@ Q5.modules.color = ($, q) => {
1760
1761
  $.blue = (c) => c.b;
1761
1762
  $.alpha = (c) => c.a;
1762
1763
  $.lightness = (c) => {
1763
- if ($._colorMode == 'oklch') return c.l;
1764
+ if (c.l) return c.l;
1764
1765
  return ((0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) * 100) / 255;
1765
1766
  };
1766
1767
  $.hue = (c) => {
1767
- if ($._colorMode == 'oklch') return c.h;
1768
+ if (c.h) return c.h;
1768
1769
  let r = c.r;
1769
1770
  let g = c.g;
1770
1771
  let b = c.b;
@@ -2680,18 +2681,19 @@ Q5.modules.vector = ($) => {
2680
2681
  };
2681
2682
 
2682
2683
  Q5.Vector = class {
2683
- constructor(_x, _y, _z, _$) {
2684
- this.x = _x || 0;
2685
- this.y = _y || 0;
2686
- this.z = _z || 0;
2687
- this._$ = _$ || window;
2684
+ constructor(x, y, z, $) {
2685
+ this.x = x || 0;
2686
+ this.y = y || 0;
2687
+ this.z = z || 0;
2688
+ this._$ = $ || window;
2688
2689
  this._cn = null;
2689
2690
  this._cnsq = null;
2690
2691
  }
2691
- set(_x, _y, _z) {
2692
- this.x = _x || 0;
2693
- this.y = _y || 0;
2694
- this.z = _z || 0;
2692
+ set(x, y, z) {
2693
+ this.x = x?.x || x || 0;
2694
+ this.y = x?.y || y || 0;
2695
+ this.z = x?.z || z || 0;
2696
+ return this;
2695
2697
  }
2696
2698
  copy() {
2697
2699
  return new Q5.Vector(this.x, this.y, this.z);
@@ -2813,6 +2815,12 @@ Q5.Vector = class {
2813
2815
  heading() {
2814
2816
  return this._$.atan2(this.y, this.x);
2815
2817
  }
2818
+ setHeading(ang) {
2819
+ let mag = this.mag(); // Calculate the magnitude of the vector
2820
+ this.x = mag * this._$.cos(ang); // Set the new x component
2821
+ this.y = mag * this._$.sin(ang); // Set the new y component
2822
+ return this;
2823
+ }
2816
2824
  rotate(ang) {
2817
2825
  let costh = this._$.cos(ang);
2818
2826
  let sinth = this._$.sin(ang);
@@ -2830,13 +2838,52 @@ Q5.Vector = class {
2830
2838
  }
2831
2839
  lerp() {
2832
2840
  let args = [...arguments];
2833
- let u = this._arg2v(...args.slice(0, -1));
2834
2841
  let amt = args.at(-1);
2842
+ if (amt == 0) return this;
2843
+ let u = this._arg2v(...args.slice(0, -1));
2835
2844
  this.x += (u.x - this.x) * amt;
2836
2845
  this.y += (u.y - this.y) * amt;
2837
2846
  this.z += (u.z - this.z) * amt;
2838
2847
  return this;
2839
2848
  }
2849
+ slerp() {
2850
+ let args = [...arguments];
2851
+ let amt = args.at(-1);
2852
+ if (amt == 0) return this;
2853
+ let u = this._arg2v(...args.slice(0, -1));
2854
+ if (amt == 1) return this.set(u);
2855
+
2856
+ let v0Mag = this.mag();
2857
+ let v1Mag = u.mag();
2858
+
2859
+ if (v0Mag == 0 || v1Mag == 0) {
2860
+ return this.mult(1 - amt).add(u.mult(amt));
2861
+ }
2862
+
2863
+ let axis = Q5.Vector.cross(this, u);
2864
+ let axisMag = axis.mag();
2865
+ let theta = Math.atan2(axisMag, this.dot(u));
2866
+
2867
+ if (axisMag > 0) {
2868
+ axis.div(axisMag);
2869
+ } else if (theta < this._$.HALF_PI) {
2870
+ return this.mult(1 - amt).add(u.mult(amt));
2871
+ } else {
2872
+ if (this.z == 0 && u.z == 0) axis.set(0, 0, 1);
2873
+ else if (this.x != 0) axis.set(this.y, -this.x, 0).normalize();
2874
+ else axis.set(1, 0, 0);
2875
+ }
2876
+
2877
+ let ey = axis.cross(this);
2878
+ let lerpedMagFactor = 1 - amt + (amt * v1Mag) / v0Mag;
2879
+ let cosMultiplier = lerpedMagFactor * Math.cos(amt * theta);
2880
+ let sinMultiplier = lerpedMagFactor * Math.sin(amt * theta);
2881
+
2882
+ this.x = this.x * cosMultiplier + ey.x * sinMultiplier;
2883
+ this.y = this.y * cosMultiplier + ey.y * sinMultiplier;
2884
+ this.z = this.z * cosMultiplier + ey.z * sinMultiplier;
2885
+ return this;
2886
+ }
2840
2887
  reflect(n) {
2841
2888
  n.normalize();
2842
2889
  return this.sub(n.mult(2 * this.dot(n)));
@@ -2889,6 +2936,7 @@ Q5.Vector.div = (v, u) => v.copy().div(u);
2889
2936
  Q5.Vector.dot = (v, u) => v.copy().dot(u);
2890
2937
  Q5.Vector.equals = (v, u, epsilon) => v.equals(u, epsilon);
2891
2938
  Q5.Vector.lerp = (v, u, amt) => v.copy().lerp(u, amt);
2939
+ Q5.Vector.slerp = (v, u, amt) => v.copy().slerp(u, amt);
2892
2940
  Q5.Vector.limit = (v, m) => v.copy().limit(m);
2893
2941
  Q5.Vector.heading = (v) => this._$.atan2(v.y, v.x);
2894
2942
  Q5.Vector.magSq = (v) => v.x * v.x + v.y * v.y + v.z * v.z;
@@ -3200,7 +3248,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3200
3248
  Q5.webgpu = async function (scope, parent) {
3201
3249
  if (!scope || scope == 'global') Q5._hasGlobal = true;
3202
3250
  if (!navigator.gpu) {
3203
- console.error('q5 WebGPU not supported on this browser!');
3251
+ console.warn('q5 WebGPU not supported on this browser!');
3204
3252
  let q = new Q5(scope, parent);
3205
3253
  q.colorMode('rgb', 1);
3206
3254
  q._beginRender = () => q.translate(q.canvas.hw, q.canvas.hh);
@@ -3563,5 +3611,190 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3563
3611
  $.pass.setBindGroup(2, colorsBindGroup);
3564
3612
  });
3565
3613
  };
3566
- Q5.renderers.webgpu.image = ($, q) => {};
3614
+ Q5.renderers.webgpu.image = ($, q) => {
3615
+ $.imageStack = [];
3616
+ $.textures = [];
3617
+
3618
+ $._hooks.postCanvas.push(() => {
3619
+ let imageVertexShader = Q5.device.createShaderModule({
3620
+ code: `
3621
+ struct VertexOutput {
3622
+ @builtin(position) position: vec4<f32>,
3623
+ @location(0) texCoord: vec2<f32>,
3624
+ @location(1) textureIndex: f32
3625
+ };
3626
+
3627
+ struct Uniforms {
3628
+ halfWidth: f32,
3629
+ halfHeight: f32
3630
+ };
3631
+
3632
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3633
+ @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3634
+
3635
+ @vertex
3636
+ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @location(2) transformIndex: f32, @location(3) textureIndex: f32) -> VertexOutput {
3637
+ var vert = vec4<f32>(pos, 0.0, 1.0);
3638
+ vert *= transforms[i32(transformIndex)];
3639
+ vert.x /= uniforms.halfWidth;
3640
+ vert.y /= uniforms.halfHeight;
3641
+
3642
+ var output: VertexOutput;
3643
+ output.position = vert;
3644
+ output.texCoord = texCoord;
3645
+ output.textureIndex = textureIndex;
3646
+ return output;
3647
+ }
3648
+ `
3649
+ });
3650
+
3651
+ let imageFragmentShader = Q5.device.createShaderModule({
3652
+ code: `
3653
+ @group(0) @binding(0) var samp: sampler;
3654
+ @group(0) @binding(1) var textures: array<texture_2d<f32>>;
3655
+
3656
+ @fragment
3657
+ fn fragmentMain(@location(0) texCoord: vec2<f32>, @location(1) textureIndex: f32) -> @location(0) vec4<f32> {
3658
+ return textureSample(textures[i32(textureIndex)], samp, texCoord);
3659
+ }
3660
+ `
3661
+ });
3662
+
3663
+ const bindGroupLayouts = [
3664
+ Q5.device.createBindGroupLayout({
3665
+ entries: [
3666
+ { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
3667
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
3668
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: '2d', sampleType: 'float' } }
3669
+ ]
3670
+ }),
3671
+ Q5.device.createBindGroupLayout({
3672
+ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }]
3673
+ })
3674
+ ];
3675
+
3676
+ const pipelineLayout = Q5.device.createPipelineLayout({
3677
+ bindGroupLayouts: bindGroupLayouts
3678
+ });
3679
+
3680
+ $.pipelines[1] = Q5.device.createRenderPipeline({
3681
+ layout: pipelineLayout,
3682
+ vertex: {
3683
+ module: imageVertexShader,
3684
+ entryPoint: 'vertexMain',
3685
+ buffers: [
3686
+ {
3687
+ arrayStride: 5 * 4, // 4 floats for position and texCoord, 1 float for textureIndex
3688
+ attributes: [
3689
+ { shaderLocation: 0, offset: 0, format: 'float32x2' },
3690
+ { shaderLocation: 1, offset: 2 * 4, format: 'float32x2' },
3691
+ { shaderLocation: 2, offset: 4 * 4, format: 'float32' } // textureIndex
3692
+ ]
3693
+ }
3694
+ ]
3695
+ },
3696
+ fragment: {
3697
+ module: imageFragmentShader,
3698
+ entryPoint: 'fragmentMain',
3699
+ targets: [{ format: 'bgra8unorm' }]
3700
+ },
3701
+ primitive: {
3702
+ topology: 'triangle-list'
3703
+ }
3704
+ });
3705
+
3706
+ $.sampler = Q5.device.createSampler({
3707
+ magFilter: 'linear',
3708
+ minFilter: 'linear'
3709
+ });
3710
+ });
3711
+
3712
+ $.loadImage = async (src) => {
3713
+ const img = new Image();
3714
+ img.onload = async () => {
3715
+ const imageBitmap = await createImageBitmap(img);
3716
+ const texture = Q5.device.createTexture({
3717
+ size: [img.width, img.height, 1],
3718
+ format: 'bgra8unorm',
3719
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
3720
+ });
3721
+
3722
+ Q5.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [img.width, img.height, 1]);
3723
+
3724
+ img.texture = texture;
3725
+ img.index = $.textures.length;
3726
+ $.textures.push(texture);
3727
+ };
3728
+ img.src = src;
3729
+ return img;
3730
+ };
3731
+
3732
+ $._hooks.preRender.push(() => {
3733
+ if (!$.imageStack.length) return;
3734
+
3735
+ // Switch to image pipeline
3736
+ $.pass.setPipeline($.pipelines[1]);
3737
+
3738
+ // Create a vertex buffer for the image quads
3739
+ const vertices = new Float32Array($.vertexStack);
3740
+
3741
+ const vertexBuffer = Q5.device.createBuffer({
3742
+ size: vertices.byteLength,
3743
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
3744
+ });
3745
+
3746
+ Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
3747
+ $.pass.setVertexBuffer(0, vertexBuffer);
3748
+
3749
+ // Set the bind group for the sampler and textures
3750
+ if ($.textures.length !== previousTextureCount) {
3751
+ previousTextureCount = $.textures.length;
3752
+
3753
+ // Create the bind group for all textures
3754
+ const textureViews = $.textures.map((tex) => tex.createView());
3755
+
3756
+ $.textureBindGroup = Q5.device.createBindGroup({
3757
+ layout: $.pipelines[1].getBindGroupLayout(0),
3758
+ entries: [
3759
+ { binding: 0, resource: $.sampler },
3760
+ ...textureViews.map((view, i) => ({ binding: i + 1, resource: view }))
3761
+ ]
3762
+ });
3763
+ }
3764
+
3765
+ // Set the bind group for the sampler and textures
3766
+ $.pass.setBindGroup(0, $.textureBindGroup);
3767
+ });
3768
+
3769
+ $.image = (img, x, y, w, h) => {
3770
+ if ($._matrixDirty) $._saveMatrix();
3771
+ let ti = $._transformIndex;
3772
+
3773
+ $.imageStack.push(img.index);
3774
+
3775
+ // Calculate half-width and half-height
3776
+ let hw = w / 2;
3777
+ let hh = h / 2;
3778
+
3779
+ // Calculate vertices positions
3780
+ let left = x - hw;
3781
+ let right = x + hw;
3782
+ let top = -(y - hh); // y is inverted in WebGPU
3783
+ let bottom = -(y + hh);
3784
+
3785
+ let ii = img.index;
3786
+
3787
+ // prettier-ignore
3788
+ $.vertexStack.push(
3789
+ left, top, 0, 0, ti, ii,
3790
+ right, top, 1, 0, ti, ii,
3791
+ left, bottom, 0, 1, ti, ii,
3792
+ right, top, 1, 0, ti, ii,
3793
+ left, bottom, 0, 1, ti, ii,
3794
+ right, bottom, 1, 1, ti, ii
3795
+ );
3796
+
3797
+ $.drawStack.push(6);
3798
+ };
3799
+ };
3567
3800
  Q5.renderers.webgpu.text = ($, q) => {};