Skip to content

Instantly share code, notes, and snippets.

@ekozhura
Last active September 18, 2018 13:15
Show Gist options
  • Save ekozhura/2c3558e487e5a4736c1aa16a1e26c09c to your computer and use it in GitHub Desktop.
Save ekozhura/2c3558e487e5a4736c1aa16a1e26c09c to your computer and use it in GitHub Desktop.
Notes on FRA implementation

Notes on FRA implementation (functional reactive animation)

Inspired by Fran (by Conal Elliott) and "The Haskell School of Expression" (by Paul Hudak).

Github: https://github.com/ekozhura/fra (work in progress).

Transform type

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);

Create animation

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);
};

Behavior type

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);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment