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.
- package/README.md +111 -0
- package/demo/public/materials.json +809 -0
- package/package.json +39 -0
- package/src/core/curve.js +71 -0
- package/src/core/frame.js +272 -0
- package/src/core/geometry.js +61 -0
- package/src/core/ids.js +31 -0
- package/src/core/input.js +294 -0
- package/src/core/layout.js +220 -0
- package/src/core/spring.js +113 -0
- package/src/core/state.js +54 -0
- package/src/core/theme.js +77 -0
- package/src/index.js +221 -0
- package/src/material/adapt.js +72 -0
- package/src/material/material.js +131 -0
- package/src/material/optics.js +49 -0
- package/src/material/params.js +83 -0
- package/src/material/presets.js +291 -0
- package/src/render/batch.js +236 -0
- package/src/render/context.js +138 -0
- package/src/render/glyphs.js +177 -0
- package/src/render/probe.js +64 -0
- package/src/render/pyramid.js +48 -0
- package/src/render/renderer.js +296 -0
- package/src/render/shaders/color.js +58 -0
- package/src/render/shaders/glass.js +241 -0
- package/src/render/shaders/ui.js +194 -0
- package/src/widgets/composite.js +355 -0
- package/src/widgets/controls.js +545 -0
- package/src/widgets/desktop.js +144 -0
- package/src/widgets/glass.js +326 -0
- package/src/widgets/primitives.js +355 -0
|
@@ -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
|
+
`
|