Add Remotion promo video project with 7-scene App Store flow
- Create feels-promo Remotion project for promotional videos - Implement FeelsPromoV1 with scenes matching App Store screenshots: - Hero scene with mood tracking - Widget + Apple Watch scene - Journal notes with photos - AI-powered insights with badge - Privacy & security features - Theme customization - Notification styles - Add screens folder with source assets and flow reference - Include phone frames, widget, and watch frame assets Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
55
feels-promo/src/HelloWorld/Arc.tsx
Normal file
55
feels-promo/src/HelloWorld/Arc.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useState } from "react";
|
||||
import { random, useVideoConfig } from "remotion";
|
||||
|
||||
const getCircumferenceOfArc = (rx: number, ry: number) => {
|
||||
return Math.PI * 2 * Math.sqrt((rx * rx + ry * ry) / 2);
|
||||
};
|
||||
|
||||
const rx = 135;
|
||||
const ry = 300;
|
||||
const cx = 960;
|
||||
const cy = 540;
|
||||
const arcLength = getCircumferenceOfArc(rx, ry);
|
||||
const strokeWidth = 30;
|
||||
|
||||
export const Arc: React.FC<{
|
||||
progress: number;
|
||||
rotation: number;
|
||||
rotateProgress: number;
|
||||
color1: string;
|
||||
color2: string;
|
||||
}> = ({ progress, rotation, rotateProgress, color1, color2 }) => {
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
// Each svg Id must be unique to not conflict with each other
|
||||
const [gradientId] = useState(() => String(random(null)));
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: `rotate(${rotation * rotateProgress}deg)`,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor={color1} />
|
||||
<stop offset="100%" stopColor={color2} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<ellipse
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
rx={rx}
|
||||
ry={ry}
|
||||
fill="none"
|
||||
stroke={`url(#${gradientId})`}
|
||||
strokeDasharray={arcLength}
|
||||
strokeDashoffset={arcLength - arcLength * progress}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
36
feels-promo/src/HelloWorld/Atom.tsx
Normal file
36
feels-promo/src/HelloWorld/Atom.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState } from "react";
|
||||
import { random, useVideoConfig } from "remotion";
|
||||
|
||||
export const Atom: React.FC<{
|
||||
scale: number;
|
||||
color1: string;
|
||||
color2: string;
|
||||
}> = ({ scale, color1, color2 }) => {
|
||||
const config = useVideoConfig();
|
||||
|
||||
// Each SVG ID must be unique to not conflict with each other
|
||||
const [gradientId] = useState(() => String(random(null)));
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${config.width} ${config.height}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor={color1} />
|
||||
<stop offset="100%" stopColor={color2} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle
|
||||
r={70}
|
||||
cx={config.width / 2}
|
||||
cy={config.height / 2}
|
||||
fill={`url(#${gradientId})`}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
87
feels-promo/src/HelloWorld/Logo.tsx
Normal file
87
feels-promo/src/HelloWorld/Logo.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
interpolate,
|
||||
spring,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
} from "remotion";
|
||||
import { Arc } from "./Arc";
|
||||
import { Atom } from "./Atom";
|
||||
import { z } from "zod";
|
||||
import { zColor } from "@remotion/zod-types";
|
||||
|
||||
export const myCompSchema2 = z.object({
|
||||
logoColor1: zColor(),
|
||||
logoColor2: zColor(),
|
||||
});
|
||||
|
||||
export const Logo: React.FC<z.infer<typeof myCompSchema2>> = ({
|
||||
logoColor1: color1,
|
||||
logoColor2: color2,
|
||||
}) => {
|
||||
const videoConfig = useVideoConfig();
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const development = spring({
|
||||
config: {
|
||||
damping: 100,
|
||||
mass: 0.5,
|
||||
},
|
||||
fps: videoConfig.fps,
|
||||
frame,
|
||||
});
|
||||
|
||||
const rotationDevelopment = spring({
|
||||
config: {
|
||||
damping: 100,
|
||||
mass: 0.5,
|
||||
},
|
||||
fps: videoConfig.fps,
|
||||
frame,
|
||||
});
|
||||
|
||||
const scale = spring({
|
||||
frame,
|
||||
config: {
|
||||
mass: 0.5,
|
||||
},
|
||||
fps: videoConfig.fps,
|
||||
});
|
||||
|
||||
const logoRotation = interpolate(
|
||||
frame,
|
||||
[0, videoConfig.durationInFrames],
|
||||
[0, 360],
|
||||
);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
transform: `scale(${scale}) rotate(${logoRotation}deg)`,
|
||||
}}
|
||||
>
|
||||
<Arc
|
||||
rotateProgress={rotationDevelopment}
|
||||
progress={development}
|
||||
rotation={30}
|
||||
color1={color1}
|
||||
color2={color2}
|
||||
/>
|
||||
<Arc
|
||||
rotateProgress={rotationDevelopment}
|
||||
rotation={90}
|
||||
progress={development}
|
||||
color1={color1}
|
||||
color2={color2}
|
||||
/>
|
||||
<Arc
|
||||
rotateProgress={rotationDevelopment}
|
||||
rotation={-30}
|
||||
progress={development}
|
||||
color1={color1}
|
||||
color2={color2}
|
||||
/>
|
||||
<Atom scale={rotationDevelopment} color1={color1} color2={color2} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
26
feels-promo/src/HelloWorld/Subtitle.tsx
Normal file
26
feels-promo/src/HelloWorld/Subtitle.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { interpolate, useCurrentFrame } from "remotion";
|
||||
import { COLOR_1, FONT_FAMILY } from "./constants";
|
||||
|
||||
const subtitle: React.CSSProperties = {
|
||||
fontFamily: FONT_FAMILY,
|
||||
fontSize: 40,
|
||||
textAlign: "center",
|
||||
position: "absolute",
|
||||
bottom: 140,
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
const codeStyle: React.CSSProperties = {
|
||||
color: COLOR_1,
|
||||
};
|
||||
|
||||
export const Subtitle: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const opacity = interpolate(frame, [0, 30], [0, 1]);
|
||||
return (
|
||||
<div style={{ ...subtitle, opacity }}>
|
||||
Edit <code style={codeStyle}>src/Root.tsx</code> and save to reload.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
58
feels-promo/src/HelloWorld/Title.tsx
Normal file
58
feels-promo/src/HelloWorld/Title.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { FONT_FAMILY } from "./constants";
|
||||
|
||||
const title: React.CSSProperties = {
|
||||
fontFamily: FONT_FAMILY,
|
||||
fontWeight: "bold",
|
||||
fontSize: 100,
|
||||
textAlign: "center",
|
||||
position: "absolute",
|
||||
bottom: 160,
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
const word: React.CSSProperties = {
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
display: "inline-block",
|
||||
};
|
||||
|
||||
export const Title: React.FC<{
|
||||
readonly titleText: string;
|
||||
readonly titleColor: string;
|
||||
}> = ({ titleText, titleColor }) => {
|
||||
const videoConfig = useVideoConfig();
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const words = titleText.split(" ");
|
||||
|
||||
return (
|
||||
<h1 style={title}>
|
||||
{words.map((t, i) => {
|
||||
const delay = i * 5;
|
||||
|
||||
const scale = spring({
|
||||
fps: videoConfig.fps,
|
||||
frame: frame - delay,
|
||||
config: {
|
||||
damping: 200,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<span
|
||||
key={t}
|
||||
style={{
|
||||
...word,
|
||||
color: titleColor,
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
5
feels-promo/src/HelloWorld/constants.ts
Normal file
5
feels-promo/src/HelloWorld/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Change any of these to update your video live.
|
||||
|
||||
export const COLOR_1 = "#86A8E7";
|
||||
|
||||
export const FONT_FAMILY = "SF Pro Text, Helvetica, Arial, sans-serif";
|
||||
Reference in New Issue
Block a user