Compare commits

...

25 Commits

Author SHA1 Message Date
Matiss Janis Aboltins
b1ee19f13f [AI] Update "much more" tagline to thank contributors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:12:19 +01:00
Matiss Janis Aboltins
fa535b1898 [AI] Center accent glow on text-only slide, simplify tagline, CLI to cyan
- Center the accent glow when no screenshot is present
- Change "much more" tagline to "45 enhancements, 32 bugfixes"
- Change Actual CLI accent color to cyan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:12:01 +01:00
Matiss Janis Aboltins
d9b78958aa [AI] Remove screenshot from "And much more" slide, center text
Made screenshot optional in Feature type. When absent, text is
centered with larger font size and no browser frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:11:07 +01:00
Matiss Janis Aboltins
cbbc9f3391 [AI] Replace Custom Themes with "And much more..." catch-all slide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:10:15 +01:00
Matiss Janis Aboltins
b043f0ae92 [AI] Change Drag & Drop accent color to cyan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:07:03 +01:00
Matiss Janis Aboltins
598292d9bf [AI] Remove bounce from title card — smooth zoom only
Changed spring config to damping: 200 to eliminate overshoot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:05:59 +01:00
Matiss Janis Aboltins
a5d130f5da [AI] Replace pulsing glow with rotating meteor border effect
Uses a conic-gradient that rotates around the browser frame,
creating a comet/meteor trail effect along the border. Driven
by useCurrentFrame() at 120 degrees per second.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:04:40 +01:00
Matiss Janis Aboltins
f289ef7fbd [AI] Add subtle pulsing glow animation to browser frame border
Glow opacity and spread oscillate using sine wave driven by
useCurrentFrame(), creating a gentle breathing effect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:03:47 +01:00
Matiss Janis Aboltins
58a7ef07d8 [AI] Add 2-second music fade-out at end, fix fragment keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:03:19 +01:00
Matiss Janis Aboltins
b7eb508cce [AI] Reorder features: Donut Chart, Budget Notes, Drag & Drop, CLI, Themes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:01:36 +01:00
Matiss Janis Aboltins
7cccdad1ba [AI] Remove Payee Locations and Smarter Imports from video
Reduced to 5 features: Drag & Drop, Donut Chart (tier 1),
Budget Notes, Actual CLI, Custom Themes (tier 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:59:48 +01:00
Matiss Janis Aboltins
60fa9bbbcb [AI] Update payee locations screenshot to mobile transaction view
Captured mobile viewport (390x844) showing the New Transaction
page with a purple highlight glow on the Payee field to draw
attention to the payee locations feature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:52:26 +01:00
Matiss Janis Aboltins
3862c48123 [AI] Update themes screenshot to show custom theme catalog
Enabled custom themes feature flag, opened theme catalog showing
theme preview cards (Black Gold, Butterfly, Catppuccin Frappé, etc).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:49:25 +01:00
Matiss Janis Aboltins
4362e4c9ff [AI] Retake budget notes screenshot with proper hover interaction
Hovered on the 400.00 budgeted cell to reveal the notes icon,
clicked it, and filled in demo text showing the notes textarea
in context next to the budget cell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:39:07 +01:00
Matiss Janis Aboltins
676778b027 [AI] Update budget notes screenshot and tagline
Captured screenshot showing the notes textarea open on a budget
cell with demo text. Updated tagline to "Monthly per-category notes".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:36:40 +01:00
Matiss Janis Aboltins
112e3b73ee [AI] Replace donut chart screenshot with actual donut chart widget
Created a custom report widget with donut chart visualization
instead of the generic reports overview page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:30:30 +01:00
Matiss Janis Aboltins
6e4d3f7869 [AI] Fix CLI mockup balance column alignment
Right-align all balance_current values so -301,380.72 lines up
with the other numbers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:26:20 +01:00
Matiss Janis Aboltins
89d89b89c3 [AI] Switch from video recordings to static screenshots
- Replace Video component with Img in FeatureScene
- Remove zoom-in effect from feature scenes
- Capture high-quality screenshots via Playwright
- Fix CLI mockup: correct columns (id, name, offBudget, closed,
  balance_current), fix alignment, remove count line
- Update Feature type: recording -> screenshot
- Fix browser frame height to fit content naturally

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:24:22 +01:00
Matiss Janis Aboltins
0bcec82523 [AI] Add screen recordings and project config for release video
Captured real Playwright screen recordings of Actual Budget features
for the v26.4.0 release teaser video. Includes recordings of account
pages, budget views, reports, payees, theme switching, and a CLI
terminal mockup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:09:11 +01:00
Matiss Janis Aboltins
9199903ca7 [AI] Wire up main composition with TransitionSeries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:54:05 +01:00
Matiss Janis Aboltins
82cb89caa5 [AI] Add TitleCard, FeatureScene, and OutroCard scene components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:54:00 +01:00
Matiss Janis Aboltins
59ce349b6b [AI] Add AnimatedText and BrowserFrame components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:53:55 +01:00
Matiss Janis Aboltins
5cbc972a7a [AI] Add constants and feature data for release video
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:53:49 +01:00
Matiss Janis Aboltins
7396f12be6 [AI] Set up dependencies for release video
Install @remotion/transitions, @remotion/media, @remotion/google-fonts, playwright, and @playwright/test. Create src/components, public/recordings, scripts directories and copy Actual Budget logo to public/logo.svg.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:49:40 +01:00
Matiss Janis Aboltins
012b0d4984 [AI] Add release video design spec and implementation plan 2026-04-02 21:42:54 +01:00
34 changed files with 7426 additions and 0 deletions

7
my-video/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
out/
.playwright-cli/
.superpowers/
# Temp screenshots from playwright-cli
*.png
!public/screenshots/*.png

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,158 @@
# Actual Budget v26.4.0 Release Video — Design Spec
## Overview
A 52-second social media teaser video showcasing the most important features in Actual Budget v26.4.0. Built with Remotion (React), using real screen recordings captured via Playwright and synced to a music track at ~139 BPM.
**Format:** 1280x720, 30fps, ~52 seconds
**Style:** Branded & colorful — Actual brand colors, energetic transitions, playful feel
**Music:** `public/music.mp3` — upbeat tech track, 139 BPM. Using first 52 seconds only.
**Content:** Real screen recordings with bold text overlays (feature name + tagline)
## Music Analysis
- **BPM:** ~139 (beat interval: ~0.43s = ~13 frames at 30fps)
- **04s:** Quiet intro build-up (energy rises from silence to medium)
- **4s+:** Full energy drop, sustained loud level throughout
- **Track total:** 4:24, but we only use 052s
## Video Timeline
### Scene 1: Title Card (0.0s 4.0s | frames 0120)
- Actual Budget logo animates in during quiet intro
- "v26.4.0" and subtitle fade in on rising energy
- Beat drop at 4.0s triggers transition to first feature
- Background: dark gradient with brand colors
### Tier 1: Hero Features (4.0s 20.0s | frames 120600)
Each feature gets ~5.3s (160 frames / 12 beats). Screen recording fills most of the frame in a styled browser mockup. Text overlay appears on the first beat.
**Feature 1: Drag & Drop Transaction Reordering (4.0s 9.3s)**
- Screen recording: user dragging transactions to reorder within the same day
- Text: "Reorder transactions — your way"
- Playwright scenario: open an account, drag a transaction up/down within same-day group
**Feature 2: Concentric Donut Chart (9.3s 14.7s)**
- Screen recording: custom reports page showing the new donut chart visualization
- Text: "Beautiful category breakdowns"
- Playwright scenario: navigate to custom reports, show a donut chart with category data
**Feature 3: Payee Locations MVP (14.7s 20.0s)**
- Screen recording: payee management showing location data
- Text: "Know where you spend"
- Playwright scenario: open payees section, show payee with location info
### Tier 2: Quick Highlights (20.0s 44.0s | frames 6001320)
Each feature gets ~6s (180 frames / 14 beats). Same layout but slightly faster-paced transitions.
**Feature 4: Monthly Budget Cell Notes (20.0s 26.0s)**
- Screen recording: adding a note to a monthly budget cell
- Text: "Annotate your budget"
- Playwright scenario: click a budget cell, add a note, show the note indicator
**Feature 5: Actual CLI Tool (26.0s 32.0s)**
- Screen recording: terminal showing CLI commands querying budget data
- Text: "Your budget, from the command line"
- Note: This will be a terminal recording/mockup rather than Playwright, since it's a CLI tool
**Feature 6: Custom Theme Improvements (32.0s 38.0s)**
- Screen recording: switching themes, showing custom fonts and light/dark options
- Text: "Make it yours"
- Playwright scenario: open settings, switch between themes, toggle light/dark
**Feature 7: Import Improvements (38.0s 44.0s)**
- Screen recording: import dialog showing new options
- Text: "Smarter imports"
- Playwright scenario: open import dialog, show "import since" date filter, swap payee/memo toggle
### Scene 9: Outro (44.0s 52.0s | frames 13201560)
- Stats flash in sequence: "4 features · 45 enhancements · 32 bugfixes"
- CTA: "Update now — actualbudget.org"
- Logo + version badge fade out
- Music continues to natural phrase ending
## Visual Design
### Color Palette
- **Background:** Dark (#1a1a2e / #16213e gradient)
- **Tier 1 accent:** Cyan (#00d2ff)
- **Tier 2 accent:** Coral/Red (#e94560)
- **Outro accent:** Gold (#ffd700)
- **Text:** White (#ffffff) with subtle shadows
- **Brand purple:** Used for logo and accent elements
### Typography
- Feature names: Bold, large sans-serif
- Taglines: Regular weight, slightly smaller
- Stats/CTA: Bold, emphasized with accent color
### Transitions
- Scene transitions: slide/zoom synced to beat hits
- Screen recordings slide in from right
- Text overlays pop in with spring animation on beat
- Slight zoom-in on screen recordings during playback for energy
### Screen Recording Frame
- Styled browser mockup wrapper (rounded corners, subtle shadow)
- Dark chrome to match overall aesthetic
- Fills ~80% of frame width, centered
## Remotion Architecture
### Composition Structure
```
<MyComposition> (1560 frames, 30fps, 1280x720)
<Audio src="music.mp3" />
<TitleCard /> {frames 0-120}
<FeatureScene /> {frames 120-280} -- Drag & Drop
<FeatureScene /> {frames 280-440} -- Donut Chart
<FeatureScene /> {frames 440-600} -- Payee Locations
<FeatureScene /> {frames 600-780} -- Budget Notes
<FeatureScene /> {frames 780-960} -- CLI Tool
<FeatureScene /> {frames 960-1140} -- Themes
<FeatureScene /> {frames 1140-1320} -- Imports
<OutroCard /> {frames 1320-1560}
</MyComposition>
```
### Key Components
- **TitleCard:** Animated logo + version text with fade/scale entrance
- **FeatureScene:** Reusable component accepting screen recording source, title, tagline, accent color, and frame range. Handles slide-in animation, text overlay timing, and zoom effect.
- **OutroCard:** Sequential stat counter animations + CTA
- **BrowserFrame:** Decorative wrapper around screen recordings
### Screen Recordings
Captured as video files via Playwright (webm/mp4) and placed in `public/recordings/`. Each recording is pre-trimmed to show the key interaction for that feature.
## Playwright Recording Plan
Each recording captures a specific user interaction in the running Actual Budget app:
1. **drag-drop.webm** — Open checking account, drag transaction to reorder
2. **donut-chart.webm** — Navigate to reports, create/view donut chart
3. **payee-locations.webm** — Open payees, show payee with location
4. **budget-notes.webm** — Click budget cell, type a note, save
5. **cli-tool.webm** — (Terminal recording, not Playwright)
6. **themes.webm** — Settings > Themes, switch themes, toggle dark/light
7. **imports.webm** — File > Import, show new import options
The app needs to be running with demo data (`yarn start` + "View demo" setup) before capturing.
## Dependencies
- **Remotion 4.0.443** (already installed)
- **@remotion/player** — for preview
- **Tailwind CSS 4** (already installed)
- **Playwright** — for screen recordings (project dependency)
- **ffmpeg** — for audio trimming if needed

View File

@@ -0,0 +1,3 @@
import { config } from "@remotion/eslint-config-flat";
export default config;

4989
my-video/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
my-video/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "my-video",
"version": "1.0.0",
"description": "My Remotion video",
"repository": {},
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@remotion/cli": "4.0.443",
"@remotion/google-fonts": "^4.0.443",
"@remotion/media": "^4.0.443",
"@remotion/tailwind-v4": "4.0.443",
"@remotion/transitions": "^4.0.443",
"react": "19.2.3",
"react-dom": "19.2.3",
"remotion": "4.0.443",
"tailwindcss": "4.0.0"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@remotion/eslint-config-flat": "4.0.443",
"@types/react": "19.2.7",
"@types/web": "0.0.166",
"eslint": "9.19.0",
"playwright": "^1.59.1",
"prettier": "3.8.1",
"typescript": "5.9.3"
},
"scripts": {
"dev": "remotion studio",
"build": "remotion bundle",
"upgrade": "remotion upgrade",
"lint": "eslint src && tsc"
},
"sideEffects": [
"*.css"
]
}

4
my-video/public/logo.svg Executable file
View File

@@ -0,0 +1,4 @@
<svg width="30" height="32" viewBox="0 0 30 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.13785 30.4226L14.9372 1.11397C14.99 1.00184 15.1027 0.930283 15.2267 0.930283H15.8318C15.9542 0.930283 16.0659 1.00015 16.1195 1.11023L25.0219 19.3999L27.8131 18.3264C27.978 18.2629 28.1632 18.3452 28.2266 18.5102L28.9695 20.4417C29.033 20.6067 28.9507 20.7918 28.7857 20.8553L26.2121 21.8452L29.3875 28.3689C29.4648 28.5278 29.3987 28.7193 29.2398 28.7967L27.379 29.7024C27.2201 29.7798 27.0286 29.7136 26.9512 29.5547L23.6739 22.8215L1.6943 31.2754C1.52935 31.3389 1.3442 31.2566 1.28075 31.0916C1.28006 31.0898 1.27938 31.088 1.27872 31.0862L1.12666 30.6684C1.09749 30.5883 1.10152 30.4998 1.13785 30.4226ZM15.56 6.1518L5.85065 26.7737L22.4837 20.3762L15.56 6.1518Z" fill="white"/>
<path d="M21.7768 14.5682L22.7095 17.1121L1.50597 24.8867C1.34004 24.9476 1.1562 24.8624 1.09536 24.6964L0.382928 22.7534C0.322087 22.5875 0.407278 22.4037 0.573207 22.3428L21.7768 14.5682Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -0,0 +1,13 @@
/**
* Note: When using the Node.JS APIs, the config file
* doesn't apply. Instead, pass options directly to the APIs.
*
* All configuration options: https://remotion.dev/docs/config
*/
import { Config } from "@remotion/cli/config";
import { enableTailwind } from '@remotion/tailwind-v4';
Config.setVideoImageFormat("jpeg");
Config.setOverwriteOutput(true);
Config.overrideWebpackConfig(enableTailwind);

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1e1e2e;
color: #f8f8f2;
padding: 40px;
width: 1024px;
height: 576px;
}
pre {
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.7;
white-space: pre;
}
.prompt { color: #50fa7b; }
.dim { color: #6272a4; }
.red { color: #ff5555; }
</style>
</head>
<body>
<pre>
<span class="prompt">$</span> actual-cli accounts list --format table
<span class="dim"> id name offBudget closed balance_current</span>
99e8f789-c982-4618-b686-3b331985374b Bank of America false false 6,929.07
cbcec281-9899-4595-9ef3-5e33725555bb Ally Savings false false 3,425.74
5f8e1bc3-136a-4669-9e00-6e36088eebc3 Capital One false false 1,388.56
713d293e-ec6b-4813-aafd-6c9e63579375 HSBC false false <span class="red">-531.05</span>
2ce33e0b-0517-458c-98d6-796c7ede90f7 Vanguard 401k true false 4,399.38
6f6d9cb2-25ea-4b62-a6f6-0a4f2bb167ad Mortgage true false <span class="red">-301,380.72</span>
7f3ff788-9af8-4109-aef5-b0b2971097df House Asset true false 341,300.00
59aec8a4-0c61-4a4a-932e-4ae927f1adb6 Roth IRA true false 3,439.18
<span class="prompt">$</span> <span style="opacity:0.7">_</span>
</pre>
</body>
</html>

View File

@@ -0,0 +1,89 @@
import React from "react";
import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
import { Audio } from "@remotion/media";
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { slide } from "@remotion/transitions/slide";
import { fade } from "@remotion/transitions/fade";
import { staticFile } from "remotion";
import {
FPS,
TITLE_DURATION,
TIER1_SCENE_DURATION,
TIER2_SCENE_DURATION,
OUTRO_DURATION,
TRANSITION_DURATION,
TIER1_FEATURES,
TIER2_FEATURES,
TOTAL_DURATION,
} from "./constants";
import { TitleCard } from "./components/TitleCard";
import { FeatureScene } from "./components/FeatureScene";
import { OutroCard } from "./components/OutroCard";
export function MyComposition() {
const fadeOutDuration = 2 * FPS; // 2 seconds fade out
return (
<AbsoluteFill>
<Audio
src={staticFile("music.mp3")}
volume={(f) =>
interpolate(f, [TOTAL_DURATION - fadeOutDuration, TOTAL_DURATION], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
})
}
/>
<TransitionSeries>
{/* Title scene */}
<TransitionSeries.Sequence durationInFrames={TITLE_DURATION}>
<TitleCard />
</TransitionSeries.Sequence>
{/* Tier 1 feature scenes */}
{TIER1_FEATURES.map((feature) => (
<React.Fragment key={feature.screenshot}>
<TransitionSeries.Transition
presentation={slide({ direction: "from-right" })}
timing={linearTiming({ durationInFrames: TRANSITION_DURATION })}
/>
<TransitionSeries.Sequence
durationInFrames={TIER1_SCENE_DURATION}
premountFor={TRANSITION_DURATION}
>
<FeatureScene feature={feature} />
</TransitionSeries.Sequence>
</React.Fragment>
))}
{/* Tier 2 feature scenes */}
{TIER2_FEATURES.map((feature) => (
<React.Fragment key={feature.screenshot}>
<TransitionSeries.Transition
presentation={slide({ direction: "from-right" })}
timing={linearTiming({ durationInFrames: TRANSITION_DURATION })}
/>
<TransitionSeries.Sequence
durationInFrames={TIER2_SCENE_DURATION}
premountFor={TRANSITION_DURATION}
>
<FeatureScene feature={feature} />
</TransitionSeries.Sequence>
</React.Fragment>
))}
{/* Outro */}
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: TRANSITION_DURATION })}
/>
<TransitionSeries.Sequence
durationInFrames={OUTRO_DURATION}
premountFor={TRANSITION_DURATION}
>
<OutroCard />
</TransitionSeries.Sequence>
</TransitionSeries>
</AbsoluteFill>
);
}

