Gradient Part 3 - Radial gradients (#2312)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-10-06 16:47:20 +02:00 committed by GitHub
parent bced71b250
commit e7443abfe6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 571 additions and 85 deletions

View File

@ -7,7 +7,8 @@ use super::color::{ColorSpaceExt, PaintEncode};
use super::page::{PageContext, Transforms};
use super::{AbsExt, PdfContext};
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
@ -54,6 +55,32 @@ pub fn write_gradients(ctx: &mut PdfContext) {
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
}
};
@ -231,12 +258,13 @@ fn register_gradient(
Relative::Parent => transforms.container_size,
};
let (offset_x, offset_y) = match gradient.angle().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 (offset_x, offset_y) =
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 transform = match gradient.unwrap_relative(false) {
Relative::Self_ => transforms.transform,
@ -252,7 +280,7 @@ fn register_gradient(
Ratio::new(size.y.to_pt()),
))
.pre_concat(Transform::rotate(Gradient::correct_aspect_ratio(
gradient.angle(),
gradient.angle().unwrap_or_else(Angle::zero),
size.aspect_ratio(),
))),
gradient: gradient.clone(),

View File

@ -14,7 +14,7 @@ use usvg::{NodeExt, TreeParsing};
use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, Meta, TextItem};
use crate::font::Font;
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,
};
use crate::image::{Image, ImageKind, RasterFormat};
@ -136,8 +136,11 @@ impl<'a> State<'a> {
}
/// Pre concat the container's transform.
fn pre_concat_container(self, container_transform: sk::Transform) -> Self {
Self { container_transform, ..self }
fn pre_concat_container(self, transform: sk::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.
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();
@ -512,7 +515,7 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
if let Some(fill) = &shape.fill {
let mut pixmap = None;
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(_)) {
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())
});
let mut pixmap = None;
let paint =
to_sk_paint(paint, state, shape.geometry.bbox_size(), None, &mut pixmap);
let bbox = shape.geometry.bbox_size();
let offset_bbox = (!matches!(shape.geometry, Geometry::Line(..)))
.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 {
width,
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.
///
/// `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>(
paint: &Paint,
state: State,
item_size: Size,
fill_transform: Option<sk::Transform>,
pixmap: &'a mut Option<Arc<sk::Pixmap>>,
gradient_map: Option<(Point, Axes<Ratio>)>,
) -> sk::Paint<'a> {
/// Actual sampling of the gradient, cached for performance.
#[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();
for x in 0..width {
for y in 0..height {
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();
pixmap.pixels_mut()[(y * width + x) as usize] =
@ -734,18 +786,18 @@ fn to_sk_paint<'a>(
sk_paint.anti_alias = true;
}
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::Parent => state.size,
};
let fill_transform =
fill_transform.unwrap_or_else(|| match gradient.unwrap_relative(false) {
Relative::Self_ => sk::Transform::identity(),
Relative::Parent => state
.container_transform
.post_concat(state.transform.invert().unwrap()),
});
let fill_transform = match relative {
Relative::Self_ => fill_transform.unwrap_or_default(),
Relative::Parent => state
.container_transform
.post_concat(state.transform.invert().unwrap()),
};
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;
@ -753,6 +805,7 @@ fn to_sk_paint<'a>(
gradient,
width.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
@ -860,3 +913,7 @@ fn alpha_mul(color: u32, scale: u32) -> u32 {
let ag = ((color >> 8) & mask) * scale;
(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)
}

View File

