Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

color.gno

3.03 Kb · 121 lines
  1package art
  2
  3import "strings"
  4
  5// Default is the colour that means "let the consumer decide": the terminal's
  6// own foreground or background in ANSI, and transparent in SVG.
  7const Default = -1
  8
  9// RGB packs three 0-255 components into the 0xRRGGBB form this package uses.
 10// Components outside 0-255 are clamped rather than wrapped, so a computed
 11// channel cannot silently alias onto a neighbouring one.
 12func RGB(r, g, b int) int {
 13	return clamp8(r)<<16 | clamp8(g)<<8 | clamp8(b)
 14}
 15
 16// Split returns the three 0-255 components of a packed colour. Default splits
 17// to zeroes, so callers must test for Default before calling it rather than
 18// after.
 19func Split(c int) (r, g, b int) {
 20	if c < 0 {
 21		return 0, 0, 0
 22	}
 23	return (c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff
 24}
 25
 26// ParseHex reads "#rrggbb", "#rgb", or either without the leading '#', and
 27// reports whether it understood the string. An empty string is not an error but
 28// it is not a colour either: it reports Default, false, which is how a palette
 29// marks a transparent entry.
 30func ParseHex(s string) (int, bool) {
 31	s = strings.TrimSpace(s)
 32	s = strings.TrimPrefix(s, "#")
 33	switch len(s) {
 34	case 3:
 35		r, ok1 := hexDigit(s[0])
 36		g, ok2 := hexDigit(s[1])
 37		b, ok3 := hexDigit(s[2])
 38		if !ok1 || !ok2 || !ok3 {
 39			return Default, false
 40		}
 41		// #abc is #aabbcc, not #0a0b0c.
 42		return RGB(r*17, g*17, b*17), true
 43	case 6:
 44		r, ok1 := hexByte(s[0], s[1])
 45		g, ok2 := hexByte(s[2], s[3])
 46		b, ok3 := hexByte(s[4], s[5])
 47		if !ok1 || !ok2 || !ok3 {
 48			return Default, false
 49		}
 50		return RGB(r, g, b), true
 51	}
 52	return Default, false
 53}
 54
 55// Hex is the inverse of ParseHex. Default renders as the empty string, because
 56// there is no hex spelling of "the consumer's default".
 57func Hex(c int) string {
 58	if c < 0 {
 59		return ""
 60	}
 61	r, g, b := Split(c)
 62	var sb strings.Builder
 63	sb.WriteByte('#')
 64	writeHexByte(&sb, r)
 65	writeHexByte(&sb, g)
 66	writeHexByte(&sb, b)
 67	return sb.String()
 68}
 69
 70// Luminance returns perceived brightness on 0-255, using the Rec. 709 weights
 71// (0.2126 R, 0.7152 G, 0.0722 B) in integer arithmetic. Default is treated as
 72// black, so an unset colour ramps to the darkest glyph rather than to a
 73// midpoint that would read as real content.
 74//
 75// This is the function [Ramp] keys on, and the package doc explains why keying
 76// on it is the wrong default for a sprite.
 77func Luminance(c int) int {
 78	if c < 0 {
 79		return 0
 80	}
 81	r, g, b := Split(c)
 82	return (2126*r + 7152*g + 722*b) / 10000
 83}
 84
 85func clamp8(v int) int {
 86	if v < 0 {
 87		return 0
 88	}
 89	if v > 255 {
 90		return 255
 91	}
 92	return v
 93}
 94
 95func hexDigit(c byte) (int, bool) {
 96	switch {
 97	case c >= '0' && c <= '9':
 98		return int(c - '0'), true
 99	case c >= 'a' && c <= 'f':
100		return int(c-'a') + 10, true
101	case c >= 'A' && c <= 'F':
102		return int(c-'A') + 10, true
103	}
104	return 0, false
105}
106
107func hexByte(hi, lo byte) (int, bool) {
108	h, ok1 := hexDigit(hi)
109	l, ok2 := hexDigit(lo)
110	if !ok1 || !ok2 {
111		return 0, false
112	}
113	return h*16 + l, true
114}
115
116const hexDigits = "0123456789abcdef"
117
118func writeHexByte(sb *strings.Builder, v int) {
119	sb.WriteByte(hexDigits[(v>>4)&0xf])
120	sb.WriteByte(hexDigits[v&0xf])
121}