From 207febef9924c8fa5391185e75d42a7ea480bb38 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Mon, 10 Aug 2026 11:11:18 -0700 Subject: Chapter 31: Software Drawing, Time, and Graphical Games Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki Co-authored-by: Claude Opus 5 --- src/main/smp/sys/draw/Button.smp | 41 +++++++++++ src/main/smp/sys/draw/Canvas.smp | 149 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 src/main/smp/sys/draw/Button.smp create mode 100644 src/main/smp/sys/draw/Canvas.smp (limited to 'src/main/smp/sys/draw') diff --git a/src/main/smp/sys/draw/Button.smp b/src/main/smp/sys/draw/Button.smp new file mode 100644 index 00000000..c90ed2d3 --- /dev/null +++ b/src/main/smp/sys/draw/Button.smp @@ -0,0 +1,41 @@ +// Feed pointer state once per event-loop iteration. Activate on release inside, +// only after a press that began inside. No native/window dependency. +struct Button { + int x, y, width, height; + u8[~] !label; + bool !enabled = true, !hovered = false, !pressed = false; + bool !_held = false, !_armed = false; + new Button = { int px, int py, int w, int h, u8[~] text -> + x = px; y = py; width = w; height = h; label = text; + }; + val contains = { i64 px, i64 py -> + if( width <= 0 || height <= 0 ) return false; + i64 right = x, bottom = y; right += width; bottom += height; + if( px < x || py < y || px >= right || py >= bottom ) return false; + return true; + }; + val update = { i64 px, i64 py, bool down -> + hovered = false; + if( enabled ) hovered = contains(px,py); + if( !enabled ) _armed = false; + if( down && !_held ) _armed = hovered; + var clicked = false; + if( !down ) if( _held ) if( _armed ) if( hovered ) clicked = true; + if( !down ) _armed = false; + _held = down; + pressed = false; + if( down ) if( _armed ) if( hovered ) pressed = true; + return clicked; + }; + val draw = { !sys.draw.Canvas screen, int scale, u32 foreground, u32 background, u32 active -> + screen.fillRect(x,y,width,height,enabled && (hovered || pressed) ? active : background); + screen.rect(x,y,width,height,2,enabled ? foreground : active); + if( scale > 0 ) { + i64 textWidth = label#; textWidth = (textWidth*4-1)*scale; + if( textWidth <= width-8 && 5*scale <= height-8 ) + screen.text(x+(width-textWidth)/2,y+(height-5*scale)/2+(pressed ? 1 : 0), + label,scale,enabled ? foreground : active); + } + return self; + }; +}; diff --git a/src/main/smp/sys/draw/Canvas.smp b/src/main/smp/sys/draw/Canvas.smp new file mode 100644 index 00000000..9ba3514a --- /dev/null +++ b/src/main/smp/sys/draw/Canvas.smp @@ -0,0 +1,149 @@ +struct Canvas { + u32 width; + u32 height; + u32[] pixels; + + new Canvas = { u32 w, u32 h -> + i64 count = w; + count *= h; + if( w <= 0 || h <= 0 || count > sys.integer.MAX_I32 ) libc._exit(1); + width = w; + height = h; + pixels = new u32[count]; + }; + + val clear = { u32 color -> + for( int i = 0; i < pixels#; i++ ) pixels[i] = color; + return self; + }; + + val pixel = { i64 x, i64 y, u32 color -> + if( 0 <= x < width && 0 <= y < height ) pixels[y*width+x] = color; + return self; + }; + + // Integer coordinates, top-left origin. Rectangle extents are half-open. + val fillRect = { i64 x, i64 y, i64 w, i64 h, u32 color -> + if( w <= 0 || h <= 0 ) return self; + i64 right = x; right += w; + i64 bottom = y; bottom += h; + if( right <= 0 || bottom <= 0 || x >= width || y >= height ) return self; + int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y; + int x1 = right > width ? width : right, y1 = bottom > height ? height : bottom; + for( int py = y0; py < y1; py++ ) + for( int px = x0; px < x1; px++ ) pixels[py*width+px] = color; + return self; + }; + + // Outline stays inside the rectangle. Wide borders become a filled rect. + val rect = { i64 x, i64 y, i64 w, i64 h, i64 thickness, u32 color -> + if( w <= 0 || h <= 0 || thickness <= 0 ) return self; + if( thickness >= w || thickness >= h || thickness >= w-thickness || thickness >= h-thickness ) + return fillRect(x,y,w,h,color); + fillRect(x,y,w,thickness,color); + fillRect(x,y+h-thickness,w,thickness,color); + fillRect(x,y+thickness,thickness,h-2*thickness,color); + return fillRect(x+w-thickness,y+thickness,thickness,h-2*thickness,color); + }; + + // Integer centers are pixel centers. Circle outlines extend inward. + val fillCircle = { i64 cx, i64 cy, int radius, u32 color -> + return _circle(cx,cy,radius,0,true,color); + }; + val circle = { i64 cx, i64 cy, int radius, int thickness, u32 color -> + return _circle(cx,cy,radius,thickness,false,color); + }; + val _circle = { i64 cx, i64 cy, int radius, int thickness, bool filled, u32 color -> + if( radius < 0 || (!filled && thickness <= 0) ) return self; + i64 left = cx; left -= radius; + i64 top = cy; top -= radius; + i64 right = cx; right += radius; + i64 bottom = cy; bottom += radius; + if( right < 0 || bottom < 0 || left >= width || top >= height ) return self; + int x0 = left < 0 ? 0 : left, y0 = top < 0 ? 0 : top; + int x1 = right >= width ? width-1 : right, y1 = bottom >= height ? height-1 : bottom; + i64 outer2 = radius; + outer2 *= radius; + i64 inner = filled ? -1 : radius-thickness; + for( int py = y0; py <= y1; py++ ) + for( int px = x0; px <= x1; px++ ) { + i64 dx = px; dx -= cx; + i64 dy = py; dy -= cy; + i64 d2 = dx*dx+dy*dy; + if( d2 <= outer2 && (inner <= 0 || d2 > inner*inner) ) pixels[py*width+px] = color; + } + return self; + }; + + // Round caps; a zero-length segment is a dot. Only the clipped bounding + // box is scanned. Floating distances avoid overflowing coordinate squares. + val line = { i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness, u32 color -> + if( thickness <= 0 ) return self; + i64 pad = thickness/2+1; + i64 left = x0 < x1 ? x0 : x1; left -= pad; + i64 top = y0 < y1 ? y0 : y1; top -= pad; + i64 right = x0 > x1 ? x0 : x1; right += pad; + i64 bottom = y0 > y1 ? y0 : y1; bottom += pad; + if( right < 0 || bottom < 0 || left >= width || top >= height ) return self; + int bx0 = left < 0 ? 0 : left, by0 = top < 0 ? 0 : top; + int bx1 = right >= width ? width-1 : right, by1 = bottom >= height ? height-1 : bottom; + for( int py = by0; py <= by1; py++ ) + for( int px = bx0; px <= bx1; px++ ) + if( _onLine(px,py,x0,y0,x1,y1,thickness) ) pixels[py*width+px] = color; + return self; + }; + + // Small 3x5 bitmap font for game labels. Lowercase maps to uppercase. + // Newline returns to x; unsupported characters draw a question mark. + val text = { i64 x, i64 y, u8[~] str, int scale, u32 color -> + if( scale <= 0 ) return self; + i64 px = x, py = y; + for( int i = 0; i < str#; i++ ) { + u8 c = str[i]; + if( c == '\n' ) { px = x; py += 6*scale; continue; } + int bits = _glyph(c); + for( int row = 0; row < 5; row++ ) + for( int col = 0; col < 3; col++ ) + if( (bits >> (14-row*3-col)) & 1 ) + fillRect(px+col*scale,py+row*scale,scale,scale,color); + px += 4*scale; + } + return self; + }; +}; + +// An old-school 3x5 font, ASCII 32..127. Five octal row digits need u16. +// Lowercase uses uppercase shapes; unsupported characters use '?'. +val _glyphs = new u16[~96]{ + 000000,022202,055000,057575,061202,051245,061202,022000, // space..' + 012221,042224,005250,002720,000024,000700,000002,011244, // (../ + 075557,026227,061247,061216,055711,074616,034757,071222, // 0..7 + 075757,075716,002020,002024,012421,007070,042124,061202, // 8..? + 061202,025755,065656,034443,065556,074647,074644,034553, // @..G + 055755,072227,011152,055655,044447,057755,057775,025552, // H..O + 065644,025573,065655,034216,072222,055557,055552,055775, // P..W + 055255,055222,071247,032223,044211,062226,025000,000007, // X.._ + 042000,025755,065656,034443,065556,074647,074644,034553, // `..g + 055755,072227,011152,055655,044447,057755,057775,025552, // h..o + 065644,025573,065655,034216,072222,055557,055552,055775, // p..w + 055255,055222,071247,032423,022222,062126,003600,061202, // x..DEL +}; + +val _glyph = { u8 c -> + return 32 <= c < 128 ? _glyphs[c-32] : 061202; +}; + +// Pixel coverage for a round-ended segment, separate from traversal. +val _onLine = { i64 px, i64 py, i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness -> + f64 dx = x1; dx -= x0; + f64 dy = y1; dy -= y0; + f64 vx = px; vx -= x0; + f64 vy = py; vy -= y0; + f64 length2 = dx*dx+dy*dy; + bool point = x0 == x1 && y0 == y1; + f64 t = point ? 0.0 : (vx*dx+vy*dy)/length2; + t = t < 0 ? 0.0 : t > 1 ? 1.0 : t; + f64 ex = vx-t*dx, ey = vy-t*dy; + f64 half = thickness/2.0; + return ex*ex+ey*ey <= half*half; +}; -- cgit v1.2.3