mirror of
https://github.com/typst/typst
synced 2025-05-20 20:15:29 +08:00
Part 5a: Gradients on text with relative: auto
or relative: "parent"
(#2364)
This commit is contained in:
parent
d3b62bd02e
commit
a59666369b
@ -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()
|
||||
});
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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");
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
BIN
tests/ref/visualize/gradient-text-decorations.png
Normal file
BIN
tests/ref/visualize/gradient-text-decorations.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
BIN
tests/ref/visualize/gradient-text-other.png
Normal file
BIN
tests/ref/visualize/gradient-text-other.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
BIN
tests/ref/visualize/gradient-text.png
Normal file
BIN
tests/ref/visualize/gradient-text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
9
tests/typ/visualize/gradient-text-decorations.typ
Normal file
9
tests/typ/visualize/gradient-text-decorations.typ
Normal 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]! \
|
14
tests/typ/visualize/gradient-text-other.typ
Normal file
14
tests/typ/visualize/gradient-text-other.typ
Normal 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)
|
@ -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])
|
||||
|
Loading…
x
Reference in New Issue
Block a user