新年春联-粒子变幻效果【附源码】

一:效果展示

本项目通过粒子系统和状态机动画创造了春节对联动态循环

二:主要功能

  1. 粒子系统: 使用5000个彩色粒子组成对联文字
  2. 动画循环: 包含四个阶段的动画循环:
  • 散开阶段:粒子从文字位置散开到屏幕边缘
  • 聚集阶段:粒子从边缘飞回形成新的对联文字
  • 展示阶段:文字稳定显示并带有微妙的脉冲效果
  • 淡出阶段:文字粒子逐渐淡出并散开
  1. 多副对联切换: 自动循环展示三副不同的春节对联
  2. 对联背景: 为每副对联添加红色背景和金色边框装饰

1. 代码分析

(1)粒子系统核心类

javascript 复制代码
class Particle {
    constructor() {
        this.reset();
        this.size = Math.random() * 3 + 1;
        const hue = Math.random() * 10 + 50; // 金黄色调
        this.color = `hsl(${hue}, 100%, ${Math.random() * 20 + 70}%)`;
        this.easing = Math.random() * 0.05 + 0.02;
        this.speed = Math.random() * 2 + 1;
    }
    
    // 粒子更新逻辑
    update() {
        if (!this.isTextParticle) return;
        
        if (this.active) {
            // 向目标位置移动
            const dx = this.targetX - this.x;
            const dy = this.targetY - this.y;
            this.x += dx * this.easing;
            this.y += dy * this.easing;
        } else {
            // 散开逻辑
            if (animationState === 'scatter' || animationState === 'fadeOut') {
                const dx = this.scatterTarget.x - this.x;
                const dy = this.scatterTarget.y - this.y;
                const dist = Math.sqrt(dx * dx + dy * dy);
                const speed = this.speed * (1 + (1 - dist/canvas.width) * 2);
                this.x += dx / dist * speed;
                this.y += dy / dist * speed;
            }
        }
    }
    
    // 绘制粒子
    draw() {
        ctx.globalAlpha = this.opacity * this.baseOpacity;
        ctx.fillStyle = this.color;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
        ctx.fill();
    }
}

(2)对联文字粒子化处理

javascript 复制代码
function calculateCoupletPositions(couplet) {
    // 创建临时canvas检测文字像素
    const tempCanvas = document.createElement('canvas');
    const tempCtx = tempCanvas.getContext('2d');
    tempCanvas.width = canvas.width;
    tempCanvas.height = canvas.height;
    
    // 设置字体样式
    tempCtx.font = `bold ${fontSizeVertical}px ${fontFamily}`;
    tempCtx.fillStyle = 'white';
    tempCtx.textAlign = 'left';
    tempCtx.textBaseline = 'top';
    
    // 绘制横批
    tempCtx.font = `bold ${fontSizeHorizontal}px ${fontFamily}`;
    const topWidth = tempCtx.measureText(couplet.top).width;
    const topX = centerX - topWidth / 2;
    tempCtx.fillText(couplet.top, topX, topY);
    
    // 绘制左联
    tempCtx.font = `bold ${fontSizeVertical}px ${fontFamily}`;
    for (let i = 0; i < couplet.left.length; i++) {
        tempCtx.fillText(couplet.left[i], leftX, leftYStart + i * verticalSpacing);
    }
    
    // 绘制右联
    for (let i = 0; i < couplet.right.length; i++) {
        tempCtx.fillText(couplet.right[i], rightX, rightYStart + i * verticalSpacing);
    }
    
    // 检测文字像素位置
    const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
    const data = imageData.data;
    const textParticles = [];
    
    for (let y = 0; y < tempCanvas.height; y += spacing) {
        for (let x = 0; x < tempCanvas.width; x += spacing) {
            const index = (y * tempCanvas.width + x) * 4;
            if (data[index + 3] > 0) { 
                textParticles.push({x, y});
            }
        }
    }
    
    // 将文字像素位置分配给粒子
    const shuffled = [...textParticles].sort(() => 0.5 - Math.random());
    particles.forEach((particle, i) => {
        if (i < shuffled.length) {
            const pos = shuffled[i];
            particle.targetX = pos.x;
            particle.targetY = pos.y;
            particle.active = true;
            particle.baseOpacity = 1;
            particle.isTextParticle = true;
        }
    });
    
    return shuffled.length;
}

