reanimate/videos/color-theory/Spectrum.hs
David Himmelstrup 4bfafa4d8e Color-theory updates and related API changes. (#50)
Rewamp the scene api, add circle plots, add blender flips, fix caching issue in driver, fix bug in partialSvg.


Former-commit-id: 5cb900521044fee13766dd7474fd9b72c7691686
2020-02-02 09:17:29 +08:00

1210 lines
36 KiB
Haskell
Executable file

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE ApplicativeDo #-}
module Spectrum
( scene2
) where
import Control.Lens ((&), (.~))
import Codec.Picture
import Codec.Picture.Types
import Control.Monad
import qualified Data.ByteString as BS
import Data.Colour
import Data.Colour.CIE
import Data.Colour.CIE.Illuminant
import Data.Colour.RGBSpace
import Data.Colour.RGBSpace.HSV (hsvView)
import Data.Colour.SRGB
import Data.Colour.SRGB.Linear
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Ord
import Data.Text (Text)
import Graphics.SvgTree hiding (Text)
import Linear.V2
import Reanimate
import Reanimate.Animation
import Reanimate.Builtin.CirclePlot
import Reanimate.Builtin.Documentation
import qualified Reanimate.Builtin.TernaryPlot as Ternary
import Reanimate.ColorMap
import Reanimate.ColorSpace
import Reanimate.Constants
import Reanimate.Driver (reanimate)
import Reanimate.Effect
import Reanimate.Interpolate
import Reanimate.LaTeX
import Reanimate.Raster
import Reanimate.Scene
import Reanimate.Signal
import Reanimate.Svg
import Reanimate.Svg.BoundingBox
import System.IO.Unsafe
{- STORYBOARD
- Wavelength scene
* Background: wavelength/sensitivity grid
* Draw SML sensitivities with labels
* Hide labels
* Morph to XYZ sensitivities
* Label as XYZ
-}
long = [ (nm, l) | (nm, (l, m, s)) <- Map.toList coneSensitivity ]
medium = [ (nm, m) | (nm, (l, m, s)) <- Map.toList coneSensitivity ]
short = [ (nm, s) | (nm, (l, m, s)) <- Map.toList coneSensitivity ]
xCoords = [ (nm, x/2) | (nm, (x, y, z)) <- Map.toList bigXYZCoordinates ]
yCoords = [ (nm, y/2) | (nm, (x, y, z)) <- Map.toList bigXYZCoordinates ]
zCoords = [ (nm, z/2) | (nm, (x, y, z)) <- Map.toList bigXYZCoordinates ]
labScaleX = 110 -- 100 -- 128
labScaleY = 110 -- 100 -- 128
blueName = "royalblue"
greenName = "green"
redName = "maroon"
drawSensitivities :: Animation
drawSensitivities = sceneAnimation $ do
bg <- newSpriteSVG $ spectrumGrid True
spriteZ bg 1
forM_ [(short, blueName), (medium, greenName), (long, redName)] $
\(dat,name) -> do
fork $ newSpriteA' SyncFreeze $ drawSensitivity 1 dat name
# setDuration drawDur
wait drawStagger
where
drawDur = 3
drawStagger = 1
drawMorphingSensitivities :: Animation
drawMorphingSensitivities = sceneAnimation $ do
bg <- newSpriteSVG $ spectrumGrid True
spriteZ bg 1
forM_ keys $ \(datA, datB, name) -> do
fork $ newSpriteA $ morphSensitivity datA datB name
# setDuration drawDur
where
keys = [ (short, zCoords, blueName)
, (medium, yCoords, greenName)
, (long, xCoords, redName)]
drawDur = 3
{-
colorSpacesScene :: Animation
colorSpacesScene = sceneAnimation $ mdo
beginT <- queryNow
fork $ play $ frame
# setDuration (endT-beginT)
fork $ playZ 1 $ animate (const spectrumGrid)
# setDuration (endT-beginT)
dur <- withSceneDuration $ do
fork $ play $ drawSensitivity 1 short blueName
# setDuration drawDur
# pauseAtBeginning (drawStagger*0)
# pauseUntil dur
fork $ do
wait (drawStagger*0 + drawDur)
play $ drawLabel "S" blueName
# setDuration 2
# applyE (overBeginning 0.3 fadeInE)
fork $ play $ drawSensitivity 1 medium greenName
# setDuration drawDur
# pauseAtBeginning (drawStagger*1)
# pauseUntil dur
fork $ do
wait (drawStagger*1 + drawDur)
play $ drawLabel "M" greenName
# setDuration 2
# applyE (overBeginning 0.3 fadeInE)
fork $ do
wait (drawStagger*2 + drawDur)
play $ drawLabel "L" redName
# setDuration 2
# applyE (overBeginning 0.3 fadeInE)
play $ drawSensitivity 1 long redName
# setDuration drawDur
# pauseAtBeginning (drawStagger*2)
# pauseAtEnd drawPause
dur2 <- withSceneDuration $ do
fork $ do
wait drawDur
zLabel <- newSpriteA $ drawLabel "Z" blueName
zLabelFade <- spriteVar zLabel 0 withGroupOpacity
fork $ tweenVar zLabelFade 0.3 (\v -> fromToS v 1)
wait drawStagger
fork $ play $ drawLabel "Y" greenName
# setDuration 2
# applyE (overBeginning 0.3 fadeInE)
wait drawStagger
fork $ play $ drawLabel "X" redName
# setDuration 2
# applyE (overBeginning 0.3 fadeInE)
fork $ play $ morphSensitivity short zCoords blueName
# setDuration drawDur
-- # pauseAtBeginning (drawStagger*0)
# pauseUntil dur2
fork $ play $ morphSensitivity medium yCoords greenName
# setDuration drawDur
-- # pauseAtBeginning (drawStagger*1)
# pauseUntil dur2
play $ morphSensitivity long xCoords redName
# setDuration drawDur
-- # pauseAtBeginning (drawStagger*2)
# pauseAtEnd drawPause
endT <- queryNow
return ()
where
playAndFreezeUntil endT ani = do
now <- queryNow
fork $ play $ ani # pauseUntil (now-endT)
drawDur = 3
drawStagger = 1
drawPause = 4
-}
drawLabelS = drawLabel "S" blueName
drawLabelM = drawLabel "M" greenName
drawLabelL = drawLabel "L" redName
drawLabelZ = drawLabel "Z" blueName
drawLabelY = drawLabel "Y" greenName
drawLabelX = drawLabel "X" redName
scene2Intro :: Animation
scene2Intro = staticFrame 10 (spectrumGrid False)
illustrateSpectrum :: Animation
illustrateSpectrum = sceneAnimation $ do
grid <- newSpriteSVG $ spectrumGrid False
spriteZ grid 1
forM_ (zip [0 ..] spectrum) $ \(nth, intensity) -> do
let nm = fromIntegral nth * (300/23/2) + 400
fork $ drawLine nm intensity
wait 0.05
wait 5
return ()
where
spectrum =
[ 0.02, 0.03, 0.07, 0.1, 0.2, 0.35, 0.7, 0.8, 0.9 ,0.75, 0.5 ] ++
[ 0.3, 0.27, 0.31, 0.25, 0.3, 0.35, 0.4, 0.5] ++
[ 0.65, 0.8, 0.95, 1.0, 0.95, 0.98, 0.93, 0.97] ++
[ 0.85, 0.75, 0.65, 0.60, 0.55, 0.52, 0.48, 0.45] ++
[ 0.45, 0.40, 0.35, 0.30, 0.25, 0.20, 0.15, 0.15, 0.15, 0.13, 0.11, 0.08]
drawLine nm intensity = do
let Just c = fmap (promotePixel.toRGB8) (nmToColor (round nm))
s <- newSpriteSVG $
pathify $
withStrokeColorPixel c $
translate (-spectrumWidth/2) (-spectrumHeight/2) $
translate (fromToS 0 spectrumWidth ((nm-400)/(700-400))) 0 $
mkLine (0,0) (0, spectrumHeight*intensity)
spriteE s $ overEnding 0.3 fadeOutE
-- spriteTween s intensity $ partialSvg . curveS 2
spriteTween s 0.8 $ partialSvg . curveS 2
scene2 :: Animation
scene2 = seqA scene2Intro $ seqA illustrateSpectrum $ sceneAnimation $ do
oldGrid <- newSpriteSVG $ spectrumGrid False
newGrid <- newSpriteSVG $ spectrumGrid True
spriteTween newGrid 0.5 withGroupOpacity
destroySprite newGrid
destroySprite oldGrid
-- SML labels and timings.
labelS <- newSpriteSVG $ drawLabelSVG "S" blueName
spriteMap labelS $ uncurry translate (labelPosition short)
labelM <- fork $ do
wait 2.1
newSpriteA $ drawLabelM
spriteMap labelM $ uncurry translate (labelPosition medium)
labelL <- fork $ do
wait 3.5
newSpriteA $ drawLabelL
spriteMap labelL $ uncurry translate (labelPosition long)
forM_ [labelS, labelM, labelL] $ \label -> do
spriteE label $ overBeginning 0.3 fadeInE
spriteE label $ overEnding 0.3 fadeOutE
play $ drawSensitivities
# pauseAtEnd 1
-- Drop SML labels
forM_ [labelS, labelM, labelL] destroySprite
xyzGraph <- fork $ newSpriteA' SyncFreeze $ drawMorphingSensitivities
-- XYZ labels and timings
wait (duration drawMorphingSensitivities)
labelZ <- fork $ newSpriteA $ drawLabelZ
spriteE labelZ $ overBeginning 0.3 fadeInE
labelZPos <- spriteVar labelZ (labelPosition zCoords) $ uncurry translate
labelY <- fork $ newSpriteA $ drawLabelY
spriteE labelY $ overBeginning 0.3 fadeInE
labelYPos <- spriteVar labelY (labelPosition yCoords) $ uncurry translate
labelX <- fork $ newSpriteA $ drawLabelX
spriteE labelX $ overBeginning 0.3 fadeInE
labelXPos <- spriteVar labelX (labelPosition xCoords) $ uncurry translate
wait 4
fork $ spriteTween xyzGraph 2 $ \t -> scale (fromToS 1 0.5 $ curveS 2 t)
fork $ spriteTween xyzGraph 2 $ \t -> translate (fromToS 0 (screenWidth/4) $ curveS 2 t) 0
fork $ tweenVar labelZPos 2 $ \(x,y) t ->
let (newX, newY) = Ternary.toOffsetCartesianCoords 0 0
s = curveS 2 t
in (fromToS x (newX*5-screenWidth/4) s, fromToS y (newY*5-1) s)
fork $ tweenVar labelYPos 2 $ \(x,y) t ->
let (newX, newY) = Ternary.toOffsetCartesianCoords 1 0
s = curveS 2 t
in (fromToS x (newX*5-screenWidth/4) s, fromToS y (newY*5) s)
fork $ tweenVar labelXPos 2 $ \(x,y) t ->
let (newX, newY) = Ternary.toOffsetCartesianCoords 0 1
s = curveS 2 t
in (fromToS x (newX*5-screenWidth/4) s, fromToS y (newY*5-1) s)
wait 2
redFactor <- newVar 0
greenFactor <- newVar 0
blueFactor <- newVar 0
xyzSpace <- newSprite $
cieXYImage
<$> unVar redFactor
<*> unVar greenFactor
<*> unVar blueFactor
<*> pure imgSize
spriteMap xyzSpace $ translate (-screenWidth/4) 0
spriteZ xyzSpace (-1)
spriteTween xyzSpace 0.5 $ aroundCenter . scale . curveS 2
wait 1
spriteE labelX (overBeginning 1 $ aroundCenterE highlightE)
tweenVar redFactor 1 $ \v -> fromToS v 1 . curveS 2
spriteE labelY (overBeginning 1 $ aroundCenterE highlightE)
tweenVar greenFactor 1 $ \v -> fromToS v 1 . curveS 2
spriteE labelZ (overBeginning 1 $ aroundCenterE highlightE)
tweenVar blueFactor 1 $ \v -> fromToS v 1 . curveS 2
wait 3
let downShift = 1
visCurve <- newVar 0
waveLine <- fork $ newSpriteSVG $
translate (screenWidth/4) 0 $
scale 0.5 $
translate 0 (-spectrumHeight/2) $
wavelengthAxis
spriteTween waveLine 1 $ \d ->
translate 0 (fromToS 0 (-downShift) $ curveS 2 d)
-- wait 1
visLine <- newSprite $
withStrokeWidth 0.03 . withStrokeColor "white" . morphXYZCoordinates
<$> unVar visCurve
spriteZ visLine (1)
spriteTween waveLine 0.5 $ \t -> withGroupOpacity (1-t)
destroySprite waveLine
visSide <- spriteVar visLine 0 $ \t ->
translate 0 (-downShift) .
translate (fromToS (screenWidth/4) (-screenWidth/4) $ curveS 2 t) 0 .
scale (fromToS 0.5 1 $ curveS 2 t) .
translate 0 (fromToS (-spectrumHeight/2) 0 $ curveS 2 t)
fork $ tweenVar visSide 3 $ \v -> fromToS v 1 . curveS 2
fork $ tweenVar visCurve 3 $ \v -> fromToS v 1 . curveS 2
fork $ spriteTween visLine 3 $ \d ->
translate 0 (fromToS 0 downShift $ curveS 2 d)
obsVisible <- newVar 1
gamut <- newVar 0
reorient <- newVar 0
cmOpacity <- newVar 1
cmDelta <- newVar 0
cmName <- newVar "sinebow"
cmFunc <- newVar sinebow
visSpace <- fork $ newSprite $ do
getObs <- unVar obsVisible
getGamut <- unVar gamut
getReorient <- unVar reorient
getOpacity <- unVar cmOpacity
getDelta <- unVar cmDelta
getFunc <- unVar cmFunc
return $
translate (-screenWidth/4) 0 $
mkGroup
[ mkClipPath "visible"
[ simplify
obsColors
]
, mkClipPath "sRGB"
[ simplify $
sRGBTriangle getGamut
]
, withGroupOpacity getObs $
withClipPathRef (Ref "visible") $
mkGroup [cieXYImage 1 1 1 imgSize]
, translate 0 (1*getReorient) $
rotate (-gamutSlope sRGBGamut * getReorient) $
mkGroup
[ scale (1+0.5*getReorient) $
withClipPathRef (Ref "sRGB") $
mkGroup [cieXYImageGamut getGamut imgSize]
, lowerTransformations $ scale (1+0.5*getReorient) $
withGroupOpacity getOpacity $
cmToTernary getDelta getFunc
]
, withGroupOpacity getReorient $
translate 0 3.5 $ scale 0.5 $
center $ withFillColor "white" $
latex "sRGB"
]
colorMap <- newSprite $ do
getOpacity <- unVar cmOpacity
getDelta <- unVar cmDelta
getFunc <- unVar cmFunc
getName <- unVar cmName
return $
withGroupOpacity getOpacity $
mkGroup
[ renderColorMap getDelta (screenWidth*0.75) (screenHeight*0.15) getFunc
, withGroupOpacity (getDelta * 2) $
withFillColor "white" $
translate 0 1.2 $
scale 0.7 $
center $ latex getName
]
spriteTween colorMap 0 $ const $ translate 0 (-3)
wait 4
fork $ spriteTween visLine 1 $ \t -> withGroupOpacity (1-t)
fork $ spriteTween xyzSpace 1 $ \t -> withGroupOpacity (1-t)
wait 2
fork $ spriteTween xyzGraph 1 $ \t -> withGroupOpacity (1-t)
fork $ tweenVar labelXPos 1 $ \(x,y) t ->
let (newX, newY) = (-1,3)
s = curveS 2 t
in (fromToS x newX s, fromToS y newY s)
fork $ tweenVar labelYPos 1 $ \(x,y) t ->
let (newX, newY) = (0,3)
s = curveS 2 t
in (fromToS x newX s, fromToS y newY s)
fork $ tweenVar labelZPos 1 $ \(x,y) t ->
let (newX, newY) = (1,3)
s = curveS 2 t
in (fromToS x newX s, fromToS y newY s)
-- fork $ spriteTween labelX 1 $ \t -> withGroupOpacity (1-t)
-- fork $ spriteTween labelY 1 $ \t -> withGroupOpacity (1-t)
-- fork $ spriteTween labelZ 1 $ \t -> withGroupOpacity (1-t)
-- wait 1
spriteZ visSpace (-1)
spriteTween visSpace 1 $ \t ->
translate (fromToS 0 (screenWidth/4) $ curveS 2 t) 0
wait 2
rgb <- newSprite $ withStrokeColor "white" . sRGBTriangle <$> unVar gamut
spriteTween rgb 1 $ partialSvg
wait 1
fork $ spriteTween rgb 1 $ \t -> withGroupOpacity (1-t)
fork $ spriteTween labelX 1 $ \t -> withGroupOpacity (1-t)
fork $ spriteTween labelY 1 $ \t -> withGroupOpacity (1-t)
fork $ spriteTween labelZ 1 $ \t -> withGroupOpacity (1-t)
tweenVar obsVisible 1 $ \v -> fromToS v 0 . curveS 2
wait 1
-- tweenVar gamut 1 $ \v -> fromToS v 1 . curveS 2
tweenVar reorient 1 $ \v -> fromToS v 1 . curveS 2
wait 1
writeVar cmName "jet"
writeVar cmFunc jet
tweenVar cmDelta 1 $ \d -> fromToS d 1
wait 2
spriteTween visSpace 1 $ \t -> translate (-2.5*curveS 2 t) 0
hsv <- newSprite $ do
getOpacity <- unVar cmOpacity
getDelta <- unVar cmDelta
getFunc <- unVar cmFunc
return $
mkGroup
[ lowerTransformations $
scaleToWidth (screenWidth*0.20) $
mkGroup [ hsvColorSpace 100
, withGroupOpacity getOpacity $
cmToHSV getDelta getFunc]
, translate 0 2 $ scale 0.5 $
center $ withFillColor "white" $
latex "HSV" ]
-- lchColorSpace 100
-- cieLABImage 100 50
-- cieLABImage 2000 2000
spriteTween hsv 0 $ const $ translate 3 1.5
spriteTween hsv 1 withGroupOpacity
wait 1
tweenVar cmOpacity 0.3 $ \o t -> fromToS o 0 t
writeVar cmName "sinebow"
writeVar cmFunc sinebow
writeVar cmOpacity 1
writeVar cmDelta 0
tweenVar cmDelta 5 $ \d -> fromToS d 1
wait 1
tweenVar cmOpacity 0.3 $ \o t -> fromToS o 0 t
writeVar cmName "parula"
writeVar cmFunc parula
writeVar cmOpacity 1
writeVar cmDelta 0
tweenVar cmDelta 5 $ \d -> fromToS d 1
wait 1
fork $ spriteTween hsv 1 $ \t -> translate (2*curveS 2 t) 0
spriteTween visSpace 1 $ \t -> translate (-2*curveS 2 t) 0
lab <- newSprite $ do
getOpacity <- unVar cmOpacity
getDelta <- unVar cmDelta
getFunc <- unVar cmFunc
return $
mkGroup
[ lowerTransformations $
scaleToWidth (screenWidth/4) $
mkGroup [ cieLABImagePixels
, withGroupOpacity getOpacity $
cmToLAB getDelta getFunc]
, translate 0 2 $ scale 0.5 $
center $ withFillColor "white" $
latex "LAB" ]
spriteTween lab 0 $ const $ translate 0 1.5
spriteTween lab 1 withGroupOpacity
-- tweenVar rgbCM 1 $ \(cm,d) t -> (parula ,fromToS 0 1 t)
wait 1
tweenVar cmOpacity 0.3 $ \o t -> fromToS o 0 t
writeVar cmName "viridis"
writeVar cmFunc viridis
writeVar cmOpacity 1
writeVar cmDelta 0
tweenVar cmDelta 5 $ \d -> fromToS d 1
wait 1
tweenVar cmOpacity 0.3 $ \o t -> fromToS o 0 t
writeVar cmName "plasma"
writeVar cmFunc plasma
writeVar cmOpacity 1
writeVar cmDelta 0
tweenVar cmDelta 5 $ \d -> fromToS d 1
wait 1
tweenVar cmOpacity 0.3 $ \o t -> fromToS o 0 t
writeVar cmName "cividis"
writeVar cmFunc cividis
writeVar cmOpacity 1
writeVar cmDelta 0
tweenVar cmDelta 5 $ \d -> fromToS d 1
wait 1
tweenVar cmOpacity 0.3 $ \o t -> fromToS o 0 t
writeVar cmName "jet"
writeVar cmFunc jet
writeVar cmOpacity 1
writeVar cmDelta 0
tweenVar cmDelta 5 $ \d -> fromToS d 1
wait 1
tweenVar cmOpacity 0.3 $ \o t -> fromToS o 0 t
writeVar cmName "turbo"
writeVar cmFunc turbo
writeVar cmOpacity 1
writeVar cmDelta 0
tweenVar cmDelta 5 $ \d -> fromToS d 1
wait 1
return ()
where
imgSize = 50
obsColors =
lowerTransformations $
scale 5 $
renderXYZCoordinatesTernary
renderColorMap :: Double -> Double -> Double -> (Double -> PixelRGB8) -> Tree
renderColorMap delta width height cmap =
translate (-width/2 * (1-delta)) 0 $
mkGroup
[ scaleToSize (width*delta) height $ showColorMap (\t -> cmap (t*delta))
, withStrokeWidth (defaultStrokeWidth*0.7) $
withStrokeColor "white" $ withFillOpacity 0 $
mkRect (width*delta) height
]
{-
scene3 :: Animation
scene3 = sceneAnimation $ do
play $ mkAnimation 1 $ \t -> cieXYImage 0 0 t imgSize
play $ mkAnimation 1 $ \t -> cieXYImage t 0 1 imgSize
play $ mkAnimation 1 (\t -> cieXYImage 1 t 1 imgSize)
# pauseAtEnd 2
where
imgSize :: Num a => a
imgSize = 300
img1 t = cieXYImage t t t imgSize
obsColors =
lowerTransformations $
scale 5 $
renderXYZCoordinatesTernary
labColors =
lowerTransformations $
scale (100/2) $
renderLABCoordinates
-}
{-
frame = mkAnimation 2 $ \t ->
-- emit $ mkBackground "black"
-- emit $ spectrumGrid
let s = t
cm = hsv
in mkGroup
[ mkClipPath "sRGB"
[ simplify $
sRGBTriangle 0
]
, mkClipPath "visible"
[ simplify
obsColors
]
, mkClipPath "spectrum" $
let margin = 1 in
[ simplify $ lowerTransformations $
translate 0 (margin/2) $
pathify $
mkRect spectrumWidth (spectrumHeight+margin)
]
]
-- emit $ mkGroup
-- [ translate (-screenWidth*0.2) 0 $ mkGroup
-- [ -- withClipPathRef (Ref "sRGB") $
-- --withClipPathRef (Ref "visible") $
-- mkGroup [img1]
-- , withStrokeColor "white" $
-- sRGBTriangle
-- , withStrokeColor "white" $
-- obsColors
-- ]
-- , translate (screenWidth*0.2) 0 $ mkGroup
-- [ -- withClipPathRef (Ref "sRGB") $
-- withClipPathRef (Ref "sRGB") $
-- mkGroup [img1]
-- ]
-- ]
where
imgSize :: Num a => a
imgSize = 1000
img1 = cieXYImage 1 1 1 imgSize
img2 = cieLABImage imgSize 50
obsColors =
lowerTransformations $
scale 5 $
renderXYZCoordinatesTernary
labColors =
lowerTransformations $
scale (100/2) $
renderLABCoordinates
-}
{-
xyzTernaryPlot = mkAnimation 2 $ \t ->
-- emit $ mkBackground "black"
-- emit $ spectrumGrid
let s = t
cm = hsv
in mkGroup
[ mkClipPath "sRGB"
[ simplify $
sRGBTriangle 0
]
, mkClipPath "sRGBGamut"
[ simplify $
sRGBTriangle t
]
, mkClipPath "visible"
[ simplify
obsColors
]
, translate (-screenWidth*0.2) 0 $ mkGroup
[ -- withClipPathRef (Ref "sRGB") $
--withClipPathRef (Ref "visible") $
mkGroup [img1 t]
, withStrokeColor "white" $
sRGBTriangle 0
, withStrokeColor "white" $
obsColors
]
, translate (screenWidth*0.2) 0 $ mkGroup
[ withClipPathRef (Ref "sRGBGamut") $
--withClipPathRef (Ref "visible") $
mkGroup [cieXYImageGamut t imgSize]
]
-- , translate (screenWidth*0.2) 0 $ mkGroup
-- [ -- withClipPathRef (Ref "sRGB") $
-- withClipPathRef (Ref "sRGB") $
-- mkGroup [img1 t]
-- ]
]
where
imgSize :: Num a => a
imgSize = 50
img1 t = cieXYImage t t t imgSize
img2 = cieLABImage imgSize 50
obsColors =
lowerTransformations $
scale 5 $
renderXYZCoordinatesTernary
labColors =
lowerTransformations $
scale (100/2) $
renderLABCoordinates
-}
renderXYZCoordinatesTernary :: Tree
renderXYZCoordinatesTernary =
withFillOpacity 0 $
mkLinePath $
[ (x, y)
| (_nm, (red,green,blue)) <- Map.toList lightXYZCoordinates
, let (x,y) = Ternary.toOffsetCartesianCoords green red
]
morphXYZCoordinates :: Double -> Tree
morphXYZCoordinates t = withFillOpacity 0 $
mkLinePath $
[ (fromToS linearX (x*5) t, fromToS 0 (y*5) t)
| (nm, (red,green,blue)) <- Map.toList lightXYZCoordinates
, let (x,y) = Ternary.toOffsetCartesianCoords green red
, let percent = (fromIntegral nm-initNM)/(lastNM-initNM)
linearX = percent * spectrumWidth - spectrumWidth/2
]
where
dat = Map.toList lightXYZCoordinates
initNM = fromIntegral $ fst (head dat)
lastNM = 700 -- fromIntegral $ fst (last dat)
-- Red corner: 0.64 0.33 0
-- Green corner: 0.3 0.6 0.1
-- Blue corner: 0.15 0.06 0.79
--1 0 0 -> 0.64 0.33 0.0
--0 1 0 -> 0.30 0.60 0.1
cieXYImageGamut :: Double -> Int -> Tree
cieXYImageGamut t density = Ternary.ternaryPlot density $ \aCoord bCoord cCoord ->
let
aCoord' = fromToS aCoord (rX * aCoord + gX * bCoord + bX * cCoord) t
bCoord' = fromToS bCoord (rY * aCoord + gY * bCoord + bY * cCoord) t
cCoord' = fromToS cCoord (rZ * aCoord + gZ * bCoord + bZ * cCoord) t
RGB r g b = toSRGBBounded (cieXYZ aCoord' bCoord' cCoord')
in PixelRGBA8 r g b 0xFF
where
RGB r g b = primaries sRGBGamut
(rX, rY, rZ) = chromaCoords $ chromaConvert r
(gX, gY, gZ) = chromaCoords $ chromaConvert g
(bX, bY, bZ) = chromaCoords $ chromaConvert b
-- slope in degrees
gamutSlope :: RGBGamut -> Double
gamutSlope gamut = atan2 (y1/y2) (x1/x2) / pi * 180
where
RGB r g b = primaries sRGBGamut
(gX, gY, _gZ) = chromaCoords $ chromaConvert g
(bX, bY, _bZ) = chromaCoords $ chromaConvert b
(x1, y1) = Ternary.toCartesianCoords gX gY
(x2, y2) = Ternary.toCartesianCoords bX bY
strokeLine :: Double -> [(Double, Double)] -> SVG
strokeLine t points = mkGroup
[ withStrokeWidth (defaultStrokeWidth*1) $
withFillOpacity 0 $ withStrokeColor "black" $
partialSvg t $
mkLinePath points
& strokeLineCap .~ pure CapRound
, withStrokeWidth (defaultStrokeWidth*0.5) $
withFillOpacity 0 $ withStrokeColor "white" $
partialSvg t $ mkLinePath points
& strokeLineCap .~ pure CapRound
]
-- 0.15 0.06 -> 0 0
cmToTernary :: Double -> (Double -> PixelRGB8) -> Tree
cmToTernary 0 _ = mkGroup []
cmToTernary t cm =
lowerTransformations $ scale 5 $
strokeLine t points
where
steps = 100
points =
[ Ternary.toOffsetCartesianCoords (cieY/s) (cieX/s)
| n <- [0..steps]
, let PixelRGB8 red green blue = cm (fromIntegral n / fromIntegral steps)
(cieX,cieY,cieZ) = cieXYZView (sRGB24 red green blue)
s = cieX + cieY + cieZ
]
cmToHSV :: Double -> (Double -> PixelRGB8) -> Tree
cmToHSV t cm =
lowerTransformations $ scale (screenHeight/2) $
strokeLine t points
where
steps = 100
points =
[ (cos radian * s, sin radian * s)
| n <- [0..steps]
, let PixelRGB8 red green blue = cm (fromIntegral n / fromIntegral steps)
(h,s,_v) = hsvView (toSRGB $ sRGB24 red green blue)
radian = h/180*pi
]
-- dim = 100
-- -labScaleX = 0
-- 0 = dim/2
-- +labScaleX = dim
-- -labScaleX to +labScaleX
cmToLAB :: Double -> (Double -> PixelRGB8) -> Tree
cmToLAB t cm =
lowerTransformations $
translate (-screenHeight/2) (-screenHeight/2) $
strokeLine t points
where
steps = 100
points =
[ ( (a+labScaleX)/(labScaleX*2)*screenHeight,
(b+labScaleY)/(labScaleY*2)*screenHeight )
| n <- [0..steps]
, let PixelRGB8 red green blue = cm (fromIntegral n / fromIntegral steps)
(_l,a,b) = cieLABView d65 (sRGB24 red green blue)
]
-- aCoord = red
-- bCoord = green
-- cCoord = blue
-- closer to white when (aCoord+cCoord) approaches 0
cieXYImage :: Double -> Double -> Double -> Int -> Tree
cieXYImage redFactor greenFactor blueFactor density =
Ternary.ternaryPlot density $ \aCoord bCoord cCoord ->
let
white = chromaColour d65 1
c = cieXYZ aCoord bCoord cCoord
redD = fromToS (1-aCoord) 1 redFactor
greenD = fromToS (1-bCoord) 1 greenFactor
blueD = fromToS (1-cCoord) 1 blueFactor
RGB r g b = toSRGBBounded $
blend (1-redD) white $
blend (1-greenD) white $
blend (1-blueD) white $ c
-- RGB r g b = toSRGBBounded (chromaColour d65 1)
in PixelRGBA8 r g b 0xFF
cieLABImagePixels :: SVG
cieLABImagePixels = scaleToHeight screenHeight $ embedImage $
unsafePerformIO $ do
dat <- BS.readFile "lab.png"
case decodePng dat of
Left err -> error err
Right img -> return $ convertRGBA8 img
cieLABImages :: Int -> Tree
cieLABImages dim = mkGroup
[ cieLABImage dim lStar
| lStar <- [40..95]
]
cieLABImage :: Int -> Double -> Tree
cieLABImage dim = embedImage . cieLABImage' dim
cieLABImage' dim lStar = generateImage gen dim dim
where
gen x y =
let
aStar = (fromIntegral x / fromIntegral dim) * labScaleX*2 - labScaleX
bStar = (1-(fromIntegral y / fromIntegral dim)) * labScaleY*2 - labScaleY
-- lStar = 50 -- findLStar aStar bStar
color = cieLAB d65 lStar aStar bStar
RGB r g b = toSRGBBounded (cieLAB d65 lStar aStar bStar)
-- RGB r g b = RGB (round $ lStar/100 * 255) (round $ lStar/100 * 255) (round $ lStar/100 * 255)
in if inGamut sRGBGamut color
then PixelRGBA8 r g b 0xFF
else PixelRGBA8 0xFF 0xFF 0xFF 0x00
cieLABImage_ :: Int -> Tree
cieLABImage_ = embedImage . cieLABImage_'
cieLABImage_' dim = generateImage gen dim dim
where
gen x y =
let
aStar = (fromIntegral x / fromIntegral dim) * labScaleX*2 - labScaleX
bStar = (1-(fromIntegral y / fromIntegral dim)) * labScaleY*2 - labScaleY
-- lStar = 50 -- findLStar aStar bStar
colors = [ toSRGBBounded color
| lStar <- reverse [40 .. 95]
, let color = cieLAB d65 lStar aStar bStar
, inGamut sRGBGamut color ]
in case listToMaybe colors of
Nothing -> PixelRGBA8 0xFF 0xFF 0xFF 0x00
Just (RGB r g b) -> PixelRGBA8 r g b 0xFF
findLStar :: Double -> Double -> Double
findLStar aStar bStar = worker 0 100 10
where
-- worker minL maxL n | trace (show (minL, maxL, n)) False = undefined
worker minL maxL 0 = minL + (maxL-minL)/2
worker minL maxL n
| total > 1 = worker minL thisL (n-1)
| otherwise = worker thisL maxL (n-1)
where
thisL = minL + (maxL-minL)/2
(x,y,z) = cieXYZView (cieLAB d65 thisL aStar bStar)
total = x+y+z
colorMapToLABCoords :: (Double -> PixelRGB8) -> Tree
colorMapToLABCoords colorMap = withFillOpacity 0 $ mkLinePath
[ (aStar, 1-bStar)
| n <- [0 .. steps]
, let PixelRGB8 r g b = colorMap (n/steps)
(lStar, aStar, bStar) = cieLABView d65 (sRGB24 r g b)
]
where
steps = 100
colorMapToXYZCoords :: (Double -> PixelRGB8) -> Tree
colorMapToXYZCoords colorMap = withFillOpacity 0 $ mkLinePath
[ (x/s, 1-(y/s))
| n <- [0 .. steps]
, let PixelRGB8 r g b = colorMap (n/steps)
(x, y, z) = cieXYZView (sRGB24 r g b)
s = x+y+z
]
where
steps = 100
sRGBTriangle :: Double -> Tree
sRGBTriangle t =
lowerTransformations $
scale 5 $
withFillOpacity 0 $
mkClosedLinePath
[ Ternary.toOffsetCartesianCoords (fromToS rY 0 t) (fromToS rX 1 t)
, Ternary.toOffsetCartesianCoords (fromToS gY 1 t) (fromToS gX 0 t)
, Ternary.toOffsetCartesianCoords (fromToS bY 0 t) (fromToS bX 0 t) ]
where
RGB r g b = primaries sRGBGamut
(rX, rY, _) = chromaCoords $ chromaConvert r
(gX, gY, _) = chromaCoords $ chromaConvert g
(bX, bY, _) = chromaCoords $ chromaConvert b
mkClosedLinePath :: [(Double, Double)] -> Tree
mkClosedLinePath [] = mkGroup []
mkClosedLinePath ((startX, startY):rest) =
PathTree $ defaultSvg & pathDefinition .~ cmds
& strokeLineJoin .~ pure JoinRound
where
cmds = [ MoveTo OriginAbsolute [V2 startX startY]
, LineTo OriginAbsolute [ V2 x y | (x, y) <- rest ]
, EndPath ]
spectrumHeight = screenHeight * 0.5
spectrumWidth = screenWidth * 0.7
wavelengthAxis :: Tree
wavelengthAxis =
withStrokeWidth strokeWidth $
mkGroup
[ translate (-spectrumWidth/2) 0 $
withFillOpacity 0 $ withStrokeColor "white" $ mkPath $
[ MoveTo OriginAbsolute [V2 0 0]
, VerticalTo OriginRelative [tickLength,-tickLength]
, HorizontalTo OriginRelative [spectrumWidth]
, VerticalTo OriginRelative [tickLength,-tickLength]
]
++ concat
[ [ MoveTo OriginAbsolute [V2 (n / (nTicksX-1) * spectrumWidth) 0]
, VerticalTo OriginRelative [tickLength]]
| n <- [0..nTicksX-1]
]
]
where
strokeWidth = 0.03
nTicksX = 24
nTicksY = fromIntegral (round (spectrumHeight/spectrumWidth * nTicksX))
tickLength = spectrumHeight*0.02
spectrumGrid :: Bool -> Tree
spectrumGrid includeSensitivity =
withStrokeWidth strokeWidth $
mkGroup
[ --center $
translate (-spectrumWidth/2) (spectrumHeight/2) $
withFillOpacity 0 $ withStrokeColor "white" $ mkPath $
[ MoveTo OriginAbsolute [V2 0 0]
, HorizontalTo OriginRelative [tickLength,-tickLength]
, VerticalTo OriginRelative [-spectrumHeight]
, HorizontalTo OriginRelative [spectrumWidth]
, VerticalTo OriginRelative [tickLength,-tickLength]
]
++ concat
[ [ MoveTo OriginAbsolute [V2 (n / (nTicksX-1) * spectrumWidth) (-spectrumHeight)]
, VerticalTo OriginRelative [tickLength]]
| n <- [0..nTicksX-1]
]
++ concat
[ [ MoveTo OriginAbsolute [V2 0 (n / (nTicksY-1) * negate spectrumHeight)]
, HorizontalTo OriginRelative [tickLength]]
| n <- [0..nTicksY-1]
]
, if includeSensitivity
then withFillColor "white" $
translate (-spectrumWidth*0.5 + svgWidth sensitivity*1.2) 0 $
sensitivity
else None
, withFillColor "white" $
translate 0 (-spectrumHeight*0.5 + svgHeight wavelength*1.2) $
wavelength
, withFillColor "white" $
translate (-spectrumWidth*0.5 - svgWidth shortWaves)
(-spectrumHeight*0.5 + svgHeight shortWaves*1.2) $
shortWaves
, withFillColor "white" $
translate (spectrumWidth*0.5 - svgWidth longWaves)
(-spectrumHeight*0.5 + svgHeight longWaves*1.2) $
longWaves
]
where
strokeWidth = 0.03
nTicksX = 24
nTicksY = fromIntegral (round (spectrumHeight/spectrumWidth * nTicksX))
tickLength = spectrumHeight*0.02
sensitivity =
center $
scale 0.7 $ rotate 90 $
latex "Sensitivity $\\rightarrow$"
wavelength =
center $
scale 0.8 $
latex "Wavelength"
shortWaves =
rotate (-45) $ center $
scale 0.3 $
latex "400 nm"
longWaves =
rotate (-45) $ center $
scale 0.3 $
latex "700 nm"
drawSensitivity :: Double -> [(Nanometer, Double)] -> String -> Animation
drawSensitivity maxHeight dat c = animate $ \limit ->
sensitivitySVG maxHeight limit dat c
morphSensitivity
:: [(Nanometer, Double)]
-> [(Nanometer, Double)]
-> String
-> Animation
morphSensitivity datA datB c = animate $ \t ->
let m = curveS 2 t
dat = [ (nm, fromToS a b m)
| ((nm,a),(_,b)) <- zip datA datB ]
in sensitivitySVG 1 1 dat c
-- emit $
-- withClipPathRef (Ref "spectrum") $
-- simplify $ lowerTransformations $ pathify $
-- mkBackground "green"
sensitivitySVG :: Double -> Double -> [(Nanometer, Double)] -> String -> Tree
sensitivitySVG maxHeight limit dat c =
mkGroup
[ mkClipPath "spectrum" $
let margin = 1 in
[ simplify $ lowerTransformations $
translate 0 (margin/2) $
pathify $
mkRect spectrumWidth (spectrumHeight+margin)
]
,
withClipPathRef (Ref "spectrum") $
simplify $ lowerTransformations $
withStrokeColor c $
withFillOpacity 0 $
translate (-spectrumWidth/2) (-spectrumHeight/2) $
mkPath $ MoveTo OriginAbsolute [V2 0 0] :
[ LineTo OriginAbsolute [V2 x y]
| (nm, n) <- dat
, fromIntegral nm <= lastNM
, let percent = (fromIntegral nm-initNM)/(lastNM-initNM)
x = percent * spectrumWidth
y = n/maxHeight * spectrumHeight
, percent <= limit
] ]
where
initNM = fromIntegral $ fst (head dat)
lastNM = 700 -- fromIntegral $ fst (last dat)
labelPosition :: [(Nanometer, Double)] -> (Double, Double)
labelPosition dat =
( spectrumWidth*percent - spectrumWidth/2
, labelY - spectrumHeight/2)
where
(nm,v) = maximumBy (comparing snd) dat
labelY = v * spectrumHeight
initNM = fromIntegral $ fst (head dat)
lastNM = 700
percent = (fromIntegral nm-initNM)/(lastNM-initNM)
drawLabel :: Text -> String -> Animation
drawLabel label c = animate $ const $
translate (0) (-svgHeight labelSVG * 1.5) $
withFillColor c $
labelSVG
where
labelSVG =
center $
scale 1 $
latex label
drawLabelSVG :: Text -> String -> SVG
drawLabelSVG label c =
translate (0) (-svgHeight labelSVG * 1.5) $
withFillColor c $
labelSVG
where
labelSVG =
center $
scale 1 $
latex label
moveUp :: SVG -> SVG
moveUp svg = translate 0 (-svgHeight svg * 1.5) svg
moveDown :: SVG -> SVG
moveDown svg = translate 0 (svgHeight svg * 1.5) svg
interpolation :: Animation
interpolation = mkAnimation 2 $ \t ->
mkGroup
[ translate 0 3 $
scaleXY (curveS 2 1) 0.2 $
showColorMap $ interpolateRGB8 hsvComponents a b
, translate 0 1 $
scaleXY (curveS 2 1) 0.2 $
showColorMap $ interpolateRGB8 xyzComponents a b
, translate 0 (-1) $
scaleXY (curveS 2 1) 0.2 $
showColorMap $ interpolateRGB8 lchComponents a b
, translate 0 (-3) $
scaleXY (curveS 2 1) 0.2 $
showColorMap $ interpolateRGB8 labComponents a b
]
where
a = cyan -- green
b = red
yellow = PixelRGB8 0xFF 0xFF 0x00
green = PixelRGB8 0x00 0xFF 0x00
blue = PixelRGB8 0x00 0x00 0xFF
cyan = PixelRGB8 0x00 0xFF 0xFF
red = PixelRGB8 0xFF 0x00 0x00
lchColorSpace :: Int -> Tree
lchColorSpace width =
circlePlot width $ \ang radius ->
let
toRad deg = deg/180 * pi
h = ang/pi*180
aStar = (cos (toRad h) * c)
bStar = (sin (toRad h) * c)
l = 50 -- findLStar aStar bStar
c = fromToS 0 (sqrt (labScaleX^2 + labScaleY^2)) radius
-- RGB r g b = toSRGBBounded (colorPack lchComponents l c h)
color = cieLAB d65 l aStar bStar
RGB r g b = toSRGBBounded color
in if inGamut sRGBGamut color
then PixelRGBA8 r g b 0xFF
else PixelRGBA8 0 0 0 0x00
hsvColorSpace :: Int -> Tree
hsvColorSpace width =
circlePlot width $ \ang radius ->
let
h = ang/pi*180
v = 1
s = radius
color = colorPack hsvComponents h s v
RGB r g b = toSRGBBounded color
in if inGamut sRGBGamut color
then PixelRGBA8 r g b 0xFF
else PixelRGBA8 0 0 0 0x00
spacesA :: Animation
spacesA = mkAnimation 10 $ \t ->
scaleToSize screenWidth screenHeight $
if t < 0.3
then cieLABImage 100 50
else if t < 0.6
then lchColorSpace 100
else hsvColorSpace 100
highlightE :: Effect
highlightE d t =
scale (1 + bellS 2 (t/d)*0.5) . rotate (wiggleS (t/d) * 20)
-- s-curve, sin, s-curve
wiggleS :: Signal
wiggleS t
| t < 0.25 = curveS 2 (t*4)
| t < 0.75 = sin ((t-0.25)*2*pi+pi/2)
| otherwise = curveS 2 ((t-0.75)*4)-1