Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

ac-native: extract boot animation renderer; macOS screenshot mode

Pulls the per-frame render loop out of ac-native.c's draw_startup_fade
into src/boot_anim.{c,h} so the Linux PID-1 init path and the macOS
host can share the same code. All Linux-only I/O (evdev key drain, TTS
greeting, boot melody, DRM present, /mnt writes) stays in the caller;
boot_anim only touches ACGraph + ACFramebuffer.

Linux side: boot_anim.c added to SRCS, draw_startup_fade now builds a
BootAnimConfig from existing state (handle, colors, city, build info,
install flags, auth badges) and calls boot_anim_render_frame each tick.
Matrix-rain state moved from inline arrays to BootAnimState.

macOS side: shared Makefile now pulls in graph.c/font.c/framebuffer.c/
color.c/qrcodegen.c alongside synth_core. New png_writer.c wraps
CoreGraphics/ImageIO for native PNG encoding. main.c gains a
screenshot branch: when AC_SHOT_PNG is set, the host skips SDL
entirely, allocates an ACFramebuffer at the requested size/density,
simulates N frames of the boot animation, upscales nearest-neighbor
for chunky retro pixels, and writes out a PNG.

Env knobs:
AC_SHOT_PNG = output path (required)
AC_SHOT_W/H = output resolution (default 1280x800)
AC_SHOT_DENSITY = internal FB divisor (default 2, clamped [1,8])
AC_SHOT_HANDLE = e.g. jeffrey -> 'hi @jeffrey'
AC_SHOT_CITY = e.g. 'Los Angeles' (default)
AC_SHOT_HOUR = 0-23, drives day/night palette (default 10)
AC_SHOT_FRAME = which boot-anim frame to capture (default last)
AC_SHOT_TITLE_SCALE = force MatrixChunky8 scale; auto-fits ~55%
of FB width when unset so product shots
don't inherit the tiny on-hardware scale
AC_SHOT_TITLE = full title override (otherwise built from HANDLE)
AC_SHOT_GIT_HASH / BUILD_TS / BUILD_NAME / DRIVER = version panel
AC_SHOT_FRESH / INSTALL / INSTALLED / CLAUDE / GITHUB = badges

