Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node searcher zoom & edited node growth/shrink animation #3327

Merged
merged 18 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#### Visual Environment

- [Node Searcher preserves its zoom factor.][3327] The visible size of the node
searcher and edited node is now fixed. It simplifies node editing on
non-standard zoom levels.
- [Nodes can be added to the graph by clicking (+) button on the screen][3278].
The button is in the bottom-left corner. Node is added at the center or pushed
down if the center is already occupied by nodes.
Expand Down Expand Up @@ -108,6 +111,7 @@
[3317]: https://github.com/enso-org/enso/pull/3317
[3318]: https://github.com/enso-org/enso/pull/3318
[3324]: https://github.com/enso-org/enso/pull/3324
[3327]: https://github.com/enso-org/enso/pull/3327
[3339]: https://github.com/enso-org/enso/pull/3339

#### Enso Compiler
Expand Down
41 changes: 41 additions & 0 deletions app/gui/view/graph-editor/src/component/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use ensogl::animation::delayed::DelayedAnimation;
use ensogl::application::Application;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::scene::Layer;
use ensogl::Animation;
use ensogl_component::shadow;
use ensogl_component::text;
Expand All @@ -38,6 +39,7 @@ pub mod action_bar;
#[warn(missing_docs)]
pub mod error;
pub mod expression;
pub mod growth_animation;
pub mod input;
pub mod output;
#[warn(missing_docs)]
Expand Down Expand Up @@ -537,6 +539,45 @@ impl NodeModel {
self
}

fn set_layers(&self, layer: &Layer, text_layer: &Layer, action_bar_layer: &Layer) {
layer.add_exclusive(&self.display_object);
action_bar_layer.add_exclusive(&self.action_bar);
self.output.set_label_layer(text_layer);
self.input.set_label_layer(text_layer);
self.profiling_label.set_label_layer(text_layer);
self.comment.add_to_scene_layer(text_layer);
}

/// Move all sub-components to `edited_node` layer.
///
/// A simple [`Layer::add_exclusive`] wouldn't work because text rendering in ensogl uses a
/// separate layer management API.
///
/// `action_bar` is moved to the `edited_node` layer as well, though normally it lives on a
/// separate `above_nodes` layer, unlike every other node component.
pub fn move_to_edited_node_layer(&self) {
let scene = &self.app.display.default_scene;
let layer = &scene.layers.edited_node;
let text_layer = &scene.layers.edited_node_text;
let action_bar_layer = &scene.layers.edited_node;
self.set_layers(layer, text_layer, action_bar_layer);
}

/// Move all sub-components to `main` layer.
///
/// A simple [`Layer::add_exclusive`] wouldn't work because text rendering in ensogl uses a
/// separate layer management API.
///
/// `action_bar` is handled separately, as it uses `above_nodes` scene layer unlike any other
/// node component.
pub fn move_to_main_layer(&self) {
let scene = &self.app.display.default_scene;
let layer = &scene.layers.main;
let text_layer = &scene.layers.label;
let action_bar_layer = &scene.layers.above_nodes;
self.set_layers(layer, text_layer, action_bar_layer);
}

