diff --git a/assets/launcher/current_theme_vid.ogv b/assets/launcher/current_theme_vid.ogv new file mode 100644 index 00000000..cca829a8 Binary files /dev/null and b/assets/launcher/current_theme_vid.ogv differ diff --git a/src/import/LauncherSettings.lua b/src/import/LauncherSettings.lua index b0e4f338..08696287 100644 --- a/src/import/LauncherSettings.lua +++ b/src/import/LauncherSettings.lua @@ -797,6 +797,16 @@ function LauncherSettings.open(hooks, version) return true end, }, + { + label = Strings("Theme Video BG"), + value = function() + return opts.themeVideoBg == false and Strings("OFF") or Strings("ON") + end, + step = function() + opts.themeVideoBg = opts.themeVideoBg == false + return true + end, + }, { label = Strings("Splash Sound"), value = function() diff --git a/src/import/LauncherThemeVideo.lua b/src/import/LauncherThemeVideo.lua new file mode 100644 index 00000000..8eb9d1ad --- /dev/null +++ b/src/import/LauncherThemeVideo.lua @@ -0,0 +1,78 @@ +-- Quiet, black-keyed video layer behind the launcher UI. +local LauncherThemeVideo = {} +LauncherThemeVideo.__index = LauncherThemeVideo + +local VIDEO_PATH = "assets/launcher/current_theme_vid.ogv" + +function LauncherThemeVideo.new() + local self = setmetatable({}, LauncherThemeVideo) + local ok, err = pcall(function() + self.video = love.graphics.newVideo(VIDEO_PATH, { audio = false }) + self.video:setFilter("linear", "linear") + local width, height = self.video:getDimensions() + self.canvas = love.graphics.newCanvas(width, height) + self.canvas:setFilter("linear", "linear") + self.shader = love.graphics.newShader([[ + vec4 effect(vec4 color, Image texture, vec2 uv, vec2 screen) { + vec4 pixel = Texel(texture, uv); + float brightness = max(pixel.r, max(pixel.g, pixel.b)); + float coverage = smoothstep(0.012, 0.10, brightness); + float gray = dot(pixel.rgb, vec3(0.299, 0.587, 0.114)); + vec3 muted = mix(vec3(gray), pixel.rgb, 0.12); + return vec4(muted, pixel.a * coverage * 0.12) * color; + } + ]]) + self.video:play() + end) + if not ok then + self:release() + print("[launcher theme video] unavailable: " .. tostring(err)) + return nil + end + return self +end + +function LauncherThemeVideo:update() + -- LÖVE's Theora stream does not expose a reliable loop flag on every target. + -- Rewind after each pass so this stays portable across desktop and mobile. + if self.video and not self.video:isPlaying() then + self.video:rewind() + self.video:play() + end +end + +function LauncherThemeVideo:draw() + local g = love.graphics + local width, height = g.getDimensions() + local vw, vh = self.video:getDimensions() + + g.push("all") + g.origin() + g.setScissor() + g.setShader() + g.setBlendMode("alpha", "alphamultiply") + + -- LÖVE Video exposes YUV planes; convert through RGB before black-keying. + local target = g.getCanvas() + g.setCanvas(self.canvas) + g.clear(0, 0, 0, 0) + g.setColor(1, 1, 1, 1) + g.draw(self.video, 0, 0) + g.setCanvas(target) + + local scale = math.max(width / vw, height / vh) + g.setShader(self.shader) + g.draw(self.canvas, (width - vw * scale) / 2, + (height - vh * scale) / 2, 0, scale, scale) + g.pop() +end + +function LauncherThemeVideo:release() + if self.video then self.video:pause() end + for _, key in ipairs({ "video", "canvas", "shader" }) do + local resource = self[key] + if resource then resource:release(); self[key] = nil end + end +end + +return LauncherThemeVideo diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index 6439b214..934f8abe 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -123,6 +123,10 @@ function LauncherView.detach(imp) end Transition.reset() Kit.occlude(nil) + if imp and imp._themeVideo then + imp._themeVideo:release() + imp._themeVideo = nil + end if not imp or not imp._flex then return end imp._flex = nil if love.keyboard and love.keyboard.setKeyRepeat then @@ -175,6 +179,24 @@ end -- below is the other half of that guarantee. function LauncherView.update(imp, dt) if not imp._flex then return end + local settingsOptions = imp._settings and imp._settings.opts + if type(settingsOptions) == "table" then + imp._themeVideoOptions = settingsOptions + elseif not imp._themeVideoOptions then + local ok, options = pcall(require("src.core.SaveData").loadOptions) + imp._themeVideoOptions = ok and type(options) == "table" and options or {} + end + local videoEnabled = imp._themeVideoOptions.themeVideoBg ~= false + if videoEnabled and not imp._themeVideo then + local ok, player = pcall(function() + return require("src.import.LauncherThemeVideo").new() + end) + if ok then imp._themeVideo = player end + elseif not videoEnabled and imp._themeVideo then + imp._themeVideo:release() + imp._themeVideo = nil + end + if imp._themeVideo then imp._themeVideo:update(dt) end if imp._launchFade then return end local down = false @@ -6807,6 +6829,7 @@ function LauncherView.draw(imp) Toast.occlude(imp) Theme.field() + if imp._themeVideo then imp._themeVideo:draw() end -- Everything from here to buildModals sits UNDER any open modal, so the -- whole stage draws shielded (no clicks, no hover, no focus ring) while