go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/experiments/huectl/pkg/hue/color.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package hue 9 10 import ( 11 "fmt" 12 "image/color" 13 "math" 14 "strconv" 15 ) 16 17 var ( 18 // ColorTransparent is a fully transparent color. 19 ColorTransparent = Color{} 20 21 // ColorWhite is white. 22 ColorWhite = Color{R: 255, G: 255, B: 255, A: 255} 23 24 // ColorBlack is black. 25 ColorBlack = Color{R: 0, G: 0, B: 0, A: 255} 26 27 // ColorRed is red. 28 ColorRed = Color{R: 255, G: 0, B: 0, A: 255} 29 30 // ColorGreen is green. 31 ColorGreen = Color{R: 0, G: 255, B: 0, A: 255} 32 33 // ColorBlue is blue. 34 ColorBlue = Color{R: 0, G: 0, B: 255, A: 255} 35 ) 36 37 // ColorFromHex returns a color from a css hex code. 38 // 39 // An example might be #efefef which would yield a 40 // color with R=239, G=239, B=239, A=255. 41 func ColorFromHex(hex string) Color { 42 var c Color 43 if len(hex) == 3 { 44 c.R = parseHex(string(hex[0])) * 0x11 45 c.G = parseHex(string(hex[1])) * 0x11 46 c.B = parseHex(string(hex[2])) * 0x11 47 } else { 48 c.R = parseHex(string(hex[0:2])) 49 c.G = parseHex(string(hex[2:4])) 50 c.B = parseHex(string(hex[4:6])) 51 } 52 c.A = 255 53 return c 54 } 55 56 var ( 57 _ color.Color = (*Color)(nil) 58 ) 59 60 // Color is our internal color type because color.Color is bad. 61 // 62 // Each "channel" (i.e. R, G, B, or A) are 0-255 values. 63 type Color struct { 64 R, G, B, A uint8 65 } 66 67 // RGBA returns the color as a pre-alpha mixed color set. 68 // 69 // It also returns the alpha channel, though effectively 70 // the RGB values are modified by it already. 71 func (c Color) RGBA() (r, g, b, a uint32) { 72 fa := float64(c.A) / 255.0 73 r = uint32(float64(uint32(c.R)) * fa) 74 r |= r << 8 75 g = uint32(float64(uint32(c.G)) * fa) 76 g |= g << 8 77 b = uint32(float64(uint32(c.B)) * fa) 78 b |= b << 8 79 a = uint32(c.A) 80 a |= a << 8 81 return 82 } 83 84 // IsZero returns if the color has been set or not. 85 func (c Color) IsZero() bool { 86 return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0 87 } 88 89 // IsTransparent returns if the colors alpha channel is zero. 90 func (c Color) IsTransparent() bool { 91 return c.A == 0 92 } 93 94 // WithAlpha returns a copy of the color with a given alpha. 95 func (c Color) WithAlpha(a uint8) Color { 96 return Color{ 97 R: c.R, 98 G: c.G, 99 B: c.B, 100 A: a, 101 } 102 } 103 104 // Equals returns true if the color equals another. 105 func (c Color) Equals(other Color) bool { 106 return c.R == other.R && 107 c.G == other.G && 108 c.B == other.B && 109 c.A == other.A 110 } 111 112 // AverageWith averages two colors. 113 func (c Color) AverageWith(other Color) Color { 114 return Color{ 115 R: (c.R + other.R) >> 1, 116 G: (c.G + other.G) >> 1, 117 B: (c.B + other.B) >> 1, 118 A: c.A, 119 } 120 } 121 122 // String returns a css string representation of the color. 123 func (c Color) String() string { 124 fa := float64(c.A) / float64(255) 125 return fmt.Sprintf("rgba(%v,%v,%v,%.1f)", c.R, c.G, c.B, fa) 126 } 127 128 func parseHex(hex string) uint8 { 129 v, _ := strconv.ParseInt(hex, 16, 16) 130 return uint8(v) 131 } 132 133 // XYPoint is a struct of two float64 points. 134 type XYPoint struct{ X, Y float64 } 135 136 // GamutA is a gamut profile. 137 var GamutA = []XYPoint{ 138 {0.704, 0.296}, 139 {0.2151, 0.7106}, 140 {0.138, 0.08}, 141 } 142 143 // GamutB is a gamut profile. 144 var GamutB = []XYPoint{ 145 {0.675, 0.322}, 146 {0.4091, 0.518}, 147 {0.167, 0.04}, 148 } 149 150 // GamutC is a gamut profile. 151 var GamutC = []XYPoint{ 152 {0.692, 0.308}, 153 {0.17, 0.7}, 154 {0.153, 0.048}, 155 } 156 157 // GetGamutForModelID returns the gamut for a given model id. 158 func GetGamutForModelID(modelID string) []XYPoint { 159 switch modelID { 160 case "LST001", "LLC005", "LLC006", "LLC007", "LLC010", "LLC011", "LLC012", "LLC013", "LLC014": 161 return GamutA 162 case "LCT001", "LCT007", "LCT002", "LCT003", "LLM001": 163 return GamutB 164 case "LCT010", "LCT011", "LCT012", "LCT014", "LCT015", "LCT016", "LLC020", "LST002": 165 return GamutC 166 default: 167 return GamutC 168 } 169 } 170 171 // XY converts a given RGB color to the xy color in a given gamut. 172 func (c Color) XY(gamut []XYPoint) []float32 { 173 red := float64(c.R) / 255.0 174 green := float64(c.G) / 255.0 175 blue := float64(c.B) / 255.0 176 177 var r, g, b float64 178 if red > 0.04045 { 179 r = math.Pow((red+0.055)/(1.0+0.055), 2.4) 180 } else { 181 r = (red / 12.92) 182 } 183 if green > 0.04045 { 184 g = math.Pow((green+0.055)/(1.0+0.055), 2.4) 185 } else { 186 g = green / 12.92 187 } 188 if blue > 0.04045 { 189 b = math.Pow((blue+0.055)/(1.0+0.055), 2.4) 190 } else { 191 b = blue / 12.92 192 } 193 194 X := r*0.664511 + g*0.154324 + b*0.162028 195 Y := r*0.283881 + g*0.668433 + b*0.047685 196 Z := r*0.000088 + g*0.072310 + b*0.986039 197 198 cx := X / (X + Y + Z) 199 cy := Y / (X + Y + Z) 200 201 // check if the given XY value is within the colourreach of our lamps. 202 xyPoint := XYPoint{cx, cy} 203 if !GamutContains(gamut, xyPoint) { 204 xyPoint = GamutPoint(gamut, xyPoint) 205 } 206 return []float32{float32(xyPoint.X), float32(xyPoint.Y)} 207 } 208 209 // CrossProduct returns the cross product of two given points. 210 func CrossProduct(p1, p2 XYPoint) float64 { 211 return (p1.X*p2.Y - p1.Y*p2.X) 212 } 213 214 // GamutContains returns if a given point exists in a given gamut. 215 func GamutContains(gamut []XYPoint, p XYPoint) bool { 216 v1 := XYPoint{gamut[1].X - gamut[0].X, gamut[1].Y - gamut[0].Y} 217 v2 := XYPoint{gamut[2].X - gamut[0].X, gamut[2].Y - gamut[0].Y} 218 q := XYPoint{p.X - gamut[0].X, p.Y - gamut[0].Y} 219 s := CrossProduct(q, v2) / CrossProduct(v1, v2) 220 t := CrossProduct(v1, q) / CrossProduct(v1, v2) 221 return (s >= 0.0) && (t >= 0.0) && (s+t <= 1.0) 222 } 223 224 // GetClosestPointToLine returns the closest point to a given line. 225 func GetClosestPointToLine(a, b, p XYPoint) XYPoint { 226 ap := XYPoint{p.X - a.X, p.Y - a.Y} 227 ab := XYPoint{p.X - a.X, b.Y - a.Y} 228 ab2 := ab.X*ab.X + ab.Y*ab.Y 229 apab := ap.X*ab.X + ap.Y*ab.Y 230 t := apab / ab2 231 if t < 0.0 { 232 t = 0.0 233 } else if t > 1.0 { 234 t = 1.0 235 } 236 return XYPoint{a.X + ab.X*t, a.Y + ab.Y*t} 237 } 238 239 // GetDistance gets the distance between two points. 240 func GetDistance(one, two XYPoint) float64 { 241 dx := one.X - two.X 242 dy := one.Y - two.Y 243 return math.Sqrt(dx*dx + dy*dy) 244 } 245 246 // GamutPoint returns the closest point in a gamut to a given point. 247 func GamutPoint(gamut []XYPoint, p XYPoint) XYPoint { 248 pAB := GetClosestPointToLine(gamut[0], gamut[1], p) 249 pAC := GetClosestPointToLine(gamut[2], gamut[0], p) 250 pBC := GetClosestPointToLine(gamut[1], gamut[2], p) 251 252 // get the distances per point and see which point is closer to our Point 253 dAB := GetDistance(p, pAB) 254 dAC := GetDistance(p, pAC) 255 dBC := GetDistance(p, pBC) 256 257 lowest := dAB 258 closestPoint := pAB 259 if dAC < lowest { 260 lowest = dAC 261 closestPoint = pAC 262 } 263 if dBC < lowest { 264 closestPoint = pBC 265 } 266 return closestPoint 267 }