add support for bidirectional rectangle border and inset (#7188)

Extend rectangle shape to allow for both inner and outer borders, at any distance from the main shape body.

No additional instance parameters has been added. Instead, the negative values of both border and inset values are used to allow extra capabilities. Additionally, the small (<1px) gap between border and body caused by anti-aliasing has been fixed.

https://github.com/enso-org/enso/assets/919491/0ae709f2-db7b-4a45-a9d3-7fbb8802dc8c


<img width="657" alt="image" src="https://github.com/enso-org/enso/assets/919491/40e5963d-0717-4662-abf4-b9687aa921ed">
This commit is contained in:
Paweł Grabarz 2023-07-05 21:10:57 +02:00 committed by GitHub
parent 6eb46afb40
commit d11f09c192
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 235 additions and 56 deletions

View File

@ -288,7 +288,7 @@ pub(super) trait ShapeParent: display::Object {
fn new_section(&self) -> Rectangle { fn new_section(&self) -> Rectangle {
let new = Rectangle::new(); let new = Rectangle::new();
new.set_corner_radius_max(); new.set_corner_radius_max();
new.set_inset_border(LINE_WIDTH); new.set_border_and_inset(LINE_WIDTH);
new.set_color(color::Rgba::transparent()); new.set_color(color::Rgba::transparent());
new.set_pointer_events(false); new.set_pointer_events(false);
self.display_object().add_child(&new); self.display_object().add_child(&new);
@ -300,9 +300,8 @@ pub(super) trait ShapeParent: display::Object {
fn new_hover_section(&self) -> Rectangle { fn new_hover_section(&self) -> Rectangle {
let new = Rectangle::new(); let new = Rectangle::new();
new.set_corner_radius_max(); new.set_corner_radius_max();
new.set_inset_border(HOVER_WIDTH); new.set_border_and_inset(HOVER_WIDTH);
new.set_color(color::Rgba::transparent()); new.set_color(color::Rgba::transparent());
new.set_border_color(INVISIBLE_HOVER_COLOR);
self.display_object().add_child(&new); self.display_object().add_child(&new);
new new
} }

View File

@ -119,7 +119,7 @@ impl Background {
let inset = selection_size + selection_offset; let inset = selection_size + selection_offset;
let shape = Rectangle(); let shape = Rectangle();
shape.set_corner_radius(RADIUS); shape.set_corner_radius(RADIUS);
shape.set_border(selection_size); shape.set_frame_border(selection_size);
shape.set_border_color(color::Rgba::transparent()); shape.set_border_color(color::Rgba::transparent());
shape.set_inset(inset); shape.set_inset(inset);
Self { shape, inset: Immutable(inset), selection_color: Immutable(selection_color) } Self { shape, inset: Immutable(inset), selection_color: Immutable(selection_color) }

View File

@ -47,7 +47,6 @@ const SKIP_TOOLTIP_LABEL: &str = "Skip";
fn hover_area() -> Rectangle { fn hover_area() -> Rectangle {
let area = Rectangle(); let area = Rectangle();
area.set_color(INVISIBLE_HOVER_COLOR); area.set_color(INVISIBLE_HOVER_COLOR);
area.set_border_color(INVISIBLE_HOVER_COLOR);
area area
} }

View File

@ -121,7 +121,7 @@ impl PlaceholderModel {
t.set_size(Vector2::new(0.0, 10.0)) t.set_size(Vector2::new(0.0, 10.0))
.allow_grow_x() .allow_grow_x()
.set_color(color::Rgba::new(1.0, 0.0, 0.0, 0.3)) .set_color(color::Rgba::new(1.0, 0.0, 0.0, 0.3))
.set_inset_border(2.0) .set_border_and_inset(2.0)
.set_border_color(color::Rgba::new(1.0, 0.0, 0.0, 1.0)); .set_border_color(color::Rgba::new(1.0, 0.0, 0.0, 1.0));
}); });
root.add_child(&viz); root.add_child(&viz);

View File

@ -20,6 +20,16 @@ pub use shape::Shape;
// =================
// === Constants ===
// =================
/// The threshold of minimum border width, below which the border is considered to be not visible.
/// This is necessary to be able to avoid rendering zero-width border completely, removing any
/// artifacts caused by anti-aliasing near the zero value.
const MINIMUM_BORDER_WIDTH: f32 = 0.1;
// ============= // =============
// === Shape === // === Shape ===
// ============= // =============
@ -32,11 +42,11 @@ pub mod shape {
( (
style: Style, style: Style,
color: Vector4, color: Vector4,
border_color: Vector4,
clip: Vector2,
corner_radius: f32, corner_radius: f32,
inset: f32, inset: f32,
border: f32, border: f32,
border_color: Vector4,
clip: Vector2,
rotate: f32, rotate: f32,
) { ) {
// === Canvas === // === Canvas ===
@ -52,21 +62,34 @@ pub mod shape {
let canvas_width = canvas_width + &canvas_clip_width_diff.abs(); let canvas_width = canvas_width + &canvas_clip_width_diff.abs();
// === Body === // === Body ===
let inset2 = (&inset * 2.0).px(); let inset2 = (Max::max(inset.clone(), Var::from(0.0)) * 2.0).px();
let width = &canvas_width - &inset2; let width = &canvas_width - &inset2;
let height = &canvas_height - &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 = Rect((&width, &height)).corners_radius(corner_radius.px());
let body = body.fill(color);
// === Border === // === Border ===
let padded_body = body.grow((inset - &border).px()); let border_center = &inset * border.negative() + &border * 0.5;
let border = padded_body.grow(border.px()) - padded_body; let abs_border = border.abs();
let border_color = Var::<color::Rgba>::from(border_color); // when border width is close enough to zero, offset it thickness into far negatives to
let border = border.fill(border_color); // avoid rendering it completely. Necessary due to anti-aliasing.
let border_below_threshold = (&abs_border - Var::from(MINIMUM_BORDER_WIDTH)).negative();
let border_thickness = abs_border - border_below_threshold * 1000.0;
let border_body = body.grow(border_center.px()).stroke(border_thickness.px());
// When the border is touching the edge of the body, extend the body by up to a pixel.
// That way there is no visual gap between the shapes caused by anti-aliasing. In those
// scenarios, the extended body will be occluded by the border, therefore it will not
// have any visible effect, other than removing the unwanted artifact.
let fwidth = Var::<f32>::from("fwidth(position.x)");
let touch_offset = border.clamp(0.0.into(), fwidth);
let body = body.grow(touch_offset);
// === Shape === // === Shape ===
let shape = border.union_exclusive(&body); let color = Var::<color::Rgba>::from(color);
let border_color = Var::<color::Rgba>::from(border_color);
let colored_body = body.fill(color);
let colored_border = border_body.fill(border_color);
let shape = colored_body.union_exclusive(&colored_border);
// === Rotation === // === Rotation ===
// Rotate about one corner. // Rotate about one corner.
@ -101,12 +124,18 @@ pub mod shape {
/// such as circles, rings, or ring segments. The advantage of having a singular shape for these /// 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 /// cases is that a single draw call can be used to render multiple GUI elements, which ultimately
/// enhances performance. /// enhances performance.
#[derive(Clone, CloneRef, Deref, Default)] #[derive(Clone, CloneRef, Deref)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct Rectangle { pub struct Rectangle {
pub view: shape::View, pub view: shape::View,
} }
impl Default for Rectangle {
fn default() -> Self {
Self::new()
}
}
impl Debug for Rectangle { impl Debug for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rectangle").finish() f.debug_struct("Rectangle").finish()
@ -121,7 +150,7 @@ impl Rectangle {
/// Constructor. /// Constructor.
pub fn new() -> Self { pub fn new() -> Self {
Self::default().build(|r| { Self { view: default() }.build(|r| {
r.set_border_color(display::shape::INVISIBLE_HOVER_COLOR); r.set_border_color(display::shape::INVISIBLE_HOVER_COLOR);
}) })
} }
@ -138,8 +167,18 @@ impl Rectangle {
self.modify_view(|view| view.color.set(color.into())) 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 /// Set the corner radius of the body.
/// the shape dimension), it will be clamped to the highest possible value. ///
/// Note that the corner radius of the border will be different, depending on the chosen border
/// settings. The corner radius of border shape will always naturally flow around the rectangle
/// body, following its curvature and keeping corners concentric.
///
/// For [outer border](Self::set_border), the outer border's radius will be equal to the body's
/// corner radius plus the border width. For [inner border](Self::set_border_inner), the inner
/// border's radius will be smaller, depending on used distance.
///
/// If the corner radius will be larger than the body size, it will be clamped to at most half
/// of the smaller body dimension.
pub fn set_corner_radius(&self, radius: f32) -> &Self { pub fn set_corner_radius(&self, radius: f32) -> &Self {
self.modify_view(|view| view.corner_radius.set(radius)) self.modify_view(|view| view.corner_radius.set(radius))
} }
@ -156,26 +195,64 @@ impl Rectangle {
/// Set the padding between edge of the frame and main shape. /// Set the padding between edge of the frame and main shape.
/// ///
/// This value should not be less than the width of the border. To set it to the same width as /// This value should not be less than the width of the border. To set it to the same width as
/// the border, you can use [`Self::set_inset_border`]. /// the border, you can use [`Self::set_border_and_inset`].
/// ///
/// If this value is greater than the border width, the extra padding will be between the body /// If the inset value is greater than the absolute value of border width, the shape will have
/// and the border. /// additional transparent padding. If the inset value is positive, the extra padding will
/// be between the body and the outside frame (size) of the rectangle. When it is negative,
/// it can be used to inset the border into the body. To avoid confusion, it is recommended
/// to use [`Self::set_inner_border`] instead.
pub fn set_inset(&self, inset: f32) -> &Self { pub fn set_inset(&self, inset: f32) -> &Self {
self.modify_view(|view| view.inset.set(inset)) 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 /// Set the outer border size of the shape. The border will grow outwards from the rectangle
/// at least of the size of the border. If you do not want the border to be animated, you can /// body, towards the frame. In order to accommodate its width, use [`Self::set_inset`] to set
/// use [`Self::set_inset_border`] instead. To make the border visible, you also need to set the /// the inset value to be greater or equal to the maximum planned border's width.
/// border color using [`Self::set_border_color`]. ///
pub fn set_border(&self, border: f32) -> &Self { /// If you don't plan to animate the border width, you can use [`Self::set_border_and_inset`] to
self.modify_view(|view| view.border.set(border)) /// set both settings to the same value.
pub fn set_border(&self, border_width: f32) -> &Self {
self.modify_view(|view| view.border.set(border_width.max(0.0)))
} }
/// Set both the inset and border at once. See documentation of [`Self::set_border`] and /// Set the inner border size of the shape, positioned within the rectangle body. The inner
/// [`Self::set_inset`] to learn more. To make the border visible, you also need to set the /// border can be offset away from the body's edge by using a non-zero `distance` value.
/// border color using [`Self::set_border_color`]. ///
pub fn set_inset_border(&self, border: f32) -> &Self { /// When the `distance` is set to 0, the effect is similar to an outer border set using
/// [`Self::set_border_and_inset`], but the interpretation of corner radius is different. In
/// this case, the effective corner radius of the border's outside edge will be equal to the
/// body's, corner radius, while the inside edge corners will be appropriately sharper.
///
/// NOTE: This setting will override the inset value set with [`Self::set_inset`], setting it to
/// a negative value. Therefore mixing it with other border settings is not recommended.
pub fn set_inner_border(&self, border_width: f32, distance: f32) -> &Self {
self.modify_view(|view| {
let clamped_width = border_width.max(0.0);
view.border.set(-clamped_width);
view.inset.set(-distance.max(0.0));
})
}
/// Set the frame border size of the shape. The border will grow inwards from the frame's
/// outside edge, towards the rectangle body. In order to create space between the body and
/// frame's border, use [`Self::set_inset`] to set the inset value to be greater or equal to the
/// maximum planned border's width.
///
/// If you don't plan to animate the border width, you can use [`Self::set_border_and_inset`] to
/// set both settings to the same value.
pub fn set_frame_border(&self, border_width: f32) -> &Self {
self.modify_view(|view| view.border.set(-border_width.max(0.0)))
}
/// Set both the inset and border at once. The effect is visually similar to an inner border set
/// using [`Self::set_inner_border`] with distance set to 0, but the interpretation of corner
/// radius is different. In this case, the effective corner radius of the border's outside edge
/// will be equal to the body's corner radius plus the border width.
///
/// See documentation of [`Self::set_border`] and [`Self::set_inset`] to learn more. To make the
/// border visible, you also need to set the border color using [`Self::set_border_color`].
pub fn set_border_and_inset(&self, border: f32) -> &Self {
self.set_inset(border).set_border(border) self.set_inset(border).set_border(border)
} }

View File

@ -128,6 +128,10 @@ where for<'t> &'t Self: IntoOwned<Owned = Self> {
} }
/// Unify the shape with another one. /// Unify the shape with another one.
///
/// Warning: This operator is an approximation - given correct exact input signed distance, it
/// will return exact distance outside the shape, but underestimate the distance inside the
/// shape. For more details, see https://iquilezles.org/articles/interiordistance/
fn union<S: IntoOwned>(&self, that: S) -> Union<Self, Owned<S>> { fn union<S: IntoOwned>(&self, that: S) -> Union<Self, Owned<S>> {
Union(self, that) Union(self, that)
} }
@ -135,16 +139,28 @@ where for<'t> &'t Self: IntoOwned<Owned = Self> {
/// Unify two shapes, blending their colors based on the foreground shape's SDF value. This /// 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 /// 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. /// blend with the background only in the anti-aliased areas.
///
/// Warning: This operator is an approximation - given correct exact input signed distance, it
/// will return exact distance outside the shape, but underestimate the distance inside the
/// shape. For more details, see https://iquilezles.org/articles/interiordistance/
fn union_exclusive<S: IntoOwned>(&self, that: S) -> UnionExclusive<Self, Owned<S>> { fn union_exclusive<S: IntoOwned>(&self, that: S) -> UnionExclusive<Self, Owned<S>> {
UnionExclusive(self, that) UnionExclusive(self, that)
} }
/// Subtracts the argument from this shape. /// Subtracts the argument from this shape.
///
/// Warning: This operator is an approximation - given correct exact input signed distance, it
/// will overestimate the distance to the resulting shape. For more details, see
/// https://iquilezles.org/articles/interiordistance/
fn difference<S: IntoOwned>(&self, that: S) -> Difference<Self, Owned<S>> { fn difference<S: IntoOwned>(&self, that: S) -> Difference<Self, Owned<S>> {
Difference(self, that) Difference(self, that)
} }
/// Computes the intersection of the shapes. /// Computes the intersection of the shapes.
///
/// Warning: This operator is an approximation - given correct exact input signed distance, it
/// will underestimate the distance to the resulting shape. For more details, see
/// https://iquilezles.org/articles/interiordistance/
fn intersection<S: IntoOwned>(&self, that: S) -> Intersection<Self, Owned<S>> { fn intersection<S: IntoOwned>(&self, that: S) -> Intersection<Self, Owned<S>> {
Intersection(self, that) Intersection(self, that)
} }
@ -190,7 +206,7 @@ where for<'t> &'t Self: IntoOwned<Owned = Self> {
Recolorize(self, r, g, b) Recolorize(self, r, g, b)
} }
/// Makes the borders of the shape crisp. Please note that it removes any form of antialiasing /// Makes the borders of the shape crisp. Please note that it removes any form of anti-aliasing
/// and can cause distortions especially with round surfaces. /// and can cause distortions especially with round surfaces.
fn pixel_snap(&self) -> PixelSnap<Self> { fn pixel_snap(&self) -> PixelSnap<Self> {
PixelSnap(self) PixelSnap(self)
@ -206,6 +222,23 @@ where for<'t> &'t Self: IntoOwned<Owned = Self> {
Shrink(self, value.into()) Shrink(self, value.into())
} }
/// Create a stroke of given thickness around shape's boundary. The stroke is centered around
/// the shape's 0 distance isoline. If you want to offset it, use `grow` or `shrink` operators
/// before applying `stroke`.
///
/// Also known as "annulus" or "onion" operator. See "Making shapes annular" section in
/// https://iquilezles.org/articles/distfunctions2d/ for more details.
///
/// Note: This operator is exact - given correct exact input signed distance, it will produce an
/// exact signed distance field for the resulting shape. But it is particularly sensitive to
/// non-exactness of the input signed distance field, both outside and inside the shape. Be
/// careful when using it after applying `union`, `difference` or `intersection` operators. See
/// following shadertoy for demonstration of potential artifacts:
/// https://www.shadertoy.com/view/dslfzH
fn stroke<T: Into<Var<Pixels>>>(&self, thickness: T) -> Stroke<Self> {
Stroke(self, thickness.into())
}
/// Repeats the shape with the given tile size. /// Repeats the shape with the given tile size.
fn repeat<T: Into<Var<Vector2<Pixels>>>>(&self, value: T) -> Repeat<Self> { fn repeat<T: Into<Var<Vector2<Pixels>>>>(&self, value: T) -> Repeat<Self> {
Repeat(self, value) Repeat(self, value)

View File

@ -129,4 +129,5 @@ define_modifiers! {
Grow grow (child) (value:f32) Grow grow (child) (value:f32)
Shrink shrink (child) (value:f32) Shrink shrink (child) (value:f32)
Repeat repeat (child) (tile_size:Vector2<Pixels>) Repeat repeat (child) (tile_size:Vector2<Pixels>)
Stroke stroke (child) (thickness:f32)
} }

View File

@ -12,6 +12,7 @@ use crate::display::shape::primitive::def::class::AnyShape;
use crate::display::shape::primitive::def::class::ShapeRef; use crate::display::shape::primitive::def::class::ShapeRef;
use crate::display::shape::primitive::shader::canvas; use crate::display::shape::primitive::shader::canvas;
use crate::display::shape::primitive::shader::canvas::Canvas; use crate::display::shape::primitive::shader::canvas::Canvas;
use crate::display::shape::Grow;
use crate::display::shape::Var; use crate::display::shape::Var;
use crate::system::gpu::shader::glsl::Glsl; use crate::system::gpu::shader::glsl::Glsl;
@ -449,14 +450,13 @@ impl Plane {
impl Rect { impl Rect {
/// Sets the radius of all the corners. /// Sets the radius of all the corners.
pub fn corners_radius<T>(&self, radius: T) -> RoundedRectByCorner pub fn corners_radius<T>(&self, radius: T) -> Grow<Rect>
where T: Into<Var<Pixels>> { where T: Into<Var<Pixels>> {
let radius = radius.into(); let size = self.size();
let top_left = radius.clone(); let min_size = Min::min(size.x(), size.y());
let top_right = radius.clone(); let radius = Min::min(min_size * 0.5, radius.into());
let bottom_left = radius.clone(); let offset = Var::<Vector2<Pixels>>::from(format!("vec2({} * 2.0)", radius.glsl()));
let bottom_right = radius; Grow(Rect(size - offset), radius)
RoundedRectByCorner(self.size(), top_left, top_right, bottom_left, bottom_right)
} }
/// Sets the radiuses of each of the corners. /// Sets the radiuses of each of the corners.

View File

@ -510,9 +510,7 @@ impl Var<f32> {
Var::Dynamic(t) => Var::Dynamic(format!("smoothstep({e1},{e2},{t})").into()), Var::Dynamic(t) => Var::Dynamic(format!("smoothstep({e1},{e2},{t})").into()),
} }
} }
}
impl Var<f32> {
/// Linearly interpolate between two values. /// Linearly interpolate between two values.
pub fn mix(&self, e1: impl RefInto<Glsl>, e2: impl RefInto<Glsl>) -> Self { pub fn mix(&self, e1: impl RefInto<Glsl>, e2: impl RefInto<Glsl>) -> Self {
let e1 = e1.glsl(); let e1 = e1.glsl();
@ -522,6 +520,22 @@ impl Var<f32> {
Var::Dynamic(t) => Var::Dynamic(format!("mix({e1},{e2},{t})").into()), Var::Dynamic(t) => Var::Dynamic(format!("mix({e1},{e2},{t})").into()),
} }
} }
/// Returns 1.0 if value is strictly greater than 0.0. Returns 0.0 otherwise.
pub fn positive(&self) -> Self {
match self {
Var::Static(t) => Var::Static((*t > 0.0) as u32 as f32),
Var::Dynamic(t) => Var::Dynamic(format!("float({t} > 0.0)").into()),
}
}
/// Returns 1.0 if value is strictly less than 0.0. Returns 0.0 otherwise.
pub fn negative(&self) -> Self {
match self {
Var::Static(t) => Var::Static((*t < 0.0) as u32 as f32),
Var::Dynamic(t) => Var::Dynamic(format!("float({t} < 0.0)").into()),
}
}
} }
@ -629,14 +643,14 @@ where T: Clamp<Output = T> + Into<Glsl>
// === Signum === // === Signum ===
// ============== // ==============
impl<T> Signum for Var<T> impl<T> Signum for &Var<T>
where T: Signum<Output = T> where T: Copy + Signum<Output = T>
{ {
type Output = Var<T>; type Output = Var<T>;
fn signum(self) -> Self { fn signum(self) -> Var<T> {
match self { match self {
Self::Static(t) => Var::Static(t.signum()), Var::Static(t) => Var::Static(t.signum()),
Self::Dynamic(t) => Var::Dynamic(format!("sign({t})").into()), Var::Dynamic(t) => Var::Dynamic(format!("sign({t})").into()),
} }
} }
} }

View File

@ -11,7 +11,7 @@
// pixels when the sprite coordinates are not integers or if the scene is zoomed. See the docs // pixels when the sprite coordinates are not integers or if the scene is zoomed. See the docs
// attached to the shape system's geometry material to learn more. // attached to the shape system's geometry material to learn more.
vec2 position = input_local.xy ; vec2 position = input_local.xy;
Shape view_box = debug_shape(rect(position, input_size)); Shape view_box = debug_shape(rect(position, input_size));
Shape shape = run(position); Shape shape = run(position);
shape = intersection_no_blend(shape, view_box); shape = intersection_no_blend(shape, view_box);

View File

@ -180,6 +180,9 @@ Sdf grow (Sdf a, float size) {
return Sdf(a.distance - size); return Sdf(a.distance - size);
} }
Sdf stroke (Sdf a, float thickness2) {
return Sdf(abs(a.distance) - thickness2);
}
// ================ // ================
@ -240,6 +243,12 @@ BoundSdf grow (BoundSdf a, float size) {
return a; return a;
} }
BoundSdf stroke (BoundSdf a, float thickness2) {
a.distance = abs(a.distance) - thickness2;
a.bounds = grow(a.bounds, thickness2);
return a;
}
BoundSdf inverse (BoundSdf a) { BoundSdf inverse (BoundSdf a) {
return bound_sdf(inverse(sdf(a)), inverse(a.bounds)); return bound_sdf(inverse(sdf(a)), inverse(a.bounds));
} }
@ -370,6 +379,10 @@ Shape grow (Shape s, float value) {
return shape(id, sdf, s.color); return shape(id, sdf, s.color);
} }
Shape stroke (Shape s1, float thickness2) {
return shape(s1.id, stroke(s1.sdf, thickness2), s1.color);
}
Shape inverse (Shape s1) { Shape inverse (Shape s1) {
return shape(s1.id, inverse(s1.sdf), s1.color); return shape(s1.id, inverse(s1.sdf), s1.color);
} }

View File

@ -333,6 +333,15 @@ impl Canvas {
this.new_shape_from_expr(&expr) this.new_shape_from_expr(&expr)
}) })
} }
/// Create a stroke of given thickness around shape's boundary.
pub fn stroke<T: Into<Var<f32>>>(&mut self, num: usize, s: Shape, thickness: T) -> Shape {
self.if_not_defined(num, |this| {
let thickness: Glsl = (thickness.into() * 0.5).glsl();
let expr = format!("return stroke({},({thickness}));", s.getter());
this.new_shape_from_expr(&expr)
})
}
} }

