Implementation of EnsoGL predefined Rectangle shape. (#6033)

This commit is contained in:
Wojciech Daniło 2023-03-23 06:47:13 +01:00 committed by GitHub
parent dd009fd1af
commit ca0779c46b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 409 additions and 21 deletions

10
Cargo.lock generated
View File

@ -2751,6 +2751,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "ensogl-example-built-in-shapes"
version = "0.1.0"
dependencies = [
"ensogl-core",
"ensogl-hardcoded-theme",
"wasm-bindgen",
]
[[package]]
name = "ensogl-example-cached-shape"
version = "0.1.0"
@ -2976,6 +2985,7 @@ version = "0.1.0"
dependencies = [
"ensogl-example-animation",
"ensogl-example-auto-layout",
"ensogl-example-built-in-shapes",
"ensogl-example-cached-shape",
"ensogl-example-complex-shape-system",
"ensogl-example-custom-shape-system",

View File

@ -132,6 +132,13 @@ where for<'t> &'t Self: IntoOwned<Owned = Self> {
Union(self, that)
}
/// Unify two shapes, blending their colors based on the foreground shape's SDF value. This
/// means that even if these shapes overlap and the foreground is semi-transparent, it will
/// blend with the background only in the anti-aliased areas.
fn union_exclusive<S: IntoOwned>(&self, that: S) -> UnionExclusive<Self, Owned<S>> {
UnionExclusive(self, that)
}
/// Subtracts the argument from this shape.
fn difference<S: IntoOwned>(&self, that: S) -> Difference<Self, Owned<S>> {
Difference(self, that)

View File

@ -115,17 +115,18 @@ macro_rules! _define_modifier {
pub use immutable::*;
define_modifiers! {
Translate translate (child) (v:Vector2<Pixels>)
Rotation rotation (child) (angle:Radians)
Scale scale (child) (value:f32)
FlipY flip_y (child) ()
Union union (child1,child2) ()
Difference difference (child1,child2) ()
Intersection intersection (child1,child2) ()
Fill fill (child) (color:Rgba)
Recolorize recolorize (child) (r: Rgba, g: Rgba, b: Rgba)
PixelSnap pixel_snap (child) ()
Grow grow (child) (value:f32)
Shrink shrink (child) (value:f32)
Repeat repeat (child) (tile_size:Vector2<Pixels>)
Translate translate (child) (v:Vector2<Pixels>)
Rotation rotation (child) (angle:Radians)
Scale scale (child) (value:f32)
FlipY flip_y (child) ()
Union union (child1,child2) ()
UnionExclusive union_exclusive (child1,child2) ()
Difference difference (child1,child2) ()
Intersection intersection (child1,child2) ()
Fill fill (child) (color:Rgba)
Recolorize recolorize (child) (r: Rgba, g: Rgba, b: Rgba)
PixelSnap pixel_snap (child) ()
Grow grow (child) (value:f32)
Shrink shrink (child) (value:f32)
Repeat repeat (child) (tile_size:Vector2<Pixels>)
}

View File

@ -49,11 +49,15 @@ Color unpremultiply(PremultipliedColor c) {
return color(rgb, alpha);
}
PremultipliedColor blend_with_ratio(PremultipliedColor bg, PremultipliedColor fg, float ratio) {
vec4 raw = fg.repr.raw + (1.0 - ratio) * bg.repr.raw;
return PremultipliedColor(rgba(raw));
}
/// Implements glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
/// in the [`Color`]'s color space. See docs of [`Color`] to learn more.
PremultipliedColor blend(PremultipliedColor bg, PremultipliedColor fg) {
vec4 raw = fg.repr.raw + (1.0 - fg.repr.raw.a) * bg.repr.raw;
return PremultipliedColor(rgba(raw));
return blend_with_ratio(bg, fg, fg.repr.raw.a);
}
Srgba srgba(PremultipliedColor color) {
@ -370,7 +374,7 @@ Shape inverse (Shape s1) {
return shape(s1.id, inverse(s1.sdf), s1.color);
}
Shape unify (Shape s1, Shape s2) {
Shape unify (Shape bg, Shape fg) {
if (input_display_mode == DISPLAY_MODE_CACHED_SHAPES_TEXTURE) {
// In DISPLAY_MODE_CACHED_SHAPES_TEXTURE the color has not [`alpha`] field applied (See
// [`color`] documentation for explanation). That means, that even outside the
@ -383,11 +387,19 @@ Shape unify (Shape s1, Shape s2) {
// results ([`alpha`] field).
// * We want to keep the color consistent near border of the both shapes.
// The code below meets the both conditions.
if (s2.sdf.distance > s1.sdf.distance) {
s2.color.repr.raw *= s2.alpha;
if (fg.sdf.distance > bg.sdf.distance) {
fg.color.repr.raw *= fg.alpha;
}
}
return shape(s1.id, unify(s1.sdf, s2.sdf), blend(s1.color, s2.color));
return shape(bg.id, unify(bg.sdf, fg.sdf), blend(bg.color, fg.color));
}
// Unify two shapes, blending their colors based on the foreground shape's SDF value. This means
// that even if these shapes overlap and the foreground is semi-transparent, it will blend with
// the background only in the anti-aliased areas.
Shape unify_exclusive (Shape bg, Shape fg) {
float ratio = render(fg.sdf);
return shape(bg.id, unify(bg.sdf, fg.sdf), blend_with_ratio(bg.color, fg.color, ratio));
}
Shape difference (Shape s1, Shape s2) {

View File

@ -176,6 +176,14 @@ impl Canvas {
})
}
/// Create an exclusive union shape from the provided shape components.
pub fn union_exclusive(&mut self, num: usize, s1: Shape, s2: Shape) -> Shape {
self.if_not_defined(num, |this| {
let expr = format!("return unify_exclusive({},{});", s1.getter(), s2.getter());
this.new_shape_from_expr(&expr)
})
}
/// Create a difference shape from the provided shape components.
pub fn difference(&mut self, num: usize, s1: Shape, s2: Shape) -> Shape {
self.if_not_defined(num, |this| {

View File

@ -10,16 +10,17 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
ensogl-example-animation = { path = "animation" }
ensogl-example-auto-layout = { path = "auto-layout" }
ensogl-example-built-in-shapes = { path = "built-in-shapes" }
ensogl-example-cached-shape = { path = "cached-shape" }
ensogl-example-complex-shape-system = { path = "complex-shape-system" }
ensogl-example-custom-shape-system = { path = "custom-shape-system" }
ensogl-example-dom-symbols = { path = "dom-symbols" }
ensogl-example-drop-down = { path = "drop-down" }
ensogl-example-drop-manager = { path = "drop-manager" }
ensogl-example-focus-management = { path = "focus-management" }
ensogl-example-easing-animator = { path = "easing-animator" }
ensogl-example-list-view = { path = "list-view" }
ensogl-example-focus-management = { path = "focus-management" }
ensogl-example-grid-view = { path = "grid-view" }
ensogl-example-list-view = { path = "list-view" }
ensogl-example-mouse-events = { path = "mouse-events" }
ensogl-example-profiling-run-graph = { path = "profiling-run-graph" }
ensogl-example-render-profile-flamegraph = { path = "render-profile-flamegraph" }

View File

@ -0,0 +1,17 @@
[package]
name = "ensogl-example-built-in-shapes"
version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
ensogl-core = { path = "../../core" }
wasm-bindgen = { workspace = true }
ensogl-hardcoded-theme = { path = "../../../ensogl/app/theme/hardcoded" }
# Stop wasm-pack from running wasm-opt, because we run it from our build scripts in order to customize options.
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

View File

@ -0,0 +1,331 @@
//! Example scene showing the usage of built-in high-level shapes.
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
#![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::let_and_return)]
use ensogl_core::display::shape::*;
use ensogl_core::display::world::*;
use ensogl_core::prelude::*;
use ensogl_core::data::color;
use ensogl_core::display;
use ensogl_core::display::navigation::navigator::Navigator;
use ensogl_core::display::object::ObjectOps;
// ==============
// === Shapes ===
// ==============
mod rectangle {
use super::*;
ensogl_core::shape! {(
style: Style,
color: Vector4,
corner_radius: f32,
inset: f32,
border: f32,
border_color: Vector4,
clip: Vector2,
) {
// === Canvas ===
let canvas_width = Var::<Pixels>::from("input_size.x");
let canvas_height = Var::<Pixels>::from("input_size.y");
// === Clip ===
// Clipping scales the shape in such a way, that the visible part will occupy whole
// canvas area. Thus, we need to recompute the new canvas size for the scaled shape.
let canvas_clip_height_diff = &canvas_height * (clip.y() * 2.0);
let canvas_clip_width_diff = &canvas_width * (clip.x() * 2.0);
let canvas_height = canvas_height + &canvas_clip_height_diff;
let canvas_width = canvas_width + &canvas_clip_width_diff;
// === Body ===
let inset2 = (&inset * 2.0).px();
let width = &canvas_width - &inset2;
let height = &canvas_height - &inset2;
let color = Var::<color::Rgba>::from(color);
let body = Rect((&width, &height)).corners_radius(corner_radius.px());
let body = body.fill(color);
// === Border ===
let border = body.grow(border.px());
let border_color = Var::<color::Rgba>::from(border_color);
let border = border.fill(border_color);
// === Shape ===
let shape = border.union_exclusive(&body);
// === Clip Adjustment ===
let shape = shape.translate((-canvas_clip_width_diff/2.0, -canvas_clip_height_diff/2.0));
shape.into()
}
}
}
/// A rectangle shape with the following configurable properties:
/// - The body color of the shape.
/// - The corner radius of the shape.
/// - The inset, padding between edge of the frame and shape itself.
/// - The border width and color.
/// - The clipping of the shape (e.g. clipping bottom half of the shape).
///
/// # Performance
/// This shape has been specifically designed to be utilized across various sections of the GUI. Its
/// numerous parameters enable a highly adaptable approach to drawing a diverse range of shapes,
/// such as circles, rings, or ring segments. The advantage of having a singular shape for these
/// cases is that a single draw call can be used to render multiple GUI elements, which ultimately
/// enhances performance.
#[derive(Clone, CloneRef, Debug, Deref, Default)]
#[allow(missing_docs)]
pub struct Rectangle {
pub view: rectangle::View,
}
impl Rectangle {
fn modify_view(&self, f: impl FnOnce(&rectangle::View)) -> &Self {
f(&self.view);
self
}
/// Constructor.
pub fn new() -> Self {
Self::default()
}
/// Builder-style modifier, allowing setting shape properties without creating a temporary
/// variable after its construction.
pub fn build(self, f: impl FnOnce(&Self)) -> Self {
f(&self);
self
}
/// Set the color of the body of the shape.
pub fn set_color(&self, color: color::Rgba) -> &Self {
self.modify_view(|view| view.color.set(color.into()))
}
/// Set the corner radius. If the corner radius will be larger than possible (e.g. larger than
/// the shape dimension), it will be clamped to the highest possible value.
pub fn set_corner_radius(&self, radius: f32) -> &Self {
self.modify_view(|view| view.corner_radius.set(radius))
}
/// Set the corner radius to maximum. If the width and height of the shape are equal, it will
/// result in a circle.
pub fn set_corner_radius_max(&self) -> &Self {
// We are using here a value bigger than anything we will ever need. We are not using
// biggest possible GLSL float value in order not to get rendering artifacts.
let max_radius = 1000000.0;
self.set_corner_radius(max_radius)
}
/// Set the padding between edge of the frame and shape itself. If you want to use border, you
/// should always set the inset at least of the size of the border. If you do not want the
/// border to be animated, you can use [`Self::set_inset_border`] instead.
pub fn set_inset(&self, inset: f32) -> &Self {
self.modify_view(|view| view.inset.set(inset))
}
/// Set the border size of the shape. If you want to use border, you should always set the inset
/// at least of the size of the border. If you do not want the border to be animated, you can
/// use [`Self::set_inset_border`] instead.
pub fn set_border(&self, border: f32) -> &Self {
self.modify_view(|view| view.border.set(border))
}
/// Set both the inset and border at once. See documentation of [`Self::set_border`] and
/// [`Self::set_inset`] to learn more.
pub fn set_inset_border(&self, border: f32) -> &Self {
self.set_inset(border).set_border(border)
}
/// Set the border color.
pub fn set_border_color(&self, color: color::Rgba) -> &Self {
self.modify_view(|view| view.border_color.set(color.into()))
}
/// Set clipping of the shape. The clipping is normalized, which means, that the value of 0.5
/// means that we are clipping 50% of the shape. The clipping is performed always on the left
/// and on the bottom of the shape. If you want to clip other sides of the shape, you can rotate
/// it after clipping or use one of the predefined helper functions, such as
/// [`Self::keep_bottom_half`].
pub fn set_clip(&self, clip: Vector2) -> &Self {
self.modify_view(|view| view.clip.set(clip))
}
/// Keep only the top half of the shape.
pub fn keep_top_half(&self) -> &Self {
self.set_clip(Vector2(0.0, 0.5))
}
/// Keep only the bottom half of the shape.
pub fn keep_bottom_half(&self) -> &Self {
self.keep_top_half().flip()
}
/// Keep only the right half of the shape.
pub fn keep_right_half(&self) -> &Self {
self.set_clip(Vector2(0.5, 0.0))
}
/// Keep only the left half of the shape.
pub fn keep_left_half(&self) -> &Self {
self.keep_right_half().flip()
}
/// Keep only the top right quarter of the shape.
pub fn keep_top_right_quarter(&self) -> &Self {
self.set_clip(Vector2(0.5, 0.5))
}
/// Keep only the bottom right quarter of the shape.
pub fn keep_bottom_right_quarter(&self) -> &Self {
self.keep_top_right_quarter().rotate_90()
}
/// Keep only the bottom left quarter of the shape.
pub fn keep_bottom_left_quarter(&self) -> &Self {
self.keep_bottom_right_quarter().rotate_180()
}
/// Keep only the top left quarter of the shape.
pub fn keep_top_left_quarter(&self) -> &Self {
self.keep_bottom_left_quarter().rotate_270()
}
/// Flip the shape via its center. This is equivalent to rotating the shape by 180 degrees.
pub fn flip(&self) -> &Self {
self.rotate_180()
}
/// Rotate the shape by 90 degrees.
pub fn rotate_90(&self) -> &Self {
self.modify_view(|view| view.set_rotation_z(-std::f32::consts::PI / 2.0))
}
/// Counter rotate the shape by 90 degrees.
pub fn counter_rotate_90(&self) -> &Self {
self.modify_view(|view| view.set_rotation_z(std::f32::consts::PI / 2.0))
}
/// Rotate the shape by 180 degrees.
pub fn rotate_180(&self) -> &Self {
self.modify_view(|view| view.set_rotation_z(-std::f32::consts::PI))
}
/// Counter rotate the shape by 180 degrees.
pub fn rotate_270(&self) -> &Self {
self.modify_view(|view| view.set_rotation_z(-3.0 / 2.0 * std::f32::consts::PI))
}
/// Counter rotate the shape by 270 degrees.
pub fn counter_rotate_270(&self) -> &Self {
self.modify_view(|view| view.set_rotation_z(3.0 / 2.0 * std::f32::consts::PI))
}
}
impl display::Object for Rectangle {
fn display_object(&self) -> &display::object::Instance {
self.view.display_object()
}
}
/// Rectangle constructor.
#[allow(non_snake_case)]
fn Rectangle() -> Rectangle {
Rectangle::default()
}
/// Rounded rectangle constructor. It is a wrapper around [`Rectangle`] with a corner radius set.
#[allow(non_snake_case)]
fn RoundedRectangle(radius: f32) -> Rectangle {
let shape = Rectangle();
shape.set_corner_radius(radius);
shape
}
/// Circle constructor. It is a wrapper around [`Rectangle`] with a corner radius set to maximum.
#[allow(non_snake_case)]
fn Circle() -> Rectangle {
let shape = Rectangle();
shape.set_corner_radius_max();
shape
}
// ===================
// === Entry Point ===
// ===================
/// The example entry point.
#[entry_point]
#[allow(dead_code)]
pub fn main() {
let world = World::new().displayed_in("root");
let scene = &world.default_scene;
let camera = scene.camera().clone_ref();
let navigator = Navigator::new(scene, &camera);
let shapes = [
Circle().build(|t| {
t.set_size(Vector2::new(100.0, 100.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_bottom_left_quarter();
}),
RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 100.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0));
}),
RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 50.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_top_half();
}),
RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 50.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_bottom_half();
}),
RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(50.0, 100.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_right_half();
}),
RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(50.0, 100.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_left_half();
}),
];
let root = display::object::Instance::new();
root.set_size(Vector2::new(300.0, 100.0));
root.use_auto_layout().set_column_count(3).set_gap((10.0, 10.0));
for shape in &shapes {
root.add_child(shape);
}
world.add_child(&root);
world.keep_alive_forever();
mem::forget(navigator);
mem::forget(root);
mem::forget(shapes);
}

View File

@ -31,6 +31,7 @@
// ==============
pub use ensogl_example_animation as animation;
pub use ensogl_example_built_in_shapes as built_in_shapes;
pub use ensogl_example_cached_shape as cached_shape;
pub use ensogl_example_complex_shape_system as complex_shape_system;
pub use ensogl_example_dom_symbols as dom_symbols;