@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.
- package/README.md +94 -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-H56LJYS5.mjs} +63 -133
- package/dist/chunk-H56LJYS5.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 +19 -6
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-CyGyGi18.d.mts → types-k1Xz1iO3.d.mts} +44 -2
- package/package.json +1 -1
- package/src/gl.ts +13 -2
- package/src/index.ts +63 -39
- package/src/tag.ts +23 -2
- package/src/types.ts +43 -1
- package/src/utils.ts +45 -10
- package/test/index.test.ts +145 -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,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
|
-
- `
|
|
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) -
|
|
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)
|
|
320
|
-
buffers.data.set(dynamicData)
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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(
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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();
|