🎮ArcadeLab

KINETIC MESH v2

by ThunderPhoenix57
357 lines10.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>KINETIC MESH v2</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
            -webkit-user-select: none;
            touch-action: none;
        }

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

        canvas {
            display: block;
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
        }

        #ui-layer {
            position: absolute;
            bottom: 40px;
            z-index: 10;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 15px;
            pointer-events: none; /* Let clicks pass through to canvas */
        }

        #hint {
            color: rgba(255, 255, 255, 0.3);
            font-size: 0.85rem;
            letter-spacing: 3px;
            text-transform: uppercase;
            transition: opacity 1s ease;
        }

        #rebuild-btn {
            background: rgba(255, 255, 255, 0.05);
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            color: #fff;
            padding: 12px 30px;
            font-size: 1rem;
            letter-spacing: 2px;
            border-radius: 30px;
            cursor: pointer;
            pointer-events: auto; /* Re-enable clicks for the button */
            transition: all 0.2s ease;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
        }

        #rebuild-btn:hover {
            background: rgba(255, 255, 255, 0.15);
            transform: translateY(-2px);
            box-shadow: 0 0 20px rgba(255, 51, 153, 0.4);
        }

        #rebuild-btn:active {
            transform: translateY(2px);
        }
    </style>
</head>
<body>

    <div id="ui-layer">
        <div id="hint">Drag to stretch. Swipe fast to tear.</div>
        <button id="rebuild-btn" onclick="initMesh()">REBUILD</button>
    </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 points = [];
    let sticks = [];
    let sparks = [];
    
    // Physics & Material properties
    const gravity = 0.4;
    const friction = 0.98;
    const spacing = 24; 
    const stiffness = 3; 
    const snapTension = 2.8; // How much it can stretch before breaking naturally
    
    // Interaction
    let mouse = { x: -1000, y: -1000, px: -1000, py: -1000, isDown: false, speed: 0 };
    const grabDistance = 35;

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

    class Point {
        constructor(x, y, pinned = false) {
            this.x = x;
            this.y = y;
            this.oldX = x;
            this.oldY = y;
            this.pinned = pinned;
        }

        update() {
            if (this.pinned) return;
            let vx = (this.x - this.oldX) * friction;
            let vy = (this.y - this.oldY) * friction;
            this.oldX = this.x;
            this.oldY = this.y;
            this.x += vx;
            this.y += vy + gravity;
        }
    }

    class Stick {
        constructor(p0, p1) {
            this.p0 = p0;
            this.p1 = p1;
            this.length = Math.hypot(p1.x - p0.x, p1.y - p0.y);
            this.isActive = true;
            this.tension = 0;
        }

        update() {
            if (!this.isActive) return;

            let dx = this.p1.x - this.p0.x;
            let dy = this.p1.y - this.p0.y;
            let distance = Math.hypot(dx, dy);
            
            // Calculate tension for colors and snapping
            this.tension = distance / this.length;

            // Auto-snap if pulled too hard
            if (this.tension > snapTension) {
                this.tear();
                return;
            }

            let difference = this.length - distance;
            let percent = difference / distance / 2;
            let offsetX = dx * percent;
            let offsetY = dy * percent;

            if (!this.p0.pinned) { this.p0.x -= offsetX; this.p0.y -= offsetY; }
            if (!this.p1.pinned) { this.p1.x += offsetX; this.p1.y += offsetY; }
        }

        draw() {
            if (!this.isActive) return;
            
            // Color shifts from deep blue (220) to hot pink (330) based on tension
            let hue = 220 + (Math.min(this.tension - 1, 1.5) * 80);
            let alpha = Math.min(0.3 + (this.tension * 0.3), 1);
            let width = Math.max(0.5, 3 - this.tension); // gets thinner as it stretches

            ctx.strokeStyle = `hsla(${hue}, 100%, 65%, ${alpha})`;
            ctx.lineWidth = width;
            
            ctx.beginPath();
            ctx.moveTo(this.p0.x, this.p0.y);
            ctx.lineTo(this.p1.x, this.p1.y);
            ctx.stroke();
        }

        tear() {
            this.isActive = false;
            createSparks((this.p0.x + this.p1.x) / 2, (this.p0.y + this.p1.y) / 2);
        }
    }

    class Spark {
        constructor(x, y) {
            this.x = x;
            this.y = y;
            this.vx = (Math.random() - 0.5) * 15;
            this.vy = (Math.random() - 0.5) * 15;
            this.life = 1.0;
            this.decay = Math.random() * 0.05 + 0.02;
            this.hue = Math.random() * 60 + 280; // Pink/Purple sparks
        }
        
        update() {
            this.x += this.vx;
            this.y += this.vy;
            this.vy += gravity * 0.5; // Sparks float a bit
            this.vx *= 0.95;
            this.life -= this.decay;
        }

        draw() {
            ctx.fillStyle = `hsla(${this.hue}, 100%, 70%, ${this.life})`;
            ctx.beginPath();
            ctx.arc(this.x, this.y, Math.random() * 2 + 1, 0, Math.PI * 2);
            ctx.fill();
        }
    }

    function createSparks(x, y) {
        for(let i=0; i<4; i++) {
            sparks.push(new Spark(x, y));
        }
    }

    function initMesh() {
        points = [];
        sticks = [];
        sparks = [];

        // Make the mesh span most of the screen width, hanging from the top
        let cols = Math.floor(width / spacing) - 2;
        let rows = Math.floor(height / spacing) - 6;
        
        // Centering
        let startX = (width - cols * spacing) / 2;
        let startY = 20;

        for (let y = 0; y < rows; y++) {
            for (let x = 0; x < cols; x++) {
                // Pin the top row
                let p = new Point(startX + x * spacing, startY + y * spacing, y === 0);
                points.push(p);
            }
        }

        for (let y = 0; y < rows; y++) {
            for (let x = 0; x < cols; x++) {
                let i = y * cols + x;
                // Right connect
                if (x < cols - 1) sticks.push(new Stick(points[i], points[i + 1]));
                // Bottom connect
                if (y < rows - 1) sticks.push(new Stick(points[i], points[i + cols]));
            }
        }
    }

    function interact() {
        if (!mouse.isDown) return;

        let dx = mouse.x - mouse.px;
        let dy = mouse.y - mouse.py;
        mouse.speed = Math.hypot(dx, dy);

        points.forEach(p => {
            let dist = Math.hypot(mouse.x - p.x, mouse.y - p.y);
            
            if (dist < grabDistance) {
                if (mouse.speed > 35) {
                    // Fast swipe cuts nearby threads
                    sticks.forEach(s => {
                        if (s.isActive && (s.p0 === p || s.p1 === p)) s.tear();
                    });
                } else {
                    // Soft pull
                    p.x += dx * 0.6;
                    p.y += dy * 0.6;
                }
            }
        });
    }

    function animate() {
        // Dark trail effect for smooth motion
        ctx.fillStyle = 'rgba(3, 4, 8, 0.3)';
        ctx.fillRect(0, 0, width, height);

        points.forEach(p => p.update());
        
        for (let i = 0; i < stiffness; i++) {
            sticks.forEach(s => s.update());
        }

        interact();

        // Draw glow effects
        ctx.globalCompositeOperation = 'lighter';
        
        sticks.forEach(s => s.draw());

        // Update and draw sparks
        for (let i = sparks.length - 1; i >= 0; i--) {
            sparks[i].update();
            sparks[i].draw();
            if (sparks[i].life <= 0) sparks.splice(i, 1);
        }

        ctx.globalCompositeOperation = 'source-over';

        mouse.px = mouse.x;
        mouse.py = mouse.y;

        requestAnimationFrame(animate);
    }

    function setMouse(e) {
        if (e.touches) {
            mouse.x = e.touches[0].clientX;
            mouse.y = e.touches[0].clientY;
        } else {
            mouse.x = e.clientX;
            mouse.y = e.clientY;
        }
    }

    window.addEventListener('mousedown', (e) => { 
        if (e.target.id === 'rebuild-btn') return;
        mouse.isDown = true; 
        setMouse(e); 
        hint.style.opacity = '0'; 
    });
    window.addEventListener('mousemove', setMouse);
    window.addEventListener('mouseup', () => { mouse.isDown = false; });

    window.addEventListener('touchstart', (e) => { 
        if (e.target.id === 'rebuild-btn') return;
        mouse.isDown = true; 
        setMouse(e); 
        hint.style.opacity = '0'; 
    }, {passive: false});
    window.addEventListener('touchmove', (e) => { 
        setMouse(e); 
        if (e.target.id !== 'rebuild-btn') e.preventDefault(); 
    }, {passive: false});
    window.addEventListener('touchend', () => { mouse.isDown = false; });

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

Game Source: KINETIC MESH v2

Creator: ThunderPhoenix57

Libraries: none

Complexity: complex (357 lines, 10.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: kinetic-mesh-v2-thunderphoenix57" to link back to the original. Then publish at arcadelab.ai/publish.