From f8da1a788c68ff58d53a173ad197ef3623dc6cff Mon Sep 17 00:00:00 2001 From: cursed22bc Date: Fri, 27 Feb 2026 13:37:36 +0200 Subject: base setup --- .vscode/settings.json | 5 ++ camera.lua | 118 +++++++++++++++++++++++++++++++++++++++++++++ fonts.lua | 27 +++++++++++ main.lua | 90 ++++++++++++++++++++++++++++++++++ shaders/smooth_camera.glsl | 5 ++ 5 files changed, 245 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 camera.lua create mode 100644 fonts.lua create mode 100644 main.lua create mode 100644 shaders/smooth_camera.glsl diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a59b386 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "Lua.diagnostics.globals": [ + "love" + ] +} \ No newline at end of file diff --git a/camera.lua b/camera.lua new file mode 100644 index 0000000..752b32e --- /dev/null +++ b/camera.lua @@ -0,0 +1,118 @@ +local Camera = {} +Camera.__index = Camera + +function Camera:new(target, width, height, mouseFollow, screenToWorldScale) + local self = setmetatable({}, Camera) + self.target = target + self.width = width + self.height = height + self.mouseFollow = mouseFollow or false + self.mouseRadius = math.min(self.width, self.height) * 0.35 + self.mouseWeight = 0.1 + self.screenToWorldScale = screenToWorldScale + + self.x = target.x + (target.width) / 2 - self.width / 2 + self.y = target.y + (target.height) / 2 - self.height / 2 + + self.vx = 0 + self.vy = 0 + + self.stiffness = 50 + self.damping = 2 * math.sqrt(self.stiffness) + + self.limits = { + maxX = nil, + minX = nil, + maxY = nil, + minY = nil, + } + + return self +end + +function Camera:setTarget(target) + self.target = target +end + +function Camera:setLimits(minX, maxX, minY, maxY) + self.limits.minX = minX + self.limits.maxX = maxX + self.limits.minY = minY + self.limits.maxY = maxY +end + +function Camera:update(dt) + if not self.target then return end + local cx = self.target.x + (self.target.width or 0) / 2 + local cy = self.target.y + (self.target.height or 0) / 2 + local targetX = cx - self.width / 2 + local targetY = cy - self.height / 2 + + if self.mouseFollow then + local sw = love.graphics.getWidth() + local sh = love.graphics.getHeight() + local mx, my = love.mouse.getPosition() + + local offsetX = (mx - sw / 2) * self.screenToWorldScale + local offsetY = (my - sh / 2) * self.screenToWorldScale + + local dist = offsetX * offsetX + offsetY * offsetY + if dist > self.mouseRadius * self.mouseRadius then + local inv = self.mouseRadius / math.sqrt(dist) + offsetX = offsetX * inv + offsetY = offsetY * inv + end + + targetX = targetX + offsetX * self.mouseWeight + targetY = targetY + offsetY * self.mouseWeight + end + + local accelerationX = (targetX - self.x) * self.stiffness - self.vx * self.damping + local accelerationY = (targetY - self.y) * self.stiffness - self.vy * self.damping + + self.vx = self.vx + accelerationX * dt + self.vy = self.vy + accelerationY * dt + + self.x = self.x + self.vx * dt + self.y = self.y + self.vy * dt + + local lim = self.limits + if lim.minX ~= nil then self.x = math.max(lim.minX, self.x) end + if lim.maxX ~= nil then self.x = math.min(self.x, lim.maxX - self.width) end + if lim.minY ~= nil then self.y = math.max(lim.minY, self.y) end + if lim.maxY ~= nil then self.y = math.min(self.y, lim.maxY - self.height) end +end + +function Camera:set(padding) + padding = padding or 0 + love.graphics.push() + + local canvasW = love.graphics.getCanvas():getWidth() + local canvasH = love.graphics.getCanvas():getHeight() + + local viewW = self.width + padding * 2 + local viewH = self.height + padding * 2 + + local scaleX = canvasW / viewW + local scaleY = canvasH / viewH + + local scale = math.floor(math.min(scaleX, scaleY)) + + love.graphics.scale(scale) + + local snapX = math.floor(self.x) + local snapY = math.floor(self.y) + love.graphics.translate(-(snapX - padding), -(snapY - padding)) +end + +function Camera:unset() + love.graphics.pop() +end + +function Camera:getSubPixelOffset() + local dx = self.x - math.floor(self.x) + local dy = self.y - math.floor(self.y) + return dx, dy +end + +return Camera \ No newline at end of file diff --git a/fonts.lua b/fonts.lua new file mode 100644 index 0000000..905b2a3 --- /dev/null +++ b/fonts.lua @@ -0,0 +1,27 @@ +---@diagnostic disable: undefined-global +-- Font loader for pixel-perfect 64x64 virtual screen +local M = {} + +local FONT_PATH = "assets/font/font.otf" +local DEFAULT_SIZE = 16 + +local function setPixelFilter(font) + font:setFilter("nearest", "nearest") + return font +end + +function M.load() + M.default = setPixelFilter(love.graphics.newFont(FONT_PATH, DEFAULT_SIZE)) + return M.default +end + +function M.get(size) + if not size then return M.default end + M.sizes = M.sizes or {} + if not M.sizes[size] then + M.sizes[size] = setPixelFilter(love.graphics.newFont(FONT_PATH, size)) + end + return M.sizes[size] +end + +return M diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..66ef333 --- /dev/null +++ b/main.lua @@ -0,0 +1,90 @@ +local VIRTUAL_WIDTH, VIRTUAL_HEIGHT = 16*10*3, 9*10*3 +local CANVAS_PADDING = 6 +local CANVAS_WIDTH = VIRTUAL_WIDTH + CANVAS_PADDING +local CANVAS_HEIGHT = VIRTUAL_HEIGHT + CANVAS_PADDING +local WORLD_TO_CANVAS = 3 + +local scale = 1 +local finalScale = 1 +local offsetX, offsetY = 0, 0 +local dpiScale = 1 +local canvas = nil +local smoothCameraShader = nil + +--TODO: separate to components +local cameraModule = require("camera") +local camera = nil +-- end + +local fonts = require("fonts") + + +local function recalcScale(w, h) + dpiScale = (love.window.getDPIScale and love.window.getDPIScale()) or 1 + scale = math.max(1, math.floor(math.min(w / VIRTUAL_WIDTH, h / VIRTUAL_HEIGHT) / dpiScale)) + finalScale = scale * dpiScale + offsetX = math.floor((w - VIRTUAL_WIDTH * finalScale) / 2) + offsetY = math.floor((h - VIRTUAL_HEIGHT * finalScale) / 2) +end + +function love.load() + camera = cameraModule:new({x = 0, y = 0, width = VIRTUAL_WIDTH, height = VIRTUAL_HEIGHT}, VIRTUAL_WIDTH, VIRTUAL_HEIGHT, true, WORLD_TO_CANVAS) + love.graphics.setDefaultFilter("nearest", "nearest") + love.window.setTitle("Openformer") + fonts.load() + love.graphics.setFont(fonts.default) + + canvas = love.graphics.newCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) + canvas:setFilter("nearest", "nearest") + + local ok, shader = pcall(love.graphics.newShader, "shaders/smooth_camera.glsl") + smoothCameraShader = ok and shader or nil + if not smoothCameraShader then + print("Warning: smooth_camera.glsl not loaded, using fallback (no sub-pixel offset)") + end + + local w, h = love.graphics.getWidth(), love.graphics.getHeight() + recalcScale(w, h) + +end + +function love.resize(w, h) + recalcScale(w, h) + if canvas then canvas:release() end + canvas = love.graphics.newCanvas(CANVAS_WIDTH, CANVAS_HEIGHT) + canvas:setFilter("nearest", "nearest") +end + + +function love.update(dt) + if not camera then return end + camera:update(dt) +end + +function love.keypressed(key, scancode, isrepeat) + if key == "f11" then + love.window.setFullscreen(not love.window.getFullscreen(), "desktop") + end +end + +function love.draw() + love.graphics.setCanvas(canvas) + love.graphics.clear() + love.graphics.push() + if camera then + camera:set() + love.graphics.print("FPS: " .. love.timer.getFPS(), 10, 10) + love.graphics.pop() + + camera:unset() + end + + love.graphics.setCanvas() + + love.graphics.clear() + local drawX = offsetX - (CANVAS_PADDING * finalScale) / 2 + local drawY = offsetY - (CANVAS_PADDING * finalScale) / 2 + love.graphics.draw(canvas, math.floor(drawX), math.floor(drawY), 0, finalScale, finalScale) +end + +return nil \ No newline at end of file diff --git a/shaders/smooth_camera.glsl b/shaders/smooth_camera.glsl new file mode 100644 index 0000000..7c8e822 --- /dev/null +++ b/shaders/smooth_camera.glsl @@ -0,0 +1,5 @@ +extern vec2 offset; + +vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { + return Texel(tex, texture_coords + offset); +} -- cgit v1.2.3