diff --git a/docs/glue_tut.md b/docs/glue_tut.md index 0f1134b..55314b2 100644 --- a/docs/glue_tut.md +++ b/docs/glue_tut.md @@ -2,13 +2,13 @@ Reanimate is a library for programmatically generating animations with a twist towards mathematics / vector drawings. A lot of inspiration was drawn from -3b1b's manim library. +[3b1b's](https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw) [manim](https://github.com/3b1b/manim) library. Reanimate aims at being a batteries-included way of gluing together different technologies: SVG as a universal image format, LaTeX for typesetting, ffmpeg for video encoding, inkscape/imagemagick for rasterization, potrace for vectorization, blender/povray for 3D graphics, and Haskell for scripting. ## Scalable Vector Graphics -Movies consists of a sequence of frames and, in reanimate, these frames are SVG images. SVGs can easily reference raster images, includes a set of drawing primitives, and offers image advanced manipulation through filter effects. +Movies consists of a sequence of frames and, in reanimate, these frames are [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Introduction) images. SVGs can easily reference raster images, includes a set of drawing primitives, and offers image advanced manipulation through filter effects. Since SVGs are plain-text documents, tools can be written to analyse and modify images. For example, reanimate includes code for applying 2D physics to shapes in SVG images. SVG features, as demonstrated in the below animation: @@ -82,7 +82,7 @@ The following examples shows how something as seemingly complicated as fourier s -Scripting in Haskell also gives access to the extensive body of code libraries. There are Haskell libraries for syntax highlighting, font manipulation, and much, much more. In the spirit of being a batteries-included framework, Reanimate ships with a built-in 2D physics library, called Chipmunk2D. The example below demonstrates how SVG shapes can be used nearly effortlessly in a physics simulation. +Scripting in Haskell also gives access to the extensive body of code libraries. There are Haskell libraries for syntax highlighting, font manipulation, and much, much more. In the spirit of being a batteries-included framework, Reanimate ships with a built-in 2D physics library, called [Chipmunk2D](https://chipmunk-physics.net/). The example below demonstrates how SVG shapes can be used nearly effortlessly in a physics simulation.
Toggle source code. @@ -100,7 +100,7 @@ Scripting in Haskell also gives access to the extensive body of code libraries. ## Pillar II: LaTeX -LaTeX is a widely used system for typesetting equations and documents. It is most commonly used by writing TeX documents which are then converted to pdfs. However, since the output of LaTeX is natively vector graphics, it is trivial to get SVG documents instead of pdfs. Armed with this knowledge, Reanimate offers a simple yet powerful function: `latex :: Text -> SVG` +[LaTeX](https://www.latex-project.org/) is a widely used system for typesetting equations and documents. It is most commonly used by writing TeX documents which are then converted to pdfs. However, since the output of LaTeX is natively vector graphics, it is trivial to get SVG documents instead of pdfs. Armed with this knowledge, Reanimate offers a simple yet powerful function: `latex :: Text -> SVG` The `latex` function takes a snippet of TeX code, passes it through the LaTeX system, and converts the result to an SVG image. Furthermore, since the result is entirely determined by the TeX code, caching is used to hide the overhead of invoking LaTeX. @@ -120,7 +120,7 @@ The resulting SVGs can be manipulated just like any other. The below examples il ## Pillar III: povray -Although incredibly expressive, SVGs are strictly limited to 2D graphics. This limitation can be overcome with a 3D renderer such as povray: povray is a nearly 30 year-old raytracer with a relatively small but solid set of features. Reanimate offers convenient functions for importing povray scenes as well as exporting animations to be used as textures. In the video below, the LaTeX animation is projected upon a plane which is then rotated and translated in 3D space. A key thing to note is that both the 2D and 3D elements are managed entirely through code. +Although incredibly expressive, SVGs are strictly limited to 2D graphics. This limitation can be overcome with a 3D renderer such as [povray](https://www.povray.org/): povray is a nearly 30 year-old raytracer with a relatively small but solid set of features. Reanimate offers convenient functions for importing povray scenes as well as exporting animations to be used as textures. In the video below, the LaTeX animation is projected upon a plane which is then rotated and translated in 3D space. A key thing to note is that both the 2D and 3D elements are managed entirely through code.
Toggle source code. @@ -135,7 +135,7 @@ Although incredibly expressive, SVGs are strictly limited to 2D graphics. This l -The video above uses a perspective camera, ie. objects further away appears to be smaller. This gives the appearance of three dimensions but it also makes it difficult to interlace SVG objects and 3D objects with pixel-perfect precision. For example, aligning a cube and a square requires the exact position of the pinhole camera. All of this can be dramatically simplified with an orthographic projection where 'x' an 'y' coordinates in 3D space always map to the same 'x' and 'y' coordinates on the screen. Shapes lose their perspective but in many cases, especially when illustrating mathematical concepts, drawing "idealized" shapes is perfectly fine. The video below shows an orthographic projection of a sphere. The sphere (3D shape) could be completely eclipsed by a circle (2D shape) of the same radius with pixel-perfection. +The video above uses a perspective camera, ie. objects further away appears to be smaller. This gives the appearance of three dimensions but it also makes it difficult to interlace SVG objects and 3D objects with pixel-perfect precision. For example, aligning a cube and a square requires the exact position of the pinhole camera. All of this can be dramatically simplified with an [orthographic projection](https://en.wikipedia.org/wiki/Orthographic_projection) where 'x' an 'y' coordinates in 3D space always map to the same 'x' and 'y' coordinates on the screen. Shapes lose their perspective but in many cases, especially when illustrating mathematical concepts, drawing "idealized" shapes is perfectly fine. The video below shows an orthographic projection of a sphere. The sphere (3D shape) could be completely eclipsed by a circle (2D shape) of the same radius with pixel-perfect accuracy.
Toggle source code. @@ -152,12 +152,26 @@ The video above uses a perspective camera, ie. objects further away appears to b ## Pillar IV: Blender -TODO: Crumble SVG animation. +[Blender](https://www.blender.org/) is a vastly more modern and capable 3D modeller than povray but has a slightly steeper learning curve. Most people interact with Blender through a graphical user interface but all of blender's features can also be used directly from Python. The [Python API](https://docs.blender.org/api/current/index.html) is sizable and, at first glance, it might seem that there are almost no tutorials for blender scripting. However, Blender can tell you the Python command for every action in the GUI, making it easy to translate graphical tutorials to scritping tutorials. -TODO: Morph SVG animation into sphere. +The example below uses built-in modifiers to bend a plane into a sphere and is rendered using the EEVEE engine. + +
+ Toggle source code. +

+  {!examples/tut_glue_blender.hs!}
+  
+
+
+ ## Pillar V: potrace +[Potrace](https://en.wikipedia.org/wiki/Potrace) takes pixel data (from an image file, or perhaps generated by povray or Blender) and automatically convert it to vector graphics. Once an image has been vectorized, it can be manipulated with the standard SVG tools. In the example below, a sphere is rendered with povray, vectorized, and then line-drawn. +
Toggle source code.

diff --git a/examples/parameters.hs b/examples/parameters.hs
new file mode 100755
index 0000000..52c5b2f
--- /dev/null
+++ b/examples/parameters.hs
@@ -0,0 +1,11 @@
+#!/usr/bin/env stack
+-- stack runghc --package reanimate
+module Main (main) where
+
+import           Reanimate
+import qualified Data.Text as T
+
+main :: IO ()
+main = reanimate $ animate $ const $
+    mkText $ T.pack $ "Test parameters: " ++ show (pFPS, pWidth, pHeight)
+
diff --git a/examples/tut_glue_blender.hs b/examples/tut_glue_blender.hs
index 6837632..3717962 100644
--- a/examples/tut_glue_blender.hs
+++ b/examples/tut_glue_blender.hs
@@ -5,16 +5,21 @@
 {-# LANGUAGE BangPatterns       #-}
 module Main (main) where
 
-import           Codec.Picture
 import           Data.String.Here
 import qualified Data.Text         as T
 import           Reanimate
 import           Reanimate.Blender
 import           Reanimate.Raster
 import           Reanimate.Scene
+import           Reanimate.Animation
+import           Reanimate.Effect
 import Graphics.SvgTree
-import System.IO.Unsafe
-import qualified Data.Text.IO as T
+import           System.Random
+import           System.Random.Shuffle
+import           Control.Lens          ((^.))
+import           Control.Monad
+import           Data.Monoid
+import           Codec.Picture.Types
 
 main :: IO ()
 main = seq texture $ reanimate $ parA bg $ sceneAnimation $ do
@@ -27,8 +32,8 @@ main = seq texture $ reanimate $ parA bg $ sceneAnimation $ do
       getTrans <- freezeVar trans
       getRotX <- freezeVar rotX
       getRotY <- freezeVar rotY
-      return $ \real_t dur t ->
-        blender (script (getBend real_t) (getTrans real_t) (getRotX real_t) (getRotY real_t))
+      return $ \real_t dur t -> seq (texture (t/dur)) $
+        blender (script (texture (t/dur)) (getBend real_t) (getTrans real_t) (getRotX real_t) (getRotY real_t))
     tweenVar trans 5 (\t v -> fromToS v (-2) $ curveS 2 (t/5))
     tweenVar bend 5 (\t v -> fromToS v 1 $ curveS 2 (t/5))
     tweenVar rotY 15 (\t v -> fromToS v (pi*2*2) $ curveS 2 (t/15))
@@ -37,7 +42,7 @@ main = seq texture $ reanimate $ parA bg $ sceneAnimation $ do
       wait 5
       tweenVar rotX 5 (\t v -> fromToS v (pi/5) $ curveS 2 (t/5))
     wait (15-5)
-    tweenVar bend 5 (\t v -> fromToS 1 0 $ curveS 2 (t/5))
+    tweenVar bend 5 (\t v -> fromToS v 0 $ curveS 2 (t/5))
     tweenVar rotX 5 (\t v -> fromToS v 0 $ curveS 2 (t/5))
     tweenVar trans 5 (\t v -> fromToS v 0 $ curveS 2 (t/5))
     wait 4
@@ -46,15 +51,14 @@ main = seq texture $ reanimate $ parA bg $ sceneAnimation $ do
   where
     bg = animate $ const $ mkBackground "grey"
 
-texture :: FilePath
-texture = svgAsPngFile (mkGroup
-  [ checker 10 10
-  , withFillColor "red" $
-    scale 2 $ center $
-    latexAlign "\\sum_{k=1}^\\infty {1 \\over k^2} = {\\pi^2 \\over 6}"])
+texture :: Double -> FilePath
+texture t = svgAsPngFile $ mkGroup
+  [ checker 20 20
+  , frameAt (t*duration latexExample) latexExample
+  ]
 
-script :: Double -> Double -> Double -> Double -> T.Text
-script bend transZ rotX rotY = [iTrim|
+script :: FilePath -> Double -> Double -> Double -> Double -> T.Text
+script img bend transZ rotX rotY = [iTrim|
 import os
 import math
 
@@ -91,7 +95,7 @@ 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'])
 
-image_node.image = bpy.data.images.load('${T.pack texture}')
+image_node.image = bpy.data.images.load('${T.pack img}')
 
 
 modifier = plane.modifiers.new(name='Subsurf', type='SUBSURF')
@@ -115,9 +119,9 @@ bendAround.deform_axis = 'Z'
 bendAround.factor = -math.pi*2*x
 
 bpy.context.view_layer.objects.active = plane
-print(bpy.ops.object.modifier_apply(modifier='Subsurf'))
-print(bpy.ops.object.modifier_apply(modifier='Bend up'))
-print(bpy.ops.object.modifier_apply(modifier='Bend around'))
+#print(bpy.ops.object.modifier_apply(modifier='Subsurf'))
+#print(bpy.ops.object.modifier_apply(modifier='Bend up'))
+#print(bpy.ops.object.modifier_apply(modifier='Bend around'))
 
 bpy.ops.object.select_all(action='DESELECT')
 plane.select_set(True);
@@ -128,21 +132,22 @@ bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN')
 plane.rotation_euler = (0, ${rotY}, 0)
 
 scn = bpy.context.scene
-# scn.render.engine = 'CYCLES'
+
+scn.render.engine = 'CYCLES'
+scn.render.resolution_percentage = 10
+
 scn.render.film_transparent = True
 
 bpy.ops.render.render( write_still=True )
 |]
 
-
 checker :: Int -> Int -> SVG
 checker w h =
-  withFillColor "white" $
-  withStrokeColor "white" $
-  withStrokeWidth 0.1 $
+  withStrokeColor "lightblue" $
+  withStrokeWidth (defaultStrokeWidth/2) $
   mkGroup
   [ withStrokeWidth 0 $
-    withFillOpacity 0.8 $ mkBackground "blue"
+    withFillOpacity 0.8 $ mkBackground "white"
   , mkGroup
     [ translate (stepX*x-offsetX + stepX/2) 0 $
       mkLine (0, -screenHeight/2*0.9) (0, screenHeight/2*0.9)
@@ -160,3 +165,83 @@ checker w h =
     stepY = screenHeight/fromIntegral h
     offsetX = screenWidth/2
     offsetY = screenHeight/2
+
+
+
+
+-----------------------------------
+-- COPIED FROM tut_glue_latex.hs --
+
+
+latexExample :: Animation
+latexExample = sceneAnimation $ do
+    -- Draw equation
+    play $ drawAnimation strokedSvg
+    sprites <- forM glyphs $ \(fn, _, elt) ->
+      newSpriteA $ animate $ const $ fn elt
+    -- Yoink each glyph
+    forM_ (reverse sprites) $ \sprite -> do
+      spriteE sprite (overBeginning 1 $ aroundCenterE $ highlightE)
+      wait 0.5
+    -- Flash glyphs randomly with color
+    forM_ (shuffleList (sprites++sprites)) $ \sprite -> do
+      spriteE sprite (overBeginning 0.5 $ aroundCenterE $ flashE)
+      wait 0.1
+    wait 0.5
+    mapM_ destroySprite sprites
+    -- Undraw equations
+    play $ drawAnimation' (Just 0xdeadbeef) 1 0.1 strokedSvg
+      # reverseA
+  where
+    glyphs = svgGlyphs svg
+    strokedSvg =
+      withStrokeWidth (defaultStrokeWidth*0.5) $
+      withStrokeColor "black" svg
+    svg = lowerTransformations $ simplify $ scale 2 $ center $
+      latexAlign "\\sum_{k=1}^\\infty {1 \\over k^2} = {\\pi^2 \\over 6}"
+    shuffleList lst = shuffle' lst (length lst) (mkStdGen 0xdeadbeef)
+
+highlightE :: Effect
+highlightE d t =
+  scale (1 + bellS 2 (t/d)*0.5) . rotate (wiggleS (t/d) * 20)
+
+flashE :: Effect
+flashE d t =
+  withStrokeColor "black" .
+  withStrokeWidth (defaultStrokeWidth*0.5*bellS 2 (t/d)) .
+  withFillColorPixel (promotePixel $ turbo (t/d))
+
+-- 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
+
+--
+
+drawAnimation :: SVG -> Animation
+drawAnimation = drawAnimation' Nothing 0.5 0.3
+
+drawAnimation' :: Maybe Int -> Double -> Double -> SVG -> Animation
+drawAnimation' mbSeed fillDur step svg = sceneAnimation $ do
+  forM_ (zip [0..] $ shuf $ svgGlyphs svg) $ \(n, (fn, attr, tree)) -> do
+    let sWidth =
+          case toUserUnit defaultDPI <$> getLast (attr ^. strokeWidth) of
+            Just (Num d) -> d
+            _            -> defaultStrokeWidth
+    fork $ do
+      wait (n*step)
+      play $ mapA fn $ (animate (\t -> withFillOpacity 0 $ partialSvg t tree)
+        # applyE (overEnding fillDur $ fadeLineOutE sWidth))
+    fork $ do
+      wait (n*step+(1-fillDur))
+      newSprite $ do
+        return $ \_real_t _d t ->
+          withStrokeWidth 0 $ fn $ withFillOpacity (min 1 $ t/fillDur) tree
+  where
+    shuf lst =
+      case mbSeed of
+        Nothing   -> lst
+        Just seed -> shuffle' lst (length lst) (mkStdGen seed)
+
diff --git a/reanimate.cabal b/reanimate.cabal
index 3b38817..559df00 100644
--- a/reanimate.cabal
+++ b/reanimate.cabal
@@ -67,6 +67,7 @@ library
                       Reanimate.Effect
                       Reanimate.Builtin.TernaryPlot
                       Reanimate.Constants
+                      Reanimate.Parameters
                       Reanimate.Chiphunk
                       Reanimate.PolyShape
                       Reanimate.Builtin.Documentation
diff --git a/src/Reanimate.hs b/src/Reanimate.hs
index b081a69..ffce199 100644
--- a/src/Reanimate.hs
+++ b/src/Reanimate.hs
@@ -104,7 +104,11 @@ module Reanimate
     screenWidth,
     screenHeight,
     defaultDPI,
-    defaultStrokeWidth
+    defaultStrokeWidth,
+    -- * Parameters
+    pFPS,
+    pHeight,
+    pWidth
   ) where
 
 import           Reanimate.Animation
@@ -120,3 +124,4 @@ import           Reanimate.Svg.BoundingBox
 import           Reanimate.Svg.Constructors
 import           Reanimate.Svg.LineCommand
 import           Reanimate.Scene
+import           Reanimate.Parameters
diff --git a/src/Reanimate/Blender.hs b/src/Reanimate/Blender.hs
index a424994..4634fac 100644
--- a/src/Reanimate/Blender.hs
+++ b/src/Reanimate/Blender.hs
@@ -13,12 +13,14 @@ import           Graphics.SvgTree           (Tree (..))
 import           Reanimate.Cache
 import           Reanimate.Misc
 import           Reanimate.Raster
+import           Reanimate.Parameters
+import           Reanimate.Animation
 import           Reanimate.Svg.Constructors
 import           System.FilePath            (replaceExtension, (<.>))
 import           System.IO.Unsafe           (unsafePerformIO)
 import           Data.Hashable
 
-blender :: Text -> Tree
+blender :: Text -> SVG
 blender script =
   (unsafePerformIO $ mkBlenderImage script)
 
@@ -27,6 +29,7 @@ blender' script =
   (unsafePerformIO $ mkBlenderImage' script)
 
 mkBlenderImage :: Text -> IO Tree
+mkBlenderImage script | pNoExternals = pure $ mkText script
 mkBlenderImage script = do
     png <- B.readFile =<< mkBlenderImage' script
     case decodePng png of
@@ -34,6 +37,7 @@ mkBlenderImage script = do
       Right img -> return $ center $ scaleToSize 16 9 $ embedDynamicImage img
 
 mkBlenderImage' :: Text -> IO FilePath
+mkBlenderImage' _ | pNoExternals = pure "/blender/has/been/disabled"
 mkBlenderImage' script = cacheFile template $ \target -> do
     exec <- requireExecutable "blender"
     let py_file = replaceExtension target "py"
diff --git a/src/Reanimate/Driver.hs b/src/Reanimate/Driver.hs
index ae3bc1d..1e8db9d 100644
--- a/src/Reanimate/Driver.hs
+++ b/src/Reanimate/Driver.hs
@@ -8,6 +8,7 @@ import           Reanimate.Driver.Check
 import           Reanimate.Driver.CLI
 import           Reanimate.Driver.Compile
 import           Reanimate.Driver.Server
+import           Reanimate.Parameters
 import           Reanimate.Render         (FPS, Format (..), Height, Width,
                                            render, renderSnippets, renderSvgs)
 import           System.Directory
@@ -94,6 +95,7 @@ reanimate animation = do
   case optsCommand of
     Raw        -> renderSvgs animation
     Test       -> do
+      setNoExternals True
       -- hSetBinaryMode stdout True
       renderSnippets animation
     Check      -> checkEnvironment
@@ -137,6 +139,9 @@ reanimate animation = do
             ,"--target", target
             ,"+RTS", "-N", "-RTS"]
         else do
+          setFPS fps
+          setWidth width
+          setHeight height
           printf "Animation options:\n\
                  \  fps:    %d\n\
                  \  width:  %d\n\
diff --git a/src/Reanimate/LaTeX.hs b/src/Reanimate/LaTeX.hs
index af6ea68..9443ad3 100644
--- a/src/Reanimate/LaTeX.hs
+++ b/src/Reanimate/LaTeX.hs
@@ -12,6 +12,7 @@ import           Graphics.SvgTree  (Tree (..), defaultSvg, parseSvgFile)
 import           Reanimate.Cache
 import           Reanimate.Misc
 import           Reanimate.Svg
+import           Reanimate.Parameters
 import           System.FilePath   (replaceExtension, takeFileName, ())
 import           System.IO.Unsafe  (unsafePerformIO)
 
@@ -24,6 +25,7 @@ import           System.IO.Unsafe  (unsafePerformIO)
 --
 --   <>
 latex :: T.Text -> Tree
+latex tex | pNoExternals = mkText tex
 latex tex = (unsafePerformIO . (cacheMem . cacheDiskSvg) (latexToSVG "dvi" exec args)) script
   where
     exec = "latex"
@@ -39,6 +41,7 @@ latex tex = (unsafePerformIO . (cacheMem . cacheDiskSvg) (latexToSVG "dvi" exec
 --
 --   <>
 xelatex :: Text -> Tree
+xelatex tex | pNoExternals = mkText tex
 xelatex tex = (unsafePerformIO . (cacheMem . cacheDiskSvg) (latexToSVG "xdv" exec args)) script
   where
     exec = "xelatex"
diff --git a/src/Reanimate/Parameters.hs b/src/Reanimate/Parameters.hs
new file mode 100644
index 0000000..608faaa
--- /dev/null
+++ b/src/Reanimate/Parameters.hs
@@ -0,0 +1,60 @@
+module Reanimate.Parameters
+  ( pFPS
+  , pWidth
+  , pHeight
+  , pNoExternals
+  , setFPS
+  , setWidth
+  , setHeight
+  , setNoExternals
+  ) where
+
+import System.IO.Unsafe
+import Data.IORef
+import Reanimate.Render
+
+{-# NOINLINE pFPSRef #-}
+pFPSRef :: IORef FPS
+pFPSRef = unsafePerformIO (newIORef 0)
+
+{-# NOINLINE pFPS #-}
+pFPS :: FPS
+pFPS = unsafePerformIO (readIORef pFPSRef)
+
+setFPS :: FPS -> IO ()
+setFPS = writeIORef pFPSRef
+
+{-# NOINLINE pWidthRef #-}
+pWidthRef :: IORef FPS
+pWidthRef = unsafePerformIO (newIORef 0)
+
+{-# NOINLINE pWidth #-}
+pWidth :: Width
+pWidth = unsafePerformIO (readIORef pWidthRef)
+
+setWidth :: Width -> IO ()
+setWidth = writeIORef pWidthRef
+
+
+{-# NOINLINE pHeightRef #-}
+pHeightRef :: IORef FPS
+pHeightRef = unsafePerformIO (newIORef 0)
+
+{-# NOINLINE pHeight #-}
+pHeight :: Height
+pHeight = unsafePerformIO (readIORef pHeightRef)
+
+setHeight :: Height -> IO ()
+setHeight = writeIORef pHeightRef
+
+{-# NOINLINE pNoExternalsRef #-}
+pNoExternalsRef :: IORef Bool
+pNoExternalsRef = unsafePerformIO (newIORef False)
+
+{-# NOINLINE pNoExternals #-}
+pNoExternals :: Bool
+pNoExternals = unsafePerformIO (readIORef pNoExternalsRef)
+
+setNoExternals :: Bool -> IO ()
+setNoExternals = writeIORef pNoExternalsRef
+
diff --git a/src/Reanimate/Povray.hs b/src/Reanimate/Povray.hs
index 1fa2b88..d3c1a82 100644
--- a/src/Reanimate/Povray.hs
+++ b/src/Reanimate/Povray.hs
@@ -19,6 +19,7 @@ import           Graphics.SvgTree           (Tree (..))
 import           Reanimate.Cache
 import           Reanimate.Misc
 import           Reanimate.Raster
+import           Reanimate.Parameters
 import           Reanimate.Svg.Constructors
 import           System.FilePath            (replaceExtension, (<.>))
 import           System.IO.Unsafe           (unsafePerformIO)
@@ -57,6 +58,7 @@ povrayExtreme' :: [String] -> Text -> FilePath
 povrayExtreme' args = povrayRaw' (["+H2160","+W3840", "+A"] ++ args)
 
 mkPovrayImage :: [String] -> Text -> IO Tree
+mkPovrayImage _ script | pNoExternals = pure $ mkText script
 mkPovrayImage args script = do
     out <- mkPovrayImage' args script
     -- return $ center $ scaleToSize 16 9 $ embedImageFile out
@@ -66,6 +68,7 @@ mkPovrayImage args script = do
       Right img -> return $ center $ scaleToSize 16 9 $ embedDynamicImage img
 
 mkPovrayImage' :: [String] -> Text -> IO FilePath
+mkPovrayImage' _ _ | pNoExternals = pure "/povray/has/been/disabled"
 mkPovrayImage' args script = cacheFile template $ \target -> do
     exec <- requireExecutable "povray"
     let pov_file = replaceExtension target "pov"
@@ -74,3 +77,4 @@ mkPovrayImage' args script = cacheFile template $ \target -> do
   where
     template = show (hash key) <.> "png"
     key = T.concat (script:map T.pack args)
+
diff --git a/src/Reanimate/Scene.hs b/src/Reanimate/Scene.hs
index 497e493..0415be4 100644
--- a/src/Reanimate/Scene.hs
+++ b/src/Reanimate/Scene.hs
@@ -154,74 +154,16 @@ withObject obj@(Object ref) scene = do
   liftST $ writeSTRef ref (Just tl)
   return a
 
-
-data Param s a = Param (STRef s (Time, Time, Time -> a))
-
-newParam :: a -> Scene s (Param s a)
-newParam initVal = do
-  now <- queryNow
-  Param <$> liftST (newSTRef (now, -1, const initVal))
-
-destroyParam :: Param s a -> Scene s ()
-destroyParam (Param ref) = do
-  now <- queryNow
-  liftST $ do
-    (startT, _endT, fn) <- readSTRef ref
-    writeSTRef ref (startT, now, fn)
-
-readParam :: Param s a -> Scene s a
-readParam (Param ref) = do
-  now <- queryNow
-  (_, _, fn) <- liftST $ readSTRef ref
-  return $ fn now
-
-paramAt :: Param s a -> Time -> ST s a
-paramAt = undefined
-
-paramFn :: Param s a -> ST s (Time -> a)
-paramFn (Param ref) = do
-  (_, _, fn) <- readSTRef ref
-  return fn
-
-withParamAt :: Param s a -> Time -> (a -> ST s SVG) -> ST s SVG
-withParamAt = undefined
-
 fromParams :: Gen s -> Scene s ()
 fromParams gen = M $ \_ -> return ((), 0, 0, emptyTimeline, [gen])
 
-setParam :: Param s a -> a -> Scene s ()
-setParam = undefined
-
--- setParamZIndex :: Param s a -> ZIndex -> Scene s ()
--- setParamZIndex = undefined
---
--- adjustParamZIndex :: Param s a -> Duration -> (Time -> ZIndex -> ZIndex) -> Scene s ()
--- adjustParamZIndex = undefined
---
--- adjustParamZIndex_ :: Param s a -> (Time -> ZIndex -> ZIndex) -> Scene s ()
--- adjustParamZIndex_ = undefined
-
-tweenParam :: Param s a -> Duration -> (Double -> a -> a) -> Scene s ()
-tweenParam (Param ref) dur fn = do
-  now <- queryNow
-  liftST $ do
-    (startT,endT,prevFn) <- readSTRef ref
-    let worker t
-          | t > now = fn (min 1 ((t-now)/dur)) (prevFn t)
-          | otherwise = prevFn t
-    writeSTRef ref (startT, endT, worker)
-  wait dur
-
-tweenParam_ :: Param s a -> (Time -> a -> a) -> Scene s ()
-tweenParam_ = undefined
-
-simpleParam :: (a -> SVG) -> a -> Scene s (Param s a)
+simpleParam :: (a -> SVG) -> a -> Scene s (Var s a)
 simpleParam render def = do
-  p <- newParam def
-  fromParams $ do
-    fn <- paramFn p
-    return $ \_d t -> (render $ fn t, 0)
-  return p
+  v <- newVar def
+  _ <- newSprite $ do
+       getV <- freezeVar v
+       return $ \real_t _d _t -> render (getV real_t)
+  return v
 
 data Var s a = Var (STRef s (Time -> a))
 
diff --git a/videos/sorting-algorithms/sorting-algorithms.hs b/videos/sorting-algorithms/sorting-algorithms.hs
index efc5433..a0bbba5 100755
--- a/videos/sorting-algorithms/sorting-algorithms.hs
+++ b/videos/sorting-algorithms/sorting-algorithms.hs
@@ -267,7 +267,7 @@ simpleSort_ lst = do
           worker dir rest
         worker dir ((i, (nth,t)):rest) = do
           z <- round <$> queryNow
-          fork $ tweenParam (params!!i) 1 $ \t (x,y,elt) ->
+          fork $ tweenVar (params!!i) 1 $ \t (x,y,elt) ->
             let s = curveS 2 t in
             (fromToS x (fromIntegral nth) s, y+sin (pi*s )*dir, elt)
           wait 0.5
@@ -289,9 +289,9 @@ quicksort_ lst = do
 
     let partition pivot lo hi = do
           loValP <- liftST (V.read params lo)
-          loVal <- readParam loValP
+          loVal <- readVar loValP
           hiValP <- liftST (V.read params hi)
-          hiVal <- readParam hiValP
+          hiVal <- readVar hiValP
           if getKey loVal < getKey pivot
             then partition pivot (lo+1) hi
             else if getKey hiVal > getKey pivot
@@ -301,10 +301,10 @@ quicksort_ lst = do
                 else do
                   liftST $ V.write params lo hiValP
                   liftST $ V.write params hi loValP
-                  fork $ tweenParam hiValP 1 $ \t (x,y,elt) ->
+                  fork $ tweenVar hiValP 1 $ \t (x,y,elt) ->
                     let s = curveS 2 t in
                     (fromToS x (getPos loVal) s, y+sin (pi*s), elt)
-                  fork $ tweenParam loValP 1 $ \t (x,y,elt) ->
+                  fork $ tweenVar loValP 1 $ \t (x,y,elt) ->
                     let s = curveS 2 t in
                     (fromToS x (getPos hiVal) s, y-sin (pi*s), elt)
                   wait 1
@@ -312,12 +312,12 @@ quicksort_ lst = do
     let worker lo hi | lo >= hi = return ()
         worker lo hi = do
           pivotP <- getPivot params lo hi
-          pivot <- readParam pivotP
-          tweenParam pivotP 1 $ \t (x,y,elt) ->
+          pivot <- readVar pivotP
+          tweenVar pivotP 1 $ \t (x,y,elt) ->
             let s = curveS 2 t in
             (x, y-s/2, elt)
           p <- partition pivot lo hi
-          tweenParam pivotP 1 $ \t (x,y,elt) ->
+          tweenVar pivotP 1 $ \t (x,y,elt) ->
             let s = curveS 2 t in
             (x, y+s/2, elt)
           fork $ worker lo p
@@ -328,7 +328,7 @@ quicksort_ lst = do
       let middle = lo + (hi-lo) `div` 2
           indices = filter (< hi) $ filter (>= lo) [middle-1,middle,middle+1]
       selected <- forM indices $ \idx -> liftST (V.read params idx)
-      keys <- mapM readParam selected
+      keys <- mapM readVar selected
       return $ head $ drop (length indices `div` 2) $ map snd $ sortBy (comparing fst) $ zip (map getKey keys) selected
     getPos (x, _, _) = x
     getKey (_x, _y, (i, _)) = i