From b55f87523fa9ca4402294cb26b77af691d97699d Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 22 Apr 2026 12:57:35 -0700 Subject: [PATCH] =?UTF-8?q?ac-native:=20shutdown=20animation=20=E2=80=94?= =?UTF-8?q?=20"bye=20@handle"=20farewell=20before=20poweroff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror of the boot animation in reverse: plays a ~1.2s sequence when ac_poweroff() is called. Same matrix-rain background, same rainbow stripes, same per-handle title color palette as the "good morning @jeffrey" boot greeting — but the title reads "bye @jeffrey" and the entire composition fades to black with a cubic ease-in so the kernel halt lands on a clean black frame. Structure: - 72 frames @ 60 fps (1.2s total) - First 18 frames (~300 ms): title fully visible, bg at full time-of-day color, matrix rain at full intensity - Remaining 54 frames: bg + stripes + rain + title all fade with fade_t = (1 - t)^3 so the last ~100 ms sits near black - One explicit black frame at the very end before sync()/reboot() Title derived from the live boot_title: "good morning @jeffrey" → "bye @jeffrey" "good afternoon @jeffrey" → "bye @jeffrey" "notepat" (no handle) → "bye" Wiring: - Forward-declared draw_shutdown_anim() near the top of the file so ac_poweroff() can call it (ac_poweroff is defined at line 160, the animation body lives after draw_boot_status around line 2990). - Added three globals — g_shutdown_graph, g_shutdown_screen, g_shutdown_pixel_scale — populated in main() immediately after graph_init(), and kept in sync when pixel_scale changes at runtime. - draw_shutdown_anim() no-ops when those globals aren't ready (early-boot emergency poweroffs before main() reaches graphics init still power off immediately without crashing). - ac_poweroff() now calls draw_shutdown_anim() before sync()/reboot(). Reuses existing helpers — title_char_color, font_draw_matrix, font_measure_matrix, graph_wipe/ink/box/plot, ac_display_present, frame_sync_60fps — no new dependencies, no build-system changes. This is the other half of the lacma-2026 demo video: once ac-os rebuilds and captures a shutdown clip, the VO pipeline at tools/vo-pipeline.mjs can stitch the full boot → notepat → shutdown sequence together. Co-Authored-By: Claude Opus 4.7 (1M context) --- fedac/native/src/ac-native.c | 216 +++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/fedac/native/src/ac-native.c b/fedac/native/src/ac-native.c index 1169abfb13..a811d07eaa 100644 --- a/fedac/native/src/ac-native.c +++ b/fedac/native/src/ac-native.c @@ -158,6 +158,10 @@ static void sigterm_handler(int sig) { // with CAP_SYS_BOOT — that's faster than round-tripping through init. // - Under systemd (NixOS): fall back to systemctl. static void ac_poweroff(void) { + // Farewell animation (1.5s) — runs only if the display context is + // available (skipped during early-boot emergency poweroffs before main + // initializes graphics). + draw_shutdown_anim(); sync(); usleep(500000); sync(); @@ -489,6 +493,16 @@ static int get_la_hour(void); // Global display pointer — exposed to js-bindings for browser DRM handoff void *g_display = NULL; +// Shutdown animation context — main() populates these once the display + +// graph are initialized so ac_poweroff() can draw a "bye @handle" farewell +// screen that mirrors the boot animation before the kernel halts. +// Forward-declared here because ac_poweroff() (defined at the top of the +// file) needs to call into drawing code further down. +static ACGraph *g_shutdown_graph = NULL; +static ACFramebuffer *g_shutdown_screen = NULL; +static int g_shutdown_pixel_scale = 3; +static void draw_shutdown_anim(void); + #ifdef USE_WAYLAND // Global Wayland display — used by ac_display_present dispatch static ACWaylandDisplay *g_wayland_display = NULL; @@ -2962,6 +2976,199 @@ static void draw_boot_status(ACGraph *graph, ACFramebuffer *screen, } } +// ─── SHUTDOWN ANIMATION ────────────────────────────────────────────────── +// Mirror of the boot animation in reverse: "bye @handle" appears briefly, +// matrix-rain fades with the background to black, ~1.2s total. Skipped +// (no-op) if the display context isn't ready yet. +// +// Uses the same helpers as the boot animation: matrix-rain column state, +// title_char_color() for per-handle rainbow, font_draw_matrix() for the +// chunky title, graph_ink()/graph_box()/graph_plot() for rendering. +static void draw_shutdown_anim(void) { + ACGraph *graph = g_shutdown_graph; + ACFramebuffer *screen = g_shutdown_screen; + ACDisplay *display = (ACDisplay *)g_display; + int pixel_scale = g_shutdown_pixel_scale; + if (!graph || !screen || !display) return; + + // Build the shutdown title from the current boot_title: + // "good morning @jeffrey" → "bye @jeffrey" + // "notepat" (no handle) → "bye" + char shutdown_title[96]; + { + const char *at = strchr(boot_title, '@'); + if (at && at[1]) { + snprintf(shutdown_title, sizeof(shutdown_title), "bye %s", at); + } else { + snprintf(shutdown_title, sizeof(shutdown_title), "bye"); + } + } + + // Time-of-day base background (same palette as boot_status). + int la_hour = get_la_hour(); + int is_day = (la_hour >= 7 && la_hour < 18); + uint8_t bg_r0, bg_g0, bg_b0; + if (is_day) { + if (la_hour < 12) { bg_r0 = 235; bg_g0 = 230; bg_b0 = 220; } + else { bg_r0 = 240; bg_g0 = 235; bg_b0 = 215; } + } else { + if (la_hour >= 5 && la_hour < 7) { bg_r0 = 100; bg_g0 = 45; bg_b0 = 20; } + else if (la_hour >= 18 && la_hour < 20){ bg_r0 = 80; bg_g0 = 25; bg_b0 = 60; } + else { bg_r0 = 15; bg_g0 = 15; bg_b0 = 40; } + } + + // Matrix-rain column state — reseed per shutdown so it feels fresh. + #define SHUTDOWN_RAIN_MAX_COLS 128 + int rain_col_y[SHUTDOWN_RAIN_MAX_COLS]; + int rain_col_speed[SHUTDOWN_RAIN_MAX_COLS]; + unsigned int rain_col_seed[SHUTDOWN_RAIN_MAX_COLS]; + { + unsigned int s = (unsigned int)time(NULL) ^ 0xBEEFCAFEu; + for (int c = 0; c < SHUTDOWN_RAIN_MAX_COLS; c++) { + s ^= s << 13; s ^= s >> 17; s ^= s << 5; + rain_col_y[c] = -(int)(s % 400); + s ^= s << 13; s ^= s >> 17; s ^= s << 5; + rain_col_speed[c] = 2 + (int)(s % 4); + s ^= s << 13; s ^= s >> 17; s ^= s << 5; + rain_col_seed[c] = s; + } + } + + struct timespec anim_time; + clock_gettime(CLOCK_MONOTONIC, &anim_time); + + // 72 frames @ 60fps = 1.2s. First 18 frames (~300ms): title fully + // visible, bg at full time-of-day color. Remaining 54 frames: linear + // fade to black on both bg and title. + int total_frames = 72; + int hold_frames = 18; + for (int f = 0; f < total_frames; f++) { + // fade_t: 1.0 during hold, then eases to 0. + double fade_t; + if (f < hold_frames) { + fade_t = 1.0; + } else { + double t = (double)(f - hold_frames) / (double)(total_frames - hold_frames); + // Cubic ease-in (fade accelerates) so the last frames are nearly black. + fade_t = 1.0 - t; + fade_t = fade_t * fade_t * fade_t; + } + + uint8_t bg_r = (uint8_t)(bg_r0 * fade_t); + uint8_t bg_g = (uint8_t)(bg_g0 * fade_t); + uint8_t bg_b = (uint8_t)(bg_b0 * fade_t); + graph_wipe(graph, (ACColor){bg_r, bg_g, bg_b, 255}); + + // Rainbow stripes top + bottom (dim with fade). + { + int stripe_h = 3; + int num_stripes = 8; + int band = num_stripes * stripe_h; + for (int s = 0; s < num_stripes; s++) { + double hue = fmod((double)s / num_stripes * 360.0 + f * 3.0, 360.0); + double h6 = hue / 60.0; + int hi = (int)h6 % 6; + double fr = h6 - (int)h6; + double cr, cg, cb; + switch (hi) { + case 0: cr = 1; cg = fr; cb = 0; break; + case 1: cr = 1-fr; cg = 1; cb = 0; break; + case 2: cr = 0; cg = 1; cb = fr; break; + case 3: cr = 0; cg = 1-fr; cb = 1; break; + case 4: cr = fr; cg = 0; cb = 1; break; + default:cr = 1; cg = 0; cb = 1-fr; break; + } + uint8_t a = (uint8_t)(80 * fade_t); + int y_top = s * stripe_h; + int y_bot = screen->height - band + s * stripe_h; + graph_ink(graph, (ACColor){(uint8_t)(cr*200), (uint8_t)(cg*200), (uint8_t)(cb*200), a}); + graph_box(graph, 0, y_top, screen->width, stripe_h, 1); + graph_box(graph, 0, y_bot, screen->width, stripe_h, 1); + } + } + + // Matrix rain — same math as boot, dimmed by fade_t. + { + int col_w = 10; + int glyph_w = 7, glyph_h = 9; + int nc = screen->width / col_w; + if (nc > SHUTDOWN_RAIN_MAX_COLS) nc = SHUTDOWN_RAIN_MAX_COLS; + int rain_alpha_scale = (int)((is_day ? 80 : 180) * fade_t); + for (int c = 0; c < nc; c++) { + rain_col_y[c] += rain_col_speed[c]; + if (rain_col_y[c] > screen->height + glyph_h * 12) { + unsigned int s = rain_col_seed[c]; + s ^= s << 13; s ^= s >> 17; s ^= s << 5; + rain_col_seed[c] = s; + rain_col_y[c] = -(int)(s % 300); + rain_col_speed[c] = 2 + (int)(s % 4); + } + int cx = c * col_w + 1; + for (int trail = 0; trail < 10; trail++) { + int gy = rain_col_y[c] - trail * glyph_h; + if (gy < -glyph_h || gy >= screen->height) continue; + int tb = (trail == 0) ? 220 : 180 - trail * 18; + if (tb < 15) continue; + int b = (tb * rain_alpha_scale) >> 8; + ACColor cg_col = (trail == 0) + ? (ACColor){ (uint8_t)(b * 0.8), 255, (uint8_t)(b * 0.8), (uint8_t)b } + : (ACColor){ 40, (uint8_t)(b * 0.9 + 20), (uint8_t)(b * 0.4), (uint8_t)b }; + graph_ink(graph, cg_col); + unsigned int gseed = rain_col_seed[c] + ^ ((unsigned)trail * 2654435761u) + ^ ((unsigned)(f / 4) * 2246822519u); + for (int row = 0; row < glyph_h; row++) { + for (int col = 0; col < glyph_w; col++) { + gseed ^= gseed << 13; gseed ^= gseed >> 17; gseed ^= gseed << 5; + if (gseed & 1) graph_plot(graph, cx + col, gy + row); + } + } + } + } + } + + // Title — "bye @handle" with per-handle colors, fades with bg. + int alpha = (int)(255.0 * fade_t); + if (alpha > 0) { + int scale = 3; + int tw = font_measure_matrix(shutdown_title, scale); + if (tw > screen->width - 20) { + scale = 2; + tw = font_measure_matrix(shutdown_title, scale); + } + int tx = (screen->width - tw) / 2; + int ty = screen->height / 2 - 20; + for (int ci = 0; shutdown_title[ci]; ci++) { + ACColor cc = title_char_color(ci, f, alpha); + graph_ink(graph, cc); + char ch[2] = { shutdown_title[ci], 0 }; + tx = font_draw_matrix(graph, ch, tx, ty, scale); + } + } + + // Subtitle — "aesthetic.computer" beneath the title, also fading. + { + uint8_t sub = (uint8_t)((is_day ? 100 : 140) * fade_t); + if (sub > 0) { + graph_ink(graph, (ACColor){sub, sub, sub, 255}); + int sw = font_measure_matrix("aesthetic.computer", 1); + font_draw_matrix(graph, "aesthetic.computer", + (screen->width - sw) / 2, + screen->height / 2 + 10, 1); + } + } + + ac_display_present(display, screen, pixel_scale); + frame_sync_60fps(&anim_time); + } + + // Final flush: one frame of pure black so the kernel halt doesn't + // leave a partially-faded frame on screen. + graph_wipe(graph, (ACColor){0, 0, 0, 255}); + ac_display_present(display, screen, pixel_scale); + #undef SHUTDOWN_RAIN_MAX_COLS +} + // Audio recording tap callback (adapts rec_callback signature to recorder_submit_audio) static void rec_audio_tap(const int16_t *pcm, int frames, void *userdata) { recorder_submit_audio((ACRecorder *)userdata, pcm, frames); @@ -3182,6 +3389,12 @@ int main(int argc, char *argv[]) { if (display) graph_init_gpu(&graph, display); font_init(); + // Expose graph + screen + pixel_scale to ac_poweroff() so the shutdown + // animation ("bye @handle") can render before the kernel halts. + g_shutdown_graph = &graph; + g_shutdown_screen = screen; + g_shutdown_pixel_scale = pixel_scale; + // Cursor overlay buffer — drawn separately so KidLisp effects don't smear it ACFramebuffer *cursor_fb = fb_create(screen->width, screen->height); @@ -4071,6 +4284,9 @@ int main(int argc, char *argv[]) { screen = new_screen; graph_init(&graph, screen); if (display) graph_init_gpu(&graph, display); + // Keep shutdown-animation globals in sync with new screen + scale + g_shutdown_screen = screen; + g_shutdown_pixel_scale = pixel_scale; input->scale = pixel_scale; // Update JS runtime's graph reference (holds framebuffer) if (rt) { -- 2.54.0