这个页面通过 canvas 和 JavaScript 实现了一个动态的爱心动画效果。每个爱心的大小、颜色和位置会根据时间动态变化,形成一种动态的视觉效果。用户可以通过点击画布重新生成动画。
大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信我,我会发送完整的压缩包给你。
演示效果
HTML&CSS
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>公众号关注:前端Hardy</title>
<style>
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: #009999;
}
</style>
</head>
<body>
<script>
console.clear();
let canvas, canvasCtx;
let canvasSize = [0, 0], scale = 1;
let state;
requestAnimationFrame(main);
function main() {
canvas = document.createElement('canvas');
document.body.appendChild(canvas);
document.body.style.margin = '0';
canvas.style.display = 'block';
canvasCtx = canvas.getContext('2d');
checkResizeAndInit();
reset();
canvas.addEventListener('mousemove', (e) => {
state.pointer.pos[0] = e.offsetX;
state.pointer.pos[1] = e.offsetY;
});
canvas.addEventListener('click', reset);
window.addEventListener('resize', reset);
requestAnimationFrame(mainLoop);
function mainLoop() {
tick();
requestAnimationFrame(mainLoop);
}
}
function reset() {
state = {
time: 0,
timeDelta: 1 / 60,
pointer: { pos: [0, 0] },
hearts: [],
};
const min = Math.min(canvasSize[0], canvasSize[1]);
const step = min / 13;
const center = [
canvasSize[0] / 2,
canvasSize[1] / 2
];
drawHeart(
center,
min,
Math.PI * state.time * 0,
null, null,
);
state.hearts.length = 0;
for (let y = 0; y < canvasSize[1]; y += step) {
for (let x = 0; x < canvasSize[0]; x += step) {
const isInside = canvasCtx.isPointInPath(x, y);
if (!isInside) continue;
const dist = Math.hypot(x - center[0], y - center[1]);
state.hearts.push({
pos: [
x + step * Math.random() * 0.25,
y + step * Math.random() * 0.25,
],
w: step * (0.25 + 0.75 * (1 - (dist / min))),
a: (Math.random() - 0.5) * Math.PI * 0.5,
timeOffset: (1 - (dist / min)) * 2,
timeScale: 1,
color1: '#ff0000',
color2: '#ff00ff',
});
}
}
}
function tick() {
checkResizeAndInit();
canvasCtx.fillStyle = `rgba(0, 0, 0, ${1})`;
canvasCtx.fillRect(0, 0, canvasSize[0], canvasSize[1]);
doIt();
state.time += state.timeDelta;
}
function checkResizeAndInit() {
if (
window.innerWidth === canvasSize[0] &&
window.innerHeight === canvasSize[1]
) return;
canvasSize[0] = canvas.width = window.innerWidth;
canvasSize[1] = canvas.height = window.innerHeight;
}
function doIt() {
const { timeDelta } = state;
for (const heart of state.hearts) {
const time = state.time * heart.timeScale + heart.timeOffset;
const s = ((Math.sin(time * Math.PI) + 1) / 2) ** 0.5 * 0.5 + 0.5;
drawHeart(
heart.pos,
heart.w * s,
heart.a,
`color-mix(in hsl, ${heart.color1}, ${heart.color2} ${100 * Math.abs(((time / 3) % 2) - 1)}%)`,
);
}
if (0) {
const s = ((Math.sin(state.time * Math.PI) + 1) / 2) ** 0.5 * 0.5 + 0.5;
drawHeart(
[canvasSize[0] / 2, canvasSize[1] / 2],
256 * s,
Math.PI * state.time * 0,
'color-mix(in hsl, #00ff00, #ff0000 50%)',
);
}
}
function drawHeart(pos, w, a, color, color2) {
const s = w / 92;
canvasCtx.save();
canvasCtx.translate(pos[0], pos[1]);
canvasCtx.rotate(a);
canvasCtx.translate(0, 12 * s);
canvasCtx.beginPath();
canvasCtx.ellipse(
-14 * s, -16 * s,
25 * s, 32 * s,
Math.PI * -0.25,
Math.PI * 1, Math.PI * 0,
);
canvasCtx.ellipse(
+14 * s, -16 * s,
25 * s, 32 * s,
Math.PI * +0.25,
Math.PI * 1, Math.PI * 0,
);
canvasCtx.quadraticCurveTo(
+14 * s, 20 * s,
0 * s, 32 * s,
);
canvasCtx.closePath();
if (color) {
canvasCtx.strokeStyle = color;
canvasCtx.stroke();
}
if (color2) {
canvasCtx.fillStyle = color2;
canvasCtx.fill();
}
canvasCtx.restore();
}
function drawSkull(pos, w, a, color, color2) {
const s = w / 92;
canvasCtx.save();
canvasCtx.translate(pos[0], pos[1]);
canvasCtx.rotate(a);
canvasCtx.strokeStyle = '#7f7f7f';
canvasCtx.beginPath();
canvasCtx.arc(
0, 0,
w * 0.5,
0, Math.PI * 2,
);
canvasCtx.stroke();
canvasCtx.translate(0, 12 * s);
canvasCtx.beginPath();
canvasCtx.ellipse(
0 * s, -20 * s,
44 * s, 32 * s,
Math.PI * 0,
Math.PI * 0.70, Math.PI * 0.3,
);
canvasCtx.lineTo(+20 * s, 28 * s);
canvasCtx.lineTo(+20 * s, 0 * s);
canvasCtx.arc(
0 * s, -10 * s,
10 * s,
Math.PI * 0.4, Math.PI * -1.4,
true,
);
canvasCtx.lineTo(-20 * s, 28 * s);
canvasCtx.closePath();
if (color) {
canvasCtx.strokeStyle = color;
canvasCtx.stroke();
}
if (color2) {
canvasCtx.fillStyle = color2;
canvasCtx.fill();
}
canvasCtx.restore();
}
function rotateByVector(out, a, v, origin, s) {
const rx = v[0];
const ry = v[1];
const x = a[0] - origin[0];
const y = a[1] - origin[1];
out[0] = origin[0] + (x * rx - y * ry) * s;
out[1] = origin[1] + (y * rx + x * ry) * s;
return out;
}
</script>
</body>
</html>
初始化和主循环
requestAnimationFrame(main);
function main() {
canvas = document.createElement('canvas');
document.body.appendChild(canvas);
document.body.style.margin = '0';
canvas.style.display = 'block';
canvasCtx = canvas.getContext('2d');
checkResizeAndInit();
reset();
canvas.addEventListener('mousemove', (e) => {
state.pointer.pos[0] = e.offsetX;
state.pointer.pos[1] = e.offsetY;
});
canvas.addEventListener('click', reset);
window.addEventListener('resize', reset);
requestAnimationFrame(mainLoop);
function mainLoop() {
tick();
requestAnimationFrame(mainLoop);
}
}
- main():初始化函数,创建 canvas 元素并附加到页面上。
- 设置 canvas 的样式,使其全屏显示。
- 初始化 canvas 的 2D 绘图上下文(canvasCtx)。
- 添加事件监听器,用于处理鼠标移动、点击和窗口大小调整。
- 启动主循环 mainLoop(),通过 requestAnimationFrame 实现持续的动画效果。
状态管理
function reset() {
state = {
time: 0,
timeDelta: 1 / 60,
pointer: { pos: [0, 0] },
hearts: [],
};
const min = Math.min(canvasSize[0], canvasSize[1]);
const step = min / 13;
const center = [
canvasSize[0] / 2,
canvasSize[1] / 2
];
drawHeart(
center,
min,
Math.PI * state.time * 0,
null, null,
);
state.hearts.length = 0;
for (let y = 0; y < canvasSize[1]; y += step) {
for (let x = 0; x < canvasSize[0]; x += step; x += step) {
const isInside = canvasCtx.isPointInPath(x, y);
if (!isInside) continue;
const dist = Math.hypot(x - center[0], y - center[1]);
state.hearts.push({
pos: [
x + step * Math.random() * 0.25,
y + step * Math.random() * 0.25,
],
w: step * (0.25 + 0.75 * (1 - (dist / min))),
a: (Math.random() - 0.5) * Math.PI * 0.5,
timeOffset: (1 - (dist / min)) * 2,
timeScale: 1,
color1: '#ff0000',
color2: '#ff00ff',
});
}
}
}
- reset():初始化或重置动画状态。
- 创建一个状态对象 state,包含时间、鼠标位置和爱心数组。
- 使用 drawHeart() 函数绘制一个大爱心作为参考路径。
- 遍历画布上的每个点,检查是否在爱心路径内,如果是,则创建一个小爱心对象并添加到数组中。
- 每个小爱心的大小、位置和颜色会根据其与中心的距离动态计算。
动画更新
function tick() {
checkResizeAndInit();
canvasCtx.fillStyle = `rgba(0, 0, 0, ${1})`;
canvasCtx.fillRect(0, 0, canvasSize[0], canvasSize[1]);
doIt();
state.time += state.timeDelta;
}
- tick():每帧调用的函数,负责更新动画状态。
- 检查窗口大小是否变化,并更新画布大小。
- 清空画布,绘制新的帧。
- 调用 doIt() 函数绘制所有爱心。
- 更新时间。
爱心绘制
function drawHeart(pos, w, a, color, color2) {
const s = w / 92;
canvasCtx.save();
canvasCtx.translate(pos[0], pos[1]);
canvasCtx.rotate(a);
canvasCtx.translate(0, 12 * s);
canvasCtx.beginPath();
canvasCtx.ellipse(
-14 * s, -16 * s,
25 * s, 32 * s,
Math.PI * -0.25,
Math.PI * 1, Math.PI * 0,
);
canvasCtx.ellipse(
+14 * s, -16 * s,
25 * s, 32 * s,
Math.PI * +0.25,
Math.PI * 1, Math.PI * 0,
);
canvasCtx.quadraticCurveTo(
+14 * s, 20 * s,
0 * s, 32 * s,
);
canvasCtx.closePath();
if (color) {
canvasCtx.strokeStyle = color;
canvasCtx.stroke();
}
if (color2) {
canvasCtx.fillStyle = color2;
canvasCtx.fill();
}
canvasCtx.restore();
}
- drawHeart():绘制一个爱心形状。
- 使用 translate 和 rotate 将画布的原点移动到指定位置并旋转。
- 使用 ellipse 和 quadraticCurveTo 绘制爱心的两个半圆和连接部分。
- 使用 stroke() 和 fill() 绘制边框和填充颜色。
动态效果
function doIt() {
const { timeDelta } = state;
for (const heart of state.hearts) {
const time = state.time * heart.timeScale + heart.timeOffset;
const s = ((Math.sin(time * Math.PI) + 1) / 2) ** 0.5 * 0.5 + 0.5;
drawHeart(
heart.pos,
heart.w * s,
heart.a,
`color-mix(in hsl, ${heart.color1}, ${heart.color2} ${100 * Math.abs(((time / 3) % 2) - 1)}%)`,
);
}
}
- doIt():绘制所有动态爱心。
- 遍历 state.hearts 数组,根据当前时间和偏移量计算每个爱心的缩放比例。
- 使用 color-mix 动态混合颜色。
- 调用 drawHeart() 绘制每个爱心。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!