Inspired by Fran (by Conal Elliott) and "The Haskell School of Expression" (by Paul Hudak).
Github: https://github.com/ekozhura/fra (work in progress).
Let's define a type Transform
:
type transform =
| Translate(float, float)
| RenderImage(imageElement)
| Stretch(float)
and operations on that type will represent instructions for HTML5 canvas:
let drawPic = imageEl => RenderImage(imageEl);
let moveXY = (x, y) => Translate(x, y);
let stretch = x => Stretch(x);
To apply those operations on canvas we need a function runTransform
:
let runTransform = (ctx, transform) => {
switch (transform) {
| Translate(x, y) => ctx->translate(x, y)
| RenderImage(el) => ctx->drawImage(el, 0., 0.)
| Stretch(f) => ctx->scale(f, f)
};
};
where ctx
is a 2d rendering context of a canvas element.
To combine transforms let's add another type constructor to our transform
type:
type transform =
...
| ComposedTransform(transform, transform)
and function andThen
:
let andThen = (transformA, transformB) => ComposedTransform(transformA, transformB);
to run a composed transform, inside runTransform
we run both transforms one after another:
...
| ComposedTransform(transformA, transformB) =>
runTransform(ctx, transformA);
runTransform(ctx, transformB);
...
draw
function runs a composed transform:
let draw = transforms => {
let ctx = elem->getContext;
runTransform(ctx, transforms);
};
An example of graphics:
let transforms = drawPic(imageEl) |> andThen(moveXY(120., 120.,));
draw(transforms);
Animation is a change of transform parameters over time. Let's first define a couple of timing functions:
let wiggle = t => 60. *. Js_math.sin(2. *. Js_math._PI *. 2. *. t /. 6000.);
let waggle = t => 60. *. Js_math.cos(2. *. Js_math._PI *. 2. *. t /. 6000.);
Initial transform will be rewritten as a function of time
:
let transforms = t => drawPic(imageEl) |> andThen(moveXY(wiggle(t), waggle(t)));
And drawAnimation
function:
let rec drawAnimation = (t) => {
let ctx = elem->getContext;
ctx->save;
runTransform(ctx, transforms(t));
ctx->restore;
requestAnimationFrame(drawAnimation);
};
The idea of time-varying value can be expressed by the new type Behavior
type time = float;
type behavior('a) =
| Behavior(time => 'a);
Then we create helper functions, which lift a value to a Behavior
:
let lift0 = x => Behavior(_ => x);
let lift1 = (fn, Behavior(a)) => Behavior(t => fn(a(t)));
let lift2 = (fn, Behavior(a), Behavior(b)) => Behavior(t => fn(a(t), b(t)));
let lift3 = (fn, Behavior(a), Behavior(b), Behavior(c)) => Behavior(t => fn(a(t), b(t), c(t)));
In order to use our transforms in animation, we need to lift them to behaviors. As if we created for each transform a corresponding function from t
(time) to transform
.
So to run transformation over time we can define a new set of operations:
let constB = lift0;
let moveXYB = lift2(moveXY);
let stretchB = lift1(stretch);
let andThenB = lift2(andThen);
For example constB
simply lifts a value to a behavior
, moveXYB
- lifts a function moveXY
to a function of two arguments of type behavior
etc.
This is how the drawAnimation
function will look like:
let wiggleB = Behavior(wiggle);
let waggleB = Behavior(waggle);
let transforms = const(drawPic(imageEl)) |> andThenB(moveXYB(wiggleB, waggleB));
let rec drawAnimation = (t) => {
let ctx = elem->getContext;
ctx->save;
switch (transforms) {
| Behavior(transform) => runTransform(ctx, transform(t))
};
ctx->restore;
requestAnimationFrame(drawAnimation);
};