q5 2.2.0 → 2.2.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.
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 {
@@ -1376,182 +1376,6 @@ Q5.POSTERIZE = 5;
1376
1376
  Q5.DILATE = 6;
1377
1377
  Q5.ERODE = 7;
1378
1378
  Q5.BLUR = 8;
1379
- /* software implementation of image filters */
1380
- Q5.renderers.q2d.soft_filters = ($) => {
1381
- let tmpBuf = null;
1382
-
1383
- function ensureTmpBuf() {
1384
- let l = $.canvas.width * $.canvas.height * 4;
1385
- if (!tmpBuf || tmpBuf.length != l) {
1386
- tmpBuf = new Uint8ClampedArray(l);
1387
- }
1388
- }
1389
-
1390
- function initSoftFilters() {
1391
- $._filters = [];
1392
- $._filters[Q5.THRESHOLD] = (data, thresh) => {
1393
- if (thresh === undefined) thresh = 127.5;
1394
- else thresh *= 255;
1395
- for (let i = 0; i < data.length; i += 4) {
1396
- const gray = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
1397
- data[i] = data[i + 1] = data[i + 2] = gray >= thresh ? 255 : 0;
1398
- }
1399
- };
1400
- $._filters[Q5.GRAY] = (data) => {
1401
- for (let i = 0; i < data.length; i += 4) {
1402
- const gray = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
1403
- data[i] = data[i + 1] = data[i + 2] = gray;
1404
- }
1405
- };
1406
- $._filters[Q5.OPAQUE] = (data) => {
1407
- for (let i = 0; i < data.length; i += 4) {
1408
- data[i + 3] = 255;
1409
- }
1410
- };
1411
- $._filters[Q5.INVERT] = (data) => {
1412
- for (let i = 0; i < data.length; i += 4) {
1413
- data[i] = 255 - data[i];
1414
- data[i + 1] = 255 - data[i + 1];
1415
- data[i + 2] = 255 - data[i + 2];
1416
- }
1417
- };
1418
- $._filters[Q5.POSTERIZE] = (data, lvl = 4) => {
1419
- let lvl1 = lvl - 1;
1420
- for (let i = 0; i < data.length; i += 4) {
1421
- data[i] = (((data[i] * lvl) >> 8) * 255) / lvl1;
1422
- data[i + 1] = (((data[i + 1] * lvl) >> 8) * 255) / lvl1;
1423
- data[i + 2] = (((data[i + 2] * lvl) >> 8) * 255) / lvl1;
1424
- }
1425
- };
1426
- $._filters[Q5.DILATE] = (data) => {
1427
- ensureTmpBuf();
1428
- tmpBuf.set(data);
1429
- let [w, h] = [$.canvas.width, $.canvas.height];
1430
- for (let i = 0; i < h; i++) {
1431
- for (let j = 0; j < w; j++) {
1432
- let l = 4 * Math.max(j - 1, 0);
1433
- let r = 4 * Math.min(j + 1, w - 1);
1434
- let t = 4 * Math.max(i - 1, 0) * w;
1435
- let b = 4 * Math.min(i + 1, h - 1) * w;
1436
- let oi = 4 * i * w;
1437
- let oj = 4 * j;
1438
- for (let k = 0; k < 4; k++) {
1439
- let kt = k + t;
1440
- let kb = k + b;
1441
- let ko = k + oi;
1442
- data[oi + oj + k] = Math.max(
1443
- /*tmpBuf[kt+l],*/ tmpBuf[kt + oj] /*tmpBuf[kt+r],*/,
1444
- tmpBuf[ko + l],
1445
- tmpBuf[ko + oj],
1446
- tmpBuf[ko + r],
1447
- /*tmpBuf[kb+l],*/ tmpBuf[kb + oj] /*tmpBuf[kb+r],*/
1448
- );
1449
- }
1450
- }
1451
- }
1452
- };
1453
- $._filters[Q5.ERODE] = (data) => {
1454
- ensureTmpBuf();
1455
- tmpBuf.set(data);
1456
- let [w, h] = [$.canvas.width, $.canvas.height];
1457
- for (let i = 0; i < h; i++) {
1458
- for (let j = 0; j < w; j++) {
1459
- let l = 4 * Math.max(j - 1, 0);
1460
- let r = 4 * Math.min(j + 1, w - 1);
1461
- let t = 4 * Math.max(i - 1, 0) * w;
1462
- let b = 4 * Math.min(i + 1, h - 1) * w;
1463
- let oi = 4 * i * w;
1464
- let oj = 4 * j;
1465
- for (let k = 0; k < 4; k++) {
1466
- let kt = k + t;
1467
- let kb = k + b;
1468
- let ko = k + oi;
1469
- data[oi + oj + k] = Math.min(
1470
- /*tmpBuf[kt+l],*/ tmpBuf[kt + oj] /*tmpBuf[kt+r],*/,
1471
- tmpBuf[ko + l],
1472
- tmpBuf[ko + oj],
1473
- tmpBuf[ko + r],
1474
- /*tmpBuf[kb+l],*/ tmpBuf[kb + oj] /*tmpBuf[kb+r],*/
1475
- );
1476
- }
1477
- }
1478
- }
1479
- };
1480
- $._filters[Q5.BLUR] = (data, rad) => {
1481
- rad = rad || 1;
1482
- rad = Math.floor(rad * $._pixelDensity);
1483
- ensureTmpBuf();
1484
- tmpBuf.set(data);
1485
-
1486
- let ksize = rad * 2 + 1;
1487
-
1488
- function gauss1d(ksize) {
1489
- let im = new Float32Array(ksize);
1490
- let sigma = 0.3 * rad + 0.8;
1491
- let ss2 = sigma * sigma * 2;
1492
- for (let i = 0; i < ksize; i++) {
1493
- let x = i - ksize / 2;
1494
- let z = Math.exp(-(x * x) / ss2) / (2.5066282746 * sigma);
1495
- im[i] = z;
1496
- }
1497
- return im;
1498
- }
1499
-
1500
- let kern = gauss1d(ksize);
1501
- let [w, h] = [$.canvas.width, $.canvas.height];
1502
- for (let i = 0; i < h; i++) {
1503
- for (let j = 0; j < w; j++) {
1504
- let s0 = 0,
1505
- s1 = 0,
1506
- s2 = 0,
1507
- s3 = 0;
1508
- for (let k = 0; k < ksize; k++) {
1509
- let jk = Math.min(Math.max(j - rad + k, 0), w - 1);
1510
- let idx = 4 * (i * w + jk);
1511
- s0 += tmpBuf[idx] * kern[k];
1512
- s1 += tmpBuf[idx + 1] * kern[k];
1513
- s2 += tmpBuf[idx + 2] * kern[k];
1514
- s3 += tmpBuf[idx + 3] * kern[k];
1515
- }
1516
- let idx = 4 * (i * w + j);
1517
- data[idx] = s0;
1518
- data[idx + 1] = s1;
1519
- data[idx + 2] = s2;
1520
- data[idx + 3] = s3;
1521
- }
1522
- }
1523
- tmpBuf.set(data);
1524
- for (let i = 0; i < h; i++) {
1525
- for (let j = 0; j < w; j++) {
1526
- let s0 = 0,
1527
- s1 = 0,
1528
- s2 = 0,
1529
- s3 = 0;
1530
- for (let k = 0; k < ksize; k++) {
1531
- let ik = Math.min(Math.max(i - rad + k, 0), h - 1);
1532
- let idx = 4 * (ik * w + j);
1533
- s0 += tmpBuf[idx] * kern[k];
1534
- s1 += tmpBuf[idx + 1] * kern[k];
1535
- s2 += tmpBuf[idx + 2] * kern[k];
1536
- s3 += tmpBuf[idx + 3] * kern[k];
1537
- }
1538
- let idx = 4 * (i * w + j);
1539
- data[idx] = s0;
1540
- data[idx + 1] = s1;
1541
- data[idx + 2] = s2;
1542
- data[idx + 3] = s3;
1543
- }
1544
- }
1545
- };
1546
- }
1547
-
1548
- $._softFilter = (typ, x) => {
1549
- if (!$._filters) initSoftFilters();
1550
- let imgData = $.ctx.getImageData(0, 0, $.canvas.width, $.canvas.height);
1551
- $._filters[typ](imgData.data, x);
1552
- $.ctx.putImageData(imgData, 0, 0);
1553
- };
1554
- };
1555
1379
  Q5.renderers.q2d.text = ($, q) => {
1556
1380
  $.NORMAL = 'normal';
1557
1381
  $.ITALIC = 'italic';
@@ -1936,8 +1760,29 @@ Q5.modules.color = ($, q) => {
1936
1760
  $.blue = (c) => c.b;
1937
1761
  $.alpha = (c) => c.a;
1938
1762
  $.lightness = (c) => {
1763
+ if ($._colorMode == 'oklch') return c.l;
1939
1764
  return ((0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) * 100) / 255;
1940
1765
  };
1766
+ $.hue = (c) => {
1767
+ if ($._colorMode == 'oklch') return c.h;
1768
+ let r = c.r;
1769
+ let g = c.g;
1770
+ let b = c.b;
1771
+ if ($._colorFormat == 255) {
1772
+ r /= 255;
1773
+ g /= 255;
1774
+ b /= 255;
1775
+ }
1776
+ let max = Math.max(r, g, b);
1777
+ let min = Math.min(r, g, b);
1778
+ let h;
1779
+ if (max == min) h = 0;
1780
+ else if (max == r) h = (60 * (g - b)) / (max - min);
1781
+ else if (max == g) h = (60 * (b - r)) / (max - min) + 120;
1782
+ else h = (60 * (r - g)) / (max - min) + 240;
1783
+ if (h < 0) h += 360;
1784
+ return h;
1785
+ };
1941
1786
 
1942
1787
  $.lerpColor = (a, b, t) => {
1943
1788
  if ($._colorMode == 'rgb') {
@@ -2392,33 +2237,21 @@ Q5.modules.math = ($, q) => {
2392
2237
  $.norm = (value, start, stop) => $.map(value, start, stop, 0, 1);
2393
2238
  $.sq = (x) => x * x;
2394
2239
  $.fract = (x) => x - Math.floor(x);
2395
- $.sin = (a) => {
2396
- if ($._angleMode == 'degrees') a = $.radians(a);
2397
- return Math.sin(a);
2398
- };
2399
- $.cos = (a) => {
2400
- if ($._angleMode == 'degrees') a = $.radians(a);
2401
- return Math.cos(a);
2402
- };
2403
- $.tan = (a) => {
2404
- if ($._angleMode == 'degrees') a = $.radians(a);
2405
- return Math.tan(a);
2406
- };
2407
- $.asin = (x) => {
2408
- let a = Math.asin(x);
2409
- if ($._angleMode == 'degrees') a = $.degrees(a);
2410
- return a;
2411
- };
2412
- $.acos = (x) => {
2413
- let a = Math.acos(x);
2414
- if ($._angleMode == 'degrees') a = $.degrees(a);
2415
- return a;
2416
- };
2417
- $.atan = (x) => {
2418
- let a = Math.atan(x);
2419
- if ($._angleMode == 'degrees') a = $.degrees(a);
2420
- return a;
2421
- };
2240
+
2241
+ for (let fn of ['sin', 'cos', 'tan']) {
2242
+ $[fn] = (a) => {
2243
+ if ($._angleMode == 'degrees') a = $.radians(a);
2244
+ return Math[fn](a);
2245
+ };
2246
+ }
2247
+
2248
+ for (let fn of ['asin', 'acos', 'atan']) {
2249
+ $[fn] = (x) => {
2250
+ let a = Math[fn](x);
2251
+ if ($._angleMode == 'degrees') a = $.degrees(a);
2252
+ return a;
2253
+ };
2254
+ }
2422
2255
  $.atan2 = (y, x) => {
2423
2256
  let a = Math.atan2(y, x);
2424
2257
  if ($._angleMode == 'degrees') a = $.degrees(a);
@@ -3067,3 +2900,668 @@ Q5.Vector.sub = (v, u) => v.copy().sub(u);
3067
2900
  for (let k of ['fromAngle', 'fromAngles', 'random2D', 'random3D']) {
3068
2901
  Q5.Vector[k] = (u, v, t) => new Q5.Vector()[k](u, v, t);
3069
2902
  }
2903
+ /**
2904
+ * q5-webgpu
2905
+ *
2906
+ * EXPERIMENTAL, for developer testing only!
2907
+ */
2908
+ Q5.renderers.webgpu = {};
2909
+
2910
+ Q5.renderers.webgpu.canvas = ($, q) => {
2911
+ let c = $.canvas;
2912
+
2913
+ c.width = $.width = 500;
2914
+ c.height = $.height = 500;
2915
+
2916
+ if ($.colorMode) $.colorMode('rgb', 'float');
2917
+
2918
+ let pass, colorsStack, envBindGroup, transformBindGroup;
2919
+
2920
+ $._createCanvas = (w, h, opt) => {
2921
+ q.ctx = q.drawingContext = c.getContext('webgpu');
2922
+
2923
+ opt.format = navigator.gpu.getPreferredCanvasFormat();
2924
+ opt.device = Q5.device;
2925
+
2926
+ $.ctx.configure(opt);
2927
+
2928
+ $.pipelines = [];
2929
+
2930
+ // pipeline changes for each draw call
2931
+ $.pipelinesStack = [];
2932
+
2933
+ // vertices for each draw call
2934
+ $.verticesStack = [];
2935
+
2936
+ // number of vertices for each draw call
2937
+ $.drawStack = [];
2938
+
2939
+ // colors used for each draw call
2940
+ colorsStack = $.colorsStack = [1, 1, 1, 1];
2941
+
2942
+ // current color index, used to associate a vertex with a color
2943
+ $._colorIndex = 0;
2944
+
2945
+ let envLayout = Q5.device.createBindGroupLayout({
2946
+ entries: [
2947
+ {
2948
+ binding: 0,
2949
+ visibility: GPUShaderStage.VERTEX,
2950
+ buffer: {
2951
+ type: 'uniform',
2952
+ hasDynamicOffset: false
2953
+ }
2954
+ }
2955
+ ]
2956
+ });
2957
+
2958
+ let transformLayout = Q5.device.createBindGroupLayout({
2959
+ entries: [
2960
+ {
2961
+ binding: 0,
2962
+ visibility: GPUShaderStage.VERTEX,
2963
+ buffer: {
2964
+ type: 'read-only-storage',
2965
+ hasDynamicOffset: false
2966
+ }
2967
+ }
2968
+ ]
2969
+ });
2970
+
2971
+ $.bindGroupLayouts = [envLayout, transformLayout];
2972
+
2973
+ const uniformBuffer = Q5.device.createBuffer({
2974
+ size: 8, // Size of two floats
2975
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
2976
+ });
2977
+
2978
+ Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
2979
+
2980
+ envBindGroup = Q5.device.createBindGroup({
2981
+ layout: envLayout,
2982
+ entries: [
2983
+ {
2984
+ binding: 0,
2985
+ resource: {
2986
+ buffer: uniformBuffer
2987
+ }
2988
+ }
2989
+ ]
2990
+ });
2991
+ };
2992
+
2993
+ $._resizeCanvas = (w, h) => {
2994
+ $._setCanvasSize(w, h);
2995
+ };
2996
+
2997
+ $.resetMatrix = () => {
2998
+ // Initialize the transformation matrix as 4x4 identity matrix
2999
+
3000
+ // prettier-ignore
3001
+ $._matrix = [
3002
+ 1, 0, 0, 0,
3003
+ 0, 1, 0, 0,
3004
+ 0, 0, 1, 0,
3005
+ 0, 0, 0, 1
3006
+ ];
3007
+ $._transformIndex = 0;
3008
+ };
3009
+ $.resetMatrix();
3010
+
3011
+ // Boolean to track if the matrix has been modified
3012
+ $._matrixDirty = false;
3013
+
3014
+ // Array to store transformation matrices for the render pass
3015
+ $.transformStates = [$._matrix.slice()];
3016
+
3017
+ // Stack to keep track of transformation matrix indexes
3018
+ $._transformIndexStack = [];
3019
+
3020
+ $.push = () => {
3021
+ // Push the current matrix index onto the stack
3022
+ $._transformIndexStack.push($._transformIndex);
3023
+ };
3024
+
3025
+ $.pop = () => {
3026
+ if ($._transformIndexStack.length > 0) {
3027
+ // Pop the last matrix index from the stack and set it as the current matrix index
3028
+ let idx = $._transformIndexStack.pop();
3029
+ $._matrix = $.transformStates[idx].slice();
3030
+ $._transformIndex = idx;
3031
+ } else {
3032
+ console.warn('Matrix index stack is empty!');
3033
+ }
3034
+ };
3035
+
3036
+ $.translate = (x, y, z) => {
3037
+ if (!x && !y && !z) return;
3038
+ // Update the translation values
3039
+ $._matrix[3] += x;
3040
+ $._matrix[7] += y;
3041
+ $._matrix[11] += z || 0;
3042
+ $._matrixDirty = true;
3043
+ };
3044
+
3045
+ $.rotate = (r) => {
3046
+ if (!r) return;
3047
+ if ($._angleMode == 'degrees') r = $.radians(r);
3048
+
3049
+ let cosR = Math.cos(r);
3050
+ let sinR = Math.sin(r);
3051
+
3052
+ let m0 = $._matrix[0],
3053
+ m1 = $._matrix[1],
3054
+ m4 = $._matrix[4],
3055
+ m5 = $._matrix[5];
3056
+ if (!m0 && !m1 && !m4 && !m5) {
3057
+ $._matrix[0] = cosR;
3058
+ $._matrix[1] = sinR;
3059
+ $._matrix[4] = -sinR;
3060
+ $._matrix[5] = cosR;
3061
+ } else {
3062
+ $._matrix[0] = m0 * cosR + m4 * sinR;
3063
+ $._matrix[1] = m1 * cosR + m5 * sinR;
3064
+ $._matrix[4] = m0 * -sinR + m4 * cosR;
3065
+ $._matrix[5] = m1 * -sinR + m5 * cosR;
3066
+ }
3067
+
3068
+ $._matrixDirty = true;
3069
+ };
3070
+
3071
+ $.scale = (sx = 1, sy, sz = 1) => {
3072
+ sy ??= sx;
3073
+
3074
+ $._matrix[0] *= sx;
3075
+ $._matrix[5] *= sy;
3076
+ $._matrix[10] *= sz;
3077
+
3078
+ $._matrixDirty = true;
3079
+ };
3080
+
3081
+ // Function to save the current matrix state if dirty
3082
+ $._saveMatrix = () => {
3083
+ $.transformStates.push($._matrix.slice());
3084
+ $._transformIndex = $.transformStates.length - 1;
3085
+ $._matrixDirty = false;
3086
+ };
3087
+
3088
+ $._beginRender = () => {
3089
+ $.encoder = Q5.device.createCommandEncoder();
3090
+
3091
+ pass = q.pass = $.encoder.beginRenderPass({
3092
+ colorAttachments: [
3093
+ {
3094
+ view: ctx.getCurrentTexture().createView(),
3095
+ loadOp: 'clear',
3096
+ storeOp: 'store'
3097
+ }
3098
+ ]
3099
+ });
3100
+ };
3101
+
3102
+ $._render = () => {
3103
+ pass.setBindGroup(0, envBindGroup);
3104
+
3105
+ if (transformStates.length > 1 || !transformBindGroup) {
3106
+ const transformBuffer = Q5.device.createBuffer({
3107
+ size: transformStates.length * 64, // Size of 16 floats
3108
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3109
+ });
3110
+
3111
+ Q5.device.queue.writeBuffer(transformBuffer, 0, new Float32Array(transformStates.flat()));
3112
+
3113
+ transformBindGroup = Q5.device.createBindGroup({
3114
+ layout: $.bindGroupLayouts[1],
3115
+ entries: [
3116
+ {
3117
+ binding: 0,
3118
+ resource: {
3119
+ buffer: transformBuffer
3120
+ }
3121
+ }
3122
+ ]
3123
+ });
3124
+ }
3125
+
3126
+ pass.setBindGroup(1, transformBindGroup);
3127
+
3128
+ // run pre-render methods
3129
+ for (let m of $._hooks.preRender) m();
3130
+
3131
+ // local variables used for performance
3132
+ let drawStack = $.drawStack;
3133
+
3134
+ let drawVertOffset = 0;
3135
+ let curPipelineIndex = -1;
3136
+
3137
+ for (let i = 0; i < drawStack.length; i += 2) {
3138
+ if (curPipelineIndex != drawStack[i]) {
3139
+ curPipelineIndex = drawStack[i];
3140
+ pass.setPipeline($.pipelines[curPipelineIndex]);
3141
+ }
3142
+
3143
+ let vertCount = drawStack[i + 1];
3144
+ pass.draw(vertCount, 1, drawVertOffset, 0);
3145
+ drawVertOffset += vertCount;
3146
+ }
3147
+ };
3148
+
3149
+ $._finishRender = () => {
3150
+ pass.end();
3151
+ const commandBuffer = $.encoder.finish();
3152
+ Q5.device.queue.submit([commandBuffer]);
3153
+ q.pass = $.encoder = null;
3154
+
3155
+ // clear the stacks for the next frame
3156
+ $.verticesStack.length = 0;
3157
+ $.drawStack.length = 0;
3158
+ $.colorsStack.length = 4;
3159
+ $.pipelinesStack.length = 0;
3160
+ $._colorIndex = 0;
3161
+ rotation = 0;
3162
+ $.resetMatrix();
3163
+ $._matrixDirty = false;
3164
+ $.transformStates.length = 1;
3165
+ $._transformIndexStack.length = 0;
3166
+ };
3167
+
3168
+ function addColor(r, g, b, a = 1) {
3169
+ if (typeof r == 'string') r = Q5.color(r);
3170
+ // grayscale mode `fill(1, 0.5)`
3171
+ if (b == undefined) {
3172
+ a = g;
3173
+ g = b = r;
3174
+ }
3175
+ if (r._q5Color) colorsStack.push(...r.levels);
3176
+ else colorsStack.push(r, g, b, a);
3177
+ $._colorIndex++;
3178
+ }
3179
+
3180
+ $.fill = function () {
3181
+ addColor(...arguments);
3182
+ $._doFill = true;
3183
+ $._fillIndex = $._colorIndex;
3184
+ };
3185
+ $.stroke = function () {
3186
+ addColor(...arguments);
3187
+ $._doStroke = true;
3188
+ $._fillIndex = $._colorIndex;
3189
+ };
3190
+
3191
+ $.noFill = () => ($._doFill = false);
3192
+ $.noStroke = () => ($._doStroke = false);
3193
+
3194
+ $._strokeWeight = 1;
3195
+ $.strokeWeight = (v) => ($._strokeWeight = v);
3196
+
3197
+ $.clear = () => {};
3198
+ };
3199
+
3200
+ Q5.webgpu = async function (scope, parent) {
3201
+ if (!scope || scope == 'global') Q5._hasGlobal = true;
3202
+ if (!navigator.gpu) {
3203
+ console.error('q5 WebGPU not supported on this browser!');
3204
+ let q = new Q5(scope, parent);
3205
+ q.colorMode('rgb', 1);
3206
+ q._beginRender = () => q.translate(q.canvas.hw, q.canvas.hh);
3207
+ return q;
3208
+ }
3209
+ let adapter = await navigator.gpu.requestAdapter();
3210
+ if (!adapter) throw new Error('No appropriate GPUAdapter found.');
3211
+ Q5.device = await adapter.requestDevice();
3212
+
3213
+ return new Q5(scope, parent, 'webgpu');
3214
+ };
3215
+ Q5.renderers.webgpu.drawing = ($, q) => {
3216
+ $.CLOSE = 1;
3217
+
3218
+ let verticesStack, drawStack, colorsStack;
3219
+
3220
+ $._hooks.postCanvas.push(() => {
3221
+ let colorsLayout = Q5.device.createBindGroupLayout({
3222
+ entries: [
3223
+ {
3224
+ binding: 0,
3225
+ visibility: GPUShaderStage.FRAGMENT,
3226
+ buffer: {
3227
+ type: 'read-only-storage',
3228
+ hasDynamicOffset: false
3229
+ }
3230
+ }
3231
+ ]
3232
+ });
3233
+
3234
+ $.bindGroupLayouts.push(colorsLayout);
3235
+
3236
+ verticesStack = $.verticesStack;
3237
+ drawStack = $.drawStack;
3238
+ colorsStack = $.colorsStack;
3239
+
3240
+ let vertexBufferLayout = {
3241
+ arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
3242
+ attributes: [
3243
+ { format: 'float32x2', offset: 0, shaderLocation: 0 }, // position
3244
+ { format: 'float32', offset: 8, shaderLocation: 1 }, // colorIndex
3245
+ { format: 'float32', offset: 12, shaderLocation: 2 } // transformIndex
3246
+ ]
3247
+ };
3248
+
3249
+ let vertexShader = Q5.device.createShaderModule({
3250
+ code: `
3251
+ struct VertexOutput {
3252
+ @builtin(position) position: vec4<f32>,
3253
+ @location(1) colorIndex: f32
3254
+ };
3255
+
3256
+ struct Uniforms {
3257
+ halfWidth: f32,
3258
+ halfHeight: f32
3259
+ };
3260
+
3261
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3262
+ @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3263
+
3264
+ @vertex
3265
+ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3266
+ var vert = vec4<f32>(pos, 0.0, 1.0);
3267
+ vert *= transforms[i32(transformIndex)];
3268
+ vert.x /= uniforms.halfWidth;
3269
+ vert.y /= uniforms.halfHeight;
3270
+
3271
+ var output: VertexOutput;
3272
+ output.position = vert;
3273
+ output.colorIndex = colorIndex;
3274
+ return output;
3275
+ }
3276
+ `
3277
+ });
3278
+
3279
+ let fragmentShader = Q5.device.createShaderModule({
3280
+ code: `
3281
+ @group(2) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
3282
+
3283
+ @fragment
3284
+ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3285
+ let index = u32(colorIndex);
3286
+ return mix(uColors[index], uColors[index + 1u], fract(colorIndex));
3287
+ }
3288
+ `
3289
+ });
3290
+
3291
+ let pipelineLayout = Q5.device.createPipelineLayout({
3292
+ bindGroupLayouts: $.bindGroupLayouts
3293
+ });
3294
+
3295
+ $._createPipeline = (blendConfig) => {
3296
+ return Q5.device.createRenderPipeline({
3297
+ layout: pipelineLayout,
3298
+ vertex: {
3299
+ module: vertexShader,
3300
+ entryPoint: 'vertexMain',
3301
+ buffers: [vertexBufferLayout]
3302
+ },
3303
+ fragment: {
3304
+ module: fragmentShader,
3305
+ entryPoint: 'fragmentMain',
3306
+ targets: [{ format: 'bgra8unorm', blend: blendConfig }]
3307
+ },
3308
+ primitive: { topology: 'triangle-list' }
3309
+ });
3310
+ };
3311
+
3312
+ $.pipelines[0] = $._createPipeline(blendConfigs.normal);
3313
+ });
3314
+
3315
+ // prettier-ignore
3316
+ let blendFactors = [
3317
+ 'zero', // 0
3318
+ 'one', // 1
3319
+ 'src-alpha', // 2
3320
+ 'one-minus-src-alpha', // 3
3321
+ 'dst', // 4
3322
+ 'dst-alpha', // 5
3323
+ 'one-minus-dst-alpha', // 6
3324
+ 'one-minus-src' // 7
3325
+ ];
3326
+ let blendOps = [
3327
+ 'add', // 0
3328
+ 'subtract', // 1
3329
+ 'reverse-subtract', // 2
3330
+ 'min', // 3
3331
+ 'max' // 4
3332
+ ];
3333
+
3334
+ const blendModes = {
3335
+ normal: [2, 3, 0, 2, 3, 0],
3336
+ lighter: [2, 1, 0, 2, 1, 0],
3337
+ subtract: [2, 1, 2, 2, 1, 2],
3338
+ multiply: [4, 0, 0, 5, 0, 0],
3339
+ screen: [1, 3, 0, 1, 3, 0],
3340
+ darken: [1, 3, 3, 1, 3, 3],
3341
+ lighten: [1, 3, 4, 1, 3, 4],
3342
+ overlay: [2, 3, 0, 2, 3, 0],
3343
+ hard_light: [2, 3, 0, 2, 3, 0],
3344
+ soft_light: [2, 3, 0, 2, 3, 0],
3345
+ difference: [2, 3, 2, 2, 3, 2],
3346
+ exclusion: [2, 3, 0, 2, 3, 0],
3347
+ color_dodge: [1, 7, 0, 1, 7, 0],
3348
+ color_burn: [6, 1, 0, 6, 1, 0],
3349
+ linear_dodge: [2, 1, 0, 2, 1, 0],
3350
+ linear_burn: [2, 7, 1, 2, 7, 1],
3351
+ vivid_light: [2, 7, 0, 2, 7, 0],
3352
+ pin_light: [2, 7, 0, 2, 7, 0],
3353
+ hard_mix: [2, 7, 0, 2, 7, 0]
3354
+ };
3355
+
3356
+ $.blendConfigs = {};
3357
+
3358
+ Object.entries(blendModes).forEach(([name, mode]) => {
3359
+ $.blendConfigs[name] = {
3360
+ color: {
3361
+ srcFactor: blendFactors[mode[0]],
3362
+ dstFactor: blendFactors[mode[1]],
3363
+ operation: blendOps[mode[2]]
3364
+ },
3365
+ alpha: {
3366
+ srcFactor: blendFactors[mode[3]],
3367
+ dstFactor: blendFactors[mode[4]],
3368
+ operation: blendOps[mode[5]]
3369
+ }
3370
+ };
3371
+ });
3372
+
3373
+ $._blendMode = 'normal';
3374
+ $.blendMode = (mode) => {
3375
+ if (mode == $._blendMode) return;
3376
+ if (mode == 'source-over') mode = 'normal';
3377
+ mode = mode.toLowerCase().replace(/[ -]/g, '_');
3378
+ $._blendMode = mode;
3379
+ $.pipelines[0] = $._createPipeline($.blendConfigs[mode]);
3380
+ };
3381
+
3382
+ let shapeVertices;
3383
+
3384
+ $.beginShape = () => {
3385
+ shapeVertices = [];
3386
+ };
3387
+
3388
+ $.vertex = (x, y) => {
3389
+ if ($._matrixDirty) $._saveMatrix();
3390
+ shapeVertices.push(x, -y, $._colorIndex, $._transformIndex);
3391
+ };
3392
+
3393
+ $.endShape = (close) => {
3394
+ let v = shapeVertices;
3395
+ if (v.length < 12) {
3396
+ throw new Error('A shape must have at least 3 vertices.');
3397
+ }
3398
+ if (close) {
3399
+ // Close the shape by adding the first vertex at the end
3400
+ v.push(v[0], v[1], v[2], v[3]);
3401
+ }
3402
+ // Convert the shape to triangles
3403
+ let triangles = [];
3404
+ for (let i = 4; i < v.length; i += 4) {
3405
+ triangles.push(
3406
+ v[0], // First vertex
3407
+ v[1],
3408
+ v[2],
3409
+ v[3],
3410
+ v[i - 4], // Previous vertex
3411
+ v[i - 3],
3412
+ v[i - 2],
3413
+ v[i - 1],
3414
+ v[i], // Current vertex
3415
+ v[i + 1],
3416
+ v[i + 2],
3417
+ v[i + 3]
3418
+ );
3419
+ }
3420
+
3421
+ verticesStack.push(...triangles);
3422
+ drawStack.push(0, triangles.length / 4);
3423
+ shapeVertices = [];
3424
+ };
3425
+
3426
+ $.triangle = (x1, y1, x2, y2, x3, y3) => {
3427
+ $.beginShape();
3428
+ $.vertex(x1, y1);
3429
+ $.vertex(x2, y2);
3430
+ $.vertex(x3, y3);
3431
+ $.endShape(1);
3432
+ };
3433
+
3434
+ $.rect = (x, y, w, h) => {
3435
+ let hw = w / 2;
3436
+ let hh = h / 2;
3437
+
3438
+ let left = x - hw;
3439
+ let right = x + hw;
3440
+ let top = -(y - hh); // y is inverted in WebGPU
3441
+ let bottom = -(y + hh);
3442
+
3443
+ let ci = $._colorIndex;
3444
+ if ($._matrixDirty) $._saveMatrix();
3445
+ let ti = $._transformIndex;
3446
+ // two triangles make a rectangle
3447
+ // prettier-ignore
3448
+ verticesStack.push(
3449
+ left, top, ci, ti,
3450
+ right, top, ci, ti,
3451
+ left, bottom, ci, ti,
3452
+ right, top, ci, ti,
3453
+ left, bottom, ci, ti,
3454
+ right, bottom, ci, ti
3455
+ );
3456
+ drawStack.push(0, 6);
3457
+ };
3458
+
3459
+ $.point = (x, y) => {
3460
+ let sw = $._strokeWeight;
3461
+ if (sw == 1) $.rect(x, y, 1, 1);
3462
+ else $.ellipse(x, y, sw, sw);
3463
+ };
3464
+
3465
+ $.background = () => {};
3466
+
3467
+ /**
3468
+ * Derived from: ceil(Math.log(d) * 7) * 2 - ceil(28)
3469
+ * This lookup table is used for better performance.
3470
+ * @param {Number} d diameter of the circle
3471
+ * @returns n number of segments
3472
+ */
3473
+ // prettier-ignore
3474
+ const getArcSegments = (d) =>
3475
+ d < 14 ? 8 :
3476
+ d < 16 ? 10 :
3477
+ d < 18 ? 12 :
3478
+ d < 20 ? 14 :
3479
+ d < 22 ? 16 :
3480
+ d < 24 ? 18 :
3481
+ d < 28 ? 20 :
3482
+ d < 34 ? 22 :
3483
+ d < 42 ? 24 :
3484
+ d < 48 ? 26 :
3485
+ d < 56 ? 28 :
3486
+ d < 64 ? 30 :
3487
+ d < 72 ? 32 :
3488
+ d < 84 ? 34 :
3489
+ d < 96 ? 36 :
3490
+ d < 98 ? 38 :
3491
+ d < 113 ? 40 :
3492
+ d < 149 ? 44 :
3493
+ d < 199 ? 48 :
3494
+ d < 261 ? 52 :
3495
+ d < 353 ? 56 :
3496
+ d < 461 ? 60 :
3497
+ d < 585 ? 64 :
3498
+ d < 1200 ? 70 :
3499
+ d < 1800 ? 80 :
3500
+ d < 2400 ? 90 :
3501
+ 100;
3502
+
3503
+ $.ellipse = (x, y, w, h) => {
3504
+ const n = getArcSegments(w == h ? w : Math.max(w, h));
3505
+
3506
+ let a = Math.max(w, 1) / 2;
3507
+ let b = w == h ? a : Math.max(h, 1) / 2;
3508
+
3509
+ let t = 0; // theta
3510
+ const angleIncrement = $.TAU / n;
3511
+ const ci = $._colorIndex;
3512
+ if ($._matrixDirty) $._saveMatrix();
3513
+ const ti = $._transformIndex;
3514
+ let vx1, vy1, vx2, vy2;
3515
+ for (let i = 0; i <= n; i++) {
3516
+ vx1 = vx2;
3517
+ vy1 = vy2;
3518
+ vx2 = x + a * Math.cos(t);
3519
+ vy2 = y + b * Math.sin(t);
3520
+ t += angleIncrement;
3521
+
3522
+ if (i == 0) continue;
3523
+
3524
+ verticesStack.push(x, y, ci, ti, vx1, vy1, ci, ti, vx2, vy2, ci, ti);
3525
+ }
3526
+
3527
+ drawStack.push(0, n * 3);
3528
+ };
3529
+
3530
+ $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3531
+
3532
+ $._hooks.preRender.push(() => {
3533
+ const vertexBuffer = Q5.device.createBuffer({
3534
+ size: verticesStack.length * 6,
3535
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
3536
+ });
3537
+
3538
+ Q5.device.queue.writeBuffer(vertexBuffer, 0, new Float32Array(verticesStack));
3539
+ $.pass.setVertexBuffer(0, vertexBuffer);
3540
+
3541
+ const colorsBuffer = Q5.device.createBuffer({
3542
+ size: colorsStack.length * 4,
3543
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3544
+ });
3545
+
3546
+ Q5.device.queue.writeBuffer(colorsBuffer, 0, new Float32Array(colorsStack));
3547
+
3548
+ const colorsBindGroup = Q5.device.createBindGroup({
3549
+ layout: $.bindGroupLayouts[2],
3550
+ entries: [
3551
+ {
3552
+ binding: 0,
3553
+ resource: {
3554
+ buffer: colorsBuffer,
3555
+ offset: 0,
3556
+ size: colorsStack.length * 4
3557
+ }
3558
+ }
3559
+ ]
3560
+ });
3561
+
3562
+ // set the bind group once before rendering
3563
+ $.pass.setBindGroup(2, colorsBindGroup);
3564
+ });
3565
+ };
3566
+ Q5.renderers.webgpu.image = ($, q) => {};
3567
+ Q5.renderers.webgpu.text = ($, q) => {};