feat: replace Remotion with custom Bradly Engine (Phases 1-3)

- Phase 1: Custom animation engine (interpolate, spring, Easing)
- Phase 2: Custom composition components (AbsoluteFill, Sequence, Img, Video, Audio)
- Phase 3: BradlyPlayer with rAF frame loop, imperative API, controls

Migrated 24 files from remotion/@remotion/player imports to src/engine/.
All type errors from migration resolved. Pre-existing errors remain unchanged.
This commit is contained in:
2026-06-02 05:20:43 -05:00
parent 0aa44afa43
commit ff07d8c492
39 changed files with 1451 additions and 55 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { AbsoluteFill, useCurrentFrame } from 'remotion';
import { AbsoluteFill } from '../engine/components';
import { useCurrentFrame } from '../engine/player';
import { RenderProps } from '../types';
import { useCanvasDrag } from './composition/useCanvasDrag';
import { BackgroundLayer } from './composition/BackgroundLayer';
+2 -2
View File
@@ -1,5 +1,5 @@
import React, { RefObject } from 'react';
import { PlayerRef } from '@remotion/player';
import type { BradlyPlayerRef } from '../engine/player';
import { Type, Image as ImageIcon, Trash2, Film, Upload, Wand2, Play, ImagePlus, Square, Plus } from 'lucide-react';
import { TimelineElement, MediaFilter, TimelineLayer, DesignMD } from '../types';
import { AudioLayerPanel } from './properties/AudioLayerPanel';
@@ -21,7 +21,7 @@ interface StudioPropertiesProps {
textOverlay: string;
setTextOverlay: (text: string) => void;
activeLayerId: string;
playerRef: RefObject<PlayerRef | null>;
playerRef: RefObject<BradlyPlayerRef | null>;
activeTool: 'select' | 'text' | 'sticker' | 'transitions' | 'media';
designMD: DesignMD;
outputFormat?: 'video' | 'image';
+2 -2
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, RefObject } from 'react';
import { Layers, GripVertical } from 'lucide-react';
import { TimelineElement, TimelineLayer, DesignMD } from '../types';
import { PlayerRef } from '@remotion/player';
import type { BradlyPlayerRef } from '../engine/player';
import { DragState, getTrackBgClass } from './timeline/timelineUtils';
import { TimelineControls } from './timeline/TimelineControls';
import { TimelineRuler } from './timeline/TimelineRuler';
@@ -27,7 +27,7 @@ interface StudioTimelineProps {
setActiveLayerId: (id: string) => void;
selectedElementId: string | null;
setSelectedElementId: (id: string | null) => void;
playerRef: RefObject<PlayerRef | null>;
playerRef: RefObject<BradlyPlayerRef | null>;
activeTool: 'select' | 'text' | 'sticker' | 'media' | 'transitions';
outputFormat?: 'video' | 'image';
designMD?: DesignMD;
+3 -3
View File
@@ -1,5 +1,5 @@
import React, { RefObject, useState, useCallback, useEffect } from 'react';
import { Player, PlayerRef } from '@remotion/player';
import { BradlyPlayer, BradlyPlayerRef } from '../engine/player';
import { BrandComposition } from './BrandComposition';
import { RenderProps, TimelineElement } from '../types';
import { PlaySquare } from 'lucide-react';
@@ -13,7 +13,7 @@ interface StudioWorkspaceProps {
activeTool: 'select' | 'text' | 'sticker' | 'media' | 'transitions';
setSelectedElementId: (id: string | null) => void;
selectedElementId?: string | null;
playerRef: RefObject<PlayerRef | null>;
playerRef: RefObject<BradlyPlayerRef | null>;
compositionProps: RenderProps;
durationInFrames: number;
timelineElements?: TimelineElement[];
@@ -560,7 +560,7 @@ export const StudioWorkspace: React.FC<StudioWorkspaceProps> = ({
</>
) : undefined}
>
<Player
<BradlyPlayer
ref={playerRef}
component={BrandComposition}
inputProps={{
@@ -1,14 +1,8 @@
import React, { useMemo, useState, useRef, useCallback, useEffect } from 'react';
import { Player } from '@remotion/player';
import {
AbsoluteFill,
Sequence,
Video,
useCurrentFrame,
useVideoConfig,
interpolate,
spring,
} from 'remotion';
import { BradlyPlayer } from '../../../engine/player';
import { AbsoluteFill, Sequence, Video } from '../../../engine/components';
import { useCurrentFrame, useVideoConfig } from '../../../engine/player';
import { interpolate, spring } from '../../../engine/animation';
import { DesignMD, CompanyProfile } from '../../../types';
import { CanvasWorkspace } from '../../ui/CanvasWorkspace';
@@ -329,7 +323,7 @@ export const PreviewRemotion: React.FC<PreviewRemotionProps> = ({ designMD, comp
</>
) : undefined}
>
<Player
<BradlyPlayer
ref={playerRef}
component={SampleComposition}
inputProps={inputProps}
@@ -1,5 +1,5 @@
import React from 'react';
import { Sequence, AbsoluteFill, Img, Video } from 'remotion';
import { Sequence, AbsoluteFill, Img, Video } from '../../engine/components';
import { TimelineElement, TimelineLayer, MediaFilter } from '../../types';
const getFilterStyle = (filter?: MediaFilter): React.CSSProperties => {
+1 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { AbsoluteFill } from 'remotion';
import { AbsoluteFill } from '../../engine/components';
import { DesignMD } from '../../types';
interface BrandOverlayProps {
@@ -1,5 +1,5 @@
import React, { useRef, useEffect, useMemo, useCallback } from 'react';
import { useCurrentFrame, useVideoConfig } from 'remotion';
import { useCurrentFrame, useVideoConfig } from '../../engine/player';
import {
applyChromaKey,
hexToRgb,
@@ -1,5 +1,6 @@
import React, { RefObject, useEffect } from 'react';
import { Sequence, AbsoluteFill, Img, Video, Audio, interpolate } from 'remotion';
import { Sequence, AbsoluteFill, Img, Video, Audio } from '../../engine/components';
import { interpolate } from '../../engine/animation';
import { TimelineElement, TimelineLayer, DesignMD } from '../../types';
import { calculateElementTransitions } from './useTransitions';
import { resolveKeyframes } from './keyframeEngine';
+1 -1
View File
@@ -1,4 +1,4 @@
import { interpolate, Easing } from 'remotion';
import { interpolate, Easing } from '../../engine/animation';
import { AnimationKeyframe, EasingType } from '../../types';
interface KeyframeDefaults {
+1 -1
View File
@@ -1,4 +1,4 @@
import { interpolate, spring } from 'remotion';
import { interpolate, spring } from '../../engine/animation';
import { TimelineElement } from '../../types';
import { resolveKeyframes } from './keyframeEngine';
@@ -10,7 +10,7 @@ import React, { useState, useMemo, useCallback, useRef } from 'react';
import {
X, ChevronLeft, ChevronRight, AlertTriangle, Eye,
} from 'lucide-react';
import { Player, PlayerRef } from '@remotion/player';
import { BradlyPlayer, BradlyPlayerRef } from '../../engine/player';
import { BrandComposition } from '../BrandComposition';
import {
compileExpressToTimeline, getAspectDimensions, getTemplateDuration,
@@ -132,7 +132,7 @@ const PieceThumbnail: React.FC<{
>
{/* Render the piece */}
{!isVideo ? (
<Player
<BradlyPlayer
key={playerKey}
component={BrandComposition}
inputProps={inputProps}
@@ -217,7 +217,7 @@ export const BatchPreviewGrid: React.FC<BatchPreviewGridProps> = ({
onActivePieceChange,
}) => {
const [carouselIndex, setCarouselIndex] = useState<number | null>(null);
const carouselPlayerRef = useRef<PlayerRef>(null);
const carouselBradlyPlayerRef = useRef<BradlyPlayerRef>(null);
const dimensions = useMemo(() => getAspectDimensions(template.aspectRatio), [template.aspectRatio]);
const totalDuration = useMemo(() => getTemplateDuration(template), [template]);
@@ -364,7 +364,7 @@ export const BatchPreviewGrid: React.FC<BatchPreviewGridProps> = ({
maxHeight: 'calc(100% - 120px)',
}}
>
<Player
<BradlyPlayer
key={videoPlayerKey}
component={BrandComposition}
inputProps={videoInputProps}
@@ -499,9 +499,9 @@ export const BatchPreviewGrid: React.FC<BatchPreviewGridProps> = ({
maxHeight: 'calc(100vh - 100px)',
}}
>
<Player
<BradlyPlayer
key={`carousel-${carouselIndex}`}
ref={carouselPlayerRef}
ref={carouselBradlyPlayerRef}
component={BrandComposition}
inputProps={carouselInputProps}
durationInFrames={totalFrames}
+2 -2
View File
@@ -3,7 +3,7 @@ import {
ArrowLeft, Sparkles, Zap, Play, ChevronRight, ChevronLeft, FileText, Download, Film,
Layers, Package,
} from 'lucide-react';
import { PlayerRef } from '@remotion/player';
import type { BradlyPlayerRef } from '../../engine/player';
import {
ExpressTemplate, CompanyProfile, DesignMD,
TemplateField, BrandSource,
@@ -95,7 +95,7 @@ export const ProductionForm: React.FC<ProductionFormProps> = ({
const [activeBatchPieceIndex, setActiveBatchPieceIndex] = useState(0);
const backgroundFieldId = useMemo(() => findBackgroundFieldId(template), [template]);
const playerRef = useRef<PlayerRef>(null);
const playerRef = useRef<BradlyPlayerRef>(null);
const designMD = brand.design;
const fps = 30;
+3 -3
View File
@@ -1,6 +1,6 @@
import React, { useState, useMemo, useCallback, useRef } from 'react';
import { ArrowLeft, Zap, Wrench, Download, ChevronRight, Play, Pause, RotateCcw } from 'lucide-react';
import { Player, PlayerRef } from '@remotion/player';
import { BradlyPlayer, BradlyPlayerRef } from '../../engine/player';
import { ExpressTemplate, DesignMD, TimelineElement, TimelineLayer, CompanyProfile } from '../../types';
import { BrandComposition } from '../BrandComposition';
import { ExpressTemplateGallery } from './ExpressTemplateGallery';
@@ -42,7 +42,7 @@ export const ExpressEditor: React.FC<ExpressEditorProps> = ({
const [showLogo, setShowLogo] = useState(true);
const [overlayOpacity, setOverlayOpacity] = useState(0);
const playerRef = useRef<PlayerRef>(null);
const playerRef = useRef<BradlyPlayerRef>(null);
const handleSelectTemplate = useCallback((template: ExpressTemplate) => {
setSelectedTemplate(template);
@@ -208,7 +208,7 @@ export const ExpressEditor: React.FC<ExpressEditorProps> = ({
maxHeight: 'calc(100% - 80px)',
}}
>
<Player
<BradlyPlayer
ref={playerRef}
component={BrandComposition}
inputProps={{
+2 -2
View File
@@ -1,12 +1,12 @@
import React, { useRef, useState, useCallback, RefObject } from 'react';
import { Play, Pause, RotateCcw, Volume2 } from 'lucide-react';
import { PlayerRef } from '@remotion/player';
import type { BradlyPlayerRef } from '../../engine/player';
import { TimelineElement } from '../../types';
import { TimelineRuler } from '../timeline/TimelineRuler';
import { TimelinePlayhead } from '../timeline/TimelinePlayhead';
interface ExpressTimelineProps {
playerRef: RefObject<PlayerRef | null>;
playerRef: RefObject<BradlyPlayerRef | null>;
elements: TimelineElement[];
durationInFrames: number;
selectedSlotId: string | null;
@@ -1,7 +1,7 @@
import React, { RefObject } from 'react';
import { Music } from 'lucide-react';
import { TimelineElement } from '../../types';
import { PlayerRef } from '@remotion/player';
import type { BradlyPlayerRef } from '../../engine/player';
import { uploadMedia } from '../../utils/mediaUploader';
import { FileDropZone } from '../ui/FileDropZone';
import { getAudioDuration, durationToFrames } from '../../utils/audioMetadata';
@@ -10,7 +10,7 @@ interface AudioLayerPanelProps {
activeLayerId: string;
setTimelineElements: React.Dispatch<React.SetStateAction<TimelineElement[]>>;
timelineElements: TimelineElement[];
playerRef: RefObject<PlayerRef | null>;
playerRef: RefObject<BradlyPlayerRef | null>;
endFrameLimit?: number;
}
@@ -1,7 +1,7 @@
import React, { useCallback, RefObject } from 'react';
import { Film, Play, Camera, Download, Grid3x3, Palette, Maximize } from 'lucide-react';
import { CollapsibleSection } from '../ui/CollapsibleSection';
import { PlayerRef } from '@remotion/player';
import type { BradlyPlayerRef } from '../../engine/player';
import { EXPORT_PRESETS } from '../../config/constants';
import { TimelineElement, TimelineLayer } from '../../types';
import { ProjectStats } from '../ui/ProjectStats';
@@ -11,7 +11,7 @@ import { BulkActionsBar } from '../ui/BulkActionsBar';
interface GlobalSettingsPanelProps {
textOverlay: string;
setTextOverlay: (text: string) => void;
playerRef?: RefObject<PlayerRef | null>;
playerRef?: RefObject<BradlyPlayerRef | null>;
aspectRatio?: '16:9' | '9:16' | '1:1' | '4:5' | '4:3';
outputFormat?: 'video' | 'image';
onExportClick?: () => void;
+4 -4
View File
@@ -1,6 +1,6 @@
import React, { useMemo, useState, useCallback, useRef, useEffect } from 'react';
import { Play, Pause, RotateCcw, Film } from 'lucide-react';
import { Player, PlayerRef } from '@remotion/player';
import { BradlyPlayer, BradlyPlayerRef } from '../../engine/player';
import { ExpressTemplate, CompanyProfile, DesignMD } from '../../types';
import { BrandComposition } from '../BrandComposition';
import { compileExpressToTimeline, getAspectDimensions, getTemplateDuration } from '../../utils/expressCompiler';
@@ -31,7 +31,7 @@ export interface LivePreviewCanvasProps {
/** Callback when user navigates to a scene */
onSceneChange?: (sceneId: string) => void;
/** External player ref */
playerRef?: React.RefObject<PlayerRef>;
playerRef?: React.RefObject<BradlyPlayerRef>;
/** Status label (e.g. "Listo" / "Faltan campos") */
statusLabel?: string;
/** Whether all required fields are complete */
@@ -67,7 +67,7 @@ export const LivePreviewCanvas: React.FC<LivePreviewCanvasProps> = ({
statusLabel,
isComplete = false,
}) => {
const internalRef = useRef<PlayerRef>(null);
const internalRef = useRef<BradlyPlayerRef>(null);
const playerRef = externalRef || internalRef;
const [isPlaying, setIsPlaying] = useState(false);
@@ -246,7 +246,7 @@ export const LivePreviewCanvas: React.FC<LivePreviewCanvasProps> = ({
maxHeight: 'calc(100% - 160px)',
}}
>
<Player
<BradlyPlayer
key={playerKey}
ref={playerRef}
component={BrandComposition}
+2 -2
View File
@@ -1,8 +1,8 @@
import React, { useEffect, useRef, RefObject } from 'react';
import { PlayerRef } from '@remotion/player';
import type { BradlyPlayerRef } from '../../engine/player';
interface TimelinePlayheadProps {
playerRef: RefObject<PlayerRef | null>;
playerRef: RefObject<BradlyPlayerRef | null>;
durationInFrames: number;
onPointerDown: (e: React.PointerEvent<HTMLDivElement>) => void;
onPointerMove?: (e: React.PointerEvent<HTMLDivElement>) => void;
+2 -2
View File
@@ -1,8 +1,8 @@
import React, { useState, useEffect, useRef } from 'react';
import { PlayerRef } from '@remotion/player';
import type { BradlyPlayerRef } from '../../engine/player';
interface PlaybackInfoProps {
playerRef: React.RefObject<PlayerRef | null>;
playerRef: React.RefObject<BradlyPlayerRef | null>;
durationInFrames: number;
fps?: number;
elementCount?: number;