Skip to content

Commit 00a0486

Browse files
authored
Merge pull request #2757 from iced-rs/feature/animation-api
`Animation` API for application code
2 parents da1726b + cd445f7 commit 00a0486

File tree

12 files changed

+883
-121
lines changed

12 files changed

+883
-121
lines changed

Cargo.lock

+238-114
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ half = "2.2"
158158
image = { version = "0.25", default-features = false }
159159
kamadak-exif = "0.5"
160160
kurbo = "0.10"
161+
lilt = "0.7"
161162
log = "0.4"
162163
lyon = "1.0"
163164
lyon_path = "1.0"
@@ -192,7 +193,7 @@ window_clipboard = "0.4.1"
192193
winit = { git = "https://github.com/iced-rs/winit.git", rev = "11414b6aa45699f038114e61b4ddf5102b2d3b4b" }
193194

194195
[workspace.lints.rust]
195-
rust_2018_idioms = { level = "forbid", priority = -1 }
196+
rust_2018_idioms = { level = "deny", priority = -1 }
196197
missing_debug_implementations = "deny"
197198
missing_docs = "deny"
198199
unsafe_code = "deny"

core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ advanced = []
2121
bitflags.workspace = true
2222
bytes.workspace = true
2323
glam.workspace = true
24+
lilt.workspace = true
2425
log.workspace = true
2526
num-traits.workspace = true
2627
palette.workspace = true

core/src/animation.rs

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//! Animate your applications.
2+
use crate::time::{Duration, Instant};
3+
4+
pub use lilt::{Easing, FloatRepresentable as Float, Interpolable};
5+
6+
/// The animation of some particular state.
7+
///
8+
/// It tracks state changes and allows projecting interpolated values
9+
/// through time.
10+
#[derive(Debug, Clone)]
11+
pub struct Animation<T>
12+
where
13+
T: Clone + Copy + PartialEq + Float,
14+
{
15+
raw: lilt::Animated<T, Instant>,
16+
}
17+
18+
impl<T> Animation<T>
19+
where
20+
T: Clone + Copy + PartialEq + Float,
21+
{
22+
/// Creates a new [`Animation`] with the given initial state.
23+
pub fn new(state: T) -> Self {
24+
Self {
25+
raw: lilt::Animated::new(state),
26+
}
27+
}
28+
29+
/// Sets the [`Easing`] function of the [`Animation`].
30+
///
31+
/// See the [Easing Functions Cheat Sheet](https://easings.net) for
32+
/// details!
33+
pub fn easing(mut self, easing: Easing) -> Self {
34+
self.raw = self.raw.easing(easing);
35+
self
36+
}
37+
38+
/// Sets the duration of the [`Animation`] to 100ms.
39+
pub fn very_quick(self) -> Self {
40+
self.duration(Duration::from_millis(100))
41+
}
42+
43+
/// Sets the duration of the [`Animation`] to 200ms.
44+
pub fn quick(self) -> Self {
45+
self.duration(Duration::from_millis(200))
46+
}
47+
48+
/// Sets the duration of the [`Animation`] to 400ms.
49+
pub fn slow(self) -> Self {
50+
self.duration(Duration::from_millis(400))
51+
}
52+
53+
/// Sets the duration of the [`Animation`] to 500ms.
54+
pub fn very_slow(self) -> Self {
55+
self.duration(Duration::from_millis(500))
56+
}
57+
58+
/// Sets the duration of the [`Animation`] to the given value.
59+
pub fn duration(mut self, duration: Duration) -> Self {
60+
self.raw = self.raw.duration(duration.as_secs_f32() * 1_000.0);
61+
self
62+
}
63+
64+
/// Sets a delay for the [`Animation`].
65+
pub fn delay(mut self, duration: Duration) -> Self {
66+
self.raw = self.raw.delay(duration.as_secs_f64() as f32 * 1000.0);
67+
self
68+
}
69+
70+
/// Makes the [`Animation`] repeat a given amount of times.
71+
///
72+
/// Providing 1 repetition plays the animation twice in total.
73+
pub fn repeat(mut self, repetitions: u32) -> Self {
74+
self.raw = self.raw.repeat(repetitions);
75+
self
76+
}
77+
78+
/// Makes the [`Animation`] repeat forever.
79+
pub fn repeat_forever(mut self) -> Self {
80+
self.raw = self.raw.repeat_forever();
81+
self
82+
}
83+
84+
/// Makes the [`Animation`] automatically reverse when repeating.
85+
pub fn auto_reverse(mut self) -> Self {
86+
self.raw = self.raw.auto_reverse();
87+
self
88+
}
89+
90+
/// Transitions the [`Animation`] from its current state to the given new state.
91+
pub fn go(mut self, new_state: T) -> Self {
92+
self.go_mut(new_state);
93+
self
94+
}
95+
96+
/// Transitions the [`Animation`] from its current state to the given new state, by reference.
97+
pub fn go_mut(&mut self, new_state: T) {
98+
self.raw.transition(new_state, Instant::now());
99+
}
100+
101+
/// Returns true if the [`Animation`] is currently in progress.
102+
///
103+
/// An [`Animation`] is in progress when it is transitioning to a different state.
104+
pub fn is_animating(&self, at: Instant) -> bool {
105+
self.raw.in_progress(at)
106+
}
107+
108+
/// Projects the [`Animation`] into an interpolated value at the given [`Instant`]; using the
109+
/// closure provided to calculate the different keyframes of interpolated values.
110+
///
111+
/// If the [`Animation`] state is a `bool`, you can use the simpler [`interpolate`] method.
112+
///
113+
/// [`interpolate`]: Animation::interpolate
114+
pub fn interpolate_with<I>(&self, f: impl Fn(T) -> I, at: Instant) -> I
115+
where
116+
I: Interpolable,
117+
{
118+
self.raw.animate(f, at)
119+
}
120+
121+
/// Retuns the current state of the [`Animation`].
122+
pub fn value(&self) -> T {
123+
self.raw.value
124+
}
125+
}
126+
127+
impl Animation<bool> {
128+
/// Projects the [`Animation`] into an interpolated value at the given [`Instant`]; using the
129+
/// `start` and `end` values as the origin and destination keyframes.
130+
pub fn interpolate<I>(&self, start: I, end: I, at: Instant) -> I
131+
where
132+
I: Interpolable + Clone,
133+
{
134+
self.raw.animate_bool(start, end, at)
135+
}
136+
}

