@ossclip/scenes 0.1.11 → 0.1.13
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/CaptionTrack.tsx +10 -1
- package/src/Watermark.tsx +39 -0
- package/src/index.ts +2 -0
- package/src/watermark-layout.ts +67 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossclip/scenes",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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.13"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"react": ">=18",
|
package/src/CaptionTrack.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
-
import type
|
|
3
|
+
import { lineDirection, type CaptionLine, type SceneCue } from "@ossclip/core/browser";
|
|
4
4
|
import { safeAreaFor, activeCueAt } from "./stage";
|
|
5
5
|
import { frameWindow } from "./frames";
|
|
6
6
|
import { captionAnchorAvoiding, regionsDuring, type OccupiedRegion } from "./source-fit";
|
|
@@ -79,6 +79,14 @@ const LineView: React.FC<{
|
|
|
79
79
|
const safeArea = safeAreaFor({ width, height });
|
|
80
80
|
// The parent <Sequence> starts at line.start, so local frame 0 === line.start.
|
|
81
81
|
const t = line.start + frame / fps;
|
|
82
|
+
// Per-line, from the text itself (first-strong, see lineDirection): Urdu
|
|
83
|
+
// captions were laying out LTR (Urdu field test 2026-08-05). On the flex
|
|
84
|
+
// row `direction: rtl` reverses the VISUAL order of the word spans and the
|
|
85
|
+
// wrap direction; the highlight below is keyed to each word's own
|
|
86
|
+
// start/end times, not to position, so it still walks in spoken order.
|
|
87
|
+
// Mixed runs INSIDE a word (Latin, digits) are the browser's bidi to
|
|
88
|
+
// resolve against this direction — never hand-reordered here.
|
|
89
|
+
const direction = lineDirection(line.words.map((w) => w.text).join(" "));
|
|
82
90
|
return (
|
|
83
91
|
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
|
84
92
|
<div
|
|
@@ -88,6 +96,7 @@ const LineView: React.FC<{
|
|
|
88
96
|
left: 0,
|
|
89
97
|
right: 0,
|
|
90
98
|
transform: "translateY(-50%)",
|
|
99
|
+
direction,
|
|
91
100
|
display: "flex",
|
|
92
101
|
justifyContent: "center",
|
|
93
102
|
gap: "0.28em",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Theme } from "@ossclip/core/browser";
|
|
3
|
+
import type { FrameSize } from "./stage";
|
|
4
|
+
import { watermarkLayout } from "./watermark-layout";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The opt-in "made with ossclip" wordmark (see watermark-layout.ts for the
|
|
8
|
+
* why and the placement rules). Styled from the THEME's display font and
|
|
9
|
+
* foreground — never hardcoded — so a themed production credits the tool in
|
|
10
|
+
* its own voice.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately invisible to the editor: no `data-edit-id`/`data-edit-scene`
|
|
13
|
+
* (the DOM is the hit-test registry — hitTest.ts walks those attributes, so
|
|
14
|
+
* an untagged node can never become a selection) and `pointerEvents: none`
|
|
15
|
+
* so `elementFromPoint` falls through to whatever the wordmark overlaps. A
|
|
16
|
+
* credit is a produce-time switch, not an editable scene element.
|
|
17
|
+
*/
|
|
18
|
+
export const Watermark: React.FC<{ theme: Theme; frame: FrameSize }> = ({ theme, frame }) => {
|
|
19
|
+
const l = watermarkLayout(frame);
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
style={{
|
|
23
|
+
position: "absolute",
|
|
24
|
+
left: l.xPx,
|
|
25
|
+
top: l.yPx,
|
|
26
|
+
fontFamily: theme.fontDisplay,
|
|
27
|
+
fontSize: l.fontPx,
|
|
28
|
+
fontWeight: 600,
|
|
29
|
+
letterSpacing: "0.02em",
|
|
30
|
+
color: theme.fg,
|
|
31
|
+
opacity: l.opacity,
|
|
32
|
+
pointerEvents: "none",
|
|
33
|
+
whiteSpace: "nowrap",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{l.text}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,4 +2,6 @@ export { EdlVideo, type EdlVideoProps } from "./EdlVideo";
|
|
|
2
2
|
export { CaptionTrack, type CaptionTrackProps } from "./CaptionTrack";
|
|
3
3
|
export { VideoStage } from "./VideoStage";
|
|
4
4
|
export { SceneLayer } from "./SceneLayer";
|
|
5
|
+
export { Watermark } from "./Watermark";
|
|
5
6
|
export * from "./stage";
|
|
7
|
+
export * from "./watermark-layout";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PORTRAIT_FRAME, safeAreaFor, type FrameSize } from "./stage";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The opt-in "made with ossclip" wordmark. OPT-IN is the whole design: the
|
|
5
|
+
* default stays off for everyone, because a forced watermark on an
|
|
6
|
+
* open-source tool reads as a free-tier limitation — this exists so the
|
|
7
|
+
* author (or anyone who wants to) can voluntarily credit the tool on videos
|
|
8
|
+
* they post, nothing more.
|
|
9
|
+
*
|
|
10
|
+
* Pure placement math, separated from the component so both frames can be
|
|
11
|
+
* asserted without React or Remotion in the loop (house rule: pure logic
|
|
12
|
+
* apart from rendering).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const WATERMARK_TEXT = "made with ossclip";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Low enough to read as a credit, not a caption — it must never compete with
|
|
19
|
+
* the captions or the scene graphics for attention.
|
|
20
|
+
*/
|
|
21
|
+
export const WATERMARK_OPACITY = 0.45;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Font size as a fraction of the frame's SHORT edge. Both output shapes share
|
|
25
|
+
* a 1080px short edge, so the wordmark renders at the same physical size in
|
|
26
|
+
* 9:16 and 16:9 instead of ballooning with the long edge.
|
|
27
|
+
*/
|
|
28
|
+
const WATERMARK_FONT_FRAC = 0.024;
|
|
29
|
+
|
|
30
|
+
export interface WatermarkLayout {
|
|
31
|
+
xPx: number;
|
|
32
|
+
yPx: number;
|
|
33
|
+
fontPx: number;
|
|
34
|
+
opacity: number;
|
|
35
|
+
text: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* TOP-LEFT of the textual safe area, per shape: 9:16 platforms overlay their
|
|
40
|
+
* own UI on the right edge (action rail) and the bottom (username/ticker),
|
|
41
|
+
* and the caption track owns the lower third — top-left is the one corner
|
|
42
|
+
* nothing else claims. `safeAreaFor` picks the portrait chrome insets or the
|
|
43
|
+
* landscape title-safe margins by frame shape, the same decision rule the
|
|
44
|
+
* stage itself uses, so the wordmark can never disagree with the layouts
|
|
45
|
+
* about where "safe" is.
|
|
46
|
+
*/
|
|
47
|
+
export function watermarkLayout(frame: FrameSize = PORTRAIT_FRAME): WatermarkLayout {
|
|
48
|
+
const a = safeAreaFor(frame);
|
|
49
|
+
return {
|
|
50
|
+
xPx: Math.round(a.left * frame.width),
|
|
51
|
+
yPx: Math.round(a.top * frame.height),
|
|
52
|
+
fontPx: Math.round(Math.min(frame.width, frame.height) * WATERMARK_FONT_FRAC),
|
|
53
|
+
opacity: WATERMARK_OPACITY,
|
|
54
|
+
text: WATERMARK_TEXT,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether a render-props `watermark` field turns the wordmark on. Strict
|
|
60
|
+
* `=== true`, not truthiness: every pre-watermark `render-props.json` has no
|
|
61
|
+
* field at all (undefined → off, so old workdirs render unchanged), and a
|
|
62
|
+
* hand-edited `"watermark": "yes"` must not coerce a credit on — values from
|
|
63
|
+
* outside are parsed, never coerced (CLAUDE.md).
|
|
64
|
+
*/
|
|
65
|
+
export function showWatermark(flag: unknown): flag is true {
|
|
66
|
+
return flag === true;
|
|
67
|
+
}
|