# Graphics Deft's graphics surface is layered by concern, with one vocabulary that draws everywhere. All namespaces are TUI-only (`retro` / `retra` / `hydra` builds) except `image/*`, which is core — available in every native host including the standalone scripting binary. | Namespace | Role | |---|---| | `image/*` | Pixel data (core, all hosts): decode/encode, transforms, stable-pointer buffers — `Image` + `PixelBuffer` | | `draw/*` | ALL 2D raster output — polymorphic on target | | `surface/*` | SDL surface lifecycle: open/close/resize/events/flush | | `tui/*` | Structure: panels/focus/widgets + geometry + capability probes | | `gl/*` | 3D scenes (render through a canvas). GL-only | | `scene/*` | Cross-panel world layer. GL-only | The core idea: every `draw/*` pixel function accepts as its first argument a panel `DrawContext`, a surface id (number), or a canvas element (`draw/canvas`). Learn one set of primitives, draw on any surface.
[draw/line $ctx 10 20 90 20 "#ff0000"]      # panel (component-local px)
[draw/line $sid 10 20 90 20 "#ff0000" 2]    # surface — same call
[draw/clear $canvas "#101018"]              # retained canvas
## `draw/*` — Cell and Pixel Primitives The primary rendering API, called inside a `Panel.draw` method. The framework passes a `DrawContext`; cell-mode functions write characters into the panel's grid, pixel-mode functions record vector commands composited over it.
defimpl MyApp Panel {
    draw {|ctx|
        [draw/fill-bg $ctx "#141420"]
        [draw/text $ctx 0 0 "Hello" "#ffffff"]
        [draw/fill-rect $ctx 10 10 100 50 "#ff0000"]
    }
}
### Cell-mode functions (panel contexts only) | Function | Purpose | |---|---| | `draw/text $ctx $col $row $text ?fg ?bg ?bold` | Put text at cell coords | | `draw/cell $ctx $col $row $char ?fg ?bg ?bold` | Set a single cell | | `draw/fill-bg $ctx $color` | Fill bounds with a background color | | `draw/fill-cells $ctx $col $row $w $h $char $fg $bg` | Fill a cell rect with one char | | `draw/scrollbar $ctx $scroll-pos $total $visible ?col` | Vertical scrollbar glyphs | | `draw/push-clip $ctx $x $y $w $h` / `draw/pop-clip $ctx` | Clip rect stack (cells) | ### Pixel-mode functions (polymorphic on target) Target is a panel `DrawContext`, a surface id, or a canvas. Coordinates are component-local pixels on panels, surface-local pixels on surfaces, canvas-local pixels on canvases. | Function | Purpose | |---|---| | `draw/clear $target $color` | Panel: background cmd. Surface: fill staging frame. Canvas: set bg + clear list | | `draw/pixel $target $x $y $color` | Single pixel (surface: writes the staging buffer) | | `draw/rect $target $x $y $w $h $color ?width` | Rectangle outline | | `draw/fill-rect $target $x $y $w $h $color` | Filled rectangle | | `draw/line $target $x1 $y1 $x2 $y2 $color ?width` | Line | | `draw/path $target $points $color ?width ?closed` | Polyline (`$points` = vector of `[x y]` pairs) | | `draw/bezier $target $x0 $y0 … $x3 $y3 $color ?width ?segments` | Cubic Bezier | | `draw/circle $target $cx $cy $r $color` | Filled circle | | `draw/circle-outline $target $cx $cy $r $color ?width` | Circle outline | | `draw/text-px $target $x $y $text ?color` | Freeform text (font atlas) | | `draw/frame $sid $bytes $w $h` | Replace a surface's whole staging frame with RGBA bytes (the decode/video fast path) | | `draw/image $target $col $row $w $h $src ?opts` | Draw an image (see below) | ### Metadata | Function | Purpose | |---|---| | `draw/bounds $ctx` | Component bounds → `%{ :x :y :w :h }` (cells, local origin) | | `draw/theme $ctx` | Current theme colors as a map | | `draw/image-preview $ctx $col $row $w $h $path ?opts` | Half-block cells (CPU-only, thread-safe). `opts`: `:zoom`, `:pan-x`/`:pan-y` | | `draw/image-dims $path` | Image dimensions → `%{ :w :h }` (no GL, any thread) | ### `draw/image` — the one image blit `$src` is a path string, a `PixelBuffer`, or an `Image` (both `image/*` value types). GL hosts draw a deferred texture; ANSI hosts fall back to half-block cells automatically — no app-level branching. Panels take the rect in cells; surfaces in pixels.
[draw/image $ctx 0 0 $w $h "icon.svg"]          # path (SVG rasterizes on GL)
[draw/image $ctx 0 0 $w $h $buf :refresh true]  # per-frame re-upload (video)
`opts`: `:refresh true` re-uploads buffer contents every frame (the video pattern); `:zoom`/`:pan-x`/`:pan-y` control the ANSI half-block fallback view. ## Colors — one form everywhere Every color argument in every `draw/*`, `gl/*` and `scene/*` call accepts: - hex strings: `"#ff0000"`, `"#ff000080"` (with alpha), `"#f80"` (shorthand) - named: `"teal"`, `"red"`, `"gray"` … - ANSI-256 index: `196` (number or decimal string) - component maps: `%{ :r 255 :g 0 :b 0 :a 128 }` (0-255) Helpers: `color/parse` (any form → `%{ :r :g :b :a }`, nil on garbage), `color/rgb`, `color/hex`, `color/names`. Tweaks: `color/lighten` / `color/darken` (HSL lightness ±amount), `color/saturate` / `color/desaturate`, `color/mix`, `color/invert`, `color/complement`, `color/rotate-hue`, `color/grayscale`, `color/alpha` (get/set 0-255), `color/fade`, `color/luminance`, `color/contrast-text`. They take any color form and return a `"#rrggbb"` (or `"#rrggbbaa"`) hex string, so tweaks chain and paste straight into theme files:
[color/lighten "#101018"]                      # "#242437"
[color/darken [color/mix "#89b4fa" "teal"] 0.2]
[color/contrast-text "#89b4fa"]                # readable text on accent
## `surface/*` — SDL Surface Lifecycle SDL surfaces run on `hydra` (own OS window) and `retro` (embedded in a panel, video-overlay style). The same script works on both hosts. `surface/*` owns lifecycle only — drawing goes through `draw/*` with the surface id as target. On the ANSI host (`retra`) `surface/open` errors cleanly. | Function | Purpose | |---|---| | `surface/open %{:title "app" :w 640 :h 480}` | Open (auto-sized when `:w`/`:h` omitted) → surface id | | `surface/close $sid` / `surface/set-title $sid $text` | Close / rename | | `surface/resize $sid $w $h` / `surface/size $sid` | Resize / current size `%{ :w :h }` | | `surface/events $sid` | Drain pending event maps | | `surface/flush $sid` | Present the staged frame (low-level path) | | `surface/list` | Open surfaces as `%{ :id :title :w :h :closed }` | | `surface/read $sid` | Snapshot the staging frame → `%{ :data RGBA-bytes :w :h }` (test/screenshot path) | | `surface/quit` / `surface/keep-alive` | Quit semantics (hydra instance exit; retro panel close) | The declarative face is the `Surface` protocol: `render {|ctx|}` (ctx = `%{ :sid :w :h }`), `on-event {|ev|}`, `on-close {}` (return `:block` to veto the X button). The host drives render/flush — see `packages/surface-demo`. ## `image/*` — Pixel Data and the Transform Pipeline The `image/*` namespace is core (every native host, including the scripting binary) and manages two value types: - `Image` — a decoded RGBA frame plus output-format metadata. Every op returns a new `Image` (immutable pipeline); drop the reference and GC reclaims it. Produced by `image/open` / `image/decode`, encoded by `image/bytes` / `image/write` / `image/encode`. - `PixelBuffer` — a CPU-only RGBA frame whose pixel pointer is stable for its lifetime. Write new frames with `image/pixel-write` and `draw/image :refresh` reuses one GL texture (the video pattern). Produced by `image/load` / `image/pixel-buffer`. Thread-safe — safe from any app thread. The geometry ops are polymorphic over both types and return a value of the same type: | Function | Purpose | |---|---| | `image/open $path` / `image/decode $bytes` | Decode → `Image` | | `image/load $path` | Decode file → `PixelBuffer` (stable pointer) | | `image/pixel-buffer $w $h` | Zero-init `PixelBuffer` | | `image/pixel-write $buf $bytes ?offset` | `memcpy` bytes into a buffer in place | | `image/dims $v` / `image/free $v` | Dims `%{ :w :h }` / early release (either type) | | `image/resize $v $w $h` / `image/rotate $v $deg` | New resized/rotated value (same type) | | `image/flip $v` (vertical) / `image/flop $v` (horizontal) / `image/crop $v $x $y $w $h` | New transformed value (same type) | | `image/jpeg \| png \| webp $img ?opts` / `image/encode $img $fmt` / `image/bytes $img` / `image/write $img $path` / `image/base64 $img` | Format + encode an `Image` | The video pattern — decode on a worker, write into one stable buffer, refresh per frame:
def buf [image/pixel-buffer 320 240]
[image/pixel-write $buf $frame-bytes]
[draw/image $ctx 0 0 $w $h $buf %{ :refresh true }]
### Working with `Image` The `image/*` pipeline (open → resize/rotate/flip → format → encode) composes directly with surfaces: every op returns a new `Image` value, and `draw/image` accepts `Image` sources on any target. No temp files, no manual conversion:
set sid [surface/open %{ :title "viewer" :w 640 :h 480}]

