{-# LANGUAGE ConstrainedClassMethods #-} ----------------------------------------------------------------------------- -- | -- Module : Data.Prizm.Color -- Copyright : (C) 2013 Parnell Springmeyer -- License : BSD3 -- Maintainer : Parnell Springmeyer -- Stability : stable -- -- @Data.Prizm.Color@ exports all of the specific color space modules -- and types for blending colors, convenience blending ('tint' and -- 'shade'), and adjusting the 'hue', 'lightness', or 'chroma' of a -- given color. You should import this module. -- -- Note that blending colors in a polar-coordinate color space, such -- as 'CIELCH', preserves the saturation and brightness of the colors -- being interpolated better than blending in RGB. It also turns out -- to be an effective even when converting from 'RGB' to 'CIELCH', -- blending, then converting back again! -- -- Here is an excellent blog post by someone discussing the differences: -- . ---------------------------------------------------------------------------- module Data.Prizm.Color ( -- * Color Transformations BlendableColor(..) , AdjustableColor(..) -- * Preset Colors , PresetColor(..) -- * Individual Color Spaces -- ** CIE Color Space , module Data.Prizm.Color.CIE -- ** RGB Color Space , module Data.Prizm.Color.RGB -- * Package Types (re-exports the individual color space type modules too) , module Data.Prizm.Types ) where import Data.MonoTraversable import Data.Prizm.Color.CIE () import qualified Data.Prizm.Color.CIE as CIE import Data.Prizm.Color.RGB () import Data.Prizm.Types -- | Preset white and black for a color space. class PresetColor c where white :: c black :: c -- | A blendable color. class BlendableColor c where -- | Linear interpolation of a color with another color, applying a -- weight. interpolate :: Percent -> (c,c) -> c -- | Blend two 'BlendableColor' colors using an interpolation weight -- of 50%. (<~>) :: c -> c -> c (<~>) l r = interpolate 50 (l,r) -- | Shade a color by blending it using a weight and the -- @PresetColor@ black. shade :: PresetColor c => c -> Percent -> c shade c w = interpolate (pctClamp w) (c, black) -- | Tint a color by blending it using a weight and the -- @PresetColor@ white. tint :: PresetColor c => c -> Percent -> c tint c w = interpolate (pctClamp w) (c, white) -- | An adjustable color. class AdjustableColor c where -- | Adjust the lightness of a color lightness :: c -> Percent -> c -- | Adjust the chroma of a color -- -- NB: not all color spaces will support this easily but it should -- be possible to convert into a color space that does then convert -- back chroma :: c -> Percent -> c -- | Adjust the hue of a color hue :: c -> Percent -> c instance PresetColor CIE.LCH where white = CIE.LCH 0.0 0.0 360.0 black = CIE.LCH 100.0 0.0 360.0 instance PresetColor RGB where white = RGB 255 255 255 black = RGB 0 0 0 instance BlendableColor CIE.LCH where -- | Interpolate two colors in the @CIE L*C*h@ color space with a -- weight. -- -- Weight is applied left to right, so if a weight of 25% is supplied, -- then the color on the left will be multiplied by 25% and the second -- color will be multiplied by 75%. interpolate w ((CIE.LCH al ac ah), (CIE.LCH bl bc bh)) = let w' = pct w (CIE.LCH nl nc nh) = omap (*w') (CIE.LCH (bl - al) (bc - ac) (shortestPath (bh - ah))) in CIE.LCH (al + nl) (ac + nc) (ah + nh) instance AdjustableColor CIE.LCH where -- | Adjust the lightness / darkness of a color. lightness (CIE.LCH l c h) w = CIE.LCH (clamp (l + (100*(pct (pctClamp w)))) 100.0) c h -- | Adjust the hue of a color. hue (CIE.LCH l c h) w = CIE.LCH l c (clamp (h + (360*(pct (pctClamp w)))) 360.0) -- | Adjust the saturation/chroma of a color. -- -- A maximum chroma value of 120 is assumed here, anything more is -- generally considered out of gamut. chroma (CIE.LCH l c h) w = CIE.LCH l (clamp (c + (120*(pct (pctClamp w)))) 120.0) h ------------------------------------------------------------------------------ -- Utilities ------------------------------------------------------------------------------ -- | Give the shortest path to the hue value. shortestPath :: Double -> Double shortestPath h | h > 180 = h - 360 | h < (-180) = h + 360 | otherwise = h -- | Give the decimal value for the given "percent" value. -- -- The @Percent@ value may range from -100 to 100. pct :: Percent -> Double pct i = fromIntegral m / 100 where m = pctClamp i -- | Clamp a 'Percent' value in the range -100 to 100. pctClamp :: Percent -> Percent pctClamp i = max (min i 100) (-100) -- | Clamp a 'Double' with a bottom of at least 0.0. clamp :: Double -> Double -> Double clamp i clmp = max (min i clmp) 0.0