Full Code of mkkellogg/TrailRendererJS for AI

master 671ab19ff706 cached
6 files
70.1 KB
17.3k tokens
75 symbols
1 requests
Download .txt
Repository: mkkellogg/TrailRendererJS
Branch: master
Commit: 671ab19ff706
Files: 6
Total size: 70.1 KB

Directory structure:
gitextract_vtl97efo/

├── .gitignore
├── README.md
├── index.html
└── js/
    ├── Main.js
    ├── OrbitControls.js
    └── TrailRenderer.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules/


================================================
FILE: README.md
================================================
# Three.js Trail Renderer

Basic 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. 

The 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. 

The 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. 

Demo of the effect can be seen [here](http://projects.markkellogg.org/threejs/demo_trail_renderer.php)

The following code shows how to attach a trail renderer in a scene named 'scene' to an existing Object3D instance named 'trailTarget'.

```javascript
// specify points to create planar trail-head geometry
const trailHeadGeometry = [];
trailHeadGeometry.push( 
  new THREE.Vector3( -10.0, 0.0, 0.0 ), 
  new THREE.Vector3( 0.0, 0.0, 0.0 ), 
  new THREE.Vector3( 10.0, 0.0, 0.0 ) 
);

// create the trail renderer object
const trail = new TrailRenderer( scene, false );

// set how often a new trail node will be added and existing nodes will be updated
trail.setAdvanceFrequency(30);

// create material for the trail renderer
const trailMaterial = TrailRenderer.createBaseMaterial();	

// specify length of trail
const trailLength = 150;

// initialize the trail
trail.initialize( trailMaterial, trailLength, false, 0, trailHeadGeometry, trailTarget );

// activate the trail
trail.activate();

function animate() {
    requestAnimationFrame( animate );
    trail.update();
    render();
 
}

```

================================================
FILE: index.html
================================================
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script type="importmap">
        {
            "imports": {
                "three": "./js/libs/three.module.min.js"
            }
        }
    </script>
    <script src="js/libs/stats.min.js"></script>  
    <script src='js/libs/dat.gui.min.js'></script>
    <style>
        body {
            font-family: Monospace;
            background-color: #000;
            color: #fff;
            margin: 0px;
            overflow: hidden;
        }
        #info {
            position: absolute;
            top: 10px;
            width: 100%;
            text-align: center;
            z-index: 50;
            display:block;
        }
        #info a { color: #f00; font-weight: bold; text-decoration: underline; cursor: pointer }
    </style>
    <title>Three.js Trail Renderer</title>
</head>
<body>

<div id="info">
    <a href="http://threejs.org" target="_blank">three.js</a> - Trail Renderer by <a href="https://github.com/mkkellogg">mkkellogg</a>
</div>

<script type="module" src="js/Main.js"></script>

<div id="renderingContainer" style="position: absolute; left:0px; top:0px"></div>

<script>

   
</script>
</body>
</html>


================================================
FILE: js/Main.js
================================================
import * as THREE from 'three';
import { OrbitControls } from './OrbitControls.js';
import { TrailRenderer } from './TrailRenderer.js';

const TrailTypes = Object.freeze({
    Basic : 1,
    Textured : 2
});

const TrailShapes = Object.freeze({
    Plane : 1,
    Star : 2,
    Circle : 3
});

let screenWidth, screenHeight;
let scene, gui, renderer, rendererContainer;
let camera, pointLight, ambientLight, controls, stats;
let options, lastFrameTime, elapsedSimTime;
let starPoints, circlePoints, planePoints;
let trailTarget;
let trailHeadGeometry, trail, lastTrailUpdateTime, lastTrailResetTime;
let trailMaterial, baseTrailMaterial, texturedTrailMaterial;

window.addEventListener("load", function load(event) {
    window.removeEventListener("load", load, false);
    init();
}, false);

function init() {
    elapsedSimTime = 0;
    lastTrailUpdateTime = performance.now();
    lastTrailResetTime = performance.now();
    getScreenDimensions();
    initTrailOptions();
    initScene();
    initGUI();
    initListeners();
    initLights();
    initSceneGeometry(function() {
        initTrailRenderers(function() {
            initRenderer();
            initControls();
            initStats();
            animate();
        });
    });
}

function initTrailOptions() {
     options = {
        headRed : 1.0,
        headGreen : 0.0,
        headBlue : 0.0,
        headAlpha : 1.0,
        tailRed : 1.0,
        tailGreen : 0.4,
        tailBlue : 1.0,
        tailAlpha : 1.0,
        trailLength : 700,
        trailType : TrailTypes.Textured,
        trailShape : TrailShapes.Star,
        textureTileFactorS : 10.0,
        textureTileFactorT : 0.8,
        dragTexture : true,
        depthWrite : true,
        pauseSim : false
    };
}

function initGUI() {
    gui = new dat.GUI();  

    gui.add(options, 'trailType', {Basic: TrailTypes.Basic, Textured: TrailTypes.Textured}).name("Trail type").onChange(function() {
        options.trailType = parseInt(options.trailType);
        updateTrailType();
    });

    gui.add(options, 'trailShape', { 
        Plane: TrailShapes.Plane, 
        Star: TrailShapes.Star, 
        Circle: TrailShapes.Circle 
    }).name("Trail shape").onChange(function() {
        options.trailShape = parseInt(options.trailShape);
        updateTrailShape();
    });

    gui.add(options, "trailLength", 0, 1000).name("Trail length").onChange(updateTrailLength);    
    gui.add(options, 'depthWrite').name("Depth write").onChange(updateTrailDepthWrite);
    gui.add(options, 'pauseSim').name("Pause simulation").onChange(pauseResumeSimulation);
    
    const headColor = gui.addFolder("Head color");
    headColor.add(options, "headRed", 0.0, 1.0, 0.025).name("Red").onChange(updateTrailColors);
    headColor.add(options, "headGreen", 0.0, 1.0, 0.025).name("Green").onChange(updateTrailColors);
    headColor.add(options, "headBlue", 0.0, 1.0, 0.025).name("Blue").onChange(updateTrailColors);
    headColor.add(options, "headAlpha", 0.0, 1.0, 0.025).name("Alpha").onChange(updateTrailColors);

    const tailColor = gui.addFolder("Tail color");
    tailColor.add(options, "tailRed", 0.0, 1.0, 0.025).name("Red").onChange(updateTrailColors);
    tailColor.add(options, "tailGreen", 0.0, 1.0, 0.025).name("Green").onChange(updateTrailColors);
    tailColor.add(options, "tailBlue", 0.0, 1.0, 0.025).name("Blue").onChange(updateTrailColors);
    tailColor.add(options, "tailAlpha", 0.0, 1.0, 0.025).name("Alpha").onChange(updateTrailColors);

    const textureOptions = gui.addFolder("Texture options");
    textureOptions.add(options, "textureTileFactorS", 0, 25).name("Texture Tile S").onChange(updateTrailTextureTileSize);
    textureOptions.add(options, "textureTileFactorT", 0, 25).name("Texture Tile T").onChange(updateTrailTextureTileSize);
    textureOptions.add(options, 'dragTexture').name("Drag Texture").onChange(updateTrailTextureDrag);

    gui.domElement.parentNode.style.zIndex = 100;
}

function initListeners() {
    window.addEventListener('resize', onWindowResize, false);
}

function initRenderer() {
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(screenWidth, screenHeight);
    renderer.setClearColor(0x000000);
    renderer.sortObjects = false;
    rendererContainer = document.getElementById('renderingContainer');
    rendererContainer.appendChild(renderer.domElement);
}

function initLights() {
    ambientLight = new THREE.AmbientLight(0x777777);
    scene.add(ambientLight);
    pointLight = new THREE.DirectionalLight(0xFFFFFF, 2);
    pointLight.position.set(5, 2, 5);
    scene.add(pointLight);
}

function initSceneGeometry(onFinished) {
    initTrailHeadGeometries();
    initTrailTarget();
    if(onFinished) {
        onFinished();
    }
}

function initTrailHeadGeometries() {
    planePoints = [];
    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));

    circlePoints = [];
    const twoPI = Math.PI * 2;
    let index = 0;
    const scale = 10.0;
    const inc = twoPI / 32.0;

    for (let i = 0; i <= twoPI + inc; i+= inc)  {
        const vector = new THREE.Vector3();
        vector.set(Math.cos(i) * scale, Math.sin(i) * scale, 0);
        circlePoints[ index ] = vector;
        index++;
    }

    starPoints = [];
    starPoints.push(new THREE.Vector3(0,  16));
    starPoints.push(new THREE.Vector3(4,  4));
    starPoints.push(new THREE.Vector3(16,  4));
    starPoints.push(new THREE.Vector3(8, -4));
    starPoints.push(new THREE.Vector3(12, -16));
    starPoints.push(new THREE.Vector3( 0, -8));
    starPoints.push(new THREE.Vector3(-12, -16));
    starPoints.push(new THREE.Vector3(-8, -4));
    starPoints.push(new THREE.Vector3(-16,  4));
    starPoints.push(new THREE.Vector3(-4,  4));
    starPoints.push(new THREE.Vector3(0,  16));
}

