Log In  


Recently, I found myself in need of a function like circfill or rectfill that draws filled pie slices instead.

I tried a few different implementations (like drawing radial lines or the mid-point and mirror method), but I wasn't happy with their performance. Finally I settled on this:

-- draws a filled pie portion
-- x,y - circle center
-- r - radius
-- c - color
-- s,e - start and end arc angle
function piefill(x,y,r,c,s,e)
 -- safety: can't use this method without a valid draw target
 if(not get_draw_target()) return
 -- whole circle, no need for calculations
 if s==e then
  circfill(x,y,r,c)
  return
 end

 local d = r+r -- math diameter
 local d_ = d+1 -- pixel diameter
 local svec,evec = vec(r*cos(s),r*sin(s)),vec(r*cos(e),r*sin(e)) -- arc points of starting and ending angles
 local rd = function(n) return flr(n+0.5) end -- rounding function for convenience
 -- linear representation of the arc
 local offset = (s>0.5 and -svec.x-d or svec.x)+3*r
 local arclen = ((e>0.5 and -evec.x-d or evec.x)+3*r-offset)%(d+d)

 local sprite = userdata("u8",d_,d_)
 get_draw_target():blit(sprite,x-r,y-r,0,0,d_,d_) -- copy background to sprite for transparency
 sprite:mutate("u8",d_*d_) -- needs to be flat for lerp

 local y_,dx,sx,ex,x1,x2,chk1,chk2,ps
 for dy = 1-r, r-1 do
  y_ = (r+dy)*d_+r -- flat index row offset
  dx = sqrt(r*r-(dy*dy)) -- x of the circle
  sx,ex = sin(s)==0 and r+1 or dy*cos(s)/sin(s), sin(e)==0 and r+1 or dy*cos(e)/sin(e) -- x of the angles
  if(sx<ex) x1,x2,chk1,chk2=sx,ex,svec,evec else x1,x2,chk1,chk2=ex,sx,evec,svec

  -- determine which points to include
  ps = {}
  if(((dy>0 and dx-d or -dx)+3*r-offset)%(d+d) >= arclen) add(ps,rd(-dx))
  if(mid(0,x1,chk1.x)==x1 and mid(0,dy,chk1.y)==dy) add(ps,rd(x1))
  if(mid(0,x2,chk2.x)==x2 and mid(0,dy,chk2.y)==dy) add(ps,rd(x2))
  if(((dy>0 and -dx-d or dx)+3*r-offset)%(d+d) >= arclen) add(ps,rd(dx))
  if(dy==0 and #ps==3) deli(ps,2)

  -- draw row to sprite
  if #ps>1 then
   sprite:set(y_+ps[1],c)
   sprite:set(y_+ps[2],c)
   sprite:lerp(y_+ps[1],ps[2]-ps[1])

   if #ps==4 then
    sprite:set(y_+ps[3],c)
    sprite:set(y_+ps[4],c)
    sprite:lerp(y_+ps[3],ps[4]-ps[3])
   end
  end
 end

 sprite:mutate("u8",d_,d_) -- unflatten
 sprite:blit(get_draw_target(),0,0,x-r,y-r,d_,d_) -- draw sprite to display
end

The s and e parameters use the same units as the regular trigonometric functions, and swapping their values swaps which part of the pie is filled in.

In previous attempts I used atan2 a lot which really tanked performance. By comparison, the single square root per row has been significantly cheaper. Still, there are probably other improvements that can be made.

Below is the same method, but with calls to line instead of the double blitting and lerping of the above implementation. While it's a bit shorter, my limited testing suggests it is actually slightly slower.

function piefill(x,y,r,c,s,e)
 -- whole circle, no need for calculations
 if s==e then
  circfill(x,y,r,c)
  return
 end

 local d = r+r -- math diameter
 local svec,evec = vec(r*cos(s),r*sin(s)),vec(r*cos(e),r*sin(e)) -- arc points of starting and ending angles
 local rd = function(n) return flr(n+0.5) end -- rounding function for convenience
 -- linear representation of the arc
 local offset = (s>0.5 and -svec.x-d or svec.x)+3*r
 local arclen = ((e>0.5 and -evec.x-d or evec.x)+3*r-offset)%(d+d)

 local y_,dx,sx,ex,x1,x2,chk1,chk2,ps
 for dy = 1-r, r-1 do
  y_ = (r+dy)*d_+r -- flat index row offset
  dx = sqrt(r*r-(dy*dy)) -- x of the circle
  sx,ex = sin(s)==0 and r+1 or dy*cos(s)/sin(s), sin(e)==0 and r+1 or dy*cos(e)/sin(e) -- x of the angles
  if(sx<ex) x1,x2,chk1,chk2=sx,ex,svec,evec else x1,x2,chk1,chk2=ex,sx,evec,svec

  -- determine which points to include
  ps = {}
  if(((dy>0 and dx-d or -dx)+3*r-offset)%(d+d) >= arclen) add(ps,rd(-dx))
  if(mid(0,x1,chk1.x)==x1 and mid(0,dy,chk1.y)==dy) add(ps,rd(x1))
  if(mid(0,x2,chk2.x)==x2 and mid(0,dy,chk2.y)==dy) add(ps,rd(x2))
  if(((dy>0 and -dx-d or dx)+3*r-offset)%(d+d) >= arclen) add(ps,rd(dx))
  if(dy==0 and #ps==3) deli(ps,2)

  -- draw row with lines
  if #ps>1 then
   line(x+ps[1],y+dy,x+ps[2],y+dy,c)
   if(#ps==4) line(x+ps[3],y+dy,x+ps[4],y+dy,c)
  end
 end
end

One thing to keep in mind is that this function starts from the positive x axis and goes counterclockwise. While this is what the other trig functions do, it may not be very convenient. Personally, I'm using a slight variant that goes clockwise instead.

4



[Please log in to post a comment]