@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossclip/scenes",
3
- "version": "0.1.11",
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.11"
22
+ "@ossclip/core": "0.1.13"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "react": ">=18",
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig } from "remotion";
3
- import type { CaptionLine, SceneCue } from "@ossclip/core/browser";
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
+ }