import React from 'react'; import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, Img, Easing, } from 'remotion'; // ============================================================================= // COMPOSITION CONFIG // ============================================================================= export const compositionConfig = { id: 'SideScrollerGame', durationInSeconds: 5, fps: 30, width: 1920, height: 1080, }; // ============================================================================= // STYLE CONSTANTS // ============================================================================= const COLORS = { primary: '#87CEEB', secondary: '#5BA3C6', accent: '#FFD700', background: '#87CEEB', text: '#FFFFFF', } as const; const TYPOGRAPHY = { fontFamily: 'Inter, system-ui, sans-serif', } as const; // ============================================================================= // ASSET CONFIGURATION // ============================================================================= const ASSETS = { bgLayer1: 'https://s3.powerkit.dev/videofiles/studio/assets/1/55a2e7b96299.png', bgLayer2: 'https://s3.powerkit.dev/videofiles/studio/assets/1/65219733d887.png', bgLayer3: 'https://s3.powerkit.dev/videofiles/studio/assets/1/be0ecd3f350c.png', character: 'https://s3.powerkit.dev/videofiles/studio/assets/1/0ea5c139c264.png', } as const; const BACKGROUND_DIMENSIONS = { width: 320, height: 180, } as const; const SPRITE_CONFIG = { frameWidth: 56, frameHeight: 56, columns: 8, } as const; // ============================================================================= // ANIMATION CONFIGURATION // ============================================================================= const ANIMATIONS = { run: { row: 2, frames: 8 }, jump: { row: 4, frames: 6 }, punch: { row: 1, frames: 8 }, } as const; const TIMING = { runEnd: 60, jumpEnd: 105, } as const; const EASINGS = { easeOut: Easing.bezier(0.33, 1, 0.68, 1), easeInOut: Easing.bezier(0.37, 0, 0.63, 1), } as const; // ============================================================================= // SPRITE ANIMATOR COMPONENT // ============================================================================= interface SpriteAnimatorProps { src: string; frameWidth: number; frameHeight: number; row: number; totalFrames: number; animationFps?: number; scale?: number; style?: React.CSSProperties; startFrame?: number; playOnce?: boolean; } const SpriteAnimator: React.FC = ({ src, frameWidth, frameHeight, row, totalFrames, animationFps = 12, scale = 1, style, startFrame = 0, playOnce = false, }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const relativeFrame = Math.max(0, frame - startFrame); const rawSpriteFrame = Math.floor((relativeFrame / fps) * animationFps); const spriteFrame = playOnce ? Math.min(rawSpriteFrame, totalFrames - 1) : rawSpriteFrame % totalFrames; const xOffset = spriteFrame * frameWidth; const yOffset = row * frameHeight; const spriteStyle: React.CSSProperties = { width: frameWidth * scale, height: frameHeight * scale, backgroundImage: `url(${src})`, backgroundPosition: `-${xOffset * scale}px -${yOffset * scale}px`, backgroundSize: `${SPRITE_CONFIG.columns * frameWidth * scale}px auto`, backgroundRepeat: 'no-repeat', imageRendering: 'pixelated', ...style, }; return
; }; // ============================================================================= // PARALLAX LAYER COMPONENT // ============================================================================= interface ParallaxLayerProps { src: string; speed: number; zIndex?: number; } const ParallaxLayer: React.FC = ({ src, speed, zIndex = 0, }) => { const frame = useCurrentFrame(); const { width, height } = useVideoConfig(); const scrollAmount = frame * speed; const scale = height / BACKGROUND_DIMENSIONS.height; const scaledWidth = BACKGROUND_DIMENSIONS.width * scale; const imagesNeeded = Math.ceil(width / scaledWidth) + 2; const offset = scrollAmount % scaledWidth; const containerStyle: React.CSSProperties = { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', zIndex, overflow: 'hidden', }; const scrollStyle: React.CSSProperties = { display: 'flex', transform: `translateX(-${offset}px)`, height: '100%', }; const imageStyle: React.CSSProperties = { height: '100%', width: scaledWidth, imageRendering: 'pixelated', flexShrink: 0, }; return (
{Array.from({ length: imagesNeeded }).map((_, i) => ( ))}
); }; // ============================================================================= // MAIN COMPONENT // ============================================================================= const SideScrollerGame: React.FC = () => { const frame = useCurrentFrame(); const { fps, durationInFrames, width, height } = useVideoConfig(); // Determine current animation phase const getAnimationState = () => { if (frame < TIMING.runEnd) { return { row: ANIMATIONS.run.row, frames: ANIMATIONS.run.frames, startFrame: 0, yOffset: 0, }; } if (frame < TIMING.jumpEnd) { const jumpMidpoint = TIMING.runEnd + (TIMING.jumpEnd - TIMING.runEnd) / 2; const yOffset = interpolate( frame, [TIMING.runEnd, jumpMidpoint, TIMING.jumpEnd], [0, -180, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } ); return { row: ANIMATIONS.jump.row, frames: ANIMATIONS.jump.frames, startFrame: TIMING.runEnd, yOffset, }; } return { row: ANIMATIONS.punch.row, frames: ANIMATIONS.punch.frames, startFrame: TIMING.jumpEnd, yOffset: 0, }; }; const { row, frames, startFrame, yOffset } = getAnimationState(); const characterContainerStyle: React.CSSProperties = { position: 'absolute', left: '20%', bottom: '15%', zIndex: 10, transform: `translateX(-50%) translateY(${yOffset}px)`, }; return ( {/* Parallax Background Layers */} {/* Character Sprite */}
); }; export default SideScrollerGame;