Saturday, November 28, 2009

Gradients: a Halftone Gradient

So much to do: so little time! I have 3 other fun subjects to blog about... all of them in varying states of completion. But this is the most finished, so it's the first to write up.

Halftone Gradients


I don't know why, but I've always wanted a gradient that looked like halftoning.

But for some reason whenever I thought of tackling it: I always envisioned myself writing a PaintContext and coming up with some ugly code that involved lots of varying radii, and antialiasing things by hand, and doing clever things with AffineTransforms and dot products that I'm not really well-versed in. And maybe that is the better/more efficient way to approach this subject... but there's a much easier way.

My last blog entry used transformed tiles to achieve a multiple-color gradient. If you look at a halftone gradient as a tiling pattern, then suddenly it becomes a piece of cake. I just need to create a tall, skinny tile that transitions from one color to another.

If I were a better blog author, I might try inserting a diagram here. I'd demonstrate exactly what the tile looks like, and maybe discuss the concept of halftoning a little bit more.

However I'm not a better author. I'm much too lazy. Plus (and I'm not *entirely* bs-ing here): I really do believe a hands-on interactive model will explain this project more thoroughly than diagrams ever will. So... here's a demo applet showing off the HalftoneGradient class:



The jar (source included) is available here.

Your Basic Options


Although I started with circles, it's even easier to make a tiled pattern using diamonds or isosceles triangles. The size of each shape scales in proportion to the width of the tile, and where in the gradient that shape is drawn.

I'm unsure, though, if the circles are done correctly. I didn't find an exact algorithm for describing how halftoning should work -- but I admit I didn't look too closely. If a gradient is expressed in terms of t=[0,1]: does the gradient at t = .5 really look correct? Is that a half-way point between the two colors? For the circles I made the radius related to t^.8 to try to improve the balance. There is surely a more scientific way to approach this subject, but I didn't have the interest or patience. Does anyone have any experience with this subject? Basically the proportion of color1 to color2 should increase linearly with t.

Oh, and as an afterthought I added the "offset" property, and the "animate" checkbox. How could I resist? (Have you seen my art? I love animating things.)

Making a Tiled Pattern Not Tile


This paint delegates all the hard work to a transformed TexturePaint, so it should be tiling like mad when the "Cycle" checkbox is unselected, right? This was probably the hardest part of the project: if a point lies outside of the strip created between P1 and P2: how do I make it a solid color?

Originally I went to Sun's website and downloaded the source for the TexturePaintContext. I was determined to roll up my sleeves and make an adaptation that solved this. It didn't take me long to realize I was waaaaaaay over my head, though, as I looked through their source code. Aside from subtle problems (like referencing non-public classes to manipulate rasters), they somehow eliminated floats/doubles. Everything was int-based, on a scale of [0, Integer.MAX_VALUE]. While probably more efficient: this hurt my head. If you locked me in a dark prison cell with just bagels and a laptop: maybe in a few days I could figure this out. But I wanted to figure this out in a few hours, so I moved on to plan B:

Instead I just rewrote the raster the TexturePaintContext returns. Is it the most efficient response? No. Does it work? Yes. The class is called CoveredContext. (It's non-public, although it's included in the jar mentioned above.) In its original draft the hard work looked like this:

public Raster getRaster(int x, int y, int w, int h) {
WritableRaster raster = (WritableRaster)context.getRaster(x, y, w, h);
...
double[] matrix = new double[6];
transform.getMatrix(matrix);
for(int y2 = 0; y2 < h; y2++) {
raster.getDataElements(0, y2, w, 1, data);

for(int x2 = 0; x2 < data.length; x2++) {
double x3 = x2+x;
double y3 = y2+y;

// apply the transform manually:
y3 = matrix[1]*x3+matrix[3]*y3+matrix[5];

if(y3 < 1) {
data[x2] = color1;
} else if(y3 >= distance-1) {
data[x2] = color2;
}
}

raster.setDataElements(0, y2, w, 1, data);
}
return raster;
}

The final draft, though, is much more mathematically clever. (It calculates the two x-points in a row of pixels that mark the beginning and end of the gradient, and flood fills everything beyond those points.) But it's basically the same approach in spirit. It involves many more special cases, though.

Conclusion


So there you have it. Very painless. The code, including comments and javadoc, probably comes to less than 500 lines of code. Eventually I plan to integrate this into some other projects I'm working on, but that will have to wait.

I haven't tried to break this down and scrutinize performance yet. This class first needs to demonstrate that it, in fact, useful...

No comments:

Post a Comment