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:
Trey t
2026-01-25 12:47:06 -06:00
parent de994d4d52
commit bf2555f360
39 changed files with 5641 additions and 0 deletions

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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";