chart-svg-0.2.2: src/Chart/Reanimate.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RebindableSyntax #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
-- | Integration of reanimate and chart-svg
module Chart.Reanimate
( ReanimateConfig (..),
defaultReanimateConfig,
animChartSvg,
ChartReanimate (..),
chartReanimate,
toTreeA,
tree,
treeFromFile,
)
where
import Chart as C hiding (Line, renderChartsWith, transform)
import Codec.Picture.Types
import Control.Lens hiding (transform)
import qualified Data.Attoparsec.Text as A
import Graphics.SvgTree as Svg hiding (Text)
import qualified Graphics.SvgTree.PathParser as Svg
import Linear.V2
import NumHask.Prelude hiding (fold)
import Reanimate as Re
-- | global reanimate configuration.
--
-- >>> defaultReanimateConfig
-- ReanimateConfig {duration = 5.0, background = Just "black", globalFontFamily = Just ["Arial","Helvetica","sans-serif"], globalFontStyle = Just FontStyleNormal, globalAlignment = AlignxMinYMin}
data ReanimateConfig = ReanimateConfig
{ duration :: Double,
background :: Maybe Text,
globalFontFamily :: Maybe [Text],
globalFontStyle :: Maybe Svg.FontStyle,
globalAlignment :: Svg.Alignment
}
deriving (Eq, Show, Generic)
-- |
defaultReanimateConfig :: ReanimateConfig
defaultReanimateConfig = ReanimateConfig 5 (Just "black") (Just ["Arial", "Helvetica", "sans-serif"]) (Just FontStyleNormal) AlignxMinYMin
-- | Animate a ChartSvg animation.
animChartSvg :: ReanimateConfig -> (Double -> ChartSvg) -> Animation
animChartSvg cfg cs =
mkAnimation (view #duration cfg) $ toTreeA cfg cs
globalAtts :: ReanimateConfig -> Svg.DrawAttributes
globalAtts cfg =
mempty
& maybe
id
(\x -> fontFamily .~ Last (Just (fmap unpack x)))
(view #globalFontFamily cfg)
. maybe
id
(\x -> fontStyle .~ Last (Just x))
(view #globalFontStyle cfg)
-- | The output of the raw translation of ChartSvg to a reanimate svg tree.
data ChartReanimate = ChartReanimate
{ trees :: [Tree],
box :: Rect Double,
size :: C.Point Double
}
deriving (Eq, Show, Generic)
-- | Render a 'ChartSvg' to 'Tree's, the fitted chart viewbox, and the suggested SVG dimensions
chartReanimate :: ChartSvg -> ChartReanimate
chartReanimate cs = ChartReanimate ts rect' size'
where
(cl'', rect', size') = renderToCRS so cl'
so = view #svgOptions cs
cl' = renderToCharts cs
ts = tree <$> cl''
-- | convert a ChartSvg animation to a Tree animation.
toTreeA :: ReanimateConfig -> (Double -> ChartSvg) -> Double -> Tree
toTreeA cfg cs x =
reCss (cs x & view (#svgOptions . #cssOptions)) $
mkGroup $
(mkBackground . unpack <$> maybeToList (view #background cfg))
<> [ ( \cr ->
let (Rect x z y w) =
view #box cr
in withViewBox'
(x, y, z - x, w - y)
(PreserveAspectRatio False (view #globalAlignment cfg) Nothing)
$ flipYAxis $
groupTrees (globalAtts cfg) $ view #trees cr
)
$ chartReanimate
(cs x)
]
reCss :: CssOptions -> (Tree -> Tree)
reCss NoCssOptions = id
reCss UseCssCrisp = Svg.cssApply (Svg.cssRulesOfText "* { shape-rendering: crispEdges; }")
reCss UseGeometricPrecision = Svg.cssApply (Svg.cssRulesOfText "* { shape-rendering: geometricPrecision; }")
withViewBox' :: (Double, Double, Double, Double) -> Svg.PreserveAspectRatio -> Tree -> Tree
withViewBox' vbox par child =
Re.translate (- screenWidth / 2) (- screenHeight / 2) $
svgTree
Document
{ _documentViewBox = Just vbox,
_documentWidth = Just (Num screenWidth),
_documentHeight = Just (Num screenHeight),
_documentElements = [child],
_documentDescription = "",
_documentLocation = "",
_documentAspectRatio = par
}
-- | Rectange svg
treeRect :: Rect Double -> Tree
treeRect a =
RectangleTree $ rectSvg a defaultSvg
-- | Text svg
treeText :: TextStyle -> Text -> C.Point Double -> Tree
treeText s t p =
TextTree Nothing (textAt (pointSvg p) t)
& maybe id (\x -> drawAttributes %~ rotatePDA x p) (s ^. #rotation)
-- | GlyphShape to svg Tree
treeShape :: GlyphShape -> Double -> C.Point Double -> Tree
treeShape CircleGlyph s p =
CircleTree $ Circle mempty (pointSvg p) (Num (s / 2))
treeShape SquareGlyph s p = treeRect (move p ((s *) <$> one))
treeShape (RectSharpGlyph x') s p =
treeRect (move p (C.scale (C.Point s (x' * s)) one))
treeShape (RectRoundedGlyph x'' rx ry) s p =
RectangleTree
. rectSvg (addPoint p $ C.scale (C.Point s (x'' * s)) one)
. (rectCornerRadius .~ (Just $ Num rx, Just $ Num ry))
$ defaultSvg
treeShape (TriangleGlyph (C.Point xa ya) (C.Point xb yb) (C.Point xc yc)) s p =
PolygonTree
. (polygonPoints .~ rps)
$ (drawAttributes %~ translateDA p) defaultSvg
where
rps =
[ V2 (s * xa) (- s * ya),
V2 (s * xb) (- s * yb),
V2 (s * xc) (- s * yc)
]
treeShape (EllipseGlyph x') s p =
EllipseTree $
Ellipse
mempty
(pointSvg p)
(Num $ s / 2)
(Num $ (x' * s) / 2)
treeShape (VLineGlyph x') s (C.Point x y) =
LineTree $
Line
(mempty & strokeWidth .~ Last (Just (Num x')))
(pointSvg (C.Point x (y - s / 2)))
(pointSvg (C.Point x (y + s / 2)))
treeShape (HLineGlyph x') s (C.Point x y) =
LineTree $
Line
(mempty & strokeWidth .~ Last (Just (Num x')))
(pointSvg (C.Point (x - s / 2) y))
(pointSvg (C.Point (x + s / 2) y))
treeShape (PathGlyph path) s p =
Svg.PathTree
( Svg.Path
( Svg.defaultSvg
& (Svg.drawAttributes %~ scaleDA (C.Point s s) . translateDA p)
)
(either mempty id $ A.parseOnly Svg.pathParser path)
)
-- | GlyphStyle to svg Tree
treeGlyph :: GlyphStyle -> C.Point Double -> Tree
treeGlyph s p =
treeShape (s ^. #shape) (s ^. #size) p
& maybe id (\x -> drawAttributes %~ rotatePDA x p) (s ^. #rotation)
-- | line svg
treeLine :: [C.Point Double] -> Tree
treeLine xs =
PolyLineTree
. (polyLinePoints .~ ((\(C.Point x y) -> V2 x (- y)) <$> xs))
$ defaultSvg
-- | GlyphStyle to svg Tree
treePath :: [PathInfo Double] -> [C.Point Double] -> Tree
treePath s p =
PathTree $
Path
mempty
( zipWith
(curry toPathCommand)
s
(fmap (\(C.Point x y) -> C.Point x (- y)) p)
)
-- | convert a 'Chart' to a 'Tree'
tree :: Chart Double -> Tree
tree (Chart (TextA s ts) xs) =
groupTrees (daText s) (zipWith (treeText s) ts (toPoint <$> xs))
tree (Chart (GlyphA s) xs) =
groupTrees (daGlyph s) (treeGlyph s . toPoint <$> xs)
tree (Chart (LineA s) xs) =
groupTrees (daLine s) [treeLine (toPoint <$> xs)]
tree (Chart (RectA s) xs) =
groupTrees (daRect s) (treeRect <$> (toRect <$> xs))
tree (Chart (PathA s pis) xs) =
groupTrees (daPath s) [treePath pis (toPoint <$> xs)]
tree (Chart BlankA _) =
groupTrees mempty []
-- | add drawing attributes as a group svg wrapping a [Tree]
groupTrees :: DrawAttributes -> [Tree] -> Tree
groupTrees da' tree' =
GroupTree (drawAttributes %~ (<> da') $ groupChildren .~ tree' $ defaultSvg)
-- * DrawAttribute computations
daRect :: RectStyle -> DrawAttributes
daRect o =
mempty
& (strokeWidth .~ Last (Just $ Num (o ^. #borderSize)))
& (strokeColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #borderColor)))
& (strokeOpacity ?~ realToFrac (opac $ o ^. #borderColor))
& (fillColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color)))
& (fillOpacity ?~ realToFrac (opac $ o ^. #color))
daText :: () => TextStyle -> DrawAttributes
daText o =
mempty
& (fontSize .~ Last (Just $ Num (o ^. #size)))
& (strokeWidth .~ Last (Just $ Num 0))
& (strokeColor .~ Last (Just FillNone))
& (fillColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color)))
& (fillOpacity ?~ realToFrac (opac $ o ^. #color))
& (textAnchor .~ Last (Just (toTextAnchor $ o ^. #anchor)))
where
toTextAnchor :: Anchor -> Svg.TextAnchor
toTextAnchor AnchorMiddle = TextAnchorMiddle
toTextAnchor AnchorStart = TextAnchorStart
toTextAnchor AnchorEnd = TextAnchorEnd
daGlyph :: GlyphStyle -> DrawAttributes
daGlyph o =
mempty
& (strokeWidth .~ Last (Just $ Num (o ^. #borderSize)))
& ( strokeColor
.~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #borderColor))
)
& (strokeOpacity ?~ realToFrac (opac $ o ^. #borderColor))
& (fillColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color)))
& (fillOpacity ?~ realToFrac (opac $ o ^. #color))
& maybe id (\(C.Point x y) -> transform ?~ [Translate x (- y)]) (o ^. #translate)
daLine :: LineStyle -> DrawAttributes
daLine o =
mempty
& (strokeWidth .~ Last (Just $ Num (o ^. #width)))
& (strokeColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color)))
& (strokeOpacity ?~ realToFrac (opac $ o ^. #color))
& (fillColor .~ Last (Just FillNone))
& maybe
id
(\x -> strokeLineCap .~ Last (Just $ fromLineCap' x))
(o ^. #linecap)
& maybe
id
(\x -> strokeLineJoin .~ Last (Just $ fromLineJoin' x))
(o ^. #linejoin)
& maybe
id
(\x -> strokeOffset .~ Last (Just $ Num x))
(o ^. #dashoffset)
& maybe
id
(\xs -> strokeDashArray .~ Last (Just (Num <$> xs)))
(o ^. #dasharray)
fromLineCap' :: LineCap -> Svg.Cap
fromLineCap' LineCapButt = CapButt
fromLineCap' LineCapRound = CapRound
fromLineCap' LineCapSquare = CapSquare
fromLineJoin' :: C.LineJoin -> Svg.LineJoin
fromLineJoin' LineJoinMiter = JoinMiter
fromLineJoin' LineJoinBevel = JoinBevel
fromLineJoin' LineJoinRound = JoinRound
daPath :: PathStyle -> DrawAttributes
daPath o =
mempty
& (strokeWidth .~ Last (Just $ Num (o ^. #borderSize)))
& ( strokeColor
.~ Last
(Just $ ColorRef (toPixelRGBA8 $ o ^. #borderColor))
)
& (strokeOpacity ?~ realToFrac (opac $ o ^. #borderColor))
& (fillColor .~ Last (Just $ ColorRef (toPixelRGBA8 $ o ^. #color)))
& (fillOpacity ?~ realToFrac (opac $ o ^. #color))
-- * svg primitives
-- | Convert to reanimate color primitive.
toPixelRGBA8 :: Colour -> PixelRGBA8
toPixelRGBA8 (Colour r g b o) =
PixelRGBA8
(fromIntegral (floor $ r * 256 :: Int))
(fromIntegral (floor $ g * 256 :: Int))
(fromIntegral (floor $ b * 256 :: Int))
(fromIntegral (floor $ o * 256 :: Int))
-- | convert a point to the svg co-ordinate system
-- The svg coordinate system has the y-axis going from top to bottom.
pointSvg :: C.Point Double -> (Svg.Number, Svg.Number)
pointSvg (C.Point x y) = (Num x, Num (- y))
-- | A DrawAttributes to rotate around a point by x degrees.
rotatePDA :: (HasDrawAttributes s) => Double -> C.Point Double -> s -> s
rotatePDA a (C.Point x y) s = s & transform %~ (Just . maybe r (<> r))
where
r = [Rotate (- a * 180 / pi) (Just (x, - y))]
-- | A DrawAttributes to translate by a Point.
translateDA :: (HasDrawAttributes s) => C.Point Double -> s -> s
translateDA (C.Point x' y') =
transform
%~ (\x -> Just $ maybe [Translate x' (- y')] (<> [Translate x' (- y')]) x)
-- | A DrawAttributes to translate by a Point.
scaleDA :: (HasDrawAttributes s) => C.Point Double -> s -> s
scaleDA (C.Point x' y') =
transform
%~ (\x -> Just $ maybe [Scale x' (Just y')] (<> [Scale x' (Just y')]) x)
-- | convert a Rect to the svg co-ordinate system
rectSvg :: Rect Double -> Svg.Rectangle -> Svg.Rectangle
rectSvg (Rect x z y w) =
(rectUpperLeftCorner .~ (Num x, Num (- w)))
. (rectWidth .~ Just (Num (z - x)))
. (rectHeight .~ Just (Num (w - y)))
-- | import a Tree from a file
treeFromFile :: FilePath -> IO Tree
treeFromFile fp = do
t <- Svg.loadSvgFile fp
pure $ maybe Svg.None Re.unbox t