forms

Fruit Slice Ninja

Fruit Slice Ninja

Fruit Slice Ninja is a high-octane, arcade-style experience built for the modern web. Inspired by classic slicing games, this version combines fluid physics, vibrant visual effects, and an escalating difficulty curve that challenges your reflexes and precision.

Key Features

1. Dynamic Physics Engine

Every fruit follows a unique parabolic trajectory with randomized rotation speeds. When sliced, the fruit splits into two distinct halves that react to the angle of your "blade," falling realistically away from the point of impact.

2. Vivid Visual Effects

Fruit Anatomy: Each fruit type features a distinct outer skin and a textured inner flesh (e.g., the bright red of a watermelon or the citrus yellow of an orange).

Particle Bursts: Slicing triggers a splash of color-coordinated juice particles that spray across the screen, providing satisfying visual feedback.

Neon Blade: Your touch or mouse movement creates a glowing neon "slice path" with a trail effect, mimicking the motion of a high-speed katana.

3. Progressive Difficulty

The game starts at a manageable pace but quickly intensifies. The Difficulty Multiplier system ensures that:

Spawn rates increase over time.

The number of fruits launched simultaneously grows.

Bombs appear more frequently to test your precision.

4. Interactive Elements

Watermelons, Apples, and Oranges: Your primary targets.

Bombs: Dangerous hazards that result in an immediate Game Over if struck.

Life System: You have three hearts. Letting a fruit fall unsliced costs you a life, adding a layer of strategic priority to your movements.

Technical Highlights

HTML5 Canvas Rendering: Optimized for smooth 60fps performance on both desktop and mobile devices.

Responsive Design: Automatically adapts to any screen size or orientation.

Touch Optimized: Features custom event handling for low-latency response on mobile browsers.

How to Play

Slice: Click and drag your mouse or swipe your finger across the screen.

Score: Hit as many fruits as possible to climb the leaderboard.

Survive: Avoid the bombs at all costs and don't let the fruit drop!

Code Snippet

<!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>Fruit Slice Ninja</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background: #1a1a1a;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            touch-action: none;
        }
        canvas {
            display: block;
            cursor: crosshair;
        }
        #ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            color: white;
            padding: 20px;
            user-select: none;
        }
        .stats {
            font-size: 24px;
            font-weight: bold;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
        }
        #game-over {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0, 0, 0, 0.85);
            padding: 40px;
            border-radius: 20px;
            text-align: center;
            display: none;
            pointer-events: auto;
            border: 2px solid #ffcc00;
            box-shadow: 0 0 20px rgba(255, 204, 0, 0.3);
        }
        .btn {
            background: #ffcc00;
            color: #000;
            padding: 12px 30px;
            border-radius: 30px;
            font-weight: bold;
            cursor: pointer;
            margin-top: 20px;
            display: inline-block;
            transition: transform 0.2s;
            pointer-events: auto;
        }
        .btn:hover {
            transform: scale(1.1);
        }
    </style>
