// Copyright ©2016 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package moreland import ( "image/color" "math" ) // rgb represents a physically linear RGB color. type rgb struct { R, G, B float64 } // cieXYZ returns a CIE XYZ color representation of the receiver. func (c rgb) cieXYZ() cieXYZ { return cieXYZ{ X: 0.4124*c.R + 0.3576*c.G + 0.1805*c.B, Y: 0.2126*c.R + 0.7152*c.G + 0.0722*c.B, Z: 0.0193*c.R + 0.1192*c.G + 0.9505*c.B, } } // sRGBA returns an sRGB color representation of the receiver using the // provided alpha which must be in [0, 1]. func (c rgb) sRGBA(alpha float64) sRGBA { // f converts from a linear RGB component to an sRGB component. f := func(v float64) float64 { if v > 0.0031308 { return 1.055*math.Pow(v, 1/2.4) - 0.055 } return 12.92 * v } return sRGBA{ R: f(c.R), G: f(c.G), B: f(c.B), A: alpha, } } // cieXYZ represents a color in CIE XYZ space. // Y must be in the range [0,1]. X and Z must be greater than 0. type cieXYZ struct { X, Y, Z float64 } // rgb returns a linear RGB representation of the receiver. func (c cieXYZ) rgb() rgb { return rgb{ R: c.X*3.2406 + c.Y*-1.5372 + c.Z*-0.4986, G: c.X*-0.9689 + c.Y*1.8758 + c.Z*0.0415, B: c.X*0.0557 + c.Y*-0.204 + c.Z*1.057, } } // cieLAB returns a CIELAB color representation of the receiver. func (c cieXYZ) cieLAB() cieLAB { // f is an intermediate step in converting from CIE XYZ to CIE LAB. f := func(v float64) float64 { if v > 0.008856 { return math.Pow(v, 1.0/3.0) } return 7.787*v + 16.0/116.0 } tempX := f(c.X / 0.9505) tempY := f(c.Y) tempZ := f(c.Z / 1.089) return cieLAB{ L: (116.0 * tempY) - 16.0, A: 500.0 * (tempX - tempY), B: 200 * (tempY - tempZ), } } // sRGBA represents a color within the sRGB color space, with an alpha channel // but not premultiplied. All values must be in the range [0,1]. type sRGBA struct { R, G, B, A float64 } // rgb returns a linear RGB representation of the receiver. func (c sRGBA) rgb() rgb { // f converts from an sRGB component to a linear RGB component. f := func(v float64) float64 { if v > 0.04045 { return math.Pow((v+0.055)/1.055, 2.4) } return v / 12.92 } return rgb{ R: f(c.R), G: f(c.G), B: f(c.B), } } // RGBA implements the color.Color interface. func (c sRGBA) RGBA() (r, g, b, a uint32) { return uint32(c.R * c.A * 0xffff), uint32(c.G * c.A * 0xffff), uint32(c.B * c.A * 0xffff), uint32(c.A * 0xffff) } // cieLAB returns a CIE LAB representation of the receiver. func (c sRGBA) cieLAB() cieLAB { return c.rgb().cieXYZ().cieLAB() } // colorTosRGBA converts a color to an sRGBA. func colorTosRGBA(c color.Color) sRGBA { r, g, b, a := c.RGBA() if a == 0 { return sRGBA{} } return sRGBA{ R: float64(r) / float64(a), G: float64(g) / float64(a), B: float64(b) / float64(a), A: float64(a) / 0xffff, } } // clamp forces all channels in c to be within the range [0, 1]. func (c *sRGBA) clamp() { if c.R > 1 { c.R = 1 } if c.G > 1 { c.G = 1 } if c.B > 1 { c.B = 1 } if c.A > 1 { c.A = 1 } if c.R < 0 { c.R = 0 } if c.G < 0 { c.G = 0 } if c.B < 0 { c.B = 0 } if c.A < 0 { c.A = 0 } } // cieLAB represents a color in CIE LAB space. // L must be in the range [0, 100]. type cieLAB struct { L, A, B float64 } // sRGBA return a linear RGB color representation of the receiver using the // provided alpha which must be in [0, 1]. func (c cieLAB) sRGBA(alpha float64) sRGBA { return c.cieXYZ().rgb().sRGBA(alpha) } // cieXYZ returns a CIE XYZ color representation of the receiver. func (c cieLAB) cieXYZ() cieXYZ { // f is an intermediate step in converting from CIE LAB to CIE XYZ. f := func(v float64) float64 { const ( xlim = 0.008856 a = 7.787 b = 16. / 116. ylim = a*xlim + b ) if v > ylim { return v * v * v } return (v - b) / a } // Reference white-point D65 const xn, yn, zn = 0.95047, 1.0, 1.08883 return cieXYZ{ X: xn * f((c.A/500)+(c.L+16)/116), Y: yn * f((c.L+16)/116), Z: zn * f((c.L+16)/116-(c.B/200)), } } // MSH returns an MSH color representation of the receiver. func (c cieLAB) MSH() msh { m := math.Pow(c.L*c.L+c.A*c.A+c.B*c.B, 0.5) return msh{ M: m, S: math.Acos(c.L / m), H: math.Atan2(c.B, c.A), } } // MSH represents a color in Magnitude-Saturation-Hue color space. type msh struct { M, S, H float64 } // colorToMSH converts a color to MSH space. // TODO: If msh ever becomes exported, change this to implment color.Model func colorToMSH(c color.Color) msh { return colorTosRGBA(c).cieLAB().MSH() } // cieLAB returns a CIELAB representation of the receiver. func (c msh) cieLAB() cieLAB { return cieLAB{ L: c.M * math.Cos(c.S), A: c.M * math.Sin(c.S) * math.Cos(c.H), B: c.M * math.Sin(c.S) * math.Sin(c.H), } } // RGBA implements the color.Color interface. func (c msh) RGBA() (r, g, b, a uint32) { return c.cieLAB().sRGBA(1.0).RGBA() } // hueTwist returns the hue twist between color c and converge magnitude // convergeM. func hueTwist(c msh, convergeM float64) float64 { signH := c.H / math.Abs(c.H) return signH * c.S * math.Sqrt(convergeM*convergeM-c.M*c.M) / (c.M * math.Sin(c.S)) }