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,77 @@
|
|
|
1
|
+
function mixColor(a, b, t) {
|
|
2
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, a[3] + (b[3] - a[3]) * t]
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const ACCENT = [0.17, 0.44, 1, 1]
|
|
6
|
+
|
|
7
|
+
const LIGHT = {
|
|
8
|
+
fg: [0.06, 0.06, 0.08, 0.94],
|
|
9
|
+
fg2: [0.06, 0.06, 0.08, 0.58],
|
|
10
|
+
fg3: [0.06, 0.06, 0.08, 0.34],
|
|
11
|
+
hover: [0.06, 0.06, 0.08, 0.07],
|
|
12
|
+
press: [0.06, 0.06, 0.08, 0.13],
|
|
13
|
+
selected: [0.06, 0.06, 0.08, 0.1],
|
|
14
|
+
track: [0.06, 0.06, 0.08, 0.14],
|
|
15
|
+
separator: [0.06, 0.06, 0.08, 0.1],
|
|
16
|
+
knob: [1, 1, 1, 1],
|
|
17
|
+
field: [1, 1, 1, 0.62],
|
|
18
|
+
onAccent: [1, 1, 1, 1],
|
|
19
|
+
accent: ACCENT,
|
|
20
|
+
accentPress: [0.13, 0.36, 0.88, 1],
|
|
21
|
+
switchOn: [0.2, 0.78, 0.35, 1],
|
|
22
|
+
focusRing: [0.17, 0.44, 1, 0.7],
|
|
23
|
+
dim: [0, 0, 0, 0.22],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DARK = {
|
|
27
|
+
fg: [1, 1, 1, 0.96],
|
|
28
|
+
fg2: [1, 1, 1, 0.62],
|
|
29
|
+
fg3: [1, 1, 1, 0.36],
|
|
30
|
+
hover: [1, 1, 1, 0.1],
|
|
31
|
+
press: [1, 1, 1, 0.18],
|
|
32
|
+
selected: [1, 1, 1, 0.14],
|
|
33
|
+
track: [1, 1, 1, 0.22],
|
|
34
|
+
separator: [1, 1, 1, 0.14],
|
|
35
|
+
knob: [1, 1, 1, 1],
|
|
36
|
+
field: [1, 1, 1, 0.14],
|
|
37
|
+
onAccent: [1, 1, 1, 1],
|
|
38
|
+
accent: [0.35, 0.58, 1, 1],
|
|
39
|
+
accentPress: [0.28, 0.5, 0.92, 1],
|
|
40
|
+
switchOn: [0.25, 0.82, 0.4, 1],
|
|
41
|
+
focusRing: [0.45, 0.65, 1, 0.8],
|
|
42
|
+
dim: [0, 0, 0, 0.3],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveTheme(dark, settings) {
|
|
46
|
+
const theme = {}
|
|
47
|
+
for (const key of Object.keys(LIGHT)) theme[key] = mixColor(LIGHT[key], DARK[key], dark)
|
|
48
|
+
theme.dark = dark
|
|
49
|
+
if (settings?.increaseContrast) {
|
|
50
|
+
const strong = dark > 0.5
|
|
51
|
+
theme.fg = strong ? [1, 1, 1, 1] : [0, 0, 0, 1]
|
|
52
|
+
theme.fg2 = strong ? [1, 1, 1, 0.85] : [0, 0, 0, 0.85]
|
|
53
|
+
theme.fg3 = strong ? [1, 1, 1, 0.7] : [0, 0, 0, 0.7]
|
|
54
|
+
theme.track = strong ? [1, 1, 1, 0.4] : [0, 0, 0, 0.35]
|
|
55
|
+
theme.separator = strong ? [1, 1, 1, 0.5] : [0, 0, 0, 0.5]
|
|
56
|
+
theme.hover = strong ? [1, 1, 1, 0.2] : [0, 0, 0, 0.12]
|
|
57
|
+
theme.press = strong ? [1, 1, 1, 0.32] : [0, 0, 0, 0.22]
|
|
58
|
+
theme.border = strong ? [1, 1, 1, 0.9] : [0, 0, 0, 0.9]
|
|
59
|
+
}
|
|
60
|
+
return theme
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function withAlpha(color, alpha) {
|
|
64
|
+
return [color[0], color[1], color[2], color[3] * alpha]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function tintFamily(color, backdropLuminance) {
|
|
68
|
+
const lift = (0.5 - backdropLuminance) * 0.18
|
|
69
|
+
return [
|
|
70
|
+
Math.min(1, Math.max(0, color[0] + lift)),
|
|
71
|
+
Math.min(1, Math.max(0, color[1] + lift)),
|
|
72
|
+
Math.min(1, Math.max(0, color[2] + lift)),
|
|
73
|
+
color[3],
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const CONTENT_THEME = resolveTheme(0, null)
|
package/src/index.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createRenderer } from './render/renderer.js'
|
|
2
|
+
import { createInput } from './core/input.js'
|
|
3
|
+
import { createStateStore } from './core/state.js'
|
|
4
|
+
import { createIdScope } from './core/ids.js'
|
|
5
|
+
import { createSpringClock, springs, springForDuration } from './core/spring.js'
|
|
6
|
+
import { createFrameDriver } from './core/frame.js'
|
|
7
|
+
import { createGlass } from './widgets/glass.js'
|
|
8
|
+
import { createPrimitives } from './widgets/primitives.js'
|
|
9
|
+
import { createControls } from './widgets/controls.js'
|
|
10
|
+
import { createComposite } from './widgets/composite.js'
|
|
11
|
+
import { createDesktop } from './widgets/desktop.js'
|
|
12
|
+
import { clonePresets, ROLE_PRESETS, ROLES, serializeMaterials, parseMaterials, assignMaterials } from './material/presets.js'
|
|
13
|
+
import { MATERIAL_PARAMS, SHAPE_PARAMS, MATERIAL_GROUPS, defaultMaterial, normalizeMaterial } from './material/params.js'
|
|
14
|
+
import { mixMaterial, unfocusedMaterial, darkMaterial } from './material/material.js'
|
|
15
|
+
import { ACCENT } from './core/theme.js'
|
|
16
|
+
|
|
17
|
+
export { ROLE_PRESETS, ROLES, serializeMaterials, parseMaterials, unfocusedMaterial, darkMaterial, MATERIAL_PARAMS, SHAPE_PARAMS, MATERIAL_GROUPS, defaultMaterial, normalizeMaterial, mixMaterial, springs, springForDuration, ACCENT }
|
|
18
|
+
|
|
19
|
+
export function createUI(canvas, options = {}) {
|
|
20
|
+
const renderer = createRenderer(canvas)
|
|
21
|
+
const input = createInput(canvas)
|
|
22
|
+
const state = createStateStore()
|
|
23
|
+
const ids = createIdScope()
|
|
24
|
+
const clock = createSpringClock()
|
|
25
|
+
const driver = createFrameDriver({ renderer, input, state, ids, clock, probe: renderer.probe })
|
|
26
|
+
|
|
27
|
+
const settings = { reduceTransparency: false, increaseContrast: false, reduceMotion: false, appearance: 'auto', ...(options.settings ?? {}) }
|
|
28
|
+
const materials = options.materials ? assignMaterials(clonePresets(), parseMaterials(options.materials)) : clonePresets()
|
|
29
|
+
const focus = { id: null, keyboard: false }
|
|
30
|
+
let focusables = []
|
|
31
|
+
let lastTime = 0
|
|
32
|
+
let running = false
|
|
33
|
+
let dirty = true
|
|
34
|
+
let wakeAt = 0
|
|
35
|
+
let continuous = false
|
|
36
|
+
let frameCallback = null
|
|
37
|
+
let cursor = null
|
|
38
|
+
let stableCollects = 2
|
|
39
|
+
let requested = true
|
|
40
|
+
|
|
41
|
+
function interaction(id, props = {}) {
|
|
42
|
+
const f = driver.frame
|
|
43
|
+
const disabled = !!props.disabled
|
|
44
|
+
if (props.focusable !== false && !disabled) driver.registerFocusable(id)
|
|
45
|
+
const focused = focus.id === id
|
|
46
|
+
let clicked = !disabled && f.clicked.has(id)
|
|
47
|
+
if (focused && focus.keyboard && !disabled && (f.keysPressed.has('Enter') || f.keysPressed.has('Space'))) clicked = true
|
|
48
|
+
return {
|
|
49
|
+
hot: !disabled && f.hot === id,
|
|
50
|
+
active: !disabled && f.active === id,
|
|
51
|
+
pressed: !disabled && f.pressed.has(id),
|
|
52
|
+
released: f.released.has(id),
|
|
53
|
+
clicked,
|
|
54
|
+
focused,
|
|
55
|
+
keyboard: focus.keyboard,
|
|
56
|
+
disabled,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function insideGlass() {
|
|
61
|
+
let node = driver.current()
|
|
62
|
+
while (node) {
|
|
63
|
+
if (node.kind === 'glass') return true
|
|
64
|
+
node = node.parent
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function updateFocus(frame) {
|
|
70
|
+
if (frame.pressedAnywhere) {
|
|
71
|
+
focus.keyboard = false
|
|
72
|
+
const target = frame.hotEntry?.id ?? null
|
|
73
|
+
focus.id = target && focusables.includes(target) ? target : null
|
|
74
|
+
}
|
|
75
|
+
if (frame.keysPressed.has('Tab') && focusables.length > 0) {
|
|
76
|
+
const index = focusables.indexOf(focus.id)
|
|
77
|
+
const direction = frame.keysDown.has('Shift') ? -1 : 1
|
|
78
|
+
const next = index < 0 ? (direction > 0 ? 0 : focusables.length - 1) : (index + direction + focusables.length) % focusables.length
|
|
79
|
+
focus.id = focusables[next]
|
|
80
|
+
focus.keyboard = true
|
|
81
|
+
}
|
|
82
|
+
if (frame.keysPressed.has('Escape') && focus.keyboard) focus.id = null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const core = {
|
|
86
|
+
renderer,
|
|
87
|
+
input,
|
|
88
|
+
state,
|
|
89
|
+
ids,
|
|
90
|
+
clock,
|
|
91
|
+
driver,
|
|
92
|
+
settings,
|
|
93
|
+
materials,
|
|
94
|
+
focus,
|
|
95
|
+
interaction,
|
|
96
|
+
insideGlass,
|
|
97
|
+
toolbarDepth: 0,
|
|
98
|
+
capsuleDepth: 0,
|
|
99
|
+
setCursor(name) {
|
|
100
|
+
cursor = name
|
|
101
|
+
},
|
|
102
|
+
invalidateSoon(ms) {
|
|
103
|
+
const at = performance.now() + ms
|
|
104
|
+
if (!wakeAt || at < wakeAt) wakeAt = at
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const glassWidget = createGlass(core)
|
|
109
|
+
const primitives = createPrimitives(core)
|
|
110
|
+
const controls = createControls(core, primitives, glassWidget)
|
|
111
|
+
const composite = createComposite(core, primitives, controls, glassWidget)
|
|
112
|
+
|
|
113
|
+
function currentSize() {
|
|
114
|
+
const bounds = canvas.getBoundingClientRect()
|
|
115
|
+
return { w: Math.max(1, bounds.width), h: Math.max(1, bounds.height), dpr: Math.min(options.maxDpr ?? 3, window.devicePixelRatio || 1) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function frame(fn, time = performance.now()) {
|
|
119
|
+
const size = currentSize()
|
|
120
|
+
renderer.resize(size.w, size.h, size.dpr)
|
|
121
|
+
const seconds = lastTime ? (time - lastTime) / 1000 : 1 / 60
|
|
122
|
+
lastTime = time
|
|
123
|
+
clock.reduceMotion = settings.reduceMotion
|
|
124
|
+
const frameInput = driver.begin(size.w, size.h, seconds)
|
|
125
|
+
updateFocus(frameInput)
|
|
126
|
+
const probes = renderer.probe.poll()
|
|
127
|
+
cursor = null
|
|
128
|
+
fn(ui)
|
|
129
|
+
const result = driver.end({ clearColor: options.clearColor })
|
|
130
|
+
canvas.style.cursor = cursor ?? 'default'
|
|
131
|
+
focusables = driver.focusOrder
|
|
132
|
+
const active = result.animating || !frameInput.idle || probes.changed || requested
|
|
133
|
+
requested = false
|
|
134
|
+
if (active) stableCollects = 0
|
|
135
|
+
else if (probes.collected > 0) stableCollects += 1
|
|
136
|
+
dirty = active
|
|
137
|
+
if (active || stableCollects < 2) core.invalidateSoon(40)
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function tick(time) {
|
|
142
|
+
if (!running) return
|
|
143
|
+
const wakeDue = wakeAt && time >= wakeAt
|
|
144
|
+
const needsFrame = dirty || continuous || input.pending() || wakeDue
|
|
145
|
+
if (needsFrame) {
|
|
146
|
+
if (wakeDue) wakeAt = 0
|
|
147
|
+
frame(frameCallback, time)
|
|
148
|
+
}
|
|
149
|
+
requestAnimationFrame(tick)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function start(fn) {
|
|
153
|
+
frameCallback = fn
|
|
154
|
+
if (running) return
|
|
155
|
+
running = true
|
|
156
|
+
input.attach()
|
|
157
|
+
dirty = true
|
|
158
|
+
const observer = new ResizeObserver(() => {
|
|
159
|
+
dirty = true
|
|
160
|
+
})
|
|
161
|
+
observer.observe(canvas)
|
|
162
|
+
window.addEventListener('resize', invalidate)
|
|
163
|
+
requestAnimationFrame(tick)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function stop() {
|
|
167
|
+
running = false
|
|
168
|
+
input.detach()
|
|
169
|
+
window.removeEventListener('resize', invalidate)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function invalidate() {
|
|
173
|
+
dirty = true
|
|
174
|
+
requested = true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ui = {
|
|
178
|
+
frame,
|
|
179
|
+
start,
|
|
180
|
+
stop,
|
|
181
|
+
invalidate,
|
|
182
|
+
settings,
|
|
183
|
+
materials,
|
|
184
|
+
focus,
|
|
185
|
+
renderer,
|
|
186
|
+
state,
|
|
187
|
+
springs,
|
|
188
|
+
...primitives,
|
|
189
|
+
...controls,
|
|
190
|
+
...composite,
|
|
191
|
+
glass: glassWidget.glass,
|
|
192
|
+
desktop: (opts) => createDesktop(ui, opts),
|
|
193
|
+
debug: { driver, core },
|
|
194
|
+
exportMaterials: () => serializeMaterials(materials),
|
|
195
|
+
importMaterials: (data) => {
|
|
196
|
+
assignMaterials(materials, parseMaterials(data))
|
|
197
|
+
invalidate()
|
|
198
|
+
},
|
|
199
|
+
setCursor: (name) => core.setCursor(name),
|
|
200
|
+
loadImage: (source, opts) => renderer.loadImage(source, opts),
|
|
201
|
+
updateImage: (image, source) => renderer.updateImage(image, source),
|
|
202
|
+
destroyImage: (image) => renderer.destroyImage(image),
|
|
203
|
+
get input() {
|
|
204
|
+
return driver.frame
|
|
205
|
+
},
|
|
206
|
+
get viewport() {
|
|
207
|
+
return driver.viewport
|
|
208
|
+
},
|
|
209
|
+
get continuous() {
|
|
210
|
+
return continuous
|
|
211
|
+
},
|
|
212
|
+
set continuous(value) {
|
|
213
|
+
continuous = value
|
|
214
|
+
},
|
|
215
|
+
get time() {
|
|
216
|
+
return lastTime
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return ui
|
|
221
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const DARK_BELOW = 0.4
|
|
2
|
+
const LIGHT_ABOVE = 0.52
|
|
3
|
+
|
|
4
|
+
export function analyzeBackdrop(probe, rect) {
|
|
5
|
+
if (!probe || probe.width === 0) return { luminance: 0.75, detail: 0.2, samples: 0 }
|
|
6
|
+
const x0 = Math.max(0, Math.floor(rect.x * probe.scaleX))
|
|
7
|
+
const y0 = Math.max(0, Math.floor(rect.y * probe.scaleY))
|
|
8
|
+
const x1 = Math.min(probe.width - 1, Math.ceil((rect.x + rect.w) * probe.scaleX))
|
|
9
|
+
const y1 = Math.min(probe.height - 1, Math.ceil((rect.y + rect.h) * probe.scaleY))
|
|
10
|
+
if (x1 < x0 || y1 < y0) return { luminance: 0.75, detail: 0.2, samples: 0 }
|
|
11
|
+
|
|
12
|
+
let sum = 0
|
|
13
|
+
let gradient = 0
|
|
14
|
+
let count = 0
|
|
15
|
+
let edges = 0
|
|
16
|
+
const data = probe.data
|
|
17
|
+
const stride = probe.width * 4
|
|
18
|
+
for (let y = y0; y <= y1; y++) {
|
|
19
|
+
const row = probe.height - 1 - y
|
|
20
|
+
for (let x = x0; x <= x1; x++) {
|
|
21
|
+
const i = row * stride + x * 4
|
|
22
|
+
const l = (0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]) / 255
|
|
23
|
+
sum += l
|
|
24
|
+
count += 1
|
|
25
|
+
if (x < x1) {
|
|
26
|
+
const j = i + 4
|
|
27
|
+
const lr = (0.2126 * data[j] + 0.7152 * data[j + 1] + 0.0722 * data[j + 2]) / 255
|
|
28
|
+
gradient += Math.abs(lr - l)
|
|
29
|
+
edges += 1
|
|
30
|
+
}
|
|
31
|
+
if (y < y1) {
|
|
32
|
+
const j = i - stride
|
|
33
|
+
const ld = (0.2126 * data[j] + 0.7152 * data[j + 1] + 0.0722 * data[j + 2]) / 255
|
|
34
|
+
gradient += Math.abs(ld - l)
|
|
35
|
+
edges += 1
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const luminance = count ? sum / count : 0.75
|
|
40
|
+
const detail = edges ? Math.min(1, (gradient / edges) / 0.12) : 0
|
|
41
|
+
return { luminance, detail, samples: count }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolvePolarity(data, analysis, surface, clock) {
|
|
45
|
+
if (surface === 'large') {
|
|
46
|
+
data.polarity = 'light'
|
|
47
|
+
return clock.animate(data, 'dark', 0, { stiffness: 120, damping: 20 })
|
|
48
|
+
}
|
|
49
|
+
if (data.polarity == null) data.polarity = analysis.luminance < (DARK_BELOW + LIGHT_ABOVE) / 2 ? 'dark' : 'light'
|
|
50
|
+
if (analysis.samples > 0) {
|
|
51
|
+
if (data.polarity === 'light' && analysis.luminance < DARK_BELOW) data.polarity = 'dark'
|
|
52
|
+
else if (data.polarity === 'dark' && analysis.luminance > LIGHT_ABOVE) data.polarity = 'light'
|
|
53
|
+
}
|
|
54
|
+
return clock.animate(data, 'dark', data.polarity === 'dark' ? 1 : 0, { stiffness: 120, damping: 20 })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function adaptiveScales(analysis) {
|
|
58
|
+
const detail = analysis.detail
|
|
59
|
+
const bright = Math.max(0, (analysis.luminance - 0.6) / 0.4)
|
|
60
|
+
const shadow = 0.55 + detail * 0.95 - bright * (1 - detail) * 0.3
|
|
61
|
+
const tint = 0.85 + detail * 0.4
|
|
62
|
+
return { shadow: Math.max(0.2, shadow), tint: Math.min(1.3, tint) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveBorderFade(data, analysis, clock) {
|
|
66
|
+
if (data.borderPolarity == null) data.borderPolarity = analysis.samples > 0 && analysis.luminance < (DARK_BELOW + LIGHT_ABOVE) / 2 ? 'dark' : 'light'
|
|
67
|
+
if (analysis.samples > 0) {
|
|
68
|
+
if (data.borderPolarity === 'light' && analysis.luminance < DARK_BELOW) data.borderPolarity = 'dark'
|
|
69
|
+
else if (data.borderPolarity === 'dark' && analysis.luminance > LIGHT_ABOVE) data.borderPolarity = 'light'
|
|
70
|
+
}
|
|
71
|
+
return clock.animate(data, 'borderDark', data.borderPolarity === 'dark' ? 1 : 0, { stiffness: 260, damping: 30 })
|
|
72
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { MATERIAL_PARAMS } from './params.js'
|
|
2
|
+
|
|
3
|
+
function lerp(a, b, t) {
|
|
4
|
+
return a + (b - a) * t
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function mixMaterial(a, b, t) {
|
|
8
|
+
if (t <= 0) return a
|
|
9
|
+
if (t >= 1) return b
|
|
10
|
+
const out = {}
|
|
11
|
+
for (const param of MATERIAL_PARAMS) {
|
|
12
|
+
const key = param.key
|
|
13
|
+
if (param.type === 'boolean') out[key] = t < 0.5 ? a[key] : b[key]
|
|
14
|
+
else if (param.type === 'color') {
|
|
15
|
+
out[key] = {
|
|
16
|
+
r: lerp(a[key].r, b[key].r, t),
|
|
17
|
+
g: lerp(a[key].g, b[key].g, t),
|
|
18
|
+
b: lerp(a[key].b, b[key].b, t),
|
|
19
|
+
a: lerp(a[key].a, b[key].a, t),
|
|
20
|
+
}
|
|
21
|
+
} else if (param.type === 'vector') out[key] = { x: lerp(a[key].x, b[key].x, t), y: lerp(a[key].y, b[key].y, t) }
|
|
22
|
+
else out[key] = lerp(a[key], b[key], t)
|
|
23
|
+
}
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function unfocusedMaterial(material) {
|
|
28
|
+
const grey = (value) => value * 0.6 + 222 * 0.4
|
|
29
|
+
return {
|
|
30
|
+
...material,
|
|
31
|
+
tint: {
|
|
32
|
+
r: grey(material.tint.r),
|
|
33
|
+
g: grey(material.tint.g),
|
|
34
|
+
b: grey(material.tint.b),
|
|
35
|
+
a: material.tint.a,
|
|
36
|
+
},
|
|
37
|
+
refDispersion: material.refDispersion * 0.3,
|
|
38
|
+
refFresnelFactor: 0,
|
|
39
|
+
glareFactor: 0,
|
|
40
|
+
shadowFactor: 0,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function darkMaterial(material) {
|
|
45
|
+
return {
|
|
46
|
+
...material,
|
|
47
|
+
tint: { r: 28, g: 28, b: 34, a: Math.max(0.38, material.tint.a * 0.85) },
|
|
48
|
+
refFresnelFactor: material.refFresnelFactor * 0.8,
|
|
49
|
+
glareFactor: material.glareFactor * 0.75,
|
|
50
|
+
shadowFactor: material.shadowFactor * 1.1,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function lenslessMaterial(material) {
|
|
55
|
+
return {
|
|
56
|
+
...material,
|
|
57
|
+
refFactor: 1,
|
|
58
|
+
refDispersion: 0,
|
|
59
|
+
refFresnelFactor: 0,
|
|
60
|
+
glareFactor: 0,
|
|
61
|
+
blurRadius: 1,
|
|
62
|
+
tint: { ...material.tint, a: 0 },
|
|
63
|
+
shadowFactor: 0,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function thickenedMaterial(material, amount) {
|
|
68
|
+
if (amount <= 0) return material
|
|
69
|
+
return {
|
|
70
|
+
...material,
|
|
71
|
+
refThickness: material.refThickness * (1 + amount * 0.15),
|
|
72
|
+
blurRadius: Math.min(200, material.blurRadius * (1 + amount * 0.8) + amount * 12),
|
|
73
|
+
shadowExpand: material.shadowExpand * (1 + amount * 0.8),
|
|
74
|
+
shadowFactor: Math.min(100, material.shadowFactor * (1 + amount * 0.6)),
|
|
75
|
+
refFresnelFactor: material.refFresnelFactor * (1 + amount * 0.2),
|
|
76
|
+
glareFactor: material.glareFactor * (1 - amount * 0.2),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function accessibleMaterial(material, settings, dark, keepTint = false) {
|
|
81
|
+
let out = material
|
|
82
|
+
if (settings.reduceTransparency && !keepTint) {
|
|
83
|
+
const base = dark ? 34 : 246
|
|
84
|
+
out = {
|
|
85
|
+
...out,
|
|
86
|
+
blurRadius: Math.max(out.blurRadius, 140),
|
|
87
|
+
refDispersion: 0,
|
|
88
|
+
tint: { r: base, g: base, b: base + (dark ? 4 : 2), a: Math.max(out.tint.a, 0.82) },
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (settings.increaseContrast) {
|
|
92
|
+
const base = dark ? 0 : 255
|
|
93
|
+
out = {
|
|
94
|
+
...out,
|
|
95
|
+
blurRadius: Math.max(out.blurRadius, 160),
|
|
96
|
+
refDispersion: 0,
|
|
97
|
+
glareFactor: 0,
|
|
98
|
+
refFresnelFactor: 0,
|
|
99
|
+
tint: keepTint ? { ...out.tint, a: 0.96 } : { r: base, g: base, b: base, a: 0.96 },
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function materialUniforms(material) {
|
|
106
|
+
return {
|
|
107
|
+
u_refThickness: material.refThickness,
|
|
108
|
+
u_refFactor: material.refFactor,
|
|
109
|
+
u_refDispersion: material.refDispersion,
|
|
110
|
+
u_refFresnelRange: material.refFresnelRange,
|
|
111
|
+
u_refFresnelHardness: material.refFresnelHardness / 100,
|
|
112
|
+
u_refFresnelFactor: material.refFresnelFactor / 100,
|
|
113
|
+
u_glareRange: material.glareRange,
|
|
114
|
+
u_glareHardness: material.glareHardness / 100,
|
|
115
|
+
u_glareConvergence: material.glareConvergence / 100,
|
|
116
|
+
u_glareOppositeFactor: material.glareOppositeFactor / 100,
|
|
117
|
+
u_glareFactor: material.glareFactor / 100,
|
|
118
|
+
u_glareAngle: (material.glareAngle * Math.PI) / 180,
|
|
119
|
+
u_blurRadius: material.blurRadius,
|
|
120
|
+
u_blurEdge: material.blurEdge ? 1 : 0,
|
|
121
|
+
u_tint: [material.tint.r / 255, material.tint.g / 255, material.tint.b / 255, material.tint.a],
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function shadowUniforms(material) {
|
|
126
|
+
return {
|
|
127
|
+
u_shadowExpand: material.shadowExpand,
|
|
128
|
+
u_shadowFactor: material.shadowFactor / 100,
|
|
129
|
+
u_shadowPosition: [-material.shadowPosition.x, -material.shadowPosition.y],
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function safeAsin(x) {
|
|
2
|
+
return Math.asin(Math.max(-1, Math.min(1, x)))
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function edgeFactor(inwardDistance, thickness, refFactor) {
|
|
6
|
+
if (inwardDistance >= thickness) return 0
|
|
7
|
+
const ratio = 1 - inwardDistance / thickness
|
|
8
|
+
const thetaI = safeAsin(ratio * ratio)
|
|
9
|
+
const thetaT = safeAsin((1 / refFactor) * Math.sin(thetaI))
|
|
10
|
+
return -Math.tan(thetaT - thetaI)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function displacement(inwardDistance, thickness, refFactor) {
|
|
14
|
+
return edgeFactor(inwardDistance, thickness, refFactor) * 70.710678
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function superellipseCorner(px, py, radius, n) {
|
|
18
|
+
const x = Math.abs(px)
|
|
19
|
+
const y = Math.abs(py)
|
|
20
|
+
return Math.pow(Math.pow(x, n) + Math.pow(y, n), 1 / n) - radius
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function roundedRectSDF(px, py, shape, radius, roundness) {
|
|
24
|
+
const hw = shape.w / 2
|
|
25
|
+
const hh = shape.h / 2
|
|
26
|
+
const qx = px - (shape.x + hw)
|
|
27
|
+
const qy = py - (shape.y + hh)
|
|
28
|
+
const cr = Math.max(0.5, Math.min(radius, Math.min(hw, hh)))
|
|
29
|
+
const n = cr >= Math.min(hw, hh) - 0.5 ? 2 : roundness
|
|
30
|
+
const dx = Math.abs(qx) - hw
|
|
31
|
+
const dy = Math.abs(qy) - hh
|
|
32
|
+
if (dx > -cr && dy > -cr) {
|
|
33
|
+
const cx = Math.sign(qx) * (hw - cr)
|
|
34
|
+
const cy = Math.sign(qy) * (hh - cr)
|
|
35
|
+
return superellipseCorner(qx - cx, qy - cy, cr, n)
|
|
36
|
+
}
|
|
37
|
+
return Math.min(Math.max(dx, dy), 0) + Math.hypot(Math.max(dx, 0), Math.max(dy, 0))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function smin(a, b, k) {
|
|
41
|
+
if (k <= 0) return Math.min(a, b)
|
|
42
|
+
const h = Math.max(0, Math.min(1, 0.5 + (0.5 * (b - a)) / k))
|
|
43
|
+
return b + (a - b) * h - k * h * (1 - h)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function edgeProfile(sdf, range, hardness) {
|
|
47
|
+
const safeRange = Math.max(range, 0.001)
|
|
48
|
+
return Math.max(0, Math.min(1, Math.pow(Math.max(1 + (sdf / 1500) * Math.pow(500 / safeRange, 2) + hardness, 0), 5)))
|
|
49
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const MATERIAL_PARAMS = [
|
|
2
|
+
{ key: 'cornerRadius', label: 'Corner radius', group: 'shape', min: 0, max: 60, step: 1, value: 12 },
|
|
3
|
+
{ key: 'cornerRoundness', label: 'Corner roundness', group: 'shape', min: 2, max: 7, step: 0.01, value: 5 },
|
|
4
|
+
{ key: 'refThickness', label: 'Thickness', group: 'refraction', min: 1, max: 80, step: 0.01, value: 20 },
|
|
5
|
+
{ key: 'refFactor', label: 'Refraction', group: 'refraction', min: 1, max: 4, step: 0.01, value: 1.4 },
|
|
6
|
+
{ key: 'refDispersion', label: 'Dispersion', group: 'refraction', min: 0, max: 50, step: 0.01, value: 7 },
|
|
7
|
+
{ key: 'refFresnelRange', label: 'Fresnel range', group: 'fresnel', min: 0, max: 100, step: 0.01, value: 30 },
|
|
8
|
+
{ key: 'refFresnelHardness', label: 'Fresnel hardness', group: 'fresnel', min: 0, max: 100, step: 0.01, value: 20 },
|
|
9
|
+
{ key: 'refFresnelFactor', label: 'Fresnel', group: 'fresnel', min: 0, max: 100, step: 0.01, value: 20 },
|
|
10
|
+
{ key: 'glareRange', label: 'Glare range', group: 'glare', min: 0, max: 100, step: 0.01, value: 30 },
|
|
11
|
+
{ key: 'glareHardness', label: 'Glare hardness', group: 'glare', min: 0, max: 100, step: 0.01, value: 20 },
|
|
12
|
+
{ key: 'glareFactor', label: 'Glare', group: 'glare', min: 0, max: 120, step: 0.01, value: 90 },
|
|
13
|
+
{ key: 'glareConvergence', label: 'Glare convergence', group: 'glare', min: 0, max: 100, step: 0.01, value: 50 },
|
|
14
|
+
{ key: 'glareOppositeFactor', label: 'Opposite glare', group: 'glare', min: 0, max: 100, step: 0.01, value: 80 },
|
|
15
|
+
{ key: 'glareAngle', label: 'Glare angle', group: 'glare', min: -180, max: 180, step: 0.01, value: -45 },
|
|
16
|
+
{ key: 'blurRadius', label: 'Blur', group: 'blur', min: 1, max: 200, step: 1, value: 1 },
|
|
17
|
+
{ key: 'blurEdge', label: 'Blur edge', group: 'blur', type: 'boolean', value: true },
|
|
18
|
+
{ key: 'tint', label: 'Tint', group: 'tint', type: 'color', value: { r: 255, g: 255, b: 255, a: 0 } },
|
|
19
|
+
{ key: 'shadowExpand', label: 'Shadow expand', group: 'shadow', min: 2, max: 100, step: 0.01, value: 25 },
|
|
20
|
+
{ key: 'shadowFactor', label: 'Shadow', group: 'shadow', min: 0, max: 100, step: 0.01, value: 15 },
|
|
21
|
+
{ key: 'shadowPosition', label: 'Shadow offset', group: 'shadow', type: 'vector', min: -50, max: 50, step: 0.1, value: { x: 0, y: -10 } },
|
|
22
|
+
{ key: 'borderWidth', label: 'Border width', group: 'border', min: 0, max: 4, step: 0.1, value: 0 },
|
|
23
|
+
{ key: 'borderColor', label: 'Border', group: 'border', type: 'color', value: { r: 255, g: 255, b: 255, a: 0.4 } },
|
|
24
|
+
{ key: 'borderAdapt', label: 'Fade over dark', group: 'border', type: 'boolean', value: false },
|
|
25
|
+
{ key: 'adaptTint', label: 'Tint response', group: 'adapt', min: 0, max: 100, step: 1, value: 100 },
|
|
26
|
+
{ key: 'adaptShadow', label: 'Shadow response', group: 'adapt', min: 0, max: 100, step: 1, value: 100 },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export const SHAPE_PARAMS = [
|
|
30
|
+
{ key: 'shapeWidth', label: 'Width', min: 20, max: 800, step: 1, value: 200 },
|
|
31
|
+
{ key: 'shapeHeight', label: 'Height', min: 20, max: 800, step: 1, value: 200 },
|
|
32
|
+
{ key: 'shapeRadius', label: 'Radius', min: 1, max: 100, step: 0.1, value: 80 },
|
|
33
|
+
{ key: 'shapeRoundness', label: 'Roundness', min: 2, max: 7, step: 0.01, value: 5 },
|
|
34
|
+
{ key: 'mergeRate', label: 'Merge rate', min: 0, max: 0.3, step: 0.01, value: 0.05 },
|
|
35
|
+
{ key: 'springSizeFactor', label: 'Spring size', min: 0, max: 50, step: 0.01, value: 10 },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
export const MATERIAL_GROUPS = [
|
|
39
|
+
{ key: 'shape', label: 'Shape' },
|
|
40
|
+
{ key: 'refraction', label: 'Refraction' },
|
|
41
|
+
{ key: 'fresnel', label: 'Fresnel' },
|
|
42
|
+
{ key: 'glare', label: 'Glare' },
|
|
43
|
+
{ key: 'blur', label: 'Blur' },
|
|
44
|
+
{ key: 'tint', label: 'Tint' },
|
|
45
|
+
{ key: 'shadow', label: 'Shadow' },
|
|
46
|
+
{ key: 'border', label: 'Border' },
|
|
47
|
+
{ key: 'adapt', label: 'Adapt' },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
export function defaultMaterial() {
|
|
51
|
+
const material = {}
|
|
52
|
+
for (const param of MATERIAL_PARAMS) material[param.key] = structuredClone(param.value)
|
|
53
|
+
return material
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function clampParam(param, value) {
|
|
57
|
+
if (param.type === 'boolean') return Boolean(value)
|
|
58
|
+
if (param.type === 'color') {
|
|
59
|
+
return {
|
|
60
|
+
r: Math.min(255, Math.max(0, value.r)),
|
|
61
|
+
g: Math.min(255, Math.max(0, value.g)),
|
|
62
|
+
b: Math.min(255, Math.max(0, value.b)),
|
|
63
|
+
a: Math.min(1, Math.max(0, value.a)),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (param.type === 'vector') {
|
|
67
|
+
return {
|
|
68
|
+
x: Math.min(param.max, Math.max(param.min, value.x)),
|
|
69
|
+
y: Math.min(param.max, Math.max(param.min, value.y)),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return Math.min(param.max, Math.max(param.min, value))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizeMaterial(input, defaults) {
|
|
76
|
+
const base = defaults ? structuredClone(defaults) : defaultMaterial()
|
|
77
|
+
for (const param of MATERIAL_PARAMS) if (base[param.key] === undefined) base[param.key] = structuredClone(param.value)
|
|
78
|
+
if (!input) return base
|
|
79
|
+
for (const param of MATERIAL_PARAMS) {
|
|
80
|
+
if (input[param.key] !== undefined) base[param.key] = clampParam(param, input[param.key])
|
|
81
|
+
}
|
|
82
|
+
return base
|
|
83
|
+
}
|