Skip to content

Commit

Permalink
FLIP technique based element transition animations. Currently applied…
Browse files Browse the repository at this point in the history
… to the caret
  • Loading branch information
disconcision committed Jan 5, 2025
1 parent 63010cf commit beb0dc9
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 20 deletions.
130 changes: 130 additions & 0 deletions src/haz3lcore/Animate.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
open Util;
open Js_of_ocaml;

/* Position & dimensions for a DOM element */
[@deriving (show({with_path: false}), sexp, yojson)]
type box = {
top: int,
left: int,
height: float,
width: float,
};

/* Options for CSS Animations API */
type options = {
duration: int,
easing: string,
};

/* CSS property-value pairs as strings */
type keyframe = (string, string);

/* Specify a transition for an element */
type transition = {
id: string,
options,
animation: option(box) => list(keyframe),
};

type transition_internal = {
id: string,
options,
animation: option(box) => list(keyframe),
box: option(box),
};

let tracked_elems: ref(list(transition_internal)) = ref([]);

let animate =
(id: string, keyframes: list((string, string)), options: options): unit => {
let elem = JsUtil.get_elem_by_id(id);

let keyframe_objs =
keyframes
|> List.map(((prop, value)) =>
Js.Unsafe.obj([|(prop, Js.Unsafe.inject(Js.string(value)))|])
)
|> Array.of_list
|> Js.array;

let options_obj =
[
("duration", Js.Unsafe.inject(options.duration)),
("easing", Js.Unsafe.inject(Js.string(options.easing))),
]
|> Array.of_list
|> Js.Unsafe.obj;

Js.Unsafe.meth_call(
elem,
"animate",
[|Js.Unsafe.inject(keyframe_objs), Js.Unsafe.inject(options_obj)|],
);
};

