<!DOCTYPE html>
<html>
<head>
	<title>Bézierova ploha s interaktivnim kontrolnim točkama</title>
	<meta charset="UTF-8">

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
	<style>
		body {
			margin: 0;
			overflow: hidden;
		}
	</style>
</head>
<body>
	<div id="WebGL-output"></div>
	<script type="text/javascript">
		$(document).ready(function() {
			var scene = new THREE.Scene();
			var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
			var cameraControls = new THREE.OrbitControls(camera);
			var renderer = new THREE.WebGLRenderer({antialias: true});
			renderer.setClearColor(0xEEEEEE);
			renderer.setSize(window.innerWidth, window.innerHeight);
			var axisHelper = new THREE.AxisHelper(20);

			$(window).on("resize", function() {
				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();
				renderer.setSize(window.innerWidth, window.innerHeight);
				renderScene();
			});

			camera.position.x = -60;
			camera.position.y = 50;
			camera.position.z = -40;
			camera.lookAt(new THREE.Vector3(20,0,15));
			camera.updateProjectionMatrix();
			$(cameraControls).on("change", renderScene);
			cameraControls.target = new THREE.Vector3(20,0,15);




			// interactive controls
			var controls = new function() {
				this.color = 0x17a6ff;
				this.x = 0.0;
				this.y = 0.0;
				this.z = 0.0;
				this.showControlPoints = false;
				this.wireframe = false;
				this.bezierCurves = 51;
				this.interactiveCamera = true;
				this.axisHelper = false;
			}

			var gui = new dat.GUI({width:370});

			gui.addColor(controls, "color").name("Boja").onChange(function(c) {
				bezierSurfaceMaterial.color.setHex(c);
				setControlPointsColor(c);
				renderScene();
			});

			gui.add(controls, "wireframe").name("Žičani model").onChange(function(v) {
				bezierSurfaceMaterial.wireframe = v;
				renderScene();
			});

			gui.add(controls, "bezierCurves").min(10).max(100).step(1).name("Broj Bézierovih krivulja").onFinishChange(function(n) {
					bezierCurveDivisions = n-1;  // interpolation will give additional 1 bezier curve
					redrawBezierSurface();
					renderScene();
			});

			gui.add(controls, "interactiveCamera").name("Interaktivna kamera").onChange(function(v) {
				cameraControls.enabled = v;
			});

			gui.add(controls, "axisHelper").name("Pomoćne koordinatne osi").onChange(function(v) {
				if(v) scene.add(axisHelper);
				else scene.remove(axisHelper);
				renderScene();
			});

			var controlx, controly, controlz;
			gui.add(controls, "showControlPoints").name("Prikaži kontrolne točke").onChange(function(visibility) {
				if (visibility) {
					controlx = gui.add(controls, "x").listen();
					controly = gui.add(controls, "y").listen();
					controlz = gui.add(controls, "z").listen();
				} else {
					gui.remove(controlx);
					gui.remove(controly);
					gui.remove(controlz);
				}

				updateActivePointControls();
				setControlPointsVisibility(visibility);
				renderScene();
			});
			
			function updateActivePointControls() {
				controls.x = activeControlPoint.position.x;
				controls.y = activeControlPoint.position.y;
				controls.z = activeControlPoint.position.z;
				controls.x.toFixed(2);
			}





			// working with bezier curves and surface
			var bezierCurveDivisions = 1;
			var bezierSurface, bezierSurfaceGeometry, bezierSurfaceMaterial;

			var bezierControlPoints = [
				[new THREE.Vector3(-10, 10, 0),
				new THREE.Vector3(0, 7, 0),
				new THREE.Vector3(15, 3, 0),
				new THREE.Vector3(30, 8, 0)],
				[new THREE.Vector3(-10, 0, 10),
				new THREE.Vector3(-5, 15, 10),
				new THREE.Vector3(20, 10, 10),
				new THREE.Vector3(30, 5, 10)],
				[new THREE.Vector3(-10, 5, 20),
				new THREE.Vector3(-5,-10, 20),
				new THREE.Vector3(10, 10, 20),
				new THREE.Vector3(30, 0, 20)],
				[new THREE.Vector3(-10, 4, 30),
				new THREE.Vector3(-5, 8, 30),
				new THREE.Vector3(20, 6, 30),
				new THREE.Vector3(30, 4, 30)]
			]

			function redrawBezierSurface() {
				scene.remove(bezierSurface);

				var basicBezierModel = [];  // 4 bezier curves calculated from bezier control points

				// calculating basic bezier model (main 4 bezier curves)
				for (var i=0; i < bezierControlPoints.length; i++) {
					var bezier = new THREE.CubicBezierCurve3(
						bezierControlPoints[i][0],
						bezierControlPoints[i][1],
						bezierControlPoints[i][2],
						bezierControlPoints[i][3]
					)
					basicBezierModel.push( bezier.getPoints(bezierCurveDivisions) );
				}


				var bezierCurvesVertices = [];

				// calculating full bezier model (50 bezier curves in one direction, each containing 50 vertices)
				for (var i=0; i <= bezierCurveDivisions; i++) {
					var bezier = new THREE.CubicBezierCurve3(
						basicBezierModel[0][i],
						basicBezierModel[1][i],
						basicBezierModel[2][i],
						basicBezierModel[3][i]
					)

					bezierCurvesVertices = bezierCurvesVertices.concat( bezier.getPoints(bezierCurveDivisions) );
				}


				// now we've got full bezier model, it's time to create bezier surface and add it to the scene
				var bezierSurfaceVertices = bezierCurvesVertices;
				var bezierSurfaceFaces = [];
            
          const bezierUVs = [];
            
               const imgLoader = new THREE.ImageLoader();
          imgLoader.load(
            "https://threejsfundamentals.org/threejs/resources/images/heightmap-96x64.png",
            createHeightmap
          );

          function createHeightmap(image) {
            // extract the data from the image by drawing it to a canvas
            // and calling getImageData
            const ctx = document.createElement("canvas").getContext("2d");
            const { width, height } = image;
            ctx.canvas.width = width;
            ctx.canvas.height = height;
            ctx.drawImage(image, 0, 0);
            const { data } = ctx.getImageData(0, 0, width, height);
            console.log(width);

            const cellsAcross =50 - 1;
            const cellsDeep = 50 - 1;
            for (let z = 0; z < cellsDeep; ++z) {
              for (let x = 0; x < cellsAcross; ++x) {
                const u0 = x / cellsAcross;
                const v0 = z / cellsDeep;
                 bezierUVs.push([
                new THREE.Vector2(0, 0),
                new THREE.Vector2(0, 1 ),
                new THREE.Vector2(1 ,1 ),
              ]);    bezierUVs.push([
                       new THREE.Vector2(1 ,1 ),
                new THREE.Vector2(0, 0),
                new THREE.Vector2(1 , 0),
          
              ]);      new THREE.Vector2(1,1),
              }
            }
          }
             

				// creating faces from vertices
				var v1, v2, v2;  // vertex indices in bezierSurfaceVertices array
				for (var i=0; i < bezierCurveDivisions; i++) {
					for (var j=0; j < bezierCurveDivisions; j++) {
						v1 = i * (bezierCurveDivisions + 1) + j;
						v2 = (i+1) * (bezierCurveDivisions + 1) + j;
						v3 = i * (bezierCurveDivisions + 1) + (j+1);
						bezierSurfaceFaces.push( new THREE.Face3(v1, v2, v3) );
         

						v1 = (i+1) * (bezierCurveDivisions + 1) + j;
						v2 = (i+1) * (bezierCurveDivisions + 1) + (j+1);
						v3 = i * (bezierCurveDivisions + 1) + (j+1);
						bezierSurfaceFaces.push( new THREE.Face3(v1, v2, v3) );
                  
          
					}
				}

           
				bezierSurfaceGeometry = new THREE.Geometry();
				bezierSurfaceGeometry.vertices = bezierSurfaceVertices;
				bezierSurfaceGeometry.faces = bezierSurfaceFaces;
            bezierSurfaceGeometry.faceVertexUvs[0] = bezierUVs;
				bezierSurfaceGeometry.computeFaceNormals();
				bezierSurfaceGeometry.computeVertexNormals();
            
            
               function createMaterial() {
            // 4096 is the maximum width for maps
      
                var loader = new THREE.TextureLoader();
               loader.setCrossOrigin('anonymous')
            var earthTexture=loader.load("https://images-na.ssl-images-amazon.com/images/I/41Xe%2B6VIgtL.jpg");
            var earthMaterial = new THREE.MeshBasicMaterial();
            earthMaterial.map = earthTexture;
            return earthMaterial;
          }

          bezierSurfaceMaterial = createMaterial();
            
				//bezierSurfaceMaterial = new THREE.MeshLambertMaterial({color: controls.color, wireframe: controls.wireframe});
				bezierSurface = new THREE.Mesh(bezierSurfaceGeometry, bezierSurfaceMaterial);
				bezierSurface.material.side = THREE.DoubleSide;
           
				scene.add(bezierSurface);
			}

			redrawBezierSurface();





			// drawing control points (spheres)
			var activeControlPoint;
			var controlPoints = [];

			(function() {
				for (var i=0; i < 4; i++) {  // 4 control points in one direction
					for (var j=0; j < 4; j++) {  // 4 control points in another direction
						if (i == 0 && j == 0) {  // first control point becomes active control point
							var controlPointGeometry = new THREE.SphereGeometry(0.7,10,10);
						} else {
							var controlPointGeometry = new THREE.SphereGeometry(0.7,10,10);
						}
						var controlPointMaterial = new THREE.MeshLambertMaterial({color: 0xffffff - controls.color});
						var controlPoint = new THREE.Mesh(controlPointGeometry, controlPointMaterial);
						controlPoint.name = + i.toString() + "-" + j.toString();
						controlPoint.position.x = bezierControlPoints[i][j].x;
						controlPoint.position.y = bezierControlPoints[i][j].y;
						controlPoint.position.z = bezierControlPoints[i][j].z;
						controlPoint.visible = false;
						if (i==0 && j==0) {
							controlPoint.scale.set(1.5, 1.5, 1.5);
							activeControlPoint = controlPoint;
						}

						controlPoints.push(controlPoint);
						scene.add(controlPoint);
					}
				}
			}) ();

			// set control points visible or invisible
			function setControlPointsVisibility(visibility) {
				$.each(controlPoints, function(k, v) {
					v.visible = visibility;
				});
			}


			// set color of control points
			function setControlPointsColor(color) {
				$.each(controlPoints, function(k, v) {
					v.material.color.setHex(0xffffff - controls.color);
				});
			}


			function updateActiveControlPointPosition(x, y, z) {
				activeControlPoint.position.set(x, y, z);
				var i = activeControlPoint.name.split("-");  // active control point index
				bezierControlPoints[ i[0] ][ i[1] ] = new THREE.Vector3(x, y, z);
				updateActivePointControls();
				redrawBezierSurface();
				renderScene();
			}


			document.addEventListener("mousedown", onControlPointClick, true);  // done with pure javascript (without jquery because jquery doesn't support event capturing, which is essential if we want to avoid bugs with mousedown event, which is used for both rotation and placement of control points)

			function onControlPointClick(e) {
				var mouse = new THREE.Vector2();
				var raycaster = new THREE.Raycaster();

				mouse.x = (e.clientX / renderer.domElement.width) * 2 - 1;
				mouse.y = - (e.clientY / renderer.domElement.height) * 2 + 1;
				raycaster.setFromCamera(mouse, camera);

				var intersectedObjects = raycaster.intersectObjects(controlPoints);

				if (intersectedObjects.length > 0) {
					cameraControls.enabled = false;  // disabling camera rotation

					var newActiveControlPoint = intersectedObjects[0].object;

					if (newActiveControlPoint.visible) {
						var oldActiveControlPoint = activeControlPoint;

						oldActiveControlPoint.scale.set(1, 1, 1);
						newActiveControlPoint.scale.set(1.5, 1.5, 1.5);

						activeControlPoint = newActiveControlPoint;

						updateActivePointControls();
						renderScene();

						var planeNormal = activeControlPoint.position.clone().sub(camera.position);
						var plane = new THREE.Plane();
						plane.setFromNormalAndCoplanarPoint(planeNormal, activeControlPoint.position);

						$(document).on("mousemove", function(e) {
							var mouseMove = new THREE.Vector3();
							mouseMove.x = (e.clientX / renderer.domElement.width) * 2 - 1;
							mouseMove.y = - (e.clientY / renderer.domElement.height) * 2 + 1;
							mouseMove.z = 1;

							mouseMove.unproject(camera);
							var ray = new THREE.Ray(camera.position, mouseMove.sub(camera.position).normalize());
							var intersection = ray.intersectPlane(plane);

							updateActiveControlPointPosition(intersection.x, intersection.y, intersection.z);
						});

						$(document).on("mouseup", function(e) {
							$(document).off("mousemove");
							$(document).off("mouseup");
							cameraControls.enabled = true;  // enabling camera controls again
						});
					}
				}
			}

				     






			// all kinds of lights
			var ambientLight = new THREE.AmbientLight(0x0c0c0c);
			scene.add(ambientLight);

			var spotLightBelow = new THREE.SpotLight(0xffffff);
			spotLightBelow.position.set(20, -40, 20);
			spotLightBelow.target = bezierSurface;
			spotLightBelow.exponent = 5;
			scene.add(spotLightBelow);

			var spotLightAbove = new THREE.SpotLight(0xffffff);
			spotLightAbove.position.set(20,40,20);
			spotLightAbove.target = bezierSurface;
			spotLightAbove.exponent = 3;
			scene.add(spotLightAbove);   






			// functions for rendering and updating scene
			function renderScene() {
				renderer.render(scene, camera);
			}
			
			function update() {
				cameraControls.update();
				requestAnimationFrame(update);
			}
			update();

			$("#WebGL-output").append(renderer.domElement);
			renderScene();
		});
	</script>
