karaoke lines vertical
This commit is contained in:
@@ -65,28 +65,216 @@ export function groupAlignedWordsToLines(alignedWords = [], { removeTags = true
|
||||
return out
|
||||
}
|
||||
|
||||
export function groupAlignedWordsToLineItems(
|
||||
alignedWords = [],
|
||||
{ removeTags = true } = {}
|
||||
) {
|
||||
const out = []
|
||||
let buf = []
|
||||
let start = null
|
||||
const clean = (txt) =>
|
||||
String(txt || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || '').trim())
|
||||
const isSectionTag = (txt) => /^\s*\[[^\]]+\]\s*$/i.test((txt || '').trim())
|
||||
const pushLine = (words, endS) => {
|
||||
const text = clean(words.map((item) => item.text).join(' '))
|
||||
if (!text) return
|
||||
out.push({
|
||||
text,
|
||||
startS: start ?? Number(words[0]?.startS || 0),
|
||||
endS: Number(endS || words[words.length - 1]?.endS || 0),
|
||||
words,
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < alignedWords.length; i++) {
|
||||
const w = alignedWords[i] || {}
|
||||
const original = String(w.word || '')
|
||||
const textNoNewline = original.replace(/\n/g, ' ')
|
||||
const textNoTag = removeTags ? textNoNewline.replace(/^\s*\[[^\]]+\]\s*/i, '') : textNoNewline
|
||||
if (buf.length === 0) start = Number(w.startS || 0)
|
||||
|
||||
if (isSectionTag(textNoNewline)) {
|
||||
if (!removeTags) {
|
||||
const joined = clean(textNoNewline)
|
||||
if (joined) {
|
||||
out.push({
|
||||
text: joined,
|
||||
startS: start ?? Number(w.startS || 0),
|
||||
endS: Number(w.endS || 0),
|
||||
words: [
|
||||
{
|
||||
text: joined,
|
||||
startS: Number(w.startS || 0),
|
||||
endS: Number(w.endS || 0),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
buf = []
|
||||
start = null
|
||||
continue
|
||||
}
|
||||
|
||||
if (!clean(textNoTag)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const wordItem = {
|
||||
text: textNoTag,
|
||||
startS: Number(w.startS || 0),
|
||||
endS: Number(w.endS || 0),
|
||||
}
|
||||
|
||||
buf.push(wordItem)
|
||||
|
||||
const eolByNewline = /\n/.test(original)
|
||||
const eolByPunct = isSentenceEnd(textNoTag)
|
||||
const isLast = i === alignedWords.length - 1
|
||||
|
||||
if (buf.length && (eolByNewline || eolByPunct || isLast)) {
|
||||
pushLine(buf, Number(w.endS || 0))
|
||||
buf = []
|
||||
start = null
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const clamp01 = (value) => Math.max(0, Math.min(1, value))
|
||||
|
||||
const alphaWhite = (alpha) => `rgba(255,255,255,${alpha})`
|
||||
|
||||
export default function KaraokeLyrics({
|
||||
alignedWords = [],
|
||||
currentTimeS = 0,
|
||||
showContext = true,
|
||||
removeTags = true,
|
||||
mode = 'focus',
|
||||
teleprompterLines = 6,
|
||||
teleprompterFontSize = 18,
|
||||
teleprompterLineHeight = 28,
|
||||
teleprompterAnchorLine = 1,
|
||||
}) {
|
||||
const lines = useMemo(
|
||||
() => groupAlignedWordsToLines(alignedWords, { removeTags }),
|
||||
[alignedWords, removeTags]
|
||||
)
|
||||
|
||||
const teleprompterData = useMemo(
|
||||
() => groupAlignedWordsToLineItems(alignedWords, { removeTags }),
|
||||
[alignedWords, removeTags]
|
||||
)
|
||||
|
||||
const activeLines = mode === 'teleprompter' ? teleprompterData : lines
|
||||
|
||||
const currentLineIdx = useMemo(() => {
|
||||
if (!lines || lines.length === 0) return -1
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const L = lines[i]
|
||||
if (!activeLines || activeLines.length === 0) return -1
|
||||
for (let i = 0; i < activeLines.length; i++) {
|
||||
const L = activeLines[i]
|
||||
if (currentTimeS >= (L.startS || 0) && currentTimeS <= (L.endS || 0)) return i
|
||||
}
|
||||
if (currentTimeS > (lines[lines.length - 1]?.endS || 0)) return lines.length - 1
|
||||
if (currentTimeS > (activeLines[activeLines.length - 1]?.endS || 0))
|
||||
return activeLines.length - 1
|
||||
return -1
|
||||
}, [lines, currentTimeS])
|
||||
}, [activeLines, currentTimeS])
|
||||
|
||||
if (!lines.length) return null
|
||||
if (!activeLines.length) return null
|
||||
|
||||
if (mode === 'teleprompter') {
|
||||
const safeLines = Math.max(1, teleprompterLines)
|
||||
const containerHeight = teleprompterLineHeight * safeLines
|
||||
const anchorLine = Math.max(0, Math.min(safeLines - 1, teleprompterAnchorLine))
|
||||
const anchorOffset = teleprompterLineHeight * anchorLine
|
||||
const safeIdx = Math.max(0, currentLineIdx)
|
||||
const translateY = anchorOffset - safeIdx * teleprompterLineHeight
|
||||
const inactiveAlpha = 0.35
|
||||
const completedAlpha = 0.75
|
||||
const activeAlpha = 1
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: containerHeight,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
transform: [{ translateY }],
|
||||
}}
|
||||
>
|
||||
{activeLines.map((line, idx) => {
|
||||
const isUpcoming = currentTimeS < (line.startS || 0)
|
||||
const isPast = currentTimeS > (line.endS || 0)
|
||||
const lineKey = `${line.startS}-${idx}`
|
||||
|
||||
if (isUpcoming) {
|
||||
return (
|
||||
<Text
|
||||
key={lineKey}
|
||||
style={{
|
||||
color: alphaWhite(inactiveAlpha),
|
||||
fontSize: teleprompterFontSize,
|
||||
lineHeight: teleprompterLineHeight,
|
||||
textAlign: 'center',
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
{line.text}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (isPast) {
|
||||
return (
|
||||
<Text
|
||||
key={lineKey}
|
||||
style={{
|
||||
color: alphaWhite(completedAlpha),
|
||||
fontSize: teleprompterFontSize,
|
||||
lineHeight: teleprompterLineHeight,
|
||||
textAlign: 'center',
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
{line.text}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
key={lineKey}
|
||||
style={{
|
||||
fontSize: teleprompterFontSize,
|
||||
lineHeight: teleprompterLineHeight,
|
||||
textAlign: 'center',
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
{line.words.map((word, wordIndex) => {
|
||||
const duration = Math.max(0.001, (word.endS || 0) - (word.startS || 0))
|
||||
const progress = clamp01((currentTimeS - (word.startS || 0)) / duration)
|
||||
const alpha = inactiveAlpha + (activeAlpha - inactiveAlpha) * progress
|
||||
const text = word.text
|
||||
return (
|
||||
<Text key={`${lineKey}-${wordIndex}`} style={{ color: alphaWhite(alpha) }}>
|
||||
{text}
|
||||
{wordIndex < line.words.length - 1 ? ' ' : ''}
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const prev = showContext && currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ''
|
||||
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : lines[0]?.text
|
||||
|
||||
@@ -1090,7 +1090,7 @@ const RecordPlayback = ({ route }) => {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: bottom + 200,
|
||||
top: top + 90,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
@@ -1099,6 +1099,9 @@ const RecordPlayback = ({ route }) => {
|
||||
<KaraokeLyrics
|
||||
alignedWords={alignedWords}
|
||||
currentTimeS={(progressInfo?.pos || 0) / 1000}
|
||||
mode="teleprompter"
|
||||
teleprompterLines={6}
|
||||
teleprompterAnchorLine={1}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
|
||||
@@ -999,13 +999,19 @@ const RecordPlayback = ({ route }) => {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: bottom + 180,
|
||||
top: top + 90,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<CreateLyricsHeader>
|
||||
{isRecording && alignedWords.length > 0 ? (
|
||||
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={pos} />
|
||||
<KaraokeLyrics
|
||||
alignedWords={alignedWords}
|
||||
currentTimeS={pos}
|
||||
mode="teleprompter"
|
||||
teleprompterLines={6}
|
||||
teleprompterAnchorLine={1}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user