The problem
The API offers two ways to draw an arc:
playdate.graphics.drawArc(arc)
playdate.graphics.drawArc(x, y, radius, startAngle, endAngle)
Neither of these methods respect the direction
of the arc (and, notably, the latter doesn't support an optional direction argument). For example, this clockwise arc works as expected, and draws a 90 degree arc in the upper right quadrant:
gfx.drawArc(200, 120, 100, 0, 90)
However, this counter-clockwise arc does not. It renders a 270 degree arc in the other three quadrants, as though it were specified as starting at 90 and ending at 360:
gfx.drawArc(200, 120, 100, 90, 0)
The same erroneous results are achieved when creating a playdate.geometry.arc
and passing it directly to the other version of the function.
A workaround
This can be worked around, of course, since the direction of the arc doesn't matter once it's drawn. (I surmise that's why the drawing API doesn't even offer the optional direction
flag.)
local arc = geom.arc.new(200, 120, 100, 90, 0)
print(arc:isClockwise()) -- false
print(arc:length()) -- 157 (25% of circumference)
if arc:isClockwise() then
gfx.drawArc(arc)
else
-- The above arc will draw in this branch since it's CCW. Swapping
-- the start and end angles causes it to draw correctly.
gfx.drawArc(arc.x, arc.y, arc.radius, arc.endAngle, arc.startAngle)
end
Why this should be fixed
If this were purely a graphics API I'd understand the limitation as imposed; but these are geometric models, and their visual representation should accurately reflect their properties. I'm working on a game that is using arcs as part of a physics system, so I've already got them defined elsewhere, and need to draw their representation on screen.
The cruel irony is that the version of the function which accepts the properties as individual arguments is easy to fix. The version which takes an actual pd.geomertry.arc
can't be fixed without unpacking its properties to pass to the other version, rendering it effectively useless unless you can guarantee you'll never see a CCW arc.
A final note/question
I commonly find myself needing to translate between world and local sprite coordinates so that I can do physics calculations in world space and draw the various objects within their individual sprites. This causes me more grief than it should, as trying to compute the appropriate offset given the location and (relative) center of the sprite is a chore, and often error prone. Is there a more sensible way to do this?
I bring this up in the context of this thread because it's even more irritating with arcs. Most of the other geometry primitives can be easily offset (point
s, line
s, and rect
s have offsetBy
, vector2D
s can just be subtracted) but arc
s (and polygon
s) lack the conveneince. Adding playdate.geometry.arc:offset
and playdate.geometry.arc:offsetBy
would be nice.
Right now I'm working around all these limitations by holding onto two separate arc objects — one for calculation, and the other for drawing, with code that looks like this:
-- we draw relative to our own center point, but collision calculations are in world space;
-- also, arc drawing doesn't respect direction, so we need to swap start/end angles accordingly
local cx, cy = self:getCenter()
self.drawnArc = self.arc:copy()
self.drawnArc.x -= self.x - self.width * cx
self.drawnArc.y -= self.y - self.height * cy
self.drawnArc.startAngle = math.min(self.arc.startAngle, self.arc.endAngle)
self.drawnArc.endAngle = math.max(self.arc.startAngle, self.arc.endAngle)
self.drawEndpoint1 = self.drawnArc:pointOnArc(0)
self.drawEndpoint2 = self.drawnArc:pointOnArc(self.drawnArc:length())
This could all be so much cleaner if I could just do something like this in draw
:
drawArc(self.arc:offsetBy(-self.x - self.width * cx, -self.y - self.height * cy))
Or even:
drawArc(self.arc:offsetBy(-self:getWorldCenter())