lgimgui 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.
@@ -0,0 +1,291 @@
1
+ import { normalizeMaterial } from './params.js'
2
+
3
+ function tint(r, g, b, a) {
4
+ return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255), a }
5
+ }
6
+
7
+ export const ROLE_PRESETS = {
8
+ window: normalizeMaterial({
9
+ cornerRadius: 36,
10
+ refThickness: 10,
11
+ refFactor: 1.38,
12
+ refDispersion: 7.2,
13
+ refFresnelRange: 21,
14
+ refFresnelHardness: 14,
15
+ refFresnelFactor: 15,
16
+ glareRange: 16,
17
+ glareHardness: 27,
18
+ glareFactor: 93,
19
+ glareConvergence: 82,
20
+ glareOppositeFactor: 100,
21
+ glareAngle: -180,
22
+ blurRadius: 92,
23
+ blurEdge: false,
24
+ tint: tint(1, 1, 1, 1),
25
+ shadowExpand: 12,
26
+ shadowFactor: 26,
27
+ shadowPosition: { x: 0, y: 3 },
28
+ borderWidth: 1,
29
+ borderColor: { r: 255, g: 255, b: 255, a: 0.45 },
30
+ adaptTint: 0,
31
+ adaptShadow: 0,
32
+ }),
33
+ dock: normalizeMaterial({
34
+ cornerRadius: 36,
35
+ refThickness: 7,
36
+ refFactor: 2.77,
37
+ refDispersion: 14.22,
38
+ refFresnelRange: 19,
39
+ refFresnelHardness: 47,
40
+ refFresnelFactor: 43,
41
+ glareRange: 18,
42
+ glareHardness: 43,
43
+ glareFactor: 34,
44
+ glareConvergence: 30,
45
+ glareOppositeFactor: 57,
46
+ glareAngle: 45,
47
+ blurRadius: 48,
48
+ blurEdge: true,
49
+ tint: tint(0.93, 0.95, 0.99, 0.36),
50
+ shadowExpand: 18,
51
+ shadowFactor: 4,
52
+ shadowPosition: { x: 0, y: -3 },
53
+ }),
54
+ clear: normalizeMaterial({
55
+ cornerRadius: 14,
56
+ refThickness: 10,
57
+ refFactor: 1.41,
58
+ refDispersion: 20.82,
59
+ refFresnelRange: 22,
60
+ refFresnelHardness: 16,
61
+ refFresnelFactor: 47,
62
+ glareRange: 35,
63
+ glareHardness: 5,
64
+ glareFactor: 47,
65
+ glareConvergence: 38,
66
+ glareOppositeFactor: 61,
67
+ glareAngle: 45,
68
+ blurRadius: 15,
69
+ blurEdge: false,
70
+ tint: tint(0.86, 0.9, 0.98, 0.22),
71
+ shadowExpand: 15,
72
+ shadowFactor: 4,
73
+ shadowPosition: { x: 0, y: -3 },
74
+ }),
75
+ thumb: normalizeMaterial({
76
+ cornerRadius: 60,
77
+ refThickness: 13,
78
+ refFactor: 1.9,
79
+ refDispersion: 32,
80
+ refFresnelRange: 30,
81
+ refFresnelHardness: 12,
82
+ refFresnelFactor: 38,
83
+ glareRange: 34,
84
+ glareHardness: 10,
85
+ glareFactor: 70,
86
+ glareConvergence: 55,
87
+ glareOppositeFactor: 75,
88
+ glareAngle: -45,
89
+ blurRadius: 4,
90
+ blurEdge: false,
91
+ tint: tint(1, 1, 1, 0.06),
92
+ shadowExpand: 14,
93
+ shadowFactor: 8,
94
+ shadowPosition: { x: 0, y: -3 },
95
+ }),
96
+ control: normalizeMaterial({
97
+ cornerRadius: 60,
98
+ refThickness: 13,
99
+ refFactor: 1.5,
100
+ refDispersion: 7,
101
+ refFresnelRange: 38,
102
+ refFresnelHardness: 11,
103
+ refFresnelFactor: 21,
104
+ glareRange: 35,
105
+ glareHardness: 15,
106
+ glareFactor: 43,
107
+ glareConvergence: 50,
108
+ glareOppositeFactor: 80,
109
+ glareAngle: -45,
110
+ blurRadius: 14,
111
+ blurEdge: false,
112
+ tint: tint(0.9, 0.9, 0.88, 0.5),
113
+ shadowExpand: 18,
114
+ shadowFactor: 14,
115
+ shadowPosition: { x: 0, y: -4 },
116
+ }),
117
+ accent: normalizeMaterial({
118
+ cornerRadius: 60,
119
+ refThickness: 12,
120
+ refFactor: 1.5,
121
+ refDispersion: 5,
122
+ refFresnelRange: 30,
123
+ refFresnelHardness: 12,
124
+ refFresnelFactor: 30,
125
+ glareRange: 30,
126
+ glareHardness: 12,
127
+ glareFactor: 60,
128
+ glareConvergence: 50,
129
+ glareOppositeFactor: 70,
130
+ glareAngle: -45,
131
+ blurRadius: 20,
132
+ blurEdge: false,
133
+ tint: tint(0.17, 0.44, 1, 0.75),
134
+ shadowExpand: 16,
135
+ shadowFactor: 10,
136
+ shadowPosition: { x: 0, y: -4 },
137
+ }),
138
+ toolbar: normalizeMaterial({
139
+ cornerRadius: 60,
140
+ refThickness: 5,
141
+ refFactor: 1.12,
142
+ refDispersion: 0,
143
+ refFresnelRange: 18,
144
+ refFresnelHardness: 12,
145
+ refFresnelFactor: 10,
146
+ glareRange: 20,
147
+ glareHardness: 10,
148
+ glareFactor: 12,
149
+ glareConvergence: 50,
150
+ glareOppositeFactor: 50,
151
+ glareAngle: -45,
152
+ blurRadius: 60,
153
+ blurEdge: true,
154
+ tint: tint(1, 1, 1, 0.78),
155
+ shadowExpand: 16,
156
+ shadowFactor: 9,
157
+ shadowPosition: { x: 0, y: -3 },
158
+ }),
159
+ menu: normalizeMaterial({
160
+ cornerRadius: 16,
161
+ refThickness: 8,
162
+ refFactor: 1.3,
163
+ refDispersion: 4,
164
+ refFresnelRange: 30,
165
+ refFresnelHardness: 12,
166
+ refFresnelFactor: 18,
167
+ glareRange: 30,
168
+ glareHardness: 15,
169
+ glareFactor: 30,
170
+ glareConvergence: 50,
171
+ glareOppositeFactor: 70,
172
+ glareAngle: -45,
173
+ blurRadius: 60,
174
+ blurEdge: true,
175
+ tint: tint(0.96, 0.96, 0.97, 0.62),
176
+ shadowExpand: 24,
177
+ shadowFactor: 16,
178
+ shadowPosition: { x: 0, y: -6 },
179
+ borderWidth: 1,
180
+ borderColor: { r: 255, g: 255, b: 255, a: 0.5 },
181
+ }),
182
+ pane: normalizeMaterial({
183
+ cornerRadius: 36,
184
+ refThickness: 4,
185
+ refFactor: 1.08,
186
+ refDispersion: 0,
187
+ refFresnelRange: 20,
188
+ refFresnelHardness: 10,
189
+ refFresnelFactor: 8,
190
+ glareRange: 0,
191
+ glareHardness: 0,
192
+ glareFactor: 0,
193
+ glareConvergence: 0,
194
+ glareOppositeFactor: 0,
195
+ glareAngle: 0,
196
+ blurRadius: 90,
197
+ blurEdge: true,
198
+ tint: tint(1, 1, 1, 0.24),
199
+ shadowExpand: 14,
200
+ shadowFactor: 5,
201
+ shadowPosition: { x: 0, y: -3 },
202
+ }),
203
+ thin: normalizeMaterial({
204
+ cornerRadius: 14,
205
+ refThickness: 1,
206
+ refFactor: 1,
207
+ refDispersion: 0,
208
+ refFresnelRange: 0,
209
+ refFresnelHardness: 0,
210
+ refFresnelFactor: 0,
211
+ glareRange: 0,
212
+ glareHardness: 0,
213
+ glareFactor: 0,
214
+ glareConvergence: 0,
215
+ glareOppositeFactor: 0,
216
+ glareAngle: 0,
217
+ blurRadius: 102,
218
+ blurEdge: true,
219
+ tint: tint(1, 1, 1, 0.18),
220
+ shadowExpand: 12,
221
+ shadowFactor: 4,
222
+ shadowPosition: { x: 0, y: -3 },
223
+ }),
224
+ studio: normalizeMaterial({ cornerRadius: 40 }),
225
+ }
226
+
227
+ export const ROLES = Object.keys(ROLE_PRESETS)
228
+
229
+ export const ROLE_SURFACE = {
230
+ window: 'large',
231
+ dock: 'small',
232
+ clear: 'small',
233
+ thumb: 'small',
234
+ control: 'small',
235
+ accent: 'small',
236
+ toolbar: 'small',
237
+ menu: 'large',
238
+ pane: 'large',
239
+ thin: 'small',
240
+ studio: 'small',
241
+ }
242
+
243
+ export function clonePresets(presets = ROLE_PRESETS) {
244
+ return structuredClone(presets)
245
+ }
246
+
247
+ export const MATERIALS_VERSION = 1
248
+
249
+ const VARIANTS = ['unfocused', 'dark']
250
+
251
+ export function serializeMaterials(materials) {
252
+ const roles = {}
253
+ for (const role of Object.keys(materials)) {
254
+ if (VARIANTS.includes(role)) continue
255
+ roles[role] = structuredClone(materials[role])
256
+ }
257
+ const out = { version: MATERIALS_VERSION, roles }
258
+ for (const variant of VARIANTS) {
259
+ out[variant] = {}
260
+ for (const role of Object.keys(materials[variant] ?? {})) out[variant][role] = structuredClone(materials[variant][role])
261
+ }
262
+ return out
263
+ }
264
+
265
+ export function parseMaterials(data) {
266
+ const source = typeof data === 'string' ? JSON.parse(data) : data
267
+ if (!source || typeof source !== 'object') throw new Error('materials file is not an object')
268
+ const roles = source.roles ?? source
269
+ const result = {}
270
+ for (const [role, material] of Object.entries(roles)) {
271
+ if (VARIANTS.includes(role) || role === 'version') continue
272
+ result[role] = normalizeMaterial(material, ROLE_PRESETS[role])
273
+ }
274
+ for (const variant of VARIANTS) {
275
+ if (!source[variant]) continue
276
+ result[variant] = {}
277
+ for (const [role, material] of Object.entries(source[variant])) result[variant][role] = normalizeMaterial(material, ROLE_PRESETS[role])
278
+ }
279
+ return result
280
+ }
281
+
282
+ export function assignMaterials(target, parsed) {
283
+ for (const key of Object.keys(target)) delete target[key]
284
+ for (const preset of Object.keys(ROLE_PRESETS)) target[preset] = structuredClone(ROLE_PRESETS[preset])
285
+ for (const [role, material] of Object.entries(parsed)) {
286
+ if (VARIANTS.includes(role)) continue
287
+ target[role] = material
288
+ }
289
+ for (const variant of VARIANTS) if (parsed[variant]) target[variant] = parsed[variant]
290
+ return target
291
+ }
@@ -0,0 +1,236 @@
1
+ import { compileProgram, setUniforms } from './context.js'
2
+ import {
3
+ uiVertex,
4
+ uiFragment,
5
+ MODE_FILL,
6
+ MODE_IMAGE,
7
+ MODE_GLYPH,
8
+ MODE_STROKE,
9
+ MODE_VIBRANT_GLYPH,
10
+ MODE_GRADIENT,
11
+ MODE_TINTED_FILL,
12
+ } from './shaders/ui.js'
13
+
14
+ const FLOATS_PER_INSTANCE = 32
15
+ const ATTRIBUTES = 8
16
+ const NO_CLIP = [0, 0, 0, 0]
17
+ const NO_FADE = [0, 0, 0, 0]
18
+
19
+ export function createBatch(gl, atlas) {
20
+ const program = compileProgram(gl, uiVertex, uiFragment, 'ui')
21
+ const vao = gl.createVertexArray()
22
+ const buffer = gl.createBuffer()
23
+ let capacity = 1024
24
+ let data = new Float32Array(capacity * FLOATS_PER_INSTANCE)
25
+ let count = 0
26
+ let mode = -1
27
+ let texture = null
28
+ let frameContext = null
29
+
30
+ gl.bindVertexArray(vao)
31
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
32
+ gl.bufferData(gl.ARRAY_BUFFER, data.byteLength, gl.DYNAMIC_DRAW)
33
+ for (let i = 0; i < ATTRIBUTES; i++) {
34
+ gl.enableVertexAttribArray(i)
35
+ gl.vertexAttribPointer(i, 4, gl.FLOAT, false, FLOATS_PER_INSTANCE * 4, i * 16)
36
+ gl.vertexAttribDivisor(i, 1)
37
+ }
38
+ gl.bindVertexArray(null)
39
+
40
+ function grow() {
41
+ capacity *= 2
42
+ const next = new Float32Array(capacity * FLOATS_PER_INSTANCE)
43
+ next.set(data)
44
+ data = next
45
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
46
+ gl.bufferData(gl.ARRAY_BUFFER, data.byteLength, gl.DYNAMIC_DRAW)
47
+ }
48
+
49
+ function begin(context) {
50
+ frameContext = context
51
+ count = 0
52
+ mode = -1
53
+ texture = null
54
+ }
55
+
56
+ function flush() {
57
+ if (count === 0) return
58
+ gl.useProgram(program.program)
59
+ gl.bindVertexArray(vao)
60
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
61
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, data.subarray(0, count * FLOATS_PER_INSTANCE))
62
+ if (mode === MODE_GLYPH || mode === MODE_VIBRANT_GLYPH) atlas.upload()
63
+ gl.activeTexture(gl.TEXTURE0)
64
+ gl.bindTexture(gl.TEXTURE_2D, texture ?? atlas.texture)
65
+ gl.activeTexture(gl.TEXTURE1)
66
+ gl.bindTexture(gl.TEXTURE_2D, frameContext.sceneTexture)
67
+ setUniforms(gl, program, {
68
+ u_viewport: [frameContext.width, frameContext.height],
69
+ u_resolution: [frameContext.width * frameContext.dpr, frameContext.height * frameContext.dpr],
70
+ u_dpr: frameContext.dpr,
71
+ u_mode: mode,
72
+ u_texture: 0,
73
+ u_scene: 1,
74
+ })
75
+ gl.enable(gl.BLEND)
76
+ gl.blendEquation(gl.FUNC_ADD)
77
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
78
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count)
79
+ gl.bindVertexArray(null)
80
+ count = 0
81
+ }
82
+
83
+ function push(nextMode, nextTexture, values) {
84
+ if (nextMode !== mode || nextTexture !== texture) {
85
+ flush()
86
+ mode = nextMode
87
+ texture = nextTexture
88
+ }
89
+ if (count >= capacity) {
90
+ flush()
91
+ grow()
92
+ }
93
+ data.set(values, count * FLOATS_PER_INSTANCE)
94
+ count += 1
95
+ }
96
+
97
+ function clipOf(op) {
98
+ const clip = op.clip
99
+ return clip ? [clip.x, clip.y, clip.w, clip.h] : NO_CLIP
100
+ }
101
+
102
+ function fadeOf(op) {
103
+ const fade = op.fade
104
+ return fade ? [fade.topStart, fade.topEnd, fade.bottomStart, fade.bottomEnd] : NO_FADE
105
+ }
106
+
107
+ function rect(op) {
108
+ const r = op.rect
109
+ const c = op.color
110
+ const rotation = op.rotation ?? 0
111
+ const expand = rotation ? (r.w + r.h) / 2 : 0
112
+ push(MODE_FILL, null, [
113
+ r.x, r.y, r.w, r.h,
114
+ op.radius ?? 0, op.roundness ?? 5, 0, 0,
115
+ c[0], c[1], c[2], c[3],
116
+ 0, 0, 1, 1,
117
+ ...clipOf(op),
118
+ 0, 0, rotation, expand,
119
+ 0, 0, 0, 0,
120
+ ...fadeOf(op),
121
+ ])
122
+ }
123
+
124
+ function tintedFill(op) {
125
+ const r = op.rect
126
+ const c = op.color
127
+ push(MODE_TINTED_FILL, null, [
128
+ r.x, r.y, r.w, r.h,
129
+ op.radius ?? 0, op.roundness ?? 5, 0, 0,
130
+ c[0], c[1], c[2], c[3],
131
+ 0, 0, 1, 1,
132
+ ...clipOf(op),
133
+ op.tone ?? 0.5, 0, 0, 0,
134
+ 0, 0, 0, 0,
135
+ ...fadeOf(op),
136
+ ])
137
+ }
138
+
139
+ function gradient(op) {
140
+ const r = op.rect
141
+ const c = op.color
142
+ const d = op.color2
143
+ push(MODE_GRADIENT, null, [
144
+ r.x, r.y, r.w, r.h,
145
+ op.radius ?? 0, op.roundness ?? 5, 0, 0,
146
+ c[0], c[1], c[2], c[3],
147
+ 0, 0, 1, 1,
148
+ ...clipOf(op),
149
+ 0, op.horizontal ? 1 : 0, 0, 0,
150
+ d[0], d[1], d[2], d[3],
151
+ ...fadeOf(op),
152
+ ])
153
+ }
154
+
155
+ function stroke(op) {
156
+ const r = op.rect
157
+ const c = op.color
158
+ push(MODE_STROKE, null, [
159
+ r.x, r.y, r.w, r.h,
160
+ op.radius ?? 0, op.roundness ?? 5, 0, 0,
161
+ c[0], c[1], c[2], c[3],
162
+ 0, 0, 1, 1,
163
+ ...clipOf(op),
164
+ op.width ?? 1, 0, 0, 1,
165
+ 0, 0, 0, 0,
166
+ ...fadeOf(op),
167
+ ])
168
+ }
169
+
170
+ function image(op) {
171
+ const r = op.rect
172
+ const c = op.color ?? [1, 1, 1, 1]
173
+ const uv = op.uv ?? [0, 0, 1, 1]
174
+ push(MODE_IMAGE, op.image.texture, [
175
+ r.x, r.y, r.w, r.h,
176
+ op.radius ?? 0, op.roundness ?? 5, 0, 0,
177
+ c[0], c[1], c[2], c[3],
178
+ uv[0], uv[1], uv[2], uv[3],
179
+ ...clipOf(op),
180
+ 0, 0, 0, 0,
181
+ 0, 0, 0, 0,
182
+ ...fadeOf(op),
183
+ ])
184
+ }
185
+
186
+ function text(op) {
187
+ const dpr = frameContext.dpr
188
+ const scale = dpr
189
+ const layout = atlas.layoutText(op.text, op.font, scale)
190
+ const c = op.color
191
+ const clip = clipOf(op)
192
+ const fade = fadeOf(op)
193
+ const glyphMode = op.vibrancy > 0 ? MODE_VIBRANT_GLYPH : MODE_GLYPH
194
+ const k = op.scale ?? 1
195
+ const metrics = atlas.metrics(op.font)
196
+ const lineBox = op.font.size * (op.font.lineHeight ?? 1.25) * k
197
+ const baseline = op.y + (lineBox - (metrics.ascent + metrics.descent) * k) / 2 + metrics.ascent * k
198
+ const snap = (value) => (k === 1 ? Math.round(value * dpr) / dpr : value)
199
+ for (const item of layout.items) {
200
+ const g = item.glyph
201
+ const x = snap(op.x + (item.x + g.offsetX) * k)
202
+ const y = snap(baseline + g.offsetY * k)
203
+ push(glyphMode, null, [
204
+ x, y, g.w * k, g.h * k,
205
+ 0, 2, 0, 0,
206
+ c[0], c[1], c[2], c[3],
207
+ g.u0, g.v0, g.u1, g.v1,
208
+ ...clip,
209
+ op.vibrancy ?? 0, 0, 0, 0,
210
+ 0, 0, 0, 0,
211
+ ...fade,
212
+ ])
213
+ }
214
+ }
215
+
216
+ function draw(op) {
217
+ switch (op.type) {
218
+ case 'rect':
219
+ return rect(op)
220
+ case 'gradient':
221
+ return gradient(op)
222
+ case 'tintedFill':
223
+ return tintedFill(op)
224
+ case 'stroke':
225
+ return stroke(op)
226
+ case 'image':
227
+ return image(op)
228
+ case 'text':
229
+ return text(op)
230
+ default:
231
+ throw new Error(`unknown ui op ${op.type}`)
232
+ }
233
+ }
234
+
235
+ return { begin, draw, flush }
236
+ }
@@ -0,0 +1,138 @@
1
+ export function createContext(canvas) {
2
+ const gl = canvas.getContext('webgl2', {
3
+ alpha: false,
4
+ antialias: false,
5
+ depth: false,
6
+ stencil: false,
7
+ premultipliedAlpha: true,
8
+ preserveDrawingBuffer: false,
9
+ powerPreference: 'high-performance',
10
+ })
11
+ if (!gl) throw new Error('webgl2 is required')
12
+ return gl
13
+ }
14
+
15
+ export function compileProgram(gl, vertexSource, fragmentSource, name) {
16
+ const program = gl.createProgram()
17
+ gl.attachShader(program, compileShader(gl, gl.VERTEX_SHADER, vertexSource, name + '.vert'))
18
+ gl.attachShader(program, compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource, name + '.frag'))
19
+ gl.linkProgram(program)
20
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
21
+ throw new Error(`link ${name}: ${gl.getProgramInfoLog(program)}`)
22
+ }
23
+ const uniforms = {}
24
+ const count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS)
25
+ for (let i = 0; i < count; i++) {
26
+ const info = gl.getActiveUniform(program, i)
27
+ const key = info.name.replace(/\[0\]$/, '')
28
+ uniforms[key] = { location: gl.getUniformLocation(program, info.name), type: info.type, size: info.size }
29
+ }
30
+ return { program, uniforms }
31
+ }
32
+
33
+ function compileShader(gl, type, source, name) {
34
+ const shader = gl.createShader(type)
35
+ gl.shaderSource(shader, source)
36
+ gl.compileShader(shader)
37
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
38
+ const log = gl.getShaderInfoLog(shader)
39
+ const lines = source.split('\n').map((line, i) => `${i + 1}: ${line}`)
40
+ throw new Error(`compile ${name}: ${log}\n${lines.join('\n')}`)
41
+ }
42
+ return shader
43
+ }
44
+
45
+ export function setUniforms(gl, program, values) {
46
+ for (const [key, value] of Object.entries(values)) {
47
+ const uniform = program.uniforms[key]
48
+ if (!uniform) continue
49
+ applyUniform(gl, uniform, value)
50
+ }
51
+ }
52
+
53
+ function applyUniform(gl, uniform, value) {
54
+ const { location, type } = uniform
55
+ switch (type) {
56
+ case gl.FLOAT:
57
+ if (Array.isArray(value) || ArrayBuffer.isView(value)) gl.uniform1fv(location, value)
58
+ else gl.uniform1f(location, value)
59
+ break
60
+ case gl.FLOAT_VEC2:
61
+ gl.uniform2fv(location, value)
62
+ break
63
+ case gl.FLOAT_VEC3:
64
+ gl.uniform3fv(location, value)
65
+ break
66
+ case gl.FLOAT_VEC4:
67
+ gl.uniform4fv(location, value)
68
+ break
69
+ case gl.INT:
70
+ case gl.SAMPLER_2D:
71
+ case gl.BOOL:
72
+ gl.uniform1i(location, value)
73
+ break
74
+ case gl.INT_VEC2:
75
+ gl.uniform2iv(location, value)
76
+ break
77
+ default:
78
+ throw new Error(`unsupported uniform type ${type}`)
79
+ }
80
+ }
81
+
82
+ export function createSurface(gl, width, height, levels) {
83
+ const texture = gl.createTexture()
84
+ gl.bindTexture(gl.TEXTURE_2D, texture)
85
+ gl.texStorage2D(gl.TEXTURE_2D, levels, gl.RGBA8, width, height)
86
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
87
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
88
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
89
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
90
+ const fbos = []
91
+ const sizes = []
92
+ for (let level = 0; level < levels; level++) {
93
+ const fbo = gl.createFramebuffer()
94
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo)
95
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, level)
96
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER)
97
+ if (status !== gl.FRAMEBUFFER_COMPLETE) throw new Error(`framebuffer incomplete at level ${level}: ${status}`)
98
+ fbos.push(fbo)
99
+ sizes.push({ w: Math.max(1, width >> level), h: Math.max(1, height >> level) })
100
+ }
101
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
102
+ return { texture, width, height, levels, fbos, sizes }
103
+ }
104
+
105
+ export function destroySurface(gl, surface) {
106
+ for (const fbo of surface.fbos) gl.deleteFramebuffer(fbo)
107
+ gl.deleteTexture(surface.texture)
108
+ }
109
+
110
+ export function createImageTexture(gl, source, options = {}) {
111
+ const texture = gl.createTexture()
112
+ gl.bindTexture(gl.TEXTURE_2D, texture)
113
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true)
114
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)
115
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source)
116
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false)
117
+ const mipmap = options.mipmap ?? true
118
+ if (mipmap) gl.generateMipmap(gl.TEXTURE_2D)
119
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mipmap ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR)
120
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
121
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
122
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
123
+ return { texture, width: source.width ?? source.videoWidth, height: source.height ?? source.videoHeight, mipmap }
124
+ }
125
+
126
+ export function updateImageTexture(gl, image, source) {
127
+ gl.bindTexture(gl.TEXTURE_2D, image.texture)
128
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true)
129
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source)
130
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false)
131
+ if (image.mipmap) gl.generateMipmap(gl.TEXTURE_2D)
132
+ image.width = source.width ?? source.videoWidth
133
+ image.height = source.height ?? source.videoHeight
134
+ }
135
+
136
+ export function mipLevelsFor(width, height) {
137
+ return Math.max(1, Math.min(8, Math.floor(Math.log2(Math.max(width, height))) - 1))
138
+ }