@twick/visualizer 0.15.24 → 0.15.25
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 +88 -88
- package/package.json +9 -9
- package/src/caption-styles/highlight-bg.handler.tsx +3 -3
- package/src/caption-styles/index.ts +2 -0
- package/src/caption-styles/karaoke-word.handler.tsx +54 -0
- package/src/caption-styles/karaoke.handler.tsx +15 -23
- package/src/caption-styles/lower-third.handler.tsx +11 -3
- package/src/caption-styles/outline-only.handler.tsx +7 -3
- package/src/caption-styles/pop-scale.handler.tsx +9 -4
- package/src/caption-styles/soft-box.handler.tsx +11 -3
- package/src/caption-styles/typewriter.handler.tsx +64 -18
- package/src/caption-styles/word-by-word-with-bg.handler.tsx +11 -3
- package/src/caption-styles/word-by-word.handler.tsx +11 -3
- package/src/components/track.tsx +50 -31
- package/src/elements/caption.element.tsx +38 -1
- package/src/helpers/constants.ts +8 -8
- package/src/helpers/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twick/visualizer",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.25",
|
|
4
4
|
"license": "https://github.com/ncounterspecialist/twick/blob/main/LICENSE.md",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "twick editor --projectFile ./src/live.tsx",
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@preact/signals": "^1.2.1",
|
|
26
|
-
"@twick/2d": "^0.15.
|
|
27
|
-
"@twick/core": "^0.15.
|
|
28
|
-
"@twick/effects": "0.15.
|
|
29
|
-
"@twick/media-utils": "0.15.
|
|
30
|
-
"@twick/renderer": "^0.15.
|
|
31
|
-
"@twick/vite-plugin": "^0.15.
|
|
26
|
+
"@twick/2d": "^0.15.25",
|
|
27
|
+
"@twick/core": "^0.15.25",
|
|
28
|
+
"@twick/effects": "0.15.25",
|
|
29
|
+
"@twick/media-utils": "0.15.25",
|
|
30
|
+
"@twick/renderer": "^0.15.25",
|
|
31
|
+
"@twick/vite-plugin": "^0.15.25",
|
|
32
32
|
"crelt": "^1.0.6",
|
|
33
33
|
"date-fns": "^4.1.0",
|
|
34
34
|
"preact": "^10.19.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@twick/cli": "^0.15.
|
|
38
|
-
"@twick/ui": "^0.15.
|
|
37
|
+
"@twick/cli": "^0.15.25",
|
|
38
|
+
"@twick/ui": "^0.15.25",
|
|
39
39
|
"typescript": "5.4.2",
|
|
40
40
|
"typedoc": "^0.25.8",
|
|
41
41
|
"typedoc-plugin-markdown": "^3.17.1",
|
|
@@ -26,11 +26,11 @@ export const highlightBgHandler: CaptionStyleHandler = {
|
|
|
26
26
|
<Rect
|
|
27
27
|
ref={bgContainerRef}
|
|
28
28
|
fill={_color}
|
|
29
|
-
width={textRef().width() + (captionProps.bgOffsetWidth ??
|
|
30
|
-
height={textRef().height() + (captionProps.bgOffsetHeight ??
|
|
29
|
+
width={textRef().width() + (captionProps.bgOffsetWidth ?? 20)}
|
|
30
|
+
height={textRef().height() + (captionProps.bgOffsetHeight ?? 8)}
|
|
31
31
|
margin={captionProps.bgMargin ?? [0, -5]}
|
|
32
32
|
radius={captionProps.bgRadius ?? 10}
|
|
33
|
-
padding={captionProps.bgPadding ?? [0,
|
|
33
|
+
padding={captionProps.bgPadding ?? [0, 10]}
|
|
34
34
|
opacity={0}
|
|
35
35
|
alignItems={"center"}
|
|
36
36
|
justifyContent={"center"}
|
|
@@ -7,6 +7,7 @@ import { softBoxHandler } from "./soft-box.handler";
|
|
|
7
7
|
import { lowerThirdHandler } from "./lower-third.handler";
|
|
8
8
|
import { typewriterHandler } from "./typewriter.handler";
|
|
9
9
|
import { karaokeHandler } from "./karaoke.handler";
|
|
10
|
+
import { karaokeWordHandler } from "./karaoke-word.handler";
|
|
10
11
|
import { popScaleHandler } from "./pop-scale.handler";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -22,6 +23,7 @@ export function registerCaptionStyles(): void {
|
|
|
22
23
|
registerCaptionStyle(lowerThirdHandler);
|
|
23
24
|
registerCaptionStyle(typewriterHandler);
|
|
24
25
|
registerCaptionStyle(karaokeHandler);
|
|
26
|
+
registerCaptionStyle(karaokeWordHandler);
|
|
25
27
|
registerCaptionStyle(popScaleHandler);
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { all, 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.45;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export const karaokeWordHandler: CaptionStyleHandler = {
|
|
10
|
+
id: "karaoke-word",
|
|
11
|
+
|
|
12
|
+
renderWords({ containerRef, words, caption }) {
|
|
13
|
+
const captionProps = caption.props;
|
|
14
|
+
|
|
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
|
|
21
|
+
ref={textRef}
|
|
22
|
+
{...captionProps}
|
|
23
|
+
text={word.t}
|
|
24
|
+
opacity={INACTIVE_OPACITY}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
refs.push({ textRef });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { refs };
|
|
31
|
+
},
|
|
32
|
+
*animateWords({ words, refs, caption }) {
|
|
33
|
+
const textColor = caption.props.colors?.text;
|
|
34
|
+
const highlightColor = caption.props.colors?.highlight ?? textColor;
|
|
35
|
+
for (let i = 0; i < words.length; i++) {
|
|
36
|
+
// Turn the previous word back to base color and remove outline,
|
|
37
|
+
// so its fill is clearly visible and not dominated by stroke.
|
|
38
|
+
if (i > 0) {
|
|
39
|
+
yield* all(
|
|
40
|
+
refs.refs[i - 1].textRef().fill(textColor, 0),
|
|
41
|
+
refs.refs[i - 1].textRef().opacity(INACTIVE_OPACITY, 0)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
// Highlight the current word using the highlight color.
|
|
45
|
+
// Keep its existing lineWidth/stroke so it feels “active”.
|
|
46
|
+
yield* all(
|
|
47
|
+
refs.refs[i].textRef().lineWidth(0, 0),
|
|
48
|
+
refs.refs[i].textRef().fill(highlightColor, 0),
|
|
49
|
+
refs.refs[i].textRef().opacity(1, 0)
|
|
50
|
+
);
|
|
51
|
+
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import { Color, createRef, waitFor } from "@twick/core";
|
|
1
|
+
import { all, Color, createRef, waitFor } from "@twick/core";
|
|
2
2
|
import { Txt } from "@twick/2d";
|
|
3
3
|
import { hexToRGB } from "../helpers/utils";
|
|
4
4
|
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
5
5
|
|
|
6
|
-
const INACTIVE_OPACITY = 0.
|
|
6
|
+
const INACTIVE_OPACITY = 0.45;
|
|
7
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
8
|
export const karaokeHandler: CaptionStyleHandler = {
|
|
13
9
|
id: "karaoke",
|
|
14
10
|
|
|
15
11
|
renderWords({ containerRef, words, caption }) {
|
|
16
12
|
const captionProps = caption.props;
|
|
17
|
-
|
|
13
|
+
|
|
18
14
|
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
19
15
|
|
|
20
16
|
for (const word of words) {
|
|
@@ -23,7 +19,6 @@ export const karaokeHandler: CaptionStyleHandler = {
|
|
|
23
19
|
<Txt
|
|
24
20
|
ref={textRef}
|
|
25
21
|
{...captionProps}
|
|
26
|
-
fill={textColor}
|
|
27
22
|
text={word.t}
|
|
28
23
|
opacity={INACTIVE_OPACITY}
|
|
29
24
|
/>
|
|
@@ -35,24 +30,21 @@ export const karaokeHandler: CaptionStyleHandler = {
|
|
|
35
30
|
},
|
|
36
31
|
|
|
37
32
|
*animateWords({ words, refs, caption }) {
|
|
38
|
-
const textColor = caption.props.colors?.text
|
|
39
|
-
const highlightColor = caption.props.colors?.highlight ??
|
|
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
|
-
|
|
33
|
+
const textColor = caption.props.colors?.text;
|
|
34
|
+
const highlightColor = caption.props.colors?.highlight ?? textColor;
|
|
44
35
|
for (let i = 0; i < words.length; i++) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
if (i > 0) {
|
|
37
|
+
yield* all(
|
|
38
|
+
refs.refs[i - 1].textRef().lineWidth(0, 0),
|
|
39
|
+
refs.refs[i - 1].textRef().fill(textColor, 0),
|
|
40
|
+
);
|
|
48
41
|
}
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
yield* all(
|
|
43
|
+
refs.refs[i].textRef().lineWidth(0, 0),
|
|
44
|
+
refs.refs[i].textRef().fill(highlightColor, 0),
|
|
45
|
+
refs.refs[i].textRef().opacity(1, 0)
|
|
46
|
+
);
|
|
51
47
|
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
52
48
|
}
|
|
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
49
|
},
|
|
58
50
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Color, createRef, waitFor } from "@twick/core";
|
|
1
|
+
import { all, Color, createRef, waitFor } from "@twick/core";
|
|
2
2
|
import { Txt } from "@twick/2d";
|
|
3
3
|
import { hexToRGB } from "../helpers/utils";
|
|
4
4
|
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
@@ -25,9 +25,17 @@ export const lowerThirdHandler: CaptionStyleHandler = {
|
|
|
25
25
|
return { refs };
|
|
26
26
|
},
|
|
27
27
|
|
|
28
|
-
*animateWords({ words, refs }) {
|
|
28
|
+
*animateWords({ words, refs, caption }) {
|
|
29
|
+
const textColor = caption.props.colors?.text;
|
|
30
|
+
const highlightColor = caption.props.colors?.highlight ?? textColor;
|
|
29
31
|
for (let i = 0; i < words.length; i++) {
|
|
30
|
-
|
|
32
|
+
if (i > 0) {
|
|
33
|
+
yield* all(refs.refs[i-1].textRef().lineWidth(0, 0), refs.refs[i-1].textRef().fill(textColor, 0));
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
yield* all(
|
|
37
|
+
refs.refs[i].textRef().fill(highlightColor, 0),
|
|
38
|
+
refs.refs[i].textRef().opacity(1, 0));
|
|
31
39
|
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
32
40
|
}
|
|
33
41
|
},
|
|
@@ -9,10 +9,11 @@ export const outlineOnlyHandler: CaptionStyleHandler = {
|
|
|
9
9
|
const captionProps = caption.props;
|
|
10
10
|
const refs: Array<{ textRef: ReturnType<typeof createRef<Txt>> }> = [];
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
for (const word of words) {
|
|
13
14
|
const textRef = createRef<Txt>();
|
|
14
15
|
containerRef().add(
|
|
15
|
-
<Txt ref={textRef} {...captionProps} text={word.t} opacity={0}
|
|
16
|
+
<Txt ref={textRef} {...captionProps} text={word.t} opacity={1} lineWidth={0}/>
|
|
16
17
|
);
|
|
17
18
|
refs.push({ textRef });
|
|
18
19
|
}
|
|
@@ -20,9 +21,12 @@ export const outlineOnlyHandler: CaptionStyleHandler = {
|
|
|
20
21
|
return { refs };
|
|
21
22
|
},
|
|
22
23
|
|
|
23
|
-
*animateWords({ words, refs }) {
|
|
24
|
+
*animateWords({ words, refs, caption }) {
|
|
24
25
|
for (let i = 0; i < words.length; i++) {
|
|
25
|
-
|
|
26
|
+
if(i > 0) {
|
|
27
|
+
yield* refs.refs[i-1].textRef().lineWidth(0, 0);
|
|
28
|
+
}
|
|
29
|
+
yield* refs.refs[i].textRef().lineWidth((caption.props.lineWidth ?? 0.4)*5, 0);
|
|
26
30
|
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
27
31
|
}
|
|
28
32
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createRef, waitFor } from "@twick/core";
|
|
1
|
+
import { all, createRef, waitFor } from "@twick/core";
|
|
2
2
|
import { Txt } from "@twick/2d";
|
|
3
3
|
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
4
4
|
|
|
5
|
-
const POP_DURATION = 0.
|
|
5
|
+
const POP_DURATION = 0.025;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Pop: words pop in with a quick opacity fade (same ref/animate pattern as word_by_word).
|
|
@@ -26,9 +26,14 @@ export const popScaleHandler: CaptionStyleHandler = {
|
|
|
26
26
|
return { refs };
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
*animateWords({ words, refs }) {
|
|
29
|
+
*animateWords({ words, refs, caption }) {
|
|
30
|
+
const textColor = caption.props.colors?.text;
|
|
31
|
+
const highlightColor = caption.props.colors?.highlight ?? textColor;
|
|
30
32
|
for (let i = 0; i < words.length; i++) {
|
|
31
|
-
|
|
33
|
+
if (i > 0) {
|
|
34
|
+
yield* all(refs.refs[i - 1].textRef().scale(0.95, 0), refs.refs[i - 1].textRef().lineWidth(0, 0), refs.refs[i-1].textRef().fill(textColor, 0))
|
|
35
|
+
}
|
|
36
|
+
yield* all(refs.refs[i].textRef().opacity(1, POP_DURATION), refs.refs[i].textRef().fill(highlightColor, 0), refs.refs[i].textRef().lineWidth(0, 0), refs.refs[i].textRef().scale(1.15, POP_DURATION));
|
|
32
37
|
yield* waitFor(Math.max(0, words[i].e - words[i].s - POP_DURATION));
|
|
33
38
|
}
|
|
34
39
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Color, createRef, waitFor } from "@twick/core";
|
|
1
|
+
import { all, Color, createRef, waitFor } from "@twick/core";
|
|
2
2
|
import { Txt } from "@twick/2d";
|
|
3
3
|
import { hexToRGB } from "../helpers/utils";
|
|
4
4
|
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
@@ -21,9 +21,17 @@ export const softBoxHandler: CaptionStyleHandler = {
|
|
|
21
21
|
return { refs };
|
|
22
22
|
},
|
|
23
23
|
|
|
24
|
-
*animateWords({ words, refs }) {
|
|
24
|
+
*animateWords({ words, refs, caption }) {
|
|
25
|
+
const textColor = caption.props.colors?.text;
|
|
26
|
+
const highlightColor = caption.props.colors?.highlight ?? textColor;
|
|
25
27
|
for (let i = 0; i < words.length; i++) {
|
|
26
|
-
|
|
28
|
+
if (i > 0) {
|
|
29
|
+
yield* all(refs.refs[i-1].textRef().lineWidth(0, 0), refs.refs[i-1].textRef().fill(textColor, 0));
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
yield* all(
|
|
33
|
+
refs.refs[i].textRef().fill(highlightColor, 0),
|
|
34
|
+
refs.refs[i].textRef().opacity(1, 0));
|
|
27
35
|
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
28
36
|
}
|
|
29
37
|
},
|
|
@@ -1,33 +1,79 @@
|
|
|
1
1
|
import { createRef, waitFor } from "@twick/core";
|
|
2
2
|
import { Txt } from "@twick/2d";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CaptionStyleHandler,
|
|
5
|
+
RenderWordsParams,
|
|
6
|
+
AnimateWordsParams,
|
|
7
|
+
} from "./types";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
* Typewriter:
|
|
7
|
-
*
|
|
10
|
+
* Typewriter: phrase-level, letter-by-letter typing effect.
|
|
11
|
+
* Mirrors the behavior of text-effects/TypewriterEffect but scoped to a
|
|
12
|
+
* single caption phrase. All words in the phrase are rendered as one
|
|
13
|
+
* continuous string and revealed character by character over the phrase
|
|
14
|
+
* duration.
|
|
8
15
|
*/
|
|
9
16
|
export const typewriterHandler: CaptionStyleHandler = {
|
|
10
17
|
id: "typewriter",
|
|
11
18
|
|
|
12
|
-
renderWords({ containerRef, words, caption }) {
|
|
19
|
+
renderWords({ containerRef, words, caption }: RenderWordsParams) {
|
|
13
20
|
const captionProps = caption.props;
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const phraseText =
|
|
22
|
+
caption.t && caption.t.length > 0
|
|
23
|
+
? caption.t
|
|
24
|
+
: words.map((w) => w.t).join(" ");
|
|
25
|
+
|
|
26
|
+
const textRef = createRef<Txt>();
|
|
27
|
+
containerRef().add(
|
|
28
|
+
<Txt ref={textRef} {...captionProps} text={phraseText} />
|
|
29
|
+
);
|
|
23
30
|
|
|
24
|
-
return { refs };
|
|
31
|
+
return { refs: [{ textRef }] };
|
|
25
32
|
},
|
|
26
33
|
|
|
27
|
-
*animateWords({ words, refs }) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
*animateWords({ words, refs }: AnimateWordsParams) {
|
|
35
|
+
if (!refs.refs.length || !words.length) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const textRef = refs.refs[0].textRef;
|
|
40
|
+
const fullText = textRef().text() ?? "";
|
|
41
|
+
|
|
42
|
+
if (!fullText.length) {
|
|
43
|
+
// Nothing to animate; just keep the text as-is.
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Compute total phrase duration from word timings.
|
|
48
|
+
const phraseStart = words[0].s;
|
|
49
|
+
const phraseEnd = words[words.length - 1].e;
|
|
50
|
+
const totalDuration = Math.max(0, phraseEnd - phraseStart);
|
|
51
|
+
|
|
52
|
+
if (totalDuration <= 0) {
|
|
53
|
+
textRef().text(fullText);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Preserve original size to avoid layout shifts while typing.
|
|
58
|
+
const size = textRef().size();
|
|
59
|
+
textRef().text("");
|
|
60
|
+
textRef().size(size);
|
|
61
|
+
|
|
62
|
+
// Left-align for a more natural typing feel.
|
|
63
|
+
textRef().textAlign("left");
|
|
64
|
+
|
|
65
|
+
// Reserve a small buffer at the end so the final state is visible.
|
|
66
|
+
const bufferTime = Math.min(totalDuration * 0.1, 0.5);
|
|
67
|
+
const effectiveDuration = Math.max(0.001, totalDuration - bufferTime);
|
|
68
|
+
const interval = effectiveDuration / fullText.length;
|
|
69
|
+
|
|
70
|
+
// Small initial pause before the first character.
|
|
71
|
+
yield* waitFor(interval);
|
|
72
|
+
|
|
73
|
+
// Reveal each character sequentially.
|
|
74
|
+
for (let i = 0; i < fullText.length; i++) {
|
|
75
|
+
yield* waitFor(interval);
|
|
76
|
+
textRef().text(fullText.substring(0, i + 1));
|
|
31
77
|
}
|
|
32
78
|
},
|
|
33
79
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Color, createRef, waitFor } from "@twick/core";
|
|
1
|
+
import { all, Color, createRef, waitFor } from "@twick/core";
|
|
2
2
|
import { Txt } from "@twick/2d";
|
|
3
3
|
import { hexToRGB } from "../helpers/utils";
|
|
4
4
|
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
@@ -21,9 +21,17 @@ export const wordByWordWithBgHandler: CaptionStyleHandler = {
|
|
|
21
21
|
return { refs };
|
|
22
22
|
},
|
|
23
23
|
|
|
24
|
-
*animateWords({ words, refs }) {
|
|
24
|
+
*animateWords({ words, refs, caption }) {
|
|
25
|
+
const textColor = caption.props.colors?.text;
|
|
26
|
+
const highlightColor = caption.props.colors?.highlight ?? textColor;
|
|
25
27
|
for (let i = 0; i < words.length; i++) {
|
|
26
|
-
|
|
28
|
+
if (i > 0) {
|
|
29
|
+
yield* all(refs.refs[i-1].textRef().lineWidth(0, 0), refs.refs[i-1].textRef().fill(textColor, 0));
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
yield* all(
|
|
33
|
+
refs.refs[i].textRef().fill(highlightColor, 0),
|
|
34
|
+
refs.refs[i].textRef().opacity(1, 0));
|
|
27
35
|
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
28
36
|
}
|
|
29
37
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createRef, waitFor } from "@twick/core";
|
|
1
|
+
import { all, createRef, useLogger, waitFor } from "@twick/core";
|
|
2
2
|
import { Txt } from "@twick/2d";
|
|
3
3
|
import type { CaptionStyleHandler, RenderWordsParams, AnimateWordsParams } from "./types";
|
|
4
4
|
|
|
@@ -20,9 +20,17 @@ export const wordByWordHandler: CaptionStyleHandler = {
|
|
|
20
20
|
return { refs };
|
|
21
21
|
},
|
|
22
22
|
|
|
23
|
-
*animateWords({ words, refs }) {
|
|
23
|
+
*animateWords({ words, refs, caption }) {
|
|
24
|
+
const textColor = caption.props.colors?.text;
|
|
25
|
+
const highlightColor = caption.props.colors?.highlight ?? textColor;
|
|
24
26
|
for (let i = 0; i < words.length; i++) {
|
|
25
|
-
|
|
27
|
+
if (i > 0) {
|
|
28
|
+
yield* all(refs.refs[i-1].textRef().lineWidth(0, 0), refs.refs[i-1].textRef().fill(textColor, 0));
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
yield* all(
|
|
32
|
+
refs.refs[i].textRef().fill(highlightColor, 0),
|
|
33
|
+
refs.refs[i].textRef().opacity(1, 0));
|
|
26
34
|
yield* waitFor(Math.max(0, words[i].e - words[i].s));
|
|
27
35
|
}
|
|
28
36
|
},
|
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, createRef, ThreadGenerator, waitFor } from "@twick/core";
|
|
8
|
+
import { all, createRef, ThreadGenerator, useLogger, waitFor } from "@twick/core";
|
|
9
9
|
import {
|
|
10
10
|
CAPTION_STYLE,
|
|
11
11
|
DEFAULT_CAPTION_COLORS,
|
|
@@ -89,74 +89,93 @@ export function* makeCaptionTrack({
|
|
|
89
89
|
track: VisualizerTrack;
|
|
90
90
|
}) {
|
|
91
91
|
let prevTime = 0;
|
|
92
|
+
|
|
92
93
|
const captionTrackRef = createRef<any>();
|
|
93
94
|
view.add(<Layout size={"100%"} ref={captionTrackRef} />);
|
|
94
95
|
|
|
95
96
|
const tProps = track?.props;
|
|
96
97
|
const defaultCapStyle = "highlight_bg";
|
|
98
|
+
|
|
99
|
+
for (const element of track.elements) {
|
|
100
|
+
const eProps = element.props ?? {};
|
|
101
|
+
// wordsMs belongs to caption timing only – keep it out of visual style merging.
|
|
102
|
+
const { wordsMs, ...elementPropsWithoutWords } = eProps as any;
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const trackDefaultProps =
|
|
101
|
-
(CAPTION_STYLE[tProps?.capStyle ?? defaultCapStyle] || CAPTION_STYLE[defaultCapStyle] || {}).word || {};
|
|
104
|
+
const resolvedCapStyle =
|
|
105
|
+
(elementPropsWithoutWords as any)?.capStyle ?? tProps?.capStyle ?? defaultCapStyle;
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const rectStyle =
|
|
107
|
-
(CAPTION_STYLE[resolvedCapStyle] || CAPTION_STYLE[defaultCapStyle] || {}).rect ||
|
|
108
|
-
{};
|
|
107
|
+
const styleConfig = CAPTION_STYLE[resolvedCapStyle] || CAPTION_STYLE[defaultCapStyle] || {};
|
|
108
|
+
const trackDefaultProps = (styleConfig as any).word || {};
|
|
109
|
+
const rectStyle = (styleConfig as any).rect || {};
|
|
109
110
|
// Cast alignItems/justifyContent as any to satisfy RectProps
|
|
110
111
|
const mappedRectStyle = {
|
|
111
112
|
...rectStyle,
|
|
112
113
|
justifyContent: rectStyle.justifyContent as any,
|
|
113
114
|
alignItems: rectStyle.alignItems as any,
|
|
115
|
+
...(tProps?.rectProps ?? {}),
|
|
114
116
|
};
|
|
115
117
|
|
|
116
|
-
|
|
118
|
+
// Resolve colors with priority: element.props.colors > track.props.colors > defaults.
|
|
119
|
+
const elementColors = (elementPropsWithoutWords as any)?.colors;
|
|
120
|
+
const trackColors = tProps?.colors;
|
|
121
|
+
const phraseColors = elementColors ?? trackColors ?? DEFAULT_CAPTION_COLORS;
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
123
|
+
// Resolve font with priority: element.props.font > track.props.font > defaults.
|
|
124
|
+
const elementFont = (elementPropsWithoutWords as any)?.font;
|
|
125
|
+
const trackFont = tProps?.font;
|
|
126
|
+
const resolvedFont = elementFont ?? trackFont ?? DEFAULT_CAPTION_FONT;
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
const basePhraseStyle = {
|
|
121
130
|
...trackDefaultProps,
|
|
122
|
-
...
|
|
131
|
+
...tProps,
|
|
132
|
+
...elementPropsWithoutWords,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const bgOpacityFromBase = (basePhraseStyle as any)?.bgOpacity;
|
|
136
|
+
|
|
137
|
+
const phraseProps = {
|
|
138
|
+
...basePhraseStyle,
|
|
123
139
|
colors: phraseColors,
|
|
140
|
+
stroke: phraseColors.outlineColor,
|
|
124
141
|
font: resolvedFont,
|
|
125
|
-
fontFamily:
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
fontFamily:
|
|
143
|
+
resolvedFont.family,
|
|
144
|
+
fontSize:
|
|
145
|
+
resolvedFont.size,
|
|
146
|
+
fontWeight:
|
|
147
|
+
resolvedFont.weight,
|
|
128
148
|
fill: phraseColors.text,
|
|
129
149
|
bgColor: phraseColors.bgColor,
|
|
130
|
-
bgOpacity: tProps?.bgOpacity ?? 1,
|
|
150
|
+
bgOpacity: bgOpacityFromBase ?? tProps?.bgOpacity ?? 1,
|
|
131
151
|
};
|
|
132
152
|
|
|
133
153
|
yield* waitFor(element?.s - prevTime);
|
|
134
154
|
const phraseRef = createRef<any>();
|
|
155
|
+
const rectProps = tProps?.rectProps ?? {};
|
|
135
156
|
captionTrackRef().add(
|
|
136
157
|
<Rect
|
|
137
158
|
ref={phraseRef}
|
|
138
159
|
key={element.id}
|
|
139
160
|
{...mappedRectStyle}
|
|
140
|
-
x={
|
|
141
|
-
y={
|
|
161
|
+
x={basePhraseStyle.x}
|
|
162
|
+
y={basePhraseStyle.y}
|
|
142
163
|
layout
|
|
143
164
|
/>
|
|
144
165
|
);
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
const
|
|
166
|
+
|
|
167
|
+
// Ensure timing metadata (wordsMs) is preserved on the caption props
|
|
168
|
+
// while keeping it out of the visual style merging above.
|
|
169
|
+
const captionPropsForElement = {
|
|
149
170
|
...phraseProps,
|
|
150
|
-
...(
|
|
151
|
-
? { stroke: phraseColors.outlineColor }
|
|
152
|
-
: {}),
|
|
171
|
+
...(wordsMs ? { wordsMs } : {}),
|
|
153
172
|
};
|
|
154
173
|
|
|
155
174
|
const styleHandler = getCaptionStyleHandler(resolvedCapStyle ?? "");
|
|
156
175
|
if (styleHandler?.preparePhraseContainer) {
|
|
157
176
|
styleHandler.preparePhraseContainer({
|
|
158
177
|
phraseRef,
|
|
159
|
-
phraseProps
|
|
178
|
+
phraseProps,
|
|
160
179
|
});
|
|
161
180
|
}
|
|
162
181
|
yield* elementController.get("caption")?.create({
|
|
@@ -165,7 +184,7 @@ export function* makeCaptionTrack({
|
|
|
165
184
|
...element,
|
|
166
185
|
t: element.t ?? "",
|
|
167
186
|
capStyle: eProps?.capStyle ?? tProps?.capStyle,
|
|
168
|
-
props:
|
|
187
|
+
props: captionPropsForElement,
|
|
169
188
|
},
|
|
170
189
|
view,
|
|
171
190
|
});
|
|
@@ -3,6 +3,43 @@ import { ThreadGenerator } from "@twick/core";
|
|
|
3
3
|
import { splitPhraseTiming } from "../helpers/caption.utils";
|
|
4
4
|
import { getCaptionStyleHandler, getDefaultCaptionStyleHandler } from "../caption-styles";
|
|
5
5
|
|
|
6
|
+
function computeWordTimings(caption: ElementParams["caption"]) {
|
|
7
|
+
if (!caption) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const text = caption.t ?? "";
|
|
12
|
+
const baseWords = text.split(" ").filter((w) => w.length > 0);
|
|
13
|
+
if (!baseWords.length) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// wordsMs is stored in milliseconds (absolute along the media timeline).
|
|
18
|
+
const wordsMs: number[] | undefined = (caption as any)?.props?.wordsMs;
|
|
19
|
+
|
|
20
|
+
if (!Array.isArray(wordsMs) || wordsMs.length < baseWords.length) {
|
|
21
|
+
// Fallback: proportional split across phrase duration.
|
|
22
|
+
return splitPhraseTiming(caption);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const phraseEndSec = caption.e;
|
|
26
|
+
|
|
27
|
+
return baseWords.map((word, index) => {
|
|
28
|
+
const startMs = wordsMs[index];
|
|
29
|
+
const endMs =
|
|
30
|
+
index + 1 < wordsMs.length ? wordsMs[index + 1] : phraseEndSec * 1000;
|
|
31
|
+
|
|
32
|
+
const startSec = startMs / 1000;
|
|
33
|
+
const endSec = endMs / 1000;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
t: word,
|
|
37
|
+
s: startSec,
|
|
38
|
+
e: endSec,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
6
43
|
/**
|
|
7
44
|
* @group CaptionElement
|
|
8
45
|
* CaptionElement creates and manages styled text overlays in the visualizer scene.
|
|
@@ -15,7 +52,7 @@ export const CaptionElement = {
|
|
|
15
52
|
name: "caption",
|
|
16
53
|
|
|
17
54
|
*create({ containerRef, caption, containerProps }: ElementParams): ThreadGenerator {
|
|
18
|
-
const words =
|
|
55
|
+
const words = computeWordTimings(caption);
|
|
19
56
|
if (!words?.length) return;
|
|
20
57
|
|
|
21
58
|
const handler =
|