mirror of
https://github.com/typst/typst
synced 2025-05-18 11:05:28 +08:00
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 ecow::{eco_format, EcoString};
|
||||||
use pdf_writer::types::FunctionShadingType;
|
use pdf_writer::types::FunctionShadingType;
|
||||||
|
use pdf_writer::writers::StreamShadingType;
|
||||||
use pdf_writer::{types::ColorSpaceOperand, Name};
|
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::page::{PageContext, Transforms};
|
||||||
use super::{AbsExt, PdfContext};
|
use super::{AbsExt, PdfContext};
|
||||||
|
use crate::export::pdf::deflate;
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
Abs, Angle, Color, ColorSpace, Gradient, Numeric, Quadrant, Ratio, Relative,
|
Abs, Angle, Color, ColorSpace, ConicGradient, Gradient, Numeric, Point, Quadrant,
|
||||||
Transform,
|
Ratio, Relative, Transform, WeightedColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A unique-transform-aspect-ratio combination that will be encoded into the
|
/// 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
|
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));
|
shading_pattern.matrix(transform_to_array(transform));
|
||||||
@ -258,29 +295,47 @@ fn register_gradient(
|
|||||||
Relative::Parent => transforms.container_size,
|
Relative::Parent => transforms.container_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (offset_x, offset_y) =
|
let (offset_x, offset_y) = match gradient {
|
||||||
match gradient.angle().unwrap_or_else(Angle::zero).quadrant() {
|
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::First => (Abs::zero(), Abs::zero()),
|
||||||
Quadrant::Second => (size.x, Abs::zero()),
|
Quadrant::Second => (size.x, Abs::zero()),
|
||||||
Quadrant::Third => (size.x, size.y),
|
Quadrant::Third => (size.x, size.y),
|
||||||
Quadrant::Fourth => (Abs::zero(), 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) {
|
let transform = match gradient.unwrap_relative(false) {
|
||||||
Relative::Self_ => transforms.transform,
|
Relative::Self_ => transforms.transform,
|
||||||
Relative::Parent => transforms.container_transform,
|
Relative::Parent => transforms.container_transform,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let scale_offset = match gradient {
|
||||||
|
Gradient::Conic(_) => 4.0_f64,
|
||||||
|
_ => 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
let pdf_gradient = PdfGradient {
|
let pdf_gradient = PdfGradient {
|
||||||
aspect_ratio: size.aspect_ratio(),
|
aspect_ratio: size.aspect_ratio(),
|
||||||
transform: transform
|
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(
|
.pre_concat(Transform::scale(
|
||||||
Ratio::new(size.x.to_pt()),
|
Ratio::new(size.x.to_pt() * scale_offset),
|
||||||
Ratio::new(size.y.to_pt()),
|
Ratio::new(size.y.to_pt() * scale_offset),
|
||||||
))
|
))
|
||||||
.pre_concat(Transform::rotate(Gradient::correct_aspect_ratio(
|
.pre_concat(Transform::rotate(Gradient::correct_aspect_ratio(
|
||||||
gradient.angle().unwrap_or_else(Angle::zero),
|
rotation,
|
||||||
size.aspect_ratio(),
|
size.aspect_ratio(),
|
||||||
))),
|
))),
|
||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
@ -301,3 +356,189 @@ fn transform_to_array(ts: Transform) -> [f32; 6] {
|
|||||||
ts.ty.to_f32(),
|
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.
|
/// Applying the necessary transform, if the paint is a gradient.
|
||||||
///
|
///
|
||||||
/// `gradient_map` is used to scale and move the gradient being sampled,
|
/// `gradient_map` is used to scale and move the gradient being sampled,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::f32::consts::TAU;
|
||||||
use std::fmt::{self, Display, Formatter, Write};
|
use std::fmt::{self, Display, Formatter, Write};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
@ -17,6 +18,11 @@ use crate::geom::{
|
|||||||
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
|
||||||
use crate::util::hash128;
|
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.
|
/// Export a frame into a SVG file.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn svg(frame: &Frame) -> String {
|
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 `Ratio` is the aspect ratio of the gradient, this is used to correct
|
||||||
/// the angle of the gradient.
|
/// the angle of the gradient.
|
||||||
gradients: Deduplicator<(Gradient, Ratio)>,
|
gradients: Deduplicator<(Gradient, Ratio)>,
|
||||||
|
/// These are the gradients that compose a conic gradient.
|
||||||
|
conic_subgradients: Deduplicator<SVGSubGradient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contextual information for rendering.
|
/// Contextual information for rendering.
|
||||||
@ -131,6 +139,21 @@ struct GradientRef {
|
|||||||
transform: Transform,
|
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.
|
/// The kind of linear gradient.
|
||||||
#[derive(Hash, Clone, Copy, PartialEq, Eq)]
|
#[derive(Hash, Clone, Copy, PartialEq, Eq)]
|
||||||
enum GradientKind {
|
enum GradientKind {
|
||||||
@ -138,6 +161,8 @@ enum GradientKind {
|
|||||||
Linear,
|
Linear,
|
||||||
/// A radial gradient.
|
/// A radial gradient.
|
||||||
Radial,
|
Radial,
|
||||||
|
/// A conic gradient.
|
||||||
|
Conic,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Gradient> for GradientKind {
|
impl From<&Gradient> for GradientKind {
|
||||||
@ -145,6 +170,7 @@ impl From<&Gradient> for GradientKind {
|
|||||||
match value {
|
match value {
|
||||||
Gradient::Linear { .. } => GradientKind::Linear,
|
Gradient::Linear { .. } => GradientKind::Linear,
|
||||||
Gradient::Radial { .. } => GradientKind::Radial,
|
Gradient::Radial { .. } => GradientKind::Radial,
|
||||||
|
Gradient::Conic { .. } => GradientKind::Conic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,6 +196,7 @@ impl SVGRenderer {
|
|||||||
clip_paths: Deduplicator::new('c'),
|
clip_paths: Deduplicator::new('c'),
|
||||||
gradient_refs: Deduplicator::new('g'),
|
gradient_refs: Deduplicator::new('g'),
|
||||||
gradients: Deduplicator::new('f'),
|
gradients: Deduplicator::new('f'),
|
||||||
|
conic_subgradients: Deduplicator::new('s'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,6 +599,7 @@ impl SVGRenderer {
|
|||||||
self.write_clip_path_defs();
|
self.write_clip_path_defs();
|
||||||
self.write_gradients();
|
self.write_gradients();
|
||||||
self.write_gradient_refs();
|
self.write_gradient_refs();
|
||||||
|
self.write_subgradients();
|
||||||
self.xml.end_document()
|
self.xml.end_document()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -681,6 +709,87 @@ impl SVGRenderer {
|
|||||||
self.xml.write_attribute("fy", &radial.focal_center.y.get());
|
self.xml.write_attribute("fy", &radial.focal_center.y.get());
|
||||||
self.xml.write_attribute("fr", &radial.focal_radius.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) {
|
for window in gradient.stops_ref().windows(2) {
|
||||||
@ -726,6 +835,43 @@ impl SVGRenderer {
|
|||||||
self.xml.end_element()
|
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) {
|
fn write_gradient_refs(&mut self) {
|
||||||
if self.gradient_refs.is_empty() {
|
if self.gradient_refs.is_empty() {
|
||||||
return;
|
return;
|
||||||
@ -749,6 +895,13 @@ impl SVGRenderer {
|
|||||||
&SvgMatrix(gradient_ref.transform),
|
&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);
|
self.xml.write_attribute("id", &id);
|
||||||
@ -996,6 +1149,26 @@ impl SvgPathBuilder {
|
|||||||
self.line_to(width, 0.0);
|
self.line_to(width, 0.0);
|
||||||
self.close();
|
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.
|
/// 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.
|
/// A color gradient.
|
||||||
///
|
///
|
||||||
/// Typst supports linear gradients through the
|
/// Typst supports linear gradients through the
|
||||||
/// [`gradient.linear` function]($gradient.linear) and radial gradients through
|
/// [`gradient.linear` function]($gradient.linear), radial gradients through
|
||||||
/// the [`gradient.radial` function]($gradient.radial). Conic gradients will be
|
/// the [`gradient.radial` function]($gradient.radial), and conic gradients
|
||||||
/// available soon.
|
/// through the [`gradient.conic` function]($gradient.conic).
|
||||||
///
|
///
|
||||||
/// See the [tracking issue](https://github.com/typst/typst/issues/2282) for
|
/// See the [tracking issue](https://github.com/typst/typst/issues/2282) for
|
||||||
/// more details on the progress of gradient implementation.
|
/// more details on the progress of gradient implementation.
|
||||||
@ -27,6 +27,7 @@ use crate::syntax::{Span, Spanned};
|
|||||||
/// dir: ltr,
|
/// dir: ltr,
|
||||||
/// square(size: 50pt, fill: gradient.linear(..color.map.rainbow)),
|
/// square(size: 50pt, fill: gradient.linear(..color.map.rainbow)),
|
||||||
/// square(size: 50pt, fill: gradient.radial(..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 {
|
pub enum Gradient {
|
||||||
Linear(Arc<LinearGradient>),
|
Linear(Arc<LinearGradient>),
|
||||||
Radial(Arc<RadialGradient>),
|
Radial(Arc<RadialGradient>),
|
||||||
|
Conic(Arc<ConicGradient>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[scope]
|
#[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.
|
/// Returns the stops of this gradient.
|
||||||
#[func]
|
#[func]
|
||||||
pub fn stops(&self) -> Vec<Stop> {
|
pub fn stops(&self) -> Vec<Stop> {
|
||||||
@ -379,6 +450,11 @@ impl Gradient {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(color, offset)| Stop { color: *color, offset: Some(*offset) })
|
.map(|(color, offset)| Stop { color: *color, offset: Some(*offset) })
|
||||||
.collect(),
|
.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 {
|
match self {
|
||||||
Self::Linear(linear) => linear.space,
|
Self::Linear(linear) => linear.space,
|
||||||
Self::Radial(radial) => radial.space,
|
Self::Radial(radial) => radial.space,
|
||||||
|
Self::Conic(conic) => conic.space,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,6 +474,7 @@ impl Gradient {
|
|||||||
match self {
|
match self {
|
||||||
Self::Linear(linear) => linear.relative,
|
Self::Linear(linear) => linear.relative,
|
||||||
Self::Radial(radial) => radial.relative,
|
Self::Radial(radial) => radial.relative,
|
||||||
|
Self::Conic(conic) => conic.relative,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,6 +484,7 @@ impl Gradient {
|
|||||||
match self {
|
match self {
|
||||||
Self::Linear(linear) => Some(linear.angle),
|
Self::Linear(linear) => Some(linear.angle),
|
||||||
Self::Radial(_) => None,
|
Self::Radial(_) => None,
|
||||||
|
Self::Conic(conic) => Some(conic.angle),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,6 +494,7 @@ impl Gradient {
|
|||||||
match self {
|
match self {
|
||||||
Self::Linear(_) => Self::linear_data().into(),
|
Self::Linear(_) => Self::linear_data().into(),
|
||||||
Self::Radial(_) => Self::radial_data().into(),
|
Self::Radial(_) => Self::radial_data().into(),
|
||||||
|
Self::Conic(_) => Self::conic_data().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,6 +516,7 @@ impl Gradient {
|
|||||||
match self {
|
match self {
|
||||||
Self::Linear(linear) => sample_stops(&linear.stops, linear.space, value),
|
Self::Linear(linear) => sample_stops(&linear.stops, linear.space, value),
|
||||||
Self::Radial(radial) => sample_stops(&radial.stops, radial.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,
|
relative: radial.relative,
|
||||||
anti_alias: false,
|
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,
|
relative: radial.relative,
|
||||||
anti_alias: radial.anti_alias,
|
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 {
|
match self {
|
||||||
Gradient::Linear(linear) => &linear.stops,
|
Gradient::Linear(linear) => &linear.stops,
|
||||||
Gradient::Radial(radial) => &radial.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 (mut x, mut y) = (x / width, y / height);
|
||||||
let t = match self {
|
let t = match self {
|
||||||
Self::Linear(linear) => {
|
Self::Linear(linear) => {
|
||||||
// Handle the direction of the gradient.
|
|
||||||
let angle = linear.angle.to_rad().rem_euclid(TAU);
|
|
||||||
|
|
||||||
// Aspect ratio correction.
|
// Aspect ratio correction.
|
||||||
let angle = (angle.tan() * height as f64).atan2(width as f64);
|
let angle = Gradient::correct_aspect_ratio(
|
||||||
let angle = match linear.angle.quadrant() {
|
linear.angle,
|
||||||
Quadrant::First => angle,
|
Ratio::new((width / height) as f64),
|
||||||
Quadrant::Second => angle + PI,
|
)
|
||||||
Quadrant::Third => angle + PI,
|
.to_rad();
|
||||||
Quadrant::Fourth => angle + TAU,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (sin, cos) = angle.sin_cos();
|
let (sin, cos) = angle.sin_cos();
|
||||||
|
|
||||||
let length = sin.abs() + cos.abs();
|
let length = sin.abs() + cos.abs();
|
||||||
@ -672,6 +764,15 @@ impl Gradient {
|
|||||||
((z - q).hypot() - fr) / (bz - fr)
|
((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))))
|
self.sample(RatioOrAngle::Ratio(Ratio::new(t.clamp(0.0, 1.0))))
|
||||||
@ -682,6 +783,7 @@ impl Gradient {
|
|||||||
match self {
|
match self {
|
||||||
Self::Linear(linear) => linear.anti_alias,
|
Self::Linear(linear) => linear.anti_alias,
|
||||||
Self::Radial(radial) => radial.anti_alias,
|
Self::Radial(radial) => radial.anti_alias,
|
||||||
|
Self::Conic(conic) => conic.anti_alias,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -717,6 +819,7 @@ impl Repr for Gradient {
|
|||||||
match self {
|
match self {
|
||||||
Self::Radial(radial) => radial.repr(),
|
Self::Radial(radial) => radial.repr(),
|
||||||
Self::Linear(linear) => linear.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(");
|
let mut r = EcoString::from("gradient.radial(");
|
||||||
|
|
||||||
if self.center.x != Ratio::new(0.5) || self.center.y != Ratio::new(0.5) {
|
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(&self.center.x.repr());
|
||||||
r.push_str(", ");
|
r.push_str(", ");
|
||||||
r.push_str(&self.center.y.repr());
|
r.push_str(&self.center.y.repr());
|
||||||
@ -848,6 +951,71 @@ impl Repr for RadialGradient {
|
|||||||
r.push_str(", ");
|
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() {
|
for (i, (color, offset)) in self.stops.iter().enumerate() {
|
||||||
r.push('(');
|
r.push('(');
|
||||||
r.push_str(&color.repr());
|
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::align::{Align, FixedAlign, HAlign, VAlign};
|
||||||
pub use self::angle::{Angle, AngleUnit, Quadrant};
|
pub use self::angle::{Angle, AngleUnit, Quadrant};
|
||||||
pub use self::axes::{Axes, Axis};
|
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::corners::{Corner, Corners};
|
||||||
pub use self::dir::Dir;
|
pub use self::dir::Dir;
|
||||||
pub use self::ellipse::ellipse;
|
pub use self::ellipse::ellipse;
|
||||||
pub use self::em::Em;
|
pub use self::em::Em;
|
||||||
pub use self::fr::Fr;
|
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::length::Length;
|
||||||
pub use self::paint::Paint;
|
pub use self::paint::Paint;
|
||||||
pub use self::path::{Path, PathItem};
|
pub use self::path::{Path, PathItem};
|
||||||
|
@ -15,6 +15,7 @@ use crate::eval::{dict, Cast, FromValue, NoneValue};
|
|||||||
/// line(stroke: 2pt + red),
|
/// line(stroke: 2pt + red),
|
||||||
/// line(stroke: (paint: blue, thickness: 4pt, cap: "round")),
|
/// line(stroke: (paint: blue, thickness: 4pt, cap: "round")),
|
||||||
/// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")),
|
/// 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(
|
#circle(
|
||||||
radius: 25pt,
|
radius: 25pt,
|
||||||
fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (75%, 35%), focal-radius: 5%),
|
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,
|
size: 100pt,
|
||||||
fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10),
|
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(
|
#square(
|
||||||
@ -19,3 +23,7 @@
|
|||||||
size: 100pt,
|
size: 100pt,
|
||||||
fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
|
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.
|
// 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 + top, square(size: 50pt, fill: black, stroke: 5pt + gradient.linear(red, blue)))
|
||||||
|
|
||||||
|
---
|
||||||
#align(
|
#align(
|
||||||
center + bottom,
|
center + bottom,
|
||||||
square(
|
square(
|
||||||
@ -12,6 +13,16 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
#align(
|
||||||
|
center + bottom,
|
||||||
|
square(
|
||||||
|
size: 50pt,
|
||||||
|
fill: black,
|
||||||
|
stroke: 10pt + gradient.conic(red, blue)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test gradient on lines
|
// Test gradient on lines
|
||||||
#set page(width: 100pt, height: 100pt)
|
#set page(width: 100pt, height: 100pt)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user