makepad/widgets/sheets/themes/ferrofluid/widgets.splash

4275 lines
228 KiB
Text

use mod.prelude.widgets_internal.*
use mod.sdf.Cap
// Minimal instrument hardware. Keys are square-ish caps standing on a pale
// case: a flat face with a hair of gradient, one device pixel of light on
// its top edge, a short skirt below it, a one-pixel contact line and a
// short, crisp drop. Wells are cut in: a shade under the top edge and a
// light catch under the lower one. The case is grained, not painted. Orange
// marks the main action, the chosen answer and the lit lamp; nothing glows.
mod.widgets.Button.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 3.0 bottom: 5.0}
mod.widgets.Button.draw_bg.border_radius = 2.5
mod.widgets.Button.draw_bg.border_size = 0.5
// A ROW OF FIELDS IS ONE HEIGHT: the box metrics are stated once here and
// handed to each field below. Keys take the same 26: 24 of face and a skirt
// of 2.
let field_height = 26.0
let field_pad_v = mod.theme.mspace_1.top
let field_radius = 1.5
let field_border = 0.5
// The room a Fit field takes: where a field is Fit, the padding IS its
// height.
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.
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 takes
// the height alone: a padding would displace its line.
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)
mod.widgets.NumberField.draw_bg.border_radius = field_radius
field_frame(mod.widgets.ValueInput)
field_frame(mod.widgets.DateField)
field_frame(mod.widgets.TimeField)
field_frame(mod.widgets.DatePicker)
field_frame(mod.widgets.DateRangePicker)
// The chrome-less input a well holds must impose no height of its own.
mod.widgets.WellInput.min_height = 0.0
mod.widgets.Window.pass.clear_color = #efedea
// Plain views stop clipping, so a key's drop and contact line may fall into
// the gap beside it. Scroll views keep their own clip.
mod.widgets.View.clip_x = false
mod.widgets.View.clip_y = false
// A held state on the still states page must show: the stock hover and
// press arrive as snapped values, which a state held from the start never
// receives, so both are restated with plain values and a snapped entry.
let fk_held_states = fn(w) {
w.animator.hover.on.from = {all: Snap}
w.animator.hover.on.apply = {draw_bg: {down: 0.0, hover: 1.0} draw_text: {down: 0.0, hover: 1.0}}
w.animator.hover.down.from = {all: Snap}
w.animator.hover.down.apply = {draw_bg: {down: 1.0, hover: 1.0} draw_text: {down: 1.0, hover: 1.0}}
}
fk_held_states(mod.widgets.ButtonFlat)
fk_held_states(mod.widgets.CheckBoxFlat)
fk_held_states(mod.widgets.ToggleFlat)
fk_held_states(mod.widgets.RadioButtonFlat)
fk_held_states(mod.widgets.RadioButtonTabFlat)
fk_held_states(mod.widgets.TextInputFlat)
fk_held_states(mod.widgets.DropDownFlat)
// ---------------------------------------------------------------------------
// SHARED PARTS. Distances in points, negative inside, y down; `px` is one
// device pixel in points. Colours are sRGB values.
let g3 = fn(v: float) -> vec3 {
return vec3(v, v, v)
}
// A rounded box, true radius.
let fk_rbox = fn(p: vec2, c: vec2, h: vec2, r: float) -> float {
let rr = clamp(r, 0.0, min(h.x, h.y))
let q = abs(p - c) - h + vec2(rr, rr)
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0, 0.0))) - rr
}
// The same with a radius per corner: (top left, top right, bottom right,
// bottom left).
let fk_rbox4 = fn(p: vec2, c: vec2, h: vec2, r4: vec4) -> float {
let q = p - c
let rt = mix(r4.x, r4.y, step(0.0, q.x))
let rb = mix(r4.w, r4.z, step(0.0, q.x))
return fk_rbox(p, c, h, mix(rt, rb, step(0.0, q.y)))
}
// A key's corners from the stock corner uniforms: -1 follows the key's own
// radius; a group squares its joints with 0; nothing rounder than the key.
let fk_corner = fn(v: float, r: float) -> float {
return mix(min(max(v, 0.0), r), r, step(v, -0.5))
}
let fk_corners = fn(tl: float, tr: float, br: float, bl: float, r: float) -> vec4 {
return vec4(fk_corner(tl, r), fk_corner(tr, r), fk_corner(br, r), fk_corner(bl, r))
}
let fk_seg = fn(p: vec2, a: vec2, b: vec2) -> float {
let pa = p - a
let ba = b - a
let k = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0)
return length(pa - ba * k)
}
// A crisp horizontal or vertical run of device pixels: 1 inside a..b.
let fk_run = fn(v: float, a: float, b: float) -> float {
return step(a, v) * (1.0 - step(b, v))
}
let fk_grow = fn(rp: vec2, rs: vec2, m: float) -> vec4 {
return vec4(rp.x - m, rp.y - m, rs.x + 2.0 * m, rs.y + 2.0 * m)
}
// The case: pale warm grey, one-pixel monochrome grain (sd about 0.005)
// and a paper fibre three pixels long and one and a half tall (sd about
// 0.006). No fall, no vignette. Two hashes of different lattices are mixed
// so no period shows.
let fk_grain = fn(q: vec2) -> float {
let i = floor(q)
let a = Finish.hash(i * 0.7548776662 + vec2(i.y * 0.5698402910, 13.71))
let b = Finish.hash(vec2(i.y, i.x) * 1.3247179572 + vec2(41.3, i.x * 0.3819660113))
return a - b
}
let fk_fibre = fn(q: vec2) -> float {
let s = vec2(q.x / 3.0, q.y / 1.5)
return Finish.noise(s + vec2(Finish.hash(vec2(floor(s.y), 7.0)) * 57.0, 0.0)) - 0.5
}
let fk_case = fn(base: vec3, p: vec2, px: float) -> vec3 {
let q = p / px
return base + g3(fk_grain(q) * 0.0115 + fk_fibre(q) * 0.021)
}
// A plate's face: the grain alone at the brief's sd 0.005 (window
// coordinates, so plates and ground share one lattice); no fibre, which
// began to structure on the smaller fields of a plate.
let fk_plate_grain = fn(base: vec3, w: vec2, px: float) -> vec3 {
return base + g3(fk_grain(w / px) * 0.0125)
}
// The colours of a keycap by tone: 0 white, 1 signal orange, 2 black,
// 3 grey. `k` picks the part: 0 face top, 1 face foot, 2 roll-off at the
// very foot, 3 top light line, 4 skirt, 5 pressed top, 6 pressed foot.
let fk_tone = fn(tone: float, k: float) -> vec3 {
let wh = step(tone, 0.5)
let orng = step(0.5, tone) * step(tone, 1.5)
let bk = step(1.5, tone) * step(tone, 2.5)
let gr = step(2.5, tone)
var w = vec3(0.961, 0.961, 0.961)
var o = vec3(0.933, 0.353, 0.141)
var b = vec3(0.220, 0.220, 0.220)
var g = vec3(0.816, 0.800, 0.796)
if k > 0.5 {
w = vec3(0.922, 0.922, 0.922)
o = vec3(0.925, 0.345, 0.137)
b = vec3(0.208, 0.208, 0.208)
g = vec3(0.796, 0.780, 0.776)
}
if k > 1.5 {
w = vec3(0.900, 0.900, 0.898)
o = vec3(0.863, 0.220, 0.094)
b = vec3(0.150, 0.150, 0.150)
g = vec3(0.720, 0.706, 0.702)
}
if k > 2.5 {
w = vec3(1.0, 1.0, 1.0)
o = vec3(0.984, 0.643, 0.365)
b = vec3(0.624, 0.624, 0.624)
g = vec3(0.961, 0.961, 0.961)
}
if k > 3.5 {
w = vec3(0.831, 0.831, 0.831)
o = vec3(0.761, 0.255, 0.059)
b = vec3(0.110, 0.110, 0.110)
g = vec3(0.690, 0.675, 0.671)
}
if k > 4.5 {
w = vec3(0.871, 0.867, 0.867)
o = vec3(0.835, 0.251, 0.059)
b = vec3(0.180, 0.180, 0.180)
g = vec3(0.741, 0.725, 0.722)
}
if k > 5.5 {
w = vec3(0.918, 0.918, 0.918)
o = vec3(0.835, 0.251, 0.059)
b = vec3(0.196, 0.196, 0.196)
g = vec3(0.788, 0.773, 0.769)
}
return w * wh + o * orng + b * bk + g * gr
}
// The shade a key throws on the case: a short drop down and a little to
// the right (reach under 6 points), the one-pixel contact line under its
// foot and a faint pixel round its sides. `lift` 1 at rest, 0 pressed.
let fk_shade = fn(p: vec2, c: vec2, h: vec2, r: float, px: float, lift: float, dis: float, drop_a: float) -> vec4 {
var out = vec4(0.0, 0.0, 0.0, 0.0)
let dr = Finish.drop(p, c, h, r, vec2(0.45, 1.1), 2.0) * drop_a * 1.12 * lift * (1.0 - dis)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, dr))
let d = fk_rbox(p, c, h, r)
let foot = smoothstep(c.y + h.y - r - 0.5, c.y + h.y - 0.2, p.y)
let ring = Finish.ring_out(d, 0.0, px, px)
let con = ring * mix(0.09, mix(0.18, 0.32, lift), foot) * mix(1.0, 0.4, dis)
return Finish.over(out, vec4(0.0, 0.0, 0.0, con))
}
// A keycap in the box lo..hi (quad-local, snapped): the skirt `sk` tall at
// the foot, the face above it with its gradient, its one-pixel top light
// line, a roll-off at the foot of coloured faces, and pressed in: the skirt
// gone, the face one pixel lower, its gradient turned and a shade under its
// top edge. `rim` the strength of a dark hairline round it.
let fk_cap4 = fn(p: vec2, lo: vec2, hi: vec2, r4: vec4, px: float, sk: float, tone: float, hover: float, press: float, dis: float, rim: float) -> vec4 {
let h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox4(p, c, h, r4)
let flo = vec2(lo.x, lo.y + press * px)
let fhi = vec2(hi.x, hi.y - sk * (1.0 - press))
let fh = (fhi - flo) * 0.5
let fc = flo + fh
let fd = fk_rbox4(p, fc, fh, r4)
let r = mix(mix(r4.x, r4.y, step(fc.x, p.x)), mix(r4.w, r4.z, step(fc.x, p.x)), step(fc.y, p.y))
let t = clamp((p.y - flo.y) / max(fhi.y - flo.y, 1.0), 0.0, 1.0)
// the face
var face = mix(fk_tone(tone, 0.0), fk_tone(tone, 1.0), t)
let roll = (1.0 - smoothstep(0.0, 4.0 * px, fhi.y - p.y)) * step(0.5, tone)
face = mix(face, fk_tone(tone, 2.0), roll * 0.8)
// hover lifts the face: +0.045 on the pale caps, +0.03 on orange,
// +0.08 on black, so it reads at arm's length and not only in a probe
let is_o = step(0.5, tone) * step(tone, 1.5)
let is_b = step(1.5, tone) * step(tone, 2.5)
let hv = mix(mix(0.045, 0.03, is_o), 0.08, is_b)
face = face + g3(hv * hover * (1.0 - press))
let pf = mix(fk_tone(tone, 5.0), fk_tone(tone, 6.0), t)
face = mix(face, pf, press)
// pressed in: a shade two pixels deep under the top edge
let ytop = p.y - flo.y
face = face - g3(0.10 * press * (1.0 - smoothstep(px, 2.0 * px, ytop)))
// the top light line, inset by the corner at its ends
let tl = fk_run(p.y, flo.y, flo.y + px) * step(abs(p.x - fc.x), fh.x - r + 0.5 * px)
face = mix(face, fk_tone(tone, 3.0), tl * (1.0 - press) * (1.0 - dis))
// wide caps catch the light on their left edge too, at half strength
let ll = fk_run(p.x, flo.x, flo.x + px) * step(abs(p.y - fc.y), fh.y - r) * step(4.0 * fh.y, 2.0 * fh.x)
face = mix(face, fk_tone(tone, 3.0), ll * 0.5 * (1.0 - press) * (1.0 - dis))
var skirt = fk_tone(tone, 4.0)
// disabled: the face most of the way to the case, the skirt paler
face = mix(face, vec3(0.925, 0.918, 0.906), dis * 0.6)
skirt = mix(skirt, vec3(0.878, 0.871, 0.859), dis)
let rl = Finish.band(d, 0.0, px, px) * rim
skirt = mix(skirt, vec3(0.0, 0.0, 0.0), rl)
face = mix(face, face * (1.0 - rim * 0.9), Finish.band(fd, 0.0, px, px))
let a = Finish.cover(d, px)
var out = vec4(skirt * a, a)
let fa = Finish.cover(fd, px)
return Finish.over(out, vec4(face * fa, fa))
}
let fk_cap = fn(p: vec2, lo: vec2, hi: vec2, r: float, px: float, sk: float, tone: float, hover: float, press: float, dis: float, rim: float) -> vec4 {
return fk_cap4(p, lo, hi, vec4(r, r, r, r), px, sk, tone, hover, press, dis, rim)
}
// The focus ring: one pixel of signal orange two pixels out. Never a glow.
let fk_focus = fn(d: float, px: float, focus: float) -> vec4 {
let f = Finish.ring_out(d, 2.0 * px, 3.0 * px, px) * focus
return vec4(0.933 * f, 0.353 * f, 0.141 * f, f)
}
// A well cut into the case: the fill, a shade under the top edge `depth`
// deep, a hairline mouth darkest at the top, and a light catch under the
// lower rim, outside it (premultiplied, drawn first by the caller).
let fk_well = fn(p: vec2, c: vec2, h: vec2, r: float, px: float, fill: vec3, shade: float, mouth: vec3, mouth_a: float) -> vec3 {
let d = fk_rbox(p, c, h, r)
let top = c.y - h.y
let sh = 1.0 - smoothstep(0.0, px, p.y - top - px)
var col = mix(fill, vec3(0.0, 0.0, 0.0), sh * shade)
let low = smoothstep(-0.5, 0.5, p.y - c.y)
col = mix(col, mouth, Finish.band(d, 0.0, px, px) * mouth_a * mix(1.0, 0.7, low))
return col
}
let fk_catch = fn(p: vec2, c: vec2, h: vec2, r: float, px: float, a: float) -> vec4 {
let d = fk_rbox(p, c, h, r)
let l = Finish.ring_out(d, 0.0, px, px) * smoothstep(c.y + h.y - r - 0.5, c.y + h.y, p.y) * a
return vec4(l, l, l, l)
}
// ---------------------------------------------------------------------------
// BUTTONS. One key for the family; `kind` picks the dressing: 0 white key,
// 1 orange key (the main action), 2 black key, 3 grey key, 4 engraved
// outline in the case, 5 bare until touched, 6 a flat printed key with no
// skirt. The quad grows by `m` (6 points) for the drop and the focus ring.
// The Default button is no key: it is the faders' ferrofluid, drawn out
// into a pill (THE DEFAULT BUTTON below).
let fk_button = fn(pos: vec2, size: vec2, o: vec2, px: float, hover: float, down: float, focus: float, disabled: float, loading: float, anim: float, r4: vec4, kind: float, m: float) -> vec4 {
let p = pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let live = 1.0 - disabled
let press = down * live
let hov = hover * live
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o
let h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox4(p, c, h, r4)
let r = max(max(r4.x, r4.y), max(r4.z, r4.w))
let raised = step(kind, 3.5)
let flat = step(5.5, kind)
var out = vec4(0.0, 0.0, 0.0, 0.0)
if kind < 3.5 {
// hovered, the key lifts: its drop from 0.17 to 0.24
out = fk_shade(p, c, h, r, px, 1.0 - press, disabled, mix(0.15, 0.215, hov))
out = Finish.over(out, fk_cap4(p, lo, hi, r4, px, 3.0 * px, kind, hov, press, disabled, 0.0))
// a joint in a group of keys: a seam pixel on each side of it
let jl = step(r4.x + r4.w, 0.01) * fk_run(p.x, lo.x, lo.x + px)
let jr = step(r4.y + r4.z, 0.01) * fk_run(p.x, hi.x - px, hi.x)
let sm = (jl + jr) * fk_run(p.y, lo.y, hi.y) * 0.28
out = Finish.over(out, vec4(0.0, 0.0, 0.0, sm))
}
if kind > 5.5 {
// flat: a printed key with no skirt, a faint edge and contact line
out = fk_shade(p, c, h, r, px, (1.0 - press) * 0.6, disabled, 0.0)
out = Finish.over(out, fk_cap4(p, lo, hi, r4, px, 0.0, 0.0, hov, press, disabled, 0.10))
}
if kind > 3.5 && kind < 5.5 {
let bare = step(4.5, kind)
// the engraving: a dark pixel on the edge, a light one under it
let dk = Finish.band(d, 0.0, px, px) * (1.0 - bare) * mix(0.24, 0.10, disabled)
let d2 = fk_rbox4(p - vec2(0.0, px), c, h, r4)
let lt = Finish.band(d2, 0.0, px, px) * (1.0 - Finish.band(d, 0.0, px, px)) * step(c.y, p.y) * (1.0 - bare) * 0.55
out = Finish.over(out, vec4(lt, lt, lt, lt))
// touched: a faint face comes up out of the case, pressed a shade
let fa = Finish.cover(d, px) * (hov * (1.0 - press) * mix(0.55, 0.45, bare))
out = Finish.over(out, vec4(fa, fa, fa, fa))
let da = Finish.cover(d, px) * press * 0.07
out = Finish.over(out, vec4(0.0, 0.0, 0.0, da))
let ed = Finish.band(d, 0.0, px, px) * max(dk, bare * (hov + press) * 0.14)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, ed))
}
// focus: one orange pixel two pixels out
out = Finish.over(out, fk_focus(d, px, focus * live))
// the wait spinner, which the stock face drew and this one must too
if loading > 0.0 {
let sdf = Sdf2d.viewport(p)
let rr = max(min(h.x, h.y) - 5.0, 2.0)
let a0 = anim * 2.0 * PI
sdf.arc_round_caps(c.x, c.y - 1.0, rr, a0, a0 + 1.5 * PI, 1.5)
var ink = vec3(0.114, 0.114, 0.114)
if kind > 0.5 && kind < 2.5 {
ink = vec3(1.0, 1.0, 1.0)
}
sdf.fill(vec4(ink, loading))
out = Finish.over(out, sdf.result)
}
return out
}
// The root is the flat printed key; the sizes and the compound button are
// handed the Default's draw object below, so they stand as drops.
mod.widgets.ButtonFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ButtonFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return self.host_over(fk_button(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, fk_corners(self.border_radius_tl, self.border_radius_tr, self.border_radius_br, self.border_radius_bl, self.rect_size.y * 0.5), 6.0, 6.0), 6.0)
}
mod.widgets.ButtonFlat.clip_x = false
mod.widgets.ButtonFlat.clip_y = false
mod.widgets.ButtonFlatter.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ButtonFlatter.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return self.host_over(fk_button(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, fk_corners(self.border_radius_tl, self.border_radius_tr, self.border_radius_br, self.border_radius_bl, self.rect_size.y * 0.5), 5.0, 6.0), 6.0)
}
// THE DEFAULT BUTTON is a pool of the faders' black ferrofluid, seen flat,
// in the plane, as a 2D simulation draws it: the pill's shape, a soft
// meniscus at its rim catching the light, a highlight streak along its top
// inside the rim, the label on top. The pointer is its magnet (the
// library's field, raised on the two controls of a window nearest the
// pointer and on no others): the pool gathers a little toward it and its
// OUTLINE grows spikes in the plane, one on each of a fixed row of roots
// round the rim, each standing straight out from its root; sharp cusped
// tips on concave flanks, broad flat valleys running into the rim between
// them. A spike never moves, it only grows and shrinks: its length is how
// near its root is to the pointer, falling off over about three spikes, so
// a few neighbours grow and shrink together and nothing travels with the
// pointer; and the pointer it reads is the library's, which follows the
// real one on a spring (about a quarter of a second), so they grow and
// shrink smoothly instead of with every pixel the mouse moves. None faces
// the eye, and the middle of the pool, under the label, stays calm.
// While the field is up their lengths shimmer a little on its clock.
// Pressed, the spikes draw back and the pool swells a touch; let go, they
// come back. Focused, it takes the orange ring; disabled, it is a still
// dull puddle. A colour an app paints on it becomes the liquid's.
// The pool's rim, for a pill whose core runs `hl` either way along x from
// its middle, `r` round: how far round the rim a point `q` lies (points
// from the middle; clockwise from the top left end of its top edge); and
// the reverse, the rim point at a length round it and the rim's outward
// normal there.
let fe_rim_t = fn(q: vec2, hl: float, r: float) -> float {
let a = atan2(q.x - clamp(q.x, -hl, hl), -q.y)
var t = q.x + hl
if q.y > 0.0 {
t = 2.0 * hl + PI * r + hl - q.x
}
if q.x > hl {
t = 2.0 * hl + r * a
}
if q.x < -hl {
t = 4.0 * hl + PI * r + r * (a + PI)
}
return t
}
let fe_rim_at = fn(t: float, hl: float, r: float) -> vec4 {
var n = vec2(0.0, -1.0)
var c = vec2(t - hl, 0.0)
if t > 2.0 * hl {
let a = (t - 2.0 * hl) / r
n = vec2(sin(a), -cos(a))
c = vec2(hl, 0.0)
}
if t > 2.0 * hl + PI * r {
n = vec2(0.0, 1.0)
c = vec2(3.0 * hl + PI * r - t, 0.0)
}
if t > 4.0 * hl + PI * r {
let a = PI + (t - 4.0 * hl - PI * r) / r
n = vec2(sin(a), -cos(a))
c = vec2(-hl, 0.0)
}
return vec4(c + n * r, n)
}
// The pool's spikes at a length `t` round the rim and a height `y` over it
// (`fac` the rim's opening out with the height on the round ends): their
// roots fixed `l` apart round the rim from its origin, `ns` of them; each
// spike his outline about its root, A pow(1 - |sin(pi u / l)|, 1.5),
// standing straight out (no lean), A `amp` times the length its root may
// grow to, which runs from `hm`.x where the rim faces sideways (the round
// ends) to `hm`.y where it faces up or down (the flat edges), smoothly round
// the ends' curve, falling away as exp(-(D / 2.75 l)^2) with its root's
// distance D from the pointer `mag` (straight, not round the rim, so the
// round ends answer as the edges do), less how deep the pointer is inside
// the pool (`dz`), so over the label the spikes nearest it still stand
// full, and shimmering by a twelfth on the field's clock `tt`. The outline's height over the rim in .x, the distance
// to it (points, negative inside) in .y and its coverage in .z: the
// distance from the point's height over the outline and its offset from
// the spike's flank at that height, as from a straight edge through the
// two; where the spike is thinner than two device pixels (`px`) its
// coverage is the share of a pixel its width takes, so a tip comes out a
// pixel wide and as much fainter, and never breaks into dots.
let fe_spike_row = fn(t: float, y: float, fac: float, l: float, ns: float, amp: float, hm: vec2, mag: vec2, dz: float, hl: float, r: float, tt: float, px: float) -> vec3 {
let k = floor(t / l + 0.5)
let kw = k - ns * floor(k / ns)
let rt = fe_rim_at(kw * l, hl, r)
let fall = max(length(rt.xy - mag) - dz, 0.0) / (2.75 * l)
let lim = mix(hm.x, hm.y, smoothstep(0.35, 0.85, abs(rt.w)))
let a = max(amp * lim * exp(-fall * fall) * (0.92 + 0.08 * sin(tt * 3.0 + kw * 1.7)), 0.0001)
let x = PI * (t - k * l) / l
let h = a * pow(max(1.0 - abs(sin(x)), 0.0), 1.5)
let dv = y - h
if y <= 0.0 {
return vec3(h, dv, 0.0)
}
let sx = abs(x) * l / PI * fac
var hw = 0.0
var dh = 1000.0
if y < a {
hw = asin(clamp(1.0 - pow(y / a, 0.6666667), 0.0, 1.0)) * l / PI * fac
dh = sx - hw
}
let d = dv * abs(dh) / sqrt(dv * dv + dh * dh + 0.000001)
let ov = max(min(sx + px * 0.5, hw) - max(sx - px * 0.5, -hw), 0.0) / px
let thin = (1.0 - smoothstep(px, 2.0 * px, 2.0 * hw)) * step(y, a)
return vec3(h, d, mix(fk_aa(d, px), ov, thin))
}
// The pool at `q`: the distance to its outline (points, negative inside)
// and its coverage. The pill (`hl`, `r`) swollen by `sw` and gathered
// toward the pointer `mag`, and over its rim the spikes, his ferrofluid
// outline, on one row of fixed roots about 4.5 points apart evenly round
// the rim. The spikes are `hm`.x long at most off the round ends and
// `hm`.y off the flat edges, as long as the field `a` is.
let fe_pool = fn(q: vec2, hl: float, r: float, sw: float, mag: vec2, a: float, hm: vec2, tt: float, px: float) -> vec2 {
let d0 = length(q - vec2(clamp(q.x, -hl, hl), 0.0)) - r - sw - a * 1.2 * (1.0 - smoothstep(0.0, 18.0, length(q - mag)))
if a < 0.001 {
return vec2(d0, fk_aa(d0, px))
}
let per = 4.0 * hl + 2.0 * PI * r
let ns = max(floor(per / 4.5 + 0.5), 3.0)
let l = per / ns
var fac = 1.0
if abs(q.x) > hl {
fac = (r + max(d0, 0.0)) / r
}
let dz = max(r - length(mag - vec2(clamp(mag.x, -hl, hl), 0.0)), 0.0)
let s = fe_spike_row(fe_rim_t(q, hl, r), d0, fac, l, ns, a, hm, mag, dz, hl, r, tt, px)
if d0 <= 0.0 {
let dv = d0 - s.x
return vec2(dv, fk_aa(dv, px))
}
return s.yz
}
mod.widgets.Button.draw_bg.vertex = fn() {
let m = vec2(14.0, 20.0)
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - m, self.rect_size + 2.0 * m)
}
mod.widgets.Button.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let size = self.rect_size
let o = self.rect_pos
let live = 1.0 - self.disabled
let m = vec2(14.0, 20.0)
let p = self.pos * (size + 2.0 * m) - m
// the box on whole device pixels, as every key's, the pill a point and
// a half inside it, `q` from its middle
let blo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let bhi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o
let hs = (bhi - blo) * 0.5 - vec2(1.5, 1.5)
let mid = blo + (bhi - blo) * 0.5
let q = p - mid
let r = min(hs.x, hs.y)
let hl = hs.x - r
let press = self.down * live
// the field (none disabled), its spikes 16 points long at most off the
// flat edges, up and down, and 10 off the round ends, so they never
// reach the next key in a row (11 points off), drawn back as it is
// pressed
let hm = vec2(min(1.75 * r, 10.0), min(1.75 * r, 16.0))
let fa = smoothstep(0.0, 0.7, clamp(self.cap_field, 0.0, 1.0)) * live * (1.0 - press)
let mag = vec2(self.cap_pointer_along, self.cap_pointer_across)
let sw = 0.8 * press
let e = 0.5 * px
let f = fe_pool(q, hl, r, sw, mag, fa, hm, self.cap_time, px)
let d = f.x
let g = vec2(fe_pool(q + vec2(e, 0.0), hl, r, sw, mag, fa, hm, self.cap_time, px).x - d, fe_pool(q + vec2(0.0, e), hl, r, sw, mag, fa, hm, self.cap_time, px).x - d)
let nr = g / max(length(g), 0.0001)
// the case under it: a soft short shadow of the pill, and a contact
// shade hugging the outline, spikes and all
var out = vec4(0.0, 0.0, 0.0, Finish.drop(p, mid, hs, r, vec2(0.0, 1.2), 1.6) * 0.16 * live)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, (1.0 - smoothstep(0.0, 1.5, d)) * step(0.0, d) * 0.12 * live))
// the fluid: its black, or the colour an app paints on it; the
// meniscus over its outer 0.18 of the round, lighter on a dark liquid
// and darker on a pale one, mostly where it faces the light (the
// spikes' flanks too); the streak along the top, a point and a half in
let body = self.host_face(self.drop_color.rgb)
let dark = 1.0 - step(0.5, Finish.lum(body))
let men = 1.0 - smoothstep(0.0, max(0.18 * r, 1.5), -d)
let face = clamp(dot(nr, vec2(-0.524, -0.852)), 0.0, 1.0)
var col = mix(body, mix(body * 0.8, body + g3(0.16), dark), men * (0.2 + 0.8 * face) * 0.85)
let sy = q.y + r + sw
let streak = (1.0 - smoothstep(0.45 * px, 0.45 * px + px, abs(sy - 1.5 - 0.5 * px))) * (1.0 - smoothstep(hl, hl + 0.6 * r, abs(q.x))) * 0.5 * live
col = mix(col, vec3(1.0, 1.0, 1.0), streak)
// disabled, most of the way to the case
col = mix(col, self.color_disabled.rgb, self.disabled * 0.6)
let a = f.y
out = Finish.over(out, vec4(col * a, a))
// focus: one orange pixel two pixels out of the pool, as on every key;
// it gives way to the spikes as the field rises
out = Finish.over(out, fk_focus(length(q - vec2(clamp(q.x, -hl, hl), 0.0)) - r - sw, px, self.focus * live * (1.0 - fa)))
// the wait spinner, white on the pool, where the label was
if self.loading > 0.0 {
let sdf = Sdf2d.viewport(p)
let a0 = self.anim_time * 2.0 * PI
sdf.arc_round_caps(mid.x, mid.y, max(r - 5.0, 2.0), a0, a0 + 1.5 * PI, 1.5)
sdf.fill(vec4(1.0, 1.0, 1.0, self.loading))
out = Finish.over(out, sdf.result)
}
return out
}
// The inks: the case the drop stands on and the drop's black, which is
// also the colour the Default rests in, so that a colour an app paints on
// it becomes the liquid's (`host_face`); the light, the shade and the
// disabled ink are the library's (the material's inks, the disabled face).
mod.widgets.Button.draw_bg.ground_color = uniform(theme.color_bg_app)
mod.widgets.Button.draw_bg.drop_color = uniform(#242324)
mod.widgets.Button.draw_bg.color = uniform(#242324)
mod.widgets.Button.draw_bg.color_default = uniform(#242324)
// The pointer's field: raised while the pointer is within this many points
// of the box (the library grants it to the nearest two controls only).
mod.widgets.Button.cap_field_reach = 20.0
mod.widgets.ButtonPrimary.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ButtonPrimary.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return self.host_over(fk_button(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, fk_corners(self.border_radius_tl, self.border_radius_tr, self.border_radius_br, self.border_radius_bl, self.rect_size.y * 0.5), 1.0, 6.0), 6.0)
}
// The second action is the white key the Default was: the black is the
// drop's now.
mod.widgets.ButtonSecondary.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return self.host_over(fk_button(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, fk_corners(self.border_radius_tl, self.border_radius_tr, self.border_radius_br, self.border_radius_bl, self.rect_size.y * 0.5), 0.0, 6.0), 6.0)
}
mod.widgets.ButtonTertiary.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return self.host_over(fk_button(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, fk_corners(self.border_radius_tl, self.border_radius_tr, self.border_radius_br, self.border_radius_bl, self.rect_size.y * 0.5), 3.0, 6.0), 6.0)
}
// The destructive action is a white key; the red is in its legend only.
mod.widgets.ButtonDanger.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return self.host_over(fk_button(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, fk_corners(self.border_radius_tl, self.border_radius_tr, self.border_radius_br, self.border_radius_bl, self.rect_size.y * 0.5), 0.0, 6.0), 6.0)
}
mod.widgets.ButtonOutline.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ButtonOutline.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return self.host_over(fk_button(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, fk_corners(self.border_radius_tl, self.border_radius_tr, self.border_radius_br, self.border_radius_bl, self.rect_size.y * 0.5), 4.0, 6.0), 6.0)
}
mod.widgets.Button.clip_x = false
mod.widgets.Button.clip_y = false
mod.widgets.ButtonPrimary.clip_x = false
mod.widgets.ButtonPrimary.clip_y = false
mod.widgets.ButtonOutline.clip_x = false
mod.widgets.ButtonOutline.clip_y = false
mod.widgets.ButtonFlatter.clip_x = false
mod.widgets.ButtonFlatter.clip_y = false
// The primary, secondary, tertiary and destructive buttons are pools too:
// the Default's draw object in their own tone, the signal orange, the
// white, the grey and the white of the keys they were, their legend
// centred as the Default's; all but the destructive one answer the field. The outline and the
// flat button stay a rim and nothing.
mod.widgets.ButtonPrimary.draw_bg = mod.widgets.Button.draw_bg{}
mod.widgets.ButtonSecondary.draw_bg = mod.widgets.Button.draw_bg{}
mod.widgets.ButtonTertiary.draw_bg = mod.widgets.Button.draw_bg{}
mod.widgets.ButtonDanger.draw_bg = mod.widgets.Button.draw_bg{}
mod.widgets.ButtonPrimary.draw_bg.drop_color = uniform(#ee5a24)
mod.widgets.ButtonPrimary.draw_bg.color = uniform(#ee5a24)
mod.widgets.ButtonPrimary.draw_bg.color_default = uniform(#ee5a24)
mod.widgets.ButtonSecondary.draw_bg.drop_color = uniform(#f5f5f5)
mod.widgets.ButtonSecondary.draw_bg.color = uniform(#f5f5f5)
mod.widgets.ButtonSecondary.draw_bg.color_default = uniform(#f5f5f5)
mod.widgets.ButtonTertiary.draw_bg.drop_color = uniform(#d0cccb)
mod.widgets.ButtonTertiary.draw_bg.color = uniform(#d0cccb)
mod.widgets.ButtonTertiary.draw_bg.color_default = uniform(#d0cccb)
mod.widgets.ButtonDanger.draw_bg.drop_color = uniform(#f5f5f5)
mod.widgets.ButtonDanger.draw_bg.color = uniform(#f5f5f5)
mod.widgets.ButtonDanger.draw_bg.color_default = uniform(#f5f5f5)
mod.widgets.ButtonPrimary.cap_field_reach = 20.0
mod.widgets.ButtonSecondary.cap_field_reach = 20.0
mod.widgets.ButtonTertiary.cap_field_reach = 20.0
mod.widgets.ButtonDanger.cap_field_reach = 0.0
mod.widgets.ButtonPrimary.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 4.0 bottom: 4.0}
mod.widgets.ButtonSecondary.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 4.0 bottom: 4.0}
mod.widgets.ButtonTertiary.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 4.0 bottom: 4.0}
mod.widgets.ButtonDanger.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 4.0 bottom: 4.0}
mod.widgets.ButtonSecondary.clip_x = false
mod.widgets.ButtonSecondary.clip_y = false
mod.widgets.ButtonTertiary.clip_x = false
mod.widgets.ButtonTertiary.clip_y = false
mod.widgets.ButtonDanger.clip_x = false
mod.widgets.ButtonDanger.clip_y = false
// The sizes and the compound button share the root's draw object; they are
// handed one of their own, the Default's, and stand as drops (their inks
// and their field below).
mod.widgets.ButtonXs.draw_bg = mod.widgets.Button.draw_bg{}
mod.widgets.ButtonSm.draw_bg = mod.widgets.Button.draw_bg{}
mod.widgets.ButtonLg.draw_bg = mod.widgets.Button.draw_bg{}
mod.widgets.ButtonXl.draw_bg = mod.widgets.Button.draw_bg{}
mod.widgets.ButtonCompound.draw_bg = mod.widgets.Button.draw_bg{}
// Faces of their own under the root keep their own quad: the grown one is
// the keys' alone.
mod.widgets.CloseButton.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size)
}
mod.widgets.CopyButton.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size)
}
mod.widgets.BurgerButton.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size)
}
mod.widgets.ButtonFloating.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size)
}
mod.widgets.ButtonFloatingSm.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size)
}
// Key geometry: 26 points, 24 of face and 2 of skirt; the legend is centred
// on the face, a point above the middle of the box. The drop has no skirt:
// on it the legend and the icon stand in the middle, the sizes' too.
mod.widgets.ButtonFlat.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 3.0 bottom: 5.0}
mod.widgets.ButtonFlat.min_height = 26.0
mod.widgets.Button.min_height = 26.0
mod.widgets.Button.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 4.0 bottom: 4.0}
mod.widgets.ButtonIcon.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 4.0 bottom: 4.0}
mod.widgets.Button.icon_walk = mod.turtle.Walk{width: 14.0 height: Fit}
mod.widgets.ButtonXs.min_height = 0.0
mod.widgets.ButtonSm.min_height = 0.0
mod.widgets.ButtonXs.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 0.0 bottom: 0.0}
mod.widgets.ButtonSm.padding = mod.turtle.Inset{left: 8.0 right: 8.0 top: 0.0 bottom: 0.0}
mod.widgets.ButtonLg.padding = mod.turtle.Inset{left: 14.0 right: 14.0 top: 0.0 bottom: 0.0}
mod.widgets.ButtonXl.padding = mod.turtle.Inset{left: 16.0 right: 16.0 top: 0.0 bottom: 0.0}
// Legends: print ink on the pale keys, white on the orange key and light on
// the drop; the main action at the heavier weight; the danger key's legend
// red. The drop's disabled legend is a muted light that still reads on the
// faded drop.
mod.widgets.ButtonPrimary.draw_text.color = #ffffff
mod.widgets.ButtonPrimary.draw_text.color_hover = #ffffff
mod.widgets.ButtonPrimary.draw_text.color_down = #fff2ed
mod.widgets.ButtonPrimary.draw_text.color_focus = #ffffff
mod.widgets.ButtonPrimary.draw_text.color_disabled = #a9a7a4
mod.widgets.ButtonPrimary.draw_icon.color = #ffffff
mod.widgets.ButtonPrimary.draw_text.text_style = mod.theme.font_bold{font_size: 8.0}
mod.widgets.ButtonSecondary.draw_text.color = #1d1d1d
mod.widgets.ButtonSecondary.draw_text.color_hover = #111111
mod.widgets.ButtonSecondary.draw_text.color_down = #1d1d1d
mod.widgets.ButtonSecondary.draw_text.color_focus = #1d1d1d
mod.widgets.ButtonSecondary.draw_icon.color = #1d1d1d
mod.widgets.ButtonSecondary.draw_text.text_style = mod.theme.font_regular{font_size: 8.0}
mod.widgets.Button.draw_text.color = #f5f5f5
mod.widgets.Button.draw_text.color_hover = #ffffff
mod.widgets.Button.draw_text.color_down = #e6e6e6
mod.widgets.Button.draw_text.color_focus = #f5f5f5
mod.widgets.Button.draw_text.color_disabled = #cfcdca
mod.widgets.ButtonTertiary.draw_text.color = #1d1d1d
mod.widgets.ButtonTertiary.draw_text.color_hover = #111111
mod.widgets.ButtonTertiary.draw_text.color_down = #1d1d1d
mod.widgets.ButtonTertiary.draw_text.color_focus = #1d1d1d
mod.widgets.ButtonTertiary.draw_icon.color = #1d1d1d
mod.widgets.ButtonTertiary.draw_text.text_style = mod.theme.font_regular{font_size: 8.0}
mod.widgets.ButtonDanger.draw_text.color = #b42318
mod.widgets.ButtonDanger.draw_text.color_hover = #a51f15
mod.widgets.ButtonDanger.draw_text.color_down = #a51f15
mod.widgets.ButtonDanger.draw_text.color_focus = #b42318
mod.widgets.ButtonDanger.draw_icon.color = #b42318
mod.widgets.ButtonDanger.draw_text.text_style = mod.theme.font_regular{font_size: 8.0}
mod.widgets.ButtonOutline.draw_text.color = #1d1d1d
mod.widgets.ButtonOutline.draw_text.color_hover = #111111
mod.widgets.ButtonOutline.draw_text.color_down = #1d1d1d
mod.widgets.ButtonOutline.draw_text.color_focus = #1d1d1d
mod.widgets.ButtonOutline.draw_text.color_disabled = #a9a7a4
mod.widgets.ButtonOutline.draw_icon.color = #1d1d1d
mod.widgets.ButtonFlat.draw_bg.loading_color = #1d1d1d
// Icons: print ink on the white and grey keys (the root's icon kept the
// SVG's own white, invisible on a white key), white on the orange key and
// light on the drop; a trailing icon that differs from the root's is handed
// an object of its own.
mod.widgets.ButtonFlat.draw_icon.color = #1d1d1d
mod.widgets.ButtonFlat.draw_icon_end.color = #1d1d1d
mod.widgets.ButtonPrimary.draw_icon_end = mod.widgets.ButtonFlat.draw_icon_end{color: #ffffff}
mod.widgets.ButtonSecondary.draw_icon_end = mod.widgets.ButtonFlat.draw_icon_end{color: #1d1d1d}
mod.widgets.Button.draw_icon.color = #f5f5f5
mod.widgets.Button.draw_icon_end = mod.widgets.ButtonFlat.draw_icon_end{color: #f5f5f5}
// An icon an app inks dark on the drop (the kitchen sink gives its star the
// print ink) is drawn in the light ink, the one that reads on black; a
// light or coloured ink, and the drawing's own colours, stand.
mod.widgets.Button.draw_icon.get_color = fn() {
let base = self.eval_gradient()
if self.color.x >= 0.0 {
let dark = 1.0 - step(0.35, dot(self.color.rgb, vec3(0.3, 0.59, 0.11)))
let ink = mix(self.color.rgb, vec3(0.961, 0.961, 0.961), dark)
return vec4(ink * self.color.a * base.a, self.color.a * base.a) * self.opacity
}
return base * self.opacity
}
// The drops that do not take the Default's inks from it: the sizes and the
// compound button, handed its face above, and the date picker's opener,
// built from it with a label of its own. Each takes the light legend, the
// Default's icons and the field.
let fe_drop_inks = fn(w) {
w.draw_text.color = #f5f5f5
w.draw_text.color_hover = #ffffff
w.draw_text.color_down = #e6e6e6
w.draw_text.color_focus = #f5f5f5
w.draw_text.color_disabled = #cfcdca
w.draw_icon = mod.widgets.Button.draw_icon{}
w.draw_icon_end = mod.widgets.Button.draw_icon_end{}
w.cap_field_reach = 20.0
}
fe_drop_inks(mod.widgets.ButtonXs)
fe_drop_inks(mod.widgets.ButtonSm)
fe_drop_inks(mod.widgets.ButtonLg)
fe_drop_inks(mod.widgets.ButtonXl)
fe_drop_inks(mod.widgets.ButtonCompound)
fe_drop_inks(mod.widgets.DatePickerOpen)
// The compound button's second line, quieter, on the drop.
mod.widgets.ButtonCompound.draw_description.color = #a9a7a4
// The split button's chevron is drawn over its second half, a drop.
mod.widgets.SplitButton.draw_chevron.color = #f5f5f5
// The select's face is built from the Default but drawn as the drop-down's
// key (DROP-DOWNS): it keeps the key's padding and print ink and carries no
// field.
let fk_key_face = fn(w) {
w.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 3.0 bottom: 5.0}
w.draw_text.color = #1d1d1d
w.draw_text.color_hover = #111111
w.draw_text.color_down = #1d1d1d
w.draw_text.color_focus = #1d1d1d
w.draw_text.color_disabled = #a9a7a4
w.cap_field_reach = 0.0
}
fk_key_face(mod.widgets.Select.face)
fk_key_face(mod.widgets.MultiSelect.face)
// ---------------------------------------------------------------------------
// ENGRAVED PRINT. A label printed on the case carries one device pixel of
// light under each glyph, read from the glyph one pixel above: one more
// sample and nothing else. One literal per template.
mod.widgets.Label.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
var s = 0.0
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
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)
s = self.sdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
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)
s = self.msdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
let e = s * 0.6 * c.a * (1.0 - step(0.35, Finish.lum(c.rgb)))
return main + vec4(e, e, e, e) * (1.0 - main.a)
}
// The small heading (a card's title) is engraved like the labels; it was
// built from Label before this sheet ran, so it takes its own literal.
mod.widgets.H4.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
var s = 0.0
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
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)
s = self.sdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
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)
s = self.msdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
let e = s * 0.6 * c.a * (1.0 - step(0.45, Finish.lum(c.rgb)))
return main + vec4(e, e, e, e) * (1.0 - main.a)
}
mod.widgets.CheckBoxFlat.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
var s = 0.0
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
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)
s = self.sdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
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)
s = self.msdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
let e = s * 0.6 * c.a
return main + vec4(e, e, e, e) * (1.0 - main.a)
}
mod.widgets.RadioButtonFlat.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
var s = 0.0
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
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)
s = self.sdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
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)
s = self.msdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
let e = s * 0.6 * c.a
return main + vec4(e, e, e, e) * (1.0 - main.a)
}
mod.widgets.ToggleFlat.draw_text.pixel = fn() {
let main = self.sample_text_pixel()
let c = self.get_color()
let dxt = length(dFdx(self.t))
let dyt = length(dFdy(self.t))
var s = 0.0
if self.texture_index < 0.5 {
let tex_size = self.grayscale_texture.size()
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)
s = self.sdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
if self.texture_index > 1.5 && self.texture_index < 2.5 {
let tex_size = self.msdf_texture.size()
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)
s = self.msdf((dxt + dyt) * tex_size.x * 0.5, ps, c)
}
let e = s * 0.6 * c.a
return main + vec4(e, e, e, e) * (1.0 - main.a)
}
// ---------------------------------------------------------------------------
// CHECK BOXES. A 14 point key with a short skirt, three points in from the
// row so its focus ring has room. Set: a flat orange square on the face, an
// indicator rather than a tick; mixed: an orange bar. Error: a red rim.
mod.widgets.CheckBoxFlat.padding = mod.turtle.Inset{left: 0.0 right: 3.0 top: 3.0 bottom: 3.0}
mod.widgets.CheckBoxFlat.label_walk.margin = mod.turtle.Inset{left: 23.0 right: 3.0 top: 0.0 bottom: 0.0}
mod.widgets.CheckBoxFlat.draw_bg.size = uniform(14.0)
mod.widgets.CheckBoxFlat.min_height = 24.0
mod.widgets.CheckBoxFlat.align.y = 0.5
mod.widgets.CheckBoxFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.CheckBoxFlat.clip_x = false
mod.widgets.CheckBoxFlat.clip_y = false
mod.widgets.CheckBoxFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 4.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let sz = self.size
let x0 = mix(3.0, self.rect_size.x - sz - 3.0, self.mark_at_end)
let lo = vec2(Finish.snap(o.x + x0, px), Finish.snap(o.y + (self.rect_size.y - sz) * 0.5, px)) - o
let hi = lo + vec2(Finish.snap(sz, px), Finish.snap(sz, px))
let h = (hi - lo) * 0.5
let c = lo + h
// the round box keeps its round key
let r = mix(2.0, h.y, step(20.0, self.border_radius))
let d = fk_rbox(p, c, h, r)
let live = 1.0 - self.disabled
let press = self.down * live
var out = fk_shade(p, c, h, r, px, 1.0 - press, self.disabled, mix(0.13, 0.19, self.hover * live))
out = Finish.over(out, fk_cap(p, lo, hi, r, px, 2.0 * px, 0.0, self.hover * live, press, self.disabled, 0.06))
// the indicator: a 6 point square of orange, its rim a shade deeper
let fy = press * px - px
let mc = c + vec2(0.0, fy)
let sq = fk_rbox(p, mc, vec2(3.0, 3.0), mix(0.5, 3.0, step(20.0, self.border_radius)))
let bar = fk_rbox(p, mc, vec2(3.5, 1.0), 0.5)
let show = self.active * (1.0 - self.mixed)
let md = mix(mix(1000.0, sq, show), bar, self.mixed)
let lit = mix(vec3(0.933, 0.353, 0.141), vec3(0.83, 0.78, 0.76), self.disabled)
var mk = mix(lit, lit * 0.85, Finish.band(md, 0.0, px, px))
let ma = Finish.cover(md, px)
out = Finish.over(out, vec4(mk * ma, ma))
// error: the rim of the key turns red
let er = Finish.band(d, 0.0, px, px) * self.error
out = Finish.over(out, vec4(0.706 * er, 0.137 * er, 0.094 * er, er))
return Finish.over(out, fk_focus(d, px, self.focus * live))
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// RADIOS. A lamp in a collar: the collar a pale dish with a light line on
// its lower inner edge, the lens a flat grey dot off and flat LED orange
// on, its rim a shade deeper. Lit, never glowing.
mod.widgets.RadioButtonFlat.padding = mod.turtle.Inset{left: 0.0 right: 3.0 top: 3.0 bottom: 3.0}
mod.widgets.RadioButtonFlat.label_walk.margin = mod.turtle.Inset{left: 23.0 right: 3.0 top: 0.0 bottom: 0.0}
mod.widgets.RadioButtonFlat.draw_bg.size = uniform(14.0)
mod.widgets.RadioButtonFlat.min_height = 24.0
mod.widgets.RadioButtonFlat.align.y = 0.5
mod.widgets.RadioButtonFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let sz = self.size
let lo = vec2(Finish.snap(o.x + 3.0, px), Finish.snap(o.y + (self.rect_size.y - sz) * 0.5, px)) - o
let rad = Finish.snap(sz, px) * 0.5
let c = lo + vec2(rad, rad)
let live = 1.0 - self.disabled
let q = p - c
let d = length(q) - rad
// the collar: a shallow dish, shaded under its top, lit on its lower lip
let v = clamp(q.y / rad * 0.5 + 0.5, 0.0, 1.0)
var collar = mix(vec3(0.800, 0.800, 0.800), vec3(0.867, 0.867, 0.867), smoothstep(0.1, 0.7, v))
collar = collar + g3(0.03 * self.hover * live)
collar = mix(collar, vec3(0.90, 0.894, 0.886), self.disabled * 0.6)
let low = smoothstep(-0.2, 0.3, q.y / rad)
collar = mix(collar, vec3(1.0, 1.0, 1.0), Finish.band(d, px, 2.0 * px, px) * low * 0.9 * live)
collar = mix(collar, vec3(0.62, 0.62, 0.62), Finish.band(d, 0.0, px, px) * (1.0 - low) * 0.6)
let a = Finish.cover(d, px)
var out = fk_catch(p, c, vec2(rad, rad), rad, px, 0.6)
out = Finish.over(out, vec4(collar * a, a))
// the lens, 8 points
let lr = 4.0
let ld = length(q) - lr
let off = vec3(0.478, 0.475, 0.467)
let lit = vec3(0.992, 0.518, 0.196)
var lens = mix(off, lit, self.active)
// hovered, the lens itself brightens (off #8c8b89), not only its collar
lens = lens + g3(0.07 * self.hover * live * (1.0 - self.down))
lens = lens - g3(0.08 * self.down * live)
lens = mix(lens, vec3(0.80, 0.792, 0.780), self.disabled * 0.7)
lens = mix(lens, lens * 0.84, Finish.band(ld, 0.0, px, px))
let la = Finish.cover(ld, px)
out = Finish.over(out, vec4(lens * la, la))
return Finish.over(out, fk_focus(d, px, self.focus * live))
}
// Radio tabs: keys in a row; the chosen one is latched in, its legend in
// the deep signal orange.
mod.widgets.RadioButtonTabFlat.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 3.0 bottom: 5.0}
mod.widgets.RadioButtonTabFlat.label_walk.margin = mod.turtle.Inset{left: 0.0 right: 0.0 top: 0.0 bottom: 0.0}
mod.widgets.RadioButtonTabFlat.min_height = 24.0
mod.widgets.RadioButtonTabFlat.clip_x = false
mod.widgets.RadioButtonTabFlat.clip_y = false
mod.widgets.RadioButtonTabFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 5.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.RadioButtonTabFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 5.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + self.rect_size.x, px), Finish.snap(o.y + self.rect_size.y, px)) - o
let h = (hi - lo) * 0.5
let c = lo + h
let r = 2.0
let d = fk_rbox(p, c, h, r)
let live = 1.0 - self.disabled
let press = max(self.active, self.down) * live
var out = fk_shade(p, c, h, r, px, 1.0 - press, self.disabled, mix(0.13, 0.19, self.hover * live))
out = Finish.over(out, fk_cap(p, lo, hi, r, px, 3.0 * px, 0.0, self.hover * live, press, self.disabled, 0.0))
return Finish.over(out, fk_focus(d, px, self.focus * live))
}
mod.widgets.RadioButtonTabFlat.draw_text.color = #1d1d1d
mod.widgets.RadioButtonTabFlat.draw_text.color_active = #b53c0b
mod.widgets.RadioButtonTabFlat.draw_text.color_disabled = #a9a7a4
// ---------------------------------------------------------------------------
// TEXT FIELDS. A white well cut into the case: a grey hairline mouth, one
// pixel of shade under its top edge, the light catch on the last row of the
// box. Focus: the mouth turns signal orange with a second orange row inside
// it, drawn in the box so an inner field needs no room outside itself.
// The well fills its whole box; the light catch lies one device row BELOW
// it, outside, so a field whose quad is grown down (see `fk_field_vertex`)
// shows it and one that is not simply has none.
let fk_field = fn(p: vec2, size: vec2, o: vec2, px: float, hover: float, focus: float, dis: float, r: float) -> vec4 {
// the side rims land on whole pixels INSIDE the box: a rim rounded out
// past it is cut by the field's own clip
let lo = vec2(ceil(o.x / px - 0.001) * px, Finish.snap(o.y, px)) - o
let hi = vec2(floor((o.x + size.x) / px + 0.001) * px, Finish.snap(o.y + size.y, px)) - o
let h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox(p, c, h, r)
var col = mix(vec3(0.992, 0.992, 0.988), vec3(1.0, 1.0, 1.0), hover)
col = mix(col, vec3(0.945, 0.941, 0.933), dis)
// cut in: two rows of shade under the top rim, 16 % then 7 %
let yt = p.y - lo.y
let sh = fk_run(yt, px, 2.0 * px) * 0.16 + fk_run(yt, 2.0 * px, 3.0 * px) * 0.07
col = mix(col, vec3(0.0, 0.0, 0.0), sh * mix(1.0, 0.4, dis))
// the rim: the top edge dark (0.49), the sides and foot the pale edge
// #a3a2a1, so the field reads as a recess lit from above
let upper = 1.0 - smoothstep(lo.y + r, lo.y + r + px, p.y)
let top_rim = mix(vec3(0.49, 0.486, 0.478), vec3(0.40, 0.396, 0.388), hover)
let low_rim = mix(vec3(0.639, 0.635, 0.631), vec3(0.58, 0.576, 0.57), hover)
var rim = mix(low_rim, top_rim, max(upper, fk_run(yt, 0.0, px)))
rim = mix(rim, vec3(0.76, 0.755, 0.745), dis)
col = mix(col, rim, Finish.band(d, 0.0, px, px))
let orange = vec3(0.933, 0.353, 0.141)
col = mix(col, orange, Finish.band(d, 0.0, px, px) * focus)
col = mix(col, orange, Finish.band(d, px, 2.0 * px, px) * focus * 0.45)
let a = Finish.cover(d, px)
// the catch: one row of #fcfbf9 under the foot, outside the box
let cl = fk_run(p.y, hi.y, hi.y + px) * step(abs(p.x - c.x), h.x - r) * 0.92
let out = vec4(0.988 * cl, 0.984 * cl, 0.976 * cl, cl)
return Finish.over(out, vec4(col * a, a))
}
mod.widgets.TextInputFlat.padding = mod.turtle.Inset{left: 8.0 right: 8.0 top: 4.0 bottom: 4.0}
// The field's quad reaches 2 points below its box for the catch; the
// field stops clipping downward only (its text still clips at its sides).
mod.widgets.TextInputFlat.clip_y = false
mod.widgets.TextInputFlat.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size + vec2(0.0, 2.0))
}
mod.widgets.TextInputFlat.draw_bg.pixel = fn() {
// The value field inside a slider or a well draws no box of its own.
if self.color.a < 0.01 {
return vec4(0.0, 0.0, 0.0, 0.0)
}
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_field(self.pos * (self.rect_size + vec2(0.0, 2.0)), self.rect_size, self.rect_pos, px, self.hover, self.focus, self.disabled, 1.5)
}
mod.widgets.TextInputFlat.draw_text.color_empty = #72716f
mod.widgets.TextInputFlat.draw_text.color_disabled = #a9a7a4
mod.widgets.TextInputFlat.draw_cursor.color = #ee5a24
mod.widgets.NumberField.clip_y = false
mod.widgets.NumberField.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size + vec2(0.0, 2.0))
}
mod.widgets.NumberField.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_field(self.pos * (self.rect_size + vec2(0.0, 2.0)), self.rect_size, self.rect_pos, px, self.hover, self.focus, self.disabled, 1.5)
}
// The toolbar number: a small white well, the value's share a faint orange
// wash behind the digits, print ink.
mod.widgets.ValueInput.arrow_color = #5c5b59
mod.widgets.ValueInput.draw_text.color = #1d1d1d
mod.widgets.ValueInput.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
var out = fk_field(p, self.rect_size, self.rect_pos, px, max(self.hover, self.drag), self.focus, 0.0, 1.5)
if self.fill >= 0.0 {
let w = (self.rect_size.x - 2.0) * self.fill
let f = fk_run(p.x, 1.0, 1.0 + w) * fk_run(p.y, 2.0 * px, self.rect_size.y - 2.0 * px) * 0.14
out = Finish.over(out, vec4(0.933 * f, 0.353 * f, 0.141 * f, f))
}
return out
}
// The stepper: two small white keys stacked in the field's end, split by a
// seam; each half lights or presses on its own.
mod.widgets.NumberSpin.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o + vec2(px, px)
let hi = vec2(Finish.snap(o.x + self.rect_size.x, px), Finish.snap(o.y + self.rect_size.y, px)) - o - vec2(px, 2.0 * px)
let live = 1.0 - self.disabled
let mid = Finish.snap(o.y + (lo.y + hi.y) * 0.5, px) - o.y
let upper = step(p.y, mid)
let hot_u = max(0.0, self.hot) * live
let hot_d = max(0.0, -self.hot) * live
let dn_u = max(0.0, self.down) * live
let dn_d = max(0.0, -self.down) * live
// each half the main key in small: the 3 pixel skirt, the white top line
var out = Finish.over(vec4(0.0, 0.0, 0.0, 0.0), fk_cap(p, lo, vec2(hi.x, mid - px), 1.0, px, 3.0 * px, 0.0, hot_u, dn_u, self.disabled, 0.08) * upper)
out = Finish.over(out, fk_cap(p, vec2(lo.x, mid + px), hi, 1.0, px, 3.0 * px, 0.0, hot_d, dn_d, self.disabled, 0.08) * (1.0 - upper))
// the seam between them
let seam = fk_run(p.y, mid - px, mid + px) * step(lo.x, p.x) * step(p.x, hi.x) * 0.45
out = Finish.over(out, vec4(0.0, 0.0, 0.0, seam))
// the legends, plus above and minus below, 1.5 pixel strokes on the
// pixel grid, centred on each face (the face sits above its skirt and
// travels one pixel when pressed)
let cx = Finish.snap(o.x + (lo.x + hi.x) * 0.5, px) - o.x
let cu = Finish.snap(o.y + (lo.y + mid - px - 3.0 * px) * 0.5, px) - o.y + dn_u * px
let cd = Finish.snap(o.y + (mid + px + hi.y - 3.0 * px) * 0.5, px) - o.y + dn_d * px
let arm = 2.25
let sw = 0.75 * px
let hbar_u = step(abs(p.x - cx), arm) * step(abs(p.y - cu), sw)
let vbar_u = step(abs(p.y - cu), arm) * step(abs(p.x - cx), sw)
let hbar_d = step(abs(p.x - cx), arm) * step(abs(p.y - cd), sw)
let g = max(max(hbar_u, vbar_u) * upper, hbar_d * (1.0 - upper))
let gink = mix(vec3(0.114, 0.114, 0.114), vec3(0.663, 0.655, 0.643), self.disabled)
out = Finish.over(out, vec4(gink * g, g))
return out
}
// The stock triangles give way to the drawn plus and minus.
mod.widgets.NumberSpin.draw_text.color = #1d1d1d00
mod.widgets.NumberSpin.draw_text.color_disabled = #a9a7a400
// ---------------------------------------------------------------------------
// SLIDERS. A 3 point dark track with a light catch under it, the orange
// fill in it up to the handle, hairline ticks printed above at a 6 point
// pitch, and for the handle the faders' ferrofluid drop made small: a round
// drop of black liquid 12 points across and 5 tall, lying on the track. It
// moves as the faders' drop moves, dragged, pressed or sent to a value that
// jumps, and when the pointer comes near it raises its spikes on the same
// lattice, marched and lit by the same material: THE FERROFLUID and FADERS
// below hold the drop, the faces here only place it. Each face grows its
// quad for the drop's spikes and cast shadow; the knobs, built on the same
// slider, keep the 6 points they had and carry no drop.
//
// The drop is the widget's cap. Its centre runs where the old handle's ran,
// 5 points in from the box's ends on the minimal face and 6 on the flat and
// round ones, so the longer drop hangs past the ends of its travel: a
// negative inset. The drag divides by that travel, so the drop keeps to the
// finger. It moves at the faders' viscosity. The pointer's field, as on the
// faders: up while the pointer is within this much plus the drop's length
// of its centre, and only on the control nearest the pointer, two while it
// passes from one to the next (the widget library keeps that count).
mod.widgets.SliderMinimal.cap_size = 12.0
mod.widgets.SliderMinimal.track_inset = -2.0
mod.widgets.SliderMinimal.cap_viscosity = 0.5
mod.widgets.SliderMinimal.cap_field_reach = 16.0
mod.widgets.SliderFlat.cap_size = 12.0
mod.widgets.SliderFlat.track_inset = -1.0
mod.widgets.SliderFlat.cap_viscosity = 0.5
mod.widgets.SliderFlat.cap_field_reach = 16.0
mod.widgets.SliderRoundFlat.cap_size = 12.0
mod.widgets.SliderRoundFlat.track_inset = -1.0
mod.widgets.SliderRoundFlat.cap_viscosity = 0.5
mod.widgets.SliderRoundFlat.cap_field_reach = 16.0
// The knobs are built on the minimal slider and draw no fader's drop: they
// take back no field and the 6 points of room every face had. They keep
// the motion, for the drop in their glass: a nominal cap of 40 points, whose
// stretch runs from 0 at rest to 1 as the value's spring trails it by 1.69
// of its radius (33.8 points), so it reads how fast the knob turns; a
// viscosity of 0.9 settles it in about half a second; the inset of -20
// keeps the drag's travel the whole box.
mod.widgets.RotaryFlat.cap_size = 40.0
mod.widgets.RotaryFlat.track_inset = -20.0
mod.widgets.RotaryFlat.cap_viscosity = 0.9
mod.widgets.RotaryFlat.cap_field_reach = 0.0
mod.widgets.RotaryKnob.cap_size = 40.0
mod.widgets.RotaryKnob.track_inset = -20.0
mod.widgets.RotaryKnob.cap_viscosity = 0.9
mod.widgets.RotaryKnob.cap_field_reach = 0.0
mod.widgets.RotaryFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.RotaryKnob.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.SliderMinimal.clip_x = false
mod.widgets.SliderMinimal.clip_y = false
// The drop's material and its field's effect, on each face so each tunes
// live on its own: the faders' numbers (FADERS says what each one does),
// the lengths the bench's proportions on this 12 point drop (the height 5,
// the reach 20, the shadow's blur 4), the slide share 12 of the bench's 55
// point view. The spikes keep the fader's size in points, not the drop's
// share of it: the fader's spacing of 3.4 and its base, and nine tenths of
// its length (17.25 points: 1.5 of this drop's heights and 9.75 points,
// where the fader's 1.5 and 6.67 make 19.2 on its 8.33), so this smaller
// drop, less liquid with less room, raises fewer of them. The inks:
// the case; the drop's near black, as ferrofluid is, and the sheet's
// disabled key when disabled, under names of their own because the stock
// slider restates its handle's colours from the theme; the material's
// white light and the bench's grey shade.
mod.widgets.SliderMinimal.draw_bg.cap_light = uniform(vec3(-0.76, -0.6, 0.36))
mod.widgets.SliderMinimal.draw_bg.cap_light_intensity = uniform(1.0)
mod.widgets.SliderMinimal.draw_bg.cap_specular = uniform(0.9)
mod.widgets.SliderMinimal.draw_bg.cap_roughness = uniform(0.08)
mod.widgets.SliderMinimal.draw_bg.cap_gloss = uniform(0.4)
mod.widgets.SliderMinimal.draw_bg.cap_rim = uniform(0.5)
mod.widgets.SliderMinimal.draw_bg.cap_ao = uniform(0.55)
mod.widgets.SliderMinimal.draw_bg.cap_ao_reach = uniform(1.0)
mod.widgets.SliderMinimal.draw_bg.cap_reflection = uniform(0.0)
mod.widgets.SliderMinimal.draw_bg.cap_reflection_perspective = uniform(0.35)
mod.widgets.SliderMinimal.draw_bg.cap_metal = uniform(0.2)
mod.widgets.SliderMinimal.draw_bg.cap_spun = uniform(0.0)
mod.widgets.SliderMinimal.draw_bg.cap_glow = uniform(0.0)
mod.widgets.SliderMinimal.draw_bg.cap_exposure = uniform(0.0)
mod.widgets.SliderMinimal.draw_bg.cap_rolloff = uniform(0.6)
mod.widgets.SliderMinimal.draw_bg.cap_shadow = uniform(0.5)
mod.widgets.SliderMinimal.draw_bg.cap_shadow_blur = uniform(4.00)
mod.widgets.SliderMinimal.draw_bg.cap_contact = uniform(0.45)
mod.widgets.SliderMinimal.draw_bg.cap_height = uniform(5.00)
mod.widgets.SliderMinimal.draw_bg.cap_perspective = uniform(0.44)
mod.widgets.SliderMinimal.draw_bg.cap_effect_spacing = uniform(3.4)
mod.widgets.SliderMinimal.draw_bg.cap_effect_height = uniform(1.5)
mod.widgets.SliderMinimal.draw_bg.cap_effect_height_abs = uniform(9.75)
mod.widgets.SliderMinimal.draw_bg.cap_effect_shrink = uniform(0.39)
mod.widgets.SliderMinimal.draw_bg.cap_effect_reach = uniform(20.0)
mod.widgets.SliderMinimal.draw_bg.cap_effect_outline = uniform(0.45)
mod.widgets.SliderMinimal.draw_bg.cap_effect_fan = uniform(0.85)
mod.widgets.SliderMinimal.draw_bg.cap_effect_base = uniform(0.43)
mod.widgets.SliderMinimal.draw_bg.cap_effect_light = uniform(0.0)
mod.widgets.SliderMinimal.draw_bg.cap_effect_normal = uniform(0.0)
mod.widgets.SliderMinimal.draw_bg.cap_effect_flow = uniform(0.0)
mod.widgets.SliderMinimal.draw_bg.cap_effect_slide = uniform(0.22)
mod.widgets.SliderMinimal.draw_bg.ground_color = uniform(theme.color_bg_app)
mod.widgets.SliderMinimal.draw_bg.drop_color = uniform(#07070b)
mod.widgets.SliderMinimal.draw_bg.drop_color_disabled = uniform(theme.color_outset_disabled)
mod.widgets.SliderMinimal.draw_bg.light_color = uniform(theme.color_material_light)
mod.widgets.SliderMinimal.draw_bg.shadow_color = uniform(#26262c)
mod.widgets.SliderFlat.draw_bg.cap_light = uniform(vec3(-0.76, -0.6, 0.36))
mod.widgets.SliderFlat.draw_bg.cap_light_intensity = uniform(1.0)
mod.widgets.SliderFlat.draw_bg.cap_specular = uniform(0.9)
mod.widgets.SliderFlat.draw_bg.cap_roughness = uniform(0.08)
mod.widgets.SliderFlat.draw_bg.cap_gloss = uniform(0.4)
mod.widgets.SliderFlat.draw_bg.cap_rim = uniform(0.5)
mod.widgets.SliderFlat.draw_bg.cap_ao = uniform(0.55)
mod.widgets.SliderFlat.draw_bg.cap_ao_reach = uniform(1.0)
mod.widgets.SliderFlat.draw_bg.cap_reflection = uniform(0.0)
mod.widgets.SliderFlat.draw_bg.cap_reflection_perspective = uniform(0.35)
mod.widgets.SliderFlat.draw_bg.cap_metal = uniform(0.2)
mod.widgets.SliderFlat.draw_bg.cap_spun = uniform(0.0)
mod.widgets.SliderFlat.draw_bg.cap_glow = uniform(0.0)
mod.widgets.SliderFlat.draw_bg.cap_exposure = uniform(0.0)
mod.widgets.SliderFlat.draw_bg.cap_rolloff = uniform(0.6)
mod.widgets.SliderFlat.draw_bg.cap_shadow = uniform(0.5)
mod.widgets.SliderFlat.draw_bg.cap_shadow_blur = uniform(4.00)
mod.widgets.SliderFlat.draw_bg.cap_contact = uniform(0.45)
mod.widgets.SliderFlat.draw_bg.cap_height = uniform(5.00)
mod.widgets.SliderFlat.draw_bg.cap_perspective = uniform(0.44)
mod.widgets.SliderFlat.draw_bg.cap_effect_spacing = uniform(3.4)
mod.widgets.SliderFlat.draw_bg.cap_effect_height = uniform(1.5)
mod.widgets.SliderFlat.draw_bg.cap_effect_height_abs = uniform(9.75)
mod.widgets.SliderFlat.draw_bg.cap_effect_shrink = uniform(0.39)
mod.widgets.SliderFlat.draw_bg.cap_effect_reach = uniform(20.0)
mod.widgets.SliderFlat.draw_bg.cap_effect_outline = uniform(0.45)
mod.widgets.SliderFlat.draw_bg.cap_effect_fan = uniform(0.85)
mod.widgets.SliderFlat.draw_bg.cap_effect_base = uniform(0.43)
mod.widgets.SliderFlat.draw_bg.cap_effect_light = uniform(0.0)
mod.widgets.SliderFlat.draw_bg.cap_effect_normal = uniform(0.0)
mod.widgets.SliderFlat.draw_bg.cap_effect_flow = uniform(0.0)
mod.widgets.SliderFlat.draw_bg.cap_effect_slide = uniform(0.22)
mod.widgets.SliderFlat.draw_bg.ground_color = uniform(theme.color_bg_app)
mod.widgets.SliderFlat.draw_bg.drop_color = uniform(#07070b)
mod.widgets.SliderFlat.draw_bg.drop_color_disabled = uniform(theme.color_outset_disabled)
mod.widgets.SliderFlat.draw_bg.light_color = uniform(theme.color_material_light)
mod.widgets.SliderFlat.draw_bg.shadow_color = uniform(#26262c)
mod.widgets.SliderRoundFlat.draw_bg.cap_light = uniform(vec3(-0.76, -0.6, 0.36))
mod.widgets.SliderRoundFlat.draw_bg.cap_light_intensity = uniform(1.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_specular = uniform(0.9)
mod.widgets.SliderRoundFlat.draw_bg.cap_roughness = uniform(0.08)
mod.widgets.SliderRoundFlat.draw_bg.cap_gloss = uniform(0.4)
mod.widgets.SliderRoundFlat.draw_bg.cap_rim = uniform(0.5)
mod.widgets.SliderRoundFlat.draw_bg.cap_ao = uniform(0.55)
mod.widgets.SliderRoundFlat.draw_bg.cap_ao_reach = uniform(1.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_reflection = uniform(0.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_reflection_perspective = uniform(0.35)
mod.widgets.SliderRoundFlat.draw_bg.cap_metal = uniform(0.2)
mod.widgets.SliderRoundFlat.draw_bg.cap_spun = uniform(0.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_glow = uniform(0.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_exposure = uniform(0.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_rolloff = uniform(0.6)
mod.widgets.SliderRoundFlat.draw_bg.cap_shadow = uniform(0.5)
mod.widgets.SliderRoundFlat.draw_bg.cap_shadow_blur = uniform(4.00)
mod.widgets.SliderRoundFlat.draw_bg.cap_contact = uniform(0.45)
mod.widgets.SliderRoundFlat.draw_bg.cap_height = uniform(5.00)
mod.widgets.SliderRoundFlat.draw_bg.cap_perspective = uniform(0.44)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_spacing = uniform(3.4)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_height = uniform(1.5)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_height_abs = uniform(9.75)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_shrink = uniform(0.39)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_reach = uniform(20.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_outline = uniform(0.45)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_fan = uniform(0.85)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_base = uniform(0.43)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_light = uniform(0.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_normal = uniform(0.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_flow = uniform(0.0)
mod.widgets.SliderRoundFlat.draw_bg.cap_effect_slide = uniform(0.22)
mod.widgets.SliderRoundFlat.draw_bg.ground_color = uniform(theme.color_bg_app)
mod.widgets.SliderRoundFlat.draw_bg.drop_color = uniform(#07070b)
mod.widgets.SliderRoundFlat.draw_bg.drop_color_disabled = uniform(theme.color_outset_disabled)
mod.widgets.SliderRoundFlat.draw_bg.light_color = uniform(theme.color_material_light)
mod.widgets.SliderRoundFlat.draw_bg.shadow_color = uniform(#26262c)
// How far a face's quad reaches past its box: the drop's reach (its spikes
// or its cast shadow, whichever is farther, as on the faders) less how near
// its centre comes to the box's edge, along (where its travel ends) or
// across (`edge`, the face's own), and never under the 6 points every face
// took before; a face sized to no cap keeps those.
// The room a horizontal face's drop needs past the box, as a plain
// function so vertex and pixel work it out alike: the drop's reach from its
// centre less the room the box already holds (`edge`), never under 6.
let fe_face_room = fn(cap: float, inset: float, lgt: vec3, height: float, blur: float, glo: float, spike: float, outline: float, edge: float) -> float {
if cap < 0.5 {
return 6.0
}
let reach = fe_cap_reach(cap, lgt, height, blur, glo, spike, outline)
return max(reach - min(edge, inset + cap * 0.5), 6.0)
}
mod.widgets.SliderMinimal.draw_bg.ferro_room = fn(edge: float) -> float {
if self.cap_px < 0.5 {
return 6.0
}
let reach = fe_cap_reach(self.cap_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline)
return max(reach - min(edge, self.inset_px + self.cap_px * 0.5), 6.0)
}
// THE DROP on a slider face, laid over the face's own layers: the faders'
// drop, its dome, its spikes and its material, in the face's frame (x along
// the track, y down the screen, in points) about the drop's centre `c`. The
// widget reads the pointer from the drop's centre along the track but from
// the box's middle across it, so the across reading is moved to the drop's
// own middle. The faders paint their ground and lay the whole over the case;
// a face's track is drawn already, so the cast shadow multiplies what lies
// under it by the shadow ink, as the faders' does (exact over the case), and
// the drop goes over that. `ring` 1 puts the focus ring round the drop, 0
// leaves the focus to the face.
mod.widgets.SliderMinimal.draw_bg.ferro_drop = fn(layers: vec4, p: vec2, c: vec2, px: float, ring: float) -> vec4 {
if self.cap_px < 0.5 {
return layers
}
let live = 1.0 - self.disabled
let l = normalize(self.cap_light)
let lt = Cap.light(l, 0.0)
let li = self.cap_light_intensity
let ground = self.ground_color.rgb
let light_ink = self.light_color.rgb
let shadow_ink = self.shadow_color.rgb
let body = self.drop_color.rgb
// held, the liquid's press spring spreads the drop 8% and settles it 14%
// lower; hovered or focused, it lifts as every key here does, and loses
// the lift while it is held
let press = self.cap_press * live
let lifted = max(self.hover, self.focus) * live * (1.0 - clamp(self.drag, 0.0, 1.0))
let r = self.cap_px * 0.5
let hs = vec2(r, r)
// the field (none while disabled), the pointer from the drop's centre,
// and how much of the field reaches that centre: so much the body
// shrinks into its spikes. The spikes take over from the plain drop over
// the first 0.15 of the field.
let fld = clamp(self.cap_field, 0.0, 1.0) * live
let mag = vec2(self.cap_pointer_along, self.cap_pointer_across + self.rect_size.y * 0.5 - c.y)
let fr = max(self.cap_effect_reach, 1.0)
let shrink = 1.0 - self.cap_effect_shrink * fld * max(1.0 - length(mag) / fr, 0.0)
let fw = smoothstep(0.0, 0.15, fld)
let spike = (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5))
let on = step(0.001, fw) * step(0.001, spike)
let height = self.cap_height * (1.0 - 0.14 * press)
let persp = self.cap_perspective
let q = p - c
var res = layers
// only near the drop: the rest of the track skips it
if length(q) < fe_cap_reach(self.cap_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, spike, self.cap_effect_outline) {
// THE SPIKES, marched once per pixel while the field is up: the body
// the drop's radius (spread by the press) and height, both shrunk by
// the field; the lattice anchored to the pointer in the face's frame
// (the pointer's offset from the drop and the drop's own travel both
// at the slide share, so a drag streams the spikes no faster than a
// hover slides them), drifting on the field's clock; the cones
// radiating from a centre on the drop's axis, at its middle for a
// fan of 2.
let size_m = vec2(r * (1.0 + 0.08 * press) * shrink, height * shrink)
var mk = vec4(1000.0, 0.0, 0.0, 0.0)
if on > 0.5 {
let sp = max(self.cap_effect_spacing, 1.0)
let tt = self.cap_time
let drift = vec2(0.35 * sin(tt * 0.6), 0.35 * cos(tt * 0.45)) * sp
let back = vec2(c.x * self.cap_effect_slide, c.y) + mag * self.cap_effect_slide - drift
let centre = vec3(0.0, 0.0, -size_m.y * (2.0 / max(self.cap_effect_fan, 0.25) - 1.0))
mk = fe_march(q, size_m, mag, vec4(fld, fr, spike, self.cap_effect_base), vec4(back.x, back.y, sp, tt), centre, persp, self.cap_effect_normal, px)
}
// THE DROP: its dome read three times, each at the point the height
// read before brings toward the eye, stretched, pressed and squashed
// by the widget's motion
// a blob stays a circle while it moves unless the face lets the
// motion flow it (the toggle): then the head leads and the tail follows
let flow = self.cap_effect_flow
let motion = vec4(self.cap_stretch * flow, self.cap_dir, mix(1.0, self.cap_squash_along, flow), mix(1.0, self.cap_squash_across, flow))
let rs = r * shrink
var f = fe_dome(Cap.perspective(q, 1.0, -1.0, persp), hs, rs, motion, press, height, shrink)
f = fe_dome(Cap.perspective(q, f.x, f.w, persp), hs, rs, motion, press, height, shrink)
f = fe_dome(Cap.perspective(q, f.x, f.w, persp), hs, rs, motion, press, height, shrink)
var hgt = f.x
var grad = f.yz
var d = f.w
var tip = 0.0
let hit = on * step(0.5, mk.w)
if on > 0.5 {
if hit > 0.5 {
// on the marched solid: its height and slope, inside, and how
// far up its spike the point stands
hgt = mix(hgt, mk.x, fw)
grad = mix(grad, vec2(mk.y, mk.z), fw)
d = mix(d, 1.0 - mk.w, fw)
tip = clamp((mk.x - 1.0) / max(spike, 0.05), 0.0, 1.0) * fw
} else {
// beside it: the drop gives way to the march's silhouette
hgt = mix(hgt, 0.0, fw)
grad = mix(grad, vec2(0.0, 0.0), fw)
d = mix(d, max(d, mk.x), fw)
}
}
// the drop lit by its material on the dome's normal...
let n = Cap.normal(grad, height, 0.0)
let v = Cap.view(q, hs, self.cap_reflection_perspective, 0.0)
var cap = Cap.shade(n, l, v, hgt, body, 0.0, 0.0, 1.0, self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim, self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_metal, li, light_ink, light_ink, shadow_ink, ground)
let fq = fld * max(1.0 - length(q - mag) / fr, 0.0)
if hit > 0.5 {
// ...and the marched solid by the same material on its own normal
// and the ray the march took from the eye, with the fluid's own
// light over it
let nm = Cap.normal(vec2(mk.y, mk.z), size_m.y, 0.0)
let vm = normalize(vec3(-q.x, -q.y, size_m.y / max(persp, 0.04) * 1.5))
var fc = Cap.shade(nm, l, vm, hgt, body, 0.0, 0.0, 1.0, self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim, self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_metal, li, light_ink, light_ink, shadow_ink, ground)
if abs(self.cap_effect_light) > 0.001 {
fc = fe_comp(fc, nm, vm, l, fq, tip, self.cap_effect_light, light_ink, li)
}
cap = mix(cap, fc, fw)
} else {
if spike > 0.001 && fld > 0.001 {
// the field darkens what the march did not reach
cap = cap * (1.0 - 0.4 * fq)
}
}
// turned grain and glow: off on this drop, still tunable; lifted, a
// shade lighter, as on every key here; disabled, most of the way to
// the case
let grain = Cap.spun(q, hs, lt.xy) * self.cap_spun
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, hgt, body, light_ink, self.cap_glow)
cap = cap + g3(0.045 * lifted)
cap = mix(cap, self.drop_color_disabled.rgb, self.disabled * 0.6)
let cov = Cap.cover(d, px)
// UNDER IT: the cast shadow, thrown from the top outline the bench
// gives this drop, along the trailing tail while it trails, pushed
// out by half the spikes' outline push as the field rises, darker
// while the drop is lifted and gone when disabled; and the contact
// shadow hugging whatever outline is drawn, spikes and all.
let rise = height
let push = 0.5 * spike * self.cap_effect_outline * self.cap_height * fld
let fb = Cap.sd_drop(q, rs, self.cap_stretch * flow, vec2(self.cap_dir, 0.0), press, mix(1.0, self.cap_squash_along, flow), mix(1.0, self.cap_squash_across, flow))
let tail = vec2(-self.cap_dir * r * 0.96 * self.cap_stretch * flow * step(0.001, self.cap_stretch), 0.0)
let thrown = fe_shadow(q, fb, r, vec3(0.6902, 0.6902, 1.0), tail, lt, rise, self.cap_shadow_blur, push, self.cap_shadow * mix(1.0, 1.375, lifted) * live, px)
let touch = Cap.contact(d, max(rise * 0.3, 1.0), self.cap_contact * mix(1.0, 0.4, self.disabled))
let under = clamp(thrown + touch, 0.0, 1.0) * (1.0 - cov)
// the shadow multiplies the face's layers by the ink, and the case
// under them by a darkening share with the ink's tint
let ks = (vec3(1.0, 1.0, 1.0) - shadow_ink) * under
let ka = max(ks.x, max(ks.y, ks.z))
res = vec4(res.rgb * (vec3(1.0, 1.0, 1.0) - ks) + ground * (1.0 - res.a) * (vec3(ka, ka, ka) - ks), 1.0 - (1.0 - res.a) * (1.0 - ka))
// a glow's light round the drop, the drop toned as on the faders, and
// the focus ring round it, giving way to the spikes
res = vec4(res.rgb + body * Cap.glow(d, hs, self.cap_glow) * 0.9 * (1.0 - cov), res.a)
let ink = Cap.tone(cap, pow(2.0, self.cap_exposure), self.cap_rolloff)
res = Finish.over(res, vec4(ink * cov, cov))
res = Finish.over(res, fk_focus(d, px, self.focus * live * (1.0 - fw) * ring))
}
return res
}
// The dark track `th` tall from lo, `w` long, filled orange from `f0` to
// `fx` (a fill from the origin runs either way).
let fk_track = fn(p: vec2, lo: vec2, w: float, th: float, px: float, f0: float, fx: float, dis: float) -> vec4 {
let h = vec2(w * 0.5, th * 0.5)
let c = lo + h
let r = h.y
let d = fk_rbox(p, c, h, r)
let filled = step(p.x, max(f0, fx)) * step(min(f0, fx), p.x)
var col = mix(vec3(0.173, 0.173, 0.173), vec3(0.933, 0.353, 0.141), filled)
// the fill's top line, one pixel
col = mix(col, vec3(0.984, 0.643, 0.365), filled * fk_run(p.y, lo.y, lo.y + px) * 0.6)
col = mix(col, vec3(0.80, 0.792, 0.780), dis * 0.7)
let a = Finish.cover(d, px)
let out = fk_catch(p, c, h, r, px, 0.8)
return Finish.over(out, vec4(col * a, a))
}
mod.widgets.SliderFlat.height = 38.0
// The flat face's drop stands on its track, whose middle is 10 points above
// the box's foot.
mod.widgets.SliderFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, fe_face_room(self.cap_px, self.inset_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline, 10.0))
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.SliderFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = fe_face_room(self.cap_px, self.inset_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline, 10.0)
let size = self.rect_size
let p = self.pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
// the fill and the scale keep the old handle's 5 point half width
let hw = 5.0
let cy = size.y - 10.0
let th = 3.0
let top = Finish.snap(o.y + cy - th * 0.5, px) - o.y
let x0 = Finish.snap(o.x + 1.0, px) - o.x
let w = Finish.snap(o.x + size.x - 1.0, px) - o.x - x0
let f0 = mix(x0 - 1.0, x0 + hw + self.fill_lo * (w - 2.0 * hw), step(0.001, self.fill_lo))
let f1 = x0 + hw + self.fill_hi * (w - 2.0 * hw)
var out = fk_track(p, vec2(x0, top), w, th, px, f0, f1, self.disabled)
// printed ticks above the track: a 6 point pitch, a longer one every
// fifth, each one device-pixel column at half ink
let span = w - 2.0 * hw
let nt = max(floor(span / 6.0 + 0.5), 1.0)
let pitch = span / nt
let fi = floor((p.x - x0 - hw) / pitch + 0.5)
let tx = Finish.snap(o.x + x0 + hw + fi * pitch, px) - o.x
let major = 1.0 - step(0.3, abs(fract(fi / 5.0 + 0.5) - 0.5))
let tl = mix(3.0, 4.5, major)
let ty = Finish.snap(o.y + top - 2.0, px) - o.y
let ty0 = Finish.snap(o.y + top - 2.0 - tl, px) - o.y
let in_x = step(-0.5, fi) * step(fi, nt + 0.5)
let tick = fk_run(p.x, tx, tx + px) * fk_run(p.y, ty0, ty) * in_x
let ta = tick * mix(0.36, 0.5, major) * mix(1.0, 0.45, self.disabled)
out = Finish.over(out, vec4(0.114 * ta, 0.114 * ta, 0.114 * ta, ta))
// the drop on the track's middle, its centre where the widget lays the
// cap
let at = self.inset_px + self.cap_px * 0.5 + self.slide_pos * max(size.x - 2.0 * self.inset_px - self.cap_px, 1.0)
return self.ferro_drop(out, p, vec2(at, top + th * 0.5), px, 1.0)
}
mod.widgets.SliderFlat.text_input.draw_text.text_style = mod.theme.font_code{font_size: 7.25}
mod.widgets.SliderFlat.text_input.draw_text.color = #4a4947
mod.widgets.SliderFlat.text_input.draw_text.color_hover = #1d1d1d
mod.widgets.SliderFlat.text_input.draw_text.color_focus = #1d1d1d
// The minimal slider is the stepped one: a printed hairline with a dot at
// every step, lit orange up to the value, and the drop lying on the line, 6
// points above the box's foot.
mod.widgets.SliderMinimal.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, fe_face_room(self.cap_px, self.inset_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline, 6.0))
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.SliderMinimal.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = fe_face_room(self.cap_px, self.inset_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline, 6.0)
let size = self.rect_size
let p = self.pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let tr = 5.0
let cy = Finish.snap(o.y + size.y - 6.0, px) - o.y
let span = size.x - 2.0 * tr
let inx = step(0.0, p.x) * step(p.x, size.x)
// the printed line: one dark pixel over one light
let dk = fk_run(p.y, cy - px, cy) * inx * 0.30
let lt = fk_run(p.y, cy, cy + px) * inx * 0.6
var out = vec4(lt, lt, lt, lt)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, dk))
// the step dots, 2 points, every 8 points or so
let nt = max(floor(span / 8.0 + 0.5), 1.0)
let pitch = span / nt
let fi = clamp(floor((p.x - tr) / pitch + 0.5), 0.0, nt)
let dc = vec2(tr + fi * pitch, cy - 0.5 * px)
let dd = length(p - dc) - 1.0
let lit = step(fi * pitch, self.slide_pos * span + 0.01)
var dcol = mix(vec3(0.639, 0.635, 0.631), vec3(0.933, 0.353, 0.141), lit)
dcol = mix(dcol, vec3(0.80, 0.792, 0.780), self.disabled * 0.7)
let da = Finish.cover(dd, px)
out = Finish.over(out, vec4(dcol * da, da))
// the drop on the line, its centre where the widget lays the cap
let at = self.inset_px + self.cap_px * 0.5 + self.slide_pos * max(size.x - 2.0 * self.inset_px - self.cap_px, 1.0)
return self.ferro_drop(out, p, vec2(at, cy - 0.5 * px), px, 1.0)
}
mod.widgets.SliderMinimal.text_input.draw_text.text_style = mod.theme.font_code{font_size: 7.25}
// The round slider is the slot slider: a 12 point dark slot beside its
// label, a groove along its middle lit orange up to the drop, and the drop
// lying in the slot, a point proud of it above and below. The focus ring
// keeps to the slot.
mod.widgets.SliderRoundFlat.height = 14.0
mod.widgets.SliderRoundFlat.text_input.margin = mod.turtle.Inset{left: 0.0 right: 8.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.SliderRoundFlat.text_input.draw_text.text_style = mod.theme.font_code{font_size: 7.25}
mod.widgets.SliderRoundFlat.text_input.draw_text.color = #d9d9d9
mod.widgets.SliderRoundFlat.text_input.draw_text.color_hover = #f0f0f0
mod.widgets.SliderRoundFlat.text_input.draw_text.color_focus = #f0f0f0
mod.widgets.SliderRoundFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, fe_face_room(self.cap_px, self.inset_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline, self.rect_size.y * 0.5))
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.SliderRoundFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = fe_face_room(self.cap_px, self.inset_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline, self.rect_size.y * 0.5)
let size = self.rect_size
let p = self.pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let live = 1.0 - self.disabled
let x0 = Finish.snap(o.x + self.label_size, px) - o.x
let y0 = Finish.snap(o.y + 1.0, px) - o.y
let hi = vec2(Finish.snap(o.x + size.x, px) - o.x, Finish.snap(o.y + size.y - 1.0, px) - o.y)
let h = (hi - vec2(x0, y0)) * 0.5
let c = vec2(x0, y0) + h
let r = h.y
let d = fk_rbox(p, c, h, r)
var slot = mix(vec3(0.118, 0.118, 0.118), vec3(0.62, 0.615, 0.605), self.disabled)
slot = slot + g3(0.03 * self.hover * live)
var col = fk_well(p, c, h, r, px, slot, 0.40, vec3(0.0, 0.0, 0.0), 0.5)
// the lower lip inside, a little lighter
col = col + g3(0.04 * fk_run(p.y, hi.y - 2.0 * px, hi.y - px) * step(abs(p.x - c.x), h.x - r))
// the travel, and the groove lit up to the drop
let kx = x0 + r + self.slide_pos * (2.0 * h.x - 2.0 * r)
let gy = Finish.snap(o.y + c.y, px) - o.y
let groove = fk_run(p.y, gy - px, gy) * step(x0 + r * 0.6, p.x) * step(p.x, hi.x - r * 0.6)
col = mix(col, vec3(0.0, 0.0, 0.0), groove * 0.6)
let lit = fk_run(p.y, gy - px, gy + px) * step(x0 + r * 0.6, p.x) * step(p.x, kx)
col = mix(col, mix(vec3(0.933, 0.353, 0.141), vec3(0.72, 0.71, 0.70), self.disabled), lit)
let a = Finish.cover(d, px)
var out = fk_catch(p, c, h, r, px, 0.8)
out = Finish.over(out, vec4(col * a, a))
// the drop in the slot, its centre where the widget lays the cap,
// counted from the slot's end beside the label
let at = x0 + self.inset_px + self.cap_px * 0.5 + self.slide_pos * max(hi.x - x0 - 2.0 * self.inset_px - self.cap_px, 1.0)
out = self.ferro_drop(out, p, vec2(at, gy - 0.5 * px), px, 0.0)
return Finish.over(out, fk_focus(d, px, self.focus * live))
}
// ---------------------------------------------------------------------------
// TOGGLES. The gooey toggle, after the pen he chose, in black shiny goo: a
// track in the shape of a pinched peanut (two round lobes and a waist 0.58
// of their height, 292 by 142 as the pen's), sunk into the case in the
// knobs' material (THE WELL AND THE LENS), its floor a light grey when off
// and the sliders' orange when on, faded in as the toggle turns; a bar
// printed in its left lobe and a ring in its right, the goo hiding the one
// in the lobe it lies in. The goo is the knobs' black ferrofluid: the pen's
// three shapes melted into one body, a pill that travels from one lobe to
// the other and a round in each lobe, the one it leaves drawn in and the
// one it comes to swelled out, so the blob stretches out of one lobe
// through the waist, its neck thinning, and gathers in the other; turned
// back it runs the other way. No cap, no spikes. The pill stands 3 points
// in from the box's start (or its end, with the label first).
mod.widgets.ToggleFlat.label_walk.margin = mod.turtle.Inset{left: 42.0 right: 3.0 top: 0.0 bottom: 0.0}
mod.widgets.ToggleFlat.draw_bg.size = uniform(16.0)
mod.widgets.ToggleFlat.draw_bg.pill_aspect = uniform(2.06)
mod.widgets.ToggleFlat.draw_bg.knob_inset = uniform(0.0)
mod.widgets.ToggleFlat.draw_bg.border_size = uniform(0.0)
mod.widgets.ToggleFlat.min_height = 26.0
mod.widgets.ToggleFlat.align.y = 0.5
mod.widgets.ToggleFlat.clip_x = false
mod.widgets.ToggleFlat.clip_y = false
mod.widgets.ToggleFlat.cap_viscosity = 0.0
mod.widgets.ToggleFlat.cap_field_reach = 0.0
// The turn takes the pen's 0.6 seconds, evenly, the face easing each part
// of it (`fe_goo_toggle`); the knob's drag moves it as it goes. The Toggle,
// the rocker and the slide inherit it; the rocker and the slide put the
// stock flip back below.
mod.widgets.ToggleFlat.animator.active.on.ease = Linear
mod.widgets.ToggleFlat.animator.active.on.from = {all: Forward {duration: 0.6}}
mod.widgets.ToggleFlat.animator.active.off.ease = Linear
mod.widgets.ToggleFlat.animator.active.off.from = {all: Forward {duration: 0.6}}
// The peanut, `th` tall, its lobes' middles at `cl` and `cr`: two rounds
// melted so the waist between them is 0.58 of their height.
let fe_peanut = fn(p: vec2, cl: vec2, cr: vec2, th: float) -> float {
return fe_smin(length(p - cl) - th * 0.5, length(p - cr) - th * 0.5, th * 0.41)
}
// The goo at `p` as the toggle stands at `x` (0 off, 1 on, the pen's ease
// on it): the pen's pill, 0.82 by 0.41 of the height, at the lobe it is
// going from or to as `x` goes; the round in the left lobe, 0.8 of the
// height across, drawn in as the pill passes 0.1 to 0.6 of its way, and
// the round in the right swelled out as it passes 0.4 to 0.9; all three
// melted a fifth of the height wide, as the pen's blur melts them. A round
// shrinks into the pill's core, its middle pulled from its lobe toward
// the core's nearest end as it goes, so it always lies on the pill and is
// gone inside it: the end it draws in or swells out stays round and never
// a drop on its own (a round shrunk in place, past nothing, drew the pill's
// end to a point and left a speck and its shadow by it). Its distance,
// negative inside.
let fe_goo_toggle = fn(p: vec2, cl: vec2, cr: vec2, th: float, x: float) -> float {
let xp = mix(cl.x, cr.x, x)
let dp = length(p - vec2(clamp(p.x, xp - 0.204 * th, xp + 0.204 * th), cl.y)) - 0.204 * th
let sl = 1.0 - smoothstep(0.1, 0.6, x)
let sr = smoothstep(0.4, 0.9, x)
let ml = vec2(mix(max(cl.x, xp - 0.204 * th), cl.x, sl), cl.y)
let mr = vec2(mix(min(cr.x, xp + 0.204 * th), cr.x, sr), cl.y)
let dl = length(p - ml) - 0.401 * sl * th
let dr = length(p - mr) - 0.401 * sr * th
return fe_smin(fe_smin(dp, dl, 0.2 * th), dr, 0.2 * th)
}
// The quad reaches 4 points past the box, for the well's mouth and the
// focus ring.
mod.widgets.ToggleFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ToggleFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 4.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let th = self.size
let w = th * self.pill_aspect
let x0 = Finish.snap(o.x + mix(3.0, self.rect_size.x - w - 3.0, self.mark_at_end), px) - o.x
let cy = Finish.snap(o.y + self.rect_size.y * 0.5, px) - o.y
let cl = vec2(x0 + th * 0.5, cy)
let cr = vec2(x0 + w - th * 0.5, cy)
let live = 1.0 - self.disabled
let hov = self.hover * live
// where the toggle stands: the animator's turn, or the knob's drag
let a = clamp(mix(self.active, self.drag_pos, self.drag), 0.0, 1.0)
let x = a * a * (3.0 - 2.0 * a)
// the well: its edge, the case's normal there, the case curving down
// into it, its floor light grey off and orange on
// (the melted rounds' distance runs short inside the waist, where the
// two rounds' slopes cancel: it is taken over its slope, so the rim's
// bands stay at the rim)
let e = 0.5 * px
let dt0 = fe_peanut(p, cl, cr, th)
let gt = vec2(fe_peanut(p + vec2(e, 0.0), cl, cr, th) - dt0, fe_peanut(p + vec2(0.0, e), cl, cr, th) - dt0) / e
let dt = dt0 / max(length(gt), 0.25)
let nt = normalize(gt + vec2(0.00001, 0.0))
var out = fk_mouth(dt, nt, 2.0, px, live, self.disabled)
var floor = mix(vec3(0.83, 0.828, 0.834), vec3(0.933, 0.353, 0.141), x)
floor = floor + g3(0.025 * hov)
floor = mix(floor, vec3(0.86, 0.855, 0.848), self.disabled * 0.7)
// the printed marks: a bar in the left lobe, white when on; a ring in
// the right, grey; the goo hides the one it lies on
let bar = fk_aa(fk_rbox(p, cl, vec2(0.042 * th, 0.225 * th), 0.042 * th), px)
floor = mix(floor, mix(vec3(0.55, 0.55, 0.55), vec3(1.0, 1.0, 1.0), x), bar * mix(1.0, 0.5, self.disabled))
let ring = fk_aa(abs(length(p - cr) - 0.183 * th) - 0.042 * th, px)
floor = mix(floor, mix(vec3(0.55, 0.55, 0.55), vec3(1.0, 0.85, 0.78), x), ring * mix(1.0, 0.5, self.disabled))
let fa = fk_aa(dt, px)
out = Finish.over(out, vec4(floor * fa, fa))
// the lens's thickness at its rim, seen on the floor: drawn under the
// goo, so where the goo passes the narrow waist close to the rim the
// band and its fringes do not cross it
out = Finish.over(out, fk_lens_rim(dt, nt, th * 0.5, px, self.disabled))
// the goo, the knobs' black ferrofluid: its distance and the field's
// slope at it, so its ink reads it as it reads the knob's drop
let dg = fe_goo_toggle(p, cl, cr, th, x)
let gx = fe_goo_toggle(p + vec2(e, 0.0), cl, cr, th, x) - dg
let gy = fe_goo_toggle(p + vec2(0.0, e), cl, cr, th, x) - dg
let ink = fk_drop_ink(vec3(1.0 - dg, -gx / e, -gy / e), th * 0.75, th * 0.75, px, mix(1.0, 0.3, self.disabled))
out = Finish.over(out, vec4(ink.rgb * fa, ink.a * fa))
// the lens over it, as on the knobs and the displays: the case's shadow
// in under the upper rim; the lens's caught light and seam at its rim
// (its crest and sheen, made for a round or a box, would cross the
// waist, so they are left out); and a crisp crescent on each lobe's
// dome toward the light, a third of the lobe in; a touch brighter under
// the pointer
let ish = smoothstep(-0.08 * th, 0.08 * th, dt - dot(nt, vec2(0.03, 0.06) * th)) * fa * 0.42 * mix(1.0, 0.5, self.disabled)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, ish))
out = Finish.over(out, fk_lens_light(p, vec2(-1000.0, -1000.0), dt, nt, th * 0.5, px, hov, self.down * live, self.disabled))
let k = th * 0.5
let cw = max(0.035 * k, 0.5 * px)
let lit = (1.0 + 0.3 * hov - 0.3 * self.down * live) * mix(1.0, 0.4, self.disabled) * 0.38 * (0.035 * k / cw) * fa
let ql = p - cl
let qr = p - cr
var crest = (1.0 - smoothstep(cw, cw + px, abs(length(ql) - 0.66 * k))) * smoothstep(0.8, 0.93, dot(ql / max(length(ql), 0.0001), vec2(-0.524, -0.852)))
crest = crest + (1.0 - smoothstep(cw, cw + px, abs(length(qr) - 0.66 * k))) * smoothstep(0.8, 0.93, dot(qr / max(length(qr), 0.0001), vec2(-0.524, -0.852)))
out = Finish.over(out, vec4(crest * lit, crest * lit, crest * lit, crest * lit))
out = Finish.over(out, vec4(vec3(0.706, 0.137, 0.094) * Finish.band(dt, 0.0, px, px) * self.error, Finish.band(dt, 0.0, px, px) * self.error))
return Finish.over(out, fk_focus(dt, px, self.focus * live))
}
// The rocker and the slide switch are built on the toggle but draw their
// own faces, with no room past the box: they keep a plain quad and the
// toggle's old numbers, and carry no motion and no field.
mod.widgets.ToggleRocker.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size)
}
mod.widgets.ToggleSlide.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size)
}
mod.widgets.ToggleRocker.draw_bg.pill_aspect = uniform(1.875)
mod.widgets.ToggleRocker.draw_bg.knob_inset = uniform(2.0)
mod.widgets.ToggleRocker.draw_bg.border_size = uniform(theme.beveling)
mod.widgets.ToggleSlide.draw_bg.border_size = uniform(theme.beveling)
mod.widgets.ToggleRocker.min_height = 24.0
mod.widgets.ToggleSlide.min_height = 24.0
mod.widgets.ToggleRocker.cap_viscosity = 0.0
mod.widgets.ToggleSlide.cap_viscosity = 0.0
mod.widgets.ToggleRocker.cap_field_reach = 0.0
mod.widgets.ToggleSlide.cap_field_reach = 0.0
// and they keep the stock flip, not the gooey toggle's 0.6 seconds
mod.widgets.ToggleRocker.animator.active.on.ease = OutQuad
mod.widgets.ToggleRocker.animator.active.on.from = {all: Forward {duration: 0.1}}
mod.widgets.ToggleRocker.animator.active.off.ease = OutQuad
mod.widgets.ToggleRocker.animator.active.off.from = {all: Forward {duration: 0.1}}
mod.widgets.ToggleSlide.animator.active.on.ease = OutQuad
mod.widgets.ToggleSlide.animator.active.on.from = {all: Forward {duration: 0.1}}
mod.widgets.ToggleSlide.animator.active.off.ease = OutQuad
mod.widgets.ToggleSlide.animator.active.off.from = {all: Forward {duration: 0.1}}
// THE WELL AND THE LENS, the one material of the knobs and the displays:
// a well pressed into the case, and in it a lens of clear glass flush with
// the case over the well's floor. `sd` is the distance to the lens's edge
// (points, negative inside), `n` the outward normal there, `k` the lens's
// size (a knob's radius, a display's half height); the light from the upper
// left, as the sheet's. A lens is a rounded box: a knob's is one with its
// corners as round as it is.
// Coverage of an edge `d` away (points, negative inside): a smooth ramp a
// device pixel either side of the edge, as steep in its middle as three
// quarters of a pixel's, so a curve's steps melt: the drop's, the lens's,
// the dots' and the pointer's edges all take it.
let fk_aa = fn(d: float, px: float) -> float {
return 1.0 - smoothstep(-px, px, d)
}
// A rounded box's outward normal at `p`.
let fk_rbox_n = fn(p: vec2, c: vec2, h: vec2, r: float) -> vec2 {
let s = p - c
let q = abs(s) - h + vec2(clamp(r, 0.0, min(h.x, h.y)), clamp(r, 0.0, min(h.x, h.y)))
var n = vec2(step(q.y, q.x), step(q.x, q.y))
if q.x > 0.0 && q.y > 0.0 {
n = normalize(q)
}
return n * vec2(mix(-1.0, 1.0, step(0.0, s.x)), mix(-1.0, 1.0, step(0.0, s.y)))
}
// The well's mouth: from `fw` out the case curves down to the lens,
// darkening on the side under the light and turning to the light on the
// far side, and catches a thin lit lip at the lens's lower edge. Nothing
// falls on the case outside it.
let fk_mouth = fn(sd: float, n: vec2, fw: float, px: float, live: float, dis: float) -> vec4 {
let face = dot(n, vec2(0.524, 0.852))
let fil = (1.0 - smoothstep(0.0, fw, sd)) * (1.0 - fk_aa(sd, px))
let fd = fil * fil * (max(-face, 0.0) * 0.24 + 0.04) * mix(1.0, 0.5, dis)
let fl = fil * fil * max(face, 0.0) * 0.4 * live
let lip = Finish.ring_out(sd, 0.0, px, px) * smoothstep(0.1, 0.7, face) * 0.75 * live
return Finish.over(Finish.over(vec4(0.0, 0.0, 0.0, fd), vec4(fl, fl, fl, fl)), vec4(lip, lip, lip, lip))
}
// The well's floor under the lens: the case's grey a step darker, plain
// (the case's print does not reach down into it), a hair darker held.
let fk_floor = fn(press: float, dis: float) -> vec3 {
return mix(vec3(0.80, 0.792, 0.782) * (1.0 - 0.04 * press * (1.0 - dis)), vec3(0.86, 0.855, 0.848), dis * 0.6)
}
// The case's shadow falling into the well under its upper rim, over the
// floor and whatever lies on it, deeper held: its strength at `p`, the
// lens the box `c`, `h`, `r`.
let fk_rim_shade = fn(p: vec2, c: vec2, h: vec2, r: float, k: float, press: float, dis: float) -> float {
return Finish.inset(p, c, h, r, vec2(0.06, 0.12) * k * (1.0 + 0.3 * press), 0.1 * k) * (0.42 + 0.08 * press) * mix(1.0, 0.5, dis)
}
// The lens's thickness at its rim: a band a fifth of `k` deep, darkest just
// inside the edge and deeper away from the light, a faint cool and warm
// fringe in it.
let fk_lens_rim = fn(sd: float, n: vec2, k: float, px: float, dis: float) -> vec4 {
let away = dot(n, vec2(0.6, 0.8))
let far = clamp(away, 0.0, 1.0)
let rim = pow(clamp(1.0 + sd / max(k * 0.22, 3.0 * px), 0.0, 1.0), 1.6) * smoothstep(0.0, 2.0 * px, -sd) * fk_aa(sd, px)
let fr = Finish.band(sd, 2.0 * px, 4.0 * px, px) * 0.15 * (1.0 - dis)
var out = vec4(0.0, 0.0, 0.0, rim * (0.12 + 0.08 * away) * mix(1.0, 0.5, dis))
out = Finish.over(out, vec4(vec3(0.80, 0.52, 0.38) * fr * far, fr * far))
return Finish.over(out, vec4(vec3(0.42, 0.60, 0.85) * fr * max(-away, 0.0), fr * max(-away, 0.0)))
}
// The light on the lens, subtle: a fine line caught a pixel inside its rim,
// strong toward the light and weaker on the far side; a crisp crescent a
// third of `k` in from the rim where it faces the light (never under a
// pixel wide, dimmed by what it lacks of one), near the corner
// toward the light (`tl`, the lens's top left: on a knob that is all of
// it, on a display the corner's arc and the start of its sides), over a
// faint soft sheen in from that corner; and the dark seam where the glass
// meets the well.
let fk_lens_light = fn(p: vec2, tl: vec2, sd: float, n: vec2, k: float, px: float, hover: float, press: float, dis: float) -> vec4 {
let away = dot(n, vec2(0.6, 0.8))
let far = clamp(away, 0.0, 1.0)
let ln = Finish.band(sd, px, 2.0 * px, px) * (0.55 * clamp(-away, 0.0, 1.0) + 0.25 * far) * mix(0.9, 1.0, hover) * mix(1.0, 0.5, dis)
let cw = max(0.035 * k, 0.5 * px)
let crest = (1.0 - smoothstep(cw, cw + px, abs(sd + 0.34 * k))) * (0.035 * k / cw) * smoothstep(0.8, 0.93, dot(n, vec2(-0.524, -0.852))) * (1.0 - smoothstep(1.1 * k, 1.6 * k, length(p - tl)))
let sheen = 1.0 - smoothstep(0.0, 0.55 * k, length(p - tl - vec2(0.7, 0.6) * k))
let hl = (crest * 0.38 + sheen * 0.07) * (1.0 + 0.3 * hover - 0.3 * press) * mix(1.0, 0.4, dis) * fk_aa(sd, px)
let ola = Finish.band(sd, 0.0, px, px) * mix(0.55, 0.3, far) * mix(1.0, 0.6, dis)
return Finish.over(Finish.over(vec4(ln, ln, ln, ln), vec4(hl, hl, hl, hl)), vec4(g3(0.36) * ola, ola))
}
// KNOBS. A well pressed into the case, a lens of clear glass set in it
// flush with the case, and round it a ring of fifteen dots lit orange up to
// the value: a meter drawn, not glowing. A short orange mark at the glass's
// rim points at the value, and deep in the well, under the glass, lies a
// drop of black ferrofluid that ripples round as the knob turns and reaches
// out as it turns faster; on the small knobs, a goo of three drops.
// The drop's field, a metaball: eight points on a circle, the k-th (k from
// 8 to 15) at the angle k pi/4 and 1 + `a` sin(`ph` + that angle) from the
// middle, so the lobes swell and draw in as a wave runs round; each lends k
// to the power of minus its distance (rounded over a quarter at the
// point), over a floor of 0.5, and the liquid is where the field reaches 1.
// The wave carries the liquid round to one side and the k's weigh it, so
// the drop is held with its centre of mass on the knob's middle: the field
// is read moved by where that centre would stand, measured offline from
// the drop over its phases and its swing and fitted, cubic in `a`, as a
// lean that turns with `ph` (under 0.05 of a point off at rest, under 0.75
// on the Dial turned fast). `u` in the drop's units; the field and its
// gradient.
let fk_drop = fn(u0: vec2, ph: float, a: float) -> vec3 {
let ap = vec4(1.0, a, a * a, a * a * a)
let u = u0 + vec2(dot(ap, vec4(0.0412, -0.0494, 0.1267, -0.0581)) + dot(ap, vec4(-0.1214, 1.3628, -1.3174, 0.4383)) * sin(ph), dot(ap, vec4(0.0507, -0.0077, 0.1062, -0.0588)) + dot(ap, vec4(-0.1136, 1.3079, -1.3277, 0.4493)) * cos(ph))
var f = vec3(0.5, 0.0, 0.0)
var k = 8.0
for j in 0..8 {
let an = k * 0.7853982
let w = u - vec2(cos(an), sin(an)) * (1.0 + a * sin(ph + an))
let l = sqrt(dot(w, w) + 0.0625)
let e = pow(k, -l)
f = f + vec3(e, -w.x * e * log(k) / l, -w.y * e * log(k) / l)
k = k + 1.0
}
return f
}
// The small knobs' drop, his goo of three: discs 0.08, 0.11 and 0.06 of a
// box across in radius, blurred by 0.05 of it and cut at 7/18 (a Gaussian
// of the blurred disc's spread stands in for each). The first two swing
// 0.15 either way along the line through the middle, every 1.5 and 2.5 of
// `t`, the third 0.1 either way across it every 3.5, each swing eased;
// `sp` scales the swings. The goo's centre of mass, the discs' centres
// weighed by their areas, is taken off them all, so the discs slide past
// one another and the goo stays on the knob's middle. `u` in box units
// from the middle; the field over the cut and its gradient.
let fk_goo = fn(u: vec2, t: float, sp: float) -> vec3 {
let ph = vec3(fract(t / 1.5), fract(t / 2.5), fract(t / 3.5)) * 2.0
let e = smoothstep(vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0), min(ph, vec3(2.0, 2.0, 2.0) - ph))
let o = (e * 2.0 - vec3(1.0, 1.0, 1.0)) * vec3(0.15, 0.15, 0.1) * sp
let cm = vec2(0.0064 * o.x + 0.0121 * o.y, 0.0036 * o.z) / 0.0221
var f = vec3(0.0, 0.0, 0.0)
var i = 0.0
for j in 0..3 {
let k = vec3(step(i, 0.5), step(abs(i - 1.0), 0.5), step(1.5, i))
let r = dot(k, vec3(0.08, 0.11, 0.06))
let oi = dot(k, o)
let w = u + cm - mix(vec2(oi, 0.0), vec2(0.0, oi), k.z)
let s2 = 0.0025 + r * r * 0.25
let g = r * r / (2.0 * s2) * exp(-dot(w, w) / (2.0 * s2)) / 0.3889
f = f + vec3(g, -w.x * g / s2, -w.y * g / s2)
i = i + 1.0
}
return f
}
// The drop's ink: `f` the field and its gradient per point, `rg` the
// glass's radius. Its edge is where the field crosses 1, a pixel soft; from
// there the liquid rises in a round 0.15 of the radius wide to a flat top,
// its slope along the field's gradient: near-black, a faint gloss, the
// floor's light in its steep edge, and a sharp bright streak where the
// surface turns to the light (from the upper left, as the sheet's). The
// face is taken at five depths across the pixel along the gradient and
// averaged, so a streak or an edge light narrower than a pixel comes out a
// pixel wide and as much dimmer, and steps nowhere. Where the gradient is
// low against `ge` (the field's slope at its edge, times the radius), at a
// saddle or in the crease where two drops meet, the distance it gives runs
// short and its direction wanders: under four tenths of `ge` the face goes
// flat, and under nine tenths the streak goes, so no glint stands alone in
// a crease. Under it its shadow on the well's floor, thrown down and to the
// right as from a drop standing off the floor. `fade` dims it all.
let fk_drop_ink = fn(f: vec3, rg: float, ge: float, px: float, fade: float) -> vec4 {
let gl = max(length(f.yz), 0.0001)
let conf = smoothstep(0.15, 0.4, gl * rg / ge)
let glint = smoothstep(0.7, 0.9, gl * rg / ge) * 0.92
let d = (1.0 - f.x) / gl
let nr = -f.yz / gl
let dsh = d - dot(nr, vec2(0.06, 0.12) * rg)
let sh = (1.0 - smoothstep(-0.08 * rg, 0.14 * rg, dsh)) * 0.3 * fade
var col = vec3(0.0, 0.0, 0.0)
var o = -0.4
for j in 0..5 {
let w = clamp(1.0 + (d + o * px) / (0.15 * rg), 0.0, 1.0)
let sl = conf * w / max(sqrt(1.0 - w * w), 0.2)
let n = normalize(vec3(nr.x * sl, nr.y * sl, 1.0))
let nh = max(dot(n, vec3(-0.223, -0.371, 0.901)), 0.0)
let c = mix(vec3(0.027, 0.027, 0.031) + g3(0.06 * pow(nh, 8.0)), vec3(0.33, 0.34, 0.36), (1.0 - n.z) * (1.0 - n.z) * 0.35)
col = col + mix(c, vec3(1.0, 1.0, 1.0), smoothstep(0.35, 0.7, pow(nh, 30.0)) * glint) * 0.2
o = o + 0.2
}
let a = fk_aa(d, px) * fade
return Finish.over(vec4(0.0, 0.0, 0.0, sh), vec4(col * a, a))
}
let fk_knob = fn(p: vec2, c: vec2, rad: float, px: float, val: float, trail: float, spd: float, f0: float, f1: float, gap: float, hover: float, press: float, dis: float, dot_r: float, dot_gap: float) -> vec4 {
let dia = rad * 2.0
let q = p - c
let live = 1.0 - dis
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the dot ring
let sweep = 2.0 * PI - gap
var ang = atan2(-q.x, q.y)
if ang < 0.0 {
ang = ang + 2.0 * PI
}
let fi = clamp(floor((ang - gap * 0.5) / sweep * 14.0 + 0.5), 0.0, 14.0)
let da = gap * 0.5 + sweep * fi / 14.0
let rs = rad + max(1.0, dia * 0.05)
let rd = rs + dot_gap + dot_r
let dc = c + vec2(-sin(da), cos(da)) * rd
let dd = length(p - dc) - dot_r
let lit = step(fi / 14.0, f1 + 0.02) * step(f0 - 0.02, fi / 14.0)
var dcol = mix(vec3(0.784, 0.773, 0.757), vec3(0.933, 0.353, 0.141), lit)
dcol = mix(dcol, vec3(0.84, 0.835, 0.825), dis * 0.7)
let dca = fk_aa(dd, px)
out = Finish.over(out, vec4(dcol * dca, dca))
// THE WELL AND THE LENS, as wide as the cap and its skirt were, the
// well's mouth six tenths of the dot ring's gap out from the glass
let rg = Finish.snap(rs, px)
let rho = length(q)
let nq = q / max(rho, 0.0001)
let sd = rho - rg
let ga = fk_aa(sd, px)
out = Finish.over(out, fk_mouth(sd, nq, 0.6 * dot_gap, px, live, dis))
out = Finish.over(out, vec4(fk_floor(press, dis) * ga, ga))
// the drop on the well's floor. On a glass under 20 points round, the
// goo of three in a box 2.8 radii wide, turned with the pointer (its
// swings across the pointer, clear of its mark), its clock 7.5 over the
// knob's travel as the value `trail` (the value on the knob's spring)
// goes, so its discs swing five, three and two and a bit times; a fast
// turn (`spd`) spreads them a fifth more. Its widest stays 0.7 of the
// radius out at rest, 0.8 turning fast. Else the eight-point drop, 0.6 of the
// glass across at rest: its wave runs round three and a half times over
// the travel; its lobes swing 0.25 at rest and up to 1.5 as fast as the
// knob turns, giving up some of its body to them, so it never reaches
// the rim.
let pa = gap * 0.5 + sweep * val
let dir = vec2(-sin(pa), cos(pa))
if rho < rg {
var fv = vec3(0.0, 0.0, 0.0)
var ge = 0.6
if rg < 20.0 {
ge = 7.0
let sb = 2.8 * rg
let fg = fk_goo(vec2(dot(q, vec2(-dir.y, dir.x)), dot(q, dir)) / sb, trail * 7.5, 1.0 + 0.2 * spd)
let gq = (vec2(-dir.y, dir.x) * fg.y + dir * fg.z) / sb
fv = vec3(fg.x, gq.x, gq.y)
} else {
let a = 0.25 + 1.25 * spd
let sc = rg * 0.405 * (1.0 - 0.2 * (a - 0.25))
let fd = fk_drop(q / sc, trail * 22.0, a)
fv = vec3(fd.x, fd.y / sc, fd.z / sc)
}
out = Finish.over(out, fk_drop_ink(fv, rg, ge, px, mix(1.0, 0.3, dis)))
}
out = Finish.over(out, vec4(0.0, 0.0, 0.0, fk_rim_shade(p, c, vec2(rg, rg), rg, rg, press, dis) * ga))
// the glass's face: clear, a breath lighter toward the top; lifted
// under the hand, a shade darker held, grey and cloudy disabled
let lift = ga * (max(-q.y / rg, 0.0) * 0.06 + 0.05 * hover - 0.04 * press) * live
out = Finish.over(out, vec4(max(lift, 0.0), max(lift, 0.0), max(lift, 0.0), abs(lift)))
let cloud = ga * 0.36 * dis
out = Finish.over(out, vec4(vec3(0.80, 0.795, 0.79) * cloud, cloud))
out = Finish.over(out, fk_lens_rim(sd, nq, rg, px, dis))
// the pointer: a short orange mark inside the rim, toward the value
let pl = fk_seg(p, c + dir * rg * 0.78, c + dir * rg * 0.9) - max(rg * 0.045, 0.75)
let pink = mix(vec3(0.878, 0.337, 0.122), vec3(0.72, 0.71, 0.70), dis)
let pma = fk_aa(pl, px)
out = Finish.over(out, vec4(pink * pma, pma))
return Finish.over(out, fk_lens_light(p, c - vec2(rg, rg), sd, nq, rg, px, hover, press, dis))
}
// Where the drop's wave stands: the value less the motion's trail (its
// stretch over 1.69 of the nominal cap's radius, along the box's height,
// the way the knob is dragged), so the wave follows the turn on the spring
// and comes to rest with it.
let fk_trail = fn(pos: float, dir: float, stretch: float, cap: float, travel: float) -> float {
return pos - dir * stretch * 0.845 * cap / max(travel, 1.0)
}
mod.widgets.RotaryKnob.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 6.0
let size = self.rect_size
let p = self.pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let live = 1.0 - self.disabled
let dia = min(size.x, size.y)
let rr = dia * 0.5
let c = vec2(Finish.snap(o.x + size.x * 0.5, px) - o.x, Finish.snap(o.y + size.y - rr, px) - o.y)
let gap = self.gap * PI / 180.0
// the ring's room scales with the knob; under 32 points the dots are
// 1.5 points and stand 3 points clear of the cap's skirt, so the ring
// and the skirt read as two edges, not one fuzzy one
let small = 1.0 - step(16.0, rr)
let dg = mix(clamp(rr * 0.16, 1.5, 4.0), 3.0, small)
let dr = mix(clamp(rr * 0.045, 0.6, 1.0), 0.75, small)
let rad = floor((rr - dg - 2.0 * dr - 0.5) / 1.1 / px) * px
let lifted = max(self.hover, max(self.focus, self.drag)) * live
var out = fk_knob(p, c, rad, px, self.slide_pos, fk_trail(self.slide_pos, self.cap_dir, self.cap_stretch, self.cap_px, self.rect_size.y), self.cap_stretch, self.fill_lo, self.fill_hi, gap, lifted, self.drag * live, self.disabled, dr, dg)
return Finish.over(out, fk_focus(length(p - c) - rad - max(1.0, rad * 0.1), px, self.focus * live))
}
mod.widgets.RotaryFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 6.0
let size = self.rect_size
let p = self.pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let live = 1.0 - self.disabled
let room = vec2(size.x, size.y - 22.0)
let dia = min(room.x, room.y)
let rr = dia * 0.5
let c = vec2(Finish.snap(o.x + size.x * 0.5, px) - o.x, Finish.snap(o.y + 22.0 + room.y * 0.5, px) - o.y)
let gap = self.gap * PI / 180.0
let small = 1.0 - step(16.0, rr)
let dg = mix(clamp(rr * 0.12, 1.5, 4.5), 3.0, small)
let dr = mix(clamp(rr * 0.035, 0.6, 1.25), 0.75, small)
let rad = floor((rr - dg - 2.0 * dr - 0.5) / 1.1 / px) * px
let lifted = max(self.hover, max(self.focus, self.drag)) * live
var out = fk_knob(p, c, rad, px, self.slide_pos, fk_trail(self.slide_pos, self.cap_dir, self.cap_stretch, self.cap_px, self.rect_size.y), self.cap_stretch, self.fill_lo, self.fill_hi, gap, lifted, self.drag * live, self.disabled, dr, dg)
return Finish.over(out, fk_focus(length(p - c) - rad - max(1.0, rad * 0.1), px, self.focus * live))
}
// The dial's readout: a black display window. A pixel of light on the case
// under it, a dark rim, the glass with faint scanlines and one pixel of
// shade under its top edge, cool white monospace digits. No glow.
let fk_display = fn(p: vec2, size: vec2, o: vec2, px: float, r: float) -> vec4 {
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o - vec2(0.0, px)
let h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox(p, c, h, r)
let row = floor((p.y + o.y) / px)
var col = vec3(0.102, 0.102, 0.102) + g3(0.012 * (fract(row * 0.5) * 2.0 - 0.5))
col = mix(col, vec3(0.0, 0.0, 0.0), fk_run(p.y, lo.y + px, lo.y + 2.0 * px) * 0.4)
col = mix(col, vec3(0.184, 0.184, 0.184), Finish.band(d, 0.0, px, px))
let a = Finish.cover(d, px)
let out = fk_catch(p, c, h, r, px, 0.75)
return Finish.over(out, vec4(col * a, a))
}
mod.widgets.RotaryFlat.text_input.padding = mod.turtle.Inset{left: 5.0 right: 5.0 top: 3.0 bottom: 3.0}
mod.widgets.RotaryFlat.text_input.min_height = 0.0
mod.widgets.RotaryFlat.text_input.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_display(self.pos * self.rect_size, self.rect_size, self.rect_pos, px, 1.0)
}
mod.widgets.RotaryFlat.text_input.draw_text.color = #e5eff8
mod.widgets.RotaryFlat.text_input.draw_text.color_hover = #e5eff8
mod.widgets.RotaryFlat.text_input.draw_text.color_focus = #ffffff
mod.widgets.RotaryFlat.text_input.draw_text.color_empty = #767676
mod.widgets.RotaryFlat.text_input.draw_text.text_style = mod.theme.font_code{font_size: 8.25}
// ---------------------------------------------------------------------------
// FADERS. The ferrofluid drop (the bench's "Ferrofluid drop" in obsidian): a
// round drop of black liquid as long as the cap, 8.33 points tall (the
// bench's 10 on its 24), its dome a quarter round in from the outline, on a
// 5 point slot cut into the case with a dark bar in it from the stop to the
// value. It moves as a liquid: dragged, its tail trails the head on a
// viscous spring; pressed, it spreads and settles lower; sent to a value
// that jumps, it travels there. When the pointer comes near, the drop
// answers its field as ferrofluid answers a magnet: the body shrinks into
// spikes, cones on a drifting hexagonal lattice radiating from a centre on
// the drop's axis below the case, longest under the pointer, ray-marched as
// one solid with the body, seen from an eye close over the drop so they fan
// out toward it, and lit by the drop's own material on the marched surface's
// own normal.
// The shape, the effect and the material are the bench's numbers; the case,
// the slot and the bar are this sheet's. Hovered or focused, the drop lifts
// as every key here does, a shade lighter with a darker shadow; disabled, it
// fades toward the case, loses its shadow and grows no spikes, and the slot
// goes pale. Lying down or standing up (`is_vertical`), the light low from
// the upper left.
mod.widgets.SliderFader.cap_size = 20.0
mod.widgets.SliderFader.track_inset = 8.0
// The cap is a liquid: the widget keeps its motion (the tail's stretch and
// direction, the press as a spring, the squash as the tail lands) at the
// bench's viscosity.
mod.widgets.SliderFader.cap_viscosity = 0.5
// The pointer's field: the widget raises it while the pointer is within this
// much plus the cap's length of the drop's centre, anywhere on the window,
// and hands over where the pointer is and a clock for the lattice's drift.
// This only says when the field is up; how far it reaches over the drop is
// the effect's own reach, cap_effect_reach below.
mod.widgets.SliderFader.cap_field_reach = 20.0
mod.widgets.SliderFader.draw_bg.track_size = uniform(5.0)
// The preset's width across. A drop does not read it: its width is the cap's
// length, as the bench draws every drop.
mod.widgets.SliderFader.draw_bg.cap_cross = uniform(0.4)
mod.widgets.SliderFader.clip_x = false
mod.widgets.SliderFader.clip_y = false
// The material, tunable live: the bench's ferrofluid drop as he exported it
// (2026-10-01). The key light (x right, y down, z toward the viewer), low
// from the upper left so the spikes' flanks catch it, and its intensity; the
// highlight, its roughness (a tight glint) and the sheen; the rim toward
// grazing; the occlusion and how far up the drop it reaches; the plain
// sky's reflection (none) and how near the eye is over the drop; metal,
// spun grain and glow; the exposure in EV and the highlight roll-off; the
// cast shadow's strength and blur, the contact shadow and the slot's inner
// shadow; the drop's height and how far its top comes toward the eye. The
// bench's drop is 24 units across, 10 high; this one is 20, so its height
// and blur are 20/24 of the bench's.
mod.widgets.SliderFader.draw_bg.cap_light = uniform(vec3(-0.76, -0.6, 0.36))
mod.widgets.SliderFader.draw_bg.cap_light_intensity = uniform(1.0)
mod.widgets.SliderFader.draw_bg.cap_specular = uniform(0.9)
mod.widgets.SliderFader.draw_bg.cap_roughness = uniform(0.08)
mod.widgets.SliderFader.draw_bg.cap_gloss = uniform(0.4)
mod.widgets.SliderFader.draw_bg.cap_rim = uniform(0.5)
mod.widgets.SliderFader.draw_bg.cap_ao = uniform(0.55)
mod.widgets.SliderFader.draw_bg.cap_ao_reach = uniform(1.0)
mod.widgets.SliderFader.draw_bg.cap_reflection = uniform(0.0)
mod.widgets.SliderFader.draw_bg.cap_reflection_perspective = uniform(0.35)
mod.widgets.SliderFader.draw_bg.cap_metal = uniform(0.2)
mod.widgets.SliderFader.draw_bg.cap_spun = uniform(0.0)
mod.widgets.SliderFader.draw_bg.cap_glow = uniform(0.0)
mod.widgets.SliderFader.draw_bg.cap_exposure = uniform(0.0)
mod.widgets.SliderFader.draw_bg.cap_rolloff = uniform(0.6)
mod.widgets.SliderFader.draw_bg.cap_shadow = uniform(0.5)
mod.widgets.SliderFader.draw_bg.cap_shadow_blur = uniform(6.67)
mod.widgets.SliderFader.draw_bg.cap_contact = uniform(0.45)
mod.widgets.SliderFader.draw_bg.cap_inner = uniform(0.62)
mod.widgets.SliderFader.draw_bg.cap_height = uniform(8.33)
mod.widgets.SliderFader.draw_bg.cap_perspective = uniform(0.44)
// The field's effect, tunable live: the spikes' spacing on the lattice; their
// length, so many of the drop's heights plus so many points (his rule,
// partly relative and partly absolute: 1.5 heights and 6.67 points, the
// bench's 2.3 heights on this drop, and relatively longer on a smaller one);
// how far the body shrinks into them at full field; how far the field
// reaches from the pointer; how far the spikes push the cast shadow out, as
// a share of their length; the fan (0 upright, 2 along the drop's normal);
// the cones' base radius as a share of the spacing (at most 0.6); the
// fluid's own light on the flanks (0 the material alone, below 0 darker);
// the shading normal (0 the marched surface's own, 1 the body's); and the
// share of the pointer's offset from the drop the lattice slides by. The
// bench takes the spacing and the reach in its layout points, each two of
// the device pixels it draws the cap's lengths in, so against its 24 unit
// drop they are 3.8 and 40, and on this 20 point drop 20/24 of that: the
// reach 33.3 and the spacing 3.17, here 3.4, the density he chose on this
// sheet. The bench slides the lattice by the pointer's whole offset in its
// 55 point view of the drop; this drop is 20, so the lattice slides by 20/55
// of the offset and the spikes pass under the pointer as they do there.
mod.widgets.SliderFader.draw_bg.cap_effect_spacing = uniform(3.4)
mod.widgets.SliderFader.draw_bg.cap_effect_height = uniform(1.5)
mod.widgets.SliderFader.draw_bg.cap_effect_height_abs = uniform(6.67)
mod.widgets.SliderFader.draw_bg.cap_effect_shrink = uniform(0.39)
mod.widgets.SliderFader.draw_bg.cap_effect_reach = uniform(33.3)
mod.widgets.SliderFader.draw_bg.cap_effect_outline = uniform(0.45)
mod.widgets.SliderFader.draw_bg.cap_effect_fan = uniform(0.85)
mod.widgets.SliderFader.draw_bg.cap_effect_base = uniform(0.43)
mod.widgets.SliderFader.draw_bg.cap_effect_light = uniform(0.0)
mod.widgets.SliderFader.draw_bg.cap_effect_normal = uniform(0.0)
mod.widgets.SliderFader.draw_bg.cap_effect_flow = uniform(0.0)
mod.widgets.SliderFader.draw_bg.cap_effect_slide = uniform(0.36)
// The inks: the case; the drop the bench's near black, as ferrofluid is, and
// the sheet's disabled key when disabled; the slot in the sheet's darkest ink
// and the bar in its softer one, both pale when disabled; the material's
// white light and the bench's grey shade.
mod.widgets.SliderFader.draw_bg.ground_color = uniform(theme.color_bg_app)
mod.widgets.SliderFader.draw_bg.handle_color = uniform(#07070b)
mod.widgets.SliderFader.draw_bg.handle_color_disabled = uniform(theme.color_outset_disabled)
mod.widgets.SliderFader.draw_bg.track_color = uniform(theme.color_terminal_bg)
mod.widgets.SliderFader.draw_bg.track_color_disabled = uniform(#9e9d9a)
mod.widgets.SliderFader.draw_bg.fill_color = uniform(theme.color_text_meta)
mod.widgets.SliderFader.draw_bg.fill_color_disabled = uniform(theme.color_bevel_disabled)
mod.widgets.SliderFader.draw_bg.light_color = uniform(theme.color_material_light)
mod.widgets.SliderFader.draw_bg.shadow_color = uniform(#26262c)
// THE FERROFLUID, the bench's spikes translated from its shader. They live
// here until he has judged them, then move into the cap engine. Points, in
// the drop's frame: x along the track, y across it, z up out of the case,
// the origin at the drop's centre on the case. `size` is the body's radius
// and height; `mag` the pointer from the drop's centre; `fx` the field's
// strength, its reach, the spikes' length (a share of the height) and their
// base (a share of the spacing); `lat` the lattice's anchor (the pointer in
// the fader's frame less the drift), its spacing and the field's clock;
// `centre` the point the spikes radiate from.
// A smooth minimum `k` wide: the cones melt into each other and the body.
let fe_smin = fn(a: float, b: float, k: float) -> float {
let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0)
return mix(b, a, h) - k * h * (1.0 - h)
}
// The body: an ellipsoid of the radius and the height, its distance by the
// usual bound.
let fe_body = fn(p: vec3, size: vec2) -> float {
let r = vec3(size.x, size.x, size.y)
let k0 = length(p / r)
let k1 = length(p / (r * r))
return k0 * (k0 - 1.0) / max(k1, 0.0001)
}
// One spike from its root along its axis, the bench's cone: a fat base,
// concave flanks (the radius falls as (1 - u) to the power 1.25 up the
// spike, so the root flares into the body and the flank curves in) and a
// fine tip rounded to 0.08 of the base so it takes a glint; under the root
// a short stub into the body, and nothing below the case. The body a spike
// has is its root: the base radius and the smooth minimum with the body
// and its neighbours, not a blunt tip.
let fe_cone = fn(p: vec3, root: vec3, ax: vec3, l: float, rb: float) -> float {
let pr = p - root
let t = dot(pr, ax)
let rt = rb * 0.08
if t > l {
return length(pr - ax * l) - rt
}
let perp = length(pr - ax * t)
let u = max(t, 0.0) / l
let sq = pow(1.0 - u, 0.25)
let r = max(rb * (1.0 - u) * sq, rt)
let dr = 1.25 * rb * sq / l
let d = (perp - r) / sqrt(1.0 + dr * dr)
return max(max(d, -t - rb), -p.z)
}
// The root a point belongs to: the point carried along its ray from the
// centre onto the body.
let fe_root_at = fn(p: vec3, size: vec2, centre: vec3) -> vec3 {
let sc = vec3(size.x, size.x, size.y)
let c = centre / sc
let dd = normalize(p / sc - c)
let b = dot(c, dd)
let disc = b * b - dot(c, c) + 1.0
if disc > 0.0 {
return (c + dd * (sqrt(disc) - b)) * sc
}
return normalize(c - dd * b) * sc
}
// The spike of one lattice site: the field there (falling to nothing at the
// reach, rippling outward with the clock) sets its length; a site off the
// body or in too weak a field grows none.
let fe_cone_at = fn(p: vec3, site: vec2, size: vec2, mag: vec2, fx: vec4, lat: vec4, centre: vec3) -> float {
let sd = site - lat.xy
let u = length(sd - mag) / max(fx.y, 1.0)
let f = fx.x * max(1.0 - u, 0.0) * (1.0 - smoothstep(0.8, 1.0, u)) * (0.8 + 0.2 * sin(u * 9.42 - lat.w * 2.5))
let fp = pow(max(f, 0.0), 0.7)
let rs = length(sd)
if rs > size.x * 0.97 || fp < 0.02 {
return 100000.0
}
let zb = size.y * sqrt(max(1.0 - rs * rs / (size.x * size.x), 0.0))
let root = vec3(sd.x, sd.y, zb)
let l = max(fx.z * size.y * fp * (0.5 + 0.5 * zb / size.y), 0.3)
return fe_cone(p, root, normalize(root - centre), l, lat.z * clamp(fx.w, 0.1, 0.6))
}
// The spikes near a point, read at the root it projects to: the nearest site
// of each of the lattice's two halves and its neighbour across the larger
// offset, four cones melted together.
let fe_spikes = fn(p: vec3, size: vec2, mag: vec2, fx: vec4, lat: vec4, centre: vec3) -> float {
let hh = vec2(lat.z, lat.z * 1.7320508)
let rt = fe_root_at(p, size, centre)
if rt.z < -0.01 {
return 100000.0
}
let qp = rt.xy + lat.xy
let k = lat.z * 0.12
let ca = floor(qp / hh + vec2(0.5, 0.5))
let fa = qp / hh - ca
var na = vec2(0.0, sign(fa.y))
if abs(fa.x) > abs(fa.y) * 1.7320508 {
na = vec2(sign(fa.x), 0.0)
}
let cb = floor(qp / hh) + vec2(0.5, 0.5)
let fb = qp / hh - cb
var nb = vec2(0.0, sign(fb.y))
if abs(fb.x) > abs(fb.y) * 1.7320508 {
nb = vec2(sign(fb.x), 0.0)
}
var res = fe_cone_at(p, ca * hh, size, mag, fx, lat, centre)
res = fe_smin(res, fe_cone_at(p, (ca + na) * hh, size, mag, fx, lat, centre), k)
res = fe_smin(res, fe_cone_at(p, cb * hh, size, mag, fx, lat, centre), k)
return fe_smin(res, fe_cone_at(p, (cb + nb) * hh, size, mag, fx, lat, centre), k)
}
// The solid: the body and its spikes as one.
let fe_sdf = fn(p: vec3, size: vec2, mag: vec2, fx: vec4, lat: vec4, centre: vec3) -> float {
return fe_smin(fe_body(p, size), fe_spikes(p, size, mag, fx, lat, centre), lat.z * 0.3)
}
// The march, from an eye over the drop at the drop's perspective down to the
// case point `q`, at most 40 steps from the top of the tallest spike. A hit
// answers the height it found in body heights, the slope there per point,
// and in .w 1 and how far inside the solid the ray gets, marched on in
// quarter device pixels (`px`) past the surface until it is half a pixel
// in, two pixels at most; a miss answers how near the ray passed to the
// solid in .x and 0 in .w. The face takes its coverage from either, a
// pixel's ramp about the edge: the outline is smooth, and a spike thinner
// than a pixel, which the ray crosses without getting half a pixel in, comes
// out a pixel wide and as much fainter, never stair-steps or dots. The
// normal is the solid's own, from four reads around the hit.
let fe_march = fn(q: vec2, size: vec2, mag: vec2, fx: vec4, lat: vec4, centre: vec3, persp: float, nmix: float, px: float) -> vec4 {
let bound = size.x + fx.z * size.y + 2.0
let lq = length(q)
if lq > bound {
return vec4(lq - bound + 0.5, 0.0, 0.0, 0.0)
}
let zc = size.y / max(persp, 0.04) * 1.5
let rd = normalize(vec3(q.x, q.y, -zc))
let top = size.y * (1.0 + fx.z) + 1.0
var p = vec3(0.0, 0.0, zc) + rd * ((zc - top) / max(-rd.z, 0.05))
var best = 100000.0
var hit = 0.0
for i in 0..40 {
let f = fe_sdf(p, size, mag, fx, lat, centre)
best = min(best, f)
if f < 0.03 {
hit = 1.0
break
}
p = p + rd * max(f * 0.75, 0.06)
if p.z < -0.5 {
break
}
}
if hit < 0.5 {
return vec4(max(best, 0.0), 0.0, 0.0, 0.0)
}
let k1 = vec3(1.0, -1.0, -1.0)
let k2 = vec3(-1.0, -1.0, 1.0)
let k3 = vec3(-1.0, 1.0, -1.0)
let k4 = vec3(1.0, 1.0, 1.0)
let e = 0.25
let no = normalize(k1 * fe_sdf(p + k1 * e, size, mag, fx, lat, centre) + k2 * fe_sdf(p + k2 * e, size, mag, fx, lat, centre) + k3 * fe_sdf(p + k3 * e, size, mag, fx, lat, centre) + k4 * fe_sdf(p + k4 * e, size, mag, fx, lat, centre))
// the shading normal leans toward the body's own, so the spikes take
// the drop's light and not each its own: `nmix` 0 is the surface's
// normal, 1 the body's
let ns = normalize(vec3(p.x / (size.x * size.x), p.y / (size.x * size.x), p.z / (size.y * size.y)))
let n = normalize(mix(no, ns, nmix))
let s = -1.0 / (max(n.z, 0.05) * size.y)
var fin = 0.0
var pin = p
for j in 0..8 {
pin = pin + rd * (0.25 * px)
fin = min(fin, fe_sdf(pin, size, mag, fx, lat, centre))
if fin < -0.5 * px {
break
}
}
return vec4(p.z / size.y, n.x * s, n.y * s, 1.0 - fin)
}
// The fluid's own light on the marched solid, `c` of it: a bright window
// behind the drop on the light's side and a ring of the horizon seen in the
// flanks, a sheen toward grazing and a glint at the tips, and the valleys
// under the field darkened. Below zero it only darkens. Screen space, as
// `Cap.shade`; `fq` the field at the point, `tip` how far up its spike the
// point stands.
let fe_comp = fn(fc: vec3, n: vec3, v: vec3, l: vec3, fq: float, tip: float, c: float, light_ink: vec3, li: float) -> vec3 {
if c < 0.0 {
return fc * (1.0 + 0.85 * max(c, -1.0))
}
let rv = n * (2.0 * dot(n, v)) - v
let lh = normalize(vec2(l.x, l.y) + vec2(0.0001, 0.0001))
let pane = smoothstep(0.45, 0.92, dot(rv, normalize(vec3(lh.x * 0.85, lh.y * 0.85, -0.5))))
let rz = (rv.z - 0.2) / 0.14
let ring = 0.2 * exp(-rz * rz)
let ft = 1.0 - max(dot(n, v), 0.0)
let fres = smoothstep(0.55, 1.0, ft * ft)
let valley = 1.0 - 0.5 * fq * (1.0 - smoothstep(0.0, 0.5, tip))
let t2 = tip * tip
return fc * mix(1.0, valley, min(c, 1.0)) + light_ink * (((pane + ring) * 0.85 + fres * 0.55 + 0.1 * t2 * t2) * li * c)
}
// One edge of a polygon's distance: the squared distance so far in .x and
// the inside sign in .y, flipped where the edge crosses the horizontal
// through `p` on the right, as the bench tests its shadow's hull.
let fe_edge = fn(p: vec2, vi: vec2, vj: vec2, acc: vec2) -> vec2 {
let e = vj - vi
let w = p - vi
let b = w - e * clamp(dot(w, e) / max(dot(e, e), 0.000001), 0.0, 1.0)
let c = step(vi.y, p.y) + 1.0 - step(vj.y, p.y) + step(e.y * w.x, e.x * w.y)
let flip = step(2.5, c) + step(c, 0.5)
return vec2(min(acc.x, dot(b, b)), acc.y * (1.0 - 2.0 * flip))
}
// The hull the drop's shadow is thrown over: from the foot at the drop's
// centre to its top thrown `off` by the light, swept back along `tail` while
// the tail trails; a parallelogram, or a segment for a round drop. Signed
// distance, negative inside.
let fe_hull = fn(p: vec2, off: vec2, tail: vec2) -> float {
var acc = vec2(dot(p - tail, p - tail), 1.0)
acc = fe_edge(p, vec2(0.0, 0.0), tail, acc)
acc = fe_edge(p, off, vec2(0.0, 0.0), acc)
acc = fe_edge(p, off + tail, off, acc)
acc = fe_edge(p, tail, off + tail, acc)
return acc.y * sqrt(acc.x)
}
// The drop's cast shadow, the bench's for its drop: the hull above, rounded
// by the corner of the height each point was cast from (the drop's radius
// `r0` at the foot, its top outline's, `top`, above) and pushed out by
// `push`, half the spikes' outline push. That height is read from the drop
// as drawn (`fb`: its distance and outward gradient, shrunk, pressed and
// stretched), so the shadow is sharp at the foot and softer and thinner the
// higher up the drop or its spikes it was cast from. The engine's
// Cap.shadow reads it from a fixed rounded box and takes no push, which put
// a hard grey disc under the spikes.
let fe_shadow = fn(q: vec2, fb: vec3, r0: float, top: vec3, tail: vec2, lt: vec3, rise: float, blur: float, push: float, strength: float, px: float) -> float {
let off = -lt.xy / max(lt.z, 0.25) * max(rise, 0.0) * 0.7
let offl = max(length(off), 0.001)
let th = clamp(max(fb.x, 0.0) / (offl * max(dot(vec2(fb.y, fb.z), off / offl), 0.25)), 0.0, 1.0)
let rt = mix(r0, top.z * r0 * mix(1.0, min(top.x, top.y), th), th)
let dsh = fe_hull(q, off, tail) - rt - push
let tipb = clamp(blur, max(rise * 0.35, 1.0), max(rise, 1.0))
let bl = max(tipb * mix(0.15, 1.0, th), px)
return strength * (1.0 - smoothstep(-bl * 0.35, bl, dsh)) * mix(1.0, 0.3, th)
}
// The drop's dome at `w` (drop-local points, the fader's frame): its height
// as a share of the drop's in .x, the gradient per point in .yz, the
// footprint's distance in .w. The footprint is the engine's drop, `radius`
// at rest, stretched, pressed and squashed by the widget's motion (`motion`:
// stretch, direction, squash along, squash across); the height the edge
// curve's quarter round in from it over the cap's half length, the bench's
// along and across curves being flat at 1. The field's shrink takes the
// radius (the caller's) and the height (here) down alike, as the bench
// shrinks the drop into its spikes.
let fe_dome = fn(w: vec2, hs: vec2, radius: float, motion: vec4, press: float, height: float, shrink: float) -> vec4 {
let fp = Cap.sd_drop(w, radius, motion.x, vec2(motion.y, 0.0), press, motion.z, motion.w)
let c_edge = Cap.curve(Cap.edge_x(fp.x, hs, 1.0), 2.0, vec2(0.0, 0.0), vec2(0.0, 0.5523), vec2(-0.5523, 0.0), vec2(1.0, 1.0), vec2(0.5523, 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 c_flat = vec2(1.0, 0.0)
// corner and edge width the whole half length, blend 0.02; no ribs, no
// mark, no cut
let f = Cap.field(w, hs, fp, c_flat, c_flat, c_edge, c_flat, vec4(1.0, 1.0, 0.02, height), vec4(0.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.5, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0))
return vec4(f.x * shrink, f.y * shrink, f.z * shrink, fp.x)
}
// How far from the drop's centre anything of it is drawn, in points: the
// spikes as far as the march looks for them (the drop spread by a press
// that overshoots a little, the spikes' length and two points), or the cast
// shadow (thrown along the light, blurred at its tip and pushed out by half
// the spikes' outline push), whichever is farther; a glow's reach when the
// drop emits; and three points for the focus ring and the contact shadow's
// tail.
let fe_cap_reach = fn(cap: float, lgt: vec3, height: float, blur: float, glo: float, spike: float, outline: float) -> float {
let ln = normalize(lgt)
let r = cap * 0.5 * 1.12
let spikes = r + spike * height + 2.0
let thrown = height * 0.7 * length(ln.xy) / max(ln.z, 0.25)
let tip = clamp(blur, max(height * 0.35, 1.0), max(height, 1.0))
let cast = r + thrown + tip + 0.5 * spike * outline * height
return max(spikes, cast) + cap * 1.6 * step(0.001, glo) + 3.0
}
// How far the fader's quad reaches past its box, worked out alike in
// `vertex` and `pixel`: the drop's centre is never nearer an edge than half
// the width across, nor than the inset and half a cap along.
let fe_fader_room = fn(across: float, cap: float, inset: float, lgt: vec3, height: float, blur: float, glo: float, spike: float, outline: float) -> float {
return max(fe_cap_reach(cap, lgt, height, blur, glo, spike, outline) - min(across * 0.5, inset + cap * 0.5), 0.0)
}
// The fader laid over the sheet's ground without painting it: the share of
// the ground it darkens away and the light it puts back, premultiplied,
// measured against the ground toned as the fader is, so the bare ground
// comes out clear. Exact for any colour, where Cap.on_ground puts back only
// the channels lighter than the ground.
let fk_on_ground = fn(col: vec3, gnd: vec3) -> vec4 {
let dk = max(gnd - col, vec3(0.0, 0.0, 0.0)) / max(gnd, vec3(0.01, 0.01, 0.01))
let k = clamp(max(dk.x, max(dk.y, dk.z)), 0.0, 1.0)
return vec4(max(col - gnd * (1.0 - k), vec3(0.0, 0.0, 0.0)), k)
}
mod.widgets.SliderFader.draw_bg.vertex = fn() {
let across = mix(self.rect_size.y, self.rect_size.x, self.is_vertical)
let m = fe_fader_room(across, self.cap_px, self.inset_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline)
let g = fk_grow(self.rect_pos, self.rect_size, m)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.SliderFader.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let size = self.rect_size
let o = self.rect_pos
let vert = self.is_vertical
let live = 1.0 - self.disabled
let along = mix(size.x, size.y, vert)
let across = mix(size.y, size.x, vert)
let m = fe_fader_room(across, self.cap_px, self.inset_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5)), self.cap_effect_outline)
let p = self.pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
// track space: `a` along the travel from its zero (up the screen when
// the fader stands), `b` across
let ab = vec2(mix(p.x, size.y - p.y, vert), mix(p.y, p.x, vert))
let ob = mix(o.y, o.x, vert)
let inset = self.inset_px
let travel = max(along - inset * 2.0 - self.cap_px, 1.0)
let foot = inset + self.cap_px * 0.5
let mid = Finish.snap(ob + across * 0.5, px) - ob
let at = foot + self.slide_pos * travel
// the key light on screen, and turned into track space
let l = normalize(self.cap_light)
let lt = Cap.light(l, vert)
let li = self.cap_light_intensity
let ground = self.ground_color.rgb
let light_ink = self.light_color.rgb
let shadow_ink = self.shadow_color.rgb
let body = self.handle_color.rgb
let track_ink = mix(self.track_color.rgb, self.track_color_disabled.rgb, self.disabled)
let fill_ink = mix(self.fill_color.rgb, self.fill_color_disabled.rgb, self.disabled)
// held, the liquid's press spring spreads the drop 8% and settles it 14%
// lower, as the bench presses it; hovered or focused, it lifts as every
// key here does, and loses the lift while it is held
let press = self.cap_press * live
let lifted = max(self.hover, self.focus) * live * (1.0 - clamp(self.drag, 0.0, 1.0))
// THE GROUND: the slot the whole track long, round ended, the bar on its
// floor from the stop (or the origin) to the value. Disabled, the slot
// loses its inner shadow and occlusion: the sheet's disabled slot is a
// flat pale line, and on the pale ink that shading drew a dark groove.
let r = self.cap_px * 0.5
let hs = vec2(r, r)
let tw = min(self.track_size, across)
let tlen = max(along - inset * 2.0, 1.0)
let tbox = vec4(inset + tlen * 0.5, mid, tlen * 0.5, tw * 0.5)
var lo = foot + self.fill_lo * travel
var hi = foot + self.fill_hi * travel
if self.fill_lo < 0.0005 {
lo = inset
}
if self.fill_hi > 0.9995 {
hi = along - inset
}
var col = Cap.slot(ground, ab, tbox, tw * 0.5, 1.0, lo, hi, at, r, lt, px, 0.0, 0.0, track_ink, fill_ink, light_ink, shadow_ink, self.cap_inner * live, self.cap_ao * live)
// THE FIELD the pointer holds over the drop (none while disabled), the
// pointer from the drop's centre, and how much of the field reaches that
// centre: so much the body shrinks into its spikes. The spikes take over
// from the plain drop over the first 0.15 of the field.
let fld = clamp(self.cap_field, 0.0, 1.0) * live
let mag = vec2(self.cap_pointer_along, self.cap_pointer_across)
let fr = max(self.cap_effect_reach, 1.0)
let shrink = 1.0 - self.cap_effect_shrink * fld * max(1.0 - length(mag) / fr, 0.0)
let fw = smoothstep(0.0, 0.15, fld)
let spike = (self.cap_effect_height + self.cap_effect_height_abs / max(self.cap_height, 0.5))
let on = step(0.001, fw) * step(0.001, spike)
let height = self.cap_height * (1.0 - 0.14 * press)
let persp = self.cap_perspective
let q = ab - vec2(at, mid)
var cap = body
var cov = 0.0
var under = 0.0
var halo = 0.0
var d = 1000.0
// only near the drop: the rest of a long track skips it
if length(q) < fe_cap_reach(self.cap_px, self.cap_light, self.cap_height, self.cap_shadow_blur, self.cap_glow, spike, self.cap_effect_outline) {
// THE SPIKES, marched once per pixel while the field is up: the body
// the drop's radius (spread by the press) and height, both shrunk by
// the field; the lattice anchored to the pointer in the fader's
// frame (the pointer's offset from the drop and the drop's own
// travel both at the slide share, so a drag streams the spikes no
// faster than a hover slides them), drifting on the field's clock;
// the cones radiating from a centre on the drop's axis, at its
// middle for a fan of 2.
let size_m = vec2(r * (1.0 + 0.08 * press) * shrink, height * shrink)
var mk = vec4(1000.0, 0.0, 0.0, 0.0)
if on > 0.5 {
let sp = max(self.cap_effect_spacing, 1.0)
let tt = self.cap_time
let drift = vec2(0.35 * sin(tt * 0.6), 0.35 * cos(tt * 0.45)) * sp
let back = vec2(at * self.cap_effect_slide, mid) + mag * self.cap_effect_slide - drift
let centre = vec3(0.0, 0.0, -size_m.y * (2.0 / max(self.cap_effect_fan, 0.25) - 1.0))
mk = fe_march(q, size_m, mag, vec4(fld, fr, spike, self.cap_effect_base), vec4(back.x, back.y, sp, tt), centre, persp, self.cap_effect_normal, px)
}
// THE DROP: its dome read three times, each at the point the height
// read before brings toward the eye.
// a blob stays a circle while it moves unless the face lets the
// motion flow it (the toggle): then the head leads and the tail follows
let flow = self.cap_effect_flow
let motion = vec4(self.cap_stretch * flow, self.cap_dir, mix(1.0, self.cap_squash_along, flow), mix(1.0, self.cap_squash_across, flow))
let rs = r * shrink
var f = fe_dome(Cap.perspective(q, 1.0, -1.0, persp), hs, rs, motion, press, height, shrink)
f = fe_dome(Cap.perspective(q, f.x, f.w, persp), hs, rs, motion, press, height, shrink)
f = fe_dome(Cap.perspective(q, f.x, f.w, persp), hs, rs, motion, press, height, shrink)
var hgt = f.x
var grad = f.yz
d = f.w
var tip = 0.0
let hit = on * step(0.5, mk.w)
if on > 0.5 {
if hit > 0.5 {
// on the marched solid: its height and slope, inside, and how
// far up its spike the point stands
hgt = mix(hgt, mk.x, fw)
grad = mix(grad, vec2(mk.y, mk.z), fw)
d = mix(d, 1.0 - mk.w, fw)
tip = clamp((mk.x - 1.0) / max(spike, 0.05), 0.0, 1.0) * fw
} else {
// beside it: the drop gives way to the march's silhouette
hgt = mix(hgt, 0.0, fw)
grad = mix(grad, vec2(0.0, 0.0), fw)
d = mix(d, max(d, mk.x), fw)
}
}
// the drop lit by its material on the dome's normal...
let n = Cap.normal(grad, height, vert)
let v = Cap.view(q, hs, self.cap_reflection_perspective, vert)
cap = Cap.shade(n, l, v, hgt, body, 0.0, 0.0, 1.0, self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim, self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_metal, li, light_ink, light_ink, shadow_ink, ground)
let fq = fld * max(1.0 - length(q - mag) / fr, 0.0)
if hit > 0.5 {
// ...and the marched solid by the same material on its own normal
// and the ray the march took from the eye, both turned to the
// screen, with the fluid's own light over it
let nm = Cap.normal(vec2(mk.y, mk.z), size_m.y, vert)
let vc = normalize(vec3(-q.x, -q.y, size_m.y / max(persp, 0.04) * 1.5))
let vm = vec3(mix(vec2(vc.x, vc.y), vec2(vc.y, -vc.x), vert), vc.z)
var fc = Cap.shade(nm, l, vm, hgt, body, 0.0, 0.0, 1.0, self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim, self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_metal, li, light_ink, light_ink, shadow_ink, ground)
if abs(self.cap_effect_light) > 0.001 {
fc = fe_comp(fc, nm, vm, l, fq, tip, self.cap_effect_light, light_ink, li)
}
cap = mix(cap, fc, fw)
} else {
if spike > 0.001 && fld > 0.001 {
// the field darkens what the march did not reach
cap = cap * (1.0 - 0.4 * fq)
}
}
// turned grain and glow: off on this drop, still tunable
let grain = Cap.spun(q, hs, lt.xy) * self.cap_spun
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, hgt, body, light_ink, self.cap_glow)
// lifted, a shade lighter, as on every key here; disabled, most of
// the way to the case
cap = cap + g3(0.045 * lifted)
cap = mix(cap, self.handle_color_disabled.rgb, self.disabled * 0.6)
cov = Cap.cover(d, px)
// UNDER IT: the cast shadow, thrown from the top outline the bench
// gives this drop, along the trailing tail while it trails, pushed
// out by half the spikes' outline push as the field rises, darker
// while the drop is lifted and gone when disabled; and the contact
// shadow hugging whatever outline is drawn, spikes and all.
let rise = height
let push = 0.5 * spike * self.cap_effect_outline * self.cap_height * fld
let fb = Cap.sd_drop(q, rs, self.cap_stretch * flow, vec2(self.cap_dir, 0.0), press, mix(1.0, self.cap_squash_along, flow), mix(1.0, self.cap_squash_across, flow))
let tail = vec2(-self.cap_dir * r * 0.96 * self.cap_stretch * flow * step(0.001, self.cap_stretch), 0.0)
let thrown = fe_shadow(q, fb, r, vec3(0.6902, 0.6902, 1.0), tail, lt, rise, self.cap_shadow_blur, push, self.cap_shadow * mix(1.0, 1.375, lifted) * live, px)
let touch = Cap.contact(d, max(rise * 0.3, 1.0), self.cap_contact * mix(1.0, 0.4, self.disabled))
under = clamp(thrown + touch, 0.0, 1.0)
halo = Cap.glow(d, hs, self.cap_glow)
}
col = mix(col, col * shadow_ink, under * (1.0 - cov))
col = col + body * halo * 0.9 * (1.0 - cov)
col = mix(col, cap, cov)
let ex = pow(2.0, self.cap_exposure)
let out = fk_on_ground(Cap.tone(col, ex, self.cap_rolloff), Cap.tone(ground, ex, self.cap_rolloff))
// focus: one orange pixel two pixels out of the drop, as on every key;
// it gives way to the spikes as the field rises
return Finish.over(out, fk_focus(d, px, self.focus * live * (1.0 - fw)))
}
// ---------------------------------------------------------------------------
// DROP-DOWNS. The white key with a readout: a seam 22 points from its right
// end (a dark pixel and a light one) and a solid marker triangle, 6 by 4.
let fk_marker = fn(p: vec2, c: vec2) -> float {
let q = p - c
// a downward triangle 6 wide, 4 tall, centred on c
let e = max(abs(q.x) * 0.8 + (q.y + 2.0) * 0.6 - 2.4, -(q.y + 2.0))
return max(e, q.y - 2.0)
}
let fk_dropdown = fn(pos: vec2, size: vec2, o: vec2, px: float, hover: float, held: float, focus: float, dis: float) -> vec4 {
let m = 6.0
var out = fk_button(pos, size, o, px, hover, held, focus, dis, 0.0, 0.0, vec4(2.5, 2.5, 2.5, 2.5), 0.0, m)
let p = pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let live = 1.0 - dis
let dn = held * live * px
let dx = Finish.snap(o.x + size.x - 22.0, px) - o.x
let in_y = step(4.0 + dn, p.y) * step(p.y, size.y - 6.0 + dn)
let sd = fk_run(p.x, dx - px, dx) * in_y * 0.30
let sl = fk_run(p.x, dx, dx + px) * in_y * 0.9
out = Finish.over(out, vec4(0.0, 0.0, 0.0, sd))
out = Finish.over(out, vec4(sl, sl, sl, sl))
let mc = vec2(dx + 11.0, (size.y - 2.0) * 0.5 + dn)
let mk = Finish.cover(fk_marker(p, mc), px)
let ink = mix(vec3(0.114, 0.114, 0.114), vec3(0.663, 0.655, 0.643), dis)
return Finish.over(out, vec4(ink * mk, mk))
}
mod.widgets.DropDownFlat.padding = mod.turtle.Inset{left: 10.0 right: 30.0 top: 3.0 bottom: 5.0}
mod.widgets.DropDownFlat.clip_x = false
mod.widgets.DropDownFlat.clip_y = false
mod.widgets.DropDownFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.DropDownFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let held = max(self.down * self.hover, self.active)
return fk_dropdown(self.pos, self.rect_size, self.rect_pos, px, self.hover, held, self.focus, self.disabled)
}
mod.widgets.DropDown.draw_bg.color_disabled = #ecebe8
// The stock drop-down restates its own padding: its marker's room, again.
mod.widgets.DropDown.padding.left = 10.0
mod.widgets.DropDown.padding.right = 30.0
// The select wears a button of its own, built before this sheet: it is
// handed the drop-down's key, its grown quad and its room for the marker.
mod.widgets.Select.face.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.Select.face.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_dropdown(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled)
}
mod.widgets.Select.face.clip_x = false
mod.widgets.Select.face.clip_y = false
mod.widgets.Select.face.padding.right = 30.0
mod.widgets.MultiSelect.face.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.MultiSelect.face.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_dropdown(self.pos, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled)
}
mod.widgets.MultiSelect.face.clip_x = false
mod.widgets.MultiSelect.face.clip_y = false
mod.widgets.MultiSelect.face.padding.right = 30.0
mod.widgets.Select.clip_x = false
mod.widgets.Select.clip_y = false
mod.widgets.MultiSelect.clip_x = false
mod.widgets.MultiSelect.clip_y = false
// The combo box: the white well with a small key joined at its right end.
mod.widgets.ComboBox.padding = mod.turtle.Inset{left: 8.0 right: 28.0 top: 4.0 bottom: 4.0}
mod.widgets.ComboBox.input.min_height = 0.0
mod.widgets.ComboBox.clip_y = false
mod.widgets.ComboBox.draw_bg.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size + vec2(0.0, 2.0))
}
mod.widgets.ComboBox.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(0.0, 2.0))
let size = self.rect_size
let o = self.rect_pos
let live = 1.0 - self.disabled
var out = fk_field(p, size, o, px, self.hover, self.focus, self.disabled, 1.5)
let kx = Finish.snap(o.x + size.x - 21.0, px) - o.x
let klo = vec2(kx, Finish.snap(o.y, px) - o.y + 3.0 * px)
let khi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o - vec2(2.0 * px, 2.0 * px)
let held = self.down * self.hover * live
out = Finish.over(out, fk_cap(p, klo, khi, 1.0, px, 2.0 * px, 0.0, self.hover * live, held, self.disabled, 0.10))
let mc = vec2((klo.x + khi.x) * 0.5, (klo.y + khi.y) * 0.5 - px + held * px)
let mk = Finish.cover(fk_marker(p, mc), px)
let ink = mix(vec3(0.114, 0.114, 0.114), vec3(0.663, 0.655, 0.643), self.disabled)
return Finish.over(out, vec4(ink * mk, mk))
}
// The open list: a pale plate with a grey hairline rim; the row under the
// pointer a shade darker, the chosen row marked by a 3 point orange bar.
mod.widgets.PopupMenuFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + self.rect_size.x, px), Finish.snap(o.y + self.rect_size.y, px)) - o
let h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox(p, c, h, 2.0)
var col = vec3(0.961, 0.961, 0.961)
col = mix(col, vec3(1.0, 1.0, 1.0), fk_run(p.y, lo.y + px, lo.y + 2.0 * px))
col = mix(col, vec3(0.639, 0.635, 0.631), Finish.band(d, 0.0, px, px))
let a = Finish.cover(d, px)
return vec4(col * a, a)
}
mod.widgets.PopupMenuItem.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let c = self.rect_size * 0.5
let live = 1.0 - self.disabled
let d = fk_rbox(p, c, c - vec2(1.0, 0.0), 1.0)
let wash = Finish.cover(d, px) * (0.05 * self.hover * live + 0.03 * self.active)
var out = vec4(0.0, 0.0, 0.0, wash)
let bar = fk_run(p.x, 1.0, 4.0) * fk_run(p.y, 3.0, self.rect_size.y - 3.0) * self.active
return Finish.over(out, vec4(0.933 * bar, 0.353 * bar, 0.141 * bar, bar))
}
mod.widgets.PopupMenuItem.draw_text.color = #1d1d1d
// ---------------------------------------------------------------------------
// SEGMENTED CONTROL. A black display window; the answers printed in pale
// ink on the glass, the chosen one a lit orange pill with white ink.
mod.widgets.SegmentedControlFlat.segment_height = 24.0
mod.widgets.SegmentedControlFlat.segment_padding = 10.0
mod.widgets.SegmentedControlFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_display(self.pos * self.rect_size, self.rect_size, self.rect_pos, px, 2.0)
}
mod.widgets.SegmentedControlFlat.draw_pill.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o + vec2(2.0 * px, 2.0 * px)
let hi = vec2(Finish.snap(o.x + self.rect_size.x, px), Finish.snap(o.y + self.rect_size.y, px)) - o - vec2(2.0 * px, 2.0 * px)
let h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox(p, c, h, h.y)
var col = vec3(0.933, 0.353, 0.141)
col = mix(col, vec3(0.835, 0.251, 0.059), step(c.y, p.y) * 0.35)
let a = Finish.cover(d, px)
return vec4(col * a, a)
}
mod.widgets.SegmentedControlFlat.draw_text.color = #cfcfcf
mod.widgets.SegmentedControlFlat.draw_text_selected.color = #ffffff
// TABS. A row of keys joined by seams: each tab a white key with a dark
// pixel at each of its ends, so two neighbours meet in a 2 pixel seam; the
// hovered one a hair brighter; the one in use latched in, its legend in the
// deep signal orange.
let fk_tabkey = fn(pos: vec2, size: vec2, o: vec2, px: float, hover: float, active: float) -> vec4 {
let m = 4.0
let p = pos * (size + vec2(0.0, m))
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o
let h = (hi - lo) * 0.5
let c = lo + h
// below the row only: the drop and the contact line
let under = step(hi.y, p.y)
let dr = Finish.drop(p, c, h, 0.0, vec2(0.0, 1.0), 1.6) * mix(0.13, 0.19, hover) * (1.0 - active) * under
var out = vec4(0.0, 0.0, 0.0, dr)
let con = fk_run(p.y, hi.y, hi.y + px) * mix(0.32, 0.18, active)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, con))
out = Finish.over(out, fk_cap(p, lo, hi, 0.0, px, 3.0 * px, 0.0, hover, active, 0.0, 0.0))
// the seam halves at both ends
let sm = (fk_run(p.x, lo.x, lo.x + px) + fk_run(p.x, hi.x - px, hi.x)) * step(p.y, hi.y) * 0.9
return Finish.over(out, vec4(0.714 * sm, 0.714 * sm, 0.706 * sm, sm))
}
mod.widgets.Tabs.height = 26.0
mod.widgets.Tabs.clip_x = false
mod.widgets.Tabs.clip_y = false
mod.widgets.Tabs.draw_bg.pixel = fn() {
return vec4(0.0, 0.0, 0.0, 0.0)
}
mod.widgets.Tabs.draw_tab.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size + vec2(0.0, 4.0))
}
mod.widgets.Tabs.draw_tab.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_tabkey(self.pos, self.rect_size, self.rect_pos, px, 0.0, 0.0)
}
mod.widgets.Tabs.draw_tab_hover.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size + vec2(0.0, 4.0))
}
mod.widgets.Tabs.draw_tab_hover.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_tabkey(self.pos, self.rect_size, self.rect_pos, px, 1.0, 0.0)
}
mod.widgets.Tabs.draw_tab_active.vertex = fn() {
self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size + vec2(0.0, 4.0))
}
mod.widgets.Tabs.draw_tab_active.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return fk_tabkey(self.pos, self.rect_size, self.rect_pos, px, 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 = #1d1d1d
mod.widgets.Tabs.draw_mark.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let c = vec2(Finish.snap(o.x + self.rect_size.x * 0.5, px), Finish.snap(o.y + self.rect_size.y * 0.5 - 1.0, px)) - o
// 7 points across, a 1 point stroke; the plus is the cross turned
let a = mix(0.7853981634, 0.0, self.plus)
let r = mix(3.2, 3.5, self.plus)
let u = vec2(cos(a), sin(a)) * r
let v = vec2(-sin(a), cos(a)) * r
let dd = min(fk_seg(p, c - u, c + u), fk_seg(p, c - v, c + v)) - 0.5
let k = Finish.cover(dd, px) * self.color.a
return vec4(self.color.rgb * k, k)
}
mod.widgets.Tabs.draw_text.color = #3a3937
mod.widgets.Tabs.draw_text_active.color = #b53c0b
// The dock's tabs: the bar is the case with an engraved line at its foot;
// the tab in use is a white face standing on it, the others printed words
// that brighten a little under the pointer.
mod.widgets.Tab.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 3.0 bottom: 3.0}
mod.widgets.Tab.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y + 2.0, px)) - o
let hi = vec2(Finish.snap(o.x + self.rect_size.x, px), Finish.snap(o.y + self.rect_size.y, px)) - o + vec2(0.0, 3.0)
let h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox(p, c, h, 2.5)
let t = clamp((p.y - lo.y) / max(self.rect_size.y - 2.0, 1.0), 0.0, 1.0)
var face = mix(vec3(0.961, 0.961, 0.961), vec3(0.941, 0.941, 0.941), t)
face = mix(face, vec3(1.0, 1.0, 1.0), fk_run(p.y, lo.y, lo.y + px))
face = mix(face, vec3(0.62, 0.62, 0.615), Finish.band(d, 0.0, px, px) * 0.5)
let a = Finish.cover(d, px) * self.active
let lift = Finish.cover(d, px) * 0.25 * self.hover * (1.0 - self.active)
return Finish.over(vec4(lift, lift, lift, lift), vec4(face * a, a))
}
mod.widgets.TabBar.draw_fill.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let yb = Finish.snap(self.rect_pos.y + self.rect_size.y, px) - self.rect_pos.y
var out = vec4(0.0, 0.0, 0.0, 0.03)
let dk = fk_run(p.y, yb - 2.0 * px, yb - px) * 0.14
let lt = fk_run(p.y, yb - px, yb) * 0.6
out = Finish.over(out, vec4(0.0, 0.0, 0.0, dk))
return Finish.over(out, vec4(lt, lt, lt, lt))
}
// ---------------------------------------------------------------------------
// PROGRESS. A 6 point track cut into the case in two flat dark tones, the
// fill flat signal orange with one pixel of light on its top. The unknown
// amount is a still ladder of lit cells: a face may not run a clock.
mod.widgets.ProgressBarFlat.height = 7.0
mod.widgets.ProgressBarFlat.draw_bg.thickness = uniform(7.0)
mod.widgets.ProgressBarFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let w = self.rect_size.x
let th = Finish.snap(min(self.thickness, self.rect_size.y) - px, px)
let top = Finish.snap(o.y + (self.rect_size.y - th - px) * 0.5, px) - o.y
let x0 = Finish.snap(o.x + w * self.start, px) - o.x
let x1 = Finish.snap(o.x + w * self.end, px) - o.x
let h = vec2((x1 - x0) * 0.5, th * 0.5)
let c = vec2(x0, top) + h
let r = 1.0
let d = fk_rbox(p, c, h, r)
let dis = step(4.5, self.intent)
var ink = vec3(0.933, 0.353, 0.141)
if self.intent > 0.5 {
ink = self.fill_color().rgb
}
ink = mix(ink, vec3(0.72, 0.71, 0.70), dis)
let xv = x0 + (x1 - x0) * clamp(self.value, 0.0, 1.0)
var filled = step(p.x, xv) * step(0.0, self.value)
var ghost = 0.0
var unknown = 0.0
if self.value < 0.0 {
// a still ladder in the black well: cells of 4 points split by one
// dark pixel, every cell at a tenth of lit, and ONE run of six lit
// cells from 30 % of the way along (a face may not run a clock)
let rel = p.x - x0
let cell = floor(rel / 4.0)
let gapx = step(4.0 - px, rel - cell * 4.0)
let c0 = floor((x1 - x0) * 0.3 / 4.0)
filled = step(c0 - 0.5, cell) * step(cell, c0 + 5.5) * (1.0 - gapx)
ghost = (1.0 - gapx) * 0.10
unknown = 1.0
}
let upper = step(p.y, top + th * 0.5)
let track = mix(mix(vec3(0.216, 0.216, 0.216), vec3(0.125, 0.125, 0.125), upper), vec3(0.102, 0.102, 0.102), unknown)
var col = mix(mix(track, ink, ghost), ink, filled)
col = mix(col, Finish.lift(ink, 0.45), filled * fk_run(p.y, top, top + px) * 0.8)
col = mix(col, vec3(0.0, 0.0, 0.0), (1.0 - filled) * fk_run(p.y, top, top + px) * 0.3)
// a stacked segment is drawn without its track: its fill alone
let a = Finish.cover(d, px) * max(self.track, filled)
var out = fk_catch(p, c, h, r, px, 0.8 * self.track)
out = Finish.over(out, vec4(col * a, a))
return out * self.opacity
}
// ---------------------------------------------------------------------------
// DISPLAYS. A ferrofluid display, in the knobs' material (THE WELL AND THE
// LENS): the case curves down round the box into a well with round
// corners, a lens of clear glass lies in it flush with the case, and on the
// well's floor under the glass stand the figures in black ferrofluid. A lit segment is a lozenge of liquid, fullest
// along its middle; where two strokes turn a corner the liquid rounds it
// outside and fills it in a round inside. The liquid parts at the middle of
// a figure where an upright hung from the top bar meets only the bar there,
// and where a 0's uprights meet in line: a 5 is a hook over a cup, a 2 a
// hook over a C, a 0 a cup over a cup. Where the bar meets both uprights of
// a side they hold together, and a run of uprights on one side only (a 1,
// a 7) holds through the middle in a gentle neck. A free end swells into a
// drop, the fullest at the foot of the figure, where the liquid runs down
// to it; the edge is never quite straight. The colon and the point are
// drops, and a figure with a point after it draws a little narrower to give
// the drop its room. An unlit segment is bare cell, or, when a ghost is
// asked for, the faint film the liquid leaves where it stood. The liquid is
// a deep black with a soft grey sheen in broad patches, stronger toward the
// foot, and a short soft shadow under it. Captions on a display take the
// screen's ink, which is the liquid's black.
// A quad grown by `m` past its box `rp`..`rp + rs`, for the drops and
// shadows that reach past it. A widget that draws in a turtle of its own
// box is clipped to that box: where the clip lies on the box's own edge
// (`lo` and `hi`, the turtle's box as this quad sees it) it is let out by
// `m`, and where a parent clips inside it, as a scrolled view does, that
// clip holds.
let fe_grown_vertex = fn(rp: vec2, rs: vec2, gp: vec2, clip: vec4, shift: vec2, view_clip: vec4, m: float, lo: vec2, hi: vec2) -> vec4 {
let g = fk_grow(rp, rs, m)
let own_lo = step(clip.xy, lo + vec2(0.01, 0.01))
let own_hi = step(hi - vec2(0.01, 0.01), clip.zw)
let p = clamp(gp * g.zw + g.xy, clip.xy - own_lo * m, clip.zw + own_hi * m)
return vec4(clamp(p + shift, view_clip.xy, view_clip.zw), g.xy)
}
// The well's mouth from the screen's bezel: two and a quarter times it up
// to the default 2 (4.5 points), a tenth of a point for each point beyond,
// so the widest bezel (12) is a mouth of 5.5 and the lens still clears the
// figures by a point and a half inside the screen's padding; none is a
// lens filling the box, its mouth two points round it.
let fe_frame = fn(bezel: float) -> float {
return max(min(bezel * 2.25, 4.5 + (bezel - 2.0) * 0.1), 0.0)
}
// The lens's corner: the box's corner less half the mouth, but never under
// a tenth of the lens's height, as the clocks round their windows.
let fe_cell_radius = fn(ro: float, fs: float, hc: vec2) -> float {
return clamp(max(ro - fs * 0.5, min(hc.x, hc.y) * 0.2), 1.0, min(hc.x, hc.y))
}
// The lens of a display box `size` at window position `o`, its well's
// mouth `f` in from the box's edge: its middle and half size, snapped.
// The housing at `p` (points from the box's top left), `r` the box's
// corner: the case curving down into the well from the box's edge and the
// well's floor under the lens (with scan lines when asked). The glass and
// the case's shadow on the floor lie over the figures (the screen's glass
// below).
let fe_lens_box = fn(size: vec2, o: vec2, px: float, f: float) -> vec4 {
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o
let h = (hi - lo) * 0.5
let fs = Finish.snap(f, px)
return vec4(lo + h, max(h - vec2(fs, fs), vec2(0.5, 0.5)))
}
let fe_housing = fn(p: vec2, size: vec2, o: vec2, px: float, r: float, f: float, scan: float) -> vec4 {
let lb = fe_lens_box(size, o, px, f)
let c = lb.xy
let hc = lb.zw
let fs = Finish.snap(f, px)
let rc = fe_cell_radius(clamp(r, 0.0, min(hc.x, hc.y) + fs), fs, hc)
let dc = fk_rbox(p, c, hc, rc)
var out = fk_mouth(dc, fk_rbox_n(p, c, hc, rc), max(fs, 2.0), px, 1.0, 0.0)
var cell = fk_floor(0.0, 0.0)
if scan > 0.0 {
cell = cell * Finish.scan(p.y - (c.y - hc.y), px, 3.0 * px, scan)
}
let a = fk_aa(dc, px)
return Finish.over(out, vec4(cell * a, a))
}
mod.widgets.ScreenView.draw_bg.border_radius = uniform(4.5)
// The quad reaches 3 points past the box, for the well's mouth round a lens
// that fills the box (no bezel).
mod.widgets.ScreenView.draw_bg.vertex = fn() {
let m = 3.0
let v = fe_grown_vertex(self.rect_pos, self.rect_size, self.geom.pos, self.draw_clip, self.draw_list.view_shift, self.draw_list.view_clip, m, self.rect_pos, self.rect_pos + self.rect_size)
self.pos = (v.xy - v.zw) / (self.rect_size + vec2(2.0 * m, 2.0 * m))
self.world = self.draw_list.view_transform * vec4(v.x, v.y, self.draw_depth + self.draw_call.zbias, 1.0)
self.vertex_pos = self.draw_pass.camera_projection * (self.draw_pass.camera_view * self.world)
}
mod.widgets.ScreenView.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 3.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
return fe_housing(p, self.rect_size, self.rect_pos, px, self.border_radius, fe_frame(self.bezel), self.scan)
}
// The screen's glass, over the figures: the case's shadow falling in under
// the well's upper rim, the lens's rim and its light, as on the knobs.
mod.widgets.ScreenView.draw_glass.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let fs = Finish.snap(fe_frame(self.bezel), px)
let lb = fe_lens_box(self.rect_size, self.rect_pos, px, fs)
let c = lb.xy
let hc = lb.zw
let rc = fe_cell_radius(clamp(self.face_radius, 0.0, min(hc.x, hc.y) + fs), fs, hc)
let dc = fk_rbox(p, c, hc, rc)
let n = fk_rbox_n(p, c, hc, rc)
let k = min(hc.x, hc.y)
var out = vec4(0.0, 0.0, 0.0, fk_rim_shade(p, c, hc, rc, k, 0.0, 0.0) * fk_aa(dc, px))
out = Finish.over(out, fk_lens_rim(dc, n, k, px, 0.0))
return Finish.over(out, fk_lens_light(p, c - hc, dc, n, k, px, 0.0, 0.0, 0.0))
}
// THE NEEDLE METER, a display in the knobs' material: the case curves down
// round its box into a well three points deep, the scale, the red zone,
// the needle and its cover drawn as the stock meter draws them on the
// well's floor under the lens, and the case's shadow and the lens's light
// over them.
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 fs = Finish.snap(3.0, px)
let lb = fe_lens_box(self.rect_size, self.rect_pos, px, fs)
let c = lb.xy
let hc = lb.zw
let rc = fe_cell_radius(clamp(self.border_radius, 0.0, min(hc.x, hc.y) + fs), fs, hc)
let dc = fk_rbox(p, c, hc, rc)
let n = fk_rbox_n(p, c, hc, rc)
let k = min(hc.x, hc.y)
let rr = self.scale_radius()
let q = p - self.pivot()
let r = length(q)
let sweep = self.sweep * 0.017453293
let t = atan2(q.x, 0.0 - q.y) / sweep + 0.5
var col = fk_floor(0.0, 0.0)
let arc = Finish.cover(abs(r - rr) - px * 0.5, px) * step(-0.0001, t) * step(t, 1.0001)
let m = max(floor(self.minors + 0.5), 1.0)
let nt = max(floor(self.majors + 0.5), 1.0) * m
let kt = floor(clamp(t, 0.0, 1.0) * nt + 0.5)
let ak = (kt / nt - 0.5) * sweep
let major = 1.0 - step(0.5, kt - m * floor(kt / m + 0.0001))
let radial = max(rr - mix(self.tick_minor, self.tick_major, major) - r, r - rr)
let tick = Finish.cover(abs(q.x * cos(ak) + q.y * sin(ak)) - px * mix(0.5, 1.0, major), px) * Finish.cover(radial, px) * step(0.0, -q.y * cos(ak) + q.x * sin(ak))
col = mix(col, self.color_scale.rgb, max(arc, tick))
if self.zone < 0.999 {
let zt = Finish.cover((self.zone - t) * sweep * r, px) * Finish.cover((t - 1.0) * sweep * r, px)
col = mix(col, self.color_zone.rgb, zt * Finish.cover(max(rr + px - r, r - rr - 2.5), px))
}
let na = self.needle_angle(self.value)
let nd = vec2(sin(na), -cos(na))
let along = dot(q, nd)
let needle = Finish.cover(abs(q.x * nd.y - q.y * nd.x) - px * 0.5, px) * Finish.cover(along - (rr + 3.0), px) * Finish.cover(-along - max(self.pivot_size - px, 0.0), px)
col = mix(col, self.color_needle.rgb, needle)
let pd = r - self.pivot_size
col = mix(col, mix(self.color_needle.rgb, col, 0.35), Finish.cover(pd, px))
col = mix(col, col * 0.55, Finish.band(pd, 0.0, px, px))
let a = fk_aa(dc, px)
var out = Finish.over(fk_mouth(dc, n, fs, px, 1.0, 0.0), vec4(col * a, a))
out = Finish.over(out, vec4(0.0, 0.0, 0.0, fk_rim_shade(p, c, hc, rc, k, 0.0, 0.0) * a))
out = Finish.over(out, fk_lens_rim(dc, n, k, px, 0.0))
return Finish.over(out, fk_lens_light(p, c - hc, dc, n, k, px, 0.0, 0.0, 0.0)) * self.opacity
}
// THE LIQUID FIGURES. Distances in digit heights, y down, negative inside.
// A lozenge of liquid from `a` to `b`: `ra` round at `a` and `rb` at `b`,
// fuller by `bulge` along its middle.
let fe_lq_seg = fn(q: vec2, a: vec2, b: vec2, ra: float, rb: float, bulge: float) -> float {
let pa = q - a
let ba = b - a
let t = clamp(dot(pa, ba) / max(dot(ba, ba), 0.000001), 0.0, 1.0)
return length(pa - ba * t) - (mix(ra, rb, t) + bulge * sin(t * 3.14159265))
}
// 1 when bit `b` (a power of two) of the mask is set.
let fe_lq_bit = fn(mask: float, b: float) -> float {
return step(0.5, fract(floor(mask / b) * 0.5))
}
// 1 for a node exactly one lit stroke reaches: a free end.
let fe_lq_end = fn(n: float) -> float {
return step(0.5, n) * step(n, 1.5)
}
// A corner of liquid at the node `n`, its strokes leaving along `u` (`tu`
// round across it) and `v` (`tv` round across it): rounded `ro` outside,
// filled in a round of `ri` inside, as liquid draws into a corner, and out
// to `len` along each stroke, where the stroke itself takes over.
let fe_lq_corner = fn(q: vec2, n: vec2, u: vec2, v: vec2, tu: float, tv: float, ro: float, ri: float, len: float) -> float {
let l = vec2(dot(q - n, u), dot(q - n, v))
let po = vec2(ro - tv, ro - tu) - l
let d_out = length(max(po, vec2(0.0, 0.0))) + min(max(po.x, po.y), 0.0) - ro
let pw = vec2(tv + ri, tu + ri) - l
let d_in = length(max(pw, vec2(0.0, 0.0))) + min(max(pw.x, pw.y), 0.0) - ri
return max(max(d_out, -d_in), max(l.x, l.y) - len)
}
// A tuft on a drop at `nc`, `rb` round: the magnet's field draws a crown
// of five solid cones out of it, fanned about `out`, the middle ones the
// longest, `l` long at most. The drop's distance with them.
let fe_lq_tuft = fn(q: vec2, nc: vec2, rb: float, out: vec2, l: float) -> float {
let w = q - nc
let an = atan2(w.y, w.x) - atan2(out.y, out.x)
let ph = an - 6.2831853 * floor((an + 3.14159265) / 6.2831853)
let step_a = 0.56
let i = floor(ph / step_a + 0.5)
let tri = max(1.0 - abs(ph / step_a - i) * 2.0, 0.0)
let fan = 1.0 - smoothstep(2.3, 2.9, abs(ph / step_a))
let ext = l * pow(tri, 0.6) * fan * (1.0 - 0.14 * abs(i))
return (length(w) - rb - ext) * 0.7
}
// The nodes of a figure `w` wide with strokes `r` round, held in from the
// box by the ripple (and at the top by an upper end's drop), so that only a
// foot's drop reaches toward its edge: x and y the left and right
// uprights, z the top bar, w the bottom bar.
let fe_lq_nodes = fn(w: float, r: float) -> vec4 {
let pad = r * 0.25
return vec4(r + pad, w - r - pad, r * 1.35, 1.0 - r - pad)
}
// A figure of liquid with its top left at the origin, `w` wide, `r` the
// stroke's half thickness, `gp` the clear gap where the liquid parts: the
// lit strokes of `mask` (a the top, then clockwise, g the middle), the bars
// a little slimmer than the uprights and the middle one the slimmest, the
// middle bar a little below half way. Two lit strokes turning a corner
// round it. At the middle of each side: where the bar meets both uprights
// the three hold together, the liquid filling in round the joints; an
// upright hung from the top bar parts from the bar when the bar is all
// that is lit there, the drop it ends in and the bar's end drawn apart on
// the diagonal (an upright with nothing over it turns into the bar
// instead); two uprights in line part, the upper one's drop hanging clear
// over the lower one's end, only when the other side is such a pair as
// well (a 0), and otherwise hold through in a neck. A free end swells into
// a drop: a bar's end only rounds, an upright's upper end swells a little,
// and a foot, the liquid run down to it, is the fullest (but not beside a
// point, `pt`). The first free end along the top grows a tuft, `tuft` of
// its length, its crown leaning up and to the left as the field draws it,
// and held inside the figure's box.
let fe_lq_digit = fn(q: vec2, mask: float, w: float, r: float, pt: float, tuft: float, gp: float) -> float {
let nd = fe_lq_nodes(w, r)
let x0 = nd.x
let x1 = nd.y
let y0 = nd.z
let y1 = nd.w
let ym = 0.52
let foot = 1.0 - r * 0.25
let la = fe_lq_bit(mask, 1.0)
let lb = fe_lq_bit(mask, 2.0)
let lc = fe_lq_bit(mask, 4.0)
let ld = fe_lq_bit(mask, 8.0)
let le = fe_lq_bit(mask, 16.0)
let lf = fe_lq_bit(mask, 32.0)
let lg = fe_lq_bit(mask, 64.0)
let far = 10.0
let rh = r * 0.94
let rg = r * 0.88
let ru = r * 1.1
let rf = r * 1.25
let rn = r * 0.85
let ro = r * 1.7
let ri = r * 0.85
let len = ro + r
let bu = r * 0.1
let bb = r * 0.06
let k = r * 0.5
// the corners: the four outer ones, and below the middle a turn from
// the bar into the upright under it when the one above is out
let ca = la * lf
let cb = la * lb
let ce = ld * le
let cf = ld * lc
let cl = le * lg * (1.0 - lf)
let cr = lc * lg * (1.0 - lb)
// and above it a turn from the bar up into an upright with nothing
// over it (a 4), which would otherwise part as a lone stub
let ul = lf * lg * (1.0 - le) * (1.0 - la)
let ur = lb * lg * (1.0 - lc) * (1.0 - la)
// the middle of each side: the three strokes held together; two
// uprights in line held through in a neck, unless the other side is
// such a pair too (a 0); else the upright from the top parted from the
// bar beside it, or from the upright under it
let tee_l = lf * le * lg
let tee_r = lb * lc * lg
let neck_l = lf * le * (1.0 - lg) * (1.0 - lb * lc)
let neck_r = lb * lc * (1.0 - lg) * (1.0 - lf * le)
let top_free = ym + 0.07
let dd = (ru + rg + gp) * 0.7071
let y_alone = mix(ym - ru * 0.6, top_free - r * 0.95 - gp - ru, le)
let f_bot = mix(mix(mix(y_alone, ym - dd, lg * (1.0 - le)), ym, max(tee_l, neck_l)), ym - ro, ul)
let y_alone_r = mix(ym - ru * 0.6, top_free - r * 0.95 - gp - ru, lc)
let b_bot = mix(mix(mix(y_alone_r, ym - dd, lg * (1.0 - lc)), ym, max(tee_r, neck_r)), ym - ro, ur)
let e_top = mix(mix(top_free, ym, max(tee_l, neck_l)), ym + ro, cl)
let c_top = mix(mix(top_free, ym, max(tee_r, neck_r)), ym + ro, cr)
let e_tr = mix(mix(r * 0.95, mix(r, rn, neck_l), max(tee_l, neck_l)), r, cl)
let c_tr = mix(mix(r * 0.95, mix(r, rn, neck_r), max(tee_r, neck_r)), r, cr)
let rfr = mix(rf, r, pt)
let e_bot = mix(foot - rf, y1 - ro, ce)
let c_bot = mix(foot - rfr, y1 - ro, cf)
let g_l = mix(mix(x0, x0 + dd, lf * (1.0 - le)), x0 + ro, max(cl, ul))
let g_r = mix(mix(x1, x1 - dd, lb * (1.0 - lc)), x1 - ro, max(cr, ur))
var d = far
d = min(d, fe_lq_seg(q, vec2(x0 + ro * ca, y0), vec2(x1 - ro * cb, y0), rh, rh, bb) + (1.0 - la) * far)
let sb = fe_lq_seg(q, vec2(x1, y0 + ro * cb), vec2(x1, b_bot), r, mix(r, rn, neck_r), bu) + (1.0 - lb) * far
let sc = fe_lq_seg(q, vec2(x1, c_top), vec2(x1, c_bot), c_tr, r, bu) + (1.0 - lc) * far
let sf = fe_lq_seg(q, vec2(x0, y0 + ro * ca), vec2(x0, f_bot), r, mix(r, rn, neck_l), bu) + (1.0 - lf) * far
let se = fe_lq_seg(q, vec2(x0, e_top), vec2(x0, e_bot), e_tr, r, bu) + (1.0 - le) * far
let run_r = min(sb, sc)
let run_l = min(sf, se)
d = min(d, min(run_l, run_r))
d = min(d, fe_lq_seg(q, vec2(x0 + ro * ce, y1), vec2(x1 - ro * cf, y1), rh, rh, bb) + (1.0 - ld) * far)
// the middle bar, filled in round its joints where it meets a run of
// three, with a little more liquid gathered there
var dg = fe_lq_seg(q, vec2(g_l, ym), vec2(g_r, ym), rg, rg, bb) + (1.0 - lg) * far
dg = fe_smin(dg, run_l + (1.0 - tee_l) * far, ri)
dg = fe_smin(dg, run_r + (1.0 - tee_r) * far, ri)
dg = fe_smin(dg, length(q - vec2(x0 + r * 0.2, ym)) - r * 1.15 + (1.0 - tee_l) * far, k)
dg = fe_smin(dg, length(q - vec2(x1 - r * 0.2, ym)) - r * 1.15 + (1.0 - tee_r) * far, k)
d = min(d, dg)
d = min(d, fe_lq_corner(q, vec2(x0, y0), vec2(1.0, 0.0), vec2(0.0, 1.0), rh, r, ro, ri, len) + (1.0 - ca) * far)
d = min(d, fe_lq_corner(q, vec2(x1, y0), vec2(-1.0, 0.0), vec2(0.0, 1.0), rh, r, ro, ri, len) + (1.0 - cb) * far)
d = min(d, fe_lq_corner(q, vec2(x0, y1), vec2(1.0, 0.0), vec2(0.0, -1.0), rh, r, ro, ri, len) + (1.0 - ce) * far)
d = min(d, fe_lq_corner(q, vec2(x1, y1), vec2(-1.0, 0.0), vec2(0.0, -1.0), rh, r, ro, ri, len) + (1.0 - cf) * far)
d = min(d, fe_lq_corner(q, vec2(x0, ym), vec2(1.0, 0.0), vec2(0.0, 1.0), rg, r, ro, ri, len) + (1.0 - cl) * far)
d = min(d, fe_lq_corner(q, vec2(x1, ym), vec2(-1.0, 0.0), vec2(0.0, 1.0), rg, r, ro, ri, len) + (1.0 - cr) * far)
d = min(d, fe_lq_corner(q, vec2(x0, ym), vec2(1.0, 0.0), vec2(0.0, -1.0), rg, r, ro, ri, len) + (1.0 - ul) * far)
d = min(d, fe_lq_corner(q, vec2(x1, ym), vec2(-1.0, 0.0), vec2(0.0, -1.0), rg, r, ro, ri, len) + (1.0 - ur) * far)
// the drops on the free ends: along the top, where an upright from the
// top ends short of the middle, and at the feet
let free_a = fe_lq_end(la + lf)
let free_b = fe_lq_end(la + lb)
let droop = vec2(0.0, r * 0.1)
d = fe_smin(d, length(q - vec2(x0, y0) - droop * la) - mix(r * 1.04, ru, lf) + (1.0 - free_a) * far, k)
d = fe_smin(d, length(q - vec2(x1, y0) - droop * la) - mix(r * 1.04, ru, lb) + (1.0 - free_b) * far, k)
d = fe_smin(d, length(q - vec2(x0, f_bot)) - ru + (1.0 - lf * (1.0 - max(max(tee_l, neck_l), ul))) * far, k)
d = fe_smin(d, length(q - vec2(x1, b_bot)) - ru + (1.0 - lb * (1.0 - max(max(tee_r, neck_r), ur))) * far, k)
d = fe_smin(d, length(q - vec2(x0, foot - rf)) - rf + (1.0 - le * (1.0 - ld)) * far, k)
d = fe_smin(d, length(q - vec2(x1, foot - rfr)) - rfr + (1.0 - lc * (1.0 - ld)) * far, k)
// the tuft: on the top left end when it is free, else on the top right
// when the top left is bare; its drop grows into a full round bead, hung
// low enough that the crown stays inside the box
if tuft > 0.01 {
let on_a = free_a
let on_b = free_b * (1.0 - la) * (1.0 - lf)
let out = normalize(vec2(-0.5, -1.0))
let rt = mix(r * 1.04, r * 1.15, tuft)
let lt = r * 1.15 * 0.66 * tuft
let yt = mix(y0, max(y0, rt + lt * 0.95 + 0.004), tuft)
let ta = fe_lq_tuft(q, vec2(x0 + r * 0.1, yt), rt, out, lt) + (1.0 - on_a) * far
let tb = fe_lq_tuft(q, vec2(x1 - r * 0.1, yt), rt, out, lt) + (1.0 - on_b) * far
d = fe_smin(d, min(ta, tb), r * 0.3)
}
return d
}
// The edge's slow ripple, -1..1, turned by `ph` so no two figures match.
let fe_lq_ripple = fn(q: vec2, ph: float) -> float {
return sin(q.x * 17.0 + q.y * 5.0 + ph) * 0.6 + sin(q.y * 13.0 - q.x * 11.0 + ph * 1.7) * 0.4
}
// Where a figure starts in its cell: a fifth of the room its advance
// leaves, the rest after it.
let fe_lq_x0 = fn(span: float, w: float) -> float {
return max(span - w, 0.0) * 0.2
}
// One readout cell, from the cell's top left: a figure with the point's
// drop after it, or the colon's two drops. `span` is the cell's advance
// and `px` a device pixel. The liquid parts by a clear gap of 0.045 of the
// height and never under 1.6 device pixels. The point is a drop near two
// thirds of a stroke across, its foot on the figures' line, in the middle
// of the gap to the next figure; the figure before it draws narrower by
// what the drop and a clear space each side of it need (0.045 of the
// height, never under 1.75 device pixels), and neither the point nor the
// edge facing it ripples, so nothing closes that space.
let fe_lq_cell = fn(q: vec2, mask: float, pt: float, colon: float, span: float, w: float, r: float, tuft: float, px: float) -> float {
let ph = mask * 1.37 + pt * 2.3 + colon * 4.1
var rip = r * 0.16 * fe_lq_ripple(q, ph)
var d = 10.0
if colon > 0.5 {
let cx = span * 0.5
let rd = r * 1.05
d = min(length((q - vec2(cx, 0.32)) * vec2(1.0, 0.92)) - rd, length((q - vec2(cx, 0.68)) * vec2(1.0, 0.92)) - rd) + rip
} else {
let gp = max(0.045, px * 1.6)
let rp = r * 0.62
let clr = max(0.045, px * 1.75)
let on = step(0.5, pt)
let wd = mix(w, min(w, span - 2.0 * (rp + clr)), on)
let dq = q - vec2(fe_lq_x0(span, w), 0.0)
let pc = vec2(wd + (span - wd) * 0.5, 1.0 - r * 0.25 - rp)
rip = rip * mix(1.0, smoothstep(rp * 1.5, rp * 4.0, length(dq - pc)), on)
d = fe_lq_digit(dq, mask, wd, r, on, tuft, gp) + rip
if pt > 0.5 {
d = min(d, length(dq - pc) - rp)
}
}
return d
}
// The liquid's face for the distance `d` and its gradient `g`, in units in
// which `r` is the stroke's half thickness and `px` a device pixel, `at`
// where on the figure (y down, 0..1 over its height): a short soft shadow
// on the cell just under it, then the black, with a soft grey sheen in
// broad loose patches over its body, stronger toward the foot where the
// liquid is fullest, kept off the edge; the edge antialiased and nothing
// more.
let fe_lq_ink = fn(d: float, g: vec2, r: float, px: float, ink: vec4, at: vec2) -> vec4 {
let gl = max(length(g), 0.0001)
let dn = d / gl
let nrm = g / gl
let dsh = dn - dot(nrm, vec2(0.04, 0.3) * r)
let sh = (1.0 - smoothstep(-r * 0.1, r * 0.55, dsh)) * 0.12
let out = vec4(0.0, 0.0, 0.0, sh * ink.a)
let body = smoothstep(r * 0.3, r * 0.85, -dn)
let low = 0.35 + 0.65 * smoothstep(0.3, 0.95, at.y)
let patch = smoothstep(0.4, 0.8, Finish.noise(at * vec2(4.0, 5.0) + vec2(3.7, 1.3)))
let col = mix(ink.rgb, vec3(0.40, 0.41, 0.44), body * low * patch * 0.3)
let a = Finish.cover(dn, px) * ink.a
return Finish.over(out, vec4(col * a, a))
}
// READOUTS. Every cell a figure of liquid (above), wider than the stock
// digit, as the clocks' figures are; the slant leans it, a disabled
// readout fades, and a ghost lays the film of the whole figure under it at
// the ghost's strength. The quad reaches a tenth of the digit past the cell
// (2 points at least) for the drops and the shadow.
mod.widgets.Readout.draw_bg.color = uniform(#07070b)
mod.widgets.Readout.draw_bg.glyph_width = uniform(0.57)
mod.widgets.Readout.draw_bg.vertex = fn() {
let m = max(2.0, self.rect_size.y * 0.1)
let over = (self.rect_size.x - self.span) * 0.5
let v = fe_grown_vertex(self.rect_pos, self.rect_size, self.geom.pos, self.draw_clip, self.draw_list.view_shift, self.draw_list.view_clip, m, self.rect_pos + vec2(over, 0.0), self.rect_pos + self.rect_size - vec2(over, 0.0))
self.pos = (v.xy - v.zw) / (self.rect_size + vec2(2.0 * m, 2.0 * m))
self.world = self.draw_list.view_transform * vec4(v.x, v.y, self.draw_depth + self.draw_call.zbias, 1.0)
self.vertex_pos = self.draw_pass.camera_projection * (self.draw_pass.camera_view * self.world)
}
mod.widgets.Readout.draw_bg.pixel = fn() {
let m = max(2.0, self.rect_size.y * 0.1)
let h = max(self.rect_size.y, 1.0)
let pp = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let over = (self.rect_size.x - self.span) * 0.5
let p = (pp - vec2(over, 0.0)) / h
let q = vec2(p.x + (p.y - 0.5) * self.slant, p.y)
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) / h
let span = self.span / h
let w = self.glyph_width
// the stroke, held thin enough on a small digit that the counter
// between two uprights stays five device pixels wide
let r = min(self.stroke * 0.625, max((w - 5.0 * px) / 4.5, self.stroke * 0.4))
// the tufts grow in from 42 device pixels of digit to their full
// length at 60: on a smaller digit they would only fray its end
let tuft = smoothstep(42.0, 60.0, h * self.draw_pass.dpi_factor)
let e = px * 0.5
let d = fe_lq_cell(q, self.mask, self.dot, self.colon, span, w, r, tuft, px)
let gx = fe_lq_cell(q + vec2(e, 0.0), self.mask, self.dot, self.colon, span, w, r, tuft, px) - d
let gy = fe_lq_cell(q + vec2(0.0, e), self.mask, self.dot, self.colon, span, w, r, tuft, px) - d
var out = vec4(0.0, 0.0, 0.0, 0.0)
if self.ghost > 0.001 && self.colon < 0.5 {
let fd = fe_lq_cell(q, 127.0, self.dot, 0.0, span, w, r, 0.0, px)
let fa = Finish.cover(fd, px) * clamp(self.ghost, 0.0, 1.0) * self.color.a
out = vec4(self.color.rgb * fa, fa)
}
out = Finish.over(out, fe_lq_ink(d, vec2(gx, gy) / e, r, px, self.color, q + vec2(self.mask * 0.37, 0.0)))
return out * self.opacity
}
// A display's level ladder: beads of liquid down the middle of the scale
// (`s` in points along it from the quiet end and across it), `pitch`
// apart, every bead up to the reading there. Each bead is a little longer
// than it is thick and no two are alike; some melt into the next and some
// barely touch it. The bead at the reading is the fullest, and the held
// peak, past the run, a drop of its own.
let fe_lq_bead = fn(s: vec2, i: float, pitch: float, rad: float, mid: float, n: float) -> float {
let lit = step(-0.5, i) * step(i, n - 0.5)
let lead = step(abs(i - n + 1.0), 0.5)
let h1 = Finish.hash(vec2(i * 1.7, 3.1))
let h2 = Finish.hash(vec2(i * 2.3, 7.9))
let ry = rad * (0.86 + 0.22 * h1 + 0.1 * lead)
let rx = pitch * (0.38 + 0.12 * h2 + 0.06 * lead)
let cx = (i + 0.5) * pitch + (h1 - 0.5) * pitch * 0.12
let q = (s - vec2(cx, mid)) / vec2(rx, ry)
return (length(q) - 1.0) * min(rx, ry) + (1.0 - lit) * 100.0
}
let fe_lq_ladder = fn(s: vec2, pitch: float, rad: float, mid: float, n: float, hold_at: float, hold_on: float) -> float {
let i = clamp(floor(s.x / pitch), 0.0, max(n - 1.0, 0.0))
let k0 = rad * (0.1 + 0.7 * Finish.hash(vec2(i * 3.3, 5.3)))
let k1 = rad * (0.1 + 0.7 * Finish.hash(vec2((i + 1.0) * 3.3, 5.3)))
var d = fe_smin(fe_lq_bead(s, i - 1.0, pitch, rad, mid, n), fe_lq_bead(s, i, pitch, rad, mid, n), k0)
d = fe_smin(d, fe_lq_bead(s, i + 1.0, pitch, rad, mid, n), k1)
let hd = length(s - vec2(hold_at, mid)) - rad * 0.8 + (1.0 - hold_on) * 100.0
return fe_smin(d, hd, rad * 0.4)
}
// LEVEL METERS. A ladder of cells in a black well with a hairline rim:
// cells 3 device pixels long with one dark pixel between, lit LED orange,
// deeper orange over the last fifth, the clip cell deepest; unlit cells a
// tenth of their lit colour on the glass. No glow. A meter cannot tell it
// stands on a display; a level one with no lamp and 6 points thin or less
// is taken for a display's ladder and drawn in the display's liquid
// (DISPLAYS), in a shallow channel of the display well's floor, so the
// liquid reads on any ground; a column keeps its well, under the knobs'
// lens (THE WELL AND THE LENS).
mod.widgets.LevelMeter.height = 8.0
mod.widgets.LevelMeterColumn.width = 8.0
mod.widgets.LevelMeter.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let vert = self.vertical
let along = mix(self.rect_size.x, self.rect_size.y, vert)
let across = mix(self.rect_size.y, self.rect_size.x, vert)
let a = mix(p.x, self.rect_size.y - p.y, vert)
let b = mix(p.y, p.x, vert)
let q = vec2(a, b)
if self.lamp < 0.5 && across < 6.5 && vert < 0.5 {
// a display's ladder, thin, level and lampless: a channel of the
// cell's pale, lost on a display's own cell; beads of liquid in
// it, each about half as long again as the meter is thick; and on
// the empty scale a faint round mark where each would stand
let pad = 0.5
let span = max(1.0, along - pad * 2.0)
let nb = max(floor(span / max(across * 1.5, 4.0) + 0.5), 1.0)
let pitch = span / nb
let rad = across * 0.36
let mid = across * 0.5
let n = floor(clamp(self.level, 0.0, 1.0) * nb + 0.5)
let hold_i = floor(clamp(self.hold, 0.0, 1.0) * nb + 0.5) - 1.0
let hold_on = step(0.002, self.hold) * step(n - 0.5, hold_i)
let hold_at = (hold_i + 0.5) * pitch
let s = vec2(a - pad, b)
let e = px * 0.5
let d = fe_lq_ladder(s, pitch, rad, mid, n, hold_at, hold_on)
let ga = fe_lq_ladder(s + vec2(e, 0.0), pitch, rad, mid, n, hold_at, hold_on) - d
let gb = fe_lq_ladder(s + vec2(0.0, e), pitch, rad, mid, n, hold_at, hold_on) - d
// the scale's gradient turned back to the screen's: a column runs up
let g = mix(vec2(ga, gb), vec2(gb, -ga), vert) / e
let ci = floor(s.x / pitch)
let mark = max(across * 0.17, px * 1.25)
let dim = Finish.cover(length(s - vec2((ci + 0.5) * pitch, mid)) - mark, px) * step(n - 0.5, ci) * step(ci, nb - 0.5) * 0.07
let tray = Finish.cover(fk_rbox(q, vec2(along, across) * 0.5, vec2(along, across) * 0.5, across * 0.5), px) * 0.92
let base = Finish.over(vec4(fk_floor(0.0, 0.0) * tray, tray), vec4(0.0, 0.0, 0.0, dim))
let out = Finish.over(base, fe_lq_ink(d, g, rad, px, vec4(0.027, 0.027, 0.043, 1.0), s / max(across, 1.0)))
return out * self.opacity
}
let lamp_room = self.lamp * (across + 3.0)
let bar = max(4.0, along - lamp_room)
let h = vec2(bar * 0.5, across * 0.5 - px * 0.5)
let c = h
let r = 1.0
let d = fk_rbox(q, c, h, r)
let pad = 2.0 * px
let span = max(1.0, bar - pad * 2.0)
let share = clamp((a - pad) / span, 0.0, 1.0)
let pitch = 4.0 * px
let cell_i = floor((a - pad) / pitch)
let in_cell = step((a - pad) - cell_i * pitch, 3.0 * px)
let cell_share = clamp((cell_i * pitch + pitch * 0.5) / span, 0.0, 1.0)
let lit = step(cell_share, self.level) * step(pad, a) * step(a, pad + span) * in_cell
var ramp = mix(vec3(0.992, 0.518, 0.196), vec3(0.933, 0.353, 0.141), step(0.8, cell_share))
ramp = mix(ramp, vec3(0.835, 0.251, 0.059), step(0.95, cell_share))
let glass = vec3(0.102, 0.102, 0.102)
let rows = step(pad, b) * step(b, across - pad)
let cells = in_cell * step(pad, a) * step(a, pad + span) * rows
var col = mix(glass, mix(glass, ramp, 0.12), cells)
col = mix(col, ramp, lit * rows)
// the hold: one cell lit where the peak stood
if self.hold > 0.002 {
let hi_cell = floor(self.hold * span / pitch)
let hm = step(abs(cell_i - hi_cell), 0.5) * in_cell * rows
col = mix(col, ramp, hm)
}
col = mix(col, vec3(0.0, 0.0, 0.0), Finish.band(d, 0.0, px, px) * 0.8)
let ca = Finish.cover(d, px)
var out = Finish.over(fk_catch(q, c, h, r, px, 0.75 * (1.0 - vert)), vec4(col * ca, ca))
// the lens over the well, in the screen's frame: the case's shadow in
// under its upper rim, the lens's light
let sc = mix(c, vec2(c.y, self.rect_size.y - c.x), vert)
let sh = mix(h, vec2(h.y, h.x), vert)
let sd = fk_rbox(p, sc, sh, r)
let sn = fk_rbox_n(p, sc, sh, r)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, fk_rim_shade(p, sc, sh, r, min(sh.x, sh.y), 0.0, 0.0) * ca))
out = Finish.over(out, fk_lens_light(p, sc - sh, sd, sn, min(sh.x, sh.y), px, 0.0, 0.0, 0.0))
if self.lamp > 0.5 {
// the over lamp: a flat square LED in a black bezel, dark until it latches
let la = bar + 3.0 + across * 0.5
let lb = across * 0.5
let lc = vec2(mix(la, lb, vert), mix(lb, self.rect_size.y - la, vert))
let ld = fk_rbox(p, lc, vec2(across * 0.5 - px, across * 0.5 - px), 1.0)
let on = step(0.5, self.over)
var lcol = mix(vec3(0.478, 0.475, 0.467), vec3(0.835, 0.251, 0.059), on)
lcol = mix(lcol, vec3(0.04, 0.04, 0.04), Finish.band(ld, 0.0, px, px))
let lca = Finish.cover(ld, px)
out = Finish.over(out, vec4(lcol * lca, lca))
}
return out * self.opacity
}
// ---------------------------------------------------------------------------
// SCROLL BARS. No track but an engraved line along the middle; a 6 point
// white key for the thumb, darker under the pointer, orange while dragged.
mod.widgets.ScrollBar.bar_size = 8.0
mod.widgets.ScrollBar.bar_side_margin = 0.0
mod.widgets.ScrollBar.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let vert = self.is_vertical
let a = mix(p.x, p.y, vert)
let b = mix(p.y, p.x, vert)
let along = mix(self.rect_size.x, self.rect_size.y, vert)
let across = mix(self.rect_size.y, self.rect_size.x, vert)
let q = vec2(a, b)
let ob = mix(self.rect_pos.y, self.rect_pos.x, vert)
let mid = Finish.snap(ob + across * 0.5, px) - ob
let seam = fk_run(b, mid - px, mid) * step(2.0, a) * step(a, along - 2.0)
let seam_l = fk_run(b, mid, mid + px) * step(2.0, a) * step(a, along - 2.0)
var out = vec4(0.0, 0.0, 0.0, seam * 0.14)
out = Finish.over(out, vec4(seam_l * 0.5, seam_l * 0.5, seam_l * 0.5, seam_l * 0.5))
// the thumb
let t0 = along * self.norm_scroll
let tl = along * self.norm_handle
let th = vec2(tl * 0.5 - 1.0, 3.0)
let tc = vec2(t0 + tl * 0.5, mid)
let td = fk_rbox(q, tc, th, 1.0)
let tt = clamp((b - (tc.y - th.y)) / 6.0, 0.0, 1.0)
var face = mix(vec3(0.855, 0.855, 0.855), vec3(0.816, 0.816, 0.816), tt)
face = face - g3(0.045 * self.hover)
face = mix(face, vec3(0.933, 0.353, 0.141), self.drag)
face = mix(face, vec3(1.0, 1.0, 1.0), fk_run(b, tc.y - th.y, tc.y - th.y + px) * step(abs(a - tc.x), th.x - 1.0) * (1.0 - self.drag))
let con = Finish.ring_out(td, 0.0, px, px) * step(tc.y, b) * 0.20
out = Finish.over(out, vec4(0.0, 0.0, 0.0, con))
let ta = Finish.cover(td, px)
out = Finish.over(out, vec4(face * ta, ta))
return out * self.opacity
}
// Rings and arcs keep their stock faces, restrung thin: a 2.5 point stroke,
// butt ends, a pale track and the signal orange.
mod.widgets.ProgressRingFlat.draw_bg.thickness = 2.5
mod.widgets.ProgressRingFlat.draw_bg.rounded_caps = 0.0
mod.widgets.ProgressRingFlat.draw_bg.track_color = #d4d4d4
mod.widgets.ProgressRingFlat.draw_bg.color_primary = #ee5a24
mod.widgets.ProgressArc.draw_bg.thickness = 3.0
mod.widgets.ActivityRings.draw_bg.thickness = 3.0
mod.widgets.ProgressBarFlat.draw_bg.color_primary = #ee5a24
// ---------------------------------------------------------------------------
// VIEWS AND PANELS. A plate a step darker than the case, laid on it with a
// short drop and a contact line, one pixel of light along its top, the
// case's grain and fibre in its face, and on a plate large enough four
// slotted screws six points in from its corners. The sunken panel is cut
// in: a shade under its top edge and a light catch under its lower one.
// A screw 4 points across (6 device pixels): the plate -0.06, a rim of one
// pixel at -0.15 and one slot a pixel wide at -0.25 across 0.8 of it.
let fk_screw = fn(p: vec2, sc: vec2, base: vec3, px: float) -> vec4 {
let q = p - sc
let d = length(q) - 2.0
var col = base - g3(0.06)
col = mix(col, base - g3(0.15), Finish.band(d, 0.0, px, px))
// one slot, a pixel wide, at an angle of its own
let ang = Finish.hash(floor(sc * 3.1) + vec2(5.0, 11.0)) * PI
let dir = vec2(cos(ang), sin(ang))
let sl = fk_seg(p, sc - dir * 1.6, sc + dir * 1.6) - px * 0.5
col = mix(col, base - g3(0.25), Finish.cover(sl, px))
let a = Finish.cover(d, px)
return vec4(col * a, a)
}
// The family's pattern: a field of perforations on a square grid, 4 device
// pixels apart (2.7 points), each hole 2 pixels (#2a2a2a), with one pixel of
// light on its lower side (+0.05). `w` is the point in window pixels, `a`
// and `b` the field's corners in window pixels (whole pixels).
let fk_perf = fn(w: vec2, a: vec2, b: vec2, base: vec3) -> vec4 {
let inside = step(a.x, w.x) * step(w.x, b.x) * step(a.y, w.y) * step(w.y, b.y)
let l = w - a
let cell = floor(l / 4.0)
let k = l - cell * 4.0 - vec2(2.0, 2.0)
// a hole is two by two whole pixels: at this pitch a round one only
// smears into a grey halo
let hole = step(abs(k.x), 1.0) * step(abs(k.y), 1.0) * inside
let lip = step(abs(k.x), 1.0) * step(1.0, k.y) * step(k.y, 2.0) * inside
let lit = base + g3(0.05)
let out = vec4(lit * lip, lip)
let hc = vec3(0.165, 0.165, 0.165)
return Finish.over(out, vec4(hc * hole, hole))
}
let fk_plate = fn(p: vec2, size: vec2, o: vec2, px: float, r: float, sunken: float, base: vec3, screws: float) -> vec4 {
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o
let h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox(p, c, h, r)
var out = vec4(0.0, 0.0, 0.0, 0.0)
let dr = Finish.drop(p, c, h, r, vec2(0.0, 1.5), 2.2) * 0.10 * (1.0 - sunken)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, dr))
let foot = smoothstep(c.y + h.y - r - 0.5, c.y + h.y - 0.2, p.y)
let con = Finish.ring_out(d, 0.0, px, px) * mix(0.07, 0.25, foot) * (1.0 - sunken)
out = Finish.over(out, vec4(0.0, 0.0, 0.0, con))
let lip = Finish.ring_out(d, 0.0, px, px) * foot * 0.75 * sunken
out = Finish.over(out, vec4(lip, lip, lip, lip))
var face = fk_plate_grain(base, p + o, px)
// sunken: the case's shade falling in under the top edge
let sh = Finish.inset(p, c, h, r, vec2(0.0, 2.0), 1.6)
face = mix(face, vec3(0.0, 0.0, 0.0), clamp(sh, 0.0, 1.0) * 0.30 * sunken)
let up = 1.0 - smoothstep(-0.5, 0.5, p.y - c.y)
let top = fk_run(p.y, lo.y, lo.y + px) * step(abs(p.x - c.x), h.x - r)
face = mix(face, vec3(1.0, 1.0, 1.0), top * 0.7 * (1.0 - sunken))
face = mix(face, vec3(0.0, 0.0, 0.0), Finish.band(d, 0.0, px, px) * mix(0.10, mix(0.10, 0.22, up), sunken))
let a = Finish.cover(d, px)
out = Finish.over(out, vec4(face * a, a))
if screws > 0.5 {
if min(size.x, size.y) > 60.0 && max(size.x, size.y) > 160.0 {
// a grille of perforations along the top edge, between the two
// upper screws: three rows, 14 points in from the sides
let wp = (p + o) / px
let ga = vec2(floor((o.x + lo.x + 14.0) / px), floor((o.y + lo.y + 2.5) / px))
let gw = floor((size.x - 28.0) / px / 4.0) * 4.0
let gs = ga + vec2(floor(((size.x - 28.0) / px - gw) * 0.5), 0.0)
out = Finish.over(out, fk_perf(wp, gs, gs + vec2(gw, 12.0), base) * Finish.cover(d, px))
let s0 = vec2(Finish.snap(o.x + lo.x + 6.0, px) - o.x, Finish.snap(o.y + lo.y + 6.0, px) - o.y)
let s1 = vec2(Finish.snap(o.x + hi.x - 6.0, px) - o.x, s0.y)
let s2 = vec2(s0.x, Finish.snap(o.y + hi.y - 6.0, px) - o.y)
let s3 = vec2(s1.x, s2.y)
let near = mix(mix(s0, s1, step(c.x, p.x)), mix(s2, s3, step(c.x, p.x)), step(c.y, p.y))
out = Finish.over(out, fk_screw(p, near, base, px))
}
}
return out
}
mod.widgets.PanelView.clip_x = false
mod.widgets.PanelView.clip_y = false
mod.widgets.PanelView.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 8.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.PanelView.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 8.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let base = mix(vec3(0.894, 0.882, 0.863), vec3(0.906, 0.898, 0.882), self.sunken)
return fk_plate(p, self.rect_size, self.rect_pos, px, 6.0, self.sunken, base, 1.0 - self.sunken)
}
// The card is the plate too, without screws; its palette and shadow
// numbers from Rust are not read.
mod.widgets.Card.radius = 6.0
mod.widgets.Card.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 8.0)
return self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.Card.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 8.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
var out = fk_plate(p, self.rect_size, self.rect_pos, px, 6.0, 0.0, vec3(0.894, 0.882, 0.863), 0.0)
let d = fk_rbox(p, self.rect_size * 0.5, self.rect_size * 0.5, 6.0)
let l = Finish.cover(d, px) * (0.06 * self.hover - 0.04 * self.down)
return Finish.over(out, vec4(max(l, 0.0), max(l, 0.0), max(l, 0.0), abs(l)))
}
// The plain rounded view keeps the colours its caller gives it; the edge is
// a snapped hairline at the true radius.
mod.widgets.RoundedView.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
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))
}
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 h = (hi - lo) * 0.5
let c = lo + h
let d = fk_rbox(p, c, h, max(self.border_radius, 1.0))
var col = fill
if self.border_size > 0.0 {
col = mix(col, self.border_color, Finish.band(d, 0.0, max(self.border_size, px), px) * self.border_color.a)
}
let a = Finish.cover(d, px) * col.a
return vec4(col.rgb * a, a)
}
// ---------------------------------------------------------------------------
// LISTS. Rows of 22 points with a faint hairline under each, inset 8 at the
// left; a hovered row a shade darker; the chosen row a little darker still
// with a 3 point orange bar at its left edge.
mod.widgets.ListItemOne.height = 22.0
mod.widgets.ListItem.padding = mod.turtle.Inset{left: 12.0 right: 8.0 top: 0.0 bottom: 0.0}
mod.widgets.ListItem.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let live = 1.0 - self.disabled
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + self.rect_size.x, px), Finish.snap(o.y + self.rect_size.y, px)) - o
let sel = step(0.5, self.selected)
let lay = (self.hover * 0.04 + self.down * 0.04) * live + sel * 0.06
var out = vec4(0.0, 0.0, 0.0, lay)
let bar = fk_run(p.x, lo.x, lo.x + 3.0) * sel
out = Finish.over(out, vec4(0.933 * bar, 0.353 * bar, 0.141 * bar, bar))
let yb = hi.y
let sep = fk_run(p.y, yb - px, yb) * step(lo.x + 8.0, p.x) * 0.08
return Finish.over(out, vec4(0.0, 0.0, 0.0, sep))
}
mod.widgets.ListItem.draw_text.color = #1d1d1d
// ---------------------------------------------------------------------------
// CHIPS AND BADGES. A chip is a small white key with round ends (the chosen
// one, which carries light ink, an orange key); a badge is a small black
// plate with pale print.
mod.widgets.ChipFlat.size = mod.widgets.ChipSize.Small
mod.widgets.Chip.size = mod.widgets.ChipSize.Small
mod.widgets.ChipFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 6.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ChipFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 6.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let ch = min(22.0, self.rect_size.y)
let dy = (self.rect_size.y - ch) * 0.5
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y + dy, px)) - o
let hi = vec2(Finish.snap(o.x + self.rect_size.x, px), Finish.snap(o.y + dy + ch, px)) - o
let h = (hi - lo) * 0.5
let c = lo + h
let r = h.y
let chosen = step(0.6, dot(self.ink.rgb, vec3(0.299, 0.587, 0.114)))
var out = fk_shade(p, c, h, r, px, 1.0 - self.down, 0.0, mix(0.13, 0.19, self.hover))
out = Finish.over(out, fk_cap(p, lo, hi, r, px, 2.0 * px, chosen, self.hover, self.down, 0.0, 0.0))
return Finish.over(out, fk_focus(fk_rbox(p, c, h, r), px, self.keyed))
}
mod.widgets.BadgePalette.neutral = #1a1a1a
mod.widgets.BadgePalette.on_neutral = #f0f0f0
mod.widgets.BadgeFlat.draw_text.text_style = mod.theme.font_bold{font_size: 6.0}
mod.widgets.Badge.draw_text.text_style = mod.theme.font_bold{font_size: 6.0}
mod.widgets.BadgeFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let bh = min(15.0, self.rect_size.y)
let y0 = Finish.snap(o.y + (self.rect_size.y - bh) * 0.5, px) - o.y
let lo = vec2(Finish.snap(o.x, px) - o.x, y0)
let hi = vec2(Finish.snap(o.x + self.rect_size.x, px) - o.x, Finish.snap(o.y + y0 + bh, px) - o.y)
let h = (hi - lo) * 0.5
let c = lo + h
// a dot (as wide as tall and small) is round; a count is a plate
let r = mix(2.0, h.y, step(self.rect_size.x, self.rect_size.y + 0.5) * step(self.rect_size.y, 12.5))
let d = fk_rbox(p, c, h, r)
var col = vec3(0.102, 0.102, 0.102)
col = mix(col, vec3(0.30, 0.30, 0.30), fk_run(p.y, lo.y, lo.y + px) * step(abs(p.x - c.x), h.x - r))
let a = Finish.cover(d, px) * self.color.a
let out = fk_catch(p, c, h, r, px, 0.6)
return Finish.over(out, vec4(col * a, a))
}
// The spinner keeps its turning arc: in print ink, thin.
mod.widgets.SpinnerFlat.size = 16.0
mod.widgets.SpinnerFlat.draw_bg.color = #1d1d1d
mod.widgets.SpinnerFlat.draw_bg.stroke_width = 1.5
mod.widgets.SpinnerFlat.draw_bg.track_alpha = 0.24
// Rules between groups: one dark device row over one light one, engraved.
mod.widgets.Hr.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let y = Finish.snap(self.rect_pos.y + self.rect_size.y * 0.5, px) - self.rect_pos.y
let dk = fk_run(p.y, y - px, y) * 0.12
let lt = fk_run(p.y, y, y + px) * 0.45
return Finish.over(vec4(0.0, 0.0, 0.0, dk), vec4(lt, lt, lt, lt))
}
mod.widgets.Vr.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let x = Finish.snap(self.rect_pos.x + self.rect_size.x * 0.5, px) - self.rect_pos.x
let dk = fk_run(p.x, x - px, x) * 0.12
let lt = fk_run(p.x, x, x + px) * 0.45
return Finish.over(vec4(0.0, 0.0, 0.0, dk), vec4(lt, lt, lt, lt))
}
// ---------------------------------------------------------------------------
// THE CASE under everything: the window's own ground, grain and fibre in
// window coordinates, so nothing swims; and printed on it, faint, the
// contour lines of a map: the levels of a smooth height (two octaves of
// noise, hills some 230 points across, turned off the grid), 26 levels
// over its range, so the lines run about 10 points apart where it is steep
// and wider on the flats. Each line is a device pixel wide, its distance
// taken from the height's fraction over its slope per pixel, and 7% darker
// than the case; every fifth a pixel and a half and 10% darker.
mod.widgets.Window.show_bg = true
mod.widgets.Window.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let u = vec2(p.x * 0.8 + p.y * 0.6, p.y * 0.8 - p.x * 0.6) / 230.0 + vec2(3.1, 7.7)
let h = (Finish.noise(u) + 0.42 * Finish.noise(vec2(u.x * 1.6 + u.y * 1.2, u.y * 1.6 - u.x * 1.2) + vec2(5.2, 1.3))) * 18.3
let n = floor(h + 0.5)
let idx = 1.0 - step(0.1, fract(n * 0.2 + 0.02))
let a = clamp(mix(1.0, 1.25, idx) - abs(h - n) / max(length(vec2(dFdx(h), dFdy(h))), 0.0001), 0.0, 1.0)
return vec4(fk_case(vec3(0.937, 0.929, 0.918), p, px) * (1.0 - a * mix(0.07, 0.1, idx)), 1.0)
}
// ---------------------------------------------------------------- band
// THE BAND is the sheet's own slider with two thumbs: the same track, the
// same lit value and the same thumb as the Slider, the value lit between
// the two thumbs. The widget's grab geometry is set to the thumbs' own, so
// what is drawn is what is dragged.
mod.widgets.RangeSliderFlat.clip_x = false
mod.widgets.RangeSliderFlat.clip_y = false
mod.widgets.RangeSliderFlat.track_inset = -1.0
mod.widgets.RangeSliderFlat.handle_size = 12.0
mod.widgets.RangeSliderFlat.draw_bg.vertex = fn() {
let g = fk_grow(self.rect_pos, self.rect_size, 10.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.RangeSliderFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 10.0
let size = self.rect_size
let p = self.pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let hw = 5.0
let cy = size.y - 10.0
let th = 3.0
let top = Finish.snap(o.y + cy - th * 0.5, px) - o.y
let x0 = Finish.snap(o.x + 1.0, px) - o.x
let w = Finish.snap(o.x + size.x - 1.0, px) - o.x - x0
let f0 = x0 + hw + self.start_pos * (w - 2.0 * hw)
let f1 = x0 + hw + self.end_pos * (w - 2.0 * hw)
var out = fk_track(p, vec2(x0, top), w, th, px, f0, f1, self.disabled)
let span = w - 2.0 * hw
let nt = max(floor(span / 6.0 + 0.5), 1.0)
let pitch = span / nt
let fi = floor((p.x - x0 - hw) / pitch + 0.5)
let tx = Finish.snap(o.x + x0 + hw + fi * pitch, px) - o.x
let major = 1.0 - step(0.3, abs(fract(fi / 5.0 + 0.5) - 0.5))
let tl = mix(3.0, 4.5, major)
let ty = Finish.snap(o.y + top - 2.0, px) - o.y
let ty0 = Finish.snap(o.y + top - 2.0 - tl, px) - o.y
let in_x = step(-0.5, fi) * step(fi, nt + 0.5)
let tick = fk_run(p.x, tx, tx + px) * fk_run(p.y, ty0, ty) * in_x
let ta = tick * mix(0.36, 0.5, major) * mix(1.0, 0.45, self.disabled)
out = Finish.over(out, vec4(0.114 * ta, 0.114 * ta, 0.114 * ta, ta))
// The two thumbs: drops of the black ferrofluid sitting on the track,
// inked as the knobs' drop is.
let travel = max(size.x + 2.0 - 12.0, 1.0)
let c0 = vec2(-1.0 + 6.0 + self.start_pos * travel, top + th * 0.5)
let c1 = vec2(-1.0 + 6.0 + self.end_pos * travel, top + th * 0.5)
let r0 = 5.5 + 0.5 * max(self.hot_start, self.drag * self.hot_start)
let r1 = 5.5 + 0.5 * max(self.hot_end, self.drag * self.hot_end)
let w0 = p - c0
let l0 = max(length(w0), 0.0001)
out = Finish.over(out, fk_drop_ink(vec3(1.0 + (r0 - l0) * 0.5 / r0, -w0.x / l0 * 0.5 / r0, -w0.y / l0 * 0.5 / r0), r0, 0.5, px, mix(1.0, 0.45, self.disabled)))
let w1 = p - c1
let l1 = max(length(w1), 0.0001)
return Finish.over(out, fk_drop_ink(vec3(1.0 + (r1 - l1) * 0.5 / r1, -w1.x / l1 * 0.5 / r1, -w1.y / l1 * 0.5 / r1), r1, 0.5, px, mix(1.0, 0.45, self.disabled)))
}
// ---------------------------------------------------------------- switches and meter
// THE SWITCHES are the sheet's own toggle, in proportion with the
// labels and the controls beside them: the rocker and the slide switch
// take the toggle's face, its size and shape, its quad and its label
// gap (the library's own faces for them are flat boxes sized for
// another face). The rocker turns on a click; the slide switch follows a
// drag, as the toggle does.
mod.widgets.ToggleRocker.label_walk.margin = mod.turtle.Inset{left: 42.0 right: 3.0 top: 0.0 bottom: 0.0}
mod.widgets.ToggleSlide.label_walk.margin = mod.turtle.Inset{left: 42.0 right: 3.0 top: 0.0 bottom: 0.0}
mod.widgets.ToggleRocker.draw_bg.size = uniform(16.0)
mod.widgets.ToggleSlide.draw_bg.size = uniform(16.0)
mod.widgets.ToggleRocker.draw_bg.pill_aspect = uniform(2.06)
mod.widgets.ToggleSlide.draw_bg.pill_aspect = uniform(2.06)
mod.widgets.ToggleRocker.draw_bg.knob_inset = uniform(0.0)
mod.widgets.ToggleSlide.draw_bg.knob_inset = uniform(0.0)
mod.widgets.ToggleRocker.draw_bg.border_size = uniform(0.0)
mod.widgets.ToggleSlide.draw_bg.border_size = uniform(0.0)
mod.widgets.ToggleRocker.min_height = 26.0
mod.widgets.ToggleSlide.min_height = 26.0
mod.widgets.ToggleRocker.align.y = 0.5
mod.widgets.ToggleSlide.align.y = 0.5
mod.widgets.ToggleRocker.clip_x = false
mod.widgets.ToggleSlide.clip_x = false
mod.widgets.ToggleRocker.clip_y = false
mod.widgets.ToggleSlide.clip_y = false
mod.widgets.ToggleRocker.cap_viscosity = 0.0
mod.widgets.ToggleSlide.cap_viscosity = 0.0
mod.widgets.ToggleRocker.cap_field_reach = 0.0
mod.widgets.ToggleSlide.cap_field_reach = 0.0
mod.widgets.ToggleRocker.draw_bg.vertex = mod.widgets.ToggleFlat.draw_bg.vertex
mod.widgets.ToggleSlide.draw_bg.vertex = mod.widgets.ToggleFlat.draw_bg.vertex
mod.widgets.ToggleRocker.draw_bg.pixel = mod.widgets.ToggleFlat.draw_bg.pixel
mod.widgets.ToggleSlide.draw_bg.pixel = mod.widgets.ToggleFlat.draw_bg.pixel
// ---------------------------------------------------------------- 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 fe_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 fe_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 fe_led_lamp_hue = fn(ink: vec3, vivid: float) -> vec3 {
let f = fe_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 fe_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 = fe_led_lamp_full(ink)
let hue = fe_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 = fe_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 well sunk into the case with a rim of clear glass, the material of its knobs: the upper wall in shadow, a catch light on the lower rim.
// 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 = fe_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.937, 0.929, 0.918), max(room - 0.5, 2.0), vec3(0.96, 0.96, 0.965), 0.35)
return res * self.opacity
}
true