Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r120/three.min.js"></script> 

<div id="scene-overlay">
  <p>Loading scene...✨</p>
</div>
<div id="scene-container">
</div>
<div id="thankyou">
  <p id="date">'20  09  23</p>
</div>
<button class="btn" id="btn-celebrate">
 Let's<br> Celebrate!
</button>
<a class="fullscreen wiggle" href="https://codepen.io/ScavengerFrontend/full/GRZzdza" target="_blank">Go <br> Full Screen!</a>

<script>
  
  function addCurrentDate() {
  
  const date = new Date();
  const year = (date.getFullYear()+'').slice(-2); 
  const month = ('0' + (date.getMonth()+1)).slice(-2);
  const day = ('0' + date.getDate()).slice(-2);
  const dateDiv = document.querySelector("#date");
    
  dateDiv.innerHTML = "'" 
    + year
    + "<span> " + month + "</span>"
    + "<span> " + day + "</span>";
  
}

</script>


<script type="x-shader/x-vertex" id="matcap-vs">

  varying vec2 vN;

	void main() {

		vec3 e = normalize(vec3(modelViewMatrix * vec4(position, 1.0)));
		vec3 n = normalize(normalMatrix * normal);

		vec3 r = reflect(e, n);
		float m = 2.0 * sqrt(pow(r.x, 2.0) + pow( r.y, 2.0) + pow(r.z + 1.0, 2.0));
		vN = r.xy / m + 0.5;

		gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

	}

</script>

<script type="x-shader/x-fragment" id="matcap-fs">

  uniform sampler2D tMatCap;

	varying vec2 vN;

	void main() {
		
		vec3 base = texture2D(tMatCap, vN).rgb;
		gl_FragColor = vec4(base, 1.0);

	}
	
</script>


<script type="x-shader/x-vertex" id="vs-iMatcap">
  
  #define PI 3.14159265359
  varying vec2 vN;
  attribute vec3 aPosition;

	void main() {

		vec3 e = normalize( vec3( modelViewMatrix * vec4( position, 1.0 ) ) );
		vec3 n = normalize( normalMatrix * normal );

		vec3 r = reflect( e, n );
		float m = 2. * sqrt( pow( r.x, 2. ) + pow( r.y, 2. ) + pow( r.z + 1., 2. ) );
		vN = r.xy / m + .5;
    
    vec3 transformed = position.xyz + aPosition.xyz;

		gl_Position = projectionMatrix * modelViewMatrix * vec4( transformed, 1. );

	}

</script>

<script type="x-shader/x-fragment" id="fs-iMatcap">

  uniform sampler2D tMatCap;

	varying vec2 vN;

	void main() {
		
		vec3 base = texture2D( tMatCap, vN ).rgb;
		gl_FragColor = vec4( base, 1.0 );

	}
	
</script>



<script>

/////////////////////////////
// buffer animation system - Szenia Zadvornykh
/////////////////////////////

THREE.BAS = {};

THREE.BAS.ShaderChunk = {};

THREE.BAS.ShaderChunk["animation_time"] = "float tDelay = aAnimation.x;\nfloat tDuration = aAnimation.y;\nfloat tTime = clamp(uTime - tDelay, 0.0, tDuration);\nfloat tProgress = ease(tTime, 0.0, 1.0, tDuration);\n";

THREE.BAS.ShaderChunk["cubic_bezier"] = "vec3 cubicBezier(vec3 p0, vec3 c0, vec3 c1, vec3 p1, float t)\n{\n    vec3 tp;\n    float tn = 1.0 - t;\n\n    tp.xyz = tn * tn * tn * p0.xyz + 3.0 * tn * tn * t * c0.xyz + 3.0 * tn * t * t * c1.xyz + t * t * t * p1.xyz;\n\n    return tp;\n}\n";

THREE.BAS.ShaderChunk["ease_in_cubic"] = "float ease(float t, float b, float c, float d) {\n  return c*(t/=d)*t*t + b;\n}\n";

THREE.BAS.ShaderChunk["ease_in_quad"] = "float ease(float t, float b, float c, float d) {\n  return c*(t/=d)*t + b;\n}\n";

THREE.BAS.ShaderChunk["ease_out_cubic"] = "float ease(float t, float b, float c, float d) {\n  return c*((t=t/d - 1.0)*t*t + 1.0) + b;\n}\n";

THREE.BAS.ShaderChunk["quaternion_rotation"] = "vec3 rotateVector(vec4 q, vec3 v)\n{\n    return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);\n}\n\nvec4 quatFromAxisAngle(vec3 axis, float angle)\n{\n    float halfAngle = angle * 0.5;\n    return vec4(axis.xyz * sin(halfAngle), cos(halfAngle));\n}\n";


THREE.BAS.PrefabBufferGeometry = function (prefab, count) {
  THREE.BufferGeometry.call(this);

  this.prefabGeometry = prefab;
  this.prefabCount = count;
  this.prefabVertexCount = prefab.vertices.length;

  this.bufferDefaults();
};
THREE.BAS.PrefabBufferGeometry.prototype = Object.create(THREE.BufferGeometry.prototype);
THREE.BAS.PrefabBufferGeometry.prototype.constructor = THREE.BAS.PrefabBufferGeometry;

THREE.BAS.PrefabBufferGeometry.prototype.bufferDefaults = function () {
  var prefabFaceCount = this.prefabGeometry.faces.length;
  var prefabIndexCount = this.prefabGeometry.faces.length * 3;
  var prefabVertexCount = this.prefabVertexCount = this.prefabGeometry.vertices.length;
  var prefabIndices = [];

  //console.log('prefabCount', this.prefabCount);
  //console.log('prefabFaceCount', prefabFaceCount);
  //console.log('prefabIndexCount', prefabIndexCount);
  //console.log('prefabVertexCount', prefabVertexCount);
  //console.log('triangles', prefabFaceCount * this.prefabCount);

  for (var h = 0; h < prefabFaceCount; h++) {
    var face = this.prefabGeometry.faces[h];
    prefabIndices.push(face.a, face.b, face.c);
  }

  var indexBuffer = new Uint32Array(this.prefabCount * prefabIndexCount);
  var positionBuffer = new Float32Array(this.prefabCount * prefabVertexCount * 3);

  this.setIndex(new THREE.BufferAttribute(indexBuffer, 1));
  this.setAttribute('position', new THREE.BufferAttribute(positionBuffer, 3));

  for (var i = 0, offset = 0; i < this.prefabCount; i++) {
    for (var j = 0; j < prefabVertexCount; j++, offset += 3) {
      var prefabVertex = this.prefabGeometry.vertices[j];

      positionBuffer[offset    ] = prefabVertex.x;
      positionBuffer[offset + 1] = prefabVertex.y;
      positionBuffer[offset + 2] = prefabVertex.z;
    }

    for (var k = 0; k < prefabIndexCount; k++) {
      indexBuffer[i * prefabIndexCount + k] = prefabIndices[k] + i * prefabVertexCount;
    }
  }
};

// todo test
THREE.BAS.PrefabBufferGeometry.prototype.bufferUvs = function() {
  var prefabFaceCount = this.prefabGeometry.faces.length;
  var prefabVertexCount = this.prefabVertexCount = this.prefabGeometry.vertices.length;
  var prefabUvs = [];

  for (var h = 0; h < prefabFaceCount; h++) {
    var face = this.prefabGeometry.faces[h];
    var uv = this.prefabGeometry.faceVertexUvs[0][h];

    prefabUvs[face.a] = uv[0];
    prefabUvs[face.b] = uv[1];
    prefabUvs[face.c] = uv[2];
  }

  var uvBuffer = this.createAttribute('uv', 2);

  for (var i = 0, offset = 0; i < this.prefabCount; i++) {
    for (var j = 0; j < prefabVertexCount; j++, offset += 2) {
      var prefabUv = prefabUvs[j];

      uvBuffer.array[offset] = prefabUv.x;
      uvBuffer.array[offset + 1] = prefabUv.y;
    }
  }
};

/**
 * based on BufferGeometry.computeVertexNormals
 * calculate vertex normals for a prefab, and repeat the data in the normal buffer
 */
THREE.BAS.PrefabBufferGeometry.prototype.computeVertexNormals = function () {
  var index = this.index;
  var attributes = this.attributes;
  var positions = attributes.position.array;

  if (attributes.normal === undefined) {
    this.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(positions.length), 3));
  }

  var normals = attributes.normal.array;

  var vA, vB, vC,

  pA = new THREE.Vector3(),
  pB = new THREE.Vector3(),
  pC = new THREE.Vector3(),

  cb = new THREE.Vector3(),
  ab = new THREE.Vector3();

  var indices = index.array;
  var prefabIndexCount = this.prefabGeometry.faces.length * 3;

  for (var i = 0; i < prefabIndexCount; i += 3) {
    vA = indices[i + 0] * 3;
    vB = indices[i + 1] * 3;
    vC = indices[i + 2] * 3;

    pA.fromArray(positions, vA);
    pB.fromArray(positions, vB);
    pC.fromArray(positions, vC);

    cb.subVectors(pC, pB);
    ab.subVectors(pA, pB);
    cb.cross(ab);

    normals[vA] += cb.x;
    normals[vA + 1] += cb.y;
    normals[vA + 2] += cb.z;

    normals[vB] += cb.x;
    normals[vB + 1] += cb.y;
    normals[vB + 2] += cb.z;

    normals[vC] += cb.x;
    normals[vC + 1] += cb.y;
    normals[vC + 2] += cb.z;
  }

  for (var j = 1; j < this.prefabCount; j++) {
    for (var k = 0; k < prefabIndexCount; k++) {
      normals[j * prefabIndexCount + k] = normals[k];
    }
  }

  this.normalizeNormals();

  attributes.normal.needsUpdate = true;
};

