@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossclip/scenes",
3
- "version": "0.1.31",
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.31"
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";