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  }