@bigmistqke/view.gl 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/README.md +83 -42
- package/dev/src/examples/bezier.ts +3 -2
- package/dev/src/examples/game-of-life.ts +13 -30
- package/dev/src/examples/glsl-composition.ts +3 -3
- package/dev/src/examples/grid.ts +20 -14
- package/dev/src/examples/pix-sim.ts +29 -22
- package/dev/src/examples/ray-casting.ts +455 -0
- package/dev/src/examples/sdf.ts +98 -0
- package/dev/src/examples/symbol.ts +2 -2
- package/dev/src/index.ts +12 -11
- package/dev/src/utils.ts +18 -4
- package/dist/index.d.mts +2 -2
- package/dist/tag.d.mts +86 -69
- package/dist/tag.mjs +78 -37
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-C4TRuvYh.d.mts → types-DaV9McmR.d.mts} +40 -33
- package/package.json +1 -1
- package/src/tag.ts +128 -59
- package/src/types.ts +76 -49
- package/test/tag.test.ts +697 -6
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { compile, glsl, uniform } from '../../../src/tag.ts'
|
|
2
|
+
import { dom } from '../utils.ts'
|
|
3
|
+
|
|
4
|
+
// Sphere data: [x, y, z, radius]
|
|
5
|
+
const sphereData = [
|
|
6
|
+
[0, 0, 0, 1.0], // Center sphere
|
|
7
|
+
[3, 1, -2, 0.8], // Right sphere
|
|
8
|
+
[-3, -1, 1, 0.9], // Left sphere
|
|
9
|
+
[1, 3, -1, 0.7], // Top sphere
|
|
10
|
+
[-2, -3, 2, 0.6], // Bottom sphere
|
|
11
|
+
[4, -2, -3, 0.5], // Far right sphere
|
|
12
|
+
[-4, 2, 3, 0.8], // Far left sphere
|
|
13
|
+
[0, -1, -4, 1.2], // Back sphere
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
// Sphere colors (RGB)
|
|
17
|
+
const sphereColors = [
|
|
18
|
+
[1.0, 0.3, 0.3], // Red
|
|
19
|
+
[0.3, 1.0, 0.3], // Green
|
|
20
|
+
[0.3, 0.3, 1.0], // Blue
|
|
21
|
+
[1.0, 1.0, 0.3], // Yellow
|
|
22
|
+
[1.0, 0.3, 1.0], // Magenta
|
|
23
|
+
[0.3, 1.0, 1.0], // Cyan
|
|
24
|
+
[1.0, 0.7, 0.3], // Orange
|
|
25
|
+
[0.7, 0.3, 1.0], // Purple
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
// Box data: [x, y, z, size] (size is half-extents for all dimensions)
|
|
29
|
+
const boxData = [
|
|
30
|
+
[2, 0, -1, 0.8], // Right box
|
|
31
|
+
[-2, 0, 1, 0.6], // Left box
|
|
32
|
+
[0, -2, -2, 0.7], // Bottom box
|
|
33
|
+
] as const
|
|
34
|
+
|
|
35
|
+
// Box colors (RGB)
|
|
36
|
+
const boxColors = [
|
|
37
|
+
[0.8, 0.8, 0.2], // Yellow-green
|
|
38
|
+
[0.2, 0.8, 0.8], // Cyan
|
|
39
|
+
[0.8, 0.2, 0.8], // Magenta
|
|
40
|
+
] as const
|
|
41
|
+
|
|
42
|
+
let SHAPE_COUNTER = 0
|
|
43
|
+
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
const canvas = dom('canvas', { parentElement: document.body })
|
|
46
|
+
|
|
47
|
+
const gl = canvas.getContext('webgl2')!
|
|
48
|
+
|
|
49
|
+
const NUM_SPHERES = 8
|
|
50
|
+
const NUM_BOXES = 3
|
|
51
|
+
|
|
52
|
+
const SHAPE_SPHERE = Symbol('SHAPE_SPHERE')
|
|
53
|
+
|
|
54
|
+
const sphereModule = glsl`
|
|
55
|
+
#define NUM_SPHERES ${NUM_SPHERES}
|
|
56
|
+
#define ${SHAPE_SPHERE} ${SHAPE_COUNTER++}
|
|
57
|
+
|
|
58
|
+
struct Sphere {
|
|
59
|
+
vec3 position;
|
|
60
|
+
float radius;
|
|
61
|
+
vec3 color;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
${uniform.vec4('sphereData', { size: NUM_SPHERES })}
|
|
65
|
+
${uniform.vec3('sphereColors', { size: NUM_SPHERES })}
|
|
66
|
+
|
|
67
|
+
float sphereIntersection(in vec3 rayOrigin, in vec3 rayDirection, in Sphere sphere)
|
|
68
|
+
{
|
|
69
|
+
// We're shooting a ray from the camera and checking if it hits a sphere
|
|
70
|
+
// Think of it like shining a laser pointer at a ball
|
|
71
|
+
|
|
72
|
+
vec3 rayToSphere = rayOrigin - sphere.position;
|
|
73
|
+
|
|
74
|
+
// Step 1: Find the closest point on the ray to the sphere's position
|
|
75
|
+
// This is like finding where the laser beam passes nearest to the ball's position
|
|
76
|
+
float closestApproach = dot(rayToSphere, rayDirection);
|
|
77
|
+
|
|
78
|
+
// Step 2: Check if we're already inside the sphere
|
|
79
|
+
// If negative, we're outside and pointing away from sphere position
|
|
80
|
+
float rayStartDistanceSquared = dot(rayToSphere, rayToSphere) - sphere.radius * sphere.radius;
|
|
81
|
+
|
|
82
|
+
// Step 3: Use geometry to check if the ray can reach the sphere
|
|
83
|
+
// If this value is negative, the ray misses the sphere entirely
|
|
84
|
+
float canReachSphere = closestApproach * closestApproach - rayStartDistanceSquared;
|
|
85
|
+
|
|
86
|
+
if(canReachSphere < 0.0) {
|
|
87
|
+
return -1.0; // Ray misses the sphere
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 4: Calculate how far along the ray we hit the sphere
|
|
91
|
+
// We want the nearest intersection point (there can be two when ray goes through)
|
|
92
|
+
float hitDistance = -closestApproach - sqrt(canReachSphere);
|
|
93
|
+
|
|
94
|
+
return hitDistance;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate normal vector at hit point on sphere surface
|
|
98
|
+
vec3 sphereNormal(in vec3 hitPosition, in Sphere sphere)
|
|
99
|
+
{
|
|
100
|
+
return (hitPosition - sphere.position) / sphere.radius;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find closest sphere intersection, returns distance and sets hitIndex and hitType
|
|
104
|
+
float findClosestSphere(in vec3 rayOrigin, in vec3 rayDirection, in float maxDistance, out int hitIndex, out int hitType)
|
|
105
|
+
{
|
|
106
|
+
float closestDistance = maxDistance;
|
|
107
|
+
hitIndex = -1;
|
|
108
|
+
hitType = -1;
|
|
109
|
+
|
|
110
|
+
for(int i = 0; i < NUM_SPHERES; i++) {
|
|
111
|
+
Sphere sphere = Sphere(sphereData[i].xyz, sphereData[i].w, sphereColors[i]);
|
|
112
|
+
float distance = sphereIntersection(rayOrigin, rayDirection, sphere);
|
|
113
|
+
|
|
114
|
+
if(distance > 0.0 && distance < closestDistance) {
|
|
115
|
+
closestDistance = distance;
|
|
116
|
+
hitIndex = i;
|
|
117
|
+
hitType = ${SHAPE_SPHERE};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return hitIndex >= 0 ? closestDistance : -1.0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Calculate sphere normal and color at hit point
|
|
125
|
+
void calculateSphereHit(in vec3 hitPosition, in int hitIndex, out vec3 normal, out vec3 color)
|
|
126
|
+
{
|
|
127
|
+
Sphere hitSphere = Sphere(sphereData[hitIndex].xyz, sphereData[hitIndex].w, sphereColors[hitIndex]);
|
|
128
|
+
normal = sphereNormal(hitPosition, hitSphere);
|
|
129
|
+
color = sphereColors[hitIndex];
|
|
130
|
+
}`
|
|
131
|
+
|
|
132
|
+
const SHAPE_BOX = Symbol('SHAPE_BOX')
|
|
133
|
+
|
|
134
|
+
const boxModule = glsl`
|
|
135
|
+
#define ${SHAPE_BOX} ${SHAPE_COUNTER++}
|
|
136
|
+
#define NUM_BOXES ${NUM_BOXES}
|
|
137
|
+
|
|
138
|
+
struct Box {
|
|
139
|
+
vec3 position;
|
|
140
|
+
float size;
|
|
141
|
+
vec3 color;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
${uniform.vec4('boxData', { size: NUM_BOXES })}
|
|
145
|
+
${uniform.vec3('boxColors', { size: NUM_BOXES })}
|
|
146
|
+
|
|
147
|
+
float boxIntersection(in vec3 rayOrigin, in vec3 rayDirection, in Box box)
|
|
148
|
+
{
|
|
149
|
+
// Ray-box intersection using slab method
|
|
150
|
+
// A box is defined by 6 planes, we find where the ray enters and exits each pair of parallel planes
|
|
151
|
+
|
|
152
|
+
vec3 boxMin = box.position - vec3(box.size);
|
|
153
|
+
vec3 boxMax = box.position + vec3(box.size);
|
|
154
|
+
|
|
155
|
+
// Calculate intersection distances for each axis
|
|
156
|
+
vec3 invDir = 1.0 / rayDirection;
|
|
157
|
+
vec3 t1 = (boxMin - rayOrigin) * invDir;
|
|
158
|
+
vec3 t2 = (boxMax - rayOrigin) * invDir;
|
|
159
|
+
|
|
160
|
+
// Find the min and max for each axis (handles negative direction)
|
|
161
|
+
vec3 tMin = min(t1, t2);
|
|
162
|
+
vec3 tMax = max(t1, t2);
|
|
163
|
+
|
|
164
|
+
// The ray enters the box at the maximum of all tMin values
|
|
165
|
+
float tNear = max(max(tMin.x, tMin.y), tMin.z);
|
|
166
|
+
|
|
167
|
+
// The ray exits the box at the minimum of all tMax values
|
|
168
|
+
float tFar = min(min(tMax.x, tMax.y), tMax.z);
|
|
169
|
+
|
|
170
|
+
// If tNear > tFar, ray misses the box
|
|
171
|
+
// If tFar < 0, box is behind the ray
|
|
172
|
+
if(tNear > tFar || tFar < 0.0) {
|
|
173
|
+
return -1.0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Return the nearest intersection (tNear, unless we're inside the box)
|
|
177
|
+
return tNear > 0.0 ? tNear : tFar;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Calculate normal vector at hit point on box surface
|
|
181
|
+
vec3 boxNormal(in vec3 hitPosition, in Box box)
|
|
182
|
+
{
|
|
183
|
+
// Find which face of the box we hit by checking which coordinate is closest to the box boundary
|
|
184
|
+
vec3 localPos = hitPosition - box.position;
|
|
185
|
+
vec3 absLocal = abs(localPos);
|
|
186
|
+
|
|
187
|
+
// Return normal for the face that's closest to the box size
|
|
188
|
+
if(absLocal.x > absLocal.y && absLocal.x > absLocal.z) {
|
|
189
|
+
return vec3(sign(localPos.x), 0.0, 0.0);
|
|
190
|
+
} else if(absLocal.y > absLocal.z) {
|
|
191
|
+
return vec3(0.0, sign(localPos.y), 0.0);
|
|
192
|
+
} else {
|
|
193
|
+
return vec3(0.0, 0.0, sign(localPos.z));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Find closest box intersection, returns distance and sets hitIndex and hitType
|
|
198
|
+
float findClosestBox(in vec3 rayOrigin, in vec3 rayDirection, in float maxDistance, out int hitIndex, out int hitType)
|
|
199
|
+
{
|
|
200
|
+
float closestDistance = maxDistance;
|
|
201
|
+
hitIndex = -1;
|
|
202
|
+
hitType = -1;
|
|
203
|
+
|
|
204
|
+
for(int i = 0; i < NUM_BOXES; i++) {
|
|
205
|
+
Box box = Box(boxData[i].xyz, boxData[i].w, boxColors[i]);
|
|
206
|
+
float distance = boxIntersection(rayOrigin, rayDirection, box);
|
|
207
|
+
|
|
208
|
+
if(distance > 0.0 && distance < closestDistance) {
|
|
209
|
+
closestDistance = distance;
|
|
210
|
+
hitIndex = i;
|
|
211
|
+
hitType = ${SHAPE_BOX};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return hitIndex >= 0 ? closestDistance : -1.0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Calculate box normal and color at hit point
|
|
219
|
+
void calculateBoxHit(in vec3 hitPosition, in int hitIndex, out vec3 normal, out vec3 color)
|
|
220
|
+
{
|
|
221
|
+
Box hitBox = Box(boxData[hitIndex].xyz, boxData[hitIndex].w, boxColors[hitIndex]);
|
|
222
|
+
normal = boxNormal(hitPosition, hitBox);
|
|
223
|
+
color = boxColors[hitIndex];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
`
|
|
227
|
+
|
|
228
|
+
const fragment = glsl`#version 300 es
|
|
229
|
+
precision mediump float;
|
|
230
|
+
|
|
231
|
+
struct HitInfo {
|
|
232
|
+
bool hit;
|
|
233
|
+
float distance;
|
|
234
|
+
vec3 position;
|
|
235
|
+
vec3 normal;
|
|
236
|
+
vec3 color;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
${sphereModule}
|
|
240
|
+
${boxModule}
|
|
241
|
+
|
|
242
|
+
${uniform.float('aspectRatio')}
|
|
243
|
+
|
|
244
|
+
in vec2 v_uv;
|
|
245
|
+
out vec4 fragColor;
|
|
246
|
+
|
|
247
|
+
vec3 lightPos = vec3(5.0, 5.0, 5.0);
|
|
248
|
+
|
|
249
|
+
vec3 calculateLighting(in vec3 position, in vec3 normal, in vec3 color)
|
|
250
|
+
{
|
|
251
|
+
vec3 lightDirection = normalize(lightPos - position);
|
|
252
|
+
float diffuse = max(0.0, dot(normal, lightDirection));
|
|
253
|
+
return color * (diffuse * 0.8 + 0.2); // 80% diffuse + 20% ambient
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
void main()
|
|
257
|
+
{
|
|
258
|
+
// Ray from camera through pixel
|
|
259
|
+
vec3 rayOrigin = vec3(0.0, 0.0, 4.0);
|
|
260
|
+
vec3 rayDirection = normalize(vec3(v_uv.x * aspectRatio, v_uv.y, -1.0));
|
|
261
|
+
|
|
262
|
+
// Find closest intersection across all shapes
|
|
263
|
+
float closestDistance = 1000.0;
|
|
264
|
+
int hitType = -1;
|
|
265
|
+
int hitIndex = -1;
|
|
266
|
+
|
|
267
|
+
// Check spheres
|
|
268
|
+
int sphereIndex, sphereType;
|
|
269
|
+
float sphereDistance = findClosestSphere(rayOrigin, rayDirection, closestDistance, sphereIndex, sphereType);
|
|
270
|
+
if(sphereDistance > 0.0) {
|
|
271
|
+
closestDistance = sphereDistance;
|
|
272
|
+
hitType = sphereType;
|
|
273
|
+
hitIndex = sphereIndex;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check boxes
|
|
277
|
+
int boxIndex, boxType;
|
|
278
|
+
float boxDistance = findClosestBox(rayOrigin, rayDirection, closestDistance, boxIndex, boxType);
|
|
279
|
+
if(boxDistance > 0.0) {
|
|
280
|
+
closestDistance = boxDistance;
|
|
281
|
+
hitType = boxType;
|
|
282
|
+
hitIndex = boxIndex;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
vec3 color = vec3(0.1, 0.1, 0.2); // Background color
|
|
286
|
+
|
|
287
|
+
// Calculate normal and lighting only for the closest hit
|
|
288
|
+
if(hitType >= 0) {
|
|
289
|
+
vec3 hitPosition = rayOrigin + closestDistance * rayDirection;
|
|
290
|
+
vec3 normal;
|
|
291
|
+
vec3 hitColor;
|
|
292
|
+
|
|
293
|
+
switch(hitType) {
|
|
294
|
+
case ${SHAPE_SPHERE}:
|
|
295
|
+
calculateSphereHit(hitPosition, hitIndex, normal, hitColor);
|
|
296
|
+
color = calculateLighting(hitPosition, normal, hitColor);
|
|
297
|
+
break;
|
|
298
|
+
case ${SHAPE_BOX}:
|
|
299
|
+
calculateBoxHit(hitPosition, hitIndex, normal, hitColor);
|
|
300
|
+
color = calculateLighting(hitPosition, normal, hitColor);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Gamma correction
|
|
306
|
+
color = pow(color, vec3(1.0 / 2.2));
|
|
307
|
+
|
|
308
|
+
fragColor = vec4(color, 1.0);
|
|
309
|
+
}`
|
|
310
|
+
|
|
311
|
+
const { program, view } = compile.toQuad(gl, fragment)
|
|
312
|
+
|
|
313
|
+
gl.useProgram(program)
|
|
314
|
+
|
|
315
|
+
// Bind quad buffer
|
|
316
|
+
view.attributes.a_quad.bind()
|
|
317
|
+
|
|
318
|
+
// Create typed arrays for sphere data and colors
|
|
319
|
+
const spheresArray = new Float32Array(
|
|
320
|
+
(function* () {
|
|
321
|
+
for (const sphere of sphereData) {
|
|
322
|
+
yield sphere[0]
|
|
323
|
+
yield sphere[1]
|
|
324
|
+
yield sphere[2]
|
|
325
|
+
yield sphere[3]
|
|
326
|
+
}
|
|
327
|
+
})(),
|
|
328
|
+
)
|
|
329
|
+
const sphereColorsArray = new Float32Array(
|
|
330
|
+
(function* () {
|
|
331
|
+
for (const color of sphereColors) {
|
|
332
|
+
yield color[0]
|
|
333
|
+
yield color[1]
|
|
334
|
+
yield color[2]
|
|
335
|
+
}
|
|
336
|
+
})(),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
// Create typed arrays for box data and colors
|
|
340
|
+
const boxesArray = new Float32Array(
|
|
341
|
+
(function* () {
|
|
342
|
+
for (const box of boxData) {
|
|
343
|
+
yield box[0]
|
|
344
|
+
yield box[1]
|
|
345
|
+
yield box[2]
|
|
346
|
+
yield box[3]
|
|
347
|
+
}
|
|
348
|
+
})(),
|
|
349
|
+
)
|
|
350
|
+
const boxColorsArray = new Float32Array(
|
|
351
|
+
(function* () {
|
|
352
|
+
for (const color of boxColors) {
|
|
353
|
+
yield color[0]
|
|
354
|
+
yield color[1]
|
|
355
|
+
yield color[2]
|
|
356
|
+
}
|
|
357
|
+
})(),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
// Set the uniform arrays
|
|
361
|
+
view.uniforms.sphereData.set(spheresArray)
|
|
362
|
+
view.uniforms.sphereColors.set(sphereColorsArray)
|
|
363
|
+
view.uniforms.boxData.set(boxesArray)
|
|
364
|
+
view.uniforms.boxColors.set(boxColorsArray)
|
|
365
|
+
|
|
366
|
+
// Set initial aspect ratio
|
|
367
|
+
view.uniforms.aspectRatio.set(canvas.width / canvas.height)
|
|
368
|
+
|
|
369
|
+
function animate(timestamp: number) {
|
|
370
|
+
const time = timestamp / 1000
|
|
371
|
+
|
|
372
|
+
// Make the center sphere pulse
|
|
373
|
+
const pulseFactor = 0.8 + 0.4 * Math.sin(time * 2)
|
|
374
|
+
spheresArray[3] = pulseFactor // Update radius of sphere 0
|
|
375
|
+
|
|
376
|
+
// Orbit some spheres
|
|
377
|
+
const orbitRadius = 2.5
|
|
378
|
+
|
|
379
|
+
// Update sphere 1
|
|
380
|
+
spheresArray[4] = Math.cos(time * 0.8) * orbitRadius
|
|
381
|
+
spheresArray[5] = Math.sin(time * 1.2) * 1.5
|
|
382
|
+
spheresArray[6] = Math.sin(time * 0.8) * orbitRadius
|
|
383
|
+
|
|
384
|
+
// Update sphere 2
|
|
385
|
+
spheresArray[8] = Math.cos(time * 0.6 + Math.PI) * orbitRadius
|
|
386
|
+
spheresArray[9] = Math.cos(time * 0.9) * 1.2
|
|
387
|
+
spheresArray[10] = Math.sin(time * 0.6 + Math.PI) * orbitRadius
|
|
388
|
+
|
|
389
|
+
// Sphere 3 - vertical bobbing
|
|
390
|
+
spheresArray[13] = 1 + Math.sin(time * 1.5) * 2 // y position
|
|
391
|
+
|
|
392
|
+
// Sphere 4 - figure-8 motion
|
|
393
|
+
spheresArray[16] = Math.sin(time * 0.7) * 2.5 // x
|
|
394
|
+
spheresArray[17] = Math.sin(time * 1.4) * 1.5 // y
|
|
395
|
+
spheresArray[18] = -2 + Math.cos(time * 0.7) // z
|
|
396
|
+
|
|
397
|
+
// Sphere 5 - circular motion + size change
|
|
398
|
+
spheresArray[20] = Math.cos(time) * 3.5 // x
|
|
399
|
+
spheresArray[22] = Math.sin(time) * 3.5 // z
|
|
400
|
+
spheresArray[23] = 0.4 + 0.2 * Math.sin(time * 3) // radius
|
|
401
|
+
|
|
402
|
+
// Sphere 6 - diagonal wave motion
|
|
403
|
+
spheresArray[24] = -4 + Math.sin(time * 0.8) * 1.5 // x
|
|
404
|
+
spheresArray[25] = 2 + Math.sin(time * 1.2) * 1 // y
|
|
405
|
+
|
|
406
|
+
// Sphere 7 - rotating in back
|
|
407
|
+
spheresArray[28] = Math.cos(time * 0.5) * 2 // x
|
|
408
|
+
spheresArray[29] = -1 + Math.sin(time * 0.5) * 0.5 // y
|
|
409
|
+
|
|
410
|
+
// Animate colors for more spheres
|
|
411
|
+
sphereColorsArray[0] = 0.5 + 0.5 * Math.sin(time * 3) // Red channel of sphere 0
|
|
412
|
+
sphereColorsArray[4] = 0.5 + 0.5 * Math.cos(time * 2) // Green channel of sphere 1
|
|
413
|
+
sphereColorsArray[9] = 0.5 + 0.5 * Math.sin(time * 2.5) // Blue channel of sphere 3
|
|
414
|
+
sphereColorsArray[15] = 0.3 + 0.7 * Math.abs(Math.sin(time * 1.8)) // All channels sphere 5 (pulsing cyan)
|
|
415
|
+
sphereColorsArray[16] = 0.3 + 0.7 * Math.abs(Math.sin(time * 1.8))
|
|
416
|
+
sphereColorsArray[17] = 0.3 + 0.7 * Math.abs(Math.sin(time * 1.8))
|
|
417
|
+
|
|
418
|
+
// Animate some boxes
|
|
419
|
+
// Box 0 - rotate around Y axis
|
|
420
|
+
boxesArray[0] = 2 + Math.cos(time * 0.7) * 0.5 // x
|
|
421
|
+
boxesArray[2] = -1 + Math.sin(time * 0.7) * 0.5 // z
|
|
422
|
+
|
|
423
|
+
// Box 1 - scale up and down
|
|
424
|
+
boxesArray[7] = 0.6 + 0.3 * Math.sin(time * 2) // size
|
|
425
|
+
|
|
426
|
+
// Update the uniform arrays with the animated data
|
|
427
|
+
view.uniforms.sphereData?.set(spheresArray)
|
|
428
|
+
view.uniforms.sphereColors?.set(sphereColorsArray)
|
|
429
|
+
view.uniforms.boxData?.set(boxesArray)
|
|
430
|
+
view.uniforms.boxColors?.set(boxColorsArray)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Animation loop
|
|
434
|
+
function draw(timestamp: number) {
|
|
435
|
+
if (controller.signal.aborted) return
|
|
436
|
+
|
|
437
|
+
animate(timestamp)
|
|
438
|
+
|
|
439
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6)
|
|
440
|
+
|
|
441
|
+
requestAnimationFrame(draw)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
requestAnimationFrame(draw)
|
|
445
|
+
|
|
446
|
+
// Handle canvas resizing
|
|
447
|
+
new ResizeObserver(() => {
|
|
448
|
+
canvas.width = window.innerWidth
|
|
449
|
+
canvas.height = window.innerHeight
|
|
450
|
+
gl.viewport(0, 0, canvas.width, canvas.height)
|
|
451
|
+
|
|
452
|
+
// Update aspect ratio uniform
|
|
453
|
+
view.uniforms.aspectRatio.set(canvas.width / canvas.height)
|
|
454
|
+
draw(performance.now())
|
|
455
|
+
}).observe(document.body)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { attribute, compile, glsl, uniform } from 'view.gl/tag'
|
|
2
|
+
import { GLSLToken, GLSLToView, View } from '../../../src'
|
|
3
|
+
import { dom } from '../utils'
|
|
4
|
+
|
|
5
|
+
function createModule<TTag>(callback: (symbol: symbol) => TTag) {
|
|
6
|
+
const symbol = Symbol()
|
|
7
|
+
return {
|
|
8
|
+
pointer: symbol,
|
|
9
|
+
module: callback(symbol),
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const controller = new AbortController()
|
|
13
|
+
const canvas = dom('canvas', { parentElement: document.body }) // (<canvas />) as HTMLCanvasElement;
|
|
14
|
+
|
|
15
|
+
const gl = canvas.getContext('webgl2')!
|
|
16
|
+
gl.viewport(0, 0, canvas.width, canvas.height)
|
|
17
|
+
|
|
18
|
+
new ResizeObserver(() => {
|
|
19
|
+
canvas.width = window.innerWidth
|
|
20
|
+
canvas.height = window.innerHeight
|
|
21
|
+
gl.viewport(0, 0, canvas.width, canvas.height)
|
|
22
|
+
draw(performance.now())
|
|
23
|
+
}).observe(canvas)
|
|
24
|
+
|
|
25
|
+
const circle = createModule(
|
|
26
|
+
symbol => glsl`
|
|
27
|
+
${uniform.vec2('u_base')}
|
|
28
|
+
|
|
29
|
+
bool ${symbol}(vec2 position){
|
|
30
|
+
return distance(position + u_base, vec2(0., 0.)) < 1.0;
|
|
31
|
+
}`,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const vertex = glsl`
|
|
35
|
+
precision mediump float;
|
|
36
|
+
|
|
37
|
+
${attribute.vec2('a_vertex')}
|
|
38
|
+
|
|
39
|
+
varying vec2 position;
|
|
40
|
+
|
|
41
|
+
void main() {
|
|
42
|
+
position = a_vertex;
|
|
43
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
44
|
+
}`
|
|
45
|
+
|
|
46
|
+
const fragment = glsl`
|
|
47
|
+
precision mediump float;
|
|
48
|
+
varying vec2 position;
|
|
49
|
+
${uniform.vec3('u_color')}
|
|
50
|
+
|
|
51
|
+
${circle.module}
|
|
52
|
+
|
|
53
|
+
void main() {
|
|
54
|
+
if(${circle.pointer}(position) == false){
|
|
55
|
+
discard;
|
|
56
|
+
}
|
|
57
|
+
gl_FragColor = vec4(${'u_color'}, 1.0);
|
|
58
|
+
}`
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
program,
|
|
62
|
+
view: { attributes, uniforms },
|
|
63
|
+
} = compile(gl, vertex, fragment)
|
|
64
|
+
|
|
65
|
+
gl.useProgram(program)
|
|
66
|
+
|
|
67
|
+
uniforms.u_color.set(0, 255, 0)
|
|
68
|
+
|
|
69
|
+
uniforms.u_base.set(1, 1)
|
|
70
|
+
|
|
71
|
+
// Create triangle vertex buffer
|
|
72
|
+
attributes.a_vertex.set(new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1])).bind()
|
|
73
|
+
|
|
74
|
+
function draw(delta: number) {
|
|
75
|
+
if (controller.signal.aborted) return
|
|
76
|
+
requestAnimationFrame(draw)
|
|
77
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, 100)
|
|
78
|
+
}
|
|
79
|
+
requestAnimationFrame(draw)
|
|
80
|
+
|
|
81
|
+
abstract class Module {
|
|
82
|
+
symbol = Symbol()
|
|
83
|
+
abstract view?: View
|
|
84
|
+
abstract template: GLSLToken
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class CircleModule extends Module {
|
|
88
|
+
view?: GLSLToView<typeof this.template>
|
|
89
|
+
base = Symbol()
|
|
90
|
+
template = glsl`
|
|
91
|
+
${uniform.vec2(this.base)}
|
|
92
|
+
bool ${this.symbol}(vec2 position){
|
|
93
|
+
return distance(position + ${this.base}, vec2(0., 0.)) < 1.0;
|
|
94
|
+
}`
|
|
95
|
+
setBase(x: number, y: number) {
|
|
96
|
+
this.view?.uniforms[this.base]?.set(x, y)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { attribute, compile, glsl, uniform } from 'view.gl/tag'
|
|
2
|
-
import {
|
|
2
|
+
import { dom } from '../utils'
|
|
3
3
|
|
|
4
|
-
const canvas =
|
|
4
|
+
const canvas = dom('canvas', {
|
|
5
5
|
width: window.innerWidth,
|
|
6
6
|
height: window.innerHeight,
|
|
7
7
|
parentElement: document.body,
|
package/dev/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import css from './index.css?url'
|
|
2
|
-
import {
|
|
2
|
+
import { dom } from './utils'
|
|
3
3
|
|
|
4
4
|
const iframe = document.querySelector('iframe')!
|
|
5
5
|
const nav = document.querySelector('nav')!
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
nav.append(dom('h3', { innerText: 'Examples' }))
|
|
8
8
|
|
|
9
9
|
const urls = Object.fromEntries(
|
|
10
10
|
Object.entries(import.meta.glob('./examples/*.ts', { eager: false })).map(([key, entry]) => {
|
|
@@ -15,15 +15,16 @@ const urls = Object.fromEntries(
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
Object.entries(urls).forEach(([name, localUrl]) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
event
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
nav.append(
|
|
19
|
+
dom('button', {
|
|
20
|
+
onclick(event) {
|
|
21
|
+
event.preventDefault()
|
|
22
|
+
load(name, localUrl)
|
|
23
|
+
},
|
|
24
|
+
innerHTML: name.replaceAll('-', ' '),
|
|
25
|
+
'data-route': name,
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
27
28
|
})
|
|
28
29
|
|
|
29
30
|
function load(name: string, url: string) {
|
package/dev/src/utils.ts
CHANGED
|
@@ -43,25 +43,39 @@ export function cursor(event: PointerEvent, callback: (event: CursorEvent) => vo
|
|
|
43
43
|
return promise
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function
|
|
46
|
+
export function dom<T extends keyof HTMLElementTagNameMap>(
|
|
47
47
|
tag: T,
|
|
48
48
|
{
|
|
49
49
|
parentElement,
|
|
50
50
|
...options
|
|
51
|
-
}: Partial<Omit<HTMLElementTagNameMap[T], 'style'> & { style:
|
|
51
|
+
}: Partial<Omit<HTMLElementTagNameMap[T], 'style'> & { style: Partial<CSSStyleDeclaration> }> &
|
|
52
52
|
Record<`data-${string}`, string> = {},
|
|
53
53
|
) {
|
|
54
54
|
const element = document.createElement(tag)
|
|
55
55
|
|
|
56
56
|
for (const key in options) {
|
|
57
57
|
if (key.startsWith('data-')) {
|
|
58
|
-
element.setAttribute(
|
|
58
|
+
element.setAttribute(
|
|
59
|
+
key,
|
|
60
|
+
// @ts-expect-error
|
|
61
|
+
options[key],
|
|
62
|
+
)
|
|
63
|
+
} else if (key == 'style') {
|
|
64
|
+
if (typeof options.style === 'string') {
|
|
65
|
+
element.style = options.style
|
|
66
|
+
} else {
|
|
67
|
+
for (const prop in options.style!) {
|
|
68
|
+
// @ts-expect-error
|
|
69
|
+
element.style[prop] = options.style![prop as any]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
59
72
|
} else {
|
|
73
|
+
// @ts-expect-error
|
|
60
74
|
element[key] = options[key]
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
|
|
78
|
+
parentElement?.appendChild(element)
|
|
65
79
|
|
|
66
80
|
return element
|
|
67
81
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { V as ViewSchema, G as GL, a as ViewOptions, b as View, U as UniformSchema, c as UniformView, A as AttributeSchema, d as AttributeView, I as InterleavedAttributeSchema, e as InterleavedAttributeView, B as BufferSchema, f as BufferView } from './types-
|
|
2
|
-
export { v as AttributeDefinition, u as AttributeKind, w as AttributeMethods, t as AttributeShortHand, a1 as
|
|
1
|
+
import { V as ViewSchema, G as GL, a as ViewOptions, b as View, U as UniformSchema, c as UniformView, A as AttributeSchema, d as AttributeView, I as InterleavedAttributeSchema, e as InterleavedAttributeView, B as BufferSchema, f as BufferView } from './types-DaV9McmR.mjs';
|
|
2
|
+
export { v as AttributeDefinition, u as AttributeKind, w as AttributeMethods, t as AttributeShortHand, a1 as AttributeToken, a2 as AttributeTokenFn, C as BufferDefinition, D as BufferMethods, a8 as CompileResult, R as Format, E as Framebuffer, F as FramebufferDefinition, _ as GLSL, Y as GLSLResult, Z as GLSLSlot, a6 as GLSLSlotsToSchema, a4 as GLSLToSchema, a5 as GLSLToView, g as GLTarget, l as GLTextureFilter, j as GLTextureFormat, i as GLTextureTarget, k as GLTextureType, m as GLTextureWrap, h as GLUsage, a3 as InterleaveToken, y as InterleavedAttributeDefinition, x as InterleavedAttributeLayout, z as InterleavedAttributeMethods, Q as InternalFormat, n as KIND_TO_SIZE_MAP, K as KIND_TO_UNIFORM_FN_NAME_MAP, a7 as MergeGLSLSchema, S as PixelType, W as TexImage2DOptions, T as TextureDefinition, L as TextureMethods, N as TextureParameterName, O as TextureParameterValue, P as TextureParameters, J as TextureSchema, H as TextureTarget, M as TextureView, q as UniformDefinition, o as UniformFnName, p as UniformKind, s as UniformMethods, r as UniformSchemaPartial, $ as UniformToken, a0 as UniformTokenFn, X as ViewSchemaPartial } from './types-DaV9McmR.mjs';
|
|
3
3
|
|
|
4
4
|
declare function toID(key: string | number | symbol): any;
|
|
5
5
|
/**********************************************************************************/
|