UNPKG

dom-2d-camera

Version:

A wrapper for attaching a 2D camera to a DOM element

605 lines (503 loc) 20.7 kB
(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; })));