THREE.BAS.PrefabBufferGeometry.prototype.createAttribute = function (name, itemSize) {
  var buffer = new Float32Array(this.prefabCount * this.prefabVertexCount * itemSize);
  var attribute = new THREE.BufferAttribute(buffer, itemSize);

  this.setAttribute(name, attribute);

  return attribute;
};

THREE.BAS.PrefabBufferGeometry.prototype.setAttribute4 = function (name, data) {
  var offset = 0;
  var array = this.geometry.attributes[name].array;
  var i, j;

  for (i = 0; i < data.length; i++) {
    var v = data[i];

    for (j = 0; j < this.prefabVertexCount; j++) {
      array[offset++] = v.x;
      array[offset++] = v.y;
      array[offset++] = v.z;
      array[offset++] = v.w;
    }
  }

  this.geometry.attributes[name].needsUpdate = true;
};
THREE.BAS.PrefabBufferGeometry.prototype.setAttribute3 = function (name, data) {
  var offset = 0;
  var array = this.geometry.attributes[name].array;
  var i, j;

  for (i = 0; i < data.length; i++) {
    var v = data[i];

    for (j = 0; j < this.prefabVertexCount; j++) {
      array[offset++] = v.x;
      array[offset++] = v.y;
      array[offset++] = v.z;
    }
  }

  this.geometry.attributes[name].needsUpdate = true;
};
THREE.BAS.PrefabBufferGeometry.prototype.setAttribute2 = function (name, data) {
  var offset = 0;
  var array = this.geometry.attributes[name].array;
  var i, j;

  for (i = 0; i < this.prefabCount; i++) {
    var v = data[i];

    for (j = 0; j < this.prefabVertexCount; j++) {
      array[offset++] = v.x;
      array[offset++] = v.y;
    }
  }

  this.geometry.attributes[name].needsUpdate = true;
};

THREE.BAS.BaseAnimationMaterial = function(parameters) {
    THREE.ShaderMaterial.call(this);

    this.shaderFunctions = [];
    this.shaderParameters = [];
    this.shaderVertexInit = [];
    this.shaderTransformNormal = [];
    this.shaderTransformPosition = [];

    this.setValues(parameters);
};
THREE.BAS.BaseAnimationMaterial.prototype = Object.create(THREE.ShaderMaterial.prototype);
THREE.BAS.BaseAnimationMaterial.prototype.constructor = THREE.BAS.BaseAnimationMaterial;

// abstract
THREE.BAS.BaseAnimationMaterial.prototype._concatVertexShader = function() {
    return '';
};

THREE.BAS.BaseAnimationMaterial.prototype._concatFunctions = function() {
    return this.shaderFunctions.join('\n');
};
THREE.BAS.BaseAnimationMaterial.prototype._concatParameters = function() {
    return this.shaderParameters.join('\n');
};
THREE.BAS.BaseAnimationMaterial.prototype._concatVertexInit = function() {
    return this.shaderVertexInit.join('\n');
};
THREE.BAS.BaseAnimationMaterial.prototype._concatTransformNormal = function() {
    return this.shaderTransformNormal.join('\n');
};
THREE.BAS.BaseAnimationMaterial.prototype._concatTransformPosition = function() {
    return this.shaderTransformPosition.join('\n');
};


THREE.BAS.BaseAnimationMaterial.prototype.setUniformValues = function(values) {
    for (var key in values) {
        if (key in this.uniforms) {
            var uniform = this.uniforms[key];
            var value = values[key];

            // todo add matrix uniform types
            switch (uniform.type) {
                case 'c': // color
                    uniform.value.set(value);
                    break;
                case 'v2': // vectors
                case 'v3':
                case 'v4':
                    uniform.value.copy(value);
                    break;
                case 'f': // float
                case 't': // texture
                    uniform.value = value;
            }
        }
    }
};

THREE.BAS.PhongAnimationMaterial = function(parameters, uniformValues) {
    THREE.BAS.BaseAnimationMaterial.call(this, parameters);

    var phongShader = THREE.ShaderLib['phong'];

    this.uniforms = THREE.UniformsUtils.merge([phongShader.uniforms, this.uniforms]);
    this.lights = true;
    this.vertexShader = this._concatVertexShader();
    this.fragmentShader = phongShader.fragmentShader;

    // todo add missing default defines
    uniformValues.map && (this.defines['USE_MAP'] = '');
    uniformValues.normalMap && (this.defines['USE_NORMALMAP'] = '');

    this.setUniformValues(uniformValues);
};
THREE.BAS.PhongAnimationMaterial.prototype = Object.create(THREE.BAS.BaseAnimationMaterial.prototype);
THREE.BAS.PhongAnimationMaterial.prototype.constructor = THREE.BAS.PhongAnimationMaterial;

THREE.BAS.PhongAnimationMaterial.prototype._concatVertexShader = function() {
    // based on THREE.ShaderLib.phong
    return [
        "#define PHONG",

        "varying vec3 vViewPosition;",

        "#ifndef FLAT_SHADED",

        "   varying vec3 vNormal;",

        "#endif",

        THREE.ShaderChunk[ "common" ],
        THREE.ShaderChunk[ "uv_pars_vertex" ],
        THREE.ShaderChunk[ "uv2_pars_vertex" ],
        THREE.ShaderChunk[ "displacementmap_pars_vertex" ],
        THREE.ShaderChunk[ "envmap_pars_vertex" ],
        THREE.ShaderChunk[ "lights_phong_pars_vertex" ],
        THREE.ShaderChunk[ "color_pars_vertex" ],
        THREE.ShaderChunk[ "morphtarget_pars_vertex" ],
        THREE.ShaderChunk[ "skinning_pars_vertex" ],
        THREE.ShaderChunk[ "shadowmap_pars_vertex" ],
        THREE.ShaderChunk[ "logdepthbuf_pars_vertex" ],

        this._concatFunctions(),

        this._concatParameters(),

        "void main() {",

        this._concatVertexInit(),

        THREE.ShaderChunk[ "uv_vertex" ],
        THREE.ShaderChunk[ "uv2_vertex" ],
        THREE.ShaderChunk[ "color_vertex" ],
        THREE.ShaderChunk[ "beginnormal_vertex" ],

        this._concatTransformNormal(),

        THREE.ShaderChunk[ "morphnormal_vertex" ],
        THREE.ShaderChunk[ "skinbase_vertex" ],
        THREE.ShaderChunk[ "skinnormal_vertex" ],
        THREE.ShaderChunk[ "defaultnormal_vertex" ],

        "#ifndef FLAT_SHADED", // Normal computed with derivatives when FLAT_SHADED

        "   vNormal = normalize( transformedNormal );",

        "#endif",

        THREE.ShaderChunk[ "begin_vertex" ],

        this._concatTransformPosition(),

        THREE.ShaderChunk[ "displacementmap_vertex" ],
        THREE.ShaderChunk[ "morphtarget_vertex" ],
        THREE.ShaderChunk[ "skinning_vertex" ],
        THREE.ShaderChunk[ "project_vertex" ],
        THREE.ShaderChunk[ "logdepthbuf_vertex" ],

        "   vViewPosition = - mvPosition.xyz;",

        THREE.ShaderChunk[ "worldpos_vertex" ],
        THREE.ShaderChunk[ "envmap_vertex" ],
        THREE.ShaderChunk[ "lights_phong_vertex" ],
        THREE.ShaderChunk[ "shadowmap_vertex" ],

        "}"

    ].join( "\n" );
};

</script>

