diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/README.md b/README.md index b353bec..928d919 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ ThreeRenderObjects({ configOptions })() | Config options | Description | Default | | --- | --- | :--: | -| controlType: str | Which type of control to use to control the camera. Choice between [trackball](https://threejs.org/examples/misc_controls_trackball.html), [orbit](https://threejs.org/examples/#misc_controls_orbit) or [fly](https://threejs.org/examples/misc_controls_fly.html). | `trackball` | +| controlType: str or (camera, domElement) => Controller | Which type of control to use to control the camera. Choice between [trackball](https://threejs.org/examples/misc_controls_trackball.html), [orbit](https://threejs.org/examples/#misc_controls_orbit) or [fly](https://threejs.org/examples/misc_controls_fly.html) or pass your own controller as a function. | `trackball` | | rendererConfig: object | Configuration parameters to pass to the [ThreeJS WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) constructor. | `{ antialias: true, alpha: true }` | | extraRenderers: array | If you wish to include objects that require a dedicated renderer besides `WebGL`, such as [CSS3DRenderer](https://threejs.org/docs/#examples/en/renderers/CSS3DRenderer), include in this array those extra renderer instances. | `[]` | | waitForLoadComplete: boolean | Whether to wait until all the asynchronous loading operations are finished (such as the background image) before rendering the objects in the scene for the first time. | `true` | diff --git a/example/custom-controller/controller.js b/example/custom-controller/controller.js new file mode 100644 index 0000000..970d66b --- /dev/null +++ b/example/custom-controller/controller.js @@ -0,0 +1,314 @@ +const _lookDirection = new THREE.Vector3(); +const _spherical = new THREE.Spherical(); +const _target = new THREE.Vector3(); + +class FirstPersonControls { + constructor(object, domElement) { + if (domElement === undefined) { + console.warn( + 'THREE.FirstPersonControls: The second parameter "domElement" is now mandatory.' + ); + domElement = document; + } + + this.object = object; + this.domElement = domElement; + + // API + + this.enabled = true; + + this.movementSpeed = 1.0; + this.lookSpeed = 0.005; + + this.lookVertical = true; + this.autoForward = false; + + this.activeLook = true; + + this.heightSpeed = false; + this.heightCoef = 1.0; + this.heightMin = 0.0; + this.heightMax = 1.0; + + this.constrainVertical = false; + this.verticalMin = 0; + this.verticalMax = Math.PI; + + this.mouseDragOn = false; + + // internals + + this.autoSpeedFactor = 0.0; + + this.mouseX = 0; + this.mouseY = 0; + + this.moveForward = false; + this.moveBackward = false; + this.moveLeft = false; + this.moveRight = false; + + this.viewHalfX = 0; + this.viewHalfY = 0; + + // resizing + + const resizeObserver = new ResizeObserver(() => { + this.handleResize(); + }); + + // private variables + + let lat = 0; + let lon = 0; + + this.handleResize = function () { + if (this.domElement === document) { + this.viewHalfX = window.innerWidth / 2; + this.viewHalfY = window.innerHeight / 2; + } else { + this.viewHalfX = this.domElement.offsetWidth / 2; + this.viewHalfY = this.domElement.offsetHeight / 2; + } + }; + + this.onMouseDown = function (event) { + if (this.domElement !== document) { + this.domElement.focus(); + } + + if (this.activeLook) { + switch (event.button) { + case 0: + this.moveForward = true; + break; + case 2: + this.moveBackward = true; + break; + } + } + + this.mouseDragOn = true; + }; + + this.onMouseUp = function (event) { + if (this.activeLook) { + switch (event.button) { + case 0: + this.moveForward = false; + break; + case 2: + this.moveBackward = false; + break; + } + } + + this.mouseDragOn = false; + }; + + this.onMouseMove = function (event) { + if (this.domElement === document) { + this.mouseX = event.pageX - this.viewHalfX; + this.mouseY = event.pageY - this.viewHalfY; + } else { + this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX; + this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY; + } + }; + + this.onKeyDown = function (event) { + console.log("DEBUG keydown", event); + + switch (event.code) { + case "ArrowUp": + case "KeyW": + this.moveForward = true; + break; + + case "ArrowLeft": + case "KeyA": + this.moveLeft = true; + break; + + case "ArrowDown": + case "KeyS": + this.moveBackward = true; + break; + + case "ArrowRight": + case "KeyD": + this.moveRight = true; + break; + + case "KeyR": + this.moveUp = true; + break; + case "KeyF": + this.moveDown = true; + break; + } + }; + + this.onKeyUp = function (event) { + switch (event.code) { + case "ArrowUp": + case "KeyW": + this.moveForward = false; + break; + + case "ArrowLeft": + case "KeyA": + this.moveLeft = false; + break; + + case "ArrowDown": + case "KeyS": + this.moveBackward = false; + break; + + case "ArrowRight": + case "KeyD": + this.moveRight = false; + break; + + case "KeyR": + this.moveUp = false; + break; + case "KeyF": + this.moveDown = false; + break; + } + }; + + this.lookAt = function (x, y, z) { + if (x.isVector3) { + _target.copy(x); + } else { + _target.set(x, y, z); + } + + this.object.lookAt(_target); + + setOrientation(this); + + return this; + }; + + this.update = (function () { + const targetPosition = new THREE.Vector3(); + + return function update(delta) { + if (this.enabled === false) return; + + if (this.heightSpeed) { + const y = THREE.MathUtils.clamp( + this.object.position.y, + this.heightMin, + this.heightMax + ); + const heightDelta = y - this.heightMin; + + this.autoSpeedFactor = delta * (heightDelta * this.heightCoef); + } else { + this.autoSpeedFactor = 0.0; + } + + const actualMoveSpeed = delta * this.movementSpeed; + + if (this.moveForward || (this.autoForward && !this.moveBackward)) + this.object.translateZ(-(actualMoveSpeed + this.autoSpeedFactor)); + if (this.moveBackward) this.object.translateZ(actualMoveSpeed); + + if (this.moveLeft) this.object.translateX(-actualMoveSpeed); + if (this.moveRight) this.object.translateX(actualMoveSpeed); + + if (this.moveUp) this.object.translateY(actualMoveSpeed); + if (this.moveDown) this.object.translateY(-actualMoveSpeed); + + let actualLookSpeed = delta * this.lookSpeed; + + if (!this.activeLook) { + actualLookSpeed = 0; + } + + let verticalLookRatio = 1; + + if (this.constrainVertical) { + verticalLookRatio = Math.PI / (this.verticalMax - this.verticalMin); + } + + lon -= this.mouseX * actualLookSpeed; + if (this.lookVertical) + lat -= this.mouseY * actualLookSpeed * verticalLookRatio; + + lat = Math.max(-85, Math.min(85, lat)); + + let phi = THREE.MathUtils.degToRad(90 - lat); + const theta = THREE.MathUtils.degToRad(lon); + + if (this.constrainVertical) { + phi = THREE.MathUtils.mapLinear( + phi, + 0, + Math.PI, + this.verticalMin, + this.verticalMax + ); + } + + const position = this.object.position; + + targetPosition.setFromSphericalCoords(1, phi, theta).add(position); + + this.object.lookAt(targetPosition); + }; + })(); + + this.dispose = function () { + this.domElement.removeEventListener("contextmenu", contextmenu); + this.domElement.removeEventListener("mousedown", _onMouseDown); + this.domElement.removeEventListener("mousemove", _onMouseMove); + this.domElement.removeEventListener("mouseup", _onMouseUp); + + window.removeEventListener("keydown", _onKeyDown); + window.removeEventListener("keyup", _onKeyUp); + + resizeObserver.disconnect(); + }; + + const _onMouseMove = this.onMouseMove.bind(this); + const _onMouseDown = this.onMouseDown.bind(this); + const _onMouseUp = this.onMouseUp.bind(this); + const _onKeyDown = this.onKeyDown.bind(this); + const _onKeyUp = this.onKeyUp.bind(this); + + this.domElement.addEventListener("contextmenu", contextmenu); + this.domElement.addEventListener("mousemove", _onMouseMove); + this.domElement.addEventListener("mousedown", _onMouseDown); + this.domElement.addEventListener("mouseup", _onMouseUp); + + window.addEventListener("keydown", _onKeyDown); + window.addEventListener("keyup", _onKeyUp); + + function setOrientation(controls) { + const quaternion = controls.object.quaternion; + + _lookDirection.set(0, 0, -1).applyQuaternion(quaternion); + _spherical.setFromVector3(_lookDirection); + + lat = 90 - THREE.MathUtils.radToDeg(_spherical.phi); + lon = THREE.MathUtils.radToDeg(_spherical.theta); + } + + this.handleResize(); + + resizeObserver.observe(domElement); + + setOrientation(this); + } +} + +function contextmenu(event) { + event.preventDefault(); +} diff --git a/example/custom-controller/index.html b/example/custom-controller/index.html new file mode 100644 index 0000000..2f18204 --- /dev/null +++ b/example/custom-controller/index.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + +
+ + + diff --git a/src/three-render-objects.js b/src/three-render-objects.js index 72c0e5a..0211476 100644 --- a/src/three-render-objects.js +++ b/src/three-render-objects.js @@ -291,14 +291,20 @@ export default Kapsule({ state.container.className = 'scene-container'; state.container.style.position = 'relative'; + // Get controller type + const controlTypeIsString = typeof controlType === typeof 'string'; + const controlTypeIsFunction = typeof controlType === typeof (() => {}); + // Add nav info section state.container.appendChild(state.navInfo = document.createElement('div')); state.navInfo.className = 'scene-nav-info'; - state.navInfo.textContent = { - orbit: 'Left-click: rotate, Mouse-wheel/middle-click: zoom, Right-click: pan', - trackball: 'Left-click: rotate, Mouse-wheel/middle-click: zoom, Right-click: pan', - fly: 'WASD: move, R|F: up | down, Q|E: roll, up|down: pitch, left|right: yaw' - }[controlType] || ''; + if (controlTypeIsString) { + state.navInfo.textContent = { + orbit: 'Left-click: rotate, Mouse-wheel/middle-click: zoom, Right-click: pan', + trackball: 'Left-click: rotate, Mouse-wheel/middle-click: zoom, Right-click: pan', + fly: 'WASD: move, R|F: up | down, Q|E: roll, up|down: pitch, left|right: yaw' + }[controlType] || ''; + } state.navInfo.style.display = state.showNavInfo ? null : 'none'; // Setup tooltip @@ -388,34 +394,43 @@ export default Kapsule({ state.postProcessingComposer = new ThreeEffectComposer(state.renderer); state.postProcessingComposer.addPass(new ThreeRenderPass(state.scene, state.camera)); // render scene as first pass - // configure controls - state.controls = new ({ - trackball: ThreeTrackballControls, - orbit: ThreeOrbitControls, - fly: ThreeFlyControls - }[controlType])(state.camera, state.renderer.domElement); - - if (controlType === 'fly') { - state.controls.movementSpeed = 300; - state.controls.rollSpeed = Math.PI / 6; - state.controls.dragToLook = true; + // create controls + if (controlTypeIsString) { + state.controls = new ({ + trackball: ThreeTrackballControls, + orbit: ThreeOrbitControls, + fly: ThreeFlyControls + }[controlType])(state.camera, state.renderer.domElement); + } else if (controlTypeIsFunction) { + state.controls = controlType(state.camera, state.renderer.domElement); + } else { + throw "Invalid controlType argument"; } - if (controlType === 'trackball' || controlType === 'orbit') { - state.controls.minDistance = 0.1; - state.controls.maxDistance = state.skyRadius; - state.controls.addEventListener('start', () => { - state.controlsEngaged = true; - }); - state.controls.addEventListener('change', () => { - if (state.controlsEngaged) { - state.controlsDragging = true; - } - }); - state.controls.addEventListener('end', () => { - state.controlsEngaged = false; - state.controlsDragging = false; - }); + // configure controls + if (controlTypeIsString) { + if (controlType === 'fly') { + state.controls.movementSpeed = 300; + state.controls.rollSpeed = Math.PI / 6; + state.controls.dragToLook = true; + } + + if (controlType === 'trackball' || controlType === 'orbit') { + state.controls.minDistance = 0.1; + state.controls.maxDistance = state.skyRadius; + state.controls.addEventListener('start', () => { + state.controlsEngaged = true; + }); + state.controls.addEventListener('change', () => { + if (state.controlsEngaged) { + state.controlsDragging = true; + } + }); + state.controls.addEventListener('end', () => { + state.controlsEngaged = false; + state.controlsDragging = false; + }); + } } [state.renderer, state.postProcessingComposer, ...state.extraRenderers]