diff --git a/Cargo.toml b/Cargo.toml index 3e287e292..3bfe2f466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,11 +82,12 @@ tiny-skia-path = {version = "0.11.0"} unicode-bidi = "0.3.7" unicode-script = "0.5.4" unicode-segmentation = "1.9.0" -usvg = {version = "0.33.0", default-features = false} +usvg = { version= "0.35.0", default-features = false } webbrowser = "0.8.8" wgpu = {version = "0.16.0"} winit = {version = "0.28.5", default-features = false, features = ["x11", "wayland", "wayland-dlopen"]} zerocopy = "0.7.3" +quick-xml = "0.30.0" [workspace.metadata.release] shared-version = true diff --git a/dev-helper/src/painter_backend_eq_image_test.rs b/dev-helper/src/painter_backend_eq_image_test.rs index 06622d073..495321fed 100644 --- a/dev-helper/src/painter_backend_eq_image_test.rs +++ b/dev-helper/src/painter_backend_eq_image_test.rs @@ -86,7 +86,7 @@ pub fn assert_texture_eq_png(img: PixelImage, file_path: &std::path::Path) { ) .unwrap(); - const TOLERANCE: f64 = 0.0000025; + const TOLERANCE: f64 = 0.000008; let (v, _) = dssim.compare(&expected, dissim_mig); let v: f64 = v.into(); diff --git a/gpu/Cargo.toml b/gpu/Cargo.toml index c4eff7bce..876b57153 100644 --- a/gpu/Cargo.toml +++ b/gpu/Cargo.toml @@ -23,7 +23,7 @@ ribir_geom = {path = "../geom", version = "0.0.1-alpha.4" } ribir_painter = {path = "../painter", features = ["tessellation"], version = "0.0.1-alpha.4" } slab = "0.4.8" wgpu = {workspace = true, optional = true} -zerocopy.workspace = true +zerocopy = {workspace=true, features = ["derive"]} [dev-dependencies] paste.workspace = true diff --git a/gpu/src/gpu_backend.rs b/gpu/src/gpu_backend.rs index 1dd406889..3ef379e19 100644 --- a/gpu/src/gpu_backend.rs +++ b/gpu/src/gpu_backend.rs @@ -1,5 +1,9 @@ use self::textures_mgr::{TextureID, TexturesMgr}; -use crate::{ColorAttr, GPUBackendImpl, ImgPrimitive, MaskLayer}; +use crate::{ + ColorAttr, GPUBackendImpl, GradientStopPrimitive, ImagePrimIndex, ImgPrimitive, + LinearGradientPrimIndex, LinearGradientPrimitive, MaskLayer, RadialGradientPrimIndex, + RadialGradientPrimitive, +}; use ribir_geom::{rect_corners, DeviceRect, DeviceSize, Point}; use ribir_painter::{ image::ColorFormat, AntiAliasing, Color, PaintCommand, PaintPath, PainterBackend, PixelImage, @@ -8,6 +12,7 @@ use ribir_painter::{ use std::{error::Error, future::Future, ops::Range, pin::Pin}; mod atlas; + mod textures_mgr; use textures_mgr::*; @@ -15,8 +20,14 @@ pub struct GPUBackend { gpu_impl: Impl, tex_mgr: TexturesMgr, color_vertices_buffer: VertexBuffers, - img_vertices_buffer: VertexBuffers, + img_vertices_buffer: VertexBuffers, img_prims: Vec, + radial_gradient_vertices_buffer: VertexBuffers, + radial_gradient_stops: Vec, + radial_gradient_prims: Vec, + linear_gradient_prims: Vec, + linear_gradient_stops: Vec, + linear_gradient_vertices_buffer: VertexBuffers, draw_indices: Vec, tex_ids_map: TextureIdxMap, viewport: DeviceRect, @@ -29,7 +40,8 @@ pub struct GPUBackend { enum DrawIndices { Color(Range), Img(Range), - _Gradient(Range), + RadialGradient(Range), + LinearGradient(Range), } struct ClipLayer { @@ -124,6 +136,28 @@ where self.gpu_impl.load_img_primitives(&self.img_prims); self.gpu_impl.load_img_vertices(&self.img_vertices_buffer); } + if !self.radial_gradient_vertices_buffer.indices.is_empty() { + self + .gpu_impl + .load_radial_gradient_primitives(&self.radial_gradient_prims); + self + .gpu_impl + .load_radial_gradient_stops(&self.radial_gradient_stops); + self + .gpu_impl + .load_radial_gradient_vertices(&self.radial_gradient_vertices_buffer); + } + if !self.linear_gradient_vertices_buffer.indices.is_empty() { + self + .gpu_impl + .load_linear_gradient_primitives(&self.linear_gradient_prims); + self + .gpu_impl + .load_linear_gradient_stops(&self.linear_gradient_stops); + self + .gpu_impl + .load_linear_gradient_vertices(&self.linear_gradient_vertices_buffer); + } self.tex_mgr.submit(&mut self.gpu_impl); self.layers_submit(output, surface); @@ -149,6 +183,12 @@ where skip_clip_cnt: 0, color_vertices_buffer: VertexBuffers::with_capacity(256, 512), img_vertices_buffer: VertexBuffers::with_capacity(256, 512), + radial_gradient_vertices_buffer: VertexBuffers::with_capacity(256, 512), + radial_gradient_prims: vec![], + radial_gradient_stops: vec![], + linear_gradient_vertices_buffer: VertexBuffers::with_capacity(256, 512), + linear_gradient_stops: vec![], + linear_gradient_prims: vec![], img_prims: vec![], draw_indices: vec![], viewport: DeviceRect::zero(), @@ -193,7 +233,70 @@ where }; self.img_prims.push(prim); let buffer = &mut self.img_vertices_buffer; - add_draw_rect_vertices(rect, output_tex_size, prim_idx, buffer); + add_draw_rect_vertices(rect, output_tex_size, ImagePrimIndex(prim_idx), buffer); + } + } + PaintCommand::RadialGradient { path, radial_gradient } => { + let ts = path.transform; + if let Some((rect, mask_head)) = self.new_mask_layer(path) { + self.update_to_radial_gradient_indices(); + let prim: RadialGradientPrimitive = RadialGradientPrimitive { + transform: ts.inverse().unwrap().to_array(), + stop_start: self.radial_gradient_stops.len() as u32, + stop_cnt: radial_gradient.stops.len() as u32, + start_center: radial_gradient.start_center.to_array(), + start_radius: radial_gradient.start_radius, + end_center: radial_gradient.end_center.to_array(), + end_radius: radial_gradient.end_radius, + mask_head, + spread: radial_gradient.spread_method as u32, + }; + self.radial_gradient_stops.extend( + radial_gradient + .stops + .into_iter() + .map(Into::::into), + ); + let prim_idx = self.radial_gradient_prims.len() as u32; + self.radial_gradient_prims.push(prim); + let buffer = &mut self.radial_gradient_vertices_buffer; + + add_draw_rect_vertices( + rect, + output_tex_size, + RadialGradientPrimIndex(prim_idx), + buffer, + ); + } + } + PaintCommand::LinearGradient { path, linear_gradient } => { + let ts = path.transform; + if let Some((rect, mask_head)) = self.new_mask_layer(path) { + self.update_to_linear_gradient_indices(); + let prim: LinearGradientPrimitive = LinearGradientPrimitive { + transform: ts.inverse().unwrap().to_array(), + stop_start: self.linear_gradient_stops.len() as u32, + stop_cnt: linear_gradient.stops.len() as u32, + start_position: linear_gradient.start.to_array(), + end_position: linear_gradient.end.to_array(), + mask_head, + spread: linear_gradient.spread_method as u32, + }; + self.linear_gradient_stops.extend( + linear_gradient + .stops + .into_iter() + .map(Into::::into), + ); + let prim_idx = self.linear_gradient_prims.len() as u32; + self.linear_gradient_prims.push(prim); + let buffer = &mut self.linear_gradient_vertices_buffer; + add_draw_rect_vertices( + rect, + output_tex_size, + LinearGradientPrimIndex(prim_idx), + buffer, + ); } } PaintCommand::Clip(path) => { @@ -235,6 +338,13 @@ where self.img_vertices_buffer.indices.clear(); self.img_prims.clear(); self.mask_layers.clear(); + self.radial_gradient_vertices_buffer.indices.clear(); + self.radial_gradient_vertices_buffer.vertices.clear(); + self.radial_gradient_prims.clear(); + self.radial_gradient_stops.clear(); + self.linear_gradient_prims.clear(); + self.linear_gradient_vertices_buffer.indices.clear(); + self.linear_gradient_stops.clear(); } fn update_to_color_indices(&mut self) { @@ -253,12 +363,43 @@ where } } + fn update_to_radial_gradient_indices(&mut self) { + if !matches!( + self.draw_indices.last(), + Some(DrawIndices::RadialGradient(_)) + ) { + self.expand_indices_range(); + let start = self.radial_gradient_vertices_buffer.indices.len() as u32; + self + .draw_indices + .push(DrawIndices::RadialGradient(start..start)); + } + } + + fn update_to_linear_gradient_indices(&mut self) { + if !matches!( + self.draw_indices.last(), + Some(DrawIndices::LinearGradient(_)) + ) { + self.expand_indices_range(); + let start = self.linear_gradient_vertices_buffer.indices.len() as u32; + self + .draw_indices + .push(DrawIndices::LinearGradient(start..start)); + } + } + fn expand_indices_range(&mut self) -> Option<&DrawIndices> { let cmd = self.draw_indices.last_mut()?; match cmd { DrawIndices::Color(rg) => rg.end = self.color_vertices_buffer.indices.len() as u32, DrawIndices::Img(rg) => rg.end = self.img_vertices_buffer.indices.len() as u32, - DrawIndices::_Gradient(_) => todo!(), + DrawIndices::RadialGradient(rg) => { + rg.end = self.radial_gradient_vertices_buffer.indices.len() as u32 + } + DrawIndices::LinearGradient(rg) => { + rg.end = self.linear_gradient_vertices_buffer.indices.len() as u32 + } }; Some(&*cmd) @@ -322,7 +463,16 @@ where .for_each(|indices| match indices { DrawIndices::Color(rg) => self.gpu_impl.draw_color_triangles(output, rg, color.take()), DrawIndices::Img(rg) => self.gpu_impl.draw_img_triangles(output, rg, color.take()), - DrawIndices::_Gradient(_) => todo!(), + DrawIndices::RadialGradient(rg) => { + self + .gpu_impl + .draw_radial_gradient_triangles(output, rg, color.take()) + } + DrawIndices::LinearGradient(rg) => { + self + .gpu_impl + .draw_linear_gradient_triangles(output, rg, color.take()) + } }); } } @@ -381,7 +531,7 @@ mod tests { use ribir_algo::ShareResource; use ribir_dev_helper::*; use ribir_geom::*; - use ribir_painter::{Brush, Color, Painter, Path, PixelImage}; + use ribir_painter::{Brush, Color, Painter, Path, PixelImage, Svg}; fn painter(bounds: Size) -> Painter { Painter::new(Rect::from_size(bounds)) } @@ -518,4 +668,14 @@ mod tests { painter } + + painter_backend_eq_image_test!(draw_svg_gradient); + fn draw_svg_gradient() -> Painter { + let mut painter = painter(Size::new(64., 64.)); + let svg = + Svg::parse_from_bytes(include_bytes!("../../tests/assets/fill_with_gradient.svg")).unwrap(); + + painter.draw_svg(&svg); + painter + } } diff --git a/gpu/src/gpu_backend/textures_mgr.rs b/gpu/src/gpu_backend/textures_mgr.rs index d4ee365d0..3e1e95137 100644 --- a/gpu/src/gpu_backend/textures_mgr.rs +++ b/gpu/src/gpu_backend/textures_mgr.rs @@ -59,6 +59,11 @@ macro_rules! id_to_texture { }; } +fn get_transform_pref_scale(transform: &Transform) -> f32 { + let Transform { m11, m12, m21, m22, .. } = *transform; + (m11.abs() + m12.abs()).max(m21.abs() + m22.abs()) +} + impl TexturesMgr where T::Host: GPUBackendImpl, @@ -109,7 +114,7 @@ where .then(path_ts) } - let prefer_scale: f32 = transform.m11.abs().max(transform.m22.abs()); + let prefer_scale: f32 = get_transform_pref_scale(transform); let key = PathKey::from_path(path); if let Some(h) = self @@ -118,13 +123,14 @@ where .filter(|h| h.attr >= prefer_scale) .copied() { - let slice = alpha_tex_slice(&self.alpha_atlas, &h).cut_blank_edge(); - let matrix = cache_to_view_matrix(key.path(), transform, slice.rect.origin, h.attr); - (slice, matrix) + let mask_slice = alpha_tex_slice(&self.alpha_atlas, &h).cut_blank_edge(); + let matrix = cache_to_view_matrix(key.path(), transform, mask_slice.rect.origin, h.attr); + (mask_slice.expand_for_paste(), matrix) } else { let path = key.path().clone(); let scale_bounds = path.bounds().scale(prefer_scale, prefer_scale); let prefer_cache_size = path_add_edges(scale_bounds.round_out().size.to_i32().cast_unit()); + let h = self .alpha_atlas .allocate(key, prefer_scale, prefer_cache_size, gpu_impl); @@ -141,7 +147,7 @@ where .fill_task .push(FillTask { slice, path, ts, clip_rect: None }); - (mask_slice, matrix) + (mask_slice.expand_for_paste(), matrix) } } @@ -174,7 +180,10 @@ where }; let offset = (clip_view.origin - slice.rect.origin).to_f32(); - (slice, Transform::translation(offset.x, offset.y)) + ( + slice.expand_for_paste(), + Transform::translation(offset.x, offset.y), + ) } pub(super) fn store_image( @@ -217,7 +226,7 @@ where let tex_width = tex_size.width as f32; let tex_height = tex_size.height as f32; - let scale = ts.m11.max(ts.m22); + let scale = get_transform_pref_scale(ts); path.tessellate(TOLERANCE / scale, buffer, |pos| { let pos = ts.transform_point(pos); @@ -390,6 +399,13 @@ impl TextureSlice { self.rect = self.rect.inner_rect(blank_side); self } + + pub fn expand_for_paste(mut self) -> TextureSlice { + const EXPANDED_EDGE: i32 = 1; + let blank_side = SideOffsets2D::new_all_same(EXPANDED_EDGE); + self.rect = self.rect.outer_rect(blank_side); + self + } } #[derive(Debug, Clone)] @@ -520,7 +536,7 @@ impl PartialEq for PathKey { impl Eq for PathKey {} pub fn prefer_cache_size(path: &Path, transform: &Transform) -> DeviceSize { - let prefer_scale: f32 = transform.m11.max(transform.m22); + let prefer_scale: f32 = get_transform_pref_scale(transform); let prefer_cache_size = path .bounds() .scale(prefer_scale, prefer_scale) @@ -633,7 +649,7 @@ pub mod tests { let (slice2, ts2) = mgr.store_clipped_path(clip_view, path, &mut wgpu); assert_eq!(slice1, slice2); assert_eq!(ts1, ts2); - assert_eq!(slice1.rect, ribir_geom::rect(2, 2, 100, 100)); + assert_eq!(slice1.rect, ribir_geom::rect(1, 1, 102, 102)); assert_eq!(ts1, Transform::new(1., 0., 0., 1., 8., 8.)); } diff --git a/gpu/src/lib.rs b/gpu/src/lib.rs index b7388324e..25ef5c13a 100644 --- a/gpu/src/lib.rs +++ b/gpu/src/lib.rs @@ -1,7 +1,7 @@ pub mod error; pub use gpu_backend::Texture; use ribir_geom::{DevicePoint, DeviceRect, DeviceSize}; -use ribir_painter::{image::ColorFormat, AntiAliasing, Color, VertexBuffers}; +use ribir_painter::{image::ColorFormat, AntiAliasing, Color, GradientStop, VertexBuffers}; use std::ops::Range; mod gpu_backend; use zerocopy::AsBytes; @@ -87,6 +87,7 @@ pub trait GPUBackendImpl { texture: &mut Self::Texture, scissor: DeviceRect, ); + /// load textures that will be use in this draw phase fn load_textures(&mut self, textures: &[&Self::Texture]); /// load the mask layers that the current draw phase will use, called at @@ -98,7 +99,25 @@ pub trait GPUBackendImpl { /// Load the vertices and indices buffer that `draw_img_triangles` will use. fn load_img_primitives(&mut self, primitives: &[ImgPrimitive]); /// Load the vertices and indices buffer that `draw_img_triangles` will use. - fn load_img_vertices(&mut self, buffers: &VertexBuffers); + fn load_img_vertices(&mut self, buffers: &VertexBuffers); + + /// Load the primitives that `draw_radial_gradient_triangles` will use. + fn load_radial_gradient_primitives(&mut self, primitives: &[RadialGradientPrimitive]); + /// Load the gradient color stops that `draw_radial_gradient_triangles` will + /// use. + fn load_radial_gradient_stops(&mut self, stops: &[GradientStopPrimitive]); + /// Load the vertices and indices buffer that `draw_radial_gradient_triangles` + /// will use. + fn load_radial_gradient_vertices(&mut self, buffers: &VertexBuffers); + + /// Load the primitives that `draw_linear_gradient_triangles` will use. + fn load_linear_gradient_primitives(&mut self, primitives: &[LinearGradientPrimitive]); + /// Load the gradient color stops that `draw_linear_gradient_triangles` will + /// use. + fn load_linear_gradient_stops(&mut self, stops: &[GradientStopPrimitive]); + /// Load the vertices and indices buffer that `draw_linear_gradient_triangles` + /// will use. + fn load_linear_gradient_vertices(&mut self, buffers: &VertexBuffers); /// Draw pure color triangles in the texture. And use the clear color clear /// the texture first if it's a Some-Value fn draw_color_triangles( @@ -115,6 +134,23 @@ pub trait GPUBackendImpl { indices: Range, clear: Option, ); + /// Draw triangles fill with color radial gradient. And use the clear color + /// clear the texture first if it's a Some-Value + fn draw_radial_gradient_triangles( + &mut self, + texture: &mut Self::Texture, + indices: Range, + clear: Option, + ); + + /// Draw triangles fill with color linear gradient. And use the clear color + /// clear the texture first if it's a Some-Value + fn draw_linear_gradient_triangles( + &mut self, + texture: &mut Self::Texture, + indices: Range, + clear: Option, + ); fn copy_texture_from_texture( &mut self, @@ -136,6 +172,87 @@ pub struct ColorAttr { pub mask_head: i32, } +#[repr(packed)] +#[derive(AsBytes, PartialEq, Clone, Copy, Debug)] +pub struct ImagePrimIndex(u32); + +#[repr(packed)] +#[derive(AsBytes, PartialEq, Clone, Copy, Debug)] +pub struct RadialGradientPrimIndex(u32); + +#[repr(packed)] +#[derive(AsBytes, PartialEq, Clone, Copy, Debug)] +pub struct LinearGradientPrimIndex(u32); + +#[repr(packed)] +#[derive(AsBytes, PartialEq, Clone, Copy, Debug)] +pub struct GradientStopPrimitive { + pub red: f32, + pub green: f32, + pub blue: f32, + pub alpha: f32, + pub offset: f32, +} + +impl From for GradientStopPrimitive { + fn from(stop: GradientStop) -> Self { + let color = stop.color.into_f32_components(); + GradientStopPrimitive { + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], + offset: stop.offset, + } + } +} + +#[repr(packed)] +#[derive(AsBytes, PartialEq, Clone, Copy, Debug)] +pub struct RadialGradientPrimitive { + /// A 2x3 column-major matrix, transform a vertex position to the texture + /// position + pub transform: [f32; 6], + /// The color stop's start index + pub stop_start: u32, + /// The size of the color stop + pub stop_cnt: u32, + /// position of the start center + pub start_center: [f32; 2], + /// position of the end center + pub end_center: [f32; 2], + /// the radius of the start circle. + pub start_radius: f32, + /// the radius of the end circle. + pub end_radius: f32, + /// The index of the head mask layer. + pub mask_head: i32, + /// the spread method of the gradient. 0 for pad, 1 for reflect and 2 + /// for repeat + pub spread: u32, +} + +#[repr(packed)] +#[derive(AsBytes, PartialEq, Clone, Copy, Debug)] +pub struct LinearGradientPrimitive { + /// A 2x3 column-major matrix, transform a vertex position to the texture + /// position + pub transform: [f32; 6], + /// The color stop's start index + pub stop_start: u32, + /// The size of the color stop + pub stop_cnt: u32, + /// position of the start center + pub start_position: [f32; 2], + /// position of the end center + pub end_position: [f32; 2], + /// The index of the head mask layer. + pub mask_head: i32, + /// the spread method of the gradient. 0 for pad, 1 for reflect and 2 + /// for repeat + pub spread: u32, +} + #[repr(packed)] #[derive(AsBytes, PartialEq, Clone, Copy)] pub struct ImgPrimitive { diff --git a/gpu/src/wgpu_impl.rs b/gpu/src/wgpu_impl.rs index 639458b40..61f9e42c8 100644 --- a/gpu/src/wgpu_impl.rs +++ b/gpu/src/wgpu_impl.rs @@ -1,9 +1,15 @@ use self::{ draw_alpha_triangles_pass::DrawAlphaTrianglesPass, draw_color_triangles_pass::DrawColorTrianglesPass, draw_img_triangles_pass::DrawImgTrianglesPass, - draw_texture_pass::DrawTexturePass, storage::Storage, + draw_linear_gradient_pass::DrawLinearGradientTrianglesPass, + draw_radial_gradient_pass::DrawRadialGradientTrianglesPass, draw_texture_pass::DrawTexturePass, + storage::Storage, +}; +use crate::{ + gpu_backend::Texture, ColorAttr, GPUBackendImpl, GradientStopPrimitive, ImagePrimIndex, + ImgPrimitive, LinearGradientPrimIndex, LinearGradientPrimitive, MaskLayer, + RadialGradientPrimIndex, RadialGradientPrimitive, }; -use crate::{gpu_backend::Texture, ColorAttr, GPUBackendImpl, ImgPrimitive, MaskLayer}; use futures::{channel::oneshot, Future}; use ribir_geom::{DevicePoint, DeviceRect, DeviceSize}; use ribir_painter::{image::ColorFormat, AntiAliasing, Color, PixelImage, VertexBuffers}; @@ -15,6 +21,8 @@ mod vertex_buffer; mod draw_alpha_triangles_pass; mod draw_color_triangles_pass; mod draw_img_triangles_pass; +mod draw_linear_gradient_pass; +mod draw_radial_gradient_pass; mod draw_texture_pass; pub struct WgpuImpl { @@ -29,6 +37,8 @@ pub struct WgpuImpl { draw_alpha_triangles_pass: DrawAlphaTrianglesPass, draw_color_triangles_pass: DrawColorTrianglesPass, draw_img_triangles_pass: DrawImgTrianglesPass, + draw_radial_gradient_pass: DrawRadialGradientTrianglesPass, + draw_linear_gradient_pass: DrawLinearGradientTrianglesPass, textures_bind: TexturesBind, mask_layers_storage: Storage, @@ -115,7 +125,7 @@ impl GPUBackendImpl for WgpuImpl { .load_triangles_vertices(buffers, &self.device, &self.queue); } - fn load_img_vertices(&mut self, buffers: &VertexBuffers) { + fn load_img_vertices(&mut self, buffers: &VertexBuffers) { self .draw_img_triangles_pass .load_triangles_vertices(buffers, &self.device, &self.queue); @@ -127,6 +137,42 @@ impl GPUBackendImpl for WgpuImpl { .load_img_primitives(&self.device, &self.queue, primitives); } + fn load_radial_gradient_primitives(&mut self, primitives: &[RadialGradientPrimitive]) { + self + .draw_radial_gradient_pass + .load_radial_gradient_primitives(&self.device, &self.queue, primitives); + } + + fn load_radial_gradient_stops(&mut self, stops: &[GradientStopPrimitive]) { + self + .draw_radial_gradient_pass + .load_gradient_stops(&self.device, &self.queue, stops); + } + + fn load_radial_gradient_vertices(&mut self, buffers: &VertexBuffers) { + self + .draw_radial_gradient_pass + .load_triangles_vertices(buffers, &self.device, &self.queue); + } + + fn load_linear_gradient_primitives(&mut self, primitives: &[LinearGradientPrimitive]) { + self + .draw_linear_gradient_pass + .load_linear_gradient_primitives(&self.device, &self.queue, primitives); + } + + fn load_linear_gradient_stops(&mut self, stops: &[GradientStopPrimitive]) { + self + .draw_linear_gradient_pass + .load_gradient_stops(&self.device, &self.queue, stops); + } + + fn load_linear_gradient_vertices(&mut self, buffers: &VertexBuffers) { + self + .draw_linear_gradient_pass + .load_triangles_vertices(buffers, &self.device, &self.queue); + } + fn load_mask_layers(&mut self, layers: &[crate::MaskLayer]) { self .mask_layers_storage @@ -144,6 +190,44 @@ impl GPUBackendImpl for WgpuImpl { ); } + fn draw_radial_gradient_triangles( + &mut self, + texture: &mut Self::Texture, + indices: Range, + clear: Option, + ) { + let encoder = command_encoder!(self); + + self.draw_radial_gradient_pass.draw_triangles( + texture, + indices, + clear, + &self.device, + encoder, + &self.textures_bind, + &self.mask_layers_storage, + ); + } + + fn draw_linear_gradient_triangles( + &mut self, + texture: &mut Self::Texture, + indices: Range, + clear: Option, + ) { + let encoder = command_encoder!(self); + + self.draw_linear_gradient_pass.draw_triangles( + texture, + indices, + clear, + &self.device, + encoder, + &self.textures_bind, + &self.mask_layers_storage, + ); + } + fn draw_alpha_triangles_with_scissor( &mut self, indices: &Range, @@ -539,6 +623,8 @@ impl WgpuImpl { let draw_color_triangles_pass = DrawColorTrianglesPass::new(&device); let draw_img_triangles_pass = DrawImgTrianglesPass::new(&device); + let draw_radial_gradient_pass = DrawRadialGradientTrianglesPass::new(&device); + let draw_linear_gradient_pass = DrawLinearGradientTrianglesPass::new(&device); let mask_layers_storage = Storage::new(&device, wgpu::ShaderStages::FRAGMENT, 512); WgpuImpl { device, @@ -551,6 +637,8 @@ impl WgpuImpl { draw_alpha_triangles_pass, draw_color_triangles_pass, draw_img_triangles_pass, + draw_radial_gradient_pass, + draw_linear_gradient_pass, textures_bind: TexturesBind::default(), mask_layers_storage, } diff --git a/gpu/src/wgpu_impl/draw_img_triangles_pass.rs b/gpu/src/wgpu_impl/draw_img_triangles_pass.rs index ee8b44b32..8e91e026a 100644 --- a/gpu/src/wgpu_impl/draw_img_triangles_pass.rs +++ b/gpu/src/wgpu_impl/draw_img_triangles_pass.rs @@ -1,11 +1,11 @@ use super::{storage::Storage, vertex_buffer::VerticesBuffer}; -use crate::{ImgPrimitive, MaskLayer, TexturesBind, WgpuTexture}; +use crate::{ImagePrimIndex, ImgPrimitive, MaskLayer, TexturesBind, WgpuTexture}; use ribir_painter::{AntiAliasing, Color, Vertex, VertexBuffers}; use std::{mem::size_of, ops::Range}; pub struct DrawImgTrianglesPass { label: &'static str, - vertices_buffer: VerticesBuffer, + vertices_buffer: VerticesBuffer, pipeline: Option, shader: wgpu::ShaderModule, @@ -40,7 +40,7 @@ impl DrawImgTrianglesPass { pub fn load_triangles_vertices( &mut self, - buffers: &VertexBuffers, + buffers: &VertexBuffers, device: &wgpu::Device, queue: &wgpu::Queue, ) { diff --git a/gpu/src/wgpu_impl/draw_linear_gradient_pass.rs b/gpu/src/wgpu_impl/draw_linear_gradient_pass.rs new file mode 100644 index 000000000..2e3989d1f --- /dev/null +++ b/gpu/src/wgpu_impl/draw_linear_gradient_pass.rs @@ -0,0 +1,200 @@ +use super::{storage::Storage, vertex_buffer::VerticesBuffer}; +use crate::{ + GradientStopPrimitive, LinearGradientPrimIndex, LinearGradientPrimitive, MaskLayer, TexturesBind, + WgpuTexture, +}; +use ribir_painter::{AntiAliasing, Color, Vertex, VertexBuffers}; +use std::{mem::size_of, ops::Range}; + +pub struct DrawLinearGradientTrianglesPass { + label: &'static str, + vertices_buffer: VerticesBuffer, + pipeline: Option, + shader: wgpu::ShaderModule, + format: Option, + prims_storage: Storage, + stops_storage: Storage, + textures_count: usize, + anti_aliasing: AntiAliasing, +} + +impl DrawLinearGradientTrianglesPass { + pub fn new(device: &wgpu::Device) -> Self { + let vertices_buffer = VerticesBuffer::new(512, 1024, device); + let label = "linear gradient triangles pass"; + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(label), + source: wgpu::ShaderSource::Wgsl( + include_str!("./shaders/linear_gradient_triangles.wgsl").into(), + ), + }); + let prims_storage = Storage::new(device, wgpu::ShaderStages::FRAGMENT, 64); + let stops_storage = Storage::new(device, wgpu::ShaderStages::FRAGMENT, 64); + + Self { + label, + vertices_buffer, + pipeline: None, + shader, + format: None, + textures_count: 0, + prims_storage, + stops_storage, + anti_aliasing: AntiAliasing::None, + } + } + + pub fn load_triangles_vertices( + &mut self, + buffers: &VertexBuffers, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) { + self.vertices_buffer.write_buffer(buffers, device, queue); + } + + pub fn load_linear_gradient_primitives( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + primitives: &[LinearGradientPrimitive], + ) { + self.prims_storage.write_buffer(device, queue, primitives); + } + + pub fn load_gradient_stops( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + stops: &[GradientStopPrimitive], + ) { + self.stops_storage.write_buffer(device, queue, stops); + } + + #[allow(clippy::too_many_arguments)] + pub fn draw_triangles( + &mut self, + texture: &WgpuTexture, + indices: Range, + clear: Option, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + textures_bind: &TexturesBind, + mask_layer_storage: &Storage, + ) { + self.update( + texture.format(), + texture.anti_aliasing, + device, + textures_bind, + mask_layer_storage.layout(), + ); + let pipeline = self.pipeline.as_ref().unwrap(); + + let color_attachments = texture.color_attachments(clear); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(self.label), + color_attachments: &[Some(color_attachments)], + depth_stencil_attachment: None, + }); + + rpass.set_vertex_buffer(0, self.vertices_buffer.vertices().slice(..)); + rpass.set_index_buffer( + self.vertices_buffer.indices().slice(..), + wgpu::IndexFormat::Uint32, + ); + rpass.set_bind_group(0, mask_layer_storage.bind_group(), &[]); + rpass.set_bind_group(1, self.stops_storage.bind_group(), &[]); + rpass.set_bind_group(2, self.prims_storage.bind_group(), &[]); + rpass.set_bind_group(3, textures_bind.assert_bind(), &[]); + + rpass.set_pipeline(pipeline); + rpass.draw_indexed(indices, 0, 0..1); + } + + fn update( + &mut self, + format: wgpu::TextureFormat, + anti_aliasing: AntiAliasing, + device: &wgpu::Device, + textures_bind: &TexturesBind, + mask_bind_layout: &wgpu::BindGroupLayout, + ) { + if self.format != Some(format) + || textures_bind.textures_count() != self.textures_count + || anti_aliasing != self.anti_aliasing + { + self.pipeline.take(); + self.format = Some(format); + self.textures_count = textures_bind.textures_count(); + self.anti_aliasing = anti_aliasing; + } + + if self.pipeline.is_none() { + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("update triangles pipeline layout"), + bind_group_layouts: &[ + mask_bind_layout, + self.stops_storage.layout(), + self.prims_storage.layout(), + textures_bind.assert_layout(), + ], + push_constant_ranges: &[], + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(self.label), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &self.shader, + entry_point: "vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: size_of::>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + // position + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + // prim_idx + wgpu::VertexAttribute { + offset: 8, + shader_location: 1, + format: wgpu::VertexFormat::Uint32, + }, + ], + }], + }, + fragment: Some(wgpu::FragmentState { + module: &self.shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::all(), + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + // Always draw rect with transform, there is no distinction between front and back, + // everything needs to be drawn. + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: anti_aliasing as u32, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + self.pipeline = Some(pipeline); + } + } +} diff --git a/gpu/src/wgpu_impl/draw_radial_gradient_pass.rs b/gpu/src/wgpu_impl/draw_radial_gradient_pass.rs new file mode 100644 index 000000000..c2ab29ee8 --- /dev/null +++ b/gpu/src/wgpu_impl/draw_radial_gradient_pass.rs @@ -0,0 +1,200 @@ +use super::{storage::Storage, vertex_buffer::VerticesBuffer}; +use crate::{ + GradientStopPrimitive, MaskLayer, RadialGradientPrimIndex, RadialGradientPrimitive, TexturesBind, + WgpuTexture, +}; +use ribir_painter::{AntiAliasing, Color, Vertex, VertexBuffers}; +use std::{mem::size_of, ops::Range}; + +pub struct DrawRadialGradientTrianglesPass { + label: &'static str, + vertices_buffer: VerticesBuffer, + pipeline: Option, + shader: wgpu::ShaderModule, + format: Option, + prims_storage: Storage, + stops_storage: Storage, + textures_count: usize, + anti_aliasing: AntiAliasing, +} + +impl DrawRadialGradientTrianglesPass { + pub fn new(device: &wgpu::Device) -> Self { + let vertices_buffer = VerticesBuffer::new(512, 1024, device); + let label = "radial triangles pass"; + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(label), + source: wgpu::ShaderSource::Wgsl( + include_str!("./shaders/radial_gradient_triangles.wgsl").into(), + ), + }); + let prims_storage = Storage::new(device, wgpu::ShaderStages::FRAGMENT, 64); + let stops_storage = Storage::new(device, wgpu::ShaderStages::FRAGMENT, 64); + + Self { + label, + vertices_buffer, + pipeline: None, + shader, + format: None, + textures_count: 0, + prims_storage, + stops_storage, + anti_aliasing: AntiAliasing::None, + } + } + + pub fn load_triangles_vertices( + &mut self, + buffers: &VertexBuffers, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) { + self.vertices_buffer.write_buffer(buffers, device, queue); + } + + pub fn load_radial_gradient_primitives( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + primitives: &[RadialGradientPrimitive], + ) { + self.prims_storage.write_buffer(device, queue, primitives); + } + + pub fn load_gradient_stops( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + stops: &[GradientStopPrimitive], + ) { + self.stops_storage.write_buffer(device, queue, stops); + } + + #[allow(clippy::too_many_arguments)] + pub fn draw_triangles( + &mut self, + texture: &WgpuTexture, + indices: Range, + clear: Option, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + textures_bind: &TexturesBind, + mask_layer_storage: &Storage, + ) { + self.update( + texture.format(), + texture.anti_aliasing, + device, + textures_bind, + mask_layer_storage.layout(), + ); + let pipeline = self.pipeline.as_ref().unwrap(); + + let color_attachments = texture.color_attachments(clear); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(self.label), + color_attachments: &[Some(color_attachments)], + depth_stencil_attachment: None, + }); + + rpass.set_vertex_buffer(0, self.vertices_buffer.vertices().slice(..)); + rpass.set_index_buffer( + self.vertices_buffer.indices().slice(..), + wgpu::IndexFormat::Uint32, + ); + rpass.set_bind_group(0, mask_layer_storage.bind_group(), &[]); + rpass.set_bind_group(1, self.stops_storage.bind_group(), &[]); + rpass.set_bind_group(2, self.prims_storage.bind_group(), &[]); + rpass.set_bind_group(3, textures_bind.assert_bind(), &[]); + + rpass.set_pipeline(pipeline); + rpass.draw_indexed(indices, 0, 0..1); + } + + fn update( + &mut self, + format: wgpu::TextureFormat, + anti_aliasing: AntiAliasing, + device: &wgpu::Device, + textures_bind: &TexturesBind, + mask_bind_layout: &wgpu::BindGroupLayout, + ) { + if self.format != Some(format) + || textures_bind.textures_count() != self.textures_count + || anti_aliasing != self.anti_aliasing + { + self.pipeline.take(); + self.format = Some(format); + self.textures_count = textures_bind.textures_count(); + self.anti_aliasing = anti_aliasing; + } + + if self.pipeline.is_none() { + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("update triangles pipeline layout"), + bind_group_layouts: &[ + mask_bind_layout, + self.stops_storage.layout(), + self.prims_storage.layout(), + textures_bind.assert_layout(), + ], + push_constant_ranges: &[], + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(self.label), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &self.shader, + entry_point: "vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: size_of::>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + // position + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + // prim_idx + wgpu::VertexAttribute { + offset: 8, + shader_location: 1, + format: wgpu::VertexFormat::Uint32, + }, + ], + }], + }, + fragment: Some(wgpu::FragmentState { + module: &self.shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::all(), + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + // Always draw rect with transform, there is no distinction between front and back, + // everything needs to be drawn. + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: anti_aliasing as u32, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + self.pipeline = Some(pipeline); + } + } +} diff --git a/gpu/src/wgpu_impl/shaders/color_triangles.wgsl b/gpu/src/wgpu_impl/shaders/color_triangles.wgsl index 128b916c2..b78b3f77c 100644 --- a/gpu/src/wgpu_impl/shaders/color_triangles.wgsl +++ b/gpu/src/wgpu_impl/shaders/color_triangles.wgsl @@ -11,7 +11,6 @@ struct FragInput { } - @vertex fn vs_main(v: Vertex) -> FragInput { var input: FragInput; @@ -63,7 +62,7 @@ fn fs_main(input: FragInput) -> @location(0) vec4 { let tex_size = textureDimensions(texture); mask_pos = mask_pos / vec2(f32(tex_size.x), f32(tex_size.y)); - let alpha = textureSample(texture, s_sampler, mask_pos).r; + let alpha = textureSampleLevel(texture, s_sampler, mask_pos, 0.).r; if alpha == 0. { color.a = 0.; break; diff --git a/gpu/src/wgpu_impl/shaders/img_triangles.wgsl b/gpu/src/wgpu_impl/shaders/img_triangles.wgsl index 367e95e44..72f9b7160 100644 --- a/gpu/src/wgpu_impl/shaders/img_triangles.wgsl +++ b/gpu/src/wgpu_impl/shaders/img_triangles.wgsl @@ -88,7 +88,7 @@ fn fs_main(f: VertexOutput) -> @location(0) vec4 { let mask_sampler = samplers[mask_tex_idx]; let mask_tex_size = textureDimensions(mask_tex); mask_pos = mask_pos / vec2(f32(mask_tex_size.x), f32(mask_tex_size.y)); - let alpha = textureSample(mask_tex, mask_sampler, mask_pos).r; + let alpha = textureSampleLevel(mask_tex, mask_sampler, mask_pos, 0.).r; if alpha == 0. { color.a = 0.; break; diff --git a/gpu/src/wgpu_impl/shaders/linear_gradient_triangles.wgsl b/gpu/src/wgpu_impl/shaders/linear_gradient_triangles.wgsl new file mode 100644 index 000000000..65f0aea83 --- /dev/null +++ b/gpu/src/wgpu_impl/shaders/linear_gradient_triangles.wgsl @@ -0,0 +1,134 @@ +struct Vertex { + @location(0) pos: vec2, + @location(1) prim_idx: u32, +}; + +struct FragInput { + @builtin(position) pos: vec4, + @location(0) prim_idx: u32, +} + +@vertex +fn vs_main(v: Vertex) -> FragInput { + var input: FragInput; + // convert from gpu-backend coords(0..1) to wgpu corrds(-1..1) + let pos = v.pos * vec2(2., -2.) + vec2(-1., 1.); + input.pos = vec4(pos, 0.0, 1.0); + input.prim_idx = v.prim_idx; + return input; +} + + +struct MaskLayer { + transform: mat3x2, + min: vec2, + max: vec2, + mask_tex_idx: u32, + prev_mask_idx: i32, +} + +struct Stop { + red: f32, + green: f32, + blue: f32, + alpha: f32, + offset: f32, +} + +struct Primitive { + transform: mat3x2, + stop_start: i32, + stop_cnt: i32, + start_position: vec2, + end_position: vec2, + mask_head: i32, + spread: u32, // 0 for pad, 1 for reflect, 2 for repeat +} + +@group(0) @binding(0) +var mask_layers: array; + +@group(1) @binding(0) +var stops: array; + +@group(2) @binding(0) +var prims: array; + +@group(3) @binding(0) +var textures: binding_array>; +@group(3) @binding(1) +var samplers: binding_array; + + +fn calc_offset(x: f32, y: f32, x_0: f32, y_0: f32, x_1: f32, y_1: f32) -> f32 { + let dx_0 = x - x_0; + let dy_0 = y - y_0; + let dx_1_0 = x_1 - x_0; + let dy_1_0 = y_1 - y_0; + + return (dx_0 * dx_1_0 + dy_0 * dy_1_0) / (dx_1_0 * dx_1_0 + dy_1_0 * dy_1_0); +} + +@fragment +fn fs_main(input: FragInput) -> @location(0) vec4 { + let prim = prims[input.prim_idx]; + let pos = prim.transform * vec3(input.pos.xy, 1.); + var alpha = 1.; + var mask_idx = prim.mask_head; + loop { + if mask_idx < 0 { + break; + } + let mask = mask_layers[u32(mask_idx)]; + + var mask_pos = mask.transform * vec3(input.pos.xy, 1.); + if any(mask_pos < mask.min) || any(mask.max < mask_pos) { + alpha = 0.; + break; + } + + let mask_tex_idx = mask.mask_tex_idx; + let texture = textures[mask_tex_idx]; + let s_sampler = samplers[mask_tex_idx]; + + let tex_size = textureDimensions(texture); + mask_pos = mask_pos / vec2(f32(tex_size.x), f32(tex_size.y)); + let a = textureSampleLevel(texture, s_sampler, mask_pos, 0.).r; + alpha = alpha * a; + if alpha == 0. { + break; + } + mask_idx = mask.prev_mask_idx; + } + + if (prim.start_position.x == prim.end_position.x && + prim.start_position.y == prim.end_position.y) { + return vec4(1., 1., 1., alpha); + } + var offset = calc_offset(pos.x, pos.y, prim.start_position.x, prim.start_position.y, prim.end_position.x, prim.end_position.y); + + if (prim.spread == 0u) { + // pad + offset = min(1., max(0., offset)); + } else if (prim.spread == 1u) { + //reflect + offset = 1. - abs(fract(offset / 2.) - 0.5) * 2.; + } else { + //repeat + offset = fract(offset); + } + + var prev = stops[prim.stop_start]; + var next = stops[prim.stop_start + 1]; + for (var i = 2; i < prim.stop_cnt && next.offset < offset; i++) { + prev = next; + next = stops[prim.stop_start + i]; + } + + offset = max(prev.offset, min(next.offset, offset)); + let weight1 = (next.offset - offset) / (next.offset - prev.offset); + let weight2 = 1. - weight1; + let prev_color = vec4(prev.red, prev.green, prev.blue, prev.alpha); + let next_color = vec4(next.red, next.green, next.blue, next.alpha); + return (prev_color * weight1 + next_color * weight2) * vec4(1., 1., 1., alpha); +} diff --git a/gpu/src/wgpu_impl/shaders/radial_gradient_triangles.wgsl b/gpu/src/wgpu_impl/shaders/radial_gradient_triangles.wgsl new file mode 100644 index 000000000..80abc8583 --- /dev/null +++ b/gpu/src/wgpu_impl/shaders/radial_gradient_triangles.wgsl @@ -0,0 +1,192 @@ +struct Vertex { + @location(0) pos: vec2, + @location(1) prim_idx: u32, +}; + +struct FragInput { + @builtin(position) pos: vec4, + @location(0) prim_idx: u32, +} + +@vertex +fn vs_main(v: Vertex) -> FragInput { + var input: FragInput; + // convert from gpu-backend coords(0..1) to wgpu corrds(-1..1) + let pos = v.pos * vec2(2., -2.) + vec2(-1., 1.); + input.pos = vec4(pos, 0.0, 1.0); + input.prim_idx = v.prim_idx; + return input; +} + + +struct MaskLayer { + transform: mat3x2, + min: vec2, + max: vec2, + mask_tex_idx: u32, + prev_mask_idx: i32, +} + +struct Stop { + red: f32, + green: f32, + blue: f32, + alpha: f32, + offset: f32, +} + +struct Primitive { + transform: mat3x2, + stop_start: i32, + stop_cnt: i32, + start_center: vec2, + end_center: vec2, + start_radius: f32, + end_radius: f32, + mask_head: i32, + spread: u32, // 0 for pad, 1 for reflect, 2 for repeat +} + +@group(0) @binding(0) +var mask_layers: array; + +@group(1) @binding(0) +var stops: array; + +@group(2) @binding(0) +var prims: array; + +@group(3) @binding(0) +var textures: binding_array>; +@group(3) @binding(1) +var samplers: binding_array; + + + +// input the center and radius of the circles, return the tag of resolvable (1. mean resolvable and -1. unresolvable) and the offset if tag is resolvable. +fn calc_offset(x: f32, y: f32, x_0: f32, y_0: f32, r_0: f32, x_1: f32, y_1: f32, r_1: f32) -> vec2 { + /* + see definition at https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createradialgradient + with offset ω, Radial gradients must be rendered by following these steps: + 1. If x0 = x1 and y0 = y1 and r0 = r1, then the radial gradient must paint nothing. Return. + Let x(ω) = (x1-x0)ω + x0 + Let y(ω) = (y1-y0)ω + y0 + Let r(ω) = (r1-r0)ω + r0 + 2. Let the color at ω be the color at that position on the gradient (with the colors coming from the interpolation + and extrapolation described above). + 3. For all values of ω where r(ω) > 0, starting with the value of ω nearest to positive infinity and ending with + the value of ω nearest to negative infinity, draw the circumference of the circle with radius r(ω) at position + (x(ω), y(ω)), with the color at ω, but only painting on the parts of the bitmap that have not yet been painted + on by earlier circles in this step for this rendering of the gradient. + + so the offset ω meet the following equation: (x(ω) - x)^2 + (y(ω) - y)^2 = r(ω)^2. + we sovle the equation and get the offset ω with the min r. + define: + dx_0 = x - x_0; + dx_1_0 = x_1 - x_0; + dy_0 = y - y_0; + dy_1_0 = y_1 - y_0; + dr_1_0 = r_1 - r_0; + the (x(ω) - x)^2 + (y(ω) - y)^2 = r(ω)^2 can be rewrite as: + (dx_1_0^2 + dy_1_0^2 - dr_1_0^2) * ω^2 - 2 * (dx_1_0 * dx_0 + dy_1_0 * dy_0 + dr_1_0 * r_0) * ω + (dx_0^2 + dy_0^2 - r_0^2) = 0 + the ω can be solve by the quadratic formula: + ω = (-b ± sqrt(b^2 - 4ac)) / 2a + where a = dx_1_0^2 + dy_1_0^2 - dr_1_0^2 + b = -2 * (dx_1_0 * dx_0 + dy_1_0 * dy_0 + dr_1_0 * r_0) + c = dx_0^2 + dy_0^2 - r_0^2 + */ + let dx_0 = x - x_0; + let dx_1_0 = x_1 - x_0; + let dy_0 = y - y_0; + let dy_1_0 = y_1 - y_0; + let dr_1_0 = r_1 - r_0; + let a = dx_1_0 * dx_1_0 + dy_1_0 * dy_1_0 - dr_1_0 * dr_1_0; + let b = -2. * (dx_1_0 * dx_0 + dy_1_0 * dy_0 + dr_1_0 * r_0); + let c = dx_0 * dx_0 + dy_0 * dy_0 - r_0 * r_0; + + let delta = b * b - 4. * a * c; + + if (abs(a) < 0.1) { + if (abs(b) < 0.1) { + return vec2(-1., 0.); + } else { + return vec2(1., -c / b); + } + } else if (delta < 0.) { + return vec2(-1., 0.); + } + + let sqrt_delta = sqrt(delta); + let _2a = 2. * a; + let w1 = (-b + sqrt_delta) / _2a; + let w2 = (-b - sqrt_delta) / _2a; + + return vec2(1., max(w1, w2)); +} + +@fragment +fn fs_main(input: FragInput) -> @location(0) vec4 { + let prim = prims[input.prim_idx]; + let pos = prim.transform * vec3(input.pos.xy, 1.); + + var alpha = 1.; + var mask_idx = prim.mask_head; + loop { + if mask_idx < 0 { + break; + } + let mask = mask_layers[u32(mask_idx)]; + + var mask_pos = mask.transform * vec3(input.pos.xy, 1.); + if any(mask_pos < mask.min) || any(mask.max < mask_pos) { + alpha = 0.; + break; + } + + let mask_tex_idx = mask.mask_tex_idx; + let texture = textures[mask_tex_idx]; + let s_sampler = samplers[mask_tex_idx]; + + let tex_size = textureDimensions(texture); + mask_pos = mask_pos / vec2(f32(tex_size.x), f32(tex_size.y)); + let a = textureSampleLevel(texture, s_sampler, mask_pos, 0.).r; + alpha = alpha * a; + if alpha == 0. { + break; + } + mask_idx = mask.prev_mask_idx; + } + + let res = calc_offset(pos.x, pos.y, prim.start_center.x, prim.start_center.y, prim.start_radius, prim.end_center.x, prim.end_center.y, prim.end_radius); + + if (res[0] < 0. || ( + prim.start_radius != prim.end_radius && + res[1] < (prim.start_radius / (prim.start_radius - prim.end_radius)))) { + return vec4(1., 1., 1., alpha); + } + var offset = res[1]; + if (prim.spread == 0u) { + // pad + offset = min(1., max(0., offset)); + } else if (prim.spread == 1u) { + //reflect + offset = 1. - abs(fract(offset / 2.) - 0.5) * 2.; + } else { + //repeat + offset = fract(offset); + } + + var prev = stops[prim.stop_start]; + var next = stops[prim.stop_start + 1]; + for (var i = 2; i < prim.stop_cnt && next.offset < offset; i++) { + prev = next; + next = stops[prim.stop_start + i]; + } + + offset = max(prev.offset, min(next.offset, offset)); + let weight1 = (next.offset - offset) / (next.offset - prev.offset); + let weight2 = 1. - weight1; + let prev_color = vec4(prev.red, prev.green, prev.blue, prev.alpha); + let next_color = vec4(next.red, next.green, next.blue, next.alpha); + return (prev_color * weight1 + next_color * weight2) * vec4(1., 1., 1., alpha); +} diff --git a/painter/src/color.rs b/painter/src/color.rs index 9751a7f63..19a0fa7c2 100644 --- a/painter/src/color.rs +++ b/painter/src/color.rs @@ -1,6 +1,9 @@ use material_color_utilities_rs::htc; +use ribir_geom::Point; use serde::{Deserialize, Serialize}; +use crate::SpreadMethod; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct Color { pub red: u8, @@ -9,6 +12,35 @@ pub struct Color { pub alpha: u8, } +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct GradientStop { + pub color: Color, + pub offset: f32, +} + +impl GradientStop { + #[inline] + pub fn new(color: Color, offset: f32) -> Self { Self { color, offset } } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct RadialGradient { + pub start_center: Point, + pub start_radius: f32, + pub end_center: Point, + pub end_radius: f32, + pub stops: Vec, + pub spread_method: SpreadMethod, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct LinearGradient { + pub start: Point, + pub end: Point, + pub stops: Vec, + pub spread_method: SpreadMethod, +} + /// Describe the light tone of a color, should between [0, 1.0], 0.0 gives /// absolute black and 1.0 give the brightest white. #[derive(Clone, Debug, Copy)] diff --git a/painter/src/lib.rs b/painter/src/lib.rs index 42be21cea..a16d491ae 100644 --- a/painter/src/lib.rs +++ b/painter/src/lib.rs @@ -5,7 +5,7 @@ pub mod color; mod painter; pub mod path; pub mod path_builder; -pub use crate::color::{Color, LightnessTone}; +pub use crate::color::{Color, GradientStop, LightnessTone}; pub use crate::painter::*; pub use path::*; pub mod image; diff --git a/painter/src/painter.rs b/painter/src/painter.rs index 564610447..d9de6126d 100644 --- a/painter/src/painter.rs +++ b/painter/src/painter.rs @@ -1,11 +1,15 @@ -use crate::{path::*, path_builder::PathBuilder, Brush, Color, PixelImage, Svg}; +use crate::{ + color::{LinearGradient, RadialGradient}, + path::*, + path_builder::PathBuilder, + Brush, Color, PixelImage, Svg, +}; use ribir_algo::ShareResource; use ribir_geom::{Angle, DeviceRect, Point, Rect, Size, Transform, Vector}; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; - /// The painter is a two-dimensional grid. The coordinate (0, 0) is at the /// upper-left corner of the canvas. Along the X-axis, values increase towards /// the right edge of the canvas. Along the Y-axis, values increase towards the @@ -73,6 +77,25 @@ pub struct PaintPath { pub transform: Transform, } +#[repr(u32)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq)] +pub enum SpreadMethod { + #[default] + Pad, + Reflect, + Repeat, +} + +impl From for SpreadMethod { + fn from(value: usvg::SpreadMethod) -> Self { + match value { + usvg::SpreadMethod::Pad => SpreadMethod::Pad, + usvg::SpreadMethod::Reflect => SpreadMethod::Reflect, + usvg::SpreadMethod::Repeat => SpreadMethod::Repeat, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PaintCommand { ColorPath { @@ -84,11 +107,33 @@ pub enum PaintCommand { img: ShareResource, opacity: f32, }, + RadialGradient { + path: PaintPath, + radial_gradient: RadialGradient, + }, + LinearGradient { + path: PaintPath, + linear_gradient: LinearGradient, + }, // Todo: keep rectangle clip. Clip(PaintPath), PopClip, } +impl PaintCommand { + pub fn transform(mut self, transform: &Transform) -> Self { + match &mut self { + PaintCommand::ColorPath { path, .. } + | PaintCommand::ImgPath { path, .. } + | PaintCommand::RadialGradient { path, .. } + | PaintCommand::LinearGradient { path, .. } + | PaintCommand::Clip(path) => path.transform(transform), + PaintCommand::PopClip => {} + } + self + } +} + #[derive(Clone)] struct PainterState { /// The line width use to stroke path. @@ -289,7 +334,12 @@ impl Painter { color: color.apply_alpha(opacity), }, Brush::Image(img) => PaintCommand::ImgPath { path, img, opacity }, - Brush::Gradient => todo!(), + Brush::RadialGradient(radial_gradient) => { + PaintCommand::RadialGradient { path, radial_gradient } + } + Brush::LinearGradient(linear_gradient) => { + PaintCommand::LinearGradient { path, linear_gradient } + } }; self.commands.push(cmd); } @@ -438,17 +488,11 @@ impl Painter { } pub fn draw_svg(&mut self, svg: &Svg) -> &mut Self { - svg.paths.iter().for_each(|c| { - self - .scale(svg.view_scale.x, svg.view_scale.y) - .set_brush(c.brush.clone()); - match &c.style { - PathPaintStyle::Fill => self.fill_path(c.path.clone()), - PathPaintStyle::Stroke(options) => self - .set_strokes(options.clone()) - .stroke_path(c.path.clone()), - }; - }); + let transform = *self.get_transform(); + svg + .paint_commands + .iter() + .for_each(|c| self.commands.push(c.clone().transform(&transform))); self } @@ -561,6 +605,11 @@ impl PaintPath { self.transform = self.transform.then_scale(scale, scale); self.paint_bounds = self.paint_bounds.scale(scale, scale); } + + pub fn transform(&mut self, transform: &Transform) { + self.transform = self.transform.then(transform); + self.paint_bounds = self.transform.outer_transformed_rect(self.path.bounds()); + } } #[cfg(test)] diff --git a/painter/src/style.rs b/painter/src/style.rs index d95641b8c..b576ad209 100644 --- a/painter/src/style.rs +++ b/painter/src/style.rs @@ -1,4 +1,7 @@ -use crate::{Color, PixelImage}; +use crate::{ + color::{LinearGradient, RadialGradient}, + Color, PixelImage, +}; use ribir_algo::ShareResource; use serde::{Deserialize, Serialize}; @@ -7,7 +10,8 @@ pub enum Brush { Color(Color), /// Image brush always use a repeat mode to brush the path. Image(ShareResource), - Gradient, // todo, + RadialGradient(RadialGradient), + LinearGradient(LinearGradient), } impl Brush { diff --git a/painter/src/svg.rs b/painter/src/svg.rs index 35baaf153..439f8e2c5 100644 --- a/painter/src/svg.rs +++ b/painter/src/svg.rs @@ -1,21 +1,29 @@ -use crate::{Brush, Color, LineCap, LineJoin, Path, PathPaintStyle, StrokeOptions}; -use ribir_geom::{Point, Size, Transform, Vector}; +use crate::{ + color::{LinearGradient, RadialGradient}, + Brush, Color, GradientStop, LineCap, LineJoin, PaintCommand, Path, StrokeOptions, +}; +use ribir_geom::{Point, Rect, Size, Transform}; use serde::{Deserialize, Serialize}; use std::{error::Error, io::Read}; -use usvg::{Options, Tree, TreeParsing}; +use usvg::{Options, Stop, Tree, TreeParsing}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct Svg { pub size: Size, - pub view_scale: Vector, - pub paths: Box<[SvgPath]>, + pub paint_commands: Vec, } -#[derive(Serialize, Deserialize)] -pub struct SvgPath { - pub path: Path, - pub brush: Brush, - pub style: PathPaintStyle, +/// Fits size into a viewbox. copy from resvg +fn fit_view_box(size: usvg::Size, vb: &usvg::ViewBox) -> usvg::Size { + let s = vb.rect.size(); + + if vb.aspect.align == usvg::Align::None { + s + } else if vb.aspect.slice { + size.expand_to(s) + } else { + size.scale_to(s) + } } // todo: we need to support currentColor to change svg color. @@ -25,34 +33,31 @@ impl Svg { let tree = Tree::from_data(svg_data, &opt).unwrap(); let view_rect = tree.view_box.rect; let size = tree.size; - let fit_size = size.fit_view_box(&tree.view_box); - let view_scale = Vector::new( - size.width() / fit_size.width(), - size.height() / fit_size.height(), - ) - .to_f32(); - let t = Transform::translation(-view_rect.x() as f32, -view_rect.y() as f32); - - let mut t_stack = TransformStack::new(t); - let mut paths = vec![]; + let fit_size = fit_view_box(size, &tree.view_box); + let bound_rect = Rect::from_size(Size::new(f32::INFINITY, f32::INFINITY)); + let mut painter = crate::Painter::new(bound_rect); + painter.apply_transform( + &Transform::translation(-view_rect.x(), -view_rect.y()).then_scale( + size.width() / fit_size.width(), + size.height() / fit_size.height(), + ), + ); tree.root.traverse().for_each(|edge| match edge { rctree::NodeEdge::Start(node) => { use usvg::NodeKind; - + painter.save(); match &*node.borrow() { NodeKind::Path(p) => { - t_stack.push(matrix_convert(p.transform)); + painter.apply_transform(&matrix_convert(p.transform)); let path = usvg_path_to_path(p); - let path = path.transform(t_stack.current_transform()); if let Some(ref fill) = p.fill { - let brush = brush_from_usvg_paint(&fill.paint, fill.opacity); - - paths.push(SvgPath { - path: path.clone(), - brush, - style: PathPaintStyle::Fill, - }); + let (brush, transform) = brush_from_usvg_paint(&fill.paint, fill.opacity, &size); + let mut painter = painter.save_guard(); + painter + .set_brush(brush.clone()) + .apply_transform(&transform) + .fill_path(path.clone().transform(&transform.inverse().unwrap())); //&o_ts.then(&n_ts.inverse().unwrap()))); } if let Some(ref stroke) = p.stroke { @@ -67,18 +72,19 @@ impl Svg { usvg::LineJoin::Round => LineJoin::Round, }; let options = StrokeOptions { - width: stroke.width.get() as f32, + width: stroke.width.get(), line_cap: cap, line_join: join, - miter_limit: stroke.miterlimit.get() as f32, + miter_limit: stroke.miterlimit.get(), }; - let brush = brush_from_usvg_paint(&stroke.paint, stroke.opacity); - paths.push(SvgPath { - path, - brush, - style: PathPaintStyle::Stroke(options), - }); + let (brush, transform) = brush_from_usvg_paint(&stroke.paint, stroke.opacity, &size); + let mut painter = painter.save_guard(); + painter + .set_brush(brush.clone()) + .apply_transform(&transform) + .set_strokes(options) + .stroke_path(path.transform(&transform.inverse().unwrap())); }; } NodeKind::Image(_) => { @@ -86,7 +92,7 @@ impl Svg { log::warn!("[painter]: not support draw embed image in svg, ignored!"); } NodeKind::Group(ref g) => { - t_stack.push(matrix_convert(g.transform)); + painter.apply_transform(&matrix_convert(g.transform)); // todo; if g.opacity.get() != 1. { log::warn!("[painter]: not support `opacity` in svg, ignored!"); @@ -107,14 +113,13 @@ impl Svg { } } rctree::NodeEdge::End(_) => { - t_stack.pop(); + painter.restore(); } }); Ok(Svg { - size: Size::new(size.width() as f32, size.height() as f32), - paths: paths.into_boxed_slice(), - view_scale, + size: Size::new(size.width(), size.height()), + paint_commands: painter.finish(), }) } @@ -136,57 +141,123 @@ impl Svg { fn usvg_path_to_path(path: &usvg::Path) -> Path { let mut builder = lyon_algorithms::path::Path::svg_builder(); path.data.segments().for_each(|seg| match seg { - usvg::PathSegment::MoveTo { x, y } => { - builder.move_to(point(x, y)); + usvg::tiny_skia_path::PathSegment::MoveTo(pt) => { + builder.move_to(point(pt.x, pt.y)); } - usvg::PathSegment::LineTo { x, y } => { - builder.line_to(point(x, y)); + usvg::tiny_skia_path::PathSegment::LineTo(pt) => { + builder.line_to(point(pt.x, pt.y)); } - usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => { - builder.cubic_bezier_to(point(x1, y1), point(x2, y2), point(x, y)); + usvg::tiny_skia_path::PathSegment::CubicTo(pt1, pt2, pt3) => { + builder.cubic_bezier_to( + point(pt1.x, pt1.y), + point(pt2.x, pt2.y), + point(pt3.x, pt3.y), + ); } - usvg::PathSegment::ClosePath => builder.close(), + usvg::tiny_skia_path::PathSegment::QuadTo(pt1, pt2) => { + builder.quadratic_bezier_to(point(pt1.x, pt1.y), point(pt2.x, pt2.y)); + } + usvg::tiny_skia_path::PathSegment::Close => builder.close(), }); builder.build().into() } -fn point(x: f64, y: f64) -> lyon_algorithms::math::Point { - Point::new(x as f32, y as f32).to_untyped() -} +fn point(x: f32, y: f32) -> lyon_algorithms::math::Point { Point::new(x, y).to_untyped() } fn matrix_convert(t: usvg::Transform) -> Transform { - let usvg::Transform { a, b, c, d, e, f } = t; - Transform::new(a as f32, b as f32, c as f32, d as f32, e as f32, f as f32) + let usvg::Transform { sx, kx, ky, sy, tx, ty } = t; + Transform::new(sx, ky, kx, sy, tx, ty) } -fn brush_from_usvg_paint(paint: &usvg::Paint, opacity: usvg::Opacity) -> Brush { +fn brush_from_usvg_paint( + paint: &usvg::Paint, + opacity: usvg::Opacity, + size: &usvg::Size, +) -> (Brush, Transform) { match paint { - usvg::Paint::Color(usvg::Color { red, green, blue }) => Color::from_rgb(*red, *green, *blue) - .with_alpha(opacity.get() as f32) - .into(), + usvg::Paint::Color(usvg::Color { red, green, blue }) => ( + Color::from_rgb(*red, *green, *blue) + .with_alpha(opacity.get()) + .into(), + Transform::identity(), + ), + usvg::Paint::LinearGradient(linear) => { + let stops = convert_to_gradient_stops(&linear.stops); + let size_scale = match linear.units { + usvg::Units::UserSpaceOnUse => (1., 1.), + usvg::Units::ObjectBoundingBox => (size.width(), size.height()), + }; + let gradient = LinearGradient { + start: Point::new(linear.x1 * size_scale.0, linear.y1 * size_scale.1), + end: Point::new(linear.x2 * size_scale.0, linear.y2 * size_scale.1), + stops, + spread_method: linear.spread_method.into(), + }; + + ( + Brush::LinearGradient(gradient), + matrix_convert(linear.transform), + ) + } + usvg::Paint::RadialGradient(radial_gradient) => { + let stops = convert_to_gradient_stops(&radial_gradient.stops); + let size_scale = match radial_gradient.units { + usvg::Units::UserSpaceOnUse => (1., 1.), + usvg::Units::ObjectBoundingBox => (size.width(), size.height()), + }; + let gradient = RadialGradient { + start_center: Point::new( + radial_gradient.fx * size_scale.0, + radial_gradient.fy * size_scale.1, + ), + start_radius: 0., // usvg not support fr + end_center: Point::new( + radial_gradient.cx * size_scale.0, + radial_gradient.cy * size_scale.1, + ), + end_radius: radial_gradient.r.get() * size_scale.0, + stops, + spread_method: radial_gradient.spread_method.into(), + }; + + ( + Brush::RadialGradient(gradient), + matrix_convert(radial_gradient.transform), + ) + } paint => { log::warn!("[painter]: not support `{paint:?}` in svg, use black instead!"); - Color::BLACK.into() + (Color::BLACK.into(), Transform::identity()) } } } -struct TransformStack { - stack: Vec, -} +fn convert_to_gradient_stops(stops: &Vec) -> Vec { + assert!(!stops.is_empty()); + + let mut stops: Vec<_> = stops + .iter() + .map(|stop| { + let usvg::Color { red, green, blue } = stop.color; + GradientStop { + offset: stop.offset.get(), + color: Color::new(red, green, blue, stop.opacity.to_u8()), + } + }) + .collect(); -impl TransformStack { - fn new(t: Transform) -> Self { TransformStack { stack: vec![t] } } + stops.sort_by(|s1, s2| s1.offset.partial_cmp(&s2.offset).unwrap()); - fn push(&mut self, mut t: Transform) { - if let Some(p) = self.stack.last() { - t = p.then(&t); + if let Some(first) = stops.first() { + if first.offset != 0. { + stops.insert(0, GradientStop { offset: 0., color: first.color }); } - self.stack.push(t); } - - fn pop(&mut self) -> Option { self.stack.pop() } - - fn current_transform(&self) -> &Transform { self.stack.last().unwrap() } + if let Some(last) = stops.last() { + if last.offset < 1. { + stops.push(GradientStop { offset: 1., color: last.color }); + } + } + stops } diff --git a/ribir/src/winit_shell_wnd.rs b/ribir/src/winit_shell_wnd.rs index 802c994d8..b40d8a444 100644 --- a/ribir/src/winit_shell_wnd.rs +++ b/ribir/src/winit_shell_wnd.rs @@ -117,6 +117,8 @@ impl ShellWindow for WinitShellWnd { commands.iter_mut().for_each(|c| match c { PaintCommand::ColorPath { path, .. } | PaintCommand::ImgPath { path, .. } + | PaintCommand::RadialGradient { path, .. } + | PaintCommand::LinearGradient { path, .. } | PaintCommand::Clip(path) => path.scale(self.winit_wnd.scale_factor() as f32), PaintCommand::PopClip => {} }); diff --git a/test_cases/counter/counter_with_default_by_wgpu.png b/test_cases/counter/counter_with_default_by_wgpu.png index 20a7e2d3e..30c8f9ad4 100644 Binary files a/test_cases/counter/counter_with_default_by_wgpu.png and b/test_cases/counter/counter_with_default_by_wgpu.png differ diff --git a/test_cases/counter/counter_with_material_by_wgpu.png b/test_cases/counter/counter_with_material_by_wgpu.png index 6479606dd..8476bfb0d 100644 Binary files a/test_cases/counter/counter_with_material_by_wgpu.png and b/test_cases/counter/counter_with_material_by_wgpu.png differ diff --git a/test_cases/messages/messages_with_default_by_wgpu.png b/test_cases/messages/messages_with_default_by_wgpu.png index fb90c998b..a44fb667b 100644 Binary files a/test_cases/messages/messages_with_default_by_wgpu.png and b/test_cases/messages/messages_with_default_by_wgpu.png differ diff --git a/test_cases/messages/messages_with_material_by_wgpu.png b/test_cases/messages/messages_with_material_by_wgpu.png index 18fa093df..55db4d544 100644 Binary files a/test_cases/messages/messages_with_material_by_wgpu.png and b/test_cases/messages/messages_with_material_by_wgpu.png differ diff --git a/test_cases/ribir_gpu/gpu_backend/tests/draw_svg_gradient_wgpu.png b/test_cases/ribir_gpu/gpu_backend/tests/draw_svg_gradient_wgpu.png new file mode 100644 index 000000000..9bb1dc953 Binary files /dev/null and b/test_cases/ribir_gpu/gpu_backend/tests/draw_svg_gradient_wgpu.png differ diff --git a/test_cases/ribir_gpu/gpu_backend/tests/transform_img_brush_wgpu.png b/test_cases/ribir_gpu/gpu_backend/tests/transform_img_brush_wgpu.png index bfd516ffd..4608176a8 100644 Binary files a/test_cases/ribir_gpu/gpu_backend/tests/transform_img_brush_wgpu.png and b/test_cases/ribir_gpu/gpu_backend/tests/transform_img_brush_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png index 96269f69d..17c19c631 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png index 8f70df777..f52ebd311 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png index 41b98d936..d5ad9403f 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png differ diff --git a/test_cases/storybook/storybook_with_default_by_wgpu.png b/test_cases/storybook/storybook_with_default_by_wgpu.png index ef383eb24..cf6ac28b6 100644 Binary files a/test_cases/storybook/storybook_with_default_by_wgpu.png and b/test_cases/storybook/storybook_with_default_by_wgpu.png differ diff --git a/test_cases/storybook/storybook_with_material_by_wgpu.png b/test_cases/storybook/storybook_with_material_by_wgpu.png index 236373d4f..70e31a7e7 100644 Binary files a/test_cases/storybook/storybook_with_material_by_wgpu.png and b/test_cases/storybook/storybook_with_material_by_wgpu.png differ diff --git a/test_cases/todos/todos_with_default_by_wgpu.png b/test_cases/todos/todos_with_default_by_wgpu.png index 0c8a017de..7808708a2 100644 Binary files a/test_cases/todos/todos_with_default_by_wgpu.png and b/test_cases/todos/todos_with_default_by_wgpu.png differ diff --git a/test_cases/todos/todos_with_material_by_wgpu.png b/test_cases/todos/todos_with_material_by_wgpu.png index fce70df59..314cad524 100644 Binary files a/test_cases/todos/todos_with_material_by_wgpu.png and b/test_cases/todos/todos_with_material_by_wgpu.png differ diff --git a/tests/assets/fill_with_gradient.svg b/tests/assets/fill_with_gradient.svg new file mode 100644 index 000000000..7723165eb --- /dev/null +++ b/tests/assets/fill_with_gradient.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/include_svg_test.rs b/tests/include_svg_test.rs index e63a91416..35eb415c8 100644 --- a/tests/include_svg_test.rs +++ b/tests/include_svg_test.rs @@ -3,5 +3,5 @@ use ribir::prelude::{include_svg, Svg}; #[test] fn include_svg() { let svg: Svg = include_svg!("./assets/test1.svg"); - assert_eq!(svg.paths.len(), 2); + assert_eq!(svg.paint_commands.len(), 2); } diff --git a/text/Cargo.toml b/text/Cargo.toml index 31023eda8..14b9698d2 100644 --- a/text/Cargo.toml +++ b/text/Cargo.toml @@ -27,6 +27,9 @@ rustybuzz.workspace = true unicode-bidi.workspace = true unicode-script.workspace = true unicode-segmentation.workspace = true +quick-xml.workspace = true +ahash.workspace = true + [features] default = ["raster_png_font"] diff --git a/text/src/font_db.rs b/text/src/font_db.rs index 4937437d1..1c82e48cc 100644 --- a/text/src/font_db.rs +++ b/text/src/font_db.rs @@ -1,10 +1,14 @@ use fontdb::{Database, Query}; pub use fontdb::{FaceInfo, Family, ID}; use lyon_path::math::Point; +use ribir_algo::ShareResource; use ribir_painter::{PixelImage, Svg}; use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder}; +use std::cell::RefCell; +use std::rc::Rc; use std::{collections::HashMap, ops::Deref, sync::Arc}; +use crate::svg_font_document::SvgDocument; use crate::{FontFace, FontFamily}; /// A wrapper of fontdb and cache font data. pub struct FontDB { @@ -13,12 +17,18 @@ pub struct FontDB { cache: HashMap>, } +type FontGlyphCache = Rc>>>; #[derive(Clone)] pub struct Face { pub face_id: ID, pub source_data: Arc + Sync + Send>, pub face_data_index: u32, pub rb_face: rustybuzz::Face<'static>, + #[cfg(feature = "raster_png_font")] + raster_image_glyphs: FontGlyphCache>, + outline_glyphs: FontGlyphCache, + svg_glyphs: FontGlyphCache, + svg_docs: FontGlyphCache<*const u8, SvgDocument>, } impl FontDB { @@ -263,6 +273,11 @@ impl Face { face_data_index: face_index, rb_face, face_id, + outline_glyphs: <_>::default(), + #[cfg(feature = "raster_png_font")] + raster_image_glyphs: <_>::default(), + svg_glyphs: <_>::default(), + svg_docs: <_>::default(), }) } @@ -273,35 +288,65 @@ impl Face { // todo: should return its tight bounds pub fn outline_glyph(&self, glyph_id: GlyphId) -> Option { - let mut builder = GlyphOutlineBuilder::default(); - let rect = self - .rb_face - .outline_glyph(glyph_id, &mut builder as &mut dyn OutlineBuilder); - rect.map(move |_| builder.into_path()) + self + .outline_glyphs + .borrow_mut() + .entry(glyph_id) + .or_insert_with(|| { + let mut builder = GlyphOutlineBuilder::default(); + let rect = self + .rb_face + .outline_glyph(glyph_id, &mut builder as &mut dyn OutlineBuilder); + rect.map(move |_| builder.into_path()) + }) + .as_ref() + .cloned() } #[cfg(feature = "raster_png_font")] - pub fn glyph_raster_image(&self, glyph_id: GlyphId, pixels_per_em: u16) -> Option { + pub fn glyph_raster_image( + &self, + glyph_id: GlyphId, + pixels_per_em: u16, + ) -> Option> { use rustybuzz::ttf_parser::RasterImageFormat; self - .rb_face - .glyph_raster_image(glyph_id, pixels_per_em) - .and_then(|img| { - if img.format == RasterImageFormat::PNG { - Some(PixelImage::from_png(img.data)) - } else { - None - } + .raster_image_glyphs + .borrow_mut() + .entry(glyph_id) + .or_insert_with(|| { + self + .rb_face + .glyph_raster_image(glyph_id, pixels_per_em) + .and_then(|img| { + if img.format == RasterImageFormat::PNG { + Some(ShareResource::new(PixelImage::from_png(img.data))) + } else { + None + } + }) }) + .clone() } - pub fn glyph_svg_image(&self, _: GlyphId) -> Option { - None - // todo: need to extract glyph svg image, but the svg parse cost too long. - // self - // .rb_face - // .glyph_svg_image(glyph_id) - // .and_then(|data| Svg::parse_from_bytes(data).ok()) + pub fn glyph_svg_image(&self, glyph_id: GlyphId) -> Option { + self + .svg_glyphs + .borrow_mut() + .entry(glyph_id) + .or_insert_with(|| { + self.rb_face.glyph_svg_image(glyph_id).and_then(|data| { + self + .svg_docs + .borrow_mut() + .entry(&data[0] as *const u8) + .or_insert_with(|| SvgDocument::parse(unsafe { std::str::from_utf8_unchecked(data) })) + .as_ref() + .and_then(|doc| doc.glyph_svg(glyph_id, self)) + .and_then(|content| Svg::parse_from_bytes(content.as_bytes()).ok()) + }) + }) + .clone() } #[inline] diff --git a/text/src/lib.rs b/text/src/lib.rs index 7837963d6..ad8622143 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -21,6 +21,7 @@ mod typography_store; pub use typography_store::{TypographyStore, VisualGlyphs}; mod text_render; pub use text_render::{draw_glyphs, draw_glyphs_in_rect, TextStyle}; +mod svg_font_document; mod text_writer; pub use text_writer::{ diff --git a/text/src/svg_font_document.rs b/text/src/svg_font_document.rs new file mode 100644 index 000000000..b78929adc --- /dev/null +++ b/text/src/svg_font_document.rs @@ -0,0 +1,231 @@ +use log::warn; +use quick_xml::events::attributes::Attribute; +use quick_xml::events::{BytesStart, Event}; +use quick_xml::name::QName; +use quick_xml::reader::Reader; +use rustybuzz::ttf_parser::GlyphId; + +use ahash::{HashMap, HashSet}; +use std::borrow::Cow; + +use std::io::prelude::*; + +use crate::font_db::Face; + +pub(crate) struct SvgDocument { + elems: HashMap, +} + +impl SvgDocument { + pub(crate) fn parse(content: &str) -> Option { + let mut reader = Reader::from_str(content); + let mut buf = Vec::new(); + let mut doc = Self { elems: HashMap::default() }; + loop { + match reader.read_event_into(&mut buf) { + Ok(ref e @ Event::Start(ref tag)) | Ok(ref e @ Event::Empty(ref tag)) => { + if tag.name() != QName(b"defs") { + let has_child = matches!(e, Event::Start(_)); + doc.collect_named_obj(&mut reader, content, tag, has_child); + } + } + Ok(Event::Eof) => break, // exits the loop when reaching end of file + Err(e) => { + warn!("Error at position {}: {:?}", reader.buffer_position(), e); + return None; + } + + _ => (), // There are several other `Event`s we do not consider here + } + } + Some(doc) + } + + pub fn glyph_svg(&self, glyph: GlyphId, face: &Face) -> Option { + let key = format!("glyph{}", glyph.0); + if !self.elems.contains_key(&key) { + return None; + } + + let mut all_links = HashSet::default(); + let mut elems = vec![key.clone()]; + + while let Some(curr) = elems.pop() { + if let Some(content) = self.elems.get(&curr) { + elems.extend(Self::collect_link(content, &mut all_links)); + } + } + + let units_per_em = face.units_per_em() as i32; + let ascender = face.rb_face.ascender() as i32; + let mut writer = std::io::Cursor::new(Vec::new()); + + writer.write_all(format!( + "", + units_per_em, units_per_em, + 0, -ascender, units_per_em, units_per_em + ).as_bytes()).ok()?; + writer.write_all("".as_bytes()).ok()?; + for link in all_links { + if let Some(content) = self.elems.get(&link) { + writer.write_all(content.as_bytes()).ok()?; + } + } + writer.write_all("".as_bytes()).ok()?; + writer + .write_all(self.elems.get(&key).unwrap().as_bytes()) + .ok()?; + writer.write_all("".as_bytes()).ok()?; + + Some( + std::str::from_utf8(&writer.into_inner()) + .unwrap() + .to_string(), + ) + } + + fn collect_named_obj( + &mut self, + reader: &mut Reader<&[u8]>, + source: &str, + e: &BytesStart, + has_children: bool, + ) { + if let Some(id) = e + .attributes() + .find(|a| a.as_ref().map_or(false, |a| a.key == QName(b"id"))) + .map(|a| a.unwrap().value) + { + unsafe { + let content = Self::extra_elem(reader, e, source, has_children); + self + .elems + .insert(std::str::from_utf8_unchecked(&id).to_string(), content); + } + }; + } + + unsafe fn extra_elem( + reader: &mut Reader<&[u8]>, + e: &BytesStart, + source: &str, + has_children: bool, + ) -> String { + let content = if has_children { + let mut buf = Vec::new(); + let rg = reader + .read_to_end_into(e.name().to_owned(), &mut buf) + .unwrap(); + &source[rg.start..rg.end] + } else { + "" + }; + + let name = e.name(); + let name = reader.decoder().decode(name.as_ref()).unwrap(); + + format!( + "<{}>{}", + std::str::from_utf8_unchecked(e), + content, + name + ) + } + + fn collect_link(content: &str, all_links: &mut HashSet) -> Vec { + let mut reader = Reader::from_str(content); + let mut buf = Vec::new(); + let mut new_links = Vec::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { + Self::collect_link_from_attrs(e, all_links, &mut new_links); + } + Ok(Event::Eof) => break, // exits the loop when reaching end of file + Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), + + _ => (), // There are several other `Event`s we do not consider here + } + } + new_links + } + + #[inline] + fn extra_link_from_iri_func(val: Cow<'_, [u8]>) -> Option { + let val: &str = std::str::from_utf8(&val) + .unwrap() + .trim() + .strip_prefix("url(")? + .trim_start() + .strip_prefix('#')? + .strip_suffix(')')?; + Some(val.to_string()) + } + + #[inline] + fn extra_link_from_href(attr: &Attribute) -> Option { + if attr.key == QName(b"xlink:href") || attr.key == QName(b"href") { + let href = std::str::from_utf8(&attr.value).unwrap(); + return Some(href.trim().strip_prefix('#')?.to_string()); + } + None + } + + fn collect_link_from_attrs( + elem: &BytesStart, + all_links: &mut HashSet, + new_links: &mut Vec, + ) { + let attributes = elem.attributes(); + + attributes.for_each(|attr| { + let attr = attr.unwrap(); + if let Some(link) = + Self::extra_link_from_href(&attr).or_else(|| Self::extra_link_from_iri_func(attr.value)) + { + if all_links.contains(&link) { + return; + } + all_links.insert(link.clone()); + new_links.push(link); + } + }); + } +} + +#[cfg(test)] +mod tests { + use rustybuzz::ttf_parser::GlyphId; + + use crate::font_db::FontDB; + + #[test] + fn test_svg_document() { + let content = r##" + + + + + + + + + + + + + "##; + let doc = super::SvgDocument::parse(content).unwrap(); + let mut db = FontDB::default(); + let dummy_face = db.face_data_or_insert(db.default_font()).unwrap(); + assert_eq!(doc.elems.len(), 4); + assert!(doc.glyph_svg(GlyphId(2428), dummy_face).is_some()); + assert!(doc.glyph_svg(GlyphId(0), dummy_face).is_none()); + } +} diff --git a/text/src/text_render.rs b/text/src/text_render.rs index 3a45d68c3..8c6e8a28c 100644 --- a/text/src/text_render.rs +++ b/text/src/text_render.rs @@ -1,5 +1,4 @@ use crate::{font_db::FontDB, Em, FontFace, FontSize, GlyphBound, Pixel, VisualGlyphs}; -use ribir_algo::ShareResource; use ribir_geom::{Rect, Size}; use ribir_painter::{Brush, Painter, Path, PathPaintStyle}; use std::{cell::RefCell, rc::Rc}; @@ -94,10 +93,19 @@ pub fn draw_glyphs( } } else if let Some(svg) = face.glyph_svg_image(g.glyph_id) { let mut painter = painter.save_guard(); + + let grid_scale = face + .vertical_height() + .map(|h| h as f32 / face.units_per_em() as f32) + .unwrap_or(1.) + .max(1.); + let size = svg.size; + let bound_size = g.bound.size; + let scale = + (bound_size.width / size.width).min(bound_size.height / size.height) / grid_scale; painter - .translate(0., unit) - .scale(scale, scale) .translate(g.bound.min_x(), g.bound.min_y()) + .scale(scale, scale) .draw_svg(&svg); } else if let Some(img) = face.glyph_raster_image(g.glyph_id, (unit / font_size) as u16) { let m_width = img.width() as f32; @@ -110,11 +118,7 @@ pub fn draw_glyphs( painter .translate(x_offset, y_offset) .scale(scale, scale) - .draw_img( - ShareResource::new(img), - &Rect::from_size(Size::new(m_width, m_height)), - &None, - ); + .draw_img(img, &Rect::from_size(Size::new(m_width, m_height)), &None); } } });