<!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>