Simulating Pebble GPath in Rocky.js

RockyJS is a black magic voodoo from Pebble Dev team. It allows you to run your JavaScript code on the actual smartwatch (unlike PebbleJS that runs on the phone). When RockyJS debuted it ran as a simulation in a browser, but since then it matured and now runs in Pebble emulators and on actual hardware.

RockyJS changed drastically since that web release. It resembles C code less and takes more standardized JavaScript approach. During that transition some features were lost. One of them is Pebble GPath concept – a graphical object that consist of set of coordinates that you can freely move and rotate. In particular missing commands gpath_move_to, gpath_rotate_to and gpath_draw_outline that move, rotate and draw the GPath. When I was porting my first Pebble watchface to Rocky I used those extensively. You can read about that implementation complete with the source code here. But now the commands are gone and I needed a substitution.

GPath is based on array of coordinates and I decided to reuse the same format of data as in previous implementation. For example these coordinates describe shape of hour hand:

var HOUR_HAND_POINTS = [
    [ -5, -50 ], 
    [ -5, 0 ],    
    [ 5, 0 ],      
    [ 5, -50 ],      
    [ 50, -50],      
    [ 50, 50],     
    [ -50, 50 ],      
    [ -50, -50 ]      
];

Note that these are coordinates relative to position (0, 0) and before we begin our rotation and drawing we need to move the shape to the center of screen. Hence comes gpath_move_by function:

function gpathMoveBy(gpath, cx, cy) {
  for (var i=0; i<gpath.length; i++) {
    gpath[i][0] += cx;
    gpath[i][1] += cy;
  }  
}

It’s pretty straightforward. gpath is the array of coordinates and they simple get shifted by passed x and y parameters.

Now that we moved out gpath it is time to rotate it. The function below is 99% based on this answer from GameDev StackExchange (for a change it’s not StackOverflow):

function gpathRotate(gpath, cx, cy, angle) {
  var gpathRotated = [];
  
  for (var i=0; i<gpath.length; i++) {
 
    // translate point to origin
    var tempX =  gpath[i][0] - cx;
    var tempY =  gpath[i][1] - cy;
    
    // now apply rotation
    var rotatedX = tempX*Math.cos(angle) - tempY*Math.sin(angle);
    var rotatedY = tempX*Math.sin(angle) + tempY*Math.cos(angle);
    
    // translate back
    gpathRotated.push([rotatedX + cx, rotatedY + cy]);
  
    
  }
  
  return gpathRotated;
  
}

Again it accepts gpath array as a paramter. Also are passed coordinates of the rotation center and angle of rotation. Function returns array of rotated coordinates.

And finally we need to actually draw the gpath. RokyJS adapts standardized Canvas approach to drawing so we can use lineTo() and moveTo() methods of 2D graphics context:

function gpathDrawOutline(ctx, gpath, strokeColor, strokeWidth) {
  
  // Configure color and width
  ctx.lineWidth = strokeWidth;
  ctx.strokeStyle = strokeColor;
  
  // Begin drawing
  ctx.beginPath();

  // Move to the initial point
  ctx.moveTo(gpath[0][0], gpath[0][1]);
  
  //drawing gpathes
  for (var i=1; i<gpath.length; i++) {
     ctx.lineTo(gpath[i][0], gpath[i][1]);
  }
  
  // final line to start point
  ctx.lineTo(gpath[0][0], gpath[0][1]);
  
  // Stroke the line (output to display)
  ctx.stroke();
  
}

This function accepts graphics context object, gpath array as well as color and width of the line to be drawn. Then, looping thru coordinates and using standard Canvas methods it draws the path.

With these functions defined all we need to do is glue it all together with Rocky’s timing and drawing events. Here’s complete source of “Meyer Objects” JavaScript code. You can copy it into your project’s index.js file, compile and run.

var MINUTE_HAND_POINTS = [
    [ -3, 0 ],  
    [ 3, 0 ],   
    [ 3, -50 ],    
    [ 50, -50],    
    [ 50, 50],    
    [ -50, 50 ],        
    [ -50, -50],      
    [ -3, -50 ]      
  ];

var HOUR_HAND_POINTS = [
    [ -5, -50 ], 
    [ -5, 0 ],    
    [ 5, 0 ],      
    [ 5, -50 ],      
    [ 50, -50],      
    [ 50, 50],     
    [ -50, 50 ],      
    [ -50, -50 ]      
];
       
