Arc Function

I have seen questions for arc drawing functions in the past, and the most commonly passed around one is a sample watch that draws the arc as a series of up to 300 fillCircle calls.... I'm posting this fully fleshed out alternate method in hopes that more arcwatch beauty can occur with a bit less battery drain. This is probably about the easiest and cheapest way to draw a smooth and pretty arc with minimal cost. It is a little rigged up with a hardwired polygon points count at 33, but I found that to be about the smallest you can reliably make the final polygon and get it to fully clear out the circle. I have it rendering 5 arcs in my last watchface and it does not drain battery much more than the stock text watchfaces, and there is no drawing lagtime like I've noticed in a couple that use the spam fillCircle method.

*edited 4/9 - migrated features from later comments into function posted here

class level globals needed:
var deg2rad = Math.PI/180;
var CLOCKWISE = -1;
var COUNTERCLOCKWISE = 1;


the method:
//dc = drawingcontext from the onUpdate(dc)
//x,y = centerpoint of circle from which to make the arc
//radius = how big
//thickness = how thick of an arc to draw
//angle = 0 (nothing) to 360 (Full circle) in degrees. If you have/use radians, you can swap to radians and remove the deg2rad conversion factor inside, but I'm a degree kind of guy :)
//offsetIn = -180 to 180 in degrees. 0 will start arc from top of screen. Depends on chosen drawing direction, -90 & CLOCKWISE starts arc at 9o'clock, 90 & CLOCKWISE starts at 3o'clock position, 180 and either direction starts from 6o'clock
//colors = array containing [arc color, background fill color(usually black), [border color]] -border color is optional, leave out for no border
//direction = either CLOCKWISE or COUNTERCLOCKWISE and determines which direction the arc will grow in
function drawArc(dc, x, y, radius, thickness, angle, offsetIn, colors, direction){
var color = colors[0];
var bg = colors[1];
var curAngle;
if(angle > 0){
dc.setColor(color,color);
dc.fillCircle(x,y,radius);

dc.setColor(bg,bg);
dc.fillCircle(x,y,radius-thickness);

if(angle < 360){
var pts = new [33];
pts[0] = [x,y];

angle = 360-angle;
var radiusClip = radius + 2;
var offset = 90*direction+offsetIn;

for(var i=1,dec=angle/30f; i <= 31; angle-=dec){
curAngle = direction*(angle-offset)*deg2rad;
pts= [x+radiusClip*Math.cos(curAngle), y+radiusClip*Math.sin(curAngle)];
i++;
}
pts[32] = [x,y];
dc.setColor(bg,bg);
dc.fillPolygon(pts);
}
}else{
dc.setColor(bg,bg);
dc.fillCircle(x,y,radius);
}
if(colors.size() == 3){
var border = colors[2];
dc.setColor(border, Gfx.COLOR_TRANSPARENT);
dc.drawCircle(x, y, radius);
dc.drawCircle(x, y, radius-thickness);
}
}
[/code]

sample call:
var screenWidth = dc.getWidth();
var screenHeight = dc.getHeight();
var clockTime = Sys.getClockTime();
drawArc(dc, screenWidth/2, screenHeight/2, screenHeight/2-5, 5, (clockTime.hour/24f) * 360, 0, [Gfx.COLOR_YELLOW, Gfx.COLOR_BLACK], CLOCKWISE);


