/* * Copyright 2021 Tyler Thrailkill (@snowe/@snowe2010) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ocean_dream.h" #include "quantum.h" #include "print.h" // Calculated Parameters #define TWINKLE_PROBABILITY_MODULATOR 100 / TWINKLE_PROBABILITY // CALCULATED: Don't Touch #define TOTAL_STARS STARS_PER_LINE *NUMBER_OF_STAR_LINES // CALCULATED: Don't Touch #define OCEAN_ANIMATION_MODULATOR NUMBER_OF_FRAMES / OCEAN_ANIMATION_SPEED // CALCULATED: Don't Touch #define SHOOTING_STAR_ANIMATION_MODULATOR NUMBER_OF_FRAMES / SHOOTING_STAR_ANIMATION_SPEED // CALCULATED: Don't Touch #define STAR_ANIMATION_MODULATOR NUMBER_OF_FRAMES / STAR_ANIMATION_SPEED // CALCULATED: Don't Touch uint8_t animation_counter = 0; // global animation counter. bool is_calm = false; uint32_t starry_night_anim_timer = 0; uint32_t starry_night_anim_sleep = 0; static int current_wpm = 0; static uint8_t increment_counter(uint8_t counter, uint8_t max) { counter++; if (counter >= max) { return 0; } else { return counter; } } #ifdef ENABLE_WAVE static uint8_t decrement_counter(uint8_t counter, uint8_t max) { counter--; if (counter < 0 || counter > max) { return max; } else { return counter; } } #endif #ifdef ENABLE_MOON // region # ifndef STATIC_MOON uint8_t moon_animation_frame = 0; // keeps track of current moon frame uint16_t moon_animation_counter = 0; // counts how many frames to wait before animating moon to next frame # endif # ifdef STATIC_MOON static const char PROGMEM moon[6] = { 0x18, 0x7E, 0xFF, 0xC3, 0x81, 0x81, }; # endif # ifndef STATIC_MOON static const char PROGMEM moon_animation[14][8] = { // clang-format off { 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, }, { 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x42, 0x00, }, { 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xC3, 0x00, 0x00, }, { 0x3C, 0x7E, 0xFF, 0xFF, 0xC3, 0x81, 0x00, 0x00, }, { 0x3C, 0x7E, 0xFF, 0xC3, 0x81, 0x00, 0x00, 0x00, }, { 0x3C, 0x7E, 0xC3, 0x81, 0x81, 0x00, 0x00, 0x00, }, { 0x3C, 0x42, 0x81, 0x81, 0x00, 0x00, 0x00, 0x00, }, { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, { 0x00, 0x00, 0x00, 0x00, 0x81, 0x81, 0x42, 0x3C, }, { 0x00, 0x00, 0x00, 0x81, 0x81, 0xC3, 0x7E, 0x3C, }, { 0x00, 0x00, 0x00, 0x81, 0xC3, 0xFF, 0x7E, 0x3C, }, { 0x00, 0x00, 0x81, 0xC3, 0xFF, 0xFF, 0x7E, 0x3C, }, { 0x00, 0x00, 0xC3, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, }, { 0x00, 0x42, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, }, // clang-format on }; # endif static void draw_moon(void) { # ifdef STATIC_MOON oled_set_cursor(MOON_COLUMN, MOON_LINE); oled_write_raw_P(moon, 6); # endif # ifndef STATIC_MOON moon_animation_counter = increment_counter(moon_animation_counter, ANIMATE_MOON_EVERY_N_FRAMES); if (moon_animation_counter == 0) { moon_animation_frame = increment_counter(moon_animation_frame, 14); oled_set_cursor(MOON_COLUMN, MOON_LINE); oled_write_raw_P(moon_animation[moon_animation_frame], 8); } # endif } #endif // endregion #ifdef ENABLE_WAVE // region uint8_t starry_night_wave_frame_width_counter = 31; uint8_t rough_waves_frame_counter = 0; // clang-format off static const char PROGMEM ocean_top[8][32] = { // still ocean { 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, }, // small ripples { 0x20, 0x60, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, }, // level 2 ripples { 0x20, 0x60, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, 0x20, 0x60, 0x40, 0x40, }, // level 3 waves { 0x40, 0x20, 0x10, 0x20, 0x40, 0x40, 0x40, 0x40, 0x40, 0x20, 0x10, 0x20, 0x40, 0x40, 0x40, 0x40, 0x40, 0x20, 0x10, 0x20, 0x40, 0x40, 0x40, 0x40, 0x40, 0x20, 0x10, 0x20, 0x40, 0x40, 0x40, 0x40, }, { 0x40, 0x40, 0x20, 0x10, 0x28, 0x50, 0x40, 0x40, 0x40, 0x40, 0x20, 0x10, 0x28, 0x50, 0x40, 0x40, 0x40, 0x40, 0x20, 0x10, 0x28, 0x50, 0x40, 0x40, 0x40, 0x40, 0x20, 0x10, 0x28, 0x50, 0x40, 0x40, }, { 0x40, 0x40, 0x40, 0x20, 0x10, 0x30, 0x70, 0x60, 0x40, 0x40, 0x40, 0x20, 0x10, 0x30, 0x70, 0x60, 0x40, 0x40, 0x40, 0x20, 0x10, 0x30, 0x70, 0x60, 0x40, 0x40, 0x40, 0x20, 0x10, 0x30, 0x70, 0x60, }, }; static const char PROGMEM ocean_bottom[8][32] = { // still ocean { 0x00, 0x40, 0x40, 0x41, 0x01, 0x01, 0x01, 0x21, 0x20, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x44, 0x44, 0x40, 0x40, 0x00, 0x00, 0x08, 0x08, 0x00, 0x01, 0x01, 0x01, 0x00, 0x40, 0x40, 0x00, 0x00, }, // small ripples { 0x00, 0x00, 0x40, 0x40, 0x01, 0x01, 0x01, 0x20, 0x20, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, }, // level 2 ripples { 0x00, 0x00, 0x40, 0x40, 0x01, 0x01, 0x01, 0x20, 0x20, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, }, // level 3 waves { 0x00, 0x40, 0x40, 0x42, 0x42, 0x03, 0x11, 0x11, 0x20, 0x20, 0x00, 0x00, 0x08, 0x0C, 0x0C, 0x04, 0x05, 0x41, 0x41, 0x21, 0x20, 0x00, 0x00, 0x08, 0x0A, 0x0A, 0x0B, 0x41, 0x41, 0x41, 0x41, 0x00, }, { 0x10, 0x10, 0x00, 0x80, 0x84, 0xC4, 0x02, 0x06, 0x84, 0x44, 0xC0, 0x80, 0x80, 0x20, 0x20, 0x10, 0x08, 0x12, 0x91, 0x81, 0x42, 0x40, 0x00, 0x00, 0x10, 0x12, 0x22, 0x22, 0x24, 0x04, 0x84, 0x80, }, { 0x08, 0x80, 0x80, 0x82, 0x82, 0x03, 0x21, 0x21, 0x10, 0x10, 0x00, 0x00, 0x04, 0x04, 0x0C, 0x08, 0x09, 0x41, 0x42, 0x22, 0x20, 0x00, 0x00, 0x08, 0x0A, 0x0A, 0x0B, 0x41, 0x43, 0x42, 0x42, 0x00, }, }; // clang-format on static void animate_waves(void) { starry_night_wave_frame_width_counter = decrement_counter(starry_night_wave_frame_width_counter, WIDTH - 1); // only 3 frames for last wave type rough_waves_frame_counter = increment_counter(rough_waves_frame_counter, 3); // only 3 frames for last wave type void draw_ocean(uint8_t frame, uint16_t offset, uint8_t byte_index) { oled_write_raw_byte(pgm_read_byte(ocean_top[frame] + byte_index), offset); oled_write_raw_byte(pgm_read_byte(ocean_bottom[frame] + byte_index), offset + WIDTH); } for (int i = 0; i < WIDTH; ++i) { uint16_t offset = OCEAN_LINE * WIDTH + i; uint8_t byte_index = starry_night_wave_frame_width_counter + i; if (byte_index >= WIDTH) { byte_index = byte_index - WIDTH; } if (is_calm || current_wpm <= WAVE_CALM) { draw_ocean(0, offset, byte_index); } else if (current_wpm <= WAVE_HEAVY_STORM) { draw_ocean(1, offset, byte_index); } else if (current_wpm <= WAVE_HURRICANE) { draw_ocean(2, offset, byte_index); } else { draw_ocean(3 + rough_waves_frame_counter, offset, byte_index); } } } #endif // endregion #ifdef ENABLE_ISLAND // region uint8_t island_frame_1 = 0; // clang-format off // only use 46 bytes (first 18 are blank, so we don't write them, makes it smaller and we can see the shooting stars properly!) // To save space and allow the shooting stars to be seen, only draw the tree on every frame. // Tree is only 14bytes wide so we save 108 bytes on just the first row. Second row, the // first 18 bytes is always the same piece of land, so only store that once, which saves 90 bytes static const char PROGMEM islandRightTop[6][14] = { {0x84, 0xEC, 0x6C, 0x3C, 0xF8, 0xFE, 0x3F, 0x6B, 0xDB, 0xB9, 0x30, 0x40, 0x00, 0x00,}, {0x80, 0xC3, 0xEE, 0x7C, 0xB8, 0xFC, 0xFE, 0x6F, 0xDB, 0x9B, 0xB2, 0x30, 0x00, 0x00,}, {0x00, 0xC0, 0xEE, 0x7F, 0x3D, 0xF8, 0xFC, 0x7E, 0x57, 0xDB, 0xDB, 0x8A, 0x00, 0x00,}, {0x00, 0xC0, 0xE6, 0x7F, 0x3B, 0xF9, 0xFC, 0xFC, 0xB6, 0xB3, 0x33, 0x61, 0x00, 0x00,}, {0x00, 0x00, 0x00, 0x00, 0x80, 0xEE, 0xFF, 0xFB, 0xF9, 0xFC, 0xDE, 0xB6, 0xB6, 0x24,}, {0x00, 0x00, 0x00, 0x00, 0xC0, 0xEE, 0xFE, 0xFF, 0xFB, 0xFD, 0xEE, 0xB6, 0xB6, 0x92,}, }; static const char PROGMEM islandRightBottom[6][14] = { {0x41, 0x40, 0x60, 0x3E, 0x3F, 0x23, 0x20, 0x60, 0x41, 0x43, 0x40, 0x40, 0x40, 0x80,}, {0x40, 0x41, 0x60, 0x3E, 0x3F, 0x23, 0x20, 0x60, 0x40, 0x40, 0x41, 0x41, 0x40, 0x80,}, {0x40, 0x40, 0x61, 0x3D, 0x3F, 0x27, 0x21, 0x60, 0x40, 0x40, 0x40, 0x40, 0x40, 0x80,}, {0x40, 0x43, 0x61, 0x3C, 0x3F, 0x27, 0x21, 0x60, 0x41, 0x43, 0x43, 0x42, 0x40, 0x80,}, {0x40, 0x40, 0x60, 0x3C, 0x3F, 0x27, 0x23, 0x63, 0x44, 0x40, 0x41, 0x41, 0x41, 0x81,}, {0x40, 0x40, 0x60, 0x3C, 0x3F, 0x27, 0x23, 0x63, 0x42, 0x42, 0x41, 0x41, 0x41, 0x80,}, }; static const char PROGMEM islandLeft[18] = { 0x80, 0x40, 0x40, 0x40, 0x40, 0x60, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x60, 0x40, 0x40, }; // clang-format on static void animate_island(void) { if (animation_counter == 0) { island_frame_1 = increment_counter(island_frame_1, 2); } void draw_island_parts(uint8_t frame) { oled_set_cursor(ISLAND_COLUMN + 3, ISLAND_LINE); oled_write_raw_P(islandRightTop[frame], 14); oled_set_cursor(ISLAND_COLUMN + 0, ISLAND_LINE + 1); oled_write_raw_P(islandLeft, 18); oled_set_cursor(ISLAND_COLUMN + 3, ISLAND_LINE + 1); oled_write_raw_P(islandRightBottom[frame], 14); } if (is_calm || current_wpm < ISLAND_CALM) { draw_island_parts(0); } else if (current_wpm >= ISLAND_CALM && current_wpm < ISLAND_HEAVY_STORM) { draw_island_parts(island_frame_1 + 1); } else if (current_wpm >= ISLAND_HEAVY_STORM && current_wpm < ISLAND_HURRICANE) { draw_island_parts(island_frame_1 + 2); } else { draw_island_parts(island_frame_1 + 4); } } #endif // endregion #ifdef ENABLE_STARS // region bool stars_setup = false; // only setup stars once, then we just twinkle them struct Coordinate { int x; int y; bool exists; }; struct Coordinate stars[TOTAL_STARS]; // tracks all stars/coordinates /** * Setup all the initial stars on the screen * This function divides the screen into regions based on STARS_PER_LINE and NUMBER_OF_STAR_LINES * where each line is made up of 8x8 pixel groups, that are populated by a single star. * * Not sure how this function will work with larger or smaller screens. * It should be fine, as long as the screen width is a multiple of 8 */ static void setup_stars(void) { // For every line, split the line into STARS_PER_LINE, find a random point in that region, and turn the pixel on // 36% probability it will not be added // (said another way, 80% chance it will start out lit in the x direction, then 80% chance it will start out lit in the y direction = 64% probability it will start out lit at all) for (int line = 0; line < NUMBER_OF_STAR_LINES; ++line) { for (int column_group = 0; column_group < STARS_PER_LINE; ++column_group) { uint8_t rand_column = rand() % 10; uint8_t rand_row = rand() % 10; if (rand_column < 8 && rand_row < 8) { int column_adder = column_group * 8; int line_adder = line * 8; int x = rand_column + column_adder; int y = rand_row + line_adder; oled_write_pixel(x, y, true); stars[column_group + (line * STARS_PER_LINE)].x = x; stars[column_group + (line * STARS_PER_LINE)].y = y; stars[column_group + (line * STARS_PER_LINE)].exists = true; } else { stars[column_group + (line * STARS_PER_LINE)].exists = false; } } } stars_setup = true; } /** * Twinkle the stars (move them one pixel in any direction) with a probability of 50% to twinkle any given star */ static void twinkle_stars(void) { for (int line = 0; line < NUMBER_OF_STAR_LINES; ++line) { for (int column_group = 0; column_group < STARS_PER_LINE; ++column_group) { struct Coordinate star = stars[column_group + (line * STARS_PER_LINE)]; // skip stars that were never added if (!star.exists) { continue; } if (rand() % TWINKLE_PROBABILITY_MODULATOR == 0) { oled_write_pixel(star.x, star.y, false); // black out pixel // don't allow stars to leave their own region if (star.x == (column_group * 8)) { // star is the farthest left it can go in its region star.x++; // move it right immediately } else if (star.x == (((column_group + 1) * 8) - 1)) { // star is farthest right it can go in its region star.x--; // move it left immediately } if (star.y == (line * 8)) { // star is the farthest up it can go in its region star.y++; // move it down immediately } else if (star.y == (((line + 1) * 8) - 1)) { // star is farthest down it can go in its region star.y--; // move it up immediately } // now decide direction int new_x; int x_choice = rand() % 3; if (x_choice == 0) { new_x = star.x - 1; } else if (x_choice == 1) { new_x = star.x + 1; } else { new_x = star.x; } int new_y; int y_choice = rand() % 3; if (y_choice == 0) { new_y = star.y - 1; } else if (y_choice == 1) { new_y = star.y + 1; } else { new_y = star.y; } star.x = new_x; star.y = new_y; oled_write_pixel(new_x, new_y, true); } stars[column_group + (line * STARS_PER_LINE)] = star; } } } /** * Setup the stars and then animate them on subsequent frames */ static void animate_stars(void) { if (!stars_setup) { setup_stars(); } else { twinkle_stars(); } } #endif // endregion #ifdef ENABLE_SHOOTING_STARS // region bool shooting_stars_setup = false; // only setup shooting stars array once with defaults struct ShootingStar { int x_1; int y_1; int x_2; int y_2; bool running; int frame; int delay; }; struct ShootingStar shooting_stars[MAX_NUMBER_OF_SHOOTING_STARS]; // tracks all the shooting stars static void setup_shooting_star(struct ShootingStar *shooting_star) { int column_to_start = rand() % (WIDTH / 2); int row_to_start = rand() % (HEIGHT - 48); // shooting_stars travel diagonally 1 down, 1 across. So the lowest a shooting_star can start and not 'hit' the ocean is 32 above the ocean. shooting_star->x_1 = column_to_start; shooting_star->y_1 = row_to_start; shooting_star->x_2 = column_to_start + 1; shooting_star->y_2 = row_to_start + 1; shooting_star->running = true; shooting_star->frame++; shooting_star->delay = rand() % SHOOTING_STAR_DELAY; } static void move_shooting_star(struct ShootingStar *shooting_star) { oled_write_pixel(shooting_star->x_1, shooting_star->y_1, false); oled_write_pixel(shooting_star->x_2, shooting_star->y_2, false); shooting_star->x_1++; shooting_star->y_1++; shooting_star->x_2++; shooting_star->y_2++; shooting_star->frame++; oled_write_pixel(shooting_star->x_1, shooting_star->y_1, true); oled_write_pixel(shooting_star->x_2, shooting_star->y_2, true); } static void finish_shooting_star(struct ShootingStar *shooting_star) { oled_write_pixel(shooting_star->x_1, shooting_star->y_1, false); oled_write_pixel(shooting_star->x_2, shooting_star->y_2, false); shooting_star->running = false; shooting_star->frame = 0; } static void animate_shooting_star(struct ShootingStar *shooting_star) { if (shooting_star->frame > SHOOTING_STAR_FRAMES) { finish_shooting_star(shooting_star); return; } else if (!shooting_star->running) { setup_shooting_star(shooting_star); } else { if (shooting_star->delay == 0) { move_shooting_star(shooting_star); } else { shooting_star->delay--; } } } static void animate_shooting_stars(void) { if (is_calm) { return; } if (!shooting_stars_setup) { for (int i = 0; i < MAX_NUMBER_OF_SHOOTING_STARS; ++i) { shooting_stars[i].running = false; } shooting_stars_setup = true; } /** * Fixes issue with stars that were falling _while_ the * wpm dropped below the condition for them to keep falling */ void end_extra_stars(uint8_t starting_index) { for (int shooting_star_index = starting_index; shooting_star_index < MAX_NUMBER_OF_SHOOTING_STARS; ++shooting_star_index) { struct ShootingStar shooting_star = shooting_stars[shooting_star_index]; if (shooting_star.running) { finish_shooting_star(&shooting_star); shooting_stars[shooting_star_index] = shooting_star; } } } int number_of_shooting_stars = current_wpm / SHOOTING_STAR_WPM_INCREMENT; number_of_shooting_stars = (number_of_shooting_stars > MAX_NUMBER_OF_SHOOTING_STARS) ? MAX_NUMBER_OF_SHOOTING_STARS : number_of_shooting_stars; if (number_of_shooting_stars == 0) { // make sure all shooting_stars are ended end_extra_stars(0); } else { for (int shooting_star_index = 0; shooting_star_index < number_of_shooting_stars; ++shooting_star_index) { struct ShootingStar shooting_star = shooting_stars[shooting_star_index]; animate_shooting_star(&shooting_star); shooting_stars[shooting_star_index] = shooting_star; } end_extra_stars(number_of_shooting_stars); } } #endif // endregion /** * Main rendering function * * Calls all different animations at different rates */ void render_stars(void) { // // animation timer if (timer_elapsed32(starry_night_anim_timer) > STARRY_NIGHT_ANIM_FRAME_DURATION) { starry_night_anim_timer = timer_read32(); current_wpm = get_current_wpm(); #ifdef ENABLE_ISLAND animate_island(); #endif #ifdef ENABLE_SHOOTING_STARS if (animation_counter % SHOOTING_STAR_ANIMATION_MODULATOR == 0) { animate_shooting_stars(); } #endif #ifdef ENABLE_STARS // TODO offsetting the star animation from the wave animation would look better, // but if I do that, then the stars appear in the water because // the ocean animation has to wait a bunch of frames to overwrite it. // Possible solutions: // 1. Only draw stars to the top of the island/ocean. // 2. Draw ocean every frame, only move ocean on frames matching modulus // Problems: // 1. What if someone wants to move the island up a bit, or they want to have the stars reflect in the water? // 2. More cpu intensive. And I'm already running out of cpu as it is... if (animation_counter % STAR_ANIMATION_MODULATOR == 0) { animate_stars(); } #endif #ifdef ENABLE_WAVE if (animation_counter % OCEAN_ANIMATION_MODULATOR == 0) { animate_waves(); } #endif #ifdef ENABLE_MOON draw_moon(); #endif animation_counter = increment_counter(animation_counter, NUMBER_OF_FRAMES); } // this fixes the screen on and off bug if (current_wpm > 0) { oled_on(); starry_night_anim_sleep = timer_read32(); } else if (timer_elapsed32(starry_night_anim_sleep) > OLED_TIMEOUT) { oled_off(); } }