function initTrailTarget() {
    const starShape = new THREE.Shape(starPoints);

    const extrusionSettings = {
        amount: 2, size: 2, height: 1, curveSegments: 3,
        bevelThickness: 1, bevelSize: 2, bevelEnabled: false,
        material: 0, extrudeMaterial: 1
    };

    const starGeometry = new THREE.ExtrudeGeometry(starShape, extrusionSettings);

    const trailTargetMaterial = new THREE.MeshStandardMaterial({
        color: 0xffffff,
        roughness: 0.7,
        metalness: 0.3
    });

    trailTarget = new THREE.Mesh(starGeometry, trailTargetMaterial);
    trailTarget.position.set(0, 0, 0);
    trailTarget.scale.multiplyScalar(1);
    trailTarget.receiveShadow = false;
    trailTarget.matrixAutoUpdate = false;
    scene.add(trailTarget);
}

function initTrailRenderers(callback) {
    trail = new TrailRenderer(scene, false);
    trail.setAdvanceFrequency(30);
    baseTrailMaterial = TrailRenderer.createBaseMaterial();
    const textureLoader = new THREE.TextureLoader();
    textureLoader.load("textures/sparkle4.jpg", function(tex) {
        tex.wrapS = THREE.RepeatWrapping;
        tex.wrapT = THREE.RepeatWrapping;
        texturedTrailMaterial = TrailRenderer.createTexturedMaterial();
        texturedTrailMaterial.uniforms.trailTexture.value = tex;
        setTrailShapeFromOptions();
        setTrailTypeFromOptions();
        initializeTrail();
        if (callback) {
            callback();
        }
    });
}

function updateTrailLength() {
    initializeTrail();
}

function setTrailTypeFromOptions() {
    switch (options.trailType) {
        case TrailTypes.Basic:
            trailMaterial = baseTrailMaterial;
        break;
        case TrailTypes.Textured:
            trailMaterial = texturedTrailMaterial;
        break;
    }
}

function updateTrailType() {
    setTrailTypeFromOptions();
    initializeTrail();
}

function setTrailShapeFromOptions() {
    switch (options.trailShape) {
        case TrailShapes.Plane:
            trailHeadGeometry = planePoints;
        break;
        case TrailShapes.Star:
            trailHeadGeometry = starPoints;
        break;
        case TrailShapes.Circle:
            trailHeadGeometry = circlePoints;
        break;
    }
}

function updateTrailShape() {
    setTrailShapeFromOptions();
    initializeTrail();
}

function updateTrailTextureDrag() {
    initializeTrail();
}

function updateTrailTextureTileSize() {
    trailMaterial.uniforms.textureTileFactor.value.set(options.textureTileFactorS, options.textureTileFactorT);
}

function updateTrailColors() {
    trailMaterial.uniforms.headColor.value.set(options.headRed, options.headGreen, options.headBlue, options.headAlpha);
    trailMaterial.uniforms.tailColor.value.set(options.tailRed, options.tailGreen, options.tailBlue, options.tailAlpha);
}

function updateTrailDepthWrite() {
    trailMaterial.depthWrite = options.depthWrite;
}

const updateTrailTarget = function updateTrailTarget() {

    const tempQuaternion = new THREE.Quaternion();

    const baseForward = new THREE.Vector3(0, 0, -1);
    const tempForward = new THREE.Vector3();
    const tempUp = new THREE.Vector3();

    const tempRotationMatrix = new THREE.Matrix4();
    const tempTranslationMatrix = new THREE.Matrix4();

    const currentTargetPosition = new THREE.Vector3();
    const lastTargetPosition = new THREE.Vector3();

    const currentDirection = new THREE.Vector3();
    const lastDirection = new THREE.Vector3();

    const lastRotationMatrix = new THREE.Matrix4();

    return function updateTrailTarget(time) {

        trail.update();

        tempRotationMatrix.identity();
        tempTranslationMatrix.identity();

        const scaledTime = time * .001;
        const areaScale = 100;

        lastTargetPosition.copy(currentTargetPosition);

        currentTargetPosition.x = Math.sin(scaledTime) * areaScale;
        currentTargetPosition.y = Math.sin(scaledTime * 1.1) * areaScale;
        currentTargetPosition.z = Math.sin(scaledTime * 1.6) * areaScale;

        currentDirection.copy(currentTargetPosition);
        currentDirection.sub(lastTargetPosition);
        if (currentDirection.lengthSq() < .001) {
            currentDirection.copy(lastDirection);
        } else {
            currentDirection.normalize();
        }

        tempUp.crossVectors(currentDirection, baseForward);
        const angle = baseForward.angleTo(currentDirection);

        if(Math.abs(angle) > .01 && tempUp.lengthSq() > .001) {
            tempQuaternion.setFromUnitVectors(baseForward, currentDirection);
            tempQuaternion.normalize();
            tempRotationMatrix.makeRotationFromQuaternion(tempQuaternion);
            lastRotationMatrix.copy(tempRotationMatrix);
        }
        
        tempTranslationMatrix.makeTranslation (currentTargetPosition.x, currentTargetPosition.y, currentTargetPosition.z);
        tempTranslationMatrix.multiply(tempRotationMatrix);  

        trailTarget.matrix.identity();
        trailTarget.applyMatrix4(tempTranslationMatrix);
        trailTarget.updateMatrixWorld();

        lastDirection.copy(currentDirection);
    }

}();

function initializeTrail() {
    trail.initialize(trailMaterial, Math.floor(options.trailLength), options.dragTexture ? 1.0 : 0.0, 0, trailHeadGeometry, trailTarget);
    updateTrailColors();
    updateTrailTextureTileSize();
    updateTrailDepthWrite();
    trail.activate();
}

function initScene() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(45, 1.0, 2, 2000);
    scene.add(camera);
    resetCamera();
}

function initStats() {
    stats = new Stats();
    stats.domElement.style.position = 'absolute';
    stats.domElement.style.bottom = '0px';
    stats.domElement.style.zIndex = 100;
    rendererContainer.appendChild(stats.domElement);
}

function initControls() {
    controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 0, 0);
    controls.update();
}

function onWindowResize() {
    getScreenDimensions();
    renderer.setSize(screenWidth, screenHeight);
    resetCamera();
}

function getScreenDimensions() {
    screenWidth = window.innerWidth;
    screenHeight = window.innerHeight;
}

function resetCamera() {
    getScreenDimensions();
    camera.aspect = screenWidth / screenHeight;
    camera.updateProjectionMatrix();
    camera.position.set(0, 200, 400);
    camera.lookAt(scene.position);
}

function animate() {
    requestAnimationFrame(animate);
    update();
    render();
}

function pauseResumeSimulation() {
    if (trail.paused) {
        trail.pause();
    } else {
        trail.resume();
    }
}

function update() {
    const currentTime = performance.now();
    const deltaTime = lastFrameTime ? currentTime - lastFrameTime : 0;
    if (!options.pauseSim){
        elapsedSimTime += deltaTime;
        updateTrailTarget(elapsedSimTime);
    }
    lastFrameTime = currentTime;
    controls.update();
    stats.update();
}

function render() {
    renderer.render(scene, camera);
}


================================================
FILE: js/OrbitControls.js
================================================
import {
	EventDispatcher,
	MOUSE,
	Quaternion,
	Spherical,
	TOUCH,
	Vector2,
	Vector3,
	Plane,
	Ray,
	MathUtils
} from 'three';

// OrbitControls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
//
//    Orbit - left mouse / touch: one-finger move
//    Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
//    Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move

const _changeEvent = { type: 'change' };
const _startEvent = { type: 'start' };
const _endEvent = { type: 'end' };
const _ray = new Ray();
const _plane = new Plane();
const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );

class OrbitControls extends EventDispatcher {

	constructor( object, domElement ) {

		super();

		this.object = object;
		this.domElement = domElement;
		this.domElement.style.touchAction = 'none'; // disable touch scroll

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

		// "target" sets the location of focus, where the object orbits around
		this.target = new Vector3();

		// How far you can dolly in and out ( PerspectiveCamera only )
		this.minDistance = 0;
		this.maxDistance = Infinity;

		// How far you can zoom in and out ( OrthographicCamera only )
		this.minZoom = 0;
		this.maxZoom = Infinity;

		// How far you can orbit vertically, upper and lower limits.
		// Range is 0 to Math.PI radians.
		this.minPolarAngle = 0; // radians
		this.maxPolarAngle = Math.PI; // radians

		// How far you can orbit horizontally, upper and lower limits.
		// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
		this.minAzimuthAngle = - Infinity; // radians
		this.maxAzimuthAngle = Infinity; // radians

		// Set to true to enable damping (inertia)
		// If damping is enabled, you must call controls.update() in your animation loop
		this.enableDamping = false;
		this.dampingFactor = 0.05;

		// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
		// Set to false to disable zooming
		this.enableZoom = true;
		this.zoomSpeed = 1.0;

		// Set to false to disable rotating
		this.enableRotate = true;
		this.rotateSpeed = 1.0;

		// Set to false to disable panning
		this.enablePan = true;
		this.panSpeed = 1.0;
		this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
		this.keyPanSpeed = 7.0;	// pixels moved per arrow key push
		this.zoomToCursor = false;

		// Set to true to automatically rotate around the target
		// If auto-rotate is enabled, you must call controls.update() in your animation loop
		this.autoRotate = false;
		this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60

		// The four arrow keys
		this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };

