q5 2.2.0 → 2.2.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.
package/q5.js CHANGED
@@ -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';
@@ -3067,3 +2891,664 @@ Q5.Vector.sub = (v, u) => v.copy().sub(u);
3067
2891
  for (let k of ['fromAngle', 'fromAngles', 'random2D', 'random3D']) {
3068
2892
  Q5.Vector[k] = (u, v, t) => new Q5.Vector()[k](u, v, t);
3069
2893
  }
2894
+ /**
2895
+ * q5-webgpu
2896
+ *
2897
+ * EXPERIMENTAL, for developer testing only!
2898
+ */
2899
+ Q5.renderers.webgpu = {};
2900
+
2901
+ Q5.renderers.webgpu.canvas = ($, q) => {
2902
+ let c = $.canvas;
2903
+
2904
+ c.width = $.width = 500;
2905
+ c.height = $.height = 500;
2906
+
2907
+ if ($.colorMode) $.colorMode('rgb', 'float');
2908
+
2909
+ let colorsStack, envBindGroup, transformBindGroup;
2910
+
2911
+ $._createCanvas = (w, h, opt) => {
2912
+ q.ctx = q.drawingContext = c.getContext('webgpu');
2913
+
2914
+ opt.format = navigator.gpu.getPreferredCanvasFormat();
2915
+ opt.device = Q5.device;
2916
+
2917
+ $.ctx.configure(opt);
2918
+
2919
+ $.pipelines = [];
2920
+
2921
+ // pipeline changes for each draw call
2922
+ $.pipelinesStack = [];
2923
+
2924
+ // vertices for each draw call
2925
+ $.verticesStack = [];
2926
+
2927
+ // number of vertices for each draw call
2928
+ $.drawStack = [];
2929
+
2930
+ // colors used for each draw call
2931
+ colorsStack = $.colorsStack = [1, 1, 1, 1];
2932
+
2933
+ // current color index, used to associate a vertex with a color
2934
+ $._colorIndex = 0;
2935
+
2936
+ let envLayout = Q5.device.createBindGroupLayout({
2937
+ entries: [
2938
+ {
2939
+ binding: 0,
2940
+ visibility: GPUShaderStage.VERTEX,
2941
+ buffer: {
2942
+ type: 'uniform',
2943
+ hasDynamicOffset: false
2944
+ }
2945
+ }
2946
+ ]
2947
+ });
2948
+
2949
+ let transformLayout = Q5.device.createBindGroupLayout({
2950
+ entries: [
2951
+ {
2952
+ binding: 0,
2953
+ visibility: GPUShaderStage.VERTEX,
2954
+ buffer: {
2955
+ type: 'read-only-storage',
2956
+ hasDynamicOffset: false
2957
+ }
2958
+ }
2959
+ ]
2960
+ });
2961
+
2962
+ $.bindGroupLayouts = [envLayout, transformLayout];
2963
+
2964
+ const uniformBuffer = Q5.device.createBuffer({
2965
+ size: 8, // Size of two floats
2966
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
2967
+ });
2968
+
2969
+ Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh]));
2970
+
2971
+ envBindGroup = Q5.device.createBindGroup({
2972
+ layout: envLayout,
2973
+ entries: [
2974
+ {
2975
+ binding: 0,
2976
+ resource: {
2977
+ buffer: uniformBuffer
2978
+ }
2979
+ }
2980
+ ]
2981
+ });
2982
+ };
2983
+
2984
+ $._resizeCanvas = (w, h) => {
2985
+ $._setCanvasSize(w, h);
2986
+ };
2987
+
2988
+ $.resetMatrix = () => {
2989
+ // Initialize the transformation matrix as 4x4 identity matrix
2990
+
2991
+ // prettier-ignore
2992
+ $._matrix = [
2993
+ 1, 0, 0, 0,
2994
+ 0, 1, 0, 0,
2995
+ 0, 0, 1, 0,
2996
+ 0, 0, 0, 1
2997
+ ];
2998
+ $._transformIndex = 0;
2999
+ };
3000
+ $.resetMatrix();
3001
+
3002
+ // Boolean to track if the matrix has been modified
3003
+ $._matrixDirty = false;
3004
+
3005
+ // Array to store transformation matrices for the render pass
3006
+ $.transformStates = [$._matrix.slice()];
3007
+
3008
+ // Stack to keep track of transformation matrix indexes
3009
+ $._transformIndexStack = [];
3010
+
3011
+ $.push = () => {
3012
+ // Push the current matrix index onto the stack
3013
+ $._transformIndexStack.push($._transformIndex);
3014
+ };
3015
+
3016
+ $.pop = () => {
3017
+ if ($._transformIndexStack.length > 0) {
3018
+ // Pop the last matrix index from the stack and set it as the current matrix index
3019
+ let idx = $._transformIndexStack.pop();
3020
+ $._matrix = $.transformStates[idx].slice();
3021
+ $._transformIndex = idx;
3022
+ } else {
3023
+ console.warn('Matrix index stack is empty!');
3024
+ }
3025
+ };
3026
+
3027
+ $.translate = (x, y, z) => {
3028
+ if (!x && !y && !z) return;
3029
+ // Update the translation values
3030
+ $._matrix[3] += x;
3031
+ $._matrix[7] += y;
3032
+ $._matrix[11] += z || 0;
3033
+ $._matrixDirty = true;
3034
+ };
3035
+
3036
+ $.rotate = (r) => {
3037
+ if (!r) return;
3038
+ if ($._angleMode == 'degrees') r = $.radians(r);
3039
+
3040
+ let cosR = Math.cos(r);
3041
+ let sinR = Math.sin(r);
3042
+
3043
+ let m0 = $._matrix[0],
3044
+ m1 = $._matrix[1],
3045
+ m4 = $._matrix[4],
3046
+ m5 = $._matrix[5];
3047
+ if (!m0 && !m1 && !m4 && !m5) {
3048
+ $._matrix[0] = cosR;
3049
+ $._matrix[1] = sinR;
3050
+ $._matrix[4] = -sinR;
3051
+ $._matrix[5] = cosR;
3052
+ } else {
3053
+ $._matrix[0] = m0 * cosR + m4 * sinR;
3054
+ $._matrix[1] = m1 * cosR + m5 * sinR;
3055
+ $._matrix[4] = m0 * -sinR + m4 * cosR;
3056
+ $._matrix[5] = m1 * -sinR + m5 * cosR;
3057
+ }
3058
+
3059
+ $._matrixDirty = true;
3060
+ };
3061
+
3062
+ $.scale = (sx = 1, sy, sz = 1) => {
3063
+ sy ??= sx;
3064
+
3065
+ $._matrix[0] *= sx;
3066
+ $._matrix[5] *= sy;
3067
+ $._matrix[10] *= sz;
3068
+
3069
+ $._matrixDirty = true;
3070
+ };
3071
+
3072
+ // Function to save the current matrix state if dirty
3073
+ $._saveMatrix = () => {
3074
+ $.transformStates.push($._matrix.slice());
3075
+ $._transformIndex = $.transformStates.length - 1;
3076
+ $._matrixDirty = false;
3077
+ };
3078
+
3079
+ $._beginRender = () => {
3080
+ $.encoder = Q5.device.createCommandEncoder();
3081
+
3082
+ q.pass = $.encoder.beginRenderPass({
3083
+ colorAttachments: [
3084
+ {
3085
+ view: ctx.getCurrentTexture().createView(),
3086
+ loadOp: 'clear',
3087
+ storeOp: 'store'
3088
+ }
3089
+ ]
3090
+ });
3091
+ };
3092
+
3093
+ $._render = () => {
3094
+ $.pass.setBindGroup(0, envBindGroup);
3095
+
3096
+ if (transformStates.length > 1 || !transformBindGroup) {
3097
+ const transformBuffer = Q5.device.createBuffer({
3098
+ size: transformStates.length * 64, // Size of 16 floats
3099
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3100
+ });
3101
+
3102
+ Q5.device.queue.writeBuffer(transformBuffer, 0, new Float32Array(transformStates.flat()));
3103
+
3104
+ transformBindGroup = Q5.device.createBindGroup({
3105
+ layout: $.bindGroupLayouts[1],
3106
+ entries: [
3107
+ {
3108
+ binding: 0,
3109
+ resource: {
3110
+ buffer: transformBuffer
3111
+ }
3112
+ }
3113
+ ]
3114
+ });
3115
+ }
3116
+
3117
+ $.pass.setBindGroup(1, transformBindGroup);
3118
+
3119
+ // run pre-render methods
3120
+ for (let m of $._hooks.preRender) m();
3121
+
3122
+ $.pass.setPipeline($.pipelines[0]);
3123
+
3124
+ // local variables used for performance
3125
+ 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];
3130
+ }
3131
+ };
3132
+
3133
+ $._finishRender = () => {
3134
+ $.pass.end();
3135
+ const commandBuffer = $.encoder.finish();
3136
+ Q5.device.queue.submit([commandBuffer]);
3137
+ q.pass = $.encoder = null;
3138
+
3139
+ // clear the stacks for the next frame
3140
+ $.verticesStack.length = 0;
3141
+ $.drawStack.length = 0;
3142
+ $.colorsStack.length = 4;
3143
+ $.pipelinesStack.length = 0;
3144
+ $._colorIndex = 0;
3145
+ rotation = 0;
3146
+ $.resetMatrix();
3147
+ $._matrixDirty = false;
3148
+ $.transformStates.length = 1;
3149
+ $._transformIndexStack.length = 0;
3150
+ };
3151
+
3152
+ $.fill = (r, g, b, a = 1) => {
3153
+ if (typeof r == 'string') r = Q5.color(r);
3154
+ // grayscale mode `fill(1, 0.5)`
3155
+ if (b == undefined) {
3156
+ a = g;
3157
+ g = b = r;
3158
+ }
3159
+ if (r._q5Color) colorsStack.push(...r.levels);
3160
+ else colorsStack.push(r, g, b, a);
3161
+ $._colorIndex++;
3162
+ };
3163
+ $.noFill = () => colorsStack.push(0, 0, 0, 0);
3164
+ $.stroke = () => {};
3165
+ $.noStroke = () => {};
3166
+
3167
+ $.clear = () => {};
3168
+ };
3169
+
3170
+ Q5.webgpu = async function (scope, parent) {
3171
+ if (!navigator.gpu) {
3172
+ console.error('q5 WebGPU not supported on this browser!');
3173
+ let q = new Q5(scope, parent);
3174
+ q.colorMode('rgb', 1);
3175
+ q._beginRender = () => q.translate(q.canvas.hw, q.canvas.hh);
3176
+ return q;
3177
+ }
3178
+ let adapter = await navigator.gpu.requestAdapter();
3179
+ if (!adapter) throw new Error('No appropriate GPUAdapter found.');
3180
+ Q5.device = await adapter.requestDevice();
3181
+
3182
+ return new Q5(scope, parent, 'webgpu');
3183
+ };
3184
+ Q5.renderers.webgpu.drawing = ($, q) => {
3185
+ $.CLOSE = 1;
3186
+
3187
+ let verticesStack, drawStack, colorsStack;
3188
+
3189
+ $._hooks.postCanvas.push(() => {
3190
+ let colorsLayout = Q5.device.createBindGroupLayout({
3191
+ entries: [
3192
+ {
3193
+ binding: 0,
3194
+ visibility: GPUShaderStage.FRAGMENT,
3195
+ buffer: {
3196
+ type: 'read-only-storage',
3197
+ hasDynamicOffset: false
3198
+ }
3199
+ }
3200
+ ]
3201
+ });
3202
+
3203
+ $.bindGroupLayouts.push(colorsLayout);
3204
+
3205
+ verticesStack = $.verticesStack;
3206
+ drawStack = $.drawStack;
3207
+ colorsStack = $.colorsStack;
3208
+
3209
+ let vertexBufferLayout = {
3210
+ arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
3211
+ attributes: [
3212
+ { format: 'float32x2', offset: 0, shaderLocation: 0 }, // position
3213
+ { format: 'float32', offset: 8, shaderLocation: 1 }, // colorIndex
3214
+ { format: 'float32', offset: 12, shaderLocation: 2 } // transformIndex
3215
+ ]
3216
+ };
3217
+
3218
+ let vertexShader = Q5.device.createShaderModule({
3219
+ code: `
3220
+ struct VertexOutput {
3221
+ @builtin(position) position: vec4<f32>,
3222
+ @location(1) colorIndex: f32
3223
+ };
3224
+
3225
+ struct Uniforms {
3226
+ halfWidth: f32,
3227
+ halfHeight: f32
3228
+ };
3229
+
3230
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
3231
+ @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
3232
+
3233
+ @vertex
3234
+ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
3235
+ var vert = vec4<f32>(pos, 0.0, 1.0);
3236
+ vert *= transforms[i32(transformIndex)];
3237
+ vert.x /= uniforms.halfWidth;
3238
+ vert.y /= uniforms.halfHeight;
3239
+
3240
+ var output: VertexOutput;
3241
+ output.position = vert;
3242
+ output.colorIndex = colorIndex;
3243
+ return output;
3244
+ }
3245
+ `
3246
+ });
3247
+
3248
+ let fragmentShader = Q5.device.createShaderModule({
3249
+ code: `
3250
+ @group(2) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
3251
+
3252
+ @fragment
3253
+ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
3254
+ let index = u32(colorIndex);
3255
+ return mix(uColors[index], uColors[index + 1u], fract(colorIndex));
3256
+ }
3257
+ `
3258
+ });
3259
+
3260
+ let pipelineLayout = Q5.device.createPipelineLayout({
3261
+ bindGroupLayouts: $.bindGroupLayouts
3262
+ });
3263
+
3264
+ $._createPipeline = (blendConfig) => {
3265
+ return Q5.device.createRenderPipeline({
3266
+ layout: pipelineLayout,
3267
+ vertex: {
3268
+ module: vertexShader,
3269
+ entryPoint: 'vertexMain',
3270
+ buffers: [vertexBufferLayout]
3271
+ },
3272
+ fragment: {
3273
+ module: fragmentShader,
3274
+ entryPoint: 'fragmentMain',
3275
+ targets: [
3276
+ {
3277
+ format: 'bgra8unorm',
3278
+ blend: blendConfig
3279
+ }
3280
+ ]
3281
+ },
3282
+ primitive: {
3283
+ topology: 'triangle-list'
3284
+ }
3285
+ });
3286
+ };
3287
+
3288
+ $.pipelines[0] = $._createPipeline(blendConfigs.normal);
3289
+ });
3290
+
3291
+ // prettier-ignore
3292
+ let blendFactors = [
3293
+ 'zero', // 0
3294
+ 'one', // 1
3295
+ 'src-alpha', // 2
3296
+ 'one-minus-src-alpha', // 3
3297
+ 'dst', // 4
3298
+ 'dst-alpha', // 5
3299
+ 'one-minus-dst-alpha', // 6
3300
+ 'one-minus-src' // 7
3301
+ ];
3302
+ let blendOps = [
3303
+ 'add', // 0
3304
+ 'subtract', // 1
3305
+ 'reverse-subtract', // 2
3306
+ 'min', // 3
3307
+ 'max' // 4
3308
+ ];
3309
+
3310
+ const blendModes = {
3311
+ normal: [2, 3, 0, 2, 3, 0],
3312
+ lighter: [2, 1, 0, 2, 1, 0],
3313
+ subtract: [2, 1, 2, 2, 1, 2],
3314
+ multiply: [4, 0, 0, 5, 0, 0],
3315
+ screen: [1, 3, 0, 1, 3, 0],
3316
+ darken: [1, 3, 3, 1, 3, 3],
3317
+ lighten: [1, 3, 4, 1, 3, 4],
3318
+ overlay: [2, 3, 0, 2, 3, 0],
3319
+ hard_light: [2, 3, 0, 2, 3, 0],
3320
+ soft_light: [2, 3, 0, 2, 3, 0],
3321
+ difference: [2, 3, 2, 2, 3, 2],
3322
+ exclusion: [2, 3, 0, 2, 3, 0],
3323
+ color_dodge: [1, 7, 0, 1, 7, 0],
3324
+ color_burn: [6, 1, 0, 6, 1, 0],
3325
+ linear_dodge: [2, 1, 0, 2, 1, 0],
3326
+ linear_burn: [2, 7, 1, 2, 7, 1],
3327
+ vivid_light: [2, 7, 0, 2, 7, 0],
3328
+ pin_light: [2, 7, 0, 2, 7, 0],
3329
+ hard_mix: [2, 7, 0, 2, 7, 0]
3330
+ };
3331
+
3332
+ $.blendConfigs = {};
3333
+
3334
+ Object.entries(blendModes).forEach(([name, mode]) => {
3335
+ $.blendConfigs[name] = {
3336
+ color: {
3337
+ srcFactor: blendFactors[mode[0]],
3338
+ dstFactor: blendFactors[mode[1]],
3339
+ operation: blendOps[mode[2]]
3340
+ },
3341
+ alpha: {
3342
+ srcFactor: blendFactors[mode[3]],
3343
+ dstFactor: blendFactors[mode[4]],
3344
+ operation: blendOps[mode[5]]
3345
+ }
3346
+ };
3347
+ });
3348
+
3349
+ $._blendMode = 'normal';
3350
+ $.blendMode = (mode) => {
3351
+ if (mode == $._blendMode) return;
3352
+ if (mode == 'source-over') mode = 'normal';
3353
+ mode = mode.toLowerCase().replace(/[ -]/g, '_');
3354
+ $._blendMode = mode;
3355
+ $.pipelines[0] = $._createPipeline($.blendConfigs[mode]);
3356
+ };
3357
+
3358
+ let shapeVertices;
3359
+
3360
+ $.beginShape = () => {
3361
+ shapeVertices = [];
3362
+ };
3363
+
3364
+ $.vertex = (x, y) => {
3365
+ if ($._matrixDirty) $._saveMatrix();
3366
+ shapeVertices.push(x, -y, $._colorIndex, $._transformIndex);
3367
+ };
3368
+
3369
+ $.endShape = (close) => {
3370
+ let v = shapeVertices;
3371
+ if (v.length < 12) {
3372
+ throw new Error('A shape must have at least 3 vertices.');
3373
+ }
3374
+ if (close) {
3375
+ // Close the shape by adding the first vertex at the end
3376
+ v.push(v[0], v[1], v[2], v[3]);
3377
+ }
3378
+ // Convert the shape to triangles
3379
+ let triangles = [];
3380
+ for (let i = 4; i < v.length; i += 4) {
3381
+ triangles.push(
3382
+ v[0], // First vertex
3383
+ v[1],
3384
+ v[2],
3385
+ v[3],
3386
+ v[i - 4], // Previous vertex
3387
+ v[i - 3],
3388
+ v[i - 2],
3389
+ v[i - 1],
3390
+ v[i], // Current vertex
3391
+ v[i + 1],
3392
+ v[i + 2],
3393
+ v[i + 3]
3394
+ );
3395
+ }
3396
+
3397
+ verticesStack.push(...triangles);
3398
+ drawStack.push(triangles.length / 4);
3399
+ shapeVertices = [];
3400
+ };
3401
+
3402
+ $.triangle = (x1, y1, x2, y2, x3, y3) => {
3403
+ $.beginShape();
3404
+ $.vertex(x1, y1);
3405
+ $.vertex(x2, y2);
3406
+ $.vertex(x3, y3);
3407
+ $.endShape(1);
3408
+ };
3409
+
3410
+ $.rect = (x, y, w, h) => {
3411
+ let hw = w / 2;
3412
+ let hh = h / 2;
3413
+
3414
+ let left = x - hw;
3415
+ let right = x + hw;
3416
+ let top = -(y - hh); // y is inverted in WebGPU
3417
+ let bottom = -(y + hh);
3418
+
3419
+ let ci = $._colorIndex;
3420
+ if ($._matrixDirty) $._saveMatrix();
3421
+ let ti = $._transformIndex;
3422
+ // two triangles make a rectangle
3423
+ 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
3448
+ );
3449
+ drawStack.push(6);
3450
+ };
3451
+
3452
+ $.background = () => {};
3453
+
3454
+ /**
3455
+ * Derived from: ceil(Math.log(d) * 7) * 2 - ceil(28)
3456
+ * This lookup table is used for better performance.
3457
+ * @param {Number} d diameter of the circle
3458
+ * @returns n number of segments
3459
+ */
3460
+ // prettier-ignore
3461
+ const getArcSegments = (d) =>
3462
+ d < 14 ? 8 :
3463
+ d < 16 ? 10 :
3464
+ d < 18 ? 12 :
3465
+ d < 20 ? 14 :
3466
+ d < 22 ? 16 :
3467
+ d < 24 ? 18 :
3468
+ d < 28 ? 20 :
3469
+ d < 34 ? 22 :
3470
+ d < 42 ? 24 :
3471
+ d < 48 ? 26 :
3472
+ d < 56 ? 28 :
3473
+ d < 64 ? 30 :
3474
+ d < 72 ? 32 :
3475
+ d < 84 ? 34 :
3476
+ d < 96 ? 36 :
3477
+ d < 98 ? 38 :
3478
+ d < 113 ? 40 :
3479
+ d < 149 ? 44 :
3480
+ d < 199 ? 48 :
3481
+ d < 261 ? 52 :
3482
+ d < 353 ? 56 :
3483
+ d < 461 ? 60 :
3484
+ d < 585 ? 64 :
3485
+ d < 1200 ? 70 :
3486
+ d < 1800 ? 80 :
3487
+ d < 2400 ? 90 :
3488
+ 100;
3489
+
3490
+ $.ellipse = (x, y, w, h) => {
3491
+ const n = getArcSegments(w == h ? w : Math.max(w, h));
3492
+
3493
+ let a = Math.max(w, 1) / 2;
3494
+ let b = w == h ? a : Math.max(h, 1) / 2;
3495
+
3496
+ let t = 0; // theta
3497
+ const angleIncrement = $.TAU / n;
3498
+ const ci = $._colorIndex;
3499
+ if ($._matrixDirty) $._saveMatrix();
3500
+ const ti = $._transformIndex;
3501
+ let vx1, vy1, vx2, vy2;
3502
+ for (let i = 0; i <= n; i++) {
3503
+ vx1 = vx2;
3504
+ vy1 = vy2;
3505
+ vx2 = x + a * Math.cos(t);
3506
+ vy2 = y + b * Math.sin(t);
3507
+ t += angleIncrement;
3508
+
3509
+ if (i == 0) continue;
3510
+
3511
+ verticesStack.push(x, y, ci, ti, vx1, vy1, ci, ti, vx2, vy2, ci, ti);
3512
+ }
3513
+
3514
+ drawStack.push(n * 3);
3515
+ };
3516
+
3517
+ $.circle = (x, y, d) => $.ellipse(x, y, d, d);
3518
+
3519
+ $._hooks.preRender.push(() => {
3520
+ const vertexBuffer = Q5.device.createBuffer({
3521
+ size: verticesStack.length * 6,
3522
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
3523
+ });
3524
+
3525
+ Q5.device.queue.writeBuffer(vertexBuffer, 0, new Float32Array(verticesStack));
3526
+ $.pass.setVertexBuffer(0, vertexBuffer);
3527
+
3528
+ const colorsBuffer = Q5.device.createBuffer({
3529
+ size: colorsStack.length * 4,
3530
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
3531
+ });
3532
+
3533
+ Q5.device.queue.writeBuffer(colorsBuffer, 0, new Float32Array(colorsStack));
3534
+
3535
+ const colorsBindGroup = Q5.device.createBindGroup({
3536
+ layout: $.bindGroupLayouts[2],
3537
+ entries: [
3538
+ {
3539
+ binding: 0,
3540
+ resource: {
3541
+ buffer: colorsBuffer,
3542
+ offset: 0,
3543
+ size: colorsStack.length * 4
3544
+ }
3545
+ }
3546
+ ]
3547
+ });
3548
+
3549
+ // set the bind group once before rendering
3550
+ $.pass.setBindGroup(2, colorsBindGroup);
3551
+ });
3552
+ };
3553
+ Q5.renderers.webgpu.image = ($, q) => {};
3554
+ Q5.renderers.webgpu.text = ($, q) => {};