Skip to content

Instantly share code, notes, and snippets.

@busypeoples
Last active May 31, 2018 12:35
Show Gist options
  • Save busypeoples/7cc7acd575c1d8cbb143221e8bf27cfe to your computer and use it in GitHub Desktop.
Save busypeoples/7cc7acd575c1d8cbb143221e8bf27cfe to your computer and use it in GitHub Desktop.
Reason-Animation
open Sliders;
ReactDOMRe.renderToElementWithId(<Sliders />, "root");
/*
Animation library heavily influenced by https://github.com/mgold/elm-animation
Enables to decouple the actual calculations for an animation from
actually running the animation.
Enables to define animation between 2 values. Define any From/To values, this
library will keep track of the calculation depending on the time.
This enables us to build very low level blocks, which can further be refined
using easing functionalities, leaving the actual rendering to user land.
*/
type durationOrSpeed =
| Duration(int)
| Speed(float);
type animation = {
start: int,
delay: int,
dos: durationOrSpeed,
ramp: option(float),
ease: float => float,
from: float,
to_: float,
};
let defaultDuration = Duration(750);
let defaultEase = ease => (1.0 -. Js.Math.cos(Js.Math._PI *. ease)) /. 2.0;
let animation =
(
~start=0,
~delay=0,
~dos=defaultDuration,
~ramp=None,
~ease=defaultEase,
~from=0.0,
~to_=1.0,
(),
) => {
start,
delay,
dos,
ramp,
ease,
from,
to_,
};
let dur = (durationOrSpeed, from, to_) =>
switch (durationOrSpeed) {
| Duration(t) => t
| Speed(s) => int_of_float(Js.Math.abs_float(to_ -. from) /. s)
};
let spd = (durationOrSpeed, from, to_) =>
switch (durationOrSpeed) {
| Duration(t) => Js.Math.abs_float(to_ -. from) /. float_of_int(t)
| Speed(s) => s
};
let createAnimation = t => animation(~start=t, ());
let static = x => animation(~from=x, ~to_=x, ());
let clamp = (a, b, x) =>
if (x < a) {
a;
} else if (b <= x) {
b;
} else {
x;
};
let animate = (t, a) => {
let {start, delay, dos, ramp, from, to_, ease} = a;
let duration = dur(dos, from, to_);
let fr =
clamp(
0.0,
1.0,
float_of_int(t - start - delay) /. float_of_int(duration),
);
let eased = ease(fr);
let correction =
switch (ramp) {
| Some(vel) =>
let eased_ = defaultEase(fr);
let from_ = vel *. float_of_int(t - start);
from_ -. from_ *. eased_;
| None => 0.0
};
from +. (to_ -. from) *. eased +. correction;
};
let timeElapsed = (t, a) => Js.Math.max_int(t - (a.start + a.delay), 0);
let timeRemaining = (t, a) => {
let {dos, from, to_} = a;
let duration = dur(dos, from, to_);
Js.Math.max_int(a.start + a.delay + duration - t, 0);
};
let velocity = (t, a) => {
let backDiff = animate(t - 10, a);
let forwDiff = animate(t + 10, a);
(forwDiff -. backDiff) /. 20.0;
};
let getDuration = a => {
let {dos, from, to_} = a;
dur(dos, from, to_);
};
let getSpeed = a => {
let {dos, from, to_} = a;
spd(dos, from, to_);
};
let isStatic = ({from, to_}) => from === to_;
let isScheduled = (t, a) => t <= a.start + a.delay && ! isStatic(a);
let isRunning = (t, a) => {
let {start, delay, dos, from, to_} = a;
let duration = dur(dos, from, to_);
t > start + delay && t < start + delay + duration && ! isStatic(a);
};
let isDone = (t, a) => {
let {start, delay, dos, from, to_} = a;
let duration = dur(dos, from, to_);
isStatic(a) || t >= start + delay + duration;
};
let equals = (a, b) =>
a.start
+ a.delay == b.start
+ b.delay
&& a.from == b.from
&& a.to_ == b.to_
&& a.ramp == b.ramp
&& (
a.dos === b.dos
||
0.001 >= float_of_int(
dur(a.dos, a.from, a.to_) - dur(b.dos, b.from, b.to_),
)
)
&& List.filter(t => a.ease(t) !== b.ease(t), [0.1, 0.3, 0.7, 0.9])
|> (l => List.length(l) == 0);
/* undo : (Time, Animation) => Animation */
let undo = (t, a) => {
...a,
from: a.to_,
to_: a.from,
start: t,
delay: - timeRemaining(t, a),
ramp: None,
ease: t => 1.0 -. a.ease(1.0 -. t),
};
let retarget = (t, newTo, a) =>
if (newTo === a.to_) {
a;
} else if (isStatic(a)) {
{...a, start: t, to_: newTo, ramp: None};
} else if (isScheduled(t, a)) {
{...a, to_: newTo, ramp: None};
} else if (isDone(t, a)) {
{...a, start: t, delay: 0, from: a.to_, to_: newTo, ramp: None};
} else {
let vel = velocity(t, a);
let pos = animate(t, a);
let newDos =
switch (a.dos) {
| Speed(_s) => a.dos
| Duration(_d) => Speed(spd(a.dos, a.from, a.to_))
};
{
start: t,
delay: 0,
dos: newDos,
ramp: Some(vel),
ease: a.ease,
from: pos,
to_: newTo,
};
};
open ReasonAnimation;
type time = int;
type state = {
clock: time,
a1: animation,
a2: animation,
a3: animation,
initial: bool,
};
let stay = static(0.0);
type action =
| Tick(time)
| Click;
let slideLen = 500.0;
[@bs.val]
external requestAnimationFrame : (int => unit) => unit =
"requestAnimationFrame";
let component = ReasonReact.reducerComponent("Sliders");
let make = _children => {
...component,
initialState: () => {clock: 0, a1: stay, a2: stay, a3: stay, initial: true},
didMount: self => self.send(Tick(0)),
reducer: (action, state) =>
switch (action) {
| Tick(s) =>
ReasonReact.UpdateWithSideEffects(
{...state, clock: s},
(self => requestAnimationFrame(t => self.send(Tick(t)))),
)
| Click =>
let {initial, clock, a1, a2, a3} = state;
if (initial) {
let a =
animation(
~start=clock,
~from=0.0,
~to_=slideLen,
~dos=Duration(1200),
(),
);
ReasonReact.Update({...state, a1: a, a2: a, a3: a, initial: false});
} else {
let t = clock;
let dur = Duration(750);
ReasonReact.Update({
...state,
a1: animation(~start=t, ~from=a1.to_, ~to_=a1.from, ~dos=dur, ()),
a2:
animation(
~start=t,
~from=animate(t, a2),
~to_=a1.from,
~dos=dur,
(),
),
a3: retarget(t, a1.from, a3) |> (a => {...a, dos: dur}),
});
};
},
render: ({state, send}) => {
let w1 = animate(state.clock, state.a1);
let w2 = animate(state.clock, state.a2);
let w3 = animate(state.clock, state.a3);
let slider = w =>
<div
style=(
ReactDOMRe.Style.make(
~padding="0px",
~margin="0px",
~pointerEvents="none",
~width="550px",
~height="50px",
(),
)
)>
<div
style=(
ReactDOMRe.Style.make(
~padding="0px",
~margin="0px",
~width="550px",
~height="50px",
~backgroundColor="rgb(238, 238, 236)",
~position="absolute",
(),
)
)
/>
<div
style=(
ReactDOMRe.Style.make(
~padding="0px",
~margin="0px",
~width=string_of_float(Js.Math.round(w) +. 50.0) ++ "px",
~height="50px",
~position="absolute",
(),
)
)>
<div
style=(
ReactDOMRe.Style.make(
~padding="0px",
~margin="0px",
~width=string_of_float(Js.Math.round(w)) ++ "px",
~height="50px",
~float="left",
(),
)
)
/>
<div
style=(
ReactDOMRe.Style.make(
~padding="0px",
~margin="0px",
~width="50px",
~height="50px",
~float="left",
~backgroundColor="rgb(52, 101, 164)",
(),
)
)
/>
</div>
</div>;
<div>
<h3> (ReasonReact.string("elm-animation examples in ReasonML")) </h3>
<div>
<a
href="https://github.com/mgold/elm-animation/blob/master/examples/sliders.elm">
(ReasonReact.string("Find the original example here"))
</a>
</div>
<div>
(
ReasonReact.string(
"This is a demo of three different approaches to interrupted animation.\n Click the mouse rapidly.",
)
)
</div>
<div onClick=(_e => send(Click))>
(
ReasonReact.string(
"The first slider is very naive. When interrupted, it pretends the previous animation has already completed, and jumps to the other side only to return. Astoundingly, this is how CSS transitions still work.",
)
)
(slider(w1))
</div>
<div onClick=(_e => send(Click))>
(
ReasonReact.string(
"This slider will undo the current animation, instantly reversing its direction.",
)
)
(slider(w2))
</div>
<div onClick=(_e => send(Click))>
(
ReasonReact.string(
"This slider will smoothly decelerate and reverse.",
)
)
(slider(w3))
</div>
<div>
(
ReasonReact.string(
"Notice that all sliders reach their destination at the same time. The first slider is discontinuous is position; the second slider is discontinuous in velocity; the third slider is smooth.",
)
)
</div>
</div>;
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment