Skip to content

Commit

Permalink
chore: add a WebWorker example
Browse files Browse the repository at this point in the history
  • Loading branch information
yomotsu committed Feb 14, 2024
1 parent fe99ab4 commit e09d1b8
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 0 deletions.
93 changes: 93 additions & 0 deletions examples/PseudoElement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { EventDispatcher } from '../dist/camera-controls.module.js';

/**
* Represents a pseudo element, works in WebWorker.
* @extends EventDispatcher
*/
export class PseudoElement extends EventDispatcher {

/** @type {PseudoDocument} */
ownerDocument = new PseudoDocument();
/** @type {any} */
style = {};
/** @private @type {DOMRect} */
_domRect = new DOMRect();

constructor() {

super();

}

/**
* @returns {DOMRect} The DOMRect.
*/
getBoundingClientRect() {

return DOMRect.fromRect( this._domRect );

}

/**
* Just for compatibility. doesn't work.
* @returns {Promise<void>} A promise that rejects.
*/
requestPointerLock() {

return Promise.reject();

}

/**
* Just for compatibility. do nothing.
* @param {string} _ The attribute name.
* @param {string} __ The attribute value.
*/
setAttribute( _, __ ) {}

/**
* Updates the PseudoElement size based on a given bound.
* @param {DOMRect} bound The new bound.
*/
update( bound ) {

this._domRect.x = bound.x;
this._domRect.y = bound.y;
this._domRect.width = bound.width;
this._domRect.height = bound.height;

}

}

/**
* Represents a pseudo document.
* @extends EventDispatcher
*/
class PseudoDocument extends EventDispatcher {

/**
* Creates an instance of PseudoDocument.
*/
constructor() {

super();

}

/**
* Exits pointer lock.
*/
exitPointerLock() {}

/**
* Just for compatibility. do nothing.
* @returns {PseudoElement|null} The element locking the pointer.
*/
get pointerLockElement() {

return null;

}

}
104 changes: 104 additions & 0 deletions examples/worker.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>=^.^=</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="info">
<p><a href="https://github.com/yomotsu/camera-controls">GitHub repo</a></p>
<button id="resetButton">reset</button>
</div>

<canvas></canvas>

<script type="module">
const canvas = document.querySelector( 'canvas' );
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const canvasBounds = canvas.getBoundingClientRect();
const offscreenCanvas = canvas.transferControlToOffscreen();

const worker = new Worker( './worker.js', { type: 'module' } );
worker.postMessage( {
action: 'init',
payload: {
canvas: offscreenCanvas,
left: canvasBounds.left,
top: canvasBounds.top,
width: canvasBounds.width,
height: canvasBounds.height,
} },
[ offscreenCanvas ]
);

const onPointerDown = ( event ) => {

const pseudoPointerEvent = {
pointerId: event.pointerId,
pointerType: event.pointerType,
clientX: event.clientX,
clientY: event.clientY,
buttons: event.buttons,
};

canvas.ownerDocument.removeEventListener( 'pointermove', onPointerMove, { passive: false } );
canvas.ownerDocument.removeEventListener( 'pointerup', onPointerUp );

canvas.ownerDocument.addEventListener( 'pointermove', onPointerMove, { passive: false } );
canvas.ownerDocument.addEventListener( 'pointerup', onPointerUp );

worker.postMessage( { action: 'pointerdown', payload: { event: pseudoPointerEvent } } );

};

const onPointerMove = ( event ) => {

if ( event.cancelable ) event.preventDefault();

const pseudoPointerEvent = {
pointerId: event.pointerId,
pointerType: event.pointerType,
clientX: event.clientX,
clientY: event.clientY,
movementX: event.movementX,
movementY: event.movementY,
buttons: event.buttons,
}

worker.postMessage( { action: 'pointermove', payload: { event: pseudoPointerEvent } } );

};

const onPointerUp = ( event ) => {

const pseudoPointerEvent = {
pointerId: event.pointerId,
pointerType: event.pointerType,
};

worker.postMessage( { action: 'pointerup', payload: { event: pseudoPointerEvent } } );

}

const endDragging = () => {

canvas.ownerDocument.removeEventListener( 'pointermove', onPointerMove, { passive: false } );
canvas.ownerDocument.removeEventListener( 'pointerup', onPointerUp );

};

worker.addEventListener( 'message', ( { data } ) => data?.action === 'controlend' && endDragging() );
canvas.addEventListener( 'pointerdown', onPointerDown );


resetButton.addEventListener( 'click', () => worker.postMessage( { action: 'reset' } ) );

// debug log
worker.addEventListener( 'message', ( { data } ) => console.log( data ) );
</script>

</body>
</html>
90 changes: 90 additions & 0 deletions examples/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as THREE from 'https://unpkg.com/three@0.161.0/build/three.module.js';
import CameraControls from '../dist/camera-controls.module.js';
import { PseudoElement } from './PseudoElement.js';

CameraControls.install( { THREE } );

// DOM element doesn't exist in WebWorker. use a virtual element in CameraControls instead.
const pseudoElement = new PseudoElement();
let cameraControls;

self.onmessage = ( { data } ) => {

const { action, payload } = data;

switch ( action ) {

case 'init': {

const { canvas, left, top, width, height } = payload;
canvas.style = { width: '', height: '' };

const clock = new THREE.Clock();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 60, width / height, 0.01, 100 );
camera.position.set( 0, 0, 5 );
const renderer = new THREE.WebGLRenderer( { canvas } );
renderer.setSize( width, height );

pseudoElement.update( { left, top, width, height } );
cameraControls = new CameraControls( camera, pseudoElement, self );
cameraControls.addEventListener( 'controlend', () => self.postMessage( { action: 'controlend' } ) );

const mesh = new THREE.Mesh(
new THREE.BoxGeometry( 1, 1, 1 ),
new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } )
);
scene.add( mesh );

const gridHelper = new THREE.GridHelper( 50, 50 );
gridHelper.position.y = - 1;
scene.add( gridHelper );

renderer.render( scene, camera );

( function anim() {

const delta = clock.getDelta();
// const elapsed = clock.getElapsedTime();
const updated = cameraControls.update( delta );

// if ( elapsed > 30 ) return;

requestAnimationFrame( anim );

if ( updated ) {

renderer.render( scene, camera );
self.postMessage( 'rendered' );

}

} )();

break;

}

case 'pointerdown':
case 'pointermove':
case 'pointerup': {

const { event } = payload;
pseudoElement.dispatchEvent( { type: action, ...event } );
break;

}

case 'reset': {

cameraControls.reset( true );
break;

}

}

// debug log
self.postMessage( data.action );

};
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ A camera control for three.js, similar to THREE.OrbitControls yet supports smoot
- [path animation](https://yomotsu.github.io/camera-controls/examples/path-animation.html) (with [gsap](https://www.npmjs.com/package/gsap))
- [complex transitions with `await`](https://yomotsu.github.io/camera-controls/examples/await-transitions.html)
- [set view padding](https://yomotsu.github.io/camera-controls/examples/padding-with-view-offset.html)
- [WebWorker (OffscreenCanvas)](https://yomotsu.github.io/camera-controls/examples/worker.html)
- [outside of iframe dragging](https://yomotsu.github.io/camera-controls/examples/iframe.html)
- [in react-three-fiber (simplest)](https://codesandbox.io/s/react-three-fiber-camera-controls-4jjor?file=/src/App.tsx)
- [in react-three-fiber (drei official)](https://codesandbox.io/s/sew669) (see [doc](https://github.com/pmndrs/drei#cameracontrols))
Expand Down

0 comments on commit e09d1b8

Please sign in to comment.