🎮ArcadeLab

魔法画板 · 灵蝶渐变光晕 | 隔空手势绘形

by GoldenTiger20
702 lines28.0 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>魔法画板 · 灵蝶渐变光晕 | 隔空手势绘形</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
        }

        body {
            background: #0a0a2a;
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', 'Poppins', system-ui, sans-serif;
            padding: 20px;
        }

        .magic-container {
            background: #000000;
            border-radius: 2rem;
            padding: 1rem;
            box-shadow: 0 25px 40px rgba(0,0,0,0.5), 0 0 0 2px rgba(255,255,255,0.1);
        }

        canvas {
            display: block;
            width: 100%;
            height: auto;
            border-radius: 1rem;
            cursor: pointer;
            box-shadow: 0 0 30px rgba(0,255,255,0.2);
        }

        .info-panel {
            margin-top: 1rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 12px;
            background: rgba(0,0,0,0.7);
            backdrop-filter: blur(4px);
            border-radius: 2rem;
            padding: 0.8rem 1.5rem;
            border: 1px solid rgba(255,255,255,0.2);
        }

        .gesture-badge {
            display: flex;
            gap: 1rem;
            background: #1a1a2e;
            padding: 0.4rem 1rem;
            border-radius: 2rem;
            font-weight: bold;
        }

        .gesture-item {
            display: flex;
            align-items: center;
            gap: 8px;
            color: white;
            font-size: 0.8rem;
        }

        .color-box {
            width: 24px;
            height: 24px;
            border-radius: 8px;
            border: 1px solid white;
        }

        .status {
            font-family: monospace;
            font-size: 0.9rem;
            background: black;
            padding: 0.3rem 1rem;
            border-radius: 2rem;
            color: cyan;
            letter-spacing: 1px;
            font-weight: bold;
        }

        .control-group {
            display: flex;
            align-items: center;
            gap: 12px;
            background: #1a1a2e;
            padding: 0.3rem 1rem;
            border-radius: 2rem;
        }

        .control-group label {
            color: white;
            font-size: 0.8rem;
        }

        input[type="range"] {
            width: 140px;
            cursor: pointer;
            background: #333;
            height: 3px;
            border-radius: 3px;
        }

        .intensity-value {
            color: #ffaa66;
            font-weight: bold;
            min-width: 35px;
        }

        button {
            background: #2c2c3e;
            border: none;
            color: white;
            padding: 6px 14px;
            border-radius: 2rem;
            font-weight: bold;
            cursor: pointer;
            transition: 0.2s;
            font-family: inherit;
        }

        button:hover {
            background: #ff4d6d;
            box-shadow: 0 0 8px #ff4d6d;
            transform: scale(0.96);
        }

        .footer {
            font-size: 0.7rem;
            color: #aaa;
            text-align: center;
            margin-top: 0.8rem;
        }
        
        .error-box {
            color: #ffaaaa;
            background: rgba(30,0,0,0.9);
            padding: 10px 12px;
            border-radius: 16px;
            margin-top: 12px;
            font-size: 0.75rem;
            text-align: center;
            border-left: 4px solid #ff5555;
            backdrop-filter: blur(4px);
        }
        
        .retry-btn {
            background: #ff884d;
            margin-left: 12px;
            padding: 4px 12px;
            font-size: 0.75rem;
        }
        
        @media (max-width: 700px) {
            .gesture-item span { display: none; }
            .gesture-item { font-size: 0.7rem; }
            .info-panel { padding: 0.6rem 1rem; }
            .control-group label { font-size: 0.7rem; }
            input[type="range"] { width: 100px; }
        }
    </style>
