extern crate alloc;
use core::f32::consts::{FRAC_PI_2, PI, TAU};
#[cfg(feature = "gpu")]
use core::fmt;
use core::time::Duration;
#[cfg(feature = "gpu")]
use num_traits::ToPrimitive;
#[cfg(feature = "gpu")]
use std::time::Instant;
#[cfg(feature = "gpu")]
use nami::Signal as _;
use nami::{Computed, SignalExt as _, signal::IntoComputed};
#[cfg(feature = "gpu")]
use shaderloom::CompiledShader;
#[cfg(feature = "gpu")]
use waterui_core::reactive::watcher::BoxWatcherGuard;
use waterui_core::{Environment, View, easing::EasingCurve, metadata::MetadataKey};
use waterui_graphics::color::Color;
#[cfg(feature = "gpu")]
use waterui_graphics::{
GpuContext, GpuFrame, GpuSurface, GpuView, reactive_color::ReactiveColor,
single_bind_group_render_stages,
};
#[cfg(feature = "gpu")]
const MORPH_SHADER: CompiledShader = include!(concat!(env!("OUT_DIR"), "/morph.rs"));
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PathCommand {
MoveTo {
x: f32,
y: f32,
},
LineTo {
x: f32,
y: f32,
},
QuadTo {
cx: f32,
cy: f32,
x: f32,
y: f32,
},
CubicTo {
c1x: f32,
c1y: f32,
c2x: f32,
c2y: f32,
x: f32,
y: f32,
},
Arc {
cx: f32,
cy: f32,
rx: f32,
ry: f32,
start: f32,
sweep: f32,
},
Close,
}
#[inline]
const fn clamp_radius(value: f32) -> f32 {
if value.is_finite() {
value.clamp(0.0, 0.5)
} else {
0.0
}
}
#[derive(Debug, Clone, Copy)]
struct CornerRadii {
top_left: f32,
top_right: f32,
bottom_right: f32,
bottom_left: f32,
}
impl CornerRadii {
#[inline]
fn sanitized(mut self) -> Self {
self.top_left = clamp_radius(self.top_left);
self.top_right = clamp_radius(self.top_right);
self.bottom_right = clamp_radius(self.bottom_right);
self.bottom_left = clamp_radius(self.bottom_left);
let mut scale = 1.0f32;
let pairs = [
self.top_left + self.top_right,
self.bottom_left + self.bottom_right,
self.top_left + self.bottom_left,
self.top_right + self.bottom_right,
];
for sum in pairs {
if sum > 1.0 {
scale = scale.min(1.0 / sum);
}
}
if scale < 1.0 {
self.top_left *= scale;
self.top_right *= scale;
self.bottom_right *= scale;
self.bottom_left *= scale;
}
self
}
}
pub trait Shape {
type Iter: IntoIterator<Item = PathCommand>;
fn path(&self) -> Self::Iter;
fn shape_kind(&self) -> ShapeKind {
ShapeKind::CustomPath
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Circle;
impl Shape for Circle {
type Iter = [PathCommand; 1];
fn path(&self) -> Self::Iter {
[PathCommand::Arc {
cx: 0.5,
cy: 0.5,
rx: 0.5,
ry: 0.5,
start: 0.0,
sweep: TAU,
}]
}
fn shape_kind(&self) -> ShapeKind {
ShapeKind::Circle
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Ellipse;
impl Shape for Ellipse {
type Iter = [PathCommand; 1];
fn path(&self) -> Self::Iter {
[PathCommand::Arc {
cx: 0.5,
cy: 0.5,
rx: 0.5,
ry: 0.5,
start: 0.0,
sweep: TAU,
}]
}
fn shape_kind(&self) -> ShapeKind {
ShapeKind::Ellipse
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Capsule;
impl Shape for Capsule {
type Iter = [PathCommand; 4];
fn path(&self) -> Self::Iter {
[
PathCommand::MoveTo { x: 0.5, y: 0.0 },
PathCommand::Arc {
cx: 0.5,
cy: 0.5,
rx: 0.5,
ry: 0.5,
start: -FRAC_PI_2,
sweep: PI,
},
PathCommand::Arc {
cx: 0.5,
cy: 0.5,
rx: 0.5,
ry: 0.5,
start: FRAC_PI_2,
sweep: PI,
},
PathCommand::Close,
]
}
fn shape_kind(&self) -> ShapeKind {
ShapeKind::Capsule
}
}
#[derive(Debug, Clone, Copy)]
pub struct RoundedRectangle {
pub corner_radius: f32,
}
impl RoundedRectangle {
#[must_use]
pub const fn new(corner_radius: f32) -> Self {
Self { corner_radius }
}
}
impl Shape for RoundedRectangle {
type Iter = [PathCommand; 10];
fn path(&self) -> Self::Iter {
let r = CornerRadii {
top_left: self.corner_radius,
top_right: self.corner_radius,
bottom_right: self.corner_radius,
bottom_left: self.corner_radius,
}
.sanitized()
.top_left;
[
PathCommand::MoveTo { x: r, y: 0.0 },
PathCommand::LineTo { x: 1.0 - r, y: 0.0 },
PathCommand::Arc {
cx: 1.0 - r,
cy: r,
rx: r,
ry: r,
start: -FRAC_PI_2,
sweep: FRAC_PI_2,
},
PathCommand::LineTo { x: 1.0, y: 1.0 - r },
PathCommand::Arc {
cx: 1.0 - r,
cy: 1.0 - r,
rx: r,
ry: r,
start: 0.0,
sweep: FRAC_PI_2,
},
PathCommand::LineTo { x: r, y: 1.0 },
PathCommand::Arc {
cx: r,
cy: 1.0 - r,
rx: r,
ry: r,
start: FRAC_PI_2,
sweep: FRAC_PI_2,
},
PathCommand::LineTo { x: 0.0, y: r },
PathCommand::Arc {
cx: r,
cy: r,
rx: r,
ry: r,
start: PI,
sweep: FRAC_PI_2,
},
PathCommand::Close,
]
}
fn shape_kind(&self) -> ShapeKind {
let r = CornerRadii {
top_left: self.corner_radius,
top_right: self.corner_radius,
bottom_right: self.corner_radius,
bottom_left: self.corner_radius,
}
.sanitized()
.top_left;
ShapeKind::RoundedRect { corner_radius: r }
}
}
#[derive(Debug, Clone, Copy)]
pub struct UnevenRoundedRectangle {
pub top_leading: f32,
pub top_trailing: f32,
pub bottom_leading: f32,
pub bottom_trailing: f32,
}
impl UnevenRoundedRectangle {
#[must_use]
pub const fn new(
top_leading: f32,
top_trailing: f32,
bottom_leading: f32,
bottom_trailing: f32,
) -> Self {
Self {
top_leading,
top_trailing,
bottom_leading,
bottom_trailing,
}
}
}
impl Shape for UnevenRoundedRectangle {
type Iter = [PathCommand; 10];
fn path(&self) -> Self::Iter {
let corners = CornerRadii {
top_left: self.top_leading,
top_right: self.top_trailing,
bottom_right: self.bottom_trailing,
bottom_left: self.bottom_leading,
}
.sanitized();
let tl = corners.top_left;
let tr = corners.top_right;
let bl = corners.bottom_left;
let br = corners.bottom_right;
[
PathCommand::MoveTo { x: tl, y: 0.0 },
PathCommand::LineTo {
x: 1.0 - tr,
y: 0.0,
},
PathCommand::Arc {
cx: 1.0 - tr,
cy: tr,
rx: tr,
ry: tr,
start: -FRAC_PI_2,
sweep: FRAC_PI_2,
},
PathCommand::LineTo {
x: 1.0,
y: 1.0 - br,
},
PathCommand::Arc {
cx: 1.0 - br,
cy: 1.0 - br,
rx: br,
ry: br,
start: 0.0,
sweep: FRAC_PI_2,
},
PathCommand::LineTo { x: bl, y: 1.0 },
PathCommand::Arc {
cx: bl,
cy: 1.0 - bl,
rx: bl,
ry: bl,
start: FRAC_PI_2,
sweep: FRAC_PI_2,
},
PathCommand::LineTo { x: 0.0, y: tl },
PathCommand::Arc {
cx: tl,
cy: tl,
rx: tl,
ry: tl,
start: PI,
sweep: FRAC_PI_2,
},
PathCommand::Close,
]
}
fn shape_kind(&self) -> ShapeKind {
let corners = CornerRadii {
top_left: self.top_leading,
top_right: self.top_trailing,
bottom_right: self.bottom_trailing,
bottom_left: self.bottom_leading,
}
.sanitized();
ShapeKind::UnevenRoundedRect {
top_left: corners.top_left,
top_right: corners.top_right,
bottom_left: corners.bottom_left,
bottom_right: corners.bottom_right,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Rectangle;
impl Shape for Rectangle {
type Iter = [PathCommand; 5];
fn path(&self) -> Self::Iter {
[
PathCommand::MoveTo { x: 0.0, y: 0.0 },
PathCommand::LineTo { x: 1.0, y: 0.0 },
PathCommand::LineTo { x: 1.0, y: 1.0 },
PathCommand::LineTo { x: 0.0, y: 1.0 },
PathCommand::Close,
]
}
fn shape_kind(&self) -> ShapeKind {
ShapeKind::Rect
}
}
#[derive(Debug, Clone, Default)]
pub struct Path {
commands: Vec<PathCommand>,
}
impl Path {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn move_to(mut self, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::MoveTo { x, y });
self
}
#[must_use]
pub fn line_to(mut self, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::LineTo { x, y });
self
}
#[must_use]
pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::QuadTo { cx, cy, x, y });
self
}
#[must_use]
pub fn cubic_to(mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) -> Self {
self.commands.push(PathCommand::CubicTo {
c1x,
c1y,
c2x,
c2y,
x,
y,
});
self
}
#[must_use]
pub fn arc(mut self, cx: f32, cy: f32, rx: f32, ry: f32, start: f32, sweep: f32) -> Self {
self.commands.push(PathCommand::Arc {
cx,
cy,
rx,
ry,
start,
sweep,
});
self
}
#[must_use]
pub fn close(mut self) -> Self {
self.commands.push(PathCommand::Close);
self
}
}
impl Shape for Path {
type Iter = alloc::vec::IntoIter<PathCommand>;
fn path(&self) -> Self::Iter {
self.commands.clone().into_iter()
}
fn shape_kind(&self) -> ShapeKind {
ShapeKind::CustomPath
}
}
#[derive(Debug)]
pub struct ClipShape {
kind: ShapeKind,
commands: Vec<PathCommand>,
}
impl ClipShape {
#[allow(clippy::needless_pass_by_value)]
pub fn new(shape: impl Shape) -> Self {
Self {
kind: shape.shape_kind(),
commands: shape.path().into_iter().collect(),
}
}
#[must_use]
pub const fn kind(&self) -> ShapeKind {
self.kind
}
#[must_use]
pub fn commands(&self) -> &[PathCommand] {
&self.commands
}
}
impl MetadataKey for ClipShape {}
#[derive(Debug, Clone, Copy, Default)]
pub enum ShapeKind {
#[default]
Rect,
Circle,
Ellipse,
RoundedRect {
corner_radius: f32,
},
UnevenRoundedRect {
top_left: f32,
top_right: f32,
bottom_left: f32,
bottom_right: f32,
},
Capsule,
CustomPath,
}
#[derive(Debug, Clone)]
pub struct ResolvedShape {
pub kind: ShapeKind,
pub commands: Vec<PathCommand>,
pub fill: Computed<waterui_graphics::ResolvedColor>,
}
waterui_core::raw_view!(ResolvedShape, waterui_core::layout::StretchAxis::Both);
#[derive(Debug, Clone)]
pub struct ResolvedMorphShape {
pub from: ShapeKind,
pub to: ShapeKind,
pub fill: Computed<waterui_graphics::ResolvedColor>,
pub animation: MorphAnimation,
pub progress: Option<Computed<f32>>,
}
impl waterui_core::NativeView for ResolvedMorphShape {
fn stretch_axis(&self) -> waterui_core::layout::StretchAxis {
waterui_core::layout::StretchAxis::Both
}
}
#[derive(Debug)]
pub struct FilledShape {
kind: ShapeKind,
commands: Vec<PathCommand>,
fill: Color,
}
impl FilledShape {
#[allow(clippy::needless_pass_by_value)]
pub fn new(shape: impl Shape, fill: impl Into<Color>) -> Self {
Self {
kind: ShapeKind::CustomPath,
commands: shape.path().into_iter().collect(),
fill: fill.into(),
}
}
#[allow(clippy::needless_pass_by_value)]
fn with_kind(kind: ShapeKind, shape: impl Shape, fill: impl Into<Color>) -> Self {
Self {
kind,
commands: shape.path().into_iter().collect(),
fill: fill.into(),
}
}
#[must_use]
pub fn commands(&self) -> &[PathCommand] {
&self.commands
}
#[must_use]
pub const fn fill(&self) -> &Color {
&self.fill
}
#[must_use]
pub const fn kind(&self) -> ShapeKind {
self.kind
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn morph_to(self, target: impl ShapeExt) -> MorphShape {
MorphShape::new(self.kind, target.shape_kind(), self.fill)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MorphAnimation {
pub duration: Duration,
pub easing: EasingCurve,
pub repeat: bool,
pub autoreverse: bool,
}
impl Default for MorphAnimation {
fn default() -> Self {
Self {
duration: Duration::from_millis(900),
easing: EasingCurve::EASE_IN_OUT,
repeat: true,
autoreverse: true,
}
}
}
impl MorphAnimation {
#[must_use]
pub const fn once(duration: Duration, easing: EasingCurve) -> Self {
Self {
duration,
easing,
repeat: false,
autoreverse: false,
}
}
#[cfg(feature = "gpu")]
#[must_use]
fn sample(self, elapsed: Duration) -> f32 {
if self.duration.is_zero() {
return 1.0;
}
let raw = elapsed.as_secs_f32() / self.duration.as_secs_f32();
let cycle = if self.repeat {
let base = raw.fract();
let index = raw
.floor()
.to_u64()
.expect("MorphAnimation::sample: cycle index must fit into u64");
if self.autoreverse && index % 2 == 1 {
1.0 - base
} else {
base
}
} else {
raw.clamp(0.0, 1.0)
};
self.easing.ease(cycle).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct MorphShape {
from: ShapeKind,
to: ShapeKind,
fill: Color,
animation: MorphAnimation,
progress: Option<Computed<f32>>,
}
impl MorphShape {
fn new(from: ShapeKind, to: ShapeKind, fill: Color) -> Self {
Self {
from,
to,
fill,
animation: MorphAnimation::default(),
progress: None,
}
}
#[must_use]
pub const fn animation(mut self, animation: MorphAnimation) -> Self {
self.animation = animation;
self
}
#[must_use]
pub const fn duration(mut self, duration: Duration) -> Self {
self.animation.duration = duration;
self
}
#[must_use]
pub const fn easing(mut self, easing: EasingCurve) -> Self {
self.animation.easing = easing;
self
}
#[must_use]
pub const fn repeat(mut self, repeat: bool) -> Self {
self.animation.repeat = repeat;
self
}
#[must_use]
pub const fn autoreverse(mut self, autoreverse: bool) -> Self {
self.animation.autoreverse = autoreverse;
self
}
#[must_use]
pub fn progress(mut self, progress: impl IntoComputed<f32>) -> Self {
self.progress = Some(progress.into_computed());
self
}
}
impl View for FilledShape {
fn body(self, env: &Environment) -> impl View {
ResolvedShape {
kind: self.kind,
commands: self.commands,
fill: self.fill.resolve(env).computed(),
}
}
}
impl View for MorphShape {
fn body(self, env: &Environment) -> impl View {
let resolved = self.fill.resolve(env).computed();
#[cfg(feature = "gpu")]
let progress_for_gpu = self.progress.clone();
let native = waterui_core::Native::new(ResolvedMorphShape {
from: self.from,
to: self.to,
fill: resolved,
animation: self.animation,
progress: self.progress,
});
#[cfg(feature = "gpu")]
let native = native.with_fallback(GpuSurface::new(MorphShapeRenderer::new(
kind_to_morph_shape(self.from)
.expect("morph source shape must be a built-in morphable shape"),
kind_to_morph_shape(self.to)
.expect("morph target shape must be a built-in morphable shape"),
ReactiveColor::new(&Computed::constant(self.fill), env),
self.animation,
progress_for_gpu,
)));
native
}
}
#[cfg(feature = "gpu")]
#[derive(Debug, Clone, Copy)]
struct MorphSdfShape {
shape_type: u32,
radii: [f32; 4],
}
#[cfg(feature = "gpu")]
fn kind_to_morph_shape(kind: ShapeKind) -> Option<MorphSdfShape> {
match kind {
ShapeKind::Rect => Some(MorphSdfShape {
shape_type: 0,
radii: [0.0; 4],
}),
ShapeKind::Circle => Some(MorphSdfShape {
shape_type: 1,
radii: [0.0; 4],
}),
ShapeKind::Ellipse => Some(MorphSdfShape {
shape_type: 2,
radii: [0.0; 4],
}),
ShapeKind::RoundedRect { corner_radius } => Some(MorphSdfShape {
shape_type: 3,
radii: [clamp_radius(corner_radius); 4],
}),
ShapeKind::UnevenRoundedRect {
top_left,
top_right,
bottom_left,
bottom_right,
} => {
let corners = CornerRadii {
top_left,
top_right,
bottom_right,
bottom_left,
}
.sanitized();
Some(MorphSdfShape {
shape_type: 3,
radii: [
corners.top_left,
corners.top_right,
corners.bottom_right,
corners.bottom_left,
],
})
}
ShapeKind::Capsule => Some(MorphSdfShape {
shape_type: 4,
radii: [0.0; 4],
}),
ShapeKind::CustomPath => None,
}
}
#[cfg(feature = "gpu")]
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)]
struct MorphUniforms {
color: [f32; 4],
dimensions_and_progress: [f32; 4], shape_types: [f32; 4], from_radii: [f32; 4], to_radii: [f32; 4], }
#[cfg(feature = "gpu")]
struct MorphShapeRenderer {
from: MorphSdfShape,
to: MorphSdfShape,
fill_color: ReactiveColor,
animation: MorphAnimation,
progress: Option<Computed<f32>>,
progress_guard: Option<BoxWatcherGuard>,
start_time: Instant,
pipeline: Option<wgpu::RenderPipeline>,
uniform_buffer: Option<wgpu::Buffer>,
bind_group: Option<wgpu::BindGroup>,
pipeline_format: Option<wgpu::TextureFormat>,
}
#[cfg(feature = "gpu")]
impl fmt::Debug for MorphShapeRenderer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MorphShapeRenderer")
.field("from", &self.from)
.field("to", &self.to)
.finish_non_exhaustive()
}
}
#[cfg(feature = "gpu")]
impl MorphShapeRenderer {
fn new(
from: MorphSdfShape,
to: MorphSdfShape,
fill_color: ReactiveColor,
animation: MorphAnimation,
progress: Option<Computed<f32>>,
) -> Self {
Self {
from,
to,
fill_color,
animation,
progress,
progress_guard: None,
start_time: Instant::now(),
pipeline: None,
uniform_buffer: None,
bind_group: None,
pipeline_format: None,
}
}
}
#[cfg(feature = "gpu")]
impl GpuView for MorphShapeRenderer {
fn setup(
&mut self,
ctx: &GpuContext<'_>,
_env: &mut waterui_core::Environment,
) -> impl core::future::Future<Output = ()> {
self.fill_color.install(&ctx.redraw_handle);
if let Some(progress) = &self.progress {
let redraw = ctx.redraw_handle.clone();
self.progress_guard = Some(progress.watch(move |_| redraw.request_redraw()));
}
let (vertex_shader, fragment_shader, bind_group_layout) = single_bind_group_render_stages(
&MORPH_SHADER,
ctx.device,
"the morph shape shader",
"vs_main",
"fs_main",
);
let uniform_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Morph Shape Uniforms"),
size: core::mem::size_of::<MorphUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Morph Shape Bind Group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let pipeline_layout = ctx
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Morph Shape Pipeline Layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let blend = ctx.alpha_blend_state();
let pipeline = ctx
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Morph Shape Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: vertex_shader.module(),
entry_point: Some(vertex_shader.entry_point()),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: fragment_shader.module(),
entry_point: Some(fragment_shader.entry_point()),
targets: &[Some(wgpu::ColorTargetState {
format: ctx.surface_format,
blend,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
self.pipeline = Some(pipeline);
self.uniform_buffer = Some(uniform_buffer);
self.bind_group = Some(bind_group);
self.pipeline_format = Some(ctx.surface_format);
self.start_time = Instant::now();
core::future::ready(())
}
fn render(&mut self, frame: &mut GpuFrame) {
assert_eq!(
self.pipeline_format,
Some(frame.format),
"MorphShape target format changed after setup"
);
let pipeline = self
.pipeline
.as_ref()
.expect("MorphShape render called before setup");
let uniform_buffer = self
.uniform_buffer
.as_ref()
.expect("MorphShape render called before setup");
let bind_group = self
.bind_group
.as_ref()
.expect("MorphShape render called before setup");
let progress = if let Some(signal) = &self.progress {
let value = signal.get();
assert!(value.is_finite(), "MorphShape progress must be finite");
value.clamp(0.0, 1.0)
} else {
self.animation.sample(self.start_time.elapsed())
};
let fill_color = self.fill_color.get();
let [r, g, b] = fill_color.linear_with_headroom();
let uniforms = MorphUniforms {
color: [r, g, b, fill_color.opacity],
dimensions_and_progress: [
u32_to_f32(frame.width),
u32_to_f32(frame.height),
progress,
0.0,
],
shape_types: [
u32_to_f32(self.from.shape_type),
u32_to_f32(self.to.shape_type),
0.0,
0.0,
],
from_radii: self.from.radii,
to_radii: self.to.radii,
};
frame
.queue
.write_buffer(uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
let mut encoder = frame
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Morph Shape Encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Morph Shape Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame.view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
frame.queue.submit(core::iter::once(encoder.finish()));
let animation_active = self.progress.is_none()
&& (self.animation.repeat || self.start_time.elapsed() < self.animation.duration);
if animation_active {
frame.request_redraw();
}
}
}
#[cfg(feature = "gpu")]
fn u32_to_f32(value: u32) -> f32 {
value
.to_f32()
.expect("shape dimensions must be representable as f32")
}
pub trait ShapeExt: Shape + Sized {
fn fill(self, color: impl Into<Color>) -> FilledShape {
FilledShape::with_kind(self.shape_kind(), self, color)
}
fn morph_to(self, target: impl ShapeExt, fill: impl Into<Color>) -> MorphShape {
MorphShape::new(self.shape_kind(), target.shape_kind(), fill.into())
}
}
impl ShapeExt for Circle {}
impl ShapeExt for Ellipse {}
impl ShapeExt for Capsule {}
impl ShapeExt for Rectangle {}
impl ShapeExt for RoundedRectangle {}
impl ShapeExt for UnevenRoundedRectangle {}
impl ShapeExt for Path {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounded_rectangle_radius_is_clamped() {
let kind = RoundedRectangle::new(9.0).shape_kind();
match kind {
ShapeKind::RoundedRect { corner_radius } => {
assert!((corner_radius - 0.5).abs() < 1e-6);
}
_ => panic!("unexpected kind"),
}
}
#[test]
fn uneven_radii_are_normalized_when_edges_overlap() {
let kind = UnevenRoundedRectangle::new(0.8, 0.8, 0.8, 0.8).shape_kind();
match kind {
ShapeKind::UnevenRoundedRect {
top_left,
top_right,
bottom_left,
bottom_right,
} => {
assert!((top_left - 0.5).abs() < 1e-6);
assert!((top_right - 0.5).abs() < 1e-6);
assert!((bottom_left - 0.5).abs() < 1e-6);
assert!((bottom_right - 0.5).abs() < 1e-6);
}
_ => panic!("unexpected kind"),
}
}
#[cfg(feature = "gpu")]
#[test]
fn one_shot_animation_reaches_end() {
let animation = MorphAnimation::once(Duration::from_millis(200), EasingCurve::LINEAR);
assert!((animation.sample(Duration::ZERO) - 0.0).abs() < 1e-6);
assert!((animation.sample(Duration::from_millis(100)) - 0.5).abs() < 1e-3);
assert!((animation.sample(Duration::from_secs(1)) - 1.0).abs() < 1e-6);
}
}