threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PopmotionPlugin.ts 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import type {Driver} from 'popmotion/lib/animations/types'
  2. import {now} from 'ts-browser-helpers'
  3. import {animate, type AnimationOptions} from 'popmotion'
  4. import {AViewerPluginSync, ThreeViewer} from '../../viewer'
  5. import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin'
  6. import type {ProgressivePlugin} from '../pipeline/ProgressivePlugin'
  7. import {generateUUID} from '../../three'
  8. import {animateCameraToViewLinear, animateCameraToViewSpherical, EasingFunctions, makeSetterFor} from '../../utils'
  9. import {ICamera, ICameraView} from '../../core'
  10. export interface AnimationResult{
  11. id: string
  12. promise: Promise<string>
  13. options: AnimationOptions<any>
  14. stop: () => void
  15. // eslint-disable-next-line @typescript-eslint/naming-convention
  16. _stop?: () => void
  17. targetRef?: {target: any, key: string}
  18. }
  19. /**
  20. * Popmotion plugin
  21. *
  22. * Provides animation capabilities to the viewer using the popmotion library: https://popmotion.io/
  23. *
  24. * Overrides the driver in popmotion to sync with the viewer and provide ways to keep track and stop animations.
  25. *
  26. * @category Plugin
  27. */
  28. export class PopmotionPlugin extends AViewerPluginSync<''> {
  29. public static readonly PluginType = 'PopmotionPlugin'
  30. enabled = true
  31. toJSON: any = undefined // disable serialization
  32. fromJSON: any = undefined // disable serialization
  33. constructor(enabled = true) {
  34. super()
  35. this.enabled = enabled
  36. this._postFrame = this._postFrame.bind(this)
  37. }
  38. // private _animating = false
  39. private _lastFrameTime = 0 // for post frame
  40. private _updaters: {u: ((timestamp: number) => void), time: number}[] = []
  41. dependencies = []
  42. private _fadeDisabled = false
  43. /**
  44. * Disable the frame fade plugin while animation is running
  45. */
  46. disableFrameFade = true
  47. // Same code used in CameraViewPlugin
  48. private _postFrame = ()=>{
  49. if (!this._viewer) return
  50. if (this.isDisabled() || Object.keys(this.animations).length < 1) {
  51. this._lastFrameTime = 0
  52. // console.log('not anim')
  53. if (this._fadeDisabled) {
  54. this._viewer.getPlugin<FrameFadePlugin>('FrameFade')?.enable(this)
  55. this._fadeDisabled = false
  56. }
  57. return
  58. }
  59. const time = now() / 1000.0
  60. if (this._lastFrameTime < 1) this._lastFrameTime = time - 1.0 / 60.0
  61. let delta = time - this._lastFrameTime
  62. this._lastFrameTime = time
  63. // todo: scrolling
  64. // delta = delta * (this.animateOnScroll ? this._scrollAnimationState : 1)
  65. const d = this._viewer.getPlugin<ProgressivePlugin>('Progressive')?.postFrameConvergedRecordingDelta()
  66. if (d && d > 0) delta = d
  67. if (d === 0) return // not converged yet.
  68. // if d < 0: not recording, do nothing
  69. delta *= 1000
  70. // delta = 16.666 // testing
  71. if (delta <= 0.001) return
  72. this._updaters.forEach(u=>{
  73. let dt = delta
  74. if (u.time + dt < 0) dt = -u.time
  75. u.time += dt
  76. if (Math.abs(dt) > 0.001)
  77. u.u(dt)
  78. })
  79. if (!this._fadeDisabled && this.disableFrameFade) {
  80. const ff = this._viewer.getPlugin<FrameFadePlugin>('FrameFade')
  81. if (ff) {
  82. ff.disable(this)
  83. this._fadeDisabled = true
  84. }
  85. }
  86. // todo: scrolling
  87. // if (this._scrollAnimationState < 0.001) this._scrollAnimationState = 0
  88. // else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping
  89. }
  90. readonly defaultDriver: Driver = (update)=>{
  91. return {
  92. start: ()=>this._updaters.push({u:update, time:0}),
  93. stop: ()=> this._updaters.splice(this._updaters.findIndex(u=>u.u === update), 1),
  94. }
  95. }
  96. onAdded(viewer: ThreeViewer): void {
  97. super.onAdded(viewer)
  98. viewer.addEventListener('postFrame', this._postFrame)
  99. }
  100. onRemove(viewer: ThreeViewer): void {
  101. viewer.removeEventListener('postFrame', this._postFrame)
  102. super.onRemove(viewer)
  103. }
  104. readonly animations: Record<string, AnimationResult> = {}
  105. animateTarget<T>(target: T, key: keyof T, options: AnimationOptions<T[keyof T]>): AnimationResult {
  106. return this.animate({...options, target, key: key as string})
  107. }
  108. animate<V>(options1: AnimationOptions<V> & {target?: any, key?: string}): AnimationResult {
  109. let targetRef = undefined
  110. const options = {...options1} as ((typeof options1) & {lastOnUpdate?: (a:V)=>void})
  111. if (options.target !== undefined) {
  112. if (options.key === undefined) throw new Error('key must be defined')
  113. if (!(options.key in options.target)) {
  114. console.warn('key not present in target, creating', options.key, options.target)
  115. options.target[options.key] = options.from || 0
  116. }
  117. const setter = makeSetterFor(options.target, options.key)
  118. const fromVal = options.target[options.key]
  119. options.lastOnUpdate = options.onUpdate
  120. options.onUpdate = (val: V)=>{
  121. setter(val)
  122. options.lastOnUpdate && options.lastOnUpdate(val)
  123. }
  124. targetRef = {target: options.target, key: options.key}
  125. if (options.from === undefined) options.from = fromVal
  126. delete options.target
  127. delete options.key
  128. }
  129. const uuid = generateUUID()
  130. const a: AnimationResult = {
  131. id: uuid,
  132. options,
  133. stop: ()=>{
  134. if (!this.animations[uuid]?._stop) console.warn('Animation not started')
  135. else this.animations[uuid]?._stop?.()
  136. },
  137. promise: undefined as any,
  138. targetRef,
  139. }
  140. this.animations[uuid] = a
  141. a.promise = new Promise<void>((resolve, reject) => {
  142. const end2 = ()=>{
  143. try {
  144. options.onEnd && options.onEnd()
  145. } catch (e: any) {
  146. reject(e)
  147. return false
  148. }
  149. return true
  150. }
  151. const opts: AnimationOptions<V> = {
  152. driver: this.defaultDriver,
  153. ...options,
  154. onComplete: ()=>{
  155. try {
  156. options.onComplete && options.onComplete()
  157. } catch (e: any) {
  158. if (!end2()) return
  159. reject(e)
  160. return
  161. }
  162. if (!end2()) return
  163. resolve()
  164. },
  165. onStop: ()=>{
  166. try {
  167. options.onStop && options.onStop()
  168. } catch (e: any) {
  169. if (!end2()) return
  170. reject(e)
  171. return
  172. }
  173. resolve()
  174. },
  175. }
  176. // todo: support boolean using timeout.
  177. const anim = animate(opts)
  178. this.animations[uuid]._stop = anim.stop
  179. this.animations[uuid].options = opts
  180. }).then(()=>{
  181. delete this.animations[uuid]
  182. return uuid
  183. })
  184. return this.animations[uuid]
  185. }
  186. async animateAsync<V>(options: AnimationOptions<V>& {target?: any, key?: string}, animations?: AnimationResult[]): Promise<string> {
  187. const anim = this.animate(options)
  188. if (animations) animations.push(anim)
  189. return anim.promise
  190. }
  191. async animateTargetAsync<T>(target: T, key: keyof T, options: AnimationOptions<T[keyof T]>, animations?: AnimationResult[]): Promise<string> {
  192. const anim = this.animate({...options, target, key: key as string})
  193. if (animations) animations.push(anim)
  194. return anim.promise
  195. }
  196. animateCamera(camera: ICamera, view: ICameraView, spherical = true, options?: Partial<AnimationOptions<any>>) {
  197. const anim = spherical ?
  198. animateCameraToViewSpherical(camera, view) :
  199. animateCameraToViewLinear(camera, view)
  200. return this.animate({
  201. ease: EasingFunctions.linear,
  202. duration: 1000,
  203. ...anim, ...options,
  204. })
  205. }
  206. async animateCameraAsync(camera: ICamera, view: ICameraView, spherical = true, options?: Partial<AnimationOptions<any>>, animations?: AnimationResult[]) {
  207. const anim = this.animateCamera(camera, view, spherical, options)
  208. if (animations) animations.push(anim)
  209. return anim.promise
  210. }
  211. }