From: hextheplanet Date: Mon, 28 Jul 2025 01:40:01 +0000 (-0700) Subject: Selectable colors with bitmap replacement, and a new bus! X-Git-Url: http://git.hexthepla.net/?a=commitdiff_plain;h=03aa16c4fc76a513ab34c7f15b715a978e1f4433;p=retro-metro Selectable colors with bitmap replacement, and a new bus! * 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 --- diff --git a/package.json b/package.json index 9df7996..a1655c0 100644 --- a/package.json +++ b/package.json @@ -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", @@ -17,25 +29,36 @@ "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 index 0000000..07ecfa6 Binary files /dev/null and b/resources/images/xdepixel.png differ diff --git a/src/c/main.c b/src/c/main.c index 0f84b0e..7aa9c55 100644 --- a/src/c/main.c +++ b/src/c/main.c @@ -1,7 +1,22 @@ #include "gbitmap_color_palette_manipulator.h" #include +// 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 index 0000000..8ee35d7 --- /dev/null +++ b/src/pkjs/config.js @@ -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 index 0000000..3128735 --- /dev/null +++ b/src/pkjs/index.js @@ -0,0 +1,3 @@ +var Clay = require('pebble-clay'); +var clayConfig = require('./config.js'); +var clay = new Clay(clayConfig);