@bigmistqke/view.gl 0.1.0 → 0.1.2

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.
Files changed (52) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.github/ISSUE_TEMPLATE/bug-report.yml +31 -0
  3. package/.github/ISSUE_TEMPLATE/feature-request.yml +27 -0
  4. package/.github/workflows/codeql.yml +46 -0
  5. package/.github/workflows/deploy-page.yml +38 -0
  6. package/.github/workflows/format.yml +40 -0
  7. package/README.md +92 -35
  8. package/dev/404.html +19 -0
  9. package/dev/dist/404.html +19 -0
  10. package/dev/dist/assets/bezier-DuLTb2d1.js +294 -0
  11. package/dev/dist/assets/game_of_life-D7qHdFcv.js +202 -0
  12. package/dev/dist/assets/grid-Ie22U_9K.js +173 -0
  13. package/dev/dist/assets/index-0qrXMlfv.js +109 -0
  14. package/dev/dist/assets/index-C2N3u8wj.css +12 -0
  15. package/dev/dist/assets/main-BZ1N6pp6.js +32 -0
  16. package/dev/dist/assets/overview-DAxtMIw5.js +78 -0
  17. package/dev/dist/assets/pix_sim-DIhzM_vu.js +501 -0
  18. package/dev/dist/assets/symbol-BouRL-Ni.js +65 -0
  19. package/dev/dist/assets/tag-C_bg62fD.js +516 -0
  20. package/dev/dist/assets/tetris-IMnkSi9p.js +344 -0
  21. package/dev/dist/assets/utils-CvCgsM08.js +50 -0
  22. package/dev/dist/index.html +51 -0
  23. package/dev/index.html +39 -10
  24. package/dev/{bezier.ts → src/examples/bezier.ts} +1 -1
  25. package/dev/{grid.ts → src/examples/grid.ts} +1 -1
  26. package/dev/{pix_sim.ts → src/examples/pix_sim.ts} +112 -97
  27. package/dev/src/examples/symbol.ts +79 -0
  28. package/dev/{tetris.ts → src/examples/tetris.ts} +1 -0
  29. package/dev/src/index.css +12 -0
  30. package/dev/src/index.ts +69 -0
  31. package/dev/{utils.ts → src/utils.ts} +8 -2
  32. package/dev/vite.config.ts +9 -1
  33. package/dist/index.d.mts +18 -0
  34. package/dist/index.mjs +324 -0
  35. package/dist/index.mjs.map +1 -0
  36. package/dist/tag.d.mts +97 -0
  37. package/dist/tag.mjs +162 -0
  38. package/dist/tag.mjs.map +1 -0
  39. package/dist/types-DoFAJoh-.d.mts +266 -0
  40. package/package.json +24 -10
  41. package/src/index.ts +39 -17
  42. package/src/tag.ts +47 -39
  43. package/src/types.ts +24 -24
  44. package/src/{utils.ts → utils/index.ts} +39 -14
  45. package/src/utils/object.ts +27 -0
  46. package/test/index.test.ts +1017 -0
  47. package/test/mocks/webgl.ts +248 -0
  48. package/test/setup.ts +13 -0
  49. package/test/tag.test.ts +790 -0
  50. package/vitest.config.ts +15 -0
  51. /package/dev/{game_of_life.ts → src/examples/game_of_life.ts} +0 -0
  52. /package/dev/{index.ts → src/examples/overview.ts} +0 -0
@@ -2,6 +2,7 @@ import { FramebufferOptions } from 'src/types'
2
2
  import { createFramebuffer } from 'src/utils'
3
3
  import { view } from 'view.gl'
4
4
  import { attribute, compile, glsl, uniform } from 'view.gl/tag'
5
+ import { createElement } from '../utils'
5
6
 
