├── LICENSE.md ├── README.md ├── editgrid.lua ├── examples ├── gamera │ ├── gamera.lua │ └── main.lua ├── hump │ ├── humpcamera.lua │ └── main.lua └── simple │ └── main.lua └── preview.gif /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Calvin Rose 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Image Not Found](https://github.com/bakpakin/Editgrid/raw/master/preview.gif) 2 | # Editgrid 3 | 4 | ## What's this? 5 | Editgrid is a module that implements a grid that automatically scales its resolution, like the background grids in 3d modeling software like blender. 6 | Its great for level editors and the like because you can zoom in and out without loosing sight of the gridlines. 7 | It also converts screen coordinates to grid coordinates and vice versa. 8 | 9 | Editgrid is also useful for adding a debugging background to games - just call editgrid.draw(camera) with 10 | a [gamera](https://github.com/kikito/gamera) or [HUMP](http://vrld.github.io/hump/) camera, or just any table. 11 | 12 | ## How to use 13 | Place editgrid.lua in your project and require it like so: 14 | ```lua 15 | local editgrid = require "editgrid" 16 | ``` 17 | 18 | ## API 19 | 20 | #### Drawing a grid 21 | ```lua 22 | editgrid.draw(camera, visuals) 23 | ``` 24 | Draws a grid to the screen from the perspective of an optional camera with optional visual effects. 25 | 26 | `camera` can be a HUMP or gamera camera, as well as a table containing the following: 27 | ```lua 28 | local camera = { 29 | x = 20, 30 | y = 20, 31 | zoom = 2, 32 | angle = math.pi/2, 33 | sx = 5, 34 | sy = 5, 35 | sw = love.graphics.getWidth() - 10, 36 | sh = love.graphics.getHeight() - 10 37 | } 38 | ``` 39 | * `(x, y)` -- the point the camera is looking at. Default is (0, 0). 40 | * `zoom` -- the zoom factor of the camera. Default is 1. 41 | * `angle` -- the angle of the camera. Default is 0. 42 | * `(sx, sy, sw, sh)` -- the clipping rectangle (scissor rectangle) for the camera. By default, 43 | the camera draws to the whole screen (0, 0, love.graphics.getWidth(), love.graphics.getHeight()). 44 | 45 | All functions in Editgrid that require a `camera` can use all types of cameras. 46 | 47 | `visuals` should be a table containing the following: 48 | ```lua 49 | local visuals = { 50 | size = 100, 51 | subdivisions = 5, 52 | color = {128, 140, 250}, 53 | drawScale = false, 54 | xColor = {255, 255, 0}, 55 | yColor = {0, 255, 255}, 56 | fadeFactor = 0.3, 57 | textFadeFactor = 0.5, 58 | hideOrigin = true, 59 | interval = 200 60 | } 61 | ``` 62 | * `size` -- the distance between each major subdivision at 1x zoom. Default is 256. 63 | * `subdivisions` -- the number of minor subdivisions between each major subdivision. Default is 4. 64 | * `color` -- a list of three numbers representing the rgb values of the grid lines. Default is {220, 220, 220}. 65 | * `drawScale` -- boolean indicating if the coordinate value is drawn for each gridline. Default is true. 66 | * `xColor` -- color of the x axis. Default is {255, 0, 0} (red). 67 | * `yColor` -- color of the y axis. Default is {0, 255, 0} (green). 68 | * `fadeFactor` -- color multiplier on subdivision grid lines. For example, if `color` is {100, 100, 100} and `fadeFactor` is 69 | 0.8, then the color of the minor gridlines will be {80, 80, 80}. Default is 0.5. 70 | * `textFadeFactor` -- color multiplier on grid labels. Similar to `fadeFactor`. Default is 1.0. 71 | * `hideOrigin` -- boolean indicating whether or not to hide the origin circle. Default is false. 72 | * `interval` -- optional argument that makes the grid use a fixed interval in world space instead of scaling with camera zoom. 73 | 74 | All functions in Editgrid that require a `visuals` table expect this format. 75 | 76 | ```lua 77 | editgrid.push(camera) 78 | -- draw() 79 | -- stuff() 80 | love.graphics.pop() 81 | ``` 82 | Editgrid enables drawing with compatible cameras in cross platform way. Surround normal drawing 83 | commands with an `editgrid.push` and a `love.graphics.pop` to convert screen space to grid 84 | space in your drawing. This should have equivalent results to drawing with a compatible 85 | camera module. 86 | 87 | #### Querying the grid 88 | ```lua 89 | local newx, newy = editgrid.convertCoords(camera, visuals, src, dest, x, y) 90 | ``` 91 | Converts coordinates from one coordinate system to another. `src` and `dest` are 92 | the source coordinate system and destination coordinate system respectively, and can each be one of 93 | three strings: `"screen"`, `"world"`, and `"cell"`. For example, to convert screen coordinates to world 94 | coordinates, say for mouse interaction, let `src = "screen"` and `dest = "world"`. `"cell"` coordinates 95 | are based on the cells that the camera sees on the screen; also, all `"cell"` coordinates are integers. 96 | 97 | ```lua 98 | local worldx, worldy = editgrid.toWorld(camera, screenx, screeny) 99 | ``` 100 | Converts screen coordinates to world coordinates. 101 | Shortcut for `editgrid.convertCoords(camera, nil, "screen", "world", screenx, screeny)` 102 | 103 | ```lua 104 | local screenx, screeny = editgrid.toScreen(camera, worldx, worldy) 105 | ``` 106 | Converts world coordinates to screen coordinates. 107 | Shortcut for `editgrid.convertCoords(camera, nil, "world", "screen", worldx, worldy)` 108 | 109 | ```lua 110 | local vx, vy, vw, vh = editgrid.visible(camera) 111 | ``` 112 | Gets an Axis Aligned Bounding Box (AABB) containing the visible part of the grid that can be seen 113 | from the camera. May contain some non-visible portions of the grid if the camera angle is not zero. 114 | 115 | ```lua 116 | local interval = editgrid.minorInterval(camera, visuals) 117 | ``` 118 | Gets the distance between minor grid lines (in world space) on the screen. To get the 119 | distance in screen space, just multiply `interval` by camera zoom. 120 | 121 | ```lua 122 | local interval = editgrid.majorInterval(camera, visuals) 123 | ``` 124 | Similar to `editgrid.minorInterval`, but returns the distance between major grid lines (the bolder grid lines). 125 | 126 | #### Wrapping it all together 127 | ```lua 128 | local grid = editgrid.grid(camera, visuals) 129 | 130 | grid:draw() -- Equivalent to editgrid.draw(camera, visuals) 131 | grid:push() -- Equivalent to editgrid.push(camera) 132 | local newx, newy = grid:convertCoords(src, dest, x, y) -- Equivalent to editgrid.convertCoords(camera, visuals, src, dest, x, y) 133 | local worldx, worldy = grid:toWorld(x, y) -- Equivalent to editgrid.toWorld(camera, x, y) 134 | local screenx, screeny = grid:toScreen(x, y) -- Equivalent to editgrid.toScreen(camera, x, y) 135 | local vx, vy, vw, vh = grid:visible() -- Equivalent to editgrid.visible(camera) 136 | local minor = grid:minorInterval() -- Equivalent to editgrid.minorInterval(camera, visuals) 137 | local major = grid:majorInterval() -- Equivalent to editgrid.majorInterval(camera, visuals) 138 | ``` 139 | Instead of passing a `camera` and a `visuals` variable around all the time for Editgrid's functions, 140 | Editgrid can create a grid object with methods that can be called with colon syntax. The `camera` and `visuals` 141 | tables can be updated at any time without any adverse effects. 142 | 143 | ## Bugs 144 | If there are bugs or you want to request features, feel free to submit issues. 145 | 146 | ## License 147 | Copyright (c) 2015 Calvin Rose 148 | 149 | Permission is hereby granted, free of charge, to any person obtaining a copy of 150 | this software and associated documentation files (the "Software"), to deal in 151 | the Software without restriction, including without limitation the rights to 152 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 153 | the Software, and to permit persons to whom the Software is furnished to do so, 154 | subject to the following conditions: 155 | 156 | The above copyright notice and this permission notice shall be included in all 157 | copies or substantial portions of the Software. 158 | 159 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 160 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 161 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 162 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 163 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 164 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 165 | -------------------------------------------------------------------------------- /editgrid.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2015 Calvin Rose 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | ]] 21 | 22 | local lg = love.graphics 23 | 24 | local EMPTY = {} 25 | 26 | local function floor(x, y) 27 | return math.floor(x / y) * y 28 | end 29 | 30 | local function mod(x, y) 31 | return x - floor(x, y) 32 | end 33 | 34 | local function checkType(x, typename, name) 35 | assert( 36 | type(x) == typename, 37 | "Expected "..name.." (type = "..type(x)..") to be a "..typename.."." 38 | ) 39 | return x 40 | end 41 | 42 | local function unpackCamera(t) 43 | local sx, sy, sw, sh 44 | if t.getWindow then -- assume t is a gamera camera 45 | sx, sy, sw, sh = t:getWindow() 46 | else 47 | sx, sy, sw, sh = 48 | t.sx or 0, 49 | t.sy or 0, 50 | t.sw or lg.getWidth(), 51 | t.sh or lg.getHeight() 52 | end 53 | return 54 | t.x or 0, 55 | t.y or 0, 56 | t.scale or t.zoom or 1, 57 | t.angle or t.rot or 0, 58 | sx, sy, sw, sh 59 | end 60 | 61 | local DEFAULT_COLOR = {0.97, 0.97, 0.97} 62 | local DEFAULT_X_COLOR = {1, 0, 0} 63 | local DEFAULT_Y_COLOR = {0, 1, 0} 64 | 65 | local function unpackVisuals(t, zoom) 66 | local size = t.size or 256 67 | if type(size) == "function" then 68 | size = size(zoom) 69 | end 70 | local sds = t.subdivisions or 4 71 | if type(sds) == "function" then 72 | sds = sds(size, zoom) 73 | end 74 | local color = t.color or DEFAULT_COLOR 75 | local drawScale 76 | if t.drawScale == nil then 77 | drawScale = true 78 | else 79 | drawScale = t.drawScale 80 | end 81 | local xColor = t.xColor or DEFAULT_X_COLOR 82 | local yColor = t.yColor or DEFAULT_Y_COLOR 83 | local fadeFactor = t.fadeFactor or 0.5 84 | local textFadeFactor = t.textFadeFactor or 1.0 85 | local hideOrigin = t.hideOrigin 86 | return size, sds, drawScale, color, xColor, yColor, fadeFactor, textFadeFactor, hideOrigin 87 | end 88 | 89 | local function getGridInterval(visuals, zoom) 90 | if visuals.interval then 91 | return visuals.interval 92 | else 93 | local size, sds = unpackVisuals(visuals, zoom) 94 | return size * math.pow(sds, -math.ceil(math.log(zoom, sds))) 95 | end 96 | end 97 | 98 | local function visible(camera) 99 | camera = checkType(camera or EMPTY, "table", "camera") 100 | local camx, camy, zoom, angle, sx, sy, sw, sh = unpackCamera(camera) 101 | local w, h = sw / zoom, sh / zoom 102 | if angle ~= 0 then 103 | local sin, cos = math.abs(math.sin(angle)), math.abs(math.cos(angle)) 104 | w, h = cos * w + sin * h, sin * w + cos * h 105 | end 106 | return camx - w * 0.5, camy - h * 0.5, w, h 107 | end 108 | 109 | local function toWorld(camera, screenx, screeny) 110 | checkType(screenx, "number", "screenx") 111 | checkType(screeny, "number", "screeny") 112 | camera = checkType(camera or EMPTY, "table", "camera") 113 | local camx, camy, zoom, angle, sx, sy, sw, sh = unpackCamera(camera) 114 | local sin, cos = math.sin(angle), math.cos(angle) 115 | local x, y = (screenx - sw/2 - sx) / zoom, (screeny - sh/2 - sy) / zoom 116 | x, y = cos * x - sin * y, sin * x + cos * y 117 | return x + camx, y + camy 118 | end 119 | 120 | local function toScreen(camera, worldx, worldy) 121 | checkType(worldx, "number", "worldx") 122 | checkType(worldy, "number", "worldy") 123 | camera = checkType(camera or EMPTY, "table", "camera") 124 | local camx, camy, zoom, angle, sx, sy, sw, sh = unpackCamera(camera) 125 | local sin, cos = math.sin(angle), math.cos(angle) 126 | local x, y = worldx - camx, worldy - camy 127 | x, y = cos * x + sin * y, -sin * x + cos * y 128 | return zoom * x + sw/2 + sx, zoom * y + sh/2 + sy 129 | end 130 | 131 | local function minorInterval(camera, visuals) 132 | camera = checkType(camera or EMPTY, "table", "camera") 133 | visuals = checkType(visuals or EMPTY, "table", "visuals") 134 | local zoom = select(3, unpackCamera(camera)) 135 | return getGridInterval(visuals, zoom) 136 | end 137 | 138 | local function majorInterval(camera, visuals) 139 | camera = checkType(camera or EMPTY, "table", "camera") 140 | visuals = checkType(visuals or EMPTY, "table", "visuals") 141 | local zoom = select(3, unpackCamera(camera)) 142 | local sds = select(2, unpackVisuals(visuals, zoom)) 143 | return sds * minorInterval(camera, visuals) 144 | end 145 | 146 | local function cellToWorld(camera, visuals, x, y) 147 | local d = minorInterval(camera, visuals) 148 | return math.floor(x) * d, math.floor(y) * d 149 | end 150 | 151 | local function cellToScreen(camera, visuals, x, y) 152 | return cellToWorld(camera, visuals, toWorld(camera, x, y)) 153 | end 154 | 155 | local function worldToCell(camera, visuals, x, y) 156 | local d = minorInterval(camera, visuals) 157 | return math.floor(x / d), math.floor(y / d) 158 | end 159 | 160 | local function screenToCell(camera, visuals, x, y) 161 | return worldToCell(camera, visuals, toWorld(camera, x, y)) 162 | end 163 | 164 | local function convertCoords(camera, visuals, src, dest, x, y) 165 | checkType(x, "number", "x") 166 | checkType(y, "number", "y") 167 | camera = checkType(camera or EMPTY, "table", "camera") 168 | visuals = checkType(visuals or EMPTY, "table", "visuals") 169 | assert( 170 | src == "screen" or src == "world" or src == "cell", 171 | "Unrecognized src " .. tostring(src) .. "." 172 | ) 173 | assert( 174 | dest == "screen" or dest == "world" or dest == "cell", 175 | "Unrecognized dest " .. tostring(dest) .. "." 176 | ) 177 | if src == dest then return x, y end 178 | if src == "screen" then 179 | if dest == "cell" then 180 | return screenToCell(camera, visuals, x, y) 181 | else -- dest == "world" 182 | return toWorld(camera, x, y) 183 | end 184 | elseif src == "cell" then 185 | if dest == "screen" then 186 | return cellToScreen(camera, visuals, x, y) 187 | else -- dest == "world" 188 | return cellToWorld(camera, visuals, x, y) 189 | end 190 | elseif src == "world" then 191 | if dest == "cell" then 192 | return worldToCell(camera, visuals, x, y) 193 | else -- dest == "screen" 194 | return toScreen(camera, x, y) 195 | end 196 | end 197 | end 198 | 199 | local function getCorners(camera) 200 | local sx, sy, sw, sh = select(5, unpackCamera(camera)) 201 | local x1, y1 = toWorld(camera, sx, sy) -- top left 202 | local x2, y2 = toWorld(camera, sx + sw, sy) -- top right 203 | local x3, y3 = toWorld(camera, sx + sw, sy + sh) -- bottom right 204 | local x4, y4 = toWorld(camera, sx, sy + sh) -- bottom left 205 | return x1, y1, x2, y2, x3, y3, x4, y4 206 | end 207 | 208 | local function intersect(x1, y1, x2, y2, x3, y3, x4, y4) 209 | local x21, x43 = x2 - x1, x4 - x3 210 | local y21, y43 = y2 - y1, y4 - y3 211 | local d = x21 * y43 - y21 * x43 212 | if d == 0 then return false end 213 | local xy34 = x3 * y4 - y3 * x4 214 | local xy12 = x1 * y2 - y1 * x2 215 | local a = xy34 * x21 - xy12 * x43 216 | local b = xy34 * y21 - xy12 * y43 217 | return a / d, b / d 218 | end 219 | 220 | local function drawLabel(camera, worldx, worly, label) 221 | lg.push() 222 | lg.origin() 223 | local x, y = toScreen(camera, worldx, worly) 224 | lg.printf(label, x + 2, y + 2, 400, "left") 225 | lg.pop() 226 | end 227 | 228 | local function push(camera) 229 | camera = checkType(camera or EMPTY, "table", "camera") 230 | local camx, camy, zoom, angle, sx, sy, sw, sh = unpackCamera(camera) 231 | lg.push() 232 | lg.scale(zoom) 233 | lg.translate((sw / 2 + sx) / zoom, (sh / 2 + sy) / zoom) 234 | lg.rotate(-angle) 235 | lg.translate(-camx, -camy) 236 | end 237 | 238 | local function draw(camera, visuals) 239 | camera = checkType(camera or EMPTY, "table", "camera") 240 | visuals = checkType(visuals or EMPTY, "table", "visuals") 241 | local camx, camy, zoom, angle, sx, sy, sw, sh = unpackCamera(camera) 242 | local size, sds, ds, color, xColor, yColor, ff, tf, hideOrigin = unpackVisuals(visuals, zoom) 243 | local x1, y1, x2, y2, x3, y3, x4, y4 = getCorners(camera) 244 | local swapXYLabels = mod(angle + math.pi/4, math.pi) > math.pi/2 245 | 246 | lg.setScissor(sx, sy, sw, sh) 247 | local vx, vy, vw, vh = visible(camera) 248 | local d = getGridInterval(visuals, zoom) 249 | local delta = d / 2 250 | 251 | push(camera) 252 | 253 | local oldLineWidth = lg.getLineWidth() 254 | lg.setLineWidth(1 / zoom) 255 | 256 | -- lines parallel to y axis 257 | local xc = sds 258 | for x = floor(vx, d * sds), vx + vw, d do 259 | if xc >= sds then 260 | lg.setColor(color[1], color[2], color[3], 1) 261 | xc = 1 262 | else 263 | lg.setColor(color[1] * ff, color[2] * ff, color[3] * ff, 1) 264 | xc = xc + 1 265 | end 266 | lg.line(x, vy, x, vy + vh) 267 | end 268 | 269 | -- lines parallel to x axis 270 | local yc = sds 271 | for y = floor(vy, d * sds), vy + vh, d do 272 | if yc >= sds then 273 | lg.setColor(color[1], color[2], color[3], 1) 274 | yc = 1 275 | else 276 | lg.setColor(color[1] * ff, color[2] * ff, color[3] * ff, 1) 277 | yc = yc + 1 278 | end 279 | if math.abs(y) > delta then 280 | lg.line(vx, y, vx + vw, y) 281 | end 282 | end 283 | 284 | -- draw labels 285 | for x = floor(vx, d * sds), vx + vw, d do 286 | if math.abs(x) < delta then 287 | lg.setColor(yColor[1] * tf, yColor[2] * tf, yColor[3] * tf, 1) 288 | lg.line(x, vy, x, vy + vh) 289 | else 290 | lg.setColor(color[1] * tf, color[2] * tf, color[3] * tf, 1) 291 | end 292 | if ds then 293 | local cx, cy 294 | if swapXYLabels then 295 | cx, cy = x4, y4 296 | else 297 | cx, cy = x2, y2 298 | end 299 | local ix, iy = intersect(x1, y1, cx, cy, x, vy, x, vy + vh) 300 | if ix then 301 | drawLabel(camera, ix, iy, "x=" .. x) 302 | end 303 | end 304 | end 305 | 306 | for y = floor(vy, d * sds), vy + vh, d do 307 | if math.abs(y) < delta then 308 | lg.setColor(xColor[1] * tf, xColor[2] * tf, xColor[3] * tf, 1) 309 | lg.line(vx, y, vx + vw, y) 310 | else 311 | lg.setColor(color[1] * tf, color[2] * tf, color[3] * tf, 1) 312 | end 313 | if ds then 314 | local cx, cy 315 | if swapXYLabels then 316 | cx, cy = x2, y2 317 | else 318 | cx, cy = x4, y4 319 | end 320 | local ix, iy = intersect(x1, y1, cx, cy, vx, y, vx + vw, y) 321 | if ix then 322 | drawLabel(camera, ix, iy, "y=" .. y) 323 | end 324 | end 325 | end 326 | 327 | lg.pop() 328 | lg.setLineWidth(1) 329 | 330 | -- draw origin 331 | if not hideOrigin then 332 | lg.setColor(1, 1, 1, 1) 333 | local ox, oy = toScreen(camera, 0, 0) 334 | lg.rectangle("fill", ox - 1, oy - 1, 2, 2) 335 | lg.circle("line", ox, oy, 8) 336 | end 337 | 338 | lg.setLineWidth(oldLineWidth) 339 | lg.setColor(1, 1, 1, 1) 340 | lg.setScissor() 341 | end 342 | 343 | local gridIndex = { 344 | toWorld = function (self, x, y) return toWorld(self.camera, x, y) end, 345 | toScreen = function (self, x, y) return toScreen(self.camera, x, y) end, 346 | convertCoords = function (self, src, dest, x, y) 347 | return convertCoords(self.camera, self.visuals, src, dest, x, y) 348 | end, 349 | draw = function (self) return draw(self.camera, self.visuals) end, 350 | minorInterval = function (self) 351 | return minorInterval(self.camera, self.visuals) 352 | end, 353 | majorInterval = function (self) 354 | return majorInterval(self.camera, self.visuals) 355 | end, 356 | visible = function (self) return visible(self.camera) end, 357 | push = function (self) return push(self.camera) end 358 | } 359 | 360 | local gridMt = { 361 | __index = gridIndex 362 | } 363 | 364 | local function grid(camera, visuals) 365 | camera = checkType(camera or {}, "table", "camera") 366 | visuals = checkType(visuals or {}, "table", "visuals") 367 | return setmetatable({ 368 | camera = camera, 369 | visuals = visuals 370 | }, gridMt) 371 | end 372 | 373 | return { 374 | toWorld = toWorld, 375 | toScreen = toScreen, 376 | convertCoords = convertCoords, 377 | draw = draw, 378 | visible = visible, 379 | minorInterval = minorInterval, 380 | majorInterval = majorInterval, 381 | grid = grid, 382 | push = push 383 | } 384 | -------------------------------------------------------------------------------- /examples/gamera/gamera.lua: -------------------------------------------------------------------------------- 1 | -- gamera.lua v1.0.1 2 | 3 | -- Copyright (c) 2012 Enrique García Cota 4 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 7 | -- Based on YaciCode, from Julien Patte and LuaObject, from Sebastien Rocca-Serra 8 | 9 | local gamera = {} 10 | 11 | -- Private attributes and methods 12 | 13 | local gameraMt = {__index = gamera} 14 | local abs, min, max = math.abs, math.min, math.max 15 | 16 | local function clamp(x, minX, maxX) 17 | return x < minX and minX or (x>maxX and maxX or x) 18 | end 19 | 20 | local function checkNumber(value, name) 21 | if type(value) ~= 'number' then 22 | error(name .. " must be a number (was: " .. tostring(value) .. ")") 23 | end 24 | end 25 | 26 | local function checkPositiveNumber(value, name) 27 | if type(value) ~= 'number' or value <=0 then 28 | error(name .. " must be a positive number (was: " .. tostring(value) ..")") 29 | end 30 | end 31 | 32 | local function checkAABB(l,t,w,h) 33 | checkNumber(l, "l") 34 | checkNumber(t, "t") 35 | checkPositiveNumber(w, "w") 36 | checkPositiveNumber(h, "h") 37 | end 38 | 39 | local function getVisibleArea(self, scale) 40 | scale = scale or self.scale 41 | local sin, cos = abs(self.sin), abs(self.cos) 42 | local w,h = self.w / scale, self.h / scale 43 | w,h = cos*w + sin*h, sin*w + cos*h 44 | return min(w,self.ww), min(h, self.wh) 45 | end 46 | 47 | local function cornerTransform(self, x,y) 48 | local scale, sin, cos = self.scale, self.sin, self.cos 49 | x,y = x - self.x, y - self.y 50 | x,y = -cos*x + sin*y, -sin*x - cos*y 51 | return self.x - (x/scale + self.l), self.y - (y/scale + self.t) 52 | end 53 | 54 | local function adjustPosition(self) 55 | local wl,wt,ww,wh = self.wl, self.wt, self.ww, self.wh 56 | local w,h = getVisibleArea(self) 57 | local w2,h2 = w*0.5, h*0.5 58 | 59 | local left, right = wl + w2, wl + ww - w2 60 | local top, bottom = wt + h2, wt + wh - h2 61 | 62 | self.x, self.y = clamp(self.x, left, right), clamp(self.y, top, bottom) 63 | end 64 | 65 | local function adjustScale(self) 66 | local w,h,ww,wh = self.w, self.h, self.ww, self.wh 67 | local rw,rh = getVisibleArea(self, 1) -- rotated frame: area around the window, rotated without scaling 68 | local sx,sy = rw/ww, rh/wh -- vert/horiz scale: minimun scales that the window needs to occupy the world 69 | local rscale = max(sx,sy) 70 | 71 | self.scale = max(self.scale, rscale) 72 | end 73 | 74 | -- Public interface 75 | 76 | function gamera.new(l,t,w,h) 77 | 78 | local sw,sh = love.graphics.getWidth(), love.graphics.getHeight() 79 | 80 | local cam = setmetatable({ 81 | x=0, y=0, 82 | scale=1, 83 | angle=0, sin=math.sin(0), cos=math.cos(0), 84 | l=0, t=0, w=sw, h=sh, w2=sw*0.5, h2=sh*0.5 85 | }, gameraMt) 86 | 87 | cam:setWorld(l,t,w,h) 88 | 89 | return cam 90 | end 91 | 92 | function gamera:setWorld(l,t,w,h) 93 | checkAABB(l,t,w,h) 94 | 95 | self.wl, self.wt, self.ww, self.wh = l,t,w,h 96 | 97 | adjustPosition(self) 98 | end 99 | 100 | function gamera:setWindow(l,t,w,h) 101 | checkAABB(l,t,w,h) 102 | 103 | self.l, self.t, self.w, self.h, self.w2, self.h2 = l,t,w,h, w*0.5, h*0.5 104 | 105 | adjustPosition(self) 106 | end 107 | 108 | function gamera:setPosition(x,y) 109 | checkNumber(x, "x") 110 | checkNumber(y, "y") 111 | 112 | self.x, self.y = x,y 113 | 114 | adjustPosition(self) 115 | end 116 | 117 | function gamera:setScale(scale) 118 | checkNumber(scale, "scale") 119 | 120 | self.scale = scale 121 | 122 | adjustScale(self) 123 | adjustPosition(self) 124 | end 125 | 126 | function gamera:setAngle(angle) 127 | checkNumber(angle, "angle") 128 | 129 | self.angle = angle 130 | self.cos, self.sin = math.cos(angle), math.sin(angle) 131 | 132 | adjustScale(self) 133 | adjustPosition(self) 134 | end 135 | 136 | function gamera:getWorld() 137 | return self.wl, self.wt, self.ww, self.wh 138 | end 139 | 140 | function gamera:getWindow() 141 | return self.l, self.t, self.w, self.h 142 | end 143 | 144 | function gamera:getPosition() 145 | return self.x, self.y 146 | end 147 | 148 | function gamera:getScale() 149 | return self.scale 150 | end 151 | 152 | function gamera:getAngle() 153 | return self.angle 154 | end 155 | 156 | function gamera:getVisible() 157 | local w,h = getVisibleArea(self) 158 | return self.x - w*0.5, self.y - h*0.5, w, h 159 | end 160 | 161 | function gamera:getVisibleCorners() 162 | local x,y,w2,h2 = self.x, self.y, self.w2, self.h2 163 | 164 | local x1,y1 = cornerTransform(self, x-w2,y-h2) 165 | local x2,y2 = cornerTransform(self, x+w2,y-h2) 166 | local x3,y3 = cornerTransform(self, x+w2,y+h2) 167 | local x4,y4 = cornerTransform(self, x-w2,y+h2) 168 | 169 | return x1,y1,x2,y2,x3,y3,x4,y4 170 | end 171 | 172 | function gamera:draw(f) 173 | love.graphics.setScissor(self:getWindow()) 174 | 175 | love.graphics.push() 176 | local scale = self.scale 177 | love.graphics.scale(scale) 178 | 179 | love.graphics.translate((self.w2 + self.l) / scale, (self.h2+self.t) / scale) 180 | love.graphics.rotate(-self.angle) 181 | love.graphics.translate(-self.x, -self.y) 182 | 183 | f(self:getVisible()) 184 | 185 | love.graphics.pop() 186 | 187 | love.graphics.setScissor() 188 | end 189 | 190 | function gamera:toWorld(x,y) 191 | local scale, sin, cos = self.scale, self.sin, self.cos 192 | x,y = (x - self.w2 - self.l) / scale, (y - self.h2 - self.t) / scale 193 | x,y = cos*x - sin*y, sin*x + cos*y 194 | return x + self.x, y + self.y 195 | end 196 | 197 | function gamera:toScreen(x,y) 198 | local scale, sin, cos = self.scale, self.sin, self.cos 199 | x,y = x - self.x, y - self.y 200 | x,y = cos*x + sin*y, -sin*x + cos*y 201 | return scale * x + self.w2 + self.l, scale * y + self.h2 + self.t 202 | end 203 | 204 | return gamera 205 | -------------------------------------------------------------------------------- /examples/gamera/main.lua: -------------------------------------------------------------------------------- 1 | -- fix path so all examples can use editgrid from parent directory. 2 | package.path = [[../../?.lua;]]..package.path 3 | 4 | local editgrid = require "editgrid" 5 | local gamera = require "gamera" 6 | 7 | -- mouse location 8 | local mx, my = 0, 0 9 | 10 | -- location of mouse on the grid 11 | local mWorldx, mWorldy = 0, 0 12 | 13 | -- location of grid origin on screen 14 | local oScreenx, oScreeny = 0, 0 15 | 16 | local cam = gamera.new(-1000, -1000, 2000, 2000) 17 | 18 | function love.draw() 19 | editgrid.draw(cam) 20 | local camx, camy = cam:getPosition() 21 | local scale = cam:getScale() 22 | local cx, cy = editgrid.convertCoords(cam, nil, "screen", "cell", mx, my) 23 | love.graphics.printf( 24 | "Camera position: (" .. 25 | camx .. ", " .. camy .. 26 | ")\nCamera zoom: " .. 27 | scale .. 28 | "\nMouse position on Grid: (" .. 29 | mWorldx .. ", " .. mWorldy .. 30 | ")\nCell coordinate under mouse: (" .. 31 | cx .. ", " .. cy .. 32 | ")\nGrid origin position on screen: (" .. 33 | oScreenx .. ", " .. oScreeny .. ")", 34 | 30, 30, 800, "left") 35 | end 36 | 37 | function love.update(dt) 38 | local newmx, newmy = love.mouse.getPosition() 39 | local camx, camy = cam:getPosition() 40 | local scale = cam:getScale() 41 | local angle = cam:getAngle() 42 | if love.mouse.isDown(1) then 43 | local s, c = cam.sin, cam.cos 44 | local dx = (-newmx + mx) / scale 45 | local dy = (-newmy + my) / scale 46 | cam:setPosition(camx + dx * c - dy * s, camy + dy * c + dx * s) 47 | end 48 | if love.keyboard.isDown("q") then 49 | cam:setAngle(cam:getAngle() + dt) 50 | end 51 | if love.keyboard.isDown("e") then 52 | cam:setAngle(cam:getAngle() - dt) 53 | end 54 | mx, my = newmx, newmy 55 | oScreenx, oScreeny = cam:toScreen(0, 0) 56 | mWorldx, mWorldy = cam:toWorld(mx, my) 57 | end 58 | 59 | function love.wheelmoved(x, y) 60 | local zoomfactor = nil 61 | if y > 0 then 62 | zoomfactor = 1.05 63 | elseif y < 0 then 64 | zoomfactor = 1 / 1.05 65 | end 66 | if zoomfactor then 67 | cam:setScale(cam:getScale() * zoomfactor) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /examples/hump/humpcamera.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2013 Matthias Richter 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | Except as contained in this notice, the name(s) of the above copyright holders 12 | shall not be used in advertising or otherwise to promote the sale, use or 13 | other dealings in this Software without prior written authorization. 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | ]]-- 22 | 23 | local _PATH = (...):match('^(.*[%./])[^%.%/]+$') or '' 24 | local cos, sin = math.cos, math.sin 25 | 26 | local camera = {} 27 | camera.__index = camera 28 | 29 | local function new(x,y, zoom, rot, smoother) 30 | x,y = x or love.graphics.getWidth()/2, y or love.graphics.getHeight()/2 31 | zoom = zoom or 1 32 | rot = rot or 0 33 | smoother = smoother or camera.smoothNone() -- for locking, see below 34 | return setmetatable({x = x, y = y, scale = zoom, rot = rot, smoother = smoother}, camera) 35 | end 36 | 37 | function camera:lookAt(x,y) 38 | self.x, self.y = x,y 39 | return self 40 | end 41 | 42 | function camera:move(x,y) 43 | self.x, self.y = self.x + x, self.y + y 44 | return self 45 | end 46 | 47 | function camera:pos() 48 | return self.x, self.y 49 | end 50 | 51 | function camera:rotate(phi) 52 | self.rot = self.rot + phi 53 | return self 54 | end 55 | 56 | function camera:rotateTo(phi) 57 | self.rot = phi 58 | return self 59 | end 60 | 61 | function camera:zoom(mul) 62 | self.scale = self.scale * mul 63 | return self 64 | end 65 | 66 | function camera:zoomTo(zoom) 67 | self.scale = zoom 68 | return self 69 | end 70 | 71 | function camera:attach() 72 | local cx,cy = love.graphics.getWidth()/(2*self.scale), love.graphics.getHeight()/(2*self.scale) 73 | love.graphics.push() 74 | love.graphics.scale(self.scale) 75 | love.graphics.translate(cx, cy) 76 | love.graphics.rotate(self.rot) 77 | love.graphics.translate(-self.x, -self.y) 78 | end 79 | 80 | function camera:detach() 81 | love.graphics.pop() 82 | end 83 | 84 | function camera:draw(func) 85 | self:attach() 86 | func() 87 | self:detach() 88 | end 89 | 90 | function camera:cameraCoords(x,y) 91 | -- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center 92 | local w,h = love.graphics.getWidth(), love.graphics.getHeight() 93 | local c,s = cos(self.rot), sin(self.rot) 94 | x,y = x - self.x, y - self.y 95 | x,y = c*x - s*y, s*x + c*y 96 | return x*self.scale + w/2, y*self.scale + h/2 97 | end 98 | 99 | function camera:worldCoords(x,y) 100 | -- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y) 101 | local w,h = love.graphics.getWidth(), love.graphics.getHeight() 102 | local c,s = cos(-self.rot), sin(-self.rot) 103 | x,y = (x - w/2) / self.scale, (y - h/2) / self.scale 104 | x,y = c*x - s*y, s*x + c*y 105 | return x+self.x, y+self.y 106 | end 107 | 108 | function camera:mousepos() 109 | return self:worldCoords(love.mouse.getPosition()) 110 | end 111 | 112 | -- camera scrolling utilities - adapted from http://gamasutra.com/blogs/ItayKeren/20150511/243083/Scroll_Back_The_Theory_and_Practice_of_Cameras_in_SideScrollers.php 113 | 114 | -- movement interpolators 115 | function camera.smoothNone() 116 | return function(dx,dy) return dx,dy end 117 | end 118 | 119 | function camera.smoothLinear(speed) 120 | assert(type(speed) == "number", "Invalid parameter: speed = "..tostring(speed)) 121 | return function(dx,dy, s) 122 | -- normalize direction 123 | local d = math.sqrt(dx*dx+dy*dy) 124 | dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal 125 | if d > 0 then 126 | dx,dy = dx/d, dy/d 127 | end 128 | 129 | return dx*dts, dy*dts 130 | end 131 | end 132 | 133 | function camera.smoothDamped(speed) 134 | assert(type(speed) == "number", "Invalid parameter: speed = "..tostring(speed)) 135 | return function(dx,dy, s) 136 | local dts = love.timer.getDelta() * (s or speed) 137 | return dx*dts, dy*dts 138 | end 139 | end 140 | 141 | -- position locking 142 | function camera:lockX(x, smoother, ...) 143 | local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...) 144 | self.x = self.x + dx 145 | end 146 | 147 | function camera:lockY(y, smoother, ...) 148 | local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...) 149 | self.y = self.y + dy 150 | end 151 | 152 | function camera:lockPos(x,y, smoother, ...) 153 | self:move((smoother or self.smoother)(x - self.x, y - self.y, ...)) 154 | end 155 | 156 | -- (possibly) move camera to keep x,y (in world coordinates) inside camera window (in camera coordinates) 157 | function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...) 158 | -- figure out displacement in camera coordinates 159 | x,y = self:cameraCoords(x,y) 160 | local dx, dy = 0,0 161 | if x < x_min then 162 | dx = x - x_min 163 | elseif x > x_max then 164 | dx = x - x_max 165 | end 166 | if y < y_min then 167 | dy = y - y_min 168 | elseif y > y_max then 169 | dy = y - y_max 170 | end 171 | 172 | -- transform displacement to movement in world coordinates 173 | local c,s = cos(-self.rot), sin(-self.rot) 174 | dx,dy = (c*dx - s*dy) * self.scale, (s*dx + c*dy) * self.scale 175 | 176 | -- move 177 | self:move((smoother or self.smoother)(dx,dy,...)) 178 | end 179 | 180 | -- the module 181 | return setmetatable({new = new, 182 | smoothNone = camera.smoothNone, 183 | smoothLinear = camera.smoothLinear, 184 | smoothDamped = camera.smoothDamped 185 | }, {__call = function(_, ...) return new(...) end}) 186 | -------------------------------------------------------------------------------- /examples/hump/main.lua: -------------------------------------------------------------------------------- 1 | -- fix path so all examples can use editgrid from parent directory. 2 | package.path = [[../../?.lua;]]..package.path 3 | 4 | local editgrid = require "editgrid" 5 | local camera = require "humpcamera" 6 | 7 | -- mouse location 8 | local mx, my = 0, 0 9 | 10 | -- location of mouse on the grid 11 | local mWorldx, mWorldy = 0, 0 12 | 13 | -- location of grid origin on screen 14 | local oScreenx, oScreeny = 0, 0 15 | 16 | local cam = camera.new(0, 0) 17 | 18 | function love.draw() 19 | editgrid.draw(cam) 20 | local camx, camy = cam:pos() 21 | local scale = cam.scale 22 | local cx, cy = editgrid.convertCoords(cam, nil, "screen", "cell", mx, my) 23 | love.graphics.printf( 24 | "Camera position: (" .. 25 | camx .. ", " .. camy .. 26 | ")\nCamera zoom: " .. 27 | scale .. 28 | "\nMouse position on Grid: (" .. 29 | mWorldx .. ", " .. mWorldy .. 30 | ")\nCell coordinate under mouse: (" .. 31 | cx .. ", " .. cy .. 32 | ")\nGrid origin position on screen: (" .. 33 | oScreenx .. ", " .. oScreeny .. ")", 34 | 30, 30, 800, "left") 35 | end 36 | 37 | function love.update(dt) 38 | local newmx, newmy = love.mouse.getPosition() 39 | local camx, camy = cam:pos() 40 | local scale = cam.scale 41 | local angle = cam.rot 42 | if love.mouse.isDown(1) then 43 | local s, c = math.sin(angle), math.cos(angle) 44 | local dx = (-newmx + mx) / scale 45 | local dy = (-newmy + my) / scale 46 | cam:lookAt(camx + dx * c - dy * s, camy + dy * c + dx * s) 47 | end 48 | if love.keyboard.isDown("q") then 49 | cam:rotateTo(cam.rot + dt) 50 | end 51 | if love.keyboard.isDown("e") then 52 | cam:rotateTo(cam.rot - dt) 53 | end 54 | mx, my = newmx, newmy 55 | oScreenx, oScreeny = cam:cameraCoords(0, 0) 56 | mWorldx, mWorldy = cam:worldCoords(mx, my) 57 | end 58 | 59 | function love.wheelmoved(x, y) 60 | local zoomfactor = nil 61 | if y < 0 then 62 | zoomfactor = 1.05 63 | elseif y > 0 then 64 | zoomfactor = 1 / 1.05 65 | end 66 | if zoomfactor then 67 | cam:zoomTo(cam.scale * zoomfactor) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /examples/simple/main.lua: -------------------------------------------------------------------------------- 1 | -- fix path so all examples can use editgrid from parent directory. 2 | package.path = [[../../?.lua;]]..package.path 3 | 4 | local editgrid = require "editgrid" 5 | 6 | -- mouse location 7 | local mx, my = 0, 0 8 | 9 | -- location of mouse on the grid 10 | local mWorldx, mWorldy = 0, 0 11 | 12 | -- location of grid origin on screen 13 | local oScreenx, oScreeny = 0, 0 14 | 15 | -- cam.x, cam.y is center of the camera 16 | local cam = { 17 | x = 0, 18 | y = 0, 19 | zoom = 1, 20 | angle = 0 21 | } 22 | 23 | local grid = editgrid.grid(cam) 24 | 25 | function love.draw() 26 | grid:draw() 27 | local cx, cy = grid:convertCoords("screen", "cell", mx, my) 28 | love.graphics.printf( 29 | "Camera position: (" .. 30 | cam.x .. ", " .. cam.y .. 31 | ")\nCamera zoom: " .. 32 | cam.zoom .. 33 | "\nMouse position on Grid: (" .. 34 | mWorldx .. ", " .. mWorldy .. 35 | ")\nCell coordinate under mouse: (" .. 36 | cx .. ", " .. cy .. 37 | ")\nGrid origin position on screen: (" .. 38 | oScreenx .. ", " .. oScreeny .. ")", 39 | 30, 30, 800, "left") 40 | end 41 | 42 | function love.update(dt) 43 | local newmx, newmy = love.mouse.getPosition() 44 | if love.mouse.isDown(1) then 45 | local s, c = math.sin(cam.angle), math.cos(cam.angle) 46 | local dx = (-newmx + mx) / cam.zoom 47 | local dy = (-newmy + my) / cam.zoom 48 | cam.x = cam.x + dx * c - dy * s 49 | cam.y = cam.y + dy * c + dx * s 50 | end 51 | if love.keyboard.isDown("q") then 52 | cam.angle = cam.angle + dt 53 | end 54 | if love.keyboard.isDown("e") then 55 | cam.angle = cam.angle - dt 56 | end 57 | mx, my = newmx, newmy 58 | oScreenx, oScreeny = grid:toScreen(0, 0) 59 | mWorldx, mWorldy = grid:toWorld(mx, my) 60 | end 61 | 62 | function love.wheelmoved(x, y) 63 | local zoomfactor = nil 64 | if y > 0 then 65 | zoomfactor = 1.05 66 | elseif y < 0 then 67 | zoomfactor = 1 / 1.05 68 | end 69 | if zoomfactor then 70 | cam.zoom = cam.zoom * zoomfactor 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/Editgrid/74af427cde22e8c68cecf33859a89b49ae3f7a07/preview.gif --------------------------------------------------------------------------------