google / iconvg

IconVG is a compact, binary format for simple vector graphics: icons, logos, glyphs and emoji.
Apache License 2.0
674 stars 11 forks source link

Linear gradient matrix math #22

Open Hixie opened 3 years ago

Hixie commented 3 years ago

The appendix says that the linear gradient matrix can have d=0 and e=0. However, that means that the matrix in question is not invertible, so it's not clear to me what the expected effect actually is.

For example, suppose your matrix is:

| a=-0.06, b=-0.14, c= 7.04 |
| d= 0.00, e= 0.00, f= 0.00 |
|    0.00,    0.00,    1.00 |

How do you go from that to the x1,y1, x2,y2 pair that describes the linear matrix?

nigeltao commented 3 years ago

The matrix transforms from graphic coordinate-space (gx, gy) (the space for viewBox and path coordinates) to pattern coordinate-space (px, py) (where a linear gradient always ranges from px=0 to px=1, regardless of py). Specifically:

px = (a * gx) + (b * gy) + c
py = (d * gx) + (e * gy) + f

When, d, e and f are all zero, that second line collapses to py = 0. The py coordinate matters for radial gradients, but not for linear gradients, because of the "regardless of py".

For your matrix, a path node at (40, 30) in g-space maps to

px = (a * gx) + (b * gy) + c = (-0.06 * 40) + (-0.14 * 30) + 7.04 = 0.44

So (40, 30) in g-space maps to a gradient offset of 0.44, a little under halfway between the first gradient stop (assuming that it's at offset 0.0) and the last gradient stop (assuming at offset 1.0).


To recover the original (gx1, gy1) and (gx2, gy2) pair, (or, more generally one pair of the infinitely many equivalent pairs), recast (gx2, gy2) as (gx1+gxΔ, gy1+gyΔ). These two points map to px=0 and px=1 in pattern space. Furthermore, rotating that delta by 90 degrees gives another point (gx3, gy3) that maps to px=0. Solve the simultaneous equations:

px1 = 0 = (a * gx1) + (b * gy1) + c
px2 = 1 = (a * (gx1+gxΔ)) + (b * (gy1+gyΔ)) + c
px3 = 0 = (a * (gx1+gyΔ)) + (b * (gy1-gxΔ)) + c

To pick one out of the infinite options, set gy1=0 to simplify:

px1 = 0 = (a * gx1) + c
px2 = 1 = (a * (gx1+gxΔ)) + (b * +gyΔ) + c
px3 = 0 = (a * (gx1+gyΔ)) + (b * -gxΔ) + c

The first line gives gx1 = -c/a. Substituting that into the remaining equations:

1 = (a * +gxΔ)) + (b * +gyΔ)
0 = (a * +gyΔ)) + (b * -gxΔ)

Adding b times the first to a times the second:

b = (a*a + b*b) * gyΔ

So gyΔ = b / (a*a + b*b). A similar reduction gives gxΔ = a / (a*a + b*b). We now know gx1, gy1, gxΔ and gyΔ. To answer your original question:

How do you go from that to the x1,y1, x2,y2 pair that describes the linear matrix?

gx1 = -c/a
gy1 = 0
gx2 = gx1 + gxΔ = -c/a + (a / (a*a + b*b))
gy2 = gy1 + gyΔ = 0 + (b / (a*a + b*b))

If a=0 then replace "set gy1=0 to simplify" with "set gx1=0 to simplify" and the same process yields similar-but-different formulae, picking a different one of the infinitely many solutions. If both a=0 and b=0 then it's a degenerate pattern where every point in g-space maps to a gradient offset of c.

Hixie commented 3 years ago

Ah, the key fact I was missing is that there's an infinite number of pairs, good point. I hadn't thought that all the way through.

Hixie commented 3 years ago

If both a=0 and b=0 then it's a degenerate pattern where every point in g-space maps to a gradient offset of c.

If both and b are zero then the equations above simplify to 0=c and 1=c, how do you get from that to every point in g-space maps to a gradient offset of c?

nigeltao commented 3 years ago

I get that from:

px = (a * gx) + (b * gy) + c
py = (d * gx) + (e * gy) + f

which collapses to px = c, so the gradient offset is c.


simplify to 0=c and 1=c

I think you're talking about

px1 = 0 = (a * gx1) + (b * gy1) + c
px2 = 1 = (a * (gx1+gxΔ)) + (b * (gy1+gyΔ)) + c
px3 = 0 = (a * (gx1+gyΔ)) + (b * (gy1-gxΔ)) + c

The math gets hand-wavy, but I'd say that while a=0 and b=0, we also have gx1 etc being 'infinite' so that (a * gx1) is finite. Visually, a flat color is sort of equivalent to a linear gradient where the two defining gradient points are off at infinity. Different infinities. As I said, admittedly hand-wavy.

Hixie commented 3 years ago

Can we make it invalid for the matrix to be non-invertible? :-) (Meaning actually non-invertible for radial, and a=0,b=0 for linear.)

nigeltao commented 3 years ago

Can we make it invalid for the matrix to be non-invertible? :-)

Possibly, but then you get questions of floating point approximations. If ((a*e) - (b*d)) is very small but non-zero (in ideal numbers), but on some CPUs or implementations this gets rounded to zero (and so the matrix is non-invertible), does this mean that implementations can differ on whether an IconVG file (and its transformation matrix) is valid?

Hixie commented 3 years ago

I guess differing on the precise pixels is less bad than differing on whether to show anything at all, true...

I think at a minimum we should describe what to do for such cases, though. Presumably we should render a solid color, but which one? (For linear I guess we pick through and find which color c maps to? What about radial?)

Hixie commented 3 years ago

For now if the radial gradient matrix is non-invertible i'm just setting a, d, b, and e to 0.0 and if the linear gradient matrix non-invertible I'm just setting x1,y1 to -1e9 and x2,y2 to +1e9 (the skia "infinities"). This makes the linear gradients show the 0.5 color and the radial gradients go black, at least on the current test files if we pretend their matrices are non-invertable.

nigeltao commented 3 years ago

(For linear I guess we pick through and find which color c maps to? What about radial?)

For radial, if the four primary matrix elements are zero, the gradient offset would uniformly be sqrt(c*c + f*f). Again, the matrix maps from g-space to p-space and in p-space, radial gradients are always origin-centered and circular with radius=1.

At least, that's self-consistent 'mathematically'. It's arguable whether that makes sense 'intuitively'.

Hixie commented 3 years ago

I'm loathe to implement a bunch of code just to figure out what the equivalent flat-color from all the stops at that particular position is, given that it's only to handle what is basically an error, but I agree that that seems like vaguely the "most correct" answer.