makepad/widgets/sheets/themes/lacquer-red/widgets.splash
Admin 53e0e85b92 draw and widgets: Slug text everywhere, no blank glyphs, slimmer widgets
All outline text draws with Slug curves on every platform (SDF/MSDF stays
selectable per size); a frame never presents a blank glyph and retained draw
lists keep theirs. One coverage model for vector feathering, widget families
split out of the core crate, fewer steady-state allocations, video keeps its
aspect (Contain/Cover), render and SSAO pass unification.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

3731 lines
213 KiB
Text

use mod.prelude.widgets_internal.*
use mod.sdf.Cap
// Candy-red lacquer and chrome. Every face here is drawn in true points (one device
// pixel is `px`), and every shadow, lip and glow is drawn OUTSIDE the layout
// box: the quad is grown in `vertex`, the templates that throw a shadow stop
// clipping, and no padding is spent on relief.
// ---------------------------------------------------------------- helpers
// One device pixel in points.
let lq_px = fn(dpi: float) -> float {
return 1.0 / max(dpi, 0.5)
}
// An edge rounded to a whole device pixel in window space, handed back in
// box space: a hairline on it lands on one pixel row.
let lq_snap = fn(v: float, o: float, px: float) -> float {
return Finish.snap(o + v, px) - o
}
// Distance from p to the segment a..b, in points.
let lq_seg = fn(p: vec2, a: vec2, b: vec2) -> float {
let pa = p - a
let ba = b - a
let t = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0)
return length(pa - ba * t)
}
// A value picked by material: 0 red lacquer, 1 smoked chrome, 2 polished
// chrome.
let lq_pick = fn(mat: float, a: vec3, b: vec3, k: vec3) -> vec3 {
let m1 = step(0.5, mat) * (1.0 - step(1.5, mat))
let m2 = step(1.5, mat)
return mix(mix(a, b, m1), k, m2)
}
// A raised cap of lacquer or chrome: the drop and contact shadows under it,
// the dark outline, a face whose upper part carries the gloss down to a soft
// horizon, the top edge line, a chrome catch-light along the foot, and the
// reflected red lifting the lowest rows. `lift` 1 casts the drop shadow, 0
// seats the cap on the ground with the contact shadow only. `rim` is the
// foot rim's ink where a lamp spills on it. `gloss` 1 is the full lacquer
// sheen, 0 a matte face. `mat` picks the material. Premultiplied.
let lq_capm = fn(p: vec2, lo: vec2, hi: vec2, r: float, px: float, hover: float, down: float, disabled: float, focus: float, lift: float, tint: vec3, rim: vec4, gloss: float, mat: float) -> vec4 {
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
let hh = hi.y - lo.y
let sh_h = min(hh, 24.0)
let d = Material.sd_box(p, c, h, r)
let up = (1.0 - down) * (1.0 - disabled)
// What the cap throws on the lacquer: a firm drop close under it and a
// soft tail, the sides gone within a point or two.
let drop1 = Finish.drop(p, c, h - vec2(0.05 * sh_h, 0.0), r, vec2(0.0, 0.10 * sh_h), 0.06 * sh_h) * 0.75
let drop2 = Finish.drop(p, c, h - vec2(0.1 * sh_h, 0.04 * sh_h), r, vec2(0.0, 0.16 * sh_h), 0.11 * sh_h) * 0.30
let drop = (1.0 - (1.0 - drop1) * (1.0 - drop2)) * lift * up
let contact = Finish.drop(p, c, h, r, vec2(0.0, px), px * 0.6) * 0.85 * (1.0 - 0.6 * disabled)
let sh = 1.0 - (1.0 - drop) * (1.0 - contact)
var under = vec4(0.03, 0.0, 0.0, 1.0) * sh
// A held cap sits in the lacquer: a 1 dp chrome lip shows on the ground
// right under its lower edge.
let lip = Finish.ring_out(d, 0.0, px, px) * step(c.y + h.y * 0.5, p.y) * down * (1.0 - disabled)
under = Finish.over(under, vec4(0.62, 0.44, 0.46, 1.0) * lip * 0.8)
let t = clamp((p.y - lo.y) / max(hh, 1.0), 0.0, 1.0)
let uv = clamp((p - lo) / max(hi - lo, vec2(1.0, 1.0)), vec2(0.0, 0.0), vec2(1.0, 1.0))
let small = 1.0 - step(23.5, hh)
let top_c = lq_pick(mat, vec3(0.860, 0.500, 0.520), vec3(0.560, 0.560, 0.585), vec3(0.900, 0.895, 0.910))
let hor_c = lq_pick(mat, vec3(0.660, 0.235, 0.255), vec3(0.330, 0.330, 0.352), vec3(0.640, 0.630, 0.655))
let low_c = lq_pick(mat, vec3(0.560, 0.120, 0.138), vec3(0.205, 0.200, 0.218), vec3(0.260, 0.240, 0.255))
let foot_c = lq_pick(mat, vec3(0.420, 0.085, 0.100), vec3(0.150, 0.140, 0.155), vec3(0.560, 0.470, 0.490))
let line_c = lq_pick(mat, vec3(0.930, 0.760, 0.770), vec3(0.760, 0.760, 0.780), vec3(1.000, 1.000, 1.000))
let rim_c = lq_pick(mat, vec3(0.780, 0.660, 0.680), vec3(0.600, 0.580, 0.600), vec3(0.930, 0.900, 0.915))
let ol_c = lq_pick(mat, vec3(0.110, 0.016, 0.020), vec3(0.055, 0.045, 0.050), vec3(0.120, 0.090, 0.100))
// The gloss: a lighter upper band whose lower edge is a soft horizon a
// little under the middle, rising a touch toward the sides.
let sx = uv.x * 2.0 - 1.0
let hz = 0.47 - 0.06 * sx * sx
let band = 1.0 - smoothstep(hz - 0.06, hz + 0.02, t)
let upper = mix(top_c, hor_c, pow(clamp(t / max(hz, 0.01), 0.0, 1.0), 0.75))
let lower = mix(low_c, foot_c, smoothstep(hz, 1.0, t))
let matte = mix(mix(hor_c, low_c, 0.55), foot_c, t)
var face = mix(matte, mix(lower, upper, band), gloss) + tint
// The reflected light: the lowest rows lift toward the chrome catch.
face = mix(face, rim_c, 0.30 * smoothstep(0.70, 1.0, t) * (1.0 - disabled))
// A window of light held in the gloss: a soft bright streak near the top.
let wy = (t - 0.14) / 0.09
face = mix(face, line_c, 0.20 * exp(-wy * wy) * gloss * up)
// Under the hand the gloss wakes: the face lifts and the streak grows.
face = face + mix(vec3(0.050, 0.022, 0.024), vec3(0.04, 0.04, 0.042), step(0.5, mat)) * (1.0 + small) * hover * up
face = mix(face, line_c, 0.10 * exp(-wy * wy) * gloss * hover * up)
// Held or chosen: sunk. The gloss goes out, the floor falls dark and a
// shade lies under the top edge for a fifth of the height.
let shade = 0.55 * (1.0 - smoothstep(0.0, 0.22 * min(hh, 24.0), p.y - lo.y - px))
let held = mix(mix(low_c, foot_c, t) * 0.78 + tint, vec3(0.0, 0.0, 0.0), shade)
face = mix(face, held, down * (1.0 - disabled))
// Disabled: the lacquer dulls, grey drawn into it.
let fl = Finish.lum(face)
face = mix(face, mix(face, vec3(fl, fl, fl), 0.45) * 0.8, disabled)
// Which way the outline faces here, so a round cap gets an arc where a
// square one gets a straight line.
let s = vec2(step(0.0, p.x - c.x) * 2.0 - 1.0, step(0.0, p.y - c.y) * 2.0 - 1.0)
let q = abs(p - c) - (h - vec2(r, r))
let qc = max(q, vec2(0.0, 0.0))
var n = vec2(0.0, s.y)
if q.x > q.y {
n = vec2(s.x, 0.0)
}
if q.x > 0.0 && q.y > 0.0 {
n = qc / max(length(qc), 0.0001) * s
}
let inner = Finish.band(d, px, px * 2.5, px)
// The top edge line, bright on the gloss, brighter under the hand.
face = mix(face, line_c, inner * smoothstep(0.5, 0.97, -n.y) * (0.75 + 0.2 * hover) * up)
// The chrome bezel all round: bright along the top, pale pink-grey down
// the sides, the brightest catch along the foot.
let bez = mix(line_c, rim_c, smoothstep(-0.7, 0.3, n.y))
let side = inner * smoothstep(0.7, 1.0, abs(n.x))
face = mix(face, bez, 0.75 * side * (1.0 - 0.6 * disabled) * (1.0 - 0.4 * down))
// The foot: the chrome catch-light, and a lamp's spill where there is one.
let foot = inner * smoothstep(0.5, 0.97, n.y)
face = mix(face, rim_c, foot * 0.9 * (1.0 - 0.6 * disabled) * (1.0 - 0.5 * down))
face = mix(face, rim.rgb, foot * rim.a * (1.0 - disabled))
face = mix(face, mix(ol_c, vec3(0.20, 0.10, 0.11), disabled), Finish.band(d, 0.0, px, px))
let a = Finish.cover(d, px)
var o = Finish.over(under, vec4(face * a, a))
// Focus: a thin ice-blue ring just outside the outline, and a breath of it.
let ring = Finish.ring_out(d, 0.0, px, px) * 0.9 + Finish.glow(d - px, 0.7) * 0.35 * (1.0 - Finish.cover(d, px))
o = Finish.over(o, vec4(0.498, 0.706, 0.910, 1.0) * clamp(ring, 0.0, 1.0) * focus * (1.0 - disabled))
return o
}
// The red lacquer cap.
let lq_cap = fn(p: vec2, lo: vec2, hi: vec2, r: float, px: float, hover: float, down: float, disabled: float, focus: float, lift: float, tint: vec3, rim: vec4, gloss: float) -> vec4 {
return lq_capm(p, lo, hi, r, px, hover, down, disabled, focus, lift, tint, rim, gloss, 0.0)
}
// The smoked-chrome cap: the scroll thumb.
let lq_smoke = fn(p: vec2, lo: vec2, hi: vec2, r: float, px: float, hover: float, down: float, disabled: float, focus: float, lift: float) -> vec4 {
return lq_capm(p, lo, hi, r, px, hover, down, disabled, focus, lift, vec3(0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0), 1.0, 1.0)
}
// The polished-chrome cap: check boxes, thumbs, the fader cap.
let lq_chrome = fn(p: vec2, lo: vec2, hi: vec2, r: float, px: float, hover: float, down: float, disabled: float, focus: float, lift: float) -> vec4 {
return lq_capm(p, lo, hi, r, px, hover, down, disabled, focus, lift, vec3(0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0), 1.0, 2.0)
}
// Brushed steel: the choice caps, segments, tabs, drop-downs, the chip and
// the badge. A raised cap of light steel with fine horizontal streaks (read
// in window space `q`, so neighbours share one brushing), a broad soft
// highlight across its upper part, a crisp light line on the top edge, a
// darker foot warmed by the red lacquer it stands on, and the thin dark
// outline; its labels are dark. Held (`down`) it sinks: the steel falls a
// step, a shade lies under the top edge and down the walls, and a light lip
// catches along the foot. `deep` 1 is the chosen answer: a deeper, cooler
// well whose label is lit. `tint` shades the steel toward a role colour.
// Premultiplied.
let lq_steel_tone = fn(t: float, sx: float, q: vec2, px: float) -> float {
let hl = exp(-((t - 0.26) / 0.20) * ((t - 0.26) / 0.20))
var l = mix(0.710, 0.585, smoothstep(0.42, 0.60, t))
l = mix(l, 0.455, smoothstep(0.76, 1.0, t))
l = l + 0.175 * hl * (1.0 - 0.30 * sx * sx)
// Fine streaks and a coarser, slower brushing under them.
let s1 = Finish.brushed(q, vec2(1.0, 0.0), px * 1.2) - 0.5
let s2 = Finish.brushed(q + vec2(31.0, 7.0), vec2(1.0, 0.0), px * 4.5) - 0.5
return l + s1 * 0.075 + s2 * 0.045
}
let lq_steel = fn(p: vec2, lo: vec2, hi: vec2, r: float, px: float, o: vec2, hover: float, down: float, deep: float, disabled: float, focus: float, lift: float, tint: vec3) -> vec4 {
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
let hh = hi.y - lo.y
let sh_h = min(hh, 24.0)
let d = Material.sd_box(p, c, h, r)
let up = (1.0 - down) * (1.0 - disabled)
let drop1 = Finish.drop(p, c, h - vec2(0.05 * sh_h, 0.0), r, vec2(0.0, 0.10 * sh_h), 0.06 * sh_h) * 0.75
let drop2 = Finish.drop(p, c, h - vec2(0.1 * sh_h, 0.04 * sh_h), r, vec2(0.0, 0.16 * sh_h), 0.11 * sh_h) * 0.30
let drop = (1.0 - (1.0 - drop1) * (1.0 - drop2)) * lift * up
let contact = Finish.drop(p, c, h, r, vec2(0.0, px), px * 0.6) * 0.85 * (1.0 - 0.6 * disabled)
let sh = 1.0 - (1.0 - drop) * (1.0 - contact)
var under = vec4(0.03, 0.0, 0.0, 1.0) * sh
let lip = Finish.ring_out(d, 0.0, px, px) * step(c.y + h.y * 0.5, p.y) * down * (1.0 - disabled)
under = Finish.over(under, vec4(0.62, 0.44, 0.46, 1.0) * lip * 0.8)
let t = clamp((p.y - lo.y) / max(hh, 1.0), 0.0, 1.0)
let uv = clamp((p - lo) / max(hi - lo, vec2(1.0, 1.0)), vec2(0.0, 0.0), vec2(1.0, 1.0))
let sx = uv.x * 2.0 - 1.0
let q = p + o
// The raised face.
let l = lq_steel_tone(t, sx, q, px) + 0.065 * hover * up
var face = vec3(l, l, l) * vec3(0.972, 0.982, 1.0)
// The red lacquer mirrored in the foot.
face = mix(face, face * vec3(1.10, 0.84, 0.86), 0.55 * smoothstep(0.66, 1.0, t))
// The sunk face: the same steel a step or two down, cooler when chosen,
// a shade under the top edge and along both walls.
let s1 = Finish.brushed(q, vec2(1.0, 0.0), px * 1.2) - 0.5
let s2 = Finish.brushed(q + vec2(31.0, 7.0), vec2(1.0, 0.0), px * 4.5) - 0.5
let wl = mix(mix(0.590, 0.330, deep), mix(0.520, 0.265, deep), t) + (s1 * 0.060 + s2 * 0.035) * mix(1.0, 0.7, deep) + 0.02 * hover
var well = vec3(wl, wl, wl) * mix(vec3(0.985, 0.975, 0.985), vec3(0.90, 0.955, 1.04), deep)
let top_sh = exp(-max(p.y - lo.y - px, 0.0) / (0.16 * sh_h))
let wall = exp(-max(min(p.x - lo.x, hi.x - p.x) - px, 0.0) / (2.5 * px))
well = mix(well, vec3(0.0, 0.0, 0.0), clamp(0.62 * top_sh + 0.40 * wall, 0.0, 0.85))
face = mix(face, well, down * (1.0 - disabled)) + tint
// Disabled: the steel dims and flattens.
let fl = Finish.lum(face)
face = mix(face, mix(face, vec3(fl, fl, fl), 0.5) * 0.66, disabled)
let s = vec2(step(0.0, p.x - c.x) * 2.0 - 1.0, step(0.0, p.y - c.y) * 2.0 - 1.0)
let qq = abs(p - c) - (h - vec2(r, r))
let qc = max(qq, vec2(0.0, 0.0))
var n = vec2(0.0, s.y)
if qq.x > qq.y {
n = vec2(s.x, 0.0)
}
if qq.x > 0.0 && qq.y > 0.0 {
n = qc / max(length(qc), 0.0001) * s
}
let inner = Finish.band(d, px, px * 2.0, px)
// The crisp light line along the top edge, brighter under the hand.
let topl = inner * smoothstep(0.5, 0.97, -n.y)
face = mix(face, vec3(0.985, 0.985, 1.0), topl * (0.85 + 0.15 * hover) * up)
face = mix(face, vec3(0.80, 0.80, 0.82), topl * 0.35 * (1.0 - down) * disabled)
// The sides: a soft light bevel on a raised cap.
let side = inner * smoothstep(0.7, 1.0, abs(n.x))
face = mix(face, vec3(0.90, 0.90, 0.92), 0.35 * side * up)
// The foot: the machined edge catches a warm light under the darker foot;
// on a sunk face the lip is the light caught inside the well's foot.
let foot = inner * smoothstep(0.5, 0.97, n.y)
face = mix(face, vec3(0.80, 0.70, 0.72), foot * 0.55 * up)
face = mix(face, mix(vec3(0.62, 0.60, 0.63), vec3(0.50, 0.62, 0.74), deep), foot * 0.7 * down * (1.0 - disabled))
face = mix(face, mix(vec3(0.070, 0.030, 0.036), vec3(0.20, 0.12, 0.13), disabled), Finish.band(d, 0.0, px, px))
let a = Finish.cover(d, px)
var res = Finish.over(under, vec4(face * a, a))
let ring = Finish.ring_out(d, 0.0, px, px) * 0.9 + Finish.glow(d - px, 0.7) * 0.35 * (1.0 - Finish.cover(d, px))
res = Finish.over(res, vec4(0.498, 0.706, 0.910, 1.0) * clamp(ring, 0.0, 1.0) * focus * (1.0 - disabled))
return res
}
// A polished chrome ball: radios, slider handles. The ball mirrors a bright
// sky above, a dark horizon a little under its middle and the red lacquer
// below, brightening toward its rim; a hot specular up and to the left; a
// dark outline and a soft shadow on the ground. `held` darkens it and sinks
// its shadow. Premultiplied over `dst`.
let lq_ball = fn(dst: vec4, p: vec2, c: vec2, rad: float, px: float, hover: float, held: float, disabled: float, lift: float) -> vec4 {
let dd = length(p - c) - rad
let sh1 = Finish.drop(p, c, vec2(rad * 0.8, rad * 0.8), rad * 0.8, vec2(0.0, rad * 0.30), rad * 0.22) * 0.70
let sh2 = Finish.drop(p, c, vec2(rad * 0.7, rad * 0.7), rad * 0.7, vec2(0.0, rad * 0.55), rad * 0.45) * 0.30
let sh = (1.0 - (1.0 - sh1) * (1.0 - sh2)) * lift * (1.0 - 0.5 * held) * (1.0 - 0.6 * disabled)
var res = Finish.over(dst, vec4(0.03, 0.0, 0.0, 1.0) * sh)
let q = (p - c) / max(rad, 0.5)
let rl = min(length(q), 1.0)
let ny = q.y
let sky = mix(vec3(0.98, 0.97, 0.98), vec3(0.66, 0.64, 0.67), smoothstep(-0.95, -0.05, ny))
let gnd = mix(vec3(0.40, 0.17, 0.19), vec3(0.86, 0.66, 0.68), smoothstep(0.35, 0.98, rl) * smoothstep(0.1, 0.8, ny))
var col = mix(sky, vec3(0.17, 0.12, 0.13), smoothstep(-0.08, 0.16, ny))
col = mix(col, gnd, smoothstep(0.16, 0.45, ny))
let sp = q - vec2(-0.32, -0.40)
col = col + vec3(1.0, 1.0, 1.0) * exp(-dot(sp, sp) / (2.0 * 0.11 * 0.11)) * 0.75
col = mix(col, vec3(0.12, 0.08, 0.09), smoothstep(0.80, 1.0, rl) * 0.55)
col = col * (1.0 + 0.16 * hover) * (1.0 - 0.30 * min(held, 1.2))
// Under the hand a pale ice rim light wakes round the ball's edge.
col = mix(col, vec3(0.80, 0.90, 1.0), smoothstep(0.72, 0.95, rl) * 0.45 * hover * (1.0 - disabled))
let fl = Finish.lum(col)
col = mix(col, mix(vec3(fl, fl, fl), vec3(0.45, 0.25, 0.27), 0.4) * 0.75, disabled)
col = mix(col, vec3(0.10, 0.05, 0.06), Finish.band(dd, 0.0, px, px))
let a = Finish.cover(dd, px)
return Finish.over(res, vec4(col * a, a))
}
// An LED bar or dot laid over `dst`: a white-hot core, a 1 dp saturated rim
// and a short halo when lit (`halo` scales it; 0 for a light that shows a
// value); a dark lens when not (a bar: one dark glass). `hs` is the half
// size; the ends are round.
let lq_led = fn(dst: vec4, p: vec2, c: vec2, hs: vec2, lit: float, halo: float, px: float, core: vec3, rimc: vec3) -> vec4 {
let d = Finish.sd_pill(p, c, hs)
let slot = Finish.cover(d, px)
// Unlit: a dark lens, not a hole -- a black rim, near-black glass that
// catches a little of the light on its upper half, and on a dot large
// enough to show it a small catch-light up and to the left.
let tl = clamp((p.y - (c.y - hs.y)) / max(2.0 * hs.y, px), 0.0, 1.0)
var lens = mix(vec3(0.165, 0.176, 0.200), vec3(0.071, 0.078, 0.090), smoothstep(0.2, 0.9, tl))
let sp = p - (c + vec2(-0.3 * hs.x, -0.35 * hs.y))
lens = lens + vec3(0.16, 0.17, 0.19) * exp(-dot(sp, sp) / max(2.0 * 0.09 * hs.y * hs.y, 0.001)) * step(1.5, hs.y)
// A bar has no room for a rim, a light row and a catch-light: unlit it
// is one glass of three device pixels, falling #1e2127 -> #15171a with a
// faint cast of the lamp's blue, its top row lifted 0.03.
let bar = step(hs.y * 2.5, hs.x)
let gc = c + vec2(0.0, px * 0.5)
let gd = Finish.sd_pill(p, gc, vec2(hs.x + px * 0.5, px * 1.5))
let gt = clamp((p.y - (gc.y - px * 1.5)) / (px * 3.0), 0.0, 1.0)
var glass = mix(vec3(0.118, 0.129, 0.153), vec3(0.082, 0.090, 0.102), gt)
glass = mix(glass, vec3(0.40, 0.62, 0.84), 0.08) + vec3(0.03, 0.03, 0.03) * (1.0 - step(gc.y - px * 0.5, p.y))
var o = Finish.over(dst, vec4(0.024, 0.024, 0.031, 1.0) * Finish.ring_out(d, 0.0, px, px) * 0.55 * (1.0 - lit) * (1.0 - bar))
o = Finish.over(o, vec4(lens, 1.0) * slot * (1.0 - lit) * (1.0 - bar))
o = Finish.over(o, vec4(glass, 1.0) * Finish.cover(gd, px) * (1.0 - lit) * bar)
// Lit: the halo first, under the core. Two falls: a thin one that is
// gone in a device pixel or two, and a wide weak one that lies a little
// light on the floor round the lamp.
let x = max(d - px, 0.0)
let g = (0.45 * exp(-(x * x) / (2.0 * 0.5 * 0.5)) + 0.22 * exp(-(x * x) / (2.0 * 1.4 * 1.4))) * (1.0 - slot)
o = Finish.over(o, vec4(rimc, 1.0) * g * 0.8 * halo * lit)
// The saturated rim, one device pixel round the core.
let rim = Finish.ring_out(d, 0.0, px, px)
o = Finish.over(o, vec4(rimc, 1.0) * rim * lit)
// A hot top on the core.
let hot = 1.0 - smoothstep(0.0, hs.y * 1.2, p.y - (c.y - hs.y))
let col = mix(core, vec3(1.0, 1.0, 1.0), hot * 0.5)
o = Finish.over(o, vec4(col, 1.0) * slot * lit)
return o
}
// The lamp under a chosen label -- the chosen segment's, the tab in use's,
// the Primary's -- counted in whole device pixels. Its core is two rows
// between `xs.x` and `xs.y` (snapped), from the row at `y0` (snapped): the
// top row `core`, the lower one tinted toward `glow`, and one light-blue
// column at either end. Round it the halo lies on the floor in two layers:
// a tight one -- a pale row just above the core (brightest over its
// middle), a blue row under it, two blue-grey rows fading after that, and
// at the ends a pale fall gone three columns out -- over a soft wide one
// that rises three rows above the core, lies faintly along its sides and
// runs on down to the foot of the well. What lies above the core stays
// under an eighth of its light past the pale row, so a descender keeps its
// well. `halo` scales the light round the core; premultiplied over `dst`.
let lq_lamp = fn(dst: vec4, p: vec2, xs: vec2, y0: float, px: float, lit: float, halo: float, core: vec3, glow: vec3) -> vec4 {
// Which row from the core's top, and how many columns past its ends
// (0 inside, 0.5 on the first column out).
let k = floor((p.y - y0) / px)
let e = max(max(xs.x - p.x, p.x - xs.y), 0.0) / px
let hw = max((xs.y - xs.x) * 0.5, px)
let dx = clamp(abs(p.x - (xs.x + xs.y) * 0.5) / hw, 0.0, 1.0)
let pale = mix(glow, core, 0.5)
let dim = mix(glow, vec3(0.56, 0.58, 0.64), 0.45)
let m4 = step(-4.5, k) * step(k, -3.5)
let m3 = step(-3.5, k) * step(k, -2.5)
let m2 = step(-2.5, k) * step(k, -1.5)
let m1 = step(-1.5, k) * step(k, -0.5)
let body = step(-0.5, k) * step(k, 1.5)
let p1 = step(1.5, k) * step(k, 2.5)
let p2 = step(2.5, k) * step(k, 3.5)
let p3 = step(3.5, k) * step(k, 4.5)
let p4 = step(4.5, k) * step(k, 5.5)
let p5 = step(5.5, k) * step(k, 6.5)
// The soft wide layer.
let wide = exp(-(e * e) / (2.0 * 3.4 * 3.4)) * (1.0 - 0.3 * dx * dx)
let wa = (0.12 * m4 + 0.22 * m3 + 0.32 * m2 + 0.15 * m1 + 0.20 * body + 0.08 * (p1 + p2 + p3) + 0.25 * p4 + 0.15 * p5) * wide
var o = Finish.over(dst, vec4(dim, 1.0) * clamp(wa * halo, 0.0, 1.0) * lit)
// The tight layer.
let top = exp(-(e * e) / (2.0 * 1.6 * 1.6)) * (1.0 - 0.3 * dx * dx)
let side = exp(-(e * e) / (2.0 * 2.2 * 2.2))
let ce = max(e - 1.5, 0.0)
let ends = 0.5 * exp(-(ce * ce) / (2.0 * 0.75 * 0.75)) * step(1.0, e) * (1.0 - 0.4 * step(0.5, k))
var a = 0.72 * top * m1 + ends * body
a = a + (0.50 * p1 + 0.46 * p2 + 0.36 * p3 + 0.10 * p4) * side
let hc = mix(mix(pale, glow, p1), dim, p2 + p3 + p4)
o = Finish.over(o, vec4(hc, 1.0) * clamp(a * halo, 0.0, 1.0) * lit)
// The core and its two end columns; the lower row takes a little of
// the glow's own hue.
let hue = glow / max(max(glow.x, glow.y), max(glow.z, 0.001))
let lower = core * mix(vec3(1.0, 1.0, 1.0), hue, 0.35)
let inside = 1.0 - step(0.25, e)
let endc = step(0.25, e) * (1.0 - step(1.0, e))
let cc = mix(mix(core, lower, step(0.5, k)), mix(core, glow, 0.4), endc)
return Finish.over(o, vec4(cc, 1.0) * (inside + 0.95 * endc) * body * lit)
}
// The three-quarter arc a waiting button turns in place of its label.
let lq_spinner = fn(dst: vec4, p: vec2, c: vec2, rad: float, t: float, amt: float, stroke: float) -> vec4 {
let sdf = Sdf2d.viewport(p)
let a0 = t * 2.0 * PI
sdf.arc_round_caps(c.x, c.y, rad, a0, a0 + 1.5 * PI, stroke)
sdf.fill(vec4(0.94, 0.98, 1.0, amt))
return Finish.over(dst, sdf.result)
}
// ---------------------------------------------------------------- buttons
// Every button is the same slim cap: 24 points on a 13 point line, 12
// points either side. Its shadow falls outside the box, so the box is the
// outline and nothing is paid in padding.
mod.widgets.ButtonFlat.padding = mod.turtle.Inset{left: 12.0 right: 12.0 top: 3.0 bottom: 3.0}
mod.widgets.ButtonFlat.min_height = 24.0
// Glyphs sit at 14 points so an icon never makes a cap taller than the row;
// an icon alone gets a square cap.
mod.widgets.ButtonFlat.icon_walk.width = 14.0
mod.widgets.ButtonIcon.padding = mod.turtle.Inset{left: 5.0 right: 5.0 top: 3.0 bottom: 3.0}
mod.widgets.ButtonFlatIcon.padding = mod.turtle.Inset{left: 5.0 right: 5.0 top: 3.0 bottom: 3.0}
mod.widgets.ButtonFlatterIcon.padding = mod.turtle.Inset{left: 5.0 right: 5.0 top: 3.0 bottom: 3.0}
// The caps that carry a lamp lift their label one device pixel, so the lamp
// can stand two rows off the bottom rim and still clear the descenders.
mod.widgets.ButtonPrimary.padding = mod.turtle.Inset{left: 12.0 right: 12.0 top: 2.3333 bottom: 3.6667}
mod.widgets.ButtonSecondary.padding = mod.turtle.Inset{left: 12.0 right: 12.0 top: 2.3333 bottom: 3.6667}
mod.widgets.ButtonDanger.padding = mod.turtle.Inset{left: 12.0 right: 12.0 top: 2.3333 bottom: 3.6667}
// The short rungs keep their own heights under the row minimum.
mod.widgets.ButtonXs.min_height = 0.0
mod.widgets.ButtonSm.min_height = 0.0
// The smallest rung's label one step down, so its cap height is a third of
// its cap like every other rung's.
mod.widgets.ButtonXs.draw_text.text_style.font_size = 6.5
mod.widgets.ButtonFlat.draw_bg.border_radius = uniform(1.5)
mod.widgets.ButtonFlat.draw_bg.border_size = uniform(0.34)
mod.widgets.ButtonFlat.clip_x = false
mod.widgets.ButtonFlat.clip_y = false
mod.widgets.ButtonFlat.draw_bg.vertex = fn() {
let m = 7.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
// The cap and its states, shared by every button template; `kind` picks the
// variant: 0 seated cap, 1 raised cap, 2 primary (lit LED), 3 secondary (dark
// LED slot), 4 tonal (tinted face), 5 danger (red LED), 6 outline, 7 flatter.
let lq_button = fn(pos: vec2, size: vec2, o: vec2, dpi: float, hover: float, down: float, disabled: float, focus: float, loading: float, anim: float, kind: float, dash: float) -> vec4 {
let m = 7.0
let px = lq_px(dpi)
let p = pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(size.x, o.x, px), lq_snap(size.y, o.y, px))
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
let r = 3.0
var res = vec4(0.0, 0.0, 0.0, 0.0)
if kind > 5.5 {
// Outline: no face, the black ring cut into the ground with its lip;
// under the hand a faint cap shows, held it sinks.
let d = Material.sd_box(p, c, h, r)
if kind > 6.5 {
// Flatter: nothing at rest.
if hover + down + focus > 0.001 {
res = lq_cap(p, lo, hi, r, px, hover, down, disabled, 0.0, 0.0, vec3(0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0), 1.0) * clamp(max(hover, down), 0.0, 1.0)
}
} else {
var ring = Finish.band(d, 0.0, px, px)
if dash > 0.0 {
ring = ring * step(0.5, fract((p.x + p.y) / (dash * 2.0)))
}
let lip = Finish.ring_out(d, 0.0, px, px) * step(c.y + h.y * 0.5, p.y)
let inner = Finish.band(d, px, px * 2.0, px) * step(p.y, c.y - h.y * 0.5)
// The ring is chrome: a pale bezel line with the dark cut inside
// it and the chrome lip on the ground under the foot.
res = vec4(0.70, 0.52, 0.54, 1.0) * lip * 0.9
res = Finish.over(res, vec4(0.0, 0.0, 0.0, 1.0) * inner * 0.35)
let chrome = mix(vec3(0.80, 0.62, 0.64), vec3(0.50, 0.34, 0.36), clamp((p.y - lo.y) / max(hi.y - lo.y, 1.0), 0.0, 1.0))
res = Finish.over(res, vec4(chrome, 1.0) * ring)
res = Finish.over(res, vec4(0.110, 0.016, 0.020, 1.0) * Finish.band(d, px, px * 2.0, px))
if hover + down > 0.001 {
res = Finish.over(res, lq_cap(p, lo, hi, r, px, hover, down, disabled, 0.0, 0.0, vec3(0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0), 1.0) * clamp(max(hover, down), 0.0, 1.0) * 0.85)
}
}
let fr = Finish.ring_out(d, 0.0, px, px) * 0.9 + Finish.glow(d - px, 0.7) * 0.3 * (1.0 - Finish.cover(d, px))
res = Finish.over(res, vec4(0.40, 0.62, 0.84, 1.0) * clamp(fr, 0.0, 1.0) * focus * (1.0 - disabled))
} else {
var tint = vec3(0.0, 0.0, 0.0)
var rim = vec4(0.0, 0.0, 0.0, 0.0)
let lit = 1.0 - disabled
if kind > 3.5 && kind < 4.5 {
// The tonal cap: the lacquer a breath deeper and cooler.
tint = vec3(-0.035, -0.006, 0.010)
}
if kind > 1.5 && kind < 2.5 {
// Primary: the LED's spill lies along the bottom inner rim, and
// a tight breath of it under the cap.
rim = vec4(0.40, 0.62, 0.84, 0.45 * lit)
let dg = Material.sd_box(p, c + vec2(0.0, 0.05 * (hi.y - lo.y)), h - vec2(2.0, 0.0), r)
let g = exp(-(max(dg, 0.0) * max(dg, 0.0)) / (2.0 * 1.2 * 1.2)) * (1.0 - Finish.cover(dg, px))
res = vec4(0.40, 0.62, 0.84, 1.0) * g * 0.18 * lit * (1.0 - down * 0.5)
}
if kind > 4.5 {
rim = vec4(0.95, 0.62, 0.30, 0.40 * lit)
}
var lift = 1.0
if kind < 0.5 {
lift = 0.0
}
res = Finish.over(res, lq_cap(p, lo, hi, r, px, hover, down, disabled, focus, lift, tint, rim, 1.0))
if kind > 1.5 && kind < 3.5 || kind > 4.5 {
// The lamp: the chosen segment's lamp (`lq_lamp`), its core two
// device pixels whose middle stands 4 points above the bottom
// edge, 16 device pixels wide with a light end column either
// side; the label (raised one device pixel on these caps)
// clears its pale top row by two rows of face. Held, its halo
// halves. Unlit (Secondary, or disabled) it is the dark glass.
let lc = vec2(c.x, lq_snap(hi.y - 4.0, o.y, px))
let hs = vec2(min(5.5, h.x * 0.4), px)
let lw = min(5.3333, h.x * 0.4)
let xs = vec2(lq_snap(c.x - lw, o.x, px), lq_snap(c.x + lw, o.x, px))
var on = lit
var core = vec3(0.910, 0.918, 0.922)
var glow = vec3(0.405, 0.593, 0.752)
if kind > 2.5 && kind < 3.5 {
on = 0.0
}
if kind > 4.5 {
// Danger: the same lamp, amber, so it reads on the red.
core = vec3(1.0, 0.93, 0.78)
glow = vec3(1.0, 0.58, 0.18)
}
let dark = lq_led(res, p, lc, hs, 0.0, 0.0, px, core, glow)
let lamp = lq_lamp(res, p, xs, lc.y - px, px, 1.0, 1.0 - down * 0.5, core, glow)
res = mix(dark, lamp, on)
}
}
if loading > 0.001 {
// The arc ends two device pixels or more above where a lamp's rim
// stands: a smaller ring, lifted one device pixel.
let rad = max(min(h.x, h.y) - 7.25, 2.0)
res = lq_spinner(res, p, c - vec2(0.0, px), rad, anim, loading, 1.5)
}
return res
}
mod.widgets.ButtonFlat.draw_bg.pixel = fn() {
return self.host_over(lq_button(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, self.hover, self.down, self.disabled, self.focus, self.loading, self.anim_time, 0.0, 0.0), 7.0)
}
mod.widgets.Button.draw_bg.pixel = fn() {
return self.host_over(lq_button(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, self.hover, self.down, self.disabled, self.focus, self.loading, self.anim_time, 1.0, 0.0), 7.0)
}
mod.widgets.ButtonPrimary.draw_bg.pixel = fn() {
return self.host_over(lq_button(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, self.hover, self.down, self.disabled, self.focus, self.loading, self.anim_time, 2.0, 0.0), 7.0)
}
mod.widgets.ButtonSecondary.draw_bg.pixel = fn() {
return self.host_over(lq_button(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, self.hover, self.down, self.disabled, self.focus, self.loading, self.anim_time, 3.0, 0.0), 7.0)
}
mod.widgets.ButtonTertiary.draw_bg.pixel = fn() {
return self.host_over(lq_button(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, self.hover, self.down, self.disabled, self.focus, self.loading, self.anim_time, 4.0, 0.0), 7.0)
}
mod.widgets.ButtonDanger.draw_bg.pixel = fn() {
return self.host_over(lq_button(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, self.hover, self.down, self.disabled, self.focus, self.loading, self.anim_time, 5.0, 0.0), 7.0)
}
mod.widgets.ButtonOutline.draw_bg.pixel = fn() {
return self.host_over(lq_button(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, self.hover, self.down, self.disabled, self.focus, self.loading, self.anim_time, 6.0, self.border_dash), 7.0)
}
mod.widgets.ButtonFlatter.draw_bg.pixel = fn() {
return self.host_over(lq_button(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, self.hover, self.down, self.disabled, self.focus, self.loading, self.anim_time, 7.0, 0.0), 7.0)
}
// Label ink per state: caps #f6e8e6, the primary's lit legend near white,
// every variant dims to #b07a7c when disabled.
mod.widgets.ButtonPrimary.draw_text.color = #ffffff
mod.widgets.ButtonPrimary.draw_text.color_hover = #ffffff
mod.widgets.ButtonPrimary.draw_text.color_down = #ffffff
mod.widgets.ButtonPrimary.draw_text.color_focus = #ffffff
mod.widgets.ButtonPrimary.draw_text.color_disabled = #b07a7c
mod.widgets.ButtonPrimary.draw_icon.color = #ffffff
mod.widgets.ButtonSecondary.draw_text.color = #f6e8e6
mod.widgets.ButtonSecondary.draw_text.color_hover = #fbf0ee
mod.widgets.ButtonSecondary.draw_text.color_down = #ecdcda
mod.widgets.ButtonSecondary.draw_text.color_focus = #f6e8e6
mod.widgets.ButtonSecondary.draw_icon.color = #f6e8e6
mod.widgets.ButtonTertiary.draw_text.color = #f6e8e6
mod.widgets.ButtonTertiary.draw_text.color_hover = #fbf0ee
mod.widgets.ButtonTertiary.draw_text.color_down = #ecdcda
mod.widgets.ButtonTertiary.draw_text.color_focus = #f6e8e6
mod.widgets.ButtonTertiary.draw_icon.color = #f6e8e6
mod.widgets.ButtonDanger.draw_text.color = #ffffff
mod.widgets.ButtonDanger.draw_text.color_hover = #ffffff
mod.widgets.ButtonDanger.draw_text.color_down = #ffffff
mod.widgets.ButtonDanger.draw_text.color_focus = #ffffff
mod.widgets.ButtonDanger.draw_icon.color = #ffffff
mod.widgets.ButtonOutline.draw_text.color = #f6e8e6
mod.widgets.ButtonOutline.draw_text.color_hover = #fbf0ee
mod.widgets.ButtonOutline.draw_text.color_down = #ecdcda
mod.widgets.ButtonOutline.draw_text.color_focus = #f6e8e6
mod.widgets.ButtonOutline.draw_text.color_disabled = #b07a7c
mod.widgets.ButtonOutline.draw_icon.color = #f6e8e6
// The trailing icon takes the cap's ink too, never the drawing's own black.
mod.widgets.ButtonFlat.draw_icon_end.color = #f6e8e6
// ---------------------------------------------------------------- stud
// The chrome pyramid stud, after his photograph of pyramid studs: the
// toggle's knob, the rocker switch's raised half, the slide switch's knob
// and the smaller sliders' thumbs. Every one of them is drawn by `lq_stud`
// from the one material below, so they cannot drift apart; the fader's ring
// pull is the same silver.
// The stud's material, as uniforms on every control that carries it, so the
// tweaker moves it live; the numbers are his export's. The key light is
// screen space (x right, y down, z toward the viewer), then its intensity;
// the highlight, its roughness, the sheen and the rim toward grazing; the
// occlusion and how far up the walls it reaches; the reflection of the
// studio and how near the eye is over the stud; metal, spun grain and glow;
// the exposure in stops and the highlight roll-off; the cast shadow's
// strength and blur and the contact shadow; the height in points of the 25
// point stud on the fader (a smaller stud stands as much lower and its
// shadow's blur shrinks with it, so its facets slope the same). The inks:
// his silver, white light, his cool shade, and a dark floor for what his
// metal mirrors where the studio does not reach.
let lq_stud_material = fn(d) {
d.cap_light = uniform(vec3(-0.25, -0.65, 0.7))
d.cap_light_intensity = uniform(1.0)
d.cap_specular = uniform(1.0)
d.cap_roughness = uniform(0.86)
d.cap_gloss = uniform(0.56)
d.cap_rim = uniform(0.0)
d.cap_ao = uniform(0.7)
d.cap_ao_reach = uniform(0.9)
d.cap_reflection = uniform(1.0)
d.cap_reflection_perspective = uniform(0.48)
d.cap_metal = uniform(1.0)
d.cap_spun = uniform(0.0)
d.cap_glow = uniform(0.0)
d.cap_exposure = uniform(0.0)
d.cap_rolloff = uniform(0.47)
d.cap_shadow = uniform(0.55)
d.cap_shadow_blur = uniform(9.0)
d.cap_contact = uniform(0.55)
d.cap_height = uniform(14.0)
d.handle_color = uniform(#c4c7cc)
d.light_color = uniform(#ffffff)
d.shadow_color = uniform(#3a3c40)
d.sky_ground_color = uniform(#3a3c40)
}
// How far the quad of a control carrying the stud reaches past its rect,
// in points: the throw of the stud's shadow from its height along the key
// light, the blur at its tip (the engine holds it between 0.35 and 1 times
// the height), the glow's reach when there is one, and four points for the
// contact shadow's tail, a slot's lip past its ends and the focus ring's
// breath. The stud itself stays in the rect: on the fader never wider than
// the fader, its ends at the track's ends at the stops, its point magnified
// by the perspective by 16% of its half size at most.
let lq_stud_room = fn(l: vec3, height: float, blur: float, glow: float, cap: float) -> float {
let n = normalize(l)
let rise = max(height, 0.0)
let reach = rise * 0.7 * length(n.xy) / max(n.z, 0.25)
let tip = clamp(blur, max(rise * 0.35, 1.0), max(rise, 1.0))
return reach + tip + step(0.001, glow) * max(cap * 0.4, 1.0) * 4.0 + 4.0
}
// The studio his photographs of chrome pyramid studs were taken in, as the
// stud's facets mirror it: a big soft box above and to the left, a dark
// wall to the right, a white sweep below, a grey room behind, the camera
// dark overhead and the table a step darker below the horizon. `r` the
// mirrored direction in screen space (x right, y down, z toward the
// viewer). Its edges are sharp, as a studio's are, so as the eye's angle
// changes across the stud they cross its facets in the light and dark
// splits his photographs show.
let lq_studio = fn(r: vec3) -> vec3 {
let a = atan2(r.y, r.x)
var c = vec3(0.66, 0.67, 0.69)
c = mix(c, vec3(0.94, 0.95, 0.96), smoothstep(0.85, 1.0, a) * (1.0 - smoothstep(2.45, 2.6, a)))
c = mix(c, vec3(1.08, 1.08, 1.08), smoothstep(-2.4, -2.25, a) * (1.0 - smoothstep(-0.95, -0.8, a)))
c = mix(c, vec3(0.30, 0.30, 0.32), smoothstep(-0.7, -0.55, a) * (1.0 - smoothstep(0.7, 0.85, a)))
c = c * mix(0.9, 1.0, smoothstep(-0.14, -0.02, r.z))
return mix(c, vec3(0.38, 0.38, 0.40), smoothstep(0.5, 0.7, r.z))
}
// The cove cap's solid at `w` (stud-local points), his curves as the bench
// reads them: along, from the centre to one end and mirrored, the ridge
// falling in a cove to the foot; across, over the half width, level to 0.94
// and then down; the edge over the whole smaller half in from the footprint
// `fp`, a straight rise, so the four facets of the stud are planes (his
// export's rise was soft); his plan is level at 1, which is no plan. No
// ribs, no mark, no cut.
let lq_cove = fn(w: vec2, hs: vec2, fp: vec3, shape: vec4) -> vec4 {
let c_along = Cap.mirror(Cap.curve(abs(w.x) / hs.x, 2.0, vec2(0.0, 1.0), vec2(0.3333, 0.0), vec2(-0.0049, 0.255), vec2(1.0, 0.0), vec2(0.0049, -0.255), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0)), w.x)
let c_across = Cap.curve(abs(w.y) / hs.y, 3.0, vec2(0.0, 1.0), vec2(0.1567, 0.0), vec2(0.0, 0.0), vec2(0.94, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0))
let c_edge = Cap.curve(Cap.edge_x(fp.x, hs, shape.y), 2.0, vec2(0.0, 0.0), vec2(0.3333, 0.3333), vec2(-0.3333, -0.3333), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0))
let zero4 = vec4(0.0, 0.0, 0.0, 0.0)
return Cap.field(w, hs, fp, c_along, c_across, c_edge, vec2(1.0, 0.0), shape, zero4, zero4, zero4, zero4)
}
// THE STUD, drawn by the cap engine (`mod.sdf.Cap`) from his cove cap export
// (the slider bench's "Cove cap, silver", a wedge, 2026-10-03): a
// square-based pyramid, four flat facets meeting in crisp creases at its
// point in the middle, its base corners nearly square, the point brought
// toward the eye by its height. The studs make the facets flat (a straight
// edge rise over the whole smaller half), the creases crisp (blend 0.02),
// the corners 0.12, and drop the turned grain. His light, highlight, sheen
// and rim, with the studio mirrored in place of the plain sky: the shade
// without the sky and without the highlight, the studio laid in as his
// metal mirrors it, darkened toward the foot by his occlusion, and the
// highlight put back over it.
//
// `q` the point in the stud's own frame in points (x along the travel, as
// the fader's track space has it), `hs` its half size, `rise` its height in
// points and `blur` its shadow's, `press` 1 while it is held, `reach` how
// far the quad runs past it (nothing farther out is worked out), `vert` 1
// on a fader stood on end. The material: `light_dir` the key light; `k1`
// the highlight, its roughness, the sheen and the rim; `k2` the occlusion,
// its reach, the reflection and how near the eye is; `k3` metal, spun
// grain, glow and the light's intensity; `k4` the exposure, the roll-off,
// the cast shadow and the contact shadow; then the inks.
//
// It is laid over what lies under it as the bench lays a cap: `under_ink`
// what is there lit on black, `keep` the share of the ground that still
// shows through it. The stud's shadow darkens both (the ground by the
// shadow ink's mean, as one alpha must), a glow adds its light, `toned` 1
// tones that as the bench tones the slot, and the stud covers all of it.
// Hovered, it lifts as the sheet's chrome caps do, twice as much when it
// stands less than 23.5 points tall on screen; held, it sinks half a point
// and the lift goes; focused, the sheet's thin ice-blue ring round its
// foot and a breath of it; disabled, it dulls as the sheet's caps do and
// throws no shadow. Premultiplied.
let lq_stud = fn(under_ink: vec3, keep: float, toned: float, q: vec2, hs: vec2, rise: float, blur: float, press: float, reach: float, px: float, vert: float, hover: float, focus: float, disabled: float, light_dir: vec3, k1: vec4, k2: vec4, k3: vec4, k4: vec4, body: vec3, light_ink: vec3, shadow_ink: vec3, sky: vec3) -> vec4 {
let live = 1.0 - disabled
let l = normalize(light_dir)
let lt = Cap.light(l, vert)
let ev = pow(2.0, k4.x)
let ro = k4.y
// Corner 0.12 of the smaller half, the edge over the whole of it, blend
// 0.02, the height less the half point a press sinks it, the point
// brought 16% toward the eye.
let shape = vec4(0.12, 1.0, 0.02, max(rise - 0.5 * press, 0.5))
let persp = 0.16
let r = shape.x * min(hs.x, hs.y)
// `cd` the footprint's own outline, which the focus ring and the shadow
// follow; `fp` the outline as drawn, the magnified top's where it
// overhangs. The plan curve is level at 1: no pinch, the engine's none.
let cd = Cap.sd_cap(q, hs, r, vec2(1.0, 0.0)).x
var fp = Cap.sd_cap(q, hs, r, vec2(1.0, 0.0))
var cov = 0.0
var cap = body
var under = 0.0
var halo = 0.0
// Only a pixel within reach of the stud and what it throws runs the rest.
let far = abs(q) - hs
if max(far.x, far.y) < reach + 2.0 {
// Where the stud or its magnified top can lie: the solid read three
// times, each at the point the height read before brings toward the
// eye. Farther out every read is the plain footprint's.
if max(far.x - persp * hs.x, far.y - persp * hs.y) < px {
var w = q / (1.0 + persp)
fp = Cap.sd_cap(w, hs, r, vec2(1.0, 0.0))
var f = lq_cove(w, hs, fp, shape)
w = Cap.perspective(q, f.x, fp.x, persp)
fp = Cap.sd_cap(w, hs, r, vec2(1.0, 0.0))
f = lq_cove(w, hs, fp, shape)
w = Cap.perspective(q, f.x, fp.x, persp)
fp = Cap.sd_cap(w, hs, r, vec2(1.0, 0.0))
f = lq_cove(w, hs, fp, shape)
let n = Cap.normal(f.yz, shape.w, vert)
let v = Cap.view(q, hs, k2.w, vert)
let lit = Cap.shade(n, l, v, f.x, body, 0.0, 0.0, 1.0, k1.x, k1.y, k1.z, k1.w, k2.x, k2.y, 0.0, k3.x, k3.w, body, light_ink, shadow_ink, sky)
let dull = Cap.shade(n, l, v, f.x, body, 0.0, 0.0, 1.0, 0.0, k1.y, 0.0, 0.0, k2.x, k2.y, 0.0, k3.x, k3.w, body, light_ink, shadow_ink, sky)
let rv = -v - n * (2.0 * dot(n, -v))
let occ = 1.0 - k2.x * pow(1.0 - clamp(f.x, 0.0, 1.0), max(k2.y, 0.2))
let mirror = lq_studio(rv) * mix(vec3(1.0, 1.0, 1.0), body, k3.x) * mix(1.0, occ, 0.35)
cap = mix(dull, mirror, k2.z) + (lit - dull)
let grain = Cap.spun(q, hs, lt.xy) * k3.y
cap = Cap.in_shadow(Cap.in_light(cap, clamp(grain, 0.0, 1.0) * 0.55, light_ink), clamp(-grain, 0.0, 1.0) * 0.42, shadow_ink)
cap = Cap.emit(cap, f.x, body, light_ink, k3.z)
cov = Cap.cover(fp.x, px)
}
// Under it, the cast shadow, thrown from his top outline (0.451 of
// the length, 0.5098 of the width, its corner by the bench's rule at
// this size), and the contact shade. Disabled, the cast shadow goes
// and the contact thins, as under the sheet's caps.
let top_x = hs.x * 0.451
let top_y = hs.y * 0.5098
let top_r = min(r + max(hs.x - top_x, hs.y - top_y), min(top_x, top_y))
let top = vec3(0.451, 0.5098, top_r / max(min(top_x, top_y), 0.01))
let sh = Cap.shadow(q, hs, r, top, lt, shape.w, blur, k4.z * live, px)
let ct = Cap.contact(fp.x, max(shape.w * 0.3, 1.0), k4.w * (1.0 - 0.6 * disabled))
under = clamp(sh + ct, 0.0, 1.0) * (1.0 - cov)
halo = Cap.glow(fp.x, hs, k3.z) * 0.9 * (1.0 - cov)
}
// The stud toned as the bench tones, lifted under the hand, dulled when
// disabled.
var face = Cap.tone(cap, ev, ro)
let small = 1.0 - step(23.5, mix(hs.y, hs.x, vert) * 2.0)
face = face + vec3(0.04, 0.04, 0.042) * (1.0 + small) * hover * (1.0 - press) * live
let fl = Finish.lum(face)
face = mix(face, mix(face, vec3(fl, fl, fl), 0.45) * 0.8, disabled)
// Laid over what lies under it, in the bench's order.
let dim = mix(vec3(1.0, 1.0, 1.0), shadow_ink, under)
let kept = keep * (1.0 - under * (1.0 - (shadow_ink.x + shadow_ink.y + shadow_ink.z) / 3.0))
var low = under_ink * dim + body * halo
if toned > 0.5 {
low = Cap.tone(low, ev, ro)
}
var res = vec4(low, 1.0 - kept)
res = mix(res, vec4(face, 1.0), cov)
let ring = Finish.ring_out(cd, 0.0, px, px) * 0.9 + Finish.glow(cd - px, 0.7) * 0.35 * (1.0 - Finish.cover(cd, px))
return Finish.over(res, vec4(0.498, 0.706, 0.910, 1.0) * clamp(ring, 0.0, 1.0) * focus * live)
}
// ---------------------------------------------------------------- selection
// A well cut into the lacquer: a chrome rim of one device pixel, dim at the
// top and bright at the foot, a dark cut inside it, a wine-black floor with
// an inner shadow under its top edge, and a faint chrome lip on the ground
// under its lower edge. Premultiplied; `fill` is the floor, `lip` 0 drops
// the lip.
let lq_well = fn(p: vec2, lo: vec2, hi: vec2, r: float, px: float, fill: vec3, focus: float, disabled: float, lip: float) -> vec4 {
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
let d = Material.sd_box(p, c, h, r)
let t = clamp((p.y - lo.y) / max(hi.y - lo.y, 1.0), 0.0, 1.0)
var res = vec4(0.70, 0.52, 0.54, 1.0) * Finish.ring_out(d, 0.0, px, px) * step(c.y, p.y) * lip * 0.45 * (1.0 - disabled * 0.6)
// The floor lifts a little toward the foot; under the top edge the
// inner shadow, black for two device pixels and gone in about five.
var col = fill + vec3(0.030, 0.012, 0.014) * t
let top = max(p.y - lo.y - px * 2.0, 0.0)
col = mix(col, vec3(0.012, 0.0, 0.002), 0.75 * exp(-top / (1.6 * px)))
col = mix(col, mix(col, vec3(0.20, 0.12, 0.13), 0.5), disabled)
// The dark cut inside the rim, then the rim itself.
// A chrome rim of two device pixels on a wide well (one on a groove),
// the dark cut inside it.
let rw = mix(px, px * 2.0, step(10.0, hi.y - lo.y))
col = mix(col, vec3(0.030, 0.004, 0.008), Finish.band(d, rw, rw + px, px))
let rimc = mix(vec3(0.56, 0.42, 0.44), vec3(0.80, 0.70, 0.72), smoothstep(0.25, 1.0, t))
let rim_in = mix(rimc * 0.78, rimc, Finish.band(d, 0.0, px, px))
col = mix(col, mix(rim_in, vec3(0.36, 0.18, 0.20), disabled), Finish.band(d, 0.0, rw, px))
let a = Finish.cover(d, px)
res = Finish.over(res, vec4(col * a, a))
let ring = Finish.ring_out(d, 0.0, px, px) * 0.9 + Finish.glow(d - px, 0.7) * 0.35 * (1.0 - a)
res = Finish.over(res, vec4(0.498, 0.706, 0.910, 1.0) * clamp(ring, 0.0, 1.0) * focus * (1.0 - disabled))
return res
}
// The quad of a small control grows by this much so its shadow and focus
// ring can fall outside the box.
mod.widgets.CheckBoxFlat.draw_bg.vertex = fn() {
let m = 5.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.CheckBoxFlat.clip_x = false
mod.widgets.CheckBoxFlat.clip_y = false
mod.widgets.CheckBoxFlat.padding = mod.turtle.Inset{left: 8.0 right: 8.0 top: 4.0 bottom: 4.0}
mod.widgets.CheckBoxFlat.draw_bg.size = uniform(14.0)
mod.widgets.CheckBoxFlat.draw_bg.border_radius = uniform(1.5)
mod.widgets.CheckBoxFlat.label_walk.margin.left = 14.0
// A lit mark: a pale core along the stroke's middle, the accent at its
// edges and a soft halo of the lamp's blue round it. `dk` is the distance
// to the stroke's edge, `w` its half width.
let lq_lit_mark = fn(dst: vec4, dk: float, w: float, px: float, on: float, disabled: float, error: float) -> vec4 {
let edge = mix(mix(vec3(0.227, 0.655, 0.965), vec3(0.94, 0.32, 0.23), error), vec3(0.294, 0.306, 0.341), disabled)
let core = mix(mix(vec3(0.88, 0.96, 1.0), vec3(1.0, 0.86, 0.82), error), vec3(0.36, 0.37, 0.40), disabled)
let glow = mix(vec3(0.40, 0.62, 0.84), vec3(0.72, 0.25, 0.18), error)
let x = max(dk, 0.0)
let g = (0.30 * exp(-(x * x) / (2.0 * 0.45 * 0.45)) + 0.14 * exp(-(x * x) / (2.0 * 1.3 * 1.3))) * (1.0 - Finish.cover(dk, px)) * (1.0 - disabled)
var res = Finish.over(dst, vec4(glow, 1.0) * g * on)
let ink = mix(edge, core, 1.0 - smoothstep(0.15 * w, 0.8 * w, dk + w))
return Finish.over(res, vec4(ink, 1.0) * Finish.cover(dk, px) * on)
}
// The check box: a 14 point polished chrome cap. Checked, it sinks into a
// dark well cast a little blue, and its mark is lit.
mod.widgets.CheckBoxFlat.draw_bg.pixel = fn() {
let m = 5.0
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let sz = self.size
let x0 = (self.rect_size.x - sz) * self.mark_at_end
let y0 = self.rect_size.y * 0.5 - sz * 0.5
let lo = vec2(lq_snap(x0, o.x, px), lq_snap(y0, o.y, px))
let hi = vec2(lq_snap(x0 + sz, o.x, px), lq_snap(y0 + sz, o.y, px))
let c = (lo + hi) * 0.5
let r = min(self.border_radius * 2.0, sz * 0.5)
let on = max(self.active, self.mixed)
let held = max(max(self.down, self.pressed), on)
let tint = vec3(-0.006, 0.030, 0.062) * on * (1.0 - self.disabled)
var res = lq_capm(p, lo, hi, r, px, self.hover, held, self.disabled, self.focus, 1.0, tint, vec4(0.0, 0.0, 0.0, 0.0), 1.0, 2.0)
// The error: the outline turns the alt red.
let d = Material.sd_box(p, c, (hi - lo) * 0.5, r)
res = Finish.over(res, vec4(0.94, 0.32, 0.23, 1.0) * Finish.band(d, 0.0, px, px) * self.error)
// The mark: a check 8 points across in a 1.4 point stroke, lit; mixed
// is a dash of three device pixel rows, a pale core row between two of
// the accent, on the row through the middle of the well. The check
// stands one device pixel lower, in the well.
let cm = c + vec2(0.0, px * 0.5)
let dm = min(lq_seg(p, vec2(cm.x - 3.6, cm.y + 0.1), vec2(cm.x - 1.1, cm.y + 2.6)), lq_seg(p, vec2(cm.x - 1.1, cm.y + 2.6), vec2(cm.x + 3.7, cm.y - 2.8)))
let cy = lq_snap((lo.y + hi.y) * 0.5, o.y, px) + px * 0.5
let dd = lq_seg(p, vec2(c.x - 3.5, cy), vec2(c.x + 3.5, cy))
let dk = mix(dm - 0.7, dd - px * 1.5, self.mixed)
return lq_lit_mark(res, dk, mix(0.7, px * 1.5, self.mixed), px, on, self.disabled, self.error)
}
// The toggle: a wine-black 34 x 18 track, and for its knob the chrome
// pyramid stud (`lq_stud`), as tall as the track and square, its point in
// its middle, at the left end off and the right end on, travelling between
// them as the knob does (and following a drag). On, the track's floor takes
// the chosen segment's blue cast and the LED bar in the open end of the
// track is lit; off, the bar is a dark slot. The stud lifts under the hand,
// sinks while held, dulls when disabled; focused, the sheet's ice-blue ring
// lies round the track. The quad reaches as far as the stud's shadow does.
mod.widgets.ToggleFlat.draw_bg.size = uniform(18.0)
mod.widgets.ToggleFlat.draw_bg.pill_aspect = uniform(1.89)
mod.widgets.ToggleFlat.label_walk.margin.left = 34.0
lq_stud_material(mod.widgets.ToggleFlat.draw_bg)
mod.widgets.ToggleFlat.draw_bg.vertex = fn() {
let m = lq_stud_room(self.cap_light, self.cap_height * self.size / 25.0, self.cap_shadow_blur * self.size / 25.0, self.cap_glow, self.size)
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.ToggleFlat.draw_bg.pixel = fn() {
let m = lq_stud_room(self.cap_light, self.cap_height * self.size / 25.0, self.cap_shadow_blur * self.size / 25.0, self.cap_glow, self.size)
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let tw = self.size * self.pill_aspect
let th = self.size
let x0 = (self.rect_size.x - tw) * self.mark_at_end
let y0 = self.rect_size.y * 0.5 - th * 0.5
let lo = vec2(lq_snap(x0, o.x, px), lq_snap(y0, o.y, px))
let hi = vec2(lq_snap(x0 + tw, o.x, px), lq_snap(y0 + th, o.y, px))
let live = 1.0 - self.disabled
// On, the track's floor takes the chosen segment's blue cast.
let on = self.active * live
var res = lq_well(p, lo, hi, 2.5, px, mix(vec3(0.125, 0.043, 0.051), vec3(0.110, 0.080, 0.130), on), 0.0, self.disabled, 1.0)
// The stud: the track's height each way, travelling the track less its
// own length, its left edge on the pixel grid.
let kt = mix(self.active, self.drag_pos, self.drag)
let ks = hi.y - lo.y
let open = hi.x - lo.x - ks
let kx = lq_snap(x0 + (tw - ks) * kt, o.x, px)
let kc = vec2(kx + ks * 0.5, (lo.y + hi.y) * 0.5)
// The LED bar, 8 x 1.5 points, in the middle of the open end, on the
// side the stud has left.
let lc = vec2(mix(hi.x - open * 0.5, lo.x + open * 0.5, kt), lq_snap((lo.y + hi.y) * 0.5, o.y, px))
res = lq_led(res, p, lc, vec2(4.0, px), on, 0.8, px, vec3(0.94, 0.984, 1.0), vec3(0.40, 0.62, 0.84))
// The stud stands as much lower than the fader's 25 point one as it is
// smaller, and its shadow's blur shrinks with it.
let rise = self.cap_height * ks / 25.0
let blur = self.cap_shadow_blur * ks / 25.0
res = lq_stud(res.rgb, 1.0 - res.a, 0.0, p - kc, vec2(ks * 0.5, ks * 0.5), rise, blur, max(self.pressed, self.down) * live, m, px, 0.0, self.hover, 0.0, self.disabled, self.cap_light, vec4(self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim), vec4(self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_reflection_perspective), vec4(self.cap_metal, self.cap_spun, self.cap_glow, self.cap_light_intensity), vec4(self.cap_exposure, self.cap_rolloff, self.cap_shadow, self.cap_contact), self.handle_color.rgb, self.light_color.rgb, self.shadow_color.rgb, self.sky_ground_color.rgb)
// Focus: the sheet's thin ice-blue ring round the track, and a breath
// of it.
let d = Material.sd_box(p, (lo + hi) * 0.5, (hi - lo) * 0.5, 2.5)
let ring = Finish.ring_out(d, 0.0, px, px) * 0.9 + Finish.glow(d - px, 0.7) * 0.35 * (1.0 - Finish.cover(d, px))
return Finish.over(res, vec4(0.498, 0.706, 0.910, 1.0) * clamp(ring, 0.0, 1.0) * self.focus * live)
}
// The stud's middle runs from half the track's height in from one end to
// as far in from the other, so a mark on the knob stands on its point and a
// drag carries it under the pointer.
mod.widgets.ToggleFlat.draw_bg.knob_centred = uniform(1.0)
// A mark on the knob lies on the stud, whose lit facets are near white: a
// mark in a pale ink would vanish there, so it is cut into the chrome
// instead, in a dark of the stud's own; a dark ink stays as the page hands
// it. Premultiplied in and out.
let lq_knob_mark = fn(c: vec4) -> vec4 {
let ink = c.rgb / max(c.a, 0.0001)
let cut = smoothstep(0.3, 0.55, Finish.lum(ink))
return vec4(mix(ink, vec3(0.10, 0.105, 0.115), cut) * c.a, c.a)
}
mod.widgets.ToggleFlat.draw_icon_off.get_color = fn() {
let base = self.eval_gradient()
if self.color.x >= 0.0 {
return lq_knob_mark(vec4(self.color.rgb * self.color.a * base.a, self.color.a * base.a) * self.opacity)
}
return lq_knob_mark(base * self.opacity)
}
mod.widgets.ToggleFlat.draw_icon_on.get_color = fn() {
let base = self.eval_gradient()
if self.color.x >= 0.0 {
return lq_knob_mark(vec4(self.color.rgb * self.color.a * base.a, self.color.a * base.a) * self.opacity)
}
return lq_knob_mark(base * self.opacity)
}
mod.widgets.Toggle.draw_icon_off.get_color = fn() {
let base = self.eval_gradient()
if self.color.x >= 0.0 {
return lq_knob_mark(vec4(self.color.rgb * self.color.a * base.a, self.color.a * base.a) * self.opacity)
}
return lq_knob_mark(base * self.opacity)
}
mod.widgets.Toggle.draw_icon_on.get_color = fn() {
let base = self.eval_gradient()
if self.color.x >= 0.0 {
return lq_knob_mark(vec4(self.color.rgb * self.color.a * base.a, self.color.a * base.a) * self.opacity)
}
return lq_knob_mark(base * self.opacity)
}
// The rocker and slide switches keep their housing, legends and lamp as the
// sheet has always drawn them: the library's own drawing in the switch's
// rect, laid over the quad the sheet's small controls grow by five points
// each way (the switches take it from the check box), so on screen the
// housing stands that much wider and taller, its corner up and to the left
// of the rect's. Their moving part is the chrome pyramid stud (`lq_stud`),
// square in true points, standing where that drawing puts the rocker's
// raised half or the slide's knob, its shadow kept in the housing. Disabled,
// the whole switch fades, as the library's does.
mod.widgets.ToggleRocker.clip_x = false
mod.widgets.ToggleRocker.clip_y = false
mod.widgets.ToggleSlide.clip_x = false
mod.widgets.ToggleSlide.clip_y = false
lq_stud_material(mod.widgets.ToggleRocker.draw_bg)
lq_stud_material(mod.widgets.ToggleSlide.draw_bg)
mod.widgets.ToggleRocker.draw_bg.vertex = fn() {
let m = 5.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.ToggleSlide.draw_bg.vertex = fn() {
let m = 5.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
// The rocker: the library's housing with the rocker in it split at the
// pivot, and of the rocker only the half pressed in, darker and lower with
// its legend (the "O" on the off half, the "I" on the on half, lit while
// on). The raised half is the stud, square, as wide as that half is drawn
// on screen, in its middle, so one half shows the stud and the other its
// legend. As the state turns the stud crosses to the other half.
mod.widgets.ToggleRocker.draw_bg.pixel = fn() {
let m = 5.0
let ext = self.rect_size + vec2(2.0 * m, 2.0 * m)
// `p` the pixel as the library's drawing reads it (the rect's own frame
// over the whole quad), `pt` the same pixel in true points from the
// rect's corner, `sc` how much larger the first stands on screen.
let p = self.pos * self.rect_size
let pt = self.pos * ext - vec2(m, m)
let sc = ext / self.rect_size
let px = lq_px(self.draw_pass.dpi_factor)
let live = 1.0 - self.disabled
let sz = vec2(self.size * self.pill_aspect, self.size)
let o0 = vec2((self.rect_size.x - sz.x) * self.mark_at_end, self.rect_size.y * 0.5 - sz.y * 0.5)
let lo = vec2(Finish.snap(self.rect_pos.x + o0.x, px), Finish.snap(self.rect_pos.y + o0.y, px)) - self.rect_pos
let hi = vec2(Finish.snap(self.rect_pos.x + o0.x + sz.x, px), Finish.snap(self.rect_pos.y + o0.y + sz.y, px)) - self.rect_pos
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
let k = min(self.border_radius * 0.5, h.y)
let housing = Material.sd_box(p, c, h, k)
let fill = self.color
.mix(self.color_focus, self.focus)
.mix(self.color_hover, self.hover)
.mix(self.color_disabled, self.disabled)
let stroke = self.border_color
.mix(self.border_color_focus, self.focus)
.mix(self.border_color_hover, self.hover)
.mix(self.border_color_error, self.error)
.mix(self.border_color_disabled, self.disabled)
// The rocker inside the housing; how far each half is pressed in
// follows `active`.
let ins = Finish.snap(self.border_size + self.knob_inset, px)
let hr = max(h - vec2(ins, ins), vec2(0.5, 0.5))
let rocker = Material.sd_box(p, c, hr, max(k - ins, 0.0))
let on_side = step(c.x, p.x)
let pressed = mix(1.0 - on_side, on_side, self.active)
let raised_col = vec4(mix(fill.rgb, self.rocker_color.rgb, self.rocker_color.a), max(fill.a, self.rocker_color.a))
let pressed_col = vec4(mix(fill.rgb, self.rocker_color_pressed.rgb, self.rocker_color_pressed.a), max(fill.a, self.rocker_color_pressed.a))
var cap = mix(raised_col, pressed_col, pressed)
let tilt = clamp(abs(p.x - c.x) / max(hr.x, 0.5), 0.0, 1.0)
cap = vec4(cap.rgb * mix(1.0, 0.9 + 0.14 * tilt, 1.0 - pressed), cap.a)
let top_edge = Finish.band(p.y - (c.y - hr.y), -px, 0.0, px)
cap = vec4(mix(cap.rgb, Finish.lift(cap.rgb, 0.18), top_edge * (1.0 - pressed)), cap.a)
let shade = 1.0 - smoothstep(0.0, hr.y * 0.9, p.y - (c.y - hr.y))
cap = vec4(cap.rgb * (1.0 - 0.28 * shade * pressed), cap.a)
let pivot = Finish.cover(abs(p.x - c.x) - px * 0.5, px)
cap = vec4(mix(cap.rgb, cap.rgb * 0.45, pivot), cap.a)
// The legends, a device pixel lower on the pressed half.
let drop = px * pressed
let at_o = vec2(c.x - hr.x * 0.5, c.y + drop)
let at_i = vec2(c.x + hr.x * 0.5, c.y + drop)
let ring = Finish.cover(abs(length(p - at_o) - hr.y * 0.34) - px * 0.6, px) * (1.0 - on_side)
let ix = Finish.snap(self.rect_pos.x + at_i.x, px) - self.rect_pos.x + px * 0.5
let bar = Finish.cover(max(abs(p.x - ix) - px * 0.5, abs(p.y - at_i.y) - hr.y * 0.38), px) * on_side
let legend = self.legend_color.mix(self.legend_on_color, self.active * on_side)
cap = vec4(mix(cap.rgb, legend.rgb, (ring + bar) * legend.a), cap.a)
var col = vec4(fill.rgb * fill.a, fill.a)
col = mix(col, vec4(stroke.rgb * stroke.a, stroke.a), Finish.band(housing, 0.0, max(self.border_size, px), px))
col = mix(col, vec4(cap.rgb * cap.a, cap.a), Finish.cover(rocker, px) * pressed)
let inside = Finish.cover(housing, px)
col = col * inside
// The stud: the middle of the raised half as it stands on screen,
// square, as wide as that half (no taller than the rocker), its edges
// on whole device pixels.
let at = vec2(c.x + hr.x * 0.5 * (1.0 - 2.0 * self.active), c.y) * sc - vec2(m, m)
let ss = max(floor(min(hr.x * sc.x, 2.0 * hr.y * sc.y) / px), 2.0) * px
let kc = vec2(lq_snap(at.x - ss * 0.5, self.rect_pos.x, px), lq_snap(at.y - ss * 0.5, self.rect_pos.y, px)) + vec2(ss * 0.5, ss * 0.5)
let rise = self.cap_height * ss / 25.0
let blur = self.cap_shadow_blur * ss / 25.0
let lit = lq_stud(col.rgb, 1.0 - col.a, 0.0, pt - kc, vec2(ss * 0.5, ss * 0.5), rise, blur, max(self.pressed, self.down) * live, 2.0 * h.x * sc.x, px, 0.0, self.hover, 0.0, self.disabled, self.cap_light, vec4(self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim), vec4(self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_reflection_perspective), vec4(self.cap_metal, self.cap_spun, self.cap_glow, self.cap_light_intensity), vec4(self.cap_exposure, self.cap_rolloff, self.cap_shadow, self.cap_contact), self.handle_color.rgb, self.light_color.rgb, self.shadow_color.rgb, self.sky_ground_color.rgb)
return mix(col, lit, inside) * (1.0 - 0.55 * self.disabled)
}
// The slide: no track -- on his word, the switch is the stud and its lamp
// standing on the lacquer, at the compact size the switch's own rect gives
// (the slot's place still sets where they stand, but it is not drawn). The
// lamp sits in the end the stud has left: a lit dot while on, an open ring
// while off. The stud is the library's knob square, its top and bottom on
// whole device pixels, as far clear of the slot's ends at both ends of its
// travel as of its long sides; it follows a drag, and its shadow falls on
// the lacquer. Disabled, the whole switch fades.
mod.widgets.ToggleSlide.draw_bg.pixel = fn() {
let m = 5.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let live = 1.0 - self.disabled
let sz = vec2(self.size * self.pill_aspect, self.size)
let o0 = vec2((self.rect_size.x - sz.x) * self.mark_at_end, self.rect_size.y * 0.5 - sz.y * 0.5)
let lo = vec2(lq_snap(o0.x, o.x, px), lq_snap(o0.y, o.y, px))
let hi = vec2(lq_snap(o0.x + sz.x, o.x, px), lq_snap(o0.y + sz.y, o.y, px))
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
// The knob's place, as the library works it out (the drag and the knob
// icons read `knob_centred` to agree): a whole number of device pixels,
// as many fewer than the slot as leaves the same clearance above and
// below, travelling the slot's length less its height.
let knob_t = mix(self.active, self.drag_pos, self.drag)
let slot_px = floor((hi.y - lo.y) / px + 0.5)
var knob_px = max(floor(2.0 * (h.y - self.border_size - self.knob_inset) / px), 2.0)
knob_px = knob_px - (slot_px - knob_px - 2.0 * floor((slot_px - knob_px) * 0.5))
let ks = knob_px * 0.5 * px
let k_top = lo.y + (slot_px - knob_px) * 0.5 * px
let kx = lq_snap(lo.x + h.y + 2.0 * (h.x - h.y) * knob_t - ks, o.x, px)
let kc = vec2(kx + ks, k_top + ks)
// The lamp in the end the stud left.
let free = vec2(mix(lo.x + 2.0 * h.x - h.y, lo.x + h.y, knob_t), c.y)
let mr = sz.y * 0.16
let dot = Finish.cover(length(p - free) - mr, px)
let ring = Finish.cover(abs(length(p - free) - mr) - px * 0.6, px)
let mark = mix(ring, dot, self.active)
let mark_ink = self.mark_color.mix(self.mark_on_color, self.active)
let col = vec4(mark_ink.rgb * mark_ink.a * mark, mark_ink.a * mark)
let rise = self.cap_height * ks * 2.0 / 25.0
let blur = self.cap_shadow_blur * ks * 2.0 / 25.0
let lit = lq_stud(col.rgb, 1.0 - col.a, 0.0, p - kc, vec2(ks, ks), rise, blur, max(self.pressed, self.down) * live, 2.0 * h.x, px, 0.0, self.hover, 0.0, self.disabled, self.cap_light, vec4(self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim), vec4(self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_reflection_perspective), vec4(self.cap_metal, self.cap_spun, self.cap_glow, self.cap_light_intensity), vec4(self.cap_exposure, self.cap_rolloff, self.cap_shadow, self.cap_contact), self.handle_color.rgb, self.light_color.rgb, self.shadow_color.rgb, self.sky_ground_color.rgb)
return lit * (1.0 - 0.55 * self.disabled)
}
// The radio: a polished chrome ball. Chosen, it sinks a little (its shadow
// shortens, the chrome darkens) and an LED dot is lit in its middle.
mod.widgets.RadioButtonFlat.draw_bg.vertex = fn() {
let m = 5.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.RadioButtonFlat.clip_x = false
mod.widgets.RadioButtonFlat.clip_y = false
mod.widgets.RadioButtonFlat.draw_bg.size = uniform(16.0)
mod.widgets.RadioButtonFlat.padding = mod.turtle.Inset{left: 8.0 right: 8.0 top: 4.0 bottom: 4.0}
mod.widgets.RadioButtonFlat.label_walk.margin.left = 14.0
mod.widgets.RadioButtonFlat.draw_bg.pixel = fn() {
let m = 5.0
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let sz = self.size
let y0 = self.rect_size.y * 0.5 - sz * 0.5
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(y0, o.y, px))
let hi = vec2(lq_snap(sz, o.x, px), lq_snap(y0 + sz, o.y, px))
let c = (lo + hi) * 0.5
let on = self.active * (1.0 - self.disabled)
let rad = (hi.x - lo.x) * 0.5
// The socket: a round well the ball sits in, its chrome lip below.
let ds = length(p - c) - rad
var res = vec4(0.70, 0.52, 0.54, 1.0) * Finish.ring_out(ds, 0.0, px, px) * step(c.y + rad * 0.3, p.y) * 0.6
let st = clamp((p.y - lo.y) / max(hi.y - lo.y, 1.0), 0.0, 1.0)
let sock = mix(vec3(0.02, 0.0, 0.004), vec3(0.16, 0.05, 0.06), st)
res = Finish.over(res, vec4(sock, 1.0) * Finish.cover(ds, px))
res = lq_ball(res, p, c + vec2(0.0, px * 0.5), rad - 1.6, px, self.hover, max(self.down, on * 1.6), self.disabled, 0.7)
// The dot: 4.5 points, lit ice blue with its halo on the chrome.
let lit = lq_led(res, p, c, vec2(2.25 - px, 2.25 - px), 1.0, 1.0, px, vec3(0.90, 0.97, 1.0), vec3(0.40, 0.62, 0.84))
res = mix(res, lit, on)
let d = length(p - c) - rad
let ring = Finish.ring_out(d, 0.0, px, px) * 0.9 + Finish.glow(d - px, 0.7) * 0.35 * (1.0 - Finish.cover(d, px))
return Finish.over(res, vec4(0.498, 0.706, 0.910, 1.0) * clamp(ring, 0.0, 1.0) * self.focus * (1.0 - self.disabled))
}
// The radio tab: a segment of a joined strip. At rest a raised smoked
// chrome cap; chosen, it sinks, its legend lit and the lamp under it.
mod.widgets.RadioButtonTabFlat.draw_bg.vertex = fn() {
let m = 7.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.RadioButtonTabFlat.clip_x = false
mod.widgets.RadioButtonTabFlat.clip_y = false
mod.widgets.RadioButtonTabFlat.padding = mod.turtle.Inset{left: 0.0 right: 8.0 top: 4.0 bottom: 4.0}
mod.widgets.RadioButtonTabFlat.min_height = 24.0
mod.widgets.RadioButtonTabFlat.draw_bg.pixel = fn() {
let m = 7.0
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(self.rect_size.x, o.x, px), lq_snap(self.rect_size.y, o.y, px))
let sel = self.active
let res = lq_steel(p, lo, hi, 3.0, px, o, self.hover, max(self.down, sel), sel, self.disabled, self.focus, 1.0, vec3(0.0, 0.0, 0.0))
// The lamp, centred under the legend (which stands centred, 1.7 points
// above the cap's middle): its core 6.5 points under the middle, so a
// descender keeps two rows of well above its pale top row.
let c = (lo + hi) * 0.5
let xs = vec2(lq_snap(c.x - 5.3333, o.x, px), lq_snap(c.x + 5.3333, o.x, px))
return lq_lamp(res, p, xs, lq_snap(c.y + 6.5, o.y, px), px, sel * (1.0 - self.disabled), 1.0, vec3(0.910, 0.918, 0.922), vec3(0.405, 0.593, 0.752))
}
mod.widgets.RadioButtonTabFlat.draw_text.color = #1e1618
mod.widgets.RadioButtonTabFlat.draw_text.color_hover = #120c0d
mod.widgets.RadioButtonTabFlat.draw_text.color_active = #d4eaff
mod.widgets.RadioButtonTabFlat.draw_text.color_disabled = #2e2426
// The ink follows the face, not the pointer: dark on the raised steel in
// every state, lit blue-white in the chosen well even under the hand.
mod.widgets.RadioButtonTabFlat.draw_text.pixel = fn() {
let raw = self.sample_text_pixel()
var ink = mix(vec3(0.118, 0.086, 0.094), vec3(0.831, 0.918, 1.0), self.active)
ink = mix(ink, mix(vec3(0.180, 0.141, 0.149), vec3(0.55, 0.60, 0.66), self.active), self.disabled)
return vec4(ink * raw.a, raw.a)
}
// ---------------------------------------------------------------- sliders
// A lit bar laid in a groove, like the lamp and like the chosen segment's
// legend: counted in whole device-pixel rows from `lo.y` (snapped), one
// pale core row and one row of the body under it in the middle of the
// floor, and on the rows round them a soft halo on the floor -- 15 % of the
// core's light on the next row, 7 % on the one after. Past the bar's end
// the halo runs on along the floor, falling by half every device pixel,
// between `gx.x` and `gx.y` (the floor's ends). `body` is the role colour; the core
// is it taken most of the way to white. Premultiplied over `dst`.
let lq_lit_bar = fn(dst: vec4, p: vec2, lo: vec2, hi: vec2, gx: vec2, px: float, body: vec3, disabled: float) -> vec4 {
let n = max(floor((hi.y - lo.y) / px + 0.5), 1.0)
let k = floor((p.y - lo.y) / px)
let rows = step(0.0, k) * step(k, n - 1.0)
let kc = max(floor(n * 0.5) - 1.0, 0.0)
let kb = min(kc + 1.0, n - 1.0)
// How far this row stands from the lit pair, in rows.
let dr = max(max(kc - k, k - kb), 0.0)
let core = mix(body, vec3(1.0, 1.0, 1.0), 0.8)
let deep = mix(body, vec3(0.12, 0.04, 0.05), 0.3)
var col = mix(core, deep, step(kb - 0.5, k) * step(kc + 0.5, kb))
col = mix(col, mix(body, vec3(0.42, 0.26, 0.28), 0.7), disabled)
let lit = step(dr, 0.5)
let halo_row = mix(0.0, 0.34, step(0.5, dr) * step(dr, 1.5)) + mix(0.0, 0.16, step(1.5, dr) * step(dr, 2.5))
let inx = step(lo.x, p.x) * step(p.x, hi.x)
let past = max(max(p.x - hi.x, lo.x - p.x), 0.0) / px
let fall = exp(-max(past - 0.5, 0.0) * 0.76) * (1.0 - inx) * step(gx.x, p.x) * step(p.x, gx.y)
let along = mix(0.34, halo_row, step(0.5, dr))
let g = (halo_row * inx + along * fall) * rows * (1.0 - disabled)
var res = Finish.over(dst, vec4(deep, 1.0) * g)
return Finish.over(res, vec4(col, 1.0) * lit * inx * rows)
}
let lq_fill = fn(dst: vec4, p: vec2, lo: vec2, hi: vec2, gx: vec2, px: float, disabled: float) -> vec4 {
return lq_lit_bar(dst, p, lo, hi, gx, px, vec3(0.50, 0.76, 1.0), disabled)
}
// A slider's thumb: the chrome pyramid stud (`lq_stud`) the fader, the
// toggles and the switches carry, from the same material, `s` points square
// with its middle at `c` (its leading edge put on the device pixel grid by
// the caller, so its edges stand crisp and it keeps one shape as it moves).
// It stands as much lower than the fader's 25 point stud as it is smaller,
// and its shadow's blur shrinks with it, so its facets slope the same. `k5`
// the material's height and shadow blur at 25 points; the rest as
// `lq_stud` takes them. Laid over `dst`, premultiplied, as the toggle lays
// its stud.
let lq_thumb = fn(dst: vec4, p: vec2, c: vec2, s: float, reach: float, px: float, hover: float, press: float, focus: float, disabled: float, light_dir: vec3, k1: vec4, k2: vec4, k3: vec4, k4: vec4, k5: vec2, body: vec3, light_ink: vec3, shadow_ink: vec3, sky: vec3) -> vec4 {
return lq_stud(dst.rgb, 1.0 - dst.a, 0.0, p - c, vec2(s * 0.5, s * 0.5), k5.x * s / 25.0, k5.y * s / 25.0, press, reach, px, 0.0, hover, focus, disabled, light_dir, k1, k2, k3, k4, body, light_ink, shadow_ink, sky)
}
// How far the quad of a slider whose thumb is `s` points square reaches
// past its rect: the stud's own room at that size.
let lq_thumb_room = fn(light_dir: vec3, height: float, blur: float, glow: float, s: float) -> float {
return lq_stud_room(light_dir, height * s / 25.0, blur * s / 25.0, glow, s)
}
// The bar slider: a 4 point well under its label row, the lit fill, and for
// its thumb the 16 point stud riding it. Hovered the stud lifts, held it
// sinks half a point, focused it wears the sheet's ice-blue ring round its
// foot, disabled it dulls and throws no shadow. The quad reaches as far as
// its shadow does.
mod.widgets.SliderFlat.clip_x = false
mod.widgets.SliderFlat.clip_y = false
lq_stud_material(mod.widgets.SliderFlat.draw_bg)
lq_stud_material(mod.widgets.Slider.draw_bg)
mod.widgets.SliderFlat.draw_bg.vertex = fn() {
let m = lq_thumb_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, 16.0)
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.SliderFlat.draw_bg.pixel = fn() {
let m = lq_thumb_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, 16.0)
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let rad = 8.0
let cy = lq_snap(20.0 + (self.rect_size.y - 20.0) * 0.5, o.y, px)
let lo = vec2(lq_snap(0.0, o.x, px), cy - 2.0)
let hi = vec2(lq_snap(self.rect_size.x, o.x, px), cy + 2.0)
var res = lq_well(p, lo, hi, 2.0, px, vec3(0.125, 0.043, 0.051), 0.0, self.disabled, 1.0)
let span = self.rect_size.x - rad * 2.0
let hx = rad + self.slide_pos * span
let ox = rad + self.origin_pos * span
let f0 = mix(lo.x + px, min(ox, hx), self.arc_origin)
let f1 = mix(hx, max(ox, hx), self.arc_origin)
res = lq_fill(res, p, vec2(f0, lo.y + px), vec2(f1, hi.y - px), vec2(lo.x + px, hi.x - px), px, self.disabled)
let c = vec2(lq_snap(hx - rad, o.x, px) + rad, cy)
return lq_thumb(res, p, c, rad * 2.0, m, px, self.hover, self.drag * (1.0 - self.disabled), self.focus, self.disabled, self.cap_light, vec4(self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim), vec4(self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_reflection_perspective), vec4(self.cap_metal, self.cap_spun, self.cap_glow, self.cap_light_intensity), vec4(self.cap_exposure, self.cap_rolloff, self.cap_shadow, self.cap_contact), vec2(self.cap_height, self.cap_shadow_blur), self.handle_color.rgb, self.light_color.rgb, self.shadow_color.rgb, self.sky_ground_color.rgb)
}
// The range slider: the bar slider's 4 point well under its label row,
// the lit fill between its two ends, and the bar slider's 16 point stud on
// each end (the hit test takes the same size and inset, so the drawn stud
// is the grabbed one). The end the pointer is over lifts; the one held, or
// both while the band between them is dragged, sinks half a point.
mod.widgets.RangeSliderFlat.handle_size = 16.0
mod.widgets.RangeSliderFlat.track_inset = 0.0
lq_stud_material(mod.widgets.RangeSliderFlat.draw_bg)
mod.widgets.RangeSliderFlat.draw_bg.vertex = fn() {
let m = lq_thumb_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, self.handle_px)
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.RangeSliderFlat.draw_bg.pixel = fn() {
let m = lq_thumb_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, self.handle_px)
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let rad = self.handle_px * 0.5
let cy = lq_snap(self.label_px + (self.rect_size.y - self.label_px) * 0.5, o.y, px)
let lo = vec2(lq_snap(0.0, o.x, px), cy - 2.0)
let hi = vec2(lq_snap(self.rect_size.x, o.x, px), cy + 2.0)
var res = lq_well(p, lo, hi, 2.0, px, vec3(0.125, 0.043, 0.051), 0.0, self.disabled, 1.0)
let span = self.rect_size.x - self.inset_px * 2.0 - self.handle_px
let sx = self.inset_px + rad + self.start_pos * span
let ex = self.inset_px + rad + self.end_pos * span
res = lq_fill(res, p, vec2(sx, lo.y + px), vec2(ex, hi.y - px), vec2(lo.x + px, hi.x - px), px, self.disabled)
let live = 1.0 - self.disabled
let held_s = clamp(self.drag * (self.hot_start + self.hot_band), 0.0, 1.0) * live
let held_e = clamp(self.drag * (self.hot_end + self.hot_band), 0.0, 1.0) * live
// One stud is worked out per pixel (two would double a shader the
// D3D compiler already takes long over): the end one where its own
// footprint is, so it lies over the start one when they meet, and
// elsewhere whichever end is nearer.
let cs = vec2(lq_snap(sx - rad, o.x, px) + rad, cy)
let ce = vec2(lq_snap(ex - rad, o.x, px) + rad, cy)
let fe = abs(p - ce) - vec2(rad * 1.16 + px, rad * 1.16 + px)
let pick = max(step(max(fe.x, fe.y), 0.0), step(abs(p.x - ce.x), abs(p.x - cs.x)))
let c = mix(cs, ce, pick)
let hover = self.hover * mix(self.hot_start, self.hot_end, pick)
let held = mix(held_s, held_e, pick)
return lq_thumb(res, p, c, self.handle_px, m, px, hover, held, 0.0, self.disabled, self.cap_light, vec4(self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim), vec4(self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_reflection_perspective), vec4(self.cap_metal, self.cap_spun, self.cap_glow, self.cap_light_intensity), vec4(self.cap_exposure, self.cap_rolloff, self.cap_shadow, self.cap_contact), vec2(self.cap_height, self.cap_shadow_blur), self.handle_color.rgb, self.light_color.rgb, self.shadow_color.rgb, self.sky_ground_color.rgb)
}
// A slider's value is a text input, and the field row's 24 point minimum
// pushed it half a row below its label: here it is only as tall as its line.
mod.widgets.SliderMinimal.text_input.min_height = 0.0
mod.widgets.SliderMinimal.text_input.padding = mod.turtle.Inset{left: 0.0 right: 0.0 top: 0.0 bottom: 0.0}
mod.widgets.SliderFlat.text_input.min_height = 0.0
mod.widgets.SliderFlat.text_input.padding = mod.turtle.Inset{left: 0.0 right: 0.0 top: 0.0 bottom: 0.0}
mod.widgets.Slider.text_input.min_height = 0.0
mod.widgets.Slider.text_input.padding = mod.turtle.Inset{left: 0.0 right: 0.0 top: 0.0 bottom: 0.0}
mod.widgets.SliderRoundFlat.text_input.min_height = 0.0
mod.widgets.SliderRoundFlat.text_input.padding = mod.turtle.Inset{left: 0.0 right: 0.0 top: 0.0 bottom: 0.0}
mod.widgets.SliderRound.text_input.min_height = 0.0
mod.widgets.SliderRound.text_input.padding = mod.turtle.Inset{left: 0.0 right: 0.0 top: 0.0 bottom: 0.0}
// A dial's value box stands in the 20 point legend row above the ring: at
// the field row's 24 points it hung over the ring's bezel, so here it is
// its line and two points either side, and clears the ring.
mod.widgets.RotaryFlat.text_input.min_height = 0.0
mod.widgets.RotaryFlat.text_input.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 2.0 bottom: 2.0}
mod.widgets.Rotary.text_input.min_height = 0.0
mod.widgets.Rotary.text_input.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 2.0 bottom: 2.0}
mod.widgets.RotaryGradientY.text_input.min_height = 0.0
mod.widgets.RotaryGradientY.text_input.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 2.0 bottom: 2.0}
// The minimal slider: only the slot and the lit value; under the hand a
// 12 point stud shows at the value, its point over the fill's end, and
// sinks half a point while held. The slot is drawn only inside the rect,
// as it was before the quad grew for the stud's shadow.
mod.widgets.SliderMinimal.clip_x = false
mod.widgets.SliderMinimal.clip_y = false
lq_stud_material(mod.widgets.SliderMinimal.draw_bg)
mod.widgets.SliderMinimal.draw_bg.vertex = fn() {
let m = lq_thumb_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, 12.0)
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.SliderMinimal.draw_bg.pixel = fn() {
let m = lq_thumb_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, 12.0)
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let cy = lq_snap(self.offset_y + (self.rect_size.y - self.offset_y) * 0.5, o.y, px)
// Four device pixels: the outline, a core row, a body row, the outline.
let lo = vec2(lq_snap(0.0, o.x, px), cy - px * 2.0)
let hi = vec2(lq_snap(self.rect_size.x, o.x, px), cy + px * 2.0)
var res = lq_well(p, lo, hi, 1.0, px, vec3(0.125, 0.043, 0.051), 0.0, self.disabled, 1.0)
let lo2 = min(self.origin_pos, self.slide_pos)
let hi2 = max(self.origin_pos, self.slide_pos)
let f0 = mix(0.0, lo2, self.arc_origin) * self.rect_size.x
let f1 = mix(self.slide_pos, hi2, self.arc_origin) * self.rect_size.x
res = lq_fill(res, p, vec2(max(f0, lo.x + px), lo.y + px), vec2(min(f1, hi.x - px), hi.y - px), vec2(lo.x + px, hi.x - px), px, self.disabled)
res = res * step(0.0, p.x) * step(p.x, self.rect_size.x) * step(0.0, p.y) * step(p.y, self.rect_size.y)
let hx = self.slide_pos * self.rect_size.x
let show = max(self.hover, self.drag)
let c = vec2(lq_snap(hx - 6.0, o.x, px) + 6.0, cy)
let stud = lq_thumb(res, p, c, 12.0, m, px, 0.0, self.drag * (1.0 - self.disabled), 0.0, self.disabled, self.cap_light, vec4(self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim), vec4(self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_reflection_perspective), vec4(self.cap_metal, self.cap_spun, self.cap_glow, self.cap_light_intensity), vec4(self.cap_exposure, self.cap_rolloff, self.cap_shadow, self.cap_contact), vec2(self.cap_height, self.cap_shadow_blur), self.handle_color.rgb, self.light_color.rgb, self.shadow_color.rgb, self.sky_ground_color.rgb)
return mix(res, stud, show)
}
// The round slider: a label column, then the bar slider's 4 point slot with
// the lit fill and the bar slider's 16 point stud. The slot ends short of
// the right edge, so the value the widget writes there stands on the
// ground, not on the track.
mod.widgets.SliderRoundFlat.clip_x = false
mod.widgets.SliderRoundFlat.clip_y = false
lq_stud_material(mod.widgets.SliderRoundFlat.draw_bg)
lq_stud_material(mod.widgets.SliderRound.draw_bg)
mod.widgets.SliderRoundFlat.draw_bg.vertex = fn() {
let m = lq_thumb_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, 16.0)
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.SliderRoundFlat.draw_bg.pixel = fn() {
let m = lq_thumb_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, 16.0)
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let rad = 8.0
let cy = lq_snap(self.rect_size.y * 0.5, o.y, px)
let x0 = lq_snap(self.label_size, o.x, px)
let lo = vec2(x0, cy - 2.0)
let hi = vec2(lq_snap(self.rect_size.x - 37.0, o.x, px), cy + 2.0)
var res = lq_well(p, lo, hi, 2.0, px, vec3(0.125, 0.043, 0.051), 0.0, self.disabled, 1.0)
let span = hi.x - lo.x - rad * 2.0
let hx = lo.x + rad + self.slide_pos * span
res = lq_fill(res, p, vec2(lo.x + px, lo.y + px), vec2(hx, hi.y - px), vec2(lo.x + px, hi.x - px), px, self.disabled)
let c = vec2(lq_snap(hx - rad, o.x, px) + rad, cy)
return lq_thumb(res, p, c, rad * 2.0, m, px, self.hover, self.drag * (1.0 - self.disabled), self.focus, self.disabled, self.cap_light, vec4(self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim), vec4(self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_reflection_perspective), vec4(self.cap_metal, self.cap_spun, self.cap_glow, self.cap_light_intensity), vec4(self.cap_exposure, self.cap_rolloff, self.cap_shadow, self.cap_contact), vec2(self.cap_height, self.cap_shadow_blur), self.handle_color.rgb, self.light_color.rgb, self.shadow_color.rgb, self.sky_ground_color.rgb)
}
// THE RING PULL, the fader's cap: a drawer pull after his photographs,
// whose base is the knob bench's `skirted` style in silver, seen from the
// front, with a round wire ring piercing its nub and hanging from it.
//
// The skirted knob's revolve (`lq_skirted`), the bench's curve with its
// anchors and the handles its curve rules give them: from the middle out a
// nub level to a quarter of the radius, rolling over at 0.28 into a steep
// wall down to 0.30 of the height at 0.34, a skirt dished a little (0.23 at
// 0.63) and rising again to 0.25 at 0.93, then a rounded fall to the case
// at the rim. The height in .x (0..1), the slope over the radius in .y
// (held to the +-8 the bench's bake stores).
let lq_skirted = fn(r: float) -> vec2 {
return Cap.curve(r, 5.0, vec2(0.0, 1.0), vec2(0.0467, -0.005), vec2(-0.02, 0.0412), vec2(0.28, 0.97), vec2(0.02, -0.0412), vec2(-0.02, 0.12), vec2(0.34, 0.30), vec2(0.1083, -0.12), vec2(-0.0233, 0.0106), vec2(0.93, 0.25), vec2(0.0233, -0.0106), vec2(-0.0117, 0.0417), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0))
}
// The pull's knob: the skirted revolve with its skirt at half the radius
// (on his word), the nub and its wall as the bench has them. `r` in the
// bench knob's radii: to 0.34 the curve itself, and from there the skirt's
// span 0.34..1 laid into 0.34..0.5, so the flange runs a sixth of the
// radius round the nub's foot and ends at half the radius.
let lq_pull_profile = fn(r: float) -> vec2 {
return lq_skirted(mix(r, 0.34 + (r - 0.34) * 4.125, step(0.34, r)))
}
// `rad` is the radius of the bench's knob the nub, the ring and the heights
// are measured on (25/18 of the fader's cap_size, which the travel counts);
// the skirt ends at half of it. The
// nub stands 0.75 of `rad` tall, the bench's chrome depth. The ring's
// centre line is 0.49 of `rad` round, wider than the skirt, its round wire's
// own radius 0.07 of `rad` (on his word, two thirds of the 0.11 it was):
// its top runs level through the nub just above the skirt, so the nub
// hides it where the nub stands taller and the wire comes out of the nub's
// lower flanks on either side, a dark rim of the hole round it and the wire
// darkening as it goes in; from there the ring hangs down the screen,
// whichever way the fader lies, clearing the skirt's edge and leaning back
// until its foot rests on what lies under the knob, the track or the
// ground, a contact shade close round the wire wherever it lies low over a
// surface, the skirt or the ground. Both are the
// stud's silver, lit and mirroring the sheet's studio as the stud does
// (the shade without the highlight, the studio laid in, darkened toward
// the foot by the occlusion, the highlight put back). The knob throws its
// shadow from its skirt and from its nub (the nub's falling on the skirt
// and past it), the ring its own on the knob and the ground, each blurred
// by its height as the stud's is; the contact shade hugs the skirt's foot.
//
// Every edge is a one device pixel coverage: the knob's and the wire's
// outlines, where the wire goes into the nub (by how fast the two heights
// part across a pixel), the hole's rim and the shadows' feet; the knob's
// slope is the curve's rise over one device pixel, so the nub's wall and the
// rim's fall, each about a pixel wide, shade as a pixel-wide band and not
// as a stair.
//
// `q` the point from the knob's middle on screen in points, `rad` its
// radius, `press` 1 while it is held (it sinks half a point), `reach` how
// far the quad runs past the rect; the material as `lq_stud` takes it, `k5`
// the shadow's blur and the stud's height it is given for. Laid over what
// lies under it as the stud is; hovered it lifts, focused the sheet's
// ice-blue ring lies round the skirt's foot, disabled it dulls and throws no
// shadow. Premultiplied.
let lq_pull = fn(under_ink: vec3, keep: float, toned: float, q: vec2, rad: float, press: float, reach: float, px: float, hover: float, focus: float, disabled: float, light_dir: vec3, k1: vec4, k2: vec4, k3: vec4, k4: vec4, k5: vec2, body: vec3, light_ink: vec3, shadow_ink: vec3, sky: vec3) -> vec4 {
let live = 1.0 - disabled
let l = normalize(light_dir)
let ev = pow(2.0, k4.x)
let ro = k4.y
let hk = max(rad * 0.75 - 0.5 * press, 1.0)
let wr = rad * 0.07
let rc = rad * 0.49
let cr = vec2(0.0, rc)
let z_top = hk * 0.30 + rad * 0.16
let z_foot = wr
let rs = rad * 0.5
let tilt = (z_top - z_foot) / (2.0 * rc)
// The shadow's toss and blur per point of height, as the stud's.
let toss = -l.xy / max(l.z, 0.25) * 0.7
let bk = k5.x / max(k5.y, 0.001)
let ql = length(q)
let dk = ql - rs
let e = q - cr
let rho = length(e)
let dr = abs(rho - rc) - wr
var cov_k = 0.0
var cov_r = 0.0
var vis = 0.0
var face_k = body
var face_r = body
var under = 0.0
if min(dk, dr) < reach + 2.0 {
// The knob's surface here, and the ring's.
let r01 = ql / rad
let pr = lq_pull_profile(r01)
// The slope as the rise over one device pixel across the radius.
let dpx = px * 0.5 / rad
let slope = (lq_pull_profile(r01 + dpx).x - lq_pull_profile(max(r01 - dpx, 0.0)).x) / (r01 + dpx - max(r01 - dpx, 0.0))
let zk = pr.x * hk
let dir = q / max(ql, 0.0001)
let nk = normalize(vec3(-dir * slope * hk / rad, 1.0))
let er = e / max(rho, 0.0001)
let s = clamp((rho - rc) / (wr + px * 0.5), -1.0, 1.0)
let sq = sqrt(max(1.0 - s * s, 0.0))
let zc = z_top - tilt * rc * (1.0 + er.y)
let zr = zc + wr * sq
let nr = normalize(vec3(er.x * s, er.y * s + tilt * sq, sq - er.y * tilt * s))
cov_k = Cap.cover(dk, px)
cov_r = Cap.cover(dr, px)
vis = clamp((zr - zk) / (px * (abs(slope) * hk / rad + 0.5)) + 0.5, 0.0, 1.0)
// The shadows. The ring's, thrown from its height over whatever
// lies under this point, where the ring itself is not on top.
let hr = max(zc - zk, 0.0)
let qo = q - toss * hr
let dro = abs(length(qo - cr) - rc) - wr
let br = max(hr * bk, px)
let s_ring = (1.0 - smoothstep(-br * 0.5, br, dro)) * k4.z * live * (1.0 - cov_r * vis)
// The skirt's, on the ground; the nub's, on the skirt and the ground.
let hs_k = hk * 0.27
let bs = max(hs_k * bk, px)
let s_skirt = (1.0 - smoothstep(-bs * 0.35, bs, length(q - toss * hs_k) - rs * 0.96)) * (1.0 - cov_k)
let hn = max(hk * 0.98 - zk, 0.0)
let bn = max(hn * bk, px)
let s_nub = (1.0 - smoothstep(-bn * 0.35, bn, length(q - toss * hn) - rad * 0.31)) * smoothstep(rad * 0.31, rad * 0.34 + px, ql)
let ct = Cap.contact(dk, max(hk * 0.3, 1.0), k4.w * (1.0 - 0.6 * disabled))
// The ring's contact shade, close round the wire and as strong as
// the wire lies low over what is under it (the skirt or the ground).
let gap = max(zc - wr - zk, 0.0)
let ao_r = exp(-max(dr, 0.0) / max(wr * 0.7, px)) * exp(-gap / max(wr, px)) * (1.0 - cov_r * vis) * smoothstep(rad * 0.30, rad * 0.40, ql) * k4.w * (1.0 - 0.6 * disabled)
under = clamp((s_skirt + s_nub) * k4.z * live + ct + s_ring + ao_r * 0.9, 0.0, 1.0) * (1.0 - cov_k)
// The knob, lit as the stud is, from an eye nearer over it than over
// the stud (3.6 times as near), as his close photographs were taken:
// the level nub and skirt then mirror the studio round them, the dark
// camera only in their middle, where a flat mirror shows it.
let near = k2.w * 3.6
let vk = normalize(vec3(-(q / rad) * near, 1.0))
// The skirt is the knob's face, not its foot: its occlusion counts
// the height from the case up to the skirt's level, so only the
// rim's fall to the case lies in it.
let ho = clamp(pr.x / 0.25, 0.0, 1.0)
let litk = Cap.shade(nk, l, vk, ho, body, 0.0, 0.0, 1.0, k1.x, k1.y, k1.z, k1.w, k2.x, k2.y, 0.0, k3.x, k3.w, body, light_ink, shadow_ink, sky)
let dullk = Cap.shade(nk, l, vk, ho, body, 0.0, 0.0, 1.0, 0.0, k1.y, 0.0, 0.0, k2.x, k2.y, 0.0, k3.x, k3.w, body, light_ink, shadow_ink, sky)
let occk = 1.0 - k2.x * pow(1.0 - ho, max(k2.y, 0.2))
let rvk = -vk - nk * (2.0 * dot(nk, -vk))
face_k = mix(dullk, lq_studio(rvk) * mix(vec3(1.0, 1.0, 1.0), body, k3.x) * mix(1.0, occk, 0.35), k2.z) + (litk - dullk)
// In the shadows of the nub and the ring, and dark in the hole round
// the wire where it goes into the nub.
face_k = face_k * mix(vec3(1.0, 1.0, 1.0), shadow_ink, clamp(s_nub * k4.z * live + s_ring, 0.0, 1.0))
// Where the ring lies low over the skirt, the skirt darkens close
// round the wire, so the ring reads on the knob's own silver.
face_k = face_k * mix(vec3(1.0, 1.0, 1.0), shadow_ink, clamp(ao_r * 0.9, 0.0, 1.0))
let hole = (1.0 - smoothstep(-px * 0.5, px * 0.5 + max(wr * 0.7, px), dr)) * (1.0 - vis) * smoothstep(rad * 0.24 - px, rad * 0.30, ql) * (1.0 - smoothstep(rad * 0.34, rad * 0.40 + px, ql))
face_k = mix(face_k, face_k * vec3(0.10, 0.10, 0.11), hole * 0.85)
// The ring, lit alike; its height share for the occlusion.
let qv = q / rad
let vr = normalize(vec3(-qv / max(length(qv) / 1.1, 1.0) * near, 1.0))
let h01 = clamp(zr / (hk * 0.25), 0.0, 1.0)
let litr = Cap.shade(nr, l, vr, h01, body, 0.0, 0.0, 1.0, k1.x, k1.y, k1.z, k1.w, k2.x, k2.y, 0.0, k3.x, k3.w, body, light_ink, shadow_ink, sky)
let dullr = Cap.shade(nr, l, vr, h01, body, 0.0, 0.0, 1.0, 0.0, k1.y, 0.0, 0.0, k2.x, k2.y, 0.0, k3.x, k3.w, body, light_ink, shadow_ink, sky)
let occr = 1.0 - k2.x * pow(1.0 - h01, max(k2.y, 0.2))
let rvr = -vr - nr * (2.0 * dot(nr, -vr))
face_r = mix(dullr, lq_studio(rvr) * mix(vec3(1.0, 1.0, 1.0), body, k3.x) * mix(1.0, occr, 0.35), k2.z) + (litr - dullr)
face_r = mix(face_r, face_r * shadow_ink, 0.7 * (1.0 - smoothstep(0.31, 0.45, r01)))
}
// Toned as the bench tones, lifted under the hand, dulled when disabled.
var fk = Cap.tone(face_k, ev, ro)
var fr = Cap.tone(face_r, ev, ro)
let lift = vec3(0.04, 0.04, 0.042) * hover * (1.0 - press) * live
fk = fk + lift
fr = fr + lift
let lk = Finish.lum(fk)
fk = mix(fk, mix(fk, vec3(lk, lk, lk), 0.45) * 0.8, disabled)
let lr = Finish.lum(fr)
fr = mix(fr, mix(fr, vec3(lr, lr, lr), 0.45) * 0.8, disabled)
// Laid over what lies under it, as the stud is.
let dim = mix(vec3(1.0, 1.0, 1.0), shadow_ink, under)
let kept = keep * (1.0 - under * (1.0 - (shadow_ink.x + shadow_ink.y + shadow_ink.z) / 3.0))
var low = under_ink * dim
if toned > 0.5 {
low = Cap.tone(low, ev, ro)
}
var res = vec4(low, 1.0 - kept)
res = mix(res, vec4(fk, 1.0), cov_k)
res = mix(res, vec4(fr, 1.0), cov_r * vis)
let ring = Finish.ring_out(dk, 0.0, px, px) * 0.9 + Finish.glow(dk - px, 0.7) * 0.35 * (1.0 - Finish.cover(dk, px))
return Finish.over(res, vec4(0.498, 0.706, 0.910, 1.0) * clamp(ring, 0.0, 1.0) * focus * live * (1.0 - cov_r * vis))
}
// The fader: the silver ring pull (`lq_pull`) running a slot, its skirt
// 18.75 points across whatever the fader's width (on his word, 'the whole
// cap and ring can be 50% bigger', half as big again as the 12.5 it was),
// its ring hanging down the screen. The skirt decides where the cap stops,
// not the ring (on his word: 'the skirt radius decides the top/bottom of
// the cap (no the ring)'), and at either stop it covers the slot's end:
// cap_size, what the widget's travel, drag and cap field count, is 13.5
// points, the post under the knob, so at a stop the knob's middle stands
// 6.75 points in from the slot's end and the 18.75 point skirt reaches
// 2.6 points past it, over the end's rounded corners too. The skirt is
// 25/18 of cap_size across, and the nub and the ring are measured on twice
// that, the bench knob the skirt was halved from, held to half the
// fader's width (20 points on a 40 point fader, so this one is not held). The slot is this sheet's: 9.33 points wide
// (fourteen device pixels at 1.5 dpi, a little wider than the sheet's other
// slots so the zipper's teeth have room) and running the whole control, its
// ends rounded by 0.3 of its half width, in the wine black of the sheet's
// wells, with its own shade and occlusion, and no scale and no well. Its
// filled part, from the value's zero up to the knob, is a closed metal
// zipper seen from above; past the knob the slot is open
// and plain. Hovered, the pull lifts as the
// sheet's chrome caps do; held, it sinks half a point and the lift goes;
// focused, the sheet's ice-blue ring; disabled, it dulls as the sheet's caps
// do and throws no shadow, the zipper's teeth dull with it, and the slot
// takes the sheet's disabled well colour.
mod.widgets.SliderFader.cap_size = 13.5
mod.widgets.SliderFader.track_inset = 0.0
mod.widgets.SliderFader.draw_bg.track_size = uniform(9.3333)
// The library's own cap's share across, left at what the 25 point stud
// was of a 40 point fader; the ring pull does not read it.
mod.widgets.SliderFader.draw_bg.cap_cross = uniform(0.625)
mod.widgets.SliderFader.clip_x = false
mod.widgets.SliderFader.clip_y = false
// The stud's silver for the ring pull, and the slot's inner shadow, which stays the sheet's.
lq_stud_material(mod.widgets.SliderFader.draw_bg)
// The ring pull's highlight is the knob bench's chrome, polished: tight
// (roughness 0.06) and with no broad sheen, so the knob's level nub and
// skirt mirror the studio instead of washing white.
mod.widgets.SliderFader.draw_bg.cap_roughness = uniform(0.06)
mod.widgets.SliderFader.draw_bg.cap_gloss = uniform(0.0)
mod.widgets.SliderFader.draw_bg.cap_inner = uniform(0.42)
// The slot's inks. The ground is the sheet's lacquer: the slot's floor is
// reckoned against it. The slot is the sheet's well colour, its disabled one
// when the fader is, under the sheet's own shade and occlusion.
mod.widgets.SliderFader.draw_bg.ground_color = uniform(theme.color_bg_app)
mod.widgets.SliderFader.draw_bg.track_color = uniform(theme.color_inset)
mod.widgets.SliderFader.draw_bg.track_color_disabled = uniform(theme.color_inset_disabled)
mod.widgets.SliderFader.draw_bg.slot_shadow_color = uniform(#5c6066)
mod.widgets.SliderFader.draw_bg.slot_ao = uniform(1.0)
mod.widgets.SliderFader.draw_bg.vertex = fn() {
let m = max(lq_stud_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, self.cap_px), self.cap_px * 1.46 + 4.0)
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.SliderFader.draw_bg.pixel = fn() {
let m = max(lq_stud_room(self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, self.cap_px), self.cap_px * 1.46 + 4.0)
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let size = self.rect_size
let p = self.pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let live = 1.0 - self.disabled
let vert = self.is_vertical
// Track space: `ab.x` along the travel from the value's zero (up the
// screen when the fader stands), `ab.y` across it; `ob` is where the
// across axis starts on screen, for snapping.
let ab = vec2(mix(p.x, size.y - p.y, vert), mix(p.y, p.x, vert))
let along = mix(size.x, size.y, vert)
let across = mix(size.y, size.x, vert)
let ob = mix(o.y, o.x, vert)
let travel = max(along - self.inset_px * 2.0 - self.cap_px, 1.0)
let foot = self.inset_px + self.cap_px * 0.5
let mid = lq_snap(across * 0.5, ob, px)
// The cap's half size, the post's: cap_size each way, held to the
// control.
let hs = vec2(self.cap_px * 0.5, min(self.cap_px, across) * 0.5)
// The cap's leading end (the left one lying, the top one standing) on
// the device pixel grid, so its ends stand crisp and it keeps one shape
// as it moves.
let at_free = foot + self.slide_pos * travel
let lead = Finish.snap(mix(o.x + at_free - hs.x, o.y + size.y - at_free - hs.x, vert), px)
let at = mix(lead - o.x + hs.x, o.y + size.y - lead - hs.x, vert)
// The key light, turned into track space.
let lt = Cap.light(normalize(self.cap_light), vert)
let ground = self.ground_color.rgb
let light_ink = self.light_color.rgb
let body = self.handle_color.rgb
let black = vec3(0.0, 0.0, 0.0)
// THE SLOT: track_size wide with its long edges on the pixel grid, as
// the sheet's slots are, from one track inset to the other, its ends
// rounded by 0.3 of its half width. It is drawn twice: on black, which
// is its own light (the floor, the walls, and the lip it lights outside
// itself, which fades when disabled as the sheet's lips do), and on the
// ground, which says how much of the ground it leaves showing. The fader
// is laid over whatever lies under it, and the story's canvas is not one
// flat colour.
let slot_shade = self.slot_shadow_color.rgb
let tw = min(self.track_size, across)
let s_lo = lq_snap(mid - tw * 0.5, ob, px)
let s_hi = lq_snap(mid + tw * 0.5, ob, px)
let tlen = max(along - self.inset_px * 2.0, 1.0)
let tbox = vec4(self.inset_px + tlen * 0.5, (s_lo + s_hi) * 0.5, tlen * 0.5, max(s_hi - s_lo, px) * 0.5)
let tr = 0.3 * tbox.w
let track_ink = mix(self.track_color.rgb, self.track_color_disabled.rgb, self.disabled)
let slot_light = light_ink * (1.0 - 0.6 * self.disabled)
var slot_own = Cap.slot(black, ab, tbox, tr, 0.0, foot, foot, at, hs.x, lt, px, 0.0, 0.0, track_ink, track_ink, slot_light, slot_shade, self.cap_inner, self.slot_ao)
let slot_over = Cap.slot(ground, ab, tbox, tr, 0.0, foot, foot, at, hs.x, lt, px, 0.0, 0.0, track_ink, track_ink, slot_light, slot_shade, self.cap_inner, self.slot_ao)
let shows = clamp(dot(slot_over - slot_own, vec3(1.0, 1.0, 1.0)) / max(dot(ground, vec3(1.0, 1.0, 1.0)), 0.001), 0.0, 1.0)
// THE ZIPPER. From the value's zero up to the stud (out of the default's
// place when the fader grows its fill from there; from the slot's own
// end when it fills from the stop, and on to the far end when the fill
// runs there) the slot is a closed metal zipper seen from above: its
// floor is the tape, and two rows of silver teeth come in from its long
// edges, one edge and then the other, and mesh down the middle, so the
// chain reads as one column of stacked links. A tooth every 2 points
// along the travel (three device pixels at 1.5 dpi), each as thick as
// its step: in the middle, where the two rows overlap, each tooth lies
// against the next, and out toward the edges each has a gap as thick as
// itself before the next from its own edge. A tooth is a block with
// rounded ends, standing one device pixel in from its edge and reaching
// two past the middle. It is polished silver, so it mirrors the room, a
// broad bright patch above the eye a little toward the light that falls
// dark away from it: round across its thickness, so a band near white
// runs along its middle and the side toward the light, and the far side
// is dark; and its top falling away from a ridge across its middle, so
// the half toward the light is the brighter and the other half greyer.
// Its two ends are a little darker and its four corners darker still,
// so the ends read round. Everything is counted in
// whole device pixels from the slot's zero end and its long edge, so
// every tooth lands on the pixel grid and keeps its place in the slot as
// the stud runs over it; only whole teeth are laid. The stud's shadow
// falls on the teeth as on the slot.
let z_end = mix(lq_snap(self.inset_px, o.x, px), size.y - lq_snap(size.y - self.inset_px, o.y, px), vert)
let z_far = mix(lq_snap(size.x - self.inset_px, o.x, px), size.y - lq_snap(self.inset_px, o.y, px), vert)
let z_lo = mix(foot + self.fill_lo * travel, z_end + 2.0 * px, step(self.fill_lo, 0.0))
let z_hi = mix(foot + self.fill_hi * travel, z_far - 2.0 * px, step(1.0, self.fill_hi))
let pitch = max(floor(2.0 / px + 0.5), 2.0)
let ia = floor((ab.x - z_end) / px)
let ic = floor((ab.y - s_lo) / px)
let nc = floor((s_hi - s_lo) / px + 0.5)
let k = floor(ia / pitch)
let ka = ia - k * pitch
let side = k - 2.0 * floor(k * 0.5)
let t_len = floor((nc - 2.0) * 0.5) + 2.0
let c0 = mix(1.0, nc - 1.0 - t_len, side)
let c1 = c0 + t_len
let t0 = z_end + k * pitch * px
let tooth = step(c0, ic) * step(ic, c1 - 1.0) * step(z_lo, t0) * step(t0 + pitch * px, z_hi)
// Where in the tooth this pixel's middle lies: `tt` along the travel
// and `ts` across it, each -1..1 from one side to the other.
let tt = (ka + 0.5) / pitch * 2.0 - 1.0
let ts = (ic - c0 + 0.5) / t_len * 2.0 - 1.0
// Its normal in track space, then on screen (the travel runs up the
// screen when the fader stands), and the ray it mirrors.
let n_a = tt * 0.45
let n_c = 0.30 * clamp(ts * 2.0, -1.0, 1.0)
let z_n = normalize(vec3(mix(n_a, n_c, vert), mix(n_c, -n_a, vert), sqrt(max(1.0 - n_a * n_a - n_c * n_c, 0.04))))
let z_r = vec3(2.0 * z_n.z * z_n.x, 2.0 * z_n.z * z_n.y, 2.0 * z_n.z * z_n.z - 1.0)
// The room's bright patch: about 20 degrees off the eye toward the key
// light, falling off over some 45 degrees.
let z_l = normalize(self.cap_light)
let z_a = acos(clamp(dot(z_r, normalize(vec3(z_l.x * 0.55, z_l.y * 0.55, 1.0))), -1.0, 1.0)) * 57.2958
// The patch's light runs from a tenth of the handle's grey where the
// tooth faces away from it up to near white where it faces it.
let z_e = pow(clamp(exp(-(z_a * z_a) / 2025.0) / 0.86, 0.0, 1.0), 1.4)
var tooth_ink = mix(body * 0.10, vec3(0.95, 0.955, 0.965), z_e)
let t_end = 1.0 - step(c0 + 0.5, ic) * (1.0 - step(c1 - 1.5, ic))
let t_edge = 1.0 - step(0.5, ka) * (1.0 - step(pitch - 1.5, ka))
tooth_ink = tooth_ink * (1.0 - 0.15 * t_end - 0.40 * t_end * t_edge)
let tooth_lum = Finish.lum(tooth_ink)
tooth_ink = mix(tooth_ink, mix(tooth_ink, vec3(tooth_lum, tooth_lum, tooth_lum), 0.45) * 0.8, self.disabled)
slot_own = mix(slot_own, tooth_ink, tooth)
// THE CAP, the ring pull, its knob at its place on the track, drawn on
// screen so its ring hangs down whichever way the fader lies.
let cs = vec2(mix(at, mid, vert), mix(mid, size.y - at, vert))
return lq_pull(slot_own, shows, 1.0, p - cs, min(self.cap_px * 1.3889, across * 0.5), self.drag * live, m, px, self.hover, self.focus, self.disabled, self.cap_light, vec4(self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim), vec4(self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_reflection_perspective), vec4(self.cap_metal, self.cap_spun, self.cap_glow, self.cap_light_intensity), vec4(self.cap_exposure, self.cap_rolloff, self.cap_shadow, self.cap_contact), vec2(self.cap_shadow_blur, self.cap_height), body, light_ink, self.shadow_color.rgb, self.sky_ground_color.rgb)
}
// ---------------------------------------------------------------- knobs
// A knob: a dark well ring with the value lit ice blue in it, round a cap
// of the same red lacquer as the window's ground, pressed into a shallow
// cone: from a thin edge its flanks run straight up to a soft rounded nub in
// the middle that rises out of the same lacquer, its top round and its foot
// running into the cone in a gentle fillet all round. A face of the cap
// turned to the eye has the ground's own red; its form shows in its shading
// and its sheen, not in a brighter red. The cap is a true solid of
// revolution, so as it turns its light and its reflections stand still and
// only the bar moves. The lacquer is lit from above and to the left and
// mirrors a broad soft box there, so a wide sheen runs down the lit flank
// and a broad soft patch lies on the nub. Through the nub runs a short
// polished steel bar with a chrome ball at either end, close to the nub; the
// bar turns with the value, and the ball at the value's end carries a small
// pip of the value's ice blue in its middle, so the bar is the pointer. A
// press lights the pip white-hot with a halo.
// Angles run from straight down, clockwise, the gap at the bottom.
// The window's ground without its reflections, at `uv` (shares of the
// window across and down): bright red at the top falling to near-black wine
// at the foot, the sides a little darker. The window draws it, and a knob
// takes it at its own middle for its lacquer.
let lq_ground_base = fn(uv: vec2) -> vec3 {
var col = mix(vec3(0.660, 0.070, 0.090), vec3(0.340, 0.048, 0.058), smoothstep(0.0, 0.55, uv.y))
col = mix(col, vec3(0.170, 0.020, 0.028), smoothstep(0.45, 1.0, uv.y))
let vx = uv.x * 2.0 - 1.0
return col * (1.0 - 0.20 * vx * vx)
}
// The ground's red under the point `w` of the window: `a` and `b` are the
// pass's projection of the window's origin and of the point one point down
// and to the right of it, which give the window's size.
let lq_ground_at = fn(w: vec2, a: vec4, b: vec4) -> vec3 {
let ws = vec2(2.0 / max(abs(b.x - a.x), 0.000001), 2.0 / max(abs(b.y - a.y), 0.000001))
return lq_ground_base(clamp(w / ws, vec2(0.0, 0.0), vec2(1.0, 1.0)))
}
// The cap's height at `v` (screen offset from the middle, in shares of the
// cap's radius), and how deep in the fillet round the nub's foot it lies
// (0..1). It depends on the distance from the middle alone, so the cap is
// a perfect revolve.
let lq_knob_h = fn(v: vec2) -> vec2 {
let rn = length(v)
// The cone: straight from the thin edge up toward the middle, its top
// rounded off under the nub; the very edge rounds down a little.
let ee = smoothstep(0.96, 1.0, rn)
var hc = 0.42 * (1.05 - sqrt(rn * rn + 0.0144)) - 0.012 * ee * ee
// The nub: a rounded bump 0.36 in radius and 0.10 high on the cone's
// top, round over its top, steepest a little past half way out and
// flattening into the cone at its foot, so it meets the cone in a
// fillet with no edge.
let s = min(rn / 0.36, 1.0)
let t = 1.0 - s * s
return vec2(hc + 0.10 * t * t, smoothstep(0.45, 0.75, s) * (1.0 - smoothstep(0.80, 1.0, s)))
}
// The sheet's chrome as a bar mirrors it, `w` across the bar from -1 to 1
// and `per` the screen direction that runs: the bright sky above, the red
// lacquer below, a hot line where it faces the light, darker toward its
// sides.
let lq_bar_ink = fn(per: vec2, w: float, hover: float, disabled: float) -> vec3 {
let n = per * w
let nz = sqrt(max(1.0 - w * w, 0.0))
let fac = dot(vec3(n.x, n.y, nz), normalize(vec3(-0.30, -0.70, 0.65)))
var col = mix(vec3(0.46, 0.30, 0.32), vec3(0.94, 0.94, 0.96), smoothstep(0.45, -0.50, n.y))
col = col + vec3(1.0, 1.0, 1.0) * pow(max(fac, 0.0), 20.0) * 0.65
col = mix(col, vec3(0.12, 0.08, 0.09), smoothstep(0.70, 1.0, abs(w)) * 0.6)
col = col * (1.0 + 0.16 * hover)
let fl = Finish.lum(col)
return mix(col, mix(vec3(fl, fl, fl), vec3(0.45, 0.25, 0.27), 0.4) * 0.75, disabled)
}
let lq_knob = fn(p: vec2, c: vec2, rr: float, px: float, slide: float, origin: float, arc_origin: float, gap: float, hover: float, held: float, focus: float, disabled: float, ground: vec3) -> vec4 {
// The ring scales with a small knob and stops at a 1.5 point gap and a
// 4 point width, so a large dial stays slim instead of a heavy donut.
let rc = max(rr / 1.32, rr - 5.5)
let ring_in = rc + min(0.08 * rc, 1.5)
let ring_out = ring_in + min(0.24 * rc, 4.0)
let ring_mid = (ring_in + ring_out) * 0.5
let ring_w = ring_out - ring_in
let dr = Finish.sd_ring(p, c, ring_mid, ring_w)
let q = p - c
let rl = length(q)
// The ring well: lip on the ground under its lower outer edge, the
// black outline both sides, a dark fill.
var res = vec4(0.70, 0.52, 0.54, 1.0) * 0.6 * Finish.ring_out(rl - ring_out, 0.0, px, px) * step(c.y + ring_out * 0.3, p.y)
var col = vec3(0.125, 0.043, 0.051)
col = mix(col, vec3(0.110, 0.016, 0.020), Finish.band(dr, 0.0, px, px))
let ra = Finish.cover(dr, px)
res = Finish.over(res, vec4(col * ra, ra))
// The value arc, lit like the lamp: 1.5 points on the middle of the
// ring, a pale core, the accent at its edges and a soft halo on the
// ring's floor round it. Angles from straight down, clockwise.
let one = PI / 180.0
let sweep = 2.0 * PI - gap * one
let start = gap * one * 0.5
let ve = start + sweep * slide
let oe = start + sweep * origin
let a0 = mix(start, min(oe, ve), arc_origin)
let a1 = mix(ve, max(oe, ve), arc_origin)
var ang = atan2(-q.x, q.y)
if ang < 0.0 {
ang = ang + 2.0 * PI
}
let ac = clamp(ang, a0, a1)
let ap = c + vec2(-sin(ac), cos(ac)) * ring_mid
let da = length(p - ap)
let live = step(0.002, a1 - a0)
let ink = mix(vec3(0.227, 0.655, 0.965), vec3(0.227, 0.29, 0.388), disabled)
let floor_in = Finish.cover(dr + px, px)
let xa = max(da - 0.75, 0.0)
let ga = (0.30 * exp(-(xa * xa) / (2.0 * 0.55 * 0.55)) + 0.14 * exp(-(xa * xa) / (2.0 * 1.5 * 1.5))) * floor_in * (1.0 - Finish.cover(da - 0.75, px)) * (1.0 - disabled) * live
res = Finish.over(res, vec4(0.227, 0.655, 0.965, 1.0) * ga)
let acol = mix(mix(vec3(0.227, 0.655, 1.0), vec3(1.0, 1.0, 1.0), 0.72 * (1.0 - disabled)), ink, smoothstep(0.1, 0.7, da))
res = Finish.over(res, vec4(acol, 1.0) * Finish.cover(da - 0.75, px) * live)
// The cap throws a soft shadow down across the ring.
let shc = Finish.drop(p, c, vec2(rc, rc) * 0.9, rc * 0.9, vec2(0.0, rc * 0.16), rc * 0.14) * 0.7 * (1.0 - 0.5 * disabled)
res = Finish.over(res, vec4(0.02, 0.0, 0.0, 1.0) * shc * (1.0 - Finish.cover(rl - rc, px)))
// THE CAP. Its height, read at this pixel and half a device pixel to
// either side, gives its normal.
let dc = rl - rc
let rcap = max(rc, 0.5)
let rn = rl / rcap
let vn = q / rcap
let u = q / max(rl, 0.001)
let he = 0.5 * px / rcap
let h0 = lq_knob_h(vn)
let hx = lq_knob_h(vn + vec2(he, 0.0)).x - lq_knob_h(vn - vec2(he, 0.0)).x
let hy = lq_knob_h(vn + vec2(0.0, he)).x - lq_knob_h(vn - vec2(0.0, he)).x
let n = normalize(vec3(-hx / (2.0 * he), -hy / (2.0 * he), 1.0))
// The key light comes from above and a little to the left, as on the
// sheet's other caps. The lacquer is lit by it and mirrors a broad soft
// box above the light, the room's faint sky over the top and a little of
// the floor along the foot.
let kl = normalize(vec3(-0.30, -0.70, 0.65))
let diff = pow(clamp((dot(n, kl) + 0.25) / 1.25, 0.0, 1.0), 1.3)
let rv = vec3(2.0 * n.z * n.x, 2.0 * n.z * n.y, 2.0 * n.z * n.z - 1.0)
// The soft box: a patch of sky some 45 degrees out from the eye toward
// eleven o'clock, the angle the cone's flank mirrors, so its sheen runs
// the length of the flank toward the light as a broad streak. The nub
// curves too tightly to mirror the box's edges: it carries a broad soft
// round patch of it over its lit side and top, with no point.
let el = acos(clamp(rv.z, -1.0, 1.0))
let az = atan2(rv.y, rv.x)
let daz = abs(az + 1.97)
let dm = 1.0 - smoothstep(0.18, 0.36, rn)
let hk = normalize(kl + vec3(0.0, 0.0, 1.0))
let lobe = pow(max(dot(n, hk), 0.0), 6.0)
let box = mix((1.0 - smoothstep(0.10, 0.45, abs(el - 0.80))) * (1.0 - smoothstep(0.30, 0.95, min(daz, 6.2832 - daz))) * 0.16, lobe * 0.16, dm)
let sky = smoothstep(0.25, -0.85, rv.y) * 0.06
let flr = smoothstep(0.35, 0.85, rv.y) * 0.04
// The key light's own reflection, soft, on the cone only.
let spec = pow(max(dot(n, hk), 0.0), 30.0) * 0.10 * (1.0 - dm)
// The red: the ground's own red where the cap faces the eye, brighter
// where it turns to the light, deeper where it turns away, a little
// darker toward the thin edge and softly in the fillet round the nub.
var face = ground * (0.15 + 1.30 * diff)
face = face * mix(1.0, 0.92, smoothstep(0.6, 1.0, rn)) * (1.0 - 0.15 * h0.y)
// The gloss: the soft box's sheen, the sky and the floor, the key
// light's soft reflection, all in step with the room's light where the
// knob stands (as the ground is: brighter near the top of the window,
// dimmer at its foot), so the sheen keeps the same share over the red
// there and a knob low in the window is not washed pale; tinted by the
// lacquer; under the hand the sheen lifts.
let up = 1.0 - disabled
let room = clamp(ground.r / 0.35, 0.4, 1.2)
let gl = (box + sky + flr) * room * (1.0 + 0.45 * hover * up) * (1.0 - 0.5 * h0.y)
face = face + vec3(1.0, 0.62, 0.65) * gl
face = face + vec3(1.0, 0.86, 0.86) * spec * room * (1.0 + 0.3 * hover) * up
// Where the sheen would carry the red past full, it rolls off instead,
// so the lit flank keeps its gradation.
face = Cap.tone(face, 1.0, 0.7)
// THE CLEAR COAT over the red, glass smooth, so it mirrors the room
// crisply on top of the colour without changing it; it follows the
// cap's own surface, a revolve, so nothing it mirrors moves as the
// knob turns. The cone mirrors
// a ring of the room some 45 degrees out from the eye: the sheet's
// studio as its chrome mirrors it, faintly, only its bright parts (the
// soft box across the top; the grey room and the dark wall to the right
// sink into the red), kept off the lower side so the shaded flank stays
// as deep; the key light's soft box toward eleven o'clock, a narrow
// window mirrored down the lit flank as a bright band, level across,
// its edges under two device pixels soft at any size; and down the
// middle of that the key light itself, a hot line some two and a half
// device pixels wide at any size in a slightly wider glow, brightest on
// the middle of the flank and tapering toward either end. On the cone
// the mirrored azimuth turns with the point's own, so the distance
// across the band in device pixels is the azimuth's distance times the
// radius. The fillet round the nub's foot sweeps through every angle as
// it rolls into the nub, so the window, the studio and the hot line
// fade in only past it, on the flank, and leave no crease there. The
// nub keeps its broad soft patch, a little brighter, with no point.
let nc = n
let rvc = vec3(2.0 * nc.z * nc.x, 2.0 * nc.z * nc.y, 2.0 * nc.z * nc.z - 1.0)
let rxy = max(length(rvc.xy), 0.001)
let elc = acos(clamp(rvc.z, -1.0, 1.0))
let dzc = abs(atan2(rvc.y, rvc.x) + 1.97)
let wd = min(dzc, 6.2832 - dzc)
let wpx = wd * rl / px
let fil = smoothstep(0.34, 0.46, rn)
let elw = 1.0 - smoothstep(0.40, 0.50, abs(elc - 0.80))
let ring = max(lq_studio(vec3(rvc.x / rxy, rvc.y / rxy, 0.0)).x - 0.66, 0.0) * 2.4
let studio = ring * (1.0 - smoothstep(-0.30, 0.20, rvc.y / rxy)) * (1.0 - smoothstep(0.45, 0.90, abs(elc - 0.80)))
let win = (1.0 - smoothstep(-0.9, 0.9, (wd - 0.16) * rl / px)) * elw
let gs = 1.5 + 0.035 * rc / px
let along = elw * fil * (0.15 + 0.85 * exp(-(rn - 0.62) * (rn - 0.62) / 0.018)) * (1.0 - smoothstep(0.86, 0.97, rn))
let core = exp(-wpx * wpx / 2.25) * along
let glow = exp(-wpx * wpx / (gs * gs)) * along
// The coat is laid over the toned red as a screen, so it only lightens,
// and together it never takes more than four fifths of the way to
// white: the hot line stays a pale glint, never a blown patch, at rest,
// under the hand or held. The soft box and the studio follow the room's
// light, as the red's own sheen does, and so does the hot line's glow;
// its core only partly, so a knob at the foot of the window still shows
// a small bright glint without a pale smear round it. Under the hand
// the coat lifts a little with the sheen beneath it.
let coat = ((studio * 0.05 + win * 0.18) * fil + lobe * 0.16 * dm) * room * (1.0 + 0.20 * hover * up) * (1.0 - 0.5 * h0.y) * (1.0 - 0.6 * disabled)
let glint = min((0.7 * core * mix(1.0, room, 0.25) + 0.3 * glow * room) * 0.9, 0.85) * up
let keep = max((vec3(1.0, 1.0, 1.0) - vec3(1.0, 0.90, 0.90) * min(coat, 0.6)) * (1.0 - glint), vec3(0.2, 0.2, 0.2))
face = vec3(1.0, 1.0, 1.0) - (vec3(1.0, 1.0, 1.0) - face) * keep
// The thin edge: a bright line of the coat where it faces the light,
// the dark outline.
let ld = dot(u, vec2(-0.40, -0.92))
face = mix(face, vec3(1.0, 0.88, 0.88), Finish.band(dc, px, px * 2.0, px) * smoothstep(0.2, 0.85, ld) * 0.92 * up)
// Disabled, the lacquer greys toward its own lightness without growing
// darker, so the cap stays the ground's lacquer, dulled, not a hole.
let fl = Finish.lum(face)
face = mix(face, mix(face, vec3(fl, fl, fl), 0.45), disabled)
face = mix(face, mix(vec3(0.110, 0.016, 0.020), vec3(0.20, 0.10, 0.11), disabled), Finish.band(dc, 0.0, px, px))
// THE BAR: polished steel through the nub along the value's angle, a
// ball at either end lying on the cone close to the nub, with a short
// stub of bar, about a ball's radius, showing between: the balls 0.08 of
// the cap's radius, their middles 0.42 out. On a small knob the balls
// keep 1.6 points of radius and the stubs a point, so they read, and sit
// that much further out; on a knob too small to hold them they fade out.
let dir = vec2(-sin(ve), cos(ve))
let per = vec2(-dir.y, dir.x)
let lp = vec2(dot(q, dir), dot(q, per))
let vis = smoothstep(4.0, 7.0, rc)
let br = max(0.08 * rc, min(1.6, 0.2 * rc))
let bb = min(0.27 * rc + max(0.074 * rc, 1.0) + br, 0.92 * rc - br)
let bw = max(0.03 * rc, 0.6)
// The bar's shadow on the lacquer, down and to the right of it, where
// the bar is out of the nub.
let sh_o = vec2(0.30, 0.70) * bw * 1.4
let qs = q - sh_o
let ls = vec2(dot(qs, dir), dot(qs, per))
let ds = length(vec2(ls.x - clamp(ls.x, -bb, bb), ls.y)) - bw
let bar_sh = (1.0 - smoothstep(-bw * 0.6, bw * 1.2, ds)) * smoothstep(0.21, 0.28, abs(ls.x) / rcap) * vis
face = face * (1.0 - 0.45 * bar_sh * (1.0 - 0.5 * disabled))
let ca = Finish.cover(dc, px)
res = Finish.over(res, vec4(face * ca, ca))
// The bar: round, its axis half its radius over the cone 0.27 out from
// the middle, so it comes out of the nub's side; it shows where its top
// stands above the cap's height there.
let w = clamp(lp.y / bw, -1.0, 1.0)
let bz = 0.3169 + (bw / rcap) * (0.5 + sqrt(max(1.0 - w * w, 0.0)))
let shows = clamp((bz - h0.x) * rcap / px + 0.5, 0.0, 1.0)
let dbar = length(vec2(lp.x - clamp(lp.x, -bb, bb), lp.y)) - bw
let bcol = lq_bar_ink(per, w, hover, disabled)
let bcov = Finish.cover(dbar, px) * shows * vis
res = Finish.over(res, vec4(mix(bcol, vec3(0.10, 0.05, 0.06), Finish.band(dbar, 0.0, px * 0.7, px) * 0.6) * bcov, bcov))
// The balls: the sheet's polished chrome.
let tip = c + dir * bb
res = mix(res, lq_ball(res, p, c - dir * bb, br, px, hover, 0.0, disabled, 0.6), vis)
res = mix(res, lq_ball(res, p, tip, br, px, hover, 0.0, disabled, 0.6), vis)
// The pip in the middle of the ball at the value's end: a little over a
// quarter of the ball across, so the chrome shows all round it, its
// middle pale ice and its edge the value's blue. On a small knob it is
// never less than a device pixel in radius and all the value's blue, so
// the pointing ball tells from the other. Held, it lights white-hot and
// a halo of the blue spreads over the ball; disabled it is a dark lens.
let rp = max(0.27 * br, px)
let dp = length(p - tip)
let xp = max(dp - rp, 0.0)
let halo = exp(-(xp * xp) / max(2.0 * 0.16 * br * br, 0.001)) * 0.75 * held * up * vis
res = Finish.over(res, vec4(0.227, 0.655, 0.965, 1.0) * halo)
let pin = clamp(dp / rp, 0.0, 1.0)
let pale = mix(vec3(0.24, 0.68, 1.0), vec3(0.62, 0.86, 1.0), smoothstep(1.2 * px, 2.5 * px, rp))
var pcol = mix(mix(pale, vec3(0.97, 0.99, 1.0), held), mix(vec3(0.227, 0.655, 0.965), vec3(0.50, 0.78, 1.0), held), smoothstep(0.30, 1.0, pin))
pcol = mix(pcol, vec3(0.09, 0.10, 0.12), disabled)
res = Finish.over(res, vec4(pcol, 1.0) * Finish.cover(dp - rp, px) * vis)
// Focus: the ice ring round the well.
let fd = rl - ring_out
let ring = Finish.ring_out(fd, 0.0, px, px) * 0.9 + Finish.glow(fd - px, 0.7) * 0.35 * (1.0 - Finish.cover(fd, px))
return Finish.over(res, vec4(0.498, 0.706, 0.910, 1.0) * clamp(ring, 0.0, 1.0) * focus * (1.0 - disabled))
}
mod.widgets.RotaryKnob.clip_x = false
mod.widgets.RotaryKnob.clip_y = false
mod.widgets.RotaryKnob.draw_bg.vertex = fn() {
let m = 5.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.RotaryKnob.draw_bg.pixel = fn() {
let m = 5.0
let px = lq_px(self.draw_pass.dpi_factor)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let dia = min(self.rect_size.x, self.rect_size.y)
let c = vec2(Finish.snap(self.rect_pos.x + self.rect_size.x * 0.5, px) - self.rect_pos.x, Finish.snap(self.rect_pos.y + self.rect_size.y - dia * 0.5, px) - self.rect_pos.y)
return lq_knob(p, c, dia * 0.5 - 1.0, px, self.slide_pos, self.origin_pos, self.arc_origin, self.gap, self.hover, self.drag, self.focus, self.disabled, lq_ground_at(self.world.xy - p + c, self.draw_pass.camera_projection * (self.draw_pass.camera_view * vec4(0.0, 0.0, 0.0, 1.0)), self.draw_pass.camera_projection * (self.draw_pass.camera_view * vec4(1.0, 1.0, 0.0, 1.0))))
}
mod.widgets.RotaryFlat.clip_x = false
mod.widgets.RotaryFlat.clip_y = false
mod.widgets.RotaryFlat.draw_bg.vertex = fn() {
let m = 5.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.RotaryFlat.draw_bg.pixel = fn() {
let m = 5.0
let px = lq_px(self.draw_pass.dpi_factor)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let dia = min(self.rect_size.x, self.rect_size.y - 22.0)
let c = vec2(Finish.snap(self.rect_pos.x + self.rect_size.x * 0.5, px) - self.rect_pos.x, Finish.snap(self.rect_pos.y + 20.0 + dia * 0.5, px) - self.rect_pos.y)
return lq_knob(p, c, dia * 0.5 - 1.0, px, self.slide_pos, self.origin_pos, self.arc_origin, self.gap, self.hover, self.drag, self.focus, self.disabled, lq_ground_at(self.world.xy - p + c, self.draw_pass.camera_projection * (self.draw_pass.camera_view * vec4(0.0, 0.0, 0.0, 1.0)), self.draw_pass.camera_projection * (self.draw_pass.camera_view * vec4(1.0, 1.0, 0.0, 1.0))))
}
// ---------------------------------------------------------------- fields
// A field well that keeps inside its own box: a text field clips what it
// holds, so its right outline takes the box's last whole column and its lip
// the last whole row. Focus lights a lamp along the foot of the well: a
// pale line with the lamp's rim colour under it and a soft halo rising on
// the floor; the outline takes a little of the blue.
let lq_field = fn(p: vec2, size: vec2, o: vec2, px: float, fill: vec3, hover: float, focus: float, disabled: float) -> vec4 {
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
// The box's far edges rounded DOWN to a whole device pixel; the bottom
// outline one pixel above the last row, the lip on it.
let hi = vec2(lq_snap(size.x - px * 0.49, o.x, px), lq_snap(size.y - px * 0.49, o.y, px) - px)
let c = (lo + hi) * 0.5
let d = Material.sd_box(p, c, (hi - lo) * 0.5, 2.0)
// Under the hand the floor lifts 0.025 and the outline takes a fifth of
// the lamp's blue.
var f = mix(fill + vec3(0.025, 0.025, 0.027) * hover, vec3(0.200, 0.090, 0.100), disabled)
var res = lq_well(p, lo, hi, 2.0, px, f, 0.0, disabled, 1.0)
let fo = focus * (1.0 - disabled)
let ho = hover * (1.0 - disabled) * (1.0 - focus)
res = Finish.over(res, vec4(0.40, 0.62, 0.84, 1.0) * Finish.band(d, 0.0, px, px) * (0.45 * fo + 0.2 * ho))
// The foot lamp: the two rows above the bottom outline, inset past the
// corners.
let y1 = hi.y - px
let infloor = Finish.cover(d + px, px) * step(lo.x + 3.0, p.x) * step(p.x, hi.x - 3.0)
let dy = max(y1 - px - p.y, 0.0)
let g = (0.34 * exp(-(dy * dy) / (2.0 * 0.7 * 0.7)) + 0.14 * exp(-(dy * dy) / (2.0 * 2.0 * 2.0))) * step(p.y, y1 - px) * infloor
res = Finish.over(res, vec4(0.40, 0.62, 0.84, 1.0) * g * fo)
let rim = step(y1 - px, p.y) * step(p.y, y1) * infloor
let core = step(y1 - px * 2.0, p.y) * step(p.y, y1 - px) * infloor
res = Finish.over(res, vec4(0.40, 0.62, 0.84, 1.0) * rim * fo)
res = Finish.over(res, vec4(0.84, 0.94, 1.0, 1.0) * core * fo)
return res
}
// Text fields: a flat well 24 points tall, -0.10 against the ground. An
// input with no fill of its own (the one a slider, a combo box or a well
// holds) draws nothing.
mod.widgets.TextInputFlat.draw_bg.pixel = fn() {
if self.color.a < 0.01 {
return vec4(0.0, 0.0, 0.0, 0.0)
}
let px = lq_px(self.draw_pass.dpi_factor)
return lq_field(self.pos * self.rect_size, self.rect_size, self.rect_pos, px, vec3(0.125, 0.043, 0.051), self.hover, self.focus, self.disabled)
}
mod.widgets.TextInputFlat.padding = mod.turtle.Inset{left: 8.0 right: 8.0 top: 3.0 bottom: 3.0}
mod.widgets.TextInputFlat.min_height = 24.0
mod.widgets.TextInputFlat.draw_cursor.color = #f4faff
mod.widgets.TextInputFlat.draw_text.color = #f6e8e6
mod.widgets.TextInputFlat.draw_text.color_empty = #a88486
mod.widgets.TextInputFlat.draw_text.color_disabled = #b07a7c
// The number field: the same well, its stepper a pair of small caps joined
// by an engraved line.
mod.widgets.NumberField.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
return lq_field(self.pos * self.rect_size, self.rect_size, self.rect_pos, px, vec3(0.125, 0.043, 0.051), self.hover, self.focus, self.disabled)
}
mod.widgets.NumberField.spin_width = 14.0
mod.widgets.NumberSpin.width = 14.0
// The widget's own filled triangles are hidden; the face strokes a 5 x 3
// point chevron in a 1 dp line of #e0c4c4 on each cap instead.
mod.widgets.NumberSpin.draw_text.color = #00000000
mod.widgets.NumberSpin.draw_text.color_disabled = #00000000
mod.widgets.NumberSpin.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * self.rect_size
// The stepper lies inside the field's well: its own floor is the
// field's, set off by an engraved divider (dark, then light) and split
// by an engraved line across the middle. The field's outline and lip
// stay the field's.
let x0 = lq_snap(0.0, o.x, px)
let xr = lq_snap(self.rect_size.x - px * 0.49, o.x, px) - px
let yt = lq_snap(0.0, o.y, px) + px
let yb = lq_snap(self.rect_size.y - px * 0.49, o.y, px) - px * 2.0
let mid = lq_snap(self.rect_size.y * 0.5, o.y, px)
let live = 1.0 - self.disabled
let inx = step(x0 + px * 2.0, p.x) * step(p.x, xr)
let upper = inx * step(yt, p.y) * step(p.y, mid - px)
let lower = inx * step(mid + px, p.y) * step(p.y, yb)
// Under the hand a half lifts a little; pressed, it sinks with a shade
// under its top edge.
var res = vec4(1.0, 1.0, 1.0, 1.0) * 0.035 * (upper * max(0.0, self.hot) + lower * max(0.0, -self.hot)) * live
let su = 0.45 * (1.0 - smoothstep(0.0, 3.0, p.y - yt)) * upper * max(0.0, self.down)
let sd = 0.45 * (1.0 - smoothstep(0.0, 3.0, p.y - mid - px)) * lower * max(0.0, -self.down)
res = Finish.over(res, vec4(0.0, 0.0, 0.0, 1.0) * (su + sd) * live)
let vin = step(yt + 2.0, p.y) * step(p.y, yb - 2.0)
let dark = step(x0, p.x) * step(p.x, x0 + px) * vin
let light = step(x0 + px, p.x) * step(p.x, x0 + px * 2.0) * vin
let hin = step(x0 + px * 2.0 + 2.0, p.x) * step(p.x, xr - 2.0)
let hdark = step(mid - px, p.y) * step(p.y, mid) * hin
let hlight = step(mid, p.y) * step(p.y, mid + px) * hin
res = Finish.over(res, vec4(0.110, 0.016, 0.020, 1.0) * (dark + hdark))
res = Finish.over(res, vec4(0.520, 0.360, 0.380, 1.0) * (light + hlight) * 0.8)
let ink = mix(vec3(0.880, 0.780, 0.790), vec3(0.55, 0.38, 0.40), self.disabled)
let cx = (x0 + px * 2.0 + xr) * 0.5
let cu = (yt + mid) * 0.5
let cd = (mid + yb) * 0.5
let du = min(lq_seg(p, vec2(cx - 2.5, cu + 1.5), vec2(cx, cu - 1.5)), lq_seg(p, vec2(cx, cu - 1.5), vec2(cx + 2.5, cu + 1.5)))
let dd = min(lq_seg(p, vec2(cx - 2.5, cd - 1.5), vec2(cx, cd + 1.5)), lq_seg(p, vec2(cx, cd + 1.5), vec2(cx + 2.5, cd - 1.5)))
let k = min(du, dd) - px * 0.5
return Finish.over(res, vec4(ink, 1.0) * Finish.cover(k, px))
}
// The drop-down: a button cap with an 18 point chevron zone at its right,
// behind an engraved divider.
mod.widgets.DropDownFlat.padding = mod.turtle.Inset{left: 8.0 right: 26.0 top: 3.0 bottom: 3.0}
mod.widgets.DropDownFlat.clip_x = false
mod.widgets.DropDownFlat.clip_y = false
mod.widgets.DropDownFlat.draw_bg.vertex = fn() {
let m = 7.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
// The engraved divider and the chevron, laid on a cap or a well.
let lq_chevron_zone = fn(dst: vec4, p: vec2, lo: vec2, hi: vec2, px: float, disabled: float) -> vec4 {
let dx = hi.x - 18.0
let inside = step(lo.y + 4.0, p.y) * step(p.y, hi.y - 4.0)
let dark = step(dx - px, p.x) * step(p.x, dx) * inside
let light = step(dx, p.x) * step(p.x, dx + px) * inside
var res = Finish.over(dst, vec4(0.110, 0.040, 0.046, 1.0) * dark * 0.85)
res = Finish.over(res, vec4(0.96, 0.95, 0.97, 1.0) * light * 0.8)
// The chevron, engraved in the steel: 7 x 4 points, a 1.5 point dark
// stroke with a light line under it.
let cc = vec2(dx + 9.0, (lo.y + hi.y) * 0.5)
let sd = Sdf2d.viewport(p - vec2(0.0, px))
sd.move_to(cc.x - 3.5, cc.y - 2.0)
sd.line_to(cc.x, cc.y + 2.0)
sd.line_to(cc.x + 3.5, cc.y - 2.0)
sd.stroke(vec4(0.96, 0.95, 0.97, 0.55), 0.75)
res = Finish.over(res, sd.result)
let sdf = Sdf2d.viewport(p)
sdf.move_to(cc.x - 3.5, cc.y - 2.0)
sdf.line_to(cc.x, cc.y + 2.0)
sdf.line_to(cc.x + 3.5, cc.y - 2.0)
sdf.stroke(vec4(mix(vec3(0.118, 0.086, 0.094), vec3(0.20, 0.16, 0.17), disabled), 1.0), 0.75)
return Finish.over(res, sdf.result)
}
mod.widgets.DropDownFlat.draw_bg.pixel = fn() {
let m = 7.0
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(self.rect_size.x, o.x, px), lq_snap(self.rect_size.y, o.y, px))
var res = lq_steel(p, lo, hi, 3.0, px, o, self.hover, max(self.down, self.active), 0.0, self.disabled, self.focus, 1.0, vec3(0.0, 0.0, 0.0))
return lq_chevron_zone(res, p, lo, hi, px, self.disabled)
}
mod.widgets.DropDownFlat.draw_text.color = #1e1618
mod.widgets.DropDownFlat.draw_text.color_hover = #120c0d
mod.widgets.DropDownFlat.draw_text.color_focus = #1e1618
mod.widgets.DropDownFlat.draw_text.color_down = #120c0d
mod.widgets.DropDownFlat.draw_text.color_disabled = #2e2426
// The combo box: the drop-down's steel cap, its words typed on it, its list
// opened from the same chevron zone.
mod.widgets.ComboBoxFlat.padding = mod.turtle.Inset{left: 8.0 right: 26.0 top: 3.0 bottom: 3.0}
mod.widgets.ComboBoxFlat.input.min_height = 0.0
mod.widgets.ComboBoxFlat.draw_bg.vertex = fn() {
let m = 7.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.ComboBoxFlat.draw_bg.pixel = fn() {
let m = 7.0
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(self.rect_size.x, o.x, px), lq_snap(self.rect_size.y, o.y, px))
var res = lq_steel(p, lo, hi, 3.0, px, o, self.hover, self.down, 0.0, self.disabled, self.focus, 1.0, vec3(0.0, 0.0, 0.0))
return lq_chevron_zone(res, p, lo, hi, px, self.disabled)
}
mod.widgets.ComboBoxFlat.input.draw_text.color = #1e1618
mod.widgets.ComboBoxFlat.input.draw_text.color_hover = #120c0d
mod.widgets.ComboBoxFlat.input.draw_text.color_focus = #120c0d
mod.widgets.ComboBoxFlat.input.draw_text.color_down = #120c0d
mod.widgets.ComboBoxFlat.input.draw_text.color_disabled = #2e2426
mod.widgets.ComboBoxFlat.input.draw_text.color_empty = #4a3e40
mod.widgets.ComboBoxFlat.input.draw_cursor.color = #120c0d
// Open lists: a flat dark face, a 1 dp top line, rows that lift 0.03 under
// the hand, and the chosen row filled with the accent -- the one fill in
// this family.
let lq_menu = fn(p: vec2, size: vec2, px: float) -> vec4 {
let lo = vec2(0.0, 0.0)
let hi = size
let c = (lo + hi) * 0.5
let d = Material.sd_box(p, c, (hi - lo) * 0.5, 3.0)
var col = mix(vec3(0.200, 0.066, 0.076), vec3(0.150, 0.046, 0.054), p.y / max(size.y, 1.0))
col = mix(col, vec3(0.50, 0.34, 0.36), Finish.band(d, px, px * 2.0, px) * step(p.y, c.y) * step(3.0, p.y))
col = mix(col, vec3(0.060, 0.010, 0.014), Finish.band(d, 0.0, px, px))
let a = Finish.cover(d, px)
return vec4(col * a, a)
}
let lq_row = fn(p: vec2, size: vec2, px: float, hover: float, active: float, disabled: float) -> vec4 {
let top = 1.0 - step(px, p.y)
var col = vec4(1.0, 1.0, 1.0, 1.0) * 0.035 * hover * (1.0 - disabled)
let sel = vec3(0.227, 0.498, 0.753)
let fill = vec4(mix(sel, vec3(0.45, 0.66, 0.86), top), 1.0)
return mix(col, fill, active)
}
// The open list is clipped to its own box by the popup that holds it, so
// its shadow cannot be drawn from here.
mod.widgets.PopupMenuFlat.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
return lq_menu(self.pos * self.rect_size, self.rect_size, px)
}
mod.widgets.PopupMenuItem.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
return lq_row(self.pos * self.rect_size, self.rect_size, px, self.hover, self.active, self.disabled)
}
mod.widgets.PopupMenuItem.draw_text.color_active = #ffffff
mod.widgets.ComboBoxFlat.draw_popup_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
return lq_menu(self.pos * self.rect_size, self.rect_size, px)
}
mod.widgets.ComboBoxFlat.draw_item.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
return lq_row(self.pos * self.rect_size, self.rect_size, px, self.hover, self.active, 0.0)
}
mod.widgets.ComboBoxFlat.draw_item_text.color_active = #ffffff
// ---------------------------------------------------------------- tabs
// The segmented control: the whole strip one raised cap; the chosen answer
// sinks, flat and tinted toward the light, its legend lit and a lamp under
// it.
mod.widgets.SegmentedControlFlat.draw_bg.vertex = fn() {
let m = 7.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.SegmentedControlFlat.draw_bg.pixel = fn() {
let m = 7.0
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(self.rect_size.x, o.x, px), lq_snap(self.rect_size.y, o.y, px))
return lq_steel(p, lo, hi, 3.0, px, o, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, vec3(0.0, 0.0, 0.0))
}
// The chosen answer is a push button pressed in: Day, Week and Month are
// three steel caps and the chosen one is sunk deep between the others, a
// well of darker, cooler brushed steel. The strip lays the answers 2 points
// in from its edge, so the well is the answer's box grown by those 2 points
// with the strip's own 3 point corners: at either end of the strip it IS the
// strip's end, outline and corners; between two answers it is a rounded key
// pressed in between raised neighbours. Black under the top edge, shaded down
// both walls, a cool lip caught along its foot, and a lamp under the word.
mod.widgets.SegmentedControlFlat.draw_pill.vertex = fn() {
let m = 9.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.SegmentedControlFlat.draw_pill.pixel = fn() {
let m = 9.0
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let lo = vec2(lq_snap(-2.0, o.x, px), lq_snap(-2.0, o.y, px))
let hi = vec2(lq_snap(self.rect_size.x + 2.0, o.x, px), lq_snap(self.rect_size.y + 2.0, o.y, px))
let cc = (lo + hi) * 0.5
let d = Material.sd_box(p, cc, (hi - lo) * 0.5, 3.0)
let hh = hi.y - lo.y
let t = clamp((p.y - lo.y) / max(hh, 1.0), 0.0, 1.0)
// The floor: the strip's steel pushed in, deeper and cooler, its
// brushing still running through it.
let bq = p + o
let s1 = Finish.brushed(bq, vec2(1.0, 0.0), px * 1.2) - 0.5
let s2 = Finish.brushed(bq + vec2(31.0, 7.0), vec2(1.0, 0.0), px * 4.5) - 0.5
let fl = mix(0.345, 0.275, t) + s1 * 0.042 + s2 * 0.025
var col = vec3(fl, fl, fl) * vec3(0.90, 0.955, 1.04)
// The inner shadow: black right under the top edge, gone in about
// seven rows; along each wall a shade gone in five.
let top = exp(-max(p.y - lo.y - px, 0.0) / (3.5 * px))
let wl = 0.80 * exp(-max(p.x - lo.x - px, 0.0) / (2.5 * px))
let wr = 0.80 * exp(-max(hi.x - px - p.x, 0.0) / (2.5 * px))
let dk = 1.0 - (1.0 - top) * (1.0 - wl) * (1.0 - wr)
col = mix(col, vec3(0.0, 0.0, 0.0), dk)
// The cool lip the light catches along the well's foot, above the
// outline, then the outline itself.
let lip = Finish.band(d, px, px * 2.0, px) * step(hi.y - px * 2.5, p.y)
col = mix(col, vec3(0.50, 0.60, 0.70), lip * 0.8)
col = mix(col, vec3(0.070, 0.030, 0.036), Finish.band(d, 0.0, px, px))
let inside = Finish.cover(d, px)
var res = vec4(col, 1.0) * inside
// The lamp, centred under the word: the widget centres the word by its
// advance, which sets the ink about a point left of the middle, so the
// lamp stands a point left of it too. The core's top row 8.4 points
// under the segment's middle. Round it a broad soft pool on the floor.
let c = vec2(self.rect_size.x * 0.5 - 1.0, self.rect_size.y * 0.5)
let y0 = lq_snap(c.y + 8.4167, o.y, px)
let q = vec2((p.x - c.x) / max((hi.x - lo.x) * 0.3, 1.0), (p.y - (y0 + px)) / 3.2)
let pool = exp(-dot(q, q)) * 0.16 * inside
res = Finish.over(res, vec4(0.405, 0.593, 0.752, 1.0) * pool)
let xs = vec2(lq_snap(c.x - 5.3333, o.x, px), lq_snap(c.x + 5.3333, o.x, px))
let lamp = lq_lamp(res, p, xs, y0, px, 1.0, 1.0, vec3(0.910, 0.918, 0.922), vec3(0.405, 0.593, 0.752))
res = mix(res, lamp, Finish.cover(d + px, px))
return res
}
// Cap labels at the family's 8 points, the chosen one included.
mod.widgets.SegmentedControlFlat.draw_text.text_style.font_size = 8.0
mod.widgets.SegmentedControlFlat.draw_text_selected.text_style.font_size = 8.0
mod.widgets.SegmentedControlFlat.draw_text.color = #1e1618
mod.widgets.SegmentedControlFlat.draw_text_selected.color = #d4eaff
mod.widgets.SegmentedControlVertical.draw_pill.radius = -1.0
// Tabs: joined segments of one strip. Each quiet tab is a raised segment
// whose joint with the next is engraved, dark then light; the tab in use
// sinks like the chosen segment, its word lit and the lamp under it.
mod.widgets.Tabs.height = 26.0
mod.widgets.Tabs.clip_x = false
mod.widgets.Tabs.clip_y = false
let lq_tab = fn(pos: vec2, size: vec2, o: vec2, dpi: float, hover: float, active: float) -> vec4 {
let m = 7.0
let px = lq_px(dpi)
let p = pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(size.x, o.x, px), lq_snap(size.y, o.y, px))
let inx = step(lo.x, p.x) * step(p.x, hi.x)
var res = lq_steel(p, lo - vec2(4.0, 0.0), hi + vec2(4.0, 0.0), 0.0, px, o, hover, active, 1.0, 0.0, 0.0, 1.0, vec3(0.0, 0.0, 0.0)) * inx
// The tab in use is a well cut between its neighbours: a shade down
// both walls of it.
let iny = step(lo.y, p.y) * step(p.y, hi.y)
let wall = exp(-max(min(p.x - lo.x, hi.x - p.x) - px, 0.0) / (2.5 * px))
res = Finish.over(res, vec4(0.0, 0.0, 0.0, 1.0) * 0.55 * wall * inx * iny * step(lo.y + px, p.y) * step(p.y, hi.y - px) * active)
// The joint: the black outline on the left, just outside the box, and a
// light line inside it; the dark line inside the right edge.
let left_out = step(lo.x - px, p.x) * (1.0 - step(lo.x, p.x))
let left_in = step(lo.x, p.x) * (1.0 - step(lo.x + px, p.x))
let right_in = step(hi.x - px, p.x) * (1.0 - step(hi.x, p.x))
res = Finish.over(res, vec4(0.070, 0.030, 0.036, 1.0) * (left_out + right_in) * iny)
res = Finish.over(res, vec4(0.94, 0.94, 0.96, 1.0) * left_in * iny * step(lo.y + px, p.y) * step(p.y, hi.y - px) * (1.0 - active))
// Under the sunk tab only, a 1 dp lip #474c56 on the ground.
let lip = step(hi.y, p.y) * (1.0 - step(hi.y + px, p.y)) * inx
res = Finish.over(res, vec4(0.70, 0.52, 0.54, 1.0) * lip * active)
// The tab in use carries the chosen segment's lamp under its word. The
// strip sets its words flush left 10 points in and the face cannot know
// how long a word is, so the lamp is centred on the first 27 points of
// it -- under a one-word name such as Mixer, Effects or Notes it stands
// in the middle; under a longer word it stays under its first letters.
// The core's top row 7.75 points under the tab's middle, as on a
// segment.
let lx = lo.x + 23.5
let xs = vec2(lq_snap(lx - 5.3333, o.x, px), lq_snap(lx + 5.3333, o.x, px))
return lq_lamp(res, p, xs, lq_snap((lo.y + hi.y) * 0.5 + 7.75, o.y, px), px, active, 1.0, vec3(0.910, 0.918, 0.922), vec3(0.405, 0.593, 0.752))
}
mod.widgets.Tabs.draw_tab.vertex = fn() {
let m = 7.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.Tabs.draw_tab_hover.vertex = fn() {
let m = 7.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.Tabs.draw_tab_active.vertex = fn() {
let m = 7.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.Tabs.draw_bg.pixel = fn() {
return vec4(0.0, 0.0, 0.0, 0.0)
}
mod.widgets.Tabs.draw_tab.pixel = fn() {
return lq_tab(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, 0.0, 0.0)
}
mod.widgets.Tabs.draw_tab_hover.pixel = fn() {
return lq_tab(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, 1.0, 0.0)
}
mod.widgets.Tabs.draw_tab_active.pixel = fn() {
return lq_tab(self.pos, self.rect_size, self.rect_pos, self.draw_pass.dpi_factor, 0.0, 1.0)
}
mod.widgets.Tabs.draw_sep.pixel = fn() {
return vec4(0.0, 0.0, 0.0, 0.0)
}
mod.widgets.Tabs.draw_mark.color = #e0c4c4
// The close cross stands on steel of either depth (the chosen well, a
// raised tab under the hand): a pale blue-white stroke in a dark halo reads
// on both. The add plus beside the strip is drawn as before.
mod.widgets.Tabs.draw_mark.pixel = fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
let cx2 = self.rect_size.x * 0.5
let cy2 = self.rect_size.y * 0.5
let r = min(self.rect_size.x, self.rect_size.y) * 0.26
let a = mix(0.7853981634, 0.0, self.plus)
let ax = cos(a) * r
let ay = sin(a) * r
let x = 1.0 - self.plus
sdf.move_to(cx2 - ax, cy2 - ay)
sdf.line_to(cx2 + ax, cy2 + ay)
sdf.stroke(vec4(0.070, 0.030, 0.036, 0.75 * x), 2.6)
sdf.move_to(cx2 + ay, cy2 - ax)
sdf.line_to(cx2 - ay, cy2 + ax)
sdf.stroke(vec4(0.070, 0.030, 0.036, 0.75 * x), 2.6)
let ink = mix(self.color, vec4(0.722, 0.816, 0.910, 1.0), x)
sdf.move_to(cx2 - ax, cy2 - ay)
sdf.line_to(cx2 + ax, cy2 + ay)
sdf.stroke(ink, 1.25)
sdf.move_to(cx2 + ay, cy2 - ax)
sdf.line_to(cx2 - ay, cy2 + ax)
sdf.stroke(ink, 1.25)
return sdf.result
}
mod.widgets.Tabs.draw_text.color = #1e1618
// The tab in use answers like the chosen segment: its ink #f6e8e6 in
// semibold, in the sunk blue well; quiet tabs grey regular.
mod.widgets.Tabs.draw_text_active.color = #d4eaff
mod.widgets.Tabs.draw_text.text_style.font_size = 8.0
mod.widgets.Tabs.draw_text_active.text_style = mod.theme.font_bold{font_size: 8.0}
// ---------------------------------------------------------------- feedback
// The progress bar: an 8 point well; the value is the lit bar of the
// sliders, a pale core row over a body row in the middle of the floor, its
// halo on the floor round it.
mod.widgets.ProgressBarFlat.height = 8.0
mod.widgets.ProgressBarFlat.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * self.rect_size
let th = min(self.thickness, self.rect_size.y)
let y0 = lq_snap((self.rect_size.y - th) * 0.5, o.y, px)
let x0 = lq_snap(self.rect_size.x * self.start, o.x, px)
let x1 = lq_snap(self.rect_size.x * self.end, o.x, px)
let lo = vec2(x0, y0)
let hi = vec2(x1, y0 + lq_snap(th, 0.0, px) - px)
var res = vec4(0.0, 0.0, 0.0, 0.0)
if self.track > 0.5 {
res = lq_well(p, lo, hi, 2.0, px, vec3(0.125, 0.043, 0.051), 0.0, 0.0, 1.0)
}
let fill = self.fill_color()
let ins = 2.0
let fl = vec2(lo.x + ins, lo.y + ins)
let span = hi.x - lo.x - ins * 2.0
var f0 = fl.x
var f1 = fl.x + span * clamp((self.value - self.start) / max(self.end - self.start, 0.001), 0.0, 1.0)
if self.value < 0.0 {
// Still: the sweep would need the pass clock, and a window at rest
// reads no clock, so the unknown is a segment standing in the
// middle with its ends fading.
f0 = fl.x + span * 0.3
f1 = fl.x + span * 0.7
}
// The fill is lit like the lamp: a pale core row over a row of the
// role colour in the middle of the well's floor, the halo on the floor
// round it and past its ends.
var fade = fill.a * step(0.001, f1 - f0)
if self.value < 0.0 {
fade = fade * smoothstep(f0, f0 + span * 0.12, p.x) * smoothstep(f1, f1 - span * 0.12, p.x)
}
let lb = lq_lit_bar(vec4(0.0, 0.0, 0.0, 0.0), p, vec2(f0, lo.y + px), vec2(f1, hi.y - px), vec2(lo.x + px, hi.x - px), px, fill.rgb, 0.0)
res = Finish.over(res, lb * fade)
return res * self.opacity
}
mod.widgets.ProgressBarFlat.draw_bg.color_primary = #8cc6ff
// The level meter: cells of 3 dp and a 1 dp black gap on black glass in a
// slim well; green, then yellow toward the top, red at the end; unlit cells
// stand at a fifth of the lit. The clip lamp is an LED. 8 points thick.
mod.widgets.LevelMeter.height = 8.0
mod.widgets.LevelMeterColumn.width = 8.0
mod.widgets.LevelMeter.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let sz = self.rect_size
let p = self.pos * sz
let v = self.vertical
let along = mix(sz.x, sz.y, v)
let across = mix(sz.y, sz.x, v)
let a = mix(p.x, sz.y - p.y, v)
let b = mix(p.y, p.x, v)
let lamp_room = self.lamp * (across + 3.0)
let bar = max(4.0, along - lamp_room)
// The window: black glass in a 1 dp outline with its lip.
let wlo = mix(vec2(0.0, 0.0), vec2(0.0, sz.y - bar), v)
let whi = mix(vec2(bar, sz.y), vec2(sz.x, sz.y), v)
let lo = vec2(lq_snap(wlo.x, o.x, px), lq_snap(wlo.y, o.y, px))
let hi = vec2(lq_snap(whi.x, o.x, px), lq_snap(whi.y, o.y, px) - px)
var res = lq_well(p, lo, hi, 1.0, px, vec3(0.040, 0.010, 0.014), 0.0, 0.0, 1.0)
let pad = 2.0 * px
let span = max(1.0, bar - pad * 2.0)
let pitch = px * 4.0
let cell = step(fract((a - pad) / pitch), 0.75)
let share = clamp((a - pad) / span, 0.0, 1.0)
let inb = step(pad, a) * step(a, bar - pad) * step(pad, b) * step(b, across - pad) * step(p.y, hi.y - px)
var lit_col = vec3(0.690, 0.820, 0.260)
lit_col = mix(lit_col, vec3(0.902, 0.788, 0.251), step(self.hot_at, share))
lit_col = mix(lit_col, vec3(0.941, 0.318, 0.227), step(self.hot_to, share))
// The first device pixel of each cell a touch lighter and paler.
let edge = step(fract((a - pad) / pitch), 0.25)
lit_col = mix(lit_col, mix(lit_col, vec3(0.85, 0.87, 0.8), 0.35), edge)
let on = step(share, self.level) * step(0.001, self.level)
let unlit = vec3(0.180, 0.080, 0.086)
let col = mix(unlit, lit_col, on)
res = Finish.over(res, vec4(col, 1.0) * cell * inb * self.opacity)
// The hold: one cell at full strength.
let hold_at = pad + span * self.hold
let hc = step(abs(a - hold_at), pitch * 0.5) * step(0.002, self.hold)
res = Finish.over(res, vec4(mix(vec3(0.690, 0.820, 0.260), vec3(0.941, 0.318, 0.227), step(self.hot_to, self.hold)), 1.0) * hc * cell * inb)
// The lamp.
if self.lamp > 0.5 {
let lc = mix(vec2(bar + 3.0 + across * 0.5, sz.y * 0.5), vec2(sz.x * 0.5, across * 0.5), v)
// A small red lamp, 3.5 points: a white-hot core, a red rim and a
// soft red halo when lit; a dark lens when out.
let rl = clamp(across * 0.5 - 1.5, 1.0, 1.75)
res = lq_led(res, p, lc, vec2(rl, rl), step(0.5, self.over), 0.45, px, vec3(1.0, 0.957, 0.941), vec3(0.941, 0.318, 0.227))
}
return res * self.opacity
}
// ---------------------------------------------------------------- surfaces
// The scroll bar: an 8 point well, the thumb a 6 point cap inset 1 point,
// no gloss, no glow.
mod.widgets.ScrollBar.bar_size = 8.0
mod.widgets.ScrollBar.bar_side_margin = 1.0
mod.widgets.ScrollBar.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let sz = self.rect_size
let p = self.pos * sz
let v = self.is_vertical
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(sz.x, o.x, px), lq_snap(sz.y, o.y, px))
var res = lq_well(p, lo, hi, 2.0, px, vec3(0.125, 0.043, 0.051), 0.0, 0.0, 0.0)
let t0 = mix(sz.x * self.norm_scroll, sz.y * self.norm_scroll, v)
let t1 = t0 + mix(sz.x * self.norm_handle, sz.y * self.norm_handle, v)
let tlo = mix(vec2(t0, 1.0), vec2(1.0, t0), v)
let thi = mix(vec2(t1, sz.y - 1.0), vec2(sz.x - 1.0, t1), v)
let slo = vec2(lq_snap(tlo.x, o.x, px), lq_snap(tlo.y, o.y, px))
let shi = vec2(lq_snap(thi.x, o.x, px), lq_snap(thi.y, o.y, px))
res = Finish.over(res, lq_smoke(p, slo, shi, 1.5, px, max(self.hover, self.drag), 0.0, 0.0, 0.0, 0.0))
return res * self.opacity
}
// The housing's grain: monochrome, one value per device pixel, sd 0.008.
// The library's hash nearly repeats every 97 device pixels (97 x 0.1031 is
// almost whole), which showed as a lattice over a wide ground; each pixel
// is first sent to a far cell picked by the hash of its own, and the grain
// is read there, so no shift lines the pattern up again.
let lq_grain = fn(p: vec2, o: vec2, px: float) -> float {
let i = floor((p + o) / px)
let j = i + vec2(Finish.hash(i) * 419.0, Finish.hash(i + vec2(5.31, 9.17)) * 263.0)
return (Finish.hash(j) - Finish.hash(j + vec2(17.13, 3.71))) * 0.014
}
// A raised lacquer slab: a soft drop that stays within the gap to its
// neighbours, the dark outline with a chrome bezel inside it, a deep red
// face falling toward the foot, a gloss over its upper part whose lower
// edge is a soft arc rising toward the sides, a bright streak along the top,
// and the lowest rows lifted by reflected light.
let lq_panel = fn(p: vec2, size: vec2, o: vec2, px: float, r: float, hover: float) -> vec4 {
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(size.x, o.x, px), lq_snap(size.y, o.y, px))
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
let d = Material.sd_box(p, c, h, r)
let sh1 = Finish.drop(p, c, h - vec2(1.0, 0.5), r, vec2(0.0, 2.0), 1.6) * 0.6
let sh2 = Finish.drop(p, c, h - vec2(3.5, 2.0), r, vec2(0.0, 4.5), 3.4) * 0.45
let sh = 1.0 - (1.0 - sh1) * (1.0 - sh2)
let contact = Finish.drop(p, c, h, r, vec2(0.0, px), px * 0.6) * 0.8
var res = vec4(0.03, 0.0, 0.0, 1.0) * (1.0 - (1.0 - sh) * (1.0 - contact))
let hh = max(hi.y - lo.y, 1.0)
let y = p.y - lo.y
let t = clamp(y / hh, 0.0, 1.0)
var col = mix(vec3(0.520, 0.082, 0.098), vec3(0.230, 0.034, 0.040), smoothstep(0.0, 1.0, t))
// The gloss band, its depth set in points so a tall slab and a short
// card carry the same reflection.
let sx = clamp((p.x - c.x) / max(h.x, 1.0), -1.0, 1.0)
let hz = min(24.0, hh * 0.42) - 7.0 * sx * sx
let band = 1.0 - smoothstep(hz - 4.0, hz + 1.0, y)
let gcol = mix(vec3(0.660, 0.235, 0.255), vec3(0.590, 0.140, 0.160), clamp(y / max(hz, 1.0), 0.0, 1.0))
col = mix(col, gcol, band * 0.9)
// The streak of a light held in the top of the gloss.
let wy = (y - 3.0) / 2.2
col = mix(col, vec3(0.96, 0.72, 0.74), 0.38 * exp(-wy * wy) * (1.0 - 0.55 * sx * sx))
// Reflected light low on the face.
col = mix(col, vec3(0.50, 0.22, 0.24), 0.30 * smoothstep(hh - 10.0, hh, y))
col = col + vec3(0.03, 0.012, 0.014) * hover
col = col + vec3(1.0, 1.0, 1.0) * lq_grain(p, o, px)
// The chrome bezel, bright at the top, dimmer down the sides.
let bez = mix(vec3(0.86, 0.66, 0.68), vec3(0.50, 0.36, 0.38), smoothstep(0.0, 0.8, t))
col = mix(col, bez, Finish.band(d, px, px * 2.5, px) * 0.9)
col = mix(col, vec3(0.060, 0.008, 0.012), Finish.band(d, 0.0, px, px))
let a = Finish.cover(d, px)
res = Finish.over(res, vec4(col * a, a))
return res
}
// A tray: a well cut into the lacquer. A darker red floor, the ground's rim
// shading its top, the chrome rim, and the gloss caught low on the far wall.
let lq_tray = fn(p: vec2, size: vec2, o: vec2, px: float, r: float) -> vec4 {
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(size.x, o.x, px), lq_snap(size.y, o.y, px) - px)
let c = (lo + hi) * 0.5
let d = Material.sd_box(p, c, (hi - lo) * 0.5, r)
var res = lq_well(p, lo, hi, r, px, vec3(0.270, 0.040, 0.050), 0.0, 0.0, 1.0)
let a = Finish.cover(d + px * 2.0, px)
let y = p.y - lo.y
let hh = max(hi.y - lo.y, 1.0)
let shade = 0.55 * (1.0 - smoothstep(0.0, 8.0, y - px * 2.0)) * a
let low = 0.16 * smoothstep(hh - 16.0, hh - 3.0, y) * (1.0 - smoothstep(hh - 3.0, hh - 1.0, y)) * a
let g = lq_grain(p, o, px) * a
var col = res.rgb * (1.0 - shade) + vec3(0.62, 0.26, 0.28) * low * res.a + vec3(g, g, g) * res.a
return vec4(col, res.a)
}
mod.widgets.PanelView.clip_x = false
mod.widgets.PanelView.clip_y = false
mod.widgets.PanelView.padding = mod.turtle.Inset{left: 12.0 right: 12.0 top: 12.0 bottom: 12.0}
mod.widgets.PanelView.draw_bg.vertex = fn() {
let m = 12.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.PanelView.draw_bg.pixel = fn() {
let m = 12.0
let px = lq_px(self.draw_pass.dpi_factor)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
if self.sunken > 0.5 {
return lq_tray(p, self.rect_size, self.rect_pos, px, 5.0)
}
return lq_panel(p, self.rect_size, self.rect_pos, px, 5.0, 0.0)
}
// The rounded view keeps the colour its caller gives it, square to the
// family: a true 2 point corner and, when it is opaque, a 1 dp outline.
mod.widgets.RoundedView.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let p = self.pos * self.rect_size
let lo = vec2(self.border_inset.x, self.border_inset.y)
let hi = self.rect_size - vec2(self.border_inset.z, self.border_inset.w)
let c = (lo + hi) * 0.5
let h = max((hi - lo) * 0.5, vec2(0.5, 0.5))
let r = min(2.0 * max(1.0, self.border_radius), min(h.x, h.y))
let d = Material.sd_box(p, c, h, r)
var fill = self.color
if self.color_2.x > -0.5 {
fill = mix(self.color, self.color_2, mix(self.pos.y, self.pos.x, self.gradient_fill_horizontal))
}
var ink = self.border_color
if self.border_size < 0.01 {
ink = vec4(0.110, 0.016, 0.020, fill.a)
}
let a = Finish.cover(d, px)
var col = mix(fill.rgb, ink.rgb, Finish.band(d, 0.0, max(self.border_size, px), px) * ink.a)
return vec4(col * a * fill.a, a * fill.a) + vec4(ink.rgb * ink.a, ink.a) * Finish.band(d, 0.0, max(self.border_size, px), px) * a * (1.0 - fill.a)
}
// The card: a raised panel of its own; a pressable card lifts under the
// hand.
mod.widgets.Card.clip_x = false
mod.widgets.Card.clip_y = false
mod.widgets.Card.padding = mod.turtle.Inset{left: 12.0 right: 12.0 top: 12.0 bottom: 12.0}
mod.widgets.Card.draw_bg.vertex = fn() {
let m = 12.0
return self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.Card.draw_bg.pixel = fn() {
let m = 12.0
let px = lq_px(self.draw_pass.dpi_factor)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
return lq_panel(p, self.rect_size, self.rect_pos, px, 5.0, self.hover)
}
// List rows: 20 points, the text 10 points in; under the hand the row
// lifts 0.03. The chosen row is a shallow well -- the chosen segment's blue
// floor, a dark line and a short shade under its top edge, a light lip on
// its foot -- with a short lamp standing at its left.
mod.widgets.ListItem.height = 20.0
mod.widgets.ListItemOne.height = 20.0
mod.widgets.ListItem.padding = mod.turtle.Inset{left: 10.0 right: 8.0 top: 0.0 bottom: 0.0}
mod.widgets.ListItemOne.padding = mod.turtle.Inset{left: 10.0 right: 8.0 top: 0.0 bottom: 0.0}
mod.widgets.ListItem.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * self.rect_size
let live = 1.0 - self.disabled
var res = vec4(1.0, 1.0, 1.0, 1.0) * (0.03 * self.hover + 0.02 * self.down) * live
let y0 = lq_snap(0.0, o.y, px)
let y1 = lq_snap(self.rect_size.y, o.y, px)
let top = p.y - y0
let t = clamp(top / max(y1 - y0, 1.0), 0.0, 1.0)
var col = mix(vec3(0.150, 0.050, 0.060), vec3(0.110, 0.036, 0.044), t)
col = mix(col, vec3(0.0, 0.0, 0.0), 0.35 * (1.0 - smoothstep(0.0, 4.0, top - px)))
col = mix(col, vec3(0.110, 0.016, 0.020), 1.0 - step(px, top))
col = mix(col, vec3(0.62, 0.46, 0.48), step(y1 - px, p.y))
var sel = vec4(col, 1.0)
let lc = vec2(lq_snap(3.5, o.x, px) - px * 0.5, (y0 + y1) * 0.5)
sel = lq_led(sel, p, lc, vec2(px * 0.5 + 0.01, max((y1 - y0) * 0.22, 2.0)), live, 0.8, px, vec3(0.88, 0.96, 1.0), vec3(0.40, 0.62, 0.84))
res = mix(res, Finish.over(sel, res), self.selected)
return res
}
mod.widgets.ListItem.draw_text.color = #f6e8e6
mod.widgets.ListItem.draw_text_2.color = #d8b8b8
// A chip is a small seated steel cap; its role colour only tints the face.
// Its label is a cap label: 8 points, dark, stamped in whatever ink the
// palette hands it.
mod.widgets.ChipFlat.draw_text.text_style.font_size = 8.0
mod.widgets.ChipFlat.draw_text.pixel = fn() {
let raw = self.sample_text_pixel()
return vec4(vec3(0.118, 0.086, 0.094) * raw.a, raw.a)
}
mod.widgets.ChipFlat.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * self.rect_size
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(self.rect_size.x, o.x, px), lq_snap(self.rect_size.y, o.y, px) - px)
let l = Finish.lum(self.color.rgb)
let tint = (self.color.rgb - vec3(l, l, l)) * 0.35 * self.color.a
var res = lq_steel(p, lo, hi, 2.0, px, o, self.hover, self.down, 0.0, 0.0, 0.0, 0.0, tint)
if self.keyed > 0.0 {
let d = Material.sd_box(p, (lo + hi) * 0.5, (hi - lo) * 0.5, 2.0)
res = Finish.over(res, vec4(0.40, 0.62, 0.84, 1.0) * Finish.band(d, 0.0, px, px) * self.keyed)
}
return res
}
// ---------------------------------------------------------------- ink
// Words sit embossed-dark: a 1 dp shadow of black at half strength directly
// under every glyph of a label. Read from the glyph one device pixel above,
// so it costs one more sample and nothing else.
// On a cap the label also sinks with it: held, the glyphs are read one
// device pixel higher in the atlas, so they stand one pixel lower in the
// well. Small labels come from the grayscale atlas, large ones from the
// multi-channel one; both paths carry the shadow and the nudge.
mod.widgets.ButtonFlat.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
// A template made from another before this sheet ran keeps its own copy
// of the label's draw, so each cap template takes the literal.
mod.widgets.Button.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonPrimary.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonSecondary.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonTertiary.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonDanger.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonOutline.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonFlatter.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonXs.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonSm.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonLg.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
mod.widgets.ButtonXl.draw_text.pixel = fn() {
let nudge = dFdy(self.t.xy) * step(0.5, self.down)
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.sdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.sdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let pm = clamp(self.t.xy - nudge, self.t_min + half_texel, self.t_max - half_texel)
let main = self.msdf(scale, pm, c) * vec4(c.rgb * c.a, c.a)
let ps = clamp(self.t.xy - nudge - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return self.sample_text_pixel()
}
// The same shadow on every label that stands on the ground or a cap; one
// literal per template, as the handbook advises. A lit legend (the tab or
// segment in use) takes a 2 dp halo of the LED's glow instead.
mod.widgets.Label.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ps = clamp(self.t.xy - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let c = self.get_color()
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return main
}
mod.widgets.CheckBoxFlat.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ps = clamp(self.t.xy - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let c = self.get_color()
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return main
}
mod.widgets.RadioButtonFlat.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ps = clamp(self.t.xy - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let c = self.get_color()
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return main
}
mod.widgets.DropDownFlat.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ps = clamp(self.t.xy - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let c = self.get_color()
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
// Stamped into steel: a light line under the glyph, not a shadow.
let e = s * 0.42 * c.a
return main + vec4(0.96 * e, 0.95 * e, 0.97 * e, e) * (1.0 - main.a)
}
return main
}
mod.widgets.SegmentedControlFlat.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ps = clamp(self.t.xy - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let c = self.get_color()
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
// Stamped into steel: a light line under the glyph, not a shadow.
let e = s * 0.42 * c.a
return main + vec4(0.96 * e, 0.95 * e, 0.97 * e, e) * (1.0 - main.a)
}
return main
}
mod.widgets.SegmentedControlFlat.draw_text_selected.pixel = fn() {
let raw = self.sample_text_pixel()
// Lit blue-white, whatever ink the palette hands the chosen answer.
let main = vec4(vec3(0.831, 0.918, 1.0) * raw.a, raw.a)
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ex = dFdx(self.t.xy) * 1.5
let ey = dFdy(self.t.xy) * 1.5
let k = vec4(0.0, 0.0, 0.0, 1.0)
var g = self.msdf(scale, clamp(self.t.xy + ex, self.t_min + half_texel, self.t_max - half_texel), k)
g = g + self.msdf(scale, clamp(self.t.xy - ex, self.t_min + half_texel, self.t_max - half_texel), k)
g = g + self.msdf(scale, clamp(self.t.xy + ey, self.t_min + half_texel, self.t_max - half_texel), k)
g = g + self.msdf(scale, clamp(self.t.xy - ey, self.t_min + half_texel, self.t_max - half_texel), k)
let c = self.get_color()
let a = clamp(g * 0.25, 0.0, 1.0) * 0.4 * c.a
return main + vec4(0.22 * a, 0.38 * a, 0.56 * a, a) * (1.0 - main.a)
}
return main
}
mod.widgets.Tabs.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ps = clamp(self.t.xy - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let c = self.get_color()
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
// Stamped into steel: a light line under the glyph, not a shadow.
let e = s * 0.42 * c.a
return main + vec4(0.96 * e, 0.95 * e, 0.97 * e, e) * (1.0 - main.a)
}
return main
}
mod.widgets.Tabs.draw_text_active.pixel = fn() {
let main = self.sample_text_pixel()
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ex = dFdx(self.t.xy) * 1.5
let ey = dFdy(self.t.xy) * 1.5
let k = vec4(0.0, 0.0, 0.0, 1.0)
var g = self.msdf(scale, clamp(self.t.xy + ex, self.t_min + half_texel, self.t_max - half_texel), k)
g = g + self.msdf(scale, clamp(self.t.xy - ex, self.t_min + half_texel, self.t_max - half_texel), k)
g = g + self.msdf(scale, clamp(self.t.xy + ey, self.t_min + half_texel, self.t_max - half_texel), k)
g = g + self.msdf(scale, clamp(self.t.xy - ey, self.t_min + half_texel, self.t_max - half_texel), k)
let c = self.get_color()
let a = clamp(g * 0.25, 0.0, 1.0) * 0.4 * c.a
return main + vec4(0.22 * a, 0.38 * a, 0.56 * a, a) * (1.0 - main.a)
}
return main
}
mod.widgets.SliderMinimal.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
let tex_size = self.msdf_texture.size()
let scale = (dxt + dyt) * tex_size.x * 0.5
let half_texel = vec2(0.5 / tex_size.x, 0.5 / tex_size.y)
let ps = clamp(self.t.xy - dFdy(self.t.xy), self.t_min + half_texel, self.t_max - half_texel)
let c = self.get_color()
let s = self.msdf(scale, ps, vec4(0.0, 0.0, 0.0, 1.0))
return main + vec4(0.0, 0.0, 0.0, s * 0.5 * c.a) * (1.0 - main.a)
}
return main
}
// ---------------------------------------------------------------- field row
// A ROW OF FIELDS IS ONE HEIGHT. Left to themselves the library's fields each
// come out at a size of their own -- in the widget catalogue's toolbar the
// search box, the number beside it and the drop down after it stood at three
// different heights -- and a sheet that reshapes the text box alone widens
// that gap instead of closing it. So the box metrics are stated ONCE here and
// handed to each field below, and a field added to the library later joins
// the row by being named here rather than by having these numbers copied
// into it.
// Fields and buttons share the one 24 point row.
let field_height = 24.0
let field_pad_v = 3.0
let field_radius = 1.0
// The outline is one device pixel; the faces below draw it themselves.
let field_border = 0.34
// The room a Fit field takes. A minimum height rather than a height,
// because a line of text is shorter than a row, and the padding with it:
// where a field is Fit, the padding IS its height, and two fields padded
// differently part company again the moment the type grows.
let field_room = fn(w) {
w.min_height = field_height
w.padding.top = field_pad_v
w.padding.bottom = field_pad_v
}
// The box a field draws. Given the draw object and not the widget, because
// not every field draws its own box, and because writing a corner onto a
// draw struct that has no corner would add a slot its shader never reads.
let field_face = fn(d) {
d.border_radius = field_radius
d.border_size = field_border
}
// A field that states a frame and lays its own parts out inside it: a number
// with a stepper, a date, a time. The height is all these take. A padding
// here would NOT hold the line off the box -- each of them measures its
// parts against the turtle's whole rect and draws them from the content
// origin, so the padding displaces the line instead of insetting it, and the
// number came out sitting on the bottom edge with its arrows split around it.
let field_frame = fn(w) {
w.min_height = field_height
}
field_room(mod.widgets.TextInput)
field_face(mod.widgets.TextInput.draw_bg)
field_room(mod.widgets.ComboBox)
field_face(mod.widgets.ComboBox.draw_bg)
field_room(mod.widgets.DropDown)
field_face(mod.widgets.DropDown.draw_bg)
field_room(mod.widgets.DropDown2)
field_face(mod.widgets.DropDown2.draw_bg)
field_room(mod.widgets.FieldWell)
field_face(mod.widgets.FieldWell.draw_bg)
field_room(mod.widgets.TagField)
field_face(mod.widgets.TagField.draw_bg)
field_frame(mod.widgets.NumberField)
// The number's box strokes its own hairline and declares no border size.
mod.widgets.NumberField.draw_bg.border_radius = field_radius
// The drag-number's corner is written into its own shader, so it takes the
// room and keeps the box it draws.
field_frame(mod.widgets.ValueInput)
field_frame(mod.widgets.DateField)
field_frame(mod.widgets.TimeField)
// A picker is a date field in an anchor: the field inside fills the frame.
field_frame(mod.widgets.DatePicker)
field_frame(mod.widgets.DateRangePicker)
// The chrome-less input a well holds must impose no height of its own. It is
// a TextInput, so it inherits the minimum above, and a minimum as tall as the
// whole field, applied inside a well already that tall, pushed the line out
// through the bottom of the box.
mod.widgets.WellInput.min_height = 0.0
// ---------------------------------------------------------------- rules
// A rule is engraved: one device pixel of the outline's black over one of
// light, across the middle of its room, no shadow.
mod.widgets.Hr.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let p = self.pos * self.rect_size
let y0 = lq_snap(self.rect_size.y * 0.5 - px, self.rect_pos.y, px)
let dark = step(y0, p.y) * step(p.y, y0 + px)
let light = step(y0 + px, p.y) * step(p.y, y0 + px * 2.0)
return vec4(0.110, 0.016, 0.020, 1.0) * dark + vec4(0.62, 0.36, 0.38, 1.0) * light
}
mod.widgets.Vr.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let p = self.pos * self.rect_size
let x0 = lq_snap(self.rect_size.x * 0.5 - px, self.rect_pos.x, px)
let dark = step(x0, p.x) * step(p.x, x0 + px)
let light = step(x0 + px, p.x) * step(p.x, x0 + px * 2.0)
return vec4(0.110, 0.016, 0.020, 1.0) * dark + vec4(0.520, 0.360, 0.380, 1.0) * light
}
// ---------------------------------------------------------------- ground
// Plain views stop clipping so a shadow can fall past their edge; scroll
// views keep clipping their content.
mod.widgets.View.clip_x = false
mod.widgets.View.clip_y = false
mod.widgets.Window.pass.clear_color = #6a0c10
// The window's own ground: #6a0c10 with the family's grain, monochrome, one
// value per device pixel, sd 0.008, peaks inside +/-0.02. The panels carry
// the same grain in the same window space, so it runs on under them.
mod.widgets.Window.show_bg = true
// The window's ground: candy-red lacquer seen under studio light. Bright red
// at the top falling to near-black wine at the foot, the sides a little
// darker; a broad soft reflection of a light over the upper middle, a
// long softbox streak under it. Lacquer is smooth: the grain is a dither only.
mod.widgets.Window.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let sz = max(self.rect_size, vec2(1.0, 1.0))
let p = self.pos * sz
var col = lq_ground_base(self.pos)
// The broad reflection of a studio light, over the upper middle.
let q1 = (p - vec2(sz.x * 0.34, sz.y * 0.15)) / vec2(max(sz.x * 0.12, 90.0), max(sz.y * 0.07, 45.0))
col = mix(col, vec3(0.94, 0.62, 0.64), 0.42 * exp(-dot(q1, q1)))
// Its hot middle, smaller and paler.
let q3 = q1 * vec2(2.2, 2.6) + vec2(0.0, 0.5)
col = mix(col, vec3(1.0, 0.80, 0.81), 0.22 * exp(-dot(q3, q3)))
// A softbox caught as a long thin streak a little lower: flat along
// its length, falling off softly at its ends and edges.
let q2 = (p - vec2(sz.x * 0.37, sz.y * 0.225)) / vec2(max(sz.x * 0.11, 80.0), 5.0)
let qx = q2.x * q2.x
col = mix(col, vec3(0.97, 0.74, 0.76), 0.17 * exp(-qx * qx - q2.y * q2.y))
let g = lq_grain(p, self.rect_pos, px)
return vec4(col + vec3(g, g, g), 1.0)
}
// ---------------------------------------------------------------- badge
// A count is a small seated plate of the brushed steel: the face its colour
// (the neutral one steel) brushed and lit from above, the one top line, the
// dark outline, dark ink.
mod.widgets.BadgePalette.neutral = #b4b3b8
mod.widgets.BadgePalette.on_neutral = #1e1618
mod.widgets.BadgeFlat.shape = mod.widgets.BadgeShape.Rounded
mod.widgets.BadgeFlat.radius = 2.0
mod.widgets.BadgeFlat.draw_bg.pixel = fn() {
let px = lq_px(self.draw_pass.dpi_factor)
let o = self.rect_pos
let p = self.pos * self.rect_size
// The box's edges rounded DOWN to a device pixel, so the bottom and
// right outline rows stay inside the quad.
let lo = vec2(lq_snap(0.0, o.x, px), lq_snap(0.0, o.y, px))
let hi = vec2(lq_snap(self.rect_size.x - px * 0.49, o.x, px), lq_snap(self.rect_size.y - px * 0.49, o.y, px))
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
let r = min(2.0, min(h.x, h.y))
let d = Material.sd_box(p, c, h, r)
let t = clamp((p.y - lo.y) / max(hi.y - lo.y, 1.0), 0.0, 1.0)
let sx = (p.x - c.x) / max(h.x, 1.0)
let k = lq_steel_tone(t, sx, p + o, px) / 0.70
var col = self.color.rgb * k
col = mix(col, col * vec3(1.10, 0.84, 0.86), 0.5 * smoothstep(0.66, 1.0, t))
col = mix(col, vec3(0.985, 0.985, 1.0), 0.85 * Finish.band(d, px, px * 2.0, px) * step(p.y, lo.y + r + px * 2.0) * step(lo.x + r * 0.5, p.x) * step(p.x, hi.x - r * 0.5) * self.color.a)
col = mix(col, vec3(0.070, 0.030, 0.036), Finish.band(d, 0.0, px, px))
let fa = max(self.color.a, Finish.band(d, 0.0, px, px))
let a = Finish.cover(d, px) * fa
return vec4(col * a, a)
}
// ---------------------------------------------------------------- states
// Every held state is written as a plain value that cuts in at once (the
// library writes some as snap() values, which a state held as a default --
// the states page -- never applies). Live, the cut is the same as the snap.
mod.widgets.ButtonFlat.animator.hover.on.from = {all: Snap}
mod.widgets.ButtonFlat.animator.hover.on.apply = {draw_bg: {down: 0.0 hover: 1.0} draw_text: {down: 0.0 hover: 1.0}}
mod.widgets.ButtonFlat.animator.hover.down.from = {all: Snap}
mod.widgets.ButtonFlat.animator.hover.down.apply = {draw_bg: {down: 1.0 hover: 1.0} draw_text: {down: 1.0 hover: 1.0}}
mod.widgets.CheckBoxFlat.animator.hover.down.from = {all: Snap}
mod.widgets.CheckBoxFlat.animator.hover.down.apply = {draw_bg: {down: 1.0 hover: 1.0} draw_text: {down: 1.0 hover: 1.0}}
mod.widgets.RadioButtonFlat.animator.hover.down.from = {all: Snap}
mod.widgets.RadioButtonFlat.animator.hover.down.apply = {draw_bg: {down: 1.0 hover: 1.0} draw_text: {down: 1.0 hover: 1.0}}
mod.widgets.TextInputFlat.animator.hover.on.from = {all: Snap}
mod.widgets.TextInputFlat.animator.hover.on.apply = {draw_bg: {down: 0.0 hover: 1.0} draw_text: {down: 0.0 hover: 1.0}}
mod.widgets.TextInputFlat.animator.hover.down.from = {all: Snap}
mod.widgets.TextInputFlat.animator.hover.down.apply = {draw_bg: {down: 1.0 hover: 1.0} draw_text: {down: 1.0 hover: 1.0}}
mod.widgets.DropDownFlat.animator.hover.on.from = {all: Snap}
mod.widgets.DropDownFlat.animator.hover.on.apply = {draw_bg: {down: 0.0 hover: 1.0} draw_text: {down: 0.0 hover: 1.0}}
mod.widgets.DropDownFlat.animator.hover.down.from = {all: Snap}
mod.widgets.DropDownFlat.animator.hover.down.apply = {draw_bg: {down: 1.0 hover: 1.0} draw_text: {down: 1.0 hover: 1.0}}
// ---------------------------------------------------------------- switches and meter
// THE METER sits in the same window as the readouts and the screen: the
// library's display housing drawn round its face -- the bezel ring, a shade
// of whatever ground it is set in with a device pixel of dark at its edge;
// the face with the surround's shadow falling across it from the top and a
// device pixel of black where the glass meets the bezel; the glass's quiet
// diagonal band and the light line inside its top edge; the lip of light on
// the ground under it -- with the same corner, so the meter and the
// readouts read as one family. The scale, the red zone, the needle and its
// pivot are the library's own.
mod.widgets.NeedleMeter.draw_bg.border_radius = uniform(theme.corner_radius * 0.5)
mod.widgets.NeedleMeter.draw_bg.color_bezel = uniform(theme.color_d_05)
mod.widgets.NeedleMeter.draw_bg.color_lip = uniform(theme.color_u_15)
mod.widgets.NeedleMeter.draw_bg.pixel = fn() {
let p = self.pos * self.rect_size
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let lo = vec2(Finish.snap(self.rect_pos.x, px), Finish.snap(self.rect_pos.y, px)) - self.rect_pos
let hi = vec2(Finish.snap(self.rect_pos.x + self.rect_size.x, px), Finish.snap(self.rect_pos.y + self.rect_size.y, px) - px) - self.rect_pos
let c = (lo + hi) * 0.5
let ho = (hi - lo) * 0.5
let r_out = min(self.border_radius, min(ho.x, ho.y))
let outer = Material.sd_box(p, c, ho, r_out)
let b = Finish.snap(2.0, px)
let hf = max(ho - vec2(b, b), vec2(0.5, 0.5))
let rf = max(r_out - b, 0.0)
let face_d = Material.sd_box(p, c, hf, rf)
let pv = self.pivot()
let rr = self.scale_radius()
let q = p - pv
let r = length(q)
let ang = atan2(q.x, 0.0 - q.y)
let sweep = self.sweep * 0.017453293
let t = ang / sweep + 0.5
let on_scale = step(-0.0001, t) * step(t, 1.0001)
var face = self.color.rgb
// The scale: a hairline arc and a tick at every division.
let arc = Finish.cover(abs(r - rr) - px * 0.5, px) * on_scale
let n = max(floor(self.majors + 0.5), 1.0) * max(floor(self.minors + 0.5), 1.0)
let k = floor(clamp(t, 0.0, 1.0) * n + 0.5)
let ak = (k / n - 0.5) * sweep
let perp = abs(q.x * cos(ak) + q.y * sin(ak))
let mn = max(floor(self.minors + 0.5), 1.0)
let major = 1.0 - step(0.5, k - mn * floor(k / mn + 0.0001))
let len = mix(self.tick_minor, self.tick_major, major)
let radial = max(rr - len - r, r - rr)
let tick = Finish.cover(perp - px * mix(0.5, 1.0, major), px) * Finish.cover(radial, px) * step(0.0, 0.0 - q.y * cos(ak) + q.x * sin(ak))
face = mix(face, self.color_scale.rgb, max(arc, tick))
if self.zone < 0.999 {
let zone_t = Finish.cover((self.zone - t) * sweep * r, px) * Finish.cover((t - 1.0) * sweep * r, px)
let zone_r = Finish.cover(max(rr + px - r, r - rr - 2.5), px)
face = mix(face, self.color_zone.rgb, zone_t * zone_r)
}
// The needle, from under the pivot cover to just past the scale.
let na = self.needle_angle(self.value)
let nd = vec2(sin(na), 0.0 - cos(na))
let along = dot(q, nd)
let across = abs(q.x * nd.y - q.y * nd.x)
let needle = Finish.cover(across - px * 0.5, px) * Finish.cover(along - (rr + 3.0), px) * Finish.cover(0.0 - along - max(self.pivot_size - px, 0.0), px)
face = mix(face, self.color_needle.rgb, needle)
let pd = r - self.pivot_size
face = mix(face, mix(self.color_needle.rgb, self.color.rgb, 0.35), Finish.cover(pd, px))
face = mix(face, face * 0.55, Finish.band(pd, 0.0, px, px))
// The housing: the surround's shadow across the face from the top, the
// black device pixel at the glass's edge, and the glass over it all.
let insh = Finish.inset(p, c, hf, rf, vec2(0.0, 2.0), 1.8)
face = face * (1.0 - 0.55 * insh)
face = mix(face, face * 0.4, Finish.band(face_d, 0.0, px, px))
let u = (p - (c - hf)) / max(hf * 2.0, vec2(1.0, 1.0))
let diag = u.x + u.y * 0.6
let band = smoothstep(0.0, 0.12, diag) * (1.0 - smoothstep(0.28, 0.5, diag))
let top = Finish.cover(abs(p.y - (c.y - hf.y) - px * 1.5) - px * 0.5, px) * 0.6
let ga = clamp(0.03 * (band + top), 0.0, 1.0) * Finish.cover(face_d + px, px)
face = face * (1.0 - ga) + vec3(ga, ga, ga)
let bz = self.color_bezel
var col = vec4(bz.rgb * bz.a, bz.a)
col = Finish.over(col, vec4(0.0, 0.0, 0.0, 0.45 * Finish.band(outer, 0.0, px, px)))
col = mix(col, vec4(face, 1.0), Finish.cover(face_d, px))
col = col * Finish.cover(outer, px)
let lip = Finish.ring_out(outer, 0.0, px, px) * step(hi.y, p.y)
let la = self.color_lip.a * lip
return Finish.over(vec4(self.color_lip.rgb * la, la), col) * self.color.a * self.opacity
}
// ---------------------------------------------------------------- lamps
// LAMPS: an LED, as the hardware in his pictures has them. The emitter is
// the light: at full current its middle burns toward white and its rim is
// its most saturated colour; at lower current the middle is no longer
// white, the colour deeper, the glow smaller; out, it is dark tinted glass,
// its colour barely there. Round it a bezel: a raised ring lit from the
// upper left, or a socket cut into the surface, its upper wall in shadow
// and a catch light on its lower rim. Over the lens one crisp highlight of
// a dome. Its light is added, never painted: a tight hot halo and a short
// bloom in its own hue, screened over whatever is there and gone in about
// two radii; on a pale ground, where light can barely brighten, its hue is
// also laid round it as a tint that keeps the ground's luminance (the
// library's halo rule: at most 0.45 one device pixel out, halving every
// 0.4 of its reach, gone at the reach), so it never darkens or greys its
// ground. A bar is the same light drawn out into a light pipe.
let lq_led_lamp_add = fn(res: vec4, light: vec3, ground: vec3) -> vec4 {
let est = res.rgb + ground * (1.0 - res.a)
let add = (vec3(1.0, 1.0, 1.0) - exp(-light)) * max(vec3(1.0, 1.0, 1.0) - est, vec3(0.0, 0.0, 0.0))
return vec4(res.rgb + add, res.a)
}
// An LED's colour at full strength: its brightest channel at one.
let lq_led_lamp_full = fn(ink: vec3) -> vec3 {
return ink / max(max(ink.x, max(ink.y, ink.z)), 0.01)
}
// The same colour at the emitter's own chroma: the hue kept and its
// saturation raised by `vivid` (0 leaves a pale colour pale; a colour with
// next to no hue, a white or a plain lamp, stays neutral either way).
let lq_led_lamp_hue = fn(ink: vec3, vivid: float) -> vec3 {
let f = lq_led_lamp_full(ink)
let mn = min(f.x, min(f.y, f.z))
let ch = 1.0 - mn
let pure = (f - vec3(mn, mn, mn)) / max(ch, 0.001)
return mix(f, pure, clamp(vivid, 0.0, 1.0) * 0.72 * smoothstep(0.03, 0.12, ch))
}
// The lamp: its lens a rounded box `hb` either side of `c` (round when the
// two are equal), `lit` its current 0..1, `ink` its colour and `vivid` how
// far its chroma is raised, `ground` what shows through where nothing is
// drawn, `room` how far its light may reach, `bez` and `raised` its bezel
// (raised 0: a recess cut in what is under it; 1: a raised ring of `bez`;
// 2: the same ring polished metal). Premultiplied over `dst`.
let lq_led_lamp = fn(dst: vec4, p: vec2, c: vec2, hb: vec2, lit: float, ink: vec3, vivid: float, px: float, ground: vec3, room: float, bez: vec3, raised: float) -> vec4 {
let rt = max(min(hb.x, hb.y), 0.4)
let d = Material.sd_box(p, c, hb, rt)
let bw = clamp(0.3 * rt, 0.7, 2.4)
let ds = d - bw
let L = clamp(lit, 0.0, 1.0)
let b = lq_led_lamp_full(ink)
let hue = lq_led_lamp_hue(ink, vivid)
let pw = L * sqrt(L)
let white = vec3(1.0, 1.0, 1.0)
let lum = vec3(0.2126, 0.7152, 0.0722)
let pale = smoothstep(0.35, 0.75, dot(ground, lum))
var res = dst
let x = max(ds, 0.0)
let reach = max(room, 1.0)
let win = 1.0 - smoothstep(0.55 * reach, reach, x)
// The lens's own frame: `q` in lens radii from its centre line, `t` its
// distance out, `nrm` the way its outline faces.
let ex = max(hb.x - hb.y, 0.0)
let ey = max(hb.y - hb.x, 0.0)
let qx = p.x - c.x
let qy = p.y - c.y
let q = vec2(qx - clamp(qx, 0.0 - ex, ex), qy - clamp(qy, 0.0 - ey, ey)) / rt
let t = clamp(length(q), 0.0, 1.0)
let nrm = q / max(length(q), 0.001)
// What the bead and its bezel throw on the ground: a soft shadow, down.
let shd = Finish.drop(p, c, hb + vec2(bw, bw), rt + bw, vec2(0.0, 0.22 * rt + 0.3), 0.32 * rt + px) * mix(0.18, 0.34, min(raised, 1.0)) * (1.0 - 0.6 * pw)
res = Finish.over(res, vec4(ground * 0.4 * shd, shd))
if L > 0.001 && pale > 0.001 {
// The tint, on a pale ground only: its hue laid over, never darker.
let h = clamp(0.42 * pw * pow(2.0, -x / max(0.4 * reach, 0.5)) * win, 0.0, 0.45) * pale
let est = res.rgb + ground * (1.0 - res.a)
let w = clamp(dot(hue, lum) / max(dot(est, lum), 0.001), 0.0, 1.0)
res = Finish.over(res, vec4(hue * h * w, h * w))
}
// The bezel: raised, a ring of `bez` rounded over, lit from the upper
// left; or a socket cut into what is under it, its upper wall in shadow
// and a catch light on its lower rim.
let a_s = Finish.cover(ds, px)
let u = clamp(d / max(bw, 0.01), 0.0, 1.0)
let facing = dot(nrm * (u - 0.5) * 2.0, vec2(-0.45, -0.89))
let metal = clamp(raised - 1.0, 0.0, 1.0)
var rc = bez * (0.94 + mix(0.12, 0.55, metal) * facing)
rc = mix(rc, min(bez + vec3(0.30, 0.30, 0.30), white), smoothstep(mix(0.35, 0.55, metal), 0.9, facing) * mix(0.6, 0.85, metal))
rc = mix(rc, rc * 0.8, Finish.band(ds, 0.0, max(px, 0.45), px) * smoothstep(0.0, 0.8, nrm.y) * 0.8)
let under = res.rgb / max(res.a, 0.001) * step(0.001, res.a) + ground * (1.0 - step(0.001, res.a))
let gy = clamp(nrm.y * t, -1.0, 1.0)
var sc = under * mix(0.55, 0.92, smoothstep(-0.9, 0.9, gy))
sc = mix(sc, min(under * 1.06 + vec3(0.28, 0.28, 0.28), white), Finish.band(ds, 0.0, max(px, 0.45), px) * smoothstep(0.2, 0.95, nrm.y) * 0.85)
sc = min(mix(sc, rc, min(raised, 1.0)), white)
res = Finish.over(res, vec4(sc * a_s, a_s))
// The light: a tight hot halo and a short bloom in its hue, gone in
// about two radii, falling on the bezel too.
if L > 0.001 {
let xl = max(d, 0.0)
let halo = exp(-xl / max(0.30 * rt, 0.4)) * 1.5 + exp(-xl / max(0.65 * rt, 0.7)) * 0.32
let hwin = 1.0 - smoothstep(1.1 * rt, 2.0 * rt + 1.0, xl)
res = lq_led_lamp_add(res, mix(hue, white, 0.15 * pw) * halo * pw * hwin * win, ground)
}
// THE BEAD: a glossy dome of coloured glass. Its colour deepens toward
// the top; light pools low inside, where it comes through the dome; lit,
// its middle burns toward white at full current and its rim is its most
// saturated colour.
let top = smoothstep(-1.0, 0.6, q.y)
let pool = exp(-(q.y - 0.55) * (q.y - 0.55) / 0.09 - q.x * q.x / 0.30)
var on = mix(hue * mix(0.55, 0.8, L), mix(b, hue, 0.5), top)
on = mix(on, min(mix(b, hue, 0.4) + vec3(0.35, 0.35, 0.35), white), pool * 0.5)
on = mix(on, vec3(1.0, 0.99, 0.975), exp(-t * t / 0.20) * smoothstep(0.5, 1.0, L) * 0.85)
on = mix(on, hue * (0.82 + 0.18 * L), smoothstep(0.62, 1.0, t))
on = on * (0.42 + 0.58 * L)
// Out: dark tinted glass on a dark ground, a grey bead on a pale one,
// its colour barely there, the same dome, a catch light along its lower
// rim.
let offc = mix(mix(vec3(0.085, 0.09, 0.115), ground * 0.62, pale), b * 0.4, 0.12)
var off = mix(offc * 0.62, offc * 1.08, top)
off = mix(off, min(offc + vec3(0.18, 0.18, 0.18), white), pool * 0.35)
off = off * mix(1.0, 0.75, smoothstep(0.75, 1.0, t))
let catchl = smoothstep(0.62, 0.9, t) * (1.0 - smoothstep(0.9, 1.0, t)) * smoothstep(0.2, 0.9, nrm.y)
off = off + vec3(0.30, 0.30, 0.32) * catchl * (1.0 - 0.5 * pale)
var col = mix(off, min(on, white), smoothstep(0.0, 0.35, L))
// One crisp highlight: a white oval across its upper third.
let e = length(vec2(q.x / 0.62, (q.y + 0.46) / 0.30))
let spec = (1.0 - smoothstep(0.55, 1.0, e)) * mix(1.0, 0.3, clamp((q.y + 0.76) / 0.6, 0.0, 1.0))
col = mix(col, white, spec * mix(0.8, 0.55, L) * smoothstep(0.9, 2.2, rt))
// Out, a faint seam where the dome meets its bezel; lit, none: the rim
// is the colour.
col = mix(col, col * 0.7, Finish.band(d, 0.0, px * 0.6, px) * 0.6 * (1.0 - smoothstep(0.0, 0.25, L)))
col = min(col, white)
let a_l = Finish.cover(d, px)
return Finish.over(res, vec4(col * a_l, a_l))
}
// The Lamp widget and the bar lamp: the lamp above, here in a ring of black lacquer.
// The halo's room is held in the lamp's own box (the library reserves a
// lens thickness either side once a halo is asked for), so the light is
// never cut at its edge.
mod.widgets.Lamp.halo = 0.3
mod.widgets.Lamp.halo_reach = 1.0
mod.widgets.Lamp.draw_bg.vertex = fn() {
let m = 2.0
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m))
}
mod.widgets.Lamp.draw_bg.pixel = fn() {
let m = 2.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let reach = clamp(self.reach, 0.0, 1.0)
let t = self.rect_size.y / (1.0 + 2.0 * reach)
let room = t * reach
let c = self.rect_size * 0.5
let half_len = max((self.rect_size.x - 2.0 * room) * 0.5, t * 0.5)
// The lens is the lamp's size; its socket stands round it in the room.
let hb = vec2(mix(t * 0.5, half_len, self.bar), t * 0.5)
let res = lq_led_lamp(vec4(0.0, 0.0, 0.0, 0.0), p, c, hb, self.lit, self.intent_color().rgb, 1.0 - step(3.5, self.intent), px, vec3(0.416, 0.047, 0.063), max(room - 0.5, 2.0), vec3(0.07, 0.045, 0.045), 1.0)
return res * self.opacity
}
true