Compare commits

..

No commits in common. "7394249cb893b325c80ac47785e99a4114aad0ca" and "c61c1875e790f250b515c5917ad5923462331e47" have entirely different histories.

12 changed files with 80 additions and 455 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,8 +0,0 @@
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);
}

View File

@ -1,24 +0,0 @@
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;
}

View File

@ -1,17 +0,0 @@
#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.1; // 0.9..1.0
return vec4(v, v, v, 1.0);
}

View File

@ -1,20 +0,0 @@
--- @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

View File

@ -1,22 +1,9 @@
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 overlayGradientMesh love.Mesh Общий градиент поверх элемента (интерполированный меш)
--- @field bounds Rect Прямоугольник, в границах которого размещается элемент. Размеры и положение в *локальных* координатах
--- @field transform love.Transform Преобразование из локальных координат элемента (bounds) в экранные координаты
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
@ -24,7 +11,8 @@ function uiElement:update(dt) end
function uiElement:draw() end
function uiElement:hitTest(screenX, screenY)
return self.bounds:hasPoint(screenX, screenY)
local lx, ly = self.transform:inverseTransformPoint(screenX, screenY)
return self.bounds:hasPoint(lx, ly)
end
--- @generic T : UIElement
@ -33,54 +21,8 @@ end
--- @return T
function uiElement.new(self, values)
values.bounds = values.bounds or Rect {}
values.overlayGradientMesh = values.overlayGradientMesh or uiElement.overlayGradientMesh;
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
--- рисует градиент поверх элемента
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

View File

@ -3,256 +3,22 @@ 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()
self.value = val < 0 and 0 or val > self.maxValue and self.maxValue or val
end
function barElement:draw()
local valueWidth = self.bounds.width * self.value / self.maxValue
local emptyWidth = self.bounds.width - valueWidth
--- шум
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(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)
--- текст поверх
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 hpBar BarElement
--- @field manaBar BarElement
local bottomBars = setmetatable({}, Element)
bottomBars.__index = bottomBars;
--- @param cid Id
function bottomBars.new(cid)
local t = setmetatable({}, bottomBars)
t.hpBar =
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,
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]
return char:try(Tree.behaviors.stats, function(stats)
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 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.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
}
self.hpBar:update(dt)
self.manaBar:update(dt)
end
function bottomBars:draw()
-- шум
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
}
--- анимация появления
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()
love.graphics.setCanvas(c)
love.graphics.clear()
self.skillRow:draw()
self.bars:draw()
self:drawBorder("outer")
--- рисуем текстуру шейдером появления
love.graphics.setCanvas()
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
-----------------------------------
local layout = {}
function layout:update(dt)
local cid = Tree.level.selector:selected()
if cid then
self.characterPanel = characterPanel.new(cid)
self.characterPanel:show()
self.skillRow = SkillRow(cid)
self.skillRow:show()
elseif Tree.level.selector:deselected() then
self.characterPanel:hide()
self.skillRow:hide()
end
if self.characterPanel then self.characterPanel:update(dt) end
if self.skillRow then self.skillRow:update(dt) end
end
function layout:draw()
if self.characterPanel then self.characterPanel:draw() end
if self.skillRow then self.skillRow:draw() end
end
return layout

View File

@ -1,2 +0,0 @@
local UI_SCALE = 0.8 -- выдуманное значение для dependency injection
return UI_SCALE

View File

@ -1,19 +1,18 @@
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"
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
@ -27,33 +26,22 @@ function skillButton:update(dt)
end
function skillButton:draw()
love.graphics.setLineWidth(2)
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
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()
love.graphics.applyTransform(self.transform)
self:drawBorder("inner")
local r, g, b, a = love.graphics.getColor()
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)
love.graphics.setColor(0.3, 1, 0.3, a)
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)
love.graphics.setColor(0.7, 1, 0.7, a)
else
love.graphics.setColor(1, 1, 1, a)
end
love.graphics.setColor(1, 1, 1)
love.graphics.translate(0, self.bounds.y)
love.graphics.draw(icons.atlas, icons:pickQuad(self.icon))
love.graphics.pop()
end
--------------------------------------------------------------------------------
@ -61,6 +49,8 @@ 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
@ -70,6 +60,7 @@ skillRow.__index = skillRow
function skillRow.new(characterId)
local t = {
characterId = characterId,
state = "show",
children = {}
}
@ -80,6 +71,7 @@ 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
@ -96,79 +88,75 @@ function skillRow.new(characterId)
end
end)
for i = #t.children + 1, 7, 1 do
t.children[i] = skillButton:new {}
end
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)
local iconSize = 64 * UI_SCALE
if self.animationNode then self.animationNode:update(dt) end
local iconSize = icons.tileSize
local scale = (64 / iconSize)
local screenW, screenH = love.graphics.getDimensions()
local padding, margin = 8, 4
local count = #self.children -- слоты под скиллы
local padding = 8
local count = #self.children
self.bounds = Rect {
width = iconSize * count + (count + 1) * margin,
height = iconSize + 2 * margin,
width = count * icons.tileSize + (count - 1) * padding,
height = iconSize,
y = self.state == "show" and 10 * (1 - self.animationNode:getValue()) or 0
}
self.bounds.y = screenH - self.bounds.height - padding -- отступ снизу
self.bounds.x = screenW / 2 - self.bounds.width / 2
self.transform = love.math.newTransform():translate(screenW / 2,
screenH - 16):scale(scale, scale):translate(-self.bounds.width / 2, -iconSize)
for i, skb in ipairs(self.children) do
skb.bounds = Rect { x = self.bounds.x + margin + (i - 1) * (iconSize + margin), -- друг за другом, включая первый отступ от границы
y = self.bounds.y + margin, height = iconSize, width = iconSize }
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:update(dt)
end
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)
-- сначала иконки скиллов
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)
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)
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")
love.graphics.setStencilTest()
--затенение
self:drawGradientOverlay()
love.graphics.setColor(1, 1, 1)
love.graphics.setCanvas(oldCanvas)
love.graphics.draw(c)
end
return skillRow.new

View File

@ -18,7 +18,7 @@ function rect.new(table)
end
function rect:hasPoint(x, y)
return x >= self.x and x < self.x + self.width and y >= self.y and y < self.y + self.height
return x >= self.x and x < self.width and y >= self.y and y < self.height
end
return rect.new

View File

@ -18,7 +18,7 @@ function love.load()
end
Tree.level.turnOrder:endRound()
print("Now playing:", Tree.level.turnOrder.current)
love.window.setMode(1280, 720, { resizable = false, msaa = 0, vsync = true })
love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true })
end
local lt = "0"