Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animation API for application code #2757

Merged
merged 10 commits into from
Jan 28, 2025
352 changes: 238 additions & 114 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ half = "2.2"
image = { version = "0.25", default-features = false }
kamadak-exif = "0.5"
kurbo = "0.10"
lilt = "0.7"
log = "0.4"
lyon = "1.0"
lyon_path = "1.0"
Expand Down Expand Up @@ -192,7 +193,7 @@ window_clipboard = "0.4.1"
winit = { git = "https://github.com/iced-rs/winit.git", rev = "11414b6aa45699f038114e61b4ddf5102b2d3b4b" }

[workspace.lints.rust]
rust_2018_idioms = { level = "forbid", priority = -1 }
rust_2018_idioms = { level = "deny", priority = -1 }
missing_debug_implementations = "deny"
missing_docs = "deny"
unsafe_code = "deny"
Expand Down
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ advanced = []
bitflags.workspace = true
bytes.workspace = true
glam.workspace = true
lilt.workspace = true
log.workspace = true
num-traits.workspace = true
palette.workspace = true
Expand Down
136 changes: 136 additions & 0 deletions core/src/animation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//! Animate your applications.
use crate::time::{Duration, Instant};

pub use lilt::{Easing, FloatRepresentable as Float, Interpolable};

/// The animation of some particular state.
///
/// It tracks state changes and allows projecting interpolated values
/// through time.
#[derive(Debug, Clone)]
pub struct Animation<T>
where
T: Clone + Copy + PartialEq + Float,
{
raw: lilt::Animated<T, Instant>,
}

impl<T> Animation<T>
where
T: Clone + Copy + PartialEq + Float,
{
/// Creates a new [`Animation`] with the given initial state.
pub fn new(state: T) -> Self {
Self {
raw: lilt::Animated::new(state),
}
}

/// Sets the [`Easing`] function of the [`Animation`].
///
/// See the [Easing Functions Cheat Sheet](https://easings.net) for
/// details!
pub fn easing(mut self, easing: Easing) -> Self {
self.raw = self.raw.easing(easing);
self
}

/// Sets the duration of the [`Animation`] to 100ms.
pub fn very_quick(self) -> Self {
self.duration(Duration::from_millis(100))
}

/// Sets the duration of the [`Animation`] to 200ms.
pub fn quick(self) -> Self {
self.duration(Duration::from_millis(200))
}

/// Sets the duration of the [`Animation`] to 400ms.
pub fn slow(self) -> Self {
self.duration(Duration::from_millis(400))
}

/// Sets the duration of the [`Animation`] to 500ms.
pub fn very_slow(self) -> Self {
self.duration(Duration::from_millis(500))
}

/// Sets the duration of the [`Animation`] to the given value.
pub fn duration(mut self, duration: Duration) -> Self {
self.raw = self.raw.duration(duration.as_secs_f32() * 1_000.0);
self
}

/// Sets a delay for the [`Animation`].
pub fn delay(mut self, duration: Duration) -> Self {
self.raw = self.raw.delay(duration.as_secs_f64() as f32 * 1000.0);
self
}

/// Makes the [`Animation`] repeat a given amount of times.
///
/// Providing 1 repetition plays the animation twice in total.
pub fn repeat(mut self, repetitions: u32) -> Self {
self.raw = self.raw.repeat(repetitions);
self
}

/// Makes the [`Animation`] repeat forever.
pub fn repeat_forever(mut self) -> Self {
self.raw = self.raw.repeat_forever();
self
}

/// Makes the [`Animation`] automatically reverse when repeating.
pub fn auto_reverse(mut self) -> Self {
self.raw = self.raw.auto_reverse();
self
}

/// Transitions the [`Animation`] from its current state to the given new state.
pub fn go(mut self, new_state: T) -> Self {
self.go_mut(new_state);
self
}

/// Transitions the [`Animation`] from its current state to the given new state, by reference.
pub fn go_mut(&mut self, new_state: T) {
self.raw.transition(new_state, Instant::now());
}

/// Returns true if the [`Animation`] is currently in progress.
///
/// An [`Animation`] is in progress when it is transitioning to a different state.
pub fn is_animating(&self, at: Instant) -> bool {
self.raw.in_progress(at)
}

/// Projects the [`Animation`] into an interpolated value at the given [`Instant`]; using the
/// closure provided to calculate the different keyframes of interpolated values.
///
/// If the [`Animation`] state is a `bool`, you can use the simpler [`interpolate`] method.
///
/// [`interpolate`]: Animation::interpolate
pub fn interpolate_with<I>(&self, f: impl Fn(T) -> I, at: Instant) -> I
where
I: Interpolable,
{
self.raw.animate(f, at)
}

/// Retuns the current state of the [`Animation`].
pub fn value(&self) -> T {
self.raw.value
}
}

