import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig, } from "remotion"; import { COLORS, NOTES_ITALIAN } from "../lib/constants"; const STAFF_TOP = 300; const STAFF_GAP = 40; const STAFF_LEFT = 200; const NOTE_START_X = 380; const NOTE_GAP = 150; const StaffLine: React.FC<{ y: number; delay: number }> = ({ y, delay }) => { const frame = useCurrentFrame(); const localFrame = frame - delay; const width = interpolate(localFrame, [0, 30], [0, 1520], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); return (
); }; const NoteHead: React.FC<{ x: number; lineOffset: number; name: string; index: number; isPlaying: boolean; isHigh: boolean; }> = ({ x, lineOffset, name, index, isPlaying, isHigh }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const entryDelay = 50 + index * 18; const noteSpring = spring({ frame: Math.max(0, frame - entryDelay), fps, config: { damping: 10, stiffness: 100, mass: 0.6 }, }); const y = STAFF_TOP + lineOffset * STAFF_GAP - 18; const opacity = interpolate(noteSpring, [0, 1], [0, 1]); const scale = interpolate(noteSpring, [0, 1], [0, 1]); const noteY = interpolate(noteSpring, [0, 1], [-40, 0]); const glowIntensity = isPlaying ? interpolate( Math.sin(frame * 0.3), [-1, 1], [0.6, 1], ) : 0; // Stem direction based on position const stemUp = lineOffset > 2; // Ledger line for notes outside the staff const needsLedgerLine = lineOffset >= 6; return (
{/* Ledger line */} {needsLedgerLine && (
)} {/* Note glow */} {isPlaying && (
)} {/* Note head (ellipse) */}
{/* Stem */}
{/* Note name label */}
{name} {isHigh && ( 2 )}
); }; const TrebleClef: React.FC = () => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const clefSpring = spring({ frame: Math.max(0, frame - 25), fps, config: { damping: 12, stiffness: 60, mass: 1 }, }); const opacity = interpolate(clefSpring, [0, 1], [0, 0.9]); const scale = interpolate(clefSpring, [0, 1], [0.3, 1]); return (
{"\uD834\uDD1E"}
); }; const Playhead: React.FC = () => { const frame = useCurrentFrame(); const playStart = 70; const playEnd = 210; const progress = interpolate(frame, [playStart, playEnd], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const x = interpolate(progress, [0, 1], [NOTE_START_X - 30, NOTE_START_X + NOTE_GAP * 7 + 30]); const opacity = interpolate(frame, [playStart - 10, playStart, playEnd, playEnd + 10], [0, 0.8, 0.8, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); return (
); }; export const StaffScene: React.FC = () => { const frame = useCurrentFrame(); // Scene fade const fadeIn = interpolate(frame, [0, 30], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const fadeOut = interpolate(frame, [240, 270], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); // Section title const titleOpacity = interpolate(frame, [10, 30], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const titleY = interpolate(frame, [10, 30], [20, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); // Calculate which note is "playing" const playStart = 70; const noteDuration = 18; const activeNoteIndex = Math.floor( (frame - playStart) / noteDuration ); // Bottom scale visualization const scaleBarCount = 24; return ( {/* Section title */}
La Scala di{" "} DO Maggiore
{/* Staff lines */} {Array.from({ length: 5 }, (_, i) => ( ))} {/* Treble clef */} {/* Playhead */} {/* Notes */} {NOTES_ITALIAN.map((note, i) => ( = playStart} isHigh={i === 7} /> ))} {/* Bottom frequency bars */}
{Array.from({ length: scaleBarCount }, (_, i) => { const barHeight = interpolate( Math.sin(frame * 0.08 + i * 0.5), [-1, 1], [10, 60 + (activeNoteIndex >= 0 && activeNoteIndex < 8 ? 20 : 0)], ); const barOpacity = interpolate(frame, [60, 80], [0, 0.5], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); return (
); })}
{/* Brace decoration */}
); };