6
7
  export const MATERIALS = {
7
8
  SAND: {
@@ -21,126 +22,140 @@ export const MATERIALS = {
21
22
  const MATERIAL_SIZE = Object.keys(MATERIALS).length
22
23
 
23
24
  let playing = false
24
- let currentMaterial = MATERIALS.SAND.id
25
-
26
- const canvas = document.createElement('canvas')
27
- const gl = canvas.getContext('webgl2', { antialias: false })!
28
- if (!gl) {
29
- throw new Error('WebGL not supported')
30
- }
25
+ let currentMaterial: (typeof MATERIALS)[keyof typeof MATERIALS]['id'] = MATERIALS.SAND.id
31
26
 
32
27
  // Create UI container
33
- const container = document.createElement('div')
34
- container.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 10px;'
28
+ const container = createElement('div', {
29
+ style: 'display: flex; flex-direction: column; align-items: center; gap: 10px;',
30
+ parentElement: document.body,
31
+ })
35
32
 
36
33
  // Create material buttons
37
- const buttonContainer = document.createElement('div')
38
- buttonContainer.style.cssText = 'display: flex; gap: 10px; margin-bottom: 10px;'
34
+ const buttonContainer = createElement('div', {
35
+ style: 'display: flex; gap: 10px; margin-bottom: 10px;',
36
+ parentElement: container,
37
+ })
39
38
 
40
39
  Object.entries(MATERIALS).forEach(([name, material]) => {
41
- const button = document.createElement('button')
42
- button.textContent = name.toLowerCase()
43
- button.style.cssText = `
44
- padding: 5px 10px;
40
+ createElement('button', {
41
+ textContent: name.toLowerCase(),
42
+ 'data-name': name,
43
+ 'data-id': material.id.toString(),
44
+ style: `padding: 5px 10px;
45
45
  font-size: 14px;
46
- font-family: times new roman;
46
+ font-family: arial;
47
47
  cursor: pointer;
48
+ color: black;
48
49
  background: ${
49
50
  currentMaterial === material.id
50
51
  ? `rgb(${material.color.map(c => Math.floor(c * 255)).join(',')})`
51
- : 'transparent'
52
+ : `rgba(${material.color.map(c => Math.floor(c * 255)).join(',')}, 0.75)`
52
53
  };
53
54
  border-color: ${name === 'SAND' ? 'black' : 'white'};
54
55
  border: 2px solid rgb(${material.color.map(c => Math.floor(c * 255)).join(',')});
55
56
  border-radius: 2px;
56
- `
57
- button.onclick = () => {
58
- currentMaterial = material.id
59
- // Update all button borders
60
- buttonContainer.querySelectorAll('button').forEach((btn, idx) => {
61
- btn.style.background =
62
- idx === material.id - 1
63
- ? `rgb(${material.color.map(c => Math.floor(c * 255)).join(',')})`
64
- : 'transparent'
65
- })
66
- }
67
- buttonContainer.appendChild(button)
57
+ `,
58
+ onclick() {
59
+ currentMaterial = material.id
60
+ // Update all button borders
61
+ buttonContainer.querySelectorAll('button').forEach(btn => {
62
+ const name = btn.getAttribute('data-name') as keyof typeof MATERIALS
63
+ const id = btn.getAttribute('data-id')
64
+ if (id === currentMaterial.toString()) {
65
+ btn.style.background = `rgb(${MATERIALS[name].color
66
+ .map(c => Math.floor(c * 255))
67
+ .join(',')})`
68
+ } else {
69
+ btn.style.background = `rgba(${MATERIALS[name].color
70
+ .map(c => Math.floor(c * 255))
71
+ .join(',')}, 0.75)`
72
+ }
73
+ })
74
+ },
75
+ parentElement: buttonContainer,
76
+ })
68
77
  })
69
78
 
70
- container.appendChild(canvas)
71
- container.appendChild(buttonContainer)
72
- document.body.append(container)
73
-
74
79
  function spray() {
75
80
  return Math.floor(Math.random() * 8) * (Math.random() < 0.5 ? 1 : -1)
76
81
  }
77
82
 
78
- canvas.addEventListener('mousedown', event => {
79
- const rect = canvas.getBoundingClientRect()
80
- const controller = new AbortController()
81
- // playing = false
82
- window.addEventListener(
83
- 'mousemove',
84
- event => {
85
- const x = Math.floor((event.clientX - rect.left + spray()) * (canvas.width / rect.width))
86
- const y = Math.floor((rect.bottom - event.clientY + spray()) * (canvas.height / rect.height))
87
-
88
- const dimensions = [8, 8] as const
89
-
90
- // Encode selected material using bit encoding
91
- const data = new Uint8Array(
92
- (function* () {
93
- for (let i = 0; i < dimensions[0] * dimensions[1]; i++) {
94
- yield (currentMaterial & 1) * 255 // Red channel (bit 0)
95
- yield ((currentMaterial >> 1) & 1) * 255 // Green channel (bit 1)
96
- yield ((currentMaterial >> 2) & 1) * 255 // Blue channel (bit 2)
97
- yield 255 // Alpha channel
98
- }
99
- })(),
100
- )
101
-
102
- // Update both read and write textures
103
- gl.bindTexture(gl.TEXTURE_2D, read.texture)
104
- gl.texSubImage2D(
105
- gl.TEXTURE_2D,
106
- 0,
107
- Math.max(0, Math.min(WIDTH - dimensions[0], x)),
108
- Math.max(0, Math.min(HEIGHT - dimensions[1], y)),
109
- dimensions[0],
110
- dimensions[1],
111
- gl.RGBA,
112
- gl.UNSIGNED_BYTE,
113
- data,
114
- )
115
-
116
- gl.bindTexture(gl.TEXTURE_2D, write.texture)
117
- gl.texSubImage2D(
118
- gl.TEXTURE_2D,
119
- 0,
120
- Math.max(0, Math.min(WIDTH - dimensions[0], x)),
121
- Math.max(0, Math.min(HEIGHT - dimensions[1], y)),
122
- dimensions[0],
123
- dimensions[1],
124
- gl.RGBA,
125
- gl.UNSIGNED_BYTE,
126
- data,
127
- )
128
-
129
- render()
130
- },
131
- controller,
132
- )
133
- window.addEventListener(
134
- 'mouseup',
135
- () => {
136
- controller.abort()
137
- // playing = true
138
- // animate()
139
- },
140
- controller,
141
- )
83
+ const canvas = createElement('canvas', {
84
+ parentElement: container,
85
+ onmousedown() {
86
+ const rect = canvas.getBoundingClientRect()
87
+ const controller = new AbortController()
88
+ // playing = false
89
+ window.addEventListener(
90
+ 'mousemove',
91
+ event => {
92
+ const x = Math.floor((event.clientX - rect.left + spray()) * (canvas.width / rect.width))
93
+ const y = Math.floor(
94
+ (rect.bottom - event.clientY + spray()) * (canvas.height / rect.height),
95
+ )
96
+
97
+ const dimensions = [8, 8] as const
98
+
99
+ // Encode selected material using bit encoding
100
+ const data = new Uint8Array(
101
+ (function* () {
102
+ for (let i = 0; i < dimensions[0] * dimensions[1]; i++) {
103
+ yield (currentMaterial & 1) * 255 // Red channel (bit 0)
104
+ yield ((currentMaterial >> 1) & 1) * 255 // Green channel (bit 1)
105
+ yield ((currentMaterial >> 2) & 1) * 255 // Blue channel (bit 2)
106
+ yield 255 // Alpha channel
107
+ }
108
+ })(),
109
+ )
110
+
111
+ // Update both read and write textures
112
+ gl.bindTexture(gl.TEXTURE_2D, read.texture)
113
+ gl.texSubImage2D(
114
+ gl.TEXTURE_2D,
115
+ 0,
116
+ Math.max(0, Math.min(WIDTH - dimensions[0], x)),
117
+ Math.max(0, Math.min(HEIGHT - dimensions[1], y)),
118
+ dimensions[0],
119
+ dimensions[1],
120
+ gl.RGBA,
121
+ gl.UNSIGNED_BYTE,
122
+ data,
123
+ )
124
+
125
+ gl.bindTexture(gl.TEXTURE_2D, write.texture)
126
+ gl.texSubImage2D(
127
+ gl.TEXTURE_2D,
128
+ 0,
129
+ Math.max(0, Math.min(WIDTH - dimensions[0], x)),
130
+ Math.max(0, Math.min(HEIGHT - dimensions[1], y)),
131
+ dimensions[0],
132
+ dimensions[1],
133
+ gl.RGBA,
134
+ gl.UNSIGNED_BYTE,
135
+ data,
136
+ )
137
+
138
+ render()
139
+ },
140
+ controller,
141
+ )
142
+ window.addEventListener(
143
+ 'mouseup',
144
+ () => {
145
+ controller.abort()
146
+ // playing = true
147
+ // animate()
148
+ },
149
+ controller,
150
+ )
151
+ },
142
152
  })
143
153
 
154
+ const gl = canvas.getContext('webgl2', { antialias: false })!
155
+ if (!gl) {
156
+ throw new Error('WebGL not supported')
157
+ }
158
+
144
159
  canvas.width = 256 * 2
145
160
  canvas.height = 256 * 2
146
161
  const WIDTH = canvas.width
@@ -0,0 +1,79 @@
1
+ import { view } from 'view.gl'
2
+ import { attribute, compile, glsl, uniform } from 'view.gl/tag'
3
+ import { createElement } from '../utils'
4
+
5
+ const canvas = createElement('canvas', {
6
+ width: window.innerWidth,
7
+ height: window.innerHeight,
8
+ parentElement: document.body,
9
+ style: 'width: 100%; height: 100%',
10
+ })
11
+
12
+ new ResizeObserver(() => {
13
+ canvas.width = window.innerWidth
14
+ canvas.height = window.innerHeight
15
+ gl.viewport(0, 0, canvas.width, canvas.height)
16
+ draw(performance.now())
17
+ }).observe(canvas)
18
+
19
+ const gl = canvas.getContext('webgl2')!
20
+ gl.viewport(0, 0, canvas.width, canvas.height)
21
+
22
+ const vertex = glsl`
23
+ precision mediump float;
24
+
25
+ ${attribute.vec2('a_vertex')}
26
+ ${uniform.float('u_time')}
27
+
28
+ void main() {
29
+ float s = sin(u_time);
30
+ float c = cos(u_time);
31
+ mat2 rotation = mat2(
32
+ c, -s,
33
+ s, c
34
+ );
35
+ vec2 position = a_vertex * rotation;
36
+ gl_Position = vec4(position, 0.0, 1.0);
37
+ }`
38
+
39
+ const u_color = Symbol('color')
40
+
41
+ const fragment = glsl`
42
+ precision mediump float;
43
+
44
+ ${uniform.vec3(u_color)}
45
+
46
+ void main() {
47
+ gl_FragColor = vec4(${u_color}, 1.0);
48
+ }`
49
+
50
+ const { program, schema } = compile(gl, vertex, fragment)
51
+ const { attributes, uniforms } = view(gl, program, schema)
52
+
53
+ gl.useProgram(program)
54
+
55
+ uniforms[u_color].set(0, 255, 0)
56
+
57
+ // Create triangle vertex buffer
58
+ attributes.a_vertex
59
+ .set(
60
+ new Float32Array([
61
+ // top
62
+ 0.0, 1.0,
63
+ // bottom left
64
+ -1.0, -1.0,
65
+ // bottom right
66
+ 1.0, -1.0,
67
+ ]),
68
+ )
69
+ .bind()
70
+
71
+ function draw(delta: number) {
72
+ requestAnimationFrame(draw)
73
+
74
+ // Set uniform
75
+ uniforms.u_time.set(delta / 1_000)
76
+
77
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 100)
78
+ }
79
+ requestAnimationFrame(draw)
@@ -429,6 +429,7 @@ function createTetris() {
429
429
  if (checkCollision()) {
430
430
  next()
431
431
  }
432
+ render()
432
433
  animationTimeout.set(animate, 1_000)
433
434
  }
434
435
  setTimeout(animate, 1_000)
@@ -0,0 +1,12 @@
1
+ body {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ margin: 0px;
6
+ height: 100%;
7
+ overscroll-behavior-y: none;
8
+ }
9
+ canvas {
10
+ margin: auto;
11
+ image-rendering: pixelated;
12
+ }
@@ -0,0 +1,69 @@
1
+ import css from './index.css?url'
2
+ import { createElement } from './utils'
3
+
4
+ const iframe = document.querySelector('iframe')!
5
+ const nav = document.querySelector('nav')!
6
+
7
+ createElement('h3', { innerText: 'Examples', parentElement: nav })
8
+
9
+ const urls = Object.fromEntries(
10
+ Object.entries(import.meta.glob('./examples/*.ts', { eager: false })).map(([key, entry]) => {
11
+ const name = key.split('/').pop()!.replace('.ts', '')
12
+ const chunk = entry.toString().match(/import\(['"]([^'"]+)['"]/)![1]!
13
+ return [name, chunk] as const
14
+ }),
15
+ )
16
+
17
+ Object.entries(urls).forEach(([name, localUrl]) => {
18
+ createElement('button', {
19
+ onclick(event) {
20
+ event.preventDefault()
21
+ load(name, localUrl)
22
+ },
23
+ innerHTML: name,
24
+ parentElement: nav,
25
+ 'data-route': name,
26
+ })
27
+ })
28
+
29
+ function load(name: string, url: string) {
30
+ // Use hash routing instead of pushState for GitHub Pages compatibility
31
+ window.location.hash = name
32
+
33
+ URL.revokeObjectURL(iframe.src)
34
+
35
+ nav.querySelectorAll('button').forEach(button => {
36
+ if (button.getAttribute('data-route') === name) {
37
+ button.classList.add('active')
38
+ } else {
39
+ button.classList.remove('active')
40
+ }
41
+ })
42
+
43
+ iframe.src = URL.createObjectURL(
44
+ new Blob(
45
+ [
46
+ `<head>
47
+ <link rel="stylesheet" href="${new URL(css, import.meta.url).href}"></link>
48
+ <script type="module" src="${new URL(url, import.meta.url).href}"></script></head>`,
49
+ ],
50
+ {
51
+ type: 'text/html',
52
+ },
53
+ ),
54
+ )
55
+ }
56
+
57
+ // Handle hash-based routing
58
+ const hash = window.location.hash.slice(1) // Remove #
59
+ if (hash && hash in urls) {
60
+ load(hash, urls[hash]!)
61
+ }
62
+
63
+ // Listen for hash changes
64
+ window.addEventListener('hashchange', () => {
65
+ const hash = window.location.hash.slice(1)
66
+ if (hash && hash in urls) {
67
+ load(hash, urls[hash]!)
68
+ }
69
+ })
@@ -48,11 +48,17 @@ export function createElement<T extends keyof HTMLElementTagNameMap>(
48
48
  {
49
49
  parentElement,
50
50
  ...options
51
- }: Partial<Omit<HTMLElementTagNameMap[T], 'style'> & { style: string }> = {},
51
+ }: Partial<Omit<HTMLElementTagNameMap[T], 'style'> & { style: string }> &
52
+ Record<`data-${string}`, string> = {},
52
53
  ) {
53
54
  const element = document.createElement(tag)
55
+
54
56
  for (const key in options) {
55
- element[key] = options[key]
57
+ if (key.startsWith('data-')) {
58
+ element.setAttribute(key, options[key])
59
+ } else {
60
+ element[key] = options[key]
61
+ }
56
62
  }
57
63
 
58
64
  ;(parentElement ?? document.body).appendChild(element)
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
2
2
  import tsconfigPaths from 'vite-tsconfig-paths'
3
3
 
4
4
  export default defineConfig({
5
- base: './',
5
+ base: process.env.GITHUB_ACTIONS ? 'https://bigmistqke.github.io/view.gl/' : undefined,
6
6
  assetsInclude: ['assets/**/*'],
7
7
  plugins: [
8
8
  tsconfigPaths(),
@@ -38,5 +38,13 @@ export default defineConfig({
38
38
  build: {
39
39
  target: 'esnext',
40
40
  minify: false,
41
+ rollupOptions: {
42
+ input: {
43
+ main: './dev/index.html',
44
+ index: './dev/src/index.ts', // Separate entry for index.ts
45
+ utils: './dev/src/utils.ts', // Separate entry for utils.ts
46
+ 404: './dev/404.html',
47
+ },
48
+ },
41
49
  },
42
50
  })
@@ -0,0 +1,18 @@
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-DoFAJoh-.mjs';
2
+
3
+ declare function toID(key: string | number | symbol): any;
4
+ /**********************************************************************************/
5
+ /**********************************************************************************/
6
+ declare function view<T extends ViewSchema>(gl: GL, program: WebGLProgram, schema: T, options?: ViewOptions): View<T>;
7
+ /**********************************************************************************/
8
+ /**********************************************************************************/
9
+ declare function uniformView<T extends UniformSchema>(gl: GL, program: WebGLProgram, schema: T): UniformView<T>;
10
+ declare function attributeView<T extends AttributeSchema>(gl: GL, program: WebGLProgram, schema: T, { signal }?: ViewOptions): AttributeView<T>;
11
+ /**********************************************************************************/
12
+ /**********************************************************************************/
13
+ declare function interleavedAttributeView<T extends InterleavedAttributeSchema>(gl: GL, program: WebGLProgram, schema: T, { signal }?: ViewOptions): InterleavedAttributeView<T>;
14
+ /**********************************************************************************/
15
+ /**********************************************************************************/
16
+ declare function bufferView<T extends BufferSchema>(gl: GL, schema: T, { signal }?: ViewOptions): BufferView<T>;
17
+
18
+ export { attributeView, bufferView, interleavedAttributeView, toID, uniformView, view };