@twick/visualizer 0.15.19 → 0.15.21

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.
@@ -3,6 +3,8 @@ import { CaptionElement } from "../elements/caption.element";
3
3
  import { CircleElement } from "../elements/circle.element";
4
4
  import { ImageElement } from "../elements/image.element";
5
5
  import { RectElement } from "../elements/rect.element";
6
+ import { ArrowElement } from "../elements/arrow.element";
7
+ import { LineElement } from "../elements/line.element";
6
8
  import { SceneElement } from "../elements/scene.element";
7
9
  import { TextElement } from "../elements/text.element";
8
10
  import { VideoElement } from "../elements/video.element";
@@ -33,6 +35,8 @@ export class ElementController {
33
35
  elementController.register(AudioElement);
34
36
  elementController.register(CircleElement);
35
37
  elementController.register(RectElement);
38
+ elementController.register(ArrowElement);
39
+ elementController.register(LineElement);
36
40
  }
37
41
 
38
42
  const elementController = new ElementController();
@@ -0,0 +1,75 @@
1
+ import { ElementParams } from "../helpers/types";
2
+ import { all, createRef, waitFor } from "@twick/core";
3
+ import { Layout, Polygon, Rect } from "@twick/2d";
4
+ import { addAnimation } from "../helpers/element.utils";
5
+ import { logger } from "../helpers/log.utils";
6
+
7
+ const HEAD_OFFSET = 2;
8
+
9
+ /**
10
+ * Arrow element: bar + triangle (callout) for the visualizer.
11
+ * Matches canvas arrow layout: bar from left, triangle overlapping at the tip.
12
+ */
13
+ export const ArrowElement = {
14
+ name: "arrow",
15
+
16
+ *create({ containerRef, element, view }: ElementParams) {
17
+ const elementRef = createRef<any>();
18
+ yield* waitFor(element?.s ?? 0);
19
+ logger(`ArrowElement: ${element?.id}`);
20
+
21
+ const w = element?.props?.width ?? 220;
22
+ const h = element?.props?.height ?? 14;
23
+ const fill = element?.props?.fill ?? "#f59e0b";
24
+ const radius = element?.props?.radius ?? 4;
25
+ const rotation = element?.props?.rotation ?? 0;
26
+ const opacity = element?.props?.opacity ?? 1;
27
+ const cx = element?.props?.x ?? 0;
28
+ const cy = element?.props?.y ?? 0;
29
+
30
+ // Match canvas triangle size: Polygon is inscribed in its size box so appears
31
+ // smaller than Fabric’s Triangle (which fills its box). Scale up ~1.3×.
32
+ const HEAD_SIZE_MULT = 1.8 * 1.3;
33
+ const headSize = h * HEAD_SIZE_MULT;
34
+ const barLength = w - headSize * 0.5 + HEAD_OFFSET;
35
+ const barWidth = w;
36
+
37
+ // Single group so one ref for animations
38
+ const arrowGroup = (
39
+ <Layout
40
+ ref={elementRef}
41
+ key={element?.id}
42
+ x={cx}
43
+ y={cy}
44
+ rotation={rotation}
45
+ opacity={opacity}
46
+ >
47
+ <Rect
48
+ x={-barWidth / 2 + barLength / 2}
49
+ y={0}
50
+ width={barLength}
51
+ height={h}
52
+ fill={fill}
53
+ radius={radius}
54
+ />
55
+ <Polygon
56
+ x={barWidth / 2 - headSize * 0.25}
57
+ y={0}
58
+ width={headSize}
59
+ height={headSize}
60
+ sides={3}
61
+ fill={fill}
62
+ rotation={90}
63
+ />
64
+ </Layout>
65
+ );
66
+
67
+ yield containerRef().add(arrowGroup);
68
+
69
+ yield* all(
70
+ addAnimation({ elementRef, element: element!, view }),
71
+ waitFor(Math.max(0, (element?.e ?? 0) - (element?.s ?? 0)))
72
+ );
73
+ yield elementRef().remove();
74
+ },
75
+ };
@@ -1,176 +1,46 @@
1
1
  import { CaptionProps, ElementParams } from "../helpers/types";
