Gradient Part 4 - Conic gradients (#2325)
This commit is contained in:
parent
877ee39a8c
commit
cef2d3afca
@ -1,14 +1,19 @@
|
||||
use std::f32::consts::{PI, TAU};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use pdf_writer::types::FunctionShadingType;
|
||||
use pdf_writer::writers::StreamShadingType;
|
||||
use pdf_writer::{types::ColorSpaceOperand, Name};
|
||||
use pdf_writer::{Finish, Ref};
|
||||
use pdf_writer::{Filter, Finish, Ref};
|
||||
|
||||
use super::color::{ColorSpaceExt, PaintEncode};
|
||||
use super::color::{ColorSpaceExt, PaintEncode, QuantizedColor};
|
||||
use super::page::{PageContext, Transforms};
|
||||
use super::{AbsExt, PdfContext};
|
||||
use crate::export::pdf::deflate;
|
||||
use crate::geom::{
|
||||
Abs, Angle, Color, ColorSpace, Gradient, Numeric, Quadrant, Ratio, Relative,
|
||||
Transform,
|
||||
Abs, Angle, Color, ColorSpace, ConicGradient, Gradient, Numeric, Point, Quadrant,
|
||||
Ratio, Relative, Transform, WeightedColor,
|
||||
};
|
||||
|
||||
/// A unique-transform-aspect-ratio combination that will be encoded into the
|
||||
@ -83,6 +88,38 @@ pub fn write_gradients(ctx: &mut PdfContext) {
|
||||
|
||||
shading_pattern
|
||||
}
|
||||
Gradient::Conic(conic) => {
|
||||
let vertices = compute_vertex_stream(conic);
|
||||
|
||||
let stream_shading_id = ctx.alloc.bump();
|
||||
let mut stream_shading =
|
||||
ctx.pdf.stream_shading(stream_shading_id, &vertices);
|
||||
|
||||
ctx.colors.write(
|
||||
conic.space,
|
||||
stream_shading.color_space(),
|
||||
&mut ctx.alloc,
|
||||
);
|
||||
|
||||
let range = conic.space.range();
|
||||
stream_shading
|
||||
.bits_per_coordinate(16)
|
||||
.bits_per_component(16)
|
||||
.bits_per_flag(8)
|
||||
.shading_type(StreamShadingType::CoonsPatch)
|
||||
.decode([
|
||||
0.0, 1.0, 0.0, 1.0, range[0], range[1], range[2], range[3],
|
||||
range[4], range[5],
|
||||
])
|
||||
.anti_alias(gradient.anti_alias())
|
||||
.filter(Filter::FlateDecode);
|
||||
|
||||
stream_shading.finish();
|
||||
|
||||
let mut shading_pattern = ctx.pdf.shading_pattern(shading);
|
||||
shading_pattern.shading_ref(stream_shading_id);
|
||||
shading_pattern
|
||||
}
|
||||
};
|
||||
|
||||
shading_pattern.matrix(transform_to_array(transform));
|
||||
@ -258,29 +295,47 @@ fn register_gradient(
|
||||
Relative::Parent => transforms.container_size,
|
||||
};
|
||||
|
||||
let (offset_x, offset_y) =
|
||||
match gradient.angle().unwrap_or_else(Angle::zero).quadrant() {
|
||||
let (offset_x, offset_y) = match gradient {
|
||||
Gradient::Conic(conic) => (
|
||||
-size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0,
|
||||
-size.y * (1.0 - conic.center.y.get() / 2.0) / 2.0,
|
||||
),
|
||||
gradient => match gradient.angle().unwrap_or_else(Angle::zero).quadrant() {
|
||||
Quadrant::First => (Abs::zero(), Abs::zero()),
|
||||
Quadrant::Second => (size.x, Abs::zero()),
|
||||
Quadrant::Third => (size.x, size.y),
|
||||
Quadrant::Fourth => (Abs::zero(), size.y),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
let rotation = match gradient {
|
||||
Gradient::Conic(_) => Angle::zero(),
|
||||
gradient => gradient.angle().unwrap_or_default(),
|
||||
};
|
||||
|
||||
let transform = match gradient.unwrap_relative(false) {
|
||||
Relative::Self_ => transforms.transform,
|
||||
Relative::Parent => transforms.container_transform,
|
||||
};
|
||||
|
||||
let scale_offset = match gradient {
|
||||
Gradient::Conic(_) => 4.0_f64,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
let pdf_gradient = PdfGradient {
|
||||
aspect_ratio: size.aspect_ratio(),
|
||||
transform: transform
|
||||
.pre_concat(Transform::translate(offset_x, offset_y))
|
||||
.pre_concat(Transform::translate(
|
||||
offset_x * scale_offset,
|
||||
offset_y * scale_offset,
|
||||
))
|
||||
.pre_concat(Transform::scale(
|
||||
Ratio::new(size.x.to_pt()),
|
||||
Ratio::new(size.y.to_pt()),
|
||||
Ratio::new(size.x.to_pt() * scale_offset),
|
||||
Ratio::new(size.y.to_pt() * scale_offset),
|
||||
))
|
||||
.pre_concat(Transform::rotate(Gradient::correct_aspect_ratio(
|
||||
gradient.angle().unwrap_or_else(Angle::zero),
|
||||
rotation,
|
||||
size.aspect_ratio(),
|
||||
))),
|
||||
gradient: gradient.clone(),
|
||||
@ -301,3 +356,189 @@ fn transform_to_array(ts: Transform) -> [f32; 6] {
|
||||
ts.ty.to_f32(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Writes a single Coons Patch as defined in the PDF specification
|
||||
/// to a binary vec.
|
||||
///
|
||||
/// Structure:
|
||||
/// - flag: `u8`
|
||||
/// - points: `[u16; 24]`
|
||||
/// - colors: `[u16; 12]`
|
||||
fn write_patch(
|
||||
target: &mut Vec<u8>,
|
||||
t: f32,
|
||||
t1: f32,
|
||||
c0: [u16; 3],
|
||||
c1: [u16; 3],
|
||||
angle: Angle,
|
||||
) {
|
||||
let theta = -TAU * t + angle.to_rad() as f32 + PI;
|
||||
let theta1 = -TAU * t1 + angle.to_rad() as f32 + PI;
|
||||
|
||||
let (cp1, cp2) =
|
||||
control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1);
|
||||
|
||||
// Push the flag
|
||||
target.push(0);
|
||||
|
||||
let p1 =
|
||||
[u16::quantize(0.5, [0.0, 1.0]).to_be(), u16::quantize(0.5, [0.0, 1.0]).to_be()];
|
||||
|
||||
let p2 = [
|
||||
u16::quantize(theta.cos(), [-1.0, 1.0]).to_be(),
|
||||
u16::quantize(theta.sin(), [-1.0, 1.0]).to_be(),
|
||||
];
|
||||
|
||||
let p3 = [
|
||||
u16::quantize(theta1.cos(), [-1.0, 1.0]).to_be(),
|
||||
u16::quantize(theta1.sin(), [-1.0, 1.0]).to_be(),
|
||||
];
|
||||
|
||||
let cp1 = [
|
||||
u16::quantize(cp1.x.to_f32(), [0.0, 1.0]).to_be(),
|
||||
u16::quantize(cp1.y.to_f32(), [0.0, 1.0]).to_be(),
|
||||
];
|
||||
|
||||
let cp2 = [
|
||||
u16::quantize(cp2.x.to_f32(), [0.0, 1.0]).to_be(),
|
||||
u16::quantize(cp2.y.to_f32(), [0.0, 1.0]).to_be(),
|
||||
];
|
||||
|
||||
// Push the points
|
||||
target.extend_from_slice(bytemuck::cast_slice(&[
|
||||
p1, p1, p2, p2, cp1, cp2, p3, p3, p1, p1, p1, p1,
|
||||
]));
|
||||
|
||||
let colors =
|
||||
[c0.map(u16::to_be), c0.map(u16::to_be), c1.map(u16::to_be), c1.map(u16::to_be)];
|
||||
|
||||
// Push the colors.
|
||||
target.extend_from_slice(bytemuck::cast_slice(&colors));
|
||||
}
|
||||
|
||||
fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point, Point) {
|
||||
let n = (TAU / (angle_end - angle_start)).abs();
|
||||
let f = ((angle_end - angle_start) / n).tan() * 4.0 / 3.0;
|
||||
|
||||
let p1 = c + Point::new(
|
||||
Abs::pt((r * angle_start.cos() - f * r * angle_start.sin()) as f64),
|
||||
Abs::pt((r * angle_start.sin() + f * r * angle_start.cos()) as f64),
|
||||
);
|
||||
|
||||
let p2 = c + Point::new(
|
||||
Abs::pt((r * angle_end.cos() + f * r * angle_end.sin()) as f64),
|
||||
Abs::pt((r * angle_end.sin() - f * r * angle_end.cos()) as f64),
|
||||
);
|
||||
|
||||
(p1, p2)
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
|
||||
// Generated vertices for the Coons patches
|
||||
let mut vertices = Vec::new();
|
||||
|
||||
// We want to generate a vertex based on some conditions, either:
|
||||
// - At the boundary of a stop
|
||||
// - At the boundary of a quadrant
|
||||
// - When we cross the boundary of a hue turn (for HSV and HSL only)
|
||||
for window in conic.stops.windows(2) {
|
||||
let ((c0, t0), (c1, t1)) = (window[0], window[1]);
|
||||
|
||||
// Skip stops with the same position
|
||||
if t0 == t1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the angle between the two stops is greater than 90 degrees, we need to
|
||||
// generate a vertex at the boundary of the quadrant.
|
||||
// However, we add more stops in-between to make the gradient smoother, so we
|
||||
// need to generate a vertex at least every 5 degrees.
|
||||
// If the colors are the same, we do it every quadrant only.
|
||||
let slope = 1.0 / (t1.get() - t0.get());
|
||||
let mut t_x = t0.get();
|
||||
let dt = (t1.get() - t0.get()).min(0.25);
|
||||
while t_x < t1.get() {
|
||||
let t_next = (t_x + dt).min(t1.get());
|
||||
|
||||
let t1 = slope * (t_x - t0.get());
|
||||
let t2 = slope * (t_next - t0.get());
|
||||
|
||||
// We don't use `Gradient::sample` to avoid issues with sharp gradients.
|
||||
let c = Color::mix_iter(
|
||||
[WeightedColor::new(c0, 1.0 - t1), WeightedColor::new(c1, t1)],
|
||||
conic.space,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let c_next = Color::mix_iter(
|
||||
[WeightedColor::new(c0, 1.0 - t2), WeightedColor::new(c1, t2)],
|
||||
conic.space,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// If the color space is HSL or HSV, and we cross the 0°/360° boundary,
|
||||
// we need to create two separate stops.
|
||||
if conic.space == ColorSpace::Hsl || conic.space == ColorSpace::Hsv {
|
||||
let [h1, s1, x1, _] = c.to_space(conic.space).to_vec4();
|
||||
let [h2, s2, x2, _] = c_next.to_space(conic.space).to_vec4();
|
||||
|
||||
// Compute the intermediary stop at 360°.
|
||||
if (h1 - h2).abs() > 180.0 {
|
||||
let h1 = if h1 < h2 { h1 + 360.0 } else { h1 };
|
||||
let h2 = if h2 < h1 { h2 + 360.0 } else { h2 };
|
||||
|
||||
// We compute where the crossing happens between zero and one
|
||||
let t = (360.0 - h1) / (h2 - h1);
|
||||
// We then map it back to the original range.
|
||||
let t_prime = t * (t_next as f32 - t_x as f32) + t_x as f32;
|
||||
|
||||
// If the crossing happens between the two stops,
|
||||
// we need to create an extra stop.
|
||||
if t_prime <= t_next as f32 && t_prime >= t_x as f32 {
|
||||
let c0 = [1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t];
|
||||
let c1 = [0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t];
|
||||
let c0 = c0.map(|c| u16::quantize(c, [0.0, 1.0]));
|
||||
let c1 = c1.map(|c| u16::quantize(c, [0.0, 1.0]));
|
||||
|
||||
write_patch(
|
||||
&mut vertices,
|
||||
t_x as f32,
|
||||
t_prime,
|
||||
conic.space.convert(c),
|
||||
c0,
|
||||
conic.angle,
|
||||
);
|
||||
|
||||
write_patch(&mut vertices, t_prime, t_prime, c0, c1, conic.angle);
|
||||
|
||||
write_patch(
|
||||
&mut vertices,
|
||||
t_prime,
|
||||
t_next as f32,
|
||||
c1,
|
||||
conic.space.convert(c_next),
|
||||
conic.angle,
|
||||
);
|
||||
|
||||
t_x = t_next;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_patch(
|
||||
&mut vertices,
|
||||
t_x as f32,
|
||||
t_next as f32,
|
||||
conic.space.convert(c),
|
||||
conic.space.convert(c_next),
|
||||
conic.angle,
|
||||
);
|
||||
|
||||
t_x = t_next;
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(deflate(&vertices))
|
||||
}
|
||||
|
@ -735,7 +735,7 @@ impl From<sk::Transform> for Transform {
|
||||
}
|
||||
}
|
||||
|
||||
// Transforms a [`Paint`] into a [`sk::Paint`].
|
||||
/// Transforms a [`Paint`] into a [`sk::Paint`].
|
||||
/// Applying the necessary transform, if the paint is a gradient.
|
||||
///
|
||||
/// `gradient_map` is used to scale and move the gradient being sampled,
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::f32::consts::TAU;
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
use std::io::Read;
|
||||
|
||||
@ -17,6 +18,11 @@ use crate::geom::{
|
||||
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
||||
use crate::util::hash128;
|
||||
|
||||
/// The number of segments in a conic gradient.
|
||||
/// This is a heuristic value that seems to work well.
|
||||
/// Smaller values could be interesting for optimization.
|
||||
const CONIC_SEGMENT: usize = 360;
|
||||
|
||||
/// Export a frame into a SVG file.
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn svg(frame: &Frame) -> String {
|
||||
@ -76,6 +82,8 @@ struct SVGRenderer {
|
||||
/// The `Ratio` is the aspect ratio of the gradient, this is used to correct
|
||||
/// the angle of the gradient.
|
||||
gradients: Deduplicator<(Gradient, Ratio)>,
|
||||
/// These are the gradients that compose a conic gradient.
|
||||
conic_subgradients: Deduplicator<SVGSubGradient>,
|
||||
}
|
||||
|
||||
/// Contextual information for rendering.
|
||||
@ -131,6 +139,21 @@ struct GradientRef {
|
||||
transform: Transform,
|
||||
}
|
||||
|
||||
/// A subgradient for conic gradients.
|
||||
#[derive(Hash)]
|
||||
struct SVGSubGradient {
|
||||
/// The center point of the gradient.
|
||||
center: Axes<Ratio>,
|
||||
/// The start point of the subgradient.
|
||||
t0: Angle,
|
||||
/// The end point of the subgradient.
|
||||
t1: Angle,
|
||||
/// The color at the start point of the subgradient.
|
||||
c0: Color,
|
||||
/// The color at the end point of the subgradient.
|
||||
c1: Color,
|
||||
}
|
||||
|
||||
/// The kind of linear gradient.
|
||||
#[derive(Hash, Clone, Copy, PartialEq, Eq)]
|
||||
enum GradientKind {
|
||||
@ -138,6 +161,8 @@ enum GradientKind {
|
||||
Linear,
|
||||
/// A radial gradient.
|
||||
Radial,
|
||||
/// A conic gradient.
|
||||
Conic,
|
||||
}
|
||||
|
||||
impl From<&Gradient> for GradientKind {
|
||||
@ -145,6 +170,7 @@ impl From<&Gradient> for GradientKind {
|
||||
match value {
|
||||
Gradient::Linear { .. } => GradientKind::Linear,
|
||||
Gradient::Radial { .. } => GradientKind::Radial,
|
||||
Gradient::Conic { .. } => GradientKind::Conic,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,6 +196,7 @@ impl SVGRenderer {
|
||||
clip_paths: Deduplicator::new('c'),
|
||||
gradient_refs: Deduplicator::new('g'),
|
||||
gradients: Deduplicator::new('f'),
|
||||
conic_subgradients: Deduplicator::new('s'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -572,6 +599,7 @@ impl SVGRenderer {
|
||||
self.write_clip_path_defs();
|
||||
self.write_gradients();
|
||||
self.write_gradient_refs();
|
||||
self.write_subgradients();
|
||||
self.xml.end_document()
|
||||
}
|
||||
|
||||
@ -681,6 +709,87 @@ impl SVGRenderer {
|
||||
self.xml.write_attribute("fy", &radial.focal_center.y.get());
|
||||
self.xml.write_attribute("fr", &radial.focal_radius.get());
|
||||
}
|
||||
Gradient::Conic(conic) => {
|
||||
self.xml.start_element("pattern");
|
||||
self.xml.write_attribute("id", &id);
|
||||
self.xml.write_attribute("viewBox", "0 0 1 1");
|
||||
self.xml.write_attribute("preserveAspectRatio", "none");
|
||||
self.xml.write_attribute("patternUnits", "userSpaceOnUse");
|
||||
self.xml.write_attribute("width", "2");
|
||||
self.xml.write_attribute("height", "2");
|
||||
self.xml.write_attribute("x", "-0.5");
|
||||
self.xml.write_attribute("y", "-0.5");
|
||||
|
||||
// The rotation angle, negated to match rotation in PNG.
|
||||
let angle: f32 =
|
||||
-(Gradient::correct_aspect_ratio(conic.angle, *ratio).to_rad()
|
||||
as f32)
|
||||
.rem_euclid(TAU);
|
||||
let center: (f32, f32) =
|
||||
(conic.center.x.get() as f32, conic.center.y.get() as f32);
|
||||
|
||||
// We build an arg segment for each segment of a circle.
|
||||
let dtheta = TAU / CONIC_SEGMENT as f32;
|
||||
for i in 0..CONIC_SEGMENT {
|
||||
let theta1 = dtheta * i as f32;
|
||||
let theta2 = dtheta * (i + 1) as f32;
|
||||
|
||||
// Create the path for the segment.
|
||||
let mut builder = SvgPathBuilder::default();
|
||||
builder.move_to(
|
||||
correct_pattern_pos(center.0),
|
||||
correct_pattern_pos(center.1),
|
||||
);
|
||||
builder.line_to(
|
||||
correct_pattern_pos(-2.0 * (theta1 + angle).cos() + center.0),
|
||||
correct_pattern_pos(2.0 * (theta1 + angle).sin() + center.1),
|
||||
);
|
||||
builder.arc(
|
||||
(2.0, 2.0),
|
||||
0.0,
|
||||
0,
|
||||
1,
|
||||
(
|
||||
correct_pattern_pos(
|
||||
-2.0 * (theta2 + angle).cos() + center.0,
|
||||
),
|
||||
correct_pattern_pos(
|
||||
2.0 * (theta2 + angle).sin() + center.1,
|
||||
),
|
||||
),
|
||||
);
|
||||
builder.close();
|
||||
|
||||
let t1 = (i as f32) / CONIC_SEGMENT as f32;
|
||||
let t2 = (i + 1) as f32 / CONIC_SEGMENT as f32;
|
||||
let subgradient = SVGSubGradient {
|
||||
center: conic.center,
|
||||
t0: Angle::rad((theta1 + angle) as f64),
|
||||
t1: Angle::rad((theta2 + angle) as f64),
|
||||
c0: gradient
|
||||
.sample(RatioOrAngle::Ratio(Ratio::new(t1 as f64))),
|
||||
c1: gradient
|
||||
.sample(RatioOrAngle::Ratio(Ratio::new(t2 as f64))),
|
||||
};
|
||||
let id = self
|
||||
.conic_subgradients
|
||||
.insert_with(hash128(&subgradient), || subgradient);
|
||||
|
||||
// Add the path to the pattern.
|
||||
self.xml.start_element("path");
|
||||
self.xml.write_attribute("d", &builder.0);
|
||||
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
|
||||
self.xml
|
||||
.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
||||
self.xml.write_attribute("stroke-width", "0");
|
||||
self.xml.write_attribute("shape-rendering", "optimizeSpeed");
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
// We skip the default stop generation code.
|
||||
self.xml.end_element();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for window in gradient.stops_ref().windows(2) {
|
||||
@ -726,6 +835,43 @@ impl SVGRenderer {
|
||||
self.xml.end_element()
|
||||
}
|
||||
|
||||
/// Write the sub-gradients that are used for conic gradients.
|
||||
fn write_subgradients(&mut self) {
|
||||
if self.conic_subgradients.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "subgradients");
|
||||
for (id, gradient) in self.conic_subgradients.iter() {
|
||||
let x1 = 2.0 - gradient.t0.cos() as f32 + gradient.center.x.get() as f32;
|
||||
let y1 = gradient.t0.sin() as f32 + gradient.center.y.get() as f32;
|
||||
let x2 = 2.0 - gradient.t1.cos() as f32 + gradient.center.x.get() as f32;
|
||||
let y2 = gradient.t1.sin() as f32 + gradient.center.y.get() as f32;
|
||||
|
||||
self.xml.start_element("linearGradient");
|
||||
self.xml.write_attribute("id", &id);
|
||||
self.xml.write_attribute("gradientUnits", "objectBoundingBox");
|
||||
self.xml.write_attribute("x1", &x1);
|
||||
self.xml.write_attribute("y1", &y1);
|
||||
self.xml.write_attribute("x2", &x2);
|
||||
self.xml.write_attribute("y2", &y2);
|
||||
|
||||
self.xml.start_element("stop");
|
||||
self.xml.write_attribute("offset", "0%");
|
||||
self.xml.write_attribute("stop-color", &gradient.c0.to_hex());
|
||||
self.xml.end_element();
|
||||
|
||||
self.xml.start_element("stop");
|
||||
self.xml.write_attribute("offset", "100%");
|
||||
self.xml.write_attribute("stop-color", &gradient.c1.to_hex());
|
||||
self.xml.end_element();
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
fn write_gradient_refs(&mut self) {
|
||||
if self.gradient_refs.is_empty() {
|
||||
return;
|
||||
@ -749,6 +895,13 @@ impl SVGRenderer {
|
||||
&SvgMatrix(gradient_ref.transform),
|
||||
);
|
||||
}
|
||||
GradientKind::Conic => {
|
||||
self.xml.start_element("pattern");
|
||||
self.xml.write_attribute(
|
||||
"patternTransform",
|
||||
&SvgMatrix(gradient_ref.transform),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.xml.write_attribute("id", &id);
|
||||
@ -996,6 +1149,26 @@ impl SvgPathBuilder {
|
||||
self.line_to(width, 0.0);
|
||||
self.close();
|
||||
}
|
||||
|
||||
/// Creates an arc path.
|
||||
fn arc(
|
||||
&mut self,
|
||||
radius: (f32, f32),
|
||||
x_axis_rot: f32,
|
||||
large_arc_flag: u32,
|
||||
sweep_flag: u32,
|
||||
pos: (f32, f32),
|
||||
) {
|
||||
write!(
|
||||
&mut self.0,
|
||||
"A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ",
|
||||
rx = radius.0,
|
||||
ry = radius.1,
|
||||
x = pos.0,
|
||||
y = pos.1,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for SVG path. This is used to build the path for a glyph.
|
||||
@ -1091,3 +1264,8 @@ impl ColorEncode for Color {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a coordinate in a unit size square to a coordinate in the pattern.
|
||||
fn correct_pattern_pos(x: f32) -> f32 {
|
||||
(x + 0.5) / 2.0
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ use crate::syntax::{Span, Spanned};
|
||||
/// A color gradient.
|
||||
///
|
||||
/// Typst supports linear gradients through the
|
||||
/// [`gradient.linear` function]($gradient.linear) and radial gradients through
|
||||
/// the [`gradient.radial` function]($gradient.radial). Conic gradients will be
|
||||
/// available soon.
|
||||
/// [`gradient.linear` function]($gradient.linear), radial gradients through
|
||||
/// the [`gradient.radial` function]($gradient.radial), and conic gradients
|
||||
/// through the [`gradient.conic` function]($gradient.conic).
|
||||
///
|
||||
/// See the [tracking issue](https://github.com/typst/typst/issues/2282) for
|
||||
/// more details on the progress of gradient implementation.
|
||||
@ -27,6 +27,7 @@ use crate::syntax::{Span, Spanned};
|
||||
/// dir: ltr,
|
||||
/// square(size: 50pt, fill: gradient.linear(..color.map.rainbow)),
|
||||
/// square(size: 50pt, fill: gradient.radial(..color.map.rainbow)),
|
||||
/// square(size: 50pt, fill: gradient.conic(..color.map.rainbow)),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
@ -174,6 +175,7 @@ use crate::syntax::{Span, Spanned};
|
||||
pub enum Gradient {
|
||||
Linear(Arc<LinearGradient>),
|
||||
Radial(Arc<RadialGradient>),
|
||||
Conic(Arc<ConicGradient>),
|
||||
}
|
||||
|
||||
#[scope]
|
||||
@ -365,6 +367,75 @@ impl Gradient {
|
||||
})))
|
||||
}
|
||||
|
||||
/// Creates a new conic gradient (i.e a gradient whose color changes
|
||||
/// radially around a center point).
|
||||
///
|
||||
/// ```example
|
||||
/// #circle(
|
||||
/// radius: 20pt,
|
||||
/// fill: gradient.conic(..color.map.viridis)
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// _Center Point_
|
||||
/// You can control the center point of the gradient by using the `center`
|
||||
/// argument. By default, the center point is the center of the shape.
|
||||
///
|
||||
/// ```example
|
||||
/// #circle(
|
||||
/// radius: 20pt,
|
||||
/// fill: gradient.conic(..color.map.viridis, center: (10%, 40%))
|
||||
/// )
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn conic(
|
||||
/// The call site of this function.
|
||||
span: Span,
|
||||
/// The color [stops](#stops) of the gradient.
|
||||
#[variadic]
|
||||
stops: Vec<Spanned<Stop>>,
|
||||
/// The angle of the gradient.
|
||||
#[named]
|
||||
#[default(Angle::zero())]
|
||||
angle: Angle,
|
||||
/// The color space in which to interpolate the gradient.
|
||||
///
|
||||
/// Defaults to a perceptually uniform color space called
|
||||
/// [Oklab]($color.oklab).
|
||||
#[named]
|
||||
#[default(ColorSpace::Oklab)]
|
||||
space: ColorSpace,
|
||||
/// The [relative placement](#relativeness) of the gradient.
|
||||
///
|
||||
/// For an element placed at the root/top level of the document, the parent
|
||||
/// is the page itself. For other elements, the parent is the innermost block,
|
||||
/// box, column, grid, or stack that contains the element.
|
||||
#[named]
|
||||
#[default(Smart::Auto)]
|
||||
relative: Smart<Relative>,
|
||||
/// The center of the last circle of the gradient.
|
||||
///
|
||||
/// A value of `{(50%, 50%)}` means that the end circle is
|
||||
/// centered inside of its container.
|
||||
#[named]
|
||||
#[default(Axes::splat(Ratio::new(0.5)))]
|
||||
center: Axes<Ratio>,
|
||||
) -> SourceResult<Gradient> {
|
||||
if stops.len() < 2 {
|
||||
bail!(error!(span, "a gradient must have at least two stops")
|
||||
.with_hint("try filling the shape with a single color instead"));
|
||||
}
|
||||
|
||||
Ok(Gradient::Conic(Arc::new(ConicGradient {
|
||||
stops: process_stops(&stops)?,
|
||||
angle,
|
||||
center: center.map(From::from),
|
||||
space,
|
||||
relative,
|
||||
anti_alias: true,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns the stops of this gradient.
|
||||
#[func]
|
||||
pub fn stops(&self) -> Vec<Stop> {
|
||||
@ -379,6 +450,11 @@ impl Gradient {
|
||||
.iter()
|
||||
.map(|(color, offset)| Stop { color: *color, offset: Some(*offset) })
|
||||
.collect(),
|
||||
Self::Conic(conic) => conic
|
||||
.stops
|
||||
.iter()
|
||||
.map(|(color, offset)| Stop { color: *color, offset: Some(*offset) })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -388,6 +464,7 @@ impl Gradient {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.space,
|
||||
Self::Radial(radial) => radial.space,
|
||||
Self::Conic(conic) => conic.space,
|
||||
}
|
||||
}
|
||||
|
||||
@ -397,6 +474,7 @@ impl Gradient {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.relative,
|
||||
Self::Radial(radial) => radial.relative,
|
||||
Self::Conic(conic) => conic.relative,
|
||||
}
|
||||
}
|
||||
|
||||
@ -406,6 +484,7 @@ impl Gradient {
|
||||
match self {
|
||||
Self::Linear(linear) => Some(linear.angle),
|
||||
Self::Radial(_) => None,
|
||||
Self::Conic(conic) => Some(conic.angle),
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,6 +494,7 @@ impl Gradient {
|
||||
match self {
|
||||
Self::Linear(_) => Self::linear_data().into(),
|
||||
Self::Radial(_) => Self::radial_data().into(),
|
||||
Self::Conic(_) => Self::conic_data().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -436,6 +516,7 @@ impl Gradient {
|
||||
match self {
|
||||
Self::Linear(linear) => sample_stops(&linear.stops, linear.space, value),
|
||||
Self::Radial(radial) => sample_stops(&radial.stops, radial.space, value),
|
||||
Self::Conic(conic) => sample_stops(&conic.stops, conic.space, value),
|
||||
}
|
||||
}
|
||||
|
||||
@ -540,6 +621,14 @@ impl Gradient {
|
||||
relative: radial.relative,
|
||||
anti_alias: false,
|
||||
})),
|
||||
Self::Conic(conic) => Self::Conic(Arc::new(ConicGradient {
|
||||
stops,
|
||||
angle: conic.angle,
|
||||
center: conic.center,
|
||||
space: conic.space,
|
||||
relative: conic.relative,
|
||||
anti_alias: false,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
@ -605,6 +694,14 @@ impl Gradient {
|
||||
relative: radial.relative,
|
||||
anti_alias: radial.anti_alias,
|
||||
})),
|
||||
Self::Conic(conic) => Self::Conic(Arc::new(ConicGradient {
|
||||
stops,
|
||||
angle: conic.angle,
|
||||
center: conic.center,
|
||||
space: conic.space,
|
||||
relative: conic.relative,
|
||||
anti_alias: conic.anti_alias,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -615,6 +712,7 @@ impl Gradient {
|
||||
match self {
|
||||
Gradient::Linear(linear) => &linear.stops,
|
||||
Gradient::Radial(radial) => &radial.stops,
|
||||
Gradient::Conic(conic) => &conic.stops,
|
||||
}
|
||||
}
|
||||
|
||||
@ -625,18 +723,12 @@ impl Gradient {
|
||||
let (mut x, mut y) = (x / width, y / height);
|
||||
let t = match self {
|
||||
Self::Linear(linear) => {
|
||||
// Handle the direction of the gradient.
|
||||
let angle = linear.angle.to_rad().rem_euclid(TAU);
|
||||
|
||||
// Aspect ratio correction.
|
||||
let angle = (angle.tan() * height as f64).atan2(width as f64);
|
||||
let angle = match linear.angle.quadrant() {
|
||||
Quadrant::First => angle,
|
||||
Quadrant::Second => angle + PI,
|
||||
Quadrant::Third => angle + PI,
|
||||
Quadrant::Fourth => angle + TAU,
|
||||
};
|
||||
|
||||
let angle = Gradient::correct_aspect_ratio(
|
||||
linear.angle,
|
||||
Ratio::new((width / height) as f64),
|
||||
)
|
||||
.to_rad();
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
|
||||
let length = sin.abs() + cos.abs();
|
||||
@ -672,6 +764,15 @@ impl Gradient {
|
||||
((z - q).hypot() - fr) / (bz - fr)
|
||||
}
|
||||
}
|
||||
Self::Conic(conic) => {
|
||||
let (x, y) =
|
||||
(x as f64 - conic.center.x.get(), y as f64 - conic.center.y.get());
|
||||
let angle = Gradient::correct_aspect_ratio(
|
||||
conic.angle,
|
||||
Ratio::new((width / height) as f64),
|
||||
);
|
||||
((-y.atan2(x) + PI + angle.to_rad()) % TAU) / TAU
|
||||
}
|
||||
};
|
||||
|
||||
self.sample(RatioOrAngle::Ratio(Ratio::new(t.clamp(0.0, 1.0))))
|
||||
@ -682,6 +783,7 @@ impl Gradient {
|
||||
match self {
|
||||
Self::Linear(linear) => linear.anti_alias,
|
||||
Self::Radial(radial) => radial.anti_alias,
|
||||
Self::Conic(conic) => conic.anti_alias,
|
||||
}
|
||||
}
|
||||
|
||||
@ -717,6 +819,7 @@ impl Repr for Gradient {
|
||||
match self {
|
||||
Self::Radial(radial) => radial.repr(),
|
||||
Self::Linear(linear) => linear.repr(),
|
||||
Self::Conic(conic) => conic.repr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -809,7 +912,7 @@ impl Repr for RadialGradient {
|
||||
let mut r = EcoString::from("gradient.radial(");
|
||||
|
||||
if self.center.x != Ratio::new(0.5) || self.center.y != Ratio::new(0.5) {
|
||||
r.push_str("space: (");
|
||||
r.push_str("center: (");
|
||||
r.push_str(&self.center.x.repr());
|
||||
r.push_str(", ");
|
||||
r.push_str(&self.center.y.repr());
|
||||
@ -848,6 +951,71 @@ impl Repr for RadialGradient {
|
||||
r.push_str(", ");
|
||||
}
|
||||
|
||||
for (i, (color, offset)) in self.stops.iter().enumerate() {
|
||||
r.push('(');
|
||||
r.push_str(&color.repr());
|
||||
r.push_str(", ");
|
||||
r.push_str(&offset.repr());
|
||||
r.push(')');
|
||||
if i != self.stops.len() - 1 {
|
||||
r.push_str(", ");
|
||||
}
|
||||
}
|
||||
|
||||
r.push(')');
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient that interpolates between two colors radially
|
||||
/// around a center point.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct ConicGradient {
|
||||
/// The color stops of this gradient.
|
||||
pub stops: Vec<(Color, Ratio)>,
|
||||
/// The direction of this gradient.
|
||||
pub angle: Angle,
|
||||
/// The center of last circle of this gradient.
|
||||
pub center: Axes<Ratio>,
|
||||
/// The color space in which to interpolate the gradient.
|
||||
pub space: ColorSpace,
|
||||
/// The relative placement of the gradient.
|
||||
pub relative: Smart<Relative>,
|
||||
/// Whether to anti-alias the gradient (used for sharp gradients).
|
||||
pub anti_alias: bool,
|
||||
}
|
||||
|
||||
impl Repr for ConicGradient {
|
||||
fn repr(&self) -> EcoString {
|
||||
let mut r = EcoString::from("gradient.conic(");
|
||||
|
||||
let angle = self.angle.to_rad().rem_euclid(TAU);
|
||||
if angle.abs() > EPSILON {
|
||||
r.push_str("angle: ");
|
||||
r.push_str(&self.angle.repr());
|
||||
r.push_str(", ");
|
||||
}
|
||||
|
||||
if self.center.x != Ratio::new(0.5) || self.center.y != Ratio::new(0.5) {
|
||||
r.push_str("center: (");
|
||||
r.push_str(&self.center.x.repr());
|
||||
r.push_str(", ");
|
||||
r.push_str(&self.center.y.repr());
|
||||
r.push_str("), ");
|
||||
}
|
||||
|
||||
if self.space != ColorSpace::Oklab {
|
||||
r.push_str("space: ");
|
||||
r.push_str(&self.space.into_value().repr());
|
||||
r.push_str(", ");
|
||||
}
|
||||
|
||||
if self.relative.is_custom() {
|
||||
r.push_str("relative: ");
|
||||
r.push_str(&self.relative.into_value().repr());
|
||||
r.push_str(", ");
|
||||
}
|
||||
|
||||
for (i, (color, offset)) in self.stops.iter().enumerate() {
|
||||
r.push('(');
|
||||
r.push_str(&color.repr());
|
||||
|
@ -32,13 +32,15 @@ pub use self::abs::{Abs, AbsUnit};
|
||||
pub use self::align::{Align, FixedAlign, HAlign, VAlign};
|
||||
pub use self::angle::{Angle, AngleUnit, Quadrant};
|
||||
pub use self::axes::{Axes, Axis};
|
||||
pub use self::color::{Color, ColorSpace, WeightedColor};
|
||||
pub use self::color::{Color, ColorSpace, Hsl, Hsv, WeightedColor};
|
||||
pub use self::corners::{Corner, Corners};
|
||||
pub use self::dir::Dir;
|
||||
pub use self::ellipse::ellipse;
|
||||
pub use self::em::Em;
|
||||
pub use self::fr::Fr;
|
||||
pub use self::gradient::{Gradient, LinearGradient, RatioOrAngle, Relative};
|
||||
pub use self::gradient::{
|
||||
ConicGradient, Gradient, LinearGradient, RatioOrAngle, Relative,
|
||||
};
|
||||
pub use self::length::Length;
|
||||
pub use self::paint::Paint;
|
||||
pub use self::path::{Path, PathItem};
|
||||
|
@ -15,6 +15,7 @@ use crate::eval::{dict, Cast, FromValue, NoneValue};
|
||||
/// line(stroke: 2pt + red),
|
||||
/// line(stroke: (paint: blue, thickness: 4pt, cap: "round")),
|
||||
/// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")),
|
||||
/// line(stroke: 2pt + gradient.linear(..color.map.rainbow)),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
|
BIN
tests/ref/visualize/gradient-conic.png
Normal file
BIN
tests/ref/visualize/gradient-conic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
tests/ref/visualize/gradient-relative-conic.png
Normal file
BIN
tests/ref/visualize/gradient-relative-conic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 15 KiB |
25
tests/typ/visualize/gradient-conic.typ
Normal file
25
tests/typ/visualize/gradient-conic.typ
Normal file
@ -0,0 +1,25 @@
|
||||
// Test conic gradients
|
||||
|
||||
---
|
||||
#square(
|
||||
size: 50pt,
|
||||
fill: gradient.conic(..color.map.rainbow, space: color.hsv),
|
||||
)
|
||||
|
||||
---
|
||||
#square(
|
||||
size: 50pt,
|
||||
fill: gradient.conic(..color.map.rainbow, space: color.hsv, center: (10%, 10%)),
|
||||
)
|
||||
|
||||
---
|
||||
#square(
|
||||
size: 50pt,
|
||||
fill: gradient.conic(..color.map.rainbow, space: color.hsv, center: (90%, 90%)),
|
||||
)
|
||||
|
||||
---
|
||||
#square(
|
||||
size: 50pt,
|
||||
fill: gradient.conic(..color.map.rainbow, space: color.hsv, angle: 90deg),
|
||||
)
|
@ -46,4 +46,4 @@
|
||||
#circle(
|
||||
radius: 25pt,
|
||||
fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (75%, 35%), focal-radius: 5%),
|
||||
)
|
||||
)
|
||||
|
29
tests/typ/visualize/gradient-relative-conic.typ
Normal file
29
tests/typ/visualize/gradient-relative-conic.typ
Normal file
@ -0,0 +1,29 @@
|
||||
// Test whether `relative: "parent"` works correctly on conic gradients.
|
||||
|
||||
---
|
||||
// The image should look as if there is a single gradient that is being used for
|
||||
// both the page and the rectangles.
|
||||
#let grad = gradient.conic(red, blue, green, purple, relative: "parent");
|
||||
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
|
||||
#set page(
|
||||
height: 200pt,
|
||||
width: 200pt,
|
||||
fill: grad,
|
||||
background: place(top + left, my-rect),
|
||||
)
|
||||
#place(top + right, my-rect)
|
||||
#place(bottom + center, rotate(45deg, my-rect))
|
||||
|
||||
---
|
||||
// The image should look as if there are multiple gradients, one for each
|
||||
// rectangle.
|
||||
#let grad = gradient.conic(red, blue, green, purple, relative: "self");
|
||||
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
|
||||
#set page(
|
||||
height: 200pt,
|
||||
width: 200pt,
|
||||
fill: grad,
|
||||
background: place(top + left, my-rect),
|
||||
)
|
||||
#place(top + right, my-rect)
|
||||
#place(bottom + center, rotate(45deg, my-rect))
|
@ -9,6 +9,10 @@
|
||||
size: 100pt,
|
||||
fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10),
|
||||
)
|
||||
#square(
|
||||
size: 100pt,
|
||||
fill: gradient.conic(..color.map.rainbow, space: color.hsl).sharp(10),
|
||||
)
|
||||
|
||||
---
|
||||
#square(
|
||||
@ -19,3 +23,7 @@
|
||||
size: 100pt,
|
||||
fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
|
||||
)
|
||||
#square(
|
||||
size: 100pt,
|
||||
fill: gradient.conic(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
|
||||
)
|
||||
|
@ -1,8 +1,9 @@
|
||||
// Test gradients on strokes.
|
||||
|
||||
---
|
||||
#set page(width: 100pt, height: auto, margin: 10pt)
|
||||
#align(center + top, square(size: 50pt, fill: black, stroke: 5pt + gradient.linear(red, blue)))
|
||||
|
||||
---
|
||||
#align(
|
||||
center + bottom,
|
||||
square(
|
||||
@ -12,6 +13,16 @@
|
||||
)
|
||||
)
|
||||
|
||||
---
|
||||
#align(
|
||||
center + bottom,
|
||||
square(
|
||||
size: 50pt,
|
||||
fill: black,
|
||||
stroke: 10pt + gradient.conic(red, blue)
|
||||
)
|
||||
)
|
||||
|
||||
---
|
||||
// Test gradient on lines
|
||||
#set page(width: 100pt, height: 100pt)
|
||||
|
Loading…
x
Reference in New Issue
Block a user