• 關閉原本的點擊特效
_config.shokax.yml
fireworks:
  enable: false
/scripts/clickHeart.js
hexo.extend.filter.register('theme_inject', function(injects) {
    injects.bodyEnd.file('clickHeart', 'views/clickHeart.pug', {}, {cache: true});
});
/views/clickHeart.pug
script(type="text/javascript").
    document.addEventListener('DOMContentLoaded', () => {
    // 配置設定
    const CONFIG = {
        // 愛心數量
        HEART_COUNT: {
            MOBILE: 3,  // 手機版愛心數量
            DESKTOP: 6  // 桌面版愛心數量
        },
        // 愛心顏色
        HEART_COLORS: ['#ff9999', '#ffb3ba', '#ffc8dd', '#bae1ff', '#a2d2ff'],
        // 愛心大小範圍(像素)
        HEART_SIZE: {
            MIN: 10,
            MAX: 20
        },
        // 動畫持續時間(毫秒)
        ANIMATION_DURATION: 1000,
        // 最大愛心數量(用於清理)
        MAX_HEARTS: 30,
        // 移動範圍(像素)
        MOVE_RANGE: {
            X: 80,
            Y: 80
        },
        // 緩動函數類型
        EASING: 'easeOutQuad'
    };
    addStyles();
    addMetaViewport();
    
    const heartsContainer = document.createElement('div');
    heartsContainer.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        pointer-events: none;
        z-index: 9999;
    `;
    document.body.appendChild(heartsContainer);
    document.body.addEventListener('click', (event) => {
        createFirework(event.clientX, event.clientY);
    });
    document.body.addEventListener('touchstart', (event) => {
        const touch = event.touches[0];
        createFirework(touch.clientX, touch.clientY);
    });
    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .heart {
                position: absolute;
                pointer-events: none;
                will-change: transform, opacity;
            }
        `;
        document.head.appendChild(style);
    }
    function addMetaViewport() {
        if (!document.querySelector('meta[name="viewport"]')) {
            const meta = document.createElement('meta');
            meta.name = 'viewport';
            meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
            document.head.appendChild(meta);
        }
    }
    function createFirework(x, y) {
        const fragment = document.createDocumentFragment();
        const heartCount = window.innerWidth < 768 ? CONFIG.HEART_COUNT.MOBILE : CONFIG.HEART_COUNT.DESKTOP;
        for (let i = 0; i < heartCount; i++) {
            const color = getRandomColor();
            const heart = createHeart(x, y, color);
            fragment.appendChild(heart);
        }
        heartsContainer.appendChild(fragment);
        clearOldHearts();
    }
    function getRandomColor() {
        return CONFIG.HEART_COLORS[Math.floor(Math.random() * CONFIG.HEART_COLORS.length)];
    }
    function createHeart(x, y, color) {
        const heart = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        heart.setAttribute("viewBox", "0 0 512 512");
        heart.classList.add('heart');
        heart.style.transform = `translate(${x}px, ${y}px)`;
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("d", "M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z");
        path.setAttribute("fill", color);
        heart.appendChild(path);
        const size = CONFIG.HEART_SIZE.MIN + Math.random() * (CONFIG.HEART_SIZE.MAX - CONFIG.HEART_SIZE.MIN);
        heart.style.width = `${size}px`;
        heart.style.height = `${size}px`;
        animateHeart(heart, x, y);
        return heart;
    }
    function animateHeart(heart, initialX, initialY) {
        const startTime = performance.now();
        const targetX = initialX + (Math.random() - 0.5) * CONFIG.MOVE_RANGE.X;
        const targetY = initialY - Math.random() * CONFIG.MOVE_RANGE.Y; 
        function update(currentTime) {
            const elapsed = currentTime - startTime;
            if (elapsed >= CONFIG.ANIMATION_DURATION) {
                heart.remove();
                return;
            }
            const progress = elapsed / CONFIG.ANIMATION_DURATION;
            const easedProgress = easing[CONFIG.EASING](progress);
            const currentX = initialX + (targetX - initialX) * easedProgress;
            const currentY = initialY + (targetY - initialY) * easedProgress;
            const currentOpacity = 1 - easedProgress;
            heart.style.transform = `translate(${currentX}px, ${currentY}px)`;
            heart.style.opacity = currentOpacity;
            requestAnimationFrame(update);
        }
        requestAnimationFrame(update);
    }
    const easing = {
        easeOutQuad: t => 1 - (1 - t) * (1 - t),
        easeOutCubic: t => 1 - Math.pow(1 - t, 3),
        easeOutQuart: t => 1 - Math.pow(1 - t, 4),
        easeOutQuint: t => 1 - Math.pow(1 - t, 5)
    };
    function clearOldHearts() {
        const hearts = heartsContainer.getElementsByClassName('heart');
        if (hearts.length > CONFIG.MAX_HEARTS) {
            for (let i = 0; i < hearts.length - CONFIG.MAX_HEARTS; i++) {
                hearts[i].remove();
            }
        }
    }});