@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/dist/project.js +56 -56
- package/package.json +8 -8
- package/src/components/track.tsx +28 -2
- package/src/controllers/watermark.controller.ts +35 -0
- package/src/elements/text.element.tsx +80 -21
- package/src/helpers/types.ts +32 -3
- package/src/helpers/watermark.types.ts +23 -0
- package/src/index.ts +5 -0
- package/src/sample.ts +12 -0
- package/src/visualizer.tsx +20 -0
- package/src/watermark-renderers/image.watermark.tsx +37 -0
- package/src/watermark-renderers/index.ts +7 -0
- package/src/watermark-renderers/text.watermark.tsx +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twick/visualizer",
|
|
3
|
-
"version": "0.15.
|
|
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.
|
|
26
|
-
"@twick/core": "^0.15.
|
|
27
|
-
"@twick/renderer": "^0.15.
|
|
28
|
-
"@twick/vite-plugin": "^0.15.
|
|
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.
|
|
32
|
+
"@twick/media-utils": "0.15.14"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@twick/cli": "^0.15.
|
|
36
|
-
"@twick/ui": "^0.15.
|
|
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",
|
package/src/components/track.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
package/src/helpers/types.ts
CHANGED
|
@@ -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,
|
|
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",
|
package/src/visualizer.tsx
CHANGED
|
@@ -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,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
|
+
};
|