<div id="world"></div>
<div id="instructions">Move your mouse <br/>to change speed and direction</div>
<div id="credits">
<p> <a href="https://codepen.io/Yakudoo/" target="blank">my other codepens</a> | <a href="https://epic.net" target="blank">www.epic.net</a></p>
</div>
@import url(https://fonts.googleapis.com/css?family=Open+Sans:800);
#world{
background: linear-gradient(#8ee4ae, #e9eba3);
position:absolute;
width:100%;
height:100%;
overflow:hidden;
}
#instructions{
position:absolute;
width:100%;
top:50%;
margin: auto;
margin-top:80px;
font-family:'Open Sans', sans-serif;
color:#71b583;
font-size:1.2em;
text-transform: uppercase;
text-align : center;
}
#credits{
position:absolute;
width:100%;
margin: auto;
bottom:0;
margin-bottom:20px;
font-family:'Open Sans', sans-serif;
color:#71b583;
font-size:0.7em;
text-transform: uppercase;
text-align : center;
}
#credits a {
color:#71b583;
}
//THREEJS RELATED VARIABLES
var scene,
camera,
fieldOfView,
aspectRatio,
nearPlane,
farPlane,
shadowLight,
light,
renderer,
container;
//SCREEN VARIABLES
var HEIGHT,
WIDTH,
windowHalfX,
windowHalfY,
xLimit,
yLimit;
// FISH BODY PARTS
var fish,
bodyFish,
tailFish,
topFish,
rightIris,
leftIris,
rightEye,
leftEye,
lipsFish,
tooth1,
tooth2,
tooth3,
tooth4,
tooth5;
// FISH SPEED
// the colors are splitted into rgb values to facilitate the transition of the color
var fishFastColor = {r:255, g:0, b:224}; // pastel blue
fishSlowColor = {r:0, g:207, b:255}; // purple
angleFin = 0; // angle used to move the fishtail
// PARTICLES COLORS
// array used to store a color scheme to randomly tint the particles
var colors = ['#dff69e',
'#00ceff',
'#002bca',
'#ff00e0',
'#3f159f',
'#71b583',
'#00a2ff'];
// PARTICLES
// as the particles are recycled, I use 2 arrays to store them
// flyingParticles used to update the flying particles and waitingParticles used to store the "unused" particles until we need them;
var flyingParticles = [];
waitingParticles = [];
// maximum z position for a particle
maxParticlesZ = 600;
// SPEED
var speed = {x:0, y:0};
var smoothing = 10;
// MISC
var mousePos = {x:0, y:0};
var stats;
var halfPI = Math.PI/2;
function init(){
// To work with THREEJS, you need a scene, a camera, and a renderer
// create the scene;
scene = new THREE.Scene();
// create the camera
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
aspectRatio = WIDTH / HEIGHT;
fieldOfView = 60;
nearPlane = 1; // the camera won't "see" any object placed in front of this plane
farPlane = 2000; // the camera wont't see any object placed further than this plane
camera = new THREE.PerspectiveCamera(
fieldOfView,
aspectRatio,
nearPlane,
farPlane);
camera.position.z = 1000;
//create the renderer
renderer = new THREE.WebGLRenderer({alpha: true, antialias: true });
renderer.setSize(WIDTH, HEIGHT);
container = document.getElementById('world');
container.appendChild(renderer.domElement);
/*
As I will recycle the particles, I need to know the left and right limits they can fly without disappearing from the camera field of view.
As soon as a particle is out of the camera view, I can recycle it : remove it from the flyingParticles array and push it back in the waitingParticles array.
I guess I can do that by raycasting each particle each frame, but I think this will be too heavy. Instead I prefer to precalculate the x coordinate from which a particle is not visible anymore. But this depends on the z position of the particle.
Here I decided to use the furthest possible z position for a particle, to be sure that all the particles won't be recycled before they are out of the camera view. But this could be much more precise, by precalculating the x limit for each particle depending on its z position and store it in the particle when it is "fired". But today, I'll keep it simple :)
!!!!!! I'm really not sure this is the best way to do it. If you find a better solution, please tell me
*/
// convert the field of view to radians
var ang = (fieldOfView/2)* Math.PI / 180;
// calculate the max y position seen by the camera related to the maxParticlesZ position, I start by calculating the y limit because fielOfView is a vertical field of view. I then calculate the x Limit
yLimit = (camera.position.z + maxParticlesZ) * Math.tan(ang); // this is a formula I found, don't ask me why it works, it just does :)
// Calculate the max x position seen by the camera related to the y Limit position
xLimit = yLimit *camera.aspect;
// precalculate the center of the screen, used to update the speed depending on the mouse position
windowHalfX = WIDTH / 2;
windowHalfY = HEIGHT / 2;
// handling resize and mouse move events
window.addEventListener('resize', onWindowResize, false);
document.addEventListener('mousemove', handleMouseMove, false);
// let's make it work on mobile too
document.addEventListener('touchstart', handleTouchStart, false);
document.addEventListener('touchend', handleTouchEnd, false);
document.addEventListener('touchmove',handleTouchMove, false);
}
function onWindowResize() {
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
windowHalfX = WIDTH / 2;
windowHalfY = HEIGHT / 2;
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix(); // force the camera to update its aspect ratio
// recalculate the limits
var ang = (fieldOfView/2)* Math.PI / 180;
yLimit = (camera.position.z + maxPartcilesZ) * Math.tan(ang);
xLimit = yLimit *camera.aspect;
}
function handleMouseMove(event) {
mousePos = {x:event.clientX, y:event.clientY};
updateSpeed()
}
function handleTouchStart(event) {
if (event.touches.length > 1) {
event.preventDefault();
mousePos = {x:event.touches[0].pageX, y:event.touches[0].pageY};
updateSpeed();
}
}
function handleTouchEnd(event) {
mousePos = {x:windowHalfX, y:windowHalfY};
updateSpeed();
}
function handleTouchMove(event) {
if (event.touches.length == 1) {
event.preventDefault();
mousePos = {x:event.touches[0].pageX, y:event.touches[0].pageY};
updateSpeed();
}
}
function updateSpeed(){
speed.x = (mousePos.x / WIDTH)*100;
speed.y = (mousePos.y-windowHalfY) / 10;
}
function loop() {
// Update fish position, rotation, scale... depending on the mouse position
// To make a smooth update of each value I use this formula :
// currentValue += (targetValue - currentValue) / smoothing
// make the fish swing according to the mouse direction
fish.rotation.z += ((-speed.y/50)-fish.rotation.z)/smoothing;
fish.rotation.x += ((-speed.y/50)-fish.rotation.x)/smoothing;
fish.rotation.y += ((-speed.y/50)-fish.rotation.y)/smoothing;
// make the fish move according to the mouse direction
fish.position.x += (((mousePos.x - windowHalfX)) - fish.position.x) / smoothing;
fish.position.y += ((-speed.y*10)-fish.position.y)/smoothing;
// make the eyes follow the mouse direction
rightEye.rotation.z = leftEye.rotation.z = -speed.y/150;
rightIris.position.x = leftIris.position.y = -10 - speed.y/2;
// make it look angry when the speed increases by narrowing the eyes
rightEye.scale.set(1,1-(speed.x/150),1);
leftEye.scale.set(1,1-(speed.x/150),1);
// in order to optimize, I precalculate a smaller speed values depending on speed.x
// these variables will be used to update the wagging of the tail, the color of the fish and the scale of the fish
var s2 = speed.x/100; // used for the wagging speed and color
var s3 = speed.x/300; // used for the scale
// I use an angle that I increment, and then use its cosine and sine to make the tail wag in a cyclic movement. The speed of the wagging depends on the global speed
angleFin += s2;
// for a better optimization, precalculate sine and cosines
var backTailCycle = Math.cos(angleFin);
var sideFinsCycle = Math.sin(angleFin/5);
tailFish.rotation.y = backTailCycle*.5;
topFish.rotation.x = sideFinsCycle*.5;
sideRightFish.rotation.x = halfPI + sideFinsCycle*.2;
sideLeftFish.rotation.x = halfPI + sideFinsCycle*.2;
// color update depending on the speed
var rvalue = (fishSlowColor.r + (fishFastColor.r - fishSlowColor.r)*s2)/255;
var gvalue = (fishSlowColor.g + (fishFastColor.g - fishSlowColor.g)*s2)/255;
var bvalue = (fishSlowColor.b + (fishFastColor.b - fishSlowColor.b)*s2)/255;
bodyFish.material.color.setRGB(rvalue,gvalue,bvalue);
lipsFish.material.color.setRGB(rvalue,gvalue,bvalue);
//scale update depending on the speed => make the fish struggling to progress
fish.scale.set(1+s3,1-s3,1-s3);
// particles update
for (var i=0; i<flyingParticles.length; i++){
var particle = flyingParticles[i];
particle.rotation.y += (1/particle.scale.x) *.05;
particle.rotation.x += (1/particle.scale.x) *.05;
particle.rotation.z += (1/particle.scale.x) *.05;
particle.position.x += -10 -(1/particle.scale.x) * speed.x *.2;
particle.position.y += (1/particle.scale.x) * speed.y *.2;
if (particle.position.x < -xLimit - 80){ // check if the particle is out of the field of view
scene.remove(particle);
waitingParticles.push(flyingParticles.splice(i,1)[0]); // recycle the particle
i--;
}
}
renderer.render(scene, camera);
stats.update();
requestAnimationFrame(loop);
}
function createStats() {
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
stats.domElement.style.right = '0px';
container.appendChild(stats.domElement);
}
// Lights
// I use 2 lights, an hemisphere to give a global ambient light
// And a harder light to add some shadows
function createLight() {
light = new THREE.HemisphereLight(0xffffff, 0xffffff, .3)
scene.add(light);
shadowLight = new THREE.DirectionalLight(0xffffff, .8);
shadowLight.position.set(1, 1, 1);
scene.add(shadowLight);
}
function createFish(){
// A group that will contain each part of the fish
fish = new THREE.Group();
// each part needs a geometry, a material, and a mesh
// Body
var bodyGeom = new THREE.BoxGeometry(120, 120, 120);
var bodyMat = new THREE.MeshLambertMaterial({
color: 0x80f5fe ,
shading: THREE.FlatShading
});
bodyFish = new THREE.Mesh(bodyGeom, bodyMat);
// Tail
var tailGeom = new THREE.CylinderGeometry(0, 60, 60, 4, false);
var tailMat = new THREE.MeshLambertMaterial({
color: 0xff00dc,
shading: THREE.FlatShading
});
tailFish = new THREE.Mesh(tailGeom, tailMat);
tailFish.scale.set(.8,1,.1);
tailFish.position.x = -60;
tailFish.rotation.z = -halfPI;
// Lips
var lipsGeom = new THREE.BoxGeometry(25, 10, 120);
var lipsMat = new THREE.MeshLambertMaterial({
color: 0x80f5fe ,
shading: THREE.FlatShading
});
lipsFish = new THREE.Mesh(lipsGeom, lipsMat);
lipsFish.position.x = 65;
lipsFish.position.y = -47;
lipsFish.rotation.z = halfPI;
// Fins
topFish = new THREE.Mesh(tailGeom, tailMat);
topFish.scale.set(.8,1,.1);
topFish.position.x = -20;
topFish.position.y = 60;
topFish.rotation.z = -halfPI;
sideRightFish = new THREE.Mesh(tailGeom, tailMat);
sideRightFish.scale.set(.8,1,.1);
sideRightFish.rotation.x = halfPI;
sideRightFish.rotation.z = -halfPI;
sideRightFish.position.x = 0;
sideRightFish.position.y = -50;
sideRightFish.position.z = -60;
sideLeftFish = new THREE.Mesh(tailGeom, tailMat);
sideLeftFish.scale.set(.8,1,.1);
sideLeftFish.rotation.x = halfPI;
sideLeftFish.rotation.z = -halfPI;
sideLeftFish.position.x = 0;
sideLeftFish.position.y = -50;
sideLeftFish.position.z = 60;
// Eyes
var eyeGeom = new THREE.BoxGeometry(40, 40,5);
var eyeMat = new THREE.MeshLambertMaterial({
color: 0xffffff,
shading: THREE.FlatShading
});
rightEye = new THREE.Mesh(eyeGeom,eyeMat );
rightEye.position.z = -60;
rightEye.position.x = 25;
rightEye.position.y = -10;
var irisGeom = new THREE.BoxGeometry(10, 10,3);
var irisMat = new THREE.MeshLambertMaterial({
color: 0x330000,
shading: THREE.FlatShading
});
rightIris = new THREE.Mesh(irisGeom,irisMat );
rightIris.position.z = -65;
rightIris.position.x = 35;
rightIris.position.y = -10;
leftEye = new THREE.Mesh(eyeGeom,eyeMat );
leftEye.position.z = 60;
leftEye.position.x = 25;
leftEye.position.y = -10;
leftIris = new THREE.Mesh(irisGeom,irisMat );
leftIris.position.z = 65;
leftIris.position.x = 35;
leftIris.position.y = -10;
var toothGeom = new THREE.BoxGeometry(20, 4, 20);
var toothMat = new THREE.MeshLambertMaterial({
color: 0xffffff,
shading: THREE.FlatShading
});
// Teeth
tooth1 = new THREE.Mesh(toothGeom,toothMat);
tooth1.position.x = 65;
tooth1.position.y = -35;
tooth1.position.z = -50;
tooth1.rotation.z = halfPI;
tooth1.rotation.x = -halfPI;
tooth2 = new THREE.Mesh(toothGeom,toothMat);
tooth2.position.x = 65;
tooth2.position.y = -30;
tooth2.position.z = -25;
tooth2.rotation.z = halfPI;
tooth2.rotation.x = -Math.PI/12;
tooth3 = new THREE.Mesh(toothGeom,toothMat);
tooth3.position.x = 65;
tooth3.position.y = -25;
tooth3.position.z = 0;
tooth3.rotation.z = halfPI;
tooth4 = new THREE.Mesh(toothGeom,toothMat);
tooth4.position.x = 65;
tooth4.position.y = -30;
tooth4.position.z = 25;
tooth4.rotation.z = halfPI;
tooth4.rotation.x = Math.PI/12;
tooth5 = new THREE.Mesh(toothGeom,toothMat);
tooth5.position.x = 65;
tooth5.position.y = -35;
tooth5.position.z = 50;
tooth5.rotation.z = halfPI;
tooth5.rotation.x = Math.PI/8;
fish.add(bodyFish);
fish.add(tailFish);
fish.add(topFish);
fish.add(sideRightFish);
fish.add(sideLeftFish);
fish.add(rightEye);
fish.add(rightIris);
fish.add(leftEye);
fish.add(leftIris);
fish.add(tooth1);
fish.add(tooth2);
fish.add(tooth3);
fish.add(tooth4);
fish.add(tooth5);
fish.add(lipsFish);
fish.rotation.y = -Math.PI/4;
scene.add(fish);
}
// PARTICLES
function createParticle(){
var particle, geometryCore, ray, w,h,d, sh, sv;
// 3 different shapes are used, chosen randomly
var rnd = Math.random();
// BOX
if (rnd<.33){
w = 10 + Math.random()*30;
h = 10 + Math.random()*30;
d = 10 + Math.random()*30;
geometryCore = new THREE.BoxGeometry(w,h,d);
}
// TETRAHEDRON
else if (rnd<.66){
ray = 10 + Math.random()*20;
geometryCore = new THREE.TetrahedronGeometry(ray);
}
// SPHERE... but as I also randomly choose the number of horizontal and vertical segments, it sometimes lead to wierd shapes
else{
ray = 5+Math.random()*30;
sh = 2 + Math.floor(Math.random()*2);
sv = 2 + Math.floor(Math.random()*2);
geometryCore = new THREE.SphereGeometry(ray, sh, sv);
}
// Choose a color for each particle and create the mesh
var materialCore = new THREE.MeshLambertMaterial({
color: getRandomColor(),
shading: THREE.FlatShading
});
particle = new THREE.Mesh(geometryCore, materialCore);
return particle;
}
// depending if there is particles stored in the waintingParticles array, get one from there or create a new one
function getParticle(){
if (waitingParticles.length) {
return waitingParticles.pop();
}else{
return createParticle();
}
}
function flyParticle(){
var particle = getParticle();
// set the particle position randomly but keep it out of the field of view, and give it a random scale
particle.position.x = xLimit;
particle.position.y = -yLimit + Math.random()*yLimit*2;
particle.position.z = Math.random()*maxParticlesZ;
var s = .1 + Math.random();
particle.scale.set(s,s,s);
flyingParticles.push(particle);
scene.add(particle);
}
function getRandomColor(){
var col = hexToRgb(colors[Math.floor(Math.random()*colors.length)]);
var threecol = new THREE.Color("rgb("+col.r+","+col.g+","+col.b+")");
return threecol;
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
init();
createStats();
createLight();
createFish();
createParticle();
loop();
setInterval(flyParticle, 70); // launch a new particle every 70ms
This Pen doesn't use any external CSS resources.