(3)动画状态机

javascript 复制代码
let animationState = 'scatter';
const animationDurations = {
    scatter: 1500,    // 散开动画时长
    gather: 2500,     // 聚集动画时长
    show: 4000,       // 展示时长
    fadeOut: 2000     // 淡出时长
};

function animate(timestamp) {
    if (!animationStartTime) animationStartTime = timestamp;
    const elapsed = timestamp - animationStartTime;
    
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // 绘制对联背景
    drawCoupletBackgrounds(0.8);
    
    // 根据当前状态处理动画
    if (animationState === 'scatter') {
        // 散开逻辑
        if (progress >= 1) {
            animationState = 'gather';
            currentCoupletIndex = (currentCoupletIndex + 1) % coupletList.length;
            textParticlesCount = calculateCoupletPositions(coupletList[currentCoupletIndex]);
        }
    }
    else if (animationState === 'gather') {
        // 聚集逻辑
        if (progress >= 1) {
            animationState = 'show';
        }
    }
    else if (animationState === 'show') {
        // 展示逻辑(脉冲效果)
        if (progress >= 1) {
            animationState = 'fadeOut';
        }
    }
    else if (animationState === 'fadeOut') {
        // 淡出逻辑
        if (progress >= 1) {
            animationState = 'scatter';
        }
    }
    
    requestAnimationFrame(animate);
}

2. 视觉效果特点

  • 粒子颜色:使用金黄色调营造氛围
  • 动态效果:文字粒子在状态转换时有流畅的动画效果
  • 自适应布局:对联位置会根据屏幕尺寸自动调整
  • 背景装饰:红色背景配金色边框,增强春节氛围

三:完整代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>新春对联 ------ 粒子变幻</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #000;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            font-family: 'Arial', sans-serif;
        }
        canvas {
            display: block;
        }

    </style>
