From 3ac39cb2bc91739941bea8d6545ecad8a3a14398 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 27 Feb 2019 10:06:38 +0100 Subject: [PATCH] Improve the web-viewer. --- reanimate.cabal | 23 +++- src/Reanimate/Arrow.hs | 9 ++ src/Reanimate/Examples.hs | 12 +- src/Reanimate/Misc.hs | 45 ++++++-- src/Reanimate/Render.hs | 23 +++- src/Reanimate/Svg.hs | 6 + web/.gitignore | 23 ++++ web/public/index.html | 2 +- web/src/App.css | 28 +++-- web/src/App.jsx | 131 ++++++++++++++++++--- web/src/Presets.jsx | 231 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 486 insertions(+), 47 deletions(-) create mode 100755 web/.gitignore create mode 100644 web/src/Presets.jsx diff --git a/reanimate.cabal b/reanimate.cabal index e07f1a6..9ce59df 100644 --- a/reanimate.cabal +++ b/reanimate.cabal @@ -28,17 +28,32 @@ library build-depends: base >=4.10 && <4.13, lucid-svg, time, text, unix, lucid, filepath, process, directory, containers, svg-tree >= 0.6.2.3, xml, bytestring, lens, linear, mtl, matrix, - JuicyPixels, attoparsec + JuicyPixels, attoparsec, parallel Flag gtk-viewer Description: Enable gtk-based viewer - Default: True + Default: False + +Flag server + Description: Enable rendering server + Default: False + +executable reanimate-server + if flag(server) + buildable: True + else + buildable: False + hs-source-dirs: server, src + main-is: Main.hs + other-modules: Reanimate.Misc + build-depends: base >=4.10 && <4.13, text, websockets, process, filepath, directory, + cache, clock, stm, containers executable reanimate-viewer if flag(gtk-viewer) - buildable: True + buildable: True else - buildable: False + buildable: False main-is: SvgViewer.hs -- other-extensions: other-modules: Reanimate.Arrow diff --git a/src/Reanimate/Arrow.hs b/src/Reanimate/Arrow.hs index b3cb6ef..420c279 100644 --- a/src/Reanimate/Arrow.hs +++ b/src/Reanimate/Arrow.hs @@ -48,6 +48,15 @@ frameAt n (Animation d fn) = svg $ fn d (n `mod'` d) () with (svg11_ content) [width_ "320" , height_ "180", viewBox_ "0 0 320 180"] -- with (svg11_ content) [width_ "1280" , height_ "720", viewBox_ "0 0 320 180"] +unboundedFrameAt :: Double -> Ani () -> Svg () +unboundedFrameAt n (Animation d fn) = svg $ fn d (n `mod'` d) () + where + svg :: Svg () -> Svg () + svg content = do + doctype_ + with (svg11_ content) [viewBox_ "0 0 320 180"] + -- with (svg11_ content) [width_ "1280" , height_ "720", viewBox_ "0 0 320 180"] + emit :: Animation (Svg ()) () emit = Animation 0 (\d t svg -> svg) diff --git a/src/Reanimate/Examples.hs b/src/Reanimate/Examples.hs index 2bdae3d..094d324 100644 --- a/src/Reanimate/Examples.hs +++ b/src/Reanimate/Examples.hs @@ -36,7 +36,7 @@ sinewave = proc () -> do idx <- signalOscillate 0 1 -< () emit -< do defs_ $ clipPath_ [id_ "clip"] $ toHtml $ - mkRect (Num 0, Num (-height)) (Num $ idx*width) (Percent 100) + mkRect (Num 0, Num (-height)) (Num $ idx*width) (Num 320) toHtml $ translate margin height $ withStrokeColor "white" $ withClipPathRef (Ref "clip") $ mkPathText $ renderPathText $ approxFnData 1000 wave toHtml $ withStrokeColor "white" $ @@ -54,9 +54,9 @@ morph_wave :: Ani () morph_wave = proc () -> do duration 5 -< () morph <- signalOscillate 0 1 -< () + emit -< toHtml $ mkBackground "black" emit -< toHtml $ withStrokeColor "white" $ mkGroup - [ mkBackground "black" - , translate 30 50 $ mkPathText $ renderPathText wave1 + [ translate 30 50 $ mkPathText $ renderPathText wave1 , translate 30 130 $ mkPathText $ renderPathText wave2 , translate 30 90 $ mkPathText $ renderPathText $ morphPath wave1 wave2 morph , mkLine (Num 30, Num 10) (Num 30, Num 170) @@ -368,7 +368,7 @@ bezier = adjustSpeed 0.4 $ proc () -> do let new' = map (\(a,b) -> between a b s) (zip old new) emit -< forM_ (zip new' (tail new')) $ \(a,b) -> do renderPath $ - approxFnData 1000 $ \idx -> + approxFnData 100 $ \idx -> between a b idx emit -< mapM_ secondaryCircleAt new' emit -< primaryCircleAt (head new') @@ -377,11 +377,11 @@ bezier = adjustSpeed 0.4 $ proc () -> do s <- signalOscillate 0 1 -< () emit -< primaryCircleAt =<< orderN' (map const lst) s <* mapM_ secondaryCircleAt lst orderN' [a] s = do - renderPath $ take (round $ 1000*s) $ approxFnData 1000 $ \idx -> a idx + renderPath $ take (round $ 100*s) $ approxFnData 100 $ \idx -> a idx return (a s) orderN' lst s = do forM_ (zip lst (tail lst)) $ \(a,b) -> renderPath $ - approxFnData 1000 $ \idx -> + approxFnData 100 $ \idx -> between (a s) (b s) idx let middlePoints = map (\(a,b) -> \idx -> between (a idx) (b idx) idx) (zip lst (tail lst)) orderN' middlePoints s <* mapM_ secondaryCircleAt (map ($s) middlePoints) diff --git a/src/Reanimate/Misc.hs b/src/Reanimate/Misc.hs index 47228fc..6f389a3 100644 --- a/src/Reanimate/Misc.hs +++ b/src/Reanimate/Misc.hs @@ -1,19 +1,24 @@ module Reanimate.Misc ( requireExecutable , runCmd + , runCmd_ + , runCmdLazy , withTempDir , withTempFile ) where import Control.Exception (evaluate, finally, throwIO) +import qualified Data.Text as T +import qualified Data.Text.IO as T import System.Directory (createDirectory, findExecutable, getTemporaryDirectory, removeDirectoryRecursive, removeFile) import System.Exit (ExitCode (..)) import System.FilePath ((<.>), ()) -import System.IO (hClose, openTempFile) -import System.Process (readProcessWithExitCode, showCommandForUser) - +import System.IO (hClose, openTempFile, hGetContents, hIsEOF) +import System.Process (readProcessWithExitCode, + runInteractiveProcess, showCommandForUser, + waitForProcess) requireExecutable :: String -> IO FilePath requireExecutable exec = do @@ -24,16 +29,42 @@ requireExecutable exec = do runCmd :: FilePath -> [String] -> IO () runCmd exec args = do + _ <- runCmd_ exec args + return () + +runCmd_ :: FilePath -> [String] -> IO (Either String String) +runCmd_ exec args = do (ret, stdout, stderr) <- readProcessWithExitCode exec args "" evaluate (length stdout + length stderr) case ret of - ExitSuccess -> return () + ExitSuccess -> return (Right stdout) ExitFailure err -> do - putStrLn $ + return $ Left $ "Failed to run: " ++ showCommandForUser exec args ++ "\n" ++ "Error code: " ++ show err ++ "\n" ++ - "stderr: " ++ show stderr - throwIO (ExitFailure err) + "stderr: " ++ stderr + +runCmdLazy :: FilePath -> [String] -> IO (IO (Either String T.Text)) +runCmdLazy exec args = do + (inp, out, err, pid) <- runInteractiveProcess exec args Nothing Nothing + hClose inp + return $ do + eof <- hIsEOF out + if eof + then do + stderr <- hGetContents err + evaluate (length stderr) + ret <- waitForProcess pid + case ret of + ExitSuccess -> return (Left "") + ExitFailure err -> do + return $ Left $ + "Failed to run: " ++ showCommandForUser exec args ++ "\n" ++ + "Error code: " ++ show err ++ "\n" ++ + "stderr: " ++ stderr + else do + line <- T.hGetLine out + return (Right line) withTempDir :: (FilePath -> IO a) -> IO a withTempDir action = do diff --git a/src/Reanimate/Render.hs b/src/Reanimate/Render.hs index 86c0698..368cba8 100644 --- a/src/Reanimate/Render.hs +++ b/src/Reanimate/Render.hs @@ -1,14 +1,31 @@ module Reanimate.Render ( render + , renderSvgs ) where import Control.Monad (forM_) -import Lucid.Svg (renderToFile) -import Reanimate.Arrow (Ani, animationDuration, frameAt) +import Lucid.Svg (renderToFile, renderBS) +import Reanimate.Arrow (Ani, animationDuration, frameAt, unboundedFrameAt) import Reanimate.Examples import Reanimate.Misc -import System.FilePath (takeExtension, ()) +import System.Directory (renameFile) +import System.FilePath (takeExtension, takeFileName, ()) import Text.Printf (printf) +import qualified Data.ByteString.Lazy.Char8 as BS +import Control.Parallel.Strategies + +renderSvgs :: Ani () -> FilePath -> IO () +renderSvgs ani tmpDir = do + let frameName nth = tmpDir printf nameTemplate nth + renderedFrames = map (BS.concat . BS.lines . renderBS . nthFrame) frames + mapM_ BS.putStrLn (renderedFrames `using` parBuffer 16 rdeepseq) + where + frames = [0..frameCount-1] + rate = 60 + nthFrame nth = unboundedFrameAt (recip (fromIntegral rate) * fromIntegral nth) ani + frameCount = round (animationDuration ani * fromIntegral rate) :: Int + nameTemplate :: String + nameTemplate = "render-%05d.svg" data Format = RenderMp4 | RenderGif | RenderWebm diff --git a/src/Reanimate/Svg.hs b/src/Reanimate/Svg.hs index 7e73434..fe38dd7 100644 --- a/src/Reanimate/Svg.hs +++ b/src/Reanimate/Svg.hs @@ -425,6 +425,12 @@ mkRect corner width height = RectangleTree $ defaultSvg & rectWidth .~ width & rectHeight .~ height +mkBoundingRect :: Tree -> Double -> Tree +mkBoundingRect src margin = + mkRect (Num $ x-margin, Num $ y-margin) (Num $ w+margin*2) (Num $ h+margin*2) + where + (x, y, w, h) = boundingBox src + mkLine :: Point -> Point -> Tree mkLine point1 point2 = LineTree $ defaultSvg & linePoint1 .~ point1 diff --git a/web/.gitignore b/web/.gitignore new file mode 100755 index 0000000..4d29575 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/web/public/index.html b/web/public/index.html index 75980d5..6694681 100755 --- a/web/public/index.html +++ b/web/public/index.html @@ -22,7 +22,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Reanimate Live Editor diff --git a/web/src/App.css b/web/src/App.css index 0194702..ebe1ea9 100755 --- a/web/src/App.css +++ b/web/src/App.css @@ -7,22 +7,32 @@ display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: max-content auto; - grid-gap: 1em; + /* grid-gap: 1em; */ grid-auto-flow: column } - +.editor { + /* min-width: 35em; */ +} +.controls { + text-align:center; +} +.controls svg { + padding-left: 5px; + padding-right: 5px; +} +.viewer { + position: static; +} div.messages { position: fixed; - bottom: 0; + top: 0; width: 100%; - transition: height 1s ease; - height: 1em + 4px; + background: #282c34; +} +div.messages pre { + margin: 0; } -div.messages:hover { - height: 100px; - transition: height 1s ease; -} #editor { diff --git a/web/src/App.jsx b/web/src/App.jsx index 371d7d5..2d73db3 100755 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -1,45 +1,142 @@ import React, {Component} from 'react'; import './App.css'; import AceEditor from 'react-ace'; -import brace from 'brace'; import 'brace/mode/haskell'; import 'brace/theme/github'; import 'brace/theme/monokai'; +import preset from './Presets'; + class App extends Component { + connect = () => { + const ws = new WebSocket("ws://localhost:9160"); + + ws.onopen = event => { + this.setState(state => ({...state, message: "Connected."})); + ws.send(this.state.program); + } + ws.onclose = event => { + this.setState(state => ({...state, message: "Disconnected."})); + setTimeout(this.connect, 1000); + } + ws.onmessage = event => { + if( event.data === "Success!" ) { + console.log("Success"); + } else if( event.data === "Rendering" ) { + this.setState({message: "Rendering..."}); + this.nFrames_new = 0; + this.svgs_new = []; + } else if( event.data === "Done" ) { + this.setState({message: "Success!"}); + console.log("Done"); + this.nFrames = this.nFrames_new; + this.svgs = this.svgs_new; + this.nFrames_new = 0; + this.svgs_new = []; + this.start = Date.now(); + } else if( event.data.startsWith("Error") ) { + console.log("Error"); + this.setState({message: event.data }); + } else { + this.setState({message: `Rendering: ${this.nFrames_new}`}); + this.nFrames_new++; + const div = document.createElement('div'); + div.innerHTML = event.data; + this.svgs_new.push(div); + } + } + this.setState(state => ({...state, socket: ws, message: "Connecting..."})); + } + constructor(props) { + super(props); + + this.state = { + program: preset[0].programs[0].code + }; + setTimeout(this.connect,0); + this.nFrames_new = 0; + this.svgs_new = []; + this.nFrames = 0; + this.svgs = []; + this.start = Date.now(); + const self = this; + const animate = () => { + const now = Date.now(); + const nFrames = self.nFrames; + const thisFrame = (Math.round((now-this.start)/1000*60))%nFrames + // const thisFrame = 0; + // console.log('Animation frame:', thisFrame, nFrames); + if(nFrames) { + // self.svg.innerHTML = self.svgs[thisFrame]; + while(self.svg.firstChild) + self.svg.removeChild(self.svg.firstChild); + self.svg.appendChild(self.svgs[thisFrame]); + } else { + self.svg.innerText = "Loading..."; + } + requestAnimationFrame(animate); + }; + requestAnimationFrame(animate); + } + onLoad = ace => { + setTimeout(function() { + ace.resize(); + }, 0); + } + onChange = text => { + this.setState({program: text}); + if (this.timeout) + clearTimeout(this.timeout); + const socket = this.state.socket + const self = this; + + this.timeout = setTimeout(function() { + console.log('change', text); + self.setState(state => ({...state, message: "Compiling..."})); + socket.send(text); + }, 500); + }; + selectPreset = evt => { + this.onChange(evt.target.value); + }; render() { + const {message, program} = this.state; return (
- + { preset.map( (group, i) => + + { group.programs.map( (elt, i) => + + )} + + )}
-
+
-
Controls
-
- Viewer +
+ +
+
+
this.svg = node}/>
-

Compilation successful.

+
{message}
diff --git a/web/src/Presets.jsx b/web/src/Presets.jsx new file mode 100644 index 0000000..ca81b4f --- /dev/null +++ b/web/src/Presets.jsx @@ -0,0 +1,231 @@ +const latex_draw = +`animation = pauseAtEnd 1 $ proc () -> do + emit -< toHtml $ mkBackground "black" + drawText msg \`andThen\` fillText msg -< () + where + msg = "\\\\sum_{k=1}^\\\\infty {1 \\\\over k^2} = {\\\\pi^2 \\\\over 6}" + placement = translate (320/2) (180/2) . scale 5 + fillText txt = proc () -> do + duration 1 -< () + s <- signal 0 1 -< () + emit -< toHtml $ placement $ + withFillColor "white" $ withFillOpacity s $ + center $ latexAlign txt + drawText txt = proc () -> do + duration 2 -< () + s <- signal 0 1 -< () + emit -< toHtml $ placement $ + withStrokeColor "white" $ withFillOpacity 0 $ withStrokeWidth (Num 0.1) $ + partialSvg s $ center $ latexAlign txt`; + +const bbox = +`animation :: Ani () +animation = proc () -> do + emit -< toHtml $ mkBackground "black" + duration 5 -< () + 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 = proc () -> do + s <- signal 0 1 -< () + emit -< do + toHtml $ mkBoundingBox $ rotate (360*s) svg + toHtml $ withFillColor "white" $ rotate (360*s) svg + where + svg = scale 3 $ center $ latexAlign "\\\\sum_{k=1}^\\\\infty" + +bbox2 :: Ani () +bbox2 = proc () -> do + s <- signalOscillate 0 1 -< () + emit -< do + toHtml $ mkBoundingBox $ partialSvg s heartShape + toHtml $ withStrokeColor "white" $ withFillOpacity 0 $ partialSvg s heartShape + +mkBoundingBox :: Tree -> Tree +mkBoundingBox svg = withStrokeColor "red" $ withFillOpacity 0 $ + mkRect (S.Num x, S.Num y) (S.Num w) (S.Num h) + where + (x, y, w, h) = boundingBox svg + +heartShape = + center $ rotateAroundCenter 225 $ mkPathString + "M0.0,40.0 v-40.0 h40.0\ + \a20.0 20.0 90.0 0 1 0.0,40.0\ + \a20.0 20.0 90.0 0 1 -40.0,0.0 Z"`; + +const sinewave = +`animation :: Ani () +animation = proc () -> do + duration 10 -< () + emit -< toHtml $ mkBackground "black" + idx <- signalOscillate 0 1 -< () + emit -< do + defs_ $ clipPath_ [id_ "clip"] $ toHtml $ + mkRect (Num 0, Num (-height)) (Num $ idx*width) (Num 320) + toHtml $ translate margin height $ withStrokeColor "white" $ + withClipPathRef (Ref "clip") $ mkPathText $ renderPathText $ approxFnData 1000 wave + toHtml $ withStrokeColor "white" $ + mkLine (Num margin, Num 10) (Num margin, Num 170) + toHtml $ withStrokeColor "white" $ + mkLine (Num margin, Num height) (Num (margin+width), Num height) + let (circX, circY) = wave idx + 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 + wave idx = (idx*width, sin (idx*pi*2*freq) * 50)`; + +const morph_wave = +`animation :: Ani () +animation = proc () -> do + duration 5 -< () + morph <- signalOscillate 0 1 -< () + emit -< toHtml $ mkBackground "black" + emit -< toHtml $ withStrokeColor "white" $ mkGroup + [ translate 30 50 $ mkPathText $ renderPathText wave1 + , translate 30 130 $ mkPathText $ renderPathText wave2 + , translate 30 90 $ mkPathText $ renderPathText $ morphPath wave1 wave2 morph + , mkLine (Num 30, Num 10) (Num 30, Num 170) + , mkLine (Num 30, Num 90) (Num 290, Num 90) ] + where + freq = 3; width = 260 + wave1 = approxFnData 1000 $ \\idx -> (idx*width, sin (idx*pi*2*freq) * 20) + wave2 = approxFnData 1000 $ \\idx -> (idx*width, sin (idx*pi*2*(freq*3)) * 20)`; + +const morph_wave_circle = +`animation :: Ani () +animation = proc t -> do + duration 5 -< () + idx <- signalOscillate 0 1 -< () + emit -< toHtml $ mkBackground "black" + emit -< toHtml $ withStrokeColor "white" $ mkGroup + [ translate 30 90 $ mkPathText $ renderPathText $ morphPath circle wave1 idx + , mkLine (Num 30, Num 10) (Num 30, Num 170) + , mkLine (Num 30, Num 90) (Num 290, Num 90) ] + where + freq = 5; width = 260; radius = 50 + wave1 = approxFnData 100 $ \\idx -> (idx*width, sin (idx*pi*2*freq) * 20) + circle = approxFnData 100 $ \\idx -> + (cos (idx*pi*2+pi/2)*radius + width/2, sin (idx*pi*2+pi/2)*radius)`; + +const progressMeters = +`animation :: Ani () +animation = proc () -> do + emit -< rect_ [width_ "100%", height_ "100%", fill_ "black"] + 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" + , text_anchor_ "middle" + , fill_ "white"] "1x" + text_ [x_ "155", y_ "150", font_size_ "20" + , text_anchor_ "middle" + , fill_ "white"] "2x" + text_ [x_ "255", y_ "150", font_size_ "20" + , text_anchor_ "middle" + , fill_ "white"] "0.5x" + +progressMeter :: Ani () +progressMeter = loop $ proc () -> do + duration 5 -< () + h <- signal 0 100 -< () + emit -< rect_ [ width_ "30", height_ "100", stroke_ "white", stroke_width_ "2", fill_opacity_ "0" ] + emit -< rect_ [ width_ "30", num_ height_ h, stroke_ "white", fill_ "white" ] + returnA -< ()` + +const highlight = +`animation :: Ani () +animation = proc () -> do + emit -< rect_ [width_ "100%", height_ "100%", fill_ "black"] + emit -< do + path_ (commonAttrs "white" ++ [d_ $ renderPathText rect1]) + path_ (commonAttrs "white" ++ [d_ $ renderPathText rect2]) + + path_ (commonAttrs "white" ++ [d_ $ renderPathText rect3]) + path_ (commonAttrs "lightblue" ++ [d_ $ renderPathText rect4]) + path_ (commonAttrs "yellow" ++ [d_ $ renderPathText rect5]) + path_ (commonAttrs "red" ++ [d_ $ renderPathText rect6]) + + follow + [ mkTransition highlight1 highlight2 + , mkTransition highlight2 highlight3 + , mkTransition highlight3 highlight4 + , mkTransition highlight4 highlight5 + , mkTransition highlight5 highlight6 + , mkTransition highlight6 highlight1 + ] -< () + + where + mkTransition from to = pauseAtEnd 1 $ proc () -> do + duration 1 -< () + s <- signalSCurve 2 0 1 -< () + let trans = morphPath from to s + emit -< + path_ (highlightAttrs "green" ++ [d_ $ renderPathText trans <> "Z"]) + mkRect x y width height = + [ (x,y), (x+width, y), (x+width, y+height), (x,y+height) ] + rect1 = mkRect margin margin w h + rect2 = mkRect (320-margin-w*2) margin (w*2) h + rect3 = mkRect margin (180-margin-h) w h + rect4 = mkRect (320/3) (180-margin-h) w h + rect5 = mkRect (320/3*2-w) (180-margin-h) w h + rect6 = mkRect (320-margin-w) (180-margin-h) w h + highlight1 = mkRect (margin-b) (margin-b) (w+2*b) (h+2*b) + highlight2 = mkRect (320-margin-w*2-b) (margin-b) (w*2+2*b) (h+2*b) + highlight3 = mkRect (320-margin-w-b) (180-margin-h-b) (w+2*b) (h+2*b) + highlight4 = mkRect (320/3*2-w-b) (180-margin-h-b) (320/3+2*b) (h+2*b) + highlight5 = mkRect (320/3-b) (180-margin-h-b) (320/3+2*b) (h+2*b) + highlight6 = mkRect (margin-b) (180-margin-h-b) (320/3+2*b) (h+2*b) + b = 7; margin = 30; w = 30; h = 30 + commonAttrs c = [stroke_width_ "2", stroke_ c, fill_ c] + highlightAttrs c = [stroke_width_ "2", stroke_ c, fill_opacity_ "0"]`; + +const latex_basic = +`animation :: Ani () +animation = proc () -> do + duration 2 -< () + s <- signalOscillate 0 1 -< () + emit -< toHtml $ mkGroup + [ mkBackground "black" + , translate (320/2) (180/2) $ mkGroup + [ withStrokeColor "white" $ withFillOpacity 0 $ withStrokeWidth (Num 0.1) text + , withFillColor "white" $ withFillOpacity s text] ] + where + text = scale 4 $ center $ latexAlign + "\\\\sum_{k=1}^\\\\infty {1 \\\\over k^2} = {\\\\pi^2 \\\\over 6}"` + +const latex_color = +`animation :: Ani () +animation = proc () -> do + duration 0.1 -< () + emit -< toHtml $ mkBackground "black" + emit -< toHtml $ translate (320/2) (180/2) $ withStrokeWidth (Num 0.2) $ + withStrokeColor "white" $ + withSubglyphs [0] (withFillColor "blue") $ + withSubglyphs [1] (withFillColor "yellow") $ + withSubglyphs [2] (withFillColor "green") $ + withSubglyphs [3] (withFillColor "red") $ + withSubglyphs [4] (withFillColor "darkslategrey") $ + svg + where + svg = scale 10 $ center $ latex "\\\\LaTeX"` + + +export default [ + { name: "Examples" + , programs: + [ {name: "LaTeX Draw", code: latex_draw } + , {name: "LaTeX Color", code: latex_color } + , {name: "LaTeX Basic", code: latex_basic } + , {name: "Bounding boxes", code: bbox } + , {name: "Sinewave", code: sinewave } + , {name: "Morphwave", code: morph_wave } + , {name: "Morphwave Circle", code: morph_wave_circle } + , {name: "Progress meters", code: progressMeters } + // , {name: "Highlight", code: highlight } + ] + }, +];