-- YataiDON-HSS — the song-loading (rainbow) transition, ROUND 11. -- -- The engine (`src/objects/game/transition.cpp`) offers a scripted backdrop: a -- skin that defines `SongTransition` gets `update` / `draw_bg` / `draw_info` -- called instead of `Transition::draw_default()`. We take `update` + `draw_bg` -- only and leave `draw_info` to the engine, so the song title / subtitle stay -- the engine's own `OutlinedText` at their `skin_config` positions with the -- engine's own fades — the band under them is ours, drawn in `draw_bg`, which -- the engine calls first, so the z-order comes out right for free. -- -- ROUND 12 (text alignment). Those `skin_config` positions are now derived from the -- movie's own `DefineEditText` records through the arcade pen rule, not eyeballed: -- song_title_text ET10 box (0,311)-(1920,439) size 64 border 7 align 2 jp_64 -- song_sub_title_text ET12 box (0,396.99)-(1920,525.01) size 40 border 0 align 2 jp_64 -- pen.y = box.top + (box.h - lineHeight)/2 + border, lineHeight = 92 * size/64 -- -> title cell top 336, subtitle cell top 432.25; the kanji ink inside a jp_64 cell runs -- 14..72 of 97, so the cabinet's ink centres are **379.0** and **459.1**. Our -- OutlinedText centres its whole image and its kanji ink sits 47.9/1024 * size above -- that centre, so `transition_title` y = 379 + 3.0 + 1224 = 1606 and -- `transition_subtitle` y = 459.1 + 2.5 + 1224 = 1686 (the +1224 is `rainbow_up`, -- which `Transition::draw_song_info` subtracts). Measured in the framebuffer -- afterwards: title ink centre 379.0, subtitle 459.5. -- See Graphics/loading/MAPPING.md ROUND 12. -- -- WHAT THE ARCADE ACTUALLY DOES (the reference build `loading/loading_song.nulm`, root MC 58, -- 110 frames @ 60 fps, labels init 0 / in 5 / stop 55 / out 60 / end 109; the -- numbers are exported by scratchpad/anim_export.py into `anim/loading_song.lua`): -- -- depth 0/1 char 47 / 49 two 960x1080 CURVED half-rainbows. f5..f34 they -- slide in from off-screen (tx -240 -> 480 / 2160 -> 1440) while -- stretching horizontally (sx 0.5 -> 1.0). At f34 they have met -- but still leave a wedge of the screen behind them uncovered. -- f79..f109 is the exact reverse — they retract sideways. -- depth 2 char 51 the flat 7-stripe full-screen rainbow (7x 1920x160 -- slabs placed at (960,1000)). Present f35..f78 ONLY: it replaces -- the two curtains the frame after they meet, and that is what -- fills the wedge. Static while present. -- depth 3 char 53 an 8x160 gradient drawn 1920x512 at y 568..1080, -- **blend = add**, alpha 0 -> 77/256 over f35..f55, held to f60, -- back to 0 by f69. A warm glow along the bottom, NOT a dark -- strip (round 2 read it as one; round 4 deleted it because the -- arcade capture showed nothing dark — both wrong, it is additive). -- depth 4 sprite 3 ELEVEN copies of one 128x128 star (char 1) at fixed -- positions and scales. No motion, no sprite sheet: the whole -- group fades 0 -> 1 over f45..f55 and 1 -> 0 over f60..f69. -- depth 5/6 don / katsu 560x560. They fly in along an ARC from off-screen -- low (f35 (-166,738)/(2086,738) -> f55 (160,786)/(1760,786), the -- ty rising to 656 at f43), hold, dip 786 -> 806 over f60..f65, -- then fly straight up to y 294 and fade out by f79. -- depth 7 char 56 the 1600x256 title band at (960,400). f45..f55 it -- OPENS: sx 0.25 -> 1 with alpha 0 -> 1. f60..f69 it SQUASHES -- shut: sx 1 -> 1.5, sy 1 -> 0.1, alpha -> 0. -- depth 8+ song title / subtitle / ruby / tips: alpha 0 until f55, 1 at -- `stop`, 0 at `out`. The cabinet parks on f55 for the whole load. -- -- MAPPING ONTO THIS ENGINE. There is no "park" here: `song_select.cpp` shows -- the transition for exactly `song_info_fade` (delay 266 + duration 266 = 532 ms, -- global animation.json ids 0/3) and then `game.cpp` builds a second one -- (`is_second`) that runs another 532 ms while the chart loads. So: -- scene 1 (is_second == false): t 0..532 ms -> arcade frame 5 .. 55 -- scene 2 (is_second == true): t 0..532 ms -> arcade frame 60 ..109 -- Both joins are frames that fully cover the screen (f55 and f60 are both inside -- the full-rainbow window), so neither scene change can flash. -- -- The title band and the hint block are the exception: they are driven off the -- engine's own fade curves rather than off the arcade frame, because they have -- to appear and disappear in lockstep with the engine-drawn title. The arcade's -- band open is linear in both sx and alpha simultaneously, so re-timing it onto -- `song_info_fade` keeps its shape exactly and only changes its window. -- -- TEXTURE SLOTS. `build/generated/texture_ids_generated.h` gives this subset -- seven names and nothing else loads, so the new layers are packed into the two -- that `draw_default()` used for 8x8 blanks and that nothing reads once a skin -- scripts `SongTransition`. See Graphics/loading/MAPPING.md ROUND 11. -- rainbow_bg the flat rainbow, 1:1 with the screen -- rainbow_bg_top atlas: curtain L (0,0,960,1080) | curtain R (960,0,960,1080) -- rainbow_bg_bottom atlas: band (0,0,1600,256) / glow (0,288,1600,512) -- / sparkle (0,832,128,128) -- text_bg 8x8 transparent — suppresses the engine's own band draw 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 written into texture.json; every draw below passes a delta off -- them. The two tables must agree (scratchpad/r11ld/bake_r11.py writes the json). local REST_CHARA_L = { -120, 506 } local REST_CHARA_R = { 1480, 506 } local REST_HINT = { 0, 568 } -- sprite 3's eleven star placements, world coords (the sprite sits at (960,540) -- with an identity matrix, so its children's tx/ty already ARE screen pixels). 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 -- Fallbacks for every value the generated table supplies, so a missing or -- renamed `anim/loading_song.lua` degrades to the arcade's own end states -- instead of taking the loading screen down. 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. `loading/loading_song.nulm`: mc `enso_skip_on` (sprite 46) -- sits at stage (350, 106) and holds shape 43, whose local bounds are -- (-312,-52)..(312,52) -- a 624x104 pill, so its top-left is (38, 54). Its child -- `text_enso_skip` is a DefineEditText with box (-2,-2)..(598,78) placed at -- (52, 71), i.e. stage x 50..650 / y 69..149, centre-aligned at font size 30. 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 -- `put()` adds the texture's OWN texture.json origin to the coordinates it is -- given, so every caller passes that origin as `rest` to cancel it out (see -- REST_HINT). badge.png declares x=38,y=54 -- the arcade position -- so its rest -- is the same pair; passing ORIGIN instead drew it at (76,108). 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.load_texture("global/rainbow_transition/rainbow_bg") self.atlas_c = tex.load_texture("global/rainbow_transition/rainbow_bg_top") self.atlas_b = tex.load_texture("global/rainbow_transition/rainbow_bg_bottom") self.chara_l = tex.load_texture("global/rainbow_transition/chara_left") self.chara_r = tex.load_texture("global/rainbow_transition/chara_right") self.hint = tex.load_texture("global/rainbow_transition/chara_center") -- The hint block is baked in Japanese. Any other interface language draws the -- three lines as text (skin_config transition_hint_title / _line1 / _line2) at the -- rows measured off the texture, on the same fade. 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 (the reference build loading/loading_song.nulm, mc `enso_skip_on`) self.skip_badge = tex.load_texture("loading/skip_badge/badge") -- NOT latched here: GameScreen::on_screen_start builds the transition -- (game.cpp:69) BEFORE it calls init_skip() (game.cpp:76), so at construction -- time tex.skip_enabled() is still reporting the PREVIOUS enso. Resolved on -- first draw instead -- see SongTransition:skip_armed(). self.skip_on = nil if text and text.create_raw_text then -- The cabinet does NOT bake this label: `text_enso_skip` is a Lumen -- DefineEditText whose HTML is --

演奏スキップON

-- black, size 30, centred, NO outline (its border field is 0.0). It has -- no language variants, so the cabinet shows the Japanese string in every -- locale and so do we. Built HERE and not in skip_armed(): the glyph -- atlas needs a frame to upload, so creating it on the first drawn frame -- left the first frames showing an empty pill. 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, once, on the first frame that actually draws: by then -- init_skip() has run and tex.skip_enabled() is true iff the feature ARMED for -- this enso (skin row on + a player has it on + a free drum, so false in 2P). 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 function SongTransition:update(current_ms, rainbow_up, song_info_fade) if not self.t0 then self.t0 = current_ms end local t = clamp(current_ms - self.t0, 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) -- the engine hands us `song_info_fade`; use it so the band can never -- drift from the title the engine draws on top of it self.info = clamp(song_info_fade or ((t - FADE_DELAY) / FADE_MS), 0, 1) end end -- Draw one texture into an absolute screen rect. `scale = 0` makes the engine's -- `x2[i] * scale + params.x2` collapse to exactly `params.x2`, which is the only -- way to get independent width and height out of `draw_texture`. 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 (char 53, 1920x512 at y568, alpha -> 77/256) 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 (560x560, registration = canvas centre) 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. Arcade open: sx 0.25 -> 1 and alpha 0 -> 1, both linear -- and simultaneous, re-timed onto the engine's own title fade. Arcade close: -- sx 1 -> 1.5, sy 1 -> 0.1, alpha -> 0, on the fade-out. 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 (「ゲームのヒント」 + 3 lines) on the same curve: the -- arcade turns every text field on at `stop` and off at `out`. if self.hint_title then -- texture rows (alpha > 200, outline included): title 165..202, line 1 228..257, -- line 2 282..310, all centred on x = 960; the texture sits at y = 568. 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 as the band. The arcade -- shows it ONLY on the song-loading screens (loading/loading_song, -- enso_ai/loading/loading_ai and the two collab AI loadings) -- there is no -- `enso_skip_on` anywhere in the song-select lumens, so nothing is drawn -- there. It is skipped in 2P for free: tex.skip_enabled() is false whenever -- the feature did not arm, which is exactly the arcade's greyed-row case. 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