我正在 React 应用程序中创建文本转语音功能,该功能可以通过在当前说出的单词后面放置背景来突出显示当前说出的单词。
该功能与 Firefox reader view 非常相似.
我实现的解决方案只是剪切段落字符串,并在每次渲染时在口语单词周围放置一个跨度,这会占用大量资源并且无法制作动画。
这是代码:(我打算废弃)
export interface SpeakEvent {
start: number;
end: number;
type: string;
}
export default function TextNode({ content }: TextNodeProps) {
const [highlight, setHighlight] = useState<SpeakEvent | null>(null);
useEffect(() => {
registerText((ev) => {
if (ev?.type === 'word' || !ev)
setHighlight((old) => {
/* Irrelevant code */
return ev;
});
}, content);
}, [content]);
const { start, end } = highlight ?? {};
let segments = [content];
if (highlight) {
segments = [
segments[0].slice(0, start),
segments[0].slice(start, end),
segments[0].slice(end),
];
}
return (
<>
{segments.map((seg, i) =>
i === 1 ? (
<span key={i} className={'highlight'}>
{seg}
</span>
) : (
seg
)
)}
</>
);
}
Firefox 阅读器正在使用更智能的方式来做到这一点。它使用放置在口语单词后面的 div,然后是 moved around :
包含高亮效果的div直接使用绝对坐标放置。
他们如何在只知道字符串索引的情况下访问段落内单词的边界矩形?
最佳答案
Here is the result of the following solution
编辑2:
正如评论中提到的,当屏幕改变尺寸以及用户缩放或滚动时,固定定位会导致问题。
要创建相对定位,可以首先获取父元素的偏移量:const { offsetTop, offsetLeft } = containerEl.current;
然后将它们减去获取的 DomRect :
return Array.from(range.getClientRects()).map(
({ top, left, width, height }) => ({
top: top - offsetTop,
left: left - offsetLeft,
width,
height,
})
);
只需申请position: relative
到文本父级,然后 position: absolute
到文本叠加,瞧。
编辑:
下面的解决方案不适用于换行字(例如下图中的non-violent
)
生成的框占据一个矩形,覆盖单词的两个部分。
相反,请使用 getClientRects
获取呈现相同字符串的所有框,然后将其映射到相同的覆盖层:
状态类型:const [highlighst, setHighlights] = useState<DOMRect[] | null>(null);
在高亮设置中:return Array.from(range.getBoundingClientRect());
渲染效果:
{highlights &&
highlights.map(({ top, left, width, height }) => (
<span
className='text-highlight'
style={{
top,
left,
width,
height,
}}
></span>
))}
结果:
我最终能够使用 Range API 做到这一点.
setStart
和setEnd
方法可以接受索引变量作为第二个参数。
然后我使用 getBoundingClientRect
获取文本坐标范围本身并将其放入我的状态中。
我现在可以将这些值应用到渲染中的固定 div 上:
const range = document.createRange();
export default function TextNode({ content, footnote }: TextNodeProps) {
const [highlight, setHighlight] = useState<DOMRect | null>(null);
const containerEl = useRef<HTMLSpanElement>(null);
useEffect(() => {
registerText((ev) => {
if (!ev) {
setHighlight(null);
return;
}
if (ev.type === 'sentence') {
(textEl.current as HTMLSpanElement | null)?.scrollIntoView(
scrollOptions
);
}
if (ev.type === 'word')
setHighlight((old) => {
const txtNode = containerEl.current?.firstChild as Node;
range.setStart(txtNode, ev.start);
range.setEnd(txtNode, ev.end);
if (!old) {
(containerEl.current as HTMLSpanElement | null)?.scrollIntoView(
scrollOptions
);
}
return range.getBoundingClientRect();
});
}, content);
}, [content]);
return (
<span ref={containerEl}>
{content}
{highlight && (
<div
className='text-highlight'
style={{
top: highlight.top,
left: highlight.left,
width: highlight.width,
height: highlight.height,
}}
></div>
)}
</span>
);
}
移动 div 的 CSS :
.text-highlight {
position: fixed;
border-bottom: 4px solid blue;
opacity: 0.7;
transition-property: top, left, height, width;
transition-duration: 0.2s;
transform-style: ease-in-out;
}
如果有人感兴趣,我将上传该解决方案的视频
关于javascript - 如何在只知道它的索引的情况下获得段落内单词的边界矩形?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61290220/