mirror of
https://gitlab.com/asus-linux/asusctl.git
synced 2026-02-06 00:15:04 +01:00
Basic fade in/out of gifs
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
#[cfg(feature = "dbus")]
|
||||
use zvariant_derive::Type;
|
||||
|
||||
use crate::{error::AnimeError, AnimTime, AnimeGif};
|
||||
|
||||
/// The first 7 bytes of a USB packet are accounted for by `USB_PREFIX1` and `USB_PREFIX2`
|
||||
const BLOCK_START: usize = 7;
|
||||
/// *Not* inclusive, the byte before this is the final for each "pane"
|
||||
@@ -79,3 +90,93 @@ impl From<AnimeDataBuffer> for AnimePacketType {
|
||||
buffers
|
||||
}
|
||||
}
|
||||
|
||||
/// This runs the animations as a blocking loop by using the `callback` to write data
|
||||
pub fn run_animation(
|
||||
frames: &AnimeGif,
|
||||
do_early_return: Arc<AtomicBool>,
|
||||
callback: &dyn Fn(AnimeDataBuffer),
|
||||
) -> Result<(), AnimeError> {
|
||||
let mut count = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
let mut timed = false;
|
||||
let mut run_time = frames.total_frame_time();
|
||||
println!("Real gif run length = {:?}", run_time);
|
||||
if let AnimTime::Fade(time) = frames.duration() {
|
||||
if let Some(middle) = time.show_for() {
|
||||
run_time = middle + time.total_fade_time();
|
||||
}
|
||||
timed = true;
|
||||
} else if let AnimTime::Time(time) = frames.duration() {
|
||||
run_time = time;
|
||||
timed = true;
|
||||
}
|
||||
|
||||
// After setting up all the data
|
||||
let mut fade_in = Duration::from_millis(0);
|
||||
let mut fade_out = Duration::from_millis(0);
|
||||
let mut fade_in_step = 0.0;
|
||||
let mut fade_in_accum = 0.0;
|
||||
let mut fade_out_step = 0.0;
|
||||
let mut fade_out_accum;
|
||||
if let AnimTime::Fade(time) = frames.duration() {
|
||||
fade_in = time.fade_in();
|
||||
fade_out = time.fade_out();
|
||||
fade_in_step = 1.0 / fade_in.as_secs_f32();
|
||||
fade_out_step = 1.0 / fade_out.as_secs_f32();
|
||||
|
||||
if time.total_fade_time() > run_time {
|
||||
println!("Total fade in/out time larger than gif run time. Setting fades to half");
|
||||
fade_in = run_time / 2;
|
||||
fade_in_step = 1.0 / (run_time / 2).as_secs_f32();
|
||||
|
||||
fade_out = run_time / 2;
|
||||
fade_out_step = 1.0 / (run_time / 2).as_secs_f32();
|
||||
}
|
||||
}
|
||||
|
||||
'animation: loop {
|
||||
for frame in frames.frames() {
|
||||
let frame_start = Instant::now();
|
||||
if do_early_return.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut output = frame.frame().clone();
|
||||
|
||||
if let AnimTime::Fade(_) = frames.duration() {
|
||||
if frame_start <= start + fade_in {
|
||||
for pixel in output.get_mut() {
|
||||
*pixel = (*pixel as f32 * fade_in_accum) as u8;
|
||||
}
|
||||
fade_in_accum = fade_in_step * (frame_start - start).as_secs_f32();
|
||||
} else if frame_start > (start + run_time) - fade_out {
|
||||
if run_time > (frame_start - start) {
|
||||
fade_out_accum =
|
||||
fade_out_step * (run_time - (frame_start - start)).as_secs_f32();
|
||||
} else {
|
||||
fade_out_accum = 0.0;
|
||||
}
|
||||
for pixel in output.get_mut() {
|
||||
*pixel = (*pixel as f32 * fade_out_accum) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(output);
|
||||
|
||||
if timed && Instant::now().duration_since(start) > run_time {
|
||||
break 'animation;
|
||||
}
|
||||
|
||||
sleep(frame.delay());
|
||||
}
|
||||
if let AnimTime::Count(times) = frames.duration() {
|
||||
count += 1;
|
||||
if count >= times {
|
||||
break 'animation;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -31,10 +31,12 @@ impl AnimeFrame {
|
||||
pub enum AnimTime {
|
||||
/// Time in milliseconds for animation to run
|
||||
Time(Duration),
|
||||
/// How many full animation loops to run
|
||||
Cycles(u32),
|
||||
/// How many full animation loops to run or how many seconds if image is static
|
||||
Count(u32),
|
||||
/// Run for infinite time
|
||||
Infinite,
|
||||
/// Fade in, play for, fade out
|
||||
Fade(Fade),
|
||||
}
|
||||
|
||||
impl Default for AnimTime {
|
||||
@@ -44,6 +46,40 @@ impl Default for AnimTime {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fancy brightness control: fade in/out, show at brightness for n time
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize)]
|
||||
pub struct Fade {
|
||||
fade_in: Duration,
|
||||
show_for: Option<Duration>,
|
||||
fade_out: Duration,
|
||||
}
|
||||
|
||||
impl Fade {
|
||||
pub fn new(fade_in: Duration, show_for: Option<Duration>, fade_out: Duration) -> Self {
|
||||
Self {
|
||||
fade_in,
|
||||
show_for,
|
||||
fade_out,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fade_in(&self) -> Duration {
|
||||
self.fade_in
|
||||
}
|
||||
|
||||
pub fn show_for(&self) -> Option<Duration> {
|
||||
self.show_for
|
||||
}
|
||||
|
||||
pub fn fade_out(&self) -> Duration {
|
||||
self.fade_out
|
||||
}
|
||||
|
||||
pub fn total_fade_time(&self) -> Duration {
|
||||
self.fade_in + self.fade_out
|
||||
}
|
||||
}
|
||||
|
||||
/// A gif animation. This is a collection of frames from the gif, and a duration
|
||||
/// that the animation should be shown for.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -67,6 +103,7 @@ impl AnimeGif {
|
||||
let mut decoder = decoder.read_info(file)?;
|
||||
|
||||
let mut frames = Vec::with_capacity(decoder.buffer_size());
|
||||
|
||||
while let Some(frame) = decoder.read_next_frame()? {
|
||||
let wait = frame.delay * 10;
|
||||
if matches!(frame.dispose, gif::DisposalMethod::Background) {
|
||||
@@ -162,6 +199,39 @@ impl AnimeGif {
|
||||
Ok(Self(frames, duration))
|
||||
}
|
||||
|
||||
/// Make a static gif out of a greyscale png. If no duration is specified then the default
|
||||
/// will be 1 second long. If `AnimTime::Cycles` is specified for `duration` then this can
|
||||
/// be considered how many seconds the image will show for.
|
||||
#[inline]
|
||||
pub fn create_png_static(
|
||||
file_name: &Path,
|
||||
scale: f32,
|
||||
angle: f32,
|
||||
translation: Vec2,
|
||||
duration: AnimTime,
|
||||
brightness: f32,
|
||||
) -> Result<Self, AnimeError> {
|
||||
let image = AnimeImage::from_png(file_name, scale, angle, translation, brightness)?;
|
||||
|
||||
let mut total = Duration::from_millis(1000);
|
||||
if let AnimTime::Fade(fade) = duration {
|
||||
total = fade.total_fade_time();
|
||||
if let Some(middle) = fade.show_for {
|
||||
total += middle;
|
||||
}
|
||||
}
|
||||
// Make frame delay 30ms, and find frame count
|
||||
let frame_count = total.as_millis() / 30;
|
||||
|
||||
let single = AnimeFrame {
|
||||
data: <AnimeDataBuffer>::from(&image),
|
||||
delay: Duration::from_millis(30),
|
||||
};
|
||||
let frames = vec![single; frame_count as usize];
|
||||
|
||||
Ok(Self(frames, duration))
|
||||
}
|
||||
|
||||
/// Get a slice of the frames this gif has
|
||||
#[inline]
|
||||
pub fn frames(&self) -> &[AnimeFrame] {
|
||||
@@ -173,4 +243,16 @@ impl AnimeGif {
|
||||
pub fn duration(&self) -> AnimTime {
|
||||
self.1
|
||||
}
|
||||
|
||||
/// Get the frame count
|
||||
pub fn frame_count(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Get total gif time for one run
|
||||
pub fn total_frame_time(&self) -> Duration {
|
||||
let mut time = 0;
|
||||
self.0.iter().for_each(|f| time += f.delay.as_millis());
|
||||
Duration::from_millis(time as u64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use glam::Vec2;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@@ -11,14 +8,14 @@ use crate::{error::AnimeError, AnimTime, AnimeDataBuffer, AnimeGif, AnimeImage};
|
||||
/// All the possible AniMe actions that can be used. This enum is intended to be
|
||||
/// a helper for loading up `ActionData`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub enum AnimeAction {
|
||||
pub enum ActionLoader {
|
||||
/// Full gif sequence. Immutable.
|
||||
AsusAnimation {
|
||||
file: PathBuf,
|
||||
time: AnimTime,
|
||||
brightness: f32,
|
||||
},
|
||||
/// Basic image, can have properties changed
|
||||
/// Animated gif. If the file is a png a static gif is created using the `time` properties
|
||||
ImageAnimation {
|
||||
file: PathBuf,
|
||||
scale: f32,
|
||||
@@ -32,6 +29,7 @@ pub enum AnimeAction {
|
||||
scale: f32,
|
||||
angle: f32,
|
||||
translation: Vec2,
|
||||
time: Option<AnimTime>,
|
||||
brightness: f32,
|
||||
},
|
||||
/// A pause to be used between sequences
|
||||
@@ -59,9 +57,9 @@ pub enum ActionData {
|
||||
}
|
||||
|
||||
impl ActionData {
|
||||
pub fn from_anime_action(action: &AnimeAction) -> Result<ActionData, AnimeError> {
|
||||
pub fn from_anime_action(action: &ActionLoader) -> Result<ActionData, AnimeError> {
|
||||
let a = match action {
|
||||
AnimeAction::AsusAnimation {
|
||||
ActionLoader::AsusAnimation {
|
||||
file,
|
||||
time: duration,
|
||||
brightness,
|
||||
@@ -70,33 +68,59 @@ impl ActionData {
|
||||
*duration,
|
||||
*brightness,
|
||||
)?),
|
||||
AnimeAction::ImageAnimation {
|
||||
ActionLoader::ImageAnimation {
|
||||
file,
|
||||
scale,
|
||||
angle,
|
||||
translation,
|
||||
time: duration,
|
||||
brightness,
|
||||
} => ActionData::Animation(AnimeGif::create_png_gif(
|
||||
&file,
|
||||
*scale,
|
||||
*angle,
|
||||
*translation,
|
||||
*duration,
|
||||
*brightness,
|
||||
)?),
|
||||
AnimeAction::Image {
|
||||
} => {
|
||||
if let Some(ext) = file.extension() {
|
||||
if ext.to_string_lossy().to_lowercase() == "png" {
|
||||
return Ok(ActionData::Animation(AnimeGif::create_png_static(
|
||||
&file,
|
||||
*scale,
|
||||
*angle,
|
||||
*translation,
|
||||
*duration,
|
||||
*brightness,
|
||||
)?));
|
||||
}
|
||||
}
|
||||
ActionData::Animation(AnimeGif::create_png_gif(
|
||||
&file,
|
||||
*scale,
|
||||
*angle,
|
||||
*translation,
|
||||
*duration,
|
||||
*brightness,
|
||||
)?)
|
||||
}
|
||||
ActionLoader::Image {
|
||||
file,
|
||||
scale,
|
||||
angle,
|
||||
translation,
|
||||
brightness,
|
||||
time,
|
||||
} => {
|
||||
if let Some(time) = time {
|
||||
return Ok(ActionData::Animation(AnimeGif::create_png_static(
|
||||
&file,
|
||||
*scale,
|
||||
*angle,
|
||||
*translation,
|
||||
*time,
|
||||
*brightness,
|
||||
)?));
|
||||
}
|
||||
// If no time then create a plain static image
|
||||
let image = AnimeImage::from_png(&file, *scale, *angle, *translation, *brightness)?;
|
||||
let data = <AnimeDataBuffer>::from(&image);
|
||||
ActionData::Image(Box::new(data))
|
||||
}
|
||||
AnimeAction::Pause(duration) => ActionData::Pause(*duration),
|
||||
ActionLoader::Pause(duration) => ActionData::Pause(*duration),
|
||||
};
|
||||
Ok(a)
|
||||
}
|
||||
@@ -115,38 +139,8 @@ impl Sequences {
|
||||
/// Use a base `AnimeAction` to generate the precomputed data and insert in to
|
||||
/// the run buffer
|
||||
#[inline]
|
||||
pub fn insert(&mut self, index: usize, action: &AnimeAction) -> Result<(), AnimeError> {
|
||||
match action {
|
||||
AnimeAction::AsusAnimation {
|
||||
file,
|
||||
time: duration,
|
||||
brightness,
|
||||
} => self.insert_asus_gif(index, &file, *duration, *brightness)?,
|
||||
AnimeAction::ImageAnimation {
|
||||
file,
|
||||
scale,
|
||||
angle,
|
||||
translation,
|
||||
time: duration,
|
||||
brightness,
|
||||
} => self.insert_image_gif(
|
||||
index,
|
||||
&file,
|
||||
*scale,
|
||||
*angle,
|
||||
*translation,
|
||||
*duration,
|
||||
*brightness,
|
||||
)?,
|
||||
AnimeAction::Image {
|
||||
file,
|
||||
scale,
|
||||
angle,
|
||||
translation,
|
||||
brightness,
|
||||
} => self.insert_png(index, &file, *scale, *angle, *translation, *brightness)?,
|
||||
AnimeAction::Pause(duration) => self.insert_pause(index, *duration),
|
||||
};
|
||||
pub fn insert(&mut self, index: usize, action: &ActionLoader) -> Result<(), AnimeError> {
|
||||
self.0.insert(index, ActionData::from_anime_action(action)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -161,66 +155,6 @@ impl Sequences {
|
||||
None
|
||||
}
|
||||
|
||||
fn insert_asus_gif(
|
||||
&mut self,
|
||||
mut index: usize,
|
||||
file: &Path,
|
||||
duration: AnimTime,
|
||||
brightness: f32,
|
||||
) -> Result<(), AnimeError> {
|
||||
if index > self.0.len() {
|
||||
index = self.0.len() - 1;
|
||||
}
|
||||
let frames = AnimeGif::create_diagonal_gif(file, duration, brightness)?;
|
||||
self.0.insert(index, ActionData::Animation(frames));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_png(
|
||||
&mut self,
|
||||
mut index: usize,
|
||||
file: &Path,
|
||||
scale: f32,
|
||||
angle: f32,
|
||||
translation: Vec2,
|
||||
brightness: f32,
|
||||
) -> Result<(), AnimeError> {
|
||||
if index > self.0.len() {
|
||||
index = self.0.len() - 1;
|
||||
}
|
||||
let image = AnimeImage::from_png(file, scale, angle, translation, brightness)?;
|
||||
let data = <AnimeDataBuffer>::from(&image);
|
||||
self.0.insert(index, ActionData::Image(Box::new(data)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert_image_gif(
|
||||
&mut self,
|
||||
mut index: usize,
|
||||
file: &Path,
|
||||
scale: f32,
|
||||
angle: f32,
|
||||
translation: Vec2,
|
||||
duration: AnimTime,
|
||||
brightness: f32,
|
||||
) -> Result<(), AnimeError> {
|
||||
if index > self.0.len() {
|
||||
index = self.0.len() - 1;
|
||||
}
|
||||
let frames =
|
||||
AnimeGif::create_png_gif(file, scale, angle, translation, duration, brightness)?;
|
||||
self.0.insert(index, ActionData::Animation(frames));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_pause(&mut self, mut index: usize, duration: Duration) {
|
||||
if index > self.0.len() {
|
||||
index = self.0.len() - 1;
|
||||
}
|
||||
self.0.insert(index, ActionData::Pause(duration));
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> ActionIterator {
|
||||
ActionIterator {
|
||||
actions: &self,
|
||||
|
||||
Reference in New Issue
Block a user