gui.color
Colours for the Salam UI standard: every colour syntax in English and Persian, checked with clear errors, conversion between colour spaces, mixing in oklab, light and dark pairs, contrast and the CSS the web target emits.
import gui.color
gui.color is the colour part of the Salam UI standard. It reads every colour a page may use, in the same syntax in page source, in a string and in a value computed at run time, and it turns a colour into what each target needs: a packed u32 for the native renderer and CSS text for the web.
A Color holds sRGB channels from 0 to 1 and an alpha. A colour from a wider gamut, such as color(display p3, 1, 0, 0), keeps channels below 0 or above 1, so nothing is lost until it is drawn.
What Parse accepts:
- Names: all 148 CSS colour names plus
transparentandcurrent(the text colour), in any case and with or without spaces (alice blue), and Persian names such asقرمز,آبیandقهوه ای. - Hex:
#rgb,#rgba,#rrggbband#rrggbbaa. - Functions:
rgb,rgba,hsl,hsla,hwb,lab,lch,oklab,oklch,cmyk(alsodevice cmyk) andcolor(space, c1, c2, c3)with the spacessrgb,srgb linear,display p3,a98 rgb,prophoto rgb,rec2020,xyz,xyz d50andxyz d65. Values are separated by commas, as inrgb(51, 102, 170), or by spaces with/before the alpha, as in CSS. The hyphenated CSS spellings (display-p3) work too. mix(a, b, amount, space):amountis how much ofb, as a number from 0 to 1 or a percent, 50% when left out; the space is oklab unless given.light dark(a, b): a colour with a light and a dark variant.- Numbers: a percent with
%or٪, Persian digits, the Persian comma،, and hue angles as a bare number of degrees or withdeg,rad,gradorturn.
Things to know:
- Values are checked, not clamped.
rgb(300, 0, 0)is an error, not red.Errorsays what was expected and the column where the problem starts, and suggests a name when one is close. Csswrites what every current browser draws the same:#rrggbbor#rrggbbaainside sRGB,color(display-p3 ...)for a wide gamut colour that fits Display P3,oklch(...)beyond that,currentColorandlight-dark(a, b).Argbclamps each channel to sRGB, so a wide gamut colour is drawn as its nearest sRGB colour on the native target.cmykis the plain formula (red is(1 - c) * (1 - k)), with no printer profile.currentcannot be mixed while parsing, because the text colour is only known when the page is shown.
A light and dark palette
A theme starts from one brand colour. Each role gets a LightDark pair, Lighten and Darken move the lightness in oklch and stay inside sRGB, and Mix works on both variants at once. Contrast then checks every pair against the 4.5 ratio that WCAG asks of body text, in light and in dark mode.
import gui.color
import math
func r2(v: f64): f64:
ret math.Round(v * 100.0) / 100.0
end
func check(name: str, text: color.Color, back: color.Color):
light := color.Contrast(color.Light(text), color.Light(back))
dark := color.Contrast(color.Dark(text), color.Dark(back))
mut verdict := "ok"
if light < 4.5 or dark < 4.5:
verdict = "too low for body text"
end
println name, "light", r2(light), "dark", r2(dark), verdict
end
func main:
mut brand := color.Color {}
if not color.Parse("#3366aa", brand):
ret 1
end
page := color.LightDark(color.Hex(0xffffff), color.Hex(0x121212))
text := color.LightDark(color.Hex(0x1a1a1a), color.Hex(0xeeeeee))
accent := color.LightDark(brand, color.Lighten(brand, 0.2))
muted := color.Mix(text, page, 0.45)
hover := color.LightDark(color.Darken(brand, 0.08), color.Lighten(brand, 0.28))
println "page ", color.Css(page)
println "text ", color.Css(text)
println "accent ", color.Css(accent)
println "muted ", color.Css(muted)
println "hover ", color.Css(hover)
check("text on page ", text, page)
check("accent on page", accent, page)
check("muted on page ", muted, page)
endChecking colours that people type
A settings screen that takes a colour as text needs to say what is wrong with it. Error returns an empty string for a colour Parse accepts, and otherwise one line with the column and the expected form, so it can be shown under the field as it is.
import gui.color
func main:
inputs := ["#3366aa", "rgb(51 102 170 / 50%)", "hsl(210, 50%, 40%)", "mix(red, blue, 30%)", "rgb(300, 0, 0)", "hsl(210 50%)", "darkgrey", "dark grey", "darkgray", "lightgreem", "rgb(51, 102 170)", "#12345"]
mut accepted := 0
each text in inputs:
message := color.Error(text)
if message == "":
mut c := color.Color {}
color.Parse(text, c)
println "ok ", text, "=>", color.Css(c)
accepted++
else:
println "refused", text
println " ", message
end
end
println accepted, "of", inputs.len(), "accepted"
endTypes
Color
struct Colorlight dark. Channels may be below 0 or above 1 for colours outside sRGB.| Name | Type | Description |
|---|---|---|
r | f64 = 0 | The red channel, 0 to 1 inside sRGB. |
g | f64 = 0 | The green channel, 0 to 1 inside sRGB. |
b | f64 = 0 | The blue channel, 0 to 1 inside sRGB. |
a | f64 = 1 | The alpha, from 0 (transparent) to 1 (opaque), not premultiplied. |
current | bool = false | True for current, the text colour of the element; the channels are then unused. |
has_dark | bool = false | True when the colour has a dark variant, as made by light dark(a, b) or LightDark. |
dark_r | f64 = 0 | The red channel of the dark variant. Read the dark variant with Dark instead of these fields. |
dark_g | f64 = 0 | The green channel of the dark variant. |
dark_b | f64 = 0 | The blue channel of the dark variant. |
dark_a | f64 = 1 | The alpha of the dark variant. |
dark_current | bool = false | True when the dark variant is current. |
import gui.color
func main:
c := color.Rgba(51, 102, 170, 0.5)
println "r", c.r, "g", c.g, "b", c.b, "a", c.a, "current", c.current
wide := color.New(1.093, -0.227, -0.15, 1.0)
println "wide gamut red keeps channels outside 0 to 1:", wide.r, wide.g
println "argb", color.Argb(c), "back", color.Css(color.FromArgb(color.Argb(c)))
println "argb clamps", color.Argb(wide)
endFunctions
Parse
func Parse(text: str, out &: Color): boolout and returns true. On an error it returns false and leaves out as it was; Error gives the message.import gui.color
func main:
each text in ["#3366aa", "rgb(51, 102, 170)", "hsl(210, 50%, 40%)", "oklch(0.7 0.25 30)", "light dark(white, #222)", "قرمز", "rgb(51, 102)", "redd"]:
mut c := color.Color {}
if color.Parse(text, c):
println text, "=>", color.Css(c)
else:
println text, "=>", color.Error(text)
end
end
endError
func Error(text: str): strParse accepts the text, and otherwise a message that starts with the column and says what was expected. The caller owns the string.The example for this item is shown with the ones above: gui-color-Parse.salam
New
func New(r: f64, g: f64, b: f64, a: f64): Colorimport gui.color
func main:
println color.Css(color.Rgb(51, 102, 170))
println color.Css(color.Rgba(51, 102, 170, 0.5))
println color.Css(color.Hex(0x3366aa))
println color.Css(color.New(0.2, 0.4, 0.6, 1.0))
println color.Css(color.Hsl(210.0, 0.5, 0.4))
println color.Css(color.Hwb(194.0, 0.0, 0.0))
println color.Css(color.Lab(29.2345, 39.3825, 20.0664))
println color.Css(color.Lch(52.2345, 72.2, 56.2))
println color.Css(color.Oklab(0.40101, 0.1147, 0.0453))
println color.Css(color.Oklch(0.7, 0.25, 30.0))
println color.Css(color.Cmyk(0.0, 0.4, 0.6, 0.1))
println color.Css(color.WithAlpha(color.Hex(0x3366aa), 0.25))
println color.Css(color.Current())
endRgb
func Rgb(r: int, g: int, b: int): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Rgba
func Rgba(r: int, g: int, b: int, a: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Hex
func Hex(v: int): Color0xrrggbb.The example for this item is shown with the ones above: gui-color-Build.salam
FromArgb
func FromArgb(v: u32): Color0xAARRGGBB value, straight (not premultiplied) alpha.The example for this item is shown with the ones above: gui-color-Fields.salam
Argb
func Argb(c: Color): u320xAARRGGBB with straight alpha, clamping every channel to sRGB. Gives the light variant of a light dark colour and 0 for current.The example for this item is shown with the ones above: gui-color-Fields.salam
Hsl
func Hsl(h: f64, s: f64, l: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Hwb
func Hwb(h: f64, w: f64, b: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Lab
func Lab(l: f64, a: f64, b: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Lch
func Lch(l: f64, c: f64, h: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Oklab
func Oklab(l: f64, a: f64, b: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Oklch
func Oklch(l: f64, c: f64, h: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Cmyk
func Cmyk(c: f64, m: f64, y: f64, k: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
WithAlpha
func WithAlpha(c: Color, a: f64): ColorThe example for this item is shown with the ones above: gui-color-Build.salam
Current
func Current(): Colorcurrent, the text colour of the element, which the renderer fills in.The example for this item is shown with the ones above: gui-color-Build.salam
LightDark
func LightDark(light: Color, dark: Color): Colorimport gui.color
func main:
c := color.LightDark(color.Hex(0x3366aa), color.Rgb(200, 220, 255))
println color.Css(c), color.HasDark(c), color.HasDark(color.Hex(0x3366aa))
println "light", color.Css(color.Light(c)), "dark", color.Css(color.Dark(c))
println "fields", c.has_dark, c.dark_r, c.dark_g, c.dark_b, c.dark_a, c.dark_current
mut t := color.Color {}
if color.Parse("light dark(current, white)", t):
println color.Css(t), t.current, t.dark_current
end
println "mix keeps both", color.Css(color.Mix(c, color.Hex(0xffffff), 0.5))
endLight
func Light(c: Color): ColorThe example for this item is shown with the ones above: gui-color-LightDark.salam
Dark
func Dark(c: Color): ColorThe example for this item is shown with the ones above: gui-color-LightDark.salam
HasDark
func HasDark(c: Color): boolThe example for this item is shown with the ones above: gui-color-LightDark.salam
Css
func Css(c: Color): str#rrggbb or #rrggbbaa inside sRGB, color(display-p3 ...) or oklch(...) outside it, currentColor, or light-dark(a, b). The caller owns the string.import gui.color
func show(text: str):
mut c := color.Color {}
if color.Parse(text, c):
println text
println " css ", color.Css(c)
println " source", color.Source(c)
println " in sRGB", color.InGamut(c), "same as #f00", color.Equal(c, color.Hex(0xff0000))
end
end
func main:
show("red")
show("rgba(0, 0, 0, 0.25)")
show("color(display p3, 1, 0, 0)")
show("color(rec2020, 1, 0, 0)")
show("current")
endSource
func Source(c: Color): strcolor.Rgb(51, 102, 170), that builds the same colour without parsing. The caller owns the string.The example for this item is shown with the ones above: gui-color-Css.salam
Mix
func Mix(a: Color, b: Color, amount: f64): Coloramount is how much of b, clamped to 0 to 1. Light dark colours mix each variant.import gui.color
func main:
red := color.Hex(0xff0000)
blue := color.Hex(0x0000ff)
println "Mix 30%", color.Css(color.Mix(red, blue, 0.3))
each space in ["oklab", "oklch", "srgb", "hsl"]:
println "MixIn", space, color.Css(color.MixIn(red, blue, 0.5, space))
end
clear := color.Rgba(255, 0, 0, 0.0)
repeat 5 in i:
t := i as f64 / 4.0
println "Lerp", t, color.Css(color.Lerp(clear, blue, t))
end
endMixIn
func MixIn(a: Color, b: Color, amount: f64, space: str): ColorMix, in the colour space named by space (oklab, oklch, lab, lch, srgb, srgb linear, hsl, hwb, display p3, a98 rgb, prophoto rgb, rec2020, xyz, xyz d50, xyz d65). Hues take the shorter arc. An unknown name mixes in oklab.The example for this item is shown with the ones above: gui-color-Mix.salam
Lerp
func Lerp(a: Color, b: Color, t: f64): Colora to b for transitions, in oklab with premultiplied alpha. t is not clamped, so an easing curve may overshoot. With current it switches at the middle.The example for this item is shown with the ones above: gui-color-Mix.salam
Luminance
func Luminance(c: Color): f64import gui.color
import math
func r2(v: f64): f64:
ret math.Round(v * 100.0) / 100.0
end
func main:
white := color.Hex(0xffffff)
each hex in [0x777777, 0x3366aa, 0x1a1a1a]:
c := color.Hex(hex)
println color.Css(c), "luminance", r2(color.Luminance(c)), "contrast on white", r2(color.Contrast(c, white))
end
base := color.Hex(0x3366aa)
println "lighter", color.Css(color.Lighten(base, 0.1)), "darker", color.Css(color.Darken(base, 0.1))
endContrast
func Contrast(a: Color, b: Color): f64The example for this item is shown with the ones above: gui-color-Contrast.salam
Lighten
func Lighten(c: Color, amount: f64): Coloramount (0 to 1). A colour inside sRGB loses chroma where needed to stay inside it.The example for this item is shown with the ones above: gui-color-Contrast.salam
Darken
func Darken(c: Color, amount: f64): Coloramount, like Lighten the other way.The example for this item is shown with the ones above: gui-color-Contrast.salam
InGamut
func InGamut(c: Color): boolCss writes it as hex.The example for this item is shown with the ones above: gui-color-Css.salam
Equal
func Equal(a: Color, b: Color): boolcurrent and both variants.The example for this item is shown with the ones above: gui-color-Css.salam