-- Song-loading (rainbow) transition backdrop, sampled from the arcade timeline. local ok, anim = pcall(require, "anim/sampler") local CLIP = (ok and anim and anim.safe) and anim.safe("anim/loading_song") or nil SongTransition = {} SongTransition.__index = SongTransition local FADE_DELAY, FADE_MS, FADE_OUT_MS = 266.0, 266.0, 133.0 local SCENE_MS = FADE_DELAY + FADE_MS -- 532 ms: how long each scene shows local F_IN, F_STOP, F_OUT, F_END = 5, 55, 60, 109 local F_FULL_A, F_FULL_B = 35, 78 -- the full-rainbow window -- sub-rects inside the two atlases local SRC_CURT_L = { x = 0, y = 0, width = 960, height = 1080 } local SRC_CURT_R = { x = 960, y = 0, width = 960, height = 1080 } local SRC_BAND = { x = 0, y = 0, width = 1600, height = 256 } local SRC_GLOW = { x = 0, y = 288, width = 1600, height = 512 } local SRC_STAR = { x = 0, y = 832, width = 128, height = 128 } -- rest positions from texture.json; draws pass a delta off them local REST_CHARA_L = { -120, 506 } local REST_CHARA_R = { 1480, 506 } local REST_HINT = { 0, 568 } -- eleven star placements, already in screen px local STARS = { { 209.75, 542.55, 0.59998 }, { 92.35, 283.00, 0.47829 }, { 154.20, 330.75, 0.26784 }, { 292.35, 246.35, 0.44998 }, { 1259.00, 201.85, 0.78998 }, { 1845.70, 228.45, 0.37422 }, { 1791.70, 268.40, 0.24998 }, { 1388.50, 664.80, 0.44713 }, { 1821.20, 423.20, 0.56000 }, { 1472.00, 607.70, 0.25038 }, { 552.60, 628.95, 0.74998 }, } local function clamp(v, a, b) if v < a then return a elseif v > b then return b else return v end end -- read a track field, falling back to `default` when the table is gone local function track(name, f, field, default) if not CLIP then return default end local v = CLIP:get(name, f, field) if v == nil then return default end return v end -- 演奏スキップON badge (624x104 pill at (38,54)) local SKIP_BADGE_X, SKIP_BADGE_Y = 38, 54 local SKIP_BADGE_CX = 350 -- 38 + 624/2, the EditText's centre local SKIP_TEXT_CY = 109 -- 69 + 80/2, the EditText box's centre local SKIP_TEXT_SIZE = 30 -- badge.png declares its own (38,54) origin, so pass it back as rest to cancel local SKIP_REST = { 38, 54 } function SongTransition.new(title, subtitle, is_second) local self = setmetatable({}, SongTransition) self.is_second = is_second and true or false self.t0 = nil self.f = self.is_second and F_OUT or F_IN self.info = 0 -- band / hint opacity, 0..1 self.rainbow = tex.get_texture("global/rainbow_transition/rainbow_bg") self.atlas_c = tex.get_texture("global/rainbow_transition/rainbow_bg_top") self.atlas_b = tex.get_texture("global/rainbow_transition/rainbow_bg_bottom") self.chara_l = tex.get_texture("global/rainbow_transition/chara_left") self.chara_r = tex.get_texture("global/rainbow_transition/chara_right") self.hint = tex.get_texture("global/rainbow_transition/chara_center") -- the hint block is baked in Japanese; other languages draw it as text local lang = tex.language and tex.language() or "ja" if lang ~= "ja" and text and text.create_text then local white, black = { 255, 255, 255, 255 }, { 0, 0, 0, 255 } self.hint_title = text.create_text("transition_hint_title", white, black, false, 4.0, 2.0) self.hint_line1 = text.create_text("transition_hint_line1", white, black, false, 3.0, 2.0) self.hint_line2 = text.create_text("transition_hint_line2", white, black, false, 3.0, 2.0) end -- 演奏スキップON badge self.skip_badge = tex.get_texture("loading/skip_badge/badge") -- resolved on first draw (skip state isn't ready at construction) self.skip_on = nil if text and text.create_raw_text then -- built here (not on first draw) so the glyph atlas is ready self.skip_text = text.create_raw_text(tex.skin_text("skip_badge_text"), SKIP_TEXT_SIZE, { 0, 0, 0, 255 }, { 0, 0, 0, 0 }, false, 0, 2) end return self end -- resolved lazily on first draw, once init_skip() has run function SongTransition:skip_armed() if self.skip_on == nil then self.skip_on = (tex.skip_enabled ~= nil) and tex.skip_enabled() or false end return self.skip_on end -- timeline the engine reads (rainbow rise for the title, title fades, done) local RISE_MS, RISE_DIST = 266.0, 1224 -- 816 at 720p, scaled to 1080p function SongTransition:state() local t = self.t or 0 local rise = RISE_DIST * clamp(t / RISE_MS, 0, 1) ^ 3 if self.is_second then return { rainbow_up = rise, fade_out = clamp(1.0 - t / FADE_OUT_MS, 0, 1), finished = t >= FADE_OUT_MS } end return { rainbow_up = rise, fade = clamp((t - FADE_DELAY) / FADE_MS, 0, 1), finished = t >= SCENE_MS } end function SongTransition:start() self.t0 = nil end function SongTransition:update(current_ms, rainbow_up, song_info_fade) if not self.t0 then self.t0 = current_ms end self.t = current_ms - self.t0 local t = clamp(self.t, 0, SCENE_MS) if self.is_second then self.f = F_OUT + (F_END - F_OUT) * (t / SCENE_MS) self.info = clamp(1.0 - t / FADE_OUT_MS, 0, 1) else self.f = F_IN + (F_STOP - F_IN) * (t / SCENE_MS) -- use the engine's fade so the band tracks the title it draws on top self.info = clamp(song_info_fade or ((t - FADE_DELAY) / FADE_MS), 0, 1) end end -- draw a texture into an absolute screen rect (scale=0 -> exact x2/y2) local function put(id, rest, x, y, w, h, a, src, blend) if not id then return end tex.draw_texture(id, { x = x - rest[1], y = y - rest[2], scale = 0, x2 = w, y2 = h, fade = a, src = src, blend = blend, }) end local ORIGIN = { 0, 0 } function SongTransition:draw_bg(total_offset, rainbow_up, mini_up, chara_down) local f = self.f -- 1. backdrop: two curved curtains, or the flat full-screen rainbow if f >= F_FULL_A and f <= F_FULL_B then put(self.rainbow, ORIGIN, 0, 0, 1920, 1080, 1) else local lx = track("#47@0", f, "tx", -240) local ls = track("#47@0", f, "sx", 0.5) local rx = track("#49@1", f, "tx", 2160) local rs = track("#49@1", f, "sx", 0.5) put(self.atlas_c, ORIGIN, lx - 480 * ls, 0, 960 * ls, 1080, 1, SRC_CURT_L) put(self.atlas_c, ORIGIN, rx - 480 * rs, 0, 960 * rs, 1080, 1, SRC_CURT_R) end -- 2. additive glow along the bottom local ga = track("#55@3", f, "a", 0) if ga > 0.002 then put(self.atlas_b, ORIGIN, 0, 568, 1920, 512, ga, SRC_GLOW, "additive") end -- 3. the eleven sparkles — fixed placements, one group alpha local sa = track("#3@4", f, "a", 0) if sa > 0.002 then for i = 1, #STARS do local s = STARS[i] local d = 128 * s[3] put(self.atlas_b, ORIGIN, s[1] - d * 0.5, s[2] - d * 0.5, d, d, sa, SRC_STAR) end end -- 4. don / katsu on their arc local da = track("don", f, "a", 1) if da > 0.002 then put(self.chara_l, REST_CHARA_L, track("don", f, "tx", 160) - 280, track("don", f, "ty", 786) - 280, 560, 560, da) end local ka = track("katsu", f, "a", 1) if ka > 0.002 then put(self.chara_r, REST_CHARA_R, track("katsu", f, "tx", 1760) - 280, track("katsu", f, "ty", 786) - 280, 560, 560, ka) end -- 5. the title band (opens/closes on the engine's title fade) local info = self.info if info > 0.002 then local bw, bh if self.is_second then bw = 1600 * (1.5 - 0.5 * info) bh = 256 * (0.1 + 0.9 * info) else bw = 1600 * (0.25 + 0.75 * info) bh = 256 end put(self.atlas_b, ORIGIN, 960 - bw * 0.5, 400 - bh * 0.5, bw, bh, info, SRC_BAND) -- 6. the hint block on the same curve if self.hint_title then -- texture rows, all centred on x = 960 local function centred(t, y) if t then t:draw({ x = math.floor(960 - t.width / 2 + 0.5), y = y, fade = info }) end end centred(self.hint_title, 568 + 165) centred(self.hint_line1, 568 + 228) centred(self.hint_line2, 568 + 282) else put(self.hint, REST_HINT, 0, 568, 1920, 512, info) end end -- 7. 演奏スキップON badge, top-left, on the same fade if self:skip_armed() and self.skip_badge then put(self.skip_badge, SKIP_REST, SKIP_BADGE_X, SKIP_BADGE_Y, 624, 104, info) if self.skip_text then self.skip_text:draw({ x = SKIP_BADGE_CX - self.skip_text.width / 2, y = SKIP_TEXT_CY - self.skip_text.height / 2, fade = info, }) end end end