Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: nopaint system integrated into AC Native runtime

Runtime-level persistent painting canvas that survives piece swaps:
- system.painting: persistent ACFramebuffer, accessible from any piece
- system.nopaint: state machine (is/cancelStroke), brush position, buffer
- Auto-creates painting+buffer when piece exports system="nopaint"
- Touch→paint→bake lifecycle handled in C (js_call_act/js_call_paint)
- Painting composited as background before piece paint()
- Buffer baked onto painting on stroke lift, then cleared
- Non-nopaint pieces (prompt) can access system.painting to show it

New rendering primitives:
- page(painting) switches render target, returns chainable proxy
- paste(painting, dx, dy) alpha-composites buffers
- painting.pixels Uint8Array for direct pixel access
- line(x0,y0,x1,y1,thickness) thick lines via filled circles
- graph_line_thick() in graph.c

Includes painting.mjs test piece for standalone testing.

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

+127
+118
fedac/native/src/js-bindings.c
··· 4465 4465 return result; 4466 4466 } 4467 4467 4468 + // nopaint.is(stateStr) — returns true if current nopaint state matches 4469 + static JSValue js_nopaint_is(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4470 + (void)this_val; 4471 + if (argc < 1 || !current_rt) return JS_FALSE; 4472 + const char *query = JS_ToCString(ctx, argv[0]); 4473 + if (!query) return JS_FALSE; 4474 + int match = 0; 4475 + if (strcmp(query, "painting") == 0) match = (current_rt->nopaint_state == 1); 4476 + else if (strcmp(query, "idle") == 0) match = (current_rt->nopaint_state == 0); 4477 + JS_FreeCString(ctx, query); 4478 + return JS_NewBool(ctx, match); 4479 + } 4480 + 4481 + // nopaint.cancelStroke() — abort current stroke 4482 + static JSValue js_nopaint_cancel(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 4483 + (void)this_val; (void)argc; (void)argv; 4484 + if (current_rt) { current_rt->nopaint_state = 0; current_rt->nopaint_needs_bake = 0; } 4485 + return JS_UNDEFINED; 4486 + } 4487 + 4468 4488 static JSValue build_system_obj(JSContext *ctx) { 4469 4489 JSValue sys = JS_NewObject(ctx); 4470 4490 ··· 5425 5445 JS_SetPropertyStr(ctx, sys, "openBrowser", 5426 5446 JS_NewCFunction(ctx, js_open_browser, "openBrowser", 1)); 5427 5447 5448 + // Nopaint system — persistent painting canvas 5449 + if (current_rt && strcmp(current_rt->system_mode, "nopaint") == 0) { 5450 + // Create painting + buffer on first use 5451 + if (!current_rt->nopaint_painting) { 5452 + int w = current_rt->graph->screen->width; 5453 + int h = current_rt->graph->screen->height; 5454 + current_rt->nopaint_painting = fb_create(w, h); 5455 + current_rt->nopaint_buffer = fb_create(w, h); 5456 + // Fill painting with theme background 5457 + fb_clear(current_rt->nopaint_painting, 0xFFF0EEE8); // light bg default 5458 + fb_clear(current_rt->nopaint_buffer, 0x00000000); // transparent 5459 + current_rt->nopaint_active = 1; 5460 + ac_log("[nopaint] Created painting %dx%d\n", w, h); 5461 + } 5462 + 5463 + JSValue np = JS_NewObject(ctx); 5464 + 5465 + // nopaint.is(state) — check nopaint state 5466 + JS_SetPropertyStr(ctx, np, "is", JS_NewCFunction(ctx, js_nopaint_is, "is", 1)); 5467 + JS_SetPropertyStr(ctx, np, "cancelStroke", JS_NewCFunction(ctx, js_nopaint_cancel, "cancelStroke", 0)); 5468 + 5469 + // nopaint.brush — current brush position 5470 + JSValue brush = JS_NewObject(ctx); 5471 + JS_SetPropertyStr(ctx, brush, "x", JS_NewInt32(ctx, current_rt->nopaint_brush_x)); 5472 + JS_SetPropertyStr(ctx, brush, "y", JS_NewInt32(ctx, current_rt->nopaint_brush_y)); 5473 + JS_SetPropertyStr(ctx, np, "brush", brush); 5474 + 5475 + // nopaint.buffer — temporary stroke overlay (as Painting object) 5476 + JSValue buf_obj = JS_NewObjectClass(ctx, painting_class_id); 5477 + JS_SetOpaque(buf_obj, current_rt->nopaint_buffer); 5478 + JS_SetPropertyStr(ctx, buf_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_buffer->width)); 5479 + JS_SetPropertyStr(ctx, buf_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_buffer->height)); 5480 + JS_SetPropertyStr(ctx, np, "buffer", buf_obj); 5481 + 5482 + // nopaint.needsBake 5483 + JS_SetPropertyStr(ctx, np, "needsBake", JS_NewBool(ctx, current_rt->nopaint_needs_bake)); 5484 + 5485 + JS_SetPropertyStr(ctx, sys, "nopaint", np); 5486 + 5487 + // system.painting — the persistent canvas (as Painting object) 5488 + JSValue ptg_obj = JS_NewObjectClass(ctx, painting_class_id); 5489 + JS_SetOpaque(ptg_obj, current_rt->nopaint_painting); 5490 + JS_SetPropertyStr(ctx, ptg_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_painting->width)); 5491 + JS_SetPropertyStr(ctx, ptg_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_painting->height)); 5492 + JS_SetPropertyStr(ctx, sys, "painting", ptg_obj); 5493 + } else if (current_rt && current_rt->nopaint_painting) { 5494 + // Even non-nopaint pieces (like prompt) can access system.painting to show it 5495 + JSValue ptg_obj = JS_NewObjectClass(ctx, painting_class_id); 5496 + JS_SetOpaque(ptg_obj, current_rt->nopaint_painting); 5497 + JS_SetPropertyStr(ctx, ptg_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_painting->width)); 5498 + JS_SetPropertyStr(ctx, ptg_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_painting->height)); 5499 + JS_SetPropertyStr(ctx, sys, "painting", ptg_obj); 5500 + } 5501 + 5428 5502 return sys; 5429 5503 } 5430 5504 ··· 5888 5962 if (!JS_IsFunction(rt->ctx, rt->paint_fn)) return; 5889 5963 current_rt = rt; 5890 5964 rt->paint_count++; 5965 + 5966 + // Nopaint: paste the persistent painting as background before piece paints 5967 + if (rt->nopaint_active && rt->nopaint_painting) { 5968 + graph_paste(rt->graph, rt->nopaint_painting, 0, 0); 5969 + } 5970 + 5891 5971 JSValue api = build_api(rt->ctx, rt, "paint"); 5892 5972 JSValue result = JS_Call(rt->ctx, rt->paint_fn, JS_UNDEFINED, 1, &api); 5893 5973 if (JS_IsException(result)) { ··· 5904 5984 } 5905 5985 JS_FreeValue(rt->ctx, result); 5906 5986 JS_FreeValue(rt->ctx, api); 5987 + 5988 + // Nopaint: if bake was requested, composite buffer → painting and clear buffer 5989 + if (rt->nopaint_active && rt->nopaint_needs_bake) { 5990 + if (rt->nopaint_buffer && rt->nopaint_painting) { 5991 + graph_paste(rt->graph, rt->nopaint_buffer, 0, 0); // show final stroke on screen 5992 + // Bake: composite buffer onto persistent painting 5993 + ACFramebuffer *saved = rt->graph->fb; 5994 + graph_page(rt->graph, rt->nopaint_painting); 5995 + graph_paste(rt->graph, rt->nopaint_buffer, 0, 0); 5996 + graph_page(rt->graph, saved); 5997 + // Clear buffer 5998 + fb_clear(rt->nopaint_buffer, 0x00000000); 5999 + } 6000 + rt->nopaint_needs_bake = 0; 6001 + } 5907 6002 } 5908 6003 5909 6004 void js_call_act(ACRuntime *rt) { ··· 5937 6032 rt->jump_param_count = 0; 5938 6033 ac_log("[system] Escape → prompt\n"); 5939 6034 return; // Skip passing events to piece 6035 + } 6036 + } 6037 + } 6038 + 6039 + // Nopaint: update brush position and state from touch/mouse events 6040 + if (rt->nopaint_active && strcmp(rt->system_mode, "nopaint") == 0) { 6041 + for (int i = 0; i < input->event_count; i++) { 6042 + ACEvent *ev = &input->events[i]; 6043 + if (ev->type == AC_EVENT_TOUCH) { 6044 + rt->nopaint_state = 1; // painting 6045 + rt->nopaint_brush_x = ev->x; 6046 + rt->nopaint_brush_y = ev->y; 6047 + rt->nopaint_needs_bake = 0; 6048 + } else if (ev->type == AC_EVENT_DRAW) { 6049 + if (rt->nopaint_state == 1) { 6050 + rt->nopaint_brush_x = ev->x; 6051 + rt->nopaint_brush_y = ev->y; 6052 + } 6053 + } else if (ev->type == AC_EVENT_LIFT) { 6054 + if (rt->nopaint_state == 1) { 6055 + rt->nopaint_state = 0; // idle 6056 + rt->nopaint_needs_bake = 1; 6057 + } 5940 6058 } 5941 6059 } 5942 6060 }
+9
fedac/native/src/js-bindings.h
··· 101 101 char handle[64]; // e.g. "jeffrey" (without @) 102 102 char piece[64]; // default piece name, e.g. "notepat" 103 103 104 + // Nopaint painting system — persistent across piece swaps 105 + ACFramebuffer *nopaint_painting; // persistent canvas (survives piece changes) 106 + ACFramebuffer *nopaint_buffer; // temporary stroke overlay 107 + int nopaint_active; // 1 = current piece uses nopaint system 108 + int nopaint_state; // 0=idle, 1=painting 109 + int nopaint_needs_bake; // 1 = stroke ended, needs bake 110 + int nopaint_brush_x, nopaint_brush_y; // current brush position 111 + int nopaint_touch_x, nopaint_touch_y; // touch start position 112 + 104 113 // JS crash overlay — set when paint/act/sim throws an unhandled exception 105 114 int crash_active; // 1 = crash bar visible 106 115 int crash_count; // total JS exceptions since piece load