效果演示
这段代码是一个使用 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, |
| duration: 2, |
| velocity: 90, |
| effect: -0.75, |
| size: 30, |
| }, |
| }; |
| |
| |
| (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; |
| 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; |
| 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, |
| duration: 2, |
| velocity: 90, |
| effect: -0.75, |
| size: 30, |
| }, |
| }; |
| |
| |
| (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; |
| 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; |
| 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> |
复制