|
import {JSX, useEffect, useRef, useState} from 'react'; |
|
import robotoFontFamilyJson from '../assets/RobotoMono-Regular-msdf.json?url'; |
|
import robotoFontTexture from '../assets/RobotoMono-Regular.png'; |
|
import ThreeMeshUIText, {ThreeMeshUITextType} from './ThreeMeshUIText'; |
|
import {getURLParams} from '../URLParams'; |
|
import {CURSOR_BLINK_INTERVAL_MS} from '../cursorBlinkInterval'; |
|
|
|
const NUM_LINES = 3; |
|
|
|
export const CHARS_PER_LINE = 37; |
|
const MAX_WIDTH = 0.89; |
|
const CHAR_WIDTH = 0.0235; |
|
const Y_COORD_START = -0.38; |
|
const Z_COORD = -1.3; |
|
const LINE_HEIGHT = 0.062; |
|
const BLOCK_SPACING = 0.02; |
|
const FONT_SIZE = 0.038; |
|
|
|
const SCROLL_Y_DELTA = 0.001; |
|
|
|
|
|
const OFFSET = 0.01; |
|
const OFFSET_WIDTH = OFFSET * 3; |
|
|
|
type Props = { |
|
content: string; |
|
|
|
y: number; |
|
|
|
startY: number; |
|
width: number; |
|
height: number; |
|
textOpacity: number; |
|
backgroundOpacity: number; |
|
|
|
index: string; |
|
enableAnimation: boolean; |
|
}; |
|
|
|
function TextBlock({ |
|
content, |
|
y, |
|
startY, |
|
width, |
|
height, |
|
textOpacity, |
|
backgroundOpacity, |
|
index, |
|
enableAnimation, |
|
}: Props) { |
|
const [scrollY, setScrollY] = useState<number>(y); |
|
|
|
|
|
const lastIndex = useRef<string>(index); |
|
useEffect(() => { |
|
if (index != lastIndex.current) { |
|
lastIndex.current = index; |
|
enableAnimation && setScrollY(startY); |
|
} else if (scrollY < y) { |
|
setScrollY((prev) => prev + SCROLL_Y_DELTA); |
|
} |
|
}, [enableAnimation, index, scrollY, setScrollY, startY, y]); |
|
|
|
|
|
const textRef = useRef<ThreeMeshUITextType>(); |
|
useEffect(() => { |
|
if (textRef.current != null) { |
|
textRef.current.set({content}); |
|
} |
|
}, [content, textRef, y, startY]); |
|
|
|
|
|
const xPosition = width / 2 - MAX_WIDTH / 2 + OFFSET_WIDTH; |
|
return ( |
|
<> |
|
<block |
|
args={[ |
|
{ |
|
backgroundOpacity, |
|
width: width + OFFSET_WIDTH, |
|
height: height, |
|
borderRadius: 0, |
|
}, |
|
]} |
|
position={[-OFFSET_WIDTH + xPosition, scrollY, Z_COORD]}></block> |
|
<block |
|
args={[{padding: 0, backgroundOpacity: 0, width, height}]} |
|
position={[xPosition, scrollY + OFFSET, Z_COORD]}> |
|
<block |
|
args={[ |
|
{ |
|
width, |
|
height, |
|
fontSize: FONT_SIZE, |
|
textAlign: 'left', |
|
backgroundOpacity: 0, |
|
// TODO: support more language charsets |
|
// This renders using MSDF format supported in WebGL. Renderable characters are defined in the "charset" json |
|
// Currently supports most default keyboard inputs but this would exclude many non latin charset based languages. |
|
// You can use https://msdf-bmfont.donmccurdy.com/ for easily generating these files |
|
// fontFamily: '/src/assets/Roboto-msdf.json', |
|
// fontTexture: '/src/assets/Roboto-msdf.png' |
|
fontFamily: robotoFontFamilyJson, |
|
fontTexture: robotoFontTexture, |
|
}, |
|
]}> |
|
<ThreeMeshUIText ref={textRef} content="" fontOpacity={textOpacity} /> |
|
</block> |
|
</block> |
|
</> |
|
); |
|
} |
|
|
|
|
|
function TranscriptionPanel() { |
|
const panelHeight = LINE_HEIGHT * NUM_LINES + 2 * BLOCK_SPACING + 2 * OFFSET; |
|
const xPosition = OFFSET_WIDTH; |
|
return ( |
|
<block |
|
args={[ |
|
{ |
|
backgroundOpacity: 1, |
|
width: |
|
MAX_WIDTH * ((CHARS_PER_LINE + 2) / CHARS_PER_LINE) + |
|
2 * OFFSET_WIDTH, |
|
height: panelHeight, |
|
borderRadius: 0, |
|
}, |
|
]} |
|
position={[ |
|
-OFFSET + xPosition, |
|
Y_COORD_START + panelHeight / 2 - 2 * OFFSET, |
|
Z_COORD, |
|
]}></block> |
|
); |
|
} |
|
|
|
export default function TextBlocks({ |
|
sentences, |
|
blinkCursor, |
|
}: { |
|
sentences: string[][]; |
|
blinkCursor: boolean; |
|
}) { |
|
const showTranscriptionPanel = |
|
getURLParams().ARTranscriptionType === 'lines_with_background'; |
|
const textBlocks: JSX.Element[] = []; |
|
|
|
const [cursorBlinkOn, setCursorBlinkOn] = useState(false); |
|
useEffect(() => { |
|
if (blinkCursor) { |
|
const interval = setInterval(() => { |
|
setCursorBlinkOn((prev) => !prev); |
|
}, CURSOR_BLINK_INTERVAL_MS); |
|
|
|
return () => clearInterval(interval); |
|
} else { |
|
setCursorBlinkOn(false); |
|
} |
|
}, [blinkCursor]); |
|
|
|
|
|
let currentY = Y_COORD_START; |
|
for (let i = sentences.length - 1; i >= 0; i--) { |
|
const sentenceLines = sentences[i]; |
|
for (let j = sentenceLines.length - 1; j >= 0; j--) { |
|
if (textBlocks.length == NUM_LINES) { |
|
if (showTranscriptionPanel) { |
|
textBlocks.push(<TranscriptionPanel key={textBlocks.length} />); |
|
} |
|
return textBlocks; |
|
} |
|
|
|
const isBottomSentence = i === sentences.length - 1; |
|
const isBottomLine = isBottomSentence && textBlocks.length === 0; |
|
const y = currentY + LINE_HEIGHT / 2; |
|
let textBlockLine = sentenceLines[j]; |
|
const numChars = textBlockLine.length; |
|
|
|
if (cursorBlinkOn && isBottomLine) { |
|
textBlockLine = textBlockLine + '|'; |
|
} |
|
|
|
|
|
const blockWidth = |
|
(numChars + (isBottomLine ? 1.1 : 0) + (numChars < 10 ? 1 : 0)) * |
|
CHAR_WIDTH; |
|
const textOpacity = 1 - 0.1 * textBlocks.length; |
|
textBlocks.push( |
|
<TextBlock |
|
key={textBlocks.length} |
|
y={y} |
|
startY={currentY} |
|
index={`${sentences.length - i},${j}`} |
|
textOpacity={textOpacity} |
|
backgroundOpacity={0.98} |
|
height={LINE_HEIGHT} |
|
width={blockWidth} |
|
// content={"BLOCK " + textBlocks.length + ": " + content} |
|
content={textBlockLine} |
|
enableAnimation={!isBottomLine} |
|
/>, |
|
); |
|
|
|
currentY = y + LINE_HEIGHT / 2; |
|
} |
|
currentY += showTranscriptionPanel ? BLOCK_SPACING / 3 : BLOCK_SPACING; |
|
} |
|
|
|
const numRemainingBlocks = textBlocks.length - NUM_LINES; |
|
if (numRemainingBlocks > 0) { |
|
Array.from({length: numRemainingBlocks}).forEach(() => { |
|
|
|
textBlocks.push( |
|
<TextBlock |
|
key={textBlocks.length} |
|
y={Y_COORD_START} |
|
startY={0} |
|
index="0,0" |
|
textOpacity={0} |
|
backgroundOpacity={0} |
|
enableAnimation={false} |
|
width={MAX_WIDTH} |
|
height={LINE_HEIGHT} |
|
content="" |
|
/>, |
|
); |
|
}); |
|
} |
|
|
|
if (showTranscriptionPanel) { |
|
textBlocks.push(<TranscriptionPanel key={textBlocks.length} />); |
|
} |
|
return textBlocks; |
|
} |
|
|