2
- import { Color, createRef, Reference, waitFor } from "@twick/core";
3
- import { Rect, Txt } from "@twick/2d";
2
+ import { ThreadGenerator } from "@twick/core";
4
3
  import { splitPhraseTiming } from "../helpers/caption.utils";
5
- import { TRANSPARENT_COLOR } from "../helpers/constants";
6
- import { hexToRGB } from "../helpers/utils";
4
+ import { getCaptionStyleHandler, getDefaultCaptionStyleHandler } from "../caption-styles";
7
5
 
8
6
  /**
9
7
  * @group CaptionElement
10
8
  * CaptionElement creates and manages styled text overlays in the visualizer scene.
11
- * Handles caption rendering, text effects, background styling, and timing
12
- * for professional video presentations and content creation.
13
- *
14
- * Features:
15
- * - Styled text with custom fonts, colors, and backgrounds
16
- * - Word-by-word timing and animation
17
- * - Background highlighting and styling options
18
- * - Text effects and animations
19
- * - Automatic timing and synchronization
9
+ * Delegates rendering and animation to registered caption style handlers.
20
10
  *
21
11
  * @param containerRef - Reference to the container element
22
12
  * @param caption - Caption configuration including text, styling, and timing
23
- *
24
- * @example
25
- * ```js
26
- * // Basic caption
27
- * {
28
- * id: "welcome-caption",
29
- * type: "caption",
30
- * s: 2,
31
- * e: 8,
32
- * t: "Welcome to our presentation!",
33
- * props: {
34
- * colors: {
35
- * text: "#ffffff",
36
- * background: "rgba(0,0,0,0.7)"
37
- * },
38
- * font: {
39
- * family: "Arial",
40
- * size: 48,
41
- * weight: 600
42
- * }
43
- * }
44
- * }
45
- *
46
- * // Caption with background highlighting
47
- * {
48
- * id: "highlighted-caption",
49
- * type: "caption",
50
- * s: 3,
51
- * e: 10,
52
- * t: "Important information",
53
- * capStyle: "highlight_bg",
54
- * props: {
55
- * colors: {
56
- * text: "#ffffff",
57
- * background: "rgba(255,0,0,0.8)"
58
- * },
59
- * font: {
60
- * family: "Helvetica",
61
- * size: 36,
62
- * weight: 700
63
- * },
64
- * bgOpacity: 0.9,
65
- * bgOffsetWidth: 20,
66
- * bgOffsetHeight: 10,
67
- * bgMargin: [10, 5],
68
- * bgRadius: 15,
69
- * bgPadding: [20, 15]
70
- * }
71
- * }
72
- * ```
73
13
  */
