| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- import React from "react";
- import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
- import type { WordTimestamp } from "@pipeline/shared";
- interface SubtitleBarProps {
- wordTimestamps: WordTimestamp[];
- style?: React.CSSProperties;
- }
- export const SubtitleBar: React.FC<SubtitleBarProps> = ({
- wordTimestamps,
- style,
- }) => {
- const frame = useCurrentFrame();
- const { fps } = useVideoConfig();
- const currentTime = frame / fps;
- // Find current word index
- let currentWordIndex = -1;
- for (let i = 0; i < wordTimestamps.length; i++) {
- if (
- currentTime >= wordTimestamps[i].startSeconds &&
- currentTime <= wordTimestamps[i].endSeconds
- ) {
- currentWordIndex = i;
- break;
- }
- }
- // Dynamic window size: fewer words when there's lots of text
- const totalWords = wordTimestamps.length;
- const windowSize =
- totalWords > 50 ? 8 : totalWords > 30 ? 12 : 16;
- const start = Math.max(0, currentWordIndex - Math.floor(windowSize / 2));
- const end = Math.min(wordTimestamps.length, start + windowSize);
- const visibleWords = wordTimestamps.slice(start, end).map((w, i) => ({
- text: w.word,
- globalIndex: start + i,
- }));
- const opacity = interpolate(frame, [0, 8], [0, 1], {
- extrapolateRight: "clamp",
- });
- // Continuous font scaling based on total word count
- const baseFontSize = 42;
- const minScale = 0.5;
- const scaleAt50 = 0.65;
- const fontSize =
- totalWords <= 15
- ? baseFontSize
- : totalWords <= 50
- ? baseFontSize * (1 - (1 - scaleAt50) * ((totalWords - 15) / 35))
- : baseFontSize * (minScale + (scaleAt50 - minScale) * Math.max(0, 1 - (totalWords - 50) / 100));
- // Dynamic max height: smaller when text is dense
- const maxHeight =
- totalWords > 50 ? 80 : totalWords > 30 ? 100 : 120;
- return (
- <div
- style={{
- position: "absolute",
- bottom: 60,
- left: 0,
- right: 0,
- display: "flex",
- justifyContent: "center",
- opacity,
- padding: "0 5%",
- ...style,
- }}
- >
- <div
- style={{
- backgroundColor: "rgba(0,0,0,0.8)",
- borderRadius: 12,
- padding: "10px 24px",
- maxWidth: "100%",
- maxHeight,
- overflow: "hidden",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- }}
- >
- <span
- style={{
- fontSize,
- lineHeight: 1.3,
- fontFamily: "sans-serif",
- display: "-webkit-box",
- WebkitLineClamp: 2,
- WebkitBoxOrient: "vertical",
- overflow: "hidden",
- }}
- >
- {visibleWords.map((w, i) => {
- const isCurrent = w.globalIndex === currentWordIndex;
- const isPast = currentTime > wordTimestamps[w.globalIndex].endSeconds;
- return (
- <span
- key={w.globalIndex}
- style={{
- color: isCurrent
- ? "#fbbf24"
- : isPast
- ? "rgba(255,255,255,0.5)"
- : "#ffffff",
- fontWeight: isCurrent ? 700 : 400,
- }}
- >
- {w.text}
- </span>
- );
- })}
- </span>
- </div>
- </div>
- );
- };
|