Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add documentation and example for custom controller #568

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ And check out the [React bindings](https://github.com/vasturiano/react-force-gra
* [Add external objects to scene](https://vasturiano.github.io/3d-force-graph/example/scene/) ([source](https://github.com/vasturiano/3d-force-graph/blob/master/example/scene/index.html))
* [Bloom Post-Processing Effect](https://vasturiano.github.io/3d-force-graph/example/bloom-effect/) ([source](https://github.com/vasturiano/3d-force-graph/blob/master/example/bloom-effect/index.html))
* [Pause / Resume animation](https://vasturiano.github.io/3d-force-graph/example/pause-resume/) ([source](https://github.com/vasturiano/3d-force-graph/blob/master/example/pause-resume/index.html))
* [Custom Controller](https://vasturiano.github.io/3d-force-graph/example/custom-controller/) ([source](https://github.com/vasturiano/3d-force-graph/blob/master/example/custom-controller/index.html))

## Quick start

Expand Down Expand Up @@ -79,7 +80,7 @@ ForceGraph3d({ configOptions })(<domElement>)

| Config options | Description | Default |
| --- | --- | :--: |
| <b>controlType</b>: <i>str</i> | 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` |
| <b>controlType</b>: <i>str</i> or <i>(camera, domElement) => Controller</i> | 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` |
| <b>rendererConfig</b>: <i>object</i> | Configuration parameters to pass to the [ThreeJS WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) constructor. | `{ antialias: true, alpha: true }` |
| <b>extraRenderers</b>: <i>array</i> | If you wish to include custom 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. | `[]` |

Expand Down
314 changes: 314 additions & 0 deletions example/custom-controller/controller.js
Original file line number Diff line number Diff line change
@@ -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();
}
44 changes: 44 additions & 0 deletions example/custom-controller/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<head>
<style>
body {
margin: 0;
}
</style>

<script src="https://unpkg.com/three"></script>

<!-- <script src="//unpkg.com/3d-force-graph"></script> -->
<script src="../../dist/3d-force-graph.js"></script>

<script src="./controller.js"></script>
</head>

<body>
<div id="3d-graph"></div>

<script>
// Random tree
const N = 300;
const gData = {
nodes: [...Array(N).keys()].map((i) => ({ id: i })),
links: [...Array(N).keys()]
.filter((id) => id)
.map((id) => ({
source: id,
target: Math.round(Math.random() * (id - 1)),
})),
};

const controllerConstructor = (camera, domElement) => {
const controller = new FirstPersonControls(camera, domElement);
controller.movementSpeed = 150;
controller.lookSpeed = 0.1;

return controller;
};

const Graph = ForceGraph3D({ controlType: controllerConstructor })(
document.getElementById("3d-graph")
).graphData(gData);
</script>
</body>