| // Copyright 2022 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| use peniko::{ |
| kurbo::{Affine, BezPath, Point, Rect, Shape, Stroke, Vec2}, |
| BlendMode, Brush, BrushRef, Color, ColorStop, ColorStops, ColorStopsSource, Compose, Extend, |
| Fill, Font, Gradient, Image, Mix, StyleRef, |
| }; |
| use skrifa::{ |
| color::ColorPainter, |
| instance::{LocationRef, NormalizedCoord}, |
| outline::{DrawSettings, OutlinePen}, |
| raw::{tables::cpal::Cpal, TableProvider}, |
| GlyphId, MetadataProvider, OutlineGlyphCollection, |
| }; |
| #[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, |
| } |
| static_assertions::assert_impl_all!(Scene: Send, Sync); |
| |
| impl Scene { |
| /// Creates a new scene. |
| pub fn new() -> Self { |
| Self::default() |
| } |
| |
| /// 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.** |
| /// |
| /// Clip layers (`blend` = [`Mix::Clip`]) should have an alpha value of 1.0. |
| /// For an opacity group with non-unity alpha, specify [`Mix::Normal`]. |
| 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_empty_shape(); |
| } 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(self, 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()); |
| } |
| } |
| |
| impl From<Encoding> for Scene { |
| fn from(encoding: Encoding) -> Self { |
| // It's fine to create a default estimator here, and that field will be |
| // removed at some point - see https://github.com/linebender/vello/issues/541 |
| Self { |
| encoding, |
| #[cfg(feature = "bump_estimate")] |
| estimator: vello_encoding::BumpEstimator::default(), |
| } |
| } |
| } |
| |
| /// Builder for encoding a glyph run. |
| pub struct DrawGlyphs<'a> { |
| scene: &'a mut Scene, |
| 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(scene: &'a mut Scene, font: &Font) -> Self { |
| let coords_start = scene.encoding.resources.normalized_coords.len(); |
| let glyphs_start = scene.encoding.resources.glyphs.len(); |
| let stream_offsets = scene.encoding.stream_offsets(); |
| Self { |
| scene, |
| 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.scene |
| .encoding |
| .resources |
| .normalized_coords |
| .truncate(self.run.normalized_coords.start); |
| self.scene |
| .encoding |
| .resources |
| .normalized_coords |
| .extend_from_slice(coords); |
| self.run.normalized_coords.end = self.scene.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. |
| /// |
| /// If the font has COLR support, it will try to draw each glyph using that table first, |
| /// falling back to non-COLR rendering. `style` is ignored for COLR glyphs. |
| /// |
| /// For these glyphs, the given [brush](Self::brush) is used as the "foreground colour", and should |
| /// be [`Solid`](Brush::Solid) for maximum compatibility. |
| pub fn draw(mut self, style: impl Into<StyleRef<'a>>, glyphs: impl Iterator<Item = Glyph>) { |
| let font_index = self.run.font.index; |
| let font = skrifa::FontRef::from_index(self.run.font.data.as_ref(), font_index).unwrap(); |
| if font.colr().is_ok() && font.cpal().is_ok() { |
| self.try_draw_colr(style.into(), glyphs); |
| } else { |
| // Shortcut path - no need to test each glyph for a colr outline |
| let outline_count = self.draw_outline_glyphs(style, glyphs); |
| if outline_count == 0 { |
| self.scene |
| .encoding |
| .resources |
| .normalized_coords |
| .truncate(self.run.normalized_coords.start); |
| } |
| } |
| } |
| |
| fn draw_outline_glyphs( |
| &mut self, |
| style: impl Into<StyleRef<'a>>, |
| glyphs: impl Iterator<Item = Glyph>, |
| ) -> usize { |
| let resources = &mut self.scene.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() { |
| return 0; |
| } |
| let index = resources.glyph_runs.len(); |
| resources.glyph_runs.push(self.run.clone()); |
| resources.patches.push(Patch::GlyphRun { index }); |
| self.scene |
| .encoding |
| .encode_brush(self.brush.clone(), 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.scene.encoding.force_next_transform_and_style(); |
| self.run.glyphs.len() |
| } |
| |
| fn try_draw_colr(&mut self, style: StyleRef<'a>, mut glyphs: impl Iterator<Item = Glyph>) { |
| let font_index = self.run.font.index; |
| let blob = &self.run.font.data.clone(); |
| let font = skrifa::FontRef::from_index(blob.as_ref(), font_index).unwrap(); |
| let upem: f32 = font.head().map(|h| h.units_per_em()).unwrap().into(); |
| let run_transform = self.run.transform.to_kurbo(); |
| let scale = Affine::scale_non_uniform( |
| (self.run.font_size / upem).into(), |
| (-self.run.font_size / upem).into(), |
| ); |
| let colour_collection = font.color_glyphs(); |
| let mut final_glyph = None; |
| let mut outline_count = 0; |
| // We copy out of the variable font coords here because we need to call an exclusive self method |
| let coords = &self.scene.encoding.resources.normalized_coords |
| [self.run.normalized_coords.clone()] |
| .to_vec(); |
| let location = LocationRef::new(coords); |
| loop { |
| let outline_glyphs = (&mut glyphs).take_while(|glyph| { |
| match colour_collection.get(GlyphId::new(glyph.id.try_into().unwrap())) { |
| Some(color) => { |
| final_glyph = Some((color, *glyph)); |
| false |
| } |
| None => true, |
| } |
| }); |
| self.run.glyphs.start = self.run.glyphs.end; |
| self.run.stream_offsets = self.scene.encoding.stream_offsets(); |
| outline_count += self.draw_outline_glyphs(clone_style_ref(&style), outline_glyphs); |
| |
| let Some((color, glyph)) = final_glyph.take() else { |
| // All of the remaining glyphs were outline glyphs |
| break; |
| }; |
| |
| let transform = run_transform |
| * Affine::translate(Vec2::new(glyph.x.into(), glyph.y.into())) |
| * scale |
| * self |
| .run |
| .glyph_transform |
| .unwrap_or(Transform::IDENTITY) |
| .to_kurbo(); |
| |
| color |
| .paint( |
| location, |
| &mut DrawColorGlyphs { |
| scene: self.scene, |
| cpal: &font.cpal().unwrap(), |
| outlines: &font.outline_glyphs(), |
| transform_stack: vec![Transform::from_kurbo(&transform)], |
| clip_box: DEFAULT_CLIP_RECT, |
| clip_depth: 0, |
| location, |
| foreground_brush: self.brush.clone(), |
| }, |
| ) |
| .unwrap(); |
| } |
| if outline_count == 0 { |
| // If we didn't draw any outline glyphs, the encoded variable font parameters were never used |
| // Therefore, we can safely discard them. |
| self.scene |
| .encoding |
| .resources |
| .normalized_coords |
| .truncate(self.run.normalized_coords.start); |
| } |
| } |
| } |
| const BOUND: f64 = 100_000.; |
| // Hack: If we don't have a clip box, we guess a rectangle we hope is big enough |
| const DEFAULT_CLIP_RECT: Rect = Rect::new(-BOUND, -BOUND, BOUND, BOUND); |
| |
| /// An adapter from [`Scene`] to [`ColorPainter`]. |
| struct DrawColorGlyphs<'a> { |
| scene: &'a mut Scene, |
| transform_stack: Vec<Transform>, |
| cpal: &'a Cpal<'a>, |
| outlines: &'a OutlineGlyphCollection<'a>, |
| clip_box: Rect, |
| clip_depth: u32, |
| location: LocationRef<'a>, |
| foreground_brush: BrushRef<'a>, |
| } |
| |
| impl ColorPainter for DrawColorGlyphs<'_> { |
| fn push_transform(&mut self, transform: skrifa::color::Transform) { |
| let transform = conv_skrifa_transform(transform); |
| let prior_transform = self.last_transform(); |
| self.transform_stack.push(prior_transform * transform); |
| } |
| |
| fn pop_transform(&mut self) { |
| self.transform_stack.pop(); |
| } |
| |
| fn push_clip_glyph(&mut self, glyph_id: GlyphId) { |
| let Some(outline) = self.outlines.get(glyph_id) else { |
| eprintln!("Didn't get expected outline"); |
| return; |
| }; |
| |
| let mut path = BezPathOutline(BezPath::new()); |
| let draw_settings = |
| DrawSettings::unhinted(skrifa::instance::Size::unscaled(), self.location); |
| |
| let Ok(_) = outline.draw(draw_settings, &mut path) else { |
| return; |
| }; |
| self.clip_depth += 1; |
| self.scene |
| .push_layer(Mix::Clip, 1.0, self.last_transform().to_kurbo(), &path.0); |
| } |
| |
| fn push_clip_box(&mut self, clip_box: skrifa::raw::types::BoundingBox<f32>) { |
| let clip_box = Rect::new( |
| clip_box.x_min.into(), |
| clip_box.y_min.into(), |
| clip_box.x_max.into(), |
| clip_box.y_max.into(), |
| ); |
| if self.clip_depth == 0 { |
| self.clip_box = clip_box; |
| } |
| self.clip_depth += 1; |
| self.scene |
| .push_layer(Mix::Clip, 1.0, self.last_transform().to_kurbo(), &clip_box); |
| } |
| |
| fn pop_clip(&mut self) { |
| self.scene.pop_layer(); |
| self.clip_depth -= 1; |
| if self.clip_depth == 0 { |
| self.clip_box = DEFAULT_CLIP_RECT; |
| } |
| } |
| |
| fn fill(&mut self, brush: skrifa::color::Brush<'_>) { |
| let brush = conv_brush(brush, self.cpal, &self.foreground_brush); |
| self.scene.fill( |
| Fill::NonZero, |
| Affine::IDENTITY, |
| &brush, |
| Some(self.last_transform().to_kurbo()), |
| &self.clip_box, |
| ); |
| } |
| |
| fn push_layer(&mut self, composite: skrifa::color::CompositeMode) { |
| let blend = match composite { |
| skrifa::color::CompositeMode::Clear => Compose::Clear, |
| skrifa::color::CompositeMode::Src => Compose::Copy, |
| skrifa::color::CompositeMode::Dest => Compose::Dest, |
| skrifa::color::CompositeMode::SrcOver => Compose::SrcOver, |
| skrifa::color::CompositeMode::DestOver => Compose::DestOver, |
| skrifa::color::CompositeMode::SrcIn => Compose::SrcIn, |
| skrifa::color::CompositeMode::DestIn => Compose::DestIn, |
| skrifa::color::CompositeMode::SrcOut => Compose::SrcOut, |
| skrifa::color::CompositeMode::DestOut => Compose::DestOut, |
| skrifa::color::CompositeMode::SrcAtop => Compose::SrcAtop, |
| skrifa::color::CompositeMode::DestAtop => Compose::DestAtop, |
| skrifa::color::CompositeMode::Xor => Compose::Xor, |
| skrifa::color::CompositeMode::Plus => Compose::Plus, |
| // TODO: |
| _ => Compose::SrcOver, |
| }; |
| self.scene |
| .push_layer(blend, 1.0, self.last_transform().to_kurbo(), &self.clip_box); |
| } |
| |
| fn pop_layer(&mut self) { |
| self.scene.pop_layer(); |
| } |
| |
| fn fill_glyph( |
| &mut self, |
| glyph_id: GlyphId, |
| brush_transform: Option<skrifa::color::Transform>, |
| brush: skrifa::color::Brush<'_>, |
| ) { |
| let Some(outline) = self.outlines.get(glyph_id) else { |
| eprintln!("Didn't get expected outline"); |
| return; |
| }; |
| |
| let mut path = BezPathOutline(BezPath::new()); |
| let draw_settings = |
| DrawSettings::unhinted(skrifa::instance::Size::unscaled(), self.location); |
| |
| let Ok(_) = outline.draw(draw_settings, &mut path) else { |
| return; |
| }; |
| |
| let transform = self.last_transform(); |
| self.scene.fill( |
| Fill::NonZero, |
| transform.to_kurbo(), |
| &conv_brush(brush, self.cpal, &self.foreground_brush), |
| brush_transform |
| .map(conv_skrifa_transform) |
| .map(|it| it.to_kurbo()), |
| &path.0, |
| ); |
| } |
| } |
| |
| // TODO: Move this into Peniko |
| fn clone_style_ref<'first>(first: &StyleRef<'first>) -> StyleRef<'first> { |
| match first { |
| StyleRef::Fill(fill) => StyleRef::Fill(*fill), |
| StyleRef::Stroke(stroke) => StyleRef::Stroke(stroke), |
| } |
| } |
| |
| struct BezPathOutline(BezPath); |
| |
| impl OutlinePen for BezPathOutline { |
| fn move_to(&mut self, x: f32, y: f32) { |
| self.0.move_to(Point::new(x.into(), y.into())); |
| } |
| |
| fn line_to(&mut self, x: f32, y: f32) { |
| self.0.line_to(Point::new(x.into(), y.into())); |
| } |
| |
| fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { |
| self.0.quad_to( |
| Point::new(cx0.into(), cy0.into()), |
| Point::new(x.into(), y.into()), |
| ); |
| } |
| |
| fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { |
| self.0.curve_to( |
| Point::new(cx0.into(), cy0.into()), |
| Point::new(cx1.into(), cy1.into()), |
| Point::new(x.into(), y.into()), |
| ); |
| } |
| |
| fn close(&mut self) { |
| self.0.close_path(); |
| } |
| } |
| |
| impl DrawColorGlyphs<'_> { |
| fn last_transform(&self) -> Transform { |
| self.transform_stack |
| .last() |
| .copied() |
| .unwrap_or(Transform::IDENTITY) |
| } |
| } |
| |
| fn conv_skrifa_transform(transform: skrifa::color::Transform) -> Transform { |
| Transform { |
| matrix: [transform.xx, transform.xy, transform.yx, transform.yy], |
| translation: [transform.dx, transform.dy], |
| } |
| } |
| |
| fn conv_brush( |
| brush: skrifa::color::Brush, |
| cpal: &Cpal<'_>, |
| foreground_brush: &BrushRef<'_>, |
| ) -> Brush { |
| match brush { |
| skrifa::color::Brush::Solid { |
| palette_index, |
| alpha, |
| } => color_index(cpal, palette_index) |
| .map(|it| Brush::Solid(it.with_alpha_factor(alpha))) |
| .unwrap_or(apply_alpha(foreground_brush.to_owned(), alpha)), |
| |
| skrifa::color::Brush::LinearGradient { |
| p0, |
| p1, |
| color_stops, |
| extend, |
| } => Brush::Gradient( |
| Gradient::new_linear(conv_point(p0), conv_point(p1)) |
| .with_extend(conv_extend(extend)) |
| .with_stops(ColorStopsConverter(color_stops, cpal, foreground_brush)), |
| ), |
| skrifa::color::Brush::RadialGradient { |
| c0, |
| r0, |
| c1, |
| r1, |
| color_stops, |
| extend, |
| } => Brush::Gradient( |
| Gradient::new_two_point_radial(conv_point(c0), r0, conv_point(c1), r1) |
| .with_extend(conv_extend(extend)) |
| .with_stops(ColorStopsConverter(color_stops, cpal, foreground_brush)), |
| ), |
| skrifa::color::Brush::SweepGradient { |
| c0, |
| start_angle, |
| end_angle, |
| color_stops, |
| extend, |
| } => Brush::Gradient( |
| Gradient::new_sweep(conv_point(c0), start_angle, end_angle) |
| .with_extend(conv_extend(extend)) |
| .with_stops(ColorStopsConverter(color_stops, cpal, foreground_brush)), |
| ), |
| } |
| } |
| |
| fn apply_alpha(mut brush: Brush, alpha: f32) -> Brush { |
| match &mut brush { |
| Brush::Solid(color) => *color = color.with_alpha_factor(alpha), |
| Brush::Gradient(grad) => grad |
| .stops |
| .iter_mut() |
| .for_each(|it| it.color = it.color.with_alpha_factor(alpha)), |
| // Cannot apply an alpha factor to |
| Brush::Image(_) => {} |
| } |
| brush |
| } |
| |
| fn color_index(cpal: &'_ Cpal<'_>, palette_index: u16) -> Option<Color> { |
| // The "application determined" foreground colour should be used |
| // This will be handled by the caller |
| if palette_index == 0xFFFF { |
| return None; |
| } |
| let actual_colors = cpal.color_records_array().unwrap().unwrap(); |
| // TODO: Error reporting in the `None` case |
| let color = actual_colors.get(usize::from(palette_index))?; |
| Some(Color::rgba8( |
| color.red, |
| color.green, |
| color.blue, |
| color.alpha, |
| )) |
| } |
| |
| fn conv_point(point: skrifa::raw::types::Point<f32>) -> Point { |
| Point::new(point.x.into(), point.y.into()) |
| } |
| |
| fn conv_extend(extend: skrifa::color::Extend) -> Extend { |
| match extend { |
| skrifa::color::Extend::Pad => Extend::Pad, |
| skrifa::color::Extend::Repeat => Extend::Reflect, |
| skrifa::color::Extend::Reflect => Extend::Repeat, |
| // TODO: Error reporting on unknown variant? |
| _ => Extend::Pad, |
| } |
| } |
| |
| struct ColorStopsConverter<'a>( |
| &'a [skrifa::color::ColorStop], |
| &'a Cpal<'a>, |
| &'a BrushRef<'a>, |
| ); |
| |
| impl ColorStopsSource for ColorStopsConverter<'_> { |
| fn collect_stops(&self, vec: &mut ColorStops) { |
| for item in self.0 { |
| let color = color_index(self.1, item.palette_index); |
| let color = match color { |
| Some(color) => color, |
| // If we should use the "application defined fallback colour", |
| // then *try* and determine that from the existing brush |
| None => match self.2 { |
| BrushRef::Solid(c) => *c, |
| // TODO: Report a warning? if either of these cases are reached |
| // In theory, it's possible to have a gradient containing images and other gradients |
| // but implementing that just for this case isn't worthwhile |
| BrushRef::Gradient(grad) => grad |
| .stops |
| .first() |
| .map(|it| it.color) |
| .unwrap_or(Color::TRANSPARENT), |
| BrushRef::Image(_) => Color::BLACK, |
| }, |
| }; |
| let color = color.with_alpha_factor(item.alpha); |
| vec.push(ColorStop { |
| color, |
| offset: item.offset, |
| }); |
| } |
| } |
| } |