Implement radial gradients along a line, following Pixman.
This commit is contained in:
parent
59783ad457
commit
40bb1b412c
|
@ -21,7 +21,7 @@ use pathfinder_geometry::transform2d::{Matrix2x2F, Transform2F};
|
||||||
use pathfinder_geometry::util;
|
use pathfinder_geometry::util;
|
||||||
use pathfinder_geometry::vector::{Vector2F, Vector2I};
|
use pathfinder_geometry::vector::{Vector2F, Vector2I};
|
||||||
use pathfinder_gpu::TextureSamplingFlags;
|
use pathfinder_gpu::TextureSamplingFlags;
|
||||||
use pathfinder_simd::default::F32x4;
|
use pathfinder_simd::default::{F32x2, F32x4};
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
// The size of a gradient tile.
|
// 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,
|
// FIXME(pcwalton): Paint transparent if line has zero size and radii are equal,
|
||||||
// per spec.
|
// per spec.
|
||||||
let tex_transform_inv = tex_transform.inverse();
|
let line = *tex_transform * line;
|
||||||
|
|
||||||
// FIXME(pcwalton): This is not correct. Follow the spec.
|
// This is based on Pixman (MIT license). Copy and pasting the excellent comment
|
||||||
let center = gradient_line.midpoint();
|
// 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 y in 0..(GRADIENT_TILE_LENGTH as i32) {
|
||||||
for x 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 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) /
|
let b = pd.dot(cd) + r0 * dr;
|
||||||
(end_radius - start_radius);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue