<meta name="viewport" content="width=device-width, initial-scale=1.0">
<canvas id="scene-container" aria-label="Two rotating spiky balls" role="img">
</canvas>
<div class="credits">
  <a class="animated" href="https://codepen.io/ScavengerFrontend" target="blank">my other Codepens</a>
</div>
@import url('https://fonts.googleapis.com/css?family=Ovo&text=cCdehmnoprsty&display=swap');

body {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  overflow: hidden;
  font-family: 'Ovo', Arial;
  font-size: 16px;
  background: rgb(255,255,255);
  background: radial-gradient(circle, rgba(255,255,255,1) 0%, rgba(162,145,134,1) 100%);
}

#scene-container {
  width: 100%;
  height: 100%;
}

.credits {
  position: fixed;
  left: 50%;
  bottom: 20px;
  transform: translate(-50%, -50%);
  margin: 7.5px auto;
}

a {
  color: rgba(0, 0, 0, 0.0);
  backface-visibility: hidden;
  -moz-backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  text-decoration: none;
  text-underline-offset: 3px;
  display: inline-block;
}

a:link { 
  color: rgba(0, 0, 0, 0.0); 
}

.animated {
  animation: fadeIn 2s ease-out 5s forwards;
  -moz-animation: fadeIn 2s ease-out 5s forwards;
  -webkit-animation: fadeIn 2s ease-out 5s forwards;
}

@keyframes fadeIn {
  
  0% { color: rgba(0, 0, 0, 0.0); }
  100% { color: rgba(0, 0, 0, 0.0); }
  
}

@-moz-keyframes fadeIn {
  
  0% { color: rgba(0, 0, 0, 0.0); }
  100% { color: rgba(0, 0, 0, 0.0); } 
  
}

@-webkit-keyframes fadeIn {
  
  0% { color: rgba(0, 0, 0, 0.0); }
  100% { color: rgba(0, 0, 0, 0.0); }
  
}

/* @keyframes fadeIn {
  
  0% { color: rgba(0, 0, 0, 0.1); }
  100% { color: rgba(0, 0, 0, 0.85); }
  
}

@-moz-keyframes fadeIn {
  
  0% { color: rgba(0, 0, 0, 0.1); }
  100% { color: rgba(0, 0, 0, 0.85); } 
  
}

@-webkit-keyframes fadeIn {
  
  0% { color: rgba(0, 0, 0, 0.1); }
  100% { color: rgba(0, 0, 0, 0.85); }
  
}
 */
/*

  ★ These calm spikes are here to brighten up your day ★
  
  art & code by
  Anna Scavenger, February 2020
  https://twitter.com/ouchpixels

*/

import * as THREE from 'https://unpkg.com/three@0.123.0/build/three.module.js';
import { OrbitControls } from "https://unpkg.com/three@0.123.0/examples/jsm/controls/OrbitControls.js";

let container, scene, camera, renderer;
let controls;
let cactus, cactus2;

let isMobile = /(Android|iPhone|iOS|iPod|iPad)/i.test(navigator.userAgent);

let mouseX = 0;
let mouseY = 0;
let time = new THREE.Clock();

init();
render();

function init() {
        
  container = document.querySelector("#scene-container");
  scene = new THREE.Scene();
  
  createCamera();
  createControls();
  createLight();
  createRenderer();
  createCactus();

  window.addEventListener("resize", onWindowResize, false);
  document.addEventListener("mousemove", onMouseMove, false);
  
}

function createCamera() {

  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.position.set(0, 0, 15);
  const cameraZ = 15;
  
  if (camera.aspect < 1 && camera.aspect > 0.75) {

    camera.position.set(0, 0, cameraZ * 1.5);

  } else if (camera.aspect < 0.75) {

    camera.position.set(0, 0, cameraZ * 2.0);

  } else {

    camera.position.set(0, 0, cameraZ);

  }
  
}

function createControls() {
  
  controls = new OrbitControls(camera, container);
  
}

function createLight() {
  
  const ambientLight = new THREE.AmbientLight(0xeeeeee); 

  const dirLight = new THREE.DirectionalLight(0xfffffff);
  dirLight.intensity = 0.5;
  dirLight.position.set(50, 120, -100);
  dirLight.target.position.set(0, 0, 0);

  scene.add(ambientLight, dirLight);
  
}

function createRenderer() {
  
  renderer = new THREE.WebGLRenderer({ 

    canvas: container,
    antialias: false,
    alpha: true

  });

  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(isMobile ? Math.min(1.25, window.devicePixelRatio) : Math.min(1.5, window.devicePixelRatio));
  renderer.gammaFactor = 2.2;
  document.body.appendChild(renderer.domElement);
  
}