var SECOND_HAND_POINTS = [
    [ 0, -50 ],
    [ 0, 0 ],    
    [ 0, -50 ],      
    [ 50, -50],      
    [ 50, 50],      
    [ -50, 50 ],      
    [ -50, -50 ]      
];      

//moves all points of gpath array by given offset
function gpathMoveBy(gpath, cx, cy) {
  for (var i=0; i<gpath.length; i++) {
    gpath[i][0] += cx;
    gpath[i][1] += cy;
  }  
}

// rotates gpath against point (cx, cy) at angle
function gpathRotate(gpath, cx, cy, angle) {
  var gpathRotated = [];
  
  for (var i=0; i<gpath.length; i++) {
    
    // cx, cy - center of square coordinates
    // x, y - coordinates of a corner point of the square
    // theta is the angle of rotation
    
    // translate point to origin
    var tempX =  gpath[i][0] - cx;
    var tempY =  gpath[i][1] - cy;
    
    // now apply rotation
    var rotatedX = tempX*Math.cos(angle) - tempY*Math.sin(angle);
    var rotatedY = tempX*Math.sin(angle) + tempY*Math.cos(angle);
    
    // translate back
    gpathRotated.push([rotatedX + cx, rotatedY + cy]);
  
    
  }
  
  return gpathRotated;
  
}

function gpathDrawOutline(ctx, gpath, strokeColor, strokeWidth) {
  
  // Configure color and width
  ctx.lineWidth = strokeWidth;
  ctx.strokeStyle = strokeColor;
  
  // Begin drawing
  ctx.beginPath();

  // Move to the initial point
  ctx.moveTo(gpath[0][0], gpath[0][1]);
  
  //drawing gpathes
  for (var i=1; i<gpath.length; i++) {
     ctx.lineTo(gpath[i][0], gpath[i][1]);
  }
  
  // final line to start point
  ctx.lineTo(gpath[0][0], gpath[0][1]);
  
  // Stroke the line (output to display)
  ctx.stroke();
  
} 


var rocky = require('rocky');
var gpathMoved = false;

function fractionToRadian(fraction) {
  return fraction * 2 * Math.PI;
}



rocky.on('draw', function(event) {
  var ctx = event.context;
  var d = new Date();
  var path;

  // Clear the screen
  ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);

  // Determine the width and height of the display
  var w = ctx.canvas.unobstructedWidth;
  var h = ctx.canvas.unobstructedHeight;

  // Determine the center point of the display
  var cx = w / 2;
  var cy = h / 2;
  
  // if we haven't already centered gpathes - do it
  if (!gpathMoved) {
      gpathMoveBy(MINUTE_HAND_POINTS, cx, cy);
      gpathMoveBy(HOUR_HAND_POINTS, cx, cy);
      gpathMoveBy(SECOND_HAND_POINTS, cx, cy);
      gpathMoved = true;
  }

  // Calculate the second hand angle
  var secondFraction = (d.getSeconds()) / 60;
  var secondAngle = fractionToRadian(secondFraction);
  
  // rotate seconds path
  path = gpathRotate(SECOND_HAND_POINTS, cx, cy, secondAngle);
  // drawing seconds path
  gpathDrawOutline(ctx, path, 'white', 1);

  // Calculate the minute hand angle
  var minuteFraction = (d.getMinutes()) / 60;
  var minuteAngle = fractionToRadian(minuteFraction);
  
  // rotate minutes path
  path = gpathRotate(MINUTE_HAND_POINTS, cx, cy, minuteAngle);
  // drawing minutes path
  gpathDrawOutline(ctx, path, 'yellow', 2);

  
  // Calculate the hour hand angle
  var hourFraction = (d.getHours() % 12 + minuteFraction) / 12;
  var hourAngle = fractionToRadian(hourFraction);

  // rotate hours path
  path = gpathRotate(MINUTE_HAND_POINTS, cx, cy, hourAngle);
  // drawing hours path
  gpathDrawOutline(ctx, path, 'green', 2);

  //dot in the middle
  ctx.fillStyle = 'red';
  ctx.rockyFillRadial(cx, cy, 0.1, 8, 0, 2 * Math.PI);
  ctx.fillStyle = 'darkcandyapplered';
  ctx.rockyFillRadial(cx, cy, 0.1, 3, 0, 2 * Math.PI);


});

rocky.on('secondchange', function(event) {
  // Request the screen to be redrawn on next pass
  rocky.requestDraw();
});

Leave a Reply

Your email address will not be published. Required fields are marked *