diff --git a/README.md b/README.md index 499ef7d..6ca032b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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); diff --git a/examples/border_demo.rs b/examples/border_demo.rs new file mode 100644 index 0000000..5df29b3 --- /dev/null +++ b/examples/border_demo.rs @@ -0,0 +1,153 @@ +// Copyright (c) 2021 Joone Hur 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(); +} diff --git a/src/layer.rs b/src/layer.rs index a5f1367..0cfc274 100644 --- a/src/layer.rs +++ b/src/layer.rs @@ -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, pub(crate) vertex_buffer: Option, @@ -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, @@ -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, @@ -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 }; @@ -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 { @@ -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); + } } diff --git a/src/play.rs b/src/play.rs index 337efa1..61fd9f6 100644 --- a/src/play.rs +++ b/src/play.rs @@ -22,6 +22,7 @@ struct VertexInput { struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) tex_coords: vec2, + @location(1) pixel_pos: vec2, } struct Uniforms { @@ -29,6 +30,11 @@ struct Uniforms { projection: mat4x4, color: vec4, use_texture: u32, + use_texture_padding: vec3, + border_width: f32, + border_color: vec4, + layer_size: vec2, + layer_size_padding: vec2, } @group(0) @binding(0) @@ -44,16 +50,41 @@ fn vs_main(vertex: VertexInput) -> VertexOutput { var out: VertexOutput; out.clip_position = uniforms.projection * uniforms.transform * vec4(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 { + var base_color: vec4; + 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; } "#;