From 9e1014bbd9d1c49e6f3e26bc7c3e508d3693a233 Mon Sep 17 00:00:00 2001 From: ashley Date: Sat, 21 Oct 2023 20:40:50 -0400 Subject: [PATCH] Easing function tool --- Cargo.toml | 4 ++- src/reskit/cli/evaluator.rs | 21 ++++++++++++- src/reskit/cli/settings.rs | 22 +++++++++++++ src/reskit/easing.rs | 63 +++++++++++++++++++++++++++++++++++++ src/reskit/mod.rs | 1 + 5 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/reskit/easing.rs diff --git a/Cargo.toml b/Cargo.toml index b3fdb38..f3d8933 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,6 @@ linked-hash-map = "0.5.6" convert_case = "0.6.0" roxmltree = "0.18.0" regex = "1.9.5" -euclid = "0.22.9" \ No newline at end of file +euclid = "0.22.9" +flo_curves = "0.7.2" +fixed = "1.24.0" \ No newline at end of file diff --git a/src/reskit/cli/evaluator.rs b/src/reskit/cli/evaluator.rs index 4d598f3..5b6f9c9 100644 --- a/src/reskit/cli/evaluator.rs +++ b/src/reskit/cli/evaluator.rs @@ -1,6 +1,6 @@ use std::{error::Error, fs::File, io::Write, path::Path, collections::HashMap}; use clap::Parser; -use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::print_good, level::{converter::get_tiled_tilemap, system::{get_tiles, get_code, get_tilemap, get_collision_map, get_objs, get_sprites}}}; +use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::print_good, level::{converter::get_tiled_tilemap, system::{get_tiles, get_code, get_tilemap, get_collision_map, get_objs, get_sprites}}, easing::get_cubic_bezier}; use super::settings::{Args, Tools, TileOutputFormat, TileOrder}; pub fn run_command() -> Result<(), Box> { @@ -129,6 +129,25 @@ pub fn run_command() -> Result<(), Box> { code_asm.write_all( &get_code( &tiled_file, "testlevel", "levels/" )?.as_bytes() )?; print_good( "exported level.asm" ); } + Tools::Easing { output_directory, interval, point_1, point_2 } => { + let cp1: Vec<&str> = point_1.split( "," ).collect(); + if cp1.len() != 2 { + return Err( format!( "invalid format for point_1: {}", point_1 ) )?; + } + + let ( cp1_x, cp1_y ): ( f64, f64 ) = ( cp1[ 0 ].parse()?, cp1[ 1 ].parse()? ); + + let cp2: Vec<&str> = point_1.split( "," ).collect(); + if cp2.len() != 2 { + return Err( format!( "invalid format for point_2: {}", point_2 ) )?; + } + + let ( cp2_x, cp2_y ): ( f64, f64 ) = ( cp2[ 0 ].parse()?, cp2[ 1 ].parse()? ); + + let mut easing = File::create( format!( "{}curve.bin", output_directory ) )?; + easing.write_all( &get_cubic_bezier( ( 0.0, 0.0 ), ( cp1_x, cp1_y ), ( cp2_x, cp2_y ), ( 1.0, 1.0 ), interval )? )?; + print_good( "exported curve.bin" ); + } }; Ok( () ) diff --git a/src/reskit/cli/settings.rs b/src/reskit/cli/settings.rs index b7ad2f3..c11bfd5 100644 --- a/src/reskit/cli/settings.rs +++ b/src/reskit/cli/settings.rs @@ -124,5 +124,27 @@ pub enum Tools { /// Console system type #[arg(short, long, value_enum, default_value_t=SystemType::Md)] console: SystemType + }, + + #[command(name = "easing")] + #[command(about = "Generate an easing curve given a time interval and control points.")] + Easing { + + /// Output directory for artifacts + #[arg(short, long, default_value_t=String::from("./"))] + output_directory: String, + + /// Time span of the easing curve (in 1/60 s increments) + #[arg(short,long)] + interval: u16, + + /// First control point of the cubic bezier (format: ,) + #[arg(long)] + point_1: String, + + /// Second control point of the cubic bezier (format: ,) + #[arg(long)] + point_2: String + } } \ No newline at end of file diff --git a/src/reskit/easing.rs b/src/reskit/easing.rs new file mode 100644 index 0000000..b756085 --- /dev/null +++ b/src/reskit/easing.rs @@ -0,0 +1,63 @@ +use std::error::Error; +use fixed::FixedI32; +use fixed::types::extra::U16; +use flo_curves::*; +use flo_curves::{bezier, Coord2}; + +use super::utility::print_info; + +/* +stinkhead7ds easing curve description: + +e.g. going from 120 to 140 +easing function is cubic bezier going from 0.0 to 1.0 + +140-120=20 increments +0.0 is +0 and 1.0 is +20 +so e.g. if you read a 0.5 at the given increment it's 0.5 * 20 = +10 + +curves are exported from reskit sampled at 1/60 increments +reskit accepts arguments for control points and exports 16.16 fixed point binary files +so a 1 second animation is 60 increments, 2 seconds is 120, etc. + +each iteration be like: +1. take cubic bezier sample and post increment +2. the value at this iteration is (final - beginning) * sample + a. so example (140-120)*0.5 for a linear curve sampled right in the middle +3. round the value to the nearest whole integer +4. do not go back to 1 if we exhausted the curve + +need fixed point arithmetic multiplication +*/ + + +/** + * Output a binary for a cubic bezier defined by the given four points and the + * time interval defined as number of 1/60 jiffies. + */ +pub fn get_cubic_bezier( p0: (f64, f64), c0: (f64, f64), c1: (f64, f64), p1: (f64, f64), jiffies: u16 ) -> Result, Box> { + let mut result: Vec = Vec::new(); + + // 2 bytes: Number of frames this curve spans + result.extend( jiffies.to_be_bytes() ); + + let curve = bezier::Curve::from_points( + Coord2( p0.0, p0.1 ), + ( Coord2( c0.0, c0.1 ), Coord2( c1.0, c1.1 ) ), + Coord2( p1.0, p1.1 ) + ); + + let mut current_jiffy = 0.0; + // (jiffies) of 16.16 fixed point integers: Each point in the curve at the given frame + for i in 0..jiffies { + let point = curve.point_at_pos( current_jiffy ); + let x = point.1; + print_info( &format!( "at iteration {}, curve is {:?}, 16.16 format will be {}", i, point, x ) ); + current_jiffy += 1.0 / jiffies as f64; + + let as_fixed = FixedI32::::from_num( x ); + result.extend( as_fixed.to_be_bytes() ); + } + + Ok( result ) +} \ No newline at end of file diff --git a/src/reskit/mod.rs b/src/reskit/mod.rs index 196bf5f..60bdb3e 100644 --- a/src/reskit/mod.rs +++ b/src/reskit/mod.rs @@ -1,4 +1,5 @@ pub mod cli; +pub mod easing; pub mod level; pub mod soundtrack; pub mod tileset;