		// Mouse buttons
		this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };

		// Touch fingers
		this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };

		// for reset
		this.target0 = this.target.clone();
		this.position0 = this.object.position.clone();
		this.zoom0 = this.object.zoom;

		// the target DOM element for key events
		this._domElementKeyEvents = null;

		//
		// public methods
		//

		this.getPolarAngle = function () {

			return spherical.phi;

		};

		this.getAzimuthalAngle = function () {

			return spherical.theta;

		};

		this.getDistance = function () {

			return this.object.position.distanceTo( this.target );

		};

		this.listenToKeyEvents = function ( domElement ) {

			domElement.addEventListener( 'keydown', onKeyDown );
			this._domElementKeyEvents = domElement;

		};

		this.stopListenToKeyEvents = function () {

			this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
			this._domElementKeyEvents = null;

		};

		this.saveState = function () {

			scope.target0.copy( scope.target );
			scope.position0.copy( scope.object.position );
			scope.zoom0 = scope.object.zoom;

		};

		this.reset = function () {

			scope.target.copy( scope.target0 );
			scope.object.position.copy( scope.position0 );
			scope.object.zoom = scope.zoom0;

			scope.object.updateProjectionMatrix();
			scope.dispatchEvent( _changeEvent );

			scope.update();

			state = STATE.NONE;

		};

		// this method is exposed, but perhaps it would be better if we can make it private...
		this.update = function () {

			const offset = new Vector3();

			// so camera.up is the orbit axis
			const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
			const quatInverse = quat.clone().invert();

			const lastPosition = new Vector3();
			const lastQuaternion = new Quaternion();
			const lastTargetPosition = new Vector3();

			const twoPI = 2 * Math.PI;

			return function update() {

				const position = scope.object.position;

				offset.copy( position ).sub( scope.target );

				// rotate offset to "y-axis-is-up" space
				offset.applyQuaternion( quat );

				// angle from z-axis around y-axis
				spherical.setFromVector3( offset );

				if ( scope.autoRotate && state === STATE.NONE ) {

					rotateLeft( getAutoRotationAngle() );

				}

				if ( scope.enableDamping ) {

					spherical.theta += sphericalDelta.theta * scope.dampingFactor;
					spherical.phi += sphericalDelta.phi * scope.dampingFactor;

				} else {

					spherical.theta += sphericalDelta.theta;
					spherical.phi += sphericalDelta.phi;

				}

				// restrict theta to be between desired limits

				let min = scope.minAzimuthAngle;
				let max = scope.maxAzimuthAngle;

				if ( isFinite( min ) && isFinite( max ) ) {

					if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;

					if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;

					if ( min <= max ) {

						spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );

					} else {

						spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
							Math.max( min, spherical.theta ) :
							Math.min( max, spherical.theta );

					}

				}

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

				spherical.makeSafe();


				// move target to panned location

				if ( scope.enableDamping === true ) {

					scope.target.addScaledVector( panOffset, scope.dampingFactor );

				} else {

					scope.target.add( panOffset );

				}

				// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
				// we adjust zoom later in these cases
				if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) {

					spherical.radius = clampDistance( spherical.radius );

				} else {

					spherical.radius = clampDistance( spherical.radius * scale );

				}


				offset.setFromSpherical( spherical );

				// rotate offset back to "camera-up-vector-is-up" space
				offset.applyQuaternion( quatInverse );

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

				scope.object.lookAt( scope.target );

				if ( scope.enableDamping === true ) {

					sphericalDelta.theta *= ( 1 - scope.dampingFactor );
					sphericalDelta.phi *= ( 1 - scope.dampingFactor );

					panOffset.multiplyScalar( 1 - scope.dampingFactor );

				} else {

					sphericalDelta.set( 0, 0, 0 );

					panOffset.set( 0, 0, 0 );

				}

				// adjust camera position
				let zoomChanged = false;
				if ( scope.zoomToCursor && performCursorZoom ) {

					let newRadius = null;
					if ( scope.object.isPerspectiveCamera ) {

						// move the camera down the pointer ray
						// this method avoids floating point error
						const prevRadius = offset.length();
						newRadius = clampDistance( prevRadius * scale );

						const radiusDelta = prevRadius - newRadius;
						scope.object.position.addScaledVector( dollyDirection, radiusDelta );
						scope.object.updateMatrixWorld();

					} else if ( scope.object.isOrthographicCamera ) {

						// adjust the ortho camera position based on zoom changes
						const mouseBefore = new Vector3( mouse.x, mouse.y, 0 );
						mouseBefore.unproject( scope.object );

						scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );
						scope.object.updateProjectionMatrix();
						zoomChanged = true;

						const mouseAfter = new Vector3( mouse.x, mouse.y, 0 );
						mouseAfter.unproject( scope.object );

						scope.object.position.sub( mouseAfter ).add( mouseBefore );
						scope.object.updateMatrixWorld();

						newRadius = offset.length();

					} else {

						console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );
						scope.zoomToCursor = false;

					}

					// handle the placement of the target
					if ( newRadius !== null ) {

						if ( this.screenSpacePanning ) {

							// position the orbit target in front of the new camera position
							scope.target.set( 0, 0, - 1 )
								.transformDirection( scope.object.matrix )
								.multiplyScalar( newRadius )
								.add( scope.object.position );

						} else {

							// get the ray and translation plane to compute target
							_ray.origin.copy( scope.object.position );
							_ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix );

							// if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
							// extremely large values
							if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) {

								object.lookAt( scope.target );

							} else {

								_plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target );
								_ray.intersectPlane( _plane, scope.target );

							}

						}

					}

				} else if ( scope.object.isOrthographicCamera ) {

					scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) );
					scope.object.updateProjectionMatrix();
					zoomChanged = true;

				}

				scale = 1;
				performCursorZoom = false;

				// update condition is:
				// min(camera displacement, camera rotation in radians)^2 > EPS
				// using small-angle approximation cos(x/2) = 1 - x^2 / 8

				if ( zoomChanged ||
					lastPosition.distanceToSquared( scope.object.position ) > EPS ||
					8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ||
					lastTargetPosition.distanceToSquared( scope.target ) > 0 ) {

					scope.dispatchEvent( _changeEvent );

					lastPosition.copy( scope.object.position );
					lastQuaternion.copy( scope.object.quaternion );
					lastTargetPosition.copy( scope.target );

					zoomChanged = false;

					return true;

				}

				return false;

			};

		}();

		this.dispose = function () {

			scope.domElement.removeEventListener( 'contextmenu', onContextMenu );

			scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
			scope.domElement.removeEventListener( 'pointercancel', onPointerUp );
			scope.domElement.removeEventListener( 'wheel', onMouseWheel );

			scope.domElement.removeEventListener( 'pointermove', onPointerMove );
			scope.domElement.removeEventListener( 'pointerup', onPointerUp );


			if ( scope._domElementKeyEvents !== null ) {

				scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
				scope._domElementKeyEvents = null;

			}

			//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?

		};

		//
		// internals
		//

		const scope = this;

		const STATE = {
			NONE: - 1,
			ROTATE: 0,
			DOLLY: 1,
			PAN: 2,
			TOUCH_ROTATE: 3,
			TOUCH_PAN: 4,
			TOUCH_DOLLY_PAN: 5,
			TOUCH_DOLLY_ROTATE: 6
		};

		let state = STATE.NONE;

		const EPS = 0.000001;

		// current position in spherical coordinates
		const spherical = new Spherical();
		const sphericalDelta = new Spherical();

		let scale = 1;
		const panOffset = new Vector3();

		const rotateStart = new Vector2();
		const rotateEnd = new Vector2();
		const rotateDelta = new Vector2();

		const panStart = new Vector2();
		const panEnd = new Vector2();
		const panDelta = new Vector2();

		const dollyStart = new Vector2();
		const dollyEnd = new Vector2();
		const dollyDelta = new Vector2();

		const dollyDirection = new Vector3();
		const mouse = new Vector2();
		let performCursorZoom = false;

		const pointers = [];
		const pointerPositions = {};

		function getAutoRotationAngle() {

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

		}

		function getZoomScale() {

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

		}

		function rotateLeft( angle ) {

			sphericalDelta.theta -= angle;

		}

		function rotateUp( angle ) {

			sphericalDelta.phi -= angle;

		}

		const panLeft = function () {

			const v = new Vector3();

			return function panLeft( distance, objectMatrix ) {

				v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
				v.multiplyScalar( - distance );

				panOffset.add( v );

			};

		}();

		const panUp = function () {

			const v = new Vector3();

			return function panUp( distance, objectMatrix ) {

				if ( scope.screenSpacePanning === true ) {

					v.setFromMatrixColumn( objectMatrix, 1 );

				} else {

					v.setFromMatrixColumn( objectMatrix, 0 );
					v.crossVectors( scope.object.up, v );

				}

				v.multiplyScalar( distance );

				panOffset.add( v );

			};

		}();

		// deltaX and deltaY are in pixels; right and down are positive
		const pan = function () {

			const offset = new Vector3();

			return function pan( deltaX, deltaY ) {

				const element = scope.domElement;

				if ( scope.object.isPerspectiveCamera ) {

					// perspective
					const position = scope.object.position;
					offset.copy( position ).sub( scope.target );
					let targetDistance = offset.length();

					// half of the fov is center to top of screen
					targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );

					// we use only clientHeight here so aspect ratio does not distort speed
					panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
					panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );

				} else if ( scope.object.isOrthographicCamera ) {

					// orthographic
					panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
					panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );

				} else {

					// camera neither orthographic nor perspective
					console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
					scope.enablePan = false;

				}

			};

		}();

		function dollyOut( dollyScale ) {

			if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {

				scale /= dollyScale;

			} else {

				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
				scope.enableZoom = false;

			}

		}

		function dollyIn( dollyScale ) {

			if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) {

				scale *= dollyScale;

			} else {

				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
				scope.enableZoom = false;

			}

		}

		function updateMouseParameters( event ) {

			if ( ! scope.zoomToCursor ) {

				return;

			}

			performCursorZoom = true;

			const rect = scope.domElement.getBoundingClientRect();
			const x = event.clientX - rect.left;
			const y = event.clientY - rect.top;
			const w = rect.width;
			const h = rect.height;

			mouse.x = ( x / w ) * 2 - 1;
			mouse.y = - ( y / h ) * 2 + 1;

			dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( object ).sub( object.position ).normalize();

		}

		function clampDistance( dist ) {

			return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) );

		}

		//
		// event callbacks - update the object state
		//

		function handleMouseDownRotate( event ) {

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

		}

		function handleMouseDownDolly( event ) {

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

		}

		function handleMouseDownPan( event ) {

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

		}

		function handleMouseMoveRotate( event ) {

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

			rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );

			const element = scope.domElement;

			rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height

			rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );

			rotateStart.copy( rotateEnd );

			scope.update();

		}

		function handleMouseMoveDolly( event ) {

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

			dollyDelta.subVectors( dollyEnd, dollyStart );

			if ( dollyDelta.y > 0 ) {

				dollyOut( getZoomScale() );

			} else if ( dollyDelta.y < 0 ) {

				dollyIn( getZoomScale() );

			}

			dollyStart.copy( dollyEnd );

			scope.update();

		}

		function handleMouseMovePan( event ) {

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

			panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );

			pan( panDelta.x, panDelta.y );

			panStart.copy( panEnd );

			scope.update();

		}

		function handleMouseWheel( event ) {

			updateMouseParameters( event );

			if ( event.deltaY < 0 ) {

				dollyIn( getZoomScale() );

			} else if ( event.deltaY > 0 ) {

				dollyOut( getZoomScale() );

			}

			scope.update();

		}

		function handleKeyDown( event ) {

			let needsUpdate = false;

			switch ( event.code ) {

				case scope.keys.UP:

					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

						rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );

					} else {

						pan( 0, scope.keyPanSpeed );

					}

					needsUpdate = true;
					break;

				case scope.keys.BOTTOM:

					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

						rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );

					} else {

						pan( 0, - scope.keyPanSpeed );

					}

					needsUpdate = true;
					break;

				case scope.keys.LEFT:

					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

						rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );

					} else {

						pan( scope.keyPanSpeed, 0 );

					}

					needsUpdate = true;
					break;

				case scope.keys.RIGHT:

					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

						rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight );

					} else {

						pan( - scope.keyPanSpeed, 0 );

					}

					needsUpdate = true;
					break;

			}

			if ( needsUpdate ) {

				// prevent the browser from scrolling on cursor keys
				event.preventDefault();

				scope.update();

			}


		}

		function handleTouchStartRotate() {

			if ( pointers.length === 1 ) {

				rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );

			} else {

				const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
				const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );

				rotateStart.set( x, y );

			}

		}

		function handleTouchStartPan() {

			if ( pointers.length === 1 ) {

				panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );

			} else {

				const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
				const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );

				panStart.set( x, y );

			}

		}

		function handleTouchStartDolly() {

			const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX;
			const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY;

			const distance = Math.sqrt( dx * dx + dy * dy );

			dollyStart.set( 0, distance );

		}

		function handleTouchStartDollyPan() {

			if ( scope.enableZoom ) handleTouchStartDolly();

			if ( scope.enablePan ) handleTouchStartPan();

		}

		function handleTouchStartDollyRotate() {

			if ( scope.enableZoom ) handleTouchStartDolly();

			if ( scope.enableRotate ) handleTouchStartRotate();

		}

		function handleTouchMoveRotate( event ) {

			if ( pointers.length == 1 ) {

				rotateEnd.set( event.pageX, event.pageY );

			} else {

				const position = getSecondPointerPosition( event );

				const x = 0.5 * ( event.pageX + position.x );
				const y = 0.5 * ( event.pageY + position.y );

				rotateEnd.set( x, y );

			}

			rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );

			const element = scope.domElement;

			rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height

			rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );

			rotateStart.copy( rotateEnd );

		}

		function handleTouchMovePan( event ) {

			if ( pointers.length === 1 ) {

				panEnd.set( event.pageX, event.pageY );

			} else {

				const position = getSecondPointerPosition( event );

				const x = 0.5 * ( event.pageX + position.x );
				const y = 0.5 * ( event.pageY + position.y );

				panEnd.set( x, y );

			}

			panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );

			pan( panDelta.x, panDelta.y );

			panStart.copy( panEnd );

		}

		function handleTouchMoveDolly( event ) {

			const position = getSecondPointerPosition( event );

			const dx = event.pageX - position.x;
			const dy = event.pageY - position.y;

			const distance = Math.sqrt( dx * dx + dy * dy );

			dollyEnd.set( 0, distance );

			dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );

			dollyOut( dollyDelta.y );

			dollyStart.copy( dollyEnd );

		}

		function handleTouchMoveDollyPan( event ) {

			if ( scope.enableZoom ) handleTouchMoveDolly( event );

			if ( scope.enablePan ) handleTouchMovePan( event );

		}

		function handleTouchMoveDollyRotate( event ) {

			if ( scope.enableZoom ) handleTouchMoveDolly( event );

			if ( scope.enableRotate ) handleTouchMoveRotate( event );

		}

		//
		// event handlers - FSM: listen for events and reset state
		//

		function onPointerDown( event ) {

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

			if ( pointers.length === 0 ) {

				scope.domElement.setPointerCapture( event.pointerId );

				scope.domElement.addEventListener( 'pointermove', onPointerMove );
				scope.domElement.addEventListener( 'pointerup', onPointerUp );

			}

			//

			addPointer( event );

			if ( event.pointerType === 'touch' ) {

				onTouchStart( event );

			} else {

				onMouseDown( event );

			}

		}

		function onPointerMove( event ) {

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

			if ( event.pointerType === 'touch' ) {

				onTouchMove( event );

			} else {

				onMouseMove( event );

			}

		}

		function onPointerUp( event ) {

			removePointer( event );

			if ( pointers.length === 0 ) {

				scope.domElement.releasePointerCapture( event.pointerId );

				scope.domElement.removeEventListener( 'pointermove', onPointerMove );
				scope.domElement.removeEventListener( 'pointerup', onPointerUp );

			}

			scope.dispatchEvent( _endEvent );

			state = STATE.NONE;

		}

		function onMouseDown( event ) {

			let mouseAction;

			switch ( event.button ) {

				case 0:

					mouseAction = scope.mouseButtons.LEFT;
					break;

				case 1:

					mouseAction = scope.mouseButtons.MIDDLE;
					break;

				case 2:

					mouseAction = scope.mouseButtons.RIGHT;
					break;

				default:

					mouseAction = - 1;

			}

			switch ( mouseAction ) {

				case MOUSE.DOLLY:

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

					handleMouseDownDolly( event );

					state = STATE.DOLLY;

					break;

				case MOUSE.ROTATE:

					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

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

						handleMouseDownPan( event );

						state = STATE.PAN;

					} else {

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

						handleMouseDownRotate( event );

						state = STATE.ROTATE;

					}

					break;

				case MOUSE.PAN:

					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {

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

						handleMouseDownRotate( event );

						state = STATE.ROTATE;

					} else {

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

						handleMouseDownPan( event );

						state = STATE.PAN;

					}

					break;

				default:

					state = STATE.NONE;

			}

			if ( state !== STATE.NONE ) {

				scope.dispatchEvent( _startEvent );

			}

		}

		function onMouseMove( event ) {

			switch ( state ) {

				case STATE.ROTATE:

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

					handleMouseMoveRotate( event );

					break;

				case STATE.DOLLY:

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

					handleMouseMoveDolly( event );

					break;

				case STATE.PAN:

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

					handleMouseMovePan( event );

					break;

			}

		}

		function onMouseWheel( event ) {

			if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;

			event.preventDefault();

			scope.dispatchEvent( _startEvent );

			handleMouseWheel( event );

			scope.dispatchEvent( _endEvent );

		}

		function onKeyDown( event ) {

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

			handleKeyDown( event );

		}

		function onTouchStart( event ) {

			trackPointer( event );

			switch ( pointers.length ) {

				case 1:

					switch ( scope.touches.ONE ) {

						case TOUCH.ROTATE:

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

							handleTouchStartRotate();

							state = STATE.TOUCH_ROTATE;

							break;

						case TOUCH.PAN:

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

							handleTouchStartPan();

							state = STATE.TOUCH_PAN;

							break;

						default:

							state = STATE.NONE;

					}

					break;

				case 2:

					switch ( scope.touches.TWO ) {

						case TOUCH.DOLLY_PAN:

							if ( scope.enableZoom === false && scope.enablePan === false ) return;

							handleTouchStartDollyPan();

							state = STATE.TOUCH_DOLLY_PAN;

							break;

						case TOUCH.DOLLY_ROTATE:

							if ( scope.enableZoom === false && scope.enableRotate === false ) return;

							handleTouchStartDollyRotate();

							state = STATE.TOUCH_DOLLY_ROTATE;

							break;

						default:

							state = STATE.NONE;

					}

					break;

				default:

					state = STATE.NONE;

			}

			if ( state !== STATE.NONE ) {

				scope.dispatchEvent( _startEvent );

			}

		}

		function onTouchMove( event ) {

			trackPointer( event );

			switch ( state ) {

				case STATE.TOUCH_ROTATE:

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

					handleTouchMoveRotate( event );

					scope.update();

					break;

				case STATE.TOUCH_PAN:

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

					handleTouchMovePan( event );

					scope.update();

					break;

				case STATE.TOUCH_DOLLY_PAN:

					if ( scope.enableZoom === false && scope.enablePan === false ) return;

					handleTouchMoveDollyPan( event );

					scope.update();

					break;

				case STATE.TOUCH_DOLLY_ROTATE:

					if ( scope.enableZoom === false && scope.enableRotate === false ) return;

					handleTouchMoveDollyRotate( event );

					scope.update();

					break;

				default:

					state = STATE.NONE;

			}

		}

		function onContextMenu( event ) {

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

			event.preventDefault();

		}

		function addPointer( event ) {

			pointers.push( event );

		}

		function removePointer( event ) {

			delete pointerPositions[ event.pointerId ];

			for ( let i = 0; i < pointers.length; i ++ ) {

				if ( pointers[ i ].pointerId == event.pointerId ) {

					pointers.splice( i, 1 );
					return;

				}

			}

		}

		function trackPointer( event ) {

			let position = pointerPositions[ event.pointerId ];

			if ( position === undefined ) {

				position = new Vector2();
				pointerPositions[ event.pointerId ] = position;

			}

			position.set( event.pageX, event.pageY );

		}

		function getSecondPointerPosition( event ) {

			const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ];

			return pointerPositions[ pointer.pointerId ];

		}

		//

		scope.domElement.addEventListener( 'contextmenu', onContextMenu );

		scope.domElement.addEventListener( 'pointerdown', onPointerDown );
		scope.domElement.addEventListener( 'pointercancel', onPointerUp );
		scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );

		// force an update at start

		this.update();

	}

}