#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
pub fn width(&self) -> f32 {
self.input.width.value()
Expand Down
125 changes: 125 additions & 0 deletions app/gui/view/graph-editor/src/component/node/growth_animation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Edited node growth/shrink animation implementation.
//!
//! When the user starts editing of the node - it smoothly growth in size to match the 1.0x zoom
//! factor size. After editing is finished, the node smothly shrinks to its original size. This is
//! implemented by using a separate `edited_node` camera that is moved in synchronization with
//! `node_searcher` camera.

use ensogl::prelude::*;

use crate::GraphEditorModelWithNetwork;
use crate::NodeId;
use enso_frp as frp;
use ensogl::animation::easing::EndStatus::Normal;
use ensogl::display::Scene;
use ensogl::Animation;
use ensogl::Easing;

/// Describes the "speed" of growth/shrink animation.
///
/// To determine the duration of the blending animation, we divide the length of the camera path by
/// this value. This is primarily used to move the edited node back to the `main` layer once editing
/// is done. If the camera is already at its destination – the duration would be close to zero, so
/// we would immediately change the layer of the node. If the camera needs to travel a lot - we
/// increase the animation duration proportionally so that the layer would be changed later.
///
/// The exact value is selected empirically. The maximum camera travel distance is about 9000.0
/// units, so our coefficient determines the maximum animation duration as 600 ms.
const ANIMATION_LENGTH_COEFFIENT: f32 = 15.0;

/// Initialize edited node growth/shrink animator. It would handle scene layer change for the edited
/// node as well.
pub fn initialize_edited_node_animator(
model: &GraphEditorModelWithNetwork,
frp: &crate::Frp,
scene: &Scene,
) {
let network = &frp.network;
let out = &frp.output;
let searcher_cam = scene.layers.node_searcher.camera();
let edited_node_cam = scene.layers.edited_node.camera();
let main_cam = scene.layers.main.camera();

let growth_animation = Animation::new(network);
let animation_blending = Easing::new(network);

frp::extend! { network
let searcher_cam_frp = searcher_cam.frp();
let main_cam_frp = main_cam.frp();


// === Starting node editing ===

previous_edited_node <- out.node_editing_started.previous();
_eval <- out.node_editing_started.map2(&previous_edited_node, f!([model] (current, previous) {
model.move_node_to_main_layer(*previous);
model.move_node_to_edited_node_layer(*current);
}));


// === Edited node camera position animation ===

is_growing <- bool(&out.node_editing_finished, &out.node_editing_started);
edited_node_cam_target <- switch(&is_growing, &main_cam_frp.position, &searcher_cam_frp.position);
growth_animation.target <+ edited_node_cam_target;

camera_path_length <- all_with
(&growth_animation.value, &growth_animation.target, |v, t| (v - t).magnitude());
on_node_editing_start_or_finish <- any(&out.node_editing_started, &out.node_editing_finished);
start_animation_blending <- camera_path_length.sample(&on_node_editing_start_or_finish);
eval start_animation_blending ((length) {
animation_blending.set_duration(*length / ANIMATION_LENGTH_COEFFIENT);
animation_blending.stop_and_rewind(0.0);
animation_blending.target(1.0);
});

// We want to:
// 1. Smothly animate edited node camera from `main_cam` position to `searcher_cam` position when we
// start/finish node editing so that the edited node "grows" or "shrinks" to reach the correct
// visible size. This is `growth_animation`.
// 2. Keep `searcher_cam` and `edited_node_cam` at the same position everywhen else so that the
// searcher and the edited node are at the same visible position at all times. This is
// `edited_node_cam_target`.
//
// Enabling/disabling "follow me" mode for the edited node camera is hard
// to implement and leads to serious visualization lags. To avoid that, we blend these two
// components together using `animation_blending` as a weight coefficient. This allows a very smooth
// transition between "follow me" mode and node growth/shrink animation.
edited_node_cam_position <- all_with3
(&edited_node_cam_target, &growth_animation.value, &animation_blending.value, |target,animation,weight| {
let weight = Vector3::from_element(*weight);
let inv_weight = Vector3::from_element(1.0) - weight;
target.component_mul(&weight) + animation.component_mul(&inv_weight)
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we format such lines this way instead, please?


edited_node_cam_position <- all_with3
    (&edited_node_cam_target, &growth_animation.value, &animation_blending.value, |target,animation,weight| {
        let weight = Vector3::from_element(*weight);
        let inv_weight = Vector3::from_element(1.0) - weight;
        target.component_mul(&weight) + animation.component_mul(&inv_weight)
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, done

eval edited_node_cam_position([edited_node_cam] (pos) edited_node_cam.set_position(*pos));


// === Finishing shrinking animation ===

on_animation_end <- animation_blending.on_end.filter(|end_status| *end_status == Normal);
shrinking_finished <- on_animation_end.gate_not(&is_growing);
node_that_finished_editing <- out.node_editing_started.sample(&shrinking_finished);
eval node_that_finished_editing ([model] (id) {
model.move_node_to_main_layer(*id);
});
}
}


// === Helpers ===

impl GraphEditorModelWithNetwork {
/// Move node to the `edited_node` scene layer, so that it is rendered by the separate camera.
fn move_node_to_edited_node_layer(&self, node_id: NodeId) {
if let Some(node) = self.nodes.get_cloned(&node_id) {
node.model.move_to_edited_node_layer();
}
}

/// Move node to the `main` scene layer, so that it is rendered by the main camera.
fn move_node_to_main_layer(&self, node_id: NodeId) {
if let Some(node) = self.nodes.get_cloned(&node_id) {
node.model.move_to_main_layer();
}
}
}
9 changes: 9 additions & 0 deletions app/gui/view/graph-editor/src/component/node/input/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ impl Model {
self
}

fn set_label_layer(&self, layer: &display::scene::Layer) {
self.label.add_to_scene_layer(layer);
}

fn scene(&self) -> &Scene {
&self.app.display.default_scene
}
Expand Down Expand Up @@ -475,6 +479,11 @@ impl Area {
pub fn label(&self) -> &text::Area {
&self.model.label
}

/// Set a scene layer for text rendering.
pub fn set_label_layer(&self, layer: &display::scene::Layer) {
self.model.set_label_layer(layer);
}
}


Expand Down
9 changes: 9 additions & 0 deletions app/gui/view/graph-editor/src/component/node/output/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ impl Model {
self
}

fn set_label_layer(&self, layer: &display::scene::Layer) {
self.label.add_to_scene_layer(layer);
}

fn set_label(&self, content: impl Into<String>) {
let str = if ARGS.node_labels.unwrap_or(true) { content.into() } else { default() };
self.label.set_content(str);
Expand Down Expand Up @@ -493,6 +497,11 @@ impl Area {
Self { frp, model }
}

/// Set a scene layer for text rendering.
pub fn set_label_layer(&self, layer: &display::scene::Layer) {
self.model.set_label_layer(layer);
}

#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
pub fn port_type(&self, crumbs: &Crumbs) -> Option<Type> {
let expression = self.model.expression.borrow();
Expand Down
5 changes: 5 additions & 0 deletions app/gui/view/graph-editor/src/component/node/profiling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ impl ProfilingLabel {

ProfilingLabel { root, label, frp, styles }
}

/// Set a scene layer for text rendering.
pub fn set_label_layer(&self, layer: &display::scene::Layer) {
self.label.add_to_scene_layer(layer);
}
}

impl display::Object for ProfilingLabel {
Expand Down
5 changes: 5 additions & 0 deletions app/gui/view/graph-editor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2875,6 +2875,11 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
}


// === Edited node growth/shrink animation ===

component::node::growth_animation::initialize_edited_node_animator(&model, &frp, scene);


// === Event Propagation ===

// See the docs of `Node` to learn about how the graph - nodes event propagation works.
Expand Down
4 changes: 2 additions & 2 deletions app/gui/view/src/documentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ impl Model {
display_object.add_child(&outer_dom);
outer_dom.add_child(&inner_dom);
display_object.add_child(&overlay);
scene.dom.layers.front.manage(&outer_dom);
scene.dom.layers.front.manage(&inner_dom);
scene.dom.layers.node_searcher.manage(&outer_dom);
scene.dom.layers.node_searcher.manage(&inner_dom);

let code_copy_closures = default();
Model { logger, outer_dom, inner_dom, size, overlay, display_object, code_copy_closures }
Expand Down
30 changes: 30 additions & 0 deletions app/gui/view/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,11 +402,41 @@ impl View {
}

let shape = scene.shape().clone_ref();

frp::extend! { network
eval shape ((shape) model.on_dom_shape_changed(shape));

// === Searcher Position and Size ===

let main_cam = app.display.default_scene.layers.main.camera();
let searcher_cam = app.display.default_scene.layers.node_searcher.camera();
let main_cam_frp = &main_cam.frp();
// We want to:
// 1. Preserve the zoom factor of the searcher.
// 2. Keep it directly below edited node at all times.
// We do that by placing `node_searcher` camera in a position calculated by the
// following equations:
// ```
// xy = main_cam.xy * main_cam.zoom
// move_to_edited_node = edited_node.xy - edited_node.xy * main_cam.zoom
// searcher_cam.z = const
// searcher_cam.xy = xy + move_to_edited_node
// ```
// To understand the `move_to_edited_node` equation, consider the following example:
// If edited_node.x = 100, zoom = 0.1, then the node is positioned at
// x = 100 * 0.1 = 10 in searcher_cam-space. To compensate for that, we need to move
// searcher (or rather searcher_cam) by 90 units, so that the node is at x = 100 both
// in searcher_cam- and in main_cam-space.
vitvakatu marked this conversation as resolved.
Show resolved Hide resolved
searcher_cam_pos <- all_with3(&main_cam_frp.position,
&main_cam_frp.zoom,
&searcher_left_top_position.value,
|&main_cam_pos, &zoom, &searcher_pos| {
vitvakatu marked this conversation as resolved.
Show resolved Hide resolved
let preserve_zoom = (main_cam_pos * zoom).xy();
let move_to_edited_node = searcher_pos * (1.0 - zoom);
preserve_zoom + move_to_edited_node
});
eval searcher_cam_pos ((pos) searcher_cam.set_position_xy(*pos));

_eval <- all_with(&searcher_left_top_position.value,&searcher.size,f!([model](lt,size) {
let x = lt.x + size.x / 2.0;
let y = lt.y - size.y / 2.0;
Expand Down
4 changes: 2 additions & 2 deletions app/gui/view/src/searcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ impl Model {
let list = app.new_view::<ListView<Entry>>();
let documentation = documentation::View::new(scene);
let doc_provider = default();
scene.layers.above_nodes.add_exclusive(&list);
scene.layers.node_searcher.add_exclusive(&list);
display_object.add_child(&documentation);
display_object.add_child(&list);

Expand All @@ -135,7 +135,7 @@ impl Model {
let style = StyleWatch::new(&app.display.default_scene.style_sheet);
let action_list_gap_path = ensogl_hardcoded_theme::application::searcher::action_list_gap;
let action_list_gap = style.get_number_or(action_list_gap_path, 0.0);
list.set_label_layer(scene.layers.above_nodes_text.id());
list.set_label_layer(scene.layers.node_searcher_text.id());
list.set_position_y(-action_list_gap);
list.set_position_x(ACTION_LIST_X);
documentation.set_position_x(DOCUMENTATION_X);
Expand Down
1 change: 0 additions & 1 deletion lib/rust/ensogl/core/src/animation/easing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,6 @@ where
data.step(time.local)
} else if let Some(animation_loop) = animation_loop.upgrade() {
animation_loop.set(None);
data.on_end.call(EndStatus::Normal);
}
}
}
Loading