diff --git a/Cargo.lock b/Cargo.lock index 2884cbd09..dd569cde4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3038,6 +3038,7 @@ name = "vello_common" version = "0.4.0" dependencies = [ "roxmltree", + "skrifa", "vello_api", ] @@ -3047,6 +3048,7 @@ version = "0.4.0" dependencies = [ "image", "oxipng", + "skrifa", "vello_common", ] @@ -3069,6 +3071,7 @@ dependencies = [ "png", "pollster", "roxmltree", + "skrifa", "vello_common", "wgpu", "winit", diff --git a/sparse_strips/vello_api/src/glyph.rs b/sparse_strips/vello_api/src/glyph.rs new file mode 100644 index 000000000..28d8b020d --- /dev/null +++ b/sparse_strips/vello_api/src/glyph.rs @@ -0,0 +1,18 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Types for glyphs. + +/// Positioned glyph. +#[derive(Copy, Clone, Default, Debug)] +pub struct Glyph { + /// The font-specific identifier for this glyph. + /// + /// This ID is specific to the font being used and corresponds to the + /// glyph index within that font. It is *not* a Unicode code point. + pub id: u32, + /// X-offset in run, relative to transform. + pub x: f32, + /// Y-offset in run, relative to transform. + pub y: f32, +} diff --git a/sparse_strips/vello_api/src/lib.rs b/sparse_strips/vello_api/src/lib.rs index 5973f4c44..7abb751e8 100644 --- a/sparse_strips/vello_api/src/lib.rs +++ b/sparse_strips/vello_api/src/lib.rs @@ -12,4 +12,5 @@ pub use peniko; pub use peniko::color; pub use peniko::kurbo; pub mod execute; +pub mod glyph; pub mod paint; diff --git a/sparse_strips/vello_common/Cargo.toml b/sparse_strips/vello_common/Cargo.toml index 0ca6bdc96..5dafb6c30 100644 --- a/sparse_strips/vello_common/Cargo.toml +++ b/sparse_strips/vello_common/Cargo.toml @@ -15,6 +15,7 @@ publish = false vello_api = { workspace = true, default-features = true } # for pico_svg roxmltree = "0.20.0" +skrifa = { workspace = true } [features] simd = ["vello_api/simd"] diff --git a/sparse_strips/vello_common/src/glyph.rs b/sparse_strips/vello_common/src/glyph.rs new file mode 100644 index 000000000..e8c55d799 --- /dev/null +++ b/sparse_strips/vello_common/src/glyph.rs @@ -0,0 +1,190 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Processing and drawing glyphs. + +use crate::peniko::Font; +use skrifa::instance::{NormalizedCoord, Size}; +use skrifa::outline::DrawSettings; +use skrifa::{ + GlyphId, MetadataProvider, + outline::{HintingInstance, HintingOptions, OutlinePen}, +}; +use vello_api::kurbo::{Affine, BezPath, Vec2}; + +pub use vello_api::glyph::*; + +/// A glyph prepared for rendering. +#[derive(Debug)] +pub enum PreparedGlyph { + /// A glyph defined by its outline. + Outline(OutlineGlyph), + // TODO: Image and Colr variants. +} + +/// A glyph defined by a path (its outline) and a local transform. +#[derive(Debug)] +pub struct OutlineGlyph { + /// The path of the glyph. + pub path: BezPath, + /// The local transform of the glyph. + pub local_transform: Affine, +} + +/// Trait for types that can render glyphs. +pub trait GlyphRenderer { + /// Fill glyphs with the current paint and fill rule. + fn fill_glyphs(&mut self, glyphs: impl Iterator); + + /// Stroke glyphs with the current paint and stroke settings. + fn stroke_glyphs(&mut self, glyphs: impl Iterator); +} + +/// A builder for configuring and drawing glyphs. +#[derive(Debug)] +pub struct GlyphRunBuilder<'a, T: GlyphRenderer + 'a> { + run: GlyphRun, + renderer: &'a mut T, +} + +impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { + /// Creates a new builder for drawing glyphs. + pub fn new(font: Font, renderer: &'a mut T) -> Self { + Self { + run: GlyphRun { + font, + font_size: 16.0, + glyph_transform: None, + hint: true, + normalized_coords: Vec::new(), + }, + renderer, + } + } + + /// Set the font size in pixels per em. + pub fn font_size(mut self, size: f32) -> Self { + self.run.font_size = size; + self + } + + /// Set the per-glyph transform. Can be used to apply skew to simulate italic text. + pub fn glyph_transform(mut self, transform: Affine) -> Self { + self.run.glyph_transform = Some(transform); + self + } + + /// Set whether font hinting is enabled. + pub fn hint(mut self, hint: bool) -> Self { + self.run.hint = hint; + self + } + + /// Set normalized variation coordinates for variable fonts. + pub fn normalized_coords(mut self, coords: Vec) -> Self { + self.run.normalized_coords = coords; + self + } + + /// Consumes the builder and fills the glyphs with the current configuration. + pub fn fill_glyphs(self, glyphs: impl Iterator) { + self.renderer + .fill_glyphs(Self::prepare_glyphs(&self.run, glyphs)); + } + + /// Consumes the builder and strokes the glyphs with the current configuration. + pub fn stroke_glyphs(self, glyphs: impl Iterator) { + self.renderer + .stroke_glyphs(Self::prepare_glyphs(&self.run, glyphs)); + } + + fn prepare_glyphs( + run: &GlyphRun, + glyphs: impl Iterator, + ) -> impl Iterator { + let font = skrifa::FontRef::from_index(run.font.data.as_ref(), run.font.index).unwrap(); + let outlines = font.outline_glyphs(); + let size = Size::new(run.font_size); + let normalized_coords = run.normalized_coords.as_slice(); + let hinting_instance = if run.hint { + // TODO: Cache hinting instance. + HintingInstance::new(&outlines, size, normalized_coords, HINTING_OPTIONS).ok() + } else { + None + }; + glyphs.filter_map(move |glyph| { + let draw_settings = if let Some(hinting_instance) = &hinting_instance { + DrawSettings::hinted(hinting_instance, false) + } else { + DrawSettings::unhinted(size, normalized_coords) + }; + let outline = outlines.get(GlyphId::new(glyph.id))?; + let mut path = OutlinePath(BezPath::new()); + outline.draw(draw_settings, &mut path).ok()?; + let mut transform = Affine::translate(Vec2::new(glyph.x as f64, glyph.y as f64)); + if let Some(glyph_transform) = run.glyph_transform { + transform *= glyph_transform; + } + Some(PreparedGlyph::Outline(OutlineGlyph { + path: path.0, + local_transform: transform, + })) + }) + } +} + +/// A sequence of glyphs with shared rendering properties. +#[derive(Clone, Debug)] +struct GlyphRun { + /// Font for all glyphs in the run. + pub font: Font, + /// Size of the font in pixels per em. + pub font_size: f32, + /// Per-glyph transform. Can be used to apply skew to simulate italic text. + pub glyph_transform: Option, + /// Normalized variation coordinates for variable fonts. + pub normalized_coords: Vec, + /// Controls whether font hinting is enabled. + pub hint: bool, +} + +// TODO: Although these are sane defaults, we might want to make them +// configurable. +const HINTING_OPTIONS: HintingOptions = HintingOptions { + engine: skrifa::outline::Engine::AutoFallback, + target: skrifa::outline::Target::Smooth { + mode: skrifa::outline::SmoothMode::Lcd, + symmetric_rendering: false, + preserve_linear_metrics: true, + }, +}; + +struct OutlinePath(BezPath); + +// Note that we flip the y-axis to match our coordinate system. +impl OutlinePen for OutlinePath { + #[inline] + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to((x, -y)); + } + + #[inline] + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to((x, -y)); + } + + #[inline] + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.0.curve_to((cx0, -cy0), (cx1, -cy1), (x, -y)); + } + + #[inline] + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + self.0.quad_to((cx, -cy), (x, -y)); + } + + #[inline] + fn close(&mut self) { + self.0.close_path(); + } +} diff --git a/sparse_strips/vello_common/src/lib.rs b/sparse_strips/vello_common/src/lib.rs index c130bb0ce..6d7c3caa4 100644 --- a/sparse_strips/vello_common/src/lib.rs +++ b/sparse_strips/vello_common/src/lib.rs @@ -14,6 +14,7 @@ only break in edge cases, and some of them are also only related to conversions pub mod coarse; pub mod flatten; +pub mod glyph; pub mod pico_svg; pub mod pixmap; pub mod strip; diff --git a/sparse_strips/vello_cpu/Cargo.toml b/sparse_strips/vello_cpu/Cargo.toml index 2a994ac49..c2dba6fed 100644 --- a/sparse_strips/vello_cpu/Cargo.toml +++ b/sparse_strips/vello_cpu/Cargo.toml @@ -17,6 +17,7 @@ vello_common = { workspace = true } [dev-dependencies] oxipng = { workspace = true, features = ["freestanding", "parallel"] } image = { workspace = true, features = ["png"] } +skrifa = { workspace = true } [lints] workspace = true diff --git a/sparse_strips/vello_cpu/snapshots/filled_glyphs.png b/sparse_strips/vello_cpu/snapshots/filled_glyphs.png new file mode 100644 index 000000000..2b9a0a614 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/filled_glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a876a4537bdc5812158aff76475b4e50ca70a8423c3e418bd8a640ca43b218e +size 2391 diff --git a/sparse_strips/vello_cpu/snapshots/skewed_glyphs.png b/sparse_strips/vello_cpu/snapshots/skewed_glyphs.png new file mode 100644 index 000000000..5de5f6724 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/skewed_glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ba918c5de97f7b2dbdb7e282cd2f57b26fd0d8b43e576c388a738b01462df79 +size 3161 diff --git a/sparse_strips/vello_cpu/snapshots/stroked_glyphs.png b/sparse_strips/vello_cpu/snapshots/stroked_glyphs.png new file mode 100644 index 000000000..15ad462f9 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/stroked_glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbed811f8c94c1cea5b7dbe17fb69b83b66f47cef0a59ba678b55225f13dfe24 +size 3343 diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index 960d1950e..874a99f6e 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs @@ -6,8 +6,10 @@ use crate::fine::Fine; use vello_common::coarse::Wide; use vello_common::flatten::Line; +use vello_common::glyph::{GlyphRenderer, GlyphRunBuilder, PreparedGlyph}; use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Shape, Stroke}; use vello_common::paint::Paint; +use vello_common::peniko::Font; use vello_common::peniko::color::palette::css::BLACK; use vello_common::peniko::{BlendMode, Compose, Fill, Mix}; use vello_common::pixmap::Pixmap; @@ -93,6 +95,11 @@ impl RenderContext { self.stroke_path(&rect.to_path(DEFAULT_TOLERANCE)); } + /// Creates a builder for drawing a run of glyphs that have the same attributes. + pub fn glyph_run(&mut self, font: &Font) -> GlyphRunBuilder<'_, Self> { + GlyphRunBuilder::new(font.clone(), self) + } + /// Set the current blend mode. pub fn set_blend_mode(&mut self, blend_mode: BlendMode) { self.blend_mode = blend_mode; @@ -174,3 +181,29 @@ impl RenderContext { self.wide.generate(&self.strip_buf, fill_rule, paint); } } + +impl GlyphRenderer for RenderContext { + fn fill_glyphs(&mut self, glyphs: impl Iterator) { + for glyph in glyphs { + match glyph { + PreparedGlyph::Outline(glyph) => { + let transform = self.transform * glyph.local_transform; + flatten::fill(&glyph.path, transform, &mut self.line_buf); + self.render_path(self.fill_rule, self.paint.clone()); + } + } + } + } + + fn stroke_glyphs(&mut self, glyphs: impl Iterator) { + for glyph in glyphs { + match glyph { + PreparedGlyph::Outline(glyph) => { + let transform = self.transform * glyph.local_transform; + flatten::stroke(&glyph.path, &self.stroke, transform, &mut self.line_buf); + self.render_path(Fill::NonZero, self.paint.clone()); + } + } + } + } +} diff --git a/sparse_strips/vello_cpu/tests/basic.rs b/sparse_strips/vello_cpu/tests/basic.rs index 19db95bbf..43ff35c18 100644 --- a/sparse_strips/vello_cpu/tests/basic.rs +++ b/sparse_strips/vello_cpu/tests/basic.rs @@ -4,13 +4,17 @@ //! Tests for basic functionality. use crate::util::{check_ref, get_ctx, render_pixmap}; +use skrifa::MetadataProvider; +use skrifa::raw::FileRef; use std::f64::consts::PI; +use std::sync::Arc; use vello_common::color::palette::css::{ BEIGE, BLUE, DARK_GREEN, GREEN, LIME, MAROON, REBECCA_PURPLE, RED, YELLOW, }; +use vello_common::glyph::Glyph; use vello_common::kurbo::{Affine, BezPath, Circle, Join, Point, Rect, Shape, Stroke}; use vello_common::peniko; -use vello_common::peniko::Compose; +use vello_common::peniko::{Blob, Compose, Font}; use vello_cpu::RenderContext; mod util; @@ -522,6 +526,98 @@ fn filled_vertical_hairline_rect_2() { check_ref(&ctx, "filled_vertical_hairline_rect_2"); } +#[test] +fn filled_glyphs() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .fill_glyphs(glyphs.iter()); + + check_ref(&ctx, "filled_glyphs"); +} + +#[test] +fn stroked_glyphs() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .stroke_glyphs(glyphs.iter()); + + check_ref(&ctx, "stroked_glyphs"); +} + +#[test] +fn skewed_glyphs() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .glyph_transform(Affine::skew(-20_f64.to_radians().tan(), 0.0)) + .fill_glyphs(glyphs.iter()); + + check_ref(&ctx, "skewed_glyphs"); +} + +fn layout_glyphs(text: &str, font_size: f32) -> (Font, Vec) { + const ROBOTO_FONT: &[u8] = include_bytes!("../../../examples/assets/roboto/Roboto-Regular.ttf"); + let font = Font::new(Blob::new(Arc::new(ROBOTO_FONT)), 0); + + let font_ref = { + let file_ref = FileRef::new(font.data.as_ref()).unwrap(); + match file_ref { + FileRef::Font(f) => f, + FileRef::Collection(collection) => collection.get(font.index).unwrap(), + } + }; + let font_size = skrifa::instance::Size::new(font_size); + let axes = font_ref.axes(); + let variations: Vec<(&str, f32)> = vec![]; + let var_loc = axes.location(variations.as_slice()); + let charmap = font_ref.charmap(); + let metrics = font_ref.metrics(font_size, &var_loc); + let line_height = metrics.ascent - metrics.descent + metrics.leading; + let glyph_metrics = font_ref.glyph_metrics(font_size, &var_loc); + + let mut pen_x = 0_f32; + let mut pen_y = 0_f32; + + let glyphs = text + .chars() + .filter_map(|ch| { + if ch == '\n' { + pen_y += line_height; + pen_x = 0.0; + return None; + } + let gid = charmap.map(ch).unwrap_or_default(); + let advance = glyph_metrics.advance_width(gid).unwrap_or_default(); + let x = pen_x; + pen_x += advance; + Some(Glyph { + id: gid.to_u32(), + x, + y: pen_y, + }) + }) + .collect::>(); + + (font, glyphs) +} + fn miter_stroke_2() -> Stroke { Stroke { width: 2.0, diff --git a/sparse_strips/vello_hybrid/Cargo.toml b/sparse_strips/vello_hybrid/Cargo.toml index 2f5f48364..74f9c6166 100644 --- a/sparse_strips/vello_hybrid/Cargo.toml +++ b/sparse_strips/vello_hybrid/Cargo.toml @@ -21,6 +21,7 @@ wgpu = { workspace = true } [dev-dependencies] winit = "0.30.9" +skrifa = { workspace = true } pollster = { workspace = true } png = "0.17.14" roxmltree = "0.20.0" diff --git a/sparse_strips/vello_hybrid/examples/text.rs b/sparse_strips/vello_hybrid/examples/text.rs new file mode 100644 index 000000000..663189ac7 --- /dev/null +++ b/sparse_strips/vello_hybrid/examples/text.rs @@ -0,0 +1,249 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Draws some text on screen. + +mod common; + +use common::{RenderContext, RenderSurface, create_vello_renderer, create_winit_window}; +use skrifa::MetadataProvider; +use skrifa::raw::FileRef; +use std::sync::Arc; +use vello_common::glyph::Glyph; +use vello_common::kurbo::Affine; +use vello_common::peniko::color::palette; +use vello_common::peniko::{Blob, Font}; +use vello_hybrid::{RenderParams, Renderer, Scene}; +use wgpu::RenderPassDescriptor; +use winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, EventLoop}, + window::{Window, WindowId}, +}; + +const ROBOTO_FONT: &[u8] = include_bytes!("../../../examples/assets/roboto/Roboto-Regular.ttf"); + +fn main() { + let mut app = App { + context: RenderContext::new(), + renderers: vec![], + state: RenderState::Suspended(None), + scene: Scene::new(900, 600), + }; + + let event_loop = EventLoop::new().unwrap(); + event_loop + .run_app(&mut app) + .expect("Couldn't run event loop"); +} + +#[derive(Debug)] +enum RenderState<'s> { + Active { + surface: Box>, + window: Arc, + }, + Suspended(Option>), +} + +struct App<'s> { + context: RenderContext, + renderers: Vec>, + state: RenderState<'s>, + scene: Scene, +} + +impl ApplicationHandler for App<'_> { + fn suspended(&mut self, _event_loop: &ActiveEventLoop) { + if let RenderState::Active { window, .. } = &self.state { + self.state = RenderState::Suspended(Some(window.clone())); + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let RenderState::Suspended(cached_window) = &mut self.state else { + return; + }; + + let window = cached_window.take().unwrap_or_else(|| { + create_winit_window( + event_loop, + self.scene.width().into(), + self.scene.height().into(), + true, + ) + }); + + let size = window.inner_size(); + let surface = pollster::block_on(self.context.create_surface( + window.clone(), + size.width, + size.height, + wgpu::PresentMode::AutoVsync, + wgpu::TextureFormat::Bgra8Unorm, + )); + + self.renderers + .resize_with(self.context.devices.len(), || None); + self.renderers[surface.dev_id] + .get_or_insert_with(|| create_vello_renderer(&self.context, &surface)); + + self.state = RenderState::Active { + surface: Box::new(surface), + window, + }; + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let surface = match &mut self.state { + RenderState::Active { surface, window } if window.id() == window_id => surface, + _ => return, + }; + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::Resized(size) => { + self.context + .resize_surface(surface, size.width, size.height); + } + WindowEvent::RedrawRequested => { + self.scene.reset(); + + draw_text(&mut self.scene); + let device_handle = &self.context.devices[surface.dev_id]; + let render_params = RenderParams { + width: surface.config.width, + height: surface.config.height, + }; + self.renderers[surface.dev_id].as_mut().unwrap().prepare( + &device_handle.device, + &device_handle.queue, + &self.scene, + &render_params, + ); + + let surface_texture = surface + .surface + .get_current_texture() + .expect("failed to get surface texture"); + + let texture_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = + device_handle + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Vello Render to Surface pass"), + }); + { + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("Render to Texture Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &texture_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + self.renderers[surface.dev_id].as_mut().unwrap().render( + &self.scene, + &mut pass, + &render_params, + ); + } + + device_handle.queue.submit([encoder.finish()]); + surface_texture.present(); + + device_handle.device.poll(wgpu::Maintain::Poll); + } + _ => {} + } + } +} + +fn draw_text(ctx: &mut Scene) { + let font = Font::new(Blob::new(Arc::new(ROBOTO_FONT)), 0); + let font_ref = { + let file_ref = FileRef::new(font.data.as_ref()).unwrap(); + match file_ref { + FileRef::Font(f) => f, + FileRef::Collection(collection) => collection.get(font.index).unwrap(), + } + }; + let axes = font_ref.axes(); + let size = 52_f32; + let font_size = skrifa::instance::Size::new(size); + let variations: Vec<(&str, f32)> = vec![]; + let var_loc = axes.location(variations.as_slice()); + let charmap = font_ref.charmap(); + let metrics = font_ref.metrics(font_size, &var_loc); + let line_height = metrics.ascent - metrics.descent + metrics.leading; + let glyph_metrics = font_ref.glyph_metrics(font_size, &var_loc); + + let mut pen_x = 0_f32; + let mut pen_y = 0_f32; + + let text = "Hello, world!"; + + let glyphs = text + .chars() + .filter_map(|ch| { + if ch == '\n' { + pen_y += line_height; + pen_x = 0.0; + return None; + } + let gid = charmap.map(ch).unwrap_or_default(); + let advance = glyph_metrics.advance_width(gid).unwrap_or_default(); + let x = pen_x; + pen_x += advance; + Some(Glyph { + id: gid.to_u32(), + x, + y: pen_y, + }) + }) + .collect::>(); + + ctx.set_paint(palette::css::WHITE.into()); + let transform = Affine::scale(2.0).then_translate((0., f64::from(size) * 2.0).into()); + ctx.set_transform(transform); + + // Fill the text + ctx.glyph_run(&font) + .normalized_coords(vec![]) + .font_size(size) + .hint(true) + .fill_glyphs(glyphs.iter()); + + ctx.set_transform(transform.then_translate((0., f64::from(size) * 2.0).into())); + + // Stroke the text + ctx.glyph_run(&font) + .font_size(size) + .hint(true) + .stroke_glyphs(glyphs.iter()); + + ctx.set_transform(transform.then_translate((0., f64::from(size) * 4.0).into())); + + // Skew the text to the right + ctx.glyph_run(&font) + .font_size(size) + .glyph_transform(Affine::skew(-20_f64.to_radians().tan(), 0.0)) + .hint(true) + .stroke_glyphs(glyphs.iter()); +} diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index a69e80b7a..b0e9a4d40 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs @@ -7,8 +7,10 @@ use crate::render::{GpuStrip, RenderData}; use vello_common::coarse::{Wide, WideTile}; use vello_common::color::PremulRgba8; use vello_common::flatten::Line; +use vello_common::glyph::{GlyphRenderer, GlyphRunBuilder, PreparedGlyph}; use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Shape, Stroke}; use vello_common::paint::Paint; +use vello_common::peniko::Font; use vello_common::peniko::color::palette::css::BLACK; use vello_common::peniko::{BlendMode, Compose, Fill, Mix}; use vello_common::strip::Strip; @@ -113,6 +115,11 @@ impl Scene { self.stroke_path(&rect.to_path(DEFAULT_TOLERANCE)); } + /// Creates a builder for drawing a run of glyphs that have the same attributes. + pub fn glyph_run(&mut self, font: &Font) -> GlyphRunBuilder<'_, Self> { + GlyphRunBuilder::new(font.clone(), self) + } + /// Set the blend mode for subsequent rendering operations. pub fn set_blend_mode(&mut self, blend_mode: BlendMode) { self.blend_mode = blend_mode; @@ -259,3 +266,29 @@ impl Scene { } } } + +impl GlyphRenderer for Scene { + fn fill_glyphs(&mut self, glyphs: impl Iterator) { + for glyph in glyphs { + match glyph { + PreparedGlyph::Outline(glyph) => { + let transform = self.transform * glyph.local_transform; + flatten::fill(&glyph.path, transform, &mut self.line_buf); + self.render_path(self.fill_rule, self.paint.clone()); + } + } + } + } + + fn stroke_glyphs(&mut self, glyphs: impl Iterator) { + for glyph in glyphs { + match glyph { + PreparedGlyph::Outline(glyph) => { + let transform = self.transform * glyph.local_transform; + flatten::stroke(&glyph.path, &self.stroke, transform, &mut self.line_buf); + self.render_path(Fill::NonZero, self.paint.clone()); + } + } + } + } +}