<script>
  
  let mParticleCount = 2500;
  let mDuration = 5;
  let scene = new THREE.Scene();
  let mParticleSystem;
  
  function initParticleSystem() {
  
  let prefabGeometry = new THREE.PlaneGeometry(1.25, 1.25);
  let bufferGeometry = new THREE.BAS.PrefabBufferGeometry(prefabGeometry, mParticleCount);
  bufferGeometry.computeVertexNormals();

  // generate additional geometry data
  let aOffset = bufferGeometry.createAttribute('aOffset', 1);
  let aStartPosition = bufferGeometry.createAttribute('aStartPosition', 3);
  let aControlPoint1 = bufferGeometry.createAttribute('aControlPoint1', 3);
  let aControlPoint2 = bufferGeometry.createAttribute('aControlPoint2', 3);
  let aEndPosition = bufferGeometry.createAttribute('aEndPosition', 3);
  let aAxisAngle = bufferGeometry.createAttribute('aAxisAngle', 4);
  let aColor = bufferGeometry.createAttribute('color', 3);

  let i, j, offset;

  // buffer time offset
  let delay;

  for (let i = 0, offset = 0; i < mParticleCount; i++) {
    delay = i / mParticleCount * mDuration;

    for (let j = 0; j < prefabGeometry.vertices.length; j++) {
      aOffset.array[offset++] = delay;
    }
  }

  // buffer start positions
  let x, y, z;

  for (let i = 0, offset = 0; i < mParticleCount; i++) {
    x = 0;
    y = -2.5;
    z = 0;

    for (let j = 0; j < prefabGeometry.vertices.length; j++) {
      aStartPosition.array[offset++] = x;
      aStartPosition.array[offset++] = y;
      aStartPosition.array[offset++] = z;
    }
  }

  // buffer control points

  for (let i = 0, offset = 0; i < mParticleCount; i++) {
    // x = THREE.Math.randFloat(-400, -350);
    // y = THREE.Math.randFloat(100, 200);
    // z = THREE.Math.randFloat(-300, -100);
    
    x = THREE.Math.randFloat(-10, 10);
    y = THREE.Math.randFloat(25, 100);
    z = THREE.Math.randFloat(-20, 20);

    for (let j = 0; j < prefabGeometry.vertices.length; j++) {
      aControlPoint1.array[offset++] = x;
      aControlPoint1.array[offset++] = y;
      aControlPoint1.array[offset++] = z;
    }
  }

  for (let i = 0, offset = 0; i < mParticleCount; i++) {
    
    x = THREE.Math.randFloat(-100, 100);
    // x = THREE.Math.randFloat(-50, 50);
    // y = 40;
    y = THREE.Math.randFloat(100, 200);
    z = THREE.Math.randFloat(-50, 50);
    
//     let distance = 50;
//     let theta = THREE.Math.randFloatSpread(360); 
//     let phi = THREE.Math.randFloatSpread(360); 

//     x = distance * Math.sin(theta) * Math.cos(phi);
//     // y = THREE.Math.randFloat(-60, 60);
//     y = 30;
//     z = distance * Math.cos(theta);

    for (let j = 0; j < prefabGeometry.vertices.length; j++) {
      aControlPoint2.array[offset++] = x;
      aControlPoint2.array[offset++] = y;
      aControlPoint2.array[offset++] = z;
    }
  }

  // buffer end positions

  for (let i = 0, offset = 0; i < mParticleCount; i++) {
//     x = 150;
//     y = 180;
//     z = 100;
    let distance = 70;
    let theta = THREE.Math.randFloatSpread(360); 
    let phi = THREE.Math.randFloatSpread(360); 

    x = distance * Math.sin(theta) * Math.cos(phi);
    y = THREE.Math.randFloat(-80, -60);
    z = distance * Math.cos(theta);

    for (let j = 0; j < prefabGeometry.vertices.length; j++) {
      aEndPosition.array[offset++] = x;
      aEndPosition.array[offset++] = y;
      aEndPosition.array[offset++] = z;
    }
  }

  // buffer axis angle
  let axis = new THREE.Vector3();
  let angle = 0;

  for (let i = 0, offset = 0; i < mParticleCount; i++) {
    axis.x = THREE.Math.randFloatSpread(2);
    axis.y = THREE.Math.randFloatSpread(2);
    axis.z = THREE.Math.randFloatSpread(2);
    axis.normalize();

    angle = Math.PI * THREE.Math.randInt(16, 32);

    for (let j = 0; j < prefabGeometry.vertices.length; j++) {
      aAxisAngle.array[offset++] = axis.x;
      aAxisAngle.array[offset++] = axis.y;
      aAxisAngle.array[offset++] = axis.z;
      aAxisAngle.array[offset++] = angle;
    }
  }

  // buffer color
  let color = new THREE.Color();
  let h, s, l;

  for (let i = 0, offset = 0; i < mParticleCount; i++) {
    h = 0.4;
    s = THREE.Math.randFloat(0.0, 0.2);
    l = THREE.Math.randFloat(0.5, 0.99);

    color.setHSL(h, s, l);

    for (let j = 0; j < prefabGeometry.vertices.length; j++) {
      aColor.array[offset++] = color.r;
      aColor.array[offset++] = color.g;
      aColor.array[offset++] = color.b;
    }
  }


  const material = new THREE.BAS.PhongAnimationMaterial(
    // custom parameters & THREE.MeshPhongMaterial parameters
    {
      vertexColors: THREE.VertexColors,
      flatShading: true,
      side: THREE.DoubleSide,
      uniforms: {
        uTime: {type: 'f', value: 0},
        uDuration: {type: 'f', value: mDuration}
      },
      shaderFunctions: [
        THREE.BAS.ShaderChunk['quaternion_rotation'],
        THREE.BAS.ShaderChunk['cubic_bezier']
      ],
      shaderParameters: [
        'uniform float uTime;',
        'uniform float uDuration;',
        'attribute float aOffset;',
        'attribute vec3 aStartPosition;',
        'attribute vec3 aControlPoint1;',
        'attribute vec3 aControlPoint2;',
        'attribute vec3 aEndPosition;',
        'attribute vec4 aAxisAngle;'
      ],
      shaderVertexInit: [
        'float tProgress = mod((uTime + aOffset), uDuration) / uDuration;',

        'float angle = aAxisAngle.w * tProgress;',
        'vec4 tQuat = quatFromAxisAngle(aAxisAngle.xyz, angle);'
      ],
      shaderTransformNormal: [
        'objectNormal = rotateVector(tQuat, objectNormal);'
      ],
      shaderTransformPosition: [
        'transformed = rotateVector(tQuat, transformed);',
        'transformed += cubicBezier(aStartPosition, aControlPoint1, aControlPoint2, aEndPosition, tProgress);'
      ]
    },
    // THREE.MeshPhongMaterial uniforms
    {
      specular: 0xff00ff,
      emissive: 0xff00ff,
      shininess: 100.0
    }
  );

  mParticleSystem = new THREE.Mesh(bufferGeometry, material);
  // because the bounding box of the particle system does not reflect its on-screen size
  // set this to false to prevent the whole thing from disappearing on certain angles
  mParticleSystem.frustumCulled = false;

  scene.add(mParticleSystem);
  
}
</script>

<script>
  
function getFibonacciSpherePoints(samples, radius, randomize) {
  // Translated from Python from https://stackoverflow.com/a/26127012
  samples = samples || 1;
  radius = radius || 1;
  randomize = randomize || true;
  let random = 1;
  
  if (randomize === true) {
      random = Math.random() * samples;
  }

  let points = [];
  let offset = 2 / samples;
  let increment = Math.PI * (3 - Math.sqrt(5));

  for (let i = 0; i < samples; i++) {
      let y = ((i * offset) - 1) + (offset / 2);
      let distance = Math.sqrt(1 - Math.pow(y, 2));
      let phi = ((i + random) % samples) * increment;
      let x = Math.cos(phi) * distance;
      let z = Math.sin(phi) * distance;
      x = x * radius;
      y = y * radius;
      z = z * radius;
      let point = {
          'x': x,
          'y': y,
          'z': z
      }
      points.push(point);
  }
  return points;

}

</script>
              
            
!

CSS

              
                :root {
  --cedar: #7b5d5d;
  --coral: rgb(230, 146, 146);
  --fadedcoral: #e69292;
  --neoncoral: #ff7979;
  --lightgrey: #ecdfdf;
  --dolphin: rgb(184, 189, 189);
  --lavender: rgb(191, 171, 210);
  --teal: rgb(61, 88, 92);
}

@font-face {
  font-family: 'DS-Digital';
  src: url('https://assets.codepen.io/911157/subset-DS-Digital-Italic.woff') format("woff");
}

body {
  width: 100vw;
  height: 100vh;
  max-width: 100%;
  padding: 0;
  margin: 0;
  overflow: hidden;
  font-size: clamp(100%, 1rem + 2vw, 20px);
  font-family: Arial;
  color: var(--cedar);
  background: radial-gradient(circle, var(--coral) 0%, var(--dolphin) 100%);
  transition: 0.25s;
  touch-action: pan-x;
}

#scene-overlay {
  width: 100%;
  height: 100%;
  position: absolute;
  display: grid;
  place-items: center;
  opacity: 1.0;
  background: radial-gradient(circle, var(--coral) 0%, rgb(--dolphin) 100%);
}

#scene-container {
  width: 100%;
  height: 100%;
  position: absolute;
  opacity: 0.2;
}

#thankyou {
  position: absolute;
  bottom: 0;
  padding: 0;
  margin: 1em 2em;
  font-family: 'DS-Digital';
  display: block;
}

#thankyou > p {
  margin: 0;
  padding: 0;
  word-spacing: 0.5em;
  color: var(--fadedcoral);
  text-shadow: 0 0 5px var(--neoncoral), 0 0 10px white, 0 0 15px var(--neoncoral), 0 0 20px var(--neoncoral), 0 0 25px var(--neoncoral), 0 0 30px var(--neoncoral), 0 0 45px var(--neoncoral);
  font-size: 1.25em;
}

button {
  padding: 1em 1.25em;
  background: transparent;
  cursor: pointer;
  border: none;
  transition: 0.5s;
  color: var(--cedar);
  font-size: 1em;
}