let box_of = (elem: Js.t(Dom_html.element)): box => {
let container_rect = elem##getBoundingClientRect;
{
top: int_of_float(container_rect##.top),
left: int_of_float(container_rect##.left),
height: Js.Optdef.get(container_rect##.height, _ => (-1.0)),
width: Js.Optdef.get(container_rect##.width, _ => (-1.0)),
};
};

let get_box = (id: string): option(box) =>
switch (JsUtil.get_elem_by_id_opt(id)) {
| Some(elem) => Some(box_of(elem))
| None => None
};

let delta_box = (init: box, final: box): box => {
left: final.left - init.left,
top: final.top - init.top,
width: final.width -. init.width,
height: final.height -. init.height,
};

let delta_box_opt = (init: option(box), final: option(box)): option(box) =>
switch (final, init) {
| (Some(final), Some(init)) => Some(delta_box(init, final))
| _ => None
};

let go = (): unit =>
if (tracked_elems^ != []) {
tracked_elems^
|> List.iter(({id, box, options, animation}) =>
animate(id, animation(delta_box_opt(get_box(id), box)), options)
);
tracked_elems := [];
};

let setup = (transitions: list(transition)): unit => {
tracked_elems :=
List.map(
({id, options, animation}: transition) =>
{id, box: get_box(id), options, animation},
transitions,
);
};

module Keyframes = {
let transform_translate = (top: int, left: int) => (
"transform",
Printf.sprintf("translate(%dpx, %dpx)", left, top),
);
let transform_scale_uniform = (scale: float) => (
"transform",
Printf.sprintf("scale(%f, %f)", scale, scale),
);
let translate = (delta: option(box)): list(keyframe) =>
switch (delta) {
| None =>
// Scale up newly inserted elements
[transform_scale_uniform(0.0), transform_scale_uniform(1.0)]
| Some({left, top, _}) =>
// Translate elements that exist in both states
[transform_translate(top, left), transform_translate(0, 0)]
};
};
1 change: 0 additions & 1 deletion src/haz3lcore/tiles/Base.re
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
open Util;
open ProjectorShape;

/* The different kinds of projector. New projectors
* types need to be registered here in order to be
Expand Down
12 changes: 11 additions & 1 deletion src/haz3lcore/zipper/action/Perform.re
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,17 @@ let go_z =
z,
)
| Move(d) =>
Move.go(d, z) |> Result.of_option(~error=Action.Failure.Cant_move)
Animate.setup([
{
id: "caret",
animation: Animate.Keyframes.translate,
options: {
duration: 125,
easing: "cubic-bezier(0.16, 1, 0.3, 1)",
},
},
]);
Move.go(d, z) |> Result.of_option(~error=Action.Failure.Cant_move);
| Jump(jump_target) =>
(
switch (jump_target) {
Expand Down
31 changes: 25 additions & 6 deletions src/haz3lcore/zipper/projectors/CardProj.re
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ module Singleton = {
module CardInHand = {
let view =
(
_elem_ids,
mode,
parent,
local: action => Ui_effect.t(unit),
Expand All @@ -455,7 +456,10 @@ module CardInHand = {
| Flipped => local(SetMode(Show))
| Show => local(SetMode(Choose))
}
| _ => Effect.Ignore
| _ =>
// Animate.setup(elem_ids);
// local(SetMode(Flipped));
Effect.Ignore
};

Node.div(
Expand Down Expand Up @@ -483,10 +487,21 @@ module CardInHand = {
};
};

let of_id = (id: Id.t) =>
"id" ++ (id |> Id.to_string |> String.sub(_, 0, 8));
// return a list of strings "card-index-<index>" for cards in hand
let hand_elem_ids = (id, hand: hand): list(string) =>
List.mapi(
(i, _) => of_id(id) ++ "card-index-" ++ string_of_int(i),
hand,
);

module Hand = {
// a card, but each subsequent card should be absoluted positioned 20px to the right of the last and higher in z-index:
let card_wrapper =
(
id,
elem_ids,
mode,
parent: external_action => Ui_effect.t(unit),
local: action => Ui_effect.t(unit),
Expand All @@ -497,23 +512,27 @@ module Hand = {
: Node.t =>
Node.div(
~attrs=[
Attr.id(of_id(id) ++ "card-index-" ++ string_of_int(index)),
Attr.class_("card-wrapper"),
Attr.create(
"style",
Printf.sprintf(
"position: absolute; left: %dpx; z-index: %d;",
index * 8,
mode == Flipped ? 0 : index * 8,
100 + index,
),
),
],
[CardInHand.view(mode, parent, local, sort, card)],
[CardInHand.view(elem_ids, mode, parent, local, sort, card)],
);

let view = (mode, parent, local, sort: Sort.t, hand: hand): Node.t => {
let view = (id, mode, parent, local, sort: Sort.t, hand: hand): Node.t => {
Node.div(
~attrs=[Attr.classes(["hand", Sort.show(sort)])],
List.mapi(card_wrapper(mode, parent, local, sort), hand),
List.mapi(
card_wrapper(id, hand_elem_ids(id, hand), mode, parent, local, sort),
hand,
),
);
};
};
Expand Down Expand Up @@ -553,7 +572,7 @@ module M: Projector = {
| (sort, Card(card)) =>
Singleton.view(model.mode, parent, local, to_sort(sort), card)
| (sort, Hand(hand)) =>
Hand.view(model.mode, parent, local, to_sort(sort), hand)
Hand.view(info.id, model.mode, parent, local, to_sort(sort), hand)
};
};
let focus = _ => ();
Expand Down
7 changes: 5 additions & 2 deletions src/haz3lweb/Main.re
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,14 @@ let start = {
// Triggers after every update
let after_display = {
Bonsai.Effect.of_sync_fun(
() =>
() => {
if (scroll_to_caret.contents) {
scroll_to_caret := false;
JsUtil.scroll_cursor_into_view_if_needed();
},
};
Haz3lcore.Animate.go();
();
},
(),
);
};
Expand Down
4 changes: 2 additions & 2 deletions src/haz3lweb/www/style/projectors/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
@import "cards.css";

/* Turn off caret when a projector is focused */
#caret:has(~ .projectors .projector *:focus) {
display: none;
#caret:has(~ .projectors .projector *:focus) .caret-path {
fill: #0000;
}

/* Default projector styles */
Expand Down
4 changes: 3 additions & 1 deletion src/haz3lweb/www/style/projectors/cards.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
}

/* Turn off caret when a block projector is focused */
#caret:has(~ .projectors .projector.card.indicated),
#caret:has(~ .projectors .projector.card.indicated) .caret-path {
fill: #0000;
}
.indication:has(~ .projectors .projector.card.indicated) {
display: none;
}
Expand Down
19 changes: 12 additions & 7 deletions src/util/JsUtil.re
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ open Virtual_dom.Vdom;

let get_elem_by_id = id => {
let doc = Dom_html.document;
Js.Opt.get(
doc##getElementById(Js.string(id)),
() => {
print_endline(id);
assert(false);
},
);
Js.Opt.get(doc##getElementById(Js.string(id)), () => {assert(false)});
};

let get_elem_by_id_opt = id =>
switch (get_elem_by_id(id)) {
| exception _ => None
| e => Some(e)
};

let get_elem_by_selector = selector => {
let doc = Dom_html.document;
Js.Opt.get(
Expand All @@ -23,6 +23,11 @@ let get_elem_by_selector = selector => {
);
};

let request_frame = kont => {
let _ = Dom_html.window##requestAnimationFrame(Js.wrap_callback(kont));
();
};

let get_child_with_class = (element: Js.t(Dom_html.element), className) => {
let rec loop = (sibling: Js.t(Dom_html.element)) =>
if (Js.to_bool(sibling##.classList##contains(Js.string(className)))) {
Expand Down

0 comments on commit beb0dc9

Please sign in to comment.