<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8">

  <title>Beating Heart ❤</title>

  <style>

    html, body {

      margin: 0;

      padding: 0;

      overflow: hidden;

      background: #ff5555;

    }

    canvas {

      display: block;

    }

  </style>

</head>

<body>


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

<script src="https://cdn.jsdelivr.net/npm/three@0.131.3/examples/js/math/MeshSurfaceSampler.js"></script>

<script src="https://cdn.jsdelivr.net/npm/three@0.131.3/examples/js/controls/TrackballControls.js"></script>

<script src="https://assets.codepen.io/127738/simplex-noise.js"></script>

<script src="https://cdn.jsdelivr.net/npm/three@0.131.3/examples/js/loaders/OBJLoader.js"></script>

<script src="https://unpkg.com/gsap@3/dist/gsap.min.js"></script>


<script>

  console.clear();


  const scene = new THREE.Scene();

  const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);

  camera.position.z = 1;


  const renderer = new THREE.WebGLRenderer({ antialias: true });

  renderer.setSize(window.innerWidth, window.innerHeight);

  document.body.appendChild(renderer.domElement);


  const controls = new THREE.TrackballControls(camera, renderer.domElement);

  controls.noPan = true;

  controls.maxDistance = 3;

  controls.minDistance = 0.7;


  const group = new THREE.Group();

  scene.add(group);


  let heart = null, sampler = null, originHeart = null;

  new THREE.OBJLoader().load('https://assets.codepen.io/127738/heart_2.obj', obj => {

    heart = obj.children[0];

    heart.geometry.rotateX(-Math.PI * 0.5);

    heart.geometry.scale(0.04, 0.04, 0.04);

    heart.geometry.translate(0, -0.4, 0);

    heart.material = new THREE.MeshBasicMaterial({ color: 0xff5555 });

    group.add(heart);

    originHeart = Array.from(heart.geometry.attributes.position.array);

    sampler = new THREE.MeshSurfaceSampler(heart).build();

    init();

    renderer.setAnimationLoop(render);

  });


  const geometry = new THREE.BufferGeometry();

  const material = new THREE.LineBasicMaterial({ color: 0xffffff });

  const lines = new THREE.LineSegments(geometry, material);

  group.add(lines);


  const simplex = new SimplexNoise();

  const pos = new THREE.Vector3();

  const spikes = [];


  class Grass {

    constructor() {

      sampler.sample(pos);

      this.pos = pos.clone();

      this.scale = Math.random() * 0.01 + 0.001;

    }

    update(a) {

      const noise = simplex.noise4D(this.pos.x * 1.5, this.pos.y * 1.5, this.pos.z * 1.5, a * 0.0005) + 1;

      this.one = this.pos.clone().multiplyScalar(1.01 + noise * 0.15 * beat.a);

      this.two = this.one.clone().add(this.one.clone().setLength(this.scale));

    }

  }


  function init() {

    for (let i = 0; i < 20000; i++) {

      spikes.push(new Grass());

    }

  }


  const beat = { a: 0 };

  gsap.timeline({ repeat: -1, repeatDelay: 0.3 })

    .to(beat, { a: 1.2, duration: 0.6, ease: 'power2.in' })

    .to(beat, { a: 0, duration: 0.6, ease: 'power3.out' });


  gsap.to(group.rotation, {

    y: Math.PI * 2,

    duration: 12,

    ease: 'none',

    repeat: -1

  });


  function render(a) {

    const positions = [];

    spikes.forEach(g => {

      g.update(a);

      positions.push(g.one.x, g.one.y, g.one.z);

      positions.push(g.two.x, g.two.y, g.two.z);

    });

    geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));


    const vs = heart.geometry.attributes.position.array;

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

      const v = new THREE.Vector3(originHeart[i], originHeart[i+1], originHeart[i+2]);

      const noise = simplex.noise4D(originHeart[i]*1.5, originHeart[i+1]*1.5, originHeart[i+2]*1.5, a*0.0005) + 1;

      v.multiplyScalar(1 + noise * 0.15 * beat.a);

      vs[i] = v.x;

      vs[i+1] = v.y;

      vs[i+2] = v.z;

    }

    heart.geometry.attributes.position.needsUpdate = true;


    controls.update();

    renderer.render(scene, camera);

  }


  window.addEventListener("resize", () => {

    camera.aspect = window.innerWidth / window.innerHeight;

    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);

  });

</script>


</body>

</html>