My thought was that by drawing at .5 opacity, if they draw over the same spot again it would add to 1.0 opacity, however it seems that returning to the same spot draws the exact same image again, so it remains at .5 opacity.
Does anyone know how I'd go about achieving additive opacity?
I think you would have to use a randomized dither, but that wouldn't be exactly right because some of the pixels will overlap, so 0.5 + 0.5 will almost never yield 1.0. Think of the spraycan tool that image editors sometimes have. The opacity will increase as you draw over the same spot, just not linearly. But maybe that's good enough for your purposes.
If you need it to work exactly like an alpha channel, the only approach I can think of is to use a grayscale bitmap internally, doing the alpha blending manually with say 8 bits per pixel, then dithering at the very end to display it. That idea isn't well-supported by our SDK, nor is it likely to have great performance, but feel free to give it a try. Wish I had a better solution for you!
Is there a way to do randomized dithering? This solution doesn’t need to be mathematically perfect, it just needs to have subsequent passes add to what’s already there.
"use a grayscale bitmap internally". Since the sdk doesn't support it, does that mean a Lua table of 400x240x2 where you store literal 1 and 0? That would be very slow to draw to the screen, right? Pixel for pixel. I might be spelling out what Dave is getting at, but just wanted to check whether I'm not missing something.
Yeah, you’d have to come up with an efficient way of storing the data. Probably not a Lua table of individual bits, but a table of integers (each representing 32 bits) might work OK.
C is probably a better language choice for this kind of code, though.
What about averaging playdate.graphics.image:sample(x, y) across all pixels under the current ‘brush’ (assuming this is some kind of art tool), then setting the dither pattern to average between this value and the selected opacity? Or add them together, per original post.
I actually do that already for a different reason. I'm using the sample color to determine your speed (the more "drawn on" an area is, the faster you go) so I suppose I could use that to understand the current color there for drawing purposes...
The only issue is I currently sample a fairly small area (3x3) to assign your speed, but I'd probably need to sample a larger area (say 16x16) to get the average for the entire brush area. Do you think that would be efficient to do every frame?
Just thinking it through a bit more - my suggestion would return 0.5 if you were half over a solid black area… if you need to differentiate between this and an existing 0.5 dither you’ll need to do something a bit more sophisticated. Maybe look at the variability within averages in 4 directions?
For efficiency, I’d use as few samples as possible - check if you can get away with something smaller.
What if you sampled every-other row and column in a 16x16 area, say? (As long as the dithers being sampled are random, not 2x2.) That would be sampling only 1/4 of the region's pixels—64 samples. Maybe that wouldn't be too much processing? (Famous last words.)
It could be even less if you ignored the corners and sampled a circular or octagonal region. The coordinates of the samples could come from a lookup table, so arbitrary sample patterns need not require computation.
I've got my mind on this problem and may have some ideas soon. But I do want to address this particular notion, since it works a bit unintuitively.
In alpha compositing, the formulas used almost universally across image software (including web browsers) were defined by Porter and Duff in 1984. They describe operations where there are two pixels--a background pixel and a foreground pixel, where each has a color and an alpha--and define how to compute the appropriate resulting pixel. In cases where the canvas itself is understood to be fully opaque (as is the case in this thought experiment), the background pixel's alpha can be assumed to be fully opaque, so I'll be working on that assumption throughout this post.
The RGB color values as well as the alpha are to be in the range of 0 to 1, inclusive. The general simple-case blending formula looks like this:
In other words, drawing the color in the same position twice with 50% opacity is not the same as drawing the color once with 100% opacity. An implementation that doesn't take this into account will yield some wrong-looking results.
I feel like using some form of ordered dithering for alpha might work, where instead of using the thresholds to select the color you use them to select whether or not to overwrite the corresponding destination pixel with the source pixel...
So I tried it and got it working reasonably well. Not sure if it's 100% correct and I may try something like librif later but I think it works well enough for now.