TLfres

About

TLfres aims to make it easy to make any game run at any screen resolution. Using it doesn't even require you to rewrite your graphics code!

It's under the ZLIB license.

Setup

Everything in the module is contained in the TLfres namespace, so don't worry about it messing with your global variables. Assuming your game was built to use the Love-default resolution of 800×600:

  1. Put tlfres.lua in your game's folder
  2. At the top of main.lua, add the line local TLfres = require "tlfres"
  3. At the beginning of love.draw(), add the line TLfres.beginRendering(800, 600)
  4. At the bottom of love.draw() add the line TLfres.endRendering()
  5. Whenever you need to know the current scaling factor, call TLfres.getScale(800, 600)
  6. Whenever you would call love.mouse.getPosition, instead call TLfres.getMousePosition(800, 600)
  7. Whenever you would call love.graphics.setPointSize(x), instead call love.graphics.setPointSize(x * TLfres.getScale()) (Love doesn't scale point sizes automatically)

FAQ

Q) Why should I use this instead of love.graphics.scale?
A) Because even stretching the screen to a given res requires tricky math, which TLfres can do for you. Furthermore, most people hate stretched graphics - that's why you should use TLfres's letterboxing option. Instead of stretching the screen, it resizes it in the correct ratio and draws black boxes at the top and bottom if needed.
Q) What resolutions / aspect ratios does it support?
A) Any.
Q) I made my game to use a non-default resolution. Will I have to change everything to make TLfres work with it?
A) Nope! Just call TLfres.beginRendering() with the resolution you built your game for (see below).

Functions

TLfres.beginRendering

TLfres.beginRendering(width, height, centered)

Call this at the very beginning of love.draw(), before drawing anything. This scales and translates the screen coordinates as needed by the current screen resolution. If you want to draw something at absolute screen coordinates, do it before this function (or after endRendering()).

number width
Width of the target canvas.
number height
Width of the target canvas.
boolean centered (false)
If true, (0, 0) is the middle of the canvas, otherwise it is the top-left corner.

TLfres.endRendering

TLfres.endRendering(letterboxColor)

Call this at the end of love.draw(), after drawing everything. This draws letterboxes to trim any graphics which should be out of the screen. It defaults to black letterboxes.

table letterboxColor ({0,0,0, 255})
The color of the letterboxes, given in RGBA (just like love.graphics.setColor). Defaults to black.

TLfres.getScale

TLfres.getScale(width, height)
number width
The canvas width expected by your code.
number height
The canvas height expected by your code.

TLfres.getMousePosition

TLfres.getMousePosition(width, height)

Use this any time you would normally call love.mouse.getPosition. The returned position is scaled to the given dimensions.

number width
The canvas width expected by your input code.
number height
The canvas height expected by your input code.

tlfres.lua

local lwGetMode     = _G.love.window.getMode
local lgPush        = _G.love.graphics.push
local lgPop         = _G.love.graphics.pop
local lgTranslate   = _G.love.graphics.translate
local lgScale       = _G.love.graphics.scale
local lgRectangle   = _G.love.graphics.rectangle
local lgSetColor    = _G.love.graphics.setColor
local lmGetPosition = _G.love.mouse.getPosition
local min = math.min

local TLfres = {}

local lastMouseX, lastMouseY = 0, 0
local currentlyRendering

-- Internal helper function
local function _getRawMousePosition(width, height)
   local x, y = lmGetPosition()
   local w, h = lwGetMode()
   local scale = min(w/width, h/height)
   return (x - (w - width * scale) * 0.5)/scale, (y - (h - height * scale) * 0.5)/scale
end

-- Use this any time you would normally call love.mouse.getPosition.
-- The returned position is scaled to the given dimensions.
function TLfres.getMousePosition(width, height)
   local x, y = _getRawMousePosition(width, height)
   if x >= 0 and x <= width and y >= 0 and y <= height then
      lastMouseX, lastMouseY = _getRawMousePosition(width, height)
   end
   return lastMouseX, lastMouseY
end

-- Calculate the current scale based on the desired dimensions and current ones
-- If called within a rendering block, width and height are optional.
function TLfres.getScale(width, height)
   if currentlyRendering then
      width  = width  or currentlyRendering[1]
      height = height or currentlyRendering[2]
   end
   local w, h = lwGetMode()
   return min(w/width, h/height)
end

-- Zooms and centers to fit width×height into the current window.
-- 0,0 is at the top-left of the canvas, or the middle if centered is true.
-- Use love.graphics.push before this and love.graphics.pop after done rendering
function TLfres.beginRendering(width, height, centered)
   if currentlyRendering then
      error("Must call tlfres.endRendering before calling beginRendering.")
      return
   end
   currentlyRendering = {width, height}
   lgPush()

   local w, h = lwGetMode()
   local scale = min(w/width, h/height)
   lgTranslate((w - width * scale) * 0.5, (h - height * scale) * 0.5)
   lgScale(scale)
   if centered then
      lgTranslate(0.5 * width, 0.5 * height)
   end
   return scale
end

local _black = {0, 0, 0, 255}

-- Pops out of the transform; if letterboxColor is true, draws black letterbox
-- bars. letterboxColor can also be any {r, g, b, a} table.
function TLfres.endRendering(letterboxColor)
   if not currentlyRendering then
      error("Must call tlfres.beginRendering before calling endRendering.")
      return
   end
   local width, height = currentlyRendering[1], currentlyRendering[2]
   currentlyRendering = nil
   lgPop()

   local w, h = lwGetMode()
   local scale = min(w/width, h/height)
   width, height = width * scale, height * scale

   lgSetColor(letterboxColor or _black)
   lgRectangle("fill", 0, 0,  w,  0.5 * (h - height)) -- top
   lgRectangle("fill", 0, h,  w, -0.5 * (h - height)) -- bottom
   lgRectangle("fill", 0, 0,  0.5 * (w - width), h)   -- left
   lgRectangle("fill", w, 0, -0.5 * (w - width), h)   -- right
end

return TLfres

Example

local TLfres = require "tlfres"

local CANVAS_WIDTH = 800
local CANVAS_HEIGHT = 600
local POINT_SIZE = 1

function love.mouse.getPosition() -- Override the standard function with our helper function
   return TLfres.getMousePosition(CANVAS_WIDTH, CANVAS_HEIGHT)
end

function love.draw()
   tlfres.beginRendering(CANVAS_WIDTH, CANVAS_HEIGHT)

      love.graphics.setPointSize(tlfres.getScale()*POINT_SIZE) -- Point size doesn't scale automatically, so multiply it manually.
      love.graphics.points(400, 300) -- Will draw at the center of the canvas no matter how the screen is resized.

   tlfres.endRendering() -- Draw black letterbox
end