feat(timeline): 模式配色区分 + 舞台放大/时间轴可拖高度 + 滚动条美化 + 镜头跟玩家

- 两模式各自背景色调:海选审核=暖棕 / 演出配置=冷青绿(顶栏/背景/列表/边框/滚动条全区分)
- 舞台自适应填充放大(画布提到960×470),时间轴默认200px固定高+中间分隔条可上下拖调高度
- 时间轴滚动条换成纤细主题化样式(替代默认丑滚动条),随模式变色
- 镜头修正:默认锁玩家(模拟真机45°俯视跟随相机),换说话人不再推近,仅显式镜头点才移焦
This commit is contained in:
2026-06-13 20:43:52 +08:00
parent f164ed0a08
commit ebff4e9b3b
3 changed files with 61 additions and 14 deletions

View File

@ -203,6 +203,7 @@
$("wrap").classList.toggle("hidden", m !== "review");
$("perform-wrap").classList.toggle("hidden", m !== "perform");
$("review-toolbar").style.display = m === "review" ? "" : "none";
document.body.classList.toggle("perform-mode", m === "perform"); // 切背景色调
Timeline.stop();
if (m === "perform") performLoadList();
}

View File

@ -230,8 +230,22 @@ button.mini { padding:2px 8px; font-size:12px; }
.mode-btn.active { background:#5a4a26; color:#f3dca0; }
header .who { margin-left:auto; font-size:12px; color:#9a8f7e; }
/* ---- 模式背景区分:海选审核=暖棕(默认) / 演出配置=冷青绿 ---- */
body.perform-mode { background:#0c1513; }
body.perform-mode header { background:#10211c; border-bottom-color:#244a3e; }
body.perform-mode .mode-btn.active { background:#1f5040; border-color:#2f7a60; color:#aee6d0; }
body.perform-mode .mode-switch { border-color:#2f7a60; }
/* ---- 演出配置页 ---- */
#perform-wrap { display:flex; flex:1; min-height:0; }
#perform-wrap { display:flex; flex:1; min-height:0; background:#0c1513; }
#perform-wrap #perform-list-pane { background:#0e1c18; border-right-color:#1c3a30; }
#perform-wrap .perform-listhead { background:#10211c; color:#8fc9b3; border-bottom-color:#1c3a30; }
#perform-wrap .ev { border-bottom-color:#13241f; }
#perform-wrap .ev:hover { background:#13241f; }
#perform-wrap .ev.sel { background:#163027; border-left-color:#3fb98a; }
#perform-wrap .tl-stagewrap { border-color:#1c3a30; }
#perform-wrap .tl-timelinepanel .tl-tracks { border-color:#1c3a30; }
#perform-wrap .tl-mapinfo { color:#7fae9c; }
#wrap.hidden, #perform-wrap.hidden { display:none; }
#perform-list-pane { width:250px; background:#19150f; border-right:1px solid #3a322a;
display:flex; flex-direction:column; flex:none; }
@ -241,13 +255,19 @@ header .who { margin-left:auto; font-size:12px; color:#9a8f7e; }
#perform-main { flex:1; min-width:0; display:flex; flex-direction:column; padding:14px 16px; min-height:0; }
#perform-main .empty-center { position:static; inset:auto; min-height:240px; }
/* ---- 演出预览:上=舞台面板 / 下=时间轴面板(分开两块)---- */
.tl-stagepanel { flex:none; }
.tl-mapinfo { font-size:12px; color:#9a8f7e; margin-bottom:8px; }
.tl-stagewrap { position:relative; background:#15130d; border:1px solid #3a322a; border-radius:6px;
overflow:hidden; line-height:0; max-width:640px; }
.tl-stage { width:100%; height:auto; display:block; }
.tl-controls { display:flex; align-items:center; gap:10px; margin:10px 0; flex-wrap:wrap; }
/* ---- 演出预览:上=舞台面板(自适应放大) / 下=时间轴面板(可拖高度) ---- */
.tl-stagepanel { flex:1 1 auto; min-height:150px; display:flex; flex-direction:column; }
.tl-mapinfo { flex:none; font-size:12px; color:#9a8f7e; margin-bottom:6px; }
.tl-stagewrap { position:relative; flex:1; min-height:0; background:#15130d; border:1px solid #3a322a;
border-radius:6px; overflow:hidden; }
.tl-stage { width:100%; height:100%; object-fit:contain; display:block; }
/* 分隔条:拖动调时间轴高度 */
.tl-resizer { flex:none; height:10px; margin:5px 0; cursor:row-resize; border-radius:5px;
background:#16120d; position:relative; }
.tl-resizer::after { content:""; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
width:48px; height:3px; border-radius:2px; background:#5a4a32; }
.tl-resizer:hover::after { background:#e6c878; }
.tl-controls { flex:none; display:flex; align-items:center; gap:10px; margin:8px 0 0; flex-wrap:wrap; }
.tl-controls .tip { font-size:11.5px; color:#7a7264; }
.tl-time { font-size:13px; color:#e6c878; font-variant-numeric:tabular-nums; min-width:90px; }
.tl-startbtn { max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@ -263,10 +283,21 @@ header .who { margin-left:auto; font-size:12px; color:#9a8f7e; }
border:1px solid #8a7038; border-radius:6px; padding:8px 14px; font-size:13px;
cursor:pointer; box-shadow:0 2px 8px rgba(0,0,0,.55); }
.tl-choice-btn:hover { background:#5a4a26; border-color:#e6c878; }
/* 时间轴面板:独立、占满剩余高度、自己横向滚动 */
.tl-timelinepanel { flex:1; min-height:120px; margin-top:8px; display:flex; }
/* 时间轴面板:固定/可拖高度、自己横向滚动 */
.tl-timelinepanel { flex:none; height:200px; min-height:80px; display:flex; }
.tl-tracks { position:relative; flex:1; min-width:0; overflow-x:auto; overflow-y:auto; cursor:grab;
background:#19150f; border:1px solid #3a322a; border-radius:6px; padding-top:20px; }
background:#19150f; border:1px solid #3a322a; border-radius:6px; padding-top:20px;
scrollbar-width:thin; scrollbar-color:#5a4a32 transparent; }
/* 纤细主题化滚动条(替代默认丑滚动条) */
.tl-tracks::-webkit-scrollbar { height:9px; width:9px; }
.tl-tracks::-webkit-scrollbar-track { background:transparent; }
.tl-tracks::-webkit-scrollbar-thumb { background:#4a4030; border-radius:6px; border:2px solid #19150f; }
.tl-tracks::-webkit-scrollbar-thumb:hover { background:#6a5a3c; }
.tl-tracks::-webkit-scrollbar-corner { background:transparent; }
#perform-wrap .tl-tracks { scrollbar-color:#2f5a48 transparent; }
#perform-wrap .tl-tracks::-webkit-scrollbar-thumb { background:#2a4a3e; border-color:#0e1c18; }
#perform-wrap .tl-resizer::after { background:#2f5a48; }
#perform-wrap .tl-resizer:hover::after { background:#3fb98a; }
.tl-ruler { position:relative; height:16px; border-bottom:1px solid #2a2419; }
.tl-tick { position:absolute; top:0; height:16px; border-left:1px solid #2a2419; }
.tl-tick span { font-size:9px; color:#6a6256; padding-left:3px; }

View File

@ -210,11 +210,10 @@
function activeDialogue(M, tau) { return M.clips.find(c => c.kind === "dialogue" && tau >= c.start && tau < c.start + c.dur) || null; }
function activeAnim(M, actor, tau) { return M.clips.find(c => c.kind === "anim" && c.actor === actor && tau >= c.start && tau < c.start + c.dur) || null; }
function focusWorldAt(M, tau) {
// 真机=45°俯视跟随玩家的相机默认锁玩家不会因换说话人而推近仅显式镜头点(camera clip)才移焦。
let fp = null;
M.clips.filter(c => c.kind === "camera").forEach(c => { if (tau >= c.start) fp = c.focus; });
if (fp) { const p = anchorXZ(M.anchors, fp); if (p) return p; }
const dlg = activeDialogue(M, tau);
if (dlg && dlg.actor) return actorPosAt(M, dlg.actor, tau);
if (actorsIn(M).includes("P1")) return actorPosAt(M, "P1", tau);
const b = M.bounds; return { x: (b.minX + b.maxX) / 2, z: (b.minZ + b.maxZ) / 2 };
}
@ -225,7 +224,7 @@
const TEMPLATE =
'<div class="tl-stagepanel">' +
' <div class="tl-mapinfo"></div>' +
' <div class="tl-stagewrap"><canvas class="tl-stage" width="780" height="380"></canvas><div class="tl-choices hidden"></div></div>' +
' <div class="tl-stagewrap"><canvas class="tl-stage" width="960" height="470"></canvas><div class="tl-choices hidden"></div></div>' +
' <div class="tl-controls">' +
' <button class="tl-play primary">▶ 播放</button>' +
' <button class="tl-restart mini">⏮ 重头</button>' +
@ -235,6 +234,7 @@
' <span class="tip">单击节点选中→「从选中处开始」,或双击节点直接开始(位置按途中走位重放) · 遇选择/战斗/随机弹选项</span>' +
' </div>' +
'</div>' +
'<div class="tl-resizer" title="拖动调整时间轴高度"></div>' +
'<div class="tl-timelinepanel"><div class="tl-tracks"></div></div>';
function show(host, IR, DICT, POINTSETS) {
@ -255,6 +255,8 @@
startbtn: host.querySelector(".tl-startbtn"),
fitbtn: host.querySelector(".tl-fitbtn"),
time: host.querySelector(".tl-time"),
resizer: host.querySelector(".tl-resizer"),
timelinepanel: host.querySelector(".tl-timelinepanel"),
playhead: null,
};
els.mapinfo.textContent = "点位集:" + psName + (ps.mapId ? "(地图 " + ps.mapId + "" : "") +
@ -265,6 +267,19 @@
els.restart.onclick = () => restart();
els.startbtn.onclick = () => { if (selNode) startFrom(selNode, true); };
els.fitbtn.onclick = () => { fitMode = !fitMode; els.fitbtn.classList.toggle("on", fitMode); refreshTimeline(); renderFrame(); };
// 拖拽分隔条调时间轴高度(向上拖=时间轴变高/舞台变小,反之)
els.resizer.onmousedown = e => {
e.preventDefault();
const startY = e.clientY, startH = els.timelinepanel.offsetHeight;
document.body.style.cursor = "row-resize";
const mv = ev => {
const h = Math.max(80, Math.min((els.host.clientHeight - 150), startH - (ev.clientY - startY)));
els.timelinepanel.style.height = h + "px";
if (fitMode) { refreshTimeline(); renderFrame(); }
};
const up = () => { document.removeEventListener("mousemove", mv); document.removeEventListener("mouseup", up); document.body.style.cursor = ""; };
document.addEventListener("mousemove", mv); document.addEventListener("mouseup", up);
};
startFrom(firstNode(IR));
}