Skip to content

leraniode/wondertone

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

23 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

✨ wondertone 🌈🎨

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!

CI Go Reference License Go Report Card Version lipgloss adapter version go-colourful adapter version Go Modules

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 returned C Γ— Energy. It now returns C Γ— Energy^Ξ³ (Ξ³β‰ˆ0.7, Stevens' power law). The same Energy value produces a different chroma output.

  • Vibrancy β†’ chroma mapping changed. PerceivedChroma now 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)

What is wondertone?

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.


Install

go get github.com/leraniode/wondertone

Import 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"
)

Core β€” Tones

Creating tones

// 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")

Reading tones

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 renders

Manipulating tones

All 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 += 180

Energy β€” the aliveness dial

Energy 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 unchanged

Tone Scale

12-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

Mixing

// 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)

Accessibility

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 only

WonderMath β€” Perceptual Colour Science

v0.2 introduces WonderMath: a layer of perceptual corrections and new dimensions built above OKLCH. Every tone now passes through this pipeline at render time.

Corrected Hue β€” blue drift fix

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 blue

Perceptual Vibrancy β€” equal vividness across hues

Yellow 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 boosted

Energy β€” now perceptually linear

Energy 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 β€” continuous warm↔cool

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.61

Mood β€” derived from colour math

Mood 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 unchanged

Colour β€” Leraniode's named tones

import "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 12

Palette

p, 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 passed

Harmony generators

palette.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 wheel

Contrast tools

ratio, 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 only

Built-in palettes

import "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()     // []string

Render β€” terminal output

import 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.


.wtone files

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.70
import "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)

Adapters

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

Installing the adapters

Lipgloss adapter

go get github.com/leraniode/wondertone/adapters/lipgloss

go-colourful adapter

go get github.com/leraniode/wondertone/adapters/colourful

Each adapter has its respective dependency and wondertone as a dependency.

Usage

Lipgloss adapter

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.Style

go-colourful adapter

Import 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())

Package layout

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.


Design principles

  1. OKLCH-first internally β€” RGB is strictly output/terminal only
  2. WonderMath above OKLCH β€” perceptual corrections as a clean layer, not patches
  3. Gamut safety mandatory β€” iterative chroma reduction, hue never drifts
  4. Human vocabulary β€” Light, Vibrancy, Hue, Energy, Temperature, Mood
  5. Immutable by default β€” every method returns a new Tone
  6. Energy is expressive β€” same palette, different aliveness
  7. Mood is mathematical β€” derived from Valence + Arousal, not just a tag
  8. One file per tone β€” the colour/ package is a browseable gallery
  9. .wtone is the primary tool β€” wondertone speaks in this format, you can use it to create tones, palettes, styles in wondertone

License

MIT β€” Leraniode


Colourful Leraniode β€’ Part of Leraniode – Building Tools that feel alive 🌱.

About

A perceptual color intelligence library for Go 🎨🌈

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Contributors

Languages