core/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
1111
)]
1212
pub mod alignment;
13+
#[cfg(not(target_arch = "wasm32"))]
14+
pub mod animation;
1315
pub mod border;
1416
pub mod clipboard;
1517
pub mod event;
@@ -49,6 +51,8 @@ mod vector;
4951

5052
pub use alignment::Alignment;
5153
pub use angle::{Degrees, Radians};
54+
#[cfg(not(target_arch = "wasm32"))]
55+
pub use animation::Animation;
5256
pub use background::Background;
5357
pub use border::Border;
5458
pub use clipboard::Clipboard;

examples/changelog/Cargo.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,4 @@ tracing-subscriber = "0.3"
2323

2424
[dependencies.reqwest]
2525
version = "0.12"
26-
default-features = false
27-
features = ["json", "rustls-tls"]
26+
features = ["json"]

examples/download_progress/Cargo.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,4 @@ iced.features = ["tokio"]
1111

1212
[dependencies.reqwest]
1313
version = "0.12"
14-
default-features = false
15-
features = ["stream", "rustls-tls"]
14+
features = ["stream"]

examples/gallery/Cargo.toml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "gallery"
3+
version = "0.1.0"
4+
authors = ["Héctor Ramón Jiménez <[email protected]>"]
5+
edition = "2021"
6+
publish = false
7+
8+
[dependencies]
9+
iced.workspace = true
10+
iced.features = ["tokio", "image", "web-colors", "debug"]
11+
12+
reqwest.version = "0.12"
13+
reqwest.features = ["json"]
14+
15+
serde.version = "1.0"
16+
serde.features = ["derive"]
17+
18+
bytes.workspace = true
19+
image.workspace = true
20+
tokio.workspace = true
21+
22+
[lints]
23+
workspace = true

