fix: 修复打砖块游戏碰撞穿透bug,添加渐进提速机制

This commit is contained in:
lenn
2026-04-29 15:43:56 +08:00
parent 26533f6916
commit 326f07ed4f
23 changed files with 786 additions and 376 deletions

View File

@@ -33,7 +33,6 @@
export let rangeLabel = "";
export let rangeMinLabel = "";
export let rangeMaxLabel = "";
export let colorMapLabel = "";
export let resetConfigLabel = "";
export let applyLiveHint = "";
export let matrixRows = 12;
@@ -42,7 +41,6 @@
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
export let colorMapPreset: PressureColorMapPreset = "emerald";
export let matrixDisplayMode: MatrixDisplayMode = "dots";
export let colorMapOptions: HudColorMapOption[] = [];
export let replaySectionLabel = "";
export let replayPlayLabel = "";
export let replayPauseLabel = "";
@@ -56,6 +54,7 @@
export let replayFileName = "";
export let replayFrameInfo = "";
export let showPrecisionTestPanel = false;
export let sessionStartedAt: number = Date.now();
let stagePlaneEl: HTMLDivElement | undefined;
let panelZoneEl: HTMLDivElement | undefined;
@@ -195,6 +194,7 @@
{rangeMax}
{colorMapPreset}
{matrixDisplayMode}
{locale}
showStatsPanel={true}
/>
{/key}
@@ -225,6 +225,7 @@
{rangeMax}
{colorMapPreset}
{matrixDisplayMode}
{locale}
showStatsPanel={true}
/>
{/key}
@@ -246,9 +247,6 @@
{rangeLabel}
{rangeMinLabel}
{rangeMaxLabel}
{colorMapLabel}
bind:colorMapPreset
{colorMapOptions}
resetLabel={resetConfigLabel}
{applyLiveHint}
on:close={() => dispatch("configclose")}
@@ -267,7 +265,7 @@
in:fly={{ x: -180, duration: 340, opacity: 0.08, easing: cubicOut }}
out:fly={{ x: -180, duration: 280, opacity: 0.06, easing: cubicIn }}
>
<SignalChart {panel} panelIndex={index} />
<SignalChart {panel} panelIndex={index} {locale} />
</div>
{/each}
@@ -281,6 +279,9 @@
{summary}
xValues={summary.xValues ?? null}
yValues={summary.points}
{locale}
{sessionStartedAt}
isRealtime={!replayHasData}
side="left"
panelIndex={leftPanels.length}
/>
@@ -298,7 +299,7 @@
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
>
<SignalChart {panel} panelIndex={index} />
<SignalChart {panel} panelIndex={index} {locale} />
</div>
{/each}
@@ -312,6 +313,9 @@
{summary}
xValues={summary.xValues ?? null}
yValues={summary.points}
{locale}
{sessionStartedAt}
isRealtime={!replayHasData}
side="right"
panelIndex={rightPanels.length}
/>
@@ -396,7 +400,7 @@
}
.stage-canvas-plane {
--rail-width: clamp(17.5rem, 23vw, 21.5rem);
--rail-width: clamp(20rem, 27vw, 26rem);
--rail-edge-inset: clamp(0.35rem, 1vw, 0.9rem);
--safe-gap: clamp(0.35rem, 0.9vw, 0.85rem);
--panel-zone-top: clamp(6.4rem, 11.8vh, 8rem);
@@ -754,7 +758,7 @@
@media (max-width: 1180px) {
.stage-canvas-plane {
--rail-width: clamp(14.2rem, 28vw, 16.4rem);
--rail-width: clamp(17rem, 32vw, 22rem);
--rail-edge-inset: clamp(0.25rem, 0.7vw, 0.55rem);
--safe-gap: clamp(0.2rem, 0.75vw, 0.45rem);
--panel-zone-top: clamp(6rem, 11.2vh, 7.2rem);

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range";
import type { HudColorMapOption, PressureColorMapPreset } from "$lib/types/hud";
export let title = "";
export let hint = "";
@@ -11,15 +10,12 @@
export let rangeLabel = "";
export let rangeMinLabel = "";
export let rangeMaxLabel = "";
export let colorMapLabel = "";
export let resetLabel = "";
export let applyLiveHint = "";
export let matrixRows = 12;
export let matrixCols = 7;
export let rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
export let colorMapPreset: PressureColorMapPreset = "emerald";
export let colorMapOptions: HudColorMapOption[] = [];
const dispatch = createEventDispatcher<{
close: void;
@@ -78,24 +74,17 @@
matrixCols = size;
}
function applyColorMapPreset(id: PressureColorMapPreset): void {
colorMapPreset = id;
}
function resetDefaults(): void {
matrixRows = 12;
matrixCols = 7;
rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
colorMapPreset = "emerald";
}
function handleSubmit(): void {
dispatch("close");
}
$: selectedColorMap = colorMapOptions.find((option) => option.id === colorMapPreset) ?? colorMapOptions[0];
$: {
const nextRows = normalizeGridValue(matrixRows);
if (nextRows !== matrixRows) {
@@ -190,31 +179,6 @@
</div>
</div>
<div class="config-section">
<div class="section-head">
<p class="section-title">{colorMapLabel}</p>
<p class="section-note">{selectedColorMap?.label ?? colorMapPreset}</p>
</div>
<div class="palette-row" role="group" aria-label={colorMapLabel}>
{#each colorMapOptions as option}
<button
type="button"
class="palette-btn"
class:is-active={colorMapPreset === option.id}
on:click={() => applyColorMapPreset(option.id)}
>
<span
class="palette-preview"
style={`--palette-stop-0: ${option.previewStops[0]}; --palette-stop-1: ${option.previewStops[1]}; --palette-stop-2: ${option.previewStops[2]};`}
aria-hidden="true"
></span>
<span class="palette-name">{option.label}</span>
</button>
{/each}
</div>
</div>
<footer class="config-foot">
<p class="live-note">{applyLiveHint}</p>
<button type="button" class="reset-btn" on:click={resetDefaults}>{resetLabel}</button>
@@ -327,15 +291,8 @@
gap: 0.42rem;
}
.palette-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.48rem;
}
.preset-btn,
.reset-btn,
.palette-btn {
.reset-btn {
border: 1px solid rgb(var(--hud-border-rgb) / 0.28);
border-radius: 999px;
padding: 0.38rem 0.72rem;
@@ -358,48 +315,6 @@
box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.14), 0 0 16px rgb(var(--hud-glow-alt-rgb) / 0.14);
}
.palette-btn {
display: grid;
gap: 0.34rem;
min-height: 4rem;
padding: 0.52rem 0.56rem 0.58rem;
border-radius: 0.74rem;
text-align: left;
}
.palette-btn.is-active {
border-color: rgb(var(--hud-lime-rgb) / 0.48);
background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.96), rgb(var(--hud-surface-rgb) / 0.92));
color: rgb(var(--hud-text-main-rgb) / 0.98);
box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.14), 0 0 16px rgb(var(--hud-glow-alt-rgb) / 0.14);
}
.palette-preview {
display: block;
inline-size: 100%;
block-size: 0.74rem;
border-radius: 999px;
border: 1px solid rgb(255 255 255 / 0.08);
background:
linear-gradient(
90deg,
var(--palette-stop-0) 0%,
var(--palette-stop-1) 52%,
var(--palette-stop-2) 100%
),
linear-gradient(180deg, rgb(255 255 255 / 0.08), transparent 55%);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.1),
0 0 12px rgb(0 0 0 / 0.14);
}
.palette-name {
color: inherit;
font-size: 0.74rem;
line-height: 1.1;
letter-spacing: 0.04em;
}
.field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -457,9 +372,5 @@
.field-grid {
grid-template-columns: 1fr;
}
.palette-row {
grid-template-columns: 1fr;
}
}
</style>
</style>

