Skip to main content
Glama
template.tsx.j29.55 kB
import React from 'react'; import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion'; interface DeviceFrameProps { startFrame: number; durationInFrames: number; device: 'phone' | 'tablet' | 'laptop'; content?: React.ReactNode; orientation: 'portrait' | 'landscape'; scale: number; glare: boolean; shadow: boolean; position: string; } export const DeviceFrame: React.FC<DeviceFrameProps> = ({ startFrame, durationInFrames, device = 'phone', content, orientation = 'portrait', scale = 1.0, glare = true, shadow = true, position = 'center', }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); // Visibility check if (frame < startFrame || frame >= startFrame + durationInFrames) { return null; } const relativeFrame = frame - startFrame; // Device dimensions (base sizes in px) const deviceDimensions = { phone: { portrait: { width: 375, height: 812, bezel: parseInt('[[ spacing.spacing.lg ]]'), radius: parseInt('[[ spacing.spacing['2xl'] ]]') }, landscape: { width: 812, height: 375, bezel: parseInt('[[ spacing.spacing.lg ]]'), radius: parseInt('[[ spacing.spacing['2xl'] ]]') }, }, tablet: { portrait: { width: 768, height: 1024, bezel: parseInt('[[ spacing.spacing.xl ]]'), radius: parseInt('[[ spacing.spacing.lg ]]') }, landscape: { width: 1024, height: 768, bezel: parseInt('[[ spacing.spacing.xl ]]'), radius: parseInt('[[ spacing.spacing.lg ]]') }, }, laptop: { portrait: { width: 1440, height: 900, bezel: parseInt('[[ spacing.spacing['2xl'] ]]'), radius: 10 }, landscape: { width: 1440, height: 900, bezel: parseInt('[[ spacing.spacing['2xl'] ]]'), radius: 10 }, }, }; const dims = deviceDimensions[device][orientation]; const totalWidth = dims.width + dims.bezel * 2; const totalHeight = dims.height + dims.bezel * 2; // Position mappings const positionMap: Record<string, { top?: string; left?: string; right?: string; bottom?: string; transform?: string }> = { center: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', }, 'top-left': { top: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'top-center': { top: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, 'top-right': { top: '[[ spacing.spacing.xl ]]', right: '[[ spacing.spacing.xl ]]' }, 'center-left': { top: '50%', left: '[[ spacing.spacing.xl ]]', transform: 'translateY(-50%)' }, 'center-right': { top: '50%', right: '[[ spacing.spacing.xl ]]', transform: 'translateY(-50%)' }, 'bottom-left': { bottom: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'bottom-center': { bottom: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, 'bottom-right': { bottom: '[[ spacing.spacing.xl ]]', right: '[[ spacing.spacing.xl ]]' }, }; // Entrance animation const entrance = spring({ frame: relativeFrame, fps: fps, config: { damping: [[ motion.default_spring.config.damping ]], stiffness: [[ motion.default_spring.config.stiffness ]], mass: [[ motion.default_spring.config.mass ]], }, }); const opacity = interpolate(relativeFrame, [0, 20], [0, 1], { extrapolateRight: 'clamp' }); const scaleAnimation = interpolate(entrance, [0, 1], [0.9, 1]); return ( <AbsoluteFill style={{ ...positionMap[position], width: totalWidth * scale, height: totalHeight * scale, opacity, transform: `${positionMap[position].transform || ''} scale(${scaleAnimation})`, }} > <div style={{ position: 'relative', width: '100%', height: '100%', filter: shadow ? `drop-shadow(0 20px 60px [[ colors.shadow.dark ]])` : 'none', }} > {/* Device bezel/frame */} <div style={{ position: 'absolute', inset: 0, background: device === 'laptop' ? 'linear-gradient(145deg, [[ colors.background.dark ]], [[ colors.background.darker ]])' : 'linear-gradient(145deg, [[ colors.background.glass ]], [[ colors.background.darker ]])', borderRadius: `${dims.radius}px`, padding: `${dims.bezel}px`, [% if device == 'laptop' %] borderTop: '[[ spacing.border_width.medium ]] solid [[ colors.border.strong ]]', [% endif %] }} > {/* Screen area */} <div style={{ position: 'relative', width: '100%', height: '100%', background: '[[ colors.background.dark ]]', borderRadius: `${dims.radius * 0.7}px`, overflow: 'hidden', }} > {/* Content */} {content && ( <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', }} > {content} </div> )} {/* Screen glare effect */} {glare && ( <> <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '50%', background: `linear-gradient(180deg, [[ colors.border.light ]] 0%, transparent 100%)`, pointerEvents: 'none', }} /> <div style={{ position: 'absolute', top: '10%', left: '15%', width: '30%', height: '40%', background: `radial-gradient(ellipse at center, [[ colors.border.light ]] 0%, transparent 70%)`, transform: 'rotate(-25deg)', pointerEvents: 'none', }} /> </> )} </div> {/* Phone-specific elements */} {device === 'phone' && ( <> {/* Notch (for portrait phone) */} {orientation === 'portrait' && ( <div style={{ position: 'absolute', top: `${dims.bezel}px`, left: '50%', transform: 'translateX(-50%)', width: '35%', height: '28px', background: '[[ colors.background.darker ]]', borderRadius: `0 0 [[ spacing.spacing.lg ]]px [[ spacing.spacing.lg ]]px`, zIndex: 10, }} > {/* Speaker */} <div style={{ position: 'absolute', top: parseInt('[[ spacing.spacing.xs ]]'), left: '50%', transform: 'translateX(-50%)', width: '40%', height: parseInt('[[ spacing.spacing.xxs ]]'), background: '[[ colors.background.dark ]]', borderRadius: '[[ spacing.border_width.medium ]]', }} /> {/* Camera */} <div style={{ position: 'absolute', top: parseInt('[[ spacing.spacing.xs ]]'), right: '20%', width: parseInt('[[ spacing.spacing.xs ]]'), height: parseInt('[[ spacing.spacing.xs ]]'), background: '[[ colors.background.darker ]]', border: '[[ spacing.border_width.thin ]] solid [[ colors.background.dark ]]', borderRadius: '50%', }} /> </div> )} </> )} {/* Laptop-specific elements */} {device === 'laptop' && ( <> {/* Camera dot */} <div style={{ position: 'absolute', top: `${dims.bezel / 2}px`, left: '50%', transform: 'translateX(-50%)', width: '6px', height: '6px', background: '[[ colors.background.darker ]]', border: '[[ spacing.border_width.thin ]] solid [[ colors.border.strong ]]', borderRadius: '50%', }} /> </> )} </div> {/* Laptop base/keyboard */} {device === 'laptop' && ( <div style={{ position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)', width: '110%', height: parseInt('[[ spacing.spacing.lg ]]'), background: 'linear-gradient(145deg, [[ colors.background.dark ]], [[ colors.background.darker ]])', borderRadius: `0 0 [[ spacing.border_radius.lg ]]px [[ spacing.border_radius.lg ]]px`, marginTop: '[[ spacing.border_width.medium ]]', }} /> )} </div> </AbsoluteFill> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-remotion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server