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
- Guest program: bouncing balls14 days ago
- New version after 3 months, and turtle graphics24 days ago
- A little timer app44 days ago
- New version after 4 monthsJun 28, 2024
- Visualizing the digits of πMay 04, 2024
- A little recursion problemApr 15, 2024
- Drawing histogramsApr 03, 2024
- The simplest possible ditherMar 30, 2024
- All the 1-D cellular automataMar 23, 2024
Leave a comment
Log in with itch.io to leave a comment.