From b8a7272fec8d47fd4ab6883c2902e9c0533ad5e5 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Tue, 7 Oct 2025 01:23:43 -0600 Subject: [PATCH 1/7] add tiny skia backend --- Cargo.lock | 180 +++- Cargo.toml | 3 + crates/anyrender_tiny_skia/Cargo.toml | 39 + .../anyrender_tiny_skia/src/image_renderer.rs | 95 ++ crates/anyrender_tiny_skia/src/lib.rs | 15 + crates/anyrender_tiny_skia/src/scene.rs | 895 ++++++++++++++++++ .../src/window_renderer.rs | 13 + crates/anyrender_vello/src/window_renderer.rs | 2 +- examples/bunnymark/Cargo.toml | 1 + examples/bunnymark/src/main.rs | 20 + examples/winit/Cargo.toml | 1 + examples/winit/src/main.rs | 21 +- 12 files changed, 1274 insertions(+), 11 deletions(-) create mode 100644 crates/anyrender_tiny_skia/Cargo.toml create mode 100644 crates/anyrender_tiny_skia/src/image_renderer.rs create mode 100644 crates/anyrender_tiny_skia/src/lib.rs create mode 100644 crates/anyrender_tiny_skia/src/scene.rs create mode 100644 crates/anyrender_tiny_skia/src/window_renderer.rs diff --git a/Cargo.lock b/Cargo.lock index a05f0fa..17689c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "anyrender" version = "0.6.1" @@ -97,7 +103,25 @@ dependencies = [ "kurbo 0.12.0", "peniko", "thiserror 2.0.17", - "usvg", + "usvg 0.45.1", +] + +[[package]] +name = "anyrender_tiny_skia" +version = "0.6.0" +dependencies = [ + "anyhow", + "anyrender", + "debug_timer", + "kurbo 0.12.0", + "peniko", + "pixels_window_renderer", + "raw-window-handle", + "resvg", + "softbuffer", + "softbuffer_window_renderer", + "swash", + "tiny-skia", ] [[package]] @@ -273,6 +297,7 @@ name = "bunnymark" version = "0.1.0" dependencies = [ "anyrender", + "anyrender_tiny_skia", "anyrender_vello", "anyrender_vello_cpu", "anyrender_vello_hybrid", @@ -807,6 +832,20 @@ dependencies = [ "roxmltree", ] +[[package]] +name = "fontdb" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.21.1", +] + [[package]] name = "fontdb" version = "0.23.0" @@ -818,7 +857,7 @@ dependencies = [ "memmap2", "slotmap", "tinyvec", - "ttf-parser", + "ttf-parser 0.25.1", ] [[package]] @@ -1137,6 +1176,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + [[package]] name = "imagesize" version = "0.13.0" @@ -1185,6 +1230,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + [[package]] name = "js-sys" version = "0.3.81" @@ -1774,7 +1825,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ - "ttf-parser", + "ttf-parser 0.25.1", ] [[package]] @@ -2088,6 +2139,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "resvg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" +dependencies = [ + "gif", + "jpeg-decoder", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg 0.42.0", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -2132,6 +2208,22 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.9.4", + "bytemuck", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring 0.2.0", + "unicode-ccc 0.2.0", + "unicode-properties", + "unicode-script", +] + [[package]] name = "rustybuzz" version = "0.20.1" @@ -2143,9 +2235,9 @@ dependencies = [ "core_maths", "log", "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", + "ttf-parser 0.25.1", + "unicode-bidi-mirroring 0.4.0", + "unicode-ccc 0.4.0", "unicode-properties", "unicode-script", ] @@ -2396,6 +2488,17 @@ dependencies = [ "siphasher", ] +[[package]] +name = "swash" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + [[package]] name = "syn" version = "1.0.109" @@ -2487,6 +2590,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", + "png 0.17.16", "tiny-skia-path", ] @@ -2575,6 +2679,12 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + [[package]] name = "ttf-parser" version = "0.25.1" @@ -2599,12 +2709,24 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + [[package]] name = "unicode-bidi-mirroring" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + [[package]] name = "unicode-ccc" version = "0.4.0" @@ -2659,6 +2781,33 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "usvg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb 0.18.0", + "imagesize 0.12.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree", + "rustybuzz 0.14.1", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "usvg" version = "0.45.1" @@ -2668,13 +2817,13 @@ dependencies = [ "base64", "data-url", "flate2", - "fontdb", - "imagesize", + "fontdb 0.23.0", + "imagesize 0.13.0", "kurbo 0.11.3", "log", "pico-args", "roxmltree", - "rustybuzz", + "rustybuzz 0.20.1", "simplecss", "siphasher", "strict-num", @@ -3638,6 +3787,7 @@ name = "winit-example" version = "0.1.0" dependencies = [ "anyrender", + "anyrender_tiny_skia", "anyrender_vello", "anyrender_vello_cpu", "anyrender_vello_hybrid", @@ -3731,6 +3881,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + [[package]] name = "zerocopy" version = "0.8.27" diff --git a/Cargo.toml b/Cargo.toml index 2fa9a87..fa698a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/anyrender_vello_cpu", "crates/anyrender_vello_hybrid", "crates/anyrender_svg", + "crates/anyrender_tiny_skia", "crates/wgpu_context", "crates/pixels_window_renderer", "crates/softbuffer_window_renderer", @@ -29,6 +30,7 @@ anyrender_vello = { version = "0.6.0", path = "./crates/anyrender_vello" } anyrender_vello_cpu = { version = "0.8.0", path = "./crates/anyrender_vello_cpu" } anyrender_vello_hybrid = { version = "0.1.0", path = "./crates/anyrender_vello_hybrid" } anyrender_svg = { version = "0.6.0", path = "./crates/anyrender_svg" } +anyrender_tiny_skia = { version = "0.6.0", path = "./crates/anyrender_tiny_skia" } wgpu_context = { version = "0.1.1", path = "./crates/wgpu_context" } pixels_window_renderer = { version = "0.1.0", path = "./crates/pixels_window_renderer" } softbuffer_window_renderer = { version = "0.1.0", path = "./crates/softbuffer_window_renderer" } @@ -59,6 +61,7 @@ rustc-hash = "1.1.0" futures-util = "0.3.30" futures-intrusive = "0.5.0" pollster = "0.4" +swash = "0.2.5" # Dev-dependencies winit = { version = "0.30.2", features = ["rwh_06"] } \ No newline at end of file diff --git a/crates/anyrender_tiny_skia/Cargo.toml b/crates/anyrender_tiny_skia/Cargo.toml new file mode 100644 index 0000000..8463d39 --- /dev/null +++ b/crates/anyrender_tiny_skia/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "anyrender_tiny_skia" +description = "tiny-skia backend for anyrender" +version = "0.6.0" +documentation = "https://docs.rs/anyrender_tiny_skia" +homepage.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +anyrender = { workspace = true } + +anyhow = "1.0" +debug_timer = { workspace = true } +kurbo = { workspace = true } +peniko = { workspace = true } +tiny-skia = "0.11" +resvg = "0.42" +softbuffer = "0.4" +raw-window-handle = "0.6" +swash = { workspace = true } + +# WindowRenderer backends +softbuffer_window_renderer = { workspace = true, optional = true } +pixels_window_renderer = { workspace = true, optional = true } + +[features] +pixels_window_renderer = ["dep:pixels_window_renderer"] +softbuffer_window_renderer = ["dep:softbuffer_window_renderer"] +log_frame_times = [ + "debug_timer/enable", + "softbuffer_window_renderer?/log_frame_times", + "pixels_window_renderer?/log_frame_times", +] + +[package.metadata.docs.rs] +features = ["pixels_window_renderer", "softbuffer_window_renderer"] + diff --git a/crates/anyrender_tiny_skia/src/image_renderer.rs b/crates/anyrender_tiny_skia/src/image_renderer.rs new file mode 100644 index 0000000..d7a898a --- /dev/null +++ b/crates/anyrender_tiny_skia/src/image_renderer.rs @@ -0,0 +1,95 @@ +use anyrender::{ImageRenderer, PaintScene}; +use debug_timer::debug_timer; +use tiny_skia::{Mask, Pixmap}; + +use crate::TinySkiaScenePainter; + +pub struct TinySkiaImageRenderer { + pub(crate) scene: TinySkiaScenePainter, +} + +impl ImageRenderer for TinySkiaImageRenderer { + type ScenePainter<'a> = TinySkiaScenePainter; + + fn new(width: u32, height: u32) -> Self { + Self { + scene: TinySkiaScenePainter::new(width, height), + } + } + + fn resize(&mut self, width: u32, height: u32) { + if !self.scene.layers.is_empty() { + self.scene.layers[0].pixmap = + Pixmap::new(width, height).expect("Failed to create pixmap"); + self.scene.layers[0].mask = Mask::new(width, height).expect("Failed to create mask"); + } else { + self.scene = TinySkiaScenePainter::new(width, height); + } + } + + fn reset(&mut self) { + self.scene.reset(); + } + + fn render_to_vec)>( + &mut self, + draw_fn: F, + vec: &mut Vec, + ) { + vec.clear(); + vec.reserve( + (self.scene.layers[0].pixmap.width() * self.scene.layers[0].pixmap.height() * 4) + as usize, + ); + + let painter = &mut self.scene; + + painter.reset(); + draw_fn(painter); + + // Convert pixmap to RGBA8 + for pixel in self.scene.layers[0].pixmap.pixels() { + vec.push(pixel.red()); + vec.push(pixel.green()); + vec.push(pixel.blue()); + vec.push(pixel.alpha()); + } + } + + fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]) { + debug_timer!(timer, feature = "log_frame_times"); + let painter = &mut self.scene; + painter.reset(); + draw_fn(painter); + timer.record_time("cmd"); + + if let Some(layer) = self.scene.layers.first() { + let pixmap = &layer.pixmap; + let width = pixmap.width() as usize; + let height = pixmap.height() as usize; + let expected_len = width * height * 4; + + assert!( + buffer.len() >= expected_len, + "buffer too small: {} < {}", + buffer.len(), + expected_len + ); + + let pixels = pixmap.pixels(); + + buffer[..expected_len] + .chunks_exact_mut(4) + .zip(pixels.iter()) + .for_each(|(chunk, pixel)| { + chunk[0] = pixel.red(); + chunk[1] = pixel.green(); + chunk[2] = pixel.blue(); + chunk[3] = pixel.alpha(); + }); + } + + timer.record_time("render"); + timer.print_times("tiny-skia image: "); + } +} diff --git a/crates/anyrender_tiny_skia/src/lib.rs b/crates/anyrender_tiny_skia/src/lib.rs new file mode 100644 index 0000000..dc30b86 --- /dev/null +++ b/crates/anyrender_tiny_skia/src/lib.rs @@ -0,0 +1,15 @@ +//! A [`tiny skia`] backend for the [`anyrender`] 2D drawing abstraction +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod image_renderer; +mod scene; +mod window_renderer; + +pub use image_renderer::TinySkiaImageRenderer; +pub use scene::TinySkiaScenePainter; + +#[cfg(any( + feature = "pixels_window_renderer", + feature = "softbuffer_window_renderer" +))] +pub use window_renderer::*; diff --git a/crates/anyrender_tiny_skia/src/scene.rs b/crates/anyrender_tiny_skia/src/scene.rs new file mode 100644 index 0000000..389a97b --- /dev/null +++ b/crates/anyrender_tiny_skia/src/scene.rs @@ -0,0 +1,895 @@ +//! A [`tiny-skia`] backend for the [`anyrender`] 2D drawing abstraction + +use anyhow::{Result, anyhow}; +use anyrender::{ + ImageRenderer, NormalizedCoord, Paint as AnyRenderPaint, PaintRef, PaintScene, WindowHandle, + WindowRenderer, +}; +use debug_timer::debug_timer; +use kurbo::{Affine, PathEl, Point, Rect, Shape}; +use peniko::{ + BlendMode, BrushRef, Color, Compose, Fill, FontData, GradientKind, ImageBrushRef, Mix, + StyleRef, color::palette, +}; +use resvg::tiny_skia::StrokeDash; +use softbuffer::{Context, Surface}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::num::NonZeroU32; +use std::rc::Rc; +use std::sync::Arc; +use swash::{ + FontRef, GlyphId, + scale::{Render, ScaleContext, Source, StrikeWith, image::Content as SwashContent}, + zeno::Format, +}; +use tiny_skia::{ + self, FillRule, FilterQuality, GradientStop, LineCap, LineJoin, LinearGradient, Mask, MaskType, + Paint, Path, PathBuilder, Pattern, Pixmap, RadialGradient, Shader, SpreadMode, Stroke, + Transform, +}; + +thread_local! { + #[allow(clippy::type_complexity)] + static IMAGE_CACHE: RefCell, (CacheColor, Rc)>> = RefCell::new(HashMap::new()); + #[allow(clippy::type_complexity)] + // The `u32` is a color encoded as a u32 so that it is hashable and eq. + static GLYPH_CACHE: RefCell>)>> = RefCell::new(HashMap::new()); + static SWASH_SCALER: RefCell = RefCell::new(ScaleContext::new()); +} + +fn cache_image(cache_color: CacheColor, image: &ImageBrushRef) -> Option> { + let data_key = image.image.data.data().to_vec(); + + if let Some(cached_pixmap) = IMAGE_CACHE.with_borrow_mut(|ic| { + if let Some((color, pixmap)) = ic.get_mut(&data_key) { + *color = cache_color; + Some(pixmap.clone()) + } else { + None + } + }) { + return Some(cached_pixmap); + } + + // Convert peniko ImageData to tiny-skia Pixmap + let pixmap = match image.image.format { + peniko::ImageFormat::Rgba8 => { + let mut pixmap = Pixmap::new(image.image.width, image.image.height)?; + let data = image.image.data.data(); + + for (i, chunk) in data.chunks_exact(4).enumerate() { + if let [r, g, b, a] = chunk { + let color = tiny_skia::ColorU8::from_rgba(*r, *g, *b, *a); + let x = (i as u32) % image.image.width; + let y = (i as u32) / image.image.width; + if x < image.image.width && y < image.image.height { + pixmap.pixels_mut()[i] = color.premultiply(); + } + } + } + + Some(Rc::new(pixmap)) + } + _ => None, // Other formats not supported yet + }; + + if let Some(pixmap) = pixmap.clone() { + IMAGE_CACHE.with_borrow_mut(|ic| { + ic.insert(data_key, (cache_color, pixmap)); + }); + } + + pixmap +} + +fn cache_glyph( + cache_color: CacheColor, + glyph_id: GlyphId, + font_size: f32, + color: Color, + font: &FontData, +) -> Option> { + let c = color.to_rgba8(); + // Create a simple cache key using glyph_id and font_size as u32 bits + let cache_key = (glyph_id, font_size.to_bits()); + + if let Some(opt_glyph) = GLYPH_CACHE.with_borrow_mut(|gc| { + if let Some((color, glyph)) = gc.get_mut(&(cache_key, c.to_u32())) { + *color = cache_color; + Some(glyph.clone()) + } else { + None + } + }) { + return opt_glyph; + }; + + let image = SWASH_SCALER.with_borrow_mut(|context| { + let font_ref = FontRef::from_index(font.data.as_ref(), font.index as usize)?; + let mut scaler = context.builder(font_ref).size(font_size).build(); + + Render::new(&[ + Source::ColorOutline(0), + Source::ColorBitmap(StrikeWith::BestFit), + Source::Outline, + ]) + .format(Format::Alpha) + .render(&mut scaler, glyph_id) + })?; + + let result = if image.placement.width == 0 || image.placement.height == 0 { + // We can't create an empty `Pixmap` + None + } else { + let mut pixmap = Pixmap::new(image.placement.width, image.placement.height)?; + + if image.content == SwashContent::Mask { + for (a, &alpha) in pixmap.pixels_mut().iter_mut().zip(image.data.iter()) { + *a = tiny_skia::Color::from_rgba8(c.r, c.g, c.b, alpha) + .premultiply() + .to_color_u8(); + } + } else if image.content == SwashContent::Color { + for (a, b) in pixmap.pixels_mut().iter_mut().zip(image.data.chunks(4)) { + *a = tiny_skia::Color::from_rgba8(b[0], b[1], b[2], b[3]) + .premultiply() + .to_color_u8(); + } + } else { + return None; + } + + Some(Rc::new(Glyph { + pixmap, + left: image.placement.left as f32, + top: image.placement.top as f32, + })) + }; + + GLYPH_CACHE + .with_borrow_mut(|gc| gc.insert((cache_key, c.to_u32()), (cache_color, result.clone()))); + + result +} + +macro_rules! try_ret { + ($e:expr) => { + if let Some(e) = $e { + e + } else { + return; + } + }; +} + +struct Glyph { + pixmap: Pixmap, + left: f32, + top: f32, +} + +#[derive(PartialEq, Clone, Copy)] +struct CacheColor(bool); + +pub(crate) struct Layer { + pub(crate) pixmap: Pixmap, + /// clip is stored with the transform at the time clip is called + pub(crate) clip: Option, + pub(crate) mask: Mask, + /// this transform should generally only be used when making a draw call to skia + transform: Affine, + // the transform that the layer was pushed with that will be used when applying the layer + combine_transform: Affine, + blend_mode: BlendMode, + alpha: f32, + cache_color: CacheColor, +} +impl Layer { + /// the img_rect should already be in the correct transformed space along with the window_scale applied + fn clip_rect(&self, img_rect: Rect) -> Option { + if let Some(clip) = self.clip { + let clip = clip.intersect(img_rect); + to_skia_rect(clip) + } else { + to_skia_rect(img_rect) + } + } + + /// Renders the pixmap at the position and transforms it with the given transform. + /// x and y should have already been scaled by the window scale + fn render_pixmap_direct(&mut self, img_pixmap: &Pixmap, x: f32, y: f32, transform: Affine) { + let img_rect = Rect::from_origin_size( + (x, y), + (img_pixmap.width() as f64, img_pixmap.height() as f64), + ); + let paint = Paint { + shader: Pattern::new( + img_pixmap.as_ref(), + SpreadMode::Pad, + FilterQuality::Nearest, + 1.0, + Transform::from_translate(x, y), + ), + ..Default::default() + }; + + let transform = transform.as_coeffs(); + let transform = Transform::from_row( + transform[0] as f32, + transform[1] as f32, + transform[2] as f32, + transform[3] as f32, + transform[4] as f32, + transform[5] as f32, + ); + if let Some(rect) = self.clip_rect(img_rect) { + self.pixmap.fill_rect(rect, &paint, transform, None); + } + } + + fn render_pixmap_rect(&mut self, pixmap: &Pixmap, rect: tiny_skia::Rect) { + let paint = Paint { + shader: Pattern::new( + pixmap.as_ref(), + SpreadMode::Pad, + FilterQuality::Bilinear, + 1.0, + Transform::from_scale( + rect.width() / pixmap.width() as f32, + rect.height() / pixmap.height() as f32, + ), + ), + ..Default::default() + }; + + self.pixmap.fill_rect( + rect, + &paint, + self.skia_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } + + fn render_pixmap_with_paint( + &mut self, + pixmap: &Pixmap, + rect: tiny_skia::Rect, + paint: Option>, + ) { + let paint = if let Some(paint) = paint { + paint + } else { + return self.render_pixmap_rect(pixmap, rect); + }; + + let mut colored_bg = try_ret!(Pixmap::new(pixmap.width(), pixmap.height())); + colored_bg.fill_rect( + try_ret!(tiny_skia::Rect::from_xywh( + 0.0, + 0.0, + pixmap.width() as f32, + pixmap.height() as f32 + )), + &paint, + Transform::identity(), + None, + ); + + let mask = Mask::from_pixmap(pixmap.as_ref(), MaskType::Alpha); + colored_bg.apply_mask(&mask); + + self.render_pixmap_rect(&colored_bg, rect); + } + + fn skia_transform(&self) -> Transform { + skia_transform(self.transform, 1.) + } +} +impl Layer { + /// The combine transform should be the transform that the layer is pushed with without combining with the previous transform. It will be used when combining layers to offset/transform this layer into the parent with the parent transform + fn new( + blend: impl Into, + alpha: f32, + combine_transform: Affine, + clip: &impl Shape, + cache_color: CacheColor, + ) -> Result { + let transform = Affine::IDENTITY; + let bbox = clip.bounding_box(); + let scaled_box = bbox; + let width = scaled_box.width() as u32; + let height = scaled_box.height() as u32; + let mut mask = Mask::new(width, height).ok_or_else(|| anyhow!("unable to create mask"))?; + mask.fill_path( + &shape_to_path(clip).ok_or_else(|| anyhow!("unable to create clip shape"))?, + FillRule::Winding, + false, + Transform::identity(), + ); + Ok(Self { + pixmap: Pixmap::new(width, height).ok_or_else(|| anyhow!("unable to create pixmap"))?, + mask, + clip: Some(bbox), + transform, + combine_transform, + blend_mode: blend.into(), + alpha, + cache_color, + }) + } + + fn transform(&mut self, transform: Affine) { + self.transform *= transform; + } + + fn clip(&mut self, shape: &impl Shape) { + self.clip = Some(self.transform.transform_rect_bbox(shape.bounding_box())); + let path = try_ret!(shape_to_path(shape)); + self.mask.clear(); + self.mask + .fill_path(&path, FillRule::Winding, false, self.skia_transform()); + } + + fn clear_clip(&mut self) { + self.clip = None; + } + + fn stroke<'b, 's>( + &mut self, + shape: &impl Shape, + brush: impl Into>, + stroke: &'s peniko::kurbo::Stroke, + ) { + let paint = try_ret!(brush_to_paint(brush)); + let path = try_ret!(shape_to_path(shape)); + let line_cap = match stroke.end_cap { + peniko::kurbo::Cap::Butt => LineCap::Butt, + peniko::kurbo::Cap::Square => LineCap::Square, + peniko::kurbo::Cap::Round => LineCap::Round, + }; + let line_join = match stroke.join { + peniko::kurbo::Join::Bevel => LineJoin::Bevel, + peniko::kurbo::Join::Miter => LineJoin::Miter, + peniko::kurbo::Join::Round => LineJoin::Round, + }; + let stroke = Stroke { + width: stroke.width as f32, + miter_limit: stroke.miter_limit as f32, + line_cap, + line_join, + dash: (!stroke.dash_pattern.is_empty()) + .then_some(StrokeDash::new( + stroke.dash_pattern.iter().map(|v| *v as f32).collect(), + stroke.dash_offset as f32, + )) + .flatten(), + }; + self.pixmap.stroke_path( + &path, + &paint, + &stroke, + self.skia_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } + + fn fill<'b>(&mut self, shape: &impl Shape, brush: impl Into>, _blur_radius: f64) { + // FIXME: Handle _blur_radius + let brush: BrushRef<'_> = brush.into(); + + // Handle images specially + if let BrushRef::Image(image) = brush { + if let Some(cached_pixmap) = cache_image(self.cache_color, &image) { + // Create a pattern from the cached pixmap + let pattern = Pattern::new( + cached_pixmap.as_ref().as_ref(), + SpreadMode::Pad, + FilterQuality::Nearest, + 1.0, + Transform::identity(), + ); + let paint = Paint { + shader: pattern, + ..Default::default() + }; + + if let Some(rect) = shape.as_rect() { + let rect = try_ret!(to_skia_rect(rect)); + self.pixmap.fill_rect( + rect, + &paint, + self.skia_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } else { + let path = try_ret!(shape_to_path(shape)); + self.pixmap.fill_path( + &path, + &paint, + FillRule::Winding, + self.skia_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } + } + } else { + // Handle non-image brushes + let paint = try_ret!(brush_to_paint(brush)); + if let Some(rect) = shape.as_rect() { + let rect = try_ret!(to_skia_rect(rect)); + self.pixmap.fill_rect( + rect, + &paint, + self.skia_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } else { + let path = try_ret!(shape_to_path(shape)); + self.pixmap.fill_path( + &path, + &paint, + FillRule::Winding, + self.skia_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } + } + } + + fn draw_glyph( + &mut self, + glyph_id: GlyphId, + font_size: f32, + color: Color, + font: &FontData, + x: f32, + y: f32, + ) { + if let Some(cached_glyph) = cache_glyph(self.cache_color, glyph_id, font_size, color, font) + { + self.render_pixmap_direct( + &cached_glyph.pixmap, + x + cached_glyph.left, + y - cached_glyph.top, + self.transform, + ); + } + } +} + +pub struct TinySkiaScenePainter { + pub(crate) layers: Vec, + cache_color: CacheColor, +} + +impl TinySkiaScenePainter { + pub fn new(width: u32, height: u32) -> Self { + let mut layers = vec![]; + if let (Some(pixmap), Some(mask)) = (Pixmap::new(width, height), Mask::new(width, height)) { + let main_layer = Layer { + pixmap, + mask, + clip: None, + alpha: 1.0, + transform: Affine::IDENTITY, + combine_transform: Affine::IDENTITY, + blend_mode: Mix::Normal.into(), + cache_color: CacheColor(false), + }; + layers.push(main_layer); + } + let cache_color = CacheColor(false); + Self { + layers, + cache_color, + } + } +} + +impl PaintScene for TinySkiaScenePainter { + fn reset(&mut self) { + if let Some(first_layer) = self.layers.last_mut() { + // first_layer.pixmap.fill(tiny_skia::Color::WHITE); + first_layer.clip = None; + first_layer.transform = Affine::IDENTITY; + } + } + + fn push_layer( + &mut self, + blend: impl Into, + alpha: f32, + transform: Affine, + clip: &impl Shape, + ) { + if let Ok(layer) = Layer::new(blend, alpha, transform, clip, self.cache_color) { + self.layers.push(layer); + } + } + + fn pop_layer(&mut self) { + if self.layers.len() <= 1 { + return; + } + let layer = self.layers.pop().unwrap(); + let parent = self.layers.last_mut().unwrap(); + apply_layer(&layer, parent); + } + + fn stroke<'b>( + &mut self, + style: &kurbo::Stroke, + transform: Affine, + brush: impl Into>, + _brush_transform: Option, + shape: &impl Shape, + ) { + if let Some(layer) = self.layers.last_mut() { + let paint_ref: PaintRef<'_> = brush.into(); + let brush_ref: BrushRef<'_> = paint_ref.into(); + + let old_transform = layer.transform; + layer.transform = old_transform * transform; + + layer.stroke(shape, brush_ref, style); + + layer.transform = old_transform; + } + } + + fn fill<'b>( + &mut self, + _style: Fill, + transform: Affine, + brush: impl Into>, + _brush_transform: Option, + shape: &impl Shape, + ) { + if let Some(layer) = self.layers.last_mut() { + let paint_ref: PaintRef<'_> = brush.into(); + let brush_ref: BrushRef<'_> = paint_ref.into(); + + let old_transform = layer.transform; + layer.transform = old_transform * transform; + + layer.fill(shape, brush_ref, 0.0); + + layer.transform = old_transform; + } + } + + fn draw_glyphs<'b, 's: 'b>( + &'s mut self, + font: &'b FontData, + font_size: f32, + _hint: bool, + _normalized_coords: &'b [NormalizedCoord], + _style: impl Into>, + brush: impl Into>, + _brush_alpha: f32, + transform: Affine, + _glyph_transform: Option, + glyphs: impl Iterator, + ) { + if let Some(layer) = self.layers.last_mut() { + let paint_ref: PaintRef<'_> = brush.into(); + let color = match paint_ref { + AnyRenderPaint::Solid(c) => c, + _ => palette::css::BLACK, + }; + + let old_transform = layer.transform; + layer.transform = old_transform * transform; + + for glyph in glyphs { + layer.draw_glyph( + glyph.id as GlyphId, + font_size, + color, + font, + glyph.x, + glyph.y, + ); + } + + layer.transform = old_transform; + } + } + + fn draw_box_shadow( + &mut self, + _transform: Affine, + _rect: Rect, + _brush: Color, + _radius: f64, + _std_dev: f64, + ) { + // TODO: Implement box shadow for tiny-skia + } +} + +fn to_color(color: Color) -> tiny_skia::Color { + let c = color.to_rgba8(); + tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a) +} + +fn to_point(point: Point) -> tiny_skia::Point { + tiny_skia::Point::from_xy(point.x as f32, point.y as f32) +} + +fn shape_to_path(shape: &impl Shape) -> Option { + let mut builder = PathBuilder::new(); + for element in shape.path_elements(0.1) { + match element { + PathEl::ClosePath => builder.close(), + PathEl::MoveTo(p) => builder.move_to(p.x as f32, p.y as f32), + PathEl::LineTo(p) => builder.line_to(p.x as f32, p.y as f32), + PathEl::QuadTo(p1, p2) => { + builder.quad_to(p1.x as f32, p1.y as f32, p2.x as f32, p2.y as f32) + } + PathEl::CurveTo(p1, p2, p3) => builder.cubic_to( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p3.x as f32, + p3.y as f32, + ), + } + } + builder.finish() +} + +fn brush_to_paint<'b>(brush: impl Into>) -> Option> { + let shader = match brush.into() { + BrushRef::Solid(c) => Shader::SolidColor(to_color(c)), + BrushRef::Gradient(g) => { + let stops = g + .stops + .iter() + .map(|s| GradientStop::new(s.offset, to_color(s.color.to_alpha_color()))) + .collect(); + match g.kind { + GradientKind::Linear(linear_pos) => LinearGradient::new( + to_point(linear_pos.start), + to_point(linear_pos.end), + stops, + SpreadMode::Pad, + Transform::identity(), + )?, + GradientKind::Radial(radial) => RadialGradient::new( + to_point(radial.start_center), + to_point(radial.end_center), + radial.end_radius, + stops, + SpreadMode::Pad, + Transform::identity(), + )?, + GradientKind::Sweep { .. } => return None, + } + } + BrushRef::Image(_) => return None, + }; + Some(Paint { + shader, + ..Default::default() + }) +} + +fn to_skia_rect(rect: Rect) -> Option { + tiny_skia::Rect::from_ltrb( + rect.x0 as f32, + rect.y0 as f32, + rect.x1 as f32, + rect.y1 as f32, + ) +} + +type TinyBlendMode = tiny_skia::BlendMode; + +enum BlendStrategy { + SinglePass(TinyBlendMode), + MultiPass { + first_pass: TinyBlendMode, + second_pass: TinyBlendMode, + }, +} + +fn determine_blend_strategy(peniko_mode: &BlendMode) -> BlendStrategy { + match (peniko_mode.mix, peniko_mode.compose) { + (Mix::Normal, compose) => BlendStrategy::SinglePass(compose_to_tiny_blend_mode(compose)), + #[allow(deprecated)] + (Mix::Clip, compose) => BlendStrategy::MultiPass { + first_pass: compose_to_tiny_blend_mode(compose), + second_pass: TinyBlendMode::Source, + }, + (mix, Compose::SrcOver) => BlendStrategy::SinglePass(mix_to_tiny_blend_mode(mix)), + (mix, compose) => BlendStrategy::MultiPass { + first_pass: compose_to_tiny_blend_mode(compose), + second_pass: mix_to_tiny_blend_mode(mix), + }, + } +} + +fn compose_to_tiny_blend_mode(compose: Compose) -> TinyBlendMode { + match compose { + Compose::Clear => TinyBlendMode::Clear, + Compose::Copy => TinyBlendMode::Source, + Compose::Dest => TinyBlendMode::Destination, + Compose::SrcOver => TinyBlendMode::SourceOver, + Compose::DestOver => TinyBlendMode::DestinationOver, + Compose::SrcIn => TinyBlendMode::SourceIn, + Compose::DestIn => TinyBlendMode::DestinationIn, + Compose::SrcOut => TinyBlendMode::SourceOut, + Compose::DestOut => TinyBlendMode::DestinationOut, + Compose::SrcAtop => TinyBlendMode::SourceAtop, + Compose::DestAtop => TinyBlendMode::DestinationAtop, + Compose::Xor => TinyBlendMode::Xor, + Compose::Plus => TinyBlendMode::Plus, + Compose::PlusLighter => TinyBlendMode::Plus, + } +} + +fn mix_to_tiny_blend_mode(mix: Mix) -> TinyBlendMode { + match mix { + Mix::Normal => TinyBlendMode::SourceOver, + Mix::Multiply => TinyBlendMode::Multiply, + Mix::Screen => TinyBlendMode::Screen, + Mix::Overlay => TinyBlendMode::Overlay, + Mix::Darken => TinyBlendMode::Darken, + Mix::Lighten => TinyBlendMode::Lighten, + Mix::ColorDodge => TinyBlendMode::ColorDodge, + Mix::ColorBurn => TinyBlendMode::ColorBurn, + Mix::HardLight => TinyBlendMode::HardLight, + Mix::SoftLight => TinyBlendMode::SoftLight, + Mix::Difference => TinyBlendMode::Difference, + Mix::Exclusion => TinyBlendMode::Exclusion, + Mix::Hue => TinyBlendMode::Hue, + Mix::Saturation => TinyBlendMode::Saturation, + Mix::Color => TinyBlendMode::Color, + Mix::Luminosity => TinyBlendMode::Luminosity, + #[allow(deprecated)] + Mix::Clip => TinyBlendMode::SourceOver, + } +} + +fn apply_layer(layer: &Layer, parent: &mut Layer) { + match determine_blend_strategy(&layer.blend_mode) { + BlendStrategy::SinglePass(blend_mode) => { + let mut paint = Paint { + blend_mode, + anti_alias: true, + ..Default::default() + }; + + let transform = skia_transform_with_scaled_translation( + parent.transform * layer.combine_transform, + 1., + 1., + ); + + let layer_pattern = Pattern::new( + layer.pixmap.as_ref(), + SpreadMode::Pad, + FilterQuality::Bilinear, + layer.alpha, + Transform::identity(), + ); + + paint.shader = layer_pattern; + + let layer_rect = try_ret!(tiny_skia::Rect::from_xywh( + 0.0, + 0.0, + layer.pixmap.width() as f32, + layer.pixmap.height() as f32, + )); + + parent.pixmap.fill_rect( + layer_rect, + &paint, + transform, + parent.clip.is_some().then_some(&parent.mask), + ); + } + BlendStrategy::MultiPass { + first_pass, + second_pass, + } => { + let original_parent = parent.pixmap.clone(); + + let mut paint = Paint { + blend_mode: first_pass, + anti_alias: true, + ..Default::default() + }; + + let transform = skia_transform_with_scaled_translation( + parent.transform * layer.combine_transform, + 1., + 1., + ); + let layer_pattern = Pattern::new( + layer.pixmap.as_ref(), + SpreadMode::Pad, + FilterQuality::Bilinear, + 1.0, + Transform::identity(), + ); + + paint.shader = layer_pattern; + + let layer_rect = try_ret!(tiny_skia::Rect::from_xywh( + 0.0, + 0.0, + layer.pixmap.width() as f32, + layer.pixmap.height() as f32, + )); + + parent.pixmap.fill_rect( + layer_rect, + &paint, + transform, + parent.clip.is_some().then_some(&parent.mask), + ); + + let intermediate = parent.pixmap.clone(); + parent.pixmap = original_parent; + + let mut paint = Paint { + blend_mode: second_pass, + anti_alias: true, + ..Default::default() + }; + + let intermediate_pattern = Pattern::new( + intermediate.as_ref(), + SpreadMode::Pad, + FilterQuality::Bilinear, + 1.0, + Transform::identity(), + ); + + paint.shader = intermediate_pattern; + + parent.pixmap.fill_rect( + layer_rect, + &paint, + transform, + parent.clip.is_some().then_some(&parent.mask), + ) + } + } + parent.transform *= layer.transform; +} + +fn skia_transform(affine: Affine, window_scale: f32) -> Transform { + let transform = affine.as_coeffs(); + Transform::from_row( + transform[0] as f32, + transform[1] as f32, + transform[2] as f32, + transform[3] as f32, + transform[4] as f32, + transform[5] as f32, + ) + .post_scale(window_scale, window_scale) +} + +fn skia_transform_with_scaled_translation( + affine: Affine, + translation_scale: f32, + render_scale: f32, +) -> Transform { + let transform = affine.as_coeffs(); + Transform::from_row( + transform[0] as f32, + transform[1] as f32, + transform[2] as f32, + transform[3] as f32, + transform[4] as f32 * translation_scale, + transform[5] as f32 * translation_scale, + ) + .post_scale(render_scale, render_scale) +} diff --git a/crates/anyrender_tiny_skia/src/window_renderer.rs b/crates/anyrender_tiny_skia/src/window_renderer.rs new file mode 100644 index 0000000..77106f2 --- /dev/null +++ b/crates/anyrender_tiny_skia/src/window_renderer.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "softbuffer_window_renderer")] +pub use softbuffer_window_renderer::SoftbufferWindowRenderer; + +#[cfg(feature = "pixels_window_renderer")] +pub use pixels_window_renderer::PixelsWindowRenderer; + +#[cfg(feature = "pixels_window_renderer")] +pub type TinySkiaWindowRenderer = PixelsWindowRenderer; +#[cfg(all( + feature = "softbuffer_window_renderer", + not(feature = "pixels_window_renderer") +))] +pub type VelloCpuWindowRenderer = SoftbufferWindowRenderer; diff --git a/crates/anyrender_vello/src/window_renderer.rs b/crates/anyrender_vello/src/window_renderer.rs index abf6265..688fd49 100644 --- a/crates/anyrender_vello/src/window_renderer.rs +++ b/crates/anyrender_vello/src/window_renderer.rs @@ -137,7 +137,7 @@ impl WindowRenderer for VelloWindowRenderer { width, height, present_mode: PresentMode::AutoVsync, - desired_maximum_frame_latency: 2, + desired_maximum_frame_latency: 1, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], }, diff --git a/examples/bunnymark/Cargo.toml b/examples/bunnymark/Cargo.toml index 5cc693f..e0a7d8c 100644 --- a/examples/bunnymark/Cargo.toml +++ b/examples/bunnymark/Cargo.toml @@ -12,6 +12,7 @@ peniko = { workspace = true } wgpu = { workspace = true } image = { workspace = true, features = ["png"] } anyrender = { workspace = true } +anyrender_tiny_skia = { workspace = true, features = ["pixels_window_renderer", "log_frame_times"] } anyrender_vello = { workspace = true, features = ["log_frame_times"] } anyrender_vello_hybrid = { workspace = true, features = ["log_frame_times"] } anyrender_vello_cpu = { workspace = true, features = ["pixels_window_renderer", "multithreading", "log_frame_times"] } diff --git a/examples/bunnymark/src/main.rs b/examples/bunnymark/src/main.rs index 2d80e84..ed8f530 100644 --- a/examples/bunnymark/src/main.rs +++ b/examples/bunnymark/src/main.rs @@ -1,4 +1,5 @@ use anyrender::{PaintScene, WindowRenderer}; +use anyrender_tiny_skia::{TinySkiaImageRenderer, TinySkiaWindowRenderer}; use anyrender_vello::VelloWindowRenderer; use anyrender_vello_cpu::VelloCpuWindowRenderer; use anyrender_vello_hybrid::VelloHybridWindowRenderer; @@ -30,6 +31,7 @@ enum Renderer { Gpu(Box), Hybrid(Box), Cpu(Box), + TinySkia(Box), } impl Renderer { @@ -38,6 +40,7 @@ impl Renderer { Renderer::Gpu(r) => r.is_active(), Renderer::Hybrid(r) => r.is_active(), Renderer::Cpu(r) => r.is_active(), + Renderer::TinySkia(r) => r.is_active(), } } @@ -46,6 +49,7 @@ impl Renderer { Renderer::Gpu(r) => r.set_size(w, h), Renderer::Hybrid(r) => r.set_size(w, h), Renderer::Cpu(r) => r.set_size(w, h), + Renderer::TinySkia(r) => r.set_size(w, h), } } } @@ -200,6 +204,7 @@ impl ApplicationHandler for App { Renderer::Gpu(_) => "vello", Renderer::Hybrid(_) => "vello_hybrid", Renderer::Cpu(_) => "vello_cpu", + Renderer::TinySkia(_) => "tiny_skia", }; print!( "[{}] [{} bunnies] ", @@ -237,6 +242,16 @@ impl ApplicationHandler for App { Color::from_rgb8(0, 255, 0), ); }), + Renderer::TinySkia(r) => r.render(|scene_painter| { + App::draw_scene( + scene_painter, + self.logical_width, + self.logical_height, + self.scale_factor, + &self.bunny_manager, + Color::from_rgb8(0, 255, 0), + ); + }), } window.request_redraw(); } @@ -267,6 +282,11 @@ impl ApplicationHandler for App { }); } Renderer::Gpu(_) => { + self.set_backend(TinySkiaWindowRenderer::new(), event_loop, |r| { + Renderer::TinySkia(Box::new(r)) + }); + } + Renderer::TinySkia(_) => { self.set_backend(VelloCpuWindowRenderer::new(), event_loop, |r| { Renderer::Cpu(Box::new(r)) }); diff --git a/examples/winit/Cargo.toml b/examples/winit/Cargo.toml index ab881c5..ae6896d 100644 --- a/examples/winit/Cargo.toml +++ b/examples/winit/Cargo.toml @@ -13,6 +13,7 @@ wgpu = { workspace = true } anyrender = { workspace = true } anyrender_vello = { workspace = true } anyrender_vello_hybrid = { workspace = true } +anyrender_tiny_skia = { workspace = true } anyrender_vello_cpu = { workspace = true, features = [ "pixels_window_renderer", "softbuffer_window_renderer", diff --git a/examples/winit/src/main.rs b/examples/winit/src/main.rs index e3a603c..c46b474 100644 --- a/examples/winit/src/main.rs +++ b/examples/winit/src/main.rs @@ -1,4 +1,5 @@ use anyrender::{NullWindowRenderer, PaintScene, WindowRenderer}; +use anyrender_tiny_skia::TinySkiaImageRenderer; use anyrender_vello::VelloWindowRenderer; use anyrender_vello_cpu::{PixelsWindowRenderer, SoftbufferWindowRenderer, VelloCpuImageRenderer}; use anyrender_vello_hybrid::VelloHybridWindowRenderer; @@ -22,8 +23,12 @@ struct App { type VelloCpuSBWindowRenderer = SoftbufferWindowRenderer; type VelloCpuWindowRenderer = PixelsWindowRenderer; +type TinySkiaSBWindowRenderer = SoftbufferWindowRenderer; +type TinySkiaWindowRenderer = PixelsWindowRenderer; + // type InitialBackend = VelloWindowRenderer; -type InitialBackend = VelloHybridWindowRenderer; +// type InitialBackend = VelloHybridWindowRenderer; +type InitialBackend = TinySkiaWindowRenderer; // type InitialBackend = VelloCpuWindowRenderer; // type InitialBackend = VelloCpuSBWindowRenderer; // type InitialBackend = NullWindowRenderer; @@ -32,6 +37,7 @@ enum Renderer { Gpu(Box), Hybrid(Box), Cpu(Box), + TinySkia(Box), CpuSoftbuffer(Box), Null(NullWindowRenderer), } @@ -55,6 +61,11 @@ impl From for Renderer { Self::CpuSoftbuffer(Box::new(renderer)) } } +impl From for Renderer { + fn from(renderer: TinySkiaWindowRenderer) -> Self { + Self::TinySkia(Box::new(renderer)) + } +} impl From for Renderer { fn from(renderer: NullWindowRenderer) -> Self { Self::Null(renderer) @@ -67,6 +78,7 @@ impl Renderer { Renderer::Gpu(r) => r.is_active(), Renderer::Hybrid(r) => r.is_active(), Renderer::Cpu(r) => r.is_active(), + Renderer::TinySkia(r) => r.is_active(), Renderer::CpuSoftbuffer(r) => r.is_active(), Renderer::Null(r) => r.is_active(), } @@ -77,6 +89,7 @@ impl Renderer { Renderer::Gpu(r) => r.set_size(w, h), Renderer::Hybrid(r) => r.set_size(w, h), Renderer::Cpu(r) => r.set_size(w, h), + Renderer::TinySkia(r) => r.set_size(w, h), Renderer::CpuSoftbuffer(r) => r.set_size(w, h), Renderer::Null(r) => r.set_size(w, h), } @@ -196,6 +209,9 @@ impl ApplicationHandler for App { Renderer::Gpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(255, 0, 0))), Renderer::Hybrid(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))), Renderer::Cpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))), + Renderer::TinySkia(r) => { + r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 255))) + } Renderer::CpuSoftbuffer(r) => { r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 255))) } @@ -222,6 +238,9 @@ impl ApplicationHandler for App { Renderer::Null(_) => { self.set_backend(VelloCpuWindowRenderer::new(), event_loop); } + Renderer::TinySkia(_) => { + self.set_backend(VelloWindowRenderer::new(), event_loop); + } }, _ => {} } From 92b8dc5736e1ab26d405058da13302ec84fb07cb Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 28 Oct 2025 22:13:37 +0000 Subject: [PATCH 2/7] Fixups --- .../anyrender_tiny_skia/src/image_renderer.rs | 50 +++++++++---------- crates/anyrender_tiny_skia/src/scene.rs | 45 ++++++++--------- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/crates/anyrender_tiny_skia/src/image_renderer.rs b/crates/anyrender_tiny_skia/src/image_renderer.rs index d7a898a..df0e6a7 100644 --- a/crates/anyrender_tiny_skia/src/image_renderer.rs +++ b/crates/anyrender_tiny_skia/src/image_renderer.rs @@ -63,31 +63,31 @@ impl ImageRenderer for TinySkiaImageRenderer { draw_fn(painter); timer.record_time("cmd"); - if let Some(layer) = self.scene.layers.first() { - let pixmap = &layer.pixmap; - let width = pixmap.width() as usize; - let height = pixmap.height() as usize; - let expected_len = width * height * 4; - - assert!( - buffer.len() >= expected_len, - "buffer too small: {} < {}", - buffer.len(), - expected_len - ); - - let pixels = pixmap.pixels(); - - buffer[..expected_len] - .chunks_exact_mut(4) - .zip(pixels.iter()) - .for_each(|(chunk, pixel)| { - chunk[0] = pixel.red(); - chunk[1] = pixel.green(); - chunk[2] = pixel.blue(); - chunk[3] = pixel.alpha(); - }); - } + + let pixmap = &self.scene.layers[0].pixmap; + let width = pixmap.width() as usize; + let height = pixmap.height() as usize; + let expected_len = width * height * 4; + + assert!( + buffer.len() >= expected_len, + "buffer too small: {} < {}", + buffer.len(), + expected_len + ); + + let pixels = pixmap.pixels(); + + buffer[..expected_len] + .chunks_exact_mut(4) + .zip(pixels.iter()) + .for_each(|(chunk, pixel)| { + chunk[0] = pixel.red(); + chunk[1] = pixel.green(); + chunk[2] = pixel.blue(); + chunk[3] = pixel.alpha(); + }); + timer.record_time("render"); timer.print_times("tiny-skia image: "); diff --git a/crates/anyrender_tiny_skia/src/scene.rs b/crates/anyrender_tiny_skia/src/scene.rs index 389a97b..b29b88c 100644 --- a/crates/anyrender_tiny_skia/src/scene.rs +++ b/crates/anyrender_tiny_skia/src/scene.rs @@ -172,6 +172,7 @@ struct Glyph { #[derive(PartialEq, Clone, Copy)] struct CacheColor(bool); +pub(crate) struct Layer { pub(crate) struct Layer { pub(crate) pixmap: Pixmap, /// clip is stored with the transform at the time clip is called @@ -465,20 +466,20 @@ pub struct TinySkiaScenePainter { impl TinySkiaScenePainter { pub fn new(width: u32, height: u32) -> Self { - let mut layers = vec![]; - if let (Some(pixmap), Some(mask)) = (Pixmap::new(width, height), Mask::new(width, height)) { - let main_layer = Layer { - pixmap, - mask, - clip: None, - alpha: 1.0, - transform: Affine::IDENTITY, - combine_transform: Affine::IDENTITY, - blend_mode: Mix::Normal.into(), - cache_color: CacheColor(false), - }; - layers.push(main_layer); - } + let width = width.max(1); + let height = height.max(1); + let pixmap = Pixmap::new(width, height).expect("Failed to create pixmap"); + let mask = Mask::new(width, height).expect("Failed to create mask"); + let main_layer = Layer { + pixmap, + mask, + clip: None, + alpha: 1.0, + transform: Affine::IDENTITY, + combine_transform: Affine::IDENTITY, + blend_mode: Mix::Normal.into(), + cache_color: CacheColor(false), + }; let cache_color = CacheColor(false); Self { layers, @@ -489,11 +490,11 @@ impl TinySkiaScenePainter { impl PaintScene for TinySkiaScenePainter { fn reset(&mut self) { - if let Some(first_layer) = self.layers.last_mut() { - // first_layer.pixmap.fill(tiny_skia::Color::WHITE); - first_layer.clip = None; - first_layer.transform = Affine::IDENTITY; - } + let first_layer = self.layers.last_mut().unwrap(); + // first_layer.pixmap.fill(tiny_skia::Color::WHITE); + first_layer.clip = None; + first_layer.transform = Affine::IDENTITY; + } fn push_layer( @@ -698,12 +699,8 @@ enum BlendStrategy { fn determine_blend_strategy(peniko_mode: &BlendMode) -> BlendStrategy { match (peniko_mode.mix, peniko_mode.compose) { - (Mix::Normal, compose) => BlendStrategy::SinglePass(compose_to_tiny_blend_mode(compose)), #[allow(deprecated)] - (Mix::Clip, compose) => BlendStrategy::MultiPass { - first_pass: compose_to_tiny_blend_mode(compose), - second_pass: TinyBlendMode::Source, - }, + (Mix::Normal | Mix::Clip, compose) => BlendStrategy::SinglePass(compose_to_tiny_blend_mode(compose)), (mix, Compose::SrcOver) => BlendStrategy::SinglePass(mix_to_tiny_blend_mode(mix)), (mix, compose) => BlendStrategy::MultiPass { first_pass: compose_to_tiny_blend_mode(compose), From 003e549b430da77e49880319f5915e1f7e271d51 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Tue, 28 Oct 2025 17:26:47 -0600 Subject: [PATCH 3/7] make it work --- .../anyrender_tiny_skia/src/image_renderer.rs | 11 +- crates/anyrender_tiny_skia/src/scene.rs | 221 ++++++++---------- 2 files changed, 98 insertions(+), 134 deletions(-) diff --git a/crates/anyrender_tiny_skia/src/image_renderer.rs b/crates/anyrender_tiny_skia/src/image_renderer.rs index df0e6a7..37e16fc 100644 --- a/crates/anyrender_tiny_skia/src/image_renderer.rs +++ b/crates/anyrender_tiny_skia/src/image_renderer.rs @@ -18,13 +18,8 @@ impl ImageRenderer for TinySkiaImageRenderer { } fn resize(&mut self, width: u32, height: u32) { - if !self.scene.layers.is_empty() { - self.scene.layers[0].pixmap = - Pixmap::new(width, height).expect("Failed to create pixmap"); - self.scene.layers[0].mask = Mask::new(width, height).expect("Failed to create mask"); - } else { - self.scene = TinySkiaScenePainter::new(width, height); - } + self.scene.layers[0].pixmap = Pixmap::new(width, height).expect("Failed to create pixmap"); + self.scene.layers[0].mask = Mask::new(width, height).expect("Failed to create mask"); } fn reset(&mut self) { @@ -63,7 +58,6 @@ impl ImageRenderer for TinySkiaImageRenderer { draw_fn(painter); timer.record_time("cmd"); - let pixmap = &self.scene.layers[0].pixmap; let width = pixmap.width() as usize; let height = pixmap.height() as usize; @@ -87,7 +81,6 @@ impl ImageRenderer for TinySkiaImageRenderer { chunk[2] = pixel.blue(); chunk[3] = pixel.alpha(); }); - timer.record_time("render"); timer.print_times("tiny-skia image: "); diff --git a/crates/anyrender_tiny_skia/src/scene.rs b/crates/anyrender_tiny_skia/src/scene.rs index b29b88c..51e8ecd 100644 --- a/crates/anyrender_tiny_skia/src/scene.rs +++ b/crates/anyrender_tiny_skia/src/scene.rs @@ -1,23 +1,16 @@ //! A [`tiny-skia`] backend for the [`anyrender`] 2D drawing abstraction use anyhow::{Result, anyhow}; -use anyrender::{ - ImageRenderer, NormalizedCoord, Paint as AnyRenderPaint, PaintRef, PaintScene, WindowHandle, - WindowRenderer, -}; -use debug_timer::debug_timer; +use anyrender::{NormalizedCoord, Paint as AnyRenderPaint, PaintRef, PaintScene}; use kurbo::{Affine, PathEl, Point, Rect, Shape}; use peniko::{ BlendMode, BrushRef, Color, Compose, Fill, FontData, GradientKind, ImageBrushRef, Mix, StyleRef, color::palette, }; use resvg::tiny_skia::StrokeDash; -use softbuffer::{Context, Surface}; use std::cell::RefCell; use std::collections::HashMap; -use std::num::NonZeroU32; use std::rc::Rc; -use std::sync::Arc; use swash::{ FontRef, GlyphId, scale::{Render, ScaleContext, Source, StrikeWith, image::Content as SwashContent}, @@ -25,8 +18,8 @@ use swash::{ }; use tiny_skia::{ self, FillRule, FilterQuality, GradientStop, LineCap, LineJoin, LinearGradient, Mask, MaskType, - Paint, Path, PathBuilder, Pattern, Pixmap, RadialGradient, Shader, SpreadMode, Stroke, - Transform, + Paint, Path, PathBuilder, Pattern, Pixmap, PixmapPaint, RadialGradient, Shader, SpreadMode, + Stroke, Transform, }; thread_local! { @@ -172,7 +165,6 @@ struct Glyph { #[derive(PartialEq, Clone, Copy)] struct CacheColor(bool); -pub(crate) struct Layer { pub(crate) struct Layer { pub(crate) pixmap: Pixmap, /// clip is stored with the transform at the time clip is called @@ -459,8 +451,14 @@ impl Layer { } } +pub enum LayerOrClip { + Layer(Layer), + Clip(Affine), +} + pub struct TinySkiaScenePainter { - pub(crate) layers: Vec, + pub(crate) layers: Vec, + pub(crate) last_non_clip_layer: usize, cache_color: CacheColor, } @@ -482,19 +480,26 @@ impl TinySkiaScenePainter { }; let cache_color = CacheColor(false); Self { - layers, + layers: vec![LayerOrClip::Layer(main_layer)], + last_non_clip_layer: 0, cache_color, } } + + pub fn non_clip_layer(&mut self) -> Option<&mut Layer> { + match self.layers.get_mut(self.last_non_clip_layer) { + Some(LayerOrClip::Layer(layer)) => Some(layer), + _ => None, + } + } } impl PaintScene for TinySkiaScenePainter { fn reset(&mut self) { - let first_layer = self.layers.last_mut().unwrap(); + let first_layer = self.non_clip_layer().unwrap(); // first_layer.pixmap.fill(tiny_skia::Color::WHITE); first_layer.clip = None; first_layer.transform = Affine::IDENTITY; - } fn push_layer( @@ -504,8 +509,18 @@ impl PaintScene for TinySkiaScenePainter { transform: Affine, clip: &impl Shape, ) { - if let Ok(layer) = Layer::new(blend, alpha, transform, clip, self.cache_color) { - self.layers.push(layer); + let blend: BlendMode = blend.into(); + #[allow(deprecated)] + if alpha == 1. && matches!(blend.mix, Mix::Normal | Mix::Clip) { + let layer = self.non_clip_layer().unwrap(); + let transform = layer.transform; + self.layers.push(LayerOrClip::Clip(transform)); + let layer = self.non_clip_layer().unwrap(); + layer.transform(transform); + layer.clip(clip); + } else if let Ok(layer) = Layer::new(blend, alpha, transform, clip, self.cache_color) { + self.layers.push(LayerOrClip::Layer(layer)); + self.last_non_clip_layer = self.layers.len() - 1; } } @@ -513,7 +528,7 @@ impl PaintScene for TinySkiaScenePainter { if self.layers.len() <= 1 { return; } - let layer = self.layers.pop().unwrap(); + let layer = self.non_clip_layer().unwrap(); let parent = self.layers.last_mut().unwrap(); apply_layer(&layer, parent); } @@ -526,17 +541,16 @@ impl PaintScene for TinySkiaScenePainter { _brush_transform: Option, shape: &impl Shape, ) { - if let Some(layer) = self.layers.last_mut() { - let paint_ref: PaintRef<'_> = brush.into(); - let brush_ref: BrushRef<'_> = paint_ref.into(); + let layer = self.layers.last_mut().unwrap(); + let paint_ref: PaintRef<'_> = brush.into(); + let brush_ref: BrushRef<'_> = paint_ref.into(); - let old_transform = layer.transform; - layer.transform = old_transform * transform; + let old_transform = layer.transform; + layer.transform = old_transform * transform; - layer.stroke(shape, brush_ref, style); + layer.stroke(shape, brush_ref, style); - layer.transform = old_transform; - } + layer.transform = old_transform; } fn fill<'b>( @@ -547,17 +561,16 @@ impl PaintScene for TinySkiaScenePainter { _brush_transform: Option, shape: &impl Shape, ) { - if let Some(layer) = self.layers.last_mut() { - let paint_ref: PaintRef<'_> = brush.into(); - let brush_ref: BrushRef<'_> = paint_ref.into(); + let layer = self.layers.last_mut().unwrap(); + let paint_ref: PaintRef<'_> = brush.into(); + let brush_ref: BrushRef<'_> = paint_ref.into(); - let old_transform = layer.transform; - layer.transform = old_transform * transform; + let old_transform = layer.transform; + layer.transform = old_transform * transform; - layer.fill(shape, brush_ref, 0.0); + layer.fill(shape, brush_ref, 0.0); - layer.transform = old_transform; - } + layer.transform = old_transform; } fn draw_glyphs<'b, 's: 'b>( @@ -573,29 +586,28 @@ impl PaintScene for TinySkiaScenePainter { _glyph_transform: Option, glyphs: impl Iterator, ) { - if let Some(layer) = self.layers.last_mut() { - let paint_ref: PaintRef<'_> = brush.into(); - let color = match paint_ref { - AnyRenderPaint::Solid(c) => c, - _ => palette::css::BLACK, - }; - - let old_transform = layer.transform; - layer.transform = old_transform * transform; - - for glyph in glyphs { - layer.draw_glyph( - glyph.id as GlyphId, - font_size, - color, - font, - glyph.x, - glyph.y, - ); - } + let layer = self.layers.last_mut().unwrap(); + let paint_ref: PaintRef<'_> = brush.into(); + let color = match paint_ref { + AnyRenderPaint::Solid(c) => c, + _ => palette::css::BLACK, + }; - layer.transform = old_transform; + let old_transform = layer.transform; + layer.transform = old_transform * transform; + + for glyph in glyphs { + layer.draw_glyph( + glyph.id as GlyphId, + font_size, + color, + font, + glyph.x, + glyph.y, + ); } + + layer.transform = old_transform; } fn draw_box_shadow( @@ -700,7 +712,9 @@ enum BlendStrategy { fn determine_blend_strategy(peniko_mode: &BlendMode) -> BlendStrategy { match (peniko_mode.mix, peniko_mode.compose) { #[allow(deprecated)] - (Mix::Normal | Mix::Clip, compose) => BlendStrategy::SinglePass(compose_to_tiny_blend_mode(compose)), + (Mix::Normal | Mix::Clip, compose) => { + BlendStrategy::SinglePass(compose_to_tiny_blend_mode(compose)) + } (mix, Compose::SrcOver) => BlendStrategy::SinglePass(mix_to_tiny_blend_mode(mix)), (mix, compose) => BlendStrategy::MultiPass { first_pass: compose_to_tiny_blend_mode(compose), @@ -754,38 +768,21 @@ fn mix_to_tiny_blend_mode(mix: Mix) -> TinyBlendMode { fn apply_layer(layer: &Layer, parent: &mut Layer) { match determine_blend_strategy(&layer.blend_mode) { BlendStrategy::SinglePass(blend_mode) => { - let mut paint = Paint { - blend_mode, - anti_alias: true, - ..Default::default() - }; - let transform = skia_transform_with_scaled_translation( parent.transform * layer.combine_transform, 1., 1., ); - let layer_pattern = Pattern::new( + parent.pixmap.draw_pixmap( + 0, + 0, layer.pixmap.as_ref(), - SpreadMode::Pad, - FilterQuality::Bilinear, - layer.alpha, - Transform::identity(), - ); - - paint.shader = layer_pattern; - - let layer_rect = try_ret!(tiny_skia::Rect::from_xywh( - 0.0, - 0.0, - layer.pixmap.width() as f32, - layer.pixmap.height() as f32, - )); - - parent.pixmap.fill_rect( - layer_rect, - &paint, + &PixmapPaint { + opacity: layer.alpha, + blend_mode, + quality: FilterQuality::Bilinear, + }, transform, parent.clip.is_some().then_some(&parent.mask), ); @@ -796,37 +793,21 @@ fn apply_layer(layer: &Layer, parent: &mut Layer) { } => { let original_parent = parent.pixmap.clone(); - let mut paint = Paint { - blend_mode: first_pass, - anti_alias: true, - ..Default::default() - }; - let transform = skia_transform_with_scaled_translation( parent.transform * layer.combine_transform, 1., 1., ); - let layer_pattern = Pattern::new( - layer.pixmap.as_ref(), - SpreadMode::Pad, - FilterQuality::Bilinear, - 1.0, - Transform::identity(), - ); - - paint.shader = layer_pattern; - - let layer_rect = try_ret!(tiny_skia::Rect::from_xywh( - 0.0, - 0.0, - layer.pixmap.width() as f32, - layer.pixmap.height() as f32, - )); - parent.pixmap.fill_rect( - layer_rect, - &paint, + parent.pixmap.draw_pixmap( + 0, + 0, + layer.pixmap.as_ref(), + &PixmapPaint { + opacity: 1.0, + blend_mode: first_pass, + quality: FilterQuality::Bilinear, + }, transform, parent.clip.is_some().then_some(&parent.mask), ); @@ -834,28 +815,18 @@ fn apply_layer(layer: &Layer, parent: &mut Layer) { let intermediate = parent.pixmap.clone(); parent.pixmap = original_parent; - let mut paint = Paint { - blend_mode: second_pass, - anti_alias: true, - ..Default::default() - }; - - let intermediate_pattern = Pattern::new( + parent.pixmap.draw_pixmap( + 0, + 0, intermediate.as_ref(), - SpreadMode::Pad, - FilterQuality::Bilinear, - 1.0, - Transform::identity(), - ); - - paint.shader = intermediate_pattern; - - parent.pixmap.fill_rect( - layer_rect, - &paint, + &PixmapPaint { + opacity: layer.alpha, + blend_mode: second_pass, + quality: FilterQuality::Bilinear, + }, transform, parent.clip.is_some().then_some(&parent.mask), - ) + ); } } parent.transform *= layer.transform; From 03e52b2346c9818d5bc54850ceb9f4219f589b2b Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Tue, 28 Oct 2025 18:31:10 -0600 Subject: [PATCH 4/7] clip optimization --- .../anyrender_tiny_skia/src/image_renderer.rs | 73 +++++++------- crates/anyrender_tiny_skia/src/scene.rs | 97 ++++++++++++------- 2 files changed, 99 insertions(+), 71 deletions(-) diff --git a/crates/anyrender_tiny_skia/src/image_renderer.rs b/crates/anyrender_tiny_skia/src/image_renderer.rs index 37e16fc..f28c628 100644 --- a/crates/anyrender_tiny_skia/src/image_renderer.rs +++ b/crates/anyrender_tiny_skia/src/image_renderer.rs @@ -18,8 +18,10 @@ impl ImageRenderer for TinySkiaImageRenderer { } fn resize(&mut self, width: u32, height: u32) { - self.scene.layers[0].pixmap = Pixmap::new(width, height).expect("Failed to create pixmap"); - self.scene.layers[0].mask = Mask::new(width, height).expect("Failed to create mask"); + if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.get_mut(0) { + layer.pixmap = Pixmap::new(width, height).expect("Failed to create pixmap"); + layer.mask = Mask::new(width, height).expect("Failed to create mask"); + } } fn reset(&mut self) { @@ -32,10 +34,9 @@ impl ImageRenderer for TinySkiaImageRenderer { vec: &mut Vec, ) { vec.clear(); - vec.reserve( - (self.scene.layers[0].pixmap.width() * self.scene.layers[0].pixmap.height() * 4) - as usize, - ); + if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.get(0) { + vec.reserve((layer.pixmap.width() * layer.pixmap.height() * 4) as usize); + } let painter = &mut self.scene; @@ -43,11 +44,13 @@ impl ImageRenderer for TinySkiaImageRenderer { draw_fn(painter); // Convert pixmap to RGBA8 - for pixel in self.scene.layers[0].pixmap.pixels() { - vec.push(pixel.red()); - vec.push(pixel.green()); - vec.push(pixel.blue()); - vec.push(pixel.alpha()); + if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.get(0) { + for pixel in layer.pixmap.pixels() { + vec.push(pixel.red()); + vec.push(pixel.green()); + vec.push(pixel.blue()); + vec.push(pixel.alpha()); + } } } @@ -58,29 +61,31 @@ impl ImageRenderer for TinySkiaImageRenderer { draw_fn(painter); timer.record_time("cmd"); - let pixmap = &self.scene.layers[0].pixmap; - let width = pixmap.width() as usize; - let height = pixmap.height() as usize; - let expected_len = width * height * 4; - - assert!( - buffer.len() >= expected_len, - "buffer too small: {} < {}", - buffer.len(), - expected_len - ); - - let pixels = pixmap.pixels(); - - buffer[..expected_len] - .chunks_exact_mut(4) - .zip(pixels.iter()) - .for_each(|(chunk, pixel)| { - chunk[0] = pixel.red(); - chunk[1] = pixel.green(); - chunk[2] = pixel.blue(); - chunk[3] = pixel.alpha(); - }); + if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.get(0) { + let pixmap = &layer.pixmap; + let width = pixmap.width() as usize; + let height = pixmap.height() as usize; + let expected_len = width * height * 4; + + assert!( + buffer.len() >= expected_len, + "buffer too small: {} < {}", + buffer.len(), + expected_len + ); + + let pixels = pixmap.pixels(); + + buffer[..expected_len] + .chunks_exact_mut(4) + .zip(pixels.iter()) + .for_each(|(chunk, pixel)| { + chunk[0] = pixel.red(); + chunk[1] = pixel.green(); + chunk[2] = pixel.blue(); + chunk[3] = pixel.alpha(); + }); + } timer.record_time("render"); timer.print_times("tiny-skia image: "); diff --git a/crates/anyrender_tiny_skia/src/scene.rs b/crates/anyrender_tiny_skia/src/scene.rs index 51e8ecd..d755a12 100644 --- a/crates/anyrender_tiny_skia/src/scene.rs +++ b/crates/anyrender_tiny_skia/src/scene.rs @@ -528,9 +528,29 @@ impl PaintScene for TinySkiaScenePainter { if self.layers.len() <= 1 { return; } - let layer = self.non_clip_layer().unwrap(); - let parent = self.layers.last_mut().unwrap(); - apply_layer(&layer, parent); + + match self.layers.pop() { + Some(LayerOrClip::Layer(layer)) => { + // This was a real layer, apply it to the parent + if let Some(LayerOrClip::Layer(parent)) = self.layers.last_mut() { + apply_layer(&layer, parent); + } + // Update last_non_clip_layer to point to the current top layer + for (i, layer_or_clip) in self.layers.iter().enumerate().rev() { + if matches!(layer_or_clip, LayerOrClip::Layer(_)) { + self.last_non_clip_layer = i; + break; + } + } + } + Some(LayerOrClip::Clip(_)) => { + // This was just a clip, clear the clip on the current layer + if let Some(layer) = self.non_clip_layer() { + layer.clear_clip(); + } + } + None => {} + } } fn stroke<'b>( @@ -541,16 +561,17 @@ impl PaintScene for TinySkiaScenePainter { _brush_transform: Option, shape: &impl Shape, ) { - let layer = self.layers.last_mut().unwrap(); - let paint_ref: PaintRef<'_> = brush.into(); - let brush_ref: BrushRef<'_> = paint_ref.into(); + if let Some(layer) = self.non_clip_layer() { + let paint_ref: PaintRef<'_> = brush.into(); + let brush_ref: BrushRef<'_> = paint_ref.into(); - let old_transform = layer.transform; - layer.transform = old_transform * transform; + let old_transform = layer.transform; + layer.transform = old_transform * transform; - layer.stroke(shape, brush_ref, style); + layer.stroke(shape, brush_ref, style); - layer.transform = old_transform; + layer.transform = old_transform; + } } fn fill<'b>( @@ -561,16 +582,17 @@ impl PaintScene for TinySkiaScenePainter { _brush_transform: Option, shape: &impl Shape, ) { - let layer = self.layers.last_mut().unwrap(); - let paint_ref: PaintRef<'_> = brush.into(); - let brush_ref: BrushRef<'_> = paint_ref.into(); + if let Some(layer) = self.non_clip_layer() { + let paint_ref: PaintRef<'_> = brush.into(); + let brush_ref: BrushRef<'_> = paint_ref.into(); - let old_transform = layer.transform; - layer.transform = old_transform * transform; + let old_transform = layer.transform; + layer.transform = old_transform * transform; - layer.fill(shape, brush_ref, 0.0); + layer.fill(shape, brush_ref, 0.0); - layer.transform = old_transform; + layer.transform = old_transform; + } } fn draw_glyphs<'b, 's: 'b>( @@ -586,28 +608,29 @@ impl PaintScene for TinySkiaScenePainter { _glyph_transform: Option, glyphs: impl Iterator, ) { - let layer = self.layers.last_mut().unwrap(); - let paint_ref: PaintRef<'_> = brush.into(); - let color = match paint_ref { - AnyRenderPaint::Solid(c) => c, - _ => palette::css::BLACK, - }; + if let Some(layer) = self.non_clip_layer() { + let paint_ref: PaintRef<'_> = brush.into(); + let color = match paint_ref { + AnyRenderPaint::Solid(c) => c, + _ => palette::css::BLACK, + }; + + let old_transform = layer.transform; + layer.transform = old_transform * transform; + + for glyph in glyphs { + layer.draw_glyph( + glyph.id as GlyphId, + font_size, + color, + font, + glyph.x, + glyph.y, + ); + } - let old_transform = layer.transform; - layer.transform = old_transform * transform; - - for glyph in glyphs { - layer.draw_glyph( - glyph.id as GlyphId, - font_size, - color, - font, - glyph.x, - glyph.y, - ); + layer.transform = old_transform; } - - layer.transform = old_transform; } fn draw_box_shadow( From 474551cb7f300bfe9eb8c2d45214d8a81cf6719c Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Tue, 28 Oct 2025 20:02:14 -0600 Subject: [PATCH 5/7] add box shadow --- crates/anyrender_tiny_skia/src/scene.rs | 70 ++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/crates/anyrender_tiny_skia/src/scene.rs b/crates/anyrender_tiny_skia/src/scene.rs index d755a12..f178c89 100644 --- a/crates/anyrender_tiny_skia/src/scene.rs +++ b/crates/anyrender_tiny_skia/src/scene.rs @@ -528,7 +528,7 @@ impl PaintScene for TinySkiaScenePainter { if self.layers.len() <= 1 { return; } - + match self.layers.pop() { Some(LayerOrClip::Layer(layer)) => { // This was a real layer, apply it to the parent @@ -635,13 +635,69 @@ impl PaintScene for TinySkiaScenePainter { fn draw_box_shadow( &mut self, - _transform: Affine, - _rect: Rect, - _brush: Color, - _radius: f64, - _std_dev: f64, + transform: Affine, + rect: Rect, + brush: Color, + radius: f64, + std_dev: f64, ) { - // TODO: Implement box shadow for tiny-skia + if let Some(layer) = self.non_clip_layer() { + let old_transform = layer.transform; + layer.transform = old_transform * transform; + + // Create a shadow using multiple passes with varying opacity + // This approximates a Gaussian blur using box blur technique + let blur_steps = (std_dev * 2.0).max(1.0) as i32; + let base_alpha = brush.components[3] * 0.1; // Reduced alpha for accumulative effect + for i in 0..blur_steps { + let offset = i as f64 - (blur_steps as f64 / 2.0); + let alpha_factor = 1.0 - (offset.abs() / (blur_steps as f64 / 2.0)).powf(0.5); + let current_alpha = (base_alpha as f64 * alpha_factor).min(1.0); + + // Create shadow color with reduced alpha + let shadow_color = Color::new([ + brush.components[0], + brush.components[1], + brush.components[2], + current_alpha as f32, + ]); + + // Create rounded rectangle for the shadow + let shadow_rect = Rect::new( + rect.x0 + offset, + rect.y0 + offset, + rect.x1 + offset, + rect.y1 + offset, + ); + + let rounded_rect = kurbo::RoundedRect::new( + shadow_rect.x0, + shadow_rect.y0, + shadow_rect.x1, + shadow_rect.y1, + radius, + ); + + // Create paint for this shadow layer + let paint = Paint { + shader: Shader::SolidColor(to_color(shadow_color)), + ..Default::default() + }; + + // Draw the shadow layer + if let Some(path) = shape_to_path(&rounded_rect) { + layer.pixmap.fill_path( + &path, + &paint, + FillRule::Winding, + layer.skia_transform(), + layer.clip.is_some().then_some(&layer.mask), + ); + } + } + + layer.transform = old_transform; + } } } From e02b52f3c289719700bb381fecba2ce82b6dc2ad Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Tue, 28 Oct 2025 20:28:50 -0600 Subject: [PATCH 6/7] improve draw glyphs this is more feature complete but transforms are all wrong --- .../anyrender_tiny_skia/src/image_renderer.rs | 10 +- crates/anyrender_tiny_skia/src/scene.rs | 342 ++++++++++++------ 2 files changed, 236 insertions(+), 116 deletions(-) diff --git a/crates/anyrender_tiny_skia/src/image_renderer.rs b/crates/anyrender_tiny_skia/src/image_renderer.rs index f28c628..c15800c 100644 --- a/crates/anyrender_tiny_skia/src/image_renderer.rs +++ b/crates/anyrender_tiny_skia/src/image_renderer.rs @@ -1,6 +1,6 @@ use anyrender::{ImageRenderer, PaintScene}; use debug_timer::debug_timer; -use tiny_skia::{Mask, Pixmap}; +use tiny_skia::Pixmap; use crate::TinySkiaScenePainter; @@ -20,7 +20,7 @@ impl ImageRenderer for TinySkiaImageRenderer { fn resize(&mut self, width: u32, height: u32) { if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.get_mut(0) { layer.pixmap = Pixmap::new(width, height).expect("Failed to create pixmap"); - layer.mask = Mask::new(width, height).expect("Failed to create mask"); + layer.mask = None; } } @@ -34,7 +34,7 @@ impl ImageRenderer for TinySkiaImageRenderer { vec: &mut Vec, ) { vec.clear(); - if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.get(0) { + if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.first() { vec.reserve((layer.pixmap.width() * layer.pixmap.height() * 4) as usize); } @@ -44,7 +44,7 @@ impl ImageRenderer for TinySkiaImageRenderer { draw_fn(painter); // Convert pixmap to RGBA8 - if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.get(0) { + if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.first() { for pixel in layer.pixmap.pixels() { vec.push(pixel.red()); vec.push(pixel.green()); @@ -61,7 +61,7 @@ impl ImageRenderer for TinySkiaImageRenderer { draw_fn(painter); timer.record_time("cmd"); - if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.get(0) { + if let Some(crate::scene::LayerOrClip::Layer(layer)) = self.scene.layers.first() { let pixmap = &layer.pixmap; let width = pixmap.width() as usize; let height = pixmap.height() as usize; diff --git a/crates/anyrender_tiny_skia/src/scene.rs b/crates/anyrender_tiny_skia/src/scene.rs index f178c89..207ade4 100644 --- a/crates/anyrender_tiny_skia/src/scene.rs +++ b/crates/anyrender_tiny_skia/src/scene.rs @@ -1,5 +1,7 @@ //! A [`tiny-skia`] backend for the [`anyrender`] 2D drawing abstraction +#![allow(clippy::too_many_arguments)] + use anyhow::{Result, anyhow}; use anyrender::{NormalizedCoord, Paint as AnyRenderPaint, PaintRef, PaintScene}; use kurbo::{Affine, PathEl, Point, Rect, Shape}; @@ -26,8 +28,8 @@ thread_local! { #[allow(clippy::type_complexity)] static IMAGE_CACHE: RefCell, (CacheColor, Rc)>> = RefCell::new(HashMap::new()); #[allow(clippy::type_complexity)] - // The `u32` is a color encoded as a u32 so that it is hashable and eq. - static GLYPH_CACHE: RefCell>)>> = RefCell::new(HashMap::new()); + // The cache key includes: (glyph_id, font_size_bits, coords_hash, hint, fill_rule, transform_hash, offset_hash), color + static GLYPH_CACHE: RefCell>)>> = RefCell::new(HashMap::new()); static SWASH_SCALER: RefCell = RefCell::new(ScaleContext::new()); } @@ -82,10 +84,39 @@ fn cache_glyph( font_size: f32, color: Color, font: &FontData, + normalized_coords: &[NormalizedCoord], + hint: bool, + fill: peniko::Fill, + glyph_transform: Option, + offset: Point, ) -> Option> { let c = color.to_rgba8(); - // Create a simple cache key using glyph_id and font_size as u32 bits - let cache_key = (glyph_id, font_size.to_bits()); + // Create a more comprehensive cache key including normalized coords, hinting, fill rule, transform, and offset + let coords_hash = normalized_coords + .iter() + .fold(0u64, |acc, x| acc.wrapping_mul(31).wrapping_add(*x as u64)); + let transform_hash = glyph_transform + .map(|t| { + let coeffs = t.as_coeffs(); + coeffs.iter().fold(0u64, |acc, &x| { + acc.wrapping_mul(31).wrapping_add(x.to_bits()) + }) + }) + .unwrap_or(0); + let offset_hash = offset + .x + .to_bits() + .wrapping_mul(31) + .wrapping_add(offset.y.to_bits()); + let cache_key = ( + glyph_id, + font_size.to_bits(), + coords_hash, + hint, + fill as u8, + transform_hash, + offset_hash, + ); if let Some(opt_glyph) = GLYPH_CACHE.with_borrow_mut(|gc| { if let Some((color, glyph)) = gc.get_mut(&(cache_key, c.to_u32())) { @@ -100,14 +131,39 @@ fn cache_glyph( let image = SWASH_SCALER.with_borrow_mut(|context| { let font_ref = FontRef::from_index(font.data.as_ref(), font.index as usize)?; - let mut scaler = context.builder(font_ref).size(font_size).build(); - + let mut scaler = context + .builder(font_ref) + .size(font_size) + .hint(hint) + .normalized_coords(normalized_coords) + .build(); + + let zeno_transform = if let Some(transform) = glyph_transform { + let coeffs = transform.as_coeffs(); + let swash_transform = swash::zeno::Transform::new( + coeffs[0] as f32, + coeffs[1] as f32, + coeffs[2] as f32, + coeffs[3] as f32, + coeffs[4] as f32, + coeffs[5] as f32, + ); + Some(swash_transform) + } else { + None + }; Render::new(&[ Source::ColorOutline(0), Source::ColorBitmap(StrikeWith::BestFit), Source::Outline, ]) .format(Format::Alpha) + .style(match fill { + peniko::Fill::NonZero => swash::zeno::Fill::NonZero, + peniko::Fill::EvenOdd => swash::zeno::Fill::EvenOdd, + }) + .transform(zeno_transform) + .offset(swash::zeno::Vector::new(offset.x as f32, offset.y as f32)) .render(&mut scaler, glyph_id) })?; @@ -167,9 +223,7 @@ struct CacheColor(bool); pub(crate) struct Layer { pub(crate) pixmap: Pixmap, - /// clip is stored with the transform at the time clip is called - pub(crate) clip: Option, - pub(crate) mask: Mask, + pub(crate) mask: Option, /// this transform should generally only be used when making a draw call to skia transform: Affine, // the transform that the layer was pushed with that will be used when applying the layer @@ -179,16 +233,6 @@ pub(crate) struct Layer { cache_color: CacheColor, } impl Layer { - /// the img_rect should already be in the correct transformed space along with the window_scale applied - fn clip_rect(&self, img_rect: Rect) -> Option { - if let Some(clip) = self.clip { - let clip = clip.intersect(img_rect); - to_skia_rect(clip) - } else { - to_skia_rect(img_rect) - } - } - /// Renders the pixmap at the position and transforms it with the given transform. /// x and y should have already been scaled by the window scale fn render_pixmap_direct(&mut self, img_pixmap: &Pixmap, x: f32, y: f32, transform: Affine) { @@ -216,11 +260,13 @@ impl Layer { transform[4] as f32, transform[5] as f32, ); - if let Some(rect) = self.clip_rect(img_rect) { - self.pixmap.fill_rect(rect, &paint, transform, None); + if let Some(rect) = to_skia_rect(img_rect) { + self.pixmap + .fill_rect(rect, &paint, transform, self.mask.as_ref()); } } + #[allow(dead_code)] fn render_pixmap_rect(&mut self, pixmap: &Pixmap, rect: tiny_skia::Rect) { let paint = Paint { shader: Pattern::new( @@ -236,14 +282,11 @@ impl Layer { ..Default::default() }; - self.pixmap.fill_rect( - rect, - &paint, - self.skia_transform(), - self.clip.is_some().then_some(&self.mask), - ); + self.pixmap + .fill_rect(rect, &paint, self.skia_transform(), self.mask.as_ref()); } + #[allow(dead_code)] fn render_pixmap_with_paint( &mut self, pixmap: &Pixmap, @@ -302,8 +345,7 @@ impl Layer { ); Ok(Self { pixmap: Pixmap::new(width, height).ok_or_else(|| anyhow!("unable to create pixmap"))?, - mask, - clip: Some(bbox), + mask: Some(mask), transform, combine_transform, blend_mode: blend.into(), @@ -317,15 +359,19 @@ impl Layer { } fn clip(&mut self, shape: &impl Shape) { - self.clip = Some(self.transform.transform_rect_bbox(shape.bounding_box())); let path = try_ret!(shape_to_path(shape)); - self.mask.clear(); - self.mask - .fill_path(&path, FillRule::Winding, false, self.skia_transform()); - } - - fn clear_clip(&mut self) { - self.clip = None; + let transform = self.skia_transform(); + if let Some(ref mut mask) = self.mask { + mask.fill_path(&path, FillRule::default(), false, transform); + } else { + // Create a new mask if none exists + let width = self.pixmap.width(); + let height = self.pixmap.height(); + if let Some(mut mask) = Mask::new(width, height) { + mask.fill_path(&path, FillRule::default(), false, self.skia_transform()); + self.mask = Some(mask); + } + } } fn stroke<'b, 's>( @@ -333,8 +379,9 @@ impl Layer { shape: &impl Shape, brush: impl Into>, stroke: &'s peniko::kurbo::Stroke, + brush_transform: Option, ) { - let paint = try_ret!(brush_to_paint(brush)); + let paint = try_ret!(brush_to_paint(brush, brush_transform)); let path = try_ret!(shape_to_path(shape)); let line_cap = match stroke.end_cap { peniko::kurbo::Cap::Butt => LineCap::Butt, @@ -363,11 +410,18 @@ impl Layer { &paint, &stroke, self.skia_transform(), - self.clip.is_some().then_some(&self.mask), + self.mask.as_ref(), ); } - fn fill<'b>(&mut self, shape: &impl Shape, brush: impl Into>, _blur_radius: f64) { + fn fill<'b>( + &mut self, + shape: &impl Shape, + brush: impl Into>, + _blur_radius: f64, + fill_style: Fill, + brush_transform: Option, + ) { // FIXME: Handle _blur_radius let brush: BrushRef<'_> = brush.into(); @@ -375,12 +429,15 @@ impl Layer { if let BrushRef::Image(image) = brush { if let Some(cached_pixmap) = cache_image(self.cache_color, &image) { // Create a pattern from the cached pixmap + let transform = brush_transform + .map(|t| skia_transform(t, 1.0)) + .unwrap_or_else(Transform::identity); let pattern = Pattern::new( cached_pixmap.as_ref().as_ref(), SpreadMode::Pad, FilterQuality::Nearest, 1.0, - Transform::identity(), + transform, ); let paint = Paint { shader: pattern, @@ -389,42 +446,42 @@ impl Layer { if let Some(rect) = shape.as_rect() { let rect = try_ret!(to_skia_rect(rect)); - self.pixmap.fill_rect( - rect, - &paint, - self.skia_transform(), - self.clip.is_some().then_some(&self.mask), - ); + self.pixmap + .fill_rect(rect, &paint, self.skia_transform(), self.mask.as_ref()); } else { + let fill_rule = match fill_style { + Fill::NonZero => FillRule::Winding, + Fill::EvenOdd => FillRule::EvenOdd, + }; let path = try_ret!(shape_to_path(shape)); self.pixmap.fill_path( &path, &paint, - FillRule::Winding, + fill_rule, self.skia_transform(), - self.clip.is_some().then_some(&self.mask), + self.mask.as_ref(), ); } } } else { // Handle non-image brushes - let paint = try_ret!(brush_to_paint(brush)); + let paint = try_ret!(brush_to_paint(brush, brush_transform)); + let fill_rule = match fill_style { + Fill::NonZero => FillRule::Winding, + Fill::EvenOdd => FillRule::EvenOdd, + }; if let Some(rect) = shape.as_rect() { let rect = try_ret!(to_skia_rect(rect)); - self.pixmap.fill_rect( - rect, - &paint, - self.skia_transform(), - self.clip.is_some().then_some(&self.mask), - ); + self.pixmap + .fill_rect(rect, &paint, self.skia_transform(), self.mask.as_ref()); } else { let path = try_ret!(shape_to_path(shape)); self.pixmap.fill_path( &path, &paint, - FillRule::Winding, + fill_rule, self.skia_transform(), - self.clip.is_some().then_some(&self.mask), + self.mask.as_ref(), ); } } @@ -438,22 +495,38 @@ impl Layer { font: &FontData, x: f32, y: f32, + normalized_coords: &[NormalizedCoord], + hint: bool, + glyph_transform: Option, + fill: peniko::Fill, ) { - if let Some(cached_glyph) = cache_glyph(self.cache_color, glyph_id, font_size, color, font) - { + if let Some(cached_glyph) = cache_glyph( + self.cache_color, + glyph_id, + font_size, + color, + font, + normalized_coords, + hint, + fill, + glyph_transform, + Point::new(x as f64, y as f64), + ) { + // Since transform and offset are now handled by swash, just render directly self.render_pixmap_direct( &cached_glyph.pixmap, - x + cached_glyph.left, - y - cached_glyph.top, + cached_glyph.left, + -cached_glyph.top, self.transform, ); } } } +#[allow(clippy::large_enum_variant)] pub enum LayerOrClip { Layer(Layer), - Clip(Affine), + Clip { previous_mask: Option }, } pub struct TinySkiaScenePainter { @@ -467,11 +540,9 @@ impl TinySkiaScenePainter { let width = width.max(1); let height = height.max(1); let pixmap = Pixmap::new(width, height).expect("Failed to create pixmap"); - let mask = Mask::new(width, height).expect("Failed to create mask"); let main_layer = Layer { pixmap, - mask, - clip: None, + mask: None, // No clipping initially alpha: 1.0, transform: Affine::IDENTITY, combine_transform: Affine::IDENTITY, @@ -486,7 +557,7 @@ impl TinySkiaScenePainter { } } - pub fn non_clip_layer(&mut self) -> Option<&mut Layer> { + pub(crate) fn non_clip_layer(&mut self) -> Option<&mut Layer> { match self.layers.get_mut(self.last_non_clip_layer) { Some(LayerOrClip::Layer(layer)) => Some(layer), _ => None, @@ -498,7 +569,7 @@ impl PaintScene for TinySkiaScenePainter { fn reset(&mut self) { let first_layer = self.non_clip_layer().unwrap(); // first_layer.pixmap.fill(tiny_skia::Color::WHITE); - first_layer.clip = None; + first_layer.mask = None; first_layer.transform = Affine::IDENTITY; } @@ -514,7 +585,11 @@ impl PaintScene for TinySkiaScenePainter { if alpha == 1. && matches!(blend.mix, Mix::Normal | Mix::Clip) { let layer = self.non_clip_layer().unwrap(); let transform = layer.transform; - self.layers.push(LayerOrClip::Clip(transform)); + + // Capture the current mask state before applying new clip + let previous_mask = layer.mask.clone(); + + self.layers.push(LayerOrClip::Clip { previous_mask }); let layer = self.non_clip_layer().unwrap(); layer.transform(transform); layer.clip(clip); @@ -543,10 +618,10 @@ impl PaintScene for TinySkiaScenePainter { } } } - Some(LayerOrClip::Clip(_)) => { - // This was just a clip, clear the clip on the current layer + Some(LayerOrClip::Clip { previous_mask }) => { + // This was just a clip, restore the previous mask state if let Some(layer) = self.non_clip_layer() { - layer.clear_clip(); + layer.mask = previous_mask; } } None => {} @@ -558,7 +633,7 @@ impl PaintScene for TinySkiaScenePainter { style: &kurbo::Stroke, transform: Affine, brush: impl Into>, - _brush_transform: Option, + brush_transform: Option, shape: &impl Shape, ) { if let Some(layer) = self.non_clip_layer() { @@ -568,7 +643,7 @@ impl PaintScene for TinySkiaScenePainter { let old_transform = layer.transform; layer.transform = old_transform * transform; - layer.stroke(shape, brush_ref, style); + layer.stroke(shape, brush_ref, style, brush_transform); layer.transform = old_transform; } @@ -576,10 +651,10 @@ impl PaintScene for TinySkiaScenePainter { fn fill<'b>( &mut self, - _style: Fill, + style: Fill, transform: Affine, brush: impl Into>, - _brush_transform: Option, + brush_transform: Option, shape: &impl Shape, ) { if let Some(layer) = self.non_clip_layer() { @@ -589,7 +664,7 @@ impl PaintScene for TinySkiaScenePainter { let old_transform = layer.transform; layer.transform = old_transform * transform; - layer.fill(shape, brush_ref, 0.0); + layer.fill(shape, brush_ref, 0.0, style, brush_transform); layer.transform = old_transform; } @@ -599,34 +674,66 @@ impl PaintScene for TinySkiaScenePainter { &'s mut self, font: &'b FontData, font_size: f32, - _hint: bool, - _normalized_coords: &'b [NormalizedCoord], - _style: impl Into>, + hint: bool, + normalized_coords: &'b [NormalizedCoord], + style: impl Into>, brush: impl Into>, - _brush_alpha: f32, + brush_alpha: f32, transform: Affine, - _glyph_transform: Option, + glyph_transform: Option, glyphs: impl Iterator, ) { if let Some(layer) = self.non_clip_layer() { let paint_ref: PaintRef<'_> = brush.into(); - let color = match paint_ref { + let style_ref: StyleRef<'_> = style.into(); + + // Extract color from paint and apply brush_alpha + let base_color = match paint_ref { AnyRenderPaint::Solid(c) => c, _ => palette::css::BLACK, }; + let color = base_color.multiply_alpha(brush_alpha); + let old_transform = layer.transform; layer.transform = old_transform * transform; for glyph in glyphs { - layer.draw_glyph( - glyph.id as GlyphId, - font_size, - color, - font, - glyph.x, - glyph.y, - ); + match style_ref { + StyleRef::Fill(fill) => { + // For fill styles, render the glyph normally + layer.draw_glyph( + glyph.id as GlyphId, + font_size, + color, + font, + glyph.x, + glyph.y, + normalized_coords, + hint, + glyph_transform, + fill, + ); + } + StyleRef::Stroke(_stroke) => { + // TODO! + // For stroke styles, we need to render the glyph outline + // Since swash doesn't directly support stroke rendering, + // we just render the glyph normally for now + layer.draw_glyph( + glyph.id as GlyphId, + font_size, + color, + font, + glyph.x, + glyph.y, + normalized_coords, + hint, + glyph_transform, + peniko::Fill::default(), // Default fill for stroke fallback + ); + } + } } layer.transform = old_transform; @@ -691,7 +798,7 @@ impl PaintScene for TinySkiaScenePainter { &paint, FillRule::Winding, layer.skia_transform(), - layer.clip.is_some().then_some(&layer.mask), + layer.mask.as_ref(), ); } } @@ -733,7 +840,10 @@ fn shape_to_path(shape: &impl Shape) -> Option { builder.finish() } -fn brush_to_paint<'b>(brush: impl Into>) -> Option> { +fn brush_to_paint<'b>( + brush: impl Into>, + brush_transform: Option, +) -> Option> { let shader = match brush.into() { BrushRef::Solid(c) => Shader::SolidColor(to_color(c)), BrushRef::Gradient(g) => { @@ -743,21 +853,31 @@ fn brush_to_paint<'b>(brush: impl Into>) -> Option> .map(|s| GradientStop::new(s.offset, to_color(s.color.to_alpha_color()))) .collect(); match g.kind { - GradientKind::Linear(linear_pos) => LinearGradient::new( - to_point(linear_pos.start), - to_point(linear_pos.end), - stops, - SpreadMode::Pad, - Transform::identity(), - )?, - GradientKind::Radial(radial) => RadialGradient::new( - to_point(radial.start_center), - to_point(radial.end_center), - radial.end_radius, - stops, - SpreadMode::Pad, - Transform::identity(), - )?, + GradientKind::Linear(linear_pos) => { + let transform = brush_transform + .map(|t| skia_transform(t, 1.0)) + .unwrap_or_else(Transform::identity); + LinearGradient::new( + to_point(linear_pos.start), + to_point(linear_pos.end), + stops, + SpreadMode::Pad, + transform, + )? + } + GradientKind::Radial(radial) => { + let transform = brush_transform + .map(|t| skia_transform(t, 1.0)) + .unwrap_or_else(Transform::identity); + RadialGradient::new( + to_point(radial.start_center), + to_point(radial.end_center), + radial.end_radius, + stops, + SpreadMode::Pad, + transform, + )? + } GradientKind::Sweep { .. } => return None, } } @@ -863,7 +983,7 @@ fn apply_layer(layer: &Layer, parent: &mut Layer) { quality: FilterQuality::Bilinear, }, transform, - parent.clip.is_some().then_some(&parent.mask), + parent.mask.as_ref(), ); } BlendStrategy::MultiPass { @@ -888,7 +1008,7 @@ fn apply_layer(layer: &Layer, parent: &mut Layer) { quality: FilterQuality::Bilinear, }, transform, - parent.clip.is_some().then_some(&parent.mask), + parent.mask.as_ref(), ); let intermediate = parent.pixmap.clone(); @@ -904,7 +1024,7 @@ fn apply_layer(layer: &Layer, parent: &mut Layer) { quality: FilterQuality::Bilinear, }, transform, - parent.clip.is_some().then_some(&parent.mask), + parent.mask.as_ref(), ); } } From 1c03f47910002c8def3975d188e206f899220d2f Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Tue, 28 Oct 2025 22:14:03 -0600 Subject: [PATCH 7/7] fix transforms --- crates/anyrender_tiny_skia/src/scene.rs | 300 +++++++++++------------- 1 file changed, 143 insertions(+), 157 deletions(-) diff --git a/crates/anyrender_tiny_skia/src/scene.rs b/crates/anyrender_tiny_skia/src/scene.rs index 207ade4..f75ee2c 100644 --- a/crates/anyrender_tiny_skia/src/scene.rs +++ b/crates/anyrender_tiny_skia/src/scene.rs @@ -29,7 +29,7 @@ thread_local! { static IMAGE_CACHE: RefCell, (CacheColor, Rc)>> = RefCell::new(HashMap::new()); #[allow(clippy::type_complexity)] // The cache key includes: (glyph_id, font_size_bits, coords_hash, hint, fill_rule, transform_hash, offset_hash), color - static GLYPH_CACHE: RefCell>)>> = RefCell::new(HashMap::new()); + static GLYPH_CACHE: RefCell>)>> = RefCell::new(HashMap::new()); static SWASH_SCALER: RefCell = RefCell::new(ScaleContext::new()); } @@ -88,7 +88,6 @@ fn cache_glyph( hint: bool, fill: peniko::Fill, glyph_transform: Option, - offset: Point, ) -> Option> { let c = color.to_rgba8(); // Create a more comprehensive cache key including normalized coords, hinting, fill rule, transform, and offset @@ -103,11 +102,6 @@ fn cache_glyph( }) }) .unwrap_or(0); - let offset_hash = offset - .x - .to_bits() - .wrapping_mul(31) - .wrapping_add(offset.y.to_bits()); let cache_key = ( glyph_id, font_size.to_bits(), @@ -115,7 +109,6 @@ fn cache_glyph( hint, fill as u8, transform_hash, - offset_hash, ); if let Some(opt_glyph) = GLYPH_CACHE.with_borrow_mut(|gc| { @@ -163,7 +156,6 @@ fn cache_glyph( peniko::Fill::EvenOdd => swash::zeno::Fill::EvenOdd, }) .transform(zeno_transform) - .offset(swash::zeno::Vector::new(offset.x as f32, offset.y as f32)) .render(&mut scaler, glyph_id) })?; @@ -220,6 +212,13 @@ struct Glyph { #[derive(PartialEq, Clone, Copy)] struct CacheColor(bool); +impl std::ops::Not for CacheColor { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} pub(crate) struct Layer { pub(crate) pixmap: Pixmap, @@ -333,9 +332,8 @@ impl Layer { ) -> Result { let transform = Affine::IDENTITY; let bbox = clip.bounding_box(); - let scaled_box = bbox; - let width = scaled_box.width() as u32; - let height = scaled_box.height() as u32; + let width = bbox.width() as u32; + let height = bbox.height() as u32; let mut mask = Mask::new(width, height).ok_or_else(|| anyhow!("unable to create mask"))?; mask.fill_path( &shape_to_path(clip).ok_or_else(|| anyhow!("unable to create clip shape"))?, @@ -354,21 +352,15 @@ impl Layer { }) } - fn transform(&mut self, transform: Affine) { - self.transform *= transform; - } - - fn clip(&mut self, shape: &impl Shape) { + fn clip(&mut self, shape: &impl Shape, transform: Affine) { + let transform = skia_transform(transform, 1.); let path = try_ret!(shape_to_path(shape)); - let transform = self.skia_transform(); if let Some(ref mut mask) = self.mask { mask.fill_path(&path, FillRule::default(), false, transform); } else { // Create a new mask if none exists - let width = self.pixmap.width(); - let height = self.pixmap.height(); - if let Some(mut mask) = Mask::new(width, height) { - mask.fill_path(&path, FillRule::default(), false, self.skia_transform()); + if let Some(mut mask) = Mask::new(self.pixmap.width(), self.pixmap.height()) { + mask.fill_path(&path, FillRule::default(), false, transform); self.mask = Some(mask); } } @@ -510,13 +502,12 @@ impl Layer { hint, fill, glyph_transform, - Point::new(x as f64, y as f64), ) { // Since transform and offset are now handled by swash, just render directly self.render_pixmap_direct( &cached_glyph.pixmap, - cached_glyph.left, - -cached_glyph.top, + cached_glyph.left + x, + -cached_glyph.top + y, self.transform, ); } @@ -557,20 +548,22 @@ impl TinySkiaScenePainter { } } - pub(crate) fn non_clip_layer(&mut self) -> Option<&mut Layer> { + pub(crate) fn non_clip_layer(&mut self) -> &mut Layer { match self.layers.get_mut(self.last_non_clip_layer) { - Some(LayerOrClip::Layer(layer)) => Some(layer), - _ => None, + Some(LayerOrClip::Layer(layer)) => layer, + _ => panic!("the base layer should aways exist and be a non clip layer"), } } } impl PaintScene for TinySkiaScenePainter { fn reset(&mut self) { - let first_layer = self.non_clip_layer().unwrap(); - // first_layer.pixmap.fill(tiny_skia::Color::WHITE); + let first_layer = self.non_clip_layer(); + first_layer.pixmap.fill(tiny_skia::Color::TRANSPARENT); first_layer.mask = None; first_layer.transform = Affine::IDENTITY; + first_layer.cache_color = !first_layer.cache_color; + self.layers.shrink_to(1); } fn push_layer( @@ -583,16 +576,14 @@ impl PaintScene for TinySkiaScenePainter { let blend: BlendMode = blend.into(); #[allow(deprecated)] if alpha == 1. && matches!(blend.mix, Mix::Normal | Mix::Clip) { - let layer = self.non_clip_layer().unwrap(); - let transform = layer.transform; + let layer = self.non_clip_layer(); // Capture the current mask state before applying new clip let previous_mask = layer.mask.clone(); self.layers.push(LayerOrClip::Clip { previous_mask }); - let layer = self.non_clip_layer().unwrap(); - layer.transform(transform); - layer.clip(clip); + let layer = self.non_clip_layer(); + layer.clip(clip, transform); } else if let Ok(layer) = Layer::new(blend, alpha, transform, clip, self.cache_color) { self.layers.push(LayerOrClip::Layer(layer)); self.last_non_clip_layer = self.layers.len() - 1; @@ -620,9 +611,8 @@ impl PaintScene for TinySkiaScenePainter { } Some(LayerOrClip::Clip { previous_mask }) => { // This was just a clip, restore the previous mask state - if let Some(layer) = self.non_clip_layer() { - layer.mask = previous_mask; - } + let layer = self.non_clip_layer(); + layer.mask = previous_mask; } None => {} } @@ -636,17 +626,16 @@ impl PaintScene for TinySkiaScenePainter { brush_transform: Option, shape: &impl Shape, ) { - if let Some(layer) = self.non_clip_layer() { - let paint_ref: PaintRef<'_> = brush.into(); - let brush_ref: BrushRef<'_> = paint_ref.into(); + let layer = self.non_clip_layer(); + let paint_ref: PaintRef<'_> = brush.into(); + let brush_ref: BrushRef<'_> = paint_ref.into(); - let old_transform = layer.transform; - layer.transform = old_transform * transform; + let old_transform = layer.transform; + layer.transform *= transform; - layer.stroke(shape, brush_ref, style, brush_transform); + layer.stroke(shape, brush_ref, style, brush_transform); - layer.transform = old_transform; - } + layer.transform = old_transform; } fn fill<'b>( @@ -657,17 +646,16 @@ impl PaintScene for TinySkiaScenePainter { brush_transform: Option, shape: &impl Shape, ) { - if let Some(layer) = self.non_clip_layer() { - let paint_ref: PaintRef<'_> = brush.into(); - let brush_ref: BrushRef<'_> = paint_ref.into(); + let layer = self.non_clip_layer(); + let paint_ref: PaintRef<'_> = brush.into(); + let brush_ref: BrushRef<'_> = paint_ref.into(); - let old_transform = layer.transform; - layer.transform = old_transform * transform; + let old_transform = layer.transform; + layer.transform *= transform; - layer.fill(shape, brush_ref, 0.0, style, brush_transform); + layer.fill(shape, brush_ref, 0.0, style, brush_transform); - layer.transform = old_transform; - } + layer.transform = old_transform; } fn draw_glyphs<'b, 's: 'b>( @@ -683,61 +671,60 @@ impl PaintScene for TinySkiaScenePainter { glyph_transform: Option, glyphs: impl Iterator, ) { - if let Some(layer) = self.non_clip_layer() { - let paint_ref: PaintRef<'_> = brush.into(); - let style_ref: StyleRef<'_> = style.into(); - - // Extract color from paint and apply brush_alpha - let base_color = match paint_ref { - AnyRenderPaint::Solid(c) => c, - _ => palette::css::BLACK, - }; + let layer = self.non_clip_layer(); + let paint_ref: PaintRef<'_> = brush.into(); + let style_ref: StyleRef<'_> = style.into(); + + // Extract color from paint and apply brush_alpha + let base_color = match paint_ref { + AnyRenderPaint::Solid(c) => c, + _ => palette::css::BLACK, + }; - let color = base_color.multiply_alpha(brush_alpha); - - let old_transform = layer.transform; - layer.transform = old_transform * transform; - - for glyph in glyphs { - match style_ref { - StyleRef::Fill(fill) => { - // For fill styles, render the glyph normally - layer.draw_glyph( - glyph.id as GlyphId, - font_size, - color, - font, - glyph.x, - glyph.y, - normalized_coords, - hint, - glyph_transform, - fill, - ); - } - StyleRef::Stroke(_stroke) => { - // TODO! - // For stroke styles, we need to render the glyph outline - // Since swash doesn't directly support stroke rendering, - // we just render the glyph normally for now - layer.draw_glyph( - glyph.id as GlyphId, - font_size, - color, - font, - glyph.x, - glyph.y, - normalized_coords, - hint, - glyph_transform, - peniko::Fill::default(), // Default fill for stroke fallback - ); - } + let color = base_color.multiply_alpha(brush_alpha); + + let old_transform = layer.transform; + layer.transform *= transform; + + for glyph in glyphs { + match style_ref { + StyleRef::Fill(fill) => { + // For fill styles, render the glyph normally + layer.draw_glyph( + glyph.id as GlyphId, + font_size, + color, + font, + glyph.x, + glyph.y, + normalized_coords, + hint, + glyph_transform, + fill, + ); + } + StyleRef::Stroke(_stroke) => { + // TODO! + // For stroke styles, we need to render the glyph outline + // Since swash doesn't directly support stroke rendering, + // we just render the glyph normally for now + layer.draw_glyph( + glyph.id as GlyphId, + font_size, + color, + font, + glyph.x, + glyph.y, + normalized_coords, + hint, + glyph_transform, + peniko::Fill::default(), // Default fill for stroke fallback + ); } } - - layer.transform = old_transform; } + + layer.transform = old_transform; } fn draw_box_shadow( @@ -748,63 +735,62 @@ impl PaintScene for TinySkiaScenePainter { radius: f64, std_dev: f64, ) { - if let Some(layer) = self.non_clip_layer() { - let old_transform = layer.transform; - layer.transform = old_transform * transform; - - // Create a shadow using multiple passes with varying opacity - // This approximates a Gaussian blur using box blur technique - let blur_steps = (std_dev * 2.0).max(1.0) as i32; - let base_alpha = brush.components[3] * 0.1; // Reduced alpha for accumulative effect - for i in 0..blur_steps { - let offset = i as f64 - (blur_steps as f64 / 2.0); - let alpha_factor = 1.0 - (offset.abs() / (blur_steps as f64 / 2.0)).powf(0.5); - let current_alpha = (base_alpha as f64 * alpha_factor).min(1.0); - - // Create shadow color with reduced alpha - let shadow_color = Color::new([ - brush.components[0], - brush.components[1], - brush.components[2], - current_alpha as f32, - ]); - - // Create rounded rectangle for the shadow - let shadow_rect = Rect::new( - rect.x0 + offset, - rect.y0 + offset, - rect.x1 + offset, - rect.y1 + offset, - ); + let layer = self.non_clip_layer(); + let old_transform = layer.transform; + layer.transform = old_transform * transform; + + // Create a shadow using multiple passes with varying opacity + // This approximates a Gaussian blur using box blur technique + let blur_steps = (std_dev * 2.0).max(1.0) as i32; + let base_alpha = brush.components[3] * 0.1; // Reduced alpha for accumulative effect + for i in 0..blur_steps { + let offset = i as f64 - (blur_steps as f64 / 2.0); + let alpha_factor = 1.0 - (offset.abs() / (blur_steps as f64 / 2.0)).powf(0.5); + let current_alpha = (base_alpha as f64 * alpha_factor).min(1.0); + + // Create shadow color with reduced alpha + let shadow_color = Color::new([ + brush.components[0], + brush.components[1], + brush.components[2], + current_alpha as f32, + ]); + + // Create rounded rectangle for the shadow + let shadow_rect = Rect::new( + rect.x0 + offset, + rect.y0 + offset, + rect.x1 + offset, + rect.y1 + offset, + ); - let rounded_rect = kurbo::RoundedRect::new( - shadow_rect.x0, - shadow_rect.y0, - shadow_rect.x1, - shadow_rect.y1, - radius, - ); + let rounded_rect = kurbo::RoundedRect::new( + shadow_rect.x0, + shadow_rect.y0, + shadow_rect.x1, + shadow_rect.y1, + radius, + ); - // Create paint for this shadow layer - let paint = Paint { - shader: Shader::SolidColor(to_color(shadow_color)), - ..Default::default() - }; + // Create paint for this shadow layer + let paint = Paint { + shader: Shader::SolidColor(to_color(shadow_color)), + ..Default::default() + }; - // Draw the shadow layer - if let Some(path) = shape_to_path(&rounded_rect) { - layer.pixmap.fill_path( - &path, - &paint, - FillRule::Winding, - layer.skia_transform(), - layer.mask.as_ref(), - ); - } + // Draw the shadow layer + if let Some(path) = shape_to_path(&rounded_rect) { + layer.pixmap.fill_path( + &path, + &paint, + FillRule::Winding, + layer.skia_transform(), + layer.mask.as_ref(), + ); } - - layer.transform = old_transform; } + + layer.transform = old_transform; } }