Struggling with multiple Clips in onPartialUpdate()

I have three items that I want to update more frequently than once a minute.  They are :-

The Secondhand (once a second)
A Steps counter (once every 2 seconds and only when it changes - next to 9 o'clock)
A Heart Rate display (once every 15 seconds and only when it changes - next to 3 o'clock)

My understanding of the processing logic is that there are, essentially, two canvases, one which is the watchface itself and which is updated once per minute by onUpdate() and the other, which is a clipped portion of the watchface canvas and which is updated each second.  The clips are additive with the whole clip as defined at that point being updated each second.  Any data written outside of the current clipped area is just ignored.

I want to be able to show the items as described above but in the order:-

Steps counter and Heart Rate
Hour & Minute hands
Central boss and secondhand

so that, effectively the minute and hour hands sit over the counters but under the secondhand.

The problem I'm having is that the Steps and heart rate counters are only being written to the clip canvas which is active at that second.  In between times they are blank although they do appear as the secondhand's clip area passes over each counter.  I can solve the problem by creating all three clips every second but that will give an even greater hit to average power consumed.

Here is my onPartialUpdate() code (note that min/hourPolyPoints are calculated in onUpdate())

    function onPartialUpdate( dc ) {
        // If we're not doing a full screen refresh we need to re-draw the background
        // before drawing the updated second hand position. Note this will only re-draw
        // the background in the area specified by the previously computed clipping region.
        if(!fullScreenRefresh) {
            drawBackground(dc);
        }

//----------  Initialise all variables   -----------------
        var width = dc.getWidth();
        var height = dc.getHeight();

        var clockTime = System.getClockTime();
        var newHR = Activity.getActivityInfo().currentHeartRate;   
	    var newSteps = ActivityMonitor.getInfo().steps;
        
//-------   Calculate all necessary clipping areas   ----------

//----------   Next is Heart Rate are we updating it?   ----------------
		if (null != newHR && newHR != HR && clockTime.sec % 10 == 0) {
			dc.setClip(width*0.75-(HRDims[0]/2),height*0.4,HRDims[0],HRDims[1]);
	    	dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_TRANSPARENT);  //  probably redundent
//			dc.clear();
			HR = newHR;
	    	dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_TRANSPARENT);
	     	dc.drawText(width*0.75,height*0.4, Graphics.FONT_TINY, HR.toString(), Graphics.TEXT_JUSTIFY_CENTER);
		}

//-----------   Finally check to see if we are updating Steps   ---------
    	if (newSteps > Steps && clockTime.sec % 2 == 0) {
			dc.setClip(width*0.25-(StepsDims[0]/2),height*0.4,StepsDims[0],StepsDims[1]);
	    	dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_BLACK);
			dc.clear();
			Steps = newSteps;
	    	dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_TRANSPARENT);
			dc.drawText(width*0.25, height*0.4, Graphics.FONT_TINY, Steps.toString(), Graphics.TEXT_JUSTIFY_CENTER);
		}

//-------   First off is the secondhand which is required every second   ----------        
        var secondHand = (clockTime.sec / 60.0) * Math.PI * 2;
        var secondHandPoints = generateHandCoordinates(screenCenterPoint, secondHand, 90, 20, 2);
        curClip = getBoundingBox( secondHandPoints );
        var bboxWidth = curClip[1][0] - curClip[0][0] + 1;
        var bboxHeight = curClip[1][1] - curClip[0][1] + 1;
        dc.setClip(curClip[0][0], curClip[0][1], bboxWidth, bboxHeight);
		
//-----------   Having established the full clipping area, now clear it to Black   -----
        dc.setColor(Graphics.COLOR_TRANSPARENT, Graphics.COLOR_TRANSPARENT);
		dc.clear();
		
//-----------   Now redraw the entire clipping area   ------------
    	dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_TRANSPARENT);
//-----------   Heart Rate   ----------
     	dc.drawText(width*0.75,height*0.4, Graphics.FONT_TINY, HR.toString(), Graphics.TEXT_JUSTIFY_CENTER);
//-----------   Steps   ------------
		dc.drawText(width*0.25, height*0.4, Graphics.FONT_TINY, Steps.toString(), Graphics.TEXT_JUSTIFY_CENTER);
