gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

hsluv.go (6167B)


      1 package colorful
      2 
      3 import "math"
      4 
      5 // Source: https://github.com/hsluv/hsluv-go
      6 // Under MIT License
      7 // Modified so that Saturation and Luminance are in [0..1] instead of [0..100].
      8 
      9 // HSLuv uses a rounded version of the D65. This has no impact on the final RGB
     10 // values, but to keep high levels of accuracy for internal operations and when
     11 // comparing to the test values, this modified white reference is used internally.
     12 //
     13 // See this GitHub thread for details on these values:
     14 //     https://github.com/hsluv/hsluv/issues/79
     15 var hSLuvD65 = [3]float64{0.95045592705167, 1.0, 1.089057750759878}
     16 
     17 func LuvLChToHSLuv(l, c, h float64) (float64, float64, float64) {
     18 	// [-1..1] but the code expects it to be [-100..100]
     19 	c *= 100.0
     20 	l *= 100.0
     21 
     22 	var s, max float64
     23 	if l > 99.9999999 || l < 0.00000001 {
     24 		s = 0.0
     25 	} else {
     26 		max = maxChromaForLH(l, h)
     27 		s = c / max * 100.0
     28 	}
     29 	return h, clamp01(s / 100.0), clamp01(l / 100.0)
     30 }
     31 
     32 func HSLuvToLuvLCh(h, s, l float64) (float64, float64, float64) {
     33 	l *= 100.0
     34 	s *= 100.0
     35 
     36 	var c, max float64
     37 	if l > 99.9999999 || l < 0.00000001 {
     38 		c = 0.0
     39 	} else {
     40 		max = maxChromaForLH(l, h)
     41 		c = max / 100.0 * s
     42 	}
     43 
     44 	// c is [-100..100], but for LCh it's supposed to be almost [-1..1]
     45 	return clamp01(l / 100.0), c / 100.0, h
     46 }
     47 
     48 func LuvLChToHPLuv(l, c, h float64) (float64, float64, float64) {
     49 	// [-1..1] but the code expects it to be [-100..100]
     50 	c *= 100.0
     51 	l *= 100.0
     52 
     53 	var s, max float64
     54 	if l > 99.9999999 || l < 0.00000001 {
     55 		s = 0.0
     56 	} else {
     57 		max = maxSafeChromaForL(l)
     58 		s = c / max * 100.0
     59 	}
     60 	return h, s / 100.0, l / 100.0
     61 }
     62 
     63 func HPLuvToLuvLCh(h, s, l float64) (float64, float64, float64) {
     64 	// [-1..1] but the code expects it to be [-100..100]
     65 	l *= 100.0
     66 	s *= 100.0
     67 
     68 	var c, max float64
     69 	if l > 99.9999999 || l < 0.00000001 {
     70 		c = 0.0
     71 	} else {
     72 		max = maxSafeChromaForL(l)
     73 		c = max / 100.0 * s
     74 	}
     75 	return l / 100.0, c / 100.0, h
     76 }
     77 
     78 // HSLuv creates a new Color from values in the HSLuv color space.
     79 // Hue in [0..360], a Saturation [0..1], and a Luminance (lightness) in [0..1].
     80 //
     81 // The returned color values are clamped (using .Clamped), so this will never output
     82 // an invalid color.
     83 func HSLuv(h, s, l float64) Color {
     84 	// HSLuv -> LuvLCh -> CIELUV -> CIEXYZ -> Linear RGB -> sRGB
     85 	l, u, v := LuvLChToLuv(HSLuvToLuvLCh(h, s, l))
     86 	return LinearRgb(XyzToLinearRgb(LuvToXyzWhiteRef(l, u, v, hSLuvD65))).Clamped()
     87 }
     88 
     89 // HPLuv creates a new Color from values in the HPLuv color space.
     90 // Hue in [0..360], a Saturation [0..1], and a Luminance (lightness) in [0..1].
     91 //
     92 // The returned color values are clamped (using .Clamped), so this will never output
     93 // an invalid color.
     94 func HPLuv(h, s, l float64) Color {
     95 	// HPLuv -> LuvLCh -> CIELUV -> CIEXYZ -> Linear RGB -> sRGB
     96 	l, u, v := LuvLChToLuv(HPLuvToLuvLCh(h, s, l))
     97 	return LinearRgb(XyzToLinearRgb(LuvToXyzWhiteRef(l, u, v, hSLuvD65))).Clamped()
     98 }
     99 
    100 // HSLuv returns the Hue, Saturation and Luminance of the color in the HSLuv
    101 // color space. Hue in [0..360], a Saturation [0..1], and a Luminance
    102 // (lightness) in [0..1].
    103 func (col Color) HSLuv() (h, s, l float64) {
    104 	// sRGB -> Linear RGB -> CIEXYZ -> CIELUV -> LuvLCh -> HSLuv
    105 	return LuvLChToHSLuv(col.LuvLChWhiteRef(hSLuvD65))
    106 }
    107 
    108 // HPLuv returns the Hue, Saturation and Luminance of the color in the HSLuv
    109 // color space. Hue in [0..360], a Saturation [0..1], and a Luminance
    110 // (lightness) in [0..1].
    111 //
    112 // Note that HPLuv can only represent pastel colors, and so the Saturation
    113 // value could be much larger than 1 for colors it can't represent.
    114 func (col Color) HPLuv() (h, s, l float64) {
    115 	return LuvLChToHPLuv(col.LuvLChWhiteRef(hSLuvD65))
    116 }
    117 
    118 // DistanceHSLuv calculates Euclidan distance in the HSLuv colorspace. No idea
    119 // how useful this is.
    120 //
    121 // The Hue value is divided by 100 before the calculation, so that H, S, and L
    122 // have the same relative ranges.
    123 func (c1 Color) DistanceHSLuv(c2 Color) float64 {
    124 	h1, s1, l1 := c1.HSLuv()
    125 	h2, s2, l2 := c2.HSLuv()
    126 	return math.Sqrt(sq((h1-h2)/100.0) + sq(s1-s2) + sq(l1-l2))
    127 }
    128 
    129 // DistanceHPLuv calculates Euclidean distance in the HPLuv colorspace. No idea
    130 // how useful this is.
    131 //
    132 // The Hue value is divided by 100 before the calculation, so that H, S, and L
    133 // have the same relative ranges.
    134 func (c1 Color) DistanceHPLuv(c2 Color) float64 {
    135 	h1, s1, l1 := c1.HPLuv()
    136 	h2, s2, l2 := c2.HPLuv()
    137 	return math.Sqrt(sq((h1-h2)/100.0) + sq(s1-s2) + sq(l1-l2))
    138 }
    139 
    140 var m = [3][3]float64{
    141 	{3.2409699419045214, -1.5373831775700935, -0.49861076029300328},
    142 	{-0.96924363628087983, 1.8759675015077207, 0.041555057407175613},
    143 	{0.055630079696993609, -0.20397695888897657, 1.0569715142428786},
    144 }
    145 
    146 const kappa = 903.2962962962963
    147 const epsilon = 0.0088564516790356308
    148 
    149 func maxChromaForLH(l, h float64) float64 {
    150 	hRad := h / 360.0 * math.Pi * 2.0
    151 	minLength := math.MaxFloat64
    152 	for _, line := range getBounds(l) {
    153 		length := lengthOfRayUntilIntersect(hRad, line[0], line[1])
    154 		if length > 0.0 && length < minLength {
    155 			minLength = length
    156 		}
    157 	}
    158 	return minLength
    159 }
    160 
    161 func getBounds(l float64) [6][2]float64 {
    162 	var sub2 float64
    163 	var ret [6][2]float64
    164 	sub1 := math.Pow(l+16.0, 3.0) / 1560896.0
    165 	if sub1 > epsilon {
    166 		sub2 = sub1
    167 	} else {
    168 		sub2 = l / kappa
    169 	}
    170 	for i := range m {
    171 		for k := 0; k < 2; k++ {
    172 			top1 := (284517.0*m[i][0] - 94839.0*m[i][2]) * sub2
    173 			top2 := (838422.0*m[i][2]+769860.0*m[i][1]+731718.0*m[i][0])*l*sub2 - 769860.0*float64(k)*l
    174 			bottom := (632260.0*m[i][2]-126452.0*m[i][1])*sub2 + 126452.0*float64(k)
    175 			ret[i*2+k][0] = top1 / bottom
    176 			ret[i*2+k][1] = top2 / bottom
    177 		}
    178 	}
    179 	return ret
    180 }
    181 
    182 func lengthOfRayUntilIntersect(theta, x, y float64) (length float64) {
    183 	length = y / (math.Sin(theta) - x*math.Cos(theta))
    184 	return
    185 }
    186 
    187 func maxSafeChromaForL(l float64) float64 {
    188 	minLength := math.MaxFloat64
    189 	for _, line := range getBounds(l) {
    190 		m1 := line[0]
    191 		b1 := line[1]
    192 		x := intersectLineLine(m1, b1, -1.0/m1, 0.0)
    193 		dist := distanceFromPole(x, b1+x*m1)
    194 		if dist < minLength {
    195 			minLength = dist
    196 		}
    197 	}
    198 	return minLength
    199 }
    200 
    201 func intersectLineLine(x1, y1, x2, y2 float64) float64 {
    202 	return (y1 - y2) / (x2 - x1)
    203 }
    204 
    205 func distanceFromPole(x, y float64) float64 {
    206 	return math.Sqrt(math.Pow(x, 2.0) + math.Pow(y, 2.0))
    207 }