From 49124ee973427b71bdb06947167fdd0b8779b70e Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Wed, 15 Jul 2026 07:50:56 -0400 Subject: [PATCH] v0.1.1 minor blend updates, lazy LUT build, example added --- .gitignore | 1 - blend.go | 32 ++++++- examples/cmd/main.go | 197 +++++++++++++++++++++++++++++++++++++++++++ rgb.go | 3 +- 4 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 examples/cmd/main.go diff --git a/.gitignore b/.gitignore index 9f0d689..d35f912 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ bin/ dev/ logs/ log/ -examples/ catalog.txt combined.txt diff --git a/blend.go b/blend.go index 4cc3420..38b547a 100644 --- a/blend.go +++ b/blend.go @@ -1,17 +1,22 @@ package color -import "math" +import ( + "math" + "sync" +) const softLightLUTSize = 256 // Perez SoftLight lookup tables (array access, no pointers) // Pre-computed at init to avoid sqrt/division in per-cell loops var ( - softLightG [softLightLUTSize]float64 - softLightDF [softLightLUTSize]float64 + softLightG [softLightLUTSize]float64 + softLightDF [softLightLUTSize]float64 + softLightOnce sync.Once ) -func init() { +// buildSoftLightLUT populates the Perez soft light tables. Called once, lazily. +func buildSoftLightLUT() { for i := range softLightLUTSize { df := float64(i) / 255.0 softLightDF[i] = df @@ -98,6 +103,7 @@ func Blend(dst, src RGB, alpha float64) RGB { // SoftLight applies Perez soft light blend, gentler than linear alpha // intensity in [0,1] mixes between dst and the blended result func SoftLight(dst, src RGB, intensity float64) RGB { + softLightOnce.Do(buildSoftLightLUT) return RGB{ R: softLightChannel(dst.R, src.R, intensity), G: softLightChannel(dst.G, src.G, intensity), @@ -185,3 +191,21 @@ func Grayscale(c RGB) RGB { g := Luma(c) return RGB{R: g, G: g, B: g} } + +// Desaturate is partial desaturation; t=0 identity, t=1 full grayscale +func Desaturate(c RGB, t float64) RGB { return c.Lerp(Grayscale(c), t) } + +// Lerp is free form of RGB.Lerp, uniform with the rest of the blend family +func Lerp(a, b RGB, t float64) RGB { return a.Lerp(b, t) } + +// LerpFixed interpolates a→b using fixed-point factor t with the given +// fractional bit count. t is nominally in [0, 1<> shift)), + G: uint8(int64(a.G) + (((int64(b.G) - int64(a.G)) * t) >> shift)), + B: uint8(int64(a.B) + (((int64(b.B) - int64(a.B)) * t) >> shift)), + } +} diff --git a/examples/cmd/main.go b/examples/cmd/main.go new file mode 100644 index 0000000..70dad4f --- /dev/null +++ b/examples/cmd/main.go @@ -0,0 +1,197 @@ +package main + +import ( + "context" + "fmt" + stdcolor "image/color" + "math" + "os" + "os/signal" + "strings" + "time" + + "github.com/lixenwraith/color" +) + +const reset = "\x1b[0m" + +// bg returns the SGR sequence for a 24-bit background. +func bg(c color.RGB) string { return fmt.Sprintf("\x1b[48;2;%d;%d;%dm", c.R, c.G, c.B) } + +// fg returns the SGR sequence for a 24-bit foreground. +func fg(c color.RGB) string { return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", c.R, c.G, c.B) } + +// block renders n background-colored spaces as a swatch. +func block(c color.RGB, n int) string { return bg(c) + strings.Repeat(" ", n) + reset } + +func showPalette() { + fmt.Println("── Named palette ──") + entries := []struct { + name string + c color.RGB + }{ + {"Black", color.Black}, {"White", color.White}, {"Red", color.Red}, + {"Orange", color.Orange}, {"Gold", color.Gold}, {"Yellow", color.Yellow}, + {"Lime", color.Lime}, {"ForestGreen", color.ForestGreen}, {"Teal", color.Teal}, + {"Cyan", color.Cyan}, {"RoyalBlue", color.RoyalBlue}, {"Blue", color.Blue}, + {"Magenta", color.Magenta}, {"HotPink", color.HotPink}, {"Coral", color.Coral}, + {"Silver", color.Silver}, + } + for _, e := range entries { + fmt.Printf("%s %-12s %s\n", block(e.c, 4), e.name, e.c.Hex()) + } + fmt.Println() +} + +func showBlendModes() { + fmt.Println("── Blend modes (dst=NavyBlue, src=Orange, alpha=0.6) ──") + dst, src := color.NavyBlue, color.Orange + const a = 0.6 + fmt.Printf("%s dst %s src\n", block(dst, 6), block(src, 6)) + modes := []struct { + name string + out color.RGB + }{ + {"Blend", color.Blend(dst, src, a)}, + {"Add", color.Add(dst, src, a)}, + {"Screen", color.Screen(dst, src, a)}, + {"Overlay", color.Overlay(dst, src, a)}, + {"SoftLight", color.SoftLight(dst, src, a)}, + {"Max", color.Max(dst, src, a)}, + } + for _, m := range modes { + fmt.Printf("%s %-10s %s\n", block(m.out, 6), m.name, m.out.Hex()) + } + fmt.Println() +} + +func showGradient() { + fmt.Println("── Lerp gradient: DarkCrimson → Gold → Teal ──") + stops := []color.RGB{color.DarkCrimson, color.Gold, color.Teal} + const steps = 36 + var line strings.Builder + for i := 0; i < steps; i++ { + seg := (float64(i) / float64(steps-1)) * float64(len(stops)-1) + idx := int(seg) + if idx >= len(stops)-1 { + idx = len(stops) - 2 + } + line.WriteString(block(color.Lerp(stops[idx], stops[idx+1], seg-float64(idx)), 1)) + } + fmt.Println(line.String()) + fmt.Println() +} + +func showGrayscale() { + fmt.Println("── Grayscale, Desaturate, Luma ──") + base := color.Coral + fmt.Printf("%s base %s luma=%d\n", block(base, 6), base.Hex(), color.Luma(base)) + fmt.Printf("%s grayscale %s\n", block(color.Grayscale(base), 6), color.Grayscale(base).Hex()) + var line strings.Builder + for i := 0; i <= 8; i++ { + line.WriteString(block(color.Desaturate(base, float64(i)/8.0), 3)) + } + fmt.Printf("%s desaturate t=0→1\n\n", line.String()) +} + +func showHex() { + fmt.Println("── Hex parse / format ──") + for _, s := range []string{"#ff8800", "0f0", "#1a1b26", "bad!"} { + c, err := color.ParseHex(s) + if err != nil { + fmt.Printf(" %-8q → error: %v\n", s, err) + continue + } + fmt.Printf(" %-8q → %s %s%s%s\n", s, block(c, 3), fg(c), c.Hex(), reset) + } + c := color.MustParseHex("#41c7c7") + fmt.Printf(" MustParseHex(#41c7c7) → %s %s\n\n", block(c, 3), c.Hex()) +} + +func showNearest() { + fmt.Println("── Nearest palette color (RedmeanDistance) ──") + named := []struct { + name string + c color.RGB + }{ + {"Red", color.Red}, {"Orange", color.Orange}, {"Gold", color.Gold}, + {"Lime", color.Lime}, {"Teal", color.Teal}, {"Blue", color.Blue}, + {"Magenta", color.Magenta}, {"White", color.White}, {"Black", color.Black}, + } + for _, hex := range []string{"#ff5522", "#118ab2", "#2b2b2b"} { + target := color.MustParseHex(hex) + best, bestD := named[0], color.RedmeanDistance(target, named[0].c) + for _, n := range named[1:] { + if d := color.RedmeanDistance(target, n.c); d < bestD { + best, bestD = n, d + } + } + fmt.Printf(" %s %s ≈ %s %-8s %s\n", block(target, 3), hex, block(best.c, 3), best.name, best.c.Hex()) + } + fmt.Println() +} + +func showLerpFixed() { + fmt.Println("── LerpFixed (Q16.16 fixed-point) ──") + const shift = 16 + a, b := color.RoyalBlue, color.Gold + var line strings.Builder + for i := 0; i <= 16; i++ { + t := int64(i) << (shift - 4) // i/16 expressed in Q16.16; spans [0, 1<