</head>
<body>
<div>
    <div class="magic-container">
        <canvas id="magicCanvas" width="800" height="600" style="width:100%; height:auto; max-width:1000px; aspect-ratio:800/600"></canvas>
        <div class="info-panel">
            <div class="gesture-badge">
                <div class="gesture-item"><div class="color-box" style="background:#ff3333;"></div><span>1指 → 方 + 红蝶</span></div>
                <div class="gesture-item"><div class="color-box" style="background:#33ff33;"></div><span>2指 → 圆 + 绿蝶</span></div>
                <div class="gesture-item"><div class="color-box" style="background:#3399ff;"></div><span>3指 → 三角 + 蓝蝶</span></div>
            </div>
            <div class="control-group">
                <label>✨ 光晕强度</label>
                <input type="range" id="glowIntensitySlider" min="0" max="5" step="0.1" value="2.5">
                <span id="intensityValue" class="intensity-value">2.5</span>
            </div>
            <div class="status" id="gestureStatus">🔍 正在请求摄像头...</div>
            <button id="clearBtn">🧹 清空画板</button>
        </div>
        <div class="footer">
            🦋 渐变光晕:中心亮色 → 边缘透明 | 滑块调节整体强度 | 点击画板固定图形
        </div>
        <div id="errorContainer" style="min-height: 65px;"></div>
    </div>
</div>

<!-- MediaPipe 核心库 -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>

