feat(274): fade the transect from and to black
The visualiser now starts and ends on a completely black screen: a black layer over everything whose opacity comes from fade.ts. - FADE_IN_PRE (0.4 s): hold on full black at the very start. - FADE_IN_S (0.3 s): then fade from black to the picture. - FADE_OUT_S (0.2 s): fade to black at the end, reaching full black on the last frame ((durationInFrames - 1) / fps), not a frame after it. Verified: eslint + tsc clean; stills show the first and last frames are pure RGB (0, 0, 0) and brightness ramps as timed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
This commit is contained in:
@@ -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<TransectProps> = ({ 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<TransectProps> = ({ 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<TransectProps> = ({ palette, audio, data }) => {
|
||||
{nodes.filter((n) => isAwake(n.look)).map(draw)}
|
||||
</g>
|
||||
</svg>
|
||||
{/* Fade from and to black at the two ends (fade.ts). */}
|
||||
{fadeToBlack > 0 ? (
|
||||
<AbsoluteFill style={{ backgroundColor: "#000000", opacity: fadeToBlack }} />
|
||||
) : null}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user