This is a Rust port of the Heck Base Providers, Tracks and Point definition functionality without depending on any game runtime.
This README gives short explanations and tiny examples to get started with the main areas of the crate.
Base providers are the runtime sources of values that point definitions and modifiers can read.
Use BaseProviderContext to store and query base values (score, time, colors, transforms, ...).
Minimal example — set/read a base value:
use tracks_rs::base_provider_context::BaseProviderContext;
use tracks_rs::base_value::BaseValue;
fn main() {
let mut ctx = BaseProviderContext::new();
// set a float base value (e.g. song time)
ctx.set_values("baseSongTime", BaseValue::from(12.5f32));
// read it back
let val = ctx.get_values("baseSongTime");
println!("song time = {:?}", val.as_float());
}You can also obtain cached ValueProviders from the context using get_value_provider (useful when parsing provider expressions like baseHeadPosition.x or smoothed variants baseSongTime.s0_5).
Providers are the runtime building blocks that supply numeric/vector/quaternion data to point definitions and modifiers.
Static— a fixed literal value from JSON (e.g.[1.0, 2.0, 3.0]).BaseProvider— references toBaseProviderContextvalues (strings starting withbase, e.g."baseSongTime"or"baseHeadPosition.x").PartialProvider— swizzled views into vector/quaternion providers (e.g..x,.xy).SmoothProviders/SmoothRotationProviders— time-smoothing wrappers created from specs likes1ors0_5.
The crate exposes a helper to convert a JSON slice into a Vec<ValueProvider> when the json feature is enabled:
use tracks_rs::base_provider_context::BaseProviderContext;
use tracks_rs::providers::deserialize_values;
use serde_json::json;
fn main() {
let mut ctx = BaseProviderContext::new();
// Mixed static numbers and base provider references. Strings that start with
// "base" are turned into BaseProvider entries; numeric sequences become Static entries.
let raw = json!([0.1, "baseSongTime", 1.5, "baseHeadPosition.x"]);
// convert to Vec<&Value> as expected by `deserialize_values`
let arr: Vec<&serde_json::Value> = raw.as_array().unwrap().iter().collect();
let providers = deserialize_values(&arr, &mut ctx);
// `providers` now contains a sequence of ValueProvider variants
println!("parsed providers: {:?}", providers);
}Use BaseProviderContext::get_value_provider if you need to parse a single provider expression and cache it for repeated sampling (e.g. baseSongTime.s0_5, baseHeadPosition.xy).
Tracks are containers of named properties and path animations. TracksHolder manages multiple Track instances and provides stable keys.
Minimal example — create and register a track:
use tracks_rs::animation::tracks_holder::TracksHolder;
use tracks_rs::animation::track::Track;
fn main() {
let mut holder = TracksHolder::new();
let mut track = Track::default();
track.name = "my_track".to_string();
let key = holder.add_track(track);
let stored = holder.get_track(key).unwrap();
assert_eq!(stored.name, "my_track");
}Properties on Track are strongly typed (e.g. position is a Vec3 property). Use the provided ValueProperty and PathProperty API to set and query values when driving animations.
Point definitions describe how values change over time. The crate provides several implementations (float, vec3, vec4, quaternion) via the PointDefinitionLike trait.
If you enable the json feature you can parse Heck-compatible point JSON into point definitions with the provided helpers.
Minimal example — parse a simple float definition (requires features = ["json"]):
use tracks_rs::base_provider_context::BaseProviderContext;
use tracks_rs::point_definition::FloatPointDefinition;
use serde_json::json;
fn main() {
let mut ctx = BaseProviderContext::new();
// A two-point definition: value 0 at time 0, value 1 at time 1
let def_json = json!([[0.0, 0.0], [1.0, 1.0]]);
let def = FloatPointDefinition::parse(def_json, &mut ctx);
let (value, finished) = def.interpolate(0.5, &ctx);
println!("interpolated float = {:?}, finished={} ", value, finished);
}The parser recognizes three logical groups inside each point entry:
- Values: numeric literals and
base*strings (these becomeValueProviders). - Modifiers: nested arrays describing modifier composition (the parser calls
deserialize_modifierrecursively). - Flags: plain strings (not starting with
base) — used for easing names, smoothing hints likesplineCatmullRom, and other markers.
Example: a two-point float definition where the second point reads from a base provider and uses easing:
use tracks_rs::base_provider_context::BaseProviderContext;
use tracks_rs::point_definition::FloatPointDefinition;
use serde_json::json;
fn main() {
let mut ctx = BaseProviderContext::new();
// Point 0: static value 0 at time 0
// Point 1: value sampled from baseSongTime at time 1 with easing flag
let complex = json!([
[0.0, 0.0],
["baseSongTime", 1.0, "easeInOutQuad"]
]);
let def = FloatPointDefinition::parse(complex, &mut ctx);
let (v_mid, finished) = def.interpolate(0.5, &ctx);
println!("value at t=0.5 = {:?}, finished = {}", v_mid, finished);
}Notes:
- Use
"base..."strings to reference context-provided values. The parser converts those intoBaseProviderentries so modifiers and interpolation can sample live base data. - Provider smoothing (e.g.
s0_5) and swizzles (e.g..x,.xy) are handled byBaseProviderContext::get_value_providerwhen the provider string contains dots or smoothing prefixes. - Modifier arrays (nested JSON arrays inside a point) are parsed recursively and turned into modifier objects via
PointDefinitionLike::deserialize_modifierandcreate_modifierimplementations. Seesrc/modifiers/andsrc/point_definition/for the concrete formats supported.
CoroutineManager orchestrates time-based events: it schedules and polls coroutines that animate Track properties over song time.
Typical host usage:
- Create a
CoroutineManagerandTracksHolder. - When an event occurs, build an
EventDataand callstart_event_coroutine(the manager converts beatmap duration -> song-time seconds usingbpm). - Each frame call
poll_events(song_time, &ctx, &mut holder)to advance active coroutines.
Minimal example — queue an animate-track event and poll until completion (requires the json feature for JSON parsing helpers):
use glam::Vec3;
use serde_json::json;
use tracks_rs::animation::coroutine_manager::CoroutineManager;
use tracks_rs::animation::events::{EventData, EventType};
use tracks_rs::animation::tracks_holder::TracksHolder;
use tracks_rs::animation::track::{ValuePropertyHandle, PathPropertyHandle, PropertyNames};
use tracks_rs::base_provider_context::BaseProviderContext;
use tracks_rs::easings::functions::Functions;
use tracks_rs::point_definition::vector3_point_definition::Vector3PointDefinition;
fn main() {
let mut ctx = BaseProviderContext::new();
let mut holder = TracksHolder::new();
let mut manager = CoroutineManager::default();
// create and register a track
let mut track = tracks_rs::animation::track::Track::default();
track.name = "queued_track".to_string();
let key = holder.add_track(track);
// build a simple two-point Vec3 definition: [x, y, z, time]
let def_json = json!([[0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]]);
let vec3_def = Vector3PointDefinition::parse(def_json, &mut ctx);
let base_def = tracks_rs::point_definition::BasePointDefinition::from(vec3_def);
let event = EventData {
raw_duration: 1.0, // beats
easing: Functions::EaseLinear,
repeat: 0,
start_song_time: 0.0,
property: EventType::AnimateTrack(ValuePropertyHandle::new("position")),
track_key: key,
point_data: Some(base_def),
};
// queue it and run a simple poll loop
let bpm = 120.0f32;
let mut song_time = 0.0f32;
manager.start_event_coroutine(bpm, song_time, &ctx, &mut holder, event);
// advance time in a simple loop (host would use frame delta)
for _ in 0..60 {
song_time += 1.0 / 60.0;
manager.poll_events(song_time, &ctx, &mut holder);
}
// --- Read ValueProperty (final stored value) ---
let stored = holder.get_track(key).expect("track present");
// Use `PropertyNames` for canonical properties
let prop = &stored.properties.position;
if let Some(value) = prop.get_value() {
println!("position property value = {:?}", value);
} else {
println!("position property has no value");
}
// --- Read PathProperty (interpolated path data) ---
let path_prop = &stored.path_properties.position;
if let Some(v) = path_prop.interpolate(0.5, &ctx) {
println!("interpolated path value = {:?}", v);
} else {
println!("no path data available");
}
}Quick poll-only example (when coroutines are started elsewhere):
use tracks_rs::animation::coroutine_manager::CoroutineManager;
use tracks_rs::base_provider_context::BaseProviderContext;
use tracks_rs::animation::tracks_holder::TracksHolder;
fn main() {
let ctx = BaseProviderContext::new();
let mut holder = TracksHolder::new();
let mut manager = CoroutineManager::default();
// Each frame, advance song time and poll events
let song_time = 0.0f32;
manager.poll_events(song_time, &ctx, &mut holder);
}See src/animation/coroutine_manager.rs for the implementation and unit tests.