console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>3D立体爱心</title>
<style>
body { margin: 0; padding: 0; background: #000; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let W = window.innerWidth;
let H = window.innerHeight;
canvas.width = W;
canvas.height = H;
const CANVAS_CENTER_X = W / 2;
const CANVAS_CENTER_Y = H / 2;
const IMAGE_ENLARGE = Math.min(W, H) / 45; // 自动适配手机屏幕大小
const HEART_COLOR = "#ff7171"; // 爱心颜色
// 核心公式:生成爱心形状的坐标
function heartFunction(t, shrinkRatio = IMAGE_ENLARGE) {
let x = 16 * Math.pow(Math.sin(t), 3);
let y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
x *= shrinkRatio;
y *= shrinkRatio;
x += CANVAS_CENTER_X;
y += CANVAS_CENTER_Y;
return { x: Math.floor(x), y: Math.floor(y) };
}
// 内部散落效果
function scatterInside(x, y, beta = 0.15) {
let ratioX = -beta * Math.log(Math.random());
let ratioY = -beta * Math.log(Math.random());
let dx = ratioX * (x - CANVAS_CENTER_X);
let dy = ratioY * (y - CANVAS_CENTER_Y);
return { x: x - dx, y: y - dy };
}
// 缩小边缘扩散
function shrink(x, y, ratio) {
let force = -1 / (Math.pow(Math.pow(x - CANVAS_CENTER_X, 2) + Math.pow(y - CANVAS_CENTER_Y, 2), 0.6));
let dx = ratio * force * (x - CANVAS_CENTER_X);
let dy = ratio * force * (y - CANVAS_CENTER_Y);
return { x: x - dx, y: y - dy };
}
// 周期曲线,控制心跳节奏
function curve(p) {
return 2 * (2 * Math.sin(4 * p)) / (2 * Math.PI);
}
let _points = [];
let _edge_diffusion_points = [];
let _center_diffusion_points = [];
let all_points = {}; // 存储每一帧的坐标
const GENERATE_FRAME = 20;
// 初始化构建点阵
function build(number) {
for (let i = 0; i < number; i++) {
let t = Math.random() * 2 * Math.PI;
let p = heartFunction(t);
_points.push(p);
}
// 边缘扩散
for (let p of _points) {
for (let i = 0; i < 3; i++) {
let ep = scatterInside(p.x, p.y, 0.05);
_edge_diffusion_points.push(ep);
}
}
// 中心扩散
for (let i = 0; i < 4000; i++) {
let p = _points[Math.floor(Math.random() * _points.length)];
let cp = scatterInside(p.x, p.y, 0.17);
_center_diffusion_points.push(cp);
}
}
function calcPosition(x, y, ratio) {
let force = 1 / (Math.pow(Math.pow(x - CANVAS_CENTER_X, 2) + Math.pow(y - CANVAS_CENTER_Y, 2), 0.520));
let dx = ratio * force * (x - CANVAS_CENTER_X) + Math.floor(Math.random() * 3) - 1;
let dy = ratio * force * (y - CANVAS_CENTER_Y) + Math.floor(Math.random() * 3) - 1;
return { x: x - dx, y: y - dy };
}
// 计算每一帧的坐标
function calc(generateFrame) {
let ratio = 10 * curve(generateFrame / 10 * Math.PI);
let haloRadius = Math.floor(4 + 6 * (1 + curve(generateFrame / 10 * Math.PI)));
let haloNumber = Math.floor(3000 + 4000 * Math.abs(Math.pow(curve(generateFrame / 10 * Math.PI), 2)));
let framePoints = [];
// 光环
let heartHaloPoint = new Set();
for (let i = 0; i < haloNumber; i++) {
let t = Math.random() * 2 * Math.PI;
let p = heartFunction(t, IMAGE_ENLARGE * 1.05);
let sp = shrink(p.x, p.y, haloRadius);
let key = sp.x + "," + sp.y;
if (!heartHaloPoint.has(key)) {
heartHaloPoint.add(key);
sp.x += Math.floor(Math.random() * 29) - 14;
sp.y += Math.floor(Math.random() * 29) - 14;
let size = Math.random() < 0.3 ? 1 : 2;
framePoints.push({ x: sp.x, y: sp.y, size: size });
}
}
// 轮廓
for (let p of _points) {
let cp = calcPosition(p.x, p.y, ratio);
framePoints.push({ x: cp.x, y: cp.y, size: Math.floor(Math.random() * 3) + 1 });
}
// 内容(边缘扩散)
for (let p of _edge_diffusion_points) {
let cp = calcPosition(p.x, p.y, ratio);
framePoints.push({ x: cp.x, y: cp.y, size: Math.floor(Math.random() * 2) + 1 });
}
// 内容(中心扩散)
for (let p of _center_diffusion_points) {
let cp = calcPosition(p.x, p.y, ratio);
framePoints.push({ x: cp.x, y: cp.y, size: Math.floor(Math.random() * 2) + 1 });
}
all_points[generateFrame] = framePoints;
}
// 初始化运行
build(2000);
for (let frame = 0; frame < GENERATE_FRAME; frame++) {
calc(frame);
}
let currentFrame = 0;
// 渲染函数
function render() {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = HEART_COLOR;
let points = all_points[currentFrame % GENERATE_FRAME];
for (let p of points) {
ctx.fillRect(p.x, p.y, p.size, p.size);
}
currentFrame++;
setTimeout(() => {
requestAnimationFrame(render);
}, 160); // 控制帧率,和原代码一致
}
render();
</script>
</body>
</html>