效果演示
这段代码是一个使用 HTML、CSS 和 JavaScript 实现的网页,主要功能是在页面上展示一个动态的心形图案,由许多粒子组成。
HTML
<canvas id="pinkboard"></canvas>
<div class="text">
<span style="font-size: 16px;color: #FF416C;">X ♥ X</span>
<span style="font-style: 12px;color: #FF416C;font-weight: 600;">I Love You</span>
</div>
- canvas元素,其id为 “pinkboard”,用于绘制图形。
- div元素,类名为 “text”,包含两个span元素,用于显示表白文字。
CSS
<style>
html,
body {
height: 100%;
padding: 0;
margin: 0;
background: #f7d6ff;
background-image: linear-gradient(to bottom right, #91defe, #99c0f9, #bdb6ec, #d7b3e3, #efb3d5, #f9bccc);
}
canvas {
position: absolute;
width: 100%;
height: 100%;
}
.text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: -20px;
font-size: 30px;
color: #ea80b0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
- html, body:设置了页面的高度为100%,并清除了内边距和外边距,同时设置了背景为线性渐变的颜色。
- canvas:通过绝对定位覆盖整个页面,设置了宽度和高度为 100%。
- .text:绝对定位在页面中心,设置了字体大小和颜色,使用 flex 布局使文字居中显示。
JavaScript
<script>
const settings = {
particles: {
length: 400, // maximum amount of particles
duration: 2, // particle duration in sec
velocity: 90, // particle velocity in pixels/sec
effect: -0.75, // play with this for a nice effect
size: 30, // particle size in pixels
},
};
// Polyfill for requestAnimationFrame and cancelAnimationFrame
(function () {
let b = 0;
const c = ["ms", "moz", "webkit", "o"];
for (let a = 0; a < c.length && !window.requestAnimationFrame; ++a) {
window.requestAnimationFrame = window[c[a] + "RequestAnimationFrame"];
window.cancelAnimationFrame = window[c[a] + "CancelAnimationFrame"] || window[c[a] + "CancelRequestAnimationFrame"];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function (h, e) {
const d = new Date().getTime();
const f = Math.max(0, 16 - (d - b));
const g = window.setTimeout(function () {
h(d + f);
}, f);
b = d + f;
return g;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function (d) {
clearTimeout(d);
};
}
})();
class Point {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
clone() {
return new Point(this.x, this.y);
}
length(length) {
if (typeof length === "undefined") {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
this.normalize();
this.x *= length;
this.y *= length;
return this;
}
normalize() {
const length = this.length();
this.x /= length;
this.y /= length;
return this;
}
}
class Particle {
constructor() {
this.position = new Point();
this.velocity = new Point();
this.acceleration = new Point();
this.age = 0;
}
initialize(x, y, dx, dy) {
this.position.x = x;
this.position.y = y;
this.velocity.x = dx;
this.velocity.y = dy;
this.acceleration.x = dx * settings.particles.effect;
this.acceleration.y = dy * settings.particles.effect;
this.age = 0;
}
update(deltaTime) {
this.position.x += this.velocity.x * deltaTime;
this.position.y += this.velocity.y * deltaTime;
this.velocity.x += this.acceleration.x * deltaTime;
this.velocity.y += this.acceleration.y * deltaTime;
this.age += deltaTime;
}
draw(context, image) {
function ease(t) {
return (-t) * t * t + 1;
}
const size = image.width * ease(this.age / settings.particles.duration);
context.globalAlpha = 1 - this.age / settings.particles.duration;
context.drawImage(image, this.position.x - size / 2, this.position.y - size / 2, size, size);
}
}
class ParticlePool {
constructor(length) {
this.particles = new Array(length);
for (let i = 0; i < this.particles.length; i++) {
this.particles[i] = new Particle();
}
this.firstActive = 0;
this.firstFree = 0;
this.duration = settings.particles.duration;
}
add(x, y, dx, dy) {
this.particles[this.firstFree].initialize(x, y, dx, dy);
this.firstFree++;
if (this.firstFree === this.particles.length) this.firstFree = 0;
if (this.firstActive === this.firstFree) this.firstActive++;
if (this.firstActive === this.particles.length) this.firstActive = 0;
}
update(deltaTime) {
let i;
if (this.firstActive < this.firstFree) {
for (i = this.firstActive; i < this.firstFree; i++) {
this.particles[i].update(deltaTime);
}
} else if (this.firstFree < this.firstActive) {
for (i = this.firstActive; i < this.particles.length; i++) {
this.particles[i].update(deltaTime);
}
for (i = 0; i < this.firstFree; i++) {
this.particles[i].update(deltaTime);
}
}
while (this.particles[this.firstActive].age >= this.duration && this.firstActive !== this.firstFree) {
this.firstActive++;
if (this.firstActive === this.particles.length) this.firstActive = 0;
}
}
draw(context, image) {
if (this.firstActive < this.firstFree) {
for (let i = this.firstActive; i < this.firstFree; i++) {
this.particles[i].draw(context, image);
}
} else if (this.firstFree < this.firstActive) {
for (let i = this.firstActive; i < this.particles.length; i++) {
this.particles[i].draw(context, image);
}
for (let i = 0; i < this.firstFree; i++) {
this.particles[i].draw(context, image);
}
}
}
}
(function (canvas) {
const context = canvas.getContext("2d");
const particles = new ParticlePool(settings.particles.length);
const particleRate = settings.particles.length / settings.particles.duration; // particles/sec
let time;
function pointOnHeart(t) {
return new Point(
160 * Math.pow(Math.sin(t), 3),
130 * Math.cos(t) - 50 * Math.cos(2 * t) - 20 * Math.cos(3 * t) - 10 * Math.cos(4 * t) + 25
);
}
const image = (function () {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = settings.particles.size;
canvas.height = settings.particles.size;
function to(t) {
const point = pointOnHeart(t);
point.x = settings.particles.size / 2 + point.x * settings.particles.size / 350;
point.y = settings.particles.size / 2 - point.y * settings.particles.size / 350;
return point;
}
context.beginPath();
let t = -Math.PI;
let point = to(t);
context.moveTo(point.x, point.y);
while (t < Math.PI) {
t += 0.01; // baby steps!
point = to(t);
context.lineTo(point.x, point.y);
}
context.closePath();
context.fillStyle = "#FF416C";
context.fill();
const image = new Image();
image.src = canvas.toDataURL();
return image;
})();
function render() {
requestAnimationFrame(render);
const newTime = new Date().getTime() / 1000;
const deltaTime = newTime - (time || newTime);
time = newTime;
context.clearRect(0, 0, canvas.width, canvas.height);
const amount = particleRate * deltaTime;
for (let i = 0; i < amount; i++) {
const pos = pointOnHeart(Math.PI - 2 * Math.PI * Math.random());
const dir = pos.clone().length(settings.particles.velocity);
particles.add(canvas.width / 2 + pos.x, canvas.height / 2 - pos.y, dir.x, -dir.y);
}
particles.update(deltaTime);
particles.draw(context, image);
}
function onResize() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
window.onresize = onResize;
setTimeout(() => {
onResize();
render();
}, 10);
})(document.getElementById("pinkboard"));
</script>
- 首先定义了一个settings对象,包含粒子的相关设置,如最大数量、持续时间、速度、效果和大小。
- 接着是一个自执行函数,用于为不支持requestAnimationFrame和cancelAnimationFrame的浏览器提供 polyfill。
- 定义了Point类:1、构造函数接受初始的x和y坐标,创建一个点对象。2、clone方法用于创建当前点的副本。3、length方法可以获取点的长度,也可以设置长度。4、normalize方法用于将点归一化,使其长度为 1。
- 定义了Particle类:1、构造函数初始化粒子的位置、速度、加速度和年龄。2、initialize方法用于设置粒子的初始状态。3、update方法根据时间更新粒子的位置、速度和年龄。4、draw方法根据粒子的年龄和大小绘制粒子,使用了一个缓动函数来调整粒子的大小和透明度。
- 定义了ParticlePool类:1、构造函数创建一个指定长度的粒子数组,初始化一些变量。2、add方法用于向粒子池中添加一个新的粒子。3、update方法更新粒子池中所有活动粒子的状态,并移除过期的粒子。4、draw方法绘制粒子池中所有活动粒子。
- 最后是一个自执行函数:1、获取canvas元素的上下文。2、创建一个粒子池对象和一个用于绘制粒子的图像。3、pointOnHeart函数用于计算心形曲线上的点。4、render函数是动画的核心,它使用requestAnimationFrame实现动画循环,根据时间更新粒子状态并绘制粒子。5、onResize函数用于在窗口大小改变时调整canvas的大小。6、为窗口的onresize事件绑定onResize函数,并在延迟 10 毫秒后调用onResize和render函数启动动画。
代码(一键复制)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表白爱心♥</title>
<style>
html,
body {
height: 100%;
padding: 0;
margin: 0;
background: #f7d6ff;
background-image: linear-gradient(to bottom right, #91defe, #99c0f9, #bdb6ec, #d7b3e3, #efb3d5, #f9bccc);
}
canvas {
position: absolute;
width: 100%;
height: 100%;
}
.text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: -20px;
font-size: 30px;
color: #ea80b0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<canvas id="pinkboard"></canvas>
<div class="text">
<span style="font-size: 16px;color: #FF416C;">X ♥ X</span>
<span style="font-style: 12px;color: #FF416C;font-weight: 600;">I Love You</span>
</div>
<script>
const settings = {
particles: {
length: 400, // maximum amount of particles
duration: 2, // particle duration in sec
velocity: 90, // particle velocity in pixels/sec
effect: -0.75, // play with this for a nice effect
size: 30, // particle size in pixels
},
};
// Polyfill for requestAnimationFrame and cancelAnimationFrame
(function () {
let b = 0;
const c = ["ms", "moz", "webkit", "o"];
for (let a = 0; a < c.length && !window.requestAnimationFrame; ++a) {
window.requestAnimationFrame = window[c[a] + "RequestAnimationFrame"];
window.cancelAnimationFrame = window[c[a] + "CancelAnimationFrame"] || window[c[a] + "CancelRequestAnimationFrame"];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function (h, e) {
const d = new Date().getTime();
const f = Math.max(0, 16 - (d - b));
const g = window.setTimeout(function () {
h(d + f);
}, f);
b = d + f;
return g;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function (d) {
clearTimeout(d);
};
}
})();
class Point {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
clone() {
return new Point(this.x, this.y);
}
length(length) {
if (typeof length === "undefined") {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
this.normalize();
this.x *= length;
this.y *= length;
return this;
}
normalize() {
const length = this.length();
this.x /= length;
this.y /= length;
return this;
}
}
class Particle {
constructor() {
this.position = new Point();
this.velocity = new Point();
this.acceleration = new Point();
this.age = 0;
}
initialize(x, y, dx, dy) {
this.position.x = x;
this.position.y = y;
this.velocity.x = dx;
this.velocity.y = dy;
this.acceleration.x = dx * settings.particles.effect;
this.acceleration.y = dy * settings.particles.effect;
this.age = 0;
}
update(deltaTime) {
this.position.x += this.velocity.x * deltaTime;
this.position.y += this.velocity.y * deltaTime;
this.velocity.x += this.acceleration.x * deltaTime;
this.velocity.y += this.acceleration.y * deltaTime;
this.age += deltaTime;
}
draw(context, image) {
function ease(t) {
return (-t) * t * t + 1;
}
const size = image.width * ease(this.age / settings.particles.duration);
context.globalAlpha = 1 - this.age / settings.particles.duration;
context.drawImage(image, this.position.x - size / 2, this.position.y - size / 2, size, size);
}
}
class ParticlePool {
constructor(length) {
this.particles = new Array(length);
for (let i = 0; i < this.particles.length; i++) {
this.particles[i] = new Particle();
}
this.firstActive = 0;
this.firstFree = 0;
this.duration = settings.particles.duration;
}
add(x, y, dx, dy) {
this.particles[this.firstFree].initialize(x, y, dx, dy);
this.firstFree++;
if (this.firstFree === this.particles.length) this.firstFree = 0;
if (this.firstActive === this.firstFree) this.firstActive++;
if (this.firstActive === this.particles.length) this.firstActive = 0;
}
update(deltaTime) {
let i;
if (this.firstActive < this.firstFree) {
for (i = this.firstActive; i < this.firstFree; i++) {
this.particles[i].update(deltaTime);
}
} else if (this.firstFree < this.firstActive) {
for (i = this.firstActive; i < this.particles.length; i++) {
this.particles[i].update(deltaTime);
}
for (i = 0; i < this.firstFree; i++) {
this.particles[i].update(deltaTime);
}
}
while (this.particles[this.firstActive].age >= this.duration && this.firstActive !== this.firstFree) {
this.firstActive++;
if (this.firstActive === this.particles.length) this.firstActive = 0;
}
}
draw(context, image) {
if (this.firstActive < this.firstFree) {
for (let i = this.firstActive; i < this.firstFree; i++) {
this.particles[i].draw(context, image);
}
} else if (this.firstFree < this.firstActive) {
for (let i = this.firstActive; i < this.particles.length; i++) {
this.particles[i].draw(context, image);
}
for (let i = 0; i < this.firstFree; i++) {
this.particles[i].draw(context, image);
}
}
}
}
(function (canvas) {
const context = canvas.getContext("2d");
const particles = new ParticlePool(settings.particles.length);
const particleRate = settings.particles.length / settings.particles.duration; // particles/sec
let time;
function pointOnHeart(t) {
return new Point(
160 * Math.pow(Math.sin(t), 3),
130 * Math.cos(t) - 50 * Math.cos(2 * t) - 20 * Math.cos(3 * t) - 10 * Math.cos(4 * t) + 25
);
}
const image = (function () {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = settings.particles.size;
canvas.height = settings.particles.size;
function to(t) {
const point = pointOnHeart(t);
point.x = settings.particles.size / 2 + point.x * settings.particles.size / 350;
point.y = settings.particles.size / 2 - point.y * settings.particles.size / 350;
return point;
}
context.beginPath();
let t = -Math.PI;
let point = to(t);
context.moveTo(point.x, point.y);
while (t < Math.PI) {
t += 0.01; // baby steps!
point = to(t);
context.lineTo(point.x, point.y);
}
context.closePath();
context.fillStyle = "#FF416C";
context.fill();
const image = new Image();
image.src = canvas.toDataURL();
return image;
})();
function render() {
requestAnimationFrame(render);
const newTime = new Date().getTime() / 1000;
const deltaTime = newTime - (time || newTime);
time = newTime;
context.clearRect(0, 0, canvas.width, canvas.height);
const amount = particleRate * deltaTime;
for (let i = 0; i < amount; i++) {
const pos = pointOnHeart(Math.PI - 2 * Math.PI * Math.random());
const dir = pos.clone().length(settings.particles.velocity);
particles.add(canvas.width / 2 + pos.x, canvas.height / 2 - pos.y, dir.x, -dir.y);
}
particles.update(deltaTime);
particles.draw(context, image);
}
function onResize() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
window.onresize = onResize;
setTimeout(() => {
onResize();
render();
}, 10);
})(document.getElementById("pinkboard"));
</script>
</body>
</html>