function createCactus() {
  
  const horBands = 120; 
  const vertBands = 120; 
  const radius = 5.5;
  
  const cactusGeometry = UVSphere(radius, vertBands, horBands);

  const spines = 2;
  // Also pretty!
  // spines = 3; 
  let amount = radius;
  let mod = 0; 
  let y = 0; 
  let knots = 4;
  let spikeVerts = [];
  let verti;
  
  for (let i = 0; i < cactusGeometry.vertices.length; i++) {
      
    verti = cactusGeometry.vertices[i];
    verti.negate();
      
    if (i > vertBands * 10) {
          
      amount += mod;
      amount += (Math.random() * 2 - 1) * 0.0005;

      if (((y < (verti.y + .01)) && (y > (verti.y - .01)))) {
        
        // modif the shape
        if (Math.random() * 20 > 10 || y < 0) {
                  
          mod = 0.00016;
                  
        } else {
      
          // IMPO: mod= -0.00016
          mod = -0.00075;
          
        }
        
      }
    }
      
    if ((i + 1) % knots == 0) {
          
      verti.setLength(amount + .005);
      cactusGeometry.colors[i] = new THREE.Color(0xcc91a3);
          
    } else if ((i - 1) % knots == 0) {
          
      verti.setLength(amount + .005);
      cactusGeometry.colors[i] = new THREE.Color(0xcc91a3);

    } else if (i % knots == 0) {
          
      verti.setLength(amount + 0.0075);
      cactusGeometry.colors[i] = new THREE.Color(0xe6c1cc);
          
      if ((Math.floor(i / horBands) % spines) == 0) {
                
        if (i > vertBands * 2) {
        // IMPO: amount + 0.23
          cactusGeometry.vertices[i].setLength(amount + 0.105);
          spikeVerts.push(i);
        }
        
      } else if ((Math.floor((i + horBands) / horBands) % spines) == 0 || (Math.floor((i - horBands) / horBands) % spines) == 0) {
              
        verti.setLength(amount + 1);
              
      } else if ((Math.floor((i + horBands * 2) / horBands) % spines) == 0 || (Math.floor((i - horBands * 2) / horBands) % spines) == 0) {
              
        verti.setLength(amount + .115);
              
      }
      
    } else {
          
      cactusGeometry.colors[i] = new THREE.Color(0xb87287);
      verti.setLength(amount);
          
    }

  }
  
  console.log('Spikes number: ' + spikeVerts.length);
  
  const faceIndices = ['a', 'b', 'c', 'd'];

  // Vertices coloring
  for (let i = 0; i < cactusGeometry.faces.length; i++) {

    let face = cactusGeometry.faces[i];
    let numberOfSides = 3;
    let vertexIndex;

    for (let j = 0; j < numberOfSides; j++) {

      vertexIndex = face[faceIndices[j]];
      face.vertexColors[j] = cactusGeometry.colors[vertexIndex];
      
    }
  
  }
  
  const cactusMaterial = new THREE.MeshLambertMaterial();
  cactusMaterial.vertexColors = THREE.VertexColors;
  cactusMaterial.side = THREE.DoubleSide;

  cactus = new THREE.Mesh(cactusGeometry, cactusMaterial);
  cactus.scale.set(1.3, 1.3, 1.3);
  cactus.position.set(0, 0, 0);

  cactus2 = cactus.clone();
  cactus2.position.set(6, -1.5, 0);

  cactus.rotateX(Math.PI / 1.9);

  scene.add(cactus, cactus2);
  
}

function render() {

  update();
  requestAnimationFrame(render);
  renderer.render(scene, camera);

};

function update() {

  let t = time.getElapsedTime();

  cactus.rotation.z = 0.125 * t * mouseX;
  cactus2.rotation.z = 0.125 * t * mouseX;
  cactus2.rotation.y = 0.025 * t * mouseY;

}

// *** EVENTS ***

function onWindowResize() {
  
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  
}

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

}

// *** UTILS ***

function SphereCoordToCartesian(r, phi, theta) {
  
  let x = Math.cos(phi) * Math.sin(theta) * r;
  let y = Math.sin(phi) * Math.sin(theta) * r;
  let z = Math.cos(theta) * r;
  
  return new THREE.Vector3(x, y, z);
  
}

function UVSphere(radius, stacks, slices) {
  
  const geometry = new THREE.Geometry();
  
  let theta1, theta2, ph1, ph2;
  let vert1, vert2, vert3, vert4;
  let index = 0;

  for (let t = 0; t < stacks; t++) {

    theta1 = (t / stacks) * Math.PI;
    theta2 = ((t + 1) / stacks) * Math.PI;

    for (let p = 0; p < slices; p++) {
        
      ph1 = (p / slices) * 2 * Math.PI;
      ph2 = ((p + 1) / slices) * 2 * Math.PI;

      vert1 = SphereCoordToCartesian(radius, ph1, theta1);
      vert3 = SphereCoordToCartesian(radius, ph2, theta1);
      vert2 = SphereCoordToCartesian(radius, ph2, theta2);
      vert4 = SphereCoordToCartesian(radius, ph1, theta2);

      geometry.vertices.push(vert1, vert2, vert3, vert4);

      if (t == 0) {

        geometry.faces.push(new THREE.Face3(0 + index, 1 + index, 3 + index));
          
      } else if (t + 1 == stacks) {

        geometry.faces.push(new THREE.Face3(1 + index, 0 + index, 2 + index));

      } else {

        geometry.faces.push(new THREE.Face3(0 + index, 2 + index, 3 + index));
        geometry.faces.push(new THREE.Face3(2 + index, 1 + index, 3 + index));

      }

      index += 4;

    }

  }

  geometry.mergeVertices();
  geometry.normalize();
  geometry.computeFaceNormals();
  geometry.computeVertexNormals(true);

  return geometry;
  
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.