创意生日祝福网页代码:从动态文字到烟花特效

一、代码整体概述

这是一段用 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 功能

  1. 滚动文字效果
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);
  1. 爱心粒子动画效果
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);
  1. 烟花和文字特效
// 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>

六、运行展示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

糖炒狗子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值