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