dom-2d-camera
Version:
A wrapper for attaching a 2D camera to a DOM element
605 lines (503 loc) • 20.7 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('gl-matrix')) :
typeof define === 'function' && define.amd ? define(['gl-matrix'], factory) :
(global = global || self, global.createDom2dCamera = factory(global.glMatrix));
}(this, (function (glMatrix) { 'use strict';
var createCamera = function (
initTarget,
initDistance,
initRotation,
initViewCenter,
initScaleBounds,
initTranslationBounds
) {
if ( initTarget === void 0 ) initTarget = [0, 0];
if ( initDistance === void 0 ) initDistance = 1;
if ( initRotation === void 0 ) initRotation = 0;
if ( initViewCenter === void 0 ) initViewCenter = [0, 0];
if ( initScaleBounds === void 0 ) initScaleBounds = [
[0, Infinity],
[0, Infinity] ];
if ( initTranslationBounds === void 0 ) initTranslationBounds = [
[-Infinity, Infinity],
[-Infinity, Infinity] ];
// Scratch variables
var scratch0 = new Float32Array(16);
var scratch1 = new Float32Array(16);
var scratch2 = new Float32Array(16);
var view = glMatrix.mat4.create();
var viewCenter = initViewCenter.slice(0, 2).concat( [0], [1]);
var scaleXBounds = Array.isArray(initScaleBounds[0])
? [].concat( initScaleBounds[0] )
: [].concat( initScaleBounds );
var scaleYBounds = Array.isArray(initScaleBounds[0])
? [].concat( initScaleBounds[1] )
: [].concat( initScaleBounds );
var translationXBounds = Array.isArray(initTranslationBounds[0])
? [].concat( initTranslationBounds[0] )
: [].concat( initTranslationBounds );
var translationYBounds = Array.isArray(initTranslationBounds[0])
? [].concat( initTranslationBounds[1] )
: [].concat( initTranslationBounds );
var getScaling = function () { return glMatrix.mat4.getScaling(scratch0, view).slice(0, 2); };
var getMinScaling = function () {
var scaling = getScaling();
return Math.min(scaling[0], scaling[1]);
};
var getMaxScaling = function () {
var scaling = getScaling();
return Math.max(scaling[0], scaling[1]);
};
var getRotation = function () { return Math.acos(view[0] / getMaxScaling()); };
var getScaleBounds = function () { return [[].concat( scaleXBounds ), [].concat( scaleYBounds )]; };
var getTranslationBounds = function () { return [
[].concat( translationXBounds ),
[].concat( translationYBounds ) ]; };
var getDistance = function () {
var scaling = getScaling();
return [1 / scaling[0], 1 / scaling[1]];
};
var getMinDistance = function () { return 1 / getMinScaling(); };
var getMaxDistance = function () { return 1 / getMaxScaling(); };
var getTranslation = function () { return glMatrix.mat4.getTranslation(scratch0, view).slice(0, 2); };
var getTarget = function () { return glMatrix.vec4
.transformMat4(scratch0, viewCenter, glMatrix.mat4.invert(scratch2, view))
.slice(0, 2); };
var getView = function () { return view; };
var getViewCenter = function () { return viewCenter.slice(0, 2); };
var lookAt = function (ref, newDistance, newRotation) {
if ( ref === void 0 ) ref = [];
var x = ref[0]; if ( x === void 0 ) x = 0;
var y = ref[1]; if ( y === void 0 ) y = 0;
if ( newDistance === void 0 ) newDistance = 1;
if ( newRotation === void 0 ) newRotation = 0;
// Reset the view
view = glMatrix.mat4.create();
translate([-x, -y]);
rotate(newRotation);
scale(1 / newDistance);
};
var translate = function (ref) {
if ( ref === void 0 ) ref = [];
var x = ref[0]; if ( x === void 0 ) x = 0;
var y = ref[1]; if ( y === void 0 ) y = 0;
scratch0[0] = x;
scratch0[1] = y;
scratch0[2] = 0;
var t = glMatrix.mat4.fromTranslation(scratch1, scratch0);
// Translate about the viewport center
// This is identical to `i * t * i * view` where `i` is the identity matrix
glMatrix.mat4.multiply(view, t, view);
};
var scale = function (d, mousePos) {
var isArray = Array.isArray(d);
var dx = isArray ? d[0] : d;
var dy = isArray ? d[1] : d;
if (dx <= 0 || dy <= 0 || (dx === 1 && dy === 1)) { return; }
var scaling = getScaling();
var newXScale = scaling[0] * dx;
var 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;
var s = glMatrix.mat4.fromScaling(scratch1, scratch0);
var scaleCenter = mousePos ? mousePos.concat( [0]) : viewCenter;
var a = glMatrix.mat4.fromTranslation(scratch0, scaleCenter);
// Translate about the scale center
// I.e., the mouse position or the view center
glMatrix.mat4.multiply(
view,
a,
glMatrix.mat4.multiply(
view,
s,
glMatrix.mat4.multiply(view, glMatrix.mat4.invert(scratch2, a), view)
)
);
};
var rotate = function (rad) {
var r = glMatrix.mat4.create();
glMatrix.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
glMatrix.mat4.multiply(view, r, view);
};
var setScaleBounds = function (newBounds) {
var 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];
};
var setTranslationBounds = function (newBounds) {
var 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];
};
var setView = function (newView) {
if (!newView || newView.length < 16) { return; }
view = newView;
};
var setViewCenter = function (newViewCenter) {
viewCenter = newViewCenter.slice(0, 2).concat( [0], [1]);
};
var reset = function () {
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: lookAt,
translate: translate,
pan: translate,
rotate: rotate,
scale: scale,
zoom: scale,
reset: reset,
set: function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
console.warn('`set()` is deprecated. Please use `setView()` instead.');
return setView.apply(void 0, args);
},
setScaleBounds: setScaleBounds,
setTranslationBounds: setTranslationBounds,
setView: setView,
setViewCenter: setViewCenter,
};
};
var MOUSE_DOWN_MOVE_ACTIONS = ["pan", "rotate"];
var KEY_MAP = {
alt: "altKey",
cmd: "metaKey",
ctrl: "ctrlKey",
meta: "metaKey",
shift: "shiftKey"
};
var dom2dCamera = function (
element,
ref
) {
if ( ref === void 0 ) ref = {};
var distance = ref.distance; if ( distance === void 0 ) distance = 1.0;
var target = ref.target; if ( target === void 0 ) target = [0, 0];
var rotation = ref.rotation; if ( rotation === void 0 ) rotation = 0;
var isNdc = ref.isNdc; if ( isNdc === void 0 ) isNdc = true;
var isFixed = ref.isFixed; if ( isFixed === void 0 ) isFixed = false;
var isPan = ref.isPan; if ( isPan === void 0 ) isPan = true;
var isPanInverted = ref.isPanInverted; if ( isPanInverted === void 0 ) isPanInverted = [false, true];
var panSpeed = ref.panSpeed; if ( panSpeed === void 0 ) panSpeed = 1;
var isRotate = ref.isRotate; if ( isRotate === void 0 ) isRotate = true;
var rotateSpeed = ref.rotateSpeed; if ( rotateSpeed === void 0 ) rotateSpeed = 1;
var defaultMouseDownMoveAction = ref.defaultMouseDownMoveAction; if ( defaultMouseDownMoveAction === void 0 ) defaultMouseDownMoveAction = "pan";
var mouseDownMoveModKey = ref.mouseDownMoveModKey; if ( mouseDownMoveModKey === void 0 ) mouseDownMoveModKey = "alt";
var isZoom = ref.isZoom; if ( isZoom === void 0 ) isZoom = true;
var zoomSpeed = ref.zoomSpeed; if ( zoomSpeed === void 0 ) zoomSpeed = 1;
var viewCenter = ref.viewCenter;
var scaleBounds = ref.scaleBounds;
var translationBounds = ref.translationBounds;
var onKeyDown = ref.onKeyDown; if ( onKeyDown === void 0 ) onKeyDown = function () {};
var onKeyUp = ref.onKeyUp; if ( onKeyUp === void 0 ) onKeyUp = function () {};
var onMouseDown = ref.onMouseDown; if ( onMouseDown === void 0 ) onMouseDown = function () {};
var onMouseUp = ref.onMouseUp; if ( onMouseUp === void 0 ) onMouseUp = function () {};
var onMouseMove = ref.onMouseMove; if ( onMouseMove === void 0 ) onMouseMove = function () {};
var onWheel = ref.onWheel; if ( onWheel === void 0 ) onWheel = function () {};
var camera = createCamera(
target,
distance,
rotation,
viewCenter,
scaleBounds,
translationBounds
);
var mouseX = 0;
var mouseY = 0;
var mouseRelX = 0;
var mouseRelY = 0;
var prevMouseX = 0;
var prevMouseY = 0;
var isLeftMousePressed = false;
var scrollDist = 0;
var width = 1;
var height = 1;
var aspectRatio = 1;
var isInteractivelyChanged = false;
var isProgrammaticallyChanged = false;
var isMouseDownMoveModActive = false;
var panOnMouseDownMove = defaultMouseDownMoveAction === "pan";
var isPanX = isPan;
var isPanY = isPan;
var isPanXInverted = isPanInverted;
var isPanYInverted = isPanInverted;
var isZoomX = isZoom;
var isZoomY = isZoom;
var spreadXYSettings = function () {
isPanX = Array.isArray(isPan) ? Boolean(isPan[0]) : isPan;
isPanY = Array.isArray(isPan) ? Boolean(isPan[1]) : isPan;
isPanXInverted = Array.isArray(isPanInverted)
? Boolean(isPanInverted[0])
: isPanInverted;
isPanYInverted = Array.isArray(isPanInverted)
? Boolean(isPanInverted[1])
: isPanInverted;
isZoomX = Array.isArray(isZoom) ? Boolean(isZoom[0]) : isZoom;
isZoomY = Array.isArray(isZoom) ? Boolean(isZoom[1]) : isZoom;
};
spreadXYSettings();
var transformPanX = isNdc
? function (dX) { return (dX / width) * 2 * aspectRatio; } // to normalized device coords
: function (dX) { return dX; };
var transformPanY = isNdc
? function (dY) { return (dY / height) * 2; } // to normalized device coords
: function (dY) { return -dY; };
var transformScaleX = isNdc
? function (x) { return (-1 + (x / width) * 2) * aspectRatio; } // to normalized device coords
: function (x) { return x; };
var transformScaleY = isNdc
? function (y) { return 1 - (y / height) * 2; } // to normalized device coords
: function (y) { return y; };
var tick = function () {
if (isFixed) {
var isChanged$1 = isProgrammaticallyChanged;
isProgrammaticallyChanged = false;
return isChanged$1;
}
isInteractivelyChanged = false;
var currentMouseX = mouseX;
var currentMouseY = mouseY;
if (
(isPanX || isPanY) &&
isLeftMousePressed &&
((panOnMouseDownMove && !isMouseDownMoveModActive) ||
(!panOnMouseDownMove && isMouseDownMoveModActive))
) {
var dX = isPanXInverted
? prevMouseX - currentMouseX
: currentMouseX - prevMouseX;
var transformedPanX = isPanX ? transformPanX(panSpeed * dX) : 0;
var dY = isPanYInverted
? prevMouseY - currentMouseY
: currentMouseY - prevMouseY;
var transformedPanY = isPanY ? transformPanY(panSpeed * dY) : 0;
if (transformedPanX !== 0 || transformedPanY !== 0) {
camera.pan([transformedPanX, transformedPanY]);
isInteractivelyChanged = true;
}
}
if ((isZoomX || isZoomY) && scrollDist) {
var dZ = zoomSpeed * Math.exp(scrollDist / height);
var transformedX = transformScaleX(mouseRelX);
var transformedY = transformScaleY(mouseRelY);
camera.scale(
[isZoomX ? 1 / dZ : 1, isZoomY ? 1 / dZ : 1],
[transformedX, transformedY]
);
isInteractivelyChanged = true;
}
if (
isRotate &&
isLeftMousePressed &&
((panOnMouseDownMove && isMouseDownMoveModActive) ||
(!panOnMouseDownMove && !isMouseDownMoveModActive)) &&
Math.abs(prevMouseX - currentMouseX) +
Math.abs(prevMouseY - currentMouseY) >
0
) {
var wh = width / 2;
var hh = height / 2;
var x1 = prevMouseX - wh;
var y1 = hh - prevMouseY;
var x2 = currentMouseX - wh;
var y2 = hh - currentMouseY;
// Angle between the start and end mouse position with respect to the
// viewport center
var radians = glMatrix.vec2.angle([x1, y1], [x2, y2]);
// Determine the orientation
var cross = x1 * y2 - x2 * y1;
camera.rotate(rotateSpeed * radians * Math.sign(cross));
isInteractivelyChanged = true;
}
// Reset scroll delta and mouse position
scrollDist = 0;
prevMouseX = currentMouseX;
prevMouseY = currentMouseY;
var isChanged = isInteractivelyChanged || isProgrammaticallyChanged;
isProgrammaticallyChanged = false;
return isChanged;
};
var config = function (ref) {
if ( ref === void 0 ) ref = {};
var newDefaultMouseDownMoveAction = ref.defaultMouseDownMoveAction; if ( newDefaultMouseDownMoveAction === void 0 ) newDefaultMouseDownMoveAction = null;
var newIsFixed = ref.isFixed; if ( newIsFixed === void 0 ) newIsFixed = null;
var newIsPan = ref.isPan; if ( newIsPan === void 0 ) newIsPan = null;
var newIsPanInverted = ref.isPanInverted; if ( newIsPanInverted === void 0 ) newIsPanInverted = null;
var newIsRotate = ref.isRotate; if ( newIsRotate === void 0 ) newIsRotate = null;
var newIsZoom = ref.isZoom; if ( newIsZoom === void 0 ) newIsZoom = null;
var newPanSpeed = ref.panSpeed; if ( newPanSpeed === void 0 ) newPanSpeed = null;
var newRotateSpeed = ref.rotateSpeed; if ( newRotateSpeed === void 0 ) newRotateSpeed = null;
var newZoomSpeed = ref.zoomSpeed; if ( newZoomSpeed === void 0 ) newZoomSpeed = null;
var newMouseDownMoveModKey = ref.mouseDownMoveModKey; if ( newMouseDownMoveModKey === void 0 ) newMouseDownMoveModKey = null;
defaultMouseDownMoveAction =
newDefaultMouseDownMoveAction !== null &&
MOUSE_DOWN_MOVE_ACTIONS.includes(newDefaultMouseDownMoveAction)
? newDefaultMouseDownMoveAction
: defaultMouseDownMoveAction;
panOnMouseDownMove = defaultMouseDownMoveAction === "pan";
isFixed = newIsFixed !== null ? newIsFixed : isFixed;
isPan = newIsPan !== null ? newIsPan : isPan;
isPanInverted =
newIsPanInverted !== null ? newIsPanInverted : isPanInverted;
isRotate = newIsRotate !== null ? newIsRotate : isRotate;
isZoom = newIsZoom !== null ? newIsZoom : isZoom;
panSpeed = +newPanSpeed > 0 ? newPanSpeed : panSpeed;
rotateSpeed = +newRotateSpeed > 0 ? newRotateSpeed : rotateSpeed;
zoomSpeed = +newZoomSpeed > 0 ? newZoomSpeed : zoomSpeed;
spreadXYSettings();
mouseDownMoveModKey =
newMouseDownMoveModKey !== null &&
Object.keys(KEY_MAP).includes(newMouseDownMoveModKey)
? newMouseDownMoveModKey
: mouseDownMoveModKey;
};
var refresh = function () {
var bBox = element.getBoundingClientRect();
width = bBox.width;
height = bBox.height;
aspectRatio = width / height;
};
var keyUpHandler = function (event) {
isMouseDownMoveModActive = false;
onKeyUp(event);
};
var keyDownHandler = function (event) {
isMouseDownMoveModActive = event[KEY_MAP[mouseDownMoveModKey]];
onKeyDown(event);
};
var mouseUpHandler = function (event) {
isLeftMousePressed = false;
onMouseUp(event);
};
var mouseDownHandler = function (event) {
isLeftMousePressed = event.buttons === 1;
onMouseDown(event);
};
var offsetXSupport =
document.createEvent("MouseEvent").offsetX !== undefined;
var updateMouseRelXY = offsetXSupport
? function (event) {
mouseRelX = event.offsetX;
mouseRelY = event.offsetY;
}
: function (event) {
var bBox = element.getBoundingClientRect();
mouseRelX = event.clientX - bBox.left;
mouseRelY = event.clientY - bBox.top;
};
var updateMouseXY = function (event) {
mouseX = event.clientX;
mouseY = event.clientY;
};
var mouseMoveHandler = function (event) {
updateMouseXY(event);
onMouseMove(event);
};
var wheelHandler = function (event) {
if ((isZoomX || isZoomY) && !isFixed) {
event.preventDefault();
updateMouseXY(event);
updateMouseRelXY(event);
var scale = event.deltaMode === 1 ? 12 : 1;
scrollDist += scale * (event.deltaY || event.deltaX || 0);
}
onWheel(event);
};
var dispose = function () {
camera = undefined;
window.removeEventListener("keydown", keyDownHandler);
window.removeEventListener("keyup", keyUpHandler);
element.removeEventListener("mousedown", mouseDownHandler);
window.removeEventListener("mouseup", mouseUpHandler);
window.removeEventListener("mousemove", mouseMoveHandler);
element.removeEventListener("wheel", wheelHandler);
};
window.addEventListener("keydown", keyDownHandler, { passive: true });
window.addEventListener("keyup", keyUpHandler, { passive: true });
element.addEventListener("mousedown", mouseDownHandler, { passive: true });
window.addEventListener("mouseup", mouseUpHandler, { passive: true });
window.addEventListener("mousemove", mouseMoveHandler, { passive: true });
element.addEventListener("wheel", wheelHandler, { passive: false });
camera.config = config;
camera.dispose = dispose;
camera.refresh = refresh;
camera.tick = tick;
var withProgrammaticChange = function (fn) { return function() {
fn.apply(null, arguments);
isProgrammaticallyChanged = true;
}; };
camera.lookAt = withProgrammaticChange(camera.lookAt);
camera.translate = withProgrammaticChange(camera.translate);
camera.pan = withProgrammaticChange(camera.pan);
camera.rotate = withProgrammaticChange(camera.rotate);
camera.scale = withProgrammaticChange(camera.scale);
camera.zoom = withProgrammaticChange(camera.zoom);
camera.reset = withProgrammaticChange(camera.reset);
camera.set = withProgrammaticChange(camera.set);
camera.setScaleBounds = withProgrammaticChange(camera.setScaleBounds);
camera.setTranslationBounds = withProgrammaticChange(
camera.setTranslationBounds
);
camera.setView = withProgrammaticChange(camera.setView);
camera.setViewCenter = withProgrammaticChange(camera.setViewCenter);
refresh();
return camera;
};
return dom2dCamera;
})));