Remotion LabRemotion Lab
返回模板庫

Pencil 工具 Logo 動畫介紹

以彈簧動畫呈現 Pencil 設計工具的 Logo 卡片,搭配旋轉星形閃光特效與程式合成音效,形成精緻的品牌開場動畫。

logospringsparkle音效品牌
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  Audio,
  Img,
  Sequence,
  interpolate,
  spring,
  staticFile,
  useCurrentFrame,
  useVideoConfig,
  delayRender,
  continueRender,
} from "remotion";
import { useEffect, useState } from "react";

const colors = {
  background: "#0B0F17",
  text: "#FFFFFF",
  accent: "#4DA3FF",
  dimmed: "rgba(255, 255, 255, 0.6)",
  cardBg: "rgba(255, 255, 255, 0.05)",
  border: "rgba(77, 163, 255, 0.3)",
};

const EX = {
  extrapolateRight: "clamp" as const,
  extrapolateLeft: "clamp" as const,
};

function writeStr(v: DataView, o: number, s: string) {
  for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i));
}

function bufferToWavUrl(ab: AudioBuffer): string {
  const nc = ab.numberOfChannels;
  const sr = ab.sampleRate;
  const ba = nc * 2;
  const dl = ab.length * ba;
  const buf = new ArrayBuffer(44 + dl);
  const v = new DataView(buf);
  writeStr(v, 0, "RIFF");
  v.setUint32(4, 36 + dl, true);
  writeStr(v, 8, "WAVE");
  writeStr(v, 12, "fmt ");
  v.setUint32(16, 16, true);
  v.setUint16(20, 1, true);
  v.setUint16(22, nc, true);
  v.setUint32(24, sr, true);
  v.setUint32(28, sr * ba, true);
  v.setUint16(32, ba, true);
  v.setUint16(34, 16, true);
  writeStr(v, 36, "data");
  v.setUint32(40, dl, true);
  const chs: Float32Array[] = [];
  for (let i = 0; i < nc; i++) chs.push(ab.getChannelData(i));
  let off = 44;
  for (let s = 0; s < ab.length; s++) {
    for (let c = 0; c < nc; c++) {
      const val = Math.max(-1, Math.min(1, chs[c][s]));
      v.setInt16(off, val < 0 ? val * 0x8000 : val * 0x7fff, true);
      off += 2;
    }
  }
  const bytes = new Uint8Array(buf);
  let bin = "";
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
  return "data:audio/wav;base64," + btoa(bin);
}

async function makePop(): Promise<string> {
  const sr = 44100;
  const dur = 0.12;
  const ctx = new OfflineAudioContext(1, Math.ceil(sr * dur), sr);
  const osc = ctx.createOscillator();
  osc.type = "sine";
  osc.frequency.setValueAtTime(600, 0);
  osc.frequency.exponentialRampToValueAtTime(150, dur);
  const g = ctx.createGain();
  g.gain.setValueAtTime(0.5, 0);
  g.gain.exponentialRampToValueAtTime(0.001, dur);
  osc.connect(g).connect(ctx.destination);
  osc.start(0);
  osc.stop(dur);
  return bufferToWavUrl(await ctx.startRendering());
}

async function makeShimmer(): Promise<string> {
  const sr = 44100;
  const dur = 0.08;
  const ctx = new OfflineAudioContext(1, Math.ceil(sr * dur), sr);
  const osc = ctx.createOscillator();
  osc.type = "sine";
  osc.frequency.setValueAtTime(1800, 0);
  osc.frequency.exponentialRampToValueAtTime(800, dur);
  const g = ctx.createGain();
  g.gain.setValueAtTime(0.2, 0);
  g.gain.exponentialRampToValueAtTime(0.001, dur);
  osc.connect(g).connect(ctx.destination);
  osc.start(0);
  osc.stop(dur);
  return bufferToWavUrl(await ctx.startRendering());
}

const Sparkle: React.FC<{ size: number; color: string }> = ({ size, color }) => (
  <svg width={size} height={size} viewBox="0 0 40 40" fill="none">
    <path
      d="M20 2 l3 14 l14 3 l-14 3 l-3 14 l-3 -14 l-14 -3 l14 -3 z"
      fill={color}
    />
  </svg>
);

const sparkles = [
  { x: -120, y: -80, size: 28, delay: 20 },
  { x: 130, y: -60, size: 22, delay: 28 },
  { x: -90, y: 90, size: 18, delay: 35 },
  { x: 110, y: 80, size: 24, delay: 32 },
];

export const PencilIntroScene: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const [audio, setAudio] = useState<{ pop: string; shimmer: string } | null>(null);
  const [handle] = useState(() => delayRender("Generating sound effects"));

  useEffect(() => {
    Promise.all([makePop(), makeShimmer()])
      .then(([pop, shimmer]) => {
        setAudio({ pop, shimmer });
        continueRender(handle);
      })
      .catch(() => continueRender(handle));
  }, [handle]);

  const logoScale = spring({
    frame: Math.max(0, frame - 8),
    fps,
    config: { damping: 10, stiffness: 80 },
  });
  const logoOp = interpolate(frame, [8, 22], [0, 1], EX);
  const floatY = frame > 30 ? Math.sin((frame - 30) * 0.06) * 5 : 0;

  const glowOp =
    frame > 30
      ? 0.12 + Math.sin((frame - 30) * 0.1) * 0.06
      : interpolate(frame, [20, 30], [0, 0.12], EX);

  return (
    <AbsoluteFill
      style={{
        backgroundColor: colors.background,
        fontFamily: "'Noto Sans TC', 'Inter', sans-serif",
        overflow: "hidden",
      }}
    >
      <div
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          transform: `translate(-50%, -50%) scale(${logoScale}) translateY(${floatY}px)`,
          opacity: logoOp,
        }}
      >
        <div
          style={{
            position: "absolute",
            inset: -60,
            borderRadius: "50%",
            background: `radial-gradient(circle, ${colors.accent} 0%, transparent 70%)`,
            opacity: glowOp,
          }}
        />
        <div
          style={{
            width: 200,
            height: 200,
            borderRadius: 32,
            backgroundColor: "#FFFFFF",
            boxShadow: "0 8px 40px rgba(0, 0, 0, 0.4)",
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
          }}
        >
          <Img
            src={staticFile("pencil-logo.jpeg")}
            style={{
              width: 140,
              height: 140,
              objectFit: "contain",
              borderRadius: 16,
            }}
          />
        </div>
        {sparkles.map((sp, i) => {
          const local = frame - sp.delay;
          if (local < 0) return null;
          const sc = spring({
            frame: local,
            fps,
            config: { damping: 8, stiffness: 120 },
          });
          const rotate = local * 1.5;
          const op = interpolate(local, [0, 10], [0, 0.7], EX);
          return (
            <div
              key={i}
              style={{
                position: "absolute",
                left: `calc(50% + ${sp.x}px)`,
                top: `calc(50% + ${sp.y}px)`,
                transform: `translate(-50%, -50%) scale(${sc}) rotate(${rotate}deg)`,
                opacity: op,
              }}
            >
              <Sparkle size={sp.size} color={colors.accent} />
            </div>
          );
        })}
      </div>
      {audio && (
        <Sequence from={8}>
          <Audio src={audio.pop} volume={0.5} />
        </Sequence>
      )}
      {audio &&
        sparkles.map((sp, i) => (
          <Sequence from={sp.delay} key={`sh-${i}`}>
            <Audio src={audio.shimmer} volume={0.3} />
          </Sequence>
        ))}
    </AbsoluteFill>
  );
};

登入後查看完整程式碼