Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linear gradient matrix math #22

Open
Hixie opened this issue Jun 1, 2021 · 10 comments
Open

Linear gradient matrix math #22

Hixie opened this issue Jun 1, 2021 · 10 comments

Comments

@Hixie
Copy link
Contributor

Hixie commented Jun 1, 2021

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
Copy link
Collaborator

nigeltao commented Jun 2, 2021

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
Copy link
Contributor Author

Hixie commented Jun 2, 2021

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
Copy link
Contributor Author

Hixie commented Jun 2, 2021

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
Copy link
Collaborator

nigeltao commented Jun 2, 2021

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
Copy link
Contributor Author

Hixie commented Jun 2, 2021

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
Copy link
Collaborator

nigeltao commented Jun 2, 2021

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
Copy link
Contributor Author

Hixie commented Jun 2, 2021

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
Copy link
Contributor Author

Hixie commented Jun 2, 2021

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
Copy link
Collaborator

nigeltao commented Jun 3, 2021

(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
Copy link
Contributor Author

Hixie commented Jun 3, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants