|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- import type {Driver} from 'popmotion/lib/animations/types'
- import {now} from 'ts-browser-helpers'
- import {animate, type AnimationOptions} from 'popmotion'
- import {AViewerPluginSync, ThreeViewer} from '../../viewer'
- import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin'
- import type {ProgressivePlugin} from '../pipeline/ProgressivePlugin'
- import {generateUUID} from '../../three'
- import {animateCameraToViewLinear, animateCameraToViewSpherical, EasingFunctions, makeSetterFor} from '../../utils'
- import {ICamera, ICameraView} from '../../core'
-
- export interface AnimationResult{
- id: string
- promise: Promise<string>
- options: AnimationOptions<any>
- stop: () => void
- // eslint-disable-next-line @typescript-eslint/naming-convention
- _stop?: () => void
-
- targetRef?: {target: any, key: string}
- }
-
- /**
- * Popmotion plugin
- *
- * Provides animation capabilities to the viewer using the popmotion library: https://popmotion.io/
- *
- * Overrides the driver in popmotion to sync with the viewer and provide ways to keep track and stop animations.
- *
- * @category Plugin
- */
- export class PopmotionPlugin extends AViewerPluginSync<''> {
- public static readonly PluginType = 'PopmotionPlugin'
- enabled = true
-
- toJSON: any = undefined // disable serialization
- fromJSON: any = undefined // disable serialization
-
- constructor(enabled = true) {
- super()
- this.enabled = enabled
- this._postFrame = this._postFrame.bind(this)
- }
-
- // private _animating = false
- private _lastFrameTime = 0 // for post frame
- private _updaters: {u: ((timestamp: number) => void), time: number}[] = []
-
- dependencies = []
-
- private _fadeDisabled = false
-
- /**
- * Disable the frame fade plugin while animation is running
- */
- disableFrameFade = true
-
- // Same code used in CameraViewPlugin
- private _postFrame = ()=>{
- if (!this._viewer) return
- if (this.isDisabled() || Object.keys(this.animations).length < 1) {
- this._lastFrameTime = 0
- // console.log('not anim')
- if (this._fadeDisabled) {
- this._viewer.getPlugin<FrameFadePlugin>('FrameFade')?.enable(this)
- this._fadeDisabled = false
- }
- return
- }
- const time = now() / 1000.0
- if (this._lastFrameTime < 1) this._lastFrameTime = time - 1.0 / 60.0
- let delta = time - this._lastFrameTime
- this._lastFrameTime = time
-
- // todo: scrolling
- // delta = delta * (this.animateOnScroll ? this._scrollAnimationState : 1)
-
- const d = this._viewer.getPlugin<ProgressivePlugin>('Progressive')?.postFrameConvergedRecordingDelta()
- if (d && d > 0) delta = d
- if (d === 0) return // not converged yet.
- // if d < 0: not recording, do nothing
-
- delta *= 1000
-
- // delta = 16.666 // testing
-
- if (delta <= 0.001) return
-
- this._updaters.forEach(u=>{
- let dt = delta
- if (u.time + dt < 0) dt = -u.time
- u.time += dt
- if (Math.abs(dt) > 0.001)
- u.u(dt)
- })
-
- if (!this._fadeDisabled && this.disableFrameFade) {
- const ff = this._viewer.getPlugin<FrameFadePlugin>('FrameFade')
- if (ff) {
- ff.disable(this)
- this._fadeDisabled = true
- }
- }
-
- // todo: scrolling
- // if (this._scrollAnimationState < 0.001) this._scrollAnimationState = 0
- // else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping
- }
-
- readonly defaultDriver: Driver = (update)=>{
- return {
- start: ()=>this._updaters.push({u:update, time:0}),
- stop: ()=> this._updaters.splice(this._updaters.findIndex(u=>u.u === update), 1),
- }
- }
-
- onAdded(viewer: ThreeViewer): void {
- super.onAdded(viewer)
- viewer.addEventListener('postFrame', this._postFrame)
- }
-
- onRemove(viewer: ThreeViewer): void {
- viewer.removeEventListener('postFrame', this._postFrame)
- super.onRemove(viewer)
- }
-
- readonly animations: Record<string, AnimationResult> = {}
-
- animateTarget<T>(target: T, key: keyof T, options: AnimationOptions<T[keyof T]>): AnimationResult {
- return this.animate({...options, target, key: key as string})
- }
-
- animate<V>(options1: AnimationOptions<V> & {target?: any, key?: string}): AnimationResult {
- let targetRef = undefined
- const options = {...options1} as ((typeof options1) & {lastOnUpdate?: (a:V)=>void})
- if (options.target !== undefined) {
- if (options.key === undefined) throw new Error('key must be defined')
- if (!(options.key in options.target)) {
- console.warn('key not present in target, creating', options.key, options.target)
- options.target[options.key] = options.from || 0
- }
- const setter = makeSetterFor(options.target, options.key)
- const fromVal = options.target[options.key]
- options.lastOnUpdate = options.onUpdate
- options.onUpdate = (val: V)=>{
- setter(val)
- options.lastOnUpdate && options.lastOnUpdate(val)
- }
- targetRef = {target: options.target, key: options.key}
- if (options.from === undefined) options.from = fromVal
- delete options.target
- delete options.key
- }
-
- const uuid = generateUUID()
- const a: AnimationResult = {
- id: uuid,
- options,
- stop: ()=>{
- if (!this.animations[uuid]?._stop) console.warn('Animation not started')
- else this.animations[uuid]?._stop?.()
- },
- promise: undefined as any,
- targetRef,
- }
- this.animations[uuid] = a
- a.promise = new Promise<void>((resolve, reject) => {
- const end2 = ()=>{
- try {
- options.onEnd && options.onEnd()
- } catch (e: any) {
- reject(e)
- return false
- }
- return true
- }
- const opts: AnimationOptions<V> = {
- driver: this.defaultDriver,
- ...options,
- onComplete: ()=>{
- try {
- options.onComplete && options.onComplete()
- } catch (e: any) {
- if (!end2()) return
- reject(e)
- return
- }
- if (!end2()) return
- resolve()
- },
- onStop: ()=>{
- try {
- options.onStop && options.onStop()
- } catch (e: any) {
- if (!end2()) return
- reject(e)
- return
- }
- resolve()
- },
- }
- // todo: support boolean using timeout.
- const anim = animate(opts)
- this.animations[uuid]._stop = anim.stop
- this.animations[uuid].options = opts
- }).then(()=>{
- delete this.animations[uuid]
- return uuid
- })
-
- return this.animations[uuid]
- }
-
- async animateAsync<V>(options: AnimationOptions<V>& {target?: any, key?: string}, animations?: AnimationResult[]): Promise<string> {
- const anim = this.animate(options)
- if (animations) animations.push(anim)
- return anim.promise
- }
-
- async animateTargetAsync<T>(target: T, key: keyof T, options: AnimationOptions<T[keyof T]>, animations?: AnimationResult[]): Promise<string> {
- const anim = this.animate({...options, target, key: key as string})
- if (animations) animations.push(anim)
- return anim.promise
- }
-
- animateCamera(camera: ICamera, view: ICameraView, spherical = true, options?: Partial<AnimationOptions<any>>) {
- const anim = spherical ?
- animateCameraToViewSpherical(camera, view) :
- animateCameraToViewLinear(camera, view)
- return this.animate({
- ease: EasingFunctions.linear,
- duration: 1000,
- ...anim, ...options,
- })
- }
- async animateCameraAsync(camera: ICamera, view: ICameraView, spherical = true, options?: Partial<AnimationOptions<any>>, animations?: AnimationResult[]) {
- const anim = this.animateCamera(camera, view, spherical, options)
- if (animations) animations.push(anim)
- return anim.promise
- }
- }
|