mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
launcher spruce p2
This commit is contained in:
+469
-194
@@ -23,21 +23,39 @@ local REQUIRED_FILES = {
|
||||
"assets/generated/audio/programs.bin",
|
||||
}
|
||||
|
||||
-- Split-screen ROM-selector palette (the "Split-screen ROM selector" design).
|
||||
-- One column per game: Red is live, Blue and Yellow are placeholders until
|
||||
-- those games are supported. Values are 0-255 RGB.
|
||||
-- "Split-screen ROM selector" first-run palette (matches FirstRun.dc.html from
|
||||
-- the Claude Design project): a dark neon arcade panel, one column per game.
|
||||
-- Red is live; Blue and Yellow are lit placeholders until those games are
|
||||
-- supported. Values are 0-255 RGB; alpha is applied per draw.
|
||||
local PAL = {
|
||||
bg = { 241, 243, 232 },
|
||||
detail = { 74, 88, 72 },
|
||||
heading = { 25, 31, 28 },
|
||||
box = { 215, 220, 202 },
|
||||
-- radial background gradient (bright navy at top-centre -> near black)
|
||||
bgTop = { 22, 34, 74 }, -- #16224a
|
||||
bgBot = { 7, 11, 29 }, -- #070b1d
|
||||
-- neon accents, one per cartridge
|
||||
red = { 255, 60, 72 }, -- rgb(255,60,72)
|
||||
blue = { 70, 150, 255 }, -- rgb(70,150,255)
|
||||
gold = { 255, 203, 5 }, -- rgb(255,203,5)
|
||||
-- card interiors (the dark colour the accent tint fades into)
|
||||
cardRed = { 20, 12, 26 }, -- #140c1a
|
||||
cardBlue = { 12, 18, 40 }, -- #0c1228
|
||||
cardGold = { 30, 22, 8 }, -- #1e1608
|
||||
-- text
|
||||
heading = { 255, 255, 255 },
|
||||
detail = { 198, 208, 230 }, -- #c6d0e6
|
||||
warning = { 159, 176, 208 }, -- #9fb0d0
|
||||
link = { 127, 208, 255 }, -- #7fd0ff, the bois.icu link
|
||||
linkHover = { 191, 234, 255 }, -- #bfeaff, brighter on hover
|
||||
white = { 255, 255, 255 },
|
||||
red = { 181, 35, 42 },
|
||||
blue = { 30, 86, 168 },
|
||||
gold = { 214, 164, 0 },
|
||||
goldInk = { 160, 120, 0 },
|
||||
disabled = { 146, 158, 178 },
|
||||
disabledInk = { 238, 242, 247 },
|
||||
-- "Play" button (green gradient) + its ink
|
||||
playTop = { 62, 224, 138 }, -- #3ee08a
|
||||
playBot = { 22, 163, 90 }, -- #16a35a
|
||||
playInk = { 6, 32, 18 }, -- #062012
|
||||
-- "Choose ROM" button (red gradient)
|
||||
chooseTop = { 255, 83, 97 }, -- #ff5361
|
||||
chooseBot = { 214, 31, 44 }, -- #d61f2c
|
||||
-- disabled "Coming soon" button
|
||||
disabled = { 120, 132, 158 },
|
||||
disabledInk = { 149, 161, 189 }, -- #95a1bd
|
||||
}
|
||||
|
||||
local function allRequiredFilesExist()
|
||||
@@ -446,205 +464,264 @@ local function clamp(v, lo, hi)
|
||||
return math.max(lo, math.min(hi, v))
|
||||
end
|
||||
|
||||
-- set the current draw color from a PAL triple (0-255), with optional alpha 0-1
|
||||
-- set the current draw colour from a PAL triple (0-255), with optional alpha 0-1
|
||||
local function col(c, a)
|
||||
love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255, a or 1)
|
||||
end
|
||||
|
||||
-- LOVE has no dashed-stroke primitive; step short segments along each edge so
|
||||
-- the "coming soon" Blue/Yellow boxes read as placeholders
|
||||
local function dashedRect(x, y, w, h, dash, gap)
|
||||
local step = dash + gap
|
||||
local function edge(x1, y1, x2, y2)
|
||||
local dx, dy = x2 - x1, y2 - y1
|
||||
local len = math.sqrt(dx * dx + dy * dy)
|
||||
if len == 0 then return end
|
||||
local ux, uy = dx / len, dy / len
|
||||
local d = 0
|
||||
while d < len do
|
||||
local e = math.min(d + dash, len)
|
||||
love.graphics.line(x1 + ux * d, y1 + uy * d, x1 + ux * e, y1 + uy * e)
|
||||
d = d + step
|
||||
end
|
||||
-- Faux-bold: the launcher's UI font ships no bold face, so 800-weight text
|
||||
-- (headings, buttons) is thickened with a second sub-pixel pass.
|
||||
local function printfB(text, x, y, w, align)
|
||||
love.graphics.printf(text, x, y, w, align)
|
||||
love.graphics.printf(text, x + 0.6, y, w, align)
|
||||
end
|
||||
local function printB(text, x, y)
|
||||
love.graphics.print(text, x, y)
|
||||
love.graphics.print(text, x + 0.6, y)
|
||||
end
|
||||
|
||||
-- One reusable unit quad, recoloured per call, for every vertical gradient
|
||||
-- fill (LOVE has no gradient primitive and a per-frame newMesh would churn
|
||||
-- the GPU). Callers set the blend mode; this only touches colour + geometry.
|
||||
local gradMesh
|
||||
local function setGrad(cTop, cBot, aTop, aBot)
|
||||
if not gradMesh then gradMesh = love.graphics.newMesh(4, "fan", "dynamic") end
|
||||
gradMesh:setVertices({
|
||||
{ 0, 0, 0, 0, cTop[1] / 255, cTop[2] / 255, cTop[3] / 255, aTop },
|
||||
{ 1, 0, 1, 0, cTop[1] / 255, cTop[2] / 255, cTop[3] / 255, aTop },
|
||||
{ 1, 1, 1, 1, cBot[1] / 255, cBot[2] / 255, cBot[3] / 255, aBot },
|
||||
{ 0, 1, 0, 1, cBot[1] / 255, cBot[2] / 255, cBot[3] / 255, aBot },
|
||||
})
|
||||
end
|
||||
local function fillGrad(x, y, w, h, cTop, cBot, aTop, aBot)
|
||||
setGrad(cTop, cBot, aTop, aBot)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(gradMesh, x, y, 0, w, h)
|
||||
end
|
||||
-- vertical gradient clipped to a rounded rectangle (via the stencil buffer)
|
||||
local function fillGradRounded(x, y, w, h, r, cTop, cBot, aTop, aBot)
|
||||
love.graphics.stencil(function()
|
||||
love.graphics.rectangle("fill", x, y, w, h, r, r)
|
||||
end, "replace", 1)
|
||||
love.graphics.setStencilTest("greater", 0)
|
||||
fillGrad(x, y, w, h, cTop, cBot, aTop, aBot)
|
||||
love.graphics.setStencilTest()
|
||||
end
|
||||
|
||||
-- Soft additive neon halo around a rounded rect. LOVE has no blur, so stack
|
||||
-- progressively larger, fainter translucent rounded rects.
|
||||
local function neonGlow(x, y, w, h, r, c, strength)
|
||||
strength = math.max(0, strength)
|
||||
if strength == 0 then return end
|
||||
love.graphics.setBlendMode("add")
|
||||
local layers = 7
|
||||
for i = 1, layers do
|
||||
local g = i * 2.4
|
||||
love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255,
|
||||
strength * 0.05 * (1 - (i - 1) / layers))
|
||||
love.graphics.rectangle("fill", x - g, y - g, w + 2 * g, h + 2 * g, r + g, r + g)
|
||||
end
|
||||
edge(x, y, x + w, y)
|
||||
edge(x + w, y, x + w, y + h)
|
||||
edge(x + w, y + h, x, y + h)
|
||||
edge(x, y + h, x, y)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
end
|
||||
|
||||
-- A white shine band that sweeps across an active button, clipped to its
|
||||
-- rounded shape. phase is 0..1 (left of the button -> right of it).
|
||||
local shineMesh
|
||||
local function buttonShine(x, y, w, h, r, phase)
|
||||
if not shineMesh then
|
||||
-- triangle strip: three columns (transparent, white, transparent)
|
||||
shineMesh = love.graphics.newMesh({
|
||||
{ 0, 0, 0, 0, 1, 1, 1, 0 },
|
||||
{ 0, 1, 0, 1, 1, 1, 1, 0 },
|
||||
{ 0.5, 0, 0.5, 0, 1, 1, 1, 0.5 },
|
||||
{ 0.5, 1, 0.5, 1, 1, 1, 1, 0.5 },
|
||||
{ 1, 0, 1, 0, 1, 1, 1, 0 },
|
||||
{ 1, 1, 1, 1, 1, 1, 1, 0 },
|
||||
}, "strip", "static")
|
||||
end
|
||||
local bandW = w * 0.6
|
||||
local bx = x - bandW + phase * (w + bandW)
|
||||
love.graphics.stencil(function()
|
||||
love.graphics.rectangle("fill", x, y, w, h, r, r)
|
||||
end, "replace", 1)
|
||||
love.graphics.setStencilTest("greater", 0)
|
||||
love.graphics.setBlendMode("add")
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(shineMesh, bx, y, 0, bandW, h)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
love.graphics.setStencilTest()
|
||||
end
|
||||
|
||||
function RomImporter:draw()
|
||||
local width, height = love.graphics.getDimensions()
|
||||
local third = width / 3
|
||||
col(PAL.bg)
|
||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
||||
local s = clamp(height / 768, 0.7, 1.6)
|
||||
local pulse = self.pulse
|
||||
|
||||
-- top tri-colour stripe (Red | Blue | Yellow)
|
||||
local stripeH = math.max(8, height * 0.02)
|
||||
col(PAL.red); love.graphics.rectangle("fill", 0, 0, third, stripeH)
|
||||
col(PAL.blue); love.graphics.rectangle("fill", third, 0, third, stripeH)
|
||||
col(PAL.gold); love.graphics.rectangle("fill", 2 * third, 0, width - 2 * third, stripeH)
|
||||
-- Hover state (desktop only -- touch has no cursor). anyHover and ptIn are
|
||||
-- captured by drawCard and the footer below; the cursor is set at the end.
|
||||
local mx, my = love.mouse.getPosition()
|
||||
local hoverEnabled = not self.android
|
||||
local anyHover = false
|
||||
local function ptIn(r)
|
||||
return r and mx >= r.x and mx <= r.x + r.width and my >= r.y and my <= r.y + r.height
|
||||
end
|
||||
|
||||
-- fonts, rebuilt only when the window size changes
|
||||
-- Fonts + size-dependent scenery, rebuilt only when the window size changes.
|
||||
local fontKey = ("%dx%d"):format(width, height)
|
||||
if self.fontKey ~= fontKey then
|
||||
self.fontKey = fontKey
|
||||
local function f(frac, lo, hi) return love.graphics.newFont(clamp(height * frac, lo, hi)) end
|
||||
self.pillFont = f(0.021, 12, 15)
|
||||
self.headFont = f(0.030, 16, 22)
|
||||
self.detailFont = f(0.023, 13, 16)
|
||||
self.buttonFont = f(0.029, 16, 22)
|
||||
self.hintFont = f(0.021, 12, 15)
|
||||
self.warningFont = f(0.017, 10, 12)
|
||||
local function f(px) return love.graphics.newFont(math.max(8, math.floor(px + 0.5))) end
|
||||
self.headFont = f(19 * s)
|
||||
self.detailFont = f(14 * s)
|
||||
self.buttonFont = f(19 * s)
|
||||
self.hintFont = f(13 * s)
|
||||
self.warningFont = f(11 * s)
|
||||
|
||||
-- Background: a radial gradient (bright navy at top-centre -> near black).
|
||||
-- A triangle fan from the top-centre gives the radial falloff; the screen
|
||||
-- is cleared to the outer colour first so the corners it does not reach
|
||||
-- match seamlessly.
|
||||
do
|
||||
local cx, cy = width / 2, 0
|
||||
local rx, ry = width * 1.3, height * 1.08
|
||||
local n = 72
|
||||
local verts = { { cx, cy, 0, 0,
|
||||
PAL.bgTop[1] / 255, PAL.bgTop[2] / 255, PAL.bgTop[3] / 255, 1 } }
|
||||
for i = 0, n do
|
||||
local a = (i / n) * math.pi * 2
|
||||
verts[#verts + 1] = { cx + math.cos(a) * rx, cy + math.sin(a) * ry, 0, 0,
|
||||
PAL.bgBot[1] / 255, PAL.bgBot[2] / 255, PAL.bgBot[3] / 255, 1 }
|
||||
end
|
||||
self.bgMesh = love.graphics.newMesh(verts, "fan", "static")
|
||||
end
|
||||
|
||||
-- CRT vignette: a gentle edge darkening, centred slightly above the middle.
|
||||
do
|
||||
local cx, cy = width / 2, height * 0.45
|
||||
local rx, ry = width * 0.78, height * 0.78
|
||||
local n = 72
|
||||
local verts = { { cx, cy, 0, 0, 0, 0, 0, 0 } }
|
||||
for i = 0, n do
|
||||
local a = (i / n) * math.pi * 2
|
||||
verts[#verts + 1] =
|
||||
{ cx + math.cos(a) * rx, cy + math.sin(a) * ry, 0, 0, 0, 0, 0, 0.32 }
|
||||
end
|
||||
self.vignetteMesh = love.graphics.newMesh(verts, "fan", "static")
|
||||
end
|
||||
|
||||
-- CRT scanlines: a 1px dark line every 3px, baked into a tiny tile and
|
||||
-- drawn once with a repeat-wrapped quad (one draw call, correct alpha).
|
||||
if not self.scanlineImage then
|
||||
local id = love.image.newImageData(1, 3)
|
||||
id:setPixel(0, 0, 0, 0, 0, 0.08)
|
||||
id:setPixel(0, 1, 0, 0, 0, 0)
|
||||
id:setPixel(0, 2, 0, 0, 0, 0)
|
||||
self.scanlineImage = love.graphics.newImage(id)
|
||||
self.scanlineImage:setWrap("repeat", "repeat")
|
||||
self.scanlineImage:setFilter("nearest", "nearest")
|
||||
end
|
||||
self.scanlineQuad = love.graphics.newQuad(0, 0, width, height, 1, 3)
|
||||
end
|
||||
|
||||
-- footer (Boi's Club Games logo + trust warning), pinned to the bottom and
|
||||
-- measured first so the columns know where they must stop
|
||||
-- Invert shader: the Boi's Club Games mark is dark ink; on this dark panel it
|
||||
-- is rendered white (the design's filter:invert(1)). Built lazily so a
|
||||
-- headless require never needs a GL context.
|
||||
self.invertShader = self.invertShader or love.graphics.newShader([[
|
||||
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
|
||||
vec4 p = Texel(tex, tc);
|
||||
return vec4((vec3(1.0) - p.rgb) * color.rgb, p.a * color.a);
|
||||
}
|
||||
]])
|
||||
|
||||
-- Shine shader: the same white sweep the active buttons get, but clipped to
|
||||
-- the logo's own shape (a soft band brightens the pixels it crosses; fully
|
||||
-- transparent pixels stay transparent).
|
||||
self.shineShader = self.shineShader or love.graphics.newShader([[
|
||||
extern number shinePos;
|
||||
extern number shineW;
|
||||
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
|
||||
vec4 p = Texel(tex, tc);
|
||||
float band = smoothstep(shineW, 0.0, abs(tc.x - shinePos));
|
||||
return vec4(p.rgb + band * 0.55, p.a) * color;
|
||||
}
|
||||
]])
|
||||
|
||||
-- background
|
||||
col(PAL.bgBot)
|
||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.bgMesh)
|
||||
|
||||
-- neon tri-segment top bar (Red | Blue | Yellow) with a soft downward bloom
|
||||
local barH = math.max(10, 16 * s)
|
||||
local segs = {
|
||||
{ PAL.red, 0, third },
|
||||
{ PAL.blue, third, third },
|
||||
{ PAL.gold, 2 * third, width - 2 * third },
|
||||
}
|
||||
love.graphics.setBlendMode("add")
|
||||
for _, seg in ipairs(segs) do
|
||||
fillGrad(seg[2], 0, seg[3], barH * 3.2, seg[1], seg[1], 0.30, 0.0)
|
||||
end
|
||||
love.graphics.setBlendMode("alpha")
|
||||
for _, seg in ipairs(segs) do
|
||||
col(seg[1]); love.graphics.rectangle("fill", seg[2], 0, seg[3], barH)
|
||||
end
|
||||
|
||||
-- Footer (Boi's Club Games logo + trust warning), measured first so the
|
||||
-- columns know where they must stop. Drawn near the end.
|
||||
local warningWidth = math.min(width - 32, 620)
|
||||
love.graphics.setFont(self.warningFont)
|
||||
local _, warningLines = self.warningFont:getWrap(TRUST_WARNING, warningWidth)
|
||||
local warningH = #warningLines * self.warningFont:getHeight()
|
||||
local warningY = height - warningH - 8
|
||||
local warningY = height - warningH - 10
|
||||
local bcgW, bcgH = self.bcg:getDimensions()
|
||||
local bcgScale = math.min(math.min(width - 48, 200) / bcgW, height * 0.075 / bcgH)
|
||||
local bcgScale = math.min(math.min(width - 48, 200) / bcgW, height * 0.07 / bcgH)
|
||||
local bcgDW, bcgDH = bcgW * bcgScale, bcgH * bcgScale
|
||||
local bcgX, bcgY = (width - bcgDW) / 2, warningY - bcgDH - 8
|
||||
self.bcgButton = { x = bcgX, y = bcgY, width = bcgDW, height = bcgDH }
|
||||
local footerTop = bcgY - 10
|
||||
local footerTop = bcgY - 12
|
||||
|
||||
-- faint per-column tints under the split
|
||||
col(PAL.red, 0.07); love.graphics.rectangle("fill", 0, stripeH, third, footerTop - stripeH)
|
||||
col(PAL.blue, 0.07); love.graphics.rectangle("fill", third, stripeH, third, footerTop - stripeH)
|
||||
col(PAL.gold, 0.08); love.graphics.rectangle("fill", 2 * third, stripeH, width - 2 * third, footerTop - stripeH)
|
||||
|
||||
-- logo (larger, no subtitle), centred over the split
|
||||
-- Logo metrics (drawn later with a bob so the cards never shift; layout uses
|
||||
-- the resting position).
|
||||
local logoW, logoH = self.logo:getDimensions()
|
||||
local logoScale = math.min(math.min(width - 40, 560) / logoW, height * 0.20 / logoH)
|
||||
local logoScale = math.min(math.min(width - 40, 440 * s) / logoW, height * 0.20 / logoH)
|
||||
local logoDW, logoDH = logoW * logoScale, logoH * logoScale
|
||||
local logoY = stripeH + height * 0.02
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.logo, (width - logoDW) / 2, logoY, 0, logoScale, logoScale)
|
||||
local logoY = barH + 20 * s
|
||||
|
||||
-- shared column metrics so the three columns line up exactly
|
||||
local m = {
|
||||
boxW = math.min(third - 32, 300),
|
||||
boxH = clamp(height * 0.22, 120, 190),
|
||||
pillH = self.pillFont:getHeight() + 10,
|
||||
buttonH = clamp(height * 0.072, 44, 56),
|
||||
gap1 = clamp(height * 0.028, 12, 22),
|
||||
gap2 = clamp(height * 0.030, 14, 24),
|
||||
gap3 = 10,
|
||||
hintH = self.hintFont:getHeight(),
|
||||
}
|
||||
local stackH = m.pillH + m.gap1 + m.boxH + m.gap2 + m.buttonH + m.gap3 + m.hintH
|
||||
local regionTop = logoY + logoDH + height * 0.03
|
||||
local top = regionTop + math.max(0, (footerTop - regionTop - stackH) / 2)
|
||||
-- shared card metrics so the three columns line up exactly
|
||||
local padX = 20 * s
|
||||
local padTop = 22 * s
|
||||
local padBot = 24 * s
|
||||
local cardW = math.min(third - 24 * s, 300 * s)
|
||||
local buttonH = 50 * s
|
||||
local gapHead = 8 * s
|
||||
local gapDetail = 18 * s
|
||||
local gapButton = 10 * s
|
||||
local hintH = self.hintFont:getHeight()
|
||||
|
||||
-- draw one column and return the button/link hit rects for the caller
|
||||
local function column(colX, spec)
|
||||
local cx = colX + third / 2
|
||||
local x = cx - m.boxW / 2
|
||||
local a = spec.alpha or 1
|
||||
local y = top
|
||||
|
||||
-- pill badge
|
||||
love.graphics.setFont(self.pillFont)
|
||||
local pillW = self.pillFont:getWidth(spec.label) + 28
|
||||
col(spec.color, a)
|
||||
love.graphics.setLineWidth(2)
|
||||
love.graphics.rectangle("line", cx - pillW / 2, y, pillW, m.pillH, m.pillH / 2, m.pillH / 2)
|
||||
love.graphics.printf(spec.label, cx - pillW / 2,
|
||||
y + (m.pillH - self.pillFont:getHeight()) / 2, pillW, "center")
|
||||
y = y + m.pillH + m.gap1
|
||||
|
||||
-- box
|
||||
col(PAL.box, a)
|
||||
love.graphics.rectangle("fill", x, y, m.boxW, m.boxH)
|
||||
love.graphics.setLineWidth(2)
|
||||
col(spec.color, a)
|
||||
if spec.dashed then
|
||||
dashedRect(x, y, m.boxW, m.boxH, 7, 5)
|
||||
else
|
||||
love.graphics.rectangle("line", x, y, m.boxW, m.boxH)
|
||||
end
|
||||
local pad = 14
|
||||
love.graphics.setFont(self.headFont)
|
||||
col(PAL.heading, a)
|
||||
love.graphics.printf(spec.heading, x + pad, y + pad, m.boxW - 2 * pad, "center")
|
||||
local _, headLines = self.headFont:getWrap(spec.heading, m.boxW - 2 * pad)
|
||||
local headBlock = math.max(1, #headLines) * self.headFont:getHeight()
|
||||
love.graphics.setFont(self.detailFont)
|
||||
col(PAL.detail, a)
|
||||
love.graphics.printf(spec.detail, x + pad, y + pad + headBlock + 8, m.boxW - 2 * pad, "center")
|
||||
if spec.progress then
|
||||
local barY = y + m.boxH - 20
|
||||
col(PAL.disabled, a)
|
||||
love.graphics.rectangle("fill", x + pad, barY, m.boxW - 2 * pad, 8)
|
||||
col(spec.color, a)
|
||||
love.graphics.rectangle("fill", x + pad, barY, (m.boxW - 2 * pad) * spec.progress, 8)
|
||||
end
|
||||
y = y + m.boxH + m.gap2
|
||||
|
||||
-- button
|
||||
local rect
|
||||
if spec.button then
|
||||
rect = { x = x, y = y, width = m.boxW, height = m.buttonH }
|
||||
col(spec.button.bg, a)
|
||||
love.graphics.rectangle("fill", x, y, m.boxW, m.buttonH)
|
||||
love.graphics.setFont(self.buttonFont)
|
||||
local label = spec.button.text
|
||||
if spec.button.play then
|
||||
-- filled play triangle + label, centred as a group
|
||||
local tw = self.buttonFont:getWidth(label)
|
||||
local tri = self.buttonFont:getHeight() * 0.55
|
||||
local groupW = tri + 12 + tw
|
||||
local gx = x + (m.boxW - groupW) / 2
|
||||
local gy = y + m.buttonH / 2
|
||||
col(spec.button.fg, a)
|
||||
love.graphics.polygon("fill", gx, gy - tri / 2, gx, gy + tri / 2, gx + tri * 0.9, gy)
|
||||
love.graphics.print(label, gx + tri + 12,
|
||||
y + (m.buttonH - self.buttonFont:getHeight()) / 2)
|
||||
else
|
||||
col(spec.button.fg, a)
|
||||
love.graphics.printf(label, x,
|
||||
y + (m.buttonH - self.buttonFont:getHeight()) / 2, m.boxW, "center")
|
||||
end
|
||||
end
|
||||
y = y + m.buttonH + m.gap3
|
||||
|
||||
-- hint line, or the "<rom> imported · re-import" link
|
||||
love.graphics.setFont(self.hintFont)
|
||||
local reimportRect
|
||||
if spec.reimport then
|
||||
local prefix = spec.romName .. " imported · "
|
||||
local link = "re-import"
|
||||
local pw = self.hintFont:getWidth(prefix)
|
||||
local lw = self.hintFont:getWidth(link)
|
||||
local startX = cx - (pw + lw) / 2
|
||||
col(PAL.detail, a)
|
||||
love.graphics.print(prefix, startX, y)
|
||||
love.graphics.print(link, startX + pw, y)
|
||||
love.graphics.setLineWidth(1)
|
||||
love.graphics.line(startX + pw, y + self.hintFont:getHeight(),
|
||||
startX + pw + lw, y + self.hintFont:getHeight())
|
||||
reimportRect = { x = startX + pw, y = y, width = lw, height = m.hintH + 2 }
|
||||
elseif spec.hint then
|
||||
col(PAL.detail, a)
|
||||
love.graphics.printf(spec.hint, x, y, m.boxW, "center")
|
||||
end
|
||||
|
||||
return rect, reimportRect
|
||||
-- natural height a card needs for its (wrapped) heading + detail
|
||||
local function contentH(spec)
|
||||
local innerW = cardW - 2 * padX
|
||||
local _, hl = self.headFont:getWrap(spec.heading, innerW)
|
||||
local _, dl = self.detailFont:getWrap(spec.detail, innerW)
|
||||
local headH = math.max(1, #hl) * self.headFont:getHeight()
|
||||
local detH = math.max(1, #dl) * self.detailFont:getHeight()
|
||||
return padTop + headH + gapHead + detH + gapDetail
|
||||
+ buttonH + gapButton + hintH + padBot
|
||||
end
|
||||
|
||||
-- Red column: live, driven by the import state machine
|
||||
-- Red column: live, driven by the import state machine.
|
||||
local redHint = self.android and "or copy the .gb via USB" or "or drop the .gb file here"
|
||||
local redSpec = { color = PAL.red, label = "RED", dashed = false, alpha = 1 }
|
||||
local redSpec = {
|
||||
accent = PAL.red, interior = PAL.cardRed,
|
||||
alpha = 1, glowScale = 1, period = 2.6,
|
||||
}
|
||||
if self.state == "ready" then
|
||||
redSpec.heading = "Red ROM ready"
|
||||
redSpec.detail = "Your ROM is verified. Press Play to start."
|
||||
redSpec.button = { text = "Play Red", bg = PAL.red, fg = PAL.white, play = true }
|
||||
redSpec.reimport = true
|
||||
redSpec.romName = self.romName
|
||||
redSpec.button = { kind = "play", text = "Play Red" }
|
||||
redSpec.link = { name = self.romName }
|
||||
elseif self.state == "working" or self.state == "complete" then
|
||||
redSpec.heading = self.status
|
||||
redSpec.detail = self.detail
|
||||
@@ -652,7 +729,7 @@ function RomImporter:draw()
|
||||
elseif self.state == "error" then
|
||||
redSpec.heading = "That ROM could not be imported"
|
||||
redSpec.detail = self.detail
|
||||
redSpec.button = { text = "Choose ROM", bg = PAL.red, fg = PAL.white }
|
||||
redSpec.button = { kind = "choose", text = "Choose ROM" }
|
||||
redSpec.hint = redHint
|
||||
else -- waiting
|
||||
if self.android then
|
||||
@@ -665,32 +742,230 @@ function RomImporter:draw()
|
||||
redSpec.heading = "Choose or drop a Red ROM"
|
||||
redSpec.detail = "The ROM is verified before any files are created."
|
||||
end
|
||||
redSpec.button = { text = "Choose ROM", bg = PAL.red, fg = PAL.white }
|
||||
redSpec.button = { kind = "choose", text = "Choose ROM" }
|
||||
redSpec.hint = redHint
|
||||
end
|
||||
self.redButton, self.reimportRect = column(0, redSpec)
|
||||
|
||||
-- Blue and Yellow columns: placeholders until those games are supported
|
||||
column(third, {
|
||||
color = PAL.blue, label = "BLUE", dashed = true, alpha = 0.72,
|
||||
-- Blue and Yellow columns: lit placeholders until those games are supported.
|
||||
local blueSpec = {
|
||||
accent = PAL.blue, interior = PAL.cardBlue,
|
||||
alpha = 0.92, glowScale = 0.5, period = 3.4,
|
||||
heading = "Choose or drop a Blue ROM", detail = "Blue support is on the way.",
|
||||
button = { text = "Coming soon", bg = PAL.disabled, fg = PAL.disabledInk },
|
||||
hint = "not yet available",
|
||||
})
|
||||
column(2 * third, {
|
||||
color = PAL.gold, label = "YELLOW", dashed = true, alpha = 0.72,
|
||||
button = { kind = "disabled", text = "Coming soon" }, hint = "not yet available",
|
||||
}
|
||||
local yellowSpec = {
|
||||
accent = PAL.gold, interior = PAL.cardGold,
|
||||
alpha = 0.92, glowScale = 0.5, period = 3.8,
|
||||
heading = "Choose or drop a Yellow ROM", detail = "Yellow support is on the way.",
|
||||
button = { text = "Coming soon", bg = PAL.disabled, fg = PAL.disabledInk },
|
||||
hint = "not yet available",
|
||||
})
|
||||
button = { kind = "disabled", text = "Coming soon" }, hint = "not yet available",
|
||||
}
|
||||
|
||||
-- footer
|
||||
local cardH = math.max(contentH(redSpec), contentH(blueSpec), contentH(yellowSpec))
|
||||
local regionTop = logoY + logoDH + 14 * s
|
||||
local top = regionTop + math.max(0, (footerTop - regionTop - cardH) / 2)
|
||||
|
||||
-- Draw one neon cartridge card and return its button/link hit rects.
|
||||
local function drawCard(colX, spec, ty)
|
||||
local cx = colX + third / 2
|
||||
local x = cx - cardW / 2
|
||||
local y = ty
|
||||
local a = spec.alpha or 1
|
||||
local r = 16 * s
|
||||
|
||||
-- pulsing neon halo
|
||||
local g = 0.5 + 0.5 * math.sin(pulse * 2 * math.pi / spec.period)
|
||||
neonGlow(x, y, cardW, cardH, r, spec.accent, (0.35 + 0.55 * g) * (spec.glowScale or 1))
|
||||
|
||||
-- card body: accent tint at the top fading into a dark interior, + border
|
||||
fillGradRounded(x, y, cardW, cardH, r, spec.accent, spec.interior, 0.16 * a, 0.55 * a)
|
||||
love.graphics.setLineWidth(math.max(1, 1.5 * s))
|
||||
col(spec.accent, 0.6 * a)
|
||||
love.graphics.rectangle("line", x, y, cardW, cardH, r, r)
|
||||
|
||||
local contentY = y + padTop
|
||||
|
||||
-- heading
|
||||
love.graphics.setFont(self.headFont)
|
||||
col(PAL.heading, a)
|
||||
printfB(spec.heading, x + padX, contentY, cardW - 2 * padX, "center")
|
||||
local _, hl = self.headFont:getWrap(spec.heading, cardW - 2 * padX)
|
||||
contentY = contentY + math.max(1, #hl) * self.headFont:getHeight() + gapHead
|
||||
|
||||
-- detail
|
||||
love.graphics.setFont(self.detailFont)
|
||||
col(PAL.detail, a)
|
||||
love.graphics.printf(spec.detail, x + padX, contentY, cardW - 2 * padX, "center")
|
||||
local _, dl = self.detailFont:getWrap(spec.detail, cardW - 2 * padX)
|
||||
contentY = contentY + math.max(1, #dl) * self.detailFont:getHeight() + gapDetail
|
||||
|
||||
-- button, or a neon progress bar while extracting
|
||||
local bx, bw, by = x + padX, cardW - 2 * padX, contentY
|
||||
local buttonRect
|
||||
if spec.progress ~= nil then
|
||||
local h2 = math.max(8, 10 * s)
|
||||
local track = by + (buttonH - h2) / 2
|
||||
col(PAL.bgBot, 0.85)
|
||||
love.graphics.rectangle("fill", bx, track, bw, h2, h2 / 2, h2 / 2)
|
||||
local pw = bw * clamp(spec.progress, 0, 1)
|
||||
if pw > h2 then
|
||||
neonGlow(bx, track, pw, h2, h2 / 2, spec.accent, 0.6)
|
||||
col(spec.accent, a)
|
||||
love.graphics.rectangle("fill", bx, track, pw, h2, h2 / 2, h2 / 2)
|
||||
end
|
||||
elseif spec.button then
|
||||
local br = 12 * s
|
||||
local kind = spec.button.kind
|
||||
buttonRect = { x = bx, y = by, width = bw, height = buttonH }
|
||||
if kind == "disabled" then
|
||||
col(PAL.disabled, 0.4 * a)
|
||||
love.graphics.rectangle("fill", bx, by, bw, buttonH, br, br)
|
||||
love.graphics.setLineWidth(1)
|
||||
col(spec.accent, 0.3 * a)
|
||||
love.graphics.rectangle("line", bx, by, bw, buttonH, br, br)
|
||||
love.graphics.setFont(self.buttonFont)
|
||||
col(PAL.disabledInk, a)
|
||||
printfB(spec.button.text, bx, by + (buttonH - self.buttonFont:getHeight()) / 2, bw, "center")
|
||||
buttonRect = nil -- placeholder columns are inert
|
||||
else
|
||||
local cTop, cBot, ink
|
||||
if kind == "play" then cTop, cBot, ink = PAL.playTop, PAL.playBot, PAL.playInk
|
||||
else cTop, cBot, ink = PAL.chooseTop, PAL.chooseBot, PAL.white end
|
||||
local hot = hoverEnabled and ptIn(buttonRect)
|
||||
if hot then anyHover = true end
|
||||
neonGlow(bx, by, bw, buttonH, br, cTop, (0.7 + 0.25 * g) * (hot and 1.7 or 1))
|
||||
fillGradRounded(bx, by, bw, buttonH, br, cTop, cBot, 1, 1)
|
||||
if hot then
|
||||
-- brighten the face on hover
|
||||
love.graphics.setBlendMode("add")
|
||||
love.graphics.setColor(1, 1, 1, 0.12)
|
||||
love.graphics.rectangle("fill", bx, by, bw, buttonH, br, br)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
end
|
||||
buttonShine(bx, by, bw, buttonH, br, (pulse % 2.8) / 2.8)
|
||||
love.graphics.setFont(self.buttonFont)
|
||||
if kind == "play" then
|
||||
-- filled play triangle + label, centred as a group
|
||||
local label = spec.button.text
|
||||
local tw = self.buttonFont:getWidth(label)
|
||||
local tri = self.buttonFont:getHeight() * 0.55
|
||||
local groupW = tri + 12 * s + tw
|
||||
local gx = bx + (bw - groupW) / 2
|
||||
local gy = by + buttonH / 2
|
||||
col(ink)
|
||||
love.graphics.polygon("fill", gx, gy - tri / 2, gx, gy + tri / 2, gx + tri * 0.9, gy)
|
||||
printB(label, gx + tri + 12 * s, by + (buttonH - self.buttonFont:getHeight()) / 2)
|
||||
else
|
||||
col(ink)
|
||||
printfB(spec.button.text, bx, by + (buttonH - self.buttonFont:getHeight()) / 2, bw, "center")
|
||||
end
|
||||
end
|
||||
end
|
||||
contentY = by + buttonH + gapButton
|
||||
|
||||
-- subline: a hint, or the "<rom> · re-import" link
|
||||
love.graphics.setFont(self.hintFont)
|
||||
local linkRect
|
||||
if spec.link then
|
||||
local prefix = spec.link.name .. " · "
|
||||
local link = "re-import"
|
||||
local pw = self.hintFont:getWidth(prefix)
|
||||
local lw = self.hintFont:getWidth(link)
|
||||
local startX = cx - (pw + lw) / 2
|
||||
col(PAL.detail, a)
|
||||
love.graphics.print(prefix, startX, contentY)
|
||||
love.graphics.print(link, startX + pw, contentY)
|
||||
love.graphics.setLineWidth(1)
|
||||
love.graphics.line(startX + pw, contentY + hintH, startX + pw + lw, contentY + hintH)
|
||||
linkRect = { x = startX + pw, y = contentY, width = lw, height = hintH + 3 }
|
||||
elseif spec.hint then
|
||||
col(PAL.detail, a)
|
||||
love.graphics.printf(spec.hint, x + padX, contentY, cardW - 2 * padX, "center")
|
||||
end
|
||||
|
||||
return buttonRect, linkRect
|
||||
end
|
||||
|
||||
self.redButton, self.reimportRect = drawCard(0, redSpec, top)
|
||||
drawCard(third, blueSpec, top)
|
||||
drawCard(2 * third, yellowSpec, top)
|
||||
|
||||
-- logo, centred over the split, with a gentle bob + gold glow
|
||||
local bob = math.sin(pulse * (2 * math.pi / 4)) * 6 * s
|
||||
local lx, ly = (width - logoDW) / 2, logoY + bob
|
||||
love.graphics.setBlendMode("add")
|
||||
love.graphics.setColor(1, 0.85, 0.2, 0.16 + 0.12 * (0.5 + 0.5 * math.sin(pulse * 1.6)))
|
||||
love.graphics.draw(self.logo, (width - logoDW * 1.05) / 2, ly - logoDH * 0.025, 0,
|
||||
logoScale * 1.05, logoScale * 1.05)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
-- main logo, with the same sweeping shine the active buttons get
|
||||
local shineW = 0.16
|
||||
self.shineShader:send("shinePos", -shineW + ((pulse % 2.8) / 2.8) * (1 + 2 * shineW))
|
||||
self.shineShader:send("shineW", shineW)
|
||||
love.graphics.setShader(self.shineShader)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.logo, lx, ly, 0, logoScale, logoScale)
|
||||
love.graphics.setShader()
|
||||
|
||||
-- footer: BCG mark (inverted to white, glowing brighter on hover) + warning
|
||||
local bcgHot = hoverEnabled and ptIn(self.bcgButton)
|
||||
if bcgHot then anyHover = true end
|
||||
love.graphics.setShader(self.invertShader)
|
||||
love.graphics.setBlendMode("add")
|
||||
love.graphics.setColor(1, 1, 1, bcgHot and 0.5 or 0.22)
|
||||
love.graphics.draw(self.bcg, bcgX - bcgDW * 0.02, bcgY - bcgDH * 0.02, 0,
|
||||
bcgScale * 1.04, bcgScale * 1.04)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.bcg, bcgX, bcgY, 0, bcgScale, bcgScale)
|
||||
col(PAL.detail)
|
||||
love.graphics.setShader()
|
||||
|
||||
-- trust warning; the bois.icu URL inside it is a real hover link
|
||||
love.graphics.setFont(self.warningFont)
|
||||
col(PAL.warning)
|
||||
love.graphics.printf(TRUST_WARNING, (width - warningWidth) / 2, warningY, warningWidth, "center")
|
||||
self.linkUrlRect = nil
|
||||
do
|
||||
local wrapX = (width - warningWidth) / 2
|
||||
local lh = self.warningFont:getHeight()
|
||||
local _, lines = self.warningFont:getWrap(TRUST_WARNING, warningWidth)
|
||||
for i, line in ipairs(lines) do
|
||||
local sidx = line:find(COMMUNITY_URL, 1, true)
|
||||
if sidx then
|
||||
-- the URL sits on a centred wrapped line: find its exact x-offset so the
|
||||
-- coloured link overdraws the plain-warning glyphs already printed there
|
||||
local before = line:sub(1, sidx - 1)
|
||||
local lineW = self.warningFont:getWidth(line)
|
||||
local ux = wrapX + (warningWidth - lineW) / 2 + self.warningFont:getWidth(before)
|
||||
local uy = warningY + (i - 1) * lh
|
||||
local uw = self.warningFont:getWidth(COMMUNITY_URL)
|
||||
self.linkUrlRect = { x = ux, y = uy, width = uw, height = lh }
|
||||
local linkHot = hoverEnabled and ptIn(self.linkUrlRect)
|
||||
if linkHot then anyHover = true end
|
||||
col(linkHot and PAL.linkHover or PAL.link)
|
||||
love.graphics.print(COMMUNITY_URL, ux, uy)
|
||||
love.graphics.setLineWidth(1)
|
||||
love.graphics.line(ux, uy + lh - 1, ux + uw, uy + lh - 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- CRT scanlines + vignette, over everything
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.scanlineImage, self.scanlineQuad, 0, 0)
|
||||
love.graphics.draw(self.vignetteMesh)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
-- pointer cursor over any interactive element (desktop only)
|
||||
if hoverEnabled and love.mouse.isCursorSupported and love.mouse.isCursorSupported() then
|
||||
if anyHover then
|
||||
self.handCursor = self.handCursor or love.mouse.getSystemCursor("hand")
|
||||
love.mouse.setCursor(self.handCursor)
|
||||
else
|
||||
self.arrowCursor = self.arrowCursor or love.mouse.getSystemCursor("arrow")
|
||||
love.mouse.setCursor(self.arrowCursor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function inside(r, x, y)
|
||||
@@ -699,7 +974,7 @@ end
|
||||
|
||||
function RomImporter:mousepressed(x, y, button)
|
||||
if button ~= 1 then return end
|
||||
if inside(self.bcgButton, x, y) then
|
||||
if inside(self.bcgButton, x, y) or inside(self.linkUrlRect, x, y) then
|
||||
love.system.openURL(COMMUNITY_URL)
|
||||
return
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user