diff --git a/src/lib/components/ScoreTimelineChart.svelte b/src/lib/components/ScoreTimelineChart.svelte new file mode 100644 index 0000000..0164de4 --- /dev/null +++ b/src/lib/components/ScoreTimelineChart.svelte @@ -0,0 +1,210 @@ + + +
+ + + 시간에 따른 점수 + + 평균 + 최고 + + + {#each yTicks as tick} + {formatAxisScore(tick)} + {/each} + +
hoverTime = null} + on:focus={() => { if (hoverTime === null) { hoverTime = 0; tooltipPercent = LEFT / viewWidth * 100; } }} + on:blur={() => hoverTime = null} + on:keydown={onKeydown} + > + + {#each timeTicks as tick} + + {/each} + {#each yTicks as tick} + + {/each} + + + {#if hoveredPoint && hoverTime !== null} + + + + {/if} + + + {#if hoveredPoint && hoverTime !== null} +
68} class="tooltip" style={`left: ${tooltipPercent}%;`} role="status"> + {formatTime(hoverTime)} + 평균 점수 {formatScore(hoveredPoint.averageScore)} + 최고 점수 {formatScore(hoveredPoint.maximumScore)} +
+ {/if} +
+
+ + diff --git a/src/routes/analysis/+page.svelte b/src/routes/analysis/+page.svelte index a17f780..e88841d 100644 --- a/src/routes/analysis/+page.svelte +++ b/src/routes/analysis/+page.svelte @@ -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 @@
-
- 초당 노트 -
- {#each noteCountsBySecond as count, second} - - {/each} -
-
+ {#each result.order as cardId, slot}
{slot + 1}{resultCardName(cardId)} @@ -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; } }