UNPKG

@omnimedia/omnitool

Version:

open source video processing tools

353 lines 11.9 kB
import { hex } from "@e280/stz"; import { filters } from "../parts/filters.js"; import { Kind } from "../parts/item.js"; import { animationPresets, makeAnimationPresets, visualAnimations } from "../parts/animations/registry.js"; import { transitions } from "../parts/transitions.js"; import { captionDuration, captionPresets } from "../parts/captions.js"; export class O { state; constructor(state) { this.state = state; } require(id) { if (id === undefined) return undefined; const item = this.state.timeline.items.find(item => item.id === id); return item; } get timeline() { return this.state.timeline; } getId() { return hex.toInteger(hex.random()); } #mutate(fn) { this.state.timeline = fn(this.state.timeline); } register(item) { this.#mutate(state => ({ ...state, items: [...state.items, item] })); } textStyle = (style) => { const item = { id: this.getId(), kind: Kind.TextStyle, style, }; this.register(item); return item; }; spatial = (transform, crop) => { const item = { id: this.getId(), kind: Kind.Spatial, transform: transform ?? this.transform(), crop, }; this.register(item); return item; }; #registerAnimation = (anims) => { const item = { id: this.getId(), kind: Kind.Animation, anims, }; this.register(item); return item; }; #attachAnimation = (item, animation) => { const next = { ...item, animationIds: [...(item.animationIds ?? []), animation.id] }; this.set(item.id, next); return next; }; anim = { scalar: (terp, track) => ({ terp, track }), vec2: (terp, source) => { const track = { x: [], y: [] }; for (const [time, [x, y]] of source) { track.x.push([time, x]); track.y.push([time, y]); } return { terp, track }; }, transform: (terp, source) => { const track = { position: { x: [], y: [] }, scale: { x: [], y: [] }, rotation: [], }; for (const [time, [position, scale, rotation]] of source) { track.position.x.push([time, position[0]]); track.position.y.push([time, position[1]]); track.scale.x.push([time, scale[0]]); track.scale.y.push([time, scale[1]]); track.rotation.push([time, rotation]); } return { terp, track }; }, }; #animationPresets = makeAnimationPresets((terp, track) => ({ terp, track }), (terp, source) => this.anim.transform(terp, source), options => this.transform(options)); #makeFilter = (type) => { const make = (params) => { const item = { id: this.getId(), kind: Kind.Filter, type, params, }; this.register(item); return item; }; const action = ((item, params) => { const filter = make(params); const next = { ...item, filterIds: [...(item.filterIds ?? []), filter.id] }; this.set(item.id, next); return next; }); action.make = make; return action; }; #makeFilters = () => { const entries = Object.entries(filters) .map(([name, filter]) => [name, this.#makeFilter(filter.type)]); return Object.fromEntries(entries); }; filter = this.#makeFilters(); #makeAnimationValue = (key, terp, track) => (key === "transform" ? this.anim.transform(terp, track) : this.anim.scalar(terp, track)); #makeAnimate = (key) => { const make = (terp, track) => this.#registerAnimation({ [key]: this.#makeAnimationValue(key, terp, track) }); const action = ((item, terp, track) => { const animation = make(terp, track); return this.#attachAnimation(item, animation); }); action.make = make; return action; }; #makePresetAnimate = (key) => { const make = (options) => { const preset = animationPresets[key]; const anim = this.#animationPresets[key](options); return this.#registerAnimation(preset.type === "motion" ? { transform: anim } : { opacity: anim }); }; const action = ((item, options) => { const animation = make(options); return this.#attachAnimation(item, animation); }); action.make = make; return action; }; #makeAnimateActions = () => { const entries = Object.keys(visualAnimations) .map(key => [key, this.#makeAnimate(key)]); return Object.fromEntries(entries); }; #makePresetAnimateActions = () => { const entries = Object.keys(animationPresets) .map(key => [key, this.#makePresetAnimate(key)]); return Object.fromEntries(entries); }; animate = { ...this.#makeAnimateActions(), presets: this.#makePresetAnimateActions(), }; sequence = (...input) => { const [first, ...rest] = input; const label = typeof first === "string" ? first : undefined; const items = (label ? rest : input); const item = { id: this.getId(), kind: Kind.Sequence, label, childrenIds: items.map(item => item.id) }; this.register(item); return item; }; stack = (...input) => { const [first, ...rest] = input; const label = typeof first === "string" ? first : undefined; const items = (label ? rest : input); const item = { kind: Kind.Stack, id: this.getId(), label, childrenIds: items.map(item => item.id) }; this.register(item); return item; }; video = (media, options) => { if (!media.hasVideo) throw new Error(`Video clip error: media "${media.datafile.filename}" has no video track.`); const item = { kind: Kind.Video, id: this.getId(), label: options?.label, enabled: options?.enabled, mediaHash: media.datafile.checksum.hash, start: options?.start ?? 0, duration: options?.duration ?? media.duration }; this.register(item); return item; }; image = (media, options) => { if (!media.isImage) throw new Error(`Image error: media "${media.datafile.filename}" is not an image.`); const item = { kind: Kind.Image, id: this.getId(), label: options?.label, enabled: options?.enabled, mediaHash: media.datafile.checksum.hash, duration: options?.duration ?? 2000 }; this.register(item); return item; }; audio = (media, options) => { if (!media.hasAudio) throw new Error(`Audio clip error: media "${media.datafile.filename}" has no audio track.`); const item = { kind: Kind.Audio, id: this.getId(), label: options?.label, enabled: options?.enabled, mediaHash: media.datafile.checksum.hash, start: options?.start ?? 0, duration: options?.duration ?? media.duration, gain: options?.gain ?? 1 }; this.register(item); return item; }; text = (content, options) => { const item = { id: this.getId(), label: options?.label, enabled: options?.enabled, content, kind: Kind.Text, duration: options?.duration ?? 2000 }; if (options?.styles) item.styleId = this.textStyle(options.styles).id; this.register(item); return item; }; #makeCaption = (transcript, preset, options) => { const start = options?.start ?? 0; const duration = options?.duration ?? Math.max(0, captionDuration(transcript, options) - start); const item = { id: this.getId(), kind: Kind.Caption, label: options?.label, enabled: options?.enabled, transcript, itemId: options?.itemId, start, duration, maxChars: options?.maxChars, maxDuration: options?.maxDuration, maxSilence: options?.maxSilence, }; item.styleId = this.textStyle(options?.styles ?? preset.styles).id; item.spatialId = this.spatial(this.transform(preset.transform)).id; this.register(item); return item; }; #makeCaptionAction = (preset) => { const make = (transcript, options) => this.#makeCaption(transcript, preset, options); const action = ((item, transcript, options) => { const caption = make(transcript, { ...options, itemId: item.id, start: options?.start ?? item.start, duration: options?.duration ?? item.duration, }); return this.stack(caption, item); }); action.make = make; return action; }; #makeCaptionPresetActions = () => { const entries = Object.entries(captionPresets) .map(([name, preset]) => [name, this.#makeCaptionAction(preset)]); return Object.fromEntries(entries); }; captions = Object.assign(this.#makeCaptionAction(captionPresets.default), { presets: this.#makeCaptionPresetActions() }); gap = (duration, options) => { const item = { id: this.getId(), kind: Kind.Gap, label: options?.label, enabled: options?.enabled, duration }; this.register(item); return item; }; #makeTransition = (key) => { return (duration, options) => { const item = { id: this.getId(), kind: Kind.Transition, label: options?.label, enabled: options?.enabled, name: transitions[key].name, duration, }; this.register(item); return item; }; }; #makeTransitions = () => { const entries = Object.keys(transitions) .map(key => [key, this.#makeTransition(key)]); return Object.fromEntries(entries); }; transition = this.#makeTransitions(); transform = (options) => { const position = [ options?.position?.[0] ?? 0, options?.position?.[1] ?? 0 ]; const scale = [ options?.scale?.[0] ?? 1, options?.scale?.[1] ?? 1 ]; const rotation = options?.rotation ?? 0; return [position, scale, rotation]; }; addChildren(parent, ...items) { this.#mutate(state => { const parentItem = state.items.find(({ id }) => id === parent.id); parentItem.childrenIds.push(...items.map(item => item.id)); return state; }); } set = (id, partial) => { this.#mutate(project => ({ ...project, items: project.items.map(item => item.id === id ? { ...item, ...partial } : item) })); }; } //# sourceMappingURL=o.js.map