Gradient Part 4 - Conic gradients (#2325)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-10-10 11:29:05 +02:00 committed by GitHub
parent 877ee39a8c
commit cef2d3afca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 694 additions and 31 deletions

View File

@ -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))
}

View File

@ -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,

View File

@ -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
}

View File

@ -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());

View File

@ -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};

View File

@ -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)),
/// )
/// ```
///

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

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

View 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),
)

View File

@ -46,4 +46,4 @@
#circle(
radius: 25pt,
fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (75%, 35%), focal-radius: 5%),
)
)

View 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))

View File

@ -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%),
)

View File

@ -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)