Remotion LabRemotion Lab
返回模板庫

App Store 上架卡片

仿 iOS App Store 暗色模式的應用程式展示卡,包含 App 圖示、評分、截圖橫向滑入、評分長條圖動畫填充,呈現完整上架頁面的動態效果。

社群商務簡約
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import React from "react";

const APP-NAME = "SuperApp";
const DEVELOPER = "Dev Studio Inc.";
const RATING = "4.8";
const RATING-COUNT = "12.3k 評分";
const CATEGORY-RANK = "#1 生產力";
const DESCRIPTION =
  "SuperApp 是最強大的生產力工具,幫您整合所有工作流程。支援 iOS 17+ 及 iPadOS。";
const VERSION = "3.2.1";
const SIZE = "142 MB";

const RATING-BARS = [
  { stars: 5, fill: 0.78 },
  { stars: 4, fill: 0.12 },
  { stars: 3, fill: 0.05 },
  { stars: 2, fill: 0.03 },
  { stars: 1, fill: 0.02 },
];

const SCREENSHOT-GRADIENTS = [
  "linear-gradient(160deg, #5e35b1, #7e57c2, #26c6da)",
  "linear-gradient(160deg, #1565c0, #42a5f5, #80cbc4)",
  "linear-gradient(160deg, #2e7d32, #66bb6a, #fff176)",
];

