This document describes the new features added to fix the textSegments display issue and enhance the template with validation, factory pattern, and multi-story support.
When using multiple textSegments, they were not displaying properly. The animations appeared broken or segments didn't show at all.
The TextLayer component was receiving a startFrame prop and subtracting it from the current frame. However, since the component was already inside a Sequence that starts at startFrame, the frame counter was already 0-based. This caused negative localFrame values and broke all animations.
Removed the redundant startFrame prop and subtraction. The Sequence component already handles the frame offset, so the TextLayer now uses the frame directly.
Before:
<Sequence from={segment.startFrame} durationInFrames={segment.durationInFrames}>
<TextLayer segment={segment} startFrame={segment.startFrame} />
</Sequence>
// Inside TextLayer:
const localFrame = frame - startFrame; // ❌ Double subtraction!After:
<Sequence from={segment.startFrame} durationInFrames={segment.durationInFrames}>
<TextLayer segment={segment} />
</Sequence>
// Inside TextLayer:
const localFrame = frame; // ✅ Correct!The MultiSegmentTest composition demonstrates the fix with 4 text segments displaying correctly at different times.
Added Zod schemas for runtime type validation with helpful error messages.
import { z } from 'zod';
// Background image schema
export const backgroundImageSchema = z.object({
url: z.string().url('Invalid image URL'),
animation: z.enum(['zoom-in', 'zoom-out', 'fade', 'none']).optional(),
});
// Text segment schema
export const textSegmentSchema = z.object({
text: z.string().min(1, 'Text cannot be empty'),
startFrame: z.number().int().nonnegative('Start frame must be non-negative'),
durationInFrames: z.number().int().positive('Duration must be positive'),
animation: z.enum(['fade', 'slide', 'typing', 'none']).optional(),
});
// News story props schema
export const newsStoryPropsSchema = z.object({
backgroundImages: z.array(backgroundImageSchema).min(1, 'At least one background image is required'),
textSegments: z.array(textSegmentSchema).min(1, 'At least one text segment is required'),
publishDate: z.string().min(1, 'Publish date is required'),
tags: z.array(z.string()).min(1, 'At least one tag is required'),
copyright: z.string().min(1, 'Copyright text is required'),
});The NewsStory component automatically validates props and shows clear error messages if validation fails:
export const NewsStory: React.FC<NewsStoryProps> = (props) => {
const validationResult = newsStoryPropsSchema.safeParse(props);
if (!validationResult.success) {
return (
<AbsoluteFill>
<h1>Validation Error</h1>
{validationResult.error.errors.map((err) => (
<div>{err.path.join('.')}: {err.message}</div>
))}
</AbsoluteFill>
);
}
// ... render story
};- Type Safety: Runtime validation in addition to TypeScript types
- Clear Errors: Helpful error messages point to exactly what's wrong
- Validation: Prevents invalid data from causing runtime errors
- Documentation: Schema serves as executable documentation
A fluent builder API for creating news stories with automatic validation.
import { createStory } from './storiesFactory';
const story = createStory()
.withBackgroundImage(
'https://images.unsplash.com/photo-1504711434969-e33886168f5c?w=1080&h=1920',
'zoom-in'
)
.withTextSegment('Breaking News: Important update', 30, 90, 'fade')
.withTextSegment('More details coming soon', 130, 90, 'slide')
.withPublishDate('2024-03-15')
.withTags('#News', '#Breaking', '#Update')
.withCopyright('© 2024 News Agency')
.build();Creates a new story builder instance.
withBackgroundImage(url: string, animation?: 'zoom-in' | 'zoom-out' | 'fade' | 'none'): StoryBuilder
Adds a background image to the story.
- url: Image URL (must be valid URL)
- animation: Optional animation type (default: 'none')
withTextSegment(text: string, startFrame: number, durationInFrames: number, animation?: 'fade' | 'slide' | 'typing' | 'none'): StoryBuilder
Adds a text segment to the story.
- text: Text to display (cannot be empty)
- startFrame: Frame when text appears (must be non-negative)
- durationInFrames: How long text stays visible (must be positive)
- animation: Optional animation type (default: 'fade')
Sets the publication date.
- date: Date string (e.g., '2024-03-15')
Adds tags to the story.
- tags: One or more tag strings (e.g., '#News', '#Breaking')
Sets the copyright text.
- copyright: Copyright string (e.g., '© 2024 Company')
Builds and validates the story. Throws an error if validation fails.
import { createStoryFromTemplate } from './storiesFactory';
const baseTemplate = {
publishDate: '2024-03-15',
tags: ['#News'],
copyright: '© 2024 News Agency',
};
const story = createStoryFromTemplate(baseTemplate)
.withBackgroundImage(url, 'zoom-in')
.withTextSegment('Custom headline', 30, 90, 'fade')
.build();import { validateStory } from './storiesFactory';
const result = validateStory(someData);
if (result.valid) {
console.log('Story is valid!');
} else {
console.error('Validation errors:', result.errors);
}The MultiStory component allows you to combine multiple stories into a single video with smooth transitions between them.
import { MultiStory } from './components/MultiStory';
const stories = [
{
story: storyData1,
durationInFrames: 240,
transition: 'fade',
transitionDuration: 20,
},
{
story: storyData2,
durationInFrames: 240,
transition: 'slide-left',
transitionDuration: 25,
},
];
<Composition
id="MultiStoryVideo"
component={MultiStory}
durationInFrames={480} // Sum of all story durations
fps={30}
width={1080}
height={1920}
defaultProps={{ stories }}
/>Smooth opacity transition in and out.
- Entry: Fades from 0 to 1
- Exit: Fades from 1 to 0
Slides in from right and out to left.
- Entry: Translates from 100% to 0%
- Exit: Translates from 0% to -100%
Slides in from left and out to right.
- Entry: Translates from -100% to 0%
- Exit: Translates from 0% to 100%
Zooms in on entry and out on exit.
- Entry: Scales from 0.8 to 1.0 with fade
- Exit: Scales from 1.0 to 1.2 with fade
No transition, instant cut between stories.
Each transition has an entry and exit phase:
- Entry: First N frames of the story
- Exit: Last N frames of the story
- transitionDuration: Number of frames for each phase (default: 15)
Example with 20-frame transitions:
- Frames 0-20: Entry transition
- Frames 20-220: Story content (fully visible)
- Frames 220-240: Exit transition
import { createStory } from './storiesFactory';
import { MultiStory } from './components/MultiStory';
// Create stories using factory
const story1 = createStory()
.withBackgroundImage('url1', 'zoom-in')
.withTextSegment('First headline', 30, 150, 'fade')
.withPublishDate('2024-03-15')
.withTags('#Breaking')
.withCopyright('© 2024')
.build();
const story2 = createStory()
.withBackgroundImage('url2', 'fade')
.withTextSegment('Second headline', 30, 150, 'slide')
.withPublishDate('2024-03-15')
.withTags('#Update')
.withCopyright('© 2024')
.build();
// Combine into multi-story
const multiStoryConfig = [
{
story: story1,
durationInFrames: 240,
transition: 'fade',
transitionDuration: 20,
},
{
story: story2,
durationInFrames: 240,
transition: 'slide-left',
transitionDuration: 25,
},
];
// Use in composition
<Composition
id="NewsReel"
component={MultiStory}
durationInFrames={480}
fps={30}
width={1080}
height={1920}
defaultProps={{ stories: multiStoryConfig }}
/>The template now includes several pre-built compositions demonstrating all features:
- BreakingNews: Breaking news with fade transition (240 frames)
- TechNews: Technology news with typing effect (240 frames)
- SportsNews: Sports news with multiple segments (300 frames)
- MultiSegmentTest: Test with 4 text segments (330 frames) - demonstrates the bug fix
-
MultiStory: 3 stories with different transitions (780 frames / 26 seconds)
- Breaking news (fade transition)
- Tech news (slide-left transition)
- Sports news (zoom transition)
-
QuickNews: 3 quick stories in succession (540 frames / 18 seconds)
- Story 1 (slide-right transition)
- Story 2 (slide-left transition)
- Story 3 (fade transition)
// Create a simple story
const myStory = createStory()
.withBackgroundImage('https://example.com/image.jpg', 'zoom-in')
.withTextSegment('Your headline here', 30, 180, 'fade')
.withPublishDate('2024-03-15')
.withTags('#YourTag')
.withCopyright('© 2024 Your Company')
.build();// Combine multiple stories
import { multiStoryExample } from './factoryExamples';
// Already configured with 3 stories and transitions
// Use in composition or create your own// Add as many text segments as you need
const story = createStory()
.withBackgroundImage(url, 'none')
.withTextSegment('Segment 1', 30, 60, 'fade')
.withTextSegment('Segment 2', 100, 60, 'slide')
.withTextSegment('Segment 3', 170, 60, 'typing')
.withTextSegment('Segment 4', 240, 60, 'fade')
.withPublishDate('2024-03-15')
.withTags('#Test')
.withCopyright('© 2024')
.build();
// All segments will display correctly!If you have existing story configurations, you can either:
- Keep using the direct approach (still works):
const story: NewsStoryProps = {
backgroundImages: [...],
textSegments: [...],
// ...
};- Migrate to factory (recommended):
const story = createStoryFromTemplate(existingStory).build();Validation is automatic in the NewsStory component. If you need to validate manually:
import { validateStory } from './storiesFactory';
const result = validateStory(myStoryData);
if (!result.valid) {
console.error(result.errors);
}- Use the factory for type safety and validation
- Plan your timing - ensure text segments don't overlap unless intended
- Choose appropriate transitions - match the mood of your content
- Test with MultiSegmentTest - verify all segments display correctly
- Keep transition durations balanced - too long feels slow, too short feels jarring
✅ Fixed! This issue has been resolved. All text segments now display correctly.
Check the error messages - they point to exactly what needs to be fixed.
Adjust transitionDuration - try 15-30 frames for most cases.
Adjust durationInFrames for each story and update the total composition duration.