</head>
<body>
    <div class="loading"></div>
    <canvas id="canvas"></canvas>

    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');

        function resizeCanvas() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        }
        window.addEventListener('resize', resizeCanvas);
        resizeCanvas();

        let coupletBackgrounds = {
            top: { x: 0, y: 0, width: 0, height: 0 },
            left: { x: 0, y: 0, width: 0, height: 0 },
            right: { x: 0, y: 0, width: 0, height: 0 }
        };

        class Particle {
            constructor() {
                this.reset();
                this.size = Math.random() * 3 + 1;
                const hue = Math.random() * 10 + 50;
                const saturation = 100;
                const lightness = Math.random() * 20 + 70;
                this.color = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
                this.easing = Math.random() * 0.05 + 0.02;
                this.active = false;
                this.speed = Math.random() * 2 + 1;
                this.baseOpacity = 0;
                this.opacity = 0;
                this.scatterTarget = { x: 0, y: 0 };
                this.isTextParticle = false;
                this.hadTarget = false;
            }

            reset() {
                this.x = 0;
                this.y = 0;
                this.baseX = 0;
                this.baseY = 0;
                this.targetX = 0;
                this.targetY = 0;
                this.hadTarget = false;
            }

            calculateScatterTarget() {
                const edge = Math.floor(Math.random() * 4);
                if (edge === 0) {
                    this.scatterTarget.x = Math.random() * canvas.width;
                    this.scatterTarget.y = -20 - Math.random() * 50;
                } else if (edge === 1) {
                    this.scatterTarget.x = canvas.width + 20 + Math.random() * 50;
                    this.scatterTarget.y = Math.random() * canvas.height;
                } else if (edge === 2) {
                    this.scatterTarget.x = Math.random() * canvas.width;
                    this.scatterTarget.y = canvas.height + 20 + Math.random() * 50;
                } else {
                    this.scatterTarget.x = -20 - Math.random() * 50;
                    this.scatterTarget.y = Math.random() * canvas.height;
                }
            }

            update() {
                if (!this.isTextParticle) return;

                if (this.active) {
                    const dx = this.targetX - this.x;
                    const dy = this.targetY - this.y;
                    this.x += dx * this.easing;
                    this.y += dy * this.easing;

                    if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
                        this.x = this.targetX;
                        this.y = this.targetY;
                    }
                } else {
                    if (animationState === 'scatter' || animationState === 'fadeOut') {
                        const dx = this.scatterTarget.x - this.x;
                        const dy = this.scatterTarget.y - this.y;
                        const dist = Math.sqrt(dx * dx + dy * dy);

                        const speed = this.speed * (1 + (1 - dist/canvas.width) * 2);

                        if (dist > 1) {
                            this.x += dx / dist * speed;
                            this.y += dy / dist * speed;
                        }
                    }
                }
            }

            draw() {
                if (!this.isTextParticle && this.baseOpacity === 0) return;

                ctx.globalAlpha = this.opacity * this.baseOpacity;
                ctx.fillStyle = this.color;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                ctx.fill();
                ctx.globalAlpha = 1;
            }
        }

        function drawCoupletBackgrounds(opacity) {
            if (opacity <= 0) return;

            ctx.save();
            ctx.globalAlpha = opacity;

            const topBg = coupletBackgrounds.top;
            ctx.fillStyle = '#c00';
            ctx.fillRect(topBg.x - 30, topBg.y - 15, topBg.width + 60, topBg.height + 30);
            ctx.strokeStyle = '#ffd700';
            ctx.lineWidth = 3;
            ctx.strokeRect(topBg.x - 30, topBg.y - 15, topBg.width + 60, topBg.height + 30);

            const leftBg = coupletBackgrounds.left;
            ctx.fillStyle = '#c00';
            ctx.fillRect(leftBg.x - 30, leftBg.y - 15, leftBg.width + 60, leftBg.height + 30);
            ctx.strokeStyle = '#ffd700';
            ctx.lineWidth = 3;
            ctx.strokeRect(leftBg.x - 30, leftBg.y - 15, leftBg.width + 60, leftBg.height + 30);

            const rightBg = coupletBackgrounds.right;
            ctx.fillStyle = '#c00';
            ctx.fillRect(rightBg.x - 30, rightBg.y - 15, rightBg.width + 60, rightBg.height + 30);
            ctx.strokeStyle = '#ffd700';
            ctx.lineWidth = 3;
            ctx.strokeRect(rightBg.x - 30, rightBg.y - 15, rightBg.width + 60, rightBg.height + 30);

            ctx.restore();
        }

        const particles = [];
        const particleCount = 5000;

        for (let i = 0; i < particleCount; i++) {
            const particle = new Particle();
            particle.calculateScatterTarget();
            particles.push(particle);
        }

        const coupletList = [
            {
                top: "常乐安宁",
                left: "常怀欣喜心常乐",
                right: "欣悦四季心安宁"
            },
            {
                top: "欢度春节",
                left: "迎喜迎春迎富贵",
                right: "接财接福接平安"
            },
            {
                top: "吉祥如意",
                left: "春风入喜财入户",
                right: "岁月更新福满门"
            }
        ];
        let currentCoupletIndex = 0;
        const fontSizeVertical = 60;
        const fontSizeHorizontal = 70;
        const fontFamily = "SimHei, Microsoft YaHei, Arial";

        function calculateCoupletPositions(couplet) {
            const tempCanvas = document.createElement('canvas');
            const tempCtx = tempCanvas.getContext('2d');
            tempCanvas.width = canvas.width;
            tempCanvas.height = canvas.height;
            tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);

            const centerX = canvas.width / 2;
            const centerY = canvas.height / 2 + 40;
            const verticalSpacing = 80;
            const horizontalOffset = 300;

            const textParticles = [];
            const coupletHeight = couplet.left.length * verticalSpacing;
            const topY = centerY - coupletHeight/2 - 80;

            tempCtx.font = `bold ${fontSizeHorizontal}px ${fontFamily}`;
            const topWidth = tempCtx.measureText(couplet.top).width;
            const topX = centerX - topWidth / 2;

            coupletBackgrounds.top = {
                x: topX,
                y: topY,
                width: topWidth,
                height: fontSizeHorizontal
            };

            tempCtx.fillStyle = 'white';
            tempCtx.textAlign = 'left';
            tempCtx.textBaseline = 'top';
            tempCtx.fillText(couplet.top, topX, topY);

            tempCtx.font = `bold ${fontSizeVertical}px ${fontFamily}`;
            const leftYStart = centerY - (couplet.left.length * verticalSpacing) / 2;
            const leftEdgeDistance = centerX - horizontalOffset;
            const rightEdgeDistance = canvas.width - (centerX + horizontalOffset);

            const uniformEdgeDistance = Math.min(leftEdgeDistance, rightEdgeDistance);
            const leftX = uniformEdgeDistance + 35 - 50;
            const rightX = canvas.width - uniformEdgeDistance - 50;

            coupletBackgrounds.left = {
                x: leftX,
                y: leftYStart,
                width: tempCtx.measureText(couplet.left[0]).width,
                height: couplet.left.length * verticalSpacing
            };

            for (let i = 0; i < couplet.left.length; i++) {
                const char = couplet.left[i];
                const y = leftYStart + i * verticalSpacing;
                tempCtx.fillText(char, leftX, y);
            }

            const rightYStart = centerY - (couplet.right.length * verticalSpacing) / 2;

            coupletBackgrounds.right = {
                x: rightX,
                y: rightYStart,
                width: tempCtx.measureText(couplet.right[0]).width,
                height: couplet.right.length * verticalSpacing
            };

            for (let i = 0; i < couplet.right.length; i++) {
                const char = couplet.right[i];
                const y = rightYStart + i * verticalSpacing;
                tempCtx.fillText(char, rightX, y);
            }

            const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
            const data = imageData.data;
            const spacing = 3;

            for (let y = 0; y < tempCanvas.height; y += spacing) {
                for (let x = 0; x < tempCanvas.width; x += spacing) {
                    const index = (y * tempCanvas.width + x) * 4;
                    if (data[index + 3] > 0) {
                        textParticles.push({x, y});
                    }
                }
            }

            const shuffled = [...textParticles].sort(() => 0.5 - Math.random());

            particles.forEach(particle => {
                particle.reset();
                particle.active = false;
                particle.baseOpacity = 0;
                particle.opacity = 0;
                particle.isTextParticle = false;
            });

            particles.forEach((particle, i) => {
                if (i < shuffled.length) {
                    const pos = shuffled[i];
                    particle.targetX = pos.x;
                    particle.targetY = pos.y;
                    particle.active = true;
                    particle.baseOpacity = 1;
                    particle.opacity = 1;
                    particle.isTextParticle = true;
                    particle.calculateScatterTarget();
                }
            });

            return shuffled.length;
        }

        let animationState = 'scatter';
        let textParticlesCount = 0;
        let animationStartTime = 0;
        const animationDurations = {
            scatter: 1500,
            gather: 2500,
            show: 4000,
            fadeOut: 2000
        };

        function animate(timestamp) {
            if (!animationStartTime) animationStartTime = timestamp;
            const elapsed = timestamp - animationStartTime;

            ctx.clearRect(0, 0, canvas.width, canvas.height);

            let bgOpacity = 0.8;

            drawCoupletBackgrounds(bgOpacity);

            if (animationState === 'scatter') {
                const progress = Math.min(elapsed / animationDurations.scatter, 1);

                particles.forEach(particle => {
                    if (particle.isTextParticle) {
                        if (particle.active) {
                            if (progress > 0.3) {
                                const p = (progress - 0.3) / 0.7;
                                const easeP = easeOutCubic(Math.min(p, 1));

                                particle.x = particle.targetX + (particle.scatterTarget.x - particle.targetX) * easeP;
                                particle.y = particle.targetY + (particle.scatterTarget.y - particle.targetY) * easeP;
                                particle.opacity = 1 - easeP;

                                if (p >= 1) {
                                    particle.active = false;
                                    particle.opacity = 0;
                                }
                            }
                        }
                    }
                    particle.update();
                    particle.draw();
                });

                if (progress >= 1) {
                    animationState = 'gather';
                    animationStartTime = timestamp;
                    currentCoupletIndex = (currentCoupletIndex + 1) % coupletList.length;
                    textParticlesCount = calculateCoupletPositions(coupletList[currentCoupletIndex]);
                }
            }
            else if (animationState === 'gather') {
                const progress = Math.min(elapsed / animationDurations.gather, 1);
                const easeP = easeOutCubic(progress);

                particles.forEach((particle, i) => {
                    if (particle.isTextParticle) {
                        if (i < textParticlesCount) {
                            particle.active = true;
                            particle.opacity = Math.min(progress * 2, 1);
                            particle.baseOpacity = 1;

                            if (!particle.hadTarget) {
                                particle.x = particle.scatterTarget.x;
                                particle.y = particle.scatterTarget.y;
                                particle.hadTarget = true;
                            }
                        }
                    }
                    particle.update();
                    particle.draw();
                });

                if (progress >= 1) {
                    animationState = 'show';
                    animationStartTime = timestamp;
                    particles.forEach(p => {
                        if (p.isTextParticle) {
                            p.hadTarget = false;
                        }
                    });
                }
            }
            else if (animationState === 'show') {
                const progress = Math.min(elapsed / animationDurations.show, 1);

                particles.forEach((particle, i) => {
                    if (particle.isTextParticle && i < textParticlesCount) {
                        particle.active = true;
                        particle.baseOpacity = 1;
                        particle.opacity = 1;
                        const pulse = Math.sin(timestamp * 0.003 + i * 0.1) * 0.4 + 0.8;
                        particle.size = 2 + pulse;
                    }
                    particle.update();
                    particle.draw();
                });

                if (progress >= 1) {
                    animationState = 'fadeOut';
                    animationStartTime = timestamp;
                }
            }
            else if (animationState === 'fadeOut') {
                const progress = Math.min(elapsed / animationDurations.fadeOut, 1);
                const easeP = easeInCubic(progress);

                particles.forEach((particle, i) => {
                    if (particle.isTextParticle && i < textParticlesCount) {
                        const dx = particle.scatterTarget.x - particle.x;
                        const dy = particle.scatterTarget.y - particle.y;
                        const dist = Math.sqrt(dx * dx + dy * dy);

                        const speed = particle.speed * (1 + (1 - dist/canvas.width) * 3);

                        if (dist > 1) {
                            particle.x += dx / dist * speed * (1 + easeP * 2);
                            particle.y += dy / dist * speed * (1 + easeP * 2);
                        }

                        particle.opacity = 1 - easeP;

                        if (progress >= 1) {
                            particle.active = false;
                            particle.opacity = 0;
                        }
                    }
                    particle.update();
                    particle.draw();
                });

                if (progress >= 1) {
                    animationState = 'scatter';
                    animationStartTime = timestamp;
                }
            }

            requestAnimationFrame(animate);
        }

        function easeOutCubic(t) {
            return 1 - Math.pow(1 - t, 3);
        }

        function easeInCubic(t) {
            return t * t * t;
        }
        document.querySelector('.loading').style.display = 'none';
        textParticlesCount = calculateCoupletPositions(coupletList[currentCoupletIndex]);
        animationState = 'gather';
        animationStartTime = performance.now();
        requestAnimationFrame(animate);
    </script>
</body>
</html>

以上为本项目的完整代码,复制粘贴即可使用

在这里提前祝大家小年快乐!!!

相关推荐
ZHOUPUYU6 小时前
PHP 8.3网关优化:我用JIT将QPS提升300%的真实踩坑录
开发语言·php
寻寻觅觅☆10 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
萧曵 丶11 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
银烛木11 小时前
黑马程序员前端h5+css3
前端·css·css3
听海边涛声11 小时前
CSS3 图片模糊处理
前端·css·css3
l1t11 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
anOnion11 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计
赶路人儿11 小时前
Jsoniter(java版本)使用介绍
java·开发语言
Amumu1213812 小时前
Vue3扩展(二)
前端·javascript·vue.js
NEXT0612 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试