index.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import React from "react";
  2. import {
  3. useCurrentFrame,
  4. useVideoConfig,
  5. AbsoluteFill,
  6. interpolate,
  7. Easing,
  8. } from "remotion";
  9. import type { RemotionScene } from "../Root";
  10. import { THEMES } from "../base/theme/colors";
  11. import { Background } from "../base/components/background";
  12. import { SubtitleBar } from "../base/components/subtitle-bar";
  13. import { AnimatedText } from "../base/components/animated-text";
  14. import { Watermark } from "../base/components/watermark";
  15. import { QuoteBlock } from "./components/quote-block";
  16. interface OpinionSceneProps {
  17. scene: RemotionScene;
  18. sceneIndex: number;
  19. totalScenes: number;
  20. totalFrames: number;
  21. title: string;
  22. channelName?: string;
  23. }
  24. const OpinionScene: React.FC<OpinionSceneProps> = ({
  25. scene,
  26. sceneIndex,
  27. totalScenes,
  28. totalFrames,
  29. title,
  30. channelName,
  31. }) => {
  32. const frame = useCurrentFrame();
  33. const colors = THEMES.opinion;
  34. const visualText = scene.displayText ?? scene.narration;
  35. const fadeOpacity = interpolate(
  36. frame,
  37. [0, 10, scene.endFrame - scene.startFrame - 10, scene.endFrame - scene.startFrame],
  38. [0, 1, 1, 0],
  39. { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
  40. );
  41. return (
  42. <AbsoluteFill style={{ opacity: fadeOpacity }}>
  43. <Background
  44. colors={colors}
  45. sceneIndex={sceneIndex}
  46. totalScenes={totalScenes}
  47. />
  48. {/* Opinion badge */}
  49. <div
  50. style={{
  51. position: "absolute",
  52. top: 40,
  53. left: 40,
  54. display: "flex",
  55. alignItems: "center",
  56. gap: 12,
  57. }}
  58. >
  59. <div
  60. style={{
  61. width: 8,
  62. height: 8,
  63. borderRadius: 4,
  64. backgroundColor: colors.primary,
  65. }}
  66. />
  67. <div
  68. style={{
  69. fontSize: 18,
  70. color: "#9ca3af",
  71. fontFamily: "Noto Sans SC",
  72. letterSpacing: 1,
  73. }}
  74. >
  75. 观点 · {title}
  76. </div>
  77. </div>
  78. {/* Main content */}
  79. <div
  80. style={{
  81. position: "absolute",
  82. top: 120,
  83. left: 80,
  84. right: 80,
  85. bottom: 160,
  86. display: "flex",
  87. flexDirection: "column",
  88. justifyContent: "center",
  89. gap: 40,
  90. overflow: "hidden",
  91. }}
  92. >
  93. {sceneIndex === 0 ? (
  94. // Opening: quote style
  95. <QuoteBlock
  96. quote={visualText}
  97. color={colors.primaryLight}
  98. />
  99. ) : (
  100. <>
  101. <AnimatedText
  102. text={visualText}
  103. fontSize={40}
  104. color="white"
  105. fontWeight={500}
  106. delay={5}
  107. />
  108. {scene.keyframes
  109. .filter((kf) => kf.type === "text" || kf.type === "highlight")
  110. .map((kf, i) => (
  111. <QuoteBlock
  112. key={i}
  113. quote={kf.content}
  114. color={colors.primaryLight}
  115. />
  116. ))}
  117. </>
  118. )}
  119. </div>
  120. {scene.wordTimestamps.length > 0 && (
  121. <SubtitleBar wordTimestamps={scene.wordTimestamps} />
  122. )}
  123. <Watermark text={channelName} />
  124. </AbsoluteFill>
  125. );
  126. };
  127. export default OpinionScene;