@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/.claude/settings.local.json +8 -0
- package/.prettierrc +10 -0
- package/README.md +338 -0
- package/dev/bezier.ts +428 -0
- package/dev/game_of_life.ts +250 -0
- package/dev/grid.ts +223 -0
- package/dev/index.html +20 -0
- package/dev/index.ts +94 -0
- package/dev/pix_sim.ts +549 -0
- package/dev/tetris.ts +439 -0
- package/dev/tsconfig.json +4 -0
- package/dev/utils.ts +61 -0
- package/dev/vite.config.ts +42 -0
- package/package.json +29 -0
- package/src/features.ts +59 -0
- package/src/index.ts +313 -0
- package/src/tag.ts +193 -0
- package/src/types.ts +718 -0
- package/src/utils.ts +266 -0
- package/tsconfig.json +30 -0
- package/tsup.config.ts +11 -0
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()
|