mirror of
https://github.com/italicsjenga/vello.git
synced 2025-01-10 12:41:30 +11:00
b103a55301
Adds full support for COLRv1 radial gradients based on the two-point conical gradient algorithm at https://skia.org/docs/dev/design/conical/ Also adds robustness to degenerate cases in gradient encoding: * Radial where p0 == p1 && r0 == r1 renders transparent solid * Empty stops render as transparent solid * Single stop renders as solid
463 lines
16 KiB
Rust
463 lines
16 KiB
Rust
// Copyright 2022 The Vello authors
|
|
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
|
|
use super::{DrawColor, DrawTag, PathEncoder, PathTag, Transform};
|
|
|
|
use peniko::{kurbo::Shape, BlendMode, BrushRef, Color};
|
|
|
|
#[cfg(feature = "full")]
|
|
use {
|
|
super::{DrawImage, DrawLinearGradient, DrawRadialGradient, Glyph, GlyphRun, Patch},
|
|
fello::NormalizedCoord,
|
|
peniko::{ColorStop, Extend, GradientKind, Image},
|
|
};
|
|
|
|
/// Encoded data streams for a scene.
|
|
#[derive(Clone, Default)]
|
|
pub struct Encoding {
|
|
/// The path tag stream.
|
|
pub path_tags: Vec<PathTag>,
|
|
/// The path data stream.
|
|
pub path_data: Vec<u8>,
|
|
/// The draw tag stream.
|
|
pub draw_tags: Vec<DrawTag>,
|
|
/// The draw data stream.
|
|
pub draw_data: Vec<u8>,
|
|
/// The transform stream.
|
|
pub transforms: Vec<Transform>,
|
|
/// The line width stream.
|
|
pub linewidths: Vec<f32>,
|
|
/// Late bound resource data.
|
|
#[cfg(feature = "full")]
|
|
pub resources: Resources,
|
|
/// Number of encoded paths.
|
|
pub n_paths: u32,
|
|
/// Number of encoded path segments.
|
|
pub n_path_segments: u32,
|
|
/// Number of encoded clips/layers.
|
|
pub n_clips: u32,
|
|
/// Number of unclosed clips/layers.
|
|
pub n_open_clips: u32,
|
|
}
|
|
|
|
impl Encoding {
|
|
/// Creates a new encoding.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Returns true if the encoding is empty.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.path_tags.is_empty()
|
|
}
|
|
|
|
/// Clears the encoding.
|
|
pub fn reset(&mut self, is_fragment: bool) {
|
|
self.transforms.clear();
|
|
self.path_tags.clear();
|
|
self.path_data.clear();
|
|
self.linewidths.clear();
|
|
self.draw_data.clear();
|
|
self.draw_tags.clear();
|
|
self.n_paths = 0;
|
|
self.n_path_segments = 0;
|
|
self.n_clips = 0;
|
|
self.n_open_clips = 0;
|
|
#[cfg(feature = "full")]
|
|
self.resources.reset();
|
|
if !is_fragment {
|
|
self.transforms.push(Transform::IDENTITY);
|
|
self.linewidths.push(-1.0);
|
|
}
|
|
}
|
|
|
|
/// Appends another encoding to this one with an optional transform.
|
|
pub fn append(&mut self, other: &Self, transform: &Option<Transform>) {
|
|
#[cfg(feature = "full")]
|
|
let glyph_runs_base = {
|
|
let offsets = self.stream_offsets();
|
|
let stops_base = self.resources.color_stops.len();
|
|
let glyph_runs_base = self.resources.glyph_runs.len();
|
|
let glyphs_base = self.resources.glyphs.len();
|
|
let coords_base = self.resources.normalized_coords.len();
|
|
self.resources
|
|
.glyphs
|
|
.extend_from_slice(&other.resources.glyphs);
|
|
self.resources
|
|
.normalized_coords
|
|
.extend_from_slice(&other.resources.normalized_coords);
|
|
self.resources
|
|
.glyph_runs
|
|
.extend(other.resources.glyph_runs.iter().cloned().map(|mut run| {
|
|
run.glyphs.start += glyphs_base;
|
|
run.normalized_coords.start += coords_base;
|
|
run.stream_offsets.path_tags += offsets.path_tags;
|
|
run.stream_offsets.path_data += offsets.path_data;
|
|
run.stream_offsets.draw_tags += offsets.draw_tags;
|
|
run.stream_offsets.draw_data += offsets.draw_data;
|
|
run.stream_offsets.transforms += offsets.transforms;
|
|
run.stream_offsets.linewidths += offsets.linewidths;
|
|
run
|
|
}));
|
|
self.resources
|
|
.patches
|
|
.extend(other.resources.patches.iter().map(|patch| match patch {
|
|
Patch::Ramp {
|
|
draw_data_offset: offset,
|
|
stops,
|
|
extend,
|
|
} => {
|
|
let stops = stops.start + stops_base..stops.end + stops_base;
|
|
Patch::Ramp {
|
|
draw_data_offset: offset + offsets.draw_data,
|
|
stops,
|
|
extend: *extend,
|
|
}
|
|
}
|
|
Patch::GlyphRun { index } => Patch::GlyphRun {
|
|
index: index + glyph_runs_base,
|
|
},
|
|
Patch::Image {
|
|
image,
|
|
draw_data_offset,
|
|
} => Patch::Image {
|
|
image: image.clone(),
|
|
draw_data_offset: *draw_data_offset + offsets.draw_data,
|
|
},
|
|
}));
|
|
self.resources
|
|
.color_stops
|
|
.extend_from_slice(&other.resources.color_stops);
|
|
glyph_runs_base
|
|
};
|
|
self.path_tags.extend_from_slice(&other.path_tags);
|
|
self.path_data.extend_from_slice(&other.path_data);
|
|
self.draw_tags.extend_from_slice(&other.draw_tags);
|
|
self.draw_data.extend_from_slice(&other.draw_data);
|
|
self.n_paths += other.n_paths;
|
|
self.n_path_segments += other.n_path_segments;
|
|
self.n_clips += other.n_clips;
|
|
self.n_open_clips += other.n_open_clips;
|
|
if let Some(transform) = *transform {
|
|
self.transforms
|
|
.extend(other.transforms.iter().map(|x| transform * *x));
|
|
#[cfg(feature = "full")]
|
|
for run in &mut self.resources.glyph_runs[glyph_runs_base..] {
|
|
run.transform = transform * run.transform;
|
|
}
|
|
} else {
|
|
self.transforms.extend_from_slice(&other.transforms);
|
|
}
|
|
self.linewidths.extend_from_slice(&other.linewidths);
|
|
}
|
|
|
|
/// Returns a snapshot of the current stream offsets.
|
|
pub fn stream_offsets(&self) -> StreamOffsets {
|
|
StreamOffsets {
|
|
path_tags: self.path_tags.len(),
|
|
path_data: self.path_data.len(),
|
|
draw_tags: self.draw_tags.len(),
|
|
draw_data: self.draw_data.len(),
|
|
transforms: self.transforms.len(),
|
|
linewidths: self.linewidths.len(),
|
|
}
|
|
}
|
|
|
|
/// Encodes a linewidth.
|
|
pub fn encode_linewidth(&mut self, linewidth: f32) {
|
|
if self.linewidths.last() != Some(&linewidth) {
|
|
self.path_tags.push(PathTag::LINEWIDTH);
|
|
self.linewidths.push(linewidth);
|
|
}
|
|
}
|
|
|
|
/// Encodes a transform.
|
|
///
|
|
/// If the given transform is different from the current one, encodes it and
|
|
/// returns true. Otherwise, encodes nothing and returns false.
|
|
pub fn encode_transform(&mut self, transform: Transform) -> bool {
|
|
if self.transforms.last() != Some(&transform) {
|
|
self.path_tags.push(PathTag::TRANSFORM);
|
|
self.transforms.push(transform);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Returns an encoder for encoding a path. If `is_fill` is true, all subpaths will
|
|
/// be automatically closed.
|
|
pub fn encode_path(&mut self, is_fill: bool) -> PathEncoder {
|
|
PathEncoder::new(
|
|
&mut self.path_tags,
|
|
&mut self.path_data,
|
|
&mut self.n_path_segments,
|
|
&mut self.n_paths,
|
|
is_fill,
|
|
)
|
|
}
|
|
|
|
/// Encodes a shape. If `is_fill` is true, all subpaths will be automatically closed.
|
|
/// Returns true if a non-zero number of segments were encoded.
|
|
pub fn encode_shape(&mut self, shape: &impl Shape, is_fill: bool) -> bool {
|
|
let mut encoder = self.encode_path(is_fill);
|
|
encoder.shape(shape);
|
|
encoder.finish(true) != 0
|
|
}
|
|
|
|
/// Encodes a brush with an optional alpha modifier.
|
|
#[allow(unused_variables)]
|
|
pub fn encode_brush<'b>(&mut self, brush: impl Into<BrushRef<'b>>, alpha: f32) {
|
|
#[cfg(feature = "full")]
|
|
use super::math::point_to_f32;
|
|
match brush.into() {
|
|
BrushRef::Solid(color) => {
|
|
let color = if alpha != 1.0 {
|
|
color.with_alpha_factor(alpha)
|
|
} else {
|
|
color
|
|
};
|
|
self.encode_color(DrawColor::new(color));
|
|
}
|
|
#[cfg(feature = "full")]
|
|
BrushRef::Gradient(gradient) => match gradient.kind {
|
|
GradientKind::Linear { start, end } => {
|
|
self.encode_linear_gradient(
|
|
DrawLinearGradient {
|
|
index: 0,
|
|
p0: point_to_f32(start),
|
|
p1: point_to_f32(end),
|
|
},
|
|
gradient.stops.iter().copied(),
|
|
alpha,
|
|
gradient.extend,
|
|
);
|
|
}
|
|
GradientKind::Radial {
|
|
start_center,
|
|
start_radius,
|
|
end_center,
|
|
end_radius,
|
|
} => {
|
|
self.encode_radial_gradient(
|
|
DrawRadialGradient {
|
|
index: 0,
|
|
p0: point_to_f32(start_center),
|
|
p1: point_to_f32(end_center),
|
|
r0: start_radius,
|
|
r1: end_radius,
|
|
},
|
|
gradient.stops.iter().copied(),
|
|
alpha,
|
|
gradient.extend,
|
|
);
|
|
}
|
|
GradientKind::Sweep { .. } => {
|
|
todo!("sweep gradients aren't supported yet!")
|
|
}
|
|
},
|
|
#[cfg(feature = "full")]
|
|
BrushRef::Image(image) => {
|
|
#[cfg(feature = "full")]
|
|
self.encode_image(image, alpha);
|
|
}
|
|
#[cfg(not(feature = "full"))]
|
|
_ => panic!("brushes other than solid require the 'full' feature to be enabled"),
|
|
}
|
|
}
|
|
|
|
/// Encodes a solid color brush.
|
|
pub fn encode_color(&mut self, color: DrawColor) {
|
|
self.draw_tags.push(DrawTag::COLOR);
|
|
self.draw_data.extend_from_slice(bytemuck::bytes_of(&color));
|
|
}
|
|
|
|
/// Encodes a linear gradient brush.
|
|
#[cfg(feature = "full")]
|
|
pub fn encode_linear_gradient(
|
|
&mut self,
|
|
gradient: DrawLinearGradient,
|
|
color_stops: impl Iterator<Item = ColorStop>,
|
|
alpha: f32,
|
|
extend: Extend,
|
|
) {
|
|
match self.add_ramp(color_stops, alpha, extend) {
|
|
RampStops::Empty => self.encode_color(DrawColor::new(Color::TRANSPARENT)),
|
|
RampStops::One(color) => self.encode_color(DrawColor::new(color)),
|
|
_ => {
|
|
self.draw_tags.push(DrawTag::LINEAR_GRADIENT);
|
|
self.draw_data
|
|
.extend_from_slice(bytemuck::bytes_of(&gradient));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Encodes a radial gradient brush.
|
|
#[cfg(feature = "full")]
|
|
pub fn encode_radial_gradient(
|
|
&mut self,
|
|
gradient: DrawRadialGradient,
|
|
color_stops: impl Iterator<Item = ColorStop>,
|
|
alpha: f32,
|
|
extend: Extend,
|
|
) {
|
|
// Match Skia's epsilon for radii comparison
|
|
const SKIA_EPSILON: f32 = 1.0 / (1 << 12) as f32;
|
|
if gradient.p0 == gradient.p1 && (gradient.r0 - gradient.r1).abs() < SKIA_EPSILON {
|
|
self.encode_color(DrawColor::new(Color::TRANSPARENT));
|
|
}
|
|
match self.add_ramp(color_stops, alpha, extend) {
|
|
RampStops::Empty => self.encode_color(DrawColor::new(Color::TRANSPARENT)),
|
|
RampStops::One(color) => self.encode_color(DrawColor::new(color)),
|
|
_ => {
|
|
self.draw_tags.push(DrawTag::RADIAL_GRADIENT);
|
|
self.draw_data
|
|
.extend_from_slice(bytemuck::bytes_of(&gradient));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Encodes an image brush.
|
|
#[cfg(feature = "full")]
|
|
pub fn encode_image(&mut self, image: &Image, _alpha: f32) {
|
|
// TODO: feed the alpha multiplier through the full pipeline for consistency
|
|
// with other brushes?
|
|
self.resources.patches.push(Patch::Image {
|
|
image: image.clone(),
|
|
draw_data_offset: self.draw_data.len(),
|
|
});
|
|
self.draw_tags.push(DrawTag::IMAGE);
|
|
self.draw_data
|
|
.extend_from_slice(bytemuck::bytes_of(&DrawImage {
|
|
xy: 0,
|
|
width_height: (image.width << 16) | (image.height & 0xFFFF),
|
|
}));
|
|
}
|
|
|
|
/// Encodes a begin clip command.
|
|
pub fn encode_begin_clip(&mut self, blend_mode: BlendMode, alpha: f32) {
|
|
use super::DrawBeginClip;
|
|
self.draw_tags.push(DrawTag::BEGIN_CLIP);
|
|
self.draw_data
|
|
.extend_from_slice(bytemuck::bytes_of(&DrawBeginClip::new(blend_mode, alpha)));
|
|
self.n_clips += 1;
|
|
self.n_open_clips += 1;
|
|
}
|
|
|
|
/// Encodes an end clip command.
|
|
pub fn encode_end_clip(&mut self) {
|
|
if self.n_open_clips > 0 {
|
|
self.draw_tags.push(DrawTag::END_CLIP);
|
|
// This is a dummy path, and will go away with the new clip impl.
|
|
self.path_tags.push(PathTag::PATH);
|
|
self.n_paths += 1;
|
|
self.n_clips += 1;
|
|
self.n_open_clips -= 1;
|
|
}
|
|
}
|
|
|
|
// Swap the last two tags in the path tag stream; used for transformed
|
|
// gradients.
|
|
pub fn swap_last_path_tags(&mut self) {
|
|
let len = self.path_tags.len();
|
|
self.path_tags.swap(len - 1, len - 2);
|
|
}
|
|
|
|
#[cfg(feature = "full")]
|
|
fn add_ramp(
|
|
&mut self,
|
|
color_stops: impl Iterator<Item = ColorStop>,
|
|
alpha: f32,
|
|
extend: Extend,
|
|
) -> RampStops {
|
|
let offset = self.draw_data.len();
|
|
let stops_start = self.resources.color_stops.len();
|
|
if alpha != 1.0 {
|
|
self.resources
|
|
.color_stops
|
|
.extend(color_stops.map(|stop| stop.with_alpha_factor(alpha)));
|
|
} else {
|
|
self.resources.color_stops.extend(color_stops);
|
|
}
|
|
let stops_end = self.resources.color_stops.len();
|
|
match stops_end - stops_start {
|
|
0 => RampStops::Empty,
|
|
1 => RampStops::One(self.resources.color_stops.pop().unwrap().color),
|
|
_ => {
|
|
self.resources.patches.push(Patch::Ramp {
|
|
draw_data_offset: offset,
|
|
stops: stops_start..stops_end,
|
|
extend,
|
|
});
|
|
RampStops::Many
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result for adding a sequence of color stops.
|
|
enum RampStops {
|
|
/// Color stop sequence was empty.
|
|
Empty,
|
|
/// Contained a single color stop.
|
|
One(Color),
|
|
/// More than one color stop.
|
|
Many,
|
|
}
|
|
|
|
/// Encoded data for late bound resources.
|
|
#[cfg(feature = "full")]
|
|
#[derive(Clone, Default)]
|
|
pub struct Resources {
|
|
/// Draw data patches for late bound resources.
|
|
pub patches: Vec<Patch>,
|
|
/// Color stop collection for gradients.
|
|
pub color_stops: Vec<ColorStop>,
|
|
/// Positioned glyph buffer.
|
|
pub glyphs: Vec<Glyph>,
|
|
/// Sequences of glyphs.
|
|
pub glyph_runs: Vec<GlyphRun>,
|
|
/// Normalized coordinate buffer for variable fonts.
|
|
pub normalized_coords: Vec<NormalizedCoord>,
|
|
}
|
|
|
|
#[cfg(feature = "full")]
|
|
impl Resources {
|
|
fn reset(&mut self) {
|
|
self.patches.clear();
|
|
self.color_stops.clear();
|
|
self.glyphs.clear();
|
|
self.glyph_runs.clear();
|
|
self.normalized_coords.clear();
|
|
}
|
|
}
|
|
|
|
/// Snapshot of offsets for encoded streams.
|
|
#[derive(Copy, Clone, Default, Debug)]
|
|
pub struct StreamOffsets {
|
|
/// Current length of path tag stream.
|
|
pub path_tags: usize,
|
|
/// Current length of path data stream.
|
|
pub path_data: usize,
|
|
/// Current length of draw tag stream.
|
|
pub draw_tags: usize,
|
|
/// Current length of draw data stream.
|
|
pub draw_data: usize,
|
|
/// Current length of transform stream.
|
|
pub transforms: usize,
|
|
/// Current length of linewidth stream.
|
|
pub linewidths: usize,
|
|
}
|
|
|
|
impl StreamOffsets {
|
|
#[cfg(feature = "full")]
|
|
pub(crate) fn add(&mut self, other: &Self) {
|
|
self.path_tags += other.path_tags;
|
|
self.path_data += other.path_data;
|
|
self.draw_tags += other.draw_tags;
|
|
self.draw_data += other.draw_data;
|
|
self.transforms += other.transforms;
|
|
self.linewidths += other.linewidths;
|
|
}
|
|
}
|