parent
dd2b153bbe
commit
298b09ddc9
@ -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( |
||||
("<clear=line><b>Mandelbrot</b> 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) |
||||
)) |
||||
Loading…
Reference in new issue