packages feed

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