Catalog

Edit Props (live)

TEXT
TEXT
config
config
config
config

Source

257 lines
import React from "react";
import {
  AbsoluteFill,
  Img,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
  staticFile,
  Easing,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";

loadInter("normal", { weights: ["400", "600", "700", "900"] });

interface FrostedGlassCardProps {
  backgroundSrc: string;
  artworkSrc: string;
  title: string;
  subtitle: string;
  trackLabel?: string; // e.g. "NOW PLAYING"
  // tint colour overlay on background (helps readability)
  tintColor?: string;
}

export const FrostedGlassCard: React.FC<FrostedGlassCardProps> = ({
  backgroundSrc,
  artworkSrc,
  title,
  subtitle,
  trackLabel = "NOW PLAYING",
  tintColor = "rgba(15, 10, 30, 0.35)",
}) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames, width, height } = useVideoConfig();

  // Card entrance spring
  const cardSpring = spring({
    frame,
    fps,
    config: { mass: 0.9, damping: 14, stiffness: 120 },
  });
  const cardY = interpolate(cardSpring, [0, 1], [160, 0]);
  const cardOpacity = interpolate(frame, [0, 15], [0, 1], {
    extrapolateRight: "clamp",
  });
  const cardScale = interpolate(cardSpring, [0, 1], [0.94, 1]);

  // Album art breathing
  const breathT = (frame / fps) * 0.7;
  const artScale = 1 + Math.sin(breathT * Math.PI * 2) * 0.012;

  // Background zoom (Ken Burns)
  const bgProgress = frame / Math.max(durationInFrames, 1);
  const bgScale = interpolate(bgProgress, [0, 1], [1.08, 1.15]);

  // Specular sweep highlight — moves across the card
  const sweepCycle = (frame / fps) % 3.5;
  const sweepT = Math.min(1, sweepCycle / 1.2);
  const sweepX = interpolate(sweepT, [0, 1], [-40, 140]);
  const sweepOpacity = sweepCycle < 1.2 ? interpolate(sweepT, [0, 0.5, 1], [0, 0.6, 0]) : 0;

  // Progress bar on the card
  const barProgress = (frame / fps) % 8;
  const barFill = interpolate(barProgress, [0, 8], [0.15, 0.85]);

  // Card layout
  const cardW = 900;
  const cardH = 280;
  const cardX = (width - cardW) / 2;
  const cardTop = height - cardH - 260;

  return (
    <AbsoluteFill style={{ backgroundColor: "#0a0612" }}>
      {/* Background image with Ken Burns + dark tint */}
      <div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
        <Img
          src={staticFile(backgroundSrc)}
          style={{
            width: "100%",
            height: "100%",
            objectFit: "cover",
            transform: `scale(${bgScale})`,
            // Heavier blur + desaturation to mimic a frosted-glass FEEL over the
            // whole background — backdrop-filter is unreliable in headless
            // Chrome, so we bake the blur into the background itself.
            filter: "saturate(1.2) brightness(0.55) blur(28px)",
          }}
        />
        <div style={{ position: "absolute", inset: 0, background: tintColor }} />
        {/* Subtle vignette */}
        <div
          style={{
            position: "absolute",
            inset: 0,
            background:
              "radial-gradient(ellipse at center, transparent 35%, rgba(0,0,0,0.65) 100%)",
          }}
        />
      </div>

      {/* Floating label above card */}
      <div
        style={{
          position: "absolute",
          top: cardTop - 80,
          left: 0,
          right: 0,
          textAlign: "center",
          fontFamily: "'Inter', sans-serif",
          fontSize: 28,
          fontWeight: 700,
          letterSpacing: 8,
          color: "rgba(255,255,255,0.85)",
          textShadow: "0 2px 20px rgba(0,0,0,0.6)",
          opacity: interpolate(frame, [5, 20], [0, 1], { extrapolateRight: "clamp" }),
          transform: `translateY(${interpolate(
            frame,
            [5, 25],
            [20, 0],
            { extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }
          )}px)`,
        }}
      >
        {trackLabel}
      </div>

      {/* Frosted glass card */}
      <div
        style={{
          position: "absolute",
          left: cardX,
          top: cardTop,
          width: cardW,
          height: cardH,
          borderRadius: 32,
          overflow: "hidden",
          opacity: cardOpacity,
          transform: `translateY(${cardY}px) scale(${cardScale})`,
          background: "rgba(255, 255, 255, 0.12)",
          backdropFilter: "blur(40px) saturate(180%)",
          WebkitBackdropFilter: "blur(40px) saturate(180%)",
          border: "1px solid rgba(255, 255, 255, 0.25)",
          boxShadow:
            "0 30px 80px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255,255,255,0.3), inset 0 -1px 0 rgba(0,0,0,0.1)",
          display: "flex",
          alignItems: "center",
          padding: 28,
          gap: 28,
        }}
      >
        {/* Specular edge highlight along top */}
        <div
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            right: 0,
            height: 3,
            background:
              "linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent)",
            pointerEvents: "none",
          }}
        />
        {/* Sweep highlight */}
        <div
          style={{
            position: "absolute",
            inset: 0,
            background: `linear-gradient(110deg, transparent ${sweepX - 15}%, rgba(255,255,255,0.35) ${sweepX}%, transparent ${sweepX + 15}%)`,
            opacity: sweepOpacity,
            pointerEvents: "none",
            mixBlendMode: "overlay",
          }}
        />

        {/* Album art */}
        <div
          style={{
            width: 224,
            height: 224,
            borderRadius: 20,
            overflow: "hidden",
            flexShrink: 0,
            boxShadow: "0 12px 40px rgba(0,0,0,0.5)",
            transform: `scale(${artScale})`,
          }}
        >
          <Img
            src={staticFile(artworkSrc)}
            style={{ width: "100%", height: "100%", objectFit: "cover" }}
          />
        </div>

        {/* Text + progress */}
        <div style={{ flex: 1, color: "#fff", fontFamily: "'Inter', sans-serif" }}>
          <div
            style={{
              fontSize: 46,
              fontWeight: 900,
              letterSpacing: -1,
              lineHeight: 1.05,
              marginBottom: 10,
              textShadow: "0 2px 20px rgba(0,0,0,0.4)",
            }}
          >
            {title}
          </div>
          <div
            style={{
              fontSize: 28,
              fontWeight: 500,
              opacity: 0.82,
              marginBottom: 24,
              textShadow: "0 1px 10px rgba(0,0,0,0.3)",
            }}
          >
            {subtitle}
          </div>
          {/* Progress bar */}
          <div
            style={{
              width: "100%",
              height: 6,
              background: "rgba(255,255,255,0.22)",
              borderRadius: 4,
              overflow: "hidden",
            }}
          >
            <div
              style={{
                width: `${barFill * 100}%`,
                height: "100%",
                background: "#fff",
                borderRadius: 4,
                boxShadow: "0 0 10px rgba(255,255,255,0.5)",
              }}
            />
          </div>
          <div
            style={{
              display: "flex",
              justifyContent: "space-between",
              marginTop: 10,
              fontSize: 18,
              opacity: 0.7,
            }}
          >
            <span>1:42</span>
            <span>3:18</span>
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: FrostedGlassCard
// 6 props detected · 2 port(s)

Frosted Glass Card

frosted_glass_card

Frosted Glass Card — Andy's hand-crafted Remotion scene. 6 props. title:TEXT, subtitle:TEXT.

Workflow Inputs (2)

  • Title
    title
    TEXTRequired
  • Subtitle
    subtitle
    TEXTRequired

Config Fields (4)

  • tintColor
  • artworkSrc
  • trackLabel
  • backgroundSrc

Meta

Updated
4/21/2026