</body>
</html> 
/**
 * @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 mrflix / http://felixniklas.de
 */
/*global THREE, console */

// This set of controls performs orbiting, dollying (zooming), and panning. It maintains
// the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is
// supported.
//
//    Orbit - left mouse / touch: one finger move
//    Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
//    Pan - right mouse, or arrow keys / touch: three finter swipe
//
// This is a drop-in replacement for (most) TrackballControls used in examples.
// That is, include this js file and wherever you see:
//    	controls = new THREE.TrackballControls( camera );
//      controls.target.z = 150;
// Simple substitute "OrbitControls" and the control should work as-is.

THREE.OrbitControls = function ( object, domElement, localElement ) {

	this.object = object;
	this.domElement = ( domElement !== undefined ) ? domElement : document;
	this.localElement = ( localElement !== undefined ) ? localElement : document;

	// API

	// Set to false to disable this control
	this.enabled = true;

	// "target" sets the location of focus, where the control orbits around
	// and where it pans with respect to.
	this.target = new THREE.Vector3();
	// center is old, deprecated; use "target" instead
	this.center = this.target;

	// This option actually enables dollying in and out; left as "zoom" for
	// backwards compatibility
	this.noZoom = false;
	this.zoomSpeed = 1.0;
	// Limits to how far you can dolly in and out
	this.minDistance = 0;
	this.maxDistance = Infinity;

	// Set to true to disable this control
	this.noRotate = false;
	this.rotateSpeed = 1.0;

	// Set to true to disable this control
	this.noPan = false;
	this.keyPanSpeed = 7.0;	// pixels moved per arrow key push

	// Set to true to automatically rotate around the target
	this.autoRotate = false;
	this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60

	// 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

	// Set to true to disable use of the keys
	this.noKeys = false;
	// The four arrow keys
	this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };

	////////////
	// internals

	var scope = this;

	var EPS = 0.000001;

	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();

	var phiDelta = 0;
	var thetaDelta = 0;
	var scale = 1;
	var pan = new THREE.Vector3();

	var lastPosition = new THREE.Vector3();

	var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };
	var state = STATE.NONE;

	// events

	var changeEvent = { type: 'change' };


	this.rotateLeft = function ( angle ) {

		if ( angle === undefined ) {

			angle = getAutoRotationAngle();

		}

		thetaDelta -= angle;

	};

	this.rotateUp = function ( angle ) {

		if ( angle === undefined ) {

			angle = getAutoRotationAngle();

		}

		phiDelta -= angle;

	};

	// pass in distance in world space to move left
	this.panLeft = function ( distance ) {

		var panOffset = new THREE.Vector3();
		var te = this.object.matrix.elements;
		// get X column of matrix
		panOffset.set( te[0], te[1], te[2] );
		panOffset.multiplyScalar(-distance);
		
		pan.add( panOffset );

	};

	// pass in distance in world space to move up
	this.panUp = function ( distance ) {

		var panOffset = new THREE.Vector3();
		var te = this.object.matrix.elements;
		// get Y column of matrix
		panOffset.set( te[4], te[5], te[6] );
		panOffset.multiplyScalar(distance);
		
		pan.add( panOffset );
	};
	
	// main entry point; pass in Vector2 of change desired in pixel space,
	// right and down are positive
	this.pan = function ( delta ) {

		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;

		if ( scope.object.fov !== undefined ) {

			// perspective
			var position = scope.object.position;
			var offset = position.clone().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 actually don't use screenWidth, since perspective camera is fixed to screen height
			scope.panLeft( 2 * delta.x * targetDistance / element.clientHeight );
			scope.panUp( 2 * delta.y * targetDistance / element.clientHeight );

		} else if ( scope.object.top !== undefined ) {

			// orthographic
			scope.panLeft( delta.x * (scope.object.right - scope.object.left) / element.clientWidth );
			scope.panUp( delta.y * (scope.object.top - scope.object.bottom) / element.clientHeight );

		} else {

			// camera neither orthographic or perspective - warn user
			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );

		}

	};

	this.dollyIn = function ( dollyScale ) {

		if ( dollyScale === undefined ) {

			dollyScale = getZoomScale();

		}

		scale /= dollyScale;

	};

	this.dollyOut = function ( dollyScale ) {

		if ( dollyScale === undefined ) {

			dollyScale = getZoomScale();

		}

		scale *= dollyScale;

	};

	this.update = function () {

		var position = this.object.position;
		var offset = position.clone().sub( this.target );

		// angle from z-axis around y-axis

		var theta = Math.atan2( offset.x, offset.z );

		// angle from y-axis

		var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );

		if ( this.autoRotate ) {

			this.rotateLeft( getAutoRotationAngle() );

		}

		theta += thetaDelta;
		phi += phiDelta;

		// restrict phi to be between desired limits
		phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) );

		// restrict phi to be betwee EPS and PI-EPS
		phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );

		var radius = offset.length() * scale;

		// restrict radius to be between desired limits
		radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
		
		// move target to panned location
		this.target.add( pan );

		offset.x = radius * Math.sin( phi ) * Math.sin( theta );
		offset.y = radius * Math.cos( phi );
		offset.z = radius * Math.sin( phi ) * Math.cos( theta );

		position.copy( this.target ).add( offset );

		this.object.lookAt( this.target );

		thetaDelta = 0;
		phiDelta = 0;
		scale = 1;
		pan.set(0,0,0);

		if ( lastPosition.distanceTo( this.object.position ) > 0 ) {

			this.dispatchEvent( changeEvent );

			lastPosition.copy( this.object.position );

		}

	};


	function getAutoRotationAngle() {

		return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;

	}

	function getZoomScale() {

		return Math.pow( 0.95, scope.zoomSpeed );

	}

	function onMouseDown( event ) {

		if ( scope.enabled === false ) { return; }
		event.preventDefault();

		if ( event.button === 0 ) {
			if ( scope.noRotate === true ) { return; }

			state = STATE.ROTATE;

			rotateStart.set( event.clientX, event.clientY );

		} else if ( event.button === 1 ) {
			if ( scope.noZoom === true ) { return; }

			state = STATE.DOLLY;

			dollyStart.set( event.clientX, event.clientY );

		} else if ( event.button === 2 ) {
			if ( scope.noPan === true ) { return; }

			state = STATE.PAN;

			panStart.set( event.clientX, event.clientY );

		}

		// Greggman fix: https://github.com/greggman/three.js/commit/fde9f9917d6d8381f06bf22cdff766029d1761be
		scope.domElement.addEventListener( 'mousemove', onMouseMove, false );
		scope.domElement.addEventListener( 'mouseup', onMouseUp, false );

	}

	function onMouseMove( event ) {

		if ( scope.enabled === false ) return;

		event.preventDefault();

		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;

		if ( state === STATE.ROTATE ) {

			if ( scope.noRotate === true ) return;

			rotateEnd.set( event.clientX, event.clientY );
			rotateDelta.subVectors( rotateEnd, rotateStart );

			// rotating across whole screen goes 360 degrees around
			scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
			// rotating up and down along whole screen attempts to go 360, but limited to 180
			scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );

			rotateStart.copy( rotateEnd );

		} else if ( state === STATE.DOLLY ) {

			if ( scope.noZoom === true ) return;

			dollyEnd.set( event.clientX, event.clientY );
			dollyDelta.subVectors( dollyEnd, dollyStart );

			if ( dollyDelta.y > 0 ) {

				scope.dollyIn();

			} else {

				scope.dollyOut();

			}

			dollyStart.copy( dollyEnd );

		} else if ( state === STATE.PAN ) {

			if ( scope.noPan === true ) return;

			panEnd.set( event.clientX, event.clientY );
			panDelta.subVectors( panEnd, panStart );
			
			scope.pan( panDelta );

			panStart.copy( panEnd );

		}

		// Greggman fix: https://github.com/greggman/three.js/commit/fde9f9917d6d8381f06bf22cdff766029d1761be
		scope.update();

	}

	function onMouseUp( /* event */ ) {

		if ( scope.enabled === false ) return;

		// Greggman fix: https://github.com/greggman/three.js/commit/fde9f9917d6d8381f06bf22cdff766029d1761be
		scope.domElement.removeEventListener( 'mousemove', onMouseMove, false );
		scope.domElement.removeEventListener( 'mouseup', onMouseUp, false );

		state = STATE.NONE;

	}

	function onMouseWheel( event ) {

		if ( scope.enabled === false || scope.noZoom === true ) return;

		var delta = 0;

		if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9

			delta = event.wheelDelta;

		} else if ( event.detail ) { // Firefox

			delta = - event.detail;

		}

		if ( delta > 0 ) {

			scope.dollyOut();

		} else {

			scope.dollyIn();

		}

	}

	function onKeyDown( event ) {

		if ( scope.enabled === false ) { return; }
		if ( scope.noKeys === true ) { return; }
		if ( scope.noPan === true ) { return; }

		// pan a pixel - I guess for precise positioning?
		// Greggman fix: https://github.com/greggman/three.js/commit/fde9f9917d6d8381f06bf22cdff766029d1761be
		var needUpdate = false;
		
		switch ( event.keyCode ) {

			case scope.keys.UP:
				scope.pan( new THREE.Vector2( 0, scope.keyPanSpeed ) );
				needUpdate = true;
				break;
			case scope.keys.BOTTOM:
				scope.pan( new THREE.Vector2( 0, -scope.keyPanSpeed ) );
				needUpdate = true;
				break;
			case scope.keys.LEFT:
				scope.pan( new THREE.Vector2( scope.keyPanSpeed, 0 ) );
				needUpdate = true;
				break;
			case scope.keys.RIGHT:
				scope.pan( new THREE.Vector2( -scope.keyPanSpeed, 0 ) );
				needUpdate = true;
				break;
		}

		// Greggman fix: https://github.com/greggman/three.js/commit/fde9f9917d6d8381f06bf22cdff766029d1761be
		if ( needUpdate ) {

			scope.update();

		}

	}
	
	function touchstart( event ) {

		if ( scope.enabled === false ) { return; }

		switch ( event.touches.length ) {

			case 1:	// one-fingered touch: rotate
				if ( scope.noRotate === true ) { return; }

				state = STATE.TOUCH_ROTATE;

				rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			case 2:	// two-fingered touch: dolly
				if ( scope.noZoom === true ) { return; }

				state = STATE.TOUCH_DOLLY;

				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 );
				break;

			case 3: // three-fingered touch: pan
				if ( scope.noPan === true ) { return; }

				state = STATE.TOUCH_PAN;

				panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			default:
				state = STATE.NONE;

		}
	}

	function touchmove( event ) {

		if ( scope.enabled === false ) { return; }

		event.preventDefault();
		event.stopPropagation();

		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;

		switch ( event.touches.length ) {

			case 1: // one-fingered touch: rotate
				if ( scope.noRotate === true ) { return; }
				if ( state !== STATE.TOUCH_ROTATE ) { return; }

				rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				rotateDelta.subVectors( rotateEnd, rotateStart );

				// rotating across whole screen goes 360 degrees around
				scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
				// rotating up and down along whole screen attempts to go 360, but limited to 180
				scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );

				rotateStart.copy( rotateEnd );
				break;

			case 2: // two-fingered touch: dolly
				if ( scope.noZoom === true ) { return; }
				if ( state !== STATE.TOUCH_DOLLY ) { return; }

				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.subVectors( dollyEnd, dollyStart );

				if ( dollyDelta.y > 0 ) {

					scope.dollyOut();

				} else {

					scope.dollyIn();

				}

				dollyStart.copy( dollyEnd );
				break;

			case 3: // three-fingered touch: pan
				if ( scope.noPan === true ) { return; }
				if ( state !== STATE.TOUCH_PAN ) { return; }

				panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				panDelta.subVectors( panEnd, panStart );
				
				scope.pan( panDelta );

				panStart.copy( panEnd );
				break;

			default:
				state = STATE.NONE;

		}

	}

	function touchend( /* event */ ) {

		if ( scope.enabled === false ) { return; }

		state = STATE.NONE;
	}

	this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
	this.localElement.addEventListener( 'mousedown', onMouseDown, false );
	this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
	this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox

	this.domElement.addEventListener( 'keydown', onKeyDown, false );

	this.localElement.addEventListener( 'touchstart', touchstart, false );
	this.domElement.addEventListener( 'touchend', touchend, false );
	this.domElement.addEventListener( 'touchmove', touchmove, false );

};

THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/r120/three.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js