Skip to content

Instantly share code, notes, and snippets.

@GrantCuster
Last active January 10, 2023 08:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GrantCuster/583ca9e235f4971fdc7bce27a652c5c9 to your computer and use it in GitHub Desktop.
Save GrantCuster/583ca9e235f4971fdc7bce27a652c5c9 to your computer and use it in GitHub Desktop.
Three.js with D3-style Pan and Zoom

This example recreates the zoom and pan behavior from the Pan and Zoom II D3 example. I'm using Three.js in a 2D data-visualization project because I'm hoping its WebGL render can handle more points than canvas. I had a hard time finding an example that zoomed and panned like I'm used to, so I made this.

This stack overflow answer by WesleyJones and the getCurrentScale function from anvaka's three.map.controls code was essential to me actually getting this working.

The scroll zoom is currently inverted compared to the D3 example, I'd like to match it but apparently there's some easing happening in the D3 scroll behavior that slows things down as you zoom in. That makes it feel weird if I just invert the zoom.

If you have suggestions on how to make this better, message me on Twitter.

<!DOCTYPE html>
<meta charset="utf-8">
<body>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="//rawgit.com/mrdoob/stats.js/master/build/stats.min.js"></script>
<script>
const width = 960;
const height = 500;
// Add canvas
let renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
// Add stats box
stats = new Stats();
document.body.appendChild(stats.dom);
// Set up camera and scene
let camera = new THREE.PerspectiveCamera(
45,
width / height,
1,
300
);
camera.position.set(0, 0, 125);
camera.lookAt(new THREE.Vector3(0,0,0));
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
// Generate points and add them to scene
const generated_points = d3.range(2000).map(phyllotaxis(2));
const pointsGeometry = new THREE.Geometry();
for (const point of generated_points) {
const vertex = new THREE.Vector3(point[0], point[1], 0);
pointsGeometry.vertices.push(vertex);
}
// Setting sizeAttenuation to false soved z-fighting problems I was having
const pointsMaterial = new THREE.PointsMaterial({ color: 0x000000, size: 6,
sizeAttenuation: false });
const points = new THREE.Points(pointsGeometry, pointsMaterial);
const pointsContainer = new THREE.Object3D();
pointsContainer.add(points);
scene.add(pointsContainer);
// Set up zoom behavior
const zoom = d3.zoom()
.scaleExtent([10, 300])
.on('zoom', () => {
const event = d3.event;
if (event.sourceEvent) {
// Get z from D3
const new_z = event.transform.k;
if (new_z !== camera.position.z) {
// Handle a zoom event
const { clientX, clientY } = event.sourceEvent;
// Project a vector from current mouse position and zoom level
// Find the x and y coordinates for where that vector intersects the new
// zoom level.
// Code from WestLangley https://stackoverflow.com/questions/13055214/mouse-canvas-x-y-to-three-js-world-x-y-z/13091694#13091694
const vector = new THREE.Vector3(
clientX / width * 2 - 1,
- (clientY / height) * 2 + 1,
1
);
vector.unproject(camera);
const dir = vector.sub(camera.position).normalize();
const distance = (new_z - camera.position.z)/dir.z;
const pos = camera.position.clone().add(dir.multiplyScalar(distance));
// Set the camera to new coordinates
camera.position.set(pos.x, pos.y, new_z);
} else {
// Handle panning
const { movementX, movementY } = event.sourceEvent;
// Adjust mouse movement by current scale and set camera
const current_scale = getCurrentScale();
camera.position.set(camera.position.x - movementX/current_scale, camera.position.y +
movementY/current_scale, camera.position.z);
}
}
});
// Add zoom listener
const view = d3.select(renderer.domElement);
view.call(zoom);
// Disable double click to zoom because I'm not handling it in Three.js
view.on('dblclick.zoom', null);
// Sync d3 zoom with camera z position
zoom.scaleTo(view, 125);
// Three.js render loop
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
stats.update();
}
animate();
// From https://github.com/anvaka/three.map.control, used for panning
function getCurrentScale() {
var vFOV = camera.fov * Math.PI / 180
var scale_height = 2 * Math.tan( vFOV / 2 ) * camera.position.z
var currentScale = height / scale_height
return currentScale
}
// Point generator function
function phyllotaxis(radius) {
const theta = Math.PI * (3 - Math.sqrt(5));
return function(i) {
const r = radius * Math.sqrt(i), a = theta * i;
return [
width / 2 + r * Math.cos(a) - width / 2,
height / 2 + r * Math.sin(a) - height / 2
];
};
}
</script>
@geohuz
Copy link

geohuz commented Jul 21, 2021

This is wonderful!

@dagumak
Copy link

dagumak commented Jan 10, 2023

This is awesome! I was looking at it, and noticed there's a wiggle when you zoom in and out. Do you have an idea for why the points "wiggle"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment