module HSLuv where
import Constants (epsilon, kappa, m, minv, refU, refV, refY)
import Control.Applicative (liftA2)
import Data.Colour.CIE (Colour, cieXYZ, cieXYZView)
import Data.Fixed (mod')
import qualified Geometry as G
import Numeric (readHex, showHex)
import Util (fromLinear, toLinear, tripleDotProduct)
newtype RGBRed =
RGBRed Double
deriving (Eq, Show)
newtype RGBGreen =
RGBGreen Double
deriving (Eq, Show)
newtype RGBBlue =
RGBBlue Double
deriving (Eq, Show)
data RGB =
RGB RGBRed
RGBGreen
RGBBlue
deriving (Eq, Show)
newtype XYZX =
XYZX Double
deriving (Eq, Show)
newtype XYZY =
XYZY Double
deriving (Eq, Show)
newtype XYZZ =
XYZZ Double
deriving (Eq, Show)
data XYZ =
XYZ XYZX
XYZY
XYZZ
deriving (Eq, Show)
newtype LUVLightness =
LUVLightness Double
deriving (Eq, Show)
newtype LUVU =
LUVU Double
deriving (Eq, Show)
newtype LUVV =
LUVV Double
deriving (Eq, Show)
data LUV =
LUV LUVLightness
LUVU
LUVV
deriving (Eq, Show)
newtype LCHLightness =
LCHLightness Double
deriving (Eq, Show)
newtype LCHChroma =
LCHChroma Double
deriving (Eq, Show)
newtype LCHHue =
LCHHue Double
deriving (Eq, Show)
data LCH =
LCH LCHLightness
LCHChroma
LCHHue
deriving (Eq, Show)
newtype HSLuvHue =
HSLuvHue Double
deriving (Eq, Show)
newtype HSLuvSaturation =
HSLuvSaturation Double
deriving (Eq, Show)
newtype HSLuvLightness =
HSLuvLightness Double
deriving (Eq, Show)
data HSLuv =
HSLuv HSLuvHue
HSLuvSaturation
HSLuvLightness
deriving (Eq, Show)
newtype HPLuvHue =
HPLuvHue Double
deriving (Eq, Show)
newtype HPLuvPastel =
HPLuvPastel Double
deriving (Eq, Show)
newtype HPLuvLightness =
HPLuvLightness Double
deriving (Eq, Show)
data HPLuv =
HPLuv HPLuvHue
HPLuvPastel
HPLuvLightness
deriving (Eq, Show)
getBounds :: HSLuvLightness -> [G.Line]
getBounds (HSLuvLightness l) =
let sub1 = ((l + 16) ** 3) / 1560896
sub2 =
if sub1 > epsilon
then sub1
else l / kappa
in liftA2 (bounds sub2) m [0, 1]
where
bounds sub2 (m1, m2, m3) t =
let top1 = (284517 * m1 - 94839 * m3) * sub2
top2 =
(838422 * m3 + 769860 * m2 + 731718 * m1) * l * sub2 -
769860 * t * l
bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t
in G.Line {G.slope = top1 / bottom, G.intercept = top2 / bottom}
maxSafeChromaForL :: HSLuvLightness -> LCHChroma
maxSafeChromaForL l' =
LCHChroma $ minimum $ map G.distanceLineFromOrigin $ getBounds l'
maxChromaForLH :: HSLuvLightness -> HSLuvHue -> LCHChroma
maxChromaForLH l' (HSLuvHue h) =
let hrad = G.Radians $ h / 360 * pi * 2
in LCHChroma .
minimum .
(:) (1 / 0) . filter (>= 0) . map (G.lengthOfRayUntilIntersect hrad) $
getBounds l'
xyzToRgb :: XYZ -> RGB
xyzToRgb (XYZ (XYZX x) (XYZY y) (XYZZ z)) =
let [r, g, b] = map (fromLinear . tripleDotProduct (x, y, z)) m
in RGB (RGBRed r) (RGBGreen g) (RGBBlue b)
rgbToXyz :: RGB -> XYZ
rgbToXyz (RGB (RGBRed r) (RGBGreen g) (RGBBlue b)) =
let [x, y, z] =
map (tripleDotProduct (toLinear r, toLinear g, toLinear b)) minv
in XYZ (XYZX x) (XYZY y) (XYZZ z)
yToL :: XYZY -> LUVLightness
yToL (XYZY y) =
if y <= epsilon
then LUVLightness $ (y / refY) * kappa
else LUVLightness $ 116 * ((y / refY) ** (1 / 3)) - 16
lToY :: LUVLightness -> XYZY
lToY (LUVLightness l) =
if l <= 8
then XYZY $ refY * l / kappa
else XYZY $ refY * (((l + 16) / 116) ** 3)
xyzToLuv :: XYZ -> LUV
xyzToLuv (XYZ (XYZX x) y'@(XYZY y) (XYZZ z)) =
let divider = (x + (15 * y) + (3 * z))
varU = (4 * x) / divider
varV = (9 * y) / divider
l'@(LUVLightness l) = yToL y'
u = 13 * l * (varU - refU)
v = 13 * l * (varV - refV)
in if l == 0
then LUV (LUVLightness 0) (LUVU 0) (LUVV 0)
else LUV l' (LUVU u) (LUVV v)
luvToXyz :: LUV -> XYZ
luvToXyz (LUV (LUVLightness 0) _ _) = XYZ (XYZX 0) (XYZY 0) (XYZZ 0)
luvToXyz (LUV l'@(LUVLightness l) (LUVU u) (LUVV v)) =
let varU = u / (13 * l) + refU
varV = v / (13 * l) + refV
(XYZY y) = lToY l'
x = -(9 * y * varU) / ((varU - 4) * varV - (varU * varV))
z = (9 * y - (15 * varV * y) - (varV * x)) / (3 * varV)
in XYZ (XYZX x) (XYZY y) (XYZZ z)
luvToLch :: LUV -> LCH
luvToLch (LUV (LUVLightness l) (LUVU u) (LUVV v)) =
let c = sqrt $ u * u + v * v
h =
if c < 0.00000001
then 0
else (atan2 v u * 180 / pi) `mod'` 360
in LCH (LCHLightness l) (LCHChroma c) (LCHHue h)
lchToLuv :: LCH -> LUV
lchToLuv (LCH (LCHLightness l) (LCHChroma c) (LCHHue h)) =
let hrad = h / 360 * 2 * pi
in LUV (LUVLightness l) (LUVU $ c * cos hrad) (LUVV $ c * sin hrad)
hsluvToLchWith :: (HSLuvLightness -> HSLuvHue -> LCHChroma) -> HSLuv -> LCH
hsluvToLchWith f (HSLuv h'@(HSLuvHue h) (HSLuvSaturation s) l'@(HSLuvLightness l))
| l > 99.9999999 = LCH (LCHLightness 100) (LCHChroma 0) (LCHHue h)
| l < 0.00000001 = LCH (LCHLightness 0) (LCHChroma 0) (LCHHue h)
| otherwise =
let (LCHChroma c) = f l' h'
in LCH (LCHLightness l) (LCHChroma (c / 100 * s)) (LCHHue h)
lchToHsluvWith :: (HSLuvLightness -> HSLuvHue -> LCHChroma) -> LCH -> HSLuv
lchToHsluvWith f (LCH (LCHLightness l) (LCHChroma c) (LCHHue h))
| l > 99.9999999 = HSLuv (HSLuvHue h) (HSLuvSaturation 0) (HSLuvLightness 100)
| l < 0.00000001 = HSLuv (HSLuvHue h) (HSLuvSaturation 0) (HSLuvLightness 0)
| otherwise =
let (LCHChroma maxC) = f (HSLuvLightness l) (HSLuvHue h)
in HSLuv (HSLuvHue h) (HSLuvSaturation (c / maxC * 100)) (HSLuvLightness l)
hsluvToLch :: HSLuv -> LCH
hsluvToLch = hsluvToLchWith maxChromaForLH
lchToHsluv :: LCH -> HSLuv
lchToHsluv = lchToHsluvWith maxChromaForLH
hpluvToLch :: HPLuv -> LCH
hpluvToLch (HPLuv (HPLuvHue h) (HPLuvPastel p) (HPLuvLightness l)) =
hsluvToLchWith
(\l' _ -> maxSafeChromaForL l')
(HSLuv (HSLuvHue h) (HSLuvSaturation p) (HSLuvLightness l))
lchToHpluv :: LCH -> HPLuv
lchToHpluv lch =
let (HSLuv (HSLuvHue h) (HSLuvSaturation s) (HSLuvLightness l)) =
lchToHsluvWith (\l' _ -> maxSafeChromaForL l') lch
in HPLuv (HPLuvHue h) (HPLuvPastel s) (HPLuvLightness l)
rgbToHex :: RGB -> String
rgbToHex (RGB (RGBRed r) (RGBGreen g) (RGBBlue b)) =
"#" ++ toHex r ++ toHex g ++ toHex b
where
leftPad s n c = replicate (max 0 (n - length s)) c ++ s
toHex c = leftPad (showHex (round $ c * 255 :: Integer) "") 2 '0'
hexToRgb :: String -> Maybe RGB
hexToRgb ['#', a, b, c, d, e, f] =
case (readHex [a, b], readHex [c, d], readHex [e, f]) of
([(rr, rr')], [(rg, rg')], [(rb, rb')]) ->
if any (/= "") [rr', rg', rb']
then Nothing
else Just $
RGB (RGBRed (rr / 255)) (RGBGreen (rg / 255)) (RGBBlue (rb / 255))
_ -> Nothing
hexToRgb _ = Nothing
lchToRgb :: LCH -> RGB
lchToRgb = xyzToRgb . luvToXyz . lchToLuv
rgbToLch :: RGB -> LCH
rgbToLch = luvToLch . xyzToLuv . rgbToXyz
hsluvToRgb :: HSLuv -> RGB
hsluvToRgb = lchToRgb . hsluvToLch
rgbToHsluv :: RGB -> HSLuv
rgbToHsluv = lchToHsluv . rgbToLch
hpluvToRgb :: HPLuv -> RGB
hpluvToRgb = lchToRgb . hpluvToLch
rgbToHpluv :: RGB -> HPLuv
rgbToHpluv = lchToHpluv . rgbToLch
hsluvToHex :: HSLuv -> String
hsluvToHex = rgbToHex . hsluvToRgb
hpluvToHex :: HPLuv -> String
hpluvToHex = rgbToHex . hpluvToRgb
hexToHsluv :: String -> Maybe HSLuv
hexToHsluv = fmap rgbToHsluv . hexToRgb
hexToHpluv :: String -> Maybe HPLuv
hexToHpluv = fmap rgbToHpluv . hexToRgb
hsluvToColour :: HSLuv -> Colour Double
hsluvToColour hsluv =
let XYZ (XYZX x) (XYZY y) (XYZZ z) = luvToXyz . lchToLuv . hsluvToLch $ hsluv
in cieXYZ x y z
colourToHsluv :: Colour Double -> HSLuv
colourToHsluv colour =
let (x, y, z) = cieXYZView colour
in lchToHsluv . luvToLch . xyzToLuv $ XYZ (XYZX x) (XYZY y) (XYZZ z)
hpluvToColour :: HPLuv -> Colour Double
hpluvToColour hsluv =
let XYZ (XYZX x) (XYZY y) (XYZZ z) = luvToXyz . lchToLuv . hpluvToLch $ hsluv
in cieXYZ x y z
colourToHpluv :: Colour Double -> HPLuv
colourToHpluv colour =
let (x, y, z) = cieXYZView colour
in lchToHpluv . luvToLch . xyzToLuv $ XYZ (XYZX x) (XYZY y) (XYZZ z)