v0.1.0 initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
.idea
|
||||||
|
bin/
|
||||||
|
dev/
|
||||||
|
logs/
|
||||||
|
log/
|
||||||
|
examples/
|
||||||
|
catalog.txt
|
||||||
|
combined.txt
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2026, Lixen Wraith
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# color
|
||||||
|
|
||||||
|
24-bit RGB values, perceptual metrics, and blend operations. No output device, no dependencies.
|
||||||
|
|
||||||
|
Extracted from [lixenwraith/terminal](https://github.com/lixenwraith/terminal) so renderers — terminal, GUI, image, framebuffer — share one color type and one set of operations without linking terminal I/O, `x/sys`, or termios.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/lixenwraith/color
|
||||||
|
```
|
||||||
|
|
||||||
|
Go 1.26+. Standard library only.
|
||||||
|
|
||||||
|
## Type
|
||||||
|
|
||||||
|
```go
|
||||||
|
type RGB struct {
|
||||||
|
R uint8 `toml:"r"`
|
||||||
|
G uint8 `toml:"g"`
|
||||||
|
B uint8 `toml:"b"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Three bytes, comparable, pointer-free: safe to embed in dense cell or pixel buffers and to bind directly from config.
|
||||||
|
|
||||||
|
`RGB` implements `image/color.Color`, so values pass into `image`, `draw`, and GUI toolkit pipelines unchanged. `From` converts back, un-premultiplying alpha and discarding it.
|
||||||
|
|
||||||
|
```go
|
||||||
|
img.Set(x, y, color.Amber) // RGB satisfies image/color.Color
|
||||||
|
c := color.From(img.At(x, y)) // back to RGB
|
||||||
|
```
|
||||||
|
|
||||||
|
Where both packages are needed at one site, alias the standard library: `import stdcolor "image/color"`.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
| Call | Behavior |
|
||||||
|
| --- | --- |
|
||||||
|
| `Blend(dst, src, alpha)` | Linear alpha compositing |
|
||||||
|
| `SoftLight(dst, src, intensity)` | Perez soft light; gentler than linear alpha |
|
||||||
|
| `Overlay(dst, src, alpha)` | Multiply on darks, screen on lights |
|
||||||
|
| `Screen(dst, src, alpha)` | Always lightens; glow accumulation without `Add` clipping |
|
||||||
|
| `Add(dst, src, alpha)` | Saturating additive |
|
||||||
|
| `Max(dst, src, alpha)` | Per-channel maximum |
|
||||||
|
| `Scale(c, factor)` | Channel multiply, saturating |
|
||||||
|
| `Grayscale(c)`, `Luma(c)` | Rec. 601 luma |
|
||||||
|
| `c.Lerp(other, t)` | Linear interpolation |
|
||||||
|
| `RedmeanDistance(a, b)` | Squared perceptual distance, for nearest-color search |
|
||||||
|
|
||||||
|
All operations are pure. `alpha` and `t` clamp to `[0,1]`; channels saturate. Integer paths avoid division; soft light is table-driven, no `sqrt` per channel.
|
||||||
|
|
||||||
|
```go
|
||||||
|
bg := color.Obsidian
|
||||||
|
glow := color.Screen(bg, color.Amber, 0.4)
|
||||||
|
edge := bg.Lerp(color.Amber, 0.75)
|
||||||
|
warm := color.SoftLight(edge, color.Terracotta, 0.3)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Palette
|
||||||
|
|
||||||
|
~120 named colors, grouped by hue and ordered dark-to-light: `Obsidian`, `Amber`, `EmeraldGreen`, `LightSkyBlue`, `Vermilion`, … Standard names (CSS, X11) where the RGB matches; descriptive compounds otherwise.
|
||||||
|
|
||||||
|
Package-level `var`s. **Read-only by contract** — the language permits assignment, the package does not. Alias them into domain parameter files rather than mutating them.
|
||||||
|
|
||||||
|
## Hex
|
||||||
|
|
||||||
|
```go
|
||||||
|
c, err := color.ParseHex("#4a90d9") // also "4a90d9", "#abc", "abc"
|
||||||
|
s := c.Hex() // "#4a90d9"
|
||||||
|
var Accent = color.MustParseHex("#ff8800")
|
||||||
|
```
|
||||||
|
|
||||||
|
`RGB` deliberately does **not** implement `encoding.TextUnmarshaler`. TOML and JSON decoders prefer it over struct-field unification, which would silently break table-form config:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
accent = { r = 255, g = 136, b = 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## With terminal
|
||||||
|
|
||||||
|
`terminal.Cell` carries `color.RGB` directly. Quantization stays device-side:
|
||||||
|
|
||||||
|
```go
|
||||||
|
idx := terminal.RGBTo256(color.EmeraldGreen) // xterm-256 index
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: when `Cell.Attrs` sets `AttrFg256` / `AttrBg256`, `Cell.Fg.R` / `Cell.Bg.R` hold a palette index, not a channel. Such values are not colors and must not be passed to this package.
|
||||||
|
|
||||||
|
## Concurrency
|
||||||
|
|
||||||
|
Values are immutable, operations are pure, lookup tables are built at package init. Safe for concurrent use.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See `LICENSE`.
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package color
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i := range softLightLUTSize {
|
||||||
|
df := float64(i) / 255.0
|
||||||
|
softLightDF[i] = df
|
||||||
|
if df <= 0.25 {
|
||||||
|
softLightG[i] = ((16.0*df-12.0)*df + 4.0) * df
|
||||||
|
} else {
|
||||||
|
softLightG[i] = math.Sqrt(df)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clampU8 converts float to uint8 with saturation
|
||||||
|
func clampU8(v float64) uint8 {
|
||||||
|
if v >= 255.0 {
|
||||||
|
return 255
|
||||||
|
}
|
||||||
|
if v <= 0.0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Round-half-up; unifies rounding across Blend/Scale/Lerp/SoftLight
|
||||||
|
return uint8(v + 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addU8 is saturating uint8 addition
|
||||||
|
func addU8(a, b uint8) uint8 {
|
||||||
|
sum := int(a) + int(b)
|
||||||
|
if sum > 255 {
|
||||||
|
return 255
|
||||||
|
}
|
||||||
|
return uint8(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fastDiv255 approximates x / 255 using integer math: (x + (x >> 8) + 1) >> 8
|
||||||
|
// Faster than DIV instruction, exact for x in [0, 255*255]
|
||||||
|
func fastDiv255(x int) int {
|
||||||
|
return (x + (x >> 8) + 1) >> 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// softLightChannel applies Perez soft light to one channel via LUTs
|
||||||
|
func softLightChannel(d, s uint8, intensity float64) uint8 {
|
||||||
|
df := softLightDF[d]
|
||||||
|
sf := softLightDF[s]
|
||||||
|
|
||||||
|
var result float64
|
||||||
|
if sf < 0.5 {
|
||||||
|
result = df - (1.0-2.0*sf)*df*(1.0-df)
|
||||||
|
} else {
|
||||||
|
// LUT replaces math.Sqrt
|
||||||
|
result = df + (2.0*sf-1.0)*(softLightG[d]-df)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lerp toward result by intensity, single dependency chain
|
||||||
|
result = df + (result-df)*intensity
|
||||||
|
|
||||||
|
return clampU8(result * 255.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// overlayChannel combines multiply (d < 128) and screen (d >= 128),
|
||||||
|
// preserving destination highlights and shadows
|
||||||
|
func overlayChannel(d, s uint8) uint8 {
|
||||||
|
if d < 128 {
|
||||||
|
return uint8(fastDiv255(2 * int(d) * int(s)))
|
||||||
|
}
|
||||||
|
return uint8(255 - fastDiv255(2*(255-int(d))*(255-int(s))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blend performs linear alpha blend of src over dst
|
||||||
|
// alpha <= 0 returns dst, alpha >= 1 returns src
|
||||||
|
func Blend(dst, src RGB, alpha float64) RGB {
|
||||||
|
if alpha >= 1.0 {
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
if alpha <= 0.0 {
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
inv := 1.0 - alpha
|
||||||
|
return RGB{
|
||||||
|
R: clampU8(float64(src.R)*alpha + float64(dst.R)*inv),
|
||||||
|
G: clampU8(float64(src.G)*alpha + float64(dst.G)*inv),
|
||||||
|
B: clampU8(float64(src.B)*alpha + float64(dst.B)*inv),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return RGB{
|
||||||
|
R: softLightChannel(dst.R, src.R, intensity),
|
||||||
|
G: softLightChannel(dst.G, src.G, intensity),
|
||||||
|
B: softLightChannel(dst.B, src.B, intensity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max returns per-channel maximum, alpha-blended over dst
|
||||||
|
func Max(dst, src RGB, alpha float64) RGB {
|
||||||
|
if alpha <= 0.0 {
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
maxed := RGB{
|
||||||
|
R: max(dst.R, src.R),
|
||||||
|
G: max(dst.G, src.G),
|
||||||
|
B: max(dst.B, src.B),
|
||||||
|
}
|
||||||
|
if alpha >= 1.0 {
|
||||||
|
return maxed
|
||||||
|
}
|
||||||
|
return Blend(dst, maxed, alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add performs saturating additive blend, alpha-blended over dst
|
||||||
|
func Add(dst, src RGB, alpha float64) RGB {
|
||||||
|
if alpha <= 0.0 {
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
added := RGB{
|
||||||
|
R: addU8(dst.R, src.R),
|
||||||
|
G: addU8(dst.G, src.G),
|
||||||
|
B: addU8(dst.B, src.B),
|
||||||
|
}
|
||||||
|
if alpha >= 1.0 {
|
||||||
|
return added
|
||||||
|
}
|
||||||
|
return Blend(dst, added, alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen applies 1-(1-dst)*(1-src), alpha-blended over dst
|
||||||
|
// Always lightens; useful for glow accumulation without clipping harshness of Add
|
||||||
|
func Screen(dst, src RGB, alpha float64) RGB {
|
||||||
|
if alpha <= 0.0 {
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
screened := RGB{
|
||||||
|
R: uint8(255 - fastDiv255((255-int(dst.R))*(255-int(src.R)))),
|
||||||
|
G: uint8(255 - fastDiv255((255-int(dst.G))*(255-int(src.G)))),
|
||||||
|
B: uint8(255 - fastDiv255((255-int(dst.B))*(255-int(src.B)))),
|
||||||
|
}
|
||||||
|
if alpha >= 1.0 {
|
||||||
|
return screened
|
||||||
|
}
|
||||||
|
return Blend(dst, screened, alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay combines multiply (darks) and screen (lights), alpha-blended over dst
|
||||||
|
func Overlay(dst, src RGB, alpha float64) RGB {
|
||||||
|
if alpha <= 0.0 {
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
overlaid := RGB{
|
||||||
|
R: overlayChannel(dst.R, src.R),
|
||||||
|
G: overlayChannel(dst.G, src.G),
|
||||||
|
B: overlayChannel(dst.B, src.B),
|
||||||
|
}
|
||||||
|
if alpha >= 1.0 {
|
||||||
|
return overlaid
|
||||||
|
}
|
||||||
|
return Blend(dst, overlaid, alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale multiplies all channels by factor, saturating (factor > 1.0 brightens)
|
||||||
|
func Scale(c RGB, factor float64) RGB {
|
||||||
|
return RGB{
|
||||||
|
R: clampU8(float64(c.R) * factor),
|
||||||
|
G: clampU8(float64(c.G) * factor),
|
||||||
|
B: clampU8(float64(c.B) * factor),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grayscale converts to grayscale using Rec. 601 luma coefficients
|
||||||
|
func Grayscale(c RGB) RGB {
|
||||||
|
// CHANGED: shared with Luma
|
||||||
|
g := Luma(c)
|
||||||
|
return RGB{R: g, G: g, B: g}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Package color provides 24-bit RGB values, perceptual metrics, and blend
|
||||||
|
// operations, independent of any output device.
|
||||||
|
//
|
||||||
|
// RGB is a plain 3-byte value with toml field tags, safe for config binding
|
||||||
|
// and for embedding in dense cell or pixel buffers. It satisfies
|
||||||
|
// image/color.Color, so values pass directly into image, draw, and GUI
|
||||||
|
// toolkit pipelines without conversion.
|
||||||
|
//
|
||||||
|
// Device concerns — terminal capability detection, xterm-256 quantization,
|
||||||
|
// SGR emission, framebuffer formats — belong in the consuming package.
|
||||||
|
//
|
||||||
|
// # Operations
|
||||||
|
//
|
||||||
|
// Blend linear alpha compositing
|
||||||
|
// SoftLight Perez soft light, gentler than linear alpha
|
||||||
|
// Overlay multiply on darks, screen on lights
|
||||||
|
// Screen always lightens, avoids the clipping harshness of Add
|
||||||
|
// Add saturating additive
|
||||||
|
// Max per-channel maximum
|
||||||
|
// Scale channel multiply, saturating
|
||||||
|
// Grayscale Rec. 601 luma
|
||||||
|
// Lerp linear interpolation
|
||||||
|
//
|
||||||
|
// # Concurrency
|
||||||
|
//
|
||||||
|
// All values are immutable and all operations are pure. Package-level palette
|
||||||
|
// variables are writable by the language but are read-only by contract.
|
||||||
|
package color
|
||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
package color
|
||||||
|
|
||||||
|
// Named 24-bit palette. Pure RGB definitions, no device or domain semantics.
|
||||||
|
// Consumers alias these in their own parameter files.
|
||||||
|
//
|
||||||
|
// Naming: standard color names where RGB closely matches (CSS, X11,
|
||||||
|
// Pantone-adjacent), descriptive compound names otherwise. Ordered dark-to-light
|
||||||
|
// within each hue group.
|
||||||
|
//
|
||||||
|
// Read-only by contract.
|
||||||
|
|
||||||
|
var (
|
||||||
|
// --- Achromatic ---
|
||||||
|
Black = RGB{0, 0, 0}
|
||||||
|
Charcoal = RGB{5, 5, 5}
|
||||||
|
Obsidian = RGB{20, 20, 30} // Blue-black
|
||||||
|
Gunmetal = RGB{26, 27, 38} // Blue-tinted near-black
|
||||||
|
DarkSlate = RGB{35, 36, 48} // Blue-gray near-black
|
||||||
|
DimGray = RGB{55, 55, 55}
|
||||||
|
DarkGray = RGB{60, 60, 60}
|
||||||
|
IronGray = RGB{80, 80, 80}
|
||||||
|
SlateGray = RGB{80, 80, 90} // Cool-tinted
|
||||||
|
Taupe = RGB{100, 95, 85} // Warm gray
|
||||||
|
Gray = RGB{120, 120, 120}
|
||||||
|
MidGray = RGB{128, 128, 128}
|
||||||
|
CoolSilver = RGB{140, 145, 155} // Blue-tinted silver
|
||||||
|
DimSilver = RGB{155, 155, 155}
|
||||||
|
Silver = RGB{180, 180, 180}
|
||||||
|
LightGray = RGB{200, 200, 200}
|
||||||
|
NearWhite = RGB{250, 250, 250}
|
||||||
|
White = RGB{255, 255, 255}
|
||||||
|
|
||||||
|
// --- Brown / Earth ---
|
||||||
|
Chocolate = RGB{90, 25, 15}
|
||||||
|
SaddleBrown = RGB{101, 67, 33}
|
||||||
|
DarkRust = RGB{140, 35, 25}
|
||||||
|
Sienna = RGB{140, 60, 0}
|
||||||
|
DarkPlum = RGB{60, 30, 40} // Warm dark purple-brown
|
||||||
|
BlueCharcoal = RGB{40, 45, 60} // Cool dark blue-gray
|
||||||
|
|
||||||
|
// --- Red ---
|
||||||
|
BlackRed = RGB{50, 15, 15}
|
||||||
|
Oxblood = RGB{100, 20, 20}
|
||||||
|
DarkBurgundy = RGB{100, 25, 20}
|
||||||
|
DarkCrimson = RGB{139, 0, 0}
|
||||||
|
Brick = RGB{180, 40, 40}
|
||||||
|
Cinnabar = RGB{200, 60, 50}
|
||||||
|
IndianRed = RGB{180, 60, 60}
|
||||||
|
BurntSienna = RGB{200, 60, 25}
|
||||||
|
Vermilion = RGB{227, 66, 82}
|
||||||
|
Red = RGB{255, 0, 0}
|
||||||
|
BrightRed = RGB{255, 60, 60}
|
||||||
|
Coral = RGB{255, 80, 80}
|
||||||
|
Salmon = RGB{255, 100, 100}
|
||||||
|
LightCoral = RGB{255, 140, 140}
|
||||||
|
LightRose = RGB{255, 150, 150}
|
||||||
|
MistyRose = RGB{255, 200, 200}
|
||||||
|
|
||||||
|
// --- Orange ---
|
||||||
|
DarkAmber = RGB{60, 40, 0}
|
||||||
|
Rust = RGB{180, 60, 20}
|
||||||
|
Amber = RGB{180, 120, 0}
|
||||||
|
Bronze = RGB{200, 100, 0}
|
||||||
|
BurntOrange = RGB{200, 110, 0}
|
||||||
|
Terracotta = RGB{220, 100, 50}
|
||||||
|
FlameOrange = RGB{240, 100, 30}
|
||||||
|
OrangeRed = RGB{255, 69, 0}
|
||||||
|
RedOrange = RGB{255, 80, 40}
|
||||||
|
Mango = RGB{255, 120, 50}
|
||||||
|
TigerOrange = RGB{255, 140, 0}
|
||||||
|
WarmOrange = RGB{255, 140, 40}
|
||||||
|
Apricot = RGB{255, 160, 60}
|
||||||
|
Orange = RGB{255, 165, 0}
|
||||||
|
|
||||||
|
// --- Yellow ---
|
||||||
|
DarkGold = RGB{200, 150, 0}
|
||||||
|
OliveYellow = RGB{200, 180, 60}
|
||||||
|
Gold = RGB{255, 215, 0}
|
||||||
|
LemonYellow = RGB{255, 240, 60}
|
||||||
|
Yellow = RGB{255, 255, 0}
|
||||||
|
PaleGold = RGB{255, 200, 100}
|
||||||
|
Buttercream = RGB{255, 250, 150}
|
||||||
|
PaleLemon = RGB{255, 255, 100}
|
||||||
|
Ivory = RGB{255, 255, 220}
|
||||||
|
Cream = RGB{255, 255, 200}
|
||||||
|
|
||||||
|
// --- Green ---
|
||||||
|
BlackGreen = RGB{0, 40, 0}
|
||||||
|
DarkFern = RGB{30, 80, 25}
|
||||||
|
DeepForest = RGB{25, 80, 35}
|
||||||
|
HunterGreen = RGB{35, 90, 30}
|
||||||
|
DarkGreen = RGB{15, 130, 15}
|
||||||
|
ForestGreen = RGB{34, 139, 34}
|
||||||
|
MediumGreen = RGB{40, 150, 40}
|
||||||
|
FernGreen = RGB{50, 140, 45}
|
||||||
|
LeafGreen = RGB{60, 160, 60}
|
||||||
|
SeaGreen = RGB{60, 180, 80}
|
||||||
|
SageGreen = RGB{70, 170, 100}
|
||||||
|
GrassGreen = RGB{70, 180, 55}
|
||||||
|
EmeraldGreen = RGB{60, 220, 100}
|
||||||
|
MintGreen = RGB{100, 220, 130}
|
||||||
|
BrightGreen = RGB{20, 200, 20}
|
||||||
|
YellowGreen = RGB{100, 220, 80}
|
||||||
|
LimeGreen = RGB{50, 205, 50}
|
||||||
|
NeonGreen = RGB{50, 255, 50}
|
||||||
|
BrightLime = RGB{120, 255, 80}
|
||||||
|
Lime = RGB{0, 255, 0}
|
||||||
|
LightGreen = RGB{144, 238, 144}
|
||||||
|
PaleGreen = RGB{120, 255, 120}
|
||||||
|
PastelGreen = RGB{100, 220, 100}
|
||||||
|
PaleMint = RGB{150, 255, 180}
|
||||||
|
Honeydew = RGB{200, 255, 200}
|
||||||
|
|
||||||
|
// --- Cyan / Teal ---
|
||||||
|
Teal = RGB{0, 139, 139}
|
||||||
|
DimCyan = RGB{0, 160, 160}
|
||||||
|
VibrantCyan = RGB{0, 200, 200}
|
||||||
|
DarkTurquoise = RGB{0, 206, 209}
|
||||||
|
BrightCyan = RGB{0, 220, 220}
|
||||||
|
Cyan = RGB{0, 255, 255}
|
||||||
|
SkyTeal = RGB{80, 200, 220}
|
||||||
|
PaleCyan = RGB{200, 255, 255}
|
||||||
|
AliceBlue = RGB{230, 245, 255}
|
||||||
|
IceCyan = RGB{240, 255, 255}
|
||||||
|
|
||||||
|
// --- Blue ---
|
||||||
|
DeepNavy = RGB{15, 25, 50}
|
||||||
|
DeepIndigo = RGB{40, 0, 180}
|
||||||
|
NavyBlue = RGB{30, 60, 120}
|
||||||
|
CobaltBlue = RGB{50, 80, 200}
|
||||||
|
SteelBlue = RGB{60, 100, 180}
|
||||||
|
MediumBlue = RGB{60, 120, 200}
|
||||||
|
RoyalBlue = RGB{65, 105, 225}
|
||||||
|
CeruleanBlue = RGB{80, 140, 220}
|
||||||
|
Cornflower = RGB{80, 130, 255}
|
||||||
|
DodgerBlue = RGB{40, 180, 255}
|
||||||
|
LightBlue = RGB{120, 170, 255}
|
||||||
|
LightSkyBlue = RGB{135, 206, 250}
|
||||||
|
BabyBlue = RGB{160, 210, 255}
|
||||||
|
Blue = RGB{0, 0, 255}
|
||||||
|
|
||||||
|
// --- Purple / Violet ---
|
||||||
|
DeepPurple = RGB{60, 20, 80}
|
||||||
|
DarkViolet = RGB{120, 40, 180}
|
||||||
|
MutedPurple = RGB{160, 100, 160}
|
||||||
|
MediumPurple = RGB{170, 100, 210}
|
||||||
|
ElectricViolet = RGB{180, 130, 255}
|
||||||
|
LightOrchid = RGB{200, 130, 210}
|
||||||
|
Orchid = RGB{200, 120, 220}
|
||||||
|
PaleVioletRed = RGB{219, 112, 147}
|
||||||
|
SoftLavender = RGB{220, 150, 230}
|
||||||
|
PaleLavender = RGB{220, 180, 255}
|
||||||
|
|
||||||
|
// --- Pink / Rose ---
|
||||||
|
RoseRed = RGB{255, 60, 120}
|
||||||
|
HotMagenta = RGB{255, 60, 200}
|
||||||
|
HotPink = RGB{255, 140, 200}
|
||||||
|
PalePink = RGB{255, 145, 220}
|
||||||
|
LightPink = RGB{255, 182, 193}
|
||||||
|
Pink = RGB{255, 192, 203}
|
||||||
|
Magenta = RGB{255, 0, 255}
|
||||||
|
)
|
||||||
|
`
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package color
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
stdcolor "image/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RGB represents a 24-bit color
|
||||||
|
type RGB struct {
|
||||||
|
R uint8 `toml:"r"`
|
||||||
|
G uint8 `toml:"g"`
|
||||||
|
B uint8 `toml:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ stdcolor.Color = RGB{}
|
||||||
|
|
||||||
|
// RGBA implements image/color.Color. Alpha is always opaque, so the
|
||||||
|
// premultiplied result equals the non-premultiplied one.
|
||||||
|
func (c RGB) RGBA() (r, g, b, a uint32) {
|
||||||
|
return uint32(c.R) * 0x101, uint32(c.G) * 0x101, uint32(c.B) * 0x101, 0xffff
|
||||||
|
}
|
||||||
|
|
||||||
|
// From converts any image/color.Color to RGB, un-premultiplying by alpha and
|
||||||
|
// discarding it. Fully transparent input yields Black.
|
||||||
|
func From(c stdcolor.Color) RGB {
|
||||||
|
r, g, b, a := c.RGBA()
|
||||||
|
switch a {
|
||||||
|
case 0:
|
||||||
|
return RGB{}
|
||||||
|
case 0xffff:
|
||||||
|
return RGB{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8)}
|
||||||
|
}
|
||||||
|
// r,g,b <= a <= 0xffff, so r*0xffff <= 0xfffe0001 and stays in uint32
|
||||||
|
return RGB{
|
||||||
|
R: uint8(r * 0xffff / a >> 8),
|
||||||
|
G: uint8(g * 0xffff / a >> 8),
|
||||||
|
B: uint8(b * 0xffff / a >> 8),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lerp linearly interpolates from c to other by t, clamped to [0,1]
|
||||||
|
func (c RGB) Lerp(other RGB, t float64) RGB {
|
||||||
|
if t <= 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if t >= 1 {
|
||||||
|
return other
|
||||||
|
}
|
||||||
|
return RGB{
|
||||||
|
R: clampU8(float64(c.R) + (float64(other.R)-float64(c.R))*t),
|
||||||
|
G: clampU8(float64(c.G) + (float64(other.G)-float64(c.G))*t),
|
||||||
|
B: clampU8(float64(c.B) + (float64(other.B)-float64(c.B))*t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Luma returns Rec. 601 luminance: R*0.299 + G*0.587 + B*0.114
|
||||||
|
func Luma(c RGB) uint8 {
|
||||||
|
return uint8((int(c.R)*299 + int(c.G)*587 + int(c.B)*114) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedmeanDistance returns squared perceptually-weighted distance between a and b.
|
||||||
|
// Monotonic in perceived difference; use for nearest-color search, not as an
|
||||||
|
// absolute metric.
|
||||||
|
// Formula: https://en.wikipedia.org/wiki/Color_difference#sRGB
|
||||||
|
func RedmeanDistance(a, b RGB) int {
|
||||||
|
rmean := (int(a.R) + int(b.R)) / 2
|
||||||
|
dr := int(a.R) - int(b.R)
|
||||||
|
dg := int(a.G) - int(b.G)
|
||||||
|
db := int(a.B) - int(b.B)
|
||||||
|
return (((512 + rmean) * dr * dr) >> 8) + 4*dg*dg + (((767 - rmean) * db * db) >> 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hex returns the color as "#rrggbb"
|
||||||
|
func (c RGB) Hex() string {
|
||||||
|
const d = "0123456789abcdef"
|
||||||
|
b := [7]byte{
|
||||||
|
'#',
|
||||||
|
d[c.R>>4], d[c.R&0xf],
|
||||||
|
d[c.G>>4], d[c.G&0xf],
|
||||||
|
d[c.B>>4], d[c.B&0xf],
|
||||||
|
}
|
||||||
|
return string(b[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var errHex = errors.New("color: invalid hex string")
|
||||||
|
|
||||||
|
// ParseHex accepts "#rgb", "#rrggbb", and the same forms without the leading '#'
|
||||||
|
func ParseHex(s string) (RGB, error) {
|
||||||
|
if len(s) > 0 && s[0] == '#' {
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
switch len(s) {
|
||||||
|
case 3:
|
||||||
|
r, ok0 := nibble(s[0])
|
||||||
|
g, ok1 := nibble(s[1])
|
||||||
|
b, ok2 := nibble(s[2])
|
||||||
|
if !ok0 || !ok1 || !ok2 {
|
||||||
|
return RGB{}, errHex
|
||||||
|
}
|
||||||
|
return RGB{R: r * 0x11, G: g * 0x11, B: b * 0x11}, nil
|
||||||
|
case 6:
|
||||||
|
var v [3]uint8
|
||||||
|
for i := range v {
|
||||||
|
hi, ok0 := nibble(s[i*2])
|
||||||
|
lo, ok1 := nibble(s[i*2+1])
|
||||||
|
if !ok0 || !ok1 {
|
||||||
|
return RGB{}, errHex
|
||||||
|
}
|
||||||
|
v[i] = hi<<4 | lo
|
||||||
|
}
|
||||||
|
return RGB{R: v[0], G: v[1], B: v[2]}, nil
|
||||||
|
}
|
||||||
|
return RGB{}, errHex
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParseHex panics on invalid input; for package-level initializers
|
||||||
|
func MustParseHex(s string) RGB {
|
||||||
|
c, err := ParseHex(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func nibble(b byte) (uint8, bool) {
|
||||||
|
switch {
|
||||||
|
case b >= '0' && b <= '9':
|
||||||
|
return b - '0', true
|
||||||
|
case b >= 'a' && b <= 'f':
|
||||||
|
return b - 'a' + 10, true
|
||||||
|
case b >= 'A' && b <= 'F':
|
||||||
|
return b - 'A' + 10, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
`
|
||||||
Reference in New Issue
Block a user