From 411c435e7add4409b56f94f1e36e528dcb3b0446 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Fri, 14 Nov 2025 01:18:34 +0300 Subject: [PATCH 1/7] straightforward hp/mana bars implementation --- lib/simple_ui/level/layout.lua | 94 ++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/lib/simple_ui/level/layout.lua b/lib/simple_ui/level/layout.lua index 38c5707..4fdfe14 100644 --- a/lib/simple_ui/level/layout.lua +++ b/lib/simple_ui/level/layout.lua @@ -4,6 +4,96 @@ local Element = require "lib.simple_ui.element" local Rect = require "lib.simple_ui.rect" local SkillRow = require "lib.simple_ui.level.skill_row" +--- @class BarElement : UIElement +--- @field getter fun() : number +--- @field value number +--- @field maxValue number +local barElement = setmetatable({}, Element) +barElement.__index = barElement + +function barElement:update(dt) + local val = self.getter() + self.value = val < 0 and 0 or val > self.maxValue and self.maxValue or val +end + +function barElement:draw() + love.graphics.push() + love.graphics.applyTransform(self.transform) + + love.graphics.setColor(1, 1, 1) + + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width * self.value / self.maxValue, + self.bounds.height) + + love.graphics.setColor(1, 1, 1) + love.graphics.pop() +end + +--- @class BottomBars : UIElement +--- @field children BarElement[] +local bottomBars = setmetatable({}, Element) +bottomBars.__index = bottomBars; + +--- @param cid Id +function bottomBars.new(cid) + local t = setmetatable({}, bottomBars) + + t.children = { + barElement:new { + getter = function() + local char = Tree.level.characters[cid] + return char:try(Tree.behaviors.stats, function(stats) + return stats.hp or 0 + end) + end, + maxValue = 20 + }, + + barElement:new { + getter = function() + local char = Tree.level.characters[cid] + return char:try(Tree.behaviors.stats, function(stats) + return stats.mana or 0 + end) + end, + maxValue = 10 + } + } + + return t +end + +function bottomBars:update(dt) + local padding = 16 + local screenW, screenH = love.graphics.getDimensions() + self.bounds = Rect { + height = 10, + width = 200 + 200 + padding, + y = screenH - 96 - padding + } + + self.transform = love.math.newTransform():translate(-self.bounds.width / 2 + screenW / 2, self.bounds.y) + + for i = 1, #self.children do + self.children[i].bounds = Rect { + width = 200, + height = 20 + } + self.children[i].transform = self.transform:clone():translate(padding * (i - 1) + (i - 1) * 200, 0) + end + + + + for _, el in ipairs(self.children) do + el:update(dt) + end +end + +function bottomBars:draw() + for _, el in ipairs(self.children) do + el:draw() + end +end local layout = {} function layout:update(dt) @@ -11,14 +101,18 @@ function layout:update(dt) if cid then self.skillRow = SkillRow(cid) self.skillRow:show() + self.bottomBars = bottomBars.new(cid) elseif Tree.level.selector:deselected() then self.skillRow:hide() + self.bottomBars = nil end if self.skillRow then self.skillRow:update(dt) end + if self.bottomBars then self.bottomBars:update(dt) end end function layout:draw() if self.skillRow then self.skillRow:draw() end + if self.bottomBars then self.bottomBars:draw() end end return layout -- 2.47.2 From bcc376030cfbfda8e93bf017925d10ddfe7b50f4 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 6 Dec 2025 20:13:55 +0300 Subject: [PATCH 2/7] make ui use screen space dimensions add some fancy looks to the skill row --- assets/shaders/soft_uniform_noise.glsl | 17 +++++ lib/simple_ui/element.lua | 40 ++++++++++- lib/simple_ui/level/layout.lua | 22 +++--- lib/simple_ui/level/scale.lua | 2 + lib/simple_ui/level/skill_row.lua | 94 ++++++++++++++++++-------- lib/simple_ui/rect.lua | 2 +- main.lua | 2 +- 7 files changed, 133 insertions(+), 46 deletions(-) create mode 100644 assets/shaders/soft_uniform_noise.glsl create mode 100644 lib/simple_ui/level/scale.lua diff --git a/assets/shaders/soft_uniform_noise.glsl b/assets/shaders/soft_uniform_noise.glsl new file mode 100644 index 0000000..6cdba61 --- /dev/null +++ b/assets/shaders/soft_uniform_noise.glsl @@ -0,0 +1,17 @@ +#pragma language glsl3 + +vec2 hash(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 34.345); + return fract(vec2(p.x * p.y, p.x + p.y)); +} + +vec4 effect(vec4 color, Image tex, vec2 uv, vec2 px) +{ + vec2 cell = floor(px / 2.0); + + float n = hash(cell).x; // 0..1 + float v = 0.9 + n * 0.2; // 0.9..1.0 + + return vec4(v, v, v, 1.0); +} diff --git a/lib/simple_ui/element.lua b/lib/simple_ui/element.lua index b2433c0..4593a57 100644 --- a/lib/simple_ui/element.lua +++ b/lib/simple_ui/element.lua @@ -11,8 +11,7 @@ function uiElement:update(dt) end function uiElement:draw() end function uiElement:hitTest(screenX, screenY) - local lx, ly = self.transform:inverseTransformPoint(screenX, screenY) - return self.bounds:hasPoint(lx, ly) + return self.bounds:hasPoint(screenX, screenY) end --- @generic T : UIElement @@ -21,8 +20,43 @@ end --- @return T function uiElement.new(self, values) values.bounds = values.bounds or Rect {} - values.transform = values.transform or love.math.newTransform() return setmetatable(values, self) end +--- Рисует границу вокруг элемента (с псевдо-затенением) +--- @param type "outer" | "inner" +function uiElement:drawBorder(type) + if type == "inner" then + love.graphics.setColor(0.2, 0.2, 0.2) + love.graphics.line({ + self.bounds.x, self.bounds.y + self.bounds.height, + self.bounds.x, self.bounds.y, + self.bounds.x + self.bounds.width, self.bounds.y, + }) + + love.graphics.setColor(0.3, 0.3, 0.3) + love.graphics.line({ + self.bounds.x + self.bounds.width, self.bounds.y, + self.bounds.x + self.bounds.width, self.bounds.y + self.bounds.height, + self.bounds.x, self.bounds.y + self.bounds.height, + }) + else + love.graphics.setColor(0.2, 0.2, 0.2) + love.graphics.line({ + self.bounds.x + self.bounds.width, self.bounds.y, + self.bounds.x + self.bounds.width, self.bounds.y + self.bounds.height, + self.bounds.x, self.bounds.y + self.bounds.height, + }) + + love.graphics.setColor(0.3, 0.3, 0.3) + love.graphics.line({ + self.bounds.x, self.bounds.y + self.bounds.height, + self.bounds.x, self.bounds.y, + self.bounds.x + self.bounds.width, self.bounds.y, + }) + end + + love.graphics.setColor(1, 1, 1) +end + return uiElement diff --git a/lib/simple_ui/level/layout.lua b/lib/simple_ui/level/layout.lua index 4fdfe14..a385f8b 100644 --- a/lib/simple_ui/level/layout.lua +++ b/lib/simple_ui/level/layout.lua @@ -64,26 +64,26 @@ function bottomBars.new(cid) end function bottomBars:update(dt) - local padding = 16 + local padding = 8 local screenW, screenH = love.graphics.getDimensions() self.bounds = Rect { - height = 10, - width = 200 + 200 + padding, - y = screenH - 96 - padding + height = 40 + 2 * padding, + width = screenW * 0.25, + y = screenH - 64 - 1 - 2 * padding - 40 } self.transform = love.math.newTransform():translate(-self.bounds.width / 2 + screenW / 2, self.bounds.y) for i = 1, #self.children do self.children[i].bounds = Rect { - width = 200, + width = screenW * 0.25, height = 20 } - self.children[i].transform = self.transform:clone():translate(padding * (i - 1) + (i - 1) * 200, 0) + self.children[i].transform = self.transform:clone():translate(0, + self.children[i].bounds.height * (i - 1) + (i - 1) * padding) end - for _, el in ipairs(self.children) do el:update(dt) end @@ -101,18 +101,18 @@ function layout:update(dt) if cid then self.skillRow = SkillRow(cid) self.skillRow:show() - self.bottomBars = bottomBars.new(cid) + -- self.bottomBars = bottomBars.new(cid) elseif Tree.level.selector:deselected() then self.skillRow:hide() - self.bottomBars = nil + -- self.bottomBars = nil end if self.skillRow then self.skillRow:update(dt) end - if self.bottomBars then self.bottomBars:update(dt) end + -- if self.bottomBars then self.bottomBars:update(dt) end end function layout:draw() if self.skillRow then self.skillRow:draw() end - if self.bottomBars then self.bottomBars:draw() end + -- if self.bottomBars then self.bottomBars:draw() end end return layout diff --git a/lib/simple_ui/level/scale.lua b/lib/simple_ui/level/scale.lua new file mode 100644 index 0000000..c2b6807 --- /dev/null +++ b/lib/simple_ui/level/scale.lua @@ -0,0 +1,2 @@ +local UI_SCALE = 0.8 -- выдуманное значение для dependency injection +return UI_SCALE diff --git a/lib/simple_ui/level/skill_row.lua b/lib/simple_ui/level/skill_row.lua index ab9a9c9..aa0e92a 100644 --- a/lib/simple_ui/level/skill_row.lua +++ b/lib/simple_ui/level/skill_row.lua @@ -4,15 +4,18 @@ local AnimationNode = require "lib.animation_node" local Element = require "lib.simple_ui.element" local Rect = require "lib.simple_ui.rect" +local UI_SCALE = require "lib.simple_ui.level.scale" + --- @class SkillButton : UIElement --- @field hovered boolean --- @field selected boolean --- @field onClick function? ---- @field icon string +--- @field icon? string local skillButton = setmetatable({}, Element) skillButton.__index = skillButton function skillButton:update(dt) + if not self.icon then return end local mx, my = love.mouse.getPosition() if self:hitTest(mx, my) then self.hovered = true @@ -26,22 +29,32 @@ function skillButton:update(dt) end function skillButton:draw() - love.graphics.push() - love.graphics.applyTransform(self.transform) + love.graphics.setLineWidth(2) - local r, g, b, a = love.graphics.getColor() - if self.selected then - love.graphics.setColor(0.3, 1, 0.3, a) - elseif self.hovered then - love.graphics.setColor(0.7, 1, 0.7, a) - else - love.graphics.setColor(1, 1, 1, a) + if not self.icon then + love.graphics.setColor(0.05, 0.05, 0.05) + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) + self:drawBorder("inner") + return end - love.graphics.translate(0, self.bounds.y) - love.graphics.draw(icons.atlas, icons:pickQuad(self.icon)) + if self.selected then + love.graphics.setColor(0.3, 1, 0.3) + elseif self.hovered then + love.graphics.setColor(0.7, 1, 0.7) + else + love.graphics.setColor(1, 1, 1) + end + + local quad = icons:pickQuad(self.icon) + love.graphics.push() + love.graphics.translate(self.bounds.x, self.bounds.y) + love.graphics.scale(self.bounds.width / icons.tileSize, self.bounds.height / icons.tileSize) + love.graphics.draw(icons.atlas, quad) love.graphics.pop() + + self:drawBorder("inner") end -------------------------------------------------------------------------------- @@ -88,6 +101,10 @@ function skillRow.new(characterId) end end) + for i = #t.children + 1, 7, 1 do + t.children[i] = skillButton:new {} + end + return t end @@ -121,32 +138,51 @@ end function skillRow:update(dt) if self.animationNode then self.animationNode:update(dt) end - local iconSize = icons.tileSize - local scale = (64 / iconSize) + local iconSize = 64 * UI_SCALE local screenW, screenH = love.graphics.getDimensions() - local padding = 8 - local count = #self.children + local padding, margin = 8, 4 + local count = #self.children -- слоты под скиллы self.bounds = Rect { - width = count * icons.tileSize + (count - 1) * padding, - height = iconSize, - y = self.state == "show" and 10 * (1 - self.animationNode:getValue()) or 0 + width = iconSize * count + (count + 1) * margin, + height = iconSize + 2 * margin, } - self.transform = love.math.newTransform():translate(screenW / 2, - screenH - 16):scale(scale, scale):translate(-self.bounds.width / 2, -iconSize) + self.bounds.y = screenH - self.bounds.height - padding -- отступ снизу + self.bounds.x = screenW / 2 - self.bounds.width / 2 for i, skb in ipairs(self.children) do - skb.bounds = Rect { height = iconSize, width = iconSize } - skb.transform = self.transform:clone():translate(self.bounds.x + (i - 1) * iconSize + - (i - 1) * - padding, -- левый край ряда + размер предыдущих иконок + размер предыдущих отступов - self.bounds.y -- высота не меняется - ) + skb.bounds = Rect { x = self.bounds.x + margin + (i - 1) * (iconSize + margin), -- друг за другом, включая первый отступ от границы + y = self.bounds.y + margin, height = iconSize, width = iconSize } skb:update(dt) end end +local c = love.graphics.newCanvas(1280, 720) --- @TODO: выставлять канвасу правильный размер в зависимости от окна + function skillRow:draw() + love.graphics.setCanvas(c) + love.graphics.clear() + -- шум + love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise) + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) + love.graphics.setShader() + + -- фон + love.graphics.setColor(38 / 255, 50 / 255, 56 / 255) + love.graphics.setBlendMode("multiply", "premultiplied") + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) + love.graphics.setBlendMode("alpha") + + --граница + self:drawBorder("outer") + + love.graphics.setColor(1, 1, 1) + for _, skb in ipairs(self.children) do + skb:draw() + end + + love.graphics.setCanvas() + local alpha = 1 if self.state == "show" then alpha = self.animationNode:getValue() @@ -154,9 +190,7 @@ function skillRow:draw() alpha = 1 - self.animationNode:getValue() end love.graphics.setColor(1, 1, 1, alpha) - for _, skb in ipairs(self.children) do - skb:draw() - end + love.graphics.draw(c) end return skillRow.new diff --git a/lib/simple_ui/rect.lua b/lib/simple_ui/rect.lua index 3f3931f..932a0ad 100644 --- a/lib/simple_ui/rect.lua +++ b/lib/simple_ui/rect.lua @@ -18,7 +18,7 @@ function rect.new(table) end function rect:hasPoint(x, y) - return x >= self.x and x < self.width and y >= self.y and y < self.height + return x >= self.x and x < self.x + self.width and y >= self.y and y < self.y + self.height end return rect.new diff --git a/main.lua b/main.lua index 26825d5..603cfd8 100644 --- a/main.lua +++ b/main.lua @@ -18,7 +18,7 @@ function love.load() end Tree.level.turnOrder:endRound() print("Now playing:", Tree.level.turnOrder.current) - love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true }) + love.window.setMode(1280, 720, { resizable = false, msaa = 0, vsync = true }) end local lt = "0" -- 2.47.2 From f2169d333c9fa423e4776b4c0f4f84feef711d9c Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 6 Dec 2025 21:41:43 +0300 Subject: [PATCH 3/7] Add alpha mask shader and stencil-based skill icon masking --- assets/masks/rrect32.png | Bin 0 -> 1586 bytes assets/masks/squircle.png | Bin 0 -> 2802 bytes assets/shaders/alpha_mask.glsl | 8 ++++++++ assets/shaders/soft_uniform_noise.glsl | 4 ++-- lib/simple_ui/level/skill_row.lua | 26 +++++++++++++++++++++---- 5 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 assets/masks/rrect32.png create mode 100644 assets/masks/squircle.png create mode 100644 assets/shaders/alpha_mask.glsl diff --git a/assets/masks/rrect32.png b/assets/masks/rrect32.png new file mode 100644 index 0000000000000000000000000000000000000000..dff51021fcd016765cdeb73ff3d9c07e55611006 GIT binary patch literal 1586 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6%U;1OBOz`!j8!i<;h*8O2% zV1450;uumf=k49Mfzj>~Z4brG8}2a5I%sjMJFtXlH_P+~LUZ^6SPzP_XmPwcAS!yqA<#ejIZZA78&i?u5p0YPv_WP}RwPICY`Csuvau1gWTW^kX^#rSUwZ`S#+~6^M!BUpNE!aJa~BZ z?zgw!j~3rQv77f^>%px}^51T) ziErC~DOtOrc(VMrdmoe2+2x`b=X|})b-?1!##dI2p^P)W?4J1MccM+0oJh=8)*oq~ z4!@B3wzKy9C$0st4j;B}Y`^$xLk082Ph1aH7q$oPHa{*N&hR$mp#3p9xA)s0Rx)*K zH|QUeRr+IoySn-J(fXkPpE*z*2(36*%%eh2U>HyU{Gke^AdDPFwa~AZe*lwwXsPiUhP+i-S#J~%Z8`t5 zI-F^qbalsv+#l|GYzyY{?J@dikQLs2{h!W#?hjXQ9A0sE_T$rxJ4$pI+_HDoJiK2Z zmT+;&effeH#SDuJqeI;fAKZU%)$PXH@;`F!B!<}O`~RzYQpq5`jmbdja=YHQ=&h^~ zSNg={e#;!Vmv#QPMdgdL_Fr8V1oQJW?lpZ~{KL_IEyF2C#~a?-{J(UgpU-SC|H~f9 zd7y5_TH~LuCHYzRZj^1y9tmFaBWBd&4zs4@!N1$J}|b&`$s7QH8^z&GL6#IriTPonNiJ z{zLTjWzh|xx9&am4z2wZ*)VtJx8Favf3o*N?Z=Td@4PH~ojW{?Bdk zsp5dO& literal 0 HcmV?d00001 diff --git a/assets/masks/squircle.png b/assets/masks/squircle.png new file mode 100644 index 0000000000000000000000000000000000000000..8bd5a6dd7ee91001e1b45152877b8a3d7d35a057 GIT binary patch literal 2802 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 Iw=+}z1Ah44+yDRo literal 0 HcmV?d00001 diff --git a/assets/shaders/alpha_mask.glsl b/assets/shaders/alpha_mask.glsl new file mode 100644 index 0000000..5339bf7 --- /dev/null +++ b/assets/shaders/alpha_mask.glsl @@ -0,0 +1,8 @@ +vec4 effect(vec4 color, Image tex, vec2 texCoord, vec2 screenCoord) +{ + vec4 px = Texel(tex, texCoord); + if (px.a == 0.0) { + discard; + } + return vec4(1.0); +} diff --git a/assets/shaders/soft_uniform_noise.glsl b/assets/shaders/soft_uniform_noise.glsl index 6cdba61..e843952 100644 --- a/assets/shaders/soft_uniform_noise.glsl +++ b/assets/shaders/soft_uniform_noise.glsl @@ -8,10 +8,10 @@ vec2 hash(vec2 p) { vec4 effect(vec4 color, Image tex, vec2 uv, vec2 px) { - vec2 cell = floor(px / 2.0); + vec2 cell = floor(px / 2.0); // тут можно размер зерна менять float n = hash(cell).x; // 0..1 - float v = 0.9 + n * 0.2; // 0.9..1.0 + float v = 0.9 + n * 0.1; // 0.9..1.0 return vec4(v, v, v, 1.0); } diff --git a/lib/simple_ui/level/skill_row.lua b/lib/simple_ui/level/skill_row.lua index aa0e92a..049e1a5 100644 --- a/lib/simple_ui/level/skill_row.lua +++ b/lib/simple_ui/level/skill_row.lua @@ -160,8 +160,29 @@ end local c = love.graphics.newCanvas(1280, 720) --- @TODO: выставлять канвасу правильный размер в зависимости от окна function skillRow:draw() - love.graphics.setCanvas(c) + love.graphics.setCanvas({ c, stencil = true }) love.graphics.clear() + love.graphics.setColor(1, 1, 1) + + -- сначала иконки скиллов + for _, skb in ipairs(self.children) do + skb:draw() + end + + -- маска для вырезов под иконки + love.graphics.setShader(Tree.assets.files.shaders.alpha_mask) + love.graphics.stencil(function() + local mask = Tree.assets.files.masks.rrect32 + local maskSize = mask:getWidth() + for _, skb in ipairs(self.children) do + love.graphics.draw(mask, skb.bounds.x, skb.bounds.y, 0, + skb.bounds.width / maskSize, skb.bounds.height / maskSize) + end + end, "replace", 1) + love.graphics.setShader() + + -- дальше рисуем панель, перекрывая иконки + love.graphics.setStencilTest("less", 1) -- шум love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise) love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) @@ -177,9 +198,6 @@ function skillRow:draw() self:drawBorder("outer") love.graphics.setColor(1, 1, 1) - for _, skb in ipairs(self.children) do - skb:draw() - end love.graphics.setCanvas() -- 2.47.2 From bc1c6cfd6a475bcd269c9ec12e1bdfabfd32df06 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 7 Dec 2025 01:57:45 +0300 Subject: [PATCH 4/7] add gradient like in windows aero --- lib/simple_ui/element.lua | 28 ++++++++++++++++++++++++++-- lib/simple_ui/level/skill_row.lua | 6 +++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/simple_ui/element.lua b/lib/simple_ui/element.lua index 4593a57..708fd34 100644 --- a/lib/simple_ui/element.lua +++ b/lib/simple_ui/element.lua @@ -1,9 +1,22 @@ local Rect = require "lib.simple_ui.rect" +local function makeGradientMesh(w, h, topColor, bottomColor) + local vertices = { + { 0, 0, 0, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- левый верх + { w, 0, 1, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- правый верх + { w, h, 1, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- правый низ + { 0, h, 0, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- левый низ + } + local mesh = love.graphics.newMesh(vertices, "fan", "static") + return mesh +end + --- @class UIElement ---- @field bounds Rect Прямоугольник, в границах которого размещается элемент. Размеры и положение в *локальных* координатах ---- @field transform love.Transform Преобразование из локальных координат элемента (bounds) в экранные координаты +--- @field bounds Rect Прямоугольник, в границах которого размещается элемент. Размеры и положение в экранных координатах +--- @field overlayGradientMesh love.Mesh Общий градиент поверх элемента (интерполированный меш) local uiElement = {} +uiElement.bounds = Rect {} +uiElement.overlayGradientMesh = makeGradientMesh(1, 1, { 0, 0, 0, 0 }, { 0, 0, 0, 0.4 }); uiElement.__index = uiElement function uiElement:update(dt) end @@ -20,6 +33,7 @@ end --- @return T function uiElement.new(self, values) values.bounds = values.bounds or Rect {} + values.overlayGradientMesh = values.overlayGradientMesh or uiElement.bounds; return setmetatable(values, self) end @@ -59,4 +73,14 @@ function uiElement:drawBorder(type) love.graphics.setColor(1, 1, 1) end +--- рисует градиент поверх элемента +function uiElement:drawGradientOverlay() + love.graphics.push() + love.graphics.translate(self.bounds.x, self.bounds.y) + love.graphics.scale(self.bounds.width, self.bounds.height) + love.graphics.setColor(1, 1, 1, 1) + love.graphics.draw(self.overlayGradientMesh) + love.graphics.pop() +end + return uiElement diff --git a/lib/simple_ui/level/skill_row.lua b/lib/simple_ui/level/skill_row.lua index 049e1a5..19d8ab0 100644 --- a/lib/simple_ui/level/skill_row.lua +++ b/lib/simple_ui/level/skill_row.lua @@ -197,8 +197,12 @@ function skillRow:draw() --граница self:drawBorder("outer") - love.graphics.setColor(1, 1, 1) + love.graphics.setStencilTest() + --затенение + self:drawGradientOverlay() + + love.graphics.setColor(1, 1, 1) love.graphics.setCanvas() local alpha = 1 -- 2.47.2 From 73ba99734c4da38d0776e421f747806b59e0841d Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 7 Dec 2025 02:04:08 +0300 Subject: [PATCH 5/7] add better skill hover & select visuals --- lib/simple_ui/level/skill_row.lua | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/simple_ui/level/skill_row.lua b/lib/simple_ui/level/skill_row.lua index 19d8ab0..65ebda3 100644 --- a/lib/simple_ui/level/skill_row.lua +++ b/lib/simple_ui/level/skill_row.lua @@ -39,14 +39,6 @@ function skillButton:draw() return end - if self.selected then - love.graphics.setColor(0.3, 1, 0.3) - elseif self.hovered then - love.graphics.setColor(0.7, 1, 0.7) - else - love.graphics.setColor(1, 1, 1) - end - local quad = icons:pickQuad(self.icon) love.graphics.push() love.graphics.translate(self.bounds.x, self.bounds.y) @@ -55,6 +47,15 @@ function skillButton:draw() love.graphics.pop() self:drawBorder("inner") + + if self.selected then + love.graphics.setColor(0.3, 1, 0.3, 0.5) + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) + elseif self.hovered then + love.graphics.setColor(0.7, 1, 0.7, 0.5) + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) + end + love.graphics.setColor(1, 1, 1) end -------------------------------------------------------------------------------- -- 2.47.2 From bc730ef48cc411159ff8a033fd5be69deccca237 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 7 Dec 2025 20:08:07 +0300 Subject: [PATCH 6/7] implement hp and mana bars --- lib/simple_ui/color.lua | 20 +++ lib/simple_ui/element.lua | 2 +- lib/simple_ui/level/layout.lua | 216 ++++++++++++++++++++++++------ lib/simple_ui/level/skill_row.lua | 47 +------ 4 files changed, 198 insertions(+), 87 deletions(-) create mode 100644 lib/simple_ui/color.lua diff --git a/lib/simple_ui/color.lua b/lib/simple_ui/color.lua new file mode 100644 index 0000000..f97788d --- /dev/null +++ b/lib/simple_ui/color.lua @@ -0,0 +1,20 @@ +--- @class Color +--- @field r number +--- @field g number +--- @field b number +--- @field a number +local color = { + r = 1, + g = 1, + b = 1, + a = 1 +} +color.__index = color + +--- @param rgba {r?: number, g?: number, b?: number, a?: number} +--- @return Color +function color.new(rgba) + return setmetatable(rgba, color) +end + +return color.new diff --git a/lib/simple_ui/element.lua b/lib/simple_ui/element.lua index 708fd34..d73e462 100644 --- a/lib/simple_ui/element.lua +++ b/lib/simple_ui/element.lua @@ -33,7 +33,7 @@ end --- @return T function uiElement.new(self, values) values.bounds = values.bounds or Rect {} - values.overlayGradientMesh = values.overlayGradientMesh or uiElement.bounds; + values.overlayGradientMesh = values.overlayGradientMesh or uiElement.overlayGradientMesh; return setmetatable(values, self) end diff --git a/lib/simple_ui/level/layout.lua b/lib/simple_ui/level/layout.lua index a385f8b..2f2fe8a 100644 --- a/lib/simple_ui/level/layout.lua +++ b/lib/simple_ui/level/layout.lua @@ -3,13 +3,19 @@ local AnimationNode = require "lib.animation_node" local Element = require "lib.simple_ui.element" local Rect = require "lib.simple_ui.rect" local SkillRow = require "lib.simple_ui.level.skill_row" +local Color = require "lib.simple_ui.color" --- @class BarElement : UIElement --- @field getter fun() : number --- @field value number --- @field maxValue number +--- @field color Color +--- @field useDividers boolean +--- @field drawText boolean local barElement = setmetatable({}, Element) barElement.__index = barElement +barElement.useDividers = false +barElement.drawText = false function barElement:update(dt) local val = self.getter() @@ -17,20 +23,54 @@ function barElement:update(dt) end function barElement:draw() - love.graphics.push() - love.graphics.applyTransform(self.transform) + local valueWidth = self.bounds.width * self.value / self.maxValue + local emptyWidth = self.bounds.width - valueWidth - love.graphics.setColor(1, 1, 1) + --- шум + love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise) + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) + love.graphics.setShader() - love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width * self.value / self.maxValue, + --- закраска пустой части + love.graphics.setColor(0.05, 0.05, 0.05) + love.graphics.setBlendMode("multiply", "premultiplied") + love.graphics.rectangle("fill", self.bounds.x + valueWidth, self.bounds.y, emptyWidth, self.bounds.height) + love.graphics.setBlendMode("alpha") + + --- закраска значимой части её цветом + love.graphics.setColor(self.color.r, self.color.g, self.color.b) + love.graphics.setBlendMode("multiply", "premultiplied") + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, valueWidth, + self.bounds.height) + love.graphics.setBlendMode("alpha") + + --- мерки + love.graphics.setColor(38 / 255, 50 / 255, 56 / 255) + if self.useDividers then + local count = self.maxValue - 1 + local measureWidth = self.bounds.width / self.maxValue + + for i = 1, count, 1 do + love.graphics.line(self.bounds.x + i * measureWidth, self.bounds.y, self.bounds.x + i * measureWidth, + self.bounds.y + self.bounds.height) + end + end + love.graphics.setColor(1, 1, 1) - love.graphics.pop() + --- текст поверх + if self.drawText then + love.graphics.printf(tostring(self.value) .. "/" .. tostring(self.maxValue), self.bounds.x, + self.bounds.y, self.bounds.width, "center") + end + + self:drawGradientOverlay() end --- @class BottomBars : UIElement ---- @field children BarElement[] +--- @field hpBar BarElement +--- @field manaBar BarElement local bottomBars = setmetatable({}, Element) bottomBars.__index = bottomBars; @@ -38,7 +78,7 @@ bottomBars.__index = bottomBars; function bottomBars.new(cid) local t = setmetatable({}, bottomBars) - t.children = { + t.hpBar = barElement:new { getter = function() local char = Tree.level.characters[cid] @@ -46,9 +86,12 @@ function bottomBars.new(cid) return stats.hp or 0 end) end, + color = Color { r = 130 / 255, g = 8 / 255, b = 8 / 255 }, + drawText = true, maxValue = 20 - }, + } + t.manaBar = barElement:new { getter = function() local char = Tree.level.characters[cid] @@ -56,63 +99,154 @@ function bottomBars.new(cid) return stats.mana or 0 end) end, + color = Color { r = 51 / 255, g = 105 / 255, b = 30 / 255 }, + useDividers = true, maxValue = 10 } - } + return t end function bottomBars:update(dt) - local padding = 8 - local screenW, screenH = love.graphics.getDimensions() - self.bounds = Rect { - height = 40 + 2 * padding, - width = screenW * 0.25, - y = screenH - 64 - 1 - 2 * padding - 40 + local height = 14 + local margin = 2 + + self.bounds.height = height + self.bounds.y = self.bounds.y - height + + self.hpBar.bounds = Rect { + width = -2 * margin + self.bounds.width / 2, + height = height, + x = self.bounds.x + margin, + y = self.bounds.y } - self.transform = love.math.newTransform():translate(-self.bounds.width / 2 + screenW / 2, self.bounds.y) + self.manaBar.bounds = Rect { + width = -2 * margin + self.bounds.width / 2, + height = height, + x = self.bounds.x + margin + self.bounds.width / 2, + y = self.bounds.y + } - for i = 1, #self.children do - self.children[i].bounds = Rect { - width = screenW * 0.25, - height = 20 - } - self.children[i].transform = self.transform:clone():translate(0, - self.children[i].bounds.height * (i - 1) + (i - 1) * padding) - end - - - for _, el in ipairs(self.children) do - el:update(dt) - end + self.hpBar:update(dt) + self.manaBar:update(dt) end function bottomBars:draw() - for _, el in ipairs(self.children) do - el:draw() - end + -- шум + love.graphics.setShader(Tree.assets.files.shaders.soft_uniform_noise) + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) + love.graphics.setShader() + + love.graphics.setColor(38 / 255, 50 / 255, 56 / 255) + love.graphics.setBlendMode("multiply", "premultiplied") + love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) + love.graphics.setBlendMode("alpha") + + self.hpBar:draw() + self.manaBar:draw() end +local c = love.graphics.newCanvas(1280, 720) --- @TODO: выставлять канвасу правильный размер в зависимости от окна + +--- @class CharacterPanel : UIElement +--- @field animationNode AnimationNode +--- @field state "show" | "idle" | "hide" +--- @field skillRow SkillRow +--- @field bars BottomBars +local characterPanel = setmetatable({}, Element) +characterPanel.__index = characterPanel + +function characterPanel.new(characterId) + local t = {} + t.state = "show" + t.skillRow = SkillRow(characterId) + t.bars = bottomBars.new(characterId) + return setmetatable(t, characterPanel) +end + +function characterPanel:show() + AnimationNode { + function(animationNode) + if self.animationNode then self.animationNode:finish() end + self.animationNode = animationNode + self.state = "show" + end, + duration = 300, + onEnd = function() + self.state = "idle" + end, + easing = easing.easeOutCubic + }:run() +end + +function characterPanel:hide() + AnimationNode { + function(animationNode) + if self.animationNode then self.animationNode:finish() end + self.animationNode = animationNode + self.state = "hide" + end, + duration = 300, + easing = easing.easeOutCubic + }:run() +end + +function characterPanel:update(dt) + if self.animationNode then self.animationNode:update(dt) end + self.skillRow:update(dt) + self.bars.bounds = Rect { + width = self.skillRow.bounds.width, + x = self.skillRow.bounds.x, + y = self.skillRow.bounds.y + } + self.bars:update(dt) + + self.bounds = Rect { + x = self.bars.bounds.x, + y = self.bars.bounds.y, + width = self.bars.bounds.width, + height = self.bars.bounds.height + self.skillRow.bounds.height + } +end + +function characterPanel:draw() + love.graphics.setCanvas(c) + love.graphics.clear() + self.skillRow:draw() + self.bars:draw() + + self:drawBorder("outer") + + local alpha = 1 + if self.state == "show" then + alpha = self.animationNode:getValue() + elseif self.state == "hide" then + alpha = 1 - self.animationNode:getValue() + end + + love.graphics.setCanvas() + love.graphics.setColor(1, 1, 1, alpha) + love.graphics.draw(c) + love.graphics.setColor(1, 1, 1) +end + +----------------------------------- local layout = {} function layout:update(dt) local cid = Tree.level.selector:selected() if cid then - self.skillRow = SkillRow(cid) - self.skillRow:show() - -- self.bottomBars = bottomBars.new(cid) + self.characterPanel = characterPanel.new(cid) + self.characterPanel:show() elseif Tree.level.selector:deselected() then - self.skillRow:hide() - -- self.bottomBars = nil + self.characterPanel:hide() end - if self.skillRow then self.skillRow:update(dt) end - -- if self.bottomBars then self.bottomBars:update(dt) end + if self.characterPanel then self.characterPanel:update(dt) end end function layout:draw() - if self.skillRow then self.skillRow:draw() end - -- if self.bottomBars then self.bottomBars:draw() end + if self.characterPanel then self.characterPanel:draw() end end return layout diff --git a/lib/simple_ui/level/skill_row.lua b/lib/simple_ui/level/skill_row.lua index 65ebda3..fc51b67 100644 --- a/lib/simple_ui/level/skill_row.lua +++ b/lib/simple_ui/level/skill_row.lua @@ -63,8 +63,6 @@ end --- @class SkillRow : UIElement --- @field characterId Id --- @field selected SkillButton? ---- @field animationNode AnimationNode ---- @field state "show" | "idle" | "hide" --- @field children SkillButton[] local skillRow = setmetatable({}, Element) skillRow.__index = skillRow @@ -74,7 +72,6 @@ skillRow.__index = skillRow function skillRow.new(characterId) local t = { characterId = characterId, - state = "show", children = {} } @@ -85,7 +82,6 @@ function skillRow.new(characterId) for i, spell in ipairs(behavior.spellbook) do local skb = skillButton:new { icon = spell.tag } skb.onClick = function() - if t.state ~= "idle" then return end skb.selected = not skb.selected if t.selected then t.selected.selected = false end t.selected = skb @@ -109,36 +105,7 @@ function skillRow.new(characterId) return t end -function skillRow:show() - AnimationNode { - function(animationNode) - if self.animationNode then self.animationNode:finish() end - self.animationNode = animationNode - self.state = "show" - end, - duration = 300, - onEnd = function() - self.state = "idle" - end, - easing = easing.easeOutCubic - }:run() -end - -function skillRow:hide() - AnimationNode { - function(animationNode) - if self.animationNode then self.animationNode:finish() end - self.animationNode = animationNode - self.state = "hide" - end, - duration = 300, - easing = easing.easeOutCubic - }:run() -end - function skillRow:update(dt) - if self.animationNode then self.animationNode:update(dt) end - local iconSize = 64 * UI_SCALE local screenW, screenH = love.graphics.getDimensions() local padding, margin = 8, 4 @@ -161,6 +128,7 @@ end local c = love.graphics.newCanvas(1280, 720) --- @TODO: выставлять канвасу правильный размер в зависимости от окна function skillRow:draw() + local oldCanvas = love.graphics.getCanvas() love.graphics.setCanvas({ c, stencil = true }) love.graphics.clear() love.graphics.setColor(1, 1, 1) @@ -195,24 +163,13 @@ function skillRow:draw() love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) love.graphics.setBlendMode("alpha") - --граница - self:drawBorder("outer") - love.graphics.setStencilTest() --затенение self:drawGradientOverlay() love.graphics.setColor(1, 1, 1) - love.graphics.setCanvas() - - local alpha = 1 - if self.state == "show" then - alpha = self.animationNode:getValue() - elseif self.state == "hide" then - alpha = 1 - self.animationNode:getValue() - end - love.graphics.setColor(1, 1, 1, alpha) + love.graphics.setCanvas(oldCanvas) love.graphics.draw(c) end -- 2.47.2 From 3d0d52438f36e9c49c5647f1abbf5f65915f603f Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 7 Dec 2025 20:27:48 +0300 Subject: [PATCH 7/7] add a shader for a cool pixel reveal effect --- assets/shaders/reveal.glsl | 24 ++++++++++++++++++++++++ lib/simple_ui/level/layout.lua | 22 ++++++++++++++-------- lib/simple_ui/level/skill_row.lua | 2 -- 3 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 assets/shaders/reveal.glsl diff --git a/assets/shaders/reveal.glsl b/assets/shaders/reveal.glsl new file mode 100644 index 0000000..2af557e --- /dev/null +++ b/assets/shaders/reveal.glsl @@ -0,0 +1,24 @@ +extern float t; +extern float blockSize; + +// hash-функция для шума по целочисленным координатам блока +float hash(vec2 p) { + p = vec2( + dot(p, vec2(127.1, 311.7)), + dot(p, vec2(269.5, 183.3)) + ); + return fract(sin(p.x + p.y) * 43758.5453123); +} + +vec4 effect(vec4 color, Image tex, vec2 texCoord, vec2 screenCoord) +{ + float blockSize = 4.0; + + vec2 cell = floor(screenCoord / blockSize); + float n = hash(cell); // [0..1] + float mask = 1.0 - step(t, n); + + vec4 base = Texel(tex, texCoord) * color; + base.a *= mask; + return base; +} diff --git a/lib/simple_ui/level/layout.lua b/lib/simple_ui/level/layout.lua index 2f2fe8a..0ace175 100644 --- a/lib/simple_ui/level/layout.lua +++ b/lib/simple_ui/level/layout.lua @@ -209,6 +209,16 @@ function characterPanel:update(dt) width = self.bars.bounds.width, height = self.bars.bounds.height + self.skillRow.bounds.height } + + --- анимация появления + local alpha = 1 + if self.state == "show" then + alpha = self.animationNode:getValue() + elseif self.state == "hide" then + alpha = 1 - self.animationNode:getValue() + end + local revealShader = Tree.assets.files.shaders.reveal + revealShader:send("t", alpha) end function characterPanel:draw() @@ -219,17 +229,13 @@ function characterPanel:draw() self:drawBorder("outer") - local alpha = 1 - if self.state == "show" then - alpha = self.animationNode:getValue() - elseif self.state == "hide" then - alpha = 1 - self.animationNode:getValue() - end - + --- рисуем текстуру шейдером появления love.graphics.setCanvas() - love.graphics.setColor(1, 1, 1, alpha) + love.graphics.setShader(Tree.assets.files.shaders.reveal) + love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(c) love.graphics.setColor(1, 1, 1) + love.graphics.setShader() end ----------------------------------- diff --git a/lib/simple_ui/level/skill_row.lua b/lib/simple_ui/level/skill_row.lua index fc51b67..ebb01bd 100644 --- a/lib/simple_ui/level/skill_row.lua +++ b/lib/simple_ui/level/skill_row.lua @@ -1,6 +1,4 @@ local icons = require("lib.utils.sprite_atlas").load(Tree.assets.files.dev_icons) -local easing = require "lib.utils.easing" -local AnimationNode = require "lib.animation_node" local Element = require "lib.simple_ui.element" local Rect = require "lib.simple_ui.rect" -- 2.47.2