19
my-video/src/Root.tsx Normal file
View File

@@ -0,0 +1,19 @@
import "./index.css";
import { Composition } from "remotion";
import { MyComposition } from "./Composition";
import { FPS, WIDTH, HEIGHT, TOTAL_DURATION } from "./constants";
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="ReleaseVideo"
component={MyComposition}
durationInFrames={TOTAL_DURATION}
fps={FPS}
width={WIDTH}
height={HEIGHT}
/>
</>
);
};

View File

@@ -0,0 +1,50 @@
import { type CSSProperties } from "react";
import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
type AnimatedTextProps = {
text: string;
delay?: number;
fontSize?: number;
fontWeight?: CSSProperties["fontWeight"];
color?: string;
style?: CSSProperties;
};
export function AnimatedText({
text,
delay = 0,
fontSize = 48,
fontWeight = "bold",
color = "#ffffff",
style,
}: AnimatedTextProps) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame: frame - delay,
fps,
config: {
damping: 20,
stiffness: 200,
},
});
const translateY = interpolate(progress, [0, 1], [30, 0]);
const opacity = interpolate(progress, [0, 1], [0, 1]);
return (
<div
style={{
fontSize,
fontWeight,
color,
transform: `translateY(${translateY}px)`,
opacity,
...style,
}}
>
{text}
</div>
);
}