//-----------   Hour and Minute hands   ----------------
    	dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
    	dc.fillPolygon(hourPolyPoints);
    	dc.fillPolygon(minPolyPoints);
//-----------  The central boss (arbor)   ----------------
	    dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT);
	    dc.fillCircle(width / 2, height / 2, 7);
	    dc.setColor(Graphics.COLOR_BLACK,Graphics.COLOR_TRANSPARENT);
	    dc.drawCircle(width / 2, height / 2, 7);
//-----------   and, finally, the secondhand   ----------------
        dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT);
        dc.fillPolygon(secondHandPoints);
    }

In essence, how can I get an item written to a previous clip continue to be displayed on the watchface canvas?

  • I'd forget the idea of "two canvases" - there's really only one which is the screen itself. Whenever you clear a clip area, then anything on the screen within that area is cleared.

    So if you clear a clip area and want to display (boss + second hand) on top of (hour + minute) on top of (steps + heartrate) then you will need to redraw all of those things in the correct order within that clip area. One optimization is to only redraw them if they overlap that clip area.

    If you want to redraw 2 sections of the screen in the same onPartialUpdate, e.g. secondhand and steps counter, then you will want to create 2 clip areas in that same second - one for each area. And you will want to redraw each area after setting up the clip for it. Although it may be cheaper to create a single combined clip around both areas - and then just redraw the whole of that area once.

    Another optimization you can use is with Graphics.BufferedBitmap - this creates an offscreen image which you can draw multiple things to (which you aren't going to change for a while). And then instead of drawing those things to a clip area every time you clear that clip area, you can instead copy your saved image from the buffered bitmap into the clip area. For instance you could use this for the hour & minute hands & central boss since those things only change once per minute. I'm not sure if you can do this with an alpha channel (since you will want to be able to see your steps & heart rate underneath), since I've never tried that, but I assume it's possible ...

  • Thanks for the clarification ... but the problem I have and why I thought in terms of two canvases is that I set a clip area for each counter on the call that it gets updated in and write the new value into that.  That value is displayed during the second of that call but is then blank until the next time it is called.  Ie the effect, given that I create the Steps clip once every 2 seconds, is that it flashes with a 2 second frequency.  It has all the appearance of the clip not being written back to the watchface.  I assume, here, that dc.clear() sets the area of interest and fills it with the background color and dc.clearClip() just resets the clip area to be the whole watchface again.  So how do I get the clip area written back to the watchface so that what was written to the clip is displayed when that clip is released?

  • It turns out that my best option is to not use clipping at all for the Steps and Heart Rate counters but to just directly update the watchface.  It is running quite happily (so far, after a couple of hours) and an eyeball estimation of 'total time' from the sim diagnostics seems to indicate no greater total time than when I was faffing around (unsuccessfully) with multiple clipping areas.  --  Don't change what ain't broke!!

  • When you set a clip area it just restricts the area you can draw within - anything you draw outside the clip area doesn't appear - it is "clipped". This is mainly useful because the smaller the area of screen drawn each partial update the cheaper it is.

    When you draw within a clip area, then it immediately gets drawn to the screen and draws on top of whatever was there before - you don't need to do anything to "write it back to the watchface".

    Yes dc.clear() will fill the clip area with the background color (but this directly writes over what is currently displayed on the watchface!)

    And dc.clearClip() resets it back to the whole screen again.

    Looking at your code above for onPartialUpdate - from your comments above drawBackground(dc) it looks like you are relying on a clip area being set from the previous update? Otherwise you are drawing the whole background over the whole of the screen.

    But if you have it running successfully on your watch, and it's staying within the timer limit for onPartialUpdate, then that's good Slight smile

  • Thanks for the explanation.  Getting it to run, though, is only one part of this.  The other part is to understand how it all fits together (a) as an exercise in itself and (b) for any future projects I want to do. That is why (good!) advice is always so helpful - thanks.

  • Nope, it transpires that I was wrong :(  I did not have dc.clearClip() at the start of onPartialUpdate() and so the clip (apparently) wasn't being cleared between calls.  Now that I've added that I CANNOT get the counters to 'stick' other than whilst they are inside of the clipping area.  I'm guessing that dc.clear() is the culprit

    Here is my current code:-

    function onPartialUpdate( dc ) {
            // If we're not doing a full screen refresh we need to re-draw the background
            // before drawing the updated second hand position. Note this will only re-draw
            // the background in the area specified by the previously computed clipping region.
    
    //  Note - HR, Steps, HRDims, StepsDims, hourPolyPoints, minPolyPoints and curClip are all global variables
    
            if(!fullScreenRefresh) {
                drawBackground(dc);
            }
    		dc.clearClip();
    		
    //----------  Initialise all variables   -----------------
    		var hasChanged = false;
    
            var width = dc.getWidth();
            var height = dc.getHeight();
    
            var clockTime = System.getClockTime();
            var newHR = Activity.getActivityInfo().currentHeartRate;   
    	    var newSteps = ActivityMonitor.getInfo().steps;
    
    //-------   Calculate the clipping area   ----------
    //-------   Firstly for the secondhand   -----------        
            var secondHand = (clockTime.sec / 60.0) * Math.PI * 2;
            var secondHandPoints = generateHandCoordinates(screenCenterPoint, secondHand, 90, 20, 2);
            var clipPoints = new [secondHandPoints.size()];
            for (var i = 0; i < secondHandPoints.size(); ++i) {
    	    	clipPoints[i] = secondHandPoints[i];
    	    }
    //-------   Next see if we need to include the Heart Rate   ------------
    		if (null != newHR && newHR != HR && clockTime.sec % 10 == 0) {
    	        clipPoints.add([(width*0.75)-15,height*0.4]);
    	        clipPoints.add([(width*0.75)+15,(height*0.4)+10]);
    			HR = newHR;
    			hasChanged = true;
    		}
    //------   Then see if we need to add Steps   ----------------
        	if (newSteps > Steps && clockTime.sec % 4 == 0) {		
    	        clipPoints.add([(width*0.25)-30,height*0.4]);
    	        clipPoints.add([(width*0.25)+30,(height*0.4)+10]);
    	        Steps = newSteps;
    	        hasChanged = true;
    	    }
    //-------   Now construct the Clipping area
            curClip = getBoundingBox( clipPoints );
            var bboxWidth = curClip[1][0] - curClip[0][0] + 1;
            var bboxHeight = curClip[1][1] - curClip[0][1] + 1;
            dc.setClip(curClip[0][0], curClip[0][1], bboxWidth, bboxHeight);
    //-------   ... and clear it to BLACK   --------------
            dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
    /*------------------------------------------------------------------------
            dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_LT_GRAY);
    -------  Change to LT_GRAY  -----  to see the current clipping area  -----
    --------------------------------------------------------------------------*/
            dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
    		dc.clear();
    //-------   Now write the current (maybe just updated) counters   --------
        	dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_TRANSPARENT);
         	dc.drawText(width*0.75,height*0.4, Graphics.FONT_TINY, HR.toString(), Graphics.TEXT_JUSTIFY_CENTER);
    		dc.drawText(width*0.25, height*0.4, Graphics.FONT_TINY, Steps.toString(), Graphics.TEXT_JUSTIFY_CENTER);
            
    //-------   If we've changed either of the counters redraw the hour & minute hands   ------
    //-------   So that they sit over the counters   ----------
    		if (hasChanged) {
    //----------   Draw the hour/minute hands and the central boss   -----------
    	    	dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
    	    	dc.fillPolygon(hourPolyPoints);
    	    	dc.fillPolygon(minPolyPoints);
    //-----------  The central boss (arbor)   ----------------
    		    dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT);
    		    dc.fillCircle(width / 2, height / 2, 7);
    		    dc.setColor(Graphics.COLOR_BLACK,Graphics.COLOR_TRANSPARENT);
    		    dc.drawCircle(width / 2, height / 2, 7);
    	    }
            dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT);
            dc.fillPolygon(secondHandPoints);
        }

    HELP! - what am I doing wrong?  As it stands this is unusable (and is driving me demented!).

  • You may want to have a dc.clearClip at the beginning of onUpdate() so there are no clip regions when you do the full screen.

    I've not really looked at the code, but with multiples, only call dc.setClip() for things you are actually going to update.  If the HR hasn't changed and you wont be updating it, don't set a clip region for it

  • Thanks for your response (yet again!)  I already have dc.clearClip() at the start of onUpdate() (as well as in onPartialUpdate()).

    I appreciate that you've not had a look at the code yet but I've noticed that the whole watchface is saved as a buffered bitmap on each call to onUpdate().  This is then written back at the start of each onPartialUpdate() call.  I'm wondering whether I need to write each of the counters to the buffered bitmap as they change.  I know that I need to try this for myself but I have to work out what to do first!

    A related question I have is that I create the total clipping area by first calculating the coordinates of the secondhand and then adding the coordinates of each of the counters to be updated.  Initially I just copied the array containing the secondhand coords (I thought) to a new array and then using array.add to add the new coordinates.  However this copy seems to just copy a reference to the original array as when I added the additional coords they became a part of the original secondhand coords.  Is that the expected outcome?  (Sorry that's a bit convoluted but I hope you understand my meaning).  Ie does copying an array just copy a reference to that array?

  • Normally the buffered bitmap is used to store the parts of the graphics which only change once a minute. And then in each call to partial update you can use the buffered bitmap for redrawing (instead of having to individually redraw all those graphics again). So in your code above where you redraw the "hour/minutes hand and the central boss", instead of that you could just draw the buffered bitmap to the clip area (and it may be cheaper).

    If you aren't using the buffered bitmap to redraw anything in the partial update (I can't see your code using it at all), then there is no point in using a buffered bitmap. I suspect you've started working from one of the sample projects, and there's still bits of code from that which it would be good to go over and make sure you know what they are doing.

    When you talk about copying the array - it depends what you actually did to copy it Slight smile Did you call a function or did you just assign the array to a new variable (which won't duplicate the memory). E.g. you could use array.slice if you really wanted to generate a copy: https://developer.garmin.com/connect-iq/api-docs/Toybox/Lang/Array.html#slice-instance_function

  • My apologies, this is, really, a follow on question from another in which I had mentioned what I was using.  I'm using the Analog sample project from the 3.1.9 SDK as my starting point and coding for a Vivoactive 4s.

    Analog.prg does, in fact, use a buffered bitmap for the basic background which is called (dc.drawBackground()) at the start of onPartialUpdate().

    I've done a whole lot more experimenting since my last comments including using bitmaps for the Steps and Heart Rate counters, so far, without success.

    As far as I can make out the problem lies with setClip().  Although dc.clearClip() is called by onUpdate() (once a minute) I cannot call it in onPartialUpdate() because that causes the average power allowance to be exceeded (by a large margin).

    Again, as far as I can make out, calls to setClip() are additive (I know that is true) but each call within onPartialUpdate() clears the whole clipping area declared to that point, using either dc.clear() of fillRectangle.  Ie the clearing to background is also additive.  Because at times I want three separate (albeit amalgamated) clipping areas these are each cleared to background in the sequence that they are called.  This doesn't matter for the second hand because it is updated every second, anyway but it does matter for the other two which are updated either every other second (the Steps counter) or every 10 seconds (the Heart Rate counter).  The effect is that if I call Steps followed by Heart Rate then Steps are cleared to backgound after being updated or vice versa if Heart Rate is called first.  This is the same whether or not I use bitmaps.  I've tried writing Steps again when Heart Rate is called but, as far as I remember, I then get two (different) versions of Steps overlaying each other.

    I've tried setting the Heart Rate clipping area from within the Steps processing immediately after using setClip() for the Steps region but that failed (I think because the average power was exceeded).  I've also tried calling all three setClips (when required) before doing any processing but that also fails, as far as I recall because at least one of the counters got blanked out periodically.  I've also tried creating a single clipping region incorporating the total area to be used.  That also didn't work, for the same reason if I recall.

    At the moment I've had to revert to updating the Heart Rate once a minute in onUpdate() and updating Steps and the second hand within onPartialUpdate().  This is working fine but not, really, what I want.

    If anyone has succeeded in using three or more clipping areas of which at least two are updated less frequently than once a second (and stayed within the power allowance) I would be very interested to know how they have done it.

    Thanks, though, for your suggestions and time.