diff --git a/package.json b/package.json index 6ee148a..e855722 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "babel-preset-es2015": "^6.13.2", "babel-preset-react": "^6.11.1", "babel-preset-stage-0": "^6.5.0", + "eases": "^1.0.8", "eslint-config-airbnb": "^9.0.1", "eslint-plugin-import": "^1.11.0", "eslint-plugin-jsx-a11y": "^2.0.1", diff --git a/src/examples/ExampleBrowser.js b/src/examples/ExampleBrowser.js index 6ba3ab8..8a15e4a 100644 --- a/src/examples/ExampleBrowser.js +++ b/src/examples/ExampleBrowser.js @@ -3,6 +3,7 @@ import { Link } from 'react-router'; import ExampleViewer from './ExampleViewer'; import SimpleExample from './Simple/index'; +import PositionTransitionExample from './PositionTransition/index'; import ManualRenderingExample from './ManualRendering/index'; import ClothExample from './AnimationCloth/index'; import GeometriesExample from './Geometries/index'; @@ -21,6 +22,12 @@ const examples = [ url: 'Simple/index', slug: 'webgl_simple', }, + { + name: 'Transitions', + component: PositionTransitionExample, + url: 'PositionTransition/index', + slug: 'webgl_transitions', + }, { name: 'Cloth', component: ClothExample, diff --git a/src/examples/PositionTransition/TransitionExampleModule.js b/src/examples/PositionTransition/TransitionExampleModule.js new file mode 100644 index 0000000..4a8384f --- /dev/null +++ b/src/examples/PositionTransition/TransitionExampleModule.js @@ -0,0 +1,207 @@ +import THREE from 'three'; + +import Module from 'react-three-renderer/lib/Module'; + +import PropTypes from 'react/lib/ReactPropTypes'; + +import propTypeInstanceOf from 'react-three-renderer/lib/utils/propTypeInstanceOf'; + +import eases from 'eases'; + +const allEaseTypes = Object.keys(eases); + +class PositionTransition { + constructor(threeObject, wantedPosition) { + this.module = threeObject; + this.threeObject = threeObject; + this.wantsToBeRemoved = false; + + threeObject.userData.events.on('dispose', this.onObjectDispose); + + this.newTarget(wantedPosition); + } + + newTarget(newPosition) { + this.startPosition = this.threeObject.position.clone(); + this.wantedPosition = newPosition; + this.transitionStartTime = new Date().getTime(); + } + + tick() { + if (this.wantsToBeRemoved) { + return false; + } + + const threeObject = this.threeObject; + const duration = threeObject.userData._transitionDuration; + const endTime = this.transitionStartTime + duration; + const now = new Date().getTime(); + + if (now > endTime) { + this.finish(); + return false; + } + + const wantedEase = eases[threeObject.userData._easeType] || eases.cubicInOut; + + const easedValue = wantedEase((now - this.transitionStartTime) / duration); + + const fullDifference = this.wantedPosition.clone() + .sub(this.startPosition); + + const finalValue = this.startPosition.clone() + .add(fullDifference.multiplyScalar(easedValue)); + + threeObject.position.copy(finalValue); + + if (threeObject.userData._lookAt) { + threeObject.lookAt(threeObject.userData._lookAt); + } + + return true; + } + + finish() { + if (this.wantsToBeRemoved) { + return; + } + + this.wantsToBeRemoved = true; + + const threeObject = this.threeObject; + + threeObject.position.copy(this.wantedPosition); + + if (threeObject.userData._lookAt) { + threeObject.lookAt(threeObject.userData._lookAt); + } + + threeObject.userData._positionTransition = undefined; + + this.threeObject.userData.events.removeListener('dispose', this.onObjectDispose); + } + + onObjectDispose() { + this.wantsToBeRemoved = true; + + this.threeObject.userData.events.removeListener('dispose', this.onObjectDispose); + } +} + +/** + * Adds three props to object types ( e.g. mesh ): + * - easeType ( see eases/index.js ) + * - transitionDuration : how many milliseconds the position transition should take + * - easePosition : bool, if true it will use a transition to set positions + * + * These will affect how their position property is applied. + */ +class TransitionExample extends Module { + constructor() { + super(); + + this.patchedDescriptors = []; + this.activeTransitions = []; + this.react3RendererInstance = null; + } + + setup(r3rInstance) { + this.react3RendererInstance = r3rInstance; + + super.setup(r3rInstance); + + const Object3DDescriptor = r3rInstance.threeElementDescriptors.object3D.constructor; + + Object.keys(r3rInstance.threeElementDescriptors).forEach(elementDescriptorName => { + const elementDescriptor = r3rInstance.threeElementDescriptors[elementDescriptorName]; + + if (elementDescriptor instanceof Object3DDescriptor) { + // replace their position property to take transitions into account + elementDescriptor.removeProp('position'); + + elementDescriptor.hasProp('position', { + type: propTypeInstanceOf(THREE.Vector3), + update: (threeObject, position) => { + if (threeObject.userData._easePosition) { + if (threeObject.userData._positionTransition) { + threeObject.userData._positionTransition.newTarget(position); + } else { + threeObject.userData._positionTransition = + new PositionTransition(threeObject, position); + + this.activeTransitions.push(threeObject.userData._positionTransition); + } + } else { + threeObject.position.copy(position); + + if (threeObject.userData._lookAt) { + threeObject.lookAt(threeObject.userData._lookAt); + } + } + }, + default: new THREE.Vector3(), + }); + + elementDescriptor.hasProp('easeType', { + type: PropTypes.oneOf(allEaseTypes), + update(threeObject, easeType) { + threeObject.userData._easeType = easeType; + }, + updateInitial: true, + default: 'cubicInOut', + }); + + elementDescriptor.hasProp('transitionDuration', { + type: PropTypes.number, + update(threeObject, transitionDuration) { + threeObject.userData._transitionDuration = transitionDuration; + }, + updateInitial: true, + default: 200, + }); + + elementDescriptor.hasProp('easePosition', { + type: PropTypes.bool, + update(threeObject, easePosition) { + if (!easePosition && threeObject.userData._positionTransition) { + // there is already a transition, force it to end + + threeObject.userData._positionTransition.finish(); + } + + threeObject.userData._easePosition = easePosition; + }, + updateInitial: true, + default: false, + }); + + this.patchedDescriptors.push(elementDescriptorName); + } + }); + } + + update() { + // if the tick returns false then it means it's finished and wants to be removed + // otherwise let them keep ticking every update + this.activeTransitions = this.activeTransitions.filter(transition => transition.tick()); + } + + dispose() { + // UNTESTED, will be called only if you want to remove the module from R3R + + this.activeTransitions.forEach(transition => transition.finish()); + + this.activeTransitions = []; + + this.patchedDescriptors.forEach(elementDescriptorName => { + const DescriptorConstructor = this.react3RendererInstance + .threeElementDescriptors[elementDescriptorName].constructor; + + // reconstruct all patched descriptors! ( UNTESTED ) + this.react3RendererInstance.threeElementDescriptors[elementDescriptorName] = + new DescriptorConstructor(this.react3RendererInstance); + }); + } +} + +export default TransitionExample; diff --git a/src/examples/PositionTransition/index.js b/src/examples/PositionTransition/index.js new file mode 100644 index 0000000..6c076f0 --- /dev/null +++ b/src/examples/PositionTransition/index.js @@ -0,0 +1,212 @@ +import React from 'react'; +import React3 from 'react-three-renderer'; +import THREE from 'three'; +import TransitionExampleModule from './TransitionExampleModule'; + +import eases from 'eases'; + +const transitionTypes = Object.keys(eases); + +class PositionTransitionExample extends React.Component { + static propTypes = { + width: React.PropTypes.number.isRequired, + height: React.PropTypes.number.isRequired, + }; + + constructor(props, context) { + super(props, context); + + this.cameraPosition = new THREE.Vector3(0, 0, 5); + this.groupOffset = new THREE.Vector3(0, 0, 0.125); + + this.state = { + firstCubePosition: new THREE.Vector3(-1, 0, 0), + firstTransitionType: 'cubicInOut', + firstTransitionDuration: 2000, + + secondTransitionType: 'linear', + secondCubePosition: new THREE.Vector3(1, 0, 0), + secondTransitionDuration: 4000, + + yOffset: 2, + }; + + [ + 'moveClicked', + + 'onFirstSelectionChange', + 'onFirstTransitionDurationChange', + + 'onSecondSelectionChange', + 'onSecondTransitionDurationChange', + ].forEach(functionName => { + this[functionName] = this[functionName].bind(this); + }); + } + + onFirstSelectionChange(event) { + this.setState({ + firstTransitionType: event.target.value, + }); + } + + onFirstTransitionDurationChange(event) { + this.setState({ + firstTransitionDuration: +event.target.value, + }); + } + + onSecondSelectionChange(event) { + this.setState({ + secondTransitionType: event.target.value, + }); + } + + onSecondTransitionDurationChange(event) { + this.setState({ + secondTransitionDuration: +event.target.value, + }); + } + + moveClicked() { + this.setState({ + yOffset: -this.state.yOffset, + firstCubePosition: new THREE.Vector3(-1, this.state.yOffset, 0), + secondCubePosition: new THREE.Vector3(1, this.state.yOffset, 0), + }); + } + + render() { + const { + width, + height, + } = this.props; + + const controls = (