Remotion — VU Meter Animation
A studio-grade stereo VU meter rendered in Remotion at 1920x1080 30fps — dual analog needle meters sweep with spring physics while LED segment bars pulse in green, yellow, and red zones driven by stacked sine waves simulating live audio dynamics, with peak-hold markers, a dB scale ruler, decorative spectrum strip, and a dark professional broadcast aesthetic complete with corner bracket accents and a breathing purple glow.
Preview
Code
import React from "react";
import {
AbsoluteFill,
Easing,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
// ── Palette ────────────────────────────────────────────────────────────────────
const C = {
bg: "#0a0a0f",
surface: "#12121a",
surface2: "#1e1e2e",
accent: "#a855f7",
accent2: "#06b6d4",
accent3: "#ec4899",
gold: "#f59e0b",
green: "#10b981",
text: "#f1f5f9",
muted: "#94a3b8",
} as const;
// ── Audio simulation helpers ──────────────────────────────────────────────────
/**
* Produces a 0..1 audio level for a given channel and frame.
* Stacks 4 sine waves at different frequencies / phases for an organic,
* music-like envelope. Phase offset differentiates L vs R channels.
*/
function channelLevel(frame: number, phaseOffset: number): number {
const w1 = Math.sin(frame * 0.14 + phaseOffset) * 0.30;
const w2 = Math.sin(frame * 0.31 + phaseOffset * 1.7 + 1.1) * 0.22;
const w3 = Math.sin(frame * 0.07 + phaseOffset * 0.4 + 2.3) * 0.28;
const w4 = Math.sin(frame * 0.58 + phaseOffset * 2.1 + 0.7) * 0.12;
// raw in -0.92..+0.92; normalize to 0..1 then clamp
const raw = (w1 + w2 + w3 + w4 + 0.92) / 1.84;
return Math.min(1, Math.max(0, raw));
}
/**
* Spectrum-analyzer style: per-band level for the needle meter background fill.
* Returns 0..1 for bandIndex out of totalBands.
*/
function spectrumBand(
bandIndex: number,
totalBands: number,
frame: number,
phaseOffset: number
): number {
const norm = bandIndex / totalBands;
const centerBoost = Math.exp(-Math.pow(norm - 0.35, 2) * 8);
const w1 = Math.sin(frame * 0.18 + bandIndex * 0.42 + phaseOffset) * 0.35;
const w2 = Math.sin(frame * 0.37 + bandIndex * 0.81 + phaseOffset + 1.5) * 0.25;
const w3 = Math.sin(frame * 0.09 + bandIndex * 0.23 + phaseOffset + 3.1) * 0.20;
const raw = (w1 + w2 + w3 + centerBoost * 0.45 + 0.80) / 1.60;
return Math.min(1, Math.max(0, raw));
}
// ── dB scale ──────────────────────────────────────────────────────────────────
// Maps a normalized 0..1 level to a dB string and color zone
function levelToDb(norm: number): number {
// Map 0..1 → -20..+3 dB (typical VU range)
return -20 + norm * 23;
}
function segmentColor(segNorm: number): string {
if (segNorm < 0.70) return C.green; // green zone
if (segNorm < 0.85) return "#f59e0b"; // yellow zone
return "#ef4444"; // red zone
}
// ── Peak-hold hook ────────────────────────────────────────────────────────────
// Returns the peak-hold position (0..1) for a given channel level history.
// We can't use useState in a render-time hook (Remotion renders every frame fresh),
// so we pre-compute the peak deterministically by scanning backward 45 frames.
function peakHold(
frame: number,
phaseOffset: number,
holdFrames = 45
): number {
let peak = 0;
const startFrame = Math.max(0, frame - holdFrames);
for (let f = startFrame; f <= frame; f++) {
const lvl = channelLevel(f, phaseOffset);
if (lvl > peak) peak = lvl;
}
// After reaching peak, drift it downward over holdFrames
const peakFrame = (() => {
let pf = 0;
let pk = 0;
for (let f = startFrame; f <= frame; f++) {
const lvl = channelLevel(f, phaseOffset);
if (lvl > pk) { pk = lvl; pf = f; }
}
return pf;
})();
const elapsed = frame - peakFrame;
const fallOff = Math.max(0, 1 - elapsed / holdFrames);
return peak * (fallOff * 0.6 + 0.4); // partial gravity fall
}
// ── Grid texture overlay ──────────────────────────────────────────────────────
function GridOverlay({ width, height }: { width: number; height: number }) {
const lines: React.ReactNode[] = [];
const step = 60;
for (let x = 0; x <= width; x += step) {
lines.push(
<line key={`v${x}`} x1={x} y1={0} x2={x} y2={height}
stroke="rgba(255,255,255,0.025)" strokeWidth={1} />
);
}
for (let y = 0; y <= height; y += step) {
lines.push(
<line key={`h${y}`} x1={0} y1={y} x2={width} y2={y}
stroke="rgba(255,255,255,0.025)" strokeWidth={1} />
);
}
return (
<svg style={{ position: "absolute", inset: 0 }} width={width} height={height}>
{lines}
</svg>
);
}
// ── LED VU Bar ────────────────────────────────────────────────────────────────
const SEGMENT_COUNT = 32;
const SEG_GAP = 3; // px gap between segments
interface LedVuBarProps {
level: number; // 0..1 current level
peak: number; // 0..1 peak-hold position
barWidth: number;
barHeight: number;
entrance: number; // spring 0..1 entrance scale
}
function LedVuBar({ level, peak, barWidth, barHeight, entrance }: LedVuBarProps) {
const segHeight = (barHeight - SEG_GAP * (SEGMENT_COUNT - 1)) / SEGMENT_COUNT;
const litCount = Math.round(level * SEGMENT_COUNT);
const peakSeg = Math.round(peak * SEGMENT_COUNT);
return (
<div
style={{
width: barWidth,
height: barHeight,
display: "flex",
flexDirection: "column-reverse",
gap: SEG_GAP,
transform: `scaleY(${entrance})`,
transformOrigin: "bottom center",
}}
>
{Array.from({ length: SEGMENT_COUNT }, (_, i) => {
const segNorm = (i + 1) / SEGMENT_COUNT;
const isLit = i < litCount;
const isPeak = i === peakSeg - 1 && peakSeg > litCount;
const color = segmentColor(segNorm);
const opacity = isLit ? 1 : isPeak ? 0.95 : 0.10;
const glow = isLit
? `0 0 ${segNorm > 0.85 ? 14 : segNorm > 0.70 ? 8 : 6}px ${color}cc`
: isPeak
? `0 0 16px #ffffffcc, 0 0 8px ${color}`
: "none";
return (
<div
key={i}
style={{
width: "100%",
height: segHeight,
borderRadius: 2,
backgroundColor: isLit
? color
: isPeak
? "#ffffff"
: `${color}22`,
opacity,
boxShadow: glow,
transition: "background-color 0.02s",
}}
/>
);
})}
</div>
);
}
// ── dB Scale Ruler ────────────────────────────────────────────────────────────
const DB_LABELS = ["+3", "0", "-3", "-6", "-10", "-20"];
// positions as fractions from top (1 = top, 0 = bottom) matching 0..1 level
const DB_POSITIONS: Record<string, number> = {
"+3": 1.0,
"0": 0.87,
"-3": 0.74,
"-6": 0.61,
"-10": 0.43,
"-20": 0.13,
};
function DbScale({ height, side = "right" }: { height: number; side?: "left" | "right" }) {
return (
<div style={{ position: "relative", height, width: 44 }}>
{DB_LABELS.map((label) => {
const top = (1 - DB_POSITIONS[label]) * height;
return (
<div
key={label}
style={{
position: "absolute",
top,
[side === "right" ? "left" : "right"]: 0,
transform: "translateY(-50%)",
fontSize: 13,
fontFamily: "'Courier New', monospace",
color: label === "+3" || label === "0" ? "#ef4444" : label === "-3" ? "#f59e0b" : C.muted,
fontWeight: 600,
lineHeight: 1,
whiteSpace: "nowrap",
}}
>
{label}
</div>
);
})}
</div>
);
}
// ── Analog Needle Meter ───────────────────────────────────────────────────────
interface NeedleMeterProps {
level: number; // 0..1
label: string;
width: number;
height: number;
entrance: number; // spring 0..1
}
function NeedleMeter({ level, label, width, height, entrance }: NeedleMeterProps) {
const cx = width / 2;
const cy = height * 0.88;
const radius = width * 0.42;
// Arc from -135° to -45° (left to right swing, 90° total)
const startAngle = -145;
const endAngle = -35;
const needleAngle = startAngle + (endAngle - startAngle) * level;
const needleRad = (needleAngle * Math.PI) / 180;
const needleLength = radius * 0.88;
const nx = cx + Math.cos(needleRad) * needleLength;
const ny = cy + Math.sin(needleRad) * needleLength;
// Arc path helpers
function arcPath(r: number, start: number, end: number): string {
const s = ((start * Math.PI) / 180);
const e = ((end * Math.PI) / 180);
const x1 = cx + Math.cos(s) * r;
const y1 = cy + Math.sin(s) * r;
const x2 = cx + Math.cos(e) * r;
const y2 = cy + Math.sin(e) * r;
const large = Math.abs(end - start) > 180 ? 1 : 0;
return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;
}
// Tick marks on the arc
const ticks = Array.from({ length: 11 }, (_, i) => {
const norm = i / 10;
const angle = startAngle + (endAngle - startAngle) * norm;
const rad = (angle * Math.PI) / 180;
const isMajor = i % 2 === 0;
const innerR = radius - (isMajor ? 20 : 12);
const outerR = radius + 4;
const tickColor =
norm > 0.87 ? "#ef4444" : norm > 0.70 ? "#f59e0b" : C.muted;
return (
<line
key={i}
x1={cx + Math.cos(rad) * innerR}
y1={cy + Math.sin(rad) * innerR}
x2={cx + Math.cos(rad) * outerR}
y2={cy + Math.sin(rad) * outerR}
stroke={tickColor}
strokeWidth={isMajor ? 2.5 : 1.5}
strokeLinecap="round"
/>
);
});
// Zone coloring along the arc
const greenEnd = startAngle + (endAngle - startAngle) * 0.70;
const yellowEnd = startAngle + (endAngle - startAngle) * 0.85;
const currentDb = levelToDb(level);
return (
<div
style={{
width,
height,
position: "relative",
background: `linear-gradient(180deg, #1a1a2e 0%, ${C.surface} 100%)`,
borderRadius: 16,
border: `1px solid rgba(255,255,255,0.08)`,
boxShadow: `0 0 40px rgba(168,85,247,0.12), inset 0 1px 0 rgba(255,255,255,0.06)`,
overflow: "hidden",
transform: `scale(${entrance})`,
transformOrigin: "center bottom",
}}
>
{/* Subtle radial bg glow */}
<div style={{
position: "absolute",
inset: 0,
background: `radial-gradient(ellipse 80% 60% at 50% 100%, rgba(168,85,247,0.07) 0%, transparent 70%)`,
pointerEvents: "none",
}} />
<svg
width={width}
height={height}
style={{ position: "absolute", inset: 0 }}
>
{/* Outer arc track */}
<path d={arcPath(radius + 6, startAngle, endAngle)}
fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth={14} strokeLinecap="round" />
{/* Green zone */}
<path d={arcPath(radius + 6, startAngle, greenEnd)}
fill="none" stroke={`${C.green}55`} strokeWidth={14} strokeLinecap="round" />
{/* Yellow zone */}
<path d={arcPath(radius + 6, greenEnd, yellowEnd)}
fill="none" stroke="#f59e0b55" strokeWidth={14} strokeLinecap="round" />
{/* Red zone */}
<path d={arcPath(radius + 6, yellowEnd, endAngle)}
fill="none" stroke="#ef444455" strokeWidth={14} strokeLinecap="round" />
{/* Active fill arc up to current level */}
<path
d={arcPath(radius + 6, startAngle, needleAngle)}
fill="none"
stroke={
level > 0.87 ? "#ef4444" : level > 0.70 ? "#f59e0b" : C.green
}
strokeWidth={14}
strokeLinecap="round"
opacity={0.75}
/>
{/* Tick marks */}
{ticks}
{/* Needle shadow/glow */}
<line
x1={cx} y1={cy}
x2={nx} y2={ny}
stroke={level > 0.87 ? "#ef444460" : "#a855f760"}
strokeWidth={8}
strokeLinecap="round"
/>
{/* Needle */}
<line
x1={cx} y1={cy}
x2={nx} y2={ny}
stroke={level > 0.87 ? "#ff6b6b" : "#f1f5f9"}
strokeWidth={2.5}
strokeLinecap="round"
/>
{/* Pivot circle */}
<circle cx={cx} cy={cy} r={9} fill={C.surface2} stroke="rgba(255,255,255,0.2)" strokeWidth={2} />
<circle cx={cx} cy={cy} r={4} fill={level > 0.87 ? "#ef4444" : C.accent} />
</svg>
{/* dB readout */}
<div style={{
position: "absolute",
bottom: 18,
left: "50%",
transform: "translateX(-50%)",
textAlign: "center",
}}>
<div style={{
fontFamily: "'Courier New', monospace",
fontSize: 22,
fontWeight: 700,
color: level > 0.87 ? "#ff6b6b" : level > 0.70 ? "#f59e0b" : C.green,
letterSpacing: "0.05em",
textShadow: `0 0 12px ${level > 0.87 ? "#ef4444" : level > 0.70 ? "#f59e0b" : C.green}`,
}}>
{currentDb >= 0 ? "+" : ""}{currentDb.toFixed(1)} dB
</div>
<div style={{
fontFamily: "Inter, sans-serif",
fontSize: 13,
color: C.muted,
letterSpacing: "0.15em",
textTransform: "uppercase",
marginTop: 2,
}}>
{label}
</div>
</div>
</div>
);
}
// ── Stereo LED VU Panel ───────────────────────────────────────────────────────
interface StereoVuPanelProps {
levelL: number;
levelR: number;
peakL: number;
peakR: number;
barWidth: number;
barHeight: number;
entrance: number;
}
function StereoVuPanel({
levelL, levelR, peakL, peakR,
barWidth, barHeight, entrance,
}: StereoVuPanelProps) {
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "stretch",
background: `linear-gradient(180deg, #1a1a2e 0%, ${C.surface} 100%)`,
borderRadius: 16,
border: `1px solid rgba(255,255,255,0.08)`,
boxShadow: `0 0 60px rgba(168,85,247,0.15), inset 0 1px 0 rgba(255,255,255,0.06)`,
padding: "24px 28px 20px",
gap: 16,
}}>
{/* Panel header */}
<div style={{
fontFamily: "'Courier New', monospace",
fontSize: 11,
color: C.muted,
letterSpacing: "0.3em",
textTransform: "uppercase",
textAlign: "center",
borderBottom: "1px solid rgba(255,255,255,0.06)",
paddingBottom: 12,
}}>
STEREO LEVEL METER · DIGITAL
</div>
{/* Bars row */}
<div style={{
display: "flex",
flexDirection: "row",
alignItems: "flex-end",
gap: 8,
justifyContent: "center",
}}>
{/* L label */}
<div style={{
fontFamily: "'Courier New', monospace",
fontSize: 14,
fontWeight: 700,
color: C.accent2,
letterSpacing: "0.15em",
paddingBottom: 4,
width: 20,
textAlign: "center",
}}>L</div>
{/* Left bar + scale */}
<LedVuBar
level={levelL}
peak={peakL}
barWidth={barWidth}
barHeight={barHeight}
entrance={entrance}
/>
{/* dB scale in the middle */}
<DbScale height={barHeight} side="right" />
{/* Right bar */}
<LedVuBar
level={levelR}
peak={peakR}
barWidth={barWidth}
barHeight={barHeight}
entrance={entrance}
/>
{/* R label */}
<div style={{
fontFamily: "'Courier New', monospace",
fontSize: 14,
fontWeight: 700,
color: C.accent3,
letterSpacing: "0.15em",
paddingBottom: 4,
width: 20,
textAlign: "center",
}}>R</div>
</div>
{/* Level readout row */}
<div style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
paddingTop: 8,
borderTop: "1px solid rgba(255,255,255,0.06)",
}}>
{[
{ ch: "L", level: levelL, color: C.accent2 },
{ ch: "R", level: levelR, color: C.accent3 },
].map(({ ch, level, color }) => {
const db = levelToDb(level);
return (
<div key={ch} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2 }}>
<div style={{
fontFamily: "'Courier New', monospace",
fontSize: 18,
fontWeight: 700,
color: level > 0.87 ? "#ff6b6b" : color,
textShadow: `0 0 10px ${level > 0.87 ? "#ef4444" : color}`,
}}>
{db >= 0 ? "+" : ""}{db.toFixed(1)} dB
</div>
<div style={{
fontFamily: "Inter, sans-serif",
fontSize: 11,
color: C.muted,
letterSpacing: "0.2em",
}}>{ch} CH</div>
</div>
);
})}
</div>
</div>
);
}
// ── Spectrum Mini-bar (decorative footer strip) ────────────────────────────────
function SpectrumStrip({ frame, width }: { frame: number; width: number }) {
const bands = 48;
const bandW = Math.floor((width - 80) / bands) - 2;
const maxH = 40;
return (
<div style={{
display: "flex",
flexDirection: "row",
alignItems: "flex-end",
gap: 2,
height: maxH + 8,
paddingBottom: 4,
}}>
{Array.from({ length: bands }, (_, i) => {
const lvl = spectrumBand(i, bands, frame, 0);
const h = Math.max(4, lvl * maxH);
const color = i < bands * 0.5 ? C.accent : C.accent2;
return (
<div key={i} style={{
width: bandW,
height: h,
borderRadius: 2,
background: `linear-gradient(180deg, ${color} 0%, ${color}88 100%)`,
boxShadow: `0 0 4px ${color}66`,
}} />
);
})}
</div>
);
}
// ── Main composition ──────────────────────────────────────────────────────────
export function VuMeterAnimation() {
const frame = useCurrentFrame();
const { fps, width, height, durationInFrames } = useVideoConfig();
// Entrance springs
const titleEntrance = spring({ frame, fps, config: { damping: 18, stiffness: 80 }, durationInFrames: 25 });
const panelEntrance = spring({ frame: Math.max(0, frame - 8), fps, config: { damping: 16, stiffness: 70 }, durationInFrames: 30 });
const needleEntrance = spring({ frame: Math.max(0, frame - 15), fps, config: { damping: 20, stiffness: 90 }, durationInFrames: 25 });
// Per-channel audio simulation
const levelL = channelLevel(frame, 0.0);
const levelR = channelLevel(frame, 1.37); // different phase offset
// Needle uses spring() for physical settle behavior
// We feed a target and spring toward it over ~4 frames
const targetL = channelLevel(frame, 0.0);
const needleLevelL = spring({
frame,
fps,
from: channelLevel(Math.max(0, frame - 4), 0.0),
to: targetL,
config: { damping: 22, stiffness: 200 },
durationInFrames: 8,
});
const targetR = channelLevel(frame, 1.37);
const needleLevelR = spring({
frame,
fps,
from: channelLevel(Math.max(0, frame - 4), 1.37),
to: targetR,
config: { damping: 22, stiffness: 200 },
durationInFrames: 8,
});
// Peak hold
const peakL = peakHold(frame, 0.0);
const peakR = peakHold(frame, 1.37);
// Title opacity / slide
const titleSlide = interpolate(titleEntrance, [0, 1], [30, 0]);
const titleOpacity = interpolate(titleEntrance, [0, 1], [0, 1], { easing: Easing.out(Easing.ease) });
// Progress through clip (for status bar)
const progress = frame / durationInFrames;
// Breathing glow intensity
const glowPulse = 0.5 + 0.5 * Math.sin(frame * 0.12);
// Layout constants
const panelW = 340;
const panelH = 520;
const needleW = 380;
const needleH = 240;
const barW = 52;
const barH = 400;
const contentTop = 120;
// Center X for groups
const totalW = needleW * 2 + 60 + panelW + 80; // two needles + gap + panel + margin
const startX = (width - totalW) / 2;
const needleLX = startX;
const needleRX = startX + needleW + 60;
const panelX = needleRX + needleW + 80;
return (
<AbsoluteFill style={{ backgroundColor: C.bg, overflow: "hidden", fontFamily: "Inter, sans-serif" }}>
{/* Grid texture */}
<GridOverlay width={width} height={height} />
{/* Background radial glow */}
<div style={{
position: "absolute",
inset: 0,
background: `radial-gradient(ellipse 70% 50% at 50% 60%, rgba(168,85,247,${0.07 + glowPulse * 0.04}) 0%, transparent 70%)`,
pointerEvents: "none",
}} />
{/* ── Title bar ── */}
<div style={{
position: "absolute",
top: 36,
left: "50%",
transform: `translateX(-50%) translateY(${titleSlide}px)`,
opacity: titleOpacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 6,
}}>
<div style={{
fontFamily: "'Courier New', monospace",
fontSize: 52,
fontWeight: 900,
color: C.text,
letterSpacing: "0.35em",
textShadow: `0 0 30px rgba(168,85,247,0.5), 0 0 60px rgba(168,85,247,0.2)`,
}}>
VU METER
</div>
<div style={{
fontFamily: "Inter, sans-serif",
fontSize: 14,
color: C.muted,
letterSpacing: "0.25em",
textTransform: "uppercase",
}}>
Studio Reference · Analog + Digital
</div>
</div>
{/* ── Content area ── */}
<div style={{
position: "absolute",
top: contentTop,
left: 0,
right: 0,
bottom: 80,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 28,
}}>
{/* ── Top row: two analog needle meters ── */}
<div style={{
display: "flex",
flexDirection: "row",
gap: 32,
alignItems: "flex-end",
}}>
<NeedleMeter
level={needleLevelL}
label="L CHANNEL"
width={needleW}
height={needleH}
entrance={needleEntrance}
/>
<NeedleMeter
level={needleLevelR}
label="R CHANNEL"
width={needleW}
height={needleH}
entrance={needleEntrance}
/>
</div>
{/* Divider label */}
<div style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 20,
opacity: interpolate(panelEntrance, [0, 1], [0, 1]),
}}>
<div style={{ width: 200, height: 1, background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.1))" }} />
<div style={{
fontFamily: "'Courier New', monospace",
fontSize: 11,
color: C.muted,
letterSpacing: "0.3em",
textTransform: "uppercase",
}}>DIGITAL LED</div>
<div style={{ width: 200, height: 1, background: "linear-gradient(90deg, rgba(255,255,255,0.1), transparent)" }} />
</div>
{/* ── Bottom row: stereo LED VU panel ── */}
<div style={{ opacity: interpolate(panelEntrance, [0, 1], [0, 1]) }}>
<StereoVuPanel
levelL={levelL}
levelR={levelR}
peakL={peakL}
peakR={peakR}
barWidth={barW}
barHeight={barH * 0.65}
entrance={panelEntrance}
/>
</div>
</div>
{/* ── Footer spectrum strip + status ── */}
<div style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 72,
background: `linear-gradient(180deg, transparent 0%, rgba(10,10,15,0.95) 100%)`,
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingLeft: 48,
paddingRight: 48,
}}>
{/* Spectrum strip */}
<SpectrumStrip frame={frame} width={width * 0.55} />
{/* Status pill */}
<div style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 10,
background: C.surface2,
borderRadius: 999,
padding: "8px 20px",
border: "1px solid rgba(255,255,255,0.07)",
}}>
{/* Pulsing dot */}
<div style={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: C.green,
boxShadow: `0 0 ${8 + glowPulse * 8}px ${C.green}`,
}} />
<div style={{
fontFamily: "'Courier New', monospace",
fontSize: 12,
color: C.text,
letterSpacing: "0.15em",
}}>
MONITORING
</div>
{/* Progress */}
<div style={{
width: 80,
height: 3,
borderRadius: 2,
backgroundColor: "rgba(255,255,255,0.08)",
overflow: "hidden",
marginLeft: 8,
}}>
<div style={{
width: `${progress * 100}%`,
height: "100%",
background: "linear-gradient(90deg, #a855f7, #06b6d4)",
borderRadius: 2,
}} />
</div>
</div>
</div>
{/* ── Corner accent brackets ── */}
{[
{ top: 16, left: 16 },
{ top: 16, right: 16 },
{ bottom: 16, left: 16 },
{ bottom: 16, right: 16 },
].map((pos, i) => {
const opacity = interpolate(frame, [i * 5, i * 5 + 20], [0, 0.4], { extrapolateRight: "clamp" });
const isRight = "right" in pos;
const isBottom = "bottom" in pos;
return (
<div
key={i}
style={{
position: "absolute",
...pos,
width: 32,
height: 32,
borderTop: isBottom ? "none" : `2px solid rgba(168,85,247,0.5)`,
borderBottom: isBottom ? `2px solid rgba(168,85,247,0.5)` : "none",
borderLeft: isRight ? "none" : `2px solid rgba(168,85,247,0.5)`,
borderRight: isRight ? `2px solid rgba(168,85,247,0.5)` : "none",
opacity,
}}
/>
);
})}
</AbsoluteFill>
);
}
// ── Composition config ────────────────────────────────────────────────────────
export const compositionConfig = {
id: "remotion-vu-meter",
component: VuMeterAnimation,
durationInFrames: 150,
fps: 30,
width: 1920,
height: 1080,
};VU Meter Animation
This Remotion composition renders a professional studio VU meter at full 1920×1080 resolution over a five-second clip. The upper half features two analog-style needle meters — one for the left channel and one for the right — each drawn as an SVG arc with colored zone bands (green, yellow, red), ten tick-mark gradations, and a spring-physics needle that settles realistically into the current level on every frame. A dB readout beneath each needle updates in real time, turning red when the signal clips past 0 dB. Both needles slide in from a spring-driven entrance during the opening half-second.
Below the analog section, a side-by-side stereo LED panel shows the same channels as segmented vertical bars made of 32 colored segments each. Segments illuminate green from the bottom (0–70%), shift to amber through the mid-range (70–85%), and flash red at the top (85–100%), with per-segment glow box-shadows that intensify proportionally to the signal. A peak-hold marker — a bright white segment — hovers at the highest recent level for 45 frames before drifting downward under simulated gravity. A center dB scale ruler labels −20, −10, −6, −3, 0, and +3 dB aligned to the bar height, and live dB channel readouts sit beneath the bars. Channel separation is achieved by giving the left and right simulated audio streams different phase offsets, so they never move in perfect lockstep.
The background is dark #0a0a0f with a subtle pixel-grid overlay, a breathing radial purple glow that pulses at a slow cadence, and four corner bracket accents that fade in sequentially. A footer spectrum strip of 48 mini-bars visualizes a full-range frequency spread, flanked by a monitoring status pill with a live progress bar and a pulsing green dot. All motion is driven entirely by Math.sin waveforms and Remotion’s spring() / interpolate() primitives — no audio file or external library is needed.
Simulated audio data — waveform values are generated mathematically. No real audio file is required.