| // Copyright 2022 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| use peniko::kurbo::{Affine, Rect, Shape, Stroke}; |
| use peniko::{BlendMode, BrushRef, Color, Fill, Font, Image, StyleRef}; |
| use skrifa::instance::NormalizedCoord; |
| #[cfg(feature = "bump_estimate")] |
| use vello_encoding::BumpAllocatorMemory; |
| use vello_encoding::{Encoding, Glyph, GlyphRun, Patch, Transform}; |
| |
| // TODO - Document invariants and edge cases (#470) |
| // - What happens when we pass a transform matrix with NaN values to the Scene? |
| // - What happens if a push_layer isn't matched by a pop_layer? |
| |
| /// The main datatype for rendering graphics. |
| /// |
| /// A Scene stores a sequence of drawing commands, their context, and the |
| /// associated resources, which can later be rendered. |
| #[derive(Clone, Default)] |
| pub struct Scene { |
| encoding: Encoding, |
| #[cfg(feature = "bump_estimate")] |
| estimator: vello_encoding::BumpEstimator, |
| } |
| |
| impl Scene { |
| /// Creates a new scene. |
| pub fn new() -> Self { |
| Self::default() |
| } |
| |
| pub fn print_path_counts(&self) { |
| println!("scene n_paths: {}, n_path_segments: {}", self.encoding.n_paths, self.encoding.n_path_segments); |
| } |
| |
| /// Removes all content from the scene. |
| pub fn reset(&mut self) { |
| self.encoding.reset(); |
| #[cfg(feature = "bump_estimate")] |
| self.estimator.reset(); |
| } |
| |
| /// Tally up the bump allocator estimate for the current state of the encoding, |
| /// taking into account an optional `transform` applied to the entire scene. |
| #[cfg(feature = "bump_estimate")] |
| pub fn bump_estimate(&self, transform: Option<Affine>) -> BumpAllocatorMemory { |
| self.estimator |
| .tally(transform.as_ref().map(Transform::from_kurbo).as_ref()) |
| } |
| |
| /// Returns the underlying raw encoding. |
| pub fn encoding(&self) -> &Encoding { |
| &self.encoding |
| } |
| |
| /// Pushes a new layer clipped by the specified shape and composed with |
| /// previous layers using the specified blend mode. |
| /// |
| /// Every drawing command after this call will be clipped by the shape |
| /// until the layer is popped. |
| /// |
| /// **However, the transforms are *not* saved or modified by the layer stack.** |
| pub fn push_layer( |
| &mut self, |
| blend: impl Into<BlendMode>, |
| alpha: f32, |
| transform: Affine, |
| clip: &impl Shape, |
| ) { |
| let blend = blend.into(); |
| let t = Transform::from_kurbo(&transform); |
| self.encoding.encode_transform(t); |
| self.encoding.encode_fill_style(Fill::NonZero); |
| if !self.encoding.encode_shape(clip, true) { |
| // If the layer shape is invalid, encode a valid empty path. This suppresses |
| // all drawing until the layer is popped. |
| self.encoding |
| .encode_shape(&Rect::new(0.0, 0.0, 0.0, 0.0), true); |
| } else { |
| #[cfg(feature = "bump_estimate")] |
| self.estimator.count_path(clip.path_elements(0.1), &t, None); |
| } |
| self.encoding |
| .encode_begin_clip(blend, alpha.clamp(0.0, 1.0)); |
| } |
| |
| /// Pops the current layer. |
| pub fn pop_layer(&mut self) { |
| self.encoding.encode_end_clip(); |
| } |
| |
| /// Fills a shape using the specified style and brush. |
| pub fn fill<'b>( |
| &mut self, |
| style: Fill, |
| transform: Affine, |
| brush: impl Into<BrushRef<'b>>, |
| brush_transform: Option<Affine>, |
| shape: &impl Shape, |
| ) { |
| let t = Transform::from_kurbo(&transform); |
| self.encoding.encode_transform(t); |
| self.encoding.encode_fill_style(style); |
| if self.encoding.encode_shape(shape, true) { |
| if let Some(brush_transform) = brush_transform { |
| if self |
| .encoding |
| .encode_transform(Transform::from_kurbo(&(transform * brush_transform))) |
| { |
| self.encoding.swap_last_path_tags(); |
| } |
| } |
| self.encoding.encode_brush(brush, 1.0); |
| #[cfg(feature = "bump_estimate")] |
| self.estimator |
| .count_path(shape.path_elements(0.1), &t, None); |
| } |
| } |
| |
| /// Strokes a shape using the specified style and brush. |
| pub fn stroke<'b>( |
| &mut self, |
| style: &Stroke, |
| transform: Affine, |
| brush: impl Into<BrushRef<'b>>, |
| brush_transform: Option<Affine>, |
| shape: &impl Shape, |
| ) { |
| // The setting for tolerance are a compromise. For most applications, |
| // shape tolerance doesn't matter, as the input is likely Bézier paths, |
| // which is exact. Note that shape tolerance is hard-coded as 0.1 in |
| // the encoding crate. |
| // |
| // Stroke tolerance is a different matter. Generally, the cost scales |
| // with inverse O(n^6), so there is moderate rendering cost to setting |
| // too fine a value. On the other hand, error scales with the transform |
| // applied post-stroking, so may exceed visible threshold. When we do |
| // GPU-side stroking, the transform will be known. In the meantime, |
| // this is a compromise. |
| const SHAPE_TOLERANCE: f64 = 0.01; |
| const STROKE_TOLERANCE: f64 = SHAPE_TOLERANCE; |
| |
| const GPU_STROKES: bool = true; // Set this to `true` to enable GPU-side stroking |
| if GPU_STROKES { |
| let t = Transform::from_kurbo(&transform); |
| self.encoding.encode_transform(t); |
| self.encoding.encode_stroke_style(style); |
| |
| // We currently don't support dashing on the GPU. If the style has a dash pattern, then |
| // we convert it into stroked paths on the CPU and encode those as individual draw |
| // objects. |
| let encode_result = if style.dash_pattern.is_empty() { |
| #[cfg(feature = "bump_estimate")] |
| self.estimator |
| .count_path(shape.path_elements(SHAPE_TOLERANCE), &t, Some(style)); |
| self.encoding.encode_shape(shape, false) |
| } else { |
| // TODO: We currently collect the output of the dash iterator because |
| // `encode_path_elements` wants to consume the iterator. We want to avoid calling |
| // `dash` twice when `bump_estimate` is enabled because it internally allocates. |
| // Bump estimation will move to resolve time rather than scene construction time, |
| // so we can revert this back to not collecting when that happens. |
| let dashed = peniko::kurbo::dash( |
| shape.path_elements(SHAPE_TOLERANCE), |
| style.dash_offset, |
| &style.dash_pattern, |
| ) |
| .collect::<Vec<_>>(); |
| #[cfg(feature = "bump_estimate")] |
| self.estimator |
| .count_path(dashed.iter().copied(), &t, Some(style)); |
| self.encoding |
| .encode_path_elements(dashed.into_iter(), false) |
| }; |
| if encode_result { |
| if let Some(brush_transform) = brush_transform { |
| if self |
| .encoding |
| .encode_transform(Transform::from_kurbo(&(transform * brush_transform))) |
| { |
| self.encoding.swap_last_path_tags(); |
| } |
| } |
| self.encoding.encode_brush(brush, 1.0); |
| } |
| } else { |
| let stroked = peniko::kurbo::stroke( |
| shape.path_elements(SHAPE_TOLERANCE), |
| style, |
| &Default::default(), |
| STROKE_TOLERANCE, |
| ); |
| self.fill(Fill::NonZero, transform, brush, brush_transform, &stroked); |
| } |
| } |
| |
| /// Draws an image at its natural size with the given transform. |
| pub fn draw_image(&mut self, image: &Image, transform: Affine) { |
| self.fill( |
| Fill::NonZero, |
| transform, |
| image, |
| None, |
| &Rect::new(0.0, 0.0, image.width as f64, image.height as f64), |
| ); |
| } |
| |
| /// Returns a builder for encoding a glyph run. |
| pub fn draw_glyphs(&mut self, font: &Font) -> DrawGlyphs { |
| // TODO: Integrate `BumpEstimator` with the glyph cache. |
| DrawGlyphs::new(&mut self.encoding, font) |
| } |
| |
| /// Appends a child scene. |
| /// |
| /// The given transform is applied to every transform in the child. |
| /// This is an O(N) operation. |
| pub fn append(&mut self, other: &Scene, transform: Option<Affine>) { |
| let t = transform.as_ref().map(Transform::from_kurbo); |
| self.encoding.append(&other.encoding, &t); |
| #[cfg(feature = "bump_estimate")] |
| self.estimator.append(&other.estimator, t.as_ref()); |
| } |
| } |
| |
| /// Builder for encoding a glyph run. |
| pub struct DrawGlyphs<'a> { |
| encoding: &'a mut Encoding, |
| run: GlyphRun, |
| brush: BrushRef<'a>, |
| brush_alpha: f32, |
| } |
| |
| impl<'a> DrawGlyphs<'a> { |
| /// Creates a new builder for encoding a glyph run for the specified |
| /// encoding with the given font. |
| pub fn new(encoding: &'a mut Encoding, font: &Font) -> Self { |
| let coords_start = encoding.resources.normalized_coords.len(); |
| let glyphs_start = encoding.resources.glyphs.len(); |
| let stream_offsets = encoding.stream_offsets(); |
| Self { |
| encoding, |
| run: GlyphRun { |
| font: font.clone(), |
| transform: Transform::IDENTITY, |
| glyph_transform: None, |
| font_size: 16.0, |
| hint: false, |
| normalized_coords: coords_start..coords_start, |
| style: Fill::NonZero.into(), |
| glyphs: glyphs_start..glyphs_start, |
| stream_offsets, |
| }, |
| brush: Color::BLACK.into(), |
| brush_alpha: 1.0, |
| } |
| } |
| |
| /// Sets the global transform. This is applied to all glyphs after the offset |
| /// translation. |
| /// |
| /// The default value is the identity matrix. |
| pub fn transform(mut self, transform: Affine) -> Self { |
| self.run.transform = Transform::from_kurbo(&transform); |
| self |
| } |
| |
| /// Sets the per-glyph transform. This is applied to all glyphs prior to |
| /// offset translation. This is common used for applying a shear to simulate |
| /// an oblique font. |
| /// |
| /// The default value is `None`. |
| pub fn glyph_transform(mut self, transform: Option<Affine>) -> Self { |
| self.run.glyph_transform = transform.map(|xform| Transform::from_kurbo(&xform)); |
| self |
| } |
| |
| /// Sets the font size in pixels per em units. |
| /// |
| /// The default value is 16.0. |
| pub fn font_size(mut self, size: f32) -> Self { |
| self.run.font_size = size; |
| self |
| } |
| |
| /// Sets whether to enable hinting. |
| /// |
| /// The default value is `false`. |
| pub fn hint(mut self, hint: bool) -> Self { |
| self.run.hint = hint; |
| self |
| } |
| |
| /// Sets the normalized design space coordinates for a variable font instance. |
| pub fn normalized_coords(mut self, coords: &[NormalizedCoord]) -> Self { |
| self.encoding |
| .resources |
| .normalized_coords |
| .truncate(self.run.normalized_coords.start); |
| self.encoding |
| .resources |
| .normalized_coords |
| .extend_from_slice(coords); |
| self.run.normalized_coords.end = self.encoding.resources.normalized_coords.len(); |
| self |
| } |
| |
| /// Sets the brush. |
| /// |
| /// The default value is solid black. |
| pub fn brush(mut self, brush: impl Into<BrushRef<'a>>) -> Self { |
| self.brush = brush.into(); |
| self |
| } |
| |
| /// Sets an additional alpha multiplier for the brush. |
| /// |
| /// The default value is 1.0. |
| pub fn brush_alpha(mut self, alpha: f32) -> Self { |
| self.brush_alpha = alpha; |
| self |
| } |
| |
| /// Encodes a fill or stroke for the given sequence of glyphs and consumes the builder. |
| /// |
| /// The `style` parameter accepts either `Fill` or `&Stroke` types. |
| pub fn draw(mut self, style: impl Into<StyleRef<'a>>, glyphs: impl Iterator<Item = Glyph>) { |
| let resources = &mut self.encoding.resources; |
| self.run.style = style.into().to_owned(); |
| resources.glyphs.extend(glyphs); |
| self.run.glyphs.end = resources.glyphs.len(); |
| if self.run.glyphs.is_empty() { |
| resources |
| .normalized_coords |
| .truncate(self.run.normalized_coords.start); |
| return; |
| } |
| let index = resources.glyph_runs.len(); |
| resources.glyph_runs.push(self.run); |
| resources.patches.push(Patch::GlyphRun { index }); |
| self.encoding.encode_brush(self.brush, self.brush_alpha); |
| // Glyph run resolve step affects transform and style state in a way |
| // that is opaque to the current encoding. |
| // See <https://github.com/linebender/vello/issues/424> |
| self.encoding.force_next_transform_and_style(); |
| } |
| } |