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.

471 lines 21.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; }; import { getOrCall, serialize } from 'ts-browser-helpers'; import { AViewerPluginSync } from '../../viewer'; import { generateUiConfig } from 'uiconfig.js'; import { PopmotionPlugin } from './PopmotionPlugin'; import { AnimationObject } from '../../utils/AnimationObject'; import { createDiv, createStyles, UndoManagerPlugin } from '../../index'; // @uiFolder('Viewer Animations') // todo rename plugin to ViewerAnimationsPlugin? export class AnimationObjectPlugin extends AViewerPluginSync { // /** // * Animations that are tracked in the scene, bound to objects, materials etc // */ // animations: Set<AnimationObject> = new Set() getAllAnimations() { return [...this.animation.animSet, ...this.runtimeAnimation.animSet]; } _getActiveIndex(ao) { if (!ao.target) return ''; const cTime = 1000 * (this._viewer?.timeline.time || 0); // current time in ui const localTime = (cTime - ao.delay) / ao.duration; const offsetTimes = ao.offsets; const closestIndex = offsetTimes.reduce((prev, curr, index) => { return Math.abs(curr - localTime) < Math.abs(offsetTimes[prev] - localTime) ? index : prev; }, 0); const dist = Math.abs(offsetTimes[closestIndex] - localTime); const activeIndex = dist * ao.duration < 50 ? closestIndex.toString() : ''; return activeIndex; } get triggerButtonsShown() { return this._triggerButtonsShown; } set triggerButtonsShown(v) { this._triggerButtonsShown = v; if (v) document.body.classList.add('aouic-triggers-visible'); else document.body.classList.remove('aouic-triggers-visible'); } showTriggers(v = true) { this.triggerButtonsShown = v; } constructor() { super(); this.enabled = true; this.dependencies = [PopmotionPlugin]; /** * Main animation with target = viewer for global properties */ this.animation = new AnimationObject(() => this._viewer, () => this._viewer, 'Viewer Animation'); this.runtimeAnimation = new AnimationObject(undefined, () => this._viewer, 'Runtime Animation'); // private _fAnimationAdd = (e: Event2<'animationAdd', AnimationObjectEventMap, AnimationObject>)=>{ // this.rebuildTimeline() // this.dispatchEvent(e) // } this._fAnimationAdd = (e) => { this.rebuildTimeline(); this.dispatchEvent({ ...e, type: 'animationAdd' }); }; this._fAnimationRemove = (e) => { this.rebuildTimeline(); this.dispatchEvent(e); if (e.fromChild && e.target === this.runtimeAnimation) { const obj = e.animation.target; if (obj?.userData?.animationObjects) this._removeAnimationFromObject(e.animation, obj); const visibleBtns = this._visibleBtns.get(e.animation); if (visibleBtns) { visibleBtns.forEach(btn => this._refreshTriggerBtn(e.animation, btn)); } } else { this._visibleBtns.delete(e.animation); } }; this._fAnimationUpdate = (e) => { this.rebuildTimeline(); this.dispatchEvent({ ...e, type: 'animationUpdate', animation: e.target }); const visibleBtns = this._visibleBtns.get(e.target); if (visibleBtns) { visibleBtns.forEach(btn => this._refreshTriggerBtn(e.target, btn)); } }; this._viewerTimelineUpdate = () => { if (!this._viewer) return; this._visibleBtns.forEach((btns, ao) => { btns.forEach(btn => this._refreshTriggerBtn(ao, btn)); }); }; this._refreshTriggerBtn = (ao, btn) => { const activeIndex = this._getActiveIndex(ao); btn.dataset.activeIndex = activeIndex; if (activeIndex.length) { btn.classList.add('anim-object-uic-trigger-active'); } else { btn.classList.remove('anim-object-uic-trigger-active'); } }; this._triggerButtonsShown = false; // uiConfig = this.animation.uiConfig this._currentTimeline = []; this._refTimeline = false; this._viewerListeners = { postFrame: () => { const pop = this._viewer?.getPlugin(PopmotionPlugin); if (this._refTimeline && pop) { this._refTimeline = false; this._currentTimeline.forEach(([_, r]) => r.stop()); this._currentTimeline = this.getAllAnimations().map(o => [o, pop.animateObject(o, 0, false, this.popmotionDriver)]); this.dispatchEvent({ type: 'rebuildTimeline', timeline: this._currentTimeline }); } }, preFrame: () => { if (!this._viewer) return; if (this.isDisabled() || Object.keys(this._updaters).length < 1) { this._lastFrameTime = 0; return; } const time = this._viewer.timeline.time * 1000; // if (this._lastFrameTime < 1) this._lastFrameTime = time - 1.0 / 60.0 const delta = time - this._lastFrameTime; this._lastFrameTime = time; if (Math.abs(delta) <= 0.0001) return; this._updaters.forEach(u => { let dt = delta; if (u.time !== time) dt = time - u.time; if (u.time + dt < 0) dt = -u.time; u.time += dt; if (Math.abs(dt) > 0.001) u.u(dt); }); }, }; this._objectAdd = (e) => { const obj = e.object; if (!obj) return; if (Array.isArray(obj.userData.animationObjects)) { obj.userData.animationObjects.forEach(ao => this._addAnimationObject(ao, obj)); } this._setupUiConfig(obj); }; this._objectRemove = (e) => { const obj = e.object; if (!obj) return; if (Array.isArray(obj.userData.animationObjects)) { obj.userData.animationObjects.forEach(ao => this._removeAnimationObject(ao)); } this._cleanUpUiConfig(obj); }; this._materialAdd = (e) => { const obj = e.material; if (!obj) return; if (Array.isArray(obj.userData.animationObjects)) { obj.userData.animationObjects.forEach(ao => this._addAnimationObject(ao, obj)); } this._setupUiConfig(obj); }; this._materialRemove = (e) => { const obj = e.material; if (!obj) return; if (Array.isArray(obj.userData.animationObjects)) { obj.userData.animationObjects.forEach(ao => this._removeAnimationObject(ao)); } this._cleanUpUiConfig(obj); }; this._visibleBtns = new Map(); this._iObservers = new Map(); this._lastFrameTime = 0; // for post frame, in ms this._updaters = []; this.popmotionDriver = (update) => ({ start: () => this._updaters.push({ u: update, time: 0 }), stop: () => { this._updaters.splice(this._updaters.findIndex(u => u.u === update), 1); }, }); // override ui config for flatten hierarchy (for now) this.uiConfig = { label: 'Viewer Animations', type: 'folder', children: [ generateUiConfig(this.animation).filter(c => { const label = getOrCall(c?.label) ?? ''; // if (label === ('animSet' as (keyof AnimationObject))) return c.children return ['Animate', 'Stop', 'Animate Reverse'].includes(label); }) ?? [], () => { const c = generateUiConfig(this.animation.animSet); return c.map(d => getOrCall(d)).filter(Boolean); }, { type: 'checkbox', label: 'Run in Parallel', property: [this.animation, 'animSetParallel'], }, { type: 'button', label: 'Add Animation', value: () => { this.animation.addAnimation(); this.uiConfig.uiRefresh?.(true, 'postFrame', 1); }, }, { type: 'checkbox', label: 'Show Triggers', property: [this, 'triggerButtonsShown'], }, // { // type: 'button', // label: 'Clear Animations', // value: ()=>{ // this.animation.animSet = [] // this.animation.refreshUi() // }, // } ], }; this.animation.animSetParallel = true; this.animation.uiConfig.uiRefresh = (...args) => this.uiConfig.uiRefresh?.(...args); this.animation.addEventListener('animationAdd', this._fAnimationAdd); this.animation.addEventListener('animationRemove', this._fAnimationRemove); this.animation.addEventListener('update', this._fAnimationUpdate); this.runtimeAnimation.animSetParallel = true; this.runtimeAnimation.uiConfig.uiRefresh = (...args) => this.uiConfig.uiRefresh?.(...args); this.runtimeAnimation.addEventListener('animationAdd', this._fAnimationAdd); this.runtimeAnimation.addEventListener('animationRemove', this._fAnimationRemove); this.runtimeAnimation.addEventListener('update', this._fAnimationUpdate); this._fAnimationAdd({ animation: this.animation }); createStyles(` .anim-object-uic-trigger{ padding: 4px; margin-top: -4px; cursor: pointer; color: var(--tp-label-foreground-color, #777); display: none; } .anim-object-uic-trigger-visible{ } .anim-object-uic-trigger-active{ color: red; } .aouic-triggers-visible .anim-object-uic-trigger{ display: inline-block; } `); } rebuildTimeline() { this._refTimeline = true; } getTimeline() { return this._currentTimeline; } _addAnimationObject(ao, obj) { ao.target = obj; this.runtimeAnimation.add(ao); } _removeAnimationObject(ao) { this.runtimeAnimation.remove(ao); ao.target = undefined; } _removeAnimationFromObject(ao, obj) { ao.target = undefined; if (!obj.userData.animationObjects) return; const ind = obj.userData.animationObjects.indexOf(ao); if (ind >= 0) { obj.userData.animationObjects.splice(ind, 1); if (obj.userData.animationObjects.length < 1) { delete obj.userData.animationObjects; } } } _setupUiConfig(obj) { const type = obj.isObject3D ? 'objects' : obj.isMaterial ? 'materials' : undefined; if (!type) return; obj.uiConfig?.children?.push({ type: 'folder', label: 'Property Animations', tags: ['animation', AnimationObjectPlugin.PluginType], children: [() => obj.userData.animationObjects?.map(ao => ao.uiConfig)], }); const components = this._animatableUiConfigs(obj); for (const config of components) { const prop = getOrCall(config.property); // todo use uiconfigmethods if (!prop) continue; const [tar, key] = prop; if (!tar || typeof key !== 'string') continue; const btn = createDiv({ innerHTML: '◆', classList: ['anim-object-uic-trigger'] }); btn.dataset.isAnimObjectTrigger = '1'; btn.title = 'Add Animation for ' + getOrCall(config.label, key); // todo use uiconfigmethods const getAo = () => { if (!obj.userData.animationObjects) obj.userData.animationObjects = []; return obj.userData.animationObjects.find(o => o.access === key); }; btn.addEventListener('click', () => { const undo = this._viewer?.getPlugin(UndoManagerPlugin); // todo use uiconfigmethods let ao = getAo(); const cTime = 1000 * (this._viewer?.timeline.time || 0); // current time in ui if (!ao) { ao = new AnimationObject(); // ao.access = type + '.' + obj.uuid + '.' + key ao.access = key; ao.name = obj.name + ' ' + (getOrCall(config.label, key) || key); ao.updateTarget = true; // calls setDirty on obj on any change ao.delay = cTime; // current time in ui ao.duration = 2000; const cao = ao; const c = { redo: () => { if (!obj.userData.animationObjects) obj.userData.animationObjects = []; obj.userData.animationObjects.push(cao); this._addAnimationObject(cao, obj); this._refreshTriggerBtn(cao, btn); }, undo: () => { cao.removeFromParent(); // this will dispatch with fromChild = true this._refreshTriggerBtn(cao, btn); }, }; c.redo(); undo?.undoManager?.record(c); } else if (ao.values.length > 1) { const cao = ao; const shownActiveIndex = btn.dataset.activeIndex || ''; const activeIndex = this._getActiveIndex(ao); if (activeIndex === shownActiveIndex) { const index = parseInt(activeIndex || '-1'); const ref = () => this._refreshTriggerBtn(cao, btn); if (undo) { if (index < 0) undo.performAction(ao, ao.addKeyframe, [cTime], 'addKeyframe-' + ao.access, ref); else undo.performAction(ao, ao.updateKeyframe, [index], 'editKeyframe-' + ao.access, ref); ref(); } else { if (index < 0) ao.addKeyframe(cTime); else ao.updateKeyframe(index); ref(); } } else { // todo something else is shown in ui, maybe user didnt want this console.error('Active index mismatch', activeIndex, shownActiveIndex); } } // btn.remove() // config.domChildren = !config.domChildren || Array.isArray(config.domChildren) ? config.domChildren?.filter(d => d !== btn) || [] : config.domChildren }); const btnObserver = new IntersectionObserver(entries => { for (const entry of entries) { if (entry.target !== btn) continue; const ao = getAo(); if (!ao) continue; if (!this._visibleBtns.has(ao)) this._visibleBtns.set(ao, new Set()); const btns = this._visibleBtns.get(ao); // console.log(entry.isIntersecting) if (entry.isIntersecting) { if (!btns.has(btn)) { btn.classList.add('anim-object-uic-trigger-visible'); btns.add(btn); // timeline time change // animation object change } } else { btn.classList.remove('anim-object-uic-trigger-visible'); btns.delete(btn); } } }); btnObserver.observe(btn); if (!this._iObservers.has(obj.uuid)) this._iObservers.set(obj.uuid, new Set()); this._iObservers.get(obj.uuid)?.add(btnObserver); const ao = getAo(); if (ao) this._refreshTriggerBtn(ao, btn); config.domChildren = !config.domChildren || Array.isArray(config.domChildren) ? [...config.domChildren || [], btn] : config.domChildren; } } _cleanUpUiConfig(obj) { const components = this._animatableUiConfigs(obj); const observers = this._iObservers.get(obj.uuid); for (const config of components) { config.domChildren = Array.isArray(config.domChildren) ? config.domChildren?.filter(d => !(d instanceof HTMLElement && d.dataset.isAnimObjectTrigger)) || [] : config.domChildren; } if (observers) { observers.forEach(o => o.disconnect()); observers.clear(); this._iObservers.delete(obj.uuid); } } _animatableUiConfigs(obj) { return obj.uiConfig?.children?.filter(c => typeof c === 'object' && c.type && ['vec3', 'color', 'number', 'checkbox', 'toggle'].includes(c.type) && Array.isArray(c.property) && c.property[0] === obj) || []; } onAdded(viewer) { super.onAdded(viewer); viewer.object3dManager.addEventListener('objectAdd', this._objectAdd); viewer.object3dManager.addEventListener('objectRemove', this._objectRemove); viewer.object3dManager.addEventListener('materialAdd', this._materialAdd); viewer.object3dManager.addEventListener('materialRemove', this._materialRemove); viewer.timeline.addEventListener('update', this._viewerTimelineUpdate); viewer._animGetters = { objects: (name, acc) => { if (!viewer) return undefined; const obj = viewer.object3dManager.findObject(name); return { tar: obj, acc, onChange: obj ? () => { obj.setDirty && obj.setDirty({ refreshScene: false, frameFade: false }); } : undefined }; }, materials: (name, acc) => { if (!viewer) return undefined; const mat = viewer.object3dManager.findMaterial(name); return { tar: mat, acc, onChange: mat ? () => { mat.setDirty && mat.setDirty({ frameFade: false }); } : undefined }; }, }; } onRemove(viewer) { super.onRemove(viewer); viewer.object3dManager.removeEventListener('objectAdd', this._objectAdd); viewer.object3dManager.removeEventListener('objectRemove', this._objectRemove); viewer.object3dManager.removeEventListener('materialAdd', this._materialAdd); viewer.object3dManager.removeEventListener('materialRemove', this._materialRemove); delete viewer._animGetters; } fromJSON(data, meta) { if (!super.fromJSON(data, meta)) return null; // this.animation.setTarget(() => this._viewer) return this; } } AnimationObjectPlugin.PluginType = 'AnimationObjectPlugin'; __decorate([ serialize() // @uiConfig() ], AnimationObjectPlugin.prototype, "animation", void 0); //# sourceMappingURL=AnimationObjectPlugin.js.map