From 298b09ddc95abbe3eae4f02e43355635f7537be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Tue, 21 Jul 2026 23:59:46 +0200 Subject: [PATCH] add generated mandelbrot demo --- lua/demo/mandelbrot.lua | 124 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 lua/demo/mandelbrot.lua diff --git a/lua/demo/mandelbrot.lua b/lua/demo/mandelbrot.lua new file mode 100644 index 0000000..5ffd518 --- /dev/null +++ b/lua/demo/mandelbrot.lua @@ -0,0 +1,124 @@ +-- The Mandelbrot set rendered as colored ASCII art, drawn row by row with +-- the tui positioning tools (alternate screen, moveTo, clearScreen), then a +-- short animated zoom into "seahorse valley". When output is piped, it +-- degrades to a single plain-text render. +-- +-- Run with: cargo run -- lua/demo/mandelbrot.lua + +-- One character + color per escape-speed bucket, slowest to fastest. +local CHARS = { ".", ":", "-", "=", "+", "*", "#", "%", "@" } +local COLORS = { + "#1e3a8a", "#1d4ed8", "#0284c7", "#0d9488", "#16a34a", + "#84cc16", "#eab308", "#f97316", "#ef4444", +} + +--- How many iterations until the point escapes, or nil if it stays bounded +--- (i.e. belongs to the set). +local function escapeTime(cx, cy, maxIter) + local x, y = 0, 0 + for i = 1, maxIter do + local x2, y2 = x * x, y * y + if x2 + y2 > 4 then + return i + end + y = 2 * x * y + cy + x = x2 - y2 + cx + end + return nil +end + +--- Render one row as a styled string, batching same-colored runs into a +--- single tag so the escape-code overhead stays small. +local function renderRow(cy, xMin, dx, cols, maxIter) + local parts, run, runColor = {}, {}, nil + local function flush() + if #run == 0 then + return + end + local text = table.concat(run) + parts[#parts + 1] = runColor and ("<%s>%s"):format(runColor, text) or text + run = {} + end + for col = 0, cols - 1 do + local i = escapeTime(xMin + col * dx, cy, maxIter) + local ch, color = " ", nil -- inside the set: leave the cell empty + if i then + local idx = math.clamp(math.floor(math.sqrt(i / maxIter) * #CHARS) + 1, 1, #CHARS) + ch, color = CHARS[idx], COLORS[idx] + end + if color ~= runColor then + flush() + runColor = color + end + run[#run + 1] = ch + end + flush() + return tui.styled(table.concat(parts)) +end + +--- Draw one full frame. Interactively each row is placed with tui.moveTo +--- (row 1 is a header line); piped, rows are just printed in order. +local function drawFrame(cx, cy, scale, maxIter, cols, rows, interactive) + -- Terminal cells are roughly twice as tall as they are wide, so step + -- twice as far in y to keep the set round. + local dx = scale / cols + local dy = dx * 2 + local xMin = cx - dx * cols / 2 + local yMin = cy - dy * rows / 2 + + if interactive then + tui.moveTo(1, 1) + tui.raw(tui.styled( + ("Mandelbrot center %.9f%+.9fi width %.2e max iter %d") + :format(cx, cy, scale, maxIter) + )) + end + for row = 1, rows do + local line = renderRow(yMin + (row - 1) * dy, xMin, dx, cols, maxIter) + if interactive then + tui.moveTo(row + 1, 1) + tui.raw(line) + else + print(line) + end + end +end + +tui.setTitle("mandelbrot") + +local info = tui.screenInfo() +local cols = info.width or 100 +local rows = (info.height or 32) - 2 -- header + one spare line for the cursor + +-- The classic full view: x roughly [-2.1, 1.1]. +local fullCx, fullCy, fullScale = -0.5, 0, 3.2 + +if not info.tty then + drawFrame(fullCx, fullCy, fullScale, 80, cols, rows, false) + return +end + +tui.altScreen(true) +tui.clearScreen() +tui.showCursor(false) + +drawFrame(fullCx, fullCy, fullScale, 80, cols, rows, true) +os.sleep(2) + +-- Zoom into "seahorse valley" between the main cardioid and the period-2 +-- bulb, cranking up the iteration budget as detail gets finer. +local cx, cy = -0.743643887, 0.131825904 +local scale = fullScale +for frame = 1, 40 do + scale = scale * 0.85 + drawFrame(cx, cy, scale, 60 + frame * 8, cols, rows, true) + os.sleep(0.03) +end +os.sleep(2) + +tui.altScreen(false) +tui.showCursor(true) +tui.success(("zoomed to a window %.2e units wide — that's about %dx magnification"):format( + scale, + math.round(fullScale / scale) +))