137 lines
3.3 KiB
Go
137 lines
3.3 KiB
Go
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 {
|
|
// +500 rounds half-up, uniform rounding across the package
|
|
return uint8((int(c.R)*299 + int(c.G)*587 + int(c.B)*114 + 500) / 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
|
|
}
|