q5 2.0.17 → 2.2.0

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.
@@ -0,0 +1,369 @@
1
+ Q5.renderers.webgpu.drawing = ($, q) => {
2
+ $.CLOSE = 1;
3
+
4
+ let verticesStack, drawStack, colorsStack;
5
+
6
+ $._hooks.postCanvas.push(() => {
7
+ let colorsLayout = Q5.device.createBindGroupLayout({
8
+ entries: [
9
+ {
10
+ binding: 0,
11
+ visibility: GPUShaderStage.FRAGMENT,
12
+ buffer: {
13
+ type: 'read-only-storage',
14
+ hasDynamicOffset: false
15
+ }
16
+ }
17
+ ]
18
+ });
19
+
20
+ $.bindGroupLayouts.push(colorsLayout);
21
+
22
+ verticesStack = $.verticesStack;
23
+ drawStack = $.drawStack;
24
+ colorsStack = $.colorsStack;
25
+
26
+ let vertexBufferLayout = {
27
+ arrayStride: 16, // 2 coordinates + 1 color index + 1 transform index * 4 bytes each
28
+ attributes: [
29
+ { format: 'float32x2', offset: 0, shaderLocation: 0 }, // position
30
+ { format: 'float32', offset: 8, shaderLocation: 1 }, // colorIndex
31
+ { format: 'float32', offset: 12, shaderLocation: 2 } // transformIndex
32
+ ]
33
+ };
34
+
35
+ let vertexShader = Q5.device.createShaderModule({
36
+ code: `
37
+ struct VertexOutput {
38
+ @builtin(position) position: vec4<f32>,
39
+ @location(1) colorIndex: f32
40
+ };
41
+
42
+ struct Uniforms {
43
+ halfWidth: f32,
44
+ halfHeight: f32
45
+ };
46
+
47
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
48
+ @group(1) @binding(0) var<storage, read> transforms: array<mat4x4<f32>>;
49
+
50
+ @vertex
51
+ fn vertexMain(@location(0) pos: vec2<f32>, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {
52
+ var vert = vec4<f32>(pos, 0.0, 1.0);
53
+ vert *= transforms[i32(transformIndex)];
54
+ vert.x /= uniforms.halfWidth;
55
+ vert.y /= uniforms.halfHeight;
56
+
57
+ var output: VertexOutput;
58
+ output.position = vert;
59
+ output.colorIndex = colorIndex;
60
+ return output;
61
+ }
62
+ `
63
+ });
64
+
65
+ let fragmentShader = Q5.device.createShaderModule({
66
+ code: `
67
+ @group(2) @binding(0) var<storage, read> uColors : array<vec4<f32>>;
68
+
69
+ @fragment
70
+ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4<f32> {
71
+ let index = u32(colorIndex);
72
+ return mix(uColors[index], uColors[index + 1u], fract(colorIndex));
73
+ }
74
+ `
75
+ });
76
+
77
+ let pipelineLayout = Q5.device.createPipelineLayout({
78
+ bindGroupLayouts: $.bindGroupLayouts
79
+ });
80
+
81
+ $._createPipeline = (blendConfig) => {
82
+ return Q5.device.createRenderPipeline({
83
+ layout: pipelineLayout,
84
+ vertex: {
85
+ module: vertexShader,
86
+ entryPoint: 'vertexMain',
87
+ buffers: [vertexBufferLayout]
88
+ },
89
+ fragment: {
90
+ module: fragmentShader,
91
+ entryPoint: 'fragmentMain',
92
+ targets: [
93
+ {
94
+ format: 'bgra8unorm',
95
+ blend: blendConfig
96
+ }
97
+ ]
98
+ },
99
+ primitive: {
100
+ topology: 'triangle-list'
101
+ }
102
+ });
103
+ };
104
+
105
+ $.pipelines[0] = $._createPipeline(blendConfigs.normal);
106
+ });
107
+
108
+ // prettier-ignore
109
+ let blendFactors = [
110
+ 'zero', // 0
111
+ 'one', // 1
112
+ 'src-alpha', // 2
113
+ 'one-minus-src-alpha', // 3
114
+ 'dst', // 4
115
+ 'dst-alpha', // 5
116
+ 'one-minus-dst-alpha', // 6
117
+ 'one-minus-src' // 7
118
+ ];
119
+ let blendOps = [
120
+ 'add', // 0
121
+ 'subtract', // 1
122
+ 'reverse-subtract', // 2
123
+ 'min', // 3
124
+ 'max' // 4
125
+ ];
126
+
127
+ const blendModes = {
128
+ normal: [2, 3, 0, 2, 3, 0],
129
+ lighter: [2, 1, 0, 2, 1, 0],
130
+ subtract: [2, 1, 2, 2, 1, 2],
131
+ multiply: [4, 0, 0, 5, 0, 0],
132
+ screen: [1, 3, 0, 1, 3, 0],
133
+ darken: [1, 3, 3, 1, 3, 3],
134
+ lighten: [1, 3, 4, 1, 3, 4],
135
+ overlay: [2, 3, 0, 2, 3, 0],
136
+ hard_light: [2, 3, 0, 2, 3, 0],
137
+ soft_light: [2, 3, 0, 2, 3, 0],
138
+ difference: [2, 3, 2, 2, 3, 2],
139
+ exclusion: [2, 3, 0, 2, 3, 0],
140
+ color_dodge: [1, 7, 0, 1, 7, 0],
141
+ color_burn: [6, 1, 0, 6, 1, 0],
142
+ linear_dodge: [2, 1, 0, 2, 1, 0],
143
+ linear_burn: [2, 7, 1, 2, 7, 1],
144
+ vivid_light: [2, 7, 0, 2, 7, 0],
145
+ pin_light: [2, 7, 0, 2, 7, 0],
146
+ hard_mix: [2, 7, 0, 2, 7, 0]
147
+ };
148
+
149
+ $.blendConfigs = {};
150
+
151
+ Object.entries(blendModes).forEach(([name, mode]) => {
152
+ $.blendConfigs[name] = {
153
+ color: {
154
+ srcFactor: blendFactors[mode[0]],
155
+ dstFactor: blendFactors[mode[1]],
156
+ operation: blendOps[mode[2]]
157
+ },
158
+ alpha: {
159
+ srcFactor: blendFactors[mode[3]],
160
+ dstFactor: blendFactors[mode[4]],
161
+ operation: blendOps[mode[5]]
162
+ }
163
+ };
164
+ });
165
+
166
+ $._blendMode = 'normal';
167
+ $.blendMode = (mode) => {
168
+ if (mode == $._blendMode) return;
169
+ if (mode == 'source-over') mode = 'normal';
170
+ mode = mode.toLowerCase().replace(/[ -]/g, '_');
171
+ $._blendMode = mode;
172
+ $.pipelines[0] = $._createPipeline($.blendConfigs[mode]);
173
+ };
174
+
175
+ let shapeVertices;
176
+
177
+ $.beginShape = () => {
178
+ shapeVertices = [];
179
+ };
180
+
181
+ $.vertex = (x, y) => {
182
+ if ($._matrixDirty) $._saveMatrix();
183
+ shapeVertices.push(x, -y, $._colorIndex, $._transformIndex);
184
+ };
185
+
186
+ $.endShape = (close) => {
187
+ let v = shapeVertices;
188
+ if (v.length < 12) {
189
+ throw new Error('A shape must have at least 3 vertices.');
190
+ }
191
+ if (close) {
192
+ // Close the shape by adding the first vertex at the end
193
+ v.push(v[0], v[1], v[2], v[3]);
194
+ }
195
+ // Convert the shape to triangles
196
+ let triangles = [];
197
+ for (let i = 4; i < v.length; i += 4) {
198
+ triangles.push(
199
+ v[0], // First vertex
200
+ v[1],
201
+ v[2],
202
+ v[3],
203
+ v[i - 4], // Previous vertex
204
+ v[i - 3],
205
+ v[i - 2],
206
+ v[i - 1],
207
+ v[i], // Current vertex
208
+ v[i + 1],
209
+ v[i + 2],
210
+ v[i + 3]
211
+ );
212
+ }
213
+
214
+ verticesStack.push(...triangles);
215
+ drawStack.push(triangles.length / 4);
216
+ shapeVertices = [];
217
+ };
218
+
219
+ $.triangle = (x1, y1, x2, y2, x3, y3) => {
220
+ $.beginShape();
221
+ $.vertex(x1, y1);
222
+ $.vertex(x2, y2);
223
+ $.vertex(x3, y3);
224
+ $.endShape(1);
225
+ };
226
+
227
+ $.rect = (x, y, w, h) => {
228
+ let hw = w / 2;
229
+ let hh = h / 2;
230
+
231
+ let left = x - hw;
232
+ let right = x + hw;
233
+ let top = -(y - hh); // y is inverted in WebGPU
234
+ let bottom = -(y + hh);
235
+
236
+ let ci = $._colorIndex;
237
+ if ($._matrixDirty) $._saveMatrix();
238
+ let ti = $._transformIndex;
239
+ // two triangles make a rectangle
240
+ verticesStack.push(
241
+ left,
242
+ top,
243
+ ci,
244
+ ti,
245
+ right,
246
+ top,
247
+ ci,
248
+ ti,
249
+ left,
250
+ bottom,
251
+ ci,
252
+ ti,
253
+ right,
254
+ top,
255
+ ci,
256
+ ti,
257
+ left,
258
+ bottom,
259
+ ci,
260
+ ti,
261
+ right,
262
+ bottom,
263
+ ci,
264
+ ti
265
+ );
266
+ drawStack.push(6);
267
+ };
268
+
269
+ $.background = () => {};
270
+
271
+ /**
272
+ * Derived from: ceil(Math.log(d) * 7) * 2 - ceil(28)
273
+ * This lookup table is used for better performance.
274
+ * @param {Number} d diameter of the circle
275
+ * @returns n number of segments
276
+ */
277
+ // prettier-ignore
278
+ const getArcSegments = (d) =>
279
+ d < 14 ? 8 :
280
+ d < 16 ? 10 :
281
+ d < 18 ? 12 :
282
+ d < 20 ? 14 :
283
+ d < 22 ? 16 :
284
+ d < 24 ? 18 :
285
+ d < 28 ? 20 :
286
+ d < 34 ? 22 :
287
+ d < 42 ? 24 :
288
+ d < 48 ? 26 :
289
+ d < 56 ? 28 :
290
+ d < 64 ? 30 :
291
+ d < 72 ? 32 :
292
+ d < 84 ? 34 :
293
+ d < 96 ? 36 :
294
+ d < 98 ? 38 :
295
+ d < 113 ? 40 :
296
+ d < 149 ? 44 :
297
+ d < 199 ? 48 :
298
+ d < 261 ? 52 :
299
+ d < 353 ? 56 :
300
+ d < 461 ? 60 :
301
+ d < 585 ? 64 :
302
+ d < 1200 ? 70 :
303
+ d < 1800 ? 80 :
304
+ d < 2400 ? 90 :
305
+ 100;
306
+
307
+ $.ellipse = (x, y, w, h) => {
308
+ const n = getArcSegments(w == h ? w : Math.max(w, h));
309
+
310
+ let a = Math.max(w, 1) / 2;
311
+ let b = w == h ? a : Math.max(h, 1) / 2;
312
+
313
+ let t = 0; // theta
314
+ const angleIncrement = $.TAU / n;
315
+ const ci = $._colorIndex;
316
+ if ($._matrixDirty) $._saveMatrix();
317
+ const ti = $._transformIndex;
318
+ let vx1, vy1, vx2, vy2;
319
+ for (let i = 0; i <= n; i++) {
320
+ vx1 = vx2;
321
+ vy1 = vy2;
322
+ vx2 = x + a * Math.cos(t);
323
+ vy2 = y + b * Math.sin(t);
324
+ t += angleIncrement;
325
+
326
+ if (i == 0) continue;
327
+
328
+ verticesStack.push(x, y, ci, ti, vx1, vy1, ci, ti, vx2, vy2, ci, ti);
329
+ }
330
+
331
+ drawStack.push(n * 3);
332
+ };
333
+
334
+ $.circle = (x, y, d) => $.ellipse(x, y, d, d);
335
+
336
+ $._hooks.preRender.push(() => {
337
+ const vertexBuffer = Q5.device.createBuffer({
338
+ size: verticesStack.length * 6,
339
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
340
+ });
341
+
342
+ Q5.device.queue.writeBuffer(vertexBuffer, 0, new Float32Array(verticesStack));
343
+ $.pass.setVertexBuffer(0, vertexBuffer);
344
+
345
+ const colorsBuffer = Q5.device.createBuffer({
346
+ size: colorsStack.length * 4,
347
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
348
+ });
349
+
350
+ Q5.device.queue.writeBuffer(colorsBuffer, 0, new Float32Array(colorsStack));
351
+
352
+ const colorsBindGroup = Q5.device.createBindGroup({
353
+ layout: $.bindGroupLayouts[2],
354
+ entries: [
355
+ {
356
+ binding: 0,
357
+ resource: {
358
+ buffer: colorsBuffer,
359
+ offset: 0,
360
+ size: colorsStack.length * 4
361
+ }
362
+ }
363
+ ]
364
+ });
365
+
366
+ // set the bind group once before rendering
367
+ $.pass.setBindGroup(2, colorsBindGroup);
368
+ });
369
+ };
@@ -0,0 +1 @@
1
+ Q5.renderers.webgpu.image = ($, q) => {};
@@ -0,0 +1 @@
1
+ Q5.renderers.webgpu.text = ($, q) => {};
package/src/readme.md CHANGED
@@ -32,12 +32,39 @@ Additional modules:
32
32
  <script src="https://q5js.org/src/q5-sensors.js"></script>
