diff --git a/src/song-274/Transect.tsx b/src/song-274/Transect.tsx index a1f1e14..3e6a951 100644 --- a/src/song-274/Transect.tsx +++ b/src/song-274/Transect.tsx @@ -18,6 +18,7 @@ import { type Frame, } from "./camera"; import type { Cue, RosterItem, TransectData } from "./data"; +import { blackOpacity } from "./fade"; import { extendBounds, GROUND, @@ -82,7 +83,7 @@ const currentCue = (list: Cue[] | undefined, t: number): Cue | undefined => { export const Transect: React.FC = ({ palette, audio, data }) => { const frame = useCurrentFrame(); - const { fps, width, height } = useVideoConfig(); + const { fps, width, height, durationInFrames } = useVideoConfig(); const projection = useMemo(() => (data ? projectGround(data.route) : null), [data]); // The main road plus the east continuation. The paint runs into the latter // after 東に続く; the camera and overview stay on the main road, so that run @@ -129,6 +130,7 @@ export const Transect: React.FC = ({ palette, audio, data }) => { } const t = frame / fps; + const fadeToBlack = blackOpacity(t, (durationInFrames - 1) / fps); const headAt = (time: number) => { const km = kmAt(time); const pos = routeAtKm(data.route, km); @@ -221,6 +223,10 @@ export const Transect: React.FC = ({ palette, audio, data }) => { {nodes.filter((n) => isAwake(n.look)).map(draw)} + {/* Fade from and to black at the two ends (fade.ts). */} + {fadeToBlack > 0 ? ( + + ) : null} ); }; diff --git a/src/song-274/fade.ts b/src/song-274/fade.ts new file mode 100644 index 0000000..e1423d5 --- /dev/null +++ b/src/song-274/fade.ts @@ -0,0 +1,24 @@ +/** + * Fade from and to a completely black screen at the two ends of the video: a + * black layer over everything, fully opaque on the first and last frames. + */ + +/** Seconds of complete black at start */ +export const FADE_IN_PRE = 0.4; +/** Seconds from black to the picture at the start. 0 disables the fade-in. */ +export const FADE_IN_S = 0.3; +/** Seconds from the picture to black at the end. 0 disables the fade-out. */ +export const FADE_OUT_S = 0.2; + +const clamp01 = (v: number) => Math.min(1, Math.max(0, v)); + +/** + * Opacity of the black layer at time `t`. + * @param lastFrameS time of the video's last frame, (durationInFrames - 1) / fps, + * so that frame is fully black rather than nearly so + */ +export const blackOpacity = (t: number, lastFrameS: number): number => { + const fadeIn = (t < FADE_IN_PRE) ? 1 : (FADE_IN_S > 0 ? 1 - clamp01((t - FADE_IN_PRE) / FADE_IN_S) : 0); + const fadeOut = FADE_OUT_S > 0 ? clamp01((t - (lastFrameS - FADE_OUT_S)) / FADE_OUT_S) : 0; + return Math.max(fadeIn, fadeOut); +};