feat: add score timeline to batch analysis
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import { createInitialReleaseClock, synchronizeReleaseClock } from '$lib/data/release-clock.ts';
|
||||
import { releasedSongCharts } from '$lib/data/song-release.ts';
|
||||
import PresetManager from '$lib/components/PresetManager.svelte';
|
||||
import ScoreTimelineChart from '$lib/components/ScoreTimelineChart.svelte';
|
||||
import SongPicker from '$lib/components/SongPicker.svelte';
|
||||
import { calculateBoardEffects, calculateExactScore, calculateUnitPower, countOptimizationCandidates, memoryParameterPermil, optimizeBoardConnectionCards, optimizeExactOrderAndCooldowns, perfectNoteScoreFromPower, toOptimizerCard } from '$lib/engine/index.ts';
|
||||
import type { CooldownReduction, EffectiveSkillWindow, ExactOptimizationResult, ExactSongChart, OptimizableConnectionCard, OptimizationProgress, ScoreAchievementProbabilities, SkillWindow } from '$lib/engine/index.ts';
|
||||
@@ -156,9 +157,7 @@
|
||||
$: perfectNoteScore = perfectNoteScoreFromPower(overallPower, selectedChart?.scoreRatio ?? null);
|
||||
$: scoreCalibrationLabel = selectedChart?.scoreCalibrationSource === 'measured' ? '실측값' : '추정값';
|
||||
$: timelineDuration = selectedChart?.durationSeconds ?? 0;
|
||||
$: noteCountsBySecond = result ? countNotesBySecond(result, timelineDuration) : [];
|
||||
$: displayedEffectiveActiveWindows = mergeEffectiveActiveWindows(result?.effectiveActiveWindows ?? []);
|
||||
$: maxNotesPerSecond = Math.max(1, ...noteCountsBySecond);
|
||||
$: timelineTicks = buildTimelineTicks(timelineDuration);
|
||||
$: timelineWidth = Math.max(720, timelineDuration * 6);
|
||||
$: candidateCount = (() => {
|
||||
@@ -691,21 +690,10 @@
|
||||
return Math.ceil(maximumScore * ratio).toLocaleString('ko-KR');
|
||||
}
|
||||
|
||||
function countNotesBySecond(scoreResult: ExactOptimizationResult, durationSeconds: number): number[] {
|
||||
const counts = Array.from({ length: Math.ceil(durationSeconds) }, () => 0);
|
||||
for (const note of scoreResult.notes) {
|
||||
const second = Math.floor(note.timeSeconds);
|
||||
if (second >= 0 && second < counts.length) counts[second] += 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function buildTimelineTicks(durationSeconds: number): number[] {
|
||||
if (durationSeconds <= 0) return [];
|
||||
const interval = 10;
|
||||
const ticks = Array.from({ length: Math.floor(durationSeconds / interval) + 1 }, (_, index) => index * interval);
|
||||
if (ticks.at(-1) !== durationSeconds) ticks.push(durationSeconds);
|
||||
return ticks;
|
||||
return Array.from({ length: Math.floor(durationSeconds / interval) + 1 }, (_, index) => index * interval);
|
||||
}
|
||||
|
||||
function timelinePosition(seconds: number): number {
|
||||
@@ -717,7 +705,7 @@
|
||||
}
|
||||
|
||||
function timelineTrackStyle(): string {
|
||||
return `width: ${timelineWidth}px; --time-grid-step: ${timelinePosition(10)}%;`;
|
||||
return `min-width: ${timelineWidth}px; --time-grid-step: ${timelinePosition(10)}%;`;
|
||||
}
|
||||
|
||||
function memberColor(cardId: string): string {
|
||||
@@ -948,14 +936,7 @@
|
||||
</div>
|
||||
<div class="timeline-scroll" role="region" aria-label="스킬 발동 시간대 그래프">
|
||||
<div class="timeline-canvas" style={`width: ${timelineWidth + 156}px;`}>
|
||||
<div class="timeline-row note-row">
|
||||
<strong class="lane-label">초당 노트</strong>
|
||||
<div class="timeline-track note-track" style={timelineTrackStyle()} aria-label={`최대 초당 ${maxNotesPerSecond}개`}>
|
||||
{#each noteCountsBySecond as count, second}
|
||||
<span class="note-bar" style={`left: ${timelinePosition(second)}%; width: ${100 / Math.max(1, noteCountsBySecond.length)}%; height: ${count / maxNotesPerSecond * 100}%; opacity: ${count === 0 ? 0 : 0.18 + count / maxNotesPerSecond * 0.82};`} title={`${second}~${second + 1}초 · ${count}개`}></span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<ScoreTimelineChart notes={result.notes} durationSeconds={timelineDuration} minimumWidth={timelineWidth} timeTicks={timelineTicks} />
|
||||
{#each result.order as cardId, slot}
|
||||
<div class="timeline-row member-row">
|
||||
<strong class="lane-label"><small style={`--member-color: ${memberColor(cardId)};`}>{slot + 1}</small>{resultCardName(cardId)}</strong>
|
||||
@@ -1133,16 +1114,13 @@
|
||||
.timeline-legend span { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
||||
.timeline-legend i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; }
|
||||
.timeline-scroll { overflow-x: auto; border: 1px solid #2c3440; border-radius: 11px; background: #10141a; scrollbar-width: thin; }
|
||||
.timeline-canvas { min-width: 100%; padding: 12px 12px 8px 0; }
|
||||
.timeline-canvas { min-width: 100%; padding: 12px 0 8px; }
|
||||
.timeline-row { display: flex; min-height: 36px; }
|
||||
.timeline-row + .timeline-row { border-top: 1px solid rgb(255 255 255 / 5%); }
|
||||
.lane-label { position: sticky; left: 0; z-index: 4; display: flex; align-items: center; gap: 7px; flex: 0 0 144px; min-width: 144px; padding: 0 10px 0 12px; overflow: hidden; color: #bac2ce; background: #141920; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lane-label small { display: grid; place-items: center; flex: none; width: 19px; height: 19px; border: 1px solid color-mix(in srgb, var(--member-color) 65%, transparent); border-radius: 5px; color: var(--member-color); background: color-mix(in srgb, var(--member-color) 12%, transparent); font-size: 11px; }
|
||||
.timeline-track { position: relative; flex: none; }
|
||||
.timeline-track { position: relative; flex: 1 0 auto; }
|
||||
.timeline-track::before { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, rgb(255 255 255 / 9%) 1px, transparent 1px); background-size: var(--time-grid-step, 10%) 100%; pointer-events: none; }
|
||||
.note-row { min-height: 68px; }
|
||||
.note-track { height: 52px; align-self: center; border-bottom: 1px solid #465062; }
|
||||
.note-bar { position: absolute; bottom: 0; min-height: 1px; border-right: 1px solid #10141a; background: #8ca9d7; }
|
||||
.skill-track { height: 36px; }
|
||||
.skill-window { position: absolute; min-width: 2px; border-radius: 3px; background: var(--member-color); }
|
||||
.active-window { top: 12px; height: 12px; opacity: .72; }
|
||||
@@ -1189,6 +1167,6 @@
|
||||
.timeline-heading { align-items: flex-start; flex-direction: column; }
|
||||
.timeline-legend { justify-content: flex-start; }
|
||||
.lane-label { flex-basis: 118px; min-width: 118px; }
|
||||
.timeline-canvas { padding-right: 8px; }
|
||||
.timeline-canvas { padding-right: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user