@bigmistqke/view.gl 0.1.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.
package/dev/bezier.ts ADDED
@@ -0,0 +1,428 @@
1
+ import { view } from 'view.gl'
2
+ import { attribute, compile, glsl, interleave, uniform } from 'view.gl/tag'
3
+ import { createElement, cursor } from './utils'
4
+
5
+ // Create canvas and WebGL context
6
+ const canvas = createElement('canvas')
7
+ canvas.width = window.innerWidth
8
+ canvas.height = window.innerHeight
9
+ const gl = canvas.getContext('webgl2', { antialias: true })!
10
+
11
+ if (!gl) {
12
+ throw new Error('WebGL not supported')
13
+ }
14
+
15
+ // Application state containing control points and view transformation
16
+ const state = {
17
+ position: { x: 0, y: 0 }, // Pan offset in screen pixels
18
+ scale: 1, // Zoom level
19
+ controlPoints: [
20
+ { x: 200, y: 400 }, // P0: Start point of the bezier curve
21
+ { x: 400, y: 100 }, // P1: Control point (influences curvature)
22
+ { x: 600, y: 400 }, // P2: End point of the bezier curve
23
+ ] as const,
24
+ dragIndex: -1, // Index of control point being dragged (-1 = none)
25
+ }
26
+
27
+ /**********************************************************************************/
28
+ /* */
29
+ /* Utils */
30
+ /* */
31
+ /**********************************************************************************/
32
+
33
+ // Convert screen coordinates (pixels) to world coordinates
34
+ // This allows us to work in a consistent coordinate system regardless of zoom/pan
35
+ function screenToWorld(screenX: number, screenY: number) {
36
+ return {
37
+ x: (screenX - state.position.x) / state.scale,
38
+ y: (screenY - state.position.y) / state.scale,
39
+ }
40
+ }
41
+
42
+ // Convert world coordinates back to screen coordinates for rendering
43
+ function worldToScreen(worldX: number, worldY: number) {
44
+ return {
45
+ x: worldX * state.scale + state.position.x,
46
+ y: worldY * state.scale + state.position.y,
47
+ }
48
+ }
49
+
50
+ // Check if a click/touch is near a control point
51
+ // Used for interactive dragging of control points
52
+ function getControlPointAt(x: number, y: number, threshold = 20) {
53
+ for (let i = 0; i < state.controlPoints.length; i++) {
54
+ const screen = worldToScreen(state.controlPoints[i]!.x, state.controlPoints[i]!.y)
55
+ const dx = x - screen.x
56
+ const dy = y - screen.y
57
+ // Use distance squared to avoid expensive sqrt calculation
58
+ if (dx * dx + dy * dy < threshold * threshold) {
59
+ return i
60
+ }
61
+ }
62
+ return -1
63
+ }
64
+
65
+ /**********************************************************************************/
66
+ /* */
67
+ /* Event Handlers */
68
+ /* */
69
+ /**********************************************************************************/
70
+
71
+ // Handle mouse/touch interaction for dragging control points or panning the view
72
+ canvas.addEventListener('pointerdown', event => {
73
+ const hitIndex = getControlPointAt(event.clientX, event.clientY)
74
+
75
+ if (hitIndex >= 0) {
76
+ // User clicked on a control point - enable dragging
77
+ state.dragIndex = hitIndex
78
+ cursor(event, event => {
79
+ const world = screenToWorld(event.clientX, event.clientY)
80
+ state.controlPoints[state.dragIndex] = world
81
+ render()
82
+ })
83
+ } else {
84
+ // User clicked on empty space - enable panning
85
+ cursor(event, event => {
86
+ state.position.x -= event.deltaX
87
+ state.position.y -= event.deltaY
88
+ render()
89
+ })
90
+ }
91
+ })
92
+
93
+ // Handle mouse wheel for zooming (with zoom-to-cursor behavior)
94
+ canvas.addEventListener(
95
+ 'wheel',
96
+ event => {
97
+ const rect = canvas.getBoundingClientRect()
98
+ const mouseX = event.clientX - rect.left
99
+ const mouseY = event.clientY - rect.top
100
+
101
+ const scaleFactor = event.deltaY > 0 ? 0.95 : 1.05
102
+ const oldScale = state.scale
103
+ const newScale = Math.max(0.1, Math.min(10, oldScale * scaleFactor))
104
+
105
+ // Calculate the world coordinate under the mouse cursor before scaling
106
+ const worldX = (mouseX - state.position.x) / oldScale
107
+ const worldY = (mouseY - state.position.y) / oldScale
108
+
109
+ // Update scale
110
+ state.scale = newScale
111
+
112
+ // Adjust pan offset to keep the same world point under the cursor
113
+ // This creates the "zoom to cursor" effect
114
+ state.position.x = mouseX - worldX * newScale
115
+ state.position.y = mouseY - worldY * newScale
116
+
117
+ render()
118
+ },
119
+ { passive: true },
120
+ )
121
+
122
+ // Handle window resizing
123
+ window.addEventListener('resize', () => {
124
+ canvas.width = window.innerWidth
125
+ canvas.height = window.innerHeight
126
+ render()
127
+ })
128
+
129
+ /**********************************************************************************/
130
+ /* */
131
+ /* Shaders */
132
+ /* */
133
+ /**********************************************************************************/
134
+
135
+ // Vertex shader for the bezier curve
136
+ // This shader evaluates the bezier curve on the GPU and creates the triangle strip
137
+ const curveVertex = glsl`#version 300 es
138
+ // Attributes (per-vertex data)
139
+ ${interleave('data', [
140
+ attribute.float('a_t'),
141
+ attribute.float('a_side'),
142
+ ])} // Bezier parameter t ∈ [0,1]
143
+
144
+ // Uniforms (shared data for all vertices)
145
+ ${uniform.vec2('u_resolution')} // Canvas size in pixels
146
+ ${uniform.vec2('u_offset')} // Pan offset in pixels
147
+ ${uniform.float('u_scale')} // Zoom level
148
+ ${uniform.vec2('u_p0')} // Control point P0 (start)
149
+ ${uniform.vec2('u_p1')} // Control point P1 (control)
150
+ ${uniform.vec2('u_p2')} // Control point P2 (end)
151
+ ${uniform.float('u_thickness')} // Curve thickness in world units
152
+
153
+ void main() {
154
+ // STEP 1: Evaluate the quadratic bezier curve at parameter t
155
+ // Quadratic bezier formula: B(t) = (1-t)²P0 + 2t(1-t)P1 + t²P2
156
+ float t = a_t;
157
+ float s = 1.0 - t; // s is often used for (1-t) for brevity
158
+
159
+ // Calculate the position on the curve
160
+ vec2 position = s * s * u_p0 + // (1-t)² * P0
161
+ 2.0 * s * t * u_p1 + // 2(1-t)t * P1
162
+ t * t * u_p2; // t² * P2
163
+
164
+ // STEP 2: Calculate the tangent vector at this point
165
+ // The derivative of the bezier curve gives us the tangent
166
+ // B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1)
167
+ vec2 tangent = 2.0 * s * (u_p1 - u_p0) + 2.0 * t * (u_p2 - u_p1);
168
+
169
+ // STEP 3: Create a normal vector (perpendicular to tangent)
170
+ // Rotate tangent 90 degrees: (x,y) → (-y,x)
171
+ vec2 normal = normalize(vec2(-tangent.y, tangent.x));
172
+
173
+ // STEP 4: Offset the curve position to create thickness
174
+ // Move the vertex perpendicular to the curve by thickness * side
175
+ vec2 offsetPosition = position + normal * u_thickness * a_side;
176
+
177
+ // STEP 5: Transform from world space to screen space
178
+ vec2 screenPos = offsetPosition * u_scale + u_offset;
179
+
180
+ // STEP 6: Convert to clip space (OpenGL's coordinate system)
181
+ // Screen space: (0,0) is top-left, (width,height) is bottom-right
182
+ // Clip space: (-1,-1) is bottom-left, (1,1) is top-right
183
+ vec2 clipPos = (screenPos / u_resolution) * 2.0 - 1.0;
184
+ clipPos.y = -clipPos.y; // Flip Y to match screen coordinates
185
+
186
+ gl_Position = vec4(clipPos, 0, 1);
187
+ }`
188
+
189
+ // Fragment shader for the bezier curve
190
+ // Simple solid color for now - could be extended for gradients, textures, etc.
191
+ const curveFragment = glsl`#version 300 es
192
+ precision mediump float;
193
+ out vec4 fragColor;
194
+
195
+ void main() {
196
+ // Output a solid blue color for the curve
197
+ vec3 curveColor = vec3(0.2, 0.6, 1.0);
198
+ fragColor = vec4(curveColor, 1.0);
199
+ }`
200
+
201
+ // Vertex shader for control points (rendered as small squares)
202
+ const pointVertex = glsl`#version 300 es
203
+ ${attribute.vec2('a_vertex')} // Quad vertex position (-1 to 1)
204
+ ${uniform.vec2('u_resolution')} // Canvas size
205
+ ${uniform.vec2('u_offset')} // Pan offset
206
+ ${uniform.float('u_scale')} // Zoom level
207
+ ${uniform.vec2('u_pointPos')} // Control point position in world space
208
+
209
+ void main() {
210
+ // Create a small square around the control point position
211
+ vec2 screenPos = (u_pointPos * u_scale + u_offset) + a_vertex * 8.0;
212
+
213
+ // Transform to clip space
214
+ vec2 clipPos = (screenPos / u_resolution) * 2.0 - 1.0;
215
+ clipPos.y = -clipPos.y;
216
+ gl_Position = vec4(clipPos, 0, 1);
217
+ }`
218
+
219
+ // Fragment shader for control points
220
+ const pointFragment = glsl`#version 300 es
221
+ precision mediump float;
222
+ out vec4 fragColor;
223
+
224
+ void main() {
225
+ // Red color for control points
226
+ fragColor = vec4(1.0, 0.3, 0.3, 1.0);
227
+ }`
228
+
229
+ // Vertex shader for control polygon lines
230
+ // The control polygon connects P0→P1→P2 and helps visualize the curve construction
231
+ const lineVertex = glsl`#version 300 es
232
+ ${attribute.vec2('a_position')} // Line vertex position
233
+ ${uniform.vec2('u_resolution')} // Canvas size
234
+ ${uniform.vec2('u_offset')} // Pan offset
235
+ ${uniform.float('u_scale')} // Zoom level
236
+
237
+ void main() {
238
+ // Transform world position to screen space then clip space
239
+ vec2 screenPos = a_position * u_scale + u_offset;
240
+ vec2 clipPos = (screenPos / u_resolution) * 2.0 - 1.0;
241
+ clipPos.y = -clipPos.y;
242
+ gl_Position = vec4(clipPos, 0, 1);
243
+ }`
244
+
245
+ // Fragment shader for control polygon lines
246
+ const lineFragment = glsl`#version 300 es
247
+ precision mediump float;
248
+ out vec4 fragColor;
249
+
250
+ void main() {
251
+ // Semi-transparent gray for control polygon
252
+ fragColor = vec4(0.5, 0.5, 0.5, 0.5);
253
+ }`
254
+
255
+ /**********************************************************************************/
256
+ /* */
257
+ /* Create Programs */
258
+ /* */
259
+ /**********************************************************************************/
260
+
261
+ // Compile shaders and create shader programs
262
+ const { program: curveProgram, schema: curveSchema } = compile(gl, curveVertex, curveFragment)
263
+ const curveView = view(gl, curveProgram, curveSchema)
264
+
265
+ const { program: pointProgram, schema: pointSchema } = compile(gl, pointVertex, pointFragment)
266
+ const pointView = view(gl, pointProgram, pointSchema)
267
+
268
+ const { program: lineProgram, schema: lineSchema } = compile(gl, lineVertex, lineFragment)
269
+ const lineView = view(gl, lineProgram, lineSchema)
270
+
271
+ /**********************************************************************************/
272
+ /* */
273
+ /* Triangle Strip Generation */
274
+ /* */
275
+ /**********************************************************************************/
276
+
277
+ // Number of segments to divide the curve into
278
+ // More segments = smoother curve but more vertices
279
+ const curveSegments = 100
280
+
281
+ // Generate the vertex data for the triangle strip
282
+ // We create pairs of vertices along the curve that will form the strip
283
+ function generateTriangleStrip() {
284
+ // The triangle strip will connect these vertices like this:
285
+ // t=0: [L0]-[R0]
286
+ // | / |
287
+ // t=0.1: [L1]-[R1]
288
+ // | \ |
289
+ // t=0.2: [L2]-[R2]
290
+ // etc...
291
+
292
+ return new Float32Array(
293
+ (function* () {
294
+ for (let i = 0; i <= curveSegments; i++) {
295
+ const t = i / curveSegments
296
+ yield t
297
+ yield -1
298
+ yield t
299
+ yield 1
300
+ }
301
+ })(),
302
+ )
303
+ /*
304
+ return {
305
+ t: new Float32Array(
306
+ (function* () {
307
+ for (let i = 0; i <= curveSegments; i++) {
308
+ const t = i / curveSegments
309
+ yield t
310
+ yield t
311
+ }
312
+ })(),
313
+ ),
314
+ side: new Float32Array(
315
+ (function* () {
316
+ for (let i = 0; i <= curveSegments; i++) {
317
+ yield -1
318
+ yield 1
319
+ }
320
+ })(),
321
+ ),
322
+ } */
323
+ }
324
+
325
+ // Generate and upload triangle strip data to GPU
326
+ const stripData = generateTriangleStrip()
327
+ curveView.interleavedAttributes.data.set(stripData)
328
+ // curveView.attributes.a_side.set(stripData.side)
329
+
330
+ // prettier-ignore
331
+ // Create geometry for control points (a simple quad)
332
+ const pointVertices = new Float32Array([
333
+ -1, -1, // Bottom-left
334
+ 1, -1, // Bottom-right
335
+ -1, 1, // Top-left
336
+ -1, 1, // Top-left (repeated for second triangle)
337
+ 1, -1, // Bottom-right (repeated for second triangle)
338
+ 1, 1, // Top-right
339
+ ])
340
+ pointView.attributes.a_vertex.set(pointVertices)
341
+
342
+ // Update control polygon line data when control points change
343
+ function updateLineData() {
344
+ const lineVerticesData = new Float32Array([
345
+ state.controlPoints[0].x,
346
+ state.controlPoints[0].y, // P0
347
+ state.controlPoints[1].x,
348
+ state.controlPoints[1].y, // P1
349
+ state.controlPoints[1].x,
350
+ state.controlPoints[1].y, // P1 (repeated)
351
+ state.controlPoints[2].x,
352
+ state.controlPoints[2].y, // P2
353
+ ])
354
+ lineView.attributes.a_position.set(lineVerticesData)
355
+ }
356
+
357
+ updateLineData()
358
+
359
+ /**********************************************************************************/
360
+ /* */
361
+ /* Render Function */
362
+ /* */
363
+ /**********************************************************************************/
364
+
365
+ function render() {
366
+ // Update canvas size if window was resized
367
+ if (canvas.width !== window.innerWidth || canvas.height !== window.innerHeight) {
368
+ canvas.width = window.innerWidth
369
+ canvas.height = window.innerHeight
370
+ }
371
+
372
+ // Set up WebGL viewport and clear the canvas
373
+ gl.viewport(0, 0, canvas.width, canvas.height)
374
+ gl.clearColor(0.1, 0.1, 0.15, 1) // Dark background
375
+ gl.clear(gl.COLOR_BUFFER_BIT)
376
+
377
+ // Enable alpha blending for transparency
378
+ gl.enable(gl.BLEND)
379
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
380
+
381
+ // Update line data when control points change
382
+ updateLineData()
383
+
384
+ // STEP 1: Render control polygon lines
385
+ gl.useProgram(lineProgram)
386
+ lineView.attributes.a_position.bind()
387
+
388
+ lineView.uniforms.u_resolution.set(canvas.width, canvas.height)
389
+ lineView.uniforms.u_offset.set(state.position.x, state.position.y)
390
+ lineView.uniforms.u_scale.set(state.scale)
391
+
392
+ gl.drawArrays(gl.LINES, 0, 4) // 2 lines × 2 vertices = 4 vertices
393
+
394
+ // STEP 2: Render bezier curve as triangle strip
395
+ gl.useProgram(curveProgram)
396
+ curveView.interleavedAttributes.data.bind()
397
+
398
+ // Set uniforms for curve rendering
399
+ curveView.uniforms.u_resolution.set(canvas.width, canvas.height)
400
+ curveView.uniforms.u_offset.set(state.position.x, state.position.y)
401
+ curveView.uniforms.u_scale.set(state.scale)
402
+ curveView.uniforms.u_p0.set(state.controlPoints[0].x, state.controlPoints[0].y)
403
+ curveView.uniforms.u_p1.set(state.controlPoints[1].x, state.controlPoints[1].y)
404
+ curveView.uniforms.u_p2.set(state.controlPoints[2].x, state.controlPoints[2].y)
405
+ curveView.uniforms.u_thickness.set(5.0) // Curve thickness in world units
406
+
407
+ // Draw the triangle strip
408
+ // Total vertices = (segments + 1) × 2 (left and right side for each t)
409
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, (curveSegments + 1) * 2)
410
+ curveView.interleavedAttributes.data.unbind()
411
+
412
+ // STEP 3: Render control points on top
413
+ gl.useProgram(pointProgram)
414
+ pointView.attributes.a_vertex.bind()
415
+
416
+ pointView.uniforms.u_resolution.set(canvas.width, canvas.height)
417
+ pointView.uniforms.u_offset.set(state.position.x, state.position.y)
418
+ pointView.uniforms.u_scale.set(state.scale)
419
+
420
+ // Draw each control point
421
+ for (const point of state.controlPoints) {
422
+ pointView.uniforms.u_pointPos.set(point.x, point.y)
423
+ gl.drawArrays(gl.TRIANGLES, 0, 6) // 2 triangles × 3 vertices = 6 vertices
424
+ }
425
+ }
426
+
427
+ // Initial render
428
+ render()
@@ -0,0 +1,250 @@
1
+ import { FramebufferOptions } from 'src/types'
2
+ import { createFramebuffer } from 'src/utils'
3
+ import { view } from 'view.gl'
4
+ import { attribute, compile, glsl, uniform } from 'view.gl/tag'
5
+
6
+ let playing = false
7
+
8
+ const canvas = document.createElement('canvas')
9
+ const gl = canvas.getContext('webgl', { antialias: false })!
10
+ if (!gl) {
11
+ throw new Error('WebGL not supported')
12
+ }
13
+ document.body.append(canvas)
14
+
15
+ function spray() {
16
+ return Math.floor(Math.random() * 8) * (Math.random() < 0.5 ? 1 : -1)
17
+ }
18
+
19
+ canvas.addEventListener('mousedown', event => {
20
+ const rect = canvas.getBoundingClientRect()
21
+ const controller = new AbortController()
22
+ playing = false
23
+ window.addEventListener(
24
+ 'mousemove',
25
+ event => {
26
+ const x = Math.floor((event.clientX - rect.left - spray()) * (canvas.width / rect.width))
27
+ const y = Math.floor((rect.bottom - event.clientY - spray()) * (canvas.height / rect.height))
28
+
29
+ const dimensions = [2, 2] as const
30
+
31
+ // Set red=255, alive; other channels remain 0/255
32
+ const data = new Uint8Array(
33
+ (function* () {
34
+ for (let i = 0; i < dimensions[0] * dimensions[1]; i++) {
35
+ yield 255
36
+ yield 0
37
+ yield 0
38
+ yield 255
39
+ }
40
+ })(),
41
+ )
42
+
43
+ gl.bindTexture(gl.TEXTURE_2D, read.texture)
44
+ gl.texSubImage2D(
45
+ gl.TEXTURE_2D,
46
+ 0,
47
+ x,
48
+ y,
49
+ dimensions[0],
50
+ dimensions[1],
51
+ gl.RGBA,
52
+ gl.UNSIGNED_BYTE,
53
+ data,
54
+ )
55
+
56
+ gl.bindTexture(gl.TEXTURE_2D, write.texture)
57
+ gl.texSubImage2D(
58
+ gl.TEXTURE_2D,
59
+ 0,
60
+ x,
61
+ y,
62
+ dimensions[0],
63
+ dimensions[1],
64
+ gl.RGBA,
65
+ gl.UNSIGNED_BYTE,
66
+ data,
67
+ )
68
+
69
+ render()
70
+ },
71
+ controller,
72
+ )
73
+ window.addEventListener(
74
+ 'mouseup',
75
+ () => {
76
+ controller.abort()
77
+ playing = true
78
+ animate()
79
+ },
80
+ controller,
81
+ )
82
+ })
83
+
84
+ canvas.width = 256 * 2
85
+ canvas.height = 256 * 2
86
+ const WIDTH = canvas.width
87
+ const HEIGHT = canvas.height
88
+
89
+ // --- Shaders ---
90
+
91
+ // Vertex shader (simple fullscreen quad)
92
+ const vertex = glsl`
93
+ ${attribute.vec2('a_vertex')}
94
+ varying vec2 v_uv;
95
+ void main() {
96
+ v_uv = a_vertex * 0.5 + 0.5;
97
+ gl_Position = vec4(a_vertex, 0, 1);
98
+ }
99
+ `
100
+
101
+ // Fragment shader for Game of Life step
102
+ // We read from u_texture the current state
103
+ // Alive cells are red > 0.5, dead < 0.5
104
+ // Compute neighbors, apply rules, output next state (red channel)
105
+ const stepFragment = glsl`
106
+ precision mediump float;
107
+ varying vec2 v_uv;
108
+ ${uniform.sampler2D('u_texture')}
109
+ ${uniform.vec2('u_texelSize')}
110
+
111
+ int getCell(vec2 uv) {
112
+ // Wrap around (toroidal)
113
+ uv = mod(uv, 1.0);
114
+ float val = texture2D(u_texture, uv).r;
115
+ return int(step(0.5, val));
116
+ }
117
+
118
+ void main() {
119
+ int sum = 0;
120
+ // Iterate neighbors -1,0,1
121
+ for (int y = -1; y <= 1; y++) {
122
+ for (int x = -1; x <= 1; x++) {
123
+ if (x == 0 && y == 0) continue;
124
+ sum += getCell(v_uv + vec2(float(x), float(y)) * u_texelSize);
125
+ }
126
+ }
127
+ int current = getCell(v_uv);
128
+
129
+ int nextState = 0;
130
+ if (current == 1) {
131
+ if (sum == 2 || sum == 3) nextState = 1;
132
+ else nextState = 0;
133
+ } else {
134
+ if (sum == 3) nextState = 1;
135
+ else nextState = 0;
136
+ }
137
+
138
+ gl_FragColor = vec4(float(nextState), 0.0, 0.0, 1.0);
139
+ }`
140
+
141
+ // Fragment shader for rendering the state texture to screen
142
+ const renderFragment = glsl`
143
+ precision mediump float;
144
+ varying vec2 v_uv;
145
+ ${uniform.sampler2D('u_texture')}
146
+
147
+ void main() {
148
+ float cell = texture2D(u_texture, v_uv).r;
149
+ gl_FragColor = vec4(vec3(cell), 1.0);
150
+ }`
151
+
152
+ // --- Create programs ---
153
+ const { program: stepProgram, schema: stepSchema } = compile(gl, vertex, stepFragment)
154
+ const stepView = view(gl, stepProgram, stepSchema)
155
+
156
+ const { program: renderProgram, schema: renderSchema } = compile(gl, vertex, renderFragment)
157
+ const renderView = view(gl, renderProgram, {
158
+ ...renderSchema,
159
+ attributes: {
160
+ ...renderSchema.attributes,
161
+ a_vertex: {
162
+ ...renderSchema.attributes.a_vertex,
163
+ buffer: stepView.attributes.a_vertex.buffer,
164
+ },
165
+ },
166
+ })
167
+
168
+ // Setup quad
169
+ stepView.attributes.a_vertex.set(new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]))
170
+
171
+ // Initialize two framebuffers (with their respective texture) for ping-pong
172
+ const framebufferOptions = {
173
+ attachment: 'color',
174
+ width: WIDTH,
175
+ height: HEIGHT,
176
+ type: 'UNSIGNED_BYTE',
177
+ magFilter: 'NEAREST',
178
+ minFilter: 'NEAREST',
179
+ } satisfies FramebufferOptions
180
+
181
+ let read = createFramebuffer(gl, framebufferOptions)
182
+ let write = createFramebuffer(gl, framebufferOptions)
183
+
184
+ // Initialize state texture (random live/dead)
185
+ gl.bindTexture(gl.TEXTURE_2D, read.texture)
186
+ gl.texSubImage2D(
187
+ gl.TEXTURE_2D,
188
+ 0,
189
+ 0,
190
+ 0,
191
+ WIDTH,
192
+ HEIGHT,
193
+ gl.RGBA,
194
+ gl.UNSIGNED_BYTE,
195
+ new Uint8Array(
196
+ (function* () {
197
+ for (let i = 0; i < WIDTH * HEIGHT; i++) {
198
+ yield 0
199
+ yield 0
200
+ yield 0
201
+ yield 255
202
+ }
203
+ })(),
204
+ ),
205
+ )
206
+
207
+ function step() {
208
+ // Run Game of Life step: render to write-feedbackBuffer using read-texture as input
209
+ gl.bindFramebuffer(gl.FRAMEBUFFER, write.framebuffer)
210
+
211
+ gl.viewport(0, 0, WIDTH, HEIGHT)
212
+ gl.useProgram(stepProgram)
213
+
214
+ gl.activeTexture(gl.TEXTURE0)
215
+ gl.bindTexture(gl.TEXTURE_2D, read.texture)
216
+
217
+ stepView.uniforms.u_texture.set(0)
218
+ stepView.uniforms.u_texelSize.set(1 / WIDTH, 1 / HEIGHT)
219
+
220
+ stepView.attributes.a_vertex.bind()
221
+
222
+ gl.drawArrays(gl.TRIANGLES, 0, 6)
223
+ }
224
+
225
+ function render() {
226
+ // Now render to screen
227
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
228
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
229
+ gl.useProgram(renderProgram)
230
+
231
+ gl.activeTexture(gl.TEXTURE0)
232
+ gl.bindTexture(gl.TEXTURE_2D, write.texture)
233
+ renderView.uniforms.u_texture.set(0)
234
+
235
+ renderView.attributes.a_vertex.bind()
236
+
237
+ gl.drawArrays(gl.TRIANGLES, 0, 6)
238
+ }
239
+
240
+ function animate() {
241
+ if (playing) {
242
+ step()
243
+ render()
244
+ // Swap read and write buffers and textures
245
+ ;[read, write] = [write, read]
246
+ requestAnimationFrame(animate)
247
+ }
248
+ }
249
+ playing = true
250
+ animate()