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,177 @@
1
+ const ATLAS_SIZE = 2048
2
+ const PADDING = 2
3
+
4
+ function makeCanvas(size) {
5
+ if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(size, size)
6
+ const canvas = document.createElement('canvas')
7
+ canvas.width = size
8
+ canvas.height = size
9
+ return canvas
10
+ }
11
+
12
+ const segmenter = typeof Intl !== 'undefined' && Intl.Segmenter ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : null
13
+
14
+ export function graphemes(text) {
15
+ if (!segmenter) return Array.from(text)
16
+ const out = []
17
+ for (const part of segmenter.segment(text)) out.push(part.segment)
18
+ return out
19
+ }
20
+
21
+ export function fontString(font) {
22
+ const weight = font.weight ?? 400
23
+ const style = font.italic ? 'italic ' : ''
24
+ return `${style}${weight} ${font.size}px ${font.family}`
25
+ }
26
+
27
+ export function createGlyphAtlas(gl) {
28
+ const canvas = makeCanvas(ATLAS_SIZE)
29
+ const context = canvas.getContext('2d', { willReadFrequently: false })
30
+ const measureCanvas = makeCanvas(8)
31
+ const measureContext = measureCanvas.getContext('2d')
32
+ const glyphs = new Map()
33
+ const widths = new Map()
34
+ const fontMetrics = new Map()
35
+ let shelfX = PADDING
36
+ let shelfY = PADDING
37
+ let shelfHeight = 0
38
+ let dirty = false
39
+ let generation = 0
40
+
41
+ const texture = gl.createTexture()
42
+ gl.bindTexture(gl.TEXTURE_2D, texture)
43
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ATLAS_SIZE, ATLAS_SIZE, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
44
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
45
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
46
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
47
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
48
+
49
+ function reset() {
50
+ glyphs.clear()
51
+ context.clearRect(0, 0, ATLAS_SIZE, ATLAS_SIZE)
52
+ shelfX = PADDING
53
+ shelfY = PADDING
54
+ shelfHeight = 0
55
+ generation += 1
56
+ dirty = true
57
+ }
58
+
59
+ function advance(fontKey, font, grapheme) {
60
+ const key = fontKey + '' + grapheme
61
+ let width = widths.get(key)
62
+ if (width == null) {
63
+ measureContext.font = fontKey
64
+ width = measureContext.measureText(grapheme).width
65
+ widths.set(key, width)
66
+ }
67
+ return width
68
+ }
69
+
70
+ function glyph(fontKey, font, grapheme, scale) {
71
+ const key = fontKey + '' + grapheme + '' + scale
72
+ let entry = glyphs.get(key)
73
+ if (entry) return entry
74
+
75
+ const renderFont = fontString({ ...font, size: font.size * scale })
76
+ measureContext.font = renderFont
77
+ const metrics = measureContext.measureText(grapheme)
78
+ const ascent = Math.ceil(metrics.actualBoundingBoxAscent ?? font.size * scale)
79
+ const descent = Math.ceil(metrics.actualBoundingBoxDescent ?? 0)
80
+ const left = Math.ceil(metrics.actualBoundingBoxLeft ?? 0)
81
+ const right = Math.ceil(metrics.actualBoundingBoxRight ?? metrics.width)
82
+ const w = Math.max(1, left + right + 2)
83
+ const h = Math.max(1, ascent + descent + 2)
84
+
85
+ if (shelfX + w + PADDING > ATLAS_SIZE) {
86
+ shelfX = PADDING
87
+ shelfY += shelfHeight + PADDING
88
+ shelfHeight = 0
89
+ }
90
+ if (shelfY + h + PADDING > ATLAS_SIZE) {
91
+ reset()
92
+ return glyph(fontKey, font, grapheme, scale)
93
+ }
94
+
95
+ context.font = renderFont
96
+ context.textBaseline = 'alphabetic'
97
+ context.fillStyle = '#fff'
98
+ context.fillText(grapheme, shelfX + left + 1, shelfY + ascent + 1)
99
+
100
+ entry = {
101
+ u0: shelfX / ATLAS_SIZE,
102
+ v0: shelfY / ATLAS_SIZE,
103
+ u1: (shelfX + w) / ATLAS_SIZE,
104
+ v1: (shelfY + h) / ATLAS_SIZE,
105
+ w: w / scale,
106
+ h: h / scale,
107
+ offsetX: -(left + 1) / scale,
108
+ offsetY: -(ascent + 1) / scale,
109
+ generation,
110
+ }
111
+ glyphs.set(key, entry)
112
+ shelfX += w + PADDING
113
+ shelfHeight = Math.max(shelfHeight, h)
114
+ dirty = true
115
+ return entry
116
+ }
117
+
118
+ function upload() {
119
+ if (!dirty) return
120
+ gl.bindTexture(gl.TEXTURE_2D, texture)
121
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false)
122
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, canvas)
123
+ dirty = false
124
+ }
125
+
126
+ function metrics(font) {
127
+ const fontKey = fontString(font)
128
+ let entry = fontMetrics.get(fontKey)
129
+ if (entry) return entry
130
+ measureContext.font = fontKey
131
+ const m = measureContext.measureText('Hg')
132
+ entry = {
133
+ ascent: m.fontBoundingBoxAscent ?? font.size * 0.8,
134
+ descent: m.fontBoundingBoxDescent ?? font.size * 0.2,
135
+ }
136
+ fontMetrics.set(fontKey, entry)
137
+ return entry
138
+ }
139
+
140
+ function measure(text, font) {
141
+ const fontKey = fontString(font)
142
+ let width = 0
143
+ for (const grapheme of graphemes(text)) width += advance(fontKey, font, grapheme)
144
+ return { w: width, h: font.size * (font.lineHeight ?? 1.25) }
145
+ }
146
+
147
+ function layoutText(text, font, scale) {
148
+ const fontKey = fontString(font)
149
+ const items = []
150
+ let x = 0
151
+ for (const grapheme of graphemes(text)) {
152
+ if (grapheme !== ' ') items.push({ x, glyph: glyph(fontKey, font, grapheme, scale) })
153
+ x += advance(fontKey, font, grapheme)
154
+ }
155
+ return { items, width: x }
156
+ }
157
+
158
+ function truncate(text, font, maxWidth) {
159
+ const fontKey = fontString(font)
160
+ const parts = graphemes(text)
161
+ let width = 0
162
+ for (let i = 0; i < parts.length; i++) width += advance(fontKey, font, parts[i])
163
+ if (width <= maxWidth) return text
164
+ const ellipsisWidth = advance(fontKey, font, '…')
165
+ let out = ''
166
+ let used = ellipsisWidth
167
+ for (const part of parts) {
168
+ const w = advance(fontKey, font, part)
169
+ if (used + w > maxWidth) break
170
+ out += part
171
+ used += w
172
+ }
173
+ return out + '…'
174
+ }
175
+
176
+ return { texture, measure, metrics, layoutText, truncate, upload, get generation() { return generation } }
177
+ }
@@ -0,0 +1,64 @@
1
+ const TARGET_WIDTH = 72
2
+ const EMPTY = { width: 0, height: 0, data: new Uint8Array(0), scaleX: 0, scaleY: 0 }
3
+
4
+ export function createProbe(gl) {
5
+ const slots = []
6
+
7
+ function slot(index) {
8
+ while (slots.length <= index) slots.push({ buffer: gl.createBuffer(), pending: null, result: EMPTY })
9
+ return slots[index]
10
+ }
11
+
12
+ function levelFor(surface) {
13
+ let level = 0
14
+ while (level + 1 < surface.levels && surface.sizes[level + 1].w >= TARGET_WIDTH) level += 1
15
+ return level
16
+ }
17
+
18
+ function request(index, surface, cssWidth, cssHeight) {
19
+ const entry = slot(index)
20
+ if (entry.pending) return
21
+ const level = levelFor(surface)
22
+ const size = surface.sizes[level]
23
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, entry.buffer)
24
+ gl.bufferData(gl.PIXEL_PACK_BUFFER, size.w * size.h * 4, gl.STREAM_READ)
25
+ gl.bindFramebuffer(gl.FRAMEBUFFER, surface.fbos[level])
26
+ gl.readPixels(0, 0, size.w, size.h, gl.RGBA, gl.UNSIGNED_BYTE, 0)
27
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
28
+ const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
29
+ gl.flush()
30
+ entry.pending = { sync, width: size.w, height: size.h, scaleX: size.w / cssWidth, scaleY: size.h / cssHeight }
31
+ }
32
+
33
+ function poll() {
34
+ let changed = false
35
+ let collected = 0
36
+ for (const entry of slots) {
37
+ const pending = entry.pending
38
+ if (!pending) continue
39
+ const status = gl.clientWaitSync(pending.sync, 0, 0)
40
+ if (status === gl.TIMEOUT_EXPIRED) continue
41
+ gl.deleteSync(pending.sync)
42
+ entry.pending = null
43
+ if (status === gl.WAIT_FAILED) continue
44
+ const length = pending.width * pending.height * 4
45
+ const data = entry.result.data.length === length ? entry.result.data : new Uint8Array(length)
46
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, entry.buffer)
47
+ gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, data)
48
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
49
+ collected += 1
50
+ let checksum = 0
51
+ for (let i = 0; i < data.length; i += 4) checksum += data[i] * 3 + data[i + 1] * 5 + data[i + 2]
52
+ if (Math.abs(checksum - (entry.checksum ?? -1)) > 64) changed = true
53
+ entry.checksum = checksum
54
+ entry.result = { width: pending.width, height: pending.height, data, scaleX: pending.scaleX, scaleY: pending.scaleY }
55
+ }
56
+ return { collected, changed }
57
+ }
58
+
59
+ function current(index) {
60
+ return slots[index]?.result ?? EMPTY
61
+ }
62
+
63
+ return { request, poll, current, get slots() { return slots.map((s) => ({ pending: Boolean(s.pending), width: s.result.width })) } }
64
+ }
@@ -0,0 +1,48 @@
1
+ import { compileProgram, setUniforms } from './context.js'
2
+ import { blitVertex, downsampleFragment } from './shaders/ui.js'
3
+
4
+ const LEVEL_SIGMA = 1.1547
5
+
6
+ export function blurRadiusToLod(radius, levels) {
7
+ const sigma = radius / 3
8
+ const lod = Math.log2(Math.max(sigma, 0.0001) / LEVEL_SIGMA)
9
+ return Math.max(0, Math.min(levels - 1, lod))
10
+ }
11
+
12
+ export function createPyramid(gl) {
13
+ const program = compileProgram(gl, blitVertex, downsampleFragment, 'downsample')
14
+
15
+ function build(surface, scratch) {
16
+ gl.useProgram(program.program)
17
+ gl.disable(gl.BLEND)
18
+ gl.disable(gl.SCISSOR_TEST)
19
+ gl.activeTexture(gl.TEXTURE0)
20
+ for (let level = 1; level < surface.levels; level++) {
21
+ const size = surface.sizes[level]
22
+ const sourceSize = surface.sizes[level - 1]
23
+ gl.bindFramebuffer(gl.FRAMEBUFFER, scratch.fbos[level])
24
+ gl.viewport(0, 0, size.w, size.h)
25
+ gl.bindTexture(gl.TEXTURE_2D, surface.texture)
26
+ setUniforms(gl, program, {
27
+ u_texture: 0,
28
+ u_texel: [1 / sourceSize.w, 1 / sourceSize.h],
29
+ u_direction: [2, 0],
30
+ u_lod: level - 1,
31
+ })
32
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
33
+
34
+ gl.bindFramebuffer(gl.FRAMEBUFFER, surface.fbos[level])
35
+ gl.viewport(0, 0, size.w, size.h)
36
+ gl.bindTexture(gl.TEXTURE_2D, scratch.texture)
37
+ setUniforms(gl, program, {
38
+ u_texture: 0,
39
+ u_texel: [1 / size.w, 1 / size.h],
40
+ u_direction: [0, 1],
41
+ u_lod: level,
42
+ })
43
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
44
+ }
45
+ }
46
+
47
+ return { build }
48
+ }
@@ -0,0 +1,296 @@
1
+ import { createContext, compileProgram, setUniforms, createSurface, destroySurface, mipLevelsFor, createImageTexture, updateImageTexture } from './context.js'
2
+ import { createPyramid, blurRadiusToLod } from './pyramid.js'
3
+ import { createGlyphAtlas } from './glyphs.js'
4
+ import { createBatch } from './batch.js'
5
+ import { createProbe } from './probe.js'
6
+ import { glassVertex, glassFragment, MAX_SHAPES } from './shaders/glass.js'
7
+ import { blitVertex, blitFragment } from './shaders/ui.js'
8
+ import { materialUniforms, shadowUniforms } from '../material/material.js'
9
+
10
+ function createColorTarget(gl, width, height) {
11
+ const texture = gl.createTexture()
12
+ gl.bindTexture(gl.TEXTURE_2D, texture)
13
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height)
14
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
15
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
16
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
17
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
18
+ const fbo = gl.createFramebuffer()
19
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo)
20
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
21
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
22
+ return { texture, fbo, width, height }
23
+ }
24
+
25
+ export function createRenderer(canvas) {
26
+ const gl = createContext(canvas)
27
+ const atlas = createGlyphAtlas(gl)
28
+ const batch = createBatch(gl, atlas)
29
+ const pyramid = createPyramid(gl)
30
+ const probe = createProbe(gl)
31
+ const glassProgram = compileProgram(gl, glassVertex, glassFragment, 'glass')
32
+ const blitProgram = compileProgram(gl, blitVertex, blitFragment, 'blit')
33
+
34
+ let width = 0
35
+ let height = 0
36
+ let dpr = 1
37
+ let physicalWidth = 0
38
+ let physicalHeight = 0
39
+ let levels = 1
40
+ let strata = []
41
+ let scratch = null
42
+
43
+ function ensureStrata(count) {
44
+ while (strata.length < count) {
45
+ strata.push({
46
+ color: createColorTarget(gl, physicalWidth, physicalHeight),
47
+ pyramid: createSurface(gl, physicalWidth, physicalHeight, levels),
48
+ })
49
+ }
50
+ }
51
+
52
+ function releaseTargets() {
53
+ for (const stratum of strata) {
54
+ gl.deleteFramebuffer(stratum.color.fbo)
55
+ gl.deleteTexture(stratum.color.texture)
56
+ destroySurface(gl, stratum.pyramid)
57
+ }
58
+ strata = []
59
+ if (scratch) destroySurface(gl, scratch)
60
+ scratch = null
61
+ }
62
+
63
+ function resize(cssWidth, cssHeight, ratio) {
64
+ const nextWidth = Math.max(1, Math.round(cssWidth * ratio))
65
+ const nextHeight = Math.max(1, Math.round(cssHeight * ratio))
66
+ if (nextWidth === physicalWidth && nextHeight === physicalHeight && ratio === dpr) {
67
+ width = cssWidth
68
+ height = cssHeight
69
+ return
70
+ }
71
+ width = cssWidth
72
+ height = cssHeight
73
+ dpr = ratio
74
+ physicalWidth = nextWidth
75
+ physicalHeight = nextHeight
76
+ canvas.width = physicalWidth
77
+ canvas.height = physicalHeight
78
+ levels = mipLevelsFor(physicalWidth, physicalHeight)
79
+ releaseTargets()
80
+ scratch = createSurface(gl, physicalWidth, physicalHeight, levels)
81
+ }
82
+
83
+ function bindTarget(target) {
84
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo)
85
+ gl.viewport(0, 0, physicalWidth, physicalHeight)
86
+ }
87
+
88
+ function blit(sourceTexture, targetFbo) {
89
+ gl.bindFramebuffer(gl.FRAMEBUFFER, targetFbo)
90
+ gl.viewport(0, 0, physicalWidth, physicalHeight)
91
+ gl.disable(gl.BLEND)
92
+ gl.disable(gl.SCISSOR_TEST)
93
+ gl.useProgram(blitProgram.program)
94
+ gl.activeTexture(gl.TEXTURE0)
95
+ gl.bindTexture(gl.TEXTURE_2D, sourceTexture)
96
+ setUniforms(gl, blitProgram, { u_texture: 0, u_lod: 0 })
97
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
98
+ }
99
+
100
+ function buildPyramid(stratum) {
101
+ blit(stratum.color.texture, stratum.pyramid.fbos[0])
102
+ pyramid.build(stratum.pyramid, scratch)
103
+ }
104
+
105
+ function scissor(clip) {
106
+ if (!clip) {
107
+ gl.disable(gl.SCISSOR_TEST)
108
+ return
109
+ }
110
+ gl.enable(gl.SCISSOR_TEST)
111
+ const x = Math.floor(clip.x * dpr)
112
+ const y = Math.floor((height - clip.y - clip.h) * dpr)
113
+ const w = Math.ceil(clip.w * dpr) + 1
114
+ const h = Math.ceil(clip.h * dpr) + 1
115
+ gl.scissor(x, y, w, h)
116
+ }
117
+
118
+ const shapeData = new Float32Array(MAX_SHAPES * 4)
119
+ const cornerData = new Float32Array(MAX_SHAPES * 2)
120
+ const fadeData = new Float32Array(MAX_SHAPES)
121
+
122
+ function uploadShapes(op) {
123
+ const count = Math.min(MAX_SHAPES, op.shapes.length)
124
+ shapeData.fill(0)
125
+ cornerData.fill(0)
126
+ fadeData.fill(1)
127
+ for (let i = 0; i < count; i++) {
128
+ const shape = op.shapes[i]
129
+ shapeData[i * 4] = shape.x
130
+ shapeData[i * 4 + 1] = shape.y
131
+ shapeData[i * 4 + 2] = shape.w
132
+ shapeData[i * 4 + 3] = shape.h
133
+ cornerData[i * 2] = shape.radius
134
+ cornerData[i * 2 + 1] = shape.roundness ?? 5
135
+ fadeData[i] = shape.fade ?? 1
136
+ }
137
+ setUniforms(gl, glassProgram, { u_shapeCount: count, u_shapes: shapeData, u_corners: cornerData, u_shapeFade: fadeData, u_mergeRate: op.mergeRate ?? 0, u_hardFirst: op.hardFirst ? 1 : 0 })
138
+ }
139
+
140
+ function drawGlass(op, sceneTexture) {
141
+ gl.useProgram(glassProgram.program)
142
+ gl.activeTexture(gl.TEXTURE0)
143
+ gl.bindTexture(gl.TEXTURE_2D, sceneTexture)
144
+ uploadShapes(op)
145
+ const material = op.material
146
+ setUniforms(gl, glassProgram, {
147
+ u_scene: 0,
148
+ u_viewport: [width, height],
149
+ u_resolution: [physicalWidth, physicalHeight],
150
+ u_dpr: dpr,
151
+ u_pass: 1,
152
+ u_quad: [op.bounds.x, op.bounds.y, op.bounds.w, op.bounds.h],
153
+ ...materialUniforms(material),
154
+ u_blurLod: blurRadiusToLod(material.blurRadius, levels),
155
+ u_dimming: op.dimming ?? 0,
156
+ u_glow: op.glow ?? [0, 0, 0, 0],
157
+ u_coverageScale: op.coverageScale ?? 1,
158
+ u_border: op.border ?? 0,
159
+ u_borderColor: op.borderColor ?? [0, 0, 0, 0],
160
+ })
161
+ scissor(op.clip)
162
+ gl.enable(gl.BLEND)
163
+ gl.blendEquation(gl.FUNC_ADD)
164
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
165
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
166
+ }
167
+
168
+ function drawShadow(op) {
169
+ const material = op.material
170
+ if (material.shadowFactor <= 0) return
171
+ gl.useProgram(glassProgram.program)
172
+ uploadShapes(op)
173
+ const shadow = shadowUniforms(material)
174
+ const reach = material.shadowExpand * 5 + Math.abs(material.shadowPosition.x) + Math.abs(material.shadowPosition.y) + (op.mergeRate ?? 0)
175
+ setUniforms(gl, glassProgram, {
176
+ u_viewport: [width, height],
177
+ u_resolution: [physicalWidth, physicalHeight],
178
+ u_dpr: dpr,
179
+ u_pass: 0,
180
+ u_quad: [op.bounds.x - reach, op.bounds.y - reach, op.bounds.w + reach * 2, op.bounds.h + reach * 2],
181
+ u_shadowExpand: shadow.u_shadowExpand,
182
+ u_shadowFactor: shadow.u_shadowFactor * (op.shadowScale ?? 1),
183
+ u_shadowOffset: [-shadow.u_shadowPosition[0], shadow.u_shadowPosition[1]],
184
+ u_shadowClipOn: op.shadowClip ? 1 : 0,
185
+ u_shadowClipRect: op.shadowClip ? [op.shadowClip.x, op.shadowClip.y, op.shadowClip.w, op.shadowClip.h] : [0, 0, 0, 0],
186
+ u_shadowClipCorner: op.shadowClip ? [op.shadowClip.radius, op.shadowClip.roundness ?? 5] : [0, 5],
187
+ })
188
+ scissor(op.clip)
189
+ gl.enable(gl.BLEND)
190
+ gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT)
191
+ gl.blendFunc(gl.ONE, gl.ONE)
192
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
193
+ gl.blendEquation(gl.FUNC_ADD)
194
+ }
195
+
196
+ function drawOps(ops, sceneTexture) {
197
+ batch.begin({ width, height, dpr, sceneTexture })
198
+ let clip = undefined
199
+ for (const op of ops) {
200
+ if (op.clip !== clip) {
201
+ batch.flush()
202
+ clip = op.clip
203
+ scissor(clip)
204
+ }
205
+ batch.draw(op)
206
+ }
207
+ batch.flush()
208
+ gl.disable(gl.SCISSOR_TEST)
209
+ }
210
+
211
+ function render(frame) {
212
+ const count = frame.strata.length
213
+ ensureStrata(count)
214
+ const generation = atlas.generation
215
+
216
+ for (let k = 0; k < count; k++) {
217
+ const stratum = strata[k]
218
+ const below = k > 0 ? strata[k - 1] : null
219
+ const layer = frame.strata[k]
220
+ const sceneTexture = below ? below.pyramid.texture : stratum.pyramid.texture
221
+
222
+ if (k === 0) {
223
+ bindTarget(stratum.color)
224
+ gl.disable(gl.SCISSOR_TEST)
225
+ const c = frame.clearColor ?? [0, 0, 0, 1]
226
+ gl.clearColor(c[0], c[1], c[2], c[3])
227
+ gl.clear(gl.COLOR_BUFFER_BIT)
228
+ } else {
229
+ blit(below.color.texture, stratum.color.fbo)
230
+ for (const op of layer.glass) drawGlass(op, sceneTexture)
231
+ gl.disable(gl.SCISSOR_TEST)
232
+ }
233
+
234
+ drawOps(layer.ops, sceneTexture)
235
+
236
+ const next = frame.strata[k + 1]
237
+ if (next) {
238
+ bindTarget(stratum.color)
239
+ for (const op of next.glass) drawShadow(op)
240
+ gl.disable(gl.SCISSOR_TEST)
241
+ buildPyramid(stratum)
242
+ probe.request(k, stratum.pyramid, width, height)
243
+ }
244
+ }
245
+
246
+ if (atlas.generation !== generation) {
247
+ render(frame)
248
+ return
249
+ }
250
+
251
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
252
+ gl.viewport(0, 0, physicalWidth, physicalHeight)
253
+ gl.disable(gl.BLEND)
254
+ gl.disable(gl.SCISSOR_TEST)
255
+ gl.useProgram(blitProgram.program)
256
+ gl.activeTexture(gl.TEXTURE0)
257
+ gl.bindTexture(gl.TEXTURE_2D, strata[count - 1].color.texture)
258
+ setUniforms(gl, blitProgram, { u_texture: 0, u_lod: 0 })
259
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
260
+ }
261
+
262
+ function loadImage(source, options) {
263
+ return createImageTexture(gl, source, options)
264
+ }
265
+
266
+ function updateImage(image, source) {
267
+ updateImageTexture(gl, image, source)
268
+ }
269
+
270
+ function destroyImage(image) {
271
+ gl.deleteTexture(image.texture)
272
+ }
273
+
274
+ return {
275
+ gl,
276
+ atlas,
277
+ probe,
278
+ resize,
279
+ render,
280
+ loadImage,
281
+ updateImage,
282
+ destroyImage,
283
+ get levels() {
284
+ return levels
285
+ },
286
+ get width() {
287
+ return width
288
+ },
289
+ get height() {
290
+ return height
291
+ },
292
+ get dpr() {
293
+ return dpr
294
+ },
295
+ }
296
+ }
@@ -0,0 +1,58 @@
1
+ export const colorLib = /* glsl */ `
2
+ const vec3 D65_WHITE = vec3(0.95045592705, 1.0, 1.08905775076);
3
+ const mat3 RGB_TO_XYZ_M = mat3(
4
+ 0.4124, 0.3576, 0.1805,
5
+ 0.2126, 0.7152, 0.0722,
6
+ 0.0193, 0.1192, 0.9505
7
+ );
8
+ const mat3 XYZ_TO_RGB_M = mat3(
9
+ 3.2406255, -1.537208 , -0.4986286,
10
+ -0.9689307, 1.8757561, 0.0415175,
11
+ 0.0557101, -0.2040211, 1.0569959
12
+ );
13
+ float UNCOMPAND_SRGB(float a) {
14
+ return a > 0.04045 ? pow((a + 0.055) / 1.055, 2.4) : a / 12.92;
15
+ }
16
+ float COMPAND_RGB(float a) {
17
+ return a <= 0.0031308 ? 12.92 * a : 1.055 * pow(a, 0.41666666666) - 0.055;
18
+ }
19
+ vec3 SRGB_TO_RGB(vec3 srgb) {
20
+ return vec3(UNCOMPAND_SRGB(srgb.x), UNCOMPAND_SRGB(srgb.y), UNCOMPAND_SRGB(srgb.z));
21
+ }
22
+ vec3 RGB_TO_SRGB(vec3 rgb) {
23
+ return vec3(COMPAND_RGB(rgb.x), COMPAND_RGB(rgb.y), COMPAND_RGB(rgb.z));
24
+ }
25
+ float XYZ_TO_LAB_F(float x) {
26
+ return x > 0.00885645167 ? pow(x, 0.333333333) : 7.78703703704 * x + 0.13793103448;
27
+ }
28
+ vec3 XYZ_TO_LAB(vec3 xyz) {
29
+ vec3 s = xyz / D65_WHITE;
30
+ s = vec3(XYZ_TO_LAB_F(s.x), XYZ_TO_LAB_F(s.y), XYZ_TO_LAB_F(s.z));
31
+ return vec3(116.0 * s.y - 16.0, 500.0 * (s.x - s.y), 200.0 * (s.y - s.z));
32
+ }
33
+ vec3 SRGB_TO_LAB(vec3 srgb) {
34
+ return XYZ_TO_LAB(SRGB_TO_RGB(srgb) * RGB_TO_XYZ_M);
35
+ }
36
+ vec3 LAB_TO_LCH(vec3 lab) {
37
+ return vec3(lab.x, sqrt(dot(lab.yz, lab.yz)), atan(lab.z, lab.y) * 57.2957795131);
38
+ }
39
+ vec3 SRGB_TO_LCH(vec3 srgb) {
40
+ return LAB_TO_LCH(SRGB_TO_LAB(srgb));
41
+ }
42
+ float LAB_TO_XYZ_F(float x) {
43
+ return x > 0.206897 ? x * x * x : 0.12841854934 * (x - 0.137931034);
44
+ }
45
+ vec3 LAB_TO_XYZ(vec3 lab) {
46
+ float w = (lab.x + 16.0) / 116.0;
47
+ return D65_WHITE * vec3(LAB_TO_XYZ_F(w + lab.y / 500.0), LAB_TO_XYZ_F(w), LAB_TO_XYZ_F(w - lab.z / 200.0));
48
+ }
49
+ vec3 LAB_TO_SRGB(vec3 lab) {
50
+ return RGB_TO_SRGB(LAB_TO_XYZ(lab) * XYZ_TO_RGB_M);
51
+ }
52
+ vec3 LCH_TO_LAB(vec3 lch) {
53
+ return vec3(lch.x, lch.y * cos(lch.z * 0.01745329251), lch.y * sin(lch.z * 0.01745329251));
54
+ }
55
+ vec3 LCH_TO_SRGB(vec3 lch) {
56
+ return LAB_TO_SRGB(LCH_TO_LAB(lch));
57
+ }
58
+ `