View File

@@ -2,8 +2,6 @@
import { createEventDispatcher } from "svelte";
export let running = false;
export let port = 50051;
export let framesSent = 0;
export let filterLiftEnabled = true;
export let saveAsXlsx = false;
export let locale: "zh-CN" | "en-US" = "zh-CN";
@@ -22,51 +20,52 @@
togglexlsx: void;
}>();
$: labels = locale === "zh-CN"
? {
title: "DevKit 配置",
status: "状态",
connected: "已连接",
disconnected: "未连接",
port: "端口",
framesSent: "已发送帧",
filterLift: "导出过滤抬起",
filterLiftHint: "导出 CSV 自动调用 Python 做梯度过滤,过滤掉抬起的小值数据",
saveXlsx: " xlsx 保存",
saveXlsxHint: "Python 处理后输出 xlsx 格式并删除源 CSV 文件",
lastResult: "最近一次处理",
output: "输出文件",
groups: "分组数",
mean: "均值",
threshold: "阈值",
rows: "行数",
kept: "保留行数",
}
: {
title: "DevKit Config",
status: "Status",
connected: "Connected",
disconnected: "Disconnected",
port: "Port",
framesSent: "Frames sent",
filterLift: "Filter lift on export",
filterLiftHint: "After CSV export, automatically call Python to filter out small values",
saveXlsx: "Save as xlsx",
saveXlsxHint: "Python outputs xlsx format and deletes the source CSV file",
lastResult: "Last process",
output: "Output",
groups: "Groups",
mean: "Mean",
threshold: "Threshold",
rows: "Rows",
kept: "Kept rows",
};
$: labels =
locale === "zh-CN"
? {
title: "开发工具配置",
close: "关闭",
status: "状态",
connected: "已连接",
disconnected: "未连接",
filterLift: "导出过滤抬起",
filterLiftHint: "导出 CSV 自动过滤掉抬起阶段的小值数据",
saveXlsx: "保存为 xlsx",
saveXlsxHint: "将导出文件转换为 xlsx 格式。",
lastResult: "最近一次处理",
output: "输出文件",
groups: "分组数",
mean: "均值",
threshold: "阈值",
rows: "行数",
kept: "保留行数",
rowsFlow: "行数变化"
}
: {
title: "DevKit Config",
close: "Close",
status: "Status",
connected: "Connected",
disconnected: "Disconnected",
filterLift: "Filter lift on export",
filterLiftHint: "Automatically filter out small values from lift-off phases during CSV export.",
saveXlsx: "Save as xlsx",
saveXlsxHint: "Convert exported file to xlsx format.",
lastResult: "Last process",
output: "Output",
groups: "Groups",
mean: "Mean",
threshold: "Threshold",
rows: "Rows",
kept: "Kept rows",
rowsFlow: "Rows flow"
};
</script>
<div class="dk-panel">
<header class="dk-head">
<h3 class="dk-title">{labels.title}</h3>
<button type="button" class="dk-close" on:click={() => dispatch("close")} aria-label="Close">
<button type="button" class="dk-close" on:click={() => dispatch("close")} aria-label={labels.close}>
<span></span><span></span>
</button>
</header>
@@ -77,18 +76,6 @@
<span class="dk-label">{labels.status}</span>
<span class="dk-value">{running ? labels.connected : labels.disconnected}</span>
</div>
{#if running}
<div class="dk-info-grid">
<div class="dk-info">
<span class="dk-info-label">{labels.port}</span>
<span class="dk-info-value">:{port}</span>
</div>
<div class="dk-info">
<span class="dk-info-label">{labels.framesSent}</span>
<span class="dk-info-value">{framesSent}</span>
</div>
</div>
{/if}
</section>
<section class="dk-section">
@@ -132,8 +119,8 @@
<span class="dk-result-value">{lastProcessResult.threshold.toFixed(3)}</span>
</div>
<div class="dk-result-item">
<span class="dk-result-label">{labels.rows}</span>
<span class="dk-result-value">{lastProcessResult.rowsTotal} {lastProcessResult.rowsKept}</span>
<span class="dk-result-label">{labels.rowsFlow}</span>
<span class="dk-result-value">{lastProcessResult.rowsTotal} -> {lastProcessResult.rowsKept}</span>
</div>
</div>
</section>
@@ -236,30 +223,6 @@
margin-inline-start: auto;
}
.dk-info-grid {
display: flex;
gap: 1rem;
}
.dk-info {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.dk-info-label {
color: rgb(var(--hud-text-dim-rgb) / 0.7);
font-size: 0.56rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.dk-info-value {
color: rgb(var(--hud-text-main-rgb) / 0.94);
font-size: 0.82rem;
font-weight: 500;
}
.dk-toggle {
display: flex;
align-items: flex-start;

View File

@@ -32,6 +32,7 @@
export let matrixDisplayMode: MatrixDisplayMode = "dots";
export let summary: HudSummary | null = null;
export let showStatsPanel = true;
export let locale: "zh-CN" | "en-US" = "zh-CN";
let viewerEl: HTMLDivElement | undefined;
let canvasEl: HTMLCanvasElement | undefined;
@@ -131,8 +132,13 @@
$: resolvedRangeMin = resolvedRange.min;
$: resolvedRangeMax = resolvedRange.max;
$: matrixLayout = buildMatrixLayout(resolvedMatrixRows, resolvedMatrixCols);
$: statsModeLabel = matrixDisplayMode === "dots" ? "dot pulse" : "numeric pulse";
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / force range ${resolvedRangeMin}-${resolvedRangeMax} / ${statsModeLabel}`;
$: statsModeLabel = matrixDisplayMode === "dots"
? (locale === "zh-CN" ? "点阵脉冲" : "dot pulse")
: (locale === "zh-CN" ? "数字脉冲" : "numeric pulse");
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / ${locale === "zh-CN" ? "力量范围" : "force range"} ${resolvedRangeMin}-${resolvedRangeMax} / ${statsModeLabel}`;
$: viewerI18n = locale === "zh-CN"
? { title: "合力", current: "当前合力", max: "最大合力", min: "最小合力" }
: { title: "Resultant Force", current: "Current RF", max: "Max RF", min: "Min RF" };
function formatForceStat(value: number | null): string {
if (value == null || !Number.isFinite(value)) {
@@ -660,18 +666,18 @@
{#if showStatsPanel}
<div class="viewer-controls">
<section class="stats-panel" aria-label="Pressure Summary">
<p class="stats-label">Resultant Force</p>
<p class="stats-label">{viewerI18n.title}</p>
<div class="stats-grid">
<article class="stats-card stats-card-wide">
<span class="stats-key">Current RF</span>
<span class="stats-key">{viewerI18n.current}</span>
<strong class="stats-value">{formatForceStat(stats.current)}</strong>
</article>
<article class="stats-card">
<span class="stats-key">Max RF</span>
<span class="stats-key">{viewerI18n.max}</span>
<strong class="stats-value">{formatForceStat(stats.max)}</strong>
</article>
<article class="stats-card">
<span class="stats-key">Min RF</span>
<span class="stats-key">{viewerI18n.min}</span>
<strong class="stats-value">{formatForceStat(stats.min)}</strong>
</article>
</div>

View File

@@ -3,6 +3,11 @@
export let panel: HudSignalPanel;
export let panelIndex = 0;
export let locale: "zh-CN" | "en-US" = "zh-CN";
$: signalI18n = locale === "zh-CN"
? { now: "当前", max: "最大", min: "最小", total: "合计" }
: { now: "Now", max: "Max", min: "Min", total: "TOTAL" };
const viewportWidth = 100;
const viewportHeight = 36;
@@ -110,7 +115,7 @@
<div class="icon-layer" aria-hidden="true">
{#each panel.icons as icon (icon.id)}
<span class="icon-chip tone-{icon.tone}">{icon.label}</span>
<span class="icon-chip tone-{icon.tone}">{icon.label === "TOTAL" ? signalI18n.total : icon.label}</span>
{/each}
</div>
</header>
@@ -136,17 +141,17 @@
<footer class="panel-foot">
<p class="foot-item">
<span class="dot tone-cyan"></span>
<span class="metric-label">Now</span>
<span class="metric-label">{signalI18n.now}</span>
<span class="value">{latestValue}</span>
</p>
<p class="foot-item">
<span class="dot tone-lime"></span>
<span class="metric-label">Max</span>
<span class="metric-label">{signalI18n.max}</span>
<span class="value">{maxValue}</span>
</p>
<p class="foot-item">
<span class="dot tone-orange"></span>
<span class="metric-label">Min</span>
<span class="metric-label">{signalI18n.min}</span>
<span class="value">{minValue}</span>
</p>
</footer>
@@ -158,7 +163,7 @@
--enter-ms: 1800ms;
--fade-ms: 1000ms;
overflow: hidden;
inline-size: min(100%, clamp(16.8rem, 23vw, 22rem));
inline-size: min(100%, clamp(19rem, 27vw, 26rem));
aspect-ratio: 1.44 / 1;
min-block-size: 11.8rem;
justify-self: start;
@@ -388,7 +393,7 @@
@media (max-width: 1180px) {
.signal-panel {
inline-size: min(100%, clamp(14rem, 30vw, 17rem));
inline-size: min(100%, clamp(16rem, 32vw, 21rem));
aspect-ratio: 1.5 / 1;
min-block-size: 10.1rem;
}

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { onMount } from "svelte";
import type { HudSummary } from "$lib/types/hud";
export let summary: HudSummary;
@@ -6,6 +7,25 @@
export let panelIndex = 0;
export let xValues: number[] | null = null;
export let yValues: number[] | null = null;
export let locale: "zh-CN" | "en-US" = "zh-CN";
export let sessionStartedAt: number = Date.now();
export let isRealtime = false;
let currentTimeSeconds = 0;
let timerId: ReturnType<typeof setInterval> | null = null;
onMount(() => {
timerId = setInterval(() => {
currentTimeSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
}, 200);
return () => {
if (timerId != null) clearInterval(timerId);
};
});
$: i18n = locale === "zh-CN"
? { now: "当前", min: "最小", max: "最大", waiting: "等待数据" }
: { now: "Now", min: "Min", max: "Max", waiting: "Waiting" };
const viewportWidth = 120;
const viewportHeight = 48;
@@ -50,7 +70,12 @@
}
if (axis === "x") {
return String(Math.round(value));
if (value < 60) {
return `${value.toFixed(1)}s`;
}
const mins = Math.floor(value / 60);
const secs = value - mins * 60;
return `${mins}:${secs.toFixed(0).padStart(2, "0")}`;
}
return `${Math.round(value)} N`;
@@ -104,14 +129,51 @@
return [];
}
let previousX = 0;
return rawYValues.map((rawY, index) => {
const x = rawXValues[index];
const y = Number.isFinite(rawY) ? Number(rawY) : 0;
const resolvedX = Number.isFinite(x) ? Number(x) : index + 1;
return { x: resolvedX, y };
const fallbackX = index === 0 ? 0 : previousX + 1;
const resolvedX = Number.isFinite(x) ? Number(x) : fallbackX;
const normalizedX = index === 0 ? resolvedX : Math.max(resolvedX, previousX);
previousX = normalizedX;
return { x: normalizedX, y };
});
}
function resolveXScaleBounds(
samples: CurveSample[],
currentSeconds: number,
realtime: boolean
): { min: number; max: number } {
if (samples.length === 0) {
return { min: 0, max: 1 };
}
const values = samples.map((sample) => sample.x);
const dataBounds = resolveBounds(values);
if (!realtime) {
return dataBounds;
}
const firstX = samples[0].x;
const lastX = samples[samples.length - 1].x;
const axisMax = Math.max(lastX, currentSeconds);
const positiveDiffs = samples
.slice(1)
.map((sample, index) => sample.x - samples[index].x)
.filter((diff) => diff > 0);
const averageSpacing =
positiveDiffs.length > 0 ? positiveDiffs.reduce((sum, diff) => sum + diff, 0) / positiveDiffs.length : 1;
const dataSpan = Math.max(lastX - firstX, 0);
const windowSpan = Math.max(dataSpan, averageSpacing * Math.max(samples.length - 1, 1), 1);
const axisMin = Math.max(0, axisMax - windowSpan);
return resolveBounds([axisMin, axisMax]);
}
function convertPoints(
samples: CurveSample[],
xBounds: { min: number; max: number },
@@ -146,14 +208,14 @@
}));
}
function buildXAxisTicks(samples: CurveSample[], xScaleBounds: { min: number; max: number }): AxisTick[] {
if (!samples.length) {
function buildXAxisTicks(xScaleBounds: { min: number; max: number }): AxisTick[] {
if (!Number.isFinite(xScaleBounds.min) || !Number.isFinite(xScaleBounds.max)) {
return [];
}
const first = samples[0].x;
const middle = samples[Math.floor((samples.length - 1) / 2)].x;
const last = samples[samples.length - 1].x;
const first = xScaleBounds.min;
const middle = xScaleBounds.min + (xScaleBounds.max - xScaleBounds.min) / 2;
const last = xScaleBounds.max;
const tickValues = [first, middle, last];
return tickValues.map((value) => ({
value,
@@ -185,9 +247,16 @@
$: sourceYValues = yValues && yValues.length ? yValues : summary.points;
$: sourceXValues = xValues && xValues.length ? xValues : summary.xValues ?? [];
$: samples = buildSamples(sourceYValues, sourceXValues);
$: samples = (() => {
const base = buildSamples(sourceYValues, sourceXValues);
if (isRealtime && base.length > 0 && currentTimeSeconds > 0) {
const lastSample = base[base.length - 1];
base[base.length - 1] = { ...lastSample, x: Math.max(lastSample.x, currentTimeSeconds) };
}
return base;
})();
$: sampleCount = samples.length;
$: xScaleBounds = resolveBounds(samples.map((sample) => sample.x));
$: xScaleBounds = resolveXScaleBounds(samples, currentTimeSeconds, isRealtime);
$: yScaleBounds = fixedYBounds;
$: xDataBounds = resolveDataBounds(samples.map((sample) => sample.x));
$: yDataBounds = resolveDataBounds(samples.map((sample) => sample.y));
@@ -196,7 +265,7 @@
$: areaPath = createAreaPath(plotPoints);
$: lastPoint = plotPoints.length > 0 ? plotPoints[plotPoints.length - 1] : null;
$: yAxisTicks = sampleCount > 0 ? buildYAxisTicks(yScaleBounds, yDataBounds) : [];
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(samples, xScaleBounds) : [];
$: xAxisTicks = sampleCount > 0 ? buildXAxisTicks(xScaleBounds) : [];
$: latestValue = formatValue(summary.latest);
$: minValue = formatValue(summary.min);
$: maxValue = formatValue(summary.max);
@@ -270,7 +339,7 @@
{#if sampleCount === 0}
<div class="empty-state">
<span>Waiting</span>
<span>{i18n.waiting}</span>
</div>
{/if}
</div>
@@ -278,17 +347,17 @@
<footer class="panel-foot">
<p class="foot-item">
<span class="dot tone-cyan"></span>
<span class="metric-text">Now</span>
<span class="metric-text">{i18n.now}</span>
<span class="value">{latestValue}</span>
</p>
<p class="foot-item">
<span class="dot tone-lime"></span>
<span class="metric-text">Min</span>
<span class="metric-text">{i18n.min}</span>
<span class="value">{minValue}</span>
</p>
<p class="foot-item">
<span class="dot tone-orange"></span>
<span class="metric-text">Max</span>
<span class="metric-text">{i18n.max}</span>
<span class="value">{maxValue}</span>
</p>
</footer>
@@ -300,12 +369,10 @@
--enter-ms: 1800ms;
--fade-ms: 1000ms;
overflow: hidden;
inline-size: min(100%, clamp(29rem, 38vw, 37rem));
aspect-ratio: 1.42 / 1;
min-block-size: 20.5rem;
inline-size: min(100%, clamp(34rem, 44vw, 44rem));
justify-self: start;
display: grid;
grid-template-rows: auto auto auto;
grid-template-rows: auto 1fr auto;
gap: 0.68rem;
padding: 0.88rem 0.96rem 1rem;
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.42);
@@ -404,6 +471,7 @@
.chart-stage {
position: relative;
block-size: clamp(12rem, 15.5vw, 15rem);
min-block-size: 5rem;
overflow: hidden;
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.32);
border-radius: 0.62rem;
@@ -474,25 +542,29 @@
.panel-foot {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
gap: 0.8rem;
margin: 0;
padding: 0;
flex-shrink: 0;
}
.foot-item {
margin: 0;
display: inline-flex;
align-items: center;
gap: 0.28rem;
gap: 0.22rem;
color: rgb(var(--hud-text-main-rgb) / 0.9);
font-size: 0.76rem;
letter-spacing: 0.04em;
font-size: 0.68rem;
letter-spacing: 0.03em;
white-space: nowrap;
flex-shrink: 0;
}
.metric-text {
color: rgb(var(--hud-text-dim-rgb) / 0.82);
text-transform: uppercase;
flex-shrink: 0;
}
.dot {
@@ -519,16 +591,18 @@
@media (max-width: 1180px) {
.signal-panel {
inline-size: min(100%, clamp(24rem, 36vw, 31rem));
aspect-ratio: 1.48 / 1;
min-block-size: 17rem;
inline-size: min(100%, clamp(28rem, 40vw, 38rem));
}
.chart-stage {
block-size: clamp(10rem, 13vw, 12rem);
}
}
@media (max-height: 900px) {
.signal-panel {
inline-size: min(100%, clamp(24rem, 33vw, 30rem));
min-block-size: 16.8rem;
inline-size: min(100%, clamp(28rem, 38vw, 36rem));
padding: 0.7rem 0.76rem 0.8rem;
}
.chart-stage {
@@ -538,46 +612,31 @@
@media (max-height: 760px) {
.signal-panel {
inline-size: min(100%, clamp(21rem, 29vw, 26rem));
min-block-size: 14.4rem;
padding: 0.7rem 0.76rem 0.8rem;
}
.panel-foot {
margin-block-start: 0.28rem;
inline-size: min(100%, clamp(24rem, 34vw, 30rem));
padding: 0.62rem 0.68rem 0.72rem;
gap: 0.48rem;
}
.chart-stage {
block-size: clamp(8.3rem, 9.6vw, 9.8rem);
block-size: clamp(8rem, 9.5vw, 9.8rem);
}
}
@media (max-height: 680px) {
.signal-panel {
inline-size: min(100%, clamp(18.5rem, 24vw, 22.5rem));
min-block-size: 12.4rem;
padding: 0.62rem 0.66rem 0.68rem;
}
.panel-head {
margin-block-end: 0.26rem;
}
.panel-foot {
margin-block-start: 0.18rem;
gap: 0.56rem;
inline-size: min(100%, clamp(20rem, 28vw, 26rem));
padding: 0.52rem 0.58rem 0.6rem;
gap: 0.36rem;
}
.chart-stage {
block-size: clamp(7rem, 7.8vw, 8rem);
block-size: clamp(6.5rem, 8vw, 7.5rem);
}
}
@media (max-width: 900px) {
.signal-panel {
inline-size: 100%;
aspect-ratio: 1.7 / 1;
min-block-size: 0;
}
}
</style>