rust-animation 0.2.8

OpenGL based Animation Toolkit written in Rust
Documentation

rust-animation Latest Version

easing_functions demo

rust-animation is an OpenGL-based graphics library written in Rust for creating hardware-accelerated user interfaces. It is designed to implement simple, animated UIs for embedded devices, inspired by the GNOME Clutter project and Apple Core Animation.

Table of Contents

Features

  • 2D Transforms: Apply translate, scale, and rotate transformations to layers
  • Rich Animation System: Support for multiple easing functions (Linear, EaseIn, EaseOut, EaseInOut, and various polynomial variants)
  • Flex Layout: CSS Flexbox-like layout system using the Stretch library
  • Hardware Acceleration: OpenGL-based rendering for high performance
  • RALayer Hierarchy: Support for nested layers with parent-child relationships
  • Event Handling: Built-in event system for keyboard input and focus management
  • Image Support: Load and display images as textures
  • Text Rendering: Font rendering capabilities for displaying text

Note: rust-animation is in early development. Some features may be incomplete or have bugs. Please report any issues you encounter.

Prerequisites

Before using rust-animation, ensure you have the following installed:

Required

  • Rust (stable): Install from rust-lang.org
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    

Platform-Specific Requirements

Ubuntu/Debian

sudo apt-get update
sudo apt-get install build-essential cmake pkg-config

macOS

brew install cmake

Windows

  • Install CMake from the official website
  • Ensure you have the Visual Studio Build Tools or Visual Studio installed

Tested Platforms

  • Ubuntu 20.04 and later
  • Windows 10 and later
  • macOS (Intel and Apple Silicon)

Installation

Using as a Library

Add rust-animation to your Cargo.toml:

[dependencies]
rust-animation = "0.2.7"

Building from Source

Clone the repository and build:

git clone https://github.com/joone/rust-animation.git
cd rust-animation
cargo build --release

Quick Start

Here's a minimal example to get started:

use rust_animation::{layer::RALayer, animation::Animation, play::Play};
use rust_animation::layer::LayoutMode;
use keyframe::EasingFunction;

fn main() {
    // Initialize GLFW and create a window (see examples for full setup)
    
    // Create a Play (the main container)
    let mut play = Play::new(
        "My First Animation".to_string(),
        800,
        600,
        LayoutMode::UserDefine,
    );
    
    // Create a stage (the root layer)
    let mut stage = RALayer::new("stage".to_string(), 800, 600, None);
    stage.set_visible(true);
    
    // Create a layer (a visual element)
    let mut layer = RALayer::new("my_layer".to_string(), 100, 100, None);
    layer.x = 50;
    layer.y = 50;
    layer.set_color(1.0, 0.0, 0.0); // Red
    
    // Create and apply an animation
    let mut animation = Animation::new();
    animation.apply_translation_x(50, 400, 2.0, EasingFunction::EaseInOut);
    layer.set_animation(Some(animation));
    
    // Add layer to stage and stage to play
    stage.add_sub_actor(layer);
    play.add_stage(stage);
    
    // Render loop (see examples for full implementation)
    // play.render();
}

For complete working examples, see the Examples section below.

Examples

rust-animation includes several examples to demonstrate its capabilities. All examples can be run using cargo:

# General format
cargo run --example <example_name>

Easing Functions

Example file: easing_functions.rs

Demonstrates all available easing functions with visual animations.

Run:

cargo run --example easing_functions

What it does: Creates 17 animated layers, each using a different easing function, moving horizontally across the screen while rotating.

Key concepts demonstrated:

  • Multiple easing functions (Linear, EaseIn, EaseOut, EaseInOut, and polynomial variants)
  • Combining multiple animations (translation + rotation)
  • RALayer positioning and coloring

Code snippet:

  let mut play = Play::new(
    "Easing functions demo".to_string(),
    1920,
    1080,
    LayoutMode::UserDefine,
  );
  let mut stage = RALayer::new("stage".to_string(), 1920, 1080, None);
  stage.set_visible(true);

  let easing_functions = vec![
    EasingFunction::EaseIn,
    EasingFunction::EaseInCubic,
    EasingFunction::EaseInOut,
    EasingFunction::EaseInOutCubic,
    EasingFunction::EaseInOutQuad,
    EasingFunction::EaseInOutQuart,
    EasingFunction::EaseInOutQuint,
    EasingFunction::EaseInQuad,
    EasingFunction::EaseInQuart,
    EasingFunction::EaseInQuint,
    EasingFunction::EaseOut,
    EasingFunction::EaseOutCubic,
    EasingFunction::EaseOutQuad,
    EasingFunction::EaseOutQuart,
    EasingFunction::EaseOutQuint,
    EasingFunction::Linear,
    EasingFunction::Step,
  ];
  let mut y = 0;
  let time = 5.0;
  let width = 63;
  let height = width;
  for i in 0..17 {
    let layer_name = format!("layer_{}", i + 1);
    let mut layer = RALayer::new(actor_name.to_string(), width, height, None);
    layer.x = 0;
    layer.y = y;
    y += height as i32;
    layer.set_color(i as f32 / 18.0, i as f32 / 18.0, i as f32 / 18.0);

    let mut animation = Animation::new();
    animation.apply_translation_x(0, (1920 - width) as i32, time, easing_functions[i]);
    animation.apply_rotation(0, 360, time, EasingFunction::Linear);
    layer.set_animation(Some(animation));
    stage.add_sub_actor(layer);
  }
  play.add_stage(stage);

  while !window.should_close() {
    // events
    process_events(&mut window, &events);

    play.render();

    // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
    window.swap_buffers();
    glfw.poll_events();
  }

Flex UI

Example file: flex_ui.rs

flex_ui demo

Demonstrates CSS Flexbox-like layout capabilities using the Stretch library.

Run:

cargo run --example flex_ui

What it does: Creates a responsive layout with multiple containers, each demonstrating different flexbox alignment properties (FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, SpaceEvenly).

Key concepts demonstrated:

  • Flex layout system
  • Custom layout implementation using the Layout trait
  • Justify content and align items properties
  • Nested layers with flex positioning

Code snippet:

pub struct FlexLayout {
  name: String,
}

impl FlexLayout {
  pub fn new() -> Self {
    let mut flex_layout = FlexLayout {
      name: "flex_layout".to_string(),
    };

    println!("new FlexLayout {}", flex_layout.name);

    flex_layout
  }
}

impl Layout for FlexLayout {
  fn layout_sub_layers(
    &mut self,
    layer: &mut RALayer,
    parent_layer: Option<&RALayer>,
    stretch: &mut Option<Stretch>,
  ) {
    println!("run layout_sub_layer for FlexLayout {}", self.name);
    if let Some(stretch_obj) = stretch {
      if let Some(style_obj) = layer.style {
        layer.node = Some(stretch_obj.new_node(style_obj, vec![]).unwrap());
      } else {
        //println!("default style: {}: {},{}", self.name, self.width, self.height);
        layer.node = Some(
          stretch_obj
            .new_node(
              Style {
                size: Size {
                  width: Dimension::Points(layer.width as f32),
                  height: Dimension::Points(layer.height as f32),
                },
                margin: Rect {
                  start: Dimension::Points(2.0),
                  end: Dimension::Points(2.0),
                  top: Dimension::Points(2.0),
                  bottom: Dimension::Points(2.0),
                  ..Default::default()
                },
                ..Default::default()
              },
              vec![],
            )
            .unwrap(),
        );
      }

      println!("layer name {}", layer.name);

      if let Some(parent_layer) = parent_layer {
        if !parent_layer.node.is_none() && !layer.node.is_none() {
          match stretch_obj.add_child(parent_layer.node.unwrap(), layer.node.unwrap()) {
            Ok(()) => {
              println!(
                " stretch node  is added {} {}",
                parent_layer.name, layer.name
              )
            }
            Err(..) => {}
          }
        }
      }
    }

    //self.update_layout(layer, stretch);
  }