<script>
    (function(){
        // ---------- DOM 元素 ----------
        const canvas = document.getElementById('magicCanvas');
        const ctx = canvas.getContext('2d');
        const gestureStatusDiv = document.getElementById('gestureStatus');
        const clearBtn = document.getElementById('clearBtn');
        const errorContainer = document.getElementById('errorContainer');
        const intensitySlider = document.getElementById('glowIntensitySlider');
        const intensitySpan = document.getElementById('intensityValue');
        
        canvas.width = 800;
        canvas.height = 600;

        // 全局强度 (0-5)
        let globalGlowIntensity = 2.5;
        intensitySlider.addEventListener('input', (e) => {
            globalGlowIntensity = parseFloat(e.target.value);
            intensitySpan.innerText = globalGlowIntensity.toFixed(1);
        });
        
        // ---------- 全局状态 ----------
        let hands = null;
        let cameraInstance = null;
        let currentFingerCount = 0;
        let handDetected = false;
        let handCenter = { x: canvas.width/2, y: canvas.height/2 };
        let shapesHistory = [];
        let currentTempShape = null;
        
        const COLOR_MAP = {
            1: '#ff3333',
            2: '#33ff33',
            3: '#3399ff'
        };
        
        // ---------- 蝴蝶特效 (径向渐变光晕) ----------
        let butterflies = [];
        const MAX_BUTTERFLIES = 45;
        let lastGenerateTime = 0;
        const GENERATE_INTERVAL = 500;
        const BUTTERFLIES_PER_GEN = 2;
        
        class Butterfly {
            constructor(x, y, colorType) {
                this.x = x;
                this.y = y;
                this.vx = (Math.random() - 0.5) * 2.2;
                this.vy = -1.5 - Math.random() * 1.5;
                this.size = 12 + Math.random() * 6;
                this.colorType = colorType;
                this.wingAngle = 0;
                this.flapSpeed = 0.15 + Math.random() * 0.1;
            }
            
            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vx *= 0.99;
                this.vy += 0.05;
                this.wingAngle += this.flapSpeed;
                if (this.x + 80 < 0 || this.x - 80 > canvas.width || this.y + 80 < 0 || this.y - 80 > canvas.height) {
                    return false;
                }
                return true;
            }
            
            // 获取渐变所需的颜色 (中心饱和色,边缘淡色)
            getGradientColors() {
                if (this.colorType === 'red') {
                    return { center: '#ff4444', edge: '#ff8888' };
                } else if (this.colorType === 'green') {
                    return { center: '#44ff44', edge: '#88ff88' };
                } else {
                    return { center: '#4488ff', edge: '#88aaff' };
                }
            }
            
            draw(ctx) {
                const intensity = globalGlowIntensity;
                const { center, edge } = this.getGradientColors();
                const baseSize = this.size;
                
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(Math.sin(this.wingAngle) * 0.5);
                
                // 1. 绘制径向渐变光晕 (从中心饱和色到边缘淡色,透明度也渐变)
                // 最大半径设定为 2.5 倍翅膀尺寸,确保羽化充分
                const maxRadius = baseSize * 2.5;
                const gradient = ctx.createRadialGradient(0, 0, baseSize * 0.3, 0, 0, maxRadius);
                // 根据强度调整颜色透明度:强度越高,颜色越不透明
                const alphaCenter = Math.min(0.9, intensity * 0.2);
                const alphaEdge = 0;
                gradient.addColorStop(0, center);
                gradient.addColorStop(0.4, edge);
                gradient.addColorStop(1, 'rgba(0,0,0,0)');
                
                ctx.globalAlpha = Math.min(0.7, intensity * 0.15);
                ctx.fillStyle = gradient;
                ctx.beginPath();
                ctx.arc(0, 0, maxRadius, 0, Math.PI * 2);
                ctx.fill();
                
                // 2. 额外增加一层柔和的光晕阴影 (模拟发光,增强渐变感)
                ctx.shadowBlur = 0;
                ctx.beginPath();
                ctx.arc(0, 0, baseSize * 1.2, 0, Math.PI * 2);
                ctx.fillStyle = center;
                ctx.shadowBlur = intensity * 1.5;
                ctx.shadowColor = center;
                ctx.fill();
                
                // 重置阴影和透明度
                ctx.shadowBlur = 0;
                ctx.globalAlpha = 1;
                
                // 绘制实体翅膀 (颜色保持鲜艳)
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.quadraticCurveTo(-baseSize, -baseSize*0.7, -baseSize*0.8, -baseSize*0.2);
                ctx.quadraticCurveTo(-baseSize*0.3, -baseSize*0.1, 0, 0);
                ctx.fillStyle = this.colorType === 'red' ? '#e34234' : (this.colorType === 'green' ? '#3cba54' : '#4285f4');
                ctx.fill();
                ctx.fillStyle = this.colorType === 'red' ? '#ff7f6e' : (this.colorType === 'green' ? '#7ed37e' : '#7baaf7');
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.quadraticCurveTo(-baseSize*0.6, -baseSize*0.5, -baseSize*0.4, -baseSize*0.15);
                ctx.fill();
                
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.quadraticCurveTo(baseSize, -baseSize*0.7, baseSize*0.8, -baseSize*0.2);
                ctx.quadraticCurveTo(baseSize*0.3, -baseSize*0.1, 0, 0);
                ctx.fillStyle = this.colorType === 'red' ? '#e34234' : (this.colorType === 'green' ? '#3cba54' : '#4285f4');
                ctx.fill();
                ctx.fillStyle = this.colorType === 'red' ? '#ff7f6e' : (this.colorType === 'green' ? '#7ed37e' : '#7baaf7');
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.quadraticCurveTo(baseSize*0.6, -baseSize*0.5, baseSize*0.4, -baseSize*0.15);
                ctx.fill();
                
                // 身体
                ctx.fillStyle = '#2c2c2c';
                ctx.beginPath();
                ctx.ellipse(0, 2, 2, 4, 0, 0, Math.PI*2);
                ctx.fill();
                ctx.fillStyle = '#f0c0a0';
                ctx.beginPath();
                ctx.ellipse(0, 0, 1.5, 2.5, 0, 0, Math.PI*2);
                ctx.fill();
                
                ctx.restore();
            }
        }
        
        function generateButterfliesFromHand(count, centerX, centerY) {
            if (count !== 1 && count !== 2 && count !== 3) return;
            let colorType = '';
            if (count === 1) colorType = 'red';
            else if (count === 2) colorType = 'green';
            else colorType = 'blue';
            
            for (let i = 0; i < BUTTERFLIES_PER_GEN; i++) {
                if (butterflies.length >= MAX_BUTTERFLIES) butterflies.splice(0, 5);
                const offsetX = (Math.random() - 0.5) * 55;
                const offsetY = (Math.random() - 0.5) * 45 - 20;
                let x = centerX + offsetX;
                let y = centerY + offsetY;
                x = Math.min(canvas.width - 15, Math.max(15, x));
                y = Math.min(canvas.height - 15, Math.max(15, y));
                butterflies.push(new Butterfly(x, y, colorType));
            }
        }
        
        function updateButterflies() {
            for (let i = butterflies.length-1; i >= 0; i--) {
                if (!butterflies[i].update()) butterflies.splice(i,1);
            }
        }
        
        function drawButterflies() {
            for (let b of butterflies) b.draw(ctx);
        }
        
        function generateButterfliesByTimer(now) {
            if (!handDetected) return;
            if (currentFingerCount === 1 || currentFingerCount === 2 || currentFingerCount === 3) {
                if (!lastGenerateTime) lastGenerateTime = now;
                if (now - lastGenerateTime >= GENERATE_INTERVAL) {
                    generateButterfliesFromHand(currentFingerCount, handCenter.x, handCenter.y);
                    lastGenerateTime = now;
                }
            } else {
                lastGenerateTime = now;
            }
        }
        
        // ---------- 手势识别 ----------
        const fingerTips = [8, 12, 16, 20];
        const fingerMCPs = [6, 10, 14, 18];
        
        function countRaisedFingers(landmarks) {
            if (!landmarks) return 0;
            let count = 0;
            const threshold = 0.02;
            for (let i = 0; i < fingerTips.length; i++) {
                const tip = landmarks[fingerTips[i]];
                const base = landmarks[fingerMCPs[i]];
                if (tip && base && (tip.y < base.y - threshold)) count++;
            }
            return count;
        }
        
        function getHandCenterPos(landmarks, canvasW, canvasH) {
            if (!landmarks || landmarks.length === 0) return null;
            let sumX = 0, sumY = 0;
            for (let lm of landmarks) {
                sumX += lm.x;
                sumY += lm.y;
            }
            return {
                x: (sumX / landmarks.length) * canvasW,
                y: (sumY / landmarks.length) * canvasH
            };
        }
        
        function getDynamicSize(landmarks, canvasW, canvasH) {
            if (!landmarks || landmarks.length === 0) return 70;
            let minX = 1, maxX = 0, minY = 1, maxY = 0;
            for (let lm of landmarks) {
                minX = Math.min(minX, lm.x);
                maxX = Math.max(maxX, lm.x);
                minY = Math.min(minY, lm.y);
                maxY = Math.max(maxY, lm.y);
            }
            const handSizeNorm = Math.max(maxX - minX, maxY - minY);
            let size = Math.floor(handSizeNorm * 180);
            return Math.min(115, Math.max(45, size));
        }
        
        // 图形绘制
        function drawShape(ctx, type, x, y, size, colorHex) {
            if (!ctx) return;
            const half = size / 2;
            ctx.save();
            ctx.shadowBlur = 0;
            ctx.beginPath();
            if (type === 'square') {
                ctx.fillStyle = colorHex;
                ctx.fillRect(x - half, y - half, size, size);
                ctx.strokeStyle = '#ffffff';
                ctx.lineWidth = 2.5;
                ctx.strokeRect(x - half, y - half, size, size);
            } 
            else if (type === 'circle') {
                ctx.fillStyle = colorHex;
                ctx.arc(x, y, half, 0, Math.PI * 2);
                ctx.fill();
                ctx.strokeStyle = '#ffffff';
                ctx.lineWidth = 2.5;
                ctx.arc(x, y, half, 0, Math.PI * 2);
                ctx.stroke();
            } 
            else if (type === 'triangle') {
                const r = half;
                const angleOffset = -Math.PI / 2;
                const points = [];
                for (let i = 0; i < 3; i++) {
                    const angle = angleOffset + i * 2 * Math.PI / 3;
                    points.push({ x: x + r * Math.cos(angle), y: y + r * Math.sin(angle) });
                }
                ctx.fillStyle = colorHex;
                ctx.moveTo(points[0].x, points[0].y);
                ctx.lineTo(points[1].x, points[1].y);
                ctx.lineTo(points[2].x, points[2].y);
                ctx.closePath();
                ctx.fill();
                ctx.strokeStyle = '#ffffff';
                ctx.lineWidth = 2.5;
                ctx.stroke();
            }
            ctx.restore();
        }
        
        function renderEverything() {
            if (!ctx) return;
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            for (let shape of shapesHistory) {
                drawShape(ctx, shape.type, shape.x, shape.y, shape.size, shape.color);
            }
            if (currentTempShape && currentTempShape.type) {
                ctx.save();
                ctx.shadowBlur = 10;
                ctx.shadowColor = currentTempShape.color;
                drawShape(ctx, currentTempShape.type, currentTempShape.x, currentTempShape.y, currentTempShape.size, currentTempShape.color);
                ctx.restore();
            }
            drawButterflies();
            
            if (shapesHistory.length === 0 && !currentTempShape) {
                ctx.font = "15px 'Segoe UI'";
                ctx.fillStyle = "#6a6a6a";
                ctx.shadowBlur = 0;
                ctx.fillText("🤚 伸出手指 (1~3根) → 魔法图形 + 渐变光晕灵蝶", canvas.width/2-190, canvas.height-35);
                ctx.fillText("✨ 点击画板固定图形 | 滑块调节整体强度 ✨", canvas.width/2-170, canvas.height-15);
            }
            if (handDetected) {
                ctx.font = "bold 20px monospace";
                ctx.fillStyle = "#aaffdd";
                ctx.fillText(`✋ ${currentFingerCount} 指`, 20, 50);
            } else {
                ctx.font = "15px monospace";
                ctx.fillStyle = "#555";
                ctx.fillText("🖐️ 等待手部识别", 20, 50);
            }
        }
        
        // MediaPipe 回调
        function onHandResults(results) {
            if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
                handDetected = true;
                const landmarks = results.multiHandLandmarks[0];
                currentFingerCount = countRaisedFingers(landmarks);
                const center = getHandCenterPos(landmarks, canvas.width, canvas.height);
                if (center) handCenter = center;
                
                if (currentFingerCount === 1) gestureStatusDiv.innerText = '🔴 方形 + 红蝶(渐变光) · 1指';
                else if (currentFingerCount === 2) gestureStatusDiv.innerText = '🟢 圆形 + 绿蝶(渐变光) · 2指';
                else if (currentFingerCount === 3) gestureStatusDiv.innerText = '🔵 三角 + 蓝蝶(渐变光) · 3指';
                else if (currentFingerCount === 0) gestureStatusDiv.innerText = '🖐️ 握拳 · 无图形';
                else gestureStatusDiv.innerText = `✨ ${currentFingerCount}指 (仅1~3特效) ✨`;
                
                const dynSize = getDynamicSize(landmarks, canvas.width, canvas.height);
                if (center && (currentFingerCount === 1 || currentFingerCount === 2 || currentFingerCount === 3)) {
                    let shapeType = null, colorHex = null;
                    if (currentFingerCount === 1) { shapeType = 'square'; colorHex = COLOR_MAP[1]; }
                    else if (currentFingerCount === 2) { shapeType = 'circle'; colorHex = COLOR_MAP[2]; }
                    else if (currentFingerCount === 3) { shapeType = 'triangle'; colorHex = COLOR_MAP[3]; }
                    if (shapeType && colorHex) {
                        currentTempShape = { type: shapeType, x: center.x, y: center.y, size: dynSize, color: colorHex };
                    } else currentTempShape = null;
                } else {
                    currentTempShape = null;
                }
            } else {
                handDetected = false;
                currentFingerCount = 0;
                currentTempShape = null;
                gestureStatusDiv.innerText = '🤚 未检测到手部 · 伸出手试试';
            }
        }
        
        // 摄像头初始化
        async function ensureCameraAndStart() {
            errorContainer.innerHTML = '';
            gestureStatusDiv.innerText = '📷 请求摄像头权限...';
            let stream = null;
            try {
                stream = await navigator.mediaDevices.getUserMedia({ video: true });
                stream.getTracks().forEach(track => track.stop());
            } catch (err) {
                let errorMsg = '';
                if (err.name === 'NotAllowedError') errorMsg = '❌ 摄像头权限被拒绝。请允许摄像头访问后刷新页面。';
                else if (err.name === 'NotFoundError') errorMsg = '❌ 未检测到摄像头设备。';
                else if (err.name === 'NotReadableError') errorMsg = '❌ 摄像头被其他应用占用。';
                else errorMsg = `❌ 无法访问摄像头: ${err.message}`;
                showPermanentError(errorMsg);
                gestureStatusDiv.innerText = '⚠️ 摄像头不可用';
                return false;
            }
            
            gestureStatusDiv.innerText = '🔄 加载手势模型...';
            try {
                hands = new Hands({
                    locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
                });
                hands.setOptions({
                    maxNumHands: 1,
                    modelComplexity: 1,
                    minDetectionConfidence: 0.6,
                    minTrackingConfidence: 0.5
                });
                hands.onResults(onHandResults);
            } catch (err) {
                showPermanentError('手势模型加载失败: ' + err.message);
                return false;
            }
            
            const videoElement = document.createElement('video');
            videoElement.style.display = 'none';
            document.body.appendChild(videoElement);
            try {
                cameraInstance = new Camera(videoElement, {
                    onFrame: async () => {
                        if (hands && videoElement.readyState >= 2) {
                            try { await hands.send({ image: videoElement }); } catch(e) {}
                        }
                    },
                    width: 800,
                    height: 600
                });
                await cameraInstance.start();
                gestureStatusDiv.innerText = '🎭 魔法画板激活 | 光晕径向渐变,中心亮边缘淡';
                errorContainer.innerHTML = '';
                return true;
            } catch (err) {
                showPermanentError('摄像头启动失败: ' + (err.message || '未知错误'));
                gestureStatusDiv.innerText = '⚠️ 摄像头启动失败';
                return false;
            }
        }
        
        function showPermanentError(msg) {
            errorContainer.innerHTML = `
                <div class="error-box">
                    ⚠️ ${msg}<br>
                    💡 解决建议:<br>
                    1️⃣ 点击地址栏锁形图标 → 允许摄像头 → 刷新页面<br>
                    2️⃣ 使用 HTTPS 或 localhost 访问<br>
                    3️⃣ 检查摄像头占用情况<br>
                    <button id="manualRetryBtn" class="retry-btn">🔄 手动重试</button>
                </div>
            `;
            const retryBtn = document.getElementById('manualRetryBtn');
            if (retryBtn) retryBtn.addEventListener('click', () => { location.reload(); });
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            ctx.fillStyle = '#ff8888';
            ctx.font = '14px monospace';
            ctx.fillText('摄像头未就绪,请授权并刷新', canvas.width/2-150, canvas.height/2);
        }
        
        function commitCurrentShape() {
            if (currentTempShape && currentTempShape.type) {
                shapesHistory.push({ ...currentTempShape });
                currentTempShape = null;
                return true;
            }
            return false;
        }
        
        function clearCanvas() {
            shapesHistory = [];
            currentTempShape = null;
            gestureStatusDiv.innerText = '✨ 画板已清空 ✨';
            setTimeout(() => {
                if (handDetected) {
                    let shape = '';
                    if (currentFingerCount === 1) shape = '方形';
                    else if (currentFingerCount === 2) shape = '圆形';
                    else if (currentFingerCount === 3) shape = '三角形';
                    else shape = '手势';
                    gestureStatusDiv.innerText = `🎨 ${currentFingerCount}指 → ${shape}`;
                } else gestureStatusDiv.innerText = '✨ 魔法画板待机 ✨';
            }, 800);
        }
        
        // 动画循环
        function animationFrame(now) {
            requestAnimationFrame(animationFrame);
            updateButterflies();
            generateButterfliesByTimer(now);
            renderEverything();
        }
        
        function setupInteractions() {
            canvas.addEventListener('click', () => {
                if (commitCurrentShape()) {
                    gestureStatusDiv.innerText = '✨ 图形已固定 ✨';
                    setTimeout(() => {
                        if (handDetected) {
                            let shape = '';
                            if (currentFingerCount === 1) shape = '方形';
                            else if (currentFingerCount === 2) shape = '圆形';
                            else if (currentFingerCount === 3) shape = '三角形';
                            else shape = '手势';
                            gestureStatusDiv.innerText = `🎨 ${currentFingerCount}指 → ${shape}`;
                        } else gestureStatusDiv.innerText = '🪄 点击固定,继续创作';
                    }, 700);
                } else {
                    gestureStatusDiv.innerText = '🤚 先伸出1~3根手指再点击固定';
                    setTimeout(() => {
                        if (handDetected) gestureStatusDiv.innerText = `🎨 ${currentFingerCount}指 → ${currentFingerCount===1?'方形':(currentFingerCount===2?'圆形':'三角')}`;
                        else gestureStatusDiv.innerText = '✨ 魔法画板 ✨';
                    }, 1000);
                }
            });
            clearBtn.addEventListener('click', clearCanvas);
        }
        
        async function start() {
            setupInteractions();
            requestAnimationFrame(animationFrame);
            await ensureCameraAndStart();
        }
        start();
    })();
</script>
</body>
</html>

Game Source: 魔法画板 · 灵蝶渐变光晕 | 隔空手势绘形

Creator: GoldenTiger20

Libraries: none

Complexity: complex (702 lines, 28.0 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: -goldentiger20" to link back to the original. Then publish at arcadelab.ai/publish.