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
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
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,135 @@
|
||||
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