set img [image/open "photo.jpg"]
set img [image/resize $img 320 240]           # transform pipeline
[draw/image $sid 0 0 320 240 $img]            # straight onto the surface
[draw/image $sid 320 0 240 320 [image/rotate $img 90]]
[surface/flush $sid]
Because the geometry ops are polymorphic, no converters are needed: `[image/resize $buf ...]` works on a `PixelBuffer` straight from `image/load`, and `[image/rotate $buf 90]` keeps the stable-pointer type for a subsequent `image/pixel-write`. ## Retained Canvases (`draw/canvas`) A retained pixel-space canvas that renders as a per-app layer, independent of the `Panel.draw` lifecycle (charts, diagrams, badges — and the viewport `gl/*` 3D scenes render through). Create and place with `draw/canvas` / `draw/set-bounds`; draw into it with the same polymorphic `draw/*` functions — the canvas element is the target. Workspace hosts only (`retro`/`retra`) — surface-window apps (hydra) draw on surfaces instead.
def cv [draw/canvas %{ :width 80 :height 25 :bg "#000000" }]

# in draw, position it over the panel (cell coords):
[draw/set-bounds $cv $bounds~x $bounds~y $bounds~w $bounds~h]
[draw/clear $cv "#000000"]
[draw/fill-rect $cv 10 5 30 10 "#ff0000"]
[draw/line $cv 0 0 80 25 "#00ff00"]
[draw/circle $cv 40 12 5 "#0000ff"]
| Function | Purpose | |---|---| | `draw/canvas %{:width … :height … :bg …}` | Create a canvas element | | `draw/set-bounds $canvas $x $y $w $h` | Reposition (cell coords) | ## `gl/*` — 3D Scene A small immediate-mode 3D API rendered through a canvas (`draw/canvas`). GL-only. `gl/reset` clears the geometry buffer for the next frame (`gl/bg` paints the background).
def cv [draw/canvas %{ :width 80 :height 40 }]

