@bigmistqke/view.gl 0.2.0 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +94 -27
  2. package/dev/dist/404.html +19 -0
  3. package/dev/dist/assets/bezier-DrA4cU_H.js +292 -0
  4. package/dev/dist/assets/game-of-life-BYlzWhEL.js +186 -0
  5. package/dev/dist/assets/glsl-composition-D_BaFFij.js +97 -0
  6. package/dev/dist/assets/grid-DfXHNLBm.js +181 -0
  7. package/dev/dist/assets/index-C2N3u8wj.css +12 -0
  8. package/dev/dist/assets/index-D-N0FDYs.js +110 -0
  9. package/dev/dist/assets/main-CSE2fki3.js +32 -0
  10. package/dev/dist/assets/overview-IeUhUflE.js +80 -0
  11. package/dev/dist/assets/pix-sim-DMJJA_WO.js +500 -0
  12. package/dev/dist/assets/ray-casting-DTQetDPd.js +411 -0
  13. package/dev/dist/assets/ray-casting-modules-Dkfw4tdc.js +765 -0
  14. package/dev/dist/assets/sdf-CS0uBonx.js +66 -0
  15. package/dev/dist/assets/symbol-BL_7GJ0M.js +67 -0
  16. package/dev/dist/assets/tag-C0Vskpp8.js +833 -0
  17. package/dev/dist/assets/tetris-D8mokodr.js +344 -0
  18. package/dev/dist/assets/utils-2dzuv_bW.js +62 -0
  19. package/dev/dist/index.html +51 -0
  20. package/dist/chunk-BZDGEIOT.mjs +278 -0
  21. package/dist/chunk-BZDGEIOT.mjs.map +1 -0
  22. package/dist/{chunk-EYXOSM7B.mjs → chunk-H56LJYS5.mjs} +63 -133
  23. package/dist/chunk-H56LJYS5.mjs.map +1 -0
  24. package/dist/gl.d.mts +2 -2
  25. package/dist/gl.mjs +1 -1
  26. package/dist/index.d.mts +13 -4
  27. package/dist/index.mjs +7 -2
  28. package/dist/tag.d.mts +6 -1
  29. package/dist/tag.mjs +19 -6
  30. package/dist/tag.mjs.map +1 -1
  31. package/dist/{types-CyGyGi18.d.mts → types-k1Xz1iO3.d.mts} +44 -2
  32. package/package.json +1 -1
  33. package/src/gl.ts +13 -2
  34. package/src/index.ts +63 -39
  35. package/src/tag.ts +23 -2
  36. package/src/types.ts +43 -1
  37. package/src/utils.ts +45 -10
  38. package/test/index.test.ts +145 -29
  39. package/test/mocks/webgl.ts +14 -0
  40. package/test/tag.test.ts +41 -0
  41. package/dist/chunk-CJTT7WPV.mjs +0 -75
  42. package/dist/chunk-CJTT7WPV.mjs.map +0 -1
  43. package/dist/chunk-EYXOSM7B.mjs.map +0 -1
package/README.md CHANGED
@@ -60,7 +60,7 @@ bun add @bigmistqke/view.gl
60
60
 
61
61
  ## 👁️ view.gl
62
62
 
63
- The view system provides type-safe WebGL resource management for uniforms, attributes, and buffers
63
+ The view system provides type-safe WebGL resource management for uniforms and attributes
64
64
 
65
65
  ### 🚀 Basic Usage
66
66
 
@@ -74,9 +74,6 @@ const { uniforms, attributes } = view(gl, program, {
74
74
  position: { kind: 'vec3' },
75
75
  uv: { kind: 'vec2' },
76
76
  },
77
- buffers: {
78
- indices: { target: 'ELEMENT_ARRAY_BUFFER' },
79
- },
80
77
  })
81
78
 
82
79
  // Type-safe uniform setting
@@ -93,7 +90,7 @@ attributes.position.bind()
93
90
  The `view()` function creates type-safe WebGL resource managers from a schema.
94
91
 
95
92
  ```typescript
96
- const { uniforms, attributes, buffers } = view(gl, program, schema)
93
+ const { uniforms, attributes } = view(gl, program, schema)
97
94
  ```
98
95
 
99
96
  **Parameters:**
