AI Summary
正在生成摘要...

这是一个集成了雪花飘落、烟花绽放(自动+交互)的网页特效插件。

主要功能

  1. 自动烟花:页面加载后自动播放,具有慢速扩散和悬浮感。
  2. 鼠标/触摸交互
    • 短按:释放小烟花。
    • 长按蓄力:出现光圈,蓄力越久烟花越大。
  3. 雪花背景:全屏雪花飘落。
  4. 滚动淡出:当用户向下滚动网页时,烟花特效会自动变透明,避免遮挡正文内容(雪花保持常驻)。
  5. 日期锁定:默认设定了 2025-12-30 后自动开启,在此之前默认隐藏(可通过左下角隐形开关手动开启)。
  6. 功能开关:代码顶部有配置区,可独立关闭雪花、自动烟花或点击交互。

如何使用

  1. 直接运行:下载 .html 文件,用浏览器打开即可。
  2. 集成到网站
    • <style> 标签内的代码放入 CSS 配置。
    • <body> 内的 HTML 元素放入网页底部。
    • <script> 标签内的代码放入 JS 配置。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>盛世烟火 · 国风雪夜</title>
    
    <!-- ========================================= -->
    <!--               1. CSS 样式区               -->
    <!-- ========================================= -->
    <style>
        /* 基础页面样式 (仅为了演示,集成时可删除) */
        body {
            margin: 0;
            padding: 0;
            background-color: #f4f4f4; /* 模拟网页背景 */
            height: 200vh; /* 模拟长页面以便测试滚动 */
        }

        /* --------------------------------------- */
        /* 以下为特效核心 CSS              */
        /* --------------------------------------- */

        /* 烟花前景画布 */
        #fireworks-canvas {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 99999;
            pointer-events: none;
            transition: opacity 1s ease;
        }

        /* 底部文字 */
        .fw-title-container {
            position: fixed;
            bottom: 15px;
            left: 0;
            width: 100%;
            text-align: center;
            z-index: 100000;
            pointer-events: none;
            font-family: "KaiTi", "STKaiti", "SimKai", serif;
            display: flex;
            justify-content: center;
            align-items: baseline;
            gap: 15px;
            text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
            transition: opacity 1s ease;
        }

        .fw-title-container h1 {
            margin: 0;
            font-size: 24px;
            color: #d43838;
            font-weight: bold;
        }

        .fw-title-container p {
            margin: 0;
            font-size: 14px;
            color: #666;
        }

        /* 移动端适配 */
        @media (max-width: 768px) {
            .fw-title-container h1 { font-size: 1.4rem; }
            .fw-title-container p { font-size: 0.9rem; }
        }

        /* 左下角隐形开关 */
        #fw-toggle-btn {
            position: fixed;
            bottom: 10px;
            left: 10px;
            width: 30px;
            height: 30px;
            border-radius: 50%;
            background: rgba(0,0,0,0.2);
            cursor: pointer;
            z-index: 100001;
            opacity: 0.1; /* PC端默认隐藏 */
            transition: all 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 16px;
            color: transparent;
            user-select: none;
            -webkit-tap-highlight-color: transparent; /* 移除移动端点击高亮 */
        }

        /* 仅在支持鼠标悬停的设备(电脑)上启用 Hover 效果 */
        @media (hover: hover) {
            #fw-toggle-btn:hover {
                opacity: 1;
                background: rgba(212, 56, 56, 0.8);
                color: white;
                box-shadow: 0 0 10px rgba(212, 56, 56, 0.5);
            }
        }

        /* 移动端(无悬停)样式调整 */
        @media (hover: none) {
            #fw-toggle-btn {
                opacity: 0.3; /* 移动端默认稍微可见,方便寻找 */
                color: rgba(255,255,255,0.5); /* 图标稍微可见 */
            }
            /* 点击时的反馈 */
            #fw-toggle-btn:active {
                opacity: 1;
                background: rgba(212, 56, 56, 0.8);
            }
        }

        .fw-hidden {
            opacity: 0 !important;
            pointer-events: none !important;
        }

        /* 鼠标蓄力光圈样式 */
        #charge-circle {
            position: fixed;
            border-radius: 50%;
            border: 2px solid rgba(255, 255, 255, 0.9); /* 稍微调亮描边 */
            background: rgba(255, 255, 255, 0.15);
            pointer-events: none;
            opacity: 0;
            z-index: 99999;
            transform-origin: center center;
            box-shadow: 0 0 10px rgba(255, 255, 255, 0.3); /* 加一点发光效果 */
        }
    </style>