button:focus {
  outline: none;
}

a.fullscreen {
  position: absolute;
  text-shadow: 0 0 5px var(--neoncoral), 0 0 10px white, 0 0 15px var(--neoncoral), 0 0 20px var(--neoncoral), 0 0 25px var(--neoncoral), 0 0 30px var(--neoncoral), 0 0 45px var(--neoncoral);
  color: #edc7c7;
  font-size: 0.9em;
  margin: 1.5em 3em 1.5em 15em;
  padding: 0;
  border-radius: 50%;
  text-decoration: none;
  transform: rotateZ(-30deg);
  text-align: center;
  transition: 0.25s ease-in;
}

a.fullscreen:hover {
  color: #f7e1e1;
}

#btn-celebrate:before { 
  height: 0%;
  width: 2px;
}

.btn {
  position: absolute;
  border-radius: 50%;
  text-shadow: 0 0 5px var(--neoncoral), 0 0 10px white, 0 0 15px var(--neoncoral), 0 0 20px var(--neoncoral), 0 0 25px var(--neoncoral), 0 0 30px var(--neoncoral), 0 0 45px var(--neoncoral);
}

#btn-celebrate {
  bottom: 1.5em;
  right: 1.5em;
  transform: rotateZ(-20deg);
  opacity: 0;
}

#btn-celebrate:hover {
  color: var(--lightgrey);
  box-shadow:  
  4px 4px 6px 0 rgba(252,176,176,.5),
  -4px -4px 6px 0 rgba(116, 125, 136, .2), 
  inset -4px -4px 6px 0 rgba(252,176,176,.5),
  inset 4px 4px 6px 0 rgba(116, 125, 136, .3);
  transform: rotateZ(-35deg);
}

a.fullscreen {
  opacity: 0;
  pointer-events: none;
}


.active {
  box-shadow: 4px 4px 6px 0 rgba(252,176,176,.5),
  -4px -4px 6px 0 rgba(116, 125, 136, .2), 
  inset -4px -4px 6px 0 rgba(252,176,176,.5),
  inset 4px 4px 6px 0 rgba(116, 125, 136, .3);
  color: var(--lightgrey);
}

.darker {
  background: radial-gradient(circle, var(--lavender) 0%, var(--teal) 100%);
}

.lighter {
  background: radial-gradient(circle, var(--coral) 0%, var(--dolphin) 100%);
}

.wiggle {
  animation: rotateMe 0.8s ease-out infinite;
  animation-direction: alternate;
}

@keyframes fadeOut {
  
  0% { opacity: 1.0;}
  100% { opacity: 0;}

}

@keyframes fadeIn {
  
  0% { opacity: 0.3;}
  100% { opacity: 1;}

}

@keyframes rotateMe {
  
  0% { transform: rotate(-20deg) scale(1);}
  100% { transform: rotate(-25deg) scale(1.2);}

}

/* lazy mobile */
@media(max-width: 800px) {
  #btn-celebrate {
    font-size: 2.5vh;
    bottom: 5.5em;
    margin: 0 auto;
  }
  
  #thankyou {
    display: none;
  }
  
}

@media (max-height: 500px) and (min-width: 1024px) {
  a.fullscreen {
    opacity: 1;
    pointer-events: auto;
  }
  #btn-celebrate {
    font-size: 0.85em;
    right: calc(20vw + 10vh);
/*     background: #fabfbf; */
  }
  #thankyou {
/*     opacity: 0; */
  }
}

@media (max-height: 500px) and (max-width: 1024px) {
  #btn-celebrate {
    font-size: 0.85em;
    right: 7vw;
/*     background: #fabfbf; */
  }
  #thankyou {
    opacity: 0;
  }
}
              
            
!

JS

              
                /*

  Celebrating 500 followers ✨ 

  Find the confetti button!✨
  (psst.. move mouse over after animation ends)
  
  The 'Thank You' letters and ruffles are modelled by me in Blender.
  Rest is Meshes and Instanced Meshes made with three.js.
  
  Confetti animation made with Szenia Zadvornykh's THREE.BAS:
  https://github.com/zadvorsky/three.bas
  
  Thanks to Davide Prati for the Palm Generator:
  https://davideprati.com/projects/palm-generator.html
  
  Design & code by Anna the Scavenger, September 2020
  https://twitter.com/ouch_pixels
  
*/

import { BufferGeometryUtils } from "https://unpkg.com/three@0.120.0/examples/jsm/utils/BufferGeometryUtils.js";
import { GLTFLoader } from "https://unpkg.com/three@0.120.0/examples/jsm/loaders/GLTFLoader.js";

'use strict';

let container, camera, renderer;
let loadingManager = null;

// GLOBAL MESHES

let lettersThanks, lettersYou;
let ruffles, ruffles1, ruffles2, ruffles3, ruffles4, candle;
let palmLeft, palmRight, rimTopBaseLeft, rimTopBaseRight;
let iDots, dottedSphere, dottedMatcapSphere, dottedSphere2;
let cylinderRimmed, fiveBottom, fiveCylinder, halfSphere5, torusStriped, torusDuo, torusSmall, sphereSmall;

// MATERIALS

let materials;
let iTeal, iApricot;

// ASSETS

const matcapGoldURL = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/911157/matcapGold_256_optimized.jpg";
const rufflesURL = "https://assets.codepen.io/911157/ruffCake5.glb";
const lettersURL = "https://assets.codepen.io/911157/thankYou500_optimized_noNormals.glb";

// PARTICLES

let btnParticles;

let mParticleCount = 2500;
// let mParticleSystem;

let mTime = 0.0;
let mTimeStep = (1/60);
let mDuration = 5;

let isParticleInitialized = false;
let isParticleAnimated = false;

// MOUSE

let mouseX = 0;
let mouseY = 0;
let hasEnteredRight = false;

// LANDSCAPE / PORTRAIT

let paramsPortrait = {
  
  statueLeftPosX: -90,
  statueLeftPosY: -90,

  statueRightPosX: 90,
  statueRightPosY: -90,
  
  lookAtY: -35
  
};

let paramsLandscape = {
  
  statueLeftPosX: -110,
  statueLeftPosY: -70,
  
  statueRightPosX: 110,
  statueRightPosY: -70,
  
  lookAtY: 5
  
};

let isMobile = /(Android|iPhone|iOS|iPod|iPad)/i.test(navigator.userAgent);
let windowRatio = window.innerWidth / window.innerHeight;
let isLandscape = (windowRatio > 1) ? true : false;
let params = isLandscape ? paramsLandscape : paramsPortrait;

// INTRO GSAP ANIMATION

let isIntroFinished = false;

window.onload = () => {
  
  init();
  render();
  
};

function init() {
  
  container = document.querySelector("#scene-container");
  
  initLoadingManager();
  initScene();
  
  materials = initMaterials();
  
  /* Separate materials for instanced mesh and regular meshes */
  
  iTeal = initMaterials().teal;
  iApricot = initMaterials().white;
  
  initMeshes();
  loadRuffles();
  loadLetters();
  
  addCurrentDate();
  
  document.addEventListener("mousemove", onMouseMove, false);
  document.addEventListener("touchmove", onTouchMove, false);
  window.addEventListener("resize", onWindowResize);
  
  btnParticles = document.querySelector("#btn-celebrate");
  btnParticles.disabled = true;
  btnParticles.addEventListener("click", onButtonClick, false);
  
}

function initLoadingManager() {
  
  const overlay = document.querySelector("#scene-overlay");
  
  loadingManager = new THREE.LoadingManager();

  loadingManager.onLoad = () => {
		
    overlay.style.animation = "fadeOut 1.5s ease-out forwards";
    container.style.animation = "fadeIn 1.5s ease-in forwards";
    startIntroAnimation();
    
  };
  
}

function initScene() {
  
  scene = new THREE.Scene();
  
  camera = new THREE.PerspectiveCamera(20, window.innerWidth / window.innerHeight, 0.1, 2000);
  const cameraZ = isLandscape ? 640 : (800 / windowRatio);
  camera.position.set(0, 25, cameraZ);
  camera.lookAt(0, params.lookAtY, 0);
  scene.add(camera);
  
  const dirLight = new THREE.DirectionalLight(0xffffff, 1, 1);
  dirLight.position.set(50, 50, -50);
  scene.add(dirLight);
  
  const dirLight2 = new THREE.DirectionalLight(0xff00ff, 1, 1);
  dirLight2.position.set(-50, 10, 50);
  scene.add(dirLight2);
  
  const dirLight3 = new THREE.DirectionalLight(0xffffff, 0.25);
  dirLight3.position.set(90, 50, 50);
  scene.add(dirLight3);

  const hemLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 1.75);
  hemLight.position.set(20, 50, 50);
  scene.add(hemLight);

  renderer = new THREE.WebGLRenderer({antialias:true, alpha: true});
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.physicallyCorrectLights = true;
  renderer.outputEncoding = THREE.GammaEncoding;
  renderer.gammaFactor = 2.2;
  renderer.setPixelRatio(window.devicePixelRatio > 1.5 ? Math.min(window.devicePixelRatio, 1.65) : Math.min(window.devicePixelRatio, 1.0));
  container.appendChild(renderer.domElement);
  
}

