Friday, December 15, 2017

Friday Fun LIV - Path Gradient

Aloha,

I've always thought about how to implement a path gradient. Meaning to say a gradient that follows a path. First of all keep in mind that this kind of gradient is not used very often but sometimes it can create some really nice effects.
I've mainly saw the effect in circular components like the fitness gauge on the Apple Watch...


The color of each bar changes along the bar as you can see on the image. For this specific problem I've created the ConicalGradient in the past which works nicely for circular components but what about this...



If you would like to fill the path above with a gradient from blue over green and yellow to red you will see that it won't work with linear gradients. You can either fill it horizontally or vertically but both times the gradient won't follow the path itself. So here are two examples...


To fill it with a gradient along the path you need to split the path into small segments and fill each segment with a linear gradient along the line.
If you go with this approach you can achieve something like follows...


As you can see here it works nicely but that was not that easy to realize. The most important part of this little fun project was taking a closer look to the goodies that are already in the JDK. Especially the Java2D package contains a lot of nice stuff.
So my idea was to use this kind of path gradient in the JavaFX canvas node and lucky me the Java2D api is quite close to the JavaFX canvas api.
I won't go through the code because it's simply to much but let me explain the steps that are needed to achieve that result...

  • Split the path into it's elements (MoveTo, LineTo, QuadCurveTo, BezierCurveTo, Close etc.)
  • Split each element into single lines
  • Stroke each single line with the right part of the complete gradient
Like already mentioned above a lot of the functionality is already in the JDK but unfortunately it sometimes is hidden in private classes that are not accessible from the outside. So the solution for this problem was to fork all parts that I needed from the Apache Harmony project (the stuff Android is based on). So in principle I've forked the all classes from the Java2D packages that are related to bounds, shapes and transforms.
In the classes above I've removed all 3D related things and completely switched to doubles instead of floats.
I've also added several other classes and added functionality to some of the classes. One problem was for example that in the JDK there was no code to split lines into "sub-lines" which means I had to add this kind of things.
I've also renamed some classes to be more compatible to the JavaFX canvas. So the CubicCurve now is a BezierCurve etc.
In the end I've needed a path class that holds all elements and a lookup mechanism for the gradient.
Long story short with my current implementation the code to produce the path gradient will look like follows...


GradientLookup gradientLookup = new GradientLookup();
gradientLookup.setStops(new Stop(0.0, Color.BLUE),
                        new Stop(0.25, Color.LIME),
                        new Stop(0.5, Color.YELLOW),
                        new Stop(0.75, Color.ORANGE),
                        new Stop(1.0, Color.RED));

Path path = new Path();
path.moveTo(91, 36);
path.lineTo(182, 124);
path.bezierCurveTo(248, 191, 92, 214, 92, 214);
path.bezierCurveTo(-26, 248, 200, 323, 200, 323);
path.bezierCurveTo(303, 355, 383, 141, 383, 141);


double lineWidth = 20;


Canvas            canvas = new Canvas(400, 400);
GraphicsContext2D ctx    = canvas.getGraphicsContext2D();
PathGradient.strokePathWithGradient(ctx, path, gradientLookup, lineWidth, StrokeLineCap.ROUND);


So as you can see I've tried to keep the api as close to the JavaFX canvas api as possible but instead of drawing the path directly on the canvas context you first create the path object, define a gradient lookup which contains the list of stops and colors you need for the gradient and define width for the stroked line.

That's already neat but it could even be better because in the Path class you will find a SVGParser and with this you can directly use a SVG path string from an SVG file.
As an example here is the SVG file of the path on the above image...


<?xml version="1.0" standalone="no"?>

<!-- Generator: Adobe Fireworks CS6, Export SVG Extension by Aaron Beall (http://fireworks.abeall.com) . Version: 0.6.1  -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg id="Untitled-Page%201" viewBox="0 0 500 500" style="background-color:#ffffff00" 
    version="1.1"xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
    x="0px" y="0px" width="500px" height="500px">
    <path d="M 91 36 L 182 124 C 248 191 92 214 92 214 C -26 248 200 323 200 323 C 303 355 383 141 383 141 " stroke="#000000" stroke-width="1" fill="none"/>
</svg>


And with the the mentioned parser the code above could also be written like follows...

Path path = new Path();
path.appendSVGPath("M 91 36 L 182 124 C 248 191 92 214 92 214 " +
                   "C -26 248 200 323 200 323 C 303 355 383 141 383 141");

double lineWidth = 20;

Canvas canvas       = new Canvas(400, 400);
GraphicsContext ctx = canvas.getGraphicsContext2D();

PathGradient.strokePathWithGradient(ctx, path, gradientLookup, lineWidth, StrokeLineCap.ROUND);

To me that looks quite ok :)
As always I do not really have any use case yet so the whole thing is not tested for all possible use cases but at least it will give you an idea what is possible :)

And also as always the code is available on github.

Well that's it for today...I hope it was somehow interesting...keep coding... :)

2 comments: