@bigmistqke/view.gl 0.1.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/dev/tetris.ts ADDED
@@ -0,0 +1,439 @@
1
+ import { GL } from 'src/types'
2
+ import { attributeView, uniformView } from 'view.gl'
3
+ import { attribute, compile, glsl, uniform } from 'view.gl/tag'
4
+
5
+ /**********************************************************************************/
6
+ /* */
7
+ /* Utils */
8
+ /* */
9
+ /**********************************************************************************/
10
+
11
+ function rotate(data: Float32Array, size: number, clockwise: boolean = true): void {
12
+ for (let layer = 0; layer < Math.floor(size / 2); layer++) {
13
+ const first = layer
14
+ const last = size - 1 - layer
15
+
16
+ for (let i = 0; i < last - first; i++) {
17
+ const top = getColorIndex(data, first, first + i, size)
18
+ const right = getColorIndex(data, first + i, last, size)
19
+ const bottom = getColorIndex(data, last, last - i, size)
20
+ const left = getColorIndex(data, last - i, first, size)
21
+
22
+ if (clockwise) {
23
+ // top -> right, right -> bottom, bottom -> left, left -> top
24
+ setColorIndex(data, first + i, last, size, top)
25
+ setColorIndex(data, last, last - i, size, right)
26
+ setColorIndex(data, last - i, first, size, bottom)
27
+ setColorIndex(data, first, first + i, size, left)
28
+ } else {
29
+ // top -> left, left -> bottom, bottom -> right, right -> top
30
+ setColorIndex(data, last - i, first, size, top)
31
+ setColorIndex(data, last, last - i, size, left)
32
+ setColorIndex(data, first + i, last, size, bottom)
33
+ setColorIndex(data, first, first + i, size, right)
34
+ }
35
+ }
36
+ }
37
+
38
+ function getColorIndex(buf: Float32Array, x: number, y: number, size: number) {
39
+ return buf[y * size + x]!
40
+ }
41
+
42
+ function setColorIndex(buf: Float32Array, x: number, y: number, size: number, pixel: number) {
43
+ buf[y * size + x] = pixel
44
+ }
45
+ }
46
+
47
+ function createTimeout() {
48
+ let timeout: number
49
+ return {
50
+ set(callback: () => void, time: number) {
51
+ clearTimeout(timeout)
52
+ timeout = setTimeout(callback, time)
53
+ },
54
+ clear() {
55
+ clearTimeout(timeout)
56
+ },
57
+ }
58
+ }
59
+
60
+ /**********************************************************************************/
61
+ /* */
62
+ /* Constants */
63
+ /* */
64
+ /**********************************************************************************/
65
+
66
+ const WIDTH = 10
67
+ const HEIGHT = 20
68
+
69
+ export const TETROMINO = {
70
+ I: {
71
+ dimensions: [4, 4] as const,
72
+ pixels: () => new Float32Array([0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]),
73
+ color: [0, 1, 1], // Cyan
74
+ },
75
+ O: {
76
+ dimensions: [2, 2] as const,
77
+ pixels: () => new Float32Array([2, 2, 2, 2]),
78
+ color: [1, 1, 0], // Yellow
79
+ },
80
+ T: {
81
+ dimensions: [3, 3] as const,
82
+ pixels: () => new Float32Array([0, 3, 0, 3, 3, 3, 0, 0, 0]),
83
+ color: [0.6, 0, 1], // Purple
84
+ },
85
+ S: {
86
+ dimensions: [3, 3] as const,
87
+ pixels: () => new Float32Array([0, 4, 4, 4, 4, 0, 0, 0, 0]),
88
+ color: [0, 1, 0],
89
+ },
90
+ Z: {
91
+ dimensions: [3, 3] as const,
92
+ pixels: () => new Float32Array([5, 5, 0, 0, 5, 5, 0, 0, 0]),
93
+ color: [1, 0, 0],
94
+ },
95
+ J: {
96
+ dimensions: [3, 3] as const,
97
+ pixels: () => new Float32Array([6, 0, 0, 6, 6, 6, 0, 0, 0]),
98
+ color: [0, 0, 1],
99
+ },
100
+ L: {
101
+ dimensions: [3, 3] as const,
102
+ pixels: () => new Float32Array([0, 0, 7, 7, 7, 7, 0, 0, 0]),
103
+ color: [1, 0.5, 0],
104
+ },
105
+ } as const
106
+
107
+ const TETROMINO_KIND = Object.keys(TETROMINO) as Array<keyof typeof TETROMINO>
108
+
109
+ /**********************************************************************************/
110
+ /* */
111
+ /* Create Tetromino */
112
+ /* */
113
+ /**********************************************************************************/
114
+
115
+ function getRandomTetrominoKind() {
116
+ return TETROMINO_KIND[Math.floor(Math.random() * TETROMINO_KIND.length)]!
117
+ }
118
+
119
+ function createTetromino(
120
+ gl: GL,
121
+ program: WebGLProgram,
122
+ schema: {
123
+ a_index: { kind: 'float'; instanced: true }
124
+ a_pixel: { kind: 'float'; instanced: true }
125
+ },
126
+ ) {
127
+ const { a_index, a_pixel } = attributeView(gl, program, schema)
128
+
129
+ let pixels: Float32Array = null!
130
+ let dimensions: [number, number] = null!
131
+ let offset: [number, number] = null!
132
+ let pixelCount: number = null!
133
+
134
+ function next(kind = getRandomTetrominoKind()) {
135
+ const template = TETROMINO[kind]
136
+ pixels = template.pixels()
137
+ pixelCount = template.dimensions[0] * template.dimensions[1]
138
+ offset = [Math.ceil((WIDTH - template.dimensions[0]) / 2), HEIGHT - template.dimensions[1]] as [
139
+ number,
140
+ number,
141
+ ]
142
+ dimensions = template.dimensions as [number, number]
143
+
144
+ a_index.set(new Float32Array(Array.from({ length: pixelCount }, (_, index) => index)))
145
+ a_pixel.set(pixels)
146
+ }
147
+
148
+ next()
149
+
150
+ return {
151
+ get dimensions() {
152
+ return dimensions
153
+ },
154
+ get pixels() {
155
+ return pixels
156
+ },
157
+ get offset() {
158
+ return offset
159
+ },
160
+ get pixelCount() {
161
+ return pixelCount
162
+ },
163
+ bindAttributes() {
164
+ a_index.bind()
165
+ a_pixel.bind()
166
+ },
167
+ next,
168
+ rotate(clockwise = true) {
169
+ rotate(pixels, dimensions[1], clockwise)
170
+ a_pixel.set(pixels).bind()
171
+ },
172
+ }
173
+ }
174
+
175
+ /**********************************************************************************/
176
+ /* */
177
+ /* Create Board */
178
+ /* */
179
+ /**********************************************************************************/
180
+
181
+ function createBoard(
182
+ gl: GL,
183
+ program: WebGLProgram,
184
+ schema: {
185
+ a_index: { kind: 'float'; instanced: true }
186
+ a_pixel: { kind: 'float'; instanced: true }
187
+ },
188
+ ) {
189
+ const { a_index, a_pixel } = attributeView(gl, program, schema)
190
+ const array = new Float32Array(Array.from({ length: WIDTH * HEIGHT }, () => 0))
191
+
192
+ a_index.set(new Float32Array(Array.from({ length: WIDTH * HEIGHT }, (_, index) => index)))
193
+ a_pixel.set(array)
194
+
195
+ return {
196
+ bindAttributes() {
197
+ a_index.bind()
198
+ a_pixel.bind()
199
+ },
200
+ array,
201
+ blit({ pixelCount, offset, dimensions, pixels }: ReturnType<typeof createTetromino>) {
202
+ for (let i = 0; i < pixelCount; i++) {
203
+ const pixel = pixels[i]!
204
+
205
+ if (pixel) {
206
+ const x = offset[0] + (i % dimensions[0])
207
+ const y = offset[1] + Math.floor(i / dimensions[0]) + 1
208
+ const index = x + y * WIDTH
209
+ array[index] = pixel
210
+ }
211
+ }
212
+
213
+ let writeRow = 0
214
+
215
+ for (let y = 0; y < HEIGHT; y++) {
216
+ let isComplete = true
217
+
218
+ for (let x = 0; x < WIDTH; x++) {
219
+ if (array[x + y * WIDTH] === 0) {
220
+ isComplete = false
221
+ break
222
+ }
223
+ }
224
+
225
+ if (!isComplete) {
226
+ if (writeRow !== y) {
227
+ for (let x = 0; x < WIDTH; x++) {
228
+ array[x + writeRow * WIDTH] = array[x + y * WIDTH]!
229
+ }
230
+ }
231
+ writeRow++
232
+ }
233
+ }
234
+
235
+ a_pixel.set(array)
236
+ },
237
+ }
238
+ }
239
+
240
+ /**********************************************************************************/
241
+ /* */
242
+ /* Create Tetris */
243
+ /* */
244
+ /**********************************************************************************/
245
+
246
+ const vertex = glsl`
247
+ precision mediump float;
248
+
249
+ ${attribute.float('a_index', { instanced: true })}
250
+ ${attribute.float('a_pixel', { instanced: true })}
251
+ ${attribute.vec2('a_vertex')}
252
+ ${uniform.vec2('u_offset')}
253
+ ${uniform.vec2('u_dimensions')}
254
+ ${uniform.vec3('u_palette', { size: 7 })}
255
+
256
+ varying vec3 v_color;
257
+
258
+ void main(){
259
+ float columns = mod(a_index, u_dimensions.x);
260
+ float rows = floor(a_index / u_dimensions.x);
261
+
262
+ vec2 offset = (vec2(columns, rows) + u_offset);
263
+
264
+ vec2 base = offset + 0.5;
265
+ vec2 pos = (a_vertex + base) / vec2(10., 20.);
266
+
267
+ gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
268
+
269
+ if(a_pixel == 0.){
270
+ v_color = vec3(0., 0., 0.);
271
+ }else{
272
+ v_color = u_palette[int(a_pixel) - 1];
273
+ }
274
+ }`
275
+
276
+ const fragment = glsl`
277
+ precision mediump float;
278
+
279
+ varying vec3 v_color;
280
+
281
+ void main() {
282
+ if(v_color.x + v_color.y + v_color.z == 0.){
283
+ discard;
284
+ }
285
+ gl_FragColor = vec4(v_color, 1.0);
286
+ }`
287
+
288
+ function createTetris() {
289
+ // Setup canvas
290
+ const canvas = document.createElement('canvas')
291
+ canvas.width = WIDTH * 30
292
+ canvas.height = HEIGHT * 30
293
+ document.body.append(canvas)
294
+
295
+ const animationTimeout = createTimeout()
296
+
297
+ // Setup gl
298
+ const gl = canvas.getContext('webgl2')!
299
+ gl.viewport(0, 0, canvas.width, canvas.height)
300
+
301
+ // Setup program + schema
302
+ const { program, schema } = compile(gl, vertex, fragment)
303
+ gl.useProgram(program)
304
+
305
+ // Setup views
306
+ const { u_dimensions, u_offset, u_palette } = uniformView(gl, program, schema.uniforms)
307
+ const { a_vertex, ...localSchema } = schema.attributes
308
+ const globalAttributes = attributeView(gl, program, { a_vertex })
309
+
310
+ // Setup board
311
+ const board = createBoard(gl, program, localSchema)
312
+
313
+ // Setup tetromino
314
+ const tetromino = createTetromino(gl, program, localSchema)
315
+
316
+ // Inititalize static uniforms/attributes
317
+ u_palette.set(new Float32Array(Object.values(TETROMINO).flatMap(({ color }) => color)))
318
+ globalAttributes.a_vertex
319
+ .set(
320
+ new Float32Array([
321
+ // triangle 1
322
+ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5,
323
+ // triangle 2
324
+ -0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
325
+ ]),
326
+ )
327
+ .bind()
328
+
329
+ function checkCollision() {
330
+ for (let i = 0; i < tetromino.pixelCount; i++) {
331
+ const x = (i % tetromino.dimensions[0]) + tetromino.offset[0]
332
+ const y = Math.floor(i / tetromino.dimensions[0]) + tetromino.offset[1]
333
+
334
+ let pixel = tetromino.pixels[i]!
335
+
336
+ if (pixel > 0) {
337
+ if (x < 0) return true
338
+ if (x >= WIDTH) return true
339
+ if (y < 0) return true
340
+
341
+ const boardPixel = board.array[x + y * WIDTH]
342
+
343
+ if (boardPixel !== 0) {
344
+ return true
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ function next() {
351
+ board.blit(tetromino)
352
+ tetromino.next()
353
+
354
+ if (checkCollision()) {
355
+ document.removeEventListener('keydown', handleKeyDown)
356
+ animationTimeout.clear()
357
+ } else {
358
+ render()
359
+ animationTimeout.set(animate, 1_000)
360
+ }
361
+ }
362
+
363
+ // Initialize event listeners
364
+ function handleKeyDown(event: KeyboardEvent) {
365
+ switch (event.key) {
366
+ case 'ArrowUp':
367
+ tetromino.rotate()
368
+ if (checkCollision()) {
369
+ tetromino.rotate(false)
370
+ } else {
371
+ render()
372
+ }
373
+ break
374
+ case 'ArrowDown':
375
+ tetromino.offset[1]--
376
+ if (checkCollision()) {
377
+ next()
378
+ } else {
379
+ render()
380
+ }
381
+ break
382
+ case 'ArrowLeft':
383
+ tetromino.offset[0]--
384
+ if (checkCollision()) {
385
+ tetromino.offset[0]++
386
+ } else {
387
+ render()
388
+ }
389
+ break
390
+ case 'ArrowRight':
391
+ tetromino.offset[0]++
392
+ if (checkCollision()) {
393
+ tetromino.offset[0]--
394
+ } else {
395
+ render()
396
+ }
397
+ break
398
+ case ' ':
399
+ while (true) {
400
+ tetromino.offset[1]--
401
+ if (checkCollision()) {
402
+ next()
403
+ break
404
+ }
405
+ }
406
+ }
407
+ }
408
+ document.addEventListener('keydown', handleKeyDown)
409
+
410
+ function render() {
411
+ gl.clearColor(0.0, 0.5, 0.0, 0.3)
412
+ gl.clear(gl.COLOR_BUFFER_BIT)
413
+
414
+ // Draw board
415
+ u_offset.set(0, 0)
416
+ u_dimensions.set(WIDTH, HEIGHT)
417
+ board.bindAttributes()
418
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, WIDTH * HEIGHT)
419
+
420
+ // Draw tetromino
421
+ u_offset.set(...tetromino.offset)
422
+ u_dimensions.set(...tetromino.dimensions)
423
+ tetromino.bindAttributes()
424
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, tetromino.pixelCount)
425
+ }
426
+
427
+ function animate() {
428
+ tetromino.offset[1]--
429
+ if (checkCollision()) {
430
+ next()
431
+ }
432
+ animationTimeout.set(animate, 1_000)
433
+ }
434
+ setTimeout(animate, 1_000)
435
+
436
+ render()
437
+ }
438
+
439
+ createTetris()
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "include": ["."]
4
+ }
package/dev/utils.ts ADDED
@@ -0,0 +1,61 @@
1
+ type CursorEvent = PointerEvent & {
2
+ distanceX: number
3
+ distanceY: number
4
+ deltaX: number
5
+ deltaY: number
6
+ }
7
+
8
+ export function cursor(event: PointerEvent, callback: (event: CursorEvent) => void) {
9
+ const { promise, resolve } = Promise.withResolvers<void>()
10
+ const controller = new AbortController()
11
+
12
+ const initialX = event.clientX
13
+ const initialY = event.clientY
14
+
15
+ let previousX = event.clientX
16
+ let previousY = event.clientY
17
+
18
+ window.addEventListener(
19
+ 'pointermove',
20
+ event => {
21
+ callback(
22
+ Object.assign(event, {
23
+ distanceX: initialX - event.clientX,
24
+ distanceY: initialY - event.clientY,
25
+ deltaX: previousX - event.clientX,
26
+ deltaY: previousY - event.clientY,
27
+ }),
28
+ )
29
+ previousX = event.clientX
30
+ previousY = event.clientY
31
+ },
32
+ controller,
33
+ )
34
+ window.addEventListener(
35
+ 'pointerup',
36
+ () => {
37
+ controller.abort()
38
+ resolve()
39
+ },
40
+ controller,
41
+ )
42
+
43
+ return promise
44
+ }
45
+
46
+ export function createElement<T extends keyof HTMLElementTagNameMap>(
47
+ tag: T,
48
+ {
49
+ parentElement,
50
+ ...options
51
+ }: Partial<Omit<HTMLElementTagNameMap[T], 'style'> & { style: string }> = {},
52
+ ) {
53
+ const element = document.createElement(tag)
54
+ for (const key in options) {
55
+ element[key] = options[key]
56
+ }
57
+
58
+ ;(parentElement ?? document.body).appendChild(element)
59
+
60
+ return element
61
+ }
@@ -0,0 +1,42 @@
1
+ import { defineConfig } from 'vite'
2
+ import tsconfigPaths from 'vite-tsconfig-paths'
3
+
4
+ export default defineConfig({
5
+ base: './',
6
+ assetsInclude: ['assets/**/*'],
7
+ plugins: [
8
+ tsconfigPaths(),
9
+ {
10
+ name: 'Reaplace env variables',
11
+ transform(code, id) {
12
+ if (id.includes('node_modules')) {
13
+ return code
14
+ }
15
+ return code
16
+ .replace(/process\.env\.SSR/g, 'false')
17
+ .replace(/process\.env\.DEV/g, 'true')
18
+ .replace(/process\.env\.PROD/g, 'false')
19
+ .replace(/process\.env\.NODE_ENV/g, '"development"')
20
+ .replace(/import\.meta\.env\.SSR/g, 'false')
21
+ .replace(/import\.meta\.env\.DEV/g, 'true')
22
+ .replace(/import\.meta\.env\.PROD/g, 'false')
23
+ .replace(/import\.meta\.env\.NODE_ENV/g, '"development"')
24
+ },
25
+ },
26
+ ],
27
+ optimizeDeps: {
28
+ esbuildOptions: {
29
+ define: {
30
+ global: 'globalThis',
31
+ },
32
+ plugins: [],
33
+ },
34
+ },
35
+ server: {
36
+ port: 3000,
37
+ },
38
+ build: {
39
+ target: 'esnext',
40
+ minify: false,
41
+ },
42
+ })
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@bigmistqke/view.gl",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./index.mjs",
8
+ "types": "./index.d.mts"
9
+ },
10
+ "./tag": {
11
+ "import": "./tag.mjs",
12
+ "types": "./tag.d.mts"
13
+ }
14
+ },
15
+ "keywords": [],
16
+ "author": "",
17
+ "license": "ISC",
18
+ "dependencies": {},
19
+ "devDependencies": {
20
+ "tsup": "^8.5.0",
21
+ "typescript": "^5.8.3",
22
+ "vite": "^7.0.2",
23
+ "vite-tsconfig-paths": "^5.1.4"
24
+ },
25
+ "scripts": {
26
+ "dev": "vite serve dev",
27
+ "build": "tsup"
28
+ }
29
+ }
@@ -0,0 +1,59 @@
1
+ import type { GL, RemoveSuffix } from './types'
2
+
3
+ const INSTANCED_ARRAYS_WRAPPER_MAP = new WeakMap<
4
+ GL,
5
+ RemoveSuffix<
6
+ Pick<
7
+ ANGLE_instanced_arrays,
8
+ 'drawArraysInstancedANGLE' | 'drawElementsInstancedANGLE' | 'vertexAttribDivisorANGLE'
9
+ >,
10
+ 'ANGLE'
11
+ > | null
12
+ >()
13
+ export function getInstancedArrays(gl: GL) {
14
+ if (gl instanceof WebGL2RenderingContext) return gl
15
+
16
+ const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl)
17
+ if (cached) return cached
18
+
19
+ const ext = gl.getExtension('ANGLE_instanced_arrays')
20
+ if (!ext) return undefined
21
+
22
+ const wrapper = {
23
+ drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext),
24
+ drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext),
25
+ vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext),
26
+ }
27
+ INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper)
28
+
29
+ return wrapper
30
+ }
31
+
32
+ const VERTEX_ARRAY_OBJECT_WRAPPER_MAP = new WeakMap<
33
+ GL,
34
+ RemoveSuffix<
35
+ Pick<
36
+ OES_vertex_array_object,
37
+ 'bindVertexArrayOES' | 'createVertexArrayOES' | 'deleteVertexArrayOES'
38
+ >,
39
+ 'OES'
40
+ > | null
41
+ >()
42
+ export function getVertexArrayObject(gl: GL) {
43
+ if (gl instanceof WebGL2RenderingContext) return gl
44
+
45
+ const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl)
46
+ if (cached) return cached
47
+
48
+ const ext = gl.getExtension('OES_vertex_array_object')
49
+ if (!ext) return null
50
+
51
+ const wrapper = {
52
+ bindVertexArray: ext.bindVertexArrayOES.bind(ext),
53
+ createVertexArray: ext.createVertexArrayOES.bind(ext),
54
+ deleteVertexArray: ext.deleteVertexArrayOES.bind(ext),
55
+ }
56
+ VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper)
57
+
58
+ return wrapper
59
+ }