feat:增加点和数字切换,减小点最大尺寸,增加range配色方案
This commit is contained in:
@@ -3,12 +3,12 @@
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { pressureColorPalettes } from "$lib/config/color-map";
|
||||
import type { PressureColorMapPreset } from "$lib/types/hud";
|
||||
import type { HudSummary, MatrixDisplayMode, PressureColorMapPreset } from "$lib/types/hud";
|
||||
|
||||
interface ViewerStats {
|
||||
total: number;
|
||||
max: number;
|
||||
avg: number;
|
||||
current: number | null;
|
||||
max: number | null;
|
||||
min: number | null;
|
||||
}
|
||||
|
||||
interface MatrixLayout {
|
||||
@@ -28,12 +28,14 @@
|
||||
export let rangeMin = 0;
|
||||
export let rangeMax = 16000;
|
||||
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||
export let summary: HudSummary | null = null;
|
||||
export let showStatsPanel = true;
|
||||
|
||||
let viewerEl: HTMLDivElement | undefined;
|
||||
let canvasEl: HTMLCanvasElement | undefined;
|
||||
let overlayEl: HTMLCanvasElement | undefined;
|
||||
let stats: ViewerStats = { total: 0, max: 0, avg: 0 };
|
||||
let stats: ViewerStats = { current: null, max: null, min: null };
|
||||
|
||||
const DEFAULT_RANGE_MAX = 16000;
|
||||
const BASE_MATRIX_SPAN = 24;
|
||||
@@ -63,6 +65,7 @@
|
||||
const CAMERA_TARGET_Y = MATRIX_OFFSET_Y + 0.2;
|
||||
const CAMERA_TARGET_Z = MATRIX_OFFSET_Z - 0.4;
|
||||
const MATRIX_ROTATION_Y = 0;
|
||||
const rangeStopPositions = [0, 0.25, 0.5, 0.75, 0.875, 1] as const;
|
||||
|
||||
const labelVector = new THREE.Vector3();
|
||||
$: resolvedColorPalette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
||||
@@ -75,6 +78,7 @@
|
||||
$: labelLowColor = new THREE.Color(resolvedColorPalette.labelLow);
|
||||
$: labelMidColor = new THREE.Color(resolvedColorPalette.labelMid);
|
||||
$: labelHighColor = new THREE.Color(resolvedColorPalette.labelHigh);
|
||||
$: rangeStopColors = resolvedColorPalette.rangeStops.map((stop) => new THREE.Color(stop));
|
||||
$: sceneClearColor = new THREE.Color(resolvedColorPalette.uiTheme.bg30);
|
||||
$: sceneBoardColor = new THREE.Color(resolvedColorPalette.uiTheme.bg20);
|
||||
$: sceneGridCenterColor = new THREE.Color(resolvedColorPalette.surfaceMid);
|
||||
@@ -84,7 +88,7 @@
|
||||
$: sceneAccentLightColor = rgbTripletToThreeColor(resolvedColorPalette.uiTheme.glowRgb);
|
||||
$: surfaceThemeAccentColor = rgbTripletToThreeColor(resolvedColorPalette.uiTheme.glowRgb);
|
||||
$: labelThemeAccentColor = rgbTripletToThreeColor(resolvedColorPalette.uiTheme.glowRgb);
|
||||
$: labelHighlightCss = colorToCss(surfaceHotColor);
|
||||
$: labelHighlightCss = colorToCss(rangeStopColors[5] ?? surfaceHotColor);
|
||||
$: viewerThemeStyle = [
|
||||
`--matrix-bg-10: ${resolvedColorPalette.uiTheme.bg10}`,
|
||||
`--matrix-bg-20: ${resolvedColorPalette.uiTheme.bg20}`,
|
||||
@@ -124,7 +128,16 @@
|
||||
$: resolvedRangeMin = resolvedRange.min;
|
||||
$: resolvedRangeMax = resolvedRange.max;
|
||||
$: matrixLayout = buildMatrixLayout(resolvedMatrixRows, resolvedMatrixCols);
|
||||
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / color range ${resolvedRangeMin}-${resolvedRangeMax} / label raw`;
|
||||
$: statsModeLabel = matrixDisplayMode === "dots" ? "dot pulse" : "numeric pulse";
|
||||
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / force range ${resolvedRangeMin}-${resolvedRangeMax} / ${statsModeLabel}`;
|
||||
|
||||
function formatForceStat(value: number | null): string {
|
||||
if (value == null || !Number.isFinite(value)) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
@@ -143,23 +156,26 @@
|
||||
return new THREE.Color(`rgb(${rgbTriplet.replace(/\s+/g, ", ")})`);
|
||||
}
|
||||
|
||||
function surfaceColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
|
||||
function sampleRangeStopColor(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
|
||||
const value = clamp(valueNormalized, 0, 1);
|
||||
let mapped: THREE.Color;
|
||||
|
||||
if (value <= 0.45) {
|
||||
const t = smoothstep(0, 0.45, value);
|
||||
mapped = target.copy(surfaceBaseColor).lerp(surfaceLowColor, t);
|
||||
} else if (value <= 0.78) {
|
||||
const t = smoothstep(0.45, 0.78, value);
|
||||
mapped = target.copy(surfaceLowColor).lerp(surfaceMidColor, t);
|
||||
} else {
|
||||
const t = smoothstep(0.78, 1, value);
|
||||
mapped = target.copy(surfaceMidColor).lerp(surfaceHighColor, t);
|
||||
for (let index = 0; index < rangeStopPositions.length - 1; index += 1) {
|
||||
const start = rangeStopPositions[index];
|
||||
const end = rangeStopPositions[index + 1];
|
||||
if (value <= end) {
|
||||
const localT = smoothstep(start, end, value);
|
||||
return target.copy(rangeStopColors[index]).lerp(rangeStopColors[index + 1], localT);
|
||||
}
|
||||
}
|
||||
|
||||
const baseAccentStrength = (1 - smoothstep(0.12, 0.52, value)) * 0.34;
|
||||
const highlightStrength = smoothstep(0.82, 1, value) * 0.3;
|
||||
return target.copy(rangeStopColors[rangeStopColors.length - 1]);
|
||||
}
|
||||
|
||||
function surfaceColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
|
||||
const value = clamp(valueNormalized, 0, 1);
|
||||
const mapped = sampleRangeStopColor(value, target);
|
||||
const baseAccentStrength = (1 - smoothstep(0.08, 0.28, value)) * 0.16;
|
||||
const highlightStrength = smoothstep(0.88, 1, value) * 0.2;
|
||||
return mapped.lerp(surfaceThemeAccentColor, baseAccentStrength).lerp(surfaceHotColor, highlightStrength);
|
||||
}
|
||||
|
||||
@@ -171,22 +187,10 @@
|
||||
|
||||
function labelColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
|
||||
const value = clamp(valueNormalized, 0, 1);
|
||||
let mapped: THREE.Color;
|
||||
|
||||
if (value <= 0.34) {
|
||||
const t = smoothstep(0, 0.34, value);
|
||||
mapped = target.copy(labelZeroColor).lerp(labelLowColor, t);
|
||||
} else if (value <= 0.76) {
|
||||
const t = smoothstep(0.34, 0.76, value);
|
||||
mapped = target.copy(labelLowColor).lerp(labelMidColor, t);
|
||||
} else {
|
||||
const t = smoothstep(0.76, 1, value);
|
||||
mapped = target.copy(labelMidColor).lerp(labelHighColor, t);
|
||||
}
|
||||
|
||||
const baseAccentStrength = (1 - smoothstep(0.16, 0.58, value)) * 0.46;
|
||||
const highlightStrength = smoothstep(0.8, 1, value) * 0.36;
|
||||
return mapped.lerp(labelThemeAccentColor, baseAccentStrength).lerp(surfaceHotColor, highlightStrength);
|
||||
const mapped = sampleRangeStopColor(value, target);
|
||||
const baseAccentStrength = (1 - smoothstep(0.08, 0.24, value)) * 0.18;
|
||||
const highlightStrength = smoothstep(0.88, 1, value) * 0.12;
|
||||
return mapped.lerp(labelThemeAccentColor, baseAccentStrength).lerp(labelHighColor, highlightStrength);
|
||||
}
|
||||
|
||||
function shapeHeightValue(valueNormalized: number): number {
|
||||
@@ -263,6 +267,24 @@
|
||||
return `rgb(${Math.round(color.r * 255)} ${Math.round(color.g * 255)} ${Math.round(color.b * 255)})`;
|
||||
}
|
||||
|
||||
function drawProjectedDot(
|
||||
context: CanvasRenderingContext2D,
|
||||
screenX: number,
|
||||
screenY: number,
|
||||
radius: number,
|
||||
fillStyle: string,
|
||||
glowStyle: string,
|
||||
opacity: number
|
||||
): void {
|
||||
context.globalAlpha = opacity;
|
||||
context.shadowBlur = radius * 2.8;
|
||||
context.shadowColor = glowStyle;
|
||||
context.fillStyle = fillStyle;
|
||||
context.beginPath();
|
||||
context.arc(screenX, screenY, radius, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
}
|
||||
|
||||
$: labelPalette = Array.from({ length: 33 }, (_, index) => {
|
||||
const t = index / 32;
|
||||
return colorToCss(labelColorMap(t, new THREE.Color()));
|
||||
@@ -431,7 +453,7 @@
|
||||
const compactField = new Uint16Array(instanceCount);
|
||||
let lastFrameAt = performance.now();
|
||||
|
||||
const drawNumberOverlay = () => {
|
||||
const drawOverlay = () => {
|
||||
if (!viewerEl || !overlayEl) {
|
||||
return;
|
||||
}
|
||||
@@ -464,10 +486,42 @@
|
||||
|
||||
const normalized = normalizedField[index];
|
||||
const displayValue = compactField[index];
|
||||
const bucket = Math.min(32, Math.round(normalized * 32));
|
||||
const isDotsMode = matrixDisplayMode === "dots";
|
||||
|
||||
if (isDotsMode) {
|
||||
const baseDotRadius = clamp(cellSpacing * 0.48, 7.2, 21.6);
|
||||
const dotRadius = clamp(baseDotRadius + smoothstep(0, 1, normalized) * (cellSpacing * 0.86 + 9.6), 7.2, 15);
|
||||
const dotOpacity = displayValue === 0 ? 0.62 : 0.98;
|
||||
|
||||
drawProjectedDot(
|
||||
overlayContext,
|
||||
screenX,
|
||||
screenY,
|
||||
dotRadius,
|
||||
labelPalette[bucket],
|
||||
labelGlowPalette[bucket],
|
||||
dotOpacity
|
||||
);
|
||||
|
||||
if (normalized >= 0.8) {
|
||||
drawProjectedDot(
|
||||
overlayContext,
|
||||
screenX,
|
||||
screenY,
|
||||
dotRadius * 0.46,
|
||||
labelHighlightCss,
|
||||
labelHighlightCss,
|
||||
smoothstep(0.8, 1, normalized) * 0.42
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const displayText = String(displayValue);
|
||||
const digitCount = displayText.length;
|
||||
const digitScale = digitCount <= 2 ? 1 : digitCount === 3 ? 0.86 : digitCount === 4 ? 0.74 : 0.64;
|
||||
const bucket = Math.min(32, Math.round(normalized * 32));
|
||||
const baseGlyphSize = fontSize + smoothstep(0, 1, normalized) * (2.3 * labelScale + cellSpacing * 0.26);
|
||||
const glyphSize = clamp(baseGlyphSize * digitScale, 5.2, 26);
|
||||
const glowSizeFactor = digitCount >= 4 ? 0.76 : digitCount === 3 ? 0.88 : 1;
|
||||
@@ -476,7 +530,6 @@
|
||||
overlayContext.font = `600 ${glyphSize.toFixed(1)}px "JetBrains Mono", "IBM Plex Mono", "Consolas", monospace`;
|
||||
overlayContext.shadowBlur = glowBlur;
|
||||
overlayContext.shadowColor = labelGlowPalette[bucket];
|
||||
|
||||
overlayContext.fillStyle = labelPalette[bucket];
|
||||
overlayContext.globalAlpha = displayValue === 0 ? 0.86 : 1;
|
||||
overlayContext.fillText(displayText, screenX, screenY);
|
||||
@@ -550,9 +603,6 @@
|
||||
}
|
||||
|
||||
const maxValue = normalizeField(smoothedField, normalizedField, resolvedRangeMin, resolvedRangeMax);
|
||||
let total = 0;
|
||||
let activeCount = 0;
|
||||
|
||||
for (let index = 0; index < instanceCount; index += 1) {
|
||||
const normalized = normalizedField[index];
|
||||
const heightValue = shapeHeightValue(normalized);
|
||||
@@ -560,20 +610,15 @@
|
||||
|
||||
heightField[index] = height;
|
||||
compactField[index] = compactDisplayValue(smoothedField[index], resolvedRangeMin, resolvedRangeMax);
|
||||
|
||||
total += smoothedField[index];
|
||||
if (smoothedField[index] > 30) {
|
||||
activeCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
drawNumberOverlay();
|
||||
drawOverlay();
|
||||
|
||||
stats = {
|
||||
total,
|
||||
max: maxValue,
|
||||
avg: activeCount > 0 ? total / activeCount : 0
|
||||
current: summary?.latest ?? null,
|
||||
max: summary?.max ?? null,
|
||||
min: summary?.min ?? null
|
||||
};
|
||||
});
|
||||
|
||||
@@ -612,19 +657,19 @@
|
||||
{#if showStatsPanel}
|
||||
<div class="viewer-controls">
|
||||
<section class="stats-panel" aria-label="Pressure Summary">
|
||||
<p class="stats-label">Pressure Matrix</p>
|
||||
<p class="stats-label">Resultant Force</p>
|
||||
<div class="stats-grid">
|
||||
<article class="stats-card stats-card-wide">
|
||||
<span class="stats-key">Total Pressure</span>
|
||||
<strong class="stats-value">{stats.total.toFixed(0)}</strong>
|
||||
<span class="stats-key">Current RF</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.current)}</strong>
|
||||
</article>
|
||||
<article class="stats-card">
|
||||
<span class="stats-key">Max</span>
|
||||
<strong class="stats-value">{stats.max.toFixed(0)}</strong>
|
||||
<span class="stats-key">Max RF</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.max)}</strong>
|
||||
</article>
|
||||
<article class="stats-card">
|
||||
<span class="stats-key">Avg</span>
|
||||
<strong class="stats-value">{stats.avg.toFixed(0)}</strong>
|
||||
<span class="stats-key">Min RF</span>
|
||||
<strong class="stats-value">{formatForceStat(stats.min)}</strong>
|
||||
</article>
|
||||
</div>
|
||||
<p class="stats-note">{statsNote}</p>
|
||||
|
||||
Reference in New Issue
Block a user