impl Animation<bool> {
/// Projects the [`Animation`] into an interpolated value at the given [`Instant`]; using the
/// `start` and `end` values as the origin and destination keyframes.
pub fn interpolate<I>(&self, start: I, end: I, at: Instant) -> I
where
I: Interpolable + Clone,
{
self.raw.animate_bool(start, end, at)
}
}
4 changes: 4 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
pub mod alignment;
#[cfg(not(target_arch = "wasm32"))]
pub mod animation;
pub mod border;
pub mod clipboard;
pub mod event;
Expand Down Expand Up @@ -49,6 +51,8 @@ mod vector;

pub use alignment::Alignment;
pub use angle::{Degrees, Radians};
#[cfg(not(target_arch = "wasm32"))]
pub use animation::Animation;
pub use background::Background;
pub use border::Border;
pub use clipboard::Clipboard;
Expand Down
3 changes: 1 addition & 2 deletions examples/changelog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,4 @@ tracing-subscriber = "0.3"

[dependencies.reqwest]
version = "0.12"
default-features = false
features = ["json", "rustls-tls"]
features = ["json"]
3 changes: 1 addition & 2 deletions examples/download_progress/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ iced.features = ["tokio"]

[dependencies.reqwest]
version = "0.12"
default-features = false
features = ["stream", "rustls-tls"]
features = ["stream"]
23 changes: 23 additions & 0 deletions examples/gallery/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "gallery"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
publish = false

[dependencies]
iced.workspace = true
iced.features = ["tokio", "image", "web-colors", "debug"]

reqwest.version = "0.12"
reqwest.features = ["json"]

serde.version = "1.0"
serde.features = ["derive"]

bytes.workspace = true
image.workspace = true
tokio.workspace = true

[lints]
workspace = true
144 changes: 144 additions & 0 deletions examples/gallery/src/civitai.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use bytes::Bytes;
use serde::Deserialize;
use tokio::task;

use std::fmt;
use std::io;
use std::sync::Arc;

#[derive(Debug, Clone, Deserialize)]
pub struct Image {
pub id: Id,
url: String,
}

impl Image {
pub const LIMIT: usize = 99;

pub async fn list() -> Result<Vec<Self>, Error> {
let client = reqwest::Client::new();

#[derive(Deserialize)]
struct Response {
items: Vec<Image>,
}

let response: Response = client
.get("https://civitai.com/api/v1/images")
.query(&[
("sort", "Most Reactions"),
("period", "Week"),
("nsfw", "None"),
("limit", &Image::LIMIT.to_string()),
])
.send()
.await?
.error_for_status()?
.json()
.await?;

Ok(response.items)
}

pub async fn download(self, size: Size) -> Result<Rgba, Error> {
let client = reqwest::Client::new();

let bytes = client
.get(match size {
Size::Original => self.url,
Size::Thumbnail => self
.url
.split("/")
.map(|part| {
if part.starts_with("width=") {
"width=640"
} else {
part
}
})
.collect::<Vec<_>>()
.join("/"),
})
.send()
.await?
.error_for_status()?
.bytes()
.await?;

let image = task::spawn_blocking(move || {
Ok::<_, Error>(
image::ImageReader::new(io::Cursor::new(bytes))
.with_guessed_format()?
.decode()?
.to_rgba8(),
)
})
.await??;

Ok(Rgba {
width: image.width(),
height: image.height(),
pixels: Bytes::from(image.into_raw()),
})
}
}

#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize,
)]
pub struct Id(u32);

#[derive(Clone)]
pub struct Rgba {
pub width: u32,
pub height: u32,
pub pixels: Bytes,
}

impl fmt::Debug for Rgba {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rgba")
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}

#[derive(Debug, Clone, Copy)]
pub enum Size {
Original,
Thumbnail,
}

#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum Error {
RequestFailed(Arc<reqwest::Error>),
IOFailed(Arc<io::Error>),
JoinFailed(Arc<task::JoinError>),
ImageDecodingFailed(Arc<image::ImageError>),
}

impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::RequestFailed(Arc::new(error))
}
}

impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::IOFailed(Arc::new(error))
}
}

impl From<task::JoinError> for Error {
fn from(error: task::JoinError) -> Self {
Self::JoinFailed(Arc::new(error))
}
}

impl From<image::ImageError> for Error {
fn from(error: image::ImageError) -> Self {
Self::ImageDecodingFailed(Arc::new(error))
}
}
Loading
Loading