【问题标题】:How can I get a word's bounding rectangle within a paragraph, while only knowing it's index?如何在段落中获取单词的边界矩形,而只知道它的索引?
【发布时间】:2020-04-18 13:49:38
【问题描述】:

我正在一个 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直接使用绝对坐标放置。

他们如何在只知道字符串索引的情况下访问段落中单词的边界矩形?

【问题讨论】:

标签: javascript html css reactjs text-to-speech


【解决方案1】:

Here is the result以下解决方案


编辑 2:

正如 cmets 中提到的,当屏幕改变大小以及用户缩放或滚动时,具有固定的定位会导致问题。

要创建相对定位,可以先获取父元素的偏移量:const { offsetTop, offsetLeft } = containerEl.current;

然后将它们减去 fetch 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&lt;DOMRect[] | null&gt;(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 做到这一点。

setStartsetEnd 方法可以接受索引变量作为第二个参数。

然后我在范围本身上使用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;
}

如果有人有兴趣,我会上传解决方案工作的视频

【讨论】:

  • 看起来棒极了。我会很高兴看到它工作!考虑在元素中使用position: absolute,它被赋予position: relative,因为position: fixed可能会在滚动、放大/缩小页面等时给您带来错误。
  • 谢谢!好的,我会录制并上传一个非常早期的版本。感谢您的建议,我会尝试改变它
  • 我在回答中添加了一个简短的视频链接
  • 看起来棒极了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-16
  • 2023-04-02
  • 2020-07-11
  • 2018-12-29
  • 1970-01-01
相关资源
最近更新 更多