Catalog

Edit Props (live)

IMAGE
config
config
config
config
config
config
config
config
config

Source

288 lines
// App Store End Card — the closing hold of an app-preview video.
// App icon → name → tagline → ★ rating → "Download on the App Store"
// badge, each springing/fading in, then held. Designed portrait for the
// App Store (e.g. iPhone 6.9" 1290×2796) but resolution-independent.
//
// Drop a real icon via the `appIcon` IMAGE port; with no icon it renders
// a branded gradient tile with a health pulse mark so the card still reads.

import {
  AbsoluteFill,
  Img,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';
import { loadFont as loadInter } from '@remotion/google-fonts/Inter';
import { loadFont as loadSpaceGrotesk } from '@remotion/google-fonts/SpaceGrotesk';

const { fontFamily: SPACE_GROTESK } = loadSpaceGrotesk();
const { fontFamily: INTER } = loadInter();

export type AppStoreEndCardProps = {
  appIcon: string;
  appName: string;
  tagline: string;
  ctaText: string;
  ratingText: string;
  accentColor: string;
  accentColor2: string;
  backgroundColor: string;
  textColor: string;
  nameFontSize: number;
};

// Apple logo silhouette (viewBox 0 0 384 512).
const APPLE_PATH =
  'M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C32.3 141.2-8.4 184.8-8.4 273.1c0 26.1 4.8 53.1 14.4 80.9 12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.8-89.9-61.8-91.8zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z';

function rise(frame: number, start: number, end: number) {
  return interpolate(frame, [start, end], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
}

export default function AppStoreEndCard({
  appIcon = '',
  appName = 'BasedHealth',
  tagline = 'AI food scanning & workout coaching',
  ctaText = 'Download on the App Store',
  ratingText = '4.9',
  accentColor = '#C44FE0',
  accentColor2 = '#3B4ED9',
  backgroundColor = '#060608',
  textColor = '#FFFFFF',
  nameFontSize = 150,
}: Partial<AppStoreEndCardProps>) {
  const frame = useCurrentFrame();
  const { fps, width } = useVideoConfig();

  // Accept any non-empty src (http(s), data:, staticFile path…).
  const hasIcon = typeof appIcon === 'string' && appIcon.trim().length > 0;

  // Icon size scales to the frame width so the card looks right at any res.
  const iconSize = Math.round(width * 0.24);
  const iconRadius = Math.round(iconSize * 0.225); // iOS-ish squircle radius

  const iconSpring = spring({
    frame,
    fps,
    config: { damping: 13, stiffness: 150, mass: 0.7 },
  });
  const iconFloat = Math.sin(frame / 16) * (width * 0.004);

  const nameIn = rise(frame, 8, 24);
  const taglineIn = rise(frame, 16, 32);
  const starsIn = rise(frame, 24, 40);
  const ctaIn = rise(frame, 34, 52);
  const ctaPulse = 1 + Math.sin(Math.max(0, frame - 52) / 14) * 0.012;

  const stars = '★★★★★';

  return (
    <AbsoluteFill
      style={{
        backgroundColor,
        overflow: 'hidden',
        fontFamily: INTER,
      }}
    >
      {/* Brand gradient glow — echoes the app icon (blue ↘ magenta) */}
      <AbsoluteFill
        style={{
          background: `radial-gradient(46% 38% at 18% 100%, ${accentColor2}3D 0%, transparent 66%), radial-gradient(52% 40% at 84% 100%, ${accentColor}42 0%, transparent 66%)`,
        }}
      />
      {/* Soft glow behind the icon so it pops on black */}
      <AbsoluteFill
        style={{
          background: `radial-gradient(32% 21% at 50% 33%, ${accentColor}22 0%, transparent 70%)`,
        }}
      />

      <AbsoluteFill
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          padding: `0 ${Math.round(width * 0.08)}px`,
          transform: 'translateY(-3%)',
        }}
      >
        {/* ── App icon ── */}
        <div
          style={{
            width: iconSize,
            height: iconSize,
            borderRadius: iconRadius,
            transform: `scale(${iconSpring}) translateY(${iconFloat}px)`,
            overflow: 'hidden',
            boxShadow: `0 ${iconSize * 0.06}px ${
              iconSize * 0.18
            }px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.06) inset`,
            background: `linear-gradient(150deg, ${accentColor2} 0%, #7B3FD9 52%, ${accentColor} 100%)`,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          {hasIcon ? (
            <Img
              src={appIcon as string}
              style={{ width: '100%', height: '100%', objectFit: 'cover' }}
            />
          ) : (
            <svg
              width={iconSize * 0.62}
              height={iconSize * 0.62}
              viewBox='0 0 200 200'
              fill='none'
            >
              <polyline
                points='22,108 70,108 92,52 120,156 146,96 168,108 182,108'
                stroke='#FFFFFF'
                strokeWidth={16}
                strokeLinecap='round'
                strokeLinejoin='round'
                opacity={0.96}
              />
            </svg>
          )}
        </div>

        {/* ── App name ── */}
        <div
          style={{
            marginTop: Math.round(width * 0.06),
            fontFamily: SPACE_GROTESK,
            fontSize: nameFontSize,
            fontWeight: 700,
            color: textColor,
            letterSpacing: '-0.03em',
            lineHeight: 1,
            textAlign: 'center',
            opacity: nameIn,
            transform: `translateY(${(1 - nameIn) * 38}px)`,
          }}
        >
          {appName}
        </div>

        {/* ── Tagline ── */}
        <div
          style={{
            marginTop: Math.round(width * 0.028),
            fontSize: Math.round(nameFontSize * 0.34),
            fontWeight: 500,
            color: textColor,
            opacity: taglineIn * 0.82,
            transform: `translateY(${(1 - taglineIn) * 30}px)`,
            textAlign: 'center',
            letterSpacing: '0.01em',
            maxWidth: '88%',
          }}
        >
          {tagline}
        </div>

        {/* ── Rating ── */}
        {ratingText ? (
          <div
            style={{
              marginTop: Math.round(width * 0.045),
              display: 'flex',
              alignItems: 'center',
              gap: Math.round(width * 0.018),
              opacity: starsIn,
              transform: `translateY(${(1 - starsIn) * 24}px)`,
            }}
          >
            <span
              style={{
                color: accentColor,
                fontSize: Math.round(nameFontSize * 0.3),
                letterSpacing: '0.08em',
              }}
            >
              {stars}
            </span>
            <span
              style={{
                color: textColor,
                opacity: 0.85,
                fontSize: Math.round(nameFontSize * 0.26),
                fontWeight: 600,
              }}
            >
              {ratingText}
            </span>
          </div>
        ) : null}

        {/* ── App Store badge ── */}
        <div
          style={{
            marginTop: Math.round(width * 0.075),
            opacity: ctaIn,
            transform: `translateY(${(1 - ctaIn) * 28}px) scale(${ctaPulse})`,
          }}
        >
          <div
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: Math.round(width * 0.022),
              background: '#000000',
              border: '1.5px solid rgba(255,255,255,0.22)',
              borderRadius: Math.round(width * 0.026),
              padding: `${Math.round(width * 0.026)}px ${Math.round(
                width * 0.05
              )}px`,
            }}
          >
            <svg
              width={Math.round(nameFontSize * 0.5)}
              height={Math.round(nameFontSize * 0.5)}
              viewBox='0 0 384 512'
              fill='#FFFFFF'
            >
              <path d={APPLE_PATH} />
            </svg>
            <div
              style={{
                display: 'flex',
                flexDirection: 'column',
                lineHeight: 1.05,
              }}
            >
              <span
                style={{
                  color: '#FFFFFF',
                  fontSize: Math.round(nameFontSize * 0.2),
                  fontWeight: 500,
                  opacity: 0.9,
                }}
              >
                Download on the
              </span>
              <span
                style={{
                  color: '#FFFFFF',
                  fontSize: Math.round(nameFontSize * 0.4),
                  fontWeight: 600,
                  letterSpacing: '-0.01em',
                }}
              >
                {ctaText.replace(/^Download on the\s*/i, '') || 'App Store'}
              </span>
            </div>
          </div>
        </div>
      </AbsoluteFill>
    </AbsoluteFill>
  );
}

