A perceptual color intelligence library for Go β OKLCH under the hood, a human vocabulary on the surface.
Note
Wondertone is still in early development, colours may look not beautiful and stylish yet, but I am making researches on colour math and systems. Feel free to open issues and discussions. Feedback and contributions welcome!
Caution
Breaking changes in v0.2.0
Wondertone is still in Development. The API and colour output are not yet stable.
-
Hex output shifts.
Tone.Hex()and all render output will produce slightly different values than v0.1.x. The WonderMath pipeline (corrected hue, perceptual chroma, energy glow) changes what every tone renders to. Update any hardcoded hex values or golden test fixtures. -
Temperature()behaviour changed. The result is now computed from a continuous formula that factors in chroma and lightness, not just hue ranges. A near-achromatic tone that previously returned"neutral"may now return"warm"or"cool"depending on its hue angle. -
EffectiveC()is no longer linear. It previously returnedC Γ Energy. It now returnsC Γ Energy^Ξ³(Ξ³β0.7, Stevens' power law). The same Energy value produces a different chroma output. -
Vibrancy β chroma mapping changed.
PerceivedChromanow applies a power-law exponent (V^Ξ±, Ξ±=0.9) and a per-hue weight k(H). Blues get slightly more chroma at the same Vibrancy value; yellows get slightly less.
import tone "github.com/leraniode/wondertone/core"
spark := tone.New(
tone.Light(75),
tone.Vibrancy(80),
tone.Hue(30),
tone.Energy(0.9),
tone.Named("Primary Spark"),
tone.Moody("vibrant"),
)
fmt.Println(spark.Hex()) // #f5a04a (gamut-safe, always)Wondertone speaks a new vocabulary. Instead of OKLCH's L, C, H β which require color-science knowledge β wondertone gives you:
| Term | Range | Meaning |
|---|---|---|
| Light | 0β100 | Perceptual lightness. 0=black, 100=white. |
| Vibrancy | 0β100 | Colorfulness as % of gamut max. 0=grey, 100=most vivid possible. |
| Hue | 0β360 | Color angle. 0=red, 120=green, 240=blue. |
| Energy | 0β1 | Aliveness multiplier. 1=full, 0=muted. |
Under the hood: OKLCH, OKLab mixing, binary-search gamut safety, perceptual tone scales. You never have to know any of that.
go get github.com/leraniode/wondertoneImport alias convention:
import (
tone "github.com/leraniode/wondertone/core"
palette "github.com/leraniode/wondertone/palette"
builtin "github.com/leraniode/wondertone/palette/builtin"
colour "github.com/leraniode/wondertone/colour"
render "github.com/leraniode/wondertone/render"
"github.com/leraniode/wondertone/wtone"
)// Wondertone vocabulary (recommended)
t := tone.New(
tone.Light(68),
tone.Vibrancy(72),
tone.Hue(142),
tone.Energy(0.95),
tone.Named("Unix"),
tone.Moody("focused"),
)
// From hex
t, err := tone.FromHex("#e94560")
t := tone.MustFromHex("#e94560")
// From raw OKLCH (power users)
t := tone.FromOKLCH(0.68, 0.18, 142.0)
// From .wtone string
t, err := tone.FromOKLCHString("0.68 0.18 142")t.Light() // float64 [0β100]
t.Vibrancy() // float64 [0β100]
t.Hue() // float64 [0β360)
t.Energy() // float64 [0β1]
t.Name() // string
t.Mood() // string
t.Hex() // "#rrggbb"
t.RGB() // uint8, uint8, uint8
t.RGBFloat() // float64, float64, float64
t.OKLCH() // l, c, h float64 (raw values β power users)
t.IsLight() // bool
t.IsDark() // bool
t.Temperature() // "warm" | "cool" | "neutral"
t.EffectiveC() // C Γ Energy β what actually rendersAll methods return a new Tone β originals are never modified.
t.WithLight(80)
t.WithVibrancy(50)
t.WithHue(200)
t.WithEnergy(0.5)
t.WithName("New Name")
t.WithMood("serene")
t.WithAlpha(0.8)
t.Lighten(15) // Light += 15
t.Darken(15) // Light -= 15
t.Saturate(20) // Vibrancy += 20
t.Desaturate(20) // Vibrancy -= 20
t.Rotate(90) // Hue += 90
t.Complement() // Hue += 180Energy scales chroma at render time without altering the stored tone:
vivid := colour.Bloom // Energy=1.0
quiet := colour.Bloom.WithEnergy(0.3) // same color, quieter
vivid.Hex() == quiet.Hex() // false β different render
vivid.OKLCH() // same L, C, H
quiet.OKLCH() // same L, C, H β stored truth unchanged12-step perceptual ladder from any tone. Hue never drifts. Every step guaranteed in-gamut.
scale := colour.Unix.Scale()
scale.Background() // step 1 β page background
scale.SubtleBackground() // step 2 β stripes, alternating rows
scale.ElementBackground()// step 3 β cards, inputs
scale.HoveredBackground()// step 4 β hover state
scale.ActiveBackground() // step 5 β selected
scale.SubtleBorder() // step 6 β separators
scale.Border() // step 7 β input borders
scale.StrongBorder() // step 8 β focus rings
scale.Solid() // step 9 β buttons, badges
scale.HoveredSolid() // step 10 β button hover
scale.Text() // step 11 β body text
scale.HighContrastText() // step 12 β headings
scale.Step(n) // step n [1β12]
scale.All() // []Tone, all 12// Mix two tones in OKLab space (no grey midpoint artifacts)
mid := tone.Mix(a, b, 0.5)
// Gradient β n steps from a to b
steps, err := tone.Gradient(a, b, 10)
// Weighted blend of multiple tones
result, err := tone.Blend([]tone.Tone{a, b, c}, []float64{1, 2, 1})
// Harmony
tones, err := tone.Harmonize(base, "complement") // 2 tones
tones, err := tone.Harmonize(base, "triadic") // 3 tones
tones, err := tone.Harmonize(base, "analogous") // 3 tones
tones, err := tone.Harmonize(base, "split") // 3 tones
tones, err := tone.Harmonize(base, "tetradic") // 4 tones
// Temperature shift
warmer, err := tone.Shift(t, "warmer", 0.3)
cooler, err := tone.Shift(t, "cooler", 0.5)ratio := fg.ContrastWith(bg) // float64 [1β21]
fg.PassesAA(bg) // bool β 4.5:1
fg.PassesAAA(bg) // bool β 7.0:1
fixed := fg.EnsureContrast(bg, "AA") // adjusts lightness onlyv0.2 introduces WonderMath: a layer of perceptual corrections and new dimensions built above OKLCH. Every tone now passes through this pipeline at render time.
OKLCH blues at high chroma drift toward purple. WonderMath applies a chroma-weighted Gaussian correction. Grey tones are never affected.
// Before v0.2: vivid blue at H=250 could look slightly purple
// After v0.2: corrected back to true blue automatically
blue := tone.New(tone.Light(50), tone.Vibrancy(95), tone.Hue(250))
fmt.Println(blue.Hex()) // perceptually accurate blueYellow naturally appears more vivid than blue at the same raw chroma.
WonderMath applies a per-hue weight k(H) so Vibrancy(80) feels
equally vivid at every hue:
yellow := tone.New(tone.Light(70), tone.Vibrancy(80), tone.Hue(60))
blue := tone.New(tone.Light(70), tone.Vibrancy(80), tone.Hue(240))
// These now feel equally vivid β yellow reduced, blue boostedEnergy uses Stevens' power law (E^Ξ³, Ξ³=0.7).
Energy=0.5 now genuinely feels half as alive, not 60%.
full := colour.Bloom // Energy=1.0 β full aliveness
half := colour.Bloom.WithEnergy(0.5) // feels half as alive
quiet := colour.Bloom.WithEnergy(0.2) // barely a whisper
// Whole palette β quieten everything at once
hushed := builtin.Midnight().WithEnergy(0.4)Temperature() now returns a label driven by a real formula that factors
in hue, chroma, and lightness. TemperatureScalar() gives you the raw value:
ember := tone.New(tone.Light(60), tone.Vibrancy(70), tone.Hue(25))
glacier := tone.New(tone.Light(60), tone.Vibrancy(70), tone.Hue(196))
ember.Temperature() // "warm"
ember.TemperatureScalar() // 0.74
glacier.Temperature() // "cool"
glacier.TemperatureScalar() // -0.61Mood is now computed from Valence and Arousal β mathematical properties
of the tone. DerivedMoodValue() gives the derived mood. Mood() returns
your manual override (or the derived value if none is set).
vivid := tone.New(tone.Light(65), tone.Vibrancy(95), tone.Hue(40), tone.Energy(1.0))
muted := tone.New(tone.Light(35), tone.Vibrancy(15), tone.Hue(220), tone.Energy(0.2))
vivid.DerivedMoodValue() // "playful"
vivid.ValenceValue() // 0.79 β positive, warm
vivid.ArousalValue() // 0.97 β activated
muted.DerivedMoodValue() // "deep"
muted.ValenceValue() // 0.18 β low positive
muted.ArousalValue() // 0.14 β calm
// Manual override still works
named := vivid.WithMood("sunrise") // custom label, math still runs
named.Mood() // "sunrise"
named.DerivedMoodValue() // "playful" β math unchangedimport "github.com/leraniode/wondertone/colour"
colour.Unix // terminal green, focused
colour.Starlight // deep indigo, mystical
colour.Ember // warm amber, comfortable
colour.Glacier // cool cyan, serene
colour.Crimson // signal red, urgent
colour.Void // near-black, deep
colour.Dawn // soft pink-orange, hopeful
colour.Bloom // vivid magenta, joyful
colour.Slate // neutral blue-grey, calm
colour.Signal // success green, earned
colour.Ink // near-black text, intentional
colour.Paper // warm off-white, easy
colour.All() // []Tone β all 12p, err := palette.New("My Palette").
Description("A beautiful collection").
Mood("vibrant").
Author("leraniode").
Add(colour.Unix).
Add(colour.Starlight).
Add(colour.Crimson).
Build()
p.Get("Unix") // (Tone, bool)
p.MustGet("Unix") // Tone β panics if missing
p.At(0) // Tone β by index
p.Has("Unix") // bool
p.All() // []Tone
p.Len() // int
// Modify
fork := p.Fork("My Fork").Add(newTone).Build()
ext, _ := p.Extend("Extended", extraTone)
rep, _ := p.Replace("Unix", differentTone)
quiet := p.WithEnergy(0.4) // quieten every tone at once
// Validate
report := p.Validate()
fmt.Println(report) // β My Palette β all checks passedpalette.Complementary(base) // 2 tones
palette.Triadic(base) // 3 tones
palette.Analogous(base, 5, 30) // 5 tones, 30Β° spread
palette.SplitComplementary(base, 30) // 3 tones
palette.Tetradic(base) // 4 tones
palette.Monochrome(base, 8) // 8 tones, same hue
palette.Rainbow(base, 12) // 12 tones, full wheelratio, err := palette.ContrastPair(p, "Midnight Text", "Midnight Base")
fixed, err := palette.EnsurePairContrast(p, "Text", "Background", "AA")
matrix := palette.ContrastMatrix(p) // every pair
pairs := palette.FindReadablePairs(p, "AA") // passing pairs onlyimport "github.com/leraniode/wondertone/palette/builtin"
builtin.Midnight() // deep dark navy
builtin.Aurora() // bright airy light
builtin.Ember() // warm amber dark
builtin.Glacier() // cool icy dark
builtin.Rosewood() // rich rose dark
builtin.All() // []*Palette β all five
builtin.Names() // []stringimport render "github.com/leraniode/wondertone/render"
profile := render.Detect() // TrueColor | ANSI256 | ANSI16 | NoColor
render.FG(t, profile) // foreground escape sequence
render.BG(t, profile) // background escape sequence
render.Colorize(t, profile, "text") // wrap text with FG + reset
render.ColorizeOnBG(fg, bg, profile, "text")
render.Swatch(t, profile, 2) // colored block preview
// Lipgloss integration
color := render.LipglossColor(t, profile) // string for lipgloss.Color()Downsampling for ANSI256/ANSI16 uses OKLab ΞE nearest-neighbor β perceptual accuracy, not RGB distance.
The .wtone format is wondertone's native, human-editable palette file. Designers can edit them without touching Go code.
name = "Leraniode Starlight"
description = "bright starful leraniode"
mood = "joyful"
version = "1.0.0"
author = "leraniode"
[[colors]]
name = "Primary Spark"
l = 0.75
c = 0.15
h = 30.0
energy = 0.85
mood = "vibrant"
[[colors]]
name = "Accent Glow"
oklch = "0.60 0.20 200" # shorthand β L C H
energy = 0.70import "github.com/leraniode/wondertone/wtone"
// Load
p, err := wtone.LoadWTone("my-palette.wtone")
// Parse from embedded bytes (go:embed)
//go:embed my-palette.wtone
var paletteFile []byte
p, err := wtone.ParseWTone(paletteFile)
// Save
err := wtone.SaveWTone("output.wtone", p)
// Marshal to bytes
data, err := wtone.MarshalWTone(p)Wondertone has Adapters for two libraries:
The adapters convert between wondertone's OKLCH and the target library's color model, so you can use wondertone's colour intelligence in your existing projects without a full rewrite.
They are located in the adapters/ package, as a seperate module
go get github.com/leraniode/wondertone/adapters/lipglossgo get github.com/leraniode/wondertone/adapters/colourfulEach adapter has its respective dependency and wondertone as a dependency.
Import the adapter as wtlip to avoid conflict with lipgloss package API:
package main
import (
tone "github.com/leraniode/wondertone/core"
wtlip "github.com/leraniode/wondertone/adapters/lipgloss"
)
wtlip.FG(colour.Unix).Bold(true).Render("hello")
wtlip.Style(colour.Unix).
Background(colour.Void).
Padding(0, 1).
Render("hello")
wtlip.PaletteStyles(builtin.Midnight()) // map[name]lipgloss.StyleImport the adapter as wcolourful to avoid conflict with go-colourful package API:
package main
import (
tone "github.com/leraniode/wondertone/core"
wcolorful "github.com/leraniode/wondertone/adapters/colorful"
)
// wondertone β go-colorful
cf := wcolorful.ToColorful(myTone)
lab, _ := cf.Lab()
// go-colorful β wondertone
t := wcolorful.FromColorful(cf)
fmt.Println(t.Hex())wondertone/
βββ adapters/ Lipgloss and go-colourful adapters (seperate module with dependencies)
β βββ lipgloss/ Lipgloss adapter
β βββ colourful/ go-colourful adapter
βββ example/main.go Example code demonstrating usage
βββ core/ Tone type, OKLCH pipeline, WonderMath, gamut, mix, scale
β βββ wondermath.go WonderSpace: corrected hue, perceived chroma, energy, mood
βββ palette/ Palette, harmony, contrast
β βββ builtin/ Midnight, Aurora, Ember, Glacier, Rosewood
βββ colour/ Leraniode named tones (one file per tone)
βββ render/ Terminal output, profile detection, lipgloss adapter
βββ wtone/ .wtone file load/save
βββ internal/
βββ testutil/ Zero-dependency test helpers
Dependencies: github.com/BurntSushi/toml (wtone only). The core/, palette/, colour/, and render/ packages have zero external dependencies.
- OKLCH-first internally β RGB is strictly output/terminal only
- WonderMath above OKLCH β perceptual corrections as a clean layer, not patches
- Gamut safety mandatory β iterative chroma reduction, hue never drifts
- Human vocabulary β Light, Vibrancy, Hue, Energy, Temperature, Mood
- Immutable by default β every method returns a new Tone
- Energy is expressive β same palette, different aliveness
- Mood is mathematical β derived from Valence + Arousal, not just a tag
- One file per tone β the
colour/package is a browseable gallery - .wtone is the primary tool β wondertone speaks in this format, you can use it to create tones, palettes, styles in wondertone
MIT β Leraniode
Colourful Leraniode β’ Part of Leraniode β Building Tools that feel alive π±.