Back to Components
Heart Animation with Particle Effects
Component

Heart Animation with Particle Effects

CodewithLord
January 30, 2026

A beautiful and romantic heart animation featuring smooth particle effects, canvas-based rendering, and mathematical curves. Pure HTML, CSS, and JavaScript implementation with adjustable particle settings.

🧠 Description


This project showcases an elegant heart animation built entirely with pure HTML, CSS, and JavaScript.

The animation uses the HTML5 Canvas API to render particles that emit from a mathematically-defined heart shape, creating a romantic and visually stunning effect.

Each particle follows physics-based movement with velocity, acceleration, and gravity effects, while gradually fading out over time.

The design is responsive, lightweight, and perfect for Valentine's Day websites, love-themed projects, and romantic UI elements.


💻 HTML Code


1<!DOCTYPE html> 2<html lang="en"> 3 4<head> 5 <meta charset="UTF-8"> 6 <title>Heart Animation | CodewithLord</title> 7 <link rel="stylesheet" href="https://public.codepenassets.com/css/reset-2.0.min.css"> 8 <link rel="stylesheet" href="./style.css"> 9</head> 10 11<body> 12 13 <canvas id="pinkboard"></canvas> 14 <script src="./script.js"></script> 15 16</body> 17</html>

HTML Structure Explanation

The HTML structure is minimal and clean, consisting of just three key elements:

A single <canvas id="pinkboard"> element serves as the rendering surface for the entire animation.

External CSS stylesheet provides styling for the page background and canvas layout.

JavaScript file (script.js) handles all the animation logic, particle generation, and rendering.

This lightweight approach ensures maximum performance and allows the JavaScript to have full control over the visual output.


🎨 CSS Code


