import { memo } from "react"; import type { TimelineTheme } from "./timelineTheme"; import { RULER_H, getTimelineBeatEntries } from "./timelineLayout"; import { formatTimelineTickLabel } from "./timelineRulerGeometry"; import { usePlayerStore } from "../store/playerStore"; import { secondsToFrame } from "../lib/time"; import type { MusicBeatAnalysis } from "@hyperframes/core/beats"; import type { TimelineTimeRange } from "../lib/timelineClipIndex"; interface TimelineRulerProps { major: number[]; minor: number[]; pps: number; trackContentWidth: number; totalH: number; effectiveDuration: number; majorTickInterval: number; theme: TimelineTheme; beatAnalysis?: MusicBeatAnalysis | null; contentOrigin: number; renderTimeRange?: TimelineTimeRange; } export const TimelineRuler = memo(function TimelineRuler({ major, minor, pps, trackContentWidth, totalH, effectiveDuration, majorTickInterval, theme, beatAnalysis, contentOrigin, renderTimeRange, }: TimelineRulerProps) { const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode); const beatTimes = beatAnalysis?.beatTimes ?? []; const beatStrengths = beatAnalysis?.beatStrengths ?? []; const beatEntries = getTimelineBeatEntries(beatTimes, beatStrengths, renderTimeRange); // Only draw beat lines when they'd be at least 5px apart const avgBeatInterval = beatTimes.length > 1 ? (beatTimes[beatTimes.length - 1]! - beatTimes[0]!) / (beatTimes.length - 1) : null; const showBeats = avgBeatInterval !== null && avgBeatInterval * pps >= 5; return ( <> {/* Background SVG — beat lines only; major-tick gridlines removed so only the ruler's own small ticks mark intervals (no full-height lines). */} {showBeats && beatEntries.map(({ time: t, index: i, strength: beatStrength }) => { const x = t * pps; // Louder beats → brighter line. Gamma curve widens the contrast. const strength = Math.pow(Math.min(1, beatStrength ?? 0.5), 2.2); const opacity = 0.08 + strength * 0.62; return ( ); })} {/* Ruler — sticky so the timestamps stay visible while the tracks scroll vertically. Opaque background (plus the label-column corner block) so clips scrolling underneath don't bleed through; z-index sits above the track rows and drag overlays but below the playhead (z 100). */}
{/* Breathing pad before 00:00 is folded into contentOrigin (see Timeline.tsx: GUTTER + TRACKS_LEFT_PAD), so no separate pad div. */}
{/* Each 1px tick line is shifted -0.5px so its CENTER sits exactly on t * pps — matching the playhead line, which is also centered on contentOrigin + t * pps (see getTimelinePlayheadLeft). Without the shift a tick spans [x, x+1) and its center is half a pixel right. */} {minor.map((t) => (
))} {major.map((t) => (
{timeDisplayMode === "frame" ? secondsToFrame(t) : formatTimelineTickLabel(t, effectiveDuration, majorTickInterval)}
))}
); });