UNPKG

camera-2d-simple

Version:
238 lines (201 loc) 6.23 kB
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 };