Selectable colors with bitmap replacement, and a new bus!
authorhextheplanet <hex@hexthepla.net>
Mon, 28 Jul 2025 01:40:01 +0000 (18:40 -0700)
committerhextheplanet <hex@hexthepla.net>
Mon, 28 Jul 2025 01:40:01 +0000 (18:40 -0700)
 * Added user settings with Clay (https://github.com/pebble/clay)
 * The default background is now b/w, with color replacement on runtime
   based on what was selected in settings. yay user-customization (and
   https://github.com/rebootsramblings/GBitmap-Colour-Palette-Manipulator)
 * Disabled the text portion (for now) since it gets in the way of clock
   face
 * Added a little pixelated bus to go by every so often, user
   configurable frequencies

package.json
resources/images/xdepixel.png [new file with mode: 0644]
src/c/main.c
src/pkjs/config.js [new file with mode: 0644]
src/pkjs/index.js [new file with mode: 0644]

index 9df79968d93fa5febbd82383f792a64421a6df65..a1655c02c71cbb83b1ed0d8e97c1e1997931affc 100644 (file)
@@ -1,15 +1,27 @@
 {
   "name": "retro-metro",
-  "author": "MakeAwesomeHappen",
+  "author": "hextheplanet",
   "version": "1.0.0",
-  "keywords": ["pebble-app"],
+  "keywords": [
+    "pebble-app"
+  ],
   "private": true,
-  "dependencies": {},
+  "dependencies": {
+    "pebble-clay": "^1.0.4"
+  },
+  "messageKeys": [
+    "BackgroundColor",
+    "ForegroundColor",
+    "SecondTick"
+  ],
   "pebble": {
     "displayName": "retro-metro",
     "uuid": "fb8764a5-4a96-4626-9c89-3ca4bdac24b1",
     "sdkVersion": "3",
     "enableMultiJS": true,
+    "capabilities": [
+      "configurable"
+    ],
     "targetPlatforms": [
       "aplite",
       "basalt",
       "diorite"
     ],
     "watchapp": {
-      "watchface": true 
+      "watchface": true
     },
     "messageKeys": [
-      "dummy"
+      "BackgroundColor",
+      "ForegroundColor",
+      "ClockColor",
+      "SecondTick",
+      "BusFrequency"
     ],
     "resources": {
       "media": [
-          {
-              "type": "bitmap",
-              "name": "IMAGE_METRO",
-              "file": "images/metrobw.png",
-              "memoryFormat": "Smallest",
-              "spaceOptimization": "memory"
-          },
-          {
-              "type": "font",
-              "name": "FONT_HELVETICA_BLACK_48",
-              "file": "helvetica.ttf"
-          }
+        {
+          "type": "bitmap",
+          "name": "IMAGE_METRO",
+          "file": "images/metrobw.png",
+          "memoryFormat": "Smallest",
+          "spaceOptimization": "memory"
+        },
+        {
+          "type": "bitmap",
+          "name": "IMAGE_XDE",
+          "file": "images/xdepixel.png",
+          "memoryFormat": "Smallest",
+          "spaceOptimization": "memory"
+        },
+        {
+          "type": "font",
+          "name": "FONT_HELVETICA_BLACK_48",
+          "file": "helvetica.ttf"
+        }
       ]
     }
   }
diff --git a/resources/images/xdepixel.png b/resources/images/xdepixel.png
new file mode 100644 (file)
index 0000000..07ecfa6
Binary files /dev/null and b/resources/images/xdepixel.png differ
index 0f84b0e1bc815ef2fe3e4a7e126c96e4bbde37f5..7aa9c555e3126e4b771bbc62cdba37cce2f7461c 100644 (file)
@@ -1,7 +1,22 @@
 #include "gbitmap_color_palette_manipulator.h"
 #include <pebble.h>
 
+// Persistent storage key
+#define SETTINGS_KEY 1
+
+// Define our settings struct
+typedef struct ClaySettings {
+  GColor BackgroundColor;
+  GColor ForegroundColor;
+  GColor ClockColor;
+  bool SecondTick;
+  int BusFrequency;
+} ClaySettings;
+
+static ClaySettings settings;
+
 static Window *s_main_window;
+static GRect window_bounds;
 static Layer *s_hands_layer;
 static TextLayer *s_time_layer;
 
@@ -9,11 +24,35 @@ static TextLayer *s_time_layer;
 static BitmapLayer *s_background_layer;
 static GBitmap *s_background_bitmap;
 
+// bus layer and bitmap
+static BitmapLayer *s_bus_layer;
+static GBitmap *s_bus_bitmap;
+static GRect bus_bitmap_bounds;
+
 static GPoint center = {
         .x = (int16_t)70,
         .y = (int16_t)68,
     };
 
+
+void draw_centered_ray(int32_t tick_angle, int16_t start, int16_t end, int8_t stroke, Layer *layer, GContext *ctx){
+    graphics_context_set_stroke_color(ctx, settings.ClockColor);
+    // from the the starting point (predetermined angle with starting_tick_length)..
+    GPoint starting_point = {
+        .x = (int16_t)(sin_lookup(tick_angle) * (int32_t)start/TRIG_MAX_RATIO) + center.x,
+        .y = (int16_t)(-cos_lookup(tick_angle) * (int32_t)start/TRIG_MAX_RATIO) + center.y,
+    };
+    // .. to the ending point (same angle, slightly more length)
+    GPoint ending_point = {
+        .x = (int16_t)(sin_lookup(tick_angle) * (int32_t)end/TRIG_MAX_RATIO) + center.x,
+        .y = (int16_t)(-cos_lookup(tick_angle) * (int32_t)end/TRIG_MAX_RATIO) + center.y,
+    }; 
+    // draw it, then wrap around to next
+    graphics_context_set_stroke_width(ctx, stroke);
+    graphics_draw_line(ctx, starting_point, ending_point);
+    
+}
+
 static void draw_ticks(Layer *layer, GContext *ctx){
     const int16_t starting_tick_length = 66;
     const int16_t ending_tick_length = 68;
@@ -22,28 +61,13 @@ static void draw_ticks(Layer *layer, GContext *ctx){
     // for each tick
     for (int i = 0; i < 12; i++){
         int32_t tick_angle = TRIG_MAX_ANGLE * i/12;
-        // from the the starting point (predetermined angle with starting_tick_length)..
-        GPoint starting_point = {
-            .x = (int16_t)(sin_lookup(tick_angle) * (int32_t)starting_tick_length/TRIG_MAX_RATIO) + center.x,
-            .y = (int16_t)(-cos_lookup(tick_angle) * (int32_t)starting_tick_length/TRIG_MAX_RATIO) + center.y,
-        };
-        // .. to the ending point (same angle, slightly more length)
-        GPoint ending_point = {
-            .x = (int16_t)(sin_lookup(tick_angle) * (int32_t)ending_tick_length/TRIG_MAX_RATIO) + center.x,
-            .y = (int16_t)(-cos_lookup(tick_angle) * (int32_t)ending_tick_length/TRIG_MAX_RATIO) + center.y,
-        };
-
-        // draw it, then wrap around to next
-        graphics_context_set_stroke_width(ctx, 2);
-        graphics_draw_line(ctx, starting_point, ending_point);
+        draw_centered_ray(tick_angle, starting_tick_length, ending_tick_length, 2, layer, ctx);
     }
 
-
 }
 
 
 static void hands_update_proc(Layer *layer, GContext *ctx) {
-  
     const int16_t second_hand_length = 64; 
     const int16_t minute_hand_length = 64;
     const int16_t hour_hand_length = 32;
@@ -54,66 +78,66 @@ static void hands_update_proc(Layer *layer, GContext *ctx) {
   
     time_t now = time(NULL);
     struct tm *t = localtime(&now);
-    int32_t second_angle = TRIG_MAX_ANGLE * t->tm_sec / 60;
-    GPoint second_hand = {
-        .x = (int16_t)(sin_lookup(second_angle) * (int32_t)second_hand_length / TRIG_MAX_RATIO) + center.x,
-        .y = (int16_t)(-cos_lookup(second_angle) * (int32_t)second_hand_length / TRIG_MAX_RATIO) + center.y,
-    };
-  
-    // second hand
-    graphics_context_set_stroke_color(ctx, GColorWhite);
-    graphics_context_set_stroke_width(ctx, 1);
-    graphics_draw_line(ctx, second_hand, center);
+    if (settings.SecondTick){
+        int32_t second_angle = TRIG_MAX_ANGLE * t->tm_sec / 60;
+        draw_centered_ray(second_angle, 0, second_hand_length, 1, layer, ctx);
+    }
   
     // minute hand drawing 
     int32_t minute_angle = TRIG_MAX_ANGLE * t->tm_min/60;
-    GPoint minute_hand = {
-        .x = (int16_t)(sin_lookup(minute_angle) * (int32_t)minute_hand_length / TRIG_MAX_RATIO) + center.x,
-        .y = (int16_t)(-cos_lookup(minute_angle) * (int32_t)minute_hand_length / TRIG_MAX_RATIO) + center.y,
-    };        
-    graphics_context_set_stroke_width(ctx, minute_stroke_width);
-
-    graphics_draw_line(ctx, minute_hand, center);
+    draw_centered_ray(minute_angle, 0, minute_hand_length, minute_stroke_width, layer, ctx);
 
     // hour hand drawing 
     int32_t hour_angle = TRIG_MAX_ANGLE * (((t->tm_hour % 12 ) * 6) + (t->tm_min/10)) / (12*6);
-    GPoint hour_hand = {
-        .x = (int16_t)(sin_lookup(hour_angle) * (int32_t)hour_hand_length / TRIG_MAX_RATIO) + center.x,
-        .y = (int16_t)(-cos_lookup(hour_angle) * (int32_t)hour_hand_length / TRIG_MAX_RATIO) + center.y,
-    };
-    graphics_context_set_stroke_width(ctx, hour_stroke_width);
+    draw_centered_ray(hour_angle, 0, hour_hand_length, hour_stroke_width, layer, ctx);
+}
 
-    graphics_draw_line(ctx, hour_hand, center);    
-  
-    // dot in the middle
-    //graphics_context_set_fill_color(ctx, GColorBlack);
-    //graphics_fill_rect(ctx, GRect(bounds.size.w / 2 - 1, bounds.size.h / 2 - 1, 3, 3), 0, GCornerNone);
+
+static void refresh_bitmap(){
+    if (s_background_bitmap){
+        // Destroy GBitmap
+        gbitmap_destroy(s_background_bitmap);
+    }
+    
+    // Create GBitmap
+    s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_METRO);
+    // color replace, only if color pebble
+#ifdef PBL_COLOR     
+    // foreground, black -> user-defined color
+    // set it to clear first to "save" it (otherwise you might set to e.g. white and replace it ALL with the next move)
+    replace_gbitmap_color(GColorBlack, GColorPastelYellow, s_background_bitmap, NULL);
+    // background, white -> user-defined color
+    replace_gbitmap_color(GColorWhite, settings.BackgroundColor, s_background_bitmap, NULL);
+    // now do the final clear -> user defined color
+    replace_gbitmap_color(GColorPastelYellow, settings.ForegroundColor, s_background_bitmap, NULL);
+#endif 
+    // Set the bitmap onto the layer and add to the window
+    bitmap_layer_set_bitmap(s_background_layer, s_background_bitmap);    
 }
 
 
+static void create_bus_layer(){
+    // Load the image data
+    s_bus_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_XDE);
+    // Get the bounds of the image
+    bus_bitmap_bounds = gbitmap_get_bounds(s_bus_bitmap);
+    bitmap_layer_set_compositing_mode(s_bus_layer, GCompOpSet);
+
+    bitmap_layer_set_bitmap(s_bus_layer, s_bus_bitmap);
+}
 
 
 static void main_window_load(Window *window) {
 
     // Get information about the Window
     Layer *window_layer = window_get_root_layer(window);
-    GRect bounds = layer_get_bounds(window_layer);  
-
-    // Create GBitmap
-    s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_METRO);
-    // color replace, only if color pebble
-#ifdef PBL_COLOR    
-    // foreground, black -> user-defined color
-    replace_gbitmap_color(GColorBlack, GColorChromeYellow, s_background_bitmap, NULL);    
-    // background, white -> user-defined color
-    replace_gbitmap_color(GColorWhite, GColorDarkCandyAppleRed, s_background_bitmap, NULL);
-#endif
+    window_bounds = layer_get_bounds(window_layer);  
 
     // Create BitmapLayer to display the GBitmap
-    s_background_layer = bitmap_layer_create(bounds);
+    s_background_layer = bitmap_layer_create(window_bounds);
+    
+    refresh_bitmap();
 
-    // Set the bitmap onto the layer and add to the window
-    bitmap_layer_set_bitmap(s_background_layer, s_background_bitmap);
     layer_add_child(window_layer, bitmap_layer_get_layer(s_background_layer));
 /*
     // Create the TextLayer with specific bounds
@@ -133,9 +157,14 @@ static void main_window_load(Window *window) {
 */
 
 
-    s_hands_layer = layer_create(bounds);
+    s_hands_layer = layer_create(window_bounds);
     layer_set_update_proc(s_hands_layer, hands_update_proc);
     layer_add_child(window_layer, s_hands_layer);    
+
+    s_bus_layer = bitmap_layer_create(window_bounds);
+    create_bus_layer();
+    layer_set_hidden(bitmap_layer_get_layer(s_bus_layer), true);
+    layer_add_child(window_layer, bitmap_layer_get_layer(s_bus_layer));
 }
 
 static void main_window_unload(Window *window) {
@@ -148,6 +177,8 @@ static void main_window_unload(Window *window) {
     text_layer_destroy(s_time_layer);
     // Destroy hands layer
     layer_destroy(s_hands_layer);
+    gbitmap_destroy(s_bus_bitmap);
+    bitmap_layer_destroy(s_bus_layer);
 }
 
 static void update_time() {
@@ -164,15 +195,148 @@ static void update_time() {
   //text_layer_set_text(s_time_layer, s_buffer);
 }
 
-static void handle_second_tick(struct tm *tick_time, TimeUnits units_changed) {
-    layer_mark_dirty(window_get_root_layer(s_main_window));
+static void anim_started_handler(Animation *animation, void *context) {
+  layer_set_hidden(bitmap_layer_get_layer(s_bus_layer), false);
+  APP_LOG(APP_LOG_LEVEL_DEBUG, "Animation started!");
+}
+
+static void anim_stopped_handler(Animation *animation, bool finished, void *context) {
+  layer_set_hidden(bitmap_layer_get_layer(s_bus_layer), true);
+  APP_LOG(APP_LOG_LEVEL_DEBUG, "Animation stopped!");
 }
 
+
+static void start_bus_animation(){
+    // The start and end frames - move the Layer 40 pixels to the right
+    GRect start = GRect(-bus_bitmap_bounds.size.w, bus_bitmap_bounds.size.h/3, bus_bitmap_bounds.size.w, bus_bitmap_bounds.size.h);
+    GRect finish = GRect(window_bounds.size.w, bus_bitmap_bounds.size.h/3, bus_bitmap_bounds.size.w, bus_bitmap_bounds.size.h);
+    // Animate the Layer
+    PropertyAnimation *prop_anim = property_animation_create_layer_frame(bitmap_layer_get_layer(s_bus_layer), &start, &finish);
+    // Get the Animation
+    Animation *anim = property_animation_get_animation(prop_anim);
+
+    // Choose parameters
+    const int delay_ms = 0;
+    const int duration_ms = 5000;
+
+    // Configure the Animation's curve, delay, and duration
+    //animation_set_curve(anim, AnimationCurveEaseOut);
+    animation_set_delay(anim, delay_ms);
+    animation_set_duration(anim, duration_ms);    
+    animation_set_handlers(anim, (AnimationHandlers) {
+      .started = anim_started_handler,
+      .stopped = anim_stopped_handler
+    }, NULL);    
+    // Play the animation
+    animation_schedule(anim);    
+}
+
+static void handle_clock_tick(struct tm *tick_time, TimeUnits units_changed) {
+    //layer_mark_dirty(window_get_root_layer(s_main_window));
+    
+    if (units_changed & MINUTE_UNIT){
+        APP_LOG(APP_LOG_LEVEL_DEBUG, "minute rollover, updating minute stuff");
+        layer_mark_dirty(window_get_root_layer(s_main_window));
+        // we want the bus to show up
+        if (settings.BusFrequency != 0 ){
+            // number of minutes past the hour, will NOT line up evenly if you choose a frequency like e.g. 13, it will advance:
+            //   0, 13, 26, 39, 52 ..(8 mins).. 0, 13 
+            // this makes more conceptual sense to me, but if you want it since start of day or whatever feel free to change
+            //APP_LOG(APP_LOG_LEVEL_DEBUG, "tick_time = %d start_of_today = %d", (int)mktime(tick_time), (int)time_start_of_today());
+            int minutes_past_the_hour = mktime(tick_time)/60 % 60;
+            // minutes since start of day == bus frequency?
+            if (minutes_past_the_hour % settings.BusFrequency == 0 ){
+                start_bus_animation();
+            }
+        }
+    } else if (settings.SecondTick){
+        layer_mark_dirty(window_get_root_layer(s_main_window));
+    }
+}
+
+
 static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
   update_time();
 }
 
+// Initialize the default settings
+static void prv_default_settings() {
+  settings.BackgroundColor = GColorWhite;
+  settings.ForegroundColor = GColorBlack;
+  settings.ClockColor = GColorWhite;
+  settings.SecondTick = false;
+  settings.BusFrequency = 0;
+}
+
+void refresh_procs(){
+    tick_timer_service_unsubscribe();
+    // choose minute or second refresh depending on hands
+    if (settings.SecondTick){
+        tick_timer_service_subscribe(SECOND_UNIT, handle_clock_tick);
+    } else {
+        tick_timer_service_subscribe(MINUTE_UNIT, handle_clock_tick);
+    }
+}
+
+
+// Save the settings to persistent storage
+static void prv_save_settings() {
+  persist_write_data(SETTINGS_KEY, &settings, sizeof(settings));
+}
+
+static void prv_inbox_received_handler(DictionaryIterator *iter, void *context) {
+  APP_LOG(APP_LOG_LEVEL_DEBUG, "received message");
+  // Read color preferences
+  Tuple *bg_color_t = dict_find(iter, MESSAGE_KEY_BackgroundColor);
+  if(bg_color_t) {
+    APP_LOG(APP_LOG_LEVEL_DEBUG, "set background color to %d", (int)bg_color_t->value->int32);
+    settings.BackgroundColor = GColorFromHEX(bg_color_t->value->int32);
+  }
+
+  Tuple *fg_color_t = dict_find(iter, MESSAGE_KEY_ForegroundColor);
+  if(fg_color_t) {
+    APP_LOG(APP_LOG_LEVEL_DEBUG, "set foreground color to %d", (int)fg_color_t->value->int32);    
+    settings.ForegroundColor = GColorFromHEX(fg_color_t->value->int32);
+  }
+
+  Tuple *clock_color_t = dict_find(iter, MESSAGE_KEY_ClockColor);
+  if(clock_color_t) {
+    APP_LOG(APP_LOG_LEVEL_DEBUG, "set clock color to %d", (int)clock_color_t->value->int32);    
+    settings.ClockColor = GColorFromHEX(clock_color_t->value->int32);
+  }
+
+  // Read boolean preferences
+  Tuple *second_tick_t = dict_find(iter, MESSAGE_KEY_SecondTick);
+  if(second_tick_t) {
+    settings.SecondTick = second_tick_t->value->int32 == 1;
+  }
+
+  Tuple *bus_freq_t = dict_find(iter, MESSAGE_KEY_BusFrequency);
+  if(bus_freq_t) {
+    settings.BusFrequency = bus_freq_t->value->int32;
+  }
+  prv_save_settings();
+  refresh_bitmap();
+  refresh_procs();
+}
+
+// Read settings from persistent storage
+static void prv_load_settings() {
+    // Load the default settings
+    prv_default_settings();
+    // Read settings from persistent storage, if they exist
+    persist_read_data(SETTINGS_KEY, &settings, sizeof(settings));    
+}
+
+void prv_init() {
+    prv_load_settings();
+    // Open AppMessage connection
+    app_message_register_inbox_received(prv_inbox_received_handler);
+    app_message_open(128, 128);
+}
+
 static void init() {
+    prv_init();
     // Create main Window element and assign to pointer
     s_main_window = window_create();
   
@@ -184,9 +348,7 @@ static void init() {
   
     // Show the Window on the watch, with animated=true
     window_stack_push(s_main_window, true);
-    // Make sure the time is displayed from the start
-    tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
-    tick_timer_service_subscribe(SECOND_UNIT, handle_second_tick);
+    refresh_procs();
     update_time();
 }
 
diff --git a/src/pkjs/config.js b/src/pkjs/config.js
new file mode 100644 (file)
index 0000000..8ee35d7
--- /dev/null
@@ -0,0 +1,64 @@
+module.exports = [
+  {
+    "type": "heading",
+    "defaultValue": "Retro Metro Configuration"
+  },
+  {
+    "type": "text",
+    "defaultValue": "Select foreground and background colors"
+  },
+  {
+    "type": "section",
+    "items": [
+      {
+        "type": "heading",
+        "defaultValue": "Colors"
+      },
+      {
+        "type": "color",
+        "messageKey": "BackgroundColor",
+        "defaultValue": "0xFFFFFF",
+        "label": "Background Color"
+      },
+      {
+        "type": "color",
+        "messageKey": "ForegroundColor",
+        "defaultValue": "0x000000",
+        "label": "Foreground Color"
+      },
+      {
+        "type": "color",
+        "messageKey": "ClockColor",
+        "defaultValue": "0xFFFFFF",
+        "label": "Clock Color"
+      },
+    ]
+  },
+  {
+    "type": "section",
+    "items": [
+      {
+        "type": "heading",
+        "defaultValue": "Features"
+      },
+      {
+        "type": "slider",
+        "messageKey": "BusFrequency",
+        "label": "Bus headway (minutes, 0=disabled)",
+        "min": 0,
+        "max": 60,
+        "defaultValue": 0
+      },
+      {
+        "type": "toggle",
+        "messageKey": "SecondTick",
+        "label": "Enable Seconds",
+        "defaultValue": false
+      },
+    ]
+  },
+  {
+    "type": "submit",
+    "defaultValue": "Save Settings"
+  }
+];
diff --git a/src/pkjs/index.js b/src/pkjs/index.js
new file mode 100644 (file)
index 0000000..3128735
--- /dev/null
@@ -0,0 +1,3 @@
+var Clay = require('pebble-clay');
+var clayConfig = require('./config.js');
+var clay = new Clay(clayConfig);