[
  {
    "path": ".gitignore",
    "content": "node_modules/\n"
  },
  {
    "path": "README.md",
    "content": "# Three.js Trail Renderer\n\nBasic trail renderer for Three.js. This library allows for the straight-forward attachment of motion trails to any 3D object. The programmer simply has to specify the shape of the trail and its target (the target must be a Three.js Object3D instance). The update of the trail is handled automatically by the library. \n\nThe shape of the trail is specified by supplying a list of 3D points. These points make up the local-space head of the trail. During the update phase for the trail, a new instance of the head is created by transforming the local points into world-space using the target Object3D instance's local-to-world transformation matrix. These points are then connected to the existing head of the trail to extend the trail's geometry. \n\nThe trail renderer currently supports both textured and non-textured trails. Non-textured trails work well with many trail shapes in both translucent and opaque modes. Textured trails work well with many shapes as long as the trail is opaque; if the trail is not opaque, flat shapes work best. \n\nDemo of the effect can be seen [here](http://projects.markkellogg.org/threejs/demo_trail_renderer.php)\n\nThe following code shows how to attach a trail renderer in a scene named 'scene' to an existing Object3D instance named 'trailTarget'.\n\n```javascript\n// specify points to create planar trail-head geometry\nconst trailHeadGeometry = [];\ntrailHeadGeometry.push( \n  new THREE.Vector3( -10.0, 0.0, 0.0 ), \n  new THREE.Vector3( 0.0, 0.0, 0.0 ), \n  new THREE.Vector3( 10.0, 0.0, 0.0 ) \n);\n\n// create the trail renderer object\nconst trail = new TrailRenderer( scene, false );\n\n// set how often a new trail node will be added and existing nodes will be updated\ntrail.setAdvanceFrequency(30);\n\n// create material for the trail renderer\nconst trailMaterial = TrailRenderer.createBaseMaterial();\t\n\n// specify length of trail\nconst trailLength = 150;\n\n// initialize the trail\ntrail.initialize( trailMaterial, trailLength, false, 0, trailHeadGeometry, trailTarget );\n\n// activate the trail\ntrail.activate();\n\nfunction animate() {\n    requestAnimationFrame( animate );\n    trail.update();\n    render();\n \n}\n\n```"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en-US\">\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n    <script type=\"importmap\">\n        {\n            \"imports\": {\n                \"three\": \"./js/libs/three.module.min.js\"\n            }\n        }\n    </script>\n    <script src=\"js/libs/stats.min.js\"></script>  \n    <script src='js/libs/dat.gui.min.js'></script>\n    <style>\n        body {\n            font-family: Monospace;\n            background-color: #000;\n            color: #fff;\n            margin: 0px;\n            overflow: hidden;\n        }\n        #info {\n            position: absolute;\n            top: 10px;\n            width: 100%;\n            text-align: center;\n            z-index: 50;\n            display:block;\n        }\n        #info a { color: #f00; font-weight: bold; text-decoration: underline; cursor: pointer }\n    </style>\n    <title>Three.js Trail Renderer</title>\n</head>\n<body>\n\n<div id=\"info\">\n    <a href=\"http://threejs.org\" target=\"_blank\">three.js</a> - Trail Renderer by <a href=\"https://github.com/mkkellogg\">mkkellogg</a>\n</div>\n\n<script type=\"module\" src=\"js/Main.js\"></script>\n\n<div id=\"renderingContainer\" style=\"position: absolute; left:0px; top:0px\"></div>\n\n<script>\n\n   \n</script>\n</body>\n</html>\n"
  },
  {
    "path": "js/Main.js",
    "content": "import * as THREE from 'three';\nimport { OrbitControls } from './OrbitControls.js';\nimport { TrailRenderer } from './TrailRenderer.js';\n\nconst TrailTypes = Object.freeze({\n    Basic : 1,\n    Textured : 2\n});\n\nconst TrailShapes = Object.freeze({\n    Plane : 1,\n    Star : 2,\n    Circle : 3\n});\n\nlet screenWidth, screenHeight;\nlet scene, gui, renderer, rendererContainer;\nlet camera, pointLight, ambientLight, controls, stats;\nlet options, lastFrameTime, elapsedSimTime;\nlet starPoints, circlePoints, planePoints;\nlet trailTarget;\nlet trailHeadGeometry, trail, lastTrailUpdateTime, lastTrailResetTime;\nlet trailMaterial, baseTrailMaterial, texturedTrailMaterial;\n\nwindow.addEventListener(\"load\", function load(event) {\n    window.removeEventListener(\"load\", load, false);\n    init();\n}, false);\n\nfunction init() {\n    elapsedSimTime = 0;\n    lastTrailUpdateTime = performance.now();\n    lastTrailResetTime = performance.now();\n    getScreenDimensions();\n    initTrailOptions();\n    initScene();\n    initGUI();\n    initListeners();\n    initLights();\n    initSceneGeometry(function() {\n        initTrailRenderers(function() {\n            initRenderer();\n            initControls();\n            initStats();\n            animate();\n        });\n    });\n}\n\nfunction initTrailOptions() {\n     options = {\n        headRed : 1.0,\n        headGreen : 0.0,\n        headBlue : 0.0,\n        headAlpha : 1.0,\n        tailRed : 1.0,\n        tailGreen : 0.4,\n        tailBlue : 1.0,\n        tailAlpha : 1.0,\n        trailLength : 700,\n        trailType : TrailTypes.Textured,\n        trailShape : TrailShapes.Star,\n        textureTileFactorS : 10.0,\n        textureTileFactorT : 0.8,\n        dragTexture : true,\n        depthWrite : true,\n        pauseSim : false\n    };\n}\n\nfunction initGUI() {\n    gui = new dat.GUI();  \n\n    gui.add(options, 'trailType', {Basic: TrailTypes.Basic, Textured: TrailTypes.Textured}).name(\"Trail type\").onChange(function() {\n        options.trailType = parseInt(options.trailType);\n        updateTrailType();\n    });\n\n    gui.add(options, 'trailShape', { \n        Plane: TrailShapes.Plane, \n        Star: TrailShapes.Star, \n        Circle: TrailShapes.Circle \n    }).name(\"Trail shape\").onChange(function() {\n        options.trailShape = parseInt(options.trailShape);\n        updateTrailShape();\n    });\n\n    gui.add(options, \"trailLength\", 0, 1000).name(\"Trail length\").onChange(updateTrailLength);    \n    gui.add(options, 'depthWrite').name(\"Depth write\").onChange(updateTrailDepthWrite);\n    gui.add(options, 'pauseSim').name(\"Pause simulation\").onChange(pauseResumeSimulation);\n    \n    const headColor = gui.addFolder(\"Head color\");\n    headColor.add(options, \"headRed\", 0.0, 1.0, 0.025).name(\"Red\").onChange(updateTrailColors);\n    headColor.add(options, \"headGreen\", 0.0, 1.0, 0.025).name(\"Green\").onChange(updateTrailColors);\n    headColor.add(options, \"headBlue\", 0.0, 1.0, 0.025).name(\"Blue\").onChange(updateTrailColors);\n    headColor.add(options, \"headAlpha\", 0.0, 1.0, 0.025).name(\"Alpha\").onChange(updateTrailColors);\n\n    const tailColor = gui.addFolder(\"Tail color\");\n    tailColor.add(options, \"tailRed\", 0.0, 1.0, 0.025).name(\"Red\").onChange(updateTrailColors);\n    tailColor.add(options, \"tailGreen\", 0.0, 1.0, 0.025).name(\"Green\").onChange(updateTrailColors);\n    tailColor.add(options, \"tailBlue\", 0.0, 1.0, 0.025).name(\"Blue\").onChange(updateTrailColors);\n    tailColor.add(options, \"tailAlpha\", 0.0, 1.0, 0.025).name(\"Alpha\").onChange(updateTrailColors);\n\n    const textureOptions = gui.addFolder(\"Texture options\");\n    textureOptions.add(options, \"textureTileFactorS\", 0, 25).name(\"Texture Tile S\").onChange(updateTrailTextureTileSize);\n    textureOptions.add(options, \"textureTileFactorT\", 0, 25).name(\"Texture Tile T\").onChange(updateTrailTextureTileSize);\n    textureOptions.add(options, 'dragTexture').name(\"Drag Texture\").onChange(updateTrailTextureDrag);\n\n    gui.domElement.parentNode.style.zIndex = 100;\n}\n\nfunction initListeners() {\n    window.addEventListener('resize', onWindowResize, false);\n}\n\nfunction initRenderer() {\n    renderer = new THREE.WebGLRenderer();\n    renderer.setSize(screenWidth, screenHeight);\n    renderer.setClearColor(0x000000);\n    renderer.sortObjects = false;\n    rendererContainer = document.getElementById('renderingContainer');\n    rendererContainer.appendChild(renderer.domElement);\n}\n\nfunction initLights() {\n    ambientLight = new THREE.AmbientLight(0x777777);\n    scene.add(ambientLight);\n    pointLight = new THREE.DirectionalLight(0xFFFFFF, 2);\n    pointLight.position.set(5, 2, 5);\n    scene.add(pointLight);\n}\n\nfunction initSceneGeometry(onFinished) {\n    initTrailHeadGeometries();\n    initTrailTarget();\n    if(onFinished) {\n        onFinished();\n    }\n}\n\nfunction initTrailHeadGeometries() {\n    planePoints = [];\n    planePoints.push(new THREE.Vector3(-14.0, 4.0, 0.0), new THREE.Vector3(0.0, 4.0, 0.0), new THREE.Vector3(14.0, 4.0, 0.0));\n\n    circlePoints = [];\n    const twoPI = Math.PI * 2;\n    let index = 0;\n    const scale = 10.0;\n    const inc = twoPI / 32.0;\n\n    for (let i = 0; i <= twoPI + inc; i+= inc)  {\n        const vector = new THREE.Vector3();\n        vector.set(Math.cos(i) * scale, Math.sin(i) * scale, 0);\n        circlePoints[ index ] = vector;\n        index++;\n    }\n\n    starPoints = [];\n    starPoints.push(new THREE.Vector3(0,  16));\n    starPoints.push(new THREE.Vector3(4,  4));\n    starPoints.push(new THREE.Vector3(16,  4));\n    starPoints.push(new THREE.Vector3(8, -4));\n    starPoints.push(new THREE.Vector3(12, -16));\n    starPoints.push(new THREE.Vector3( 0, -8));\n    starPoints.push(new THREE.Vector3(-12, -16));\n    starPoints.push(new THREE.Vector3(-8, -4));\n    starPoints.push(new THREE.Vector3(-16,  4));\n    starPoints.push(new THREE.Vector3(-4,  4));\n    starPoints.push(new THREE.Vector3(0,  16));\n}\n\nfunction initTrailTarget() {\n    const starShape = new THREE.Shape(starPoints);\n\n    const extrusionSettings = {\n        amount: 2, size: 2, height: 1, curveSegments: 3,\n        bevelThickness: 1, bevelSize: 2, bevelEnabled: false,\n        material: 0, extrudeMaterial: 1\n    };\n\n    const starGeometry = new THREE.ExtrudeGeometry(starShape, extrusionSettings);\n\n    const trailTargetMaterial = new THREE.MeshStandardMaterial({\n        color: 0xffffff,\n        roughness: 0.7,\n        metalness: 0.3\n    });\n\n    trailTarget = new THREE.Mesh(starGeometry, trailTargetMaterial);\n    trailTarget.position.set(0, 0, 0);\n    trailTarget.scale.multiplyScalar(1);\n    trailTarget.receiveShadow = false;\n    trailTarget.matrixAutoUpdate = false;\n    scene.add(trailTarget);\n}\n\nfunction initTrailRenderers(callback) {\n    trail = new TrailRenderer(scene, false);\n    trail.setAdvanceFrequency(30);\n    baseTrailMaterial = TrailRenderer.createBaseMaterial();\n    const textureLoader = new THREE.TextureLoader();\n    textureLoader.load(\"textures/sparkle4.jpg\", function(tex) {\n        tex.wrapS = THREE.RepeatWrapping;\n        tex.wrapT = THREE.RepeatWrapping;\n        texturedTrailMaterial = TrailRenderer.createTexturedMaterial();\n        texturedTrailMaterial.uniforms.trailTexture.value = tex;\n        setTrailShapeFromOptions();\n        setTrailTypeFromOptions();\n        initializeTrail();\n        if (callback) {\n            callback();\n        }\n    });\n}\n\nfunction updateTrailLength() {\n    initializeTrail();\n}\n\nfunction setTrailTypeFromOptions() {\n    switch (options.trailType) {\n        case TrailTypes.Basic:\n            trailMaterial = baseTrailMaterial;\n        break;\n        case TrailTypes.Textured:\n            trailMaterial = texturedTrailMaterial;\n        break;\n    }\n}\n\nfunction updateTrailType() {\n    setTrailTypeFromOptions();\n    initializeTrail();\n}\n\nfunction setTrailShapeFromOptions() {\n    switch (options.trailShape) {\n        case TrailShapes.Plane:\n            trailHeadGeometry = planePoints;\n        break;\n        case TrailShapes.Star:\n            trailHeadGeometry = starPoints;\n        break;\n        case TrailShapes.Circle:\n            trailHeadGeometry = circlePoints;\n        break;\n    }\n}\n\nfunction updateTrailShape() {\n    setTrailShapeFromOptions();\n    initializeTrail();\n}\n\nfunction updateTrailTextureDrag() {\n    initializeTrail();\n}\n\nfunction updateTrailTextureTileSize() {\n    trailMaterial.uniforms.textureTileFactor.value.set(options.textureTileFactorS, options.textureTileFactorT);\n}\n\nfunction updateTrailColors() {\n    trailMaterial.uniforms.headColor.value.set(options.headRed, options.headGreen, options.headBlue, options.headAlpha);\n    trailMaterial.uniforms.tailColor.value.set(options.tailRed, options.tailGreen, options.tailBlue, options.tailAlpha);\n}\n\nfunction updateTrailDepthWrite() {\n    trailMaterial.depthWrite = options.depthWrite;\n}\n\nconst updateTrailTarget = function updateTrailTarget() {\n\n    const tempQuaternion = new THREE.Quaternion();\n\n    const baseForward = new THREE.Vector3(0, 0, -1);\n    const tempForward = new THREE.Vector3();\n    const tempUp = new THREE.Vector3();\n\n    const tempRotationMatrix = new THREE.Matrix4();\n    const tempTranslationMatrix = new THREE.Matrix4();\n\n    const currentTargetPosition = new THREE.Vector3();\n    const lastTargetPosition = new THREE.Vector3();\n\n    const currentDirection = new THREE.Vector3();\n    const lastDirection = new THREE.Vector3();\n\n    const lastRotationMatrix = new THREE.Matrix4();\n\n    return function updateTrailTarget(time) {\n\n        trail.update();\n\n        tempRotationMatrix.identity();\n        tempTranslationMatrix.identity();\n\n        const scaledTime = time * .001;\n        const areaScale = 100;\n\n        lastTargetPosition.copy(currentTargetPosition);\n\n        currentTargetPosition.x = Math.sin(scaledTime) * areaScale;\n        currentTargetPosition.y = Math.sin(scaledTime * 1.1) * areaScale;\n        currentTargetPosition.z = Math.sin(scaledTime * 1.6) * areaScale;\n\n        currentDirection.copy(currentTargetPosition);\n        currentDirection.sub(lastTargetPosition);\n        if (currentDirection.lengthSq() < .001) {\n            currentDirection.copy(lastDirection);\n        } else {\n            currentDirection.normalize();\n        }\n\n        tempUp.crossVectors(currentDirection, baseForward);\n        const angle = baseForward.angleTo(currentDirection);\n\n        if(Math.abs(angle) > .01 && tempUp.lengthSq() > .001) {\n            tempQuaternion.setFromUnitVectors(baseForward, currentDirection);\n            tempQuaternion.normalize();\n            tempRotationMatrix.makeRotationFromQuaternion(tempQuaternion);\n            lastRotationMatrix.copy(tempRotationMatrix);\n        }\n        \n        tempTranslationMatrix.makeTranslation (currentTargetPosition.x, currentTargetPosition.y, currentTargetPosition.z);\n        tempTranslationMatrix.multiply(tempRotationMatrix);  \n\n        trailTarget.matrix.identity();\n        trailTarget.applyMatrix4(tempTranslationMatrix);\n        trailTarget.updateMatrixWorld();\n\n        lastDirection.copy(currentDirection);\n    }\n\n}();\n\nfunction initializeTrail() {\n    trail.initialize(trailMaterial, Math.floor(options.trailLength), options.dragTexture ? 1.0 : 0.0, 0, trailHeadGeometry, trailTarget);\n    updateTrailColors();\n    updateTrailTextureTileSize();\n    updateTrailDepthWrite();\n    trail.activate();\n}\n\nfunction initScene() {\n    scene = new THREE.Scene();\n    camera = new THREE.PerspectiveCamera(45, 1.0, 2, 2000);\n    scene.add(camera);\n    resetCamera();\n}\n\nfunction initStats() {\n    stats = new Stats();\n    stats.domElement.style.position = 'absolute';\n    stats.domElement.style.bottom = '0px';\n    stats.domElement.style.zIndex = 100;\n    rendererContainer.appendChild(stats.domElement);\n}\n\nfunction initControls() {\n    controls = new OrbitControls(camera, renderer.domElement);\n    controls.target.set(0, 0, 0);\n    controls.update();\n}\n\nfunction onWindowResize() {\n    getScreenDimensions();\n    renderer.setSize(screenWidth, screenHeight);\n    resetCamera();\n}\n\nfunction getScreenDimensions() {\n    screenWidth = window.innerWidth;\n    screenHeight = window.innerHeight;\n}\n\nfunction resetCamera() {\n    getScreenDimensions();\n    camera.aspect = screenWidth / screenHeight;\n    camera.updateProjectionMatrix();\n    camera.position.set(0, 200, 400);\n    camera.lookAt(scene.position);\n}\n\nfunction animate() {\n    requestAnimationFrame(animate);\n    update();\n    render();\n}\n\nfunction pauseResumeSimulation() {\n    if (trail.paused) {\n        trail.pause();\n    } else {\n        trail.resume();\n    }\n}\n\nfunction update() {\n    const currentTime = performance.now();\n    const deltaTime = lastFrameTime ? currentTime - lastFrameTime : 0;\n    if (!options.pauseSim){\n        elapsedSimTime += deltaTime;\n        updateTrailTarget(elapsedSimTime);\n    }\n    lastFrameTime = currentTime;\n    controls.update();\n    stats.update();\n}\n\nfunction render() {\n    renderer.render(scene, camera);\n}\n"
  },
  {
    "path": "js/OrbitControls.js",
    "content": "import {\n\tEventDispatcher,\n\tMOUSE,\n\tQuaternion,\n\tSpherical,\n\tTOUCH,\n\tVector2,\n\tVector3,\n\tPlane,\n\tRay,\n\tMathUtils\n} from 'three';\n\n// OrbitControls performs orbiting, dollying (zooming), and panning.\n// Unlike TrackballControls, it maintains the \"up\" direction object.up (+Y by default).\n//\n//    Orbit - left mouse / touch: one-finger move\n//    Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish\n//    Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move\n\nconst _changeEvent = { type: 'change' };\nconst _startEvent = { type: 'start' };\nconst _endEvent = { type: 'end' };\nconst _ray = new Ray();\nconst _plane = new Plane();\nconst TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );\n\nclass OrbitControls extends EventDispatcher {\n\n\tconstructor( object, domElement ) {\n\n\t\tsuper();\n\n\t\tthis.object = object;\n\t\tthis.domElement = domElement;\n\t\tthis.domElement.style.touchAction = 'none'; // disable touch scroll\n\n\t\t// Set to false to disable this control\n\t\tthis.enabled = true;\n\n\t\t// \"target\" sets the location of focus, where the object orbits around\n\t\tthis.target = new Vector3();\n\n\t\t// How far you can dolly in and out ( PerspectiveCamera only )\n\t\tthis.minDistance = 0;\n\t\tthis.maxDistance = Infinity;\n\n\t\t// How far you can zoom in and out ( OrthographicCamera only )\n\t\tthis.minZoom = 0;\n\t\tthis.maxZoom = Infinity;\n\n\t\t// How far you can orbit vertically, upper and lower limits.\n\t\t// Range is 0 to Math.PI radians.\n\t\tthis.minPolarAngle = 0; // radians\n\t\tthis.maxPolarAngle = Math.PI; // radians\n\n\t\t// How far you can orbit horizontally, upper and lower limits.\n\t\t// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )\n\t\tthis.minAzimuthAngle = - Infinity; // radians\n\t\tthis.maxAzimuthAngle = Infinity; // radians\n\n\t\t// Set to true to enable damping (inertia)\n\t\t// If damping is enabled, you must call controls.update() in your animation loop\n\t\tthis.enableDamping = false;\n\t\tthis.dampingFactor = 0.05;\n\n\t\t// This option actually enables dollying in and out; left as \"zoom\" for backwards compatibility.\n\t\t// Set to false to disable zooming\n\t\tthis.enableZoom = true;\n\t\tthis.zoomSpeed = 1.0;\n\n\t\t// Set to false to disable rotating\n\t\tthis.enableRotate = true;\n\t\tthis.rotateSpeed = 1.0;\n\n\t\t// Set to false to disable panning\n\t\tthis.enablePan = true;\n\t\tthis.panSpeed = 1.0;\n\t\tthis.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up\n\t\tthis.keyPanSpeed = 7.0;\t// pixels moved per arrow key push\n\t\tthis.zoomToCursor = false;\n\n\t\t// Set to true to automatically rotate around the target\n\t\t// If auto-rotate is enabled, you must call controls.update() in your animation loop\n\t\tthis.autoRotate = false;\n\t\tthis.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60\n\n\t\t// The four arrow keys\n\t\tthis.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };\n\n\t\t// Mouse buttons\n\t\tthis.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };\n\n\t\t// Touch fingers\n\t\tthis.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };\n\n\t\t// for reset\n\t\tthis.target0 = this.target.clone();\n\t\tthis.position0 = this.object.position.clone();\n\t\tthis.zoom0 = this.object.zoom;\n\n\t\t// the target DOM element for key events\n\t\tthis._domElementKeyEvents = null;\n\n\t\t//\n\t\t// public methods\n\t\t//\n\n\t\tthis.getPolarAngle = function () {\n\n\t\t\treturn spherical.phi;\n\n\t\t};\n\n\t\tthis.getAzimuthalAngle = function () {\n\n\t\t\treturn spherical.theta;\n\n\t\t};\n\n\t\tthis.getDistance = function () {\n\n\t\t\treturn this.object.position.distanceTo( this.target );\n\n\t\t};\n\n\t\tthis.listenToKeyEvents = function ( domElement ) {\n\n\t\t\tdomElement.addEventListener( 'keydown', onKeyDown );\n\t\t\tthis._domElementKeyEvents = domElement;\n\n\t\t};\n\n\t\tthis.stopListenToKeyEvents = function () {\n\n\t\t\tthis._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );\n\t\t\tthis._domElementKeyEvents = null;\n\n\t\t};\n\n\t\tthis.saveState = function () {\n\n\t\t\tscope.target0.copy( scope.target );\n\t\t\tscope.position0.copy( scope.object.position );\n\t\t\tscope.zoom0 = scope.object.zoom;\n\n\t\t};\n\n\t\tthis.reset = function () {\n\n\t\t\tscope.target.copy( scope.target0 );\n\t\t\tscope.object.position.copy( scope.position0 );\n\t\t\tscope.object.zoom = scope.zoom0;\n\n\t\t\tscope.object.updateProjectionMatrix();\n\t\t\tscope.dispatchEvent( _changeEvent );\n\n\t\t\tscope.update();\n\n\t\t\tstate = STATE.NONE;\n\n\t\t};\n\n\t\t// this method is exposed, but perhaps it would be better if we can make it private...\n\t\tthis.update = function () {\n\n\t\t\tconst offset = new Vector3();\n\n\t\t\t// so camera.up is the orbit axis\n\t\t\tconst quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );\n\t\t\tconst quatInverse = quat.clone().invert();\n\n\t\t\tconst lastPosition = new Vector3();\n\t\t\tconst lastQuaternion = new Quaternion();\n\t\t\tconst lastTargetPosition = new Vector3();\n\n\t\t\tconst twoPI = 2 * Math.PI;\n\n\t\t\treturn function update() {\n\n\t\t\t\tconst position = scope.object.position;\n\n\t\t\t\toffset.copy( position ).sub( scope.target );\n\n\t\t\t\t// rotate offset to \"y-axis-is-up\" space\n\t\t\t\toffset.applyQuaternion( quat );\n\n\t\t\t\t// angle from z-axis around y-axis\n\t\t\t\tspherical.setFromVector3( offset );\n\n\t\t\t\tif ( scope.autoRotate && state === STATE.NONE ) {\n\n\t\t\t\t\trotateLeft( getAutoRotationAngle() );\n\n\t\t\t\t}\n\n\t\t\t\tif ( scope.enableDamping ) {\n\n\t\t\t\t\tspherical.theta += sphericalDelta.theta * scope.dampingFactor;\n\t\t\t\t\tspherical.phi += sphericalDelta.phi * scope.dampingFactor;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tspherical.theta += sphericalDelta.theta;\n\t\t\t\t\tspherical.phi += sphericalDelta.phi;\n\n\t\t\t\t}\n\n\t\t\t\t// restrict theta to be between desired limits\n\n\t\t\t\tlet min = scope.minAzimuthAngle;\n\t\t\t\tlet max = scope.maxAzimuthAngle;\n\n\t\t\t\tif ( isFinite( min ) && isFinite( max ) ) {\n\n\t\t\t\t\tif ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;\n\n\t\t\t\t\tif ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;\n\n\t\t\t\t\tif ( min <= max ) {\n\n\t\t\t\t\t\tspherical.theta = Math.max( min, Math.min( max, spherical.theta ) );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tspherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?\n\t\t\t\t\t\t\tMath.max( min, spherical.theta ) :\n\t\t\t\t\t\t\tMath.min( max, spherical.theta );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\t// restrict phi to be between desired limits\n\t\t\t\tspherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );\n\n\t\t\t\tspherical.makeSafe();\n\n\n\t\t\t\t// move target to panned location\n\n\t\t\t\tif ( scope.enableDamping === true ) {\n\n\t\t\t\t\tscope.target.addScaledVector( panOffset, scope.dampingFactor );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tscope.target.add( panOffset );\n\n\t\t\t\t}\n\n\t\t\t\t// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera\n\t\t\t\t// we adjust zoom later in these cases\n\t\t\t\tif ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) {\n\n\t\t\t\t\tspherical.radius = clampDistance( spherical.radius );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tspherical.radius = clampDistance( spherical.radius * scale );\n\n\t\t\t\t}\n\n\n\t\t\t\toffset.setFromSpherical( spherical );\n\n\t\t\t\t// rotate offset back to \"camera-up-vector-is-up\" space\n\t\t\t\toffset.applyQuaternion( quatInverse );\n\n\t\t\t\tposition.copy( scope.target ).add( offset );\n\n\t\t\t\tscope.object.lookAt( scope.target );\n\n\t\t\t\tif ( scope.enableDamping === true ) {\n\n\t\t\t\t\tsphericalDelta.theta *= ( 1 - scope.dampingFactor );\n\t\t\t\t\tsphericalDelta.phi *= ( 1 - scope.dampingFactor );\n\n\t\t\t\t\tpanOffset.multiplyScalar( 1 - scope.dampingFactor );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tsphericalDelta.set( 0, 0, 0 );\n\n\t\t\t\t\tpanOffset.set( 0, 0, 0 );\n\n\t\t\t\t}\n\n\t\t\t\t// adjust camera position\n\t\t\t\tlet zoomChanged = false;\n\t\t\t\tif ( scope.zoomToCursor && performCursorZoom ) {\n\n\t\t\t\t\tlet newRadius = null;\n\t\t\t\t\tif ( scope.object.isPerspectiveCamera ) {\n\n\t\t\t\t\t\t// move the camera down the pointer ray\n\t\t\t\t\t\t// this method avoids floating point error\n\t\t\t\t\t\tconst prevRadius = offset.length();\n\t\t\t\t\t\tnewRadius = clampDistance( prevRadius * scale );\n\n\t\t\t\t\t\tconst radiusDelta = prevRadius - newRadius;\n\t\t\t\t\t\tscope.object.position.addScaledVector( dollyDirection, radiusDelta );\n\t\t\t\t\t\tscope.object.updateMatrixWorld();\n\n\t\t\t\t\t} else if ( scope.object.isOrthographicCamera ) {\n\n\t\t\t\t\t\t// adjust the ortho camera position based on zoom changes\n\t\t\t\t\t\tconst mouseBefore = new Vector3( mouse.x, mouse.y, 0 );\n\t\t\t\t\t\tmouseBefore.unproject( scope.object );\n\n\t\t\t\t\t\tscope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );\n\t\t\t\t\t\tscope.object.updateProjectionMatrix();\n\t\t\t\t\t\tzoomChanged = true;\n\n\t\t\t\t\t\tconst mouseAfter = new Vector3( mouse.x, mouse.y, 0 );\n\t\t\t\t\t\tmouseAfter.unproject( scope.object );\n\n\t\t\t\t\t\tscope.object.position.sub( mouseAfter ).add( mouseBefore );\n\t\t\t\t\t\tscope.object.updateMatrixWorld();\n\n\t\t\t\t\t\tnewRadius = offset.length();\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );\n\t\t\t\t\t\tscope.zoomToCursor = false;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle the placement of the target\n\t\t\t\t\tif ( newRadius !== null ) {\n\n\t\t\t\t\t\tif ( this.screenSpacePanning ) {\n\n\t\t\t\t\t\t\t// position the orbit target in front of the new camera position\n\t\t\t\t\t\t\tscope.target.set( 0, 0, - 1 )\n\t\t\t\t\t\t\t\t.transformDirection( scope.object.matrix )\n\t\t\t\t\t\t\t\t.multiplyScalar( newRadius )\n\t\t\t\t\t\t\t\t.add( scope.object.position );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// get the ray and translation plane to compute target\n\t\t\t\t\t\t\t_ray.origin.copy( scope.object.position );\n\t\t\t\t\t\t\t_ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix );\n\n\t\t\t\t\t\t\t// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid\n\t\t\t\t\t\t\t// extremely large values\n\t\t\t\t\t\t\tif ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) {\n\n\t\t\t\t\t\t\t\tobject.lookAt( scope.target );\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t_plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target );\n\t\t\t\t\t\t\t\t_ray.intersectPlane( _plane, scope.target );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} else if ( scope.object.isOrthographicCamera ) {\n\n\t\t\t\t\tscope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );\n\t\t\t\t\tscope.object.updateProjectionMatrix();\n\t\t\t\t\tzoomChanged = true;\n\n\t\t\t\t}\n\n\t\t\t\tscale = 1;\n\t\t\t\tperformCursorZoom = false;\n\n\t\t\t\t// update condition is:\n\t\t\t\t// min(camera displacement, camera rotation in radians)^2 > EPS\n\t\t\t\t// using small-angle approximation cos(x/2) = 1 - x^2 / 8\n\n\t\t\t\tif ( zoomChanged ||\n\t\t\t\t\tlastPosition.distanceToSquared( scope.object.position ) > EPS ||\n\t\t\t\t\t8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ||\n\t\t\t\t\tlastTargetPosition.distanceToSquared( scope.target ) > 0 ) {\n\n\t\t\t\t\tscope.dispatchEvent( _changeEvent );\n\n\t\t\t\t\tlastPosition.copy( scope.object.position );\n\t\t\t\t\tlastQuaternion.copy( scope.object.quaternion );\n\t\t\t\t\tlastTargetPosition.copy( scope.target );\n\n\t\t\t\t\tzoomChanged = false;\n\n\t\t\t\t\treturn true;\n\n\t\t\t\t}\n\n\t\t\t\treturn false;\n\n\t\t\t};\n\n\t\t}();\n\n\t\tthis.dispose = function () {\n\n\t\t\tscope.domElement.removeEventListener( 'contextmenu', onContextMenu );\n\n\t\t\tscope.domElement.removeEventListener( 'pointerdown', onPointerDown );\n\t\t\tscope.domElement.removeEventListener( 'pointercancel', onPointerUp );\n\t\t\tscope.domElement.removeEventListener( 'wheel', onMouseWheel );\n\n\t\t\tscope.domElement.removeEventListener( 'pointermove', onPointerMove );\n\t\t\tscope.domElement.removeEventListener( 'pointerup', onPointerUp );\n\n\n\t\t\tif ( scope._domElementKeyEvents !== null ) {\n\n\t\t\t\tscope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );\n\t\t\t\tscope._domElementKeyEvents = null;\n\n\t\t\t}\n\n\t\t\t//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?\n\n\t\t};\n\n\t\t//\n\t\t// internals\n\t\t//\n\n\t\tconst scope = this;\n\n\t\tconst STATE = {\n\t\t\tNONE: - 1,\n\t\t\tROTATE: 0,\n\t\t\tDOLLY: 1,\n\t\t\tPAN: 2,\n\t\t\tTOUCH_ROTATE: 3,\n\t\t\tTOUCH_PAN: 4,\n\t\t\tTOUCH_DOLLY_PAN: 5,\n\t\t\tTOUCH_DOLLY_ROTATE: 6\n\t\t};\n\n\t\tlet state = STATE.NONE;\n\n\t\tconst EPS = 0.000001;\n\n\t\t// current position in spherical coordinates\n\t\tconst spherical = new Spherical();\n\t\tconst sphericalDelta = new Spherical();\n\n\t\tlet scale = 1;\n\t\tconst panOffset = new Vector3();\n\n\t\tconst rotateStart = new Vector2();\n\t\tconst rotateEnd = new Vector2();\n\t\tconst rotateDelta = new Vector2();\n\n\t\tconst panStart = new Vector2();\n\t\tconst panEnd = new Vector2();\n\t\tconst panDelta = new Vector2();\n\n\t\tconst dollyStart = new Vector2();\n\t\tconst dollyEnd = new Vector2();\n\t\tconst dollyDelta = new Vector2();\n\n\t\tconst dollyDirection = new Vector3();\n\t\tconst mouse = new Vector2();\n\t\tlet performCursorZoom = false;\n\n\t\tconst pointers = [];\n\t\tconst pointerPositions = {};\n\n\t\tfunction getAutoRotationAngle() {\n\n\t\t\treturn 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;\n\n\t\t}\n\n\t\tfunction getZoomScale() {\n\n\t\t\treturn Math.pow( 0.95, scope.zoomSpeed );\n\n\t\t}\n\n\t\tfunction rotateLeft( angle ) {\n\n\t\t\tsphericalDelta.theta -= angle;\n\n\t\t}\n\n\t\tfunction rotateUp( angle ) {\n\n\t\t\tsphericalDelta.phi -= angle;\n\n\t\t}\n\n\t\tconst panLeft = function () {\n\n\t\t\tconst v = new Vector3();\n\n\t\t\treturn function panLeft( distance, objectMatrix ) {\n\n\t\t\t\tv.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix\n\t\t\t\tv.multiplyScalar( - distance );\n\n\t\t\t\tpanOffset.add( v );\n\n\t\t\t};\n\n\t\t}();\n\n\t\tconst panUp = function () {\n\n\t\t\tconst v = new Vector3();\n\n\t\t\treturn function panUp( distance, objectMatrix ) {\n\n\t\t\t\tif ( scope.screenSpacePanning === true ) {\n\n\t\t\t\t\tv.setFromMatrixColumn( objectMatrix, 1 );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tv.setFromMatrixColumn( objectMatrix, 0 );\n\t\t\t\t\tv.crossVectors( scope.object.up, v );\n\n\t\t\t\t}\n\n\t\t\t\tv.multiplyScalar( distance );\n\n\t\t\t\tpanOffset.add( v );\n\n\t\t\t};\n\n\t\t}();\n\n\t\t// deltaX and deltaY are in pixels; right and down are positive\n\t\tconst pan = function () {\n\n\t\t\tconst offset = new Vector3();\n\n\t\t\treturn function pan( deltaX, deltaY ) {\n\n\t\t\t\tconst element = scope.domElement;\n\n\t\t\t\tif ( scope.object.isPerspectiveCamera ) {\n\n\t\t\t\t\t// perspective\n\t\t\t\t\tconst position = scope.object.position;\n\t\t\t\t\toffset.copy( position ).sub( scope.target );\n\t\t\t\t\tlet targetDistance = offset.length();\n\n\t\t\t\t\t// half of the fov is center to top of screen\n\t\t\t\t\ttargetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );\n\n\t\t\t\t\t// we use only clientHeight here so aspect ratio does not distort speed\n\t\t\t\t\tpanLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );\n\t\t\t\t\tpanUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );\n\n\t\t\t\t} else if ( scope.object.isOrthographicCamera ) {\n\n\t\t\t\t\t// orthographic\n\t\t\t\t\tpanLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );\n\t\t\t\t\tpanUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// camera neither orthographic nor perspective\n\t\t\t\t\tconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );\n\t\t\t\t\tscope.enablePan = false;\n\n\t\t\t\t}\n\n\t\t\t};\n\n\t\t}();\n\n\t\tfunction dollyOut( dollyScale ) {\n\n\t\t\tif ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {\n\n\t\t\t\tscale /= dollyScale;\n\n\t\t\t} else {\n\n\t\t\t\tconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );\n\t\t\t\tscope.enableZoom = false;\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction dollyIn( dollyScale ) {\n\n\t\t\tif ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {\n\n\t\t\t\tscale *= dollyScale;\n\n\t\t\t} else {\n\n\t\t\t\tconsole.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );\n\t\t\t\tscope.enableZoom = false;\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction updateMouseParameters( event ) {\n\n\t\t\tif ( ! scope.zoomToCursor ) {\n\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tperformCursorZoom = true;\n\n\t\t\tconst rect = scope.domElement.getBoundingClientRect();\n\t\t\tconst x = event.clientX - rect.left;\n\t\t\tconst y = event.clientY - rect.top;\n\t\t\tconst w = rect.width;\n\t\t\tconst h = rect.height;\n\n\t\t\tmouse.x = ( x / w ) * 2 - 1;\n\t\t\tmouse.y = - ( y / h ) * 2 + 1;\n\n\t\t\tdollyDirection.set( mouse.x, mouse.y, 1 ).unproject( object ).sub( object.position ).normalize();\n\n\t\t}\n\n\t\tfunction clampDistance( dist ) {\n\n\t\t\treturn Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) );\n\n\t\t}\n\n\t\t//\n\t\t// event callbacks - update the object state\n\t\t//\n\n\t\tfunction handleMouseDownRotate( event ) {\n\n\t\t\trotateStart.set( event.clientX, event.clientY );\n\n\t\t}\n\n\t\tfunction handleMouseDownDolly( event ) {\n\n\t\t\tupdateMouseParameters( event );\n\t\t\tdollyStart.set( event.clientX, event.clientY );\n\n\t\t}\n\n\t\tfunction handleMouseDownPan( event ) {\n\n\t\t\tpanStart.set( event.clientX, event.clientY );\n\n\t\t}\n\n\t\tfunction handleMouseMoveRotate( event ) {\n\n\t\t\trotateEnd.set( event.clientX, event.clientY );\n\n\t\t\trotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );\n\n\t\t\tconst element = scope.domElement;\n\n\t\t\trotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height\n\n\t\t\trotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );\n\n\t\t\trotateStart.copy( rotateEnd );\n\n\t\t\tscope.update();\n\n\t\t}\n\n\t\tfunction handleMouseMoveDolly( event ) {\n\n\t\t\tdollyEnd.set( event.clientX, event.clientY );\n\n\t\t\tdollyDelta.subVectors( dollyEnd, dollyStart );\n\n\t\t\tif ( dollyDelta.y > 0 ) {\n\n\t\t\t\tdollyOut( getZoomScale() );\n\n\t\t\t} else if ( dollyDelta.y < 0 ) {\n\n\t\t\t\tdollyIn( getZoomScale() );\n\n\t\t\t}\n\n\t\t\tdollyStart.copy( dollyEnd );\n\n\t\t\tscope.update();\n\n\t\t}\n\n\t\tfunction handleMouseMovePan( event ) {\n\n\t\t\tpanEnd.set( event.clientX, event.clientY );\n\n\t\t\tpanDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );\n\n\t\t\tpan( panDelta.x, panDelta.y );\n\n\t\t\tpanStart.copy( panEnd );\n\n\t\t\tscope.update();\n\n\t\t}\n\n\t\tfunction handleMouseWheel( event ) {\n\n\t\t\tupdateMouseParameters( event );\n\n\t\t\tif ( event.deltaY < 0 ) {\n\n\t\t\t\tdollyIn( getZoomScale() );\n\n\t\t\t} else if ( event.deltaY > 0 ) {\n\n\t\t\t\tdollyOut( getZoomScale() );\n\n\t\t\t}\n\n\t\t\tscope.update();\n\n\t\t}\n\n\t\tfunction handleKeyDown( event ) {\n\n\t\t\tlet needsUpdate = false;\n\n\t\t\tswitch ( event.code ) {\n\n\t\t\t\tcase scope.keys.UP:\n\n\t\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\t\trotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tpan( 0, scope.keyPanSpeed );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tneedsUpdate = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase scope.keys.BOTTOM:\n\n\t\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\t\trotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tpan( 0, - scope.keyPanSpeed );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tneedsUpdate = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase scope.keys.LEFT:\n\n\t\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\t\trotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tpan( scope.keyPanSpeed, 0 );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tneedsUpdate = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase scope.keys.RIGHT:\n\n\t\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\t\trotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tpan( - scope.keyPanSpeed, 0 );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tneedsUpdate = true;\n\t\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t\tif ( needsUpdate ) {\n\n\t\t\t\t// prevent the browser from scrolling on cursor keys\n\t\t\t\tevent.preventDefault();\n\n\t\t\t\tscope.update();\n\n\t\t\t}\n\n\n\t\t}\n\n\t\tfunction handleTouchStartRotate() {\n\n\t\t\tif ( pointers.length === 1 ) {\n\n\t\t\t\trotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );\n\n\t\t\t} else {\n\n\t\t\t\tconst x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );\n\t\t\t\tconst y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );\n\n\t\t\t\trotateStart.set( x, y );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction handleTouchStartPan() {\n\n\t\t\tif ( pointers.length === 1 ) {\n\n\t\t\t\tpanStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );\n\n\t\t\t} else {\n\n\t\t\t\tconst x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );\n\t\t\t\tconst y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );\n\n\t\t\t\tpanStart.set( x, y );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction handleTouchStartDolly() {\n\n\t\t\tconst dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX;\n\t\t\tconst dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY;\n\n\t\t\tconst distance = Math.sqrt( dx * dx + dy * dy );\n\n\t\t\tdollyStart.set( 0, distance );\n\n\t\t}\n\n\t\tfunction handleTouchStartDollyPan() {\n\n\t\t\tif ( scope.enableZoom ) handleTouchStartDolly();\n\n\t\t\tif ( scope.enablePan ) handleTouchStartPan();\n\n\t\t}\n\n\t\tfunction handleTouchStartDollyRotate() {\n\n\t\t\tif ( scope.enableZoom ) handleTouchStartDolly();\n\n\t\t\tif ( scope.enableRotate ) handleTouchStartRotate();\n\n\t\t}\n\n\t\tfunction handleTouchMoveRotate( event ) {\n\n\t\t\tif ( pointers.length == 1 ) {\n\n\t\t\t\trotateEnd.set( event.pageX, event.pageY );\n\n\t\t\t} else {\n\n\t\t\t\tconst position = getSecondPointerPosition( event );\n\n\t\t\t\tconst x = 0.5 * ( event.pageX + position.x );\n\t\t\t\tconst y = 0.5 * ( event.pageY + position.y );\n\n\t\t\t\trotateEnd.set( x, y );\n\n\t\t\t}\n\n\t\t\trotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );\n\n\t\t\tconst element = scope.domElement;\n\n\t\t\trotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height\n\n\t\t\trotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );\n\n\t\t\trotateStart.copy( rotateEnd );\n\n\t\t}\n\n\t\tfunction handleTouchMovePan( event ) {\n\n\t\t\tif ( pointers.length === 1 ) {\n\n\t\t\t\tpanEnd.set( event.pageX, event.pageY );\n\n\t\t\t} else {\n\n\t\t\t\tconst position = getSecondPointerPosition( event );\n\n\t\t\t\tconst x = 0.5 * ( event.pageX + position.x );\n\t\t\t\tconst y = 0.5 * ( event.pageY + position.y );\n\n\t\t\t\tpanEnd.set( x, y );\n\n\t\t\t}\n\n\t\t\tpanDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );\n\n\t\t\tpan( panDelta.x, panDelta.y );\n\n\t\t\tpanStart.copy( panEnd );\n\n\t\t}\n\n\t\tfunction handleTouchMoveDolly( event ) {\n\n\t\t\tconst position = getSecondPointerPosition( event );\n\n\t\t\tconst dx = event.pageX - position.x;\n\t\t\tconst dy = event.pageY - position.y;\n\n\t\t\tconst distance = Math.sqrt( dx * dx + dy * dy );\n\n\t\t\tdollyEnd.set( 0, distance );\n\n\t\t\tdollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );\n\n\t\t\tdollyOut( dollyDelta.y );\n\n\t\t\tdollyStart.copy( dollyEnd );\n\n\t\t}\n\n\t\tfunction handleTouchMoveDollyPan( event ) {\n\n\t\t\tif ( scope.enableZoom ) handleTouchMoveDolly( event );\n\n\t\t\tif ( scope.enablePan ) handleTouchMovePan( event );\n\n\t\t}\n\n\t\tfunction handleTouchMoveDollyRotate( event ) {\n\n\t\t\tif ( scope.enableZoom ) handleTouchMoveDolly( event );\n\n\t\t\tif ( scope.enableRotate ) handleTouchMoveRotate( event );\n\n\t\t}\n\n\t\t//\n\t\t// event handlers - FSM: listen for events and reset state\n\t\t//\n\n\t\tfunction onPointerDown( event ) {\n\n\t\t\tif ( scope.enabled === false ) return;\n\n\t\t\tif ( pointers.length === 0 ) {\n\n\t\t\t\tscope.domElement.setPointerCapture( event.pointerId );\n\n\t\t\t\tscope.domElement.addEventListener( 'pointermove', onPointerMove );\n\t\t\t\tscope.domElement.addEventListener( 'pointerup', onPointerUp );\n\n\t\t\t}\n\n\t\t\t//\n\n\t\t\taddPointer( event );\n\n\t\t\tif ( event.pointerType === 'touch' ) {\n\n\t\t\t\tonTouchStart( event );\n\n\t\t\t} else {\n\n\t\t\t\tonMouseDown( event );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction onPointerMove( event ) {\n\n\t\t\tif ( scope.enabled === false ) return;\n\n\t\t\tif ( event.pointerType === 'touch' ) {\n\n\t\t\t\tonTouchMove( event );\n\n\t\t\t} else {\n\n\t\t\t\tonMouseMove( event );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction onPointerUp( event ) {\n\n\t\t\tremovePointer( event );\n\n\t\t\tif ( pointers.length === 0 ) {\n\n\t\t\t\tscope.domElement.releasePointerCapture( event.pointerId );\n\n\t\t\t\tscope.domElement.removeEventListener( 'pointermove', onPointerMove );\n\t\t\t\tscope.domElement.removeEventListener( 'pointerup', onPointerUp );\n\n\t\t\t}\n\n\t\t\tscope.dispatchEvent( _endEvent );\n\n\t\t\tstate = STATE.NONE;\n\n\t\t}\n\n\t\tfunction onMouseDown( event ) {\n\n\t\t\tlet mouseAction;\n\n\t\t\tswitch ( event.button ) {\n\n\t\t\t\tcase 0:\n\n\t\t\t\t\tmouseAction = scope.mouseButtons.LEFT;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 1:\n\n\t\t\t\t\tmouseAction = scope.mouseButtons.MIDDLE;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 2:\n\n\t\t\t\t\tmouseAction = scope.mouseButtons.RIGHT;\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\n\t\t\t\t\tmouseAction = - 1;\n\n\t\t\t}\n\n\t\t\tswitch ( mouseAction ) {\n\n\t\t\t\tcase MOUSE.DOLLY:\n\n\t\t\t\t\tif ( scope.enableZoom === false ) return;\n\n\t\t\t\t\thandleMouseDownDolly( event );\n\n\t\t\t\t\tstate = STATE.DOLLY;\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase MOUSE.ROTATE:\n\n\t\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\t\tif ( scope.enablePan === false ) return;\n\n\t\t\t\t\t\thandleMouseDownPan( event );\n\n\t\t\t\t\t\tstate = STATE.PAN;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tif ( scope.enableRotate === false ) return;\n\n\t\t\t\t\t\thandleMouseDownRotate( event );\n\n\t\t\t\t\t\tstate = STATE.ROTATE;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase MOUSE.PAN:\n\n\t\t\t\t\tif ( event.ctrlKey || event.metaKey || event.shiftKey ) {\n\n\t\t\t\t\t\tif ( scope.enableRotate === false ) return;\n\n\t\t\t\t\t\thandleMouseDownRotate( event );\n\n\t\t\t\t\t\tstate = STATE.ROTATE;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tif ( scope.enablePan === false ) return;\n\n\t\t\t\t\t\thandleMouseDownPan( event );\n\n\t\t\t\t\t\tstate = STATE.PAN;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\n\t\t\t\t\tstate = STATE.NONE;\n\n\t\t\t}\n\n\t\t\tif ( state !== STATE.NONE ) {\n\n\t\t\t\tscope.dispatchEvent( _startEvent );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction onMouseMove( event ) {\n\n\t\t\tswitch ( state ) {\n\n\t\t\t\tcase STATE.ROTATE:\n\n\t\t\t\t\tif ( scope.enableRotate === false ) return;\n\n\t\t\t\t\thandleMouseMoveRotate( event );\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase STATE.DOLLY:\n\n\t\t\t\t\tif ( scope.enableZoom === false ) return;\n\n\t\t\t\t\thandleMouseMoveDolly( event );\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase STATE.PAN:\n\n\t\t\t\t\tif ( scope.enablePan === false ) return;\n\n\t\t\t\t\thandleMouseMovePan( event );\n\n\t\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction onMouseWheel( event ) {\n\n\t\t\tif ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;\n\n\t\t\tevent.preventDefault();\n\n\t\t\tscope.dispatchEvent( _startEvent );\n\n\t\t\thandleMouseWheel( event );\n\n\t\t\tscope.dispatchEvent( _endEvent );\n\n\t\t}\n\n\t\tfunction onKeyDown( event ) {\n\n\t\t\tif ( scope.enabled === false || scope.enablePan === false ) return;\n\n\t\t\thandleKeyDown( event );\n\n\t\t}\n\n\t\tfunction onTouchStart( event ) {\n\n\t\t\ttrackPointer( event );\n\n\t\t\tswitch ( pointers.length ) {\n\n\t\t\t\tcase 1:\n\n\t\t\t\t\tswitch ( scope.touches.ONE ) {\n\n\t\t\t\t\t\tcase TOUCH.ROTATE:\n\n\t\t\t\t\t\t\tif ( scope.enableRotate === false ) return;\n\n\t\t\t\t\t\t\thandleTouchStartRotate();\n\n\t\t\t\t\t\t\tstate = STATE.TOUCH_ROTATE;\n\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase TOUCH.PAN:\n\n\t\t\t\t\t\t\tif ( scope.enablePan === false ) return;\n\n\t\t\t\t\t\t\thandleTouchStartPan();\n\n\t\t\t\t\t\t\tstate = STATE.TOUCH_PAN;\n\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tdefault:\n\n\t\t\t\t\t\t\tstate = STATE.NONE;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 2:\n\n\t\t\t\t\tswitch ( scope.touches.TWO ) {\n\n\t\t\t\t\t\tcase TOUCH.DOLLY_PAN:\n\n\t\t\t\t\t\t\tif ( scope.enableZoom === false && scope.enablePan === false ) return;\n\n\t\t\t\t\t\t\thandleTouchStartDollyPan();\n\n\t\t\t\t\t\t\tstate = STATE.TOUCH_DOLLY_PAN;\n\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase TOUCH.DOLLY_ROTATE:\n\n\t\t\t\t\t\t\tif ( scope.enableZoom === false && scope.enableRotate === false ) return;\n\n\t\t\t\t\t\t\thandleTouchStartDollyRotate();\n\n\t\t\t\t\t\t\tstate = STATE.TOUCH_DOLLY_ROTATE;\n\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tdefault:\n\n\t\t\t\t\t\t\tstate = STATE.NONE;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\n\t\t\t\t\tstate = STATE.NONE;\n\n\t\t\t}\n\n\t\t\tif ( state !== STATE.NONE ) {\n\n\t\t\t\tscope.dispatchEvent( _startEvent );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction onTouchMove( event ) {\n\n\t\t\ttrackPointer( event );\n\n\t\t\tswitch ( state ) {\n\n\t\t\t\tcase STATE.TOUCH_ROTATE:\n\n\t\t\t\t\tif ( scope.enableRotate === false ) return;\n\n\t\t\t\t\thandleTouchMoveRotate( event );\n\n\t\t\t\t\tscope.update();\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase STATE.TOUCH_PAN:\n\n\t\t\t\t\tif ( scope.enablePan === false ) return;\n\n\t\t\t\t\thandleTouchMovePan( event );\n\n\t\t\t\t\tscope.update();\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase STATE.TOUCH_DOLLY_PAN:\n\n\t\t\t\t\tif ( scope.enableZoom === false && scope.enablePan === false ) return;\n\n\t\t\t\t\thandleTouchMoveDollyPan( event );\n\n\t\t\t\t\tscope.update();\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase STATE.TOUCH_DOLLY_ROTATE:\n\n\t\t\t\t\tif ( scope.enableZoom === false && scope.enableRotate === false ) return;\n\n\t\t\t\t\thandleTouchMoveDollyRotate( event );\n\n\t\t\t\t\tscope.update();\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\n\t\t\t\t\tstate = STATE.NONE;\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction onContextMenu( event ) {\n\n\t\t\tif ( scope.enabled === false ) return;\n\n\t\t\tevent.preventDefault();\n\n\t\t}\n\n\t\tfunction addPointer( event ) {\n\n\t\t\tpointers.push( event );\n\n\t\t}\n\n\t\tfunction removePointer( event ) {\n\n\t\t\tdelete pointerPositions[ event.pointerId ];\n\n\t\t\tfor ( let i = 0; i < pointers.length; i ++ ) {\n\n\t\t\t\tif ( pointers[ i ].pointerId == event.pointerId ) {\n\n\t\t\t\t\tpointers.splice( i, 1 );\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction trackPointer( event ) {\n\n\t\t\tlet position = pointerPositions[ event.pointerId ];\n\n\t\t\tif ( position === undefined ) {\n\n\t\t\t\tposition = new Vector2();\n\t\t\t\tpointerPositions[ event.pointerId ] = position;\n\n\t\t\t}\n\n\t\t\tposition.set( event.pageX, event.pageY );\n\n\t\t}\n\n\t\tfunction getSecondPointerPosition( event ) {\n\n\t\t\tconst pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ];\n\n\t\t\treturn pointerPositions[ pointer.pointerId ];\n\n\t\t}\n\n\t\t//\n\n\t\tscope.domElement.addEventListener( 'contextmenu', onContextMenu );\n\n\t\tscope.domElement.addEventListener( 'pointerdown', onPointerDown );\n\t\tscope.domElement.addEventListener( 'pointercancel', onPointerUp );\n\t\tscope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );\n\n\t\t// force an update at start\n\n\t\tthis.update();\n\n\t}\n\n}\n\nexport { OrbitControls };\n"
  },
  {
    "path": "js/TrailRenderer.js",
    "content": "/**\n* @author Mark Kellogg - http://www.github.com/mkkellogg\n*/\nimport * as THREE from 'three';\n\n//=======================================\n// Trail Renderer\n//=======================================\n\nexport class TrailRenderer extends THREE.Object3D {\n\n    constructor (scene, orientToMovement) {\n        super();\n        this.active = false;\n        this.orientToMovement = false;\n        if (orientToMovement) this.orientToMovement = true;\n        this.scene = scene;\n        this.geometry = null;\n        this.mesh = null;\n        this.nodeCenters = null;\n        this.lastNodeCenter = null;\n        this.currentNodeCenter = null;\n        this.lastOrientationDir = null;\n        this.nodeIDs = null;\n        this.currentLength = 0;\n        this.currentEnd = 0;\n        this.currentNodeID = 0;\n        this.advanceFrequency = 60;\n        this.advancePeriod = 1 / this.advanceFrequency;\n        this.lastAdvanceTime = 0;\n        this.paused = false;\n        this.pauseAdvanceUpdateTimeDiff = 0;\n    }\n\n    setAdvanceFrequency(advanceFrequency) {\n        this.advanceFrequency = advanceFrequency;\n        this.advancePeriod = 1.0 / this.advanceFrequency;\n    }\n\n    initialize (material, length, dragTexture, localHeadWidth, localHeadGeometry, targetObject) {\n        this.deactivate();\n        this.destroyMesh();\n\n        this.length = (length > 0) ? length + 1 : 0;\n        this.dragTexture = (! dragTexture) ? 0 : 1;\n        this.targetObject = targetObject;\n\n        this.initializeLocalHeadGeometry(localHeadWidth, localHeadGeometry);\n\n        this.nodeIDs = [];\n        this.nodeCenters = [];\n\n        for(let i = 0; i < this.length; i ++) {\n            this.nodeIDs[ i ] = -1;\n            this.nodeCenters[ i ] = new THREE.Vector3();\n        }\n\n        this.material = material;\n\n        this.initializeGeometry();\n        this.initializeMesh();\n\n        this.material.uniforms.trailLength.value = 0;\n        this.material.uniforms.minID.value = 0;\n        this.material.uniforms.maxID.value = 0;\n        this.material.uniforms.dragTexture.value = this.dragTexture;\n        this.material.uniforms.maxTrailLength.value = this.length;\n        this.material.uniforms.verticesPerNode.value = this.VerticesPerNode;\n        this.material.uniforms.textureTileFactor.value = new THREE.Vector2(1.0, 1.0);\n\n        this.reset();\n    }\n\n    initializeLocalHeadGeometry (localHeadWidth, localHeadGeometry) {\n        this.localHeadGeometry = [];\n        if (!localHeadGeometry) {\n            const halfWidth = (localHeadWidth || 1.0) / 2.0;\n            this.localHeadGeometry.push(new THREE.Vector3(-halfWidth, 0, 0));\n            this.localHeadGeometry.push(new THREE.Vector3(halfWidth, 0, 0));\n            this.VerticesPerNode = 2;\n        } else {\n            this.VerticesPerNode = 0;\n            for (let i = 0; i < localHeadGeometry.length && i < TrailRenderer.MaxHeadVertices; i++) {\n                const vertex = localHeadGeometry[ i ];\n                if (vertex && vertex instanceof THREE.Vector3) {\n                    const vertexCopy = new THREE.Vector3();\n                    vertexCopy.copy(vertex);\n                    this.localHeadGeometry.push(vertexCopy);\n                    this.VerticesPerNode ++;\n                }\n            }\n        }\n        this.FacesPerNode = (this.VerticesPerNode - 1) * 2;\n        this.FaceIndicesPerNode = this.FacesPerNode * 3;\n    }\n\n    initializeGeometry () {\n        this.vertexCount = this.length * this.VerticesPerNode;\n        this.faceCount = this.length * this.FacesPerNode;\n\n        const geometry = new THREE.BufferGeometry();\n\n        const nodeIDs = new Float32Array(this.vertexCount);\n        const nodeVertexIDs = new Float32Array(this.vertexCount * this.VerticesPerNode);\n        const positions = new Float32Array(this.vertexCount * TrailRenderer.PositionComponentCount);\n        const nodeCenters = new Float32Array(this.vertexCount * TrailRenderer.PositionComponentCount);\n        const uvs = new Float32Array(this.vertexCount * TrailRenderer.UVComponentCount);\n        const indices = new Uint32Array(this.faceCount * TrailRenderer.IndicesPerFace);\n\n        const nodeIDAttribute = new THREE.BufferAttribute(nodeIDs, 1);\n        nodeIDAttribute.dynamic = true;\n        geometry.setAttribute('nodeID', nodeIDAttribute);\n\n        const nodeVertexIDAttribute = new THREE.BufferAttribute(nodeVertexIDs, 1);\n        nodeVertexIDAttribute.dynamic = true;\n        geometry.setAttribute('nodeVertexID', nodeVertexIDAttribute);\n\n        const nodeCenterAttribute = new THREE.BufferAttribute(nodeCenters, TrailRenderer.PositionComponentCount);\n        nodeCenterAttribute.dynamic = true;\n        geometry.setAttribute('nodeCenter', nodeCenterAttribute);\n\n        const positionAttribute = new THREE.BufferAttribute(positions, TrailRenderer.PositionComponentCount);\n        positionAttribute.dynamic = true;\n        geometry.setAttribute('position', positionAttribute);\n\n        const uvAttribute = new THREE.BufferAttribute(uvs, TrailRenderer.UVComponentCount);\n        uvAttribute.dynamic = true;\n        geometry.setAttribute('uv', uvAttribute);\n\n        const indexAttribute = new THREE.BufferAttribute(indices, 1);\n        indexAttribute.dynamic = true;\n        geometry.setIndex(indexAttribute);\n\n        this.geometry = geometry;\n    }\n\n    zeroVertices () {\n        const positions = this.geometry.getAttribute('position');\n        for (let i = 0; i < this.vertexCount; i ++) {\n            const index = i * 3;\n            positions.array[ index ] = 0;\n            positions.array[ index + 1 ] = 0;\n            positions.array[ index + 2 ] = 0;\n        }\n        positions.needsUpdate = true;\n        positions.updateRange.count = - 1;\n    }\n\n    zeroIndices () {\n        const indices = this.geometry.getIndex();\n        for (let i = 0; i < this.faceCount; i ++) {\n            const index = i * 3;\n            indices.array[ index ] = 0;\n            indices.array[ index + 1 ] = 0;\n            indices.array[ index + 2 ] = 0;\n        }\n        indices.needsUpdate = true;\n        indices.updateRange.count = - 1;\n    }\n\n    formInitialFaces () {\n        this.zeroIndices();\n        const indices = this.geometry.getIndex();\n        for (let i = 0; i < this.length - 1; i ++) {\n            this.connectNodes(i, i + 1);\n        }\n        indices.needsUpdate = true;\n        indices.updateRange.count = - 1;\n    }\n\n    initializeMesh () {\n        this.mesh = new THREE.Mesh(this.geometry, this.material);\n        this.mesh.dynamic = true;\n        this.mesh.matrixAutoUpdate = false;\n    }\n\n    destroyMesh () {\n        if (this.mesh) {\n            this.scene.remove(this.mesh);\n            this.mesh = null;\n        }\n    }\n\n    reset () {\n        this.currentLength = 0;\n        this.currentEnd = -1;\n        this.lastNodeCenter = null;\n        this.currentNodeCenter = null;\n        this.lastOrientationDir = null;\n        this.currentNodeID = 0;\n        this.formInitialFaces();\n        this.zeroVertices();\n        this.geometry.setDrawRange(0, 0);\n    }\n\n    updateUniforms () {\n        if (this.currentLength < this.length) {\n            this.material.uniforms.minID.value = 0;\n        } else {\n            this.material.uniforms.minID.value = this.currentNodeID - this.length;\n        }\n        this.material.uniforms.maxID.value = this.currentNodeID;\n        this.material.uniforms.trailLength.value = this.currentLength;\n        this.material.uniforms.maxTrailLength.value = this.length;\n        this.material.uniforms.verticesPerNode.value = this.VerticesPerNode;\n    }\n\n    advance = function() {\n        const tempMatrix4 = new THREE.Matrix4();\n        return function advance() {\n            this.targetObject.updateMatrixWorld();\n            tempMatrix4.copy(this.targetObject.matrixWorld);\n            this.advanceWithTransform(tempMatrix4);\n            this.updateUniforms();\n        };\n    }();\n\n    advanceWithPositionAndOrientation (nextPosition, orientationTangent) {\n        this.advanceGeometry({ position : nextPosition, tangent : orientationTangent }, null);\n    }\n\n    advanceWithTransform (transformMatrix) {\n        this.advanceGeometry(null, transformMatrix);\n    }\n\n    advanceGeometry = function() { \n\n        return function advanceGeometry(positionAndOrientation, transformMatrix) {\n            const nextIndex = this.currentEnd + 1 >= this.length ? 0 : this.currentEnd + 1; \n            if(transformMatrix) {\n                this.updateNodePositionsFromTransformMatrix(nextIndex, transformMatrix);\n            } else {\n                this.updateNodePositionsFromOrientationTangent(nextIndex, positionAndOrientation.position, positionAndOrientation.tangent);\n            }\n\n            if (this.currentLength >= 1) {\n                this.connectNodes(this.currentEnd , nextIndex);\n                if(this.currentLength >= this.length) {\n                    const disconnectIndex  = this.currentEnd + 1  >= this.length ? 0 : this.currentEnd + 1;\n                    this.disconnectNodes(disconnectIndex);\n                }\n            }\n\n            if(this.currentLength < this.length) {\n                this.currentLength ++;\n            }\n\n            this.currentEnd++;\n            if (this.currentEnd >= this.length) {\n                this.currentEnd = 0;\n            }\n\n            if (this.currentLength >= 1) {\n                if(this.currentLength < this.length) {\n                    this.geometry.setDrawRange(0, (this.currentLength - 1) * this.FaceIndicesPerNode);\n                } else {\n                    this.geometry.setDrawRange(0, this.currentLength * this.FaceIndicesPerNode);\n                }\n            }\n            this.updateNodeID(this.currentEnd,  this.currentNodeID);\n            this.currentNodeID ++;\n        };\n\n    }();\n\n    currentTime() {\n        return performance.now() / 1000;\n    }\n\n    pause() {\n        if(!this.paused) {\n            this.paused = true;\n            this.pauseAdvanceUpdateTimeDiff = this.currentTime() - this.lastAdvanceTime;\n        }\n    }\n\n    resume() {\n        if(this.paused) {\n            this.paused = false;\n            this.lastAdvanceTime = this.currentTime() - this.pauseAdvanceUpdateTimeDiff;\n        }\n    }\n\n    update() {\n        if (!this.paused) {\n            const time = this.currentTime();\n            if (!this.lastAdvanceTime) this.lastAdvanceTime = time;\n            if (time - this.lastAdvanceTime > this.advancePeriod) {\n                this.advance();\n                this.lastAdvanceTime = time;\n            } else {\n                this.updateHead();\n            }\n        }\n    }\n\n    updateHead = function() {\n\n        const tempMatrix4 = new THREE.Matrix4();\n\n        return function updateHead() {\n            if(this.currentEnd < 0) return;\n            this.targetObject.updateMatrixWorld();\n            tempMatrix4.copy(this.targetObject.matrixWorld);\n            this.updateNodePositionsFromTransformMatrix(this.currentEnd, tempMatrix4);\n        };\n\n    }();\n\n    updateNodeID (nodeIndex, id) { \n        this.nodeIDs[ nodeIndex ] = id;\n        const nodeIDs = this.geometry.getAttribute('nodeID');\n        const nodeVertexIDs = this.geometry.getAttribute('nodeVertexID');\n        for (let i = 0; i < this.VerticesPerNode; i ++) {\n            const baseIndex = nodeIndex * this.VerticesPerNode + i ;\n            nodeIDs.array[ baseIndex ] = id;\n            nodeVertexIDs.array[ baseIndex ] = i;\n        }    \n        nodeIDs.needsUpdate = true;\n        nodeVertexIDs.needsUpdate = true;\n        nodeIDs.updateRange.offset = nodeIndex * this.VerticesPerNode; \n        nodeIDs.updateRange.count = this.VerticesPerNode;\n        nodeVertexIDs.updateRange.offset = nodeIndex * this.VerticesPerNode;\n        nodeVertexIDs.updateRange.count = this.VerticesPerNode;\n    }\n\n    updateNodeCenter (nodeIndex, nodeCenter) { \n        this.lastNodeCenter = this.currentNodeCenter;\n        this.currentNodeCenter = this.nodeCenters[ nodeIndex ];\n        this.currentNodeCenter.copy(nodeCenter);\n        const nodeCenters = this.geometry.getAttribute('nodeCenter');\n        for (let i = 0; i < this.VerticesPerNode; i ++) {\n            const baseIndex = (nodeIndex * this.VerticesPerNode + i) * 3;\n            nodeCenters.array[ baseIndex ] = nodeCenter.x;\n            nodeCenters.array[ baseIndex + 1 ] = nodeCenter.y;\n            nodeCenters.array[ baseIndex + 2 ] = nodeCenter.z;\n        }    \n        nodeCenters.needsUpdate = true;\n        nodeCenters.updateRange.offset = nodeIndex * this.VerticesPerNode * TrailRenderer.PositionComponentCount; \n        nodeCenters.updateRange.count = this.VerticesPerNode * TrailRenderer.PositionComponentCount; \n    }\n\n    updateNodePositionsFromOrientationTangent = function() { \n\n        const tempQuaternion = new THREE.Quaternion();\n        const tempOffset = new THREE.Vector3();\n        const tempLocalHeadGeometry = [];\n        for (let i = 0; i < TrailRenderer.MaxHeadVertices; i ++) {\n            const vertex = new THREE.Vector3();\n            tempLocalHeadGeometry.push(vertex);\n        }\n\n        return function updateNodePositionsFromOrientationTangent(nodeIndex, nodeCenter, orientationTangent ) {\n            const positions = this.geometry.getAttribute('position');\n            this.updateNodeCenter(nodeIndex, nodeCenter);\n            tempOffset.copy(nodeCenter);\n            tempOffset.sub(TrailRenderer.LocalHeadOrigin);\n            tempQuaternion.setFromUnitVectors(TrailRenderer.LocalOrientationTangent, orientationTangent);\n\n            for (let i = 0; i < this.localHeadGeometry.length; i ++) {\n                const vertex = tempLocalHeadGeometry[ i ];\n                vertex.copy(this.localHeadGeometry[ i ]);\n                vertex.applyQuaternion(tempQuaternion);\n                vertex.add(tempOffset);\n            }\n\n            for (let i = 0; i <  this.localHeadGeometry.length; i ++) {\n                const positionIndex = ((this.VerticesPerNode * nodeIndex) + i) * TrailRenderer.PositionComponentCount;\n                const transformedHeadVertex = tempLocalHeadGeometry[ i ];\n                positions.array[ positionIndex ] = transformedHeadVertex.x;\n                positions.array[ positionIndex + 1 ] = transformedHeadVertex.y;\n                positions.array[ positionIndex + 2 ] = transformedHeadVertex.z;\n            }\n\n            positions.needsUpdate = true;\n        };\n\n    }();\n\n    updateNodePositionsFromTransformMatrix = function() { \n\n        const tempMatrix3 = new THREE.Matrix3();\n        const tempQuaternion = new THREE.Quaternion();\n        const tempPosition = new THREE.Vector3();\n        const tempOffset = new THREE.Vector3();\n        const worldOrientation = new THREE.Vector3();\n        const tempDirection = new THREE.Vector3();\n\n        const tempLocalHeadGeometry = [];\n        for (let i = 0; i < TrailRenderer.MaxHeadVertices; i ++) {\n            const vertex = new THREE.Vector3();\n            tempLocalHeadGeometry.push(vertex);\n        }\n\n        function getMatrix3FromMatrix4(matrix3, matrix4) {\n            const e = matrix4.elements;\n            matrix3.set(e[0], e[1], e[2],\n                        e[4], e[5], e[6],\n                        e[8], e[9], e[10]);\n\n        }\n\n        return function updateNodePositionsFromTransformMatrix(nodeIndex, transformMatrix) {\n            const positions = this.geometry.getAttribute('position');\n            tempPosition.set(0, 0, 0);\n            tempPosition.applyMatrix4(transformMatrix);\n            this.updateNodeCenter(nodeIndex, tempPosition);\n            for (let i = 0; i < this.localHeadGeometry.length; i ++) {\n                const vertex = tempLocalHeadGeometry[ i ];\n                vertex.copy(this.localHeadGeometry[ i ]);\n            }\n\n            for (let i = 0; i < this.localHeadGeometry.length; i ++) {\n                const vertex = tempLocalHeadGeometry[ i ];\n                vertex.applyMatrix4(transformMatrix);\n            }\n            \n            if(this.lastNodeCenter && this.orientToMovement) {\n                getMatrix3FromMatrix4(tempMatrix3, transformMatrix);\n                worldOrientation.set(0, 0, -1);\n                worldOrientation.applyMatrix3(tempMatrix3);\n                tempDirection.copy(this.currentNodeCenter);\n                tempDirection.sub(this.lastNodeCenter);\n                tempDirection.normalize();\n\n                if(tempDirection.lengthSq() <= .0001 && this.lastOrientationDir) {\n                    tempDirection.copy(this.lastOrientationDir);\n                }\n\n                if(tempDirection.lengthSq() > .0001) {\n                    if(! this.lastOrientationDir) this.lastOrientationDir = new THREE.Vector3();\n                    tempQuaternion.setFromUnitVectors(worldOrientation, tempDirection);\n                    tempOffset.copy(this.currentNodeCenter);\n                    for (let i = 0; i < this.localHeadGeometry.length; i ++) {\n                        const vertex = tempLocalHeadGeometry[ i ];\n                        vertex.sub(tempOffset);\n                        vertex.applyQuaternion(tempQuaternion);\n                        vertex.add(tempOffset);\n                    }\n                }\n            }\n\n            for (let i = 0; i < this.localHeadGeometry.length; i ++) {\n                const positionIndex = ((this.VerticesPerNode * nodeIndex) + i) * TrailRenderer.PositionComponentCount;\n                const transformedHeadVertex = tempLocalHeadGeometry[ i ];\n                positions.array[ positionIndex ] = transformedHeadVertex.x;\n                positions.array[ positionIndex + 1 ] = transformedHeadVertex.y;\n                positions.array[ positionIndex + 2 ] = transformedHeadVertex.z;\n            }\n            positions.needsUpdate = true;\n            positions.updateRange.offset = nodeIndex * this.VerticesPerNode * TrailRenderer.PositionComponentCount; \n            positions.updateRange.count = this.VerticesPerNode * TrailRenderer.PositionComponentCount; \n        };\n\n    }();\n\n    connectNodes = function() {\n\n        const returnObj = {\n            \"attribute\" : null,\n            \"offset\" : 0,\n            \"count\" : - 1\n        };\n\n        return function connectNodes(srcNodeIndex, destNodeIndex) {\n            const indices = this.geometry.getIndex();\n            for (let i = 0; i < this.localHeadGeometry.length - 1; i ++) {\n                const srcVertexIndex = (this.VerticesPerNode * srcNodeIndex) + i;\n                const destVertexIndex = (this.VerticesPerNode * destNodeIndex) + i;\n                const faceIndex = ((srcNodeIndex * this.FacesPerNode) + (i * TrailRenderer.FacesPerQuad )) * TrailRenderer.IndicesPerFace;\n                indices.array[ faceIndex ] = srcVertexIndex;\n                indices.array[ faceIndex + 1 ] = destVertexIndex;\n                indices.array[ faceIndex + 2 ] = srcVertexIndex + 1;\n                indices.array[ faceIndex + 3 ] = destVertexIndex;\n                indices.array[ faceIndex + 4 ] = destVertexIndex + 1;\n                indices.array[ faceIndex + 5 ] = srcVertexIndex + 1;\n            }\n            indices.needsUpdate = true;\n            indices.updateRange.count = - 1;\n            returnObj.attribute = indices;\n            returnObj.offset =  srcNodeIndex * this.FacesPerNode * TrailRenderer.IndicesPerFace;\n            returnObj.count = this.FacesPerNode * TrailRenderer.IndicesPerFace;\n            return returnObj;\n\n        };\n\n    }();\n\n    disconnectNodes = function() {\n\n        const returnObj = {\n            \"attribute\" : null,\n            \"offset\" : 0,\n            \"count\" : - 1\n        };\n\n        return function disconnectNodes(srcNodeIndex) {\n            const indices = this.geometry.getIndex();\n            for (let i = 0; i < this.localHeadGeometry.length - 1; i ++) {\n                const faceIndex = ((srcNodeIndex * this.FacesPerNode) + (i * TrailRenderer.FacesPerQuad)) * TrailRenderer.IndicesPerFace;\n                indices.array[ faceIndex ] = 0;\n                indices.array[ faceIndex + 1 ] = 0;\n                indices.array[ faceIndex + 2 ] = 0;\n                indices.array[ faceIndex + 3 ] = 0;\n                indices.array[ faceIndex + 4 ] = 0;\n                indices.array[ faceIndex + 5 ] = 0;\n            }\n            indices.needsUpdate = true;\n            indices.updateRange.count = - 1;\n            returnObj.attribute = indices;\n            returnObj.offset = srcNodeIndex * this.FacesPerNode * TrailRenderer.IndicesPerFace;\n            returnObj.count = this.FacesPerNode * TrailRenderer.IndicesPerFace;\n            return returnObj;\n        };\n\n    }();\n\n    deactivate () {\n        if (this.isActive) {\n            this.scene.remove(this.mesh);\n            this.isActive = false;\n        }\n    }\n\n    activate () {\n        if (! this.isActive) {\n            this.scene.add(this.mesh);\n            this.isActive = true;\n        }\n    }\n\n    static createMaterial(vertexShader, fragmentShader, customUniforms) {\n\n        customUniforms = customUniforms || {};\n        customUniforms.trailLength = { type: \"f\", value: null };\n        customUniforms.verticesPerNode = { type: \"f\", value: null };\n        customUniforms.minID = { type: \"f\", value: null };\n        customUniforms.maxID = { type: \"f\", value: null };\n        customUniforms.dragTexture = { type: \"f\", value: null };\n        customUniforms.maxTrailLength = { type: \"f\", value: null };\n        customUniforms.textureTileFactor = { type: \"v2\", value: null };\n        customUniforms.headColor = { type: \"v4\", value: new THREE.Vector4() };\n        customUniforms.tailColor = { type: \"v4\", value: new THREE.Vector4() };\n\n        vertexShader = vertexShader || TrailRenderer.Shader.BaseVertexShader;\n        fragmentShader = fragmentShader || TrailRenderer.Shader.BaseFragmentShader;\n\n        return new THREE.ShaderMaterial({\n            uniforms: customUniforms,\n            vertexShader: vertexShader,\n            fragmentShader: fragmentShader,\n            transparent: true,\n            alphaTest: 0.5,\n            blending : THREE.CustomBlending,\n            blendSrc : THREE.SrcAlphaFactor,\n            blendDst : THREE.OneMinusSrcAlphaFactor,\n            blendEquation : THREE.AddEquation,\n            depthTest: true,\n            depthWrite: false,\n            side: THREE.DoubleSide\n        });\n    \n    }\n    \n    static createBaseMaterial(customUniforms) {\n        return TrailRenderer.createMaterial(TrailRenderer.Shader.BaseVertexShader, TrailRenderer.Shader.BaseFragmentShader, customUniforms);\n    }\n\n    static createTexturedMaterial (customUniforms) {\n        customUniforms = {};\n        customUniforms.trailTexture = { type: \"t\", value: null };\n        return TrailRenderer.createMaterial(TrailRenderer.Shader.TexturedVertexShader, TrailRenderer.Shader.TexturedFragmentShader, customUniforms);\n    }\n\n    static get MaxHeadVertices () {\n        return 128;\n    }\n\n    static _LocalOrientationTangent = new THREE.Vector3(1, 0, 0);\n    static get LocalOrientationTangent () {\n        return _LocalOrientationTangent;\n    }\n\n    static _LocalHeadOrigin = new THREE.Vector3(0, 0, 0);\n    static get LocalHeadOrigin () {\n        return _LocalHeadOrigin;\n    }\n\n    static get PositionComponentCount () {\n        return 3;\n    }\n\n    static get UVComponentCount () {\n        return 2;\n    }\n\n    static get IndicesPerFace () {\n        return 3;\n    }\n\n    static get FacesPerQuad () {\n        return 2;\n    }\n\n    static Shader = {\n\n        get BaseVertexVars() {\n            return [\n                \"attribute float nodeID;\",\n                \"attribute float nodeVertexID;\",\n                \"attribute vec3 nodeCenter;\",\n                \"uniform float minID;\",\n                \"uniform float maxID;\",\n                \"uniform float trailLength;\",\n                \"uniform float maxTrailLength;\",\n                \"uniform float verticesPerNode;\",\n                \"uniform vec2 textureTileFactor;\",\n                \"uniform vec4 headColor;\",\n                \"uniform vec4 tailColor;\",\n                \"varying vec4 vColor;\",\n            ].join(\"\\n\")\n        },\n\n        get TexturedVertexVars() {\n            return [\n                this.BaseVertexVars, \n                \"varying vec2 vUV;\",\n                \"uniform float dragTexture;\",\n            ].join(\"\\n\");\n        },\n\n        BaseFragmentVars: [\n            \"varying vec4 vColor;\",\n            \"uniform sampler2D trailTexture;\",\n        ].join(\"\\n\"),\n\n        get TexturedFragmentVars() {\n            return [\n                this.BaseFragmentVars,\n                \"varying vec2 vUV;\"\n            ].join(\"\\n\");\n        },\n\n        get VertexShaderCore() {\n            return [\n                \"float fraction = (maxID - nodeID) / (maxID - minID);\",\n                \"vColor = (1.0 - fraction) * headColor + fraction * tailColor;\",\n                \"vec4 realPosition = vec4((1.0 - fraction) * position.xyz + fraction * nodeCenter.xyz, 1.0); \", \n            ].join(\"\\n\");\n        },\n\n        get BaseVertexShader() {\n            return [\n                this.BaseVertexVars,\n                \"void main() { \",\n                    this.VertexShaderCore,\n                    \"gl_Position = projectionMatrix * viewMatrix * realPosition;\",\n                \"}\"\n            ].join(\"\\n\");\n        },\n\n        get BaseFragmentShader() {\n            return [\n                this.BaseFragmentVars,\n                \"void main() { \",\n                    \"gl_FragColor = vColor;\",\n                \"}\"\n            ].join(\"\\n\");\n        },\n\n        get TexturedVertexShader() {\n            return [\n                this.TexturedVertexVars,\n                \"void main() { \",\n                    this.VertexShaderCore,\n                    \"float s = 0.0;\",\n                    \"float t = 0.0;\",\n                    \"if (dragTexture == 1.0) { \",\n                    \"   s = fraction *  textureTileFactor.s; \",\n                    \"     t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;\",\n                    \"} else { \",\n                    \"    s = nodeID / maxTrailLength * textureTileFactor.s;\",\n                    \"     t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;\",\n                    \"}\",\n                    \"vUV = vec2(s, t); \",\n                    \"gl_Position = projectionMatrix * viewMatrix * realPosition;\",\n                \"}\"\n            ].join(\"\\n\");\n        },\n\n        get TexturedFragmentShader() {\n            return [\n                this.TexturedFragmentVars,\n                \"void main() { \",\n                    \"vec4 textureColor = texture2D(trailTexture, vUV);\",\n                    \"gl_FragColor = vColor * textureColor;\",\n                \"}\"\n            ].join(\"\\n\");\n        }\n    };\n}\n"
  }
]