feat: use lyon for easing
This commit is contained in:
parent
cdfb8b3068
commit
2ebc923197
7 changed files with 214 additions and 116 deletions
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "progress_indicators"
|
||||
name = "loading_spinners"
|
||||
version = "0.1.0"
|
||||
authors = ["Nick Senger <dev@nsenger.com>"]
|
||||
edition = "2021"
|
||||
|
|
@ -10,4 +10,5 @@ flo_curves = "0.7"
|
|||
iced = { path = "../..", features = ["canvas"] }
|
||||
iced_core = { path = "../../core" }
|
||||
iced_widget = { path = "../../widget" }
|
||||
lazy_static = "1.4"
|
||||
once_cell = "1"
|
||||
lyon = "1"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
## Progress indicators
|
||||
## Loading Spinners
|
||||
|
||||
Example implementation of animated indeterminate progress indicators.
|
||||
Example implementation of animated indeterminate loading spinners.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/importantdevotedhammerheadbird">
|
||||
|
|
@ -10,5 +10,5 @@ Example implementation of animated indeterminate progress indicators.
|
|||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package progress_indicators
|
||||
cargo run --package loading_spinners
|
||||
```
|
||||
|
|
@ -14,7 +14,6 @@ use iced_core::{
|
|||
|
||||
use super::easing::{self, Easing};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::f32::consts::PI;
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ type R<Theme> = iced_widget::renderer::Renderer<Theme>;
|
|||
|
||||
const MIN_RADIANS: f32 = PI / 8.0;
|
||||
const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0;
|
||||
const PROCESSION_VELOCITY: u32 = u32::MAX / 120;
|
||||
const BASE_ROTATION_SPEED: u32 = u32::MAX / 80;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Circular<'a, Theme>
|
||||
|
|
@ -32,8 +31,9 @@ where
|
|||
size: f32,
|
||||
bar_height: f32,
|
||||
style: <Theme as StyleSheet>::Style,
|
||||
easing: Cow<'a, Easing>,
|
||||
easing: &'a Easing,
|
||||
cycle_duration: Duration,
|
||||
rotation_speed: u32,
|
||||
}
|
||||
|
||||
impl<'a, Theme> Circular<'a, Theme>
|
||||
|
|
@ -46,8 +46,9 @@ where
|
|||
size: 40.0,
|
||||
bar_height: 4.0,
|
||||
style: <Theme as StyleSheet>::Style::default(),
|
||||
easing: Cow::Borrowed(&easing::STANDARD),
|
||||
easing: &easing::STANDARD,
|
||||
cycle_duration: Duration::from_millis(600),
|
||||
rotation_speed: BASE_ROTATION_SPEED,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,8 +71,8 @@ where
|
|||
}
|
||||
|
||||
/// Sets the easing of this [`Circular`].
|
||||
pub fn easing(mut self, easing: impl Into<Cow<'a, Easing>>) -> Self {
|
||||
self.easing = easing.into();
|
||||
pub fn easing(mut self, easing: &'a Easing) -> Self {
|
||||
self.easing = easing;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +81,14 @@ where
|
|||
self.cycle_duration = duration / 2;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the rotation speed of this [`Circular`]. Must be set to between 0.0 and 10.0.
|
||||
/// Defaults to 1.0.
|
||||
pub fn rotation_speed(mut self, speed: f32) -> Self {
|
||||
let multiplier = speed.min(10.0).max(0.0);
|
||||
self.rotation_speed = (BASE_ROTATION_SPEED as f32 * multiplier) as u32;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Theme> Default for Circular<'a, Theme>
|
||||
|
|
@ -121,13 +130,13 @@ impl State {
|
|||
Self::Expanding { procession, .. } => Self::Contracting {
|
||||
start: now,
|
||||
progress: 0.0,
|
||||
procession: procession.wrapping_add(PROCESSION_VELOCITY),
|
||||
procession: procession.wrapping_add(BASE_ROTATION_SPEED),
|
||||
},
|
||||
Self::Contracting { procession, .. } => Self::Expanding {
|
||||
start: now,
|
||||
progress: 0.0,
|
||||
procession: procession.wrapping_add(
|
||||
PROCESSION_VELOCITY.wrapping_add(
|
||||
BASE_ROTATION_SPEED.wrapping_add(
|
||||
((WRAP_RADIANS / (2.0 * PI)) * u32::MAX as f32) as u32,
|
||||
),
|
||||
),
|
||||
|
|
@ -143,18 +152,24 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self {
|
||||
fn timed_transition(
|
||||
&self,
|
||||
cycle_duration: Duration,
|
||||
rotation_speed: u32,
|
||||
now: Instant,
|
||||
) -> Self {
|
||||
let elapsed = now.duration_since(self.start());
|
||||
|
||||
match elapsed {
|
||||
elapsed if elapsed > cycle_duration => self.next(now),
|
||||
_ => self.with_elapsed(cycle_duration, elapsed),
|
||||
_ => self.with_elapsed(cycle_duration, rotation_speed, elapsed),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_elapsed(
|
||||
&self,
|
||||
cycle_duration: Duration,
|
||||
rotation_speed: u32,
|
||||
elapsed: Duration,
|
||||
) -> Self {
|
||||
let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32();
|
||||
|
|
@ -164,14 +179,14 @@ impl State {
|
|||
} => Self::Expanding {
|
||||
start: *start,
|
||||
progress,
|
||||
procession: procession.wrapping_add(PROCESSION_VELOCITY),
|
||||
procession: procession.wrapping_add(rotation_speed),
|
||||
},
|
||||
Self::Contracting {
|
||||
start, procession, ..
|
||||
} => Self::Contracting {
|
||||
start: *start,
|
||||
progress,
|
||||
procession: procession.wrapping_add(PROCESSION_VELOCITY),
|
||||
procession: procession.wrapping_add(rotation_speed),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -231,7 +246,11 @@ where
|
|||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(now)) = event {
|
||||
*state = state.timed_transition(self.cycle_duration, now);
|
||||
*state = state.timed_transition(
|
||||
self.cycle_duration,
|
||||
self.rotation_speed,
|
||||
now,
|
||||
);
|
||||
|
||||
shell.request_redraw(RedrawRequest::NextFrame);
|
||||
}
|
||||
|
|
@ -263,7 +282,7 @@ where
|
|||
state,
|
||||
style: &self.style,
|
||||
bar_height: self.bar_height,
|
||||
easing: self.easing.as_ref(),
|
||||
easing: self.easing,
|
||||
},
|
||||
&(),
|
||||
renderer,
|
||||
132
examples/loading_spinners/src/easing.rs
Normal file
132
examples/loading_spinners/src/easing.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
use iced_core::Point;
|
||||
use lyon::algorithms::measure::PathMeasurements;
|
||||
use lyon::path::builder::NoAttributes;
|
||||
use lyon::path::path::BuilderImpl;
|
||||
use lyon::path::Path;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub static EMPHASIZED: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.05, 0.0], [0.133333, 0.06], [0.166666, 0.4])
|
||||
.cubic_bezier_to([0.208333, 0.82], [0.25, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static EMPHASIZED_DECELERATE: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.05, 0.7], [0.1, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static EMPHASIZED_ACCELERATE: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.3, 0.0], [0.8, 0.15], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static STANDARD: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.2, 0.0], [0.0, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static STANDARD_DECELERATE: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.0, 0.0], [0.0, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static STANDARD_ACCELERATE: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.3, 0.0], [1.0, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub struct Easing {
|
||||
path: Path,
|
||||
measurements: PathMeasurements,
|
||||
}
|
||||
|
||||
impl Easing {
|
||||
pub fn builder() -> Builder {
|
||||
Builder::new()
|
||||
}
|
||||
|
||||
pub fn y_at_x(&self, x: f32) -> f32 {
|
||||
let mut sampler = self.measurements.create_sampler(
|
||||
&self.path,
|
||||
lyon::algorithms::measure::SampleType::Normalized,
|
||||
);
|
||||
let sample = sampler.sample(x);
|
||||
|
||||
sample.position().y
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Builder(NoAttributes<BuilderImpl>);
|
||||
|
||||
impl Builder {
|
||||
pub fn new() -> Self {
|
||||
let mut builder = Path::builder();
|
||||
builder.begin(lyon::geom::point(0.0, 0.0));
|
||||
|
||||
Self(builder)
|
||||
}
|
||||
|
||||
/// Adds a line segment. Points must be between 0,0 and 1,1
|
||||
pub fn line_to(mut self, to: impl Into<Point>) -> Self {
|
||||
self.0.line_to(Self::point(to));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a quadratic bézier curve. Points must be between 0,0 and 1,1
|
||||
pub fn quadratic_bezier_to(
|
||||
mut self,
|
||||
ctrl: impl Into<Point>,
|
||||
to: impl Into<Point>,
|
||||
) -> Self {
|
||||
self.0
|
||||
.quadratic_bezier_to(Self::point(ctrl), Self::point(to));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a cubic bézier curve. Points must be between 0,0 and 1,1
|
||||
pub fn cubic_bezier_to(
|
||||
mut self,
|
||||
ctrl1: impl Into<Point>,
|
||||
ctrl2: impl Into<Point>,
|
||||
to: impl Into<Point>,
|
||||
) -> Self {
|
||||
self.0.cubic_bezier_to(
|
||||
Self::point(ctrl1),
|
||||
Self::point(ctrl2),
|
||||
Self::point(to),
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> Easing {
|
||||
self.0.line_to(lyon::geom::point(1.0, 1.0));
|
||||
self.0.end(false);
|
||||
|
||||
let path = self.0.build();
|
||||
let measurements = PathMeasurements::from_path(&path, 0.0);
|
||||
|
||||
Easing { path, measurements }
|
||||
}
|
||||
|
||||
fn point(p: impl Into<Point>) -> lyon::geom::Point<f32> {
|
||||
let p: Point = p.into();
|
||||
lyon::geom::point(p.x.min(1.0).max(0.0), p.y.min(1.0).max(0.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Builder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ use iced_core::{
|
|||
|
||||
use super::easing::{self, Easing};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::time::Duration;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
|
|
@ -25,7 +24,7 @@ where
|
|||
width: Length,
|
||||
height: Length,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
easing: Cow<'a, Easing>,
|
||||
easing: &'a Easing,
|
||||
cycle_duration: Duration,
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +39,7 @@ where
|
|||
width: Length::Fixed(100.0),
|
||||
height: Length::Fixed(4.0),
|
||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||
easing: Cow::Borrowed(&easing::STANDARD),
|
||||
easing: &easing::STANDARD,
|
||||
cycle_duration: Duration::from_millis(600),
|
||||
}
|
||||
}
|
||||
|
|
@ -67,8 +66,8 @@ where
|
|||
}
|
||||
|
||||
/// Sets the motion easing of this [`Linear`].
|
||||
pub fn easing(mut self, easing: impl Into<Cow<'a, Easing>>) -> Self {
|
||||
self.easing = easing.into();
|
||||
pub fn easing(mut self, easing: &'a Easing) -> Self {
|
||||
self.easing = easing;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -12,17 +12,17 @@ use circular::Circular;
|
|||
use linear::Linear;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
ProgressIndicators::run(Settings {
|
||||
LoadingSpinners::run(Settings {
|
||||
antialiasing: true,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
struct ProgressIndicators {
|
||||
struct LoadingSpinners {
|
||||
cycle_duration: f32,
|
||||
}
|
||||
|
||||
impl Default for ProgressIndicators {
|
||||
impl Default for LoadingSpinners {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cycle_duration: 2.0,
|
||||
|
|
@ -35,7 +35,7 @@ enum Message {
|
|||
CycleDurationChanged(f32),
|
||||
}
|
||||
|
||||
impl Application for ProgressIndicators {
|
||||
impl Application for LoadingSpinners {
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
type Executor = executor::Default;
|
||||
|
|
@ -46,7 +46,7 @@ impl Application for ProgressIndicators {
|
|||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Progress Indicators - Iced")
|
||||
String::from("Loading Spinners - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
|
|
@ -60,25 +60,39 @@ impl Application for ProgressIndicators {
|
|||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let column = easing::EXAMPLES
|
||||
.iter()
|
||||
.zip(["Decelerating:", "Accelerating:", "Standard:"])
|
||||
.fold(column![], |column, (easing, label)| {
|
||||
column.push(
|
||||
row![
|
||||
text(label).width(150),
|
||||
Linear::new().easing(easing).cycle_duration(
|
||||
Duration::from_secs_f32(self.cycle_duration)
|
||||
),
|
||||
Circular::new().easing(easing).cycle_duration(
|
||||
Duration::from_secs_f32(self.cycle_duration)
|
||||
)
|
||||
]
|
||||
.align_items(iced::Alignment::Center)
|
||||
.spacing(20.0),
|
||||
)
|
||||
})
|
||||
.spacing(20);
|
||||
let column = [
|
||||
&easing::EMPHASIZED,
|
||||
&easing::EMPHASIZED_DECELERATE,
|
||||
&easing::EMPHASIZED_ACCELERATE,
|
||||
&easing::STANDARD,
|
||||
&easing::STANDARD_DECELERATE,
|
||||
&easing::STANDARD_ACCELERATE,
|
||||
]
|
||||
.iter()
|
||||
.zip([
|
||||
"Emphasized:",
|
||||
"Emphasized Decelerate:",
|
||||
"Emphasized Accelerate:",
|
||||
"Standard:",
|
||||
"Standard Decelerate:",
|
||||
"Standard Accelerate:",
|
||||
])
|
||||
.fold(column![], |column, (easing, label)| {
|
||||
column.push(
|
||||
row![
|
||||
text(label).width(250),
|
||||
Linear::new().easing(easing).cycle_duration(
|
||||
Duration::from_secs_f32(self.cycle_duration)
|
||||
),
|
||||
Circular::new().easing(easing).cycle_duration(
|
||||
Duration::from_secs_f32(self.cycle_duration)
|
||||
)
|
||||
]
|
||||
.align_items(iced::Alignment::Center)
|
||||
.spacing(20.0),
|
||||
)
|
||||
})
|
||||
.spacing(20);
|
||||
|
||||
container(
|
||||
column.push(
|
||||
|
|
@ -87,7 +101,7 @@ impl Application for ProgressIndicators {
|
|||
slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| {
|
||||
Message::CycleDurationChanged(x / 100.0)
|
||||
})
|
||||
.width(150.0)
|
||||
.width(200.0)
|
||||
.into(),
|
||||
text(format!("{:.2}s", self.cycle_duration)).into(),
|
||||
])
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
use flo_curves::bezier::Curve;
|
||||
use flo_curves::*;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref EXAMPLES: [Easing; 3] = [
|
||||
Easing::CubicBezier(Curve::from_points(
|
||||
Coord2(0.0, 0.0),
|
||||
(Coord2(0.05, 0.7), Coord2(0.1, 1.0)),
|
||||
Coord2(1.0, 1.0),
|
||||
)),
|
||||
Easing::CubicBezier(Curve::from_points(
|
||||
Coord2(0.0, 0.0),
|
||||
(Coord2(0.3, 0.0), Coord2(0.8, 0.15)),
|
||||
Coord2(1.0, 1.0),
|
||||
)),
|
||||
Easing::CubicBezier(Curve::from_points(
|
||||
Coord2(0.0, 0.0),
|
||||
(Coord2(0.2, 0.0), Coord2(0.0, 1.0)),
|
||||
Coord2(1.0, 1.0),
|
||||
))
|
||||
];
|
||||
pub static ref STANDARD: Easing = {
|
||||
Easing::CubicBezier(Curve::from_points(
|
||||
Coord2(0.0, 0.0),
|
||||
(Coord2(0.2, 0.0), Coord2(0.0, 1.0)),
|
||||
Coord2(1.0, 1.0),
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Easing {
|
||||
BezierPath(Vec<Curve<Coord2>>),
|
||||
CubicBezier(Curve<Coord2>),
|
||||
}
|
||||
|
||||
impl Easing {
|
||||
pub fn y_at_x(&self, x: f32) -> f32 {
|
||||
let x = x as f64;
|
||||
|
||||
match self {
|
||||
Self::BezierPath(curves) => curves
|
||||
.iter()
|
||||
.find_map(|curve| {
|
||||
(curve.start_point().0 <= x && curve.end_point().0 >= x)
|
||||
.then(|| curve.point_at_pos(x).1 as f32)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
Self::CubicBezier(curve) => curve.point_at_pos(x).1 as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Easing> for Cow<'a, Easing> {
|
||||
fn from(easing: Easing) -> Self {
|
||||
Cow::Owned(easing)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Easing> for Cow<'a, Easing> {
|
||||
fn from(easing: &'a Easing) -> Self {
|
||||
Cow::Borrowed(easing)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue