init
This commit is contained in:
commit
c6863f2bfc
34 files changed
+6909
No files matched your search
@@ -0,0 +1,135 @@
|
||||
import type { Scene } from '@/types/scene'
|
||||
import { drawSceneFrame, loadSceneImages } from './render'
|
||||
|
||||
function effectiveEnd(scene: Scene): number {
|
||||
let max = 0
|
||||
for (const layer of scene.layers) {
|
||||
for (const kf of layer.keyframes) {
|
||||
if (kf.time > max) max = kf.time
|
||||
}
|
||||
}
|
||||
if (max <= 0) return scene.duration
|
||||
return Math.min(max, scene.duration)
|
||||
}
|
||||
|
||||
export interface VideoExportResult {
|
||||
blob: Blob
|
||||
mimeType: string
|
||||
extension: string
|
||||
}
|
||||
|
||||
export interface VideoExportOptions {
|
||||
onProgress?: (progress: number) => void
|
||||
signal?: AbortSignal
|
||||
bitsPerSecond?: number
|
||||
}
|
||||
|
||||
const MIME_CANDIDATES = [
|
||||
'video/webm;codecs=vp9',
|
||||
'video/webm;codecs=vp8',
|
||||
'video/webm',
|
||||
'video/mp4;codecs=h264',
|
||||
'video/mp4',
|
||||
]
|
||||
|
||||
export function pickMimeType(): string | null {
|
||||
if (typeof MediaRecorder === 'undefined') return null
|
||||
for (const mime of MIME_CANDIDATES) {
|
||||
if (MediaRecorder.isTypeSupported?.(mime)) return mime
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function renderSceneToVideo(
|
||||
scene: Scene,
|
||||
opts: VideoExportOptions = {},
|
||||
): Promise<VideoExportResult> {
|
||||
const mimeType = pickMimeType()
|
||||
if (!mimeType) {
|
||||
throw new Error('MediaRecorder wird von diesem Browser nicht unterstützt.')
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = scene.canvas.width
|
||||
canvas.height = scene.canvas.height
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('2D context unavailable')
|
||||
|
||||
const images = await loadSceneImages(scene)
|
||||
|
||||
drawSceneFrame(ctx, scene, 0, images)
|
||||
|
||||
const stream = canvas.captureStream(scene.fps)
|
||||
const recorder = new MediaRecorder(stream, {
|
||||
mimeType,
|
||||
videoBitsPerSecond: opts.bitsPerSecond ?? 8_000_000,
|
||||
})
|
||||
|
||||
const chunks: Blob[] = []
|
||||
recorder.ondataavailable = (e) => {
|
||||
if (e.data && e.data.size > 0) chunks.push(e.data)
|
||||
}
|
||||
|
||||
const stopped = new Promise<void>((resolve) => {
|
||||
recorder.onstop = () => resolve()
|
||||
})
|
||||
|
||||
recorder.start()
|
||||
const startedAt = performance.now()
|
||||
const renderDuration = effectiveEnd(scene)
|
||||
|
||||
const finished = new Promise<void>((resolve, reject) => {
|
||||
let raf = 0
|
||||
const step = () => {
|
||||
if (opts.signal?.aborted) {
|
||||
cancelAnimationFrame(raf)
|
||||
try {
|
||||
recorder.stop()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
stream.getTracks().forEach((t) => t.stop())
|
||||
reject(new DOMException('aborted', 'AbortError'))
|
||||
return
|
||||
}
|
||||
const elapsed = (performance.now() - startedAt) / 1000
|
||||
const t = Math.min(elapsed, renderDuration)
|
||||
drawSceneFrame(ctx, scene, t, images)
|
||||
opts.onProgress?.(renderDuration > 0 ? Math.min(1, t / renderDuration) : 1)
|
||||
if (elapsed >= renderDuration) {
|
||||
try {
|
||||
recorder.stop()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
stream.getTracks().forEach((t) => t.stop())
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
raf = requestAnimationFrame(step)
|
||||
}
|
||||
raf = requestAnimationFrame(step)
|
||||
})
|
||||
|
||||
await finished
|
||||
await stopped
|
||||
|
||||
const blob = new Blob(chunks, { type: mimeType })
|
||||
const extension = mimeType.includes('mp4') ? 'mp4' : 'webm'
|
||||
return { blob, mimeType, extension }
|
||||
}
|
||||
|
||||
export async function downloadSceneAsVideo(
|
||||
scene: Scene,
|
||||
opts: VideoExportOptions = {},
|
||||
): Promise<void> {
|
||||
const { blob, extension } = await renderSceneToVideo(scene, opts)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `animation.${extension}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
}
|
||||
Reference in new issue
Block a user