UNPKG

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