memononen / nanosvg

Simple stupid SVG parser
zlib License
1.65k stars 350 forks source link

Gradients #26

Open trogmaniac opened 9 years ago

trogmaniac commented 9 years ago

Although you say "it only renders flat filled shapes", i noticed a lot of gradient related code, but i never saw any gradients rendered. After some debugging, i found that nsvg__findGradientData is often called with something like "#linearGradient2802" as id, while the stored gradient has an id of "linearGradient2802", so it doesn't get found. After changing that, gradients started appearing and it's getting closer to what it's supposed to look like ;) I haven't found yet why the linear gradient doesn't look right.

nanosvg: gradients_nanosvg

ImageMagick gradients_imagemagick

I have some problems with implementing gradients for my own renderer though. The structure i have access to through the "chain of structures" is NSVGgradient:

typedef struct NSVGgradient { float xform[6]; char spread; float fx, fy; int nstops; NSVGgradientStop stops[1]; } NSVGgradient;

But i think i need the information stored in NSVGgradientData, which gives me these: typedef struct NSVGlinearData { float x1, y1, x2, y2; } NSVGlinearData;

typedef struct NSVGradialData { float cx, cy, r, fx, fy; } NSVGradialData;

I think i am missing something.

memononen commented 9 years ago

The code should handle removing the hash, looks like a bug. Can you make a separate issue about that with the above example file?

If the linear gradient is positioned using percentage, that will makes the parser to fail (if that is the case, please add a separate issues).

The xform[6] combines the shape transform and inverse of the gradient transform. So give a screen space position, you get a point in gradient space:

gx = fx*t[0] + fy*t[2] + t[4];
gy = fx*t[1] + fy*t[3] + t[5];

Where fx,fy is a screen space position, and gx, gyis in gradient space. For linear gradient the first point is at (0,0), and second point is at (0,1). For radial gradients, the center is at (0,0), and radius is at 1.

That form makes it easy for renderers, you can use to calculate texture coordinates or directly map the gradient on software renderer.

NanoSVG does not currently support offsetting the radial gradient focus point.

memononen commented 8 years ago

I impemented percent coordinates, and the gradients should work as expected. Can you check and let me know how it works for you?

trogmaniac commented 8 years ago

I tested many SVG from the Tango icon set and they all looked fine. I didn't do a side-by-side comparison to the ImageMagick output, but there were no apparent errors.

Very nice.

jamislike commented 8 years ago

Hey @memononen @trogmaniac , i was wondering if anyone could point me in the right direction, I am working on a renderer for Mac/iOS using Quartz/Core Graphics, for the most part, things are working quite well but I am a little confused with the gradient points stuff. CoreGraphics gradient drawing function takes a start point and end point but i can't seem

gx = fx_t[0] + fy_t[2] + t[4]; gy = fx_t[1] + fy_t[3] + t[5];

Above you gave this bit of code and said fx,fy should be screen space position and gy will be gradient space (are gx & gy 0.0 to 1.0 values).

So to get x1 and y1 should 'fx' & 'fy' be the origin of the shape, and to get 'x2 and y2 should fx & fy be the shape height and width plus the origin??

What i have in the linear gradient section is this : -

            PX_Rect shapeRect;
            shapeRect.origin.x    = shape->bounds[0];
            shapeRect.origin.y    = shape->bounds[1];
            shapeRect.size.width  = shape->bounds[2] - shapeRect.origin.x;
            shapeRect.size.height = shape->bounds[3] - shapeRect.origin.y;

            //
            float * t = shape->fill.gradient->xform;

From what you have said above should i be doing something like this : -

CGFloat gx = shape->bounds[0]_t[0] + shape->bounds[1]_t[2] + t[4];

And 'gx' would be 0 - 1.0 value which i scale up to the shape size?

Any help would be great!

memononen commented 8 years ago

This should allow you to get the the screen space start and end points:

static void xformInverse(float* inv, float* t)
{
    double invdet, det = (double)t[0] * t[3] - (double)t[2] * t[1];
    if (det > -1e-6 && det < 1e-6) {
        nsvg__xformIdentity(t);
        return;
    }
    invdet = 1.0 / det;
    inv[0] = (float)(t[3] * invdet);
    inv[2] = (float)(-t[2] * invdet);
    inv[4] = (float)(((double)t[2] * t[5] - (double)t[3] * t[4]) * invdet);
    inv[1] = (float)(-t[1] * invdet);
    inv[3] = (float)(t[0] * invdet);
    inv[5] = (float)(((double)t[1] * t[4] - (double)t[0] * t[5]) * invdet);
}

