diff --git a/client/src/graphics/graphics_util.lua b/client/src/graphics/graphics_util.lua index d66aa93b..6bdcb4a7 100644 --- a/client/src/graphics/graphics_util.lua +++ b/client/src/graphics/graphics_util.lua @@ -1,6 +1,7 @@ local consts = require("common.engine.consts") local logger = require("common.lib.logger") local FileUtils = require("client.src.FileUtils") +local system = require("client.src.system") -- Utility methods for drawing local GraphicsUtil = { @@ -379,9 +380,50 @@ function GraphicsUtil.resetAlignment() love.graphics.pop() end -local loveMajor = love.getVersion() +--- A wrapper function to create a love.Texture that persists in memory for drawing +---@param width number Canvas width +---@param height number Canvas height +---@param drawFunc function Function to call inside renderTo +---@param dpiscale number DPI scale for the canvas +---@param filterMin string filter mode +---@param filterMag string filter mode +---@return love.graphics.Texture +function GraphicsUtil.renderToTexture(width, height, drawFunc, dpiscale, filterMin, filterMag) + love.graphics.push("all") + love.graphics.reset() -if loveMajor >= 12 then + local canvas = love.graphics.newCanvas(width, height, {dpiscale = dpiscale}) + + canvas:setFilter(filterMin, filterMag) + + canvas:renderTo(drawFunc) + + -- Restore graphics state + love.graphics.pop() + + if not system.meetsLoveVersionRequirement(12, 0) then + -- in love 11.5 Image and Canvas are distinct types inheriting from the Texture type that behave differently in a number of things + -- a main difference is that love.window.updateMode and love.window.setMode cause all Canvas objects to be cleared so that they need to be redrawn + -- to avoid this, immediately use the canvas's ImageData to create an Image type object that does not get cleared by window updates + local imageData + imageData = canvas:newImageData() + + local image = love.graphics.newImage(imageData, {dpiscale = dpiscale}) + + -- Preserve filter settings on the image + if filterMin and filterMag then + image:setFilter(filterMin, filterMag) + end + + return image + else + -- in love 12.0 the Canvas type does no longer exist, everything declared with newCanvas is a Texture and window updates don't clear these + -- that means we can use the canvas directly + return canvas + end +end + +if system.meetsLoveVersionRequirement(12, 0) then GraphicsUtil.newText = love.graphics.newTextBatch else GraphicsUtil.newText = love.graphics.newText diff --git a/client/src/mods/Character.lua b/client/src/mods/Character.lua index e452e02a..5d89e51d 100644 --- a/client/src/mods/Character.lua +++ b/client/src/mods/Character.lua @@ -392,26 +392,35 @@ end -- bundles without stage icon display up to 4 icons of their substages function Character:createBundleIcon() - local canvas = love.graphics.newCanvas(2 * 168, 2 * 168) - canvas:renderTo(function() - for i, subCharacterId in ipairs(self.subIds) do - -- only draw up to 4 and only draw sub mods that are actually there unless there are none - if i <= 4 and (characters[subCharacterId] or (allCharacters[subCharacterId] and #self:getSubMods() == 0)) then - local character = allCharacters[subCharacterId] - local x = 0 - local y = 0 - if i % 2 == 0 then - x = 168 + local firstCharacter = allCharacters[self.subIds[1]] + assert(firstCharacter ~= nil, "Expected a valid character in sub IDs") + local filterMin, filterMag = firstCharacter.images.icon:getFilter() + local image = GraphicsUtil.renderToTexture( + 2 * 168, + 2 * 168, + function() + for i, subCharacterId in ipairs(self.subIds) do + -- only draw up to 4 and only draw sub mods that are actually there unless there are none + if i <= 4 and (characters[subCharacterId] or (allCharacters[subCharacterId] and #self:getSubMods() == 0)) then + local character = allCharacters[subCharacterId] + local x = 0 + local y = 0 + if i % 2 == 0 then + x = 168 + end + if i > 2 then + y = 168 + end + local width, height = character.images.icon:getDimensions() + love.graphics.draw(character.images.icon, x, y, 0, 168 / width, 168 / height) end - if i > 2 then - y = 168 - end - local width, height = character.images.icon:getDimensions() - love.graphics.draw(character.images.icon, x, y, 0, 168 / width, 168 / height) end - end - end) - return canvas + end, + GAME:newCanvasSnappedScale(), + filterMin, + filterMag + ) + return image end function Character.graphics_uninit(self) @@ -581,17 +590,23 @@ function Character:createGarbageTexture(width, height) local relativeScale = self.images.pop:getWidth() / 16 -- create all canvases as if we were working with the 360x240 resolution but use the canvas dpi scale to use the real resolution -- that makes it easy to scale later as everything can be treated the same while love handles the dpi scale resolution for us - local canvas = love.graphics.newCanvas(width * 16, height * 16, {dpiscale = self.images.pop:getDPIScale() * relativeScale}) + local dpiscale = self.images.pop:getDPIScale() * relativeScale -- Use the same filter as the garbage images so that upscaling looks right for pixel art - local min, mag = self.images.pop:getFilter() - canvas:setFilter(min, mag) + local filterMin, filterMag = self.images.pop:getFilter() - canvas:renderTo(function() - self:__drawGarbage(width, height) - end) + local image = GraphicsUtil.renderToTexture( + width * 16, + height * 16, + function() + self:__drawGarbage(width, height) + end, + dpiscale, + filterMin, + filterMag + ) - return canvas + return image end --- returns an existing prerender or if there is none, creates one and caches it for reuse @@ -604,21 +619,7 @@ function Character:getGarbageTexture(width, height) end if not self.garbagePrerenders[width][height] then - -- canvases are affected by scissors and transformations so we need to make sure to suspend them - local sx, sy, w, h = love.graphics.getScissor() - if sx then - love.graphics.setScissor() - end - love.graphics.push("transform") - love.graphics.origin() - self.garbagePrerenders[width][height] = self:createGarbageTexture(width, height) - - -- and then reapply them - love.graphics.pop() - if sx then - love.graphics.setScissor(sx, sy, w, h) - end end return self.garbagePrerenders[width][height] diff --git a/client/src/mods/Panels.lua b/client/src/mods/Panels.lua index 33d8c67b..f318394a 100644 --- a/client/src/mods/Panels.lua +++ b/client/src/mods/Panels.lua @@ -190,31 +190,36 @@ function Panels:loadSheets() self.size = self.sheets[1]:getHeight() / maxRowUsed end --- +-- function Panels:convertSinglesToSheetTexture(images, animationStates) - local canvas = love.graphics.newCanvas(self.size * 10, self.size * #animationStates, {dpiscale = images[1]:getDPIScale()}) - if self.size <= 24 then - -- none of the panels is bigger than 24x24 so we can assume pixel art style panels - canvas:setFilter("nearest", "nearest") - end - canvas:renderTo(function() - local row = 1 - -- ipairs over a static table so the ordering is definitely consistent - for _, animationState in ipairs(animationStates) do - local animationConfig = self.animationConfig[animationState] - for frameNumber, imageIndex in ipairs(animationConfig.frames) do - local widthScale = self.size / images[imageIndex]:getWidth() - local heightScale = self.size / images[imageIndex]:getHeight() - if heightScale > 1 or widthScale > 1 then - images[imageIndex]:setFilter("nearest", "nearest") + local dpiscale = images[1]:getDPIScale() + local filterMin, filterMag = images[1]:getFilter() + + local image = GraphicsUtil.renderToTexture( + self.size * 10, + self.size * #animationStates, + function() + local row = 1 + -- ipairs over a static table so the ordering is definitely consistent + for _, animationState in ipairs(animationStates) do + local animationConfig = self.animationConfig[animationState] + for frameNumber, imageIndex in ipairs(animationConfig.frames) do + local widthScale = self.size / images[imageIndex]:getWidth() + local heightScale = self.size / images[imageIndex]:getHeight() + if heightScale > 1 or widthScale > 1 then + images[imageIndex]:setFilter("nearest", "nearest") + end + love.graphics.draw(images[imageIndex], self.size * (frameNumber - 1), self.size * (row - 1), nil, widthScale, heightScale) end - love.graphics.draw(images[imageIndex], self.size * (frameNumber - 1), self.size * (row - 1),nil, widthScale, heightScale) + row = row + 1 end - row = row + 1 - end - end) + end, + dpiscale, + filterMin, + filterMag + ) - return canvas + return image end local function validateSingleFilesAgainstConfig(imagesByColorAndIndex, animationConfig) @@ -344,12 +349,21 @@ function Panels:load() self.quad = love.graphics.newQuad(0, 0, self.size, self.size, self.sheets[1]:getDimensions()) self.displayIcons = {} + + local dpiscale = self.sheets[1]:getDPIScale() + local filterMin, filterMag = self.sheets[1]:getFilter() + for color = 1, 8 do - local canvas = love.graphics.newCanvas(self.size, self.size) - canvas:renderTo(function() - self:drawPanelFrame(color, "normal", 0, 0) - end) - self.displayIcons[color] = canvas + self.displayIcons[color] = GraphicsUtil.renderToTexture( + self.size, + self.size, + function() + self:drawPanelFrame(color, "normal", 0, 0) + end, + dpiscale, + filterMin, + filterMag + ) --fileUtils.saveTextureToFile(self.sheets[color], self.path .. "/panel-" .. color, "png") self.batches[color] = love.graphics.newSpriteBatch(self.sheets[color], 100, "stream") end diff --git a/client/src/mods/Stage.lua b/client/src/mods/Stage.lua index 601ddf4e..ccc090fd 100644 --- a/client/src/mods/Stage.lua +++ b/client/src/mods/Stage.lua @@ -183,25 +183,34 @@ end -- bundles without stage thumbnail display up to 4 thumbnails of their substages function Stage:createBundleThumbnail() - local canvas = love.graphics.newCanvas(2 * 80, 2 * 45) - canvas:renderTo(function() - for i, substageId in ipairs(self.subIds) do - if i <= 4 and (stages[substageId] or (allStages[substageId] and #self:getSubMods() == 0)) then - local stage = allStages[substageId] - local x = 0 - local y = 0 - if i % 2 == 0 then - x = 80 + local firstStage = allStages[self.subIds[1]] + assert(firstStage ~= nil, "Expected a valid character in sub IDs") + local filterMin, filterMag = firstStage.images.thumbnail:getFilter() + local image = GraphicsUtil.renderToTexture( + 2 * 80, + 2 * 45, + function() + for i, substageId in ipairs(self.subIds) do + if i <= 4 and (stages[substageId] or (allStages[substageId] and #self:getSubMods() == 0)) then + local stage = allStages[substageId] + local x = 0 + local y = 0 + if i % 2 == 0 then + x = 80 + end + if i > 2 then + y = 45 + end + local width, height = stage.images.thumbnail:getDimensions() + love.graphics.draw(stage.images.thumbnail, x, y, 0, 80 / width, 45 / height) end - if i > 2 then - y = 45 - end - local width, height = stage.images.thumbnail:getDimensions() - love.graphics.draw(stage.images.thumbnail, x, y, 0, 80 / width, 45 / height) end - end - end) - return canvas + end, + GAME:newCanvasSnappedScale(), + filterMin, + filterMag + ) + return image end -- uninits stage graphics diff --git a/client/src/scenes/CharacterSelect.lua b/client/src/scenes/CharacterSelect.lua index 8ceac080..7152f10e 100644 --- a/client/src/scenes/CharacterSelect.lua +++ b/client/src/scenes/CharacterSelect.lua @@ -338,15 +338,22 @@ function CharacterSelect:getCharacterButtons() end if character.panels and panels[character.panels] then - -- draw the color 1 normal panel in the left corner - -- it's only available on the sheet so we got to render it to its own canvas first local panels = panels[character.panels] - local canvas = love.graphics.newCanvas(panels.size, panels.size) - canvas:renderTo(function() - panels:drawPanelFrame(1, "normal", 0, 0, panels.size) - end) - - characterButton.panelIcon = ui.ImageContainer({image = canvas, vAlign = "bottom", hAlign = "left", x = 2, y = -2, width = 16, height = 16}) + local dpiscale = panels.sheets[1]:getDPIScale() + local filterMin, filterMag = panels.sheets[1]:getFilter() + + local panelImage = GraphicsUtil.renderToTexture( + panels.size, + panels.size, + function() + panels:drawPanelFrame(1, "normal", 0, 0, panels.size) + end, + dpiscale, + filterMin, + filterMag + ) + + characterButton.panelIcon = ui.ImageContainer({image = panelImage, vAlign = "bottom", hAlign = "left", x = 2, y = -2, width = 16, height = 16}) characterButton:addChild(characterButton.panelIcon) end diff --git a/client/src/scenes/PuzzleEditorScene.lua b/client/src/scenes/PuzzleEditorScene.lua index 34fa939c..cb777f24 100644 --- a/client/src/scenes/PuzzleEditorScene.lua +++ b/client/src/scenes/PuzzleEditorScene.lua @@ -1,15 +1,15 @@ -local GameBase = require("client.src.scenes.GameBase") -local TouchInputDetector = require("client.src.TouchInputDetector") local Panel = require("common.engine.Panel") local Puzzle = require("common.engine.Puzzle") -local PuzzleEditorStackOverlay = require("client.src.ui.PuzzleEditorStackOverlay") local class = require("common.lib.class") local logger = require("common.lib.logger") local consts = require("common.engine.consts") local ui = require("client.src.ui") +local TouchInputDetector = require("client.src.TouchInputDetector") +local GameBase = require("client.src.scenes.GameBase") +local GraphicsUtil = require("client.src.graphics.graphics_util") local focusable = require("client.src.ui.Focusable") local directsFocus = require("client.src.ui.FocusDirector") - +local PuzzleEditorStackOverlay = require("client.src.ui.PuzzleEditorStackOverlay") ---@class PuzzleEditorScene : GameBase ---@field puzzleSet PuzzleSet @@ -393,40 +393,40 @@ end function PuzzleEditorScene:createShockGarbageButton(size) local stack = self.match.stacks[1] local panelsData = panels[stack.panels_dir] - - -- Create a canvas to draw the shock garbage with end caps like it appears in game - local canvas = love.graphics.newCanvas(size, size) - local prevCanvas = love.graphics.getCanvas() - - canvas:renderTo(function() - local shockImages = panelsData.images.metals - local leftImage = shockImages.left - local midImage = shockImages.mid - local rightImage = shockImages.right - - -- Calculate scaling to fit the button size - local targetWidth = size * 0.8 -- Leave some padding - local targetHeight = size * 0.6 - - -- Draw left cap - local leftWidth = targetWidth * 0.25 - love.graphics.draw(leftImage, size * 0.1, size * 0.2, 0, leftWidth / leftImage:getWidth(), targetHeight / leftImage:getHeight()) - - -- Draw middle section - local midWidth = targetWidth * 0.5 - local midX = size * 0.1 + leftWidth - love.graphics.draw(midImage, midX, size * 0.2, 0, midWidth / midImage:getWidth(), targetHeight / midImage:getHeight()) - - -- Draw right cap - local rightWidth = targetWidth * 0.25 - local rightX = midX + midWidth - love.graphics.draw(rightImage, rightX, size * 0.2, 0, rightWidth / rightImage:getWidth(), targetHeight / rightImage:getHeight()) - end) - - love.graphics.setCanvas(prevCanvas) + local shockImages = panelsData.images.metals + + local dpiscale = shockImages.mid:getDPIScale() + local filterMin, filterMag = shockImages.mid:getFilter() + + local garbageImage = GraphicsUtil.renderToTexture( + size, + size, + function() + local leftImage = shockImages.left + local midImage = shockImages.mid + local rightImage = shockImages.right + + local targetWidth = size * 0.8 + local targetHeight = size * 0.6 + + local leftWidth = targetWidth * 0.25 + love.graphics.draw(leftImage, size * 0.1, size * 0.2, 0, leftWidth / leftImage:getWidth(), targetHeight / leftImage:getHeight()) + + local midWidth = targetWidth * 0.5 + local midX = size * 0.1 + leftWidth + love.graphics.draw(midImage, midX, size * 0.2, 0, midWidth / midImage:getWidth(), targetHeight / midImage:getHeight()) + + local rightWidth = targetWidth * 0.25 + local rightX = midX + midWidth + love.graphics.draw(rightImage, rightX, size * 0.2, 0, rightWidth / rightImage:getWidth(), targetHeight / rightImage:getHeight()) + end, + dpiscale, + filterMin, + filterMag + ) return ui.ImageButton({ - image = canvas, + image = garbageImage, width = size, height = size, onClick = function() diff --git a/client/tests/PlayerSettingsTests.lua b/client/tests/PlayerSettingsTests.lua index 680b0dfa..fc249d3d 100644 --- a/client/tests/PlayerSettingsTests.lua +++ b/client/tests/PlayerSettingsTests.lua @@ -71,8 +71,6 @@ local function testVsSelfChangesEndlessClassicSettingsToModern() endlessPlayer:setLevelData(LevelPresets.getClassicEndless(1)) -- Verify endless settings - assert(endlessPlayer.settings.style == GameModes.Styles.CLASSIC, - "Player should have classic style before vs self") assert(endlessPlayer.settings.levelData.colors == 5, "Player should have 5 colors before vs self") assert(endlessPlayer.settings.levelData.adjacentDenialFrequency == 0,