</head>
<body>

    <!-- ========================================= -->
    <!--             2. HTML 结构区                -->
    <!-- ========================================= -->

    <!-- 烟花画布 -->
    <canvas id="fireworks-canvas"></canvas>

    <!-- 底部文字 -->
    <div class="fw-title-container">
        <!-- 可选标题 -->
        <!-- <h1>新年快乐</h1> -->
        <p>新年快乐 |</p>
        <p>岁岁常欢愉 · 万事皆胜意</p>
        <p>| 吉庆有余</p>
        <!-- 可选标题 -->
        <!-- <h1>吉庆有余</h1> -->
    </div>

    <!-- 左下角开关 -->
    <div id="fw-toggle-btn" title="切换特效">🎆</div>

    <!-- 鼠标蓄力光圈 -->
    <div id="charge-circle"></div>


    <!-- ========================================= -->
    <!--             3. JavaScript 逻辑区          -->
    <!-- ========================================= -->
    <script>
    (function() {
        // ==========================================================
        //                【参数配置区】
        // ==========================================================
        const config = {
            // --- 8. 功能开关 ---
            enableSnow: true,           // 是否启用雪花(true/false)
            enableAutoFireworks: true,  // 是否启用自动烟花(true/false)
            enableClickFireworks: true, // 是否启用鼠标点击/蓄力烟花(true/false)

            // 1. 自动发射烟花的大小控制
            largeFireworkChance: 0.3, 
            
            // 2. 烟花扩散速度
            baseSpeedSmall: 1.5,  
            baseSpeedLarge: 4.5,  

            // 3. 物理环境
            friction: 0.99,   
            gravity: 0.005,   

            // 4. 存活时间
            decayMin: 0.0008, 
            decayMax: 0.002,  
            
            // 5. 粒子视觉
            particleLineWidth: 2.5, 

            // 6. 自动发射频率
            autoLaunchMin: 80, 
            autoLaunchMax: 180, 

            // 7. 鼠标/触摸蓄力机制
            chargeMinSpeed: 1.0,      
            chargeMaxSpeed: 7.0,      
            chargeDuration: 1500,    
            chargeCircleMaxSize: 80  
        };
        // ==========================================================

        const canvas = document.getElementById('fireworks-canvas');
        const titleContainer = document.querySelector('.fw-title-container');
        const toggleBtn = document.getElementById('fw-toggle-btn');
        const chargeCircle = document.getElementById('charge-circle');
        
        if (!canvas || !toggleBtn || !chargeCircle) return;
        
        const ctx = canvas.getContext('2d');
        let width, height;

        // --- 状态控制 ---  此处可以设置定时自动开启  ---
        const targetDate = new Date('2025-12-30T00:00:00+08:00').getTime();
        const now = Date.now();
        let isVisible = now >= targetDate;

        function updateVisibility() {
            if (isVisible) {
                canvas.classList.remove('fw-hidden');
                if(titleContainer) titleContainer.classList.remove('fw-hidden');
            } else {
                canvas.classList.add('fw-hidden');
                if(titleContainer) titleContainer.classList.add('fw-hidden');
            }
        }
        updateVisibility();

        toggleBtn.addEventListener('click', function() {
            isVisible = !isVisible;
            updateVisibility();
        });

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

        const random = (min, max) => Math.random() * (max - min) + min;
        const colors = [
            '#FF4500', '#FFD700', '#ADFF2F', '#00BFFF', '#BA55D3', 
            '#FF6347', '#FF8C00', '#FFFF00', '#00FF7F', '#1E90FF', 
            '#DA70D6', '#FF1493'
        ];

        // --- 烟花粒子类 ---
        class Particle {
            constructor(x, y, color, speedScale = 1) { 
                this.x = x; this.y = y; this.color = color;
                const angle = random(0, Math.PI * 2); 
                const baseSpeed = random(config.baseSpeedSmall, config.baseSpeedLarge);
                const speed = baseSpeed * random(0.8, 1.2) * speedScale; 

                this.vx = Math.cos(angle) * speed;
                this.vy = Math.sin(angle) * speed;
                this.alpha = 1;
                
                this.friction = config.friction; 
                this.gravity = config.gravity;  
                this.decay = random(config.decayMin, config.decayMax); 
                
                this.coordinates = [];
                this.coordinateCount = 8; 
                while(this.coordinateCount--) this.coordinates.push([this.x, this.y]);
            }
            update() {
                this.coordinates.pop();
                this.coordinates.unshift([this.x, this.y]);
                this.vx *= this.friction; 
                this.vy *= this.friction;
                this.vy += this.gravity;
                this.x += this.vx; 
                this.y += this.vy;
                this.alpha -= this.decay;
            }
            draw(scrollOpacity) { 
                ctx.beginPath();
                ctx.moveTo(this.coordinates[this.coordinates.length - 1][0], this.coordinates[this.coordinates.length - 1][1]);
                ctx.lineTo(this.x, this.y);
                
                const currentAlpha = this.alpha * scrollOpacity;
                if (currentAlpha <= 0) return;

                ctx.strokeStyle = this.color;
                ctx.globalAlpha = currentAlpha;
                ctx.lineWidth = config.particleLineWidth; 
                ctx.stroke();

                ctx.fillStyle = this.color;
                ctx.globalAlpha = currentAlpha * 0.7;
                ctx.arc(this.x, this.y, config.particleLineWidth * 0.5, 0, Math.PI * 2);
                ctx.fill();

                ctx.globalAlpha = 1;
            }
        }

        // --- 雪花类 ---
        class Snowflake {
            constructor() { this.reset(); }
            reset() {
                this.x = Math.random() * width;
                this.y = Math.random() * -height;
                this.vy = 1 + Math.random() * 2;
                this.vx = 0.5 - Math.random();
                this.r = 1 + Math.random() * 2;
                this.o = 0.5 + Math.random() * 0.5;
            }
            update() {
                this.y += this.vy; this.x += this.vx;
                if (this.y > height) this.reset();
            }
            draw() {
                ctx.fillStyle = `rgba(255, 255, 255, ${this.o})`; 
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
                ctx.fill();
            }
        }

        let particles = [], snowflakes = [];
        for(let i=0; i<80; i++) snowflakes.push(new Snowflake());

        function createParticles(x, y, speedScale = 1, particleCountScale = 1) { 
            const isLarge = Math.random() < config.largeFireworkChance;
            const baseColorIndex = Math.floor(random(0, colors.length));
            const color = colors[baseColorIndex];
            
            let count = isLarge ? Math.floor(random(150, 220)) : Math.floor(random(60, 100));
            count = Math.floor(count * particleCountScale); 
            
            while(count--) {
                const pColor = Math.random() < 0.2 ? colors[Math.floor(random(0, colors.length))] : color;
                particles.push(new Particle(x, y, pColor, speedScale));
            }
        }

        let timerTick = 0, timerTotal = 80;

        // --- 主循环 ---
        function loop() {
            requestAnimationFrame(loop);
            ctx.clearRect(0, 0, width, height);
            
            if (isVisible) {
                const scrollY = window.scrollY || document.documentElement.scrollTop;
                const fadeStart = window.innerHeight * 0.2;
                const fadeEnd = window.innerHeight * 0.9;
                
                let fwOpacity = 1;
                if (scrollY > fadeStart) {
                    fwOpacity = 1 - (scrollY - fadeStart) / (fadeEnd - fadeStart);
                }
                if (fwOpacity < 0) fwOpacity = 0;

                // 1. 绘制雪花 (受开关控制)
                if (config.enableSnow) {
                    snowflakes.forEach(f => { f.update(); f.draw(); });
                }

                // 2. 绘制烟花
                let j = particles.length;
                while(j--) {
                    particles[j].update(); 
                    if (fwOpacity > 0) {
                        particles[j].draw(fwOpacity); 
                    }
                    if(particles[j].alpha <= 0) particles.splice(j, 1);
                }

                // 3. 自动发射 (受开关控制)
                if (fwOpacity > 0 && config.enableAutoFireworks) {
                    if(timerTick >= timerTotal) {
                        createParticles(random(0, width), random(0, height * 0.6), random(0.8, 1.2), random(0.8, 1.2)); 
                        timerTick = 0;
                        timerTotal = random(config.autoLaunchMin, config.autoLaunchMax); 
                    } else { timerTick++; }
                }
            }
        }

        // --- 鼠标蓄力逻辑 ---
        let mouseDownTime = 0;
        let mouseX = 0;
        let mouseY = 0;
        let chargeAnimFrameId;

        function updateChargeAnim() {
            if (mouseDownTime > 0 && isVisible) {
                const holdDuration = Date.now() - mouseDownTime;
                const chargeProgress = Math.min(1, holdDuration / config.chargeDuration);
                
                const circleSize = config.chargeCircleMaxSize * chargeProgress;
                
                chargeCircle.style.width = circleSize + 'px';
                chargeCircle.style.height = circleSize + 'px';
                chargeCircle.style.left = (mouseX - circleSize / 2) + 'px';
                chargeCircle.style.top = (mouseY - circleSize / 2) + 'px';
                chargeCircle.style.opacity = '1';
                
                chargeAnimFrameId = requestAnimationFrame(updateChargeAnim);
            }
        }

        window.addEventListener('mousedown', function(e) {
            if (isVisible && config.enableClickFireworks) {
                if (window.scrollY > window.innerHeight * 0.5) return;

                mouseDownTime = Date.now();
                mouseX = e.pageX;
                mouseY = e.pageY - window.scrollY;
                
                chargeCircle.style.width = '0px';
                chargeCircle.style.height = '0px';
                chargeCircle.style.left = mouseX + 'px';
                chargeCircle.style.top = mouseY + 'px';
                chargeCircle.style.opacity = '1';

                cancelAnimationFrame(chargeAnimFrameId);
                updateChargeAnim();
            }
        });
        
        window.addEventListener('mousemove', function(e) {
            if (mouseDownTime > 0 && isVisible && config.enableClickFireworks) {
                mouseX = e.pageX;
                mouseY = e.pageY - window.scrollY;
            }
        });

        window.addEventListener('mouseup', function(e) {
            if (isVisible && mouseDownTime > 0 && config.enableClickFireworks) {
                cancelAnimationFrame(chargeAnimFrameId); 

                const holdDuration = Date.now() - mouseDownTime;
                const chargeProgress = Math.min(1, holdDuration / config.chargeDuration);
                const speedScale = config.chargeMinSpeed + (config.chargeMaxSpeed - config.chargeMinSpeed) * chargeProgress;
                const particleCountScale = 0.5 + (2.0 - 0.5) * chargeProgress; 

                createParticles(mouseX, mouseY, speedScale, particleCountScale);
                
                mouseDownTime = 0;
                chargeCircle.style.opacity = '0';
                setTimeout(() => {
                        if (mouseDownTime === 0) { 
                        chargeCircle.style.width = '0px';
                        chargeCircle.style.height = '0px';
                        }
                }, 100);
            }
        });

        // --- 触摸逻辑 (已修复滚动冲突) ---
        let touchStartX = 0;
        let touchStartY = 0;
        let isScrolling = false;

        window.addEventListener('touchstart', function(e) {
            if (isVisible && e.touches.length === 1 && config.enableClickFireworks) {
                if (window.scrollY > window.innerHeight * 0.5) return;

                // 注意:不调用 preventDefault,允许浏览器处理滚动
                
                isScrolling = false;
                touchStartX = e.touches[0].pageX;
                touchStartY = e.touches[0].pageY;

                mouseDownTime = Date.now();
                mouseX = e.touches[0].pageX;
                mouseY = e.touches[0].pageY - window.scrollY;

                chargeCircle.style.width = '0px';
                chargeCircle.style.height = '0px';
                chargeCircle.style.left = mouseX + 'px';
                chargeCircle.style.top = mouseY + 'px';
                chargeCircle.style.opacity = '1';

                cancelAnimationFrame(chargeAnimFrameId);
                updateChargeAnim();
            }
        }, { passive: true }); // 开启 passive

        window.addEventListener('touchmove', function(e) {
            if (mouseDownTime > 0 && isVisible && e.touches.length === 1 && config.enableClickFireworks) {
                let currentX = e.touches[0].pageX;
                let currentY = e.touches[0].pageY;
                
                let diffX = Math.abs(currentX - touchStartX);
                let diffY = Math.abs(currentY - touchStartY);

                // 如果发生了明显的移动,判定为滚动,取消蓄力
                if (diffX > 10 || diffY > 10) {
                    isScrolling = true;
                    mouseDownTime = 0;
                    chargeCircle.style.opacity = '0';
                    chargeCircle.style.width = '0px';
                    chargeCircle.style.height = '0px';
                    cancelAnimationFrame(chargeAnimFrameId);
                    return;
                }

                mouseX = currentX;
                mouseY = currentY - window.scrollY;
            }
        }, { passive: true });

        window.addEventListener('touchend', function(e) {
            if (isVisible && mouseDownTime > 0 && !isScrolling && config.enableClickFireworks) {
                cancelAnimationFrame(chargeAnimFrameId);

                const holdDuration = Date.now() - mouseDownTime;
                const chargeProgress = Math.min(1, holdDuration / config.chargeDuration);
                const speedScale = config.chargeMinSpeed + (config.chargeMaxSpeed - config.chargeMinSpeed) * chargeProgress;
                const particleCountScale = 0.5 + (2.0 - 0.5) * chargeProgress; 

                createParticles(mouseX, mouseY, speedScale, particleCountScale);
                
                mouseDownTime = 0;
                chargeCircle.style.opacity = '0';
                setTimeout(() => {
                        if (mouseDownTime === 0) {
                        chargeCircle.style.width = '0px';
                        chargeCircle.style.height = '0px';
                        }
                }, 100);
            }
        });

        loop();
    })();
    </script>
</body>
</html>



新年快乐,吉庆有余!