rsagl-0.6.0.0: RSAGL/Color/ColorSpace.hs
module RSAGL.Color.ColorSpace
(ColorSpace(..),
ImportColorCoordinates(..),
ExportColorCoordinates(..),
AffineColorSpace,
ColorWheel,
ColorChannel,
ChannelIndex,
LinearMetric(..),
newChannel,
newAngularChannel,
newRadialChannel,
newMaximalChannel,
viewChannel,
channel_u,
channel_v,
channel_w,
newColorSpace,
newColorWheel,
color_space_rgb,
color_wheel_rgbl,
transformColorFromTo,
transformColor)
where
import RSAGL.Math.Types
import RSAGL.Math.Matrix
import RSAGL.Math.Vector
import RSAGL.Math.AbstractVector
import RSAGL.Math.Affine
import Data.Vec (nearZero)
import RSAGL.Math.Angle
import Control.Arrow (first,second)
-- | An affine transformation from the default RGB color space
-- to the specified color space.
newtype AffineColorSpace = AffineColorSpace Matrix
deriving (Show)
-- | A rotatable color space.
newtype ColorWheel = ColorWheel Matrix
deriving (Show)
-- | A specific component of a 3-channel color space.
data ColorChannel =
-- A color space in which the 'u_channel' is the channel in question.
LinearChannel Matrix
-- | A channel that depends on the input color.
| Preconfigure (Color -> ColorChannel)
-- | 'channel_u', 'channel_v', 'channel_w', of a 3-channel color space.
data ChannelIndex = ChannelIndex { channel_index :: Matrix }
-- | The first channel of a color space represented by the ordered tripple,
-- @(u,v,w)@.
channel_u :: ChannelIndex
channel_u = ChannelIndex identity_matrix
-- | The second channel of a color space represented by the ordered tripple,
-- @(u,v,w)@.
channel_v :: ChannelIndex
channel_v = ChannelIndex $ matrix
[[0,1,0,0],
[1,0,0,0],
[0,0,1,0],
[0,0,0,1]]
-- | The third channel of a color space represented by the ordered tripple,
-- @(u,v,w)@.
channel_w :: ChannelIndex
channel_w = ChannelIndex $ matrix
[[0,0,1,0],
[0,1,0,0],
[1,0,0,0],
[0,0,0,1]]
-- | Pick a channel from a color space.
newChannel :: (ColorSpace cs) => ChannelIndex -> cs -> ColorChannel
newChannel (ChannelIndex ch_ix) cs = LinearChannel $ ch_ix `matrixMultiply` m
where (AffineColorSpace m) = affineColorSpaceOf cs
-- | Construct an isotropic 'ColorChannel' that runs along a
-- hue angle. The meaning of the hue angle depends on the
-- primary colors used in the construction of the color wheel.
newAngularChannel :: ColorWheel -> Angle -> ColorChannel
newAngularChannel (ColorWheel m) a = LinearChannel $
rotationMatrix (Vector3D 0 0 1) (scalarMultiply (-1) a)
`matrixMultiply`
m
-- | Construct an isotropic 'ColorChannel' along the radii
-- of a color wheel. This is exactly like calling
-- 'newAngularChannel' knowing in advance the specific
-- hue of the color in question.
newRadialChannel :: ColorWheel -> ColorChannel
newRadialChannel (ColorWheel m) = Preconfigure $ \c ->
let Point3D u v w = exportColorCoordinates c $ AffineColorSpace m
(a,_) = cartesianToPolar (u,v)
in newAngularChannel (ColorWheel m) a
-- | Construct a 'ColorChannel' representing the maximum
-- of the three color components. For example, the
-- maximum of @RGB 0.25 0.5 0.4@ is 0.5.
newMaximalChannel :: AffineColorSpace -> ColorChannel
newMaximalChannel cs@(AffineColorSpace m) = Preconfigure $ \c ->
let Point3D u v w = exportColorCoordinates c cs
maxi = maximum [abs u,abs v, abs w]
magn = magnitude [u,v,w]
in LinearChannel $
scale' (maxi / if nearZero magn then 1.0 else magn) $
rotateToFrom (Vector3D 1 0 0) (Vector3D u v w) $
transform m identity_matrix
-- | A view of a specific color channel, such as red, or luminance.
data LinearMetric = LinearMetric {
-- | The range of a color channel that is within gamut.
-- This range depends on the channel and the particular
-- color being observed, and may not exist if the
-- color itself is out of gamut.
linear_gamut_bounds :: Maybe (RSdouble,RSdouble),
-- | A function to modify a color channel independantly
-- from the other color channels in the same color space.
linear_color_function :: RSdouble -> Color,
-- | The value of the particular color channel for the
-- particular color.
linear_value :: RSdouble,
-- | The original color.
linear_original :: Color }
instance Show LinearMetric where
show x = show (linear_gamut_bounds x,
linear_value x,
exportColorCoordinates (linear_original x) color_space_rgb)
-- | Read a specific channel of a color.
viewChannel :: (ExportColorCoordinates c) =>
ColorChannel -> c -> LinearMetric
viewChannel (LinearChannel m) c = LinearMetric {
linear_gamut_bounds = case intersections of
[] -> Nothing
is -> Just (minimum is,maximum is),
linear_color_function = \x -> importColorCoordinates $
transformColorFromTo (AffineColorSpace m) (Point3D x v w),
linear_value = u,
linear_original = transformColor c }
where Point3D u v w = exportColorCoordinates c $ AffineColorSpace m
rgb@(Point3D r g b) = exportColorCoordinates c color_space_rgb
-- The vector describinb an adjustment of the specified
-- color channel.
rgb'@(Vector3D r' g' b') = colorVectorOf (AffineColorSpace m)
(Vector3D 1 0 0)
-- Find the intersections between the vector describing an
-- adjustment of the specified channel and the boundaries
-- of the RGB gamut's color cube.
intersections = map (+u) $ filter gamutValid $
map (uncurry (/)) $ filter (not . nearZero . snd)
[(1-r,r'),(1-g,g'),(1-b,b'),
(-r,r'),(-g,g'),(-b,b')]
-- Construct the color with a particular linear adjustment of the
-- specified color channel
colorFunction x = rgb `add` scalarMultiply x rgb'
-- Test that a color is within the RGB device gamut.
gamutValid x = let Point3D r'' g'' b'' = colorFunction x
in (>=3) $ length $ filter (>= (-0.001)) $ filter (<= 1.001) $ [r'',g'',b'']
viewChannel (Preconfigure f) c =
viewChannel (f $ transformColor c) c
-- | A color space specification or color type that has an associated
-- color space.
--
-- If a type implements both 'ImportColorCoordinates' and
-- 'ColorSpace', then it must ensure that:
--
-- @importColorCoordinates f =
-- (let c = importColorCoordinates (const $ f $ affineColorSpaceOf c) in c)@
--
-- This is not hard -- all that is required is that
-- @affineColorSpaceOf undefined@ is defined.
--
class ColorSpace cs where
affineColorSpaceOf :: cs -> AffineColorSpace
-- | A color type that can export its color coordinates.
-- An easy implementation is
--
-- @transformColorFromTo your_color_space your_color_coordinates@
--
-- If a type implements both 'ExportColorCoordinates' and
-- 'ImportColorCoordinates', then it must ensure that
-- @importColorCoordinates . exportColorCoordinates = id@.
--
class ExportColorCoordinates c where
exportColorCoordinates ::
c -> AffineColorSpace -> Point3D
-- | A color type that can import its color coordinates.
class ImportColorCoordinates c where
importColorCoordinates ::
(AffineColorSpace -> Point3D) -> c
instance ColorSpace AffineColorSpace where
affineColorSpaceOf = id
instance ColorSpace ColorWheel where
affineColorSpaceOf (ColorWheel m) = AffineColorSpace m
-- | A generic representation of Color.
newtype Color = Color { fromColor ::
AffineColorSpace -> Point3D }
instance ExportColorCoordinates Color where
exportColorCoordinates = fromColor
instance ImportColorCoordinates Color where
importColorCoordinates = Color
-- | Construct a new color space. This requires a minimal point
-- (the black point in an additive color space, or the white point
-- in a subtractive color space), and three primary colors.
-- The three primarys color correspond to the 'channel_u',
-- 'channel_v', and 'channel_w' respectively.
newColorSpace :: (ExportColorCoordinates c) =>
c -> c -> c -> c -> AffineColorSpace
newColorSpace k u v w = AffineColorSpace $ matrixInverse $
translationMatrix (vectorToFrom k' zero)
`matrixMultiply`
xyzMatrix (u' `sub` k')
(v' `sub` k')
(w' `sub` k')
where k' = exportColorCoordinates k color_space_rgb
u' = exportColorCoordinates u color_space_rgb
v' = exportColorCoordinates v color_space_rgb
w' = exportColorCoordinates w color_space_rgb
-- | Construct a new color wheel. This requires a minimal point,
-- (the black point in an additive color space, or the white point
-- in a subtractive color space), and three primary colors with
-- assigned hue angles and value parameters.
-- The hue angle maps onto 'channel_u' and 'channel_v', while
-- the value parameter maps directly and additively onto
-- 'channel_w'.
newColorWheel :: (ExportColorCoordinates c) =>
c -> (c,Angle,RSdouble) ->
(c,Angle,RSdouble) ->
(c,Angle,RSdouble) ->
ColorWheel
newColorWheel k (u,theta_u,u') (v,theta_v,v') (w,theta_w,w') =
ColorWheel $ uvw_to_wheel `matrixMultiply` matrixInverse uvw_to_rgb
where uvw_to_wheel = matrix $
[ [cosine theta_u, cosine theta_v, cosine theta_w, 0 ],
[ sine theta_u, sine theta_v, sine theta_w, 0 ],
[ u', v', w', 0 ],
[ 0, 0, 0, 1 ] ]
AffineColorSpace uvw_to_rgb = newColorSpace k u v w
-- | A color wheel constructed with red, green and blue device primaries
-- and a luminance component. This is the basis of the HCL color system.
{-# NOINLINE color_wheel_rgbl #-}
color_wheel_rgbl :: ColorWheel
color_wheel_rgbl = ColorWheel $ matrix $
[ [1.0 , -0.5 , -0.5 , 0.0],
[0.0 , (sqrt 3/2) , (negate $ sqrt 3 / 2) , 0.0],
[0.2126 , 0.7152 , 0.0722 , 0.0],
[0.0 , 0.0 , 0.0 , 1.0] ]
-- | The red-green-blue device color space.
{-# NOINLINE color_space_rgb #-}
color_space_rgb :: AffineColorSpace
color_space_rgb = AffineColorSpace $ identity_matrix
-- | Transform ordered triples between color spaces.
transformColorFromTo :: AffineColorSpace ->
Point3D ->
AffineColorSpace ->
Point3D
transformColorFromTo (AffineColorSpace source)
uvw
(AffineColorSpace destination) =
transform (destination `matrixMultiply` matrixInverse source)
uvw
-- | Transform a color vector into RGB space.
colorVectorOf :: AffineColorSpace ->
Vector3D ->
Vector3D
colorVectorOf (AffineColorSpace m) uvw =
inverseTransform m uvw
{-# RULES
"transformColor::a->a" transformColor = id
#-}
-- | Transform colors between color spaces.
transformColor :: (ExportColorCoordinates source,
ImportColorCoordinates dest) =>
source -> dest
transformColor = importColorCoordinates . exportColorCoordinates