// Gradient xform allows us to quickly find normalized gradient position [0,1]
// based on screenspace position (fx,fy):
// float u = fx*t[1] + fy*t[3] + t[5]; 
// In order to get the screen space points from the gradient, we can used
// the inverse the gradient form, so that:
// (0,0) will give screen space start position, and
// (0,1) will give screen space end position

float t[6];
xformInverse(t, shape->fill.gradient->xform);

// (0,0)
float sx = t[4];
float sy = t[5];
// (0,1)
float ex = t[2] + t[4];
float ey = t[3] + t[5];
hoxsiew commented 7 years ago

Memononen, I hope you're still following this thread. I found your xformInverse() function to do the trick for me. Now, however, I'm perplexed by the shape->fill.gradient->fx and fy values. I'm rendering via Gdiplus and using a PathGradientBrush generating an ellipse for the path. The ellipse is set using a RectF from -1,-1 to 1,1 (center at 0,0) and this transforms perfectly using the inv[] array generated as above and the brush's SetTransform() function. The PathGradientBrush uses a SetCenterPoint() function to set the focus point offset from 0,0, but I can't figure out how to transform the fx,fy point to anything usable.

I would appreciate any insight you could provide.

memononen commented 7 years ago

fx,fy should be in the local coordinates of the gradient. So if you multiple the fx,fy by the t matrix in above code, you should get the focal point in world coordinates.

hoxsiew commented 7 years ago

Thanks for the quick reply, and thanks for an awesome tool for SVG.

This doesn't seem to be the case, or maybe I just can't get my head wrapped around all these coordinate spaces and the linear algebra.

For testing purposes, I have a gradient that is defined in the SVG (created by inkscape) basically as a circle with a 100 radius and filling a 200x200 rectangle with cx,cy at 100,100. My fx,fy is at 150,150. After parsing the image with nanosvg and looping through my shape structure, I get an fx,fy for the gradient of 1.5,1.5. I get an inverted matrix, t[] (as per above), of [100,0,0,100,150,150] If I multiply fx,fy by t[] , I get 300,300 which is not where I need to be. To do the multiplication, I'm filling a matrix with [1,0,0,1,fx,fy] and calling nsvg__xformMultiply().

I hope I've supplied enough info. If you'd like to see my test svg and/or my code, let me know.

memononen commented 7 years ago

You should use matrix-vector multiply, see nsvg__xformPoint, where fx,fy is the point.

I think there's a problem with parsing too: https://github.com/memononen/nanosvg/blob/master/src/nanosvg.h#L864 should be:

        grad->fx = (fx - cx) / r;
        grad->fy = (fy - cy) / r;
hoxsiew commented 7 years ago

Thanks! The code change makes it work. And nsvg__xformPoint() makes it easier too.

memononen commented 7 years ago

Good! Would you min making a pull request for the parser fix?

hoxsiew commented 7 years ago

OK. It's been awhile since I've used github. Did I do the pull request correctly?

One other thing I see in the linear gradient code. I do it pretty much like the radial gradient as above, where I apply the inverse matrix (t[] above) to the brush, but I have to additionally rotate my gradient brush by 90° for the gradient to render correctly. This may just be a quirk of the gdiplus gradient brush though as I'm not clear from the MSDN documentation exactly how the linear gradient brush works.

RobinD42 commented 4 years ago

I'm playing around with yet another svg renderer based on nanosvg, this time for wxPython. I've got the rasterizer working well but I would also like to enable rendering direct to a wxGraphicsContext (which works much like CoreGraphics on OSX.) That is mostly working now too, but I'm stuck on radial gradients. Based on comments here and in a couple other places I've been experimenting with code like this:

        nsvg__xformInverse(inverse, self._ptr.xform)
        nsvg__xformPoint(&cx, &cy, 0, 0, inverse)
        nsvg__xformPoint(&r1, &r2, 0, 1, inverse)
        radius = r2 - cy

That gives me a (cx,cy) point that appears to be in the correct place, but the radius is way too short. Can anybody point me in the right direction? Here's a screenshot of my test. The rasterizer version is on the right, my GraphicsContext version on the left.

image

Here is the svg file: ring.zip

RobinD42 commented 4 years ago

Hi all, ignore my message above. I found how to properly apply the gradient's transform and all is rendering properly now.