  fn update_layout(&mut self, layer: &mut RALayer, stretch: &mut Option<Stretch>) {
    if let Some(stretch_obj) = stretch {
      if !layer.node.is_none() {
        let layout = stretch_obj.layout(layer.node.unwrap()).unwrap();
        layer.x = layout.location.x as i32;
        layer.y = layout.location.y as i32;
        println!(
          "run update_layout for FlexLayout {} = {},{}",
          layer.name, layer.x, layer.y
        );
      }
    }
  }

  fn finalize(&mut self) {
    println!("finalize {}", self.name);
  }
}

fn main() {
  let mut glfw = glfw::init(glfw::FAIL_ON_ERRORS).unwrap();
  glfw.window_hint(glfw::WindowHint::ContextVersion(3, 3));
  glfw.window_hint(glfw::WindowHint::OpenGlProfile(
    glfw::OpenGlProfileHint::Core,
  ));
  #[cfg(target_os = "macos")]
  glfw.window_hint(glfw::WindowHint::OpenGlForwardCompat(true));

  let (mut window, events) = glfw
    .create_window(1920, 1080, "Flex UI demo", glfw::WindowMode::Windowed)
    .expect("Failed to create GLFW window.");

  window.set_key_polling(true);
  window.make_current();
  window.set_framebuffer_size_polling(true);

  gl::load_with(|symbol| window.get_proc_address(symbol) as *const _);

  let mut play = Play::new("Flex UI test".to_string(), 1920, 1080, LayoutMode::Flex);
  let mut stage = RALayer::new("stage".to_string(), 1920, 1080, None);
  stage.set_style(Style {
    size: Size {
      width: Dimension::Points(1920.0),
      height: Dimension::Points(1080.0),
    },
    justify_content: JustifyContent::Center,
    flex_direction: FlexDirection::Column,
    align_items: AlignItems::Center,
    margin: Rect {
      start: Dimension::Points(1.0),
      end: Dimension::Points(1.0),
      top: Dimension::Points(1.0),
      bottom: Dimension::Points(1.0),
      ..Default::default()
    },
    ..Default::default()
  });
  stage.set_visible(true);

  let justify_content = vec![
    JustifyContent::FlexStart,
    JustifyContent::FlexEnd,
    JustifyContent::Center,
    JustifyContent::SpaceBetween,
    JustifyContent::SpaceAround,
    JustifyContent::SpaceEvenly,
  ];
  let width = 1500;
  let height = 108;
  for i in 0..6 {
    let layer_name = format!("layer_{}", i + 1);
    let mut layer = RALayer::new(actor_name.to_string(), width, height, None);
    layer.set_color(i as f32 / 6.0, i as f32 / 6.0, i as f32 / 6.0);
    layer.set_style(Style {
      size: Size {
        width: Dimension::Points(width as f32),
        height: Dimension::Points(height as f32),
      },
      justify_content: justify_content[i],
      align_items: AlignItems::Center,
      margin: Rect {
        start: Dimension::Points(1.0),
        end: Dimension::Points(1.0),
        top: Dimension::Points(1.0),
        bottom: Dimension::Points(1.0),
        ..Default::default()
      },
      padding: Rect {
        start: Dimension::Points(2.0),
        end: Dimension::Points(2.0),
        ..Default::default()
      },
      ..Default::default()
    });
    for j in 0..10 {
      let mut sub_actor = RALayer::new(
        format!("actor_{}_{}", i + 1, j + 1).to_string(),
        100,
        100,
        None,
      );
      sub_actor.set_color(1.0, j as f32 / 10.0, j as f32 / 10.0);
      sub_actor.set_layout(Some(Box::new(FlexLayout::new())));
      layer.add_sub_actor(sub_actor);
    }
    layer.set_layout(Some(Box::new(FlexLayout::new())));
    stage.add_sub_actor(layer);
  }

  stage.set_layout(Some(Box::new(FlexLayout::new())));
  play.add_stage(stage);

  //play.set_stage_needs_layout(&"stage".to_string());

  while !window.should_close() {
    // events
    process_events(&mut window, &events);

    play.render();

    // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
    window.swap_buffers();
    glfw.poll_events();
  }
}