74
14
  export const CaptionElement = {
75
15
  name: "caption",
76
-
77
- /**
78
- * Generator function that creates and manages caption elements in the scene.
79
- * Handles caption creation, word timing, styling, and text effects.
80
- *
81
- * @param params - Element parameters including container reference and caption config
82
- * @returns Generator that controls the caption element lifecycle
83
- *
84
- * @example
85
- * ```js
86
- * yield* CaptionElement.create({
87
- * containerRef: mainContainer,
88
- * caption: captionConfig
89
- * });
90
- * ```
91
- */
92
- *create({ containerRef, caption, containerProps }: ElementParams) {
16
+
17
+ *create({ containerRef, caption, containerProps }: ElementParams): ThreadGenerator {
93
18
  const words = splitPhraseTiming(caption);
94
- let phraseStart = 0;
95
- if (words?.length) {
96
- phraseStart = words[0].s;
97
- }
98
- let wordsState: {
99
- refs: Array<{ bgRef?: Reference<any>; textRef: Reference<any> }>;
100
- props: CaptionProps[];
101
- idx: number;
102
- prevTime: number;
103
- } = {
104
- refs: [],
105
- props: [],
106
- idx: 0,
107
- prevTime: phraseStart,
108
- };
19
+ if (!words?.length) return;
20
+
21
+ const handler =
22
+ getCaptionStyleHandler(caption.capStyle ?? "") ?? getDefaultCaptionStyleHandler();
109
23
 
110
- // Set container properties
111
24
  containerRef().maxWidth(containerProps?.maxWidth ?? "95%");
112
25
  containerRef().wrap(containerProps?.wrap ?? "wrap");
113
26
  containerRef().justifyContent(containerProps?.justifyContent ?? "center");
114
27
  containerRef().alignItems(containerProps?.alignItems ?? "center");
115
28
 
116
- for (const word of words) {
117
- wordsState.props.push(caption.props);
118
- const textRef = createRef<Txt>();
119
- const captionProps = caption.props;
120
- containerRef().add(
121
- <Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
122
- );
123
- if (caption.capStyle == "highlight_bg") {
124
- const bgContainerRef = createRef();
125
- const childTextRef = createRef();
126
- const _color = new Color({...hexToRGB(captionProps.colors.bgColor), a: captionProps?.bgOpacity ?? 1});
127
- containerRef().add(
128
- <Rect
129
- ref={bgContainerRef}
130
- fill={_color}
131
- width={textRef().width() + (captionProps.bgOffsetWidth ?? 30)}
132
- height={textRef().height() + (captionProps.bgOffsetHeight ?? 10)}
133
- margin={captionProps.bgMargin ?? [0, -5]}
134
- radius={captionProps.bgRadius ?? 10}
135
- padding={captionProps.bgPadding ?? [0, 15]}
136
- opacity={0}
137
- alignItems={"center"}
138
- justifyContent={"center"}
139
- layout
140
- >
141
- <Txt ref={childTextRef} {...captionProps} text={word.t} />
142
- </Rect>
143
- );
144
- textRef().remove();
145
- wordsState.refs.push({
146
- bgRef: bgContainerRef,
147
- textRef: childTextRef,
148
- });
149
- } else {
150
- wordsState.refs.push({
151
- textRef: textRef,
152
- });
153
- }
154
- wordsState.prevTime = word.e;
155
- wordsState.idx = wordsState.idx + 1;
156
- }
29
+ const captionWithProps = {
30
+ ...caption,
31
+ props: caption.props ?? ({} as CaptionProps),
32
+ };
157
33
 
158
- wordsState.prevTime = phraseStart;
159
- wordsState.idx = 0;
34
+ const refs = handler.renderWords({
35
+ containerRef,
36
+ words,
37
+ caption: captionWithProps,
38
+ });
160
39
 
161
- for (const word of words) {
162
- if (caption.capStyle == "highlight_bg") {
163
- yield* wordsState.refs[wordsState.idx]?.bgRef?.().opacity(1, 0);
164
- yield* waitFor(Math.max(0, word.e - word.s));
165
- yield* wordsState.refs[wordsState.idx]
166
- ?.bgRef?.()
167
- .fill(TRANSPARENT_COLOR, 0);
168
- } else {
169
- yield* wordsState.refs[wordsState.idx]?.textRef?.().opacity(1, 0);
170
- yield* waitFor(Math.max(0, word.e - word.s));
171
- }
172
- wordsState.prevTime = word.e;
173
- wordsState.idx = wordsState.idx + 1;
174
- }
40
+ yield* handler.animateWords({
41
+ words,
42
+ refs,
43
+ caption: captionWithProps,
44
+ });
175
45
  },
176
46
  };
@@ -9,5 +9,7 @@ export { AudioElement } from './audio.element';
9
9
  export { TextElement } from './text.element';
10
10
  export { CaptionElement } from './caption.element';
11
11
  export { RectElement } from './rect.element';
12
+ export { ArrowElement } from './arrow.element';
13
+ export { LineElement } from './line.element';
12
14
  export { CircleElement } from './circle.element';
13
15
  export { SceneElement } from './scene.element';
@@ -0,0 +1,17 @@
1
+ import { ElementParams } from "../helpers/types";
2
+ import { RectElement } from "./rect.element";
3
+
4
+ /**
5
+ * LineElement: visualizer representation for line/segment shapes.
6
+ * Uses the same renderer as RectElement so width/height/fill/rotation
7
+ * map directly to the exported video.
8
+ */
9
+ export const LineElement = {
10
+ name: "line",
11
+
12
+ *create(params: ElementParams) {
13
+ // Delegate to RectElement to reuse rendering and animation behavior.
14
+ yield* RectElement.create(params);
15
+ },
16
+ };
17
+
@@ -122,6 +122,10 @@ export const TextElement = {
122
122
  alignItems={"center"}
123
123
  justifyContent={"center"}
124
124
  layout
125
+ x={element.props?.x}
126
+ y={element.props?.y}
127
+ rotation={element.props?.rotation}
128
+ opacity={element.props?.opacity}
125
129
  >
126
130
  <Txt
127
131
  ref={innerTextRef}
@@ -41,6 +41,7 @@ export const TRACK_TYPES = {
41
41
  CAPTION: "caption",
42
42
  SCENE: "scene",
43
43
  ELEMENT: "element",
44
+ EFFECT: "effect",
44
45
  } as const;
45
46
 
46
47
  export const CAPTION_STYLE: Record<string, CaptionStyle> = {
@@ -106,6 +107,122 @@ export const CAPTION_STYLE: Record<string, CaptionStyle> = {
106
107
  fontSize: 46,
107
108
  },
108
109
  },
110
+ outline_only: {
111
+ rect: {
112
+ alignItems: "center",
113
+ justifyContent: "center",
114
+ gap: 8,
115
+ },
116
+ word: {
117
+ lineWidth: 0.5,
118
+ stroke: "#000000",
119
+ fontWeight: 600,
120
+ strokeFirst: true,
121
+ shadowOffset: [0, 0],
122
+ shadowBlur: 0,
123
+ fontFamily: "Arial",
124
+ fill: "#FFFFFF",
125
+ fontSize: 42,
126
+ },
127
+ },
128
+ soft_box: {
129
+ rect: {
130
+ alignItems: "center",
131
+ justifyContent: "center",
132
+ gap: 8,
133
+ padding: [12, 24],
134
+ radius: 12,
135
+ },
136
+ word: {
137
+ lineWidth: 0.2,
138
+ stroke: "#000000",
139
+ fontWeight: 600,
140
+ strokeFirst: true,
141
+ shadowOffset: [-1, 1],
142
+ shadowColor: "rgba(0,0,0,0.3)",
143
+ shadowBlur: 3,
144
+ fontFamily: "Montserrat",
145
+ fill: "#FFFFFF",
146
+ fontSize: 40,
147
+ },
148
+ },
149
+ lower_third: {
150
+ rect: {
151
+ alignItems: "center",
152
+ justifyContent: "center",
153
+ gap: 8,
154
+ padding: [14, 32],
155
+ radius: 0,
156
+ },
157
+ word: {
158
+ lineWidth: 0.2,
159
+ stroke: "#000000",
160
+ fontWeight: 600,
161
+ strokeFirst: true,
162
+ shadowOffset: [0, 1],
163
+ shadowColor: "rgba(0,0,0,0.5)",
164
+ shadowBlur: 2,
165
+ fontFamily: "Arial",
166
+ fill: "#FFFFFF",
167
+ fontSize: 38,
168
+ },
169
+ },
170
+ typewriter: {
171
+ rect: {
172
+ alignItems: "center",
173
+ justifyContent: "center",
174
+ gap: 8,
175
+ },
176
+ word: {
177
+ lineWidth: 0.3,
178
+ stroke: "#000000",
179
+ fontWeight: 600,
180
+ strokeFirst: true,
181
+ shadowOffset: [0, 0],
182
+ shadowBlur: 0,
183
+ fontFamily: "Monaco",
184
+ fill: "#FFFFFF",
185
+ fontSize: 40,
186
+ },
187
+ },
188
+ karaoke: {
189
+ rect: {
190
+ alignItems: "center",
191
+ justifyContent: "center",
192
+ gap: 8,
193
+ },
194
+ word: {
195
+ lineWidth: 0.35,
196
+ stroke: "#000000",
197
+ fontWeight: 700,
198
+ strokeFirst: true,
199
+ shadowOffset: [-2, 2],
200
+ shadowColor: "#000000",
201
+ shadowBlur: 4,
202
+ fontFamily: "Bangers",
203
+ fill: "#FFFFFF",
204
+ fontSize: 46,
205
+ },
206
+ },
207
+ pop_scale: {
208
+ rect: {
209
+ alignItems: "center",
210
+ justifyContent: "center",
211
+ gap: 8,
212
+ },
213
+ word: {
214
+ lineWidth: 0.35,
215
+ stroke: "#000000",
216
+ fontWeight: 700,
217
+ strokeFirst: true,
218
+ shadowOffset: [-2, 2],
219
+ shadowColor: "#000000",
220
+ shadowBlur: 5,
221
+ fontFamily: "Bangers",
222
+ fill: "#FFFFFF",
223
+ fontSize: 46,
224
+ },
225
+ },
109
226
  };
110
227
 
111
228
  export const DEFAULT_CAPTION_COLORS = {
@@ -114,9 +231,9 @@ export const DEFAULT_CAPTION_COLORS = {
114
231
  };
115
232
 
116
233
  export const DEFAULT_CAPTION_FONT = {
117
- family: "Poppins",
118
- size: 48,
119
- weight: 400,
234
+ family: "Bangers",
235
+ size: 46,
236
+ weight: 700,
120
237
  };
121
238
 
122
239
  export const TRANSPARENT_COLOR = "#FFFFFF00";
@@ -180,9 +180,14 @@ export type CaptionProps = {
180
180
  * Defines text color, background color, and highlight colors.
181
181
  */
182
182
  export type CaptionColors = {
183
+ /** Main text fill color */
183
184
  text?: string;
185
+ /** Background color behind words/boxes */
184
186
  bgColor?: string;
187
+ /** Color used for per-word highlights (e.g. karaoke-style) */
185
188
  highlight?: string;
189
+ /** Stroke/outline color around text (e.g. classic outline style) */
190
+ outlineColor?: string;
186
191
  };
187
192
 
188
193
  /**
package/src/project.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { makeProject } from "@twick/core";
2
+ import { getActiveEffectsForFrame } from "@twick/effects";
2
3
  import { scene } from "./visualizer";
3
4
 
4
- export default makeProject({
5
+ const project = makeProject({
5
6
  scenes: [scene],
6
7
  });
8
+ (project as import("@twick/core").Project).getActiveEffectsForFrame =
9
+ getActiveEffectsForFrame;
10
+ export default project;
@@ -52,6 +52,18 @@ export { AudioElement } from './elements/audio.element';
52
52
  export { TextElement } from './elements/text.element';
53
53
  export { CaptionElement } from './elements/caption.element';
54
54
 
55
+ /**
56
+ * @group Caption Styles
57
+ * @description Plugin-based caption style handlers for extensible caption rendering
58
+ */
59
+ export {
60
+ registerCaptionStyle,
61
+ registerCaptionStyles,
62
+ getCaptionStyleHandler,
63
+ getDefaultCaptionStyleHandler,
64
+ } from './caption-styles';
65
+ export type { CaptionStyleHandler, WordRefs, WordTiming } from './caption-styles';
66
+
55
67
  // Shape and UI elements
56
68
  export { RectElement } from './elements/rect.element';
57
69
  export { CircleElement } from './elements/circle.element';