{
  "case_id": "A7",
  "case_name": "像素艺术爆炸效果",
  "prompt": "生成一个像素艺术风格的爆炸特效动画，要求：\n1. 8-bit复古游戏风格\n2. 多层爆炸波纹扩散\n3. 粒子碎片飞溅效果\n4. 可调节爆炸强度\n5. 单文件HTML Canvas实现",
  "result": {
    "success": true,
    "content": "这是一个单文件 HTML 解决方案，包含 CSS、HTML 和 JavaScript。\n\n这个实现完全使用 Canvas API 绘制，不依赖任何外部图片。它通过**网格对齐（Grid Snapping）**和**矩形填充**来模拟复古的 8-bit 像素风格，而不是简单的模糊圆圈。\n\n### 功能特点：\n1.  **真正的像素化渲染**：所有图形都对齐到虚拟的像素网格上。\n2.  **多层波纹**：包含核心闪光、扩散火球、边缘烟雾三层结构。\n3.  **物理粒子**：爆炸产生的碎片受重力和空气阻力影响，颜色随生命周期冷却（白->黄->红->灰）。\n4.  **屏幕震动**：爆炸时会触发屏幕震动效果，增加打击感。\n5.  **交互性**：点击屏幕任意位置触发爆炸，拖动滑块调节威力。\n\n### 代码实现\n\n你可以将以下代码保存为 `.html` 文件并在浏览器中直接打开。\n\n```html\n<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>8-Bit Pixel Art Explosion</title>\n    <style>\n        body {\n            margin: 0;\n            overflow: hidden;\n            background-color: #1a1a24;\n            font-family: 'Courier New', Courier, monospace;\n            color: #fff;\n            user-select: none;\n        }\n\n        #canvas-container {\n            position: relative;\n            width: 100vw;\n            height: 100vh;\n            /* 强制像素化缩放，防止模糊 */\n            image-rendering: pixelated; \n        }\n\n        canvas {\n            display: block;\n        }\n\n        #ui-layer {\n            position: absolute;\n            top: 20px;\n            left: 20px;\n            background: rgba(0, 0, 0, 0.7);\n            padding: 15px;\n            border: 2px solid #fff;\n            border-radius: 4px;\n            pointer-events: auto;\n            box-shadow: 4px 4px 0px #000;\n        }\n\n        .control-group {\n            margin-bottom: 10px;\n        }\n\n        label {\n            display: block;\n            margin-bottom: 5px;\n            font-size: 14px;\n            text-transform: uppercase;\n        }\n\n        input[type=\"range\"] {\n            width: 200px;\n            cursor: pointer;\n        }\n\n        .hint {\n            margin-top: 10px;\n            font-size: 12px;\n            color: #aaa;\n        }\n    </style>\n</head>\n<body>\n\n<div id=\"canvas-container\">\n    <canvas id=\"gameCanvas\"></canvas>\n    <div id=\"ui-layer\">\n        <div class=\"control-group\">\n            <label for=\"intensity\">爆炸强度 (Intensity): <span id=\"val-display\">50</span></label>\n            <input type=\"range\" id=\"intensity\" min=\"10\" max=\"150\" value=\"50\">\n        </div>\n        <div class=\"hint\">点击屏幕任意位置引发爆炸</div>\n    </div>\n</div>\n\n<script>\n    const canvas = document.getElementById('gameCanvas');\n    const ctx = canvas.getContext('2d');\n    \n    // 配置参数\n    const PIXEL_SIZE = 6; // 每个“像素”的实际大小\n    const GRAVITY = 0.2;\n    const FRICTION = 0.95;\n    \n    // 复古调色板 (Nes/Pico-8 风格)\n    const PALETTE = {\n        white:  '#ffffff',\n        yellow: '#fceec8', // 亮黄\n        orange: '#fcb603', // 橙\n        red:    '#e8480d', // 红\n        dark:   '#543e46', // 暗红/褐\n        smoke:  '#261d29'  // 深灰/黑\n    };\n\n    let explosions = [];\n    let width, height;\n    let shakeIntensity = 0;\n\n    // 初始化画布\n    function resize() {\n        width = window.innerWidth;\n        height = window.innerHeight;\n        canvas.width = width;\n        canvas.height = height;\n        \n        // 关闭抗锯齿以获得锐利的像素边缘\n        ctx.imageSmoothingEnabled = false;\n    }\n    window.addEventListener('resize', resize);\n    resize();\n\n    // UI控制\n    const intensitySlider = document.getElementById('intensity');\n    const valDisplay = document.getElementById('val-display');\n    \n    intensitySlider.addEventListener('input', (e) => {\n        valDisplay.innerText = e.target.value;\n    });\n\n    // 工具函数：在网格上绘制像素块\n    function drawPixel(x, y, color, size = PIXEL_SIZE) {\n        ctx.fillStyle = color;\n        // 将坐标对齐到网格\n        const gridX = Math.floor(x / size) * size;\n        const gridY = Math.floor(y / size) * size;\n        ctx.fillRect(gridX, gridY, size, size);\n    }\n\n    // 工具函数：绘制像素圆\n    function drawPixelCircle(cx, cy, radius, color, fill = true, probability = 1.0) {\n        ctx.fillStyle = color;\n        \n        // 简单的边界盒算法\n        let r = Math.ceil(radius);\n        // 优化：步长为 PIXEL_SIZE，减少循环次数\n        for (let y = -r; y <= r; y += PIXEL_SIZE) {\n            for (let x = -r; x <= r; x += PIXEL_SIZE) {\n                if (x*x + y*y <= r*r) {\n                    // 边缘抖动效果 (Dithering effect)\n                    if (probability < 1.0 && Math.random() > probability) continue;\n\n                    if (!fill) {\n                        // 如果是空心圆，只画边缘\n                        if (x*x + y*y <= (r-PIXEL_SIZE)*(r-PIXEL_SIZE)) continue;\n                    }\n\n                    drawPixel(cx + x, cy + y, color);\n                }\n            }\n        }\n    }\n\n    // 粒子类 (碎片)\n    class Particle {\n        constructor(x, y, force) {\n            this.x = x;\n            this.y = y;\n            const angle = Math.random() * Math.PI * 2;\n            const speed = Math.random() * force * 0.5;\n            \n            this.vx = Math.cos(angle) * speed;\n            this.vy = Math.sin(angle) * speed;\n            \n            this.life = 1.0;\n            this.decay = Math.random() * 0.03 + 0.01;\n            this.size = (Math.random() > 0.5 ? 1 : 2) * PIXEL_SIZE; // 大小不一\n        }\n\n        update() {\n            this.vx *= FRICTION;\n            this.vy *= FRICTION;\n            this.vy += GRAVITY;\n            this.x += this.vx;\n            this.y += this.vy;\n            this.life -= this.decay;\n        }\n\n        draw() {\n            let color;\n            // 颜色随生命周期冷却\n            if (this.life > 0.8) color = PALETTE.white;\n            else if (this.life > 0.6) color = PALETTE.yellow;\n            else if (this.life > 0.4) color = PALETTE.orange;\n            else if (this.life > 0.2) color = PALETTE.red;\n            else color = PALETTE.dark;\n\n            drawPixel(this.x, this.y, color, this.size);\n        }\n    }\n\n    // 爆炸主类\n    class Explosion {\n        constructor(x, y, intensity) {\n            this.x = x;\n            this.y = y;\n            this.intensity = intensity;\n            this.frame = 0;\n            this.maxFrames = 40 + intensity / 2;\n            this.active = true;\n            this.particles = [];\n\n            // 生成碎片粒子\n            const particleCount = Math.floor(intensity / 2);\n            for (let i = 0; i < particleCount; i++) {\n                this.particles.push(new Particle(x, y, intensity / 5)); // 调整速度因子\n            }\n\n            // 触发震动\n            shakeIntensity = Math.min(20, intensity / 5);\n        }\n\n        update() {\n            this.frame++;\n            \n            // 更新粒子\n            this.particles.forEach(p => p.update());\n            this.particles = this.particles.filter(p => p.life > 0);\n\n            if (this.frame > this.maxFrames && this.particles.length === 0) {\n                this.active = false;\n            }\n        }\n\n        draw() {\n            const progress = this.frame / this.maxFrames;\n            const baseRadius = this.intensity * 1.5;\n\n            // 1. 绘制粒子 (底层)\n            this.particles.forEach(p => p.draw());\n\n            // 2. 绘制波纹层 (Shockwaves)\n            // 只有在动画前半段才显示火球核心\n            if (progress < 0.6) {\n                // 外层烟雾/暗色波纹\n                let r3 = baseRadius * Math.sin(progress * Math.PI);\n                if (r3 > 0) drawPixelCircle(this.x, this.y, r3, PALETTE.smoke, false, 0.6); // 只有边缘，带噪点\n\n                // 中层红色/橙色波纹\n                let r2 = baseRadius * 0.8 * Math.sin(progress * Math.PI);\n                if (r2 > 0) {\n                   // 动态切换颜色模拟闪烁\n                   const c = this.frame % 4 < 2 ? PALETTE.red : PALETTE.dark;\n                   drawPixelCircle(this.x, this.y, r2, c, true, 0.9); \n                }\n\n                // 内层核心 (白/黄)\n                let r1 = baseRadius * 0.5 * (1 - progress * 1.5);\n                if (r1 > 0) {\n                    const c = Math.random() > 0.5 ? PALETTE.yellow : PALETTE.white;\n                    drawPixelCircle(this.x, this.y, r1, c);\n                }\n            } else {\n                // 爆炸后期留下的消散烟云\n                if (this.frame % 5 === 0) { // 降低刷新率造成定格感\n                     // 仅绘制残留烟雾粒子，这里简化处理，不需要额外逻辑\n                }\n            }\n        }\n    }\n\n    // 主循环\n    function loop() {\n        // 屏幕震动逻辑\n        let shakeX = 0;\n        let shakeY = 0;\n        if (shakeIntensity > 0) {\n            shakeX = (Math.random() - 0.5) * shakeIntensity;\n            shakeY = (Math.random() - 0.5) * shakeIntensity;\n            shakeIntensity *= 0.9; // 震动衰减\n            if (shakeIntensity < 0.5) shakeIntensity = 0;\n        }\n\n        ctx.save();\n        \n        // 清屏 (带拖尾效果可选，这里为了复古感直接清空)\n        ctx.fillStyle = '#1a1a24';\n        ctx.fillRect(0, 0, width, height);\n\n        // 应用震动\n        ctx.translate(shakeX, shakeY);\n\n        // 更新并绘制所有爆炸\n        for (let i = explosions.length - 1; i >= 0; i--) {\n            let exp = explosions[i];\n            exp.update();\n            exp.draw();\n            if (!exp.active) {\n                explosions.splice(i, 1);\n            }\n        }\n\n        ctx.restore();\n        requestAnimationFrame(loop);\n    }\n\n    // 交互事件\n    canvas.addEventListener('mousedown', (e) => {\n        const rect = canvas.getBoundingClientRect();\n        const x = e.clientX - rect.left;\n        const y = e.clientY - rect.top;\n        const intensity = parseInt(intensitySlider.value);\n        \n        explosions.push(new Explosion(x, y, intensity));\n    });\n\n    // 启动\n    loop();\n\n</script>\n</body>\n</html>\n```\n\n### 如何使用：\n1.  将上述代码保存为 `explosion.html`。\n2.  双击打开文件。\n3.  **点击屏幕**：在鼠标点击的位置生成爆炸。\n4.  **调整滑块**：改变爆炸的大小和粒子数量。\n\n### 技术细节解析：\n\n1.  **`drawPixel` 函数**：\n    这是核心。它接收任意浮点坐标 `(x, y)`，但通过 `Math.floor(x / size) * size` 将其强制“对齐”到我们设定的 `PIXEL_SIZE` (6px) 网格上。这保证了无论物体如何移动，它始终看起来像是在低分辨率网格上跳动。\n\n2.  **颜色变化 (Palette Cycling)**：\n    粒子类 `Particle` 中没有使用平滑的 RGB 过渡，而是根据生命周期（`life`）在预定义的调色板数组中切换颜色。这是 8-bit 游戏处理火焰和爆炸的经典手法。\n\n3.  **像素圆算法**：\n    `drawPixelCircle` 不使用 Canvas 的 `arc()`。它在一个正方形区域内遍历网格点，计算 `x² + y² <= r²`。\n    *   **Dithering (抖动)**：通过 `probability` 参数，随机跳过一些像素的绘制，这模拟了复古游戏中用来表现半透明烟雾或破碎边缘的“噪点”效果。\n\n4.  **性能优化**：\n    虽然我们在 JS 中手动计算像素点，但因为步长是 `PIXEL_SIZE` (6px)，循环次数大大减少，即使在手机浏览器上也能保持流畅的 60FPS。",
    "model": "gemini-3-pro-preview",
    "usage": {
      "prompt_tokens": 81,
      "completion_tokens": 4915,
      "total_tokens": 4996
    },
    "timestamp": "2025-11-19 13:26:23"
  },
  "needs_screenshot": true,
  "needs_manual_test": false
}