export const AppStoreCard: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Card: slide up + fade, frames 0-20
  const cardProgress = spring({
    frame,
    fps,
    config: { damping: 22, stiffness: 140 },
    durationInFrames: 20,
  });
  const cardY = interpolate(cardProgress, [0, 1], [60, 0]);
  const cardOpacity = interpolate(cardProgress, [0, 0.3], [0, 1], {
    extrapolateRight: "clamp",
  });

  // App icon: scale bounce, frames 10-28
  const iconProgress = spring({
    frame: frame - 10,
    fps,
    config: { damping: 14, stiffness: 200 },
    durationInFrames: 18,
  });
  const iconScale = interpolate(iconProgress, [0, 0.7, 1], [0, 1.12, 1], {
    extrapolateRight: "clamp",
  });

  // Name + rating: slide from top, frames 18-32
  const nameProgress = spring({
    frame: frame - 18,
    fps,
    config: { damping: 22, stiffness: 160 },
    durationInFrames: 14,
  });
  const nameY = interpolate(nameProgress, [0, 1], [-20, 0]);
  const nameOpacity = interpolate(nameProgress, [0, 0.4], [0, 1], {
    extrapolateRight: "clamp",
  });

  // GET button: scale pop, frames 28-40
  const btnProgress = spring({
    frame: frame - 28,
    fps,
    config: { damping: 16, stiffness: 220 },
    durationInFrames: 12,
  });
  const btnScale = interpolate(btnProgress, [0, 0.7, 1], [0, 1.1, 1], {
    extrapolateRight: "clamp",
  });
  const btnOpacity = interpolate(btnProgress, [0, 0.3], [0, 1], {
    extrapolateRight: "clamp",
  });

  // Screenshots: slide in from right one by one, frames 35-70
  const screenshotTranslates = SCREENSHOT-GRADIENTS.map((_, i) => {
    const p = spring({
      frame: frame - (35 + i * 12),
      fps,
      config: { damping: 22, stiffness: 150 },
      durationInFrames: 16,
    });
    const x = interpolate(p, [0, 1], [120, 0]);
    const opacity = interpolate(p, [0, 0.4], [0, 1], {
      extrapolateRight: "clamp",
    });
    return { x, opacity };
  });

  // Description: fade in, frames 55-72
  const descOpacity = interpolate(frame, [55, 72], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Rating bars: fill left to right, frames 65-95
  const barFill = interpolate(frame, [65, 95], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <AbsoluteFill
      style={{
        background: "#0f0f0f",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      {/* Card */}
      <div
        style={{
          transform: `translateY(${cardY}px)`,
          opacity: cardOpacity,
          background: "#1c1c1e",
          borderRadius: 16,
          padding: "32px 36px",
          width: 880,
          boxSizing: "border-box",
          fontFamily: "sans-serif",
          boxShadow: "0 32px 80px rgba(0,0,0,0.8)",
        }}
      >
        {/* Top section: icon + info + GET button */}
        <div
          style={{
            display: "flex",
            alignItems: "flex-start",
            gap: 20,
            marginBottom: 16,
          }}
        >
          {/* App icon */}
          <div
            style={{
              transform: `scale(${iconScale})`,
              width: 100,
              height: 100,
              borderRadius: 22,
              background: "linear-gradient(135deg, #7b2ff7, #00d4ff)",
              flexShrink: 0,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              fontSize: 44,
              boxShadow: "0 8px 24px rgba(123,47,247,0.4)",
            }}
          >

          </div>

          {/* Name + developer + rating */}
          <div
            style={{
              flex: 1,
              transform: `translateY(${nameY}px)`,
              opacity: nameOpacity,
            }}
          >
            <div
              style={{
                fontSize: 26,
                fontWeight: 700,
                color: "#f2f2f7",
                marginBottom: 4,
              }}
            >
              {APP-NAME}
            </div>
            <div
              style={{ fontSize: 15, color: "#8e8e93", marginBottom: 8 }}
            >
              {DEVELOPER}
            </div>
            <div
              style={{
                display: "flex",
                alignItems: "center",
                gap: 8,
              }}
            >
              <div style={{ display: "flex", gap: 2 }}>
                {[1, 2, 3, 4, 5].map((s) => (
                  <span key={s} style={{ fontSize: 16, color: "#ff9f0a" }}>

                  </span>
                ))}
              </div>
              <span style={{ fontSize: 15, fontWeight: 700, color: "#f2f2f7" }}>
                {RATING}
              </span>
              <span style={{ fontSize: 13, color: "#8e8e93" }}>
                ({RATING-COUNT})
              </span>
            </div>
          </div>

          {/* GET button + Category */}
          <div
            style={{
              display: "flex",
              flexDirection: "column",
              alignItems: "flex-end",
              gap: 10,
              paddingTop: 6,
            }}
          >
            <div
              style={{
                transform: `scale(${btnScale})`,
                opacity: btnOpacity,
                background: "#0a84ff",
                borderRadius: 20,
                padding: "8px 24px",
                fontSize: 17,
                fontWeight: 700,
                color: "#ffffff",
                cursor: "pointer",
              }}
            >
              取得
            </div>
            <div
              style={{
                background: "#2c2c2e",
                borderRadius: 8,
                padding: "4px 10px",
                fontSize: 12,
                color: "#8e8e93",
                fontWeight: 600,
                opacity: btnOpacity,
              }}
            >
              {CATEGORY-RANK}
            </div>
          </div>
        </div>

        {/* Divider */}
        <div
          style={{
            height: 1,
            background: "#2c2c2e",
            marginBottom: 20,
          }}
        />

        {/* Screenshots row */}
        <div
          style={{
            display: "flex",
            gap: 12,
            marginBottom: 24,
            overflow: "hidden",
          }}
        >
          {SCREENSHOT-GRADIENTS.map((gradient, i) => (
            <div
              key={i}
              style={{
                transform: `translateX(${screenshotTranslates[i].x}px)`,
                opacity: screenshotTranslates[i].opacity,
                width: 190,
                height: 340,
                borderRadius: 14,
                background: gradient,
                flexShrink: 0,
                display: "flex",
                alignItems: "flex-end",
                justifyContent: "flex-start",
                padding: "16px",
                boxSizing: "border-box",
                overflow: "hidden",
                position: "relative",
              }}
            >
              <div
                style={{
                  position: "absolute",
                  top: 20,
                  left: 14,
                  right: 14,
                  display: "flex",
                  flexDirection: "column",
                  gap: 8,
                }}
              >
                <div
                  style={{
                    height: 8,
                    background: "rgba(255,255,255,0.3)",
                    borderRadius: 4,
                    width: "70%",
                  }}
                />
                <div
                  style={{
                    height: 6,
                    background: "rgba(255,255,255,0.2)",
                    borderRadius: 4,
                    width: "90%",
                  }}
                />
                <div
                  style={{
                    height: 6,
                    background: "rgba(255,255,255,0.2)",
                    borderRadius: 4,
                    width: "55%",
                  }}
                />
              </div>
              <div
                style={{
                  position: "absolute",
                  bottom: 16,
                  left: 14,
                  fontSize: 11,
                  color: "rgba(255,255,255,0.7)",
                  fontWeight: 600,
                }}
              >
                截圖 {i + 1}
              </div>
            </div>
          ))}
        </div>

        {/* Description */}
        <div
          style={{
            fontSize: 15,
            color: "#ebebf5",
            lineHeight: 1.6,
            marginBottom: 20,
            opacity: descOpacity,
          }}
        >
          {DESCRIPTION}
        </div>

        {/* Divider */}
        <div
          style={{
            height: 1,
            background: "#2c2c2e",
            marginBottom: 16,
            opacity: barFill > 0 ? 1 : 0,
          }}
        />

        {/* Rating bars */}
        <div
          style={{
            display: "flex",
            gap: 32,
            marginBottom: 20,
            opacity: barFill > 0 ? 1 : 0,
          }}
        >
          {/* Big rating display */}
          <div
            style={{
              display: "flex",
              flexDirection: "column",
              alignItems: "center",
              justifyContent: "center",
              minWidth: 80,
            }}
          >
            <div
              style={{
                fontSize: 52,
                fontWeight: 800,
                color: "#f2f2f7",
                lineHeight: 1,
              }}
            >
              {RATING}
            </div>
            <div style={{ fontSize: 12, color: "#8e8e93", marginTop: 4 }}>
              滿分 5 分
            </div>
          </div>

          {/* Bar chart */}
          <div
            style={{
              flex: 1,
              display: "flex",
              flexDirection: "column",
              gap: 6,
              justifyContent: "center",
            }}
          >
            {RATING-BARS.map(({ stars, fill }) => (
              <div
                key={stars}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: 8,
                }}
              >
                <span
                  style={{
                    fontSize: 12,
                    color: "#8e8e93",
                    width: 8,
                    textAlign: "right",
                    flexShrink: 0,
                  }}
                >
                  {stars}
                </span>
                <span style={{ fontSize: 11, color: "#ff9f0a" }}>★</span>
                <div
                  style={{
                    flex: 1,
                    height: 6,
                    background: "#2c2c2e",
                    borderRadius: 3,
                    overflow: "hidden",
                  }}
                >
                  <div
                    style={{
                      width: `${fill * barFill * 100}%`,
                      height: "100%",
                      background: "#8e8e93",
                      borderRadius: 3,
                    }}
                  />
                </div>
              </div>
            ))}
          </div>
        </div>

        {/* Version info */}
        <div
          style={{
            display: "flex",
            justifyContent: "space-between",
            opacity: barFill,
            paddingTop: 8,
            borderTop: "1px solid #2c2c2e",
          }}
        >
          <span style={{ fontSize: 13, color: "#8e8e93" }}>
            版本 {VERSION} · 更新於昨天
          </span>
          <span style={{ fontSize: 13, color: "#8e8e93" }}>
            大小:{SIZE}
          </span>
        </div>
      </div>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