mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 13:02:07 +03:00
Node searcher zoom & edited node growth/shrink animation (#3327)
In this PR two things are implemented: 1. Node Searcher zoom factor (and therefore its size) is fixed no matter how you move the main camera. The node searcher is also positioned directly below currently edited node at all times. 2. Node growth/shrink animation when you start/finish node editing. After animation end the edited node zoom factor is also fixed and matches the zoom factor of the node searcher. See attached video with different ways of editing/creating nodes: https://user-images.githubusercontent.com/6566674/157348758-2880aa2b-494d-46e6-8eee-a22be84081ed.mp4 #### Technical details 1. Added several additional scene layers for separate rendering: `node_searcher`, `node_searcher_text`, `edited_node`, `edited_node_text`. Searcher is always rendered by `node_searcher` camera, edited node moves between its usual layers and `edited_node` layer. Because text rendering uses different API, all node components were modified to support change of the layer. 2. Also added `node_searcher` DOM layer, because documentation is implemented as a DOM object. 3. Added two FRP endpoints for `ensogl::Animation`: `on_end` and `set_value`. These endpoints are useful while implementing growth/shrink animation. 4. Added FRP endpoints for the `Camera2d`: `position` and `zoom` outputs. This allows to synchronize cameras easily using FRP networks. 5. Growth/shrink animation implemented in GraphEditor by blending two animations, similar to Node Snapping implementation. However, shrinking animation is a bit tricky to implement correctly, as we must always return node back to the `main` scene layer after editing is done.
This commit is contained in:
parent
11dfd7bfc9
commit
cdcc852e03
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
#### Visual Environment
|
#### 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].
|
- [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
|
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.
|
down if the center is already occupied by nodes.
|
||||||
@ -108,6 +111,7 @@
|
|||||||
[3317]: https://github.com/enso-org/enso/pull/3317
|
[3317]: https://github.com/enso-org/enso/pull/3317
|
||||||
[3318]: https://github.com/enso-org/enso/pull/3318
|
[3318]: https://github.com/enso-org/enso/pull/3318
|
||||||
[3324]: https://github.com/enso-org/enso/pull/3324
|
[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
|
[3339]: https://github.com/enso-org/enso/pull/3339
|
||||||
|
|
||||||
#### Enso Compiler
|
#### Enso Compiler
|
||||||
|
@ -20,6 +20,7 @@ use ensogl::animation::delayed::DelayedAnimation;
|
|||||||
use ensogl::application::Application;
|
use ensogl::application::Application;
|
||||||
use ensogl::data::color;
|
use ensogl::data::color;
|
||||||
use ensogl::display;
|
use ensogl::display;
|
||||||
|
use ensogl::display::scene::Layer;
|
||||||
use ensogl::Animation;
|
use ensogl::Animation;
|
||||||
use ensogl_component::shadow;
|
use ensogl_component::shadow;
|
||||||
use ensogl_component::text;
|
use ensogl_component::text;
|
||||||
@ -38,6 +39,7 @@ pub mod action_bar;
|
|||||||
#[warn(missing_docs)]
|
#[warn(missing_docs)]
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod expression;
|
pub mod expression;
|
||||||
|
pub mod growth_animation;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
#[warn(missing_docs)]
|
#[warn(missing_docs)]
|
||||||
@ -537,6 +539,45 @@ impl NodeModel {
|
|||||||
self
|
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.
|
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
|
||||||
pub fn width(&self) -> f32 {
|
pub fn width(&self) -> f32 {
|
||||||
self.input.width.value()
|
self.input.width.value()
|
||||||
|
125
app/gui/view/graph-editor/src/component/node/growth_animation.rs
Normal file
125
app/gui/view/graph-editor/src/component/node/growth_animation.rs
Normal file
@ -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)
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -286,6 +286,10 @@ impl Model {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_label_layer(&self, layer: &display::scene::Layer) {
|
||||||
|
self.label.add_to_scene_layer(layer);
|
||||||
|
}
|
||||||
|
|
||||||
fn scene(&self) -> &Scene {
|
fn scene(&self) -> &Scene {
|
||||||
&self.app.display.default_scene
|
&self.app.display.default_scene
|
||||||
}
|
}
|
||||||
@ -475,6 +479,11 @@ impl Area {
|
|||||||
pub fn label(&self) -> &text::Area {
|
pub fn label(&self) -> &text::Area {
|
||||||
&self.model.label
|
&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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -216,6 +216,10 @@ impl Model {
|
|||||||
self
|
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>) {
|
fn set_label(&self, content: impl Into<String>) {
|
||||||
let str = if ARGS.node_labels.unwrap_or(true) { content.into() } else { default() };
|
let str = if ARGS.node_labels.unwrap_or(true) { content.into() } else { default() };
|
||||||
self.label.set_content(str);
|
self.label.set_content(str);
|
||||||
@ -493,6 +497,11 @@ impl Area {
|
|||||||
Self { frp, model }
|
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.
|
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
|
||||||
pub fn port_type(&self, crumbs: &Crumbs) -> Option<Type> {
|
pub fn port_type(&self, crumbs: &Crumbs) -> Option<Type> {
|
||||||
let expression = self.model.expression.borrow();
|
let expression = self.model.expression.borrow();
|
||||||
|
@ -247,6 +247,11 @@ impl ProfilingLabel {
|
|||||||
|
|
||||||
ProfilingLabel { root, label, frp, styles }
|
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 {
|
impl display::Object for ProfilingLabel {
|
||||||
|
@ -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 ===
|
// === Event Propagation ===
|
||||||
|
|
||||||
// See the docs of `Node` to learn about how the graph - nodes event propagation works.
|
// See the docs of `Node` to learn about how the graph - nodes event propagation works.
|
||||||
|
@ -111,8 +111,8 @@ impl Model {
|
|||||||
display_object.add_child(&outer_dom);
|
display_object.add_child(&outer_dom);
|
||||||
outer_dom.add_child(&inner_dom);
|
outer_dom.add_child(&inner_dom);
|
||||||
display_object.add_child(&overlay);
|
display_object.add_child(&overlay);
|
||||||
scene.dom.layers.front.manage(&outer_dom);
|
scene.dom.layers.node_searcher.manage(&outer_dom);
|
||||||
scene.dom.layers.front.manage(&inner_dom);
|
scene.dom.layers.node_searcher.manage(&inner_dom);
|
||||||
|
|
||||||
let code_copy_closures = default();
|
let code_copy_closures = default();
|
||||||
Model { logger, outer_dom, inner_dom, size, overlay, display_object, code_copy_closures }
|
Model { logger, outer_dom, inner_dom, size, overlay, display_object, code_copy_closures }
|
||||||
|
@ -402,11 +402,40 @@ impl View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let shape = scene.shape().clone_ref();
|
let shape = scene.shape().clone_ref();
|
||||||
|
|
||||||
frp::extend! { network
|
frp::extend! { network
|
||||||
eval shape ((shape) model.on_dom_shape_changed(shape));
|
eval shape ((shape) model.on_dom_shape_changed(shape));
|
||||||
|
|
||||||
// === Searcher Position and Size ===
|
// === 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.
|
||||||
|
searcher_cam_pos <- all_with3
|
||||||
|
(&main_cam_frp.position, &main_cam_frp.zoom, &searcher_left_top_position.value,
|
||||||
|
|&main_cam_pos, &zoom, &searcher_pos| {
|
||||||
|
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) {
|
_eval <- all_with(&searcher_left_top_position.value,&searcher.size,f!([model](lt,size) {
|
||||||
let x = lt.x + size.x / 2.0;
|
let x = lt.x + size.x / 2.0;
|
||||||
let y = lt.y - size.y / 2.0;
|
let y = lt.y - size.y / 2.0;
|
||||||
|
@ -126,7 +126,7 @@ impl Model {
|
|||||||
let list = app.new_view::<ListView<Entry>>();
|
let list = app.new_view::<ListView<Entry>>();
|
||||||
let documentation = documentation::View::new(scene);
|
let documentation = documentation::View::new(scene);
|
||||||
let doc_provider = default();
|
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(&documentation);
|
||||||
display_object.add_child(&list);
|
display_object.add_child(&list);
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ impl Model {
|
|||||||
let style = StyleWatch::new(&app.display.default_scene.style_sheet);
|
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_path = ensogl_hardcoded_theme::application::searcher::action_list_gap;
|
||||||
let action_list_gap = style.get_number_or(action_list_gap_path, 0.0);
|
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_y(-action_list_gap);
|
||||||
list.set_position_x(ACTION_LIST_X);
|
list.set_position_x(ACTION_LIST_X);
|
||||||
documentation.set_position_x(DOCUMENTATION_X);
|
documentation.set_position_x(DOCUMENTATION_X);
|
||||||
|
@ -425,7 +425,6 @@ where
|
|||||||
data.step(time.local)
|
data.step(time.local)
|
||||||
} else if let Some(animation_loop) = animation_loop.upgrade() {
|
} else if let Some(animation_loop) = animation_loop.upgrade() {
|
||||||
animation_loop.set(None);
|
animation_loop.set(None);
|
||||||
data.on_end.call(EndStatus::Normal);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,21 +21,19 @@ pub mod hysteretic;
|
|||||||
// === Animation ===
|
// === Animation ===
|
||||||
// =================
|
// =================
|
||||||
|
|
||||||
// crate::define_endpoints! { <T>
|
|
||||||
// Input {
|
|
||||||
// target (f32),
|
|
||||||
// precision (f32),
|
|
||||||
// skip (),
|
|
||||||
// }
|
|
||||||
// Output {
|
|
||||||
// value (f32),
|
|
||||||
// on_end (inertia::EndStatus),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Simulator used to run the animation.
|
/// Simulator used to run the animation.
|
||||||
pub type AnimationSimulator<T> = inertia::DynSimulator<mix::Repr<T>>;
|
pub type AnimationSimulator<T> = inertia::DynSimulator<mix::Repr<T>>;
|
||||||
|
|
||||||
|
/// Default animation precision.
|
||||||
|
///
|
||||||
|
/// This value defines the threshold of how close the current animation value should be to the
|
||||||
|
/// target value so that the animation is considered finished.
|
||||||
|
/// FIXME[WD]: The precision should should be increased in all simulators
|
||||||
|
/// that work with pixels. The reason is that by default the simulator should
|
||||||
|
/// give nice results for animations in the range of 0 .. 1, while it should not
|
||||||
|
/// make too many steps when animating bigger values (like pixels).
|
||||||
|
pub const DEFAULT_PRECISION: f32 = 0.001;
|
||||||
|
|
||||||
/// Smart animation handler. Contains of dynamic simulation and frp endpoint. Whenever a new value
|
/// Smart animation handler. Contains of dynamic simulation and frp endpoint. Whenever a new value
|
||||||
/// is computed, it is emitted via the endpoint.
|
/// is computed, it is emitted via the endpoint.
|
||||||
#[derive(CloneRef, Derivative, Debug)]
|
#[derive(CloneRef, Derivative, Debug)]
|
||||||
@ -46,6 +44,7 @@ pub struct Animation<T: mix::Mixable + frp::Data> {
|
|||||||
pub precision: frp::Any<f32>,
|
pub precision: frp::Any<f32>,
|
||||||
pub skip: frp::Any,
|
pub skip: frp::Any,
|
||||||
pub value: frp::Stream<T>,
|
pub value: frp::Stream<T>,
|
||||||
|
pub on_end: frp::Stream<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
@ -56,14 +55,12 @@ where mix::Repr<T>: inertia::Value
|
|||||||
pub fn new(network: &frp::Network) -> Self {
|
pub fn new(network: &frp::Network) -> Self {
|
||||||
frp::extend! { network
|
frp::extend! { network
|
||||||
value_src <- any_mut::<T>();
|
value_src <- any_mut::<T>();
|
||||||
|
on_end_src <- any_mut();
|
||||||
}
|
}
|
||||||
let on_step = Box::new(f!((t) value_src.emit(mix::from_space::<T>(t))));
|
let on_step = Box::new(f!((t) value_src.emit(mix::from_space::<T>(t))));
|
||||||
let simulator = AnimationSimulator::<T>::new(on_step, (), ());
|
let on_end = Box::new(f!((_) on_end_src.emit(())));
|
||||||
// FIXME[WD]: The precision should become default and should be increased in all simulators
|
let simulator = AnimationSimulator::<T>::new(on_step, (), on_end);
|
||||||
// that work with pixels. The reason is that by default the simulator should
|
simulator.set_precision(DEFAULT_PRECISION);
|
||||||
// give nice results for animations in the range of 0 .. 1, while it should not
|
|
||||||
// make too many steps when animating bigger values (like pixels).
|
|
||||||
simulator.set_precision(0.001);
|
|
||||||
frp::extend! { network
|
frp::extend! { network
|
||||||
target <- any_mut::<T>();
|
target <- any_mut::<T>();
|
||||||
precision <- any_mut::<f32>();
|
precision <- any_mut::<f32>();
|
||||||
@ -73,8 +70,9 @@ where mix::Repr<T>: inertia::Value
|
|||||||
eval_ skip (simulator.skip());
|
eval_ skip (simulator.skip());
|
||||||
}
|
}
|
||||||
let value = value_src.into();
|
let value = value_src.into();
|
||||||
|
let on_end = on_end_src.into();
|
||||||
network.store(&simulator);
|
network.store(&simulator);
|
||||||
Self { target, precision, skip, value }
|
Self { target, precision, skip, value, on_end }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructor. The initial value is provided explicitly.
|
/// Constructor. The initial value is provided explicitly.
|
||||||
@ -141,7 +139,8 @@ where mix::Repr<T>: inertia::Value
|
|||||||
def target = source::<T>();
|
def target = source::<T>();
|
||||||
}
|
}
|
||||||
let on_step = Box::new(f!((t) target.emit(mix::from_space::<T>(t))));
|
let on_step = Box::new(f!((t) target.emit(mix::from_space::<T>(t))));
|
||||||
let simulator = inertia::DynSimulator::<T::Repr>::new(on_step, (), ());
|
let on_end = Box::new(|_| ());
|
||||||
|
let simulator = inertia::DynSimulator::<T::Repr>::new(on_step, (), on_end);
|
||||||
let value = target.into();
|
let value = target.into();
|
||||||
Self { simulator, value }
|
Self { simulator, value }
|
||||||
}
|
}
|
||||||
|
@ -617,7 +617,7 @@ where
|
|||||||
// =================
|
// =================
|
||||||
|
|
||||||
/// Handy alias for `Simulator` with a boxed closure callback.
|
/// Handy alias for `Simulator` with a boxed closure callback.
|
||||||
pub type DynSimulator<T> = Simulator<T, Box<dyn Fn(T)>, (), ()>;
|
pub type DynSimulator<T> = Simulator<T, Box<dyn Fn(T)>, (), Box<dyn Fn(EndStatus)>>;
|
||||||
|
|
||||||
/// The `SimulationDataCell` with an associated animation loop. The simulation is updated every
|
/// The `SimulationDataCell` with an associated animation loop. The simulation is updated every
|
||||||
/// frame in an efficient way – when the simulation finishes, it automatically unregisters the
|
/// frame in an efficient way – when the simulation finishes, it automatically unregisters the
|
||||||
|
@ -9,6 +9,7 @@ use crate::control::callback;
|
|||||||
use crate::data::dirty;
|
use crate::data::dirty;
|
||||||
use crate::display;
|
use crate::display;
|
||||||
use crate::display::scene::Scene;
|
use crate::display::scene::Scene;
|
||||||
|
use crate::frp;
|
||||||
|
|
||||||
use nalgebra::Perspective3;
|
use nalgebra::Perspective3;
|
||||||
|
|
||||||
@ -158,6 +159,16 @@ impl Default for Matrix {
|
|||||||
// === Camera2dData ===
|
// === Camera2dData ===
|
||||||
// ====================
|
// ====================
|
||||||
|
|
||||||
|
/// Frp outputs of the Camera2d.
|
||||||
|
#[derive(Debug, Clone, CloneRef)]
|
||||||
|
pub struct Frp {
|
||||||
|
network: frp::Network,
|
||||||
|
/// Camera position.
|
||||||
|
pub position: frp::Source<Vector3<f32>>,
|
||||||
|
/// Camera zoom factor.
|
||||||
|
pub zoom: frp::Source<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Function used to return the updated screen dimensions.
|
/// Function used to return the updated screen dimensions.
|
||||||
pub trait ScreenUpdateFn = Fn(Vector2<f32>) + 'static;
|
pub trait ScreenUpdateFn = Fn(Vector2<f32>) + 'static;
|
||||||
|
|
||||||
@ -177,6 +188,7 @@ struct Camera2dData {
|
|||||||
dirty: Dirty,
|
dirty: Dirty,
|
||||||
zoom_update_registry: callback::registry::CopyMut1<f32>,
|
zoom_update_registry: callback::registry::CopyMut1<f32>,
|
||||||
screen_update_registry: callback::registry::CopyMut1<Vector2<f32>>,
|
screen_update_registry: callback::registry::CopyMut1<Vector2<f32>>,
|
||||||
|
frp: Frp,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectionDirty = dirty::SharedBool<()>;
|
type ProjectionDirty = dirty::SharedBool<()>;
|
||||||
@ -197,7 +209,14 @@ impl Camera2dData {
|
|||||||
display_object.set_on_updated(f_!(dirty.transform.set()));
|
display_object.set_on_updated(f_!(dirty.transform.set()));
|
||||||
display_object.mod_position(|p| p.z = 1.0);
|
display_object.mod_position(|p| p.z = 1.0);
|
||||||
dirty.projection.set();
|
dirty.projection.set();
|
||||||
|
let network = frp::Network::new("Camera2d");
|
||||||
|
frp::extend! { network
|
||||||
|
frp_position <- source();
|
||||||
|
frp_zoom <- source();
|
||||||
|
}
|
||||||
|
let frp = Frp { network, position: frp_position, zoom: frp_zoom };
|
||||||
Self {
|
Self {
|
||||||
|
frp,
|
||||||
display_object,
|
display_object,
|
||||||
screen,
|
screen,
|
||||||
zoom,
|
zoom,
|
||||||
@ -272,7 +291,10 @@ impl Camera2dData {
|
|||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
self.matrix.view_projection = self.matrix.projection * self.matrix.view;
|
self.matrix.view_projection = self.matrix.projection * self.matrix.view;
|
||||||
self.zoom_update_registry.run_all(self.zoom);
|
let zoom = self.zoom;
|
||||||
|
self.zoom_update_registry.run_all(zoom);
|
||||||
|
self.frp.position.emit(self.display_object.position());
|
||||||
|
self.frp.zoom.emit(zoom);
|
||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
@ -424,6 +446,10 @@ impl Camera2d {
|
|||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
impl Camera2d {
|
impl Camera2d {
|
||||||
|
pub fn frp(&self) -> Frp {
|
||||||
|
self.data.borrow().frp.clone_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clipping(&self) -> Clipping {
|
pub fn clipping(&self) -> Clipping {
|
||||||
self.data.borrow().clipping
|
self.data.borrow().clipping
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,8 @@ impl NavigatorModel {
|
|||||||
fn create_simulator(camera: &Camera2d) -> physics::inertia::DynSimulator<Vector3> {
|
fn create_simulator(camera: &Camera2d) -> physics::inertia::DynSimulator<Vector3> {
|
||||||
let camera_ref = camera.clone_ref();
|
let camera_ref = camera.clone_ref();
|
||||||
let on_step = Box::new(move |p: Vector3| camera_ref.set_position(p));
|
let on_step = Box::new(move |p: Vector3| camera_ref.set_position(p));
|
||||||
let simulator = physics::inertia::DynSimulator::new(on_step, (), ());
|
let on_end = Box::new(|_| ());
|
||||||
|
let simulator = physics::inertia::DynSimulator::new(on_step, (), on_end);
|
||||||
// FIXME[WD]: This one is emitting camera position in next frame, which is not intended.
|
// FIXME[WD]: This one is emitting camera position in next frame, which is not intended.
|
||||||
// Should be fixed when reworking navigator to use FRP events.
|
// Should be fixed when reworking navigator to use FRP events.
|
||||||
simulator.set_value(camera.position());
|
simulator.set_value(camera.position());
|
||||||
|
@ -470,8 +470,8 @@ impl Dom {
|
|||||||
// === DomLayers ===
|
// === DomLayers ===
|
||||||
// =================
|
// =================
|
||||||
|
|
||||||
/// DOM DomLayers of the scene. It contains a 2 CSS 3D layers and a canvas layer in the middle. The
|
/// DOM DomLayers of the scene. It contains several CSS 3D layers and a canvas layer in the middle.
|
||||||
/// CSS layers are used to manage DOM elements and to simulate depth-sorting of DOM and canvas
|
/// The CSS layers are used to manage DOM elements and to simulate depth-sorting of DOM and canvas
|
||||||
/// elements.
|
/// elements.
|
||||||
///
|
///
|
||||||
/// Each DomLayer is created with `pointer-events: none` CSS property to avoid "stealing" mouse
|
/// Each DomLayer is created with `pointer-events: none` CSS property to avoid "stealing" mouse
|
||||||
@ -492,6 +492,9 @@ pub struct DomLayers {
|
|||||||
pub fullscreen_vis: DomScene,
|
pub fullscreen_vis: DomScene,
|
||||||
/// Front DOM scene layer.
|
/// Front DOM scene layer.
|
||||||
pub front: DomScene,
|
pub front: DomScene,
|
||||||
|
/// DOM scene layer for Node Searcher DOM elements. This layer should probably be removed once
|
||||||
|
/// all parts of Node Searcher will use ensogl primitives instead of DOM objects for rendering.
|
||||||
|
pub node_searcher: DomScene,
|
||||||
/// The WebGL scene layer.
|
/// The WebGL scene layer.
|
||||||
pub canvas: web::HtmlCanvasElement,
|
pub canvas: web::HtmlCanvasElement,
|
||||||
}
|
}
|
||||||
@ -525,12 +528,17 @@ impl DomLayers {
|
|||||||
canvas.set_style_or_warn("pointer-events", "none");
|
canvas.set_style_or_warn("pointer-events", "none");
|
||||||
dom.append_or_warn(&canvas);
|
dom.append_or_warn(&canvas);
|
||||||
|
|
||||||
|
let node_searcher = DomScene::new(logger);
|
||||||
|
node_searcher.dom.set_class_name("node-searcher");
|
||||||
|
node_searcher.dom.set_style_or_warn("z-index", "4");
|
||||||
|
dom.append_or_warn(&node_searcher.dom);
|
||||||
|
|
||||||
let front = DomScene::new(logger);
|
let front = DomScene::new(logger);
|
||||||
front.dom.set_class_name("front");
|
front.dom.set_class_name("front");
|
||||||
front.dom.set_style_or_warn("z-index", "4");
|
front.dom.set_style_or_warn("z-index", "5");
|
||||||
dom.append_or_warn(&front.dom);
|
dom.append_or_warn(&front.dom);
|
||||||
|
|
||||||
Self { back, welcome_screen, fullscreen_vis, front, canvas }
|
Self { back, welcome_screen, fullscreen_vis, front, node_searcher, canvas }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -698,6 +706,9 @@ pub struct HardcodedLayers {
|
|||||||
pub panel: Layer,
|
pub panel: Layer,
|
||||||
pub panel_text: Layer,
|
pub panel_text: Layer,
|
||||||
pub node_searcher: Layer,
|
pub node_searcher: Layer,
|
||||||
|
pub node_searcher_text: Layer,
|
||||||
|
pub edited_node: Layer,
|
||||||
|
pub edited_node_text: Layer,
|
||||||
pub node_searcher_mask: Layer,
|
pub node_searcher_mask: Layer,
|
||||||
pub tooltip: Layer,
|
pub tooltip: Layer,
|
||||||
pub tooltip_text: Layer,
|
pub tooltip_text: Layer,
|
||||||
@ -726,13 +737,19 @@ impl HardcodedLayers {
|
|||||||
let panel = Layer::new(logger.sub("panel"));
|
let panel = Layer::new(logger.sub("panel"));
|
||||||
let panel_text = Layer::new(logger.sub("panel_text"));
|
let panel_text = Layer::new(logger.sub("panel_text"));
|
||||||
let node_searcher = Layer::new(logger.sub("node_searcher"));
|
let node_searcher = Layer::new(logger.sub("node_searcher"));
|
||||||
|
let node_searcher_cam = node_searcher.camera();
|
||||||
|
let searcher_text_logger = logger.sub("node_searcher_text");
|
||||||
|
let node_searcher_text = Layer::new_with_cam(searcher_text_logger, &node_searcher_cam);
|
||||||
|
let edited_node = Layer::new(logger.sub("edited_node"));
|
||||||
|
let edited_node_cam = edited_node.camera();
|
||||||
|
let edited_node_text_logger = logger.sub("edited_node_text");
|
||||||
|
let edited_node_text = Layer::new_with_cam(edited_node_text_logger, &edited_node_cam);
|
||||||
let node_searcher_mask = Layer::new(logger.sub("node_searcher_mask"));
|
let node_searcher_mask = Layer::new(logger.sub("node_searcher_mask"));
|
||||||
let tooltip = Layer::new_with_cam(logger.sub("tooltip"), main_cam);
|
let tooltip = Layer::new_with_cam(logger.sub("tooltip"), main_cam);
|
||||||
let tooltip_text = Layer::new_with_cam(logger.sub("tooltip_text"), main_cam);
|
let tooltip_text = Layer::new_with_cam(logger.sub("tooltip_text"), main_cam);
|
||||||
let cursor = Layer::new(logger.sub("cursor"));
|
let cursor = Layer::new(logger.sub("cursor"));
|
||||||
|
|
||||||
let mask = Layer::new_with_cam(logger.sub("mask"), main_cam);
|
let mask = Layer::new_with_cam(logger.sub("mask"), main_cam);
|
||||||
node_searcher.set_mask(&node_searcher_mask);
|
|
||||||
root.set_sublayers(&[
|
root.set_sublayers(&[
|
||||||
&viz,
|
&viz,
|
||||||
&below_main,
|
&below_main,
|
||||||
@ -744,6 +761,9 @@ impl HardcodedLayers {
|
|||||||
&panel,
|
&panel,
|
||||||
&panel_text,
|
&panel_text,
|
||||||
&node_searcher,
|
&node_searcher,
|
||||||
|
&node_searcher_text,
|
||||||
|
&edited_node,
|
||||||
|
&edited_node_text,
|
||||||
&tooltip,
|
&tooltip,
|
||||||
&tooltip_text,
|
&tooltip_text,
|
||||||
&cursor,
|
&cursor,
|
||||||
@ -760,6 +780,9 @@ impl HardcodedLayers {
|
|||||||
panel,
|
panel,
|
||||||
panel_text,
|
panel_text,
|
||||||
node_searcher,
|
node_searcher,
|
||||||
|
node_searcher_text,
|
||||||
|
edited_node,
|
||||||
|
edited_node_text,
|
||||||
node_searcher_mask,
|
node_searcher_mask,
|
||||||
tooltip,
|
tooltip,
|
||||||
tooltip_text,
|
tooltip_text,
|
||||||
@ -1008,6 +1031,11 @@ impl SceneData {
|
|||||||
self.dom.layers.fullscreen_vis.update_view_projection(&fullscreen_vis_camera);
|
self.dom.layers.fullscreen_vis.update_view_projection(&fullscreen_vis_camera);
|
||||||
self.dom.layers.welcome_screen.update_view_projection(&welcome_screen_camera);
|
self.dom.layers.welcome_screen.update_view_projection(&welcome_screen_camera);
|
||||||
}
|
}
|
||||||
|
let node_searcher_camera = self.layers.node_searcher.camera();
|
||||||
|
let node_searcher_camera_changed = node_searcher_camera.update(scene);
|
||||||
|
if node_searcher_camera_changed {
|
||||||
|
self.dom.layers.node_searcher.update_view_projection(&node_searcher_camera);
|
||||||
|
}
|
||||||
|
|
||||||
// Updating all other cameras (the main camera was already updated, so it will be skipped).
|
// Updating all other cameras (the main camera was already updated, so it will be skipped).
|
||||||
self.layers.iter_sublayers_and_masks_nested(|layer| {
|
self.layers.iter_sublayers_and_masks_nested(|layer| {
|
||||||
|
@ -117,7 +117,6 @@ pub fn entry_point_complex_shape_system() {
|
|||||||
.before_frame
|
.before_frame
|
||||||
.add(move |_time| {
|
.add(move |_time| {
|
||||||
mask.set_position_x(((frame as f32) / 30.0).sin() * 100.0);
|
mask.set_position_x(((frame as f32) / 30.0).sin() * 100.0);
|
||||||
|
|
||||||
let _keep_alive = &navigator;
|
let _keep_alive = &navigator;
|
||||||
let _keep_alive = &style_watch;
|
let _keep_alive = &style_watch;
|
||||||
let _keep_alive = &theme_manager;
|
let _keep_alive = &theme_manager;
|
||||||
|
Loading…
Reference in New Issue
Block a user