summaryrefslogtreecommitdiff
path: root/users/snowe/ocean_dream.c
blob: bd95ea16f8890686e14bc46801024db71e008e46 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
/*
 * Copyright 2021 Tyler Thrailkill (@snowe/@snowe2010) <tyler.b.thrailkill@gmail.com>
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

#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) {
    current_wpm             = get_current_wpm();

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


    // Turn screen on/off based on typing and timeout
    if (current_wpm > 0 && timer_elapsed32(starry_night_anim_timer) > STARRY_NIGHT_ANIM_FRAME_DURATION) {
        starry_night_anim_timer = timer_read32();
        oled_on();
        render_stars_anim();
        starry_night_anim_sleep = timer_read32();
    } else if (timer_elapsed32(starry_night_anim_sleep) > OLED_TIMEOUT) {
        oled_off();
    }
}