@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.
- package/dist/project.js +127 -61
- package/package.json +11 -10
- package/src/caption-styles/highlight-bg.handler.tsx +60 -0
- package/src/caption-styles/index.ts +32 -0
- package/src/caption-styles/karaoke.handler.tsx +58 -0
- package/src/caption-styles/lower-third.handler.tsx +44 -0
- package/src/caption-styles/outline-only.handler.tsx +29 -0
- package/src/caption-styles/pop-scale.handler.tsx +35 -0
- package/src/caption-styles/registry.ts +32 -0
- package/src/caption-styles/soft-box.handler.tsx +40 -0
- package/src/caption-styles/types.ts +77 -0
- package/src/caption-styles/typewriter.handler.tsx +33 -0
- package/src/caption-styles/word-by-word-with-bg.handler.tsx +39 -0
- package/src/caption-styles/word-by-word.handler.tsx +29 -0
- package/src/components/track.tsx +38 -18
- package/src/controllers/element.controller.ts +4 -0
- package/src/elements/arrow.element.tsx +75 -0
- package/src/elements/caption.element.tsx +23 -153
- package/src/elements/index.ts +2 -0
- package/src/elements/line.element.tsx +17 -0
- package/src/elements/text.element.tsx +4 -0
- package/src/helpers/constants.ts +120 -3
- package/src/helpers/types.ts +5 -0
- package/src/project.ts +5 -1
- package/src/visualizer-grouped.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twick/visualizer",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.21",
|
|
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,19 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@preact/signals": "^1.2.1",
|
|
25
|
-
"@twick/2d": "^0.15.
|
|
26
|
-
"@twick/core": "^0.15.
|
|
27
|
-
"@twick/
|
|
28
|
-
"@twick/
|
|
29
|
-
"
|
|
30
|
-
"
|
|
25
|
+
"@twick/2d": "^0.15.21",
|
|
26
|
+
"@twick/core": "^0.15.21",
|
|
27
|
+
"@twick/effects": "0.15.21",
|
|
28
|
+
"@twick/media-utils": "0.15.21",
|
|
29
|
+
"@twick/renderer": "^0.15.21",
|
|
30
|
+
"@twick/vite-plugin": "^0.15.21",
|
|
31
31
|
"crelt": "^1.0.6",
|
|
32
|
-
"
|
|
32
|
+
"date-fns": "^4.1.0",
|
|
33
|
+
"preact": "^10.19.2"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@twick/cli": "^0.15.
|
|
36
|
-
"@twick/ui": "^0.15.
|
|
36
|
+
"@twick/cli": "^0.15.21",
|
|
37
|
+
"@twick/ui": "^0.15.21",
|
|
37
38
|
"typescript": "5.4.2",
|
|
38
39
|
"typedoc": "^0.25.8",
|
|
39
40
|
"typedoc-plugin-markdown": "^3.17.1",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Color, createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Rect, Txt } from "@twick/2d";
|
|
3
|
+
import { TRANSPARENT_COLOR } from "../helpers/constants";
|
|
4
|
+
import { hexToRGB } from "../helpers/utils";
|
|
5
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
6
|
+
|
|
7
|
+
export const highlightBgHandler: CaptionStyleHandler = {
|
|
8
|
+
id: "highlight_bg",
|
|
9
|
+
|
|
10
|
+
renderWords({ containerRef, words, caption }) {
|
|
11
|
+
const captionProps = caption.props;
|
|
12
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>>; bgRef: ReturnType<typeof createRef<Rect>> }> = [];
|
|
13
|
+
|
|
14
|
+
for (const word of words) {
|
|
15
|
+
const textRef = createRef<Txt>();
|
|
16
|
+
containerRef().add(
|
|
17
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
|
|
18
|
+
);
|
|
19
|
+
const bgContainerRef = createRef<Rect>();
|
|
20
|
+
const childTextRef = createRef<Txt>();
|
|
21
|
+
const _color = new Color({
|
|
22
|
+
...hexToRGB(captionProps.colors.bgColor ?? "#444444"),
|
|
23
|
+
a: captionProps?.bgOpacity ?? 1,
|
|
24
|
+
});
|
|
25
|
+
containerRef().add(
|
|
26
|
+
<Rect
|
|
27
|
+
ref={bgContainerRef}
|
|
28
|
+
fill={_color}
|
|
29
|
+
width={textRef().width() + (captionProps.bgOffsetWidth ?? 30)}
|
|
30
|
+
height={textRef().height() + (captionProps.bgOffsetHeight ?? 10)}
|
|
31
|
+
margin={captionProps.bgMargin ?? [0, -5]}
|
|
32
|
+
radius={captionProps.bgRadius ?? 10}
|
|
33
|
+
padding={captionProps.bgPadding ?? [0, 15]}
|
|
34
|
+
opacity={0}
|
|
35
|
+
alignItems={"center"}
|
|
36
|
+
justifyContent={"center"}
|
|
37
|
+
layout
|
|
38
|
+
>
|
|
39
|
+
<Txt ref={childTextRef} {...captionProps} text={word.t} />
|
|
40
|
+
</Rect>
|
|
41
|
+
);
|
|
42
|
+
textRef().remove();
|
|
43
|
+
refs.push({ textRef: childTextRef, bgRef: bgContainerRef });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { refs };
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
*animateWords({ words, refs }) {
|
|
50
|
+
for (let i = 0; i < words.length; i++) {
|
|
51
|
+
const r = refs.refs[i];
|
|
52
|
+
const bgRef = r.bgRef;
|
|
53
|
+
if (bgRef) {
|
|
54
|
+
yield* bgRef().opacity(1, 0);
|
|
55
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
56
|
+
yield* bgRef().fill(TRANSPARENT_COLOR, 0);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { registerCaptionStyle } from "./registry";
|
|
2
|
+
import { highlightBgHandler } from "./highlight-bg.handler";
|
|
3
|
+
import { wordByWordHandler } from "./word-by-word.handler";
|
|
4
|
+
import { wordByWordWithBgHandler } from "./word-by-word-with-bg.handler";
|
|
5
|
+
import { outlineOnlyHandler } from "./outline-only.handler";
|
|
6
|
+
import { softBoxHandler } from "./soft-box.handler";
|
|
7
|
+
import { lowerThirdHandler } from "./lower-third.handler";
|
|
8
|
+
import { typewriterHandler } from "./typewriter.handler";
|
|
9
|
+
import { karaokeHandler } from "./karaoke.handler";
|
|
10
|
+
import { popScaleHandler } from "./pop-scale.handler";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register all built-in caption style handlers.
|
|
14
|
+
* Called automatically on first import.
|
|
15
|
+
*/
|
|
16
|
+
export function registerCaptionStyles(): void {
|
|
17
|
+
registerCaptionStyle(highlightBgHandler);
|
|
18
|
+
registerCaptionStyle(wordByWordHandler);
|
|
19
|
+
registerCaptionStyle(wordByWordWithBgHandler);
|
|
20
|
+
registerCaptionStyle(outlineOnlyHandler);
|
|
21
|
+
registerCaptionStyle(softBoxHandler);
|
|
22
|
+
registerCaptionStyle(lowerThirdHandler);
|
|
23
|
+
registerCaptionStyle(typewriterHandler);
|
|
24
|
+
registerCaptionStyle(karaokeHandler);
|
|
25
|
+
registerCaptionStyle(popScaleHandler);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Auto-register on module load so handlers are available
|
|
29
|
+
registerCaptionStyles();
|
|
30
|
+
|
|
31
|
+
export { getCaptionStyleHandler, getDefaultCaptionStyleHandler, registerCaptionStyle } from "./registry";
|
|
32
|
+
export type { CaptionStyleHandler, WordRefs, WordTiming } from "./types";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Color, createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Txt } from "@twick/2d";
|
|
3
|
+
import { hexToRGB } from "../helpers/utils";
|
|
4
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
5
|
+
|
|
6
|
+
const INACTIVE_OPACITY = 0.5;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Karaoke: all words visible; current word switches to highlight color (one Txt per word, animate fill).
|
|
10
|
+
* Same ref/animate pattern as word_by_word; no overlay needed.
|
|
11
|
+
*/
|
|
12
|
+
export const karaokeHandler: CaptionStyleHandler = {
|
|
13
|
+
id: "karaoke",
|
|
14
|
+
|
|
15
|
+
renderWords({ containerRef, words, caption }) {
|
|
16
|
+
const captionProps = caption.props;
|
|
17
|
+
const textColor = captionProps.colors?.text ?? "#ffffff";
|
|
18
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
19
|
+
|
|
20
|
+
for (const word of words) {
|
|
21
|
+
const textRef = createRef<Txt>();
|
|
22
|
+
containerRef().add(
|
|
23
|
+
<Txt
|
|
24
|
+
ref={textRef}
|
|
25
|
+
{...captionProps}
|
|
26
|
+
fill={textColor}
|
|
27
|
+
text={word.t}
|
|
28
|
+
opacity={INACTIVE_OPACITY}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
refs.push({ textRef });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { refs };
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
*animateWords({ words, refs, caption }) {
|
|
38
|
+
const textColor = caption.props.colors?.text ?? "#ffffff";
|
|
39
|
+
const highlightColor = caption.props.colors?.highlight ?? "#ff4081";
|
|
40
|
+
const textColorObj = new Color({ ...hexToRGB(textColor), a: 1 });
|
|
41
|
+
const highlightColorObj = new Color({ ...hexToRGB(highlightColor), a: 1 });
|
|
42
|
+
const n = refs.refs.length;
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < words.length; i++) {
|
|
45
|
+
for (let j = 0; j < n; j++) {
|
|
46
|
+
refs.refs[j].textRef().opacity(INACTIVE_OPACITY);
|
|
47
|
+
refs.refs[j].textRef().fill(textColorObj);
|
|
48
|
+
}
|
|
49
|
+
refs.refs[i].textRef().opacity(1);
|
|
50
|
+
refs.refs[i].textRef().fill(highlightColorObj, 0);
|
|
51
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
52
|
+
}
|
|
53
|
+
for (let j = 0; j < n; j++) {
|
|
54
|
+
refs.refs[j].textRef().opacity(INACTIVE_OPACITY);
|
|
55
|
+
refs.refs[j].textRef().fill(textColorObj);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Color, createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Txt } from "@twick/2d";
|
|
3
|
+
import { hexToRGB } from "../helpers/utils";
|
|
4
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lower third: broadcast-style bar. Same word-by-word animation as word_by_word;
|
|
8
|
+
* preparePhraseContainer gives the phrase a bar background (mirrors soft_box pattern).
|
|
9
|
+
*/
|
|
10
|
+
export const lowerThirdHandler: CaptionStyleHandler = {
|
|
11
|
+
id: "lower_third",
|
|
12
|
+
|
|
13
|
+
renderWords({ containerRef, words, caption }) {
|
|
14
|
+
const captionProps = caption.props;
|
|
15
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
16
|
+
|
|
17
|
+
for (const word of words) {
|
|
18
|
+
const textRef = createRef<Txt>();
|
|
19
|
+
containerRef().add(
|
|
20
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
|
|
21
|
+
);
|
|
22
|
+
refs.push({ textRef });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { refs };
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
*animateWords({ words, refs }) {
|
|
29
|
+
for (let i = 0; i < words.length; i++) {
|
|
30
|
+
yield* refs.refs[i].textRef().opacity(1, 0);
|
|
31
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
preparePhraseContainer({ phraseRef, phraseProps }) {
|
|
36
|
+
const opacity = Math.min(phraseProps?.bgOpacity ?? 0.75, 0.85);
|
|
37
|
+
const bgColor = phraseProps.colors?.bgColor ?? "#1a1a1a";
|
|
38
|
+
const _color = new Color({
|
|
39
|
+
...hexToRGB(bgColor),
|
|
40
|
+
a: opacity,
|
|
41
|
+
});
|
|
42
|
+
phraseRef().fill(_color);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Txt } from "@twick/2d";
|
|
3
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
4
|
+
|
|
5
|
+
export const outlineOnlyHandler: CaptionStyleHandler = {
|
|
6
|
+
id: "outline_only",
|
|
7
|
+
|
|
8
|
+
renderWords({ containerRef, words, caption }) {
|
|
9
|
+
const captionProps = caption.props;
|
|
10
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
11
|
+
|
|
12
|
+
for (const word of words) {
|
|
13
|
+
const textRef = createRef<Txt>();
|
|
14
|
+
containerRef().add(
|
|
15
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
|
|
16
|
+
);
|
|
17
|
+
refs.push({ textRef });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { refs };
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
*animateWords({ words, refs }) {
|
|
24
|
+
for (let i = 0; i < words.length; i++) {
|
|
25
|
+
yield* refs.refs[i].textRef().opacity(1, 0);
|
|
26
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Txt } from "@twick/2d";
|
|
3
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
4
|
+
|
|
5
|
+
const POP_DURATION = 0.2;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pop: words pop in with a quick opacity fade (same ref/animate pattern as word_by_word).
|
|
9
|
+
* Scale-based bounce can be added later if Txt scale API is confirmed in this engine.
|
|
10
|
+
*/
|
|
11
|
+
export const popScaleHandler: CaptionStyleHandler = {
|
|
12
|
+
id: "pop_scale",
|
|
13
|
+
|
|
14
|
+
renderWords({ containerRef, words, caption }) {
|
|
15
|
+
const captionProps = caption.props;
|
|
16
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
17
|
+
|
|
18
|
+
for (const word of words) {
|
|
19
|
+
const textRef = createRef<Txt>();
|
|
20
|
+
containerRef().add(
|
|
21
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
|
|
22
|
+
);
|
|
23
|
+
refs.push({ textRef });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { refs };
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
*animateWords({ words, refs }) {
|
|
30
|
+
for (let i = 0; i < words.length; i++) {
|
|
31
|
+
yield* refs.refs[i].textRef().opacity(1, POP_DURATION);
|
|
32
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s - POP_DURATION));
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CaptionStyleHandler } from "./types";
|
|
2
|
+
|
|
3
|
+
const handlers = new Map<string, CaptionStyleHandler>();
|
|
4
|
+
|
|
5
|
+
const DEFAULT_STYLE_ID = "word_by_word";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register a caption style handler.
|
|
9
|
+
*/
|
|
10
|
+
export function registerCaptionStyle(handler: CaptionStyleHandler): void {
|
|
11
|
+
handlers.set(handler.id, handler);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get handler for a style ID.
|
|
16
|
+
*/
|
|
17
|
+
export function getCaptionStyleHandler(id: string): CaptionStyleHandler | undefined {
|
|
18
|
+
return handlers.get(id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get default handler when style is unknown or missing.
|
|
23
|
+
*/
|
|
24
|
+
export function getDefaultCaptionStyleHandler(): CaptionStyleHandler {
|
|
25
|
+
const handler = handlers.get(DEFAULT_STYLE_ID);
|
|
26
|
+
if (!handler) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Caption style "${DEFAULT_STYLE_ID}" not registered. Ensure caption-styles are initialized.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return handler;
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Color, createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Txt } from "@twick/2d";
|
|
3
|
+
import { hexToRGB } from "../helpers/utils";
|
|
4
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
5
|
+
|
|
6
|
+
export const softBoxHandler: CaptionStyleHandler = {
|
|
7
|
+
id: "soft_box",
|
|
8
|
+
|
|
9
|
+
renderWords({ containerRef, words, caption }) {
|
|
10
|
+
const captionProps = caption.props;
|
|
11
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
12
|
+
|
|
13
|
+
for (const word of words) {
|
|
14
|
+
const textRef = createRef<Txt>();
|
|
15
|
+
containerRef().add(
|
|
16
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
|
|
17
|
+
);
|
|
18
|
+
refs.push({ textRef });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { refs };
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
*animateWords({ words, refs }) {
|
|
25
|
+
for (let i = 0; i < words.length; i++) {
|
|
26
|
+
yield* refs.refs[i].textRef().opacity(1, 0);
|
|
27
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
preparePhraseContainer({ phraseRef, phraseProps }) {
|
|
32
|
+
const opacity = Math.min(phraseProps?.bgOpacity ?? 0.6, 0.7);
|
|
33
|
+
const bgColor = phraseProps.colors?.bgColor ?? "#333333";
|
|
34
|
+
const _color = new Color({
|
|
35
|
+
...hexToRGB(bgColor),
|
|
36
|
+
a: opacity,
|
|
37
|
+
});
|
|
38
|
+
phraseRef().fill(_color);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Reference, ThreadGenerator } from "@twick/core";
|
|
2
|
+
import type { Rect, Txt } from "@twick/2d";
|
|
3
|
+
import type { Caption, CaptionProps } from "../helpers/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Word timing from splitPhraseTiming.
|
|
7
|
+
*/
|
|
8
|
+
export interface WordTiming {
|
|
9
|
+
t: string;
|
|
10
|
+
s: number;
|
|
11
|
+
e: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Refs returned by renderWords for animation.
|
|
16
|
+
* - textRef: main text element (required).
|
|
17
|
+
* - bgRef: optional background rect (e.g. highlight_bg).
|
|
18
|
+
* - highlightRef: optional overlay Txt in highlight color (e.g. karaoke current word).
|
|
19
|
+
* - prefixRefs: optional refs per character-step for letter-by-letter reveal (e.g. typewriter).
|
|
20
|
+
*/
|
|
21
|
+
export interface WordRefs {
|
|
22
|
+
refs: Array<{
|
|
23
|
+
textRef: Reference<Txt>;
|
|
24
|
+
bgRef?: Reference<Rect>;
|
|
25
|
+
highlightRef?: Reference<Txt>;
|
|
26
|
+
prefixRefs?: Reference<Txt>[];
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parameters for renderWords.
|
|
32
|
+
*/
|
|
33
|
+
export interface RenderWordsParams {
|
|
34
|
+
containerRef: Reference<Rect>;
|
|
35
|
+
words: WordTiming[];
|
|
36
|
+
caption: Caption & { props: CaptionProps };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parameters for animateWords.
|
|
41
|
+
*/
|
|
42
|
+
export interface AnimateWordsParams {
|
|
43
|
+
words: WordTiming[];
|
|
44
|
+
refs: WordRefs;
|
|
45
|
+
caption: Caption & { props: CaptionProps };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parameters for preparePhraseContainer (optional hook for phrase-level styling).
|
|
50
|
+
*/
|
|
51
|
+
export interface PreparePhraseContainerParams {
|
|
52
|
+
phraseRef: Reference<Rect>;
|
|
53
|
+
phraseProps: CaptionProps;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Caption style handler contract.
|
|
58
|
+
* Each style implements its own rendering and animation logic.
|
|
59
|
+
*/
|
|
60
|
+
export interface CaptionStyleHandler {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build word elements into container. Returns refs needed for animation.
|
|
65
|
+
*/
|
|
66
|
+
renderWords(params: RenderWordsParams): WordRefs;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generator: animate words over time.
|
|
70
|
+
*/
|
|
71
|
+
animateWords(params: AnimateWordsParams): ThreadGenerator;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Optional: customize phrase container before words are rendered (e.g. fill background).
|
|
75
|
+
*/
|
|
76
|
+
preparePhraseContainer?(params: PreparePhraseContainerParams): void;
|
|
77
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Txt } from "@twick/2d";
|
|
3
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Typewriter: words appear one by one (same pattern as word_by_word).
|
|
7
|
+
* Letter-by-letter would require a wrapper per word so prefixes overlay; follow highlight-bg structure if adding later.
|
|
8
|
+
*/
|
|
9
|
+
export const typewriterHandler: CaptionStyleHandler = {
|
|
10
|
+
id: "typewriter",
|
|
11
|
+
|
|
12
|
+
renderWords({ containerRef, words, caption }) {
|
|
13
|
+
const captionProps = caption.props;
|
|
14
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
15
|
+
|
|
16
|
+
for (const word of words) {
|
|
17
|
+
const textRef = createRef<Txt>();
|
|
18
|
+
containerRef().add(
|
|
19
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
|
|
20
|
+
);
|
|
21
|
+
refs.push({ textRef });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { refs };
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
*animateWords({ words, refs }) {
|
|
28
|
+
for (let i = 0; i < words.length; i++) {
|
|
29
|
+
yield* refs.refs[i].textRef().opacity(1, 0);
|
|
30
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Color, createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Txt } from "@twick/2d";
|
|
3
|
+
import { hexToRGB } from "../helpers/utils";
|
|
4
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
5
|
+
|
|
6
|
+
export const wordByWordWithBgHandler: CaptionStyleHandler = {
|
|
7
|
+
id: "word_by_word_with_bg",
|
|
8
|
+
|
|
9
|
+
renderWords({ containerRef, words, caption }) {
|
|
10
|
+
const captionProps = caption.props;
|
|
11
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
12
|
+
|
|
13
|
+
for (const word of words) {
|
|
14
|
+
const textRef = createRef<Txt>();
|
|
15
|
+
containerRef().add(
|
|
16
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
|
|
17
|
+
);
|
|
18
|
+
refs.push({ textRef });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { refs };
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
*animateWords({ words, refs }) {
|
|
25
|
+
for (let i = 0; i < words.length; i++) {
|
|
26
|
+
yield* refs.refs[i].textRef().opacity(1, 0);
|
|
27
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
preparePhraseContainer({ phraseRef, phraseProps }) {
|
|
32
|
+
const bgColor = phraseProps.colors?.bgColor ?? "#444444";
|
|
33
|
+
const _color = new Color({
|
|
34
|
+
...hexToRGB(bgColor),
|
|
35
|
+
a: phraseProps?.bgOpacity ?? 1,
|
|
36
|
+
});
|
|
37
|
+
phraseRef().fill(_color);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRef, waitFor } from "@twick/core";
|
|
2
|
+
import { Txt } from "@twick/2d";
|
|
3
|
+
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
4
|
+
|
|
5
|
+
export const wordByWordHandler: CaptionStyleHandler = {
|
|
6
|
+
id: "word_by_word",
|
|
7
|
+
|
|
8
|
+
renderWords({ containerRef, words, caption }) {
|
|
9
|
+
const captionProps = caption.props;
|
|
10
|
+
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
11
|
+
|
|
12
|
+
for (const word of words) {
|
|
13
|
+
const textRef = createRef<Txt>();
|
|
14
|
+
containerRef().add(
|
|
15
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0} />
|
|
16
|
+
);
|
|
17
|
+
refs.push({ textRef });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { refs };
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
*animateWords({ words, refs }) {
|
|
24
|
+
for (let i = 0; i < words.length; i++) {
|
|
25
|
+
yield* refs.refs[i].textRef().opacity(1, 0);
|
|
26
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
package/src/components/track.tsx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { Layout, Rect, View2D, Audio, Img, Txt } from "@twick/2d";
|
|
7
7
|
import { VisualizerTrack, WatermarkInput } from "../helpers/types";
|
|
8
|
-
import { all,
|
|
8
|
+
import { all, createRef, ThreadGenerator, waitFor } from "@twick/core";
|
|
9
9
|
import {
|
|
10
10
|
CAPTION_STYLE,
|
|
11
11
|
DEFAULT_CAPTION_COLORS,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { logger } from "../helpers/log.utils";
|
|
15
15
|
import elementController from "../controllers/element.controller";
|
|
16
16
|
import watermarkController from "../controllers/watermark.controller";
|
|
17
|
-
import {
|
|
17
|
+
import { getCaptionStyleHandler } from "../caption-styles";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Creates a video track with specified configuration
|
|
@@ -93,16 +93,18 @@ export function* makeCaptionTrack({
|
|
|
93
93
|
view.add(<Layout size={"100%"} ref={captionTrackRef} />);
|
|
94
94
|
|
|
95
95
|
const tProps = track?.props;
|
|
96
|
+
const defaultCapStyle = "highlight_bg";
|
|
96
97
|
|
|
97
98
|
const applyToAll = tProps?.applyToAll ?? false;
|
|
98
99
|
|
|
99
100
|
const trackDefaultProps =
|
|
100
|
-
(CAPTION_STYLE[tProps?.capStyle ??
|
|
101
|
+
(CAPTION_STYLE[tProps?.capStyle ?? defaultCapStyle] || CAPTION_STYLE[defaultCapStyle] || {}).word || {};
|
|
101
102
|
|
|
102
103
|
for (const element of track.elements) {
|
|
103
104
|
const eProps = element.props;
|
|
105
|
+
const resolvedCapStyle = eProps?.capStyle ?? tProps?.capStyle ?? defaultCapStyle;
|
|
104
106
|
const rectStyle =
|
|
105
|
-
(CAPTION_STYLE[
|
|
107
|
+
(CAPTION_STYLE[resolvedCapStyle] || CAPTION_STYLE[defaultCapStyle] || {}).rect ||
|
|
106
108
|
{};
|
|
107
109
|
// Cast alignItems/justifyContent as any to satisfy RectProps
|
|
108
110
|
const mappedRectStyle = {
|
|
@@ -113,11 +115,16 @@ export function* makeCaptionTrack({
|
|
|
113
115
|
|
|
114
116
|
const phraseColors = applyToAll ? tProps?.colors : eProps?.colors ?? tProps?.colors ?? DEFAULT_CAPTION_COLORS;
|
|
115
117
|
|
|
118
|
+
const resolvedFont = applyToAll ? tProps?.font : eProps?.font ?? tProps?.font ?? DEFAULT_CAPTION_FONT;
|
|
119
|
+
const defaults = trackDefaultProps as { fontFamily?: string; fontSize?: number; fontWeight?: number };
|
|
116
120
|
const phraseProps = {
|
|
117
121
|
...trackDefaultProps,
|
|
118
122
|
...(tProps?.captionProps || {}),
|
|
119
123
|
colors: phraseColors,
|
|
120
|
-
font:
|
|
124
|
+
font: resolvedFont,
|
|
125
|
+
fontFamily: resolvedFont?.family ?? defaults?.fontFamily ?? DEFAULT_CAPTION_FONT.family,
|
|
126
|
+
fontSize: resolvedFont?.size ?? defaults?.fontSize ?? DEFAULT_CAPTION_FONT.size,
|
|
127
|
+
fontWeight: resolvedFont?.weight ?? defaults?.fontWeight ?? DEFAULT_CAPTION_FONT.weight,
|
|
121
128
|
fill: phraseColors.text,
|
|
122
129
|
bgColor: phraseColors.bgColor,
|
|
123
130
|
bgOpacity: tProps?.bgOpacity ?? 1,
|
|
@@ -135,12 +142,22 @@ export function* makeCaptionTrack({
|
|
|
135
142
|
layout
|
|
136
143
|
/>
|
|
137
144
|
);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
// Allow styles to tweak how phrase-level props are interpreted.
|
|
146
|
+
// For classic outline, use explicit outlineColor so it is independent
|
|
147
|
+
// from per-word highlight color.
|
|
148
|
+
const styledPhraseProps = {
|
|
149
|
+
...phraseProps,
|
|
150
|
+
...(resolvedCapStyle === "outline_only" && phraseColors?.outlineColor
|
|
151
|
+
? { stroke: phraseColors.outlineColor }
|
|
152
|
+
: {}),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const styleHandler = getCaptionStyleHandler(resolvedCapStyle ?? "");
|
|
156
|
+
if (styleHandler?.preparePhraseContainer) {
|
|
157
|
+
styleHandler.preparePhraseContainer({
|
|
158
|
+
phraseRef,
|
|
159
|
+
phraseProps: styledPhraseProps,
|
|
142
160
|
});
|
|
143
|
-
phraseRef().fill(_color);
|
|
144
161
|
}
|
|
145
162
|
yield* elementController.get("caption")?.create({
|
|
146
163
|
containerRef: phraseRef,
|
|
@@ -148,7 +165,7 @@ export function* makeCaptionTrack({
|
|
|
148
165
|
...element,
|
|
149
166
|
t: element.t ?? "",
|
|
150
167
|
capStyle: eProps?.capStyle ?? tProps?.capStyle,
|
|
151
|
-
props:
|
|
168
|
+
props: styledPhraseProps,
|
|
152
169
|
},
|
|
153
170
|
view,
|
|
154
171
|
});
|
|
@@ -204,13 +221,16 @@ export function* makeElementTrack({
|
|
|
204
221
|
const sequence: ThreadGenerator[] = [];
|
|
205
222
|
try {
|
|
206
223
|
for (const element of track.elements) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
224
|
+
const handler = elementController.get(element.type);
|
|
225
|
+
if (handler) {
|
|
226
|
+
sequence.push(
|
|
227
|
+
handler.create({
|
|
228
|
+
containerRef: elementTrackRef,
|
|
229
|
+
element,
|
|
230
|
+
view,
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
}
|
|
214
234
|
}
|
|
215
235
|
} catch (error) {
|
|
216
236
|
logger("Error creating element track", error);
|