lib.bend source
lib.bend on the hub · documented module
# Color — Layer 1: Rgb/Hsl types, conversion, and string show for Bend 2.0.2.## Channel conventions (documented, not enforced by the constructors):# - Rgb.r/g/b: U32 octets in 0..=255 (values outside are clamped by convert# helpers that go through channel_u8 / channel_to_u8).# - Rgb.a / Hsl.a: F32 alpha, conventionally 0.0..=1.0.# - Hsl.h: F32 hue in degrees, conventionally 0.0..=360.0.# - Hsl.s / Hsl.l: F32 saturation and lightness in the unit interval 0.0..=1.0.## Conversion uses Base F32 arithmetic. Results are approximate: F32 ops are# opaque to definitional equality, and channel_to_u8 rounds with F32.round# then clamps. Primary colors and many modest RGB triples round-trip at# runtime; that is not a definitional claim.## Layer 1 only: no random_*, no sampling. ColorRange validates min/max without RNG.# Base only; no termination escapes.import Basetype Rgb is Data: Rgb{r: U32, g: U32, b: U32, a: F32}type Hsl is Data: Hsl{h: F32, s: F32, l: F32, a: F32}# Inclusive RGB axis-aligned box for later sampling layers. Alpha is part of# each endpoint; validate checks every channel with the natural order.type ColorRange is Data: ColorRange{min: Rgb, max: Rgb}# Constructors# ------------def Color.rgb(r: U32, g: U32, b: U32, a: F32) -> Rgb: Rgb{r, g, b, a}def Color.hsl(h: F32, s: F32, l: F32, a: F32) -> Hsl: Hsl{h, s, l, a}def Color.range(min: Rgb, max: Rgb) -> ColorRange: ColorRange{min, max}# Clamp helpers# -------------def Color.clamp_u8(x: U32) -> U32: U32.clamp(x, 0, 255)def Color.clamp_unit(+x: F32) -> F32: F32.clamp(x, 0.0, 1.0)def Color.clamp_hue(+h: F32) -> F32: F32.mod(F32.add(F32.mod(h, 360.0), 360.0), 360.0)def Color.clamp_rgb(c: Rgb) -> Rgb: match c: case Rgb{r, g, b, a}: Rgb{ Color.clamp_u8(r), Color.clamp_u8(g), Color.clamp_u8(b), Color.clamp_unit(a)}def Color.clamp_hsl(c: Hsl) -> Hsl: match c: case Hsl{h, s, l, a}: Hsl{ Color.clamp_hue(h), Color.clamp_unit(s), Color.clamp_unit(l), Color.clamp_unit(a)}# Field projections (for laws and callers that prefer functions)# -------------------------------------------------------------def Color.rgb_r(c: Rgb) -> U32: match c: case Rgb{r, g, b, a}: rdef Color.rgb_g(c: Rgb) -> U32: match c: case Rgb{r, g, b, a}: gdef Color.rgb_b(c: Rgb) -> U32: match c: case Rgb{r, g, b, a}: bdef Color.rgb_a(c: Rgb) -> F32: match c: case Rgb{r, g, b, a}: adef Color.hsl_h(c: Hsl) -> F32: match c: case Hsl{h, s, l, a}: hdef Color.hsl_s(c: Hsl) -> F32: match c: case Hsl{h, s, l, a}: sdef Color.hsl_l(c: Hsl) -> F32: match c: case Hsl{h, s, l, a}: ldef Color.hsl_a(c: Hsl) -> F32: match c: case Hsl{h, s, l, a}: a# Internal channel mapping# ------------------------def Color.channel_u8(+x: U32) -> F32: F32.div(U32.to_f32(Color.clamp_u8(x)), 255.0)def Color.channel_to_u8(+x: F32) -> U32: Color.clamp_u8( F32.to_u32(F32.round(F32.mul(Color.clamp_unit(x), 255.0))))# RGB -> HSL# ----------def Color.rgb_to_hsl.hue_from_r(+r: F32, +g: F32, +b: F32, +d: F32) -> F32: +base = F32.div(F32.sub(g, b), d) Bool.pick(F32, F32.is_lt(g, b), F32.add(base, 6.0), base)def Color.rgb_to_hsl.hue(+r: F32, +g: F32, +b: F32, +mx: F32, +d: F32) -> F32: Bool.pick(F32, F32.is_eq(mx, r), Color.rgb_to_hsl.hue_from_r(r, g, b, d), Bool.pick(F32, F32.is_eq(mx, g), F32.add(F32.div(F32.sub(b, r), d), 2.0), F32.add(F32.div(F32.sub(r, g), d), 4.0)))def Color.rgb_to_hsl.sat(+mx: F32, +mn: F32, +l: F32, +d: F32) -> F32: Bool.pick(F32, F32.is_gt(l, 0.5), F32.div(d, F32.sub(F32.sub(2.0, mx), mn)), F32.div(d, F32.add(mx, mn)))def Color.rgb_to_hsl.chromatic( +r: F32, +g: F32, +b: F32, +mx: F32, +mn: F32, +l: F32, +a: F32) -> Hsl: +d = F32.sub(mx, mn) h6 = Color.rgb_to_hsl.hue(r, g, b, mx, d) h = F32.mul(F32.div(h6, 6.0), 360.0) s = Color.rgb_to_hsl.sat(mx, mn, l, d) Hsl{h, s, l, a}def Color.rgb_to_hsl(c: Rgb) -> Hsl: match c: case Rgb{+r, +g, +b, +a}: +rf = Color.channel_u8(r) +gf = Color.channel_u8(g) +bf = Color.channel_u8(b) +mx = F32.max(rf, F32.max(gf, bf)) +mn = F32.min(rf, F32.min(gf, bf)) +l = F32.mul(F32.add(mx, mn), 0.5) Bool.pick(Hsl, F32.is_eq(mx, mn), Hsl{0.0, 0.0, l, a}, Color.rgb_to_hsl.chromatic(rf, gf, bf, mx, mn, l, a))# HSL -> RGB# ----------def Color.hue2rgb.go2(+p: F32, +q: F32, +t: F32, c2: Bool) -> F32: Bool.pick(F32, c2, q, Bool.pick(F32, F32.is_lt(t, F32.div(2.0, 3.0)), F32.add(p, F32.mul( F32.mul(F32.sub(q, p), F32.sub(F32.div(2.0, 3.0), t)), 6.0)), p))def Color.hue2rgb.go(+p: F32, +q: F32, +t: F32, c1: Bool) -> F32: Bool.pick(F32, c1, F32.add(p, F32.mul(F32.mul(F32.sub(q, p), 6.0), t)), Color.hue2rgb.go2(p, q, t, F32.is_lt(t, 0.5)))def Color.hue2rgb.wrap(+t: F32) -> F32: Bool.pick(F32, F32.is_lt(t, 0.0), F32.add(t, 1.0), Bool.pick(F32, F32.is_gt(t, 1.0), F32.sub(t, 1.0), t))def Color.hue2rgb(+p: F32, +q: F32, +t0: F32) -> F32: +t = Color.hue2rgb.wrap(t0) Color.hue2rgb.go(p, q, t, F32.is_lt(t, F32.div(1.0, 6.0)))def Color.hsl_to_rgb.chromatic(+h: F32, +s: F32, +l: F32, +a: F32) -> Rgb: +q = Bool.pick(F32, F32.is_lt(l, 0.5), F32.mul(l, F32.add(1.0, s)), F32.sub(F32.add(l, s), F32.mul(l, s))) +p = F32.sub(F32.mul(2.0, l), q) +hn = F32.div(h, 360.0) rf = Color.hue2rgb(p, q, F32.add(hn, F32.div(1.0, 3.0))) gf = Color.hue2rgb(p, q, hn) bf = Color.hue2rgb(p, q, F32.sub(hn, F32.div(1.0, 3.0))) Rgb{ Color.channel_to_u8(rf), Color.channel_to_u8(gf), Color.channel_to_u8(bf), a}def Color.hsl_to_rgb(c: Hsl) -> Rgb: match c: case Hsl{+h, +s, +l, +a}: +gray = Color.channel_to_u8(l) Bool.pick(Rgb, F32.is_eq(s, 0.0), Rgb{gray, gray, gray, a}, Color.hsl_to_rgb.chromatic(h, s, l, a))# Stable string formatting# ------------------------# rgba(r,g,b,a) and hsla(h,s,l,a) with no spaces. RGB channels use U32.show;# floating fields use F32.show (runtime spelling; opaque to {==} proofs).def Color.to_rgba_string(c: Rgb) -> String: match c: case Rgb{r, g, b, a}: "rgba(" ++ U32.show(r) ++ "," ++ U32.show(g) ++ "," ++ U32.show(b) ++ "," ++ F32.show(a) ++ ")"def Color.to_hsla_string(c: Hsl) -> String: match c: case Hsl{h, s, l, a}: "hsla(" ++ F32.show(h) ++ "," ++ F32.show(s) ++ "," ++ F32.show(l) ++ "," ++ F32.show(a) ++ ")"# ColorRange validation (no RNG)# ------------------------------def Color.range_rgb_le(a: Rgb, b: Rgb) -> Bool: match a b: case Rgb{ar, ag, ab, aa} Rgb{br, bg, bb, ba}: Bool.and(U32.is_le(ar, br), Bool.and(U32.is_le(ag, bg), Bool.and(U32.is_le(ab, bb), F32.is_le(aa, ba))))def Color.range_valid(r: ColorRange) -> Bool: match r: case ColorRange{min, max}: Color.range_rgb_le(min, max)