diff --git a/renderer/src/paint.rs b/renderer/src/paint.rs index 6766fe97..76e0556e 100644 --- a/renderer/src/paint.rs +++ b/renderer/src/paint.rs @@ -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); } } }