Interactively zooming in to the Mandelbrot set on a touchscreen
Lua Carousel » Devlog

I've implemented the Mandelbrot set a few times, but this was still surprisingly delightful.
A basic version of the mandelbrot set is just 20 lines:
function mandelbrot(x0, y0, w, n)
local zoom = w/Safe_width
for y = 0, Safe_height do
local ci = y0 + y*zoom
for x = 0, Safe_width do
local cr = x0 + x*zoom
local niters = mandel_iters(cr, ci, n)
color(1-niters/n, 1-niters/n, 1-niters/n)
pt(x, y)
end end end
function mandel_iters(cr, ci, n)
local zr, zi = 0, 0
for i=0,n-1 do
local nzr = zr*zr - zi*zi + cr
local nzi = 2*zr*zi + ci
if nzr*nzr + nzi*nzi > 4 then return i end
zr, zi = nzr, nzi
end
return n
end
mandelbrot(-2.2, -1.5, 3, 256)
Another 20 lines adds color.
A 100 more lines, and we can give it panning and zooming on a multitouch screen. Since it's a little CPU intensive, we'll rerender at a much lower resolution while panning/zooming are in progress.
pan_zoom_coarsen = 10 -- how much to coarsen the picture on pan/zoom. 1 = perfect, larger number = more coarse
-- 16-color Mandelbrot palette
Palette = {
{66, 30, 15}, -- brown 3
{25, 7, 26}, -- dark violet
{9, 1, 47}, -- darkest blue
{4, 4, 73}, -- blue 5
{0, 7, 100}, -- blue 4
{12, 44, 138}, -- blue 3
{24, 82, 177}, -- blue 2
{57, 125, 209}, -- blue 1
{134, 181, 229}, -- blue 0
{211, 236, 248}, -- lightest blue
{241, 233, 191}, -- lightest yellow
{248, 201, 95}, -- light yellow
{255, 170, 0}, -- dirty yellow
{204, 128, 0}, -- brown 0
{153, 87, 0}, -- brown 1
{106, 52, 3}, -- brown 2
}
mandel = {}
work_factor = 1
function mandelbrot(x0, y0, w, W, H, n)
mandel = {}
mandel.x0, mandel.y0 = x0, y0
local zoom = w/W
for y = 1, H/work_factor do
local row = {}
table.insert(mandel, row)
local ci = y0 + y*work_factor*zoom
for x = 1, W/work_factor do
local cr = x0 + x*work_factor*zoom
table.insert(row, mandel_iters(cr, ci, n))
end end end
function render(X, Y, W, H)
color(0.5, 0.5, 0.5)
rect('line', X, Y, W, H)
for y = 1, H/work_factor do
for x = 1, W/work_factor do
local niters = mandel[y][x]
local c = Palette[niters%16+1]
color(c[1]/255, c[2]/255, c[3]/255)
rect('fill', X+x*work_factor, Y+y*work_factor, work_factor, work_factor)
end end end
function mandel_iters(cr, ci, n)
local zr, zi = 0, 0
for i=0,n-1 do
local nzr = zr*zr - zi*zi + cr
local nzi = 2*zr*zi + ci
if nzr*nzr + nzi*nzi > 4 then return i end
zr, zi = nzr, nzi
end
return n
end
dirty = true
function car.draw()
if dirty then
-- map (0, Safe_width) on the viewport at zoom 1.0
-- to (-2.2, 0.8)
local w = 3/v.zoom
local x0 = -2.2 + v.x*3/Safe_width
local y0 = -1.1 + v.y*3/Safe_width
mandelbrot(x0, y0, w, Safe_width, Safe_height, 256)
dirty = false
end
render(0, 0, Safe_width, Safe_height)
color(1,1,1)
g.print(('zoom: %.2f'):format(v.zoom), 30,60)
g.print(('x: %f'):format(mandel.x0), 30,85)
g.print(('y: %f'):format(mandel.y0), 30,110)
end
-- pan/zoom surface
-- v: the viewport
-- in units of screen pixels
if v == nil then
v ={x=0, y=0, w=Safe_width, h=Safe_height, zoom=1.0}
end
f,s = nil -- ids of first and second touches
start, curr = {}, {} -- coords of touches
initzoom = nil
initpos = nil -- for panning
function car.keychord_press(chord, key)
if chord == 'M--' then
v.zoom = v.zoom * 0.9
dirty = true
elseif chord == 'M-=' then
v.zoom = v.zoom / 0.9
dirty = true
elseif chord == 'M-0' then
v.zoom = 1.0
dirty = true
elseif chord == 'up' then
v.y = v.y - iscale(Safe_height/10)
dirty = true
elseif chord == 'down' then
v.y = v.y + iscale(Safe_height/10)
dirty = true
elseif chord == 'left' then
v.x = v.x - iscale(Safe_width/10)
dirty = true
elseif chord == 'right' then
v.x = v.x + iscale(Safe_width/10)
dirty = true
elseif chord == 'pageup' or chord == 'C-up' then
v.y = v.y - iscale(Safe_height/2)
dirty = true
elseif chord == 'pagedown' or chord == 'C-down' then
v.y = v.y + iscale(Safe_height/2)
dirty = true
elseif chord == 'C-left' then
v.x = v.x - iscale(Safe_width/2)
dirty = true
elseif chord == 'C-right' then
v.x = v.x + iscale(Safe_width/2)
dirty = true
end end
function car.mousepressed(x, y) -- only if no touchscreen
initpos = {x=v.x, y=v.y}
start.mouse = {x=x, y=y}
dirty = true
work_factor = pan_zoom_coarsen
end
function car.mousereleased(x, y)
initpos = nil
dirty = true
work_factor = 1
end
function car.update()
if f then return end -- touchscreen exists
if initpos == nil then return end
local x,y = love.mouse.getPosition()
v.x = initpos.x + iscale(start.mouse.x - x)
v.y = initpos.y + iscale(start.mouse.y - y)
dirty = true
work_factor = pan_zoom_coarsen
end
function car.touchpressed(id, x,y, ...)
car.mousepressed = nil -- disable the mouse the first time the touchscreen triggers
car.mousereleased = nil
if f == nil then
f = id
initpos = {x=v.x, y=v.y}
else
s = id
initzoom = v.zoom
end
start[id] = {x=x, y=y}
curr[id] = {x=x, y=y}
dirty = true
work_factor = pan_zoom_coarsen
end
function car.touchreleased(id, x,y, ...)
f,s = nil
start, curr = {}, {}
initzoom = nil
initpos = nil
dirty = true
work_factor = 1
end
function car.touchmoved(id, x,y, ...)
if start[id] then
curr[id] = {x=x, y=y}
if s then
local oldzoom = v.zoom
v.zoom = dist(curr[f], curr[s])/dist(start[f], start[s])*initzoom
adjust_viewport(oldzoom, v.zoom)
elseif f then
v.x = initpos.x + iscale(start[f].x - x)
v.y = initpos.y + iscale(start[f].y - y)
end
dirty = true
work_factor = pan_zoom_coarsen
end end
function adjust_viewport(oldzoom, zoom)
-- ensure centroid of fingers remains in view
local c = centroid(curr[f], curr[s])
v.x = v.x + c.x/oldzoom - c.x/zoom
v.y = v.y + c.y/oldzoom - c.y/zoom
end
function centroid(a, b)
return{x=(a.x+b.x)/2, y=(a.y+b.y)/2}
end
function vx(sx) return scale(sx-v.x) end
function vy(sy) return scale(sy-v.y) end
function scale(d) return d*v.zoom end
function sx(vx) return v.x + iscale(vx) end
function sy(vy) return v.y + iscale(vy) end
function iscale(d) return d/v.zoom end
function dist(p1, p2)
return ((p2.x-p1.x)^2 + (p2.y-p1.y)^2) ^ 0.5
end
If you try pasting any of these programs into Lua Carousel, remember to first run the abbreviations on one of the example screens. Or if you've deleted that screen, here are the abbreviations I used in this post:
g = love.graphics rect = g.rectangle color = g.setColor
Get Lua Carousel
Lua Carousel
Write programs on desktop and mobile
| Status | In development |
| Category | Tool |
| Author | Kartik Agaram |
| Tags | LÖVE |
More posts
- Programming on your device with your preferred language51 days ago
- Lua Carousel: program on the device you have, with docs at your fingertipsMay 12, 2025
- Pong Wars, MMO editionFeb 16, 2025
- New version after 41 days, and stop-motion animationFeb 15, 2025
- Drawing with a pen on a pendulumJan 11, 2025
- New version after 16 daysJan 04, 2025
- New version after 9 daysDec 19, 2024
- New version after 3 daysNov 17, 2024
- New version after 40 daysNov 14, 2024
- Turn your phone or tablet into a chess clockNov 01, 2024
Leave a comment
Log in with itch.io to leave a comment.