function initMaterials() {
    
  const white = new THREE.MeshPhongMaterial({
    
    color: 0xfc9f8d,
    shininess: 100,
    specular: 0x605d5d
    
  });
  
  const teal = new THREE.MeshPhongMaterial({

    color: 0x72a8a0,
    shininess: 100,
    specular: 0x605d5d
    
  });
  
  const ivoryMatte = new THREE.MeshPhongMaterial({
    
    color: 0xddc696,
    shininess: 0,
    specular: 0x7a7575
    
  });
  
  const textureLoader = new THREE.TextureLoader(loadingManager);
  
  const matcapGoldTex = textureLoader.load(matcapGoldURL);
  
  const matcapGold = new THREE.ShaderMaterial({
    
    transparent: false,
    side: THREE.DoubleSide,
    uniforms: {
      tMatCap: {
        type: "t",
        value: matcapGoldTex
      }
    },
    vertexShader: document.querySelector("#matcap-vs").textContent,
    fragmentShader: document.querySelector("#matcap-fs").textContent,
    flatShading: false
    
  });
  
  return {
    
    white,
    teal,
    ivoryMatte,
    matcapGold
    
  }

}

function loadLetters() {
  
  const scale = 0.05;
  const mat = materials.white;
  
  const loader = new GLTFLoader(loadingManager);

  loader.load(
    
    lettersURL, 
    
    function (gltf) {
            
      const letters = gltf.scene;
      letters.children[2].geometry.computeVertexNormals();
      letters.children[3].geometry.computeVertexNormals();
      lettersThanks = letters.children[2];
      lettersYou = letters.children[3];
      
      lettersThanks.position.set(-30, 58, -20);
      lettersThanks.rotation.z = 0.1 * Math.PI;
      lettersThanks.scale.set(scale, scale, scale);
      lettersThanks.material = mat;
      
      lettersYou.position.set(lettersThanks.position.x + 35, lettersThanks.position.y - 40, -20);
      lettersYou.rotation.z = 0.1 * Math.PI;
      lettersYou.scale.set(scale, scale, scale);
      lettersYou.material = mat;
      scene.add(lettersThanks, lettersYou);
      
	  },
    
    function (xhr) {
      console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
    },
    
    function (error) {
      console.log('GLTF loading error');
    }
    
  );

}

function loadRuffles() {
  
  const scale = 19;
  const rufflesMat = materials.white;
  
  const loader = new GLTFLoader(loadingManager);

  loader.load(
    
    rufflesURL, 
    
    function (gltf) {
            
      ruffles = gltf.scene;
      ruffles.scale.set(scale, 0.93 * scale, scale);
      
      ruffles1 = ruffles.children[2];
      ruffles1.geometry.center();
      ruffles1.material = rufflesMat;
      ruffles1.position.set(0, params.statueRightPosY + 60, 0);
      ruffles1.rotation.set(0, 0.1 * Math.PI, 0);
      ruffles1.scale.set(scale, scale, scale);
      
      let ruffX = ruffles1.position.x;
      let ruffY = ruffles1.position.y;
      let ruffZ = ruffles1.position.z;
      
      ruffles2 = ruffles.children[3];
      ruffles2.geometry.center();
      ruffles2.material = rufflesMat;
      
      ruffles2.scale.set(0.9 * scale, 0.9 * scale, 0.9 * scale);
      ruffles2.position.set(ruffX, ruffY - 19, ruffZ - 3);
      ruffles2.rotation.set(0, -0.59 * Math.PI, 0);
      ruffles2.scale.set(scale, scale, scale);
      
      ruffles3 = ruffles2.clone();
      ruffles3.position.set(ruffX, ruffY - 30, ruffZ - 7);
      ruffles3.rotation.set(0, 0.1 * Math.PI, 0);
      
      ruffles4 = ruffles2.clone();
      ruffles4.position.set(ruffX, ruffY - 40, ruffZ - 11);
      ruffles4.rotation.set(0, -0.59 * Math.PI, 0);

      scene.add(ruffles1, ruffles2, ruffles3, ruffles4);

	  },
    
    function (xhr) {
      console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
    },
    
    function (error) {
      console.log('GLTF loading error');
    }
    
  );

}

function CylinderRimMesh(cylinderMesh, material, thickness) {
  
  this.hoopMaterial = material,
  this.cylinderMesh = cylinderMesh;
  this.cylinderGeom = cylinderMesh.geometry;
  this.thickness = thickness;
    
  let cylinderRadius = this.cylinderGeom.parameters.radiusTop;
  let cylinderHeight = this.cylinderGeom.parameters.height;
  let hoopRadius = cylinderRadius; 
  
  const hoopGeomTop = new THREE.TorusBufferGeometry(hoopRadius, this.thickness, 16, 30);
  hoopGeomTop.rotateX(0.5 * Math.PI);
  hoopGeomTop.translate(0, cylinderHeight / 2, 0);
  
  const hoopGeomBottom = new THREE.TorusBufferGeometry(hoopRadius, this.thickness, 16, 30);
  hoopGeomBottom.rotateX(0.5 * Math.PI);
  hoopGeomBottom.translate(0, -cylinderHeight / 2, 0);
  
  const hoopTop = new THREE.Mesh(hoopGeomTop, this.hoopMaterial);
  hoopTop.position.copy(cylinderMesh.position);
  hoopTop.rotation.copy(cylinderMesh.rotation);
  hoopTop.scale.copy(cylinderMesh.scale);
  
  const hoopBottom = new THREE.Mesh(hoopGeomBottom, this.hoopMaterial);
  hoopBottom.position.copy(cylinderMesh.position);
  hoopBottom.rotation.copy(cylinderMesh.rotation);
  hoopBottom.scale.copy(cylinderMesh.scale);
  
  return {
    
    hoopTop,
    hoopBottom
    
  }
  
}

function generatePalmGeom() {
  
  let leaf_opt = {
    
    length: 70,
    length_stem: 2,
    width_stem: 0.2,
    leaf_width: 1,
    leaf_up: 6,
    density: 16,
    curvature: 0.01,
    curvature_border: 0.002,
    leaf_inclination: 0.8
    
  };

  let trunkGeometry = new THREE.BoxGeometry(5, 5, 5);
  let leafGeometry = new LeafGeometry(leaf_opt);

  let palm_opt = {
    
    spread: 0.2,
    angle: 137.66,
    num: 410,
    growth: 0.25,
    foliage_start_at: 65.64,
    trunk_regular: true,
    buffers: false,
    angle_open: 75.87,
    starting_angle_open: 51.65
    
  };

  let palm = new PalmGenerator(leafGeometry,trunkGeometry, palm_opt);
  let geometry = palm.geometry;
  let bufGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
  
  return bufGeometry;
  
}

function MatcapFibonacciDots(sphereRadius = 15, dotRadius = 1.1, dotCount = 50) {
  
  const textureLoader = new THREE.TextureLoader();
  const matcapTex = textureLoader.load(matcapGoldURL);

  const gold = new THREE.ShaderMaterial({
    
    transparent: false,
    side: THREE.DoubleSide,
    uniforms: {
      tMatCap: {
        type: "t",
        value: matcapTex
      }
    },
    vertexShader: document.querySelector("#vs-iMatcap").textContent,
    fragmentShader: document.querySelector("#fs-iMatcap").textContent,
    flatShading: false
    
  });
  
  this.sphereRadius = sphereRadius;
  this.dotRadius = dotRadius;
  this.dotCount = dotCount;
  
  let baseGeometry = new THREE.IcosahedronBufferGeometry(this.dotRadius, 1);

  let instancedGeometry = new THREE.InstancedBufferGeometry().copy(baseGeometry);
  instancedGeometry.instanceCount = this.dotCount;
  
  let fibonacciSpherePoints = getFibonacciSpherePoints(this.dotCount, this.sphereRadius);

  let aPosition = [];
  
  for (let i = 0; i < this.dotCount; i++) {
    
    let point = fibonacciSpherePoints[i];
    aPosition.push(point.x, point.y, point.z);
    
  }
  
  instancedGeometry.setAttribute(
      "aPosition",
      new THREE.InstancedBufferAttribute(new Float32Array(aPosition), 3, false)
  );
  
  let mesh = new THREE.Mesh(instancedGeometry, gold);

  return mesh;
  
}

function FibonacciDots(sphereRadius = 17.5, dotRadius = 0.85, dotCount = 200) {
  
  this.sphereRadius = sphereRadius;
  this.dotRadius = dotRadius;
  this.dotCount = dotCount;
  
  const white = new THREE.MeshPhongMaterial({
    
    color: 0xfc9f8d,
    shininess: 100,
    specular: 0x605d5d
    
  });
  
  let fibonacciSpherePoints = getFibonacciSpherePoints(this.dotCount, this.sphereRadius);
  
  let dotGeom = new THREE.IcosahedronBufferGeometry(this.dotRadius, 1);
  
  let iDots = new THREE.InstancedMesh(dotGeom, white, this.dotCount);
  
  let dummy = new THREE.Object3D();
  
  for ( let i = 0; i < this.dotCount; i ++ ) {
    
    let point = fibonacciSpherePoints[i];
    
    dummy.position.set(
      
      point.x,
      point.y,
      point.z
      
    );

    dummy.updateMatrix();

    iDots.setMatrixAt( i, dummy.matrix );

  }
  
  return iDots;

}

