Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
13 kB · 449 lines
C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450// kidlisp.c - KidLisp Playdate Runtime Implementation// Provides the graphics, input, and math API for KidLisp programs
#include <stdlib.h>#include <math.h>#include <string.h>#include "kidlisp.h"
// Global statestatic KidLispState state = {0};
// Fontsstatic LCDFont* default_font = NULL;static LCDFont* small_font = NULL;static const char* font_path = "/System/Fonts/Asheville-Sans-14-Bold.pft";static const char* small_font_path = "/System/Fonts/Roobert-10-Bold.pft";
// HUD buffer (separate bitmap composited on top)static LCDBitmap* hud_buffer = NULL;static const int HUD_HEIGHT = 80;
// QR Code for https://kidlisp.com (25x25)#define QR_SIZE 25static const uint8_t qr_data[] = { 0xFE, 0x9A, 0x3F, 0x80, 0x82, 0x60, 0xA0, 0x80, 0xBA, 0x2E, 0xAE, 0x80, 0xBA, 0x0C, 0xAE, 0x80, 0xBA, 0x56, 0xAE, 0x80, 0x82, 0x2A, 0xA0, 0x80, 0xFE, 0xAA, 0xBF, 0x80, 0x00, 0xB5, 0x80, 0x00, 0xDA, 0x74, 0xA0, 0x80, 0x59, 0x4F, 0x9F, 0x00, 0x7E, 0xB5, 0x54, 0x80, 0x45, 0xFC, 0xAF, 0x80, 0xAF, 0x5D, 0x30, 0x80, 0xD1, 0xC7, 0x89, 0x00, 0xFA, 0x9D, 0x6F, 0x80, 0x88, 0xB1, 0xB6, 0x80, 0xB3, 0x63, 0xFB, 0x00, 0x00, 0x9F, 0x8B, 0x00, 0xFE, 0x70, 0xA8, 0x80, 0x82, 0x0B, 0x89, 0x00, 0xBA, 0xAF, 0xF8, 0x80, 0xBA, 0xC3, 0xE1, 0x80, 0xBA, 0x4B, 0xCF, 0x80, 0x82, 0xC2, 0x1B, 0x80, 0xFE, 0xA6, 0xE4, 0x80,};
void kl_init(PlaydateAPI* pd) { state.pd = pd; state.frame = 0; state.ink_color = KL_BLACK; state.crank_angle = 0; state.crank_delta = 0; state.current_buttons = 0; state.pressed_buttons = 0; state.released_buttons = 0; // Load default font const char* err; default_font = pd->graphics->loadFont(font_path, &err); if (default_font == NULL) { pd->system->logToConsole("Failed to load font: %s", err); } // Load small font for HUD small_font = pd->graphics->loadFont(small_font_path, &err); if (small_font == NULL) { pd->system->logToConsole("Failed to load small font: %s", err); small_font = default_font; // Fallback } // Seed random with current time srand(pd->system->getCurrentTimeMilliseconds());}
void kl_update(void) { state.frame++; // Update crank state.crank_delta = state.pd->system->getCrankChange(); state.crank_angle = state.pd->system->getCrankAngle(); // Update buttons PDButtons current, pressed, released; state.pd->system->getButtonState(¤t, &pressed, &released); state.current_buttons = current; state.pressed_buttons = pressed; state.released_buttons = released;}
KidLispState* kl_state(void) { return &state;}
// === Graphics ===
void kl_wipe(KLColor color) { state.pd->graphics->clear(color);}
void kl_ink(KLColor color) { state.ink_color = color;}
void kl_line(int x1, int y1, int x2, int y2) { state.pd->graphics->drawLine(x1, y1, x2, y2, 1, state.ink_color);}
void kl_line_random(void) { int x1 = rand() % KL_WIDTH; int y1 = rand() % KL_HEIGHT; int x2 = rand() % KL_WIDTH; int y2 = rand() % KL_HEIGHT; state.pd->graphics->drawLine(x1, y1, x2, y2, 1, state.ink_color);}
void kl_box(int x, int y, int w, int h, int filled) { if (filled) { state.pd->graphics->fillRect(x, y, w, h, state.ink_color); } else { state.pd->graphics->drawRect(x, y, w, h, state.ink_color); }}
void kl_circle(int x, int y, int r, int filled) { // Playdate uses ellipse API (x, y, width, height) int diameter = r * 2; int left = x - r; int top = y - r; if (filled) { state.pd->graphics->fillEllipse(left, top, diameter, diameter, 0, 360, state.ink_color); } else { state.pd->graphics->drawEllipse(left, top, diameter, diameter, 1, 0, 360, state.ink_color); }}
void kl_plot(int x, int y) { // Draw a 1x1 filled rect for a single pixel state.pd->graphics->fillRect(x, y, 1, 1, state.ink_color);}
void kl_write(const char* text, int x, int y) { if (default_font != NULL) { state.pd->graphics->setFont(default_font); } state.pd->graphics->drawText(text, strlen(text), kASCIIEncoding, x, y);}
void kl_write_small(const char* text, int x, int y) { if (small_font != NULL) { state.pd->graphics->setFont(small_font); } state.pd->graphics->drawText(text, strlen(text), kASCIIEncoding, x, y);}
void kl_write_outlined(const char* text, int x, int y) { // Draw white outline by drawing in all 8 directions + diagonals if (small_font != NULL) { state.pd->graphics->setFont(small_font); } // Draw white "shadow" in all directions for outline effect state.pd->graphics->setDrawMode(kDrawModeFillWhite); for (int dy = -1; dy <= 1; dy++) { for (int dx = -1; dx <= 1; dx++) { if (dx != 0 || dy != 0) { state.pd->graphics->drawText(text, strlen(text), kASCIIEncoding, x + dx, y + dy); } } } // Draw black text on top state.pd->graphics->setDrawMode(kDrawModeFillBlack); state.pd->graphics->drawText(text, strlen(text), kASCIIEncoding, x, y); // Reset draw mode state.pd->graphics->setDrawMode(kDrawModeCopy);}
void kl_scroll(int dx, int dy) { // Use getFrame() for direct framebuffer access uint8_t* data = state.pd->graphics->getFrame(); if (!data) return; if (dx == 0 && dy == 0) return; int rowbytes = 52; // LCD_ROWSIZE int height = 240; // LCD_ROWS int width_pixels = 400; // Full screen temp buffer for proper wrap-around static uint8_t temp_screen[52 * 240]; // Copy entire framebuffer to temp first memcpy(temp_screen, data, rowbytes * height); // Now do the scroll with wrap-around, reading from temp, writing to data for (int y = 0; y < height; y++) { // Calculate source Y with wrap int src_y = y - dy; while (src_y < 0) src_y += height; while (src_y >= height) src_y -= height; uint8_t* src_row = temp_screen + src_y * rowbytes; uint8_t* dst_row = data + y * rowbytes; if (dx == 0) { // No horizontal scroll, just copy the row memcpy(dst_row, src_row, rowbytes); } else { // Horizontal scroll with wrap - pixel by pixel for (int px = 0; px < width_pixels; px++) { // Calculate source X with wrap int src_px = px - dx; while (src_px < 0) src_px += width_pixels; while (src_px >= width_pixels) src_px -= width_pixels; // Get source bit (MSB = leftmost pixel) int src_byte = src_px / 8; int src_bit = 7 - (src_px % 8); int src_val = (src_row[src_byte] >> src_bit) & 1; // Set dest bit int dst_byte = px / 8; int dst_bit = 7 - (px % 8); if (src_val) { dst_row[dst_byte] |= (1 << dst_bit); } else { dst_row[dst_byte] &= ~(1 << dst_bit); } } } } state.pd->graphics->markUpdatedRows(0, height - 1);}
void kl_blur(int amount) { // 1-bit "blur" effect - erode black pixels slightly // This creates a fading/dissolving effect if (amount <= 0) return; // Use getFrame() for direct framebuffer access (not a copy!) uint8_t* data = state.pd->graphics->getFrame(); if (!data) { state.pd->system->logToConsole("blur: no framebuffer!"); return; } // LCD_ROWSIZE is 52 bytes per row, 240 rows int rowbytes = 52; // LCD_ROWSIZE from SDK int height = 240; // LCD_ROWS int total_bytes = height * rowbytes; // Random erosion: amount * 100 pixels per frame int erosion_count = amount * 100; for (int i = 0; i < erosion_count; i++) { int byte_idx = rand() % total_bytes; int bit_idx = rand() % 8; // Check if this bit is black (0) and flip to white (1) if ((data[byte_idx] & (0x80 >> bit_idx)) == 0) { data[byte_idx] |= (0x80 >> bit_idx); } } state.pd->graphics->markUpdatedRows(0, height - 1);}
// === System ===
int kl_width(void) { return KL_WIDTH;}
int kl_height(void) { return KL_HEIGHT;}
int kl_frame(void) { return state.frame;}
// === Input ===
float kl_crank(void) { return state.crank_angle;}
float kl_crank_delta(void) { return state.crank_delta;}
int kl_button_a(void) { return (state.current_buttons & kButtonA) != 0;}
int kl_button_b(void) { return (state.current_buttons & kButtonB) != 0;}
int kl_button_up(void) { return (state.current_buttons & kButtonUp) != 0;}
int kl_button_down(void) { return (state.current_buttons & kButtonDown) != 0;}
int kl_button_left(void) { return (state.current_buttons & kButtonLeft) != 0;}
int kl_button_right(void) { return (state.current_buttons & kButtonRight) != 0;}
int kl_button_pressed_a(void) { return (state.pressed_buttons & kButtonA) != 0;}
int kl_button_pressed_b(void) { return (state.pressed_buttons & kButtonB) != 0;}
// === Math ===
float kl_sin(float x) { // Convert degrees to radians return sinf(x * M_PI / 180.0f);}
float kl_cos(float x) { // Convert degrees to radians return cosf(x * M_PI / 180.0f);}
int kl_random(int max) { if (max <= 0) return 0; return rand() % max;}
int kl_wiggle(int amount) { if (amount <= 0) return 0; return kl_random(amount) - (amount / 2);}
// === HUD Buffer ===// Separate bitmap for HUD overlay that gets composited on top
void kl_hud_init(void) { if (hud_buffer == NULL) { hud_buffer = state.pd->graphics->newBitmap(KL_WIDTH, HUD_HEIGHT, kColorWhite); }}
void kl_hud_clear(void) { if (hud_buffer == NULL) return; state.pd->graphics->clearBitmap(hud_buffer, kColorClear); // Transparent}
void kl_hud_write(const char* text, int x, int y) { if (hud_buffer == NULL) return; // Push context to draw into HUD buffer state.pd->graphics->pushContext(hud_buffer); // Use small font if (small_font != NULL) { state.pd->graphics->setFont(small_font); } // Draw white outline state.pd->graphics->setDrawMode(kDrawModeFillWhite); for (int dy = -1; dy <= 1; dy++) { for (int dx = -1; dx <= 1; dx++) { if (dx != 0 || dy != 0) { state.pd->graphics->drawText(text, strlen(text), kASCIIEncoding, x + dx, y + dy); } } } // Draw black text on top state.pd->graphics->setDrawMode(kDrawModeFillBlack); state.pd->graphics->drawText(text, strlen(text), kASCIIEncoding, x, y); // Restore state.pd->graphics->setDrawMode(kDrawModeCopy); state.pd->graphics->popContext();}
void kl_hud_composite(void) { if (hud_buffer == NULL) return; // Draw HUD buffer on top of main framebuffer with transparency state.pd->graphics->setDrawMode(kDrawModeNXOR); state.pd->graphics->drawBitmap(hud_buffer, 0, 0, kBitmapUnflipped); state.pd->graphics->setDrawMode(kDrawModeCopy);}
// Clear HUD regions to white before effects run// This prevents old HUD text from being scrolled into the effectvoid kl_clear_hud_regions(void) { // Clear top-left text area (source code display) // Text is at (5, 4) with 8px line spacing, up to 5 lines // Font is ~10px tall, so clear about 50px height with some margin state.pd->graphics->fillRect(0, 0, 250, 50, kColorWhite); // Clear bottom-right QR area // QR is 25x25 at (369, 206) with 3px border = 31x31 starting at (366, 203) int qr_x = KL_WIDTH - QR_SIZE - 6; int qr_y = KL_HEIGHT - QR_SIZE - 5; state.pd->graphics->fillRect(qr_x - 4, qr_y - 4, QR_SIZE + 8, QR_SIZE + 8, kColorWhite);}
void kl_draw_qr(void) { // Draw QR code in bottom right corner with double border (white outer, black inner) int qr_x = KL_WIDTH - QR_SIZE - 6; // 6px margin from right int qr_y = KL_HEIGHT - QR_SIZE - 5; // 5px margin from bottom // Draw 1px white outer border (visible on dark backgrounds) state.pd->graphics->fillRect(qr_x - 3, qr_y - 3, QR_SIZE + 6, QR_SIZE + 6, kColorWhite); // Draw 1px black inner border state.pd->graphics->fillRect(qr_x - 2, qr_y - 2, QR_SIZE + 4, QR_SIZE + 4, kColorBlack); // Draw white background state.pd->graphics->fillRect(qr_x - 1, qr_y - 1, QR_SIZE + 2, QR_SIZE + 2, kColorWhite); // Draw QR code pixels for (int row = 0; row < QR_SIZE; row++) { int byte_offset = row * 4; // 4 bytes per row (25 bits padded to 32) for (int col = 0; col < QR_SIZE; col++) { int byte_idx = byte_offset + (col / 8); int bit_idx = 7 - (col % 8); if (qr_data[byte_idx] & (1 << bit_idx)) { // Black pixel state.pd->graphics->fillRect(qr_x + col, qr_y + row, 1, 1, kColorBlack); } } }}