[gl/scene $cv]                                   # attach 3D scene to canvas
[gl/reset]                                       # clear vertex buffer
[gl/perspective 60.0 1.5 0.1 100.0]
[gl/lookat 0 0 5  0 0 0  0 1 0]
[gl/identity]
[gl/translate -1.5 0 0]
[gl/triangle 0.0 1.0 0.0  -1.0 -1.0 0.0  1.0 -1.0 0.0  "#ff0000"]
[gl/present $cv]
| Function | Purpose | |---|---| | `gl/scene $canvas` | Attach 3D scene to canvas | | `gl/reset` | Clear the vertex buffer (not the background — see `gl/bg`) | | `gl/perspective $fov $aspect $near $far` | Perspective projection | | `gl/lookat $ex $ey $ez $cx $cy $cz $ux $uy $uz` | View matrix | | `gl/identity` / `gl/translate` / `gl/rotate-x\|-y\|-z` / `gl/scale` | Model matrix stack | | `gl/triangle … $color` / `gl/quad … $color` | Push geometry | | `gl/line $x0 $y0 $z0 $x1 $y1 $z1 $color ?width` | 3D line | | `gl/bg $color` / `gl/depth $bool` | Clear color / depth test | | `gl/present $canvas` | Mark scene+canvas dirty | ## `scene/*` — Cross-Panel World Layer A workspace-wide overlay shared across all panels. Nodes have workspace-pixel coordinates and can be dragged between panels. GL-only — no-op on ANSI backends.
def id [scene/add :rect :at @{100 100} :size @{80 40} :color "#ff0000" :filled true :z 1]

