Question about the scope of setColor() and setDitherPattern()

I was drawing a series of circles on top of each other with different dithering amounts to create a fade effect but I noticed that even though I set the color to white before drawing the circles, only the first circle drawn used white and the rest used black. The original method I tried was:

setColor() -> setDitherPattern() -> fillCircle() -> setDitherPattern() -> fillCircle() -> …

The way I eventually got it to work was:

setColor() -> setDitherPattern() -> fillCircle() -> setColor() -> setDitherPattern() -> fillCircle() -> …

This felt redundant since I was just setting the color to white over and over again. So my question is, is setColor() only used for a single draw call? Or is there something with setDitherPattern() that makes it act weird?

setting color resets the pattern, and there is some additional gotcha with dither pattern.

https://sdk.play.date/inside-playdate/#f-graphics.setDitherPattern

I'm not sure this answers your question...

Interesting... is the opposite true, i.e. does setting pattern reset the color?

Also, just to illustrate my point, for my stencil I start with a screen size black image, as shown here (ignore the UI on the right):

playdate-20230509-214134

I then draw a white dither pattern over it:

gfx.setColor(gfx.kColorWhite)
gfx.setDitherPattern(.75,gfx.image.kDitherTypeBayer8x8)
gfx.fillRect(0,0,screenWidth,screenHeight)

which results in:
playdate-20230509-214217

So far so good. But if I try to change the dither pattern before drawing a circle in the middle:

gfx.setColor(gfx.kColorWhite)
gfx.setDitherPattern(.75,gfx.image.kDitherTypeBayer8x8)
gfx.fillRect(0,0,screenWidth,screenHeight)

gfx.setDitherPattern(0,gfx.image.kDitherTypeBayer8x8)
gfx.fillCircleAtPoint(screenWidth/2,screenHeight/2,minRingSize)

here is what I get:
playdate-20230509-214257

I would expect that since I previously set the color to white, this circle would be white, but apparently I need to explicitly set the color to white again before drawing:

gfx.setColor(gfx.kColorWhite)
gfx.setDitherPattern(.75,gfx.image.kDitherTypeBayer8x8)
gfx.fillRect(0,0,screenWidth,screenHeight)

gfx.setColor(gfx.kColorWhite)
gfx.setDitherPattern(0,gfx.image.kDitherTypeBayer8x8)
gfx.fillCircleAtPoint(screenWidth/2,screenHeight/2,minRingSize)

to make it look how I expect:
playdate-20230509-214313

So that's why I'm wondering if changing the dither pattern resets the color somehow?

It looks that way, let's await official confirmation and docs update

Under the hood patterns and colors are interchangeable, and we don't have a separate slot for "current color" versus "current pattern". I had the original Macintosh graphics model in mind when I wrote that code. Where it went sideways is having setDitherPattern()'s behavior change if the current color/pattern is kColorWhite. That was a shortcut I added to help deal with masked patterns I think? The design of that API is not good. :frowning:

playdate.graphics.setDitherPattern(alpha, [ditherType])
Sets the pattern used for drawing to a dithered pattern. If the current drawing color is white, the pattern is white pixels on a transparent background and (due to a bug) the alpha value is inverted: 1.0 is transparent and 0 is opaque. Otherwise, the pattern is black pixels on a transparent background and alpha 0 is transparent while 1.0 is opaque.

I don't think there's much we can do now about that implementation, but I've filed an issue to add a note in Inside Playdate to explain that setting a color replaces a previously-set pattern and vice versa.

Interesting... So, you can only have one dither pattern or color active at a time, is that correct? That is to say, if I setColor then setDitherPattern, only the dither pattern is used?

However setDitherPattern looks at the currently set color to determine the dither behavior to use: if it's black it uses black pixels on transparent, if it's white it uses white pixels (with 0 meaning all white and 1 meaning all transparent)? But then after setting the dither pattern, the "set color" is actually just the dither pattern, so setting a new dither pattern doesn't have a color to look at so defaults to black?

That's exactly right. setColor() and setDitherPattern() both affect the same variable, a union type that can hold either a solid color or a pattern. The weird hack in setDitherPattern only applies when the current color/pattern is kColorWhite, as set by setColor().

This behavior might be more obvious if setColor were overloaded to support setting a color with a dither pattern. What if you were to add new API:

setColor(color, [alpha, [ditherType]])

  • It would be more clear that patterns are just “fancy colors.” It also makes logical sense, since setting a color with transparency is a normal operation in non-bitmap contexts.
  • It would use the specified color in the call, rather than the previously set color value, making it more explicit, and hopefully avoiding the confusion in this thread.
  • You could distinguish the function flavors based on the number of arguments.
  • This version could behave differently with respect to the bug noted for the existing implementation.
  • You could leave the legacy setDitherPattern API in place as-is to avoid a breaking change

I probably haven’t thought this all the way through, but on the back of a napkin it seems promising to me.

2 Likes

I actually love this idea. My mental model of setting color/dither pattern was like selecting a brush in Photoshop, where you can set the color and hardness of the brush independently, which is why I was a little confused when changing the opacity changed the color (and vice versa). Framing it as a single function call makes it more clear that color and opacity are controlled together, and you specify the color and the opacity of the “brush” at the same time as a package.

2 Likes