subtitle-bar.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import React from "react";
  2. import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
  3. import type { WordTimestamp } from "@pipeline/shared";
  4. interface SubtitleBarProps {
  5. wordTimestamps: WordTimestamp[];
  6. style?: React.CSSProperties;
  7. }
  8. export const SubtitleBar: React.FC<SubtitleBarProps> = ({
  9. wordTimestamps,
  10. style,
  11. }) => {
  12. const frame = useCurrentFrame();
  13. const { fps } = useVideoConfig();
  14. const currentTime = frame / fps;
  15. // Find current word index
  16. let currentWordIndex = -1;
  17. for (let i = 0; i < wordTimestamps.length; i++) {
  18. if (
  19. currentTime >= wordTimestamps[i].startSeconds &&
  20. currentTime <= wordTimestamps[i].endSeconds
  21. ) {
  22. currentWordIndex = i;
  23. break;
  24. }
  25. }
  26. // Dynamic window size: fewer words when there's lots of text
  27. const totalWords = wordTimestamps.length;
  28. const windowSize =
  29. totalWords > 50 ? 8 : totalWords > 30 ? 12 : 16;
  30. const start = Math.max(0, currentWordIndex - Math.floor(windowSize / 2));
  31. const end = Math.min(wordTimestamps.length, start + windowSize);
  32. const visibleWords = wordTimestamps.slice(start, end).map((w, i) => ({
  33. text: w.word,
  34. globalIndex: start + i,
  35. }));
  36. const opacity = interpolate(frame, [0, 8], [0, 1], {
  37. extrapolateRight: "clamp",
  38. });
  39. // Continuous font scaling based on total word count
  40. const baseFontSize = 42;
  41. const minScale = 0.5;
  42. const scaleAt50 = 0.65;
  43. const fontSize =
  44. totalWords <= 15
  45. ? baseFontSize
  46. : totalWords <= 50
  47. ? baseFontSize * (1 - (1 - scaleAt50) * ((totalWords - 15) / 35))
  48. : baseFontSize * (minScale + (scaleAt50 - minScale) * Math.max(0, 1 - (totalWords - 50) / 100));
  49. // Dynamic max height: smaller when text is dense
  50. const maxHeight =
  51. totalWords > 50 ? 80 : totalWords > 30 ? 100 : 120;
  52. return (
  53. <div
  54. style={{
  55. position: "absolute",
  56. bottom: 60,
  57. left: 0,
  58. right: 0,
  59. display: "flex",
  60. justifyContent: "center",
  61. opacity,
  62. padding: "0 5%",
  63. ...style,
  64. }}
  65. >
  66. <div
  67. style={{
  68. backgroundColor: "rgba(0,0,0,0.8)",
  69. borderRadius: 12,
  70. padding: "10px 24px",
  71. maxWidth: "100%",
  72. maxHeight,
  73. overflow: "hidden",
  74. display: "flex",
  75. alignItems: "center",
  76. justifyContent: "center",
  77. }}
  78. >
  79. <span
  80. style={{
  81. fontSize,
  82. lineHeight: 1.3,
  83. fontFamily: "sans-serif",
  84. display: "-webkit-box",
  85. WebkitLineClamp: 2,
  86. WebkitBoxOrient: "vertical",
  87. overflow: "hidden",
  88. }}
  89. >
  90. {visibleWords.map((w, i) => {
  91. const isCurrent = w.globalIndex === currentWordIndex;
  92. const isPast = currentTime > wordTimestamps[w.globalIndex].endSeconds;
  93. return (
  94. <span
  95. key={w.globalIndex}
  96. style={{
  97. color: isCurrent
  98. ? "#fbbf24"
  99. : isPast
  100. ? "rgba(255,255,255,0.5)"
  101. : "#ffffff",
  102. fontWeight: isCurrent ? 700 : 400,
  103. }}
  104. >
  105. {w.text}
  106. </span>
  107. );
  108. })}
  109. </span>
  110. </div>
  111. </div>
  112. );
  113. };