## 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(); });