Skip to content

Commit

Permalink
Scroll bounce (#315)
Browse files Browse the repository at this point in the history
* Adding `tween` to Animation, start changing types towards functional API

* Adding Chain module and first buggy bouncing back animation

* cleanup

* add cubicbezier

* remove logs

* Making chains a list. Allowing to interrupt bouncing animations if scrolling on the opposite direction.

* Using rebex and fixing timing for chained animations. improving the general feel.

* Fix tests

* Point rebez to github

* remove link-dev

* esy updates

* Adding bounce prop and Environment.os

* refmt

* Fix platform detection on Windows

* Formatting

* Fix line ordering
  • Loading branch information
jchavarri authored and bryphe committed Feb 13, 2019
1 parent ed0f2d9 commit 70b772a
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 68 deletions.
33 changes: 32 additions & 1 deletion esy.lock/index.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"checksum": "00f17c2f78e59e271f094f9ffc9c5d07",
"checksum": "059efe49cf4f9d53e2c71dfa539a48d2",
"root": "revery@link-dev:./package.json",
"node": {
"revery@link-dev:./package.json": {
Expand All @@ -13,6 +13,7 @@
},
"overrides": [],
"dependencies": [
"rebez@github:jchavarri/rebez#46cbc183@d41d8cd9",
"reason-glfw@3.2.1015@d41d8cd9",
"reason-gl-matrix@0.9.9302@d41d8cd9",
"reason-fontkit@2.2.0@d41d8cd9", "ocaml@4.7.1004@d41d8cd9",
Expand Down Expand Up @@ -70,6 +71,22 @@
],
"devDependencies": []
},
"rebez@github:jchavarri/rebez#46cbc183@d41d8cd9": {
"id": "rebez@github:jchavarri/rebez#46cbc183@d41d8cd9",
"name": "rebez",
"version": "github:jchavarri/rebez#46cbc183",
"source": {
"type": "install",
"source": [ "github:jchavarri/rebez#46cbc183" ]
},
"overrides": [],
"dependencies": [
"refmterr@3.1.10@d41d8cd9", "pesy@0.4.1@d41d8cd9",
"ocaml@4.7.1004@d41d8cd9", "@opam/dune@opam:1.7.0@81948d64",
"@esy-ocaml/reason@3.4.0@d41d8cd9"
],
"devDependencies": []
},
"reason-glfw@3.2.1015@d41d8cd9": {
"id": "reason-glfw@3.2.1015@d41d8cd9",
"name": "reason-glfw",
Expand Down Expand Up @@ -132,6 +149,20 @@
],
"devDependencies": []
},
"pesy@0.4.1@d41d8cd9": {
"id": "pesy@0.4.1@d41d8cd9",
"name": "pesy",
"version": "0.4.1",
"source": {
"type": "install",
"source": [
"archive:https://registry.npmjs.org/pesy/-/pesy-0.4.1.tgz#sha1:37b3faccb3ecdb37f4bf3d95d04ffbd2633247af"
]
},
"overrides": [],
"dependencies": [],
"devDependencies": []
},
"ocaml@4.7.1004@d41d8cd9": {
"id": "ocaml@4.7.1004@d41d8cd9",
"name": "ocaml",
Expand Down
4 changes: 1 addition & 3 deletions examples/Examples.re
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,7 @@ let init = app => {
let example = exampleRender(win);

<View
onMouseWheel={evt =>
print_endline("onMouseWheel: " ++ string_of_float(evt.deltaY))
}
onMouseWheel={_evt => ()}
style=Style.[
position(`Absolute),
justifyContent(`Center),
Expand Down
22 changes: 18 additions & 4 deletions examples/ScrollView.re
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,23 @@ module Sample = {
let component = React.component("Sample");

let createElement = (~children as _, ()) =>
component((_slots: React.Hooks.empty) =>
component(slots => {
let (bounce, setBounce, _slots: React.Hooks.empty) =
React.Hooks.state(true, slots);
<View style=containerStyle>
<ScrollView style=outerBox>
<Text
text="Bounce"
style=Style.[
marginBottom(10),
fontFamily("Roboto-Regular.ttf"),
fontSize(20),
]
/>
<Checkbox
onChange={checked => setBounce(checked)}
style=Style.[marginBottom(10)]
/>
<ScrollView style=outerBox bounce>
<Image
src="outrun-logo.png"
style=Style.[width(512), height(256)]
Expand All @@ -44,8 +58,8 @@ module Sample = {
style=Style.[width(512), height(256)]
/>
</ScrollView>
</View>
);
</View>;
});
};

let render = () => <Sample />;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"reason-glfw": "^3.2.1015",
"reason-fontkit": "^2.2.0",
"reason-gl-matrix": "^0.9.9302",
"rebez": "*",
"@opam/color": "^0.2.0",
"@opam/js_of_ocaml": "*",
"@opam/js_of_ocaml-compiler": "*",
Expand All @@ -44,6 +45,7 @@
"flex": "^1.2.2"
},
"resolutions": {
"rebez": "github:jchavarri/rebez#46cbc183",
"@opam/cmdliner": "1.0.2",
"@opam/js_of_ocaml": "github:ocsigen/js_of_ocaml:js_of_ocaml.opam#db257ce",
"@opam/js_of_ocaml-compiler": "github:ocsigen/js_of_ocaml:js_of_ocaml-compiler.opam#db257ce",
Expand Down
21 changes: 21 additions & 0 deletions src/Core/Environment.re
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,24 @@ let getExecutingDirectory = () =>
isNative ? Filename.dirname(Sys.argv[0]) ++ Filename.dir_sep : "";

let getWorkingDirectory = () => Sys.getcwd();

type os =
| Windows
| Mac
| Linux
| Unknown;

let os = {
switch (Sys.os_type) {
| "Win32" => Windows
| _ =>
let ic = Unix.open_process_in("uname");
let uname = input_line(ic);
let _ = close_in(ic);
switch (uname) {
| "Darwin" => Mac
| "Linux" => Linux
| _ => Unknown
};
};
};
133 changes: 99 additions & 34 deletions src/UI/Animation.re
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ module type AnimationTicker = {
let onTick: Event.t(Time.t);
};

let optCall = (opt, param) =>
switch (opt) {
| Some(f) => f(param)
| None => ()
};

module Make = (AnimationTickerImpl: AnimationTicker) => {
type animationValue = {mutable current: float};

Expand All @@ -25,7 +31,18 @@ module Make = (AnimationTickerImpl: AnimationTicker) => {
easing: float => float,
};

let activeAnimations: ref(list(animation)) = ref([]);
type activeAnimation = {
animation,
update: option(float => unit),
complete: option(unit => unit),
};

type playback = {
pause: unit => unit,
stop: unit => unit,
};

let activeAnimations: ref(list(activeAnimation)) = ref([]);

type animationOptions = {
duration: Time.t,
Expand All @@ -35,26 +52,15 @@ module Make = (AnimationTickerImpl: AnimationTicker) => {
easing: float => float,
};

/*y=u0(1−t)3+3u1(1−t)2t+3u2(1−t)t2+u3t3
u0, u3 = 0,1; t in [0,1]
equivalent to a normal bezier curve where p1x = 1/3 and p2x = 2/3
*/
let genOneDimBezierFunc = (u1: float, u2: float) => {
let a = 3. *. (u1 -. u2) +. 1.;
let b = (-6.) *. u1 +. 3. *. u2;
let c = 3. *. u1;
let ret = (t: float) => {
let t2 = t ** 2.;
a *. t2 *. t +. b *. t2 +. c *. t;
};
ret;
};
let linear = (t: float) => t;
let quadratic = (t: float) => t *. t;
let cubic = (t: float) => t *. t *. t;
let easeIn = genOneDimBezierFunc(0., 0.45);
let easeOut = genOneDimBezierFunc(0.45, 1.);
let easeInOut = genOneDimBezierFunc(0., 1.);
let cubicBezier = Rebez.make;
// From https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function#Keywords_for_common_cubic-bezier_timing_functions
let ease = cubicBezier(0.25, 0.1, 0.25, 1.0);
let easeIn = cubicBezier(0.42, 0.0, 1.0, 1.0);
let easeOut = cubicBezier(0.0, 0.0, 0.58, 1.0);
let easeInOut = cubicBezier(0.42, 0.0, 0.58, 1.0);

let floatValue = (v: float) => {
let ret = {current: v};
Expand All @@ -67,28 +73,31 @@ module Make = (AnimationTickerImpl: AnimationTicker) => {
(clock -. adjustedStart) /. (endTime -. adjustedStart);
};

let hasStarted = (clock: float, anim: animation) => {
let t = getLocalTime(clock, anim);
let hasStarted = (clock: float, {animation, _}) => {
let t = getLocalTime(clock, animation);
t > 0.;
};

let isComplete = (clock: float, anim: animation) => {
let isComplete = (clock: float, {animation: anim, _}) => {
let t = getLocalTime(clock, anim);
t > 1. && !anim.repeat;
};

let tickAnimation = (clock: float, anim: animation) => {
let tickAnimation = (clock: float, {animation: anim, update, complete}) => {
let t = anim.easing(getLocalTime(clock, anim));

/* If the anim is set to repeat and the time has expired, restart */
if (anim.repeat && t > 1.) {
/* reset */

anim.startTime = anim.startTime +. anim.delay +. anim.duration;
let newT = getLocalTime(clock, anim);
anim.value.current = interpolate(newT, anim.startValue, anim.toValue);
if (t >= 1.) {
if (anim.repeat) {
/* If the anim is set to repeat and the time has expired, restart */
anim.startTime = anim.startTime +. anim.delay +. anim.duration;
let newT = getLocalTime(clock, anim);
anim.value.current = interpolate(newT, anim.startValue, anim.toValue);
} else {
optCall(complete, ());
};
} else {
anim.value.current = interpolate(t, anim.startValue, anim.toValue);
optCall(update, anim.value.current);
};
};

Expand All @@ -100,9 +109,9 @@ module Make = (AnimationTickerImpl: AnimationTicker) => {
List.length(anims) > 0;
};

let start =
let tween =
(animationValue: animationValue, animationOptions: animationOptions) => {
let animation: animation = {
let animation = {
id: AnimationId.getUniqueId(),
delay: Time.to_float_seconds(animationOptions.delay),
duration: Time.to_float_seconds(animationOptions.duration),
Expand All @@ -113,13 +122,69 @@ module Make = (AnimationTickerImpl: AnimationTicker) => {
startValue: animationValue.current,
easing: animationOptions.easing,
};

activeAnimations := List.append([animation], activeAnimations^);
animation;
};

let start = (~update=?, ~complete=?, animation) => {
let activeAnimation = {animation, update, complete};
animation.startTime = Time.to_float_seconds(AnimationTickerImpl.time());
activeAnimations := List.append([activeAnimation], activeAnimations^);
let removeAnimation = l =>
List.filter(({animation: a, _}) => a.id !== animation.id, l);
let playback = {
pause: () => {
activeAnimations := removeAnimation(activeAnimations^);
},
stop: () => {
animation.value.current = animation.startValue;
activeAnimations := removeAnimation(activeAnimations^);
},
};
playback;
};

module Chain = {
type t = {animations: list(animation)};
let make = animation => {animations: [animation]};
let add = (animation, {animations: l}) => {
animations: List.cons(animation, l),
};
let start = (~update=?, ~complete=?, {animations: l}) => {
let currentPlayback = ref(None);
let rec runAnimation = (animations, index) => {
switch (animations) {
| [a, ...xl] =>
let playback =
a |> start(~update?, ~complete=() => runAnimation(xl, index + 1));
currentPlayback := Some(playback);
| [] => optCall(complete, ())
};
};
runAnimation(List.rev(l), 0);
let playback = {
pause: () => {
switch (currentPlayback^) {
| Some(p) => p.pause()
| None => ()
};
},
stop: () => {
switch (currentPlayback^) {
| Some(p) => p.stop()
| None => ()
};
},
};
playback;
};
};

let cancel = (anim: animation) =>
activeAnimations := List.filter(a => a.id !== anim.id, activeAnimations^);
activeAnimations :=
List.filter(
({animation: a, _}) => a.id !== anim.id,
activeAnimations^,
);

let cancelAll = () => activeAnimations := [];

Expand Down
8 changes: 4 additions & 4 deletions src/UI/Hooks.re
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/* Hooks specific to Revery */
open Animated;

let animation =
(v: Animated.animationValue, opts: Animated.animationOptions, slots) => {
let animation = (v: animationValue, opts: animationOptions, slots) => {
let (currentV, _set, slots) = UiReact.Hooks.state(v, slots);

let slots =
UiReact.Hooks.effect(
OnMount,
() => {
let anim = Animated.start(v, opts);
let {stop, _} = tween(v, opts) |> start;

Some(() => Animated.cancel(anim));
Some(() => stop());
},
slots,
);
Expand Down
2 changes: 1 addition & 1 deletion src/UI/dune
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
(name Revery_UI)
(public_name Revery_UI)
(preprocess (pps lwt_ppx))
(libraries brisk-reconciler lwt lwt.unix reglfw flex fontkit Revery_Core Revery_Shaders Revery_Geometry Revery_Math))
(libraries brisk-reconciler lwt lwt.unix reglfw flex fontkit rebez.lib Revery_Core Revery_Shaders Revery_Geometry Revery_Math))

0 comments on commit 70b772a

Please sign in to comment.