@bigmistqke/view.gl 0.1.13 → 0.1.15
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 +4 -9
- package/.vscode/settings.json +2 -0
- package/dist/{chunk-Z4F7UTKN.mjs → chunk-4YLGYQ3G.mjs} +36 -111
- package/dist/chunk-4YLGYQ3G.mjs.map +1 -0
- package/dist/chunk-BZU5QEWD.mjs +72 -0
- package/dist/chunk-BZU5QEWD.mjs.map +1 -0
- package/dist/gl.d.mts +12 -0
- package/dist/gl.mjs +13 -0
- package/dist/gl.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -1
- package/dist/tag.d.mts +1 -1
- package/dist/tag.mjs +4 -2
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-Cggy6Gkm.d.mts → types-lm9fTTy8.d.mts} +4 -1
- package/package.json +5 -1
- package/src/gl.ts +128 -0
- package/src/index.ts +11 -19
- package/src/tag.ts +2 -1
- package/src/{utils/index.ts → utils.ts} +29 -47
- package/tsup.config.ts +1 -1
- package/dev/dist/404.html +0 -19
- package/dev/dist/assets/bezier-DuLTb2d1.js +0 -294
- package/dev/dist/assets/game_of_life-D7qHdFcv.js +0 -202
- package/dev/dist/assets/grid-Ie22U_9K.js +0 -173
- package/dev/dist/assets/index-0qrXMlfv.js +0 -109
- package/dev/dist/assets/index-C2N3u8wj.css +0 -12
- package/dev/dist/assets/main-BZ1N6pp6.js +0 -32
- package/dev/dist/assets/overview-DAxtMIw5.js +0 -78
- package/dev/dist/assets/pix_sim-DIhzM_vu.js +0 -501
- package/dev/dist/assets/symbol-BouRL-Ni.js +0 -65
- package/dev/dist/assets/tag-C_bg62fD.js +0 -516
- package/dev/dist/assets/tetris-IMnkSi9p.js +0 -344
- package/dev/dist/assets/utils-CvCgsM08.js +0 -50
- package/dev/dist/index.html +0 -51
- package/dist/chunk-Z4F7UTKN.mjs.map +0 -1
- package/src/features.ts +0 -60
- package/src/utils/object.ts +0 -27
package/src/gl.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { RemoveSuffix } from './type-utils'
|
|
2
|
+
import type { GL } from './types'
|
|
3
|
+
|
|
4
|
+
/**********************************************************************************/
|
|
5
|
+
/* */
|
|
6
|
+
/* Create Shader */
|
|
7
|
+
/* */
|
|
8
|
+
/**********************************************************************************/
|
|
9
|
+
|
|
10
|
+
export function createShader(gl: GL, type: number, source: string): WebGLShader {
|
|
11
|
+
const shader = gl.createShader(type)
|
|
12
|
+
if (!shader) throw new Error('Failed to create shader')
|
|
13
|
+
|
|
14
|
+
gl.shaderSource(shader, source)
|
|
15
|
+
gl.compileShader(shader)
|
|
16
|
+
|
|
17
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
18
|
+
const info = gl.getShaderInfoLog(shader)
|
|
19
|
+
gl.deleteShader(shader)
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Failed to compile ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader: ${info}`,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return shader
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**********************************************************************************/
|
|
29
|
+
/* */
|
|
30
|
+
/* Create Program */
|
|
31
|
+
/* */
|
|
32
|
+
/**********************************************************************************/
|
|
33
|
+
|
|
34
|
+
export function createProgram(gl: GL, vertexSource: string, fragmentSource: string): WebGLProgram {
|
|
35
|
+
const program = gl.createProgram()
|
|
36
|
+
if (!program) throw new Error('Failed to create WebGL program')
|
|
37
|
+
|
|
38
|
+
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
|
|
39
|
+
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
|
|
40
|
+
|
|
41
|
+
gl.attachShader(program, vertexShader)
|
|
42
|
+
gl.attachShader(program, fragmentShader)
|
|
43
|
+
gl.linkProgram(program)
|
|
44
|
+
|
|
45
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
46
|
+
const info = gl.getProgramInfoLog(program)
|
|
47
|
+
gl.deleteProgram(program)
|
|
48
|
+
gl.deleteShader(vertexShader)
|
|
49
|
+
gl.deleteShader(fragmentShader)
|
|
50
|
+
throw new Error(`Failed to link program: ${info}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Clean up shaders after linking
|
|
54
|
+
gl.deleteShader(vertexShader)
|
|
55
|
+
gl.deleteShader(fragmentShader)
|
|
56
|
+
|
|
57
|
+
return program
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**********************************************************************************/
|
|
61
|
+
/* */
|
|
62
|
+
/* Get Instanced Arrays */
|
|
63
|
+
/* */
|
|
64
|
+
/**********************************************************************************/
|
|
65
|
+
|
|
66
|
+
const INSTANCED_ARRAYS_WRAPPER_MAP = new WeakMap<
|
|
67
|
+
GL,
|
|
68
|
+
RemoveSuffix<
|
|
69
|
+
Pick<
|
|
70
|
+
ANGLE_instanced_arrays,
|
|
71
|
+
'drawArraysInstancedANGLE' | 'drawElementsInstancedANGLE' | 'vertexAttribDivisorANGLE'
|
|
72
|
+
>,
|
|
73
|
+
'ANGLE'
|
|
74
|
+
> | null
|
|
75
|
+
>()
|
|
76
|
+
export function getInstancedArrays(gl: GL) {
|
|
77
|
+
if (gl instanceof WebGL2RenderingContext) return gl
|
|
78
|
+
|
|
79
|
+
const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl)
|
|
80
|
+
if (cached) return cached
|
|
81
|
+
|
|
82
|
+
const ext = gl.getExtension('ANGLE_instanced_arrays')
|
|
83
|
+
if (!ext) return undefined
|
|
84
|
+
|
|
85
|
+
const wrapper = {
|
|
86
|
+
drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext),
|
|
87
|
+
drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext),
|
|
88
|
+
vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext),
|
|
89
|
+
}
|
|
90
|
+
INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper)
|
|
91
|
+
|
|
92
|
+
return wrapper
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**********************************************************************************/
|
|
96
|
+
/* */
|
|
97
|
+
/* Get Vertex Array Object */
|
|
98
|
+
/* */
|
|
99
|
+
/**********************************************************************************/
|
|
100
|
+
|
|
101
|
+
const VERTEX_ARRAY_OBJECT_WRAPPER_MAP = new WeakMap<
|
|
102
|
+
GL,
|
|
103
|
+
RemoveSuffix<
|
|
104
|
+
Pick<
|
|
105
|
+
OES_vertex_array_object,
|
|
106
|
+
'bindVertexArrayOES' | 'createVertexArrayOES' | 'deleteVertexArrayOES'
|
|
107
|
+
>,
|
|
108
|
+
'OES'
|
|
109
|
+
> | null
|
|
110
|
+
>()
|
|
111
|
+
export function getVertexArrayObject(gl: GL) {
|
|
112
|
+
if (gl instanceof WebGL2RenderingContext) return gl
|
|
113
|
+
|
|
114
|
+
const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl)
|
|
115
|
+
if (cached) return cached
|
|
116
|
+
|
|
117
|
+
const ext = gl.getExtension('OES_vertex_array_object')
|
|
118
|
+
if (!ext) return null
|
|
119
|
+
|
|
120
|
+
const wrapper = {
|
|
121
|
+
bindVertexArray: ext.bindVertexArrayOES.bind(ext),
|
|
122
|
+
createVertexArray: ext.createVertexArrayOES.bind(ext),
|
|
123
|
+
deleteVertexArray: ext.deleteVertexArrayOES.bind(ext),
|
|
124
|
+
}
|
|
125
|
+
VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper)
|
|
126
|
+
|
|
127
|
+
return wrapper
|
|
128
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getInstancedArrays, getVertexArrayObject } from './
|
|
1
|
+
import { getInstancedArrays, getVertexArrayObject } from './gl'
|
|
2
2
|
import type {
|
|
3
3
|
AttributeMethods,
|
|
4
4
|
AttributeSchema,
|
|
@@ -15,15 +15,8 @@ import type {
|
|
|
15
15
|
ViewOptions,
|
|
16
16
|
ViewSchema,
|
|
17
17
|
} from './types'
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
createUpsertMap,
|
|
21
|
-
isMatKind,
|
|
22
|
-
isSamplerKind,
|
|
23
|
-
kindToSize,
|
|
24
|
-
kindToUniformFnName,
|
|
25
|
-
ObjectUtils,
|
|
26
|
-
} from './utils'
|
|
18
|
+
import { isSamplerKind, kindToUniformFnName, isMatKind, assertedNotNullish, kindToSize, forEachObject } from './utils'
|
|
19
|
+
import { createUpsertMap, mapObject } from './utils'
|
|
27
20
|
export * from './types'
|
|
28
21
|
|
|
29
22
|
/**********************************************************************************/
|
|
@@ -94,7 +87,7 @@ export function uniformView<T extends UniformSchema>(
|
|
|
94
87
|
schema: T,
|
|
95
88
|
): InferUniformView<T> {
|
|
96
89
|
// @ts-expect-error - conditional return type based on size is hard to infer
|
|
97
|
-
return
|
|
90
|
+
return mapObject(schema, ({ kind, size }, key) => {
|
|
98
91
|
const name = toID(key)
|
|
99
92
|
|
|
100
93
|
if (isSamplerKind(kind)) {
|
|
@@ -191,7 +184,7 @@ export function attributeView<T extends AttributeSchema>(
|
|
|
191
184
|
schema: T,
|
|
192
185
|
{ signal }: ViewOptions = {},
|
|
193
186
|
): AttributeView<T> {
|
|
194
|
-
const attributes =
|
|
187
|
+
const attributes = mapObject(
|
|
195
188
|
schema,
|
|
196
189
|
(
|
|
197
190
|
{ kind, instanced, buffer = assertedNotNullish(gl.createBuffer()) },
|
|
@@ -226,7 +219,7 @@ export function attributeView<T extends AttributeSchema>(
|
|
|
226
219
|
)
|
|
227
220
|
|
|
228
221
|
signal?.addEventListener('abort', function dispose() {
|
|
229
|
-
|
|
222
|
+
forEachObject(attributes, value => value.dispose())
|
|
230
223
|
})
|
|
231
224
|
|
|
232
225
|
return attributes
|
|
@@ -245,7 +238,7 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
|
|
|
245
238
|
{ signal }: ViewOptions = {},
|
|
246
239
|
): InterleavedAttributeView<T> {
|
|
247
240
|
// Initialize interleaved attributes
|
|
248
|
-
const interleavedAttributes =
|
|
241
|
+
const interleavedAttributes = mapObject(schema, ({ layout, instanced }) => {
|
|
249
242
|
// Increment number to keep track of offset
|
|
250
243
|
let index = 0
|
|
251
244
|
|
|
@@ -325,9 +318,8 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
|
|
|
325
318
|
set(value, usage = 'STATIC_DRAW') {
|
|
326
319
|
if (vao) {
|
|
327
320
|
vao.bind()
|
|
328
|
-
} else {
|
|
329
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
|
|
330
321
|
}
|
|
322
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
|
|
331
323
|
gl.bufferData(gl.ARRAY_BUFFER, value, gl[usage])
|
|
332
324
|
if (vao) {
|
|
333
325
|
vao.unbind()
|
|
@@ -337,7 +329,7 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
|
|
|
337
329
|
})
|
|
338
330
|
|
|
339
331
|
signal?.addEventListener('abort', function dispose() {
|
|
340
|
-
|
|
332
|
+
forEachObject(interleavedAttributes, value => value.dispose())
|
|
341
333
|
})
|
|
342
334
|
|
|
343
335
|
return interleavedAttributes
|
|
@@ -355,7 +347,7 @@ export function bufferView<T extends BufferSchema>(
|
|
|
355
347
|
{ signal }: ViewOptions = {},
|
|
356
348
|
): BufferView<T> {
|
|
357
349
|
// Initialize buffers
|
|
358
|
-
const buffers =
|
|
350
|
+
const buffers = mapObject(schema, ({ target = 'ARRAY_BUFFER', usage = 'STATIC_DRAW' }) => {
|
|
359
351
|
const buffer = assertedNotNullish(gl.createBuffer())
|
|
360
352
|
return {
|
|
361
353
|
bind() {
|
|
@@ -372,7 +364,7 @@ export function bufferView<T extends BufferSchema>(
|
|
|
372
364
|
})
|
|
373
365
|
|
|
374
366
|
signal?.addEventListener('abort', function dispose() {
|
|
375
|
-
|
|
367
|
+
forEachObject(buffers, value => value.dispose())
|
|
376
368
|
})
|
|
377
369
|
|
|
378
370
|
return buffers
|
package/src/tag.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toID, view } from '.'
|
|
2
|
+
import { createProgram } from './gl'
|
|
2
3
|
import type { Prettify } from './type-utils'
|
|
3
4
|
import type {
|
|
4
5
|
AttributeDefinition,
|
|
@@ -17,7 +18,7 @@ import type {
|
|
|
17
18
|
ViewSchema,
|
|
18
19
|
ViewSchemaPartial,
|
|
19
20
|
} from './types'
|
|
20
|
-
import {
|
|
21
|
+
import { createUpsertMap } from './utils'
|
|
21
22
|
|
|
22
23
|
export function glsl<TSlot extends GLSLSlot, const TSlots extends TSlot[]>(
|
|
23
24
|
template: TemplateStringsArray,
|
|
@@ -6,59 +6,13 @@ import type {
|
|
|
6
6
|
KIND_TO_UNIFORM_FN_NAME_MAP,
|
|
7
7
|
TextureDefinition,
|
|
8
8
|
UniformKind,
|
|
9
|
-
} from '
|
|
10
|
-
|
|
11
|
-
export * as ObjectUtils from './object'
|
|
9
|
+
} from './types'
|
|
12
10
|
|
|
13
11
|
export function assertedNotNullish<T>(value: T, message?: string): NonNullable<T> {
|
|
14
12
|
if (value === undefined || value === null) throw new Error(message)
|
|
15
13
|
return value
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
function createGLShader(gl: GL, type: number, source: string): WebGLShader {
|
|
19
|
-
const shader = gl.createShader(type)
|
|
20
|
-
if (!shader) throw new Error('Failed to create shader')
|
|
21
|
-
|
|
22
|
-
gl.shaderSource(shader, source)
|
|
23
|
-
gl.compileShader(shader)
|
|
24
|
-
|
|
25
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
26
|
-
const info = gl.getShaderInfoLog(shader)
|
|
27
|
-
gl.deleteShader(shader)
|
|
28
|
-
throw new Error(
|
|
29
|
-
`Failed to compile ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader: ${info}`,
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return shader
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function createProgram(gl: GL, vertexSource: string, fragmentSource: string): WebGLProgram {
|
|
37
|
-
const program = gl.createProgram()
|
|
38
|
-
if (!program) throw new Error('Failed to create WebGL program')
|
|
39
|
-
|
|
40
|
-
const vertexShader = createGLShader(gl, gl.VERTEX_SHADER, vertexSource)
|
|
41
|
-
const fragmentShader = createGLShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
|
|
42
|
-
|
|
43
|
-
gl.attachShader(program, vertexShader)
|
|
44
|
-
gl.attachShader(program, fragmentShader)
|
|
45
|
-
gl.linkProgram(program)
|
|
46
|
-
|
|
47
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
48
|
-
const info = gl.getProgramInfoLog(program)
|
|
49
|
-
gl.deleteProgram(program)
|
|
50
|
-
gl.deleteShader(vertexShader)
|
|
51
|
-
gl.deleteShader(fragmentShader)
|
|
52
|
-
throw new Error(`Failed to link program: ${info}`)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Clean up shaders after linking
|
|
56
|
-
gl.deleteShader(vertexShader)
|
|
57
|
-
gl.deleteShader(fragmentShader)
|
|
58
|
-
|
|
59
|
-
return program
|
|
60
|
-
}
|
|
61
|
-
|
|
62
16
|
/**********************************************************************************/
|
|
63
17
|
/* */
|
|
64
18
|
/* Kind To Uniform Name */
|
|
@@ -292,3 +246,31 @@ export function createUpsertMap<T extends UpsertMapKind<any, any> = Map<any, any
|
|
|
292
246
|
},
|
|
293
247
|
})
|
|
294
248
|
}
|
|
249
|
+
|
|
250
|
+
export function forEachObject<T extends Record<string, any>>(
|
|
251
|
+
value: T,
|
|
252
|
+
callback: (value: T[keyof T], key: keyof T, index: number) => void,
|
|
253
|
+
) {
|
|
254
|
+
let index = 0
|
|
255
|
+
for (const key in value) {
|
|
256
|
+
callback(value[key], key, index)
|
|
257
|
+
index++
|
|
258
|
+
}
|
|
259
|
+
for (const key of Object.getOwnPropertySymbols(value)) {
|
|
260
|
+
callback(value[key as keyof T], key as keyof T, index)
|
|
261
|
+
index++
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function mapObject<T extends Record<string, any>, TReturn>(
|
|
266
|
+
value: T,
|
|
267
|
+
callback: (value: T[keyof T], key: keyof T, index: number) => TReturn,
|
|
268
|
+
): { [TKey in keyof T]: TReturn } {
|
|
269
|
+
const result = {} as { [TKey in keyof T]: TReturn }
|
|
270
|
+
|
|
271
|
+
forEachObject(value, (value, key, index) => {
|
|
272
|
+
result[key] = callback(value, key, index)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
return result
|
|
276
|
+
}
|
package/tsup.config.ts
CHANGED
package/dev/dist/404.html
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
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>
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import { i as interleave, u as uniform, a as attribute, g as glsl, c as compile, v as view } from './tag-C_bg62fD.js';
|
|
2
|
-
import { c as createElement, a as cursor } from './utils-CvCgsM08.js';
|
|
3
|
-
|
|
4
|
-
const canvas = createElement("canvas");
|
|
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, schema: curveSchema } = compile(gl, curveVertex, curveFragment);
|
|
196
|
-
const curveView = view(gl, curveProgram, curveSchema);
|
|
197
|
-
const { program: pointProgram, schema: pointSchema } = compile(gl, pointVertex, pointFragment);
|
|
198
|
-
const pointView = view(gl, pointProgram, pointSchema);
|
|
199
|
-
const { program: lineProgram, schema: lineSchema } = compile(gl, lineVertex, lineFragment);
|
|
200
|
-
const lineView = view(gl, lineProgram, lineSchema);
|
|
201
|
-
const curveSegments = 100;
|
|
202
|
-
function generateTriangleStrip() {
|
|
203
|
-
return new Float32Array(
|
|
204
|
-
function* () {
|
|
205
|
-
for (let i = 0; i <= curveSegments; i++) {
|
|
206
|
-
const t = i / curveSegments;
|
|
207
|
-
yield t;
|
|
208
|
-
yield -1;
|
|
209
|
-
yield t;
|
|
210
|
-
yield 1;
|
|
211
|
-
}
|
|
212
|
-
}()
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
const stripData = generateTriangleStrip();
|
|
216
|
-
curveView.interleavedAttributes.data.set(stripData);
|
|
217
|
-
const pointVertices = new Float32Array([
|
|
218
|
-
-1,
|
|
219
|
-
-1,
|
|
220
|
-
// Bottom-left
|
|
221
|
-
1,
|
|
222
|
-
-1,
|
|
223
|
-
// Bottom-right
|
|
224
|
-
-1,
|
|
225
|
-
1,
|
|
226
|
-
// Top-left
|
|
227
|
-
-1,
|
|
228
|
-
1,
|
|
229
|
-
// Top-left (repeated for second triangle)
|
|
230
|
-
1,
|
|
231
|
-
-1,
|
|
232
|
-
// Bottom-right (repeated for second triangle)
|
|
233
|
-
1,
|
|
234
|
-
1
|
|
235
|
-
// Top-right
|
|
236
|
-
]);
|
|
237
|
-
pointView.attributes.a_vertex.set(pointVertices);
|
|
238
|
-
function updateLineData() {
|
|
239
|
-
const lineVerticesData = new Float32Array([
|
|
240
|
-
state.controlPoints[0].x,
|
|
241
|
-
state.controlPoints[0].y,
|
|
242
|
-
// P0
|
|
243
|
-
state.controlPoints[1].x,
|
|
244
|
-
state.controlPoints[1].y,
|
|
245
|
-
// P1
|
|
246
|
-
state.controlPoints[1].x,
|
|
247
|
-
state.controlPoints[1].y,
|
|
248
|
-
// P1 (repeated)
|
|
249
|
-
state.controlPoints[2].x,
|
|
250
|
-
state.controlPoints[2].y
|
|
251
|
-
// P2
|
|
252
|
-
]);
|
|
253
|
-
lineView.attributes.a_position.set(lineVerticesData);
|
|
254
|
-
}
|
|
255
|
-
updateLineData();
|
|
256
|
-
function render() {
|
|
257
|
-
if (canvas.width !== window.innerWidth || canvas.height !== window.innerHeight) {
|
|
258
|
-
canvas.width = window.innerWidth;
|
|
259
|
-
canvas.height = window.innerHeight;
|
|
260
|
-
}
|
|
261
|
-
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
262
|
-
gl.clearColor(0.1, 0.1, 0.15, 1);
|
|
263
|
-
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
264
|
-
gl.enable(gl.BLEND);
|
|
265
|
-
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
266
|
-
updateLineData();
|
|
267
|
-
gl.useProgram(lineProgram);
|
|
268
|
-
lineView.attributes.a_position.bind();
|
|
269
|
-
lineView.uniforms.u_resolution.set(canvas.width, canvas.height);
|
|
270
|
-
lineView.uniforms.u_offset.set(state.position.x, state.position.y);
|
|
271
|
-
lineView.uniforms.u_scale.set(state.scale);
|
|
272
|
-
gl.drawArrays(gl.LINES, 0, 4);
|
|
273
|
-
gl.useProgram(curveProgram);
|
|
274
|
-
curveView.interleavedAttributes.data.bind();
|
|
275
|
-
curveView.uniforms.u_resolution.set(canvas.width, canvas.height);
|
|
276
|
-
curveView.uniforms.u_offset.set(state.position.x, state.position.y);
|
|
277
|
-
curveView.uniforms.u_scale.set(state.scale);
|
|
278
|
-
curveView.uniforms.u_p0.set(state.controlPoints[0].x, state.controlPoints[0].y);
|
|
279
|
-
curveView.uniforms.u_p1.set(state.controlPoints[1].x, state.controlPoints[1].y);
|
|
280
|
-
curveView.uniforms.u_p2.set(state.controlPoints[2].x, state.controlPoints[2].y);
|
|
281
|
-
curveView.uniforms.u_thickness.set(5);
|
|
282
|
-
gl.drawArrays(gl.TRIANGLE_STRIP, 0, (curveSegments + 1) * 2);
|
|
283
|
-
curveView.interleavedAttributes.data.unbind();
|
|
284
|
-
gl.useProgram(pointProgram);
|
|
285
|
-
pointView.attributes.a_vertex.bind();
|
|
286
|
-
pointView.uniforms.u_resolution.set(canvas.width, canvas.height);
|
|
287
|
-
pointView.uniforms.u_offset.set(state.position.x, state.position.y);
|
|
288
|
-
pointView.uniforms.u_scale.set(state.scale);
|
|
289
|
-
for (const point of state.controlPoints) {
|
|
290
|
-
pointView.uniforms.u_pointPos.set(point.x, point.y);
|
|
291
|
-
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
render();
|