Basic Animation

Example file: ani.rs

Demonstrates basic animation features including transforms and image loading.

Run:

cargo run --example ani

What it does: Shows multiple animations running simultaneously - scaling, translating, and rotating layers, including image-based layers and colored shapes with nested sub-layers.

Key concepts demonstrated:

  • Multiple simultaneous animations (scale, translate, rotate)
  • Loading and animating images
  • Nested layer hierarchies
  • Different easing functions

Code snippet:

  let mut play = Play::new(
    "Animation test".to_string(),
    1920,
    1080,
    LayoutMode::UserDefine,
  );
  let mut stage = RALayer::new("stage".to_string(), 1920, 1080, None);
  stage.set_visible(true);

  let mut layer_1 = RALayer::new("layer_1".to_string(), 400, 225, None);
  layer_1.x = 100;
  layer_1.y = 100;
  layer_1.set_image("examples/splash.png".to_string());

  let mut animation_1 = Animation::new();

  // 1X -> 2X for 5 sec.
  let time = 5.0;
  animation_1.apply_scale(1.0, 2.0, time, EasingFunction::Linear);
  animation_1.apply_translation_x(100, 1000, time, EasingFunction::EaseInOut);
  animation_1.apply_translation_y(100, 300, time, EasingFunction::EaseInOut);
  animation_1.apply_rotation(0, 360, time, EasingFunction::EaseInOut);
  layer_1.set_animation(Some(animation_1));

  let mut actor_2 = Play::new_actor("actor_2".to_string(), 120, 120, None);
  actor_2.x = 100;
  actor_2.y = 100;
  actor_2.scale_x = 1.5;
  actor_2.scale_y = 1.5;
  actor_2.set_color(0.0, 0.0, 1.0);
  // 0 degree -> 360 degree for 5 sec

  let mut animation_2 = Animation::new();
  animation_2.apply_rotation(0, 360, 5.0, EasingFunction::EaseInOut);
  actor_2.set_animation(Some(animation_2));

  let mut layer_3 = Play::new_actor("layer_3".to_string(), 50, 50, None);
  layer_3.x = 10;
  layer_3.y = 10;
  layer_3.set_color(1.0, 0.0, 0.0);
  actor_2.add_sub_actor(layer_3);

  stage.add_sub_actor(layer_1);
  stage.add_sub_actor(actor_2);

  play.add_stage(stage);

  while !window.should_close() {
    process_events(&mut window, &events);
    play.render();
    window.swap_buffers();
    glfw.poll_events();
  }

Picture Viewer

Example file: picture_viewer.rs

Demonstrates event handling and custom user-defined layouts.

Run:

cargo run --example picture_viewer

What it does: Creates a thumbnail grid viewer with keyboard navigation and focus animations. Currently implements thumbnail view functionality.

Key concepts demonstrated:

  • Custom event handlers (EventHandler trait)
  • Keyboard input handling (arrow keys for navigation)
  • Focus management (key_focus_in/out events)
  • Custom layout implementation (Layout trait)
  • Grid-based positioning

Note: This example is a work in progress. Currently, only the thumbnail view is fully functional.

Code snippet:

pub struct RALayerEvent {
  name: String,
}

impl RALayerEvent {
  pub fn new() -> Self {
    RALayerEvent {
      name: "layer_event".to_string(),
    }
  }
}

impl EventHandler for RALayerEvent {
  fn key_focus_in(&mut self, layer: &mut RALayer) {
    let mut animation = Animation::new();
    animation.apply_scale(1.0, 1.1, 0.3, EasingFunction::EaseInOut);
    layer.set_animation(Some(animation));
  }

  fn key_focus_out(&mut self, layer: &mut RALayer) {
    layer.scale_x = 1.0;
    layer.scale_y = 1.0;
  }