@@ -106,14 +103,29 @@ const { uniforms, attributes, buffers } = view(gl, program, schema)
106
103
 
107
104
  - `uniforms`: Type-safe uniform setters
108
105
  - `attributes`: Attribute managers with buffer handling
109
- - `buffers`: Generic buffer managers
106
+ - `dispose`: Deletes everything the view made — see [Disposal](#-disposal)
110
107
 
111
108
  #### 📋 ViewSchema
112
109
  The complete schema object that defines all WebGL resources. Contains mappings for:
113
110
  - [`UniformSchema`](#-uniformschema) - uniform variable definitions
114
111
  - [`AttributeSchema`](#-attributeschema) - vertex attribute definitions
115
112
  - [`InterleavedAttributeSchema`](#-interleavedattributeschema) - interleaved vertex data definitions
116
- - [`BufferSchema`](#-bufferschema) - generic buffer definitions
113
+ - [`BufferSchema`](#-bufferschema) - buffers the view owns
114
+
115
+ A buffer is the one schema member with no name in the source, so it is not
116
+ resolved against the program the way a uniform or an attribute is — a buffer key
117
+ is a label for the reader. What putting it in the schema decides is its
118
+ **lifetime**, and that is the question worth answering at the declaration:
119
+
120
+ | the buffer | declare it | disposed by |
121
+ | --- | --- | --- |
122
+ | lives and dies with the view | `buffers:` in the schema | the view |
123
+ | outlives the view, or is shared between views | a standalone [`bufferView`](#-bufferview) | you |
124
+ | belongs to somebody else | name it on an attribute | nobody — it is borrowed |
125
+
126
+ An index buffer whose lifetime is its program's belongs in the schema, beside
127
+ the attributes it is drawn with, on the one signal. A quad cached per context,
128
+ or a buffer handed from one program's view to another's, is its own thing.
117
129
 
118
130
  <details>
119
131
  <summary>TypeScript Types</summary>
@@ -123,12 +135,52 @@ interface ViewSchema {
123
135
  uniforms: UniformSchema
124
136
  attributes: AttributeSchema
125
137
  interleavedAttributes: InterleavedAttributeSchema
126
- buffers: BufferSchema
127
138
  }
128
139
  ```
129
140
 
130
141
  </details>
131
142
 
143
+ ### 🗑️ Disposal
144
+
145
+ Everything that creates a WebGL resource takes an `AbortSignal` and offers a
146
+ `dispose()`, and the two are the same teardown reached two ways:
147
+
148
+ ```typescript
149
+ const controller = new AbortController()
150
+
151
+ const { view, dispose } = compile(gl, vertex, fragment, { signal: controller.signal })
152
+ const buffers = bufferView(gl, { indices: {} }, { signal: controller.signal })
153
+ const target = createFramebuffer(gl, definition, { signal: controller.signal })
154
+
155
+ controller.abort() // deletes the program, the buffers, the vertex arrays, the framebuffer
156
+ ```
157
+
158
+ Two rules make this safe to reach for.
159
+
160
+ **Only what it made.** A resource handed in from outside is borrowed, never
161
+ deleted: a buffer named by an attribute schema, a texture passed to
162
+ `createFramebuffer`. Those belong to whoever created them, and the same buffer
163
+ often feeds several views. Delete one out from under its owner and WebGL does
164
+ not fault where it happened — the object survives while it is bound, so the
165
+ handle is invalidated and the *other* view fails later, on its next upload.
166
+
167
+ **Once.** Every `dispose()` is idempotent. Disposing by hand and then aborting,
168
+ or aborting twice, deletes each resource exactly once.
169
+
170
+ | creates | disposed by |
171
+ | --- | --- |
172
+ | `view()` | its own attribute, layout and schema buffers, and vertex arrays still held from `vao()` |
173
+ | `compile()` | the program, and its view |
174
+ | `attributeView` / `interleavedAttributeView` | the buffer, unless the schema named one |
175
+ | `bufferView` | its buffers |
176
+ | `vaoView` | the vertex array, never its participants |
177
+ | `createProgram` | the program |
178
+ | `createTexture` | the texture |
179
+ | `createFramebuffer` | the framebuffer, and the texture unless one was passed in |
180
+
181
+ `uniformView` takes no signal: a uniform location is not a resource, so there is
182
+ nothing to delete.
183
+
132
184
  ### 👀 Resource Views
133
185
 
134
186
  Each view type can be imported individually.
@@ -316,8 +368,10 @@ const buffers = bufferView(gl, {
316
368
  data: { target: 'ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
317
369
  })
318
370
 
319
- buffers.indices.set(indexData).bind()
320
- buffers.data.set(dynamicData).bind()
371
+ buffers.indices.set(indexData)
372
+ buffers.data.set(dynamicData)
373
+
374
+ const unbind = buffers.indices.bind()
321
375
 
322
376
  // bind() returns a disposer restoring the previous binding for that target.
323
377
  // Worth calling for ELEMENT_ARRAY_BUFFER, whose binding is recorded in the
@@ -628,19 +682,23 @@ You can provide an optional override schema to enhance or override the automatic
628
682
 
629
683
  ```typescript
630
684
  const { program, schema, view } = compile(gl, vertexShader, fragmentShader, {
631
- uniforms: {
632
- // Add additional uniforms not automatically inferred
633
- customTime: { kind: 'float' },
634
- },
635
- buffers: {
636
- // Add buffer definitions
637
- indices: { target: 'ELEMENT_ARRAY_BUFFER' },
685
+ schema: {
686
+ uniforms: {
687
+ // Add additional uniforms not automatically inferred
688
+ customTime: { kind: 'float' },
689
+ },
638
690
  },
639
691
  })
640
692
 
641
693
  // The override schema is merged with the extracted schema
642
694
  view.uniforms.customTime.set(123.45)
643
- view.buffers.indices.set(indexData).bind()
695
+
696
+ // An index buffer that outlives this program — shared between views, or cached
697
+ // per context — is its own thing. One that dies with the view goes in the
698
+ // schema instead, and is disposed with it.
699
+ const buffers = bufferView(gl, { indices: { target: 'ELEMENT_ARRAY_BUFFER' } })
700
+ buffers.indices.set(indexData)
701
+ const vao = view.vao([view.attributes.position, buffers.indices])
644
702
  ```
645
703
 
646
704
  **Returns**:
@@ -773,7 +831,7 @@ uniforms[u_time].set(performance.now())
773
831
  Creates and links a WebGL program from vertex and fragment shader sources.
774
832
 
775
833
  ```typescript
776
- const program = createProgram(gl, vertexShaderSource, fragmentShaderSource)
834
+ const program = createProgram(gl, vertexShaderSource, fragmentShaderSource, { signal })
777
835
  ```
778
836
 
779
837
  ### 🖼️ createTexture
@@ -795,6 +853,7 @@ const texture = createTexture(
795
853
  wrapT: 'CLAMP_TO_EDGE',
796
854
  },
797
855
  data,
856
+ { signal },
798
857
  )
799
858
  ```
800
859
 
@@ -805,18 +864,26 @@ Automatically validates WebGL2-only formats and provides fallbacks for WebGL1.
805
864
  Creates a framebuffer with attached texture for render-to-texture operations.
806
865
 
807
866
  ```typescript
808
- const { framebuffer, texture } = createFramebuffer(gl, {
809
- width: 512,
810
- height: 512,
811
- attachment: 'color',
812
- internalFormat: 'RGBA',
813
- format: 'RGBA',
814
- type: 'UNSIGNED_BYTE',
815
- })
867
+ const { framebuffer, texture, dispose } = createFramebuffer(
868
+ gl,
869
+ {
870
+ width: 512,
871
+ height: 512,
872
+ attachment: 'color',
873
+ internalFormat: 'RGBA',
874
+ format: 'RGBA',
875
+ type: 'UNSIGNED_BYTE',
876
+ },
877
+ { signal },
878
+ )
816
879
  ```
817
880
 
818
881
  Supports color, depth, stencil, and combined depth-stencil attachments with completeness validation.
819
882
 
883
+ Pass a `texture` to attach one you already have — ping-ponging a pair of
884
+ framebuffers over shared textures, say. `dispose()` then deletes the framebuffer
885
+ and leaves that texture alone, because it is not the framebuffer's to delete.
886
+
820
887
  ## 🔍 WebGL Type Compatibility
821
888
 
822
889
  ### 🎯 Uniform Types
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>view.gl</title>
6
+ <script>
7
+ // Redirect to index.html with the current path as a query parameter
8
+ const path = window.location.pathname.slice(1); // Remove leading slash
9
+ if (path) {
10
+ window.location.replace(window.location.origin + '/?redirect=' + path);
11
+ } else {
12
+ window.location.replace(window.location.origin);
13
+ }
14
+ </script>
15
+ </head>
16
+ <body>
17
+ Redirecting...
18
+ </body>
19
+ </html>
@@ -0,0 +1,292 @@
1
+ import { i as interleave, u as uniform, a as attribute, g as glsl, c as compile } from './tag-C0Vskpp8.js';
2
+ import { d as dom, c as cursor } from './utils-2dzuv_bW.js';
3
+
4
+ const canvas = dom("canvas", { parentElement: document.body });
5
+ canvas.width = window.innerWidth;
6
+ canvas.height = window.innerHeight;
7
+ const gl = canvas.getContext("webgl2", { antialias: true });
8
+ if (!gl) {
9
+ throw new Error("WebGL not supported");
10
+ }
11
+ const state = {
12
+ position: { x: 0, y: 0 },
13
+ // Pan offset in screen pixels
14
+ scale: 1,
15
+ // Zoom level
16
+ controlPoints: [
17
+ { x: 200, y: 400 },
18
+ // P0: Start point of the bezier curve
19
+ { x: 400, y: 100 },
20
+ // P1: Control point (influences curvature)
21
+ { x: 600, y: 400 }
22
+ // P2: End point of the bezier curve
23
+ ],
24
+ dragIndex: -1
25
+ // Index of control point being dragged (-1 = none)
26
+ };
27
+ function screenToWorld(screenX, screenY) {
28
+ return {
29
+ x: (screenX - state.position.x) / state.scale,
30
+ y: (screenY - state.position.y) / state.scale
31
+ };
32
+ }
33
+ function worldToScreen(worldX, worldY) {
34
+ return {
35
+ x: worldX * state.scale + state.position.x,
36
+ y: worldY * state.scale + state.position.y
37
+ };
38
+ }
39
+ function getControlPointAt(x, y, threshold = 20) {
40
+ for (let i = 0; i < state.controlPoints.length; i++) {
41
+ const screen = worldToScreen(state.controlPoints[i].x, state.controlPoints[i].y);
42
+ const dx = x - screen.x;
43
+ const dy = y - screen.y;
44
+ if (dx * dx + dy * dy < threshold * threshold) {
45
+ return i;
46
+ }
47
+ }
48
+ return -1;
49
+ }
50
+ canvas.addEventListener("pointerdown", (event) => {
51
+ const hitIndex = getControlPointAt(event.clientX, event.clientY);
52
+ if (hitIndex >= 0) {
53
+ state.dragIndex = hitIndex;
54
+ cursor(event, (event2) => {
55
+ const world = screenToWorld(event2.clientX, event2.clientY);
56
+ state.controlPoints[state.dragIndex] = world;
57
+ render();
58
+ });
59
+ } else {
60
+ cursor(event, (event2) => {
61
+ state.position.x -= event2.deltaX;
62
+ state.position.y -= event2.deltaY;
63
+ render();
64
+ });
65
+ }
66
+ });
67
+ canvas.addEventListener(
68
+ "wheel",
69
+ (event) => {
70
+ const rect = canvas.getBoundingClientRect();
71
+ const mouseX = event.clientX - rect.left;
72
+ const mouseY = event.clientY - rect.top;
73
+ const scaleFactor = event.deltaY > 0 ? 0.95 : 1.05;
74
+ const oldScale = state.scale;
75
+ const newScale = Math.max(0.1, Math.min(10, oldScale * scaleFactor));
76
+ const worldX = (mouseX - state.position.x) / oldScale;
77
+ const worldY = (mouseY - state.position.y) / oldScale;
78
+ state.scale = newScale;
79
+ state.position.x = mouseX - worldX * newScale;
80
+ state.position.y = mouseY - worldY * newScale;
81
+ render();
82
+ },
83
+ { passive: true }
84
+ );
85
+ window.addEventListener("resize", () => {
86
+ canvas.width = window.innerWidth;
87
+ canvas.height = window.innerHeight;
88
+ render();
89
+ });
90
+ const curveVertex = glsl`#version 300 es
91
+ // Attributes (per-vertex data)
92
+ ${interleave("data", [
93
+ attribute.float("a_t"),
94
+ attribute.float("a_side")
95
+ ])} // Bezier parameter t ∈ [0,1]
96
+
97
+ // Uniforms (shared data for all vertices)
98
+ ${uniform.vec2("u_resolution")} // Canvas size in pixels
99
+ ${uniform.vec2("u_offset")} // Pan offset in pixels
100
+ ${uniform.float("u_scale")} // Zoom level
101
+ ${uniform.vec2("u_p0")} // Control point P0 (start)
102
+ ${uniform.vec2("u_p1")} // Control point P1 (control)
103
+ ${uniform.vec2("u_p2")} // Control point P2 (end)
104
+ ${uniform.float("u_thickness")} // Curve thickness in world units
105
+
106
+ void main() {
107
+ // STEP 1: Evaluate the quadratic bezier curve at parameter t
108
+ // Quadratic bezier formula: B(t) = (1-t)²P0 + 2t(1-t)P1 + t²P2
109
+ float t = a_t;
110
+ float s = 1.0 - t; // s is often used for (1-t) for brevity
111
+
112
+ // Calculate the position on the curve
113
+ vec2 position = s * s * u_p0 + // (1-t)² * P0
114
+ 2.0 * s * t * u_p1 + // 2(1-t)t * P1
115
+ t * t * u_p2; // t² * P2
116
+
117
+ // STEP 2: Calculate the tangent vector at this point
118
+ // The derivative of the bezier curve gives us the tangent
119
+ // B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1)
120
+ vec2 tangent = 2.0 * s * (u_p1 - u_p0) + 2.0 * t * (u_p2 - u_p1);
121
+
122
+ // STEP 3: Create a normal vector (perpendicular to tangent)
123
+ // Rotate tangent 90 degrees: (x,y) → (-y,x)
124
+ vec2 normal = normalize(vec2(-tangent.y, tangent.x));
125
+
126
+ // STEP 4: Offset the curve position to create thickness
127
+ // Move the vertex perpendicular to the curve by thickness * side
128
+ vec2 offsetPosition = position + normal * u_thickness * a_side;
129
+
130
+ // STEP 5: Transform from world space to screen space
131
+ vec2 screenPos = offsetPosition * u_scale + u_offset;
132
+
133
+ // STEP 6: Convert to clip space (OpenGL's coordinate system)
134
+ // Screen space: (0,0) is top-left, (width,height) is bottom-right
135
+ // Clip space: (-1,-1) is bottom-left, (1,1) is top-right
136
+ vec2 clipPos = (screenPos / u_resolution) * 2.0 - 1.0;
137
+ clipPos.y = -clipPos.y; // Flip Y to match screen coordinates
138
+
139
+ gl_Position = vec4(clipPos, 0, 1);
140
+ }`;
141
+ const curveFragment = glsl`#version 300 es
142
+ precision mediump float;
143
+ out vec4 fragColor;
144
+
145
+ void main() {
146
+ // Output a solid blue color for the curve
147
+ vec3 curveColor = vec3(0.2, 0.6, 1.0);
148
+ fragColor = vec4(curveColor, 1.0);
149
+ }`;
150
+ const pointVertex = glsl`#version 300 es
151
+ ${attribute.vec2("a_vertex")} // Quad vertex position (-1 to 1)
152
+ ${uniform.vec2("u_resolution")} // Canvas size
153
+ ${uniform.vec2("u_offset")} // Pan offset
154
+ ${uniform.float("u_scale")} // Zoom level
155
+ ${uniform.vec2("u_pointPos")} // Control point position in world space
156
+
157
+ void main() {
158
+ // Create a small square around the control point position
159
+ vec2 screenPos = (u_pointPos * u_scale + u_offset) + a_vertex * 8.0;
160
+
161
+ // Transform to clip space
162
+ vec2 clipPos = (screenPos / u_resolution) * 2.0 - 1.0;
163
+ clipPos.y = -clipPos.y;
164
+ gl_Position = vec4(clipPos, 0, 1);
165
+ }`;
166
+ const pointFragment = glsl`#version 300 es
167
+ precision mediump float;
168
+ out vec4 fragColor;
169
+
170
+ void main() {
171
+ // Red color for control points
172
+ fragColor = vec4(1.0, 0.3, 0.3, 1.0);
173
+ }`;
174
+ const lineVertex = glsl`#version 300 es
175
+ ${attribute.vec2("a_position")} // Line vertex position
176
+ ${uniform.vec2("u_resolution")} // Canvas size
177
+ ${uniform.vec2("u_offset")} // Pan offset
178
+ ${uniform.float("u_scale")} // Zoom level
179
+
180
+ void main() {
181
+ // Transform world position to screen space then clip space
182
+ vec2 screenPos = a_position * u_scale + u_offset;
183
+ vec2 clipPos = (screenPos / u_resolution) * 2.0 - 1.0;
184
+ clipPos.y = -clipPos.y;
185
+ gl_Position = vec4(clipPos, 0, 1);
186
+ }`;
187
+ const lineFragment = glsl`#version 300 es
188
+ precision mediump float;
189
+ out vec4 fragColor;
190
+
191
+ void main() {
192
+ // Semi-transparent gray for control polygon
193
+ fragColor = vec4(0.5, 0.5, 0.5, 0.5);
194
+ }`;
195
+ const { program: curveProgram, view: curveView } = compile(gl, curveVertex, curveFragment);
196
+ const { program: pointProgram, view: pointView } = compile(gl, pointVertex, pointFragment);
197
+ const { program: lineProgram, view: lineView } = compile(gl, lineVertex, lineFragment);
198
+ const curveSegments = 100;
199
+ function generateTriangleStrip() {
200
+ return new Float32Array(
201
+ function* () {
202
+ for (let i = 0; i <= curveSegments; i++) {
203
+ const t = i / curveSegments;
204
+ yield t;
205
+ yield -1;
206
+ yield t;
207
+ yield 1;
208
+ }
209
+ }()
210
+ );
211
+ }
212
+ const stripData = generateTriangleStrip();
213
+ curveView.interleavedAttributes.data.set(stripData);
214
+ const pointVertices = new Float32Array([
215
+ -1,
216
+ -1,
217
+ // Bottom-left
218
+ 1,
219
+ -1,
220
+ // Bottom-right
221
+ -1,
222
+ 1,
223
+ // Top-left
224
+ -1,
225
+ 1,
226
+ // Top-left (repeated for second triangle)
227
+ 1,
228
+ -1,
229
+ // Bottom-right (repeated for second triangle)
230
+ 1,
231
+ 1
232
+ // Top-right
233
+ ]);
234
+ pointView.attributes.a_vertex.set(pointVertices);
235
+ function updateLineData() {
236
+ const lineVerticesData = new Float32Array([
237
+ state.controlPoints[0].x,
238
+ state.controlPoints[0].y,
239
+ // P0
240
+ state.controlPoints[1].x,
241
+ state.controlPoints[1].y,
242
+ // P1
243
+ state.controlPoints[1].x,
244
+ state.controlPoints[1].y,
245
+ // P1 (repeated)
246
+ state.controlPoints[2].x,
247
+ state.controlPoints[2].y
248
+ // P2
249
+ ]);
250
+ lineView.attributes.a_position.set(lineVerticesData);
251
+ }
252
+ updateLineData();
253
+ function render() {
254
+ console.log("render!");
255
+ if (canvas.width !== window.innerWidth || canvas.height !== window.innerHeight) {
256
+ canvas.width = window.innerWidth;
257
+ canvas.height = window.innerHeight;
258
+ }
259
+ gl.viewport(0, 0, canvas.width, canvas.height);
260
+ gl.clearColor(0.1, 0.1, 0.15, 1);
261
+ gl.clear(gl.COLOR_BUFFER_BIT);
262
+ gl.enable(gl.BLEND);
263
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
264
+ updateLineData();
265
+ gl.useProgram(lineProgram);
266
+ lineView.attributes.a_position.bind();
267
+ lineView.uniforms.u_resolution.set(canvas.width, canvas.height);
268
+ lineView.uniforms.u_offset.set(state.position.x, state.position.y);
269
+ lineView.uniforms.u_scale.set(state.scale);
270
+ gl.drawArrays(gl.LINES, 0, 4);
271
+ gl.useProgram(curveProgram);
272
+ curveView.interleavedAttributes.data.bind();
273
+ curveView.uniforms.u_resolution.set(canvas.width, canvas.height);
274
+ curveView.uniforms.u_offset.set(state.position.x, state.position.y);
275
+ curveView.uniforms.u_scale.set(state.scale);
276
+ curveView.uniforms.u_p0.set(state.controlPoints[0].x, state.controlPoints[0].y);
277
+ curveView.uniforms.u_p1.set(state.controlPoints[1].x, state.controlPoints[1].y);
278
+ curveView.uniforms.u_p2.set(state.controlPoints[2].x, state.controlPoints[2].y);
279
+ curveView.uniforms.u_thickness.set(5);
280
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, (curveSegments + 1) * 2);
281
+ curveView.interleavedAttributes.data.unbind();
282
+ gl.useProgram(pointProgram);
283
+ pointView.attributes.a_vertex.bind();
284
+ pointView.uniforms.u_resolution.set(canvas.width, canvas.height);
285
+ pointView.uniforms.u_offset.set(state.position.x, state.position.y);
286
+ pointView.uniforms.u_scale.set(state.scale);
287
+ for (const point of state.controlPoints) {
288
+ pointView.uniforms.u_pointPos.set(point.x, point.y);
289
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
290
+ }
291
+ }
292
+ render();
@@ -0,0 +1,186 @@
1
+ import { b as createFramebuffer, u as uniform, g as glsl, c as compile } from './tag-C0Vskpp8.js';
2
+ import { d as dom } from './utils-2dzuv_bW.js';
3
+
4
+ let playing = false;
5
+ const canvas = dom("canvas");
6
+ const gl = canvas.getContext("webgl", { antialias: false });
7
+ if (!gl) {
8
+ throw new Error("WebGL not supported");
9
+ }
10
+ document.body.append(canvas);
11
+ function spray() {
12
+ return Math.floor(Math.random() * 8) * (Math.random() < 0.5 ? 1 : -1);
13
+ }
14
+ canvas.addEventListener("mousedown", (event) => {
15
+ const rect = canvas.getBoundingClientRect();
16
+ const controller = new AbortController();
17
+ playing = false;
18
+ window.addEventListener(
19
+ "mousemove",
20
+ (event2) => {
21
+ const x = Math.floor((event2.clientX - rect.left - spray()) * (canvas.width / rect.width));
22
+ const y = Math.floor((rect.bottom - event2.clientY - spray()) * (canvas.height / rect.height));
23
+ const dimensions = [2, 2];
24
+ const data = new Uint8Array(
25
+ function* () {
26
+ for (let i = 0; i < dimensions[0] * dimensions[1]; i++) {
27
+ yield 255;
28
+ yield 0;
29
+ yield 0;
30
+ yield 255;
31
+ }
32
+ }()
33
+ );
34
+ gl.bindTexture(gl.TEXTURE_2D, read.texture);
35
+ gl.texSubImage2D(
36
+ gl.TEXTURE_2D,
37
+ 0,
38
+ x,
39
+ y,
40
+ dimensions[0],
41
+ dimensions[1],
42
+ gl.RGBA,
43
+ gl.UNSIGNED_BYTE,
44
+ data
45
+ );
46
+ gl.bindTexture(gl.TEXTURE_2D, write.texture);
47
+ gl.texSubImage2D(
48
+ gl.TEXTURE_2D,
49
+ 0,
50
+ x,
51
+ y,
52
+ dimensions[0],
53
+ dimensions[1],
54
+ gl.RGBA,
55
+ gl.UNSIGNED_BYTE,
56
+ data
57
+ );
58
+ render();
59
+ },
60
+ controller
61
+ );
62
+ window.addEventListener(
63
+ "mouseup",
64
+ () => {
65
+ controller.abort();
66
+ playing = true;
67
+ animate();
68
+ },
69
+ controller
70
+ );
71
+ });
72
+ canvas.width = 256 * 2;
73
+ canvas.height = 256 * 2;
74
+ const WIDTH = canvas.width;
75
+ const HEIGHT = canvas.height;
76
+ const stepFragment = glsl`
77
+ precision mediump float;
78
+ varying vec2 v_uv;
79
+ ${uniform.sampler2D("u_texture")}
80
+ ${uniform.vec2("u_texelSize")}
81
+
82
+ int getCell(vec2 uv) {
83
+ // Wrap around (toroidal)
84
+ uv = mod(uv, 1.0);
85
+ float val = texture2D(u_texture, uv).r;
86
+ return int(step(0.5, val));
87
+ }
88
+
89
+ void main() {
90
+ vec2 uv = v_uv * 0.5 + 0.5;
91
+
92
+ int sum = 0;
93
+ // Iterate neighbors -1,0,1
94
+ for (int y = -1; y <= 1; y++) {
95
+ for (int x = -1; x <= 1; x++) {
96
+ if (x == 0 && y == 0) continue;
97
+ sum += getCell(uv + vec2(float(x), float(y)) * u_texelSize);
98
+ }
99
+ }
100
+ int current = getCell(uv);
101
+
102
+ int nextState = 0;
103
+ if (current == 1) {
104
+ if (sum == 2 || sum == 3) nextState = 1;
105
+ else nextState = 0;
106
+ } else {
107
+ if (sum == 3) nextState = 1;
108
+ else nextState = 0;
109
+ }
110
+
111
+ gl_FragColor = vec4(float(nextState), 0.0, 0.0, 1.0);
112
+ }`;
113
+ const renderFragment = glsl`
114
+ precision mediump float;
115
+ varying vec2 v_uv;
116
+ ${uniform.sampler2D("u_texture")}
117
+
118
+ void main() {
119
+ vec2 uv = v_uv * 0.5 + 0.5;
120
+ float cell = texture2D(u_texture, uv).r;
121
+ gl_FragColor = vec4(vec3(cell), 1.0);
122
+ }`;
123
+ const { program: stepProgram, view: stepView } = compile.toQuad(gl, stepFragment);
124
+ const { program: renderProgram, view: renderView } = compile.toQuad(gl, renderFragment);
125
+ const framebufferOptions = {
126
+ attachment: "color",
127
+ width: WIDTH,
128
+ height: HEIGHT,
129
+ type: "UNSIGNED_BYTE",
130
+ magFilter: "NEAREST",
131
+ minFilter: "NEAREST"
132
+ };
133
+ let read = createFramebuffer(gl, framebufferOptions);
134
+ let write = createFramebuffer(gl, framebufferOptions);
135
+ gl.bindTexture(gl.TEXTURE_2D, read.texture);
136
+ gl.texSubImage2D(
137
+ gl.TEXTURE_2D,
138
+ 0,
139
+ 0,
140
+ 0,
141
+ WIDTH,
142
+ HEIGHT,
143
+ gl.RGBA,
144
+ gl.UNSIGNED_BYTE,
145
+ new Uint8Array(
146
+ function* () {
147
+ for (let i = 0; i < WIDTH * HEIGHT; i++) {
148
+ yield 0;
149
+ yield 0;
150
+ yield 0;
151
+ yield 255;
152
+ }
153
+ }()
154
+ )
155
+ );
156
+ function step() {
157
+ gl.bindFramebuffer(gl.FRAMEBUFFER, write.framebuffer);
158
+ gl.viewport(0, 0, WIDTH, HEIGHT);
159
+ gl.useProgram(stepProgram);
160
+ gl.activeTexture(gl.TEXTURE0);
161
+ gl.bindTexture(gl.TEXTURE_2D, read.texture);
162
+ stepView.uniforms.u_texture.set(0);
163
+ stepView.uniforms.u_texelSize.set(1 / WIDTH, 1 / HEIGHT);
164
+ stepView.attributes.a_quad.bind();
165
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
166
+ }
167
+ function render() {
168
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
169
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
170
+ gl.useProgram(renderProgram);
171
+ gl.activeTexture(gl.TEXTURE0);
172
+ gl.bindTexture(gl.TEXTURE_2D, write.texture);
173
+ renderView.uniforms.u_texture.set(0);
174
+ renderView.attributes.a_quad.bind();
175
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
176
+ }
177
+ function animate() {
178
+ if (playing) {
179
+ step();
180
+ render();
181
+ [read, write] = [write, read];
182
+ requestAnimationFrame(animate);
183
+ }
184
+ }
185
+ playing = true;
186
+ animate();