View File

@ -10,6 +10,7 @@ use ensogl_core::display::shape::compound::rectangle::*;
use ensogl_core::display::world::*; use ensogl_core::display::world::*;
use ensogl_core::prelude::*; use ensogl_core::prelude::*;
use ensogl_core::animation::TimeInfo;
use ensogl_core::data::color; use ensogl_core::data::color;
use ensogl_core::display; use ensogl_core::display;
use ensogl_core::display::navigation::navigator::Navigator; use ensogl_core::display::navigation::navigator::Navigator;
@ -30,51 +31,71 @@ pub fn main() {
let camera = scene.camera().clone_ref(); let camera = scene.camera().clone_ref();
let navigator = Navigator::new(scene, &camera); let navigator = Navigator::new(scene, &camera);
let border_demo_1 = RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 100.0))
.set_color(color::Rgba::new(0.8, 0.2, 0.2, 0.5))
.set_border_color(color::Rgba::new(0.0, 0.5, 0.5, 1.0));
});
let border_demo_2 = RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 100.0))
.set_color(color::Rgba::new(0.2, 0.8, 0.2, 0.5))
.set_border_color(color::Rgba::new(0.2, 0.2, 0.5, 1.0));
});
let border_demo_3 = RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 100.0))
.set_color(color::Rgba::new(0.2, 0.2, 0.8, 0.5))
.set_border_color(color::Rgba::new(0.2, 0.5, 0.2, 1.0));
});
let shapes = [ let shapes = [
Circle().build(|t| { Circle().build(|t| {
t.set_size(Vector2::new(100.0, 100.0)) t.set_size(Vector2::new(100.0, 100.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0) .set_border_and_inset(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_bottom_left_quarter(); .keep_bottom_left_quarter();
}), }),
RoundedRectangle(10.0).build(|t| { RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 100.0)) t.set_size(Vector2::new(100.0, 100.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0) .set_border_and_inset(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)); .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0));
}), }),
RoundedRectangle(10.0).build(|t| { RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 50.0)) t.set_size(Vector2::new(100.0, 50.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0) .set_border_and_inset(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_top_half(); .keep_top_half();
}), }),
RoundedRectangle(10.0).build(|t| { RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(100.0, 50.0)) t.set_size(Vector2::new(100.0, 50.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0) .set_border_and_inset(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_bottom_half(); .keep_bottom_half();
}), }),
RoundedRectangle(10.0).build(|t| { RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(50.0, 100.0)) t.set_size(Vector2::new(50.0, 100.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0) .set_border_and_inset(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_right_half(); .keep_right_half();
}), }),
RoundedRectangle(10.0).build(|t| { RoundedRectangle(10.0).build(|t| {
t.set_size(Vector2::new(50.0, 100.0)) t.set_size(Vector2::new(50.0, 100.0))
.set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3))
.set_inset_border(5.0) .set_border_and_inset(5.0)
.set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0))
.keep_left_half(); .keep_left_half();
}), }),
SimpleTriangle::from_base_and_altitude(100.0, 25.0).into(), SimpleTriangle::from_base_and_altitude(100.0, 25.0).into(),
SimpleTriangle::from_base_and_altitude(100.0, 50.0).into(), SimpleTriangle::from_base_and_altitude(100.0, 50.0).into(),
SimpleTriangle::from_base_and_altitude(100.0, 100.0).into(), SimpleTriangle::from_base_and_altitude(100.0, 100.0).into(),
border_demo_1.clone(),
border_demo_2.clone(),
border_demo_3.clone(),
]; ];
let root = display::object::Instance::new(); let root = display::object::Instance::new();
@ -85,6 +106,19 @@ pub fn main() {
} }
world.add_child(&root); world.add_child(&root);
world
.on
.before_frame
.add(move |time: TimeInfo| {
let t = time.frame_start().as_s();
let inset = 10.0 + (t * 1.3).sin() * 10.0;
let width = 10.0 + (t * 2.77).cos() * 10.0;
border_demo_1.set_inset(inset).set_border(width);
border_demo_2.set_inset(inset).set_frame_border(width);
border_demo_3.set_inner_border(width, inset);
})
.forget();
world.keep_alive_forever(); world.keep_alive_forever();
mem::forget(navigator); mem::forget(navigator);
mem::forget(root); mem::forget(root);

View File

@ -53,7 +53,7 @@ pub fn main() {
let new = Circle().build(|t| { let new = Circle().build(|t| {
t.set_size(Vector2::new(64.0, 64.0)) t.set_size(Vector2::new(64.0, 64.0))
.set_color(*color) .set_color(*color)
.set_inset_border(5.0) .set_border_and_inset(5.0)
.set_border_color(*border); .set_border_color(*border);
}); });
let x = rng.gen_range(-512.0..512.0); let x = rng.gen_range(-512.0..512.0);