local anim = require("anim/sampler") local CLIP = anim.safe("anim/dani_intro") local M = {} local SCREEN_W, SCREEN_H = 1920, 1080 local DOOR_W = 1200 -- door panel local width (main__shape5) local KANJI = 400 -- each kanji cell is 400x400 (main__shape7/9/11/13) local SCRIM_SRC = { x = 1200, y = 800, width = 64, height = 64 } local CELLS = { { track = "#7@4", sx = 1200, sy = 0 }, -- 段 { track = "#9@5", sx = 1600, sy = 0 }, -- 位 { track = "#11@6", sx = 1200, sy = 400 }, -- 道 { track = "#13@7", sx = 1600, sy = 400 }, -- 場 } local CLOSE_POSE = 62 -- shut doors, scrim already eased to 0.5 local STAMP_FROM = 62 -- the arcade's scrim has just settled to 0.5 local STAMP_TO = 182 -- all four landed, before the fade local OPEN_FROM = 182 -- kanji fade (182-185), doors part (187-200), local OPEN_TO = 262 -- scrim clears (203), bg push-in + `out` fade local MS_PER_FRAME = 1000.0 / 60.0 local CLOSE_MS = 300.0 local STAMP_MS = (STAMP_TO - STAMP_FROM) * MS_PER_FRAME -- 120 frames = 2000 ms local OPEN_MS = (OPEN_TO - OPEN_FROM) * MS_PER_FRAME -- 80 frames = 1333 ms local DURATION_MS = 2300.0 local function duration_ms() return DURATION_MS end local FB_SCRIM = 0.5 -- #1@3's settled alpha (f62..193 in the arcade data) local function clamp01(v) return math.max(0.0, math.min(1.0, v)) end local function clip() return CLIP end local _id, _id_tried local function tex_id() if not _id_tried then _id_tried = true _id = tex.get_texture("song_select/dan_transition/background") end return _id end local _bg, _bg_tried local function bg_id() if not _bg_tried then _bg_tried = true _bg = tex.get_texture("dan_select/global/bg") end return _bg end local function fallback_door() return -240, 960, DOOR_W, 0, FB_SCRIM end local function fallback_kanji(i, shown) if not shown or shown <= 0 then return nil end return 1.0, 1.0 -- scale 1, alpha 1 — the landed pose, nothing finer is recoverable end local function fire_sfx() if M.sfx_done then return end if audio and audio.play_sound then audio.play_sound("dan_intro", "sound") end M.sfx_done = true end local function phases() local d = duration_ms() local move = CLOSE_MS + STAMP_MS local k = (d >= move) and 1.0 or (d / move) -- compress only if we must local c = CLOSE_MS * k local st = STAMP_MS * k return c / d, (c + st) / d end -- u (0..1 across our transition) -> arcade frame. local function frame_of(u) local close_u, stamp_u = phases() if u <= close_u then return CLOSE_POSE elseif u <= stamp_u then local v = (u - close_u) / (stamp_u - close_u) return STAMP_FROM + (STAMP_TO - STAMP_FROM) * v end return STAMP_TO -- parked: doors shut, kanji up end local function draw_frame(f, op) local id = tex_id() if not id then return false end local c = clip() op = (op == nil) and 1.0 or clamp01(op) if op <= 0 then return true end local btx, bty, bsx, _, ba = c:at("#3@0", f) if btx and ba and ba > 0 then local bid = bg_id() if bid then local w, h = SCREEN_W * (bsx or 1), SCREEN_H * (bsx or 1) tex.draw_texture(bid, { x = (btx or 960) - w * 0.5, y = (bty or 540) - h * 0.5, x2 = w - SCREEN_W, y2 = h - SCREEN_H, fade = ba * op, }) end end -- doors local lx, rx, dw, dtop, scrim local rtx, rty, rsx = c:at("#5@1", f) local ltx, _, lsx = c:at("#5@2", f) if rtx and ltx then dw = DOOR_W * math.abs(rsx) rx = rtx lx = ltx + DOOR_W * lsx dtop = rty - SCREEN_H * 0.5 scrim = c:get("#1@3", f, "a") or FB_SCRIM else lx, rx, dw, dtop, scrim = fallback_door() end -- left door = the same panel mirrored (a negative src width is raylib's flip) tex.draw_texture(id, { src = { x = 0, y = 0, width = -DOOR_W, height = SCREEN_H }, x = lx, y = dtop, x2 = dw, y2 = SCREEN_H, fade = op }) tex.draw_texture(id, { src = { x = 0, y = 0, width = DOOR_W, height = SCREEN_H }, x = rx, y = dtop, x2 = dw, y2 = SCREEN_H, fade = op }) -- the full-screen black at depth 3: above both doors, below all four kanji if scrim and scrim > 0 then tex.draw_texture(id, { src = SCRIM_SRC, x = 0, y = 0, x2 = SCREEN_W, y2 = SCREEN_H, fade = scrim * op }) end -- kanji for i = 1, #CELLS do local cell = CELLS[i] local cx, cy, s, a local tx, ty, sx, _, al = c:at(cell.track, f) -- glyph tracks start at their placed frame, so gate explicitly local first = c:has(cell.track) and c.tracks[cell.track][1][1] or nil if tx and first and f >= first then cx, cy, s, a = tx, ty, sx, al elseif not tx then local fs, fa = fallback_kanji(i, (f >= STAMP_TO) and 1 or 0) if fs then cx = (i == 1 or i == 3) and 455 or 1420 cy = (i <= 2) and 255 or 765 s, a = fs, fa end end if cx and a and a > 0 then local w = KANJI * s tex.draw_texture(id, { src = { x = cell.sx, y = cell.sy, width = KANJI, height = KANJI }, x = cx - w * 0.5, y = cy - w * 0.5, x2 = w, y2 = w, fade = a * op, }) end end return true end M.armed = false M.engine_u = nil M.has_draw_top = false M.t0 = nil M.armed_open = false M.open_t0 = nil M.sfx_done = false function M.arm(active) M.armed = active and true or false if not M.armed then M.t0 = nil -- let the fallback clock restart cleanly next time end end function M.set_progress(p) M.has_draw_top = true if p and p >= 0 then M.engine_u = p else M.engine_u = nil M.armed_open = false M.open_t0 = nil end end function M.progress() if M.has_draw_top then -- the engine supplies real progress every frame; nil/negative = not running if M.engine_u and M.engine_u >= 0 then return clamp01(M.engine_u) end return nil end -- fallback: no draw_top on this engine build, self-clock off arm() if not M.armed then return nil end local now = tex.get_current_ms() if not M.t0 then M.t0 = now end local d = duration_ms() local u = (now - M.t0) / d if u >= 1.0 then return nil end return u end local function off_screen() local scr = tex.current_screen and tex.current_screen() or nil if scr == nil or scr == "SONG_SELECT" or scr == "DAN_SELECT" then return false end M.engine_u = nil M.armed = false M.armed_open = false M.open_t0 = nil M.t0 = nil M.open_held = false return true end function M.draw() if off_screen() then return false end local u = M.progress() if not u then return false end fire_sfx(u) local close_u = phases() local f = frame_of(u) if not draw_frame(f, (u <= close_u) and (u / close_u) or nil) then return false end if not M.armed_open then M.armed_open = true M.open_t0 = nil end M._drawn_ms = tex.get_current_ms() return true end -- true iff M.draw() already painted this exact engine frame function M.drawn_this_frame() return M._drawn_ms ~= nil and M._drawn_ms == tex.get_current_ms() end local INTRO_FROM = 5 -- clip label `in` local REVEAL_F = OPEN_FROM -- 182 function M.draw_open() if off_screen() then __hss_dan_intro = nil; return false end local req = __hss_dan_intro if req ~= nil then __hss_dan_intro = nil if req == "full" and not M.armed_open then M.armed_open = true M.open_t0 = nil M.open_held = false M.open_from = INTRO_FROM fire_sfx() elseif req == "open" then M.open_from = OPEN_FROM end end if not M.armed_open then return false end local now = tex.get_current_ms() if not M.open_t0 then M.open_t0 = now end local from = M.open_from or OPEN_FROM local f = from + (now - M.open_t0) / MS_PER_FRAME local ready = (__hss_dan_ready ~= false) if not ready then if f > REVEAL_F then f = REVEAL_F M.open_held = true end elseif M.open_held then M.open_held = false M.open_t0 = now - (REVEAL_F - from) * MS_PER_FRAME f = REVEAL_F end M.open_f = f if f >= OPEN_TO then M.armed_open = false M.open_t0 = nil M.open_held = false M.open_from = nil M.sfx_done = false M.engine_u = nil M.t0 = nil return false end return draw_frame(f, nil) end return M