diff --git a/3d-plotter/main.js b/3d-plotter/main.js
index 1394de3..27840e8 100644
--- a/3d-plotter/main.js
+++ b/3d-plotter/main.js
@@ -55,6 +55,13 @@ function initEquation() {
}
});
+ $("#createLink").click(function () {
+ var search = "?equation=" + encodeURIComponent(graph.currEquation);
+ var url = location.href.split("?")[0] + search;
+
+ prompt("Use this URL to shere or save the current equation.", url);
+ });
+
function update() {
const equation = equationInput.val();
const plot = graph.objects.plot;
@@ -581,9 +588,19 @@ function animate() {
requestAnimationFrame(animate);
};
+//function readURLParms() {
+// var hash = decodeURIComponent(location.hash.substring(1)); //"substr" is deprecated
+// if (hash.length > 1) $("#equations").val(hash);
+//}
+
function readURLParms() {
- var hash = decodeURIComponent(location.hash.substring(1)); //"substr" is deprecated
- if (hash.length > 1) $("#equations").val(hash);
+ location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) {
+ value = decodeURIComponent(value);
+ if (key == "equation") {
+ graph.currEquation = value;
+ $("#equations").val(value);
+ }
+ });
}
$(window).resize(onResize);
diff --git a/3d-plotter/style.css b/3d-plotter/style.css
index c8cd21d..74a8360 100644
--- a/3d-plotter/style.css
+++ b/3d-plotter/style.css
@@ -97,9 +97,9 @@ h1, h2, h3, p {
background: rgb(231, 231, 231) !important;
border-radius: 5px;
overflow: auto;
- line-height: 20px;
+ line-height: 18px;
z-index: 1;
- font-size: 16px;
+ font-size: 16px !important;
transition: height 0.3s ease, width 0.3s ease;
}
diff --git a/source/mapping/OrbitControlls.js b/source/mapping/OrbitControlls.js
new file mode 100644
index 0000000..754d745
--- /dev/null
+++ b/source/mapping/OrbitControlls.js
@@ -0,0 +1,1177 @@
+console.warn( "THREE.OrbitControls: As part of the transition to ES6 Modules, the files in 'examples/js' were deprecated in May 2020 (r117) and will be deleted in December 2020 (r124). You can find more information about developing using ES6 Modules in https://threejs.org/docs/index.html#manual/en/introduction/Import-via-modules." );
+/**
+ * @author qiao / https://github.com/qiao
+ * @author mrdoob / http://mrdoob.com
+ * @author alteredq / http://alteredqualia.com/
+ * @author WestLangley / http://github.com/WestLangley
+ * @author erich666 / http://erichaines.com
+ * @author ScieCode / http://github.com/sciecode
+ */
+
+// This set of controls performs orbiting, dollying (zooming), and panning.
+// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+//
+// Orbit - left mouse / touch: one-finger move
+// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
+// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
+
+THREE.OrbitControls = function ( object, domElement ) {
+
+ if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' );
+ if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );
+
+ this.object = object;
+ this.domElement = domElement;
+
+ // Set to false to disable this control
+ this.enabled = true;
+
+ // "target" sets the location of focus, where the object orbits around
+ this.target = new THREE.Vector3();
+
+ // How far you can dolly in and out ( PerspectiveCamera only )
+ this.minDistance = 0;
+ this.maxDistance = Infinity;
+
+ // How far you can zoom in and out ( OrthographicCamera only )
+ this.minZoom = 0;
+ this.maxZoom = Infinity;
+
+ // How far you can orbit vertically, upper and lower limits.
+ // Range is 0 to Math.PI radians.
+ this.minPolarAngle = 0; // radians
+ this.maxPolarAngle = Math.PI; // radians
+
+ // How far you can orbit horizontally, upper and lower limits.
+ // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
+ this.minAzimuthAngle = - Infinity; // radians
+ this.maxAzimuthAngle = Infinity; // radians
+
+ // Set to true to enable damping (inertia)
+ // If damping is enabled, you must call controls.update() in your animation loop
+ this.enableDamping = false;
+ this.dampingFactor = 0.05;
+
+ // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
+ // Set to false to disable zooming
+ this.enableZoom = true;
+ this.zoomSpeed = 1.0;
+
+ // Set to false to disable rotating
+ this.enableRotate = true;
+ this.rotateSpeed = 1.0;
+
+ // Set to false to disable panning
+ this.enablePan = true;
+ this.panSpeed = 1.0;
+ this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
+ this.keyPanSpeed = 7.0; // pixels moved per arrow key push
+
+ // Set to true to automatically rotate around the target
+ // If auto-rotate is enabled, you must call controls.update() in your animation loop
+ this.autoRotate = false;
+ this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
+
+ // Set to false to disable use of the keys
+ this.enableKeys = true;
+
+ // The four arrow keys
+ this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
+
+ // Mouse buttons
+ this.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN };
+
+ // Touch fingers
+ this.touches = { ONE: THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN };
+
+ // for reset
+ this.target0 = this.target.clone();
+ this.position0 = this.object.position.clone();
+ this.zoom0 = this.object.zoom;
+
+ //
+ // public methods
+ //
+
+ this.getPolarAngle = function () {
+
+ return spherical.phi;
+
+ };
+
+ this.getAzimuthalAngle = function () {
+
+ return spherical.theta;
+
+ };
+
+ this.saveState = function () {
+
+ scope.target0.copy( scope.target );
+ scope.position0.copy( scope.object.position );
+ scope.zoom0 = scope.object.zoom;
+
+ };
+
+ this.reset = function () {
+
+ scope.target.copy( scope.target0 );
+ scope.object.position.copy( scope.position0 );
+ scope.object.zoom = scope.zoom0;
+
+ scope.object.updateProjectionMatrix();
+ scope.dispatchEvent( changeEvent );
+
+ scope.update();
+
+ state = STATE.NONE;
+
+ };
+
+ // this method is exposed, but perhaps it would be better if we can make it private...
+ this.update = function () {
+
+ var offset = new THREE.Vector3();
+
+ // so camera.up is the orbit axis
+ var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
+ var quatInverse = quat.clone().inverse();
+
+ var lastPosition = new THREE.Vector3();
+ var lastQuaternion = new THREE.Quaternion();
+
+ var twoPI = 2 * Math.PI;
+
+ return function update() {
+
+ var position = scope.object.position;
+
+ offset.copy( position ).sub( scope.target );
+
+ // rotate offset to "y-axis-is-up" space
+ offset.applyQuaternion( quat );
+
+ // angle from z-axis around y-axis
+ spherical.setFromVector3( offset );
+
+ if ( scope.autoRotate && state === STATE.NONE ) {
+
+ rotateLeft( getAutoRotationAngle() );
+
+ }
+
+ if ( scope.enableDamping ) {
+
+ spherical.theta += sphericalDelta.theta * scope.dampingFactor;
+ spherical.phi += sphericalDelta.phi * scope.dampingFactor;
+
+ } else {
+
+ spherical.theta += sphericalDelta.theta;
+ spherical.phi += sphericalDelta.phi;
+
+ }
+
+ // restrict theta to be between desired limits
+
+ var min = scope.minAzimuthAngle;
+ var max = scope.maxAzimuthAngle;
+
+ if ( isFinite ( min ) && isFinite( max ) ) {
+
+ if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
+
+ if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
+
+ if ( min < max ) {
+
+ spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
+
+ } else {
+
+ spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
+ Math.max( min, spherical.theta ) :
+ Math.min( max, spherical.theta );
+
+ }
+
+ }
+
+ // restrict phi to be between desired limits
+ spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
+
+ spherical.makeSafe();
+
+
+ spherical.radius *= scale;
+
+ // restrict radius to be between desired limits
+ spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
+
+ // move target to panned location
+
+ if ( scope.enableDamping === true ) {
+
+ scope.target.addScaledVector( panOffset, scope.dampingFactor );
+
+ } else {
+
+ scope.target.add( panOffset );
+
+ }
+
+ offset.setFromSpherical( spherical );
+
+ // rotate offset back to "camera-up-vector-is-up" space
+ offset.applyQuaternion( quatInverse );
+
+ position.copy( scope.target ).add( offset );
+
+ scope.object.lookAt( scope.target );
+
+ if ( scope.enableDamping === true ) {
+
+ sphericalDelta.theta *= ( 1 - scope.dampingFactor );
+ sphericalDelta.phi *= ( 1 - scope.dampingFactor );
+
+ panOffset.multiplyScalar( 1 - scope.dampingFactor );
+
+ } else {
+
+ sphericalDelta.set( 0, 0, 0 );
+
+ panOffset.set( 0, 0, 0 );
+
+ }
+
+ scale = 1;
+
+ // update condition is:
+ // min(camera displacement, camera rotation in radians)^2 > EPS
+ // using small-angle approximation cos(x/2) = 1 - x^2 / 8
+
+ if ( zoomChanged ||
+ lastPosition.distanceToSquared( scope.object.position ) > EPS ||
+ 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
+
+ scope.dispatchEvent( changeEvent );
+
+ lastPosition.copy( scope.object.position );
+ lastQuaternion.copy( scope.object.quaternion );
+ zoomChanged = false;
+
+ return true;
+
+ }
+
+ return false;
+
+ };
+
+ }();
+
+ this.dispose = function () {
+
+ scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
+ scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
+ scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );
+
+ scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
+ scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
+ scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
+
+ scope.domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove, false );
+ scope.domElement.ownerDocument.removeEventListener( 'mouseup', onMouseUp, false );
+
+ scope.domElement.removeEventListener( 'keydown', onKeyDown, false );
+
+ //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+
+ };
+
+ //
+ // internals
+ //
+
+ var scope = this;
+
+ var changeEvent = { type: 'change' };
+ var startEvent = { type: 'start' };
+ var endEvent = { type: 'end' };
+
+ var STATE = {
+ NONE: - 1,
+ ROTATE: 0,
+ DOLLY: 1,
+ PAN: 2,
+ TOUCH_ROTATE: 3,
+ TOUCH_PAN: 4,
+ TOUCH_DOLLY_PAN: 5,
+ TOUCH_DOLLY_ROTATE: 6
+ };
+
+ var state = STATE.NONE;
+
+ var EPS = 0.000001;
+
+ // current position in spherical coordinates
+ var spherical = new THREE.Spherical();
+ var sphericalDelta = new THREE.Spherical();
+
+ var scale = 1;
+ var panOffset = new THREE.Vector3();
+ var zoomChanged = false;
+
+ var rotateStart = new THREE.Vector2();
+ var rotateEnd = new THREE.Vector2();
+ var rotateDelta = new THREE.Vector2();
+
+ var panStart = new THREE.Vector2();
+ var panEnd = new THREE.Vector2();
+ var panDelta = new THREE.Vector2();
+
+ var dollyStart = new THREE.Vector2();
+ var dollyEnd = new THREE.Vector2();
+ var dollyDelta = new THREE.Vector2();
+
+ function getAutoRotationAngle() {
+
+ return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
+
+ }
+
+ function getZoomScale() {
+
+ return Math.pow( 0.95, scope.zoomSpeed );
+
+ }
+
+ function rotateLeft( angle ) {
+
+ sphericalDelta.theta -= angle;
+
+ }
+
+ function rotateUp( angle ) {
+
+ sphericalDelta.phi -= angle;
+
+ }
+
+ var panLeft = function () {
+
+ var v = new THREE.Vector3();
+
+ return function panLeft( distance, objectMatrix ) {
+
+ v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
+ v.multiplyScalar( - distance );
+
+ panOffset.add( v );
+
+ };
+
+ }();
+
+ var panUp = function () {
+
+ var v = new THREE.Vector3();
+
+ return function panUp( distance, objectMatrix ) {
+
+ if ( scope.screenSpacePanning === true ) {
+
+ v.setFromMatrixColumn( objectMatrix, 1 );
+
+ } else {
+
+ v.setFromMatrixColumn( objectMatrix, 0 );
+ v.crossVectors( scope.object.up, v );
+
+ }
+
+ v.multiplyScalar( distance );
+
+ panOffset.add( v );
+
+ };
+
+ }();
+
+ // deltaX and deltaY are in pixels; right and down are positive
+ var pan = function () {
+
+ var offset = new THREE.Vector3();
+
+ return function pan( deltaX, deltaY ) {
+
+ var element = scope.domElement;
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ // perspective
+ var position = scope.object.position;
+ offset.copy( position ).sub( scope.target );
+ var targetDistance = offset.length();
+
+ // half of the fov is center to top of screen
+ targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
+
+ // we use only clientHeight here so aspect ratio does not distort speed
+ panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
+ panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ // orthographic
+ panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
+ panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
+
+ } else {
+
+ // camera neither orthographic nor perspective
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+ scope.enablePan = false;
+
+ }
+
+ };
+
+ }();
+
+ function dollyOut( dollyScale ) {
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ scale /= dollyScale;
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+
+ } else {
+
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ scope.enableZoom = false;
+
+ }
+
+ }
+
+ function dollyIn( dollyScale ) {
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ scale *= dollyScale;
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+
+ } else {
+
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ scope.enableZoom = false;
+
+ }
+
+ }
+
+ //
+ // event callbacks - update the object state
+ //
+
+ function handleMouseDownRotate( event ) {
+
+ rotateStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseDownDolly( event ) {
+
+ dollyStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseDownPan( event ) {
+
+ panStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseMoveRotate( event ) {
+
+ rotateEnd.set( event.clientX, event.clientY );
+
+ rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+
+ var element = scope.domElement;
+
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+
+ rotateStart.copy( rotateEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseMoveDolly( event ) {
+
+ dollyEnd.set( event.clientX, event.clientY );
+
+ dollyDelta.subVectors( dollyEnd, dollyStart );
+
+ if ( dollyDelta.y > 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ } else if ( dollyDelta.y < 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ }
+
+ dollyStart.copy( dollyEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseMovePan( event ) {
+
+ panEnd.set( event.clientX, event.clientY );
+
+ panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+
+ pan( panDelta.x, panDelta.y );
+
+ panStart.copy( panEnd );
+
+ scope.update();
+
+ }
+
+ function handleMouseUp( /*event*/ ) {
+
+ // no-op
+
+ }
+
+ function handleMouseWheel( event ) {
+
+ if ( event.deltaY < 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ } else if ( event.deltaY > 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ }
+
+ scope.update();
+
+ }
+
+ function handleKeyDown( event ) {
+
+ var needsUpdate = false;
+
+ switch ( event.keyCode ) {
+
+ case scope.keys.UP:
+ pan( 0, scope.keyPanSpeed );
+ needsUpdate = true;
+ break;
+
+ case scope.keys.BOTTOM:
+ pan( 0, - scope.keyPanSpeed );
+ needsUpdate = true;
+ break;
+
+ case scope.keys.LEFT:
+ pan( scope.keyPanSpeed, 0 );
+ needsUpdate = true;
+ break;
+
+ case scope.keys.RIGHT:
+ pan( - scope.keyPanSpeed, 0 );
+ needsUpdate = true;
+ break;
+
+ }
+
+ if ( needsUpdate ) {
+
+ // prevent the browser from scrolling on cursor keys
+ event.preventDefault();
+
+ scope.update();
+
+ }
+
+
+ }
+
+ function handleTouchStartRotate( event ) {
+
+ if ( event.touches.length == 1 ) {
+
+ rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ } else {
+
+ var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+
+ rotateStart.set( x, y );
+
+ }
+
+ }
+
+ function handleTouchStartPan( event ) {
+
+ if ( event.touches.length == 1 ) {
+
+ panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ } else {
+
+ var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+
+ panStart.set( x, y );
+
+ }
+
+ }
+
+ function handleTouchStartDolly( event ) {
+
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+ var distance = Math.sqrt( dx * dx + dy * dy );
+
+ dollyStart.set( 0, distance );
+
+ }
+
+ function handleTouchStartDollyPan( event ) {
+
+ if ( scope.enableZoom ) handleTouchStartDolly( event );
+
+ if ( scope.enablePan ) handleTouchStartPan( event );
+
+ }
+
+ function handleTouchStartDollyRotate( event ) {
+
+ if ( scope.enableZoom ) handleTouchStartDolly( event );
+
+ if ( scope.enableRotate ) handleTouchStartRotate( event );
+
+ }
+
+ function handleTouchMoveRotate( event ) {
+
+ if ( event.touches.length == 1 ) {
+
+ rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ } else {
+
+ var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+
+ rotateEnd.set( x, y );
+
+ }
+
+ rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+
+ var element = scope.domElement;
+
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+
+ rotateStart.copy( rotateEnd );
+
+ }
+
+ function handleTouchMovePan( event ) {
+
+ if ( event.touches.length == 1 ) {
+
+ panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ } else {
+
+ var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+
+ panEnd.set( x, y );
+
+ }
+
+ panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+
+ pan( panDelta.x, panDelta.y );
+
+ panStart.copy( panEnd );
+
+ }
+
+ function handleTouchMoveDolly( event ) {
+
+ var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+
+ var distance = Math.sqrt( dx * dx + dy * dy );
+
+ dollyEnd.set( 0, distance );
+
+ dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
+
+ dollyOut( dollyDelta.y );
+
+ dollyStart.copy( dollyEnd );
+
+ }
+
+ function handleTouchMoveDollyPan( event ) {
+
+ if ( scope.enableZoom ) handleTouchMoveDolly( event );
+
+ if ( scope.enablePan ) handleTouchMovePan( event );
+
+ }
+
+ function handleTouchMoveDollyRotate( event ) {
+
+ if ( scope.enableZoom ) handleTouchMoveDolly( event );
+
+ if ( scope.enableRotate ) handleTouchMoveRotate( event );
+
+ }
+
+ function handleTouchEnd( /*event*/ ) {
+
+ // no-op
+
+ }
+
+ //
+ // event handlers - FSM: listen for events and reset state
+ //
+
+ function onMouseDown( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ // Prevent the browser from scrolling.
+ event.preventDefault();
+
+ // Manually set the focus since calling preventDefault above
+ // prevents the browser from setting it automatically.
+
+ scope.domElement.focus ? scope.domElement.focus() : window.focus();
+
+ var mouseAction;
+
+ switch ( event.button ) {
+
+ case 0:
+
+ mouseAction = scope.mouseButtons.LEFT;
+ break;
+
+ case 1:
+
+ mouseAction = scope.mouseButtons.MIDDLE;
+ break;
+
+ case 2:
+
+ mouseAction = scope.mouseButtons.RIGHT;
+ break;
+
+ default:
+
+ mouseAction = - 1;
+
+ }
+
+ switch ( mouseAction ) {
+
+ case THREE.MOUSE.DOLLY:
+
+ if ( scope.enableZoom === false ) return;
+
+ handleMouseDownDolly( event );
+
+ state = STATE.DOLLY;
+
+ break;
+
+ case THREE.MOUSE.ROTATE:
+
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+
+ if ( scope.enablePan === false ) return;
+
+ handleMouseDownPan( event );
+
+ state = STATE.PAN;
+
+ } else {
+
+ if ( scope.enableRotate === false ) return;
+
+ handleMouseDownRotate( event );
+
+ state = STATE.ROTATE;
+
+ }
+
+ break;
+
+ case THREE.MOUSE.PAN:
+
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+
+ if ( scope.enableRotate === false ) return;
+
+ handleMouseDownRotate( event );
+
+ state = STATE.ROTATE;
+
+ } else {
+
+ if ( scope.enablePan === false ) return;
+
+ handleMouseDownPan( event );
+
+ state = STATE.PAN;
+
+ }
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ if ( state !== STATE.NONE ) {
+
+ scope.domElement.ownerDocument.addEventListener( 'mousemove', onMouseMove, false );
+ scope.domElement.ownerDocument.addEventListener( 'mouseup', onMouseUp, false );
+
+ scope.dispatchEvent( startEvent );
+
+ }
+
+ }
+
+ function onMouseMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+
+ switch ( state ) {
+
+ case STATE.ROTATE:
+
+ if ( scope.enableRotate === false ) return;
+
+ handleMouseMoveRotate( event );
+
+ break;
+
+ case STATE.DOLLY:
+
+ if ( scope.enableZoom === false ) return;
+
+ handleMouseMoveDolly( event );
+
+ break;
+
+ case STATE.PAN:
+
+ if ( scope.enablePan === false ) return;
+
+ handleMouseMovePan( event );
+
+ break;
+
+ }
+
+ }
+
+ function onMouseUp( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ handleMouseUp( event );
+
+ scope.domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove, false );
+ scope.domElement.ownerDocument.removeEventListener( 'mouseup', onMouseUp, false );
+
+ scope.dispatchEvent( endEvent );
+
+ state = STATE.NONE;
+
+ }
+
+ function onMouseWheel( event ) {
+
+ if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ scope.dispatchEvent( startEvent );
+
+ handleMouseWheel( event );
+
+ scope.dispatchEvent( endEvent );
+
+ }
+
+ function onKeyDown( event ) {
+
+ if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
+
+ handleKeyDown( event );
+
+ }
+
+ function onTouchStart( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault(); // prevent scrolling
+
+ switch ( event.touches.length ) {
+
+ case 1:
+
+ switch ( scope.touches.ONE ) {
+
+ case THREE.TOUCH.ROTATE:
+
+ if ( scope.enableRotate === false ) return;
+
+ handleTouchStartRotate( event );
+
+ state = STATE.TOUCH_ROTATE;
+
+ break;
+
+ case THREE.TOUCH.PAN:
+
+ if ( scope.enablePan === false ) return;
+
+ handleTouchStartPan( event );
+
+ state = STATE.TOUCH_PAN;
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ break;
+
+ case 2:
+
+ switch ( scope.touches.TWO ) {
+
+ case THREE.TOUCH.DOLLY_PAN:
+
+ if ( scope.enableZoom === false && scope.enablePan === false ) return;
+
+ handleTouchStartDollyPan( event );
+
+ state = STATE.TOUCH_DOLLY_PAN;
+
+ break;
+
+ case THREE.TOUCH.DOLLY_ROTATE:
+
+ if ( scope.enableZoom === false && scope.enableRotate === false ) return;
+
+ handleTouchStartDollyRotate( event );
+
+ state = STATE.TOUCH_DOLLY_ROTATE;
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ if ( state !== STATE.NONE ) {
+
+ scope.dispatchEvent( startEvent );
+
+ }
+
+ }
+
+ function onTouchMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault(); // prevent scrolling
+ event.stopPropagation();
+
+ switch ( state ) {
+
+ case STATE.TOUCH_ROTATE:
+
+ if ( scope.enableRotate === false ) return;
+
+ handleTouchMoveRotate( event );
+
+ scope.update();
+
+ break;
+
+ case STATE.TOUCH_PAN:
+
+ if ( scope.enablePan === false ) return;
+
+ handleTouchMovePan( event );
+
+ scope.update();
+
+ break;
+
+ case STATE.TOUCH_DOLLY_PAN:
+
+ if ( scope.enableZoom === false && scope.enablePan === false ) return;
+
+ handleTouchMoveDollyPan( event );
+
+ scope.update();
+
+ break;
+
+ case STATE.TOUCH_DOLLY_ROTATE:
+
+ if ( scope.enableZoom === false && scope.enableRotate === false ) return;
+
+ handleTouchMoveDollyRotate( event );
+
+ scope.update();
+
+ break;
+
+ default:
+
+ state = STATE.NONE;
+
+ }
+
+ }
+
+ function onTouchEnd( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ handleTouchEnd( event );
+
+ scope.dispatchEvent( endEvent );
+
+ state = STATE.NONE;
+
+ }
+
+ function onContextMenu( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ event.preventDefault();
+
+ }
+
+ //
+
+ scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
+
+ scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
+ scope.domElement.addEventListener( 'wheel', onMouseWheel, false );
+
+ scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
+ scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
+ scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
+
+ scope.domElement.addEventListener( 'keydown', onKeyDown, false );
+
+ // make sure element can receive keys.
+
+ if ( scope.domElement.tabIndex === - 1 ) {
+
+ scope.domElement.tabIndex = 0;
+
+ }
+
+ // force an update at start
+
+ this.update();
+
+};
+
+THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
+
+
+// This set of controls performs orbiting, dollying (zooming), and panning.
+// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+// This is very similar to OrbitControls, another set of touch behavior
+//
+// Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
+// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
+// Pan - left mouse, or arrow keys / touch: one-finger move
+
+THREE.MapControls = function ( object, domElement ) {
+
+ THREE.OrbitControls.call( this, object, domElement );
+
+ this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up
+
+ this.mouseButtons.LEFT = THREE.MOUSE.PAN;
+ this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE;
+
+ this.touches.ONE = THREE.TOUCH.PAN;
+ this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE;
+
+};
+
+THREE.MapControls.prototype = Object.create( THREE.EventDispatcher.prototype );
+THREE.MapControls.prototype.constructor = THREE.MapControls;
diff --git a/source/mapping/functionLibrary.js b/source/mapping/functionLibrary.js
new file mode 100644
index 0000000..3092ebb
--- /dev/null
+++ b/source/mapping/functionLibrary.js
@@ -0,0 +1,187 @@
+/*
+ Author: David Block
+ Source: https://replit.com/@DavidBrock1/complexplotter#functionLibrary.js
+*/
+
+//Create GLSL code for all function
+var commonShaderCode = "";
+var baseScope = {
+ variables: [],
+ functions: []
+};
+
+//Add variables that are defines in shader code
+baseScope.variables.push("c");
+baseScope.variables.push("i");
+baseScope.variables.push("e");
+baseScope.variables.push("pi");
+
+//Math constants
+commonShaderCode += "uniform vec2 c;\n";
+commonShaderCode += "const vec2 i = vec2(0.0, 1.0);\n";
+commonShaderCode += "const vec2 e = vec2(" + Math.E + ", 0.0);\n";
+commonShaderCode += "const vec2 pi = vec2(" + Math.PI + ", 0.0);\n";
+
+//Add basic functions that are defined in shader code
+baseScope.functions.push("re1");
+baseScope.functions.push("im1");
+baseScope.functions.push("conj1");
+baseScope.functions.push("abs1");
+baseScope.functions.push("mul2");
+baseScope.functions.push("div2");
+baseScope.functions.push("polar1");
+baseScope.functions.push("ln1");
+baseScope.functions.push("exp1");
+baseScope.functions.push("gamma1");
+baseScope.functions.push("zeta1");
+baseScope.functions.push("zeta2");
+
+//Enementry functions that other functions are defined in terms of
+commonShaderCode += `
+vec2 re_C(vec2 a) {
+ return vec2(a.x, 0.0);
+}
+vec2 im_C(vec2 a) {
+ return vec2(a.y, 0.0);
+}
+vec2 conj_C(vec2 a) {
+ return vec2(a.x, -a.y);
+}
+vec2 abs_C(vec2 a) {
+ return vec2(sqrt(a.x*a.x + a.y*a.y), 0.0);
+}
+vec2 mul_C(vec2 a, vec2 b) {
+ return vec2(a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x);
+}
+vec2 div_C(vec2 a, vec2 b) {
+ return mul_C(a, conj_C(b)) / (b.x*b.x + b.y*b.y);
+}
+vec2 polar_C(vec2 a) {
+ return vec2(abs_C(a).x, atan(a.y, a.x));
+}
+vec2 ln_C(vec2 a) {
+ return vec2(log(abs_C(a).x), atan(a.y, a.x));
+}
+vec2 exp_C(vec2 a) {
+ return exp(a.x) * vec2(cos(a.y), sin(a.y));
+}`;
+
+commonShaderCode += GLcompile("pow(b, p) = exp(ln(b) * p)", baseScope);
+commonShaderCode += GLcompile("sqrt(z) = z^0.5", baseScope);
+commonShaderCode += GLcompile("log(z) = ln(z) / 2.30258509;", baseScope);
+commonShaderCode += GLcompile("log(z, b) = ln(z) / ln(b)", baseScope);
+
+commonShaderCode += GLcompile("sin(z) = (e^(i*z) - e^(-i*z)) / 2i", baseScope);
+commonShaderCode += GLcompile("cos(z) = (e^(i*z) + e^(-i*z)) / 2", baseScope);
+commonShaderCode += GLcompile("tan(z) = sin(z)/cos(z)", baseScope);
+commonShaderCode += GLcompile("sec(z) = 1/cos(z)", baseScope);
+commonShaderCode += GLcompile("csc(z) = 1/sin(z)", baseScope);
+commonShaderCode += GLcompile("cot(z) = 1/tan(z)", baseScope);
+
+commonShaderCode += GLcompile("asin(z) = -i * ln(i*z + sqrt(1 - z^2))", baseScope);
+commonShaderCode += GLcompile("acos(z) = -i * ln(z + sqrt(z^2 - 1))", baseScope);
+commonShaderCode += GLcompile("atan(z) = (i/2) * ln((i + z) / (i - z))", baseScope);
+
+commonShaderCode += GLcompile("sinh(z) = (e^z - e^(-z)) / 2", baseScope);
+commonShaderCode += GLcompile("cosh(z) = (e^z + e^(-z)) / 2", baseScope);
+commonShaderCode += GLcompile("tanh(z) = sinh(z)/cosh(z)", baseScope);
+
+commonShaderCode += GLcompile("asinh(z) = ln(z + sqrt(z^2 + 1))", baseScope);
+commonShaderCode += GLcompile("acosh(z) = ln(z + sqrt(z^2 - 1))", baseScope);
+commonShaderCode += GLcompile("atanh(z) = (1/2) * ln((1 + z) / (1 - z))", baseScope);
+
+//These functions are a bit too complex to write in one line
+commonShaderCode += `
+vec2 gammaPartial_C(vec2 z) {
+ z.x -= 1.0;
+ vec2 x = vec2(1.0, 0.0);
+
+ x += ${GLcompile("+676.5203681218851 / (z+1)", {variables: ["z"]})};
+ x += ${GLcompile("-1259.139216722403 / (z+2)", {variables: ["z"]})};
+ x += ${GLcompile("+771.3234287776531 / (z+3)", {variables: ["z"]})};
+ x += ${GLcompile("-176.6150291621406 / (z+4)", {variables: ["z"]})};
+ x += ${GLcompile("+12.50734327868691 / (z+5)", {variables: ["z"]})};
+ x += ${GLcompile("-0.1385710952657201 / (z+6)", {variables: ["z"]})};
+ x += ${GLcompile("+0.000009984369578019572 / (z+7)", {variables: ["z"]})};
+ x += ${GLcompile("+0.0000001505632735149312 / (z+8)", {variables: ["z"]})};
+
+ ${GLcompile("t = z + 7.5", {variables: ["z"]})};
+ return ${GLcompile("sqrt(2pi) * t^(z+0.5) * exp(-t) * x", {variables: ["z", "t", "x"]})};
+}
+
+vec2 gamma_C(vec2 z) {
+ vec2 y;
+ if (z.x < 0.5) {
+ y = ${GLcompile("pi / (sin(pi*z) * gammaPartial(1-z))", {variables: ["z"], functions: ["gammaPartial1"]})}; //Reflection formula
+ } else {
+ y = gammaPartial_C(z);
+ }
+ return y;
+}
+
+vec2 zetaPartial_C(vec2 s, vec2 count) {
+ vec2 total = vec2(1.0, 0.0);
+ vec2 n1 = vec2(0.0);
+ vec2 n2 = vec2(0.0);
+
+ for (float i = 1.0; i < 1024.0; i++) {
+ n1.x = 2.0 * i;
+ n2.x = 2.0 * i + 1.0;
+ total += ${GLcompile("-1/n1^s + 1/n2^s", {variables: ["n1", "n2", "s"]})};
+ if (2.0 * i >= count.x) break;
+ }
+ return ${GLcompile("1/(1-2^(1-s)) * total", {variables: ["s", "total"]})};
+}
+
+vec2 zeta_C(vec2 s, vec2 count) {
+ vec2 y;
+ if (s.x < 0.5) {
+ y = ${GLcompile("2^s * pi^(s-1) * sin(pi*s/2) * gamma(1-s) * zetaPartial(1-s, count)", {variables: ["s", "count"], functions: ["zetaPartial2"]})};
+ } else {
+ y = zetaPartial_C(s, count);
+ }
+ return y;
+}
+
+vec2 zeta_C(vec2 s) {
+ return zeta_C(s, vec2(64.0, 0.0));
+}`;
+
+//Import functions not included in the math library so they can be used in JavaScript
+math.import({
+ zeta: math.typed({
+ "Complex, Complex": (function () {
+ var sumTerm = math.compile("total -1/n1^s +1/n2^s");
+ var result = math.compile("1/(1-2^(1-s)) * total");
+ var reflection = math.compile("2^s * pi^(s-1) * sin(pi*s/2) * gamma(1-s) * zetaPartial(1-s, count)");
+
+ function zetaPartial(s, count) {
+ var total = math.complex(1.0, 0.0);
+ var n1 = 0.0;
+ var n2 = 0.0;
+
+ for (var i = 1.0; i < 1024.0; i++) {
+ n1 = 2.0 * i;
+ n2 = 2.0 * i + 1.0;
+ total = sumTerm.evaluate({s: s, total: total, n1: n1, n2: n2});
+ if (2.0 * i >= count) break;
+ }
+ return result.evaluate({s: s, total: total});
+ }
+
+ return function (s, count) {
+ var y;
+ if (s.re < 0.5) {
+ y = reflection.evaluate({s: s, count: count.re, zetaPartial: zetaPartial});
+ } else {
+ y = zetaPartial(s, count.re);
+ }
+ return y;
+ };
+ })(),
+
+ "Complex": function (s) {
+ return math.zeta(s, 64);
+ }
+ }),
+});
\ No newline at end of file
diff --git a/source/mapping/index.html b/source/mapping/index.html
new file mode 100644
index 0000000..fe62c03
--- /dev/null
+++ b/source/mapping/index.html
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+ Complex Plotter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1+2i
+
+
+
+ Enter equation
+ (?)
+
+
+
+
+
+
+ c = 0
+
+ Re:
+ Im:
+
+
+
+
+
+
+
+
+
+ (?)
+
+
+
+
+
+
+
+
+
+
+ (?)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (?)
+
+
+
+
+
+
+
+
+
+
+ (?)
+
+
+
+
+
+
+
+
+
+
+
+ About this program
+
+
+
+
+
+
×
+
+
Complex Function Plotter
+
Background
+
+ Most graphing calculators take a set of real numbers on the X axis, run them through a function, and plot the result on the Y axis. Complex numbers are an extension of the real numbers and are usually written in the form a+b*i, where a and b are real numbers and i*i=-1 (the imaginary part). To graph a complex function, or a function that takes a complex number as an input and outputs a complex number, a two dimensional graph will not work well. The input itself is two dimensional (real and imaginary) and so is the output, so four dimensions would be needed. To get around this, the graph here is shown in three dimensions and color is used to encode the fourth dimension needed.
+
+
How to use
+
+ Start by typing an equation in the textbox in the upper-left. It should be in the form f(z)=..., where the function takes a complex number and does something to it. If the function is valid, the 3D graph should update.
+
+
+ The height of the graph defaults to the absolute value of the output of the current function at every input (this can be changed to the real or imaginary parts under options). By default, the hue of a point equals its angle (in polar coordinates) and the brightness gives a sense of the absolute value.
+
+
+ To rotate the graph, click and drag or touch and drag with one finger (on a touch screen). Use the mouse wheel or two fingers to zoom in or out. Right click and drag or drag with two fingers to pan the view.
+
+ Note: This program works best in FireFox and Chrome. It may work in Edge, but does not usually work in Internet Explorer.
+
Some things to try
+
+ The function f(z)=z is good to start with. Polynomials (f(z)=z^2+1 for example) are always neat. You can clearly see that a polynomial always has as many zeros as the exponent on its highest term (some may be in the same position). The function f(z)=z^c is neat (with the slider enabled).
+ The functions f(z)=((((z^2)+z)^2+z)^2+z)^2 and f(z)=z^z^z^z^z both have much fine detail, so are best viewed with height as flat. The functions sin, cos, tan, gamma, and zeta are also interesting to see in 3D (for example f(z)=gamma(z)).
+
+
About
+
+ This program was created by me, David Brock. While several 3D graphing calculators, and a few programs for creating 2D colored plots of complex functions exist, I did not find an online program that could create 3D colored plots. Since I love thinking about and playing with complex numbers and 4D things, I decided to create one.
+
+
+ I am sure many bugs are still in this program, so do not be discouraged if something does not work. I am frequently making changes, so new features will probably be added, too.
+
+
+ If you see "grass" extending off the screen, this usually means the function is extremely chaotic. I have not figured out why this happens yet.
+
How this works
+
+ The real power of this program is that it can turn any equation into OpenGL code that can be run on a GPU and plotted on the screen (the console shows the generated code). I use Three.js for the 3D graphics. I use Math.js to compile the equation into an expression tree and then replace each node with the correct OpenGL function, using 2D vectors for all complex numbers. Almost every function (except addition) had to be written to accept vectors as complex numbers.
+
+
+
+
+
+
+
×
+
Equation
+
+
Available Variables and Functions
+
+ z This is the input to the function being plotted.
+ x, y These are the real and imaginary parts of z.
+ i The imaginary unit. i^2=-1
+ pi 3.14159265...
+ e 2.718281828...
+ c This is a constant. It can be adjusted with a slider, if enabled in options.
+
+ re The real part of a number
+ im The imaginary part of a number
+ conj The conjugate
+ abs The absolute value, or magnitude (abs(z)=sqrt(re(z)^2+im(z)^2))
+ polar The number in polar coordinates. The real part is the magnitude and the imaginary part is the angle (in radians).
+ ln Natural log, or loge. For complex numbers the result is similar to polar coordinates.
+ exp Raise e to a power (exp(z)=e^z)
+ sqrt Square root (sqrt(z)=z^0.5)
+ sin, cos, tan, csc, sec, cot Trigonometric functions
+ asin, acos, atan Inverse trigonometric functions
+ sinh, cosh, tanh Hyperbolic trigonometric functions
+ asinh, acosh, atanh Inverse hyperbolic trigonometric functions
+ gamma The gamma function. It can be thought of an an extension of factorials.
+ zeta The Riemann zeta function.
+
+
+
+
+
+
+
×
+
Constant c
+
+
This is a number you can use in equations (like pi, e, etc.). You can change it by clicking the line "c = ...", or using sliders if enabled with the checkbox in Options. The advantage of using c instead of defining a constant in the equation box is that it can be changed without the equation needung to be recompiled.
+
+
+
+
+
+
×
+
3D Plot
+
+
This is the actual plot of your function. You can change what the color and height mean.
+
+
+
+
+
+
×
+
Input Plane
+
+
This is the set of inputs to your function. It is analogous to the X axid on a traditional graphing calculator. It is a portion of the complex plane with the real and imaginary axes numbered.
+
+
+
+
+
+
×
+
Plot Cursor
+
+
This allows you to inspect the plot more closely. If you click (or mouse over) a point on the input plane, you can see the exact input and output of the function at that point. The "Continuous" option toggles whether the cursor only moves when you click. Un-checking this is useful if an equation takes too long to recalculate or you want to watch a specific point.
+