UNPKG

@omnimedia/omnitool

Version:

open source video processing tools

485 lines (417 loc) 13.2 kB
import {hex} from "@e280/stz" import {TextStyleOptions} from "pixi.js" import {Media} from "../parts/media.js" import {Id, TimelineFile} from "../parts/basics.js" import {FilterAction, FilterActions} from "../parts/filters.js" import {filters, FilterParams, FilterType} from "../parts/filters.js" import {Transcription} from "../../features/speech/transcribe/types.js" import {Crop, FilterableItem, Item, ItemBase, Kind, VisualAnimatableItem} from "../parts/item.js" import {animationPresets, makeAnimationPresets, visualAnimations} from "../parts/animations/registry.js" import {TransitionAction, TransitionActions, transitions, TransitionName} from "../parts/transitions.js" import {AnimationPreset, PresetAnimateAction, PresetAnimateActions, PresetAnimation, PresetOptions} from "../parts/animations/types.js" import {CaptionAction, CaptionActions, captionDuration, CaptionOptions, CaptionPreset, captionPresets, CaptionSourceItem} from "../parts/captions.js" import {Anim, AnimateAction, Interpolation, Keyframes, ScalarAnimation, TrackTransform, Transform, TransformAnimation, TransformOptions, Vec2, VisualAnimationInput, VisualAnimationValue, VisualAnimations} from "../types.js" type VisualAnimateActions = { [TKey in keyof VisualAnimations]-?: AnimateAction<TKey> } type ContainerInput = [label: string, ...items: Item.Any[]] | Item.Any[] export class O { constructor(public state: {timeline: TimelineFile}) {} require<T extends Item.Any>(id: Id | undefined) { if (id === undefined) return undefined const item = this.state.timeline.items.find(item => item.id === id) return item as T | undefined } get timeline() { return this.state.timeline } getId() { return hex.toInteger(hex.random()) } #mutate(fn: (project: TimelineFile) => TimelineFile) { this.state.timeline = fn(this.state.timeline) } register(item: Item.Any) { this.#mutate(state => ({ ...state, items: [...state.items, item] })) } textStyle = (style: TextStyleOptions): Item.TextStyle => { const item = { id: this.getId(), kind: Kind.TextStyle, style, } as Item.TextStyle this.register(item) return item } spatial = (transform?: Transform, crop?: Crop): Item.Spatial => { const item: Item.Spatial = { id: this.getId(), kind: Kind.Spatial, transform: transform ?? this.transform(), crop, } this.register(item) return item } #registerAnimation = (anims: VisualAnimations): Item.Animation => { const item: Item.Animation = { id: this.getId(), kind: Kind.Animation, anims, } this.register(item) return item } #attachAnimation = <T extends VisualAnimatableItem>(item: T, animation: Item.Animation): T => { const next = { ...item, animationIds: [...(item.animationIds ?? []), animation.id] } this.set<T>(item.id, next as Partial<T>) return next } anim = { scalar: (terp: Interpolation, track: Keyframes): Anim<Keyframes> => ({terp, track}), vec2: (terp: Interpolation, source: Keyframes<Vec2>) => { const track = {x: [] as Keyframes, y: [] as Keyframes} for (const [time, [x, y]] of source) { track.x.push([time, x]) track.y.push([time, y]) } return {terp, track} }, transform: (terp: Interpolation, source: Keyframes<Transform>): Anim<TrackTransform> => { const track: TrackTransform = { 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 = <TFilter extends FilterType>(type: TFilter) => { const make = (params?: FilterParams<TFilter>) => { const item: Item.Filter<TFilter> = { id: this.getId(), kind: Kind.Filter, type, params, } this.register(item) return item } const action = (<T extends FilterableItem>(item: T, params?: FilterParams<TFilter>): T => { const filter = make(params) const next = { ...item, filterIds: [...(item.filterIds ?? []), filter.id] } this.set<T>(item.id, next as Partial<T>) return next }) as FilterAction<TFilter> action.make = make return action } #makeFilters = (): FilterActions => { const entries = Object.entries(filters) .map(([name, filter]) => [name, this.#makeFilter(filter.type)]) return Object.fromEntries(entries) as FilterActions } filter = this.#makeFilters() #makeAnimationValue = <TKey extends keyof VisualAnimations>( key: TKey, terp: Interpolation, track: VisualAnimationInput<TKey> ): VisualAnimationValue<TKey> => ( key === "transform" ? this.anim.transform(terp, track as Keyframes<Transform>) : this.anim.scalar(terp, track as Keyframes) ) as VisualAnimationValue<TKey> #makeAnimate = <TKey extends keyof VisualAnimations>(key: TKey): AnimateAction<TKey> => { const make = (terp: Interpolation, track: VisualAnimationInput<TKey>) => this.#registerAnimation({ [key]: this.#makeAnimationValue(key, terp, track) } as Pick<VisualAnimations, TKey>) const action = (<T extends VisualAnimatableItem>( item: T, terp: Interpolation, track: VisualAnimationInput<TKey> ): T => { const animation = make(terp, track) return this.#attachAnimation(item, animation) }) as AnimateAction<TKey> action.make = make return action } #makePresetAnimate = <TKey extends AnimationPreset>(key: TKey): PresetAnimateAction => { const make = (options?: PresetOptions) => { const preset = animationPresets[key] const anim = this.#animationPresets[key](options as never) as PresetAnimation return this.#registerAnimation( preset.type === "motion" ? {transform: anim as TransformAnimation} : {opacity: anim as ScalarAnimation} ) } const action = (<T extends VisualAnimatableItem>(item: T, options?: PresetOptions): T => { const animation = make(options) return this.#attachAnimation(item, animation) }) as PresetAnimateAction action.make = make return action } #makeAnimateActions = (): VisualAnimateActions => { const entries = Object.keys(visualAnimations) .map(key => [key, this.#makeAnimate(key as keyof VisualAnimations)]) return Object.fromEntries(entries) as VisualAnimateActions } #makePresetAnimateActions = (): PresetAnimateActions => { const entries = Object.keys(animationPresets) .map(key => [key, this.#makePresetAnimate(key as AnimationPreset)]) return Object.fromEntries(entries) as PresetAnimateActions } animate = { ...this.#makeAnimateActions(), presets: this.#makePresetAnimateActions(), } sequence = (...input: ContainerInput): Item.Sequence => { const [first, ...rest] = input const label = typeof first === "string" ? first : undefined const items = (label ? rest : input) as Item.Any[] const item = { id: this.getId(), kind: Kind.Sequence, label, childrenIds: items.map(item => item.id) } as Item.Sequence this.register(item) return item } stack = (...input: ContainerInput): Item.Stack => { const [first, ...rest] = input const label = typeof first === "string" ? first : undefined const items = (label ? rest : input) as Item.Any[] const item = { kind: Kind.Stack, id: this.getId(), label, childrenIds: items.map(item => item.id) } as Item.Stack this.register(item) return item } video = ( media: Media, options?: { start?: number, duration?: number label?: string enabled?: boolean }): Item.Video => { if(!media.hasVideo) throw new Error(`Video clip error: media "${media.datafile.filename}" has no video track.`) const item: Item.Video = { 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: Media, options?: { duration?: number label?: string enabled?: boolean }): Item.Image => { if(!media.isImage) throw new Error(`Image error: media "${media.datafile.filename}" is not an image.`) const item: Item.Image = { 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: Media, options?: { start?: number, duration?: number, gain?: number label?: string enabled?: boolean }): Item.Audio => { if(!media.hasAudio) throw new Error(`Audio clip error: media "${media.datafile.filename}" has no audio track.`) const item: Item.Audio = { 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: string, options?: { duration?: number, styles?: TextStyleOptions label?: string enabled?: boolean }): Item.Text => { const item = { id: this.getId(), label: options?.label, enabled: options?.enabled, content, kind: Kind.Text, duration: options?.duration ?? 2000 } as Item.Text if(options?.styles) item.styleId = this.textStyle(options.styles).id this.register(item) return item } #makeCaption = ( transcript: Transcription, preset: CaptionPreset, options?: CaptionOptions, ): Item.Caption => { const start = options?.start ?? 0 const duration = options?.duration ?? Math.max(0, captionDuration(transcript, options) - start) const item: Item.Caption = { 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: CaptionPreset): CaptionAction => { const make = (transcript: Transcription, options?: CaptionOptions) => this.#makeCaption(transcript, preset, options) const action = ((item: CaptionSourceItem, transcript: Transcription, options?: CaptionOptions): Item.Stack => { const caption = make(transcript, { ...options, itemId: item.id, start: options?.start ?? item.start, duration: options?.duration ?? item.duration, }) return this.stack(caption, item) }) as CaptionAction action.make = make return action } #makeCaptionPresetActions = () => { const entries = Object.entries(captionPresets) .map(([name, preset]) => [name, this.#makeCaptionAction(preset)]) return Object.fromEntries(entries) as CaptionActions["presets"] } captions = Object.assign( this.#makeCaptionAction(captionPresets.default), {presets: this.#makeCaptionPresetActions()} ) as CaptionActions gap = (duration: number, options?: ItemBase): Item.Gap => { const item = { id: this.getId(), kind: Kind.Gap, label: options?.label, enabled: options?.enabled, duration } as Item.Gap this.register(item) return item } #makeTransition = (key: TransitionName): TransitionAction => { return (duration: number, options?: ItemBase): Item.Transition => { const item: Item.Transition = { id: this.getId(), kind: Kind.Transition, label: options?.label, enabled: options?.enabled, name: transitions[key].name, duration, } this.register(item) return item } } #makeTransitions = (): TransitionActions => { const entries = Object.keys(transitions) .map(key => [key, this.#makeTransition(key as TransitionName)]) return Object.fromEntries(entries) as TransitionActions } transition = this.#makeTransitions() transform = (options?: TransformOptions): Transform => { const position: Vec2 = [ options?.position?.[0] ?? 0, options?.position?.[1] ?? 0 ] const scale: Vec2 = [ options?.scale?.[0] ?? 1, options?.scale?.[1] ?? 1 ] const rotation = options?.rotation ?? 0 return [position, scale, rotation] } addChildren(parent: Item.Stack | Item.Sequence, ...items: Item.Any[]) { this.#mutate(state => { const parentItem = state.items.find(({id}) => id === parent.id) as Item.Stack parentItem.childrenIds.push(...items.map(item => item.id)) return state }) } set = <K extends Item.Any>( id: Id, partial: Partial<K> ) => { this.#mutate(project => ({ ...project, items: project.items.map(item => item.id === id ? { ...item, ...partial } : item ) })) } }