svg
and canvas
libraries.
traverse
Event
: a value at a point in time.Behaviour
: a value at all points in time.Dynamic
: Combination of an Event
and a Behaviour
.
Functor
, Applicative
, Monad
, and friends.
JavaScript
context.beginPath(); context.moveTo(a,b); context.lineTo(c,d); context.closePath(); context.stroke();
ghcjs-dom
package for API functionsHaskell
beginPath context moveTo context a b lineTo context c d closePath context stroke context
We need a canvas rendering context:
Create your canvas
(canvas, _) <- RD.divClass "canvas-wrapper" $ RD.elAttr' "canvas" canvasAttrs RD.blank
Ask reflex-dom-canvas
for a 2d drawing context
dContext2d :: ... => CanvasConfig 'TwoD t -> m (Dynamic t (CanvasInfo 'TwoD t))
C.dContext2d (CTypes.CanvasConfig canvas mempty)
Yay!
data CanvasInfo (c :: ContextType) t = CanvasInfo { _canvasInfo_El :: RD.El t , _canvasInfo_context :: RenderContext c ...
Decide to turn left or right…
data LR = LeftToRight | RightToLeft deriving (Show, Eq)
ltor1 :: IO LR ltor1 = MRnd.uniform [RightToLeft, LeftToRight] ltor2 :: Rnd.StdGen -> (LR, Rnd.StdGen) ltor2 = first (bool RightToLeft LeftToRight . (>= 0.5)) . Rnd.randomR (0::Double,1.0) ltor3 :: MonadIO m => m LR ltor3 = bool RightToLeft LeftToRight . (>= (0.5::Double)) <$> liftIO (Rnd.randomRIO (0::Double,1.0))
Repeat, lots.
imperative, mutable
for (var x = 0; x < size; x += step) { for (var y = 0; y < size; y += step) { } }
functional, immutable
let xy = [0,stp .. size] forM_ xy $ \x -> forM_ xy $ \y ->
requestDomAction :: DomRenderHook t m => Event t (JSM a) -> m (Event t a) _ <- requestDomAction $ current (drawSteps <$> dCx) <@ ePost
Create a text input for our step size
dStepSize <- RD._textInput_value <$> B.bsNumberInput "Step Size" "step-size" defStepSize
Add some buttons
eInc <- B.bsButton_ "+ Step" B.Primary eDec <- B.bsButton_ "- Step" B.Primary
eInc, eDec :: Event t ()
Keep track of the step size as it changes over time.
dStep <- R.foldDyn ($) (fromIntegral defStepSize) $ R.mergeWith (.) [ incSize <$> eStepSize eInc , decSize <$> eStepSize eDec ]
foldDyn :: (... t, ... m) => (a -> b -> b) -> b -> Event t a -> m (Dynamic t b)
eStepSize eTrigger = R.fmapMaybe (readMaybe . Text.unpack) $ R.current dStepSize <@ eTrigger
_ <- requestDomAction $ current (drawSteps <$> dStep <*> dCx) <@ ( ePost <> eStep )
drawSteps stp cap cx = do DOM_CR.clearRect cx 0 0 (fromIntegral size) (fromIntegral size) DOM_CR.beginPath cx let xy = [0,stp .. size] forM_ xy $ \x -> forM_ xy $ \y -> draw x y stp cap cx DOM_CR.stroke cx
Using custom elements in reflex-dom
is stringly
elAttr "rect" ("x" =: "30" <> "y" =: "40" <> "width" =: "100" <> "height" =: "200") blank
Text
valuesreflex-dom-svg
rect
, path
, circle
, …path
propertiesanimate
elementWhat was sad
... "rect" ("x" =: "30" <> "y" =: "40" <> "width" =: "100" <> "height" =: "200")
Becomes happy
data SVG_Rect = SVG_Rect { _svg_rect_pos_x :: Pos X , _svg_rect_pos_y :: Pos Y , _svg_rect_width :: Width , _svg_rect_height :: Height , _svg_rect_cornerRadius_x :: Maybe (CornerRadius X) , _svg_rect_cornerRadius_y :: Maybe (CornerRadius Y) }
To put a basic shape on the page
-- No child elements svgBasicDyn_ :: s -> (p -> Map Text Text) -> Dynamic t p -> m (SVGEl t s) -- Only allow a subset of SVG elements as possible child elements svgBasicDyn :: s -> (p -> Map Text Text) -> Dynamic t p -> Dynamic t (Map (CanBeNested s) (Map Text Text)) -> m (SVGEl t s)
Looks like
SVG.svgBasicDyn_ SVG.Rect -- Indicate we want a '<rect>' element SVG.makeRectProps -- Use the library provided function for handling properties dMyRectProps -- Provide a Dynamic of our SVG_Rect
We will use a polygon
for our squares
data SVG_Polygon = SVG_Polygon { _svg_polygon_start :: (Pos X, Pos Y) , _svg_polygon_path :: NonEmpty (Pos X, Pos Y) }
Functor
, Applicative
, Traversable
Given
newtype Poly = Poly { unPoly :: (PolyAttrs, SVG_Polygon) } deriving (Eq,Show) makeWrapped ''Poly
We can lens & traverse
our way to victory!
over (_Wrapped . _2) ( (SVGT.svg_polygon_start %~ addNoise seed scale) . (SVGT.svg_polygon_path . traverse %~ addNoise seed scale) )
reflex
provides additional functionality for collections
simpleList :: Dynamic t [v] -> (Dynamic t v -> m a) -> m (Dynamic t [a])
So if you have a list of polygons that will change over time…
dPolys :: Dynamic t (NonEmpty Poly) dPerlin :: Dynamic t (Poly -> Poly)
-- Polygons are kept in a 'NonEmpty' list so we always have something to draw RD.simpleList (NE.toList <$> dPolys) $ \dPoly -> -- Apply the shifting perlin noise function to our polygon SVG.svgBasicDyn_ SVG.Polygon makePolyProps (dPerlin <*> dPoly)
We have a few things to juggle:
rec (dSqCount, eSqCountChg) <- RD.divClass "sqr-inc-dec" $ do eIncSq <- B.bsButton_ "+ Sqr" B.Info _ <- RD.dynText $ (<> "# Squares") . tshow . unCount <$> dSqCount eDecSq <- B.bsButton_ "- Sqr" B.Info dSqrs <- RD.foldDyn ($) sqCount $ RD.mergeWith (.) [ incSqCount <$ eIncSq , decSqCount <$ eDecSq ]
dScaleInp <- RD.divClass "scale-slider" $ do RD.text "Scale" fmap (fmap realToFrac . RD._rangeInput_value) . RD.rangeInput $ B.rangeInpConf 0.0 "scale" & RD.rangeInputConfig_attributes . mapped %~ \m -> m & at "step" ?~ "0.0001" & at "min" ?~ "0.0" & at "max" ?~ "0.1"
dFoofen <- RD.holdDyn dScaleInp $ RD.leftmost [ dScaleInp <$ eOnButton , dScaleRange <$ eOnTick ]
Alright, so brace yourself, but what about…
We can have nicer things.
newtype WebGLM a = WebGLM { runWebGLM :: ExceptT Error JSM a } deriving ( Functor , Applicative , Monad #ifdef ghcjs_HOST_OS , MonadIO #else , MonadIO , MonadJSM #endif , MonadError Error ) liftGLM :: Either Error a -> WebGLM a liftGLM = either throwError pure
nb: MonadJSM
~ MonadIO
when building with GHCJS.
initShader :: GHCJS.GLenum -> Getter s Text -> s -> WebGLRenderingContext -> WebGLM WebGLShader initShader sType sL source cx = do
initProgram :: VertSrc -> FragSrc -> WebGLRenderingContext -> WebGLM WebGLProgram initProgram vs fs cx = do
Check for errors
ok <- GHCJS.liftJSM . GHCJS.fromJSValUnchecked =<< GLB.getShaderParameter cx (Just s) GLB.COMPILE_STATUS
ok <- GHCJS.liftJSM . GHCJS.fromJSValUnchecked =<< GLB.getProgramParameter cx (Just p) GLB.LINK_STATUS
Lean on abstractions to handle plumbing
vs' <- initShader GLB.VERTEX_SHADER (to unVertSrc) vs cx fs' <- initShader GLB.FRAGMENT_SHADER (to unFragSrc) fs cx GLB.attachShader cx (Just p) (Just vs') GLB.attachShader cx (Just p) (Just fs')
Event
Reflex helps out here too:
fanEither :: ... => Event t (Either a b) -> (Event t a, Event t b)
createGOL :: ... => WebGLRenderingContext -> m (Either Error GOL)
(eError, eGol) <- fmap R.fanEither . RD.requestDomAction $ R.current (createGOL <$> dCx) <@ ePost
eError :: Event t Error eGol :: Event t GOL
Switching
The FP way, narrow your focus/function to a smaller problem.
One to display the error
golError :: ... => Error -> m ()
One to run the orchestration for our WebGL
golRender :: ... => GOLInfo t -> StdGen -> GOL -> m ()
_ <- RD.widgetHold (RD.text "Nothing Ready Yet.") $ R.leftmost [ golError <$> eError , golRender (GOLInfo eReset eStepOnce dTick dCx) sGen <$> eDrawn ]
Let Reflex manage our GOL
record in a Dynamic
dGOL <- R.holdDyn gol' $ R.leftmost [ eStepRendered , eWasReset ]
Updated on a step or reset Event
eStepRendered :: Event t GOL eWasReset :: Event t GOL
step :: ... => WebGLRenderingContext -> GOL -> m GOL setInitialState :: ... => StdGen -> WebGLRenderingContext -> GOL -> m GOL
eStepRendered <- glRun step . R.switchDyn $ _golAuto golInfo eWasReset <- glRun (setInitialState sGen) $ _golReset golInfo
glRun f eGo = RD.requestDomAction $ R.current ( (\c -> f c >=> draw c) <$> _golCx golInfo <*> dGOL ) <@ eGo
golRender golInfo sGen gol' = mdo dGOL <- R.holdDyn gol' $ R.leftmost [ eStepRendered , eWasReset ] let glRun f eGo = RD.requestDomAction $ R.current ( (\c -> f c >=> draw c) <$> _golCx golInfo <*> dGOL ) <@ eGo eStepRendered <- glRun step . R.switchDyn $ _golAuto golInfo eWasReset <- glRun (setInitialState sGen) $ _golReset golInfo
ArrayBufferView
doesn't exist in JS
Snip of MDN for Canvas 2D fill
function
void ctx.fill([fillRule]); void ctx.fill(path[, fillRule]);
Type of fill
from ghcjs-dom
for 2D Canvas
fill :: ... => CanvasRenderingContext2D -> Maybe CanvasWindingRule -> m ()
From here, many good things can come.
Thank you. :)
…and so many more, please come and ask!