@twick/visualizer 0.15.12 → 0.15.14

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": "@twick/visualizer",
3
- "version": "0.15.12",
3
+ "version": "0.15.14",
4
4
  "license": "https://github.com/ncounterspecialist/twick/blob/main/LICENSE.md",
5
5
  "scripts": {
6
6
  "start": "twick editor --projectFile ./src/live.tsx",
@@ -22,18 +22,18 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@preact/signals": "^1.2.1",
25
- "@twick/2d": "^0.15.12",
26
- "@twick/core": "^0.15.12",
27
- "@twick/renderer": "^0.15.12",
28
- "@twick/vite-plugin": "^0.15.12",
25
+ "@twick/2d": "^0.15.14",
26
+ "@twick/core": "^0.15.14",
27
+ "@twick/renderer": "^0.15.14",
28
+ "@twick/vite-plugin": "^0.15.14",
29
29
  "date-fns": "^4.1.0",
30
30
  "preact": "^10.19.2",
31
31
  "crelt": "^1.0.6",
32
- "@twick/media-utils": "0.15.12"
32
+ "@twick/media-utils": "0.15.14"
33
33
  },
34
34
  "devDependencies": {
35
- "@twick/cli": "^0.15.12",
36
- "@twick/ui": "^0.15.12",
35
+ "@twick/cli": "^0.15.14",
36
+ "@twick/ui": "^0.15.14",
37
37
  "typescript": "5.4.2",
38
38
  "typedoc": "^0.25.8",
39
39
  "typedoc-plugin-markdown": "^3.17.1",
@@ -3,8 +3,8 @@
3
3
  * including video, audio, captions, scenes, and elements.
4
4
  */
5
5
 
6
- import { Layout, Rect, View2D, Audio } from "@twick/2d";
7
- import { VisualizerTrack } from "../helpers/types";
6
+ import { Layout, Rect, View2D, Audio, Img, Txt } from "@twick/2d";
7
+ import { VisualizerTrack, WatermarkInput } from "../helpers/types";
8
8
  import { all, Color, createRef, ThreadGenerator, waitFor } from "@twick/core";
9
9
  import {
10
10
  CAPTION_STYLE,
@@ -13,6 +13,7 @@ import {
13
13
  } from "../helpers/constants";
14
14
  import { logger } from "../helpers/log.utils";
15
15
  import elementController from "../controllers/element.controller";
16
+ import watermarkController from "../controllers/watermark.controller";
16
17
  import { hexToRGB } from "../helpers/utils";
17
18
 
18
19
  /**
@@ -218,3 +219,28 @@ export function* makeElementTrack({
218
219
  yield* all(...sequence);
219
220
  yield elementTrackRef().remove();
220
221
  }
222
+
223
+ /**
224
+ * Creates a watermark overlay on top of all tracks.
225
+ * Dispatches to the registered watermark renderer by type (text | image).
226
+ * Added last to ensure it renders on top of all content.
227
+ */
228
+ export function* makeWatermarkTrack({
229
+ view,
230
+ watermark,
231
+ duration,
232
+ }: {
233
+ view: View2D;
234
+ watermark: WatermarkInput;
235
+ duration: number;
236
+ }) {
237
+ if (duration <= 0) return;
238
+
239
+ // Add watermark after other tracks have started (ensures it renders on top)
240
+ yield* waitFor(0.001);
241
+
242
+ const renderer = watermarkController.get(watermark.type);
243
+ if (renderer) {
244
+ yield* renderer.render({ view, watermark, duration });
245
+ }
246
+ }
@@ -0,0 +1,35 @@
1
+ import type { WatermarkRendererContract } from "../helpers/watermark.types";
2
+ import { TextWatermarkRenderer } from "../watermark-renderers/text.watermark";
3
+ import { ImageWatermarkRenderer } from "../watermark-renderers/image.watermark";
4
+
5
+ /**
6
+ * Registry for watermark renderers. Enables scalable dispatch by type:
7
+ * watermarkController.get(watermark.type)?.render({ view, watermark, duration }).
8
+ * Add new watermark types (e.g. svg) by registering a new renderer.
9
+ */
10
+ export class WatermarkController {
11
+ private renderers = new Map<string, WatermarkRendererContract>();
12
+
13
+ register(renderer: WatermarkRendererContract) {
14
+ this.renderers.set(renderer.name, renderer);
15
+ }
16
+
17
+ get(name: string): WatermarkRendererContract | undefined {
18
+ return this.renderers.get(name);
19
+ }
20
+
21
+ list(): string[] {
22
+ return Array.from(this.renderers.keys());
23
+ }
24
+ }
25
+
26
+ const watermarkController = new WatermarkController();
27
+
28
+ function registerWatermarkRenderers() {
29
+ watermarkController.register(TextWatermarkRenderer);
30
+ watermarkController.register(ImageWatermarkRenderer);
31
+ }
32
+
33
+ registerWatermarkRenderers();
34
+
35
+ export default watermarkController;
@@ -1,8 +1,8 @@
1
1
  import { ElementParams } from "../helpers/types";
2
- import { all, createRef, waitFor } from "@twick/core";
3
- import { Txt } from "@twick/2d";
2
+ import { all, Color, createRef, waitFor } from "@twick/core";
3
+ import { Rect, Txt } from "@twick/2d";
4
4
  import { addAnimation, addTextEffect } from "../helpers/element.utils";
5
- import { logger } from "../helpers/log.utils";
5
+ import { hexToRGB } from "../helpers/utils";
6
6
 
7
7
  /**
8
8
  * @group TextElement
@@ -79,26 +79,85 @@ export const TextElement = {
79
79
  * });
80
80
  * ```
81
81
  */
82
- *create({ containerRef, element, view }: ElementParams) {
82
+ *create({ containerRef, element, view }: ElementParams) {
83
83
  const elementRef = createRef<any>();
84
+ const hasBackground =
85
+ element.props?.backgroundColor != null &&
86
+ element.props.backgroundColor !== "";
84
87
 
85
88
  yield* waitFor(element?.s);
86
- yield containerRef().add(
87
- <Txt
88
- ref={elementRef}
89
- key={element.id}
90
- text={element.t}
91
- textWrap={element.props?.textWrap ?? true}
92
- textAlign={element.props?.textAlign ?? "center"}
93
- {...element.props}
94
- />
95
- );
96
- yield* all(
97
- addAnimation({ elementRef: elementRef, element: element, view }),
98
- addTextEffect({ elementRef: elementRef, element: element }),
99
- waitFor(Math.max(0, element.e - element.s))
100
- );
101
- yield elementRef().remove();
102
- }
89
+
90
+ if (hasBackground) {
91
+ const textRef = createRef<Txt>();
92
+ const wrapperRef = createRef<any>();
93
+ const innerTextRef = createRef<Txt>();
94
+
95
+ yield containerRef().add(
96
+ <Txt
97
+ ref={textRef}
98
+ key={`${element.id}-measure`}
99
+ text={element.t}
100
+ textWrap={element.props?.textWrap ?? true}
101
+ textAlign={element.props?.textAlign ?? "center"}
102
+ opacity={0}
103
+ {...element.props}
104
+ />
105
+ );
106
+
107
+ const bgColor = new Color({
108
+ ...hexToRGB(element.props!.backgroundColor!),
109
+ a: element.props?.backgroundOpacity ?? 1,
110
+ });
111
+ const paddingX = 20;
112
+ const paddingY = 4;
113
+
114
+ yield containerRef().add(
115
+ <Rect
116
+ ref={wrapperRef}
117
+ key={element.id}
118
+ fill={bgColor}
119
+ width={textRef().width() + paddingX}
120
+ height={textRef().height() + paddingY}
121
+ padding={[0, paddingX / 2]}
122
+ alignItems={"center"}
123
+ justifyContent={"center"}
124
+ layout
125
+ >
126
+ <Txt
127
+ ref={innerTextRef}
128
+ text={element.t}
129
+ textWrap={element.props?.textWrap ?? true}
130
+ textAlign={element.props?.textAlign ?? "center"}
131
+ {...element.props}
132
+ />
133
+ </Rect>
134
+ );
135
+ textRef().remove();
136
+
137
+ yield* all(
138
+ addAnimation({ elementRef: wrapperRef, element: element, view }),
139
+ addTextEffect({ elementRef: innerTextRef, element: element }),
140
+ waitFor(Math.max(0, element.e - element.s))
141
+ );
142
+ yield wrapperRef().remove();
143
+ } else {
144
+ yield containerRef().add(
145
+ <Txt
146
+ ref={elementRef}
147
+ key={element.id}
148
+ text={element.t}
149
+ textWrap={element.props?.textWrap ?? true}
150
+ textAlign={element.props?.textAlign ?? "center"}
151
+ {...element.props}
152
+ />
153
+ );
154
+ yield* all(
155
+ addAnimation({ elementRef: elementRef, element: element, view }),
156
+ addTextEffect({ elementRef: elementRef, element: element }),
157
+ waitFor(Math.max(0, element.e - element.s))
158
+ );
159
+ yield elementRef().remove();
160
+ }
161
+ },
103
162
  }
104
163
 
@@ -1,19 +1,48 @@
1
1
  import { View2D } from "@twick/2d";
2
2
  import { Reference, ThreadGenerator, Vector2 } from "@twick/core";
3
3
 
4
+ /**
5
+ * Watermark configuration for overlay on video.
6
+ * Supports text or image watermarks with position, rotation, and opacity.
7
+ * Compatible with WatermarkJSON from @twick/timeline.
8
+ */
9
+ export type WatermarkInput = {
10
+ type: "text" | "image";
11
+ position?: Position;
12
+ rotation?: number;
13
+ opacity?: number;
14
+ props: {
15
+ text?: string;
16
+ fontSize?: number;
17
+ fontFamily?: string;
18
+ fill?: string;
19
+ stroke?: string;
20
+ strokeWidth?: number;
21
+ textAlign?: string;
22
+ fontWeight?: number;
23
+ lineWidth?: number;
24
+ fontStyle?: string;
25
+ src?: string;
26
+ width?: number;
27
+ height?: number;
28
+ objectFit?: ObjectFit;
29
+ };
30
+ };
31
+
4
32
  /**
5
33
  * Main input configuration for video visualization.
6
- * Contains player settings, background color, dimensions, and track definitions
7
- * for creating complete video visualizations.
34
+ * Contains player settings, background color, dimensions, track definitions,
35
+ * and optional watermark for creating complete video visualizations.
8
36
  */
9
37
  export type VideoInput = {
10
- playerId: string,
38
+ playerId: string;
11
39
  backgroundColor: string;
12
40
  properties: {
13
41
  width: number;
14
42
  height: number;
15
43
  };
16
44
  tracks: VisualizerTrack[];
45
+ watermark?: WatermarkInput;
17
46
  };
18
47
 
19
48
  /**
@@ -0,0 +1,23 @@
1
+ import { View2D } from "@twick/2d";
2
+ import { ThreadGenerator } from "@twick/core";
3
+ import { WatermarkInput } from "./types";
4
+
5
+ /**
6
+ * Parameters passed to a watermark renderer.
7
+ */
8
+ export type WatermarkRendererParams = {
9
+ view: View2D;
10
+ watermark: WatermarkInput;
11
+ duration: number;
12
+ };
13
+
14
+ /**
15
+ * Interface for watermark renderers. Register with WatermarkController
16
+ * to support text, image, or future watermark types (e.g. svg).
17
+ */
18
+ export interface WatermarkRendererContract {
19
+ /** Renderer type: "text" | "image" */
20
+ name: "text" | "image";
21
+ /** Renders the watermark overlay for the given duration */
22
+ render(params: WatermarkRendererParams): ThreadGenerator;
23
+ }
package/src/index.ts CHANGED
@@ -194,3 +194,8 @@ export * from './elements';
194
194
  export * from './text-effects';
195
195
  export * from './frame-effects';
196
196
 
197
+ // Watermark renderer registry (Option B – register custom watermark types)
198
+ export { default as watermarkController } from './controllers/watermark.controller';
199
+ export { WatermarkController } from './controllers/watermark.controller';
200
+ export type { WatermarkRendererContract, WatermarkRendererParams } from './helpers/watermark.types';
201
+
package/src/sample.ts CHANGED
@@ -26,6 +26,18 @@ export const sample = {
26
26
  "orgId": "a251d9971a55"
27
27
  },
28
28
  "basePath": "carlyn",
29
+ "watermark": {
30
+ "id": "e-watermark",
31
+ "type": "text",
32
+ "position": { "x": 0, "y": -200 },
33
+ "opacity": 0.7,
34
+ "props": {
35
+ "text": "© Twick SDK",
36
+ "fontSize": 24,
37
+ "fontFamily": "Arial",
38
+ "fill": "#ffffff"
39
+ }
40
+ },
29
41
  "tracks": [
30
42
  {
31
43
  "id": "t-scene",
@@ -57,6 +57,7 @@ import {
57
57
  makeElementTrack,
58
58
  makeSceneTrack,
59
59
  makeVideoTrack,
60
+ makeWatermarkTrack,
60
61
  } from "./components/track";
61
62
  import { dispatchWindowEvent } from "./helpers/event.utils";
62
63
 
@@ -159,6 +160,25 @@ export const scene = makeScene2D("scene", function* (view: View2D) {
159
160
  }
160
161
  index++;
161
162
  }
163
+
164
+ // Add watermark on top of all tracks when present
165
+ if (input.watermark) {
166
+ const duration =
167
+ input.tracks?.length > 0
168
+ ? Math.max(
169
+ 0,
170
+ ...input.tracks.flatMap((t) => t.elements || []).map((e) => e.e)
171
+ )
172
+ : 10;
173
+ movie.push(
174
+ makeWatermarkTrack({
175
+ view,
176
+ watermark: input.watermark,
177
+ duration,
178
+ })
179
+ );
180
+ }
181
+
162
182
  // Execute all track animations in parallel
163
183
  yield* all(...movie);
164
184
  dispatchWindowEvent(EVENT_TYPES.PLAYER_UPDATE, {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Image watermark renderer for the visualizer.
3
+ * Renders image overlay with position, rotation, opacity, and dimensions.
4
+ */
5
+
6
+ import { Img } from "@twick/2d";
7
+ import { createRef, waitFor } from "@twick/core";
8
+ import type { WatermarkRendererContract } from "../helpers/watermark.types";
9
+
10
+ export const ImageWatermarkRenderer: WatermarkRendererContract = {
11
+ name: "image",
12
+
13
+ *render({ view, watermark, duration }) {
14
+ const props = watermark.props;
15
+ if (!props?.src) return;
16
+
17
+ const position = watermark.position ?? { x: 0, y: 0 };
18
+ const rotation = (watermark.rotation ?? 0) * (Math.PI / 180);
19
+ const opacity = watermark.opacity ?? 1;
20
+
21
+ const watermarkRef = createRef<any>();
22
+ view.add(
23
+ <Img
24
+ ref={watermarkRef}
25
+ src={props.src}
26
+ x={position.x}
27
+ y={position.y}
28
+ width={props.width}
29
+ height={props.height}
30
+ rotation={rotation}
31
+ opacity={opacity}
32
+ />
33
+ );
34
+
35
+ yield* waitFor(duration);
36
+ },
37
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Watermark renderers registry.
3
+ * Export renderers and shared types for the watermark controller.
4
+ */
5
+
6
+ export { TextWatermarkRenderer } from "./text.watermark";
7
+ export { ImageWatermarkRenderer } from "./image.watermark";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Text watermark renderer for the visualizer.
3
+ * Renders text overlay with position, rotation, opacity, and text styling.
4
+ */
5
+
6
+ import { Txt } from "@twick/2d";
7
+ import { createRef, waitFor } from "@twick/core";
8
+ import type { WatermarkRendererContract } from "../helpers/watermark.types";
9
+
10
+ export const TextWatermarkRenderer: WatermarkRendererContract = {
11
+ name: "text",
12
+
13
+ *render({ view, watermark, duration }) {
14
+ const props = watermark.props;
15
+ if (!props?.text) return;
16
+
17
+ const position = watermark.position ?? { x: 0, y: 0 };
18
+ const rotation = (watermark.rotation ?? 0) * (Math.PI / 180);
19
+ const opacity = watermark.opacity ?? 1;
20
+
21
+ const watermarkRef = createRef<any>();
22
+ view.add(
23
+ <Txt
24
+ ref={watermarkRef}
25
+ text={props.text}
26
+ x={position.x}
27
+ y={position.y}
28
+ rotation={rotation}
29
+ opacity={opacity}
30
+ fontSize={props.fontSize ?? 24}
31
+ fontFamily={props.fontFamily ?? "Arial"}
32
+ fill={props.fill ?? "#ffffff"}
33
+ stroke={props.stroke}
34
+ lineWidth={props.lineWidth ?? props.strokeWidth ?? 0}
35
+ textAlign={(props.textAlign as any) ?? "center"}
36
+ fontWeight={props.fontWeight ?? 400}
37
+ fontStyle={props.fontStyle ?? "normal"}
38
+ />
39
+ );
40
+
41
+ yield* waitFor(duration);
42
+ },
43
+ };