Tested: 1280x800 + AC_SHOT_HANDLE=jeffrey + Los Angeles renders the
full boot screen with matrix rain, drifting triangles, rainbow title,
time bar, and city subtitle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+744 -322
+1
fedac/native/Makefile
··· 74 74 $(SRCDIR)/graph3d.c \ 75 75 $(SRCDIR)/font.c \ 76 76 $(SRCDIR)/color.c \ 77 + $(SRCDIR)/boot_anim.c \ 77 78 $(SRCDIR)/input.c \ 78 79 $(SRCDIR)/audio.c \ 79 80 $(SRCDIR)/usb-midi.c \
+16 -4
fedac/native/macos/Makefile
··· 35 35 $(error AUDIO must be 'core' or 'sdl') 36 36 endif 37 37 38 - HOST_SRCS := main.c piece.c $(AUDIO_SRC) 38 + HOST_SRCS := main.c piece.c png_writer.c $(AUDIO_SRC) 39 39 HOST_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(HOST_SRCS)) 40 - SHARED_SRCS := ../src/synth_core.c 41 - SHARED_OBJS := $(patsubst ../src/%.c,$(BUILD)/shared-%.o,$(SHARED_SRCS)) 42 - LDFLAGS += $(SDL3_LIBS_BASE) $(AUDIO_FRAMEWORKS) 40 + # Shared with Linux ac-native: audio synth, boot-screen renderer, and the 41 + # portable graphics/font/framebuffer stack the boot animation draws into. 42 + # qrcodegen is a transitive dep of graph.c (for graph_qr) even though the 43 + # boot animation itself doesn't render QR codes. 44 + SHARED_CORE_SRCS := synth_core.c boot_anim.c graph.c font.c framebuffer.c color.c 45 + SHARED_QR_SRCS := qrcodegen.c 46 + SHARED_OBJS := $(patsubst %.c,$(BUILD)/shared-%.o,$(SHARED_CORE_SRCS) $(SHARED_QR_SRCS)) 47 + LDFLAGS += $(SDL3_LIBS_BASE) $(AUDIO_FRAMEWORKS) 48 + # PNG writer uses ImageIO + CoreGraphics for native encoding. 49 + LDFLAGS += -framework ImageIO -framework CoreGraphics -framework CoreFoundation 50 + # qrcodegen.h lives outside ../src. 51 + CFLAGS += -I../lib/qrcodegen 43 52 44 53 # QuickJS core object set (must match what the Linux Makefile uses at line 104) 45 54 QJS_SRCS := quickjs.c libunicode.c libregexp.c cutils.c libbf.c ··· 92 101 $(CC) $(CFLAGS) -c -o $@ $< 93 102 94 103 $(BUILD)/shared-%.o: ../src/%.c | $(BUILD) 104 + $(CC) $(CFLAGS) -c -o $@ $< 105 + 106 + $(BUILD)/shared-%.o: ../lib/qrcodegen/%.c | $(BUILD) 95 107 $(CC) $(CFLAGS) -c -o $@ $< 96 108 97 109 # QuickJS objects: compiled with the same CONFIG_VERSION the Linux side uses.
+146
fedac/native/macos/main.c
··· 15 15 16 16 #include "piece.h" 17 17 #include "audio.h" 18 + #include "png_writer.h" 19 + #include "boot_anim.h" 20 + #include "graph.h" 21 + #include "font.h" 22 + #include "framebuffer.h" 18 23 19 24 // Initial window size in logical points. The framebuffer is win / DENSITY, 20 25 // so 640×480 @ d=2 yields a 320×240 canvas — a classic retro resolution ··· 238 243 g_hotkey = NULL; g_hotkey_handler = NULL; 239 244 } 240 245 246 + // Nearest-neighbor upscale an ARGB framebuffer by an integer factor. Used 247 + // by the screenshot path so the retro pixel look stays crisp when a 248 + // density-2 canvas is written out at 2× resolution. Allocates; caller 249 + // free()s. Returns NULL on allocation failure. 250 + static uint32_t *upscale_nn(const uint32_t *src, int sw, int sh, int scale) { 251 + if (scale <= 1) return NULL; 252 + int dw = sw * scale, dh = sh * scale; 253 + uint32_t *dst = malloc((size_t)dw * dh * sizeof(uint32_t)); 254 + if (!dst) return NULL; 255 + for (int y = 0; y < dh; y++) { 256 + const uint32_t *srow = src + (y / scale) * sw; 257 + uint32_t *drow = dst + y * dw; 258 + for (int x = 0; x < dw; x++) drow[x] = srow[x / scale]; 259 + } 260 + return dst; 261 + } 262 + 263 + // Screenshot mode — when AC_SHOT_PNG is set, we skip SDL entirely and 264 + // render a single frame of the boot animation to a PNG. Size + density 265 + // are configurable; frame index defaults to just before the end so the 266 + // subtitle has faded in and the time-bar is small. 267 + static int run_screenshot_mode(void) { 268 + const char *out_path = getenv("AC_SHOT_PNG"); 269 + if (!out_path || !out_path[0]) return 0; 270 + 271 + int out_w = getenv("AC_SHOT_W") ? atoi(getenv("AC_SHOT_W")) : 1280; 272 + int out_h = getenv("AC_SHOT_H") ? atoi(getenv("AC_SHOT_H")) : 800; 273 + int density = getenv("AC_SHOT_DENSITY") ? atoi(getenv("AC_SHOT_DENSITY")) : 2; 274 + if (density < 1) density = 1; 275 + if (density > 8) density = 8; 276 + int fb_w = out_w / density; if (fb_w < 32) fb_w = 32; 277 + int fb_h = out_h / density; if (fb_h < 32) fb_h = 32; 278 + 279 + // Which frame to capture. Default = last real frame (n-1), so the 280 + // subtitle has peaked and the fade-in is complete. 281 + int frame = getenv("AC_SHOT_FRAME") 282 + ? atoi(getenv("AC_SHOT_FRAME")) 283 + : BOOT_ANIM_FRAMES - 1; 284 + if (frame < 0) frame = 0; 285 + if (frame >= BOOT_ANIM_FRAMES) frame = BOOT_ANIM_FRAMES - 1; 286 + 287 + // Build title: AC_SHOT_TITLE wins outright; AC_SHOT_HANDLE wraps it 288 + // in "hi @handle"; falling back to a neutral "hi" greeting. 289 + char title_buf[128]; 290 + const char *t_env = getenv("AC_SHOT_TITLE"); 291 + const char *h_env = getenv("AC_SHOT_HANDLE"); 292 + if (t_env && t_env[0]) { 293 + snprintf(title_buf, sizeof title_buf, "%s", t_env); 294 + } else if (h_env && h_env[0]) { 295 + const char *h = (h_env[0] == '@') ? h_env + 1 : h_env; 296 + snprintf(title_buf, sizeof title_buf, "hi @%s", h); 297 + } else { 298 + snprintf(title_buf, sizeof title_buf, "hi"); 299 + } 300 + 301 + const char *city = getenv("AC_SHOT_CITY"); 302 + if (!city || !city[0]) city = "Los Angeles"; 303 + 304 + int hour = getenv("AC_SHOT_HOUR") ? atoi(getenv("AC_SHOT_HOUR")) : 10; 305 + 306 + // Title scale. Product shots need a bigger handle than the on-hardware 307 + // default (scale 3 is tuned for native resolution where the text is a 308 + // small label). Target ~55 % of FB width when unset, clamped to keep 309 + // the MatrixChunky8 bitmap from going mushy. Override with 310 + // AC_SHOT_TITLE_SCALE for total control. 311 + int title_scale = 0; 312 + const char *ts_env = getenv("AC_SHOT_TITLE_SCALE"); 313 + if (ts_env && ts_env[0]) { 314 + title_scale = atoi(ts_env); 315 + } else { 316 + int title_len = 0; 317 + for (const char *p = (t_env && t_env[0]) ? t_env : title_buf; *p; p++) title_len++; 318 + if (title_len < 1) title_len = 1; 319 + // Matrix font is ~4 px wide per char at scale 1. 320 + int target_px = fb_w * 55 / 100; 321 + int auto_s = target_px / (title_len * 4); 322 + if (auto_s < 3) auto_s = 3; 323 + if (auto_s > 8) auto_s = 8; 324 + title_scale = auto_s; 325 + } 326 + 327 + BootAnimConfig cfg = { 328 + .title = title_buf, 329 + .city = city, 330 + .title_colors = NULL, 331 + .title_colors_len = 0, 332 + .hour = hour, 333 + .git_hash = getenv("AC_SHOT_GIT_HASH"), 334 + .build_ts = getenv("AC_SHOT_BUILD_TS"), 335 + .build_name = getenv("AC_SHOT_BUILD_NAME"), 336 + .driver_name = getenv("AC_SHOT_DRIVER"), 337 + .is_new_version = getenv("AC_SHOT_FRESH") ? 1 : 0, 338 + .show_install = getenv("AC_SHOT_INSTALL") ? 1 : 0, 339 + .is_installed = getenv("AC_SHOT_INSTALLED") ? 1 : 0, 340 + .has_claude_badge = getenv("AC_SHOT_CLAUDE") ? 1 : 0, 341 + .has_github_badge = getenv("AC_SHOT_GITHUB") ? 1 : 0, 342 + .title_scale = title_scale, 343 + }; 344 + 345 + ACFramebuffer *fb = fb_create(fb_w, fb_h); 346 + if (!fb) { fprintf(stderr, "[shot] fb_create failed\n"); return 1; } 347 + ACGraph g; 348 + graph_init(&g, fb); 349 + font_init(); 350 + 351 + BootAnimState state = {0}; 352 + // Simulate all frames up to target so rain state evolves naturally. 353 + for (int f = 0; f <= frame; f++) { 354 + boot_anim_render_frame(&g, fb, f, &cfg, &state); 355 + } 356 + 357 + uint32_t *out_pixels = fb->pixels; 358 + uint32_t *scaled = NULL; 359 + int stride = fb->stride; 360 + int w = fb->width, h = fb->height; 361 + if (density > 1) { 362 + scaled = upscale_nn(fb->pixels, fb->width, fb->height, density); 363 + if (!scaled) { fprintf(stderr, "[shot] upscale_nn failed\n"); fb_destroy(fb); return 1; } 364 + out_pixels = scaled; 365 + w = fb->width * density; 366 + h = fb->height * density; 367 + stride = w; 368 + } 369 + 370 + int ok = png_write_argb(out_path, out_pixels, w, h, stride); 371 + if (ok) { 372 + fprintf(stderr, "[shot] %dx%d (fb %dx%d @ density %d) frame %d → %s\n", 373 + w, h, fb->width, fb->height, density, frame, out_path); 374 + } 375 + free(scaled); 376 + fb_destroy(fb); 377 + return ok ? 0 : 1; 378 + } 379 + 241 380 int main(int argc, char **argv) { 381 + // Screenshot mode short-circuits everything else — no SDL, no piece, 382 + // just one frame of the boot animation to a PNG. Gated on AC_SHOT_PNG 383 + // so normal launches (and `make app`) behave exactly as before. 384 + if (getenv("AC_SHOT_PNG")) { 385 + return run_screenshot_mode(); 386 + } 387 + 242 388 // --test-tone: exercise the audio engine only; no window, no piece. 243 389 // Plays a 440 Hz sine for ~1s and prints the peak output sample. Useful 244 390 // for verifying audio works in isolation (CI / headless regression).
+76
fedac/native/macos/png_writer.c
··· 1 + // png_writer.c — Write an ARGB framebuffer to PNG using native macOS APIs. 2 + // 3 + // CoreGraphics expects planar row data with a bitmap info flag describing 4 + // channel order. Our framebuffer is `uint32_t`s laid out as 0xAARRGGBB on 5 + // little-endian, which on-disk reads as bytes BB,GG,RR,AA. The matching 6 + // bitmap-info constant is 7 + // kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little 8 + // so CGImageCreate interprets the bytes correctly without us having to 9 + // repack to straight RGBA. 10 + #include "png_writer.h" 11 + 12 + #include <CoreFoundation/CoreFoundation.h> 13 + #include <CoreGraphics/CoreGraphics.h> 14 + #include <ImageIO/ImageIO.h> 15 + #include <stdio.h> 16 + #include <stdlib.h> 17 + #include <string.h> 18 + 19 + int png_write_argb(const char *out_path, 20 + const uint32_t *argb_pixels, 21 + int width, int height, int stride_pixels) { 22 + if (!out_path || !argb_pixels || width <= 0 || height <= 0) return 0; 23 + 24 + size_t row_bytes = (size_t)stride_pixels * 4; 25 + CGDataProviderRef provider = CGDataProviderCreateWithData( 26 + NULL, argb_pixels, row_bytes * (size_t)height, NULL); 27 + if (!provider) { 28 + fprintf(stderr, "[png] CGDataProviderCreateWithData failed\n"); 29 + return 0; 30 + } 31 + 32 + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); 33 + CGImageRef img = CGImageCreate( 34 + (size_t)width, (size_t)height, 35 + /*bits per component*/ 8, 36 + /*bits per pixel */ 32, 37 + row_bytes, cs, 38 + kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little, 39 + provider, NULL, false, kCGRenderingIntentDefault); 40 + CGColorSpaceRelease(cs); 41 + CGDataProviderRelease(provider); 42 + if (!img) { 43 + fprintf(stderr, "[png] CGImageCreate failed\n"); 44 + return 0; 45 + } 46 + 47 + CFStringRef path_cf = CFStringCreateWithCString( 48 + kCFAllocatorDefault, out_path, kCFStringEncodingUTF8); 49 + CFURLRef url = CFURLCreateWithFileSystemPath( 50 + kCFAllocatorDefault, path_cf, kCFURLPOSIXPathStyle, false); 51 + CFRelease(path_cf); 52 + if (!url) { 53 + fprintf(stderr, "[png] CFURLCreateWithFileSystemPath failed\n"); 54 + CGImageRelease(img); 55 + return 0; 56 + } 57 + 58 + CGImageDestinationRef dst = CGImageDestinationCreateWithURL( 59 + url, CFSTR("public.png"), 1, NULL); 60 + CFRelease(url); 61 + if (!dst) { 62 + fprintf(stderr, "[png] CGImageDestinationCreateWithURL failed\n"); 63 + CGImageRelease(img); 64 + return 0; 65 + } 66 + 67 + CGImageDestinationAddImage(dst, img, NULL); 68 + bool ok = CGImageDestinationFinalize(dst); 69 + CFRelease(dst); 70 + CGImageRelease(img); 71 + if (!ok) { 72 + fprintf(stderr, "[png] CGImageDestinationFinalize failed\n"); 73 + return 0; 74 + } 75 + return 1; 76 + }
+14
fedac/native/macos/png_writer.h
··· 1 + // png_writer.h — ARGB8888 framebuffer → PNG via CoreGraphics/ImageIO. 2 + // Pure C (no ObjC). Writes `width * height * 4` bytes from `argb_pixels` 3 + // (kARGB8888, premultiplied last — matches ACFramebuffer layout) to 4 + // `out_path`. Returns 1 on success, 0 on failure (fprintf logs reason). 5 + #ifndef AC_MACOS_PNG_WRITER_H 6 + #define AC_MACOS_PNG_WRITER_H 7 + 8 + #include <stdint.h> 9 + 10 + int png_write_argb(const char *out_path, 11 + const uint32_t *argb_pixels, 12 + int width, int height, int stride_pixels); 13 + 14 + #endif
+33 -318
fedac/native/src/ac-native.c
··· 28 28 #include "framebuffer.h" 29 29 #include "graph.h" 30 30 #include "font.h" 31 + #include "boot_anim.h" 31 32 #include "input.h" 32 33 #include "audio.h" 33 34 #include "wifi.h" ··· 2449 2450 int hold_frames = 40; 2450 2451 int skip_anim = 0; 2451 2452 int w_pressed = 0; 2452 - // Matrix-rain background state — persistent across frames. One entry 2453 - // per column; column width fixed at 10 px. 2454 - #define RAIN_MAX_COLS 128 2455 - int rain_col_y[RAIN_MAX_COLS]; 2456 - int rain_col_speed[RAIN_MAX_COLS]; 2457 - unsigned int rain_col_seed[RAIN_MAX_COLS]; 2458 - { 2459 - unsigned int s = (unsigned int)time(NULL); 2460 - for (int c = 0; c < RAIN_MAX_COLS; c++) { 2461 - s ^= s << 13; s ^= s >> 17; s ^= s << 5; 2462 - rain_col_y[c] = -(int)(s % 400); 2463 - s ^= s << 13; s ^= s >> 17; s ^= s << 5; 2464 - rain_col_speed[c] = 2 + (int)(s % 4); 2465 - s ^= s << 13; s ^= s >> 17; s ^= s << 5; 2466 - rain_col_seed[c] = s; 2467 - } 2468 - } 2453 + // Per-frame render is handled by src/boot_anim.c (shared with the 2454 + // macOS host). State struct carries matrix-rain columns across frames. 2455 + BootAnimState rain_state = {0}; 2469 2456 for (int f = 0; f < total_frames && !skip_anim && !w_pressed; f++) { 2470 2457 double t = (double)f / (double)total_frames; 2471 2458 ··· 2524 2511 } 2525 2512 // W hint is visual only — no TTS 2526 2513 2527 - // Fade from black/white to time-of-day themed bg (complete in first 0.3s) 2528 - double fade_t = t * 3.33; 2529 - if (fade_t > 1.0) fade_t = 1.0; 2530 - int hour = get_la_hour(); 2531 - int is_day = (hour >= 7 && hour < 18); // light mode 7am-6pm 2532 - int target_r, target_g, target_b; 2533 - if (is_day) { 2534 - // Light mode: soft warm backgrounds 2535 - if (hour >= 7 && hour < 12) { 2536 - target_r = 235; target_g = 230; target_b = 220; // morning cream 2537 - } else { 2538 - target_r = 240; target_g = 235; target_b = 215; // afternoon warm white 2539 - } 2540 - } else { 2541 - // Dark mode: rich atmospheric backgrounds 2542 - if (hour >= 5 && hour < 7) { 2543 - target_r = 100; target_g = 45; target_b = 20; // sunrise orange 2544 - } else if (hour >= 18 && hour < 20) { 2545 - target_r = 80; target_g = 25; target_b = 60; // sunset purple 2546 - } else { 2547 - target_r = 15; target_g = 15; target_b = 40; // night deep blue 2548 - } 2549 - } 2550 - int start_r = is_day ? 255 : 0; 2551 - int start_g = is_day ? 255 : 0; 2552 - int start_b = is_day ? 255 : 0; 2553 - int bg_r = start_r + (int)((target_r - start_r) * fade_t); 2554 - int bg_g = start_g + (int)((target_g - start_g) * fade_t); 2555 - int bg_b = start_b + (int)((target_b - start_b) * fade_t); 2556 - graph_wipe(graph, (ACColor){(uint8_t)bg_r, (uint8_t)bg_g, (uint8_t)bg_b, 255}); 2557 - 2558 - // Matrix-rain BG: pseudo-CJK glyphs falling in columns. Started 2559 - // as an homage to the "garbled-character" bug we hit when the 2560 - // firmware loaded a chain-loading EFI binary directly — user 2561 - // liked the look, so we're keeping it as a boot aesthetic. 2562 - // Glyphs are random 7x9 pixel patterns drawn per-frame with a 2563 - // per-column xorshift seed; no CJK font needed. Brighter leader 2564 - // + fading trail for the classic terminal-rain feel. 2565 - { 2566 - int col_w = 10; 2567 - int glyph_w = 7, glyph_h = 9; 2568 - int nc = screen->width / col_w; 2569 - if (nc > RAIN_MAX_COLS) nc = RAIN_MAX_COLS; 2570 - // Slight fade toward title, so rain doesn't fight the text. 2571 - int rain_alpha_scale = is_day ? 80 : 180; 2572 - for (int c = 0; c < nc; c++) { 2573 - rain_col_y[c] += rain_col_speed[c]; 2574 - if (rain_col_y[c] > screen->height + glyph_h * 12) { 2575 - unsigned int s = rain_col_seed[c]; 2576 - s ^= s << 13; s ^= s >> 17; s ^= s << 5; 2577 - rain_col_seed[c] = s; 2578 - rain_col_y[c] = -(int)(s % 300); 2579 - rain_col_speed[c] = 2 + (int)(s % 4); 2580 - } 2581 - int cx = c * col_w + 1; 2582 - // Trail of ~10 glyphs fading behind the leader. 2583 - for (int trail = 0; trail < 10; trail++) { 2584 - int gy = rain_col_y[c] - trail * glyph_h; 2585 - if (gy < -glyph_h || gy >= screen->height) continue; 2586 - int tb = (trail == 0) ? 220 : 180 - trail * 18; 2587 - if (tb < 15) continue; 2588 - int b = (tb * rain_alpha_scale) >> 8; 2589 - // Leader glows a bit whitish — classic matrix-rain tip. 2590 - ACColor cg = (trail == 0) 2591 - ? (ACColor){ (uint8_t)(b * 0.8), 255, (uint8_t)(b * 0.8), (uint8_t)b } 2592 - : (ACColor){ 40, (uint8_t)(b * 0.9 + 20), (uint8_t)(b * 0.4), (uint8_t)b }; 2593 - graph_ink(graph, cg); 2594 - // Random 7x9 bitmap from deterministic seed + column + trail + slow frame shimmer. 2595 - unsigned int gseed = rain_col_seed[c] ^ ((unsigned)trail * 2654435761u) ^ ((unsigned)(f / 4) * 2246822519u); 2596 - for (int row = 0; row < glyph_h; row++) { 2597 - for (int col = 0; col < glyph_w; col++) { 2598 - gseed ^= gseed << 13; gseed ^= gseed >> 17; gseed ^= gseed << 5; 2599 - if (gseed & 1) graph_plot(graph, cx + col, gy + row); 2600 - } 2601 - } 2602 - } 2603 - } 2604 - } 2605 - 2606 - // Title — per-handle palette (fallback rainbow), animated pulse 2607 - int alpha = (int)(255.0 * fade_t); 2608 - if (alpha > 0) { 2609 - const char *title = boot_title; 2610 - // Auto-scale: use 3x unless title is too wide, then 2x 2611 - int scale = 3; 2612 - int tw = font_measure_matrix(title, scale); 2613 - if (tw > screen->width - 20) { 2614 - scale = 2; 2615 - tw = font_measure_matrix(title, scale); 2616 - } 2617 - int tx = (screen->width - tw) / 2; 2618 - int ty = screen->height / 2 - 20; 2619 - for (int ci = 0; title[ci]; ci++) { 2620 - ACColor cc = title_char_color(ci, f, alpha); 2621 - graph_ink(graph, cc); 2622 - char ch[2] = { title[ci], 0 }; 2623 - tx = font_draw_matrix(graph, ch, tx, ty, scale); 2624 - } 2625 - } 2626 - 2627 - // Version + build name + build date (high-contrast panel, top-right) 2514 + // Per-frame render lives in src/boot_anim.c now (shared with the 2515 + // macOS host). This call is everything between the old 2516 + // "Fade from black/white..." and the install-hint block. 2517 + BootAnimConfig anim_cfg = { 2518 + .title = boot_title, 2519 + .city = greet_city, 2520 + .title_colors = (boot_title_colors_len > 0) ? boot_title_colors : NULL, 2521 + .title_colors_len = boot_title_colors_len, 2522 + .hour = get_la_hour(), 2628 2523 #ifdef AC_GIT_HASH 2629 - if (alpha > 40) { 2630 - char ver[64]; 2631 - char bts[64]; 2632 - char bname[64] = ""; 2633 - char ddrv[64] = ""; 2634 - snprintf(ver, sizeof(ver), "version %s", AC_GIT_HASH); 2524 + .git_hash = AC_GIT_HASH, 2525 + #else 2526 + .git_hash = NULL, 2527 + #endif 2635 2528 #ifdef AC_BUILD_TS 2636 - snprintf(bts, sizeof(bts), "%s", AC_BUILD_TS); 2529 + .build_ts = AC_BUILD_TS, 2637 2530 #else 2638 - snprintf(bts, sizeof(bts), "build unknown"); 2531 + .build_ts = NULL, 2639 2532 #endif 2640 2533 #ifdef AC_BUILD_NAME 2641 - snprintf(bname, sizeof(bname), "%s", AC_BUILD_NAME); 2642 - #endif 2643 - const char *driver = drm_display_driver(display); 2644 - snprintf(ddrv, sizeof(ddrv), "display %s", driver); 2645 - int wv = font_measure_matrix(ver, 1); 2646 - int wt = font_measure_matrix(bts, 1); 2647 - int wn = bname[0] ? font_measure_matrix(bname, 1) : 0; 2648 - int wd = font_measure_matrix(ddrv, 1); 2649 - int max_w = wv; 2650 - if (wt > max_w) max_w = wt; 2651 - if (wn > max_w) max_w = wn; 2652 - if (wd > max_w) max_w = wd; 2653 - int panel_w = max_w + 8; 2654 - int panel_h = (bname[0] ? 28 : 20) + 8; 2655 - int panel_x = screen->width - panel_w - 3; 2656 - int panel_y = 3; 2657 - graph_ink(graph, is_day 2658 - ? (ACColor){255, 255, 255, (uint8_t)(alpha * 0.7)} 2659 - : (ACColor){0, 0, 0, (uint8_t)(alpha * 0.82)}); 2660 - graph_box(graph, panel_x, panel_y, panel_w, panel_h, 1); 2661 - // Build name (top line) 2662 - if (bname[0]) { 2663 - graph_ink(graph, is_day 2664 - ? (ACColor){140, 100, 0, (uint8_t)alpha} 2665 - : (ACColor){255, 200, 60, (uint8_t)alpha}); 2666 - font_draw_matrix(graph, bname, panel_x + 4, panel_y + 3, 1); 2667 - } 2668 - int line_y = panel_y + (bname[0] ? 11 : 3); 2669 - graph_ink(graph, is_day 2670 - ? (ACColor){60, 60, 60, (uint8_t)alpha} 2671 - : (ACColor){255, 255, 255, (uint8_t)alpha}); 2672 - font_draw_matrix(graph, ver, panel_x + 4, line_y, 1); 2673 - graph_ink(graph, is_day 2674 - ? (ACColor){80, 100, 90, (uint8_t)alpha} 2675 - : (ACColor){210, 235, 220, (uint8_t)alpha}); 2676 - font_draw_matrix(graph, bts, panel_x + 4, line_y + 8, 1); 2677 - // Display driver line 2678 - graph_ink(graph, is_day 2679 - ? (ACColor){90, 60, 120, (uint8_t)alpha} 2680 - : (ACColor){180, 160, 255, (uint8_t)alpha}); 2681 - font_draw_matrix(graph, ddrv, panel_x + 4, line_y + 16, 1); 2682 - // "FRESH" badge when first boot of this version 2683 - if (is_new_version) { 2684 - graph_ink(graph, is_day 2685 - ? (ACColor){0, 140, 60, (uint8_t)alpha} 2686 - : (ACColor){80, 255, 120, (uint8_t)alpha}); 2687 - font_draw_matrix(graph, "FRESH", panel_x - font_measure_matrix("FRESH", 1) - 4, panel_y + 6, 1); 2688 - } 2689 - } 2534 + .build_name = AC_BUILD_NAME, 2535 + #else 2536 + .build_name = NULL, 2690 2537 #endif 2691 - 2692 - // Subtitle: "enjoy <city>!" appears after frame 130 2693 - // Scaled for 120-frame total (was gated at 130 of 180). 2694 - if (f > 80) { 2695 - double sub_t = (double)(f - 80) / 20.0; 2696 - if (sub_t > 1.0) sub_t = 1.0; 2697 - int sub_alpha = (int)(180.0 * sub_t); 2698 - graph_ink(graph, is_day 2699 - ? (ACColor){120, 100, 80, (uint8_t)sub_alpha} 2700 - : (ACColor){220, 180, 140, (uint8_t)sub_alpha}); 2701 - char subtitle[128]; 2702 - snprintf(subtitle, sizeof subtitle, "enjoy %s!", greet_city); 2703 - int sw = font_measure_matrix(subtitle, 1); 2704 - font_draw_matrix(graph, subtitle, 2705 - (screen->width - sw) / 2, screen->height / 2 + 10, 1); 2706 - } 2707 - 2708 - // Auth badges (bottom-left): pixel crab = Claude, pixel octocat = GitHub 2709 - // Badges — scaled to appear earlier in the shorter animation. 2710 - if (f > 40 && alpha > 80) { 2711 - int badge_x = 6; 2712 - int badge_y = screen->height - 22; 2713 - double badge_t = (double)(f - 40) / 30.0; 2714 - if (badge_t > 1.0) badge_t = 1.0; 2715 - int ba = (int)(220.0 * badge_t); // badge alpha 2538 + .driver_name = drm_display_driver(display), 2539 + .is_new_version = is_new_version, 2540 + .show_install = show_install, 2541 + .is_installed = is_installed_on_hd(), 2542 + .has_claude_badge = (access("/claude-token", F_OK) == 0 || 2543 + getenv("CLAUDE_CODE_OAUTH_TOKEN") != NULL), 2544 + .has_github_badge = (access("/github-pat", F_OK) == 0 || 2545 + getenv("GH_TOKEN") != NULL), 2546 + .title_scale = 0, // legacy auto — preserve on-hardware look 2547 + }; 2548 + boot_anim_render_frame(graph, screen, f, &anim_cfg, &rain_state); 2716 2549 2717 - // 11x9 pixel crab (Claude/Anthropic) 2718 - if (access("/claude-token", F_OK) == 0 || getenv("CLAUDE_CODE_OAUTH_TOKEN")) { 2719 - static const char crab[9][12] = { 2720 - " . . ", 2721 - " . . ", 2722 - " ..##.##.. ", 2723 - ".# #### #.", 2724 - ". ####### .", 2725 - " ####### ", 2726 - " ## . ## ", 2727 - " . . ", 2728 - " . . ", 2729 - }; 2730 - for (int cy = 0; cy < 9; cy++) 2731 - for (int cx = 0; cx < 11; cx++) { 2732 - char c = crab[cy][cx]; 2733 - if (c == '#') 2734 - graph_ink(graph, (ACColor){255, 120, 50, (uint8_t)ba}); 2735 - else if (c == '.') 2736 - graph_ink(graph, (ACColor){200, 90, 30, (uint8_t)(ba*2/3)}); 2737 - else continue; 2738 - graph_box(graph, badge_x + cx*2, badge_y + cy*2, 2, 2, 1); 2739 - } 2740 - badge_x += 28; 2741 - } 2742 - // 11x11 pixel octocat (GitHub) 2743 - if (access("/github-pat", F_OK) == 0 || getenv("GH_TOKEN")) { 2744 - static const char octo[11][12] = { 2745 - " .###. ", 2746 - " ####### ", 2747 - " ## o#o ## ", 2748 - " ######### ", 2749 - " ## ### ## ", 2750 - " ####### ", 2751 - " ##### ", 2752 - " .# . #. ", 2753 - " .# . #. ", 2754 - " . . . ", 2755 - ". . .", 2756 - }; 2757 - for (int cy = 0; cy < 11; cy++) 2758 - for (int cx = 0; cx < 11; cx++) { 2759 - char c = octo[cy][cx]; 2760 - if (c == '#') 2761 - graph_ink(graph, (ACColor){180, 210, 255, (uint8_t)ba}); 2762 - else if (c == 'o') 2763 - graph_ink(graph, (ACColor){60, 80, 120, (uint8_t)ba}); 2764 - else if (c == '.') 2765 - graph_ink(graph, (ACColor){120, 150, 200, (uint8_t)(ba*2/3)}); 2766 - else continue; 2767 - graph_box(graph, badge_x + cx*2, badge_y + cy*2, 2, 2, 1); 2768 - } 2769 - badge_x += 28; 2770 - } 2771 - } 2772 - 2773 - // Animated triangles — geometric decoration 2774 - if (alpha > 30) { 2775 - int tri_alpha = (int)(alpha * 0.15); 2776 - int W = screen->width; 2777 - int H = screen->height; 2778 - // Drifting triangles based on frame counter 2779 - for (int ti = 0; ti < 6; ti++) { 2780 - double phase = (double)f * 0.02 + ti * 1.047; // 60° apart 2781 - int cx = (int)(W * 0.5 + W * 0.35 * sin(phase + 1.5708)); 2782 - int cy = (int)(H * 0.5 + H * 0.3 * sin(phase * 0.7)); 2783 - int sz = 8 + ti * 3 + (int)(4.0 * sin(f * 0.05 + ti)); 2784 - ACColor tc = is_day 2785 - ? (ACColor){180 - ti*15, 140 - ti*10, 120, (uint8_t)tri_alpha} 2786 - : (ACColor){80 + ti*20, 60 + ti*15, 120 + ti*10, (uint8_t)tri_alpha}; 2787 - graph_ink(graph, tc); 2788 - // Draw triangle as 3 lines 2789 - int x0 = cx, y0 = cy - sz; 2790 - int x1 = cx - sz, y1 = cy + sz/2; 2791 - int x2 = cx + sz, y2 = cy + sz/2; 2792 - graph_line(graph, x0, y0, x1, y1); 2793 - graph_line(graph, x1, y1, x2, y2); 2794 - graph_line(graph, x2, y2, x0, y0); 2795 - } 2796 - } 2797 - 2798 - // Bottom: shrinking time bar 2799 - int bar_full = screen->width - 40; 2800 - int bar_remaining = (int)((1.0 - t) * bar_full); 2801 - if (bar_remaining > 0) { 2802 - graph_ink(graph, (ACColor){200, 150, 180, (uint8_t)(80 * (1.0 - t))}); 2803 - graph_box(graph, 20, screen->height - 6, bar_remaining, 3, 1); 2804 - } 2805 - 2806 - // Animated W install prompt 2807 - if (alpha > 100 && show_install) { 2808 - // Pulsing box behind the text 2809 - double pulse = 0.5 + 0.5 * sin(f * 0.1); 2810 - int pa = (int)(40 + 30 * pulse); 2811 - const char *hint = is_installed_on_hd() 2812 - ? "W: update" : "W: install to disk"; 2813 - int hw = font_measure_matrix(hint, 1); 2814 - int hx = (screen->width - hw) / 2; 2815 - int hy = screen->height - 20; 2816 - // Pulsing background pill 2817 - graph_ink(graph, is_day 2818 - ? (ACColor){200, 160, 120, (uint8_t)pa} 2819 - : (ACColor){60, 40, 80, (uint8_t)pa}); 2820 - graph_box(graph, hx - 4, hy - 2, hw + 8, 12, 1); 2821 - // Triangle arrow pointing down at the text 2822 - int ax = hx - 10; 2823 - int ay = hy + 3; 2824 - graph_ink(graph, is_day 2825 - ? (ACColor){180, 120, 60, (uint8_t)(alpha / 2)} 2826 - : (ACColor){200, 150, 255, (uint8_t)(alpha / 2)}); 2827 - graph_line(graph, ax, ay - 3, ax, ay + 3); 2828 - graph_line(graph, ax, ay + 3, ax - 3, ay); 2829 - // Text with higher contrast 2830 - graph_ink(graph, is_day 2831 - ? (ACColor){120, 60, 0, (uint8_t)(alpha * 2 / 3)} 2832 - : (ACColor){220, 180, 255, (uint8_t)(alpha * 2 / 3)}); 2833 - font_draw_matrix(graph, hint, hx, hy, 1); 2834 - } 2835 2550 2836 2551 ac_display_present(display, screen, pixel_scale); 2837 2552 frame_sync_60fps(&anim_time);
+381
fedac/native/src/boot_anim.c
··· 1 + // boot_anim.c — extracted render loop from ac-native.c:draw_startup_fade. 2 + // Stateless per-frame: caller owns the framebuffer, the key-drain, the 3 + // display present, and the rain state. See boot_anim.h for the contract. 4 + #include "boot_anim.h" 5 + #include "font.h" 6 + 7 + #include <math.h> 8 + #include <stdio.h> 9 + #include <string.h> 10 + 11 + static uint8_t clamp_u8(int v) { 12 + if (v < 0) return 0; 13 + if (v > 255) return 255; 14 + return (uint8_t)v; 15 + } 16 + 17 + // Rainbow fallback for characters not covered by a custom palette. 18 + // HSV → RGB with fixed saturation/value and hue rotating with frame. 19 + static ACColor rainbow_title_color(int ci, int frame, int alpha) { 20 + double hue = fmod((double)ci / 7.0 * 360.0 + frame * 2.0, 360.0); 21 + double h6 = hue / 60.0; 22 + int hi = (int)h6 % 6; 23 + double fr = h6 - (int)h6; 24 + double sv = 0.7, vv = 1.0; 25 + double p = vv * (1.0 - sv); 26 + double q = vv * (1.0 - sv * fr); 27 + double tt = vv * (1.0 - sv * (1.0 - fr)); 28 + double cr = 0, cg = 0, cb = 0; 29 + switch (hi) { 30 + case 0: cr = vv; cg = tt; cb = p; break; 31 + case 1: cr = q; cg = vv; cb = p; break; 32 + case 2: cr = p; cg = vv; cb = tt; break; 33 + case 3: cr = p; cg = q; cb = vv; break; 34 + case 4: cr = tt; cg = p; cb = vv; break; 35 + default: cr = vv; cg = p; cb = q; break; 36 + } 37 + return (ACColor){(uint8_t)(cr * 255), (uint8_t)(cg * 255), (uint8_t)(cb * 255), 38 + clamp_u8(alpha)}; 39 + } 40 + 41 + // Per-character palette lookup. If the config has a color list, apply it 42 + // to chars after the first '@' (the handle); otherwise fall back to 43 + // rainbow. Palette colors get a small sinusoidal pulse for liveness and 44 + // are lifted toward white so dark boot backgrounds don't wash them out. 45 + static ACColor title_char_color(int ci, int frame, int alpha, 46 + const char *title, 47 + const ACColor *pal, int pal_len) { 48 + if (pal_len <= 0 || !pal) return rainbow_title_color(ci, frame, alpha); 49 + const char *at = title ? strchr(title, '@') : NULL; 50 + int handle_start = at ? (int)(at - title) + 1 : 0; 51 + if (ci < handle_start) return rainbow_title_color(ci, frame, alpha); 52 + int idx = ci - handle_start; 53 + if (idx < 0) idx = 0; 54 + idx %= pal_len; 55 + ACColor c = pal[idx]; 56 + int pulse = (int)(18.0 * sin((double)(frame + ci * 6) * 0.08)); 57 + int r = (c.r * 7 + 255 * 3) / 10 + pulse; 58 + int g = (c.g * 7 + 255 * 3) / 10 + pulse; 59 + int b = (c.b * 7 + 255 * 3) / 10 + pulse; 60 + return (ACColor){clamp_u8(r), clamp_u8(g), clamp_u8(b), clamp_u8(alpha)}; 61 + } 62 + 63 + static void seed_rain(BootAnimState *st) { 64 + // Keep deterministic-ish by xorshift32-ing a fixed seed rather than 65 + // time(NULL) — screenshot runs should be reproducible. 66 + unsigned int s = 0xC1A9E5A3u; 67 + for (int c = 0; c < BOOT_ANIM_RAIN_MAX_COLS; c++) { 68 + s ^= s << 13; s ^= s >> 17; s ^= s << 5; 69 + st->col_y[c] = -(int)(s % 400); 70 + s ^= s << 13; s ^= s >> 17; s ^= s << 5; 71 + st->col_speed[c] = 2 + (int)(s % 4); 72 + s ^= s << 13; s ^= s >> 17; s ^= s << 5; 73 + st->col_seed[c] = s; 74 + } 75 + st->initialized = 1; 76 + } 77 + 78 + void boot_anim_render_frame(ACGraph *graph, ACFramebuffer *screen, 79 + int f, const BootAnimConfig *cfg, 80 + BootAnimState *state) { 81 + if (!state->initialized) seed_rain(state); 82 + 83 + const int total_frames = BOOT_ANIM_FRAMES; 84 + double t = (double)f / (double)total_frames; 85 + 86 + // ── Background fade (first 0.3 s) ──────────────────────────────── 87 + double fade_t = t * 3.33; 88 + if (fade_t > 1.0) fade_t = 1.0; 89 + int hour = cfg->hour; 90 + int is_day = (hour >= 7 && hour < 18); 91 + int target_r, target_g, target_b; 92 + if (is_day) { 93 + if (hour >= 7 && hour < 12) { 94 + target_r = 235; target_g = 230; target_b = 220; // morning cream 95 + } else { 96 + target_r = 240; target_g = 235; target_b = 215; // afternoon warm white 97 + } 98 + } else { 99 + if (hour >= 5 && hour < 7) { 100 + target_r = 100; target_g = 45; target_b = 20; // sunrise orange 101 + } else if (hour >= 18 && hour < 20) { 102 + target_r = 80; target_g = 25; target_b = 60; // sunset purple 103 + } else { 104 + target_r = 15; target_g = 15; target_b = 40; // night deep blue 105 + } 106 + } 107 + int start_r = is_day ? 255 : 0; 108 + int start_g = is_day ? 255 : 0; 109 + int start_b = is_day ? 255 : 0; 110 + int bg_r = start_r + (int)((target_r - start_r) * fade_t); 111 + int bg_g = start_g + (int)((target_g - start_g) * fade_t); 112 + int bg_b = start_b + (int)((target_b - start_b) * fade_t); 113 + graph_wipe(graph, (ACColor){(uint8_t)bg_r, (uint8_t)bg_g, (uint8_t)bg_b, 255}); 114 + 115 + // ── Matrix rain BG ─────────────────────────────────────────────── 116 + { 117 + int col_w = 10; 118 + int glyph_w = 7, glyph_h = 9; 119 + int nc = screen->width / col_w; 120 + if (nc > BOOT_ANIM_RAIN_MAX_COLS) nc = BOOT_ANIM_RAIN_MAX_COLS; 121 + int rain_alpha_scale = is_day ? 80 : 180; 122 + for (int c = 0; c < nc; c++) { 123 + state->col_y[c] += state->col_speed[c]; 124 + if (state->col_y[c] > screen->height + glyph_h * 12) { 125 + unsigned int s = state->col_seed[c]; 126 + s ^= s << 13; s ^= s >> 17; s ^= s << 5; 127 + state->col_seed[c] = s; 128 + state->col_y[c] = -(int)(s % 300); 129 + state->col_speed[c] = 2 + (int)(s % 4); 130 + } 131 + int cx = c * col_w + 1; 132 + for (int trail = 0; trail < 10; trail++) { 133 + int gy = state->col_y[c] - trail * glyph_h; 134 + if (gy < -glyph_h || gy >= screen->height) continue; 135 + int tb = (trail == 0) ? 220 : 180 - trail * 18; 136 + if (tb < 15) continue; 137 + int b = (tb * rain_alpha_scale) >> 8; 138 + ACColor cg = (trail == 0) 139 + ? (ACColor){ (uint8_t)(b * 0.8), 255, (uint8_t)(b * 0.8), (uint8_t)b } 140 + : (ACColor){ 40, (uint8_t)(b * 0.9 + 20), (uint8_t)(b * 0.4), (uint8_t)b }; 141 + graph_ink(graph, cg); 142 + unsigned int gseed = state->col_seed[c] 143 + ^ ((unsigned)trail * 2654435761u) 144 + ^ ((unsigned)(f / 4) * 2246822519u); 145 + for (int row = 0; row < glyph_h; row++) { 146 + for (int col = 0; col < glyph_w; col++) { 147 + gseed ^= gseed << 13; gseed ^= gseed >> 17; gseed ^= gseed << 5; 148 + if (gseed & 1) graph_plot(graph, cx + col, gy + row); 149 + } 150 + } 151 + } 152 + } 153 + } 154 + 155 + // ── Title (handle greeting) ────────────────────────────────────── 156 + int alpha = (int)(255.0 * fade_t); 157 + // MatrixChunky8 glyph height is ~8 px at scale 1, hence `* 8`. 158 + int title_scale = 0, title_y_bottom = screen->height / 2 - 20 + 8; 159 + if (alpha > 0 && cfg->title) { 160 + const char *title = cfg->title; 161 + int scale; 162 + if (cfg->title_scale > 0) { 163 + scale = cfg->title_scale; 164 + } else { 165 + // Legacy auto: prefer 3, drop to 2 if the title overflows. 166 + // Matches the on-hardware Linux boot aesthetic. 167 + scale = 3; 168 + if (font_measure_matrix(title, scale) > screen->width - 20) scale = 2; 169 + } 170 + int tw = font_measure_matrix(title, scale); 171 + int tx = (screen->width - tw) / 2; 172 + // Large titles push up so the subtitle can sit below without 173 + // colliding. (scale−3)·4 is the legacy anchor offset; bigger 174 + // scales also shift the baseline higher by a proportional amount. 175 + int ty = screen->height / 2 - 20 - (scale - 3) * 4; 176 + title_scale = scale; 177 + title_y_bottom = ty + 8 * scale; 178 + for (int ci = 0; title[ci]; ci++) { 179 + ACColor cc = title_char_color(ci, f, alpha, title, 180 + cfg->title_colors, 181 + cfg->title_colors_len); 182 + graph_ink(graph, cc); 183 + char ch[2] = { title[ci], 0 }; 184 + tx = font_draw_matrix(graph, ch, tx, ty, scale); 185 + } 186 + } 187 + 188 + // ── Version panel (top-right) ──────────────────────────────────── 189 + if (alpha > 40 && cfg->git_hash) { 190 + char ver[64]; char bts[64]; char bname[64] = ""; char ddrv[64] = ""; 191 + snprintf(ver, sizeof(ver), "version %s", cfg->git_hash); 192 + snprintf(bts, sizeof(bts), "%s", cfg->build_ts ? cfg->build_ts : "build unknown"); 193 + if (cfg->build_name) snprintf(bname, sizeof(bname), "%s", cfg->build_name); 194 + if (cfg->driver_name) snprintf(ddrv, sizeof(ddrv), "display %s", cfg->driver_name); 195 + 196 + int wv = font_measure_matrix(ver, 1); 197 + int wt = font_measure_matrix(bts, 1); 198 + int wn = bname[0] ? font_measure_matrix(bname, 1) : 0; 199 + int wd = ddrv[0] ? font_measure_matrix(ddrv, 1) : 0; 200 + int max_w = wv; 201 + if (wt > max_w) max_w = wt; 202 + if (wn > max_w) max_w = wn; 203 + if (wd > max_w) max_w = wd; 204 + int panel_w = max_w + 8; 205 + int panel_h = (bname[0] ? 28 : 20) + 8; 206 + int panel_x = screen->width - panel_w - 3; 207 + int panel_y = 3; 208 + graph_ink(graph, is_day 209 + ? (ACColor){255, 255, 255, (uint8_t)(alpha * 0.7)} 210 + : (ACColor){0, 0, 0, (uint8_t)(alpha * 0.82)}); 211 + graph_box(graph, panel_x, panel_y, panel_w, panel_h, 1); 212 + if (bname[0]) { 213 + graph_ink(graph, is_day 214 + ? (ACColor){140, 100, 0, (uint8_t)alpha} 215 + : (ACColor){255, 200, 60, (uint8_t)alpha}); 216 + font_draw_matrix(graph, bname, panel_x + 4, panel_y + 3, 1); 217 + } 218 + int line_y = panel_y + (bname[0] ? 11 : 3); 219 + graph_ink(graph, is_day 220 + ? (ACColor){60, 60, 60, (uint8_t)alpha} 221 + : (ACColor){255, 255, 255, (uint8_t)alpha}); 222 + font_draw_matrix(graph, ver, panel_x + 4, line_y, 1); 223 + graph_ink(graph, is_day 224 + ? (ACColor){80, 100, 90, (uint8_t)alpha} 225 + : (ACColor){210, 235, 220, (uint8_t)alpha}); 226 + font_draw_matrix(graph, bts, panel_x + 4, line_y + 8, 1); 227 + if (ddrv[0]) { 228 + graph_ink(graph, is_day 229 + ? (ACColor){90, 60, 120, (uint8_t)alpha} 230 + : (ACColor){180, 160, 255, (uint8_t)alpha}); 231 + font_draw_matrix(graph, ddrv, panel_x + 4, line_y + 16, 1); 232 + } 233 + if (cfg->is_new_version) { 234 + graph_ink(graph, is_day 235 + ? (ACColor){0, 140, 60, (uint8_t)alpha} 236 + : (ACColor){80, 255, 120, (uint8_t)alpha}); 237 + font_draw_matrix(graph, "FRESH", 238 + panel_x - font_measure_matrix("FRESH", 1) - 4, 239 + panel_y + 6, 1); 240 + } 241 + } 242 + 243 + // ── Subtitle "enjoy <city>!" (fades in after frame 80) ─────────── 244 + if (f > 80 && cfg->city && cfg->city[0]) { 245 + double sub_t = (double)(f - 80) / 20.0; 246 + if (sub_t > 1.0) sub_t = 1.0; 247 + int sub_alpha = (int)(180.0 * sub_t); 248 + graph_ink(graph, is_day 249 + ? (ACColor){120, 100, 80, (uint8_t)sub_alpha} 250 + : (ACColor){220, 180, 140, (uint8_t)sub_alpha}); 251 + // Scale subtitle with the title so proportions stay balanced — 252 + // big title + tiny subtitle looks wrong at screenshot resolutions. 253 + int sub_scale = title_scale >= 6 ? 2 : 1; 254 + char subtitle[128]; 255 + snprintf(subtitle, sizeof subtitle, "enjoy %s!", cfg->city); 256 + int sw = font_measure_matrix(subtitle, sub_scale); 257 + // Anchor below the title baseline so enlarged titles can't collide. 258 + int sy = title_y_bottom + 4; 259 + if (sy < screen->height / 2 + 10) sy = screen->height / 2 + 10; 260 + font_draw_matrix(graph, subtitle, 261 + (screen->width - sw) / 2, sy, sub_scale); 262 + } 263 + 264 + // ── Auth badges (bottom-left) ──────────────────────────────────── 265 + if (f > 40 && alpha > 80) { 266 + int badge_x = 6; 267 + int badge_y = screen->height - 22; 268 + double badge_t = (double)(f - 40) / 30.0; 269 + if (badge_t > 1.0) badge_t = 1.0; 270 + int ba = (int)(220.0 * badge_t); 271 + 272 + if (cfg->has_claude_badge) { 273 + static const char crab[9][12] = { 274 + " . . ", 275 + " . . ", 276 + " ..##.##.. ", 277 + ".# #### #.", 278 + ". ####### .", 279 + " ####### ", 280 + " ## . ## ", 281 + " . . ", 282 + " . . ", 283 + }; 284 + for (int cy = 0; cy < 9; cy++) 285 + for (int cx = 0; cx < 11; cx++) { 286 + char c = crab[cy][cx]; 287 + if (c == '#') 288 + graph_ink(graph, (ACColor){255, 120, 50, (uint8_t)ba}); 289 + else if (c == '.') 290 + graph_ink(graph, (ACColor){200, 90, 30, (uint8_t)(ba * 2 / 3)}); 291 + else continue; 292 + graph_box(graph, badge_x + cx * 2, badge_y + cy * 2, 2, 2, 1); 293 + } 294 + badge_x += 28; 295 + } 296 + if (cfg->has_github_badge) { 297 + static const char octo[11][12] = { 298 + " .###. ", 299 + " ####### ", 300 + " ## o#o ## ", 301 + " ######### ", 302 + " ## ### ## ", 303 + " ####### ", 304 + " ##### ", 305 + " .# . #. ", 306 + " .# . #. ", 307 + " . . . ", 308 + ". . .", 309 + }; 310 + for (int cy = 0; cy < 11; cy++) 311 + for (int cx = 0; cx < 11; cx++) { 312 + char c = octo[cy][cx]; 313 + if (c == '#') 314 + graph_ink(graph, (ACColor){180, 210, 255, (uint8_t)ba}); 315 + else if (c == 'o') 316 + graph_ink(graph, (ACColor){60, 80, 120, (uint8_t)ba}); 317 + else if (c == '.') 318 + graph_ink(graph, (ACColor){120, 150, 200, (uint8_t)(ba * 2 / 3)}); 319 + else continue; 320 + graph_box(graph, badge_x + cx * 2, badge_y + cy * 2, 2, 2, 1); 321 + } 322 + badge_x += 28; 323 + } 324 + } 325 + 326 + // ── Drifting triangles (atmospheric decor) ─────────────────────── 327 + if (alpha > 30) { 328 + int tri_alpha = (int)(alpha * 0.15); 329 + int W = screen->width; 330 + int H = screen->height; 331 + for (int ti = 0; ti < 6; ti++) { 332 + double phase = (double)f * 0.02 + ti * 1.047; 333 + int cx = (int)(W * 0.5 + W * 0.35 * sin(phase + 1.5708)); 334 + int cy = (int)(H * 0.5 + H * 0.3 * sin(phase * 0.7)); 335 + int sz = 8 + ti * 3 + (int)(4.0 * sin(f * 0.05 + ti)); 336 + ACColor tc = is_day 337 + ? (ACColor){180 - ti*15, 140 - ti*10, 120, (uint8_t)tri_alpha} 338 + : (ACColor){80 + ti*20, 60 + ti*15, 120 + ti*10, (uint8_t)tri_alpha}; 339 + graph_ink(graph, tc); 340 + int x0 = cx, y0 = cy - sz; 341 + int x1 = cx - sz, y1 = cy + sz / 2; 342 + int x2 = cx + sz, y2 = cy + sz / 2; 343 + graph_line(graph, x0, y0, x1, y1); 344 + graph_line(graph, x1, y1, x2, y2); 345 + graph_line(graph, x2, y2, x0, y0); 346 + } 347 + } 348 + 349 + // ── Shrinking time bar at the bottom ───────────────────────────── 350 + int bar_full = screen->width - 40; 351 + int bar_remaining = (int)((1.0 - t) * bar_full); 352 + if (bar_remaining > 0) { 353 + graph_ink(graph, (ACColor){200, 150, 180, (uint8_t)(80 * (1.0 - t))}); 354 + graph_box(graph, 20, screen->height - 6, bar_remaining, 3, 1); 355 + } 356 + 357 + // ── Install hint pill (when running from USB) ──────────────────── 358 + if (alpha > 100 && cfg->show_install) { 359 + double pulse = 0.5 + 0.5 * sin(f * 0.1); 360 + int pa = (int)(40 + 30 * pulse); 361 + const char *hint = cfg->is_installed ? "W: update" : "W: install to disk"; 362 + int hw = font_measure_matrix(hint, 1); 363 + int hx = (screen->width - hw) / 2; 364 + int hy = screen->height - 20; 365 + graph_ink(graph, is_day 366 + ? (ACColor){200, 160, 120, (uint8_t)pa} 367 + : (ACColor){60, 40, 80, (uint8_t)pa}); 368 + graph_box(graph, hx - 4, hy - 2, hw + 8, 12, 1); 369 + int ax = hx - 10; 370 + int ay = hy + 3; 371 + graph_ink(graph, is_day 372 + ? (ACColor){180, 120, 60, (uint8_t)(alpha / 2)} 373 + : (ACColor){200, 150, 255, (uint8_t)(alpha / 2)}); 374 + graph_line(graph, ax, ay - 3, ax, ay + 3); 375 + graph_line(graph, ax, ay + 3, ax - 3, ay); 376 + graph_ink(graph, is_day 377 + ? (ACColor){120, 60, 0, (uint8_t)(alpha * 2 / 3)} 378 + : (ACColor){220, 180, 255, (uint8_t)(alpha * 2 / 3)}); 379 + font_draw_matrix(graph, hint, hx, hy, 1); 380 + } 381 + }
+77
fedac/native/src/boot_anim.h
··· 1 + // boot_anim.h — host-independent renderer for the ac-native boot animation. 2 + // 3 + // The Linux PID-1 init path (src/ac-native.c) and the macOS host 4 + // (macos/main.c) both invoke this to paint a frame of the boot greeting 5 + // (handle title, city subtitle, matrix rain, version panel, badges, 6 + // optional install hint). All Linux-only concerns — evdev key drain, 7 + // /mnt writes, machines_init, DRM display present — stay in the caller; 8 + // this module only touches ACGraph + ACFramebuffer. 9 + // 10 + // Caller owns frame-timing and display present. To replay the animation 11 + // once, loop f from 0 to BOOT_ANIM_FRAMES and call boot_anim_render_frame 12 + // each tick. 13 + #ifndef AC_BOOT_ANIM_H 14 + #define AC_BOOT_ANIM_H 15 + 16 + #include "graph.h" 17 + 18 + #define BOOT_ANIM_FRAMES 120 // 2 s @ 60 fps 19 + #define BOOT_ANIM_HOLD_FRAMES 40 // input ignored before this frame 20 + #define BOOT_ANIM_RAIN_MAX_COLS 128 21 + 22 + typedef struct { 23 + // Required. 24 + const char *title; // e.g. "hi @jeffrey" — drawn centered 25 + const char *city; // e.g. "Los Angeles" — "enjoy <city>!" 26 + 27 + // Optional per-handle palette. NULL → rainbow. When set, applies to 28 + // chars after the first '@' in title; chars before stay rainbow. 29 + const ACColor *title_colors; 30 + int title_colors_len; 31 + 32 + // Hour-of-day drives the background fade + palette. Pass the LA hour 33 + // (0–23) or whatever locale you want to key the color scheme off. 34 + int hour; 35 + 36 + // Version panel (top-right). Any NULL field hides that line; pass 37 + // all-NULL to suppress the panel entirely. `is_new_version` shows the 38 + // "FRESH" badge next to the panel. 39 + const char *git_hash; 40 + const char *build_ts; 41 + const char *build_name; 42 + const char *driver_name; 43 + int is_new_version; 44 + 45 + // Install hint (bottom-center). `show_install` gates visibility; 46 + // `is_installed` picks between "W: update" and "W: install to disk". 47 + int show_install; 48 + int is_installed; 49 + 50 + // Auth badges (bottom-left). Caller decides eligibility (e.g. via 51 + // access("/claude-token") on Linux or env checks on macOS). 52 + int has_claude_badge; 53 + int has_github_badge; 54 + 55 + // Title scale override. 0 = legacy auto (tries 3, falls back to 2 if 56 + // it overflows width — matches Linux hardware look). Any positive 57 + // value forces that scale for MatrixChunky8 — useful for big 58 + // marketing shots where the handle should dominate the frame. 59 + int title_scale; 60 + } BootAnimConfig; 61 + 62 + // Persistent per-column state for the matrix rain. Zero-initialize before 63 + // the first render frame (boot_anim_render_frame auto-seeds on first use). 64 + typedef struct { 65 + int col_y[BOOT_ANIM_RAIN_MAX_COLS]; 66 + int col_speed[BOOT_ANIM_RAIN_MAX_COLS]; 67 + unsigned int col_seed[BOOT_ANIM_RAIN_MAX_COLS]; 68 + int initialized; 69 + } BootAnimState; 70 + 71 + // Render frame `f` (0-indexed, must be < BOOT_ANIM_FRAMES) into `graph`/`screen`. 72 + // State is mutable — caller keeps it across frames for rain animation. 73 + void boot_anim_render_frame(ACGraph *graph, ACFramebuffer *screen, 74 + int f, const BootAnimConfig *cfg, 75 + BootAnimState *state); 76 + 77 + #endif