Preventing dither flashing/flickering on moving objects by snapping to even pixels

Quantizing motions to 2-pixel increments solves the annoying flicker for 2x2 dithers (like checker and pinstripes), and reduces it for 4x4 or larger patterned dithers. The result feels much smother and nicer.

It's easy to do, even after your motion is already coded—you can still use easing, the crank, or any other arbitrary method to define the motion.

In case it helps anyone, here are some code examples.

Do these at the last minute before rendering the change. If you quantize earlier in the process, you may be rounding off values too soon, with unintended consequences.

For instance, if you are tracking a player position based on the crank, physics, or other arbitrary factors, you probably need to track position and changes in fractional pixels behind the scenes, so that curves and accelerations and the sums of multiple changes all stay accurate. Don't quantize those behind-the-scenes values—only the visible result at render time.

Example 1: Taking a progress value between 0 and 1 (from an animator with easing, for instance) and snapping it to quantized 2-pixel steps between a start point and end point (which need not be quantized themselves):

function interpolate2px (startX, startY, endX, endY, progress)
	local newX
	local newY
	
	newX = startX + 2 * math.floor((endX - startX) * progress / 2 + .5)
	newY = startY + 2 * math.floor((endY - startY) * progress / 2 + .5)
	
	return newX, newY
end

Example 2: Taking an arbitrary x, y coordinate (like a screen position—or a change in position) and snapping it to quantized 2-pixel steps:

function coordinates2px (oldX, oldY)
	local newX
	local newY
	
	newX = 2 * math.floor(oldX / 2 + .5)
	newY = 2 * math.floor(oldY / 2 + .5)
	
	return newX, newY
end

Note: the purpose of adding .5 is to make the quantizing happen by rounding to the nearest step. In some cases that's not necessary—omit the .5, and you will simply always snap lower (floor). In other cases, though, that will cause a little jump at the end of a movement, or will cause the object to end up slightly short of where you want. The .5 fixes that.

These examples make things move in 2-pixel increments to prevent flicker. They can still end on an odd pixel coordinate if they started on one. So if you also want all dithered objects to line up with each other and never be at odd coordinates, make sure they start that way: don't position an object starting at 3, 3, say.

If your dither is larger than 2x2 and you want to quantize to greater increments, just change the "2"s.

I'm new at Playdate... improvements and variations on these techniques are welcomed!

7 Likes