index.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import React from "react";
  2. import {
  3. useCurrentFrame,
  4. useVideoConfig,
  5. Sequence,
  6. AbsoluteFill,
  7. interpolate,
  8. Easing,
  9. } from "remotion";
  10. import type { RemotionScene } from "../Root";
  11. import { THEMES } from "../base/theme/colors";
  12. import { Background } from "../base/components/background";
  13. import { SubtitleBar } from "../base/components/subtitle-bar";
  14. import { LowerThird } from "../base/components/lower-third";
  15. import { Watermark } from "../base/components/watermark";
  16. import { HeadlineCard } from "./components/headline-card";
  17. import { NewsTicker } from "./components/news-ticker";
  18. interface NewsSceneProps {
  19. scene: RemotionScene;
  20. sceneIndex: number;
  21. totalScenes: number;
  22. totalFrames: number;
  23. title: string;
  24. }
  25. const NewsScene: React.FC<NewsSceneProps> = ({
  26. scene,
  27. sceneIndex,
  28. totalScenes,
  29. totalFrames,
  30. title,
  31. }) => {
  32. const frame = useCurrentFrame();
  33. const { fps } = useVideoConfig();
  34. const colors = THEMES.news;
  35. // Scene-level fade
  36. const fadeOpacity = interpolate(
  37. frame,
  38. [0, 8, scene.endFrame - scene.startFrame - 8, scene.endFrame - scene.startFrame],
  39. [0, 1, 1, 0],
  40. { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
  41. );
  42. return (
  43. <AbsoluteFill style={{ opacity: fadeOpacity }}>
  44. <Background
  45. colors={colors}
  46. sceneIndex={sceneIndex}
  47. totalScenes={totalScenes}
  48. />
  49. {/* Breaking news badge */}
  50. {sceneIndex === 0 && (
  51. <div
  52. style={{
  53. position: "absolute",
  54. top: 40,
  55. left: 40,
  56. display: "flex",
  57. alignItems: "center",
  58. gap: 16,
  59. }}
  60. >
  61. <div
  62. style={{
  63. backgroundColor: "#dc2626",
  64. color: "white",
  65. padding: "8px 20px",
  66. fontSize: 20,
  67. fontWeight: 800,
  68. fontFamily: "sans-serif",
  69. borderRadius: 4,
  70. letterSpacing: 2,
  71. }}
  72. >
  73. BREAKING
  74. </div>
  75. <div
  76. style={{
  77. fontSize: 20,
  78. color: "#94a3b8",
  79. fontFamily: "sans-serif",
  80. }}
  81. >
  82. {title}
  83. </div>
  84. </div>
  85. )}
  86. {/* Main content area */}
  87. <div
  88. style={{
  89. position: "absolute",
  90. top: 120,
  91. left: 80,
  92. right: 80,
  93. bottom: 160,
  94. display: "flex",
  95. flexDirection: "column",
  96. justifyContent: "center",
  97. overflow: "hidden",
  98. }}
  99. >
  100. {sceneIndex === 0 ? (
  101. // Intro scene: big headline
  102. <div style={{ overflow: "hidden" }}>
  103. <div
  104. style={{
  105. fontSize: scene.narration.length > 60 ? 40 : 64,
  106. fontWeight: 800,
  107. color: "white",
  108. fontFamily: "sans-serif",
  109. lineHeight: 1.2,
  110. marginBottom: 24,
  111. maxHeight: 260,
  112. overflow: "hidden",
  113. display: "-webkit-box",
  114. WebkitLineClamp: 4,
  115. WebkitBoxOrient: "vertical",
  116. }}
  117. >
  118. {scene.narration}
  119. </div>
  120. {scene.keyframes.map((kf, i) => (
  121. <HeadlineCard
  122. key={i}
  123. headline={kf.content}
  124. index={i}
  125. />
  126. ))}
  127. </div>
  128. ) : (
  129. // Other scenes: cards layout
  130. <div style={{ overflow: "hidden" }}>
  131. <div
  132. style={{
  133. fontSize: scene.narration.length > 60 ? 30 : 42,
  134. fontWeight: 700,
  135. color: "white",
  136. fontFamily: "sans-serif",
  137. marginBottom: 32,
  138. lineHeight: 1.3,
  139. maxHeight: 200,
  140. overflow: "hidden",
  141. display: "-webkit-box",
  142. WebkitLineClamp: 4,
  143. WebkitBoxOrient: "vertical",
  144. }}
  145. >
  146. {scene.narration}
  147. </div>
  148. <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
  149. {scene.keyframes
  150. .filter((kf) => kf.type === "text" || kf.type === "highlight")
  151. .map((kf, i) => (
  152. <HeadlineCard
  153. key={i}
  154. headline={kf.content}
  155. index={i}
  156. />
  157. ))}
  158. </div>
  159. </div>
  160. )}
  161. </div>
  162. {/* Subtitle bar */}
  163. {scene.wordTimestamps.length > 0 && (
  164. <SubtitleBar wordTimestamps={scene.wordTimestamps} />
  165. )}
  166. {/* News ticker */}
  167. <NewsTicker
  168. headlines={scene.keyframes.map((kf) => kf.content)}
  169. />
  170. <Watermark />
  171. </AbsoluteFill>
  172. );
  173. };
  174. export default NewsScene;