[scene/node-set $id :at @{120 120} :color "#00ff00"]
echo [scene/node-get $id]
# => %{ :id 1 :kind :rect :x 120 :y 120 :w 80 :h 40 :opacity 1.0 :z 1 :visible true }

[scene/select $id]                               # draw halo handles
echo [scene/selected]                            # => $id
echo [scene/hit-test $x $y]                      # node id at (x, y) or 0

[scene/remove $id]
[scene/clear]
| Function | Purpose | |---|---| | `scene/add :kind :at @{x y} :size @{w h} :color … :width … :filled … :text … :z … :opacity … :tag …` | Add a node | | `scene/remove $id` / `scene/clear` / `scene/count` | Remove / clear / count | | `scene/node-set $id :at … :to … :color … :width … :filled … :opacity … :z … :visible … :tag …` | Mutate | | `scene/node-get $id` | Describe a node | | `scene/hit-test $x $y` | Topmost node at coords | | `scene/select $id` / `scene/selected` | Selection | Cross-panel drag-and-drop: `tui/begin-scene-drag` starts a drag; the main thread renders a ghost that follows the cursor; on mouse-up the target panel receives a `:scene-drop` event and the source receives `:scene-drag-end`. GL-only.
[tui/begin-scene-drag %{ :kind :text :text "hello" :color "#ff0" }]
## Geometry and Capability Probes | Function | Purpose | |---|---| | `tui/cell-size` | Font cell size → `%{ :w :h }` (the single source of truth) | | `tui/px->cell $x $y` / `tui/cell->px $col $row` | Convert `@[x y]` ↔ `@[col row]` | | `tui/backend` | `:gl` or `:ansi` — the canonical host probe | | `tui/gl?` | True on GL hosts (pixels, textures, SDL surfaces) | All size/rect maps use `:w`/`:h`: `draw/bounds`, `tui/cell-size`, `surface/size`, `draw/image-dims`, `image/dims`. Bounds-shaped maps add `:x`/`:y`. ## Choosing The Right Tool | You want to... | Use | |---|---| | Render text and UI in a panel | `draw/*` cell mode | | Draw lines/shapes over a panel | `draw/*` pixel mode | | Show an image (any host) | `draw/image` — ANSI fallback is built in | | Play decoded video | `image/pixel-buffer` + `image/pixel-write` + `draw/image :refresh` | | Own OS window(s) / in-panel surface | `surface/open` + `draw/*` on the surface id | | A retained chart/diagram overlay | `draw/canvas` + `draw/*` on the canvas | | Transient badges across panels | `scene/*` | | Render a 3D model | `gl/*` | | Encode/transform an image | `image/*` (polymorphic over `Image` / `PixelBuffer`) | ## Performance Notes - `draw/*` calls are queued and batched per frame; panel cell buffers and pixel command lists are double-buffered across threads. - Scene and GL state persist across frames — call `gl/reset` at the start of each redraw. - GL textures load through deferred per-thread queues (`draw/image` returns false on the panel's first frame while the texture uploads, then re-renders). The stable-pointer `image/pixel-buffer` avoids per-frame texture allocation for video. - ANSI backends (`retra`) get half-block fallbacks for images and cell-mode rendering; `scene/*`, `gl/*` and SDL surfaces are GL-only. ## Reference - `packages/surface-demo` — `Surface` protocol + `draw/*` on surface ids - `packages/cube` — `draw/canvas` + `gl/*` 3D - `packages/ants`, `packages/tetris` — cell-mode animation - `packages/image` — `draw/image` with `Image` sources and pan/zoom - `packages/stdext/src/ffmpeg` — the video pattern end-to-end