33
33
  ```
34
34
 
35
+ WebGPU rendering modules are in development:
36
+
37
+ ```html
38
+ <script src="https://q5js.org/src/q5-webgpu-canvas.js"></script>
39
+ <script src="https://q5js.org/src/q5-webgpu-drawing.js"></script>
40
+ ```
41
+
35
42
  # Module Info
36
43
 
44
+ - [Modular Use](#modular-use)
45
+ - [Module Info](#module-info)
46
+ - [core](#core)
47
+ - [canvas](#canvas)
48
+ - [q2d-canvas](#q2d-canvas)
49
+ - [q2d-drawing](#q2d-drawing)
50
+ - [q2d-image](#q2d-image)
51
+ - [q2d-soft-filters](#q2d-soft-filters)
52
+ - [q2d-text](#q2d-text)
53
+ - [webgpu-canvas](#webgpu-canvas)
54
+ - [webgpu-drawing](#webgpu-drawing)
55
+ - [math](#math)
56
+ - [noisier](#noisier)
57
+
37
58
  ## core
38
59
 
39
60
  The core module provides the absolute basic functionality necessary to run q5.
40
61
 
62
+ It loads other modules by passing `$` (alias for `this`) and `q` (which in global mode is a proxy for `this` and `window` or `global`).
63
+
64
+ ## canvas
65
+
66
+ The canvas module provides shared functionality for all canvas renderers, such as adding the canvas to the DOM, resizing the canvas, setting pixel density,
67
+
41
68
  ## q2d-canvas
42
69
 
43
70
  Adds canvas 2D rendering support to q5.
@@ -60,27 +87,77 @@ The filters in q5-image use the [CanvasRenderingContext2D.filter](https://develo
60
87
 
61
88
  Software implementation of image filters.
62
89
 
63
- This module includes additional filters not implemented in q5-image and legacy filter support for Safari which lacks ctx.filter.
90
+ This module includes additional filters not implemented in q5-image and legacy filter support for Safari which lacks `ctx.filter`.
64
91
 
65
- These filters are slow, real-time use of them is not recommended.
92
+ These filters are slow. Real-time use of them is not recommended.
66
93
 
67
- As of April 2024, Safari Technology Preview supports ctx.filter under a flag. Hopefully in the near future mainline Safari will support ctx.filter and this module can be omitted from the default bundle.
94
+ As of April 2024, Safari Technology Preview supports `ctx.filter` under a flag. Hopefully in the near future this module can be omitted from the default bundle.
68
95
 
69
96
  ## q2d-text
70
97
 
71
98
  Adds canvas 2D text rendering support to q5.
72
99
 
100
+ Image based features in this module require the q5-2d-image module.
101
+
73
102
  `createTextImage(str, w, h)` provides a simple way for users to create images from text.
74
103
 
75
104
  `textImage(img, x, y)` displays text images, complying with the user's text position settings instead of their image position settings. The idea is that text will appear in the same place as it would if it were drawn with the `text` function.
76
105
 
77
106
  `textCache(bool, maxSize)` enables or disables text caching. As of June 2024, drawing rotated text is super slow in all browsers, so q5 creates and stores images of text and rotates that instead. Can improve rendering performance 90x but uses more memory. `maxSize` param determines the maximum number of text images to cache, default is 500 since these images will typically be quite small. The text image cache (tic) is a timed cache, so the oldest images are removed first.
78
107
 
79
- Of course, use of these image based features requires the q5-2d-image module.
108
+ ## webgpu-canvas
109
+
110
+ > ⚠️ Experimental features! ⚠️
111
+
112
+ This module adds WebGPU renderer support to q5. Note that images, text, and strokes can not be rendered yet.
113
+
114
+ Instead of `new Q5()`, run the async function `Q5.webgpu()`. Explicit use of `createCanvas` is required.
115
+
116
+ ```js
117
+ let q = await Q5.webgpu();
118
+
119
+ createCanvas(500, 500);
120
+ ```
121
+
122
+ Set the script type of your sketch to "module" to use `await` on the top level.
123
+
124
+ ```html
125
+ <script type="module" src="sketch.js"></script>
126
+ ```
127
+
128
+ Using q5 with the webgpu renderer requires a different approach to setting up sketches. That's because variables and functions declared in a module are not added to the global `window` object.
129
+
130
+ Add functions like `setup` and `draw` as properties of `q`, the instance of Q5.
131
+
132
+ ```js
133
+ q.draw = function () {
134
+ // draw stuff
135
+ };
136
+ ```
137
+
138
+ The sketches you create with the q5-webgpu renderer will still display properly if WebGPU is not supported on a viewer's browser.
139
+
140
+ In that case, q5 will put a warning in the console and fall back to the q2d renderer. A compatibility layer is applied which sets the color mode to "rgba" in float format and translates the origin to the center of the canvas on every frame. For now, be sure to set `noStroke` in your setup code and `clear` the canvas at the start of your `draw` function to match current q5 webgpu limitations.
141
+
142
+ Implemented functions:
143
+
144
+ `createCanvas`, `resizeCanvas`, `fill`, `clear`, `push`, `pop`, `resetMatrix`, `translate`, `rotate`, `scale`
145
+
146
+ ## webgpu-drawing
147
+
148
+ > Uses `colorMode('rgb', 'float')` by default. Changing it to 'oklch' is not supported yet for the webgpu renderer.
149
+
150
+ All basic shapes are drawn from their center. Strokes are not implemented yet.
151
+
152
+ q5's WebGPU renderer drawing functions like `rect` don't immediately draw on the canvas. Instead, they prepare vertex and color data to be sent to the GPU in bulk, which occurs after the user's `draw` function and any post-draw functions are run. This approach better utilizes the GPU, so it doesn't have to repeatedly wait for the CPU to send small chunks of data that describe each individual shape. It's why WebGPU is faster than Canvas2D.
153
+
154
+ Implemented functions:
155
+
156
+ `rect`, `circle`, `ellipse`, `triangle`, `beginShape`, `vertex`, `endShape`, `blendMode`
80
157
 
81
158
  ## math
82
159
 
83
- `PerlinNoise` is q5's default noise algorithm. Kevin Perlin won an Academy Award for his work on the original algorithm for the 1982 movie Tron. q5's JavaScript implementation was authored by Tezumie.
160
+ `PerlinNoise` is q5's default noise algorithm. Kevin Perlin won an Academy Award for his work on the original algorithm for the 1982 movie Tron. The JavaScript implementation of it in q5 was authored by Tezumie.
84
161
 
85
162
  `noiseMode` enables users to switch between noise algorithms, although only "perlin" is included in q5-math.
86
163
 
@@ -90,4 +167,4 @@ Adds additional noise functions to q5.
90
167
 
91
168
  `SimplexNoise` is a simplex noise implementation in JavaScript by Tezumie. Kevin Perlin's patent on simplex noise expired in 2022. Simplex noise is slightly faster but arguably less visually appealing than perlin noise.
92
169
 
93
- `BlockyNoise` is similar to p5's default `noise` function, which is a bit notorious in the gen art community for not actually being perlin noise, despite its claims to be. It looks closer to value noise but is not a standard implementation of that either. It's a bit blocky at 1 octave, hence the name.
170
+ `BlockyNoise` is similar to p5's default `noise` function, which is a bit notorious in the gen art community for not actually being perlin noise, despite its claims to be. It looks closer to value noise but is not a standard implementation of that either. When visualized in 2d it's a bit blocky at 1 octave, hence the name.