threepipe
Version:
A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.
518 lines • 20.1 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var AnimationObject_1;
import { deepAccessObject, getOrCall, onChange, serializable, serialize, uuidV4 } from 'ts-browser-helpers';
import { generateUiConfig, generateValueConfig, uiButton, uiDropdown, uiSlider, uiToggle, } from 'uiconfig.js';
import { generateUUID } from '../three';
import { EasingFunctions } from './animation';
import { ThreeSerialization } from './serialization';
import { EventDispatcher } from 'three';
const viewerOptions = {
'None': '',
['Background Color']: 'scene.backgroundColor',
['Environment Rotation']: 'scene.environment.rotation',
['Environment Intensity']: 'scene.envMapIntensity',
// '[Fixed Env Map Direction']: 'scene.fixedEnvMapDirection',
['Camera Position']: 'scene.mainCamera.position',
['Camera Rotation']: 'scene.mainCamera.rotation',
['Camera Zoom']: 'scene.mainCamera.zoom',
['Camera FOV']: 'scene.mainCamera.fov',
// '[Directional Light Color']: 'plugins.RandomizedDirectionalLight.light.color',
// - todo dosent update shadows every frame
// '[Directional Light Direction']: 'plugins.RandomizedDirectionalLight.light.randomParams.direction',
// '[Diamond Env Map Rotation']: 'plugins.Diamond.envMap.rotation',
['Tonemap Exposure']: 'plugins.Tonemap.exposure',
['Tonemap Saturation']: 'plugins.Tonemap.saturation',
['Tonemap Contrast']: 'plugins.Tonemap.contrast',
['Tonemap Tone Mapping']: 'plugins.Tonemap.toneMapping',
['SSR Intensity']: 'plugins.SSReflection.passes.ssr.passObject.intensity',
['SSR Boost']: 'plugins.SSReflection.passes.ssr.passObject.boost',
['Chromatic Aberration Intensity']: 'plugins.ChromaticAberration.intensity',
['Film Grain Intensity']: 'plugins.FilmicGrain.intensity',
['Vignette Color']: 'plugins.Vignette.color',
['Vignette Power']: 'plugins.Vignette.power',
['Depth of Field Focal Point']: 'plugins.DepthOfField._focalPointHit',
['Depth of Field Near Far Blur Scale X']: 'plugins.DepthOfField.pass.nearFarBlurScale.x',
['Depth of Field Near Far Blur Scale Y']: 'plugins.DepthOfField.pass.nearFarBlurScale.y',
['Depth of Field Focal Depth Range Y']: 'plugins.DepthOfField.pass.focalDepthRange.y',
['Bloom Intensity']: 'plugins.Bloom.pass.intensity',
['Bloom Radius']: 'plugins.Bloom.pass.radius',
['Bloom Power']: 'plugins.Bloom.pass.power',
};
export function extractAnimationKey(o, extraGetters) {
let acc = Array.from((o.access ?? '').split(/(?<!\\)\./)); // split by dot, but not escaped dots
let tar = o.targetObject;
let onChange1 = undefined;
const key = acc.pop()?.replace(/\\\./g, '.'); // deep access till the last element, then bind
if (!key || key.length === 0)
return { key: undefined, tar };
extraGetters = extraGetters ?? tar?._animGetters; // _animGetters are set in AnimationObjectPlugin
const getterType = acc.length >= 1 ? acc[0] : undefined;
const getterName = acc.length >= 2 ? acc[1]?.replace(/\\\./g, '.') : undefined;
if (extraGetters && getterType && getterType in extraGetters && getterName) {
acc = acc.slice(2);
const res = extraGetters[getterType](getterName, acc);
if (!res)
tar = res;
else {
tar = res.tar;
// acc = acc.slice(res.i + 1)
acc = res.acc;
onChange1 = res.onChange ?? onChange1;
}
}
tar = deepAccessObject(acc, tar);
return { key, tar, onChange: onChange1 };
}
let AnimationObject = AnimationObject_1 = class AnimationObject extends EventDispatcher {
// targetObject?: Record<string, any>
get targetObject() {
return getOrCall(this.target) ?? this.parent?.targetObject;
}
getViewer() {
return this.viewer ? getOrCall(this.viewer) : this.parent?.getViewer();
}
constructor(target, viewer, name = '') {
super();
this.uuid = generateUUID();
this.setDirty = () => {
// console.log('update')
this.updater = [];
if (this.options) {
this.options.repeatType = this.repeatType;
this.options.repeat = this.repeat;
}
if (!this._upfn)
return;
if (this.updateScene)
this.updater.push(this._upfn.scene);
if (this.updateCamera)
this.updater.push(this._upfn.camera);
if (this.updateViewer)
this.updater.push(this._upfn.viewer);
if (this.updateTarget)
this.updater.push(this._upfn.target);
this.dispatchEvent({ type: 'update' });
};
this.name = '';
this.access = ''; // dot separated target accessor. 'scene.modelRoot.rotation' will give this.model.rotation
// @uiConfig(undefined, {params: (t: AnimationObject)=>({onChange: t.setDirty})})
// @serialize() from?: V
//
// @uiConfig(undefined, {params: (t: AnimationObject)=>({onChange: t.setDirty})})
// @serialize()
// to?: V // | ((fromVal: V, target: any) => V)
this.values = [];
this.offsets = [];
this.options = { // extra options
// onUpdate: (v: V)=>{
// console.log(v)
// },
// onPlay: ()=>{
// if (this.updateCamera) getOrCall(this.target)?.scene.mainCamera.setInteractions(false, this.uuid)
// },
// onStop: ()=>{
// if (this.updateCamera) getOrCall(this.target)?.scene.mainCamera.setInteractions(true, this.uuid)
// },
// onComplete: ()=>{
// if (this.updateCamera) getOrCall(this.target)?.scene.mainCamera.setInteractions(true, this.uuid)
// },
};
this.duration = 1000; // ms
this.delay = 0;
/**
* Number of times to repeat the animation.
* Doesn't work right now
*/
this.repeat = 0;
/**
* Delay between repeats in milliseconds.
* Doesn't work right now
*/
this.repeatDelay = 0;
/**
* Type of repeat behavior.
* - 'loop': repeats the animation from the beginning.
* - 'reverse': plays the animation in reverse after it completes.
* - 'mirror': plays the animation in reverse after it completes. todo only mirrors the time, not values?
*
* Doesn't work right now
*/
this.repeatType = 'reverse';
this.ease = 'easeInOutSine';
this.updater = [];
this.updateScene = false;
this.updateCamera = false;
this.updateViewer = false;
this.updateTarget = false;
this._upfn = {
viewer: () => this.getViewer()?.setDirty(),
renderer: () => this.getViewer()?.renderManager.reset(),
scene: () => {
this.getViewer()?.scene.setDirty();
},
camera: () => this.getViewer()?.scene.mainCamera.setDirty(),
target: () => {
const t = this.targetObject;
if (t && typeof t.setDirty === 'function') {
t.setDirty({ frameFade: false, refreshScene: false });
}
},
};
this.animSetParallel = false;
this.animSet = [];
this.uiConfig = {
type: 'folder',
label: () => this.name || this.access || 'Animation',
children: [
() => this.target ? null : {
type: 'input',
label: 'Property',
property: [this, 'access'],
children: Object.entries(viewerOptions).map(([label, value]) => ({ label, value })),
},
() => this.values.flatMap((val, i) => [
{
...generateValueConfig(this.values, i + '', undefined, val),
label: i === 0 ? 'From' : i === this.values.length - 1 ? 'To' : 'Key ' + i,
onChange: () => this.setDirty(),
},
i > 0 && i < this.values.length - 1 ? {
type: 'number',
label: 'Offset ' + i,
property: [this.offsets, i + ''],
bounds: [0, 1],
onChange: () => this.setDirty(),
} : null,
]),
generateUiConfig(this),
],
uuid: uuidV4(),
};
this.target = target;
this.viewer = viewer;
this.name = name;
this.dispatchEvent = this.dispatchEvent.bind(this);
}
fromJSON(data, meta) {
if (data.from !== undefined) { // old files with to/from
data = { ...data };
data.values = [data.from, data.to];
data.offsets = [0, 1];
delete data.from;
delete data.to;
}
ThreeSerialization.Deserialize(data, this, meta, true);
this.animSet.map(i => {
i.parent = this;
});
return this;
}
_onAccessChanged() {
const tar = this.targetObject;
if (tar && tar === this.getViewer() && !Object.values(viewerOptions).includes(this.access))
this.access = ''; // todo check for now...
this.values = [];
this.offsets = [];
const clone = this._thisValueCloner();
if (!clone) {
this.refreshUi();
return;
}
this.values = [clone(), clone()];
this.offsets = [0, 1];
this.refreshUi();
}
_thisValueCloner() {
const { key, tar } = extractAnimationKey(this);
const val = tar && key !== undefined ? tar[key] : null;
return val === undefined || val === null ? null : () => {
if (!val)
return val;
if (val.isColor)
return '#' + val.getHexString();
const res = typeof val.clone === 'function' ? val.clone() : typeof val === 'object' ? { ...val } : val;
return res;
};
}
addKeyframe(time) {
if (this.values.length < 2) {
console.warn('AnimationObject: Values not initialized, cannot add keyframe', this);
return;
}
const value = this._thisValueCloner();
if (!value) {
console.warn('AnimationObject: No value to add keyframe for', this);
return;
}
const offsetTime = time - this.delay;
const duration = this.duration;
const delay = this.delay;
const offsets = [...this.offsets];
const values = [...this.values];
let offset = offsetTime / this.duration;
let index;
let newDuration = duration;
let newDelay = delay;
const newValues = [...this.values];
const newOffsets = [...this.offsets];
if (offset < 0) {
const o = -offset;
offset = 0;
for (let i = 0; i < offsets.length; i++) {
newOffsets[i] = (offsets[i] + o) / (1 + o);
}
newDuration = duration - offsetTime;
newDelay = delay + offsetTime;
index = 0;
}
else if (offset > 1) {
const o = offset - 1;
offset = 1;
for (let i = 0; i < offsets.length; i++) {
newOffsets[i] = offsets[i] / (1 + o);
}
newDuration = offsetTime;
index = offsets.length;
}
else {
index = offsets.findIndex(o => o >= offset);
if (index < 0) {
index = this.offsets.length;
}
else if (this.offsets[index] === offset) {
console.warn('AnimationObject: Keyframe already exists at offset', offset, this);
return;
}
}
const val = value();
newValues.splice(index, 0, val);
newOffsets.splice(index, 0, offset);
const redo = () => {
this.duration = newDuration;
this.delay = newDelay;
this.values = newValues;
this.offsets = newOffsets;
this.setDirty();
};
const undo = () => {
this.duration = duration;
this.delay = delay;
this.values = values;
this.offsets = offsets;
this.setDirty();
};
redo();
return { undo, redo };
}
updateKeyframe(index) {
if (index < 0 || index >= this.values.length) {
console.warn('AnimationObject: Invalid keyframe index', index, this);
return;
}
const value = this._thisValueCloner();
if (!value) {
console.warn('AnimationObject: No value to update keyframe for', this);
return;
}
const oldValue = this.values[index];
const newValue = value();
const redo = () => {
this.values[index] = newValue;
this.setDirty();
};
const undo = () => {
this.values[index] = oldValue;
this.setDirty();
};
redo();
return { undo, redo };
}
refreshUi() {
this.setDirty();
this.uiConfig?.uiRefresh?.(true, 'postFrame', 1);
}
add(o) {
this.animSet.push(o);
o.parent = this;
this.dispatchEvent({ type: 'animationAdd', animation: o });
o.addEventListener('update', this.dispatchEvent);
o.addEventListener('animationAdd', this.dispatchEvent);
o.addEventListener('animationRemove', this.dispatchEvent);
this.refreshUi();
}
remove(o, fromChild = false) {
const idx = this.animSet.indexOf(o);
if (idx >= 0) {
this.animSet.splice(idx, 1);
o.parent = undefined;
this.dispatchEvent({ type: 'animationRemove', animation: o, fromChild });
o.removeEventListener('update', this.dispatchEvent);
o.removeEventListener('animationAdd', this.dispatchEvent);
o.removeEventListener('animationRemove', this.dispatchEvent);
this.refreshUi();
}
}
animate(delay = 0, canComplete = true) {
// console.log('animate', this)
if (typeof delay !== 'number' || isNaN(delay)) { // called from ui
delay = 0;
}
if (canComplete && this.result) {
console.warn('AnimationObject: Already animating, stopping previous animation');
this.stop();
}
const viewer = this.getViewer();
const pop = viewer?.getPlugin('PopmotionPlugin');
if (!pop) {
console.error(`AnimationObject: No ${!viewer ? 'viewer' : 'PopmotionPlugin'}`);
const id = generateUUID();
return {
id,
options: this.options,
stop: () => { return; },
promise: Promise.resolve(id),
anims: [],
// completed: true,
};
}
return pop.animateObject(this, 0, canComplete, undefined, delay);
}
// todo during reverse delay should be time - duration
// @uiButton('Animate Reverse')
// async animateReverse() {
// await this.animate(true)
// }
stop() {
if (!this.result)
return;
this.result.stop();
this.result = undefined;
}
async removeFromParent2() {
const viewer = this.getViewer();
if (this.parent && viewer) {
const confirm = await viewer.dialog.confirm(`Delete: Are you sure you want to delete the animation ${this.name}?`);
if (confirm)
this.removeFromParent();
}
}
removeFromParent() {
if (this.parent)
this.parent.remove(this, true);
}
// @uiButton('Add Animation')
addAnimation() {
const o = new AnimationObject_1(this.target);
this.add(o);
return o;
}
};
__decorate([
serialize(),
onChange('setDirty')
// @uiInput()
], AnimationObject.prototype, "name", void 0);
__decorate([
serialize()
// @uiInput()
// @uiDropdown('Property', Object.entries(options).map(([label, value])=>({label, value})))
,
onChange(AnimationObject.prototype._onAccessChanged)
], AnimationObject.prototype, "access", void 0);
__decorate([
serialize()
], AnimationObject.prototype, "values", void 0);
__decorate([
serialize()
], AnimationObject.prototype, "offsets", void 0);
__decorate([
serialize()
// @uiConfig()
], AnimationObject.prototype, "options", void 0);
__decorate([
serialize(),
uiSlider(undefined, [0, 10000], 1, (t) => ({ hidden: () => !t.access })),
onChange('setDirty')
], AnimationObject.prototype, "duration", void 0);
__decorate([
serialize(),
uiSlider(undefined, [0, 10000], 1, (t) => ({ hidden: () => !t.access })),
onChange('setDirty')
], AnimationObject.prototype, "delay", void 0);
__decorate([
serialize()
// @uiSlider(undefined, [0, 10], 1, (t: AnimationObject)=>({hidden: ()=>!t.access}))
,
onChange('setDirty')
], AnimationObject.prototype, "repeat", void 0);
__decorate([
serialize()
// @uiSlider(undefined, [0, 10], 1, (t: AnimationObject)=>({hidden: ()=>!t.access}))
,
onChange('setDirty')
], AnimationObject.prototype, "repeatDelay", void 0);
__decorate([
serialize()
// @uiDropdown('repeatType', ['loop', 'reverse'/* , 'mirror'*/].map((label:string)=>({label})), (t: AnimationObject)=>({hidden: ()=>!t.access}))
,
onChange('setDirty')
], AnimationObject.prototype, "repeatType", void 0);
__decorate([
serialize(),
uiDropdown('ease', Object.keys(EasingFunctions).map((label) => ({ label })), (t) => ({ hidden: () => !t.access })),
onChange('setDirty')
], AnimationObject.prototype, "ease", void 0);
__decorate([
serialize(),
uiToggle(undefined, (t) => ({ hidden: () => !t.access })),
onChange('setDirty')
], AnimationObject.prototype, "updateScene", void 0);
__decorate([
serialize(),
uiToggle(undefined, (t) => ({ hidden: () => !t.access })),
onChange('setDirty')
], AnimationObject.prototype, "updateCamera", void 0);
__decorate([
serialize(),
uiToggle(undefined, (t) => ({ hidden: () => !t.access })),
onChange('setDirty')
], AnimationObject.prototype, "updateViewer", void 0);
__decorate([
serialize()
// @uiToggle(undefined, (t: AnimationObject)=>({hidden: ()=>!t.access}))
,
onChange('setDirty')
], AnimationObject.prototype, "updateTarget", void 0);
__decorate([
onChange(AnimationObject.prototype._onAccessChanged)
], AnimationObject.prototype, "target", void 0);
__decorate([
onChange(AnimationObject.prototype._onAccessChanged)
], AnimationObject.prototype, "viewer", void 0);
__decorate([
uiButton('Animate')
], AnimationObject.prototype, "animate", null);
__decorate([
uiButton('Stop')
], AnimationObject.prototype, "stop", null);
__decorate([
uiButton('Delete')
], AnimationObject.prototype, "removeFromParent2", null);
__decorate([
serialize()
// @uiToggle()
], AnimationObject.prototype, "animSetParallel", void 0);
__decorate([
serialize()
// @uiConfig()
], AnimationObject.prototype, "animSet", void 0);
AnimationObject = AnimationObject_1 = __decorate([
serializable('AnimationObject')
], AnimationObject);
export { AnimationObject };
//# sourceMappingURL=AnimationObject.js.map