camera-2d-simple
Version:
2D camera for WebGL
238 lines (201 loc) • 6.23 kB
JavaScript
import { mat4, vec4 } from 'gl-matrix';
const createCamera = (
initTarget = [0, 0],
initDistance = 1,
initRotation = 0,
initViewCenter = [0, 0],
initScaleBounds = [
[0, Infinity],
[0, Infinity],
],
initTranslationBounds = [
[-Infinity, Infinity],
[-Infinity, Infinity],
]
) => {
// Scratch variables
const scratch0 = new Float32Array(16);
const scratch1 = new Float32Array(16);
const scratch2 = new Float32Array(16);
let view = mat4.create();
let viewCenter = [...initViewCenter.slice(0, 2), 0, 1];
const scaleXBounds = Array.isArray(initScaleBounds[0])
? [...initScaleBounds[0]]
: [...initScaleBounds];
const scaleYBounds = Array.isArray(initScaleBounds[0])
? [...initScaleBounds[1]]
: [...initScaleBounds];
const translationXBounds = Array.isArray(initTranslationBounds[0])
? [...initTranslationBounds[0]]
: [...initTranslationBounds];
const translationYBounds = Array.isArray(initTranslationBounds[0])
? [...initTranslationBounds[1]]
: [...initTranslationBounds];
const getScaling = () => mat4.getScaling(scratch0, view).slice(0, 2);
const getMinScaling = () => {
const scaling = getScaling();
return Math.min(scaling[0], scaling[1]);
};
const getMaxScaling = () => {
const scaling = getScaling();
return Math.max(scaling[0], scaling[1]);
};
const getRotation = () => Math.acos(view[0] / getMaxScaling());
const getScaleBounds = () => [[...scaleXBounds], [...scaleYBounds]];
const getTranslationBounds = () => [
[...translationXBounds],
[...translationYBounds],
];
const getDistance = () => {
const scaling = getScaling();
return [1 / scaling[0], 1 / scaling[1]];
};
const getMinDistance = () => 1 / getMinScaling();
const getMaxDistance = () => 1 / getMaxScaling();
const getTranslation = () => mat4.getTranslation(scratch0, view).slice(0, 2);
const getTarget = () =>
vec4
.transformMat4(scratch0, viewCenter, mat4.invert(scratch2, view))
.slice(0, 2);
const getView = () => view;
const getViewCenter = () => viewCenter.slice(0, 2);
const lookAt = ([x = 0, y = 0] = [], newDistance = 1, newRotation = 0) => {
// Reset the view
view = mat4.create();
translate([-x, -y]);
rotate(newRotation);
scale(1 / newDistance);
};
const translate = ([x = 0, y = 0] = []) => {
scratch0[0] = x;
scratch0[1] = y;
scratch0[2] = 0;
const t = mat4.fromTranslation(scratch1, scratch0);
// Translate about the viewport center
// This is identical to `i * t * i * view` where `i` is the identity matrix
mat4.multiply(view, t, view);
};
const scale = (d, mousePos) => {
const isArray = Array.isArray(d);
let dx = isArray ? d[0] : d;
let dy = isArray ? d[1] : d;
if (dx <= 0 || dy <= 0 || (dx === 1 && dy === 1)) return;
const scaling = getScaling();
const newXScale = scaling[0] * dx;
const newYScale = scaling[1] * dy;
dx =
Math.max(scaleXBounds[0], Math.min(newXScale, scaleXBounds[1])) /
scaling[0];
dy =
Math.max(scaleYBounds[0], Math.min(newYScale, scaleYBounds[1])) /
scaling[1];
if (dx === 1 && dy === 1) return; // There is nothing to do
scratch0[0] = dx;
scratch0[1] = dy;
scratch0[2] = 1;
const s = mat4.fromScaling(scratch1, scratch0);
const scaleCenter = mousePos ? [...mousePos, 0] : viewCenter;
const a = mat4.fromTranslation(scratch0, scaleCenter);
// Translate about the scale center
// I.e., the mouse position or the view center
mat4.multiply(
view,
a,
mat4.multiply(
view,
s,
mat4.multiply(view, mat4.invert(scratch2, a), view)
)
);
};
const rotate = (rad) => {
const r = mat4.create();
mat4.fromRotation(r, rad, [0, 0, 1]);
// Rotate about the viewport center
// This is identical to `i * r * i * view` where `i` is the identity matrix
mat4.multiply(view, r, view);
};
const setScaleBounds = (newBounds) => {
const isArray = Array.isArray(newBounds[0]);
scaleXBounds[0] = isArray ? newBounds[0][0] : newBounds[0];
scaleXBounds[1] = isArray ? newBounds[0][1] : newBounds[1];
scaleYBounds[0] = isArray ? newBounds[1][0] : newBounds[0];
scaleYBounds[1] = isArray ? newBounds[1][1] : newBounds[1];
};
const setTranslationBounds = (newBounds) => {
const isArray = Array.isArray(newBounds[0]);
translationXBounds[0] = isArray ? newBounds[0][0] : newBounds[0];
translationXBounds[1] = isArray ? newBounds[0][1] : newBounds[1];
translationYBounds[0] = isArray ? newBounds[1][0] : newBounds[0];
translationYBounds[1] = isArray ? newBounds[1][1] : newBounds[1];
};
const setView = (newView) => {
if (!newView || newView.length < 16) return;
view = newView;
};
const setViewCenter = (newViewCenter) => {
viewCenter = [...newViewCenter.slice(0, 2), 0, 1];
};
const reset = () => {
lookAt(initTarget, initDistance, initRotation);
};
// Init
lookAt(initTarget, initDistance, initRotation);
return {
get translation() {
return getTranslation();
},
get target() {
return getTarget();
},
get scaling() {
return getScaling();
},
get minScaling() {
return getMinScaling();
},
get maxScaling() {
return getMaxScaling();
},
get scaleBounds() {
return getScaleBounds();
},
get translationBounds() {
return getTranslationBounds();
},
get distance() {
return getDistance();
},
get minDistance() {
return getMinDistance();
},
get maxDistance() {
return getMaxDistance();
},
get rotation() {
return getRotation();
},
get view() {
return getView();
},
get viewCenter() {
return getViewCenter();
},
lookAt,
translate,
pan: translate,
rotate,
scale,
zoom: scale,
reset,
set: (...args) => {
console.warn('`set()` is deprecated. Please use `setView()` instead.');
return setView(...args);
},
setScaleBounds,
setTranslationBounds,
setView,
setViewCenter,
};
};
export { createCamera as default };