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