View File

@@ -0,0 +1,131 @@
import { type ReactNode } from "react";
import { useCurrentFrame, useVideoConfig } from "remotion";
import { COLORS } from "../constants";
type BrowserFrameProps = {
children: ReactNode;
accentColor?: string;
};
export function BrowserFrame({
children,
accentColor = COLORS.accentCyan,
}: BrowserFrameProps) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Rotate once every 3 seconds
const angle = (frame / fps) * 120; // 120 degrees per second
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
{/* Rotating gradient border layer */}
<div
style={{
position: "absolute",
inset: -2,
borderRadius: 14,
background: `conic-gradient(from ${angle}deg, transparent 0%, transparent 60%, ${accentColor} 75%, ${accentColor}cc 80%, transparent 95%, transparent 100%)`,
filter: "blur(4px)",
}}
/>
{/* Subtle static glow underneath */}
<div
style={{
position: "absolute",
inset: -1,
borderRadius: 13,
border: `1px solid ${accentColor}22`,
boxShadow: `0 0 30px ${accentColor}15`,
}}
/>
{/* Main frame content */}
<div
style={{
position: "relative",
borderRadius: 12,
overflow: "hidden",
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
}}
>
{/* Title bar */}
<div
style={{
background: "#2d2d3a",
height: 40,
display: "flex",
alignItems: "center",
paddingLeft: 16,
paddingRight: 16,
flexShrink: 0,
position: "relative",
}}
>
{/* Traffic lights */}
<div style={{ display: "flex", gap: 8, zIndex: 1 }}>
<div
style={{
width: 12,
height: 12,
borderRadius: "50%",
background: "#ff5f57",
}}
/>
<div
style={{
width: 12,
height: 12,
borderRadius: "50%",
background: "#ffbd2e",
}}
/>
<div
style={{
width: 12,
height: 12,
borderRadius: "50%",
background: "#28c840",
}}
/>
</div>
{/* Centered title */}
<div
style={{
position: "absolute",
left: 0,
right: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<span
style={{
color: COLORS.textSecondary,
fontSize: 13,
fontFamily: "sans-serif",
}}
>
Actual Budget
</span>
</div>
</div>
{/* Content area */}
<div
style={{
flex: 1,
background: "#0f0f1a",
overflow: "hidden",
}}
>
{children}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,116 @@
import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
import { loadFont } from "@remotion/google-fonts/Inter";
import { type Feature, COLORS, FRAMES_PER_BEAT } from "../constants";
import { AnimatedText } from "./AnimatedText";
import { BrowserFrame } from "./BrowserFrame";
const { fontFamily } = loadFont();
type FeatureSceneProps = {
feature: Feature;
};
export function FeatureScene({ feature }: FeatureSceneProps) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Browser frame slides in from right
const slideProgress = spring({
frame,
fps,
config: {
damping: 20,
stiffness: 200,
},
});
const translateX = interpolate(slideProgress, [0, 1], [400, 0]);
return (
<AbsoluteFill
style={{
background: `radial-gradient(ellipse at 30% 50%, ${feature.accentColor}18 0%, ${COLORS.bgDark} 60%)`,
display: "flex",
flexDirection: "row",
alignItems: "center",
padding: "60px 80px",
gap: 60,
fontFamily,
}}
>
{/* Background gradient overlay */}
<div
style={{
position: "absolute",
inset: 0,
background: `linear-gradient(135deg, ${COLORS.bgDark} 0%, ${COLORS.bgGradient} 100%)`,
zIndex: 0,
}}
/>
{/* Accent glow */}
<div
style={{
position: "absolute",
left: feature.screenshot ? -100 : "50%",
top: "50%",
transform: feature.screenshot ? "translateY(-50%)" : "translate(-50%, -50%)",
width: 500,
height: 500,
borderRadius: "50%",
background: `radial-gradient(ellipse at center, ${feature.accentColor}20 0%, transparent 70%)`,
zIndex: 0,
pointerEvents: "none",
}}
/>
{/* Text area */}
<div
style={{
flex: feature.screenshot ? "0 0 360px" : 1,
display: "flex",
flexDirection: "column",
alignItems: feature.screenshot ? "flex-start" : "center",
justifyContent: feature.screenshot ? "flex-start" : "center",
gap: 16,
zIndex: 1,
}}
>
<AnimatedText
text={feature.title}
delay={0}
fontSize={feature.screenshot ? 42 : 56}
fontWeight="bold"
color={feature.accentColor}
style={{ lineHeight: 1.2, textAlign: feature.screenshot ? "left" : "center" }}
/>
<AnimatedText
text={feature.tagline}
delay={FRAMES_PER_BEAT}
fontSize={feature.screenshot ? 22 : 28}
fontWeight={400}
color={COLORS.textSecondary}
style={{ lineHeight: 1.5, textAlign: feature.screenshot ? "left" : "center" }}
/>
</div>
{/* Browser frame with screenshot */}
{feature.screenshot && (
<div
style={{
flex: 1,
transform: `translateX(${translateX}px)`,
zIndex: 1,
}}
>
<BrowserFrame accentColor={feature.accentColor}>
<Img
src={staticFile(`screenshots/${feature.screenshot}`)}
style={{ width: "100%", display: "block" }}
/>
</BrowserFrame>
</div>
)}
</AbsoluteFill>
);
}

View File

@@ -0,0 +1,201 @@
import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
import { loadFont } from "@remotion/google-fonts/Inter";
import { COLORS, FRAMES_PER_BEAT, OUTRO_DURATION } from "../constants";
const { fontFamily } = loadFont();
const STATS = [
{ number: "4", label: "Features" },
{ number: "45", label: "Enhancements" },
{ number: "32", label: "Bugfixes" },
];
type StatCardProps = {
number: string;
label: string;
delay: number;
};
function StatCard({ number, label, delay }: StatCardProps) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame: frame - delay,
fps,
config: {
damping: 15,
stiffness: 120,
},
});
const translateY = interpolate(progress, [0, 1], [40, 0]);
const opacity = interpolate(progress, [0, 1], [0, 1]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 8,
transform: `translateY(${translateY}px)`,
opacity,
}}
>
<div
style={{
fontSize: 80,
fontWeight: "bold",
color: COLORS.accentGold,
lineHeight: 1,
}}
>
{number}
</div>
<div
style={{
fontSize: 22,
color: COLORS.textSecondary,
fontWeight: 500,
}}
>
{label}
</div>
</div>
);
}
export function OutroCard() {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const statsEndDelay = STATS.length * FRAMES_PER_BEAT * 2;
// CTA appears after stats
const ctaProgress = spring({
frame: frame - statsEndDelay,
fps,
config: {
damping: 20,
stiffness: 150,
},
});
const ctaOpacity = interpolate(ctaProgress, [0, 1], [0, 1]);
const ctaTranslateY = interpolate(ctaProgress, [0, 1], [20, 0]);
// Fade out in last 1 second (30 frames)
const fadeOutOpacity = interpolate(
frame,
[OUTRO_DURATION - 30, OUTRO_DURATION],
[1, 0],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}
);
return (
<AbsoluteFill
style={{
background: `radial-gradient(ellipse at center, ${COLORS.bgGradient} 0%, ${COLORS.bgDark} 100%)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 48,
fontFamily,
opacity: fadeOutOpacity,
}}
>
{/* Background glow */}
<div
style={{
position: "absolute",
width: 700,
height: 700,
borderRadius: "50%",
background: `radial-gradient(ellipse at center, ${COLORS.accentGold}10 0%, transparent 70%)`,
pointerEvents: "none",
}}
/>
{/* Stats row */}
<div
style={{
display: "flex",
flexDirection: "row",
gap: 80,
alignItems: "center",
}}
>
{STATS.map((stat, i) => (
<StatCard
key={stat.label}
number={stat.number}
label={stat.label}
delay={i * FRAMES_PER_BEAT * 2}
/>
))}
</div>
{/* CTA */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 12,
opacity: ctaOpacity,
transform: `translateY(${ctaTranslateY}px)`,
}}
>
<div
style={{
fontSize: 36,
fontWeight: "bold",
color: COLORS.white,
}}
>
Update now
</div>
<div
style={{
fontSize: 22,
color: COLORS.accentCyan,
fontWeight: 500,
}}
>
actualbudget.org
</div>
</div>
{/* Logo at bottom */}
<div
style={{
position: "absolute",
bottom: 40,
opacity: 0.3,
display: "flex",
alignItems: "center",
gap: 12,
}}
>
<Img
src={staticFile("logo.svg")}
style={{ width: 36, height: 36 }}
/>
<span
style={{
color: COLORS.textSecondary,
fontSize: 16,
fontWeight: 500,
}}
>
Actual Budget
</span>
</div>
</AbsoluteFill>
);
}

View File

@@ -0,0 +1,124 @@
import { AbsoluteFill, Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
import { loadFont } from "@remotion/google-fonts/Inter";
import { COLORS } from "../constants";
const { fontFamily } = loadFont();
export function TitleCard() {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Logo springs in with heavy config
const logoProgress = spring({
frame,
fps,
config: {
damping: 200,
},
});
const logoScale = interpolate(logoProgress, [0, 1], [0.5, 1]);
const logoOpacity = interpolate(logoProgress, [0, 1], [0, 1]);
// Version badge fades in at 1-2s (30-60 frames)
const badgeOpacity = interpolate(frame, [30, 60], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// "Here's what's new" subtitle springs in at 2s (frame 60)
const subtitleProgress = spring({
frame: frame - 60,
fps,
config: {
damping: 200,
stiffness: 200,
},
});
const subtitleTranslateY = interpolate(subtitleProgress, [0, 1], [20, 0]);
const subtitleOpacity = interpolate(subtitleProgress, [0, 1], [0, 1]);
return (
<AbsoluteFill
style={{
background: `radial-gradient(ellipse at center, ${COLORS.bgGradient} 0%, ${COLORS.bgDark} 100%)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
fontFamily,
}}
>
{/* Radial glow */}
<div
style={{
position: "absolute",
width: 600,
height: 600,
borderRadius: "50%",
background: `radial-gradient(ellipse at center, ${COLORS.accentPurple}22 0%, transparent 70%)`,
pointerEvents: "none",
}}
/>
{/* Logo + title */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 24,
transform: `scale(${logoScale})`,
opacity: logoOpacity,
}}
>
<Img
src={staticFile("logo.svg")}
style={{ width: 120, height: 120 }}
/>
<div
style={{
fontSize: 64,
fontWeight: "bold",
color: COLORS.white,
letterSpacing: -1,
}}
>
Actual Budget
</div>
</div>
{/* Version badge */}
<div
style={{
marginTop: 20,
opacity: badgeOpacity,
background: COLORS.accentPurple,
color: COLORS.white,
fontSize: 22,
fontWeight: 600,
padding: "6px 20px",
borderRadius: 999,
letterSpacing: 1,
}}
>
v26.4.0
</div>
{/* Subtitle */}
<div
style={{
marginTop: 32,
fontSize: 32,
color: COLORS.textSecondary,
fontWeight: 400,
opacity: subtitleOpacity,
transform: `translateY(${subtitleTranslateY}px)`,
}}
>
{"Here's what's new"}
</div>
</AbsoluteFill>
);
}

