Gradient Part 3 - Radial gradients (#2312)
@ -7,7 +7,8 @@ use super::color::{ColorSpaceExt, PaintEncode};
|
|||||||
use super::page::{PageContext, Transforms};
|
use super::page::{PageContext, Transforms};
|
||||||
use super::{AbsExt, PdfContext};
|
use super::{AbsExt, PdfContext};
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
Abs, Color, ColorSpace, Gradient, Numeric, Quadrant, Ratio, Relative, Transform,
|
Abs, Angle, Color, ColorSpace, Gradient, Numeric, Quadrant, Ratio, Relative,
|
||||||
|
Transform,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A unique-transform-aspect-ratio combination that will be encoded into the
|
/// A unique-transform-aspect-ratio combination that will be encoded into the
|
||||||
@ -54,6 +55,32 @@ pub fn write_gradients(ctx: &mut PdfContext) {
|
|||||||
|
|
||||||
shading.finish();
|
shading.finish();
|
||||||
|
|
||||||
|
shading_pattern
|
||||||
|
}
|
||||||
|
Gradient::Radial(radial) => {
|
||||||
|
let shading_function = shading_function(ctx, &gradient);
|
||||||
|
let mut shading_pattern = ctx.pdf.shading_pattern(shading);
|
||||||
|
let mut shading = shading_pattern.function_shading();
|
||||||
|
shading.shading_type(FunctionShadingType::Radial);
|
||||||
|
|
||||||
|
ctx.colors
|
||||||
|
.write(gradient.space(), shading.color_space(), &mut ctx.alloc);
|
||||||
|
|
||||||
|
shading
|
||||||
|
.anti_alias(gradient.anti_alias())
|
||||||
|
.function(shading_function)
|
||||||
|
.coords([
|
||||||
|
radial.focal_center.x.get() as f32,
|
||||||
|
radial.focal_center.y.get() as f32,
|
||||||
|
radial.focal_radius.get() as f32,
|
||||||
|
radial.center.x.get() as f32,
|
||||||
|
radial.center.y.get() as f32,
|
||||||
|
radial.radius.get() as f32,
|
||||||
|
])
|
||||||
|
.extend([true; 2]);
|
||||||
|
|
||||||
|
shading.finish();
|
||||||
|
|
||||||
shading_pattern
|
shading_pattern
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -231,7 +258,8 @@ fn register_gradient(
|
|||||||
Relative::Parent => transforms.container_size,
|
Relative::Parent => transforms.container_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (offset_x, offset_y) = match gradient.angle().quadrant() {
|
let (offset_x, offset_y) =
|
||||||
|
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),
|
||||||
@ -252,7 +280,7 @@ fn register_gradient(
|
|||||||
Ratio::new(size.y.to_pt()),
|
Ratio::new(size.y.to_pt()),
|
||||||
))
|
))
|
||||||
.pre_concat(Transform::rotate(Gradient::correct_aspect_ratio(
|
.pre_concat(Transform::rotate(Gradient::correct_aspect_ratio(
|
||||||
gradient.angle(),
|
gradient.angle().unwrap_or_else(Angle::zero),
|
||||||
size.aspect_ratio(),
|
size.aspect_ratio(),
|
||||||
))),
|
))),
|
||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
|
@ -14,7 +14,7 @@ use usvg::{NodeExt, TreeParsing};
|
|||||||
use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, Meta, TextItem};
|
use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, Meta, TextItem};
|
||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
self, Abs, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
|
self, Abs, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
|
||||||
PathItem, Point, Ratio, Relative, Shape, Size, Transform,
|
PathItem, Point, Ratio, Relative, Shape, Size, Transform,
|
||||||
};
|
};
|
||||||
use crate::image::{Image, ImageKind, RasterFormat};
|
use crate::image::{Image, ImageKind, RasterFormat};
|
||||||
@ -136,8 +136,11 @@ impl<'a> State<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Pre concat the container's transform.
|
/// Pre concat the container's transform.
|
||||||
fn pre_concat_container(self, container_transform: sk::Transform) -> Self {
|
fn pre_concat_container(self, transform: sk::Transform) -> Self {
|
||||||
Self { container_transform, ..self }
|
Self {
|
||||||
|
container_transform: self.container_transform.pre_concat(transform),
|
||||||
|
..self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +381,7 @@ fn render_outline_glyph(
|
|||||||
|
|
||||||
// TODO: Implement gradients on text.
|
// TODO: Implement gradients on text.
|
||||||
let mut pixmap = None;
|
let mut pixmap = None;
|
||||||
let paint = to_sk_paint(&text.fill, state, Size::zero(), None, &mut pixmap);
|
let paint = to_sk_paint(&text.fill, state, Size::zero(), None, &mut pixmap, None);
|
||||||
|
|
||||||
let rule = sk::FillRule::default();
|
let rule = sk::FillRule::default();
|
||||||
|
|
||||||
@ -512,7 +515,7 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
|
|||||||
if let Some(fill) = &shape.fill {
|
if let Some(fill) = &shape.fill {
|
||||||
let mut pixmap = None;
|
let mut pixmap = None;
|
||||||
let mut paint: sk::Paint =
|
let mut paint: sk::Paint =
|
||||||
to_sk_paint(fill, state, shape.geometry.bbox_size(), None, &mut pixmap);
|
to_sk_paint(fill, state, shape.geometry.bbox_size(), None, &mut pixmap, None);
|
||||||
|
|
||||||
if matches!(shape.geometry, Geometry::Rect(_)) {
|
if matches!(shape.geometry, Geometry::Rect(_)) {
|
||||||
paint.anti_alias = false;
|
paint.anti_alias = false;
|
||||||
@ -547,10 +550,42 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
|
|||||||
sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
|
sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut pixmap = None;
|
let bbox = shape.geometry.bbox_size();
|
||||||
let paint =
|
let offset_bbox = (!matches!(shape.geometry, Geometry::Line(..)))
|
||||||
to_sk_paint(paint, state, shape.geometry.bbox_size(), None, &mut pixmap);
|
.then(|| offset_bounding_box(bbox, *thickness))
|
||||||
|
.unwrap_or(bbox);
|
||||||
|
|
||||||
|
let fill_transform =
|
||||||
|
(!matches!(shape.geometry, Geometry::Line(..))).then(|| {
|
||||||
|
sk::Transform::from_translate(
|
||||||
|
-thickness.to_f32(),
|
||||||
|
-thickness.to_f32(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let gradient_map =
|
||||||
|
(!matches!(shape.geometry, Geometry::Line(..))).then(|| {
|
||||||
|
(
|
||||||
|
Point::new(
|
||||||
|
-*thickness * state.pixel_per_pt as f64,
|
||||||
|
-*thickness * state.pixel_per_pt as f64,
|
||||||
|
),
|
||||||
|
Axes::new(
|
||||||
|
Ratio::new(offset_bbox.x / bbox.x),
|
||||||
|
Ratio::new(offset_bbox.y / bbox.y),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut pixmap = None;
|
||||||
|
let paint = to_sk_paint(
|
||||||
|
paint,
|
||||||
|
state,
|
||||||
|
offset_bbox,
|
||||||
|
fill_transform,
|
||||||
|
&mut pixmap,
|
||||||
|
gradient_map,
|
||||||
|
);
|
||||||
let stroke = sk::Stroke {
|
let stroke = sk::Stroke {
|
||||||
width,
|
width,
|
||||||
line_cap: line_cap.into(),
|
line_cap: line_cap.into(),
|
||||||
@ -700,23 +735,40 @@ 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,
|
||||||
|
/// this is used to line up the stroke and the fill of a shape.
|
||||||
fn to_sk_paint<'a>(
|
fn to_sk_paint<'a>(
|
||||||
paint: &Paint,
|
paint: &Paint,
|
||||||
state: State,
|
state: State,
|
||||||
item_size: Size,
|
item_size: Size,
|
||||||
fill_transform: Option<sk::Transform>,
|
fill_transform: Option<sk::Transform>,
|
||||||
pixmap: &'a mut Option<Arc<sk::Pixmap>>,
|
pixmap: &'a mut Option<Arc<sk::Pixmap>>,
|
||||||
|
gradient_map: Option<(Point, Axes<Ratio>)>,
|
||||||
) -> sk::Paint<'a> {
|
) -> sk::Paint<'a> {
|
||||||
/// Actual sampling of the gradient, cached for performance.
|
/// Actual sampling of the gradient, cached for performance.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn cached(gradient: &Gradient, width: u32, height: u32) -> Arc<sk::Pixmap> {
|
fn cached(
|
||||||
|
gradient: &Gradient,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
gradient_map: Option<(Point, Axes<Ratio>)>,
|
||||||
|
) -> Arc<sk::Pixmap> {
|
||||||
|
let (offset, scale) =
|
||||||
|
gradient_map.unwrap_or_else(|| (Point::zero(), Axes::splat(Ratio::one())));
|
||||||
let mut pixmap = sk::Pixmap::new(width.max(1), height.max(1)).unwrap();
|
let mut pixmap = sk::Pixmap::new(width.max(1), height.max(1)).unwrap();
|
||||||
for x in 0..width {
|
for x in 0..width {
|
||||||
for y in 0..height {
|
for y in 0..height {
|
||||||
let color: sk::Color = gradient
|
let color: sk::Color = gradient
|
||||||
.sample_at((x as f32, y as f32), (width as f32, height as f32))
|
.sample_at(
|
||||||
|
(
|
||||||
|
(x as f32 + offset.x.to_f32()) * scale.x.get() as f32,
|
||||||
|
(y as f32 + offset.y.to_f32()) * scale.y.get() as f32,
|
||||||
|
),
|
||||||
|
(width as f32, height as f32),
|
||||||
|
)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
pixmap.pixels_mut()[(y * width + x) as usize] =
|
pixmap.pixels_mut()[(y * width + x) as usize] =
|
||||||
@ -734,18 +786,18 @@ fn to_sk_paint<'a>(
|
|||||||
sk_paint.anti_alias = true;
|
sk_paint.anti_alias = true;
|
||||||
}
|
}
|
||||||
Paint::Gradient(gradient) => {
|
Paint::Gradient(gradient) => {
|
||||||
let container_size = match gradient.unwrap_relative(false) {
|
let relative = gradient.unwrap_relative(false);
|
||||||
|
let container_size = match relative {
|
||||||
Relative::Self_ => item_size,
|
Relative::Self_ => item_size,
|
||||||
Relative::Parent => state.size,
|
Relative::Parent => state.size,
|
||||||
};
|
};
|
||||||
|
|
||||||
let fill_transform =
|
let fill_transform = match relative {
|
||||||
fill_transform.unwrap_or_else(|| match gradient.unwrap_relative(false) {
|
Relative::Self_ => fill_transform.unwrap_or_default(),
|
||||||
Relative::Self_ => sk::Transform::identity(),
|
|
||||||
Relative::Parent => state
|
Relative::Parent => state
|
||||||
.container_transform
|
.container_transform
|
||||||
.post_concat(state.transform.invert().unwrap()),
|
.post_concat(state.transform.invert().unwrap()),
|
||||||
});
|
};
|
||||||
let width = (container_size.x.to_f32() * state.pixel_per_pt).ceil() as u32;
|
let width = (container_size.x.to_f32() * state.pixel_per_pt).ceil() as u32;
|
||||||
let height = (container_size.y.to_f32() * state.pixel_per_pt).ceil() as u32;
|
let height = (container_size.y.to_f32() * state.pixel_per_pt).ceil() as u32;
|
||||||
|
|
||||||
@ -753,6 +805,7 @@ fn to_sk_paint<'a>(
|
|||||||
gradient,
|
gradient,
|
||||||
width.max(state.pixel_per_pt.ceil() as u32),
|
width.max(state.pixel_per_pt.ceil() as u32),
|
||||||
height.max(state.pixel_per_pt.ceil() as u32),
|
height.max(state.pixel_per_pt.ceil() as u32),
|
||||||
|
gradient_map,
|
||||||
));
|
));
|
||||||
|
|
||||||
// We can use FilterQuality::Nearest here because we're
|
// We can use FilterQuality::Nearest here because we're
|
||||||
@ -860,3 +913,7 @@ fn alpha_mul(color: u32, scale: u32) -> u32 {
|
|||||||
let ag = ((color >> 8) & mask) * scale;
|
let ag = ((color >> 8) & mask) * scale;
|
||||||
(rb & mask) | (ag & !mask)
|
(rb & mask) | (ag & !mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn offset_bounding_box(bbox: Size, stroke_width: Abs) -> Size {
|
||||||
|
Size::new(bbox.x + stroke_width * 2.0, bbox.y + stroke_width * 2.0)
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ use ttf_parser::{GlyphId, OutlineBuilder};
|
|||||||
use xmlwriter::XmlWriter;
|
use xmlwriter::XmlWriter;
|
||||||
|
|
||||||
use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
|
use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
|
||||||
|
use crate::eval::Repr;
|
||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
|
Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
|
||||||
@ -135,12 +136,15 @@ struct GradientRef {
|
|||||||
enum GradientKind {
|
enum GradientKind {
|
||||||
/// A linear gradient.
|
/// A linear gradient.
|
||||||
Linear,
|
Linear,
|
||||||
|
/// A radial gradient.
|
||||||
|
Radial,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Gradient> for GradientKind {
|
impl From<&Gradient> for GradientKind {
|
||||||
fn from(value: &Gradient) -> Self {
|
fn from(value: &Gradient) -> Self {
|
||||||
match value {
|
match value {
|
||||||
Gradient::Linear { .. } => GradientKind::Linear,
|
Gradient::Linear { .. } => GradientKind::Linear,
|
||||||
|
Gradient::Radial { .. } => GradientKind::Radial,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -664,14 +668,27 @@ impl SVGRenderer {
|
|||||||
self.xml.write_attribute("y1", &y1);
|
self.xml.write_attribute("y1", &y1);
|
||||||
self.xml.write_attribute("x2", &x2);
|
self.xml.write_attribute("x2", &x2);
|
||||||
self.xml.write_attribute("y2", &y2);
|
self.xml.write_attribute("y2", &y2);
|
||||||
|
}
|
||||||
|
Gradient::Radial(radial) => {
|
||||||
|
self.xml.start_element("radialGradient");
|
||||||
|
self.xml.write_attribute("id", &id);
|
||||||
|
self.xml.write_attribute("spreadMethod", "pad");
|
||||||
|
self.xml.write_attribute("gradientUnits", "userSpaceOnUse");
|
||||||
|
self.xml.write_attribute("cx", &radial.center.x.get());
|
||||||
|
self.xml.write_attribute("cy", &radial.center.y.get());
|
||||||
|
self.xml.write_attribute("r", &radial.radius.get());
|
||||||
|
self.xml.write_attribute("fx", &radial.focal_center.x.get());
|
||||||
|
self.xml.write_attribute("fy", &radial.focal_center.y.get());
|
||||||
|
self.xml.write_attribute("fr", &radial.focal_radius.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for window in linear.stops.windows(2) {
|
for window in gradient.stops_ref().windows(2) {
|
||||||
let (start_c, start_t) = window[0];
|
let (start_c, start_t) = window[0];
|
||||||
let (end_c, end_t) = window[1];
|
let (end_c, end_t) = window[1];
|
||||||
|
|
||||||
self.xml.start_element("stop");
|
self.xml.start_element("stop");
|
||||||
self.xml
|
self.xml.write_attribute("offset", &start_t.repr());
|
||||||
.write_attribute_fmt("offset", format_args!("{start_t:?}"));
|
|
||||||
self.xml.write_attribute("stop-color", &start_c.to_hex());
|
self.xml.write_attribute("stop-color", &start_c.to_hex());
|
||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
|
|
||||||
@ -681,7 +698,7 @@ impl SVGRenderer {
|
|||||||
// The goal is to have smooth gradients but not to balloon the file size
|
// The goal is to have smooth gradients but not to balloon the file size
|
||||||
// too much if there are already a lot of stops as in most presets.
|
// too much if there are already a lot of stops as in most presets.
|
||||||
let len = if gradient.anti_alias() {
|
let len = if gradient.anti_alias() {
|
||||||
(256 / linear.stops.len() as u32).max(2)
|
(256 / gradient.stops_ref().len() as u32).max(2)
|
||||||
} else {
|
} else {
|
||||||
2
|
2
|
||||||
};
|
};
|
||||||
@ -692,21 +709,19 @@ impl SVGRenderer {
|
|||||||
let c = gradient.sample(RatioOrAngle::Ratio(t));
|
let c = gradient.sample(RatioOrAngle::Ratio(t));
|
||||||
|
|
||||||
self.xml.start_element("stop");
|
self.xml.start_element("stop");
|
||||||
self.xml.write_attribute_fmt("offset", format_args!("{t:?}"));
|
self.xml.write_attribute("offset", &t.repr());
|
||||||
self.xml.write_attribute("stop-color", &c.to_hex());
|
self.xml.write_attribute("stop-color", &c.to_hex());
|
||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.xml.start_element("stop");
|
self.xml.start_element("stop");
|
||||||
self.xml.write_attribute_fmt("offset", format_args!("{end_t:?}"));
|
self.xml.write_attribute("offset", &end_t.repr());
|
||||||
self.xml.write_attribute("stop-color", &end_c.to_hex());
|
self.xml.write_attribute("stop-color", &end_c.to_hex());
|
||||||
self.xml.end_element()
|
self.xml.end_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.xml.end_element()
|
self.xml.end_element()
|
||||||
}
|
}
|
||||||
@ -727,6 +742,13 @@ impl SVGRenderer {
|
|||||||
&SvgMatrix(gradient_ref.transform),
|
&SvgMatrix(gradient_ref.transform),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
GradientKind::Radial => {
|
||||||
|
self.xml.start_element("radialGradient");
|
||||||
|
self.xml.write_attribute(
|
||||||
|
"gradientTransform",
|
||||||
|
&SvgMatrix(gradient_ref.transform),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.xml.write_attribute("id", &id);
|
self.xml.write_attribute("id", &id);
|
||||||
|
@ -290,6 +290,18 @@ cast! {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cast! {
|
||||||
|
Axes<Ratio>,
|
||||||
|
self => array![self.x, self.y].into_value(),
|
||||||
|
array: Array => {
|
||||||
|
let mut iter = array.into_iter();
|
||||||
|
match (iter.next(), iter.next(), iter.next()) {
|
||||||
|
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
|
||||||
|
_ => bail!("ratio array must contain exactly two entries"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Resolve> Resolve for Axes<T> {
|
impl<T: Resolve> Resolve for Axes<T> {
|
||||||
type Output = Axes<T::Output>;
|
type Output = Axes<T::Output>;
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ use std::f64::{EPSILON, NEG_INFINITY};
|
|||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use kurbo::Vec2;
|
||||||
|
|
||||||
use super::color::{Hsl, Hsv};
|
use super::color::{Hsl, Hsv};
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::diag::{bail, error, SourceResult};
|
use crate::diag::{bail, error, SourceResult};
|
||||||
@ -13,15 +15,25 @@ 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). Radial and conic gradients
|
/// [`gradient.linear` function]($gradient.linear) and radial gradients through
|
||||||
/// will be available soon.
|
/// the [`gradient.radial` function]($gradient.radial). Conic gradients will be
|
||||||
|
/// available soon.
|
||||||
///
|
///
|
||||||
/// 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.
|
||||||
///
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #stack(
|
||||||
|
/// dir: ltr,
|
||||||
|
/// square(size: 50pt, fill: gradient.linear(..color.map.rainbow)),
|
||||||
|
/// square(size: 50pt, fill: gradient.radial(..color.map.rainbow)),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
/// # Stops
|
/// # Stops
|
||||||
/// A gradient is composed of a series of stops. Each of these stops has a color
|
/// A gradient is composed of a series of stops. Each of these stops has a color
|
||||||
/// and an offset. The offset is a [ratio]($ratio) between `{0%}` and `{100%}`
|
/// and an offset. The offset is a [ratio]($ratio) between `{0%}` and `{100%}` or
|
||||||
|
/// an angle between `{0deg}` and `{360deg}`. The offset is a relative position
|
||||||
/// that determines how far along the gradient the stop is located. The stop's
|
/// that determines how far along the gradient the stop is located. The stop's
|
||||||
/// color is the color of the gradient at that position. You can choose to omit
|
/// color is the color of the gradient at that position. You can choose to omit
|
||||||
/// the offsets when defining a gradient. In this case, Typst will space all
|
/// the offsets when defining a gradient. In this case, Typst will space all
|
||||||
@ -161,11 +173,21 @@ use crate::syntax::{Span, Spanned};
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum Gradient {
|
pub enum Gradient {
|
||||||
Linear(Arc<LinearGradient>),
|
Linear(Arc<LinearGradient>),
|
||||||
|
Radial(Arc<RadialGradient>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[scope]
|
#[scope]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
impl Gradient {
|
impl Gradient {
|
||||||
/// Creates a new linear gradient.
|
/// Creates a new linear gradient.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #rect(
|
||||||
|
/// width: 100%,
|
||||||
|
/// height: 20pt,
|
||||||
|
/// fill: gradient.linear(..color.map.viridis)
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
#[func(title = "Linear Gradient")]
|
#[func(title = "Linear Gradient")]
|
||||||
pub fn linear(
|
pub fn linear(
|
||||||
/// The args of this function.
|
/// The args of this function.
|
||||||
@ -226,6 +248,123 @@ impl Gradient {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new radial gradient.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #circle(
|
||||||
|
/// radius: 20pt,
|
||||||
|
/// fill: gradient.radial(..color.map.viridis)
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// _Focal Point_
|
||||||
|
/// The gradient is defined by two circles: the focal circle and the end circle.
|
||||||
|
/// The focal circle is a circle with center `focal-center` and radius `focal-radius`,
|
||||||
|
/// that defines the points at which the gradient starts and has the color of the
|
||||||
|
/// first stop. The end circle is a circle with center `center` and radius `radius`,
|
||||||
|
/// that defines the points at which the gradient ends and has the color of the last
|
||||||
|
/// stop. The gradient is then interpolated between these two circles.
|
||||||
|
///
|
||||||
|
/// Using these four values, also called the focal point for the starting circle and
|
||||||
|
/// the center and radius for the end circle, we can define a gradient with more
|
||||||
|
/// interesting properties than a basic radial gradient:
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #circle(
|
||||||
|
/// radius: 20pt,
|
||||||
|
/// fill: gradient.radial(..color.map.viridis, focal-center: (10%, 40%), focal-radius: 5%)
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#[func]
|
||||||
|
fn radial(
|
||||||
|
/// The call site of this function.
|
||||||
|
span: Span,
|
||||||
|
/// The color [stops](#stops) of the gradient.
|
||||||
|
#[variadic]
|
||||||
|
stops: Vec<Spanned<Stop>>,
|
||||||
|
/// 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>,
|
||||||
|
/// The radius of the last circle of the gradient.
|
||||||
|
///
|
||||||
|
/// By default, it is set to `{50%}`. The ending radius must be bigger
|
||||||
|
/// than the focal radius.
|
||||||
|
#[named]
|
||||||
|
#[default(Spanned::new(Ratio::new(0.5), Span::detached()))]
|
||||||
|
radius: Spanned<Ratio>,
|
||||||
|
/// The center of the focal circle of the gradient.
|
||||||
|
///
|
||||||
|
/// The focal center must be inside of the end circle.
|
||||||
|
///
|
||||||
|
/// A value of `{(50%, 50%)}` means that the focal circle is
|
||||||
|
/// centered inside of its container.
|
||||||
|
///
|
||||||
|
/// By default it is set to the same as the center of the last circle.
|
||||||
|
#[named]
|
||||||
|
#[default(Smart::Auto)]
|
||||||
|
focal_center: Smart<Axes<Ratio>>,
|
||||||
|
/// The radius of the focal circle of the gradient.
|
||||||
|
///
|
||||||
|
/// The focal center must be inside of the end circle.
|
||||||
|
///
|
||||||
|
/// By default, it is set to `{0%}`. The focal radius must be smaller
|
||||||
|
/// than the ending radius`.
|
||||||
|
#[named]
|
||||||
|
#[default(Spanned::new(Ratio::new(0.0), Span::detached()))]
|
||||||
|
focal_radius: Spanned<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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if focal_radius.v > radius.v {
|
||||||
|
bail!(error!(
|
||||||
|
focal_radius.span,
|
||||||
|
"the focal radius must be smaller than the end radius"
|
||||||
|
)
|
||||||
|
.with_hint("try using a focal radius of `0%` instead"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let focal_center = focal_center.unwrap_or(center);
|
||||||
|
let d_center_sqr = (focal_center.x - center.x).get().powi(2)
|
||||||
|
+ (focal_center.y - center.y).get().powi(2);
|
||||||
|
if d_center_sqr.sqrt() >= (radius.v - focal_radius.v).get() {
|
||||||
|
bail!(error!(span, "the focal circle must be inside of the end circle")
|
||||||
|
.with_hint("try using a focal center of `auto` instead"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Gradient::Radial(Arc::new(RadialGradient {
|
||||||
|
stops: process_stops(&stops)?,
|
||||||
|
center: center.map(From::from),
|
||||||
|
radius: radius.v,
|
||||||
|
focal_center,
|
||||||
|
focal_radius: focal_radius.v,
|
||||||
|
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> {
|
||||||
@ -235,6 +374,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::Radial(radial) => radial
|
||||||
|
.stops
|
||||||
|
.iter()
|
||||||
|
.map(|(color, offset)| Stop { color: *color, offset: Some(*offset) })
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,6 +387,7 @@ impl Gradient {
|
|||||||
pub fn space(&self) -> ColorSpace {
|
pub fn space(&self) -> ColorSpace {
|
||||||
match self {
|
match self {
|
||||||
Self::Linear(linear) => linear.space,
|
Self::Linear(linear) => linear.space,
|
||||||
|
Self::Radial(radial) => radial.space,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,14 +396,16 @@ impl Gradient {
|
|||||||
pub fn relative(&self) -> Smart<Relative> {
|
pub fn relative(&self) -> Smart<Relative> {
|
||||||
match self {
|
match self {
|
||||||
Self::Linear(linear) => linear.relative,
|
Self::Linear(linear) => linear.relative,
|
||||||
|
Self::Radial(radial) => radial.relative,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the angle of this gradient.
|
/// Returns the angle of this gradient.
|
||||||
#[func]
|
#[func]
|
||||||
pub fn angle(&self) -> Angle {
|
pub fn angle(&self) -> Option<Angle> {
|
||||||
match self {
|
match self {
|
||||||
Self::Linear(linear) => linear.angle,
|
Self::Linear(linear) => Some(linear.angle),
|
||||||
|
Self::Radial(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,6 +414,7 @@ impl Gradient {
|
|||||||
pub fn kind(&self) -> Func {
|
pub fn kind(&self) -> Func {
|
||||||
match self {
|
match self {
|
||||||
Self::Linear(_) => Self::linear_data().into(),
|
Self::Linear(_) => Self::linear_data().into(),
|
||||||
|
Self::Radial(_) => Self::radial_data().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,6 +435,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,6 +530,16 @@ impl Gradient {
|
|||||||
relative: linear.relative,
|
relative: linear.relative,
|
||||||
anti_alias: false,
|
anti_alias: false,
|
||||||
})),
|
})),
|
||||||
|
Self::Radial(radial) => Self::Radial(Arc::new(RadialGradient {
|
||||||
|
stops,
|
||||||
|
center: radial.center,
|
||||||
|
radius: radial.radius,
|
||||||
|
focal_center: radial.focal_center,
|
||||||
|
focal_radius: radial.focal_radius,
|
||||||
|
space: radial.space,
|
||||||
|
relative: radial.relative,
|
||||||
|
anti_alias: false,
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,12 +588,22 @@ impl Gradient {
|
|||||||
stops.dedup();
|
stops.dedup();
|
||||||
|
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
Self::Linear(grad) => Self::Linear(Arc::new(LinearGradient {
|
Self::Linear(linear) => Self::Linear(Arc::new(LinearGradient {
|
||||||
stops,
|
stops,
|
||||||
angle: grad.angle,
|
angle: linear.angle,
|
||||||
space: grad.space,
|
space: linear.space,
|
||||||
relative: grad.relative,
|
relative: linear.relative,
|
||||||
anti_alias: grad.anti_alias,
|
anti_alias: linear.anti_alias,
|
||||||
|
})),
|
||||||
|
Self::Radial(radial) => Self::Radial(Arc::new(RadialGradient {
|
||||||
|
stops,
|
||||||
|
center: radial.center,
|
||||||
|
radius: radial.radius,
|
||||||
|
focal_center: radial.focal_center,
|
||||||
|
focal_radius: radial.focal_radius,
|
||||||
|
space: radial.space,
|
||||||
|
relative: radial.relative,
|
||||||
|
anti_alias: radial.anti_alias,
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -445,17 +614,17 @@ impl Gradient {
|
|||||||
pub fn stops_ref(&self) -> &[(Color, Ratio)] {
|
pub fn stops_ref(&self) -> &[(Color, Ratio)] {
|
||||||
match self {
|
match self {
|
||||||
Gradient::Linear(linear) => &linear.stops,
|
Gradient::Linear(linear) => &linear.stops,
|
||||||
|
Gradient::Radial(radial) => &radial.stops,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Samples the gradient at a given position, in the given container.
|
/// Samples the gradient at a given position, in the given container.
|
||||||
/// Handles the aspect ratio and angle directly.
|
/// Handles the aspect ratio and angle directly.
|
||||||
pub fn sample_at(&self, (x, y): (f32, f32), (width, height): (f32, f32)) -> Color {
|
pub fn sample_at(&self, (x, y): (f32, f32), (width, height): (f32, f32)) -> Color {
|
||||||
let t = match self {
|
|
||||||
Self::Linear(linear) => {
|
|
||||||
// Normalize the coordinates.
|
// Normalize the coordinates.
|
||||||
let (mut x, mut y) = (x / width, y / height);
|
let (mut x, mut y) = (x / width, y / height);
|
||||||
|
let t = match self {
|
||||||
|
Self::Linear(linear) => {
|
||||||
// Handle the direction of the gradient.
|
// Handle the direction of the gradient.
|
||||||
let angle = linear.angle.to_rad().rem_euclid(TAU);
|
let angle = linear.angle.to_rad().rem_euclid(TAU);
|
||||||
|
|
||||||
@ -481,15 +650,38 @@ impl Gradient {
|
|||||||
|
|
||||||
(x as f64 * cos.abs() + y as f64 * sin.abs()) / length
|
(x as f64 * cos.abs() + y as f64 * sin.abs()) / length
|
||||||
}
|
}
|
||||||
|
Self::Radial(radial) => {
|
||||||
|
// Source: @Enivex - https://typst.app/project/pYLeS0QyCCe8mf0pdnwoAI
|
||||||
|
let cr = radial.radius.get();
|
||||||
|
let fr = radial.focal_radius.get();
|
||||||
|
let z = Vec2::new(x as f64, y as f64);
|
||||||
|
let p = Vec2::new(radial.center.x.get(), radial.center.y.get());
|
||||||
|
let q =
|
||||||
|
Vec2::new(radial.focal_center.x.get(), radial.focal_center.y.get());
|
||||||
|
|
||||||
|
if (z - q).hypot() < fr {
|
||||||
|
0.0
|
||||||
|
} else if (z - p).hypot() > cr {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
let uz = (z - q).normalize();
|
||||||
|
let az = (q - p).dot(uz);
|
||||||
|
let rho = cr.powi(2) - (q - p).hypot().powi(2);
|
||||||
|
let bz = (az.powi(2) + rho).sqrt() - az;
|
||||||
|
|
||||||
|
((z - q).hypot() - fr) / (bz - fr)
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sample(RatioOrAngle::Ratio(Ratio::new(t)))
|
self.sample(RatioOrAngle::Ratio(Ratio::new(t.clamp(0.0, 1.0))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Does this gradient need to be anti-aliased?
|
/// Does this gradient need to be anti-aliased?
|
||||||
pub fn anti_alias(&self) -> bool {
|
pub fn anti_alias(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Linear(linear) => linear.anti_alias,
|
Self::Linear(linear) => linear.anti_alias,
|
||||||
|
Self::Radial(radial) => radial.anti_alias,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -523,6 +715,7 @@ impl Gradient {
|
|||||||
impl Repr for Gradient {
|
impl Repr for Gradient {
|
||||||
fn repr(&self) -> EcoString {
|
fn repr(&self) -> EcoString {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Radial(radial) => radial.repr(),
|
||||||
Self::Linear(linear) => linear.repr(),
|
Self::Linear(linear) => linear.repr(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -590,6 +783,87 @@ impl Repr for LinearGradient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A gradient that interpolates between two colors along a circle.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct RadialGradient {
|
||||||
|
/// The color stops of this gradient.
|
||||||
|
pub stops: Vec<(Color, Ratio)>,
|
||||||
|
/// The center of last circle of this gradient.
|
||||||
|
pub center: Axes<Ratio>,
|
||||||
|
/// The radius of last circle of this gradient.
|
||||||
|
pub radius: Ratio,
|
||||||
|
/// The center of first circle of this gradient.
|
||||||
|
pub focal_center: Axes<Ratio>,
|
||||||
|
/// The radius of first circle of this gradient.
|
||||||
|
pub focal_radius: 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 RadialGradient {
|
||||||
|
fn repr(&self) -> EcoString {
|
||||||
|
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(&self.center.x.repr());
|
||||||
|
r.push_str(", ");
|
||||||
|
r.push_str(&self.center.y.repr());
|
||||||
|
r.push_str("), ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.radius != Ratio::new(0.5) {
|
||||||
|
r.push_str("radius: ");
|
||||||
|
r.push_str(&self.radius.repr());
|
||||||
|
r.push_str(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.focal_center != self.center {
|
||||||
|
r.push_str("focal-center: (");
|
||||||
|
r.push_str(&self.focal_center.x.repr());
|
||||||
|
r.push_str(", ");
|
||||||
|
r.push_str(&self.focal_center.y.repr());
|
||||||
|
r.push_str("), ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.focal_radius != Ratio::zero() {
|
||||||
|
r.push_str("focal-radius: ");
|
||||||
|
r.push_str(&self.focal_radius.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 {
|
||||||
|
r.push_str(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.push(')');
|
||||||
|
r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What is the gradient relative to.
|
/// What is the gradient relative to.
|
||||||
#[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum Relative {
|
pub enum Relative {
|
||||||
|
BIN
tests/ref/visualize/gradient-radial.png
Normal file
After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 474 KiB After Width: | Height: | Size: 474 KiB |
BIN
tests/ref/visualize/gradient-relative-radial.png
Normal file
After Width: | Height: | Size: 394 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 11 KiB |
49
tests/typ/visualize/gradient-radial.typ
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Test the different radial gradient features.
|
||||||
|
---
|
||||||
|
|
||||||
|
#square(
|
||||||
|
size: 100pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl),
|
||||||
|
)
|
||||||
|
---
|
||||||
|
|
||||||
|
#grid(
|
||||||
|
columns: 2,
|
||||||
|
square(
|
||||||
|
size: 50pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (0%, 0%)),
|
||||||
|
),
|
||||||
|
square(
|
||||||
|
size: 50pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (0%, 100%)),
|
||||||
|
),
|
||||||
|
square(
|
||||||
|
size: 50pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (100%, 0%)),
|
||||||
|
),
|
||||||
|
square(
|
||||||
|
size: 50pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (100%, 100%)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#square(
|
||||||
|
size: 50pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl, radius: 10%),
|
||||||
|
)
|
||||||
|
#square(
|
||||||
|
size: 50pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl, radius: 72%),
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
#circle(
|
||||||
|
radius: 25pt,
|
||||||
|
fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (35%, 35%), focal-radius: 5%),
|
||||||
|
)
|
||||||
|
#circle(
|
||||||
|
radius: 25pt,
|
||||||
|
fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (75%, 35%), focal-radius: 5%),
|
||||||
|
)
|
@ -1,5 +1,4 @@
|
|||||||
// Test whether `relative: "parent"` works correctly.
|
// Test whether `relative: "parent"` works correctly on linear gradients.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
// The image should look as if there is a single gradient that is being used for
|
// The image should look as if there is a single gradient that is being used for
|
29
tests/typ/visualize/gradient-relative-radial.typ
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Test whether `relative: "parent"` works correctly on radial 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.radial(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.radial(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))
|
@ -5,9 +5,17 @@
|
|||||||
size: 100pt,
|
size: 100pt,
|
||||||
fill: gradient.linear(..color.map.rainbow, space: color.hsl).sharp(10),
|
fill: gradient.linear(..color.map.rainbow, space: color.hsl).sharp(10),
|
||||||
)
|
)
|
||||||
|
#square(
|
||||||
|
size: 100pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10),
|
||||||
|
)
|
||||||
|
|
||||||
---
|
---
|
||||||
#square(
|
#square(
|
||||||
size: 100pt,
|
size: 100pt,
|
||||||
fill: gradient.linear(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
|
fill: gradient.linear(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
|
||||||
)
|
)
|
||||||
|
#square(
|
||||||
|
size: 100pt,
|
||||||
|
fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
|
||||||
|
)
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
// Test gradients on strokes.
|
// Test gradients on strokes.
|
||||||
|
|
||||||
---
|
---
|
||||||
#set page(width: 100pt, height: 100pt)
|
#set page(width: 100pt, height: auto, margin: 10pt)
|
||||||
#align(center + horizon, 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(
|
||||||
|
center + bottom,
|
||||||
|
square(
|
||||||
|
size: 50pt,
|
||||||
|
fill: gradient.radial(red, blue, radius: 70.7%, focal-center: (10%, 10%)),
|
||||||
|
stroke: 10pt + gradient.radial(red, blue, radius: 70.7%, focal-center: (10%, 10%))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test gradient on lines
|
// Test gradient on lines
|
||||||
|