  fn key_down(&mut self, key: rust_animation::layer::Key, layer: &mut RALayer) {
    if key == rust_animation::layer::Key::Right {
      // right cursor
      layer.select_next_sub_actor();
    } else if key == rust_animation::layer::Key::Left {
      // left cursor
      layer.select_prev_sub_actor();
    }
  }
}

pub struct ActorLayout {
  name: String,
  cur_x: i32,
}

impl ActorLayout {
  pub fn new() -> Self {
    ActorLayout {
      name: "actor_layout".to_string(),
      cur_x: 0,
    }
  }
}

impl Layout for ActorLayout {
  fn layout_sub_layers(
    &mut self,
    layer: &mut RALayer,
    parent_layer: Option<&RALayer>,
    stretch: &mut Option<Stretch>,
  ) {
    println!("layout_sub_layer {}", self.name);
    let mut index: i32 = 0;
    for sub_actor in layer.sub_actor_list.iter_mut() {
      self.cur_x += sub_actor.width as i32;
      sub_actor.x = index % 5 * IMAGE_WIDTH as i32;
      let col = index / 5;
      sub_actor.y = col * IMAGE_HEIGHT as i32;
      index += 1;
    }
  }

  fn update_layout(&mut self, layer: &mut RALayer, stretch: &mut Option<Stretch>) {
    println!("update_layout {}", self.name);
  }

  fn finalize(&mut self) {
    println!("finalize {}", self.name);
  }
}

API Overview

Core Concepts

Play: The main container and render manager

  • Manages the rendering loop
  • Holds one or more stages
  • Handles projection matrices and OpenGL setup

RALayer: Visual elements in the scene graph

  • Can have position (x, y, z), size (width, height)
  • Supports transforms: translate, scale, rotate
  • Can have colors or textures
  • Supports nested hierarchies (parent-child relationships)
  • Can have animations, event handlers, and custom layouts

Animation: Defines time-based property changes

  • Apply transformations over time with easing functions
  • Supports: translation (x, y), scaling, rotation
  • Multiple animations can run simultaneously on one layer

Easing Functions: Control animation timing curves

  • Linear, Step
  • EaseIn, EaseOut, EaseInOut (sine-based)
  • Quad, Cubic, Quart, Quint variants (polynomial-based)

Main APIs

// Create a Play (main container)
let play = Play::new(name, width, height, layout_mode);

// Create layers
let mut layer = RALayer::new(name, width, height, event_handler);
layer.x = x;
layer.y = y;
layer.set_color(r, g, b);
layer.set_image(path);

// Create animations
let mut animation = Animation::new();
animation.apply_translation_x(from, to, duration, easing);
animation.apply_translation_y(from, to, duration, easing);
animation.apply_scale(from, to, duration, easing);
animation.apply_rotation(from_deg, to_deg, duration, easing);
layer.set_animation(Some(animation));

// Build scene graph
parent_layer.add_sub_actor(child_actor);
stage.add_sub_actor(layer);
play.add_stage(stage);

// Render
play.render();

Contributing

Contributions are welcome! Here's how you can help:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development Guidelines

  • Run cargo fmt before committing to ensure consistent code style
  • Use the provided run-check-style.sh script for formatting
  • Write examples to demonstrate new features
  • Update documentation for API changes

Releasing

For information about publishing new versions to crates.io, see RELEASING.md.

Reporting Issues

Found a bug or have a feature request? Please open an issue with:

  • Clear description of the problem/feature
  • Steps to reproduce (for bugs)
  • Expected vs. actual behavior
  • System information (OS, Rust version)

License

This project is licensed under the BSD-3-Clause License - see the LICENSE file for details.

Acknowledgments

This project was inspired by:

Dependencies

rust-animation uses several excellent open-source libraries:

  • cgmath - Linear algebra and mathematics for graphics
  • gl - OpenGL bindings
  • image - Image encoding and decoding
  • keyframe - Keyframe animation library
  • stretch - Flexbox layout engine
  • ab_glyph - Font rendering
  • glfw - Window and OpenGL context creation (examples only)

Author: Joone Hur

Repository: https://github.com/joone/rust-animation