Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

- **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)
- **Border Rendering**: CoreAnimation-style border support with customizable width and RGBA color
- **Flex Layout**: CSS Flexbox-like layout system using the [Stretch](https://github.com/vislyhq/stretch) library
- **Hardware Acceleration**: wgpu-based rendering for high performance across multiple backends (Vulkan, Metal, D3D12, OpenGL)
- **Layer Hierarchy**: Support for nested layers with parent-child relationships
Expand Down Expand Up @@ -472,6 +473,7 @@ impl Layout for ActorLayout {
- Can have position (x, y, z), size (width, height)
- Supports transforms: translate, scale, rotate
- Can have colors or textures
- Supports border rendering with customizable width and color (CoreAnimation-style)
- Supports nested hierarchies (parent-child relationships)
- Can have animations, event handlers, and custom layouts

Expand All @@ -498,6 +500,15 @@ layer.y = y;
layer.set_color(r, g, b);
layer.set_image(path);

// Set border (CoreAnimation-style)
layer.set_border(width, r, g, b, a); // width in pixels, RGBA color

// CoreAnimation-style property setters
layer.set_position(x, y);
layer.set_background_color(r, g, b);
layer.set_opacity(opacity);
layer.set_bounds(width, height);

// Create animations
let mut animation = Animation::new();
animation.apply_translation_x(from, to, duration, easing);
Expand Down
153 changes: 153 additions & 0 deletions examples/border_demo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) 2021 Joone Hur <joone@chromium.org> All rights reserved.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copyright (c) 2026 Joone Hur joone@chromium.org All rights reserved

// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This example demonstrates border rendering with CoreAnimation-style API

use std::sync::Arc;
use winit::{
event::{Event, KeyEvent, WindowEvent},
event_loop::{ControlFlow, EventLoop},
keyboard::{KeyCode, PhysicalKey},
window::WindowBuilder,
};

use rust_animation::layer::LayoutMode;
use rust_animation::layer::Layer;
use rust_animation::play::Play;

fn main() {
let event_loop = EventLoop::new().unwrap();
let window = Arc::new(
WindowBuilder::new()
.with_title("Border Rendering Demo")
.with_inner_size(winit::dpi::LogicalSize::new(1280, 720))
.build(&event_loop)
.unwrap(),
);

// Get the actual window size (may differ from requested due to DPI scaling)
let window_size = window.inner_size();
let (width, height) = (window_size.width, window_size.height);

let mut play = Play::new(
"Border Rendering Demo".to_string(),
width as i32,
height as i32,
LayoutMode::UserDefine,
);

// Initialize wgpu context with surface using actual window size
play.init_wgpu_with_surface(window.clone(), width, height);

let mut stage = Layer::new("stage".to_string(), width, height, None);
stage.set_visible(true);
stage.set_background_color(0.95, 0.95, 0.95); // Light gray background

// Example 1: Red layer with black border
let mut layer1 = Layer::new("layer1".to_string(), 150, 150, None);
layer1.set_position(100, 100);
layer1.set_background_color(1.0, 0.0, 0.0); // Red
layer1.set_border(5.0, 0.0, 0.0, 0.0, 1.0); // 5px black border

// Example 2: Green layer with white border
let mut layer2 = Layer::new("layer2".to_string(), 150, 150, None);
layer2.set_position(300, 100);
layer2.set_background_color(0.0, 1.0, 0.0); // Green
layer2.set_border(3.0, 1.0, 1.0, 1.0, 1.0); // 3px white border

// Example 3: Blue layer with yellow border
let mut layer3 = Layer::new("layer3".to_string(), 150, 150, None);
layer3.set_position(500, 100);
layer3.set_background_color(0.0, 0.0, 1.0); // Blue
layer3.set_border(10.0, 1.0, 1.0, 0.0, 1.0); // 10px yellow border

// Example 4: White layer with semi-transparent red border
let mut layer4 = Layer::new("layer4".to_string(), 150, 150, None);
layer4.set_position(700, 100);
layer4.set_background_color(1.0, 1.0, 1.0); // White
layer4.set_border(8.0, 1.0, 0.0, 0.0, 0.5); // 8px semi-transparent red border

// Example 5: Nested layers with borders
let mut parent_layer = Layer::new("parent".to_string(), 200, 200, None);
parent_layer.set_position(100, 300);
parent_layer.set_background_color(0.8, 0.8, 0.8); // Light gray
parent_layer.set_border(5.0, 0.2, 0.2, 0.2, 1.0); // 5px dark gray border

let mut child_layer = Layer::new("child".to_string(), 100, 100, None);
child_layer.set_position(50, 50);
child_layer.set_background_color(1.0, 0.5, 0.0); // Orange
child_layer.set_border(3.0, 0.5, 0.0, 0.5, 1.0); // 3px purple border

parent_layer.add_sublayer(child_layer);

// Example 6: Various border widths
let border_widths = vec![1.0, 2.0, 5.0, 10.0, 15.0];
for (i, width) in border_widths.iter().enumerate() {
let mut layer = Layer::new(format!("border_{}", i), 80, 80, None);
layer.set_position(350 + (i as i32 * 100), 300);
layer.set_background_color(0.5, 0.5, 1.0); // Light blue
layer.set_border(*width, 0.0, 0.0, 0.5, 1.0); // Dark blue border
stage.add_sublayer(layer);
}

// Example 7: Layer with border and optional image
// If splash.png exists, it will be displayed with a border, otherwise just the border is shown
let mut image_layer = Layer::new("image_layer".to_string(), 200, 200, None);
image_layer.set_position(100, 520);
image_layer.set_background_color(0.9, 0.9, 0.9); // Light gray fallback
image_layer.set_image("examples/splash.png".to_string());
image_layer.set_border(5.0, 1.0, 0.0, 1.0, 1.0); // 5px magenta border

// Add all layers to stage
stage.add_sublayer(layer1);
stage.add_sublayer(layer2);
stage.add_sublayer(layer3);
stage.add_sublayer(layer4);
stage.add_sublayer(parent_layer);
stage.add_sublayer(image_layer);

play.add_stage(stage);

println!("Border Rendering Demo");
println!("=====================");
println!("Demonstrates various border styles:");
println!("- Top row: Different colors and widths");
println!("- Middle left: Nested layers with borders");
println!("- Middle: Progressive border widths (1px to 15px)");
println!("- Bottom: Image with border");
println!("\nPress ESC to exit");

event_loop
.run(move |event, elwt| {
elwt.set_control_flow(ControlFlow::Poll);

match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => elwt.exit(),
WindowEvent::KeyboardInput {
event:
KeyEvent {
physical_key: PhysicalKey::Code(KeyCode::Escape),
..
},
..
} => elwt.exit(),
WindowEvent::Resized(new_size) => {
// Update wgpu surface and projection when window is resized
play.resize(new_size.width, new_size.height);
}
WindowEvent::RedrawRequested => {
play.render();
window.request_redraw();
}
_ => {}
},
Event::AboutToWait => {
window.request_redraw();
}
_ => {}
}
})
.unwrap();
}
65 changes: 63 additions & 2 deletions src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ pub struct Layer {
pub visible: bool,
color: [f32; 3],
pub opacity: f32, // CoreAnimation-style property
border_width: f32, // CoreAnimation-style property
border_color: [f32; 4], // CoreAnimation-style property (RGBA)
pub image_path: String,
pub sub_layer_list: Vec<Layer>,
pub(crate) vertex_buffer: Option<wgpu::Buffer>,
Expand Down Expand Up @@ -133,6 +135,8 @@ impl Layer {
visible: true,
color: [1.0, 1.0, 1.0],
opacity: 1.0,
border_width: 0.0,
border_color: [0.0, 0.0, 0.0, 1.0],
image_path: "".to_string(),
sub_layer_list: Vec::new(),
vertex_buffer: None,
Expand Down Expand Up @@ -596,6 +600,29 @@ impl Layer {
&mut self.sub_layer_list
}

/// Set border width and color (CoreAnimation-style API)
///
/// # Arguments
/// * `width` - Border width in pixels (negative values are clamped to 0.0)
/// * `r` - Red component (0.0 to 1.0)
/// * `g` - Green component (0.0 to 1.0)
/// * `b` - Blue component (0.0 to 1.0)
/// * `a` - Alpha component (0.0 to 1.0)
pub fn set_border(&mut self, width: f32, r: f32, g: f32, b: f32, a: f32) {
self.border_width = width.max(0.0);
self.border_color = [r, g, b, a];
}

/// Get border width (CoreAnimation-style API)
pub fn border_width(&self) -> f32 {
self.border_width
}

/// Get border color (CoreAnimation-style API)
pub fn border_color(&self) -> (f32, f32, f32, f32) {
(self.border_color[0], self.border_color[1], self.border_color[2], self.border_color[3])
}

/// Create uniform buffer with transform matrix and color
pub fn create_uniform_buffer(
&self,
Expand All @@ -612,7 +639,11 @@ impl Layer {
projection: [[f32; 4]; 4],
color: [f32; 4],
use_texture: u32,
_padding: [u32; 3],
_use_texture_padding: [u32; 3],
border_width: f32,
border_color: [f32; 4],
layer_size: [f32; 2],
_layer_size_padding: [f32; 2],
}

let use_texture = if self.texture.is_some() { 1 } else { 0 };
Expand All @@ -622,7 +653,11 @@ impl Layer {
projection: (*projection).into(),
color: [self.color[0], self.color[1], self.color[2], self.opacity],
use_texture,
_padding: [0; 3],
_use_texture_padding: [0; 3],
border_width: self.border_width,
border_color: self.border_color,
layer_size: [self.width as f32, self.height as f32],
_layer_size_padding: [0.0; 2],
};

device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
Expand Down Expand Up @@ -791,4 +826,30 @@ mod tests {

assert!(layer.animation.is_some());
}

#[test]
fn test_border_api() {
let mut layer = Layer::new("test".to_string(), 100, 100, None);

// Test default border
assert_eq!(layer.border_width(), 0.0);
let (r, g, b, a) = layer.border_color();
assert_eq!(r, 0.0);
assert_eq!(g, 0.0);
assert_eq!(b, 0.0);
assert_eq!(a, 1.0);

// Test setting border
layer.set_border(5.0, 1.0, 0.0, 0.0, 0.5);
assert_eq!(layer.border_width(), 5.0);
let (r, g, b, a) = layer.border_color();
assert_eq!(r, 1.0);
assert_eq!(g, 0.0);
assert_eq!(b, 0.0);
assert_eq!(a, 0.5);

// Test border width clamping (negative values should be clamped to 0)
layer.set_border(-5.0, 0.0, 1.0, 0.0, 1.0);
assert_eq!(layer.border_width(), 0.0);
}
}
35 changes: 33 additions & 2 deletions src/play.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ struct VertexInput {
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
@location(1) pixel_pos: vec2<f32>,
}

struct Uniforms {
transform: mat4x4<f32>,
projection: mat4x4<f32>,
color: vec4<f32>,
use_texture: u32,
use_texture_padding: vec3<u32>,
border_width: f32,
border_color: vec4<f32>,
layer_size: vec2<f32>,
layer_size_padding: vec2<f32>,
}

@group(0) @binding(0)
Expand All @@ -44,16 +50,41 @@ fn vs_main(vertex: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = uniforms.projection * uniforms.transform * vec4<f32>(vertex.position, 1.0);
out.tex_coords = vertex.tex_coords;
// Pass through untransformed position for border calculation (in pixel space: 0..width, 0..height)
out.pixel_pos = vertex.position.xy;
return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
var base_color: vec4<f32>;

if (uniforms.use_texture > 0u) {
return textureSample(t_texture, t_sampler, in.tex_coords);
base_color = textureSample(t_texture, t_sampler, in.tex_coords);
} else {
return uniforms.color;
base_color = uniforms.color;
}

// Render border if border_width > 0
// pixel_pos is in layer's local coordinate space (0..width, 0..height)
if (uniforms.border_width > 0.0) {
let x = in.pixel_pos.x;
let y = in.pixel_pos.y;
let width = uniforms.layer_size.x;
let height = uniforms.layer_size.y;

// Check if we're in the border region (edges of the layer)
let is_border = x < uniforms.border_width ||
x > (width - uniforms.border_width) ||
y < uniforms.border_width ||
y > (height - uniforms.border_width);

if (is_border) {
return uniforms.border_color;
}
}

return base_color;
}
"#;

Expand Down