export { OrbitControls };


================================================
FILE: js/TrailRenderer.js
================================================
/**
* @author Mark Kellogg - http://www.github.com/mkkellogg
*/
import * as THREE from 'three';

//=======================================
// Trail Renderer
//=======================================

export class TrailRenderer extends THREE.Object3D {

    constructor (scene, orientToMovement) {
        super();
        this.active = false;
        this.orientToMovement = false;
        if (orientToMovement) this.orientToMovement = true;
        this.scene = scene;
        this.geometry = null;
        this.mesh = null;
        this.nodeCenters = null;
        this.lastNodeCenter = null;
        this.currentNodeCenter = null;
        this.lastOrientationDir = null;
        this.nodeIDs = null;
        this.currentLength = 0;
        this.currentEnd = 0;
        this.currentNodeID = 0;
        this.advanceFrequency = 60;
        this.advancePeriod = 1 / this.advanceFrequency;
        this.lastAdvanceTime = 0;
        this.paused = false;
        this.pauseAdvanceUpdateTimeDiff = 0;
    }

    setAdvanceFrequency(advanceFrequency) {
        this.advanceFrequency = advanceFrequency;
        this.advancePeriod = 1.0 / this.advanceFrequency;
    }

    initialize (material, length, dragTexture, localHeadWidth, localHeadGeometry, targetObject) {
        this.deactivate();
        this.destroyMesh();

        this.length = (length > 0) ? length + 1 : 0;
        this.dragTexture = (! dragTexture) ? 0 : 1;
        this.targetObject = targetObject;

        this.initializeLocalHeadGeometry(localHeadWidth, localHeadGeometry);

        this.nodeIDs = [];
        this.nodeCenters = [];

        for(let i = 0; i < this.length; i ++) {
            this.nodeIDs[ i ] = -1;
            this.nodeCenters[ i ] = new THREE.Vector3();
        }

        this.material = material;

        this.initializeGeometry();
        this.initializeMesh();

        this.material.uniforms.trailLength.value = 0;
        this.material.uniforms.minID.value = 0;
        this.material.uniforms.maxID.value = 0;
        this.material.uniforms.dragTexture.value = this.dragTexture;
        this.material.uniforms.maxTrailLength.value = this.length;
        this.material.uniforms.verticesPerNode.value = this.VerticesPerNode;
        this.material.uniforms.textureTileFactor.value = new THREE.Vector2(1.0, 1.0);

        this.reset();
    }

