577 lines
20 KiB
Lua
577 lines
20 KiB
Lua
local logger = require("utils/logger")
|
||
local lovehandler = require("utils/love_file_handler")
|
||
local zip = require("utils/zip")
|
||
|
||
local multi, thread = require("multi"):init({priority=true})
|
||
multi.setClock(require("socket").gettime) -- When on linux os.clock doesn't reture actual seconds the program has elapsed for
|
||
local GLOBAL, THREAD = require("multi.integration.loveManager"):init()
|
||
|
||
local fileHandler, logCtrl = lovehandler.new("logs/jeopardy.log", {
|
||
max_size = 512 * 1024, -- rotate at 512 KB
|
||
max_files = 3,
|
||
buffered = false, -- write every line immediately (safest for games)
|
||
})
|
||
|
||
lovehandler.auto_flush_on_quit(logCtrl)
|
||
|
||
log = logger.new("jeopardy", {
|
||
level = logger.LEVELS.INFO,
|
||
handlers = {
|
||
logger.handlers.ConsoleHandler({ colours = true }),
|
||
fileHandler,
|
||
},
|
||
})
|
||
|
||
local gui = require("gui")
|
||
local color = require("gui.core.color")
|
||
|
||
color.indexColor("bg","#2b348b")
|
||
color.indexColor("tile_bg","#5459e4")
|
||
color.indexColor("header_tile","#2a2e91")
|
||
color.indexColor("status_font", "#e4b82a")
|
||
color.indexColor("title_font","#b16d24")
|
||
color.indexColor("question_bg","#363ac8")
|
||
color.indexColor("tile_font","#ffffff")
|
||
color.indexColor("header_font","#fef6eeda")
|
||
color.indexColor("correct", "#52b11b")
|
||
color.indexColor("skip", "#5d5d5d")
|
||
color.indexColor("wrong", "#bd2626")
|
||
color.indexColor("shaderColA","rgb01(0.10, 0.12, 0.38)")
|
||
color.indexColor("shaderColB","rgb01(0.20, 0.22, 0.58)")
|
||
color.indexColor("shaderColC","rgb01(0.55, 0.58, 0.78)")
|
||
color.indexColor("scoreboard_panel_bg", "#1a1a2e")
|
||
color.indexColor("scoreboard_header_bg", "#16213e")
|
||
color.indexColor("scoreboard_accent_color", "#e94560")
|
||
color.indexColor("scoreboard_row_top_bg", "#1c2641")
|
||
color.indexColor("scoreboard_row_norm_bg", "#121226")
|
||
color.indexColor("scoreboard_border_top_color", "#323c6e")
|
||
color.indexColor("scoreboard_border_nrm_color", "#1c1c37")
|
||
color.indexColor("scoreboard_bar_empty_color", "#232341")
|
||
color.indexColor("scoreboard_text_muted_color", "#786e96")
|
||
color.indexColor("scoreboard_pure_white", "#ffffff")
|
||
color.indexColor("scoreboard_precious_gold", "#ffd700")
|
||
color.indexColor("scoreboard_precious_silver", "#C0C0C0")
|
||
color.indexColor("scoreboard_precious_bronze", "#cd7f32")
|
||
color.indexColor("timer_start","#12a217")
|
||
color.indexColor("timer_font","#000000")
|
||
color.indexColor("timer_warn","#ffff14")
|
||
color.indexColor("timer_end","#e50000")
|
||
color.indexColor("question_font","rgb(255,255,255)")
|
||
|
||
local title = require("menus.title")
|
||
local menu = require("gui.core.menus")
|
||
|
||
require("gui.addons")
|
||
|
||
gui:setHotKey({"f11"})(function()
|
||
fullscreen, fstype = love.window.getFullscreen()
|
||
love.window.setFullscreen(not fullscreen, "desktop")
|
||
end)
|
||
|
||
local loadLog = log:child("load")
|
||
local old = multi.error
|
||
|
||
-- multi.error = function(self, err)
|
||
-- loadLog:error("Error: %s", err)
|
||
-- loadLog:error(debug.traceback())
|
||
-- love.quit()
|
||
-- end
|
||
|
||
function readFileData(path)
|
||
return io.open(path,"r"):read("*a")
|
||
end
|
||
|
||
local picker = THREAD:newFunction(function()
|
||
local filepicker = require("utils/filepicker")
|
||
local path, err = filepicker.open_file{ title = "Select a file" }
|
||
if path then
|
||
return path
|
||
else
|
||
return nil, "No file selected." .. (err and (" (" .. err .. ")") or "")
|
||
end
|
||
end, true)
|
||
local nof = gui.noOf
|
||
function gui:newNumberPicker(x, y, w, h, sx, sy, sw, sh, opts)
|
||
opts = opts or {}
|
||
opts.value = opts.value or 0
|
||
opts.inc = opts.inc or 1
|
||
opts.decimals = opts.decimals or 0 -- 0 = integers only
|
||
-- opts.min / opts.max / opts.onChange are optional, leave nil for "no limit"
|
||
|
||
local picker = self:newFrame(x, y, w, h, sx, sy, sw, sh)
|
||
picker:setRoundness(6, 6)
|
||
picker.active = false
|
||
|
||
|
||
local leftF = picker:newFrame(0, 0, 0, 0, 0, 0, .15, 1)
|
||
local left = leftF:newTextButton("-", 0, 0, 0, 0, 0, 0, 1, 1)
|
||
local rightF = picker:newFrame(0, 0, 0, 0, .85, 0, .15, 1)
|
||
local right = rightF:newTextButton("+", 0, 0, 0, 0, 0, 0, 1, 1)
|
||
local textbox = picker:newTextBox("", 0, 0, 0, 0, .15, 0, .7, 1)
|
||
|
||
textbox.blink = false
|
||
|
||
gui.apply({
|
||
scaleFont = {.45},
|
||
centerFont = {},
|
||
run = {function(self)
|
||
if self.text == "-" or self.text == "+" then
|
||
self:centerX(true)
|
||
self:centerY(true) -- also center vertically, not just horizontally
|
||
end
|
||
end},
|
||
align = gui.ALIGN_CENTER,
|
||
}, textbox, left, right)
|
||
|
||
gui.apply({
|
||
visibility = 0,
|
||
drawBorder = false, -- flat, no boxy outline around the arrows
|
||
}, left, right, leftF, rightF)
|
||
|
||
-- ---- value handling ----------------------------------------------
|
||
|
||
local function clamp(n)
|
||
if opts.min and n < opts.min then n = opts.min end
|
||
if opts.max and n > opts.max then n = opts.max end
|
||
return n
|
||
end
|
||
|
||
local function round(n)
|
||
if opts.decimals > 0 then
|
||
local mult = 10 ^ opts.decimals
|
||
return math.floor(n * mult + 0.5) / mult
|
||
end
|
||
return math.floor(n + 0.5)
|
||
end
|
||
|
||
local function format(n)
|
||
if opts.decimals > 0 then
|
||
return string.format("%." .. opts.decimals .. "f", n)
|
||
end
|
||
return tostring(math.floor(n))
|
||
end
|
||
|
||
local function updateButtons()
|
||
local canDec = (opts.min == nil) or (opts.value > opts.min)
|
||
local canInc = (opts.max == nil) or (opts.value < opts.max)
|
||
left.active, right.active = canDec, canInc
|
||
left.textColor = canDec and {0, 0, 0, 1} or {.6, .6, .6, 1}
|
||
right.textColor = canInc and {0, 0, 0, 1} or {.6, .6, .6, 1}
|
||
end
|
||
|
||
function picker:setValue(n, silent)
|
||
n = round(clamp(n))
|
||
opts.value = n
|
||
textbox.text = format(n)
|
||
textbox.cur_pos = #textbox.text
|
||
updateButtons()
|
||
if not silent and opts.onChange then opts.onChange(n) end
|
||
end
|
||
|
||
function picker:getValue()
|
||
return opts.value
|
||
end
|
||
|
||
picker:setValue(opts.value, true)
|
||
|
||
left:OnReleased(function() picker:setValue(opts.value - opts.inc) end)
|
||
right:OnReleased(function() picker:setValue(opts.value + opts.inc) end)
|
||
|
||
textbox:OnWheelMoved(function(_,dx, dy)
|
||
if dy > 0 then picker:setValue(opts.value + opts.inc)
|
||
elseif dy < 0 then picker:setValue(opts.value - opts.inc) end
|
||
end)
|
||
|
||
-- ---- restrict typing to numbers only -------------------------------
|
||
|
||
local allowNegative = opts.min == nil or opts.min < 0
|
||
local allowDecimal = opts.decimals > 0
|
||
|
||
local function sanitize(s)
|
||
local out, seenDot = {}, false
|
||
for i = 1, #s do
|
||
local ch = s:sub(i, i)
|
||
if ch:match("%d") then
|
||
out[#out + 1] = ch
|
||
elseif ch == "-" and i == 1 and allowNegative then
|
||
out[#out + 1] = ch
|
||
elseif ch == "." and allowDecimal and not seenDot then
|
||
out[#out + 1] = ch
|
||
seenDot = true
|
||
end
|
||
end
|
||
return table.concat(out)
|
||
end
|
||
|
||
local tiRef = gui.Events.OnTextInputed(function(input)
|
||
if gui:getObjectFocus() ~= textbox then return end
|
||
local clean = sanitize(textbox.text)
|
||
if clean ~= textbox.text then
|
||
textbox.text = clean
|
||
textbox.cur_pos = math.min(textbox.cur_pos, #clean)
|
||
end
|
||
end)
|
||
|
||
local kpRef = gui.Events.OnKeyPressed(function(key)
|
||
if gui:getObjectFocus() ~= textbox then return end
|
||
if key == "up" then picker:setValue(opts.value + opts.inc)
|
||
elseif key == "down" then picker:setValue(opts.value - opts.inc) end
|
||
end)
|
||
|
||
-- re-normalize (round/clamp/reformat) whatever was typed once they
|
||
-- hit enter or click away, so "3." or "-" don't linger as invalid text
|
||
local function commit()
|
||
picker:setValue(tonumber(textbox.text) or opts.value)
|
||
end
|
||
textbox:OnReturn(commit)
|
||
textbox:OnPressedOuter(commit)
|
||
|
||
textbox:OnDestroy(function()
|
||
gui.Events.OnTextInputed:Unconnect(tiRef)
|
||
gui.Events.OnKeyPressed:Unconnect(kpRef)
|
||
end)
|
||
|
||
return picker
|
||
end
|
||
|
||
-- ---- HSV <-> RGB helpers ----
|
||
local function hsvToRgb(h, s, v)
|
||
local c = v * s
|
||
local x = c * (1 - math.abs((h / 60) % 2 - 1))
|
||
local m = v - c
|
||
local r, g, b
|
||
if h < 60 then r, g, b = c, x, 0
|
||
elseif h < 120 then r, g, b = x, c, 0
|
||
elseif h < 180 then r, g, b = 0, c, x
|
||
elseif h < 240 then r, g, b = 0, x, c
|
||
elseif h < 300 then r, g, b = x, 0, c
|
||
else r, g, b = c, 0, x end
|
||
return r + m, g + m, b + m
|
||
end
|
||
|
||
local function rgbToHsv(r, g, b)
|
||
local max, min = math.max(r, g, b), math.min(r, g, b)
|
||
local d = max - min
|
||
local h = 0
|
||
if d ~= 0 then
|
||
if max == r then h = ((g - b) / d) % 6
|
||
elseif max == g then h = (b - r) / d + 2
|
||
else h = (r - g) / d + 4 end
|
||
h = h * 60
|
||
end
|
||
local s = (max == 0) and 0 or (d / max)
|
||
return h, s, max
|
||
end
|
||
|
||
local function clampNum(n, lo, hi) return math.max(lo, math.min(hi, n)) end
|
||
|
||
local function newCornerGradient(tl, tr, br, bl)
|
||
return love.graphics.newMesh({
|
||
{0, 0, 0, 0, tl[1], tl[2], tl[3], tl[4]},
|
||
{1, 0, 0, 0, tr[1], tr[2], tr[3], tr[4]},
|
||
{1, 1, 0, 0, br[1], br[2], br[3], br[4]},
|
||
{0, 1, 0, 0, bl[1], bl[2], bl[3], bl[4]},
|
||
}, "fan")
|
||
end
|
||
|
||
local _whiteFade, _blackFade
|
||
local function getFadeImages()
|
||
if not _whiteFade then
|
||
-- opaque white on the left fading to transparent on the right
|
||
_whiteFade = newCornerGradient(
|
||
{1,1,1,1}, {1,1,1,0}, {1,1,1,0}, {1,1,1,1}
|
||
)
|
||
-- transparent at top fading to opaque black at bottom
|
||
_blackFade = newCornerGradient(
|
||
{0,0,0,0}, {0,0,0,0}, {0,0,0,1}, {0,0,0,1}
|
||
)
|
||
end
|
||
return _whiteFade, _blackFade
|
||
end
|
||
|
||
function gui:newColorPicker(x, y, w, h, sx, sy, sw, sh, opts)
|
||
opts = opts or {}
|
||
local r0 = (opts.r or 255) / 255
|
||
local g0 = (opts.g or 0) / 255
|
||
local b0 = (opts.b or 0) / 255
|
||
|
||
local picker = self:newFrame(x, y, w, h, sx, sy, sw, sh)
|
||
picker.active = false -- container only; don't compete for focus with its children
|
||
picker.drawBorder = false
|
||
picker.visibility = 0
|
||
|
||
local hue, sat, val = rgbToHsv(r0, g0, b0)
|
||
local hueR, hueG, hueB = hsvToRgb(hue, 1, 1)
|
||
|
||
-- ---- Saturation/Value square ----
|
||
local svFrame = picker:newFrame(6, 6, -12, -12, 0, 0, .55, 1)
|
||
svFrame.drawBorder = false
|
||
svFrame.visibility = 0
|
||
|
||
local whiteFade, blackFade = getFadeImages()
|
||
svFrame.post = function(self)
|
||
local sx2, sy2, sw2, sh2 = self:getAbsolutes()
|
||
love.graphics.setColor(hueR, hueG, hueB, 1)
|
||
love.graphics.rectangle("fill", sx2, sy2, sw2, sh2)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.draw(whiteFade, sx2, sy2, 0, sw2, sh2)
|
||
love.graphics.setColor(1, 1, 1, .95)
|
||
love.graphics.draw(blackFade, sx2, sy2, 0, sw2, sh2)
|
||
love.graphics.setColor(0, 0, 0, .4)
|
||
love.graphics.rectangle("line", sx2, sy2, sw2, sh2)
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
|
||
local svThumb = svFrame:newFrame(0, 0, 0, 0)
|
||
svThumb:makeCircle(0, 0, 6)
|
||
svThumb.color = {1, 1, 1}
|
||
svThumb.borderColor = {0, 0, 0}
|
||
svThumb.drawBorder = true
|
||
|
||
-- ---- Hue strip ----
|
||
local hueSlider = picker:newFrame(0, 6, 0, -12, .57, 0, .10, 1)
|
||
-- applyGradient's "horizontal"/"vertical" naming is backwards from what
|
||
-- you'd expect visually (it names how colors are laid out in the source
|
||
-- image, not the on-screen direction) -- if this renders sideways for
|
||
-- you, just flip the string to "vertical".
|
||
hueSlider:applyGradient("horizontal",
|
||
{1, 0, 0, 1}, {1, 1, 0, 1}, {0, 1, 0, 1},
|
||
{0, 1, 1, 1}, {0, 0, 1, 1}, {1, 0, 1, 1}, {1, 0, 0, 1})
|
||
hueSlider.drawBorder = false
|
||
|
||
local hueThumb = hueSlider:newFrame(0, 0, 0, 4, 0, 0, 1, 0)
|
||
hueThumb.color = {1, 1, 1}
|
||
hueThumb.borderColor = {0, 0, 0}
|
||
hueThumb.drawBorder = true
|
||
|
||
-- ---- Preview + numeric fields ----
|
||
local preview = picker:newFrame(6, 6, -12, -6, .70, 0, .30, .20)
|
||
preview:setRoundness(4, 4)
|
||
|
||
local rPicker, gPicker, bPicker
|
||
|
||
local function refresh(fireChange)
|
||
local r, g, b = hsvToRgb(hue, sat, val)
|
||
hueR, hueG, hueB = hsvToRgb(hue, 1, 1)
|
||
|
||
preview.color = {r, g, b}
|
||
|
||
local _, _, sw2, sh2 = svFrame:getAbsolutes()
|
||
svThumb:rawSetDualDim(sat * sw2 - 12, (1 - val) * sh2 - 6)
|
||
|
||
local _, _, _, hh2 = hueSlider:getAbsolutes()
|
||
hueThumb:rawSetDualDim(nil, (hue / 360) * hh2 - 2)
|
||
|
||
local r255 = math.floor(r * 255 + .5)
|
||
local g255 = math.floor(g * 255 + .5)
|
||
local b255 = math.floor(b * 255 + .5)
|
||
if rPicker then
|
||
rPicker:setValue(r255, true)
|
||
gPicker:setValue(g255, true)
|
||
bPicker:setValue(b255, true)
|
||
end
|
||
|
||
if fireChange ~= false and opts.onChange then
|
||
opts.onChange(r255, g255, b255)
|
||
end
|
||
end
|
||
|
||
local function onRGBEdited()
|
||
local r = rPicker:getValue() / 255
|
||
local g = gPicker:getValue() / 255
|
||
local b = bPicker:getValue() / 255
|
||
hue, sat, val = rgbToHsv(r, g, b)
|
||
refresh(true)
|
||
end
|
||
|
||
rPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .26, .30, .20,
|
||
{value = math.floor(r0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited})
|
||
gPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .52, .30, .20,
|
||
{value = math.floor(g0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited})
|
||
bPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .78, .30, .20,
|
||
{value = math.floor(b0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited})
|
||
|
||
gui.apply({
|
||
scaleFont={.1}
|
||
},rPicker,gPicker,bPicker)
|
||
|
||
-- ---- drag interaction (same press/move/release pattern the built-in
|
||
-- textbox uses for its own text-selection dragging) ----
|
||
local function setSV(mx, my)
|
||
local sx2, sy2, sw2, sh2 = svFrame:getAbsolutes()
|
||
local lx = clampNum(mx - sx2, 0, sw2)
|
||
local ly = clampNum(my - sy2, 0, sh2)
|
||
sat = sw2 > 0 and (lx / sw2) or 0
|
||
val = sh2 > 0 and (1 - ly / sh2) or 0
|
||
refresh(true)
|
||
end
|
||
|
||
local svDragging = false
|
||
svFrame:OnPressed(function(_, mx, my) svDragging = true; setSV(mx, my) end)
|
||
svFrame:OnMoved(function(_, mx, my) if svDragging then setSV(mx, my) end end)
|
||
svFrame:OnReleased(function() svDragging = false end)
|
||
svFrame:OnReleasedOuter(function() svDragging = false end)
|
||
|
||
local function setHue(mx, my)
|
||
local _, sy2, _, sh2 = hueSlider:getAbsolutes()
|
||
local ly = clampNum(my - sy2, 0, sh2)
|
||
hue = sh2 > 0 and (ly / sh2) * 360 or 0
|
||
refresh(true)
|
||
end
|
||
|
||
local hueDragging = false
|
||
hueSlider:OnPressed(function(_, mx, my) hueDragging = true; setHue(mx, my) end)
|
||
hueSlider:OnMoved(function(_, mx, my) if hueDragging then setHue(mx, my) end end)
|
||
hueSlider:OnReleased(function() hueDragging = false end)
|
||
hueSlider:OnReleasedOuter(function() hueDragging = false end)
|
||
|
||
-- ---- public API ----
|
||
function picker:getColor()
|
||
local r, g, b = hsvToRgb(hue, sat, val)
|
||
return math.floor(r * 255 + .5), math.floor(g * 255 + .5), math.floor(b * 255 + .5)
|
||
end
|
||
|
||
function picker:setColor(r255, g255, b255)
|
||
hue, sat, val = rgbToHsv(r255 / 255, g255 / 255, b255 / 255)
|
||
refresh(false)
|
||
end
|
||
|
||
refresh(false)
|
||
return picker
|
||
end
|
||
|
||
function love.load()
|
||
love.keyboard.setKeyRepeat(true)
|
||
-- local z = zip.new()
|
||
-- z:add("main.lua",readFileData("main.lua"))
|
||
-- z:add("package.bat",readFileData("package.bat"))
|
||
-- local data = z:build()
|
||
-- local file = io.open("test.zip","wb")
|
||
-- file:write(data)
|
||
-- file:close()
|
||
|
||
-- thread:newThread(function()
|
||
-- local file = picker()
|
||
-- print("File selected",file)
|
||
-- end).OnError(print)
|
||
|
||
-- local numpicker = gui:newNumberPicker(0, 0, 200, 40, .5, .5, 0, 0, {
|
||
-- value = 1, inc = 1, min = 1, max = 4,
|
||
-- onChange = function(v) print("value is now", v) end,
|
||
-- })
|
||
|
||
local frame = gui:newFrame(0, 0, 500, 250, .5, .5, 0, 0)
|
||
local colorpicker = frame:newColorPicker(0, 0, 0, 0, 0, 0, 1, 1, {
|
||
r = 255, g = 100, b = 50,
|
||
onChange = function(r, g, b) print("color:", r, g, b) end,
|
||
})
|
||
|
||
--[[ -- bootstrap
|
||
loadLog:info("Starting Jeopardy Game")
|
||
loadLog:debug("Setting game identity to jeopardy")
|
||
for i,v in ipairs(color.listColors(showAllColors)) do
|
||
loadLog:debug("Indexed color %s",v)
|
||
end
|
||
love.filesystem.setIdentity("jeopardy")
|
||
loadLog:trace("Creating save directory")
|
||
success = love.filesystem.createDirectory("")
|
||
if not success then
|
||
loadLog:fatal("Unable to create save directory")
|
||
love.quit()
|
||
end
|
||
loadLog:trace("Creating save directory")
|
||
gui:cacheImage({"assets/images/checked.png","assets/images/unchecked.png","assets/images/speaker.png"})
|
||
gui:setAspectSize(1920, 1080)
|
||
gui.aspect_ratio = true
|
||
loadLog:debug("Creating main background image")
|
||
local bg = gui:newImageLabel("assets/images/background.png")
|
||
bg:fullFrame()
|
||
|
||
loadLog:debug("Init menu module")
|
||
menu.init(bg)
|
||
loadLog:debug("Displaying title menu")
|
||
title:visible(true)
|
||
]]
|
||
end
|
||
|
||
function love.update(dt)
|
||
gui.update(dt)
|
||
multi:uManager(dt)
|
||
end
|
||
|
||
function love.draw()
|
||
gui.draw()
|
||
end
|
||
|
||
-- custom shaders
|
||
|
||
gui.NewShader("background", [[
|
||
extern float time;
|
||
extern vec2 size;
|
||
extern vec3 colA;
|
||
extern vec3 colB;
|
||
extern vec3 colC;
|
||
|
||
// Hash function (pseudo-random from 2D input)
|
||
float hash(vec2 p) {
|
||
p = fract(p * vec2(127.1, 311.7));
|
||
p += dot(p, p + 19.19);
|
||
return fract(p.x * p.y);
|
||
}
|
||
|
||
// Smooth value noise
|
||
float noise(vec2 p) {
|
||
vec2 i = floor(p);
|
||
vec2 f = fract(p);
|
||
// Quintic smoothstep
|
||
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
||
|
||
float a = hash(i);
|
||
float b = hash(i + vec2(1.0, 0.0));
|
||
float c = hash(i + vec2(0.0, 1.0));
|
||
float d = hash(i + vec2(1.0, 1.0));
|
||
|
||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||
}
|
||
|
||
// Fractional Brownian Motion – layers of noise at different scales
|
||
float fbm(vec2 p) {
|
||
float value = 0.0;
|
||
float amplitude = 0.5;
|
||
float frequency = 1.0;
|
||
for (int i = 0; i < 6; i++) {
|
||
value += amplitude * noise(p * frequency);
|
||
frequency *= 2.0;
|
||
amplitude *= 0.5;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
vec4 effect(vec4 color, Image tex, vec2 texCoord, vec2 screenCoord) {
|
||
// Normalized UV with aspect ratio correction
|
||
vec2 uv = screenCoord / size;
|
||
|
||
// Slow, asymmetric time offsets per axis for lava-lamp feel
|
||
float t1 = time * 0.008;
|
||
float t2 = time * 0.005;
|
||
|
||
// Domain-warped FBM: feed FBM output back into itself
|
||
// This creates the bulging, self-folding blob shapes.
|
||
vec2 q = vec2(
|
||
fbm(uv + vec2(0.0, 0.0) + t1),
|
||
fbm(uv + vec2(5.2, 1.3) + t2)
|
||
);
|
||
|
||
vec2 r = vec2(
|
||
fbm(uv + 4.0 * q + vec2(1.7, 9.2) + t1 * 0.7),
|
||
fbm(uv + 4.0 * q + vec2(8.3, 2.8) + t2 * 0.9)
|
||
);
|
||
|
||
float n = fbm(uv + 4.0 * r + t1 * 0.4);
|
||
|
||
vec3 col = mix(colA, colB, clamp(n * 2.0, 0.0, 1.0));
|
||
col = mix(col, colC, clamp(n * 2.0 - 0.6, 0.0, 1.0));
|
||
|
||
return vec4(col, 1.0);
|
||
}
|
||
]]) |