Smooth camera zooming/scrolling around arbitrary points

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
User avatar
spill
Prole
Posts: 27
Joined: Thu May 07, 2015 1:53 am
Contact:

Smooth camera zooming/scrolling around arbitrary points

Post by spill »

I wanted to have a löve camera that could zoom in on an arbitrary part of the screen, similar to how google maps works. I couldn't find any complete solution online, either for löve or for opengl in general. Since it was a hassle to get the camera to behave how as I wanted it to, I figured I'd save the next person the same pain by providing my working solution. :awesome:

Code: Select all

-- camera.love
local Camera = {}
Camera.__index = Camera

function Camera:new(x, y, sx, sy)
    -- Create a new camera with (x,y) positioned on the screen at (sx,sy) or centered if sx/sy are not given
    local camera = setmetatable({scale=1, _scale=1, shake=0}, self)
    camera:moveTo(x,y,sx,sy)
    return camera
end

function Camera:moveTo(x,y,sx,sy)
    -- Move the camera so that (x,y) is positioned on the screen at (sx,sy) or centered if sx/sy are not given
    sx = sx or .5*love.graphics.getWidth()
    sy = sy or .5*love.graphics.getHeight()
    self._x, self._y = x-sx/self._scale, y-sy/self._scale
    self.target = {
        screenX=sx, screenY=sy,
        prevScreenX=sx, prevScreenY=sy,
    }
end

function Camera:getWorldPos(screenX, screenY)
    -- Return the position of the screen coordinates in world coordinates.
    return self._x + screenX / self._scale, self._y + screenY / self._scale
end

function Camera:getScreenPos(worldX, worldY)
    -- Return the position of the world coordinates in screen coordinates.
    return (worldX - self._x) * self._scale, (worldY - self._y) * self._scale
end

function Camera:getMidpoint()
    -- Return the world coordinates of the center of the screen. Do not access/modify camera._x or camera._y directly.
    return self:getWorldPos(.5*love.graphics.getWidth(), .5*love.graphics.getHeight())
end

function Camera:set()
    -- Call this function once before drawing all the objects in the world.
    local prevX, prevY = self:getWorldPos(self.target.prevScreenX, self.target.prevScreenY)
    self.target.prevScreenX, self.target.prevScreenY = self.target.screenX, self.target.screenY
    self._scale = self.scale
    local x, y = self:getWorldPos(self.target.screenX, self.target.screenY)
    self._x, self._y = self._x - (x - prevX), self._y - (y - prevY)

    love.graphics.push()
    love.graphics.translate(self.target.screenX, self.target.screenY)
    love.graphics.scale(self._scale, self._scale)
    love.graphics.translate(-prevX, -prevY)
    love.graphics.translate(love.math.randomNormal(self.shake), love.math.randomNormal(self.shake))
end

function Camera:unset()
    -- Call this function once after drawing all the objects in the world.
    love.graphics.pop()
end

return Camera
And here's an example usage:

Code: Select all

-- main.lua
local Camera = require 'camera'

function love.load()
    dots = {}; for i=1,100 do dots[i] = {x=math.random(1000), y=math.random(1000)} end
    camera = Camera:new(dots[1].x, dots[1].y)
end

function love.draw()
    -- Draw world objects
    camera:set()
    for i, dot in ipairs(dots) do
        love.graphics.setColor(((i*0.61803398875)%1)*255,0,255) -- Magic unique color per dot
        love.graphics.circle('fill',dot.x,dot.y,20)
    end
    camera:unset()
    camera.shake = camera.shake * .9

    -- Draw UI
    love.graphics.setColor(255,0,0)
    love.graphics.circle('line',.5*love.graphics.getWidth(),.5*love.graphics.getHeight(),15)
end

function love.mousepressed(x,y,button)
    -- Scroll for zooming
    if button == 'wu' then
        camera.scale = camera.scale / 1.1
    elseif button == 'wd' then
        camera.scale = camera.scale * 1.1
    -- Left click starts a drag, everything else does nothing.
    elseif button ~= 'l' then return end
    -- Set where the camera is zooming/scrolling relative to.
    camera.target.screenX, camera.target.screenY = x, y
    camera.target.prevScreenX, camera.target.prevScreenY = x, y
end

function love.mousemoved(x,y,dx,dy)
    -- Drag the camera around with left click.
    if love.mouse.isDown('l') then
        camera.target.screenX, camera.target.screenY = x, y
    end
end

function love.keypressed(key)
    -- Focus the camera on a random dot
    if key == 'r' then
        local r = dots[math.random(#dots)]
        camera:moveTo(r.x, r.y)
    -- Shake the camera
    elseif key == ' ' then
        camera.shake = camera.shake + 16
    end
end
Demo of the scrolling/zooming in action.
Demo of the scrolling/zooming in action.
camera.gif (431.6 KiB) Viewed 2749 times
Screenshake, because I'm a fan of Vlambeer.
Screenshake, because I'm a fan of Vlambeer.
screenshake.gif (110.24 KiB) Viewed 2749 times
camera.love
A simple demo with the code shown above.
(2.41 KiB) Downloaded 226 times
Ideally, this should work for rotation too, but it's a pain in the neck to implement and my current use doesn't require it. If anyone wanted/needed to implement rotation for themselves, I'd be interested to see how they did it, so please post it here.
User avatar
Garmelon
Prole
Posts: 19
Joined: Tue Jun 02, 2015 5:25 pm

Re: Smooth camera zooming/scrolling around arbitrary points

Post by Garmelon »

I did something like that myself in an orbital mechanics simulation thingy I made a while ago.
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot] and 2 guests