    initializeLocalHeadGeometry (localHeadWidth, localHeadGeometry) {
        this.localHeadGeometry = [];
        if (!localHeadGeometry) {
            const halfWidth = (localHeadWidth || 1.0) / 2.0;
            this.localHeadGeometry.push(new THREE.Vector3(-halfWidth, 0, 0));
            this.localHeadGeometry.push(new THREE.Vector3(halfWidth, 0, 0));
            this.VerticesPerNode = 2;
        } else {
            this.VerticesPerNode = 0;
            for (let i = 0; i < localHeadGeometry.length && i < TrailRenderer.MaxHeadVertices; i++) {
                const vertex = localHeadGeometry[ i ];
                if (vertex && vertex instanceof THREE.Vector3) {
                    const vertexCopy = new THREE.Vector3();
                    vertexCopy.copy(vertex);
                    this.localHeadGeometry.push(vertexCopy);
                    this.VerticesPerNode ++;
                }
            }
        }
        this.FacesPerNode = (this.VerticesPerNode - 1) * 2;
        this.FaceIndicesPerNode = this.FacesPerNode * 3;
    }

    initializeGeometry () {
        this.vertexCount = this.length * this.VerticesPerNode;
        this.faceCount = this.length * this.FacesPerNode;

        const geometry = new THREE.BufferGeometry();

        const nodeIDs = new Float32Array(this.vertexCount);
        const nodeVertexIDs = new Float32Array(this.vertexCount * this.VerticesPerNode);
        const positions = new Float32Array(this.vertexCount * TrailRenderer.PositionComponentCount);
        const nodeCenters = new Float32Array(this.vertexCount * TrailRenderer.PositionComponentCount);
        const uvs = new Float32Array(this.vertexCount * TrailRenderer.UVComponentCount);
        const indices = new Uint32Array(this.faceCount * TrailRenderer.IndicesPerFace);

        const nodeIDAttribute = new THREE.BufferAttribute(nodeIDs, 1);
        nodeIDAttribute.dynamic = true;
        geometry.setAttribute('nodeID', nodeIDAttribute);

        const nodeVertexIDAttribute = new THREE.BufferAttribute(nodeVertexIDs, 1);
        nodeVertexIDAttribute.dynamic = true;
        geometry.setAttribute('nodeVertexID', nodeVertexIDAttribute);

        const nodeCenterAttribute = new THREE.BufferAttribute(nodeCenters, TrailRenderer.PositionComponentCount);
        nodeCenterAttribute.dynamic = true;
        geometry.setAttribute('nodeCenter', nodeCenterAttribute);

        const positionAttribute = new THREE.BufferAttribute(positions, TrailRenderer.PositionComponentCount);
        positionAttribute.dynamic = true;
        geometry.setAttribute('position', positionAttribute);

        const uvAttribute = new THREE.BufferAttribute(uvs, TrailRenderer.UVComponentCount);
        uvAttribute.dynamic = true;
        geometry.setAttribute('uv', uvAttribute);

        const indexAttribute = new THREE.BufferAttribute(indices, 1);
        indexAttribute.dynamic = true;
        geometry.setIndex(indexAttribute);

        this.geometry = geometry;
    }

    zeroVertices () {
        const positions = this.geometry.getAttribute('position');
        for (let i = 0; i < this.vertexCount; i ++) {
            const index = i * 3;
            positions.array[ index ] = 0;
            positions.array[ index + 1 ] = 0;
            positions.array[ index + 2 ] = 0;
        }
        positions.needsUpdate = true;
        positions.updateRange.count = - 1;
    }

