summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorSébastien d'Herbais de Thun <sebastien.d.herbais@gmail.com>2023-10-10 11:29:05 +0200
committerGitHub <noreply@github.com>2023-10-10 11:29:05 +0200
commitcef2d3afcae87230dbe361ef48ecb5dad50ad0a5 (patch)
treee7016794152f799d08f792a67f6547c807a8dcd0 /crates
parent877ee39a8c7634b85e2652e8a335be4020468dfd (diff)
Gradient Part 4 - Conic gradients (#2325)
Diffstat (limited to 'crates')
-rw-r--r--crates/typst/src/export/pdf/gradient.rs263
-rw-r--r--crates/typst/src/export/render.rs2
-rw-r--r--crates/typst/src/export/svg.rs178
-rw-r--r--crates/typst/src/geom/gradient.rs198
-rw-r--r--crates/typst/src/geom/mod.rs6
-rw-r--r--crates/typst/src/geom/stroke.rs1
6 files changed, 619 insertions, 29 deletions
diff --git a/crates/typst/src/export/pdf/gradient.rs b/crates/typst/src/export/pdf/gradient.rs
index 18b4de36..1123b53e 100644
--- a/crates/typst/src/export/pdf/gradient.rs
+++ b/crates/typst/src/export/pdf/gradient.rs
@@ -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))
+}
diff --git a/crates/typst/src/export/render.rs b/crates/typst/src/export/render.rs
index 0b26045e..6fd47387 100644
--- a/crates/typst/src/export/render.rs
+++ b/crates/typst/src/export/render.rs
@@ -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,
diff --git a/crates/typst/src/export/svg.rs b/crates/typst/src/export/svg.rs
index 1aa5141a..25cb7519 100644
--- a/crates/typst/src/export/svg.rs
+++ b/crates/typst/src/export/svg.rs
@@ -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
+}
diff --git a/crates/typst/src/geom/gradient.rs b/crates/typst/src/geom/gradient.rs
index 33b8ca77..f4c750a3 100644
--- a/crates/typst/src/geom/gradient.rs
+++ b/crates/typst/src/geom/gradient.rs
@@ -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());
@@ -852,6 +955,71 @@ impl Repr for RadialGradient {
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());
+ r.push_str(", ");
r.push_str(&Angle::deg(offset.get() * 360.0).repr());
r.push(')');
if i != self.stops.len() - 1 {
diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs
index 29ecbe74..105ee5a3 100644
--- a/crates/typst/src/geom/mod.rs
+++ b/crates/typst/src/geom/mod.rs
@@ -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};
diff --git a/crates/typst/src/geom/stroke.rs b/crates/typst/src/geom/stroke.rs
index 2b1bb876..e88d887d 100644
--- a/crates/typst/src/geom/stroke.rs
+++ b/crates/typst/src/geom/stroke.rs
@@ -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)),
/// )
/// ```
///