That will draw the hour hand as seen in FaceIt4rc
Image here:
Or another of my arc-filled designs: FaceIt5Arc
  • If someone does have further optimizations (I'm sure there are some to be made, I only whipped this one up in a couple of hours last night), i'd be glad to add those as well.
  • If you want to be able to make an arc with a border around the full path that it can travel, add this to the bottom of the drawArc function outside any other if/else block, and pass in 3 colors when you call drawArc, where the 3rd one is the border color for the arc:

    *** this was incorporated into method in main post. does not need to be added again, but leaving code piece here for posterity
    if(colors.size() == 3){
    var border = colors[2];
    dc.setColor(border, Gfx.COLOR_TRANSPARENT);
    dc.drawCircle(x, y, radius);
    dc.drawCircle(x, y, radius-thickness);
    }


    If you don't pass a 3rd color in the colors array, it will remain borderless.
    Adds 2 more drawCircles to the mix, but still not too bad.
  • To draw the arc starting from any offset rather than always starting at the top of the screen, change the method to add an input offset parameter and add that to the offset inside the method. In the example below, passing in -90 for offsetIn and drawing CLOCKWISE would start your arc from the left side of the screen (and look a lot like the built-in Move Widget's arc.

    *** this was incorporated into method in main post. does not need to be added again, but leaving code piece here for posterity

    function drawArc(dc, x, y, radius, thickness, angle, offsetIn, colors, direction){
    .
    .
    .
    var offset = 90*direction+offsetIn; // or just drop the 90*direction altogether and pass in the entire offset yourself
    .
    .
    .
    }
  • Another cool thing that can be done with this is that the onShow can be easily animated to fill the arcs when the watchface first shows on the screen.
    Depending on how many arcs you have going on, it could get a little sluggish during the animation, because the animate method calls the update repeatedly each second rather than just once per second. If you have multiple arcs, it probably works best if limited to only displaying 1 arc during the animation period.

    First add a new class extending drawables for the Animation method to work on. Animation can currently (v2.6) work on either the locX or locY property of a drawable. I'm just using locX here to determine the % completion of the animation for each draw.

    class Animation extends Ui.Drawable {
    function initialize(params) {
    locX = params.get(:locX);
    if (locX == null) {
    locX = 1;
    }
    }
    }


    initialize your animatable state in the onLayout:
    var animatable;
    var screenWidth;
    var screenHeight;
    function onLayout(dc) {
    screenWidth = dc.getWidth();
    screenHeight = dc.getHeight();
    animatable = new Animation({}); //initializes with locX=1 from class def
    }


    add a call to UI.animate using your custom animatable:
    function onShow() {
    //this will run the EASE_IN style updating the locX property of animatable from 0 -> 1 (ie, 0->100%) over 2.5 seconds.
    //as alternative to null, pass method(:onFinishAnimate) or some other method in your view to run as a callback when the animation completes

    Ui.animate(animatable, :locX, Ui.ANIM_TYPE_EASE_IN, 0, 1, 2.5, null);

    }


    in your onUpdate, multiply the arc angle by animatable.locX to animate the filling of the arc over the 2.5 second animation period
    function onUpdate(dc) {
    var clockTime = Sys.getClockTime();
    var arcHourPct = clockTime.hour / 24f;

    //draw an arc representing hour of the day
    drawArc(dc, screenWidth/2, screenHeight/2, (screenHeight/2), 8, (arcHourPct) * 360 * animatable.locX, -90, [Gfx.COLOR_YELLOW, Gfx.COLOR_BLACK, Gfx.COLOR_LT_GRAY], CLOCKWISE);

    if(animatable.locX==1){
    //anything drawn inside here will be hidden until the animation completes.
    //in this case I'd be hiding the time text until the arc is done.
    dc.drawText(screenWidth/2, screenHeight/2, Gfx.FONT_LARGE, Lang.format("$1$$2$$3$",[hour.toString(), ":", min.format("%02d")])
    , Gfx.TEXT_JUSTIFY_CENTER | Gfx.TEXT_JUSTIFY_VCENTER);
    }
    }


    Of course this methodology could be applied to anything, not just the arcs. but be careful, the animate is COSTLY in terms of battery drainage. You definitely only want it happening in onShow() calls. Even putting it in an onExitSleep() would probably use too much battery over the course of a day to be worth the stylepoints.
  • This method draws the arc entirely by doing polygons, allowing for doing multiple arcs of the same radius and different colors like a top half/bottom half arc. A little less pretty, but also more friendly in terms of not erasing half your screen if you had other stuff inside the arc radius.
    Same parameters/functionality as the other method except colors is no longer needing an array, just the foreground color.

    var deg2rad = Math.PI/180;
    var CLOCKWISE = -1;
    var COUNTERCLOCKWISE = 1;

    function drawPolygonArc(dc, x, y, radius, thickness, angle, offsetIn, color, direction){
    var curAngle;
    direction = direction*-1;
    var ptCnt = 30;

    if(angle > 0f){
    var pts = new [ptCnt*2+2];
    var offset = 90f*direction+offsetIn;
    var dec = angle / ptCnt.toFloat();
    for(var i=0,angle=0; i <= ptCnt; angle+=dec){
    curAngle = direction*(angle-offset)*deg2rad;
    pts= [x+radius*Math.cos(curAngle), y+radius*Math.sin(curAngle)];
    i++;
    }
    for(var i=ptCnt+1; i <= ptCnt*2+1; angle-=dec){
    curAngle = direction*(angle-offset)*deg2rad;
    pts= [x+(radius-thickness)*Math.cos(curAngle), y+(radius-thickness)*Math.sin(curAngle)];
    i++;
    }
    dc.setColor(color,Gfx.COLOR_TRANSPARENT);
    dc.fillPolygon(pts);
    }
    }
    [/code]

    here's a semi-circle arc for hour of the day:
    var screenWidth = dc.getWidth();
    var screenHeight = dc.getHeight();
    var clockTime = Sys.getClockTime();
    drawArc(dc, screenWidth/2, screenHeight/2, screenHeight/2-5, 5, (clockTime.hour/24f) * 180, 0, Gfx.COLOR_YELLOW, CLOCKWISE);