UNPKG

threepipe

Version:

A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.

231 lines 8.65 kB
import { now } from 'ts-browser-helpers'; import { animate } from 'popmotion'; import { AViewerPluginSync } from '../../viewer'; import { generateUUID } from '../../three'; import { animateCameraToViewLinear, animateCameraToViewSpherical, EasingFunctions, makeSetterFor } from '../../utils'; /** * 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 Plugins */ export class PopmotionPlugin extends AViewerPluginSync { constructor(enabled = true) { super(); this.enabled = true; this.toJSON = undefined; // disable serialization this.fromJSON = undefined; // disable serialization // private _animating = false this._lastFrameTime = 0; // for post frame this._updaters = []; this.dependencies = []; this._fadeDisabled = false; /** * Disable the frame fade plugin while animation is running */ this.disableFrameFade = true; // Same code used in CameraViewPlugin this._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('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('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('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 }; this.defaultDriver = (update) => { return { start: () => this._updaters.push({ u: update, time: 0 }), stop: () => this._updaters.splice(this._updaters.findIndex(u => u.u === update), 1), }; }; this.animations = {}; this.enabled = enabled; this._postFrame = this._postFrame.bind(this); } onAdded(viewer) { super.onAdded(viewer); viewer.addEventListener('postFrame', this._postFrame); } onRemove(viewer) { viewer.removeEventListener('postFrame', this._postFrame); super.onRemove(viewer); } animateTarget(target, key, options) { return this.animate({ ...options, target, key: key }); } animate(options1) { let targetRef = undefined; const options = { ...options1 }; 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) => { 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 = { id: uuid, options, stop: () => { if (!this.animations[uuid]?._stop) console.warn('Animation not started'); else this.animations[uuid]?._stop?.(); }, promise: undefined, targetRef, }; this.animations[uuid] = a; a.promise = new Promise((resolve, reject) => { const end2 = () => { try { options.onEnd && options.onEnd(); } catch (e) { reject(e); return false; } return true; }; // todo: test boolean if (options.from === undefined) { console.warn('from is undefined', options); resolve(); return; } const isBool = typeof options.from === 'boolean'; if (isBool) { options.from = options.from ? 1 : 0; options.to = options.to ? 1 : 0; } const opts = { driver: this.defaultDriver, ...options, onUpdate: !isBool ? options.onUpdate : undefined, onComplete: async () => { try { if (isBool) options.onUpdate?.(options.to); options.onComplete && await options.onComplete(); } catch (e) { if (!end2()) return; reject(e); return; } if (!end2()) return; resolve(); }, onStop: async () => { try { options.onStop && await options.onStop(); } catch (e) { if (!end2()) return; reject(e); return; } resolve(); }, }; 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(options, animations) { const anim = this.animate(options); if (animations) animations.push(anim); return anim.promise; } async animateTargetAsync(target, key, options, animations) { const anim = this.animate({ ...options, target, key: key }); if (animations) animations.push(anim); return anim.promise; } animateCamera(camera, view, spherical = true, options) { const anim = spherical ? animateCameraToViewSpherical(camera, view) : animateCameraToViewLinear(camera, view); return this.animate({ ease: EasingFunctions.linear, duration: 1000, ...anim, ...options, }); } async animateCameraAsync(camera, view, spherical = true, options, animations) { const anim = this.animateCamera(camera, view, spherical, options); if (animations) animations.push(anim); return anim.promise; } } PopmotionPlugin.PluginType = 'PopmotionPlugin'; //# sourceMappingURL=PopmotionPlugin.js.map