~/bend-docscommunity

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)