Implement radial gradients along a line, following Pixman.

This commit is contained in:
Patrick Walton 2020-02-27 16:24:02 -08:00
parent 59783ad457
commit 40bb1b412c
1 changed files with 109 additions and 12 deletions

View File

@ -21,7 +21,7 @@ use pathfinder_geometry::transform2d::{Matrix2x2F, Transform2F};
use pathfinder_geometry::util;
use pathfinder_geometry::vector::{Vector2F, Vector2I};
use pathfinder_gpu::TextureSamplingFlags;
use pathfinder_simd::default::F32x4;
use pathfinder_simd::default::{F32x2, F32x4};
use std::fmt::{self, Debug, Formatter};
// The size of a gradient tile.
@ -382,26 +382,123 @@ impl Palette {
}
}
GradientGeometry::Radial { line: gradient_line, start_radius, end_radius } => {
GradientGeometry::Radial { line, start_radius: r0, end_radius: r1 } => {
// FIXME(pcwalton): Paint transparent if line has zero size and radii are equal,
// per spec.
let tex_transform_inv = tex_transform.inverse();
let line = *tex_transform * line;
// FIXME(pcwalton): This is not correct. Follow the spec.
let center = gradient_line.midpoint();
// This is based on Pixman (MIT license). Copy and pasting the excellent comment
// from there:
// Implementation of radial gradients following the PDF specification.
// See section 8.7.4.5.4 Type 3 (Radial) Shadings of the PDF Reference
// Manual (PDF 32000-1:2008 at the time of this writing).
//
// In the radial gradient problem we are given two circles (c₁,r₁) and
// (c₂,r₂) that define the gradient itself.
//
// Mathematically the gradient can be defined as the family of circles
//
// ((1-t)·c₁ + t·(c₂), (1-t)·r₁ + t·r₂)
//
// excluding those circles whose radius would be < 0. When a point
// belongs to more than one circle, the one with a bigger t is the only
// one that contributes to its color. When a point does not belong
// to any of the circles, it is transparent black, i.e. RGBA (0, 0, 0, 0).
// Further limitations on the range of values for t are imposed when
// the gradient is not repeated, namely t must belong to [0,1].
//
// The graphical result is the same as drawing the valid (radius > 0)
// circles with increasing t in [-inf, +inf] (or in [0,1] if the gradient
// is not repeated) using SOURCE operator composition.
//
// It looks like a cone pointing towards the viewer if the ending circle
// is smaller than the starting one, a cone pointing inside the page if
// the starting circle is the smaller one and like a cylinder if they
// have the same radius.
//
// What we actually do is, given the point whose color we are interested
// in, compute the t values for that point, solving for t in:
//
// length((1-t)·c₁ + t·(c₂) - p) = (1-t)·r₁ + t·r₂
//
// Let's rewrite it in a simpler way, by defining some auxiliary
// variables:
//
// cd = c₂ - c₁
// pd = p - c₁
// dr = r₂ - r₁
// length(t·cd - pd) = r₁ + t·dr
//
// which actually means
//
// hypot(t·cdx - pdx, t·cdy - pdy) = r₁ + t·dr
//
// or
//
// ⎷((t·cdx - pdx)² + (t·cdy - pdy)²) = r₁ + t·dr.
//
// If we impose (as stated earlier) that r₁ + t·dr >= 0, it becomes:
//
// (t·cdx - pdx)² + (t·cdy - pdy)² = (r₁ + t·dr)²
//
// where we can actually expand the squares and solve for t:
//
// t²cdx² - 2t·cdx·pdx + pdx² + t²cdy² - 2t·cdy·pdy + pdy² =
// = r₁² + 2·r₁·t·dr + t²·dr²
//
// (cdx² + cdy² - dr²)t² - 2(cdx·pdx + cdy·pdy + r₁·dr)t +
// (pdx² + pdy² - r₁²) = 0
//
// A = cdx² + cdy² - dr²
// B = pdx·cdx + pdy·cdy + r₁·dr
// C = pdx² + pdy² - r₁²
// At² - 2Bt + C = 0
//
// The solutions (unless the equation degenerates because of A = 0) are:
//
// t = (B ± ⎷(B² - A·C)) / A
//
// The solution we are going to prefer is the bigger one, unless the
// radius associated to it is negative (or it falls outside the valid t
// range).
//
// Additional observations (useful for optimizations):
// A does not depend on p
//
// A < 0 <=> one of the two circles completely contains the other one
// <=> for every p, the radiuses associated with the two t solutions
// have opposite sign
let cd = line.vector();
let dr = r1 - r0;
let a = cd.square_length() - dr * dr;
let a_inv = 1.0 / a;
// TODO(pcwalton): Optimize this:
// 1. Calculate ∇t up front and use differencing in the inner loop, if possible.
// 2. Go four pixels at a time with SIMD.
for y in 0..(GRADIENT_TILE_LENGTH as i32) {
for x in 0..(GRADIENT_TILE_LENGTH as i32) {
let point = tex_rect.origin() + Vector2I::new(x, y);
let vector = tex_transform_inv * point.to_f32().scale_xy(tex_scale);
let point_f = point.to_f32();
let pd = point_f - line.from();
let t = util::clamp((vector - center).length(), start_radius, end_radius) /
(end_radius - start_radius);
let b = pd.dot(cd) + r0 * dr;
let c = pd.square_length() - r0 * r0;
let discrim = b * b - a * c;
put_pixel(point, gradient.sample(t), texels, tex_size);
let mut color = ColorU::transparent_black();
if !util::approx_eq(discrim, 0.0) {
let discrim_sqrt = f32::sqrt(discrim);
let discrim_sqrts = F32x2::new(discrim_sqrt, -discrim_sqrt);
let ts = (discrim_sqrts + F32x2::splat(b)) * F32x2::splat(a_inv);
let t_min = f32::min(ts.x(), ts.y());
let t_max = f32::max(ts.x(), ts.y());
let t = if t_max <= 1.0 { t_max } else { t_min };
if t >= 0.0 {
color = gradient.sample(t);
}
};
put_pixel(point, color, texels, tex_size);
}
}
}