function StripedTorusGeom(radius = 15, tube = 8, sliceThickness = 0.05 * Math.PI) {
  
  this.radius = radius;
  this.tube = tube;
  this.stripeGeom = new THREE.TorusBufferGeometry(this.radius, this.tube, 25, 30, 0.05 * Math.PI);
  this.sliceThickness = sliceThickness;
  
  let numSlices = 0.5 * 2 * Math.PI / sliceThickness;
  
  let rotationStep = 2 * Math.PI / numSlices;
  
  let geoms = [];
  
  for (let i = 0; i < numSlices; i++) {
    
    let s = this.stripeGeom.clone();
    s.rotateZ(rotationStep * i);
    geoms.push(s);
    
  }
  
  let sliceGroup1 = BufferGeometryUtils.mergeBufferGeometries(geoms, true);
  let sliceGroup2 = sliceGroup1.clone();
  sliceGroup2.rotateZ(sliceThickness);
  
  return {
    
    sliceGroup1,
    sliceGroup2
    
  }
      
}

function initMeshes() {
  
  // GEOMETRIES
  
  const cylinderGeom = new THREE.CylinderBufferGeometry(25, 25, 30, 17);
  const cylinderGeom2 = new THREE.CylinderBufferGeometry(25, 25, 30, 17);
  cylinderGeom2.translate(0, -0.5 * cylinderGeom2.parameters.height, 0);
  const cylinderThinGeom = new THREE.CylinderBufferGeometry(7.5, 7.5, 40, 10); 
  const cylinderThinnerGeom = new THREE.CylinderBufferGeometry(4, 4, 48, 10);
  const torusQuarterGeom = new THREE.TorusBufferGeometry(20, 4, 10, 70, Math.PI / 2);
  const sphereGeom = new THREE.SphereBufferGeometry(17, 10, 10);
  const torusThinGeom = new THREE.TorusBufferGeometry(120, 2, 10, 70);
  
  // BIG HOOP IN THE BACK
 
  const torusBack = new THREE.Mesh(torusThinGeom, materials.white);
  torusBack.position.set(0, 10, -200);
  scene.add(torusBack);
  
  // PALM STATUE BASE LEFT
  
  const statueLeft = new THREE.Group();
  const statueBase = new THREE.Mesh(cylinderGeom, materials.white);
  rimTopBaseLeft = new CylinderRimMesh(statueBase, materials.white, 1).hoopTop;
  const rimBottomLeft = new CylinderRimMesh(statueBase, materials.matcapGold, 1).hoopBottom;
  statueLeft.add(statueBase, rimTopBaseLeft, rimBottomLeft);
  statueLeft.position.set(params.statueLeftPosX, params.statueLeftPosY, 0);
  
  // PALM STATUE BASE RIGHT
  
  const statueRight = new THREE.Group();
  const cylinderStatue2 = new THREE.Mesh(cylinderGeom, materials.white);
  rimTopBaseRight = new CylinderRimMesh(statueBase, materials.white, 1).hoopTop;
  const rimBottomRight = new CylinderRimMesh(statueBase, materials.matcapGold, 1).hoopBottom;
  statueRight.add(cylinderStatue2, rimTopBaseRight, rimBottomRight);
  statueRight.position.set(params.statueRightPosX, params.statueRightPosY, 0);
  
  // PALM LEFT + PALM RIGHT
  
  const palmGeom = generatePalmGeom();
  const palmScale = 0.9;
  
  palmLeft = new THREE.Mesh(palmGeom, materials.white);
  palmLeft.position.set(params.statueLeftPosX, params.statueLeftPosY + 10, 0);
  palmLeft.scale.set(palmScale, palmScale, palmScale);
  palmRight = palmLeft.clone();
  palmRight.position.set(params.statueRightPosX, params.statueRightPosY + 10, 0);
  scene.add(palmLeft, palmRight, statueLeft, statueRight);
  
  // TABLE LEG
 
  const rufflesLeg = new THREE.Group();
  const rufflesLegTop = new THREE.Mesh(cylinderThinnerGeom, materials.white);
  rufflesLegTop.scale.set(0.3, 0.5, 0.3);
  rufflesLegTop.position.set(0, params.statueLeftPosY, 0);
  const rufflesLegBottom = new THREE.Mesh(cylinderGeom2, materials.matcapGold);
  rufflesLegBottom.scale.set(0.3, 0.1, 0.3);
  rufflesLegBottom.position.set(0, params.statueLeftPosY - 12, 0);
  const rufflesLegBall = new THREE.Mesh(sphereGeom, materials.matcapGold);
  rufflesLegBall.scale.set(0.12, 0.12, 0.12);
  rufflesLegBall.position.set(-6, rufflesLegBottom.position.y - 5, 0);
  const rufflesLegBall2 = rufflesLegBall.clone();
  rufflesLegBall.position.set(6, rufflesLegBottom.position.y - 5, 0);
  rufflesLeg.add(rufflesLegTop, rufflesLegBottom, rufflesLegBall, rufflesLegBall2);
  scene.add(rufflesLeg);
  
  // CANDLE ON TABLE
  
  candle = new THREE.Mesh(cylinderThinnerGeom, materials.matcapGold);
  candle.position.set(0, params.statueLeftPosY + 40, 0);
  candle.scale.set(0.15, 0.3, 0.15);
  scene.add(candle);
  
  // NUMBER 500
  
  // NUMBER FIVE PART 1
  
  const fiveBottomGeom = new THREE.TorusBufferGeometry(15, 8, 16, 30, 1.5 * Math.PI);
  fiveBottom = new THREE.Group();
  const fiveBottomTorus = new THREE.Mesh(fiveBottomGeom, materials.white);
  fiveBottom.add(fiveBottomTorus);
  const halfSphereGeom = new THREE.SphereBufferGeometry(8, 16, 16, 0, Math.PI);
  const halfSphere = new THREE.Mesh(halfSphereGeom, materials.matcapGold);
  halfSphere.rotation.x = Math.PI / 2;
  halfSphere.position.x = fiveBottomTorus.position.x + fiveBottomTorus.geometry.parameters.radius;
  const halfSphere2 = new THREE.Mesh(halfSphereGeom, materials.white);
  halfSphere2.rotation.y = Math.PI / 2;
  halfSphere2.position.x = fiveBottomTorus.position.x;
  halfSphere2.position.y = fiveBottomTorus.position.y - fiveBottomTorus.geometry.parameters.radius;
  fiveBottom.add(halfSphere, halfSphere2);
  fiveBottom.position.set(-49, 25, -70);
  fiveBottom.rotation.z = -0.75 * Math.PI;
  fiveBottom.scale.set(1.1, 1.1, 1.1);
  scene.add(fiveBottom);
  
  fiveCylinder = new THREE.Mesh(cylinderThinnerGeom, materials.white);
  fiveCylinder.position.x = fiveBottom.position.x - 25;
  fiveCylinder.position.y = fiveBottom.position.y + 15;
  fiveCylinder.position.z = fiveBottom.position.z - 20;
  fiveCylinder.rotation.z = 0.125 * Math.PI;
  fiveCylinder.scale.set(0.6, 0.4, 0.6);
  halfSphere5 = new THREE.Mesh(sphereGeom, materials.matcapGold);
  halfSphere5.position.set(fiveCylinder.position.x + 1.75, fiveCylinder.position.y - 9.5, fiveCylinder.position.z +5);
  halfSphere5.scale.set(0.2, 0.2, 0.2);
  scene.add(fiveCylinder);
  scene.add(halfSphere5);
  
  // NUMBER FIVE PART 2
  
  cylinderRimmed = new THREE.Group();
  const cylinderThin = new THREE.Mesh(cylinderThinGeom, materials.white);

  cylinderThin.rotation.x = -0.2 * Math.PI;
  cylinderThin.rotation.z = 1.65 * Math.PI;
  const cylinderRimTop = new CylinderRimMesh(cylinderThin, materials.matcapGold, 1).hoopTop;
  const cylinderRimBottom = new CylinderRimMesh(cylinderThin, materials.matcapGold, 1).hoopBottom;
  cylinderRimmed.add(cylinderThin, cylinderRimTop, cylinderRimBottom);
  cylinderRimmed.position.set(-60, 58, -50);
  cylinderRimmed.scale.set(0.9, 0.9, 0.9);
  scene.add(cylinderRimmed);
  
  // TORUS STRIPES
  
  const stripesGeom1 = new StripedTorusGeom(15, 8).sliceGroup1;
  const stripesGeom2 = new StripedTorusGeom(15, 8).sliceGroup2;
  torusStriped = new THREE.Group();
  const stripes1 = new THREE.Mesh(stripesGeom1, materials.white);
  const stripes2 = new THREE.Mesh(stripesGeom2, materials.ivoryMatte);
  torusStriped.add(stripes1, stripes2);
  torusStriped.position.set(0, 55, -50);
  torusStriped.scale.set(1.1, 1.1, 1.1);
  scene.add(torusStriped);
  
  // TORUS DUO
  
  const halfTorusGeom = new THREE.TorusBufferGeometry(15, 8, 16, 30, Math.PI);
  torusDuo = new THREE.Group();
  const torusDuoTop = new THREE.Mesh(halfTorusGeom, materials.white);
  const torusDuoBottom = new THREE.Mesh(halfTorusGeom, materials.ivoryMatte);
  torusDuoBottom.rotation.x = Math.PI;
  torusDuo.add(torusDuoTop, torusDuoBottom);
  torusDuo.position.set(57, 44, -70);
  torusDuo.scale.set(1.15, 1.15, 1.15);
  scene.add(torusDuo);
  
  // SPHERES

  sphereSmall = new THREE.Mesh(sphereGeom, materials.ivoryMatte);
  sphereSmall.position.set(params.statueRightPosX + 23.75, params.statueRightPosY + 90, 0);
  sphereSmall.scale.set(0.35, 0.35, 0.35);
  scene.add(sphereSmall);
  
  // DOTTED SPHERE
  
  dottedSphere = new THREE.Group();
  const sphere = new THREE.Mesh(sphereGeom, materials.white);
  iDots = new FibonacciDots(17.25, 1.75, 200);
  iDots.position.copy(sphere.position);
  dottedSphere.add(sphere, iDots);
  dottedSphere.position.set(72, -5, -30);
  dottedSphere.scale.set(0.7, 0.7, 0.7);
  scene.add(dottedSphere);
  
  // DOTTED MATCAP SPHERE
  
  dottedMatcapSphere = new THREE.Group();
  const sphere2 = new THREE.Mesh(sphereGeom, materials.white);
  const iMatcapDots = new MatcapFibonacciDots(17.25, 1.15, 34);
  dottedMatcapSphere.add(sphere2, iMatcapDots);
  dottedMatcapSphere.position.set(params.statueLeftPosX, params.statueLeftPosY - 5, -30);
  dottedMatcapSphere.scale.set(0.7, 0.7, 0.7);
  scene.add(dottedMatcapSphere);
  
  // DOTTED SPHERE 2
  
  dottedSphere2 = new THREE.Group();
  const sphere3 = new THREE.Mesh(sphereGeom, materials.white);
  const iMatcapDots2 = new FibonacciDots(17.25, 1.25, 90);
  dottedSphere2.add(sphere3, iMatcapDots2);
  dottedSphere2.position.set(params.statueLeftPosX - 30, params.statueLeftPosY + 80, -30);
  dottedSphere2.scale.set(0.5, 0.5, 0.5);
  scene.add(dottedSphere2);
  
  const cylinderThinner = new THREE.Group();
  const cylThinner = new THREE.Mesh(cylinderThinnerGeom, materials.white);
  const rimCylinderTop = new CylinderRimMesh(cylThinner, materials.matcapGold, 1).hoopTop;
  const rimCylinderBottom = new CylinderRimMesh(cylThinner, materials.matcapGold, 1).hoopBottom;
  cylinderThinner.add(cylThinner, rimCylinderTop, rimCylinderBottom);
  cylinderThinner.position.set(params.statueLeftPosX - 50, params.statueLeftPosY + 12, 0);
  cylinderThinner.rotation.x = -Math.PI * 0.4;
  cylinderThinner.rotation.z = -Math.PI * 0.8;
  scene.add(cylinderThinner);
  
  const coneGeom = new THREE.ConeBufferGeometry(12, 28, 18);
  const cone = new THREE.Mesh(coneGeom, materials.white);
  cone.position.set(params.statueRightPosX + 55, params.statueRightPosY + 5, -20);
  scene.add(cone);
  
  const cylinderSmall = new THREE.Mesh(cylinderThinnerGeom, materials.ivoryMatte);
  cylinderSmall.position.set(params.statueLeftPosX - 45, params.statueLeftPosY + 30, -20);
  cylinderSmall.scale.set(0.6, 0.6, 0.6);
  cylinderSmall.rotation.set(0.35 * Math.PI, 0, 1.75 * Math.PI);
  scene.add(cylinderSmall);
  
  const torusGeom = new THREE.TorusBufferGeometry(12, 4, 16, 30);
  torusSmall = new THREE.Mesh(torusGeom, materials.white);
  torusSmall.position.set(params.statueRightPosX + 55, params.statueRightPosY + 100, -20);
  torusSmall.scale.set(0.05, 0.05, 0.05);
  torusSmall.rotation.set(-0.4 * Math.PI, 0, 0);
  scene.add(torusSmall);
  
  const coneLeg1 = new THREE.Mesh(sphereGeom, materials.matcapGold);
  coneLeg1.scale.set(0.12, 0.12, 0.12);
  coneLeg1.position.set(cone.position.x - 12, cone.position.y - cone.geometry.parameters.height / 2 - 2, cone.position.z);
  const coneLeg2 = coneLeg1.clone();
  coneLeg2.position.x = cone.position.x + 12;
  scene.add(coneLeg1, coneLeg2);
  
}

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

