92 lines
2.5 KiB
TypeScript
92 lines
2.5 KiB
TypeScript
import type { Scene } from '@/types/scene'
|
|
|
|
const FILE_VERSION = 1
|
|
const MAGIC = 'animatr-project'
|
|
|
|
export interface ProjectFile {
|
|
magic: typeof MAGIC
|
|
fileVersion: number
|
|
savedAt: string
|
|
scene: Scene
|
|
}
|
|
|
|
export function serializeProject(scene: Scene): string {
|
|
const payload: ProjectFile = {
|
|
magic: MAGIC,
|
|
fileVersion: FILE_VERSION,
|
|
savedAt: new Date().toISOString(),
|
|
scene,
|
|
}
|
|
return JSON.stringify(payload, null, 2)
|
|
}
|
|
|
|
export class ProjectFileError extends Error {
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = 'ProjectFileError'
|
|
}
|
|
}
|
|
|
|
export function parseProject(text: string): Scene {
|
|
let data: unknown
|
|
try {
|
|
data = JSON.parse(text)
|
|
} catch {
|
|
throw new ProjectFileError('File is not valid JSON.')
|
|
}
|
|
if (!data || typeof data !== 'object') {
|
|
throw new ProjectFileError('File is empty or not an object.')
|
|
}
|
|
const obj = data as Record<string, unknown>
|
|
if (obj.magic !== MAGIC) {
|
|
throw new ProjectFileError('Not an Animatr project file (magic missing).')
|
|
}
|
|
const scene = obj.scene
|
|
if (!scene || typeof scene !== 'object') {
|
|
throw new ProjectFileError('Project file has no scene.')
|
|
}
|
|
const s = scene as Record<string, unknown>
|
|
if (!s.canvas || typeof s.canvas !== 'object') {
|
|
throw new ProjectFileError('Scene data incomplete: canvas missing.')
|
|
}
|
|
if (!Array.isArray(s.layers)) {
|
|
throw new ProjectFileError('Scene data incomplete: layers missing.')
|
|
}
|
|
if (!Array.isArray(s.assets)) {
|
|
throw new ProjectFileError('Scene data incomplete: assets missing.')
|
|
}
|
|
return scene as Scene
|
|
}
|
|
|
|
export function downloadProject(scene: Scene, filename = 'projekt.animatr.json') {
|
|
const source = serializeProject(scene)
|
|
const blob = new Blob([source], { type: 'application/json' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = filename
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
}
|
|
|
|
export function pickProjectFile(): Promise<File | null> {
|
|
return new Promise((resolve) => {
|
|
const input = document.createElement('input')
|
|
input.type = 'file'
|
|
input.accept = '.json,application/json'
|
|
input.onchange = () => {
|
|
const f = input.files?.[0]
|
|
resolve(f ?? null)
|
|
}
|
|
input.oncancel = () => resolve(null)
|
|
input.click()
|
|
})
|
|
}
|
|
|
|
export async function readProjectFile(file: File): Promise<Scene> {
|
|
const text = await file.text()
|
|
return parseProject(text)
|
|
}
|