add marketing movies
This commit is contained in:
@@ -21,7 +21,20 @@
|
||||
"Bash( comm -23 /tmp/code_keys.txt /tmp/xcstrings_keys.txt)",
|
||||
"Bash( comm -13 /tmp/code_keys.txt /tmp/xcstrings_keys.txt)",
|
||||
"Bash(xargs cat:*)",
|
||||
"Bash(xcrun simctl:*)"
|
||||
"Bash(xcrun simctl:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(open:*)",
|
||||
"Bash(npx skills:*)",
|
||||
"Bash(npx create-video@latest:*)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(npx remotion render:*)",
|
||||
"Bash(ffmpeg:*)",
|
||||
"Bash(sips:*)",
|
||||
"Bash(unzip:*)",
|
||||
"Bash(plutil:*)",
|
||||
"Bash(done)",
|
||||
"Bash(for:*)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(git commit:*)",
|
||||
|
||||
1
.claude/skills/remotion-best-practices
Symbolic link
1
.claude/skills/remotion-best-practices
Symbolic link
@@ -0,0 +1 @@
|
||||
../../.agents/skills/remotion-best-practices
|
||||
@@ -12,12 +12,12 @@
|
||||
<key>Feels (macOS).xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Feels Watch App.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>FeelsWidgetExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
||||
7
feels-promo/.claude/settings.local.json
Normal file
7
feels-promo/.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebSearch"
|
||||
]
|
||||
}
|
||||
}
|
||||
270
feels-promo/STORYBOARD-V2-CONCEPTS.md
Normal file
270
feels-promo/STORYBOARD-V2-CONCEPTS.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Feels Promo Video - Future Concepts
|
||||
|
||||
This document outlines additional promotional video concepts to target different user segments and marketing channels.
|
||||
|
||||
---
|
||||
|
||||
## Concept A: "30 Seconds to Self-Awareness"
|
||||
|
||||
**Duration**: 30 seconds
|
||||
**Target**: New Year's resolution crowd, mental health awareness campaigns
|
||||
**Tone**: Motivational, aspirational
|
||||
|
||||
### Scene Flow
|
||||
|
||||
```
|
||||
[0-5s] HOOK: "What if understanding yourself took 5 seconds a day?"
|
||||
[5-12s] DEMO: Quick-fire mood logging from widget, watch, app
|
||||
[12-18s] PAYOFF: Calendar filling with colors → Year View heatmap
|
||||
[18-25s] INSIGHTS: AI insight card appearing with personalized message
|
||||
[25-30s] CTA: App icon + "Start today. Understand tomorrow."
|
||||
```
|
||||
|
||||
### Key Visuals
|
||||
- Time-lapse of mood entries accumulating
|
||||
- Split-screen: logging (left) → patterns emerging (right)
|
||||
- Focus on the "aha moment" when patterns become visible
|
||||
|
||||
---
|
||||
|
||||
## Concept B: "The No-Journal Journal"
|
||||
|
||||
**Duration**: 20 seconds
|
||||
**Target**: People who've tried and abandoned journaling apps
|
||||
**Tone**: Relatable, humorous, practical
|
||||
|
||||
### Scene Flow
|
||||
|
||||
```
|
||||
[0-4s] PROBLEM: Text overlay "Journaling is hard" → crossed out
|
||||
[4-8s] SOLUTION: "Feels is easy" → one tap → done animation
|
||||
[8-14s] COMPARISON: Journal app (complex) vs Feels (simple)
|
||||
[14-18s] BENEFIT: Same insights, fraction of effort
|
||||
[18-20s] CTA: "Finally, tracking that sticks"
|
||||
```
|
||||
|
||||
### Key Visuals
|
||||
- Side-by-side comparison (typing vs tapping)
|
||||
- Exaggerated "effort meter" going from high to low
|
||||
- Calendar streak growing effortlessly
|
||||
|
||||
---
|
||||
|
||||
## Concept C: "Your Year in Feelings"
|
||||
|
||||
**Duration**: 15 seconds
|
||||
**Target**: Social media (Instagram Reels, TikTok)
|
||||
**Tone**: Aesthetic, shareable, FOMO-inducing
|
||||
|
||||
### Scene Flow
|
||||
|
||||
```
|
||||
[0-3s] HOOK: Beautiful Year View heatmap filling in
|
||||
[3-8s] ZOOM: Into specific days, showing mood details
|
||||
[8-12s] SHARE: Year-in-review card being generated
|
||||
[12-15s] CTA: "What will YOUR year look like?"
|
||||
```
|
||||
|
||||
### Key Visuals
|
||||
- Satisfying animation of colors appearing
|
||||
- Premium, aesthetic design emphasis
|
||||
- The shareable card as social proof
|
||||
|
||||
---
|
||||
|
||||
## Concept D: "Always There"
|
||||
|
||||
**Duration**: 25 seconds
|
||||
**Target**: Apple ecosystem users
|
||||
**Tone**: Seamless, integrated, premium
|
||||
|
||||
### Scene Flow
|
||||
|
||||
```
|
||||
[0-5s] SCENE 1: Morning - iPhone widget tap while coffee
|
||||
[5-10s] SCENE 2: Midday - Apple Watch glance at work
|
||||
[10-15s] SCENE 3: Evening - Lock Screen Live Activity
|
||||
[15-20s] SCENE 4: Night - Quick note before bed
|
||||
[20-25s] CTA: "Your mood tracker, wherever you are"
|
||||
```
|
||||
|
||||
### Key Visuals
|
||||
- Lifestyle shots (hands, devices in context)
|
||||
- Seamless device transitions
|
||||
- Emphasis on Apple ecosystem integration
|
||||
|
||||
---
|
||||
|
||||
## Concept E: "Make It Yours"
|
||||
|
||||
**Duration**: 20 seconds
|
||||
**Target**: Customization-focused users, Gen Z
|
||||
**Tone**: Expressive, creative, personal
|
||||
|
||||
### Scene Flow
|
||||
|
||||
```
|
||||
[0-5s] RAPID MONTAGE: Different themes flashing by
|
||||
[5-10s] FOCUS: One theme in detail, showing variations
|
||||
[10-15s] ICONS: Different mood icon packs cycling
|
||||
[15-18s] WIDGETS: Custom widgets on home screen
|
||||
[18-20s] CTA: "12 themes. Unlimited you."
|
||||
```
|
||||
|
||||
### Key Visuals
|
||||
- Quick cuts between dramatically different looks
|
||||
- Color explosions during theme transitions
|
||||
- Focus on personality expression
|
||||
|
||||
---
|
||||
|
||||
## Concept F: "Privacy First"
|
||||
|
||||
**Duration**: 15 seconds
|
||||
**Target**: Privacy-conscious users
|
||||
**Tone**: Trustworthy, secure, Apple-aligned
|
||||
|
||||
### Scene Flow
|
||||
|
||||
```
|
||||
[0-3s] TEXT: "Your feelings are personal"
|
||||
[3-8s] VISUAL: Data flowing into iCloud (encrypted visual)
|
||||
[8-12s] HEALTH: Apple Health integration badge
|
||||
[12-15s] CTA: "Private. Secure. Yours."
|
||||
```
|
||||
|
||||
### Key Visuals
|
||||
- Shield/lock iconography
|
||||
- iCloud and HealthKit logos
|
||||
- No server, no tracking messaging
|
||||
|
||||
---
|
||||
|
||||
## Concept G: "The Streak Effect"
|
||||
|
||||
**Duration**: 20 seconds
|
||||
**Target**: Gamification lovers, habit builders
|
||||
**Tone**: Motivational, game-like, rewarding
|
||||
|
||||
### Scene Flow
|
||||
|
||||
```
|
||||
[0-5s] HOOK: Streak counter at 0 → building up
|
||||
[5-12s] BUILD: Calendar days filling, streak growing
|
||||
[12-16s] CELEBRATE: Milestone celebration animation
|
||||
[16-20s] CTA: "How long can you go?"
|
||||
```
|
||||
|
||||
### Key Visuals
|
||||
- Streak number incrementing dramatically
|
||||
- Celebration particles/confetti
|
||||
- Progress bar filling satisfaction
|
||||
|
||||
---
|
||||
|
||||
## Seasonal Variants
|
||||
|
||||
### New Year (January)
|
||||
**Hook**: "New year, new understanding of yourself"
|
||||
**Visual**: Year View starting fresh, first entry being made
|
||||
**CTA**: "Start your journey January 1st"
|
||||
|
||||
### Mental Health Month (May)
|
||||
**Hook**: "Small check-ins. Big self-awareness."
|
||||
**Visual**: Gentle, soft colors, supportive messaging
|
||||
**CTA**: "Your mental wellness companion"
|
||||
|
||||
### Back to School (August)
|
||||
**Hook**: "Track your journey through the semester"
|
||||
**Visual**: Student-relatable scenarios
|
||||
**CTA**: "The simplest self-care routine"
|
||||
|
||||
### Holiday Season (December)
|
||||
**Hook**: "Reflect on your year. All of it."
|
||||
**Visual**: Year in review, sharing with loved ones
|
||||
**CTA**: "See how far you've come"
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Cuts
|
||||
|
||||
### App Store Preview (30s max)
|
||||
- Full feature showcase
|
||||
- High production value
|
||||
- Sound on expected
|
||||
|
||||
### Instagram Reels (15-30s)
|
||||
- Vertical format
|
||||
- Hook in first 2 seconds
|
||||
- Text overlays (sound off friendly)
|
||||
- Trending audio compatible
|
||||
|
||||
### TikTok (15-60s)
|
||||
- Casual, authentic feel
|
||||
- Quick cuts
|
||||
- Trend-adaptable structure
|
||||
|
||||
### YouTube Pre-Roll (6s)
|
||||
- Ultra-condensed hook
|
||||
- Single feature focus
|
||||
- Immediate brand recognition
|
||||
|
||||
### Facebook/Meta (15s)
|
||||
- Sound off friendly
|
||||
- Clear text overlays
|
||||
- Emotional appeal
|
||||
|
||||
---
|
||||
|
||||
## A/B Testing Priorities
|
||||
|
||||
### Test 1: Hook Variations
|
||||
- A: "Track your mood in one tap"
|
||||
- B: "Your mood. Your journey. Your way."
|
||||
- C: "The simplest mood tracker"
|
||||
|
||||
### Test 2: Feature Focus
|
||||
- A: Lead with Widgets
|
||||
- B: Lead with Insights
|
||||
- C: Lead with Year View
|
||||
|
||||
### Test 3: Visual Style
|
||||
- A: Lifestyle (hands, real contexts)
|
||||
- B: Product (device mockups)
|
||||
- C: Abstract (colors, animations)
|
||||
|
||||
### Test 4: CTA Messaging
|
||||
- A: "Download now"
|
||||
- B: "Start your journey"
|
||||
- C: "Try free for 30 days"
|
||||
|
||||
---
|
||||
|
||||
## Production Checklist
|
||||
|
||||
### Pre-Production
|
||||
- [ ] Script final approval
|
||||
- [ ] Shot list created
|
||||
- [ ] Assets gathered (screenshots, icons)
|
||||
- [ ] Music licensed
|
||||
|
||||
### Production
|
||||
- [ ] Remotion scenes built
|
||||
- [ ] All animations tuned
|
||||
- [ ] Preview reviewed on mobile
|
||||
|
||||
### Post-Production
|
||||
- [ ] Color grading consistent
|
||||
- [ ] Audio levels balanced
|
||||
- [ ] Captions/text readable at small sizes
|
||||
- [ ] Export in all required formats
|
||||
|
||||
### Delivery
|
||||
- [ ] App Store (1080x1920, H.264)
|
||||
- [ ] Instagram (1080x1920, <30s)
|
||||
- [ ] TikTok (1080x1920, with watermark)
|
||||
- [ ] YouTube (1080x1920 or 1920x1080)
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: January 2026*
|
||||
266
feels-promo/STORYBOARD.md
Normal file
266
feels-promo/STORYBOARD.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# Feels Promo Video Storyboard
|
||||
|
||||
**Version**: V1 - App Store Feature Video
|
||||
**Duration**: ~25 seconds (7 scenes + outro)
|
||||
**Resolution**: 1080x1920 (9:16 Portrait)
|
||||
**FPS**: 30
|
||||
|
||||
---
|
||||
|
||||
## Video Concept
|
||||
|
||||
**Tagline**: *Track your mood. Understand yourself.*
|
||||
|
||||
**Target Audience**: People seeking a simple, beautiful way to track their emotional wellbeing without the complexity of journaling apps.
|
||||
|
||||
**Key Differentiators to Highlight**:
|
||||
1. One-tap simplicity (vs. complex journaling)
|
||||
2. Beautiful widgets & Apple Watch integration
|
||||
3. AI-powered insights (Apple Intelligence)
|
||||
4. Deep customization (themes, icons, colors)
|
||||
5. Privacy-first with iCloud sync
|
||||
|
||||
---
|
||||
|
||||
## Scene Breakdown
|
||||
|
||||
### Scene 1: Hero (0:00-3:30)
|
||||
**Title**: "Your mood. Your journey. Your way."
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| **Background** | Dark forest green gradient (#1a472a → #2d5a3d) with tiled app icons |
|
||||
| **Animation** | Title springs in from top; phone slides in from right |
|
||||
| **Phone Screen** | Day View showing mood list with colorful entries |
|
||||
| **Layout** | Title top-left, phone large on right (extends off-screen) |
|
||||
| **Emotion** | Empowering, personal, inviting |
|
||||
|
||||
**Screenshot Required**: `screen1-day.png` - Day view with several mood entries
|
||||
|
||||
---
|
||||
|
||||
### Scene 2: Widget & Watch (3:30-7:00)
|
||||
**Title**: "Tap. Logged. Done."
|
||||
**Subtitle**: "Never miss a day"
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| **Background** | Golden yellow gradient (#c4a000 → #d4b400) |
|
||||
| **Animation** | Title springs in; widget scales up; watch scales up with delay |
|
||||
| **Widget** | Large square widget showing mood selection |
|
||||
| **Watch** | Apple Watch with Feels complication |
|
||||
| **Labels** | "One-tap widgets" under widget, "Wrist ready" under watch |
|
||||
| **Emotion** | Quick, effortless, accessible |
|
||||
|
||||
**Screenshots Required**:
|
||||
- `screen2-widget.png` - Large widget with 5 mood options
|
||||
- `screen2-watch.png` - Watch face with Feels complication
|
||||
|
||||
---
|
||||
|
||||
### Scene 3: Journal (7:00-10:30)
|
||||
**Title**: "Reflect & Record"
|
||||
**Subtitle**: "Add notes & photos to remember why"
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| **Background** | Medium green gradient (#3d7a4a → #4a8f5a) |
|
||||
| **Animation** | Title springs in; phone scales up from center |
|
||||
| **Phone Screen** | Note editor with text and photo attachment |
|
||||
| **Layout** | Title top, phone centered with slight tilt |
|
||||
| **Emotion** | Thoughtful, meaningful, personal |
|
||||
|
||||
**Screenshot Required**: `screen3-journal.png` - Note editor with example entry
|
||||
|
||||
---
|
||||
|
||||
### Scene 4: Insights (10:30-14:00)
|
||||
**Title**: "Beautiful Insights"
|
||||
**Badge**: "Powered by Apple AI"
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| **Background** | Blue gradient (#2563eb → #3b82f6) |
|
||||
| **Animation** | Title springs in centered; phone scales up; AI badge pops in |
|
||||
| **Phone Screen** | Insights view showing month/year analysis |
|
||||
| **Badge** | White pill with sparkle emoji, appears at bottom |
|
||||
| **Emotion** | Intelligent, modern, trustworthy |
|
||||
|
||||
**Screenshot Required**: `screen4-insights.png` - Insights view with AI badge visible
|
||||
|
||||
---
|
||||
|
||||
### Scene 5: Privacy (14:00-17:30)
|
||||
**Title**: "Private & Secure"
|
||||
**Subtitle**: "Syncs with Apple Health - Locked to you"
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| **Background** | Teal green gradient (#059669 → #10b981) |
|
||||
| **Animation** | Title centered; shield emoji scales in; phone slides from right |
|
||||
| **Phone Screen** | Settings or HealthKit integration screen |
|
||||
| **Shield** | Large shield emoji (left side) |
|
||||
| **Emotion** | Safe, trustworthy, Apple ecosystem |
|
||||
|
||||
**Screenshot Required**: `screen5-privacy.png` - Privacy/HealthKit settings
|
||||
|
||||
---
|
||||
|
||||
### Scene 6: Themes (17:30-21:00)
|
||||
**Title**: "Complete Customization"
|
||||
**Subtitle**: "Your Style"
|
||||
**Detail**: "12 Thoughtful Themes"
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| **Background** | Purple gradient (#7c3aed → #8b5cf6) |
|
||||
| **Animation** | Title right-aligned springs in; phone slides from left |
|
||||
| **Phone Screen** | Theme picker showing color palette options |
|
||||
| **Layout** | Title on right, phone on left (extends off-screen) |
|
||||
| **Emotion** | Personal, expressive, premium |
|
||||
|
||||
**Screenshot Required**: `screen6-themes.png` - Theme/customization picker
|
||||
|
||||
---
|
||||
|
||||
### Scene 7: Notifications (21:00-24:30)
|
||||
**Title**: "Guidance that gets you"
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| **Background** | Cyan gradient (#0891b2 → #06b6d4) |
|
||||
| **Animation** | Title springs in; phone scales up from right |
|
||||
| **Phone Screen** | Lock screen with Feels notification or Live Activity |
|
||||
| **Layout** | Title top-left, phone large on right |
|
||||
| **Emotion** | Supportive, timely, helpful |
|
||||
|
||||
**Screenshot Required**: `screen7-notifications.png` - Notification or Live Activity
|
||||
|
||||
---
|
||||
|
||||
### Outro (24:30-27:00)
|
||||
**Text**: "Feels" + "Track your mood. Understand yourself."
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| **Background** | Default purple gradient with tiled icons |
|
||||
| **Animation** | App icon scales up with glow; text fades in |
|
||||
| **Glow** | Pulsing white radial glow behind icon |
|
||||
| **Emotion** | Memorable, brand moment, call to action |
|
||||
|
||||
**Asset Required**: `app-icon.png` - App icon (1024x1024)
|
||||
|
||||
---
|
||||
|
||||
## Required Assets Checklist
|
||||
|
||||
### Static Assets
|
||||
- [ ] `app-icon.png` - App icon (1024x1024)
|
||||
- [ ] `phone.png` - iPhone frame overlay
|
||||
- [ ] `watch-frame.png` - Apple Watch frame overlay
|
||||
|
||||
### Screenshots (capture from app)
|
||||
- [ ] `screen1-day.png` - Day view with mood entries
|
||||
- [ ] `screen2-widget.png` - Large widget mockup
|
||||
- [ ] `screen2-watch.png` - Watch face/complication
|
||||
- [ ] `screen3-journal.png` - Note editor with photo
|
||||
- [ ] `screen4-insights.png` - Insights view
|
||||
- [ ] `screen5-privacy.png` - Privacy/HealthKit settings
|
||||
- [ ] `screen6-themes.png` - Theme customization
|
||||
- [ ] `screen7-notifications.png` - Notification/Live Activity
|
||||
|
||||
---
|
||||
|
||||
## Color Palette by Scene
|
||||
|
||||
| Scene | Primary Color | Hex |
|
||||
|-------|--------------|-----|
|
||||
| 1. Hero | Forest Green | #1a472a |
|
||||
| 2. Widget | Golden Yellow | #c4a000 |
|
||||
| 3. Journal | Medium Green | #3d7a4a |
|
||||
| 4. Insights | Royal Blue | #2563eb |
|
||||
| 5. Privacy | Teal | #059669 |
|
||||
| 6. Themes | Purple | #7c3aed |
|
||||
| 7. Notifications | Cyan | #0891b2 |
|
||||
| Outro | Purple | #667eea |
|
||||
|
||||
---
|
||||
|
||||
## Animation Timing Reference
|
||||
|
||||
| Animation | Duration | Easing |
|
||||
|-----------|----------|--------|
|
||||
| Title entrance | ~0.5s | Spring (damping: 200) |
|
||||
| Phone entrance | ~0.8s | Spring (damping: 12, stiffness: 80) |
|
||||
| Badge/icon pop | ~0.4s | Spring (damping: 10-15, stiffness: 100) |
|
||||
| Scene transition | 0.6s | Linear fade |
|
||||
|
||||
---
|
||||
|
||||
## Future Video Concepts
|
||||
|
||||
### V2: "Year in Pixels" Focus
|
||||
Highlight the Year View heatmap visualization - emphasize seeing a full year of emotions at a glance.
|
||||
|
||||
**Key Scene**: Animated heatmap filling in with colors over time.
|
||||
|
||||
### V3: "Streak Challenge"
|
||||
Gamification angle - show streak building, Live Activity on lock screen, celebrating milestones.
|
||||
|
||||
**Key Scene**: Streak counter incrementing with celebration animations.
|
||||
|
||||
### V4: "Personality Packs"
|
||||
Customization deep-dive - showcase different icon packs, themes, and color combinations.
|
||||
|
||||
**Key Scene**: Rapid montage of different visual styles.
|
||||
|
||||
### V5: "Share Your Year"
|
||||
Social sharing feature - show the shareable year review card being generated and shared.
|
||||
|
||||
**Key Scene**: Year card animation rendering, share sheet appearing.
|
||||
|
||||
---
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Remotion Commands
|
||||
```bash
|
||||
# Start preview
|
||||
npm run dev
|
||||
|
||||
# Render video
|
||||
npx remotion render FeelsPromoV1 out/feels-promo-v1.mp4
|
||||
|
||||
# Render specific frame range (for testing)
|
||||
npx remotion render FeelsPromoV1 out/test.mp4 --frames=0-30
|
||||
```
|
||||
|
||||
### Screenshot Capture Tips
|
||||
1. Use iPhone 15 Pro Max simulator for highest quality
|
||||
2. Enable "Show Touch" in simulator for interaction demos
|
||||
3. Capture at 3x scale for maximum resolution
|
||||
4. Remove status bar in post-processing if needed
|
||||
|
||||
---
|
||||
|
||||
## Messaging Framework
|
||||
|
||||
### Primary Message
|
||||
"Track your mood in seconds, understand your patterns over time."
|
||||
|
||||
### Supporting Messages
|
||||
1. **Simplicity**: "One tap. That's all it takes."
|
||||
2. **Insight**: "See your emotional journey unfold."
|
||||
3. **Privacy**: "Your feelings, your data, your control."
|
||||
4. **Style**: "Make it yours with 12 beautiful themes."
|
||||
5. **Ecosystem**: "Works seamlessly with Apple Watch and widgets."
|
||||
|
||||
### Emotional Arc
|
||||
1. **Hook** (Scene 1): Personal, empowering
|
||||
2. **Features** (Scenes 2-6): Practical, impressive
|
||||
3. **Trust** (Scene 5-6): Secure, personalized
|
||||
4. **CTA** (Outro): Memorable, action-driving
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: January 2026*
|
||||
674
feels-promo/src/ConceptA-SelfAwareness.tsx
Normal file
674
feels-promo/src/ConceptA-SelfAwareness.tsx
Normal file
@@ -0,0 +1,674 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
|
||||
// Shared tiled background component
|
||||
const TiledIconBackground: React.FC<{ color?: string }> = ({
|
||||
color = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
const iconSize = 80;
|
||||
const gap = 40;
|
||||
const cellSize = iconSize + gap;
|
||||
|
||||
const cols = Math.ceil(width / cellSize) + 4;
|
||||
const rows = Math.ceil(height / cellSize) + 4;
|
||||
|
||||
const offsetX = (frame * 0.3) % cellSize;
|
||||
const offsetY = (frame * 0.2) % cellSize;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: color,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -cellSize * 2,
|
||||
left: -cellSize * 2,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`,
|
||||
}}
|
||||
>
|
||||
{[...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>
|
||||
);
|
||||
};
|
||||
|
||||
// Phone frame component
|
||||
const PhoneFrame: React.FC<{
|
||||
mediaSrc: string;
|
||||
width?: number;
|
||||
rotation?: number;
|
||||
style?: React.CSSProperties;
|
||||
}> = ({ mediaSrc, width = 460, rotation = 0, style }) => {
|
||||
const aspectRatio = 2760 / 1350;
|
||||
const height = width * aspectRatio;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
height,
|
||||
transform: rotation ? `rotate(${rotation}deg)` : undefined,
|
||||
filter: "drop-shadow(0 40px 80px rgba(0,0,0,0.5))",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2.5%",
|
||||
left: "5.33%",
|
||||
right: "5.33%",
|
||||
bottom: "2.5%",
|
||||
borderRadius: width * 0.083,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile(mediaSrc)}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Img
|
||||
src={staticFile("phone.png")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Hook - "What if understanding yourself took 5 seconds a day?"
|
||||
const HookScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const textProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
const textOpacity = interpolate(textProgress, [0, 1], [0, 1]);
|
||||
const textY = interpolate(textProgress, [0, 1], [50, 0]);
|
||||
|
||||
const secondLineProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
const secondLineOpacity = interpolate(secondLineProgress, [0, 1], [0, 1]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #1e3a5f 0%, #2d5a87 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 60,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textAlign: "center",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
lineHeight: 1.2,
|
||||
transform: `translateY(${-textY}px)`,
|
||||
opacity: textOpacity,
|
||||
}}
|
||||
>
|
||||
What if understanding yourself
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "#fbbf24",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textAlign: "center",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
lineHeight: 1.2,
|
||||
marginTop: 20,
|
||||
opacity: secondLineOpacity,
|
||||
}}
|
||||
>
|
||||
took 5 seconds a day?
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: Demo - Quick-fire mood logging
|
||||
const DemoScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const widget1Progress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const widget1Scale = interpolate(widget1Progress, [0, 1], [0.5, 1]);
|
||||
|
||||
const widget2Progress = spring({
|
||||
frame: frame - 20,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const widget2Scale = interpolate(widget2Progress, [0, 1], [0.5, 1]);
|
||||
|
||||
const widget3Progress = spring({
|
||||
frame: frame - 40,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const widget3Scale = interpolate(widget3Progress, [0, 1], [0.5, 1]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #059669 0%, #10b981 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: 700,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Log anywhere, anytime
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Three devices */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "flex-end",
|
||||
gap: 40,
|
||||
}}
|
||||
>
|
||||
{/* Widget */}
|
||||
<div
|
||||
style={{
|
||||
transform: `scale(${widget1Scale})`,
|
||||
filter: "drop-shadow(0 20px 40px rgba(0,0,0,0.4))",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("screen2-widget.png")}
|
||||
style={{
|
||||
width: 280,
|
||||
height: 280,
|
||||
borderRadius: 30,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
Widget
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
<div
|
||||
style={{
|
||||
transform: `scale(${widget2Scale})`,
|
||||
}}
|
||||
>
|
||||
<PhoneFrame mediaSrc="screen1-day.png" width={320} />
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
App
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Watch */}
|
||||
<div
|
||||
style={{
|
||||
transform: `scale(${widget3Scale})`,
|
||||
filter: "drop-shadow(0 20px 40px rgba(0,0,0,0.4))",
|
||||
}}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "22%",
|
||||
left: "15%",
|
||||
width: "70%",
|
||||
height: "42%",
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("screen2-watch.png")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Img
|
||||
src={staticFile("watch-frame.png")}
|
||||
style={{
|
||||
width: 180,
|
||||
height: 288,
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
Watch
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Payoff - Calendar filling with colors
|
||||
const PayoffScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const phoneProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const phoneScale = interpolate(phoneProgress, [0, 1], [0.8, 1]);
|
||||
|
||||
// Simulate cells filling in
|
||||
const fillProgress = interpolate(frame, [0, fps * 4], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #7c3aed 0%, #8b5cf6 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 60,
|
||||
right: 60,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
lineHeight: 1.1,
|
||||
}}
|
||||
>
|
||||
Watch patterns
|
||||
<br />
|
||||
emerge
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone with year view */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
bottom: -150,
|
||||
transform: `translateX(-50%) scale(${phoneScale})`,
|
||||
}}
|
||||
>
|
||||
<PhoneFrame mediaSrc="screen1-day.png" width={600} />
|
||||
</div>
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 120,
|
||||
left: 60,
|
||||
right: 60,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: "rgba(255,255,255,0.3)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${fillProgress * 100}%`,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
marginTop: 12,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{Math.round(fillProgress * 365)} days tracked
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: AI Insights
|
||||
const InsightsScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const cardProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const cardScale = interpolate(cardProgress, [0, 1], [0.8, 1]);
|
||||
const cardOpacity = interpolate(cardProgress, [0, 1], [0, 1]);
|
||||
|
||||
const sparkleProgress = spring({
|
||||
frame: frame - 20,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 100 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #2563eb 0%, #3b82f6 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
AI understands you
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Insight card */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${cardScale})`,
|
||||
opacity: cardOpacity,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.95)",
|
||||
borderRadius: 30,
|
||||
padding: 50,
|
||||
width: 800,
|
||||
boxShadow: "0 30px 60px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 20, marginBottom: 30 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 48,
|
||||
transform: `scale(${interpolate(sparkleProgress, [0, 1], [0, 1])})`,
|
||||
}}
|
||||
>
|
||||
✨
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
}}
|
||||
>
|
||||
This Month
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
color: "#374151",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
"You tend to feel better on weekends. Consider bringing more of that
|
||||
weekend energy into your weekdays."
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 5: CTA
|
||||
const CTAScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoScale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
const textOpacity = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
const glowIntensity = interpolate(Math.sin(frame * 0.1), [-1, 1], [0.3, 0.6]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #1e3a5f 0%, #2d5a87 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 500,
|
||||
height: 500,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, rgba(255,255,255,${glowIntensity}) 0%, transparent 70%)`,
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.3, 1])})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.5, 1])})`,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: 200 * 0.22,
|
||||
marginBottom: 40,
|
||||
filter: `drop-shadow(0 0 60px rgba(255,255,255,${glowIntensity}))`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 80,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: 500,
|
||||
color: "#fbbf24",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
marginTop: 30,
|
||||
opacity: interpolate(textOpacity, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
Start today. Understand tomorrow.
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition
|
||||
export const ConceptASelfAwareness: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(5 * fps)}>
|
||||
<HookScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(5 * fps)} durationInFrames={Math.round(7 * fps)}>
|
||||
<DemoScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(12 * fps)} durationInFrames={Math.round(6 * fps)}>
|
||||
<PayoffScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(18 * fps)} durationInFrames={Math.round(7 * fps)}>
|
||||
<InsightsScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(25 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<CTAScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
589
feels-promo/src/ConceptB-NoJournalJournal.tsx
Normal file
589
feels-promo/src/ConceptB-NoJournalJournal.tsx
Normal file
@@ -0,0 +1,589 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
|
||||
// Shared tiled background component
|
||||
const TiledIconBackground: React.FC<{ color?: string }> = ({
|
||||
color = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
const iconSize = 80;
|
||||
const gap = 40;
|
||||
const cellSize = iconSize + gap;
|
||||
|
||||
const cols = Math.ceil(width / cellSize) + 4;
|
||||
const rows = Math.ceil(height / cellSize) + 4;
|
||||
|
||||
const offsetX = (frame * 0.3) % cellSize;
|
||||
const offsetY = (frame * 0.2) % cellSize;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: color,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -cellSize * 2,
|
||||
left: -cellSize * 2,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`,
|
||||
}}
|
||||
>
|
||||
{[...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>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Problem - "Journaling is hard"
|
||||
const ProblemScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const textProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
const textOpacity = interpolate(textProgress, [0, 1], [0, 1]);
|
||||
|
||||
const strikeProgress = interpolate(frame, [fps * 1.5, fps * 2], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #991b1b 0%, #b91c1c 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 60,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 96,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textAlign: "center",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
opacity: textOpacity,
|
||||
}}
|
||||
>
|
||||
Journaling is hard
|
||||
</div>
|
||||
{/* Strike-through line */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: 0,
|
||||
height: 12,
|
||||
backgroundColor: "#fbbf24",
|
||||
width: `${strikeProgress * 100}%`,
|
||||
transform: "translateY(-50%) rotate(-2deg)",
|
||||
borderRadius: 6,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: Solution - "Feels is easy"
|
||||
const SolutionScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const textProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
const textOpacity = interpolate(textProgress, [0, 1], [0, 1]);
|
||||
const textScale = interpolate(textProgress, [0, 1], [0.8, 1]);
|
||||
|
||||
const tapProgress = spring({
|
||||
frame: frame - 20,
|
||||
fps,
|
||||
config: { damping: 15, stiffness: 100 },
|
||||
});
|
||||
|
||||
const checkProgress = spring({
|
||||
frame: frame - 40,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 100 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #059669 0%, #10b981 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 60,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 96,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textAlign: "center",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
opacity: textOpacity,
|
||||
transform: `scale(${textScale})`,
|
||||
marginBottom: 60,
|
||||
}}
|
||||
>
|
||||
Feels is easy
|
||||
</div>
|
||||
|
||||
{/* Tap animation sequence */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 30,
|
||||
opacity: interpolate(tapProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 25,
|
||||
backgroundColor: "#fbbf24",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 48,
|
||||
boxShadow: "0 10px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
😊
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
→
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
transform: `scale(${interpolate(checkProgress, [0, 1], [0, 1])})`,
|
||||
}}
|
||||
>
|
||||
✅
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 600,
|
||||
color: "white",
|
||||
opacity: interpolate(checkProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
Done!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Comparison - Complex vs Simple
|
||||
const ComparisonScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const leftProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
|
||||
const rightProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
|
||||
const vsProgress = spring({
|
||||
frame: frame - 30,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 100 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #374151 0%, #4b5563 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 40,
|
||||
gap: 40,
|
||||
}}
|
||||
>
|
||||
{/* Left: Other apps */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
opacity: interpolate(leftProgress, [0, 1], [0, 1]),
|
||||
transform: `translateX(${interpolate(leftProgress, [0, 1], [-50, 0])}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
borderRadius: 30,
|
||||
padding: 40,
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 30,
|
||||
backgroundColor: "rgba(255,255,255,0.3)",
|
||||
borderRadius: 8,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
height: 30,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
borderRadius: 8,
|
||||
marginBottom: 20,
|
||||
width: "80%",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
height: 100,
|
||||
backgroundColor: "rgba(255,255,255,0.15)",
|
||||
borderRadius: 12,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
height: 30,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
borderRadius: 8,
|
||||
width: "60%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: "#ef4444",
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
Other apps: 5+ minutes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VS */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
transform: `scale(${interpolate(vsProgress, [0, 1], [0, 1])})`,
|
||||
}}
|
||||
>
|
||||
VS
|
||||
</div>
|
||||
|
||||
{/* Right: Feels */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
opacity: interpolate(rightProgress, [0, 1], [0, 1]),
|
||||
transform: `translateX(${interpolate(rightProgress, [0, 1], [50, 0])}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#059669",
|
||||
borderRadius: 30,
|
||||
padding: 40,
|
||||
width: 400,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: 20,
|
||||
}}
|
||||
>
|
||||
{["😢", "😕", "😐", "😊", "😄"].map((emoji, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 15,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 32,
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: "#10b981",
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
Feels: 5 seconds
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: Benefit
|
||||
const BenefitScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const textProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
const textOpacity = interpolate(textProgress, [0, 1], [0, 1]);
|
||||
|
||||
const subProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #7c3aed 0%, #8b5cf6 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 60,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textAlign: "center",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
opacity: textOpacity,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Same insights.
|
||||
<br />
|
||||
Fraction of effort.
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 42,
|
||||
fontWeight: 500,
|
||||
color: "#fbbf24",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textAlign: "center",
|
||||
marginTop: 40,
|
||||
opacity: interpolate(subProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
Finally, tracking that sticks.
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 5: CTA
|
||||
const CTAScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoScale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
const glowIntensity = interpolate(Math.sin(frame * 0.1), [-1, 1], [0.3, 0.6]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #059669 0%, #10b981 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, rgba(255,255,255,${glowIntensity}) 0%, transparent 70%)`,
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.3, 1])})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.5, 1])})`,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 180 * 0.22,
|
||||
marginBottom: 30,
|
||||
filter: `drop-shadow(0 0 60px rgba(255,255,255,${glowIntensity}))`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: 600,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
marginTop: 20,
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
The no-journal journal
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 20 seconds total
|
||||
export const ConceptBNoJournalJournal: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(4 * fps)}>
|
||||
<ProblemScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(4 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<SolutionScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(8 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<ComparisonScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(13 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<BenefitScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(17 * fps)} durationInFrames={Math.round(3 * fps)}>
|
||||
<CTAScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
410
feels-promo/src/ConceptC-YearInFeelings.tsx
Normal file
410
feels-promo/src/ConceptC-YearInFeelings.tsx
Normal file
@@ -0,0 +1,410 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
|
||||
// Shared tiled background component
|
||||
const TiledIconBackground: React.FC<{ color?: string }> = ({
|
||||
color = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
const iconSize = 80;
|
||||
const gap = 40;
|
||||
const cellSize = iconSize + gap;
|
||||
|
||||
const cols = Math.ceil(width / cellSize) + 4;
|
||||
const rows = Math.ceil(height / cellSize) + 4;
|
||||
|
||||
const offsetX = (frame * 0.3) % cellSize;
|
||||
const offsetY = (frame * 0.2) % cellSize;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: color,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -cellSize * 2,
|
||||
left: -cellSize * 2,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`,
|
||||
}}
|
||||
>
|
||||
{[...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>
|
||||
);
|
||||
};
|
||||
|
||||
// Mood colors for the heatmap
|
||||
const MOOD_COLORS = ["#ef4444", "#f97316", "#fbbf24", "#22c55e", "#10b981"];
|
||||
|
||||
// Scene 1: Heatmap filling in
|
||||
const HeatmapScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Generate a consistent grid of mood values
|
||||
const gridCols = 12;
|
||||
const gridRows = 31;
|
||||
|
||||
const fillProgress = interpolate(frame, [0, fps * 6], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const totalCells = gridCols * gridRows;
|
||||
const filledCells = Math.floor(fillProgress * totalCells);
|
||||
|
||||
// Title animation
|
||||
const titleOpacity = interpolate(frame, [0, fps * 0.5], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #1e1b4b 0%, #312e81 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: titleOpacity,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Your Year in Feelings
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Heatmap grid */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${gridCols}, 60px)`,
|
||||
gap: 6,
|
||||
padding: 30,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
borderRadius: 30,
|
||||
}}
|
||||
>
|
||||
{[...Array(totalCells)].map((_, i) => {
|
||||
const isFilled = i < filledCells;
|
||||
const moodIndex = Math.floor(Math.random() * 5);
|
||||
const color = isFilled ? MOOD_COLORS[moodIndex] : "rgba(255,255,255,0.1)";
|
||||
|
||||
// Stagger the scale animation
|
||||
const cellDelay = (i / totalCells) * fps * 5;
|
||||
const cellProgress = spring({
|
||||
frame: frame - cellDelay,
|
||||
fps,
|
||||
config: { damping: 15, stiffness: 150 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 60,
|
||||
height: 16,
|
||||
borderRadius: 4,
|
||||
backgroundColor: isFilled ? color : "rgba(255,255,255,0.1)",
|
||||
transform: `scaleX(${isFilled ? interpolate(cellProgress, [0, 1], [0, 1]) : 1})`,
|
||||
transformOrigin: "left",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Year label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 120,
|
||||
fontWeight: 800,
|
||||
color: "rgba(255,255,255,0.1)",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
}}
|
||||
>
|
||||
2025
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: Share card appearing
|
||||
const ShareScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const cardProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const cardScale = interpolate(cardProgress, [0, 1], [0.7, 1]);
|
||||
const cardOpacity = interpolate(cardProgress, [0, 1], [0, 1]);
|
||||
|
||||
const shareProgress = spring({
|
||||
frame: frame - 30,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 100 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #7c3aed 0%, #a855f7 100%)" />
|
||||
|
||||
{/* Share card */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${cardScale})`,
|
||||
opacity: cardOpacity,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderRadius: 40,
|
||||
padding: 60,
|
||||
width: 700,
|
||||
boxShadow: "0 40px 80px rgba(0,0,0,0.4)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 800,
|
||||
color: "#1a1a1a",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
2025
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#6b7280",
|
||||
marginBottom: 40,
|
||||
}}
|
||||
>
|
||||
Year in Review
|
||||
</div>
|
||||
|
||||
{/* Mini heatmap */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(12, 1fr)",
|
||||
gap: 4,
|
||||
marginBottom: 40,
|
||||
}}
|
||||
>
|
||||
{[...Array(60)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
height: 12,
|
||||
borderRadius: 3,
|
||||
backgroundColor: MOOD_COLORS[Math.floor(Math.random() * 5)],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{ display: "flex", justifyContent: "space-around" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<div style={{ fontSize: 48, fontWeight: 700, color: "#10b981" }}>
|
||||
312
|
||||
</div>
|
||||
<div style={{ fontSize: 20, color: "#6b7280" }}>Days Tracked</div>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<div style={{ fontSize: 48 }}>😊</div>
|
||||
<div style={{ fontSize: 20, color: "#6b7280" }}>Top Mood</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feels branding */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 40,
|
||||
textAlign: "center",
|
||||
fontSize: 24,
|
||||
color: "#9ca3af",
|
||||
}}
|
||||
>
|
||||
ifeel
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Share button */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 150,
|
||||
left: "50%",
|
||||
transform: `translateX(-50%) scale(${interpolate(shareProgress, [0, 1], [0, 1])})`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderRadius: 50,
|
||||
padding: "20px 50px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 15,
|
||||
boxShadow: "0 10px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 32 }}>📤</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
}}
|
||||
>
|
||||
Share Your Year
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: CTA
|
||||
const CTAScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const textProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #1e1b4b 0%, #312e81 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 60,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
borderRadius: 160 * 0.22,
|
||||
marginBottom: 40,
|
||||
filter: "drop-shadow(0 20px 40px rgba(0,0,0,0.4))",
|
||||
transform: `scale(${interpolate(textProgress, [0, 1], [0.5, 1])})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: 800,
|
||||
color: "#fbbf24",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textAlign: "center",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
opacity: interpolate(textProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
What will YOUR year look like?
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 15 seconds total
|
||||
export const ConceptCYearInFeelings: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(7 * fps)}>
|
||||
<HeatmapScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(7 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<ShareScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(12 * fps)} durationInFrames={Math.round(3 * fps)}>
|
||||
<CTAScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
645
feels-promo/src/ConceptD-AlwaysThere.tsx
Normal file
645
feels-promo/src/ConceptD-AlwaysThere.tsx
Normal file
@@ -0,0 +1,645 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
|
||||
// Shared tiled background component
|
||||
const TiledIconBackground: React.FC<{ color?: string }> = ({
|
||||
color = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
const iconSize = 80;
|
||||
const gap = 40;
|
||||
const cellSize = iconSize + gap;
|
||||
|
||||
const cols = Math.ceil(width / cellSize) + 4;
|
||||
const rows = Math.ceil(height / cellSize) + 4;
|
||||
|
||||
const offsetX = (frame * 0.3) % cellSize;
|
||||
const offsetY = (frame * 0.2) % cellSize;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: color,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -cellSize * 2,
|
||||
left: -cellSize * 2,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`,
|
||||
}}
|
||||
>
|
||||
{[...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>
|
||||
);
|
||||
};
|
||||
|
||||
// Phone frame component
|
||||
const PhoneFrame: React.FC<{
|
||||
mediaSrc: string;
|
||||
width?: number;
|
||||
rotation?: number;
|
||||
style?: React.CSSProperties;
|
||||
}> = ({ mediaSrc, width = 460, rotation = 0, style }) => {
|
||||
const aspectRatio = 2760 / 1350;
|
||||
const height = width * aspectRatio;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
height,
|
||||
transform: rotation ? `rotate(${rotation}deg)` : undefined,
|
||||
filter: "drop-shadow(0 40px 80px rgba(0,0,0,0.5))",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2.5%",
|
||||
left: "5.33%",
|
||||
right: "5.33%",
|
||||
bottom: "2.5%",
|
||||
borderRadius: width * 0.083,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile(mediaSrc)}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Img
|
||||
src={staticFile("phone.png")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Morning - Widget tap while coffee
|
||||
const MorningScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const widgetProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const widgetScale = interpolate(widgetProgress, [0, 1], [0.8, 1]);
|
||||
|
||||
const labelProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #f59e0b 0%, #fbbf24 100%)" />
|
||||
|
||||
{/* Time label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
left: 60,
|
||||
opacity: interpolate(labelProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
}}
|
||||
>
|
||||
7:30 AM
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
>
|
||||
Morning coffee
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Widget */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${widgetScale})`,
|
||||
filter: "drop-shadow(0 30px 60px rgba(0,0,0,0.3))",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("screen2-widget.png")}
|
||||
style={{
|
||||
width: 500,
|
||||
height: 500,
|
||||
borderRadius: 50,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tap indicator */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 15,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 40 }}>☕</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "white",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
One tap from home screen
|
||||
</span>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: Midday - Apple Watch at work
|
||||
const MiddayScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const watchProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const watchScale = interpolate(watchProgress, [0, 1], [0.8, 1]);
|
||||
|
||||
const labelProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #3b82f6 0%, #60a5fa 100%)" />
|
||||
|
||||
{/* Time label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
right: 60,
|
||||
textAlign: "right",
|
||||
opacity: interpolate(labelProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
}}
|
||||
>
|
||||
12:45 PM
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
>
|
||||
Lunch break
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Watch */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${watchScale})`,
|
||||
filter: "drop-shadow(0 30px 60px rgba(0,0,0,0.3))",
|
||||
}}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "22%",
|
||||
left: "15%",
|
||||
width: "70%",
|
||||
height: "42%",
|
||||
borderRadius: 25,
|
||||
overflow: "hidden",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("screen2-watch.png")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Img
|
||||
src={staticFile("watch-frame.png")}
|
||||
style={{
|
||||
width: 400,
|
||||
height: 640,
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 15,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 40 }}>⌚</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "white",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Quick glance from your wrist
|
||||
</span>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Evening - Lock Screen Live Activity
|
||||
const EveningScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const phoneProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const phoneScale = interpolate(phoneProgress, [0, 1], [0.8, 1]);
|
||||
|
||||
const labelProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #8b5cf6 0%, #a78bfa 100%)" />
|
||||
|
||||
{/* Time label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
left: 60,
|
||||
opacity: interpolate(labelProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
}}
|
||||
>
|
||||
6:30 PM
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
>
|
||||
Evening wind-down
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone with lock screen */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -50,
|
||||
bottom: -150,
|
||||
transform: `scale(${phoneScale})`,
|
||||
}}
|
||||
>
|
||||
<PhoneFrame mediaSrc="screen7-notifications.png" width={550} rotation={-5} />
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 60,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "white",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
🔔 Gentle reminder on your Lock Screen
|
||||
</span>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: Night - Quick note before bed
|
||||
const NightScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const phoneProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const phoneScale = interpolate(phoneProgress, [0, 1], [0.8, 1]);
|
||||
|
||||
const labelProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #1e3a5f 0%, #1e293b 100%)" />
|
||||
|
||||
{/* Time label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
right: 60,
|
||||
textAlign: "right",
|
||||
opacity: interpolate(labelProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
}}
|
||||
>
|
||||
10:15 PM
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
>
|
||||
Before sleep
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone with journal */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
bottom: -150,
|
||||
transform: `translateX(-50%) scale(${phoneScale})`,
|
||||
}}
|
||||
>
|
||||
<PhoneFrame mediaSrc="screen3-journal.png" width={550} />
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 15,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 40 }}>🌙</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "white",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Add a note to remember the day
|
||||
</span>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 5: CTA
|
||||
const CTAScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoScale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
const textOpacity = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
const glowIntensity = interpolate(Math.sin(frame * 0.1), [-1, 1], [0.3, 0.6]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #059669 0%, #10b981 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, rgba(255,255,255,${glowIntensity}) 0%, transparent 70%)`,
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.3, 1])})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.5, 1])})`,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 180 * 0.22,
|
||||
marginBottom: 40,
|
||||
filter: `drop-shadow(0 0 60px rgba(255,255,255,${glowIntensity}))`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: 600,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
marginTop: 20,
|
||||
opacity: interpolate(textOpacity, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
Your mood tracker, wherever you are
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 25 seconds total
|
||||
export const ConceptDAlwaysThere: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(5 * fps)}>
|
||||
<MorningScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(5 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<MiddayScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(10 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<EveningScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(15 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<NightScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(20 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<CTAScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
664
feels-promo/src/ConceptE-MakeItYours.tsx
Normal file
664
feels-promo/src/ConceptE-MakeItYours.tsx
Normal file
@@ -0,0 +1,664 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
|
||||
// Theme colors for montage
|
||||
const THEMES = [
|
||||
{ name: "Forest", colors: ["#1a472a", "#2d5a3d"] },
|
||||
{ name: "Ocean", colors: ["#1e3a5f", "#2563eb"] },
|
||||
{ name: "Sunset", colors: ["#f59e0b", "#ef4444"] },
|
||||
{ name: "Lavender", colors: ["#7c3aed", "#a855f7"] },
|
||||
{ name: "Rose", colors: ["#e11d48", "#f43f5e"] },
|
||||
{ name: "Mint", colors: ["#059669", "#10b981"] },
|
||||
{ name: "Neon", colors: ["#0ea5e9", "#06b6d4"] },
|
||||
{ name: "Charcoal", colors: ["#1f2937", "#374151"] },
|
||||
];
|
||||
|
||||
// Shared tiled background component
|
||||
const TiledIconBackground: React.FC<{ color?: string }> = ({
|
||||
color = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
const iconSize = 80;
|
||||
const gap = 40;
|
||||
const cellSize = iconSize + gap;
|
||||
|
||||
const cols = Math.ceil(width / cellSize) + 4;
|
||||
const rows = Math.ceil(height / cellSize) + 4;
|
||||
|
||||
const offsetX = (frame * 0.3) % cellSize;
|
||||
const offsetY = (frame * 0.2) % cellSize;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: color,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -cellSize * 2,
|
||||
left: -cellSize * 2,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`,
|
||||
}}
|
||||
>
|
||||
{[...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>
|
||||
);
|
||||
};
|
||||
|
||||
// Phone frame component
|
||||
const PhoneFrame: React.FC<{
|
||||
mediaSrc: string;
|
||||
width?: number;
|
||||
rotation?: number;
|
||||
style?: React.CSSProperties;
|
||||
}> = ({ mediaSrc, width = 460, rotation = 0, style }) => {
|
||||
const aspectRatio = 2760 / 1350;
|
||||
const height = width * aspectRatio;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
height,
|
||||
transform: rotation ? `rotate(${rotation}deg)` : undefined,
|
||||
filter: "drop-shadow(0 40px 80px rgba(0,0,0,0.5))",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2.5%",
|
||||
left: "5.33%",
|
||||
right: "5.33%",
|
||||
bottom: "2.5%",
|
||||
borderRadius: width * 0.083,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile(mediaSrc)}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Img
|
||||
src={staticFile("phone.png")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Rapid theme montage
|
||||
const ThemeMontageScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Cycle through themes every 0.4 seconds
|
||||
const themeDuration = Math.round(0.4 * fps);
|
||||
const currentThemeIndex = Math.floor(frame / themeDuration) % THEMES.length;
|
||||
const theme = THEMES[currentThemeIndex];
|
||||
|
||||
const titleProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
|
||||
// Calculate progress within current theme for animation
|
||||
const themeProgress = (frame % themeDuration) / themeDuration;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: `linear-gradient(180deg, ${theme.colors[0]} 0%, ${theme.colors[1]} 100%)`,
|
||||
transition: "background 0.1s ease-out",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(titleProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Make it yours
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Theme name */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "45%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(themeProgress, [0, 0.5, 1], [0.8, 1, 0.8])})`,
|
||||
opacity: interpolate(themeProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 120,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 60px rgba(0,0,0,0.5)",
|
||||
}}
|
||||
>
|
||||
{theme.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mood icons row */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: 30,
|
||||
}}
|
||||
>
|
||||
{["😢", "😕", "😐", "😊", "😄"].map((emoji, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 48,
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: Theme picker detail
|
||||
const ThemeDetailScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const phoneProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const phoneScale = interpolate(phoneProgress, [0, 1], [0.8, 1]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #1f2937 0%, #374151 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 60,
|
||||
right: 60,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
12 thoughtful themes
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 500,
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
marginTop: 15,
|
||||
}}
|
||||
>
|
||||
Light, dark, and everything in between
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone with theme picker */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
bottom: -150,
|
||||
transform: `translateX(-50%) scale(${phoneScale})`,
|
||||
}}
|
||||
>
|
||||
<PhoneFrame mediaSrc="screen6-themes.png" width={550} />
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Icon packs
|
||||
const IconPacksScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const titleProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
|
||||
// Cycle through icon sets
|
||||
const iconSets = [
|
||||
["😢", "😕", "😐", "😊", "😄"],
|
||||
["💔", "😤", "😑", "🙂", "😁"],
|
||||
["🌧️", "☁️", "⛅", "🌤️", "☀️"],
|
||||
["1", "2", "3", "4", "5"],
|
||||
];
|
||||
const setDuration = Math.round(0.8 * fps);
|
||||
const currentSetIndex = Math.floor(frame / setDuration) % iconSets.length;
|
||||
const iconSet = iconSets[currentSetIndex];
|
||||
|
||||
const setProgress = (frame % setDuration) / setDuration;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #7c3aed 0%, #a855f7 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(titleProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Choose your icons
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Icon set display */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(setProgress, [0, 0.5, 1], [0.9, 1, 0.9])})`,
|
||||
opacity: interpolate(setProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 30,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
padding: 50,
|
||||
borderRadius: 40,
|
||||
}}
|
||||
>
|
||||
{iconSet.map((icon, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 30,
|
||||
backgroundColor: `hsl(${i * 30 + 0}, 70%, 50%)`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 64,
|
||||
color: "white",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Multiple icon packs to match your style
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: Widgets
|
||||
const WidgetsScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const widget1Progress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
|
||||
const widget2Progress = spring({
|
||||
frame: frame - 10,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #059669 0%, #10b981 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Custom widgets
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Widgets */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
gap: 40,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{/* Large widget */}
|
||||
<div
|
||||
style={{
|
||||
transform: `scale(${interpolate(widget1Progress, [0, 1], [0.5, 1])})`,
|
||||
opacity: interpolate(widget1Progress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("screen2-widget.png")}
|
||||
style={{
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: 40,
|
||||
filter: "drop-shadow(0 30px 60px rgba(0,0,0,0.4))",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Small widgets */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 20,
|
||||
transform: `scale(${interpolate(widget2Progress, [0, 1], [0.5, 1])})`,
|
||||
opacity: interpolate(widget2Progress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: 30,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 80,
|
||||
}}
|
||||
>
|
||||
😊
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 200,
|
||||
height: 90,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0 20px",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 28, color: "white", fontWeight: 600 }}>
|
||||
🔥 15 day streak
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 150,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Small, medium, and large sizes
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 5: CTA
|
||||
const CTAScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoScale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
const glowIntensity = interpolate(Math.sin(frame * 0.1), [-1, 1], [0.3, 0.6]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #7c3aed 0%, #a855f7 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, rgba(255,255,255,${glowIntensity}) 0%, transparent 70%)`,
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.3, 1])})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.5, 1])})`,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 180 * 0.22,
|
||||
marginBottom: 40,
|
||||
filter: `drop-shadow(0 0 60px rgba(255,255,255,${glowIntensity}))`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 42,
|
||||
fontWeight: 700,
|
||||
color: "#fbbf24",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
marginTop: 20,
|
||||
}}
|
||||
>
|
||||
12 themes. Unlimited you.
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 20 seconds total
|
||||
export const ConceptEMakeItYours: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(5 * fps)}>
|
||||
<ThemeMontageScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(5 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<ThemeDetailScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(9 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<IconPacksScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(13 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<WidgetsScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(17 * fps)} durationInFrames={Math.round(3 * fps)}>
|
||||
<CTAScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
549
feels-promo/src/ConceptF-PrivacyFirst.tsx
Normal file
549
feels-promo/src/ConceptF-PrivacyFirst.tsx
Normal file
@@ -0,0 +1,549 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
|
||||
// Shared tiled background component
|
||||
const TiledIconBackground: React.FC<{ color?: string }> = ({
|
||||
color = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
const iconSize = 80;
|
||||
const gap = 40;
|
||||
const cellSize = iconSize + gap;
|
||||
|
||||
const cols = Math.ceil(width / cellSize) + 4;
|
||||
const rows = Math.ceil(height / cellSize) + 4;
|
||||
|
||||
const offsetX = (frame * 0.3) % cellSize;
|
||||
const offsetY = (frame * 0.2) % cellSize;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: color,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -cellSize * 2,
|
||||
left: -cellSize * 2,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`,
|
||||
}}
|
||||
>
|
||||
{[...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>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Statement - "Your feelings are personal"
|
||||
const StatementScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const textProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
const textOpacity = interpolate(textProgress, [0, 1], [0, 1]);
|
||||
const textScale = interpolate(textProgress, [0, 1], [0.9, 1]);
|
||||
|
||||
const lockProgress = spring({
|
||||
frame: frame - 20,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 100 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #1e3a5f 0%, #1e293b 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 60,
|
||||
}}
|
||||
>
|
||||
{/* Lock icon */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 120,
|
||||
marginBottom: 40,
|
||||
transform: `scale(${interpolate(lockProgress, [0, 1], [0, 1])})`,
|
||||
}}
|
||||
>
|
||||
🔒
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 80,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textAlign: "center",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
opacity: textOpacity,
|
||||
transform: `scale(${textScale})`,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Your feelings
|
||||
<br />
|
||||
are personal
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: iCloud visualization
|
||||
const iCloudScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Data flowing animation
|
||||
const flowProgress = interpolate(frame, [0, fps * 4], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const titleProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
|
||||
// Create particles for data flow
|
||||
const particles = [...Array(12)].map((_, i) => {
|
||||
const delay = i * 5;
|
||||
const particleProgress = interpolate(
|
||||
frame - delay,
|
||||
[0, fps * 1.5],
|
||||
[0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
return {
|
||||
x: interpolate(particleProgress, [0, 1], [0, 300]),
|
||||
y: interpolate(particleProgress, [0, 0.5, 1], [0, -30, 0]) + i * 25,
|
||||
opacity: interpolate(particleProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0]),
|
||||
scale: interpolate(particleProgress, [0, 0.5, 1], [0.5, 1, 0.5]),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #059669 0%, #10b981 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(titleProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Encrypted & synced
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data flow visualization */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 80,
|
||||
}}
|
||||
>
|
||||
{/* Phone icon */}
|
||||
<div
|
||||
style={{
|
||||
width: 200,
|
||||
height: 350,
|
||||
backgroundColor: "rgba(255,255,255,0.15)",
|
||||
borderRadius: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 100,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
📱
|
||||
{/* Particles emanating */}
|
||||
{particles.map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -p.x - 50,
|
||||
top: p.y + 100,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "rgba(255,255,255,0.8)",
|
||||
opacity: p.opacity,
|
||||
transform: `scale(${p.scale})`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 80,
|
||||
color: "white",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
→
|
||||
</div>
|
||||
|
||||
{/* iCloud icon */}
|
||||
<div
|
||||
style={{
|
||||
width: 250,
|
||||
height: 250,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
borderRadius: 50,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 15,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 80 }}>☁️</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: 600,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
iCloud
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 150,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: 60,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
padding: "15px 25px",
|
||||
borderRadius: 30,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 28 }}>🔐</span>
|
||||
<span style={{ fontSize: 24, color: "white", fontWeight: 500 }}>
|
||||
End-to-end encrypted
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
padding: "15px 25px",
|
||||
borderRadius: 30,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 28 }}>🔄</span>
|
||||
<span style={{ fontSize: 24, color: "white", fontWeight: 500 }}>
|
||||
Syncs across devices
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Apple Health
|
||||
const HealthScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const badgeProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
|
||||
const titleProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #dc2626 0%, #ef4444 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(titleProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Works with Apple Health
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Health badge */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(badgeProgress, [0, 1], [0.5, 1])})`,
|
||||
opacity: interpolate(badgeProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderRadius: 60,
|
||||
padding: 60,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 30,
|
||||
boxShadow: "0 30px 60px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
{/* Apple Health icon approximation */}
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
height: 140,
|
||||
borderRadius: 35,
|
||||
background: "linear-gradient(180deg, #ff6b6b 0%, #ff4757 100%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 80 }}>❤️</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: 700,
|
||||
color: "#1a1a1a",
|
||||
}}
|
||||
>
|
||||
Apple Health
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#6b7280",
|
||||
textAlign: "center",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
Your mood data syncs with State of Mind
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: CTA
|
||||
const CTAScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoScale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
const textProgress = spring({
|
||||
frame: frame - 10,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
const glowIntensity = interpolate(Math.sin(frame * 0.1), [-1, 1], [0.3, 0.6]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #1e3a5f 0%, #1e293b 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, rgba(255,255,255,${glowIntensity}) 0%, transparent 70%)`,
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.3, 1])})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.5, 1])})`,
|
||||
}}
|
||||
>
|
||||
{/* Trust badges */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 30,
|
||||
marginBottom: 50,
|
||||
opacity: interpolate(textProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 60 }}>🔒</span>
|
||||
<span style={{ fontSize: 60 }}>☁️</span>
|
||||
<span style={{ fontSize: 60 }}>❤️</span>
|
||||
</div>
|
||||
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
borderRadius: 160 * 0.22,
|
||||
marginBottom: 30,
|
||||
filter: `drop-shadow(0 0 60px rgba(255,255,255,${glowIntensity}))`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 42,
|
||||
fontWeight: 700,
|
||||
color: "#10b981",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
marginTop: 20,
|
||||
opacity: interpolate(textProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
Private. Secure. Yours.
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 15 seconds total
|
||||
export const ConceptFPrivacyFirst: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(3.5 * fps)}>
|
||||
<StatementScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(3.5 * fps)} durationInFrames={Math.round(4.5 * fps)}>
|
||||
<iCloudScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(8 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<HealthScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(12 * fps)} durationInFrames={Math.round(3 * fps)}>
|
||||
<CTAScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
571
feels-promo/src/ConceptG-StreakEffect.tsx
Normal file
571
feels-promo/src/ConceptG-StreakEffect.tsx
Normal file
@@ -0,0 +1,571 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
Easing,
|
||||
} from "remotion";
|
||||
|
||||
// Shared tiled background component
|
||||
const TiledIconBackground: React.FC<{ color?: string }> = ({
|
||||
color = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
const iconSize = 80;
|
||||
const gap = 40;
|
||||
const cellSize = iconSize + gap;
|
||||
|
||||
const cols = Math.ceil(width / cellSize) + 4;
|
||||
const rows = Math.ceil(height / cellSize) + 4;
|
||||
|
||||
const offsetX = (frame * 0.3) % cellSize;
|
||||
const offsetY = (frame * 0.2) % cellSize;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: color,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -cellSize * 2,
|
||||
left: -cellSize * 2,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`,
|
||||
}}
|
||||
>
|
||||
{[...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>
|
||||
);
|
||||
};
|
||||
|
||||
// Confetti particle component
|
||||
const Confetti: React.FC<{ count: number; startFrame: number }> = ({
|
||||
count,
|
||||
startFrame,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, width, height } = useVideoConfig();
|
||||
|
||||
const localFrame = frame - startFrame;
|
||||
if (localFrame < 0) return null;
|
||||
|
||||
const colors = ["#fbbf24", "#ef4444", "#10b981", "#3b82f6", "#8b5cf6", "#ec4899"];
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...Array(count)].map((_, i) => {
|
||||
const seed = i * 1234.5678;
|
||||
const startX = (Math.sin(seed) * 0.5 + 0.5) * width;
|
||||
const startY = -50;
|
||||
const endX = startX + (Math.sin(seed * 2) * 200);
|
||||
const endY = height + 100;
|
||||
|
||||
const progress = interpolate(localFrame, [0, fps * 3], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.quad),
|
||||
});
|
||||
|
||||
const x = interpolate(progress, [0, 1], [startX, endX]);
|
||||
const y = interpolate(progress, [0, 1], [startY, endY]);
|
||||
const rotation = localFrame * (5 + (i % 10));
|
||||
const opacity = interpolate(progress, [0, 0.7, 1], [1, 1, 0]);
|
||||
const scale = 0.5 + (i % 5) * 0.2;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: x,
|
||||
top: y,
|
||||
width: 20,
|
||||
height: 20,
|
||||
backgroundColor: colors[i % colors.length],
|
||||
borderRadius: i % 2 === 0 ? "50%" : 4,
|
||||
transform: `rotate(${rotation}deg) scale(${scale})`,
|
||||
opacity,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Streak counter building up
|
||||
const StreakBuildScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Count up from 0 to target
|
||||
const targetStreak = 30;
|
||||
const countProgress = interpolate(frame, [fps * 0.5, fps * 4], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
});
|
||||
const currentCount = Math.round(countProgress * targetStreak);
|
||||
|
||||
const titleProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
|
||||
// Fire emoji scale on each increment
|
||||
const fireScale = interpolate(
|
||||
Math.sin(frame * 0.5),
|
||||
[-1, 1],
|
||||
[0.9, 1.1]
|
||||
);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #f59e0b 0%, #ef4444 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(titleProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Build your streak
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Streak counter */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 120,
|
||||
transform: `scale(${fireScale})`,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
🔥
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 200,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 60px rgba(0,0,0,0.3)",
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{currentCount}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 600,
|
||||
color: "rgba(255,255,255,0.9)",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
day streak
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 100,
|
||||
right: 100,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "rgba(255,255,255,0.3)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${countProgress * 100}%`,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 10,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: Calendar filling
|
||||
const CalendarScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const titleProgress = spring({ frame, fps, config: { damping: 200 } });
|
||||
|
||||
// Calendar grid
|
||||
const days = 35;
|
||||
const cols = 7;
|
||||
const fillProgress = interpolate(frame, [0, fps * 4], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const filledDays = Math.floor(fillProgress * 30);
|
||||
|
||||
const MOOD_COLORS = ["#10b981", "#22c55e", "#fbbf24", "#10b981", "#22c55e"];
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #059669 0%, #10b981 100%)" />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(titleProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Watch it grow
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar grid */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${cols}, 100px)`,
|
||||
gap: 15,
|
||||
padding: 40,
|
||||
backgroundColor: "rgba(0,0,0,0.2)",
|
||||
borderRadius: 30,
|
||||
}}
|
||||
>
|
||||
{/* Day labels */}
|
||||
{["S", "M", "T", "W", "T", "F", "S"].map((day, i) => (
|
||||
<div
|
||||
key={`label-${i}`}
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
color: "rgba(255,255,255,0.6)",
|
||||
textAlign: "center",
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Days */}
|
||||
{[...Array(days)].map((_, i) => {
|
||||
const isFilled = i < filledDays;
|
||||
const cellProgress = spring({
|
||||
frame: frame - i * 2,
|
||||
fps,
|
||||
config: { damping: 15, stiffness: 150 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 20,
|
||||
backgroundColor: isFilled
|
||||
? MOOD_COLORS[i % MOOD_COLORS.length]
|
||||
: "rgba(255,255,255,0.1)",
|
||||
transform: `scale(${isFilled ? interpolate(cellProgress, [0, 1], [0.5, 1]) : 1})`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
color: isFilled ? "white" : "rgba(255,255,255,0.3)",
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Milestone celebration
|
||||
const CelebrationScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const badgeProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
const textProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ overflow: "hidden" }}>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #7c3aed 0%, #a855f7 100%)" />
|
||||
|
||||
{/* Confetti */}
|
||||
<Confetti count={50} startFrame={10} />
|
||||
|
||||
{/* Achievement badge */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(badgeProgress, [0, 1], [0, 1])})`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderRadius: 60,
|
||||
padding: 60,
|
||||
textAlign: "center",
|
||||
boxShadow: "0 30px 60px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 100, marginBottom: 20 }}>🏆</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 800,
|
||||
color: "#1a1a1a",
|
||||
marginBottom: 15,
|
||||
}}
|
||||
>
|
||||
30 Day Streak!
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#6b7280",
|
||||
}}
|
||||
>
|
||||
You're on fire!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 150,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(textProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: 600,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
Celebrate every milestone
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: CTA
|
||||
const CTAScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoScale = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
const textProgress = spring({
|
||||
frame: frame - 15,
|
||||
fps,
|
||||
config: { damping: 200 },
|
||||
});
|
||||
|
||||
const glowIntensity = interpolate(Math.sin(frame * 0.1), [-1, 1], [0.3, 0.6]);
|
||||
|
||||
// Pulsing fire
|
||||
const fireScale = interpolate(Math.sin(frame * 0.15), [-1, 1], [0.9, 1.1]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TiledIconBackground color="linear-gradient(180deg, #f59e0b 0%, #ef4444 100%)" />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 400,
|
||||
height: 400,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, rgba(255,255,255,${glowIntensity}) 0%, transparent 70%)`,
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.3, 1])})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
transform: `scale(${interpolate(logoScale, [0, 1], [0.5, 1])})`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 100,
|
||||
marginBottom: 30,
|
||||
transform: `scale(${fireScale})`,
|
||||
}}
|
||||
>
|
||||
🔥
|
||||
</div>
|
||||
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
borderRadius: 160 * 0.22,
|
||||
marginBottom: 30,
|
||||
filter: `drop-shadow(0 0 60px rgba(255,255,255,${glowIntensity}))`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 4px 30px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 42,
|
||||
fontWeight: 700,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
marginTop: 20,
|
||||
opacity: interpolate(textProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
How long can you go?
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 20 seconds total
|
||||
export const ConceptGStreakEffect: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(5 * fps)}>
|
||||
<StreakBuildScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(5 * fps)} durationInFrames={Math.round(6 * fps)}>
|
||||
<CalendarScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(11 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<CelebrationScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(16 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<CTAScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
788
feels-promo/src/ConceptH-MoodHeist.tsx
Normal file
788
feels-promo/src/ConceptH-MoodHeist.tsx
Normal file
@@ -0,0 +1,788 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
Easing,
|
||||
} from "remotion";
|
||||
|
||||
// Cinematic letterbox bars
|
||||
const LetterBox: React.FC = () => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 120,
|
||||
background: "black",
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 120,
|
||||
background: "black",
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
// Scan lines for that heist movie feel
|
||||
const ScanLines: React.FC<{ opacity?: number }> = ({ opacity = 0.1 }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const offset = (frame * 2) % 4;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: `repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0,0,0,${opacity}) 2px,
|
||||
rgba(0,0,0,${opacity}) 4px
|
||||
)`,
|
||||
transform: `translateY(${offset}px)`,
|
||||
pointerEvents: "none",
|
||||
zIndex: 50,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Glitch effect text
|
||||
const GlitchText: React.FC<{
|
||||
children: string;
|
||||
style?: React.CSSProperties;
|
||||
}> = ({ children, style }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const glitchOffset = Math.sin(frame * 0.5) * 2;
|
||||
const shouldGlitch = frame % 30 < 3;
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", ...style }}>
|
||||
{shouldGlitch && (
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: glitchOffset,
|
||||
color: "#ff0000",
|
||||
opacity: 0.8,
|
||||
clipPath: "inset(10% 0 60% 0)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: -glitchOffset,
|
||||
color: "#00ffff",
|
||||
opacity: 0.8,
|
||||
clipPath: "inset(60% 0 10% 0)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span style={{ position: "relative" }}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: The Setup - "They took something from you"
|
||||
const SetupScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const fadeIn = interpolate(frame, [0, fps * 0.5], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const textReveal = interpolate(frame, [fps * 0.5, fps * 2], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const typewriterLength = Math.floor(textReveal * 28);
|
||||
const fullText = "They took something from you";
|
||||
const displayText = fullText.slice(0, typewriterLength);
|
||||
|
||||
// Flicker effect
|
||||
const flicker = frame % 60 < 2 ? 0.3 : 1;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundColor: "#0a0a0a",
|
||||
opacity: fadeIn * flicker,
|
||||
}}
|
||||
>
|
||||
<ScanLines opacity={0.15} />
|
||||
<LetterBox />
|
||||
|
||||
{/* Dramatic spotlight */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
width: 800,
|
||||
height: 800,
|
||||
transform: "translate(-50%, -50%)",
|
||||
background:
|
||||
"radial-gradient(circle, rgba(255,255,255,0.05) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Main text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<GlitchText
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 300,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
letterSpacing: 8,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
</GlitchText>
|
||||
|
||||
{/* Blinking cursor */}
|
||||
{typewriterLength < fullText.length && (
|
||||
<span
|
||||
style={{
|
||||
opacity: frame % 30 < 15 ? 1 : 0,
|
||||
color: "#ef4444",
|
||||
fontSize: 72,
|
||||
fontWeight: 300,
|
||||
}}
|
||||
>
|
||||
_
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 180,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(frame, [fps * 3, fps * 4], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#ef4444",
|
||||
fontFamily: "monospace",
|
||||
letterSpacing: 4,
|
||||
}}
|
||||
>
|
||||
YOUR EMOTIONS
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: The Crew - Mood emojis as heist team
|
||||
const CrewScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const crew = [
|
||||
{ emoji: "😊", name: "THE OPTIMIST", color: "#10b981", role: "INFILTRATION" },
|
||||
{ emoji: "😤", name: "THE MUSCLE", color: "#ef4444", role: "FIREPOWER" },
|
||||
{ emoji: "🤔", name: "THE BRAINS", color: "#3b82f6", role: "STRATEGY" },
|
||||
{ emoji: "😌", name: "THE COOL", color: "#8b5cf6", role: "EXTRACTION" },
|
||||
];
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
<ScanLines opacity={0.1} />
|
||||
<LetterBox />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 180,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "#fbbf24",
|
||||
fontFamily: "monospace",
|
||||
letterSpacing: 8,
|
||||
opacity: interpolate(frame, [0, fps * 0.5], [0, 1]),
|
||||
}}
|
||||
>
|
||||
ASSEMBLING THE CREW
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Crew grid */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: 60,
|
||||
}}
|
||||
>
|
||||
{crew.map((member, i) => {
|
||||
const delay = i * 8;
|
||||
const memberProgress = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 100 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={member.name}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
opacity: interpolate(memberProgress, [0, 1], [0, 1]),
|
||||
transform: `translateY(${interpolate(memberProgress, [0, 1], [50, 0])}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 100,
|
||||
marginBottom: 15,
|
||||
filter: `drop-shadow(0 0 20px ${member.color})`,
|
||||
}}
|
||||
>
|
||||
{member.emoji}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: member.color,
|
||||
fontFamily: "monospace",
|
||||
fontWeight: 700,
|
||||
letterSpacing: 2,
|
||||
}}
|
||||
>
|
||||
{member.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "#666",
|
||||
fontFamily: "monospace",
|
||||
marginTop: 5,
|
||||
}}
|
||||
>
|
||||
{member.role}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: The Plan - Blueprint style
|
||||
const PlanScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, width, height } = useVideoConfig();
|
||||
|
||||
// Grid lines drawing animation
|
||||
const gridProgress = interpolate(frame, [0, fps * 2], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a1628" }}>
|
||||
<ScanLines opacity={0.05} />
|
||||
<LetterBox />
|
||||
|
||||
{/* Blueprint grid */}
|
||||
<svg
|
||||
style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }}
|
||||
>
|
||||
{/* Vertical lines */}
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<line
|
||||
key={`v-${i}`}
|
||||
x1={(i * width) / 20}
|
||||
y1={0}
|
||||
x2={(i * width) / 20}
|
||||
y2={height * gridProgress}
|
||||
stroke="#1e40af"
|
||||
strokeWidth={0.5}
|
||||
opacity={0.3}
|
||||
/>
|
||||
))}
|
||||
{/* Horizontal lines */}
|
||||
{[...Array(30)].map((_, i) => (
|
||||
<line
|
||||
key={`h-${i}`}
|
||||
x1={0}
|
||||
y1={(i * height) / 30}
|
||||
x2={width * gridProgress}
|
||||
y2={(i * height) / 30}
|
||||
stroke="#1e40af"
|
||||
strokeWidth={0.5}
|
||||
opacity={0.3}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
{/* Phone blueprint */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
opacity: interpolate(frame, [fps, fps * 2], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 300,
|
||||
height: 600,
|
||||
border: "2px solid #3b82f6",
|
||||
borderRadius: 40,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Target markers */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "30%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 100,
|
||||
height: 100,
|
||||
border: "2px dashed #ef4444",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
fontSize: 14,
|
||||
color: "#ef4444",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
TARGET
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entry point */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 80,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
fontSize: 12,
|
||||
color: "#10b981",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
ENTRY POINT
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 180,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "#3b82f6",
|
||||
fontFamily: "monospace",
|
||||
letterSpacing: 8,
|
||||
}}
|
||||
>
|
||||
THE PLAN
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 60,
|
||||
right: 60,
|
||||
display: "flex",
|
||||
justifyContent: "space-around",
|
||||
}}
|
||||
>
|
||||
{["DOWNLOAD", "TAP MOOD", "REPEAT"].map((step, i) => (
|
||||
<div
|
||||
key={step}
|
||||
style={{
|
||||
opacity: interpolate(
|
||||
frame,
|
||||
[fps * 2 + i * 10, fps * 2.5 + i * 10],
|
||||
[0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
color: "#fbbf24",
|
||||
fontFamily: "monospace",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "#94a3b8",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{step}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: The Execution - Dramatic mood selection
|
||||
const ExecutionScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const moods = ["😢", "😕", "😐", "🙂", "😊"];
|
||||
const selectedIndex = 4; // Great mood
|
||||
|
||||
// Dramatic slow reveal
|
||||
const revealProgress = interpolate(frame, [0, fps * 2], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
});
|
||||
|
||||
// Finger approaching
|
||||
const fingerProgress = interpolate(frame, [fps * 2, fps * 3], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
// Selection flash
|
||||
const selectFlash = frame > fps * 3 && frame < fps * 3.5;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
<ScanLines opacity={0.1} />
|
||||
<LetterBox />
|
||||
|
||||
{/* Dramatic lighting */}
|
||||
{selectFlash && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(16, 185, 129, 0.3)",
|
||||
zIndex: 10,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mood buttons */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
gap: 30,
|
||||
}}
|
||||
>
|
||||
{moods.map((mood, i) => {
|
||||
const isSelected = i === selectedIndex && frame > fps * 3;
|
||||
const buttonProgress = interpolate(
|
||||
revealProgress,
|
||||
[i * 0.15, i * 0.15 + 0.3],
|
||||
[0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 30,
|
||||
backgroundColor: isSelected ? "#10b981" : "rgba(255,255,255,0.1)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 60,
|
||||
opacity: buttonProgress,
|
||||
transform: `scale(${isSelected ? 1.2 : 1}) translateY(${interpolate(buttonProgress, [0, 1], [50, 0])}px)`,
|
||||
boxShadow: isSelected ? "0 0 60px #10b981" : "none",
|
||||
transition: "all 0.3s",
|
||||
}}
|
||||
>
|
||||
{mood}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Approaching finger/cursor */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "60%",
|
||||
left: `${50 + 20 - fingerProgress * 8}%`,
|
||||
fontSize: 80,
|
||||
transform: `translateX(-50%) rotate(-30deg)`,
|
||||
opacity: fingerProgress < 1 ? fingerProgress : 0,
|
||||
}}
|
||||
>
|
||||
👆
|
||||
</div>
|
||||
|
||||
{/* "ACQUIRED" text */}
|
||||
{frame > fps * 3.2 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 250,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<GlitchText
|
||||
style={{
|
||||
fontSize: 48,
|
||||
color: "#10b981",
|
||||
fontFamily: "monospace",
|
||||
letterSpacing: 16,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
MOOD ACQUIRED
|
||||
</GlitchText>
|
||||
</div>
|
||||
)}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 5: The Getaway - Success celebration
|
||||
const GetawayScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
// Vault door opening effect
|
||||
const vaultOpen = interpolate(frame, [0, fps], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
<ScanLines opacity={0.05} />
|
||||
<LetterBox />
|
||||
|
||||
{/* Vault doors */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "50%",
|
||||
height: "100%",
|
||||
backgroundColor: "#1a1a1a",
|
||||
transform: `translateX(${-vaultOpen * 100}%)`,
|
||||
borderRight: "4px solid #333",
|
||||
zIndex: 20,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: "50%",
|
||||
height: "100%",
|
||||
backgroundColor: "#1a1a1a",
|
||||
transform: `translateX(${vaultOpen * 100}%)`,
|
||||
borderLeft: "4px solid #333",
|
||||
zIndex: 20,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bright light behind vault */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 600,
|
||||
height: 600,
|
||||
background: `radial-gradient(circle, rgba(251,191,36,${vaultOpen * 0.5}) 0%, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* App icon revealed */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(logoProgress, [0, 1], [0.5, 1])})`,
|
||||
textAlign: "center",
|
||||
opacity: vaultOpen,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: 200 * 0.22,
|
||||
marginBottom: 40,
|
||||
filter: `drop-shadow(0 0 60px rgba(251,191,36,0.8))`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 80,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
textShadow: "0 0 40px rgba(255,255,255,0.5)",
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#fbbf24",
|
||||
fontFamily: "monospace",
|
||||
marginTop: 20,
|
||||
letterSpacing: 4,
|
||||
opacity: interpolate(frame, [fps * 2, fps * 2.5], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
TAKE BACK YOUR EMOTIONS
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 25 seconds total
|
||||
export const ConceptHMoodHeist: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(5 * fps)}>
|
||||
<SetupScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(5 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<CrewScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(10 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<PlanScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(15 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<ExecutionScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(20 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<GetawayScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
833
feels-promo/src/ConceptI-RetroArcade.tsx
Normal file
833
feels-promo/src/ConceptI-RetroArcade.tsx
Normal file
@@ -0,0 +1,833 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
Easing,
|
||||
} from "remotion";
|
||||
|
||||
// Pixel font style helper
|
||||
const pixelText: React.CSSProperties = {
|
||||
fontFamily: "'Press Start 2P', 'Courier New', monospace",
|
||||
imageRendering: "pixelated",
|
||||
textShadow: "4px 4px 0 #000",
|
||||
};
|
||||
|
||||
// CRT screen effect
|
||||
const CRTEffect: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Scan lines */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: `repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0,0,0,0.3) 2px,
|
||||
rgba(0,0,0,0.3) 4px
|
||||
)`,
|
||||
pointerEvents: "none",
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
{/* Screen flicker */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(255,255,255,0.02)",
|
||||
opacity: Math.sin(frame * 0.5) > 0.9 ? 1 : 0,
|
||||
pointerEvents: "none",
|
||||
zIndex: 101,
|
||||
}}
|
||||
/>
|
||||
{/* Vignette */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background:
|
||||
"radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.5) 100%)",
|
||||
pointerEvents: "none",
|
||||
zIndex: 99,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Pixel art mood character
|
||||
const MoodSprite: React.FC<{
|
||||
mood: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size?: number;
|
||||
bounce?: boolean;
|
||||
}> = ({ mood, x, y, size = 80, bounce = true }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const colors = ["#ef4444", "#f97316", "#fbbf24", "#22c55e", "#10b981"];
|
||||
const faces = [":(", ":/", ":|", ":)", ":D"];
|
||||
|
||||
const bounceY = bounce ? Math.sin(frame * 0.3) * 5 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: x,
|
||||
top: y + bounceY,
|
||||
width: size,
|
||||
height: size,
|
||||
backgroundColor: colors[mood],
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: "4px solid #000",
|
||||
boxShadow: "4px 4px 0 #000",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: size * 0.4,
|
||||
...pixelText,
|
||||
color: "#000",
|
||||
textShadow: "none",
|
||||
}}
|
||||
>
|
||||
{faces[mood]}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Insert Coin
|
||||
const InsertCoinScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const blink = frame % 30 < 20;
|
||||
|
||||
const titleDrop = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 8, stiffness: 100 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}>
|
||||
<CRTEffect />
|
||||
|
||||
{/* Starfield background */}
|
||||
{[...Array(50)].map((_, i) => {
|
||||
const x = (i * 137.5) % 100;
|
||||
const y = (i * 73.3 + frame * 0.5) % 100;
|
||||
const size = (i % 3) + 1;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${x}%`,
|
||||
top: `${y}%`,
|
||||
width: size * 2,
|
||||
height: size * 2,
|
||||
backgroundColor: "#fff",
|
||||
opacity: 0.5 + (i % 5) * 0.1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
transform: `translateY(${interpolate(titleDrop, [0, 1], [-200, 0])}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 80,
|
||||
color: "#fbbf24",
|
||||
...pixelText,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
FEELS
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#10b981",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
THE GAME
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mood characters parade */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
gap: 20,
|
||||
}}
|
||||
>
|
||||
{[0, 1, 2, 3, 4].map((mood, i) => {
|
||||
const delay = i * 5;
|
||||
const appearProgress = spring({
|
||||
frame: frame - delay - 15,
|
||||
fps,
|
||||
config: { damping: 12 },
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={mood}
|
||||
style={{
|
||||
transform: `scale(${interpolate(appearProgress, [0, 1], [0, 1])})`,
|
||||
}}
|
||||
>
|
||||
<MoodSprite mood={mood} x={0} y={0} size={100} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Insert Coin */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 300,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: blink ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#fff",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
INSERT COIN
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Credits */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
© 2025 FEELS CORP
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: Gameplay - Catching moods
|
||||
const GameplayScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, width, height } = useVideoConfig();
|
||||
|
||||
// Player position (bottom of screen)
|
||||
const playerX = width / 2 - 50 + Math.sin(frame * 0.1) * 200;
|
||||
|
||||
// Falling moods
|
||||
const fallingMoods = [
|
||||
{ mood: 4, startX: 200, delay: 0 },
|
||||
{ mood: 3, startX: 500, delay: 20 },
|
||||
{ mood: 2, startX: 800, delay: 40 },
|
||||
{ mood: 4, startX: 350, delay: 60 },
|
||||
{ mood: 3, startX: 650, delay: 80 },
|
||||
];
|
||||
|
||||
// Score counter
|
||||
const score = Math.floor(interpolate(frame, [0, fps * 4], [0, 9999], {
|
||||
extrapolateRight: "clamp",
|
||||
}));
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#16213e" }}>
|
||||
<CRTEffect />
|
||||
|
||||
{/* HUD */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 40,
|
||||
left: 40,
|
||||
right: 40,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 20, color: "#fff", ...pixelText }}>
|
||||
SCORE: {score.toString().padStart(5, "0")}
|
||||
</div>
|
||||
<div style={{ fontSize: 20, color: "#ef4444", ...pixelText }}>
|
||||
♥♥♥
|
||||
</div>
|
||||
<div style={{ fontSize: 20, color: "#fbbf24", ...pixelText }}>
|
||||
STREAK: 7
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Falling moods */}
|
||||
{fallingMoods.map((item, i) => {
|
||||
const fallProgress = interpolate(
|
||||
frame - item.delay,
|
||||
[0, fps * 2],
|
||||
[0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
const y = interpolate(fallProgress, [0, 1], [-100, height - 300]);
|
||||
|
||||
return (
|
||||
<MoodSprite
|
||||
key={i}
|
||||
mood={item.mood}
|
||||
x={item.startX}
|
||||
y={y}
|
||||
size={80}
|
||||
bounce={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Player basket */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: playerX,
|
||||
bottom: 150,
|
||||
width: 100,
|
||||
height: 80,
|
||||
backgroundColor: "#3b82f6",
|
||||
border: "4px solid #000",
|
||||
borderRadius: "0 0 20px 20px",
|
||||
boxShadow: "4px 4px 0 #000",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -30,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
fontSize: 40,
|
||||
}}
|
||||
>
|
||||
🧺
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ground */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 100,
|
||||
backgroundColor: "#2d3748",
|
||||
borderTop: "4px solid #4a5568",
|
||||
}}
|
||||
>
|
||||
{/* Ground pattern */}
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: i * 60,
|
||||
top: 20,
|
||||
width: 40,
|
||||
height: 20,
|
||||
backgroundColor: "#3d4a5c",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* "CATCH THE GOOD VIBES" */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 150,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#10b981",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
CATCH THE GOOD VIBES!
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Power Up - Streak bonus
|
||||
const PowerUpScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const powerUpPulse = Math.sin(frame * 0.3) * 0.2 + 1;
|
||||
const rotateSpeed = frame * 3;
|
||||
|
||||
const streakNumber = Math.floor(
|
||||
interpolate(frame, [0, fps * 2], [7, 30], {
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0f0f23" }}>
|
||||
<CRTEffect />
|
||||
|
||||
{/* Explosion rays */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) rotate(${rotateSpeed}deg)`,
|
||||
}}
|
||||
>
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 20,
|
||||
height: 600,
|
||||
backgroundColor: i % 2 === 0 ? "#fbbf24" : "#f97316",
|
||||
transform: `rotate(${i * 30}deg)`,
|
||||
transformOrigin: "center center",
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Power up text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 250,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
color: "#fbbf24",
|
||||
...pixelText,
|
||||
transform: `scale(${powerUpPulse})`,
|
||||
}}
|
||||
>
|
||||
POWER UP!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Giant streak number */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${powerUpPulse})`,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 200,
|
||||
color: "#fff",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
{streakNumber}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "#10b981",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
DAY STREAK!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bonus items floating */}
|
||||
{["🔥", "⭐", "💎", "🏆"].map((emoji, i) => {
|
||||
const angle = (frame * 2 + i * 90) * (Math.PI / 180);
|
||||
const radius = 300;
|
||||
const x = Math.cos(angle) * radius;
|
||||
const y = Math.sin(angle) * radius * 0.5;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(calc(-50% + ${x}px), calc(-50% + ${y}px))`,
|
||||
fontSize: 60,
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* XP bonus */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 300,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#a78bfa",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
+500 XP BONUS!
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: High Score - Leaderboard
|
||||
const HighScoreScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const scores = [
|
||||
{ rank: 1, name: "YOU", score: 99999, isPlayer: true },
|
||||
{ rank: 2, name: "PRO", score: 88420, isPlayer: false },
|
||||
{ rank: 3, name: "ACE", score: 77350, isPlayer: false },
|
||||
{ rank: 4, name: "ZEN", score: 66100, isPlayer: false },
|
||||
{ rank: 5, name: "JOY", score: 55000, isPlayer: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}>
|
||||
<CRTEffect />
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 150,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
color: "#fbbf24",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
HIGH SCORES
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leaderboard */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 350,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
}}
|
||||
>
|
||||
{scores.map((entry, i) => {
|
||||
const delay = i * 8;
|
||||
const rowProgress = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 12 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 40,
|
||||
marginBottom: 30,
|
||||
opacity: interpolate(rowProgress, [0, 1], [0, 1]),
|
||||
transform: `translateX(${interpolate(rowProgress, [0, 1], [-100, 0])}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: entry.rank === 1 ? "#fbbf24" : "#666",
|
||||
...pixelText,
|
||||
width: 60,
|
||||
}}
|
||||
>
|
||||
{entry.rank}.
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: entry.isPlayer ? "#10b981" : "#fff",
|
||||
...pixelText,
|
||||
width: 150,
|
||||
}}
|
||||
>
|
||||
{entry.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: entry.isPlayer ? "#10b981" : "#fff",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
{entry.score.toLocaleString()}
|
||||
</div>
|
||||
{entry.isPlayer && (
|
||||
<span style={{ fontSize: 28, marginLeft: 10 }}>👑</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* New high score flash */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 250,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: frame % 20 < 15 ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "#ef4444",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
★ NEW HIGH SCORE ★
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 5: Game Over - CTA
|
||||
const GameOverScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
const blink = frame % 40 < 30;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
<CRTEffect />
|
||||
|
||||
{/* Pixelated app icon */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "40%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(logoProgress, [0, 1], [0.5, 1])})`,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 180 * 0.22,
|
||||
border: "6px solid #fff",
|
||||
boxShadow: "8px 8px 0 #000",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "60%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
color: "#10b981",
|
||||
...pixelText,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
FEELS
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#fbbf24",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
LEVEL UP YOUR MOOD
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Press Start */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 300,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: blink ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#fff",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
PRESS START
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download prompt */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
...pixelText,
|
||||
}}
|
||||
>
|
||||
DOWNLOAD NOW ON APP STORE
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 20 seconds total
|
||||
export const ConceptIRetroArcade: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(4 * fps)}>
|
||||
<InsertCoinScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(4 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<GameplayScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(9 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<PowerUpScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(13 * fps)} durationInFrames={Math.round(3 * fps)}>
|
||||
<HighScoreScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(16 * fps)} durationInFrames={Math.round(4 * fps)}>
|
||||
<GameOverScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
609
feels-promo/src/ConceptJ-Conspiracy.tsx
Normal file
609
feels-promo/src/ConceptJ-Conspiracy.tsx
Normal file
@@ -0,0 +1,609 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
Easing,
|
||||
} from "remotion";
|
||||
|
||||
// Film grain effect
|
||||
const FilmGrain: React.FC<{ intensity?: number }> = ({ intensity = 0.15 }) => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
opacity: intensity,
|
||||
background: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' seed='${frame}' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
|
||||
pointerEvents: "none",
|
||||
zIndex: 200,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Redacted text bar
|
||||
const Redacted: React.FC<{ width: number }> = ({ width }) => (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width,
|
||||
height: "1em",
|
||||
backgroundColor: "#000",
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Glitch/static overlay
|
||||
const StaticOverlay: React.FC<{ active: boolean }> = ({ active }) => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "#fff",
|
||||
opacity: 0.1 + Math.random() * 0.2,
|
||||
zIndex: 150,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Document stamp
|
||||
const ClassifiedStamp: React.FC<{ delay?: number }> = ({ delay = 0 }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const stampProgress = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 8, stiffness: 200 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) rotate(-15deg) scale(${interpolate(stampProgress, [0, 1], [3, 1])})`,
|
||||
opacity: interpolate(stampProgress, [0, 0.5, 1], [0, 1, 1]),
|
||||
border: "8px solid #ef4444",
|
||||
padding: "20px 40px",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 900,
|
||||
color: "#ef4444",
|
||||
fontFamily: "Impact, sans-serif",
|
||||
letterSpacing: 8,
|
||||
}}
|
||||
>
|
||||
CLASSIFIED
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: The Hook - "They don't want you to know"
|
||||
const HookScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const staticActive = frame < fps * 0.5 || (frame > fps * 2 && frame < fps * 2.3);
|
||||
|
||||
const textOpacity = interpolate(frame, [fps * 0.5, fps * 1], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const zoomIn = interpolate(frame, [0, fps * 4], [1, 1.1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
<FilmGrain intensity={0.2} />
|
||||
<StaticOverlay active={staticActive} />
|
||||
|
||||
{/* Dark vignette */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background:
|
||||
"radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.8) 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Ominous text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${zoomIn})`,
|
||||
textAlign: "center",
|
||||
opacity: textOpacity,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#666",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
marginBottom: 40,
|
||||
letterSpacing: 4,
|
||||
}}
|
||||
>
|
||||
WHAT IF EVERYTHING YOU KNEW
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
color: "#fff",
|
||||
fontWeight: 900,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
letterSpacing: 2,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
WAS A LIE?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timestamp */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
right: 60,
|
||||
fontSize: 14,
|
||||
color: "#ef4444",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
DOC-7X-{frame.toString().padStart(4, "0")}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: The Evidence - Redacted documents
|
||||
const EvidenceScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const docSlide = interpolate(frame, [0, fps], [100, 0], {
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#1a1a1a" }}>
|
||||
<FilmGrain intensity={0.15} />
|
||||
|
||||
{/* Cork board background */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "#8B4513",
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Document */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) translateY(${docSlide}px) rotate(-2deg)`,
|
||||
width: 700,
|
||||
backgroundColor: "#f5f5dc",
|
||||
padding: 60,
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.5)",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "#333",
|
||||
fontFamily: "Courier New, monospace",
|
||||
marginBottom: 20,
|
||||
borderBottom: "1px solid #999",
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
INTERNAL MEMO - RESTRICTED ACCESS
|
||||
</div>
|
||||
|
||||
{/* Body text with redactions */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: "#222",
|
||||
fontFamily: "Georgia, serif",
|
||||
lineHeight: 2,
|
||||
}}
|
||||
>
|
||||
Subject: Emotional Awareness Initiative
|
||||
<br />
|
||||
<br />
|
||||
The <Redacted width={120} /> has determined that widespread
|
||||
<Redacted width={180} /> of emotional patterns could lead to
|
||||
<Redacted width={80} /> self-improvement. This is
|
||||
<Redacted width={100} /> to our interests.
|
||||
<br />
|
||||
<br />
|
||||
Recommendation: Continue suppression of <Redacted width={150} />
|
||||
tracking tools.
|
||||
</div>
|
||||
|
||||
{/* Classified stamp */}
|
||||
<ClassifiedStamp delay={fps} />
|
||||
</div>
|
||||
|
||||
{/* Red strings connecting */}
|
||||
<svg
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<line
|
||||
x1="100"
|
||||
y1="200"
|
||||
x2="400"
|
||||
y2="400"
|
||||
stroke="#ef4444"
|
||||
strokeWidth="2"
|
||||
opacity="0.6"
|
||||
/>
|
||||
<line
|
||||
x1="900"
|
||||
y1="300"
|
||||
x2="650"
|
||||
y2="500"
|
||||
stroke="#ef4444"
|
||||
strokeWidth="2"
|
||||
opacity="0.6"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Narration text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 150,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(frame, [fps * 2, fps * 2.5], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#fff",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
"They don't want you to understand yourself."
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: The Truth - Revelation
|
||||
const TruthScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const revealProgress = interpolate(frame, [0, fps * 2], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const glitchActive = frame % 45 < 3;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#000" }}>
|
||||
<FilmGrain intensity={0.25} />
|
||||
<StaticOverlay active={glitchActive} />
|
||||
|
||||
{/* Dramatic light rays */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "50%",
|
||||
width: 400,
|
||||
height: "100%",
|
||||
background:
|
||||
"linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.05) 50%, transparent 100%)",
|
||||
transform: "translateX(-50%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* The truth revealed */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "30%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: "#ef4444",
|
||||
fontFamily: "monospace",
|
||||
letterSpacing: 8,
|
||||
marginBottom: 40,
|
||||
opacity: interpolate(revealProgress, [0, 0.3], [0, 1]),
|
||||
}}
|
||||
>
|
||||
THE TRUTH IS
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
color: "#fff",
|
||||
fontWeight: 900,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
opacity: interpolate(revealProgress, [0.3, 0.6], [0, 1]),
|
||||
}}
|
||||
>
|
||||
YOUR EMOTIONS
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
color: "#10b981",
|
||||
fontWeight: 900,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
marginTop: 20,
|
||||
opacity: interpolate(revealProgress, [0.6, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
MATTER
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(frame, [fps * 3, fps * 3.5], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#666",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
And there's an app that knows it.
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: The Solution - Feels reveal
|
||||
const SolutionScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
|
||||
// Typing effect for tagline
|
||||
const tagline = "Track your truth.";
|
||||
const typedLength = Math.floor(
|
||||
interpolate(frame, [fps * 1.5, fps * 3], [0, tagline.length], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
<FilmGrain intensity={0.1} />
|
||||
|
||||
{/* Spotlight effect */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "40%",
|
||||
left: "50%",
|
||||
width: 600,
|
||||
height: 600,
|
||||
transform: "translate(-50%, -50%)",
|
||||
background:
|
||||
"radial-gradient(circle, rgba(16,185,129,0.2) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* App icon */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "35%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(logoProgress, [0, 1], [0.5, 1])})`,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: 200 * 0.22,
|
||||
boxShadow: "0 0 100px rgba(16,185,129,0.5)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* App name */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "58%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 80,
|
||||
fontWeight: 800,
|
||||
color: "#fff",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
opacity: interpolate(logoProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
|
||||
{/* Typed tagline */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "#10b981",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
marginTop: 30,
|
||||
minHeight: 50,
|
||||
}}
|
||||
>
|
||||
{tagline.slice(0, typedLength)}
|
||||
<span style={{ opacity: frame % 30 < 15 ? 1 : 0 }}>|</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* "Wake up" text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(frame, [fps * 3.5, fps * 4], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: "#ef4444",
|
||||
fontFamily: "monospace",
|
||||
letterSpacing: 8,
|
||||
}}
|
||||
>
|
||||
WAKE UP. DOWNLOAD NOW.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File number */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
right: 60,
|
||||
fontSize: 12,
|
||||
color: "#333",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
CASE CLOSED
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 20 seconds total
|
||||
export const ConceptJConspiracy: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(4.5 * fps)}>
|
||||
<HookScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(4.5 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<EvidenceScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(9.5 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<TruthScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(14.5 * fps)} durationInFrames={Math.round(5.5 * fps)}>
|
||||
<SolutionScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
701
feels-promo/src/ConceptK-SportsCenter.tsx
Normal file
701
feels-promo/src/ConceptK-SportsCenter.tsx
Normal file
@@ -0,0 +1,701 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
Easing,
|
||||
} from "remotion";
|
||||
|
||||
// ESPN-style colors
|
||||
const ESPN_RED = "#cc0000";
|
||||
const ESPN_DARK = "#1a1a1a";
|
||||
const ESPN_YELLOW = "#ffc629";
|
||||
|
||||
// Breaking news ticker
|
||||
const NewsTicker: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width } = useVideoConfig();
|
||||
|
||||
const headlines = [
|
||||
"BREAKING: Local user hits 30-day streak, experts baffled",
|
||||
"MOOD ALERT: Wednesday showing signs of improvement league-wide",
|
||||
"TRADE DEADLINE: Bad vibes traded for good vibes in blockbuster deal",
|
||||
"INJURY REPORT: Monday motivation OUT indefinitely",
|
||||
"STATS: Average mood up 15% since app download",
|
||||
];
|
||||
|
||||
const tickerText = headlines.join(" • ");
|
||||
const tickerWidth = tickerText.length * 14;
|
||||
const offset = (frame * 3) % (tickerWidth + width);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 60,
|
||||
backgroundColor: ESPN_RED,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
width: 200,
|
||||
height: "100%",
|
||||
backgroundColor: ESPN_DARK,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 18,
|
||||
fontWeight: 900,
|
||||
color: "#fff",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
FEELS CENTER
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 200,
|
||||
whiteSpace: "nowrap",
|
||||
fontSize: 20,
|
||||
color: "#fff",
|
||||
fontWeight: 600,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
transform: `translateX(${width - offset}px)`,
|
||||
}}
|
||||
>
|
||||
{tickerText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Score bug / mood tracker
|
||||
const MoodScoreBug: React.FC<{ mood: string; score: number; streak: number }> = ({
|
||||
mood,
|
||||
score,
|
||||
streak,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 40,
|
||||
left: 40,
|
||||
backgroundColor: "rgba(0,0,0,0.8)",
|
||||
borderLeft: `4px solid ${ESPN_RED}`,
|
||||
padding: "15px 25px",
|
||||
display: "flex",
|
||||
gap: 30,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, color: "#888", fontWeight: 600 }}>TODAY</div>
|
||||
<div style={{ fontSize: 36, color: "#fff" }}>{mood}</div>
|
||||
</div>
|
||||
<div style={{ borderLeft: "1px solid #444", paddingLeft: 30 }}>
|
||||
<div style={{ fontSize: 14, color: "#888", fontWeight: 600 }}>SCORE</div>
|
||||
<div style={{ fontSize: 36, color: ESPN_YELLOW, fontWeight: 700 }}>
|
||||
{score}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ borderLeft: "1px solid #444", paddingLeft: 30 }}>
|
||||
<div style={{ fontSize: 14, color: "#888", fontWeight: 600 }}>STREAK</div>
|
||||
<div style={{ fontSize: 36, color: "#10b981", fontWeight: 700 }}>
|
||||
{streak}🔥
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Opening - "Welcome to Feels Center"
|
||||
const OpeningScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoSlam = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 8, stiffness: 150 },
|
||||
});
|
||||
|
||||
const textReveal = interpolate(frame, [fps * 0.5, fps * 1.5], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: ESPN_DARK }}>
|
||||
{/* Dramatic spotlight */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "50%",
|
||||
width: 800,
|
||||
height: "100%",
|
||||
background: `linear-gradient(90deg, transparent 0%, rgba(204,0,0,0.1) 50%, transparent 100%)`,
|
||||
transform: "translateX(-50%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Logo slam */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "35%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(logoSlam, [0, 1], [3, 1])})`,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 120,
|
||||
fontWeight: 900,
|
||||
color: "#fff",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
textShadow: `0 0 60px ${ESPN_RED}`,
|
||||
}}
|
||||
>
|
||||
FEELS
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 700,
|
||||
color: ESPN_RED,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
letterSpacing: 16,
|
||||
}}
|
||||
>
|
||||
CENTER
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "60%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: textReveal,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#888",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
YOUR EMOTIONAL HIGHLIGHTS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ESPN-style corner graphics */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
right: 40,
|
||||
fontSize: 16,
|
||||
color: "#444",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
LIVE
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 10,
|
||||
height: 10,
|
||||
backgroundColor: ESPN_RED,
|
||||
borderRadius: "50%",
|
||||
marginLeft: 10,
|
||||
animation: "blink 1s infinite",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NewsTicker />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: Play of the Day - Mood selection replay
|
||||
const PlayOfDayScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const replayProgress = interpolate(frame, [fps * 0.5, fps * 3], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
// Slow-mo finger movement
|
||||
const fingerX = interpolate(replayProgress, [0, 0.8, 1], [0, 200, 200]);
|
||||
const fingerY = interpolate(replayProgress, [0, 0.8, 0.9, 1], [100, 0, -20, 0]);
|
||||
|
||||
// Impact flash
|
||||
const impactFlash = replayProgress > 0.85 && replayProgress < 0.95;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: ESPN_DARK }}>
|
||||
{/* "PLAY OF THE DAY" banner */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 40,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
backgroundColor: ESPN_YELLOW,
|
||||
padding: "10px 40px",
|
||||
transform: "skewX(-10deg)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 900,
|
||||
color: "#000",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
transform: "skewX(10deg)",
|
||||
display: "inline-block",
|
||||
}}
|
||||
>
|
||||
🏆 PLAY OF THE DAY
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Replay frame */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
border: "4px solid #333",
|
||||
borderRadius: 20,
|
||||
padding: 40,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
}}
|
||||
>
|
||||
{/* Mood buttons */}
|
||||
<div style={{ display: "flex", gap: 20 }}>
|
||||
{["😢", "😕", "😐", "🙂", "😊"].map((emoji, i) => {
|
||||
const isTarget = i === 4;
|
||||
const isSelected = isTarget && replayProgress > 0.85;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 25,
|
||||
backgroundColor: isSelected ? "#10b981" : "rgba(255,255,255,0.1)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 50,
|
||||
boxShadow: isSelected ? `0 0 40px #10b981` : "none",
|
||||
transform: isSelected ? "scale(1.1)" : "scale(1)",
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Slow-mo finger */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 40 + fingerX,
|
||||
bottom: -50 + fingerY,
|
||||
fontSize: 60,
|
||||
transform: "rotate(-30deg)",
|
||||
opacity: replayProgress < 0.9 ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
👆
|
||||
</div>
|
||||
|
||||
{/* Impact lines */}
|
||||
{impactFlash && (
|
||||
<>
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: "50%",
|
||||
width: 60,
|
||||
height: 4,
|
||||
backgroundColor: "#fff",
|
||||
transform: `rotate(${i * 45}deg)`,
|
||||
transformOrigin: "right center",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Slow-mo indicator */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 150,
|
||||
left: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: ESPN_YELLOW,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
SLOW-MO REPLAY
|
||||
</div>
|
||||
<div style={{ fontSize: 24 }}>🎬</div>
|
||||
</div>
|
||||
|
||||
{/* Commentary */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: replayProgress > 0.9 ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
"WHAT A SELECTION! ABSOLUTELY CLINICAL!"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NewsTicker />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Stats breakdown
|
||||
const StatsScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const stats = [
|
||||
{ label: "DAYS TRACKED", value: 127, color: "#10b981" },
|
||||
{ label: "CURRENT STREAK", value: 23, color: ESPN_YELLOW },
|
||||
{ label: "BEST STREAK", value: 45, color: ESPN_RED },
|
||||
{ label: "MOOD AVERAGE", value: "4.2", color: "#3b82f6" },
|
||||
];
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: ESPN_DARK }}>
|
||||
{/* Stats title */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: 900,
|
||||
color: "#fff",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
📊 SEASON STATS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: 40,
|
||||
}}
|
||||
>
|
||||
{stats.map((stat, i) => {
|
||||
const delay = i * 8;
|
||||
const statProgress = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 12 },
|
||||
});
|
||||
|
||||
const countUp = Math.floor(
|
||||
interpolate(statProgress, [0, 1], [0, typeof stat.value === "number" ? stat.value : parseFloat(stat.value)])
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={stat.label}
|
||||
style={{
|
||||
backgroundColor: "rgba(255,255,255,0.05)",
|
||||
borderLeft: `4px solid ${stat.color}`,
|
||||
padding: "30px 50px",
|
||||
opacity: interpolate(statProgress, [0, 1], [0, 1]),
|
||||
transform: `translateY(${interpolate(statProgress, [0, 1], [30, 0])}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "#888",
|
||||
fontWeight: 600,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{stat.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 64,
|
||||
color: stat.color,
|
||||
fontWeight: 900,
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
{typeof stat.value === "number" ? countUp : stat.value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<MoodScoreBug mood="😊" score={85} streak={23} />
|
||||
<NewsTicker />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: CTA - "Download to join the league"
|
||||
const CTAScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const logoProgress = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: ESPN_DARK }}>
|
||||
{/* Dramatic background */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: `radial-gradient(ellipse at center, rgba(204,0,0,0.2) 0%, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* App icon */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "35%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(logoProgress, [0, 1], [0.5, 1])})`,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 180 * 0.22,
|
||||
border: `4px solid ${ESPN_RED}`,
|
||||
boxShadow: `0 0 60px ${ESPN_RED}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* CTA text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "58%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 900,
|
||||
color: "#fff",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: ESPN_YELLOW,
|
||||
fontWeight: 700,
|
||||
marginTop: 20,
|
||||
opacity: interpolate(frame, [fps, fps * 1.5], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
JOIN THE LEAGUE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ESPN-style "Download" button */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 200,
|
||||
left: "50%",
|
||||
transform: `translateX(-50%) scale(${interpolate(frame, [fps * 2, fps * 2.5], [0.8, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
})})`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: ESPN_RED,
|
||||
padding: "20px 60px",
|
||||
borderRadius: 8,
|
||||
transform: "skewX(-5deg)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: 900,
|
||||
color: "#fff",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
transform: "skewX(5deg)",
|
||||
display: "inline-block",
|
||||
}}
|
||||
>
|
||||
DOWNLOAD NOW
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* "THIS HAS BEEN" outro */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(frame, [fps * 3, fps * 3.5], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: "#666",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
This has been FEELS CENTER. Track responsibly.
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 20 seconds total
|
||||
export const ConceptKSportsCenter: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(4 * fps)}>
|
||||
<OpeningScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(4 * fps)} durationInFrames={Math.round(6 * fps)}>
|
||||
<PlayOfDayScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(10 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<StatsScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(15 * fps)} durationInFrames={Math.round(5 * fps)}>
|
||||
<CTAScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
724
feels-promo/src/ConceptL-Musical.tsx
Normal file
724
feels-promo/src/ConceptL-Musical.tsx
Normal file
@@ -0,0 +1,724 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
Easing,
|
||||
} from "remotion";
|
||||
|
||||
// Stage curtain component
|
||||
const Curtain: React.FC<{ side: "left" | "right"; open: number }> = ({
|
||||
side,
|
||||
open,
|
||||
}) => {
|
||||
const offset = interpolate(open, [0, 1], [0, 100]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
[side]: 0,
|
||||
width: "55%",
|
||||
height: "100%",
|
||||
background: "linear-gradient(180deg, #8B0000 0%, #4a0000 100%)",
|
||||
transform: `translateX(${side === "left" ? -offset : offset}%)`,
|
||||
zIndex: 50,
|
||||
boxShadow:
|
||||
side === "left"
|
||||
? "10px 0 30px rgba(0,0,0,0.5)"
|
||||
: "-10px 0 30px rgba(0,0,0,0.5)",
|
||||
}}
|
||||
>
|
||||
{/* Curtain folds */}
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: `${i * 12.5}%`,
|
||||
width: "12.5%",
|
||||
height: "100%",
|
||||
background: `linear-gradient(90deg, rgba(0,0,0,0.2) 0%, transparent 50%, rgba(0,0,0,0.1) 100%)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Spotlight effect
|
||||
const Spotlight: React.FC<{
|
||||
x: number;
|
||||
y: number;
|
||||
size?: number;
|
||||
color?: string;
|
||||
intensity?: number;
|
||||
}> = ({ x, y, size = 400, color = "#fff", intensity = 0.6 }) => (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: x - size / 2,
|
||||
top: y - size / 2,
|
||||
width: size,
|
||||
height: size,
|
||||
background: `radial-gradient(circle, ${color}${Math.floor(intensity * 255).toString(16).padStart(2, "0")} 0%, transparent 70%)`,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Dancing emoji character
|
||||
const DancingEmoji: React.FC<{
|
||||
emoji: string;
|
||||
x: number;
|
||||
y: number;
|
||||
size?: number;
|
||||
delay?: number;
|
||||
danceStyle?: "bounce" | "spin" | "sway";
|
||||
}> = ({ emoji, x, y, size = 100, delay = 0, danceStyle = "bounce" }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const localFrame = frame - delay;
|
||||
|
||||
let transform = "";
|
||||
|
||||
switch (danceStyle) {
|
||||
case "bounce":
|
||||
const bounceY = Math.abs(Math.sin(localFrame * 0.2)) * 30;
|
||||
transform = `translateY(${-bounceY}px)`;
|
||||
break;
|
||||
case "spin":
|
||||
transform = `rotate(${localFrame * 5}deg)`;
|
||||
break;
|
||||
case "sway":
|
||||
const swayX = Math.sin(localFrame * 0.15) * 20;
|
||||
const swayRotate = Math.sin(localFrame * 0.15) * 10;
|
||||
transform = `translateX(${swayX}px) rotate(${swayRotate}deg)`;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: x,
|
||||
top: y,
|
||||
fontSize: size,
|
||||
transform,
|
||||
filter: "drop-shadow(0 10px 20px rgba(0,0,0,0.5))",
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Stage lights at top
|
||||
const StageLights: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const colors = ["#ff6b6b", "#feca57", "#48dbfb", "#ff9ff3", "#1dd1a1"];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 80,
|
||||
backgroundColor: "#1a1a1a",
|
||||
display: "flex",
|
||||
justifyContent: "space-around",
|
||||
alignItems: "center",
|
||||
zIndex: 60,
|
||||
}}
|
||||
>
|
||||
{colors.map((color, i) => {
|
||||
const pulse = Math.sin(frame * 0.1 + i) * 0.3 + 0.7;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 50,
|
||||
backgroundColor: "#333",
|
||||
borderRadius: "50% 50% 0 0",
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "center",
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
opacity: pulse,
|
||||
boxShadow: `0 0 30px ${color}, 0 20px 60px ${color}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 1: Curtain Rise - "Welcome to the show"
|
||||
const CurtainRiseScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const curtainOpen = interpolate(frame, [fps * 1, fps * 3], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.out(Easing.cubic),
|
||||
});
|
||||
|
||||
const titleProgress = spring({
|
||||
frame: frame - fps * 2,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
{/* Stage floor */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 400,
|
||||
background: "linear-gradient(180deg, #2d2d2d 0%, #1a1a1a 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Spotlight */}
|
||||
<Spotlight x={540} y={600} size={600} intensity={0.4} />
|
||||
|
||||
{/* Title card */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "45%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(titleProgress, [0, 1], [0.5, 1])})`,
|
||||
textAlign: "center",
|
||||
opacity: interpolate(titleProgress, [0, 1], [0, 1]),
|
||||
zIndex: 40,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "#feca57",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
Welcome to
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 100,
|
||||
fontWeight: 900,
|
||||
color: "#fff",
|
||||
fontFamily: "Georgia, serif",
|
||||
textShadow: "0 0 60px rgba(255,200,100,0.5)",
|
||||
letterSpacing: 8,
|
||||
}}
|
||||
>
|
||||
FEELS
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
color: "#ff6b6b",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
The Musical
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StageLights />
|
||||
<Curtain side="left" open={curtainOpen} />
|
||||
<Curtain side="right" open={curtainOpen} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 2: "The Feelings Song" - Dancing mood emojis
|
||||
const FeelingSongScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, width, height } = useVideoConfig();
|
||||
|
||||
const emojis = [
|
||||
{ emoji: "😢", color: "#3b82f6", name: "SADNESS" },
|
||||
{ emoji: "😤", color: "#ef4444", name: "ANGER" },
|
||||
{ emoji: "😊", color: "#10b981", name: "JOY" },
|
||||
{ emoji: "😰", color: "#8b5cf6", name: "ANXIETY" },
|
||||
{ emoji: "🥰", color: "#ec4899", name: "LOVE" },
|
||||
];
|
||||
|
||||
// Choreographed positions
|
||||
const getPosition = (index: number, progress: number) => {
|
||||
const centerX = width / 2;
|
||||
const baseY = height * 0.5;
|
||||
|
||||
// Dance formation - V shape to line to circle
|
||||
const phase = Math.floor(progress * 3) % 3;
|
||||
|
||||
if (phase === 0) {
|
||||
// V formation
|
||||
const vX = centerX + (index - 2) * 150;
|
||||
const vY = baseY + Math.abs(index - 2) * 80;
|
||||
return { x: vX - 50, y: vY };
|
||||
} else if (phase === 1) {
|
||||
// Line formation
|
||||
const lineX = centerX + (index - 2) * 180;
|
||||
return { x: lineX - 50, y: baseY };
|
||||
} else {
|
||||
// Circle formation
|
||||
const angle = (index / 5) * Math.PI * 2 + progress * 2;
|
||||
const radius = 250;
|
||||
return {
|
||||
x: centerX + Math.cos(angle) * radius - 50,
|
||||
y: baseY + Math.sin(angle) * radius * 0.5,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const danceProgress = interpolate(frame, [0, fps * 5], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#1a1a1a" }}>
|
||||
{/* Stage floor with reflection */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 500,
|
||||
background:
|
||||
"linear-gradient(180deg, #2d2d2d 0%, #1a1a1a 50%, #0a0a0a 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Multiple spotlights */}
|
||||
{emojis.map((e, i) => {
|
||||
const pos = getPosition(i, danceProgress);
|
||||
return (
|
||||
<Spotlight
|
||||
key={i}
|
||||
x={pos.x + 50}
|
||||
y={pos.y + 50}
|
||||
size={300}
|
||||
color={e.color}
|
||||
intensity={0.3}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* "EVERYBODY FEEL!" banner */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 150,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 900,
|
||||
color: "#feca57",
|
||||
fontFamily: "Georgia, serif",
|
||||
textShadow: "0 0 30px rgba(254,202,87,0.5)",
|
||||
transform: `scale(${1 + Math.sin(frame * 0.2) * 0.05})`,
|
||||
}}
|
||||
>
|
||||
♪ EVERYBODY FEEL! ♪
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dancing emojis */}
|
||||
{emojis.map((e, i) => {
|
||||
const pos = getPosition(i, danceProgress);
|
||||
const delay = i * 3;
|
||||
|
||||
return (
|
||||
<DancingEmoji
|
||||
key={i}
|
||||
emoji={e.emoji}
|
||||
x={pos.x}
|
||||
y={pos.y}
|
||||
size={100}
|
||||
delay={delay}
|
||||
danceStyle={i % 3 === 0 ? "bounce" : i % 3 === 1 ? "sway" : "spin"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Musical notes floating */}
|
||||
{["♪", "♫", "♬", "♩"].map((note, i) => {
|
||||
const noteX = (frame * 2 + i * 200) % (width + 100) - 50;
|
||||
const noteY = 200 + Math.sin(frame * 0.1 + i) * 100;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: noteX,
|
||||
top: noteY,
|
||||
fontSize: 60,
|
||||
color: "#feca57",
|
||||
opacity: 0.6,
|
||||
transform: `rotate(${Math.sin(frame * 0.1 + i) * 20}deg)`,
|
||||
}}
|
||||
>
|
||||
{note}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<StageLights />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 3: Solo number - Joy takes center stage
|
||||
const SoloNumberScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, width, height } = useVideoConfig();
|
||||
|
||||
const spotlightPulse = Math.sin(frame * 0.1) * 0.2 + 0.8;
|
||||
|
||||
// Joy's dramatic entrance
|
||||
const joyEnter = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 60 },
|
||||
});
|
||||
|
||||
const joyY = interpolate(joyEnter, [0, 1], [height, height * 0.4]);
|
||||
|
||||
// Star burst behind Joy
|
||||
const starRotation = frame * 0.5;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
{/* Dramatic backdrop */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background:
|
||||
"radial-gradient(ellipse at center, #1a472a 0%, #0a0a0a 70%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Star burst */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: joyY - 100,
|
||||
left: width / 2,
|
||||
transform: `translate(-50%, -50%) rotate(${starRotation}deg)`,
|
||||
}}
|
||||
>
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 8,
|
||||
height: 300,
|
||||
background: `linear-gradient(180deg, #feca57 0%, transparent 100%)`,
|
||||
transform: `rotate(${i * 30}deg)`,
|
||||
transformOrigin: "center bottom",
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Giant spotlight on Joy */}
|
||||
<Spotlight
|
||||
x={width / 2}
|
||||
y={joyY}
|
||||
size={800}
|
||||
color="#10b981"
|
||||
intensity={spotlightPulse * 0.5}
|
||||
/>
|
||||
|
||||
{/* Joy - center stage */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: width / 2 - 100,
|
||||
top: joyY - 100,
|
||||
fontSize: 200,
|
||||
filter: "drop-shadow(0 20px 40px rgba(16,185,129,0.5))",
|
||||
transform: `scale(${1 + Math.sin(frame * 0.15) * 0.1})`,
|
||||
}}
|
||||
>
|
||||
😊
|
||||
</div>
|
||||
|
||||
{/* Lyric text */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 300,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 40,
|
||||
color: "#fff",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
opacity: interpolate(frame, [fps, fps * 1.5], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
"One tap is all it takes..."
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 40,
|
||||
color: "#10b981",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
marginTop: 20,
|
||||
opacity: interpolate(frame, [fps * 2, fps * 2.5], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
"To know how your heart feels today!"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StageLights />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Scene 4: Finale - All together + app reveal
|
||||
const FinaleScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, width, height } = useVideoConfig();
|
||||
|
||||
const emojis = ["😢", "😕", "😐", "🙂", "😊"];
|
||||
|
||||
const logoProgress = spring({
|
||||
frame: frame - fps * 1,
|
||||
fps,
|
||||
config: { damping: 10, stiffness: 80 },
|
||||
});
|
||||
|
||||
// Confetti
|
||||
const confettiColors = ["#ff6b6b", "#feca57", "#48dbfb", "#ff9ff3", "#1dd1a1"];
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
||||
{/* Grand finale lighting */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: `
|
||||
radial-gradient(ellipse at 50% 30%, rgba(254,202,87,0.3) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 30% 70%, rgba(236,72,153,0.2) 0%, transparent 40%),
|
||||
radial-gradient(ellipse at 70% 70%, rgba(72,219,251,0.2) 0%, transparent 40%)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Line of emojis at top */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: 40,
|
||||
}}
|
||||
>
|
||||
{emojis.map((emoji, i) => {
|
||||
const delay = i * 3;
|
||||
const emojiProgress = spring({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 12 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: 80,
|
||||
transform: `translateY(${interpolate(emojiProgress, [0, 1], [100, 0])}px) scale(${interpolate(emojiProgress, [0, 1], [0.5, 1])})`,
|
||||
opacity: interpolate(emojiProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* App icon center stage */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(logoProgress, [0, 1], [0, 1])})`,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile("app-icon.png")}
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: 200 * 0.22,
|
||||
boxShadow: "0 0 100px rgba(254,202,87,0.5)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* App name */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "68%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 80,
|
||||
fontWeight: 900,
|
||||
color: "#fff",
|
||||
fontFamily: "Georgia, serif",
|
||||
textShadow: "0 0 40px rgba(255,255,255,0.5)",
|
||||
opacity: interpolate(logoProgress, [0, 1], [0, 1]),
|
||||
}}
|
||||
>
|
||||
Feels
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: "#feca57",
|
||||
fontFamily: "Georgia, serif",
|
||||
fontStyle: "italic",
|
||||
marginTop: 20,
|
||||
opacity: interpolate(frame, [fps * 2.5, fps * 3], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
♪ Download Today ♪
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confetti */}
|
||||
{[...Array(40)].map((_, i) => {
|
||||
const seed = i * 137.5;
|
||||
const x = (Math.sin(seed) * 0.5 + 0.5) * width;
|
||||
const fallProgress = interpolate(
|
||||
frame,
|
||||
[fps * 1 + i * 2, fps * 5],
|
||||
[0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
const y = interpolate(fallProgress, [0, 1], [-50, height + 50]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: x,
|
||||
top: y,
|
||||
width: 15,
|
||||
height: 15,
|
||||
backgroundColor: confettiColors[i % confettiColors.length],
|
||||
borderRadius: i % 2 === 0 ? "50%" : 2,
|
||||
transform: `rotate(${frame * (3 + (i % 5))}deg)`,
|
||||
opacity: interpolate(fallProgress, [0, 0.8, 1], [1, 1, 0]),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<StageLights />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Main composition - 25 seconds total
|
||||
export const ConceptLMusical: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={Math.round(5 * fps)}>
|
||||
<CurtainRiseScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(5 * fps)} durationInFrames={Math.round(7 * fps)}>
|
||||
<FeelingSongScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(12 * fps)} durationInFrames={Math.round(6 * fps)}>
|
||||
<SoloNumberScene />
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={Math.round(18 * fps)} durationInFrames={Math.round(7 * fps)}>
|
||||
<FinaleScene />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,166 @@
|
||||
import { Composition } from "remotion";
|
||||
import { FeelsPromoV1 } from "./FeelsPromo";
|
||||
import { ConceptASelfAwareness } from "./ConceptA-SelfAwareness";
|
||||
import { ConceptBNoJournalJournal } from "./ConceptB-NoJournalJournal";
|
||||
import { ConceptCYearInFeelings } from "./ConceptC-YearInFeelings";
|
||||
import { ConceptDAlwaysThere } from "./ConceptD-AlwaysThere";
|
||||
import { ConceptEMakeItYours } from "./ConceptE-MakeItYours";
|
||||
import { ConceptFPrivacyFirst } from "./ConceptF-PrivacyFirst";
|
||||
import { ConceptGStreakEffect } from "./ConceptG-StreakEffect";
|
||||
// Wild concepts
|
||||
import { ConceptHMoodHeist } from "./ConceptH-MoodHeist";
|
||||
import { ConceptIRetroArcade } from "./ConceptI-RetroArcade";
|
||||
import { ConceptJConspiracy } from "./ConceptJ-Conspiracy";
|
||||
import { ConceptKSportsCenter } from "./ConceptK-SportsCenter";
|
||||
import { ConceptLMusical } from "./ConceptL-Musical";
|
||||
|
||||
export const RemotionRoot: React.FC = () => {
|
||||
const fps = 30;
|
||||
const sceneDuration = 3.5 * fps; // 3.5 seconds per scene
|
||||
const transitionDuration = Math.round(0.6 * fps); // 0.6 second transitions
|
||||
const outroDuration = Math.round(2.5 * fps);
|
||||
|
||||
// Calculate total duration accounting for transition overlaps
|
||||
// 7 scenes + outro - 7 transitions
|
||||
const totalDuration =
|
||||
// V1 calculations
|
||||
const sceneDuration = 3.5 * fps;
|
||||
const transitionDuration = Math.round(0.6 * fps);
|
||||
const outroDuration = Math.round(2.5 * fps);
|
||||
const v1TotalDuration =
|
||||
sceneDuration * 7 + outroDuration - transitionDuration * 7;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ═══════════════════════════════════════════════════════════════
|
||||
ORIGINAL PROMO
|
||||
═══════════════════════════════════════════════════════════════ */}
|
||||
<Composition
|
||||
id="FeelsPromoV1"
|
||||
component={FeelsPromoV1}
|
||||
durationInFrames={totalDuration}
|
||||
durationInFrames={v1TotalDuration}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════
|
||||
STANDARD CONCEPTS (A-G)
|
||||
═══════════════════════════════════════════════════════════════ */}
|
||||
|
||||
{/* Concept A: 30 Seconds to Self-Awareness */}
|
||||
<Composition
|
||||
id="ConceptA-SelfAwareness"
|
||||
component={ConceptASelfAwareness}
|
||||
durationInFrames={Math.round(30 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept B: The No-Journal Journal (20s) */}
|
||||
<Composition
|
||||
id="ConceptB-NoJournalJournal"
|
||||
component={ConceptBNoJournalJournal}
|
||||
durationInFrames={Math.round(20 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept C: Your Year in Feelings (15s) */}
|
||||
<Composition
|
||||
id="ConceptC-YearInFeelings"
|
||||
component={ConceptCYearInFeelings}
|
||||
durationInFrames={Math.round(15 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept D: Always There (25s) */}
|
||||
<Composition
|
||||
id="ConceptD-AlwaysThere"
|
||||
component={ConceptDAlwaysThere}
|
||||
durationInFrames={Math.round(25 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept E: Make It Yours (20s) */}
|
||||
<Composition
|
||||
id="ConceptE-MakeItYours"
|
||||
component={ConceptEMakeItYours}
|
||||
durationInFrames={Math.round(20 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept F: Privacy First (15s) */}
|
||||
<Composition
|
||||
id="ConceptF-PrivacyFirst"
|
||||
component={ConceptFPrivacyFirst}
|
||||
durationInFrames={Math.round(15 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept G: The Streak Effect (20s) */}
|
||||
<Composition
|
||||
id="ConceptG-StreakEffect"
|
||||
component={ConceptGStreakEffect}
|
||||
durationInFrames={Math.round(20 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════
|
||||
WILD CONCEPTS (H-L) - OFF THE WALL CREATIVE
|
||||
═══════════════════════════════════════════════════════════════ */}
|
||||
|
||||
{/* Concept H: The Mood Heist (25s) - Ocean's Eleven style thriller */}
|
||||
<Composition
|
||||
id="ConceptH-MoodHeist"
|
||||
component={ConceptHMoodHeist}
|
||||
durationInFrames={Math.round(25 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept I: Retro Arcade Feels (20s) - 8-bit video game */}
|
||||
<Composition
|
||||
id="ConceptI-RetroArcade"
|
||||
component={ConceptIRetroArcade}
|
||||
durationInFrames={Math.round(20 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept J: The Feels Conspiracy (20s) - Dark documentary thriller */}
|
||||
<Composition
|
||||
id="ConceptJ-Conspiracy"
|
||||
component={ConceptJConspiracy}
|
||||
durationInFrames={Math.round(20 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept K: Sports Center Emotions (20s) - ESPN broadcast parody */}
|
||||
<Composition
|
||||
id="ConceptK-SportsCenter"
|
||||
component={ConceptKSportsCenter}
|
||||
durationInFrames={Math.round(20 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
/>
|
||||
|
||||
{/* Concept L: Feelings The Musical (25s) - Broadway musical number */}
|
||||
<Composition
|
||||
id="ConceptL-Musical"
|
||||
component={ConceptLMusical}
|
||||
durationInFrames={Math.round(25 * fps)}
|
||||
fps={fps}
|
||||
width={1080}
|
||||
height={1920}
|
||||
|
||||
Reference in New Issue
Block a user