From 09a6bcfffc24f5abdc8709403bab7ae1e01563f1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 13:15:17 +0200 Subject: Add `Image` rotation support Co-authored-by: DKolter <68352124+DKolter@users.noreply.github.com> --- core/src/image.rs | 2 ++ core/src/lib.rs | 2 ++ core/src/renderer/null.rs | 4 +++ core/src/rotation.rs | 37 +++++++++++++++++++++++ core/src/svg.rs | 2 ++ graphics/src/image.rs | 16 ++++++++-- renderer/src/fallback.rs | 12 ++++++-- src/lib.rs | 4 +-- tiny_skia/src/engine.rs | 49 +++++++++++++++++++++---------- tiny_skia/src/layer.rs | 32 +++++++++++++++++--- tiny_skia/src/lib.rs | 17 +++++++++-- tiny_skia/src/vector.rs | 4 ++- wgpu/src/image/mod.rs | 65 +++++++++++++++++++++++++++++++++++------ wgpu/src/layer.rs | 13 +++++++-- wgpu/src/lib.rs | 22 ++++++++++++-- wgpu/src/shader/image.wgsl | 37 ++++++++++++++++++----- widget/src/image.rs | 73 ++++++++++++++++++++++++++++++++++------------ widget/src/image/viewer.rs | 2 ++ widget/src/svg.rs | 65 +++++++++++++++++++++++++++++++---------- 19 files changed, 374 insertions(+), 84 deletions(-) create mode 100644 core/src/rotation.rs diff --git a/core/src/image.rs b/core/src/image.rs index c38239bc..5d1ab441 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -173,5 +173,7 @@ pub trait Renderer: crate::Renderer { handle: Self::Handle, filter_method: FilterMethod, bounds: Rectangle, + rotation: f32, + scale: Size, ); } diff --git a/core/src/lib.rs b/core/src/lib.rs index feda4fb4..da3ddcac 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -39,6 +39,7 @@ mod padding; mod pixels; mod point; mod rectangle; +mod rotation; mod shadow; mod shell; mod size; @@ -64,6 +65,7 @@ pub use pixels::Pixels; pub use point::Point; pub use rectangle::Rectangle; pub use renderer::Renderer; +pub use rotation::RotationLayout; pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index fe38ec87..d2dcfe4d 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -171,6 +171,8 @@ impl image::Renderer for () { _handle: Self::Handle, _filter_method: image::FilterMethod, _bounds: Rectangle, + _rotation: f32, + _scale: Size, ) { } } @@ -185,6 +187,8 @@ impl svg::Renderer for () { _handle: svg::Handle, _color: Option, _bounds: Rectangle, + _rotation: f32, + _scale: Size, ) { } } diff --git a/core/src/rotation.rs b/core/src/rotation.rs new file mode 100644 index 00000000..821aa494 --- /dev/null +++ b/core/src/rotation.rs @@ -0,0 +1,37 @@ +//! Control the rotation of some content (like an image) with the `RotationLayout` within a +//! space. +use crate::Size; + +/// The strategy used to rotate the content. +/// +/// This is used to control the behavior of the layout when the content is rotated +/// by a certain angle. +#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] +pub enum RotationLayout { + /// The layout is kept exactly as it was before the rotation. + /// + /// This is especially useful when used for animations, as it will avoid the + /// layout being shifted or resized when smoothly i.e. an icon. + Keep, + /// The layout is adjusted to fit the rotated content. + /// + /// This allows you to rotate an image and have the layout adjust to fit the new + /// size of the image. + Change, +} + +impl RotationLayout { + /// Applies the rotation to the layout while respecting the [`RotationLayout`] strategy. + /// The rotation is given in radians. + pub fn apply_to_size(&self, size: Size, rotation: f32) -> Size { + match self { + Self::Keep => size, + Self::Change => Size { + width: (size.width * rotation.cos()).abs() + + (size.height * rotation.sin()).abs(), + height: (size.width * rotation.sin()).abs() + + (size.height * rotation.cos()).abs(), + }, + } + } +} diff --git a/core/src/svg.rs b/core/src/svg.rs index 0106e0c2..74dd7f4a 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -100,5 +100,7 @@ pub trait Renderer: crate::Renderer { handle: Handle, color: Option, bounds: Rectangle, + rotation: f32, + scale: Size, ); } diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 04c45057..083248bf 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,9 +2,7 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::image; -use crate::core::svg; -use crate::core::{Color, Rectangle}; +use crate::core::{image, svg, Color, Rectangle, Size}; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] @@ -19,6 +17,12 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, + + /// The rotation of the image in radians + rotation: f32, + + /// The scale of the image after rotation + scale: Size, }, /// A vector image. Vector { @@ -30,6 +34,12 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, + + /// The rotation of the image in radians + rotation: f32, + + /// The scale of the image after rotation + scale: Size, }, } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 5f69b420..37e6ac43 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -154,11 +154,13 @@ where handle: Self::Handle, filter_method: image::FilterMethod, bounds: Rectangle, + rotation: f32, + scale: Size, ) { delegate!( self, renderer, - renderer.draw_image(handle, filter_method, bounds) + renderer.draw_image(handle, filter_method, bounds, rotation, scale) ); } } @@ -177,8 +179,14 @@ where handle: svg::Handle, color: Option, bounds: Rectangle, + rotation: f32, + scale: Size, ) { - delegate!(self, renderer, renderer.draw_svg(handle, color, bounds)); + delegate!( + self, + renderer, + renderer.draw_svg(handle, color, bounds, rotation, scale) + ); } } diff --git a/src/lib.rs b/src/lib.rs index 7517dd14..9d07fed6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,8 +200,8 @@ pub use crate::core::gradient; pub use crate::core::theme; pub use crate::core::{ Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, - Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Theme, - Transformation, Vector, + Length, Padding, Pixels, Point, Radians, Rectangle, RotationLayout, Shadow, + Size, Theme, Transformation, Vector, }; pub mod clipboard { diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index fbca1274..564d3752 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -539,10 +539,10 @@ impl Engine { pub fn draw_image( &mut self, image: &Image, - _transformation: Transformation, - _pixels: &mut tiny_skia::PixmapMut<'_>, - _clip_mask: &mut tiny_skia::Mask, - _clip_bounds: Rectangle, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, ) { match image { #[cfg(feature = "image")] @@ -550,22 +550,31 @@ impl Engine { handle, filter_method, bounds, + rotation, + scale, } => { - let physical_bounds = *bounds * _transformation; + let physical_bounds = *bounds * transformation; - if !_clip_bounds.intersects(&physical_bounds) { + if !clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) - .then_some(_clip_mask as &_); + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let center = physical_bounds.center(); + let transform = into_transform(transformation) + .post_rotate_at(rotation.to_degrees(), center.x, center.y) + .post_translate(-center.x, -center.y) + .post_scale(scale.width, scale.height) + .post_translate(center.x, center.y); self.raster_pipeline.draw( handle, *filter_method, *bounds, - _pixels, - into_transform(_transformation), + pixels, + transform, clip_mask, ); } @@ -574,21 +583,31 @@ impl Engine { handle, color, bounds, + rotation, + scale, } => { - let physical_bounds = *bounds * _transformation; + let physical_bounds = *bounds * transformation; - if !_clip_bounds.intersects(&physical_bounds) { + if !clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) - .then_some(_clip_mask as &_); + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let center = physical_bounds.center(); + let transform = into_transform(transformation) + .post_rotate_at(rotation.to_degrees(), center.x, center.y) + .post_translate(-center.x, -center.y) + .post_scale(scale.width, scale.height) + .post_translate(center.x, center.y); self.vector_pipeline.draw( handle, *color, physical_bounds, - _pixels, + pixels, + transform, clip_mask, ); } diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 3e42e4aa..e9651814 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,7 +1,7 @@ -use crate::core::image; -use crate::core::renderer::Quad; -use crate::core::svg; -use crate::core::{Background, Color, Point, Rectangle, Transformation}; +use crate::core::{ + image, renderer::Quad, svg, Background, Color, Point, Rectangle, Size, + Transformation, +}; use crate::graphics::damage; use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph, Text}; @@ -121,11 +121,15 @@ impl Layer { filter_method: image::FilterMethod, bounds: Rectangle, transformation: Transformation, + rotation: f32, + scale: Size, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, + rotation, + scale, }; self.images.push(image); @@ -137,11 +141,15 @@ impl Layer { color: Option, bounds: Rectangle, transformation: Transformation, + rotation: f32, + scale: Size, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, + rotation, + scale, }; self.images.push(svg); @@ -256,6 +264,22 @@ impl Layer { Image::eq, ); + // let center = bounds.center(); + // let rotated_size = RotationLayout::Change + // .apply_to_size(bounds.size(), *rotation); + // + // let scaled_size = Size::new( + // rotated_size.width * scale.width, + // rotated_size.height * scale.height, + // ); + // + // let top_left = Point::new( + // center.x - scaled_size.width / 2.0, + // center.y - scaled_size.height / 2.0, + // ); + // + // Rectangle::new(top_left, scaled_size).expand(1.0) + damage.extend(text); damage.extend(primitives); damage.extend(images); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 4c2c9430..4e3ebad3 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,7 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; use crate::engine::Engine; use crate::graphics::compositor; @@ -377,9 +377,18 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, + rotation: f32, + scale: Size, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image(handle, filter_method, bounds, transformation); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + scale, + ); } } @@ -397,9 +406,11 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color: Option, bounds: Rectangle, + rotation: f32, + scale: Size, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color, bounds, transformation); + layer.draw_svg(handle, color, bounds, transformation, rotation, scale); } } diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index 5150cffe..8e3463f2 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -4,6 +4,7 @@ use crate::graphics::text; use resvg::usvg::{self, TreeTextToPath}; use rustc_hash::{FxHashMap, FxHashSet}; +use tiny_skia::Transform; use std::cell::RefCell; use std::collections::hash_map; @@ -34,6 +35,7 @@ impl Pipeline { color: Option, bounds: Rectangle, pixels: &mut tiny_skia::PixmapMut<'_>, + transform: Transform, clip_mask: Option<&tiny_skia::Mask>, ) { if let Some(image) = self.cache.borrow_mut().draw( @@ -46,7 +48,7 @@ impl Pipeline { bounds.y as i32, image, &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::identity(), + transform, clip_mask, ); } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 8b831a3c..69f8a8ca 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -135,14 +135,20 @@ impl Pipeline { attributes: &wgpu::vertex_attr_array!( // Position 0 => Float32x2, - // Scale + // Center 1 => Float32x2, - // Atlas position + // Image size 2 => Float32x2, + // Rotation + 3 => Float32, + // Scale + 4 => Float32x2, + // Atlas position + 5 => Float32x2, // Atlas scale - 3 => Float32x2, + 6 => Float32x2, // Layer - 4 => Sint32, + 7 => Sint32, ), }], }, @@ -208,9 +214,10 @@ impl Pipeline { belt: &mut wgpu::util::StagingBelt, images: &Batch, transformation: Transformation, - scale: f32, + global_scale: f32, ) { - let transformation = transformation * Transformation::scale(scale); + let transformation = + transformation * Transformation::scale(global_scale); let nearest_instances: &mut Vec = &mut Vec::new(); let linear_instances: &mut Vec = &mut Vec::new(); @@ -224,6 +231,8 @@ impl Pipeline { handle, filter_method, bounds, + rotation, + scale, } => { if let Some(atlas_entry) = cache.upload_raster(device, encoder, handle) @@ -231,6 +240,8 @@ impl Pipeline { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], + *rotation, + [scale.width, scale.height], atlas_entry, match filter_method { crate::core::image::FilterMethod::Nearest => { @@ -251,15 +262,24 @@ impl Pipeline { handle, color, bounds, + rotation, + scale, } => { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = cache.upload_vector( - device, encoder, handle, *color, size, scale, + device, + encoder, + handle, + *color, + size, + global_scale, ) { add_instances( [bounds.x, bounds.y], size, + *rotation, + [scale.width, scale.height], atlas_entry, nearest_instances, ); @@ -487,7 +507,10 @@ impl Data { #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Instance { _position: [f32; 2], + _center: [f32; 2], _size: [f32; 2], + _rotation: f32, + _scale: [f32; 2], _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -506,12 +529,27 @@ struct Uniforms { fn add_instances( image_position: [f32; 2], image_size: [f32; 2], + rotation: f32, + scale: [f32; 2], entry: &atlas::Entry, instances: &mut Vec, ) { + let center = [ + image_position[0] + image_size[0] / 2.0, + image_position[1] + image_size[1] / 2.0, + ]; + match entry { atlas::Entry::Contiguous(allocation) => { - add_instance(image_position, image_size, allocation, instances); + add_instance( + image_position, + center, + image_size, + rotation, + scale, + allocation, + instances, + ); } atlas::Entry::Fragmented { fragments, size } => { let scaling_x = image_size[0] / size.width as f32; @@ -537,7 +575,10 @@ fn add_instances( fragment_height as f32 * scaling_y, ]; - add_instance(position, size, allocation, instances); + add_instance( + position, center, size, rotation, scale, allocation, + instances, + ); } } } @@ -546,7 +587,10 @@ fn add_instances( #[inline] fn add_instance( position: [f32; 2], + center: [f32; 2], size: [f32; 2], + rotation: f32, + scale: [f32; 2], allocation: &atlas::Allocation, instances: &mut Vec, ) { @@ -556,7 +600,10 @@ fn add_instance( let instance = Instance { _position: position, + _center: center, _size: size, + _rotation: rotation, + _scale: scale, _position_in_atlas: [ (x as f32 + 0.5) / atlas::SIZE as f32, (y as f32 + 0.5) / atlas::SIZE as f32, diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 9526c5a8..648ec476 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,5 +1,6 @@ -use crate::core::renderer; -use crate::core::{Background, Color, Point, Rectangle, Transformation}; +use crate::core::{ + renderer, Background, Color, Point, Rectangle, Size, Transformation, +}; use crate::graphics; use crate::graphics::color; use crate::graphics::layer; @@ -117,11 +118,15 @@ impl Layer { filter_method: crate::core::image::FilterMethod, bounds: Rectangle, transformation: Transformation, + rotation: f32, + scale: Size, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, + rotation, + scale, }; self.images.push(image); @@ -133,11 +138,15 @@ impl Layer { color: Option, bounds: Rectangle, transformation: Transformation, + rotation: f32, + scale: Size, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, + rotation, + scale, }; self.images.push(svg); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 178522de..a42d71c3 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -517,9 +517,18 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, + rotation: f32, + scale: Size, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image(handle, filter_method, bounds, transformation); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + scale, + ); } } @@ -534,9 +543,18 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color_filter: Option, bounds: Rectangle, + rotation: f32, + scale: Size, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color_filter, bounds, transformation); + layer.draw_svg( + handle, + color_filter, + bounds, + transformation, + rotation, + scale, + ); } } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 7b2e5238..de962098 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -9,10 +9,13 @@ struct Globals { struct VertexInput { @builtin(vertex_index) vertex_index: u32, @location(0) pos: vec2, - @location(1) scale: vec2, - @location(2) atlas_pos: vec2, - @location(3) atlas_scale: vec2, - @location(4) layer: i32, + @location(1) center: vec2, + @location(2) image_size: vec2, + @location(3) rotation: f32, + @location(4) scale: vec2, + @location(5) atlas_pos: vec2, + @location(6) atlas_scale: vec2, + @location(7) layer: i32, } struct VertexOutput { @@ -25,24 +28,42 @@ struct VertexOutput { fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; - let v_pos = vertex_position(input.vertex_index); + // Generate a vertex position in the range [0, 1] from the vertex index. + var v_pos = vertex_position(input.vertex_index); + // Map the vertex position to the atlas texture. out.uv = vec2(v_pos * input.atlas_scale + input.atlas_pos); out.layer = f32(input.layer); - var transform: mat4x4 = mat4x4( + // Calculate the vertex position and move the center to the origin + v_pos = input.pos + v_pos * input.image_size - input.center; + + // Apply the rotation around the center of the image + let cos_rot = cos(input.rotation); + let sin_rot = sin(input.rotation); + let rotate = mat4x4( + vec4(cos_rot, sin_rot, 0.0, 0.0), + vec4(-sin_rot, cos_rot, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(0.0, 0.0, 0.0, 1.0) + ); + + // Scale the image and then translate to the final position by moving the center to the position + let scale_translate = mat4x4( vec4(input.scale.x, 0.0, 0.0, 0.0), vec4(0.0, input.scale.y, 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0), - vec4(input.pos, 0.0, 1.0) + vec4(input.center, 0.0, 1.0) ); - out.position = globals.transform * transform * vec4(v_pos, 0.0, 1.0); + // Calculate the final position of the vertex + out.position = globals.transform * scale_translate * rotate * vec4(v_pos, 0.0, 1.0); return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { + // Sample the texture at the given UV coordinate and layer. return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)); } diff --git a/widget/src/image.rs b/widget/src/image.rs index 21d371b7..3a0a5e53 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,5 +1,6 @@ //! Display images in your user interface. pub mod viewer; +use iced_renderer::core::{Point, RotationLayout}; pub use viewer::Viewer; use crate::core::image; @@ -8,7 +9,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Rectangle, Size, Widget, }; pub use image::{FilterMethod, Handle}; @@ -36,6 +37,8 @@ pub struct Image { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + rotation: f32, + rotation_layout: RotationLayout, } impl Image { @@ -47,6 +50,8 @@ impl Image { height: Length::Shrink, content_fit: ContentFit::Contain, filter_method: FilterMethod::default(), + rotation: 0.0, + rotation_layout: RotationLayout::Change, } } @@ -75,6 +80,18 @@ impl Image { self.filter_method = filter_method; self } + + /// Rotates the [`Image`] by the given angle in radians. + pub fn rotation(mut self, degrees: f32) -> Self { + self.rotation = degrees; + self + } + + /// Sets the [`RotationLayout`] of the [`Image`]. + pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { + self.rotation_layout = rotation_layout; + self + } } /// Computes the layout of an [`Image`]. @@ -85,22 +102,25 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, + rotation: f32, + rotation_layout: RotationLayout, ) -> layout::Node where Renderer: image::Renderer, { // The raw w/h of the underlying image - let image_size = { - let Size { width, height } = renderer.measure_image(handle); + let image_size = renderer.measure_image(handle); + let image_size = + Size::new(image_size.width as f32, image_size.height as f32); - Size::new(width as f32, height as f32) - }; + // The rotated size of the image + let rotated_size = rotation_layout.apply_to_size(image_size, rotation); // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(width, height, image_size); + let raw_size = limits.resolve(width, height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = content_fit.fit(image_size, raw_size); + let full_size = content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -124,32 +144,45 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, + rotation: f32, + rotation_layout: RotationLayout, ) where Renderer: image::Renderer, Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = rotation_layout.apply_to_size(image_size, rotation); let bounds = layout.bounds(); - let adjusted_fit = content_fit.fit(image_size, bounds.size()); + let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); + let scale = Size::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds + let position = match content_fit { + ContentFit::None => Point::new( + bounds.position().x + + (rotated_size.width - image_size.width) / 2.0, + bounds.position().y + + (rotated_size.height - image_size.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - image_size.width / 2.0, + bounds.center_y() - image_size.height / 2.0, + ), }; + let drawing_bounds = Rectangle::new(position, image_size); + renderer.draw_image( handle.clone(), filter_method, - drawing_bounds + offset, + drawing_bounds, + rotation, + scale, ); }; @@ -187,6 +220,8 @@ where self.width, self.height, self.content_fit, + self.rotation, + self.rotation_layout, ) } @@ -206,6 +241,8 @@ where &self.handle, self.content_fit, self.filter_method, + self.rotation, + self.rotation_layout, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 214cb996..ccdfdebb 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -341,6 +341,8 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + 0.0, + Size::UNIT, ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index eb142189..21946af8 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,8 +5,8 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector, - Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, + RotationLayout, Size, Theme, Widget, }; use std::path::PathBuf; @@ -29,6 +29,8 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, + rotation: f32, + rotation_layout: RotationLayout, } impl<'a, Theme> Svg<'a, Theme> @@ -43,6 +45,8 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), + rotation: 0.0, + rotation_layout: RotationLayout::Change, } } @@ -95,6 +99,18 @@ where self.class = class.into(); self } + + /// Rotates the [`Svg`] by the given angle in radians. + pub fn rotation(mut self, degrees: f32) -> Self { + self.rotation = degrees; + self + } + + /// Sets the [`RotationLayout`] of the [`Svg`]. + pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { + self.rotation_layout = rotation_layout; + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -120,11 +136,16 @@ where let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + // The rotated size of the svg + let rotated_size = self + .rotation_layout + .apply_to_size(image_size, self.rotation); + // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(self.width, self.height, image_size); + let raw_size = limits.resolve(self.width, self.height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = self.content_fit.fit(image_size, raw_size); + let full_size = self.content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -153,23 +174,35 @@ where ) { let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = self + .rotation_layout + .apply_to_size(image_size, self.rotation); let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); + let scale = Size::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); + let is_mouse_over = cursor.is_over(bounds); let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.position().x + + (rotated_size.width - image_size.width) / 2.0, + bounds.position().y + + (rotated_size.height - image_size.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - image_size.width / 2.0, + bounds.center_y() - image_size.height / 2.0, + ), }; + let drawing_bounds = Rectangle::new(position, image_size); + let status = if is_mouse_over { Status::Hovered } else { @@ -181,7 +214,9 @@ where renderer.draw_svg( self.handle.clone(), style.color, - drawing_bounds + offset, + drawing_bounds, + self.rotation, + scale, ); }; -- cgit From a57313b23ecb9843856ca0ea08635b6121fcb2cb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 15:21:22 +0200 Subject: Simplify image rotation API and its internals --- core/src/angle.rs | 6 ++++ core/src/content_fit.rs | 3 +- core/src/image.rs | 5 ++- core/src/lib.rs | 2 +- core/src/rectangle.rs | 16 +++++++++ core/src/renderer/null.rs | 9 +++--- core/src/rotation.rs | 71 ++++++++++++++++++++++++++++------------ core/src/size.rs | 14 ++++++++ core/src/svg.rs | 5 ++- core/src/vector.rs | 3 ++ graphics/src/image.rs | 12 ++----- renderer/src/fallback.rs | 12 +++---- src/lib.rs | 4 +-- tiny_skia/src/engine.rs | 26 ++++++++------- tiny_skia/src/layer.rs | 10 ++---- tiny_skia/src/lib.rs | 11 +++---- wgpu/src/image/mod.rs | 24 ++++---------- wgpu/src/layer.rs | 10 ++---- wgpu/src/lib.rs | 20 +++--------- wgpu/src/shader/image.wgsl | 21 ++++-------- widget/src/image.rs | 70 +++++++++++++++++---------------------- widget/src/image/viewer.rs | 7 ++-- widget/src/svg.rs | 81 +++++++++++++++++++--------------------------- 23 files changed, 218 insertions(+), 224 deletions(-) diff --git a/core/src/angle.rs b/core/src/angle.rs index dc3c0e93..69630717 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -65,6 +65,12 @@ impl From for Radians { } } +impl From for f32 { + fn from(radians: Radians) -> Self { + radians.0 + } +} + impl From for f64 { fn from(radians: Radians) -> Self { Self::from(radians.0) diff --git a/core/src/content_fit.rs b/core/src/content_fit.rs index 6bbedc7a..56d2ffa6 100644 --- a/core/src/content_fit.rs +++ b/core/src/content_fit.rs @@ -11,7 +11,7 @@ use crate::Size; /// in CSS, see [Mozilla's docs][1], or run the `tour` example /// /// [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Default)] pub enum ContentFit { /// Scale as big as it can be without needing to crop or hide parts. /// @@ -23,6 +23,7 @@ pub enum ContentFit { /// This is a great fit for when you need to display an image without losing /// any part of it, particularly when the image itself is the focus of the /// screen. + #[default] Contain, /// Scale the image to cover all of the bounding box, cropping if needed. diff --git a/core/src/image.rs b/core/src/image.rs index 5d1ab441..91a7fd36 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,7 +1,7 @@ //! Load and draw raster graphics. pub use bytes::Bytes; -use crate::{Rectangle, Size}; +use crate::{Radians, Rectangle, Size}; use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; @@ -173,7 +173,6 @@ pub trait Renderer: crate::Renderer { handle: Self::Handle, filter_method: FilterMethod, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ); } diff --git a/core/src/lib.rs b/core/src/lib.rs index da3ddcac..32156441 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -65,7 +65,7 @@ pub use pixels::Pixels; pub use point::Point; pub use rectangle::Rectangle; pub use renderer::Renderer; -pub use rotation::RotationLayout; +pub use rotation::Rotation; pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 2ab50137..fb66131a 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -227,3 +227,19 @@ where } } } + +impl std::ops::Mul> for Rectangle +where + T: std::ops::Mul + Copy, +{ + type Output = Rectangle; + + fn mul(self, scale: Vector) -> Self { + Rectangle { + x: self.x * scale.x, + y: self.y * scale.y, + width: self.width * scale.x, + height: self.height * scale.y, + } + } +} diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index d2dcfe4d..91519b40 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -4,7 +4,8 @@ use crate::renderer::{self, Renderer}; use crate::svg; use crate::text::{self, Text}; use crate::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, + Transformation, }; impl Renderer for () { @@ -171,8 +172,7 @@ impl image::Renderer for () { _handle: Self::Handle, _filter_method: image::FilterMethod, _bounds: Rectangle, - _rotation: f32, - _scale: Size, + _rotation: Radians, ) { } } @@ -187,8 +187,7 @@ impl svg::Renderer for () { _handle: svg::Handle, _color: Option, _bounds: Rectangle, - _rotation: f32, - _scale: Size, + _rotation: Radians, ) { } } diff --git a/core/src/rotation.rs b/core/src/rotation.rs index 821aa494..ebb85f3c 100644 --- a/core/src/rotation.rs +++ b/core/src/rotation.rs @@ -1,37 +1,68 @@ -//! Control the rotation of some content (like an image) with the `RotationLayout` within a -//! space. -use crate::Size; +//! Control the rotation of some content (like an image) within a space. +use crate::{Radians, Size}; /// The strategy used to rotate the content. /// /// This is used to control the behavior of the layout when the content is rotated /// by a certain angle. -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] -pub enum RotationLayout { - /// The layout is kept exactly as it was before the rotation. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Rotation { + /// The element will float while rotating. The layout will be kept exactly as it was + /// before the rotation. /// /// This is especially useful when used for animations, as it will avoid the /// layout being shifted or resized when smoothly i.e. an icon. - Keep, - /// The layout is adjusted to fit the rotated content. + /// + /// This is the default. + Floating(Radians), + /// The element will be solid while rotating. The layout will be adjusted to fit + /// the rotated content. /// /// This allows you to rotate an image and have the layout adjust to fit the new /// size of the image. - Change, + Solid(Radians), } -impl RotationLayout { - /// Applies the rotation to the layout while respecting the [`RotationLayout`] strategy. - /// The rotation is given in radians. - pub fn apply_to_size(&self, size: Size, rotation: f32) -> Size { +impl Rotation { + /// Returns the angle of the [`Rotation`] in [`Radians`]. + pub fn radians(self) -> Radians { + match self { + Rotation::Floating(radians) | Rotation::Solid(radians) => radians, + } + } + + /// Rotates the given [`Size`]. + pub fn apply(self, size: Size) -> Size { match self { - Self::Keep => size, - Self::Change => Size { - width: (size.width * rotation.cos()).abs() - + (size.height * rotation.sin()).abs(), - height: (size.width * rotation.sin()).abs() - + (size.height * rotation.cos()).abs(), - }, + Self::Floating(_) => size, + Self::Solid(rotation) => { + let radians = f32::from(rotation); + + Size { + width: (size.width * radians.cos()).abs() + + (size.height * radians.sin()).abs(), + height: (size.width * radians.sin()).abs() + + (size.height * radians.cos()).abs(), + } + } } } } + +impl Default for Rotation { + fn default() -> Self { + Self::Floating(Radians(0.0)) + } +} + +impl From for Rotation { + fn from(radians: Radians) -> Self { + Self::Floating(radians) + } +} + +impl From for Rotation { + fn from(radians: f32) -> Self { + Self::Floating(Radians(radians)) + } +} diff --git a/core/src/size.rs b/core/src/size.rs index c2b5671a..66be2d85 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -113,3 +113,17 @@ where } } } + +impl std::ops::Mul> for Size +where + T: std::ops::Mul + Copy, +{ + type Output = Size; + + fn mul(self, scale: Vector) -> Self::Output { + Size { + width: self.width * scale.x, + height: self.height * scale.y, + } + } +} diff --git a/core/src/svg.rs b/core/src/svg.rs index 74dd7f4a..01f102e3 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -1,5 +1,5 @@ //! Load and draw vector graphics. -use crate::{Color, Rectangle, Size}; +use crate::{Color, Radians, Rectangle, Size}; use rustc_hash::FxHasher; use std::borrow::Cow; @@ -100,7 +100,6 @@ pub trait Renderer: crate::Renderer { handle: Handle, color: Option, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ); } diff --git a/core/src/vector.rs b/core/src/vector.rs index 1380c3b3..049e648f 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -18,6 +18,9 @@ impl Vector { impl Vector { /// The zero [`Vector`]. pub const ZERO: Self = Self::new(0.0, 0.0); + + /// The unit [`Vector`]. + pub const UNIT: Self = Self::new(0.0, 0.0); } impl std::ops::Add for Vector diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 083248bf..4fd6998d 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,7 +2,7 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::{image, svg, Color, Rectangle, Size}; +use crate::core::{image, svg, Color, Radians, Rectangle}; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] @@ -19,10 +19,7 @@ pub enum Image { bounds: Rectangle, /// The rotation of the image in radians - rotation: f32, - - /// The scale of the image after rotation - scale: Size, + rotation: Radians, }, /// A vector image. Vector { @@ -36,10 +33,7 @@ pub enum Image { bounds: Rectangle, /// The rotation of the image in radians - rotation: f32, - - /// The scale of the image after rotation - scale: Size, + rotation: Radians, }, } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 37e6ac43..a077031b 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -3,7 +3,7 @@ use crate::core::image; use crate::core::renderer; use crate::core::svg; use crate::core::{ - self, Background, Color, Point, Rectangle, Size, Transformation, + self, Background, Color, Point, Radians, Rectangle, Size, Transformation, }; use crate::graphics; use crate::graphics::compositor; @@ -154,13 +154,12 @@ where handle: Self::Handle, filter_method: image::FilterMethod, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { delegate!( self, renderer, - renderer.draw_image(handle, filter_method, bounds, rotation, scale) + renderer.draw_image(handle, filter_method, bounds, rotation) ); } } @@ -179,13 +178,12 @@ where handle: svg::Handle, color: Option, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { delegate!( self, renderer, - renderer.draw_svg(handle, color, bounds, rotation, scale) + renderer.draw_svg(handle, color, bounds, rotation) ); } } diff --git a/src/lib.rs b/src/lib.rs index 9d07fed6..50ee7ecc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,8 +200,8 @@ pub use crate::core::gradient; pub use crate::core::theme; pub use crate::core::{ Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, - Length, Padding, Pixels, Point, Radians, Rectangle, RotationLayout, Shadow, - Size, Theme, Transformation, Vector, + Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, + Theme, Transformation, Vector, }; pub mod clipboard { diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 564d3752..e9935bdb 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -551,7 +551,6 @@ impl Engine { filter_method, bounds, rotation, - scale, } => { let physical_bounds = *bounds * transformation; @@ -563,11 +562,13 @@ impl Engine { .then_some(clip_mask as &_); let center = physical_bounds.center(); - let transform = into_transform(transformation) - .post_rotate_at(rotation.to_degrees(), center.x, center.y) - .post_translate(-center.x, -center.y) - .post_scale(scale.width, scale.height) - .post_translate(center.x, center.y); + let radians = f32::from(*rotation); + + let transform = into_transform(transformation).post_rotate_at( + radians.to_degrees(), + center.x, + center.y, + ); self.raster_pipeline.draw( handle, @@ -584,7 +585,6 @@ impl Engine { color, bounds, rotation, - scale, } => { let physical_bounds = *bounds * transformation; @@ -596,11 +596,13 @@ impl Engine { .then_some(clip_mask as &_); let center = physical_bounds.center(); - let transform = into_transform(transformation) - .post_rotate_at(rotation.to_degrees(), center.x, center.y) - .post_translate(-center.x, -center.y) - .post_scale(scale.width, scale.height) - .post_translate(center.x, center.y); + let radians = f32::from(*rotation); + + let transform = into_transform(transformation).post_rotate_at( + radians.to_degrees(), + center.x, + center.y, + ); self.vector_pipeline.draw( handle, diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index e9651814..c8a31ba3 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,5 +1,5 @@ use crate::core::{ - image, renderer::Quad, svg, Background, Color, Point, Rectangle, Size, + image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle, Transformation, }; use crate::graphics::damage; @@ -121,15 +121,13 @@ impl Layer { filter_method: image::FilterMethod, bounds: Rectangle, transformation: Transformation, - rotation: f32, - scale: Size, + rotation: Radians, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, rotation, - scale, }; self.images.push(image); @@ -141,15 +139,13 @@ impl Layer { color: Option, bounds: Rectangle, transformation: Transformation, - rotation: f32, - scale: Size, + rotation: Radians, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, rotation, - scale, }; self.images.push(svg); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 4e3ebad3..75aaaf92 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,7 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Transformation, }; use crate::engine::Engine; use crate::graphics::compositor; @@ -377,8 +377,7 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -387,7 +386,6 @@ impl core::image::Renderer for Renderer { bounds, transformation, rotation, - scale, ); } } @@ -406,11 +404,10 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color: Option, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color, bounds, transformation, rotation, scale); + layer.draw_svg(handle, color, bounds, transformation, rotation); } } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 69f8a8ca..3ec341fc 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -141,14 +141,12 @@ impl Pipeline { 2 => Float32x2, // Rotation 3 => Float32, - // Scale - 4 => Float32x2, // Atlas position - 5 => Float32x2, + 4 => Float32x2, // Atlas scale - 6 => Float32x2, + 5 => Float32x2, // Layer - 7 => Sint32, + 6 => Sint32, ), }], }, @@ -232,7 +230,6 @@ impl Pipeline { filter_method, bounds, rotation, - scale, } => { if let Some(atlas_entry) = cache.upload_raster(device, encoder, handle) @@ -240,8 +237,7 @@ impl Pipeline { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], - *rotation, - [scale.width, scale.height], + f32::from(*rotation), atlas_entry, match filter_method { crate::core::image::FilterMethod::Nearest => { @@ -263,7 +259,6 @@ impl Pipeline { color, bounds, rotation, - scale, } => { let size = [bounds.width, bounds.height]; @@ -278,8 +273,7 @@ impl Pipeline { add_instances( [bounds.x, bounds.y], size, - *rotation, - [scale.width, scale.height], + f32::from(*rotation), atlas_entry, nearest_instances, ); @@ -510,7 +504,6 @@ struct Instance { _center: [f32; 2], _size: [f32; 2], _rotation: f32, - _scale: [f32; 2], _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -530,7 +523,6 @@ fn add_instances( image_position: [f32; 2], image_size: [f32; 2], rotation: f32, - scale: [f32; 2], entry: &atlas::Entry, instances: &mut Vec, ) { @@ -546,7 +538,6 @@ fn add_instances( center, image_size, rotation, - scale, allocation, instances, ); @@ -576,8 +567,7 @@ fn add_instances( ]; add_instance( - position, center, size, rotation, scale, allocation, - instances, + position, center, size, rotation, allocation, instances, ); } } @@ -590,7 +580,6 @@ fn add_instance( center: [f32; 2], size: [f32; 2], rotation: f32, - scale: [f32; 2], allocation: &atlas::Allocation, instances: &mut Vec, ) { @@ -603,7 +592,6 @@ fn add_instance( _center: center, _size: size, _rotation: rotation, - _scale: scale, _position_in_atlas: [ (x as f32 + 0.5) / atlas::SIZE as f32, (y as f32 + 0.5) / atlas::SIZE as f32, diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 648ec476..e0242c59 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,5 +1,5 @@ use crate::core::{ - renderer, Background, Color, Point, Rectangle, Size, Transformation, + renderer, Background, Color, Point, Radians, Rectangle, Transformation, }; use crate::graphics; use crate::graphics::color; @@ -118,15 +118,13 @@ impl Layer { filter_method: crate::core::image::FilterMethod, bounds: Rectangle, transformation: Transformation, - rotation: f32, - scale: Size, + rotation: Radians, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, rotation, - scale, }; self.images.push(image); @@ -138,15 +136,13 @@ impl Layer { color: Option, bounds: Rectangle, transformation: Transformation, - rotation: f32, - scale: Size, + rotation: Radians, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, rotation, - scale, }; self.images.push(svg); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index a42d71c3..6920067b 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -61,7 +61,8 @@ pub use settings::Settings; pub use geometry::Geometry; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, + Transformation, Vector, }; use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; @@ -378,7 +379,6 @@ impl Renderer { use crate::core::alignment; use crate::core::text::Renderer as _; use crate::core::Renderer as _; - use crate::core::Vector; self.with_layer( Rectangle::with_size(viewport.logical_size()), @@ -517,8 +517,7 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -527,7 +526,6 @@ impl core::image::Renderer for Renderer { bounds, transformation, rotation, - scale, ); } } @@ -543,18 +541,10 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color_filter: Option, bounds: Rectangle, - rotation: f32, - scale: Size, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg( - handle, - color_filter, - bounds, - transformation, - rotation, - scale, - ); + layer.draw_svg(handle, color_filter, bounds, transformation, rotation); } } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index de962098..71bf939c 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -10,12 +10,11 @@ struct VertexInput { @builtin(vertex_index) vertex_index: u32, @location(0) pos: vec2, @location(1) center: vec2, - @location(2) image_size: vec2, + @location(2) scale: vec2, @location(3) rotation: f32, - @location(4) scale: vec2, - @location(5) atlas_pos: vec2, - @location(6) atlas_scale: vec2, - @location(7) layer: i32, + @location(4) atlas_pos: vec2, + @location(5) atlas_scale: vec2, + @location(6) layer: i32, } struct VertexOutput { @@ -36,7 +35,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { out.layer = f32(input.layer); // Calculate the vertex position and move the center to the origin - v_pos = input.pos + v_pos * input.image_size - input.center; + v_pos = input.pos + v_pos * input.scale - input.center; // Apply the rotation around the center of the image let cos_rot = cos(input.rotation); @@ -48,16 +47,8 @@ fn vs_main(input: VertexInput) -> VertexOutput { vec4(0.0, 0.0, 0.0, 1.0) ); - // Scale the image and then translate to the final position by moving the center to the position - let scale_translate = mat4x4( - vec4(input.scale.x, 0.0, 0.0, 0.0), - vec4(0.0, input.scale.y, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(input.center, 0.0, 1.0) - ); - // Calculate the final position of the vertex - out.position = globals.transform * scale_translate * rotate * vec4(v_pos, 0.0, 1.0); + out.position = globals.transform * (vec4(input.center, 0.0, 0.0) + rotate * vec4(v_pos, 0.0, 1.0)); return out; } diff --git a/widget/src/image.rs b/widget/src/image.rs index 3a0a5e53..45209a91 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,6 +1,5 @@ //! Display images in your user interface. pub mod viewer; -use iced_renderer::core::{Point, RotationLayout}; pub use viewer::Viewer; use crate::core::image; @@ -9,7 +8,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -37,8 +37,7 @@ pub struct Image { height: Length, content_fit: ContentFit, filter_method: FilterMethod, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, } impl Image { @@ -48,10 +47,9 @@ impl Image { handle: handle.into(), width: Length::Shrink, height: Length::Shrink, - content_fit: ContentFit::Contain, + content_fit: ContentFit::default(), filter_method: FilterMethod::default(), - rotation: 0.0, - rotation_layout: RotationLayout::Change, + rotation: Rotation::default(), } } @@ -81,15 +79,9 @@ impl Image { self } - /// Rotates the [`Image`] by the given angle in radians. - pub fn rotation(mut self, degrees: f32) -> Self { - self.rotation = degrees; - self - } - - /// Sets the [`RotationLayout`] of the [`Image`]. - pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { - self.rotation_layout = rotation_layout; + /// Applies the given [`Rotation`] to the [`Image`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); self } } @@ -102,8 +94,7 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, ) -> layout::Node where Renderer: image::Renderer, @@ -114,7 +105,7 @@ where Size::new(image_size.width as f32, image_size.height as f32); // The rotated size of the image - let rotated_size = rotation_layout.apply_to_size(image_size, rotation); + let rotated_size = rotation.apply(image_size); // The size to be available to the widget prior to `Shrink`ing let raw_size = limits.resolve(width, height, rotated_size); @@ -144,45 +135,44 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, ) where Renderer: image::Renderer, Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); - let rotated_size = rotation_layout.apply_to_size(image_size, rotation); + let rotated_size = rotation.apply(image_size); let bounds = layout.bounds(); let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); - let scale = Size::new( + + let scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); - let render = |renderer: &mut Renderer| { - let position = match content_fit { - ContentFit::None => Point::new( - bounds.position().x - + (rotated_size.width - image_size.width) / 2.0, - bounds.position().y - + (rotated_size.height - image_size.height) / 2.0, - ), - _ => Point::new( - bounds.center_x() - image_size.width / 2.0, - bounds.center_y() - image_size.height / 2.0, - ), - }; + let final_size = image_size * scale; + + let position = match content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; - let drawing_bounds = Rectangle::new(position, image_size); + let drawing_bounds = Rectangle::new(position, final_size); + let render = |renderer: &mut Renderer| { renderer.draw_image( handle.clone(), filter_method, drawing_bounds, - rotation, - scale, + rotation.radians(), ); }; @@ -221,7 +211,6 @@ where self.height, self.content_fit, self.rotation, - self.rotation_layout, ) } @@ -242,7 +231,6 @@ where self.content_fit, self.filter_method, self.rotation, - self.rotation_layout, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index ccdfdebb..ee4c0fba 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,8 +6,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle, + Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. @@ -341,8 +341,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, - 0.0, - Size::UNIT, + Radians(0.0), ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 21946af8..c1fccba1 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,8 +5,8 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - Color, ContentFit, Element, Layout, Length, Point, Rectangle, - RotationLayout, Size, Theme, Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, + Size, Theme, Vector, Widget, }; use std::path::PathBuf; @@ -29,8 +29,7 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, } impl<'a, Theme> Svg<'a, Theme> @@ -45,8 +44,7 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), - rotation: 0.0, - rotation_layout: RotationLayout::Change, + rotation: Rotation::default(), } } @@ -100,15 +98,9 @@ where self } - /// Rotates the [`Svg`] by the given angle in radians. - pub fn rotation(mut self, degrees: f32) -> Self { - self.rotation = degrees; - self - } - - /// Sets the [`RotationLayout`] of the [`Svg`]. - pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { - self.rotation_layout = rotation_layout; + /// Applies the given [`Rotation`] to the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); self } } @@ -137,9 +129,7 @@ where let image_size = Size::new(width as f32, height as f32); // The rotated size of the svg - let rotated_size = self - .rotation_layout - .apply_to_size(image_size, self.rotation); + let rotated_size = self.rotation.apply(image_size); // The size to be available to the widget prior to `Shrink`ing let raw_size = limits.resolve(self.width, self.height, rotated_size); @@ -174,49 +164,46 @@ where ) { let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); - let rotated_size = self - .rotation_layout - .apply_to_size(image_size, self.rotation); + let rotated_size = self.rotation.apply(image_size); let bounds = layout.bounds(); let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); - let scale = Size::new( + let scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); + let final_size = image_size * scale; + + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; + + let drawing_bounds = Rectangle::new(position, final_size); + let is_mouse_over = cursor.is_over(bounds); - let render = |renderer: &mut Renderer| { - let position = match self.content_fit { - ContentFit::None => Point::new( - bounds.position().x - + (rotated_size.width - image_size.width) / 2.0, - bounds.position().y - + (rotated_size.height - image_size.height) / 2.0, - ), - _ => Point::new( - bounds.center_x() - image_size.width / 2.0, - bounds.center_y() - image_size.height / 2.0, - ), - }; - - let drawing_bounds = Rectangle::new(position, image_size); - - let status = if is_mouse_over { - Status::Hovered - } else { - Status::Idle - }; - - let style = theme.style(&self.class, status); + let status = if is_mouse_over { + Status::Hovered + } else { + Status::Idle + }; + let style = theme.style(&self.class, status); + + let render = |renderer: &mut Renderer| { renderer.draw_svg( self.handle.clone(), style.color, drawing_bounds, - self.rotation, - scale, + self.rotation.radians(), ); }; -- cgit From 610394b6957d9424aec1c50d927e34a0fb3fe5fd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 15:28:46 +0200 Subject: Rename `global_scale` to `scale` in `wgpu::image` --- wgpu/src/image/mod.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 3ec341fc..285eb2f6 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -212,10 +212,9 @@ impl Pipeline { belt: &mut wgpu::util::StagingBelt, images: &Batch, transformation: Transformation, - global_scale: f32, + scale: f32, ) { - let transformation = - transformation * Transformation::scale(global_scale); + let transformation = transformation * Transformation::scale(scale); let nearest_instances: &mut Vec = &mut Vec::new(); let linear_instances: &mut Vec = &mut Vec::new(); @@ -263,12 +262,7 @@ impl Pipeline { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = cache.upload_vector( - device, - encoder, - handle, - *color, - size, - global_scale, + device, encoder, handle, *color, size, scale, ) { add_instances( [bounds.x, bounds.y], -- cgit From efc55b655bfce98fc32e698cf3c2007e27be941a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 17:14:20 +0200 Subject: Create `ferris` example to showcase `ContentFit` and `Rotation` --- core/src/angle.rs | 43 ++++++++++++ core/src/content_fit.rs | 14 ++++ core/src/rotation.rs | 7 +- examples/ferris/Cargo.toml | 10 +++ examples/ferris/src/main.rs | 161 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 examples/ferris/Cargo.toml create mode 100644 examples/ferris/src/main.rs diff --git a/core/src/angle.rs b/core/src/angle.rs index 69630717..8322273c 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -7,6 +7,11 @@ use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Sub, SubAssign}; #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Degrees(pub f32); +impl Degrees { + /// The range of degrees of a circle. + pub const RANGE: RangeInclusive = Self(0.0)..=Self(360.0); +} + impl PartialEq for Degrees { fn eq(&self, other: &f32) -> bool { self.0.eq(other) @@ -19,6 +24,44 @@ impl PartialOrd for Degrees { } } +impl From for Degrees { + fn from(degrees: f32) -> Self { + Self(degrees) + } +} + +impl From for Degrees { + fn from(degrees: u8) -> Self { + Self(f32::from(degrees)) + } +} + +impl From for f32 { + fn from(degrees: Degrees) -> Self { + degrees.0 + } +} + +impl From for f64 { + fn from(degrees: Degrees) -> Self { + Self::from(degrees.0) + } +} + +impl num_traits::FromPrimitive for Degrees { + fn from_i64(n: i64) -> Option { + Some(Self(n as f32)) + } + + fn from_u64(n: u64) -> Option { + Some(Self(n as f32)) + } + + fn from_f64(n: f64) -> Option { + Some(Self(n as f32)) + } +} + /// Radians #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Radians(pub f32); diff --git a/core/src/content_fit.rs b/core/src/content_fit.rs index 56d2ffa6..19642716 100644 --- a/core/src/content_fit.rs +++ b/core/src/content_fit.rs @@ -1,6 +1,8 @@ //! Control the fit of some content (like an image) within a space. use crate::Size; +use std::fmt; + /// The strategy used to fit the contents of a widget to its bounding box. /// /// Each variant of this enum is a strategy that can be applied for resolving @@ -118,3 +120,15 @@ impl ContentFit { } } } + +impl fmt::Display for ContentFit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + ContentFit::Contain => "Contain", + ContentFit::Cover => "Cover", + ContentFit::Fill => "Fill", + ContentFit::None => "None", + ContentFit::ScaleDown => "Scale Down", + }) + } +} diff --git a/core/src/rotation.rs b/core/src/rotation.rs index ebb85f3c..f36ef089 100644 --- a/core/src/rotation.rs +++ b/core/src/rotation.rs @@ -1,5 +1,5 @@ //! Control the rotation of some content (like an image) within a space. -use crate::{Radians, Size}; +use crate::{Degrees, Radians, Size}; /// The strategy used to rotate the content. /// @@ -31,6 +31,11 @@ impl Rotation { } } + /// Returns the angle of the [`Rotation`] in [`Degrees`]. + pub fn degrees(self) -> Degrees { + Degrees(self.radians().0.to_degrees()) + } + /// Rotates the given [`Size`]. pub fn apply(self, size: Size) -> Size { match self { diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml new file mode 100644 index 00000000..c9fb8c13 --- /dev/null +++ b/examples/ferris/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ferris" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["image"] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs new file mode 100644 index 00000000..d846f560 --- /dev/null +++ b/examples/ferris/src/main.rs @@ -0,0 +1,161 @@ +use iced::widget::{column, container, image, pick_list, row, slider, text}; +use iced::{ + Alignment, Color, ContentFit, Degrees, Element, Length, Rotation, Theme, +}; + +pub fn main() -> iced::Result { + iced::program("Ferris - Iced", Image::update, Image::view) + .theme(|_| Theme::TokyoNight) + .run() +} + +struct Image { + width: f32, + rotation: Rotation, + content_fit: ContentFit, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + WidthChanged(f32), + RotationStrategyChanged(RotationStrategy), + RotationChanged(Degrees), + ContentFitChanged(ContentFit), +} + +impl Image { + fn update(&mut self, message: Message) { + match message { + Message::WidthChanged(width) => { + self.width = width; + } + Message::RotationStrategyChanged(strategy) => { + self.rotation = match strategy { + RotationStrategy::Floating => { + Rotation::Floating(self.rotation.radians()) + } + RotationStrategy::Solid => { + Rotation::Solid(self.rotation.radians()) + } + }; + } + Message::RotationChanged(rotation) => { + self.rotation = match self.rotation { + Rotation::Floating(_) => { + Rotation::Floating(rotation.into()) + } + Rotation::Solid(_) => Rotation::Solid(rotation.into()), + }; + } + Message::ContentFitChanged(content_fit) => { + self.content_fit = content_fit; + } + } + } + + fn view(&self) -> Element { + let i_am_ferris = container( + column![ + "Hello!", + Element::from( + image(format!( + "{}/../tour/images/ferris.png", + env!("CARGO_MANIFEST_DIR") + )) + .width(self.width) + .content_fit(self.content_fit) + .rotation(self.rotation) + ) + .explain(Color::WHITE), + "I am Ferris!" + ] + .spacing(20) + .align_items(Alignment::Center), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y(); + + let sizing = row![ + pick_list( + [ + ContentFit::Contain, + ContentFit::Cover, + ContentFit::Fill, + ContentFit::None, + ContentFit::ScaleDown + ], + Some(self.content_fit), + Message::ContentFitChanged + ) + .width(Length::Fill), + column![ + slider(100.0..=500.0, self.width, Message::WidthChanged), + text(format!("Width: {}px", self.width)) + .size(14) + .line_height(1.0) + ] + .spacing(5) + .align_items(Alignment::Center) + ] + .spacing(10); + + let rotation = row![ + pick_list( + [RotationStrategy::Floating, RotationStrategy::Solid], + Some(match self.rotation { + Rotation::Floating(_) => RotationStrategy::Floating, + Rotation::Solid(_) => RotationStrategy::Solid, + }), + Message::RotationStrategyChanged, + ) + .width(Length::Fill), + column![ + slider( + Degrees::RANGE, + self.rotation.degrees(), + Message::RotationChanged + ), + text(format!( + "Rotation: {:.0}°", + f32::from(self.rotation.degrees()) + )) + .size(14) + .line_height(1.0) + ] + .spacing(5) + .align_items(Alignment::Center) + ] + .spacing(10); + + container(column![i_am_ferris, sizing, rotation].spacing(10)) + .padding(10) + .into() + } +} + +impl Default for Image { + fn default() -> Self { + Self { + width: 300.0, + rotation: Rotation::default(), + content_fit: ContentFit::default(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RotationStrategy { + Floating, + Solid, +} + +impl std::fmt::Display for RotationStrategy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Floating => "Floating", + Self::Solid => "Solid", + }) + } +} -- cgit From 568ac66486937a294f2a79cefea277e4eb46b81e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 17:15:26 +0200 Subject: Remove commented code in `tiny_skia::layer` --- tiny_skia/src/layer.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index c8a31ba3..c907c93c 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -260,22 +260,6 @@ impl Layer { Image::eq, ); - // let center = bounds.center(); - // let rotated_size = RotationLayout::Change - // .apply_to_size(bounds.size(), *rotation); - // - // let scaled_size = Size::new( - // rotated_size.width * scale.width, - // rotated_size.height * scale.height, - // ); - // - // let top_left = Point::new( - // center.x - scaled_size.width / 2.0, - // center.y - scaled_size.height / 2.0, - // ); - // - // Rectangle::new(top_left, scaled_size).expand(1.0) - damage.extend(text); damage.extend(primitives); damage.extend(images); -- cgit From eac5bcb64f17dfbb52b64ea4f95693462986bb69 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 May 2024 07:04:57 +0200 Subject: Fix `Image::bounds` when rotation present in `iced_graphics` --- core/src/rectangle.rs | 16 ++++++++++++++-- core/src/rotation.rs | 14 +++----------- core/src/size.rs | 15 ++++++++++++++- examples/ferris/Cargo.toml | 2 +- graphics/src/image.rs | 7 +++++-- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index fb66131a..1556e072 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -1,6 +1,6 @@ -use crate::{Point, Size, Vector}; +use crate::{Point, Radians, Size, Vector}; -/// A rectangle. +/// An axis-aligned rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Rectangle { /// X coordinate of the top-left corner. @@ -172,6 +172,18 @@ impl Rectangle { height: self.height + amount * 2.0, } } + + /// Rotates the [`Rectangle`] and returns the smallest [`Rectangle`] + /// containing it. + pub fn rotate(self, rotation: Radians) -> Self { + let size = self.size().rotate(rotation); + let position = Point::new( + self.center_x() - size.width / 2.0, + self.center_y() - size.height / 2.0, + ); + + Self::new(position, size) + } } impl std::ops::Mul for Rectangle { diff --git a/core/src/rotation.rs b/core/src/rotation.rs index f36ef089..00a8c302 100644 --- a/core/src/rotation.rs +++ b/core/src/rotation.rs @@ -36,20 +36,12 @@ impl Rotation { Degrees(self.radians().0.to_degrees()) } - /// Rotates the given [`Size`]. + /// Applies the [`Rotation`] to the given [`Size`], returning + /// the minimum [`Size`] containing the rotated one. pub fn apply(self, size: Size) -> Size { match self { Self::Floating(_) => size, - Self::Solid(rotation) => { - let radians = f32::from(rotation); - - Size { - width: (size.width * radians.cos()).abs() - + (size.height * radians.sin()).abs(), - height: (size.width * radians.sin()).abs() - + (size.height * radians.cos()).abs(), - } - } + Self::Solid(rotation) => size.rotate(rotation), } } } diff --git a/core/src/size.rs b/core/src/size.rs index 66be2d85..d7459355 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,4 +1,4 @@ -use crate::Vector; +use crate::{Radians, Vector}; /// An amount of space in 2 dimensions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] @@ -51,6 +51,19 @@ impl Size { height: self.height + other.height, } } + + /// Rotates the given [`Size`] and returns the minimum [`Size`] + /// containing it. + pub fn rotate(self, rotation: Radians) -> Size { + let radians = f32::from(rotation); + + Size { + width: (self.width * radians.cos()).abs() + + (self.height * radians.sin()).abs(), + height: (self.width * radians.sin()).abs() + + (self.height * radians.cos()).abs(), + } + } } impl From<[T; 2]> for Size { diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml index c9fb8c13..0d91749b 100644 --- a/examples/ferris/Cargo.toml +++ b/examples/ferris/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image"] +iced.features = ["image", "debug"] diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 4fd6998d..9d09bf4c 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -41,9 +41,12 @@ impl Image { /// Returns the bounds of the [`Image`]. pub fn bounds(&self) -> Rectangle { match self { - Image::Raster { bounds, .. } | Image::Vector { bounds, .. } => { - *bounds + Image::Raster { + bounds, rotation, .. } + | Image::Vector { + bounds, rotation, .. + } => bounds.rotate(*rotation), } } } -- cgit From 4010e3983d40e24a5d7b590d8fec9801651199ce Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 May 2024 07:23:55 +0200 Subject: Add `spin` mode to `ferris` example :crab: --- core/src/angle.rs | 26 ++++++++++++++++- core/src/rotation.rs | 7 +++++ examples/ferris/Cargo.toml | 2 +- examples/ferris/src/main.rs | 68 +++++++++++++++++++++++++++++++++++---------- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/core/src/angle.rs b/core/src/angle.rs index 8322273c..9c8a9b24 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -1,7 +1,7 @@ use crate::{Point, Rectangle, Vector}; use std::f32::consts::{FRAC_PI_2, PI}; -use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Sub, SubAssign}; +use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Rem, Sub, SubAssign}; /// Degrees #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] @@ -48,6 +48,14 @@ impl From for f64 { } } +impl Mul for Degrees { + type Output = Degrees; + + fn mul(self, rhs: f32) -> Self::Output { + Self(self.0 * rhs) + } +} + impl num_traits::FromPrimitive for Degrees { fn from_i64(n: i64) -> Option { Some(Self(n as f32)) @@ -156,6 +164,14 @@ impl Add for Radians { } } +impl Add for Radians { + type Output = Self; + + fn add(self, rhs: Degrees) -> Self::Output { + Self(self.0 + rhs.0.to_radians()) + } +} + impl AddAssign for Radians { fn add_assign(&mut self, rhs: Radians) { self.0 = self.0 + rhs.0; @@ -202,6 +218,14 @@ impl Div for Radians { } } +impl Rem for Radians { + type Output = Self; + + fn rem(self, rhs: Self) -> Self::Output { + Self(self.0 % rhs.0) + } +} + impl PartialEq for Radians { fn eq(&self, other: &f32) -> bool { self.0.eq(other) diff --git a/core/src/rotation.rs b/core/src/rotation.rs index 00a8c302..afa8d79e 100644 --- a/core/src/rotation.rs +++ b/core/src/rotation.rs @@ -31,6 +31,13 @@ impl Rotation { } } + /// Returns a mutable reference to the angle of the [`Rotation`] in [`Radians`]. + pub fn radians_mut(&mut self) -> &mut Radians { + match self { + Rotation::Floating(radians) | Rotation::Solid(radians) => radians, + } + } + /// Returns the angle of the [`Rotation`] in [`Degrees`]. pub fn degrees(self) -> Degrees { Degrees(self.radians().0.to_degrees()) diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml index 0d91749b..e98341d2 100644 --- a/examples/ferris/Cargo.toml +++ b/examples/ferris/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image", "debug"] +iced.features = ["image", "tokio", "debug"] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index d846f560..b7536740 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -1,10 +1,16 @@ -use iced::widget::{column, container, image, pick_list, row, slider, text}; +use iced::time::Instant; +use iced::widget::{ + checkbox, column, container, image, pick_list, row, slider, text, +}; +use iced::window; use iced::{ - Alignment, Color, ContentFit, Degrees, Element, Length, Rotation, Theme, + Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation, + Subscription, Theme, }; pub fn main() -> iced::Result { iced::program("Ferris - Iced", Image::update, Image::view) + .subscription(Image::subscription) .theme(|_| Theme::TokyoNight) .run() } @@ -13,6 +19,8 @@ struct Image { width: f32, rotation: Rotation, content_fit: ContentFit, + spin: bool, + last_tick: Instant, } #[derive(Debug, Clone, Copy)] @@ -21,6 +29,8 @@ enum Message { RotationStrategyChanged(RotationStrategy), RotationChanged(Degrees), ContentFitChanged(ContentFit), + SpinToggled(bool), + RedrawRequested(Instant), } impl Image { @@ -50,6 +60,29 @@ impl Image { Message::ContentFitChanged(content_fit) => { self.content_fit = content_fit; } + Message::SpinToggled(spin) => { + self.spin = spin; + self.last_tick = Instant::now(); + } + Message::RedrawRequested(now) => { + const ROTATION_SPEED: Degrees = Degrees(360.0); + + let delta = (now - self.last_tick).as_millis() as f32 / 1_000.0; + + *self.rotation.radians_mut() = (self.rotation.radians() + + ROTATION_SPEED * delta) + % (2.0 * Radians::PI); + + self.last_tick = now; + } + } + } + + fn subscription(&self) -> Subscription { + if self.spin { + window::frames().map(Message::RedrawRequested) + } else { + Subscription::none() } } @@ -111,18 +144,23 @@ impl Image { Message::RotationStrategyChanged, ) .width(Length::Fill), - column![ - slider( - Degrees::RANGE, - self.rotation.degrees(), - Message::RotationChanged - ), - text(format!( - "Rotation: {:.0}°", - f32::from(self.rotation.degrees()) - )) - .size(14) - .line_height(1.0) + row![ + column![ + slider( + Degrees::RANGE, + self.rotation.degrees(), + Message::RotationChanged + ), + text(format!( + "Rotation: {:.0}°", + f32::from(self.rotation.degrees()) + )) + .size(14) + .line_height(1.0) + ] + .spacing(5) + .align_items(Alignment::Center), + checkbox("Spin!", self.spin).on_toggle(Message::SpinToggled) ] .spacing(5) .align_items(Alignment::Center) @@ -141,6 +179,8 @@ impl Default for Image { width: 300.0, rotation: Rotation::default(), content_fit: ContentFit::default(), + spin: false, + last_tick: Instant::now(), } } } -- cgit