| - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer | - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer | ||||
| - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | - [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer | ||||
| - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations | ||||
| - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening | |||||
| - [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas | - [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas | ||||
| - [FrameFadePlugin](#framefadeplugin) - Post-render pass to smoothly fade to a new rendered frame over time | - [FrameFadePlugin](#framefadeplugin) - Post-render pass to smoothly fade to a new rendered frame over time | ||||
| - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | - [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files | ||||
| To play individual animations, with custom choreography, use the {@link GLTFAnimationPlugin.animations} property to get reference to the animation clips and actions. Create your own mixers and control the animation playback like in three.js | To play individual animations, with custom choreography, use the {@link GLTFAnimationPlugin.animations} property to get reference to the animation clips and actions. Create your own mixers and control the animation playback like in three.js | ||||
| ## PopmotionPlugin | |||||
| todo: image | |||||
| Example: https://threepipe.org/examples/#popmotion-plugin/ | |||||
| Source Code: [src/plugins/animation/PopmotionPlugin.ts](./src/plugins/animation/PopmotionPlugin.ts) | |||||
| API Reference: [PopmotionPlugin](https://threepipe.org/docs/classes/PopmotionPlugin.html) | |||||
| Provides animation/tweening capabilities to the viewer using the [popmotion.io](https://popmotion.io/) library. | |||||
| Overrides the driver in popmotion to sync with the viewer and provide ways to store and stop animations. | |||||
| ```typescript | |||||
| import {PopmotionPlugin, ThreeViewer} from 'threepipe' | |||||
| const viewer = new ThreeViewer({...}) | |||||
| const cube = viewer.scene.getObjectByName('cube'); | |||||
| const popmotion = viewer.addPluginSync(new PopmotionPlugin()) | |||||
| // Move the object cube 1 unit up. | |||||
| const anim = popmotion.animate({ | |||||
| from: cube.position.y, | |||||
| to: cube.position.y + 1, | |||||
| duration: 500, // ms | |||||
| onUpdate: (v) => { | |||||
| cube.position.setY(v) | |||||
| cube.setDirty() | |||||
| }, | |||||
| onComplete: () => isMovedUp = !isMovedUp, | |||||
| }) | |||||
| // await for animation | |||||
| await anim.promise; | |||||
| // or stop the animation | |||||
| // anim.stop() | |||||
| // Animate the color | |||||
| await popmotion.animateAsync({ // Also await for the animation. | |||||
| from: '#' + cube.material.color.getHexString(), | |||||
| to: '#' + new Color().setHSL(Math.random(), 1, 0.5).getHexString(), | |||||
| duration: 500, | |||||
| onUpdate: (v) => { | |||||
| cube.material.color.set(v) | |||||
| cube.material.setDirty() | |||||
| }, | |||||
| }) | |||||
| ``` | |||||
| Note: The animation is started when the animate or animateAsync function is called. | |||||
| ## RenderTargetPreviewPlugin | ## RenderTargetPreviewPlugin | ||||
| todo: image | todo: image |
| <h2 class="category">Animation</h2> | <h2 class="category">Animation</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./gltf-animation-plugin/">glTF Animation Plugin </a></li> | <li><a href="./gltf-animation-plugin/">glTF Animation Plugin </a></li> | ||||
| <li><a href="./gltf-animation-plugin/">Popmotion Plugin </a></li> | |||||
| <li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li> | <li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li> | ||||
| <li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li> | <li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li> | ||||
| </ul> | </ul> |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Popmotion Plugin</title> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, BoxGeometry, Color, Mesh, PhysicalMaterial, PopmotionPlugin, ThreeViewer} from 'threepipe' | |||||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| }) | |||||
| const popmotion = viewer.addPluginSync(PopmotionPlugin) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| const cube = viewer.scene.addObject(new Mesh( | |||||
| new BoxGeometry(1, 1, 1), | |||||
| new PhysicalMaterial({color: 0xff0000}) | |||||
| )) | |||||
| let isMovedUp = false | |||||
| createSimpleButtons({ | |||||
| ['Move Up/Down']: async(btn) => { | |||||
| btn.disabled = true | |||||
| await popmotion.animateAsync({ | |||||
| from: cube.position.y, | |||||
| to: cube.position.y + (isMovedUp ? -1 : 1), | |||||
| duration: 500, // ms | |||||
| onUpdate: (v) => { | |||||
| cube.position.setY(v) | |||||
| cube.setDirty() | |||||
| }, | |||||
| onComplete: () => isMovedUp = !isMovedUp, | |||||
| }) | |||||
| btn.disabled = false | |||||
| }, | |||||
| ['Rotate +90deg']: async(btn) => { | |||||
| btn.disabled = true | |||||
| await popmotion.animateAsync({ | |||||
| from: cube.rotation.y, | |||||
| to: cube.rotation.y + Math.PI / 2, | |||||
| duration: 500, | |||||
| onUpdate: (v) => { | |||||
| cube.rotation.y = v | |||||
| cube.setDirty() | |||||
| }, | |||||
| }) | |||||
| btn.disabled = false | |||||
| }, | |||||
| ['Change Color']: async(btn)=>{ | |||||
| btn.disabled = true | |||||
| await popmotion.animateAsync({ | |||||
| from: '#' + cube.material.color.getHexString(), | |||||
| to: '#' + new Color().setHSL(Math.random(), 1, 0.5).getHexString(), | |||||
| duration: 500, | |||||
| onUpdate: (v) => { | |||||
| cube.material.color.set(v) | |||||
| cube.material.setDirty() | |||||
| }, | |||||
| }) | |||||
| btn.disabled = false | |||||
| }, | |||||
| }) | |||||
| } | |||||
| init().then(_testFinish) |
| "eslint-plugin-import": "^2.27.5", | "eslint-plugin-import": "^2.27.5", | ||||
| "local-web-server": "^5.3.0", | "local-web-server": "^5.3.0", | ||||
| "markdown-to-html-cli": "^3.7.0", | "markdown-to-html-cli": "^3.7.0", | ||||
| "popmotion": "^11.0.5", | |||||
| "rimraf": "^5.0.1", | "rimraf": "^5.0.1", | ||||
| "rollup": "^3.23.0", | "rollup": "^3.23.0", | ||||
| "rollup-plugin-glsl": "^1.3.0", | "rollup-plugin-glsl": "^1.3.0", | ||||
| "url": "https://github.com/sponsors/isaacs" | "url": "https://github.com/sponsors/isaacs" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/framesync": { | |||||
| "version": "6.1.2", | |||||
| "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz", | |||||
| "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==", | |||||
| "dev": true, | |||||
| "dependencies": { | |||||
| "tslib": "2.4.0" | |||||
| } | |||||
| }, | |||||
| "node_modules/framesync/node_modules/tslib": { | |||||
| "version": "2.4.0", | |||||
| "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", | |||||
| "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", | |||||
| "dev": true | |||||
| }, | |||||
| "node_modules/fresh": { | "node_modules/fresh": { | ||||
| "version": "0.5.2", | "version": "0.5.2", | ||||
| "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", | ||||
| "url": "https://opencollective.com/unified" | "url": "https://opencollective.com/unified" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/hey-listen": { | |||||
| "version": "1.0.8", | |||||
| "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", | |||||
| "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", | |||||
| "dev": true | |||||
| }, | |||||
| "node_modules/html-void-elements": { | "node_modules/html-void-elements": { | ||||
| "version": "2.0.1", | "version": "2.0.1", | ||||
| "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", | "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", | ||||
| "url": "https://github.com/sponsors/sindresorhus" | "url": "https://github.com/sponsors/sindresorhus" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/popmotion": { | |||||
| "version": "11.0.5", | |||||
| "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz", | |||||
| "integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==", | |||||
| "dev": true, | |||||
| "dependencies": { | |||||
| "framesync": "6.1.2", | |||||
| "hey-listen": "^1.0.8", | |||||
| "style-value-types": "5.1.2", | |||||
| "tslib": "2.4.0" | |||||
| } | |||||
| }, | |||||
| "node_modules/popmotion/node_modules/tslib": { | |||||
| "version": "2.4.0", | |||||
| "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", | |||||
| "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", | |||||
| "dev": true | |||||
| }, | |||||
| "node_modules/postcss": { | "node_modules/postcss": { | ||||
| "version": "8.4.24", | "version": "8.4.24", | ||||
| "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", | ||||
| "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", | "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", | ||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "node_modules/style-value-types": { | |||||
| "version": "5.1.2", | |||||
| "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz", | |||||
| "integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==", | |||||
| "dev": true, | |||||
| "dependencies": { | |||||
| "hey-listen": "^1.0.8", | |||||
| "tslib": "2.4.0" | |||||
| } | |||||
| }, | |||||
| "node_modules/style-value-types/node_modules/tslib": { | |||||
| "version": "2.4.0", | |||||
| "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", | |||||
| "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", | |||||
| "dev": true | |||||
| }, | |||||
| "node_modules/stylehacks": { | "node_modules/stylehacks": { | ||||
| "version": "5.1.1", | "version": "5.1.1", | ||||
| "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", | "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", | ||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "framesync": { | |||||
| "version": "6.1.2", | |||||
| "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz", | |||||
| "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==", | |||||
| "dev": true, | |||||
| "requires": { | |||||
| "tslib": "2.4.0" | |||||
| }, | |||||
| "dependencies": { | |||||
| "tslib": { | |||||
| "version": "2.4.0", | |||||
| "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", | |||||
| "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", | |||||
| "dev": true | |||||
| } | |||||
| } | |||||
| }, | |||||
| "fresh": { | "fresh": { | ||||
| "version": "0.5.2", | "version": "0.5.2", | ||||
| "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", | ||||
| "space-separated-tokens": "^2.0.0" | "space-separated-tokens": "^2.0.0" | ||||
| } | } | ||||
| }, | }, | ||||
| "hey-listen": { | |||||
| "version": "1.0.8", | |||||
| "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", | |||||
| "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", | |||||
| "dev": true | |||||
| }, | |||||
| "html-void-elements": { | "html-void-elements": { | ||||
| "version": "2.0.1", | "version": "2.0.1", | ||||
| "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", | "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", | ||||
| "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", | "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", | ||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "popmotion": { | |||||
| "version": "11.0.5", | |||||
| "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz", | |||||
| "integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==", | |||||
| "dev": true, | |||||
| "requires": { | |||||
| "framesync": "6.1.2", | |||||
| "hey-listen": "^1.0.8", | |||||
| "style-value-types": "5.1.2", | |||||
| "tslib": "2.4.0" | |||||
| }, | |||||
| "dependencies": { | |||||
| "tslib": { | |||||
| "version": "2.4.0", | |||||
| "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", | |||||
| "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", | |||||
| "dev": true | |||||
| } | |||||
| } | |||||
| }, | |||||
| "postcss": { | "postcss": { | ||||
| "version": "8.4.24", | "version": "8.4.24", | ||||
| "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", | ||||
| "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", | "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", | ||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "style-value-types": { | |||||
| "version": "5.1.2", | |||||
| "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz", | |||||
| "integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==", | |||||
| "dev": true, | |||||
| "requires": { | |||||
| "hey-listen": "^1.0.8", | |||||
| "tslib": "2.4.0" | |||||
| }, | |||||
| "dependencies": { | |||||
| "tslib": { | |||||
| "version": "2.4.0", | |||||
| "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", | |||||
| "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", | |||||
| "dev": true | |||||
| } | |||||
| } | |||||
| }, | |||||
| "stylehacks": { | "stylehacks": { | ||||
| "version": "5.1.1", | "version": "5.1.1", | ||||
| "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", | "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", |
| "typescript": "^5.0.4", | "typescript": "^5.0.4", | ||||
| "typescript-plugin-css-modules": "^5.0.1", | "typescript-plugin-css-modules": "^5.0.1", | ||||
| "uiconfig.js": "^0.0.6", | "uiconfig.js": "^0.0.6", | ||||
| "rollup-plugin-replace": "^2.2.0" | |||||
| "rollup-plugin-replace": "^2.2.0", | |||||
| "popmotion": "^11.0.5" | |||||
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1012/package.tgz", | "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1012/package.tgz", |
| import terser from "@rollup/plugin-terser"; | import terser from "@rollup/plugin-terser"; | ||||
| import postcss from 'rollup-plugin-postcss' | import postcss from 'rollup-plugin-postcss' | ||||
| import glsl from "rollup-plugin-glsl" | import glsl from "rollup-plugin-glsl" | ||||
| import replace from "rollup-plugin-replace"; | |||||
| const __filename = fileURLToPath(import.meta.url); | const __filename = fileURLToPath(import.meta.url); | ||||
| const __dirname = path.dirname(__filename); | const __dirname = path.dirname(__filename); | ||||
| ], | ], | ||||
| external: [], | external: [], | ||||
| plugins: [ | plugins: [ | ||||
| replace({ | |||||
| 'process.env.NODE_ENV': JSON.stringify( 'production' ) | |||||
| }), | |||||
| glsl({ // todo: minify glsl. | glsl({ // todo: minify glsl. | ||||
| include: "src/**/*.glsl" | include: "src/**/*.glsl" | ||||
| }), | }), |
| import type {Driver} from 'popmotion/lib/animations/types' | |||||
| import {now} from 'ts-browser-helpers' | |||||
| import {animate, type AnimationOptions} from 'popmotion' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import type {FrameFadePlugin} from '../pipeline/FrameFadePlugin' | |||||
| import type {ProgressivePlugin} from '../pipeline/ProgressivePlugin' | |||||
| import {generateUUID} from '../../three' | |||||
| export interface AnimationResult{ | |||||
| id: string | |||||
| promise: Promise<string> | |||||
| options: AnimationOptions<any> | |||||
| stop: () => void | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| _stop?: () => void | |||||
| } | |||||
| /** | |||||
| * 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 store and stop animations. | |||||
| * | |||||
| * @category Plugin | |||||
| */ | |||||
| export class PopmotionPlugin extends AViewerPluginSync<''> { | |||||
| public static readonly PluginType = 'PopmotionPlugin' | |||||
| enabled = true | |||||
| toJSON: any = undefined // disable serialization | |||||
| fromJSON: any = undefined // disable serialization | |||||
| constructor(enabled = true) { | |||||
| super() | |||||
| this.enabled = enabled | |||||
| this._postFrame = this._postFrame.bind(this) | |||||
| } | |||||
| // private _animating = false | |||||
| private _lastFrameTime = 0 // for post frame | |||||
| private _updaters: {u: ((timestamp: number) => void), time: number}[] = [] | |||||
| dependencies = [] | |||||
| private _fadeDisabled = false | |||||
| /** | |||||
| * Disable the frame fade plugin while animation is running | |||||
| */ | |||||
| disableFrameFade = true | |||||
| // Same code used in CameraViewPlugin | |||||
| private _postFrame = ()=>{ | |||||
| if (!this._viewer) return | |||||
| if (!this.enabled || Object.keys(this.animations).length < 1) { | |||||
| this._lastFrameTime = 0 | |||||
| // console.log('not anim') | |||||
| if (this._fadeDisabled) { | |||||
| this._viewer.getPlugin<FrameFadePlugin>('FrameFade')?.enable(PopmotionPlugin.PluginType) | |||||
| 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<ProgressivePlugin>('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<FrameFadePlugin>('FrameFade') | |||||
| if (ff) { | |||||
| ff.disable(PopmotionPlugin.PluginType) | |||||
| this._fadeDisabled = true | |||||
| } | |||||
| } | |||||
| // todo: scrolling | |||||
| // if (this._scrollAnimationState < 0.001) this._scrollAnimationState = 0 | |||||
| // else this._scrollAnimationState *= 1.0 - this.scrollAnimationDamping | |||||
| } | |||||
| readonly defaultDriver: Driver = (update)=>{ | |||||
| return { | |||||
| start: ()=>this._updaters.push({u:update, time:0}), | |||||
| stop: ()=> this._updaters.splice(this._updaters.findIndex(u=>u.u === update), 1), | |||||
| } | |||||
| } | |||||
| onAdded(viewer: ThreeViewer): void { | |||||
| super.onAdded(viewer) | |||||
| viewer.addEventListener('postFrame', this._postFrame) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer): void { | |||||
| viewer.removeEventListener('postFrame', this._postFrame) | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| readonly animations: Record<string, AnimationResult> = {} | |||||
| animate<V>(options: AnimationOptions<V>): AnimationResult { | |||||
| const uuid = generateUUID() | |||||
| const a: any = { | |||||
| id: uuid, | |||||
| options, | |||||
| stop: ()=>{ | |||||
| if (!this.animations[uuid]?._stop) console.warn('Animation not started') | |||||
| else this.animations[uuid]?._stop?.() | |||||
| }, | |||||
| } | |||||
| this.animations[uuid] = a | |||||
| a.promise = new Promise<void>((resolve) => { | |||||
| const opts: AnimationOptions<V> = { | |||||
| driver: this.defaultDriver, | |||||
| ...options, | |||||
| onComplete: ()=>{ | |||||
| options.onComplete?.() | |||||
| resolve() | |||||
| }, | |||||
| onStop: ()=>{ | |||||
| options.onStop?.() | |||||
| 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<V>(options: AnimationOptions<V>): Promise<string> { | |||||
| return this.animate(options).promise | |||||
| } | |||||
| // todo : animateObject/animateTarget | |||||
| } |
| // animation | // animation | ||||
| export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | ||||
| export {PopmotionPlugin} from './animation/PopmotionPlugin' |