diff --git a/.gitignore b/.gitignore index 73bb17e..63a3a5a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ __pycache__/ Thumbs.db results/local-*.json prompt0.md + +# Agent instructions belong in the user-level DSH home, not in this repo. +AGENTS.md diff --git a/docs/dan.md b/docs/dan.md index 6a488d3..a7ccf0f 100644 --- a/docs/dan.md +++ b/docs/dan.md @@ -156,23 +156,70 @@ active ──四道全部通关──→ cleared(通过) **第一层是单题结算**,在每道题出成绩时弹出:用的就是普通计时练习那张结算皮(`public/dx-result.css` 的 `.dx-result-sheet`),显示达成率、此前最佳、评级、用时、判定计数、单题 rating,以及结算那一刻的 DX Rating 总分与增量。这段对比读的是 `practice_timers.settlement_json` 里冻结的那一份 —— 段位認定不另算一套分。 -**第二层是整轮结算页**,四道打完(或中途失败)后出现,版式照原作的段位認定结算(`Process/Course/CourseResultProcess`): - -- **奶油色纸盘**:橙红双圈描边,两侧各一条水引红绳(蝴蝶结 + 竖绳 + 流苏)。 -- **逐曲卡**:每道一张。左侧一枚八角的「可」印(没通关是「不可」,未开始是灰的);右侧是难度配色的横卡 —— 封面那一格放题号(我们抽的是 CF 题、没有曲绘,放题号比放装饰诚实),然后是 `STAGE 0n` / 难度 / 题目名,下面一行「达成率」+ 金色描边的大数字 + 判定(评级,或超时/中断),最右边是 `LV`、`DX分数`(该题算出的单题 rating)与这道题的限时。 -- **底部一行**:左边判定框写「合格」/「不合格」与通关题数;中间一颗心;右边「总达成率」与 `DX分数`。 -- **右下角**一个「好」按钮,旁边是「再来一轮 · 档位」。 +**第二层是整轮结算页**,四道打完(或中途失败)后出现,直接上原作的画面素材(`Process/Course/CourseResultProcess` 那一屏): + +段位認定结算页,虚拟演示数据 + +- **底图**:整屏就是原作那张 980×928 的成品图 —— `UI_DNM_Result_Base_01`(「段位認定」)与 `UI_DNM_Result_Base_03`(「ランダム段位認定」)。页头文字、格纹页眉、两侧水引(蝴蝶结 + 竖绳 + 流苏)、下半部色带与左下角那格白框**全部烘焙在底图里**,所以档位挑战与随机段位各用自己那一版,标题不再由页面画。 +- **底图上那几块白不补**:下半部留了三处**挖白的空面板** —— 量下来是「标签白格」`(503,663) 354×50`(中间被折页的斜边切开,上半段 `503–708`、下半段被切成两小段)与「DX 分数白牌」`(608,810) 360×80`(左端还有个空着的圆白圆头)。那是原作留给运行时文字的位置,运行时会被盖住。我们一开始照「露白像空框」的思路用 `scripts/patch-maimai-result-band.py` 把这几块续画填掉了 —— 那是错的:官方版式本来就要在这三处写字,填掉等于自己把位置抹了。现在补丁脚本已删、底图回到原始素材,标签与大数字就摆回这三格(见下「底部色带」)。 +- **组件按官方坐标拼**:底图之上每一件都放在原作预制体(`Process/Course/CourseResultProcess`、逐题行的 `UI_CMN_KOP_TrackData`)量出来的位置上,尺寸也照官方:逐题行 `x 267–843`、第 1 行 `y 152`、行距 `122`(576 宽 = 官方 640 × 0.9);行首的印是 `108×108`(官方原始尺寸,不是缩过的);达成率数字字高 `44`;行内的每一项(边框本体、曲绘槽、歌名条、白盒、底带、右下白牌、徽章)另外照官方贴图 `UI_CMN_RSL_KopMBase_*` 逐像素量了一遍,换算与数字写在 `public/dan.css` 的「逐题行」一节,`scripts/dan-settlement-e2e.mjs` 里逐条量它们(`边框本体照原版量出来的比例摆`、`歌名条占顶部一整行`、`DX 分数在卡片右下角`、`限时写进底带左侧`、`评级徽章放进右上那块紫场`),底部白框、白牌的坐标也是拿底图逐行数像素量出来的。 +- **逐题卡**:每道一张。左侧一枚原作印(`UI_DNM_Icon_Result_01` 可 / `_02` 不可,未抽到的变灰);右侧横卡用原作底板 `UI_DNM_Result_musicBase_01` 撑起来,上面再压一层**乐曲边框** —— 版式照官方 `UI_CMN_RSL_KopMBase_*` 逐像素量出来重画:外圈 **2px 主色环** + 一块平的主色场 + 底部**浅色带** + 左侧方曲绘槽 + **深蓝歌名条(占顶部一整行)** + 白色达成率盒(**右下角一道大 R 弧** —— 原版那块「右侧带缺口的面板」)+ **右下角白牌**(原版烘的是日文 `でらっくスコア`)。**换算**:导出的原版是 636×136 的精裁版(官方设计 640×140,左右上下各被紧裁掉 2px 透明边),我们的行内区 576×126 = 官方 ×0.9,所以 `行内坐标 = (原版坐标 + 2) × 0.9`;边框本体落在行内 `(3.6, 3.6)`、尺寸 `568.8×118.8`,边框内的组件再减去这 3.6。**颜色不烘死**:官方那五张(BSC/ADV/EXP/MST/MST_Re)把难度色和难度名都印在图里,套不上我们自己的 CF 分档,所以主色场取 `--tc1`、外圈环取 `--tc2`(`cfRatingColor(...).tone` → `TRACK_TONE`)—— **一块平色**,不做上下渐变;曲绘槽与底部浅色带取主色的**浅色版本**(`color-mix(in srgb, var(--tc1) 42% / 55%, #fff)`),原版那格是曲绘,换成纯白牌会像在卡上挖了个洞。 +- **信息分配(照 2026-10-10 的口径重排)**:**最上面歌名占一整行**(原版左上那块难度名牌删掉,见下);左侧曲绘槽里是完整题号(`1554C` 这种「场次 + 小问」)与这题在 CF 上的 rating;达成率大数字 + 官方彩色「%」字形排在**白盒里**,数字**右对齐**:满 100 的那串右端固定在同一个位置,不到 100 的少一位数就少在左边(`96.0000%` 的右端仍与 `100.8000%` 齐平);**DX 分数挪到卡片右下角那块白牌**(行内 `450.9, 97.2`、`119.7×23.4` 胶囊,原版 `でらっくスコア` 那一格);**限时写进底带的左侧**(原版那条浅色带是空的一块,行内从 `122.4` 起);逐题评级徽章放进**右上那块紫场**(白盒右端…边框内右沿 / 歌名条下沿…右下白牌上沿 = `119.5×68.4`,原版空着,白盒右下角那道大 R 弧就是给它让位的)。 + - 官方那一格难度名牌(烘的是 `MASTER 大師` 这类难度名)**整个删掉了**:同一张卡上题号、达成率、单题 rating 已经把信息说全,再写一遍档位名既重复、又占掉半个标题条。歌名条因此从行内 `x 122.4` 一路铺到 `565.2`(边框内 `118.8 + 442.8`,右端离边框外沿 7.2px,与原版一致)。 +- **评级徽章用游戏内那套**:`UI_GAM_Rank_*`(`D`/`C`/`B`/`BB`/`BBB`/`A`/`AA`/`AAA`/`S`/`S+`/`SS`/`SS+`/`SSS`/`SSS+` 单级各一张,源图紧裁 36–110 × 42–44)。早先用的是 `UI_CMN_TabTitle_Rank_*` —— 那是**页签标题**用的区间图:AAA 那张画的是「A~AAA」、BBB 那张是「~BBB」,摆在逐题卡片上根本不是那一级的评级,而且它没有 D/C/B/BB/A/AA 这些级,低分全挤在同一张牌上。换过来之后 14 级一级一张,框按 `99×40`(官方 SSS+ 那张紧裁图 110×44 ×0.9)按高度贴齐,摆在右上那块紫场里。 +- **底部色带**:左下白框那一格原作放**段位名牌**,我们发档位不发段位名,就把档位名(`初级`/`中级`/`上级`/`超上级`/…)照原版那样写进去 —— 深棕重字 + 很淡的米白描边(`-webkit-text-stroke` + `paint-order: stroke fill`)+ 一点投影。原版那两个字是**烘死的毛笔字贴图**(`UI_DNM_DaniTitle_*` 只有日文段位名,套不上我们的档位),只能用系统字体近似:字体栈取 `Noto Serif SC` 的 **900** 字重(次选系统宋体/楷体那一类衬线),SimSun 那种细宋笔画只有原版一半粗。名字超过两个字自动缩号(`data-len`)。名字下面一行小字写「合格 · 4 / 4 道通关」。中间是原作的剩余生命底盘(`UI_DNM_Base_Life_01` 绿 / `_03` 红),照官方放大到 `200×200`,心里那个数字是「四道里过了几道」(数字图集里没有「/」字形,退回普通文字会被两边数字的描边吃掉,所以不写 `4/4`)。右边两块**照底图预留的白格**摆:`总达成率` 写进上面那格白(实测白区是 `x 503..708`,右边那截 `753..856` 被折页斜边切开,所以四个字按 `503..708` **居中**、字号 23),官方数字图集拼的大数字落在下面的色带上(`y 727`、字高 66,右端对齐 `x 950`);`DX分数` 与它的数字写进下面那条白牌(`(608,810) 360×80`,标签在左、数字在右,两端各留 46px,左端那个空的白圆头按原样空着,文字靠上排 —— 白牌下半截也被折页切掉一块),两处都是和标题条同一个深蓝 `#16306b`。 +- 数字不是 CSS 字体画的:**官方数字图集逐格切**。图集是规整 4×4 网格,格子由同名 Sprite 定 rect;白字族每格拆成「官方 `_Outline` 剪影当遮罩上深色(描边)+ 官方填充图集」两层 —— 图集本身是**纯白剪影**(游戏运行时才染色),background 层没法单独染色,所以用两个真元素叠。这套「剪影 + 运行时染色」的能力也直接用在了 DX 分数上(染成帧上的深蓝 `#0d2a63`,见下)。对照表(一张张看出来的):白字族 `[0-9]` `[10]'+'` `[11]'-'` `[12]','` `[13]'·'`(居中的圆点,就是这套字体的小数点)`[14]'%'`;分数族 `[10]'+'` `[11]'/'` `[12]'%'` `[13]'.'`。 +- **数字怎么排**:**一个字形一格,格子之间按 `advance` 等距** —— 就是图集自己的排法,小数点、逗号这些跟数字走同一条路,不开特例。唯一要做的是把每格右边的留白收掉:图集格宽比字形宽(分数族格宽 74、字形只有 60–63),照格宽排会读成「一个字一个字分开」,所以每格收回 `格宽 − advance`(分数族 74→67、90p 89→67、26p 34→24,见 `NUM_FONTS.advance`),最后一格不收回。 + - **小数点**:白字族 `[13]` 那一格是**画在字高中线上的方块**(26p 只有 8×8、90p 24×24),直接排会浮在数字中间像断字符;官方那只方块的**底边压在数字基线上**(在 `official-dani.png` 上量过)。所以这一格照用,只按 `dotBaseline`(该格墨迹底线在格高里的比例:26p 0.775 / 90p 0.6)把它压到基线上 —— 字形来自同一个族,粗细和数字一致,不需要另画也不需要借用别的族。 + - 旁边有小字标签时(两处 `DX分数`)用 `numText` 的 `baseline` 选项:整块数字往下补 `(1 − baseline) × 字高`。flex 认的基线是这一格的**底边**,而图集里数字的墨迹底线在格高的 `baseline` 处,不补这一下数字就浮在标签上面;两处都用 `align-items: baseline` 排,实测标签与数字的基线都落在同一条 y 上。 + - 末尾那个「%」是独立图片(80×80、墨迹 `x 8..70`,比图集里的 `%` 格大一号),按 `` 直排会在数字和 `%` 之间多出一截,所以放大到 `1.1 × 字高` 再用负边距把左边的留白抵掉。 +- **达成率按分数换色**:官方那套 `UI_NUM_Score_0001111_{Blue,Gold,Red}` 是同布局的三色分数图集(自带颜色、不叠描边),末尾的大「%」字形 `UI_RSL_Score_Per_{Blue,Gold,Red}` 同样三色 —— 所以达成率的档位就是三档:`≥100%` 金(SSS/SSS+)、`≥97%` 蓝(S~SS+)、再低红。`scoreTone()` 在 `public/dan.js` 里,换色在同一条 `achievementNumber()` 里做。**官方那张 `UI_NUM_Score_0001111_Base` 是一张空图集**(16 格里一个字形都没有,整张只有几列杂散像素),照它渲染出来就是一片看不见的灰影,别用。三档同时出现的样子: + +四道达成率分别为 100.8 / 98.5 / 96.0 / 92.0 时的结算页:金、蓝、红三色 +- **DX 分数不上分数色**:那一格显示的是单题 rating、不是 DX 分数比率,所以三色专门留给达成率;它用白字族剪影染深蓝(跟标题条、标签同一个深蓝)。**所有「没有成绩」的占位「—」也走这套染色**:白字族是纯白剪影,直接铺在白色达成率框或浅色底图上等于看不见。 +- **说明与按钮在画面之外**(深色底上):「好」按钮与「再来一轮 · 档位」不往那张成品屏里塞;这一轮的范围说明(档位区间、题数、限时、抽题方式、标签)也挪到了成品屏下面 —— 原作那张纸上没有放这行字的位置,硬塞只能压住页头或第一行。 两处刻意的改写: -- 原作那一格写的是**段位名**(例如「十段」)。随机段位認定不发段位名,所以改成写判定结果「合格」/「不合格」。 -- 原作那颗心是**剩余生命**。我们没有生命值这个概念,用它显示「通关几道」(合格绿、失败灰)。 +- 原作那一格写的是**段位名**(例如「十段」)。我们不发段位名(挑战模式只发档位),所以那一格写**档位名**、字重描边照原版那一路走;「合格/不合格」不另盖印,改成名字下面一行小字。 +- 原作那颗心是**剩余生命**。我们没有生命值这个概念,用它显示「四道里过了几道」(全过绿底、失败红底)。 + +两处数字口径与原来的对照:原作的「总达成率」是四首歌达成率之和,这里就是各道达成率之和;原作的 `DX分数` 是四首 DX 分数之和,这里对应各道单题 rating 之和(也就是本轮的 `totalRating`)。**这是展示口径**:那一格不等于 `B50` 里这道题贡献的分数增量,真实增量写在结算屏下面那行说明里。 + +### 结算页的素材来源 + +`public/assets/maimai/` 下那 31 张 PNG 是从 maimai DX 客户端资源里直接提取的,不是重画的: + +| 仓库文件 | 客户端贴图 | 用途 | +| --- | --- | --- | +| `result-dani.png` | `UI_DNM_Result_Base_01` | 「段位認定」整屏底图 980×928 | +| `result-random.png` | `UI_DNM_Result_Base_03` | 「ランダム段位認定」整屏底图 | +| `track-plate.png` | `UI_DNM_Result_musicBase_01` | 逐题行底板 576×124 | +| `stamp-clear.png` / `stamp-fail.png` | `UI_DNM_Icon_Result_01` / `_02` | 逐题「可」/「不可」印 | +| `num-26p.png` / `num-90p.png` | `UI_CMN_Num_26p` / `90p` | 小 / 大号白字图集(136×160 · 356×420,4×4 格) | +| `num-26p-outline.png` / `num-90p-outline.png` | `UI_CMN_Num_26p_Outline` / `90p` | 同布局的描边剪影(上深色当描边用) | +| `num-score-blue.png` / `-gold.png` / `-red.png` | `UI_NUM_Score_0001111_Blue` / `_Gold` / `_Red` | 达成率的三色分数图集(自带颜色,296×392) | +| `score-per-blue.png` / `-gold.png` / `-red.png` | `UI_RSL_Score_Per_Blue` / `_Gold` / `_Red` | 达成率末尾那个大「%」字形(80×80,同样三色) | +| `gam-rank-sssp.png` … `gam-rank-d.png`(14 张) | `UI_GAM_Rank_SSSp` … `_D` | 逐题评级徽章(游戏内那套,单级各一张) | +| `life-base-green.png` / `life-base-red.png` | `UI_DNM_Base_Life_01` / `_03` | 剩余生命底盘(绿 = 通关 / 红 = 未通关) | + +**故意不搬的一件**:逐题行的乐曲边框 `UI_CMN_RSL_KopMBase_{BSC,ADV,EXP,MST,MST_Re}`(640×140,五种难度各一张)。它是逐题行那一圈底色,但难度色与难度名(BASIC/ADVANCED/EXPERT/MASTER/Re:MASTER)都烘死在图里,套不上我们的 CF 分档,所以照它的版式用 CSS 画一层、颜色交给 `--tc1/--tc2`(见 `public/dan.css` 的「逐题行」一节,尺寸全按那张贴图量出来)。 + +提取脚本留在 `scripts/extract-maimai-assets.py`(开发工具,需要 `UnityPy` + `texture2ddecoder` + `Pillow`,不是应用依赖): + +```bash +python scripts/extract-maimai-assets.py "<含 resources.assets 的游戏目录>" +``` + +贴图对应的**贴图名**是拿 UnityPy 枚举客户端 `resources.assets` 的 `Texture2D` 得到的(那一个文件里有 5916 张,`UI_DNM_*` 家族 159 张)。数字图集的**格位**不用量:每格都有同名 Sprite(`UI_CMN_Num_90p_0 … _14`),网格正好 4×4,`下标 × 格宽/格高` 就是裁切位置;`[12]` 是逗号、`[13]` 是居中的圆点(原作这套字体的小数点就画在字高中线上,不是落在基线上的点),这两格是放大到 6 倍 + 量墨迹包围盒确认的。分数族那三套的格位同样按 4×4 / 74×98 切;**同名的 `Base` 那张是空图集**,是数每格不透明像素数(16 格全是 0)发现的 —— 只看缩略图会以为它有内容。 + +版式数字不是估的:它们是拿 `UnityPy` 读 `CourseResultProcess` / `UI_CMN_KOP_TrackData` 预制体里每个 `RectTransform` 的 anchors / anchoredPosition / pivot / sizeDelta,再按「美术坐标 = 屏幕中心 + 局部位置」换算到 980×928 底图上(逐题行 576×126 = 官方 640×140 × 0.9)。乐曲边框那几块(边框本体、外圈环、歌名条、曲绘槽、白盒、底带、右下 DX 白牌)另外拿官方贴图 `UI_CMN_RSL_KopMBase_MST.png` 逐行扫像素复核过一遍(每块的首像素…末像素,再按 `(坐标 + 2) × 0.9` 换算),两边对得上。早先那版是按底图像素分区(纸区上边界 `y=140/928=15.1%`、底部色带从 `71.6%` 起、左下白框 `x 8.7–31.6% / y 73.3–94.8%`)粗摆的,现在换成官方坐标后位置差了几十像素 —— 也就是「组件没对齐」的那几十像素。 -两处数字口径与原来的对照:原作的「总达成率」是四首歌达成率之和,这里就是各道达成率之和;原作的 `DX分数` 是四首 DX 分数之和,这里对应各道单题 rating 之和(也就是本轮的 `totalRating`)。段位名那一格改成写判定结果,所以「总达成率」下面不再有段位线。 +盘面锁死 `980×928`(固定宽高,不再用 `aspect-ratio` 缩放),窄屏让盘面横向滚动而不是压扁 —— 内部一律绝对像素,缩放交给浏览器等比处理,这样「一个数字多宽」这类关系可以直接抄官方数字。 -对照的素材包元素(用 UnityPy 解 `resources.assets` 的 GameObject / RectTransform 得到):那枚「可」印是 `UI_DNM_Stamp` 的 `Clear` / `NoClear`,两侧绳子是 `UI_DNM_Separator`,底部那块是 `UI_DNM_DaniData`。 +一处已知的将就:**单题模式(自定义条件/每日一题)也复用这张结算盘**,它们是 1 道题,底图借的是档位那一版,所以页头会写着「段位認定」。这不是这轮引入的 —— 改造前那张 CSS 皮的标题也是写死「段位認定」(`#danRunTitle` 一直没按 `kind` 变过),这一版只是把标题从 CSS 文字换成了烘进底图的字。单题模式要专属皮的话,得另找一张不带页头的纸底图,或者按 `kind` 裁掉底图上那 15.1% 的页头带。 -**贴图没有从素材包里搬**:全部用 CSS 重画。游戏美术有版权,打进一个 MIT 仓库、再提到上游是不合适的;这里只沿用版式与构成。盘面用大圆角而不是正圆 —— 四道题卡竖排进正圆,在常见宽度下会挤到不可读。 +**这些美术素材有版权**:它们不属于这个仓库的 MIT 许可范围,也不随上游项目走。只在自用/自部署的实例里使用;要不要把它们留在分支上,由部署的人自己判断。这一段替换了早先「用 CSS 重画、不搬游戏美术」的做法,是按 2026-10-09 的明确要求改的。 ### 下一道由你点,不由轮询抽 diff --git a/docs/development.md b/docs/development.md index 0ee4a95..1a6d7fb 100644 --- a/docs/development.md +++ b/docs/development.md @@ -49,7 +49,14 @@ public/ index.html 面板骨架(含账号管理区) app.js Feed 渲染、筛选器、折叠、账号管理与同步进度 dx.html / dx.js DX Rating 榜(b35 + b15),填写完成用时 - styles.css 深色主题,无外部字体或 CDN + styles.css / ui.css 重做前那一版的配色与组件样式,现在是皮肤的浅色底子 + theme.css maimai 官网皮肤层,参数取自 vendor/maimai/style.css + theme-dark.css 深色模式层,规则一律带 [data-mm-theme="dark"] 前缀 + theme-night.css 夜版背景,只换 /assets/night/ 那套贴图 + theme.js 主题开关、移动端菜单、回到顶部(换主题的唯一写入口) + bg.html 背景 iframe 的宿主页:官网那份动态背景 + vendor/maimai/** 官网素材原样快照(style.css / img / fonts,见其 README) + assets/night/** 夜版贴图,由官方素材离线重上色生成 tests/ phase1.test.ts 旧接口回归测试 phase2.test.ts 迁移、适配器、同步和调度测试 @@ -68,6 +75,21 @@ data/ 首次运行生成,本地数据不提交 data/backups/ npm run backup 生成的快照目录 ``` +## 前端皮肤与深色模式 + +皮肤复刻 maimai DX 国际版官网 PRiSM PLUS 时期的样式:`public/vendor/maimai/` 是原样快照(只改过 URL), +落到本站组件上的那一层全在 `public/theme.css`,与官方的有意偏差集中在它末尾那节,改之前先看那节。 + +- 主题落在 ``,偏好存在 `localStorage['mm-theme']`,没手动选过就跟系统。 +- 各页 `` 里有一小段脚本在首帧前定好主题(六页重复同一段是有意的:抽成外链会先亮后暗)。 +- 载入顺序 `fonts.css → styles.css → 各页自己的 CSS → ui.css → theme.css → theme-dark.css`, + 深色层一律带 `[data-mm-theme="dark"]` 前缀,所以浅色那套不用改。 +- 画布里的图是 JS 画的,CSS 管不到:`theme.js` 换主题时发 `mm-theme-change`,`stat-charts.js` 收到就按 + 当前主题重画(配色在它自己的 PALETTE 里)。 +- 页面不引外部字体或 CDN,离线可用:M PLUS 1p 只自托管 latin / latin-ext 子集,中文回退系统字体。 +- `bg.html` 是背景 iframe 的宿主页(结构取自归档快照,另加三行重置与夜版切换脚本),放在 `public/` 根下, + 好让 `vendor/maimai/` 只留原样素材。 + ## 数据模型 - `users`:一个本地人物。`is_self` 标记自己(最多一个,是主视图与 DX 榜的锚点);`is_followed`(v7)标记**关注** —— 只决定「出不出在主视图」,不限数量,且与 `is_self` 是两层。详见下文「关注 ≠ 存在」。 diff --git a/docs/images/dan-card-vs-original.png b/docs/images/dan-card-vs-original.png new file mode 100644 index 0000000..0d1363f Binary files /dev/null and b/docs/images/dan-card-vs-original.png differ diff --git a/docs/images/dan-card.png b/docs/images/dan-card.png new file mode 100644 index 0000000..597e3b0 Binary files /dev/null and b/docs/images/dan-card.png differ diff --git a/docs/images/dan-card2.png b/docs/images/dan-card2.png new file mode 100644 index 0000000..1f93601 Binary files /dev/null and b/docs/images/dan-card2.png differ diff --git a/docs/images/dan-dialog-mixed.png b/docs/images/dan-dialog-mixed.png new file mode 100644 index 0000000..63b188b Binary files /dev/null and b/docs/images/dan-dialog-mixed.png differ diff --git a/docs/images/dan-page-mixed.png b/docs/images/dan-page-mixed.png new file mode 100644 index 0000000..889d7a5 Binary files /dev/null and b/docs/images/dan-page-mixed.png differ diff --git a/docs/images/dan-page-perfect.png b/docs/images/dan-page-perfect.png new file mode 100644 index 0000000..f6e38f6 Binary files /dev/null and b/docs/images/dan-page-perfect.png differ diff --git a/docs/images/dan-settlement-mixed.jpg b/docs/images/dan-settlement-mixed.jpg new file mode 100644 index 0000000..8c74b83 Binary files /dev/null and b/docs/images/dan-settlement-mixed.jpg differ diff --git a/docs/images/dan-settlement.jpg b/docs/images/dan-settlement.jpg new file mode 100644 index 0000000..5e95901 Binary files /dev/null and b/docs/images/dan-settlement.jpg differ diff --git a/docs/images/original-card/UI_CMN_RSL_AllScorekBase.png b/docs/images/original-card/UI_CMN_RSL_AllScorekBase.png new file mode 100644 index 0000000..243782f Binary files /dev/null and b/docs/images/original-card/UI_CMN_RSL_AllScorekBase.png differ diff --git a/docs/images/original-card/UI_CMN_RSL_JaketTrack.png b/docs/images/original-card/UI_CMN_RSL_JaketTrack.png new file mode 100644 index 0000000..217d5db Binary files /dev/null and b/docs/images/original-card/UI_CMN_RSL_JaketTrack.png differ diff --git a/docs/images/original-card/UI_CMN_RSL_JaketTrackNo_0.png b/docs/images/original-card/UI_CMN_RSL_JaketTrackNo_0.png new file mode 100644 index 0000000..00df414 Binary files /dev/null and b/docs/images/original-card/UI_CMN_RSL_JaketTrackNo_0.png differ diff --git a/docs/images/original-card/UI_CMN_RSL_KopMBase_ADV.png b/docs/images/original-card/UI_CMN_RSL_KopMBase_ADV.png new file mode 100644 index 0000000..805dfa1 Binary files /dev/null and b/docs/images/original-card/UI_CMN_RSL_KopMBase_ADV.png differ diff --git a/docs/images/original-card/UI_CMN_RSL_KopMBase_BSC.png b/docs/images/original-card/UI_CMN_RSL_KopMBase_BSC.png new file mode 100644 index 0000000..752cde2 Binary files /dev/null and b/docs/images/original-card/UI_CMN_RSL_KopMBase_BSC.png differ diff --git a/docs/images/original-card/UI_CMN_RSL_KopMBase_EXP.png b/docs/images/original-card/UI_CMN_RSL_KopMBase_EXP.png new file mode 100644 index 0000000..3af4b0d Binary files /dev/null and b/docs/images/original-card/UI_CMN_RSL_KopMBase_EXP.png differ diff --git a/docs/images/original-card/UI_CMN_RSL_KopMBase_MST.png b/docs/images/original-card/UI_CMN_RSL_KopMBase_MST.png new file mode 100644 index 0000000..be5f3f9 Binary files /dev/null and b/docs/images/original-card/UI_CMN_RSL_KopMBase_MST.png differ diff --git a/docs/images/original-card/UI_CMN_RSL_KopMBase_MST_Re.png b/docs/images/original-card/UI_CMN_RSL_KopMBase_MST_Re.png new file mode 100644 index 0000000..e649a75 Binary files /dev/null and b/docs/images/original-card/UI_CMN_RSL_KopMBase_MST_Re.png differ diff --git a/docs/images/original-card/UI_DNM_Result_musicBase_01.png b/docs/images/original-card/UI_DNM_Result_musicBase_01.png new file mode 100644 index 0000000..034a719 Binary files /dev/null and b/docs/images/original-card/UI_DNM_Result_musicBase_01.png differ diff --git a/docs/platform-rating-colors.md b/docs/platform-rating-colors.md new file mode 100644 index 0000000..73484bb --- /dev/null +++ b/docs/platform-rating-colors.md @@ -0,0 +1,661 @@ +# 各平台 rating / 难度 → 颜色的对照与配置提案 + +[返回项目首页](../README.md) + +本文是**调研记录 + 配置提案**,不是已实现的代码。目的是给「新增一个平台」提供可直接粘贴的 +断点表,形状与 `public/cf-rating-colors.js` 完全一致(`[阈值, tone, 标签, 区间]`,从高到低, +外加一张 `tone → [主色, 深色]` 表)。 + +调研时间 2026-10-18。每条结论后面都跟来源链接。**官方来源**指平台自己的文档 / 自己的 CSS / +自己的 API;**二手来源**指第三方主题、浏览器扩展、博客,文中会点名。凡是我没能从一手来源 +证实的数字或颜色,一律在 [第 6 节](#6-不确定项诚实清单) 列出,不写成结论。 + +本轮调研中,`codeforces.com` 的 HTML 页面对本机请求返回 **403**(静态 CSS 可以取,页面不行), +`leetcode.com` 同样 403;因此 CF 的 CSS 颜色、LeetCode 的难度色分别用了「官方 CSS 静态文件」 +替代路径(见下文),并在不确定项里标注了替代范围。 + +--- + +## 0. 仓库现有形状(对照用) + +`public/cf-rating-colors.js` 现有一张表: + +```js +const bands = [ + [2400, 'red', '红色', '2400+'], + [2100, 'orange', '橙色', '2100–2399'], + [1900, 'violet', '紫色', '1900–2099'], + [1600, 'blue', '蓝色', '1600–1899'], + [1400, 'cyan', '青色', '1400–1599'], + [1200, 'green', '绿色', '1200–1399'], + [0, 'gray', '灰色', '低于 1200'], +]; +``` + +关键口径:**tone 是唯一色源**,卡片的 `--tc1 / --tc2`、底板、滤镜都从 tone 查 +`CF_TONE_COLORS`。所以新增平台的成本 = 一张断点表 + (必要时)几个新 tone。 + +下面每个平台给两张东西:**数据表**(供人读)和**同形状的代码**(供直接粘贴,集中在第 4 节)。 + +--- + +## 1. Codeforces + +### 1.1 比赛 rating → 头衔 / 颜色(官方) + +颜色与头衔出自官方公告 [The Second Revolution of Colors and Titles](https://codeforces.com/blog/entry/20638); +**注意那是 2015 年的表,CM / Master 的分界后来动过**,所以边界以 CF 自己的 API 为准。 + +我用官方 API 拉了当前全部活跃 rated 用户,按 `rank` 分组求 rating 区间: + +``` +https://codeforces.com/api/user.ratedList?activeOnly=true&includeRetired=false +→ 49384 人(2026-10-18) +legendary grandmaster n=23 rating 3006..3657 +international grandmaster n=66 rating 2601..2997 +grandmaster n=131 rating 2400..2599 +international master n=108 rating 2300..2397 +master n=425 rating 2100..2299 +candidate master n=702 rating 1900..2099 +expert n=2833 rating 1600..1899 +specialist n=4269 rating 1400..1599 +pupil n=6958 rating 1200..1399 +newbie n=33869 rating -38..1199 +``` + +| rating | 头衔(官方) | 颜色名(官方公告) | tone | 来源 | +| --- | --- | --- | --- | --- | +| 3000+ | Legendary Grandmaster | red(句首字母黑色) | red | [blog/20638](https://codeforces.com/blog/entry/20638) + 上面的 API 区间 | +| 2600–2999 | International Grandmaster | red | red | 同上 | +| 2400–2599 | Grandmaster | red | red | 同上 | +| 2300–2399 | International Master | orange | orange | 同上 | +| 2100–2299 | Master | orange | orange | 同上 | +| 1900–2099 | Candidate Master | violet | violet | 同上 | +| 1600–1899 | Expert | blue | blue | 同上 | +| 1400–1599 | Specialist | cyan | cyan | 同上 | +| 1200–1399 | Pupil | green | green | 同上 | +| <1200 | Newbie | gray | gray | 同上 | + +**两处与 2015 年公告不一致,必须按新版理解**: + +- 公告写 `2900+` 是 LGM,且 `1900–2199` 是 Candidate Master、`2200–2299` 是 Master。 + 当前实际是 **LGM 3000+**、**CM 1900–2099**、**Master 2100+**。本仓库现有的 + `cf-rating-colors.js`(violet 到 2099、orange 从 2100 起)**与当前实际一致**,是对的。 +- Legendary Grandmaster 与其它红色档位的视觉区别不是新颜色,而是**句首字母渲染成黑色** + (CF 的 CSS 里叫 `legendary-user-first-letter`,见 CF 站内讨论 + [blog/94503](https://codeforces.com/blog/entry/94503) 提到的类名)。 + +**十六进制(不完整,见第 6 节)**:CF 官方页面没有公布十六进制,它的样式表用的是 CSS +关键字。我只拿到一个可点名的二手来源:[GaurangTandon/codeforces-darktheme](https://github.com/GaurangTandon/codeforces-darktheme) +的 README 表格,它把原作颜色与新色并排列出,其中明确写了 +**cyan = `#03a89e`**、**green = `#00d700`**(gray / blue / violet / orange / red 只写了颜色名, +没给原值)。我没有拿到 CF 自己的 CSS(`codeforces.com/css/style.css` 取得到,但里面没有 +`user-*` 规则;页面 403),所以**其余 hex 一律不写死**。 + +### 1.2 题目 rating 的刻度和颜色 + +题目 rating 是官方数字,直接来自 CF 的 problemset API。实测(2026-10-18): + +``` +https://codeforces.com/api/problemset.problems +→ 11152 道有 rating 的题;min = 800,max = 3500; + 全部是 100 的整数倍;共 28 个不同取值(800,900,...,3500) +``` + +也就是说 **CF 题目 rating 是 800–3500、步长 100 的离散刻度**,量纲和 handle rating 一样, +可以直接当「定数」用(`src/dx/rating.ts` 现在就是这么用的)。 + +**题目 rating 的颜色:官方没有规定。** CF 没有一份文档说明 problemset 里的 rating 该涂什么色; +本仓库现在的做法——把 handle 的七档色界直接套到题目 rating 上——是**社区通行做法** +(例如 [True Codeforces Heatmap](https://codeforces.com/blog/entry/140881) 这类扩展自己另定了 +更细的题目 rating 色带)。我无法确认 problemset 页面的 rating 文本本身是否着色(页面 403)。 +**建议:沿用现有 handle 色界,并在注释里说明这是社区约定、不是 CF 官方规定。** + +--- + +## 2. AtCoder + +### 2.1 比赛 rating → 颜色(官方,含官方 hex) + +AtCoder 官方公告 [AtCoder Rating System](https://atcoder.jp/posts/16) 给出色带;官方样式表 +[`img.atcoder.jp/public/5b4b344/css/base.css`](https://img.atcoder.jp/public/5b4b344/css/base.css) +给出精确 hex(我直接抓了这个文件): + +| rating | 颜色名 | tone | 官方 hex | 来源 | +| --- | --- | --- | --- | --- | +| 2800–3199 | Red | red | `#FF0000` | 官方 CSS `.user-red` | +| 2400–2799 | Orange | orange | `#FF8000` | 官方 CSS `.user-orange` | +| 2000–2399 | Yellow | yellow | `#C0C000` | 官方 CSS `.user-yellow` | +| 1600–1999 | Blue | blue | `#0000FF` | 官方 CSS `.user-blue` | +| 1200–1599 | Cyan | cyan | `#00C0C0` | 官方 CSS `.user-cyan` | +| 800–1199 | Green | green | `#008000` | 官方 CSS `.user-green` | +| 400–799 | Brown | brown | `#804000` | 官方 CSS `.user-brown` | +| 0–399 | Gray | gray | `#808080` | 官方 CSS `.user-gray` | +| (未 rated) | — | unrated | `#000000` | 官方 CSS `.user-unrated` | +| (admin) | — | (不用) | `#C000C0` | 官方 CSS `.user-admin` | + +**3200 以上没有官方颜色。** 官方公告里 `3200-3599` 与 `3600-3999` 两行写的就是 `???`;官方 CSS +里最高的用户颜色类就是 `.user-red`,没有更高档。**所以 3200+ 只能落回 red,不能自己造一档。** + +AtCoder 没有「头衔」这种东西,只有颜色 + rating 数字;用户页显示的是 rating 本身。 + +### 2.2 AtCoder 自己的题目难度:不存在 + +AtCoder **不发布题目难度**。比赛页面给的是**配点**(100 / 200 / 300 / …,ABC 的 A–G), +配点不等于难度(同一场里 E 有时比 F 难)。所以「AtCoder 官方题目难度色」这个东西是没有的, +不要假装有。 + +题目难度的**事实标准**是第三方 AtCoder Problems(kenkoooo)算的 `difficulty`: + +- 数值来自 [AtCoder Problems API](https://github.com/kenkoooo/AtCoderProblems/blob/main/doc/api.md) + 的 `problem-models.json` / `merged-problems.json`,是社区用解题者 rating 拟合出来的, + **不是 AtCoder 官方数据**。 +- 颜色分档我从它发布的打包文件里读出来了(这两条是它的原样实现,不是我推的): + - [`main.40f7eac8.chunk.css`](https://kenkoooo.com/atcoder/static/css/main.40f7eac8.chunk.css): + `.difficulty-red{color:red}`、`.difficulty-orange{color:#ff8000}`、 + `.difficulty-yellow{color:#c0c000}`、`.difficulty-blue{color:#00f}`、 + `.difficulty-cyan{color:#00c0c0}`、`.difficulty-green{color:green}`、 + `.difficulty-brown{color:#804000}`、`.difficulty-grey{color:grey}` + (dark 主题另有一套浅化值,如 `#ff6767 / #ffb836 / …`)。 + - [`main.11188a2c.chunk.js`](https://kenkoooo.com/atcoder/static/js/main.11188a2c.chunk.js): + ```js + de = ["Black","Grey","Brown","Green","Cyan","Blue","Yellow","Orange","Red"]; + je = e => de[Math.min(Math.floor(e/400), de.length-2) + 1]; + ``` + 即 **步长恰恰是 400,颜色顺序与 AtCoder 用户色完全一致**: + +| difficulty | tone | light 主题 hex(AtCoder Problems) | +| --- | --- | --- | +| 2800+ | red | `red`(= `#FF0000`) | +| 2400–2799 | orange | `#ff8000` | +| 2000–2399 | yellow | `#c0c000` | +| 1600–1999 | blue | `#0000ff` | +| 1200–1599 | cyan | `#00c0c0` | +| 800–1199 | green | `green`(= `#008000`) | +| 400–799 | brown | `#804000` | +| 0–399 | grey | `grey`(= `#808080`) | + +**一个实现细节值得注意**:它的颜色数组里第一个元素是 `"Black"`,但 `+1` 之后 +`Black` 永远取不到(`floor(d/400)` 最小是 0,取 `de[1]`)。所以**数值色带里没有黑色档**, +`difficulty-black` 这个类名在 CSS 里也没有配色规则。不要照抄一个「黑色难度档」出来。 + +--- + +## 3. LeetCode + +### 3.1 比赛 rating 的档位:**百分位制,不是固定分数线**(官方) + +官方帖 [🎖 The new contest badge is here! 🎖](https://leetcode.com/discuss/post/934706/the-new-contest-badge-is-here-by-leetcod-34lb) +(由官方账号 LeetCode 发布)原文: + +> **Among LeetCoders with rating >=1600**, +> If you are in the top **5%** of the contest rating, you'll get the "**Guardian**" badge. +> If you are in the top **25%** of the contest rating, you'll get the "**Knight**" badge. + +所以: + +- **没有任何官方数字门槛。** 分数线随参赛人群漂移;「Knight ≈ 1600」「Guardian ≈ 2200」 + 是社区对某个时刻百分位的快照,**不是官方边界**(Reddit 上关于「Knight 到底多少分」的 + 反复提问本身就是它不固定的证据)。 +- 档位名只有两个:**Knight**、**Guardian**,其余人无 badge。没有「专家 / 大师」之类的细分。 +- **颜色**:badge 是图片,不是色带。我下载了官方 badge 资产 + (`https://leetcode.com/static/images/badges/guardian.png`、`knight.png`)并采样主色, + 得到的是深灰 / 近黑(`#2F302E`、`#262626` 一类),**不构成可用的颜色阶梯**。官方没有 + 公布过 Guardian / Knight 的十六进制配色。 + **结论:不要从官方来源给 tier 绑颜色。** 若要显示,只能自己定(见第 4 节的提案,已注明是自定)。 + +### 3.2 题目难度 Easy / Medium / Hard:**分类变量,且官方 hex 有据可查** + +难度是三个字符串枚举(GraphQL 里就是 `EASY` / `MEDIUM` / `HARD`),**没有数字**。 + +颜色我拿到了**官方自己的 CSS**(LeetCode 设计系统的 token)。`leetcode.com` 对本机 403, +但**力扣中国站与 LeetCode 共用同一套设计系统**,它的构建产物可以取: + +- [`static.leetcode.cn/.../17fab917895643c3.css`](https://static.leetcode.cn/cn-frontendx-assets/production/_next/static/css/17fab917895643c3.css) + 里定义: + ```css + --difficulty-easy: var(--light-teal-60); /* dark: var(--dark-teal-60) */ + --difficulty-medium: var(--light-yellow-60); /* dark: var(--dark-yellow-60) */ + --difficulty-hard: var(--light-red-60); /* dark: var(--dark-red-60) */ + ``` + 同文件里的对应色值: + +| 难度 | dark/light token | 浅色主题 hex | 深色主题 hex | +| --- | --- | --- | --- | +| Easy | teal-60 | `#1cb8b8` | `#46c6c2` | +| Medium | yellow-60 | `#ffb800` | `#fac31d` | +| Hard | red-60 | `#f63636` | `#f8615c` | + +- **旧版调色板**(长期被各种扩展和文章引用):Easy `#00b8a3`、Medium `#ffc01e`、 + Hard `#ff375f`。这组值仍是**二手来源**(例如 xbar 插件 + [`leetcode_reminder.5m.py`](https://github.com/matryer/xbar-plugins/blob/main/Web/leetcode_reminder.5m.py) + 里的 `color_easy/medium/hard`)。旁证:`#ffc01e` 确实还留在上面的官方 CSS 里(作为 + `dark-yellow` 旧 token),说明新旧两套并存。 + +**建议**:主色取官方 CSS 的新 token(`#1cb8b8 / #ffb800 / #f63636`),并在注释里注明旧值 +`#00b8a3 / #ffc01e / #ff375f` 是历史版本。 + +--- + +## 4. 洛谷(Luogu) + +### 4.1 题目难度:**8 档有序枚举 + 官方 hex**(注意:2026-07 起已不是 7 档) + +洛谷站务版官方帖 [新版难度体系更新下的临时难度定义](https://www.luogu.com.cn/discuss/1312513) +(2026-07-02,作者 chen_zhe)给出**八档**,颜色直接写在正文的 LaTeX `\color{}` 里: + +| 难度码 | 名称 | 颜色 | 官方 hex | 国际站名 | +| --- | --- | --- | --- | --- | +| 1 | 入门 | 红 | `#FE4C61` | Red | +| 2 | 普及- | 橙 | `#F39C11` | Orange | +| 3 | 普及 | 黄 | `#FFC116` | Yellow | +| 4 | 普及+/提高- | 绿 | `#53C41A` | Green | +| 5 | 提高 | **青** | `#13C2C2` | Cyan | +| 6 | 提高+/省选- | 蓝 | `#3498DB` | Blue | +| 7 | 省选/NOI- | 紫 | `#9C3DCF` | Purple | +| 8 | NOI/NOI+/CTS | **黑** | `#0E1D69` | Black | +| 0 | 暂无评定 | — | — | — | + +**两个重要偏差,实现前必须知道:** + +1. **档数变了。** 老体系是 7 档(入门 / 普及- / 普及·提高- / 普及+·提高 / 提高+·省选- / + 省选·NOI- / NOI·NOI+·CTSC)。新体系插进了 **`提高` 这一档(青色 `#13C2C2`)**, + 共 8 档。任务书里给的 7 档列表、以及 `src/dx/rating.ts` 注释里的 + 「洛谷只提供等级码(0–4)」,都是**过时**的说法。帖子自称「临时」,后续可能再动。 +2. **官方 hex 与社区表有两个字符级差异**:官方帖是 `#53C41A` 与 `#9C3DCF`;社区整理的 + [洛谷相关颜色参考表(2025-7 版)](https://www.luogu.com/article/uqkvrysw) 写的是 + `#52C41A` 与 `#9D3DCF`。**以官方帖为准**,社区值只作为可能的渲染差异记录。 + +**难度码是实时可验证的**:洛谷题页的 `lentille-context` 里有 `difficulty` 字段, +2026-10-18 实测 —— P1001 `difficulty=1`、P3373 `difficulty=4`、P1080 `difficulty=5`、 +P5659 `difficulty=8`。与上表逐项吻合,说明**新 8 档已经在线上生效**。这也反驳了仓库注释里的 +「0–4」。 + +### 4.2 洛谷比赛等级分(Elo):**有数字,但没有官方颜色阶梯** + +官方帮助中心 [洛谷比赛等级分](https://help.luogu.com.cn/manual/luogu/account/elo-ranking) +说明:洛谷引入了类似 AtCoder 的 Elo 等级分,显示在用户页;全站前 1000 名有榜单 +()。官方还给出各难度比赛的**封顶阈值**: + +- Div.4 级比赛:1200 +- Div.3 级比赛:1600 +- Div.2 级比赛:2000 +- Div.1 级比赛:不限 + +**但官方没有规定等级分的颜色分档。** 所以洛谷的「比赛 rating」是一个**纯数字**, +tone 只能自己定(见第 4 节提案,会标明是自定),不能宣称是洛谷的官方配色。 + +--- + +## 5. 其它平台 + +### 5.1 CodeChef —— 星级(官方)+ 题目难度分档(官方) + +**星级**出自官方博客 [Understanding CodeChef's Rating System](https://www.codechef.com/blogs/how-does-codechef-rating-system-work); +精确区间与徽章底色取自**官方 CDN 上的星带图**(`https://cdn.codechef.com/images/blogs/star-rating-bands.png`, +我下载后用 Pillow 采样七个徽章主色,徽章有轻微渐变,取的是最高频像素): + +| rating | 星 | 徽章底色(采样) | +| --- | --- | --- | +| ≤1399 | 1★ | `#666666` | +| 1400–1599 | 2★ | `#3D7B30` | +| 1600–1799 | 3★ | `#4066C5` | +| 1800–1999 | 4★ | `#634470` | +| 2000–2199 | 5★ | `#F5C142` | +| 2200–2499 | 6★ | `#EF8633` | +| ≥2500 | 7★ | `#BF2828` | + +同一篇官方博客给的**division** 是:Div.4 `0–1399`、Div.3 `1400–1599`、Div.2 `1600–1999`、 +Div.1 `2000+`。注意 division 与星**不是一回事**:Div.2 横跨 3★ 和 4★。 + +**题目难度**是数字(题面 URL 里直接带,例如 +[`.../course/logical-problems/DIFF800/problems/TODOLIST`](https://www.codechef.com/practice/course/logical-problems/DIFF800/problems/TODOLIST)), +官方练习页按区间分类([codechef.com/practice](https://www.codechef.com/practice)): + +| 区间 | 官方页面的说法 | +| --- | --- | +| 0–500 | 500 difficulty rating(最基础) | +| 500–1000 | 500 to 1000 | +| 1000–1400 | 帮到 1 star | +| 1400–1600 | 帮到 2 star | +| 1600–1800 | 帮到 3 star | +| 1800–2000 | 帮到 4 star | +| 2000–2500 | 帮到 5 star 及以上 | + +**CodeChef 题目难度的官方配色我没能证实**:他们的练习页是客户端渲染,做题页的难度标签 +颜色没有落到我可取的一手文件里。第 4 节的提案里,CodeChef 题目难度色是**我按星级色近似的 +自定映射**,不是官方值。 + +### 5.2 TopCoder —— 唯一一个「官方把色阶画成图」的平台 + +官方文章 [Ratings](https://www.topcoder.com/thrive/articles/Ratings) 正文: +"A coder's handle is color-coded depending on their rating within the Arena.", +并附 "Rating Key" 图。我下载了那张图(`assets.topcoder.com/.../Key-....jpg`)并读了内容: + +| rating | 颜色 | +| --- | --- | +| 2200+ | red | +| 1500–2199 | yellow | +| 1200–1499 | blue | +| 900–1199 | green | +| 0–899 | grey | + +TopCoder 的**题目**没有 rating 刻度,只有 division 与 problem level(I / II / III), +不构成颜色阶梯。 + +### 5.3 HackerRank —— 分类难度,没有 rating 色阶 + +题目难度是 `Easy` / `Medium` / `Hard` 三个字符串(部分 track 另有 `Advanced` 子标签), +例如官方题目页 [hackerrank.com/interview/hackerrank](https://www.hackerrank.com/interview/hackerrank) +列出的 "Easy / Hard" 标签。**没有找到官方把难度或 contest rating 绑到颜色的规则**: +其 Skills 体系用的是星级与奖牌([hackerrank.com/scoring](https://www.hackerrank.com/scoring)), +与题目难度是两套东西。 + +### 5.4 CSES —— 完全没有难度 + +[CSES Problem Set](https://cses.fi/problemset/) 只有分类(Introductory / Sorting and Searching / +Dynamic Programming / Graph Algorithms / Range Queries / Tree Algorithms / Mathematics / +Geometry / String Algorithms …),**没有任何难度数值或颜色**。不要给它造一档。 + +### 5.5 牛客(Nowcoder)—— 本仓库已确认「不提供难度」 + +本仓库自己的适配器文档 [docs/platforms.md](platforms.md) 明确写了两件事: +「保留提交结果、得分、语言、运行时间(ms)和内存(KB)」以及「**不提供难度**,不参与 B50」。 +牛客的出题规范帖([牛客系列赛出题难度要求](https://www.nowcoder.com/discuss/1222899))是用 +**CF div2 的 rating 段**(800–900、800–1400 …)来描述题目难度的,这反过来说明牛客**自身没有 +题目难度刻度**。该帖是 JS 渲染页,我只读到标题与检索摘要,未能读全文(见第 6 节)。 +**结论:牛客没有可绑的颜色维度。** + +### 5.6 Virtual Judge —— 聚合器,没有自己的刻度 + +VJudge 是**提交聚合器**,题目来自各源 OJ,它自己既没有 contest rating 也没有题目难度色阶; +难度信息只在借用源站元数据时出现(例如它的某个训练赛页面写 "a collection of 800-rated +problems from Codeforces",见 [vjudge.net/contest/750384](https://vjudge.net/contest/750384))。 +做题列表页 [vjudge.net/problem](https://vjudge.net/problem) 需要登录,我未登录看不到难度列。 +**建议**:VJudge 上的题按 source judge 走对应平台表,不要给 VJudge 建独立表。 + +--- + +## 6. 不确定项(诚实清单) + +以下每一条都是**我没能从一手来源证实**的,实现时不要当成事实: + +1. **CF 的十六进制颜色**。官方只给颜色名。`cyan = #03a89e`、`green = #00d700` 来自二手来源 + [codeforces-darktheme](https://github.com/GaurangTandon/codeforces-darktheme); + gray / blue / violet / orange / red 的 hex 我**没有**可点名来源,故不写。 + (2026-10-18 `codeforces.com` 页面请求 403,拿不到 CF 自己的 `user-*` CSS。) +2. **CF 题目 rating 是否在 problemset 页面着色**。未证实(同样因 403)。本仓库把它按 handle + 色界着色是社区约定,不是 CF 官方规定。 +3. **CF 2015 公告与当前边界的差异**。公告(2900+ LGM、CM 到 2199)已被现行实践取代; + 我确认的是**当前 API 的实际区间**,没有找到一份官方公告写明「LGM 改为 3000」。上表按 API + 实测写。 +4. **AtCoder 3200+ 的颜色**。官方公告写 `???`,官方 CSS 最高只到 `.user-red`。我用 red, + 但**官方并未定义** 3200/3600 两档的显示方式。 +5. **AtCoder 题目难度**。官方没有这东西;`difficulty` 全部来自第三方 AtCoder Problems, + 会随其算法/数据更新而变,且第三方镜像可能缺题。 +6. **AtCoder Problems 的 dark 主题配色与 light 不同**(例如 red 在 dark 下是 `#ff6767`)。 + 上表给的是 light 值;本仓库是深色卡面的话要另取 dark 值。 +7. **LeetCode 的 Guardian / Knight 没有官方分数线**,只有百分位规则。任何「1600 / 2200」 + 都是社区快照。另外「top 5% / 25%」的分母是**活跃参赛者**,官方没有公开活跃人数 + (见该帖下的官方回复与用户讨论)。 +8. **LeetCode 的难度色取自力扣中国站(leetcode.cn)的 CSS bundle**,因为 `leetcode.com` + 对本机 403。两者共用设计系统,但我**没有**在 `leetcode.com` 自己的 bundle 上复核。 +9. **LeetCode Guardian / Knight 的官方配色不存在**,badge 是图片;第 7 节给的颜色是我自定。 +10. **洛谷新难度体系被称为「临时」**(帖子原文自述),档数与颜色可能再变;官方 hex 与社区 + 整理的 hex 存在 `53C41A vs 52C41A`、`9C3DCF vs 9D3DCF` 的差异。 +11. **洛谷比赛等级分没有官方颜色分档**;只有封顶阈值(Div.4/3/2 = 1200/1600/2000)。 +12. **CodeChef 题目难度的官方配色未证实**;星带图的 hex 是我从官方 PNG 采样的 + (渐变 + 抗锯齿会带来 ±1~2 的误差),不是官方 token。 +13. **牛客出题规范帖未读全文**(JS 渲染),只有标题与检索摘要。 +14. **VJudge 登录后的题目列表未看**,结论「没有自己的刻度」基于其聚合器定位与公开页面, + 不是穷尽验证。 +15. **HackerRank 是否存在我没找到的颜色规则**:我只确认了「没找到」,不等于「不存在」。 + +--- + +## 7. 配置提案(同形,可直接粘贴) + +设计原则: + +- **tone 名字沿用 `CF_TONE_COLORS` 的词汇**,缺什么补什么(`brown / yellow / black / teal / + amber / rose`)——新增平台不该改动已有 tone 的观感。 +- **表形状与 `cf-rating-colors.js` 完全一致**:`[阈值, tone, 标签, 区间]`,从高到低。 +- **映射不干净的地方如实建模**:分类用字符串 key,有序枚举用平台自己的码, + 百分位档位带 `approximate` 标记,绝不把非数值的东西塞进数字阈值。 + +### 7.1 新增 tone 的配色 + +共享 tone(`red/orange/violet/blue/cyan/green/gray/unrated`)**继续用仓库现有的 +`CF_TONE_COLORS`,不要动**,否则所有已上线的卡片会跟着变色。只补下面这些新 tone: + +```js +// 与 CF_TONE_COLORS 合并使用;[主色, 深色]。 +// 主色:brown/yellow 取 AtCoder 官方 CSS 值,black 取洛谷官方值, +// teal/amber/rose 取 LeetCode 官方 CSS 值。深色是本文档按现有表的明度关系生成的, +// 不是任何平台的官方值 —— 如需更贴近 maimai 观感可整体柔化。 +export const EXTRA_TONE_COLORS = { + brown: ['#804000', '#4a2600'], // AtCoder 官方 .user-brown + yellow: ['#c0c000', '#6f6f00'], // AtCoder 官方 .user-yellow + black: ['#0e1d69', '#060d33'], // 洛谷 NOI/NOI+/CTS 官方 #0E1D69 + teal: ['#1cb8b8', '#0d6b6b'], // LeetCode 官方 --light-teal-60 + amber: ['#ffb800', '#a67400'], // LeetCode 官方 --light-yellow-60 + rose: ['#f63636', '#a01c1c'], // LeetCode 官方 --light-red-60 +}; +``` + +### 7.2 Codeforces + +```js +// 与现有 cf-rating-colors.js 的 bands 一致,此处仅为「平台表统一化」后的形态。 +// 阈值以 https://codeforces.com/api/user.ratedList 实测为准(2026-10-18)。 +export const CF_BANDS = [ + [3000, 'red', 'Legendary Grandmaster', '3000+'], + [2600, 'red', 'International Grandmaster', '2600–2999'], + [2400, 'red', 'Grandmaster', '2400–2599'], + [2300, 'orange', 'International Master', '2300–2399'], + [2100, 'orange', 'Master', '2100–2299'], + [1900, 'violet', 'Candidate Master', '1900–2099'], + [1600, 'blue', 'Expert', '1600–1899'], + [1400, 'cyan', 'Specialist', '1400–1599'], + [1200, 'green', 'Pupil', '1200–1399'], + [0, 'gray', 'Newbie', '低于 1200'], +]; +``` + +**题目 rating 用同一张表**(800–3500、步长 100),只是 label 换成区间文案。 +`[2400,'red']` 之下就是 `[2100,'orange']`,与本仓库现状一致。 + +### 7.3 AtCoder · 比赛 rating + +```js +// 阈值与颜色名:https://atcoder.jp/posts/16 +// hex 出处:https://img.atcoder.jp/public/5b4b344/css/base.css +// 3200+ 官方只写 "???",官方 CSS 最高到 .user-red,故落回 red(见第 6 节第 4 条)。 +export const ATCODER_BANDS = [ + [2800, 'red', 'Red', '2800+'], + [2400, 'orange', 'Orange', '2400–2799'], + [2000, 'yellow', 'Yellow', '2000–2399'], + [1600, 'blue', 'Blue', '1600–1999'], + [1200, 'cyan', 'Cyan', '1200–1599'], + [800, 'green', 'Green', '800–1199'], + [400, 'brown', 'Brown', '400–799'], + [0, 'gray', 'Gray', '0–399'], +]; +``` + +### 7.4 AtCoder · 题目难度(AtCoder Problems,第三方) + +```js +// 分档逻辑抄自 AtCoder Problems 前端打包文件(步长 400,颜色名与 AtCoder 用户色同序): +// de = ["Black","Grey","Brown","Green","Cyan","Blue","Yellow","Orange","Red"] +// je = e => de[Math.min(Math.floor(e/400), de.length-2) + 1] +// "Black" 在该公式下取不到,故表中没有黑色档 —— 这是它的原样,不要"修好"。 +export const ATCODER_PROBLEM_BANDS = [ + [2800, 'red', 'Red', '2800+'], + [2400, 'orange', 'Orange', '2400–2799'], + [2000, 'yellow', 'Yellow', '2000–2399'], + [1600, 'blue', 'Blue', '1600–1999'], + [1200, 'cyan', 'Cyan', '1200–1599'], + [800, 'green', 'Green', '800–1199'], + [400, 'brown', 'Brown', '400–799'], + [0, 'gray', 'Grey', '0–399'], +]; +// 注意:difficulty 是第三方算出来的,不是 AtCoder 官方数据; +// 且 AtCoder 官方给的是"配点"(100/200/300…),与难度不是一回事。 +``` + +### 7.5 洛谷 · 题目难度(**有序枚举,不是 rating**) + +```js +// 洛谷的 difficulty 是 0–8 的有序枚举码,本身就是平台给的值 —— 直接当"阈值"用是诚实的, +// 不存在"编一个数字"的问题。数值区间文案只是展示用。 +// 出处:https://www.luogu.com.cn/discuss/1312513 (官方帖,2026-07-02,自称"临时") +// 线上实测:P1001=1、P3373=4、P1080=5、P5659=8(2026-10-18) +export const LUOGU_DIFFICULTY_BANDS = [ + [8, 'black', 'NOI/NOI+/CTS', 'NOI/NOI+/CTS'], + [7, 'violet', '省选/NOI-', '省选/NOI-'], + [6, 'blue', '提高+/省选-', '提高+/省选-'], + [5, 'cyan', '提高', '提高'], + [4, 'green', '普及+/提高-', '普及+/提高-'], + [3, 'yellow', '普及', '普及'], + [2, 'orange', '普及-', '普及-'], + [1, 'red', '入门', '入门'], +]; +// code 0(暂无评定)→ tone 'unrated',照 cfRatingColor 的写法在函数里兜底。 +``` + +注意洛谷的「红」是**最低档**(入门),和 CF/AtCoder 把红当最高档**方向相反**。这是平台语义, +不要用同一个「红=强」的假设去渲染。 + +### 7.6 洛谷 · 比赛等级分(**有数字,无官方颜色**) + +```js +// 洛谷只有 Elo 数值,官方没有颜色分档,所以这里没有 bands。 +// 若卡片必须给色,建议按官方封顶阈值当"软档位"展示,并标注为自定: +// Div.4 封顶 1200 / Div.3 封顶 1600 / Div.2 封顶 2000 / Div.1 不限 +// 出处:https://help.luogu.com.cn/manual/luogu/account/elo-ranking +export const LUOGU_ELO_DIVISION_CAPS = { div4: 1200, div3: 1600, div2: 2000, div1: null }; +``` + +### 7.7 CodeChef · 星级(比赛 rating) + +```js +// 区间来自官方 CDN 图 https://cdn.codechef.com/images/blogs/star-rating-bands.png +// (我下载后逐档采样徽章底色);division 区间来自官方博客。 +export const CODECHEF_STAR_BANDS = [ + [2500, 'red', '7★', '2500+'], + [2200, 'orange', '6★', '2200–2499'], + [2000, 'yellow', '5★', '2000–2199'], + [1800, 'violet', '4★', '1800–1999'], + [1600, 'blue', '3★', '1600–1799'], + [1400, 'green', '2★', '1400–1599'], + [0, 'gray', '1★', '低于 1400'], +]; +``` + +### 7.8 CodeChef · 题目难度(**官方分档,颜色是自定**) + +```js +// 区间来自官方练习页 https://www.codechef.com/practice 的分类标题。 +// ⚠️ tone 是我按星级色近似的自定映射:CodeChef 题目难度的官方配色未证实(第 6 节第 12 条)。 +export const CODECHEF_PROBLEM_BANDS = [ + [2000, 'red', '2000–2500', '5 star+'], + [1800, 'violet', '1800–2000', '4 star'], + [1600, 'blue', '1600–1800', '3 star'], + [1400, 'green', '1400–1600', '2 star'], + [1000, 'yellow', '1000–1400', '1 star'], + [500, 'orange', '500–1000', '基础逻辑'], + [0, 'gray', '0–500', '最基础'], +]; +``` + +### 7.9 LeetCode · 题目难度(**分类变量:不要塞进数字阈值**) + +```js +// 难度是字符串枚举 EASY/MEDIUM/HARD,没有数字。用 key 查表,别伪造 rating。 +// hex 出自 LeetCode 官方 CSS token: +// https://static.leetcode.cn/cn-frontendx-assets/production/_next/static/css/17fab917895643c3.css +// --difficulty-easy: var(--light-teal-60) = #1cb8b8 +// --difficulty-medium: var(--light-yellow-60) = #ffb800 +// --difficulty-hard: var(--light-red-60) = #f63636 +// 旧版常用值(#00b8a3 / #ffc01e / #ff375f,二手来源)在注释里留档即可。 +export const LEETCODE_PROBLEM_DIFFICULTY = { + EASY: { tone: 'teal', label: 'Easy', labelZh: '简单' }, + MEDIUM: { tone: 'amber', label: 'Medium', labelZh: '中等' }, + HARD: { tone: 'rose', label: 'Hard', labelZh: '困难' }, +}; +export function leetcodeDifficultyColor(difficulty) { + const key = String(difficulty ?? '').toUpperCase(); + return LEETCODE_PROBLEM_DIFFICULTY[key] ?? null; // null = 认不出来,按无档处理 +} +``` + +### 7.10 LeetCode · 比赛档位(**百分位:必须带"近似"标记**) + +```js +// 官方规则(https://leetcode.com/discuss/post/934706/...): +// "Among LeetCoders with rating >=1600, top 5% => Guardian, top 25% => Knight" +// 没有官方分数线,官方也没有配色(badge 是图片)。所以: +// - 不给它 bands(那是假的); +// - rating 只用来做一个"大概会不会拿到 badge"的提示,且必须带 approximate: true; +// - tone 是自定(官方无配色)。 +export const LEETCODE_CONTEST_TIERS = [ + { key: 'guardian', label: 'Guardian', tone: 'red', basis: 'rating>=1600 的前 5%', + approximateRatingFloor: 2200, approximate: true }, + { key: 'knight', label: 'Knight', tone: 'violet', basis: 'rating>=1600 的前 25%', + approximateRatingFloor: 1600, approximate: true }, +]; +// approximateRatingFloor 是社区对某时刻百分位的快照,会漂移; +// 界面上若用它,应显示成"约 2200+",不能写成官方门槛。 +``` + +### 7.11 统一查表入口 + +```js +// 与 cfRatingColor 同构:输入"数值型"的难度/rating,输出 {tone,label,range}。 +// 分类型(LeetCode 难度)与档位型(LeetCode badge)不走这里。 +export const PLATFORM_BANDS = { + codeforces: CF_BANDS, + atcoder: ATCODER_BANDS, + 'atcoder-problems': ATCODER_PROBLEM_BANDS, + codechef: CODECHEF_STAR_BANDS, + 'codechef-problem': CODECHEF_PROBLEM_BANDS, + 'luogu-difficulty': LUOGU_DIFFICULTY_BANDS, // 注意:key 是洛谷自己的 1–8 难度码 +}; + +export function platformRatingColor(platform, value) { + const bands = PLATFORM_BANDS[platform]; + if (!bands || !Number.isFinite(value) || value < 0) { + return { tone: 'unrated', label: '未评级', range: '待定' }; + } + const [, tone, label, range] = bands.find(([minimum]) => value >= minimum); + return { tone, label, range }; +} +``` + +--- + +## 8. 命名(结算卡上的「平台名 + 每题标签」) + +每个平台对自己的档位叫法都不一样,卡片文案建议照下表直接取: + +| 平台 | 平台名建议 | 这一档叫什么(官方叫法) | 每题标签建议 | +| --- | --- | --- | --- | +| Codeforces | `Codeforces` | 头衔:Newbie / Pupil / Specialist / Expert / Candidate Master / Master / International Master / Grandmaster / International Grandmaster / Legendary Grandmaster | 题目用 `rating`(如 `*1400`),结算用头衔或纯数字 | +| AtCoder | `AtCoder` | 比赛 rating 只有**颜色名**(Gray/Brown/Green/Cyan/Blue/Yellow/Orange/Red),无头衔 | 比赛用颜色名或 rating 数字;题目难度标 `Difficulty`(第三方值) | +| AtCoder Problems | `AtCoder Problems` | 同上颜色名 | `Difficulty 1623`,并注明非官方 | +| LeetCode | `LeetCode` | 比赛档位:**Knight** / **Guardian**(仅两个);题目难度:**Easy / Medium / Hard** | 题目直接印 `Easy/Medium/Hard`;比赛档位印 `Knight`/`Guardian`,别印自造的头衔 | +| 洛谷 | `洛谷` / `Luogu` | 难度:**入门 / 普及- / 普及 / 普及+/提高- / 提高 / 提高+/省选- / 省选/NOI- / NOI/NOI+/CTS**;比赛分:**比赛等级分** | 题目印难度名(中文);比赛印 `等级分 1234` | +| CodeChef | `CodeChef` | 比赛:**N★**(1–7 星)+ Division;题目:**difficulty rating** | 比赛印 `4★`;题目印难度分 | +| TopCoder | `TopCoder` | 只有颜色名,无头衔名 | 印 rating 数字 | +| HackerRank | `HackerRank` | 题目难度:**Easy / Medium / Hard**(部分有 Advanced) | 印难度词;无 rating 可印 | +| CSES | `CSES` | 只有分类名(Introductory / Sorting and Searching / …) | 印分类名,**不要印难度** | +| 牛客 | `牛客` | 无难度字段(本仓库已确认) | 不印难度 | +| Virtual Judge | `Virtual Judge`(或标为来源平台) | 无自有档位 | 按 source judge 走对应表 | + +两处易错点: + +1. **别把 CF 的头衔名搬到别的平台**。AtCoder / TopCoder 只有颜色名,LeetCode 只有 + Knight / Guardian 两个,洛谷只有难度名。凭空造一个「AtCoder Master」是错的。 +2. **红色在不同平台含义相反**:CF / AtCoder / CodeChef 里红是最高档;洛谷里红是**最低档** + (入门)。文案与图例都要跟着平台走。 diff --git a/docs/settlement-shots/00-settlement.png b/docs/settlement-shots/00-settlement.png new file mode 100644 index 0000000..1a9dab5 Binary files /dev/null and b/docs/settlement-shots/00-settlement.png differ diff --git a/docs/settlement-shots/01-rank-ladder.png b/docs/settlement-shots/01-rank-ladder.png new file mode 100644 index 0000000..5926bab Binary files /dev/null and b/docs/settlement-shots/01-rank-ladder.png differ diff --git a/docs/settlement-shots/02-card-tones.png b/docs/settlement-shots/02-card-tones.png new file mode 100644 index 0000000..39fbba0 Binary files /dev/null and b/docs/settlement-shots/02-card-tones.png differ diff --git a/docs/settlement-shots/03-dxrating-tones.png b/docs/settlement-shots/03-dxrating-tones.png new file mode 100644 index 0000000..cf58db1 Binary files /dev/null and b/docs/settlement-shots/03-dxrating-tones.png differ diff --git a/docs/settlement-shots/04-mascots.png b/docs/settlement-shots/04-mascots.png new file mode 100644 index 0000000..d43ac65 Binary files /dev/null and b/docs/settlement-shots/04-mascots.png differ diff --git a/public/algorithm-dx-icon-light.svg b/public/algorithm-dx-icon-light.svg new file mode 100644 index 0000000..c359168 --- /dev/null +++ b/public/algorithm-dx-icon-light.svg @@ -0,0 +1 @@ + diff --git a/public/assets/maimai/gam-rank-a.png b/public/assets/maimai/gam-rank-a.png new file mode 100644 index 0000000..2e3a5f6 Binary files /dev/null and b/public/assets/maimai/gam-rank-a.png differ diff --git a/public/assets/maimai/gam-rank-aa.png b/public/assets/maimai/gam-rank-aa.png new file mode 100644 index 0000000..1574915 Binary files /dev/null and b/public/assets/maimai/gam-rank-aa.png differ diff --git a/public/assets/maimai/gam-rank-aaa.png b/public/assets/maimai/gam-rank-aaa.png new file mode 100644 index 0000000..d2e16e4 Binary files /dev/null and b/public/assets/maimai/gam-rank-aaa.png differ diff --git a/public/assets/maimai/gam-rank-b.png b/public/assets/maimai/gam-rank-b.png new file mode 100644 index 0000000..d2bb9a8 Binary files /dev/null and b/public/assets/maimai/gam-rank-b.png differ diff --git a/public/assets/maimai/gam-rank-bb.png b/public/assets/maimai/gam-rank-bb.png new file mode 100644 index 0000000..248c4b7 Binary files /dev/null and b/public/assets/maimai/gam-rank-bb.png differ diff --git a/public/assets/maimai/gam-rank-bbb.png b/public/assets/maimai/gam-rank-bbb.png new file mode 100644 index 0000000..63ab8ee Binary files /dev/null and b/public/assets/maimai/gam-rank-bbb.png differ diff --git a/public/assets/maimai/gam-rank-c.png b/public/assets/maimai/gam-rank-c.png new file mode 100644 index 0000000..4bf5f27 Binary files /dev/null and b/public/assets/maimai/gam-rank-c.png differ diff --git a/public/assets/maimai/gam-rank-d.png b/public/assets/maimai/gam-rank-d.png new file mode 100644 index 0000000..b8e69ae Binary files /dev/null and b/public/assets/maimai/gam-rank-d.png differ diff --git a/public/assets/maimai/gam-rank-s.png b/public/assets/maimai/gam-rank-s.png new file mode 100644 index 0000000..23a458a Binary files /dev/null and b/public/assets/maimai/gam-rank-s.png differ diff --git a/public/assets/maimai/gam-rank-sp.png b/public/assets/maimai/gam-rank-sp.png new file mode 100644 index 0000000..aa152fa Binary files /dev/null and b/public/assets/maimai/gam-rank-sp.png differ diff --git a/public/assets/maimai/gam-rank-ss.png b/public/assets/maimai/gam-rank-ss.png new file mode 100644 index 0000000..107b8d4 Binary files /dev/null and b/public/assets/maimai/gam-rank-ss.png differ diff --git a/public/assets/maimai/gam-rank-ssp.png b/public/assets/maimai/gam-rank-ssp.png new file mode 100644 index 0000000..7eb5baf Binary files /dev/null and b/public/assets/maimai/gam-rank-ssp.png differ diff --git a/public/assets/maimai/gam-rank-sss.png b/public/assets/maimai/gam-rank-sss.png new file mode 100644 index 0000000..a2e97e5 Binary files /dev/null and b/public/assets/maimai/gam-rank-sss.png differ diff --git a/public/assets/maimai/gam-rank-sssp.png b/public/assets/maimai/gam-rank-sssp.png new file mode 100644 index 0000000..92734a5 Binary files /dev/null and b/public/assets/maimai/gam-rank-sssp.png differ diff --git a/public/assets/maimai/life-base-green.png b/public/assets/maimai/life-base-green.png new file mode 100644 index 0000000..0e98d0d Binary files /dev/null and b/public/assets/maimai/life-base-green.png differ diff --git a/public/assets/maimai/life-base-red.png b/public/assets/maimai/life-base-red.png new file mode 100644 index 0000000..7903fec Binary files /dev/null and b/public/assets/maimai/life-base-red.png differ diff --git a/public/assets/maimai/medal-accepted.png b/public/assets/maimai/medal-accepted.png new file mode 100644 index 0000000..c8af4b4 Binary files /dev/null and b/public/assets/maimai/medal-accepted.png differ diff --git a/public/assets/maimai/medal-cleansolve.png b/public/assets/maimai/medal-cleansolve.png new file mode 100644 index 0000000..570fefa Binary files /dev/null and b/public/assets/maimai/medal-cleansolve.png differ diff --git a/public/assets/maimai/num-26p-outline.png b/public/assets/maimai/num-26p-outline.png new file mode 100644 index 0000000..4b1c14c Binary files /dev/null and b/public/assets/maimai/num-26p-outline.png differ diff --git a/public/assets/maimai/num-26p.png b/public/assets/maimai/num-26p.png new file mode 100644 index 0000000..23d4313 Binary files /dev/null and b/public/assets/maimai/num-26p.png differ diff --git a/public/assets/maimai/num-90p-outline.png b/public/assets/maimai/num-90p-outline.png new file mode 100644 index 0000000..4376753 Binary files /dev/null and b/public/assets/maimai/num-90p-outline.png differ diff --git a/public/assets/maimai/num-90p.png b/public/assets/maimai/num-90p.png new file mode 100644 index 0000000..ad1078c Binary files /dev/null and b/public/assets/maimai/num-90p.png differ diff --git a/public/assets/maimai/num-score-blue.png b/public/assets/maimai/num-score-blue.png new file mode 100644 index 0000000..342b9d9 Binary files /dev/null and b/public/assets/maimai/num-score-blue.png differ diff --git a/public/assets/maimai/num-score-gold.png b/public/assets/maimai/num-score-gold.png new file mode 100644 index 0000000..049d37a Binary files /dev/null and b/public/assets/maimai/num-score-gold.png differ diff --git a/public/assets/maimai/num-score-red.png b/public/assets/maimai/num-score-red.png new file mode 100644 index 0000000..9d7ceea Binary files /dev/null and b/public/assets/maimai/num-score-red.png differ diff --git a/public/assets/maimai/result-bg/result-bg-a.png b/public/assets/maimai/result-bg/result-bg-a.png new file mode 100644 index 0000000..6a5239e Binary files /dev/null and b/public/assets/maimai/result-bg/result-bg-a.png differ diff --git a/public/assets/maimai/result-bg/result-bg-b.png b/public/assets/maimai/result-bg/result-bg-b.png new file mode 100644 index 0000000..e9618ad Binary files /dev/null and b/public/assets/maimai/result-bg/result-bg-b.png differ diff --git a/public/assets/maimai/result-bg/result-bg-s.png b/public/assets/maimai/result-bg/result-bg-s.png new file mode 100644 index 0000000..8010054 Binary files /dev/null and b/public/assets/maimai/result-bg/result-bg-s.png differ diff --git a/public/assets/maimai/result-bg/result-bg-ss.png b/public/assets/maimai/result-bg/result-bg-ss.png new file mode 100644 index 0000000..21d13fc Binary files /dev/null and b/public/assets/maimai/result-bg/result-bg-ss.png differ diff --git a/public/assets/maimai/result-bg/result-bg-sss.png b/public/assets/maimai/result-bg/result-bg-sss.png new file mode 100644 index 0000000..f036897 Binary files /dev/null and b/public/assets/maimai/result-bg/result-bg-sss.png differ diff --git a/public/assets/maimai/result-dani.png b/public/assets/maimai/result-dani.png new file mode 100644 index 0000000..abb5cf8 Binary files /dev/null and b/public/assets/maimai/result-dani.png differ diff --git a/public/assets/maimai/result-random.png b/public/assets/maimai/result-random.png new file mode 100644 index 0000000..71dea32 Binary files /dev/null and b/public/assets/maimai/result-random.png differ diff --git a/public/assets/maimai/score-per-blue.png b/public/assets/maimai/score-per-blue.png new file mode 100644 index 0000000..bd0ae7f Binary files /dev/null and b/public/assets/maimai/score-per-blue.png differ diff --git a/public/assets/maimai/score-per-gold.png b/public/assets/maimai/score-per-gold.png new file mode 100644 index 0000000..707bdf8 Binary files /dev/null and b/public/assets/maimai/score-per-gold.png differ diff --git a/public/assets/maimai/score-per-red.png b/public/assets/maimai/score-per-red.png new file mode 100644 index 0000000..8e43e27 Binary files /dev/null and b/public/assets/maimai/score-per-red.png differ diff --git a/public/assets/maimai/stamp-clear.png b/public/assets/maimai/stamp-clear.png new file mode 100644 index 0000000..b877a64 Binary files /dev/null and b/public/assets/maimai/stamp-clear.png differ diff --git a/public/assets/maimai/stamp-fail.png b/public/assets/maimai/stamp-fail.png new file mode 100644 index 0000000..3c6d8d3 Binary files /dev/null and b/public/assets/maimai/stamp-fail.png differ diff --git a/public/assets/maimai/track-plate.png b/public/assets/maimai/track-plate.png new file mode 100644 index 0000000..7515a67 Binary files /dev/null and b/public/assets/maimai/track-plate.png differ diff --git a/public/assets/mm-parts/LiberationSans.ttf b/public/assets/mm-parts/LiberationSans.ttf new file mode 100644 index 0000000..626dd93 Binary files /dev/null and b/public/assets/mm-parts/LiberationSans.ttf differ diff --git a/public/assets/mm-parts/bed-07.png b/public/assets/mm-parts/bed-07.png new file mode 100644 index 0000000..9fd433c Binary files /dev/null and b/public/assets/mm-parts/bed-07.png differ diff --git a/public/assets/mm-parts/chara-matte.png b/public/assets/mm-parts/chara-matte.png new file mode 100644 index 0000000..1fa5daa Binary files /dev/null and b/public/assets/mm-parts/chara-matte.png differ diff --git a/public/assets/mm-parts/chara-placeholder.png b/public/assets/mm-parts/chara-placeholder.png new file mode 100644 index 0000000..77e927e Binary files /dev/null and b/public/assets/mm-parts/chara-placeholder.png differ diff --git a/public/assets/mm-parts/jacket-1820.png b/public/assets/mm-parts/jacket-1820.png new file mode 100644 index 0000000..051b066 Binary files /dev/null and b/public/assets/mm-parts/jacket-1820.png differ diff --git a/public/assets/mm-parts/navichara-01-p1-02.png b/public/assets/mm-parts/navichara-01-p1-02.png new file mode 100644 index 0000000..5ff8cf2 Binary files /dev/null and b/public/assets/mm-parts/navichara-01-p1-02.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_0.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_0.png new file mode 100644 index 0000000..ddd2acb Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_0.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_1.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_1.png new file mode 100644 index 0000000..ef3a829 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_1.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_10.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_10.png new file mode 100644 index 0000000..af44a78 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_10.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_11.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_11.png new file mode 100644 index 0000000..32cf8e7 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_11.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_12.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_12.png new file mode 100644 index 0000000..ac89c79 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_12.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_13.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_13.png new file mode 100644 index 0000000..a57bc4f Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_13.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_14.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_14.png new file mode 100644 index 0000000..ea12831 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_14.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_2.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_2.png new file mode 100644 index 0000000..eecc0a5 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_2.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_3.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_3.png new file mode 100644 index 0000000..ae27cb5 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_3.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_4.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_4.png new file mode 100644 index 0000000..9951d55 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_4.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_5.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_5.png new file mode 100644 index 0000000..803d24b Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_5.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_6.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_6.png new file mode 100644 index 0000000..52bf685 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_6.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_7.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_7.png new file mode 100644 index 0000000..6bd86f0 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_7.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_8.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_8.png new file mode 100644 index 0000000..80407e6 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_8.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_BSC_9.png b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_9.png new file mode 100644 index 0000000..32a0941 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_BSC_9.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_0.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_0.png new file mode 100644 index 0000000..ff42bb1 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_0.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_1.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_1.png new file mode 100644 index 0000000..da7786c Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_1.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_10.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_10.png new file mode 100644 index 0000000..f05a264 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_10.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_11.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_11.png new file mode 100644 index 0000000..2c16558 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_11.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_12.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_12.png new file mode 100644 index 0000000..adf716b Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_12.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_13.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_13.png new file mode 100644 index 0000000..eaae072 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_13.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_14.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_14.png new file mode 100644 index 0000000..e594e7c Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_14.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_2.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_2.png new file mode 100644 index 0000000..53eacb5 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_2.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_3.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_3.png new file mode 100644 index 0000000..f170dfc Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_3.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_4.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_4.png new file mode 100644 index 0000000..cf238fe Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_4.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_5.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_5.png new file mode 100644 index 0000000..2baa728 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_5.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_6.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_6.png new file mode 100644 index 0000000..97567eb Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_6.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_7.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_7.png new file mode 100644 index 0000000..d2a6c25 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_7.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_8.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_8.png new file mode 100644 index 0000000..a2f5910 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_8.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_EXP_9.png b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_9.png new file mode 100644 index 0000000..10486d2 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_EXP_9.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_0.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_0.png new file mode 100644 index 0000000..e99c963 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_0.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_1.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_1.png new file mode 100644 index 0000000..99763ea Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_1.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_10.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_10.png new file mode 100644 index 0000000..d6e8582 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_10.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_11.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_11.png new file mode 100644 index 0000000..e5fc7ff Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_11.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_12.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_12.png new file mode 100644 index 0000000..97df945 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_12.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_13.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_13.png new file mode 100644 index 0000000..2b7afcf Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_13.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_14.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_14.png new file mode 100644 index 0000000..75b0fa7 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_14.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_2.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_2.png new file mode 100644 index 0000000..844501a Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_2.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_3.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_3.png new file mode 100644 index 0000000..8daf475 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_3.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_4.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_4.png new file mode 100644 index 0000000..6bd5d33 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_4.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_5.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_5.png new file mode 100644 index 0000000..00dfd11 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_5.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_6.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_6.png new file mode 100644 index 0000000..703b9e3 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_6.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_7.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_7.png new file mode 100644 index 0000000..ddfa910 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_7.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_8.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_8.png new file mode 100644 index 0000000..70d40fd Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_8.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_9.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_9.png new file mode 100644 index 0000000..3099b6f Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_9.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_0.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_0.png new file mode 100644 index 0000000..b39c584 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_0.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_1.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_1.png new file mode 100644 index 0000000..c2e214d Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_1.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_10.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_10.png new file mode 100644 index 0000000..110731a Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_10.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_11.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_11.png new file mode 100644 index 0000000..92ff8cd Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_11.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_12.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_12.png new file mode 100644 index 0000000..33b039b Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_12.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_13.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_13.png new file mode 100644 index 0000000..6bf737b Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_13.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_14.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_14.png new file mode 100644 index 0000000..43bb996 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_14.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_2.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_2.png new file mode 100644 index 0000000..9f81bb2 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_2.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_3.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_3.png new file mode 100644 index 0000000..ac5c9e5 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_3.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_4.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_4.png new file mode 100644 index 0000000..8982e2d Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_4.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_5.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_5.png new file mode 100644 index 0000000..1c7f62e Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_5.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_6.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_6.png new file mode 100644 index 0000000..0142c7f Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_6.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_7.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_7.png new file mode 100644 index 0000000..f44b285 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_7.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_8.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_8.png new file mode 100644 index 0000000..0b5cae6 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_8.png differ diff --git a/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_9.png b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_9.png new file mode 100644 index 0000000..6bd05ac Binary files /dev/null and b/public/assets/mm-result/UI_CMN_MusicLevel_MST_Re_9.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p.png b/public/assets/mm-result/UI_CMN_Num_26p.png new file mode 100644 index 0000000..23d4313 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_0.png b/public/assets/mm-result/UI_CMN_Num_26p_0.png new file mode 100644 index 0000000..2f2ecc7 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_0.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_1.png b/public/assets/mm-result/UI_CMN_Num_26p_1.png new file mode 100644 index 0000000..34a4955 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_1.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_10.png b/public/assets/mm-result/UI_CMN_Num_26p_10.png new file mode 100644 index 0000000..ed7e7e9 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_10.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_11.png b/public/assets/mm-result/UI_CMN_Num_26p_11.png new file mode 100644 index 0000000..369f2cc Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_11.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_12.png b/public/assets/mm-result/UI_CMN_Num_26p_12.png new file mode 100644 index 0000000..1cf7870 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_12.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_13.png b/public/assets/mm-result/UI_CMN_Num_26p_13.png new file mode 100644 index 0000000..c259427 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_13.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_14.png b/public/assets/mm-result/UI_CMN_Num_26p_14.png new file mode 100644 index 0000000..12300f4 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_14.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_2.png b/public/assets/mm-result/UI_CMN_Num_26p_2.png new file mode 100644 index 0000000..37878c5 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_2.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_3.png b/public/assets/mm-result/UI_CMN_Num_26p_3.png new file mode 100644 index 0000000..a31b11a Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_3.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_4.png b/public/assets/mm-result/UI_CMN_Num_26p_4.png new file mode 100644 index 0000000..a8a8305 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_4.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_5.png b/public/assets/mm-result/UI_CMN_Num_26p_5.png new file mode 100644 index 0000000..2b7127c Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_5.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_6.png b/public/assets/mm-result/UI_CMN_Num_26p_6.png new file mode 100644 index 0000000..81cce0f Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_6.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_7.png b/public/assets/mm-result/UI_CMN_Num_26p_7.png new file mode 100644 index 0000000..86555af Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_7.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_8.png b/public/assets/mm-result/UI_CMN_Num_26p_8.png new file mode 100644 index 0000000..2562e3b Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_8.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_9.png b/public/assets/mm-result/UI_CMN_Num_26p_9.png new file mode 100644 index 0000000..28f491b Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_9.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline.png new file mode 100644 index 0000000..4b1c14c Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_0.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_0.png new file mode 100644 index 0000000..7733abe Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_0.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_1.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_1.png new file mode 100644 index 0000000..2d53bfc Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_1.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_10.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_10.png new file mode 100644 index 0000000..6392446 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_10.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_11.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_11.png new file mode 100644 index 0000000..5a92a3b Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_11.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_12.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_12.png new file mode 100644 index 0000000..c56a648 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_12.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_13.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_13.png new file mode 100644 index 0000000..1e6e1fe Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_13.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_14.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_14.png new file mode 100644 index 0000000..713d0ed Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_14.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_2.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_2.png new file mode 100644 index 0000000..590a712 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_2.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_3.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_3.png new file mode 100644 index 0000000..59a2147 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_3.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_4.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_4.png new file mode 100644 index 0000000..f16cd57 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_4.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_5.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_5.png new file mode 100644 index 0000000..888f443 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_5.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_6.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_6.png new file mode 100644 index 0000000..1099e81 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_6.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_7.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_7.png new file mode 100644 index 0000000..3d080f3 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_7.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_8.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_8.png new file mode 100644 index 0000000..00ad57b Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_8.png differ diff --git a/public/assets/mm-result/UI_CMN_Num_26p_Outline_9.png b/public/assets/mm-result/UI_CMN_Num_26p_Outline_9.png new file mode 100644 index 0000000..01e9d13 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_Num_26p_Outline_9.png differ diff --git a/public/assets/mm-result/UI_CMN_RSL_TrackNo.png b/public/assets/mm-result/UI_CMN_RSL_TrackNo.png new file mode 100644 index 0000000..2449367 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_RSL_TrackNo.png differ diff --git a/public/assets/mm-result/UI_CMN_RSL_TrackNo_0.png b/public/assets/mm-result/UI_CMN_RSL_TrackNo_0.png new file mode 100644 index 0000000..e74e565 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_RSL_TrackNo_0.png differ diff --git a/public/assets/mm-result/UI_CMN_RSL_TrackNo_1.png b/public/assets/mm-result/UI_CMN_RSL_TrackNo_1.png new file mode 100644 index 0000000..e9a58bd Binary files /dev/null and b/public/assets/mm-result/UI_CMN_RSL_TrackNo_1.png differ diff --git a/public/assets/mm-result/UI_CMN_RSL_TrackNo_2.png b/public/assets/mm-result/UI_CMN_RSL_TrackNo_2.png new file mode 100644 index 0000000..6085594 Binary files /dev/null and b/public/assets/mm-result/UI_CMN_RSL_TrackNo_2.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base.png new file mode 100644 index 0000000..3c95313 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_0.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_0.png new file mode 100644 index 0000000..58f2407 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_0.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_1.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_1.png new file mode 100644 index 0000000..e4ad683 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_1.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_10.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_10.png new file mode 100644 index 0000000..8bb3deb Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_10.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_11.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_11.png new file mode 100644 index 0000000..26c780f Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_11.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_12.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_12.png new file mode 100644 index 0000000..5306708 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_12.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_13.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_13.png new file mode 100644 index 0000000..1fb8d3e Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_13.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_2.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_2.png new file mode 100644 index 0000000..0fd8c96 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_2.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_3.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_3.png new file mode 100644 index 0000000..2f6a195 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_3.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_4.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_4.png new file mode 100644 index 0000000..49b9ef1 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_4.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_5.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_5.png new file mode 100644 index 0000000..72914dd Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_5.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_6.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_6.png new file mode 100644 index 0000000..7b2e0ad Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_6.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_7.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_7.png new file mode 100644 index 0000000..3debbb7 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_7.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_8.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_8.png new file mode 100644 index 0000000..f10a8d0 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_8.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Base_9.png b/public/assets/mm-result/UI_NUM_Score_0001111_Base_9.png new file mode 100644 index 0000000..69518d9 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Base_9.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue.png new file mode 100644 index 0000000..342b9d9 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_0.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_0.png new file mode 100644 index 0000000..ced6aba Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_0.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_1.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_1.png new file mode 100644 index 0000000..50f50e0 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_1.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_10.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_10.png new file mode 100644 index 0000000..7af0909 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_10.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_11.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_11.png new file mode 100644 index 0000000..17bb895 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_11.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_12.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_12.png new file mode 100644 index 0000000..16e542c Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_12.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_13.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_13.png new file mode 100644 index 0000000..a83f912 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_13.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_2.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_2.png new file mode 100644 index 0000000..acfdf61 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_2.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_3.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_3.png new file mode 100644 index 0000000..71e24ba Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_3.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_4.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_4.png new file mode 100644 index 0000000..ecb542d Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_4.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_5.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_5.png new file mode 100644 index 0000000..de3c62c Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_5.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_6.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_6.png new file mode 100644 index 0000000..b37892b Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_6.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_7.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_7.png new file mode 100644 index 0000000..24b4616 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_7.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_8.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_8.png new file mode 100644 index 0000000..bb63284 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_8.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Blue_9.png b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_9.png new file mode 100644 index 0000000..34fab1c Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Blue_9.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold.png new file mode 100644 index 0000000..049d37a Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_0.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_0.png new file mode 100644 index 0000000..fcc25e5 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_0.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_1.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_1.png new file mode 100644 index 0000000..ec31f80 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_1.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_10.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_10.png new file mode 100644 index 0000000..d01258c Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_10.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_11.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_11.png new file mode 100644 index 0000000..1636da3 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_11.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_12.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_12.png new file mode 100644 index 0000000..3647934 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_12.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_13.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_13.png new file mode 100644 index 0000000..63b5004 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_13.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_2.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_2.png new file mode 100644 index 0000000..55d722c Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_2.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_3.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_3.png new file mode 100644 index 0000000..38f2b43 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_3.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_4.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_4.png new file mode 100644 index 0000000..1800a30 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_4.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_5.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_5.png new file mode 100644 index 0000000..698d0e2 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_5.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_6.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_6.png new file mode 100644 index 0000000..cd1eb49 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_6.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_7.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_7.png new file mode 100644 index 0000000..92bbb61 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_7.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_8.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_8.png new file mode 100644 index 0000000..2e90952 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_8.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Gold_9.png b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_9.png new file mode 100644 index 0000000..31acfad Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Gold_9.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red.png new file mode 100644 index 0000000..9d7ceea Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_0.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_0.png new file mode 100644 index 0000000..043ace6 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_0.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_1.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_1.png new file mode 100644 index 0000000..491832b Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_1.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_10.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_10.png new file mode 100644 index 0000000..50a29c1 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_10.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_11.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_11.png new file mode 100644 index 0000000..22cfc7a Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_11.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_12.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_12.png new file mode 100644 index 0000000..45e0b9b Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_12.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_13.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_13.png new file mode 100644 index 0000000..bca49f8 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_13.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_2.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_2.png new file mode 100644 index 0000000..be5e345 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_2.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_3.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_3.png new file mode 100644 index 0000000..0fd0275 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_3.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_4.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_4.png new file mode 100644 index 0000000..cd1b7af Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_4.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_5.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_5.png new file mode 100644 index 0000000..3af9e49 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_5.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_6.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_6.png new file mode 100644 index 0000000..4db4320 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_6.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_7.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_7.png new file mode 100644 index 0000000..114ba98 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_7.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_8.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_8.png new file mode 100644 index 0000000..8b57c4e Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_8.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_0001111_Red_9.png b/public/assets/mm-result/UI_NUM_Score_0001111_Red_9.png new file mode 100644 index 0000000..4d6ef87 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_0001111_Red_9.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base.png new file mode 100644 index 0000000..6c6efb4 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_0.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_0.png new file mode 100644 index 0000000..34b061f Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_0.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_1.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_1.png new file mode 100644 index 0000000..eb4e585 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_1.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_10.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_10.png new file mode 100644 index 0000000..9c27d5f Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_10.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_11.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_11.png new file mode 100644 index 0000000..eb80d87 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_11.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_12.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_12.png new file mode 100644 index 0000000..1c6135a Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_12.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_13.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_13.png new file mode 100644 index 0000000..e77f662 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_13.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_2.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_2.png new file mode 100644 index 0000000..b2913a4 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_2.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_3.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_3.png new file mode 100644 index 0000000..d874057 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_3.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_4.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_4.png new file mode 100644 index 0000000..7af2476 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_4.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_5.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_5.png new file mode 100644 index 0000000..187ec25 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_5.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_6.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_6.png new file mode 100644 index 0000000..ff45163 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_6.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_7.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_7.png new file mode 100644 index 0000000..8a4a46a Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_7.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_8.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_8.png new file mode 100644 index 0000000..c9dec44 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_8.png differ diff --git a/public/assets/mm-result/UI_NUM_Score_1110000_Base_9.png b/public/assets/mm-result/UI_NUM_Score_1110000_Base_9.png new file mode 100644 index 0000000..4a41793 Binary files /dev/null and b/public/assets/mm-result/UI_NUM_Score_1110000_Base_9.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue.png new file mode 100644 index 0000000..5dcf1d4 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_0.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_0.png new file mode 100644 index 0000000..1598cc1 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_0.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_1.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_1.png new file mode 100644 index 0000000..29b1107 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_1.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_10.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_10.png new file mode 100644 index 0000000..5f370f2 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_10.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_11.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_11.png new file mode 100644 index 0000000..9c76c54 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_11.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_12.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_12.png new file mode 100644 index 0000000..65ac589 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_12.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_14.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_14.png new file mode 100644 index 0000000..17a18fe Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_14.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_2.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_2.png new file mode 100644 index 0000000..8c60868 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_2.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_3.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_3.png new file mode 100644 index 0000000..3de20d6 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_3.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_4.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_4.png new file mode 100644 index 0000000..5045357 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_4.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_5.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_5.png new file mode 100644 index 0000000..2b34a60 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_5.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_6.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_6.png new file mode 100644 index 0000000..ff90623 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_6.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_7.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_7.png new file mode 100644 index 0000000..a9bb479 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_7.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_8.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_8.png new file mode 100644 index 0000000..88cf689 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_8.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Blue_9.png b/public/assets/mm-result/UI_Num_Score_1110000_Blue_9.png new file mode 100644 index 0000000..2f4dace Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Blue_9.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold.png new file mode 100644 index 0000000..33411e0 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_0.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_0.png new file mode 100644 index 0000000..45926d5 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_0.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_1.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_1.png new file mode 100644 index 0000000..0d0289e Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_1.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_10.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_10.png new file mode 100644 index 0000000..64ba7a7 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_10.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_11.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_11.png new file mode 100644 index 0000000..f4a80b3 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_11.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_12.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_12.png new file mode 100644 index 0000000..45e55ae Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_12.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_13.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_13.png new file mode 100644 index 0000000..b3dc196 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_13.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_2.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_2.png new file mode 100644 index 0000000..2fc710f Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_2.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_3.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_3.png new file mode 100644 index 0000000..2b5b04e Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_3.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_4.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_4.png new file mode 100644 index 0000000..0a6df20 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_4.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_5.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_5.png new file mode 100644 index 0000000..c6d0753 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_5.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_6.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_6.png new file mode 100644 index 0000000..f28ed59 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_6.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_7.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_7.png new file mode 100644 index 0000000..c103dcd Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_7.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_8.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_8.png new file mode 100644 index 0000000..4eb12c6 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_8.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Gold_9.png b/public/assets/mm-result/UI_Num_Score_1110000_Gold_9.png new file mode 100644 index 0000000..8f17294 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Gold_9.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red.png b/public/assets/mm-result/UI_Num_Score_1110000_Red.png new file mode 100644 index 0000000..3884b4b Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_0.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_0.png new file mode 100644 index 0000000..1c1f184 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_0.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_1.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_1.png new file mode 100644 index 0000000..b0c4390 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_1.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_10.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_10.png new file mode 100644 index 0000000..318f3d9 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_10.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_11.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_11.png new file mode 100644 index 0000000..ec96d04 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_11.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_12.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_12.png new file mode 100644 index 0000000..a645148 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_12.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_13.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_13.png new file mode 100644 index 0000000..257736c Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_13.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_2.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_2.png new file mode 100644 index 0000000..ca8eb4f Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_2.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_3.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_3.png new file mode 100644 index 0000000..3b1ebea Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_3.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_4.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_4.png new file mode 100644 index 0000000..31ad8f4 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_4.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_5.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_5.png new file mode 100644 index 0000000..213b464 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_5.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_6.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_6.png new file mode 100644 index 0000000..83a9960 Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_6.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_7.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_7.png new file mode 100644 index 0000000..ffd0a5d Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_7.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_8.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_8.png new file mode 100644 index 0000000..d9d88de Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_8.png differ diff --git a/public/assets/mm-result/UI_Num_Score_1110000_Red_9.png b/public/assets/mm-result/UI_Num_Score_1110000_Red_9.png new file mode 100644 index 0000000..d08c3ba Binary files /dev/null and b/public/assets/mm-result/UI_Num_Score_1110000_Red_9.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_ADV.png b/public/assets/mm-result/UI_RSL_MBase_ADV.png new file mode 100644 index 0000000..7daa69b Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_ADV.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_BSC.png b/public/assets/mm-result/UI_RSL_MBase_BSC.png new file mode 100644 index 0000000..075a09f Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_BSC.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_DMY.png b/public/assets/mm-result/UI_RSL_MBase_DMY.png new file mode 100644 index 0000000..907df2e Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_DMY.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_EXP.png b/public/assets/mm-result/UI_RSL_MBase_EXP.png new file mode 100644 index 0000000..6567767 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_EXP.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_MST.png b/public/assets/mm-result/UI_RSL_MBase_MST.png new file mode 100644 index 0000000..ab011ed Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_MST.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_MST_Re.png b/public/assets/mm-result/UI_RSL_MBase_MST_Re.png new file mode 100644 index 0000000..bc08198 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_MST_Re.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Mini_ADV.png b/public/assets/mm-result/UI_RSL_MBase_Mini_ADV.png new file mode 100644 index 0000000..2df0615 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Mini_ADV.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Mini_BSC.png b/public/assets/mm-result/UI_RSL_MBase_Mini_BSC.png new file mode 100644 index 0000000..2a709ee Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Mini_BSC.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Mini_DMY.png b/public/assets/mm-result/UI_RSL_MBase_Mini_DMY.png new file mode 100644 index 0000000..ccdefb2 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Mini_DMY.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Mini_EXP.png b/public/assets/mm-result/UI_RSL_MBase_Mini_EXP.png new file mode 100644 index 0000000..48542f1 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Mini_EXP.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Mini_MST.png b/public/assets/mm-result/UI_RSL_MBase_Mini_MST.png new file mode 100644 index 0000000..e30830a Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Mini_MST.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Mini_MST_Re.png b/public/assets/mm-result/UI_RSL_MBase_Mini_MST_Re.png new file mode 100644 index 0000000..6eea548 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Mini_MST_Re.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Mini_UTG.png b/public/assets/mm-result/UI_RSL_MBase_Mini_UTG.png new file mode 100644 index 0000000..51302a8 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Mini_UTG.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Parts_01.png b/public/assets/mm-result/UI_RSL_MBase_Parts_01.png new file mode 100644 index 0000000..abd6d56 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Parts_01.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Parts_02.png b/public/assets/mm-result/UI_RSL_MBase_Parts_02.png new file mode 100644 index 0000000..3804f93 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Parts_02.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Parts_UTG.png b/public/assets/mm-result/UI_RSL_MBase_Parts_UTG.png new file mode 100644 index 0000000..1476ebd Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Parts_UTG.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_Parts_UTG_2p.png b/public/assets/mm-result/UI_RSL_MBase_Parts_UTG_2p.png new file mode 100644 index 0000000..39fc92a Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_Parts_UTG_2p.png differ diff --git a/public/assets/mm-result/UI_RSL_MBase_UTG.png b/public/assets/mm-result/UI_RSL_MBase_UTG.png new file mode 100644 index 0000000..116b165 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_MBase_UTG.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_A.png b/public/assets/mm-result/UI_RSL_Rank_A.png new file mode 100644 index 0000000..cfaf95c Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_A.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_01.png b/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_01.png new file mode 100644 index 0000000..42c8014 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_02.png b/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_02.png new file mode 100644 index 0000000..cc89993 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_02.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_03.png b/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_03.png new file mode 100644 index 0000000..bdbd088 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_03.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_04.png b/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_04.png new file mode 100644 index 0000000..744c61a Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_A_Effect_Piece_04.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_A_White.png b/public/assets/mm-result/UI_RSL_Rank_A_White.png new file mode 100644 index 0000000..1681a73 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_A_White.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_B.png b/public/assets/mm-result/UI_RSL_Rank_B.png new file mode 100644 index 0000000..8d11889 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_B.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_01.png b/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_01.png new file mode 100644 index 0000000..fd27020 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_02.png b/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_02.png new file mode 100644 index 0000000..6fedf9d Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_02.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_03.png b/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_03.png new file mode 100644 index 0000000..3694748 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_03.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_04.png b/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_04.png new file mode 100644 index 0000000..5645887 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_B_Effect_Piece_04.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_B_White.png b/public/assets/mm-result/UI_RSL_Rank_B_White.png new file mode 100644 index 0000000..1d94de5 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_B_White.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_C.png b/public/assets/mm-result/UI_RSL_Rank_C.png new file mode 100644 index 0000000..38ea5f1 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_C.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_D.png b/public/assets/mm-result/UI_RSL_Rank_D.png new file mode 100644 index 0000000..5e4deb4 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_D.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_EF_Circle.png b/public/assets/mm-result/UI_RSL_Rank_EF_Circle.png new file mode 100644 index 0000000..4c65740 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_EF_Circle.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Effect_01.png b/public/assets/mm-result/UI_RSL_Rank_Effect_01.png new file mode 100644 index 0000000..216e641 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Effect_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Effect_02.png b/public/assets/mm-result/UI_RSL_Rank_Effect_02.png new file mode 100644 index 0000000..d6bcbb3 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Effect_02.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Effect_03.png b/public/assets/mm-result/UI_RSL_Rank_Effect_03.png new file mode 100644 index 0000000..32c223d Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Effect_03.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Effect_Circle_01.png b/public/assets/mm-result/UI_RSL_Rank_Effect_Circle_01.png new file mode 100644 index 0000000..cd37a21 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Effect_Circle_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Effect_Line_01.png b/public/assets/mm-result/UI_RSL_Rank_Effect_Line_01.png new file mode 100644 index 0000000..5f1ff73 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Effect_Line_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Effect_Line_02.png b/public/assets/mm-result/UI_RSL_Rank_Effect_Line_02.png new file mode 100644 index 0000000..40134aa Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Effect_Line_02.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Effect_Radiation.png b/public/assets/mm-result/UI_RSL_Rank_Effect_Radiation.png new file mode 100644 index 0000000..d6055fa Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Effect_Radiation.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Effect_Slide_01.png b/public/assets/mm-result/UI_RSL_Rank_Effect_Slide_01.png new file mode 100644 index 0000000..4fad4c4 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Effect_Slide_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Plus.png b/public/assets/mm-result/UI_RSL_Rank_Plus.png new file mode 100644 index 0000000..8c95641 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Plus.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_Plus_White.png b/public/assets/mm-result/UI_RSL_Rank_Plus_White.png new file mode 100644 index 0000000..4ba796e Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_Plus_White.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S.png b/public/assets/mm-result/UI_RSL_Rank_S.png new file mode 100644 index 0000000..5623099 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_Blue.png b/public/assets/mm-result/UI_RSL_Rank_S_Blue.png new file mode 100644 index 0000000..fd6ac72 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_Blue.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_Effect_01.png b/public/assets/mm-result/UI_RSL_Rank_S_Effect_01.png new file mode 100644 index 0000000..5dd7c0d Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_Effect_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_01.png b/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_01.png new file mode 100644 index 0000000..a3ecba4 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_02.png b/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_02.png new file mode 100644 index 0000000..5d89db9 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_02.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_03.png b/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_03.png new file mode 100644 index 0000000..d01fbe3 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_03.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_04.png b/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_04.png new file mode 100644 index 0000000..e09c6b6 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_Effect_Piece_04.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_Red.png b/public/assets/mm-result/UI_RSL_Rank_S_Red.png new file mode 100644 index 0000000..e28b28f Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_Red.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_White.png b/public/assets/mm-result/UI_RSL_Rank_S_White.png new file mode 100644 index 0000000..58b063b Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_White.png differ diff --git a/public/assets/mm-result/UI_RSL_Rank_S_Yellow.png b/public/assets/mm-result/UI_RSL_Rank_S_Yellow.png new file mode 100644 index 0000000..5623099 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Rank_S_Yellow.png differ diff --git a/public/assets/mm-result/UI_RSL_Score_Per_Blue.png b/public/assets/mm-result/UI_RSL_Score_Per_Blue.png new file mode 100644 index 0000000..cfc4fdf Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Score_Per_Blue.png differ diff --git a/public/assets/mm-result/UI_RSL_Score_Per_Gold.png b/public/assets/mm-result/UI_RSL_Score_Per_Gold.png new file mode 100644 index 0000000..228bd3b Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Score_Per_Gold.png differ diff --git a/public/assets/mm-result/UI_RSL_Score_Per_Red.png b/public/assets/mm-result/UI_RSL_Score_Per_Red.png new file mode 100644 index 0000000..5d4ba4c Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Score_Per_Red.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_01_01.png b/public/assets/mm-result/UI_RSL_Text_Clear_01_01.png new file mode 100644 index 0000000..157001a Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_01_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_01_02.png b/public/assets/mm-result/UI_RSL_Text_Clear_01_02.png new file mode 100644 index 0000000..2f297bb Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_01_02.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_01_03.png b/public/assets/mm-result/UI_RSL_Text_Clear_01_03.png new file mode 100644 index 0000000..aaa5396 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_01_03.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_01_04.png b/public/assets/mm-result/UI_RSL_Text_Clear_01_04.png new file mode 100644 index 0000000..536f7b7 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_01_04.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_01_05.png b/public/assets/mm-result/UI_RSL_Text_Clear_01_05.png new file mode 100644 index 0000000..11707cb Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_01_05.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_01_06.png b/public/assets/mm-result/UI_RSL_Text_Clear_01_06.png new file mode 100644 index 0000000..c36e1ee Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_01_06.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_02_01.png b/public/assets/mm-result/UI_RSL_Text_Clear_02_01.png new file mode 100644 index 0000000..157001a Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_02_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_02_02.png b/public/assets/mm-result/UI_RSL_Text_Clear_02_02.png new file mode 100644 index 0000000..2f297bb Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_02_02.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_02_03.png b/public/assets/mm-result/UI_RSL_Text_Clear_02_03.png new file mode 100644 index 0000000..aaa5396 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_02_03.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_02_04.png b/public/assets/mm-result/UI_RSL_Text_Clear_02_04.png new file mode 100644 index 0000000..536f7b7 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_02_04.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_02_05.png b/public/assets/mm-result/UI_RSL_Text_Clear_02_05.png new file mode 100644 index 0000000..11707cb Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_02_05.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_02_06.png b/public/assets/mm-result/UI_RSL_Text_Clear_02_06.png new file mode 100644 index 0000000..c36e1ee Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_02_06.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_03_01.png b/public/assets/mm-result/UI_RSL_Text_Clear_03_01.png new file mode 100644 index 0000000..157001a Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_03_01.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_03_02.png b/public/assets/mm-result/UI_RSL_Text_Clear_03_02.png new file mode 100644 index 0000000..2f297bb Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_03_02.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_03_03.png b/public/assets/mm-result/UI_RSL_Text_Clear_03_03.png new file mode 100644 index 0000000..aaa5396 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_03_03.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_03_04.png b/public/assets/mm-result/UI_RSL_Text_Clear_03_04.png new file mode 100644 index 0000000..536f7b7 Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_03_04.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_03_05.png b/public/assets/mm-result/UI_RSL_Text_Clear_03_05.png new file mode 100644 index 0000000..11707cb Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_03_05.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_Clear_03_06.png b/public/assets/mm-result/UI_RSL_Text_Clear_03_06.png new file mode 100644 index 0000000..c36e1ee Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_Clear_03_06.png differ diff --git a/public/assets/mm-result/UI_RSL_Text_TRACK.png b/public/assets/mm-result/UI_RSL_Text_TRACK.png new file mode 100644 index 0000000..53f2feb Binary files /dev/null and b/public/assets/mm-result/UI_RSL_Text_TRACK.png differ diff --git a/public/assets/night/MANIFEST.txt b/public/assets/night/MANIFEST.txt new file mode 100644 index 0000000..c26c8aa --- /dev/null +++ b/public/assets/night/MANIFEST.txt @@ -0,0 +1,20 @@ +source: public/vendor/maimai/img/prism/bg/ (PRiSM PLUS era, recoloured offline) +generated: one-off local tool, not in the repo (see README.md) +aurora-pc.png 197598 18a6832610a9e21f3a3bb1a4c7e185b7 +aurora-sp.png 145513 3cde15f7d5434c57ec879bb231f97d03 +cloud-back-c.png 8409 e531b5609f02a493988a909c86bbefd8 +cloud-back-l.png 9202 6115d9c264af03399bae8424e31b2942 +cloud-back-r.png 24061 e94f971ef5118e59a200ff43f92acd1a +cloud-front-c.png 12656 3092cc9a619adcff2af05e40d8ca8438 +cloud-front-l.png 14177 cd7ad6c0810d073624ff7bbc4f3b907c +cloud-front-r.png 14604 80ad1fd499ea7cfccdbdacf9501cc958 +pattern.png 391 b6d4555a3ef84dc7254f0a2fae1ea292 +rainbow-back-sp.png 20168 4f87fa4e84e99c13cb2e5eeb8c5ae34f +rainbow-bottom-pc.png 45950 d8030f1827eb986ba60d92bc14be7c60 +rainbow-bottom-sp.png 28625 828af06998ae40d221717ddf672d2746 +rainbow-pc.png 30680 ef0394046890f7ada7c6fe734739cbf8 +rainbow-sp.png 27202 96b51196cc94ca6fff993d745cc750e5 +shines-pc.png 13572 632fde21eee7ebdb3e66847f92f2d7a2 +shines-sp.png 12218 8c6c281587dc92f6b232d7996dc804fb +sky-frame.png 449 9fb8230b6de747cbe4476fe9058b0221 +sky.png 405 863652de17136067c9ac1dbc6032111a diff --git a/public/assets/night/README.md b/public/assets/night/README.md new file mode 100644 index 0000000..9ecc79e --- /dev/null +++ b/public/assets/night/README.md @@ -0,0 +1,34 @@ +# assets/night —— 夜版背景贴图 + +夜版背景用的素材:结构和参数都还是官网那份 `style.css`,只是把 `vendor/maimai/img/prism/bg/` +里那几张贴图按夜色重画了一遍。谁换哪一张、在哪个断点换,写在 `public/theme-night.css` 里。 + +## 来源与做法 + +- 底图来自 `public/vendor/maimai/img/prism/bg/**`(官方原图,未改动;字节与 MD5 见那份 MANIFEST)。 +- 重上色是离线做的一次性步骤:按原图的亮度分层,把白/亮部压成夜蓝、保留原来的粉/橙描边。 + 做法与每个参数的来由写在生成脚本的注释里;生成脚本是一次性的本地工具,没有进仓库。 +- **带 `-sp` / `-pc` 的素材必须跟着官方的断点走**:官方在基础规则里用 `_sp`、在 + `@media (min-width:769px)` 里换成 `_pc`(尺寸也各不相同)。只出一份的话,宽屏会拿手机那张 + 顶 PC 那格 —— 彩虹两端那片云的 SP 版是被图片边界切平的窄裁版(左右两边 85% / 44% 的像素 + 是实心的),铺进 PC 那格就会在画面中间露出两条平直的切边。 +- 三处贴图在夜里额外淡了边,都是「官方素材被图片边界切平、白天靠底色看不出来、夜里露出来」这一类: + 彩虹两端那片云的 PC 版底边淡 12%、曙光两端各淡 4%(`repeat-x` 的接缝)、彩虹后面那片云两端各淡 6%。 +- 月亮和流星直接用官方素材,没有派生版本(夜里它们本来就该是最亮的)。 + +## 文件 + +| 文件 | 对应官方贴图 | 说明 | +| --- | --- | --- | +| `sky-frame.png` | — | `.bg` 圆角外那圈底色(官方是 CSS 渐变,这里做成一条竖向贴图) | +| `sky.png` | — | `.bg--gradient` 的内层渐变,同上 | +| `pattern.png` | `bg_pattern.png` | 渐变上的图案叠加 | +| `shines-sp.png` / `shines-pc.png` | `bg_shines_sp/_pc.png` | 闪点(两张长宽比完全不同) | +| `cloud-back-{l,c,r}.png` | `bg_cloud_back_*.png` | 三片远云:白体换夜蓝,暖色描边保留 | +| `cloud-front-{l,c,r}.png` | `bg_cloud_front_*.png` | 三片近云,同上 | +| `rainbow-bottom-sp.png` / `-pc.png` | `bg_rainbow_btm_sp/_pc.png` | 彩虹两端那两片云(PC 版底边淡出) | +| `rainbow-back-sp.png` | `bg_rainbow_back_sp.png` | 彩虹后面那片云(官方只有一张,两端淡出) | +| `rainbow-sp.png` / `rainbow-pc.png` | `bg_rainbow_sp/_pc.png` | 月虹(降饱和) | +| `aurora-sp.png` / `aurora-pc.png` | `bg_aurora_sp/_pc.png` | 曙光,1:1 重上色,两端各淡 4% | + +`MANIFEST.txt` 记录每个文件的字节数与 MD5,跟 `vendor/maimai/img/MANIFEST.txt` 同一个格式。 \ No newline at end of file diff --git a/public/assets/night/aurora-pc.png b/public/assets/night/aurora-pc.png new file mode 100644 index 0000000..08e0fb8 Binary files /dev/null and b/public/assets/night/aurora-pc.png differ diff --git a/public/assets/night/aurora-sp.png b/public/assets/night/aurora-sp.png new file mode 100644 index 0000000..649001b Binary files /dev/null and b/public/assets/night/aurora-sp.png differ diff --git a/public/assets/night/cloud-back-c.png b/public/assets/night/cloud-back-c.png new file mode 100644 index 0000000..24d7145 Binary files /dev/null and b/public/assets/night/cloud-back-c.png differ diff --git a/public/assets/night/cloud-back-l.png b/public/assets/night/cloud-back-l.png new file mode 100644 index 0000000..4388e46 Binary files /dev/null and b/public/assets/night/cloud-back-l.png differ diff --git a/public/assets/night/cloud-back-r.png b/public/assets/night/cloud-back-r.png new file mode 100644 index 0000000..17cd71d Binary files /dev/null and b/public/assets/night/cloud-back-r.png differ diff --git a/public/assets/night/cloud-front-c.png b/public/assets/night/cloud-front-c.png new file mode 100644 index 0000000..4eced6b Binary files /dev/null and b/public/assets/night/cloud-front-c.png differ diff --git a/public/assets/night/cloud-front-l.png b/public/assets/night/cloud-front-l.png new file mode 100644 index 0000000..bd9bb01 Binary files /dev/null and b/public/assets/night/cloud-front-l.png differ diff --git a/public/assets/night/cloud-front-r.png b/public/assets/night/cloud-front-r.png new file mode 100644 index 0000000..b284226 Binary files /dev/null and b/public/assets/night/cloud-front-r.png differ diff --git a/public/assets/night/pattern.png b/public/assets/night/pattern.png new file mode 100644 index 0000000..7a605d4 Binary files /dev/null and b/public/assets/night/pattern.png differ diff --git a/public/assets/night/rainbow-back-sp.png b/public/assets/night/rainbow-back-sp.png new file mode 100644 index 0000000..9b11630 Binary files /dev/null and b/public/assets/night/rainbow-back-sp.png differ diff --git a/public/assets/night/rainbow-bottom-pc.png b/public/assets/night/rainbow-bottom-pc.png new file mode 100644 index 0000000..1c94d0e Binary files /dev/null and b/public/assets/night/rainbow-bottom-pc.png differ diff --git a/public/assets/night/rainbow-bottom-sp.png b/public/assets/night/rainbow-bottom-sp.png new file mode 100644 index 0000000..fcc718b Binary files /dev/null and b/public/assets/night/rainbow-bottom-sp.png differ diff --git a/public/assets/night/rainbow-pc.png b/public/assets/night/rainbow-pc.png new file mode 100644 index 0000000..3e7b308 Binary files /dev/null and b/public/assets/night/rainbow-pc.png differ diff --git a/public/assets/night/rainbow-sp.png b/public/assets/night/rainbow-sp.png new file mode 100644 index 0000000..b91d9e4 Binary files /dev/null and b/public/assets/night/rainbow-sp.png differ diff --git a/public/assets/night/shines-pc.png b/public/assets/night/shines-pc.png new file mode 100644 index 0000000..c287668 Binary files /dev/null and b/public/assets/night/shines-pc.png differ diff --git a/public/assets/night/shines-sp.png b/public/assets/night/shines-sp.png new file mode 100644 index 0000000..3a49169 Binary files /dev/null and b/public/assets/night/shines-sp.png differ diff --git a/public/assets/night/sky-frame.png b/public/assets/night/sky-frame.png new file mode 100644 index 0000000..0fa05d8 Binary files /dev/null and b/public/assets/night/sky-frame.png differ diff --git a/public/assets/night/sky.png b/public/assets/night/sky.png new file mode 100644 index 0000000..96a03ab Binary files /dev/null and b/public/assets/night/sky.png differ diff --git a/public/bg.html b/public/bg.html new file mode 100644 index 0000000..bacd4d4 --- /dev/null +++ b/public/bg.html @@ -0,0 +1,95 @@ + + + + + +maimai DX · PRiSM PLUS 动态背景 + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/public/cf-rating-colors.js b/public/cf-rating-colors.js index 1590de5..aaad4df 100644 --- a/public/cf-rating-colors.js +++ b/public/cf-rating-colors.js @@ -13,3 +13,20 @@ export function cfRatingColor(rating) { const [, tone, label, range] = bands.find(([minimum])=>rating>=minimum); return { tone, label, range }; } + +/** + * 每一档 tone 对应的【主色 + 深色】,段位结算页逐题卡片(.dan-track-frame 的 + * --tc1 / --tc2)用的就是这一对。题目卡也走同一张表,避免两处各写一套色。 + * 顺序和上面 bands 一致:红 > 橙 > 紫 > 蓝 > 青 > 绿 > 灰;未评级与灰色同色。 + */ +export const CF_TONE_COLORS = { + red: ['#e8564f', '#a3211c'], orange: ['#f08c3a', '#a85a12'], + violet: ['#8b6cf0', '#4a2fb0'], blue: ['#4a7fe0', '#1e3f96'], + cyan: ['#3fa8c4', '#166b83'], green: ['#4aa85c', '#1d6b30'], + gray: ['#7b8391', '#3f4652'], unrated: ['#7b8391', '#3f4652'], +}; + +/** tone → [主色, 深色],认不出来的一律按未评级 */ +export function cfToneColors(tone) { + return CF_TONE_COLORS[tone] ?? CF_TONE_COLORS.unrated; +} diff --git a/public/circle.html b/public/circle.html index 479cc26..b8ad56b 100644 --- a/public/circle.html +++ b/public/circle.html @@ -1,14 +1,16 @@ -题友圈 · Algorithm DX +题友圈 · Algorithm DX + - + +
TOGETHER, ONE PROBLEM AT A TIME

每一份坚持,都有回响。

看看关注的人在挑战什么,也为自己的下一题寻找灵感。

+ 添加题友
@@ -24,4 +30,6 @@

题友的新鲜事

-
+TOP + + diff --git a/public/dan.css b/public/dan.css index fd3d0bc..6eddaaa 100644 --- a/public/dan.css +++ b/public/dan.css @@ -138,144 +138,261 @@ .dx-result-time .dx-result-limit { color: var(--muted); font-size: .62rem; } .dx-result-time { flex-direction: column; gap: 1px; } -/* ---------- 段位認定结算页 ---------- - * 版式照原作段位認定结算(Process/Course/CourseResultProcess): - * 奶油色纸盘 + 两侧水引红绳 + 每道一个「可」印 + 竖排逐曲卡 - * (封面 / TRACK 0n / 难度 / 曲名 / 达成率 / LV / DX分数) - * + 底部「十段」框 · 绿心 · 总达成率 · DX分数 + 「好」按钮。 - * 逐个对得上素材包 resources.assets 里的 UI_DNM_*:那枚「可」印是 UI_DNM_Stamp 的 - * Clear / NoClear,两侧绳子是 UI_DNM_Separator,底部那块是 UI_DNM_DaniData。 - * 贴图没有从素材包里搬:全部用 CSS 重画,避免把游戏美术带进这个 MIT 仓库。 - * 盘面用大圆角而不是正圆 —— 三道题卡竖排进正圆,在常见宽度下会挤到不可读。 +/* ---------- 段位認定结算页(原作底图 + 官方组件按官方坐标拼) ---------- + * 版式不是照感觉摆的,是从 resources.assets 的 CourseResultProcess 预制体里量出来的 + * (dump-layout.py 读每个 RectTransform 的 anchors / anchoredPosition / pivot / sizeDelta, + * 再按「美术坐标 = 屏幕中心 + 局部位置」换成 980×928 底图上的像素)。关键坐标: + * 逐题行 x 267–843(576 = 官方的 640 × 0.9),第 1 行 y 152–276,行距 122,共四行 + * 可/不可印 108×108(UI_DNM_Icon_Result 的原始尺寸),圆心 x 203、第 1 行圆心 y 212 + * 底部白框(原作放段位名牌那一格)x 85–310 / y 680–880 + * 心(原作剩余生命那一格)100×100,圆心 (434, 790) + * 总达成率 / DX分数右下角一区:x ~470–965 / y ~620–845 + * 组件也全是官方素材,不用 CSS 画: + * 数字 = UI_CMN_Num_26p/70p/90p 图集逐格切,配同布局的 _Outline 剪影层叠出描边 + * 达成率 = UI_NUM_Score_0001111_{Blue,Gold,Red} 三色分数图集(按分数换色)+ 官方彩色「%」 + * (同名那套 Base 是空图集,不能用;「%」字形也正好只有三张 ⇒ 官方就是三档) + * 评级 = UI_CMN_TabTitle_Rank_*,心 = UI_DNM_Base_Life_01/03,DX 标签 = UI_RSL_DXScore_Base + * 底图 = UI_DNM_Result_Base_01(段位認定)/ _03(ランダム段位認定),行底板 = ..._musicBase_01 + * 唯一的例外是逐题行的**乐曲边框**:官方那张 `UI_CMN_RSL_KopMBase_*` 把难度色与难度名 + * 烘死在图里(五种难度各一张),套不上我们自己的 CF 分档,所以照它逐像素量出来的版式 + * 重画一层(见下面的「逐题行」一节),颜色由 CF 分档给。 + * 素材来源、量法与重新提取见 docs/dan.md「结算页的素材来源」与 + * scripts/extract-maimai-assets.py。 + * + * 盘面锁死 980×928、内部一律绝对像素:缩放交给浏览器对整张图等比缩放, + * 于是「一个数字多宽」这类关系到还原度的结论可以直接抄官方数字。 */ -/* dialog 默认是 shrink-to-fit,只写 max-width 撑不宽,这里必须给 width。 */ -.dan-result { border: 0; padding: 0; background: transparent; width: min(1040px, 96vw); max-width: none; color: #2b1c12; } -.dan-result::backdrop { background: #05070ce0; } -.dan-result-sheet { - position: relative; overflow: auto; max-height: 90vh; - background: radial-gradient(120% 80% at 50% -10%, #fffaf0, #f7e7cd 55%, #eed7b4), #f7e7cd; - border: 9px solid #d8461f; border-radius: 46px; - box-shadow: inset 0 0 0 3px #f7b23f, inset 0 0 0 7px #d8461f, inset 0 0 60px #e8b98155, 0 22px 60px #000b; - padding: 20px 46px 22px; display: grid; gap: 11px; justify-items: center; -} -.dan-result[data-verdict="failed"] .dan-result-sheet { border-color: #8c2b1a; } -.dan-result[data-verdict="abandoned"] .dan-result-sheet { border-color: #8a8578; } -.dan-result-close { position: absolute; top: 12px; right: 18px; padding: 0; font-size: 1.5rem; line-height: 1; color: #a8623a; background: none; border: 0; cursor: pointer; z-index: 2; } -.dan-result-close:hover { color: #d8461f; } -.dan-result-title { margin: 2px 0 0; font-size: 1.85rem; font-weight: 900; letter-spacing: .22em; color: #33200f; text-shadow: 0 1px 0 #fff; } -.dan-result-sub { margin: 0 0 2px; font-size: .8rem; color: #9a7550; text-align: center; } +/* dialog 默认 shrink-to-fit,只写 max-width 撑不宽,这里必须给 width。 */ +.dan-result { border: 0; padding: 0; background: transparent; width: min(1000px, 96vw); max-width: none; color: #23324c; } +.dan-result::backdrop { background: #05070ce8; } +/* 窄屏不把 980px 的成品屏压扁,而是让盘面横向滚动:压扁了字没法读,也谈不上还原。 */ +.dan-result-sheet { overflow: auto; max-height: 92vh; border-radius: 18px; } +.dan-result-body { display: grid; gap: 11px; width: 980px; } -/* 两侧水引(UI_DNM_Separator):上端蝴蝶结 + 竖绳 + 下端流苏 */ -.dan-cord { position: absolute; top: 16px; bottom: 16px; width: 30px; pointer-events: none; } -.dan-cord.left { left: 14px; } -.dan-cord.right { right: 14px; } -.dan-cord::before { - content: ''; position: absolute; left: 50%; top: 34px; bottom: 36px; width: 5px; transform: translateX(-50%); - background: linear-gradient(#ff9d7a, #e0402a 40%, #c9381f); border-radius: 3px; -} -.dan-cord::after { - content: ''; position: absolute; left: 50%; top: 8px; width: 22px; height: 22px; - transform: translateX(-50%) rotate(45deg); background: #f2603c; border-radius: 4px 13px 6px 13px; - box-shadow: inset 0 0 0 2px #ffd2bd, 0 2px 5px #0004; -} -.dan-cord i { - position: absolute; left: 50%; bottom: 6px; width: 20px; height: 30px; transform: translateX(-50%); - background: repeating-linear-gradient(90deg, #e0402a 0 2px, transparent 2px 5px); +.dan-result-art { + position: relative; width: 980px; height: 928px; overflow: hidden; border-radius: 18px; + background: #f7f1e7 url('/assets/maimai/result-dani.png') 0 0 / 100% 100% no-repeat; + box-shadow: 0 18px 50px #000a; } +.dan-result-art[data-art="random"] { background-image: url('/assets/maimai/result-random.png'); } +/* 980×928 的设计尺寸舞台:所有组件在这层里用官方像素坐标绝对定位。 */ +.dan-result-stage { position: absolute; inset: 0; } + +.dan-result-close { position: absolute; top: 10px; right: 16px; z-index: 3; padding: 0; font-size: 1.5rem; line-height: 1; color: #2b6cb8; background: none; border: 0; cursor: pointer; } +.dan-result-close:hover { color: #d8461f; } +/* 页头那行字烘焙在底图里,这个 h2 只留给读屏(id 仍挂在 dialog 的 aria-labelledby 上)。 */ +.dan-result-title { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } +/* 这一轮的范围说明放在成品屏外面(深色底上):原作那张纸上没有放这行字的位置, + * 硬塞进去就只能压在页头或第一行上。 */ +.dan-result-sub { margin: 0; justify-self: center; text-align: center; font-size: .8rem; line-height: 1.6; color: #c9d2de; } -/* 逐曲卡:左边「可」印(UI_DNM_Stamp),右边一条难度配色的横卡 */ -.dan-result-tracks { display: grid; gap: 7px; width: 100%; margin: 0; padding: 0; list-style: none; } -.dan-track { display: grid; grid-template-columns: 46px minmax(0, 1fr); gap: 9px; align-items: center; } +/* ---------- 官方数字贴图 ---------- + * 每格是一个 ,里面两层 :官方 _Outline 剪影(当遮罩上深色 = 描边)+ 官方填充图集。 + * 两层都是绝对定位,DOM 顺序决定填充盖在描边之上。图集本身是纯白剪影, + * 颜色是运行时给的 —— 和游戏里一样。 */ +.dan-num { display: inline-flex; align-items: center; } +/* 图集每格都留了空白边,照格宽排会读成「一个字一个字分开」。官方数字是密排的, + * 所以 numText 会把「格宽 − 步进」写成 --dan-num-trim,每格右边收回这么多; + * 最后一格不收回,免得数字尾巴多出一段空白。 */ +.dan-num-char { position: relative; display: block; flex: none; margin-right: calc(-1 * var(--dan-num-trim, 0px)); } +.dan-num-char.dan-num-last { margin-right: 0; } +.dan-num-char > b { position: absolute; inset: 0; display: block; background-repeat: no-repeat; } +.dan-num-plain { display: grid; place-items: center; color: #4a3a2a; font-family: var(--mono); font-weight: 900; line-height: 1; } + +/* ---------- 逐题行 ---------- + * 一行 = 原作的行底板(UI_DNM_Result_musicBase_01,576×124)+ 一层**乐曲边框**。 + * 边框照原作 `UI_CMN_RSL_KopMBase_*` 逐像素量出来重画 —— 官方那五张的颜色与难度名 + * (BASiC/ADVANCED/EXPERT/MASTER/Re:MASTER)都烘死在图里,套不上我们自己的 CF 分档, + * 所以只抄版式:主色场交给 --tc1、外圈主色环交给 --tc2(**一块平色**,不做上下渐变)。 + * + * **换算**(下面每个数字的唯一来源):素材包里的 Sprite 精裁版是 636×136,官方设计尺寸 + * 640×140(左右上下各被紧裁掉 2px 透明边,`docs/images/original-card/UI_CMN_RSL_KopMBase_MST.png` + * 就是精裁版);而我们的行内区是 576×126 = 官方 640×140 × 0.9。于是 + * 行内坐标 = (原版文件坐标 + 2) × 0.9 + * 「边框内坐标」= 行内坐标 − 3.6(边框本体左上角落在行内 (3.6, 3.6))。 + * 量法是逐行/逐列扫那张贴图,取每块的首像素…末像素,再把「末像素 + 1」当开区间边界代入: + * 边框本体 原版 2…634 × 2…134 → 行内 (3.6, 3.6) 568.8×118.8;外圈主色环 2px、圆角 ~8 + * 曲绘槽 原版 x 10…127 / y 10…127 → 行内 (10.8, 10.8) 105.3×105.3(白槽、圆角 ~3) + * 歌名条 原版 y 10…30 / 右端 626 → 行内 (122.4, 10.8) 442.8×18 深蓝 #012268 胶囊 + * (左端本来接着难度名牌那一格,名牌删掉后由歌名条顶满上面一整行) + * 白盒 原版 x 134…499 / y 38…106 → 行内 (122.4, 36) 328.5×61.2 + * (原版圆角:左上 9 / 右上 17 / 右下 17 / 左下 0 —— 右下那道大弧就是原版 + * 那块「右侧带缺口的面板」,它一路弯到底带里) + * 底带 原版 x 134…518 / y 106…132 → 行内 (122.4, 97.2) 345.6×23.4 浅色带 + * 右下白牌 原版 x 499…632 / y 106…132 → 行内 (450.9, 97.2) 119.7×23.4 胶囊(两端圆头) + * (原版烘的是日文 `でらっくスコア`;我们寄不出那套字,写「DX分数」) + * 右上紫场 白盒右端…边框内右沿 / 歌名条下沿…白牌上沿 = 119.5×68.4,原版空着, + * 我们拿它放逐题评级徽章(原版白盒那个大 R 弧就是为它让位的)。 + * + * 信息分配(照用户口径):**最上面歌名占一整行**(难度名牌删掉)、曲绘槽在左、 + * 达成率大数字排在白盒里、**DX 分数挪到卡片右下角那块白牌**、**限时写进底带左侧**。 + */ +.dan-result-tracks { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; } +.dan-track { position: absolute; left: 267px; width: 576px; height: 126px; } +/* 印在行左边、行外那一条(原作里印和底板本来就不在同一个容器里)。 */ .dan-track-stamp { - box-sizing: border-box; width: 46px; height: 50px; display: grid; place-items: center; - font-size: 1.15rem; font-weight: 900; color: #e0402a; background: #fff8ec; - border: 3px solid #e0402a; border-radius: 2px; box-shadow: 0 2px 6px #0002; - clip-path: polygon(29% 0, 71% 0, 100% 29%, 100% 71%, 71% 100%, 29% 100%, 0 71%, 0 29%); -} -.dan-track[data-outcome="timeout"] .dan-track-stamp, -.dan-track[data-outcome="interrupted"] .dan-track-stamp { color: #8f8f8f; border-color: #9d9d9d; background: #f4f1ec; } -.dan-track[data-outcome="pending"] .dan-track-stamp { color: #b9b1a6; border-color: #bcb4a8; background: #f4f1ec; } -.dan-track-card { - display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 10px; align-items: center; - padding: 6px 12px 6px 6px; border-radius: 9px; border: 2px solid #ffffff59; - background: linear-gradient(100deg, var(--tc1, #8b6cf0), var(--tc2, #4a2fb0)); - box-shadow: 0 3px 10px #0005, inset 0 1px #ffffff40; + position: absolute; left: -118px; top: 8px; width: 108px; height: 108px; + object-fit: contain; filter: drop-shadow(0 2px 3px #0003); +} +/* 还没抽到的那道不盖章 —— 印是真的,只是这一格还没轮到他。 */ +.dan-track[data-outcome="pending"] .dan-track-stamp { opacity: .22; filter: grayscale(1); } +.dan-track-plate { position: absolute; inset: 0; background: url('/assets/maimai/track-plate.png') 0 0 / 100% 100% no-repeat; } + +/* 乐曲边框:**单色**主色场(原版那块底板就是一块平色)+ 外圈 2px 主色环 + 底部浅色带。 + * 环用 inset box-shadow 画、带用子元素画,都不占盒模型 —— 边框**不是**真 border, + * 真 border 会把子元素坐标系推进边框宽度。 */ +.dan-track-frame { + position: absolute; left: 3.6px; top: 3.6px; width: 568.8px; height: 118.8px; border-radius: 7px; + overflow: hidden; + background: var(--tc1, #8b6cf0); + box-shadow: inset 0 0 0 2px var(--tc2, #4a2fb0); } +/* 底部浅色带(原版那一块:从曲绘槽右边一直铺到右下白牌底下)。它是**主色的浅色版本**, + * 不是白色 —— 白色留给白盒与右下白牌。`overflow: hidden` 让它的左下角跟着边框圆弧收掉。 */ +.dan-track-band { + position: absolute; left: 118.8px; top: 93.6px; width: 345.6px; height: 23.4px; + background: color-mix(in srgb, var(--tc1, #8b6cf0) 55%, #ffffff); +} +/* 边框里的组件用**边框内坐标**(= 行内坐标 − 3.6)。 */ +/* 深蓝歌名条:删掉难度名牌之后它**占满上面一整行** —— 左端与白盒左端对齐(边框内 118.8), + * 右端贴到边框内右沿(118.8+442.8=561.6,离主色环还有 ~7px,与原版一致)。 */ +.dan-track-titlebar { + position: absolute; left: 118.8px; top: 7.2px; width: 442.8px; height: 18px; + border-radius: 9px; background: #012268; +} +/* 行首那块「STAGE 01」小牌删掉了:官方那张牌压在曲绘上,我们没有曲绘,它就只是在左上角 + * 多挂一块深蓝方块。样式留成 display:none(不再渲染这个节点,这条只是兜底)。 */ +.dan-track-no { display: none; } +/* 官方的曲绘槽(JacketImage_S):原版是一块 105×105 的**方槽**(圆角只有 ~3px), + * 从边框左上角一直占到接近底部。我们抽的是 CF 题、没有曲绘 —— 槽里放题号本身 + * (`1554C` 这种「题号 + 小问」的写法)与这题在 CF 上的 rating。 + * 底色是**卡片主色的浅色版本**(原版那格是曲绘,换成白牌会像挖了个洞)。 */ .dan-track-jacket { - width: 96px; height: 68px; border-radius: 5px; display: grid; place-items: center; - border: 2px solid #ffffff66; box-shadow: inset 0 0 22px #00000066; - background: linear-gradient(140deg, #ffffff2e, #00000059); -} -.dan-track-jacket b { font-family: var(--mono); font-size: 1.5rem; font-weight: 900; line-height: 1; color: #fff; text-shadow: 0 2px 5px #000b; } -.dan-track-jacket small { font-family: var(--mono); font-size: .58rem; color: #ffffffcc; } -.dan-track-info { display: grid; gap: 3px; min-width: 0; } -.dan-track-head { display: flex; gap: 7px; align-items: baseline; flex-wrap: nowrap; min-width: 0; } -.dan-track-no { font-family: var(--mono); font-size: .66rem; font-weight: 700; letter-spacing: .1em; color: #ffe0b0; white-space: nowrap; } -.dan-track-diff { font-family: var(--mono); font-size: .64rem; color: #fff; background: #0000004d; border-radius: 3px; padding: 1px 6px; white-space: nowrap; } -.dan-track-name { flex: 1; min-width: 0; font-size: .76rem; color: #ffffffeb; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.dan-track-ach { display: flex; align-items: baseline; gap: 8px; } -.dan-track-achlabel { font-size: .68rem; color: #ffffffd0; } -.dan-track-achvalue { font-family: var(--mono); font-size: 1.8rem; font-weight: 900; letter-spacing: -.02em; } -.dan-track-judge { font-size: .62rem; color: #3a2418; background: #ffd76a; border-radius: 999px; padding: 1px 9px; white-space: nowrap; } + position: absolute; left: 10.8px; top: 10.8px; width: 105.3px; height: 105.3px; + border-radius: 3px; display: flex; flex-direction: column; + justify-content: center; align-items: center; gap: 2px; + background: color-mix(in srgb, var(--tc1, #8b6cf0) 42%, #ffffff); + box-shadow: inset 0 0 0 2px #ffffff4d, 0 2px 6px #00000024; +} +.dan-track-jacket b { + font-family: var(--mono); font-size: 1.3rem; font-weight: 900; line-height: 1; color: #17265c; +} +.dan-track-jacket small { font-family: var(--mono); font-size: .85rem; font-weight: 700; color: #3d4a75; } +/* 单题 DX 分数(= 该题算出的 rating):原版那块牌烘着日文 `でらっくスコア`,压在**右下角**、 + * 底带的右端上(它就是原版「右下角是 dx 分数」那一格)。两端是圆头 ⇒ 胶囊。 + * `align-items: baseline`:小字标签与数字按**同一条基线**排(数字那一侧由 numText 的 + * `baseline` 选项按图集基线补一个相对偏移,见 dan.js)。 */ +.dan-track-dxbox { + position: absolute; left: 450.9px; top: 97.2px; width: 119.7px; height: 23.4px; + border-radius: 11.7px; background: #fff; display: flex; align-items: baseline; padding: 0 13px; + box-shadow: 0 1px 4px #00000033; +} +.dan-track-dxlabel { flex: none; font-size: .54rem; font-weight: 900; color: #0d2a63; } +.dan-track-dxnum { margin-left: auto; } +.dan-track-name { + position: absolute; left: 128.8px; top: 7.2px; width: 422.8px; height: 18px; + font-size: .68rem; line-height: 18px; color: #fff; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +/* 达成率白盒:原版那一格是 `UI_AchivementCounter`,白底、**右下角一道大 R 弧**(连续到 + * 底带上,为右上那块紫场让位)。左上角烘着原版的 `ACHIEVEMENT` 小字,我们照位置写 + * 「達成率」。圆角按原版量到再 ×0.9:左上 8.1、右上 15.3,**下面两角都是直角** —— 白盒的 + * 底边和下面那条底带连成一整块,中间不该出现圆角缺口(左下、右下都为 0)。 */ +.dan-track-achbox { + position: absolute; left: 118.8px; top: 32.4px; width: 328.5px; height: 61.2px; + border-radius: 8.1px 15.3px 0 0; background: #fff; +} +.dan-track-achlabel { + position: absolute; left: 10px; top: 4px; font-size: .6rem; font-weight: 900; letter-spacing: .08em; color: #0d2a63; +} +/* 达成率数字**右对齐**:满 100 的那串(`100.8000%`)右端永远贴在同一个位置,不到 100 的 + * 少一位数就少在**左边**(`96.0000%` 的右端仍与 100 的齐平)。左对齐时短的那串右端会缩回来。 + * 垂直上排在「達成率」那行小字**下面**:盒内 y 13…61(盒高 61.2),字高 44 居中, + * 整串右端离盒右沿 8px。 */ +.dan-track-achvalue { position: absolute; left: 9px; top: 13px; width: 311.5px; height: 48px; display: flex; align-items: center; justify-content: flex-end; } +.dan-per { display: block; } +/* 右上那块紫场:白盒右端…边框内右沿 / 歌名条下沿…右下白牌上沿 = 行内 450.9…570.4 / + * 28.8…97.2,原版空着(白盒那道大 R 弧就是给它让位的)。逐题评级徽章放这里 —— + * 99×40 是官方 SSS+ 那张紧裁图(110×44)×0.9,摆进去离白盒 13px、离边框内右沿 7.4px。 + * 徽章源图是紧裁的(SSS+ 110×44、D 38×42),给 99×40 的框按高度贴齐。 */ +.dan-track-badge { position: absolute; left: 464px; top: 43px; width: 99px; height: 40px; object-fit: contain; filter: drop-shadow(0 2px 5px #0000003d); } +.dan-track-judge { + position: absolute; left: 464px; top: 43px; width: 99px; height: 40px; + display: grid; place-items: center; font-size: .72rem; font-weight: 900; color: #0d2a63; + background: #ffd76a; border-radius: 10px; box-shadow: inset 0 0 0 2px #ffffffa6, 0 2px 5px #0000003d; +} .dan-track[data-outcome="timeout"] .dan-track-judge, .dan-track[data-outcome="interrupted"] .dan-track-judge, -.dan-track[data-outcome="pending"] .dan-track-judge { background: #d9d2c8; } -.dan-track-side { display: grid; gap: 4px; justify-items: end; } -.dan-track-lv { - font-family: var(--mono); font-size: .95rem; font-weight: 900; color: #fff; padding: 0 8px; - border: 2px solid #ffffff8c; border-radius: 6px; background: linear-gradient(#7b5cff, #4628b8); -} -.dan-track-score { display: flex; align-items: baseline; gap: 5px; } -.dan-track-score small { font-size: .58rem; color: #ffffffb8; } -.dan-track-score b { font-family: var(--mono); font-size: .9rem; color: #fff; } -.dan-track-limit { font-size: .56rem; color: #ffffff9e; } - -/* 金色描边数字:白→金→橙渐变填充 + 深棕描边(原作那套 DX 数字,用 CSS 重画)。 - * 必须分两层,试过两条捷径都不行: - * - 八向 text-shadow:偏移副本会把渐变填充整个盖住,只剩一圈棕边; - * - -webkit-text-stroke + paint-order: stroke fill:Chromium 的 paint-order 只对 - * SVG 生效,HTML 文字上描边照样盖在填充之上,6px 描边直接吃掉整个笔画。 - * 所以描边交给 ::before、渐变填充交给 ::after,两层叠在同一段文字(data-text)上。 - * 这两点是抠图像素数数出来的,DOM 层面完全看不出来。 */ -.dan-gold { position: relative; display: inline-block; color: transparent; } -.dan-gold::before, .dan-gold::after { - content: attr(data-text); position: absolute; left: 0; top: 0; white-space: nowrap; pointer-events: none; -} -.dan-gold::before { color: #5a2a10; -webkit-text-stroke: 6px #5a2a10; } -.dan-gold::after { - background: linear-gradient(#fffce8, #ffd85c 42%, #ff9a1f 68%, #ffd85c); - -webkit-background-clip: text; background-clip: text; color: transparent; -} -.dan-gold { filter: drop-shadow(0 4px 7px #00000059); } +.dan-track[data-outcome="pending"] .dan-track-judge { background: #e8e2d6; } +/* 限时写进**底带的左侧空白**(原版这条底带是空的一块浅色,右下角才是 DX 分数):所以 + * 它自己没有白牌面,只把字压在浅色带上,左端与底带左端对齐。 */ +.dan-track-limit { + position: absolute; left: 122.4px; top: 97.2px; width: 328.5px; height: 23.4px; + display: flex; align-items: center; padding-left: 12px; + font-size: .58rem; font-weight: 700; color: #0d2a63; +} -/* 底部:判定框 · 心 · 总达成率 / DX分数 */ -.dan-result-bottomrow { display: grid; grid-template-columns: minmax(140px, 210px) auto minmax(0, 1fr); gap: 16px; align-items: center; width: 100%; margin-top: 3px; } -.dan-result-rankbox { - display: grid; place-items: center; gap: 1px; padding: 9px 12px; background: #fffdf6; - border: 3px solid #e6d3b4; border-radius: 13px; box-shadow: 0 3px 10px #00000030, inset 0 1px #fff; -} -.dan-result-rankbox strong { font-size: 2.35rem; font-weight: 900; letter-spacing: .05em; line-height: 1.1; color: #2b1c12; } -.dan-result[data-verdict="failed"] .dan-result-rankbox strong { color: #b32a15; } -.dan-result[data-verdict="abandoned"] .dan-result-rankbox strong { color: #7d8794; } -.dan-result-rankbox small { font-size: .68rem; color: #9a7550; } -.dan-result-life { position: relative; width: 88px; height: 88px; display: grid; place-items: center; } -.dan-result-life::before { - content: '♥'; position: absolute; inset: 0; display: grid; place-items: center; - font-size: 92px; line-height: 1; color: #35c257; - -webkit-text-stroke: 5px #e6fbec; text-shadow: 0 4px 10px #0000004d; -} -.dan-result-life[data-state="bad"]::before { color: #9aa3ae; } -.dan-result-life span { position: relative; z-index: 1; font-family: var(--mono); font-size: 1.2rem; font-weight: 900; color: #fff; text-shadow: 0 1px 4px #000000b3; } -.dan-result-life span small { font-size: .62rem; opacity: .9; } -.dan-result-totalblock { display: grid; gap: 2px; min-width: 0; } -.dan-total-label { font-size: .74rem; font-weight: 700; letter-spacing: .1em; color: #8a5a2a; } -.dan-total-ach { font-family: var(--mono); font-size: 2.5rem; font-weight: 900; letter-spacing: -.02em; } -.dan-total-score { display: inline-flex; align-items: baseline; gap: 7px; justify-self: start; margin-top: 2px; padding: 2px 12px; border-radius: 6px; background: #fffdf699; border: 2px solid #e6d3b4; } -.dan-total-score small { font-size: .64rem; color: #9a7550; } -.dan-total-score b { font-family: var(--mono); font-size: 1rem; color: #2b1c12; } +/* ---------- 底部:段位名牌 · 心 · 总达成率 / DX分数 ---------- + * 底图在这一块**预留了三处白**(官方在那儿写运行时文字):(503,663) 与 (753,677) 是 + * 「総達成率」那个标签的白格(中间被折页的斜边切开),(608,810) 360×80 是「DX分数」 + * 那条白牌(左端还有个空的圆白圆头)。所以这三块的**位置不用猜**,照白格摆: + * 标签进白格 → 大数字落在下面的色带上 → DX 分数进下面那条白牌。 */ +.dan-bottom { position: absolute; inset: 0; } +/* 左下白框(底图 (74,672) 240×230):原版这里写段位名(十段/皆伝…)。 + * 我们发的是档位、不发段位名,所以照原版把**档位名**写进去: + * 深棕重字 + 米白描边 + 一点投影(原版那一路毛笔字我们寄不出字体,用系统里的重明朝近似)。 */ +.dan-verdict { + position: absolute; left: 74px; top: 672px; width: 240px; height: 230px; + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; +} +.dan-plate-name { + /* 原版那两个字是**重毛笔字**。客户端里它是烘死的贴图,我们只能用系统字体近似: + * 先取思源宋体的 900(这台机器上是 `NotoSerifSC-VF`,最重的一档),再退回系统宋体/ + * 楷体那一类衬线。SimSun 那种细宋差得太远(笔画只有原版一半粗)。 */ + font-family: "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STKaiti", "KaiTi", "SimSun", serif; + font-size: 104px; font-weight: 900; line-height: 1.08; letter-spacing: .01em; + color: #3a2114; paint-order: stroke fill; + /* 原版那两个字是深棕重字 + 很淡的米白边缘 + 一点投影;描边给厚了会像贴纸。 */ + -webkit-text-stroke: 7px #fff6e6; + text-shadow: 0 3px 0 #00000024; +} +/* 名字长的档位(小随机段位 / 大随机段位)自动缩号,别顶穿白框。 */ +.dan-plate-name[data-len="3"] { font-size: 88px; } +.dan-plate-name[data-len="4"] { font-size: 68px; } +.dan-plate-name[data-len="5"], .dan-plate-name[data-len="6"] { font-size: 54px; } +.dan-verdict-count { font-size: .76rem; font-weight: 800; letter-spacing: .04em; color: #7a5a34; } +/* 原作这一格是剩余生命的圆心。素材本身是 272×272,之前压到 104 —— 照官方放大到 200, + * 位置保持原来那个圆心(434,790)。 */ +.dan-life { position: absolute; left: 334px; top: 690px; width: 200px; height: 200px; } +.dan-life-base { width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 3px 8px #00000038); } +.dan-life-count { position: absolute; inset: 0; display: grid; place-items: center; } +/* 总达成率:标签写进底图预留的白格,大数字落在下面的色带上,右端对齐 x 945 + * (与下面那条 DX 分数白牌的右端同一条边)。 */ +.dan-total-row { position: absolute; inset: 0; } +/* 「总达成率」四个字写在白格**偏左**的位置(官方那四个字不在白格正中),字号压到 23。 + * 底图这块白实测是 x 503..708(右边那截 753..856 被折页斜边切开),所以按 503..708 居中。 */ +.dan-total-label { + position: absolute; left: 503px; top: 663px; width: 354px; height: 52px; + display: grid; place-items: center; padding-right: 140px; padding-bottom: 12px; + font-size: 23px; font-weight: 800; letter-spacing: .1em; color: #16306b; +} +.dan-total-num { + position: absolute; left: 490px; top: 727px; width: 460px; height: 66px; + display: flex; align-items: center; justify-content: flex-end; +} +/* DX 分数:官方这条白牌就是底图预留的 (608,810) 360×80,标签在左、数字在右, + * 都是深蓝(和每题那张卡上的 DX 分数同一个色)。白牌下半截被折页斜切掉一块, + * 所以文字靠上排(顶到 y 816),不跟着整块矩形居中。 */ +.dan-dxscore-row { + position: absolute; left: 608px; top: 810px; width: 360px; height: 80px; + display: flex; align-items: baseline; justify-content: space-between; padding: 0 46px 0; +} +.dan-dxscore-label { font-size: 30px; font-weight: 800; letter-spacing: .06em; color: #16306b; } +.dan-dxscore-num { display: flex; align-items: center; } -.dan-result-note { margin: 2px 0 0; max-width: 72ch; text-align: center; font-size: .78rem; line-height: 1.7; color: #96714b; } -.dan-result-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; align-items: center; width: 100%; } +/* 说明与按钮在成品屏之外(深色底上),不往那张纸上塞东西。 */ +.dan-result-note { margin: 0; max-width: 92ch; justify-self: center; text-align: center; font-size: .78rem; line-height: 1.7; color: #a9b4c4; } +.dan-result-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; align-items: center; } .dan-result-ok { min-width: 112px; padding: 7px 26px; font-size: 1.25rem; font-weight: 900; color: #fff; cursor: pointer; background: linear-gradient(#ff9a4d, #e8542a); border: 3px solid #c9381f; border-radius: 11px; @@ -286,17 +403,8 @@ @media (max-width: 860px) { .dan-grid { grid-template-columns: 1fr; } - .dan-result-sheet { padding: 16px 22px 18px; border-radius: 30px; } - .dan-result-title { font-size: 1.4rem; } - .dan-cord { display: none; } - .dan-track-card { grid-template-columns: 62px minmax(0, 1fr); row-gap: 6px; } - .dan-track-jacket { width: 62px; height: 56px; } - .dan-track-jacket b { font-size: 1.15rem; } - .dan-track-side { grid-column: 2; grid-auto-flow: column; align-items: baseline; gap: 8px; justify-items: start; } - .dan-track-achvalue { font-size: 1.35rem; } - .dan-track-name { flex-basis: 8ch; } - .dan-result-bottomrow { grid-template-columns: 1fr; justify-items: center; } - .dan-total-ach { font-size: 2rem; } - .dan-total-score { justify-self: center; } + /* 结算盘是一张 980×928 的成品屏,窄屏不重排、只横向滚动(见 .dan-result-body 的 + * min-width)—— 把四行字和页头压进 360px 宽只会压成不可读。 */ + .dan-result-close { top: 6px; right: 10px; font-size: 1.25rem; } .dan-result-actions { justify-content: center; } } \ No newline at end of file diff --git a/public/dan.html b/public/dan.html index 5cc330c..c67b0d8 100644 --- a/public/dan.html +++ b/public/dan.html @@ -1,14 +1,16 @@ -随机抽题 · Algorithm DX +随机抽题 · Algorithm DX + - + +
@@ -75,4 +81,6 @@

抽一道,别挑。

口径:这是自测,不是防作弊。开始做题之后你照样可以查题解、问 AI —— 这一点拦不住,和「辅助解题」自报标签是同一条现实。它唯一能保证的是你没法挑题。
另外,抽到的题如果正好是你在 Codeforces 上已经提交过的(含只有 WA 的)、或者在你已归档账号上做过的、或者是你正在计时的那道,都会被自动排除。排除依据是本机已同步的记录,同步不完整时(比如刚 AC 还没同步)可能仍有漏网。

- \ No newline at end of file +TOP + + \ No newline at end of file diff --git a/public/dan.js b/public/dan.js index 4cc9861..5c72a2a 100644 --- a/public/dan.js +++ b/public/dan.js @@ -1,5 +1,6 @@ import { startAutoSync } from './auto-sync.js'; -import { cfRatingColor } from './cf-rating-colors.js'; +import { cfRatingColor, CF_TONE_COLORS as TRACK_TONE } from './cf-rating-colors.js'; +import { trackLabel, trackNumber } from './maimai-glyphs.js'; /* * 随机抽题 / 每日一题 / 段位認定 的页面逻辑。 @@ -430,7 +431,7 @@ function buildStageDialog() { dialog.setAttribute('aria-labelledby', 'danResultTitle'); dialog.innerHTML = `
-
STAGE
+

DX
@@ -465,7 +466,9 @@ function showStageSheet(pending) { const score = stage.comparison?.currentScore ?? null; const comparison = stage.comparison ?? null; - field(dialog, '.dx-result-kicker b').textContent = String(stage.index).padStart(2, '0'); + // 用官方素材:TRACK 是唯一一张图,号码是数字精灵(数字层,不带那块实心白) + field(dialog, '.dx-result-track-word').replaceChildren(trackLabel()); + field(dialog, '.dx-result-kicker b').replaceChildren(trackNumber(String(stage.index).padStart(2, '0'))); field(dialog, '.dx-result-kicker span').textContent = `${session.tierName} · ${session.stageCount} 道中的第 ${stage.index} 道`; field(dialog, '#danResultTitle').textContent = stage.outcome === 'cleared' ? 'CLEAR!' : stage.outcome === 'timeout' ? 'TIME UP' : 'INTERRUPTED'; field(dialog, '.dx-result-kind').textContent = KIND_LABEL[session.kind] ?? session.kind; @@ -590,38 +593,52 @@ function drainSheet() { /* ---------------- 段位認定结算页(对齐原作 UI_DNM_Result_* 的构成) ---------------- */ -/** 难度色号 → 逐曲卡的渐变(与 CF 分色同一套边界;violet 是原作 MASTER 的紫)。 */ -const TRACK_TONE = { - red: ['#e8564f', '#a3211c'], orange: ['#f08c3a', '#a85a12'], - violet: ['#8b6cf0', '#4a2fb0'], blue: ['#4a7fe0', '#1e3f96'], - cyan: ['#3fa8c4', '#166b83'], green: ['#4aa85c', '#1d6b30'], - gray: ['#7b8391', '#3f4652'], unrated: ['#7b8391', '#3f4652'], -}; +/* 难度色号 → 逐曲卡的 [主色场, 外圈主色环] 见 cf-rating-colors.js 的 CF_TONE_COLORS + * (题目卡也用同一张表)。两个色都只当**平色**用,不做上下渐变 —— 原版那块乐曲底板就是一块平色。 */ let resultDialog = null; function buildResultDialog() { const dialog = el('dialog', 'dan-result'); dialog.setAttribute('aria-labelledby', 'danRunTitle'); + // 盘面是一张 980×928 的固定设计尺寸舞台,里面所有组件都用**官方预制体量出来的像素坐标** + // (见 dan.css 顶部注释)。窄屏横向滚动,不做响应式重排 —— 重排就还原不出来了。 dialog.innerHTML = `
- - -

段位認定

-

-
    -
    -
    -
    -
    - 总达成率 - - DX分数 +
    +
    +
    +

    段位認定

    +
      +
      + +
      + + +
      +
      + + +
      + +
      + 总达成率 + +
      +
      + DX分数 + +
      +
      +
      +
      +

      +

      +
      + +
      -
      -

      -
      - -
      `; document.body.append(dialog); field(dialog, '.dan-result-close').addEventListener('click', () => dialog.close()); @@ -629,13 +646,204 @@ function buildResultDialog() { return dialog; } -/** `.dan-gold` 要两层(描边 ::before + 渐变填充 ::after),两层都读 `data-text`。 */ -function goldNode(cls, text) { - const node = el('strong', cls, text); - node.dataset.text = text; - return node; +/* ---------- 官方数字字体(图集逐格切) ---------- + * 图集是规整的 4×4 网格,每格的位置由素材包里同名的 Sprite 给出 + * (UI_CMN_Num_90p_0 … _14),不用自己量。 + * 「下标 → 字符」是拿对照表一张张看出来的: + * 白字族(UI_CMN_Num_26p/90p)0-9 · [10]'+' · [11]'-' · [12]',' · [13]'.' · [14]'%' + * 分数族(UI_NUM_Score_0001111_*)0-9 · [10]'+' · [11]'/' · [12]'%' · [13]'.' + * 白字族各带一层 _Outline(同布局的实心剪影)—— 官方就是这么叠出描边数字的: + * 两个背景层同一位置,填充层画在描边层之上。 + */ +const NUM_INDEX = { + '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, + '+': 10, '-': 11, ',': 12, '.': 13, '%': 14, +}; +const SCORE_INDEX = { + '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, + '+': 10, '/': 11, '%': 12, '.': 13, +}; +/** 白字族借用的小数点已经不需要了:注释见 NUM_FONTS 的 `dotBaseline`。 */ +/** + * 图集每格留了空白边(90p 那格 89px 宽、字形只有 63px),照格宽排字会读起来「一个字一个字 + * 分开」。官方是等距密排(字与字几乎贴上),所以每格按 `advance` 收回去一点,见 numText。 + * + * `baseline`:数字墨迹底线在格高里的比例(量出来的)—— 旁边有小字标签时用它把两边按同一条 + * 基线排(见 numText 的 `baseline` 选项)。 + * + * `decimal`:**借来的小数点**。白字族里没有句点这一格 —— `[13]` 是中黑点(画在字高中线上)、 + * `[12]` 是逗号,都不是小数点该有的样子(小字号下读出来像断字符)。分数族 `[13]` 才是这套 + * 数字里真正的句点(方形、压在基线附近),官方达成率图里的「101.0000%」用的就是它。 + * `dotBaseline`:小数点那一格(`[13]`)墨迹底线在格高里的比例。这一格在字高中线上 + * (26p 只有 8×8、90p 24×24),照原样排出来是浮在数字中间的一粒、像断字符 —— 官方那只 + * 方块的底边压在数字基线上,所以按它压下去(字号小的那两个族才需要,见 numText)。 + */ +const NUM_FONTS = { + n26: { + fill: 'num-26p', outline: 'num-26p-outline', atlas: [136, 160], cell: [34, 40], advance: 24, + baseline: 0.825, dotBaseline: 0.775, chars: NUM_INDEX, + }, + n90: { + fill: 'num-90p', outline: 'num-90p-outline', atlas: [356, 420], cell: [89, 105], advance: 67, + baseline: 0.886, dotBaseline: 0.6, chars: NUM_INDEX, + }, + // 分数数字是**三套**同布局不同颜色的图集(自带颜色,不再叠描边层): +// blue / gold / red,按达成率高低换。官方还有一张 `UI_NUM_Score_0001111_Base`, +// 但那是一张**空图集**(整张几乎全透明,量下来 16 格一个字形都没有),不能用; +// 「%」字形也正好只有蓝/金/红三张 —— 所以官方的档位就是三档,不是四档。 +'score-blue': { fill: 'num-score-blue', outline: '', atlas: [296, 392], cell: [74, 98], advance: 67, baseline: 0.949, dotBaseline: 0.827, chars: SCORE_INDEX }, + 'score-gold': { fill: 'num-score-gold', outline: '', atlas: [296, 392], cell: [74, 98], advance: 67, baseline: 0.949, dotBaseline: 0.827, chars: SCORE_INDEX }, + 'score-red': { fill: 'num-score-red', outline: '', atlas: [296, 392], cell: [74, 98], advance: 67, baseline: 0.949, dotBaseline: 0.827, chars: SCORE_INDEX }, +}; +const NUM_COLS = 4; +/** 达成率末尾那个大「%」字形:官方按分数分色,蓝/金/红三张。 */ +const PERCENT_ART = { gold: 'score-per-gold.png', blue: 'score-per-blue.png', red: 'score-per-red.png' }; + +/** + * 达成率取哪一档颜色 —— 原作就是按分数换 `UI_NUM_Score_0001111_*` 那一套。 + * 三档的界限(2026-10-10 用户给的官方口径):**97% 以上金、80% 以上红、再低蓝**。 + * 注意低分段是**蓝**、不是红 —— 别按「高金/中蓝/低红」的直觉写。 + */ +function scoreTone(achievement) { + if (achievement >= 97) return 'gold'; + if (achievement >= 80) return 'red'; + return 'blue'; +} + +/** 达成率数字 + 官方「%」字形。 */ +function achievementNumber(achievement, height) { + const tone = scoreTone(achievement); + const art = PERCENT_ART[tone]; + const text = achievement.toFixed(4); + const wrap = numText(text, `score-${tone}`, height); + // 「%」是独立字形(img 的 alt 为空),读屏那行 aria-label 要把它补回去。 + wrap.setAttribute('aria-label', `${text}%`); + const percent = el('img', 'dan-per'); + percent.src = `/assets/maimai/${art}`; + percent.alt = ''; + // 官方那个「%」跟数字差不多高,而且**贴着**最后一个数字。`score-per-*` 是 80×80 的方图、 + // 四周留白(墨迹 x 8..70 / y 10..69),直排会在数字和 % 之间多出一截空隙,所以放大一点 + // 再把左边那圈留白用负边距抵掉。 + percent.style.height = `${Math.round(height * 1.1)}px`; + percent.style.marginLeft = `${-Math.round(height * 0.16)}px`; + wrap.append(percent); + return wrap; +} + +/** DX 分数(其实是单题 rating)用的深蓝:和边框里的标题条、标签同一个色 —— 不走分数色, + * 那套三色专门留给达成率。 */ +const DX_NUM_COLOR = '#0d2a63'; + +/** + * 把一串字符渲染成官方数字贴图。`height` 是字高(px,按 980×928 的设计尺寸)。 + * 每格拆成两个叠起来的层:官方 `_Outline` 剪影上深色(描边)+ 官方填充图集。 + * 之所以要两个真元素而不是两个 background 层:图集是**纯白剪影**(游戏运行时才上色), + * background 层没法单独染色,而伪元素又只能画在宿主背景之上(描边会盖住填充)。 + * `color` 给定时就用图集当**遮罩**把数字染成这个颜色 —— 这正是游戏里给白字族上色的做法 + * (分数族自带颜色,不走这条路;注意 `UI_NUM_Score_0001111_Base` 是张空图集,不能用)。 + * 字体里没有的字符(例如数字族没有 '/')退回普通文字,不会静默丢字符。 + * + * `opts.outline === false`:**不叠描边层**。小号的 DX 分数在官方那里是干净的深蓝数字, + * 叠一层 4px 描边会糊成一团 —— 小字一律不用描边。 + * `opts.trim === false`:不收紧字距(留给需要等宽对齐的场景)。 + * + * 排版就一件事:**一个字形一格**,格子之间按 `advance` 等距。小数点(含逗号、加号这些) + * 跟数字走同一条路 —— 它本来就是图集里的一格,官方也是这么排的,别为它开小灶。 + * + * `opts.baseline`:把整块数字往下补 `(1 − baseline) × 字高`。旁边有小字标签、两边用 + * `align-items: baseline` 排的时候要用它 —— flex 认的基线是这一格的**底边**,而图集里数字的 + * 墨迹底线在格高的 `baseline` 处,不补这一下,数字就会浮在标签上面。 + */ +const NUM_OUTLINE_COLOR = '#2b3350'; +function numText(text, fontKey, height, color, opts = {}) { + const font = NUM_FONTS[fontKey]; + const scale = height / font.cell[1]; + const wrap = el('span', `dan-num dan-num-${fontKey}`); + wrap.setAttribute('role', 'img'); + wrap.setAttribute('aria-label', text); + wrap.style.height = `${height}px`; + if (opts.baseline) { + wrap.style.position = 'relative'; + wrap.style.top = `${((1 - font.baseline) * height).toFixed(2)}px`; + } + const chars = [...text]; + if (opts.trim !== false) { + // 图集每格右边都留了空(分数族格宽 74、字形只有 60 左右),照格宽排会把一串数字 + // 读成「一个字一个字分开」。收回 (格宽 − 步进):注意 CSS 里是**负**边距。 + wrap.style.setProperty('--dan-num-trim', `${(font.cell[0] - (font.advance ?? font.cell[0])) * scale}px`); + } + chars.forEach((ch, index) => { + const cell = el('i', 'dan-num-char'); + cell.style.height = `${height}px`; + if (index === chars.length - 1) cell.classList.add('dan-num-last'); + const idx = font.chars[ch]; + if (idx === undefined) { + cell.textContent = ch; + cell.classList.add('dan-num-plain'); + if (color) cell.style.color = color; + wrap.append(cell); + return; + } + const sx = (idx % NUM_COLS) * font.cell[0]; + const sy = Math.floor(idx / NUM_COLS) * font.cell[1]; + const size = `${font.atlas[0] * scale}px ${font.atlas[1] * scale}px`; + // 小数点那一格画在字高中线上(26p 只有 8×8、90p 24×24),照原样排出来就是浮在数字中间 + // 的一粒,像断字符。官方那只方块是**底边压在数字基线上**的,所以按 `dotBaseline` + // (那一格墨迹底线在格高里的比例)把它压下去 —— 字体自己的字形,只是摆到该在的位置。 + // + // 注意:**不能**用挪 `background-position` 的办法下压 —— 图集是整张背景,往下挪 dy 就会把 + // 上一格(同一列、上一行)的下半截露在这一格里(用户提过两次的那个 bug)。裁剪也救不了, + // 因为承载背景的那个 `b` 就是格子本身、并没有溢出。改成把**整格**往下挪:图集位置不动, + // 字形还是它自己那一格,只是这一格整体下沉 dy。 + const dy = ch === '.' && font.dotBaseline ? (font.baseline - font.dotBaseline) * height : 0; + if (dy) cell.style.top = `${dy}px`; + const pos = `${-sx * scale}px ${-sy * scale}px`; + cell.style.width = `${font.cell[0] * scale}px`; + if (font.outline && opts.outline !== false) { + const outline = el('b', 'dan-num-outline'); + const url = `url(/assets/maimai/${font.outline}.png)`; + outline.style.setProperty('background-color', NUM_OUTLINE_COLOR); + for (const prop of ['mask-image', '-webkit-mask-image']) outline.style.setProperty(prop, url); + for (const prop of ['mask-size', '-webkit-mask-size']) outline.style.setProperty(prop, size); + for (const prop of ['mask-position', '-webkit-mask-position']) outline.style.setProperty(prop, pos); + for (const prop of ['mask-repeat', '-webkit-mask-repeat']) outline.style.setProperty(prop, 'no-repeat'); + cell.append(outline); + } + const fill = el('b', 'dan-num-fill'); + const fillUrl = `url(/assets/maimai/${font.fill}.png)`; + if (color) { + // 白字族是纯白剪影:当遮罩 + 纯色背景 = 想染什么色就什么色(游戏里也是运行时染色)。 + fill.style.setProperty('background-color', color); + for (const prop of ['mask-image', '-webkit-mask-image']) fill.style.setProperty(prop, fillUrl); + for (const prop of ['mask-size', '-webkit-mask-size']) fill.style.setProperty(prop, size); + for (const prop of ['mask-position', '-webkit-mask-position']) fill.style.setProperty(prop, pos); + for (const prop of ['mask-repeat', '-webkit-mask-repeat']) fill.style.setProperty(prop, 'no-repeat'); + } else { + fill.style.backgroundImage = fillUrl; + fill.style.backgroundSize = size; + fill.style.backgroundPosition = pos; + } + cell.append(fill); + wrap.append(cell); + }); + return wrap; } +/** + * 官方评级徽章:`UI_GAM_Rank_*` —— 游戏内那套**单级全套**(D/C/B/BB/BBB/A/AA/AAA/S/S+/SS/SS+/SSS/SSS+)。 + * + * 之前用的是 `UI_CMN_TabTitle_Rank_*`,但那是**页签标题**用的区间图:AAA 那张画的是 + * 「A~AAA」、BBB 那张是「~BBB」,摆在逐题卡片上根本不是那一级的评级(而且没有 + * D/C/B/BB/A/AA 这些级,低分全挤到同一张牌上)。换成游戏内那套之后 14 级各有一张。 + */ +const RANK_ART = { + 'SSS+': 'gam-rank-sssp.png', SSS: 'gam-rank-sss.png', 'SS+': 'gam-rank-ssp.png', SS: 'gam-rank-ss.png', + 'S+': 'gam-rank-sp.png', S: 'gam-rank-s.png', + AAA: 'gam-rank-aaa.png', AA: 'gam-rank-aa.png', A: 'gam-rank-a.png', + BBB: 'gam-rank-bbb.png', BB: 'gam-rank-bb.png', B: 'gam-rank-b.png', + C: 'gam-rank-c.png', D: 'gam-rank-d.png', +}; + function openRunResult(run) { resultDialog ??= buildResultDialog(); const dialog = resultDialog; @@ -644,6 +852,22 @@ function openRunResult(run) { const scored = cleared.filter((stage) => stage.achievement !== null); dialog.dataset.verdict = verdict; + // 底图用原作素材,按模式选:档位挑战是「段位認定」那一版, + // 小/大随机段位是「ランダム段位認定」那一版(原作本来就把这两件事分成两块屏)。 + // 页头文字与两侧水引都烘焙在底图里,所以标题 h2 只留给读屏。 + field(dialog, '.dan-result-art').dataset.art = + run.tier === 'small_random' || run.tier === 'big_random' ? 'random' : 'dani'; + const clearedRun = run.status === 'cleared'; + // 左下那格是原版的段位名牌:照原版写**名字**(我们发档位不发段位名),不盖合格印。 + // 通关几道另起一行小字放在名字下面,别把名字挤掉。 + const plateName = field(dialog, '.dan-plate-name'); + plateName.textContent = run.tierName; + plateName.dataset.len = String([...run.tierName].length); + // 结论(合格/不合格/中断)从原来那枚印改成名字下面的小字:印让位给段位名, + // 但「到底过没过」这句话还是得留着。 + field(dialog, '.dan-verdict-count').textContent = + `${VERDICT_TEXT[run.status] ?? '中断'} · ${cleared.length} / ${run.stageCount} 道通关`; + const limitText = run.perStageLimit ? '每道限时按当题难度' : `每道限时 ${fmtLimit(run.limitSeconds)}`; field(dialog, '.dan-result-sub').textContent = `${run.tierName} · ${run.minRating}–${run.maxRating} · 共 ${run.stageCount} 道 · ${limitText}` @@ -653,66 +877,118 @@ function openRunResult(run) { const tracks = field(dialog, '.dan-result-tracks'); tracks.replaceChildren(); for (const stage of run.stages) { - const tone = TRACK_TONE[cfRatingColor(stage.difficulty).tone] ?? TRACK_TONE.violet; + const color = cfRatingColor(stage.difficulty); + const tone = TRACK_TONE[color.tone] ?? TRACK_TONE.violet; const item = el('li', 'dan-track'); item.dataset.outcome = stage.outcome ?? 'pending'; - item.append(el('span', 'dan-track-stamp', stage.outcome === 'cleared' ? '可' : '不可')); - - const card = el('div', 'dan-track-card'); - card.style.setProperty('--tc1', tone[0]); - card.style.setProperty('--tc2', tone[1]); - - // 「封面」那一格放题号:我们抽的是 CF 题、没有曲绘,放题号比放装饰诚实。 + // 逐题行的纵向节距是官方的 122px(见 dan.css)。 + item.style.top = `${152 + 122 * (stage.index - 1)}px`; + // 底色变量挂在这一行上:乐曲边框读它(--tc1 主色场 / --tc2 外圈主色环)。 + // 官方那五张边框的颜色与难度名是烘死的,我们按自己的 CF 分档上色。 + item.style.setProperty('--tc1', tone[0]); + item.style.setProperty('--tc2', tone[1]); + + // 可 / 不可 用原作那两枚印(UI_DNM_Icon_Result_01/02)。 + const stamp = el('img', 'dan-track-stamp'); + stamp.src = stage.outcome === 'cleared' ? '/assets/maimai/stamp-clear.png' : '/assets/maimai/stamp-fail.png'; + stamp.alt = stage.outcome === 'cleared' ? '通过' : '未通过'; + item.append(stamp); + + // 行底板还是原作那张(UI_DNM_Result_musicBase_01);边框本体比行内区小 3.6px, + // 四条边会露出一点底板(原版贴图本来就留了这圈透明边)。 + item.append(el('div', 'dan-track-plate')); + + // 乐曲边框:照原作 UI_CMN_RSL_KopMBase_* 的版式画一层(尺寸全是从那张贴图量出来的比例, + // 见 dan.css 的「逐题行」一节)—— 外圈主色环 + 平色主色场 + 底部浅色带 + 左侧曲绘槽 + // + 深蓝歌名条(占顶部一整行)+ 白色达成率盒 + 右下角 DX 分数白牌。 + // 官方那五张(BSC/ADV/EXP/MST/MST_Re)颜色与难度名都烘死在图里,套不上我们的 CF 分档。 + const frame = el('div', 'dan-track-frame'); + // 底带先 append:它在白盒与右下白牌**下面**(原版就是这样一层浅色)。 + frame.append(el('div', 'dan-track-band')); + // 歌名条:原版左上那格是难度名牌(MASTER 那块),删掉之后歌名条顶满上面一整行。 + frame.append(el('span', 'dan-track-titlebar')); + // 题目名压在歌名条上(两者框内坐标相同)。抽到的题在本人提交里还没有记录时,退回题号 —— + // 空着比写「—」更难看,题号至少能对上。 + frame.append(el('span', 'dan-track-name', stage.title ?? stage.problemId ?? '—')); + const achBox = el('div', 'dan-track-achbox'); + // 官方白盒左上角那行小字(原作烘在边框贴图里,我们画边框所以自己写)。 + // 用原版那三个字:`達成率` —— 别自己写英文,官方结算这一行就是日文。 + achBox.append(el('small', 'dan-track-achlabel', '達成率')); + // 官方分数图集(按分数分色)+ 官方彩色「%」字形 —— 这是这一行最大的一块数字。 + // 没有成绩时用白字族的「—」,**染深蓝**:白字族是纯白剪影,落在达成率那个白框上会看不见 + // (numText 里没这个字形,会退回普通文字,不会静默丢字符)。 + const achValue = stage.achievement === null + ? numText('—', 'n90', 44, DX_NUM_COLOR) + : achievementNumber(stage.achievement, 44); + achValue.classList.add('dan-track-achvalue'); + achBox.append(achValue); + frame.append(achBox); + item.append(frame); + + // 单题 DX 分数(= 该题算出的 rating)挪到**卡片右下角**:原版那块白牌就压在底带右端, + // 上面烘的是日文 `でらっくスコア`(我们写「DX分数」)。底带的左半边留给限时。 + const dxBox = el('div', 'dan-track-dxbox'); + dxBox.append(el('small', 'dan-track-dxlabel', 'DX分数')); + // 这一格是**小字**:不叠描边层 —— 官方那处也是干净的深蓝数字,叠上去只会糊。 + const dx = stage.rating === null + ? numText('—', 'n26', 16, DX_NUM_COLOR, { outline: false, baseline: true }) + : numText(String(stage.rating.toFixed(1)), 'n26', 16, DX_NUM_COLOR, { outline: false, baseline: true }); + dx.classList.add('dan-track-dxnum'); + dxBox.append(dx); + item.append(dxBox); + + // 官方的 JacketImage_S 那一格:我们抽的是 CF 题、没有曲绘,槽里放**题号**(`1554C`: + // 场次 + 小问)与这题在 CF 上的 rating。原来那版把场次和小问拆成两行、题号只留个小问 + // 字母,一屏四张卡看不出是哪道题 —— 现在一行题号、一行 rating。 const [contest, index] = (stage.problemId ?? '').split(':'); const jacket = el('div', 'dan-track-jacket'); - jacket.append(el('b', '', index || '?'), el('small', '', contest || '未开始')); - card.append(jacket); - - const info = el('div', 'dan-track-info'); - const head = el('div', 'dan-track-head'); - head.append(el('span', 'dan-track-no', `STAGE ${String(stage.index).padStart(2, '0')}`)); - head.append(el('span', 'dan-track-diff', `${stage.difficulty} 分`)); - head.append(el('span', 'dan-track-name', stage.title ?? '—')); - info.append(head); - - const achievement = el('div', 'dan-track-ach'); - achievement.append(el('span', 'dan-track-achlabel', '达成率')); - achievement.append(goldNode('dan-track-achvalue dan-gold', - stage.achievement === null ? '—' : `${stage.achievement.toFixed(4)}%`)); - achievement.append(el('span', 'dan-track-judge', - stage.outcome === 'cleared' ? (stage.rank ?? '通关') - : stage.outcome === 'timeout' ? '超时' : stage.outcome === 'interrupted' ? '中断' : '未开始')); - info.append(achievement); - card.append(info); - - const side = el('div', 'dan-track-side'); - side.append(el('span', 'dan-track-lv', String(stage.difficulty))); - const score = el('span', 'dan-track-score'); - score.append(el('small', '', 'DX分数'), el('b', '', stage.rating === null ? '—' : stage.rating.toFixed(1))); - side.append(score); - side.append(el('small', 'dan-track-limit', `限时 ${fmtLimit(stage.limitSeconds)}`)); - card.append(side); - item.append(card); + jacket.append( + el('b', '', stage.problemId ? `${contest}${index}` : '未开始'), + // 题号没下发(还没开始做)时 rating 也不写:那边 `problemId` 是 null,这一格照同一口径。 + el('small', '', stage.problemId && stage.difficulty != null ? String(stage.difficulty) : '—'), + ); + item.append(jacket); + + // 题目名已经在上面跟歌名条一起 append 了(两者框内坐标相同)。原来标题条左边还有一格 + // **档位名牌**(写「上级」「初级」),删掉了:同一张卡上达成率、单题分、题号已经把信息 + // 说全,档位名重复而且占了半个标题条 —— 歌名条因此顶满上面一整行。 + + // 评级用官方徽章;超时/中断/未开始没有评级,退回文字。徽章放在**右上那块紫场**里 + // (白盒右端…边框内右沿 / 歌名条下沿…右下白牌上沿):原版那一块本来就是空的。 + if (stage.outcome === 'cleared' && RANK_ART[stage.rank]) { + const badge = el('img', 'dan-track-badge'); + badge.src = `/assets/maimai/${RANK_ART[stage.rank]}`; + badge.alt = stage.rank; + item.append(badge); + } else { + item.append(el('span', 'dan-track-judge', + stage.outcome === 'timeout' ? '超时' : stage.outcome === 'interrupted' ? '中断' : '未开始')); + } + + // 限时写在**底带的左侧**(原版这条底带是空的一块浅色,右端才是 DX 分数那块白牌)。 + item.append(el('small', 'dan-track-limit', `限时 ${fmtLimit(stage.limitSeconds)}`)); + tracks.append(item); } - field(dialog, '.dan-result-rankbox strong').textContent = VERDICT_TEXT[run.status] ?? '中断'; - field(dialog, '.dan-result-rankbox small').textContent = `${cleared.length} / ${run.stageCount} 道通关`; - - // 原作这一格是显示剩余生命的心;我们没有生命值,用「通关几道」占这一格。 - const life = field(dialog, '.dan-result-life'); + // 原作这一格是显示剩余生命的心;我们没有生命值,用心里的那个数字表示「四道里过了几道」。 + // 不写 4/4:数字图集里没有「/」这个字形,退回普通文字会被两边数字的描边吃掉看不见; + // 过没过、过几道,白框名字下面那行小字写得更清楚。 + const life = field(dialog, '.dan-life'); life.dataset.state = run.status === 'cleared' ? 'ok' : 'bad'; - field(dialog, '.dan-result-life span').replaceChildren( - document.createTextNode(String(cleared.length)), el('small', '', `/${run.stageCount}`)); - - // 原作的「总达成率」是四首歌达成率之和、「DX分数」是四首 DX 分数之和; - // 这里对应各道达成率之和与各道单题 rating 之和。 + field(dialog, '.dan-life-base').src = clearedRun + ? '/assets/maimai/life-base-green.png' : '/assets/maimai/life-base-red.png'; + field(dialog, '.dan-life-base').alt = clearedRun ? '通关' : '未通关'; + field(dialog, '.dan-life-count').replaceChildren(numText(String(cleared.length), 'n26', 46)); + + // 原作的「总达成率」是四首歌达成率之和、「でらっくスコア」是四首单曲 DX 分数之和。 + // 我们这边对应:各道达成率之和,以及**各道单题 rating 之和**(同一道题进 B50 用的就是 + // 这个单题 rating;之和只是展示,不等于 B50 总分真的涨这么多)。 const totalAchievement = scored.reduce((sum, stage) => sum + stage.achievement, 0); - const totalText = scored.length ? `${totalAchievement.toFixed(4)}%` : '—'; - const totalNode = field(dialog, '.dan-total-ach'); - totalNode.textContent = totalText; - totalNode.dataset.text = totalText; - field(dialog, '.dan-total-score b').textContent = run.totalRating === null ? '—' : run.totalRating.toFixed(1); + field(dialog, '.dan-total-num').replaceChildren( + scored.length ? achievementNumber(totalAchievement, 60) : numText('—', 'n90', 60, DX_NUM_COLOR)); + field(dialog, '.dan-dxscore-num').replaceChildren( + numText(run.totalRating === null ? '—' : String(run.totalRating.toFixed(1)), 'n90', 30, DX_NUM_COLOR, { outline: false, baseline: true })); const deltas = run.stages.map((stage) => stage.comparison?.ratingDelta).filter((value) => typeof value === 'number'); let note = run.status === 'cleared' diff --git a/public/dx-rank.js b/public/dx-rank.js new file mode 100644 index 0000000..1526f6a --- /dev/null +++ b/public/dx-rank.js @@ -0,0 +1,27 @@ +/** + * 达成率 → 段位。 + * + * 官方规则:一局游玩取得的评级**只与达成率有关**(见 maimai 的评级表,A 是 80.0000%~89.9999%、 + * SSS+ 是 100.5000% 起)。服务端 `src/dx/rating.ts` 的 `rankOf` 就是查这张表。 + * + * 这里放一份同表的前端副本,为的是让结算单上显示的**数字和段位永远对得上**: + * 数字可以被覆盖(调试用 `?achievement=`),段位跟着它走, + * 不会出现「88.8888% 配 S」这种自相矛盾的画面。 + * + * 两边必须一致:`tests/dx-rank.test.ts` 会把它和服务端那张表逐项比, + * 只改一边会让测试变红。 + */ + +/** 段位门槛,顺序从高到低;和服务端 RANK_LADDER 同源 */ +export const RANK_LADDER = [ + ['SSS+', 100.5], ['SSS', 100], ['SS+', 99.5], ['SS', 99], ['S+', 98], ['S', 97], + ['AAA', 94], ['AA', 90], ['A', 80], ['BBB', 75], ['BB', 70], ['B', 60], ['C', 50], +]; + +/** 查表取段位;低于最低门槛(50%)是 D */ +export function rankOf(achievement) { + const value = Number(achievement); + if (!Number.isFinite(value)) return 'D'; + for (const [name, floor] of RANK_LADDER) if (value >= floor) return name; + return 'D'; +} diff --git a/public/dx-result.css b/public/dx-result.css index 4728f23..12a5ecf 100644 --- a/public/dx-result.css +++ b/public/dx-result.css @@ -4,37 +4,451 @@ .dx-result-sheet { isolation:isolate; position:relative; overflow:hidden; padding:18px 28px 20px; line-height:1.25; background:linear-gradient(120deg,#27d8f3 0%,#77e6fa 13%,#d99af4 27%,#f1a8ed 36%,#fff391 52%,#fff5b0 65%,#ffd476 80%,#f6ace5 100%); } .dx-result-sheet::before { content:''; position:absolute; z-index:-1; inset:0; opacity:.5; background-image:linear-gradient(45deg,#fff7 25%,transparent 25%,transparent 75%,#fff7 75%),linear-gradient(45deg,#fff7 25%,transparent 25%,transparent 75%,#fff7 75%); background-position:0 0,13px 13px; background-size:26px 26px; mask-image:linear-gradient(130deg,#0008,transparent 32%,#0005 80%); } .dx-result-sheet::after { content:''; position:absolute; z-index:-1; inset:0; background:radial-gradient(ellipse at 50% 35%,#fff8,transparent 60%),linear-gradient(160deg,transparent 42%,#fff4 42.2%,transparent 45%),linear-gradient(20deg,transparent 76%,#fff5 76.2%,transparent 77%); } +/* 动态背景:把逆向出来的客户端结算背景页整页嵌进来(云、星屑、点阵、流星都在里面)。 + 那一页是固定 903x906,所以这里按【宽高比例里较大的那个】等比放大,再四周外扩 + --result-bg-bleed,让背景比结算单大一圈 —— 否则某一维差一点就会在边上露出底色。 + 静态的 result-bg-*.png 只是纯色底、没有云,保留给不支持 iframe 的场景兜底。 */ +.dx-result-bg { position:absolute; left:calc(-1 * var(--result-bg-bleed)); + top:calc(-1 * var(--result-bg-bleed)); width:903px; height:906px; + border:0; z-index:-2; pointer-events:none; transform-origin:0 0; + transform:scale(var(--result-bg-scale,0.9)); } +/* 官方结算底图:按本次评级换色。素材取自客户端 UI_RSL_BG_{A,B,S,SS,SSS}, + 名称里的字母就是成绩等级,运行时由脚本按等级替换(客户端里搜不到 + "UI_RSL_BG_" 字面量,是拼出来的;prefab 里只有一个 BG_Pat 挂 _A 占位)。 + 素材只有 A/B/S/SS/SSS 五档,没有 _C/_D,所以 C 以下都落到 b(最蓝的那档)。 + javascript 端在 .dx-result-sheet 上挂 data-bg。 */ +.dx-result-sheet[data-bg] { background-image:var(--result-bg); background-size:cover; background-position:center; } +.dx-result-sheet[data-bg="a"] { --result-bg:url('/assets/maimai/result-bg/result-bg-a.png'); } +.dx-result-sheet[data-bg="b"] { --result-bg:url('/assets/maimai/result-bg/result-bg-b.png'); } +.dx-result-sheet[data-bg="s"] { --result-bg:url('/assets/maimai/result-bg/result-bg-s.png'); } +.dx-result-sheet[data-bg="ss"] { --result-bg:url('/assets/maimai/result-bg/result-bg-ss.png'); } +.dx-result-sheet[data-bg="sss"] { --result-bg:url('/assets/maimai/result-bg/result-bg-sss.png'); } .dx-result-close { position:absolute; z-index:3; right:12px; top:12px; width:30px; height:30px; border:1px solid #ffffffa0; border-radius:50%; background:#ffffff90; color:#18327c; font-size:23px; cursor:pointer; } -.dx-result-kicker { display:flex; align-items:center; justify-content:center; gap:8px; font-size:12px; font-weight:900; letter-spacing:1px; } -.dx-result-kicker b { font-size:24px; font-style:italic; -webkit-text-stroke:1px #fff; filter:drop-shadow(1px 1px #243c8a); } -.dx-result-kicker > span { position:absolute; left:28px; top:24px; font-size:9px; letter-spacing:2px; opacity:.6; } +.dx-result-kicker { display:flex; align-items:center; justify-content:center; gap:3px; font-size:12px; font-weight:900; letter-spacing:1px; } +.dx-result-kicker b { display:inline-flex; align-items:flex-end; } +/* TRACK 标题:素材 UI_RSL_Text_TRACK 是 110x36 原生。官方抓图里它占屏幕宽度的 + 10.3%(93/903),弹窗宽 820 时对应约 84px 宽 => 高 84*36/110 ≈ 27.5px。 + 之前按"抓图尺寸"直接写 30px 是按 903 宽的画面算的,放在更窄的弹窗里就偏大。 */ +.dx-result-kicker .mm-track-label { display:inline-flex; align-items:flex-end; } +.dx-result-kicker .mm-track-label .mm-glyph { height:22px; width:auto; } +/* 轨道号码:不是文字,是官方数字精灵。 + prefab 里容器 NUM_TrackNumber 是 50x28 抓图 = 60x34 原生,放两位数字 + => 每格 30x34 原生。⚠️ 同位的 Outline 是一整块实心白(靠 Subtract 混合镂空), + 不能叠上来,否则数字被盖成全白 —— 这里用 CSS 描边近似。 */ +/* 轨道号码:两个数字当【一个整体】加描边。 + ⚠️ 不能一个数字一个数字地加 —— 那样两位之间的缝里会多出一圈"内描边"。 + 描边色 #123F80,四向零模糊(= 硬描边,不是投影),2px。 + 字间距做负的,两位咬在一起像一个字块。 */ +.mm-track-num { display:inline-flex; align-items:flex-end; + filter:drop-shadow(2px 0 0 #123F80) drop-shadow(-2px 0 0 #123F80) + drop-shadow(0 2px 0 #123F80) drop-shadow(0 -2px 0 #123F80); } +.mm-track-digit { position:relative; display:inline-block; width:15px; height:22.6px; } +.mm-track-digit + .mm-track-digit { margin-left:-5px; } +.mm-track-digit .mm-track-face { position:absolute; left:0; top:0; + width:15px; height:22.6px; } +.dx-result-kicker > span:last-child { position:absolute; left:28px; top:24px; font-size:9px; letter-spacing:2px; opacity:.6; } +/* TRACK 现在是官方那张精灵图,大小按结算单比例给 */ +.dx-result-track-word { display:inline-flex; } +.dx-result-track-word .mm-glyph { height:24px; } .dx-result-clear { margin:0; text-align:center; font-family:Georgia,'Times New Roman',serif; font-weight:900; font-size:66px; line-height:.98; letter-spacing:7px; color:#fff; background:linear-gradient(110deg,#2ce2ff 8%,#effcff 18%,#945bff 32%,#fff0fa 44%,#ff79bf 55%,#fff4a2 66%,#51dafa 78%,#fff 88%); background-clip:text; -webkit-text-fill-color:transparent; -webkit-text-stroke:1px #fff; filter:drop-shadow(1px 0 #23368b) drop-shadow(-1px 0 #23368b) drop-shadow(0 2px #23368b) drop-shadow(0 2px #fff) drop-shadow(2px 2px #5caef3); } .dx-result-subtitle { margin:2px 0 13px; text-align:center; font-weight:900; font-size:15px; color:#ffef8e; -webkit-text-stroke:.6px #46318a; text-shadow:0 2px #fff; } -.dx-result-track { position:relative; display:grid; grid-template-columns:62px 1fr 74px; gap:10px; align-items:center; padding:7px 8px; border:3px solid #6224b1; border-radius:12px 35px 35px 12px; background:linear-gradient(100deg,#bb51f1,#a846e8 45%,#8339dc); color:#fff; box-shadow:inset 0 1px 0 #edb5ff,0 3px 0 #f2c4fb; } -.dx-result-track::before { content:''; position:absolute; pointer-events:none; inset:0; border-radius:inherit; opacity:.24; background:repeating-conic-gradient(#4d0c97 0% 25%,transparent 0% 50%) 0 0/18px 18px; } -.dx-result-track-icon { z-index:1; position:relative; height:62px; border:2px solid #d2fbff; border-radius:4px; display:grid; place-content:center; overflow:hidden; font-style:italic; font-size:25px; font-weight:1000; color:#fff7aa; text-shadow:1px 2px #223d8e; background:conic-gradient(from 35deg,#8d49e9,#30cffa,#122e7e,#9f46cd,#8d49e9); box-shadow:0 0 0 1px #25468c; } -.dx-result-track-icon span { position:absolute; color:#ffffff70; font-size:70px; left:-5px; top:-15px; z-index:-1; } -.dx-result-track-content { position:relative; min-width:0; } -.dx-result-track-heading { display:flex; align-items:center; gap:10px; margin-bottom:6px; font-weight:900; font-size:14px; } -.dx-result-problem { font-size:10px; opacity:.85; } -.dx-result-track-heading i { margin-left:auto; padding:2px 10px; color:#f88728; background:#edf8ff; border-radius:12px; font-size:12px; font-style:normal; text-shadow:1px 0 #fff084; } -.dx-result-name { margin:0; padding:6px 12px; border-radius:7px; color:#fff; background:linear-gradient(#153c91,#092b73); box-shadow:inset 0 1px 3px #060d66; font-size:17px; font-weight:800; text-align:center; overflow-wrap:anywhere; } -.dx-result-level { z-index:1; display:flex; flex-direction:column; text-align:center; } -.dx-result-level small { font-size:11px; font-weight:800; } -.dx-result-level strong { font-size:28px; letter-spacing:-1px; text-shadow:2px 2px #562795; } -.dx-result-achievement-section { position:relative; margin:18px -28px 0; padding:0 28px 2px; } +/* ---------- 题目卡(原「乐曲卡」) ---------- + 整块卡是官方那张乐曲底板 UI_RSL_MBase_*(745x142 原生)撑起来的,不再是 CSS 画的圆角条。 + 卡片高度 = --mm-card-h;卡里横向位置一律用底板的百分比(x/745)、纵向用(y/142), + 所以改高度整块卡会等比缩放(上下略压扁,官方板本来也是横长条)。 + 构件坐标来自 prefab(原生 px,相对底板左上角): + 曲绘槽 x11..95 y47..131 → 放题号(板上那块纯黑就是游戏里的曲绘位) + 难度名 x96..304 y41..87 → 板上烤着 BASIC/ADVANCED/EXPERT…,要盖掉 + 标题条 x106..709 y88..128 → 放题名 + GameType x488..572 y51..75 → 官方 でらっくす 小牌 + LV/等级 x≈605..745(右侧圆盘,圆心≈(672,73)、r≈67) + 颜色:--mm-plate / --mm-plate-filter 由 maimai-glyphs.js 按 cfRatingColor 的 tone 挂上来 + (见那边的 CARD_TONES);底板主色被滤镜拉到 CF 那一档的颜色上,所以七档都能对上。 */ +.dx-result-track { + --mm-card-h:118px; --mm-digit-h:42px; --mm-lv-h:29px; + /* 深蓝标题条的左边缘(底板的百分比,x106/745≈14.2%,扣掉自压那条四周多出来的 2px + 原生 → 13.96%)。右上那行小字要和它对左,所以两边共用这一个变量, + 以后挪条子不会有一边忘掉。 */ + --mm-bar-left:13.96%; + /* 等级组(LV + 数字)在右侧圆盘里的整体抬升量,按卡高算。 + 圆盘 = 底板右端那个圆头,按底板 alpha 轮廓拟合(Kasa + 几何精修,残差 0): + 原生圆心 (672.6,72.7)、r=70.6,映射到卡片上(1200 视口,卡 749x118)就是 + 圆心 (676.2,60.4)、rx 71.0 / ry 58.7,即盒子 x 605.2..747.2 / y 1.7..119.0 + (底板本身 745x142、alpha 只到 x742 / y139,右边和下边被图边裁掉一点)。 + ⚠️ 圆心的 y(60.4)比深蓝条顶(71.45)只高 11px,而数字组有 42px 高 —— + 真按圆心居中会让数字往条子里扎 8px。所以「居中」取的是**圆盘露在条子上面的 + 那半个碗**:圆盘顶 1.7 ~ 条子顶 71.45。 + 这一版实测(1200 视口):整组盒 y 22.9..64.9,盒底离条子顶 6.6px, + 数字墨迹(含描边)底边离条子顶 8.7px;右边留 1.85%(见 .dx-result-level)。 + ⚠️ 只动整组:LV 与数字彼此的相对位置不变。 */ + --mm-level-rise:13.8%; + /* 三处文字的字号按官方抓图里的「字高 ÷ 卡片高」反推(抓图卡片 616x117, + 标题条实测 33px、EXPERT 字高 19px): + 难度名 19/117 = 16.2% → 这里是 22 个字的「赛制 + 平台」,塞不进那一段; + 15px(≈9.5%)是上限,再大窄屏那档就会顶到 52% 的盒子 + 标题 字高 ≈14/117 = 12% → M PLUS 1p 800 的 cap 高 0.73em,故 18px + 题号 官方曲绘槽放的是曲绘、没有字,按标题同族的重量给 19px */ + --mm-title-size:18px; --mm-heading-size:15px; --mm-slot-size:19px; + /* 「待定」那两个字自己一份字号:它跟着标题字号走的话,右半区的排版会被标题的改动带偏 */ + --mm-pending-size:16px; + /* 未评级的底板用官方 **UTAGE**(宴)卡,不是灰 DMY —— 见 maimai-glyphs.js 的 CARD_TONES。 + --mm-field 是 cover 那圈紧邻的板面平色(#ff86fe),cover 拿它盖掉板上烤的 + 「U・TA・GE」;换了底板就必须跟着换,否则粉面上会留一块深色方块。 + ⚠️ 这块补丁里压着一片洋红 (249,45,218),但补丁四周是浅粉 —— 取色取外面那圈。 */ + --mm-plate:url('/assets/mm-result/UI_RSL_MBase_UTG.png'); + --mm-field:#ff86fe; --mm-plate-filter:none; + --track-main:#7b8391; --track-deep:#3f4652; + position:relative; height:var(--mm-card-h); margin:2px 0 3px; color:#fff; + /* ⚠️ 下面这条硬投影只留给 dan.js 的单题结算(它共用 .dx-result-track、但没有底板)。 + 精灵卡(带 data-rating-tone 的那张)会把这条撤掉,原因见后面那条同名注释。 */ + box-shadow:0 4px 0 var(--track-deep),0 8px 16px #00000033; +} +/* 底板那一层。滤镜只作用在这里 —— cover 也在里面,所以盖上去的那块底色会一起被拉过去 */ +.dx-result-track-art { position:absolute; inset:0; filter:var(--mm-plate-filter); } +.dx-result-track-plate { position:absolute; inset:0; background:var(--mm-plate) 0 0/100% 100% no-repeat; } +/* 精灵卡(problemCard)不要那条 `0 4px 0` 的硬投影 —— 它不是卡片的边,是 CSS 盒子的矩形边。 + 卡片真正的剪影在底板精灵里(圆角 + 右端圆盘),而外投影是按**盒子**投的,于是画出来是 + "卡片圆角底下面一条游离的方角暗杠": + · 精灵底部还多出 2 行全透明(MST/BSC/ADV/EXP/DMY 在 y140..141,UTG 在 y139..140), + 盒底比剪影底低 2.5px(1200 视口,卡高 118)→ 暗杠和卡片之间隔着一道 2.5px 亮缝, + 看着不是"卡片的边",而是浮在下面的一条杠; + · 端点按盒子方角走 → 跨满 764px 盒宽,而卡片在那一行只有 722px,左右各多出约 10px, + 正好压在圆角外面那两块本该透出背景的地方。 + ⚠️ 换 background-size / 裁精灵都修不掉它:底板本身**没有**圆角轮廓以外的画(逐行验证过 + alpha 每行都是连通的一段,包围盒 x2..742 / y2..139,框外全透明),暗色来自 CSS 盒子。 + 也试过用底板当 mask 把这条投影改成随圆角走的"底座"——暗杠确实贴上了卡片,但卡片底下 + 还是留着一条 4px 暗带,用户投诉的就是这条带,等于没解决,所以没采用。 + ✅ 环境投影也一并撤掉 —— **外投影按盒子投,盒子是方的,卡片是圆的**。 + 实测(1200 视口,纯色底 #ffcc66 上逐像素扫,卡盒 x199.5..963.5 / y95.1..213.1): + `0 8px 16px #00000033` 在 y215 / y222 两行铺出 x≈205..975 的一片暗色,**两端是方角**; + 而卡片自己的剪影在那两行只到 x≈203..938(左下圆角 + 右端圆盘收进去), + 多出来的那截方角暗块就是「抠图没抠干净」的观感来源。 + 外投影被规范裁在 border-box 之外,所以它在卡片顶部的透明带里不显形、 + 只在盒子外圈显形 —— 最扎眼的就是盒子底边那条比卡片还宽的方角暗杠。 + ✅ 改成把投影挂在**底板那一层**(drop-shadow 跟 alpha 走 = 跟卡片剪影走): + `.dx-result-track-plate` 是这张卡里**唯一**带底板 alpha 的层。见下面那条 filter。 + ⚠️ 只给精灵卡(data-rating-tone 由 dx-timer.js:76 挂):dan.js:436 的单题结算也写 + .dx-result-track,但它没有底板,那条硬投影是它唯一的底边,不能跟着一起撤。 */ +.dx-result-track[data-rating-tone] { box-shadow:none; } +/* 底板剪影投影。⚠️ 只能挂在这一层: + · 挂 .dx-result-track-art 不行 —— 那层要套 --mm-plate-filter,父层再加 filter + 会把两条滤镜串成一条链,且 cover 补丁也会被一起投影; + · 挂 .dx-result-track 就是回到方盒投影(上面那段否掉的东西)。 */ +.dx-result-track[data-rating-tone] .dx-result-track-plate { + filter:drop-shadow(0 8px 14px #00000040); } +/* 整块卡上提,把它拉到 CLEAR! 词标跟前。 + 底板精灵顶上有 37/142 个卡高是**全透明**的(只有右端圆盘冒头,见 UI_RSL_MBase_* 的 + alpha 轮廓),所以光看盒子间距看不出这段空档有多虚 —— + 1200 视口实测(卡 764x118):CLEAR! 墨迹底 y108.1 → 卡面实体顶 y165.1 = 57.0px, + 其中 30.75px 就是这条透明带,盒顶离墨迹底只有 26.3px。 + margin-top 从 2px 改成 calc(2px - .35 * --mm-card-h) = -39.3px 后整块卡上移 39.3px + —— ⚠️ 不是 41.3px:原来那 2px 本来就被上面 15px 的 margin-bottom 折叠吃掉了 + (相邻兄弟的正外边距取 max,负值再叠加),所以位移 = 0 + 39.3。 + 上移后同一视口实测: + 盒顶 134.4 → 95.1,卡面实体顶 165.1 → 125.8,离词标墨迹底只剩 17.7px; + 右端圆盘顶 136.0 → 96.7,比词标墨迹底还高 11.4px,**探进词标那条带子里** —— + 这是允许的:圆盘在卡宽 80.8% 起(1200 时 x841..963),词标墨迹右缘只到 x744, + 横向差 97px,圆盘盖不到任何一个字;卡面实体横跨整卡宽,仍低于墨迹底 17.7px。 + 逐像素验证(1200 视口,词标墨迹那一带 x430..755 / y64..125,把卡 visibility 藏掉对比): + 最大像素差 7(= 结算背景 iframe 的动画噪声),字形墨迹被盖掉 0 个像素。 + ⚠️ 选择器必须限定在有底板的精灵卡上(data-rating-tone 由 dx-timer.js:76 挂): + dan.js:436 的单题结算共用 .dx-result-track,但它没有底板、也不是这个版面, + 上提会把它的版式带坏(dan 的 data-rating-tone 是挂在 .dx-result-total 上的)。 */ +.dx-result-track[data-rating-tone] { margin-top:calc(2px - var(--mm-card-h) * .35); } +/* 盖掉板上烤着的难度名:填同一张板那一带的干净底色(不是渐变,板面在那里是平色), + 位置按 prefab 量(原生 x96..304 / y41..87)。不带圆角,否则边上会露出来一线 */ +.dx-result-track-cover { position:absolute; left:12.89%; top:28.87%; width:27.92%; height:32.39%; background:var(--mm-field); } +/* 深蓝标题条:板上本来有一条,但蓝/青两档整块板要转色相,navy 也会被带着偏, + 所以恒压一条自己的(比板上那条四周各多 2px 原生,确保盖住) */ +.dx-result-track-bar { position:absolute; left:var(--mm-bar-left); top:60.56%; width:81.48%; height:30.99%; border-radius:15px; background:#0e3d7e; box-shadow:inset 0 2px 0 #ffffff26,0 1px 0 #062155; } +/* 曲绘槽 → 题号。原来用 ui-monospace(等宽),官方界面里没有这个字体 —— 换成官方那套 + M PLUS 1p(仓库自托管,见 theme.css 的 --mm-sans),重量取 800 和标题同级。 + 等宽改比例字体后字面变宽,字号保持 19px:'1999A' 实测 64px,槽宽 86px,仍然放得下。 + ★ 面板本身照段位结算页那一格曲绘槽画(dan.css 的 .dan-track-jacket,见 dan.js:944): + 底色是**本档主色的浅色版** —— color-mix(in srgb, 主色 42%, #fff),和 dan 那边同一个配比, + 主色就是挂在卡上的 --track-main(cf-rating-colors.js 的 CF_TONE_COLORS[tone][0], + dan 那边叫 --tc1、取的是同一张表);再压一层 2px 半透明白内环 + 一点外投影, + 字照 dan 那边改成深蓝 #17265c(原来是白字压黑晕)。板上那块纯黑只是「游戏里盖曲绘的槽」, + 不是我们该拿来做底的东西。 + ⚠️ 不学 dan 的 3px 圆角:底板那块纯黑是**方角**(实测 MST 板 x11..94 / y47..130 全是 + (0,0,0),UTG 板同形),一圆角四个角就会露出底板的黑三角,方角才对得上。 + ⚠️ 底板那块黑比这个盒子还大一圈(MST 的黑从 y47 起、UTG 从 y46 起,盒子顶边 33.1% = 47; + 右侧 AA 列在 x95),所以面板外加 1px 同色 spread 盖住那一行 —— 跟深蓝标题条 + 「四周各多 2px 原生」同一个理由,一行黑边压在浅色面板上面会非常显眼。 + ⚠️ 面板不吃 --mm-plate-filter:那层滤镜在 .dx-result-track-art 里,槽是它的兄弟节点; + --track-main 已经是目标档位的颜色了。 */ +.dx-result-track-slot { --mm-slot-fill:color-mix(in srgb, var(--track-main) 42%, #ffffff); position:absolute; left:1.48%; top:33.1%; width:11.28%; height:59.15%; display:grid; place-items:center; overflow:hidden; font-family:var(--mm-sans); font-weight:800; font-size:var(--mm-slot-size); line-height:1; letter-spacing:.01em; color:#17265c; background:var(--mm-slot-fill); box-shadow:inset 0 0 0 2px #ffffff4d,0 0 0 1px var(--mm-slot-fill),0 2px 6px #00000024; } +/* 难度名那一段 → 赛制 + 平台(题号在左边曲绘槽里,这里不重复)。 + 盒子比 cover 宽:cover 只要盖住板上那几个字,文字用多少算多少,右边留白不影响。 + 官方这一段是「难度名」一个词,同一字号同一白色;这里两段也统一成 800 的白字, + 只靠 .06em 的字距把平台分开 —— 原来 13px/900 的 Arial 加 0.84em 的小字, + 两截大小不一,反而像两种层级。 + 左右:这两个元素自己都没有 padding / border,所以「文字左边缘对条子左边缘」 + = 两边的 left 取同一个值(--mm-bar-left)。原来这里是 13.4%,比条子左了 + 0.56%(卡片 749px 时 4.19px),标题就被那条深蓝比下去了一截。 + 纵向:盒子顶边留在 28.87%(= cover 那块补丁的顶边,和官方难度名所在的横带对齐), + 只把盒子**收短**到 26%,因为原来 28.87%+32.39% 的底边落在 61.26%,比深蓝条顶 + (60.56%)还低 0.7 个百分点 —— 也就是这一行字的下伸区压在条子上。 + 收短后(1200 视口,卡 118px):盒 y 34.1..64.7,盒底离条子顶(71.45)6.7px; + 墨迹底边 54.9,离条子顶 16.5px ≈ 1.45 倍字面高(原来 58.7 / 12.8px)。 + 盒子仍然比行高(18.75px)高,overflow:hidden 不会切到字。 */ +.dx-result-track-heading { position:absolute; left:var(--mm-bar-left); top:28.87%; width:52%; height:26%; display:flex; align-items:center; gap:6px; overflow:hidden; font-family:var(--mm-sans); font-size:var(--mm-heading-size); font-weight:800; letter-spacing:.01em; white-space:nowrap; text-shadow:0 1px 2px #000000a6,0 0 4px #00000073; } +.dx-result-track-heading .dx-result-problem { font-size:1em; letter-spacing:.06em; } +/* GameType:官方 でらっくす 小牌(84x28 原生) */ +.mm-gametype { position:absolute; left:65.5%; top:35.9%; width:11.28%; height:16.9%; } +.dx-result-track .mm-gametype .mm-glyph { width:100%; height:100%; object-fit:contain; } +/* 标题条 → 题名。官方那条里的曲名是官方哥特体(M PLUS 1p 那一类)、纯白、字距近乎为零, + 所以这里也用 --mm-sans 的 800(原来落到 Arial,'a/e/R' 的字形和官方不是一路)。 + ⚠️ 选择器要写两段类:theme.css 里 `body.mm-theme dialog h3` + (0,1,3)直接改 h3 的颜色,只写 .dx-result-name(0,1,0)会被它压过去 —— 题名会变成深蓝 */ +.dx-result .dx-result-name { position:absolute; left:14.2%; top:61.5%; width:80.6%; height:29.2%; margin:0; display:grid; place-items:center; padding:0 14px; color:#fff; } +.dx-result-name .dx-result-title { max-width:100%; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; font-family:var(--mm-sans); font-size:var(--mm-title-size); font-weight:800; letter-spacing:.005em; text-shadow:0 1px 2px #04123a99; } +/* 右侧圆盘 → LV + 难度分。数字是官方 UI_CMN_MusicLevel_{族}_{格}, + 格子 48x60,第 14 格就是那个小 LV。 + ★ 两条锚点(本次改动):**LV 钉死在一个 x 上不动,数字(或「?」)自己以圆盘圆心为心居中**。 + 改之前整组靠右对齐(right:1.85%),LV 的 x 跟着数字宽度跑:4 位数时 LV 在 599, + `?` 时整个组缩到 50px 宽、LV 被推到 685(1200 视口,实测差 85px)—— + 用户说未评级那档「LV ?」看着太靠右,就是这条的锅。 + 圆盘圆心(原生 672.58 / 745 = 90.279% 卡宽,见 .dx-result-track 上 --mm-level-rise 的注释): + · 数字:left:90.279% + translateX(-50%) —— 3 位、4 位、`?` 的**盒中心**都落在圆心上。 + · LV:右边缘钉在「最宽的那档(4 位数)数字盒的左边缘」再左移 2px(= 原来那条 margin-right), + 所以 LV 的位置只跟 --mm-digit-h 有关,跟眼前这串数字几位数无关。 + 4 位数的盒宽 = (4×0.8 − 3×0.19) × 格高 = 2.63×格高,故盒左 = 90.279% − 1.315×格高。 + 这一档同时最接近官方:抓图里 LV 墨迹右缘到「13」墨迹左缘是 5.9 原生 px, + 这里(2px 盒距 + 两侧各自的墨迹边距)折出来约 6.3px。 + ⚠️ 官方抓图里 LV 墨迹右缘(native 631.9)离圆心 40.7px,比这里还靠右 5px 左右 —— + 但官方数字只有两位、且官方 LV 画得比我们这支大(墨迹宽 31 native vs 我们 19), + 照抄那个 x 的话 4 位数会直接压到 LV 上。以「不叠字」为准。 + ⚠️ 这里用 calc(90.279% − k×格高) 而不是纯 %:数字是固定 px(不随卡宽缩), + 窄屏卡片缩得比数字快,纯 % 会让 4 位数在 430 视口压回 LV 身上约 9px。 + 纵向:两行都用 bottom 顶在**和改之前同一条线**上(盒底 = 49% + --mm-level-rise − 格高/2, + 由原来「top:calc(18% − rise),高 66%,居中」推出来的:0.18−0.138+0.33 = 0.372, + 故盒底离卡底 62.8% − 格高/2 = 49% + rise − 格高/2),LV 再按老规矩抬 0.095×格高。 + 所以 1200 视口下数字盒底仍是 y64.9、LV 盒底 y60.9,1200/820/430 三档实测逐像素对得上。 + ⚠️ 新版卡这一格挂的是 `.dx-result-level-disc`(problemCard 里两个类一起写)。 + 下面那条 `.dx-result-level` 的右对齐规则**原样留着**:dan.js 的单题结算弹窗 + (dan.js:439)还在用同一个类名装「LEVEL + 分数」两段文字,那套版式不能跟着动。 + ================ 以下是改之前那条规则,dan 的单题结算靠它 ================ + right 1.85%(1200 视口 749px 卡 → 13.8px):让整组右边缘收进圆盘里, + 离圆盘右缘 12.0px、离卡片右缘 13.8px。原来 1.2% 时整组右边缘(740.0) + 离圆盘右缘只剩 7.2px,最后一个数字的墨迹几乎贴着卡边。 + 纵向:整组一起抬 --mm-level-rise,理由见那条变量上的注释。 */ +.dx-result-level { position:absolute; right:1.85%; top:calc(18% - var(--mm-level-rise)); height:66%; display:flex; align-items:center; justify-content:flex-end; } +/* 新版卡(problemCard)的那一层:铺满整张卡,当绝对定位的坐标系用。 + ⚠️ 必须显式写 height:100% 和 right:auto —— 只写 inset:0 的话上面那条的 height:66% + 会赢过 bottom(绝对定位里 top/height/bottom 三者同时有值时不看 bottom)。 */ +.dx-result-track .dx-result-level-disc { position:absolute; left:0; top:0; right:auto; width:100%; height:100%; display:block; } +/* 「待定」那条现在没有调用方了:无难度一律走 UTAGE 卡 + 「LV ?」(见 maimai-glyphs.js + 的 problemCard)。样式留着 —— 「LV ?」和「待定」是同一件事,并排写会重复; + 要换回来改 problemCard 那一行就行。 */ +.dx-result-level-pending { font-size:var(--mm-pending-size); font-weight:900; text-shadow:0 2px 3px #000000a6; } +/* 这一层只负责挂 --mm-plate-filter(等级精灵是按族取图的,靠它拉到本档颜色上), + 铺满整张卡、里面两个子元素各自绝对定位。 */ +.mm-level { position:absolute; inset:0; filter:var(--mm-plate-filter); } +/* 数字那一串(含未评级时的「?」):盒中心 = 圆盘圆心。宽度由内容定, + translateX(-50%) 保证不管 3 位还是 4 位,中心都落在同一个点上。 */ +.dx-result-track .mm-level .mm-level-digits { position:absolute; left:90.279%; bottom:calc(49% + var(--mm-level-rise) - var(--mm-digit-h) * .5); transform:translateX(-50%); display:inline-flex; align-items:flex-end; } +.dx-result-track .mm-level .mm-level-digit { height:var(--mm-digit-h); width:auto; } +/* 等级未知时顶在数字位置上的那个「?」。官方图集 UI_CMN_MusicLevel_* 每族 15 格 + (0-9 加 + - , . 和 LV),**没有问号**,只能排字。 + 字号取 --mm-digit-h:数字精灵的白芯占格高 0.7(实测 EXP/MST 族芯 42/60, + 整块墨迹含 2px 描边是 48/60),M PLUS 1p 800 的问号墨迹高约 0.72em, + 42px 字号 → 30.2px,和白芯 29.4px 基本等高。 + 基线:这一族的字形度量 A/D = 1.0667/0.3333em,line-height:1 时基线落在盒底 + 上方 0.1333em = 0.1333×格高 —— 正好和数字白芯的底边(格底上方 0.1333×格高) + 同一根线,所以不用再补 translate。描边用四向 drop-shadow 补一个 + 0.0333×格高(= 精灵那 2px/60 描边)的硬边,颜色取卡片自己的深色 --track-deep。 + ⚠️ 它现在在 .mm-level-digits 里,盒中心同样落在圆盘圆心 —— 判据量的是盒中心, + 问号自己的右边距(advance 减去墨迹)不影响这个中心。 */ +.dx-result-track .mm-level .mm-level-unknown { + font-family:var(--mm-sans); font-weight:800; font-size:var(--mm-digit-h); line-height:1; color:#fff; + filter:drop-shadow(calc(var(--mm-digit-h) * .0333) 0 0 var(--track-deep)) + drop-shadow(calc(var(--mm-digit-h) * -.0333) 0 0 var(--track-deep)) + drop-shadow(0 calc(var(--mm-digit-h) * .0333) 0 var(--track-deep)) + drop-shadow(0 calc(var(--mm-digit-h) * -.0333) 0 var(--track-deep)); +} +/* 数字字距:精灵格子 48x60,字形只占中间一块,直接并排(=整格宽)会散开。 + 官方抓图里量出来的推进量是 25.8px(缩放 0.71 时格宽 34.1px),即 **格宽的 0.757**, + 换算成格子高就是 -0.19×格高 —— 官方 '1''3' 之间的墨迹间隙只有 5px(≈字高的 13%), + 这里原来 33.6px 的推进量间隙有 12.6px(≈34%)。所以按格高收 0.19。 + ⚠️ 选择器必须带 .dx-result-track 前缀:maimai-glyphs.css 在 dx-result.css 之后加载, + 单类选择器会被它的源序压过去。 */ +.dx-result-track .mm-level .mm-level-digit + .mm-level-digit { margin-left:calc(var(--mm-digit-h) * -0.19); } +/* LV:位置见上面那段(left 里 1.315 = 4 位数盒宽的一半 / 格高 = (4×0.8 − 3×0.19)/2, + translateX(-100%) 把盒子整体挪到那个点的左边,即 LV 的右边缘钉在那个点上)。 + 垂直:官方抓图里 LV 的墨迹底边(y171)和数字的(y169/170)基本在同一条线上, + 但这边数字自带一圈深色描边,描边底边比白芯低 2.8px —— 照抄"墨迹对齐"时 LV 看着就往下坠。 + 抬 0.095×格高(默认 42px → 4px,就是原来那条 margin-bottom 的量), + LV 的墨迹底边正好落在数字白芯的基线上。 */ +.dx-result-track .mm-level .mm-level-lv { position:absolute; left:calc(90.279% - var(--mm-digit-h) * 1.315 - 2px); bottom:calc(49% + var(--mm-level-rise) - var(--mm-digit-h) * .405); transform:translateX(-100%); height:var(--mm-lv-h); width:auto; margin:0; } +/* 达成率那一段也按「左列 + 侧栏」分栏(列定义与 .dx-result-main 共用同一组变量), + 数字放进第一列居中 —— 这样它的水平中轴和段位/徽章/DX 评分框天然是同一条, + 不用去凑 -6px 那种魔法数(原来那条 translateX(-6px) 已删)。 */ +.dx-result-achievement-section { position:relative; display:grid; + grid-template-columns:minmax(0,1fr) var(--mm-side-w); + /* ⚠️ 只能用 column-gap:写成 gap 会连 row-gap 一起给成 28px, + 而这里的两行(ACHIEVEMENT 标题行 / 数字行)是**上下相邻**的, + 凭空多出 28px 行距 —— 那正是"标题和数字中间太空"的来源。 */ + column-gap:var(--mm-col-gap); row-gap:0; + margin:18px -28px 0; padding:0 28px 2px; } +.dx-result-achievement-section > * { grid-column:1; } +/* 达成率底下那条**白色半透明横带**(用户口径「数字下面有一条白色半透明背景条」)。 + 素材:`UI_RSL_Base_Achievement.png`(408x122)—— 不是纯色带,是**一条带框的面板**: + 左端三枚淡紫箭头、右端一枚、上下各一道淡紫边线,中间才是那块半透明白(中心 RGBA + 255,255,255,204)。所以这里用 border-image 九宫格铺:两端箭头/边线按原比例, + 中间拉伸 —— 直接 `background-size:100% 100%` 会把箭头横向拉成 2 倍、纵向压扁。 + 素材结构(逐行/逐列量):内容行 y12..113(上边线 12 / 下边线 9), + 左端箭头组宽 ≈115、右端箭头 ≈38。我们要的带高 66 → 统一 ×0.65 折到目标尺寸。 + 位置:官方(903 抓图)条 y306..371、数字墨迹顶 y277 → + **条顶 = 数字墨迹顶 - 0.44×条高**(≈ -29px),也就是数字下缘那条带正好咬着数字。 + 我们这边数字行上移后(见下面那条 margin-top:0)条顶落在 section 顶 +70。 */ +.dx-result-achievement-section::before { content:''; position:absolute; z-index:-1; + left:0; right:0; top:82px; height:66px; pointer-events:none; + border:0 solid transparent; + border-image:url('/assets/mm-result/UI_RSL_Base_Achievement.png') 12 38 9 115 fill / + 7.8px 24.7px 5.9px 74.8px stretch; } .dx-result-achievement-top { display:flex; gap:12px; align-items:center; } -.dx-result-achievement-label { margin-left:-28px; padding:5px 16px 5px 28px; background:linear-gradient(100deg,#082c92,#057cf4); border-radius:0 0 16px 0; color:#fff; font-size:14px; font-weight:900; white-space:nowrap; } +/* 这一条原来自己画了蓝渐变底 + 右下圆角(linear-gradient(100deg,#082c92,#057cf4) / border-radius:0 0 16px 0) + —— 那是改造前文字标签的样式残留,会在 ACHIEVEMENT 上方留下一根空蓝条。 + 现在标题只用原版精灵,底由精灵自带,所以这里不再画任何底色。 */ +.dx-result-achievement-label { margin-left:-30px; margin-top:-10px; padding:0; background:none; + border-radius:0; color:inherit; } .dx-result-achievement-label > span { margin-left:12px; color:#80ebff; letter-spacing:2px; } -.dx-result-best { display:flex; align-items:center; border:2px solid #d9e8f2; border-radius:20px; background:#073579; box-shadow:0 2px 0 #4c6395; font-size:10px; font-weight:700; color:#d0e9ff; white-space:nowrap; } -.dx-result-best > span:first-child { padding:4px 10px; } -.dx-result-best b { display:block; color:#fff; font-size:13px; } -.dx-result-achievement-delta { padding:8px 14px; align-self:stretch; display:flex; align-items:center; color:#665505; font-size:17px; font-weight:900; background:linear-gradient(#fff69d,#f8d549); border-radius:0 20px 20px 0; box-shadow:inset 0 1px #fff; } -.dx-result-achievement-delta[data-direction=down] { color:#46578a; background:linear-gradient(#ebf2ff,#cad9f4); } -.dx-result-achievement { position:relative; margin:8px -28px 0; padding:3px 28px; font-size:clamp(44px,7.6vw,76px); font-weight:1000; letter-spacing:-3px; font-variant-numeric:tabular-nums; line-height:1.05; color:#ffe85a; -webkit-text-stroke:1.4px #ba3915; background:linear-gradient(100deg,#ffffffd9,#ffffff70 80%,#fff2); text-shadow:2px 2px #fff,3px 4px #e68c31; } -.dx-result-record { position:absolute; right:30px; bottom:23px; transform:rotate(-8deg); padding:4px 7px; color:#e0373d; border:2px solid #e0373d; border-radius:4px; font-size:11px; letter-spacing:1px; font-weight:900; background:#fff3cda8; } +/* 看板娘:站在右半区(.dx-result-side)的右下角,整个人沉在所有内容下面。 + ⚠️ .dx-result-sheet 有 isolation:isolate,所以这里的 z-index:-1 只会沉到 + 「结算单自己的背景之上、单子里所有内容之下」—— 立绘不会盖住任何文字或按钮, + 成绩板只要半透明就能自然浮在它上面(见文件末尾「右半区」那一节)。 + ✅ 竖直:**头锚在卡片下沿,身高一路探到「下一步」按钮那条带子**(用户口径: + "高度一直往下探到下一步那个按钮都没太大关系")。 + 立绘的大小**完全由框高决定**:两张图都是竖长条(默认远端 lolicount SVG 150x420, + 本地兜底立绘 524x812),contain 一定先顶满高度,框宽再大也不会让画变大。 + 所以改大小 = 只改 --mm-mascot-h。 + ⚠️ 锚点用 top 而不是 bottom:改前用 bottom 时,下界被页脚顶(763)卡住, + 只能到 530。现在挂 top,量的是「卡片下沿 → 侧栏顶」这段距离 —— + 跨视口实测(820/1000/1200/1440 宽 × 800/741/900 高,六档)**恒为 181.7px** + (这个数只由「卡片以下、主行以上」的内容决定:最早 178,达成率那一段收掉 + 28px 行距后变 169.7,用户要求把标题/数字再拉开 12px 后又回到 181.7 —— + **动上面任何一块都要回来重对一次**),而「侧栏底 → 单子底」恒为 129px。 + 所以 top = -(181.7) + 卡片下沿留白 6px ≈ -176px; + 身高上限 = 181.7 + 侧栏高 334 + 129 = 644.7px,现取 632 留 12px 余量。 + ✅ 取 --mm-mascot-h:632px(= 641 减 9px 余量,再用 top:-172px 让头顶离卡片留 6px): + · 框 y 235..867,单子裁切边在 870 → 框底留 3px(框底是画布的透明边,切到也无所谓) + · contain 后画出来 225.7x632,其中**人**(SVG 画布里角色墨迹只占画布宽 109/150) + 是 164 x 603,落在 x740.3..904.2 / y235.3..838.6,中心 822.2 + 下面那点偏心补偿 = 824.5 + · 改前(530 / bottom 锚):人是 137.6 x 504.7 —— 人 +19% 线性尺寸、+46% 面积 + · 成绩板上沿 472.3:板以上露出 237px,**板以下还露出 123.6px 的腿和靴子** + (530 那版只有 21px,等于刚过板底就断腿)→ 人可见 52% → **60%** + · 脚底 838.6 落在「下一步」按钮(782..845)后面 —— 人被按钮挡住是允许的: + 立绘 z-index:-1,按钮照常可点;官方也是立绘沉在最底下 + ✅ 横向:**人和成绩板水平居中对齐**(用户口径)。不是调 right,而是 + 把框改成「以侧栏水平中点为心」——`left:50% + translateX(-50%)`, + 侧栏中点就是成绩板中点(板整栏宽:1200 视口 x674.5..974.5 → 中点 824.5), + 所以人和板天然共中线,换任何比例的素材都成立 + (原来 right:-24px 把人推到 x893.6,偏右 69px)。 + ⚠️ --mm-mascot-shift:默认那张 SVG **自己的偏心**,不是版式需要。 + 它的画布里角色墨迹在 x12.4..121.4 / 画布宽 150 → 墨迹中心 66.9, + 比画布中心 75 靠左 8.1(=画布宽 5.4%),而墨迹的**质量中心**也是 66.8 + (逐段算过:头 65.4 / 上身 65.2 / 下摆 65.0),所以按 bbox 居中就是按视觉居中。 + 图宽 = 0.357 × 框高(150/420),故补偿 = 0.054 × 0.357 × H = 0.01929 × H + —— 写成随 --mm-mascot-h 缩放的 calc,改身高不会把中线带偏。 + ⚠️ 换素材要重测:本地兜底 navichara-01-p1-02.png 的墨迹占满整张画布 + (bbox x0..524),中心本来就在画布中线上,挂着这一下会反过来偏右 12px + —— 164px 宽的人身上 12px 是 7%,介意就把 --mm-mascot-shift 设成 0。 + ⚠️ 框宽 340(原 430):框只是 contain 的容器,默认素材是高度受限,340 一样画出 + 225.7x632,看不出区别。收窄是为了**对称外扩不越界**:框以侧栏中点为心, + 侧栏中点到单子裁切边只有 178px(侧栏半宽 150 + 内边距 28),430 的框会伸出去; + 340 → 两侧各外扩 170,安全。宽比例素材(比如去掉 theme 后 400x420)在 340 里 + 是 340x357,仍然比现在宽 2 倍。 + ⚠️ ACHIEVEMENT 那一行(y286.3 起、盒横跨整张单子)就在头顶这个高度上: + 数字墨迹到 x≈701 为止;人左缘 740.3,横向差 39px,盖不到任何一个字。 + ⚠️ 不再写 max-height:740px 那一档的矮窗覆盖:那时以为矮窗几何会变,实测 + (1200x741 vs 1200x900)卡片下沿→侧栏顶、侧栏底→单子底、侧栏高**三个数完全一样**, + 矮窗只是让弹窗滚动,单子内部几何不变,再压一遍只会让 740 以下的窗口里人变小。 */ +.dx-result-mascot { --mm-mascot-h:632px; + --mm-mascot-shift:calc(var(--mm-mascot-h) * .01929); + position:absolute; z-index:-1; left:calc(50% + var(--mm-mascot-shift)); top:-176px; margin:0; + width:min(340px,46vw); height:var(--mm-mascot-h); + transform:translateX(-50%); + display:flex; align-items:flex-end; justify-content:center; + pointer-events:none; } +/* 看板娘图:contain 铺进框,**水平居中、顶部对齐**。 + ⚠️ object-position 的纵轴必须是 top、不能是 bottom:框高 632 是给默认那张竖长条 SVG + 量身定的(它高度受限、正好填满框),但换别的素材就未必 —— + · 本地兜底立绘 524x812 是**宽度受限**,画出来 340x527,比框矮 105px; + · 站上别的 theme(比如去掉 theme 后的 400x420 画布)更矮。 + 纵轴写 bottom 的话这些"矮素材"会整体沉到框底 → 头埋进成绩板后面(实测头在 y506, + 板子上沿才 468);写 top 则是**头顶永远贴着卡片下沿**,矮的素材脚下留一截空 —— + 这一条比"脚踩按钮"重要,因为头是识别"这是个人"的部位。 + (默认那张 SVG 高度正好填满,两种写法在它身上完全等价,所以这个改动不影响已定稿的观感。)*/ +.dx-result-mascot-img { width:100%; height:100%; object-fit:contain; + object-position:center top; filter:drop-shadow(0 8px 14px #16307a4d); + /* 去掉看板娘图自带的那行计数数字(用户口径「右侧人物下面的计数器数字可以删掉」)。 + 默认那张 lolicount SVG 的结构是:外层画布 150x420,角色的 只占 y0..400, + 计数是画在外层底部的 74 —— 正好在最后 + 20/420 = 4.762% 的横带里。图是高度受限(object-fit:contain + 高比例的竖长条), + 渲染出来贴着框底,所以按**框高**切掉底下 4.762% 就是切掉那行字、一个像素不碰脚: + 角色墨迹到 y=400(=切点),计数在 400..420。 + ⚠️ 宽比例素材(本地兜底 524x812)是宽度受限,图内容在框里上下留白, + 这一刀切在空留白上,不会有副作用。 */ + clip-path:inset(0 0 4.762% 0); } +/* 窄屏放不下就收起来:它会压到 TRACK/达成率那几行上 */ +@media (max-width:640px) { .dx-result-mascot { display:none; } } +/* 这一条原来自己画了淡白横带(linear-gradient(100deg,#ffffffd9,...)) + 外加文字时代的 text-shadow / text-stroke —— 现在数字是官方精灵, + prefab 里数字直接画在画面上、底下没有任何垫层,所以全部去掉,只留排版。 */ +/* ⚠️ martin-top 12px:这一格被用户来回调过两轮,记一下取值依据 —— + · 原状 gap 43px(标题盒底 → 数字行顶,其中 28px 是分栏网格误加的 row-gap)→ 用户「太空中」 + · 全收平(margin 0 + row-gap 0)→ 标签墨迹底到数字墨迹顶只剩 ≈7px,贴着 → 用户「又太近了」 + · 现在 margin-top:12px → 墨迹之间 ≈19px,标题和数字分得开、又还在同一组里。 + ⚠️ 官方抓图量到的其实是 7px(标签板底 y265 / 数字墨迹顶 y272),官方是"贴着"的; + 这里是**按用户口径偏离官方**,不要再拿官方数去"纠正"回来。 + ⚠️ 改这个值有三处联动,必须一起动(都在同一份注释里有记录): + 1) .dx-result-achievement-section::before 的 top(白带跟着数字走,+12 → 82px) + 2) .dx-result-mascot 的 top(这一段变高多少,卡片下沿→侧栏顶就变多少,-164 → -176px) */ +.dx-result-achievement { position:relative; margin:12px -28px 0; padding:3px 28px; + line-height:1.05; } +/* 达成率数字是官方金色精灵。--n 是整数部分的格子高:官方抓图里是 98px(900px 画面), + 弹窗比它略窄,取 90px 视觉上等大;小数点后和 '%' 的大小由 --n 按官方比例推。 */ +.dx-result-achievement-value { --n: 90px; } +.dx-result-achievement-value .mm-number { line-height:0; } +/* 数字样式已回退到上个 commit(2024 的平铺排法)。只保留两处【位置】调整: + 1) 整体左移 30px —— prefab 里整数 -215 / 小数 +4 / 百分号 +273,中心在 +29 偏右 + 2) 百分号底边和小数下端对齐(align-self:flex-end,不写死 translateY) + 数字底下那层白色半透明底板删掉了 —— prefab 里数字直接画在画面上,没有垫层。*/ +.dx-result-achievement-value .mm-number { align-items:flex-end; } +.dx-result-achievement-value .mm-number-percent { align-self:flex-end; } +/* 数字在左列里居中:整串的中轴 = 段位/徽章/DX 评分框的中轴(见上面的列宽变量一节)。 + ⚠️ 原来这里是 translateX(-6px)(配合左对齐用的偏移),居中了就不需要了。 */ +.dx-result-achievement { text-align:center; } +/* 达成率标题:只画原版的字 UI_RSL_Text_Achievement(prefab 抓图 247x40)。 + 那条 UI_RSL_Base_Achievement(408x122,prefab 抓图 1667x107)不再画 —— + 它比画面还宽、本是溢出裁切的装饰底,铺进这一行会露出它那个深藏青的左端头, + 在左边看起来就是一块"意义不明的色块"。 */ +.dx-result-achievement-label { position:relative; display:block; height:41px; } +.dx-result-achievement-label .mm-achv-label-text { display:block; height:41px; width:auto; } + +/* NEW RECORD:原来 right:30px; bottom:23px 被甩到弹窗最右下, + 原版它是挨着【小数那一段的上方偏中】。改成相对数字行定位。 */ +.dx-result-record { position:absolute; right:6%; top:52%; transform:rotate(-8deg); padding:4px 7px; color:#e0373d; border:2px solid #e0373d; border-radius:4px; font-size:11px; letter-spacing:1px; font-weight:900; background:#fff3cda8; } .dx-result-record[hidden] { display:none; } -.dx-result-main { display:grid; grid-template-columns:1fr 250px; gap:24px; align-items:center; padding:20px 0 14px; } +/* ═══ 列宽变量:左列 / 侧栏 ═════════════════════════════════════════════════════ + 用户口径:「达成率、评级标识、徽章和 DX 评分框要保持水平居中对齐」。 + 这四件东西分别住在三个不同的容器里(.dx-result-achievement-section 的第一列、 + .dx-result-main 的左列、.dx-result-bottom 的第一列),所以**只有让三个容器 + 共用同一份列定义,它们的水平中轴才会天然重合**,不用去凑魔法数字。 + 合计实测(1200 视口,内容宽 764):侧栏 300、列距 **28** → 左列 436、中轴 428.5。 + ⚠️ 列距取 28 不是 24:main 实际生效的是这份文件后面那条 `.dx-result-main{gap:28px}` + (24 那条早被压掉了)。取 28 = 保持右半区(侧栏 300 / 板子 / 立绘)一个像素不动, + 只把 section 和 bottom 拉到同一条中轴上。 + 改前:main 是 clamp(260,100vw-560,300) + gap28,bottom 却是固定 260 + gap28 + → bottom 的第一列 476,中轴比左列右 18px;达成率数字更是根本没居中 + (它左对齐在整张单子宽上,中轴 399.9,比左列中轴 428.5 左 28.6px)。 + 修完三处实测中轴:数字 428.5 / 段位 428.5 / 徽章 428.5 / DX 评分框 428.5。 */ +.dx-result { --mm-side-w:250px; --mm-col-gap:28px; } +@media (min-width:641px) { .dx-result { --mm-side-w:clamp(260px, calc(100vw - 560px), 300px); } } +@media (max-width:600px) { .dx-result { --mm-side-w:145px; --mm-col-gap:12px; } } + +.dx-result-main { display:grid; grid-template-columns:minmax(0,1fr) var(--mm-side-w); gap:var(--mm-col-gap); align-items:center; padding:20px 0 14px; } .dx-result-performance { min-width:0; } .dx-result-rank { text-align:center; font-family:Georgia,'Times New Roman',serif; font-size:clamp(80px,13vw,124px); line-height:1.05; font-weight:900; font-style:italic; letter-spacing:-6px; padding-right:12px; background:linear-gradient(125deg,#52e8fd 0%,#48b8f7 13%,#fbfcff 21%,#958cf4 28%,#733bee 34%,#f9b5e7 43%,#ff80b9 50%,#fff298 58%,#ffd946 65%,#e9ffff 72%,#43d6fd 81%,#9996ee 90%,#ea8fdd); background-clip:text; -webkit-text-fill-color:transparent; -webkit-text-stroke:1.1px #fff; filter:drop-shadow(1px 0 #283ea5) drop-shadow(-1px 0 #283ea5) drop-shadow(0 2px #283ea5) drop-shadow(0 2px #fff) drop-shadow(3px 3px #675ecfaa); } .dx-result-badges { display:flex; justify-content:center; gap:18px; margin-top:14px; } @@ -80,7 +494,7 @@ .dx-result button:focus-visible { outline:3px solid #153483; outline-offset:4px; } .dx-result[open] { animation:dx-result-in .3s ease-out; } @keyframes dx-result-in { from { opacity:0; transform:translateY(16px) scale(.98); } to { opacity:1; transform:none; } } -@media (max-width:600px) { .dx-result-sheet { padding:16px 16px 18px; } .dx-result-kicker > span { display:none; } .dx-result-clear { font-size:52px; } .dx-result-track { grid-template-columns:44px 1fr 50px; gap:7px; } .dx-result-track-icon { height:48px; font-size:20px; } .dx-result-track-heading { font-size:10px; gap:5px; } .dx-result-problem { font-size:8px; } .dx-result-track-heading i { display:none; } .dx-result-name { font-size:13px; padding:5px; } .dx-result-level strong { font-size:21px; } .dx-result-achievement-section { margin-left:-16px; margin-right:-16px; padding-left:16px; padding-right:16px; } .dx-result-achievement-label { margin-left:-16px; padding-left:16px; font-size:12px; } .dx-result-achievement-label > span { display:none; } .dx-result-achievement-top { gap:7px; } .dx-result-best > span:first-child { padding:3px 6px; } .dx-result-best b { font-size:10px; } .dx-result-achievement-delta { padding:7px; font-size:12px; } .dx-result-achievement { margin-left:-16px; margin-right:-16px; padding-left:16px; font-size:48px; letter-spacing:-2px; } .dx-result-record { position:static; display:inline-block; margin-top:8px; font-size:9px; transform:none; } .dx-result-main { grid-template-columns:1fr 145px; gap:12px; padding-top:12px; } .dx-result-rank { font-size:70px; letter-spacing:-4px; padding-right:0; } .dx-result-badges { gap:12px; margin-top:14px; } .dx-result-medal { width:54px; height:54px; border-width:4px; font-size:18px; } .dx-result-medal small { font-size:9px; } .dx-result-clean b { font-size:14px; } .dx-result-time { flex-direction:column; gap:2px; padding:5px; } .dx-result-time small span { display:none; } .dx-result-time strong { font-size:17px; } .dx-result-verdict { grid-template-columns:30px 1fr 30px; min-height:24px; padding-left:5px; font-size:10px; } .dx-result-verdict small { font-size:8px; } .dx-result-verdict strong { font-size:15px; } .dx-result-rating { flex-wrap:wrap; gap:4px; padding:4px; } .dx-result-rating small { font-size:9px; } .dx-result-rating strong { font-size:15px; } .dx-result-single-delta { font-size:9px; } .dx-result-total { gap:8px; padding-left:8px; } .dx-result-total-label { font-size:23px; } .dx-result-total-score { padding:5px 9px; } .dx-result-total-score strong { font-size:25px; letter-spacing:1px; } .dx-result-total-delta { font-size:18px; } .dx-result-footer { gap:12px; } .dx-result-note { font-size:9px; } .dx-result-next { gap:10px; padding:8px 13px; font-size:17px; } } +@media (max-width:600px) { .dx-result-sheet { padding:16px 16px 18px; } .dx-result-kicker > span { display:none; } .dx-result-clear { font-size:52px; } .dx-result-track { --mm-card-h:92px; --mm-digit-h:27px; --mm-lv-h:19px; --mm-title-size:14px; --mm-heading-size:11px; --mm-slot-size:11px; --mm-pending-size:12px; } .dx-result-achievement-section { margin-left:-16px; margin-right:-16px; padding-left:16px; padding-right:16px; } .dx-result-achievement-label { margin-left:-16px; padding-left:16px; font-size:12px; } .dx-result-achievement-label > span { display:none; } .dx-result-achievement-top { gap:7px; } .dx-result-best > span:first-child { padding:3px 6px; } .dx-result-best b { font-size:10px; } .dx-result-achievement-delta { padding:7px; font-size:12px; } .dx-result-achievement { margin-left:-16px; margin-right:-16px; padding-left:16px; font-size:48px; letter-spacing:-2px; } .dx-result-record { position:static; display:inline-block; margin-top:8px; font-size:9px; transform:none; } .dx-result-main { grid-template-columns:1fr 145px; gap:12px; padding-top:12px; } .dx-result-rank { font-size:70px; letter-spacing:-4px; padding-right:0; } .dx-result-badges { gap:12px; margin-top:14px; } .dx-result-medal { width:54px; height:54px; border-width:4px; font-size:18px; } .dx-result-medal small { font-size:9px; } .dx-result-clean b { font-size:14px; } .dx-result-time { flex-direction:column; gap:2px; padding:5px; } .dx-result-time small span { display:none; } .dx-result-time strong { font-size:17px; } .dx-result-verdict { grid-template-columns:30px 1fr 30px; min-height:24px; padding-left:5px; font-size:10px; } .dx-result-verdict small { font-size:8px; } .dx-result-verdict strong { font-size:15px; } .dx-result-rating { flex-wrap:wrap; gap:4px; padding:4px; } .dx-result-rating small { font-size:9px; } .dx-result-rating strong { font-size:15px; } .dx-result-single-delta { font-size:9px; } .dx-result-total { gap:8px; padding-left:8px; } .dx-result-total-label { font-size:23px; } .dx-result-total-score { padding:5px 9px; } .dx-result-total-score strong { font-size:25px; letter-spacing:1px; } .dx-result-total-delta { font-size:18px; } .dx-result-footer { gap:12px; } .dx-result-note { font-size:9px; } .dx-result-next { gap:10px; padding:8px 13px; font-size:17px; } } @media (max-height:740px) and (min-width:601px) { .dx-result-sheet { padding-top:12px; padding-bottom:15px; } .dx-result-clear { font-size:54px; } .dx-result-kicker b { font-size:20px; } .dx-result-subtitle { margin-bottom:9px; } .dx-result-main { padding:13px 0 12px; } .dx-result-rank { font-size:108px; } .dx-result-achievement-section { margin-top:13px; } .dx-result-footer { margin-top:10px; } } @media (prefers-reduced-motion:reduce) { .dx-result[open] { animation:none; } } @media (max-height:740px) and (min-width:601px) { @@ -94,19 +508,10 @@ .dx-result-total-score strong { font-size:28px; } .dx-result-total-label { font-size:25px; } } -/* Problem difficulty borrows Codeforces handle hues; the scene keeps its prism palette. */ -.dx-result-track { --track-light:#cf8cf7; --track-main:#a846e8; --track-deep:#6224b1; --track-title:#35147c; border-color:var(--track-deep); background:linear-gradient(105deg,var(--track-light),var(--track-main) 48%,var(--track-main)); } -.dx-result-track::before { background:repeating-conic-gradient(var(--track-deep) 0% 25%,transparent 0% 50%) 0 0/18px 18px; } -.dx-result-track-icon { background:conic-gradient(from 35deg,var(--track-light),var(--track-main),var(--track-deep),var(--track-main),var(--track-light)); } -.dx-result-name { background:linear-gradient(var(--track-title),var(--track-deep)); } -.dx-result-level strong { text-shadow:2px 2px var(--track-deep); } -.dx-result-track[data-rating-tone=gray],.dx-result-track[data-rating-tone=unrated] { --track-light:#f0f2f4; --track-main:#aeb6bf; --track-deep:#677481; --track-title:#3e4955; } -.dx-result-track[data-rating-tone=green] { --track-light:#76d17a; --track-main:#249b43; --track-deep:#0e6129; --track-title:#0b4620; } -.dx-result-track[data-rating-tone=cyan] { --track-light:#68e4db; --track-main:#03a89e; --track-deep:#007870; --track-title:#005650; } -.dx-result-track[data-rating-tone=blue] { --track-light:#7695ff; --track-main:#395de3; --track-deep:#192eaf; --track-title:#172673; } -.dx-result-track[data-rating-tone=violet] { --track-light:#de8ce8; --track-main:#b23cc4; --track-deep:#7b178b; --track-title:#50165d; } -.dx-result-track[data-rating-tone=orange] { --track-light:#ffcf77; --track-main:#ed921f; --track-deep:#ac5d09; --track-title:#754108; } -.dx-result-track[data-rating-tone=red] { --track-light:#ff8e93; --track-main:#eb394a; --track-deep:#ac172c; --track-title:#791426; } +/* 题目难度借用 Codeforces 名称色,场景本身保持棱镜色。 + 色对来自 cf-rating-colors.js 的 CF_TONE_COLORS(和段位结算页逐题卡同一张表), + 底板文件 / 滤镜 / 板面底色由 maimai-glyphs.js 按同一个 tone 挂到卡上, + 所以这里不再按 tone 重复写颜色。 */ .dx-result-bottom { display:grid; grid-template-columns:1fr auto; align-items:center; column-gap:18px; row-gap:12px; } .dx-result-bottom .dx-result-footer { display:contents; } .dx-result-bottom .dx-result-total { grid-column:1; grid-row:1; } @@ -116,8 +521,6 @@ /* Gloss follows the cabinet's gold achievement lettering and enamel green AC medal. */ .dx-result-clear { margin-bottom:15px; } .dx-result-achievement { text-shadow:none; -webkit-text-stroke:0; } -.dx-result-achievement-value { display:inline-block; background:linear-gradient(180deg,#fffde8 0%,#fff49b 18%,#fffad3 28%,#ffdd4d 40%,#ffb920 58%,#fff091 78%,#ffd543 100%); background-clip:text; -webkit-text-fill-color:transparent; -webkit-text-stroke:1.4px #bd3819; filter:drop-shadow(1px 0 #fffbe0) drop-shadow(-1px 0 #fffbe0) drop-shadow(0 2px #fffbe0) drop-shadow(1px 2px #d67b21); } -.dx-result-achievement-delta { background:linear-gradient(180deg,#ffffd8 0%,#fff49c 36%,#fffad1 44%,#f5d84e 48%,#ffe573 100%); box-shadow:inset 0 1px 0 #fff, inset 0 -2px 0 #d7b42e55; text-shadow:0 1px #fff9c7; } .dx-result-record { transform:none; border:2px solid #fff4bd; border-radius:7px; padding:6px 10px; color:#244a98; background:linear-gradient(180deg,#fffbd9 0%,#fff198 36%,#fffbe1 44%,#f6cb42 48%,#ffe67a 100%); box-shadow:0 0 0 1px #cf9c2b,0 2px 0 #c7953580,0 0 12px #fff8ad80; text-shadow:0 1px #fff; font-style:italic; } .dx-result-medal { border:5px solid #4fb841; outline:2px solid #216e3b; background:radial-gradient(ellipse at 35% 15%,#b7f37b 0%,#52c54d 28%,#179548 62%,#06763e 100%); box-shadow:inset 0 0 0 2px #e6ffc1,inset 0 -6px 10px #045f3d55,0 2px 0 #fff,0 5px 8px #38597050; text-shadow:0 1px #074a2c,0 2px #0b663c; } .dx-result-medal::before { inset:-6px; border:0; outline:1px solid #236c3e; background:repeating-conic-gradient(from -8deg,#f1ffc3 0deg 13deg,#7dda62 13deg 25deg,#218946 25deg 30deg); mask:radial-gradient(circle,transparent 62%,#000 64%); } @@ -203,9 +606,35 @@ @media (max-width:600px) { .dx-result-medal > b { font-size:21px; } .dx-result-medal > small { font-size:5.5px; padding:1px 2px; margin-top:2px; } .dx-result-medal::after { right:-6px; top:-6px; font-size:17px; } } /* Balanced result layout: shared columns and centered score / action anchors. */ -.dx-result-main { grid-template-columns:minmax(0,1fr) 260px; gap:28px; padding:20px 0 22px; align-items:center; } +.dx-result-main { grid-template-columns:minmax(0,1fr) var(--mm-side-w); gap:var(--mm-col-gap); padding:20px 0 22px; align-items:center; } .dx-result-performance { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:18px; } .dx-result-rank { width:100%; font-size:112px; line-height:1.08; letter-spacing:-5px; padding-right:0; } +/* 段位现在是官方字母精灵:高度的比例对着官方抓图(段位约占画面高的 30%)。 + font-size 只用来给同行文字定尺寸,字形本身来自图。 */ +.dx-result-rank .mm-rank-word { display:inline-flex; align-items:flex-end; } +.dx-result-rank .mm-rank-word .mm-glyph { height:176px; } +/* 多字符段位的拼接量,来自客户端 prefab(不是估的): + Null_RankObjectParent = 420x120 原生(= 抓图 350x100),运行时在里面塞字母 + 单个 S = UI_RSL_Rank_S,原生 176x212 + 三字横排 => 单字间距 420/3 = 140 原生 = 116.7 抓图 px + 单字渲染宽 146.7px => 相邻重叠 30px + 加号 UI_RSL_Rank_Plus 原生 88x96(= 字母高的 45%),贴右上角。 */ +.dx-result-rank .mm-rank-letter + .mm-rank-letter { margin-left:-30px; } +/* 多字母段位:A/B 族的第 2 个字母起要缩小 —— 量官方预制整图 UI_GAM_Rank_* + 的内容宽度得到 AAA=3x35 的 78%、BBB=3x30 的 83%,所以取 0.72。 + SSS 是 94%(基本等大),不加这个类。缩放锚点放左下,字母就还踩在同一条基线上。 */ +.dx-result-rank .mm-rank-word .mm-rank-letter-sm { height:127px; transform-origin:0 100%; } +/* ⚠️ 加号必须写足三层选择器。上面 .dx-result-rank .mm-rank-word .mm-glyph 是 + 3 个类名,只写 2 层会被它压掉 —— height 又变回 176px,加号就撑得跟字母一样大。 + 尺寸来自官方整图 UI_GAM_Rank_SSSp 的差分测量(SSSp 减 SSS 就是加号): + 单字母 30x39,加号 9x22 => 高 0.564、宽 0.300、顶边偏移 +0.051*字母高 + 176 * 0.564 ≈ 99px,顶对齐再下移 0.051*176 ≈ 9px。 + ⚠️ 用户口径「加号可以小一点」:99 → 78px(-21%),此时加号墨迹高 ≈ 44px, + 相对字母 176px 是 0.25;原来 99px 时是 0.31 —— 官方整图差分出来的是 0.564/… + 那套是相对**单字母 39px 墨迹**算的,直接套在 176px 的格子高上偏大,实测就是 + "加号跟字母一样壮"。78px 是用户要的偏小一档。 */ +.dx-result-rank .mm-rank-word .mm-rank-plus { height:78px; width:auto; + margin-left:-4px; align-self:flex-start; transform:translateY(-4px); } .dx-result-badges { margin-top:0; gap:22px; } .dx-result-detail { padding:12px; } .dx-result-time { display:flex; align-items:center; justify-content:space-between; padding:2px 0 10px; margin-bottom:10px; text-align:left; } @@ -214,7 +643,21 @@ .dx-result-verdicts { gap:5px; } .dx-result-verdict { min-height:26px; } .dx-result-rating { margin-top:10px; padding:8px 10px; } -.dx-result-bottom { grid-template-columns:minmax(0,1fr) 260px; gap:28px; align-items:center; } +.dx-result-bottom { grid-template-columns:minmax(0,1fr) var(--mm-side-w); gap:var(--mm-col-gap); align-items:center; + --mm-total-lift:60px; position:relative; } +/* DX RATING 后面那条**淡蓝色横带**(用户口径「底下的 dx 分数框也有一个淡蓝色半透明背景条」)。 + 素材:`UI_RSL_Base_DXRATING.png`(224x86)—— 也不是纯色带,是一段**可横向平铺的花纹**: + 左端 ‹‹‹、中间一颗空心星、右端 ›››,整条淡青底 + 上下淡紫边线 + (官方抓图 y690..726 那一条上就重复着「☆ ››› ☆ ›››」✓)。 + 所以用 repeat-x 平铺、高度顶满 40px(官方 36/58 × 我们的框高 64.8 ≈ 40), + 平铺周期 = 224 × 40/86 ≈ 104px。 + 位置:官方黄框(y680..737,中心 708.5)与这条**同一中心**,我们照此对齐。 + ⚠️ 必须是 .dx-result-bottom 的伪元素、不能挂在框自己身上:框带 transform, + 自建层叠上下文,挂负 z-index 的伪元素会被自己那层背景盖掉。 + ⚠️ --mm-total-lift 与框的上抬共用同一个变量,改一处两边都跟上。 */ +.dx-result-bottom::before { content:''; position:absolute; z-index:-1; left:-28px; right:-28px; + top:calc(12px - var(--mm-total-lift)); height:40px; pointer-events:none; + background:url('/assets/mm-result/UI_RSL_Base_DXRATING.png') left center / auto 100% repeat-x; } .dx-result-bottom .dx-result-total { justify-self:center; width:min(340px,100%); box-sizing:border-box; justify-content:center; gap:14px; padding:8px 12px; } .dx-result-total-score strong { letter-spacing:1px; } .dx-result-bottom .dx-result-next { justify-self:center; justify-content:center; width:210px; padding:12px 18px; box-sizing:border-box; gap:20px; transform:rotate(-2deg); } @@ -229,7 +672,7 @@ .dx-result-rating { margin-top:8px; padding:7px 10px; } } @media (max-width:600px) { - .dx-result-main { grid-template-columns:minmax(0,1fr) 145px; gap:12px; padding:16px 0 18px; } + .dx-result-main { grid-template-columns:minmax(0,1fr) var(--mm-side-w); gap:var(--mm-col-gap); padding:16px 0 18px; } .dx-result-performance { gap:14px; } .dx-result-rank { font-size:clamp(48px,12vw,70px); letter-spacing:-3px; } .dx-result-badges { gap:14px; } @@ -327,3 +770,303 @@ } .dx-board-rating-frame .dx-result-total-score { box-sizing:border-box; max-width:100%; } .dx-board-rating-frame .dx-result-total-score strong { font-size:clamp(20px,3vw,28px); } + +/* 结算勋章:底座 + 两枚官方徽章。尺寸位置来自 prefab(capture 基准), + 弹窗宽 820 / 画面 903 => 缩放 0.908: + 底座 UI_RSL_MedalBase 273x130 capture -> 248x116 + 单枚徽章 83x83 capture -> 75x75 + 两槽间距 91 capture -> 83 + ⚠️ 必须写两层选择器(.mm-medals .mm-medal-xxx)。maimai-glyphs.css 后加载, + 里面 .mm-glyph{width:auto} 只有一层,但顺序在后,会把一层的 width 全压掉 —— + 底座就会按原生 326px 渲染、徽章按 86px,两枚叠在一起。 + 这和加号那次是同一类 bug:权重不够。 */ +.dx-result-badges { display:block; } +.mm-medals { position:relative; display:block; width:248px; height:118px; + /* 左移 27 / 上移 28:Codex 量的偏移是 3 倍放大图坐标(80/83),折算成 CSS px。 + -30px 是原先的垂直值,合计上移 58。 */ + transform:translate(0px, -58px); } +.mm-medals .mm-medal-base { position:absolute; left:0; bottom:0; + width:248px; height:auto; } +.mm-medals .mm-medal { position:absolute; bottom:27px; width:75px; height:75px; + display:block; object-fit:contain; } +/* Clean Solve 没达成:变灰 + 压暗(Accepted 只要有 AC 就一定有) */ +.mm-medals .mm-medal[data-earned="false"] { filter:grayscale(1) brightness(.72); + opacity:.55; } +.mm-medals .mm-medal--ap { left:33px; } /* 中心 70.5 vs 凹槽 69.6 */ +.mm-medals .mm-medal--fsd { left:133px; } /* 中心 170.5 vs 凹槽 170.8 */ +.mm-medals .mm-medal-skin { position:absolute; left:0; top:0; + width:75px; height:75px; } +.mm-medals .mm-medal-text { position:absolute; left:0; top:6px; + width:75px; height:auto; } + +/* 段位整体往上一点(用户要求) */ +.dx-result-rank { transform:translateY(-16px); } + +/* 达成率大数字的字距/底线:见下面「达成率大数字」那一节(本文件里唯一一处生效的定义) */ + +/* 底下的 DX RATING 框整体往上提一点(用户要求) */ +/* DX RATING:挪到徽章槽的下一行,并和徽章槽水平对齐。 + 徽章组中心相对弹窗内容区偏左 27px,这里同步左移并对齐到同一垂直中线。 */ +/* DX RATING:只上移,不再左移。 + 读图量过:数字/段位/徽章的中心都在 445~452,CLEAR! 在 592(=弹窗中心), + 而这个框原来在 422 —— 是我多加了 -27px 造成的,去掉。 */ +.dx-result-bottom .dx-result-total { transform:translateY(-60px); } +.dx-result-total-delta { top:calc(100% + 3px); } + +/* ═══ 达成率大数字:字距 + 一条底线 ═══════════════════════════════════════════ + 用户口径:「字间距小一点,所有文字的最底端对齐」。 + + ⚠️ 这里所有选择器都写了两层类(.dx-result-achievement-value .mm-number …), + 不是啰嗦:maimai-glyphs.css 在 dx-result.css **之后**加载,而 + `.mm-number .mm-number-int + .mm-number-int` 与 + `.dx-result-achievement-value .mm-number-int + .mm-number-int` 同是 (0,3,0), + 源序在后的赢 —— 所以这个文件里原来那两条 -12px / -10px **从来没生效过**, + 实际字距一直是 glyphs 表的 -0.08n / -0.067n(--n:90 时 = -7.2 / -6.03px)。 + 多挂一层 .mm-number 提到 (0,4,0) 才压得住。 + ⚠️ 数值一律按 --n 的倍数写(旁边的 px 是 --n:90px 时的实际值),--n 变了两者一起缩。 + + 量法:拿每个精灵的**实心**墨迹(alpha>200)在格子里的留白,两字之间的实心缝 + = 外边距 + 前字右留白 + 后字左留白。 + 实心留白(--n:90px 下的显示尺寸): + 整数格 左2.3 右2.3 底3.8 ('1' 左4.9 右4.3;'9'/'0' 同 2.3) + 小数格 左4.6 右4.6 底3.8 ('7' 左6.9 右5.4 底4.6) + 小数点 左16.9 右15.4 底12.3 ← 它才是字距松的元凶 + '%' 左2.3 右2.3 底2.3 + 目标:数字之间实心重叠约 3px(官方也是咬在一起的),小数点两侧各留约 5px。 */ + +/* 数字之间:实心重叠 3.05px = -0.085×90 + 2.3 + 2.3 */ +.dx-result-achievement-value .mm-number .mm-number-int + .mm-number-int { + margin-left:calc(var(--n, 1em) * -0.085); } +/* 小数位之间:按同样比例给(实心重叠 2.3px = -0.128×90 + 4.6 + 4.6) */ +.dx-result-achievement-value .mm-number .mm-number-frac + .mm-number-frac { + margin-left:calc(var(--n, 1em) * -0.128); } +/* 小数点两侧:原来左边没有规则(靠格子留白 = 19.2px 的大空档),右边走 frac+frac 只有 + 16.3px。现在两边都收到 5px 上下。 + ⚠️ 小数点同时挂着 mm-number-frac,所以下面这两条必须写在 frac+frac 那条**之后**: + 两者的选择器都是 (0,4,0),靠源序压住它。 */ +.dx-result-achievement-value .mm-number .mm-number-int + .mm-number-dot { + margin-left:calc(var(--n, 1em) * -0.158); } +.dx-result-achievement-value .mm-number .mm-number-dot + .mm-number-frac { + margin-left:calc(var(--n, 1em) * -0.174); } +/* '%':原来 glyphs 表的 -0.04n 让实心缝只有 1.5px,偏挤,放到 2.4px */ +.dx-result-achievement-value .mm-number .mm-number-percent { + margin-left:calc(var(--n, 1em) * -0.05); } + +/* 底线:三族格子的**底留心**不一样(整数 3.8 / 小数 3.8 / 小数点 12.3 / '%' 2.3), + 所以盒子底对齐之后墨迹底并不齐 —— 小数位整族再往下压 9px 的老写法,是把 + **小数点**的账记到了整族头上:实测 '7000' 的墨迹底因此比 '99' 低 9.1px。 + 现在按族分开记: + 小数位 底留心 3.8 = 整数格 3.8 → 不动('7' 是 4.6,比别的字高 0.8px, + 同族内没法再分,忽略) + 小数点 12.3 - 3.8 = 8.5px → margin-bottom 压 8.5px + '%' 2.3 - 3.8 = -1.5px → margin-bottom 抬 1.5px + 结果:整串的实心墨迹底落在同一条线上(同族内剩余离散度 <0.9px)。 */ +.dx-result-achievement-value .mm-number-dot { + margin-bottom:calc(var(--n, 1em) * -0.101); } +.dx-result-achievement-value .mm-number-percent { + margin-bottom:calc(var(--n, 1em) * 0.017); } + +/* ═══ 右半区:看板娘站在右下角,半透明成绩板浮在它上面 ═══════════════════════ + 官方结算画面里看板娘的容器是 Main/Null_NavicharaRoot: + 抓图 471.6 x 261.7,中心在 [658.3, 755.0](这是 903x906 的画面坐标,中心而非左上)。 + 弹窗宽 820,缩放 = 820/903 = 0.908: + 宽 471.6 x 0.908 ≈ 428 -> 取 430 + 右边缘 (658.3 + 235.8) x 0.908 ≈ 812(离画面右边约 8px,基本贴边) + 中心横坐标 658.3 x 0.908 ≈ 598 = 画面宽的 72.9% + 纵向不照搬:906 是街机整屏,弹窗高度由内容撑出来,两者高度不是一回事。 + 所以只保留 prefab 给的横向结论(贴右、宽约 428、中心在 72.9% 处), + 纵向换成「脚踩在成绩行下缘、头探到成绩板上方」—— 这才是「右下角的立绘」。 + + 分层:立绘 z-index:-1(沉到 .dx-result-sheet 的隔离层里,单子里所有内容之下), + 成绩板 z-index:1 显式压在上面 —— 顺序不靠 DOM 先后,也不怕以后有人加节点。 + ────────────────────────────────────────────────────────────────────────── */ +.dx-result-side { position:relative; align-self:stretch; display:grid; + align-content:end; padding-bottom:26px; min-width:0; } +/* ⚠️ 板子在列里**底对齐**(原来居中):板高 242.6 固定,居中时上沿落在 436.4, + 正好从人的胸口切过去;底对齐到距列底 26px(=面板底 698.8、上沿 456.2)后, + 板子上沿往下让出 19.8px,同时板底仍比人的脚(720.8)高 22px —— 脚照样露在板子下面。 + ⚠️ 这 26px 不能再大:再下去板底就会盖住脚。想让人露更多只能压板高,不能挪板子。 */ + +/* 成绩板 = 客户端 `UI_ScoreBoardDisplay/Base`,也就是 `UI_RSL_DetailsBase`。 + 那张 232×198 的图逐像素量过,事实是: + · 内容从 (9, 6) 起,圆角半径 ≈10px(占宽 4.6%),四角一样圆 + · 内层填充 alpha 均值 187/255、颜色 #F7F7FC —— 近白的磨砂玻璃,不是深蓝 + · 最外一圈 1~2px 是 alpha 229 的纯白描边,上边缘比下边缘亮 + → 官方这块板子的语言是「近白半透明玻璃 + 白描边 + 小圆角」, + 之前那套深蓝底 + 亮青硬描边 + 四角切角不是它。 + 板子要能让立绘透出来。中间试过「底色 0.59 + 顶部 0.88 的渐变」那一版 —— + 两层叠加后实际不透明度 0.79~0.95,立绘整片糊掉,跟不透明没区别,已否掉。 + 现在是底色 0.39 + 渐变 0.24/0.06/0.15,叠加后实际约 0.43~0.53, + 这才是「看得见人的半透明板」(官方自己那张是 0.73,它底下没有站人)。 + ⚠️ 上一版是 0.46 + 0.30/0.08/0.18(叠出来 0.50~0.62),人隔着板子只读得出个影子, + 用户口径是"整个人被盖住",所以再让一档 —— 判定行的色带本身是不透明的, + 数字和标签都压在色带上,降这一档不影响读数。 + 官方图上的那层高光和描边不手画:::before 把原图按板子尺寸铺开再压到半透明, + 圆角就是原图的圆角;图挂了也不影响,底下的 CSS 自己成立。 + 不用 backdrop-filter:那会把立绘糊掉,"看得见人"优先于"字更好读"。 */ +.dx-result-detail { + position:relative; z-index:1; box-sizing:border-box; width:100%; + padding:12px 13px 11px; + border:2px solid #ffffffeb; + border-radius:13px; + background-color:#ffffff63; + background-image:linear-gradient(180deg,#ffffff3d 0%,#e2ecff0f 52%,#c4d6f626 100%); + box-shadow:inset 0 2px 0 #ffffffd9,inset 0 0 0 1px #cbdcf894, + inset 0 -14px 26px #9db6de3d,0 1px 0 #ffffff9c,0 10px 24px #24386b3d; + color:#17356e; + overflow:hidden; +} +.dx-result-detail::before { content:''; position:absolute; inset:0; z-index:-1; pointer-events:none; + border-radius:inherit; opacity:.5; + background:url('/assets/mm-result/UI_RSL_DetailsBase.png') center/100% 100% no-repeat; } + +/* PLAY TIME:官方面板上的小标题条。 + `UI_RSL_DXScore_Base`(226×54)量出来是「深蓝标签带压在上、白底数值位在下、 + 外圈一道深色描边、圆角 ≈10px」,`UI_CMN_RSL_AllScorekBase` 的「合計スコア」 + 则是一条白胶囊;抓图里曲目板顶行也是「标签 + 一排 >>>」。 + 这里就照这个来:浅色小板、左标签右数值、右端收一排雪佛龙。 */ +.dx-result-time { position:relative; display:flex; align-items:baseline; justify-content:space-between; + gap:8px; margin:0 0 9px; padding:5px 40px 5px 9px; border:0; border-radius:8px; + background:linear-gradient(180deg,#fffffff2,#e7effdf2); + box-shadow:inset 0 0 0 1px #ffffff,0 1px 0 #b7cbeb99; text-align:left; } +.dx-result-time small { display:block; margin:0; color:#4b6ba6; font-size:8.5px; + font-weight:800; letter-spacing:2px; text-shadow:0 1px 0 #fff; } +.dx-result-time strong { color:#12307c; font-size:24px; font-weight:900; letter-spacing:.5px; + font-variant-numeric:tabular-nums; text-shadow:0 1px 0 #ffffffb8; } +/* 右端那排 `>>>`:官方 `UI_RSL_Base_DXRATING` 右端就是三连箭头。 + 用 SVG 画箭头比手搓渐变靠谱,缩放也不糊。 */ +.dx-result-time::after { content:''; position:absolute; right:9px; top:50%; width:27px; height:12px; + transform:translateY(-50%); opacity:.5; pointer-events:none; + background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='12' viewBox='0 0 10 12'%3E%3Cpath d='M1.5 1l5 5-5 5' fill='none' stroke='%236f8fc4' stroke-width='2.2' stroke-linecap='square'/%3E%3C/svg%3E") left center/9px 12px repeat-x; } + +/* 判定行 = 客户端 `UI_RSL_Totalscore`(162×152)那张表。量出来的事实: + · 四行色带:PERFECT rgb(251,193,36) / GREAT rgb(255,131,182) / + GOOD rgb(71,212,93) / MISS rgb(196,197,189) + · 行与行**紧贴**、中间没有缝,整块只有最外侧两角是圆的(上排顶角、下排底角) + · 每行左边留白,右侧一个白底圆角计数格,边框用本行的颜色(约 2px) + · 标签是白字 + 同色深描边,不是深色实心字 + → 所以这里做成「一张分段的实心表」,不是四条各自圆角的胶囊。 + 判定色沿用本页原来的语义色:AC 绿 / WA 粉 / TLE 琥珀 / RE 紫 / CE 灰。 */ +.dx-result-verdicts { gap:0; border-radius:9px; overflow:hidden; + box-shadow:0 0 0 1px #ffffffd0,0 2px 6px #24386b2e; } +.dx-result-verdict { --verdict-rgb:16 176 132; + display:grid; grid-template-columns:1fr 62px; align-items:center; + min-height:26px; padding-left:11px; border:0; border-radius:0; + background:linear-gradient(100deg,rgb(var(--verdict-rgb) / .94),rgb(var(--verdict-rgb) / .72)); + box-shadow:inset 0 1px 0 #ffffff59,inset 0 -1px 0 #0b23471f; + color:#fff; font-size:12px; font-weight:900; letter-spacing:1px; + text-shadow:0 1px 2px rgb(var(--verdict-rgb) / .95),0 0 1px #0b2347a8; } +.dx-result-verdict strong { align-self:stretch; display:grid; align-items:center; justify-items:center; + margin:3px 3px 3px 0; border:2px solid rgb(var(--verdict-rgb)); border-radius:6px; + background:linear-gradient(#ffffff,#eef5ff); color:rgb(var(--verdict-rgb)); + font-family:'Arial',sans-serif; font-size:17px; font-weight:900; letter-spacing:0; + line-height:1; text-shadow:none; } +.dx-result-verdict-wa { --verdict-rgb:219 84 117; } +.dx-result-verdict-tle { --verdict-rgb:201 154 46; } +.dx-result-verdict-re { --verdict-rgb:150 112 202; } +.dx-result-verdict-ce,.dx-result-verdict-other,.dx-result-verdict-pending,.dx-result-verdict-mle { --verdict-rgb:107 136 174; } + +/* Rating = 客户端 `UI_RSL_DXScore_Base` 那块「深蓝圆角板 + 白字段」: + 深蓝标签带上放标签(官方用黄字),下半是白底数值位,外圈一道深蓝描边。 + 抓图里那块蓝底 rating 牌(蓝板 + 红字 + 白底数字位 + 底下小白胶囊)是同一套。 */ +.dx-result-rating { display:flex; align-items:center; gap:8px; color:#fff; + margin-top:10px; padding:6px 7px 6px 10px; + border:2px solid #16307c; border-radius:9px; + background:linear-gradient(180deg,#2f5cbb,#16307c); + box-shadow:inset 0 1px 0 #ffffff4d,0 2px 0 #ffffff8c,0 4px 10px #24386b33; } +.dx-result-rating small { color:#ffe065; font-size:11px; font-weight:900; letter-spacing:.4px; + text-shadow:0 1px 2px #0a1c4c; } +.dx-result-rating strong { margin-left:auto; padding:2px 10px; border-radius:6px; + background:linear-gradient(#ffffff,#e8f0ff); color:#12307c; + font-family:'Arial',sans-serif; font-size:20px; font-weight:900; line-height:1.2; + font-variant-numeric:tabular-nums; } +.dx-result-single-delta { color:#ffe38e; font-size:12px; font-weight:800; text-shadow:0 1px 2px #0a1c4c; } + +/* 矮窗 / 窄屏:上面那两段媒体查询写在我前面,会被这一节的基线规则盖掉, + 所以同样的压缩量必须在这里再写一遍。 + ⚠️ 立绘不在这张名单里:矮窗只让弹窗滚动,单子内部几何不变(见 .dx-result-mascot + 那段注释里的六档实测),再压一遍只会让 740 以下的窗口里人变小、头顶离卡片更远。 */ +@media (max-height:740px) and (min-width:601px) { + .dx-result-detail { padding:9px; } + .dx-result-time { padding:4px 37px 4px 8px; margin-bottom:7px; } + .dx-result-time strong { font-size:22px; } + .dx-result-verdict { min-height:23px; } + .dx-result-verdict strong { font-size:16px; margin:2px 2px 2px 0; } + .dx-result-rating { margin-top:8px; padding:5px 6px 5px 9px; } + .dx-result-rating strong { font-size:18px; } +} +@media (max-width:600px) { + .dx-result-detail { padding:7px; } + .dx-result-time { flex-direction:column; align-items:flex-start; gap:2px; + padding:5px 34px 5px 7px; margin-bottom:7px; } + .dx-result-time strong { font-size:19px; letter-spacing:.3px; } + .dx-result-time small { font-size:7.5px; letter-spacing:1.4px; } + .dx-result-time::after { right:6px; width:21px; } + .dx-result-verdict { grid-template-columns:1fr 38px; padding-left:7px; font-size:10px; min-height:24px; } + .dx-result-verdict strong { font-size:15px; margin:2px 2px 2px 0; } + .dx-result-rating { margin-top:7px; padding:5px 6px 5px 8px; gap:5px; } + .dx-result-rating small { font-size:9px; } + .dx-result-rating strong { font-size:18px; padding:2px 7px; } + .dx-result-single-delta { font-size:9px; } +} + +/* unrated 档用的是 UTAGE 卡(洋红),但共享色表里 unrated 的主色是灰, + 槽位按 --track-main 混白就是一块灰压在粉卡上。这一档改跟卡面平色走。 */ +.dx-result-track[data-rating-tone="unrated"] .dx-result-track-slot { + --mm-slot-fill:color-mix(in srgb, var(--mm-field) 42%, #ffffff); +} + +/* ═══ 右半区(续):板子加宽压矮 ═══════════════════════════════════════════════ + 用户口径:「人被板子整片盖住 —— 板子要更宽更矮,人要在板子周围读得出来」。 + 分两件事:列宽(下面两段 @media)和板子自身的高度(这一节末尾那组规则)。 + 立绘的高度在看板娘那条基线规则里已经改成 470。 + + ⚠️ 这一段所有选择器都多挂一层 `.dx-result-side`: + dan.js 的单题结算共用 .dx-result-detail / .dx-result-time / .dx-result-verdict / + .dx-result-rating(见 dan.js 的 buildStageDialog),但它的成绩板外面**没有** + .dx-result-side —— 多这一层就只改 dx 这张单子,dan 那页一个像素都不动。 + ────────────────────────────────────────────────────────────────────────── */ + +/* 列宽:立绘那一列比原来宽。上限不是审美定的,是左列段位字串顶出来的 —— + 三字 + 加号(SSS+)实测 465.1px 宽,居中摆在左列里,比列还宽、向两侧对称溢出, + 而加号的墨迹只比它的盒子窄 4px(精灵 88x96,ink 到 x=83),再叠上段位那条 + 3px 的投影,基本就是贴边画。所以板子左缘必须离它远一点。 + 实测(当前布局): + 820 视口 弹窗被 100vw-28 卡住,单子只有 771 宽、内容 715: + 段位右缘 497、板子左缘 500 —— 只剩 3.4px,这一档根本加不了宽; + 870 视口 单子回到 820 满宽(801 那档还在被 100vw 卡着),内容 764, + 段位右缘 662、板子左缘 675 —— 13px,能加到 300; + 1200 同上,300 就是上限(310 只剩 8px,327 开始啃单子左边缘)。 + 所以用 clamp 让它随视口线性长:vw-560 这条线是「单子还没满宽时左列正好 427px」 + 量出来的 —— 820 时算出来 260(= 改前,不动),~870 时 310 被上限截到 300。 + 改前板子恒为 260;改后 1200 视口 300、820 视口仍是 260。 */ +@media (min-width:641px) { + .dx-result-main:has(> .dx-result-side) { + grid-template-columns:minmax(0,1fr) var(--mm-side-w); + } +} + +/* 压矮:原 290.3 -> 242.6(-16.4%),宽高比 0.90 -> 1.24。 + 逐块量出来的账(1200 视口): + PLAY TIME 带 45.3 -> 30 字 24->21、上下内边距 5->4 + 五行判定 155 -> 135 行高 31->27(计数格 margin 3->2、字号 17->15) + Rating 条 44 -> 39.6 字 20->18、内边距 6->5 + 板子内边距 12/13/11 -> 10/13/9 + 字号只往下一档,判定色带和计数格描边照旧,读数不受影响。 + ⚠️ 整套只写在 min-width:641px 里(= 立绘还在的那一档): + 601 以下的窄屏有自己的竖排 PLAY TIME 和 7px 内边距,这几条挂上去只会把 + 那块板子顶高 6px(量过),所以窄屏照旧。 */ +@media (min-width:641px) { + .dx-result-side > .dx-result-detail { padding:10px 13px 9px; } + .dx-result-side .dx-result-time { padding:4px 38px 4px 9px; margin-bottom:7px; } + .dx-result-side .dx-result-time strong { font-size:21px; } + .dx-result-side .dx-result-verdict { min-height:22px; font-size:11.5px; } + .dx-result-side .dx-result-verdict strong { margin:2px 2px 2px 0; font-size:15px; } + .dx-result-side .dx-result-rating { margin-top:8px; padding:5px 7px 5px 9px; } + .dx-result-side .dx-result-rating strong { font-size:18px; } +} + +/* 立绘的矮窗覆盖已撤(原 `.dx-result-side .dx-result-mascot{height:470px}`): + 身高现在按 --mm-mascot-h 走、头顶锚在卡片下沿,而「卡片下沿→侧栏顶 178px」 + 和「侧栏底→单子底 129px」这两段在矮窗里量出来与基线完全一致, + 所以矮窗不需要也不应该再缩一次。要调大小只改 --mm-mascot-h 一个数。 */ diff --git a/public/dx-timer.js b/public/dx-timer.js index 85e1156..4cd78fd 100644 --- a/public/dx-timer.js +++ b/public/dx-timer.js @@ -1,4 +1,25 @@ -import { cfRatingColor } from './cf-rating-colors.js'; +import { cfRatingColor, cfToneColors } from './cf-rating-colors.js'; +import { mascotImage } from './mascot.js'; +import { clearWord, trackLabel, trackNumber, rankWord, scoreNumber, achievementColour, achievementLabel, medalPair, problemCard, cardTone, NUMBER_COLOURS } from './maimai-glyphs.js'; +import { rankOf } from './dx-rank.js'; + +/** + * 题目难度 → 数字。服务端给的是 number 或 null(见 src/dx/timer.ts)。 + * 这里显式转一次:老数据/预览样例里有把难度写成字符串的('1700'), + * 直接丢给 cfRatingColor 会得到 unrated —— 整张卡会错成灰的。 + * 空值仍然回 null(= 未评级),别把"没难度"变成"0 分"。 + */ +function difficultyOf(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) && number >= 0 ? number : null; +} + +/** + * 练习类型 → 卡片上那行小字的**第一个词**(第二个词是平台,见 problem-source.js 的 + * PLATFORM_LABELS)。这里也是 key→字面,不在模板里写死。 + */ +const PRACTICE_KIND_LABEL = { first: 'INDEPENDENT', repeat: 'REPLAY', assisted: 'ASSISTED', unknown: 'PRACTICE' }; export function initDxTimer({ getUser, onComplete, onResultClose }) { const $ = id => document.getElementById(id); @@ -9,15 +30,15 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) { dialog.setAttribute('aria-labelledby', 'dxResultTitle'); dialog.innerHTML = `
      -
      TRACK 01ALGORITHM DX
      -

      CLEAR!

      -
      DX

      LEVEL
      +
      01ALGORITHM DX
      +

      +
      -
      达成率 ›››✦
      此前最佳
      -
      +
      +
      -
      ACAccepted
      CSClean Solve
      -
      PLAY TIME
      Rating
      +
      +
      PLAY TIME
      Rating
      DXRATING
      @@ -28,27 +49,114 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) { function showResult() { if (!result || timer?.status !== 'completed' || user !== getUser()) return; const score = result.score; - field('.dx-result-kicker b').textContent = String(result.dailyTrack || 1).padStart(2, '0'); - field('.dx-result-kicker').title = '当天第 ' + (result.dailyTrack || 1) + ' 道 AC 的题目'; - const color = cfRatingColor(result.difficulty); - field('.dx-result-track').dataset.ratingTone = color.tone; - field('.dx-result-track').title = `题目难度配色参考 CF 名称颜色:${color.label}(${color.range})`; - field('.dx-result-problem').textContent = 'CODEFORCES / '+timer.problem_id; - field('.dx-result-name').textContent = result.title; - field('.dx-result-level strong').textContent = result.difficulty ?? '待定'; - field('.dx-result-achievement-value').textContent = score ? score.achievementShown.toFixed(4)+'%' : '已通关'; - field('.dx-result-rank').textContent = score?.rank ?? 'AC'; - field('.dx-result-kind').textContent = {first:'INDEPENDENT',repeat:'REPLAY',assisted:'ASSISTED',unknown:'PRACTICE'}[result.practiceKind]; + // 这几处用官方精灵拼:CLEAR! / TRACK / 段位 / 大数字,形和配色都是客户端那套 + const previewParams = new URLSearchParams(location.search); + field('.dx-result-clear').replaceChildren(clearWord()); + // 轨道号只在段位/课题模式里有意义(客户端那边也是跑课程才显示 TRACK n)。 + // 普通练习模式这里是当天第几道题的固定值,没有意义。 + // ⚠️ 只藏内容、不删元素 —— 删掉会把 kicker 这一行压塌,上面的组件整体位移。 + field('.dx-result-track-word').style.visibility = 'hidden'; + field('.dx-result-kicker b').style.visibility = 'hidden'; + // ?track=03 时强制显示这一行,方便核对 TRACK 和数字的字体(正常用户看不到)。 + const trackPreview = previewParams.get('track'); + if (trackPreview !== null) { + field('.dx-result-track-word').replaceChildren(trackLabel()); + field('.dx-result-track-word').style.visibility = ''; + field('.dx-result-kicker b').replaceChildren( + trackNumber(trackPreview.replace(/\D/g, '').padStart(2, '0').slice(-2))); + field('.dx-result-kicker b').style.visibility = ''; + } + // 卡片整块用官方乐曲底板 + 官方等级数字重画(maimai-glyphs.js 的 problemCard)。 + // 配色还是那一条:难度 → cfRatingColor 的 tone → 底板/滤镜/板面底色。 + const difficulty = difficultyOf(result.difficulty); + const color = cfRatingColor(difficulty); + const [toneMain, toneDeep] = cfToneColors(color.tone); + const tone = cardTone(color.tone); + const card = field('.dx-result-track'); + card.dataset.ratingTone = color.tone; + card.title = `题目难度配色参考 CF 名称颜色:${color.label}(${color.range})`; + card.style.setProperty('--mm-plate', `url('/assets/mm-result/${tone.plate}')`); + card.style.setProperty('--mm-field', tone.field); + card.style.setProperty('--mm-plate-filter', tone.filter); + card.style.setProperty('--track-main', toneMain); + card.style.setProperty('--track-deep', toneDeep); + // 题面来源的平台**不传**:服务端今天只发 Codeforces,result 和 timer 上都没有 platform + // 字段(见 src/dx/timer.ts 的 timerResult —— 平台在 SQL 里就是写死的 'codeforces'), + // 所以这里不能凭空写 result.platform。不传 → problemCard → platformLabel() 落回 + // problem-source.js 的默认值,卡片照旧印 CODEFORCES;哪天服务端把平台发下来了, + // 在这里补一个 platform: result.platform 就切换完了。 + card.replaceChildren(problemCard({ + problemId: timer.problem_id, + kind: PRACTICE_KIND_LABEL[result.practiceKind] ?? result.practiceKind ?? '', + title: result.title, + difficulty, + tone: color.tone, + })); + const achievementText = score ? score.achievementShown.toFixed(4) : null; + // 显示哪个数字:调试用的 ?achievement= 优先,否则用本次成绩 + // 两条路都补齐到四位小数 —— 结算单上的达成率永远是 4 位(100.0000%) + const achievementOverride = previewParams.get('achievement'); + const shownAchievement = achievementOverride && /^\d+(\.\d+)?$/.test(achievementOverride) + ? Number(achievementOverride).toFixed(4) : achievementText; + // 达成率 < 80% 不算 clear(客户端也是这个门槛),CLEAR! 就不该出现。 + // 这里必须放在 shownAchievement 算出来之后 —— 放在前面会踩 TDZ, + // 直接抛 ReferenceError 让整个 showResult() 中断(什么精灵都画不出来)。 + // shownAchievement 为 null 表示这条没有成绩数字、按已通关处理,仍然显示。 + const isCleared = !shownAchievement || Number(shownAchievement) >= 80; + // 用 visibility 而不是 hidden:达成率 <80 时只是把文字藏掉, + // 这一块的高度必须留着 —— 否则底下的组件会整体往上顶。 + field('.dx-result-clear').style.visibility = isCleared ? '' : 'hidden'; + // 配色由达成率决定(<80 蓝、>=97 金、其余红);调试时可以用 ?achvColor= 覆盖来看别的配色 + const numberColour = shownAchievement + ? (NUMBER_COLOURS.includes(previewParams.get('achvColor')) ? previewParams.get('achvColor') : achievementColour(shownAchievement)) + : 'Gold'; + // 达成率标题用原版素材(UI_RSL_Base_Achievement 长条 + UI_RSL_Text_Achievement 字) + field('.dx-result-achievement-label').replaceChildren(achievementLabel()); + field('.dx-result-achievement-value').replaceChildren( + shownAchievement ? scoreNumber(shownAchievement + '%', { colour: numberColour }) : document.createTextNode('已通关')); + // 段位只跟达成率有关,所以直接由上面显示的数字推 —— 两个数据源打架就会画出「88.8888 配 S」 + const rankText = previewParams.get('rank') || (shownAchievement ? rankOf(shownAchievement) : score?.rank) || 'AC'; + field('.dx-result-rank').replaceChildren(rankWord(rankText)); + // 结算底图:嵌逆向出来的客户端动态结算背景页(云/星屑/点阵/流星都在里面), + // 按评级切色版。它只有 A/B/S/SS/SSS 五档,细分档(AA/AAA/BB/BBB 等)按下表归并, + // 低于最低档、以及 AC 兜底(AC = Accepted,不是段位),都落到 B。 + const bgRank = { 'SSS+':'SSS', 'SSS':'SSS', 'SS+':'SS', 'SS':'SS', + 'S+':'S', 'S':'S', 'AAA':'A', 'AA':'A', 'A':'A' + }[String(rankText).trim()] || 'B'; + const sheet = field('.dx-result-sheet'); + sheet.dataset.bg = bgRank.toLowerCase(); + if (!sheet.querySelector('.dx-result-bg')) { + const bf = document.createElement('iframe'); + bf.className = 'dx-result-bg'; + bf.setAttribute('scrolling', 'no'); + bf.setAttribute('aria-hidden', 'true'); + bf.tabIndex = -1; + sheet.insertBefore(bf, sheet.firstChild); + // 背景比结算单大一圈:四周外扩 BLEED 像素,缩放取宽/高比例的较大者, + // 这样无论哪一维差一点都不会在边上露出底色(用户要的"四周多几个像素")。 + const BLEED = 24; + sheet.style.setProperty('--result-bg-bleed', BLEED + 'px'); + const fit = () => { + const kw = (sheet.clientWidth + BLEED * 2) / 903; + const kh = (sheet.clientHeight + BLEED * 2) / 906; + sheet.style.setProperty('--result-bg-scale', + Math.max(kw, kh, 0.2).toFixed(4)); + }; + fit(); + if (window.ResizeObserver) new ResizeObserver(fit).observe(sheet); + window.addEventListener('resize', fit); + } + // ?bgStill=1 时只留静态色版兜底,不嵌 iframe(排查用) + if (previewParams.get('bgStill') !== '1') { + sheet.querySelector('.dx-result-bg').src = + '/mm-result-bg.html?ui=0&clear=1&t=0&rank=' + bgRank; + } field('.dx-result-time strong').textContent = duration(result.seconds); field('.dx-result-rating strong').textContent = score ? score.rating.toFixed(1) : '—'; const comparison = result.comparison, previous = comparison?.previousScore; const signed = (number, digits) => (number>=0?'+':'−')+Math.abs(number).toFixed(digits); const previousAchievement = previous?.achievementShown ?? 0; const improvement = score && comparison ? score.achievementShown-previousAchievement : null; - field('.dx-result-best b').textContent = comparison ? previousAchievement.toFixed(4)+'%' : '—'; - field('.dx-result-achievement-delta').textContent = improvement!==null ? signed(improvement,4)+'%' : '暂无对比'; - field('.dx-result-achievement-delta').dataset.direction = improvement!==null && improvement<0 ? 'down' : 'up'; - field('.dx-result-record').hidden = !comparison || !score || result.practiceKind==='assisted' || (previous && improvement<=0); field('.dx-result-single-delta').textContent = score && previous ? signed(score.rating-previous.rating,1) : ''; const totalText = comparison ? comparison.ratingAfter.toFixed(1) : '—'; const totalFrame = field('.dx-result-total'); @@ -67,10 +175,13 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) { digits.append(digit); } field('.dx-result-total-delta').textContent = comparison ? signed(comparison.ratingDelta,1) : '—'; + // cleanSolve 原本用来切换 AC/CS 那对 CSS 勋章;现在勋章是官方精灵 + // (medalPair():底座 + AP/FSD),这一段只剩判定,留着以后接精灵的亮暗。 const cleanSolve = result.verdicts.AC > 0 && Object.entries(result.verdicts).every(([verdict, count]) => verdict === 'AC' || count === 0); - field('.dx-result-award-cs').hidden = !cleanSolve; - field('.dx-result-medal-cs').setAttribute('aria-label','clean solve'); - field('.dx-result-award-cs').title = 'Clean Solve · AC only'; + // 勋章挂在这里:Accepted 有 AC 就有;Clean Solve 要求 AC 之外全为 0。 + field('.dx-result-badges').replaceChildren(medalPair({ cleanSolve })); + // 勋章:官方底座 + 两枚圆章。Accepted 有 AC 就有;Clean Solve 要求 AC 之外全为 0。 + // cleanSolve 在下面才算,所以这里必须放在它后面挂载。 field('.dx-result-verdicts').replaceChildren(); for (const verdict of ['AC','WA','TLE','RE','CE','MLE','OTHER','PENDING']) { if (['MLE','OTHER','PENDING'].includes(verdict) && !result.verdicts[verdict]) continue; @@ -80,6 +191,9 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) { tag.append(name,count); field('.dx-result-verdicts').append(tag); } + // 看板娘每次都重画:用户可能在设置里刚换过,图源就是那串 lolicount.top 链接。 + // 远端挂了 mascotImage 会自己退回本地占位图,这里的排版不用管加载成功与否。 + field('.dx-result-mascot').replaceChildren(mascotImage({ className: 'dx-result-mascot-img' })); if (!dialog.open) dialog.showModal(); } dialog.addEventListener('close', async () => { @@ -198,5 +312,16 @@ export function initDxTimer({ getUser, onComplete, onResultClose }) { startPolling(); window.addEventListener('pageshow',event=>{if(event.persisted){startPolling();void check();}}); window.addEventListener('pagehide',()=>{clearInterval(clock);clearInterval(poll);}); - return {refresh,running:()=>timer?.status==='running'}; + /** + * 开发/设计预览:塞一份样例把结算弹窗直接弹出来,不必真的做完一道题。 + * 只改这份闭包里的状态,showResult 照常走;页面用 ?resultPreview=1 触发。 + */ + function previewResult(sample) { + timer = { problem_id: sample.problemId, status: 'completed' }; + result = sample.result; + user = getUser(); + showResult(); + } + + return {refresh,running:()=>timer?.status==='running',previewResult}; } diff --git a/public/dx.html b/public/dx.html index 86b02f5..ffda91d 100644 --- a/public/dx.html +++ b/public/dx.html @@ -4,19 +4,21 @@ Algorithm DX · Rating 总览 - + + - + - + - + +
      @@ -292,5 +299,8 @@

      支持项目

      微信扫一扫,或点击图片查看原图。

      +TOP + + diff --git a/public/dx.js b/public/dx.js index b4e0d6a..95f32ce 100644 --- a/public/dx.js +++ b/public/dx.js @@ -1165,6 +1165,31 @@ async function focusResultOnBoard({ userId, problemId }) { const timerController=initDxTimer({getUser:()=>state.userId,onComplete:()=>load(true),onResultClose:focusResultOnBoard}); await loadMeta(); await timerController.refresh().catch(error=>{$('timerStatus').textContent=error.message;}); +// ?resultPreview=1 用样例把结算弹窗直接弹出来:调这块 UI 时不用真做完一道题。 +// 再配 ?achievement=98.7654 能试不同达成率下的排版(段位/数字配色都跟着它走); +// ?difficulty=3000 / ?difficulty= 能试题目卡换难度配色、以及「还没难度」那一档(?difficulty= 给空值); +// ?rating=2500 能试 DX RATING 那块底板的六档配色(绿/青/蓝/紫/橙/红,见 cf-rating-colors.js)。 +if (pageParams.get('resultPreview') === '1') { + const previewDifficulty = pageParams.get('difficulty'); + const previewRating = pageParams.get('rating'); + const rating = previewRating === null || previewRating.trim() === '' + ? 1523.4 : Number(previewRating); + timerController.previewResult({ + problemId: pageParams.get('problem') ?? '1999A', + result: { + dailyTrack: 3, + difficulty: previewDifficulty === null ? '1700' : (previewDifficulty.trim() === '' ? null : previewDifficulty), + title: pageParams.get('title') ?? 'Two Permutations', + practiceKind: 'first', + seconds: 1837, + // 段位不用给:弹窗按达成率自己查表(评级只跟达成率有关), + // 这样显示的数字和段位不会打架 + score: { achievementShown: 98.7654, rating }, + comparison: { previousScore: { achievementShown: 97.2, rating: rating - 23.3 }, ratingAfter: rating, ratingDelta: 23.3 }, + verdicts: { AC: 12, WA: 0, TLE: 0, RE: 0, CE: 0 }, + }, + }); +} renderYearOptions([currentYear()]); await refresh(); if (pageParams.get('add') === '1' && state.userId !== null) { diff --git a/public/help.html b/public/help.html index b22843c..bb9847c 100644 --- a/public/help.html +++ b/public/help.html @@ -1,13 +1,15 @@ -使用指南 · Algorithm DX + +使用指南 · Algorithm DX - + +
      GETTING STARTED

      使用指南

      从绑定第一个账号,到记录练习、看懂成绩与统计。

      前往账号管理 ↗
      @@ -136,4 +142,6 @@

      2. 取得 API key / secret 后配置

      - +TOP + + diff --git a/public/index.html b/public/index.html index 8445a4b..6ff52d7 100644 --- a/public/index.html +++ b/public/index.html @@ -4,18 +4,22 @@ Algorithm DX · AC 记录 - + + + + - + +
      -
      PRACTICE JOURNAL

      每一次通过,都值得记录

      汇集各平台的解题足迹,回看尝试,也发现下一步。

      +
      PRACTICE JOURNAL

      每一次通过,都值得记录

      汇集各平台的解题足迹,回看尝试,也发现下一步。

      —
      + 练习历史 ↗ + 查看我的 DX Rating ↗
      @@ -210,6 +220,8 @@

      这个筛选条件下没有记录

      +TOP + diff --git a/public/maimai-glyphs.css b/public/maimai-glyphs.css new file mode 100644 index 0000000..1e3f719 --- /dev/null +++ b/public/maimai-glyphs.css @@ -0,0 +1,41 @@ +/** + * 官方字形精灵的排版。 + * + * 这些精灵是**方格切片**(每格带透明边距),所以直接并排会显得字距很松; + * 这里用负外边距把字收紧。每个族的收紧量不同,是按实际观感定的。 + */ + +.mm-glyph { display: block; width: auto; } + +.mm-clear-word, +.mm-track-label, +.mm-rank-word, +.mm-number { display: inline-flex; align-items: flex-end; justify-content: center; } + +/* CLEAR! 六格,格子 80x80,字母只占中间一块,收 -0.2em 左右才连成词。 + 高度按官方的比例给:官方抓图里 CLEAR! 约占画面高的 7.5%。 */ +.mm-clear-word { margin: 0 -0.2em; } +.mm-clear-word .mm-glyph { height: 0.95em; } + +.mm-track-label .mm-glyph { height: 1em; } + +/* 段位:S/A/B/C 的格子同样是方图,+ 另有一张 */ +.mm-rank-word { margin: 0 -0.12em; } +.mm-rank-word .mm-glyph { height: 1em; } +/* 加号的位置/大小是对着官方 SSS+ 抓图做模板匹配量出来的(素材确认就是 UI_RSL_Rank_Plus): + · 墨水高 ≈ 字母格的 0.38(素材字形只占格子 83%,所以图取 0.445) + · 顶边在字母顶边下方约 0.08 个格子高 —— 也就是**右上角**,不是和字母底边对齐 + · 左边紧贴最后一个字母的右缘 + 之前底对齐 + 偏大,看着就像"挂"在字下面。 */ +.mm-rank-word .mm-rank-plus { height: 0.445em; align-self: flex-start; margin-top: 0.05em; margin-left: -0.02em; } + +/* 大数字:官方是两套图集混排 + 单独的 '%',三者的格子底边对齐、共用一条基线。 + 缩放比 98 : 82 : 54 是对着官方抓图量的(整数格 98、小数格 82、'%' 54)。 + 高度由调用方给 --n(整数部分的格子高),其余按比例推。 */ +.mm-number { display: inline-flex; align-items: flex-end; margin: 0; } +.mm-number .mm-number-int { height: var(--n, 1em); } +.mm-number .mm-number-frac { height: calc(var(--n, 1em) * 0.837); } +/* 相邻字形重叠 8%:格子带透明边距,不收会散开,收太多(-24%)又会挤成一团 */ +.mm-number .mm-number-int + .mm-number-int { margin-left: calc(var(--n, 1em) * -0.08); } +.mm-number .mm-number-frac + .mm-number-frac { margin-left: calc(var(--n, 1em) * -0.067); } +.mm-number .mm-number-percent { height: calc(var(--n, 1em) * 0.551); margin-left: calc(var(--n, 1em) * -0.04); } diff --git a/public/maimai-glyphs.js b/public/maimai-glyphs.js new file mode 100644 index 0000000..42ea3c1 --- /dev/null +++ b/public/maimai-glyphs.js @@ -0,0 +1,410 @@ +/** + * 官方结算素材拼字:CLEAR! / TRACK / 段位字母 / 大数字。 + * + * 页面上这些原本是 CSS 画的花体字,形似但终归不是官方的。客户端里这几样都有现成精灵, + * 尺寸和配色都是官方那套,这里只负责把格子拼成词,交给调用方塞进页面。 + * + * 几个只看文件名读不出来的地方(都是对着抓图核过的): + * · CLEAR! 是六个字母精灵,有 01/02/03 三套配色;抓图那套是 _01_(白字 + 蓝描边) + * · 段位没有 SS/SSS,是用 S 的颜色变体拼的:三个 S 就是 Blue/Red/Yellow + * · 大数字用分数那套金色精灵,格子号 0-9 是数字,10='+'、11='-'、12='/'、13='.' + * · '%' 是单独一张 UI_RSL_Score_Per_Gold + */ + +import { platformLabel } from './problem-source.js'; + +const ASSETS = '/assets/mm-result/'; +const RANK_LETTERS = new Set(['S', 'A', 'B', 'C', 'D']); + +/** 大数字可选的四套配色,和客户端里的族名一致 */ +export const NUMBER_COLOURS = ['Gold', 'Red', 'Blue', 'Base']; +/** 数字的两套图集:同一字体的两个尺寸,1110000 是大号 */ +export const NUMBER_ATLASES = ['1110000', '0001111']; + +/** 大数字的格子号:和 layout 里的编号一致 */ +const DIGIT_CELL = { ...Object.fromEntries([...'0123456789'].map((c, i) => [c, i])), '+': 10, '-': 11, '/': 12, '.': 13 }; + +/** + * 小数点的坑号**不是每族都一样**,这是逐族比字形才看出来的: + * 0001111_Red 的 12/13 是反的(13 画的是逗号,12 才是小数点)。其余族都是 13。 + * 只影响小数点,数字 0~9 各族的坑号一致。 + */ +const DOT_CELL = { Red: 12 }; + +function glyph(file, { className = '', height = '' } = {}) { + const img = document.createElement('img'); + img.className = 'mm-glyph' + (className ? ' ' + className : ''); + img.src = ASSETS + file; + img.alt = ''; + img.decoding = 'async'; + if (height) img.style.height = height; + return img; +} + +/** CLEAR! —— 六个字母精灵;抓图用的是 _01_ 那套配色 */ +export function clearWord({ variant = '01' } = {}) { + const word = document.createElement('span'); + word.className = 'mm-clear-word'; + for (let i = 1; i <= 6; i += 1) word.append(glyph(`UI_RSL_Text_Clear_${variant}_${String(i).padStart(2, '0')}.png`)); + return word; +} + +/** TRACK —— 官方就一张图。prefab 里 UI_Track 是 93x30 抓图 = 112x36 原生, + 素材 UI_RSL_Text_TRACK 正好 110x36,所以是按原生尺寸贴的。 */ +export function trackLabel() { + const label = document.createElement('span'); + label.className = 'mm-track-label'; + label.append(glyph('UI_RSL_Text_TRACK.png')); + return label; +} + +/** + * 达成率标题那一行 —— 用原版素材,不用页面自己拼的文字。 + * + * 只画 UI_RSL_Text_Achievement(prefab: AchievementLogo,296x41,抓图 247x40)。 + * prefab 里还有一条 UI_RSL_Base_Achievement(Achievment_Base/Base_01,抓图 1667x107) + * —— 那是 1667px 宽、比 903 的画面还宽的长条,本是溢出裁切的装饰底; + * 整条铺进这一行会变成一大片空色(文字只占最左一点),看着就是"意义不明的色块", + * 所以不画它。 + */ +export function achievementLabel() { + const wrap = document.createElement('span'); + wrap.className = 'mm-achv-label'; + wrap.append(glyph('UI_RSL_Text_Achievement.png', { className: 'mm-glyph mm-achv-label-text' })); + return wrap; +} + +/** + * 结算勋章:一块底座 + 两枚徽章。 + * + * prefab(UI_MedalDisplay,capture 250x125@(144,553)): + * Medal_Base UI_RSL_MedalBase 326x153 原生 -> capture 273x130@(172,552) 底座 + * v110 (左槽) capture 83x83@(227,574) -> AP_Base/AP_Text_01 + * Null_FS_FSD_Medal(右槽) capture 83x83@(318,574) -> FSD_Base/FSD_Text_01 + * 两槽间距 318-227 = 91 capture;槽相对底座左边 227-172 = 55 capture。 + * 整组中心 x=314,段位中心 x=292(差 22 capture ~ 26 原生),基本水平居中。 + */ +export function medalPair({ cleanSolve = true } = {}) { + const wrap = document.createElement('span'); + wrap.className = 'mm-medals'; + wrap.append(glyph('UI_RSL_MedalBase.png', { className: 'mm-glyph mm-medal-base' })); + // 两枚圆片是 Codex 生图产出的(青绿 Accepted / 香槟金 Clean Solve), + // 官方素材 UI_RSL_AP_* / UI_RSL_FSD_* 是分「底 + 字」两层、画风偏写实, + // 这里按需求换成配套的一对圆章。 + const ap = document.createElement('img'); + ap.className = 'mm-medal mm-medal--ap'; + ap.src = '/assets/maimai/medal-accepted.png'; + ap.alt = 'Accepted'; + wrap.append(ap); + const cs = document.createElement('img'); + cs.className = 'mm-medal mm-medal--fsd'; + cs.src = '/assets/maimai/medal-cleansolve.png'; + cs.alt = 'Clean Solve'; + // 语义:有 AC 就有 Accepted;AC 之外还有 WA/TLE/RE/CE 就不算 Clean Solve + cs.dataset.earned = cleanSolve ? 'true' : 'false'; + wrap.append(cs); + return wrap; +} + +/** + * 轨道号码("03")。 + * + * 官方不是写文字,是【数字精灵】+【同位的一块实心白块】两层: + * prefab: TrackNumber/UI_Track/NUM_TrackNumber UI_CMN_Num_26p_N 50x28 抓图 + * TrackNumber/UI_Track/NUM_TrackNumber_Outline UI_CMN_Num_26p_Outline_N 50x28 抓图(同位) + * 反编译: KOP_ResultTrackData 里 `_trackNoImages[0].ChangeSprite(trackNumber)` + * —— _trackNoImages 是数组,每个元素显示一位数字 + * + * ⚠️ 那个 Outline **不是描边图,是一整块实心白**(实测不透明 56.8%,ASCII 是一片白)。 + * 它靠 MultipleImage 的 Subtract 混合(MultiImageBlendType.Subtract / _BlendOp=2) + * 去"减掉"周围做镂空,**不能当普通图层叠上去** —— 叠上去数字会被整块盖住变全白。 + * 所以这里只画数字,描边用 CSS drop-shadow 近似;等解出 Subtract 的真实参数再换。 + * 数字本身是白色,这里按像素尺寸从 prefab 推:容器 60x34 原生放两位 => 每格 30x34 原生。 + */ +export function trackNumber(value) { + const row = document.createElement('span'); + row.className = 'mm-track-num'; + for (const ch of String(value)) { + if (!/[0-9]/.test(ch)) continue; + const cell = document.createElement('span'); + cell.className = 'mm-track-digit'; + cell.append(glyph(`UI_CMN_Num_26p_${ch}.png`, { className: 'mm-glyph mm-track-face' })); + row.append(cell); + } + return row; +} + +/** + * 段位:从 D 到 SSS+。 + * + * 官方只有 D/C/B/A/S 五张**单字**精灵,多字母的段位是重复拼出来的(SS/SSS、AA/AAA、BB/BBB)。 + * S 另有 Blue/Red/Yellow 三个配色:**三个 S 用主色 + Blue + Red,少于三个用 Yellow** + * (客户端里就是这么配的)。末尾带 + 时补一张 UI_RSL_Rank_Plus。 + * + * 注意:名字里带 _White 的那几张**不是白字**,是段位动画的碎裂特效,别拿来当字母用。 + */ +export function rankWord(rank) { + const word = document.createElement('span'); + word.className = 'mm-rank-word'; + const raw = String(rank ?? '').toUpperCase(); + const plus = raw.endsWith('+'); + const letters = raw.replace(/\+$/, ''); + if (!letters || ![...letters].every((c) => RANK_LETTERS.has(c))) { + // 认不出来的段位(例如 AC)原样显示,至少不会画错 + word.textContent = String(rank ?? ''); + return word; + } + const files = letters === 'SSS' + ? ['UI_RSL_Rank_S.png', 'UI_RSL_Rank_S_Blue.png', 'UI_RSL_Rank_S_Red.png'] + : [...letters].map((letter) => letter === 'S' + ? 'UI_RSL_Rank_S_Yellow.png' + : `UI_RSL_Rank_${letter}.png`); + // ⭐ 多字母段位里,后面的字母比第一个小 —— 这是官方的排法,不是等大重复。 + // 量官方预制整图(UI_GAM_Rank_*)的内容宽度: + // A 单字 35 | AA 58 | AAA 82 => AAA 只有 3x35 的 78% + // B 单字 30 | BB 52 | BBB 75 => BBB 只有 3x30 的 83% + // S 单字 30 | SS 58 | SSS 85 => SSS 是 3x30 的 94%(基本等大) + // 所以 A/B 族的第 2 个起缩到 0.72,S 族保持等大。 + const smallTail = letters[0] === 'A' || letters[0] === 'B'; + files.forEach((file, i) => { + const cls = 'mm-rank-letter' + (smallTail && i > 0 ? ' mm-rank-letter-sm' : ''); + word.append(glyph(file, { className: cls })); + }); + if (plus) word.append(glyph('UI_RSL_Rank_Plus.png', { className: 'mm-rank-plus' })); + return word; +} + +/** + * 大数字:'98.7654' 或 '100.1445%'。 + * + * 官方这套排法和普通文字排版不一样,是**两套图集混排**(对着抓图核过): + * · 整数部分用大号图集 1110000(格子 72x98,字形 103 原生) + * · 小数点和小数部分用小号图集 0001111(格子 62x82,字形 83 原生) + * · '%' 用单独的 UI_RSL_Score_Per_*(58x54) + * 三者的格子**底边对齐**,所以整行共用一条基线;缩放比 98 : 82 : 54。 + * + * 配色由达成率决定:< 80 蓝、>= 97 金、其余红。 + */ +export function scoreNumber(text, { colour = 'Gold' } = {}) { + const row = document.createElement('span'); + row.className = 'mm-number'; + const value = String(text); + const dot = value.indexOf('.'); + const whole = dot === -1 ? value.replace('%', '') : value.slice(0, dot); + const fraction = dot === -1 ? '' : value.slice(dot + 1).replace('%', ''); + const hasPercent = value.endsWith('%'); + // '%' 只有 Gold/Red/Blue 三个变体,没有 Base,缺的落到 Gold + const percentColour = ['Gold', 'Red', 'Blue'].includes(colour) ? colour : 'Gold'; + + const digit = (ch, cls) => { + const cell = ch === '.' ? (DOT_CELL[colour] ?? DIGIT_CELL['.']) : DIGIT_CELL[ch]; + if (cell === undefined) return null; + const atlas = cls === 'mm-number-int' ? '1110000' : '0001111'; + return glyph(`UI_NUM_Score_${atlas}_${colour}_${cell}.png`, { className: 'mm-glyph ' + cls }); + }; + for (const ch of whole) { + const img = digit(ch, 'mm-number-int'); + if (img) row.append(img); + } + if (fraction) { + // 小数点单独挂一个 mm-number-dot:它在格子里的留白和数字完全不是一回事 + // (左16.9 / 右15.4 / 底12.3 px,数字是 2.3 / 2.3 / 3.8), + // 排版要单独压、两侧单独收,见 dx-result.css 里那段「达成率大数字」。 + const point = digit('.', 'mm-number-frac mm-number-dot'); + if (point) row.append(point); + for (const ch of fraction) { + const img = digit(ch, 'mm-number-frac'); + if (img) row.append(img); + } + } + if (hasPercent) row.append(glyph(`UI_RSL_Score_Per_${percentColour}.png`, { className: 'mm-glyph mm-number-percent' })); + return row; +} + +/** 达成率对应哪套配色:< 80 蓝、>= 97 金、其余红 */ +export function achievementColour(value) { + const number = Number(value); + if (!Number.isFinite(number)) return 'Gold'; + if (number >= 97) return 'Gold'; + if (number < 80) return 'Blue'; + return 'Red'; +} + +/* ------------------------------------------------------------------ * + * 题目卡(原「乐曲卡」,prefab 里是 UI_MusicData) + * ------------------------------------------------------------------ */ + +/** + * 卡片底板:官方 `UI_RSL_MBase_*`,745x142 原生,一张一个难度色。 + * + * prefab 量出来的构件(原生 px,相对底板左上角)——下面 CSS 里的百分比都按这张表算: + * IMG_Background UI_RSL_MBase_ADV 整张 745x142 + * JacketImage 槽位 x11..95 / y47..131(板上烤着一块纯黑,游戏里拿曲绘盖住) + * Mask 深蓝标题条 x106..709 / y88..128(也烤在板上) + * 难度名 x≈106..335 / y≈50..80(BASIC/ADVANCED/EXPERT/MASTER/…) + * GameType UI_RSL_MBase_Parts_01 84x28 → 摆在 x≈488..572 / y≈51..75 + * + * ⚠️ 板上烤着难度名,题目卡不能直接显示它 —— 见 `problemCard()` 里那块 cover: + * 用同一张板的**干净底色**把那一小段盖掉,位置也是按 prefab 量的。 + * + * ⚠️ 官方七张板(BSC/ADV/EXP/MST/MST_Re/UTG/DMY)里**没有蓝也没有青** + * —— maimai 的难度色本来就没有蓝。所以按色相挑一张最近的板, + * 再用 saturate()+hue-rotate() 把板面主色拉到 cfRatingColor 那一档的颜色上。 + * 每组参数都是按 CSS 滤镜矩阵解出来的(sRGB 误差 < 25/255): + * green 板面 #45c124 → #4aa85c cyan #45c124 → #3fa8c4 + * blue #9f51dc → #4a7fe0 violet #9f51dc → #8b6cf0 + * orange #ffba01 → #f08c3a red #ff5a66 → #e8564f + * gray #969696 → #7b8391 + * 深蓝标题条会被这个滤镜带偏,所以脚本另外压一条自己的 navy(`.dx-result-track-bar`)。 + */ +const CARD_TONES = { + green: { plate: 'UI_RSL_MBase_BSC.png', level: 'BSC', field: '#45c124', filter: 'saturate(0.66) hue-rotate(15deg)' }, + cyan: { plate: 'UI_RSL_MBase_BSC.png', level: 'BSC', field: '#45c124', filter: 'saturate(0.68) hue-rotate(79deg)' }, + blue: { plate: 'UI_RSL_MBase_MST.png', level: 'MST', field: '#9f51dc', filter: 'saturate(0.98) hue-rotate(-44deg)' }, + violet: { plate: 'UI_RSL_MBase_MST.png', level: 'MST', field: '#9f51dc', filter: 'saturate(1.08) hue-rotate(-14deg)' }, + orange: { plate: 'UI_RSL_MBase_ADV.png', level: 'EXP', field: '#ffba01', filter: 'saturate(1.2) hue-rotate(-37deg)' }, + red: { plate: 'UI_RSL_MBase_EXP.png', level: 'EXP', field: '#ff5a66', filter: 'saturate(0.9) hue-rotate(12deg)' }, + gray: { plate: 'UI_RSL_MBase_DMY.png', level: 'MST', field: '#969696', filter: 'saturate(0) brightness(0.87)' }, + /* 未评级(题目还没难度):底板用官方 **UTAGE**(宴)卡 UI_RSL_MBase_UTG.png(745x141)。 + 它的轮廓和灰 DMY(板上烤着 DUMMY)**逐点相同**(alpha 包围盒、圆盘拟合完全一致), + 差别只有配色:UTG 板面是粉紫,板上烤着「U・TA・GE」(x110..243/y53..70, + 落在 cover 那块补丁 x96..304/y41..87 里,会被完全盖掉)。 + 所以灰档配 DMY 其实等于没换 —— 要的是 UTAGE 那张卡本身。 + filter 取 none:那套 saturate/hue-rotate 是为了把七张难度板的板面主色拉到 + CF 那一档的颜色上(蓝/青/绿…),unrated 没有对应档位,UTAGE 卡是什么色就该是什么色。 + field #ff86fe:这是 cover 那圈**紧邻的板面平色**,和另外五张板同一个取法 + (BSC→#45c124、MST→#9f51dc、DMY→#969696 都是这么取的,逐个核对过)。 + ⚠️ 不能拿 cover **里面**的主色:UTG 那块补丁里压着一片洋红 (249,45,218), + 而补丁四周是浅粉 (255,134,254),照搬里面那块会在标题左边留下一个 + 深洋红的方块(上下两条边都露)。补丁必须和**外面**接得上。 + ⚠️ level 仍是 MST:图集里没有 UTG 族(只有 BSC/EXP/MST/MST_Re), + 「LV」那个字母只能借一族,MST 的紫最接近这张粉卡。 */ + unrated: { plate: 'UI_RSL_MBase_UTG.png', level: 'MST', field: '#ff86fe', filter: 'none' }, +}; + +/** tone → { plate, level, field, filter },认不出来的一律按未评级(官方那张灰 DMY) */ +export function cardTone(tone) { + return CARD_TONES[tone] ?? CARD_TONES.unrated; +} + +/** 等级数字图集 `UI_CMN_MusicLevel_{族}_{格}.png` 的格子号:0-9 是数字,14 是「LV」 */ +const LEVEL_CELL = { '+': 10, '-': 11, ',': 12, '.': 13, LV: 14 }; + +/** 难度未知时顶在数字位置上的那个字。图集里没有问号(15 格 = 0-9 + - , . LV),只能排字 */ +const UNKNOWN_LEVEL = '?'; + +/** 「?」那颗字。样式在 dx-result.css 的 .mm-level-unknown(按数字精灵的白芯配大小/基线) */ +function unknownLevelMark() { + const node = document.createElement('span'); + node.className = 'mm-level-unknown'; + node.textContent = UNKNOWN_LEVEL; + node.setAttribute('aria-label', '难度待定'); + return node; +} + +/** + * 官方等级数字。等宽格子 48x60,数字/符号直接按格子号取图。 + * ⚠️ 第 14 格画的是 **LV** 两个字母(不是任务描述里说的 '%')—— + * 对着官方抓图核过:卡片右端那个小 `LV` 就是这个格子,所以不用 UI_KLD_MSS_Level_* + * (那五张是「LEVEL」美字的**噪声动画帧**,单张拿出来是散开的色偏切片)。 + * `value` 里的 '?' 走 unknownLevelMark()(文字,不是精灵),其余认不出的字符跳过。 + * + * 结构:`.mm-level` 里两个**各自绝对定位**的子节点 —— + * `.mm-level-lv` LV,钉死在卡片里一个固定 x(见 dx-result.css) + * `.mm-level-digits` 这一串数字(或那个 `?`),整体以圆盘圆心为心居中 + * 分开包一层是必须的:LV 固定、数字居中,两者不能还在同一条 flex 行里, + * 否则 LV 的 x 一定会跟着数字宽度跑(改之前就是这样)。见 dx-result.css 那段注释。 + */ +export function levelNumber(value, { tone = 'unrated' } = {}) { + const { level } = cardTone(tone); + const row = document.createElement('span'); + row.className = 'mm-level'; + const cell = ch => (/^[0-9]$/.test(ch) ? Number(ch) : LEVEL_CELL[ch]); + const lv = cell('LV'); + row.append(glyph(`UI_CMN_MusicLevel_${level}_${lv}.png`, { className: 'mm-level-lv' })); + const digits = document.createElement('span'); + digits.className = 'mm-level-digits'; + for (const ch of String(value)) { + if (ch === UNKNOWN_LEVEL) { digits.append(unknownLevelMark()); continue; } + const index = cell(ch); + if (index === undefined) continue; + digits.append(glyph(`UI_CMN_MusicLevel_${level}_${index}.png`, { className: 'mm-level-digit' })); + } + row.append(digits); + return row; +} + +/** GameType 徽章(官方 `UI_RSL_MBase_Parts_01`,就是抓图上那块「でらっくす」小牌) */ +export function gameTypeBadge() { + const wrap = document.createElement('span'); + wrap.className = 'mm-gametype'; + wrap.append(glyph('UI_RSL_MBase_Parts_01.png')); + return wrap; +} + +/** + * 题目卡内容:官方底板 + 官方等级数字。 + * + * 结构(外层 `.dx-result-track` 保留在弹窗里,只往里塞这些东西): + * art 底板的画布 —— saturate/hue-rotate 只作用在这一层 + * plate 底板本体 + * cover 盖掉板上烤着的难度名(用同一张板的干净底色,所以滤镜一加还是严丝合缝) + * bar 深蓝标题条(自己压的 navy,免得被滤镜带偏) + * slot 曲绘槽 → 放题号 + * heading 难度名那一段 → 放赛制/平台 + * badge でらっくす + * title 标题条 → 放题名 + * level 右侧圆盘 → LV + 难度分 + * + * `difficulty` 为 null 时(题目还没难度)不画数字,等级那一格画一个文字「?」, + * 底板落到 UTAGE 那张卡(`tone` 会是 unrated,见 CARD_TONES.unrated)。 + * + * `kind` 是**已经翻好的字面**(练习类型:INDEPENDENT/REPLAY/…,调用方从 dx-timer.js 的 + * PRACTICE_KIND_LABEL 取),`platform` 是**平台 key**(codeforces/atcoder/…,不是字面), + * 由 problem-source.js 的 PLATFORM_LABELS 翻成卡片上的字。给 key 而不是字面, + * 是为了让「平台词」永远有值:不传/认不出都会落回那边的默认值,卡片不会空一格。 + * 两个词都不在这份模板里写死。 + */ +export function problemCard({ problemId = '', kind = '', platform = '', title = '', difficulty = null, tone = 'unrated' } = {}) { + const info = cardTone(tone); + const card = document.createDocumentFragment(); + const span = (className, text) => { + const node = document.createElement('span'); + node.className = className; + if (text !== undefined) node.textContent = text; + return node; + }; + + const art = span('dx-result-track-art'); + art.setAttribute('aria-hidden', 'true'); + art.append(span('dx-result-track-plate'), span('dx-result-track-cover')); + const bar = span('dx-result-track-bar'); + bar.setAttribute('aria-hidden', 'true'); + + const slot = span('dx-result-track-slot'); + slot.textContent = String(problemId).replace(/[:\s]/g, ''); + slot.setAttribute('aria-label', `题目 ${problemId}`); + + // 难度名那一段换成赛制 + 平台:左边的题号已经在曲绘槽里了,这里不重复。 + // 两个词都来自 key→字面的表,不在模板里写死字符串(见上面的参数说明)。 + const heading = span('dx-result-track-heading'); + heading.append(span('dx-result-kind', kind), span('dx-result-problem', platformLabel(platform))); + + const name = document.createElement('h3'); + name.className = 'dx-result-name'; + const titleText = span('dx-result-title', String(title || problemId)); + name.append(titleText); + + const level = span('dx-result-level dx-result-level-disc'); + if (Number.isFinite(difficulty)) level.append(levelNumber(String(difficulty), { tone })); + /* 没难度:底板是 UTAGE 卡(见 CARD_TONES.unrated),等级那一格画「?」—— + 官方图集里没有问号精灵,用文字顶上,字号/基线对着数字精灵的白芯配(见 CSS)。 + 原来这里退回文字「待定」(.dx-result-level-pending,样式还留在 dx-result.css 里, + 要换回来改这一行就行):「LV ?」和「待定」说的是同一件事,并排写重复; + 而且两段加起来会把整组往左推 38px(1200 视口实测盒 x 683 → 645)。 */ + else level.append(levelNumber(UNKNOWN_LEVEL, { tone })); + + card.append(art, bar, slot, heading, gameTypeBadge(), name, level); + return card; +} + diff --git a/public/mascot.css b/public/mascot.css new file mode 100644 index 0000000..2bc361c --- /dev/null +++ b/public/mascot.css @@ -0,0 +1,99 @@ +/** + * 看板娘设置弹窗的样式。 + * + * 和 dx-result.css 分开:这一套是全站设置,任何页面都能挂;结算弹窗里那块看板娘的 + * 样式跟着结算走,写在 dx-result.css。配色沿用站点既有的深浅主题变量。 + */ + +.mm-mascot-dialog { + width: min(460px, calc(100vw - 32px)); + padding: 0; + border: 1px solid var(--line, #d5d9e6); + border-radius: 14px; + background: var(--panel, #fff); + color: var(--ink, #1b1f2a); + box-shadow: 0 24px 70px #0308244d; +} + +.mm-mascot-dialog::backdrop { background: #071126a8; backdrop-filter: blur(4px); } + +.mm-mascot-form { position: relative; padding: 18px 20px 16px; } + +.mm-mascot-form h2 { margin: 0 0 8px; font-size: 17px; } + +.mm-mascot-close { + position: absolute; + right: 10px; + top: 10px; + width: 28px; + height: 28px; + border: 1px solid var(--line, #d5d9e6); + border-radius: 50%; + background: transparent; + color: inherit; + font-size: 19px; + line-height: 1; + cursor: pointer; +} + +.mm-mascot-hint { margin: 0 0 12px; font-size: 12.5px; line-height: 1.6; opacity: .82; } +.mm-mascot-hint a { color: var(--accent, #2f6fed); } +.mm-mascot-hint code { + padding: 1px 4px; + border-radius: 4px; + background: var(--code-bg, #00000010); + font-size: 12px; +} + +.mm-mascot-label { display: block; margin-bottom: 4px; font-size: 12px; font-weight: 700; } + +.mm-mascot-input { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--line, #d5d9e6); + border-radius: 8px; + background: var(--input-bg, #f7f8fc); + color: inherit; + font: 12.5px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; + word-break: break-all; +} + +.mm-mascot-input:focus { outline: 2px solid var(--accent, #2f6fed); outline-offset: 1px; } + +/* 预览固定在一条高度里:看板娘是竖长图,高度不定会把弹窗顶来顶去 */ +.mm-mascot-preview { + display: grid; + place-items: center; + height: 190px; + margin: 12px 0; + border: 1px dashed var(--line, #d5d9e6); + border-radius: 10px; + overflow: hidden; + background: var(--input-bg, #f7f8fc); +} + +.mm-mascot-preview-img { max-width: 100%; max-height: 190px; object-fit: contain; } + +.mm-mascot-actions { display: flex; justify-content: flex-end; gap: 8px; } + +/* 顶栏入口:和深色模式开关并排,沿用同一个圆钮语言 */ +body.mm-theme .mm-mascot-btn { + flex: 0 0 auto; + height: 44px; + margin-left: 6px; + padding: 0 16px; + border: 3px solid var(--mm-purple, #8b6fd6); + border-radius: 100px; + background: #fff; + color: var(--mm-purple, #6b4bbd); + font-size: 13px; + font-weight: 800; + cursor: pointer; +} + +body.mm-theme .mm-mascot-btn:hover { background: #f3efff; } +body.mm-theme[data-mm-theme="dark"] .mm-mascot-btn { background: #221c33; color: #d8ccff; } +body.mm-theme[data-mm-theme="dark"] .mm-mascot-btn:hover { background: #2c2542; } + +/* 结算弹窗里的看板娘没加载出来时别留破图 */ +.mm-mascot-failed { display: none; } diff --git a/public/mascot.js b/public/mascot.js new file mode 100644 index 0000000..241084f --- /dev/null +++ b/public/mascot.js @@ -0,0 +1,181 @@ +/** + * 看板娘:一处全局设置,一处展示。 + * + * 图片来自 lolicount.top —— 那个站按 URL 参数出图(`@用户名` 选人、`theme` 选主题、 + * `scale`/`fsize` 调大小、`text` 是计数文字)。直接用 引用就行,不走 fetch: + * 不会碰上跨域,站上那边换图我们也不用改代码。 + * + * 链路:设置弹窗写 localStorage(mm-mascot)→ 发 mm-mascot-change → + * 要展示的地方(目前是结算弹窗)自己重画。无依赖,原生 ESM,和 theme.js 一个路数。 + * + * 站点:https://lolicount.top —— 设置页里给出入口,让用户自己去挑。 + */ + +const MASCOT_KEY = 'mm-mascot'; + +/** 默认看板娘:一串普通的 lolicount.top 图片链接,用户随时可以换成自己的 */ +export const MASCOT_DEFAULT = + 'https://lolicount.top/@ttttt?theme=manosaba-ema&fsize=16&scale=1&unshowf=true&text=%7Bn%7D'; + +/** 站上「获取链接」的入口,设置页引导用户去这里挑 */ +const MASCOT_SITE = 'https://lolicount.top'; + +/** + * 远端图挂了就退回这张本地立绘(仓库自带,不联外网)。 + * + * 不是随便找的占位图:这是客户端里结算画面那位导航角色的立绘 + * `UI_Navichara_01_P1_02`(524×812 RGBA,全身站姿、底色全透明)—— + * 结算画面的 `Null_NavicharaRoot` 挂的就是 `Navi_Default` 这一族。 + * 同族的 02/03/04/05 是同一姿势的几帧表情差分,02 是两眼睁开、 + * 站得最"正"的那一帧。 + * + * 之前挑过 `UI_ADV_Img_chara155_01_01`(616×1160,撑伞的紫发角色), + * 那张本身没问题,但她是淡紫配白,放在成绩板那块近白玻璃后面整片糊掉, + * 立绘就"读"不出来了;这一位是红裙 + 黑色条纹袜,浅底板上反差够, + * 隔着板子还看得见人。(同族的 `UI_ADV_Img_chara155_01_shadow` + * 证明 chara155 那族也是"站到背景上"的图,只是不适合这个底板。) + * + * 出图路子见 `scripts/extract-result-assets.py`(UnityPy 读 resources.assets + * 的 Texture2D),这里只放结果。 + */ +export const MASCOT_FALLBACK = '/assets/mm-parts/navichara-01-p1-02.png'; + +export function getMascotUrl() { + try { + const value = localStorage.getItem(MASCOT_KEY); + return value && value.trim() ? value.trim() : MASCOT_DEFAULT; + } catch { + return MASCOT_DEFAULT; + } +} + +export function setMascotUrl(url) { + const value = typeof url === 'string' ? url.trim() : ''; + try { + // 空的就删掉键,回到默认;这样「恢复默认」和「填了自己的」共用一条路径 + if (!value || value === MASCOT_DEFAULT) localStorage.removeItem(MASCOT_KEY); + else localStorage.setItem(MASCOT_KEY, value); + } catch { + /* 隐私模式下写不进去,当次会话照样能用 */ + } + document.dispatchEvent(new CustomEvent('mm-mascot-change', { detail: { url: getMascotUrl() } })); +} + +/** + * 造一个看板娘 。 + * + * lolicount.top 给的是 SVG,浏览器能直接在 里画;这里不解析它的内容, + * 将来它换成别的位图格式也照用。 + * + * 三级降级:用户链接 → 本地占位图(fallback)→ 彻底藏起来。 + * 前两级任何一级失败都不会露出浏览器的破图标;最后一级加 .mm-mascot-failed, + * 由 mascot.css 设成 display:none —— 布局照样是完整的一块,只是里面没有图。 + */ +export function mascotImage({ className = '', alt = '看板娘', fallback = MASCOT_FALLBACK } = {}) { + const img = document.createElement('img'); + img.className = className; + img.alt = alt; + img.loading = 'lazy'; + img.decoding = 'async'; + img.referrerPolicy = 'no-referrer'; + let dead = false; // 已经退到尽头,别再互相触发 error + const fail = () => { + img.classList.add('mm-mascot-failed'); + img.alt = '看板娘加载失败,检查一下链接'; + img.removeAttribute('src'); + }; + const show = (url) => { + if (!url) { dead = true; fail(); return; } + dead = false; + img.classList.remove('mm-mascot-failed'); + img.src = url; + }; + img.addEventListener('error', () => { + if (dead) return; + // 先退回本地占位图;占位图自己也没了就藏起来,别留破图 + if (fallback && img.getAttribute('src') !== fallback) { img.src = fallback; return; } + dead = true; + fail(); + }); + show(getMascotUrl()); + // 设置改了就换图;同一个元素上只挂一次 + const onchange = (event) => show(event.detail?.url || getMascotUrl()); + img.addEventListener('mm-mascot-detach', () => document.removeEventListener('mm-mascot-change', onchange)); + document.addEventListener('mm-mascot-change', onchange); + return img; +} + +let settingsDialog = null; + +/** 打开看板娘设置:一个链接输入框 + 实时预览 + 去站上挑的引导 */ +export function openMascotSettings() { + if (!settingsDialog) settingsDialog = buildSettings(); + const input = settingsDialog.querySelector('.mm-mascot-input'); + const preview = settingsDialog.querySelector('.mm-mascot-preview'); + input.value = getMascotUrl(); + preview.replaceChildren(mascotImage({ className: 'mm-mascot-preview-img' })); + if (!settingsDialog.open) settingsDialog.showModal(); + input.focus(); + input.select(); +} + +function buildSettings() { + const dialog = document.createElement('dialog'); + dialog.className = 'mm-mascot-dialog'; + dialog.setAttribute('aria-labelledby', 'mmMascotTitle'); + dialog.innerHTML = ` +
      + +

      看板娘

      +

      + 图片来自 lolicount.top ↗: + 在那个站点里挑好角色和主题,复制它给你的图片链接,粘贴到下面即可。链接里 + @名字 选角色、theme 选主题、scale 调节大小。 +

      + + +
      +
      + + +
      +
      `; + const input = dialog.querySelector('.mm-mascot-input'); + const preview = dialog.querySelector('.mm-mascot-preview'); + + const redraw = () => { + // 预览走同一个地址,所见即所得;这里不写 localStorage,避免边打字边存进半截链接 + const img = document.createElement('img'); + img.className = 'mm-mascot-preview-img'; + img.alt = '看板娘预览'; + img.referrerPolicy = 'no-referrer'; + img.src = input.value.trim() || MASCOT_DEFAULT; + preview.replaceChildren(img); + }; + + dialog.querySelector('.mm-mascot-save').addEventListener('click', () => { + setMascotUrl(input.value); + dialog.close(); + }); + dialog.querySelector('.mm-mascot-reset').addEventListener('click', () => { + input.value = MASCOT_DEFAULT; + redraw(); + }); + input.addEventListener('input', redraw); + dialog.addEventListener('close', () => { + // 关闭时丢弃没保存的编辑,免得下次打开显示的是没生效的内容 + input.value = getMascotUrl(); + }); + document.body.append(dialog); + return dialog; +} + +const button = document.getElementById('mmMascotBtn'); +if (button) button.addEventListener('click', () => openMascotSettings()); + +// ?mascot=1 直接打开设置:截图、演示、自动化测试都用得上 +if (new URLSearchParams(location.search).get('mascot') === '1') { + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => openMascotSettings()); + else openMascotSettings(); +} diff --git a/public/mm-result-bg.html b/public/mm-result-bg.html new file mode 100644 index 0000000..d066993 --- /dev/null +++ b/public/mm-result-bg.html @@ -0,0 +1,279 @@ + + +maimai DX 结算 · 纯动态背景 + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      评级 + + ·?rank= | ?ui=0
      + diff --git a/public/problem-source.js b/public/problem-source.js new file mode 100644 index 0000000..dfbe284 --- /dev/null +++ b/public/problem-source.js @@ -0,0 +1,39 @@ +/** + * 结算卡片那行小字里的**平台词**:平台 key → 卡片上印的字面。 + * + * 卡片右上那行小字是两个词:练习类型(INDEPENDENT / REPLAY / …,由 dx-timer.js 的 + * PRACTICE_KIND_LABEL 给)+ 判题平台(这一份)。平台词原来是写死在 + * maimai-glyphs.js 的 problemCard() 里的字符串 'CODEFORCES',以后题目会来自别的判题站, + * 所以收到这张表里,全站只有这一处决定卡片印什么。 + * + * key 用的是 `src/fetchers/registry.ts` 里那套平台标识(accounts.platform 用的也是它), + * 别另造一套拼法。字面走**全大写拉丁**:底板那一格官方烤的是难度名(BASIC/EXPERT…), + * 都是大写拉丁,这里跟着它。注意这和 stat-labels.js 的 platformNames 不是一件事 —— + * 那是账号/统计页的中文名(Codeforces、洛谷、码蹄集),卡片要的是板上的短词, + * 分开放,谁的措辞要改都不会带偏对方。 + * + * 今天服务端只会发 Codeforces(见 dx-timer.js 里的说明),所以实际渲染仍然是 CODEFORCES; + * 以后接别的判题站时,只要那边把 platform 一起发下来、 + * 调用方把 key 传给 problemCard({ platform }),这张表直接就能用。 + */ +export const PLATFORM_LABELS = { + codeforces: 'CODEFORCES', + atcoder: 'ATCODER', + leetcode: 'LEETCODE', + 'leetcode-cn': 'LEETCODE CN', + luogu: 'LUOGU', + nowcoder: 'NOWCODER', + matiji: 'MATIJI', +}; + +/** 没有平台信息时(今天就是这种)卡片印哪一个。 */ +export const DEFAULT_PLATFORM = 'codeforces'; + +/** + * 平台 key → 卡片上的字面。空值、认不出的 key 一律落回 DEFAULT_PLATFORM: + * 卡片这一格不能空着。 + */ +export function platformLabel(platform) { + const key = String(platform ?? '').trim().toLowerCase(); + return PLATFORM_LABELS[key] ?? PLATFORM_LABELS[DEFAULT_PLATFORM]; +} diff --git a/public/stat-charts.js b/public/stat-charts.js index 14f7410..3e76789 100644 --- a/public/stat-charts.js +++ b/public/stat-charts.js @@ -1,7 +1,15 @@ const colors = ['#5bceac','#62a8ff','#ad93ff','#f2c66d','#f38a96','#68d5e7','#c1cbdd']; const node = (tag,text,cls) => { const n=document.createElement(tag); if(text)n.textContent=text; if(cls)n.className=cls;return n; }; -const font = 'system-ui, "Microsoft YaHei", sans-serif'; +// 画布里的字也要跟官方那套字体(@font-face 已加载,render 前会 await document.fonts.ready) +const font = '"M PLUS 1p", "M PLUS 1p Symbols", "Microsoft YaHei", "PingFang SC", sans-serif'; function text(ctx, value, x, y, size=12, color='#a9b8cd',align='left') { ctx.font=`${size}px ${font}`;ctx.fillStyle=color;ctx.textAlign=align;ctx.fillText(String(value),x,y); } +// 画布上的底色、网格和字号不跟 CSS 变量走,得按主题自己取一套; +// 切换主题时由 stats-page 重新画一遍(导出图片始终用深色海报底,不受这里影响) +const PALETTE = { + light: { plot:'#f2eefb', grid:'#ded6ee', track:'#e7e1f4', text:'#5a5470', muted:'#7b7490', value:'#2f2a44', stroke:'#00000022' }, + dark: { plot:'#141e2e', grid:'#27364b', track:'#233147', text:'#a9b8cd', muted:'#8798b2', value:'#e0eafb', stroke:'#ffffff55' }, +}; +const pal = () => PALETTE[document.documentElement.dataset.mmTheme === 'dark' ? 'dark' : 'light']; function shorten(value,n=24) { return value.length>n ? value.slice(0,n-1)+'…' : value; } export function chart(title, input, note='', type='bar') { @@ -11,16 +19,16 @@ export function chart(title, input, note='', type='bar') { const height=type==='bar' ? Math.max(160,rows.length*29+30) : type==='donut' ? Math.max(250,rows.length*27+40) : 260; const canvas=node('canvas',null,'stat-canvas');canvas.width=1040;canvas.height=height*2;canvas.setAttribute('role','img'); canvas.setAttribute('aria-label',`${title}。${rows.map(r=>`${r.label}:${r.count}`).join(';')}`); - const ctx=canvas.getContext('2d');ctx.scale(2,2);ctx.fillStyle='#141e2e';ctx.fillRect(0,0,520,height); - if (!total) {text(ctx,'此范围暂无数据',260,height/2,15,'#8798b2','center');} + const ctx=canvas.getContext('2d');ctx.scale(2,2);const p=pal();ctx.fillStyle=p.plot;ctx.fillRect(0,0,520,height); + if (!total) {text(ctx,'此范围暂无数据',260,height/2,15,p.muted,'center');} else if (type==='donut') { let angle=-Math.PI/2; rows.forEach((r,i)=> {const end=angle+r.count/total*Math.PI*2;ctx.beginPath();ctx.arc(115,125,76,angle,end);ctx.strokeStyle=colors[i%colors.length];ctx.lineWidth=24;ctx.stroke();angle=end; - ctx.fillStyle=colors[i%colors.length];ctx.fillRect(225,22+i*27,9,9);text(ctx,shorten(r.label,23),243,31+i*27);text(ctx,`${r.count} · ${(r.count/total*100).toFixed(1)}%`,505,31+i*27,11,'#e0eafb','right');}); - text(ctx,total,115,127,30,'#e6edf3','center');text(ctx,'次提交',115,151,12,'#8798b2','center'); + ctx.fillStyle=colors[i%colors.length];ctx.fillRect(225,22+i*27,9,9);text(ctx,shorten(r.label,23),243,31+i*27,12,p.text);text(ctx,`${r.count} · ${(r.count/total*100).toFixed(1)}%`,505,31+i*27,11,p.value,'right');}); + text(ctx,total,115,127,30,p.value,'center');text(ctx,'次提交',115,151,12,p.muted,'center'); } else if(type==='line' || type==='columns') { const left=42,right=500,top=28,bottom=210,axisMax=Math.ceil(max/4)*4; - for(let i=0;i<=4;i++){const y=bottom-(bottom-top)*i/4;ctx.strokeStyle='#27364b';ctx.beginPath();ctx.moveTo(left,y);ctx.lineTo(right,y);ctx.stroke();text(ctx,axisMax*i/4,left-8,y+4,10,'#8798b2','right');} + for(let i=0;i<=4;i++){const y=bottom-(bottom-top)*i/4;ctx.strokeStyle=p.grid;ctx.beginPath();ctx.moveTo(left,y);ctx.lineTo(right,y);ctx.stroke();text(ctx,axisMax*i/4,left-8,y+4,10,p.muted,'right');} const x=i=>left+(right-left)*(rows.length===1?0.5:i/(rows.length-1)); const y=r=>bottom-r.count/axisMax*(bottom-top); if(type==='line') { @@ -28,10 +36,10 @@ export function chart(title, input, note='', type='bar') { ctx.lineTo(x(rows.length-1),bottom);ctx.lineTo(x(0),bottom);ctx.closePath();const g=ctx.createLinearGradient(0,top,0,bottom);g.addColorStop(0,'#5bceac55');g.addColorStop(1,'#5bceac00');ctx.fillStyle=g;ctx.fill(); rows.forEach((r,i)=>{if(rows.length<=40){ctx.beginPath();ctx.arc(x(i),y(r),3,0,Math.PI*2);ctx.fillStyle=colors[0];ctx.fill();}}); } else rows.forEach((r,i)=>{const w=(right-left)/rows.length;ctx.fillStyle=colors[1];ctx.fillRect(left+i*w+2,y(r),Math.max(1,w-4),bottom-y(r));}); - rows.forEach((r,i)=>{if(i===0||i===rows.length-1||i%Math.max(1,Math.ceil(rows.length/6))===0) text(ctx,r.label.length>7?r.label.slice(5):r.label,type==='line'?x(i):left+(i+.5)*(right-left)/rows.length,233,10,'#8798b2','center');}); - text(ctx,'提交次数',left,14,10); + rows.forEach((r,i)=>{if(i===0||i===rows.length-1||i%Math.max(1,Math.ceil(rows.length/6))===0) text(ctx,r.label.length>7?r.label.slice(5):r.label,type==='line'?x(i):left+(i+.5)*(right-left)/rows.length,233,10,p.muted,'center');}); + text(ctx,'提交次数',left,14,10,p.muted); } else { - rows.forEach((r,i)=>{const y=20+i*29; text(ctx,shorten(r.label,25),5,y+12,12);ctx.fillStyle='#233147';ctx.fillRect(195,y,250,12);ctx.fillStyle=r.color||colors[i%colors.length];ctx.fillRect(195,y,250*r.count/max,12);if(r.color){ctx.strokeStyle='#ffffff55';ctx.lineWidth=1;ctx.strokeRect(195,y,250*r.count/max,12);}text(ctx,r.count,508,y+12,12,'#e0eafb','right');}); + rows.forEach((r,i)=>{const y=20+i*29; text(ctx,shorten(r.label,25),5,y+12,12,p.text);ctx.fillStyle=p.track;ctx.fillRect(195,y,250,12);ctx.fillStyle=r.color||colors[i%colors.length];ctx.fillRect(195,y,250*r.count/max,12);if(r.color){ctx.strokeStyle=p.stroke;ctx.lineWidth=1;ctx.strokeRect(195,y,250*r.count/max,12);}text(ctx,r.count,508,y+12,12,p.value,'right');}); } const viewport=node('div',null,'stat-canvas-frame');viewport.append(canvas);box.append(viewport); const detail=node('details',null,'chart-values');detail.append(node('summary','查看完整数据')); diff --git a/public/stats-page.js b/public/stats-page.js index c4b2455..6f0cabc 100644 --- a/public/stats-page.js +++ b/public/stats-page.js @@ -12,6 +12,9 @@ const heatmap = initHeatmap(document.getElementById('heatmapHost'), syncUrl); let revision = 0; let heatRevision = 0; let detailReady = false, heatReady = false; +// 图表是画布画的,换主题得按新配色重画一遍(数据没变,直接用上一次的响应) +let lastAnalytics = null; +document.addEventListener('mm-theme-change', () => { if (lastAnalytics) analytics.render(lastAnalytics); }); const saveButton = document.getElementById('saveStatsImage'); const updateSave = () => { saveButton.disabled = !detailReady || !heatReady; }; saveButton.addEventListener('click', async () => { @@ -51,6 +54,7 @@ async function refresh(includeHeat = true, background = false) { if (platform.value) params.platform = platform.value; const detail = get('/api/stats', params).then(body => { if (current !== revision) return; + lastAnalytics = body.analytics; analytics.render(body.analytics); renderToday(body.today); detailReady = true; updateSave(); diff --git a/public/stats.html b/public/stats.html index 173686e..4784c9e 100644 --- a/public/stats.html +++ b/public/stats.html @@ -3,19 +3,21 @@ - + + Algorithm DX · 练习数据 - - + + - + +
      @@ -40,5 +46,7 @@
      +TOP + diff --git a/public/theme-dark.css b/public/theme-dark.css new file mode 100644 index 0000000..a2621fb --- /dev/null +++ b/public/theme-dark.css @@ -0,0 +1,383 @@ +/* Algorithm DX · 深色模式 + * + * 只在 时生效,作为 theme.css 之后的一层「同结构换材质」: + * 选择器都带 [data-mm-theme="dark"] 前缀,权重高于 theme.css 的 body.mm-theme 规则, + * 所以浅色那一套一个字节都不用改。 + * + * 背景同样是官方那份动态背景,只是换成夜版贴图:theme.js 给 iframe 发 + * postMessage({mmTheme:'dark'}),bg.html 挂上 theme-night.css(见那两个文件)。 + * + * 底色沿用夜景:深靛蓝面板 + 官方那套紫/粉横条与 14px 硬阴影;卡片、输入框、气泡、 + * 表格、说明卡全部转深色,正文色跟着 token 一起翻。 + * + * 不翻的两类「成品屏」:段位卡/题库卡的卡面(styles.css 里写死的深色渐变)和点开后的 + * 成绩单(.dx-result / .dan-result-*)—— 它们本来就是一张画好的屏,明暗两版都保留自己的脸, + * 翻成夜版反而两不像;外壳(弹窗遮罩、关闭钮)照常跟主题。 + */ + +/* ============ 1. 调色 ============ */ + +[data-mm-theme="dark"] { + color-scheme: dark; + + /* 夜景里直接用的几档色(与 assets/night/sky.png 同源) */ + --mm-night: #101538; + --mm-night-plate: #1b2148; /* 顶栏胶囊 */ + --mm-night-surface: #191e42; /* 大盒子 */ + --mm-night-surface-2: #1d2348; /* 盒子里的块、输入框 */ + --mm-night-surface-3: #232a55; /* 再抬一层:hover、选中 */ + --mm-night-line: rgba(255, 255, 255, .12); + --mm-night-line-soft: rgba(255, 255, 255, .08); + --mm-night-ink: #eceefb; + --mm-night-dim: #c6cbe8; + --mm-night-muted: #9aa0c4; + + /* 官方调色在深色底上的几处调整 */ + --mm-navy: #e3e7ff; /* 这个 token 在本站当「标题色」用 */ + --mm-ink: #eceefb; + --mm-grey: rgba(255, 255, 255, .18); + --mm-purple-tint: #231f4c; + --mm-purple-deep: #d3a1ff; + /* 只改硬阴影的颜色,偏移留给 theme.css 里那三档 vw 阶梯 */ + --mm-card-shadow-color: rgba(0, 0, 0, .45); + --mm-brand-mark: url(/algorithm-dx-icon.svg); + --mm-nav-shadow: 10px 10px 0 rgba(0, 0, 0, .45); + --mm-soft: 0 10px 26px -14px rgba(0, 0, 0, .65); + --mm-gradient: linear-gradient(0deg, #1a1f45 6%, #1d2150 16%, #262a63 30%, #332c72 46%, + #402f80 56%, #453a8f 67%, #2a3a74 85%, #151f47 95%); + + /* 仓库既有 token 一并翻过来(styles.css:131-154 那套深色语义在这里复活) */ + --bg: var(--mm-night); + --bg-soft: #141a3c; + --panel: var(--mm-night-surface); + --panel-2: var(--mm-night-surface-2); + --border: var(--mm-night-line); + --border-soft: var(--mm-night-line-soft); + --text: var(--mm-night-ink); + --text-dim: var(--mm-night-dim); + --muted: var(--mm-night-muted); + --accent: var(--mm-purple-soft); + --accent-soft: rgba(186, 94, 234, .18); + --ac: #58e08a; + --ac-soft: rgba(75, 224, 112, .16); + --warn: #ffc266; + --warn-soft: rgba(255, 151, 42, .16); + --bad: #ff7d92; + --bad-soft: rgba(231, 33, 57, .18); + --shadow: 8px 8px 0 0 rgba(0, 0, 0, .45); + --surface-gradient: var(--mm-night-surface-2); + --focus-ring: var(--mm-purple-soft); +} + +html[data-mm-theme="dark"], +[data-mm-theme="dark"] body { background: var(--mm-night); } + +[data-mm-theme="dark"] body.mm-theme { + background: var(--mm-night); + background-image: var(--mm-gradient); + color: var(--mm-night-ink); +} + +[data-mm-theme="dark"] body.mm-theme ::selection { background: var(--mm-purple); color: #fff; } + +/* ============ 2. 顶栏与开关 ============ */ + +[data-mm-theme="dark"] body.mm-theme .site-header { + background: var(--mm-night-plate); + border-color: var(--mm-night-line-soft); + box-shadow: var(--mm-nav-shadow); +} +[data-mm-theme="dark"] body.mm-theme .site-header .brand, +[data-mm-theme="dark"] body.mm-theme .site-header .brand-text h1 { color: var(--mm-night-ink); } +[data-mm-theme="dark"] body.mm-theme .site-header .nav .btn { color: var(--mm-night-dim); } +[data-mm-theme="dark"] body.mm-theme .site-header .nav .btn:hover { color: var(--mm-purple-soft); } +[data-mm-theme="dark"] body.mm-theme .site-header .global-sync-bar { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line-soft); +} + +[data-mm-theme="dark"] .mm-menu-btn { background: var(--mm-night-plate); } +[data-mm-theme="dark"] .mm-menu-btn span { background: var(--mm-pink); } +[data-mm-theme="dark"] body.mm-menu-open .mm-menu-btn { background: var(--mm-purple); border-color: var(--mm-purple); } +[data-mm-theme="dark"] body.mm-menu-open .mm-menu-btn span { background: #fff; } +@media (max-width: 1000px) { + [data-mm-theme="dark"] body.mm-theme .site-header .nav { background: var(--mm-night); } + [data-mm-theme="dark"] body.mm-theme .site-header .nav .btn:not(:last-child) { border-bottom-color: var(--mm-night-line); } +} + +/* 开关本身:深色下换成太阳,底色转深 */ +[data-mm-theme="dark"] body.mm-theme .mm-theme-btn { + border-color: var(--mm-purple-soft); + background: var(--mm-night-surface-3); + box-shadow: 4px 4px 0 rgba(0, 0, 0, .45); +} +[data-mm-theme="dark"] body.mm-theme .mm-theme-btn__moon { opacity: 0; transform: scale(.4) rotate(90deg); } +[data-mm-theme="dark"] body.mm-theme .mm-theme-btn__sun { opacity: 1; transform: none; } +[data-mm-theme="dark"] body.mm-theme .mm-theme-btn:hover { background: var(--mm-purple); } +[data-mm-theme="dark"] body.mm-theme .mm-theme-btn:hover .mm-theme-btn__sun { fill: #fff; } + +/* ============ 3. 文字:白色描边换成暗色投影 ============ */ + +[data-mm-theme="dark"] body.mm-theme .ac-page-heading h2, +[data-mm-theme="dark"] body.mm-theme .stats-toolbar h2, +[data-mm-theme="dark"] body.mm-theme .circle-hero h2, +[data-mm-theme="dark"] body.mm-theme .dan-hero h2, +[data-mm-theme="dark"] body.mm-theme .help-heading h2, +[data-mm-theme="dark"] body.mm-theme .dx-page-intro h2, +[data-mm-theme="dark"] body.mm-theme .circle-feed-head h2, +[data-mm-theme="dark"] body.mm-theme .today-section-title { + color: var(--mm-navy); + text-shadow: 0 2px 8px rgba(0, 0, 0, .55); +} +[data-mm-theme="dark"] body.mm-theme .analytics-note, +[data-mm-theme="dark"] body.mm-theme #message, +[data-mm-theme="dark"] body.mm-theme .dan-msg, +[data-mm-theme="dark"] body.mm-theme .dan-disclaimer, +[data-mm-theme="dark"] body.mm-theme .circle-note, +[data-mm-theme="dark"] body.mm-theme .pager-hint { + color: var(--mm-night-dim); + text-shadow: 0 1px 4px rgba(0, 0, 0, .6); +} +[data-mm-theme="dark"] body.mm-theme .help-page > p { + color: var(--mm-night-dim); + text-shadow: 0 1px 4px rgba(0, 0, 0, .6); +} +[data-mm-theme="dark"] body.mm-theme .help-page .help-heading p, +[data-mm-theme="dark"] body.mm-theme .help-page > section p, +[data-mm-theme="dark"] body.mm-theme .help-page > section li, +[data-mm-theme="dark"] body.mm-theme .help-page details p { color: var(--mm-night-dim); } +[data-mm-theme="dark"] body.mm-theme .circle-hero p, +[data-mm-theme="dark"] body.mm-theme .ac-page-heading p, +[data-mm-theme="dark"] body.mm-theme .stats-toolbar p, +[data-mm-theme="dark"] body.mm-theme .dx-page-intro p { color: var(--mm-night-muted); } +/* 数据条里两个写死的深色数字(§9) */ +[data-mm-theme="dark"] body.mm-theme .stat.is-ac .stat-value { color: #5fe08f; } +[data-mm-theme="dark"] body.mm-theme .stat.is-unac .stat-value { color: #ffc266; } + +/* ============ 4. 玻璃板(页头/工具条/侧栏这些浮在背景上的块) ============ */ + +[data-mm-theme="dark"] body.mm-theme .ac-page-heading, +[data-mm-theme="dark"] body.mm-theme .stats-toolbar, +[data-mm-theme="dark"] body.mm-theme .circle-hero, +[data-mm-theme="dark"] body.mm-theme .dan-hero, +[data-mm-theme="dark"] body.mm-theme .help-heading, +[data-mm-theme="dark"] body.mm-theme .help-toc, +[data-mm-theme="dark"] body.mm-theme .pager, +[data-mm-theme="dark"] body.mm-theme .dx-page-intro, +[data-mm-theme="dark"] body.mm-theme .circle-feed-head { + background: rgba(19, 24, 52, .88); + border: 1px solid var(--mm-night-line); + box-shadow: 0 18px 40px -30px rgba(0, 0, 0, .8); +} +@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) { + [data-mm-theme="dark"] body.mm-theme .ac-page-heading, + [data-mm-theme="dark"] body.mm-theme .stats-toolbar, + [data-mm-theme="dark"] body.mm-theme .circle-hero, + [data-mm-theme="dark"] body.mm-theme .dan-hero, + [data-mm-theme="dark"] body.mm-theme .help-heading, + [data-mm-theme="dark"] body.mm-theme .help-toc, + [data-mm-theme="dark"] body.mm-theme .pager, + [data-mm-theme="dark"] body.mm-theme .dx-page-intro, + [data-mm-theme="dark"] body.mm-theme .circle-feed-head { + background: rgba(19, 24, 52, .88); + -webkit-backdrop-filter: blur(12px) saturate(1.2); + backdrop-filter: blur(12px) saturate(1.2); + } +} + +/* 落在背景花纹上的那几处小字:浅色层给的是白底板,这里换成夜色底板 */ +[data-mm-theme="dark"] body.mm-theme .app > .analytics-note, +[data-mm-theme="dark"] body.mm-theme .app > #message, +[data-mm-theme="dark"] body.mm-theme .app > .dan-msg, +[data-mm-theme="dark"] body.mm-theme .circle-main > #message, +[data-mm-theme="dark"] body.mm-theme .heatmap-top > .analytics-note, +[data-mm-theme="dark"] body.mm-theme .help-page > p { + background: var(--mm-night-surface-2); + box-shadow: 0 8px 20px -14px rgba(0, 0, 0, .9); + text-shadow: none; +} + +/* ============ 5. 官方盒子与通用块 ============ */ + +[data-mm-theme="dark"] body.mm-theme .filters, +[data-mm-theme="dark"] body.mm-theme .admin, +[data-mm-theme="dark"] body.mm-theme .foot, +[data-mm-theme="dark"] body.mm-theme .section, +[data-mm-theme="dark"] body.mm-theme .stat-row, +[data-mm-theme="dark"] body.mm-theme .ac-platform-strip, +[data-mm-theme="dark"] body.mm-theme .placeholder, +[data-mm-theme="dark"] body.mm-theme .empty, +[data-mm-theme="dark"] body.mm-theme.ac-page .filters, +[data-mm-theme="dark"] body.mm-theme .analytics, +[data-mm-theme="dark"] body.mm-theme .today-panel, +[data-mm-theme="dark"] body.mm-theme .dx-timer, +[data-mm-theme="dark"] body.mm-theme .circle-sidebar, +[data-mm-theme="dark"] body.mm-theme .circle-filters, +[data-mm-theme="dark"] body.mm-theme .dx-rules { + background: var(--mm-night-surface); + color: var(--mm-night-ink); +} +[data-mm-theme="dark"] body.mm-theme .analytics-chart, +[data-mm-theme="dark"] body.mm-theme .today-card, +[data-mm-theme="dark"] body.mm-theme .dx-pages, +[data-mm-theme="dark"] body.mm-theme .dx-records-filters, +[data-mm-theme="dark"] body.mm-theme .circle-card, +[data-mm-theme="dark"] body.mm-theme .help-page > section { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line-soft); + color: var(--mm-night-ink); +} +/* 这块左侧有官方绿条,别被上面的 border-color 一起抹掉 */ +[data-mm-theme="dark"] body.mm-theme .analytics-metrics > div { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line-soft); + border-left: 4px solid var(--mm-green); + color: var(--mm-night-ink); +} +/* 空格子原来和面板几乎同色(#1b2145 对 #191e42),整年没练的日子直接看不见, + 图例最短的一格也是空的:抬亮一点,靠白描边分层 */ +[data-mm-theme="dark"] body.mm-theme .heat-cell { border-color: rgba(255, 255, 255, .14); } +[data-mm-theme="dark"] body.mm-theme .level-0 { background: rgba(255, 255, 255, .09); } +[data-mm-theme="dark"] body.mm-theme .level-1 { background: #1d4a37; } +[data-mm-theme="dark"] body.mm-theme .level-2 { background: #256f4a; } +[data-mm-theme="dark"] body.mm-theme .level-3 { background: #2f9d62; } +[data-mm-theme="dark"] body.mm-theme .level-4 { background: var(--mm-green); } + +[data-mm-theme="dark"] body.mm-theme .foot-note { color: var(--mm-night-muted); } +[data-mm-theme="dark"] body.mm-theme.ac-page .card:hover { background: var(--mm-night-surface-3); } +[data-mm-theme="dark"] body.mm-theme .ac-page .card-stats .k { color: var(--mm-night-muted); } +[data-mm-theme="dark"] body.mm-theme .ac-page .card-stats .v { color: var(--mm-night-ink); } +[data-mm-theme="dark"] body.mm-theme .dx-card.is-empty { color: var(--mm-night-muted); } +[data-mm-theme="dark"] body.mm-theme .dx-more a { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line); + color: var(--mm-navy); +} +[data-mm-theme="dark"] body.mm-theme .dx-ranked, +[data-mm-theme="dark"] body.mm-theme .dx-rank, +[data-mm-theme="dark"] body.mm-theme .today-total { color: #7ef0a4; } + +/* ============ 6. 控件:输入、按钮、气泡、表格、弹窗 ============ */ + +[data-mm-theme="dark"] body.mm-theme .input, +[data-mm-theme="dark"] body.mm-theme .select { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line); + color: var(--mm-night-ink); + color-scheme: dark; +} +[data-mm-theme="dark"] body.mm-theme .input::placeholder { color: #8f94b4; } +[data-mm-theme="dark"] body.mm-theme .analytics-controls .input { color: var(--mm-night-ink); color-scheme: dark; } +[data-mm-theme="dark"] body.mm-theme .btn-ghost { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line); + color: var(--mm-night-dim); +} +[data-mm-theme="dark"] body.mm-theme .btn-ghost:hover { background: var(--mm-night-surface-3); color: var(--mm-navy); } +[data-mm-theme="dark"] body.mm-theme .chip { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line); + color: var(--mm-night-dim); +} +/* 浅色里这组是「淡紫底 + 白药丸」,深色照搬会变成「紫底 + 深蓝药丸」,看着像反的。 + 夜里的对应关系是「比盒子更深一层的底 + 比底更亮的药丸」 */ +[data-mm-theme="dark"] body.mm-theme .tabs { background: #151a3c; } +[data-mm-theme="dark"] body.mm-theme .tab.is-active { background: #262d5c; color: var(--mm-navy); box-shadow: 0 2px 0 rgba(0, 0, 0, .35); } +[data-mm-theme="dark"] body.mm-theme .db-chip { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line); + color: #a8e6c8; +} +[data-mm-theme="dark"] body.mm-theme .ac-page .db-chip { color: #a8e6c8; border-color: var(--mm-night-line); } +[data-mm-theme="dark"] body.mm-theme .expand-btn { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line); + color: var(--mm-night-dim); +} +[data-mm-theme="dark"] body.mm-theme .rt-table { background: var(--mm-night-surface-2); } +[data-mm-theme="dark"] body.mm-theme .rt-table th { background: var(--mm-purple-tint); color: var(--mm-navy); } +[data-mm-theme="dark"] body.mm-theme .rt-table td { border-color: var(--mm-night-line-soft); } +[data-mm-theme="dark"] body.mm-theme .modal-card, +[data-mm-theme="dark"] body.mm-theme dialog { + background: var(--mm-night-surface); + border-color: var(--mm-night-line); + color: var(--mm-night-ink); +} +[data-mm-theme="dark"] body.mm-theme dialog::backdrop { background: rgba(6, 9, 26, .62); } +[data-mm-theme="dark"] body.mm-theme .account-owner { background: var(--mm-night-surface-2); } +[data-mm-theme="dark"] body.mm-theme .account-owner-content { background: #1c224a; } +[data-mm-theme="dark"] body.mm-theme .help-inline { background: var(--mm-night-surface-2); border-color: var(--mm-night-line-soft); } +[data-mm-theme="dark"] body.mm-theme .help-inline summary { color: var(--mm-navy); } +[data-mm-theme="dark"] body.mm-theme .account-owner-avatar { background: var(--mm-purple-tint); color: var(--mm-purple-soft); } + +/* 气泡:底色转深,字色跟着暗色不刺眼 */ +[data-mm-theme="dark"] body.mm-theme .badge, +[data-mm-theme="dark"] body.mm-theme .ac-card-tags .badge { background: rgba(255, 255, 255, .1); color: #cdd3f2; border-color: transparent; } +[data-mm-theme="dark"] body.mm-theme .badge.ac { background: rgba(75, 224, 112, .16); color: #7ef0a4; } +[data-mm-theme="dark"] body.mm-theme .badge.warn { background: rgba(255, 151, 42, .16); color: #ffc266; } +[data-mm-theme="dark"] body.mm-theme .badge.bad { background: rgba(231, 33, 57, .18); color: #ff93a5; } +[data-mm-theme="dark"] body.mm-theme .badge.diff { background: rgba(53, 132, 254, .18); color: #9cc6ff; } +[data-mm-theme="dark"] body.mm-theme .badge.contest { background: rgba(186, 94, 234, .18); color: #d7aeff; } +[data-mm-theme="dark"] body.mm-theme .badge.platform { background: rgba(2, 200, 211, .16); color: #7fe6ee; } +[data-mm-theme="dark"] body.mm-theme .badge.score { background: rgba(255, 151, 42, .16); color: #ffc266; } + +/* 提示条:浅色那版是实心暖白底,所以深色也必须是实心底 —— 只铺一层 10% 的色纱的话, + 夜里的云和彩虹会直接透到字缝里(这块正好压在彩虹上)。色纱按同一个比例调进实心底里, + 颜色关系不变,字下面有底。 */ +[data-mm-theme="dark"] body.mm-theme .notice { background: #2e283c; color: #f0dcb4; } +[data-mm-theme="dark"] body.mm-theme .notice strong { color: #ffd79a; } +[data-mm-theme="dark"] body.mm-theme .notice-warn { background: #2e283c; color: #ecd9a4; } +[data-mm-theme="dark"] body.mm-theme .notice-warn strong { color: #f6dfa8; } +[data-mm-theme="dark"] body.mm-theme .notice-warn span { color: #d9c48d; } +[data-mm-theme="dark"] body.mm-theme .notice-ac { background: #1c2f43; color: #b6e3bc; } +[data-mm-theme="dark"] body.mm-theme .notice-ac strong { color: #d6f0da; } +[data-mm-theme="dark"] body.mm-theme .notice-ac.is-bad, +[data-mm-theme="dark"] body.mm-theme .notice-danger { background: #301c3d; color: #ffb9b4; } +[data-mm-theme="dark"] body.mm-theme .notice-ac.is-bad strong, +[data-mm-theme="dark"] body.mm-theme .notice-danger strong { color: #ff9d97; } +[data-mm-theme="dark"] body.mm-theme .notice-neutral { background: #25294a; color: var(--mm-night-muted); } + +/* ============ 7. 题友圈 ============ */ + +[data-mm-theme="dark"] body.mm-theme .circle-eyebrow { color: var(--mm-purple-soft); } +[data-mm-theme="dark"] body.mm-theme .circle-person:hover { background: var(--mm-night-surface-3); } +[data-mm-theme="dark"] body.mm-theme .circle-person[aria-pressed="true"] { + background: rgba(186, 94, 234, .22); + border-color: var(--mm-purple-soft); + color: #e3c6ff; +} +[data-mm-theme="dark"] body.mm-theme .circle-avatar { + background: hsl(var(--hue, 160) 34% 26%); + color: hsl(var(--hue, 160) 70% 84%); +} +[data-mm-theme="dark"] body.mm-theme .circle-result { background: rgba(255, 255, 255, .08); color: #c3c9e6; } +[data-mm-theme="dark"] body.mm-theme .circle-result.ac { background: rgba(75, 224, 112, .14); color: #7ef0a4; } +[data-mm-theme="dark"] body.mm-theme .circle-coverage-status.needs-attention { + background: rgba(255, 151, 42, .14); + color: #ffc266; +} +[data-mm-theme="dark"] body.mm-theme .circle-card-head, +[data-mm-theme="dark"] body.mm-theme .circle-card-foot { border-color: var(--mm-night-line-soft); } + +/* ============ 8. 使用指南 ============ */ + +[data-mm-theme="dark"] body.mm-theme .help-page .help-toc a { + background: var(--mm-night-surface-2); + border-color: var(--mm-night-line); + color: var(--mm-navy); +} +[data-mm-theme="dark"] body.mm-theme .help-page .help-toc a:hover { + background: var(--mm-purple-tint); + border-color: var(--mm-purple-soft); + color: #e3c6ff; +} +[data-mm-theme="dark"] body.mm-theme kbd { + background: var(--mm-purple-tint); + border-color: var(--mm-purple-soft); + color: var(--mm-navy); +} + +/* ============ 9. 底图之外还需要单独一提的两处 ============ */ + +/* 背景 iframe 之上、正文之下的兜底层,深色下别闪白 */ +[data-mm-theme="dark"] body.mm-theme .app { position: relative; } \ No newline at end of file diff --git a/public/theme-night.css b/public/theme-night.css new file mode 100644 index 0000000..dfe6618 --- /dev/null +++ b/public/theme-night.css @@ -0,0 +1,81 @@ +/* ============ 夜版背景 ============ + * + * 结构、层级、尺寸、动画全部沿用官方 style.css,这里只做一件事:换贴图素材 + * (全部在 /assets/night/,由官方素材重上色生成,官方文件一个没动)。 + * + * 用哪一层: + * sky / sky-frame 天空渐变(内层渐变块 / 圆角外那圈) + * pattern 渐变上的图案叠加 + * shines 闪点(官方在 ≥769px 换成 PC 那张,这里跟着换) + * cloud-* 六片云(白体换成夜蓝,原来的粉/橙暖色描边按原色留着) + * rainbow-back 彩虹后面那片云 + * rainbow-bottom 彩虹两端那两片云(同样分 SP / PC 两份) + * rainbow 彩虹(降成月虹) + * aurora 曙光(配合 screen 叠加 + 模糊化成柔光) + * 月亮和流星保持官方素材:夜里它们本来就该是最亮的。 + * + * **带 -sp / -pc 的素材必须跟着官方的断点走**:官方在基础规则里用 `_sp`、在 + * `@media (min-width:769px)` 里换成 `_pc`。只挂一份的话,宽屏会拿手机那张顶 PC 那格 —— + * 彩虹两端那片云的 SP 版是被图片边界切平的窄裁版(左右两边 85% / 44% 的像素是实心的), + * 铺进 PC 那格就会在画面中间露出两条平直的切边。下面每个断点都跟官方一一对应。 + * + * 换素材之外还有四处按夜版需要动了样式,都在下面并注明原因: + * html/body 底色、彩虹底边淡出、曙光的叠加方式与柔光、月亮那圈月光。 + */ + +/* iframe 底色(渐变块圆角之外露出的那一圈由 .bg 的贴图负责) */ +html, body { background: #0e1234; } + +/* —— 天空 —— */ +.bg { background-image: url(/assets/night/sky-frame.png); } +.bg--gradient { background-image: url(/assets/night/sky.png); } +.bg--gradient:before { background-image: url(/assets/night/pattern.png); } + +/* —— 手机档(官方基础规则那套素材)—— */ +.bg--shine { background-image: url(/assets/night/shines-sp.png); } +.bg--aurora, +.bg--aurora:before, +.bg--aurora:after { background-image: url(/assets/night/aurora-sp.png); } +.bg--rainbow:before { background-image: url(/assets/night/rainbow-sp.png); } +.bg--rainbow__cloud { background-image: url(/assets/night/rainbow-bottom-sp.png); } +.bg--rainbow__cloud:after { background-image: url(/assets/night/rainbow-back-sp.png); } + +/* —— 云(两张云官方不分档,同一张素材)—— */ +.bg--cloud__item.i1 { background-image: url(/assets/night/cloud-back-l.png); } +.bg--cloud__item.i2 { background-image: url(/assets/night/cloud-back-r.png); } +.bg--cloud__item.i3 { background-image: url(/assets/night/cloud-back-c.png); } +.bg--cloud__item.i4 { background-image: url(/assets/night/cloud-front-l.png); } +.bg--cloud__item.i5 { background-image: url(/assets/night/cloud-front-r.png); } +.bg--cloud__item.i6 { background-image: url(/assets/night/cloud-front-c.png); } + +/* —— 宽屏档:官方在这里把闪点、曙光、彩虹、彩虹两端那片云都换成 PC 素材 —— + 曙光的 :before/:after 官方在宽屏没有换图(还是 SP 那张),所以它们只在上面那一档里换。 */ +@media only screen and (min-width:769px) { + .bg--shine { background-image: url(/assets/night/shines-pc.png); } + .bg--aurora { background-image: url(/assets/night/aurora-pc.png); } + .bg--rainbow:before { background-image: url(/assets/night/rainbow-pc.png); } + .bg--rainbow__cloud { background-image: url(/assets/night/rainbow-bottom-pc.png); } +} + +/* —— 彩虹:弧贴近地平线处淡出 —— + 官方那张彩虹贴图的两个「脚」是被图片边界切平的。白天那里正好是渐变底部的白色地平光, + 切边看不出来;夜里背景压深之后,平直的切边就露出来了,所以让弧在贴近地平线处淡出。 + 两端那片云的底边同理,那一刀直接做在 rainbow-bottom-pc 素材里(见生成脚本)。 */ +.bg--rainbow:before { + -webkit-mask-image: linear-gradient(0deg, transparent 0, #000 13%); + mask-image: linear-gradient(0deg, transparent 0, #000 13%); +} + +/* —— 曙光:换素材之外还要叠加方式,这两行没法省 —— + 官方那几道竖向光幕在深色天空上会显出硬边方块,所以夜里用 screen 只加光、再模糊成柔光; + 贴图两端本来是硬接缝(官方自己在浅色天空上被背景亮度盖住了),夜版素材两端各淡了 4%。 */ +.bg--aurora, +.bg--aurora:before, +.bg--aurora:after { + mix-blend-mode: screen; + filter: blur(1.1vw) saturate(1.2) brightness(.95); +} +.bg--aurora { opacity: .32; } + +/* —— 月亮:素材保持官方,这里只加一圈月光(夜里唯一需要「加光」的地方)—— */ +.bg--moon { filter: drop-shadow(0 0 2.6vw rgba(168, 205, 255, .5)); } \ No newline at end of file diff --git a/public/theme.css b/public/theme.css new file mode 100644 index 0000000..50582a7 --- /dev/null +++ b/public/theme.css @@ -0,0 +1,1482 @@ +/* Algorithm DX · 前端皮肤层:maimai DX 国际版官网(PRiSM PLUS 时期) + * + * 参数来源:`public/vendor/maimai/style.css`(2025-08-04 归档快照里的 + * `https://maimai.sega.com/assets/css/style.css`)。每条关键数值后面标了官方选择器, + * 以后要调整就照着官方那行改。 + * + * 载入顺序:fonts.css → styles.css → 各页自己的 CSS(circle/dx-result/dan…)→ ui.css + * → theme.css → theme-dark.css。所以在同权重下本文件覆盖前面几个; + * 少数带 #id 或 .ac-page 前缀的旧规则用 `body.mm-theme` 提权重压过去。 + * + * 深色模式是叠加在后面的 theme-dark.css:里面的规则都带 [data-mm-theme="dark"] 前缀, + * 只覆盖材质与配色,改浅色那套不用动这个文件。 + * + * 背景整页复用官网那份动态背景,见 public/bg.html。 + * + * 与官方的八处有意偏差,见文件末尾「与官方的偏差」。 + */ + +/* ============ 1. 调色与尺度(数值全部取自官方) ============ */ + +:root { + color-scheme: light; + font-synthesis: none; + + --mm-purple: #ba5eea; /* .common--box:before、.header--nav__link.on::before */ + --mm-purple-deep: #a051dc; + --mm-purple-soft: #d3a1ff; + --mm-purple-tint: #f6ecff; /* 由主紫派生的浅底,官方只有纯色,用于表格/内嵌块 */ + --mm-pink: #ffcedf; /* .common--box:after */ + --mm-pink-soft: #ffd5cf; /* .bg--gradient */ + --mm-pink-hot: #ff699d; /* .header--nav__language__link:hover */ + --mm-magenta: #dc39b8; + --mm-red: #e72139; /* .common--btn */ + --mm-red-dark: #a51426; /* .common--btn 的 3D 底影 */ + --mm-navy: #0b3871; /* .footer--bottom color、导航字形 fill */ + --mm-ink: #000; /* body,html color */ + --mm-grey: #e5e5e5; /* .header--nav__item:not(:last-child)::after */ + --mm-green: #4be070; + --mm-cyan: #02c8d3; + --mm-blue: #3584fe; + --mm-orange: #ff972a; + --mm-yellow: #f8b73d; + + /* 背景渐变:官方 .bg--gradient 的 linear-gradient(0deg,#fff 14%,…) 原样搬过来 */ + --mm-gradient: linear-gradient(0deg, #fff 14%, #ffd5cf 24%, #ffd5cf 46%, #ffc5d5 56%, + #eaabff 67%, #72bcfe 85%, #65f2df 95%); + + /* 卡片:官方 .common--box 是三档 vw 阶梯,下面按档照搬(默认这档就是官方 @min-width:1100px + 的 900px / 30px / 14px 14px 0 / 600×8),另两档写在后面的媒体查询里 */ + --mm-card-radius: 30px; + /* 阴影拆成「偏移」与「颜色」两段:窄屏只改偏移(官方那三档),深色只改颜色 */ + --mm-card-shadow-x: 14px; + --mm-card-shadow-color: rgba(0, 0, 0, .2); + --mm-card-shadow: var(--mm-card-shadow-x) var(--mm-card-shadow-x) 0 0 var(--mm-card-shadow-color); + --mm-bar-w: min(66.6667%, 600px); + --mm-bar-h: 8px; + + /* 品牌图标:浅色用浅色版,深色版在 theme-dark.css 里换掉(首帧前由 上的主题决定, + 所以不会先亮后暗) */ + --mm-brand-mark: url(/algorithm-dx-icon-light.svg); + + /* 顶栏胶囊:官方 .header--nav @min-width:1400px → 1200×100 / 50px / 10px 10px 0 */ + --mm-nav-h: clamp(64px, 7.1429vw, 100px); + --mm-nav-radius: 50px; + --mm-nav-shadow: 10px 10px 0 rgba(0, 0, 0, .2); + /* 版心:官方顶栏 1200px、内容盒 900px。顶栏保持 1200,正文收窄一点(见「偏差」) */ + --mm-shell: min(1200px, 91.4286vw); + --mm-content: min(1080px, 91.4286vw); + /* 区块之间的呼吸:官方盒子内部 padding 是 50px,这里按同一量级放宽 */ + --mm-gap: clamp(26px, 2.8vw, 44px); + --mm-pad: clamp(20px, 2.8vw, 40px); + /* 缓动:UI 交互统一用一条强 ease-out(技能库给的曲线) */ + --mm-ease-out: cubic-bezier(.23, 1, .32, 1); + + /* 字体:官方 `body,html{font-family:"M PLUS 1p",sans-serif}` */ + --mm-sans: "M PLUS 1p Symbols", "M PLUS 1p", "Segoe UI", "Hiragino Kaku Gothic Pro", "Yu Gothic", + "Microsoft YaHei", "PingFang SC", "Noto Sans SC", sans-serif; + --mm-mono: "M PLUS 1p Symbols", "M PLUS 1p", ui-monospace, SFMono-Regular, Consolas, monospace; + + /* —— 仓库既有 token 的重映射:暗色 → 官方亮色(styles.css:131-154、ui.css:2-6) —— */ + --bg: #fff; + --bg-soft: #fffafd; + --panel: #fff; + --panel-2: #fbf3ff; + --border: var(--mm-grey); + --border-soft: #efe7f7; + --text: var(--mm-ink); + --text-dim: var(--mm-navy); + --muted: #6f6a7a; + --accent: var(--mm-purple); + --accent-soft: #f3e6ff; + --ac: #16883f; + --ac-soft: #e6fbe9; + --warn: #a35c00; + --warn-soft: #fff3e2; + --bad: #b3122a; + --bad-soft: #ffe9ec; + --radius: var(--mm-card-radius); + --radius-sm: 16px; + --shadow: 8px 8px 0 0 rgba(0, 0, 0, .2); + /* 小东西(行、内嵌块)用柔和投影,硬阴影只留给官方大盒子 */ + --mm-soft: 0 10px 26px -14px rgba(11, 56, 113, .35); + --surface-gradient: #fff; + --focus-ring: var(--mm-purple); + --sans: var(--mm-sans); + --mono: var(--mm-mono); +} + +/* —— 官方 .common--box 的另外两档(原样取自 style.css,数值一个没改)—— + :root 里那档是 ≥1100px;下面两档在窄屏把圆角、硬阴影与横条一起缩小。 + 少了它们,手机上会一直用 30px 圆角 + 14px 阴影,比官方重得多。 */ +@media only screen and (max-width: 768.98px) { + :root { + --mm-card-radius: 3.75vw; + --mm-card-shadow-x: 1.75vw; + --mm-bar-w: 75vw; + --mm-bar-h: 1.5vw; + } +} +@media only screen and (min-width: 769px) and (max-width: 1099.98px) { + :root { + --mm-card-radius: 2.7272727273vw; + --mm-card-shadow-x: 1.2727272727vw; + --mm-bar-w: 54.5454545455vw; + --mm-bar-h: .7272727273vw; + } +} + +/* ============ 2. 全局 ============ */ + +html, body { background: #fff; } + +body.mm-theme { + background: #fff; + color: var(--mm-ink); + font-family: var(--mm-sans); + /* 官方 body 是 line-height:1(lib.css 的重置),这里保留可读行高(见「偏差」) */ + overflow-x: hidden; +} + +body.mm-theme a { color: var(--mm-purple-deep); } +body.mm-theme a:hover { color: var(--mm-pink-hot); } + +body.mm-theme ::selection { background: var(--mm-purple-soft); color: var(--mm-navy); } + +/* 背景:官网那份动态背景整页复用(./bg.html), + 官方 CSS 里的全局选择器关在 iframe 里,不会碰到本站布局。 + body 自己只留一层兜底渐变,iframe 没加载出来时不至于白屏。 */ +body.mm-theme { + background-image: var(--mm-gradient); + background-attachment: fixed; + background-size: cover; +} + +.mm-bg-frame { + position: fixed; + inset: 0; + z-index: 0; + width: 100%; + height: 100%; + border: 0; + pointer-events: none; +} + +@media (prefers-reduced-motion: reduce) { + .mm-bg-frame { display: none; } +} + +/* 官方 .wrapper{position:relative;z-index:2}:内容压在背景层之上 */ +body.mm-theme .site-header, +body.mm-theme .app { position: relative; z-index: 2; } + +/* ============ 3. 顶栏(官方 .header--nav 桌面胶囊 / .header--btn 移动汉堡) ============ */ + +body.mm-theme .site-header { + display: flex; + align-items: center; + gap: 18px; + max-width: var(--mm-shell); + /* 顶栏往下挪一档:官方背景是内缩 3.14vw 的圆角渐变块,顶栏压到渐变开始线以下, + 上面这一段就露出动态背景 */ + margin: clamp(26px, 3.8vw, 58px) auto 0; + min-height: var(--mm-nav-h); + padding: 0 clamp(14px, 1.7143vw, 24px) 0 clamp(18px, 2.3571428571vw, 33px); + background: #fff; + border: 1px solid transparent; + border-radius: var(--mm-nav-radius); + box-shadow: var(--mm-nav-shadow); +} + +body.mm-theme .site-header .brand { gap: 12px; color: var(--mm-navy); text-decoration: none; } +body.mm-theme .site-header .brand .mark { + display: block; + width: 56px; + height: 34px; + /* 图标走 CSS 背景而不是 :主题在 上,首帧前就定好了, + 换成图片标签的话浅色主题会先闪一下深色版图标 */ + background: var(--mm-brand-mark) center / contain no-repeat; +} +body.mm-theme .site-header .brand-text h1 { + margin: 0; + font-size: 21px; + font-weight: 800; + letter-spacing: .2px; + color: var(--mm-navy); +} +body.mm-theme .site-header .brand-text p { margin: 3px 0 0; font-size: 12px; color: var(--muted); } + +/* 导航项:官方 .header--nav__link(字色 #0b3871、选中态紫色下横条 5px) */ +body.mm-theme .site-header .nav { + justify-self: auto; + margin-left: auto; + display: flex; + align-items: center; + gap: clamp(12px, 1.7143vw, 24px); + padding: 4px 0; + max-width: 100%; + /* 这里不能开 overflow:当前页的紫色下横条画在项框之外,会被裁掉 */ + overflow: visible; +} + +body.mm-theme .site-header .nav .btn, +body.mm-theme .site-header .nav .btn.btn-primary, +body.mm-theme .site-header .nav .btn.btn-ghost { + position: relative; + flex: 0 0 auto; + min-height: 0; + padding: 8px 2px; + border: 0; + border-radius: 0; + background: none; + box-shadow: none; + color: var(--mm-navy); + font-family: var(--mm-sans); + font-size: 17px; + font-weight: 800; + line-height: 1.55; + letter-spacing: .2px; + text-decoration: none; + transition: color .15s; +} +body.mm-theme .site-header .nav .btn:hover { color: var(--mm-purple); transform: none; box-shadow: none; } + +/* 分隔线:官方 .header--nav__item:not(:last-child)::after → 1px × 14px #e5e5e5 */ +body.mm-theme .site-header .nav .btn:not(:last-child)::after { + content: ""; + position: absolute; + top: 50%; + right: calc(-1 * clamp(6px, .8571428571vw, 12px)); + width: 1px; + height: 14px; + background: var(--mm-grey); + transform: translateY(-50%); +} + +/* 当前页 / 悬停:官方 .header--nav__link.on::before → 高 5px、圆角 100px 100px 0 0 */ +body.mm-theme .site-header .nav .btn[aria-current="page"]::before, +body.mm-theme .site-header .nav .btn:hover::before { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: calc((var(--mm-nav-h) - 42px) / -2); + height: 5px; + border-radius: 100px 100px 0 0; + background: var(--mm-purple); +} + +/* 汉堡按钮:官方 .header--btn(白底 + 1.25vw 红边 + 10px 硬阴影),开合时变 .header--nav__btn 的紫底叉 */ +.mm-menu-btn { + display: none; + position: relative; + flex: 0 0 auto; + width: 44px; + height: 44px; + padding: 0; + border: 3px solid var(--mm-red); + border-radius: 100px; + background: #fff; + box-shadow: 4px 4px 0 rgba(0, 0, 0, .2); + cursor: pointer; +} +.mm-menu-btn span { + display: block; + width: 20px; + height: 3px; + margin: 4px auto; + border-radius: 100px; + background: var(--mm-red); + transition: transform .2s, background .2s; +} +body.mm-menu-open .mm-menu-btn { border-color: var(--mm-purple); background: var(--mm-purple); } +body.mm-menu-open .mm-menu-btn span { background: #fff; } +body.mm-menu-open .mm-menu-btn span:first-child { transform: translateY(3.5px) rotate(45deg); } +body.mm-menu-open .mm-menu-btn span:last-child { transform: translateY(-3.5px) rotate(-45deg); } + +/* 窄一点的桌面端:七项中文标签会顶到胶囊右沿,先收字号与间距。 + 1001~1015px 是最挤的一档,收不够就会折成两层(开关掉到第二行),所以这里收得狠一点 */ +@media (max-width: 1200px) and (min-width: 1001px) { + body.mm-theme .site-header { gap: 12px; } + body.mm-theme .site-header .nav { gap: 12px; } + body.mm-theme .site-header .nav .btn, + body.mm-theme .site-header .nav .btn.btn-primary, + body.mm-theme .site-header .nav .btn.btn-ghost { font-size: 15px; } +} + +@media (max-width: 1000px) { + body.mm-theme .site-header { + justify-content: space-between; + padding: 0 clamp(14px, 3vw, 20px); + min-height: 68px; + gap: 10px; + } + .mm-menu-btn { display: block; } + /* 窄屏要同时站下品牌、深色开关和汉堡:品牌收一档,否则 header 会折行成两层 + (开关自己的尺寸在 §16 的媒体查询里,因为那节的规则排在更后面) */ + body.mm-theme .site-header { gap: 12px; } + body.mm-theme .site-header .brand .mark { width: 46px; height: 28px; } + body.mm-theme .site-header .brand-text h1 { font-size: 18px; } + /* 49px 的圆圈才够手指点:两个圆钮各 44px,中间留 12px,别再挤在一起 */ + .mm-menu-btn { width: 44px; height: 44px; } + /* 菜单展开时整屏铺的是 .nav(z-index:40),品牌、开关、汉堡要浮在它上面, + 否则开着菜单就够不到深浅色开关了 */ + body.mm-theme .site-header .brand, + body.mm-theme .site-header .mm-theme-btn, + body.mm-theme .site-header .mm-menu-btn { position: relative; z-index: 41; } + body.mm-theme .site-header .nav { + position: fixed; + inset: 0; + z-index: 40; + display: none; + flex-direction: column; + align-items: stretch; + gap: 0; + margin: 0; + padding: 84px 24px 40px; + overflow: auto; + background: #fff; + box-shadow: var(--mm-nav-shadow); + } + body.mm-menu-open .site-header .nav { display: flex; } + /* 顶栏平时和 .app 同层(都是 z-index:2),展开菜单时必须抬到 .app 之上, + 否则白色面板会被正文盖住 */ + body.mm-menu-open .site-header { z-index: 30; } + /* html 上也锁一份:只锁 body 拦不住 html 那只滚动条,深色面板右上角会露一截浅色滑块。 + 这两条都必须留在媒体查询里 —— 拉宽窗口后菜单按钮已经隐藏,锁还在就谁也解不开了 */ + body.mm-menu-open, + html.mm-menu-open { overflow: hidden; } + body.mm-theme .site-header .nav .btn, + body.mm-theme .site-header .nav .btn.btn-primary, + body.mm-theme .site-header .nav .btn.btn-ghost { + margin: 0; + padding: 16px 4px; + font-size: 20px; + text-align: center; + } + body.mm-theme .site-header .nav .btn::after { display: none; } + body.mm-theme .site-header .nav .btn:not(:last-child) { + border-bottom: 3px solid var(--mm-grey); + } + body.mm-theme .site-header .nav .btn[aria-current="page"]::before, + body.mm-theme .site-header .nav .btn:hover::before { display: none; } + body.mm-theme .site-header .nav .btn[aria-current="page"] { color: var(--mm-purple); } + .mm-menu-btn { position: relative; z-index: 50; } + body.mm-theme .site-header .global-sync-bar { + left: clamp(8px, 2vw, 16px); + right: clamp(8px, 2vw, 16px); + justify-content: space-between; + } + body.mm-theme .site-header .global-sync-bar .auto-sync-status { max-width: none; } +} + +/* 再窄一点(320~380px)品牌、开关、汉堡才挤得下同一行 */ +@media (max-width: 380px) { + body.mm-theme .site-header { padding: 0 10px; } + body.mm-theme .site-header .brand .mark { width: 40px; height: 24px; } + body.mm-theme .site-header .brand-text h1 { font-size: 17px; } +} +/* 到 344px 以下只留图标当品牌,把宽度全让给开关和汉堡 */ +@media (max-width: 344px) { + body.mm-theme .site-header .brand-text { display: none; } +} + +/* 后台同步状态:绑在胶囊右下角外侧(app.js/auto-sync.js 把它塞进
      ), + 不占导航的位置,也不改变胶囊的官方比例 */ +body.mm-theme .site-header .global-sync-bar { + position: absolute; + top: calc(100% + 8px); + right: clamp(6px, 1.4286vw, 16px); + z-index: 6; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + margin: 0; + padding: 3px 5px 3px 14px; + max-width: calc(100% - 28px); + background: #fff; + border-radius: 100px; + box-shadow: 4px 4px 0 rgba(0, 0, 0, .15); +} +body.mm-theme .site-header .global-sync-bar:has(.auto-sync-status[hidden]):has(button[hidden]) { display: none; } +body.mm-theme .site-header .global-sync-bar .auto-sync-status { + position: static; + margin: 0; + max-width: min(420px, 46vw); + font-size: 12px; + line-height: 1.6; + color: var(--mm-navy); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +body.mm-theme .site-header .global-sync-bar .btn { min-height: 30px; padding: 4px 14px; font-size: 12px; } + +/* 回到顶部:官方 .common--top(右下 120px,桌面 1.4286vw 边距) */ +.mm-top { + position: fixed; + right: 2.5vw; + bottom: 2.5vw; + z-index: 3; + display: block; + width: 15vw; + max-width: 120px; + transition: transform .3s, opacity .3s; +} +.mm-top.off { opacity: 0; transform: translateY(30%); pointer-events: none; } +.mm-top:hover { transform: translateY(-5%); } +.mm-top img { display: block; width: 100%; height: auto; } + +/* ============ 4. 版心与页面节奏 ============ */ + +body.mm-theme .app, +body.mm-theme.ac-page .app { + max-width: var(--mm-content); + margin: 0 auto; + padding: clamp(26px, 3vw, 46px) clamp(16px, 1.6vw, 22px) clamp(76px, 7vw, 120px); +} + +/* 页头:左边标题块、右边主按钮,一行放下;剩余的次级操作归到下面的工具行 */ +body.mm-theme .ac-page-heading { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: clamp(20px, 2.4vw, 36px); + flex-wrap: wrap; + margin-bottom: clamp(24px, 2.6vw, 38px); +} + +/* 背景太花,文字不能直接落在上面:页面顶部这几块统一加一层「玻璃板」。 + 不支持的浏览器回退成更实的白(见下面的 @supports)。 */ +body.mm-theme .ac-page-heading, +body.mm-theme .stats-toolbar, +body.mm-theme .circle-hero, +body.mm-theme .dan-hero, +body.mm-theme .help-heading, +body.mm-theme .help-toc, +body.mm-theme .pager { + position: relative; + background: rgba(255, 255, 255, .92); + border: 1px solid rgba(255, 255, 255, .75); + border-radius: 28px; + padding: clamp(18px, 2.2vw, 32px) clamp(20px, 2.6vw, 38px); + box-shadow: 0 20px 44px -30px rgba(11, 56, 113, .6); +} +@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) { + body.mm-theme .ac-page-heading, + body.mm-theme .stats-toolbar, + body.mm-theme .circle-hero, + body.mm-theme .dan-hero, + body.mm-theme .help-heading, + body.mm-theme .help-toc, + body.mm-theme .pager { + background: rgba(255, 255, 255, .88); + -webkit-backdrop-filter: blur(12px) saturate(1.2); + backdrop-filter: blur(12px) saturate(1.2); + } +} +body.mm-theme .help-toc { padding: 12px 18px; border-radius: 100px; } +body.mm-theme .pager { border-radius: 100px; padding: 10px 18px; } +/* 玻璃板里的标题再加一圈细白描边,字更贴地 */ +body.mm-theme .ac-page-heading h2, +body.mm-theme .stats-toolbar h2, +body.mm-theme .circle-hero h2, +body.mm-theme .dan-hero h2, +body.mm-theme .help-heading h2 { + text-shadow: 0 1px 0 rgba(255, 255, 255, .9), 0 0 14px rgba(255, 255, 255, .8); +} + +/* 小字:在卡片或玻璃板里的留一圈很淡的白描边就够了;直接落在背景花纹上的那几处 + (.app 的直接子元素、题友动态的状态行、说明页正文、热力图加载提示)另给一块 + 不透明底板,否则花哨图案会从字底下透出来 */ +body.mm-theme .analytics-note, +body.mm-theme #message, +body.mm-theme .dan-msg, +body.mm-theme .circle-note, +body.mm-theme .pager-hint { + text-shadow: 0 1px 0 rgba(255, 255, 255, .95), 0 0 10px rgba(255, 255, 255, .9); +} +body.mm-theme .app > .analytics-note, +body.mm-theme .app > #message, +body.mm-theme .app > .dan-msg, +body.mm-theme .circle-main > #message, +body.mm-theme .heatmap-top > .analytics-note, +body.mm-theme .help-page > p { + background: #fff; + border-radius: 12px; + padding: 8px 14px; + box-shadow: 0 8px 20px -14px rgba(11, 56, 113, .55); + text-shadow: none; +} +body.mm-theme .stats-eyebrow { display: block; margin-bottom: 14px; } +body.mm-theme .ac-page-heading h2 { margin: 0 0 14px; } +body.mm-theme .ac-page-heading p { margin: 0; } +body.mm-theme .ac-page-heading .form-actions { flex-wrap: nowrap; } + +body.mm-theme .page-toolbar { + justify-content: flex-end; + gap: 12px; + margin-bottom: clamp(28px, 3.2vw, 48px); +} + +/* ============ 5. 官方大盒子 .common--box(白底 + 上紫下粉横条 + 14px 硬阴影) ============ */ + +body.mm-theme .filters, +body.mm-theme .admin, +body.mm-theme .foot, +body.mm-theme .section, +body.mm-theme .stat-row, +body.mm-theme .ac-platform-strip, +body.mm-theme .placeholder, +body.mm-theme .empty { + position: relative; + background: #fff; + border: 0; + color: var(--mm-ink); + border-radius: var(--mm-card-radius); + box-shadow: var(--mm-card-shadow); + overflow: visible; +} + +/* 横条只画在该有横条的盒子上:概览、平台、筛选、记录、页脚、账号管理 */ +body.mm-theme .filters::before, +body.mm-theme .admin::before, +body.mm-theme .foot::before, +body.mm-theme .section::before, +body.mm-theme .stat-row::before, +body.mm-theme .ac-platform-strip::before, +body.mm-theme .analytics::before, +body.mm-theme .today-panel::before { + content: ""; + position: absolute; + top: 0; + left: 50%; + width: var(--mm-bar-w); + height: var(--mm-bar-h); + border-radius: 0 0 var(--mm-bar-h) var(--mm-bar-h); + background: var(--mm-purple); + transform: translateX(-50%); +} +body.mm-theme .filters::after, +body.mm-theme .admin::after, +body.mm-theme .foot::after, +body.mm-theme .section::after, +body.mm-theme .stat-row::after, +body.mm-theme .ac-platform-strip::after, +body.mm-theme .analytics::after, +body.mm-theme .today-panel::after { + content: ""; + position: absolute; + bottom: 0; + left: 50%; + width: var(--mm-bar-w); + height: var(--mm-bar-h); + border-radius: var(--mm-bar-h) var(--mm-bar-h) 0 0; + background: var(--mm-pink); + transform: translateX(-50%); +} + +/* 大盒子统一的内边距与节奏 */ +body.mm-theme .section, +body.mm-theme .stat-row, +body.mm-theme .ac-platform-strip { + margin-bottom: var(--mm-gap); + padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad); +} + +body.mm-theme.ac-page .filters { + display: grid; + gap: clamp(22px, 2.6vw, 34px) clamp(20px, 2.4vw, 32px); + padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad); + background: #fff; + border: 0; + border-radius: var(--mm-card-radius); + box-shadow: var(--mm-card-shadow); + margin-bottom: var(--mm-gap); +} +/* 官方三栏只在大屏成立。这条 grid-template-columns 原来没带媒体查询,把 styles.css / + ui.css 里窄屏的 1~2 栏折叠全压掉了:手机上硬排三栏,每栏只剩 70~100px, + 平台 chips 竖着堆、状态三连被挤成两行、搜索框只剩一半宽 */ +@media (min-width: 1001px) { + body.mm-theme.ac-page .filters { grid-template-columns: 1fr 1.4fr 1fr; } +} +@media (max-width: 1000px) { + body.mm-theme.ac-page .filters { gap: 16px; } +} + +/* ============ 6. 按钮(官方 .common--btn:红底 + 同色系 3D 底影,按下时沉到阴影上) ============ */ + +body.mm-theme .btn { + border: 0; + border-radius: 100px; + font-family: var(--mm-sans); + font-size: 14px; + font-weight: 800; + letter-spacing: .2px; + min-height: 40px; + padding: 8px 22px; + box-shadow: 0 5px 0 0 rgba(11, 56, 113, .18); + transition: transform .15s, box-shadow .15s, background .15s, color .15s; +} + +body.mm-theme .btn-primary { + background: var(--mm-red); + color: #fff; + box-shadow: 0 5px 0 0 var(--mm-red-dark); +} +body.mm-theme .btn-primary:hover { background: var(--mm-red); color: #fff; transform: translateY(5px); box-shadow: none; } + +body.mm-theme .btn-ghost { + background: #fff; + color: var(--mm-navy); + border: 1px solid var(--mm-grey); +} +body.mm-theme .btn-ghost:hover { + background: #fff; + color: var(--mm-purple); + border-color: var(--mm-purple-soft); + transform: translateY(5px); + box-shadow: none; +} + +body.mm-theme .btn:active { transform: translateY(5px); box-shadow: none; } +body.mm-theme .btn:focus-visible { outline: 3px solid var(--mm-purple-soft); outline-offset: 2px; } +body.mm-theme .btn[disabled] { opacity: .45; box-shadow: none; transform: none; } + +body.mm-theme .btn-small { min-height: 32px; padding: 6px 16px; font-size: 12.5px; } +body.mm-theme .btn-mini { min-height: 28px; padding: 4px 12px; font-size: 12px; } + +/* ============ 7. 表单 ============ */ + +body.mm-theme .input, +body.mm-theme .select { + font-family: var(--mm-sans); + font-size: 14px; + color: var(--mm-ink); + background: #fff; + border: 1px solid var(--mm-grey); + border-radius: 100px; + min-height: 44px; + padding: 8px 18px; + box-shadow: none; +} +body.mm-theme .input::placeholder { color: #a8a2b4; } +body.mm-theme .input:focus, +body.mm-theme .select:focus { + outline: none; + border-color: var(--mm-purple); + box-shadow: 0 0 0 3px rgba(186, 94, 234, .2); +} +body.mm-theme .select { cursor: pointer; } + +body.mm-theme .field-label, +body.mm-theme .filter-label { + color: var(--mm-navy); + font-weight: 800; + font-size: 13px; + letter-spacing: .2px; +} +body.mm-theme .field-hint, +body.mm-theme .section-hint { color: var(--muted); } + +/* ============ 8. 标签、胶囊、分页 ============ */ + +body.mm-theme .chip { + font-family: var(--mm-sans); + font-size: 13px; + font-weight: 700; + color: var(--mm-navy); + background: #fff; + border: 1px solid var(--mm-grey); + border-radius: 100px; + padding: 6px 14px; +} +body.mm-theme .chip:hover { border-color: var(--mm-purple-soft); color: var(--mm-purple); } +body.mm-theme .chip.is-on { + background: var(--mm-purple); + border-color: var(--mm-purple); + color: #fff; + box-shadow: 0 3px 0 0 var(--mm-purple-deep); +} +body.mm-theme .chip.is-on .chip-n { color: rgba(255, 255, 255, .85); } +body.mm-theme .chip .chip-n { color: var(--muted); font-family: var(--mm-mono); } + +body.mm-theme .tabs { + background: var(--mm-purple-tint); + border: 0; + border-radius: 100px; + padding: 3px; +} +body.mm-theme .tab { + font-family: var(--mm-sans); + font-size: 13px; + font-weight: 700; + color: var(--mm-navy); + background: transparent; + border-radius: 100px; + padding: 7px 18px; +} +body.mm-theme .tab:hover { color: var(--mm-purple); } +body.mm-theme .tab.is-active { + background: #fff; + color: var(--mm-purple); + box-shadow: 0 2px 0 rgba(11, 56, 113, .12); +} + +body.mm-theme .badge { + font-family: var(--mm-sans); + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + border: 0; + border-radius: 100px; + background: #f2eef7; + color: #5f5870; +} +body.mm-theme .badge.ac { background: #e6fbe9; color: #137a3a; } +body.mm-theme .badge.warn { background: #fff2e0; color: #a35c00; } +body.mm-theme .badge.bad { background: #ffe9ec; color: #b3122a; } +body.mm-theme .badge.diff { background: #e8f0ff; color: #1c53b8; } +body.mm-theme .badge.contest { background: #f3e6ff; color: #7d2fb0; } +body.mm-theme .badge.platform { background: #e9f6ff; color: #135a8f; text-transform: none; } +body.mm-theme .badge.score { background: #fff2e0; color: #a35c00; } + +body.mm-theme .db-chip { + font-family: var(--mm-mono); + font-size: 12px; + color: var(--mm-navy); + background: #fff; + border: 1px solid var(--mm-grey); + border-radius: 100px; + padding: 6px 14px; +} +body.mm-theme.ac-page .db-chip { color: var(--mm-navy); border-color: var(--mm-grey); max-width: 260px; } + +body.mm-theme .pager { padding: 12px 0 4px; } +body.mm-theme .pager-hint { color: var(--muted); font-family: var(--mm-mono); } + +/* ============ 9. 概览数字(盒子是官方的,格子是平的) ============ */ + +body.mm-theme .stat-row, +body.mm-theme.ac-page #statRow { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); + gap: 6px; + align-items: stretch; +} +/* 六个指标就该是一行:auto-fit 在 1000px 出头会掉成 5+1,很难看 */ +@media (min-width: 1080px) { + body.mm-theme .stat-row, + body.mm-theme.ac-page #statRow { grid-template-columns: repeat(6, minmax(0, 1fr)); } +} +@media (max-width: 1079px) and (min-width: 760px) { + body.mm-theme .stat-row, + body.mm-theme.ac-page #statRow { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} +@media (max-width: 759px) { + body.mm-theme .stat-row, + body.mm-theme.ac-page #statRow { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +body.mm-theme .stat, +body.mm-theme.ac-page #statRow .stat { + background: transparent; + border: 0; + border-radius: 20px; + padding: 14px 18px; + box-shadow: none; + transition: background .2s var(--mm-ease-out); +} +body.mm-theme .stat:hover, +body.mm-theme.ac-page #statRow .stat:hover { background: var(--mm-purple-tint); } + +body.mm-theme .stat-label { color: var(--mm-navy); font-weight: 700; font-size: 12.5px; } +body.mm-theme .stat-value { + font-family: var(--mm-sans); + font-size: clamp(28px, 2.4vw, 34px); + font-weight: 800; + color: var(--mm-navy); + letter-spacing: -.5px; +} +body.mm-theme .stat-sub { + color: var(--muted); + font-size: 11.5px; + line-height: 1.5; + text-wrap: pretty; +} +body.mm-theme .stat.is-ac .stat-value { color: #16883f; } +body.mm-theme .stat.is-unac .stat-value { color: #c47f00; } +body.mm-theme .stat.is-sub .stat-value { color: var(--mm-purple-deep); } + +body.mm-theme.ac-page #statRow .stat.is-ac { border: 0; background: transparent; } +body.mm-theme.ac-page #statRow .stat:last-child .stat-value { font-size: 20px; line-height: 1.8; } + +/* 平台条:同样是「一个白盒子 + 平铺的格子」 */ +body.mm-theme .ac-platform-strip { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(232px, 1fr)); + gap: 6px; +} +body.mm-theme .ac-platform-strip .stat { + background: transparent; + border: 0; + border-radius: 18px; + box-shadow: none; + padding: 12px 16px; + transition: background .2s var(--mm-ease-out); +} +body.mm-theme .ac-platform-strip .stat:hover { background: var(--mm-purple-tint); } +body.mm-theme .ac-platform-strip .stat-value { font: 800 18px var(--mm-sans); color: var(--mm-navy); } +body.mm-theme .ac-platform-strip .stat-sub { color: var(--muted); } + +/* ============ 10. 记录列表(区块是官方盒子,每条记录是盒子里的一行) ============ */ + +/* 区块标题占一行,下面用发丝线接记录行 —— 与官网 top--update 的列表同一套做法 */ +body.mm-theme.ac-page .section-head { + margin-bottom: 6px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border-soft); +} + +body.mm-theme.ac-page .cards { display: block; } + +body.mm-theme.ac-page .card { + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(150px, .62fr) auto; + grid-template-areas: + "top stats foot" + "context stats foot"; + align-items: center; + column-gap: clamp(14px, 1.6vw, 28px); + row-gap: 2px; + padding: 12px 4px; + background: transparent; + border: 0; + border-bottom: 1px solid var(--border-soft); + border-radius: 0; + box-shadow: none; + transform: none; + transition: background .18s var(--mm-ease-out); +} +body.mm-theme.ac-page .card:last-child { border-bottom: 0; } +body.mm-theme.ac-page .card.is-ac, +body.mm-theme.ac-page .card.is-unac { background: transparent; border-left: 0; } +@media (hover: hover) and (pointer: fine) { + body.mm-theme.ac-page .card:hover { background: #fdf8ff; } +} + +body.mm-theme.ac-page .card .card-top { grid-area: top; min-height: 0; justify-content: flex-start; align-items: center; gap: 10px; } +body.mm-theme.ac-page .card .ac-card-context { grid-area: context; gap: 3px; } +body.mm-theme.ac-page .card .card-stats { grid-area: stats; } +body.mm-theme.ac-page .card .card-foot { grid-area: foot; } +/* 题号/平台/难度挤在一行里就别铺开了:小一号、间距收掉,一行读完 */ +body.mm-theme.ac-page .card-meta > .badge { padding: 2px 8px; font-size: 11px; } +body.mm-theme.ac-page .ac-card-identity { padding-top: 0; font-size: 11.5px; } + +body.mm-theme.ac-page .card-title { + font-size: 16px; + font-weight: 800; + line-height: 1.35; + color: var(--mm-navy); +} +body.mm-theme .card-title a:hover { color: var(--mm-purple); border-bottom-color: var(--mm-purple-soft); } +body.mm-theme .card-meta { color: var(--muted); } + +body.mm-theme.ac-page .card-stats { + background: transparent; + border: 0; + border-radius: 0; + padding: 0; + gap: 2px 14px; +} +/* 「通过时间 2026-10-10 14:50」并成一行,右侧那一栏才不空得发慌 */ +body.mm-theme.ac-page .card-stats > span { display: flex; align-items: baseline; gap: 8px; } +body.mm-theme.ac-page .card-stats .k { color: var(--muted); font-size: 11.5px; } +body.mm-theme.ac-page .card-stats .v { color: var(--mm-navy); font-weight: 700; font-size: 14px; } +body.mm-theme.ac-page .card-stats > span:first-child { border-bottom: 0; padding-bottom: 0; } + +body.mm-theme.ac-page .card-foot { + border-top: 0; + padding-top: 0; + flex-direction: column; + align-items: flex-end; + gap: 5px; + flex-wrap: nowrap; + color: var(--muted); + text-align: right; +} +body.mm-theme .ac-card-tags .badge { background: #f2eef7; border: 0; color: #5f5870; } + +/* 窄屏回到「一张一张卡片」的堆叠形态(与汉堡菜单同一个断点) */ +@media (max-width: 1000px) { + /* 工具行排成两列网格:旧样式在窄屏会一行一个按钮、还全部右对齐,边缘很毛 */ + body.mm-theme .page-toolbar { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + body.mm-theme .page-toolbar .db-chip { grid-column: 1 / -1; justify-self: start; max-width: 100%; } + body.mm-theme .page-toolbar .btn { + width: 100%; + justify-content: center; + padding: 8px 12px; + font-size: 13px; + white-space: nowrap; + } + body.mm-theme .ac-page-heading .form-actions, + body.mm-theme .ac-page-heading .btn-primary { width: 100%; } + + body.mm-theme.ac-page .section-head { padding-bottom: 14px; } + body.mm-theme.ac-page .cards { display: flex; flex-direction: column; gap: 16px; } + body.mm-theme.ac-page .card { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 14px; + padding: 18px; + border: 1px solid var(--border-soft); + border-radius: 20px; + box-shadow: var(--mm-soft); + } + body.mm-theme.ac-page .card:last-child { border-bottom: 1px solid var(--border-soft); } + body.mm-theme.ac-page .card-foot { justify-content: flex-start; text-align: left; align-items: flex-start; } + + /* 状态三连(全部/已AC/未AC)在窄屏只有 100px 出头,会被挤成两行、 + 右半边还被圆形的选中块盖住:让每组平分整行、文字不许折行,选中块改成圆角矩形 */ + body.mm-theme.ac-page .filters .tabs { display: flex; width: 100%; } + body.mm-theme.ac-page .filters .tab { + flex: 1 1 0; + min-width: 0; + padding: 9px 6px; + border-radius: 12px; + font-size: 13px; + white-space: nowrap; + } + + /* 点得着的目标:手机上一行的操作按钮别只有 23px 高 */ + body.mm-theme .collapse-toggle, + body.mm-theme .expand-btn, + body.mm-theme .dx-more a, + body.mm-theme button.circle-name { min-height: 40px; } + + /* 全年热力图一行放不下(53 列),给它一句滑动提示,别让人以为只有半年 */ + body.mm-theme #heatmapHost::before { + content: '热力图一行放不下一年,左右滑动查看 →'; + display: block; + margin-bottom: 8px; + font-size: 11.5px; + color: var(--muted); + } +} + +/* 手机上按钮不排到半格,直接一列到底 */ +@media (max-width: 560px) { + body.mm-theme .page-toolbar { grid-template-columns: minmax(0, 1fr); } +} + +/* 卡片页脚的小操作:旧样式是深蓝实心小块,这里换成官方语言的白底胶囊 */ +body.mm-theme.ac-page .expand-btn { + font-family: var(--mm-sans); + font-size: 12px; + font-weight: 700; + color: var(--mm-navy); + background: #fff; + border: 1px solid var(--mm-grey); + border-radius: 100px; + padding: 6px 14px; + gap: 6px; + box-shadow: 0 3px 0 0 rgba(11, 56, 113, .15); + transition: transform .15s, box-shadow .15s, color .15s, border-color .15s; +} +body.mm-theme.ac-page .expand-btn:hover { + color: var(--mm-purple); + border-color: var(--mm-purple-soft); + text-decoration: none; + transform: translateY(3px); + box-shadow: none; +} + +/* ============ 11. 区块标题与提示 ============ */ + +body.mm-theme .section-head h2, +body.mm-theme.ac-page .section-head h2 { + font-size: 20px; + font-weight: 800; + color: var(--mm-navy); +} +body.mm-theme .section-count { + display: inline-block; + margin-left: 8px; + padding: 2px 12px; + background: var(--mm-purple-tint); + border-radius: 100px; + color: var(--mm-purple-deep); + font-family: var(--mm-sans); + font-size: 12px; + font-weight: 800; +} +body.mm-theme .collapse-toggle { color: var(--mm-navy); font-weight: 800; background: none; border: 0; } +body.mm-theme .collapse-toggle:hover { color: var(--mm-purple); } +body.mm-theme .caret { color: var(--mm-purple); } +body.mm-theme .timeline { border-top: 1px dashed var(--border-soft); } +body.mm-theme .tl-time { color: var(--muted); } +body.mm-theme .tl-row { border-color: var(--border-soft); } + +body.mm-theme .ac-page-heading h2, +body.mm-theme .stats-toolbar h2 { font-size: 30px; font-weight: 800; color: var(--mm-navy); } +body.mm-theme .ac-page-heading p, +body.mm-theme .stats-toolbar p { color: #4b4b4b; } +body.mm-theme .stats-eyebrow { + color: var(--mm-purple); + letter-spacing: 3px; + font-weight: 800; +} + +/* 通知:不抢戏 —— 一点暖底 + 一条状态色 + 发丝分隔行 */ +body.mm-theme .notice { + background: #fffcf7; + border: 0; + border-left: 5px solid var(--mm-orange); + border-radius: 14px; + padding: 14px 18px; + box-shadow: none; + color: var(--mm-ink); + font-size: 12.5px; + line-height: 1.7; +} +body.mm-theme .notice + .notice { margin-top: 10px; } +body.mm-theme .notice > div + div { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-soft); +} +body.mm-theme .notice strong { color: var(--mm-navy); } +body.mm-theme .notice-ac { background: #f7fdf9; border-left-color: var(--mm-green); } +body.mm-theme .notice-ac strong { color: var(--mm-navy); } +body.mm-theme .notice-ac.is-bad, +body.mm-theme .notice-danger { background: #fff8f9; border-left-color: var(--mm-red); } +body.mm-theme .notice-ac.is-bad strong, +body.mm-theme .notice-danger strong { color: var(--mm-red-dark); } +body.mm-theme .notice-warn { background: #fffcf7; border-left-color: var(--mm-orange); } +body.mm-theme .notice-warn strong { color: var(--mm-navy); } +body.mm-theme .notice-warn span { color: #6f6a7a; } +body.mm-theme .notice-neutral { background: #fcfcfc; border-left-color: var(--mm-grey); color: var(--muted); } + +body.mm-theme .placeholder, +body.mm-theme .empty { + border-radius: var(--mm-card-radius); + box-shadow: var(--mm-card-shadow); + padding: 56px 20px; + color: var(--muted); +} +body.mm-theme .empty h3 { color: var(--mm-navy); font-weight: 800; } +body.mm-theme .spinner { border-color: var(--mm-purple-tint); border-top-color: var(--mm-purple); } + +/* ============ 12. 页脚(官方 .footer--bottom:白底、#0b3871 字色、上沿软阴影) ============ */ + +body.mm-theme .foot { + margin-top: clamp(30px, 3.4vw, 52px); + padding: calc(var(--mm-bar-h) + clamp(20px, 2.4vw, 34px)) var(--mm-pad); + color: var(--mm-navy); + font-size: 12.5px; + box-shadow: var(--mm-card-shadow); +} +body.mm-theme .foot-note { color: var(--muted); } +body.mm-theme .foot a { color: var(--mm-purple-deep); } + +/* ============ 13. 暗色残留(旧规则里写死的浅色字 / 深色底,逐条扳回亮色) ============ */ + +body.mm-theme .ac-card-identity, +body.mm-theme.ac-page .ac-card-identity { color: var(--muted); } +body.mm-theme .form-status { color: var(--mm-navy); } +body.mm-theme .form-status.is-bad { color: var(--bad); } +body.mm-theme .form-status.is-ok { color: var(--ac); } +body.mm-theme .form-status.is-warn { color: var(--warn); } + +body.mm-theme .stat-row { gap: 18px; } + +body.mm-theme .admin-head h2 { color: var(--mm-navy); font-weight: 800; font-size: 16px; } +body.mm-theme .admin-block { background: var(--mm-purple-tint); border: 1px solid var(--border-soft); } +body.mm-theme .admin-block h3 { color: var(--mm-navy); font-weight: 800; } + +body.mm-theme .account-owner { background: #fff; border: 1px solid var(--border-soft); } +body.mm-theme .account-owner > summary:hover { background: var(--mm-purple-tint); } +body.mm-theme .account-owner-avatar { color: var(--mm-purple-deep); background: var(--mm-purple-tint); } +body.mm-theme .account-owner-content { background: #fffafd; } +body.mm-theme .account-row { border-color: var(--border-soft); } + +body.mm-theme .help-inline { background: #fff; border-color: var(--border-soft); } +body.mm-theme .help-inline summary { color: var(--mm-navy); } + +@media (max-width: 600px) { + body.mm-theme .ac-page-heading h2, + body.mm-theme .stats-toolbar h2 { font-size: 26px; } +} + +/* ============ 14. 表格与弹窗 ============ */ + +body.mm-theme .rt-table { + background: #fff; + border-radius: 18px; + overflow: hidden; +} +body.mm-theme .rt-table th { color: var(--mm-navy); font-weight: 800; background: var(--mm-purple-tint); } +body.mm-theme .rt-table td, +body.mm-theme .rt-table th { border-color: var(--border-soft); } +body.mm-theme .rt-table tbody th { color: var(--mm-navy); font-weight: 700; } + +body.mm-theme .modal-card, +body.mm-theme dialog { + background: #fff; + color: var(--mm-ink); + border: 0; + border-radius: var(--mm-card-radius); + box-shadow: var(--mm-card-shadow); +} +body.mm-theme dialog::backdrop { background: rgba(11, 56, 113, .35); } +body.mm-theme .modal-card h2, +body.mm-theme dialog h2, +body.mm-theme dialog h3 { color: var(--mm-navy); } + +/* ============ 15. 其余页面自带组件(旧文件里写死的深色,逐条扳回官方亮色) ============ + * + * styles.css / ui.css / circle.css 里有一批硬编码的深色面(分析卡、今日面板、热力图、 + * DX 计时器、题友圈人物、帮助目录…),token 重映射管不到,只能按选择器覆盖。 + * 另:这些页面原本没有「盒子」,正文直接落在动态背景上,所以这里一并给它们官方白盒。 + */ + +/* —— 统计页:分析卡 / 今日面板 / 热力图 —— */ +body.mm-theme .analytics { + position: relative; + background: #fff; + border: 0; + border-radius: var(--mm-card-radius); + padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad); +} +body.mm-theme .analytics-chart { + background: #fff; + border: 1px solid var(--border-soft); + border-radius: 22px; + padding: 22px 24px; + box-shadow: var(--mm-soft); +} +/* 热力图那块是直接挂在 #heatmapHost 下的一级面板(不像别的图表嵌在 .analytics 里), + 所以它也得算一个官方大盒子:同样的圆角、硬阴影和上下横条 */ +body.mm-theme .heatmap-top > .analytics-chart { + position: relative; + border: 0; + border-radius: var(--mm-card-radius); + padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad); + box-shadow: var(--mm-card-shadow); +} +body.mm-theme .heatmap-top > .analytics-chart::before, +body.mm-theme .heatmap-top > .analytics-chart::after { + content: ""; + position: absolute; + left: 50%; + width: var(--mm-bar-w); + height: var(--mm-bar-h); + background: var(--mm-purple); + transform: translateX(-50%); +} +body.mm-theme .heatmap-top > .analytics-chart::before { + top: 0; + border-radius: 0 0 var(--mm-bar-h) var(--mm-bar-h); +} +body.mm-theme .heatmap-top > .analytics-chart::after { + bottom: 0; + border-radius: var(--mm-bar-h) var(--mm-bar-h) 0 0; + background: var(--mm-pink); +} +body.mm-theme .analytics-chart h3 { color: var(--mm-navy); font-weight: 800; } +body.mm-theme .analytics-chart h4 { color: var(--muted); } +body.mm-theme .analytics-head { gap: 12px; } +body.mm-theme .analytics-controls { + background: var(--mm-purple-tint); + border: 0; + border-radius: 100px; + padding: 8px 10px; + gap: 8px; +} +body.mm-theme .analytics-controls .input { color: var(--mm-ink); color-scheme: light; } +body.mm-theme .chart-values { color: var(--muted); } +body.mm-theme .chart-values dl { background: var(--mm-purple-tint); border-radius: 14px; } +body.mm-theme .chart-values dd { color: var(--mm-navy); } +body.mm-theme .analytics-metrics > div { + background: #fff; + border: 1px solid var(--border-soft); + border-radius: 18px; + border-left: 4px solid var(--mm-green); + padding: 16px 18px; + box-shadow: var(--mm-soft); +} +body.mm-theme .analytics-metrics strong { color: var(--mm-navy); } +body.mm-theme .analytics-metrics span { color: var(--muted); } +body.mm-theme .analytics-track { background: var(--mm-purple-tint); height: 10px; } +body.mm-theme .analytics-track i { background: linear-gradient(90deg, #38c46b, var(--mm-green)); } +body.mm-theme .analytics-bar-row b { color: var(--mm-navy); } + +body.mm-theme .today-panel { + position: relative; + background: #fff; + border: 0; + border-radius: var(--mm-card-radius); + padding: calc(var(--mm-bar-h) + var(--mm-pad)) var(--mm-pad); + box-shadow: var(--mm-card-shadow); +} +body.mm-theme .today-metrics > div, +body.mm-theme .today-rhythm { + background: var(--mm-purple-tint); + border: 0; + border-radius: 18px; +} +body.mm-theme .today-metrics strong { color: var(--mm-navy); } +body.mm-theme .today-metrics span, +body.mm-theme .today-metrics small, +body.mm-theme .today-performance span { color: var(--muted); } +body.mm-theme .today-card { background: #fff; border-color: var(--border-soft); box-shadow: var(--mm-soft); } + +/* 热力图格子:深色系 → 官方绿渐变 */ +body.mm-theme .heat-cell { border: 1px solid rgba(11, 56, 113, .08); border-radius: 4px; } +body.mm-theme .level-0 { background: #eeeaf5; } +body.mm-theme .level-1 { background: #d8f6e2; } +body.mm-theme .level-2 { background: #a9eebf; } +body.mm-theme .level-3 { background: #74e49b; } +body.mm-theme .level-4 { background: var(--mm-green); } +body.mm-theme .heat-weekdays, +body.mm-theme .heat-months, +body.mm-theme .heat-legend { color: var(--muted); } +body.mm-theme .heat-cell:hover, +body.mm-theme .heat-cell:focus-visible { outline: 2px solid var(--mm-purple); outline-offset: 1px; } + +/* —— DX Rating 页 —— */ +/* 旧样式把 .dx-eyebrow 固定成浅蓝 #8e9fff,白底上只有 2.46 的对比度, + 和另外两个 eyebrow 一样收进官方紫里 */ +body.mm-theme .dx-eyebrow { color: var(--mm-purple); letter-spacing: 2px; font-weight: 800; } +body.mm-theme .dx-pages { + background: #fff; + border: 1px solid var(--border-soft); + border-radius: 100px; + padding: 8px 10px; + box-shadow: var(--mm-soft); +} +body.mm-theme .dx-timer { + background: #fff; + border: 0; + border-radius: var(--mm-card-radius); + box-shadow: var(--mm-card-shadow); + padding: var(--mm-pad); +} +body.mm-theme .dx-timer h2 { color: var(--mm-navy); } +body.mm-theme .dx-more a { background: #fff; color: var(--mm-navy); border: 1px solid var(--mm-grey); } +body.mm-theme .dx-ranked { color: #a35c00; } +body.mm-theme .dx-records-filters { background: #fff; border: 0; box-shadow: var(--mm-soft); } + +/* —— 题友圈 —— */ +body.mm-theme .circle-eyebrow { color: var(--mm-purple); letter-spacing: 2px; font-weight: 800; } +body.mm-theme .circle-hero h2 { color: var(--mm-navy); } +body.mm-theme .circle-hero p { color: #4b4b4b; } +body.mm-theme .circle-sidebar, +body.mm-theme .circle-main { color: var(--mm-ink); } +body.mm-theme .circle-sidebar { + background: #fff; + border: 0; + border-radius: var(--mm-card-radius); + box-shadow: var(--mm-card-shadow); + padding: var(--mm-pad); +} +body.mm-theme .circle-person { color: var(--mm-navy); border-radius: 18px; } +/* 窄屏原样式把「我的关注」排成一条横向滚动条:卡片里看不出还能滑, + 第二个人就停在卡片边缘,名字后面的通过数被切一半。干脆换行排成格子,全都看得见 */ +@media (max-width: 850px) { + body.mm-theme #people { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(146px, 1fr)); + gap: 8px; + overflow: visible; + } + body.mm-theme #people .circle-person { min-width: 0; width: 100%; } +} +body.mm-theme .circle-person:hover { background: var(--mm-purple-tint); } +body.mm-theme .circle-person[aria-pressed="true"] { + background: var(--mm-purple-tint); + border-color: var(--mm-purple-soft); + color: var(--mm-purple-deep); +} +body.mm-theme .circle-avatar { + background: hsl(var(--hue, 160) 62% 92%); + color: hsl(var(--hue, 160) 48% 30%); +} +body.mm-theme .circle-name:hover { color: var(--mm-purple); } +body.mm-theme .circle-result { background: #f2eef7; color: #5f5870; } +body.mm-theme .circle-result.ac { background: #e6fbe9; color: #137a3a; } +body.mm-theme .circle-coverage-status.needs-attention { color: #a35c00; background: #fff3e2; } +body.mm-theme .circle-card { + background: #fff; + border: 1px solid var(--border-soft); + border-radius: 22px; + box-shadow: var(--mm-soft); +} +body.mm-theme .circle-filters { + background: #fff; + border: 0; + border-radius: var(--mm-card-radius); + box-shadow: var(--mm-card-shadow); + padding: var(--mm-pad); +} +body.mm-theme .circle-card-head, +body.mm-theme .circle-card-foot { border-color: var(--border-soft); } + +/* —— 使用指南:正文直接落在背景上,给每节一个白盒 —— */ +body.mm-theme .help-page > section { + background: #fff; + border-radius: 24px; + padding: clamp(20px, 2.2vw, 30px) clamp(20px, 2.4vw, 34px); + margin-bottom: 18px; + box-shadow: var(--mm-soft); +} +body.mm-theme .help-page > section h2 { color: var(--mm-navy); } +body.mm-theme .help-page > section > p, +body.mm-theme .help-page > section li { color: #3d3a46; } +body.mm-theme .help-page .help-toc a { + background: #fff; + border: 1px solid var(--mm-grey); + border-radius: 100px; + color: var(--mm-navy); +} +body.mm-theme .help-page .help-toc a:hover { background: var(--mm-purple-tint); color: var(--mm-purple-deep); border-color: var(--mm-purple-soft); } +body.mm-theme kbd { + background: var(--mm-purple-tint); + border: 1px solid var(--mm-purple-soft); + border-radius: 6px; + color: var(--mm-navy); +} + +/* 扫描剩下的浅色字(深色面已经处理完,这几处是字色本身写死的浅色) */ +body.mm-theme .today-total { color: #16883f; } +body.mm-theme .dx-rank { color: #a35c00; } +/* 段位卡本身是深色卡面(深蓝底 + 位阶色描边),和主题无关;浅色主题把页面刷白以后, + 卡面上的字必须跟着卡面走 —— 否则黑字压深蓝,题名/完成度/难度全看不见 */ +body.mm-theme .dx-card { color: #eef1fb; } +body.mm-theme .dx-card .dx-library-card-foot, +body.mm-theme .dx-card .dx-library-metrics span, +body.mm-theme .dx-card .dx-card-id, +body.mm-theme .dx-card .dx-card-meta, +body.mm-theme .dx-card .dx-card-name, +body.mm-theme .dx-card .dx-card-time, +body.mm-theme .dx-card .dx-pending-id { color: #bcc4dc; } +body.mm-theme .dx-card .dx-rank { color: var(--rank, #ffd166); } +body.mm-theme .dx-card .dx-library-title a { color: #f4f6ff; } +body.mm-theme .dx-card .dx-library-title a:hover { color: #d3a1ff; } +body.mm-theme .help-page .help-heading p, +body.mm-theme .help-page > section p, +body.mm-theme .help-page > section li, +body.mm-theme .help-page details p { color: #3d3a46; } +body.mm-theme .help-page #message { color: var(--muted); } +/* 还有几处页内标题/说明直接落在背景上(不在上面的页头列表里) */ +body.mm-theme .dx-page-intro, +body.mm-theme .circle-feed-head { + background: rgba(255, 255, 255, .92); + border: 1px solid rgba(255, 255, 255, .75); + border-radius: 24px; + padding: clamp(16px, 1.8vw, 24px) clamp(20px, 2.4vw, 32px); + box-shadow: 0 18px 40px -30px rgba(11, 56, 113, .55); +} +@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) { + body.mm-theme .dx-page-intro, + body.mm-theme .circle-feed-head { + background: rgba(255, 255, 255, .88); + -webkit-backdrop-filter: blur(12px) saturate(1.2); + backdrop-filter: blur(12px) saturate(1.2); + } +} +body.mm-theme .dx-page-intro h2, +body.mm-theme .circle-feed-head h2 { color: var(--mm-navy); text-shadow: 0 1px 0 rgba(255, 255, 255, .9); } +body.mm-theme .dx-page-intro p { color: #4b4b4b; } +body.mm-theme .dx-rules { + background: #fff; + border: 0; + border-radius: 24px; + padding: 20px 26px; + box-shadow: var(--mm-soft); + color: var(--mm-ink); +} +body.mm-theme .dan-disclaimer, +body.mm-theme .dan-msg { color: var(--muted); } +body.mm-theme .today-section-title { color: var(--mm-navy); } +body.mm-theme .help-page > p { color: #3d3a46; } +/* ============ 16. 深色模式开关(顶栏右端,官方 .header--btn 的圆钮语言) ============ */ + +body.mm-theme .mm-theme-btn { + position: relative; + flex: 0 0 auto; + width: 44px; + height: 44px; + margin-left: 6px; + padding: 0; + border: 3px solid var(--mm-purple); + border-radius: 100px; + background: #fff; + box-shadow: 4px 4px 0 rgba(0, 0, 0, .2); + cursor: pointer; + transition: background .2s, border-color .2s, box-shadow .2s; +} +body.mm-theme .mm-theme-btn svg { display: block; width: 22px; height: 22px; margin: 0 auto; } +body.mm-theme .mm-theme-btn svg > * { + transform-origin: 50% 50%; + transition: opacity .2s, transform .3s var(--mm-ease-out); +} +body.mm-theme .mm-theme-btn__moon { fill: var(--mm-navy); } +body.mm-theme .mm-theme-btn__sun { fill: var(--mm-yellow); } +/* 浅色模式显示月亮(点了变深色);深色模式的另一半在 theme-dark.css */ +body.mm-theme .mm-theme-btn__sun { opacity: 0; transform: scale(.4) rotate(-90deg); } +body.mm-theme .mm-theme-btn:hover { border-color: var(--mm-purple); background: var(--mm-purple); } +body.mm-theme .mm-theme-btn:hover .mm-theme-btn__moon, +body.mm-theme .mm-theme-btn:hover .mm-theme-btn__sun { fill: #fff; } +body.mm-theme .mm-theme-btn:focus-visible { outline: 3px solid var(--mm-pink-hot); outline-offset: 3px; } + +@media (max-width: 1000px) { + body.mm-theme .mm-theme-btn { width: 44px; height: 44px; margin-left: auto; } + body.mm-theme .mm-theme-btn svg { width: 21px; height: 21px; } +} + +/* ============ 17. 换主题的那一帧 ============ + 页面里只有一部分元件带 transition(卡片、按钮、输入框),换主题时它们会慢半拍淡过去, + 看上去就像「有的卡片没跟着换皮」。theme.js 在改 data-mm-theme 之前先加上 + .mm-theme-switching,两帧后摘掉 —— 这一帧内所有过渡停用,整页同时换色。 */ +html.mm-theme-switching, +html.mm-theme-switching *, +html.mm-theme-switching *::before, +html.mm-theme-switching *::after { transition: none !important; } +/* 只有开关自己该动:月亮沉下去、太阳升起来,这一下是要给人看的 */ +html.mm-theme-switching .mm-theme-btn { transition: background .2s, border-color .2s, box-shadow .2s !important; } +html.mm-theme-switching .mm-theme-btn svg > * { transition: opacity .2s, transform .3s var(--mm-ease-out) !important; } + +/* ============ 18. 与官方的偏差(改这里之前先想清楚) ============ + * + * 1) 中日韩字形:官方 `font-family:"M PLUS 1p",sans-serif`,但 M PLUS 1p 没有简体字, + * 所以中文回落到系统字体(雅黑/苹方/思源)。数字与拉丁字母仍是官方的 M PLUS 1p —— + * 这一层观感(大数字、编号、DX Rating)已经和官网一致。 + * 2) 版心宽度:官方顶栏 1200px、内容盒 900px;本站顶栏保持 1200,正文收到 1080, + * 否则表格与列表会被压得太窄。 + * 3) 版式:官网没有等价的界面,这里按同一套语言重新编排 —— 每块内容是一个官方 + * `.common--box`(白底 + 上紫下粉横条 + 14px 硬影),记录列表照官网 top--update + * 的做法排成「盒子里的行 + 发丝分隔」,不再是一堆各自带阴影的小卡片。 + * 4) 按钮尺寸:官方 .common--btn 是 60px 高、28px 字、10px 底影;列表里的操作按钮 + * 按同一比例缩到 40px 高、5px 底影,避免一行按钮撑满半屏。 + * 5) 移动端断点:官方在 769px 就收成汉堡 + 语言胶囊;本站导航是七个中文词,比官方的 + * SVG 字标更宽,所以断点挪到 1000px(769~1000px 之间也走汉堡菜单)。 + * 6) 记录列表密度:官方 top--update 一行 157px 高,一屏只看得到三条;本站记录里 + * 题名、平台、通过时间都是短字段,行高收到 95px(一屏六条),大盒子的外观不变。 + * 7) 数据页的图表:官方没有等价界面,图表由 stat-charts.js 用画布画,配色按明暗两套 + * 写在脚本里(CSS 变量管不到 canvas),换主题时由 stats-page.js 重画。 + * 8) 符号字形:官方那套 webfont 自己画箭头、方块、✓ 这类符号;M PLUS 1p 的 latin 子集 + * 没覆盖它们(会被画成一根没有箭头的斜线),所以 fonts.css 另外声明了一个只圈这些码位、 + * 指向系统符号字体的 "M PLUS 1p Symbols",排在 M PLUS 1p 之后。 + * + * 背景没有偏差:整页复用官网那份动态背景(public/bg.html, + * 结构与样式都是归档快照原文),用 iframe 铺在内容下面,官方 CSS 的全局选择器 + * (html/body/.wrapper…)不会碰到本站布局。夜版只是换 /assets/night/ 那套贴图, + * 结构与参数不变(见 theme-night.css)。 + */ \ No newline at end of file diff --git a/public/theme.js b/public/theme.js new file mode 100644 index 0000000..a7b42f1 --- /dev/null +++ b/public/theme.js @@ -0,0 +1,124 @@ +/** + * maimai 官网皮肤的行为层:深色模式开关、移动端菜单开关、回到顶部。 + * + * 只做这一层需要的行为,不碰 app.js 的数据逻辑;无依赖、原生 ESM。 + * + * 深色模式(主题的唯一写入口,其它模块只读 dataset.mmTheme): + * · 主题落在 ,首屏由各页 里的一小段脚本先定好, + * 这里负责开关、记忆(localStorage)、跟随系统,以及下面这些跟着主题走的东西: + * 开关的 aria-checked 与 title、iframe 背景的主题、 + * 画布重画(发 mm-theme-change)、换皮那一帧的过渡开关。 + * 品牌图标不用 JS:它读 上的主题走 CSS 背景,首帧就是对的。 + * · 背景是 iframe 里的官方动态页,换主题不重载它,改发 postMessage 就地换贴图, + * 官方那套入场动画不会重播。 + */ + +const THEME_KEY = 'mm-theme'; + +const prefersDark = () => window.matchMedia('(prefers-color-scheme: dark)').matches; + +function storedTheme() { + try { + const value = localStorage.getItem(THEME_KEY); + return value === 'dark' || value === 'light' ? value : null; + } catch { + return null; + } +} + +const currentTheme = () => (document.documentElement.dataset.mmTheme === 'dark' ? 'dark' : 'light'); + +const bgFrame = document.querySelector('.mm-bg-frame'); + +function tellBackground(theme) { + if (!bgFrame || !bgFrame.contentWindow) return; + // 背景页认的就是 data-mm-theme 那两个词,见 bg.html 的 message 监听 + bgFrame.contentWindow.postMessage({ mmTheme: theme }, location.origin); +} + +function applyTheme(theme, { remember = true, notify = true } = {}) { + // 换皮那一帧先把过渡关掉:只有一部分卡片/按钮带 transition,不关的话它们会慢半拍, + // 看上去像「有的卡片没跟着换主题」。两帧后摘掉,之后 hover 之类的过渡照旧。 + const root = document.documentElement; + root.classList.add('mm-theme-switching'); + root.dataset.mmTheme = theme; + root.style.colorScheme = theme; + const button = document.getElementById('mmThemeBtn'); + if (button) { + button.setAttribute('aria-checked', String(theme === 'dark')); + button.title = theme === 'dark' ? '切换到浅色模式' : '切换到深色模式'; + } + if (remember) { + try { + localStorage.setItem(THEME_KEY, theme); + } catch { + /* 隐私模式下写不进去就算了,当次会话照样能用 */ + } + } + if (notify) tellBackground(theme); + // 画布里的图形是 JS 画的,CSS 换皮管不到,交给需要重画的模块自己接 + document.dispatchEvent(new CustomEvent('mm-theme-change', { detail: { theme } })); + requestAnimationFrame(() => requestAnimationFrame(() => root.classList.remove('mm-theme-switching'))); +} + +// 首屏脚本已经定过主题,这里补齐图标/开关状态,并把主题同步给背景 +applyTheme(storedTheme() ?? (prefersDark() ? 'dark' : 'light'), { remember: false }); + +const themeBtn = document.getElementById('mmThemeBtn'); + +if (themeBtn) { + themeBtn.addEventListener('click', () => { + applyTheme(currentTheme() === 'dark' ? 'light' : 'dark'); + }); +} + +// 用户没手动选过时,跟随系统切换 +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => { + if (!storedTheme()) applyTheme(event.matches ? 'dark' : 'light', { remember: false }); +}); + +// iframe 的 src 放在 data-src 里,这里按当前主题挂上(深色直接带 ?night=1, +// 免得先亮后暗地闪一下);之后切换主题只发消息,不重载,官方入场动画不会重播。 +if (bgFrame) { + const baseSrc = bgFrame.getAttribute('data-src') || '/bg.html'; + if (!bgFrame.getAttribute('src')) { + bgFrame.setAttribute('src', currentTheme() === 'dark' ? `${baseSrc}?night=1` : baseSrc); + } + bgFrame.addEventListener('load', () => tellBackground(currentTheme())); +} + +const menuBtn = document.getElementById('mmMenuBtn'); + +if (menuBtn) { + const setOpen = (open) => { + document.body.classList.toggle('mm-menu-open', open); + // html 上也挂一份:只锁 body 拦不住 html 那只滚动条,深色面板右上角会露一截浅色滑块 + document.documentElement.classList.toggle('mm-menu-open', open); + menuBtn.setAttribute('aria-expanded', String(open)); + }; + menuBtn.addEventListener('click', () => setOpen(!document.body.classList.contains('mm-menu-open'))); + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') setOpen(false); + }); + // 窄屏展开、随后把窗口拉宽的话:菜单按钮这时已经隐藏,锁滚动的那条规则也出了媒体查询, + // 不收起来就会卡在一个看不见、又关不掉的展开态上 + window.matchMedia('(min-width: 1001px)').addEventListener('change', (event) => { + if (event.matches) setOpen(false); + }); + // 菜单里点走一项就收起来,否则新页面加载前会停在展开态 + document.querySelectorAll('.site-header .nav a').forEach((link) => { + link.addEventListener('click', () => setOpen(false)); + }); +} + +const topLink = document.querySelector('.mm-top'); + +if (topLink) { + const sync = () => topLink.classList.toggle('off', window.scrollY < 600); + sync(); + window.addEventListener('scroll', sync, { passive: true }); + topLink.addEventListener('click', (event) => { + event.preventDefault(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); +} \ No newline at end of file diff --git a/public/vendor/maimai/README.md b/public/vendor/maimai/README.md new file mode 100644 index 0000000..75bab1d --- /dev/null +++ b/public/vendor/maimai/README.md @@ -0,0 +1,37 @@ +# vendor/maimai —— maimai DX 国际版官网素材(PRiSM PLUS 时期) + +前端重构的参照物:`maimai DX International ver.` 官网在 **PRiSM PLUS** 时期的样式与皮肤素材。 +这里只放「原样搬过来」的东西;把它落到本站组件上的那一层在 [`public/theme.css`](../../theme.css)。 + +## 来源 + +| 文件 | 原始地址 | 快照 | +| --- | --- | --- | +| `style.css` | `https://maimai.sega.com/assets/css/style.css` | `web.archive.org/web/20250804084236` | +| `img/**`(92 个) | `https://maimai.sega.com/assets/img/**` | 同上(线上仍在,直接抓线上) | +| `fonts/**` + `fonts.css` | `https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@400;500;700;800;900` | 2026-10-10 抓取 | + +归档页面本身(`https://web.archive.org/web/20250804084236/https://maimai.sega.com/`)与线上现站 +(已更新到 CiRCLE PLUS)**样式表不同**:PRiSM PLUS 版的 `.common--box` 是上紫 `#ba5eea` / +下粉 `#ffcedf`,CiRCLE PLUS 版换成了 `#f93eac` / `#6dbefe`。所以这里以归档快照为准。 + +## 对原文件做了什么 + +- `style.css`:只改了 URL。wayback 前缀与 `https://maimai.sega.com/assets/img/` 全部改写成 + `/vendor/maimai/img/`(73 处),Google Fonts 的 `@import` 换成注释(字体改为自托管, + 见 `fonts.css`)。**选择器与数值一个都没动**,要核对官方参数直接在这个文件里查。 + 归档页还在文件末尾追加了两段 wayback 自己的注释(版权声明与 playback timings), + 不属于官方文件,已删掉。 +- `img/**`:原始字节,未做任何压缩或转码。`img/MANIFEST.txt` 记录每个文件的字节数与 MD5。 +- `fonts.css`:只自托管 latin / latin-ext 子集(5 个字重 × 2 子集,共 10 个 woff2)。 + M PLUS 1p 没有简体字,中文由 `theme.css` 里的系统字体栈回退。另外补了一个 `"M PLUS 1p Symbols"` + 家族:M PLUS 1p 的 latin 子集只覆盖 ↑↓ 两个箭头,↗ ✓ ▲ 这类符号会被画成一根没有箭头的 + 斜线,所以只把那些码位圈出来交给系统符号字体(`theme.css` 的 `--mm-sans` 排在 M PLUS 1p 之后)。 +- 本站的背景宿主页 `public/bg.html` 不放在这里:它带着自己那三行 reset 和夜版切换脚本, + 放在 `public/` 根下,好让这个目录只留「原样搬过来」的素材。 + +## 授权 + +- `M PLUS 1p` 是 SIL Open Font License 1.1,可自由再分发。 +- `style.css` 与 `img/**` 是 **SEGA 的版权素材**,这里作为个人自用项目的样式参照/复刻保留, + 不属于 MIT 授权范围,也不代表 SEGA 授权。若要公开发布,请自行替换或取得许可。 \ No newline at end of file diff --git a/public/vendor/maimai/fonts.css b/public/vendor/maimai/fonts.css new file mode 100644 index 0000000..9545824 --- /dev/null +++ b/public/vendor/maimai/fonts.css @@ -0,0 +1,103 @@ +/* maimai DX International 官网(PRiSM PLUS 时期)使用的字体:M PLUS 1p(SIL OFL 1.1)。 + 这里只自托管 latin / latin-ext 子集(数字与拉丁字形是官方观感的关键), + 中日韩字形由 theme.css 里的系统字体栈回退。 + 来源: https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@400;500;700;800;900 抓取于 2026-10-10 */ + +/* M PLUS 1p 的 latin 子集只覆盖 U+2191/U+2193 两个箭头,↗ ✓ ▲ 这类符号会被它画成一根 + 没有箭头的斜线。这里单独声明一个只圈「M PLUS 1p 没覆盖的符号码位」的家族, + 交给系统符号字体渲染;其余字符仍旧走 M PLUS 1p。 */ +@font-face { + font-family: "M PLUS 1p Symbols"; + src: local("Segoe UI Symbol"), local("Segoe UI"), local("Apple Symbols"), local("Noto Sans Symbols 2"); + unicode-range: U+2190, U+2192, U+2194-21FF, U+25A0-27BF, U+2B00-2BFF, U+FE0F; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-400-latin-ext.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-400-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-500-latin-ext.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-500-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-700-latin-ext.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-700-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-800-latin-ext.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-800-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-900-latin-ext.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: "M PLUS 1p"; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("/vendor/maimai/fonts/mplus1p-900-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/public/vendor/maimai/fonts/mplus1p-400-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-400-latin-ext.woff2 new file mode 100644 index 0000000..4e06334 Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-400-latin-ext.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-400-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-400-latin.woff2 new file mode 100644 index 0000000..ac77e19 Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-400-latin.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-500-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-500-latin-ext.woff2 new file mode 100644 index 0000000..636c799 Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-500-latin-ext.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-500-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-500-latin.woff2 new file mode 100644 index 0000000..cfa6e04 Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-500-latin.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-700-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-700-latin-ext.woff2 new file mode 100644 index 0000000..1b8d09e Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-700-latin-ext.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-700-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-700-latin.woff2 new file mode 100644 index 0000000..b2eb083 Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-700-latin.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-800-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-800-latin-ext.woff2 new file mode 100644 index 0000000..d0c3b48 Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-800-latin-ext.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-800-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-800-latin.woff2 new file mode 100644 index 0000000..cc8f341 Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-800-latin.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-900-latin-ext.woff2 b/public/vendor/maimai/fonts/mplus1p-900-latin-ext.woff2 new file mode 100644 index 0000000..66f2b0e Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-900-latin-ext.woff2 differ diff --git a/public/vendor/maimai/fonts/mplus1p-900-latin.woff2 b/public/vendor/maimai/fonts/mplus1p-900-latin.woff2 new file mode 100644 index 0000000..8f3a39e Binary files /dev/null and b/public/vendor/maimai/fonts/mplus1p-900-latin.woff2 differ diff --git a/public/vendor/maimai/img/MANIFEST.txt b/public/vendor/maimai/img/MANIFEST.txt new file mode 100644 index 0000000..5326d0a --- /dev/null +++ b/public/vendor/maimai/img/MANIFEST.txt @@ -0,0 +1,94 @@ +source: https://maimai.sega.com/assets/img/ (PRiSM PLUS era) +captured: 2026-10-10 +common/arrow.svg 329 cd00d3b2a1aa2e2b5949f52dad24a558 +common/arrow_double.svg 446 0b5879f64d05aa1dc3e9fdc23ad55348 +common/download.png 4205 827094c65cef3383d9bb6dec7d876227 +common/icon_blank.png 175 89c27d1b7cd4fc7bb82ff39610665523 +common/icon_download.png 1258 b86c9e5c949b768dccc6d02da8e76838 +common/icon_next.png 667 705ee4253d5288fb297c76901452ecd4 +common/icon_select.png 3199 c03b0478d7a33f1bb4207464f26a610e +common/top.png 27682 00c901d61c45b23c51b37d87edcb91ca +favicon.png 6608 ef1d521081485dcd0d243f5bf923b217 +footer/footer_banner.jpg 39146 13a49137d35613da2cbb54a7ca36c47f +footer/footer_logo.png 5910 44e2b3c77749e055c5715474dd9c9e23 +footer/footer_sns_fb.png 2601 2882e1d277bb11d37fc2e68fc56cbf83 +footer/footer_sns_li.png 3497 19b49a359ac818230aff99911d17dfda +footer/footer_sns_x.png 3556 9f88bbfd72fee27ad8125598dd58897a +header/btn_arrow.png 1062 a6668541db9e0c9ca0a032181a06e372 +header/btn_arrow.svg 257 1c4a56dc66e329e5a6ea4985215d931d +ogp.png 308605 539bb531128070267670e0ccb8bb9148 +play/dash.png 214 0f646c1459e9c36b7cc3c1c43480f3ba +prism/bg/bg_aurora_pc.png 77561 5497b5bd7905e9fd74b587068cb860a7 +prism/bg/bg_aurora_sp.png 57495 e97a42bf83603df79ca0915345cfc4e2 +prism/bg/bg_cloud_back_c.png 2481 9911f23ca057640c1edc7ea15d027e50 +prism/bg/bg_cloud_back_l.png 3159 f00ef39719406641db4d8d3e9d991461 +prism/bg/bg_cloud_back_r.png 6380 2323f4753d9236f02bde7c878911dcb1 +prism/bg/bg_cloud_front_c.png 4211 11fba18997bdc5926abeb6cc4049c49c +prism/bg/bg_cloud_front_l.png 4314 a6f46856c6b759ad45a37dd483b4bc8c +prism/bg/bg_cloud_front_r.png 4107 f8f985b0df2581ca61485c3ce990a631 +prism/bg/bg_diamond_pink.png 293 48ba7b62ea9c739446893df834f5ff49 +prism/bg/bg_diamond_white.png 207 a6b102103edae42cd6329c2791e3c25b +prism/bg/bg_diamond_yellow.png 207 bbcc23a8e51be8b5e432614d70ce31a5 +prism/bg/bg_moon.png 14556 f939430c51d1544b10a5dabba4540d60 +prism/bg/bg_pattern.png 227 2fd46e9c9ddcec95cff99f0414791b2e +prism/bg/bg_rainbow_back_sp.png 6257 02c4872894e69365e2a4a7a97df14032 +prism/bg/bg_rainbow_btm_pc.png 13015 e6da0e89ec1d0fcbb0f4f3ca9cd3b522 +prism/bg/bg_rainbow_btm_sp.png 7951 634fef106b9c6cd363087d36beec21de +prism/bg/bg_rainbow_pc.png 14960 809799f6ed17a1c12d1926515672a16d +prism/bg/bg_rainbow_sp.png 13011 63207806f9db589b0df05ff34e1271b0 +prism/bg/bg_shines_pc.png 6610 b1450d76088e46622af0d689ad6ce979 +prism/bg/bg_shines_sp.png 5787 8d4c977e41e5f171db8ef7eb6301e72a +prism/bg/bg_shootingstar_head.png 1150 43eb091b339f87e0bbeec39cb70fe8a0 +prism/bg/bg_shootingstar_head_green.png 2442 6e60786ee48c82228519b0def2f16337 +prism/bg/bg_shootingstar_head_pink.png 2436 55e939f2f8a2ee7741d0d980c7e0d735 +prism/bg/bg_shootingstar_tail.png 3563 945fa11339fbff7bafb853fecba7eb59 +prism/bg/bg_shootingstar_tail_green.png 7624 65ac85eeba3c2aa6feb62b1a7ba525b4 +prism/bg/bg_shootingstar_tail_pink.png 8409 c091f0c3f4631212e3a111712ca91d30 +prism/bg/bg_star_white.png 2350 ce64b60ef33d71493b63a677744cdb1b +prism/bg/bg_star_yellow.png 2347 43b38e1237f59cb20a886528e69e0440 +prism/common/icon_header_left.svg 594 4fea9e2436d2fd407ea5c17553d90a76 +prism/common/icon_header_right.svg 592 f0ce643895d280ace29012e1a36c3a53 +prism/common/logo.png 25044 30e42459cafa4f48238b2666b37f1419 +prism/top/kv.png 227129 dac17b4963c92b8189bc7cd0a4386eaf +prism/top/kv_logo.png 88199 0bd8f99caf8b1ef1f7d405a9c33ae2de +prism/top/kv_logo_pc.png 188016 e76e9cb6373a9d0c2abc7a1fe0820c6a +prism/top/kv_pc.png 392031 8d611c4e3891b3d494003f1f631b3bbd +ranking/line_dashed_pc.png 2954 01ae4b1f39717dbc571ce45d293505e3 +ranking/line_dashed_sp.png 2890 984331ca21584984a04f7b0d040e8a2d +ranking/rank_num_bz0.png 5181 bef6dddc56330c8b4fec77536783e62e +ranking/rank_num_bz1.png 3237 03ad6e320c0f8d764b65691283efd548 +ranking/rank_num_bz100.png 6445 3509e9bb8f3222ab9408c6c5a0b0f3ff +ranking/rank_num_bz2.png 4748 2ecbfca66420d7201f573e84e26ae9b6 +ranking/rank_num_bz3.png 5352 a7ed94f7217f12a6e90a4be4794085f1 +ranking/rank_num_bz4.png 3870 ad8faeaf926db2a73e004e369deb6045 +ranking/rank_num_bz5.png 4771 cdf683c607f29f4d0e14f303edba88bf +ranking/rank_num_bz6.png 5388 28f21950f383f668b274e235cb7c4405 +ranking/rank_num_bz7.png 4152 7fa594a19a9b17cdcdf006fdd02c4e44 +ranking/rank_num_bz8.png 5407 2b07f8d98eab2b6382404297fee0afeb +ranking/rank_num_bz9.png 5477 a7765a53524058e5761c0f0f22ba63ed +ranking/rank_num_gold1.png 3490 a993aae223f0614911c8e72cb91448d0 +ranking/rank_num_sv0.png 5575 29ece737de4f96c10342b1cbc21ff122 +ranking/rank_num_sv1.png 3561 b756bc38f3dc43859e16698dd40b1444 +ranking/rank_num_sv2.png 5113 24bc53a9202110055c1da4d71f56b903 +ranking/rank_num_sv3.png 5719 72ba8459a334b1cee688eb6c664193d5 +ranking/rank_num_sv4.png 4293 5c80541f3d06205c4628542b35118b72 +ranking/rank_num_sv5.png 5092 a8780b5c47e721e7469d4f1bc37879be +ranking/rank_num_sv6.png 5779 86a8bf17c35a2ee159c07fe67ea7e511 +ranking/rank_num_sv7.png 4481 4a069b41c5cab1418528b4779b096f2a +ranking/rank_num_sv8.png 5822 22e8fc1381aefc6b512d8b3d63aded97 +ranking/rank_num_sv9.png 5802 f8da52b0d1b47620b628e7251ae86de4 +song/arrow.svg 255 29e1504d49b8b0da15a9a6b3266f4c93 +song/check.svg 214 24c8fa70832962e419b805d7daf04e7d +song/inner_dx.png 3740 0b100bf7f84ca28288f6ffccfa8886c4 +song/inner_st.png 3400 9a579e3a5bdc52a9840851aa20c2331d +song/lock.png 8026 baa6e48d518823bc1106c55c289d1f95 +song/piapro_logo.png 1650 8f4b97ddf187f2bd78cda20d5239752f +song/plus.png 913 ca9172a51a3dc49351097141bdaa0280 +song/search_del.png 1082 196405ba565bcc07f1fff571c8f98d5c +song/search_lens.svg 549 543411a2724886c04b9ed7a481d1af57 +song/utage/icon_buddy.png 3312 bf55c0de2f42a4d7515e874fe1504c94 +top/btn_chu.png 69468 48331d4bb9cd1da95b399cc1ab212024 +top/btn_top_1.png 12883 474aa55b03dd50d68166ceb0a105af39 +top/btn_top_2.png 13054 52cdb1892cc968c74e9b728aa46a6d90 +top/btn_top_4.png 21799 2e730a41deca13012068fdf12d5c9c5d +top/btn_top_kop.png 50820 b9ab4e5b5bb9dbbaeb49afd0af70745b diff --git a/public/vendor/maimai/img/common/arrow.svg b/public/vendor/maimai/img/common/arrow.svg new file mode 100644 index 0000000..085ef2d --- /dev/null +++ b/public/vendor/maimai/img/common/arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vendor/maimai/img/common/arrow_double.svg b/public/vendor/maimai/img/common/arrow_double.svg new file mode 100644 index 0000000..202e592 --- /dev/null +++ b/public/vendor/maimai/img/common/arrow_double.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vendor/maimai/img/common/download.png b/public/vendor/maimai/img/common/download.png new file mode 100644 index 0000000..ffbc8b0 Binary files /dev/null and b/public/vendor/maimai/img/common/download.png differ diff --git a/public/vendor/maimai/img/common/icon_blank.png b/public/vendor/maimai/img/common/icon_blank.png new file mode 100644 index 0000000..d337eb3 Binary files /dev/null and b/public/vendor/maimai/img/common/icon_blank.png differ diff --git a/public/vendor/maimai/img/common/icon_download.png b/public/vendor/maimai/img/common/icon_download.png new file mode 100644 index 0000000..0110133 Binary files /dev/null and b/public/vendor/maimai/img/common/icon_download.png differ diff --git a/public/vendor/maimai/img/common/icon_next.png b/public/vendor/maimai/img/common/icon_next.png new file mode 100644 index 0000000..ccc9dcb Binary files /dev/null and b/public/vendor/maimai/img/common/icon_next.png differ diff --git a/public/vendor/maimai/img/common/icon_select.png b/public/vendor/maimai/img/common/icon_select.png new file mode 100644 index 0000000..5d3f7d8 Binary files /dev/null and b/public/vendor/maimai/img/common/icon_select.png differ diff --git a/public/vendor/maimai/img/common/top.png b/public/vendor/maimai/img/common/top.png new file mode 100644 index 0000000..3ded60e Binary files /dev/null and b/public/vendor/maimai/img/common/top.png differ diff --git a/public/vendor/maimai/img/favicon.png b/public/vendor/maimai/img/favicon.png new file mode 100644 index 0000000..1d5328f Binary files /dev/null and b/public/vendor/maimai/img/favicon.png differ diff --git a/public/vendor/maimai/img/footer/footer_banner.jpg b/public/vendor/maimai/img/footer/footer_banner.jpg new file mode 100644 index 0000000..2fa547c Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_banner.jpg differ diff --git a/public/vendor/maimai/img/footer/footer_logo.png b/public/vendor/maimai/img/footer/footer_logo.png new file mode 100644 index 0000000..ddbbf88 Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_logo.png differ diff --git a/public/vendor/maimai/img/footer/footer_sns_fb.png b/public/vendor/maimai/img/footer/footer_sns_fb.png new file mode 100644 index 0000000..9598222 Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_sns_fb.png differ diff --git a/public/vendor/maimai/img/footer/footer_sns_li.png b/public/vendor/maimai/img/footer/footer_sns_li.png new file mode 100644 index 0000000..1e90faf Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_sns_li.png differ diff --git a/public/vendor/maimai/img/footer/footer_sns_x.png b/public/vendor/maimai/img/footer/footer_sns_x.png new file mode 100644 index 0000000..0eaa1e2 Binary files /dev/null and b/public/vendor/maimai/img/footer/footer_sns_x.png differ diff --git a/public/vendor/maimai/img/header/btn_arrow.png b/public/vendor/maimai/img/header/btn_arrow.png new file mode 100644 index 0000000..9b64476 Binary files /dev/null and b/public/vendor/maimai/img/header/btn_arrow.png differ diff --git a/public/vendor/maimai/img/header/btn_arrow.svg b/public/vendor/maimai/img/header/btn_arrow.svg new file mode 100644 index 0000000..9492485 --- /dev/null +++ b/public/vendor/maimai/img/header/btn_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vendor/maimai/img/ogp.png b/public/vendor/maimai/img/ogp.png new file mode 100644 index 0000000..5a1ae75 Binary files /dev/null and b/public/vendor/maimai/img/ogp.png differ diff --git a/public/vendor/maimai/img/play/dash.png b/public/vendor/maimai/img/play/dash.png new file mode 100644 index 0000000..6d3d38e Binary files /dev/null and b/public/vendor/maimai/img/play/dash.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_aurora_pc.png b/public/vendor/maimai/img/prism/bg/bg_aurora_pc.png new file mode 100644 index 0000000..fc884b2 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_aurora_pc.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_aurora_sp.png b/public/vendor/maimai/img/prism/bg/bg_aurora_sp.png new file mode 100644 index 0000000..95971e1 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_aurora_sp.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_back_c.png b/public/vendor/maimai/img/prism/bg/bg_cloud_back_c.png new file mode 100644 index 0000000..32552b9 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_back_c.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_back_l.png b/public/vendor/maimai/img/prism/bg/bg_cloud_back_l.png new file mode 100644 index 0000000..eb0efe3 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_back_l.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_back_r.png b/public/vendor/maimai/img/prism/bg/bg_cloud_back_r.png new file mode 100644 index 0000000..078b9a2 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_back_r.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_front_c.png b/public/vendor/maimai/img/prism/bg/bg_cloud_front_c.png new file mode 100644 index 0000000..bbc3017 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_front_c.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_front_l.png b/public/vendor/maimai/img/prism/bg/bg_cloud_front_l.png new file mode 100644 index 0000000..242e7ab Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_front_l.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_cloud_front_r.png b/public/vendor/maimai/img/prism/bg/bg_cloud_front_r.png new file mode 100644 index 0000000..fb437a2 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_cloud_front_r.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_diamond_pink.png b/public/vendor/maimai/img/prism/bg/bg_diamond_pink.png new file mode 100644 index 0000000..6bead5c Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_diamond_pink.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_diamond_white.png b/public/vendor/maimai/img/prism/bg/bg_diamond_white.png new file mode 100644 index 0000000..0ce4a74 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_diamond_white.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_diamond_yellow.png b/public/vendor/maimai/img/prism/bg/bg_diamond_yellow.png new file mode 100644 index 0000000..acd09a4 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_diamond_yellow.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_moon.png b/public/vendor/maimai/img/prism/bg/bg_moon.png new file mode 100644 index 0000000..1e29b93 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_moon.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_pattern.png b/public/vendor/maimai/img/prism/bg/bg_pattern.png new file mode 100644 index 0000000..110d6ec Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_pattern.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_back_sp.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_back_sp.png new file mode 100644 index 0000000..7e56299 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_back_sp.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_pc.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_pc.png new file mode 100644 index 0000000..148fea7 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_pc.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_sp.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_sp.png new file mode 100644 index 0000000..8be6192 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_btm_sp.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_pc.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_pc.png new file mode 100644 index 0000000..959322b Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_pc.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_rainbow_sp.png b/public/vendor/maimai/img/prism/bg/bg_rainbow_sp.png new file mode 100644 index 0000000..02eceb0 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_rainbow_sp.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_shines_pc.png b/public/vendor/maimai/img/prism/bg/bg_shines_pc.png new file mode 100644 index 0000000..5ac4fb6 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shines_pc.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_shines_sp.png b/public/vendor/maimai/img/prism/bg/bg_shines_sp.png new file mode 100644 index 0000000..006cf75 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shines_sp.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_head.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head.png new file mode 100644 index 0000000..71c9e4f Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_green.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_green.png new file mode 100644 index 0000000..89a9870 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_green.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_pink.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_pink.png new file mode 100644 index 0000000..4e42d4b Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_head_pink.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail.png new file mode 100644 index 0000000..80496f4 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_green.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_green.png new file mode 100644 index 0000000..247c9d8 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_green.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_pink.png b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_pink.png new file mode 100644 index 0000000..c08c4e8 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_shootingstar_tail_pink.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_star_white.png b/public/vendor/maimai/img/prism/bg/bg_star_white.png new file mode 100644 index 0000000..f35da4c Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_star_white.png differ diff --git a/public/vendor/maimai/img/prism/bg/bg_star_yellow.png b/public/vendor/maimai/img/prism/bg/bg_star_yellow.png new file mode 100644 index 0000000..dad9408 Binary files /dev/null and b/public/vendor/maimai/img/prism/bg/bg_star_yellow.png differ diff --git a/public/vendor/maimai/img/prism/common/icon_header_left.svg b/public/vendor/maimai/img/prism/common/icon_header_left.svg new file mode 100644 index 0000000..014f7dc --- /dev/null +++ b/public/vendor/maimai/img/prism/common/icon_header_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vendor/maimai/img/prism/common/icon_header_right.svg b/public/vendor/maimai/img/prism/common/icon_header_right.svg new file mode 100644 index 0000000..1ab4991 --- /dev/null +++ b/public/vendor/maimai/img/prism/common/icon_header_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vendor/maimai/img/prism/common/logo.png b/public/vendor/maimai/img/prism/common/logo.png new file mode 100644 index 0000000..74ae960 Binary files /dev/null and b/public/vendor/maimai/img/prism/common/logo.png differ diff --git a/public/vendor/maimai/img/prism/top/kv.png b/public/vendor/maimai/img/prism/top/kv.png new file mode 100644 index 0000000..a58b99c Binary files /dev/null and b/public/vendor/maimai/img/prism/top/kv.png differ diff --git a/public/vendor/maimai/img/prism/top/kv_logo.png b/public/vendor/maimai/img/prism/top/kv_logo.png new file mode 100644 index 0000000..68b81cd Binary files /dev/null and b/public/vendor/maimai/img/prism/top/kv_logo.png differ diff --git a/public/vendor/maimai/img/prism/top/kv_logo_pc.png b/public/vendor/maimai/img/prism/top/kv_logo_pc.png new file mode 100644 index 0000000..7255a63 Binary files /dev/null and b/public/vendor/maimai/img/prism/top/kv_logo_pc.png differ diff --git a/public/vendor/maimai/img/prism/top/kv_pc.png b/public/vendor/maimai/img/prism/top/kv_pc.png new file mode 100644 index 0000000..57bd82c Binary files /dev/null and b/public/vendor/maimai/img/prism/top/kv_pc.png differ diff --git a/public/vendor/maimai/img/ranking/line_dashed_pc.png b/public/vendor/maimai/img/ranking/line_dashed_pc.png new file mode 100644 index 0000000..18f61dc Binary files /dev/null and b/public/vendor/maimai/img/ranking/line_dashed_pc.png differ diff --git a/public/vendor/maimai/img/ranking/line_dashed_sp.png b/public/vendor/maimai/img/ranking/line_dashed_sp.png new file mode 100644 index 0000000..9ce5c33 Binary files /dev/null and b/public/vendor/maimai/img/ranking/line_dashed_sp.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz0.png b/public/vendor/maimai/img/ranking/rank_num_bz0.png new file mode 100644 index 0000000..b104593 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz0.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz1.png b/public/vendor/maimai/img/ranking/rank_num_bz1.png new file mode 100644 index 0000000..9f42309 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz1.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz100.png b/public/vendor/maimai/img/ranking/rank_num_bz100.png new file mode 100644 index 0000000..31c333b Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz100.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz2.png b/public/vendor/maimai/img/ranking/rank_num_bz2.png new file mode 100644 index 0000000..f08a927 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz2.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz3.png b/public/vendor/maimai/img/ranking/rank_num_bz3.png new file mode 100644 index 0000000..cbbc01c Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz3.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz4.png b/public/vendor/maimai/img/ranking/rank_num_bz4.png new file mode 100644 index 0000000..75cad0f Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz4.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz5.png b/public/vendor/maimai/img/ranking/rank_num_bz5.png new file mode 100644 index 0000000..ab16905 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz5.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz6.png b/public/vendor/maimai/img/ranking/rank_num_bz6.png new file mode 100644 index 0000000..3109408 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz6.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz7.png b/public/vendor/maimai/img/ranking/rank_num_bz7.png new file mode 100644 index 0000000..b56b39c Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz7.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz8.png b/public/vendor/maimai/img/ranking/rank_num_bz8.png new file mode 100644 index 0000000..642c8c1 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz8.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_bz9.png b/public/vendor/maimai/img/ranking/rank_num_bz9.png new file mode 100644 index 0000000..33cd78e Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_bz9.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_gold1.png b/public/vendor/maimai/img/ranking/rank_num_gold1.png new file mode 100644 index 0000000..eed8b6b Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_gold1.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv0.png b/public/vendor/maimai/img/ranking/rank_num_sv0.png new file mode 100644 index 0000000..3b994c4 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv0.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv1.png b/public/vendor/maimai/img/ranking/rank_num_sv1.png new file mode 100644 index 0000000..4816375 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv1.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv2.png b/public/vendor/maimai/img/ranking/rank_num_sv2.png new file mode 100644 index 0000000..c469e24 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv2.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv3.png b/public/vendor/maimai/img/ranking/rank_num_sv3.png new file mode 100644 index 0000000..ffb29b6 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv3.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv4.png b/public/vendor/maimai/img/ranking/rank_num_sv4.png new file mode 100644 index 0000000..9cd5dc1 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv4.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv5.png b/public/vendor/maimai/img/ranking/rank_num_sv5.png new file mode 100644 index 0000000..212ed60 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv5.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv6.png b/public/vendor/maimai/img/ranking/rank_num_sv6.png new file mode 100644 index 0000000..4f703e6 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv6.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv7.png b/public/vendor/maimai/img/ranking/rank_num_sv7.png new file mode 100644 index 0000000..c9a5d77 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv7.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv8.png b/public/vendor/maimai/img/ranking/rank_num_sv8.png new file mode 100644 index 0000000..9b19a10 Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv8.png differ diff --git a/public/vendor/maimai/img/ranking/rank_num_sv9.png b/public/vendor/maimai/img/ranking/rank_num_sv9.png new file mode 100644 index 0000000..280574d Binary files /dev/null and b/public/vendor/maimai/img/ranking/rank_num_sv9.png differ diff --git a/public/vendor/maimai/img/song/arrow.svg b/public/vendor/maimai/img/song/arrow.svg new file mode 100644 index 0000000..7b5af85 --- /dev/null +++ b/public/vendor/maimai/img/song/arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vendor/maimai/img/song/check.svg b/public/vendor/maimai/img/song/check.svg new file mode 100644 index 0000000..1bb791f --- /dev/null +++ b/public/vendor/maimai/img/song/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vendor/maimai/img/song/inner_dx.png b/public/vendor/maimai/img/song/inner_dx.png new file mode 100644 index 0000000..ae4a54a Binary files /dev/null and b/public/vendor/maimai/img/song/inner_dx.png differ diff --git a/public/vendor/maimai/img/song/inner_st.png b/public/vendor/maimai/img/song/inner_st.png new file mode 100644 index 0000000..774dd42 Binary files /dev/null and b/public/vendor/maimai/img/song/inner_st.png differ diff --git a/public/vendor/maimai/img/song/lock.png b/public/vendor/maimai/img/song/lock.png new file mode 100644 index 0000000..9937467 Binary files /dev/null and b/public/vendor/maimai/img/song/lock.png differ diff --git a/public/vendor/maimai/img/song/piapro_logo.png b/public/vendor/maimai/img/song/piapro_logo.png new file mode 100644 index 0000000..7777559 Binary files /dev/null and b/public/vendor/maimai/img/song/piapro_logo.png differ diff --git a/public/vendor/maimai/img/song/plus.png b/public/vendor/maimai/img/song/plus.png new file mode 100644 index 0000000..6f7cc03 Binary files /dev/null and b/public/vendor/maimai/img/song/plus.png differ diff --git a/public/vendor/maimai/img/song/search_del.png b/public/vendor/maimai/img/song/search_del.png new file mode 100644 index 0000000..f1f3736 Binary files /dev/null and b/public/vendor/maimai/img/song/search_del.png differ diff --git a/public/vendor/maimai/img/song/search_lens.svg b/public/vendor/maimai/img/song/search_lens.svg new file mode 100644 index 0000000..5eeac2e --- /dev/null +++ b/public/vendor/maimai/img/song/search_lens.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/public/vendor/maimai/img/song/utage/icon_buddy.png b/public/vendor/maimai/img/song/utage/icon_buddy.png new file mode 100644 index 0000000..5390767 Binary files /dev/null and b/public/vendor/maimai/img/song/utage/icon_buddy.png differ diff --git a/public/vendor/maimai/img/top/btn_chu.png b/public/vendor/maimai/img/top/btn_chu.png new file mode 100644 index 0000000..1f6ee2c Binary files /dev/null and b/public/vendor/maimai/img/top/btn_chu.png differ diff --git a/public/vendor/maimai/img/top/btn_top_1.png b/public/vendor/maimai/img/top/btn_top_1.png new file mode 100644 index 0000000..0346248 Binary files /dev/null and b/public/vendor/maimai/img/top/btn_top_1.png differ diff --git a/public/vendor/maimai/img/top/btn_top_2.png b/public/vendor/maimai/img/top/btn_top_2.png new file mode 100644 index 0000000..ea8efeb Binary files /dev/null and b/public/vendor/maimai/img/top/btn_top_2.png differ diff --git a/public/vendor/maimai/img/top/btn_top_4.png b/public/vendor/maimai/img/top/btn_top_4.png new file mode 100644 index 0000000..8126ef7 Binary files /dev/null and b/public/vendor/maimai/img/top/btn_top_4.png differ diff --git a/public/vendor/maimai/img/top/btn_top_kop.png b/public/vendor/maimai/img/top/btn_top_kop.png new file mode 100644 index 0000000..6587cba Binary files /dev/null and b/public/vendor/maimai/img/top/btn_top_kop.png differ diff --git a/public/vendor/maimai/style.css b/public/vendor/maimai/style.css new file mode 100644 index 0000000..94e2e35 --- /dev/null +++ b/public/vendor/maimai/style.css @@ -0,0 +1 @@ +@charset "UTF-8";/* 字体自托管:见 /vendor/maimai/fonts.css */input[type=text]{margin:0;padding:0;border:0;background-color:#fff;outline:0}.common--box{position:relative;width:87.5vw;border-radius:3.75vw;box-shadow:1.75vw 1.75vw 0 0 rgba(0,0,0,.2);background-color:#fff;margin:0 auto;padding:8.125vw 0}@media only screen and (min-width:769px){.common--box{width:81.8181818182vw;border-radius:2.7272727273vw;box-shadow:1.2727272727vw 1.2727272727vw 0 0 rgba(0,0,0,.2);padding:4.5454545455vw 0}}@media only screen and (min-width:1100px){.common--box{width:900px;border-radius:30px;box-shadow:14px 14px 0 0 rgba(0,0,0,.2);padding:50px 0}}.common--box:after,.common--box:before{position:absolute;display:block;content:"";width:75vw;height:1.5vw;left:50%;transform:translateX(-50%)}@media only screen and (min-width:769px){.common--box:after,.common--box:before{width:54.5454545455vw;height:.7272727273vw}}@media only screen and (min-width:1100px){.common--box:after,.common--box:before{width:600px;height:8px}}.common--box:before{top:0;border-radius:0 0 1.5vw 1.5vw;background-color:#ba5eea}@media only screen and (min-width:769px){.common--box:before{border-radius:0 0 .7272727273vw .7272727273vw}}@media only screen and (min-width:1100px){.common--box:before{border-radius:0 0 8px 8px}}.common--box:after{bottom:0;border-radius:1.5vw 1.5vw 0 0;background-color:#ffcedf}@media only screen and (min-width:769px){.common--box:after{border-radius:.7272727273vw .7272727273vw 0 0}}@media only screen and (min-width:1400px){.common--box:after{border-radius:8px 8px 0 0}}.common--top{position:fixed;z-index:100;display:block;bottom:2.5vw;right:2.5vw;width:15vw;transform:translateY(0);transition:.3s}.common--top.off{opacity:0;transform:translateY(30%);pointer-events:none}@media only screen and (min-width:769px){.common--top{transition:transform .3s}.common--top:hover{transform:translateY(-5%)}}@media only screen and (min-width:769px){.common--top{bottom:1.4285714286vw;right:1.4285714286vw;width:8.5714285714vw;transition:.3s}}@media only screen and (min-width:1400px){.common--top{bottom:20px;right:20px;width:120px}}.common--share{position:relative;width:87.5vw;border-radius:3.75vw;box-shadow:1.75vw 1.75vw 0 0 rgba(0,0,0,.2);background-color:#fff;margin:0 auto;padding:8.125vw 0}@media only screen and (min-width:769px){.common--share{width:81.8181818182vw;border-radius:2.7272727273vw;box-shadow:1.2727272727vw 1.2727272727vw 0 0 rgba(0,0,0,.2);padding:4.5454545455vw 0}}@media only screen and (min-width:1100px){.common--share{width:900px;border-radius:30px;box-shadow:14px 14px 0 0 rgba(0,0,0,.2);padding:50px 0}}.common--share:after,.common--share:before{position:absolute;display:block;content:"";width:75vw;height:1.5vw;left:50%;transform:translateX(-50%)}@media only screen and (min-width:769px){.common--share:after,.common--share:before{width:54.5454545455vw;height:.7272727273vw}}@media only screen and (min-width:1100px){.common--share:after,.common--share:before{width:600px;height:8px}}.common--share:before{top:0;border-radius:0 0 1.5vw 1.5vw;background-color:#ba5eea}@media only screen and (min-width:769px){.common--share:before{border-radius:0 0 .7272727273vw .7272727273vw}}@media only screen and (min-width:1100px){.common--share:before{border-radius:0 0 8px 8px}}.common--share:after{bottom:0;border-radius:1.5vw 1.5vw 0 0;background-color:#ffcedf}@media only screen and (min-width:769px){.common--share:after{border-radius:.7272727273vw .7272727273vw 0 0}}@media only screen and (min-width:1400px){.common--share:after{border-radius:8px 8px 0 0}}.common--header{display:flex;justify-content:center;align-items:center;margin:0 auto}.common--header__txt{margin:0 1.125vw}@media only screen and (min-width:769px){.common--header__txt{margin:0 1.8181818182vw}}@media only screen and (min-width:1400px){.common--header__txt{margin:0 20px}}.common--header__txt svg{width:100%;fill:#0b3871;fill-rule:evenodd}.common--header__icon{background-image:url(/vendor/maimai/img/prism/common/icon_header_left.svg);background-size:contain;background-repeat:no-repeat;background-position:center center;display:block;width:14vw;height:7vw}@media only screen and (min-width:769px){.common--header__icon{width:8vw;height:4vw;margin-bottom:0}}@media only screen and (min-width:1400px){.common--header__icon{width:112px;height:56px}}.common--header__icon.right{background-image:url(/vendor/maimai/img/prism/common/icon_header_right.svg)}.common--header__icon svg{width:100%;height:100%;fill:#f8b73d;fill-rule:evenodd}.common--btn{display:flex;align-items:center;justify-content:center;color:#fff;margin:0 auto;width:61.25vw;height:11.25vw;font-size:4.25vw;border-radius:12.5vw;font-weight:900;background-color:#e72139;box-shadow:0 2vw 0 0 #a51426;position:relative;word-break:break-all;white-space:nowrap}@media only screen and (min-width:769px){.common--btn{width:36.3636363636vw;height:5.4545454545vw;font-size:2.5454545455vw;box-shadow:0 .9090909091vw 0 0 #a51426;transition:.3s}.common--btn:hover{box-shadow:none;transform:translateY(.9090909091vw)}}@media only screen and (min-width:1100px){.common--btn{width:400px;height:60px;font-size:28px;box-shadow:0 10px 0 0 #a51426}.common--btn:hover{transform:translateY(10px)}}.common--btn:after{right:4.375vw}@media only screen and (min-width:769px){.common--btn:after{right:1.8181818182vw}}@media only screen and (min-width:1100px){.common--btn:after{right:20px}}.common--btn.next:after{position:absolute;display:block;content:"";width:2.25vw;height:2.5vw;background-image:url(/vendor/maimai/img/common/icon_next.png);background-size:cover;background-repeat:no-repeat}@media only screen and (min-width:769px){.common--btn.next:after{width:1.6363636364vw;height:1.8181818182vw}}@media only screen and (min-width:1100px){.common--btn.next:after{width:18px;height:20px}}.common--btn.blank:after{position:absolute;display:block;content:"";width:3.25vw;height:2.5vw;background-image:url(/vendor/maimai/img/common/icon_blank.png);background-size:cover;background-repeat:no-repeat}@media only screen and (min-width:769px){.common--btn.blank:after{width:2.3636363636vw;height:1.8181818182vw}}@media only screen and (min-width:1100px){.common--btn.blank:after{width:26px;height:20px}}.common--btn.download:after{position:absolute;display:block;content:"";width:4.5vw;height:5vw;background-image:url(/vendor/maimai/img/common/icon_download.png);background-size:cover;background-repeat:no-repeat}@media only screen and (min-width:769px){.common--btn.download:after{width:1.8181818182vw;height:2vw}}@media only screen and (min-width:1100px){.common--btn.download:after{width:20px;height:22px}}.common--title{padding-top:21.25vw;width:88.25vw;margin:0 auto}@media only screen and (min-width:769px){.common--title{padding-top:0;margin:6.3636363636vw auto 0;width:82.3636363636vw}}@media only screen and (min-width:1100px){.common--title{width:906px;margin:70px auto 0}}@keyframes gradient{0%{background-position:0 0}50%{background-position:0 100%}100%{background-position:0 0}}@keyframes pattern{0%{background-position:0 0}100%{background-position:-20% 20%}}@keyframes diamond{0%{transform:scaleX(1)}80%{transform:scaleX(1)}90%{transform:scaleX(0)}100%{transform:scaleX(1)}}@keyframes star{0%{opacity:0;transform:rotate(0)}20%{opacity:1}40%{opacity:0;transform:rotate(1turn)}100%{opacity:0;transform:rotate(1turn)}}@keyframes auroras{0%{opacity:1}20%{opacity:.6}80%{opacity:.6}100%{opacity:1}}@keyframes aurora{0%{opacity:0;transform:translateX(0)}1%{opacity:.8}80%{opacity:.8}99%{opacity:0;transform:translateX(-100%)}100%{opacity:0;transform:translateX(0)}}@keyframes cloud{0%{opacity:0;transform:translateX(0)}20%{opacity:1}80%{opacity:1}90%{opacity:0;transform:translateX(100%)}100%{opacity:0;transform:translateX(0)}}@keyframes moon{0%{transform:rotate(0)}100%{transform:rotate(30deg)}}@keyframes shooting-star{0%{transform:translateX(0) translateY(0);opacity:0}20%{opacity:1}40%{opacity:1}60%{transform:translateX(-100%) translateY(100%);opacity:0}to{transform:translateX(0) translateY(0);opacity:0}}@keyframes shooting-star-head{0%{transform:rotate(0)}to{transform:rotate(-1turn)}}@keyframes rainbow{0%{-webkit-clip-path:polygon(0 0,0 0,0 100%,0 100%);clip-path:polygon(0 0,0 0,0 100%,0 100%);transform:scale(.8)}to{-webkit-clip-path:polygon(0 0,100% 0,100% 100%,0 100%);clip-path:polygon(0 0,100% 0,100% 100%,0 100%);transform:scale(1)}}@keyframes rainbow-bottom{0%{transform:scale(.8)}to{transform:scale(1)}}.bg{pointer-events:none;background-image:linear-gradient(0deg,#45c7e3,#7dc0fd 10%,#d3a1ff 25%,#ff99c0 50%,#d3a1ff 75%,#7dc0fd 90%,#45c7e3)}.bg.prism{width:100%;height:100%;position:fixed;top:0;left:0;z-index:0;overflow:hidden}.bg--wrap{position:relative;z-index:1;width:100%;height:100%;overflow:hidden}.bg--gradient{position:relative;top:50%;left:50%;width:calc(100% - 5.5vw);height:calc(100% - 5.5vw);border-radius:5vw;background-size:cover;background-image:linear-gradient(0deg,#fff 14%,#ffd5cf 24%,#ffd5cf 46%,#ffc5d5 56%,#eaabff 67%,#72bcfe 85%,#65f2df 95%);overflow:hidden;z-index:-99;transform:translate3d(-50%,-50%,0);animation:gradient 10s infinite linear}@media only screen and (min-width:769px){.bg--gradient{width:calc(100% - 3.1428571429vw);height:calc(100% - 3.1428571429vw);border-radius:2.8571428571vw}}.bg--gradient:before{content:"";display:block;background:transparent url(/vendor/maimai/img/prism/bg/bg_pattern.png) repeat;width:100%;height:100%;background-size:6.5vw auto;background-position:50%;position:fixed;top:0;left:0;mix-blend-mode:overlay;animation:pattern 10s infinite linear}@media only screen and (min-width:769px){.bg--gradient:before{background-size:3.7142857143vw auto}}@media only screen and (min-width:1400px){.bg--gradient:before{background-size:52px auto}}.bg--shine{position:fixed;top:0;left:0;width:100%;height:100%;z-index:3;background-image:url(/vendor/maimai/img/prism/bg/bg_shines_sp.png);background-repeat:repeat;background-size:95vw auto;background-position:center 7vw}@media only screen and (min-width:769px){.bg--shine{background-image:url(/vendor/maimai/img/prism/bg/bg_shines_pc.png);background-size:95.1428571429vw;background-position:center 1.7142857143vw}}@media only screen and (min-width:1400px){.bg--shine{background-size:1332px;background-position:center 24px}}.bg--shine__wrap{position:relative;width:100%;height:100%}.bg--shine__item{position:absolute;width:5vw;height:5vw;top:0;left:0;background-size:contain;background-repeat:no-repeat}@media only screen and (min-width:769px){.bg--shine__item{width:1.4285714286vw;height:1.4285714286vw}}@media only screen and (min-width:1400px){.bg--shine__item{width:20px;height:20px}}.bg--shine__item.diamond-pink{background-image:url(/vendor/maimai/img/prism/bg/bg_diamond_pink.png);top:9vw;left:68.25vw;animation:diamond 3s infinite 1s linear}@media only screen and (max-width:769px){.bg--shine__item.diamond-pink{width:3vw;height:3vw}}@media only screen and (min-width:769px){.bg--shine__item.diamond-pink{top:2.2857142857vw;left:23.1428571429vw}}@media only screen and (min-width:1400px){.bg--shine__item.diamond-pink{top:32px;left:324px}}.bg--shine__item.diamond-yellow{background-image:url(/vendor/maimai/img/prism/bg/bg_diamond_yellow.png);top:6.5vw;left:74.5vw;animation:diamond 3s infinite 2s linear}@media only screen and (min-width:769px){.bg--shine__item.diamond-yellow{top:21.8571428571vw;left:9.7142857143vw}}@media only screen and (min-width:1400px){.bg--shine__item.diamond-yellow{top:306px;left:136px}}.bg--shine__item.diamond-white{background-image:url(/vendor/maimai/img/prism/bg/bg_diamond_white.png);top:16.5vw;left:50.5vw;animation:diamond 3s infinite linear}@media only screen and (min-width:769px){.bg--shine__item.diamond-white{top:18.2857142857vw;left:84.2857142857vw}}@media only screen and (min-width:1400px){.bg--shine__item.diamond-white{top:256px;left:1180px}}.bg--shine__item.star-white{background-image:url(/vendor/maimai/img/prism/bg/bg_star_white.png);width:7.5vw;height:7.5vw;top:21vw;left:79.5vw;opacity:0;animation:star 3s infinite linear}@media only screen and (min-width:769px){.bg--shine__item.star-white{width:2.1428571429vw;height:2.1428571429vw;top:16.4285714286vw;left:39.4285714286vw}}@media only screen and (min-width:1400px){.bg--shine__item.star-white{width:30px;height:30px;top:230px;left:552px}}.bg--shine__item.star-yellow{width:7.5vw;height:7.5vw;opacity:0;animation:star 3s infinite 1s linear}@media only screen and (min-width:769px){.bg--shine__item.star-yellow{width:2.1428571429vw;height:2.1428571429vw;top:16.4285714286vw;left:39.4285714286vw}}@media only screen and (min-width:1400px){.bg--shine__item.star-yellow{width:30px;height:30px;top:230px;left:552px}}.bg--shine__item.star-yellow.i1{background-image:url(/vendor/maimai/img/prism/bg/bg_star_yellow.png);top:28.5vw;left:3vw}@media only screen and (min-width:769px){.bg--shine__item.star-yellow.i1{top:8.1428571429vw;left:.8571428571vw}}@media only screen and (min-width:1400px){.bg--shine__item.star-yellow.i1{top:114px;left:12px}}.bg--shine__item.star-yellow.i2{background-image:url(/vendor/maimai/img/prism/bg/bg_star_yellow.png);top:5vw;left:30vw}@media only screen and (min-width:769px){.bg--shine__item.star-yellow.i2{top:28.8571428571vw;left:96.8571428571vw}}@media only screen and (min-width:1400px){.bg--shine__item.star-yellow.i2{top:404px;left:1356px}}.bg--aurora{position:absolute;top:0;left:0;width:100%;height:61.5vw;background-image:url(/vendor/maimai/img/prism/bg/bg_aurora_sp.png);background-repeat:repeat-x;background-size:415vw;background-position:left bottom 37.5vw;animation:auroras 40s infinite linear;z-index:2}@media only screen and (min-width:769px){.bg--aurora{background-image:url(/vendor/maimai/img/prism/bg/bg_aurora_pc.png);height:17.5714285714vw;background-size:228.2857142857vw auto;background-position:0 100%;animation:auroras 50s infinite linear}}@media only screen and (min-width:1400px){.bg--aurora{height:246px;background-size:3196px auto}}.bg--aurora:after,.bg--aurora:before{content:"";display:block;background-image:url(/vendor/maimai/img/prism/bg/bg_aurora_sp.png);position:absolute;top:0;left:0;background-size:contain;background-repeat:no-repeat;width:200%}.bg--aurora:before{height:42vw;top:0;background-repeat:repeat-x;opacity:0;animation:aurora 120s infinite linear}@media only screen and (min-width:769px){.bg--aurora:before{height:33.7857142857vw;top:-17.8571428571vw;left:0}}@media only screen and (min-width:1400px){.bg--aurora:before{height:473px;top:-250px}}.bg--aurora:after{height:101.75vw;top:-16.5vw;background-repeat:repeat-x;opacity:0;animation:aurora 90s infinite linear reverse}@media only screen and (min-width:769px){.bg--aurora:after{height:12vw;top:-19.2857142857vw;left:0}}@media only screen and (min-width:1400px){.bg--aurora:after{height:168px;top:-270px}}.bg--cloud{position:absolute;width:100%;height:100%;top:0;left:0}.bg--cloud__item{display:block;position:absolute;top:0;left:0;background-size:contain;background-repeat:no-repeat;z-index:1}.bg--cloud__item.i1{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_back_l.png);top:.5vw;left:-20.5vw;width:32.75vw;height:17.5vw;animation:cloud 12s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i1{top:16.8571428571vw;left:-4.1428571429vw;width:18.7142857143vw;height:10vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i1{top:236px;left:-58px;width:262px;height:140px}}.bg--cloud__item.i2{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_back_r.png);top:28.5vw;left:48vw;width:67vw;height:35vw;animation:cloud 14s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i2{top:11.7142857143vw;left:71.4285714286vw;width:38.2857142857vw;height:20vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i2{top:164px;left:1000px;width:536px;height:280px}}.bg--cloud__item.i3{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_back_c.png);top:81vw;left:75vw;width:30.25vw;height:16.125vw;animation:cloud 13s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i3{top:29.2857142857vw;left:81.4285714286vw;width:17.2857142857vw;height:9.2142857143vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i3{top:410px;left:1140px;width:242px;height:129px}}.bg--cloud__item.i4{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_front_l.png);top:67vw;left:4.5vw;width:37.375vw;height:17.25vw;animation:cloud 18s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i4{top:26.4285714286vw;left:7.4285714286vw;width:21.3571428571vw;height:9.8571428571vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i4{top:370px;left:104px;width:299px;height:138px}}.bg--cloud__item.i5{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_front_r.png);top:36.5vw;left:66.5vw;width:35.5vw;height:20vw;animation:cloud 20s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i5{top:16.5vw;left:82.2857142857vw;width:20.3571428571vw;height:11.4285714286vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i5{top:231px;left:1152px;width:185px;height:160px}}.bg--cloud__item.i6{background-image:url(/vendor/maimai/img/prism/bg/bg_cloud_front_c.png);top:90vw;left:59vw;width:32.375vw;height:15.375vw;animation:cloud 19s infinite linear}@media only screen and (min-width:769px){.bg--cloud__item.i6{top:34.5714285714vw;left:72.8571428571vw;width:18.5vw;height:8.7857142857vw}}@media only screen and (min-width:1400px){.bg--cloud__item.i6{top:484px;left:1020px;width:259px;height:123px}}.bg--moon{position:absolute;top:4vw;left:4vw;width:24.75vw;height:24.75vw;z-index:2;background-size:contain;background-repeat:no-repeat;background-image:url(/vendor/maimai/img/prism/bg/bg_moon.png)}@media only screen and (min-width:769px){.bg--moon{top:0;left:1.4285714286vw;width:14.1428571429vw;height:14.1428571429vw;animation:moon 6s infinite linear alternate}}@media only screen and (min-width:1400px){.bg--moon{left:20px;width:198px;height:198px}}.bg--star__item{position:absolute;top:0;left:0;z-index:0;animation:shooting-star 2s infinite linear;opacity:0}.bg--star__item:after,.bg--star__item:before{content:"";display:block;position:absolute;background-size:contain;background-repeat:no-repeat}.bg--star__item:before{top:0;right:0;width:91%;height:91%;background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_tail.png);background-size:contain}.bg--star__item:after{position:absolute;bottom:0;left:0;width:14%;height:14%;background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_head.png);background-size:contain;background-repeat:no-repeat;transform-origin:50% 50%;animation:shooting-star-head 2.2s infinite linear}.bg--star__item.green:before{background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_tail_green.png)}.bg--star__item.green:after{background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_head_green.png)}.bg--star__item.pink:before{background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_tail_pink.png)}.bg--star__item.pink:after{background-image:url(/vendor/maimai/img/prism/bg/bg_shootingstar_head_pink.png)}.bg--star__item.i1{width:19.25vw;height:19.25vw;top:-11.5vw;left:4vw}@media only screen and (min-width:769px){.bg--star__item.i1{width:11vw;height:11vw;top:5.7857142857vw;left:10.5vw}}@media only screen and (min-width:1400px){.bg--star__item.i1{width:154px;height:154px;top:81px;left:147px}}.bg--star__item.i2{width:19.25vw;height:19.25vw;top:-11vw;left:88vw;animation:shooting-star 2s infinite .8s linear}@media only screen and (min-width:769px){.bg--star__item.i2{width:11vw;height:11vw;top:5.2857142857vw;left:34.7142857143vw}}@media only screen and (min-width:1400px){.bg--star__item.i2{width:154px;height:154px;top:74px;left:486px}}.bg--star__item.i3{width:38.5vw;height:38.5vw;top:11.5vw;left:9vw}@media only screen and (min-width:769px){.bg--star__item.i3{width:22vw;height:22vw;top:1.4285714286vw;left:31.1428571429vw}}@media only screen and (min-width:1400px){.bg--star__item.i3{width:308px;height:308px;top:20px;left:436px}}.bg--star__item.i4{width:38.5vw;height:38.5vw;top:3vw;left:313vw;animation:shooting-star 2s infinite 1s linear}@media only screen and (min-width:769px){.bg--star__item.i4{width:11vw;height:11vw;top:.8571428571vw;left:89.4285714286vw}}@media only screen and (min-width:1400px){.bg--star__item.i4{width:154px;height:154px;top:12px;left:1252px}}.bg--star__item.i5{display:none}@media only screen and (min-width:769px){.bg--star__item.i5{display:block;width:22vw;height:22vw;top:-4.2857142857vw;left:87.1428571429vw;animation:shooting-star 2s infinite .8s linear}}@media only screen and (min-width:1400px){.bg--star__item.i5{width:308px;height:308px;top:-60px;left:1220px}}.bg--star__item.i6{display:none}@media only screen and (min-width:769px){.bg--star__item.i6{display:block;width:11vw;height:11vw;top:21.7142857143vw;left:87.8571428571vw;animation:shooting-star 2s infinite 1s linear}}@media only screen and (min-width:1400px){.bg--star__item.i6{width:154px;height:154px;top:304px;left:1230px}}.bg--star__item.i7{display:none}@media only screen and (min-width:769px){.bg--star__item.i7{display:block;width:18.1428571429vw;height:18.1428571429vw;top:14.5714285714vw;left:110vw;animation:shooting-star 2s infinite 1s linear}}@media only screen and (min-width:1400px){.bg--star__item.i7{width:254px;height:254px;top:204px;left:1540px}}.bg--rainbow:before{content:"";display:block;position:absolute;background-repeat:no-repeat;width:100%;height:90vw;bottom:44vw;background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_sp.png);background-size:100%;background-repeat:no-repeat;background-position:50%;-webkit-clip-path:polygon(0 0,0 0,0 100%,0 100%);clip-path:polygon(0 0,0 0,0 100%,0 100%);animation:rainbow 2s ease forwards;z-index:3}@media only screen and (min-width:769px){.bg--rainbow:before{background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_pc.png);background-size:54.5vw 28.5vw;height:28.5vw;bottom:10vw}}@media only screen and (min-width:1400px){.bg--rainbow:before{background-size:763px 399px;height:399px;bottom:140px}}.bg--rainbow__cloud{position:absolute;background-repeat:no-repeat;width:100%;height:57.5vw;bottom:35vw;background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_btm_sp.png);background-size:100vw 27vw;background-position:50%;animation:rainbow-bottom 2s ease forwards;z-index:3}@media only screen and (min-width:769px){.bg--rainbow__cloud{background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_btm_pc.png);background-size:98.5714285714vw 16.4285714286vw;height:16.4285714286vw;bottom:2.8571428571vw}}@media only screen and (min-width:1400px){.bg--rainbow__cloud{background-size:1380px 230px;height:230px;bottom:40px}}.bg--rainbow__cloud:after{content:"";display:block;position:absolute;background-repeat:no-repeat;width:100%;height:100%;top:-26vw;background-image:url(/vendor/maimai/img/prism/bg/bg_rainbow_back_sp.png);background-size:119%;background-position:0;z-index:-4}@media only screen and (min-width:769px){.bg--rainbow__cloud:after{display:none}}.header{position:relative;z-index:1000000}.header--language{width:93.125vw;height:10vw;top:3.75vw;border-radius:100px;background-color:#fff;position:absolute;left:50%;transform:translateX(-50%);box-shadow:1.25vw 1.25vw 0 rgba(0,0,0,.2);display:flex;align-items:center;justify-content:space-between;padding-left:3.75vw}@media only screen and (min-width:769px){.header--language{box-shadow:.7142857143vw .7142857143vw 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1400px){.header--language{box-shadow:10px 10px 0 rgba(0,0,0,.2)}}@media only screen and (min-width:769px){.header--language{display:none}}.header--language__link{background-color:#ba5eea;border-radius:0 100px 100px 0;width:23.875vw;padding-left:2.5vw;height:100%;display:flex;align-items:center}.header--language__text{font-size:2.5vw;font-weight:800;flex:1}.header--language__svg{width:15vw;margin-top:.5vw}.header--language__svg .svg{width:100%;height:auto;fill:#fff;fill-rule:evenodd}.header--language__arrow{width:3.75vw;margin-left:1.25vw;font-size:0;position:relative}.header--language__arrow img{position:relative;z-index:10}.header--language__arrow::after{display:block;content:"";position:absolute;background-color:#ba5eea;width:1.875vw;height:1.875vw;top:50%;left:50%;transform:translate(-50%,-50%);z-index:0}.header--btn{width:15vw;height:15vw;background-color:#fff;border:1.25vw solid #e72139;position:fixed;z-index:10;top:7.5vw;right:2.5vw;border-radius:100px;box-shadow:1.25vw 1.25vw 0 rgba(0,0,0,.2);display:flex;align-items:center;justify-content:center;flex-direction:column;transition:.3s;transform:translateY(0)}@media only screen and (min-width:769px){.header--btn{box-shadow:.7142857143vw .7142857143vw 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1400px){.header--btn{box-shadow:10px 10px 0 rgba(0,0,0,.2)}}.header--btn.off{opacity:0;transform:translateY(-100%);pointer-events:none}@media only screen and (min-width:769px){.header--btn{display:none}}.header--btn span{display:block;width:5.625vw;height:1vw;background-color:#e72139;border-radius:100px;position:relative}.header--btn span:first-child{top:-1vw}.header--btn span:last-child{top:1vw}.header--nav{padding-top:7.5vw;position:fixed;top:-100%;left:0;width:100%;height:100%;background-color:#fff;z-index:1000;transition:all .3s;opacity:0;box-shadow:1.25vw 1.25vw 0 rgba(0,0,0,.2)}@media only screen and (min-width:769px){.header--nav{box-shadow:.7142857143vw .7142857143vw 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1400px){.header--nav{box-shadow:10px 10px 0 rgba(0,0,0,.2)}}@media only screen and (min-width:769px){.header--nav{display:flex;align-items:flex-end;justify-content:space-between;opacity:1;margin:1.4285714286vw auto 0;width:85.7142857143vw;height:7.1428571429vw;border-radius:3.5714285714vw;padding-top:0;padding-left:2.3571428571vw;position:relative;top:auto;left:auto;transition:none}}@media only screen and (min-width:1400px){.header--nav{margin:20px auto 0;width:1200px;height:100px;border-radius:50px;padding-left:33px}}.header--nav.open{top:0;opacity:1}.header--nav__btn{width:15vw;height:15vw;background-color:#ba5eea;position:absolute;top:7.5vw;right:2.5vw;border-radius:100px;display:flex;align-items:center;justify-content:center;flex-direction:column}@media only screen and (min-width:769px){.header--nav__btn{display:none}}.header--nav__btn span{display:block;width:6.25vw;height:1vw;background-color:#fff;border-radius:100px;position:relative}.header--nav__btn span:first-child{top:.5vw;transform:rotate(45deg)}.header--nav__btn span:last-child{top:-.5vw;transform:rotate(-45deg)}.header--nav__logo{display:block;width:52.625vw;transform:translate3d(0,0,0)}@media only screen and (max-width:769px){.header--nav__logo{position:relative;z-index:2;left:50%;transform:translate3d(-50%,0,0)}}@media only screen and (min-width:769px){.header--nav__logo{width:12.7857142857vw;margin:0 0 1.5vw;transition:opacity .3s}.header--nav__logo:hover{opacity:.7}}@media only screen and (min-width:1400px){.header--nav__logo{width:179px;margin:0 0 21px}}.header--nav__inner{margin:6.5vw 0 0 0}@media only screen and (min-width:769px){.header--nav__inner{margin:0 3.5714285714vw 1.4285714286vw 0}}@media only screen and (min-width:1400px){.header--nav__inner{margin:0 50px 20px 0}}@media only screen and (min-width:769px){.header--nav__list{display:flex}}.header--nav__item{margin:5vw 0 0}@media only screen and (min-width:769px){.header--nav__item{display:flex;align-items:center;margin:0}}@media only screen and (min-width:769px){.header--nav__item:not(:last-child){margin:0 .8571428571vw 0 0}}@media only screen and (min-width:1400px){.header--nav__item:not(:last-child){margin:0 12px 0 0}}.header--nav__item:not(:last-child)::after{display:block;content:"";width:3.75vw;height:.5vw;background-color:#e5e5e5;margin:5vw auto 0}@media only screen and (min-width:769px){.header--nav__item:not(:last-child)::after{width:1px;height:1vw;margin:0 0 0 1vw}}@media only screen and (min-width:1400px){.header--nav__item:not(:last-child)::after{height:14px;margin:0 0 0 14px}}.header--nav__link{display:flex;align-items:center;justify-content:center;margin:0 auto;position:relative}@media only screen and (min-width:769px){.header--nav__link.on::before,.header--nav__link:hover::before{position:absolute;bottom:-1.4285714286vw;content:"";display:block;width:100%;background-color:#ba5eea;height:.3571428571vw;border-radius:100px 100px 0 0}}@media only screen and (min-width:1400px){.header--nav__link.on::before,.header--nav__link:hover::before{height:5px;bottom:-20px}}.header--nav__link.blank:after{display:block;content:"";background-image:url('data:image/svg+xml;utf8,');background-size:contain;background-repeat:no-repeat;width:3.25vw;height:2.5vw;margin-left:1.75vw}@media only screen and (min-width:769px){.header--nav__link.blank:after{width:.7857142857vw;height:.5714285714vw;margin-left:.4285714286vw}}@media only screen and (min-width:1400px){.header--nav__link.blank:after{width:11px;height:8px;margin-left:6px}}.header--nav__link svg{fill:#0b3871;fill-rule:evenodd;height:6.25vw}@media only screen and (min-width:769px){.header--nav__link svg{height:1.2857142857vw}}@media only screen and (min-width:1400px){.header--nav__link svg{height:18px}}.header--nav__link.l01{width:15.375vw}@media only screen and (min-width:769px){.header--nav__link.l01{width:4.5vw}}@media only screen and (min-width:1400px){.header--nav__link.l01{width:63px}}.header--nav__link.l02{width:31.75vw}@media only screen and (min-width:769px){.header--nav__link.l02{width:9.1428571429vw}}@media only screen and (min-width:1400px){.header--nav__link.l02{width:128px}}.header--nav__link.l03{width:24.875vw}@media only screen and (min-width:769px){.header--nav__link.l03{width:7.2142857143vw}}@media only screen and (min-width:1400px){.header--nav__link.l03{width:101px}}.header--nav__link.l04{width:21.25vw}@media only screen and (min-width:769px){.header--nav__link.l04{width:6.0714285714vw}}@media only screen and (min-width:1400px){.header--nav__link.l04{width:85px}}.header--nav__link.l05{width:25.875vw}@media only screen and (min-width:769px){.header--nav__link.l05{width:7.5vw}}@media only screen and (min-width:1400px){.header--nav__link.l05{width:105px}}.header--nav__link.l06{width:19.5vw}@media only screen and (min-width:769px){.header--nav__link.l06{width:5.4285714286vw}}@media only screen and (min-width:1400px){.header--nav__link.l06{width:76px}}.header--nav__link.l07{width:42.625vw}@media only screen and (min-width:769px){.header--nav__link.l07{width:12vw}}@media only screen and (min-width:1400px){.header--nav__link.l07{width:168px}}.header--nav__language{display:none}@media only screen and (min-width:769px){.header--nav__language{display:flex;align-items:center;justify-content:flex-end;margin-bottom:1.4285714286vw}}@media only screen and (min-width:1400px){.header--nav__language{margin-bottom:20px}}@media only screen and (min-width:769px){.header--nav__language__text{font-size:1vw;font-weight:800}}@media only screen and (min-width:1400px){.header--nav__language__text{font-size:14px}}@media only screen and (min-width:769px){.header--nav__language__link{display:flex;align-items:center;justify-content:center;position:relative;background-color:#ba5eea;width:13.5714285714vw;height:2.1428571429vw;border-radius:1000px}}.header--nav__language__link:hover{background-color:#ff699d}.header--nav__language__link:hover:before{background-color:#ff699d}.header--nav__language__link:hover .header--nav__language__linkText svg{fill:#fff;fill-rule:evenodd}@media only screen and (min-width:1400px){.header--nav__language__link{width:190px;height:30px}}@media only screen and (min-width:769px){.header--nav__language__btn{position:relative;width:13.5714285714vw;height:2.1428571429vw;display:flex;align-items:center;justify-content:center;margin-left:1.0714285714vw}}@media only screen and (min-width:1400px){.header--nav__language__btn{width:190px;height:30px;margin-left:15px}}@media only screen and (min-width:769px){.header--nav__language__btn:after{display:block;content:"";background-image:url(/vendor/maimai/img/header/btn_arrow.svg);background-size:contain;background-repeat:no-repeat;position:absolute;width:1vw;height:1vw;right:.7142857143vw}}@media only screen and (min-width:1400px){.header--nav__language__btn:after{width:15px;height:15px;right:10px}}@media only screen and (min-width:769px){.header--nav__language__btn:before{position:absolute;display:block;content:"";background-color:#ba5eea;width:.7142857143vw;height:.7142857143vw;right:.8571428571vw;border-radius:100px}}@media only screen and (min-width:1400px){.header--nav__language__btn:before{width:10px;height:10px;right:12px}}@media only screen and (min-width:769px){.header--nav__language__linkText{width:9.7857142857vw;margin-left:-.4285714286vw}}@media only screen and (min-width:1400px){.header--nav__language__linkText{width:137px;margin-left:-6px}}.header--nav__language__linkText svg{fill:#fff;fill-rule:evenodd}.song .header--btn{top:17.5vw}.footer--share{width:87.5vw;border-radius:12.5vw;background-color:#fff;margin:17.5vw auto 0;box-shadow:1.25vw 1.25vw 0 rgba(0,0,0,.2);position:relative;padding:4vw 0 5vw}@media only screen and (min-width:769px){.footer--share{box-shadow:.7142857143vw .7142857143vw 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1400px){.footer--share{box-shadow:10px 10px 0 rgba(0,0,0,.2)}}@media only screen and (min-width:769px){.footer--share{width:54.5454545455vw;border-radius:9.0909090909vw;margin:7.2727272727vw auto 0;padding:1.8181818182vw 0 2.7272727273vw}}@media only screen and (min-width:1100px){.footer--share{width:600px;border-radius:100px;margin:80px auto 0;padding:20px 0 30px}}.footer--share:after,.footer--share:before{position:absolute;display:block;content:"";width:50vw;height:1.5vw;left:50%;transform:translateX(-50%)}@media only screen and (min-width:769px){.footer--share:after,.footer--share:before{width:31.8181818182vw;height:.7272727273vw}}@media only screen and (min-width:1100px){.footer--share:after,.footer--share:before{width:350px;height:8px}}.footer--share:before{top:0;border-radius:0 0 1.5vw 1.5vw;background-color:#ba5eea}@media only screen and (min-width:769px){.footer--share:before{border-radius:0 0 .7272727273vw .7272727273vw}}@media only screen and (min-width:1100px){.footer--share:before{border-radius:0 0 8px 8px}}.footer--share:after{bottom:0;border-radius:1.5vw 1.5vw 0 0;background-color:#ffcedf}@media only screen and (min-width:769px){.footer--share:after{border-radius:.7272727273vw .7272727273vw 0 0}}@media only screen and (min-width:1400px){.footer--share:after{border-radius:8px 8px 0 0}}.footer--share__header{width:18.25vw;margin:0 auto}@media only screen and (min-width:769px){.footer--share__header{width:11.8181818182vw}}@media only screen and (min-width:1400px){.footer--share__header{width:130px}}.footer--share__header svg{width:100%;fill:#0b3871;fill-rule:evenodd}.footer--sns{display:flex;justify-content:center;align-items:center;margin-top:2.5vw}@media only screen and (min-width:769px){.footer--sns{margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.footer--sns{margin-top:10px}}.footer--sns__link{display:block;width:10.125vw}@media only screen and (min-width:769px){.footer--sns__link{width:6.3636363636vw;transition:.3s}.footer--sns__link:hover{opacity:.7}}@media only screen and (min-width:1100px){.footer--sns__link{width:70px}}.footer--sns__item:nth-child(2){margin:0 2.5vw}@media only screen and (min-width:769px){.footer--sns__item:nth-child(2){margin:0 1.8181818182vw}}@media only screen and (min-width:1100px){.footer--sns__item:nth-child(2){margin:0 20px}}.footer--bottom{background-color:#fff;width:100%;height:106.5vw;margin-top:10vw;padding:18.75vw 6.25vw;color:#0b3871;position:relative}@media only screen and (min-width:769px){.footer--bottom{height:25vw;margin-top:6.4285714286vw;padding:5.9285714286vw 0 0}}@media only screen and (min-width:1400px){.footer--bottom{height:350px;margin-top:90px;padding:83px 0 0}}.footer--bottom:before{content:"";display:block;background-color:#fff;box-shadow:0 -1.25vw 1.25vw -.5vw rgba(28,112,176,.7);height:2.5vw;width:100%;opacity:.5;position:absolute;top:-2.5vw;left:0;right:0}@media only screen and (min-width:769px){.footer--bottom:before{box-shadow:0 -.2857142857vw .3571428571vw -.1428571429vw rgba(28,112,176,.8);height:.7142857143vw;top:-.7142857143vw}}@media only screen and (min-width:1400px){.footer--bottom:before{box-shadow:0 -4px 5px -2px rgba(28,112,176,.8);height:10px;top:-10px}}@media only screen and (min-width:769px){.footer--bottom__bnr{transition:transform .3s}.footer--bottom__bnr:hover{transform:translateY(-5%)}}@media only screen and (min-width:769px){.footer--bottom__bnr{display:block;width:35.7142857143vw;margin:0 auto}}@media only screen and (min-width:1400px){.footer--bottom__bnr{width:500px}}.footer--bottom__wrapper{margin-top:13.625vw}@media only screen and (min-width:769px){.footer--bottom__wrapper{width:70vw;margin:3.8571428571vw auto 0;display:flex;justify-content:space-between;align-items:flex-start}}@media only screen and (min-width:1400px){.footer--bottom__wrapper{margin:54px auto 0;width:980px}}.footer--bottom__text{font-size:2.75vw}@media only screen and (min-width:769px){.footer--bottom__text{font-size:.7857142857vw}}@media only screen and (min-width:1400px){.footer--bottom__text{font-size:11px}}.footer--bottom__logo{width:31.25vw;margin:7.5vw auto;display:block}@media only screen and (min-width:769px){.footer--bottom__logo{width:8vw;margin:0;transition:.3s}.footer--bottom__logo:hover{opacity:.7}}@media only screen and (min-width:1400px){.footer--bottom__logo{width:112px}}.top--kv{width:100vw;margin:0 auto 1.5vw;position:relative;height:143.875vw;z-index:2;overflow:hidden}@media only screen and (max-width:769px){.top--kv{margin-left:0;margin-right:0;padding-top:3.75vw}}@media only screen and (min-width:769px){.top--kv{margin:0 auto;height:auto;margin-top:-.8571428571vw;padding-left:.7142857143vw;padding-right:11.5vw;width:68.4285714286vw;padding-bottom:9.3571428571vw;margin-bottom:-.8571428571vw}}@media only screen and (min-width:1400px){.top--kv{margin-top:-12px;padding-left:10px;padding-right:161px;width:958px;padding-bottom:131px;margin-bottom:-12px}}.top--kv__logo{position:absolute;bottom:0vw;left:calc(50% - 50vw);width:100vw}@media only screen and (min-width:769px){.top--kv__logo{width:49.2857142857vw;bottom:0;left:calc(50% - 24.1428571429vw)}}@media only screen and (min-width:1400px){.top--kv__logo{width:690px;left:calc(50% - 338px)}}.top--link{display:flex;width:91.25vw;flex-wrap:wrap;margin:-2.5vw auto 0;justify-content:space-between;align-items:flex-end;position:relative;z-index:100}@media only screen and (min-width:769px){.top--link{width:78.5714285714vw;margin-top:0}}@media only screen and (min-width:1400px){.top--link{width:1100px}}@media only screen and (max-width:769px){.top--link__item:nth-child(n+3){margin-top:2.5vw}}.top--link__link{display:block;width:45vw;font-size:0}@media only screen and (min-width:769px){.top--link__link{transition:transform .3s}.top--link__link:hover{transform:translateY(-5%)}}@media only screen and (min-width:769px){.top--link__link{width:18.8571428571vw}}@media only screen and (min-width:1400px){.top--link__link{width:264px}}.top--info{margin-top:6.25vw}@media only screen and (min-width:769px){.top--info{margin-top:4.2857142857vw}}@media only screen and (min-width:1400px){.top--info{margin-top:60px}}.top--info__header--txt{width:50vw;height:3.75vw}@media only screen and (min-width:769px){.top--info__header--txt{width:32.7272727273vw;height:2.5454545455vw}}@media only screen and (min-width:1100px){.top--info__header--txt{width:360px;height:28px}}.top--info__wrapper{position:relative}.top--info__container{width:75vw;margin-top:6.25vw}@media only screen and (min-width:769px){.top--info__container{width:63.6363636364vw;margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.top--info__container{width:700px;margin-top:40px}}.top--info__arrow{width:8.75vw;height:8.75vw;background-color:#ff5668;border-radius:12.5vw;transition:.3s}@media only screen and (min-width:769px){.top--info__arrow{width:4.5454545455vw;height:4.5454545455vw}.top--info__arrow:hover{background-color:#ccc}}@media only screen and (min-width:1100px){.top--info__arrow{width:50px;height:50px}}.top--info__arrow.prev{transform:scale(-1,1);left:1.25vw}@media only screen and (min-width:769px){.top--info__arrow.prev{left:2.7272727273vw}}@media only screen and (min-width:1100px){.top--info__arrow.prev{left:30px}}.top--info__arrow.next{right:1.25vw}@media only screen and (min-width:769px){.top--info__arrow.next{right:2.7272727273vw}}@media only screen and (min-width:1400px){.top--info__arrow.next{right:30px}}.top--info__arrow:after{display:block;content:"";background-image:url(/vendor/maimai/img/common/arrow.svg);background-size:cover;width:6.25vw;height:6.25vw}@media only screen and (min-width:769px){.top--info__arrow:after{width:3.6363636364vw;height:3.6363636364vw}}@media only screen and (min-width:1100px){.top--info__arrow:after{width:40px;height:40px}}.top--info__pagination{position:relative;display:flex;justify-content:center;margin-top:5vw}@media only screen and (min-width:769px){.top--info__pagination{margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.top--info__pagination{margin-top:40px}}.top--info__pagination .swiper-pagination-bullet{background-color:#ccc;opacity:1;width:2vw;height:2vw;margin:0 .625vw}@media only screen and (min-width:769px){.top--info__pagination .swiper-pagination-bullet{width:1.4545454545vw;height:1.4545454545vw;margin:0 .4545454545vw}}@media only screen and (min-width:1100px){.top--info__pagination .swiper-pagination-bullet{width:16px;height:16px;margin:0 5px}}.top--info__pagination .swiper-pagination-bullet-active{background-color:#e72139}.top--info__btn{margin-top:5vw}@media only screen and (max-width:769px){.top--info__btn{width:57.5vw}}@media only screen and (min-width:769px){.top--info__btn{margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.top--info__btn{margin-top:40px}}.top--info__btn.blank:after{right:4.375vw}@media only screen and (min-width:769px){.top--info__btn.blank:after{right:2.7272727273vw}}@media only screen and (min-width:1100px){.top--info__btn.blank:after{right:30px}}.top--info__btn.customer{justify-content:flex-start;padding-left:2.875vw}@media only screen and (min-width:769px){.top--info__btn.customer{padding-left:2.9090909091vw}}@media only screen and (min-width:1100px){.top--info__btn.customer{padding-left:32px}}.top--update{margin-top:6.25vw}@media only screen and (min-width:769px){.top--update{margin-top:4.2857142857vw}}@media only screen and (min-width:1400px){.top--update{margin-top:60px}}.top--update__header--txt{width:40.625vw;height:4.375vw}@media only screen and (min-width:769px){.top--update__header--txt{width:26.1818181818vw;height:2.5454545455vw}}@media only screen and (min-width:1100px){.top--update__header--txt{width:288px;height:28px}}.top--update__box{width:75vw;height:65vw;margin:8.75vw auto 0;overflow:scroll;padding-right:2.5vw}@media only screen and (min-width:769px){.top--update__box{width:56.3636363636vw;height:17.2727272727vw;margin:3.6363636364vw auto 0;overflow:scroll;padding-right:1.8181818182vw}}@media only screen and (min-width:1100px){.top--update__box{width:620px;height:190px;margin:40px auto 0;overflow:scroll;padding-right:20px}}.top--update__box::-webkit-scrollbar{width:2.5vw}@media only screen and (min-width:769px){.top--update__box::-webkit-scrollbar{width:.9090909091vw}}@media only screen and (min-width:1400px){.top--update__box::-webkit-scrollbar{width:10px}}.top--update__box::-webkit-scrollbar-track{background:0 0}.top--update__box::-webkit-scrollbar-thumb{background:#e72139;border-radius:100px}.top--update__item{width:100%;padding-bottom:5vw;border-bottom:1px solid #000}@media only screen and (min-width:769px){.top--update__item{display:flex;padding-bottom:1.8181818182vw}}@media only screen and (min-width:1400px){.top--update__item{padding-bottom:20px}}.top--update__item:last-child{border:none}.top--update__item:not(:first-child){margin-top:3.75vw}@media only screen and (min-width:769px){.top--update__item:not(:first-child){margin-top:2.7272727273vw}}@media only screen and (min-width:1400px){.top--update__item:not(:first-child){margin-top:30px}}.top--update__time{color:#e72139;font-size:3.5vw;font-weight:900}@media only screen and (min-width:769px){.top--update__time{font-size:1.4545454545vw;margin-top:.4545454545vw}}@media only screen and (min-width:1100px){.top--update__time{font-size:16px;margin-top:5px}}.top--update__text{font-size:3vw;margin-top:1.875vw;line-height:1.5;color:#0b3871;font-weight:500}@media only screen and (min-width:769px){.top--update__text{font-size:1.2727272727vw;margin-left:2.7272727273vw;margin-top:0}}@media only screen and (min-width:1100px){.top--update__text{font-size:14px;margin-left:30px}}.top--update__text>a{text-decoration:underline;color:#e72139}.top--update__text>a:hover{text-decoration:none}.top--fb{margin-top:6.25vw}@media only screen and (min-width:769px){.top--fb{margin-top:4.2857142857vw}}@media only screen and (min-width:1400px){.top--fb{margin-top:60px}}.top--fb__header--txt{width:25.5vw;height:4.375vw}@media only screen and (min-width:769px){.top--fb__header--txt{width:16.3636363636vw;height:2.5454545455vw}}@media only screen and (min-width:1100px){.top--fb__header--txt{width:180px;height:28px}}.top--fb__page{margin:6.25vw auto 0;width:62.5vw;max-width:340px}@media only screen and (min-width:769px){.top--fb__page{margin-top:4.5454545455vw;width:45.4545454545vw;max-width:500px}}@media only screen and (min-width:1100px){.top--fb__page{margin-top:50px}}.top--chu{display:block;width:78.75vw;margin:10vw auto 0;padding-left:1.25vw;font-size:0}@media only screen and (min-width:769px){.top--chu{transition:transform .3s}.top--chu:hover{transform:translateY(-5%)}}@media only screen and (min-width:769px){.top--chu{width:23.4285714286vw;margin:5.7142857143vw auto 0;padding-left:.7142857143vw}}@media only screen and (min-width:1400px){.top--chu{width:328px;margin:80px auto 0;padding-left:10px}}.play--nav{display:flex;justify-content:center;flex-wrap:wrap;width:88.75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.play--nav{width:64.2857142857vw;margin:3.5714285714vw auto 0}}@media only screen and (min-width:1400px){.play--nav{width:915px;margin:50px auto 0}}.play--nav__link{display:block;box-shadow:.625vw .625vw 0 rgba(0,0,0,.2);width:42.75vw;height:10.625vw;border-radius:5.25vw;background-color:#4b4b4b;display:flex;align-items:center;justify-content:center}.play--nav__link.on{background-color:#ba5eea}@media only screen and (min-width:769px){.play--nav__link{transition:.3s;width:20.8571428571vw;height:4.2857142857vw;border-radius:3vw;box-shadow:.3571428571vw .3571428571vw 0 rgba(0,0,0,.2)}.play--nav__link:hover{background-color:#ba5eea}}@media only screen and (min-width:1400px){.play--nav__link{width:292px;height:60px;border-radius:42px;box-shadow:5px 5px 0 rgba(0,0,0,.2)}}@media only screen and (max-width:768px){.play--nav__item:nth-child(n+3){margin-top:2vw}.play--nav__item:nth-of-type(n+2){margin-left:3.25vw}.play--nav__item:nth-of-type(2n+1){margin-left:0}}@media only screen and (min-width:769px){.play--nav__item:nth-of-type(n+2){margin-left:.8571428571vw}.play--nav__item:nth-child(n+4){margin-top:.8571428571vw}.play--nav__item:nth-of-type(4n+0){margin-left:0}}@media only screen and (min-width:1400px){.play--nav__item:nth-of-type(n+3){margin-left:12px}.play--nav__item:nth-child(n+4){margin-top:12px}.play--nav__item:nth-of-type(4n+0){margin-left:0}}.play--nav__text.l1{width:22.25vw;margin-bottom:.625vw}@media only screen and (min-width:769px){.play--nav__text.l1{width:10.3571428571vw;margin-bottom:0}}@media only screen and (min-width:1400px){.play--nav__text.l1{width:145px}}.play--nav__text.l2{width:26.125vw}@media only screen and (min-width:769px){.play--nav__text.l2{width:12.1428571429vw;margin-top:.2857142857vw}}@media only screen and (min-width:1400px){.play--nav__text.l2{width:170px;margin-top:4px}}.play--nav__text.l3{width:31vw;margin-top:.5vw}@media only screen and (min-width:769px){.play--nav__text.l3{width:13.2857142857vw;margin-top:.1428571429vw}}@media only screen and (min-width:1400px){.play--nav__text.l3{width:186px;margin-top:2px}}.play--nav__text.l4{width:30.25vw;margin-bottom:.2857142857vw}@media only screen and (min-width:769px){.play--nav__text.l4{width:13.1428571429vw;margin-bottom:0}}@media only screen and (min-width:1400px){.play--nav__text.l4{width:184px}}.play--nav__text.l5{width:30.875vw;margin-bottom:.2857142857vw}@media only screen and (min-width:769px){.play--nav__text.l5{width:13.2857142857vw;margin-bottom:0}}@media only screen and (min-width:1400px){.play--nav__text.l5{width:186px}}.play--nav__text.l6{width:25vw}@media only screen and (min-width:769px){.play--nav__text.l6{width:10.9285714286vw}}@media only screen and (min-width:1400px){.play--nav__text.l6{width:158px}}.play--text{font-size:4.75vw;text-align:center;color:#e72139;font-weight:900;margin-top:12.5vw;line-height:1.5}@media only screen and (min-width:769px){.play--text{font-size:2.5454545455vw;margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--text{font-size:28px;margin-top:50px}}.play--text:after{display:block;content:"";width:5vw;height:1vw;margin:4vw auto 0;background-color:#bebebe;border-radius:100px}@media only screen and (min-width:769px){.play--text:after{width:3.6363636364vw;height:.7272727273vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--text:after{width:40px;height:8px;margin:40px auto 0}}.play--text.first{margin-top:5vw}@media only screen and (min-width:769px){.play--text.first{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--text.first{margin-top:50px}}.play--text.second{margin-top:12vw}@media only screen and (min-width:769px){.play--text.second{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--text.second{margin-top:50px}}.play--text.thrd{margin-top:10vw}@media only screen and (min-width:769px){.play--text.thrd{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--text.thrd{margin-top:50px}}.play--text.thrd.chinese{font-family:ヒラギノ角ゴ Pro W3,Hiragino Kaku Gothic Pro,メイリオ,Meiryo,sans-serif;font-weight:700}.play--about{margin-top:6.25vw}@media only screen and (min-width:769px){.play--about{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--about{margin-top:50px}}.play--about__header .common--header__txt{width:36.25vw;height:4vw}@media only screen and (min-width:769px){.play--about__header .common--header__txt{width:23.2727272727vw;height:2.5454545455vw}}@media only screen and (min-width:1100px){.play--about__header .common--header__txt{width:256px;height:28px}}.play--about__img.i1{width:100%}.play--about__img.i2{width:81.875vw;margin:10.75vw auto 0}@media only screen and (min-width:769px){.play--about__img.i2{width:69.4545454545vw;margin:5.4545454545vw auto 0}}@media only screen and (min-width:1100px){.play--about__img.i2{width:764px;margin:60px auto 0}}.play--about__img.i3{width:84vw}@media only screen and (min-width:769px){.play--about__img.i3{width:72.4545454545vw;margin:3.6363636364vw auto}}@media only screen and (min-width:1100px){.play--about__img.i3{width:797px;margin:40px auto}}.play--about__img.i4{width:82.125vw;margin-left:2.25vw}@media only screen and (min-width:769px){.play--about__img.i4{width:76.8181818182vw;margin:3.6363636364vw auto}}@media only screen and (min-width:1100px){.play--about__img.i4{width:845px;margin:40px auto}}.play--about__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;background-size:3vw auto;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--about__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0;background-size:2.1818181818vw auto}}@media only screen and (min-width:1100px){.play--about__border{width:824px;height:7px;margin:70px auto 0;background-size:24px auto}}.play--about__btn{margin-top:5vw;width:75vw}@media only screen and (min-width:769px){.play--about__btn{margin-top:4.0909090909vw;width:47.2727272727vw}}@media only screen and (min-width:1100px){.play--about__btn{margin-top:45px;width:520px}}.play--about__btn.bt1{height:15vw;margin-top:-11.5vw}@media only screen and (min-width:769px){.play--about__btn.bt1{margin-top:-12.7272727273vw;margin-bottom:4.5454545455vw;margin-left:2.7272727273vw;width:54.5454545455vw;height:5.4545454545vw}}@media only screen and (min-width:1100px){.play--about__btn.bt1{margin-top:-140px;margin-bottom:50px;margin-left:30px;width:600px;height:60px}}.play--about__btn.bt1>span{width:57.875vw;margin-left:-5vw}@media only screen and (min-width:769px){.play--about__btn.bt1>span{width:45.0909090909vw;margin-left:-1.8181818182vw}}@media only screen and (min-width:1100px){.play--about__btn.bt1>span{width:496px;margin-left:-20px}}.play--about__btn.bt2{margin-top:5vw}@media only screen and (max-width:769px){.play--about__btn.bt2.next::after{right:2.25vw}}@media only screen and (min-width:769px){.play--about__btn.bt2{margin-top:0;margin-bottom:10vw}}@media only screen and (min-width:1100px){.play--about__btn.bt2{margin-bottom:110px}}.play--howto{margin-top:6.25vw}@media only screen and (min-width:769px){.play--howto{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--howto{margin-top:50px}}.play--howto__header .common--header__txt{width:42.25vw;height:4.5vw}@media only screen and (min-width:769px){.play--howto__header .common--header__txt{width:27.5454545455vw;height:3vw}}@media only screen and (min-width:1100px){.play--howto__header .common--header__txt{width:303px;height:33px}}.play--howto__img.i1{width:87.5vw;margin:6.25vw auto 0}@media only screen and (min-width:769px){.play--howto__img.i1{width:73.8181818182vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--howto__img.i1{width:812px;margin-top:40px}}.play--howto__img.i2{width:82.5vw;margin:12.5vw auto 0 3.5vw}@media only screen and (min-width:769px){.play--howto__img.i2{width:66.5454545455vw;margin:10.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--howto__img.i2{width:732px;margin:114px auto 0}}.play--howto__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--howto__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--howto__border{width:824px;height:7px;margin:70px auto 0}}.play--function{margin-top:6.25vw;padding-top:9.625vw;padding-bottom:12.5vw}@media only screen and (min-width:769px){.play--function{margin-top:4.5454545455vw;padding-top:6.1818181818vw;padding-bottom:9.0909090909vw}}@media only screen and (min-width:1100px){.play--function{margin-top:50px;padding-top:68px;padding-bottom:100px}}.play--function__header{position:relative}@media only screen and (max-width:768px){.play--function__header .common--header__icon{position:absolute;top:3.75vw;left:calc(50% - 40.875vw)}.play--function__header .common--header__icon.right{left:calc(50% + 27.125vw)}}.play--function__header .common--header__txt{width:55.25vw;height:11.875vw;margin:0 2vw}@media only screen and (min-width:769px){.play--function__header .common--header__txt{width:39.9090909091vw;height:7.7272727273vw;margin:0 1vw}}@media only screen and (min-width:1100px){.play--function__header .common--header__txt{width:439px;height:85px;margin:0 11px}}.play--function__header .common--header__txt svg{height:100%}.play--function__subheader{background-color:#ba5eea;font-size:4.75vw;text-align:center;color:#fff;font-weight:800;margin-top:8.75vw;margin-bottom:8.75vw;margin-inline:auto;line-height:1.3421052632;display:flex;align-items:center;justify-content:center;width:81.25vw;height:19.375vw;border-radius:9.375vw}@media only screen and (min-width:769px){.play--function__subheader{font-size:2.5454545455vw;margin-top:6.3636363636vw;margin-bottom:6.3636363636vw;line-height:1.2857142857;width:63.6363636364vw;height:9.0909090909vw;border-radius:4.5454545455vw}}@media only screen and (min-width:1100px){.play--function__subheader{font-size:28px;margin-top:70px;margin-bottom:70px;width:700px;height:100px;border-radius:50px}}@media only screen and (min-width:769px){.play--function .play--text{font-size:2vw;margin-top:6.3636363636vw}}@media only screen and (min-width:1100px){.play--function .play--text{font-size:22px;margin-top:70px}}.play--function__img.i1{width:100%;margin:2.5vw auto 0}@media only screen and (min-width:769px){.play--function__img.i1{margin:4.0909090909vw auto 0}}@media only screen and (min-width:1100px){.play--function__img.i1{margin:45px auto 0}}.play--function__img.i2{width:100%;margin:5vw auto 0}@media only screen and (min-width:769px){.play--function__img.i2{margin:5.4545454545vw auto 0}}@media only screen and (min-width:1100px){.play--function__img.i2{margin:60px auto 0}}.play--function__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--function__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--function__border{width:824px;height:7px;margin:70px auto 0}}.play--function__btn{width:80.5vw;height:15.5vw;box-shadow:none}@media only screen and (min-width:769px){.play--function__btn{width:59.0909090909vw;height:7.2727272727vw;transition:opacity .3s}.play--function__btn:hover{transform:none;opacity:80%}}@media only screen and (min-width:1100px){.play--function__btn{width:650px;height:80px}}.play--function__btn span{display:inline-block;width:80%;text-align:center;line-height:1.2;word-break:keep-all}@media only screen and (min-width:769px){.play--function__btn span{width:100%}}.play--function__btn.blank:after{width:5.75vw;height:4.25vw;right:5.75vw}@media only screen and (min-width:769px){.play--function__btn.blank:after{width:2.7272727273vw;height:2vw;right:2.6363636364vw}}@media only screen and (min-width:1100px){.play--function__btn.blank:after{width:30px;height:22px;right:29px}}.play--function__btn.btn-pink{background-color:#e84472}.play--function__btn.btn-yellow{background-color:#ff699d;margin-top:7.75vw}@media only screen and (min-width:769px){.play--function__btn.btn-yellow{margin-top:5.4545454545vw}}@media only screen and (min-width:1100px){.play--function__btn.btn-yellow{margin-top:60px}}.play--function2{margin-top:6.25vw}@media only screen and (min-width:769px){.play--function2{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--function2{margin-top:50px}}.play--function2__header .common--header__txt{width:45.5vw;height:4vw}@media only screen and (min-width:769px){.play--function2__header .common--header__txt{width:29.5454545455vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.play--function2__header .common--header__txt{width:325px;height:30px}}.play--function2__img.i1{width:100%;margin:2.5vw auto 0}@media only screen and (min-width:769px){.play--function2__img.i1{margin:4.0909090909vw auto 0}}@media only screen and (min-width:1100px){.play--function2__img.i1{margin:45px auto 0}}.play--function2__img.i2{width:81vw;margin:5vw auto 0 3.375vw}@media only screen and (min-width:769px){.play--function2__img.i2{width:73.2727272727vw;margin:3.6363636364vw auto 0 3.2727272727vw}}@media only screen and (min-width:1100px){.play--function2__img.i2{width:806px;margin:40px auto 0 36px}}.play--function2__img.i3{width:86.375vw;margin:5vw auto 0 1vw}@media only screen and (min-width:769px){.play--function2__img.i3{width:77.2727272727vw;margin:3.6363636364vw auto 0 4.0909090909vw}}@media only screen and (min-width:1100px){.play--function2__img.i3{width:850px;margin-top:40px;margin-left:45px}}.play--function2__img.i4{width:82vw;margin:5vw auto 0 2.75vw}@media only screen and (min-width:769px){.play--function2__img.i4{width:72.3636363636vw;margin:3.6363636364vw auto 0 4.5454545455vw}}@media only screen and (min-width:1100px){.play--function2__img.i4{width:796px;margin:40px auto 0 50px}}.play--function2__img.i5{width:100%;margin:2.5vw auto 3.75vw 0}@media only screen and (min-width:769px){.play--function2__img.i5{margin:3.6363636364vw auto 4.0909090909vw 0}}@media only screen and (min-width:1100px){.play--function2__img.i5{margin:40px auto 45px 0}}.play--function2__img.i6{width:100%;margin:2.5vw auto 0}@media only screen and (min-width:769px){.play--function2__img.i6{margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--function2__img.i6{margin:40px auto 0}}.play--function2__img.i7{width:87.5vw;margin:5vw auto 6.25vw 0}@media only screen and (min-width:769px){.play--function2__img.i7{width:81.8181818182vw;margin:3.6363636364vw auto 4.5454545455vw 0}}@media only screen and (min-width:1100px){.play--function2__img.i7{width:900px;margin:40px auto 50px 0}}.play--function2__img.i8{width:87.5vw;margin:5vw auto 0}@media only screen and (min-width:769px){.play--function2__img.i8{width:81.8181818182vw;margin:0 auto}}@media only screen and (min-width:1100px){.play--function2__img.i8{width:900px;margin:0 auto}}.play--function2__img.i9{width:100%;margin-top:3.5vw}@media only screen and (min-width:769px){.play--function2__img.i9{margin-top:4vw;margin-bottom:5.8181818182vw}}@media only screen and (min-width:1100px){.play--function2__img.i9{margin-top:44px;margin-bottom:64px}}.play--function2__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--function2__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--function2__border{width:824px;height:7px;margin:70px auto 0}}.play--function3{margin-top:6.25vw}@media only screen and (min-width:769px){.play--function3{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--function3{margin-top:50px}}.play--function3__header .common--header__txt{width:46.375vw;height:4vw}@media only screen and (min-width:769px){.play--function3__header .common--header__txt{width:29.8181818182vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.play--function3__header .common--header__txt{width:328px;height:30px}}.play--function3__img.i1{width:100%;margin:3vw auto 12.5vw 0}@media only screen and (min-width:769px){.play--function3__img.i1{margin:4.0909090909vw auto 5.0909090909vw 0}}@media only screen and (min-width:1100px){.play--function3__img.i1{margin:45px 0 56px 0}}.play--function3__img.i2{width:100%;margin:1.25vw auto 0}@media only screen and (min-width:769px){.play--function3__img.i2{margin:0 auto}}.play--function3__img.i3{width:100%}.play--function3__img.i4{width:86.25vw;margin:5vw auto 0 1.25vw}@media only screen and (min-width:769px){.play--function3__img.i4{width:81.8181818182vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--function3__img.i4{width:900px;margin:40px auto 0}}.play--function3__img.i5{width:82.25vw;margin:5vw auto 0 2.625vw}@media only screen and (min-width:769px){.play--function3__img.i5{width:81.8181818182vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--function3__img.i5{width:900px;margin:40px auto 0}}.play--function3__img.i6{width:82vw;margin:5vw auto 0 2.75vw}@media only screen and (min-width:769px){.play--function3__img.i6{width:81.8181818182vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.play--function3__img.i6{width:900px;margin:40px auto 0}}.play--function3__img.i7{width:81.875vw;margin:5vw auto 7.5vw}@media only screen and (min-width:769px){.play--function3__img.i7{width:81.8181818182vw;margin:3.6363636364vw auto 5.0909090909vw}}@media only screen and (min-width:1100px){.play--function3__img.i7{margin:40px auto 56px;width:900px}}.play--function3__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--function3__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--function3__border{width:824px;height:7px;margin:70px auto 0}}.play--function4{margin-top:6.25vw}@media only screen and (min-width:769px){.play--function4{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.play--function4{margin-top:50px}}.play--function4__header span{width:68.875vw;height:10.5vw}@media only screen and (min-width:769px){.play--function4__header span{width:44.3636363636vw;height:6.7272727273vw}}@media only screen and (min-width:1100px){.play--function4__header span{width:488px;height:74px}}.play--function4__img.i1{width:87.5vw;margin:5vw auto 0 0vw}@media only screen and (min-width:769px){.play--function4__img.i1{width:81.8181818182vw;margin:3.6363636364vw auto 0 0vw}}@media only screen and (min-width:1100px){.play--function4__img.i1{width:900px;margin-top:40px;margin-left:0}}.play--function4__img.i2{width:81.875vw;margin:5vw auto 0 2.75vw}@media only screen and (min-width:769px){.play--function4__img.i2{width:81.8181818182vw;margin:3.6363636364vw auto 0 0vw}}@media only screen and (min-width:1100px){.play--function4__img.i2{width:900px;margin:40px auto 0 0}}.play--function4__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--function4__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--function4__border{width:824px;height:7px;margin:70px auto 0}}.play--terms{margin-top:7.5vw;padding-bottom:8.75vw}@media only screen and (min-width:769px){.play--terms{margin-top:4.5454545455vw;padding-bottom:8.75vw}}@media only screen and (min-width:1100px){.play--terms{margin-top:50px;padding-bottom:70px}}.play--terms__header .common--header__txt{width:46.375vw;height:4vw}@media only screen and (min-width:769px){.play--terms__header .common--header__txt{width:27.0909090909vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.play--terms__header .common--header__txt{width:298px;height:30px}}.play--terms__text{font-size:3.5vw;text-align:left;color:#000;font-weight:500;margin:5vw 6.25vw 0 6.25vw;line-height:1.5}@media only screen and (min-width:769px){.play--terms__text{font-size:1.8181818182vw;margin:3.75vw 5vw 0 5vw;text-align:justify}}@media only screen and (min-width:1100px){.play--terms__text{font-size:20px;margin:40px 50px 0 50px}}.play--terms__text li{margin-left:1vw;list-style:"• ";text-align:justify}.play--terms__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.play--terms__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.play--terms__border{width:824px;height:7px;margin:70px auto 0}}.song--search{margin-top:6.25vw;position:sticky;top:-75vw;z-index:100}@media only screen and (min-width:769px){.song--search{margin-top:4.5454545455vw;top:-41.8181818182vw}}@media only screen and (min-width:1100px){.song--search{margin-top:50px;top:calc(-460px - .2vw)}}@media only screen and (min-width:1400px){.song--search{top:-458px}}.song--search:after,.song--search:before{display:none}.song--search__text{font-size:2.5vw;text-align:center;line-height:1.5}@media only screen and (min-width:769px){.song--search__text{font-size:1.2727272727vw}}@media only screen and (min-width:1100px){.song--search__text{font-size:14px}}.song--search__btn{width:75vw;height:11.875vw;margin:5vw auto;border-radius:12.5vw;background-color:#4b4b4b;display:flex;align-items:center;justify-content:center;position:relative}.song--search__btn.new{background-color:#fb5f99}.song--search__btn.pops_anime{background-color:#ff972a}.song--search__btn.niconico{background-color:#02c8d3}.song--search__btn.toho{background-color:#ad59ee}.song--search__btn.variety{background-color:#4be070}.song--search__btn.maimai{background-color:#f64849}.song--search__btn.gekichu{background-color:#3584fe}.song--search__btn.utage{background-color:#dc39b8}.song--search__btn.new .song--search__btn__text{width:29.375vw}.song--search__btn.pops_anime .song--search__btn__text{width:33.375vw}.song--search__btn.niconico .song--search__btn__text{width:29.25vw}.song--search__btn.toho .song--search__btn__text{width:27.25vw}.song--search__btn.variety .song--search__btn__text{width:20vw}.song--search__btn.maimai .song--search__btn__text{width:17.375vw}.song--search__btn.gekichu .song--search__btn__text{width:25vw}.song--search__btn.utage .song--search__btn__text{width:13.5vw}.song--search__btn.on:after{transform:scale(1,-1)}.song--search__btn:after{position:absolute;display:block;content:"";background-image:url(/vendor/maimai/img/song/arrow.svg);background-size:contain;background-repeat:no-repeat;width:2.5vw;height:2.25vw;right:4.5vw}@media only screen and (min-width:769px){.song--search__btn{display:none}}.song--search__btn__text svg{fill:#fff;fill-rule:evenodd;height:4vw}@media only screen and (min-width:769px){.song--search__btn__text svg{height:1.6363636364vw}}@media only screen and (min-width:1100px){.song--search__btn__text svg{height:18px}}.song--search__list{display:none;width:75vw;margin-top:-2.5vw!important;left:50%;transform:translateX(-50%);position:absolute;box-sizing:border-box;background-color:#242424;z-index:200;text-align:center;color:#fff;font-size:3.5vw;font-weight:900;padding:2.5vw 6.25vw}.song--search__item{padding:5vw 0}.song--search__item:not(:first-child){border-top:1px solid #707070}.song--search__check{display:flex;align-items:center;justify-content:center;flex-wrap:wrap}@media only screen and (min-width:769px){.song--search__check{margin-top:2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__check{margin-top:30px}}@media only screen and (min-width:769px){.song--search__check.bt{width:54.5454545455vw;margin:0 auto}}@media only screen and (min-width:1100px){.song--search__check.bt{width:500px}}.song--search__check.bt>label:nth-child(n+4){margin-top:3.75vw}@media only screen and (min-width:769px){.song--search__check.bt>label:nth-child(n+4){margin-top:1.3636363636vw}}.song--search__check.bt>label:not(:first-child){margin-left:2.5vw}@media only screen and (min-width:769px){.song--search__check.bt>label:not(:first-child){margin-left:2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__check.bt>label:not(:first-child){margin-left:30px}}.song--search__check>label:not(:first-child){margin-left:4.375vw}@media only screen and (min-width:769px){.song--search__check>label:not(:first-child){margin-left:4.0909090909vw}}@media only screen and (min-width:1100px){.song--search__check>label:not(:first-child){margin-left:45px}}.song--search__check__input{display:none}.song--search__check__input:checked+.song--search__check__text:before{background-color:#e72139;border:.75vw solid #e72139}.song--search__check__input:checked+.song--search__check__text:after{display:block}.song--search__check__text{display:flex;align-items:center;position:relative}.song--search__check__text::before{box-sizing:border-box;content:"";display:block;width:5vw;height:5vw;border:.75vw solid #e5e5e5;border-radius:.625vw}@media only screen and (min-width:769px){.song--search__check__text::before{width:2.7272727273vw;height:2.7272727273vw;border:.3636363636vw solid #e5e5e5;border-radius:.4545454545vw}}@media only screen and (min-width:1100px){.song--search__check__text::before{width:30px;height:30px;border-width:4px;border-radius:5px}}.song--search__check__text:after{box-sizing:border-box;content:"";display:none;position:absolute;left:.75vw;background-image:url(/vendor/maimai/img/song/check.svg);background-size:contain;background-repeat:no-repeat;width:3.375vw;height:2.875vw}@media only screen and (min-width:769px){.song--search__check__text:after{width:1.8181818182vw;height:1.4545454545vw;left:.5454545455vw}}@media only screen and (min-width:1100px){.song--search__check__text:after{width:20px;height:16px;left:6px}}.song--search__check__text>span{display:flex;align-items:center;width:20vw;height:4.125vw;margin-left:2.5vw}@media only screen and (min-width:769px){.song--search__check__text>span{width:10.6363636364vw;height:2.7272727273vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span{width:117px;height:30px;margin-left:10px}}.song--search__check__text>span.re{width:22.375vw}@media only screen and (min-width:769px){.song--search__check__text>span.re{width:11.2727272727vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.re{width:124px}}.song--search__check__text>span.ba{width:12.5vw;height:3.75vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.ba{width:7vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.ba{width:77px;height:20px;margin-left:10px}}.song--search__check__text>span.bas{width:22.5vw;height:4vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.bas{width:12.8181818182vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.bas{width:141px;height:20px;margin-left:10px}}.song--search__check__text>span.ex{width:16.25vw;height:3.75vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.ex{width:9.0909090909vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.ex{width:100px;height:20px;margin-left:10px}}.song--search__check__text>span.ma{width:17.5vw;height:3.75vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.ma{width:9.0909090909vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.ma{width:100px;height:20px;margin-left:10px}}.song--search__check__text>span.re{width:26.25vw;height:3.75vw;margin-left:1.25vw}@media only screen and (min-width:769px){.song--search__check__text>span.re{width:12.7272727273vw;height:1.8181818182vw;margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__check__text>span.re{width:140px;height:20px;margin-left:10px}}.song--search__nav{display:none}@media only screen and (min-width:769px){.song--search__nav{width:77.2727272727vw;margin:2.7272727273vw auto;display:flex;align-items:center;justify-content:center;flex-wrap:wrap}}@media only screen and (min-width:1100px){.song--search__nav{width:850px;margin:30px auto 30px}}@media only screen and (min-width:769px){.song--search__nav__item{display:flex;align-items:center;justify-content:center;width:18.1818181818vw;height:7.2727272727vw;background-color:#4b4b4b;border-radius:100px;transition:.3s;cursor:pointer}}@media only screen and (min-width:1100px){.song--search__nav__item{width:200px;height:80px}}.song--search__nav__item.on.new,.song--search__nav__item:hover.new{background-color:#fb5f99}.song--search__nav__item.on.pops_anime,.song--search__nav__item:hover.pops_anime{background-color:#ff972a}.song--search__nav__item.on.niconico,.song--search__nav__item:hover.niconico{background-color:#02c8d3}.song--search__nav__item.on.toho,.song--search__nav__item:hover.toho{background-color:#ad59ee}.song--search__nav__item.on.variety,.song--search__nav__item:hover.variety{background-color:#4be070}.song--search__nav__item.on.maimai,.song--search__nav__item:hover.maimai{background-color:#f64849}.song--search__nav__item.on.gekichu,.song--search__nav__item:hover.gekichu{background-color:#3584fe}.song--search__nav__item.on.utage,.song--search__nav__item:hover.utage{background-color:#dc39b8}@media only screen and (min-width:769px){.song--search__nav__item:nth-child(n+5){margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__nav__item:nth-child(n+5){margin-top:10px}}@media only screen and (min-width:769px){.song--search__nav__item:not(:nth-child(4n+1)){margin-left:1.3636363636vw}}@media only screen and (min-width:1100px){.song--search__nav__item:not(:nth-child(4n+1)){margin-left:15px}}@media only screen and (min-width:769px){.song--search__nav__img{height:1.7272727273vw}}@media only screen and (min-width:1100px){.song--search__nav__img{height:19px}}@media only screen and (min-width:769px){.song--search__nav__img.i01{width:11.8181818182vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i01{width:130px}}@media only screen and (min-width:769px){.song--search__nav__img.i02{width:13.5454545455vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i02{width:149px}}@media only screen and (min-width:769px){.song--search__nav__img.i03{width:12.9090909091vw;height:4.1818181818vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i03{width:142px;height:46px}}@media only screen and (min-width:769px){.song--search__nav__img.i04{width:11vw;height:2vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i04{width:121px;height:22px}}@media only screen and (min-width:769px){.song--search__nav__img.i05{width:9.1818181818vw;height:4.1818181818vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i05{width:101px;height:46px}}@media only screen and (min-width:769px){.song--search__nav__img.i06{width:7vw;height:1.5454545455vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i06{width:77px;height:17px}}@media only screen and (min-width:769px){.song--search__nav__img.i07{width:11.1818181818vw;height:4.1818181818vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i07{width:123px;height:46px}}@media only screen and (min-width:769px){.song--search__nav__img.i08{width:5.1818181818vw;height:1.9090909091vw}}@media only screen and (min-width:1100px){.song--search__nav__img.i08{width:57px;height:21px}}.song--search__input{position:relative;display:flex;align-items:center;justify-content:center;margin-top:5.625vw}@media only screen and (min-width:769px){.song--search__input{margin-top:0}}.song--search__input:before{display:block;content:"";background-image:url(/vendor/maimai/img/song/search_lens.svg);background-size:contain;background-repeat:no-repeat;width:6.25vw;height:6.25vw;left:8.75vw;position:absolute;z-index:10}@media only screen and (min-width:769px){.song--search__input:before{width:2.7272727273vw;height:2.7272727273vw;left:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__input:before{width:30px;height:30px;left:10px}}.song--search__input .clear{display:block;content:"";background-image:url(/vendor/maimai/img/song/search_del.png);background-size:contain;background-repeat:no-repeat;width:6.25vw;height:6.25vw;position:absolute;right:8.75vw;z-index:10;cursor:pointer}@media only screen and (min-width:769px){.song--search__input .clear{width:2.7272727273vw;height:2.7272727273vw;right:.9090909091vw}}@media only screen and (min-width:1100px){.song--search__input .clear{width:30px;height:30px;right:10px}}.song--search__input input{position:relative;width:75vw;height:11.25vw;border-radius:100px;background-color:#eee;margin:0 auto;font-size:4.5vw;padding:0 10vw;display:block;font-weight:900}.song--search__input input::placeholder{font-size:2.25vw;font-weight:400}@media only screen and (min-width:769px){.song--search__input input{width:40.6363636364vw;height:3.6363636364vw;font-size:1.8181818182vw;font-weight:900;padding:0 4.5454545455vw}.song--search__input input::placeholder{font-size:1.2727272727vw;font-weight:400}}@media only screen and (min-width:1100px){.song--search__input input{width:447px;height:40px;font-weight:900;font-size:20px;padding:0 50px}.song--search__input input::placeholder{font-size:14px;font-weight:400}}.song--search__attention{margin:2.5vw auto 0;padding:0 6.25vw}@media only screen and (min-width:769px){.song--search__attention{margin:0;padding:0;margin-left:2.2727272727vw}}@media only screen and (min-width:1100px){.song--search__attention{margin-left:25px}}.song--search__attention__text{font-size:2.5vw;padding-left:1em;text-indent:-1em;display:flex;align-items:center;line-height:1.5}.song--search__attention__text:not(:first-child){margin-top:1.25vw}@media only screen and (min-width:769px){.song--search__attention__text:not(:first-child){margin-top:.1818181818vw}}@media only screen and (min-width:1100px){.song--search__attention__text:not(:first-child){margin-top:2px}}@media only screen and (min-width:769px){.song--search__attention__text{font-size:1.0909090909vw}}@media only screen and (min-width:1100px){.song--search__attention__text{font-size:12px}}.song--search__attention__key{display:flex;align-items:center;justify-content:center;width:3.75vw;margin:0 3.25vw 0 .5vw}@media only screen and (min-width:769px){.song--search__attention__key{width:1.8181818182vw;margin:0 1.2727272727vw 0 .1818181818vw}}@media only screen and (min-width:1100px){.song--search__attention__key{width:20px;margin:0 14px 0 2px}}@media only screen and (min-width:769px){.song--search__box{display:flex;align-items:center;justify-content:center;margin-top:2.2727272727vw}}@media only screen and (min-width:1100px){.song--search__box{margin-top:25px}}.song--search__open{position:absolute;width:100%;margin:5vw 0 -8.25vw;height:11.25vw;font-size:4.5vw;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:900;background-color:#e72139;position:relative;border-radius:0 0 3.75vw 3.75vw;z-index:100;transition:.3s;cursor:pointer}.song--search__open.open{opacity:0;pointer-events:none}@media only screen and (min-width:769px){.song--search__open{margin:3.6363636364vw 0 -4.6363636364vw;height:5.4545454545vw;font-size:2.3636363636vw;border-radius:0 0 2.7272727273vw 2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__open{margin:40px 0 -52px;height:60px;font-size:26px;border-radius:0 0 30px 30px}}.song--search__open:after{display:block;content:"";position:absolute;right:3.75vw;width:6.25vw;height:6.25vw;background-image:url(/vendor/maimai/img/song/plus.png);background-size:cover}@media only screen and (min-width:769px){.song--search__open:after{right:26.3636363636vw;width:2.7272727273vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__open:after{right:290px;width:30px;height:30px}}.song--search__detail{padding:0 6.25vw;position:relative;display:none}@media only screen and (min-width:769px){.song--search__detail{padding:0 2.2727272727vw}}@media only screen and (min-width:1100px){.song--search__detail{padding:0 25px}}.song--search__border{width:100%;height:.75vw;border-radius:100px;background-color:#e5e5e5}@media only screen and (min-width:769px){.song--search__border{height:.5454545455vw}}@media only screen and (min-width:1100px){.song--search__border{height:6px}}.song--search__inner{margin-top:5vw}@media only screen and (min-width:769px){.song--search__inner{margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.song--search__inner{margin-top:40px}}.song--search__lv{margin-top:3.75vw;display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:flex-start}@media only screen and (min-width:769px){.song--search__lv{margin-top:2.7272727273vw}}@media only screen and (min-width:1100px){.song--search__lv{margin-top:30px}}.song--search__lv__btn{display:flex;align-items:center;justify-content:center;color:#fff;font-weight:900;width:13.75vw;height:11.25vw;border-radius:1.25vw;background-color:#4b4b4b;font-size:3.75vw}@media only screen and (min-width:769px){.song--search__lv__btn{width:5vw;height:4.0909090909vw;border-radius:.9090909091vw;font-size:1.6363636364vw}}@media only screen and (min-width:1100px){.song--search__lv__btn{width:55px;height:45px;border-radius:10px;font-size:18px}}.song--search__lv__btn.on{background-color:#e72139}@media only screen and (max-width:769px){.song--search__lv__btn:not(:nth-child(5n+1)){margin-left:1.5vw}.song--search__lv__btn:nth-child(n+6){margin-top:1.875vw}}@media only screen and (min-width:769px){.song--search__lv__btn:not(:nth-child(14n+1)){margin-left:.5454545455vw}.song--search__lv__btn:nth-child(n+15){margin-top:1.0909090909vw}}@media only screen and (min-width:1100px){.song--search__lv__btn:not(:nth-child(14n+1)){margin-left:6px}.song--search__lv__btn:nth-child(n+15){margin-top:12px}}.song--search__select{margin-top:3.75vw;display:flex;justify-content:space-between}@media only screen and (min-width:769px){.song--search__select{width:39.0909090909vw;margin:3.75vw auto 0}}@media only screen and (min-width:1100px){.song--search__select{width:430px;margin:30px auto 0}}.song--search__select__btn{box-sizing:border-box;font-size:3vw;color:#e72139;font-weight:900;width:36.25vw;height:10vw;border:1vw solid #e5e5e5;display:flex;align-items:center;justify-content:center;border-radius:100px}@media only screen and (min-width:769px){.song--search__select__btn{width:18.1818181818vw;height:3.6363636364vw;font-size:1.6363636364vw;border:.3636363636vw solid #e5e5e5;border-radius:1.8181818182vw}}@media only screen and (min-width:1100px){.song--search__select__btn{width:200px;height:40px;font-size:18px;border-width:4px;border-radius:20px}}.song--search__count{width:75vw;height:15vw;margin:3.75vw auto;border:3px solid #e72139;border-radius:125vw;background:linear-gradient(90deg,#e72139 0,#e72139 50%,#eee 50%,#eee 100%);display:flex;overflow:hidden;box-sizing:border-box}@media only screen and (min-width:769px){.song--search__count{background:linear-gradient(90deg,#e72139 0,#e72139 40%,#eee 40%,#eee 100%);width:37.8181818182vw;height:7.2727272727vw;margin:2.7272727273vw auto}}@media only screen and (min-width:1100px){.song--search__count{width:416px;height:80px;margin:30px auto}}.song--search__count__text{color:#fff;width:37.5vw;height:110%;margin-left:-.625vw;margin-top:-.625vw;font-size:3vw;background-color:#e72139;display:flex;justify-content:center;align-items:center;font-weight:700;text-align:center}@media only screen and (min-width:769px){.song--search__count__text{width:18.1818181818vw;margin-left:-.4545454545vw;margin-top:-.2727272727vw;font-size:1.6363636364vw}}@media only screen and (min-width:1100px){.song--search__count__text{width:200px;margin-left:-5px;margin-top:-3px;font-size:18px}}.song--search__count__num{width:37.5vw;height:13.75vw;font-size:5vw;display:flex;justify-content:center;align-items:center;font-weight:900;text-align:center;color:#e72139}@media only screen and (min-width:769px){.song--search__count__num{width:19.6363636364vw;height:6.3636363636vw;font-size:2.9090909091vw}}@media only screen and (min-width:1100px){.song--search__count__num{width:216px;height:70px;font-size:32px}}.song--search__count__num>.small{margin-top:1.25vw;margin-left:1.25vw;font-size:2.5vw}@media only screen and (min-width:769px){.song--search__count__num>.small{margin-top:.9090909091vw;margin-left:.9090909091vw;font-size:1.6363636364vw}}@media only screen and (min-width:1100px){.song--search__count__num>.small{margin-top:10px;margin-left:10px;font-size:18px}}.song--search__close{width:6.25vw;position:absolute;right:2.5vw;bottom:-9.375vw}@media only screen and (min-width:769px){.song--search__close{width:3.6363636364vw;right:.9090909091vw;bottom:-6.3636363636vw}}@media only screen and (min-width:1100px){.song--search__close{width:40px;right:10px;bottom:-70px}}.song--box{margin-top:6.25vw;position:relative;transform:translate3d(0,0,0)}.song--box__title{display:flex;flex-wrap:wrap;align-items:center;justify-content:center}.song--box__title--icon{background-image:none;width:13.5vw;height:7vw}@media only screen and (min-width:769px){.song--box__title--icon{width:12.5454545455vw;height:6.1818181818vw}}@media only screen and (min-width:1100px){.song--box__title--icon{width:138px;height:68px}}.song--box__tit{margin:0 1.25vw;display:flex;align-items:center;justify-content:center}@media only screen and (min-width:769px){.song--box__tit{margin:0 1.2727272727vw}}@media only screen and (min-width:1100px){.song--box__tit{margin:0 14px}}.song--box.new .song--box__title--icon svg{fill:#fb5f99}.song--box.new .song--box__tit{width:28.625vw}@media only screen and (min-width:769px){.song--box.new .song--box__tit{width:19.6363636364vw}}@media only screen and (min-width:1100px){.song--box.new .song--box__tit{width:216px}}.song--box.new:after,.song--box.new:before{background-color:#fb5f99}.song--box.pops_anime .song--box__title--icon svg{fill:#ff972a}.song--box.pops_anime .song--box__tit{width:36.125vw}@media only screen and (min-width:769px){.song--box.pops_anime .song--box__tit{width:24.3636363636vw}}@media only screen and (min-width:1100px){.song--box.pops_anime .song--box__tit{width:268px}}.song--box.pops_anime:after,.song--box.pops_anime:before{background-color:#ff972a}.song--box.niconico .song--box__title--icon svg{fill:#02c8d3}.song--box.niconico .song--box__tit{width:48.875vw}@media only screen and (min-width:769px){.song--box.niconico .song--box__tit{width:40.7272727273vw}}@media only screen and (min-width:1100px){.song--box.niconico .song--box__tit{width:448px}}.song--box.niconico:after,.song--box.niconico:before{background-color:#02c8d3}.song--box.toho .song--box__title--icon svg{fill:#ad59ee}.song--box.toho .song--box__tit{width:28.625vw}@media only screen and (min-width:769px){.song--box.toho .song--box__tit{width:20.3636363636vw}}@media only screen and (min-width:1100px){.song--box.toho .song--box__tit{width:224px}}.song--box.toho:after,.song--box.toho:before{background-color:#ad59ee}.song--box.maimai .song--box__title--icon svg{fill:#f64849}.song--box.maimai .song--box__tit{width:18.25vw;margin-bottom:.75vw}@media only screen and (min-width:769px){.song--box.maimai .song--box__tit{width:13.5454545455vw;margin-bottom:.7272727273vw}}@media only screen and (min-width:1100px){.song--box.maimai .song--box__tit{width:149px;margin-bottom:8px}}.song--box.maimai:after,.song--box.maimai:before{background-color:#f64849}.song--box.variety .song--box__title--icon svg{fill:#4be070}.song--box.variety .song--box__tit{width:43.75vw}@media only screen and (min-width:769px){.song--box.variety .song--box__tit{width:29.2727272727vw}}@media only screen and (min-width:1100px){.song--box.variety .song--box__tit{width:322px}}.song--box.variety:after,.song--box.variety:before{background-color:#4be070}.song--box.gekichu .song--box__title--icon svg{fill:#3584fe}.song--box.gekichu .song--box__tit{width:45.125vw}@media only screen and (min-width:769px){.song--box.gekichu .song--box__tit{width:36.3636363636vw}}@media only screen and (min-width:1100px){.song--box.gekichu .song--box__tit{width:400px}}.song--box.gekichu:after,.song--box.gekichu:before{background-color:#3584fe}.song--box.utage .song--box__title--icon svg{fill:#dc39b8}.song--box.utage .song--box__tit{width:11.25vw}@media only screen and (min-width:769px){.song--box.utage .song--box__tit{width:8.6363636364vw}}@media only screen and (min-width:1100px){.song--box.utage .song--box__tit{width:95px}}.song--box.utage:after,.song--box.utage:before{background-color:#dc39b8}.song--box__none{font-size:3.75vw;margin-top:5vw;line-height:2;font-weight:900;text-align:center}@media only screen and (min-width:769px){.song--box__none{font-size:1.6363636364vw;margin-top:3.6363636364vw}}@media only screen and (min-width:1100px){.song--box__none{font-size:18px;margin-top:40px}}.song--item{padding:6.25vw 6.25vw 0}.song--item:last-child .song--item__border{display:none}@media only screen and (min-width:769px){.song--item{padding:4.5454545455vw 3.6363636364vw 0;display:flex;align-items:flex-start;justify-content:space-between}}@media only screen and (min-width:1100px){.song--item{padding:50px 40px 0}}@media only screen and (min-width:769px){.song--item__box{width:53.4545454545vw}}@media only screen and (min-width:1100px){.song--item__box{width:588px}}.song--item__top{display:flex;align-items:center;justify-content:space-between}.song--item__top.lock:after{display:block;content:"";background-image:url(/vendor/maimai/img/song/lock.png);background-size:contain;width:6.125vw;height:6.125vw}@media only screen and (min-width:769px){.song--item__top.lock:after{width:4.4545454545vw;height:4.4545454545vw}}@media only screen and (min-width:1100px){.song--item__top.lock:after{width:49px;height:49px}}.song--item__genre.pops_anime{width:35.25vw}@media only screen and (min-width:769px){.song--item__genre.pops_anime{width:15.5454545455vw}}@media only screen and (min-width:1100px){.song--item__genre.pops_anime{width:171px}}.song--item__genre.toho{width:29.875vw}@media only screen and (min-width:769px){.song--item__genre.toho{width:13.4545454545vw}}@media only screen and (min-width:1100px){.song--item__genre.toho{width:148px}}.song--item__genre.maimai{width:21.125vw}@media only screen and (min-width:769px){.song--item__genre.maimai{width:9.4545454545vw}}@media only screen and (min-width:1100px){.song--item__genre.maimai{width:104px}}.song--item__genre.variety{width:41.875vw}@media only screen and (min-width:769px){.song--item__genre.variety{width:18.8181818182vw}}@media only screen and (min-width:1100px){.song--item__genre.variety{width:207px}}.song--item__genre.niconico{width:54.875vw}@media only screen and (min-width:769px){.song--item__genre.niconico{width:24.8181818182vw}}@media only screen and (min-width:1100px){.song--item__genre.niconico{width:273px}}.song--item__genre.gekichu{width:51.25vw}@media only screen and (min-width:769px){.song--item__genre.gekichu{width:23.1818181818vw}}@media only screen and (min-width:1100px){.song--item__genre.gekichu{width:255px}}.song--item__genre.utage{width:18vw}@media only screen and (min-width:769px){.song--item__genre.utage{width:8.3636363636vw}}@media only screen and (min-width:1100px){.song--item__genre.utage{width:92px}}.song--item__middle{display:flex;margin-top:3.75vw}@media only screen and (min-width:769px){.song--item__middle{margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--item__middle{margin-top:10px}}.song--item__info{width:46.25vw;margin-left:3.75vw}@media only screen and (min-width:769px){.song--item__info{margin-left:0;width:100%}}.song--item__info.utage{position:relative}.song--item__info.utage .song--item__icon{background:transparent url(/vendor/maimai/img/song/utage/icon_buddy.png) no-repeat;background-size:contain;width:8.625vw;height:6.625vw;position:absolute;right:0;bottom:0;pointer-events:none}@media only screen and (min-width:769px){.song--item__info.utage .song--item__icon{width:5vw;height:4.4545454545vw}}@media only screen and (min-width:1100px){.song--item__info.utage .song--item__icon{width:55px;height:49px}}.song--item__thumb{width:25vw;font-size:0}@media only screen and (min-width:769px){.song--item__thumb{width:18.1818181818vw}}@media only screen and (min-width:1100px){.song--item__thumb{width:200px}}.song--item__tit{font-size:4vw;font-weight:900;margin-top:-1vw;padding-bottom:1.25vw;border-bottom:1px solid #e5e5e5;line-height:1.5}@media only screen and (min-width:769px){.song--item__tit{margin:0;padding:0;font-size:2.5454545455vw;border:none}}@media only screen and (min-width:1100px){.song--item__tit{font-size:28px}}.song--item__artist{font-size:2.5vw;font-weight:900;margin-top:2.5vw}@media only screen and (min-width:769px){.song--item__artist{font-size:1.2727272727vw;margin-top:1.0909090909vw}}@media only screen and (min-width:1100px){.song--item__artist{font-size:14px;margin-top:12px}}.song--item__artistName{font-size:3vw;font-weight:900;margin-top:1.25vw}@media only screen and (min-width:769px){.song--item__artistName{font-size:1.4545454545vw;margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--item__artistName{font-size:16px;margin-top:10px}}.song--item__bottom{margin-top:3.75vw}@media only screen and (min-width:769px){.song--item__bottom{margin-top:1.8181818182vw}}@media only screen and (min-width:1100px){.song--item__bottom{margin-top:20px}}.song--item__lv{display:flex;align-items:center;justify-content:space-between}.song--item__lv:not(:first-child){margin-top:1.25vw}@media only screen and (min-width:769px){.song--item__lv:not(:first-child){margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--item__lv:not(:first-child){margin-top:10px}}.song--item__lv:after{display:block;content:"";background-size:contain;background-repeat:no-repeat;width:19vw;height:3.75vw}@media only screen and (min-width:769px){.song--item__lv:after{width:13.6363636364vw;height:2.7272727273vw}}@media only screen and (min-width:1100px){.song--item__lv:after{width:150px;height:30px}}.song--item__lv.st:after{background-image:url(/vendor/maimai/img/song/inner_st.png)}.song--item__lv.dx:after{background-image:url(/vendor/maimai/img/song/inner_dx.png)}.song--item__lv.utage .song--item__lv__inner{width:100%;flex-wrap:wrap}.song--item__lv.utage .song--item__lv__num{background-color:#dc39b8;font-size:4vw;width:37.125vw}@media only screen and (min-width:769px){.song--item__lv.utage .song--item__lv__num{font-size:2.5454545455vw;width:26.5454545455vw}}@media only screen and (min-width:1100px){.song--item__lv.utage .song--item__lv__num{font-size:28px;width:292px}}.song--item__lv.utage:after{display:none}.song--item__lv__inner{width:53.75vw;display:flex;justify-content:space-between}@media only screen and (min-width:769px){.song--item__lv__inner{width:39.0909090909vw}}@media only screen and (min-width:1100px){.song--item__lv__inner{width:430px}}.song--item__lv__num{width:10vw;height:7.5vw;border-radius:1.25vw;font-size:4.25vw;font-weight:900;display:flex;align-items:center;justify-content:center;color:#fff}@media only screen and (min-width:769px){.song--item__lv__num{width:7.2727272727vw;height:5.4545454545vw;border-radius:.9090909091vw;font-size:3.0909090909vw}}@media only screen and (min-width:1100px){.song--item__lv__num{width:80px;height:60px;border-radius:10px;font-size:34px}}.song--item__lv__num:nth-child(1){background-color:#6ed43e}.song--item__lv__num:nth-child(2){background-color:#f7b807}.song--item__lv__num:nth-child(3){background-color:#ff828d}.song--item__lv__num:nth-child(4){background-color:#a051dc}.song--item__lv__num:nth-child(5){border:.5vw solid #a051dc;color:#a051dc;background-color:#fff}@media only screen and (min-width:769px){.song--item__lv__num:nth-child(5){border-width:.3636363636vw}}@media only screen and (min-width:1100px){.song--item__lv__num:nth-child(5){border-width:4px}}.song--item__lv__comment{background-color:#dc39b8;border-radius:1.25vw;display:flex;align-items:center;justify-content:center;color:#fff;width:100%;font-size:3vw;font-weight:900;margin-top:1.25vw;padding:2.25vw 1.25vw}@media only screen and (min-width:769px){.song--item__lv__comment{border-radius:.9090909091vw;justify-content:flex-start;font-size:2.1818181818vw;margin-top:.9090909091vw;padding:1.6363636364vw 1.8181818182vw}}@media only screen and (min-width:1100px){.song--item__lv__comment{border-radius:10px;font-size:24px;margin-top:10px;padding:18px 20px}}.song--item__copy{font-size:2.25vw;margin-top:2.5vw;line-height:2}@media only screen and (min-width:769px){.song--item__copy{font-size:.9090909091vw;margin-top:.9090909091vw}}@media only screen and (min-width:1100px){.song--item__copy{font-size:10px;margin-top:10px}}.song--item__border{width:100%;height:.75vw;border-radius:100px;background-color:#e5e5e5;margin-top:6.25vw}@media only screen and (min-width:769px){.song--item__border{height:.5454545455vw;margin-top:4.5454545455vw;width:140%;margin-left:-40%}}@media only screen and (min-width:1100px){.song--item__border{height:6px;margin-top:50px}}.song--copy{position:relative;width:87.5vw;border-radius:3.75vw;box-shadow:1.75vw 1.75vw 0 0 rgba(0,0,0,.2);background-color:rgba(255,255,255,.7);margin:8.75vw auto 0;padding:5.625vw 5vw;font-size:2.5vw;line-height:1.8}@media only screen and (min-width:769px){.song--copy{width:81.8181818182vw;border-radius:2.7272727273vw;box-shadow:1.2727272727vw 1.2727272727vw 0 0 rgba(0,0,0,.2);padding:4.0909090909vw get(40,1100);margin-top:7.2727272727vw;font-size:1.0909090909vw}}@media only screen and (min-width:1100px){.song--copy{width:900px;border-radius:30px;box-shadow:14px 14px 0 0 rgba(0,0,0,.2);padding:45px 40px;margin-top:80px;font-size:12px}}.song--copy .piapro{width:11.875vw;height:2.75vw;display:inline-block;margin:0 .625vw -.625vw;-webkit-backface-visibility:hidden;background-image:url(/vendor/maimai/img/song/piapro_logo.png);background-size:contain;background-repeat:no-repeat}@media only screen and (min-width:769px){.song--copy .piapro{width:6.8181818182vw;height:1.6363636364vw;margin:0 .4545454545vw -.4545454545vw}}@media only screen and (min-width:1100px){.song--copy .piapro{width:75px;height:18px;margin:0 5px -5px}}#song--search.m_fixed{position:fixed;left:50%;transform:translateX(-50%);z-index:200;margin-top:0}.dl--nav{display:flex;justify-content:space-between;flex-wrap:wrap;width:88.75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.dl--nav{width:64.2857142857vw;margin:3.5714285714vw auto 0}}@media only screen and (min-width:1100px){.dl--nav{width:900px;margin:50px auto 0}}.dl--nav__link{display:block;box-shadow:.625vw .625vw 0 rgba(0,0,0,.2);width:42.75vw;height:10.625vw;border-radius:5.25vw;background-color:#4b4b4b;display:flex;align-items:center;justify-content:center}.dl--nav__link.on{background-color:#e72139}@media only screen and (min-width:769px){.dl--nav__link{transition:.3s;width:31.7142857143vw;height:4.2857142857vw;border-radius:3vw;box-shadow:.3571428571vw .3571428571vw 0 rgba(0,0,0,.2)}.dl--nav__link:hover{background-color:#e72139}}@media only screen and (min-width:1100px){.dl--nav__link{width:444px;height:60px;border-radius:42px;box-shadow:5px 5px 0 rgba(0,0,0,.2)}}@media only screen and (max-width:769px){.dl--nav__item:nth-child(n+3){margin-top:2vw}}@media only screen and (min-width:769px){.dl--nav__item:nth-child(n+4){margin-top:.8571428571vw}}@media only screen and (min-width:1100px){.dl--nav__item:nth-child(n+4){margin-top:12px}}.dl--nav__text.l1{width:20.25vw}@media only screen and (min-width:769px){.dl--nav__text.l1{width:9.5vw}}@media only screen and (min-width:1100px){.dl--nav__text.l1{width:133px}}.dl--nav__text.l2{width:31.5vw}@media only screen and (min-width:769px){.dl--nav__text.l2{width:14.7857142857vw}}@media only screen and (min-width:1100px){.dl--nav__text.l2{width:207px}}.dl--pop{margin-top:6.25vw}@media only screen and (min-width:769px){.dl--pop{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.dl--pop{margin-top:50px}}.dl--pop__header .common--header__txt{width:33.25vw;height:4vw}@media only screen and (min-width:769px){.dl--pop__header .common--header__txt{width:21.6363636364vw;height:2.4545454545vw}}@media only screen and (min-width:1100px){.dl--pop__header .common--header__txt{width:238px;height:27px}}.dl--pop__wrapper{width:75vw;margin:8.75vw auto 0}@media only screen and (min-width:769px){.dl--pop__wrapper{display:flex;flex-wrap:wrap;width:72.7272727273vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.dl--pop__wrapper{width:800px;margin:70px auto 0}}@media only screen and (max-width:769px){.dl--pop__item:not(:first-child){margin-top:5.625vw}}@media only screen and (min-width:769px){.dl--pop__item{width:34.5454545455vw}.dl--pop__item:nth-child(even){margin-left:3.6363636364vw}.dl--pop__item:nth-child(n+3){margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.dl--pop__item{width:380px}.dl--pop__item:nth-child(even){margin-left:40px}.dl--pop__item:nth-child(n+3){margin-top:50px}}.dl--pop__head{width:100%;height:5vw;font-size:3.5vw;font-weight:900;background-color:#e72139;justify-content:center;color:#fff;border-radius:100px;display:flex;align-items:center}@media only screen and (min-width:769px){.dl--pop__head{height:2.7272727273vw;font-size:1.8181818182vw}}@media only screen and (min-width:1100px){.dl--pop__head{height:30px;font-size:20px}}.dl--pop__thumb{display:block;width:100%;margin-top:2.5vw;font-size:0;position:relative}@media only screen and (min-width:769px){.dl--pop__thumb{margin-top:1.8181818182vw;transition:.3s}.dl--pop__thumb:hover{opacity:.7}}@media only screen and (min-width:1100px){.dl--pop__thumb{margin-top:20px}}.dl--pop__thumb::after{display:block;content:"";position:absolute;width:10vw;height:10vw;background-image:url(/vendor/maimai/img/common/download.png);background-size:contain;right:3.75vw;bottom:3.75vw}@media only screen and (min-width:769px){.dl--pop__thumb::after{width:4.5454545455vw;height:4.5454545455vw;right:1.8181818182vw;bottom:1.8181818182vw}}@media only screen and (min-width:1100px){.dl--pop__thumb::after{width:50px;height:50px;right:20px;bottom:20px}}.dl--pop__inner{width:75vw;margin:7.5vw auto 0}@media only screen and (min-width:769px){.dl--pop__inner{width:72.7272727273vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--pop__inner{width:800px;margin:40px auto 0}}.dl--pop__text{font-size:2.5vw;padding-left:1em;text-indent:-1em;line-height:1.5}@media only screen and (min-width:769px){.dl--pop__text{font-size:1.0909090909vw}}@media only screen and (min-width:1100px){.dl--pop__text{font-size:12px}}.dl--pn{display:flex;margin:7.5vw auto 0;width:87.5vw;justify-content:space-between;align-items:center}@media only screen and (min-width:769px){.dl--pn{margin-top:5.4545454545vw;width:73.6363636364vw}}@media only screen and (min-width:1100px){.dl--pn{margin-top:60px;width:810px}}.dl--pn__inner{display:flex}.dl--pn__inner.dl--pn__left .dl--pn__arrow:before{transform:scale(-1,1)}.dl--pn__center{width:40.25vw;display:flex}@media only screen and (min-width:769px){.dl--pn__center{width:48.1818181818vw}}@media only screen and (min-width:1100px){.dl--pn__center{width:530px}}.dl--pn__num{display:flex;align-items:center;justify-content:center;width:8.75vw;height:8.75vw;border-radius:12.5vw;background-color:#4b4b4b;box-shadow:.625vw .625vw 0 0 rgba(0,0,0,.2);cursor:pointer;color:#fff;font-size:3.5vw;font-weight:900}.dl--pn__num.on,.dl--pn__num:hover{background-color:#e72139}.dl--pn__num:not(:first-child){margin-left:1.75vw}@media only screen and (min-width:769px){.dl--pn__num{width:4.5454545455vw;height:4.5454545455vw;box-shadow:.4545454545vw .4545454545vw 0 0 rgba(0,0,0,.2);font-size:1.8181818182vw;font-weight:900}.dl--pn__num:not(:first-child){margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.dl--pn__num{width:50px;height:50px;box-shadow:5px 5px 0 0 rgba(0,0,0,.2);font-size:20px}.dl--pn__num:not(:first-child){margin-left:10px}}.dl--pn__arrow{display:flex;align-items:center;justify-content:center;width:8.75vw;height:8.75vw;border-radius:12.5vw;background-color:#e72139;box-shadow:.625vw .625vw 0 0 rgba(0,0,0,.2);cursor:pointer}@media only screen and (min-width:769px){.dl--pn__arrow{transition:transform .3s}.dl--pn__arrow:hover{transform:translateY(-5%)}}.dl--pn__arrow.off{background-color:#dcdcdc;pointer-events:none}@media only screen and (min-width:769px){.dl--pn__arrow{width:4.5454545455vw;height:4.5454545455vw;box-shadow:.4545454545vw .4545454545vw 0 0 rgba(0,0,0,.2)}}@media only screen and (min-width:1100px){.dl--pn__arrow{width:50px;height:50px;box-shadow:5px 5px 0 0 rgba(0,0,0,.2)}}.dl--pn__arrow:nth-child(2){margin-left:1.75vw}@media only screen and (min-width:769px){.dl--pn__arrow:nth-child(2){margin-left:.9090909091vw}}@media only screen and (min-width:1100px){.dl--pn__arrow:nth-child(2){margin-left:10px}}.dl--pn__arrow:before{display:block;content:"";width:6.875vw;height:6.875vw;background-image:url(/vendor/maimai/img/common/arrow.svg);background-size:contain;background-repeat:no-repeat}@media only screen and (min-width:769px){.dl--pn__arrow:before{width:3.6363636364vw;height:3.6363636364vw}}@media only screen and (min-width:1100px){.dl--pn__arrow:before{width:40px;height:40px}}.dl--pn__arrow.double:before{background-image:url(/vendor/maimai/img/common/arrow_double.svg)}.dl--event{margin-top:6.25vw}@media only screen and (min-width:769px){.dl--event{margin-top:4.5454545455vw}}@media only screen and (min-width:1100px){.dl--event{margin-top:50px}}.dl--event__header .common--header__txt{width:51.625vw;height:3.75vw}@media only screen and (min-width:769px){.dl--event__header .common--header__txt{width:33.4545454545vw;height:2.4545454545vw}}@media only screen and (min-width:1100px){.dl--event__header .common--header__txt{width:368px;height:27px}}.dl--event__img.i1{width:75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.dl--event__img.i1{width:57.7272727273vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--event__img.i1{width:635px;margin:40px auto 0}}.dl--event__img.i2{width:75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.dl--event__img.i2{width:74.5454545455vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--event__img.i2{width:820px;margin:40px auto 0}}.dl--event__img.i3{width:75vw;margin:5vw auto 0}@media only screen and (min-width:769px){.dl--event__img.i3{width:54.9090909091vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--event__img.i3{width:604px;margin:40px auto 0}}.dl--event__border{width:74.375vw;height:.75vw;background-image:url(/vendor/maimai/img/play/dash.png);background-repeat:repeat-x;margin:12.5vw auto 0}@media only screen and (min-width:769px){.dl--event__border{width:74.9090909091vw;height:.6363636364vw;margin:6.3636363636vw auto 0}}@media only screen and (min-width:1100px){.dl--event__border{width:824px;height:7px;margin:70px auto 0}}.dl--event__inner{width:75vw;margin:7.5vw auto 0}@media only screen and (min-width:769px){.dl--event__inner{width:72.7272727273vw;margin:3.6363636364vw auto 0}}@media only screen and (min-width:1100px){.dl--event__inner{width:800px;margin:40px auto 0}}.dl--event__text{font-size:2.5vw;padding-left:1em;text-indent:-1em;line-height:1.5}@media only screen and (min-width:769px){.dl--event__text{font-size:1.0909090909vw}}@media only screen and (min-width:1100px){.dl--event__text{font-size:12px}}.dl--event__btn{margin-top:7.5vw;width:75vw}@media only screen and (min-width:769px){.dl--event__btn{margin-top:5.9090909091vw;width:47.2727272727vw}}@media only screen and (min-width:1100px){.dl--event__btn{margin-top:65px;width:520px}}.dl--event__btn.bt1{height:15vw;margin-top:-11.25vw}@media only screen and (min-width:769px){.dl--event__btn.bt1{margin-top:-12.7272727273vw;margin-bottom:4.5454545455vw;margin-left:2.7272727273vw;width:54.5454545455vw;height:5.4545454545vw}}@media only screen and (min-width:1100px){.dl--event__btn.bt1{margin-top:-140px;margin-bottom:50px;margin-left:30px;width:600px;height:60px}}.dl--event__btn.bt1>span{width:57.875vw;margin-left:-5vw}@media only screen and (min-width:769px){.dl--event__btn.bt1>span{width:45.0909090909vw;margin-left:-1.8181818182vw}}@media only screen and (min-width:1100px){.dl--event__btn.bt1>span{width:496px;margin-left:-20px}}.ranking--box{margin-top:4.375vw}@media only screen and (min-width:769px){.ranking--box{margin-top:2.7272727273vw}}@media only screen and (min-width:1100px){.ranking--box{margin-top:30px}}.ranking--box__header--txt{color:#0b3871;font-size:3vw;font-weight:900;text-align:center;line-height:1.5;width:12.5vw}@media only screen and (min-width:769px){.ranking--box__header--txt{font-size:1.2857142857vw;width:9.0909090909vw}}@media only screen and (min-width:1100px){.ranking--box__header--txt{font-size:18px;width:100px}}.ranking--season{margin:2.5vw auto 0;width:75vw;position:relative;overflow:hidden;border-radius:45px}@media only screen and (min-width:769px){.ranking--season{margin-top:1.2727272727vw;width:76.7272727273vw;border-radius:2.7272727273vw}}@media only screen and (min-width:1100px){.ranking--season{margin-top:14px;width:844px;border-radius:30px}}.ranking--season__selectbox{overflow:hidden;text-align:center;width:100%;max-width:100%}.ranking--season__selectbox select{background:0 0;background-image:none;border:none;box-shadow:none;appearance:none;cursor:pointer;padding-right:1em;text-indent:.01px;text-overflow:ellipsis;outline:0;width:100%}.ranking--season__selectbox select::-ms-expand{display:none}.ranking--season__selectbox.slb{background:0 0;border:1vw solid #ececec;border-radius:45px;position:relative}@media only screen and (min-width:769px){.ranking--season__selectbox.slb{border-width:.3636363636vw;border-radius:2.7272727273vw}}@media only screen and (min-width:1100px){.ranking--season__selectbox.slb{border-width:4px;border-radius:30px}}.ranking--season__selectbox.slb select{color:#0b3871;font-size:3vw;font-weight:900;padding:2.75vw 0 2.75vw 16.25vw}@media only screen and (min-width:769px){.ranking--season__selectbox.slb select{font-size:2.1818181818vw;padding:1.1818181818vw 0 1.0909090909vw 26.3636363636vw}}@media only screen and (min-width:1100px){.ranking--season__selectbox.slb select{font-size:24px;padding:13px 0 12px 290px}}.ranking--season__btn{background-color:#e72139;box-sizing:border-box;pointer-events:none;position:absolute;top:0;right:0;height:110%;width:10.75vw;padding:0;z-index:2}@media only screen and (min-width:769px){.ranking--season__btn{width:5.4545454545vw}}@media only screen and (min-width:1100px){.ranking--season__btn{width:60px}}.ranking--season__btn:after{content:"";display:block;background:url(/vendor/maimai/img/common/icon_select.png) no-repeat;background-size:contain;position:absolute;top:4.625vw;right:4.5vw;height:2.25vw;width:2.5vw;z-index:3}@media only screen and (min-width:769px){.ranking--season__btn:after{top:2.1818181818vw;right:2.1818181818vw;height:1.2727272727vw;width:1.3636363636vw}}@media only screen and (min-width:1100px){.ranking--season__btn:after{top:24px;right:24px;height:14px;width:15px}}.ranking--date{font-size:2.75vw;margin-top:3.5vw;text-align:center}@media only screen and (min-width:769px){.ranking--date{font-size:1.4545454545vw;margin-top:1.6363636364vw}}@media only screen and (min-width:1100px){.ranking--date{font-size:16px;margin-top:18px}}.ranking--list{margin:2vw auto 0;width:75vw}@media only screen and (min-width:769px){.ranking--list{margin-top:1.2727272727vw;width:74.5454545455vw}}@media only screen and (min-width:1100px){.ranking--list{margin-top:14px;width:820px}}.ranking--list__item{padding:4.5vw 0;width:100%;position:relative}@media only screen and (min-width:769px){.ranking--list__item{padding:2.7272727273vw 0 2.4545454545vw}}@media only screen and (min-width:1100px){.ranking--list__item{padding:30px 0 27px}}.ranking--list__item:nth-of-type(n+2):before{content:"";display:block;background-color:#e5e5e5;border-radius:.5vw;height:1vw;width:100%;position:absolute;top:0;left:0;right:0}@media only screen and (min-width:769px){.ranking--list__item:nth-of-type(n+2):before{border-radius:.2727272727vw;height:.5454545455vw}}@media only screen and (min-width:1100px){.ranking--list__item:nth-of-type(n+2):before{border-radius:3px;height:6px}}.ranking--list__wrap{display:flex;flex-wrap:wrap;align-items:center;justify-content:center}.ranking--list__rank{display:flex;flex-wrap:wrap;justify-content:center;width:12.5vw}@media only screen and (min-width:769px){.ranking--list__rank{width:13.5454545455vw;padding-right:3.8181818182vw}}@media only screen and (min-width:1100px){.ranking--list__rank{width:149px;padding-right:42px}}.ranking--list__rank .ranking--list__num{background-repeat:no-repeat;background-size:100% auto;user-select:none;height:6.25vw;width:5.25vw}@media only screen and (min-width:769px){.ranking--list__rank .ranking--list__num{height:4.5454545455vw;width:3.8181818182vw}}@media only screen and (min-width:1100px){.ranking--list__rank .ranking--list__num{height:50px;width:42px}}.ranking--list__rank .ranking--list__num:nth-of-type(2n){margin-left:-.5vw}@media only screen and (min-width:769px){.ranking--list__rank .ranking--list__num:nth-of-type(2n){margin-left:-.3636363636vw}}@media only screen and (min-width:1100px){.ranking--list__rank .ranking--list__num:nth-of-type(2n){margin-left:-4px}}.ranking--list__rank .ranking--list__num.num0{background-image:url(/vendor/maimai/img/ranking/rank_num_sv0.png);height:6.25vw;width:5.375vw}@media only screen and (min-width:769px){.ranking--list__rank .ranking--list__num.num0{height:4.5454545455vw;width:3.7272727273vw}}@media only screen and (min-width:1100px){.ranking--list__rank .ranking--list__num.num0{height:50px;width:41px}}.ranking--list__rank .ranking--list__num.num1{background-image:url(/vendor/maimai/img/ranking/rank_num_sv1.png);height:6.375vw;width:5.125vw}@media only screen and (min-width:769px){.ranking--list__rank .ranking--list__num.num1{height:4.6363636364vw;width:3.7272727273vw}}@media only screen and (min-width:1100px){.ranking--list__rank .ranking--list__num.num1{height:51px;width:41px}}.ranking--list__rank .ranking--list__num.num2{background-image:url(/vendor/maimai/img/ranking/rank_num_sv2.png)}.ranking--list__rank .ranking--list__num.num3{background-image:url(/vendor/maimai/img/ranking/rank_num_sv3.png)}.ranking--list__rank .ranking--list__num.num4{background-image:url(/vendor/maimai/img/ranking/rank_num_sv4.png)}.ranking--list__rank .ranking--list__num.num5{background-image:url(/vendor/maimai/img/ranking/rank_num_sv5.png)}.ranking--list__rank .ranking--list__num.num6{background-image:url(/vendor/maimai/img/ranking/rank_num_sv6.png)}.ranking--list__rank .ranking--list__num.num7{background-image:url(/vendor/maimai/img/ranking/rank_num_sv7.png)}.ranking--list__rank .ranking--list__num.num8{background-image:url(/vendor/maimai/img/ranking/rank_num_sv8.png)}.ranking--list__rank .ranking--list__num.num9{background-image:url(/vendor/maimai/img/ranking/rank_num_sv9.png)}.ranking--list__rank.gold .ranking--list__num.num1{background-image:url(/vendor/maimai/img/ranking/rank_num_gold1.png);height:6.125vw;width:5vw}@media only screen and (min-width:769px){.ranking--list__rank.gold .ranking--list__num.num1{height:4.5454545455vw;width:3.7272727273vw}}@media only screen and (min-width:1100px){.ranking--list__rank.gold .ranking--list__num.num1{height:50px;width:41px}}.ranking--list__rank.bronze .ranking--list__num.num0{background-image:url(/vendor/maimai/img/ranking/rank_num_bz0.png);height:6.25vw;width:5.375vw}@media only screen and (min-width:769px){.ranking--list__rank.bronze .ranking--list__num.num0{height:4.5454545455vw;width:3.9090909091vw}}@media only screen and (min-width:1100px){.ranking--list__rank.bronze .ranking--list__num.num0{height:50px;width:43px}}.ranking--list__rank.bronze .ranking--list__num.num1{background-image:url(/vendor/maimai/img/ranking/rank_num_bz1.png);width:5.125vw}@media only screen and (min-width:769px){.ranking--list__rank.bronze .ranking--list__num.num1{width:3.7272727273vw}}@media only screen and (min-width:1100px){.ranking--list__rank.bronze .ranking--list__num.num1{width:41px}}.ranking--list__rank.bronze .ranking--list__num.num2{background-image:url(/vendor/maimai/img/ranking/rank_num_bz2.png)}.ranking--list__rank.bronze .ranking--list__num.num3{background-image:url(/vendor/maimai/img/ranking/rank_num_bz3.png)}.ranking--list__rank.bronze .ranking--list__num.num4{background-image:url(/vendor/maimai/img/ranking/rank_num_bz4.png)}.ranking--list__rank.bronze .ranking--list__num.num5{background-image:url(/vendor/maimai/img/ranking/rank_num_bz5.png)}.ranking--list__rank.bronze .ranking--list__num.num6{background-image:url(/vendor/maimai/img/ranking/rank_num_bz6.png)}.ranking--list__rank.bronze .ranking--list__num.num7{background-image:url(/vendor/maimai/img/ranking/rank_num_bz7.png)}.ranking--list__rank.bronze .ranking--list__num.num8{background-image:url(/vendor/maimai/img/ranking/rank_num_bz8.png)}.ranking--list__rank.bronze .ranking--list__num.num9{background-image:url(/vendor/maimai/img/ranking/rank_num_bz9.png)}.ranking--list__rank.bronze .ranking--list__num.num100{background-image:url(/vendor/maimai/img/ranking/rank_num_bz100.png);width:14.125vw}@media only screen and (min-width:769px){.ranking--list__rank.bronze .ranking--list__num.num100{height:4.6363636364vw;width:10.2727272727vw}}@media only screen and (min-width:1100px){.ranking--list__rank.bronze .ranking--list__num.num100{height:51px;width:113px}}.ranking--list__dtl{margin-left:3.75vw;width:57.5vw}@media only screen and (min-width:769px){.ranking--list__dtl{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:baseline;margin-left:0;width:60.9090909091vw}}@media only screen and (min-width:1100px){.ranking--list__dtl{width:670px}}.ranking--list__dtl__txt{display:flex;flex-wrap:wrap;align-items:baseline;font-weight:800}@media only screen and (min-width:769px){.ranking--list__dtl__txt{align-items:center}}.ranking--list__dtl__txt .tag{background-color:#0b3871;border-radius:1.375vw;color:#fff;display:inline-block;font-size:2.5vw;padding:.125vw 0 .25vw;text-align:center;width:10.5vw}@media only screen and (min-width:769px){.ranking--list__dtl__txt .tag{border-radius:.6363636364vw;font-size:1.0909090909vw;padding:.0909090909vw 0;margin-top:.4545454545vw;width:4.5454545455vw}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt .tag{border-radius:7px;font-size:12px;padding:1px 0;margin-top:5px;width:50px}}.ranking--list__dtl__txt .name,.ranking--list__dtl__txt .point{font-size:3.5vw;margin-left:1.875vw}@media only screen and (min-width:769px){.ranking--list__dtl__txt .name,.ranking--list__dtl__txt .point{font-size:2.1818181818vw;margin-left:1.8181818182vw}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt .name,.ranking--list__dtl__txt .point{font-size:24px;margin-left:20px}}.ranking--list__dtl__txt:last-of-type{margin-top:2.75vw;padding-top:2.375vw;position:relative}@media only screen and (min-width:769px){.ranking--list__dtl__txt:last-of-type{margin-top:0;padding:1.6363636364vw 1.0909090909vw 1.6363636364vw 2.5454545455vw}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt:last-of-type{padding:18px 12px 18px 28px}}.ranking--list__dtl__txt:last-of-type:before{content:"";display:block;background:transparent url(/vendor/maimai/img/ranking/line_dashed_sp.png) no-repeat;background-size:57.5vw auto;height:.5vw;width:57.5vw;position:absolute;top:0;right:-.625vw}@media only screen and (min-width:769px){.ranking--list__dtl__txt:last-of-type:before{background-image:url(/vendor/maimai/img/ranking/line_dashed_pc.png);background-size:.3636363636vw auto;height:5.4545454545vw;width:.3636363636vw;right:initial;left:0}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt:last-of-type:before{background-size:4px auto;height:60px;width:4px}}@media only screen and (min-width:769px){.ranking--list__dtl__txt:first-of-type{width:35.4545454545vw}}@media only screen and (min-width:1100px){.ranking--list__dtl__txt:first-of-type{width:390px}}::selection{background:#001518;color:#ffcedf}body,html{height:100%;color:#000;background-color:#fff;font-family:"M PLUS 1p",sans-serif;-webkit-font-smoothing:antialiased}:focus{outline:0}*{box-sizing:border-box}body{width:100%}a{color:#000;text-decoration:none}h1,h2,h3{line-height:1}li,ul{list-style:none}td,th,tr{font-weight:400;text-align:left}img,picture{width:100%}.pc-none{display:none}@media only screen and (max-width:769px){.pc-none{display:block}}@media only screen and (max-width:769px){.sp-none{display:none}}.wrapper{position:relative;z-index:2} diff --git a/scripts/dan-settlement-e2e.mjs b/scripts/dan-settlement-e2e.mjs new file mode 100644 index 0000000..5fba8f4 --- /dev/null +++ b/scripts/dan-settlement-e2e.mjs @@ -0,0 +1,588 @@ +/** + * 段位認定结算页的浏览器 E2E:真的起一个 headless Edge,打开 /dan.html, + * 把整轮结算那一屏量一遍,顺手出三张图给 paint-probe.py 数像素: + * + * 1. 四道都打满(SSS+,达成率全金)—— 版式与着色是否照官方那套 + * 2. 四道达成率故意拉开(金 / 蓝 / 红)—— 分数分色到底有没有落实 + * 3. 一道超时取消 —— 不合格那一屏的占位符与红心 + * + * 需要本地已经跑着一个服务(默认 `127.0.0.1:8789`,库 `data/dan-ui.sqlite`): + * + * node scripts/dan-settlement-e2e.mjs + * DAN_BASE=http://127.0.0.1:8788 DAN_DB=data/demo-b50.sqlite node scripts/dan-settlement-e2e.mjs + * + * 这不是 `node --test` 那套单测(那些不碰浏览器);这个脚本要 Edge、要一个跑着的服务, + * 所以不进 CI,只当本地验收用。段位规则本身由 tests/dan.test.ts 覆盖。 + */ +import { spawn } from 'node:child_process'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { DatabaseSync } from 'node:sqlite'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const DB = process.env.DAN_DB ? resolve(process.env.DAN_DB) : join(REPO, 'data', 'dan-ui.sqlite'); +const BASE = process.env.DAN_BASE ?? 'http://127.0.0.1:8789'; +const SHOTS = process.env.DAN_SHOTS ?? join(tmpdir(), 'dan-e2e-shots'); +const EDGE = process.env.DAN_EDGE ?? 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'; +const USER = Number(process.env.DAN_USER ?? 1); + +/** 结算盘坐标:底图 980×928,逐题行与底部三块都按底图里量出来的位置。 */ +const ROW = { left: 267, width: 576, tops: [152, 274, 396, 518] }; +const PLATE = { left: 74, top: 672, w: 240, h: 230 }; // 左下段位名牌 +const LIFE = { left: 334, top: 690, w: 200, h: 200 }; +const TOTAL_LABEL = { left: 503, top: 663, w: 354, h: 52 }; // 底图预留的白格 +const DX_ROW = { left: 608, top: 810, w: 360, h: 80 }; // 底图预留的白牌 +const RIGHT_EDGE = 950; // 大数字的右端(与 DX 白牌右端同一条边) +/** + * 逐题行内部的版式(**行内坐标**)。每个数字都是从原版 `UI_CMN_RSL_KopMBase_MST.png` + * (636×136 的精裁版,官方设计尺寸 640×140)逐行扫像素量出来的,换算见 public/dan.css: + * `行内坐标 = (原版文件坐标 + 2) × 0.9`(我们的行内区 576×126 = 官方 640×140 × 0.9)。 + */ +const CARD = { + frame: { left: 3.6, top: 3.6, w: 568.8, h: 118.8 }, // 边框本体(外圈 2px 主色环) + slot: { left: 10.8, top: 10.8, w: 105.3, h: 105.3 }, // 左上的曲绘槽(原版是方槽) + titlebar: { left: 122.4, top: 10.8, w: 442.8, h: 18 }, // 深蓝歌名条(占顶部一整行) + achbox: { left: 122.4, top: 36, w: 328.5, h: 61.2 }, // 白色达成率盒(右下角一道大 R 弧) + band: { left: 122.4, top: 97.2, w: 345.6, h: 23.4 }, // 底部浅色带 + dxbox: { left: 450.9, top: 97.2, w: 119.7, h: 23.4 }, // 右下角 DX 分数白牌(胶囊) + badge: { left: 464, top: 43, w: 99, h: 40 }, // 右上紫场里的评级徽章 +}; +const near = (value, want, tol = 2) => Math.abs(value - want) <= tol; +const rectNear = (rect, want, tol = 2, keys = ['left', 'top', 'w', 'h']) => Boolean(rect) + && keys.every((key) => near(rect[key], want[key], tol)); +const rectIn = (inner, outer, slack = 2) => Boolean(inner && outer) + && inner.left >= outer.left - slack && inner.top >= outer.top - slack + && inner.left + inner.w <= outer.left + outer.w + slack + && inner.top + inner.h <= outer.top + outer.h + slack; +const fmtRect = (rect) => (rect ? `${rect.left},${rect.top} ${rect.w}×${rect.h}` : '缺失'); +/** 等级 → 素材:官方 `UI_GAM_Rank_*`(游戏内那套单级全套)。 */ +const RANK_FILE = { + 'SSS+': 'gam-rank-sssp.png', SSS: 'gam-rank-sss.png', 'SS+': 'gam-rank-ssp.png', SS: 'gam-rank-ss.png', + 'S+': 'gam-rank-sp.png', S: 'gam-rank-s.png', AAA: 'gam-rank-aaa.png', AA: 'gam-rank-aa.png', + A: 'gam-rank-a.png', BBB: 'gam-rank-bbb.png', BB: 'gam-rank-bb.png', B: 'gam-rank-b.png', + C: 'gam-rank-c.png', D: 'gam-rank-d.png', +}; + +let failures = 0; +const check = (label, ok, extra = '') => { + console.log(`${ok ? ' PASS' : ' FAIL'} ${label}${extra ? ` — ${extra}` : ''}`); + if (!ok) failures += 1; +}; +const open = () => { + const db = new DatabaseSync(DB); + db.exec('PRAGMA busy_timeout = 8000'); + return db; +}; +const call = async (path, body) => { + const res = await fetch(`${BASE}${path}`, body + ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } + : { cache: 'no-store' }); + return { status: res.status, body: await res.json() }; +}; +const run = async (path, body) => { + const res = await call(path, body); + if (res.status !== 200) throw new Error(`${path} → HTTP ${res.status} ${JSON.stringify(res.body)}`); + return res.body; +}; + +if (!existsSync(EDGE)) { + console.log(`Edge 不在 ${EDGE},跳过浏览器验收`); + process.exit(0); +} +if (!existsSync(SHOTS)) mkdirSync(SHOTS, { recursive: true }); + +/** 题目名:先看本人提交里的真名,没有就尽力从 CF 全量清单里查一次,都没有就退回题号。 */ +let cfNames = null; +const problemName = async (problemId) => { + const known = open().prepare( + "SELECT MAX(problem_title) AS title FROM submissions WHERE platform='codeforces' AND problem_id=?").get(problemId); + if (known?.title) return known.title; + if (cfNames === null) { + cfNames = new Map(); + try { + const res = await fetch('https://codeforces.com/api/problemset.problems', { signal: AbortSignal.timeout(20000) }); + const body = await res.json(); + for (const p of body?.result?.problems ?? []) cfNames.set(`${p.contestId}:${p.index}`, p.name); + } catch { /* 离线就用题号,不影响验收 */ } + } + return cfNames.get(problemId) ?? problemId; +}; + +const { secondsForAchievement } = await import(pathToFileURL(join(REPO, 'src', 'dx', 'rating.ts')).href); + +/* ---------- 造一轮:四道都按指定达成率通关 ---------- */ + +const seeded = []; + +async function clearRun({ tier, kind, achievements }) { + seeded.length = 0; + const started = await run('/api/dan/start', { userId: USER, kind, tier, tz: 480 }); + const sessionId = started.session.id; + const stageCount = started.session.stageCount; + for (let round = 1; round <= stageCount; round += 1) { + await run('/api/dan/claim', { userId: USER, sessionId }); + const timer = open().prepare( + "SELECT id, problem_id FROM practice_timers WHERE status='running' ORDER BY started_at DESC LIMIT 1").get(); + const stage = open().prepare( + 'SELECT difficulty FROM dan_stages WHERE session_id=? AND stage_index=?').get(sessionId, round); + const now = Math.floor(Date.now() / 1000); + const target = achievements[round - 1]; + // `SLOW_SECONDS_FACTOR[i]` 把第 i 道的时间乘一个系数 —— 评分曲线只在 ≥97% 那段可逆, + const seconds = Math.round(secondsForAchievement(stage.difficulty, target).seconds); + const title = await problemName(timer.problem_id); + const write = open(); + write.prepare('UPDATE practice_timers SET started_at=? WHERE id=?').run(now - seconds, timer.id); + write.prepare(`INSERT INTO submissions(account_id,platform,submission_id,problem_id,problem_title,status,submitted_at,difficulty) + VALUES(?,?,?,?,?,?,?,?)`).run(accountId, 'codeforces', `dan-e2e-${sessionId.slice(0, 6)}-${round}-${now}`, + timer.problem_id, title, 'AC', now, stage.difficulty); + write.close(); + seeded.push({ sessionId, index: round, title }); + await run('/api/dan/settle', { userId: USER, tz: 480 }); + if (round < stageCount) await run('/api/dan/next', { userId: USER, tz: 480 }); + } + const row = (await call(`/api/dan?user=${USER}&tz=480`)).body.history.find((r) => r.id === sessionId); + return row; +} + +// 清场:收掉遗留的活动会话、清掉 dan 记录,从零开始。 +const overview = (await call(`/api/dan?user=${USER}&tz=480`)).body; +if (overview.active) await call('/api/dan/abandon', { userId: USER, sessionId: overview.active.id }); +{ + const clean = open(); + clean.exec('DELETE FROM dan_stages; DELETE FROM dan_sessions; DELETE FROM practice_timers;'); + clean.close(); +} +const accountId = Number(open().prepare( + "SELECT id FROM accounts WHERE platform='codeforces' AND is_archived=0 ORDER BY id LIMIT 1").get().id); +console.log(`账号 id=${accountId},库 ${DB},服务 ${BASE},截图 ${SHOTS}`); + +console.log('\n=== 1) 四道都打满(SSS+) ==='); +const perfect = await clearRun({ tier: 'advanced', kind: 'challenge', achievements: [100.8, 100.8, 100.8, 100.8] }); +check('这一轮判为 cleared', perfect?.status === 'cleared', `${perfect?.status}`); +check('四道都记了达成率与单题 rating', + perfect?.stages.every((s) => typeof s.achievement === 'number' && typeof s.rating === 'number'), + perfect?.stages.map((s) => `${s.difficulty}:${s.achievement.toFixed(2)}%/${s.rating}`).join(' ')); +const expectAch = perfect.stages.reduce((sum, s) => sum + s.achievement, 0); +const expectRating = Math.round(perfect.stages.reduce((sum, s) => sum + s.rating, 0) * 10) / 10; +check('本轮总分 = 四道单题 rating 之和', Math.abs(perfect.totalRating - expectRating) < 0.05, + `${perfect.totalRating} vs ${expectRating}`); + +/* ---------- 起浏览器 ---------- */ + +const dir = mkdtempSync(join(tmpdir(), 'dan-e2e-')); +const edge = spawn(EDGE, ['--headless=new', '--remote-debugging-port=0', `--user-data-dir=${dir}`, + '--no-first-run', '--no-default-browser-check', '--window-size=1440,1400', 'about:blank'], + { stdio: 'ignore', windowsHide: true }); + +let cdp = null; +const problems = []; +const badResponses = []; +try { + for (let i = 0; i < 40 && !cdp; i += 1) { + await sleep(250); + try { + const [port, wsPath] = readFileSync(join(dir, 'DevToolsActivePort'), 'utf8').trim().split(/\r?\n/); + cdp = await (await import(pathToFileURL(join(REPO, 'src', 'fetchers', 'cf-browser.ts')).href)) + .Cdp.connect(`ws://127.0.0.1:${port}${wsPath}`); + } catch { /* Edge 还没起来 */ } + } + if (!cdp) throw new Error('没连上 Edge 的 CDP'); + const { targetId } = await cdp.call('Target.createTarget', { url: 'about:blank' }); + const { sessionId } = await cdp.call('Target.attachToTarget', { targetId, flatten: true }); + cdp.socket.addEventListener('message', (event) => { + const message = JSON.parse(String(event.data)); + if (message.method === 'Runtime.exceptionThrown') { + problems.push(`异常: ${message.params.exceptionDetails.text} ${message.params.exceptionDetails.exception?.description ?? ''}`); + } + if (message.method === 'Runtime.consoleAPICalled' && ['error', 'warning'].includes(message.params.type)) { + problems.push(`console.${message.params.type}: ${message.params.args.map((a) => a.value ?? a.description).join(' ')}`); + } + if (message.method === 'Network.responseReceived') { + const { url, status } = message.params.response; + if (status >= 400 && url.includes('127.0.0.1')) badResponses.push(`${status} ${url}`); + } + }); + for (const domain of ['Runtime', 'Log', 'Page', 'Network']) await cdp.call(`${domain}.enable`, {}, sessionId); + await cdp.call('Emulation.setDeviceMetricsOverride', + { width: 1440, height: 1400, deviceScaleFactor: 1, mobile: false }, sessionId); + + const evaluate = async (expression) => { + const result = await cdp.call('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }, sessionId); + if (result.exceptionDetails) throw new Error(`页面内异常 ${JSON.stringify(result.exceptionDetails)}`); + return result.result.value; + }; + const openPage = async (url, wait = 3200) => { await cdp.call('Page.navigate', { url }, sessionId); await sleep(wait); }; + const shot = async (name) => { + const { data } = await cdp.call('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true }, sessionId); + writeFileSync(join(SHOTS, name), Buffer.from(data, 'base64')); + console.log(` 截图 → ${join(SHOTS, name)}`); + }; + /** 只拍结算盘本身(文档插图用,不带上页面下那堆说明)。 */ + const shotDialog = async (name) => { + const rect = JSON.parse(await evaluate(`(() => { + const d = document.querySelector('dialog.dan-result'); + if (!d) return 'null'; + const r = d.getBoundingClientRect(); + return JSON.stringify({ x: r.left + window.scrollX, y: r.top + window.scrollY, + width: r.width, height: r.height }); + })()`)); + if (!rect) return; + const { data } = await cdp.call('Page.captureScreenshot', + { format: 'png', clip: { ...rect, scale: 2 }, captureBeyondViewport: true }, sessionId); + writeFileSync(join(SHOTS, name), Buffer.from(data, 'base64')); + console.log(` 截图 → ${join(SHOTS, name)}`); + }; + const clickByText = (selector, text) => evaluate(`(() => { + const node = [...document.querySelectorAll(${JSON.stringify(selector)})].find((n) => n.textContent.includes(${JSON.stringify(text)})); + if (!node) return 'NOT_FOUND'; + node.click(); + return 'CLICKED'; + })()`); + /** 打开最新一轮的结算页:认定记录里那条「查看结算」。 */ + const openLatestResult = async () => { + await openPage(`${BASE}/dan.html`); + const clicked = await clickByText('.dan-actions .btn', '查看结算'); + await sleep(1500); + return clicked; + }; + /** 读结算盘:行内组件用**行底板**做原点,底部组件用底图做原点(都是底图 980×928 那一套坐标)。 */ + const readLayout = () => evaluate(`(() => { + const dialog = document.querySelector('dialog.dan-result'); + const art = dialog?.querySelector('.dan-result-art'); + if (!art) return JSON.stringify({ open: false, rows: [] }); + const box = art.getBoundingClientRect(); + const relTo = (origin, node) => { + if (!node) return null; + const r = node.getBoundingClientRect(); + return { left: Math.round(r.left - origin.left), top: Math.round(r.top - origin.top), + w: Math.round(r.width), h: Math.round(r.height) }; + }; + const rel = (node) => relTo(box, node); + const text = (root, sel) => root.querySelector(sel)?.textContent ?? null; + // aria-label 挂在最里面那层 .dan-num 上;选择器可能指的是包着它的容器。 + const numNode = (root, sel) => { + const node = root.querySelector(sel); + if (!node) return null; + return node.classList.contains('dan-num') ? node : node.querySelector('.dan-num'); + }; + const shown = (root, sel) => numNode(root, sel)?.getAttribute('aria-label') ?? null; + const font = (root, sel) => { + const node = numNode(root, sel); + if (!node) return null; + // 真正上色/上贴图的是里面那两层 :分数族走 background-image(自带颜色), + // 白字族走 mask + background-color(游戏里就是这样运行时染色的)。 + const fill = node.querySelector('.dan-num-fill'); + const style = getComputedStyle(fill ?? node); + const cell = node.querySelector('.dan-num-char'); + const cellStyle = cell ? getComputedStyle(cell) : null; + const advance = cell + ? cell.getBoundingClientRect().width + (parseFloat(cellStyle.marginRight) || 0) : 0; + // 图集里没有的字符(「—」这种占位符)走普通文字那条路:颜色落在文字本身上。 + const plain = node.querySelector('.dan-num-plain'); + return { + fill: plain ? getComputedStyle(plain).color : style.backgroundColor, + mask: style.maskImage || style.webkitMaskImage, + atlas: style.backgroundImage, + outline: Boolean(node.querySelector('.dan-num-outline')), + label: node.getAttribute('aria-label'), + height: Math.round(node.getBoundingClientRect().height), + advance: Math.round(advance * 100) / 100, + width: Math.round(node.getBoundingClientRect().width), + }; + }; + return JSON.stringify({ + open: Boolean(dialog?.open), + plate: { rect: rel(dialog.querySelector('.dan-verdict')), name: text(dialog, '.dan-plate-name'), + nameLen: dialog.querySelector('.dan-plate-name')?.dataset.len ?? null, + nameFont: (() => { const n = dialog.querySelector('.dan-plate-name'); if (!n) return null; + const s = getComputedStyle(n); return { size: s.fontSize, weight: s.fontWeight, fill: s.color }; })(), + count: text(dialog, '.dan-verdict-count'), + legacyStamp: Boolean(dialog.querySelector('.dan-verdict-img')), + nameRect: rel(dialog.querySelector('.dan-plate-name')) }, + life: { rect: rel(dialog.querySelector('.dan-life')), label: shown(dialog, '.dan-life-count'), + base: dialog.querySelector('.dan-life-base')?.getAttribute('src')?.split('/').pop() ?? null }, + total: { label: text(dialog, '.dan-total-label'), labelRect: rel(dialog.querySelector('.dan-total-label')), + num: shown(dialog, '.dan-total-num'), rect: rel(dialog.querySelector('.dan-total-num')), + font: font(dialog, '.dan-total-num') }, + dx: { label: text(dialog, '.dan-dxscore-label'), num: shown(dialog, '.dan-dxscore-num'), + rect: rel(numNode(dialog, '.dan-dxscore-num')), rowRect: rel(dialog.querySelector('.dan-dxscore-row')), + labelRect: rel(dialog.querySelector('.dan-dxscore-label')), font: font(dialog, '.dan-dxscore-num') }, + rows: [...dialog.querySelectorAll('.dan-track')].map((track) => { + const rowBox = track.getBoundingClientRect(); + return { + rect: rel(track), + no: text(track, '.dan-track-no'), name: text(track, '.dan-track-name'), + // 档位名牌已经删掉;这两项留着是为了断言它**不在**(并且标题条左移顶上那一格)。 + diff: text(track, '.dan-track-diff'), diffLines: track.querySelectorAll('.dan-track-diff > *').length, + titlebar: relTo(rowBox, track.querySelector('.dan-track-titlebar')), + jacket: text(track, '.dan-track-jacket'), jacketBig: text(track, '.dan-track-jacket b'), + jacketSmall: text(track, '.dan-track-jacket small'), + ach: shown(track, '.dan-track-achvalue'), achFont: font(track, '.dan-track-achvalue'), + // 达成率那串数字自己的框(取里面那层 .dan-num,不是外面那个定宽容器)。 + achNumRect: relTo(rowBox, numNode(track, '.dan-track-achvalue')), + dx: shown(track, '.dan-track-dxnum'), dxFont: font(track, '.dan-track-dxnum'), + limit: text(track, '.dan-track-limit'), judge: text(track, '.dan-track-judge'), + badge: track.querySelector('.dan-track-badge')?.getAttribute('src')?.split('/').pop() ?? '', + // badgeRect / stamp 是**底图坐标**(抠图用),badgeRow 才是行内坐标。 + badgeRect: rel(track.querySelector('.dan-track-badge')), + badgeRow: relTo(rowBox, track.querySelector('.dan-track-badge')), + stamp: rel(track.querySelector('.dan-track-stamp')), + boxes: ['no', 'jacket', 'dxbox', 'dxnum', 'titlebar', 'name', 'diff', 'achbox', 'achvalue', + 'badge', 'limit', 'frame', 'band'] + .map((key) => [key, relTo(rowBox, track.querySelector('.dan-track-' + key))]).filter(([, r]) => r), + }; + }), + }); + })()`); + + console.log('\n=== 2) 结算页版式(四道满分成 → 金) ==='); + check('认定记录里点得到「查看结算」', await openLatestResult() === 'CLICKED'); + const page = JSON.parse(await readLayout()); + check('结算页弹出来了', page.open === true); + const rows = page.rows ?? []; + check('四道各一张卡', rows.length === 4, `${rows.length} 张`); + check('四张卡按官方坐标摆:x 267 宽 576,行距 122', + rows.every((row, i) => Math.abs(row.rect.left - ROW.left) <= 2 && Math.abs(row.rect.w - ROW.width) <= 2 + && Math.abs(row.rect.top - ROW.tops[i]) <= 2), + rows.map((row) => `${row.rect.top}@${row.rect.w}`).join(' ')); + const outOfRow = []; + for (const row of rows) { + for (const [key, rect] of row.boxes) { + if (rect.left < -2 || rect.top < -2 || rect.left + rect.w > row.rect.w + 2 || rect.top + rect.h > row.rect.h + 2) { + outOfRow.push(`${row.no}:${key}`); + } + } + } + check('行内组件都待在底板里', outOfRow.length === 0, outOfRow.join(',')); + /** 行内子组件的 rect(`boxes` 里量的,全部是**行内坐标**)。 */ + const boxOf = (row, key) => (row.boxes.find(([name]) => name === key) ?? [])[1]; + check('边框本体照原版量出来的比例摆(行内 3.6,3.6 / 568.8×118.8)', + rows.every((row) => rectNear(boxOf(row, 'frame'), CARD.frame)), + fmtRect(boxOf(rows[0], 'frame'))); + check('曲绘槽是原版那块方槽(行内 10.8,10.8 / 105.3×105.3),里面写完整题号 + 这一题的 CF rating', + rows.every((row) => rectNear(boxOf(row, 'jacket'), CARD.slot) + && /^\d+[A-Z]+\d*$/.test(row.jacketBig) && /^\d+$/.test(row.jacketSmall)), + `${fmtRect(boxOf(rows[0], 'jacket'))} · ${rows[0].jacketBig}/${rows[0].jacketSmall}`); + check('难度名牌已删掉,歌名条占顶部一整行(左端与白盒左端对齐、右端贴到边框内右沿)', + rows.every((row) => !row.diff && row.diffLines === 0 + && rectNear(row.titlebar, CARD.titlebar) + // 「占一整行」= 左端跟白盒左端对齐(原版那格难度名牌的位置被歌名条顶掉), + // 右端离边框外沿只剩 7.2px(原版 8px × 0.9)。 + && near(row.titlebar.left, boxOf(row, 'achbox').left) + && near(row.titlebar.left + row.titlebar.w, CARD.frame.left + CARD.frame.w - 7.2) + && near(row.titlebar.top, CARD.frame.top + 7.2)), + rows.map((row) => `diff=${row.diff || '无'} 条 ${row.titlebar?.left}…${row.titlebar?.left + row.titlebar?.w}`).join(' ')); + // 达成率那串数字必须待在白盒里,而且白盒不能碰到右边那块紫场里的评级徽章 + // (原版白盒右下角那道大 R 弧就是给徽章让位的)。 + const spill = rows.map((row) => { + const box = boxOf(row, 'achbox'); + const num = row.achNumRect; + if (!box || !num) return 'no-box'; + const right = num.left + num.w; + if (num.left < box.left) return `数字出盒左 ${num.left} < ${box.left}`; + if (right > box.left + box.w - 6) return `数字出盒右 ${right} > ${box.left + box.w - 6}`; + // 右对齐:整串的右端固定在离白盒右沿 6…20px 的地方(短一位数的串不许往左缩)。 + if (right < box.left + box.w - 20) return `数字没右对齐 ${right} < ${box.left + box.w - 20}`; + if (num.top < box.top || num.top + num.h > box.top + box.h + 1) { + return `数字出盒高 ${num.top}..${num.top + num.h} vs ${box.top}..${box.top + box.h}`; + } + if (row.badgeRow && box.left + box.w + 10 > row.badgeRow.left) { + return `白盒碰徽章 ${Math.round(box.left + box.w + 10 - row.badgeRow.left)}px`; + } + return ''; + }).filter(Boolean); + check('达成率数字不溢出白盒、白盒与右侧徽章留 10px 以上空隙', + spill.length === 0, + spill.join(',') || `数字 ${fmtRect(rows[0].achNumRect)} 盒 ${fmtRect(boxOf(rows[0], 'achbox'))}` + + ` 徽章左 ${rows[0].badgeRow?.left}`); + check('DX 分数在卡片右下角、右端贴到边框内右沿(与底带同高的那块白牌)', + rows.every((row) => { + const dx = boxOf(row, 'dxbox'); + const box = boxOf(row, 'achbox'); + return rectNear(dx, CARD.dxbox) + && near(dx.left + dx.w, CARD.frame.left + CARD.frame.w - 1.8) + && dx.top >= box.top + box.h - 2 && dx.left >= box.left + box.w; + }), + fmtRect(boxOf(rows[0], 'dxbox'))); + check('限时写进底带左侧(原版那条浅色带,右端才是 DX 分数白牌)', + rows.every((row) => { + const band = boxOf(row, 'band'); + const limit = boxOf(row, 'limit'); + const dx = boxOf(row, 'dxbox'); + return rectNear(band, CARD.band) && rectIn(limit, band, 0) && near(limit.left, band.left) + && limit.left + limit.w <= dx.left + 1; + }), + `带 ${fmtRect(boxOf(rows[0], 'band'))} · 限时 ${fmtRect(boxOf(rows[0], 'limit'))}`); + check('评级徽章放进右上那块紫场(白盒右边 · 歌名条下边 · 白牌上边)', + rows.every((row) => { + if (!row.badgeRow) return true; // 超时/未开始那一屏用文字判定,没有徽章 + const box = boxOf(row, 'achbox'); + const dx = boxOf(row, 'dxbox'); + return rectNear(row.badgeRow, CARD.badge) + && row.badgeRow.left >= box.left + box.w + && row.badgeRow.top >= row.titlebar.top + row.titlebar.h + && row.badgeRow.top + row.badgeRow.h <= dx.top; + }), + fmtRect(rows[0].badgeRow)); + check('标题条写题目名,不是「段位認定第 N 道」', + rows.every((row, i) => row.name && row.name === seeded[i]?.title && !/段位認定第/.test(row.name)), + rows.map((row) => row.name).join(' | ')); + check('评级徽章是游戏内那套单级素材且按原版比例放到 99×40(不再拿「A~AAA」区间图充数)', + rows.every((row) => row.badge === 'gam-rank-sssp.png' && Math.abs(row.badgeRect.w - CARD.badge.w) <= 2 + && Math.abs(row.badgeRect.h - CARD.badge.h) <= 2), + `${rows[0]?.badge} ${rows[0]?.badgeRect?.w}×${rows[0]?.badgeRect?.h}`); + check('四道达成率都上了官方金色图集', + rows.every((row) => /num-score-gold/.test(row.achFont?.atlas ?? '')), rows.map((row) => row.achFont?.atlas).join(' | ')); + check('单题 DX 分数不叠描边层(小字要干净)', + rows.every((row) => row.dxFont?.outline === false && /rgb\(13, 42, 99\)/.test(row.dxFont?.fill ?? '')), + `描边层 ${rows[0]?.dxFont?.outline} / ${rows[0]?.dxFont?.fill}`); + + check('左下白框里写档位名(不再是合格印)', + page.plate.name === '上级' && page.plate.legacyStamp === false, `${page.plate.name} · 旧印 ${page.plate.legacyStamp}`); + check('段位名落在白框里、字重够重', + page.plate.rect.left === PLATE.left && page.plate.rect.top === PLATE.top + && page.plate.rect.w === PLATE.w && page.plate.rect.h === PLATE.h && Number(page.plate.nameFont.weight) >= 800, + `${page.plate.rect.left},${page.plate.rect.top} ${page.plate.rect.w}×${page.plate.rect.h} · ${page.plate.nameFont.size}/${page.plate.nameFont.weight}`); + check('结论变成名字下面一行小字', /合格 · 4 \/ 4 道通关/.test(page.plate.count ?? ''), page.plate.count); + check('心放大到 200×200 且用官方绿色底盘,心里写通关道数', + page.life.rect.w === LIFE.w && page.life.rect.h === LIFE.h + && page.life.base === 'life-base-green.png' && page.life.label === '4', + `${page.life.rect.w}×${page.life.rect.h} · ${page.life.base} · ${page.life.label}`); + + check('「总达成率」标签落进底图预留的白格', + Math.abs(page.total.labelRect.left - TOTAL_LABEL.left) <= 2 && Math.abs(page.total.labelRect.top - TOTAL_LABEL.top) <= 2 + && Math.abs(page.total.labelRect.w - TOTAL_LABEL.w) <= 2 && Math.abs(page.total.labelRect.h - TOTAL_LABEL.h) <= 2, + `${page.total.labelRect.left},${page.total.labelRect.top} ${page.total.labelRect.w}×${page.total.labelRect.h}`); + check('总达成率 = 四道达成率之和(官方金色图集)', + Math.abs(Number(String(page.total.num).replace('%', '')) - expectAch) < 0.01 && /%$/.test(page.total.num ?? ''), + `${page.total.num} vs ${expectAch.toFixed(4)}%`); + const totalRight = page.total.rect.left + page.total.rect.w; + check('大数字落在色带上、右端对齐 x 950', + Math.abs(totalRight - RIGHT_EDGE) <= 6 && page.total.rect.top >= 700 && page.total.rect.top + page.total.rect.h <= DX_ROW.top, + `右 ${totalRight} · y ${page.total.rect.top}..${page.total.rect.top + page.total.rect.h}`); + const glyphs = String(page.total.num).length + 1; + // 官方是密排:分数族那套图集格宽 74 / 格高 98,密排后每字步进 67 ⇒ 0.68 上下。 + check('数字密排(官方字距,不再一个字一个字分开)', + page.total.font.advance / page.total.font.height <= 0.69, + `每字步进 ${page.total.font.advance}px / 字高 ${page.total.font.height} = ${(page.total.font.advance / page.total.font.height).toFixed(3)}(占位 ${glyphs} 个字形,整串宽 ${page.total.font.width})`); + + check('DX 分数条就是底图预留的那条白牌', + Math.abs(page.dx.rowRect.left - DX_ROW.left) <= 2 && Math.abs(page.dx.rowRect.top - DX_ROW.top) <= 2 + && Math.abs(page.dx.rowRect.w - DX_ROW.w) <= 2 && Math.abs(page.dx.rowRect.h - DX_ROW.h) <= 2, + `${page.dx.rowRect.left},${page.dx.rowRect.top} ${page.dx.rowRect.w}×${page.dx.rowRect.h}`); + check('标签写「DX分数」(照官方,不再自造「合计」)', page.dx.label === 'DX分数', page.dx.label); + check('DX 分数 = 四道单题 rating 之和(深蓝、不带描边)', + Math.abs(Number(page.dx.num) - expectRating) < 0.05 && /rgb\(13, 42, 99\)/.test(page.dx.font?.fill ?? '') + && page.dx.font?.outline === false, + `${page.dx.num} vs ${expectRating} · ${page.dx.font?.fill} · 描边层 ${page.dx.font?.outline}`); + check('DX 数字在白牌右半边、且没出牌面', + page.dx.rect.left >= DX_ROW.left + 120 && page.dx.rect.left + page.dx.rect.w <= DX_ROW.left + DX_ROW.w + 2, + `${page.dx.rect.left}..${page.dx.rect.left + page.dx.rect.w} 牌面 ${DX_ROW.left}..${DX_ROW.left + DX_ROW.w}`); + + await shot('E-整轮结算页.png'); + await shotDialog('D-结算盘.png'); + // 给 paint-probe.py 数像素用的小图(按底图坐标抠) + const origin = JSON.parse(await evaluate(`(() => { + const b = document.querySelector('dialog.dan-result .dan-result-art').getBoundingClientRect(); + return JSON.stringify({ x: b.left + window.scrollX, y: b.top + window.scrollY }); + })()`)); + const row0 = rows[0]; + // 行内那些小图是**行内相对坐标**,抠图要加上第 1 行在底图上的原点,否则会拍错地方; + // 印与徽章本来就是底图坐标(readLayout 里用 rel(art) 量的),不能再加一次。 + const inRow = (rel) => (rel ? { left: rel.left + row0.rect.left, top: rel.top + row0.rect.top, w: rel.w, h: rel.h } : null); + const rectOf = (key) => inRow((row0.boxes.find(([name]) => name === key) ?? [])[1]); + const clipTargets = { + ach: rectOf('achvalue'), achbox: rectOf('achbox'), score: rectOf('dxnum'), + stamp: row0.stamp, badge: row0.badgeRect, + total: page.total.rect, dxtotal: page.dx.rect, life: page.life.rect, + }; + const rectsOut = {}; + for (const [name, rect] of Object.entries(clipTargets)) { + if (!rect) continue; + const clip = { x: origin.x + rect.left, y: origin.y + rect.top, width: rect.w, height: rect.h, scale: 1 }; + rectsOut[name] = clip; + const { data } = await cdp.call('Page.captureScreenshot', { format: 'png', clip, captureBeyondViewport: true }, sessionId); + writeFileSync(join(SHOTS, `H-${name}.png`), Buffer.from(data, 'base64')); + } + writeFileSync(join(SHOTS, 'H-rects.json'), JSON.stringify(rectsOut, null, 2)); + + /* ---------- 3) 达成率拉开:金 / 蓝 / 红 ---------- */ + + console.log('\n=== 3) 四道达成率拉开(金 / 蓝 / 红) ==='); + /* 注:达成率是由目标值反推秒数再提交的,而评分曲线只在 ≥97% 段可逆 —— 想看到 <80% 的 + * 蓝档只能从秒数那头放大(见 SLOW_SECONDS_FACTOR)。第 4 道 ×1.6 落到 60% 上下 ⇒ 蓝。 */ + let slowSeconds = []; + const mixed = await clearRun({ tier: 'advanced', kind: 'challenge', achievements: [100.8, 98.5, 96.0, 92.0] }); + check('这一轮同样判为 cleared(限时内 AC 就算过,跟达成率高低无关)', mixed?.status === 'cleared', `${mixed?.status}`); + check('四道达成率确实拉开了', + new Set(mixed.stages.map((s) => Math.round(s.achievement))).size === 4, + mixed.stages.map((s) => `${s.achievement.toFixed(2)}%`).join(' ')); + await openLatestResult(); + const mixedPage = JSON.parse(await readLayout()); + const tones = mixedPage.rows.map((row) => (/num-score-(\w+)\.png/.exec(row.achFont?.atlas ?? '') ?? [])[1]); + check('颜色按达成率换:≥97 金、80~96.99 红(合成用例只够得到这两档)', + tones.join(',') === 'gold,gold,red,red', `${tones.join(',')} · ${mixed.stages.map((s) => `${s.achievement.toFixed(2)}%`).join(' ')}`); + check('评级徽章跟着达成率走(逐级对应,不再共用一个区间图)', + mixedPage.rows.every((row, i) => row.badge === RANK_FILE[mixed.stages[i].rank]), + mixedPage.rows.map((row, i) => `${mixed.stages[i].rank}=${row.badge}`).join(' ')); + check('总达成率是四道之和 ⇒ 仍然 ≥100% ⇒ 金', + /num-score-gold/.test(mixedPage.total.font?.atlas ?? '') && /%$/.test(mixedPage.total.num ?? ''), + `${mixedPage.total.num} · ${mixedPage.total.font?.atlas}`); + await shot('F-分数分色结算页.png'); + await shotDialog('D-结算盘-分色.png'); + + /* ---------- 4) 超时取消 ---------- */ + + console.log('\n=== 4) 超时取消那一轮 ==='); + const timedId = `e2e-timeout-${Date.now()}`; + const timerId = `${timedId}-1`; + { + const t = Math.floor(Date.now() / 1000); + const write = open(); + write.prepare(`INSERT INTO dan_sessions(id,user_id,tier,kind,stage_count,limit_seconds,min_rating,max_rating,status,started_at) + VALUES(?,?,?,?,?,?,?,?,?,?)`).run(timedId, USER, 'beginner', 'challenge', 4, 1800, 800, 1100, 'active', t + 1); + write.prepare(`INSERT INTO practice_timers(id,user_id,account_id,handle_key,problem_id,practice_kind,status,started_at,ended_at) + VALUES(?,?,?,?,?,?,?,?,?)`).run(timerId, USER, accountId, 'dan-e2e', '559:A', 'unknown', 'completed', t - 4000, t - 1000); + write.prepare(`INSERT INTO dan_stages(session_id,stage_index,problem_id,difficulty,drawn_at,claimed_at,timer_id,limit_seconds) + VALUES(?,?,?,?,?,?,?,?)`).run(timedId, 1, '559:A', 800, t - 4000, t - 4000, timerId, 1800); + write.close(); + } + await call('/api/dan/settle', { userId: USER, tz: 480 }); + const failed = (await call(`/api/dan?user=${USER}&tz=480`)).body.history.find((r) => r.id === timedId); + check('超时那一道被记为 timeout', failed?.stages[0]?.outcome === 'timeout', `${failed?.stages[0]?.outcome}`); + check('这一轮判为 failed,且没有继续抽第 2 道', failed?.status === 'failed' && failed?.stages.length === 1, + `${failed?.status} / ${failed?.stages.length} 道`); + await openLatestResult(); + const failPage = JSON.parse(await readLayout()); + check('失败那一屏也弹得出来', failPage.open === true); + check('档位名照样写在白框里,结论是小字「不合格」', + failPage.plate.name === '初级' && /不合格 · 0 \/ 4 道通关/.test(failPage.plate.count ?? ''), + `${failPage.plate.name} · ${failPage.plate.count}`); + check('只有一张卡、没有评级徽章、判定写「超时」', + failPage.rows.length === 1 && failPage.rows[0].badge === '' && failPage.rows[0].judge === '超时', + `${failPage.rows.length} 张 / 徽章「${failPage.rows[0]?.badge}」/ 判定「${failPage.rows[0]?.judge}」`); + check('没有成绩的两处都是占位「—」', failPage.total.num === '—' && failPage.dx.num === '—', + `${failPage.total.num} / ${failPage.dx.num}`); + check('占位「—」也染了深蓝(白字族是纯白剪影,白底上会看不见)', + /rgb\(13, 42, 99\)/.test(failPage.total.font?.fill ?? '') && /rgb\(13, 42, 99\)/.test(failPage.dx.font?.fill ?? ''), + `${failPage.total.font?.fill} / ${failPage.dx.font?.fill}`); + check('心换成红色底盘,写 0', failPage.life.base === 'life-base-red.png' && failPage.life.label === '0', + `${failPage.life.base} / ${failPage.life.label}`); + await shot('G-不合格结算页.png'); + + console.log('\n=== 5) 控制台与请求 ==='); + check('页面没有抛异常 / console.error', problems.length === 0, problems.slice(0, 3).join(' | ')); + check('没有指向本服务的 4xx/5xx', badResponses.length === 0, badResponses.slice(0, 3).join(' | ')); +} catch (error) { + console.log(`\n出错:${error instanceof Error ? error.message : String(error)}`); + failures += 1; +} finally { + try { cdp?.socket.close(); } catch { /* 已经关了 */ } + edge.kill(); +} + +console.log(`\n结果:${failures === 0 ? '全部通过' : `${failures} 项失败`}`); +process.exit(failures === 0 ? 0 : 1); \ No newline at end of file diff --git a/scripts/export-original-card.py b/scripts/export-original-card.py new file mode 100644 index 0000000..cbafa9b --- /dev/null +++ b/scripts/export-original-card.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""把**原版**的乐曲卡片素材原样导出来,供对照。 + +用途:现在卡片的边框是照着官方贴图的版式用 CSS 画的(`.dan-track-frame`),用户要直接看 +原版素材本身。这里把逐题行那一圈的原图导到 `docs/images/original-card/`,并顺手把当前渲染 +出来的第 1 张卡裁成一张独立小图 `docs/images/dan-card.png`。 + +用法: + python scripts/export-original-card.py "<游戏目录>" # 含 resources.assets 的那一层 +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import UnityPy + +# 逐题行那一圈的原版贴图。`UI_CMN_RSL_KopMBase_*` 就是官方乐曲卡片的边框(640×140, +# 五种难度各一张,颜色与难度名 BASiC/ADVANCED/EXPERT/MASTER/Re:MASTER 都烘在图里)。 +WANTED = [ + "UI_CMN_RSL_KopMBase_BSC", + "UI_CMN_RSL_KopMBase_ADV", + "UI_CMN_RSL_KopMBase_EXP", + "UI_CMN_RSL_KopMBase_MST", + "UI_CMN_RSL_KopMBase_MST_Re", + "UI_CMN_RSL_JacketImage_S", + "UI_CMN_RSL_MusicJacket_Base", + "UI_CMN_RSL_JaketTrack", + "UI_CMN_RSL_JaketTrackNo_0", + "UI_CMN_RSL_AllScorekBase", + "UI_DNM_Result_musicBase_01", + "UI_RSL_AchivementCounter", + "UI_DNM_AchivementCounter", +] +PREFIXES = ("UI_CMN_RSL_", "UI_DNM_Result_music", "UI_RSL_Achivement", "UI_DNM_Achivement") + + +def main() -> int: + if len(sys.argv) < 2: + print(__doc__) + return 2 + game = Path(sys.argv[1]) + out = Path(__file__).resolve().parent.parent / "docs" / "images" / "original-card" + out.mkdir(parents=True, exist_ok=True) + env = UnityPy.load(str(game / "resources.assets")) + saved, extra = [], [] + for obj in env.objects: + if obj.type.name not in ("Texture2D", "Sprite"): + continue + try: + data = obj.read() + name = getattr(data, "m_Name", "") or "" + if not name: + continue + if name in WANTED: + image = data.image + image.save(out / f"{name}.png") + saved.append((name, image.size)) + elif any(name.startswith(p) for p in PREFIXES): + extra.append(name) + except Exception as exc: # 有的贴图数据在别的 bundle 里,读不到就跳过 + if getattr(obj, "read", None): + try: + name = obj.read().m_Name + except Exception: + name = "?" + if name in WANTED: + print(f" 跳过 {name}:{type(exc).__name__}") + for name, size in sorted(saved): + print(f" 导出 {name} {size[0]}x{size[1]}") + if extra: + print("同前缀但未导出的(想看哪个再说):") + for name in sorted(extra): + print(f" {name}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/scripts/extract-maimai-assets.py b/scripts/extract-maimai-assets.py new file mode 100644 index 0000000..061ace6 --- /dev/null +++ b/scripts/extract-maimai-assets.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""从 maimai DX 客户端资源里提取段位認定(Dani Mode)结算页用到的贴图。 + +这是**开发工具**,不是应用依赖:仓库里已经放好了提取结果 +(public/assets/maimai/*.png),只有要重新提一遍时才跑它。 + +用法: + pip install UnityPy texture2ddecoder Pillow + python scripts/extract-maimai-assets.py "<游戏目录>" [输出目录] + +`<游戏目录>` 是客户端里含 `resources.assets` 的那一层,例如 + E:/Downloads/156Sinmai_Data/156Sinmai_Data/156Sinmai_Data + +为什么要这么提:段位認定结算那整屏是**成品图**(页头文字、格纹页眉、两侧水引、 +下半部色带与左下角段位名牌白框全部烘焙在底图里),运行时只是把逐题行、达成率数字、 +段位名与总达成率盖上去。所以页面直接用这张底图 + 几件小素材,比自己用 CSS 画近似得多。 +注意:底图下半部那两三块**挖白的空面板**是原作留给运行时文字的位置,不要再拿别的脚本 +把它们续画填掉(第 12 轮干过一次,第 13 轮撤了)。 +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import UnityPy + +# 仓库里用的文件名 ← 游戏里的贴图名。只搬这几件,不是整个 UI_DNM 家族(159 张)。 +# +# 故意**不**搬的一件:逐题行的乐曲边框 `UI_CMN_RSL_KopMBase_{BSC,ADV,EXP,MST,MST_Re}` +# (640×140,五种难度各一张)。它是逐题行那一圈底色,但颜色与难度名(BASIC/ADVANCED/ +# EXPERT/MASTER/Re:MASTER)都烘死在图里,套不上我们自己的 CF 难度分档。所以边框按它的 +# 版式用 CSS 画一层(见 dan.css 的 .dan-track-frame):外圈浅色环 + 主色场 + 底部浅色带 +# + 左侧白曲绘槽 + 深蓝标题条 + 白色达成率框,尺寸全按这张贴图量出来的像素。 +WANTED: dict[str, str] = { + "UI_DNM_Result_Base_01": "result-dani.png", # 「段位認定」整屏底图 980×928 + "UI_DNM_Result_Base_03": "result-random.png", # 「ランダム段位認定」整屏底图 + "UI_DNM_Result_musicBase_01": "track-plate.png", # 逐题行底板 576×124 + "UI_DNM_Icon_Result_01": "stamp-clear.png", # 逐题「可」印 108×108 + "UI_DNM_Icon_Result_02": "stamp-fail.png", # 逐题「不可」印 + # 数字图集:规整 4×4 网格,逐格由同名 Sprite 定 rect(见 dan.js 的 NUM_FONTS)。 + # 图集里全是**纯白剪影**(游戏运行时才染色),所以描边要另外提一层 _Outline。 + "UI_CMN_Num_26p": "num-26p.png", # 小号数字 136×160,格 34×40 + "UI_CMN_Num_26p_Outline": "num-26p-outline.png", # 同布局的描边剪影 + "UI_CMN_Num_90p": "num-90p.png", # 没有成绩时的占位「—」用,356×420,格 89×105 + "UI_CMN_Num_90p_Outline": "num-90p-outline.png", + # 分数数字:同布局的三套(blue / gold / red)296×392,格 74×98,自带颜色、 + # 不用描边层 —— 原作就是按达成率高低换这一套的颜色。 + # 同名的 `UI_NUM_Score_0001111_Base` 故意不搬:那是一张**空图集**(16 格里一个字形都没有, + # 整张只有几列杂散像素),照它渲染数字会得到一片看不见的灰影。 + "UI_NUM_Score_0001111_Blue": "num-score-blue.png", + "UI_NUM_Score_0001111_Gold": "num-score-gold.png", + "UI_NUM_Score_0001111_Red": "num-score-red.png", + # 达成率末尾那个大「%」字形:同样按分数分色(没有 base 那版,低档用图集里的 % 格) + "UI_RSL_Score_Per_Gold": "score-per-gold.png", # 80×80 + "UI_RSL_Score_Per_Blue": "score-per-blue.png", + "UI_RSL_Score_Per_Red": "score-per-red.png", + # 逐题评级徽章:`UI_GAM_Rank_*` 是**游戏内**那套,单级各一张(源图紧裁,36–110 × 42–44)。 + # 故意不用 `UI_CMN_TabTitle_Rank_*`:那是**页签标题**上的区间图 —— AAA 那张画的是 + # 「A~AAA」、BBB 是「~BBB」,而且它没有 D/C/B/BB/A/AA,低分全挤在同一张牌上。 + "UI_GAM_Rank_D": "gam-rank-d.png", + "UI_GAM_Rank_C": "gam-rank-c.png", + "UI_GAM_Rank_B": "gam-rank-b.png", + "UI_GAM_Rank_BB": "gam-rank-bb.png", + "UI_GAM_Rank_BBB": "gam-rank-bbb.png", + "UI_GAM_Rank_A": "gam-rank-a.png", + "UI_GAM_Rank_AA": "gam-rank-aa.png", + "UI_GAM_Rank_AAA": "gam-rank-aaa.png", + "UI_GAM_Rank_S": "gam-rank-s.png", + "UI_GAM_Rank_Sp": "gam-rank-sp.png", + "UI_GAM_Rank_SS": "gam-rank-ss.png", + "UI_GAM_Rank_SSp": "gam-rank-ssp.png", + "UI_GAM_Rank_SSS": "gam-rank-sss.png", + "UI_GAM_Rank_SSSp": "gam-rank-sssp.png", + # 右下角那个组件:剩余生命底盘(绿 = 通关 / 红 = 未通关)。 + # `UI_RSL_DXScore_Base`(「でらっくスコア」标签)**故意不搬**:官方那格是单曲按打击 + # 精度算的 DX 分数,我们显示的是四道单题 rating 之和,口径不同,套那个框等于撒谎。 + "UI_DNM_Base_Life_01": "life-base-green.png", + "UI_DNM_Base_Life_03": "life-base-red.png", +} + + +def main(argv: list[str]) -> int: + if len(argv) < 2: + print(__doc__) + return 2 + game_dir = Path(argv[1]) + out_dir = Path(argv[2]) if len(argv) > 2 else Path("public/assets/maimai") + assets = game_dir / "resources.assets" + if not assets.exists(): + print(f"找不到 {assets}") + return 1 + + out_dir.mkdir(parents=True, exist_ok=True) + env = UnityPy.load(str(assets)) + written: dict[str, str] = {} + for obj in env.objects: + if obj.type.name != "Texture2D": + continue + data = obj.read() + name = getattr(data, "m_Name", None) or getattr(data, "name", "") or "" + target = WANTED.get(name) + if target is None or target in written: + continue + image = data.image + image.save(out_dir / target) + written[target] = f"{name} ({image.width}x{image.height})" + + for target in WANTED.values(): + print(f" {written.get(target, '缺失!'):<34} → {target}") + return 0 if len(written) == len(WANTED) else 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) \ No newline at end of file