Part 5a: Gradients on text with relative: auto or relative: "parent" (#2364)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-10-12 18:03:52 +02:00 committed by GitHub
parent d3b62bd02e
commit a59666369b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 389 additions and 84 deletions

View File

@ -400,7 +400,7 @@ pub(super) fn decorate(
let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift;
let stroke = stroke.clone().unwrap_or(FixedStroke {
paint: text.fill.clone(),
paint: text.fill.as_decoration(),
thickness: metrics.thickness.at(text.size),
..FixedStroke::default()
});

View File

@ -182,10 +182,16 @@ pub struct TextElem {
#[parse({
let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
if let Some(paint) = &paint {
// TODO: Implement gradients on text.
if matches!(paint.v, Paint::Gradient(_)) {
bail!(error!(paint.span, "text fill must be a solid color")
.with_hint("gradients on text will be supported soon"));
if let Paint::Gradient(gradient) = &paint.v {
if gradient.relative() == Smart::Custom(Relative::Self_) {
bail!(
error!(
paint.span,
"gradients on text must be relative to the parent"
)
.with_hint("make sure to set `relative: auto` on your text fill")
);
}
}
}
paint.map(|paint| paint.v)

View File

@ -274,30 +274,35 @@ impl ColorEncode for ColorSpace {
/// Encodes a paint into either a fill or stroke color.
pub(super) trait PaintEncode {
/// Set the paint as the fill color.
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms);
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms);
/// Set the paint as the stroke color.
fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms);
fn set_as_stroke(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms);
}
impl PaintEncode for Paint {
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms) {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) {
match self {
Self::Solid(c) => c.set_as_fill(ctx, transforms),
Self::Gradient(gradient) => gradient.set_as_fill(ctx, transforms),
Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms),
}
}
fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
fn set_as_stroke(
&self,
ctx: &mut PageContext,
on_text: bool,
transforms: Transforms,
) {
match self {
Self::Solid(c) => c.set_as_stroke(ctx, transforms),
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, transforms),
Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms),
}
}
}
impl PaintEncode for Color {
fn set_as_fill(&self, ctx: &mut PageContext, _: Transforms) {
fn set_as_fill(&self, ctx: &mut PageContext, _: bool, _: Transforms) {
match self {
Color::Luma(_) => {
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);
@ -350,7 +355,7 @@ impl PaintEncode for Color {
}
}
fn set_as_stroke(&self, ctx: &mut PageContext, _: Transforms) {
fn set_as_stroke(&self, ctx: &mut PageContext, _: bool, _: Transforms) {
match self {
Color::Luma(_) => {
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);

View File

@ -27,12 +27,14 @@ pub struct PdfGradient {
pub aspect_ratio: Ratio,
/// The gradient.
pub gradient: Gradient,
/// Whether the gradient is applied to text.
pub on_text: bool,
}
/// Writes the actual gradients (shading patterns) to the PDF.
/// This is performed once after writing all pages.
pub fn write_gradients(ctx: &mut PdfContext) {
for PdfGradient { transform, aspect_ratio, gradient } in
for PdfGradient { transform, aspect_ratio, gradient, on_text } in
ctx.gradient_map.items().cloned().collect::<Vec<_>>()
{
let shading = ctx.alloc.bump();
@ -89,7 +91,7 @@ pub fn write_gradients(ctx: &mut PdfContext) {
shading_pattern
}
Gradient::Conic(conic) => {
let vertices = compute_vertex_stream(conic);
let vertices = compute_vertex_stream(conic, aspect_ratio, on_text);
let stream_shading_id = ctx.alloc.bump();
let mut stream_shading =
@ -254,20 +256,25 @@ fn single_gradient(
}
impl PaintEncode for Gradient {
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms) {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) {
ctx.reset_fill_color_space();
let id = register_gradient(ctx, self, transforms);
let id = register_gradient(ctx, self, on_text, transforms);
let name = Name(id.as_bytes());
ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_fill_pattern(None, name);
}
fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
fn set_as_stroke(
&self,
ctx: &mut PageContext,
on_text: bool,
transforms: Transforms,
) {
ctx.reset_stroke_color_space();
let id = register_gradient(ctx, self, transforms);
let id = register_gradient(ctx, self, on_text, transforms);
let name = Name(id.as_bytes());
ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
@ -279,6 +286,7 @@ impl PaintEncode for Gradient {
fn register_gradient(
ctx: &mut PageContext,
gradient: &Gradient,
on_text: bool,
mut transforms: Transforms,
) -> EcoString {
// Edge cases for strokes.
@ -290,17 +298,21 @@ fn register_gradient(
transforms.size.y = Abs::pt(1.0);
}
let size = match gradient.unwrap_relative(false) {
let size = match gradient.unwrap_relative(on_text) {
Relative::Self_ => transforms.size,
Relative::Parent => transforms.container_size,
};
// Correction for y-axis flipping on text.
let angle = gradient.angle().unwrap_or_else(Angle::zero);
let angle = if on_text { Angle::rad(TAU as f64) - angle } else { angle };
let (offset_x, offset_y) = match gradient {
Gradient::Conic(conic) => (
-size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0,
-size.y * (1.0 - conic.center.y.get() / 2.0) / 2.0,
),
gradient => match gradient.angle().unwrap_or_else(Angle::zero).quadrant() {
_ => match angle.quadrant() {
Quadrant::First => (Abs::zero(), Abs::zero()),
Quadrant::Second => (size.x, Abs::zero()),
Quadrant::Third => (size.x, size.y),
@ -310,10 +322,10 @@ fn register_gradient(
let rotation = match gradient {
Gradient::Conic(_) => Angle::zero(),
gradient => gradient.angle().unwrap_or_default(),
_ => angle,
};
let transform = match gradient.unwrap_relative(false) {
let transform = match gradient.unwrap_relative(on_text) {
Relative::Self_ => transforms.transform,
Relative::Parent => transforms.container_transform,
};
@ -339,6 +351,7 @@ fn register_gradient(
size.aspect_ratio(),
))),
gradient: gradient.clone(),
on_text,
};
let index = ctx.parent.gradient_map.insert(pdf_gradient);
@ -371,9 +384,16 @@ fn write_patch(
c0: [u16; 3],
c1: [u16; 3],
angle: Angle,
on_text: bool,
) {
let theta = -TAU * t + angle.to_rad() as f32 + PI;
let theta1 = -TAU * t1 + angle.to_rad() as f32 + PI;
let mut theta = -TAU * t + angle.to_rad() as f32 + PI;
let mut theta1 = -TAU * t1 + angle.to_rad() as f32 + PI;
// Correction for y-axis flipping on text.
if on_text {
theta = (TAU - theta).rem_euclid(TAU);
theta1 = (TAU - theta1).rem_euclid(TAU);
}
let (cp1, cp2) =
control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1);
@ -434,10 +454,17 @@ fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point,
}
#[comemo::memoize]
fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
fn compute_vertex_stream(
conic: &ConicGradient,
aspect_ratio: Ratio,
on_text: bool,
) -> Arc<Vec<u8>> {
// Generated vertices for the Coons patches
let mut vertices = Vec::new();
// Correct the gradient's angle
let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio);
// We want to generate a vertex based on some conditions, either:
// - At the boundary of a stop
// - At the boundary of a quadrant
@ -507,10 +534,19 @@ fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
t_prime,
conic.space.convert(c),
c0,
conic.angle,
angle,
on_text,
);
write_patch(&mut vertices, t_prime, t_prime, c0, c1, conic.angle);
write_patch(
&mut vertices,
t_prime,
t_prime,
c0,
c1,
angle,
on_text,
);
write_patch(
&mut vertices,
@ -518,7 +554,8 @@ fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
t_next as f32,
c1,
conic.space.convert(c_next),
conic.angle,
angle,
on_text,
);
t_x = t_next;
@ -533,7 +570,8 @@ fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
t_next as f32,
conic.space.convert(c),
conic.space.convert(c_next),
conic.angle,
angle,
on_text,
);
t_x = t_next;

View File

@ -354,11 +354,11 @@ impl PageContext<'_, '_> {
self.state.size = size;
}
fn set_fill(&mut self, fill: &Paint, transforms: Transforms) {
fn set_fill(&mut self, fill: &Paint, on_text: bool, transforms: Transforms) {
if self.state.fill.as_ref() != Some(fill)
|| matches!(self.state.fill, Some(Paint::Gradient(_)))
{
fill.set_as_fill(self, transforms);
fill.set_as_fill(self, on_text, transforms);
self.state.fill = Some(fill.clone());
}
}
@ -390,7 +390,7 @@ impl PageContext<'_, '_> {
miter_limit,
} = stroke;
paint.set_as_stroke(self, transforms);
paint.set_as_stroke(self, false, transforms);
self.content.set_line_width(thickness.to_f32());
if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) {
@ -455,13 +455,21 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
let translation = Transform::translate(pos.x, pos.y);
ctx.save_state();
ctx.transform(translation.pre_concat(group.transform));
if group.frame.kind().is_hard() {
ctx.group_transform(translation.pre_concat(group.transform));
ctx.group_transform(
translation
.pre_concat(
ctx.state
.transform
.post_concat(ctx.state.container_transform.invert().unwrap()),
)
.pre_concat(group.transform),
);
ctx.size(group.frame.size());
}
ctx.transform(translation.pre_concat(group.transform));
if let Some(clip_path) = &group.clip_path {
write_path(ctx, 0.0, 0.0, clip_path);
ctx.content.clip_nonzero();
@ -485,7 +493,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
glyph_set.entry(g.id).or_insert_with(|| segment.into());
}
ctx.set_fill(&text.fill, ctx.state.transforms(Size::zero(), pos));
ctx.set_fill(&text.fill, true, ctx.state.transforms(Size::zero(), pos));
ctx.set_font(&text.font, text.size);
ctx.set_opacities(None, Some(&text.fill));
ctx.content.begin_text();
@ -550,7 +558,7 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
}
if let Some(fill) = &shape.fill {
ctx.set_fill(fill, ctx.state.transforms(shape.geometry.bbox_size(), pos));
ctx.set_fill(fill, false, ctx.state.transforms(shape.geometry.bbox_size(), pos));
}
if let Some(stroke) = stroke {

View File

@ -149,7 +149,7 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
for (pos, item) in frame.items() {
match item {
FrameItem::Group(group) => {
render_group(canvas, state.pre_translate(*pos), group);
render_group(canvas, state, *pos, group);
}
FrameItem::Text(text) => {
render_text(canvas, state.pre_translate(*pos), text);
@ -172,11 +172,18 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
}
/// Render a group frame with optional transform and clipping into the canvas.
fn render_group(canvas: &mut sk::Pixmap, state: State, group: &GroupItem) {
fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &GroupItem) {
let state = match group.frame.kind() {
FrameKind::Soft => state.pre_concat(group.transform.into()),
FrameKind::Soft => state.pre_translate(pos).pre_concat(group.transform.into()),
FrameKind::Hard => state
.pre_translate(pos)
.pre_concat(group.transform.into())
.pre_concat_container(
state
.transform
.post_concat(state.container_transform.invert().unwrap()),
)
.pre_concat_container(Transform::translate(pos.x, pos.y).into())
.pre_concat_container(group.transform.into())
.with_size(group.frame.size()),
};
@ -375,15 +382,23 @@ fn render_outline_glyph(
builder.0.finish()?
};
// TODO: Implement gradients on text.
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
let mut pixmap = None;
let paint = to_sk_paint(&text.fill, state, Size::zero(), None, &mut pixmap, None);
let paint = to_sk_paint(
&text.fill,
state.pre_concat(sk::Transform::from_scale(scale, -scale)),
Size::zero(),
true,
None,
&mut pixmap,
None,
);
let rule = sk::FillRule::default();
// Flip vertically because font design coordinate
// system is Y-up.
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
let ts = ts.pre_scale(scale, -scale);
canvas.fill_path(&path, &paint, rule, ts, state.mask);
return Some(());
@ -410,31 +425,47 @@ fn render_outline_glyph(
// doesn't exist, yet.
let bitmap =
rasterize(&text.font, id, ts.tx.to_bits(), ts.ty.to_bits(), ppem.to_bits())?;
match &text.fill {
Paint::Gradient(gradient) => {
let sampler = GradientSampler::new(gradient, &state, Size::zero(), true);
write_bitmap(canvas, &bitmap, &state, sampler)?;
}
Paint::Solid(color) => {
write_bitmap(canvas, &bitmap, &state, *color)?;
}
}
Some(())
}
fn write_bitmap<S: PaintSampler>(
canvas: &mut sk::Pixmap,
bitmap: &Bitmap,
state: &State,
sampler: S,
) -> Option<()> {
// If we have a clip mask we first render to a pixmap that we then blend
// with our canvas
if state.mask.is_some() {
let mw = bitmap.width;
let mh = bitmap.height;
let color = text.fill.unwrap_solid();
let color = sk::ColorU8::from(color);
// Pad the pixmap with 1 pixel in each dimension so that we do
// not get any problem with floating point errors along their border
let mut pixmap = sk::Pixmap::new(mw + 2, mh + 2)?;
for x in 0..mw {
for y in 0..mh {
let alpha = bitmap.coverage[(y * mw + x) as usize];
let color = sk::ColorU8::from_rgba(
color.red(),
color.green(),
color.blue(),
alpha,
)
.premultiply();
let color: sk::ColorU8 = sampler.sample((x, y)).into();
pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] = color;
pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] =
sk::ColorU8::from_rgba(
color.red(),
color.green(),
color.blue(),
alpha,
)
.premultiply();
}
}
@ -461,10 +492,6 @@ fn render_outline_glyph(
let top = bitmap.top;
let bottom = top + mh;
// Premultiply the text color.
let Paint::Solid(color) = text.fill else { todo!() };
let color = bytemuck::cast(sk::ColorU8::from(color).premultiply());
// Blend the glyph bitmap with the existing pixels on the canvas.
let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut());
for x in left.clamp(0, cw)..right.clamp(0, cw) {
@ -475,6 +502,8 @@ fn render_outline_glyph(
continue;
}
let color: sk::ColorU8 = sampler.sample((x as _, y as _)).into();
let color = bytemuck::cast(color.premultiply());
let pi = (y * cw + x) as usize;
if cov == 255 {
pixels[pi] = color;
@ -510,8 +539,15 @@ 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, None);
let mut paint: sk::Paint = to_sk_paint(
fill,
state,
shape.geometry.bbox_size(),
false,
None,
&mut pixmap,
None,
);
if matches!(shape.geometry, Geometry::Rect(_)) {
paint.anti_alias = false;
@ -578,6 +614,7 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
paint,
state,
offset_bbox,
false,
fill_transform,
&mut pixmap,
gradient_map,
@ -731,6 +768,71 @@ impl From<sk::Transform> for Transform {
}
}
/// Trait for sampling of a paint, used as a generic
/// abstraction over solid colors and gradients.
trait PaintSampler: Copy {
/// Sample the color at the `pos` in the pixmap.
fn sample(self, pos: (u32, u32)) -> Color;
}
impl PaintSampler for Color {
fn sample(self, _: (u32, u32)) -> Color {
self
}
}
/// State used when sampling colors for text.
///
/// It caches the inverse transform to the parent, so that we can
/// reuse it instead of recomputing it for each pixel.
#[derive(Clone, Copy)]
struct GradientSampler<'a> {
gradient: &'a Gradient,
container_size: Size,
transform_to_parent: sk::Transform,
}
impl<'a> GradientSampler<'a> {
fn new(
gradient: &'a Gradient,
state: &State,
item_size: Size,
on_text: bool,
) -> Self {
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
Relative::Self_ => item_size,
Relative::Parent => state.size,
};
let fill_transform = match relative {
Relative::Self_ => sk::Transform::identity(),
Relative::Parent => state.container_transform.invert().unwrap(),
};
Self {
gradient,
container_size,
transform_to_parent: fill_transform,
}
}
}
impl PaintSampler for GradientSampler<'_> {
/// Samples a single point in a glyph.
fn sample(self, (x, y): (u32, u32)) -> Color {
// Compute the point in the gradient's coordinate space.
let mut point = sk::Point { x: x as f32, y: y as f32 };
self.transform_to_parent.map_point(&mut point);
// Sample the gradient
self.gradient.sample_at(
(point.x, point.y),
(self.container_size.x.to_f32(), self.container_size.y.to_f32()),
)
}
}
/// Transforms a [`Paint`] into a [`sk::Paint`].
/// Applying the necessary transform, if the paint is a gradient.
///
@ -740,6 +842,7 @@ fn to_sk_paint<'a>(
paint: &Paint,
state: State,
item_size: Size,
on_text: bool,
fill_transform: Option<sk::Transform>,
pixmap: &'a mut Option<Arc<sk::Pixmap>>,
gradient_map: Option<(Point, Axes<Ratio>)>,
@ -782,7 +885,7 @@ fn to_sk_paint<'a>(
sk_paint.anti_alias = true;
}
Paint::Gradient(gradient) => {
let relative = gradient.unwrap_relative(false);
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
Relative::Self_ => item_size,
Relative::Parent => state.size,

View File

@ -51,7 +51,7 @@ pub fn svg_merged(frames: &[Frame], padding: Abs) -> String {
let [x, mut y] = [padding; 2];
for frame in frames {
let ts = Transform::translate(x, y);
let state = State::new(frame.size(), ts);
let state = State::new(frame.size(), Transform::identity());
renderer.render_frame(state, ts, frame);
y += frame.height() + padding;
}
@ -262,9 +262,9 @@ impl SVGRenderer {
fn render_group(&mut self, state: State, group: &GroupItem) {
let state = match group.frame.kind() {
FrameKind::Soft => state.pre_concat(group.transform),
FrameKind::Hard => {
state.with_transform(group.transform).with_size(group.frame.size())
}
FrameKind::Hard => state
.with_transform(Transform::identity())
.with_size(group.frame.size()),
};
self.xml.start_element("g");
@ -283,8 +283,7 @@ impl SVGRenderer {
/// Render a text item. The text is rendered as a group of glyphs. We will
/// try to render the text as SVG first, then bitmap, then outline. If none
/// of them works, we will skip the text.
// TODO: implement gradient on text.
fn render_text(&mut self, _state: State, text: &TextItem) {
fn render_text(&mut self, state: State, text: &TextItem) {
let scale: f64 = text.size.to_pt() / text.font.units_per_em();
let inv_scale: f64 = text.font.units_per_em() / text.size.to_pt();
@ -302,7 +301,23 @@ impl SVGRenderer {
self.render_svg_glyph(text, id, offset, inv_scale)
.or_else(|| self.render_bitmap_glyph(text, id, offset, inv_scale))
.or_else(|| self.render_outline_glyph(text, id, offset, inv_scale));
.or_else(|| {
self.render_outline_glyph(
state
.pre_concat(Transform::scale(
Ratio::new(scale),
Ratio::new(-scale),
))
.pre_translate(Point::new(
Abs::pt(offset / scale),
Abs::zero(),
)),
text,
id,
offset,
inv_scale,
)
});
x += glyph.x_advance.at(text.size).to_pt();
}
@ -388,25 +403,45 @@ impl SVGRenderer {
/// Render a glyph defined by an outline.
fn render_outline_glyph(
&mut self,
state: State,
text: &TextItem,
id: GlyphId,
glyph_id: GlyphId,
x_offset: f64,
inv_scale: f64,
) -> Option<()> {
let path = convert_outline_glyph_to_path(&text.font, id)?;
let hash = hash128(&(&text.font, id));
let path = convert_outline_glyph_to_path(&text.font, glyph_id)?;
let hash = hash128(&(&text.font, glyph_id));
let id = self.glyphs.insert_with(hash, || RenderedGlyph::Path(path));
self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml
.write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
self.write_fill(&text.fill, Size::zero(), Transform::identity());
self.write_fill(
&text.fill,
state.size,
self.text_paint_transform(state, &text.fill),
);
self.xml.end_element();
Some(())
}
fn text_paint_transform(&self, state: State, paint: &Paint) -> Transform {
let Paint::Gradient(gradient) = paint else {
return Transform::identity();
};
match gradient.unwrap_relative(true) {
Relative::Self_ => Transform::scale(Ratio::one(), Ratio::one()),
Relative::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
.post_concat(state.transform.invert().unwrap()),
}
}
/// Render a shape element.
fn render_shape(&mut self, state: State, shape: &Shape) {
self.xml.start_element("path");

View File

@ -19,9 +19,6 @@ use crate::syntax::{Span, Spanned};
/// the [`gradient.radial` function]($gradient.radial), and conic gradients
/// through the [`gradient.conic` function]($gradient.conic).
///
/// See the [tracking issue](https://github.com/typst/typst/issues/2282) for
/// more details on the progress of gradient implementation.
///
/// ```example
/// #stack(
/// dir: ltr,
@ -31,6 +28,25 @@ use crate::syntax::{Span, Spanned};
/// )
/// ```
///
/// # Gradients on text
/// Gradients are supported on text but only when setting the relativeness to
/// either `{auto}` (the default value) or `{"parent"}`. It was decided that
/// glyph-by-glyph gradients would not be supported out-of-the-box but can be
/// emulated using [show rules]($styling/#show-rules).
///
/// You can use gradients on text as follows:
///
/// ```example
/// #set page(margin: 1pt)
/// #set text(fill: gradient.linear(red, blue))
/// #let rainbow(content) = {
/// set text(fill: gradient.linear(..color.map.rainbow))
/// box(content)
/// }
///
/// This is a gradient on text, but with a #rainbow[twist]!
/// ```
///
/// # 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%}` or
@ -56,7 +72,8 @@ use crate::syntax::{Span, Spanned};
/// of a container. This container can either be the shape they are painted on,
/// or to the closest container ancestor. This is controlled by the `relative`
/// argument of a gradient constructor. By default, gradients are relative to
/// the shape they are painted on.
/// the shape they are painted on, unless the gradient is applied on text, in
/// which case they are relative to the closest ancestor container.
///
/// Typst determines the ancestor container as follows:
/// - For shapes that are placed at the root/top level of the document, the
@ -707,6 +724,22 @@ impl Gradient {
}
impl Gradient {
/// Clones this gradient, but with a different relative placement.
pub fn with_relative(mut self, relative: Relative) -> Self {
match &mut self {
Self::Linear(linear) => {
Arc::make_mut(linear).relative = Smart::Custom(relative);
}
Self::Radial(radial) => {
Arc::make_mut(radial).relative = Smart::Custom(relative);
}
Self::Conic(conic) => {
Arc::make_mut(conic).relative = Smart::Custom(relative);
}
}
self
}
/// Returns a reference to the stops of this gradient.
pub fn stops_ref(&self) -> &[(Color, Ratio)] {
match self {

View File

@ -10,14 +10,26 @@ pub enum Paint {
}
impl Paint {
/// Temporary method to unwrap a solid color used for text rendering.
/// Unwraps a solid color used for text rendering.
pub fn unwrap_solid(&self) -> Color {
// TODO: Implement gradients on text.
match self {
Self::Solid(color) => *color,
Self::Gradient(_) => panic!("expected solid color"),
}
}
/// Turns this paint into a paint for a text decoration.
///
/// If this paint is a gradient, it will be converted to a gradient with
/// relative set to [`Relative::Parent`].
pub fn as_decoration(&self) -> Self {
match self {
Self::Solid(color) => Self::Solid(*color),
Self::Gradient(gradient) => {
Self::Gradient(gradient.clone().with_relative(Relative::Parent))
}
}
}
}
impl<T: Into<Color>> From<T> for Paint {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,9 @@
// Tests gradients on text decorations.
---
#set text(fill: gradient.linear(red, blue))
Hello #underline[World]! \
Hello #overline[World]! \
Hello #strike[World]! \

View File

@ -0,0 +1,14 @@
// Test text gradients with radial and conic gradients.
---
#set page(width: 200pt, height: auto, margin: 10pt)
#set par(justify: true)
#set text(fill: gradient.radial(red, blue))
#lorem(30)
---
#set page(width: 200pt, height: auto, margin: 10pt)
#set par(justify: true)
#set text(fill: gradient.conic(red, blue, angle: 45deg))
#lorem(30)

View File

@ -1,7 +1,49 @@
// Test that gradient fills on text don't work (for now).
// Ref: false
// Test that gradient fills on text.
// The solid bar gradients are used to make sure that all transforms are
// correct: if you can see the text through the bar, then the gradient is
// misaligned to its reference container.
// Ref: true
---
// Hint: 17-43 gradients on text will be supported soon
// Error: 17-43 text fill must be a solid color
// Ref: false
// Make sure they don't work when `relative: "self"`.
// Hint: 17-61 make sure to set `relative: auto` on your text fill
// Error: 17-61 gradients on text must be relative to the parent
#set text(fill: gradient.linear(red, blue, relative: "self"))
---
// Test that gradient fills on text work for globally defined gradients.
#set page(width: 200pt, height: auto, margin: 10pt, background: {
rect(width: 100%, height: 30pt, fill: gradient.linear(red, blue))
})
#set par(justify: true)
#set text(fill: gradient.linear(red, blue))
#lorem(30)
---
// Sanity check that the direction works on text.
#set page(width: 200pt, height: auto, margin: 10pt, background: {
rect(height: 100%, width: 30pt, fill: gradient.linear(dir: btt, red, blue))
})
#set par(justify: true)
#set text(fill: gradient.linear(dir: btt, red, blue))
#lorem(30)
---
// Test that gradient fills on text work for locally defined gradients.
#set page(width: auto, height: auto, margin: 10pt)
#show box: set text(fill: gradient.linear(..color.map.rainbow))
Hello, #box[World]!
---
// Test that gradients fills on text work with transforms.
#set page(width: auto, height: auto, margin: 10pt)
#show box: set text(fill: gradient.linear(..color.map.rainbow))
#rotate(45deg, box[World])