73
my-video/src/constants.ts Normal file
View File

@@ -0,0 +1,73 @@
export const FPS = 30;
export const WIDTH = 1280;
export const HEIGHT = 720;
export const BPM = 139;
export const FRAMES_PER_BEAT = Math.round((60 / BPM) * FPS); // ~13
export const TITLE_DURATION = 120;
export const TIER1_SCENE_DURATION = 160;
export const TIER2_SCENE_DURATION = 180;
export const OUTRO_DURATION = 240;
export const TRANSITION_DURATION = FRAMES_PER_BEAT;
export const COLORS = {
bgDark: "#1a1a2e",
bgGradient: "#16213e",
accentCyan: "#00d2ff",
accentCoral: "#e94560",
accentGold: "#ffd700",
accentPurple: "#7c3aed",
white: "#ffffff",
textSecondary: "#94a3b8",
};
export type Feature = {
title: string;
tagline: string;
screenshot?: string;
accentColor: string;
};
export const TIER1_FEATURES: Feature[] = [
{
title: "Donut Chart Reports",
tagline: "Beautiful category breakdowns",
screenshot: "donut-chart.png",
accentColor: COLORS.accentCyan,
},
{
title: "Budget Notes",
tagline: "Monthly per-category notes",
screenshot: "budget-notes.png",
accentColor: COLORS.accentCyan,
},
];
export const TIER2_FEATURES: Feature[] = [
{
title: "Drag & Drop Reordering",
tagline: "Reorder transactions — your way",
screenshot: "drag-drop.png",
accentColor: COLORS.accentCyan,
},
{
title: "Actual CLI",
tagline: "Your budget, from the command line",
screenshot: "cli-tool.png",
accentColor: COLORS.accentCyan,
},
{
title: "And much more...",
tagline: "Thanks to all the contributors",
accentColor: COLORS.accentCoral,
},
];
// Total = TITLE + 3*TIER1 + 4*TIER2 + OUTRO - 8*TRANSITION
export const TOTAL_DURATION =
TITLE_DURATION +
TIER1_FEATURES.length * TIER1_SCENE_DURATION +
TIER2_FEATURES.length * TIER2_SCENE_DURATION +
OUTRO_DURATION -
(TIER1_FEATURES.length + TIER2_FEATURES.length + 1) * TRANSITION_DURATION;

1
my-video/src/index.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

4
my-video/src/index.ts Normal file
View File

@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);

15
my-video/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"lib": ["es2015"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true
},
"exclude": ["remotion.config.ts"]
}