Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: nopaint painting system for AC Native + DJ/wifi/handle fixes

Painting system:
- page(painting) switches render target, returns chainable proxy
- paste(painting, dx, dy) alpha-composites onto current target
- painting(w,h,cb) now exposes .pixels Uint8Array for direct access
- graph_line_thick() for variable-width brush strokes
- line(x0,y0,x1,y1,thickness) JS API with 5th thickness param
- New painting.mjs piece: persistent canvas, freehand line brush,
color cycling (c key), clear (n key), scroll thickness

DJ fixes:
- Speed control moved to audio callback with linear interpolation
(was broken in decoder resampler) — scratching actually works
- Negative speed support (-4x to +4x) for reverse scratching
- TTS fixed: was calling tts.speak() but API is sound.speak()

WiFi: ClearPass captive portal cmd=authenticate strategy
ac-os: strip @ prefix from handle, USE_SDL defaults off

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

+254 -5
+160
fedac/native/pieces/painting.mjs
··· 1 + // painting.mjs — Nopaint freehand drawing for AC Native 2 + // Persistent canvas with line brush. Touch to draw, lift to bake. 3 + // Commands: "no" clears, scroll adjusts thickness. 4 + 5 + let T = __theme.update(); 6 + 7 + // Canvas state 8 + let canvas = null; // persistent painting buffer 9 + let buffer = null; // temporary stroke overlay 10 + let sw = 0, sh = 0; 11 + 12 + // Brush state 13 + let thickness = 2; 14 + let color = [255, 255, 255]; // default white (will adapt to theme) 15 + let painting = false; 16 + let points = []; 17 + let frame = 0; 18 + let cursorX = 0, cursorY = 0; // Track cursor for preview 19 + let mkPaintingFn = null; // Stored for "no" command 20 + 21 + // Color palette (cycle with middle-click or 'c' key) 22 + const palette = [ 23 + [255, 255, 255], [255, 0, 0], [255, 165, 0], [255, 255, 0], 24 + [0, 128, 0], [0, 255, 255], [0, 0, 255], [128, 0, 128], 25 + [128, 128, 128], [0, 0, 0], 26 + ]; 27 + let colorIdx = 0; 28 + 29 + function boot({ screen, wipe, painting: mkPainting }) { 30 + sw = screen.width; 31 + sh = screen.height; 32 + mkPaintingFn = mkPainting; 33 + 34 + // Create persistent canvas and stroke buffer 35 + canvas = mkPainting(sw, sh, (p) => { 36 + T = __theme.update(); 37 + p.wipe(T.dark ? 20 : 240, T.dark ? 20 : 238, T.dark ? 25 : 232); 38 + }); 39 + buffer = mkPainting(sw, sh, (p) => { p.wipe(0, 0, 0, 0); }); 40 + 41 + color = T.dark ? [255, 255, 255] : [0, 0, 0]; 42 + cursorX = sw / 2; 43 + cursorY = sh / 2; 44 + wipe(T.bg[0], T.bg[1], T.bg[2]); 45 + } 46 + 47 + function act({ event: e, screen, sound, system, page }) { 48 + // Track cursor position from any mouse/touch event 49 + if (e.x !== undefined) { cursorX = e.x; cursorY = e.y; } 50 + 51 + // Start stroke 52 + if (e.is("touch")) { 53 + painting = true; 54 + points = [{ x: e.x, y: e.y }]; 55 + } 56 + 57 + // Continue stroke 58 + if (e.is("draw") && painting) { 59 + const last = points[points.length - 1]; 60 + if (!last || last.x !== e.x || last.y !== e.y) { 61 + points.push({ x: e.x, y: e.y }); 62 + } 63 + } 64 + 65 + // End stroke — bake will happen in paint 66 + if (e.is("lift") && painting) { 67 + painting = false; 68 + } 69 + 70 + // Scroll: adjust thickness 71 + if (e.is("scroll")) { 72 + const delta = e.y > 0 ? -1 : e.y < 0 ? 1 : 0; 73 + thickness = Math.max(1, Math.min(thickness + delta, 50)); 74 + } 75 + 76 + // 'c' key: cycle color 77 + if (e.is("keyboard:down:c")) { 78 + colorIdx = (colorIdx + 1) % palette.length; 79 + color = palette[colorIdx]; 80 + } 81 + 82 + // 'n' key: clear canvas (the "no" command) 83 + if (e.is("keyboard:down:n") && canvas && mkPaintingFn) { 84 + canvas = mkPaintingFn(sw, sh, (p) => { 85 + T = __theme.update(); 86 + p.wipe(T.dark ? 20 : 240, T.dark ? 20 : 238, T.dark ? 25 : 232); 87 + }); 88 + } 89 + 90 + // Escape: jump to prompt 91 + if (e.is("keyboard:down:escape")) { 92 + system?.jump?.("prompt"); 93 + } 94 + } 95 + 96 + function paint({ wipe, ink, line, circle, paste, page, screen, write }) { 97 + frame++; 98 + T = __theme.update(); 99 + 100 + // 1. Draw the persistent canvas to screen 101 + wipe(T.bg[0], T.bg[1], T.bg[2]); 102 + if (canvas) paste(canvas); 103 + 104 + // 2. If actively painting, draw current stroke into buffer and overlay it 105 + if (painting && points.length > 1) { 106 + // Clear buffer 107 + page(buffer).wipe(0, 0, 0, 0); 108 + 109 + // Draw stroke into buffer 110 + for (let i = 0; i < points.length - 1; i++) { 111 + const a = points[i], b = points[i + 1]; 112 + if (thickness === 1) { 113 + ink(color[0], color[1], color[2]).line(a.x, a.y, b.x, b.y); 114 + } else { 115 + ink(color[0], color[1], color[2]).line(a.x, a.y, b.x, b.y, thickness); 116 + } 117 + } 118 + 119 + // Switch back to screen 120 + page(screen); 121 + 122 + // Overlay buffer onto screen 123 + paste(buffer); 124 + } 125 + 126 + // 3. If stroke just ended, bake buffer onto canvas 127 + if (!painting && points.length > 1) { 128 + // Draw stroke directly onto canvas 129 + page(canvas); 130 + for (let i = 0; i < points.length - 1; i++) { 131 + const a = points[i], b = points[i + 1]; 132 + if (thickness === 1) { 133 + ink(color[0], color[1], color[2]).line(a.x, a.y, b.x, b.y); 134 + } else { 135 + ink(color[0], color[1], color[2]).line(a.x, a.y, b.x, b.y, thickness); 136 + } 137 + } 138 + page(screen); 139 + points = []; 140 + 141 + // Redraw canvas to screen 142 + paste(canvas); 143 + } 144 + 145 + // 4. Brush preview cursor (when not painting) 146 + if (!painting && thickness > 2) { 147 + ink(color[0], color[1], color[2], 80).circle(cursorX, cursorY, Math.floor(thickness / 2), false); 148 + } 149 + 150 + // 5. HUD 151 + const fg = T.fg; 152 + ink(fg, fg, fg, 180).write(`${thickness}px`, { x: 4, y: sh - 12, size: 1, font: "font_1" }); 153 + ink(color[0], color[1], color[2]).box(30, sh - 12, 8, 8, true); 154 + ink(fg, fg, fg, 120).write("c:color n:clear esc:exit", { x: 44, y: sh - 12, size: 1, font: "font_1" }); 155 + } 156 + 157 + function sim() {} 158 + function leave() {} 159 + 160 + export { boot, act, paint, sim, leave };
+18
fedac/native/src/graph.c
··· 59 59 } 60 60 } 61 61 62 + // Thick line: draw filled circles along the Bresenham path 63 + void graph_line_thick(ACGraph *g, int x0, int y0, int x1, int y1, int thickness) { 64 + if (thickness <= 1) { graph_line(g, x0, y0, x1, y1); return; } 65 + int r = (thickness - 1) / 2; 66 + int dx = abs(x1 - x0); 67 + int dy = -abs(y1 - y0); 68 + int sx = x0 < x1 ? 1 : -1; 69 + int sy = y0 < y1 ? 1 : -1; 70 + int err = dx + dy; 71 + for (;;) { 72 + graph_circle(g, x0, y0, r, 1); 73 + if (x0 == x1 && y0 == y1) break; 74 + int e2 = 2 * err; 75 + if (e2 >= dy) { err += dy; x0 += sx; } 76 + if (e2 <= dx) { err += dx; y0 += sy; } 77 + } 78 + } 79 + 62 80 void graph_box(ACGraph *g, int x, int y, int w, int h, int filled) { 63 81 if (filled) { 64 82 // Clip to framebuffer bounds once
+1
fedac/native/src/graph.h
··· 21 21 void graph_ink(ACGraph *g, ACColor color); 22 22 void graph_plot(ACGraph *g, int x, int y); 23 23 void graph_line(ACGraph *g, int x0, int y0, int x1, int y1); 24 + void graph_line_thick(ACGraph *g, int x0, int y0, int x1, int y1, int thickness); 24 25 void graph_box(ACGraph *g, int x, int y, int w, int h, int filled); 25 26 void graph_circle(ACGraph *g, int cx, int cy, int r, int filled); 26 27
+75 -5
fedac/native/src/js-bindings.c
··· 536 536 JS_ToInt32(ctx, &y0, argv[1]); 537 537 JS_ToInt32(ctx, &x1, argv[2]); 538 538 JS_ToInt32(ctx, &y1, argv[3]); 539 - graph_line(current_rt->graph, x0, y0, x1, y1); 539 + if (argc >= 5) { 540 + int thickness; 541 + JS_ToInt32(ctx, &thickness, argv[4]); 542 + graph_line_thick(current_rt->graph, x0, y0, x1, y1, thickness); 543 + } else { 544 + graph_line(current_rt->graph, x0, y0, x1, y1); 545 + } 540 546 return JS_UNDEFINED; 541 547 } 542 548 ··· 1798 1804 // Register top-level graphics functions 1799 1805 JS_SetPropertyStr(ctx, global, "wipe", JS_NewCFunction(ctx, js_wipe, "wipe", 3)); 1800 1806 JS_SetPropertyStr(ctx, global, "ink", JS_NewCFunction(ctx, js_ink, "ink", 4)); 1801 - JS_SetPropertyStr(ctx, global, "line", JS_NewCFunction(ctx, js_line, "line", 4)); 1807 + JS_SetPropertyStr(ctx, global, "line", JS_NewCFunction(ctx, js_line, "line", 5)); 1802 1808 JS_SetPropertyStr(ctx, global, "box", JS_NewCFunction(ctx, js_box, "box", 5)); 1803 1809 JS_SetPropertyStr(ctx, global, "circle", JS_NewCFunction(ctx, js_circle, "circle", 4)); 1804 1810 JS_SetPropertyStr(ctx, global, "qr", JS_NewCFunction(ctx, js_qr, "qr", 4)); ··· 1915 1921 JS_SetPropertyStr(ctx, painting, "width", JS_NewInt32(ctx, w)); 1916 1922 JS_SetPropertyStr(ctx, painting, "height", JS_NewInt32(ctx, h)); 1917 1923 1924 + // Expose pixels as a Uint8Array view into the framebuffer memory. 1925 + // Nopaint uses buffer.pixels[i] to adjust alpha during bake. 1926 + // Note: pixel format is ARGB32 (native byte order). 1927 + { 1928 + int byte_len = tex_fb->stride * h * 4; 1929 + JSValue ab = JS_NewArrayBuffer(ctx, (uint8_t *)tex_fb->pixels, byte_len, 1930 + NULL, NULL, 0); // No free — painting finalizer handles the framebuffer 1931 + // Construct Uint8Array from the ArrayBuffer via JS eval 1932 + JSValue global = JS_GetGlobalObject(ctx); 1933 + JSValue uint8_ctor = JS_GetPropertyStr(ctx, global, "Uint8Array"); 1934 + JSValue pixels = JS_CallConstructor(ctx, uint8_ctor, 1, &ab); 1935 + JS_SetPropertyStr(ctx, painting, "pixels", pixels); 1936 + JS_FreeValue(ctx, uint8_ctor); 1937 + JS_FreeValue(ctx, global); 1938 + JS_FreeValue(ctx, ab); 1939 + } 1940 + 1918 1941 // If callback provided, render into the off-screen buffer 1919 1942 if (argc >= 3 && JS_IsFunction(ctx, argv[2])) { 1920 1943 // Save current render target and switch to off-screen ··· 1942 1965 } 1943 1966 1944 1967 return painting; 1968 + } 1969 + 1970 + // page(painting) — switch render target to a painting buffer (or back to screen) 1971 + // page(painting) returns an object with wipe/ink/box/line/etc that draw into that buffer. 1972 + // In AC web, page() returns a chainable proxy. Here we just switch the graph target. 1973 + static JSValue js_page(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1974 + (void)this_val; 1975 + if (!current_rt || !current_rt->graph) return JS_UNDEFINED; 1976 + 1977 + if (argc < 1 || JS_IsUndefined(argv[0]) || JS_IsNull(argv[0])) { 1978 + // page() with no args or page(screen) — restore screen target 1979 + graph_page(current_rt->graph, current_rt->graph->screen); 1980 + } else { 1981 + // page(painting) — switch to painting buffer 1982 + ACFramebuffer *fb = JS_GetOpaque(argv[0], painting_class_id); 1983 + if (fb) graph_page(current_rt->graph, fb); 1984 + } 1985 + 1986 + // Return a page proxy object with wipe() so callers can do page(buf).wipe(...) 1987 + JSValue proxy = JS_NewObject(ctx); 1988 + JSValue global = JS_GetGlobalObject(ctx); 1989 + JS_SetPropertyStr(ctx, proxy, "wipe", JS_GetPropertyStr(ctx, global, "wipe")); 1990 + JS_SetPropertyStr(ctx, proxy, "ink", JS_GetPropertyStr(ctx, global, "ink")); 1991 + JS_SetPropertyStr(ctx, proxy, "box", JS_GetPropertyStr(ctx, global, "box")); 1992 + JS_SetPropertyStr(ctx, proxy, "line", JS_GetPropertyStr(ctx, global, "line")); 1993 + JS_SetPropertyStr(ctx, proxy, "circle", JS_GetPropertyStr(ctx, global, "circle")); 1994 + JS_SetPropertyStr(ctx, proxy, "plot", JS_GetPropertyStr(ctx, global, "plot")); 1995 + JS_SetPropertyStr(ctx, proxy, "write", JS_GetPropertyStr(ctx, global, "write")); 1996 + JS_FreeValue(ctx, global); 1997 + return proxy; 1998 + } 1999 + 2000 + // paste(painting, dx?, dy?) — alpha-composite a painting onto the current render target 2001 + static JSValue js_paste(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2002 + (void)this_val; 2003 + if (!current_rt || !current_rt->graph) return JS_UNDEFINED; 2004 + if (argc < 1) return JS_UNDEFINED; 2005 + 2006 + ACFramebuffer *src = JS_GetOpaque(argv[0], painting_class_id); 2007 + if (!src) return JS_UNDEFINED; 2008 + 2009 + int dx = 0, dy = 0; 2010 + if (argc >= 2) JS_ToInt32(ctx, &dx, argv[1]); 2011 + if (argc >= 3) JS_ToInt32(ctx, &dy, argv[2]); 2012 + 2013 + graph_paste(current_rt->graph, src, dx, dy); 2014 + return JS_UNDEFINED; 1945 2015 } 1946 2016 1947 2017 // sound.bpm(val?) — get or set BPM, returns current value ··· 5545 5615 // painting(w, h, callback) — creates a stub painting object with width/height 5546 5616 JS_SetPropertyStr(ctx, api, "painting", JS_NewCFunction(ctx, js_painting, "painting", 3)); 5547 5617 5548 - // paste, page, layer, sharpen (stubs) 5549 - JS_SetPropertyStr(ctx, api, "paste", JS_NewCFunction(ctx, js_noop, "paste", 4)); 5550 - JS_SetPropertyStr(ctx, api, "page", JS_NewCFunction(ctx, js_noop, "page", 1)); 5618 + // paste, page (real implementations), layer, sharpen (stubs) 5619 + JS_SetPropertyStr(ctx, api, "paste", JS_NewCFunction(ctx, js_paste, "paste", 3)); 5620 + JS_SetPropertyStr(ctx, api, "page", JS_NewCFunction(ctx, js_page, "page", 1)); 5551 5621 JS_SetPropertyStr(ctx, api, "layer", JS_NewCFunction(ctx, js_noop, "layer", 1)); 5552 5622 JS_SetPropertyStr(ctx, api, "sharpen", JS_NewCFunction(ctx, js_noop, "sharpen", 1)); 5553 5623