Implementing a floating heart animation for a "like" feature requires moving a heart image along a Bezier curve trajectory while gradually fading it out. The core challenge lies in calculating a series of points along the Bezier curve.
Calculating Bezier Curve Points
The mathematical formula for an n-order Bezier curve allows us to compute coordinates at any given parameter t. For a cubic Bezier curve, we need four control points and a t value between 0 and 1 to calculate a specific point on the curve. By varyinng t from 0 to 1, we generate a sequence of points that form the curve.
The following implementation calculates points along a Bezier curve:
class BezierCurve {
constructor(controlPoints) {
this.controlPoints = controlPoints;
this.order = controlPoints.length - 1;
}
factorial(n) {
if (n <= 1) return 1;
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
getPoint(t) {
let x = 0, y = 0;
const n = this.order;
this.controlPoints.forEach((point, index) => {
const coefficient = this.factorial(n) /
(this.factorial(index) * this.factorial(n - index));
const term = Math.pow(1 - t, n - index) * Math.pow(t, index);
x += coefficient * point.x * term;
y += coefficient * point.y * term;
});
return { x, y };
}
generatePoints(segments = 100) {
const points = [];
for (let i = 0; i <= segments; i++) {
points.push(this.getPoint(i / segments));
}
return points;
}
}
This class accepts an array of control points and generates a specified number of points along the curve by iterating t from 0 to 1.
Implementing the Floating Heart Animation
With the curve points calculated, we can animate a heart image moving along the trajectory. The opacity decreases as the heart progresses along the path, creating a fading effect.
class FloatingHeart {
constructor(context, image, startX, startY) {
this.context = context;
this.image = image;
this.width = 24;
this.height = 24;
const randomOffset = () => (Math.random() - 0.5) * 60;
const curve = new BezierCurve([
{ x: startX, y: startY },
{ x: startX - 20 + randomOffset(), y: startY - 40 },
{ x: startX + 30 + randomOffset(), y: startY - 100 },
{ x: startX - 50 + randomOffset(), y: startY - 180 }
]);
this.trajectory = curve.generatePoints(90);
this.currentIndex = 0;
this.lastPosition = null;
}
render() {
if (this.currentIndex >= this.trajectory.length) {
this.clear();
return false;
}
const position = this.trajectory[this.currentIndex];
this.clear();
this.context.save();
const fadeRatio = (this.trajectory.length - this.currentIndex) / 30;
this.context.globalAlpha = Math.max(0, Math.min(1, fadeRatio));
this.context.drawImage(
this.image,
position.x - this.width / 2,
position.y - this.height / 2,
this.width,
this.height
);
this.context.restore();
this.lastPosition = position;
this.currentIndex++;
return true;
}
clear() {
if (this.lastPosition) {
this.context.clearRect(
this.lastPosition.x - this.width / 2 - 1,
this.lastPosition.y - this.height / 2 - 1,
this.width + 2,
this.height + 2
);
}
}
}
The animation loop handles multiple hearts simultaneously:
const canvas = document.getElementById('animationCanvas');
const ctx = canvas.getContext('2d');
const heartImage = document.getElementById('heartImage');
let activeHearts = [];
canvas.addEventListener('click', (event) => {
const rect = canvas.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const clickY = event.clientY - rect.top;
activeHearts.push(new FloatingHeart(ctx, heartImage, clickX, clickY));
});
function animate() {
activeHearts = activeHearts.filter(heart => heart.render());
requestAnimationFrame(animate);
}
animate();
When the user clicks anywhere on the canvas, a new heart instance is created at that position. The animation loop continuously renders each heart, advancing it along its trajectory while managing opacity based on its progress. Heart are removed from the active array once they complete their animation.