@ossclip/scenes 0.1.31 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/CoverInVideo.tsx +62 -0
- package/src/cover-in-video.ts +72 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossclip/scenes",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "ossclip's scene library and stage geometry — React components shared by preview and render",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"zod": "^3.25.0",
|
|
22
|
-
"@ossclip/core": "0.1.
|
|
22
|
+
"@ossclip/core": "0.1.33"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"react": ">=18",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AbsoluteFill, Img, Sequence, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
+
import {
|
|
4
|
+
coverInVideoFrames,
|
|
5
|
+
coverInVideoOpacity,
|
|
6
|
+
type CoverInVideoProps,
|
|
7
|
+
} from "./cover-in-video";
|
|
8
|
+
|
|
9
|
+
/** The overlay's own frame — inside the Sequence, so frame 0 is frame 1 of
|
|
10
|
+
* the video and the fade math (cover-in-video.ts) reads the local clock. */
|
|
11
|
+
const CoverImage: React.FC<{ cover: CoverInVideoProps; durationInFrames: number }> = ({
|
|
12
|
+
cover,
|
|
13
|
+
durationInFrames,
|
|
14
|
+
}) => {
|
|
15
|
+
const frame = useCurrentFrame();
|
|
16
|
+
return (
|
|
17
|
+
<AbsoluteFill
|
|
18
|
+
style={{
|
|
19
|
+
// Black behind the image, not transparent: the cover is rendered at
|
|
20
|
+
// the OUTPUT frame's exact size (produce renders it from `frame`), but
|
|
21
|
+
// an image that ever disagrees must letterbox against the same black
|
|
22
|
+
// the composition's own backdrop uses rather than flash the video
|
|
23
|
+
// through the margins.
|
|
24
|
+
backgroundColor: "black",
|
|
25
|
+
opacity: coverInVideoOpacity(frame, durationInFrames),
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<Img
|
|
29
|
+
// An http(s) URL passes through untouched, everything else is a name
|
|
30
|
+
// in the render's public dir — CoverComposition's exact rule, and the
|
|
31
|
+
// one that also lets the editor hand this component a `/media/…` URL
|
|
32
|
+
// (staticFile leaves an already-rooted path alone).
|
|
33
|
+
src={/^https?:\/\//.test(cover.fileName) ? cover.fileName : staticFile(cover.fileName)}
|
|
34
|
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
35
|
+
/>
|
|
36
|
+
</AbsoluteFill>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `--cover-in-video`: the cover image over the short's first frames (see
|
|
42
|
+
* cover-in-video.ts for the overlay-not-insertion argument and the window
|
|
43
|
+
* bounds). Mounted ABOVE everything — video, scenes, captions, watermark —
|
|
44
|
+
* because a cover with a caption burned through it is not a cover.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately invisible to the editor, the Watermark's rule for the same
|
|
47
|
+
* reason: no `data-edit-id`/`data-edit-scene` so hitTest.ts can never resolve
|
|
48
|
+
* it as a selection, and `pointerEvents: none` so a click at frame 0 falls
|
|
49
|
+
* through to whatever the overlay covers. It is a produce-time switch, not a
|
|
50
|
+
* scene element.
|
|
51
|
+
*/
|
|
52
|
+
export const CoverInVideo: React.FC<{ cover: CoverInVideoProps }> = ({ cover }) => {
|
|
53
|
+
const { fps } = useVideoConfig();
|
|
54
|
+
const { from, durationInFrames } = coverInVideoFrames(cover.durationSec, fps);
|
|
55
|
+
return (
|
|
56
|
+
<Sequence from={from} durationInFrames={durationInFrames} layout="none">
|
|
57
|
+
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
|
58
|
+
<CoverImage cover={cover} durationInFrames={durationInFrames} />
|
|
59
|
+
</AbsoluteFill>
|
|
60
|
+
</Sequence>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { frameWindow } from "./frames";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `--cover-in-video` overlay (core's cover-in-video.ts owns the WHY and
|
|
5
|
+
* the window derivation): the cover image painted over the opening frames,
|
|
6
|
+
* for the platforms that use frame 1 instead of an uploaded cover.
|
|
7
|
+
*
|
|
8
|
+
* Pure and JSX-free (house rule, `punch-plan.ts`'s posture): the gate, the
|
|
9
|
+
* frame math and the fade are the whole behavior, and this package carries no
|
|
10
|
+
* jsdom — none of it would be assertable if it lived inside the component.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface CoverInVideoProps {
|
|
14
|
+
/** Image file name inside the render's public dir (or an absolute http(s) URL). */
|
|
15
|
+
fileName: string;
|
|
16
|
+
/** How long the overlay lasts, in OUTPUT seconds from frame 0. */
|
|
17
|
+
durationSec: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* How many frames the overlay spends fading out. Long enough that the cut to
|
|
22
|
+
* video is a transition rather than a pop, short enough that it does not eat
|
|
23
|
+
* a meaningful slice of a window whose cap is half a second.
|
|
24
|
+
*/
|
|
25
|
+
export const COVER_IN_VIDEO_FADE_FRAMES = 4;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether a render-props `coverInVideo` field is an overlay this renderer
|
|
29
|
+
* will mount — `punchPropsFor`'s posture (parse, never coerce, CLAUDE.md):
|
|
30
|
+
* render-props.json is user-visible and hand-editable, every pre-feature file
|
|
31
|
+
* has no key at all, and a mangled one must fall back to NO overlay rather
|
|
32
|
+
* than mount an `undefined` src or a NaN-frame Sequence over the hook.
|
|
33
|
+
*/
|
|
34
|
+
export function coverInVideoPropsFor(value: unknown): CoverInVideoProps | null {
|
|
35
|
+
if (typeof value !== "object" || value === null) return null;
|
|
36
|
+
const v = value as { fileName?: unknown; durationSec?: unknown };
|
|
37
|
+
if (typeof v.fileName !== "string" || v.fileName.length === 0) return null;
|
|
38
|
+
if (typeof v.durationSec !== "number" || !Number.isFinite(v.durationSec) || v.durationSec <= 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return { fileName: v.fileName, durationSec: v.durationSec };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The overlay's `<Sequence>` window. Always from frame 0 — the whole point is
|
|
46
|
+
* frame 1 — and the end comes from the end TIME through `frameWindow`
|
|
47
|
+
* (FINDINGS §115), not from a rounded duration, so the overlay's last frame
|
|
48
|
+
* and the first uncovered frame can never be the same one.
|
|
49
|
+
*/
|
|
50
|
+
export function coverInVideoFrames(
|
|
51
|
+
durationSec: number,
|
|
52
|
+
fps: number,
|
|
53
|
+
): { from: number; durationInFrames: number } {
|
|
54
|
+
return frameWindow(0, durationSec, fps);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Opacity at `frame` (SEQUENCE-relative, so 0 is the first covered frame) for
|
|
59
|
+
* a window `durationInFrames` long: solid, then a linear ramp over the last
|
|
60
|
+
* `COVER_IN_VIDEO_FADE_FRAMES`.
|
|
61
|
+
*
|
|
62
|
+
* The ramp is capped at the window's own length, so a sub-fade window (a 0.2s
|
|
63
|
+
* floor at a low fps) fades across what it has instead of starting below 1 —
|
|
64
|
+
* a cover that is never fully opaque looks like a rendering fault, not a
|
|
65
|
+
* transition.
|
|
66
|
+
*/
|
|
67
|
+
export function coverInVideoOpacity(frame: number, durationInFrames: number): number {
|
|
68
|
+
const fade = Math.min(COVER_IN_VIDEO_FADE_FRAMES, durationInFrames);
|
|
69
|
+
const remaining = durationInFrames - frame;
|
|
70
|
+
if (remaining >= fade) return 1;
|
|
71
|
+
return Math.min(1, Math.max(0, remaining / fade));
|
|
72
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,9 @@ export { CaptionTrack, type CaptionTrackProps } from "./CaptionTrack";
|
|
|
3
3
|
export { VideoStage } from "./VideoStage";
|
|
4
4
|
export { SceneLayer } from "./SceneLayer";
|
|
5
5
|
export { Watermark } from "./Watermark";
|
|
6
|
+
export { CoverInVideo } from "./CoverInVideo";
|
|
6
7
|
export * from "./stage";
|
|
7
8
|
export * from "./watermark-layout";
|
|
9
|
+
export * from "./cover-in-video";
|
|
8
10
|
export * from "./punch-plan";
|
|
9
11
|
export * from "./caption-visibility";
|