q5 2.2.1 → 2.2.3

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/q5.js CHANGED
@@ -239,7 +239,7 @@ function Q5(scope, parent, renderer) {
239
239
  raf($._draw);
240
240
  }
241
241
 
242
- if ((arguments.length && scope != 'namespace') || preloadDefined) {
242
+ if ((arguments.length && scope != 'namespace' && renderer != 'webgpu') || preloadDefined) {
243
243
  $.preload();
244
244
  _start();
245
245
  } else {
@@ -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
 
@@ -1728,13 +1735,20 @@ Q5.modules.color = ($, q) => {
1728
1735
  c0 = parseInt(c0.slice(1, 3), 16);
1729
1736
  }
1730
1737
  } else if ($._namedColors[c0]) {
1731
- c0 = $._namedColors[c0];
1738
+ [c0, c1, c2, c3] = $._namedColors[c0];
1732
1739
  } else {
1733
1740
  console.error(
1734
1741
  "q5 can't parse color: " + c0 + '\nOnly numeric input, hex, and common named colors are supported.'
1735
1742
  );
1736
1743
  return new C(0, 0, 0);
1737
1744
  }
1745
+
1746
+ if ($._colorFormat == 1) {
1747
+ c0 /= 255;
1748
+ if (c1) c1 /= 255;
1749
+ if (c2) c2 /= 255;
1750
+ if (c3) c3 /= 255;
1751
+ }
1738
1752
  }
1739
1753
  if (Array.isArray(c0)) {
1740
1754
  c1 = c0[1];
@@ -1744,13 +1758,6 @@ Q5.modules.color = ($, q) => {
1744
1758
  }
1745
1759
  }
1746
1760
 
1747
- if ($._colorFormat == 1) {
1748
- c0 /= 255;
1749
- if (c1) c1 /= 255;
1750
- if (c2) c2 /= 255;
1751
- if (c3) c3 /= 255;
1752
- }
1753
-
1754
1761
  if (c2 == undefined) return new C(c0, c0, c0, c1);
1755
1762
  return new C(c0, c1, c2, c3);
1756
1763
  };
