@twick/visualizer 0.0.1

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/.eslintrc.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "root": true,
3
+ "extends": [
4
+ "eslint:recommended",
5
+ "plugin:@typescript-eslint/recommended"
6
+ ],
7
+ "parser": "@typescript-eslint/parser",
8
+ "plugins": ["@typescript-eslint"],
9
+ "parserOptions": {
10
+ "ecmaVersion": "latest",
11
+ "sourceType": "module"
12
+ },
13
+ "env": {
14
+ "es2022": true,
15
+ "node": true
16
+ },
17
+ "rules": {
18
+ "@typescript-eslint/no-explicit-any": "warn",
19
+ "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
20
+ }
21
+ }
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @twick/visualizer
2
+
3
+ A visualization library built on top of [@revideo/2d](https://github.com/re-video/2d) for creating interactive visualizations.
4
+
5
+ ## Installation
6
+ ```
7
+ pnpm install
8
+ ```
9
+
10
+ ## Build
11
+ ```
12
+ pnpm build
13
+ ```
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@twick/visualizer",
3
+ "version": "0.0.1",
4
+ "license": "Apache-2.0",
5
+ "scripts": {
6
+ "start": "revideo editor --projectFile ./src/live.tsx",
7
+ "build": "tsc && vite build",
8
+ "docs": "typedoc --out docs src/index.ts",
9
+ "clean": "rimraf .turbo node_modules dist"
10
+ },
11
+ "dependencies": {
12
+ "@preact/signals": "^1.2.1",
13
+ "@revideo/2d": "^0.10.4",
14
+ "@revideo/core": "^0.10.4",
15
+ "@revideo/renderer": "^0.10.4",
16
+ "@revideo/vite-plugin": "^0.10.4",
17
+ "date-fns": "^4.1.0",
18
+ "preact": "^10.19.2",
19
+ "crelt": "^1.0.6",
20
+ "@twick/media-utils": "0.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "@revideo/cli": "0.10.4",
24
+ "@revideo/ui": "0.10.4",
25
+ "typescript": "5.4.2",
26
+ "typedoc": "^0.25.8",
27
+ "typedoc-plugin-markdown": "^3.17.1",
28
+ "vite-plugin-dts": "^3.7.3",
29
+ "vite": "^5.1.4"
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ import { all, Reference } from "@revideo/core";
2
+
3
+ export function* addAnimations({ elementRef }: { elementRef: Reference<any>;}) {
4
+ yield elementRef();
5
+ const animations:any[] = []
6
+ return yield* all(...animations);
7
+ }
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Element component that handles the creation and management of visual elements
3
+ * with various properties like position, size, rotation, and effects.
4
+ */
5
+
6
+ import { createRef, waitFor, all, Reference, Color } from "@revideo/core";
7
+ import { Audio, Circle, Icon, Img, Rect, Txt, Video } from "@revideo/2d";
8
+ import { logger } from "../helpers/log.utils";
9
+ import {
10
+ DEFAULT_BACKGROUND_COLOR,
11
+ ELEMENT_TYPES,
12
+ TRANSPARENT_COLOR,
13
+ } from "../helpers/constants";
14
+ import { fitElement } from "../helpers/element.utils";
15
+ import { applyColorFilter } from "../helpers/filters";
16
+ import { addFrameEffect } from "./frame-effects";
17
+ import { Caption, CaptionProps, VisualizerElement } from "../helpers/types";
18
+ import { splitPhraseTiming } from "../helpers/caption.utils";
19
+ import { addAnimations } from "./animation";
20
+
21
+
22
+ export function* addMediaElement({
23
+ containerRef,
24
+ element,
25
+ mediaType,
26
+ waitOnStart = true,
27
+ }: {
28
+ containerRef: Reference<any>;
29
+ element: VisualizerElement;
30
+ mediaType: string;
31
+ waitOnStart?: boolean;
32
+ }) {
33
+ if (waitOnStart) {
34
+ yield* waitFor(element.s);
35
+ }
36
+ const frameContainerRef = createRef<any>();
37
+ const frameElementRef = createRef<any>();
38
+ if (mediaType === ELEMENT_TYPES.VIDEO) {
39
+ const frameProps = element.frame || element.props;
40
+ logger(`Adding video element ${element.id}`);
41
+ yield containerRef().add(
42
+ <Rect ref={frameContainerRef} key={element.id} {...frameProps}>
43
+ <Video
44
+ ref={frameElementRef}
45
+ key={`child-${element.id}`}
46
+ {...element.props}
47
+ />
48
+ </Rect>
49
+ );
50
+ } else if (mediaType === ELEMENT_TYPES.IMAGE) {
51
+ logger(`Adding image element ${element.id}`);
52
+ yield containerRef().add(
53
+ <Rect ref={frameContainerRef} key={element.id} {...element.frame}>
54
+ <Img
55
+ ref={frameElementRef}
56
+ key={`child-${element.id}`}
57
+ {...element.props}
58
+ />
59
+ </Rect>
60
+ );
61
+ }
62
+ if (frameContainerRef()) {
63
+ yield fitElement({
64
+ elementRef: frameElementRef,
65
+ containerSize: frameContainerRef().size(),
66
+ elementSize: frameElementRef().size(),
67
+ objectFit: element.objectFit,
68
+ });
69
+
70
+ if (element?.props?.mediaFilter) {
71
+ applyColorFilter(frameElementRef, element.props.mediaFilter);
72
+ }
73
+
74
+ yield* all(
75
+ addAnimations({
76
+ elementRef: frameElementRef,
77
+ }),
78
+ addFrameEffect({
79
+ containerRef: frameContainerRef,
80
+ elementRef: frameElementRef,
81
+ frameElement,
82
+ }),
83
+ waitFor(Math.max(0, element.e - element.s))
84
+ );
85
+ if (element.type === ELEMENT_TYPES.VIDEO) {
86
+ yield frameElementRef().playing(false);
87
+ }
88
+ yield frameElementRef().remove();
89
+ yield frameContainerRef().remove();
90
+ }
91
+ }
92
+
93
+ export function* addCaptionElement({
94
+ caption,
95
+ captionProps,
96
+ containerRef,
97
+ capStyle,
98
+ }: {
99
+ caption: Caption;
100
+ captionProps: CaptionProps;
101
+ containerRef: Reference<any>;
102
+ capStyle: string;
103
+ }) {
104
+ const words = splitPhraseTiming(caption);
105
+ let phraseStart = 0;
106
+ if (words?.length) {
107
+ phraseStart = words[0].s;
108
+ }
109
+ let wordsState: {
110
+ refs: Array<{ bgRef?: Reference<any>; textRef: Reference<any> }>;
111
+ props: CaptionProps[];
112
+ idx: number;
113
+ prevTime: number;
114
+ } = {
115
+ refs: [],
116
+ props: [],
117
+ idx: 0,
118
+ prevTime: phraseStart,
119
+ };
120
+
121
+ for (const word of words) {
122
+ wordsState.props.push(captionProps);
123
+ const textRef = createRef<Txt>();
124
+ containerRef().add(
125
+ <Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
126
+ );
127
+ if (capStyle == "highlight_bg") {
128
+ const bgContainerRef = createRef();
129
+ const childTextRef = createRef();
130
+ containerRef().add(
131
+ <Rect
132
+ ref={bgContainerRef}
133
+ fill={new Color(captionProps.colors.background).alpha(
134
+ captionProps?.bgOpacity ?? 1
135
+ )}
136
+ width={textRef().width() + (captionProps.bgOffsetWidth ?? 30)}
137
+ height={textRef().height() + (captionProps.bgOffsetHeight ?? 10)}
138
+ margin={captionProps.bgMargin ?? [0, -5]}
139
+ radius={captionProps.bgRadius ?? 10}
140
+ padding={captionProps.bgPadding ?? [0, 15]}
141
+ opacity={0}
142
+ alignItems={"center"}
143
+ justifyContent={"center"}
144
+ layout
145
+ >
146
+ <Txt ref={childTextRef} {...captionProps} text={word.t} />
147
+ </Rect>
148
+ );
149
+ textRef().remove();
150
+ wordsState.refs.push({
151
+ bgRef: bgContainerRef,
152
+ textRef: childTextRef,
153
+ });
154
+ } else {
155
+ wordsState.refs.push({
156
+ textRef: textRef,
157
+ });
158
+ }
159
+ wordsState.prevTime = word.e;
160
+ wordsState.idx = wordsState.idx + 1;
161
+ }
162
+
163
+ wordsState.prevTime = phraseStart;
164
+ wordsState.idx = 0;
165
+
166
+ for (const word of words) {
167
+ if (capStyle == "highlight_bg") {
168
+ yield* wordsState.refs[wordsState.idx]?.bgRef?.().opacity(1, 0);
169
+ yield* waitFor(Math.max(0, word.e - word.s));
170
+ yield* wordsState.refs[wordsState.idx]
171
+ ?.bgRef?.()
172
+ .fill(TRANSPARENT_COLOR, 0);
173
+ } else {
174
+ yield* wordsState.refs[wordsState.idx]?.textRef?.().opacity(1, 0);
175
+ yield* waitFor(Math.max(0, word.e - word.s));
176
+ }
177
+ wordsState.prevTime = word.e;
178
+ wordsState.idx = wordsState.idx + 1;
179
+ }
180
+ }
181
+
182
+ export function* makeSceneElements({
183
+ containerRef,
184
+ element,
185
+ }: {
186
+ containerRef: Reference<any>;
187
+ element: VisualizerElement;
188
+ }) {
189
+ switch (element.type) {
190
+ case ELEMENT_TYPES.RECT:
191
+ yield* addSceneRectElement({ containerRef, element });
192
+ break;
193
+ case ELEMENT_TYPES.IMAGE:
194
+ case ELEMENT_TYPES.VIDEO:
195
+ const mediaContainerRef = createRef<any>();
196
+ yield containerRef().add(
197
+ <Rect
198
+ ref={mediaContainerRef}
199
+ fill={element.backgroundColor ?? DEFAULT_BACKGROUND_COLOR}
200
+ size={"100%"}
201
+ />
202
+ );
203
+ yield* addMediaElement({
204
+ containerRef: mediaContainerRef,
205
+ element,
206
+ mediaType: element.type,
207
+ waitOnStart: false,
208
+ });
209
+ yield mediaContainerRef().remove();
210
+ break;
211
+ default:
212
+ yield* waitFor(Math.max(0, element.e - element.s));
213
+ break;
214
+ }
215
+ }
216
+
217
+ function* addSceneRectElement({
218
+ containerRef,
219
+ element,
220
+ }: {
221
+ containerRef: Reference<any>;
222
+ element: VisualizerElement;
223
+ }) {
224
+ const elementRef = createRef<any>();
225
+ let sequence: any[] = [];
226
+ yield containerRef().add(
227
+ <Rect ref={elementRef} key={element.id} {...element.props}></Rect>
228
+ );
229
+ for (const childElement of element.elements || []) {
230
+ sequence.push(
231
+ makeSceneElements({
232
+ containerRef: elementRef,
233
+ element: childElement,
234
+ })
235
+ );
236
+ }
237
+
238
+ yield* all(
239
+ ...sequence,
240
+ addAnimations({
241
+ elementRef,
242
+ }),
243
+ waitFor(Math.max(0, element.e - element.s))
244
+ );
245
+ yield elementRef().remove();
246
+ }
247
+
248
+ export function* addRectElement({
249
+ containerRef,
250
+ element,
251
+ }: {
252
+ containerRef: Reference<any>;
253
+ element: VisualizerElement;
254
+ }) {
255
+ const elementRef = createRef<any>();
256
+ yield* waitFor(element?.s);
257
+ yield containerRef().add(
258
+ <Rect ref={elementRef} key={element.id} {...element.props} />
259
+ );
260
+ yield* all(
261
+ addAnimations({ elementRef: elementRef }),
262
+ waitFor(Math.max(0, element.e - element.s))
263
+ );
264
+ yield elementRef().remove();
265
+ }
266
+
267
+ export function* addTextElement({
268
+ containerRef,
269
+ element,
270
+ }: {
271
+ containerRef: Reference<any>;
272
+ element: VisualizerElement;
273
+ }) {
274
+ const elementRef = createRef<any>();
275
+ yield* waitFor(element?.s);
276
+ yield containerRef().add(
277
+ <Txt
278
+ ref={elementRef}
279
+ key={element.id}
280
+ text={element.t}
281
+ {...element.props}
282
+ />
283
+ );
284
+ yield* all(
285
+ addAnimations({ elementRef: elementRef }),
286
+ waitFor(Math.max(0, element.e - element.s))
287
+ );
288
+ yield elementRef().remove();
289
+ }
290
+
291
+ export function* addCircleElement({
292
+ containerRef,
293
+ element,
294
+ }: {
295
+ containerRef: Reference<any>;
296
+ element: VisualizerElement;
297
+ }) {
298
+ const elementRef = createRef<any>();
299
+ yield* waitFor(element?.s);
300
+ yield containerRef().add(
301
+ <Circle ref={elementRef} key={element.id} {...element.props} />
302
+ );
303
+ yield* all(
304
+ addAnimations({ elementRef: elementRef }),
305
+ waitFor(Math.max(0, element.e - element.s))
306
+ );
307
+ yield elementRef().remove();
308
+ }
309
+
310
+ export function* addIconElement({
311
+ containerRef,
312
+ element,
313
+ }: {
314
+ containerRef: Reference<any>;
315
+ element: VisualizerElement;
316
+ }) {
317
+ const elementRef = createRef<any>();
318
+ yield* waitFor(element?.s);
319
+ yield containerRef().add(
320
+ <Icon ref={elementRef} key={element.id} {...element.props} />
321
+ );
322
+ yield* all(
323
+ addAnimations({ elementRef: elementRef }),
324
+ waitFor(Math.max(0, element.e - element.s))
325
+ );
326
+ yield elementRef().remove();
327
+ }
328
+
329
+ export function* addAudioElement({
330
+ containerRef,
331
+ element,
332
+ }: {
333
+ containerRef: Reference<any>;
334
+ element: VisualizerElement;
335
+ }) {
336
+ const elementRef = createRef<any>();
337
+ yield* waitFor(element?.s);
338
+ yield containerRef().add(
339
+ <Audio ref={elementRef} key={element.id} {...element.props} />
340
+ );
341
+ yield* waitFor(Math.max(0, element.e - element.s));
342
+ yield elementRef().playing(false);
343
+ yield elementRef().remove();
344
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Frame effects component that handles the application of various visual effects
3
+ * to frames and elements in the scene.
4
+ */
5
+
6
+ import { all, Reference, waitFor } from "@revideo/core";
7
+ import { getTimingFunction } from "../helpers/timing.utils";
8
+ import { fitElement } from "../helpers/element.utils";
9
+ import { DEFAULT_POSITION, DEFAULT_TIMING_FUNCTION, FRAME_SHAPE } from "../helpers/constants";
10
+ import { FrameEffect } from "../helpers/types";
11
+ import { View2D } from "@revideo/2d";
12
+ import { createSignal } from "@revideo/core";
13
+ import { logger } from "../helpers/log.utils";
14
+
15
+ /**
16
+ * Applies frame effects to a view or element
17
+ * @param {Object} params - Parameters for applying frame effects
18
+ * @param {View2D} params.view - The 2D view to apply effects to
19
+ * @param {FrameEffect[]} params.effects - Array of effects to apply
20
+ * @returns {void}
21
+ */
22
+ export function applyFrameEffects({ view, effects }: { view: View2D; effects: FrameEffect[] }) {
23
+ // ... existing code ...
24
+ }
25
+
26
+ export function* addFrameEffect({
27
+ containerRef,
28
+ elementRef,
29
+ frameElement,
30
+ }: {
31
+ containerRef: Reference<any>;
32
+ elementRef: Reference<any>;
33
+ frameElement: any;
34
+ }) {
35
+ let frameEffects = [];
36
+ const initProps = {
37
+ frame: {
38
+ size: containerRef().size(),
39
+ pos: containerRef().position(),
40
+ radius: containerRef().radius(),
41
+ scale: containerRef().scale(),
42
+ rotation: containerRef().rotation(),
43
+ },
44
+ element: {
45
+ size: containerRef().size(),
46
+ pos: elementRef().position(),
47
+ scale: elementRef().scale(),
48
+ },
49
+ };
50
+
51
+ for (let i = 0; i < frameElement?.frameEffects?.length; i++) {
52
+ const frameEffect = frameElement.frameEffects[i];
53
+ const restore =
54
+ i === frameElement.frameEffects.length - 1 ||
55
+ frameElement.frameEffects[i + 1].s !== frameEffect.e;
56
+ frameEffects.push(
57
+ getFrameEffect({
58
+ containerRef,
59
+ elementRef,
60
+ frameEffect,
61
+ element: frameElement,
62
+ restore,
63
+ initProps,
64
+ })
65
+ );
66
+ }
67
+ yield* all(...frameEffects);
68
+ }
69
+
70
+ function* getFrameEffect({
71
+ containerRef,
72
+ elementRef,
73
+ frameEffect,
74
+ element,
75
+ initProps,
76
+ restore = true,
77
+ }: {
78
+ containerRef: Reference<any>;
79
+ elementRef: Reference<any>;
80
+ frameEffect: FrameEffect;
81
+ element: any;
82
+ initProps: any;
83
+ restore?: boolean;
84
+ }) {
85
+ yield* waitFor(frameEffect.s);
86
+ const props = frameEffect.props;
87
+ const sequence = [];
88
+
89
+ if (restore) {
90
+ sequence.push(
91
+ restoreState({
92
+ containerRef,
93
+ elementRef,
94
+ frameEffect,
95
+ element,
96
+ initProps,
97
+ })
98
+ );
99
+ }
100
+
101
+ sequence.push(
102
+ containerRef().size(
103
+ props.frameSize,
104
+ props.transitionDuration,
105
+ getTimingFunction(props.transitionEasing ?? DEFAULT_TIMING_FUNCTION)
106
+ )
107
+ );
108
+
109
+ sequence.push(
110
+ containerRef().position(
111
+ props.framePosition ?? { x: 0, y: 0 },
112
+ props.transitionDuration,
113
+ getTimingFunction(props.transitionEasing ?? DEFAULT_TIMING_FUNCTION)
114
+ )
115
+ );
116
+
117
+ sequence.push(
118
+ elementRef().position(
119
+ props.elementPosition ?? DEFAULT_POSITION,
120
+ props.transitionDuration,
121
+ getTimingFunction(props.transitionEasing ?? DEFAULT_TIMING_FUNCTION)
122
+ )
123
+ );
124
+
125
+ switch (props.frameShape) {
126
+ case FRAME_SHAPE.CIRCLE:
127
+ sequence.push(
128
+ containerRef().radius(
129
+ props.frameSize[0] / 2,
130
+ props.transitionDuration,
131
+ getTimingFunction(props.transitionEasing ?? DEFAULT_TIMING_FUNCTION)
132
+ )
133
+ );
134
+ break;
135
+ default:
136
+ sequence.push(
137
+ containerRef().radius(
138
+ props.radius ?? 0,
139
+ props.transitionDuration,
140
+ getTimingFunction(props.transitionEasing ?? DEFAULT_TIMING_FUNCTION)
141
+ )
142
+ );
143
+ }
144
+
145
+ if (props.objectFit) {
146
+ sequence.push(
147
+ fitElement({
148
+ elementRef,
149
+ containerSize: { x: props.frameSize[0], y: props.frameSize[1] },
150
+ elementSize: initProps.element.size,
151
+ objectFit: props.objectFit,
152
+ })
153
+ );
154
+ }
155
+ yield* all(...sequence);
156
+ }
157
+
158
+ function* restoreState({
159
+ containerRef,
160
+ elementRef,
161
+ frameEffect,
162
+ element,
163
+ initProps,
164
+ }: {
165
+ containerRef: Reference<any>;
166
+ elementRef: Reference<any>;
167
+ frameEffect: any;
168
+ element: any;
169
+ initProps: any;
170
+ }) {
171
+ yield* waitFor(frameEffect.e - frameEffect.s);
172
+ let sequence = [];
173
+ sequence.push(containerRef().size(initProps.frame.size));
174
+ sequence.push(containerRef().position(initProps.frame.pos));
175
+ sequence.push(containerRef().scale(initProps.frame.scale));
176
+ sequence.push(containerRef().rotation(initProps.frame.rotation));
177
+ sequence.push(containerRef().radius(initProps.frame.radius));
178
+ sequence.push(elementRef().size(initProps.element.size));
179
+ sequence.push(elementRef().position(initProps.element.pos));
180
+ sequence.push(elementRef().scale(initProps.element.scale));
181
+ sequence.push(
182
+ fitElement({
183
+ elementRef,
184
+ containerSize: initProps.frame.size,
185
+ elementSize: initProps.element.size,
186
+ objectFit: element.objectFit ?? "none",
187
+ })
188
+ );
189
+ yield* all(...sequence);
190
+ }