function update() {
  
  if ( isParticleAnimated ) {
    
    mParticleSystem.material.uniforms['uTime'].value = mTime;
    mTime += mTimeStep;
    mTime %= mDuration;
  
  }
  
  if ( isIntroFinished ) {

    toggleColorPalette();
    
    if (ruffles) {
    
      ruffles1.rotation.y = 0.2 * Math.PI + -0.25 * Math.PI * mouseX;
      ruffles2.rotation.y = -0.8 * Math.PI + -0.25 * Math.PI * mouseX;
      ruffles3.rotation.y = 0.2 * Math.PI + -0.25 * Math.PI * mouseX;
      ruffles4.rotation.y = -0.8 * Math.PI + -0.25 * Math.PI * mouseX;
      palmLeft.rotation.y = 0.2 * Math.PI * mouseX;
      palmRight.rotation.y = -0.2 * Math.PI * mouseX;
      lettersThanks.rotation.x = 0.1 * mouseX * Math.PI;
      lettersThanks.rotation.z = 0.1 * Math.PI;
      lettersYou.rotation.x = -0.1 * mouseX * Math.PI;
      lettersYou.rotation.z = 0.1 * Math.PI;
      torusStriped.position.x = -50 * mouseX;
      torusStriped.rotation.z = 2 * mouseX;
      sphereSmall.position.x = params.statueRightPosX + 23.5 * Math.cos(2 * Math.PI * mouseX);
      sphereSmall.position.z = 23.5 * Math.sin(2 * Math.PI * mouseX);
    
    }
    
  }

  else {
    return;
    // console.log('Ruffles still loading or don\'t exist');
  }
  
}

function toggleColorPalette() {
  
  if ( mouseX > 0.3 && !hasEnteredRight ) {
    
    hasEnteredRight = true;
    container.classList.add("darker");
    container.classList.remove("lighter");

    scene.traverse(function(child) {
      
      if (child.material === materials.white) {
          
        child.material = materials.teal;

      } else {
        
        return;
        
      }
    
    });
      
    iDots.material = iTeal;
    palmLeft.material = materials.white;
    palmRight.material = materials.white;
    lettersThanks.material = materials.white;
    lettersYou.material = materials.white;
      
  } else if ( mouseX < -0.3 && hasEnteredRight ) {
    
    hasEnteredRight = false;
    container.classList.remove("darker");
    container.classList.add("lighter");

    scene.traverse(function(child) {
      
      if (child.material === materials.teal) {
          
        child.material = materials.white;

      } else {
        
        return;
        
      }
    
    });
    
    iDots.material = iApricot;
    palmLeft.material = materials.teal;
    palmRight.material = materials.teal;
    lettersThanks.material = materials.teal;
    lettersYou.material = materials.teal;
    
  } else {
    
    return;
    
  }
  
}

function onMouseMove(event) {
  
  mouseX = (event.clientX / window.innerWidth) * 2 - 1;
  mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
  
}

// EVENTS

function onTouchMove(event) {
    
	let x = event.changedTouches[0].clientX;
  let y = event.changedTouches[0].clientY;
  
  mouseX = (x / window.innerWidth) * 2 - 1;
  mouseY = (x / window.innerWidth) * 2 - 1;
      
}

function onWindowResize() {
  
  camera.aspect = container.clientWidth / container.clientHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(container.clientWidth, container.clientHeight);
  
}

function onButtonClick() {
  
  btnParticles.classList.remove("wiggle");
  
  if ( !isParticleAnimated && !isParticleInitialized ) {
    
    let candleTL = gsap.timeline({paused: true, onComplete: candleComplete });
      
    candleTL
      .to(candle.position, {y: params.statueLeftPosY + 60, duration: 1.1, ease: "power2.out"});
    
    candleTL.play();
    
    function candleComplete() {
      
      isParticleAnimated = !isParticleAnimated;
      isParticleInitialized = !isParticleInitialized;
      initParticleSystem();
      toggleButtonStyle(btnParticles);
      
    }
        
  } else {
    
    isParticleAnimated = !isParticleAnimated;  
    toggleButtonStyle(btnParticles);
    
  } 
  
}

function toggleButtonStyle(btn) {
  
  if ( isParticleAnimated ) {
      
      btn.classList.add("active");
      
  } else {
      
      btn.classList.remove("active");
  }

}

