lgimgui
v0.1.3
Published
liquid glass immediate mode gui for the browser
Readme
lgimgui
An immediate mode GUI for the browser whose navigation layer is liquid glass. Rendering is WebGL2. The optical model is a port of liquid-glass-studio (the same parameters, ranges, and formulas), with the role presets tuned in lgwm. The composition rules come from Apple's Liquid Glass guidance and are enforced by the library rather than left to the caller.
npm install
make client # demo on http://localhost:5173
make testThe demo is a desktop: a dock, windows with sidebars and scroll edges, a merged toolbar group, menus that pop from their buttons, a slider whose thumb lifts into clear glass while held, and a material editor written with the library itself that edits every studio parameter live for each role.
Writing a frame
import { createUI } from 'lgimgui'
const ui = createUI(canvas)
const state = { volume: 0.6, wifi: true, tab: 0 }
ui.start(() => {
ui.image(wallpaper, { x: 0, y: 0, w: 'fill', h: 'fill' })
const win = ui.window({ key: 'settings', title: 'Settings', x: 80, y: 60, w: 520, h: 360 }, () => {
ui.column({ pad: 16, gap: 12 }, () => {
state.tab = ui.segmented(['General', 'Display'], state.tab, { key: 'tab' })
state.volume = ui.slider(state.volume, { key: 'volume' })
state.wifi = ui.toggle(state.wifi, { key: 'wifi' })
if (ui.button('Done', { primary: true })) close()
})
})
ui.glass({ key: 'dock', role: 'dock', shape: 'pill', direction: 'row', gap: 8, pad: 8, y: 800, alignSelf: 'center' }, () => {
if (ui.button(null, { key: 'mail', icon: 'grid' })) openMail()
})
})Every call records a node for this frame. After the frame callback returns the tree is measured and positioned, painted into strata, rendered, and hit tested for the next frame. Widget state (springs, scroll offsets, carets, menu open flags) lives in a store keyed by id and is collected when the id stops appearing.
Layout
Containers take direction (row, column, stack), gap, pad, align (start, center, end, stretch), justify (start, center, end, between). Children size with w/h as a number, 'fill', or omitted for intrinsic size, plus fr to share leftover space. Setting x or y positions a child absolutely inside its parent; alignSelf aligns the other axis.
Ids
Ids are paths built from the enclosing containers and the widget label. Pass key when labels repeat or change. Repeated keys in one scope are disambiguated in order, so a list of identical buttons still works as long as its order is stable.
Glass
ui.glass(props, fn) draws one glass element and lays out its children as overlays on it.
role:window,pane,toolbar,menu,control,thumb,dock,clear,thin,studio. A role is a material preset; every preset exposes all studio parameters inui.materials[role], with independentunfocusedanddarkvariants.variant:regular(default) adapts to its backdrop;clearnever adapts, always dims, and forces bright foreground.shape:pill,circle, or a numericradiuswithroundness(2 is round, 5 is the squircle default).merge: trueturns each direct child into a shape of one smooth-min union withmergeRatepixels of bridging.focused: falseswitches to the unfocused material so a whole hierarchy recedes together.tint: [r, g, b, a]applies a semantic tint mapped to backdrop brightness.surface:smallflips light and dark with the backdrop;largeholds polarity.lift: truelets a transient element render as glass while inside glass.
Controls called inside glass paint as fills, vibrancy, and strokes on that glass. Controls called outside glass bring their own glass backing in the control role. Nesting ui.glass inside ui.glass without lift throws.
Widgets
text, label, heading, icon, image, rect, separator, spacer, box, row, column, stack, scroll, popup, button, iconButton, capsule, toolbar, trafficLights, toggle, slider, segmented, progress, item, menu, menuButton, textInput, pane, window, dragHandle, hitArea.
Interactive widgets return their result immediately: button returns whether it was clicked, slider and toggle return the new value, menuButton returns the chosen item, window returns { closed, moved, dx, dy, focusRequested, minimized, zoomed }.
scroll accepts edge: { size } to fade its content out at the edges; the content's own pixels become transparent, nothing is painted over them.
Windows and the desktop
ui.desktop() returns a window manager. Register each window every frame with desktop.window(key, spec, fn) and call desktop.draw() once; the manager owns geometry (with cascade placement for new keys), z-order, focus (a press on nothing unfocuses everything), drag, resize, close, minimize, and zoom. spec takes title or a toolbar callback, sidebar and sidebarWidth, w, h, optional x, y, open: false to start hidden, minSize, and resizable. desktop.open(key), close, toggle, isOpen, and focused drive it from the outside (a dock, a menu).
const desktop = ui.desktop()
ui.start(() => {
desktop.window('sliders', { title: 'Sliders', w: 360, h: 200 }, () => {
state.volume = ui.slider(state.volume, { key: 'volume' })
})
desktop.draw()
})ui.window underneath composes the sheet: traffic lights at the corner, a full-height pane for the sidebar, a toolbar band (also the drag region), and the content below. Buttons and capsules inside a toolbar take the toolbar material; everything else inside a window is a fill.
Settings
ui.settings.reduceTransparency, ui.settings.increaseContrast, and ui.settings.reduceMotion are material modifiers applied everywhere.
How rendering works
Each frame is a list of strata. Stratum 0 is the content layer. A glass element samples the completed stratum beneath it and writes itself and its overlays into the stratum above. Glass elements that overlap earlier glass are pushed up a stratum automatically, so a higher window sees a lower window only as flattened pixels and two independent windows never refract each other. Popups and lifted elements follow the same rule.
Per stratum the renderer draws the content, the shadows of the glass above (into the backdrop, so the rim refracts them like the studio does), builds a gaussian mip pyramid of the result, and moves on. Blur radius selects a fractional pyramid level, so every element can carry its own blur without extra passes. A tiny level of each stratum's pyramid is read back asynchronously every frame and feeds the adaptation of small Regular glass sitting on it: polarity with hysteresis, shadow depth over detail, and tint opacity.
Text is rasterized into a glyph atlas at device pixel ratio and drawn sharp above the glass. UI fills, strokes, images, glyphs, and gradients go through one instanced batch.
Project layout
src/index.js createUI
src/core/ ids, state, springs, input, layout, frame driver, theme
src/material/ studio parameter schema, role presets, material transforms, backdrop adaptation, optics
src/render/ WebGL2 context, batch, glyph atlas, blur pyramid, probe, renderer, shaders
src/widgets/ glass, primitives, controls, composite widgets, desktop manager
demo/ the desktop demo, material editor, and showcase windows
test/ vitest suites for the pure modulesui.glass({ ..., shadow: false }) draws a shape without its material's drop shadow.