@ -8,6 +8,7 @@ use ttf_parser::{GlyphId, OutlineBuilder};
use xmlwriter::XmlWriter;
use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
use crate::eval::Repr;
use crate::font::Font;
use crate::geom::{
Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
@ -135,12 +136,15 @@ struct GradientRef {
enum GradientKind {
/// A linear gradient.
Linear,
/// A radial gradient.
Radial,
}
impl From<&Gradient> for GradientKind {
fn from(value: &Gradient) -> Self {
match value {
Gradient::Linear { .. } => GradientKind::Linear,
Gradient::Radial { .. } => GradientKind::Radial,
}
}
}
@ -664,48 +668,59 @@ impl SVGRenderer {
self.xml.write_attribute("y1", &y1);
self.xml.write_attribute("x2", &x2);
self.xml.write_attribute("y2", &y2);
for window in linear.stops.windows(2) {
let (start_c, start_t) = window[0];
let (end_c, end_t) = window[1];
self.xml.start_element("stop");
self.xml
.write_attribute_fmt("offset", format_args!("{start_t:?}"));
self.xml.write_attribute("stop-color", &start_c.to_hex());
self.xml.end_element();
// Generate (256 / len) stops between the two stops.
// This is a workaround for a bug in many readers:
// They tend to just ignore the color space of the gradient.
// 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.
let len = if gradient.anti_alias() {
(256 / linear.stops.len() as u32).max(2)
} else {
2
};
for i in 1..(len - 1) {
let t0 = i as f64 / (len - 1) as f64;
let t = start_t + (end_t - start_t) * t0;
let c = gradient.sample(RatioOrAngle::Ratio(t));
self.xml.start_element("stop");
self.xml.write_attribute_fmt("offset", format_args!("{t:?}"));
self.xml.write_attribute("stop-color", &c.to_hex());
self.xml.end_element();
}
self.xml.start_element("stop");
self.xml.write_attribute_fmt("offset", format_args!("{end_t:?}"));
self.xml.write_attribute("stop-color", &end_c.to_hex());
self.xml.end_element()
}
self.xml.end_element();
}
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 gradient.stops_ref().windows(2) {
let (start_c, start_t) = window[0];
let (end_c, end_t) = window[1];
self.xml.start_element("stop");
self.xml.write_attribute("offset", &start_t.repr());
self.xml.write_attribute("stop-color", &start_c.to_hex());
self.xml.end_element();
// Generate (256 / len) stops between the two stops.
// This is a workaround for a bug in many readers:
// They tend to just ignore the color space of the gradient.
// 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.
let len = if gradient.anti_alias() {
(256 / gradient.stops_ref().len() as u32).max(2)
} else {
2
};
for i in 1..(len - 1) {
let t0 = i as f64 / (len - 1) as f64;
let t = start_t + (end_t - start_t) * t0;
let c = gradient.sample(RatioOrAngle::Ratio(t));
self.xml.start_element("stop");
self.xml.write_attribute("offset", &t.repr());
self.xml.write_attribute("stop-color", &c.to_hex());
self.xml.end_element();
}
self.xml.start_element("stop");
self.xml.write_attribute("offset", &end_t.repr());
self.xml.write_attribute("stop-color", &end_c.to_hex());
self.xml.end_element()
}
self.xml.end_element();
}
self.xml.end_element()
@ -727,6 +742,13 @@ impl SVGRenderer {
&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);

View File

@ -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> {
type Output = Axes<T::Output>;

View File

@ -3,6 +3,8 @@ use std::f64::{EPSILON, NEG_INFINITY};
use std::hash::Hash;
use std::sync::Arc;
use kurbo::Vec2;
use super::color::{Hsl, Hsv};
use super::*;
use crate::diag::{bail, error, SourceResult};
@ -13,15 +15,25 @@ use crate::syntax::{Span, Spanned};
/// A color gradient.
///
/// Typst supports linear gradients through the
/// [`gradient.linear` function]($gradient.linear). Radial and conic gradients
/// will be available soon.
/// [`gradient.linear` function]($gradient.linear) and radial gradients through
/// the [`gradient.radial` function]($gradient.radial). Conic gradients will be
/// available soon.
///
/// See the [tracking issue](https://github.com/typst/typst/issues/2282) for
/// 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
/// 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
/// 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
@ -161,11 +173,21 @@ use crate::syntax::{Span, Spanned};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Gradient {
Linear(Arc<LinearGradient>),
Radial(Arc<RadialGradient>),
}
#[scope]
#[allow(clippy::too_many_arguments)]
impl Gradient {
/// Creates a new linear gradient.
///
/// ```example
/// #rect(
/// width: 100%,
/// height: 20pt,
/// fill: gradient.linear(..color.map.viridis)
/// )
/// ```
#[func(title = "Linear Gradient")]
pub fn linear(
/// 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.
#[func]
pub fn stops(&self) -> Vec<Stop> {
@ -235,6 +374,11 @@ impl Gradient {
.iter()
.map(|(color, offset)| Stop { color: *color, offset: Some(*offset) })
.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 {
match self {
Self::Linear(linear) => linear.space,
Self::Radial(radial) => radial.space,
}
}
@ -251,14 +396,16 @@ impl Gradient {
pub fn relative(&self) -> Smart<Relative> {
match self {
Self::Linear(linear) => linear.relative,
Self::Radial(radial) => radial.relative,
}
}
/// Returns the angle of this gradient.
#[func]
pub fn angle(&self) -> Angle {
pub fn angle(&self) -> Option<Angle> {
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 {
match self {
Self::Linear(_) => Self::linear_data().into(),
Self::Radial(_) => Self::radial_data().into(),
}
}
@ -287,6 +435,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),
}
}
@ -381,6 +530,16 @@ impl Gradient {
relative: linear.relative,
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();
Ok(match self {
Self::Linear(grad) => Self::Linear(Arc::new(LinearGradient {
Self::Linear(linear) => Self::Linear(Arc::new(LinearGradient {
stops,
angle: grad.angle,
space: grad.space,
relative: grad.relative,
anti_alias: grad.anti_alias,
angle: linear.angle,
space: linear.space,
relative: linear.relative,
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)] {
match self {
Gradient::Linear(linear) => &linear.stops,
Gradient::Radial(radial) => &radial.stops,
}
}
/// Samples the gradient at a given position, in the given container.
/// Handles the aspect ratio and angle directly.
pub fn sample_at(&self, (x, y): (f32, f32), (width, height): (f32, f32)) -> Color {
// Normalize the coordinates.
let (mut x, mut y) = (x / width, y / height);
let t = match self {
Self::Linear(linear) => {
// Normalize the coordinates.
let (mut x, mut y) = (x / width, y / height);
// Handle the direction of the gradient.
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
}
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?
pub fn anti_alias(&self) -> bool {
match self {
Self::Linear(linear) => linear.anti_alias,
Self::Radial(radial) => radial.anti_alias,
}
}
@ -523,6 +715,7 @@ impl Gradient {
impl Repr for Gradient {
fn repr(&self) -> EcoString {
match self {
Self::Radial(radial) => radial.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.
#[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Relative {

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

Before

Width:  |  Height:  |  Size: 474 KiB

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 11 KiB

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

View File

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

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

View File

@ -5,9 +5,17 @@
size: 100pt,
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(
size: 100pt,
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%),
)

View File

@ -1,8 +1,16 @@
// Test gradients on strokes.
---
#set page(width: 100pt, height: 100pt)
#align(center + horizon, square(size: 50pt, fill: black, stroke: 5pt + gradient.linear(red, blue)))
#set page(width: 100pt, height: auto, margin: 10pt)
#align(center + top, square(size: 50pt, fill: black, stroke: 5pt + gradient.linear(red, blue)))
#align(
center + bottom,
square(
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