    zeroIndices () {
        const indices = this.geometry.getIndex();
        for (let i = 0; i < this.faceCount; i ++) {
            const index = i * 3;
            indices.array[ index ] = 0;
            indices.array[ index + 1 ] = 0;
            indices.array[ index + 2 ] = 0;
        }
        indices.needsUpdate = true;
        indices.updateRange.count = - 1;
    }

    formInitialFaces () {
        this.zeroIndices();
        const indices = this.geometry.getIndex();
        for (let i = 0; i < this.length - 1; i ++) {
            this.connectNodes(i, i + 1);
        }
        indices.needsUpdate = true;
        indices.updateRange.count = - 1;
    }

    initializeMesh () {
        this.mesh = new THREE.Mesh(this.geometry, this.material);
        this.mesh.dynamic = true;
        this.mesh.matrixAutoUpdate = false;
    }

    destroyMesh () {
        if (this.mesh) {
            this.scene.remove(this.mesh);
            this.mesh = null;
        }
    }

    reset () {
        this.currentLength = 0;
        this.currentEnd = -1;
        this.lastNodeCenter = null;
        this.currentNodeCenter = null;
        this.lastOrientationDir = null;
        this.currentNodeID = 0;
        this.formInitialFaces();
        this.zeroVertices();
        this.geometry.setDrawRange(0, 0);
    }

    updateUniforms () {
        if (this.currentLength < this.length) {
            this.material.uniforms.minID.value = 0;
        } else {
            this.material.uniforms.minID.value = this.currentNodeID - this.length;
        }
        this.material.uniforms.maxID.value = this.currentNodeID;
        this.material.uniforms.trailLength.value = this.currentLength;
        this.material.uniforms.maxTrailLength.value = this.length;
        this.material.uniforms.verticesPerNode.value = this.VerticesPerNode;
    }

    advance = function() {
        const tempMatrix4 = new THREE.Matrix4();
        return function advance() {
            this.targetObject.updateMatrixWorld();
            tempMatrix4.copy(this.targetObject.matrixWorld);
            this.advanceWithTransform(tempMatrix4);
            this.updateUniforms();
        };
    }();

    advanceWithPositionAndOrientation (nextPosition, orientationTangent) {
        this.advanceGeometry({ position : nextPosition, tangent : orientationTangent }, null);
    }

    advanceWithTransform (transformMatrix) {
        this.advanceGeometry(null, transformMatrix);
    }

    advanceGeometry = function() { 

        return function advanceGeometry(positionAndOrientation, transformMatrix) {
            const nextIndex = this.currentEnd + 1 >= this.length ? 0 : this.currentEnd + 1; 
            if(transformMatrix) {
                this.updateNodePositionsFromTransformMatrix(nextIndex, transformMatrix);
            } else {
                this.updateNodePositionsFromOrientationTangent(nextIndex, positionAndOrientation.position, positionAndOrientation.tangent);
            }

            if (this.currentLength >= 1) {
                this.connectNodes(this.currentEnd , nextIndex);
                if(this.currentLength >= this.length) {
                    const disconnectIndex  = this.currentEnd + 1  >= this.length ? 0 : this.currentEnd + 1;
                    this.disconnectNodes(disconnectIndex);
                }
            }

            if(this.currentLength < this.length) {
                this.currentLength ++;
            }

            this.currentEnd++;
            if (this.currentEnd >= this.length) {
                this.currentEnd = 0;
            }

            if (this.currentLength >= 1) {
                if(this.currentLength < this.length) {
                    this.geometry.setDrawRange(0, (this.currentLength - 1) * this.FaceIndicesPerNode);
                } else {
                    this.geometry.setDrawRange(0, this.currentLength * this.FaceIndicesPerNode);
                }
            }
            this.updateNodeID(this.currentEnd,  this.currentNodeID);
            this.currentNodeID ++;
        };

    }();

    currentTime() {
        return performance.now() / 1000;
    }

    pause() {
        if(!this.paused) {
            this.paused = true;
            this.pauseAdvanceUpdateTimeDiff = this.currentTime() - this.lastAdvanceTime;
        }
    }

    resume() {
        if(this.paused) {
            this.paused = false;
            this.lastAdvanceTime = this.currentTime() - this.pauseAdvanceUpdateTimeDiff;
        }
    }

    update() {
        if (!this.paused) {
            const time = this.currentTime();
            if (!this.lastAdvanceTime) this.lastAdvanceTime = time;
            if (time - this.lastAdvanceTime > this.advancePeriod) {
                this.advance();
                this.lastAdvanceTime = time;
            } else {
                this.updateHead();
            }
        }
    }

    updateHead = function() {

        const tempMatrix4 = new THREE.Matrix4();

        return function updateHead() {
            if(this.currentEnd < 0) return;
            this.targetObject.updateMatrixWorld();
            tempMatrix4.copy(this.targetObject.matrixWorld);
            this.updateNodePositionsFromTransformMatrix(this.currentEnd, tempMatrix4);
        };

    }();

    updateNodeID (nodeIndex, id) { 
        this.nodeIDs[ nodeIndex ] = id;
        const nodeIDs = this.geometry.getAttribute('nodeID');
        const nodeVertexIDs = this.geometry.getAttribute('nodeVertexID');
        for (let i = 0; i < this.VerticesPerNode; i ++) {
            const baseIndex = nodeIndex * this.VerticesPerNode + i ;
            nodeIDs.array[ baseIndex ] = id;
            nodeVertexIDs.array[ baseIndex ] = i;
        }    
        nodeIDs.needsUpdate = true;
        nodeVertexIDs.needsUpdate = true;
        nodeIDs.updateRange.offset = nodeIndex * this.VerticesPerNode; 
        nodeIDs.updateRange.count = this.VerticesPerNode;
        nodeVertexIDs.updateRange.offset = nodeIndex * this.VerticesPerNode;
        nodeVertexIDs.updateRange.count = this.VerticesPerNode;
    }

    updateNodeCenter (nodeIndex, nodeCenter) { 
        this.lastNodeCenter = this.currentNodeCenter;
        this.currentNodeCenter = this.nodeCenters[ nodeIndex ];
        this.currentNodeCenter.copy(nodeCenter);
        const nodeCenters = this.geometry.getAttribute('nodeCenter');
        for (let i = 0; i < this.VerticesPerNode; i ++) {
            const baseIndex = (nodeIndex * this.VerticesPerNode + i) * 3;
            nodeCenters.array[ baseIndex ] = nodeCenter.x;
            nodeCenters.array[ baseIndex + 1 ] = nodeCenter.y;
            nodeCenters.array[ baseIndex + 2 ] = nodeCenter.z;
        }    
        nodeCenters.needsUpdate = true;
        nodeCenters.updateRange.offset = nodeIndex * this.VerticesPerNode * TrailRenderer.PositionComponentCount; 
        nodeCenters.updateRange.count = this.VerticesPerNode * TrailRenderer.PositionComponentCount; 
    }

    updateNodePositionsFromOrientationTangent = function() { 

        const tempQuaternion = new THREE.Quaternion();
        const tempOffset = new THREE.Vector3();
        const tempLocalHeadGeometry = [];
        for (let i = 0; i < TrailRenderer.MaxHeadVertices; i ++) {
            const vertex = new THREE.Vector3();
            tempLocalHeadGeometry.push(vertex);
        }

        return function updateNodePositionsFromOrientationTangent(nodeIndex, nodeCenter, orientationTangent ) {
            const positions = this.geometry.getAttribute('position');
            this.updateNodeCenter(nodeIndex, nodeCenter);
            tempOffset.copy(nodeCenter);
            tempOffset.sub(TrailRenderer.LocalHeadOrigin);
            tempQuaternion.setFromUnitVectors(TrailRenderer.LocalOrientationTangent, orientationTangent);

            for (let i = 0; i < this.localHeadGeometry.length; i ++) {
                const vertex = tempLocalHeadGeometry[ i ];
                vertex.copy(this.localHeadGeometry[ i ]);
                vertex.applyQuaternion(tempQuaternion);
                vertex.add(tempOffset);
            }

            for (let i = 0; i <  this.localHeadGeometry.length; i ++) {
                const positionIndex = ((this.VerticesPerNode * nodeIndex) + i) * TrailRenderer.PositionComponentCount;
                const transformedHeadVertex = tempLocalHeadGeometry[ i ];
                positions.array[ positionIndex ] = transformedHeadVertex.x;
                positions.array[ positionIndex + 1 ] = transformedHeadVertex.y;
                positions.array[ positionIndex + 2 ] = transformedHeadVertex.z;
            }

            positions.needsUpdate = true;
        };

    }();

