合并请求 #1

feat:add game!
This commit is contained in:
Lenn
2026-04-06 17:45:38 +00:00
4 changed files with 1294 additions and 116 deletions

View File

@@ -5,6 +5,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import ConfigPanel from "$lib/components/ConfigPanel.svelte"; import ConfigPanel from "$lib/components/ConfigPanel.svelte";
import NeonBreakoutArena from "$lib/components/NeonBreakoutArena.svelte";
import PressureMatrixViewer from "$lib/components/PressureMatrixViewer.svelte"; import PressureMatrixViewer from "$lib/components/PressureMatrixViewer.svelte";
import SignalChart from "$lib/components/SignalChart.svelte"; import SignalChart from "$lib/components/SignalChart.svelte";
import SummaryCurve from "$lib/components/SummaryCurve.svelte"; import SummaryCurve from "$lib/components/SummaryCurve.svelte";
@@ -12,12 +13,14 @@
HudColorMapOption, HudColorMapOption,
HudSignalPanel, HudSignalPanel,
HudSummary, HudSummary,
LocaleCode,
PressureColorMapPreset, PressureColorMapPreset,
StageStatusTone StageStatusTone
} from "$lib/types/hud"; } from "$lib/types/hud";
export let title = ""; export let title = "";
export let hint = ""; export let hint = "";
export let locale: LocaleCode = "zh-CN";
export let statusText = ""; export let statusText = "";
export let statusTone: StageStatusTone = "idle"; export let statusTone: StageStatusTone = "idle";
export let leftPanels: HudSignalPanel[] = []; export let leftPanels: HudSignalPanel[] = [];
@@ -54,6 +57,7 @@
export let replayProgress = 0; export let replayProgress = 0;
export let replayFileName = ""; export let replayFileName = "";
export let replayFrameInfo = ""; export let replayFrameInfo = "";
export let showPrecisionTestPanel = false;
let stagePlaneEl: HTMLDivElement | undefined; let stagePlaneEl: HTMLDivElement | undefined;
let topOverlayEl: HTMLDivElement | undefined; let topOverlayEl: HTMLDivElement | undefined;
@@ -81,6 +85,8 @@
$: replaySide = summarySide === "left" ? "right" : "left"; $: replaySide = summarySide === "left" ? "right" : "left";
$: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel; $: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel;
$: replayProgressPercent = Math.round(Math.min(1, Math.max(0, replayProgress)) * 100); $: replayProgressPercent = Math.round(Math.min(1, Math.max(0, replayProgress)) * 100);
$: splitMatrixTitle = locale === "zh-CN" ? "数字矩阵" : "Matrix";
$: splitMatrixHint = locale === "zh-CN" ? "实时压力数据 / 数字矩阵" : "Live pressure matrix";
function toPxNumber(rawValue: string): number { function toPxNumber(rawValue: string): number {
const value = Number.parseFloat(rawValue); const value = Number.parseFloat(rawValue);
@@ -181,6 +187,7 @@
bind:this={stagePlaneEl} bind:this={stagePlaneEl}
style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};" style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};"
> >
{#if !showPrecisionTestPanel}
<div class="stage-top-overlay" bind:this={topOverlayEl}> <div class="stage-top-overlay" bind:this={topOverlayEl}>
<div class="stage-meta"> <div class="stage-meta">
<p class="meta-label">WebGL2 Stage</p> <p class="meta-label">WebGL2 Stage</p>
@@ -191,7 +198,43 @@
{statusText} {statusText}
</p> </p>
</div> </div>
{/if}
{#if showPrecisionTestPanel}
<div class="split-game-wrap">
<section class="split-panel split-matrix-panel">
<header class="split-panel-head">
<p>{splitMatrixTitle}</p>
<span>{splitMatrixHint}</span>
</header>
<div class="split-panel-body">
{#key `${matrixRows}x${matrixCols}:${colorMapPreset}:split`}
<PressureMatrixViewer
{pressureMatrix}
{matrixRows}
{matrixCols}
{rangeMin}
{rangeMax}
{colorMapPreset}
showStatsPanel={true}
/>
{/key}
</div>
</section>
<section class="split-panel split-breakout-panel">
<NeonBreakoutArena
{locale}
{pressureMatrix}
{matrixRows}
{matrixCols}
{rangeMin}
{rangeMax}
{colorMapPreset}
/>
</section>
</div>
{:else}
<div class="canvas-wrap"> <div class="canvas-wrap">
{#key `${matrixRows}x${matrixCols}:${colorMapPreset}`} {#key `${matrixRows}x${matrixCols}:${colorMapPreset}`}
<PressureMatrixViewer <PressureMatrixViewer
@@ -201,11 +244,13 @@
{rangeMin} {rangeMin}
{rangeMax} {rangeMax}
{colorMapPreset} {colorMapPreset}
showStatsPanel={true}
/> />
{/key} {/key}
</div> </div>
{/if}
{#if showConfigPanel} {#if showConfigPanel && !showPrecisionTestPanel}
<div class="config-panel-wrap"> <div class="config-panel-wrap">
<ConfigPanel <ConfigPanel
bind:matrixRows bind:matrixRows
@@ -230,6 +275,7 @@
</div> </div>
{/if} {/if}
{#if !showPrecisionTestPanel}
<div class="panel-zone" bind:this={panelZoneEl}> <div class="panel-zone" bind:this={panelZoneEl}>
<aside class="side-rail left-rail"> <aside class="side-rail left-rail">
<div class="rail-stack" bind:this={leftStackEl}> <div class="rail-stack" bind:this={leftStackEl}>
@@ -293,8 +339,9 @@
</div> </div>
</aside> </aside>
</div> </div>
{/if}
{#if replayHasData} {#if replayHasData && !showPrecisionTestPanel}
<aside class="replay-floating-panel" class:is-left={replaySide === "left"} class:is-right={replaySide === "right"}> <aside class="replay-floating-panel" class:is-left={replaySide === "left"} class:is-right={replaySide === "right"}>
<div class="replay-panel-head"> <div class="replay-panel-head">
<div class="replay-panel-title-group"> <div class="replay-panel-title-group">
@@ -332,9 +379,11 @@
</aside> </aside>
{/if} {/if}
{#if !showPrecisionTestPanel}
<div class="stage-bottom-overlay"> <div class="stage-bottom-overlay">
<slot /> <slot />
</div> </div>
{/if}
</div> </div>
</article> </article>
</section> </section>
@@ -463,6 +512,70 @@
max-inline-size: min(24rem, 40vw); max-inline-size: min(24rem, 40vw);
} }
.split-game-wrap {
position: absolute;
inset: clamp(0.46rem, 1vw, 0.82rem);
z-index: 6;
display: grid;
grid-template-columns: minmax(0, 0.98fr) minmax(0, 1.02fr);
gap: clamp(0.45rem, 1vw, 0.9rem);
}
.split-panel {
position: relative;
min-block-size: 0;
overflow: hidden;
border: 1px solid rgb(var(--hud-border-rgb) / 0.26);
border-radius: 0.58rem;
background:
linear-gradient(180deg, rgb(var(--hud-surface-rgb) / 0.84), rgb(var(--hud-surface-deep-rgb) / 0.9)),
radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.06), transparent 56%);
box-shadow:
inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.07),
0 0 20px rgb(var(--hud-glow-rgb) / 0.08);
}
.split-panel-head {
position: absolute;
top: 0.42rem;
left: 0.52rem;
z-index: 5;
display: grid;
gap: 0.1rem;
margin: 0;
pointer-events: none;
}
.split-panel-head p {
margin: 0;
color: rgb(var(--hud-text-main-rgb) / 0.96);
font-size: 0.62rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.split-panel-head span {
color: rgb(var(--hud-text-dim-rgb) / 0.82);
font-size: 0.52rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.split-panel-body {
position: absolute;
inset: 0;
}
.split-matrix-panel :global(.viewer-controls) {
left: clamp(0.7rem, 1.7vw, 1.15rem);
top: clamp(3.8rem, 8.8vh, 4.9rem);
max-inline-size: min(13.2rem, 65%);
}
.split-matrix-panel :global(.stats-panel) {
padding: 0.62rem 0.68rem 0.72rem;
}
.panel-zone { .panel-zone {
position: absolute; position: absolute;
top: var(--panel-zone-top-dyn, var(--panel-zone-top)); top: var(--panel-zone-top-dyn, var(--panel-zone-top));
@@ -744,6 +857,10 @@
.replay-floating-panel { .replay-floating-panel {
inline-size: min(var(--rail-width), 20.8rem); inline-size: min(var(--rail-width), 20.8rem);
} }
.split-game-wrap {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
} }
@media (max-height: 900px) { @media (max-height: 900px) {
@@ -787,5 +904,10 @@
right: calc(var(--rail-edge-inset) + 0.1rem); right: calc(var(--rail-edge-inset) + 0.1rem);
inline-size: auto; inline-size: auto;
} }
.split-game-wrap {
grid-template-columns: 1fr;
grid-template-rows: minmax(0, 0.92fr) minmax(0, 1.08fr);
}
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@
export let rangeMin = 0; export let rangeMin = 0;
export let rangeMax = 16000; export let rangeMax = 16000;
export let colorMapPreset: PressureColorMapPreset = "emerald"; export let colorMapPreset: PressureColorMapPreset = "emerald";
export let showStatsPanel = true;
let viewerEl: HTMLDivElement | undefined; let viewerEl: HTMLDivElement | undefined;
let canvasEl: HTMLCanvasElement | undefined; let canvasEl: HTMLCanvasElement | undefined;
@@ -608,6 +609,7 @@
<div class="viewer-vignette" aria-hidden="true"></div> <div class="viewer-vignette" aria-hidden="true"></div>
<div class="viewer-noise" aria-hidden="true"></div> <div class="viewer-noise" aria-hidden="true"></div>
{#if showStatsPanel}
<div class="viewer-controls"> <div class="viewer-controls">
<section class="stats-panel" aria-label="Pressure Summary"> <section class="stats-panel" aria-label="Pressure Summary">
<p class="stats-label">Pressure Matrix</p> <p class="stats-label">Pressure Matrix</p>
@@ -628,6 +630,7 @@
<p class="stats-note">{statsNote}</p> <p class="stats-note">{statsNote}</p>
</section> </section>
</div> </div>
{/if}
</div> </div>
<style> <style>

View File

@@ -204,6 +204,7 @@
let isWindowMaximized = false; let isWindowMaximized = false;
let activeConfigLinkId = "stream-on"; let activeConfigLinkId = "stream-on";
let isConfigPanelOpen = false; let isConfigPanelOpen = false;
let isPrecisionTestOpen = false;
let hasSignalData = false; let hasSignalData = false;
let signalPanels: HudSignalPanel[] = buildInactivePanels(); let signalPanels: HudSignalPanel[] = buildInactivePanels();
let summary: HudSummary = buildEmptySummary(); let summary: HudSummary = buildEmptySummary();
@@ -232,7 +233,7 @@
let fileExplorerFileName = ""; let fileExplorerFileName = "";
$: uiCopy = copyByLocale[locale]; $: uiCopy = copyByLocale[locale];
$: configLinks = buildConfigLinks(locale, activeConfigLinkId, isConfigPanelOpen); $: configLinks = buildConfigLinks(locale, activeConfigLinkId, isConfigPanelOpen, isPrecisionTestOpen);
$: stageStatusText = webglStatusTone === "ok" ? uiCopy.runtimeReady : uiCopy.runtimeFallback; $: stageStatusText = webglStatusTone === "ok" ? uiCopy.runtimeReady : uiCopy.runtimeFallback;
$: leftSignalPanels = signalPanels.filter((panel) => panel.side === "left"); $: leftSignalPanels = signalPanels.filter((panel) => panel.side === "left");
$: rightSignalPanels = signalPanels.filter((panel) => panel.side === "right"); $: rightSignalPanels = signalPanels.filter((panel) => panel.side === "right");
@@ -970,19 +971,26 @@
}); });
} }
function buildConfigLinks(currentLocale: LocaleCode, activeId: string, isSettingsOpen: boolean): HudConfigLink[] { function buildConfigLinks(
currentLocale: LocaleCode,
activeId: string,
isSettingsOpen: boolean,
isPrecisionOpen: boolean
): HudConfigLink[] {
const labels = const labels =
currentLocale === "zh-CN" currentLocale === "zh-CN"
? { ? {
streamOn: "打开", streamOn: "打开",
streamOff: "关闭", streamOff: "关闭",
calibrate: "校准", calibrate: "校准",
precisionTest: "游戏",
settings: "参数" settings: "参数"
} }
: { : {
streamOn: "Open", streamOn: "Open",
streamOff: "Close", streamOff: "Close",
calibrate: "Calib", calibrate: "Calib",
precisionTest: "Game",
settings: "Setup" settings: "Setup"
}; };
@@ -1005,6 +1013,12 @@
tone: "cyan", tone: "cyan",
active: activeId === "calibrate" active: activeId === "calibrate"
}, },
{
id: "precision-test",
label: labels.precisionTest,
tone: "lime",
active: isPrecisionOpen
},
{ {
id: "settings", id: "settings",
label: labels.settings, label: labels.settings,
@@ -1443,11 +1457,19 @@
} }
function handleConfigLink(event: CustomEvent<string>): void { function handleConfigLink(event: CustomEvent<string>): void {
if (event.detail === "precision-test") {
isPrecisionTestOpen = !isPrecisionTestOpen;
isConfigPanelOpen = false;
return;
}
if (event.detail === "settings") { if (event.detail === "settings") {
isPrecisionTestOpen = false;
isConfigPanelOpen = !isConfigPanelOpen; isConfigPanelOpen = !isConfigPanelOpen;
return; return;
} }
isPrecisionTestOpen = false;
isConfigPanelOpen = false; isConfigPanelOpen = false;
activeConfigLinkId = event.detail; activeConfigLinkId = event.detail;
console.info("[hud] config link clicked:", event.detail); console.info("[hud] config link clicked:", event.detail);
@@ -1562,6 +1584,7 @@
/> />
<CenterStage <CenterStage
{locale}
bind:matrixRows bind:matrixRows
bind:matrixCols bind:matrixCols
bind:rangeMin bind:rangeMin
@@ -1599,6 +1622,7 @@
rightPanels={rightSignalPanels} rightPanels={rightSignalPanels}
{pressureMatrix} {pressureMatrix}
showConfigPanel={isConfigPanelOpen} showConfigPanel={isConfigPanelOpen}
showPrecisionTestPanel={isPrecisionTestOpen}
{summary} {summary}
on:replaytoggle={handleReplayToggle} on:replaytoggle={handleReplayToggle}
on:replaystop={handleReplayStop} on:replaystop={handleReplayStop}
@@ -1607,6 +1631,7 @@
on:replayclose={handleReplayClose} on:replayclose={handleReplayClose}
on:configclose={() => (isConfigPanelOpen = false)} on:configclose={() => (isConfigPanelOpen = false)}
> >
{#if !isPrecisionTestOpen}
<section class="range-scale" aria-label="Signal Range"> <section class="range-scale" aria-label="Signal Range">
<p class="range-label">Range</p> <p class="range-label">Range</p>
<div class="range-track"> <div class="range-track">
@@ -1615,6 +1640,7 @@
{/each} {/each}
</div> </div>
</section> </section>
{/if}
</CenterStage> </CenterStage>
</div> </div>