一、代码整体概述
这是一段用 HTML、CSS 和 JavaScript 编写的网页代码,其主要功能是通过多种视觉效果为用户送上生日祝福。网页以黑色为背景,首先展示动态滚动的文字效果,接着呈现爱心粒子动画,最后展示烟花特效并逐步显示生日祝福文字,整体营造出温馨且富有创意的生日氛围。
二、HTML 结构
<!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>Happy</title>
<style>
/* 此处省略 CSS 样式代码 */
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<canvas id="pinkboard"></canvas>
<div id="child"><h4></h4></div>
<script>
/* 此处省略 JavaScript 代码 */
</script>
</body>
</html>
三、CSS 样式
body {margin: 0;padding: 0;overflow: hidden;height: 100vh;background-color: #000;}
canvas {position: absolute;width: 100%;height: 100%;}
#child {position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);text-align: center;}
h4 {font-family: "STKaiti", cursive;font-size: clamp(24px, 5vw, 40px);color: #FF69B4;margin: 0;}
@media (max-width: 768px) {}
四、JavaScript 功能
- 滚动文字效果
const scrollCanvas = document.getElementById('canvas');
const scrollCtx = scrollCanvas.getContext('2d');
function setCanvasSize(canvas) {canvas.width = window.innerWidth;canvas.height = window.innerHeight;}
setCanvasSize(scrollCanvas);
const scrollText = 'Happy birthday';
const scrollFontSize = Math.min(16, window.innerWidth / 20);
const scrollColumns = Math.floor(scrollCanvas.width / scrollFontSize);
const scrollDrops = Array.from({ length: scrollColumns }, () => 1);
let currentIndex = 0;
function drawScrollText() {
scrollCtx.fillStyle = 'rgba(0, 0, 0, 0.05)';
scrollCtx.fillRect(0, 0, scrollCanvas.width, scrollCanvas.height);
scrollCtx.fillStyle = '#FF69B4';
scrollCtx.font = `${scrollFontSize}px Arial`;
for (let i = 0; i < scrollDrops.length; i++) {
if (scrollDrops[i] * scrollFontSize > scrollCanvas.height || scrollDrops[i] === 1) {
if (currentIndex >= scrollText.length) {currentIndex = 0;}
const text = scrollText[currentIndex];
scrollCtx.fillText(text, i * scrollFontSize, scrollDrops[i] * scrollFontSize);
currentIndex++;
} else {
const adjustedIndex = (currentIndex - (scrollColumns - i)) % scrollText.length;
const text = scrollText[adjustedIndex >= 0 ? adjustedIndex : adjustedIndex + scrollText.length];
scrollCtx.fillText(text, i * scrollFontSize, scrollDrops[i] * scrollFontSize);
}
if (scrollDrops[i] * scrollFontSize > scrollCanvas.height || Math.random() > 0.95) {scrollDrops[i] = 0;}
scrollDrops[i]++;
}
}
setInterval(drawScrollText, 100);
- 爱心粒子动画效果
const heartCanvas = document.getElementById('pinkboard');
const heartCtx = heartCanvas.getContext('2d');
const particleSettings = {length: 500,duration: 2,velocity: 100,effect: -0.75,size: Math.min(20, window.innerWidth / 20)};
// 省略部分代码,用于兼容不同浏览器的 requestAnimationFrame 和 cancelAnimationFrame
class Point {
constructor(x = 0, y = 0) {this.x = x;this.y = y;}
clone() {return new Point(this.x, this.y);}
length(newLength) {
if (typeof newLength === 'undefined') {return Math.sqrt(this.x * this.x + this.y * this.y);}
const currentLength = this.length();
this.x = (this.x / currentLength) * newLength;
this.y = (this.y / currentLength) * newLength;
return this;
}
normalize() {const currentLength = this.length();this.x /= currentLength;this.y /= currentLength;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 * particleSettings.effect;
this.acceleration.y = dy * particleSettings.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 / particleSettings.duration);
context.globalAlpha = 1 - this.age / particleSettings.duration;
context.drawImage(image, this.position.x - size / 2, this.position.y - size / 2, size, size);
}
}
class ParticlePool {
constructor(length) {
this.particles = Array.from({ length }, () => new Particle());
this.firstActive = 0;
this.firstFree = 0;
this.duration = particleSettings.duration;
}
add(x, y, dx, dy) {
this.particles[this.firstFree].initialize(x, y, dx, dy);
this.firstFree = (this.firstFree + 1) % this.particles.length;
if (this.firstActive === this.firstFree) {this.firstActive = (this.firstActive + 1) % this.particles.length;}
}
update(deltaTime) {
if (this.firstActive < this.firstFree) {
for (let i = this.firstActive; i < this.firstFree; i++) {this.particles[i].update(deltaTime);}
} else {
for (let i = this.firstActive; i < this.particles.length; i++) {this.particles[i].update(deltaTime);}
for (let 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 = (this.firstActive + 1) % this.particles.length;
}
}
draw(context, image) {
if (this.firstActive < this.firstFree) {
for (let i = this.firstActive; i < this.firstFree; i++) {this.particles[i].draw(context, image);}
} else {
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 createHeartImage() {
const heartCanvas = document.createElement('canvas');
const heartCtx = heartCanvas.getContext('2d');
heartCanvas.width = particleSettings.size;
heartCanvas.height = particleSettings.size;
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
);
}
function to(t) {
const point = pointOnHeart(t);
point.x = particleSettings.size / 2 + point.x * (particleSettings.size / 350);
point.y = particleSettings.size / 2 - point.y * (particleSettings.size / 350);
return point;
}
heartCtx.beginPath();
let t = -Math.PI;
let point = to(t);
heartCtx.moveTo(point.x, point.y);
while (t < Math.PI) {
t += 0.01;
point = to(t);
heartCtx.lineTo(point.x, point.y);
}
heartCtx.closePath();
heartCtx.fillStyle = '#FF69B4';
heartCtx.fill();
const heartImage = new Image();
heartImage.src = heartCanvas.toDataURL();
return heartImage;
}
const heartImage = createHeartImage();
const particlePool = new ParticlePool(particleSettings.length);
const particleRate = particleSettings.length / particleSettings.duration;
let lastTime;
function render() {
requestAnimationFrame(render);
const currentTime = performance.now() / 1000;
const deltaTime = lastTime ? currentTime - lastTime : 0;
lastTime = currentTime;
heartCtx.clearRect(0, 0, heartCanvas.width, heartCanvas.height);
const particleCount = particleRate * deltaTime;
for (let i = 0; i < particleCount; i++) {
const t = Math.PI - 2 * Math.PI * Math.random();
const pos = pointOnHeart(t);
const dir = pos.clone().length(particleSettings.velocity);
particlePool.add(heartCanvas.width / 2 + pos.x, heartCanvas.height / 2 - pos.y, dir.x, -dir.y);
}
particlePool.update(deltaTime);
particlePool.draw(heartCtx, heartImage);
}
window.addEventListener('resize', () => {
setCanvasSize(scrollCanvas);
setCanvasSize(heartCanvas);
scrollFontSize = Math.min(16, window.innerWidth / 20);
particleSettings.size = Math.min(20, window.innerWidth / 20);
});
setCanvasSize(heartCanvas);
setTimeout(() => {render();}, 10);
- 烟花和文字特效
// 5 秒后隐藏爱心,显示烟花及文字
setTimeout(() => {
// 隐藏爱心画布
const heartCanvas = document.getElementById('pinkboard');
heartCanvas.style.display = 'none';
// 创建烟花画布并添加到页面中
const fireworkCanvas = document.createElement('canvas');
fireworkCanvas.id = 'fireworkCanvas';
document.body.appendChild(fireworkCanvas);
fireworkCanvas.width = window.innerWidth;
fireworkCanvas.height = window.innerHeight;
const fireworkCtx = fireworkCanvas.getContext('2d');
const width = fireworkCanvas.width;
const height = fireworkCanvas.height;
// 用于管理画布上下文的类
class GameCanvas {
constructor() {
this.ctx = fireworkCtx;
}
}
// 绘制圆形的函数
function circle(x, y, radius, color) {
fireworkCtx.beginPath();
fireworkCtx.arc(x, y, radius, 0, Math.PI * 2);
fireworkCtx.fillStyle = color;
fireworkCtx.fill();
}
// 绘制背景的函数
function background(color) {
fireworkCtx.fillStyle = color;
fireworkCtx.fillRect(0, 0, width, height);
}
// 将画布内容转换为图像的函数
function canvasToImage() {
const img = new Image();
img.src = fireworkCanvas.toDataURL();
return img;
}
var gc = new GameCanvas();
// 要显示的文字数组
var texts = ["嘿!", "亲爱的朋友", "今天是你的生日", "跟我一起数", "3", "2", "1", "生日快乐!"];
var currentTextIndex = 0;
// 文字缩放比例和烟花缩放比例
const textScale = 0.7;
const fireworkScale = 0.5;
// 文字大小
const textSize = 17 * textScale;
// 将初始文字转换为点的数组
var points = textToPoints(texts[currentTextIndex], textSize, "Anton");
// 存储标题粒子、烟花和普通粒子的数组
var titleParticles = [];
var fireworks = [];
var particles = [];
// 重力加速度
var gravity = 0.1;
// 延迟 1.5 秒后,每隔 0.5 秒创建一个新的烟花
setTimeout(function () {
setInterval(function () {
fireworks.push(new Firework(Math.random() * width, height, Math.random() - 0.5, -(Math.random() * 7 + 5)));
}, 500);
}, 1500);
// 添加一个初始的烟花(用于触发显示文字的粒子效果)
fireworks.push(new Firework(width / 2, height, 0, -9.5, 10 * fireworkScale, "gold", true));
// 主循环函数
function loop() {
// 持续绘制滚动文字作为背景(这里假设 drawScrollText 函数在其他地方已定义)
drawScrollText();
// 设置绘图的合成操作并绘制背景
gc.ctx.globalCompositeOperation = "source-over";
background("rgba(0, 0, 0, 0.1)");
gc.ctx.globalCompositeOperation = "lighter";
// 更新并渲染所有烟花
for (var i = 0; i < fireworks.length; i++) {
var firework = fireworks[i];
firework.update();
firework.render();
}
// 更新并渲染所有普通粒子
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
particle.update();
particle.render();
}
// 更新并渲染所有标题粒子
for (var i = 0; i < titleParticles.length; i++) {
var p = titleParticles[i];
p.update();
p.render();
}
// 当标题粒子和烟花都消失时,切换到下一段文字并添加新的烟花
if (titleParticles.length === 0 && fireworks.length === 0) {
currentTextIndex++;
if (currentTextIndex < texts.length) {
points = textToPoints(texts[currentTextIndex], textSize, "Anton");
fireworks.push(new Firework(width / 2, height, 0, -9.5, 10 * fireworkScale, "gold", true));
}
}
// 递归调用自身,实现动画循环
requestAnimationFrame(loop);
}
// 标题粒子类
function TitleParticle(x, y, vx, vy) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.ay = 0.2;
this.radius = 4 * fireworkScale;
this.maxHealth = 500;
this.health = 400;
// 更新粒子状态的方法
this.update = function () {
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.95;
this.vy *= 0.95;
this.vy += this.ay;
this.ay *= 0.95;
this.radius = (this.health / this.maxHealth) * 4 * fireworkScale;
this.health--;
if (this.health <= 0) {
titleParticles.splice(titleParticles.indexOf(this), 1);
}
}
// 渲染粒子的方法
this.render = function () {
circle(this.x, this.y, this.radius, "rgba(255, 255, 255, " + (this.health / this.maxHealth) + ")");
}
}
// 烟花类
function Firework(x, y, vx, vy, radius = 5 * fireworkScale, color = "white", title = false) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.radius = radius;
this.title = title;
this.color = color;
// 更新烟花状态的方法
this.update = function () {
this.x += this.vx;
this.y += this.vy;
this.vy += gravity;
if (this.vy >= 0) {
fireworks.splice(fireworks.indexOf(this), 1);
if (this.title) {
var scaleFactor = 0.3 * textScale;
for (var i = 0; i < points.length; i++) {
var p = points[i];
var v = {
x: (p.x - 60) * scaleFactor + (Math.random() - 0.5) * 0.1,
y: (p.y - 20) * scaleFactor + (Math.random() - 0.5) * 0.1
}
var particle = new TitleParticle(this.x, this.y, v.x, v.y);
titleParticles.push(particle);
}
} else {
var color = [Math.random() * 256 >> 0, Math.random() * 256 >> 0, Math.random() * 256 >> 0];
for (var i = 0; i < Math.PI * 2; i += 0.1) {
var power = (Math.random() + 0.5) * 4;
var vx = Math.cos(i) * power;
var vy = Math.sin(i) * power;
particles.push(new Particle(this.x, this.y, vx, vy, (Math.random() + 3) * fireworkScale, color));
}
}
}
}
// 渲染烟花的方法
this.render = function () {
circle(this.x, this.y, this.radius, this.color);
}
}
// 普通粒子类
function Particle(x, y, vx, vy, radius, color) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.life = 100;
this.color = color;
this.radius = radius;
// 更新粒子状态的方法
this.update = function () {
this.x += this.vx;
this.y += this.vy;
this.vy += gravity;
this.vx *= 0.95;
this.vy *= 0.95;
this.life--;
if (this.life <= 0) {
particles.splice(particles.indexOf(this), 1);
}
}
// 渲染粒子的方法
this.render = function () {
circle(this.x, this.y, 3 * (this.life / 100) * fireworkScale, "rgba(" + this.color[0] + ", " + this.color[1] + ", " + this.color[2] + ", " + (this.life / 100) + ")");
}
}
// 将文字转换为点的数组的函数
function textToPoints(text, textSize, font) {
var canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = textSize * 1.3;
var ctx = canvas.getContext("2d");
ctx.textBaseline = "middle";
ctx.font = textSize + "px " + font;
ctx.fillText(text, 0, canvas.height / 2);
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
var points = [];
var index = (x, y) => (x + canvas.width * y) * 4;
var threshold = 50;
for (var i = 0; i < data.length; i += 4) {
if (data[i + 3] > threshold) {
var p = {
x: (i / 4) % canvas.width + 20,
y: (i / 4) / canvas.width >> 0
};
if (data[index(p.x - 20 + 1, p.y) + 3] < threshold ||
data[index(p.x - 20 - 1, p.y) + 3] < threshold ||
data[index(p.x - 20, p.y + 1) + 3] < threshold ||
data[index(p.x - 20, p.y - 1) + 3] < threshold) {
points.push({
x: p.x,
y: p.y
});
}
}
}
return points;
}
// 启动主循环
loop();
}, 5000);
五、完整代码
<!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>Happy</title>
<style>
body {margin: 0;padding: 0;overflow: hidden;height: 100vh;background-color: #000;}
canvas {position: absolute;width: 100%;height: 100%;}
#child {position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);text-align: center;}
h4 {font-family: "STKaiti", cursive;font-size: clamp(24px, 5vw, 40px);color: #FF69B4;margin: 0;}
@media (max-width: 768px) {}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<canvas id="pinkboard"></canvas>
<div id="child"><h4></h4></div>
<script>
const scrollCanvas = document.getElementById('canvas');
const scrollCtx = scrollCanvas.getContext('2d');
function setCanvasSize(canvas) {canvas.width = window.innerWidth;canvas.height = window.innerHeight;}
setCanvasSize(scrollCanvas);
const scrollText = 'Happy birthday';
const scrollFontSize = Math.min(16, window.innerWidth / 20);
const scrollColumns = Math.floor(scrollCanvas.width / scrollFontSize);
const scrollDrops = Array.from({ length: scrollColumns }, () => 1);
let currentIndex = 0;
function drawScrollText() {
scrollCtx.fillStyle = 'rgba(0, 0, 0, 0.05)';
scrollCtx.fillRect(0, 0, scrollCanvas.width, scrollCanvas.height);
scrollCtx.fillStyle = '#FF69B4';
scrollCtx.font = `${scrollFontSize}px Arial`;
for (let i = 0; i < scrollDrops.length; i++) {
if (scrollDrops[i] * scrollFontSize > scrollCanvas.height || scrollDrops[i] === 1) {
if (currentIndex >= scrollText.length) {currentIndex = 0;}
const text = scrollText[currentIndex];
scrollCtx.fillText(text, i * scrollFontSize, scrollDrops[i] * scrollFontSize);
currentIndex++;
} else {
const adjustedIndex = (currentIndex - (scrollColumns - i)) % scrollText.length;
const text = scrollText[adjustedIndex >= 0 ? adjustedIndex : adjustedIndex + scrollText.length];
scrollCtx.fillText(text, i * scrollFontSize, scrollDrops[i] * scrollFontSize);
}
if (scrollDrops[i] * scrollFontSize > scrollCanvas.height || Math.random() > 0.95) {scrollDrops[i] = 0;}
scrollDrops[i]++;
}
}
setInterval(drawScrollText, 100);
const heartCanvas = document.getElementById('pinkboard');
const heartCtx = heartCanvas.getContext('2d');
const particleSettings = {length: 500,duration: 2,velocity: 100,effect: -0.75,size: Math.min(20, window.innerWidth / 20)};
(function () {
const vendors = ['ms', 'moz', 'webkit', 'o'];
for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function (callback) {return setTimeout(callback, 16);};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function (id) {clearTimeout(id);};
}
})();
class Point {
constructor(x = 0, y = 0) {this.x = x;this.y = y;}
clone() {return new Point(this.x, this.y);}
length(newLength) {
if (typeof newLength === 'undefined') {return Math.sqrt(this.x * this.x + this.y * this.y);}
const currentLength = this.length();
this.x = (this.x / currentLength) * newLength;
this.y = (this.y / currentLength) * newLength;
return this;
}
normalize() {const currentLength = this.length();this.x /= currentLength;this.y /= currentLength;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 * particleSettings.effect;
this.acceleration.y = dy * particleSettings.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 / particleSettings.duration);
context.globalAlpha = 1 - this.age / particleSettings.duration;
context.drawImage(image, this.position.x - size / 2, this.position.y - size / 2, size, size);
}
}
class ParticlePool {
constructor(length) {
this.particles = Array.from({ length }, () => new Particle());
this.firstActive = 0;
this.firstFree = 0;
this.duration = particleSettings.duration;
}
add(x, y, dx, dy) {
this.particles[this.firstFree].initialize(x, y, dx, dy);
this.firstFree = (this.firstFree + 1) % this.particles.length;
if (this.firstActive === this.firstFree) {this.firstActive = (this.firstActive + 1) % this.particles.length;}
}
update(deltaTime) {
if (this.firstActive < this.firstFree) {
for (let i = this.firstActive; i < this.firstFree; i++) {this.particles[i].update(deltaTime);}
} else {
for (let i = this.firstActive; i < this.particles.length; i++) {this.particles[i].update(deltaTime);}
for (let 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 = (this.firstActive + 1) % this.particles.length;
}
}
draw(context, image) {
if (this.firstActive < this.firstFree) {
for (let i = this.firstActive; i < this.firstFree; i++) {this.particles[i].draw(context, image);}
} else {
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 createHeartImage() {
const heartCanvas = document.createElement('canvas');
const heartCtx = heartCanvas.getContext('2d');
heartCanvas.width = particleSettings.size;
heartCanvas.height = particleSettings.size;
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
);
}
function to(t) {
const point = pointOnHeart(t);
point.x = particleSettings.size / 2 + point.x * (particleSettings.size / 350);
point.y = particleSettings.size / 2 - point.y * (particleSettings.size / 350);
return point;
}
heartCtx.beginPath();
let t = -Math.PI;
let point = to(t);
heartCtx.moveTo(point.x, point.y);
while (t < Math.PI) {
t += 0.01;
point = to(t);
heartCtx.lineTo(point.x, point.y);
}
heartCtx.closePath();
heartCtx.fillStyle = '#FF69B4';
heartCtx.fill();
const heartImage = new Image();
heartImage.src = heartCanvas.toDataURL();
return heartImage;
}
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 heartImage = createHeartImage();
const particlePool = new ParticlePool(particleSettings.length);
const particleRate = particleSettings.length / particleSettings.duration;
let lastTime;
function render() {
requestAnimationFrame(render);
const currentTime = performance.now() / 1000;
const deltaTime = lastTime ? currentTime - lastTime : 0;
lastTime = currentTime;
heartCtx.clearRect(0, 0, heartCanvas.width, heartCanvas.height);
const particleCount = particleRate * deltaTime;
for (let i = 0; i < particleCount; i++) {
const t = Math.PI - 2 * Math.PI * Math.random();
const pos = pointOnHeart(t);
const dir = pos.clone().length(particleSettings.velocity);
particlePool.add(heartCanvas.width / 2 + pos.x, heartCanvas.height / 2 - pos.y, dir.x, -dir.y);
}
particlePool.update(deltaTime);
particlePool.draw(heartCtx, heartImage);
}
window.addEventListener('resize', () => {
setCanvasSize(scrollCanvas);
setCanvasSize(heartCanvas);
scrollFontSize = Math.min(16, window.innerWidth / 20);
particleSettings.size = Math.min(20, window.innerWidth / 20);
});
setCanvasSize(heartCanvas);
setTimeout(() => {render();}, 10);
// 5 秒后隐藏爱心,显示烟花及文字
setTimeout(() => {
heartCanvas.style.display = 'none';
// 烟花及文字逻辑
const fireworkCanvas = document.createElement('canvas');
fireworkCanvas.id = 'fireworkCanvas';
document.body.appendChild(fireworkCanvas);
fireworkCanvas.width = window.innerWidth;
fireworkCanvas.height = window.innerHeight;
const fireworkCtx = fireworkCanvas.getContext('2d');
const width = fireworkCanvas.width;
const height = fireworkCanvas.height;
class GameCanvas {
constructor() {
this.ctx = fireworkCtx;
}
}
function circle(x, y, radius, color) {
fireworkCtx.beginPath();
fireworkCtx.arc(x, y, radius, 0, Math.PI * 2);
fireworkCtx.fillStyle = color;
fireworkCtx.fill();
}
function background(color) {
fireworkCtx.fillStyle = color;
fireworkCtx.fillRect(0, 0, width, height);
}
function canvasToImage() {
const img = new Image();
img.src = fireworkCanvas.toDataURL();
return img;
}
var gc = new GameCanvas();
var texts = ["嘿!", "亲爱的朋友", "今天是你的生日", "跟我一起数", "3", "2", "1", "生日快乐!"];
var currentTextIndex = 0;
const textScale = 0.7;
const fireworkScale = 0.5;
const textSize = 17 * textScale;
var points = textToPoints(texts[currentTextIndex], textSize, "Anton");
var titleParticles = [];
var fireworks = [];
var particles = [];
var gravity = 0.1;
setTimeout(function () {
setInterval(function () {
fireworks.push(new Firework(Math.random() * width, height, Math.random() - 0.5, -(Math.random() * 7 + 5)));
}, 500);
}, 1500);
fireworks.push(new Firework(width / 2, height, 0, -9.5, 10 * fireworkScale, "gold", true));
function loop() {
// 持续绘制滚动文字作为背景
drawScrollText();
gc.ctx.globalCompositeOperation = "source-over";
background("rgba(0, 0, 0, 0.1)");
gc.ctx.globalCompositeOperation = "lighter";
for (var i = 0; i < fireworks.length; i++) {
var firework = fireworks[i];
firework.update();
firework.render();
}
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
particle.update();
particle.render();
}
for (var i = 0; i < titleParticles.length; i++) {
var p = titleParticles[i];
p.update();
p.render();
}
if (titleParticles.length === 0 && fireworks.length === 0) {
currentTextIndex++;
if (currentTextIndex < texts.length) {
points = textToPoints(texts[currentTextIndex], textSize, "Anton");
fireworks.push(new Firework(width / 2, height, 0, -9.5, 10 * fireworkScale, "gold", true));
}
}
requestAnimationFrame(loop);
}
function TitleParticle(x, y, vx, vy) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.ay = 0.2;
this.radius = 4 * fireworkScale;
this.maxHealth = 500;
this.health = 400;
this.update = function () {
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.95;
this.vy *= 0.95;
this.vy += this.ay;
this.ay *= 0.95;
this.radius = (this.health / this.maxHealth) * 4 * fireworkScale;
this.health--;
if (this.health <= 0) {
titleParticles.splice(titleParticles.indexOf(this), 1);
}
}
this.render = function () {
circle(this.x, this.y, this.radius, "rgba(255, 255, 255, " + (this.health / this.maxHealth) + ")");
}
}
function Firework(x, y, vx, vy, radius = 5 * fireworkScale, color = "white", title = false) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.radius = radius;
this.title = title;
this.color = color;
this.update = function () {
this.x += this.vx;
this.y += this.vy;
this.vy += gravity;
if (this.vy >= 0) {
fireworks.splice(fireworks.indexOf(this), 1);
if (this.title) {
var scaleFactor = 0.3 * textScale;
for (var i = 0; i < points.length; i++) {
var p = points[i];
var v = {
x: (p.x - 60) * scaleFactor + (Math.random() - 0.5) * 0.1,
y: (p.y - 20) * scaleFactor + (Math.random() - 0.5) * 0.1
}
var particle = new TitleParticle(this.x, this.y, v.x, v.y);
titleParticles.push(particle);
}
} else {
var color = [Math.random() * 256 >> 0, Math.random() * 256 >> 0, Math.random() * 256 >> 0];
for (var i = 0; i < Math.PI * 2; i += 0.1) {
var power = (Math.random() + 0.5) * 4;
var vx = Math.cos(i) * power;
var vy = Math.sin(i) * power;
particles.push(new Particle(this.x, this.y, vx, vy, (Math.random() + 3) * fireworkScale, color));
}
}
}
}
this.render = function () {
circle(this.x, this.y, this.radius, this.color);
}
}
function Particle(x, y, vx, vy, radius, color) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.life = 100;
this.color = color;
this.radius = radius;
this.update = function () {
this.x += this.vx;
this.y += this.vy;
this.vy += gravity;
this.vx *= 0.95;
this.vy *= 0.95;
this.life--;
if (this.life <= 0) {
particles.splice(particles.indexOf(this), 1);
}
}
this.render = function () {
circle(this.x, this.y, 3 * (this.life / 100) * fireworkScale, "rgba(" + this.color[0] + ", " + this.color[1] + ", " + this.color[2] + ", " + (this.life / 100) + ")");
}
}
function textToPoints(text, textSize, font) {
var canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = textSize * 1.3;
var ctx = canvas.getContext("2d");
ctx.textBaseline = "middle";
ctx.font = textSize + "px " + font;
ctx.fillText(text, 0, canvas.height / 2);
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
var points = [];
var index = (x, y) => (x + canvas.width * y) * 4;
var threshold = 50;
for (var i = 0; i < data.length; i += 4) {
if (data[i + 3] > threshold) {
var p = {
x: (i / 4) % canvas.width + 20,
y: (i / 4) / canvas.width >> 0
};
if (data[index(p.x - 20 + 1, p.y) + 3] < threshold ||
data[index(p.x - 20 - 1, p.y) + 3] < threshold ||
data[index(p.x - 20, p.y + 1) + 3] < threshold ||
data[index(p.x - 20, p.y - 1) + 3] < threshold) {
points.push({
x: p.x,
y: p.y
});
}
}
}
return points;
}
loop();
}, 5000);
</script>
</body>
</html>