1html, 2body { 3 height: 100%; 4 padding: 0; 5 margin: 0; 6 background: #000; 7} 8 9canvas { 10 position: absolute; 11 width: 100%; 12 height: 100%; 13}

CSS Breakdown

Minimal Styling Approach

The CSS is intentionally minimal, allowing the canvas animation to take center stage.

Full-Screen Canvas

html and body are set to 100% height with no padding or margins for a seamless full-screen experience.

Black Background

A pure black background (#000) provides perfect contrast for the pink heart animation and particles.

Absolute Positioning

The canvas element uses absolute positioning to fill the entire viewport, ensuring responsive behavior across all screen sizes.

This lightweight CSS approach keeps the page performant while providing the perfect visual foundation for the JavaScript animation.


⚙️ JavaScript Code


1let settings = { 2 particles: { 3 length: 500, 4 duration: 2, 5 velocity: 100, 6 effect: -0.75, 7 size: 30 8 } 9}; 10 11(function () { 12 let b = 0; 13 let c = ["ms", "moz", "webkit", "o"]; 14 for (let a = 0; a < c.length && !window.requestAnimationFrame; ++a) { 15 window.requestAnimationFrame = window[c[a] + "RequestAnimationFrame"]; 16 window.cancelAnimationFrame = 17 window[c[a] + "CancelAnimationFrame"] || 18 window[c[a] + "CancelRequestAnimationFrame"]; 19 } 20 if (!window.requestAnimationFrame) { 21 window.requestAnimationFrame = function (h) { 22 let d = new Date().getTime(); 23 let f = Math.max(0, 16 - (d - b)); 24 let g = window.setTimeout(function () { 25 h(d + f); 26 }, f); 27 b = d + f; 28 return g; 29 }; 30 } 31 if (!window.cancelAnimationFrame) { 32 window.cancelAnimationFrame = function (d) { 33 clearTimeout(d); 34 }; 35 } 36})(); 37 38let Point = (function () { 39 function Point(x, y) { 40 this.x = typeof x !== "undefined" ? x : 0; 41 this.y = typeof y !== "undefined" ? y : 0; 42 } 43 Point.prototype.clone = function () { 44 return new Point(this.x, this.y); 45 }; 46 Point.prototype.length = function (length) { 47 if (typeof length == "undefined") 48 return Math.sqrt(this.x * this.x + this.y * this.y); 49 this.normalize(); 50 this.x *= length; 51 this.y *= length; 52 return this; 53 }; 54 Point.prototype.normalize = function () { 55 var length = this.length(); 56 this.x /= length; 57 this.y /= length; 58 return this; 59 }; 60 return Point; 61})(); 62 63let Particle = (function () { 64 function Particle() { 65 this.position = new Point(); 66 this.velocity = new Point(); 67 this.acceleration = new Point(); 68 this.age = 0; 69 } 70 Particle.prototype.initialize = function (x, y, dx, dy) { 71 this.position.x = x; 72 this.position.y = y; 73 this.velocity.x = dx; 74 this.velocity.y = dy; 75 this.acceleration.x = dx * settings.particles.effect; 76 this.acceleration.y = dy * settings.particles.effect; 77 this.age = 0; 78 }; 79 Particle.prototype.update = function (deltaTime) { 80 this.position.x += this.velocity.x * deltaTime; 81 this.position.y += this.velocity.y * deltaTime; 82 this.velocity.x += this.acceleration.x * deltaTime; 83 this.velocity.y += this.acceleration.y * deltaTime; 84 this.age += deltaTime; 85 }; 86 Particle.prototype.draw = function (context, image) { 87 function ease(t) { 88 return --t * t * t + 1; 89 } 90 let size = image.width * ease(this.age / settings.particles.duration); 91 context.globalAlpha = 1 - this.age / settings.particles.duration; 92 context.drawImage( 93 image, 94 this.position.x - size / 2, 95 this.position.y - size / 2, 96 size, 97 size 98 ); 99 }; 100 return Particle; 101})(); 102 103let ParticlePool = (function () { 104 let particles, 105 firstActive = 0, 106 firstFree = 0, 107 duration = settings.particles.duration; 108 109 function ParticlePool(length) { 110 particles = new Array(length); 111 for (let i = 0; i < particles.length; i++) particles[i] = new Particle(); 112 } 113 ParticlePool.prototype.add = function (x, y, dx, dy) { 114 particles[firstFree].initialize(x, y, dx, dy); 115 firstFree++; 116 if (firstFree == particles.length) firstFree = 0; 117 if (firstActive == firstFree) firstActive++; 118 if (firstActive == particles.length) firstActive = 0; 119 }; 120 ParticlePool.prototype.update = function (deltaTime) { 121 let i; 122 if (firstActive < firstFree) { 123 for (i = firstActive; i < firstFree; i++) particles[i].update(deltaTime); 124 } 125 if (firstFree < firstActive) { 126 for (i = firstActive; i < particles.length; i++) 127 particles[i].update(deltaTime); 128 for (i = 0; i < firstFree; i++) particles[i].update(deltaTime); 129 } 130 while (particles[firstActive].age >= duration && firstActive != firstFree) { 131 firstActive++; 132 if (firstActive == particles.length) firstActive = 0; 133 } 134 }; 135 ParticlePool.prototype.draw = function (context, image) { 136 if (firstActive < firstFree) { 137 for (i = firstActive; i < firstFree; i++) 138 particles[i].draw(context, image); 139 } 140 if (firstFree < firstActive) { 141 for (i = firstActive; i < particles.length; i++) 142 particles[i].draw(context, image); 143 for (i = 0; i < firstFree; i++) particles[i].draw(context, image); 144 } 145 }; 146 return ParticlePool; 147})(); 148 149(function (canvas) { 150 let context = canvas.getContext("2d"), 151 particles = new ParticlePool(settings.particles.length), 152 particleRate = settings.particles.length / settings.particles.duration, 153 time; 154 155 function pointOnHeart(t) { 156 return new Point( 157 160 * Math.pow(Math.sin(t), 3), 158 130 * Math.cos(t) - 159 50 * Math.cos(2 * t) - 160 20 * Math.cos(3 * t) - 161 10 * Math.cos(4 * t) + 162 25 163 ); 164 } 165 166 let image = (function () { 167 let canvas = document.createElement("canvas"), 168 context = canvas.getContext("2d"); 169 canvas.width = settings.particles.size; 170 canvas.height = settings.particles.size; 171 172 function to(t) { 173 let point = pointOnHeart(t); 174 point.x = 175 settings.particles.size / 2 + (point.x * settings.particles.size) / 350; 176 point.y = 177 settings.particles.size / 2 - (point.y * settings.particles.size) / 350; 178 return point; 179 } 180 181 context.beginPath(); 182 let t = -Math.PI; 183 let point = to(t); 184 context.moveTo(point.x, point.y); 185 while (t < Math.PI) { 186 t += 0.01; 187 point = to(t); 188 context.lineTo(point.x, point.y); 189 } 190 context.closePath(); 191 context.fillStyle = "#ea80b0"; 192 context.fill(); 193 194 let image = new Image(); 195 image.src = canvas.toDataURL(); 196 return image; 197 })(); 198 199 function render() { 200 requestAnimationFrame(render); 201 let newTime = new Date().getTime() / 1000, 202 deltaTime = newTime - (time || newTime); 203 time = newTime; 204 205 context.clearRect(0, 0, canvas.width, canvas.height); 206 207 let amount = particleRate * deltaTime; 208 for (let i = 0; i < amount; i++) { 209 let pos = pointOnHeart(Math.PI - 2 * Math.PI * Math.random()); 210 let dir = pos.clone().length(settings.particles.velocity); 211 particles.add( 212 canvas.width / 2 + pos.x, 213 canvas.height / 2 - pos.y, 214 dir.x, 215 -dir.y 216 ); 217 } 218 219 particles.update(deltaTime); 220 particles.draw(context, image); 221 } 222 223 function onResize() { 224 canvas.width = canvas.clientWidth; 225 canvas.height = canvas.clientHeight; 226 } 227 window.onresize = onResize; 228 229 setTimeout(function () { 230 onResize(); 231 render(); 232 }, 10); 233})(document.getElementById("pinkboard"));

JavaScript Breakdown

Settings Configuration

The settings object stores all customizable parameters:

  • length: Number of particles (500) - more particles = more density
  • duration: Particle lifespan (2 seconds) - how long particles stay visible
  • velocity: Initial particle speed (100) - controls spread speed
  • effect: Gravity/deceleration (-0.75) - negative values slow particles down
  • size: Heart particle size (30px) - scales the rendered particles

RequestAnimationFrame Polyfill

Ensures cross-browser compatibility for smooth 60fps animations.

Point Class

A utility class for 2D vector operations (position, velocity, acceleration).

Methods: clone(), length(), and normalize() for vector math.

Particle Class

Each particle has position, velocity, acceleration, and age properties.

The draw() function applies easing to create smooth particle shrinkage.

Particles fade out gradually using globalAlpha opacity.

ParticlePool Class

Manages an object pool of particles for optimized memory usage.

Uses circular buffer technique to efficiently reuse particle instances.

Methods:

  • add(): Initializes a new particle with position and velocity
  • update(): Updates all active particles with deltaTime
  • draw(): Renders all particles to canvas

Mathematical Heart Shape

pointOnHeart(t) uses parametric equations to define the heart curve.

The formula creates a smooth, mathematically perfect heart outline.

Canvas Rendering Loop

render() function runs continuously at 60fps.

Particles are generated from random points on the heart outline.

Each particle receives a direction and velocity emanating outward.

The canvas is cleared and redrawn each frame for smooth animation.

Responsive Behavior

onResize() function adjusts canvas dimensions when window resizes.

Ensures the animation fills the entire viewport on all devices.

Performance Optimization

The particle pool prevents memory leaks from continuous particle creation.

DeltaTime calculations ensure smooth animation regardless of frame rate.

This results in a smooth, performant heart animation that runs beautifully on all modern browsers.

Love this component?

Explore more components and build amazing UIs.

View All Components