Skip to content

A live, networked Three.js visualisation of device identity and diversity.

Notifications You must be signed in to change notification settings

jhancock532/fingerprint-garden

Repository files navigation

Fingerprint Garden

Run this project with npm start.

Device Specific Bugs!

  • Doesn't load on Google Pixel 5 (two reports)
  • Doesn't load on Daniels's PC (Windows computer, none of the browsers work... works on other Windows computers, all browsers???) (one report)
  • The flowers can't be clicked when using the Brave browser? According to Sam (one report)
  • Doesn't load on Sue's Mac when she uses Chrome (but on Safari it works fine) (one report)

Sorry for my limited testing capability - this is a time constrained project and I don't have access to many other devices for debugging.

Code Highlights!

Canvas Resizing Best Practices

This is the best resizing code I've found for fullscreen Three.js canvas.

function resizeRendererToDisplaySize( renderer ) {

	// Resizing code taken from
	// https://threejsfundamentals.org/threejs/lessons/threejs-responsive.html
	// the best solution I've found

	const canvas = renderer.domElement;
	const width = canvas.clientWidth;
	const height = canvas.clientHeight;

	const needResize = canvas.width !== width || canvas.height !== height;

	if ( needResize ) {

	   renderer.setSize( width, height, false );

	}

	return needResize;

}

Then chuck this in the animation loop:

if ( resizeRendererToDisplaySize( renderer ) ) {

  const canvas = renderer.domElement;
  camera.aspect = canvas.clientWidth / canvas.clientHeight;
  camera.updateProjectionMatrix();

}

The "You Should Learn React" Realisation

I didn't realise how much I wanted JS to interact with the DOM until it was too late to turn back. Hence, these decelerations near the top of script.js:

const loadingSplashElement = document.getElementById( "loading-splash" );
const aboutButtonElement = document.getElementById( "about-button" );
const aboutInformationElement = document.getElementById( "about-information" );
const informationOverlayElement = document.getElementById( "information-overlay" );
const goBackGradientElement = document.getElementById( "go-back-gradient" );

const sideWindow = document.getElementById( "side-window" );
const sideWindowTitle = document.getElementById( "side-window-title" );
const sideWindowHash = document.getElementById( "side-window-hash" );
const sideWindowStatus = document.getElementById( "side-window-status" );
const sideWindowDate = document.getElementById( "side-window-date" );
const sideWindowLastConnected = document.getElementById( "side-window-last-connected" );
const sideWindowToggleButton = document.getElementById( "side-window-toggle-button" );

Then, when I need to update the DOM, you see a lot of code like this...

if ( connectionStatus == "LIVE" ) {

  sideWindowStatus.innerText = "LIVE CONNECTION";
  sideWindowStatus.classList.add( "live-dot" );
  sideWindowTitle.innerText = "NETWORKED PARTICIPANT";
  sideWindowDate.innerText = "";
  sideWindowLastConnected.style.display = "none";

  if ( sceneManager.visitorParticipant.hash == hash ) {

    sideWindowTitle.innerText = "YOUR FINGERPRINT";

  }

} else {

  sideWindowTitle.innerText = "PREVIOUS PARTICIPANT";
  sideWindowLastConnected.style.display = "block";

  sideWindowStatus.innerText = "OFFLINE GHOST";
  sideWindowStatus.classList.remove( "live-dot" );

  const connectionDate = new Date( connectionStatus );
  const dateOutput = getDateString( connectionDate );
  const timeOutput = getTimeString( connectionDate );

  sideWindowLastConnected.style.display = "block";
  sideWindowDate.innerHTML = dateOutput + "<br>" + timeOutput;

}

At least it does what I want it to! I like the atomic, squential control of elements, but it's not a great idea at scale. The paragraphs of updates start getting out of hand.

Many Lessons in JavaScript Classes

So much so I dedicated 1568 lines of code to them in my Fingerprint.js file, which is a mini game engine at this point.

class Participant { ... }
class Ghost { ... }
class Garden { ... }
class CameraTower { ... }
class Seat { ... }
class ObserverCamera { ... }
class ItemViewCamera { ... }
class Manager { ... }

The Manager calls all the shots, the rest of the classes are my way of reducing this projects scale into brain sized pieces.

Not a huge fan of the class names, some idiosyncrasies are creeping in here...

Spooky Scary Duplicating Character Model Bugs

A nice little ditty for future reference.

let mesh = SkeletonUtils.clone( this.modelMeshes[ meshNumber ] );
//To duplicate a mesh with bones you can't just .clone() - remember Skeleton!
//https://discourse.threejs.org/t/loading-a-gltf-model-twice-inside-the-loader-load/8373/2

Multiple Canvas Elements in Three.js aren't Performant

So take a single render snapshot instead of rendering to two canvases continuously. Added in ItemViewCamera.

	/*
	A nice little note from Mugen87 on solving multiple canvases with one renderer
	https://discourse.threejs.org/t/multiple-renderer-vs-multiple-canvas/3085/2
	*/
  
 	snapshot() {

		this.renderer.setSize( this.rtWidth, this.rtHeight, false );
		this.renderer.render( this.scene, this.camera );
		this.canvasContext.drawImage( this.renderer.domElement, 0, 0, this.rtWidth, this.rtHeight );

	}

Finally, the Model Animation System...

As found in Participant. I am quite proud of this because it took a fair bit of trial, error and documentation combing, but the end result was just a few lines of code.

	fadeToAnimationClip( animationClip, duration ) {

		//Based on the similar function in the Three.js example,
		//https://github.com/mrdoob/three.js/blob/master/examples/webgl_animation_multiple.html

		this.previousAction = this.activeAction;
		this.activeAction = this.mixer.existingAction( animationClip );

		if ( this.previousAction !== this.activeAction ) {

			this.previousAction.fadeOut( duration );

		}

		this.activeAction
			.reset()
			.setEffectiveTimeScale( 1 )
			.setEffectiveWeight( 1 )
			.fadeIn( duration )
			.play();

	}

Very smooth. I love it. On the other hand, getting my characters to move about and turn around was a bit more complicated. Tween.js to the rescue! Also, quaternions. I love the quaternions used here, also the halt hack to pause the sitting animation.

	sitDown( seat ) {

		this.movePosition( seat.sittingPosition, () => {

			const quaternionTime = { t: 0 };
			const startQuaternion = this.model.quaternion.clone();
			const endQuaternion = new THREE.Quaternion();
			endQuaternion.setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), seat.rotation );

			new TWEEN.Tween( quaternionTime )
				.to( { t: 1 }, 1000 )
				.onUpdate( () => {

					THREE.Quaternion.slerp( startQuaternion, endQuaternion, this.model.quaternion, quaternionTime.t );

				} )
				.easing( TWEEN.Easing.Quadratic.InOut )
				.onComplete( () => {

					this.fadeToAnimationClip( this.modelAnimations[ 0 ][ 3 ], 0.5 );

					this.activeAction.halt( 8.0 );
					//halt gradually slows animation to a stop, preventing sitting down animation looping.

					this.moving = false;
					this.sitting = true;

				} ).start();

		} );

	}

Having this.moving and this.sitting in the same class is wild, I know.

The movePosition( position, onFinish ) function is another great example of Tween.js, quaternions and lots of nested callbacks. Using the anonymous function () => {} within each callback gave me access to all the contents of the parent function, super useful!

About

A live, networked Three.js visualisation of device identity and diversity.

Topics

Resources

Stars

Watchers

Forks