    updateNodePositionsFromTransformMatrix = function() { 

        const tempMatrix3 = new THREE.Matrix3();
        const tempQuaternion = new THREE.Quaternion();
        const tempPosition = new THREE.Vector3();
        const tempOffset = new THREE.Vector3();
        const worldOrientation = new THREE.Vector3();
        const tempDirection = new THREE.Vector3();

        const tempLocalHeadGeometry = [];
        for (let i = 0; i < TrailRenderer.MaxHeadVertices; i ++) {
            const vertex = new THREE.Vector3();
            tempLocalHeadGeometry.push(vertex);
        }

        function getMatrix3FromMatrix4(matrix3, matrix4) {
            const e = matrix4.elements;
            matrix3.set(e[0], e[1], e[2],
                        e[4], e[5], e[6],
                        e[8], e[9], e[10]);

        }

        return function updateNodePositionsFromTransformMatrix(nodeIndex, transformMatrix) {
            const positions = this.geometry.getAttribute('position');
            tempPosition.set(0, 0, 0);
            tempPosition.applyMatrix4(transformMatrix);
            this.updateNodeCenter(nodeIndex, tempPosition);
            for (let i = 0; i < this.localHeadGeometry.length; i ++) {
                const vertex = tempLocalHeadGeometry[ i ];
                vertex.copy(this.localHeadGeometry[ i ]);
            }

            for (let i = 0; i < this.localHeadGeometry.length; i ++) {
                const vertex = tempLocalHeadGeometry[ i ];
                vertex.applyMatrix4(transformMatrix);
            }
            
            if(this.lastNodeCenter && this.orientToMovement) {
                getMatrix3FromMatrix4(tempMatrix3, transformMatrix);
                worldOrientation.set(0, 0, -1);
                worldOrientation.applyMatrix3(tempMatrix3);
                tempDirection.copy(this.currentNodeCenter);
                tempDirection.sub(this.lastNodeCenter);
                tempDirection.normalize();

                if(tempDirection.lengthSq() <= .0001 && this.lastOrientationDir) {
                    tempDirection.copy(this.lastOrientationDir);
                }

                if(tempDirection.lengthSq() > .0001) {
                    if(! this.lastOrientationDir) this.lastOrientationDir = new THREE.Vector3();
                    tempQuaternion.setFromUnitVectors(worldOrientation, tempDirection);
                    tempOffset.copy(this.currentNodeCenter);
                    for (let i = 0; i < this.localHeadGeometry.length; i ++) {
                        const vertex = tempLocalHeadGeometry[ i ];
                        vertex.sub(tempOffset);
                        vertex.applyQuaternion(tempQuaternion);
                        vertex.add(tempOffset);
                    }
                }
            }

            for (let i = 0; i < this.localHeadGeometry.length; i ++) {
                const positionIndex = ((this.VerticesPerNode * nodeIndex) + i) * TrailRenderer.PositionComponentCount;
                const transformedHeadVertex = tempLocalHeadGeometry[ i ];
                positions.array[ positionIndex ] = transformedHeadVertex.x;
                positions.array[ positionIndex + 1 ] = transformedHeadVertex.y;
                positions.array[ positionIndex + 2 ] = transformedHeadVertex.z;
            }
            positions.needsUpdate = true;
            positions.updateRange.offset = nodeIndex * this.VerticesPerNode * TrailRenderer.PositionComponentCount; 
            positions.updateRange.count = this.VerticesPerNode * TrailRenderer.PositionComponentCount; 
        };

    }();

    connectNodes = function() {

        const returnObj = {
            "attribute" : null,
            "offset" : 0,
            "count" : - 1
        };

        return function connectNodes(srcNodeIndex, destNodeIndex) {
            const indices = this.geometry.getIndex();
            for (let i = 0; i < this.localHeadGeometry.length - 1; i ++) {
                const srcVertexIndex = (this.VerticesPerNode * srcNodeIndex) + i;
                const destVertexIndex = (this.VerticesPerNode * destNodeIndex) + i;
                const faceIndex = ((srcNodeIndex * this.FacesPerNode) + (i * TrailRenderer.FacesPerQuad )) * TrailRenderer.IndicesPerFace;
                indices.array[ faceIndex ] = srcVertexIndex;
                indices.array[ faceIndex + 1 ] = destVertexIndex;
                indices.array[ faceIndex + 2 ] = srcVertexIndex + 1;
                indices.array[ faceIndex + 3 ] = destVertexIndex;
                indices.array[ faceIndex + 4 ] = destVertexIndex + 1;
                indices.array[ faceIndex + 5 ] = srcVertexIndex + 1;
            }
            indices.needsUpdate = true;
            indices.updateRange.count = - 1;
            returnObj.attribute = indices;
            returnObj.offset =  srcNodeIndex * this.FacesPerNode * TrailRenderer.IndicesPerFace;
            returnObj.count = this.FacesPerNode * TrailRenderer.IndicesPerFace;
            return returnObj;

        };

    }();

    disconnectNodes = function() {

        const returnObj = {
            "attribute" : null,
            "offset" : 0,
            "count" : - 1
        };

        return function disconnectNodes(srcNodeIndex) {
            const indices = this.geometry.getIndex();
            for (let i = 0; i < this.localHeadGeometry.length - 1; i ++) {
                const faceIndex = ((srcNodeIndex * this.FacesPerNode) + (i * TrailRenderer.FacesPerQuad)) * TrailRenderer.IndicesPerFace;
                indices.array[ faceIndex ] = 0;
                indices.array[ faceIndex + 1 ] = 0;
                indices.array[ faceIndex + 2 ] = 0;
                indices.array[ faceIndex + 3 ] = 0;
                indices.array[ faceIndex + 4 ] = 0;
                indices.array[ faceIndex + 5 ] = 0;
            }
            indices.needsUpdate = true;
            indices.updateRange.count = - 1;
            returnObj.attribute = indices;
            returnObj.offset = srcNodeIndex * this.FacesPerNode * TrailRenderer.IndicesPerFace;
            returnObj.count = this.FacesPerNode * TrailRenderer.IndicesPerFace;
            return returnObj;
        };

    }();

    deactivate () {
        if (this.isActive) {
            this.scene.remove(this.mesh);
            this.isActive = false;
        }
    }

    activate () {
        if (! this.isActive) {
            this.scene.add(this.mesh);
            this.isActive = true;
        }
    }

    static createMaterial(vertexShader, fragmentShader, customUniforms) {

        customUniforms = customUniforms || {};
        customUniforms.trailLength = { type: "f", value: null };
        customUniforms.verticesPerNode = { type: "f", value: null };
        customUniforms.minID = { type: "f", value: null };
        customUniforms.maxID = { type: "f", value: null };
        customUniforms.dragTexture = { type: "f", value: null };
        customUniforms.maxTrailLength = { type: "f", value: null };
        customUniforms.textureTileFactor = { type: "v2", value: null };
        customUniforms.headColor = { type: "v4", value: new THREE.Vector4() };
        customUniforms.tailColor = { type: "v4", value: new THREE.Vector4() };

        vertexShader = vertexShader || TrailRenderer.Shader.BaseVertexShader;
        fragmentShader = fragmentShader || TrailRenderer.Shader.BaseFragmentShader;

        return new THREE.ShaderMaterial({
            uniforms: customUniforms,
            vertexShader: vertexShader,
            fragmentShader: fragmentShader,
            transparent: true,
            alphaTest: 0.5,
            blending : THREE.CustomBlending,
            blendSrc : THREE.SrcAlphaFactor,
            blendDst : THREE.OneMinusSrcAlphaFactor,
            blendEquation : THREE.AddEquation,
            depthTest: true,
            depthWrite: false,
            side: THREE.DoubleSide
        });
    
    }
    
    static createBaseMaterial(customUniforms) {
        return TrailRenderer.createMaterial(TrailRenderer.Shader.BaseVertexShader, TrailRenderer.Shader.BaseFragmentShader, customUniforms);
    }

    static createTexturedMaterial (customUniforms) {
        customUniforms = {};
        customUniforms.trailTexture = { type: "t", value: null };
        return TrailRenderer.createMaterial(TrailRenderer.Shader.TexturedVertexShader, TrailRenderer.Shader.TexturedFragmentShader, customUniforms);
    }

    static get MaxHeadVertices () {
        return 128;
    }

    static _LocalOrientationTangent = new THREE.Vector3(1, 0, 0);
    static get LocalOrientationTangent () {
        return _LocalOrientationTangent;
    }

    static _LocalHeadOrigin = new THREE.Vector3(0, 0, 0);
    static get LocalHeadOrigin () {
        return _LocalHeadOrigin;
    }

    static get PositionComponentCount () {
        return 3;
    }

    static get UVComponentCount () {
        return 2;
    }

    static get IndicesPerFace () {
        return 3;
    }

    static get FacesPerQuad () {
        return 2;
    }