</head>
<body>

    <div id="ui-layer">
        <div class="flex justify-between items-start">
            <div class="stats">SCORE: <span id="score-val">0</span></div>
            <div class="stats">LIVES: <span id="lives-val">❤❤❤</span></div>
        </div>
        
        <div id="game-over">
            <h1 class="text-5xl font-black mb-4 text-yellow-500">GAME OVER</h1>
            <p class="text-2xl mb-6">Final Score: <span id="final-score">0</span></p>
            <div class="btn" onclick="resetGame()">PLAY AGAIN</div>
        </div>
    </div>

    <canvas id="gameCanvas"></canvas>

    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const scoreEl = document.getElementById('score-val');
        const livesEl = document.getElementById('lives-val');
        const gameOverEl = document.getElementById('game-over');
        const finalScoreEl = document.getElementById('final-score');

        // Game State
        let score = 0;
        let lives = 3;
        let isGameOver = false;
        let fruits = [];
        let particles = [];
        let slicePath = [];
        let lastTime = 0;
        let spawnTimer = 0;
        let spawnRate = 1500; // ms
        let difficultyMultiplier = 1;

        const FRUIT_TYPES = [
            { name: 'watermelon', color: '#2ecc71', innerColor: '#e74c3c', radius: 45, points: 10 },
            { name: 'orange', color: '#f39c12', innerColor: '#f1c40f', radius: 35, points: 15 },
            { name: 'apple', color: '#c0392b', innerColor: '#ecf0f1', radius: 32, points: 15 },
            { name: 'bomb', color: '#333', innerColor: '#ff0000', radius: 30, points: -50, isBomb: true }
        ];

        function resize() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        }

        window.addEventListener('resize', resize);
        resize();

        class Particle {
            constructor(x, y, color) {
                this.x = x;
                this.y = y;
                this.color = color;
                this.radius = Math.random() * 4 + 2;
                this.vx = (Math.random() - 0.5) * 10;
                this.vy = (Math.random() - 0.5) * 10;
                this.life = 1.0;
            }

            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vy += 0.2; // gravity
                this.life -= 0.02;
            }

            draw() {
                ctx.globalAlpha = this.life;
                ctx.fillStyle = this.color;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
                ctx.fill();
                ctx.globalAlpha = 1.0;
            }
        }

        class Fruit {
            constructor() {
                const type = FRUIT_TYPES[Math.floor(Math.random() * FRUIT_TYPES.length)];
                this.type = type;
                this.radius = type.radius;
                this.x = Math.random() * (canvas.width - 100) + 50;
                this.y = canvas.height + this.radius;
                
                // Physics
                this.vx = (canvas.width / 2 - this.x) * 0.01 + (Math.random() - 0.5) * 2;
                this.vy = -(Math.random() * 5 + 12);
                this.gravity = 0.2;
                this.rotation = 0;
                this.rotationSpeed = (Math.random() - 0.5) * 0.1;
                
                this.sliced = false;
                this.sliceAngle = 0;
            }

            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vy += this.gravity;
                this.rotation += this.rotationSpeed;

                if (this.y > canvas.height + 100 && !this.sliced && !this.type.isBomb) {
                    loseLife();
                    this.sliced = true; // Mark as "gone" so we don't lose lives multiple times
                }
            }

            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);

                if (!this.sliced) {
                    // Draw Whole Fruit
                    this.drawFruitBody(0, 0, this.radius, this.type.color);
                    
                    if (this.type.isBomb) {
                        // Draw fuse
                        ctx.strokeStyle = '#fff';
                        ctx.lineWidth = 3;
                        ctx.beginPath();
                        ctx.moveTo(0, -this.radius);
                        ctx.quadraticCurveTo(10, -this.radius - 10, 5, -this.radius - 20);
                        ctx.stroke();
                        // Spark
                        ctx.fillStyle = '#ffff00';
                        ctx.beginPath();
                        ctx.arc(5 + Math.random()*2, -this.radius - 20 + Math.random()*2, 4, 0, Math.PI*2);
                        ctx.fill();
                    }
                } else {
                    // Draw Sliced Halves
                    ctx.save();
                    ctx.translate(-5, 0);
                    ctx.rotate(-0.2);
                    this.drawHalfFruit(this.radius, this.type.color, this.type.innerColor, true);
                    ctx.restore();

                    ctx.save();
                    ctx.translate(5, 0);
                    ctx.rotate(0.2);
                    this.drawHalfFruit(this.radius, this.type.color, this.type.innerColor, false);
                    ctx.restore();
                }

                ctx.restore();
            }

            drawFruitBody(x, y, r, color) {
                ctx.shadowBlur = 15;
                ctx.shadowColor = 'rgba(0,0,0,0.3)';
                ctx.fillStyle = color;
                ctx.beginPath();
                ctx.arc(x, y, r, 0, Math.PI * 2);
                ctx.fill();
                
                // Shine
                ctx.shadowBlur = 0;
                ctx.fillStyle = 'rgba(255,255,255,0.2)';
                ctx.beginPath();
                ctx.arc(x - r/3, y - r/3, r/3, 0, Math.PI * 2);
                ctx.fill();
            }

            drawHalfFruit(r, color, innerColor, isLeft) {
                ctx.fillStyle = color;
                ctx.beginPath();
                if (isLeft) {
                    ctx.arc(0, 0, r, Math.PI * 0.5, Math.PI * 1.5);
                } else {
                    ctx.arc(0, 0, r, Math.PI * 1.5, Math.PI * 0.5);
                }
                ctx.closePath();
                ctx.fill();

                // Inner flesh
                ctx.fillStyle = innerColor;
                ctx.beginPath();
                if (isLeft) {
                    ctx.ellipse(-2, 0, r*0.8, r*0.9, 0, 0, Math.PI * 2);
                } else {
                    ctx.ellipse(2, 0, r*0.8, r*0.9, 0, 0, Math.PI * 2);
                }
                ctx.fill();
            }

            slice() {
                if (this.sliced) return;
                this.sliced = true;
                
                if (this.type.isBomb) {
                    score += this.type.points;
                    loseLife(true);
                    createExplosion(this.x, this.y, '#ff0000');
                } else {
                    score += this.type.points;
                    createExplosion(this.x, this.y, this.type.color);
                }
                
                scoreEl.innerText = Math.max(0, score);
            }
        }

        function createExplosion(x, y, color) {
            for (let i = 0; i < 15; i++) {
                particles.push(new Particle(x, y, color));
            }
        }

        function loseLife(isBomb = false) {
            if (isBomb) lives = 0;
            else lives--;
            
            livesEl.innerText = "❤".repeat(Math.max(0, lives));
            if (lives <= 0) {
                endGame();
            }
        }

        function endGame() {
            isGameOver = true;
            gameOverEl.style.display = 'block';
            finalScoreEl.innerText = score;
        }

        function resetGame() {
            score = 0;
            lives = 3;
            isGameOver = false;
            fruits = [];
            particles = [];
            difficultyMultiplier = 1;
            spawnRate = 1500;
            scoreEl.innerText = "0";
            livesEl.innerText = "❤❤❤";
            gameOverEl.style.display = 'none';
        }

        // Input Handling
        let isSlicing = false;
        
        function startSlicing(e) {
            isSlicing = true;
            slicePath = [];
            addPoint(e);
        }

        function doSlicing(e) {
            if (!isSlicing) return;
            addPoint(e);
            checkSlices();
        }

        function stopSlicing() {
            isSlicing = false;
            slicePath = [];
        }

        function addPoint(e) {
            const rect = canvas.getBoundingClientRect();
            const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
            const y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
            slicePath.push({ x, y, time: Date.now() });
            if (slicePath.length > 10) slicePath.shift();
        }

        function checkSlices() {
            if (slicePath.length < 2) return;
            const head = slicePath[slicePath.length - 1];
            const prev = slicePath[slicePath.length - 2];

            fruits.forEach(fruit => {
                if (!fruit.sliced) {
                    const dist = distToSegment(fruit, prev, head);
                    if (dist < fruit.radius) {
                        fruit.slice();
                    }
                }
            });
        }

        function distToSegment(p, v, w) {
            const l2 = (v.x - w.x)**2 + (v.y - w.y)**2;
            if (l2 == 0) return Math.sqrt((p.x - v.x)**2 + (p.y - v.y)**2);
            let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
            t = Math.max(0, Math.min(1, t));
            return Math.sqrt((p.x - (v.x + t * (w.x - v.x)))**2 + (p.y - (v.y + t * (w.y - v.y)))**2);
        }

        canvas.addEventListener('mousedown', startSlicing);
        window.addEventListener('mousemove', doSlicing);
        window.addEventListener('mouseup', stopSlicing);
        canvas.addEventListener('touchstart', startSlicing);
        window.addEventListener('touchmove', (e) => { e.preventDefault(); doSlicing(e); }, { passive: false });
        window.addEventListener('touchend', stopSlicing);

        function gameLoop(timestamp) {
            const deltaTime = timestamp - lastTime;
            lastTime = timestamp;

            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // Draw Background Gradients
            const grad = ctx.createRadialGradient(canvas.width/2, canvas.height/2, 0, canvas.width/2, canvas.height/2, canvas.width);
            grad.addColorStop(0, '#2c3e50');
            grad.addColorStop(1, '#000000');
            ctx.fillStyle = grad;
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            if (!isGameOver) {
                // Spawning Logic
                spawnTimer += deltaTime;
                if (spawnTimer > spawnRate) {
                    const count = Math.floor(Math.random() * difficultyMultiplier) + 1;
                    for(let i=0; i<count; i++) fruits.push(new Fruit());
                    spawnTimer = 0;
                    // Increase difficulty
                    difficultyMultiplier = Math.min(5, difficultyMultiplier + 0.01);
                    spawnRate = Math.max(600, 1500 - (difficultyMultiplier * 100));
                }

                // Update & Draw Fruits
                for (let i = fruits.length - 1; i >= 0; i--) {
                    fruits[i].update();
                    fruits[i].draw();
                    if (fruits[i].y > canvas.height + 200) {
                        fruits.splice(i, 1);
                    }
                }

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

                // Draw Slice Path
                if (slicePath.length > 1) {
                    ctx.beginPath();
                    ctx.strokeStyle = 'white';
                    ctx.lineWidth = 4;
                    ctx.lineCap = 'round';
                    ctx.lineJoin = 'round';
                    ctx.shadowBlur = 10;
                    ctx.shadowColor = '#00ffff';
                    ctx.moveTo(slicePath[0].x, slicePath[0].y);
                    for (let i = 1; i < slicePath.length; i++) {
                        ctx.lineTo(slicePath[i].x, slicePath[i].y);
                    }
                    ctx.stroke();
                    ctx.shadowBlur = 0;
                }
            }

            requestAnimationFrame(gameLoop);
        }

        requestAnimationFrame(gameLoop);
    </script>
</body>
</html>

Media & Assets

Fruit Slice Ninja
Resource ID: #00036
Posted: December 25, 2025
© 2025 Your Code Library.