examples/gallery/src/civitai.rs

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
use bytes::Bytes;
2+
use serde::Deserialize;
3+
use tokio::task;
4+
5+
use std::fmt;
6+
use std::io;
7+
use std::sync::Arc;
8+
9+
#[derive(Debug, Clone, Deserialize)]
10+
pub struct Image {
11+
pub id: Id,
12+
url: String,
13+
}
14+
15+
impl Image {
16+
pub const LIMIT: usize = 99;
17+
18+
pub async fn list() -> Result<Vec<Self>, Error> {
19+
let client = reqwest::Client::new();
20+
21+
#[derive(Deserialize)]
22+
struct Response {
23+
items: Vec<Image>,
24+
}
25+
26+
let response: Response = client
27+
.get("https://civitai.com/api/v1/images")
28+
.query(&[
29+
("sort", "Most Reactions"),
30+
("period", "Week"),
31+
("nsfw", "None"),
32+
("limit", &Image::LIMIT.to_string()),
33+
])
34+
.send()
35+
.await?
36+
.error_for_status()?
37+
.json()
38+
.await?;
39+
40+
Ok(response.items)
41+
}
42+
43+
pub async fn download(self, size: Size) -> Result<Rgba, Error> {
44+
let client = reqwest::Client::new();
45+
46+
let bytes = client
47+
.get(match size {
48+
Size::Original => self.url,
49+
Size::Thumbnail => self
50+
.url
51+
.split("/")
52+
.map(|part| {
53+
if part.starts_with("width=") {
54+
"width=640"
55+
} else {
56+
part
57+
}
58+
})
59+
.collect::<Vec<_>>()
60+
.join("/"),
61+
})
62+
.send()
63+
.await?
64+
.error_for_status()?
65+
.bytes()
66+
.await?;
67+
68+
let image = task::spawn_blocking(move || {
69+
Ok::<_, Error>(
70+
image::ImageReader::new(io::Cursor::new(bytes))
71+
.with_guessed_format()?
72+
.decode()?
73+
.to_rgba8(),
74+
)
75+
})
76+
.await??;
77+
78+
Ok(Rgba {
79+
width: image.width(),
80+
height: image.height(),
81+
pixels: Bytes::from(image.into_raw()),
82+
})
83+
}
84+
}
85+
86+
#[derive(
87+
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize,
88+
)]
89+
pub struct Id(u32);
90+
91+
#[derive(Clone)]
92+
pub struct Rgba {
93+
pub width: u32,
94+
pub height: u32,
95+
pub pixels: Bytes,
96+
}
97+
98+
impl fmt::Debug for Rgba {
99+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100+
f.debug_struct("Rgba")
101+
.field("width", &self.width)
102+
.field("height", &self.height)
103+
.finish()
104+
}
105+
}
106+
107+
#[derive(Debug, Clone, Copy)]
108+
pub enum Size {
109+
Original,
110+
Thumbnail,
111+
}
112+
113+
#[derive(Debug, Clone)]
114+
#[allow(dead_code)]
115+
pub enum Error {
116+
RequestFailed(Arc<reqwest::Error>),
117+
IOFailed(Arc<io::Error>),
118+
JoinFailed(Arc<task::JoinError>),
119+
ImageDecodingFailed(Arc<image::ImageError>),
120+
}
121+
122+
impl From<reqwest::Error> for Error {
123+
fn from(error: reqwest::Error) -> Self {
124+
Self::RequestFailed(Arc::new(error))
125+
}
126+
}
127+
128+
impl From<io::Error> for Error {
129+
fn from(error: io::Error) -> Self {
130+
Self::IOFailed(Arc::new(error))
131+
}
132+
}
133+
134+
impl From<task::JoinError> for Error {
135+
fn from(error: task::JoinError) -> Self {
136+
Self::JoinFailed(Arc::new(error))
137+
}
138+
}
139+
140+
impl From<image::ImageError> for Error {
141+
fn from(error: image::ImageError) -> Self {
142+
Self::ImageDecodingFailed(Arc::new(error))
143+
}
144+
}

0 commit comments

Comments
 (0)