@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/pix_sim.ts ADDED
@@ -0,0 +1,549 @@
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
+ export const MATERIALS = {
7
+ SAND: {
8
+ color: [1, 0.8, 0], // Yellow sand
9
+ id: 1,
10
+ },
11
+ WATER: {
12
+ color: [0, 0.5, 1], // Blue water
13
+ id: 2,
14
+ },
15
+ STONE: {
16
+ color: [0.5, 0.5, 0.5], // Gray stone
17
+ id: 3,
18
+ },
19
+ } as const
20
+
21
+ const MATERIAL_SIZE = Object.keys(MATERIALS).length
22
+
23
+ let playing = false
24
+ let currentMaterial = MATERIALS.SAND.id
25
+
26
+ const canvas = document.createElement('canvas')
27
+ const gl = canvas.getContext('webgl2', { antialias: false })!
28
+ if (!gl) {
29
+ throw new Error('WebGL not supported')
30
+ }
31
+
32
+ // Create UI container
33
+ const container = document.createElement('div')
34
+ container.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 10px;'
35
+
36
+ // Create material buttons
37
+ const buttonContainer = document.createElement('div')
38
+ buttonContainer.style.cssText = 'display: flex; gap: 10px; margin-bottom: 10px;'
39
+
40
+ Object.entries(MATERIALS).forEach(([name, material]) => {
41
+ const button = document.createElement('button')
42
+ button.textContent = name.toLowerCase()
43
+ button.style.cssText = `
44
+ padding: 5px 10px;
45
+ font-size: 14px;
46
+ font-family: times new roman;
47
+ cursor: pointer;
48
+ background: ${
49
+ currentMaterial === material.id
50
+ ? `rgb(${material.color.map(c => Math.floor(c * 255)).join(',')})`
51
+ : 'transparent'
52
+ };
53
+ border-color: ${name === 'SAND' ? 'black' : 'white'};
54
+ border: 2px solid rgb(${material.color.map(c => Math.floor(c * 255)).join(',')});
55
+ border-radius: 2px;
56
+ `
57
+ button.onclick = () => {
58
+ currentMaterial = material.id
59
+ // Update all button borders
60
+ buttonContainer.querySelectorAll('button').forEach((btn, idx) => {
61
+ btn.style.background =
62
+ idx === material.id - 1
63
+ ? `rgb(${material.color.map(c => Math.floor(c * 255)).join(',')})`
64
+ : 'transparent'
65
+ })
66
+ }
67
+ buttonContainer.appendChild(button)
68
+ })
69
+
70
+ container.appendChild(canvas)
71
+ container.appendChild(buttonContainer)
72
+ document.body.append(container)
73
+
74
+ function spray() {
75
+ return Math.floor(Math.random() * 8) * (Math.random() < 0.5 ? 1 : -1)
76
+ }
77
+
78
+ canvas.addEventListener('mousedown', event => {
79
+ const rect = canvas.getBoundingClientRect()
80
+ const controller = new AbortController()
81
+ // playing = false
82
+ window.addEventListener(
83
+ 'mousemove',
84
+ event => {
85
+ const x = Math.floor((event.clientX - rect.left + spray()) * (canvas.width / rect.width))
86
+ const y = Math.floor((rect.bottom - event.clientY + spray()) * (canvas.height / rect.height))
87
+
88
+ const dimensions = [8, 8] as const
89
+
90
+ // Encode selected material using bit encoding
91
+ const data = new Uint8Array(
92
+ (function* () {
93
+ for (let i = 0; i < dimensions[0] * dimensions[1]; i++) {
94
+ yield (currentMaterial & 1) * 255 // Red channel (bit 0)
95
+ yield ((currentMaterial >> 1) & 1) * 255 // Green channel (bit 1)
96
+ yield ((currentMaterial >> 2) & 1) * 255 // Blue channel (bit 2)
97
+ yield 255 // Alpha channel
98
+ }
99
+ })(),
100
+ )
101
+
102
+ // Update both read and write textures
103
+ gl.bindTexture(gl.TEXTURE_2D, read.texture)
104
+ gl.texSubImage2D(
105
+ gl.TEXTURE_2D,
106
+ 0,
107
+ Math.max(0, Math.min(WIDTH - dimensions[0], x)),
108
+ Math.max(0, Math.min(HEIGHT - dimensions[1], y)),
109
+ dimensions[0],
110
+ dimensions[1],
111
+ gl.RGBA,
112
+ gl.UNSIGNED_BYTE,
113
+ data,
114
+ )
115
+
116
+ gl.bindTexture(gl.TEXTURE_2D, write.texture)
117
+ gl.texSubImage2D(
118
+ gl.TEXTURE_2D,
119
+ 0,
120
+ Math.max(0, Math.min(WIDTH - dimensions[0], x)),
121
+ Math.max(0, Math.min(HEIGHT - dimensions[1], y)),
122
+ dimensions[0],
123
+ dimensions[1],
124
+ gl.RGBA,
125
+ gl.UNSIGNED_BYTE,
126
+ data,
127
+ )
128
+
129
+ render()
130
+ },
131
+ controller,
132
+ )
133
+ window.addEventListener(
134
+ 'mouseup',
135
+ () => {
136
+ controller.abort()
137
+ // playing = true
138
+ // animate()
139
+ },
140
+ controller,
141
+ )
142
+ })
143
+
144
+ canvas.width = 256 * 2
145
+ canvas.height = 256 * 2
146
+ const WIDTH = canvas.width
147
+ const HEIGHT = canvas.height
148
+
149
+ // --- Shaders ---
150
+
151
+ // Vertex shader (simple fullscreen quad)
152
+ const vertex = glsl`#version 300 es
153
+ ${attribute.vec2('a_vertex')}
154
+ out vec2 v_uv;
155
+ void main() {
156
+ v_uv = a_vertex * 0.5 + 0.5;
157
+ gl_Position = vec4(a_vertex, 0, 1);
158
+ }`
159
+
160
+ // Fragment shader for sand simulation step
161
+ const stepFragment = glsl`#version 300 es
162
+ precision highp float;
163
+ in vec2 v_uv;
164
+ ${uniform.sampler2D('u_texture')}
165
+ ${uniform.vec2('u_texelSize')}
166
+ out vec4 fragColor;
167
+
168
+ // Material constants (like enums)
169
+ const int MAT_EMPTY = 0;
170
+ const int MAT_SAND = 1;
171
+ const int MAT_WATER = 2;
172
+ const int MAT_STONE = 3;
173
+
174
+ // Direction constants
175
+ const vec2 DIR_UP = vec2(0.0, 1.0);
176
+ const vec2 DIR_DOWN = vec2(0.0, -1.0);
177
+ const vec2 DIR_LEFT = vec2(-1.0, 0.0);
178
+ const vec2 DIR_RIGHT = vec2(1.0, 0.0);
179
+
180
+ // Helper function to decode material from RGB
181
+ int getMaterial(vec4 color) {
182
+ return int(color.r + color.g * 2.0 + color.b * 4.0);
183
+ }
184
+
185
+ // Helper function to encode material to RGB
186
+ vec4 encodeMaterial(int material) {
187
+ return vec4(
188
+ float(material & 1),
189
+ float((material >> 1) & 1),
190
+ float((material >> 2) & 1),
191
+ 1.0
192
+ );
193
+ }
194
+
195
+ void main() {
196
+ // Sample the current state
197
+ vec4 current = texture(u_texture, v_uv);
198
+
199
+ // Check boundaries
200
+ bool atBottom = v_uv.y - u_texelSize.y < 0.0;
201
+ bool atLeft = v_uv.x - u_texelSize.x < 0.0;
202
+ bool atRight = v_uv.x + u_texelSize.x > 1.0;
203
+ bool atTop = v_uv.y + u_texelSize.y > 1.0;
204
+
205
+ // Default: keep current state
206
+ fragColor = current;
207
+
208
+ int material = getMaterial(current);
209
+
210
+ int below = getMaterial(texture(u_texture, v_uv + DIR_DOWN * u_texelSize));
211
+ int above = getMaterial(texture(u_texture, v_uv + DIR_UP * u_texelSize));
212
+ int aboveLeft = getMaterial(texture(u_texture, v_uv + (DIR_UP + DIR_LEFT) * u_texelSize));
213
+ int aboveRight = getMaterial(texture(u_texture, v_uv + (DIR_UP + DIR_RIGHT) * u_texelSize));
214
+ int belowLeft = getMaterial(texture(u_texture, v_uv + (DIR_DOWN + DIR_LEFT) * u_texelSize));
215
+ int belowRight = getMaterial(texture(u_texture, v_uv + (DIR_DOWN + DIR_RIGHT) * u_texelSize));
216
+
217
+ // Physics simulation using switch
218
+ switch (material) {
219
+ case MAT_EMPTY:{
220
+ // Empty cell - check what can fall/flow into it
221
+
222
+ // Check for sand from above (highest priority - sand is denser)
223
+ bool sandDirectlyAbove = !atTop && above == MAT_SAND;
224
+ if (sandDirectlyAbove) {
225
+ fragColor = encodeMaterial(MAT_SAND);
226
+ break;
227
+ }
228
+
229
+ // Check for sand sliding from diagonal
230
+ bool sandAboveLeft = !atTop && !atRight && aboveLeft == MAT_SAND;
231
+ bool sandAboveRight = !atTop && !atLeft && aboveRight == MAT_SAND;
232
+
233
+ if (sandAboveLeft) {
234
+ vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
235
+ bool leftBlocked = getMaterial(left) != MAT_EMPTY;
236
+ bool canSlideFromLeft = leftBlocked || atBottom;
237
+ if (canSlideFromLeft) {
238
+ fragColor = encodeMaterial(MAT_SAND);
239
+ break;
240
+ }
241
+ }
242
+
243
+ if (sandAboveRight) {
244
+ vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
245
+ bool rightBlocked = getMaterial(right) != MAT_EMPTY;
246
+ bool canSlideFromRight = rightBlocked || atBottom;
247
+ if (canSlideFromRight) {
248
+ fragColor = encodeMaterial(MAT_SAND);
249
+ break;
250
+ }
251
+ }
252
+
253
+ // Check for water from above
254
+ bool waterDirectlyAbove = !atTop && above == MAT_WATER;
255
+ if (waterDirectlyAbove) {
256
+ fragColor = encodeMaterial(MAT_WATER);
257
+ break;
258
+ }
259
+
260
+ // Check for water sliding from diagonal
261
+ bool waterAboveLeft = !atTop && !atRight && aboveLeft == MAT_WATER;
262
+ bool waterAboveRight = !atTop && !atLeft && aboveRight == MAT_WATER;
263
+
264
+ if (waterAboveLeft) {
265
+ vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
266
+ bool sandToLeft = getMaterial(left) == MAT_SAND;
267
+ if (!sandToLeft) {
268
+ fragColor = encodeMaterial(MAT_WATER);
269
+ break;
270
+ }
271
+ }
272
+
273
+ if (waterAboveRight) {
274
+ vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
275
+ bool sandToRight = getMaterial(right) == MAT_SAND;
276
+ if (!sandToRight) {
277
+ fragColor = encodeMaterial(MAT_WATER);
278
+ break;
279
+ }
280
+ }
281
+
282
+ // Check for water flowing horizontally
283
+ vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
284
+ vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
285
+ int matLeft = getMaterial(left);
286
+ int matRight = getMaterial(right);
287
+
288
+ bool waterToLeft = !atRight && matLeft == MAT_WATER;
289
+ bool waterToRight = !atLeft && matRight == MAT_WATER;
290
+
291
+ if (!waterToLeft && !waterToRight) break;
292
+
293
+ // Random choice for symmetric flow
294
+ float rand = fract(sin(dot(v_uv * 1000.0, vec2(12.9898, 78.233))) * 43758.5453);
295
+ bool preferLeft = rand < 0.5;
296
+
297
+ if (
298
+ waterToLeft && waterToRight ||
299
+ waterToLeft ||
300
+ waterToRight
301
+ ) {
302
+ fragColor = encodeMaterial(MAT_WATER);
303
+ break;
304
+ }
305
+ }
306
+
307
+ case MAT_SAND: {
308
+ // Sand particle - check if it should fall
309
+ bool canFallThrough = below == MAT_EMPTY || below == MAT_WATER;
310
+ bool canFallDown = !atBottom && canFallThrough;
311
+
312
+ if (canFallDown) {
313
+ fragColor = encodeMaterial(MAT_EMPTY);
314
+ break;
315
+ }
316
+
317
+ // Check diagonal falling
318
+ bool canFallThroughLeft = belowLeft == MAT_EMPTY || belowLeft == MAT_WATER;
319
+ bool canFallThroughRight = belowRight == MAT_EMPTY || belowRight == MAT_WATER;
320
+
321
+ bool canFallDownLeft = !atBottom && !atLeft && canFallThroughLeft;
322
+ bool canFallDownRight = !atBottom && !atRight && canFallThroughRight;
323
+
324
+ if (canFallDownLeft || canFallDownRight) {
325
+ fragColor = encodeMaterial(MAT_EMPTY);
326
+ break;
327
+ }
328
+
329
+ // Sand stays in place
330
+ break;
331
+ }
332
+
333
+ case MAT_WATER: {
334
+ // Water - check if being displaced by sand first
335
+ bool sandFallingFromAbove = !atTop && above == MAT_SAND;
336
+ if (sandFallingFromAbove) {
337
+ fragColor = encodeMaterial(MAT_SAND);
338
+ break;
339
+ }
340
+
341
+ // Check sand sliding into water from diagonals
342
+ bool sandFromAboveLeft = !atTop && !atRight && aboveLeft == MAT_SAND;
343
+ bool sandFromAboveRight = !atTop && !atLeft && aboveRight == MAT_SAND;
344
+
345
+ if (sandFromAboveLeft) {
346
+ vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
347
+ bool leftNotEmpty = getMaterial(left) != MAT_EMPTY;
348
+ bool sandCanSlideIn = leftNotEmpty || atBottom;
349
+
350
+ if (sandCanSlideIn) {
351
+ fragColor = encodeMaterial(MAT_SAND);
352
+ break;
353
+ }
354
+ }
355
+
356
+ if (sandFromAboveRight) {
357
+ vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
358
+ bool rightNotEmpty = getMaterial(right) != MAT_EMPTY;
359
+ bool sandCanSlideIn = rightNotEmpty || atBottom;
360
+
361
+ if (sandCanSlideIn) {
362
+ fragColor = encodeMaterial(MAT_SAND);
363
+ break;
364
+ }
365
+ }
366
+
367
+ // Normal water physics - falling
368
+ bool canFallDown = !atBottom && below == MAT_EMPTY;
369
+ bool canFlowDownLeft = !atBottom && !atLeft && belowLeft == MAT_EMPTY;
370
+ bool canFlowDownRight = !atBottom && !atRight && belowRight == MAT_EMPTY;
371
+
372
+ if (canFallDown || canFlowDownLeft || canFlowDownRight) {
373
+ fragColor = encodeMaterial(MAT_EMPTY);
374
+ break;
375
+ }
376
+
377
+ // Horizontal spreading
378
+ vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
379
+ vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
380
+ int matLeft = getMaterial(left);
381
+ int matRight = getMaterial(right);
382
+
383
+ bool canFlowLeft = !atLeft && matLeft == MAT_EMPTY;
384
+ bool canFlowRight = !atRight && matRight == MAT_EMPTY;
385
+
386
+ if (!canFlowLeft && !canFlowRight) {
387
+ break;
388
+ }
389
+
390
+ // Random horizontal movement for fluid behavior
391
+ float rand = fract(sin(dot(v_uv * 1000.0, vec2(12.9898, 78.233))) * 43758.5453);
392
+ bool preferLeft = rand < 0.5;
393
+
394
+ if (
395
+ canFlowLeft && preferLeft ||
396
+ canFlowRight && !preferLeft ||
397
+ canFlowLeft ||
398
+ canFlowDownRight
399
+ ) {
400
+ fragColor = encodeMaterial(MAT_EMPTY);
401
+ break;
402
+ }
403
+
404
+ }
405
+
406
+ case MAT_STONE:
407
+ // Stone doesn't move
408
+ break;
409
+
410
+ default:
411
+ // Unknown material - keep as is
412
+ break;
413
+ }
414
+ }`
415
+
416
+ // Fragment shader for rendering the state texture to screen
417
+ const renderFragment = glsl`#version 300 es
418
+ precision mediump float;
419
+ in vec2 v_uv;
420
+ ${uniform.sampler2D('u_texture')}
421
+ ${uniform.vec3('u_palette', { size: MATERIAL_SIZE })}
422
+ out vec4 fragColor;
423
+
424
+ // Material constants (must match stepFragment)
425
+ const int MAT_EMPTY = 0;
426
+ const int MAT_SAND = 1;
427
+ const int MAT_WATER = 2;
428
+ const int MAT_STONE = 3;
429
+
430
+ // Helper function to decode material from RGB
431
+ int getMaterial(vec4 color) {
432
+ return int(color.r + color.g * 2.0 + color.b * 4.0);
433
+ }
434
+
435
+ void main() {
436
+ vec4 texel = texture(u_texture, v_uv);
437
+ int material = getMaterial(texel);
438
+
439
+ if (material == MAT_EMPTY) {
440
+ fragColor = vec4(0.0, 0.0, 0.0, 1.0); // Black background
441
+ } else if (material > 0 && material <= ${MATERIAL_SIZE}) {
442
+ fragColor = vec4(u_palette[material - 1], 1.0);
443
+ } else {
444
+ fragColor = vec4(1.0, 0.0, 1.0, 1.0); // Magenta for unknown materials
445
+ }
446
+ }`
447
+
448
+ // --- Create programs ---
449
+ const { program: stepProgram, schema: stepSchema } = compile(gl, vertex, stepFragment)
450
+ const stepView = view(gl, stepProgram, stepSchema)
451
+
452
+ const { program: renderProgram, schema: renderSchema } = compile(gl, vertex, renderFragment)
453
+ const renderView = view(gl, renderProgram, {
454
+ ...renderSchema,
455
+ attributes: {
456
+ ...renderSchema.attributes,
457
+ a_vertex: {
458
+ ...renderSchema.attributes.a_vertex,
459
+ buffer: stepView.attributes.a_vertex.buffer,
460
+ },
461
+ },
462
+ })
463
+
464
+ gl.useProgram(renderProgram)
465
+ renderView.uniforms.u_palette.set(
466
+ new Float32Array(Object.values(MATERIALS).flatMap(({ color }) => color)),
467
+ )
468
+
469
+ gl.useProgram(stepProgram)
470
+ // Setup quad
471
+ stepView.attributes.a_vertex.set(new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]))
472
+
473
+ // Initialize two framebuffers (with their respective texture) for ping-pong
474
+ const framebufferOptions = {
475
+ attachment: 'color',
476
+ width: WIDTH,
477
+ height: HEIGHT,
478
+ type: 'UNSIGNED_BYTE',
479
+ magFilter: 'NEAREST',
480
+ minFilter: 'NEAREST',
481
+ internalFormat: 'RGBA',
482
+ format: 'RGBA',
483
+ } satisfies FramebufferOptions
484
+
485
+ let read = createFramebuffer(gl, framebufferOptions)
486
+ let write = createFramebuffer(gl, framebufferOptions)
487
+
488
+ // Initialize state texture (empty)
489
+ const initData = new Uint8Array(
490
+ (function* () {
491
+ for (let i = 0; i < WIDTH * HEIGHT; i++) {
492
+ yield 0 // Red channel (material ID)
493
+ yield 0 // Green channel
494
+ yield 0 // Blue channel
495
+ yield 255 // Alpha channel
496
+ }
497
+ })(),
498
+ )
499
+
500
+ gl.bindTexture(gl.TEXTURE_2D, read.texture)
501
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, WIDTH, HEIGHT, gl.RGBA, gl.UNSIGNED_BYTE, initData)
502
+
503
+ gl.bindTexture(gl.TEXTURE_2D, write.texture)
504
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, WIDTH, HEIGHT, gl.RGBA, gl.UNSIGNED_BYTE, initData)
505
+
506
+ function step() {
507
+ // Run sand simulation step: render to write-framebuffer using read-texture as input
508
+ gl.bindFramebuffer(gl.FRAMEBUFFER, write.framebuffer)
509
+ gl.viewport(0, 0, WIDTH, HEIGHT)
510
+ gl.useProgram(stepProgram)
511
+
512
+ gl.activeTexture(gl.TEXTURE0)
513
+ gl.bindTexture(gl.TEXTURE_2D, read.texture)
514
+
515
+ stepView.uniforms.u_texture.set(0)
516
+ stepView.uniforms.u_texelSize.set(1 / WIDTH, 1 / HEIGHT)
517
+
518
+ stepView.attributes.a_vertex.bind()
519
+ gl.drawArrays(gl.TRIANGLES, 0, 6)
520
+ }
521
+
522
+ function render() {
523
+ // Render to screen
524
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
525
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
526
+ gl.useProgram(renderProgram)
527
+
528
+ gl.activeTexture(gl.TEXTURE0)
529
+ gl.bindTexture(gl.TEXTURE_2D, write.texture)
530
+
531
+ renderView.uniforms.u_texture.set(0)
532
+ renderView.attributes.a_vertex.bind()
533
+
534
+ gl.drawArrays(gl.TRIANGLES, 0, 6)
535
+ }
536
+
537
+ function animate() {
538
+ if (playing) {
539
+ step()
540
+ render()
541
+ // Swap read and write buffers
542
+ ;[read, write] = [write, read]
543
+ requestAnimationFrame(animate)
544
+ }
545
+ }
546
+
547
+ // Start animation
548
+ playing = true
549
+ animate()