First things first. DISCLAMER: Everything described here is a hack upon a crude hack and most likely, barring a divine intervention, won’t work in final product. And I apologize in advance to Pebble dev team if my attempts at “hacking” seem silly. Now to business. Pebble SDK offers very cool framebuffer API that allows developers to address display memory of the watch directly. This makes possible creation of many cool special effects (matter of fact EffectLayer library uses framebuffer extensively).
Rocky.js is JavaScript incarnation of Pebble SDK and it made me wonder whether it offers framebuffer access. Turned out it is hidden, but it’s there. At least at the latest commit at the time of this article it is. If you take a look at source file html-bindings.js you will see that binding function looks something like this:
Rocky.bindCanvas = function(canvas, options) { //... var framebufferPixels = new Uint8Array(module.HEAPU8.buffer, framebufferPixelPTR, canvasW * canvasH); //... var binding = { //... } //... return binding; };
Framebuffer is defined as private array, so, to expose it to outside world I simply assign it as a property of the returned object:
//... binding.framebuffer = framebufferPixels; return binding; };
Now that I have access to framebuffer I decided to recreate Fireplace Pebble watchface. It uses EffectLayer library or Mask effect in particular. It works like this:
- A bitmap with image is created and array of raw data is created from it via gbitmap_get_data() method
- Screen framebuffer (of black screen with white text) is captured as a bitmap and an array of raw data is created as well
- We then loop thru both arrays, checking: If screen array has a white point – it’s replaced with the point from bitmap array, otherwise original color remains
This creates the effect of transparent text thru which bitmap image is showing. And if you use sequence of bitmaps as animation – animation is showing thru text.
I wanted to reproduce this effect in in Rocky.js but the problem is – it doesn’t have gbitmap_get_data()
method (or at least I didn’t find an equivalent). So I decided to cheat. I already knew I could read my main canvas element (called “pebble”) as a framebuffer – so I will add another canvas, this one will be invisible, called “util” and serve one purpose only: convert bitmap into raw data: I’d write bitmap into it and read raw data as a framebuffer. It’s clumsy, I know, but I couldn’t think of anything better.
So now I have this HTML:
<canvas id="util" class="rocky" width="144" height="168" style="display:none !important" ></canvas> <canvas id="pebble" class="rocky" width="144" height="168"></canvas>
and this code that initializes “util” canvas:
var util = Rocky.bindCanvas(document.getElementById("util")); util.update_proc = function (ctx, bounds) { util.graphics_draw_bitmap_in_rect(ctx, frames[frame_no], [0, 0, BITMAP_WIDTH, BITMAP_HEIGHT]); rocky.mark_dirty(); }
This update proc draws bitmap to “util” canvas (from current element of array of bitmaps created elsewhere) and marks main canvas as dirty so it can perform real visible drawing.
Now main canvas update proc looks like this:
var rocky = Rocky.bindCanvas(document.getElementById("pebble")); rocky.update_proc = function (ctx, bounds) { //time var date = new Date(); var tm_hour = date.getHours(); var tm_min = date.getMinutes(); //black background rocky.graphics_context_set_fill_color(ctx, rocky.GColorBlack); rocky.graphics_fill_rect(ctx, [0, 0, bounds.w, bounds.h]); //white text rocky.graphics_context_set_text_color(ctx, rocky.GColorWhite); rocky.graphics_draw_text(ctx, (tm_hour < 10 ? '0' : '') + tm_hour.toString(), font, [0, 20, 144, 80], rocky.GTextOverflowModeWordWrap, rocky.GTextAlignmentCenter); rocky.graphics_draw_text(ctx, (tm_min < 10 ? '0' : '') + tm_min.toString(), font, [0, 90, 144, 160], rocky.GTextOverflowModeWordWrap, rocky.GTextAlignmentCenter); //flames for (i = 0; i < 144 * 168; i++) { rocky.framebuffer[i] &= util.framebuffer[i]; } }
Here we first get current time (Lines 06-08), draw black background (Lines 11-12) and white text with time (Lines 15-17). Fun begins on Lines 20-22: here we loop thru all the pixels, doing binary and
between main framebuffer and “util” framebuffer. Where main framebuffer had black point – it remains black, but where it was white – that point takes color of “util” framebuffer point, essentially drawing our bitmap only inside of text.
Now to tie everything together:
// loading animation frames var frames = []; for (i = 1; i <= NO_OF_FRAMES; i++) { frames.push( util.gbitmap_create("http://image_repository/frame_" + i + '.png') ); } //loading font var font = rocky.fonts_load_custom_font({ height: 50, url: "http://font_repository/very_cool_font.ttf"}); //current frame no var frame_no = 0; setInterval(function () { util.mark_dirty(); frame_no++; if (frame_no == OPT.NO_OF_FRAMES) frame_no = 0; }, DELAY_TIME);
Here we first load our images into array of bitmaps (and loading custom font if you’re feeling fancy), setting initial frame number and kicking of the timer. On every iteration it marks “util” canvas as dirty – which will draw bitmap from current array element, then in turn mark main canvas as dirty – which wild draw time text and perform framebuffer magic to make it seem transparent. We then increment frame counter and repeat the whole thing again. Phew.
Yeah I couldn’t believe it would work either, but you can see live result at the beginning of this article.
UPDATE 2016-02-15: Width the advent of Effects.JS library of special effects for Rocky.js the above exercise becomes much easier. No need to create secondary canvas element, no need for double update procedure. Just reference the library, create and add effect on page load and render it in rocky’s update proc. Here’s the complete source of the “Fireplace” watchface in JavaScript when using Effects.JS library:
// options var OPT = { IMG_URL: "/resources/images/", FONT_URL: "/resources/fonts/", NO_OF_FRAMES: 25, NEXT_DELAY: 70, CANVAS_W: 144, CANVAS_H: 168 } var rocky = Rocky.bindCanvas(document.getElementById("pebble")); rocky.export_global_c_symbols(); rocky.update_proc = function (ctx, bounds) { //time var date = new Date; var tm_hour = date.getHours(); var tm_min = date.getMinutes(); //background graphics_context_set_fill_color(ctx, GColorBlack); graphics_fill_rect(ctx, [0, 0, OPT.CANVAS_W, OPT.CANVAS_H]); // text graphics_context_set_text_color(ctx, GColorWhite); graphics_draw_text(ctx, (tm_hour < 10 ? '0' : '') + tm_hour.toString(), font, [0, 20, 144, 80], GTextOverflowModeWordWrap, GTextAlignmentCenter); graphics_draw_text(ctx, (tm_min < 10 ? '0' : '') + tm_min.toString(), font, [0, 90, 144, 160], GTextOverflowModeWordWrap, GTextAlignmentCenter); //rendering effectes effectHub.renderEffects(ctx); }; //defining param for mask effect var eMask = { text: null, bitmap_mask: null, mask_colors: [GColorWhite], background_color: null, bitmap_background_info: null } // loading bitmap infos of animation frames var bitmap_infos = new Array(OPT.NO_OF_FRAMES); var img_url; for (i = 1; i <= OPT.NO_OF_FRAMES; i++) { img_url = location.href.substring(0, location.href.lastIndexOf("/")) + OPT.IMG_URL + 'frame-0' + (i < 10 ? '0' : '') + i + '.png'; Effects.gbitmap_get_data(img_url, function (bitmap_info, arr_index) { bitmap_infos[arr_index] = bitmap_info; }, i - 1); } //loading font var font_url = location.href.substring(0, location.href.lastIndexOf("/")) + OPT.FONT_URL + 'Futura_Extra_Bold.ttf; var font = fonts_load_custom_font({ height: 50, url: font_url }); //initializing effects var effectHub = Effects.getEffectHub(rocky, OPT.CANVAS_W, OPT.CANVAS_H); effectHub.addEffect(Effects.EFFECT_MASK, { x: 0, y: 0, w: OPT.CANVAS_W, h: OPT.CANVAS_H }, eMask); //current frame no var frame_no = 0; setInterval(function () { eMask.bitmap_background_info = bitmap_infos[frame_no]; rocky.mark_dirty(); frame_no++; if (frame_no == OPT.NO_OF_FRAMES) frame_no = 0; }, OPT.NEXT_DELAY);
Effect.JS provides multitude of effects, here we’re using “Mask” effect that allows you to show background bitmap thru transparent mask. For more info on the Mask and other effects follow the link to the library description. Meanwhile here’s brief description of the above code:
Lines 34-40: on initial page load we create mask parameter for mask effect. In this case the only useful property we set directly is array of mask colors – colors that will become transparent, here we only specify white.
Lines 44-51: create an array of images we use for animations. We need raw binary data of the images, but since current implementation of Rocky doesn’t have gbitmap_get_data()
function like original C SDK does – we use library function Effects.gbitmap_get_data()
to load image info by passing image URL and callback function to catch image info. In my case images are located in “http://mysite/mypage/resources/images/” folder and have names from “frame-001.png” to “frame-025.png”, hence the loop and format of “img_url”. Note that we pass array index into the function; because callback functions could return in any order – this assures that frames will be assigned in correct order.
Lines 54-55: load custom font. It will be used to display time.
Line 58: creates effect hub – main object that creates and manipulates effects.
Line 59: adds Mask effect to the hub specifying area to be covered by the effect and our mask parameter we created before.
Lines 62-69: set initial frame number and kick off timer that will assign current animation frame to mask effect, force canvas to update and increment frame number, and at defined interval (70ms in this case).
We’re done with the setup, now let’s take a look at the rocky update procedure that actually draws the screen – and that’s the easiest part.
Lines 16-18: gets current hour and minute.
Lines 21-22: draws black rectangle as a background
Lines 25-27: draws time (hour and minute) in white color (remember white will become transparent)
Line 30: that single line of code executes all added special effects. In this case a single Mask effect.
Every time update procedure runs (set by setTimeout
timer) time is written in white over black, and then mask effect is rendered to show current frame thru time text.
Do you know when i will be able to run javascript watchapps directly in Pebble without connected mobile?
@DIEGO That is definitely a final Pebble goal for this project. And knowing Pebble – they’re working on it furiously. No definite date yet though.