🎮ArcadeLab

CHROMA BOARD

by ThunderPhoenix57
239 lines7.2 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>CHROMA BOARD</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
            -webkit-user-select: none;
            touch-action: none;
        }

        body {
            background-color: #050505;
            overflow: hidden;
            width: 100vw;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', sans-serif;
        }

        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }

        #hint {
            position: absolute;
            top: 30px;
            width: 100%;
            text-align: center;
            color: rgba(255, 255, 255, 0.2);
            font-size: 0.85rem;
            letter-spacing: 6px;
            pointer-events: none;
            text-transform: uppercase;
            transition: opacity 1s ease;
        }
    </style>
</head>
<body>

    <div id="hint">SWIPE & TAP</div>
    <canvas id="canvas"></canvas>

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

    let width, height;
    let grid = [];
    let ripples = [];
    
    // Performance and Visual Tuning
    const SPACING = 25; // Distance between dots
    const BASE_RADIUS = 1.5;
    const MAX_RADIUS = 10;
    const MOUSE_INFLUENCE = 120; // How wide your brush is
    
    let pointer = { x: -1000, y: -1000, active: false };
    let time = 0;

    class Dot {
        constructor(x, y) {
            this.x = x;
            this.y = y;
            this.radius = BASE_RADIUS;
            this.hue = 200;
            this.lightness = 15;
            // Base offset for organic idle movement
            this.offset = (x + y) * 0.01; 
        }

        update() {
            let targetRadius = BASE_RADIUS;
            let targetLightness = 15;
            let targetHue = this.hue;

            // 1. Mouse Interaction (Hover/Swipe)
            if (pointer.active) {
                let dx = pointer.x - this.x;
                let dy = pointer.y - this.y;
                let dist = Math.sqrt(dx * dx + dy * dy);

                if (dist < MOUSE_INFLUENCE) {
                    let intensity = 1 - (dist / MOUSE_INFLUENCE);
                    targetRadius = BASE_RADIUS + (MAX_RADIUS * intensity);
                    targetLightness = 15 + (60 * intensity);
                    targetHue = (time * 50 + intensity * 100) % 360;
                }
            }

            // 2. Ripple Interaction (Clicks)
            for (let i = 0; i < ripples.length; i++) {
                let r = ripples[i];
                let dx = r.x - this.x;
                let dy = r.y - this.y;
                let dist = Math.sqrt(dx * dx + dy * dy);
                
                // If dot is sitting on the expanding ripple ring
                if (Math.abs(dist - r.radius) < r.thickness) {
                    let intensity = 1 - (Math.abs(dist - r.radius) / r.thickness);
                    let fade = Math.max(0, 1 - (r.radius / r.maxRadius));
                    
                    targetRadius = Math.max(targetRadius, BASE_RADIUS + (MAX_RADIUS * 1.5 * intensity * fade));
                    targetLightness = Math.max(targetLightness, 15 + (85 * intensity * fade));
                    targetHue = r.hue;
                }
            }

            // 3. Smooth Lerp (Interpolation) for buttery animation without physics lag
            this.radius += (targetRadius - this.radius) * 0.15;
            this.lightness += (targetLightness - this.lightness) * 0.1;
            
            // Idle breathing effect
            let idleBreath = Math.sin(time + this.offset) * 0.5;
            this.displayRadius = Math.max(0.1, this.radius + idleBreath);
        }

        draw() {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.displayRadius, 0, Math.PI * 2);
            ctx.fillStyle = `hsl(${this.hue}, 80%, ${this.lightness}%)`;
            ctx.fill();
        }
    }

    class Ripple {
        constructor(x, y) {
            this.x = x;
            this.y = y;
            this.radius = 0;
            this.maxRadius = Math.max(width, height) * 0.8;
            this.thickness = 60;
            this.speed = 15;
            this.hue = (time * 100) % 360;
            this.active = true;
        }

        update() {
            this.radius += this.speed;
            this.thickness += 1; // Spreads out as it expands
            if (this.radius > this.maxRadius) this.active = false;
        }
    }

    function initGrid() {
        grid = [];
        // Add padding to ensure dots reach the edges beautifully
        let cols = Math.ceil(width / SPACING) + 2;
        let rows = Math.ceil(height / SPACING) + 2;
        
        let startX = (width - (cols * SPACING)) / 2;
        let startY = (height - (rows * SPACING)) / 2;

        for (let y = 0; y < rows; y++) {
            for (let x = 0; x < cols; x++) {
                grid.push(new Dot(startX + x * SPACING, startY + y * SPACING));
            }
        }
    }

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

    function animate() {
        // Pure black clear, no trailing effects, highly performant
        ctx.fillStyle = '#050505';
        ctx.fillRect(0, 0, width, height);

        time += 0.05;

        // Update & clear dead ripples
        for (let i = ripples.length - 1; i >= 0; i--) {
            ripples[i].update();
            if (!ripples[i].active) ripples.splice(i, 1);
        }

        // Draw grid
        for (let i = 0; i < grid.length; i++) {
            grid[i].update();
            grid[i].draw();
        }

        requestAnimationFrame(animate);
    }

    // Input Handling
    function setPointer(e) {
        if (e.touches) {
            pointer.x = e.touches[0].clientX;
            pointer.y = e.touches[0].clientY;
        } else {
            pointer.x = e.clientX;
            pointer.y = e.clientY;
        }
    }

    window.addEventListener('mousedown', (e) => { 
        pointer.active = true; 
        setPointer(e); 
        ripples.push(new Ripple(pointer.x, pointer.y));
        hint.style.opacity = '0';
    });
    window.addEventListener('mousemove', (e) => { 
        if(pointer.active) setPointer(e); 
    });
    window.addEventListener('mouseup', () => { pointer.active = false; });

    window.addEventListener('touchstart', (e) => { 
        pointer.active = true; 
        setPointer(e); 
        ripples.push(new Ripple(pointer.x, pointer.y));
        hint.style.opacity = '0';
    }, {passive: false});
    window.addEventListener('touchmove', (e) => { 
        setPointer(e); 
        e.preventDefault(); // Prevents scrolling on mobile
    }, {passive: false});
    window.addEventListener('touchend', () => { pointer.active = false; });

    animate();
</script>
</body>
</html>

Game Source: CHROMA BOARD

Creator: ThunderPhoenix57

Libraries: none

Complexity: complex (239 lines, 7.2 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: chroma-board-thunderphoenix57" to link back to the original. Then publish at arcadelab.ai/publish.