@bigmistqke/view.gl 0.2.0 → 0.3.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/README.md +82 -27
- package/dev/dist/404.html +19 -0
- package/dev/dist/assets/bezier-DrA4cU_H.js +292 -0
- package/dev/dist/assets/game-of-life-BYlzWhEL.js +186 -0
- package/dev/dist/assets/glsl-composition-D_BaFFij.js +97 -0
- package/dev/dist/assets/grid-DfXHNLBm.js +181 -0
- package/dev/dist/assets/index-C2N3u8wj.css +12 -0
- package/dev/dist/assets/index-D-N0FDYs.js +110 -0
- package/dev/dist/assets/main-CSE2fki3.js +32 -0
- package/dev/dist/assets/overview-IeUhUflE.js +80 -0
- package/dev/dist/assets/pix-sim-DMJJA_WO.js +500 -0
- package/dev/dist/assets/ray-casting-DTQetDPd.js +411 -0
- package/dev/dist/assets/ray-casting-modules-Dkfw4tdc.js +765 -0
- package/dev/dist/assets/sdf-CS0uBonx.js +66 -0
- package/dev/dist/assets/symbol-BL_7GJ0M.js +67 -0
- package/dev/dist/assets/tag-C0Vskpp8.js +833 -0
- package/dev/dist/assets/tetris-D8mokodr.js +344 -0
- package/dev/dist/assets/utils-2dzuv_bW.js +62 -0
- package/dev/dist/index.html +51 -0
- package/dist/chunk-BZDGEIOT.mjs +278 -0
- package/dist/chunk-BZDGEIOT.mjs.map +1 -0
- package/dist/{chunk-EYXOSM7B.mjs → chunk-X4M3LV4R.mjs} +60 -133
- package/dist/chunk-X4M3LV4R.mjs.map +1 -0
- package/dist/gl.d.mts +2 -2
- package/dist/gl.mjs +1 -1
- package/dist/index.d.mts +13 -4
- package/dist/index.mjs +7 -2
- package/dist/tag.d.mts +6 -1
- package/dist/tag.mjs +13 -5
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-CyGyGi18.d.mts → types-CNdqh7tB.d.mts} +34 -4
- package/package.json +1 -1
- package/src/gl.ts +13 -2
- package/src/index.ts +55 -39
- package/src/tag.ts +18 -2
- package/src/types.ts +33 -3
- package/src/utils.ts +45 -10
- package/test/index.test.ts +111 -29
- package/test/mocks/webgl.ts +14 -0
- package/test/tag.test.ts +41 -0
- package/dist/chunk-CJTT7WPV.mjs +0 -75
- package/dist/chunk-CJTT7WPV.mjs.map +0 -1
- 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
|
|
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
|
|
93
|
+
const { uniforms, attributes } = view(gl, program, schema)
|
|
97
94
|
```
|
|
98
95
|
|
|
99
96
|
**Parameters:**
|
|
@@ -106,14 +103,18 @@ 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
|
-
- `
|
|
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
|
-
|
|
113
|
+
|
|
114
|
+
A schema names what the program has names for, so buffers are not part of it: a
|
|
115
|
+
buffer has no name in the source to resolve against the program, and its lifetime
|
|
116
|
+
is rarely a program's — the same buffer often feeds several. Build one with
|
|
117
|
+
[`bufferView`](#-bufferview) and hand it to `vao()` alongside the attributes.
|
|
117
118
|
|
|
118
119
|
<details>
|
|
119
120
|
<summary>TypeScript Types</summary>
|
|
@@ -123,12 +124,52 @@ interface ViewSchema {
|
|
|
123
124
|
uniforms: UniformSchema
|
|
124
125
|
attributes: AttributeSchema
|
|
125
126
|
interleavedAttributes: InterleavedAttributeSchema
|
|
126
|
-
buffers: BufferSchema
|
|
127
127
|
}
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
</details>
|
|
131
131
|
|
|
132
|
+
### 🗑️ Disposal
|
|
133
|
+
|
|
134
|
+
Everything that creates a WebGL resource takes an `AbortSignal` and offers a
|
|
135
|
+
`dispose()`, and the two are the same teardown reached two ways:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const controller = new AbortController()
|
|
139
|
+
|
|
140
|
+
const { view, dispose } = compile(gl, vertex, fragment, { signal: controller.signal })
|
|
141
|
+
const buffers = bufferView(gl, { indices: {} }, { signal: controller.signal })
|
|
142
|
+
const target = createFramebuffer(gl, definition, { signal: controller.signal })
|
|
143
|
+
|
|
144
|
+
controller.abort() // deletes the program, the buffers, the vertex arrays, the framebuffer
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Two rules make this safe to reach for.
|
|
148
|
+
|
|
149
|
+
**Only what it made.** A resource handed in from outside is borrowed, never
|
|
150
|
+
deleted: a buffer named by an attribute schema, a texture passed to
|
|
151
|
+
`createFramebuffer`. Those belong to whoever created them, and the same buffer
|
|
152
|
+
often feeds several views. Delete one out from under its owner and WebGL does
|
|
153
|
+
not fault where it happened — the object survives while it is bound, so the
|
|
154
|
+
handle is invalidated and the *other* view fails later, on its next upload.
|
|
155
|
+
|
|
156
|
+
**Once.** Every `dispose()` is idempotent. Disposing by hand and then aborting,
|
|
157
|
+
or aborting twice, deletes each resource exactly once.
|
|
158
|
+
|
|
159
|
+
| creates | disposed by |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| `view()` | its own attribute and layout buffers, and vertex arrays still held from `vao()` |
|
|
162
|
+
| `compile()` | the program, and its view |
|
|
163
|
+
| `attributeView` / `interleavedAttributeView` | the buffer, unless the schema named one |
|
|
164
|
+
| `bufferView` | its buffers |
|
|
165
|
+
| `vaoView` | the vertex array, never its participants |
|
|
166
|
+
| `createProgram` | the program |
|
|
167
|
+
| `createTexture` | the texture |
|
|
168
|
+
| `createFramebuffer` | the framebuffer, and the texture unless one was passed in |
|
|
169
|
+
|
|
170
|
+
`uniformView` takes no signal: a uniform location is not a resource, so there is
|
|
171
|
+
nothing to delete.
|
|
172
|
+
|
|
132
173
|
### 👀 Resource Views
|
|
133
174
|
|
|
134
175
|
Each view type can be imported individually.
|
|
@@ -316,8 +357,10 @@ const buffers = bufferView(gl, {
|
|
|
316
357
|
data: { target: 'ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
|
|
317
358
|
})
|
|
318
359
|
|
|
319
|
-
buffers.indices.set(indexData)
|
|
320
|
-
buffers.data.set(dynamicData)
|
|
360
|
+
buffers.indices.set(indexData)
|
|
361
|
+
buffers.data.set(dynamicData)
|
|
362
|
+
|
|
363
|
+
const unbind = buffers.indices.bind()
|
|
321
364
|
|
|
322
365
|
// bind() returns a disposer restoring the previous binding for that target.
|
|
323
366
|
// Worth calling for ELEMENT_ARRAY_BUFFER, whose binding is recorded in the
|
|
@@ -628,19 +671,22 @@ You can provide an optional override schema to enhance or override the automatic
|
|
|
628
671
|
|
|
629
672
|
```typescript
|
|
630
673
|
const { program, schema, view } = compile(gl, vertexShader, fragmentShader, {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
// Add buffer definitions
|
|
637
|
-
indices: { target: 'ELEMENT_ARRAY_BUFFER' },
|
|
674
|
+
schema: {
|
|
675
|
+
uniforms: {
|
|
676
|
+
// Add additional uniforms not automatically inferred
|
|
677
|
+
customTime: { kind: 'float' },
|
|
678
|
+
},
|
|
638
679
|
},
|
|
639
680
|
})
|
|
640
681
|
|
|
641
682
|
// The override schema is merged with the extracted schema
|
|
642
683
|
view.uniforms.customTime.set(123.45)
|
|
643
|
-
|
|
684
|
+
|
|
685
|
+
// An index buffer is its own thing — no program names it, and it outlives any
|
|
686
|
+
// one of them. Build it with bufferView and pass it into the vertex array.
|
|
687
|
+
const buffers = bufferView(gl, { indices: { target: 'ELEMENT_ARRAY_BUFFER' } })
|
|
688
|
+
buffers.indices.set(indexData)
|
|
689
|
+
const vao = view.vao([view.attributes.position, buffers.indices])
|
|
644
690
|
```
|
|
645
691
|
|
|
646
692
|
**Returns**:
|
|
@@ -773,7 +819,7 @@ uniforms[u_time].set(performance.now())
|
|
|
773
819
|
Creates and links a WebGL program from vertex and fragment shader sources.
|
|
774
820
|
|
|
775
821
|
```typescript
|
|
776
|
-
const program = createProgram(gl, vertexShaderSource, fragmentShaderSource)
|
|
822
|
+
const program = createProgram(gl, vertexShaderSource, fragmentShaderSource, { signal })
|
|
777
823
|
```
|
|
778
824
|
|
|
779
825
|
### 🖼️ createTexture
|
|
@@ -795,6 +841,7 @@ const texture = createTexture(
|
|
|
795
841
|
wrapT: 'CLAMP_TO_EDGE',
|
|
796
842
|
},
|
|
797
843
|
data,
|
|
844
|
+
{ signal },
|
|
798
845
|
)
|
|
799
846
|
```
|
|
800
847
|
|
|
@@ -805,18 +852,26 @@ Automatically validates WebGL2-only formats and provides fallbacks for WebGL1.
|
|
|
805
852
|
Creates a framebuffer with attached texture for render-to-texture operations.
|
|
806
853
|
|
|
807
854
|
```typescript
|
|
808
|
-
const { framebuffer, texture } = createFramebuffer(
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
855
|
+
const { framebuffer, texture, dispose } = createFramebuffer(
|
|
856
|
+
gl,
|
|
857
|
+
{
|
|
858
|
+
width: 512,
|
|
859
|
+
height: 512,
|
|
860
|
+
attachment: 'color',
|
|
861
|
+
internalFormat: 'RGBA',
|
|
862
|
+
format: 'RGBA',
|
|
863
|
+
type: 'UNSIGNED_BYTE',
|
|
864
|
+
},
|
|
865
|
+
{ signal },
|
|
866
|
+
)
|
|
816
867
|
```
|
|
817
868
|
|
|
818
869
|
Supports color, depth, stencil, and combined depth-stencil attachments with completeness validation.
|
|
819
870
|
|
|
871
|
+
Pass a `texture` to attach one you already have — ping-ponging a pair of
|
|
872
|
+
framebuffers over shared textures, say. `dispose()` then deletes the framebuffer
|
|
873
|
+
and leaves that texture alone, because it is not the framebuffer's to delete.
|
|
874
|
+
|
|
820
875
|
## 🔍 WebGL Type Compatibility
|
|
821
876
|
|
|
822
877
|
### 🎯 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();
|