// GSAP INTRO ANIMATION

function startIntroAnimation() {
  
  let mainTL = gsap.timeline({ paused: true });
  
  function palmTimeline() {
    
    let palmTL = gsap.timeline({paused: true, onComplete: onIntroComplete});
    let rufflesEase = "elastic.out(1, 0.75)";
    let fallEase = "bounce.out";
    let fallDur = 1.15;
    
    
    palmTL
      .to({}, {x: 8, duration: 1.75 })
      .add("ruffles")
      .to(ruffles1.rotation, {y: 0.2 * Math.PI, duration: 0.9, ease: "elastic.out(1.1, 1)"}, "ruffles")
      .to(palmRight, {material: materials.matcapGold, duration: 0.0}, "ruffles+=0.35")
      .to(palmRight.scale, {x: 0.95, duration: 0.35, ease: rufflesEase }, "ruffles+=0.35")
      .to(palmRight.scale, {y: 0.95, duration: 0.35, ease: rufflesEase }, "ruffles+=0.35") 
      .to(palmRight.scale, {z: 0.95, duration: 0.35, ease: rufflesEase }, "ruffles+=0.35")
    
      .to(ruffles2.rotation, {y: -0.8 * Math.PI, duration: 0.9, ease: "elastic.out(1.1, 1)"}, "rufflesleft1")
      .to(palmLeft, {material: materials.matcapGold, duration: 0.0}, "rufflesleft1+=0.35")
      .to(palmLeft.scale, {x: 0.95, duration: 0.35, ease: rufflesEase }, "rufflesleft1+=0.35")
      .to(palmLeft.scale, {y: 0.95, duration: 0.35, ease: rufflesEase }, "rufflesleft1+=0.35") 
      .to(palmLeft.scale, {z: 0.95, duration: 0.35, ease: rufflesEase }, "rufflesleft1+=0.35")
    
      .to(ruffles3.rotation, {y: 0.2 * Math.PI, duration: 0.9, ease: "elastic.out(1.1, 1)"}, "lettersIn")
      .to(lettersThanks, {material: materials.matcapGold, duration: 0.0, ease: "elastic.out(1, 1)"}, "lettersIn+=0.35")
      .to(lettersThanks.scale, {x: 7.75, duration: 0.75, ease: rufflesEase}, "lettersIn+=0.35")
      .to(lettersThanks.scale, {y: 7.75, duration: 0.75, ease: rufflesEase}, "lettersIn+=0.35")
      .to(lettersThanks.scale, {z: 7.75, duration: 0.75, ease: rufflesEase}, "lettersIn+=0.35")

      .to(ruffles4.rotation, {y: -0.8 * Math.PI, duration: 0.9, ease: "elastic.out(1.1, 1)"}, "lettersIn+=1.0")
      .to(lettersYou, {material: materials.matcapGold, duration: 0.0, ease: "elastic.out(1, 1)"}, "lettersIn+=1.35")
      .to(lettersYou.scale, {x: 7.75, duration: 0.75, ease: "elastic.out(1.1, 1)"}, "lettersIn+=1.35")
      .to(lettersYou.scale, {y: 7.75, duration: 0.75, ease: rufflesEase}, "lettersIn+=1.35")
      .to(lettersYou.scale, {z: 7.75, duration: 0.75, ease: rufflesEase}, "lettersIn+=1.35")
      
      .add("rufflesUp")
      .to(ruffles1.position, {y: params.statueLeftPosY + 64, duration: 0.2, ease: "power3.in"}, "rufflesUp")
      .to(ruffles2.position, {y: params.statueLeftPosY + 45, duration: 0.2, ease: "power3.in"}, "rufflesUp+=0.05")
      .to(ruffles3.position, {y: params.statueLeftPosY + 33, duration: 0.2, ease: "power3.in"}, "rufflesUp+=0.1")
      .to(ruffles4.position, {y: params.statueLeftPosY + 21, duration: 0.2, ease: "power3.in"}, "rufflesUp+=0.15")
    
      .add("rufflesDown")
      .to(ruffles1.position, {y: params.statueLeftPosY + 36, duration: 1.15, ease: "power3.inOut"}, "rufflesDown")
      .to(ruffles2.position, {y: params.statueLeftPosY + 17, duration: 1.15, ease: "power3.inOut"}, "rufflesDown")
      .to(ruffles3.position, {y: params.statueLeftPosY + 5, duration: 1.15, ease: "power3.inOut"}, "rufflesDown+=0.1")
      .to(ruffles4.position, {y: params.statueLeftPosY - 7, duration: 1.15, ease: "power3.inOut"}, "rufflesDown+=0.15")
    
      .to(rimTopBaseLeft.position, {y: 15, duration: 0.65, ease: "power1.out"}, "startFall")
      .to(rimTopBaseRight.position, {y: 15, duration: 0.65, ease: "power1.out"}, "startFall")
    
      .to(sphereSmall.position, {y: params.statueRightPosY + 36.75, duration: 1.5, ease: fallEase}, "startFall+=0.9")
      .to(dottedSphere2.position, {y: params.statueLeftPosY + 45, duration: 1.5, ease: fallEase}, "startFall+=1.0")
      .to(fiveCylinder.position, {y: -70, duration: fallDur, ease: fallEase}, "startFall+=1.1")
      .to(fiveCylinder.rotation, {z: 0.5 * Math.PI, duration: fallDur, ease: fallEase}, "startFall+=1.1")
      .to(cylinderRimmed.position, {x: -75, duration: fallDur, ease: fallEase}, "startFall+=1.1")
      .to(cylinderRimmed.position, {y: -75, duration: fallDur, ease: fallEase}, "startFall+=1.1")
      .to(cylinderRimmed.position, {z: -90, duration: fallDur, ease: fallEase}, "startFall+=1.1")
      .to(cylinderRimmed.rotation, {x: -0.3 * Math.PI, duration: fallDur, ease: fallEase}, "startFall+=1.1")
      .to(cylinderRimmed.rotation, {z: 0.75 * Math.PI, duration: fallDur, ease: fallEase}, "startFall+=1.1")
      .to(fiveBottom.position, {x: -65, duration: fallDur, ease: fallEase}, "startFall+=1.3")
      .to(fiveBottom.position, {y: -60, duration: fallDur, ease: fallEase}, "startFall+=1.3")
      .to(fiveBottom.rotation, {x: 3 * Math.PI, duration: fallDur, ease: fallEase}, "startFall+=1.3")
      
      .to(torusStriped.position, {y: -65, duration: fallDur, ease: fallEase}, "startFall+=1.55")
      .to(torusStriped.rotation, {x: 2 * Math.PI, duration: fallDur, ease: "power3.out"}, "startFall+=1.55")
      .to(torusDuo.position, {x: 70, duration: fallDur, ease: fallEase}, "startFall+=1.8")
      .to(torusDuo.position, {y: -70, duration: fallDur, ease: fallEase}, "startFall+=1.8")
      .to(torusDuo.rotation, {x: -3.5 * Math.PI, duration: fallDur, ease: "power3.out"}, "startFall+=1.8")
      .to(halfSphere5.position, {y: -70, duration: fallDur, ease: fallEase}, "startFall+=1.4")
    
      .to(dottedSphere.position, {y: -70, duration: 1.25, ease: fallEase}, "startFall+=2")
      .to(dottedSphere.position, {x: 65, duration: 1.25, ease: fallEase}, "startFall+=2")
      .to(dottedSphere.scale, {x: 1.1, duration: 0.3, ease: "power2.in"}, "startFall+=1.7")
      .to(dottedSphere.scale, {y: 1.1, duration: 0.3, ease: "power2.in"}, "startFall+=1.7")
      .to(dottedSphere.scale, {z: 1.1, duration: 0.3, ease: "power2.in"}, "startFall+=1.7")
      .to(torusSmall.position, {y: -60, duration: fallDur, ease: fallEase}, "startFall+=1.7")
      .to(torusSmall.scale, {x: 0.6, duration: fallDur-0.2, ease: fallEase}, "startFall+=1.7")
      .to(torusSmall.scale, {y: 0.6, duration: fallDur-0.2, ease: fallEase}, "startFall+=1.7")
      .to(torusSmall.scale, {z: 0.6, duration: fallDur-0.2, ease: fallEase}, "startFall+=1.7")
    ;
    
    return palmTL;
    
  }
  
  function sphereTimeline() {
    
    let sphereTL = gsap.timeline({paused: true});
    sphereTL
      .to(dottedMatcapSphere.rotation, {z: -2 * Math.PI, duration: 3., ease: "power2.out"}, "startSphere")
      .to(dottedMatcapSphere.position, {x: params.statueLeftPosX + 40, duration: 3., ease: "power3.out"}, "startSphere+=0.2");
    
    return sphereTL;
      
    
  }
 
  mainTL
    .add(palmTimeline().play())
    .add(sphereTimeline().play())
  ;
    
  mainTL.play();

}

function onIntroComplete() {
  
  isIntroFinished = true;
  btnParticles.style.opacity = 1;
  btnParticles.disabled = false;
  btnParticles.classList.add("wiggle");

}
              
            
!
999px

Console