Schema (Zod)

import { z } from 'zod';

export const Schema = z.object({
  appIcon: z
    .string()
    .default('https://www.wireflow.ai/brand/basedhealth-icon.png')
    .describe(
      '$port:image App icon (square PNG). Falls back to a branded tile.'
    ),

  appName: z.string().default('BasedHealth').describe('App name'),
  tagline: z
    .string()
    .default('AI food scanning & workout coaching')
    .describe('One-line tagline'),
  ctaText: z
    .string()
    .default('Download on the App Store')
    .describe('Call to action / store badge label'),
  ratingText: z
    .string()
    .default('4.9')
    .describe('Rating shown next to the stars (blank to hide)'),

  accentColor: z
    .string()
    .default('#C44FE0')
    .describe('$style:color Accent / brand color (stars, glow)'),
  accentColor2: z
    .string()
    .default('#3B4ED9')
    .describe('$style:color Secondary brand color (gradient glow)'),
  backgroundColor: z
    .string()
    .default('#060608')
    .describe('$style:color Background color'),
  textColor: z.string().default('#FFFFFF').describe('$style:color Text color'),
  nameFontSize: z
    .number()
    .default(150)
    .describe('$style:size App name font size (other type scales off this)'),
});

App Store End Card

app_store_end_card

Closing hold for an app-preview video: app icon, name, tagline, ★ rating and a "Download on the App Store" badge. Portrait, springs in then holds. Drop a real icon via the IMAGE port.

Workflow Inputs (1)

  • App icon (square PNG). Falls back to a branded tile.
    appIcon
    IMAGE

Config Fields (9)

  • appName
  • ctaText
  • tagline
  • textColor
  • ratingText
  • accentColor
  • accentColor2
  • nameFontSize
  • backgroundColor

Meta

Updated
7/10/2026