@@ -1760,8 +1767,29 @@ Q5.modules.color = ($, q) => {
1760
1767
  $.blue = (c) => c.b;
1761
1768
  $.alpha = (c) => c.a;
1762
1769
  $.lightness = (c) => {
1770
+ if ($._colorMode == 'oklch') return c.l;
1763
1771
  return ((0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) * 100) / 255;
1764
1772
  };
1773
+ $.hue = (c) => {
1774
+ if ($._colorMode == 'oklch') return c.h;
1775
+ let r = c.r;
1776
+ let g = c.g;
1777
+ let b = c.b;
1778
+ if ($._colorFormat == 255) {
1779
+ r /= 255;
1780
+ g /= 255;
1781
+ b /= 255;
1782
+ }
1783
+ let max = Math.max(r, g, b);
1784
+ let min = Math.min(r, g, b);
1785
+ let h;
1786
+ if (max == min) h = 0;
1787
+ else if (max == r) h = (60 * (g - b)) / (max - min);
1788
+ else if (max == g) h = (60 * (b - r)) / (max - min) + 120;
1789
+ else h = (60 * (r - g)) / (max - min) + 240;
1790
+ if (h < 0) h += 360;
1791
+ return h;
1792
+ };
1765
1793
 
1766
1794
  $.lerpColor = (a, b, t) => {
1767
1795
  if ($._colorMode == 'rgb') {
@@ -2216,33 +2244,21 @@ Q5.modules.math = ($, q) => {
2216
2244
  $.norm = (value, start, stop) => $.map(value, start, stop, 0, 1);
2217
2245
  $.sq = (x) => x * x;
2218
2246
  $.fract = (x) => x - Math.floor(x);
2219
- $.sin = (a) => {
2220
- if ($._angleMode == 'degrees') a = $.radians(a);
2221
- return Math.sin(a);
2222
- };
2223
- $.cos = (a) => {
2224
- if ($._angleMode == 'degrees') a = $.radians(a);
2225
- return Math.cos(a);
2226
- };
2227
- $.tan = (a) => {
2228
- if ($._angleMode == 'degrees') a = $.radians(a);
2229
- return Math.tan(a);
2230
- };
2231
- $.asin = (x) => {
2232
- let a = Math.asin(x);
2233
- if ($._angleMode == 'degrees') a = $.degrees(a);
2234
- return a;
2235
- };
2236
- $.acos = (x) => {
2237
- let a = Math.acos(x);
2238
- if ($._angleMode == 'degrees') a = $.degrees(a);
2239
- return a;
2240
- };
2241
- $.atan = (x) => {
2242
- let a = Math.atan(x);
2243
- if ($._angleMode == 'degrees') a = $.degrees(a);
2244
- return a;
2245
- };
2247
+
2248
+ for (let fn of ['sin', 'cos', 'tan']) {
2249
+ $[fn] = (a) => {
2250
+ if ($._angleMode == 'degrees') a = $.radians(a);
2251
+ return Math[fn](a);
2252
+ };
2253
+ }
2254
+
2255
+ for (let fn of ['asin', 'acos', 'atan']) {
2256
+ $[fn] = (x) => {
2257
+ let a = Math[fn](x);
2258
+ if ($._angleMode == 'degrees') a = $.degrees(a);
2259
+ return a;
2260
+ };
2261
+ }
2246
2262
  $.atan2 = (y, x) => {
2247
2263
  let a = Math.atan2(y, x);
2248
2264
  if ($._angleMode == 'degrees') a = $.degrees(a);
@@ -2906,7 +2922,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
2906
2922
 
2907
2923
  if ($.colorMode) $.colorMode('rgb', 'float');
2908
2924
 
2909
- let colorsStack, envBindGroup, transformBindGroup;
2925
+ let pass, colorsStack, envBindGroup, transformBindGroup;
2910
2926
 
2911
2927
  $._createCanvas = (w, h, opt) => {
2912
2928
  q.ctx = q.drawingContext = c.getContext('webgpu');
@@ -3079,7 +3095,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3079
3095
  $._beginRender = () => {
3080
3096
  $.encoder = Q5.device.createCommandEncoder();
3081
3097
 
3082
- q.pass = $.encoder.beginRenderPass({
3098
+ pass = q.pass = $.encoder.beginRenderPass({
3083
3099
  colorAttachments: [
3084
3100
  {
3085
3101
  view: ctx.getCurrentTexture().createView(),
@@ -3091,7 +3107,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3091
3107
  };
3092
3108
 
3093
3109
  $._render = () => {
3094
- $.pass.setBindGroup(0, envBindGroup);
3110
+ pass.setBindGroup(0, envBindGroup);
3095
3111
 
3096
3112
  if (transformStates.length > 1 || !transformBindGroup) {
3097
3113
  const transformBuffer = Q5.device.createBuffer({
@@ -3114,24 +3130,31 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3114
3130
  });
3115
3131
  }
3116
3132
 
3117
- $.pass.setBindGroup(1, transformBindGroup);
3133
+ pass.setBindGroup(1, transformBindGroup);
3118
3134
 
3119
3135
  // run pre-render methods
3120
3136
  for (let m of $._hooks.preRender) m();
3121
3137
 
3122
- $.pass.setPipeline($.pipelines[0]);
3123
-
3124
3138
  // local variables used for performance
3125
3139
  let drawStack = $.drawStack;
3126
- let o = 0; // vertex offset
3127
- for (let i = 0; i < drawStack.length; i++) {
3128
- $.pass.draw(drawStack[i], 1, o, 0);
3129
- o += drawStack[i];
3140
+
3141
+ let drawVertOffset = 0;
3142
+ let curPipelineIndex = -1;
3143
+
3144
+ for (let i = 0; i < drawStack.length; i += 2) {
3145
+ if (curPipelineIndex != drawStack[i]) {
3146
+ curPipelineIndex = drawStack[i];
3147
+ pass.setPipeline($.pipelines[curPipelineIndex]);
3148
+ }
3149
+
3150
+ let vertCount = drawStack[i + 1];
3151
+ pass.draw(vertCount, 1, drawVertOffset, 0);
3152
+ drawVertOffset += vertCount;
3130
3153
  }
3131
3154
  };
3132
3155
 
3133
3156
  $._finishRender = () => {
3134
- $.pass.end();
3157
+ pass.end();
3135
3158
  const commandBuffer = $.encoder.finish();
3136
3159
  Q5.device.queue.submit([commandBuffer]);
3137
3160
  q.pass = $.encoder = null;
@@ -3149,7 +3172,7 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3149
3172
  $._transformIndexStack.length = 0;
3150
3173
  };
3151
3174
 
3152
- $.fill = (r, g, b, a = 1) => {
3175
+ function addColor(r, g, b, a = 1) {
3153
3176
  if (typeof r == 'string') r = Q5.color(r);
3154
3177
  // grayscale mode `fill(1, 0.5)`
3155
3178
  if (b == undefined) {
@@ -3159,17 +3182,32 @@ Q5.renderers.webgpu.canvas = ($, q) => {
3159
3182
  if (r._q5Color) colorsStack.push(...r.levels);
3160
3183
  else colorsStack.push(r, g, b, a);
3161
3184
  $._colorIndex++;
3185
+ }
3186
+
3187
+ $.fill = function () {
3188
+ addColor(...arguments);
3189
+ $._doFill = true;
3190
+ $._fillIndex = $._colorIndex;
3191
+ };
3192
+ $.stroke = function () {
3193
+ addColor(...arguments);
3194
+ $._doStroke = true;
3195
+ $._fillIndex = $._colorIndex;
3162
3196
  };
3163
- $.noFill = () => colorsStack.push(0, 0, 0, 0);
3164
- $.stroke = () => {};
3165
- $.noStroke = () => {};
3197
+
3198
+ $.noFill = () => ($._doFill = false);
3199
+ $.noStroke = () => ($._doStroke = false);
3200
+
3201
+ $._strokeWeight = 1;
3202
+ $.strokeWeight = (v) => ($._strokeWeight = v);
3166
3203
 
3167
3204
  $.clear = () => {};
3168
3205
  };
3169
3206
 
3170
3207
  Q5.webgpu = async function (scope, parent) {
3208
+ if (!scope || scope == 'global') Q5._hasGlobal = true;
3171
3209
  if (!navigator.gpu) {
3172
- console.error('q5 WebGPU not supported on this browser!');
3210
+ console.warn('q5 WebGPU not supported on this browser!');
3173
3211
  let q = new Q5(scope, parent);
3174
3212
  q.colorMode('rgb', 1);
3175
3213
  q._beginRender = () => q.translate(q.canvas.hw, q.canvas.hh);
@@ -3272,16 +3310,9 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3272
3310
  fragment: {
3273
3311
  module: fragmentShader,
3274
3312
  entryPoint: 'fragmentMain',
3275
- targets: [
3276
- {
3277
- format: 'bgra8unorm',
3278
- blend: blendConfig
3279
- }
3280
- ]
3313
+ targets: [{ format: 'bgra8unorm', blend: blendConfig }]
3281
3314
  },
3282
- primitive: {
3283
- topology: 'triangle-list'
3284
- }
3315
+ primitive: { topology: 'triangle-list' }
3285
3316
  });
3286
3317
  };
3287
3318
 
@@ -3395,7 +3426,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3395
3426
  }
3396
3427
 
3397
3428
  verticesStack.push(...triangles);
3398
- drawStack.push(triangles.length / 4);
3429
+ drawStack.push(0, triangles.length / 4);
3399
3430
  shapeVertices = [];
3400
3431
  };
3401
3432
 
@@ -3420,33 +3451,22 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3420
3451
  if ($._matrixDirty) $._saveMatrix();
3421
3452
  let ti = $._transformIndex;
3422
3453
  // two triangles make a rectangle
3454
+ // prettier-ignore
3423
3455
  verticesStack.push(
3424
- left,
3425
- top,
3426
- ci,
3427
- ti,
3428
- right,
3429
- top,
3430
- ci,
3431
- ti,
3432
- left,
3433
- bottom,
3434
- ci,
3435
- ti,
3436
- right,
3437
- top,
3438
- ci,
3439
- ti,
3440
- left,
3441
- bottom,
3442
- ci,
3443
- ti,
3444
- right,
3445
- bottom,
3446
- ci,
3447
- ti
3456
+ left, top, ci, ti,
3457
+ right, top, ci, ti,
3458
+ left, bottom, ci, ti,
3459
+ right, top, ci, ti,
3460
+ left, bottom, ci, ti,
3461
+ right, bottom, ci, ti
3448
3462
  );
3449
- drawStack.push(6);
3463
+ drawStack.push(0, 6);
3464
+ };
3465
+
3466
+ $.point = (x, y) => {
3467
+ let sw = $._strokeWeight;
3468
+ if (sw == 1) $.rect(x, y, 1, 1);
3469
+ else $.ellipse(x, y, sw, sw);
3450
3470
  };
3451
3471
 
3452
3472
  $.background = () => {};
@@ -3511,7 +3531,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3511
3531
  verticesStack.push(x, y, ci, ti, vx1, vy1, ci, ti, vx2, vy2, ci, ti);
3512
3532
  }
3513
3533
 
3514
- drawStack.push(n * 3);
3534
+ drawStack.push(0, n * 3);
3515
3535
  };
3516
3536
 
3517
3537
  $.circle = (x, y, d) => $.ellipse(x, y, d, d);
@@ -3550,5 +3570,191 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3550
3570
  $.pass.setBindGroup(2, colorsBindGroup);
3551
3571
  });
3552
3572
  };
3553
- Q5.renderers.webgpu.image = ($, q) => {};
3573
+ Q5.renderers.webgpu.image = ($, q) => {
3574
+ $.imageStack = [];
3575
+ $.textures = [];
3576
+
3577
+ $._hooks.postCanvas.push(() => {
3578
+ let imageVertexShader = Q5.device.createShaderModule({
3579
+ code: `
3580
+ struct VertexOutput {
3581
+ @builtin(position) position: vec4<f32>,
3582
+ @location(0) texCoord: vec2<f32>,
3583
+ @location(1) textureIndex: f32
3584
+ };
3585
+
3586
+ struct Uniforms {
3587
+ halfWidth: f32,
3588
+ halfHeight: f32
3589
+ };
3590
+
3591
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3592
+ @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3593
+
3594
+ @vertex
3595
+ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) texCoord: vec2<f32>, @location(2) transformIndex: f32, @location(3) textureIndex: f32) -> VertexOutput {
3596
+ var vert = vec4<f32>(pos, 0.0, 1.0);
3597
+ vert *= transforms[i32(transformIndex)];
3598
+ vert.x /= uniforms.halfWidth;
3599
+ vert.y /= uniforms.halfHeight;
3600
+
3601
+ var output: VertexOutput;
3602
+ output.position = vert;
3603
+ output.texCoord = texCoord;
3604
+ output.textureIndex = textureIndex;
3605
+ return output;
3606
+ }
3607
+ `
3608
+ });
3609
+
3610
+ let imageFragmentShader = Q5.device.createShaderModule({
3611
+ code: `
3612
+ @group(0) @binding(0) var samp: sampler;
3613
+ @group(0) @binding(1) var textures: array<texture_2d<f32>>;
3614
+
3615
+ @fragment
3616
+ fn fragmentMain(@location(0) texCoord: vec2<f32>, @location(1) textureIndex: f32) -> @location(0) vec4<f32> {
3617
+ return textureSample(textures[i32(textureIndex)], samp, texCoord);
3618
+ }
3619
+ `
3620
+ });
3621
+
3622
+ const bindGroupLayouts = [
3623
+ Q5.device.createBindGroupLayout({
3624
+ entries: [
3625
+ { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
3626
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
3627
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: '2d', sampleType: 'float' } }
3628
+ ]
3629
+ }),
3630
+ Q5.device.createBindGroupLayout({
3631
+ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }]
3632
+ })
3633
+ ];
3634
+
3635
+ const pipelineLayout = Q5.device.createPipelineLayout({
3636
+ bindGroupLayouts: bindGroupLayouts
3637
+ });
3638
+
3639
+ $.pipelines[1] = Q5.device.createRenderPipeline({
3640
+ layout: pipelineLayout,
3641
+ vertex: {
3642
+ module: imageVertexShader,
3643
+ entryPoint: 'vertexMain',
3644
+ buffers: [
3645
+ {
3646
+ arrayStride: 5 * 4, // 4 floats for position and texCoord, 1 float for textureIndex
3647
+ attributes: [
3648
+ { shaderLocation: 0, offset: 0, format: 'float32x2' },
3649
+ { shaderLocation: 1, offset: 2 * 4, format: 'float32x2' },
3650
+ { shaderLocation: 2, offset: 4 * 4, format: 'float32' } // textureIndex
3651
+ ]
3652
+ }
3653
+ ]
3654
+ },
3655
+ fragment: {
3656
+ module: imageFragmentShader,
3657
+ entryPoint: 'fragmentMain',
3658
+ targets: [{ format: 'bgra8unorm' }]
3659
+ },
3660
+ primitive: {
3661
+ topology: 'triangle-list'
3662
+ }
3663
+ });
3664
+
3665
+ $.sampler = Q5.device.createSampler({
3666
+ magFilter: 'linear',
3667
+ minFilter: 'linear'
3668
+ });
3669
+ });
3670
+
3671
+ $.loadImage = async (src) => {
3672
+ const img = new Image();
3673
+ img.onload = async () => {
3674
+ const imageBitmap = await createImageBitmap(img);
3675
+ const texture = Q5.device.createTexture({
3676
+ size: [img.width, img.height, 1],
3677
+ format: 'bgra8unorm',
3678
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
3679
+ });
3680
+
3681
+ Q5.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [img.width, img.height, 1]);
3682
+
3683
+ img.texture = texture;
3684
+ img.index = $.textures.length;
3685
+ $.textures.push(texture);
3686
+ };
3687
+ img.onerror = reject;
3688
+ img.src = src;
3689
+ return img;
3690
+ };
3691
+
3692
+ $._hooks.preRender.push(() => {
3693
+ if (!$.imageStack.length) return;
3694
+
3695
+ // Switch to image pipeline
3696
+ $.pass.setPipeline($.pipelines[1]);
3697
+
3698
+ // Create a vertex buffer for the image quads
3699
+ const vertices = new Float32Array($.vertexStack);
3700
+
3701
+ const vertexBuffer = Q5.device.createBuffer({
3702
+ size: vertices.byteLength,
3703
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
3704
+ });
3705
+
3706
+ Q5.device.queue.writeBuffer(vertexBuffer, 0, vertices);
3707
+ $.pass.setVertexBuffer(0, vertexBuffer);
3708
+
3709
+ // Set the bind group for the sampler and textures
3710
+ if ($.textures.length !== previousTextureCount) {
3711
+ previousTextureCount = $.textures.length;
3712
+
3713
+ // Create the bind group for all textures
3714
+ const textureViews = $.textures.map((tex) => tex.createView());
3715
+
3716
+ $.textureBindGroup = Q5.device.createBindGroup({
3717
+ layout: $.pipelines[1].getBindGroupLayout(0),
3718
+ entries: [
3719
+ { binding: 0, resource: $.sampler },
3720
+ ...textureViews.map((view, i) => ({ binding: i + 1, resource: view }))
3721
+ ]
3722
+ });
3723
+ }
3724
+
3725
+ // Set the bind group for the sampler and textures
3726
+ $.pass.setBindGroup(0, $.textureBindGroup);
3727
+ });
3728
+
3729
+ $.image = (img, x, y, w, h) => {
3730
+ if ($._matrixDirty) $._saveMatrix();
3731
+ let ti = $._transformIndex;
3732
+
3733
+ $.imageStack.push(img.index);
3734
+
3735
+ // Calculate half-width and half-height
3736
+ let hw = w / 2;
3737
+ let hh = h / 2;
3738
+
3739
+ // Calculate vertices positions
3740
+ let left = x - hw;
3741
+ let right = x + hw;
3742
+ let top = -(y - hh); // y is inverted in WebGPU
3743
+ let bottom = -(y + hh);
3744
+
3745
+ let ii = img.index;
3746
+
3747
+ // prettier-ignore
3748
+ $.vertexStack.push(
3749
+ left, top, 0, 0, ti, ii,
3750
+ right, top, 1, 0, ti, ii,
3751
+ left, bottom, 0, 1, ti, ii,
3752
+ right, top, 1, 0, ti, ii,
3753
+ left, bottom, 0, 1, ti, ii,
3754
+ right, bottom, 1, 1, ti, ii
3755
+ );
3756
+
3757
+ $.drawStack.push(6);
3758
+ };
3759
+ };
3554
3760
  Q5.renderers.webgpu.text = ($, q) => {};