Add basic support for bounding boxes.

This commit is contained in:
David 2019-02-18 16:00:03 +01:00
commit b8e7f0cb06
6 changed files with 337 additions and 108 deletions

View file

@ -10,7 +10,13 @@ import Data.Monoid ((<>))
import Data.Text (Text, pack)
import qualified Graphics.Svg as S
import Linear.V2
import Lucid.Svg
import Lucid.Svg (Svg, circle_, clip_path_, cx_, cy_, d_, id_, defs_, clipPath_,
fill_, fill_opacity_, font_size_, g_,
height_, line_, opacity_, path_, r_,
rect_, stroke_, stroke_width_, text_,
text_anchor_, toHtml, transform_,
width_, x1_, x2_, x_, y1_, y2_, y_)
import qualified Lucid.Svg as Lucid
import Numeric
import Text.Printf
@ -19,6 +25,8 @@ import Reanimate.Combinators
import Reanimate.LaTeX
import Reanimate.Svg
import Debug.Trace
sinewave :: Ani ()
sinewave = proc () -> do
duration 10 -< ()
@ -28,7 +36,7 @@ sinewave = proc () -> do
emit -< do
defs_ $ clipPath_ [id_ "clip"] $
rect_ [x_ "0", num_ y_ (-height), num_ width_ (idx * width), height_ "100%"]
g_ [transform_ $ translate margin height, clip_path_ "url(#clip)"] $
g_ [transform_ $ Lucid.translate margin height, clip_path_ "url(#clip)"] $
renderPath $ approxFnData 1000 wave
line_ [ num_ x1_ margin, num_ x2_ margin, y1_ "10", y2_ "170"
, stroke_ "white"]
@ -36,7 +44,7 @@ sinewave = proc () -> do
, stroke_ "white"]
let (circX, circY) = wave idx
emit -< g_ [transform_ $ translate margin height] $
emit -< g_ [transform_ $ Lucid.translate margin height] $
circle_ [num_ cx_ circX, num_ cy_ circY, r_ "3", fill_ "red"]
where
freq = 3; margin = 30; width = 260; height = 90
@ -49,9 +57,9 @@ morph_wave = proc () -> do
morph <- signalOscillate 0 1 -< ()
emit -< do
g_ [transform_ $ translate 30 50] $ renderPath wave1
g_ [transform_ $ translate 30 130] $ renderPath wave2
g_ [transform_ $ translate 30 90] $ renderPath $ morphPath wave1 wave2 morph
g_ [transform_ $ Lucid.translate 30 50] $ renderPath wave1
g_ [transform_ $ Lucid.translate 30 130] $ renderPath wave2
g_ [transform_ $ Lucid.translate 30 90] $ renderPath $ morphPath wave1 wave2 morph
line_ [x1_ "30", x2_ "30", y1_ "10", y2_ "170", stroke_ "white"]
line_ [x1_ "30", x2_ "290", y1_ "90", y2_ "90", stroke_ "white"]
where
@ -66,7 +74,7 @@ morph_wave_circle = proc t -> do
idx <- signalOscillate 0 1 -< ()
emit -< do
g_ [transform_ $ translate 30 90] $
g_ [transform_ $ Lucid.translate 30 90] $
renderPath $ morphPath circle wave1 idx
line_ [x1_ "30", x2_ "30", y1_ "10", y2_ "170", stroke_ "white"]
line_ [x1_ "30", x2_ "290", y1_ "90", y2_ "90", stroke_ "white"]
@ -79,9 +87,9 @@ morph_wave_circle = proc t -> do
progressMeters :: Ani ()
progressMeters = proc () -> do
emit -< rect_ [width_ "100%", height_ "100%", fill_ "black"]
annotate' (adjustSpeed 1.0 progressMeter) -< g_ [transform_ $ translate 40 20]
annotate' (adjustSpeed 2.0 progressMeter) -< g_ [transform_ $ translate 140 20]
annotate' (adjustSpeed 0.5 progressMeter) -< g_ [transform_ $ translate 240 20]
annotate' (adjustSpeed 1.0 progressMeter) -< g_ [transform_ $ Lucid.translate 40 20]
annotate' (adjustSpeed 2.0 progressMeter) -< g_ [transform_ $ Lucid.translate 140 20]
annotate' (adjustSpeed 0.5 progressMeter) -< g_ [transform_ $ Lucid.translate 240 20]
emit -< do
text_ [x_ "55", y_ "150", font_size_ "20"
@ -164,10 +172,10 @@ clip_rect = proc () -> do
where
paintStatic nth = proc () ->
annotate' (obj "white" (20+nth*10) (20+nth*10))
-< g_ [transform_ $ translate 160 90]
-< g_ [transform_ $ Lucid.translate 160 90]
runAni color nth = defineAnimation $ proc () ->
annotate' (circle_clip (obj color (20+nth*10) (20+nth*10)))
-< g_ [transform_ $ translate 160 90]
-< g_ [transform_ $ Lucid.translate 160 90]
obj c width height = proc () -> do
duration 1 -< ()
emit -< rect_ [ num_ width_ width, num_ height_ height
@ -195,7 +203,7 @@ circle_clip sub = proc () -> do
scaling :: Ani ()
scaling = adjustSpeed 2 $ syncAll
[ defineAnimation $ proc () ->
annotate' animation -< g_ [transform_ $ translate x y <> " " <> scale 0.5 0.5]
annotate' animation -< g_ [transform_ $ Lucid.translate x y <> " " <> Lucid.scale 0.5 0.5]
| x <- [0,160]
, y <- [0,90]
| animation <- [sinewave, morph_wave, highlight, progressMeters]]
@ -210,7 +218,7 @@ heart :: Ani ()
heart = proc () -> do
emit -< rect_ [width_ "100%", height_ "100%", fill_ "#FFFFFF"]
-- duration 1 -< ()
-- annotate' drawHeart -< g_ [transform_ $ scale 0.5 0.5 <> " " <> translate 200 200]
-- annotate' drawHeart -< g_ [transform_ $ Lucid.scale 0.5 0.5 <> " " <> translate 200 200]
emit -< rect_ [width_ "100%", height_ "100%", fill_ "black"]
follow
[ all_read
@ -254,11 +262,11 @@ heart = proc () -> do
heart_ani = repeatAni 10 $ defineAnimation $ proc () -> do
duration 1 -< ()
n <- signalOscillateSCurve 2 0.9 1.1 -< ()
annotate' drawHeart -< g_ [transform_ $ translate 160 110] . g_ [transform_ $ scale n n <> " "]
annotate' drawHeart -< g_ [transform_ $ Lucid.translate 160 110] . g_ [transform_ $ Lucid.scale n n <> " "]
heart_disappear = defineAnimation $ proc () -> do
duration 3 -< ()
n <- signal 0.9 10 -< ()
annotate' drawHeart -< g_ [transform_ $ translate 160 110] . g_ [transform_ $ scale n n <> " "]
annotate' drawHeart -< g_ [transform_ $ Lucid.translate 160 110] . g_ [transform_ $ Lucid.scale n n <> " "]
white = loop $ defineAnimation $ proc () -> do
duration 1 -< ()
emit -< rect_ [width_ "100%", height_ "100%", fill_ "#FFFFFF"]
@ -267,8 +275,8 @@ heart = proc () -> do
n <- signal 0 1 -< ()
o <- signalOscillate (-1) 1 -< ()
emit -<
g_ [transform_ $ translate (xPos*360) (210*n)] $
g_ [transform_ $ rotate (45*o)] $
g_ [transform_ $ Lucid.translate (xPos*360) (210*n)] $
g_ [transform_ $ Lucid.rotate (45*o)] $
text_ [font_size_ "18"
,text_anchor_ "middle"
,fill_ "red"] ""
@ -277,16 +285,16 @@ heart = proc () -> do
o <- signalOscillate 0 1 -< ()
n <- signalOscillateSCurve 2 0.9 1.1 -< ()
emit -<
g_ [transform_ $ translate 160 110, num_ opacity_ o] $
g_ [transform_ $ scale n n ] $
g_ [transform_ $ Lucid.translate 160 110, num_ opacity_ o] $
g_ [transform_ $ Lucid.scale n n ] $
text_ [x_ "0", y_ "-12", font_size_ "24"
, text_anchor_ "middle"
, fill_ "white"] txt
drawHeart = proc () -> do
emit -<
g_ [transform_ $ translate (-170) (-260)] $
g_ [transform_ $ rotateAround 225 150 121 <> " " <> scale 0.4 0.4] $
g_ [transform_ $ Lucid.translate (-170) (-260)] $
g_ [transform_ $ Lucid.rotateAround 225 150 121 <> " " <> Lucid.scale 0.4 0.4] $
path_ ([stroke_ "red", fill_"red", d_ dat])
dat = "M0 200 v-200 h200 a100,100 90 0,1 0,200 a100,100 90 0,1 -200,0 z"
hex n = if n < 0x10 then "0" ++ showHex (round n) ""
@ -318,14 +326,14 @@ frequencies = proc () -> do
duration 3 -< ()
n <- signal 0 1 -< ()
emit -< do
g_ [transform_ $ translate margin height] $ renderPath $ morphPath line1 (wave1 move) n
g_ [transform_ $ Lucid.translate margin height] $ renderPath $ morphPath line1 (wave1 move) n
let circleY = sum [ sin ((1+move)*pi*2*freq) * 20 | freq <- freqs ]
circle_ [num_ cx_ (width+margin), num_ cy_ (height+circleY*n), num_ r_ 3, fill_ "red"]
drawSecondWave = defineAnimation $ proc move -> do
label "drawSecondWave" -< ()
duration 3 -< ()
emit -< do
g_ [transform_ $ translate margin height] $ renderPath $ wave1 move
g_ [transform_ $ Lucid.translate margin height] $ renderPath $ wave1 move
let circleY = sum [ sin ((1+move)*pi*2*freq) * 20 | freq <- freqs ]
circle_ [num_ cx_ (width+margin), num_ cy_ (height+circleY), num_ r_ 3, fill_ "red"]
drawUpWave = defineAnimation $ proc move -> do
@ -333,8 +341,8 @@ frequencies = proc () -> do
duration 2 -< ()
n <- signal 0 1 -< ()
emit -< do
g_ [transform_ $ scale 1 (1-0.5*n)] $ do
g_ [transform_ $ translate margin height] $ renderPath $ wave1 move
g_ [transform_ $ Lucid.scale 1 (1-0.5*n)] $ do
g_ [transform_ $ Lucid.translate margin height] $ renderPath $ wave1 move
let circleY = sum [ sin ((1+move)*pi*2*freq) * 20 | freq <- freqs ]
circle_ [num_ cx_ (width+margin), num_ cy_ (height+circleY), num_ r_ 3, fill_ "red"]
line1 = approxFnData 1000 $ \idx ->
@ -349,7 +357,7 @@ latex_basic = proc () -> do
s <- signalOscillate 0 1 -< ()
emit -< rect_ [width_ "100%", height_ "100%", fill_ "black"]
emit -<
g_ [transform_ $ translate 20 15 <> " " <> scale 4 4] $ do
g_ [transform_ $ Lucid.translate 20 15 <> " " <> Lucid.scale 4 4] $ do
g_ [stroke_ "white", fill_opacity_ "0", stroke_width_ "0.1"] text
g_ [fill_ "white", num_ fill_opacity_ s] text
where
@ -422,7 +430,7 @@ latex_draw = pauseAtEnd 1 $ defineAnimation $ proc () -> do
drawText msg `andThen` fillText msg -< ()
where
msg = "\\sum_{k=1}^\\infty {1 \\over k^2} = {\\pi^2 \\over 6}"
placement = g_ [transform_ $ translate 20 15 <> " " <> scale 5 5]
placement = g_ [transform_ $ Lucid.translate 20 15 <> " " <> Lucid.scale 5 5]
fillText txt = defineAnimation $ proc () -> do
duration 1 -< ()
s <- signal 0 1 -< ()
@ -435,3 +443,49 @@ latex_draw = pauseAtEnd 1 $ defineAnimation $ proc () -> do
emit -< placement $
g_ [stroke_ "white", fill_opacity_ "0", stroke_width_ "0.1"] $
toHtml $ partialSvg s $ latexAlign txt
bbox :: Ani ()
bbox = proc () -> do
emit -< rect_ [width_ "100%", height_ "100%", fill_ "black"]
annotate' bbox1 -< g_ [transform_ $ Lucid.translate (320/2-50) (180/2)]
annotate' bbox2 -< g_ [transform_ $ Lucid.translate (320/2+50) (180/2)]
bbox1 :: Ani ()
bbox1 = defineAnimation $ proc () -> do
duration 5 -< ()
s <- signal 0 1 -< ()
let rotated = rotate (360*s) svg
(x, y, w, h) = boundingBox rotated
emit -< do
g_ [transform_ $ Lucid.translate x y] $
rect_ [num_ width_ w, num_ height_ h, stroke_ "red", fill_opacity_ "0", stroke_width_ "1"]
g_ [fill_ "white"] $ toHtml rotated
where
msg = "\\sum_{k=1}^\\infty"
svg = scale 3 $ center $ latexAlign msg
bbox2 :: Ani ()
bbox2 = defineAnimation $ proc () -> do
duration 5 -< ()
s <- signalOscillate 0 1 -< ()
let rotated = partialSvg s heartShape
(x, y, w, h) = boundingBox rotated
emit -< do
g_ [transform_ $ Lucid.translate x y] $
rect_ [num_ width_ w, num_ height_ h, stroke_ "red", fill_opacity_ "0", stroke_width_ "1"]
g_ [fill_ "white", fill_opacity_ "0", stroke_width_ "4", stroke_ "white"] $
toHtml rotated
heartShape =
scale 0.15 $ rotate 225 $ center $ p
where
p = S.PathTree $ S.defaultSvg & S.pathDefinition .~ cmds
abs = S.OriginAbsolute
rel = S.OriginRelative
cmds =
[S.MoveTo abs [V2 0 200]
,S.VerticalTo rel [-200],S.HorizontalTo rel [200]
,S.EllipticalArc rel [(100,100,90,False,True, V2 0 200)]
,S.EllipticalArc rel [(100,100,90,False,True, V2 (-200) 0)]
,S.EndPath]

View file

@ -10,6 +10,7 @@ import Graphics.Svg
import Linear.Metric
import Linear.V2
import Linear.Vector
import qualified Reanimate.Transform as Transform
import Debug.Trace
@ -101,10 +102,10 @@ adjustLineLength alpha from cmd =
lineLength :: LineCommand -> CmdM Double
lineLength cmd =
case cmd of
LineMove to -> pure 0 <* put to
LineDraw to -> gets (distance to) <* put to
LineBezier points -> gets (distance (last points)) <* put (last points)
case cmd of
LineMove to -> pure 0 <* put to
LineDraw to -> gets (distance to) <* put to
LineBezier points -> gets (distance (last points)) <* put (last points)
toLineCommands :: [PathCommand] -> [LineCommand]
toLineCommands ps = evalState (worker zero Nothing ps) zero
@ -161,7 +162,10 @@ toLineCommand startPos mbPrevControlPt cmd = do
from <- get <* adjustPosition o to
let c1 = maybe from (mirrorPoint from) mbControl
pure $ LineBezier [c1,makeAbsolute o from to]
EllipticalArc origin points -> undefined
EllipticalArc o points -> concat <$>
(forM points $ \(rotX, rotY, angle, largeArc, sweepFlag, to) -> do
from <- get <* adjustPosition o to
return $ convertSvgArc from rotX rotY angle largeArc sweepFlag (makeAbsolute o from to))
EndPath -> put startPos *> pure [LineDraw startPos]
where
mirrorPoint c p = c*2-p
@ -171,6 +175,82 @@ toLineCommand startPos mbPrevControlPt cmd = do
makeAbsolute OriginRelative from p = from+p
calculateVectorAngle :: Double -> Double -> Double -> Double -> Double
calculateVectorAngle ux uy vx vy
| tb >= ta
= tb - ta
| otherwise
= pi * 2 - (ta - tb)
where
ta = atan2 uy ux
tb = atan2 vy vx
-- ported from: https://github.com/vvvv/SVG/blob/master/Source/Paths/SvgArcSegment.cs
convertSvgArc :: RPoint -> Coord -> Coord -> Coord -> Bool -> Bool -> RPoint -> [LineCommand]
convertSvgArc (V2 x0 y0) radiusX radiusY angle largeArcFlag sweepFlag (V2 x y)
| x0 == x && y0 == y
= []
| radiusX == 0.0 && radiusY == 0.0
= [LineDraw (V2 x y)]
| otherwise
= calcSegments x0 y0 theta1' segments'
where
sinPhi = sin (angle * pi/180)
cosPhi = cos (angle * pi/180)
x1dash = cosPhi * (x0 - x) / 2.0 + sinPhi * (y0 - y) / 2.0
y1dash = -sinPhi * (x0 - x) / 2.0 + cosPhi * (y0 - y) / 2.0
numerator = radiusX * radiusX * radiusY * radiusY - radiusX * radiusX * y1dash * y1dash - radiusY * radiusY * x1dash * x1dash
s = sqrt(1.0 - numerator / (radiusX * radiusX * radiusY * radiusY))
rx = if (numerator < 0.0) then (radiusX * s) else radiusX
ry = if (numerator < 0.0) then (radiusY * s) else radiusY
root = if (numerator < 0.0)
then (0.0)
else ((if ((largeArcFlag && sweepFlag) || (not largeArcFlag && not sweepFlag)) then (-1.0) else 1.0) *
sqrt(numerator / (radiusX * radiusX * y1dash * y1dash + radiusY * radiusY * x1dash * x1dash)))
cxdash = root * rx * y1dash / ry
cydash = -root * ry * x1dash / rx
cx = cosPhi * cxdash - sinPhi * cydash + (x0 + x) / 2.0
cy = sinPhi * cxdash + cosPhi * cydash + (y0 + y) / 2.0
theta1' = calculateVectorAngle 1.0 0.0 ((x1dash - cxdash) / rx) ((y1dash - cydash) / ry)
dtheta' = calculateVectorAngle ((x1dash - cxdash) / rx) ((y1dash - cydash) / ry) ((-x1dash - cxdash) / rx) ((-y1dash - cydash) / ry)
dtheta = if (not sweepFlag && dtheta' > 0)
then (dtheta' - 2 * pi)
else (if (sweepFlag && dtheta' < 0) then (dtheta' + 2 * pi) else dtheta')
segments' = ceiling (abs (dtheta / (pi / 2.0)))
delta = dtheta / fromInteger segments'
t = 8.0 / 3.0 * sin(delta / 4.0) * sin(delta / 4.0) / sin(delta / 2.0)
calcSegments startX startY theta1 segments
| segments == 0
= []
| otherwise
= LineBezier [ V2 (startX + dx1) (startY + dy1)
, V2 (endpointX + dxe) (endpointY + dye)
, V2 endpointX endpointY ] : calcSegments endpointX endpointY theta2 (segments - 1)
where
cosTheta1 = cos theta1
sinTheta1 = sin theta1
theta2 = theta1 + delta
cosTheta2 = cos theta2
sinTheta2 = sin theta2
endpointX = cosPhi * rx * cosTheta2 - sinPhi * ry * sinTheta2 + cx
endpointY = sinPhi * rx * cosTheta2 + cosPhi * ry * sinTheta2 + cy
dx1 = t * (-cosPhi * rx * sinTheta1 - sinPhi * ry * cosTheta1)
dy1 = t * (-sinPhi * rx * sinTheta1 + cosPhi * ry * cosTheta1)
dxe = t * (cosPhi * rx * sinTheta2 + sinPhi * ry * cosTheta2)
dye = t * (sinPhi * rx * sinTheta2 - cosPhi * ry * cosTheta2)
-- Algorithm taken from manim. It's magic.
bezier :: [RPoint] -> Double -> RPoint
bezier points t = sum
@ -179,6 +259,8 @@ bezier points t = sum
where
n = length points -1
choose n k = product [n,n-1 .. n-k+1] `div` product [1..k]
partial_bezier_points :: [RPoint] -> Double -> Double -> [RPoint]
partial_bezier_points points a b
| isNaN end_prop || isInfinite end_prop = replicate (length points) (last points)
| otherwise = [ bezier (take (i+1) a_to_1) end_prop | i <- [0..length points-1] ]
@ -190,78 +272,6 @@ partial_bezier_points points a b
interpolatePathCommands :: Double -> [PathCommand] -> [PathCommand]
interpolatePathCommands alpha = lineToPath . partialLine alpha . toLineCommands
-- evalState (worker 0 cmds) zero
-- where
-- worker d [] = pure []
-- worker d (x:xs) | d > minDistance = pure []
-- worker d (LineTo OriginAbsolute []:xs) = worker d xs
-- worker d (LineTo OriginAbsolute (to:tos):xs) = do
-- from <- get <* put to
-- let d' = distance from to
-- out = LineTo OriginAbsolute [lerp (min 1 ((minDistance-d)/d')) to from]
-- rest = LineTo OriginAbsolute tos
-- (out:) <$> worker (d+d') (rest:xs)
-- worker d (LineTo OriginRelative []:xs) = worker d xs
-- worker d (LineTo OriginRelative (to:tos):xs) = do
-- from <- get <* modify (+to)
-- let d' = distance zero to
-- out = LineTo OriginRelative [lerp (min 1 ((minDistance-d)/d')) to zero]
-- rest = LineTo OriginRelative tos
-- (out:) <$> worker (d+d') (rest:xs)
-- worker d (x:xs) = do
-- d' <- estimateCmdLength x
-- (x:) <$> worker (d+d') xs
-- minDistance = estimatePathLength cmds * alpha
--
-- splitPathCommands :: [PathCommand] -> [PathCommand]
-- splitPathCommands cmds = evalState (concat <$> mapM splitPathCommand cmds) zero
--
-- splitPathCommand :: PathCommand -> CmdM [PathCommand]
-- splitPathCommand cmd = do
-- case cmd of
-- MoveTo OriginAbsolute [] -> pure [cmd]
-- MoveTo OriginAbsolute lst -> put (last lst) *> pure [cmd]
-- MoveTo OriginRelative lst -> modify (+ sum lst) *> pure [cmd]
-- -- LineTo OriginAbsolute lst -> do
-- -- concat <$> forM lst (\to -> do
-- -- from <- get <* put (to :: RPoint)
-- -- let towardsTo = (to - from) / fromIntegral pieces :: RPoint
-- -- return $ replicate pieces (LineTo OriginRelative [towardsTo]))
-- -- LineTo OriginRelative lst -> do
-- -- concat <$> forM lst (\to -> do
-- -- from <- get <* modify (+to)
-- -- let towardsTo = to / fromIntegral pieces :: RPoint
-- -- return $ replicate pieces (LineTo OriginRelative [towardsTo]))
-- _ -> pure [cmd]
-- where
-- pieces = 100 :: Int
--
-- estimatePathLength :: [PathCommand] -> Double
-- estimatePathLength cmds = evalState (sum <$> mapM estimateCmdLength cmds) zero
--
-- estimateCmdLength :: PathCommand -> CmdM Double
-- estimateCmdLength cmd =
-- case cmd of
-- MoveTo OriginAbsolute [] -> pure 0
-- MoveTo OriginAbsolute lst -> put (last lst) *> pure 0
-- MoveTo OriginRelative lst -> modify (+ sum lst) *> pure 0
-- LineTo OriginAbsolute lst ->
-- sum <$> forM lst (\to -> gets (distance to) <* put to)
-- LineTo OriginRelative lst ->
-- sum <$> forM lst (\to -> modify (+to) *> pure (distance zero to))
-- HorizontalTo OriginAbsolute lst -> undefined
-- HorizontalTo OriginRelative lst -> undefined
-- VerticalTo OriginAbsolute lst -> undefined
-- VerticalTo OriginRelative lst -> undefined
-- CurveTo origin quads -> undefined
-- SmoothCurveTo origin lst -> undefined
-- QuadraticBezier origin pairs -> undefined
-- SmoothQuadraticBezierCurveTo origin points -> undefined
-- EllipticalArc origin points -> undefined
-- EndPath -> pure 0
-- lineToPath :: [LineCommand] -> [PathCommand]
-- partialLine :: Double -> [LineCommand] -> [LineCommand]
partialSvg :: Double -> Tree -> Tree
partialSvg alpha = mapTree worker
@ -269,3 +279,74 @@ partialSvg alpha = mapTree worker
worker (PathTree path) =
PathTree $ path & pathDefinition %~ lineToPath . partialLine alpha . toLineCommands
worker t = t
-- (x,y,w,h)
boundingBox :: Tree -> (Double, Double, Double, Double)
boundingBox t =
case svgBoundingPoints t of
[] -> (0,0,0,0)
(V2 x y:rest) ->
let (minx, miny, maxx, maxy) = foldl' worker (x, y, x, y) rest
in (minx, miny, maxx-minx, maxy-miny)
where
worker (minx, miny, maxx, maxy) (V2 x y) =
(min minx x, min miny y, max maxx x, max maxy y)
linePoints :: [LineCommand] -> [RPoint]
linePoints = worker zero
where
worker from [] = []
worker from (x:xs) =
case x of
LineMove to -> worker to xs
LineDraw to -> from:to:worker to xs
LineBezier ctrl -> -- approximation
[ last (partial_bezier_points (from:ctrl) 0 (recip chunks*i)) | i <- [0..chunks]] ++
worker (last ctrl) xs
chunks = 10
svgBoundingPoints :: Tree -> [RPoint]
svgBoundingPoints t = map (Transform.transformPoint m) $
case t of
None -> []
UseTree{} -> []
GroupTree g -> concatMap svgBoundingPoints (g^.groupChildren)
SymbolTree (Symbol g) -> concatMap svgBoundingPoints (g^.groupChildren)
PathTree p -> linePoints $ toLineCommands (p^.pathDefinition)
CircleTree{} -> error "CircleTree"
PolyLineTree{} -> error "PolyLineTree"
EllipseTree{} -> error "EllipseTree"
LineTree{} -> error "LineTree"
RectangleTree{} -> error "RectangleTree"
TextTree{} -> []
ImageTree{} -> []
MeshGradientTree{} -> []
where
m = Transform.mkMatrix (t^.drawAttr.transform)
withTransformations :: [Transformation] -> Tree -> Tree
withTransformations transformations tree = GroupTree $ defaultSvg
& drawAttr .~ attr
& groupChildren .~ [tree]
where
attr = defaultSvg & transform .~ Just transformations
translate :: Double -> Double -> Tree -> Tree
translate x y = withTransformations [Translate x y]
rotate :: Double -> Tree -> Tree
rotate a = withTransformations [Rotate a Nothing]
rotateAround :: Double -> RPoint -> Tree -> Tree
rotateAround a (V2 x y) = withTransformations [Rotate a (Just (x,y))]
scale :: Double -> Tree -> Tree
scale a = withTransformations [Scale a Nothing]
scaleXY :: Double -> Double -> Tree -> Tree
scaleXY x y = withTransformations [Scale x (Just y)]
center :: Tree -> Tree
center t = translate (-x-w/2) (-y-h/2) t
where
(x, y, w, h) = boundingBox t

View file

@ -0,0 +1,47 @@
module Reanimate.Transform
( identity
, transformPoint
, mkMatrix
) where
-- XXX: Use Linear.Matrix instead of Data.Matrix to drop the 'matrix' dependency.
import Data.List
import Data.Matrix (Matrix)
import qualified Data.Matrix as M
import Data.Maybe
import Graphics.Svg
import Linear.V2
type TMatrix = Matrix Coord
identity :: TMatrix
identity = M.identity 3
fromList :: [Coord] -> TMatrix
fromList [a,b,c,d,e,f] = M.fromList 3 3 [a,c,e,b,d,f,0,0,1]
fromList _ = error "Reanimate.Transform.fromList: bad input"
transformPoint :: TMatrix -> RPoint -> RPoint
transformPoint m (V2 x y) = V2 (a*x +c*y + e) (b*x + d*y +f)
where
(a:c:e:b:d:f:_) = M.toList m
mkMatrix :: Maybe [Transformation] -> TMatrix
mkMatrix Nothing = identity
mkMatrix (Just ts) = foldl' (*) identity (map transformationMatrix ts)
transformationMatrix :: Transformation -> TMatrix
transformationMatrix transformation =
case transformation of
TransformMatrix a b c d e f -> fromList [a,b,c,d,e,f]
Translate x y -> translate x y
Scale sx mbSy -> fromList [sx,0,0,fromMaybe sx mbSy,0,0]
Rotate a Nothing -> rotate a
Rotate a (Just (x,y)) -> translate x y * rotate a * translate (-x) (-y)
SkewX a -> fromList [1,0,tan (a*pi/180),1,0,0]
SkewY a -> fromList [1,tan (a*pi/180),0,1,0,0]
TransformUnknown -> identity
where
translate x y = fromList [1,0,0,1,x,y]
rotate a = fromList [cos r,sin r,-sin r,cos r,0,0]
where r = a * pi / 180