    static Shader = {

        get BaseVertexVars() {
            return [
                "attribute float nodeID;",
                "attribute float nodeVertexID;",
                "attribute vec3 nodeCenter;",
                "uniform float minID;",
                "uniform float maxID;",
                "uniform float trailLength;",
                "uniform float maxTrailLength;",
                "uniform float verticesPerNode;",
                "uniform vec2 textureTileFactor;",
                "uniform vec4 headColor;",
                "uniform vec4 tailColor;",
                "varying vec4 vColor;",
            ].join("\n")
        },

        get TexturedVertexVars() {
            return [
                this.BaseVertexVars, 
                "varying vec2 vUV;",
                "uniform float dragTexture;",
            ].join("\n");
        },

        BaseFragmentVars: [
            "varying vec4 vColor;",
            "uniform sampler2D trailTexture;",
        ].join("\n"),

        get TexturedFragmentVars() {
            return [
                this.BaseFragmentVars,
                "varying vec2 vUV;"
            ].join("\n");
        },

        get VertexShaderCore() {
            return [
                "float fraction = (maxID - nodeID) / (maxID - minID);",
                "vColor = (1.0 - fraction) * headColor + fraction * tailColor;",
                "vec4 realPosition = vec4((1.0 - fraction) * position.xyz + fraction * nodeCenter.xyz, 1.0); ", 
            ].join("\n");
        },

        get BaseVertexShader() {
            return [
                this.BaseVertexVars,
                "void main() { ",
                    this.VertexShaderCore,
                    "gl_Position = projectionMatrix * viewMatrix * realPosition;",
                "}"
            ].join("\n");
        },

        get BaseFragmentShader() {
            return [
                this.BaseFragmentVars,
                "void main() { ",
                    "gl_FragColor = vColor;",
                "}"
            ].join("\n");
        },

        get TexturedVertexShader() {
            return [
                this.TexturedVertexVars,
                "void main() { ",
                    this.VertexShaderCore,
                    "float s = 0.0;",
                    "float t = 0.0;",
                    "if (dragTexture == 1.0) { ",
                    "   s = fraction *  textureTileFactor.s; ",
                    "     t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;",
                    "} else { ",
                    "    s = nodeID / maxTrailLength * textureTileFactor.s;",
                    "     t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;",
                    "}",
                    "vUV = vec2(s, t); ",
                    "gl_Position = projectionMatrix * viewMatrix * realPosition;",
                "}"
            ].join("\n");
        },

        get TexturedFragmentShader() {
            return [
                this.TexturedFragmentVars,
                "void main() { ",
                    "vec4 textureColor = texture2D(trailTexture, vUV);",
                    "gl_FragColor = vColor * textureColor;",
                "}"
            ].join("\n");
        }
    };
}
Download .txt
gitextract_vtl97efo/

├── .gitignore
├── README.md
├── index.html
└── js/
    ├── Main.js
    ├── OrbitControls.js
    └── TrailRenderer.js
Download .txt
SYMBOL INDEX (75 symbols across 3 files)

FILE: js/Main.js
  function init (line 30) | function init() {
  function initTrailOptions (line 50) | function initTrailOptions() {
  function initGUI (line 71) | function initGUI() {
  function initListeners (line 112) | function initListeners() {
  function initRenderer (line 116) | function initRenderer() {
  function initLights (line 125) | function initLights() {
  function initSceneGeometry (line 133) | function initSceneGeometry(onFinished) {
  function initTrailHeadGeometries (line 141) | function initTrailHeadGeometries() {
  function initTrailTarget (line 172) | function initTrailTarget() {
  function initTrailRenderers (line 197) | function initTrailRenderers(callback) {
  function updateTrailLength (line 216) | function updateTrailLength() {
  function setTrailTypeFromOptions (line 220) | function setTrailTypeFromOptions() {
  function updateTrailType (line 231) | function updateTrailType() {
  function setTrailShapeFromOptions (line 236) | function setTrailShapeFromOptions() {
  function updateTrailShape (line 250) | function updateTrailShape() {
  function updateTrailTextureDrag (line 255) | function updateTrailTextureDrag() {
  function updateTrailTextureTileSize (line 259) | function updateTrailTextureTileSize() {
  function updateTrailColors (line 263) | function updateTrailColors() {
  function updateTrailDepthWrite (line 268) | function updateTrailDepthWrite() {
  function initializeTrail (line 337) | function initializeTrail() {
  function initScene (line 345) | function initScene() {
  function initStats (line 352) | function initStats() {
  function initControls (line 360) | function initControls() {
  function onWindowResize (line 366) | function onWindowResize() {
  function getScreenDimensions (line 372) | function getScreenDimensions() {
  function resetCamera (line 377) | function resetCamera() {
  function animate (line 385) | function animate() {
  function pauseResumeSimulation (line 391) | function pauseResumeSimulation() {
  function update (line 399) | function update() {
  function render (line 411) | function render() {

FILE: js/OrbitControls.js
  constant TILT_LIMIT (line 26) | const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );
  class OrbitControls (line 28) | class OrbitControls extends EventDispatcher {
    method constructor (line 30) | constructor( object, domElement ) {

FILE: js/TrailRenderer.js
  class TrailRenderer (line 10) | class TrailRenderer extends THREE.Object3D {
    method constructor (line 12) | constructor (scene, orientToMovement) {
    method setAdvanceFrequency (line 35) | setAdvanceFrequency(advanceFrequency) {
    method initialize (line 40) | initialize (material, length, dragTexture, localHeadWidth, localHeadGe...
    method initializeLocalHeadGeometry (line 74) | initializeLocalHeadGeometry (localHeadWidth, localHeadGeometry) {
    method initializeGeometry (line 97) | initializeGeometry () {
    method zeroVertices (line 137) | zeroVertices () {
    method zeroIndices (line 149) | zeroIndices () {
    method formInitialFaces (line 161) | formInitialFaces () {
    method initializeMesh (line 171) | initializeMesh () {
    method destroyMesh (line 177) | destroyMesh () {
    method reset (line 184) | reset () {
    method updateUniforms (line 196) | updateUniforms () {
    method advanceWithPositionAndOrientation (line 218) | advanceWithPositionAndOrientation (nextPosition, orientationTangent) {
    method advanceWithTransform (line 222) | advanceWithTransform (transformMatrix) {
    method currentTime (line 266) | currentTime() {
    method pause (line 270) | pause() {
    method resume (line 277) | resume() {
    method update (line 284) | update() {
    method updateNodeID (line 310) | updateNodeID (nodeIndex, id) {
    method updateNodeCenter (line 327) | updateNodeCenter (nodeIndex, nodeCenter) {
    method getMatrix3FromMatrix4 (line 395) | function getMatrix3FromMatrix4(matrix3, matrix4) {
    method deactivate (line 518) | deactivate () {
    method activate (line 525) | activate () {
    method createMaterial (line 532) | static createMaterial(vertexShader, fragmentShader, customUniforms) {
    method createBaseMaterial (line 565) | static createBaseMaterial(customUniforms) {
    method createTexturedMaterial (line 569) | static createTexturedMaterial (customUniforms) {
    method MaxHeadVertices (line 575) | static get MaxHeadVertices () {
    method LocalOrientationTangent (line 580) | static get LocalOrientationTangent () {
    method LocalHeadOrigin (line 585) | static get LocalHeadOrigin () {
    method PositionComponentCount (line 589) | static get PositionComponentCount () {
    method UVComponentCount (line 593) | static get UVComponentCount () {
    method IndicesPerFace (line 597) | static get IndicesPerFace () {
    method FacesPerQuad (line 601) | static get FacesPerQuad () {
    method BaseVertexVars (line 607) | get BaseVertexVars() {
    method TexturedVertexVars (line 624) | get TexturedVertexVars() {
    method TexturedFragmentVars (line 637) | get TexturedFragmentVars() {
    method VertexShaderCore (line 644) | get VertexShaderCore() {
    method BaseVertexShader (line 652) | get BaseVertexShader() {
    method BaseFragmentShader (line 662) | get BaseFragmentShader() {
    method TexturedVertexShader (line 671) | get TexturedVertexShader() {
    method TexturedFragmentShader (line 691) | get TexturedFragmentShader() {
Condensed preview — 6 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (78K chars).
[
  {
    "path": ".gitignore",
    "chars": 14,
    "preview": "node_modules/\n"
  },
  {
    "path": "README.md",
    "chars": 2170,
    "preview": "# Three.js Trail Renderer\n\nBasic trail renderer for Three.js. This library allows for the straight-forward attachment of"
  },
  {
    "path": "index.html",
    "chars": 1401,
    "preview": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\""
  },
  {
    "path": "js/Main.js",
    "chars": 12839,
    "preview": "import * as THREE from 'three';\nimport { OrbitControls } from './OrbitControls.js';\nimport { TrailRenderer } from './Tra"
  },
  {
    "path": "js/OrbitControls.js",
    "chars": 28753,
    "preview": "import {\n\tEventDispatcher,\n\tMOUSE,\n\tQuaternion,\n\tSpherical,\n\tTOUCH,\n\tVector2,\n\tVector3,\n\tPlane,\n\tRay,\n\tMathUtils\n} from "
  },
  {
    "path": "js/TrailRenderer.js",
    "chars": 26611,
    "preview": "/**\n* @author Mark Kellogg - http://www.github.com/mkkellogg\n*/\nimport * as THREE from 'three';\n\n//====================="
  }
]

About this extraction

This page contains the full source code of the mkkellogg/TrailRendererJS GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 6 files (70.1 KB), approximately 17.3k tokens, and a symbol index with 75 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!