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
JavaScript
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