Small library for generating claude-code like unicde block mascots, and providing animations when they do stuff
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: chain multiple saved animations with comma-separated names

Use clood buddy blink,wave,shrug to play animations in sequence.
Each animation plays for one full ping-pong cycle (LCM of all its
parameter periods) before advancing to the next, then loops.

goose.art e936dd86 b4c98f6a

verified
+77 -19
+41 -2
src/animation.rs
··· 294 294 295 295 // ── Animation loop ────────────────────────────────────────────────────────── 296 296 297 - /// Run an infinite animation loop, rendering at the given FPS. 298 - pub fn run_loop(base: &Clood, animations: &[Animation], fps: u32) -> ! { 297 + /// Compute the cycle length for a set of animations (LCM of all ping-pong periods). 298 + fn cycle_length(animations: &[Animation]) -> usize { 299 + let mut len = 1usize; 300 + for anim in animations { 301 + let range = anim.min.abs_diff(anim.max) as usize; 302 + if range > 0 { 303 + let period = range * 2; 304 + len = lcm(len, period); 305 + } 306 + } 307 + len 308 + } 309 + 310 + fn gcd(mut a: usize, mut b: usize) -> usize { 311 + while b != 0 { 312 + let t = b; 313 + b = a % b; 314 + a = t; 315 + } 316 + a 317 + } 318 + 319 + fn lcm(a: usize, b: usize) -> usize { 320 + if a == 0 || b == 0 { 1 } else { a / gcd(a, b) * b } 321 + } 322 + 323 + /// Run an infinite animation loop, cycling through a sequence of animation sets. 324 + /// Each set plays for one full ping-pong cycle before advancing to the next. 325 + pub fn run_loop(base: &Clood, sequence: &[Vec<Animation>], fps: u32) -> ! { 299 326 let frame_delay = Duration::from_millis(1000 / fps.max(1) as u64); 300 327 let mut tick: usize = 0; 301 328 let mut prev_height: usize = 0; 329 + let mut seq_index: usize = 0; 330 + 331 + // Precompute cycle lengths for each animation set. 332 + let cycle_lengths: Vec<usize> = sequence.iter().map(|s| cycle_length(s)).collect(); 302 333 303 334 hide_cursor(); 304 335 ··· 309 340 .expect("Failed to set Ctrl-C handler"); 310 341 311 342 loop { 343 + let animations = &sequence[seq_index]; 344 + 312 345 let mut state = FrameState::from_clood(base); 313 346 for anim in animations { 314 347 state.apply(anim.param, anim.value_at(tick)); ··· 322 355 323 356 prev_height = height; 324 357 tick = tick.wrapping_add(1); 358 + 359 + // Advance to next animation set after one full cycle. 360 + if sequence.len() > 1 && tick >= cycle_lengths[seq_index] { 361 + seq_index = (seq_index + 1) % sequence.len(); 362 + tick = 0; 363 + } 325 364 326 365 thread::sleep(frame_delay); 327 366 }
+36 -17
src/main.rs
··· 232 232 build_clood_from_args(&args, &mut rng) 233 233 }; 234 234 235 - // Resolve animations: from CLI --anim flags, a saved animation name, or both. 236 - let mut animations = parse_cli_animations(&args); 235 + // Resolve animations: from CLI --anim flags, saved animation name(s), or both. 236 + // The animation name can be comma-separated to chain multiple: "blink,wave,shrug" 237 + let cli_animations = parse_cli_animations(&args); 238 + 239 + let mut animation_sequence: Vec<Vec<Animation>> = Vec::new(); 237 240 238 - if let Some(ref anim_name) = args.anim_name { 239 - match save_file.animations.get(anim_name) { 240 - Some(saved_anims) => animations.extend(saved_anims.iter().cloned()), 241 - None => { 242 - eprintln!("Unknown animation '{}'. Saved animations:", anim_name); 243 - if save_file.animations.is_empty() { 244 - eprintln!(" (none — use --saveanim <name> to create one)"); 245 - } else { 246 - for key in save_file.animations.keys() { 247 - eprintln!(" {}", key); 241 + if let Some(ref anim_names) = args.anim_name { 242 + for anim_name in anim_names.split(',') { 243 + let anim_name = anim_name.trim(); 244 + if anim_name.is_empty() { 245 + continue; 246 + } 247 + match save_file.animations.get(anim_name) { 248 + Some(saved_anims) => { 249 + // Merge CLI anims with this saved set 250 + let mut combined = cli_animations.clone(); 251 + combined.extend(saved_anims.iter().cloned()); 252 + animation_sequence.push(combined); 253 + } 254 + None => { 255 + eprintln!("Unknown animation '{}'. Saved animations:", anim_name); 256 + if save_file.animations.is_empty() { 257 + eprintln!(" (none — use --saveanim <name> to create one)"); 258 + } else { 259 + for key in save_file.animations.keys() { 260 + eprintln!(" {}", key); 261 + } 248 262 } 263 + std::process::exit(1); 249 264 } 250 - std::process::exit(1); 251 265 } 252 266 } 267 + } else if !cli_animations.is_empty() { 268 + animation_sequence.push(cli_animations.clone()); 253 269 } 270 + 271 + // Flatten for saving (save only CLI-specified anims, not loaded ones). 272 + let animations_flat = cli_animations; 254 273 255 274 // Save character if requested. 256 275 if let Some(ref save_name) = args.save { ··· 263 282 264 283 // Save animation if requested. 265 284 if let Some(ref save_name) = args.saveanim { 266 - if animations.is_empty() { 285 + if animations_flat.is_empty() { 267 286 eprintln!("No animations to save. Use --anim to define some first."); 268 287 std::process::exit(1); 269 288 } 270 289 save_file 271 290 .animations 272 - .insert(save_name.clone(), animations.clone()); 291 + .insert(save_name.clone(), animations_flat); 273 292 eprintln!("Saved animation '{}'", save_name); 274 293 write_save_file(&save_file); 275 294 } 276 295 277 296 // Render or animate. 278 - if animations.is_empty() { 297 + if animation_sequence.is_empty() { 279 298 print!("{}", clood::render(&base)); 280 299 } else { 281 - animation::run_loop(&base, &animations, args.fps); 300 + animation::run_loop(&base, &animation_sequence, args.fps); 282 301 } 283 302 } 284 303