Files
tianshu-engine/src/components/Subtitles.vue
cocos02 319a379921 feat: P2 - QTE system, subtitles, save thumbnails
- QTESystem: trigger detection via timeupdate, multi-key matching, timeout handling
- QTEOverlay: SVG countdown ring + key prompts + success/fail animation
- Engine: integrate QTE (timeupdate check, conditional branching, effect application)
- Subtitles: WebVTT parsing + synchronized subtitle rendering
- GamePlayer: overlay QTE and subtitle components
- SaveSystem: DB v2 with thumbnail field, canvas snapshot at 320x180 JPEG
- SaveLoadMenu: thumbnail preview for save slots
- VideoManager: getActiveVideoElement() for canvas capture
- App.vue: QTE/subtitle integration, thumbnail capture on save
- stores: QTE state management, save list with thumbnails
- demo.json: QTE scene (right_door), subtitles, new event types
- ROADMAP: mark P2 as completed
2026-06-07 19:35:14 +08:00

116 lines
2.5 KiB
Vue

<script setup lang="ts">
import { ref, watch } from 'vue'
interface SubCue {
start: number
end: number
text: string
}
const props = defineProps<{
currentTime: number
videoUrl: string | null
}>()
const cues = ref<SubCue[]>([])
const currentText = ref('')
const loadedUrl = ref('')
watch(() => props.videoUrl, async (url) => {
if (!url || url === loadedUrl.value) return
loadedUrl.value = url
try {
const resp = await fetch(url)
const text = await resp.text()
cues.value = parseVTT(text)
} catch {
cues.value = []
}
}, { immediate: true })
watch(() => props.currentTime, (t) => {
if (cues.value.length === 0) {
currentText.value = ''
return
}
const active = cues.value.find((c) => t >= c.start && t <= c.end)
currentText.value = active?.text ?? ''
})
function parseVTT(raw: string): SubCue[] {
const result: SubCue[] = []
const lines = raw.split(/\r?\n/)
let i = 0
while (i < lines.length && !lines[i].includes('-->')) {
i++
}
for (; i < lines.length; i++) {
const line = lines[i].trim()
const timeMatch = line.match(
/(\d{1,2}:)?(\d{1,2}:)?(\d{1,2}[.,]\d{1,3})\s*-->\s*(\d{1,2}:)?(\d{1,2}:)?(\d{1,2}[.,]\d{1,3})/
)
if (!timeMatch) continue
const start = vttTimeToSeconds(timeMatch[0].split('-->')[0].trim())
const end = vttTimeToSeconds(timeMatch[0].split('-->')[1].trim())
let text = ''
i++
while (i < lines.length && lines[i].trim() !== '') {
text += (text ? '\n' : '') + lines[i].trim()
i++
}
if (text) {
result.push({ start, end, text })
}
}
return result
}
function vttTimeToSeconds(ts: string): number {
const parts = ts.replace(',', '.').split(':')
if (parts.length === 3) {
return parseFloat(parts[0]) * 3600 + parseFloat(parts[1]) * 60 + parseFloat(parts[2])
}
if (parts.length === 2) {
return parseFloat(parts[0]) * 60 + parseFloat(parts[1])
}
return parseFloat(parts[0])
}
</script>
<template>
<div class="subtitles" v-if="currentText">
<div class="sub-text">{{ currentText }}</div>
</div>
</template>
<style scoped>
.subtitles {
position: absolute;
bottom: 80px;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 5;
pointer-events: none;
}
.sub-text {
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 20px;
padding: 8px 20px;
border-radius: 4px;
max-width: 80%;
text-align: center;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
white-space: pre-line;
}
</style>