diff --git a/README.md b/README.md index 177bb34..f7a969f 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,29 @@ scripting. In more practical terms, reanimate is a library for turning code like this: ```haskell -main = reanimate $ docEnv $ playThenReverseA $ mkAnimation duration $ \t -> - partialSvg t $ pathify $ mkCircle radius - where duration = 2; radius = screenHeight/3 +main = reanimate $ docEnv $ playThenReverseA drawCircle ``` ... into animations like this: -![Draw Circle](docs/gifs/doc_playThenReverseA.gif) +[![Draw Circle](https://i.imgur.com/C02hPw8.gif)](examples/doc_playThenReverseA.hs) +# What is reanimate good at? + +## Vector graphics and math +[![Tangent/Normal](https://i.imgur.com/w6gEkbl.gif)](examples/demo_tangent.hs) +[![Fourier](https://i.imgur.com/pX4YRa4.gif)](examples/tut_glue_fourier.hs) + +## Mapping and tracing +[![Geo JSON](https://i.imgur.com/OrKiOqF.gif)](videos/map-projection/gif.hs) +[![Object tracing](https://i.imgur.com/Y6NsPWF.gif)](examples/tut_glue_potrace.hs) + +## Mathematical typesetting +[![LaTeX](https://i.imgur.com/e6oO4wz.gif)](examples/tut_glue_latex.hs) + +## 2D physics and 3D graphics +[![2D Physics](https://i.imgur.com/ZHUfWdp.gif)](examples/tut_glue_physics.hs) +[![3D graphics](https://i.imgur.com/4wdtuJw.gif)](examples/tut_glue_povray.hs) # Prerequisites @@ -62,22 +76,6 @@ animation source code, the browser window will automatically reload and show the * Design overview: https://reanimate.readthedocs.io/en/latest/glue_tut/ * Gallery with source code: https://reanimate.readthedocs.io/en/latest/gallery/ -# Examples - -The example gifs are displayed at 25 fps. - -![Map projections](gifs/map.gif) -![LaTeX wheel](gifs/latex_wheel.gif) -![Sunflower](gifs/sunflower.gif) -![Tangent](gifs/tangent.gif) -![Goo](gifs/goo.gif) -![Drawing LaTeX equations](gifs/latex_draw.gif) -![Bounding boxes](gifs/bbox.gif) -![Colorful LaTeX](gifs/latex_color.gif) -![Bezier curves](gifs/bezier.gif) -![Valentine's Day](gifs/valentine.gif) -![Basic LaTeX](gifs/latex_basic.gif) - # Authors * David Himmelstrup. @@ -104,4 +102,4 @@ means. Completed animations are uploaded to the [Reanimated Science](https://www.youtube.com/channel/UCbZujyI7i6JbI-I0shPvDgg) channel. -Animation snippets are uploaded to the [Reanimated Science Playground](https://www.youtube.com/channel/UCL7MwXLtQbhJeb6Ts3_HooA) channel. +Animation snippets are uploaded to the [Reanimated Science Shorts](https://www.youtube.com/channel/UCL7MwXLtQbhJeb6Ts3_HooA) channel. diff --git a/examples/demo_tangent.hs b/examples/demo_tangent.hs new file mode 100755 index 0000000..ada6c79 --- /dev/null +++ b/examples/demo_tangent.hs @@ -0,0 +1,110 @@ +#!/usr/bin/env stack +-- stack runghc --package reanimate +{-# LANGUAGE OverloadedStrings #-} +module Main(main) where + +import Control.Lens ((^.)) +import Control.Monad.State +import qualified Data.Vector.Unboxed as V +import Geom2D.CubicBezier (AnyBezier (..), Point (..), + evalBezierDeriv) +import Graphics.SvgTree (Coord, Tree (..), mapTree, + pathDefinition) +import Linear.Metric +import Linear.V2 (V2 (..)) +import Linear.Vector +import Reanimate +import Reanimate.Builtin.Documentation + +main :: IO () +main = reanimate $ docEnv $ mkAnimation 30 $ \t -> + let piSvg = pathify $ lowerTransformations $ center $ scale 10 $ latex "s" in + mkGroup + [ mkBackgroundPixel rtfdBackgroundColor + , piSvg + , drawTangent t piSvg ] + +drawTangent :: Double -> SVG -> SVG +drawTangent alpha | alpha >= 1 = id +drawTangent alpha = mapTree worker + where + worker (PathTree path) = + let (V2 posX posY, tangent) = + atPartial alpha $ toLineCommands $ path^.pathDefinition + normed@(V2 tangentX tangentY) = normalize tangent ^* 4 + V2 midX midY = lerp 0.5 0 normed + V2 normVectX normVectY = normalize tangent ^* (svgWidth normalTxt*1.1) + tangentSvg = + translate (posX) (posY) $ + rotate (unangle normed/pi*180 + 180) $ + translate 0 (svgHeight tangentTxt/2) $ + tangentTxt + normalSvg = + translate (posX) (posY) $ + rotate (unangle normed/pi*180 + 90) $ + translate (svgWidth normalTxt/2*1.1) (svgHeight normalTxt/2*1.3) $ + normalTxt + in mkGroup + [ withStrokeWidth (defaultStrokeWidth) $ + withStrokeColor "black" $ + translate (posX-midX) (posY-midY) $ + mkLine (0, 0) (tangentX, tangentY) + , withStrokeWidth (defaultStrokeWidth) $ + withStrokeColor "black" $ + translate (posX) (posY) $ + mkLine (0, 0) (-normVectY, normVectX) + , withStrokeWidth (defaultStrokeWidth*2) $ + withStrokeColor "white" $ + tangentSvg + , withFillOpacity 1 $ withFillColor "black" $ withStrokeWidth 0 $ + tangentSvg + , withStrokeWidth (defaultStrokeWidth*2) $ + withStrokeColor "white" $ + normalSvg + , withFillOpacity 1 $ withFillColor "black" $ withStrokeWidth 0 $ + normalSvg + ] + worker t = t + tangentTxt = scale 1.1 $ center $ latex "tangent" + normalTxt = scale 1.1 $ center $ latex "normal" + +atPartial :: Double -> [LineCommand] -> (V2 Double, V2 Double) +atPartial alpha cmds = evalState (worker 0 cmds) zero + where + worker _d [] = pure (0, 0) + worker d (cmd:xs) = do + from <- get + len <- lineLength cmd + let frac = (targetLen-d) / len + if len == 0 || frac >= 1 + then worker (d+len) xs + else do + let bezier = lineCommandToBezier from cmd + (pos, tangent) = evalBezierDeriv bezier frac + pure $ (fromPoint pos, fromPoint tangent) + totalLen = evalState (sum <$> mapM lineLength cmds) zero + targetLen = totalLen * alpha + +lineCommandToBezier :: V2 Coord -> LineCommand -> AnyBezier Coord +lineCommandToBezier from line = + case line of + LineBezier [a] -> + AnyBezier $ V.fromList [toTuple from, toTuple a] + LineBezier [a,b] -> + AnyBezier $ V.fromList [toTuple from, toTuple a, toTuple b] + LineBezier [a,b,c] -> + AnyBezier $ V.fromList [toTuple from, toTuple a, toTuple b, toTuple c] + _ -> error (show line) + +fromPoint :: Point a -> V2 a +fromPoint (Point x y) = V2 x y + +toTuple :: V2 a -> (a,a) +toTuple (V2 x y) = (x, y) + +unangle :: (Floating a, Ord a) => V2 a -> a +unangle a@(V2 ax ay) = + let alpha = asin $ ay / norm a + in if ax < 0 + then pi - alpha + else alpha diff --git a/examples/tut_glue_fourier.hs b/examples/tut_glue_fourier.hs index 3dbe5b7..0b4f15b 100755 --- a/examples/tut_glue_fourier.hs +++ b/examples/tut_glue_fourier.hs @@ -12,8 +12,9 @@ import Codec.Picture -- layer 3 main :: IO () -main = reanimate $ parA bg $ sceneAnimation $ do - play $ fourierA (fromToS 0 15) -- Rotate 15 times +main = reanimate $ setDuration 30 $ sceneAnimation $ do + _ <- newSpriteSVG $ mkBackgroundPixel (PixelRGBA8 252 252 252 0xFF) + play $ fourierA (fromToS 0 5) -- Rotate 15 times # setDuration 50 # signalA (reverseS . powerS 2 . reverseS) -- Start fast, end slow # pauseAtEnd 2 @@ -22,8 +23,6 @@ main = reanimate $ parA bg $ sceneAnimation $ do # reverseA # signalA (powerS 2) -- Start slow, end fast # pauseAtEnd 2 - where - bg = animate $ const $ mkBackgroundPixel (PixelRGBA8 252 252 252 0xFF) -- layer 2 fourierA :: (Double -> Double) -> Animation diff --git a/examples/tut_glue_latex.hs b/examples/tut_glue_latex.hs index 9565b3e..427b80c 100755 --- a/examples/tut_glue_latex.hs +++ b/examples/tut_glue_latex.hs @@ -25,7 +25,7 @@ latexExample = sceneAnimation $ do -- Draw equation play $ drawAnimation strokedSvg sprites <- forM glyphs $ \(fn, _, elt) -> - newSpriteA $ animate $ const $ fn elt + newSpriteSVG $ fn elt -- Yoink each glyph forM_ (reverse sprites) $ \sprite -> do spriteE sprite (overBeginning 1 $ aroundCenterE $ highlightE) diff --git a/examples/tut_glue_potrace.hs b/examples/tut_glue_potrace.hs index 9baac61..b3433a9 100755 --- a/examples/tut_glue_potrace.hs +++ b/examples/tut_glue_potrace.hs @@ -18,13 +18,10 @@ main = reanimate $ parA bg $ sceneAnimation $ do xRot <- newVar (-45) yRot <- newVar 220 wf <- newSprite $ wireframe <$> unVar xRot <*> unVar yRot - tweenVar yRot spinDur (\t v -> fromToS v (v+60*3) $ curveS 2 (t/spinDur)) + fork $ tweenVar yRot spinDur $ \v -> fromToS v (v+60*3) . curveS 2 replicateM_ wobbles $ do - tweenVar xRot (wobbleDur/2) (\t v -> fromToS v (v+90) $ curveS 2 (t/(wobbleDur/2))) - fork $ do - wait (wobbleDur/2) - tweenVar xRot (wobbleDur/2) (\t v -> fromToS v (v-90) $ curveS 2 (t/(wobbleDur/2))) - wait wobbleDur + tweenVar xRot (wobbleDur/2) $ \v -> fromToS v (v+90) . curveS 2 + tweenVar xRot (wobbleDur/2) $ \v -> fromToS v (v-90) . curveS 2 destroySprite wf play $ mkAnimation drawDuration (\t -> partialSvg t (wireframe (-45) 220)) # reverseA diff --git a/examples/tut_glue_povray.hs b/examples/tut_glue_povray.hs index a3ff75d..5e11a39 100755 --- a/examples/tut_glue_povray.hs +++ b/examples/tut_glue_povray.hs @@ -22,7 +22,8 @@ import System.Random.Shuffle main :: IO () -main = reanimate $ parA bg $ sceneAnimation $ do +main = reanimate $ sceneAnimation $ do + newSpriteSVG $ mkBackgroundPixel $ PixelRGBA8 252 252 252 0xFF zPos <- newVar 0 xRot <- newVar 0 zRot <- newVar 0 @@ -41,8 +42,6 @@ main = reanimate $ parA bg $ sceneAnimation $ do fork $ tweenVar zRot 9 $ \v -> fromToS v 360 . curveS 2 wait 10 tweenVar zPos 2 $ \v -> fromToS v 0 . curveS 3 - where - bg = animate $ const $ mkBackgroundPixel $ PixelRGBA8 252 252 252 0xFF texture :: Double -> SVG texture t = frameAt (t*duration latexExample) latexExample @@ -92,7 +91,7 @@ latexExample = sceneAnimation $ do -- Draw equation play $ drawAnimation strokedSvg sprites <- forM glyphs $ \(fn, _, elt) -> - newSpriteA $ animate $ const $ fn elt + newSpriteSVG $ fn elt -- Yoink each glyph forM_ (reverse sprites) $ \sprite -> do spriteE sprite (overBeginning 1 $ aroundCenterE $ highlightE) diff --git a/misc/01_cube.py b/misc/01_cube.py deleted file mode 100644 index 37914b9..0000000 --- a/misc/01_cube.py +++ /dev/null @@ -1,53 +0,0 @@ -# blender --background --python 01_cube.py -- - -import bpy -import os -import sys -import math - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import utils - - -def get_output_file_path() -> str: - return str(sys.argv[sys.argv.index('--') + 1]) - - -def get_resolution_percentage() -> int: - return int(sys.argv[sys.argv.index('--') + 2]) - - -if __name__ == "__main__": - # Args - output_file_path = get_output_file_path() - resolution_percentage = get_resolution_percentage() - - # Setting - default_scene = bpy.context.scene - default_camera_object = bpy.data.objects["Camera"] - - bpy.ops.object.empty_add(location=(0.0, 0, 0.0)) - focus_target = bpy.context.object - bpy.ops.object.select_all(action='DESELECT') - default_camera_object.select_set(True) - focus_target.select_set(True) - bpy.ops.object.parent_set() - focus_target.rotation_mode = 'XYZ' - focus_target.rotation_euler = (0,0,0) - focus_target.keyframe_insert(data_path='rotation_euler', frame=1) - focus_target.rotation_euler = (0,0,math.pi*2) - focus_target.keyframe_insert(data_path='rotation_euler', frame=120) - - for k in focus_target.animation_data.action.fcurves.find('rotation_euler', index=2).keyframe_points: - k.interpolation = 'LINEAR' - - - num_samples = 32 - - utils.set_animation(default_scene, fps=60, frame_start=1, frame_end=120) - - utils.set_cycles_renderer(default_scene, resolution_percentage, output_file_path, default_camera_object, - num_samples, use_denoising=True) - - # Rendering - bpy.ops.render.render(animation=True, write_still=True) diff --git a/misc/bend.py b/misc/bend.py deleted file mode 100644 index 29616fd..0000000 --- a/misc/bend.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import math - -import bpy - -cam = bpy.data.objects['Camera'] -origin = bpy.data.objects['Cube'] - -bpy.ops.object.select_all(action='DESELECT') -origin.select_set(True) -bpy.ops.object.delete() - -#bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=4) -#bpy.ops.mesh.primitive_uv_sphere_add() -x = 0.9 -bpy.ops.mesh.primitive_plane_add() -plane = bpy.context.object -plane.scale = (1,1.778,1) -plane.scale = (1,2,1) -bpy.ops.object.shade_smooth() -modifier = plane.modifiers.new(name='Subsurf', type='SUBSURF') -modifier.levels = 5 -modifier.render_levels = 5 -modifier.subdivision_type = 'SIMPLE' - -bpy.ops.object.empty_add(type='ARROWS',rotation=(math.pi/2,0,0)) -empty = bpy.context.object - -bendUp = plane.modifiers.new(name='Bend up', type='SIMPLE_DEFORM') -bendUp.deform_method = 'BEND' -bendUp.origin = empty -bendUp.deform_axis = 'Z' -bendUp.factor = math.pi*x - -bendAround = plane.modifiers.new(name='Bend up', type='SIMPLE_DEFORM') -bendAround.deform_method = 'BEND' -bendAround.origin = empty -bendAround.deform_axis = 'X' -bendAround.factor = math.pi*2*x - -scn = bpy.context.scene -# scn.render.engine = 'CYCLES' -scn.render.film_transparent = True - -bpy.ops.render.render( write_still=True ) diff --git a/misc/test.py b/misc/test.py deleted file mode 100644 index 1289cc1..0000000 --- a/misc/test.py +++ /dev/null @@ -1,43 +0,0 @@ -import os - -import bpy - -cam = bpy.data.objects['Camera'] -origin = bpy.data.objects['Cube'] - -bpy.ops.object.select_all(action='DESELECT') -origin.select_set(True) -bpy.ops.object.delete() - -#bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=4) -bpy.ops.mesh.primitive_uv_sphere_add() -bpy.ops.object.shade_smooth() - -# bpy.data.materials.new('Prog mat') -bpy.context.object.active_material = bpy.data.materials['Material'] -mat = bpy.context.object.active_material -image_node = mat.node_tree.nodes.new('ShaderNodeTexImage') -texture = mat.node_tree.nodes['Principled BSDF'] -texture.inputs['Roughness'].default_value = 1 -mat.node_tree.links.new(image_node.outputs['Color'], texture.inputs['Base Color']) - -bpy.ops.image.open(filepath='/home/lemmih/Downloads/earth.jpg') -image_node.image = bpy.data.images['earth.jpg'] - - - -# image_node.image = bpy.data.images['earth.jpg'] -# bpy.ops.image.open(filepath='/home/lemmih/Downloads/earth.jpg') -# image_node = mat.node_tree.nodes.new('ShaderNodeTexImage') -# image_node = mat.node_tree.nodes[1] -# texture = mat.node_tree.nodes[2] -# mat.node_tree.links.new(image_node.outputs[0], texture.inputs[0]) -# bpy.data.materials.new('Prog mat') -# bpy.context.object.active_material = bpy.data.materials['Prog mat'] - -scn = bpy.context.scene -# scn.render.engine = 'CYCLES' -scn.render.film_transparent = True - -bpy.data.scenes["Scene"].render.filepath = '/tmp/blender.png' -bpy.ops.render.render( write_still=True ) diff --git a/reanimate.cabal b/reanimate.cabal index 979a761..5003632 100644 --- a/reanimate.cabal +++ b/reanimate.cabal @@ -91,7 +91,7 @@ library JuicyPixels, attoparsec, parallel, cubicbezier, websockets, hashable, fsnotify, open-browser, random-shuffle, base64-bytestring, - vector, colour, cassava, ansi-wl-pprint, here, temporary, + vector >= 0.12.0.0, colour, cassava, ansi-wl-pprint, here, temporary, optparse-applicative, chiphunk >= 0.1.2.1, geojson, aeson >= 1.3.0.0 ghc-options: -Wall diff --git a/src/Reanimate/Builtin/Documentation.hs b/src/Reanimate/Builtin/Documentation.hs index 8946e40..d2793c9 100644 --- a/src/Reanimate/Builtin/Documentation.hs +++ b/src/Reanimate/Builtin/Documentation.hs @@ -48,3 +48,6 @@ showColorMap f = center $ scaleToSize screenWidth screenHeight $ embedImage img height = 1 img = generateImage pixelRenderer width height pixelRenderer x _y = f (fromIntegral x / fromIntegral (width-1)) + +rtfdBackgroundColor :: PixelRGBA8 +rtfdBackgroundColor = PixelRGBA8 252 252 252 0xFF diff --git a/stack.yaml b/stack.yaml index 4b92e50..bc05ef6 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,4 +1,4 @@ -resolver: lts-13.14 +resolver: lts-13.19 allow-newer: false diff --git a/stack.yaml.lock b/stack.yaml.lock index 9172026..de2ee5f 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -41,7 +41,7 @@ packages: hackage: matrices-0.5.0@sha256:b2761813f6a61c84224559619cc60a16a858ac671c8436bbac8ec89e85473058 snapshots: - completed: - size: 497078 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/13/14.yaml - sha256: 9e5ba48e5188aeb52bb0df9bc65dbd858a12c33f5b8d495a3eb588dc9ffcb15a - original: lts-13.14 + size: 498155 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/13/19.yaml + sha256: b9367a80d4393d02e58a46b8a9fdfbd7bc19f59c0c2bbf90034ba15cf52cf213 + original: lts-13.19 diff --git a/videos/map-projection/gif.hs b/videos/map-projection/gif.hs index b638840..e12030f 100644 --- a/videos/map-projection/gif.hs +++ b/videos/map-projection/gif.hs @@ -14,6 +14,7 @@ import Reanimate import Reanimate.Animation import Reanimate.Scene import Reanimate.GeoProjection +import Reanimate.Builtin.Documentation import System.IO.Unsafe import Data.Geospatial hiding (LonLat) import Data.LinearRing @@ -27,8 +28,8 @@ import Control.Lens ((^.)) main :: IO () -main = seq equirectangular $ reanimate $ sceneAnimation $ do - newSpriteSVG $ mkBackground "white" +main = reanimate $ sceneAnimation $ do + newSpriteSVG $ mkBackgroundPixel rtfdBackgroundColor prevProj <- newVar equirectangularP let push label proj = do prev <- readVar prevProj @@ -43,24 +44,24 @@ main = seq equirectangular $ reanimate $ sceneAnimation $ do -- [ grid equirectangularP ] -- push "Lambert" lambertP - push "Web Mercator" mercatorP + --push "Web Mercator" mercatorP push "Mollweide" mollweideP - -- push "Bottomley 30\\degree" (bottomleyP (toRads 30)) + push "Bottomley 30\\degree" (bottomleyP (toRads 30)) -- 4 - -- pushInterp "Werner" wernerP + push "Werner" wernerP -- 5 - -- pushInterp "Bonne 45\\degree" (bonneP (toRads 45)) + -- push "Bonne 45\\degree" (bonneP (toRads 45)) -- pushT -- (\t -> "Bonne " <> T.pack (show $ round $ fromToS 45 0 t) <> "\\degree") -- (bonneP . toRads . fromToS 45 0) -- 6 - -- pushInterp "Eckert I" eckert1P - -- pushInterp "Eckert III" eckert3P - -- pushInterp "Eckert IV" eckert5P + -- push "Eckert I" eckert1P + -- push "Eckert III" eckert3P + -- push "Eckert IV" eckert5P -- 7 -- push "Fahey" faheyP -- 8 - push "August" augustP + -- push "August" augustP -- 9 push "Foucaut" foucautP -- 10 @@ -70,17 +71,9 @@ main = seq equirectangular $ reanimate $ sceneAnimation $ do play $ signalA (curveS 2) $ mkAnimation morphT $ grid . mergeP prev equirectangularP where - src = equirectangular waitT = 0 morphT = 1 -equirectangular :: Image PixelRGB8 -equirectangular = unsafePerformIO $ do - dat <- BS.readFile "earth.jpg" - case decodeJpeg dat of - Left err -> error err - Right img -> return $ convertRGB8 img - toRads :: Double -> Double toRads dec = dec/180 * pi