Published unlisted
Edited
Sep 15, 2021
3 forks
21 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
cameraController
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class CameraController {
constructor(map) {
this.map = map;
this.raf = null;
this.currentLookAt = mapboxgl.MercatorCoordinate.fromLngLat(
map.transform.center
);
this.previousLookAt = this.currentLookAt;
this.previousLookAtTime = NaN;
this.targetLookAt = this.currentLookAt;
this.targetDistance = 1100; // km
this.targetPitch = 500;
this.targetBearing = 0;
this.dirty = true;
this.previousTime = NaN;
this.accumError = [0, 0, 0];
this.Kp_pan = 0;
this.Ki_pan = 0;
this.Kd_pan = 0;
this.Kp_pitch = 0;
this.Kp_dist = 1;
this.Kp_bearing = 1;
this.followStrength = 1;
}

setTargetDistance(value) {
this.targetDistance = value;
}

setTargetPitch(value) {
this.targetPitch = value;
}

setTargetBearing(value) {
this.targetBearing = value;
}

setCenter(center) {
this.targetCenter = center;
}

start() {
if (this.raf) return;
this.raf = requestAnimationFrame(this._frame.bind(this));
}

stop() {
if (!this.raf) return;
cancelAnimationFrame(this.raf);
this.raf = null;
}

setKi(value) {
this.Ki = value;
}

setPanPID(P = 1, I = 0, D = 0) {
this.Kp_pan = P;
this.Ki_pan = I;
this.Kd_pan = D;
}

setPitchP(P = 1) {
this.Kp_pitch = P;
}

setDistP(P = 1) {
this.Kp_dist = P;
}

setBearingP(P = 1) {
this.Kp_bearing = P;
}

setFollowStrength(value) {
this.followStrength = value;
}

_step(t) {
const dt = isNaN(this.previousTime)
? 0
: Math.min(t / 1000 - this.previousTime, 32 / 1000);
this.previousTime = t / 1000;
const cam = map.getFreeCameraOptions();

const mLookAt = new mapboxgl.MercatorCoordinate(
this.currentLookAt.x,
this.currentLookAt.y,
this.currentLookAt.z
);

const mLookTarget = this.targetCenter;
const mCamera = cam.position;
const toMeters = 1 / mLookTarget.meterInMercatorCoordinateUnits();
const mPanErr = [
mLookTarget.x - mLookAt.x,
mLookTarget.y - mLookAt.y,
mLookTarget.z - mLookAt.z
];
const mdist = vec3length(mPanErr);
const targetErrorMeters = mdist * toMeters;

this.accumError[0] += mPanErr[0] * dt;
this.accumError[1] += mPanErr[1] * dt;
this.accumError[2] += mPanErr[2] * dt;

const sky = [0, 0, 1];
const vcam = [
mLookTarget.x - mCamera.x,
mLookTarget.y - mCamera.y,
mLookTarget.z - mCamera.z
];
const e0cam = vec3normalize([], vcam);
const e1cam = vec3normalize([], vec3cross([], e0cam, sky));
const e2cam = vec3cross([], e1cam, e0cam); // already normalized

const camDist = vec3length(vcam) * toMeters;
const camDistForce = 1.0 - camDist / this.targetDistance;

const camPitch = Math.acos(-vcam[2] / vec3length(vcam));
const pitchForce = this.targetPitch * (Math.PI / 180) - camPitch;

const bearingTarget = [
Math.cos(this.targetBearing * (Math.PI / 180)),
Math.sin(this.targetBearing * (Math.PI / 180))
];
const curBearing = vec2normalize([], [e0cam[0], e0cam[1]]);

//console.log(bearingTarget, curBearing);
const b = vec2dot(bearingTarget, curBearing) * Math.sin(camPitch);
const bearingForce = [-b * curBearing[1], b * curBearing[0]];

const lx = mLookAt.x;
const ly = mLookAt.y;
const lz = mLookAt.z;
if (
targetErrorMeters > 0.001 ||
Math.abs(camDistForce) > 0.001 ||
Math.abs(pitchForce) > 0.1 ||
Math.abs(b) > 1e-4
) {
const distp = this.Kp_dist * (camDist / toMeters);
const pitchp = this.Kp_pitch * (camDist / toMeters);
const bearingp = this.Kp_bearing * (camDist / toMeters);
const pani = this.Ki_pan;
const panp = this.Kp_pan;
const pand = this.Kd_pan;

let dlookdt = [0, 0, 0];
if (!isNaN(this.previousLookAtTime) && t > this.previousLookAtTime) {
let dt = t - this.previousLookAtTime;
dlookdt[0] = (mLookAt.x - this.previousLookAt.x) / dt;
dlookdt[1] = (mLookAt.y - this.previousLookAt.y) / dt;
dlookdt[2] = (mLookAt.z - this.previousLookAt.z) / dt;
}

let e = this.accumError;
const panx = (mPanErr[0] * panp + e[0] * pani + dlookdt[0] * pand) * dt;
const pany = (mPanErr[1] * panp + e[1] * pani + dlookdt[1] * pand) * dt;
const panz = (mPanErr[2] * panp + e[2] * pani + dlookdt[2] * pand) * dt;
mLookAt.x += panx;
mLookAt.y += pany;
mLookAt.z += panz;

mCamera.x += panx * (1 - this.followStrength);
mCamera.y += pany * (1 - this.followStrength);
mCamera.z += panz * (1 - this.followStrength);

mCamera.x -= e2cam[0] * pitchForce * pitchp * dt;
mCamera.y -= e2cam[1] * pitchForce * pitchp * dt;
mCamera.z -= e2cam[2] * pitchForce * pitchp * dt;

mCamera.x += bearingForce[0] * bearingp * dt;
mCamera.y += bearingForce[1] * bearingp * dt;

vec3normalize(vcam, vcam);
mCamera.x -= vcam[0] * camDistForce * distp * dt;
mCamera.y -= vcam[1] * camDistForce * distp * dt;
mCamera.z -= vcam[2] * camDistForce * distp * dt;

this.currentLookAt = mLookAt;
cam.lookAtPoint(this.currentLookAt.toLngLat());
map.setFreeCameraOptions(cam);
}

if (isNaN(this.previousLookAtTime) || t > this.previousLookAtTime) {
this.previousLookAt = new mapboxgl.MercatorCoordinate(lx, ly, lz);
this.previousLookAtTime = t;
}
}

_frame(t) {
this._step(t);
this.raf = requestAnimationFrame(this._frame.bind(this));
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more