Add promo components and screenshot images

- Add LiveActivityAnimation, LiveActivityCard, BackgroundStill components
- Update Root.tsx composition
- Add widget and voting screenshot images

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-31 09:02:24 -06:00
parent 70451804ba
commit 6a4b86740a
25 changed files with 623 additions and 5 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -0,0 +1,52 @@
import { AbsoluteFill, Img, staticFile } from "remotion";
// Static version of the tiled background (no animation)
export const BackgroundStill: React.FC = () => {
const width = 1080;
const height = 1920;
const iconSize = 80;
const gap = 40;
const cellSize = iconSize + gap;
const cols = Math.ceil(width / cellSize) + 4;
const rows = Math.ceil(height / cellSize) + 4;
return (
<AbsoluteFill
style={{
background: "linear-gradient(180deg, #f59e0b 0%, #ef4444 100%)",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
top: -cellSize * 2,
left: -cellSize * 2,
}}
>
{[...Array(rows)].map((_, row) =>
[...Array(cols)].map((_, col) => {
const staggerX = row % 2 === 0 ? 0 : cellSize / 2;
return (
<Img
key={`${row}-${col}`}
src={staticFile("app-icon.png")}
style={{
position: "absolute",
width: iconSize,
height: iconSize,
left: col * cellSize + staggerX,
top: row * cellSize,
opacity: 0.08,
borderRadius: iconSize * 0.22,
}}
/>
);
})
)}
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,280 @@
import React from "react";
import {
AbsoluteFill,
interpolate,
useCurrentFrame,
useVideoConfig,
spring,
Easing,
} from "remotion";
// Mood colors matching the app
const MOOD_COLORS = {
horrible: "#F44336",
bad: "#FF9800",
average: "#FFC107",
good: "#8BC34A",
great: "#4CAF50",
};
// Get mood based on progress
const getMoodForProgress = (progress: number): { name: string; color: string } => {
if (progress < 0.2) return { name: "Horrible", color: MOOD_COLORS.horrible };
if (progress < 0.4) return { name: "Bad", color: MOOD_COLORS.bad };
if (progress < 0.6) return { name: "Average", color: MOOD_COLORS.average };
if (progress < 0.8) return { name: "Good", color: MOOD_COLORS.good };
return { name: "Great", color: MOOD_COLORS.great };
};
// Flame SVG icon
const FlameIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill={color}
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 23C16.1421 23 19.5 19.6421 19.5 15.5C19.5 14.1183 19.1425 12.8052 18.5 11.6447C18.5 11.6447 18 12.5 17 12.5C17 12.5 18 9.5 16 6C14.5 7.5 13.5 8 12.5 8C12.5 8 13.5 5 12 2C10.5 4 9.5 5 8 6.5C6.5 8 5 10 5 12.5C5 12.5 4.5 12 4 11.5C4 11.5 3.5 13 3.5 14.5C3.5 19.1944 7.30558 23 12 23ZM12 20C9.79086 20 8 18.2091 8 16C8 15.3504 8.15822 14.7369 8.43721 14.1967C8.43721 14.1967 9 15 10 15C10 15 9 13 10 11C10.75 11.75 11.25 12 11.75 12C11.75 12 11.25 10.5 12 9C12.75 10 13.25 10.5 14 11.25C14.75 12 15.5 13 15.5 14.5C15.5 14.5 16 14 16.25 13.75C16.25 13.75 16.5 14.5 16.5 15.25C16.5 17.8734 14.5 20 12 20Z" />
</svg>
);
export const LiveActivityAnimation: React.FC = () => {
const frame = useCurrentFrame();
const { fps, durationInFrames, width, height } = useVideoConfig();
const targetStreak = 365;
// Animation timing
const animationStartFrame = fps * 1; // Start after 1 second
const animationEndFrame = durationInFrames - fps * 1; // End 1 second before end
const animationDuration = animationEndFrame - animationStartFrame;
// Calculate current streak with easing
const rawProgress = interpolate(
frame,
[animationStartFrame, animationEndFrame],
[0, 1],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
}
);
const currentStreak = Math.round(rawProgress * targetStreak);
const progressPercent = currentStreak / targetStreak;
const mood = getMoodForProgress(progressPercent);
// Spring animation for the flame icon
const flameScale = spring({
frame: frame % 15, // Pulse every 15 frames
fps,
config: {
damping: 10,
stiffness: 200,
mass: 0.5,
},
});
const flameScaleValue = interpolate(flameScale, [0, 1], [1, 1.1]);
// Fade in animation
const fadeIn = interpolate(frame, [0, fps * 0.5], [0, 1], {
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
backgroundColor: "transparent",
justifyContent: "center",
alignItems: "center",
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
opacity: fadeIn,
}}
>
{/* Live Activity Card */}
<div
style={{
width: width * 0.9,
backgroundColor: "rgba(44, 44, 46, 0.95)",
borderRadius: 24,
padding: 24,
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.4)",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 24,
}}
>
{/* Streak Indicator */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 8,
}}
>
<div style={{ transform: `scale(${flameScaleValue})` }}>
<FlameIcon size={64} color="#FF9500" />
</div>
<div
style={{
fontSize: 56,
fontWeight: 700,
color: "#FFFFFF",
lineHeight: 1,
}}
>
{currentStreak}
</div>
<div
style={{
fontSize: 18,
color: "rgba(255, 255, 255, 0.6)",
}}
>
day streak
</div>
</div>
{/* Divider */}
<div
style={{
width: 1,
height: 100,
backgroundColor: "rgba(255, 255, 255, 0.2)",
}}
/>
{/* Status Section */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 12,
flex: 1,
}}
>
{currentStreak > 0 ? (
<>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 12,
}}
>
{/* Mood Color Circle */}
<div
style={{
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: mood.color,
boxShadow: `0 0 20px ${mood.color}80`,
}}
/>
<div style={{ display: "flex", flexDirection: "column" }}>
<div
style={{
fontSize: 16,
color: "rgba(255, 255, 255, 0.6)",
}}
>
Today's mood
</div>
<div
style={{
fontSize: 24,
fontWeight: 600,
color: "#FFFFFF",
}}
>
{mood.name}
</div>
</div>
</div>
</>
) : (
<div style={{ display: "flex", flexDirection: "column" }}>
<div
style={{
fontSize: 24,
fontWeight: 600,
color: "#FFFFFF",
}}
>
Start your streak!
</div>
<div
style={{
fontSize: 16,
color: "rgba(255, 255, 255, 0.6)",
}}
>
Tap to log your mood
</div>
</div>
)}
</div>
</div>
{/* Progress Bar Section */}
<div
style={{
marginTop: 24,
display: "flex",
flexDirection: "column",
gap: 12,
}}
>
{/* Progress Bar */}
<div
style={{
width: "100%",
height: 12,
backgroundColor: "rgba(255, 255, 255, 0.1)",
borderRadius: 6,
overflow: "hidden",
}}
>
<div
style={{
width: `${progressPercent * 100}%`,
height: "100%",
background: `linear-gradient(90deg, ${MOOD_COLORS.horrible}, ${MOOD_COLORS.bad}, ${MOOD_COLORS.average}, ${MOOD_COLORS.good}, ${MOOD_COLORS.great})`,
borderRadius: 6,
transition: "width 0.1s ease-out",
}}
/>
</div>
{/* Progress Label */}
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: 18,
color: "rgba(255, 255, 255, 0.6)",
}}
>
<span>0</span>
<span style={{ color: "#FFFFFF", fontWeight: 600 }}>
{currentStreak} / {targetStreak} days
</span>
<span>{targetStreak}</span>
</div>
</div>
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,261 @@
import { interpolate, useCurrentFrame, useVideoConfig, spring, Easing } from "remotion";
// Mood colors matching the app
const MOOD_COLORS = {
horrible: "#F44336",
bad: "#FF9800",
average: "#FFC107",
good: "#8BC34A",
great: "#4CAF50",
};
// Get mood based on progress
const getMoodForProgress = (progress: number): { name: string; color: string } => {
if (progress < 0.2) return { name: "Horrible", color: MOOD_COLORS.horrible };
if (progress < 0.4) return { name: "Bad", color: MOOD_COLORS.bad };
if (progress < 0.6) return { name: "Average", color: MOOD_COLORS.average };
if (progress < 0.8) return { name: "Good", color: MOOD_COLORS.good };
return { name: "Great", color: MOOD_COLORS.great };
};
// Flame SVG icon
const FlameIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill={color}
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 23C16.1421 23 19.5 19.6421 19.5 15.5C19.5 14.1183 19.1425 12.8052 18.5 11.6447C18.5 11.6447 18 12.5 17 12.5C17 12.5 18 9.5 16 6C14.5 7.5 13.5 8 12.5 8C12.5 8 13.5 5 12 2C10.5 4 9.5 5 8 6.5C6.5 8 5 10 5 12.5C5 12.5 4.5 12 4 11.5C4 11.5 3.5 13 3.5 14.5C3.5 19.1944 7.30558 23 12 23ZM12 20C9.79086 20 8 18.2091 8 16C8 15.3504 8.15822 14.7369 8.43721 14.1967C8.43721 14.1967 9 15 10 15C10 15 9 13 10 11C10.75 11.75 11.25 12 11.75 12C11.75 12 11.25 10.5 12 9C12.75 10 13.25 10.5 14 11.25C14.75 12 15.5 13 15.5 14.5C15.5 14.5 16 14 16.25 13.75C16.25 13.75 16.5 14.5 16.5 15.25C16.5 17.8734 14.5 20 12 20Z" />
</svg>
);
interface LiveActivityCardProps {
width: number;
targetStreak?: number;
animationSpeed?: number; // multiplier for animation speed
showProgressBar?: boolean;
}
export const LiveActivityCard: React.FC<LiveActivityCardProps> = ({
width,
targetStreak = 365,
animationSpeed = 1,
showProgressBar = false,
}) => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
// Animation timing - use full scene duration
const animationStartFrame = Math.round(fps * 0.3); // Start after 0.3 seconds
const animationEndFrame = durationInFrames - Math.round(fps * 0.2);
// Calculate current streak with easing
const rawProgress = interpolate(
frame * animationSpeed,
[animationStartFrame, animationEndFrame],
[0, 1],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
}
);
const currentStreak = Math.round(rawProgress * targetStreak);
const progressPercent = currentStreak / targetStreak;
const mood = getMoodForProgress(progressPercent);
// Spring animation for the flame icon
const flameScale = spring({
frame: frame % 15,
fps,
config: {
damping: 10,
stiffness: 200,
mass: 0.5,
},
});
const flameScaleValue = interpolate(flameScale, [0, 1], [1, 1.1]);
return (
<div
style={{
width: width,
backgroundColor: "rgba(44, 44, 46, 0.95)",
borderRadius: 24,
padding: 24,
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.4)",
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 24,
}}
>
{/* Streak Indicator */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 8,
}}
>
<div style={{ transform: `scale(${flameScaleValue})` }}>
<FlameIcon size={64} color="#FF9500" />
</div>
<div
style={{
fontSize: 56,
fontWeight: 700,
color: "#FFFFFF",
lineHeight: 1,
}}
>
{currentStreak}
</div>
<div
style={{
fontSize: 18,
color: "rgba(255, 255, 255, 0.6)",
}}
>
day streak
</div>
</div>
{/* Divider */}
<div
style={{
width: 1,
height: 100,
backgroundColor: "rgba(255, 255, 255, 0.2)",
}}
/>
{/* Status Section */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 12,
flex: 1,
}}
>
{currentStreak > 0 ? (
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 12,
}}
>
{/* Mood Color Circle */}
<div
style={{
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: mood.color,
boxShadow: `0 0 20px ${mood.color}80`,
}}
/>
<div style={{ display: "flex", flexDirection: "column" }}>
<div
style={{
fontSize: 16,
color: "rgba(255, 255, 255, 0.6)",
}}
>
Today's mood
</div>
<div
style={{
fontSize: 24,
fontWeight: 600,
color: "#FFFFFF",
}}
>
{mood.name}
</div>
</div>
</div>
) : (
<div style={{ display: "flex", flexDirection: "column" }}>
<div
style={{
fontSize: 24,
fontWeight: 600,
color: "#FFFFFF",
}}
>
Start your streak!
</div>
<div
style={{
fontSize: 16,
color: "rgba(255, 255, 255, 0.6)",
}}
>
Tap to log your mood
</div>
</div>
)}
</div>
</div>
{/* Progress Bar Section */}
{showProgressBar && (
<div
style={{
marginTop: 24,
display: "flex",
flexDirection: "column",
gap: 12,
}}
>
<div
style={{
width: "100%",
height: 12,
backgroundColor: "rgba(255, 255, 255, 0.1)",
borderRadius: 6,
overflow: "hidden",
}}
>
<div
style={{
width: `${progressPercent * 100}%`,
height: "100%",
background: `linear-gradient(90deg, ${MOOD_COLORS.horrible}, ${MOOD_COLORS.bad}, ${MOOD_COLORS.average}, ${MOOD_COLORS.good}, ${MOOD_COLORS.great})`,
borderRadius: 6,
}}
/>
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: 18,
color: "rgba(255, 255, 255, 0.6)",
}}
>
<span>0</span>
<span style={{ color: "#FFFFFF", fontWeight: 600 }}>
{currentStreak} / {targetStreak} days
</span>
<span>{targetStreak}</span>
</div>
</div>
)}
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { Composition } from "remotion";
import { Composition, Still } from "remotion";
import { FeelsPromoV1 } from "./FeelsPromo";
import { ConceptASelfAwareness } from "./ConceptA-SelfAwareness";
import { ConceptBNoJournalJournal } from "./ConceptB-NoJournalJournal";
@@ -13,6 +13,9 @@ import { ConceptIRetroArcade } from "./ConceptI-RetroArcade";
import { ConceptJConspiracy } from "./ConceptJ-Conspiracy";
import { ConceptKSportsCenter } from "./ConceptK-SportsCenter";
import { ConceptLMusical } from "./ConceptL-Musical";
// Utility animations
import { LiveActivityAnimation } from "./LiveActivityAnimation";
import { BackgroundStill } from "./BackgroundStill";
export const RemotionRoot: React.FC = () => {
const fps = 30;
@@ -52,11 +55,11 @@ export const RemotionRoot: React.FC = () => {
height={1920}
/>
{/* Concept B: The No-Journal Journal (20s) */}
{/* Concept B: The No-Journal Journal (15s) */}
<Composition
id="ConceptB-NoJournalJournal"
component={ConceptBNoJournalJournal}
durationInFrames={Math.round(20 * fps)}
durationInFrames={Math.round(15 * fps)}
fps={fps}
width={1080}
height={1920}
@@ -102,11 +105,11 @@ export const RemotionRoot: React.FC = () => {
height={1920}
/>
{/* Concept G: The Streak Effect (20s) */}
{/* Concept G: The Streak Effect (12s) */}
<Composition
id="ConceptG-StreakEffect"
component={ConceptGStreakEffect}
durationInFrames={Math.round(20 * fps)}
durationInFrames={Math.round(12 * fps)}
fps={fps}
width={1080}
height={1920}
@@ -165,6 +168,28 @@ export const RemotionRoot: React.FC = () => {
width={1080}
height={1920}
/>
{/* ═══════════════════════════════════════════════════════════════
UTILITY ANIMATIONS
═══════════════════════════════════════════════════════════════ */}
{/* Live Activity Preview - Streak 0 to 365 animation (12s) */}
<Composition
id="LiveActivityAnimation"
component={LiveActivityAnimation}
durationInFrames={Math.round(12 * fps)}
fps={fps}
width={1080}
height={1920}
/>
{/* Background Still for export */}
<Still
id="BackgroundStill"
component={BackgroundStill}
width={1080}
height={1920}
/>
</>
);
};

BIN
screens/ai_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

BIN
screens/aj_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

BIN
screens/insights_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

BIN
screens/insights_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
screens/voting_header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
screens/watch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB