Inspired From Fruit Ninja Games I Made it on Html 5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>🍉 Fruit Ninja — Slice Master</title>
<style>
/* === DESIGN SYSTEM === */
/* Font: Outfit — premium geometric sans for game UI */
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;500;700;900&display=swap');
:root {
--accent: #ff5e3a;
--accent-glow: rgba(255,94,58,0.4);
--freeze: #4fc3f7;
--super: #ffd93d;
--rain: #6ddb4f;
--bg-dark: #1a0a14;
--bg-mid: #2d1b1e;
--text: #fff5e6;
--danger: #ff3b3b;
}
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-user-select: none; }
html, body { overflow: hidden; height: 100%; width: 100%; }
body {
font-family: 'Outfit', sans-serif;
background: radial-gradient(ellipse at center, var(--bg-mid) 0%, var(--bg-dark) 100%);
color: var(--text);
touch-action: none;
}
canvas { display: block; position: absolute; top: 0; left: 0; z-index: 1; }
/* === HUD === */
#hud {
position: absolute;
top: 0; left: 0; right: 0;
z-index: 10;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
}
.hud-panel {
background: rgba(0,0,0,0.45);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px;
padding: 10px 18px;
display: flex;
align-items: center;
gap: 10px;
}
.hud-panel .label {
font-size: 11px;
font-weight: 500;
letter-spacing: 1.5px;
text-transform: uppercase;
opacity: 0.6;
}
.hud-panel .value {
font-size: 28px;
font-weight: 900;
line-height: 1;
}
#livesPanel { gap: 6px; }
.life-icon {
width: 22px; height: 22px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent-glow);
transition: all 0.3s ease;
}
.life-icon.lost {
background: rgba(255,255,255,0.1);
box-shadow: none;
transform: scale(0.7);
}
/* === POWER-UP BAR === */
#powerBar {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex;
gap: 12px;
pointer-events: none;
}
.power-badge {
display: flex;
align-items: center;
gap: 8px;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 30px;
padding: 6px 16px;
font-size: 13px;
font-weight: 700;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s, transform 0.3s;
}
.power-badge.active { opacity: 1; transform: translateY(0); }
.power-badge .pdot {
width: 10px; height: 10px;
border-radius: 50%;
animation: pulse 1s infinite;
}
.power-badge.freeze .pdot { background: var(--freeze); box-shadow: 0 0 8px var(--freeze); }
.power-badge.super .pdot { background: var(--super); box-shadow: 0 0 8px var(--super); }
.power-badge.rain .pdot { background: var(--rain); box-shadow: 0 0 8px var(--rain); }
@keyframes pulse {
0%,100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: 0.7; }
}
/* === COMBO POPUP === */
#comboPopup {
position: absolute;
z-index: 15;
font-size: 36px;
font-weight: 900;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
text-shadow: 0 0 20px currentColor;
}
/* === SCREENS === */
#screen {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
background: radial-gradient(ellipse at center, rgba(45,27,30,0.85) 0%, rgba(26,10,20,0.95) 100%);
backdrop-filter: blur(6px);
transition: opacity 0.5s, visibility 0.5s;
}
#screen.hidden { opacity: 0; visibility: hidden; }
#screen h1 {
font-size: 56px;
font-weight: 900;
letter-spacing: -2px;
text-align: center;
line-height: 1.05;
background: linear-gradient(135deg, #ff5e3a, #ff9f43, #ffd93d);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
filter: drop-shadow(0 4px 18px rgba(255,94,58,0.25));
}
#screen h1 .sub {
display: block;
font-size: 18px;
font-weight: 500;
letter-spacing: 4px;
text-transform: uppercase;
-webkit-text-fill-color: var(--text);
color: var(--text);
opacity: 0.7;
margin-top: 6px;
}
#screen .tagline {
font-size: 16px;
opacity: 0.6;
max-width: 400px;
text-align: center;
line-height: 1.5;
}
.features-row {
display: flex;
gap: 14px;
flex-wrap: wrap;
justify-content: center;
max-width: 460px;
}
.feat-chip {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 20px;
padding: 6px 14px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.5px;
}
.feat-chip .dot { width: 8px; height: 8px; border-radius: 50%; }
.btn {
font-family: inherit;
font-size: 18px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: #fff;
background: var(--accent);
border: none;
border-radius: 14px;
padding: 14px 42px;
cursor: pointer;
box-shadow: 0 8px 28px var(--accent-glow);
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 12px 34px var(--accent-glow); }
.btn:active { transform: translateY(0); }
.final-score {
font-size: 64px;
font-weight: 900;
line-height: 1;
}
.final-label {
font-size: 14px;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
opacity: 0.5;
margin-bottom: -10px;
}
.score-row {
display: flex;
gap: 30px;
}
.score-row .stat { text-align: center; }
.score-row .stat .num { font-size: 28px; font-weight: 900; }
.score-row .stat .lbl { font-size: 11px; opacity: 0.5; text-transform: uppercase; letter-spacing: 1.5px; }
/* How to play */
.howto {
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
max-width: 500px;
}
.howto-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
opacity: 0.75;
}
.howto-item .ico {
width: 36px; height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
}
</style>
</head>
<body>
<!-- Game Canvas -->
<canvas id="canvas"></canvas>
<!-- HUD -->
<div id="hud">
<div class="hud-panel" id="scorePanel">
<span class="label">Score</span>
<span class="value" id="scoreEl">0</span>
</div>
<div class="hud-panel" id="livesPanel">
<span class="label" style="margin-right:4px">Lives</span>
<div class="life-icon" data-life="1"></div>
<div class="life-icon" data-life="2"></div>
<div class="life-icon" data-life="3"></div>
</div>
<div class="hud-panel" id="bestPanel">
<span class="label">Best</span>
<span class="value" id="bestEl">0</span>
</div>
</div>
<!-- Power-up active bar -->
<div id="powerBar">
<div class="power-badge freeze" id="pb-freeze"><div class="pdot"></div> FREEZE</div>
<div class="power-badge super" id="pb-super"><div class="pdot"></div> SUPER SLICE</div>
<div class="power-badge rain" id="pb-rain"><div class="pdot"></div> FRUIT RAIN</div>
</div>
<!-- Combo popup -->
<div id="comboPopup"></div>
<!-- Start / Game Over Screen -->
<div id="screen">
<h1 id="screenTitle">🍉 Fruit Ninja<span class="sub">Slice Master</span></h1>
<p class="tagline" id="screenTagline">Swipe to slice fruits, chain combos, grab power-ups, and dodge the bombs!</p>
<div class="features-row" id="featRow">
<div class="feat-chip"><span class="dot" style="background:var(--accent)"></span> Combos</div>
<div class="feat-chip"><span class="dot" style="background:var(--freeze)"></span> Freeze</div>
<div class="feat-chip"><span class="dot" style="background:var(--super)"></span> Super Power</div>
<div class="feat-chip"><span class="dot" style="background:var(--rain)"></span> Fruit Rain</div>
<div class="feat-chip"><span class="dot" style="background:var(--danger)"></span> Traps</div>
</div>
<div class="howto" id="howto">
<div class="howto-item"><div class="ico">🍉</div> <span>Slice fruits<br>for points</span></div>
<div class="howto-item"><div class="ico">💣</div> <span>Avoid bombs<br>or lose a life</span></div>
<div class="howto-item"><div class="ico">❄️</div> <span>Freeze slows<br>everything</span></div>
<div class="howto-item"><div class="ico">⚡</div> <span>Super = double<br>score slices</span></div>
</div>
<button class="btn" id="startBtn">Play</button>
</div>
<script>
"use strict";
/* =================================================================
FRUIT NINJA — SLICE MASTER
Single-file HTML5 Canvas Game
Features: Realistic fruits, slice physics, combo system,
power-ups (Freeze, Super, Fruit Rain), traps/bombs, particles
================================================================= */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { alpha: false });
const DPR = Math.max(1, window.devicePixelRatio || 1);
let W, H;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * DPR;
canvas.height = H * DPR;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener('resize', resize);
resize();
/* ===== GAME STATE ===== */
const State = { MENU: 'menu', PLAYING: 'playing', OVER: 'over' };
let gameState = State.MENU;
let score = 0;
let best = parseInt(localStorage.getItem('fruitNinjaBest') || '0');
let lives = 3;
let timeAccumulator = 0;
let spawnTimer = 0;
let spawnInterval = 1400; // ms
let difficulty = 1;
let shakeAmount = 0;
let freezeTimer = 0;
let superTimer = 0;
let rainTimer = 0;
let comboCount = 0;
let comboTimer = 0;
let comboHits = [];
let slicesThisSwipe = []; // for combo detection
let lastSwipeTime = 0;
document.getElementById('bestEl').textContent = best;
/* ===== UTILITIES ===== */
const rand = (a, b) => a + Math.random() * (b - a);
const randInt = (a, b) => Math.floor(rand(a, b + 1));
const choose = arr => arr[Math.floor(Math.random() * arr.length)];
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const lerp = (a, b, t) => a + (b - a) * t;
/* ===== COLORS / TEXTURES for fruits ===== */
const FRUIT_TYPES = [
{ name: 'watermelon', rCol: '#ff5e5e', rCol2: '#ff8a8a', skinColor1: '#2d8f3e', skinColor2: '#1a6b2e', innerColor: '#ff7a7a', seeds: true, seedColor: '#3d2b1f', radius: 48 },
{ name: 'orange', rCol: '#ff9f43', rCol2: '#ffb96b', skinColor1: '#ff8c1a', skinColor2: '#e07a0a', innerColor: '#ffb15c', seeds: false, radius: 38 },
{ name: 'apple', rCol: '#e84855', rCol2: '#ff7070', skinColor1: '#d63447', skinColor2: '#a52a35', innerColor: '#fff5e6', seeds: true, seedColor: '#5a3a1a', radius: 40 },
{ name: 'lime', rCol: '#6ddb4f', rCol2: '#9af07d', skinColor1: '#4caf50', skinColor2: '#357a38', innerColor: '#d4f5c4', seeds: false, radius: 34 },
{ name: 'grape', rCol: '#9b59b6', rCol2: '#b370cc', skinColor1: '#8e44ad', skinColor2: '#6c3492', innerColor: '#c8a4d4', seeds: false, radius: 32 },
{ name: 'pineapple', rCol: '#f1c40f', rCol2: '#ffe066', skinColor1: '#e6b800', skinColor2: '#c79a00', innerColor: '#ffe88a', seeds: false, radius: 42 },
{ name: 'strawberry', rCol: '#e84855', rCol2: '#ff6870', skinColor1: '#e84855', skinColor2: '#c0392b', innerColor: '#ffb3c1', seeds: true, seedColor: '#ffe066', radius: 36 },
];
const SPECIAL_TYPES = [
{ name: 'freeze', glow: '#4fc3f7', radius: 38 }, // ❄️ Freeze powerup
{ name: 'super', glow: '#ffd93d', radius: 38 }, // ⚡ Super powerup
{ name: 'rain', glow: '#6ddb4f', radius: 38 }, // 🌧️ Fruit Rain
];
// Trap / Bomb
const BOMB = { radius: 42, glow: '#ff3b3b' };
/* ===== FRUIT / OBJECT ===== */
class GameObject {
constructor(typeData, isFruit, isSpecial, isBomb) {
this.type = typeData;
this.isFruit = isFruit;
this.isSpecial = isSpecial;
this.isBomb = isBomb;
this.radius = typeData.radius || 40;
this.x = rand(this.radius * 2, W - this.radius * 2);
this.y = H + this.radius + 10;
// Launch upward with angle
const angle = rand(-Math.PI * 0.75, -Math.PI * 0.25);
const power = rand(980, 1350) * difficulty * 0.001;
// Velocity (tilt toward center)
const targetX = W / 2;
const dirX = (targetX - this.x) / W;
this.vx = dirX * rand(150, 350);
this.vy = -rand(750, 1100);
this.rotation = rand(0, Math.PI * 2);
this.rotSpeed = rand(-3, 3);
this.sliced = false;
this.alive = true;
this.sliceProgress = 0;
this.angleSlice = 0;
this.halfA = null; // leftover half (canvas rendering)
this.halfB = null;
this.popOffset = 0;
}
update(dt, slowed) {
const factor = slowed ? 0.28 : 1;
this.x += this.vx * dt * factor;
this.y += this.vy * dt * factor;
this.vy += 980 * dt * factor; // gravity
this.rotation += this.rotSpeed * dt * factor;
if (this.sliced) this.sliceProgress = Math.min(1, this.sliceProgress + dt * 2);
// Remove if off screen
if (this.y > H + 200 && this.vy > 0) {
this.alive = false;
// Missed fruit = lose life (only for real fruits)
if (this.isFruit && !this.sliced && gameState === State.PLAYING) {
loseLife();
}
}
}
removePeaceful() {
this.alive = false;
}
/* Slicinq */
slice(angle) {
if (this.sliced) return false;
this.sliced = true;
this.angleSlice = angle;
// Not removing here; update handles removal
return true;
}
/* Check if point (global) near fruit */
hitTest(x1, y1, x2, y2) {
// Distance from point to segment
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy);
if (len === 0) return false;
const t = clamp(((this.x - x1) * dx + (this.y - y1) * dy) / (len * len), 0, 1);
const px = x1 + t * dx;
const py = y1 + t * dy;
const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
return dist < this.radius + 8;
}
/* === Render fruit / bomb / special === */
draw(c) {
c.save();
c.translate(this.x, this.y);
if (this.isBomb) this.drawBomb(c);
else if (this.isSpecial) this.drawSpecial(c);
else this.drawFruit(c);
c.restore();
}
drawFruit(c) {
const r = this.radius;
c.rotate(this.rotation);
if (!this.sliced) {
this.drawWholeFruit(c, r);
} else {
// Draw splitting fruit: two halves sliding apart
const off = r * 1.5 * this.sliceProgress;
const ang = this.angleSlice;
// Half A
c.save();
c.rotate(ang - this.rotation); // undo self-rotation for slice line
c.beginPath();
c.arc(0, 0, r, -Math.PI / 2, Math.PI / 2);
c.closePath();
c.fillStyle = this.type.innerColor;
c.fill();
// inner gradient
const ig = c.createRadialGradient(0, 0, r * 0.2, 0, 0, r);
ig.addColorStop(0, this.type.innerColor);
ig.addColorStop(0.8, this.type.rCol2);
ig.addColorStop(1, this.type.skinColor1);
c.fillStyle = ig;
c.fill();
if (this.type.seeds) this.drawSeeds(c, r, 0);
// skin
c.lineWidth = 4;
c.strokeStyle = this.type.skinColor2;
c.stroke();
c.restore();
// Half B (rotate other side)
c.save();
c.rotate(ang - this.rotation + Math.PI);
c.beginPath();
c.arc(0, 0, r, -Math.PI / 2, Math.PI / 2);
c.closePath();
c.fillStyle = this.type.innerColor;
c.fill();
const ig2 = c.createRadialGradient(0, 0, r * 0.2, 0, 0, r);
ig2.addColorStop(0, this.type.innerColor);
ig2.addColorStop(0.8, this.type.rCol2);
ig2.addColorStop(1, this.type.skinColor1);
c.fillStyle = ig2;
c.fill();
if (this.type.seeds) this.drawSeeds(c, r, 0);
c.lineWidth = 4;
c.strokeStyle = this.type.skinColor2;
c.stroke();
c.restore();
}
}
drawSeeds(c, r, seedColor) {
c.save();
c.fillStyle = this.type.seedColor || '#3d2b1f';
const positions = [
[0, -r*0.3], [0, r*0.3], [-r*0.25, 0], [r*0.25, 0],
[-r*0.15, -r*0.18], [r*0.15, -r*0.18], [-r*0.15, r*0.18], [r*0.15, r*0.18]
];
positions.forEach(([sx, sy]) => {
c.beginPath();
c.ellipse(sx, sy, 2.5, 4, 0, 0, Math.PI * 2);
c.fill();
});
c.restore();
}
drawWholeFruit(c, r) {
// Outer glow
c.shadowColor = this.type.rCol;
c.shadowBlur = 12;
c.beginPath();
c.arc(0, 0, r, 0, Math.PI * 2);
c.closePath();
// gradient fill
const g = c.createRadialGradient(-r * 0.3, -r * 0.3, r * 0.15, 0, 0, r);
g.addColorStop(0, this.type.rCol2);
g.addColorStop(0.6, this.type.rCol);
g.addColorStop(1, this.type.skinColor2);
c.fillStyle = g;
c.fill();
c.shadowBlur = 0;
// Highlight
c.beginPath();
c.ellipse(-r * 0.35, -r * 0.35, r * 0.2, r * 0.12, -0.6, 0, Math.PI * 2);
c.fillStyle = 'rgba(255,255,255,0.35)';
c.fill();
// Stem
c.strokeStyle = '#3d2b1f';
c.lineWidth = 3;
c.beginPath();
c.moveTo(0, -r);
c.lineTo(4, -r - 12);
c.stroke();
// Leaf
c.save();
c.translate(4, -r - 10);
c.fillStyle = '#5db840';
c.beginPath();
c.ellipse(-4, 0, 8, 3, 1.2, 0, Math.PI * 2);
c.fill();
c.restore();
}
drawBomb(c) {
const r = this.radius;
c.rotate(this.rotation);
c.shadowColor = BOMB.glow;
c.shadowBlur = 18 + Math.sin(timeAccumulator * 8) * 6;
// Body
const g = c.createRadialGradient(-r*0.3, -r*0.3, r*0.1, 0, 0, r);
g.addColorStop(0, '#555');
g.addColorStop(0.7, '#2a2a2a');
g.addColorStop(1, '#0a0a0a');
c.fillStyle = g;
c.beginPath();
c.arc(0, 0, r, 0, Math.PI * 2);
c.fill();
c.shadowBlur = 0;
// Fuse top
c.strokeStyle = '#8B6F47';
c.lineWidth = 4;
c.beginPath();
c.moveTo(0, -r);
c.quadraticCurveTo(14, -r - 15, 8, -r - 24);
c.stroke();
// Spark
c.fillStyle = '#ffd93d';
c.shadowColor = '#ffd93d';
c.shadowBlur = 14;
c.beginPath();
c.arc(8, -r - 24, 5, 0, Math.PI * 2);
c.fill();
// Inner spark ring
c.fillStyle = '#ff6b3b';
c.beginPath();
c.arc(8, -r - 24, 2.5, 0, Math.PI * 2);
c.fill();
c.shadowBlur = 0;
// Highlight ball
c.beginPath();
c.ellipse(-r*0.35, -r*0.35, r*0.18, r*0.1, -0.5, 0, Math.PI*2);
c.fillStyle = 'rgba(255,255,255,0.18)';
c.fill();
}
drawSpecial(c) {
const r = this.radius;
c.rotate(this.rotation);
c.shadowColor = this.type.glow;
c.shadowBlur = 20 + Math.sin(timeAccumulator * 6) * 8;
// Core sphere
const g = c.createRadialGradient(-r*0.3, -r*0.3, r*0.1, 0, 0, r);
g.addColorStop(0, '#fff');
g.addColorStop(0.4, this.type.glow);
g.addColorStop(1, this.type.glow);
c.fillStyle = g;
c.beginPath();
c.arc(0, 0, r, 0, Math.PI * 2);
c.fill();
c.shadowBlur = 0;
// Inner symbol
c.fillStyle = '#fff';
c.font = 'bold 28px Outfit, sans-serif';
c.textAlign = 'center';
c.textBaseline = 'middle';
if (this.type.name === 'freeze') c.fillText('❄', 0, 1);
else if (this.type.name === 'super') c.fillText('⚡', 0, 1);
else if (this.type.name === 'rain') c.fillText('🌧', 0, 1);
}
}
/* ===== PARTICLES ===== */
const particles = [];
class JuiceParticle {
constructor(x, y, color, vx, vy, size, life) {
this.x = x; this.y = y;
this.vx = vx; this.vy = vy;
this.size = size;
this.color = color;
this.life = life;
this.maxLife = life;
this.gravity = rand(300, 600);
}
update(dt, slowed) {
const f = slowed ? 0.3 : 1;
this.x += this.vx * dt * f;
this.y += this.vy * dt * f;
this.vy += this.gravity * dt * f;
this.life -= dt;
}
draw(c) {
const alpha = clamp(this.life / this.maxLife, 0, 1);
c.globalAlpha = alpha;
c.fillStyle = this.color;
c.beginPath();
c.arc(this.x, this.y, this.size * alpha, 0, Math.PI * 2);
c.fill();
c.globalAlpha = 1;
}
get alive() { return this.life > 0; }
}
class SparkParticle {
constructor(x, y, color, vx, vy, life) {
this.x = x; this.y = y;
this.vx = vx; this.vy = vy;
this.color = color;
this.life = life;
this.maxLife = life;
this.size = rand(2, 5);
}
update(dt) {
this.x += this.vx * dt;
this.y += this.vy * dt;
this.vx *= 0.95;
this.vy *= 0.95;
this.life -= dt;
}
draw(c) {
const a = clamp(this.life / this.maxLife, 0, 1);
c.globalAlpha = a;
c.fillStyle = this.color;
c.beginPath();
c.arc(this.x, this.y, this.size * a, 0, Math.PI * 2);
c.fill();
c.globalAlpha = 1;
}
get alive() { return this.life > 0; }
}
class ScorePopup {
constructor(x, y, text, color, big) {
this.x = x; this.y = y;
this.text = text;
this.color = color;
this.life = 1.2;
this.maxLife = 1.2;
this.vy = -120;
this.big = big;
}
update(dt) {
this.x += 0;
this.y += this.vy * dt;
this.vy *= 0.93;
this.life -= dt;
}
draw(c) {
const a = clamp(this.life / this.maxLife, 0, 1);
c.globalAlpha = a;
c.font = this.big ? 'bold 28px Outfit' : 'bold 20px Outfit';
c.textAlign = 'center';
c.fillStyle = this.color;
c.shadowColor = this.color;
c.shadowBlur = 12;
c.fillText(this.text, this.x, this.y);
c.shadowBlur = 0;
c.globalAlpha = 1;
}
get alive() { return this.life > 0; }
}
const scorePopups = [];
/* ===== GAME OBJECTS POOL ===== */
let objects = [];
/* ===== SLICE TRAIL ===== */
const trailPoints = [];
const MAX_TRAIL = 28;
/* ===== INPUT HANDLING ===== */
let mouseDown = false;
let lastMouseX = 0, lastMouseY = 0;
function getPointerPos(e) {
const rect = canvas.getBoundingClientRect();
if (e.touches && e.touches.length > 0) {
return { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top };
}
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
function onPointerDown(e) {
e.preventDefault();
const p = getPointerPos(e);
mouseDown = true;
lastMouseX = p.x;
lastMouseY = p.y;
trailPoints.length = 0;
}
function onPointerMove(e) {
if (gameState !== State.PLAYING) return;
e.preventDefault();
const p = getPointerPos(e);
// Also track for mouse (mouseDown check) but allow hover-slice for touch
if (e.touches) {
// Touch: always slice on move
handleSlice(lastMouseX, lastMouseY, p.x, p.y);
lastMouseX = p.x;
lastMouseY = p.y;
addTrail(p.x, p.y);
} else if (mouseDown) {
handleSlice(lastMouseX, lastMouseY, p.x, p.y);
lastMouseX = p.x;
lastMouseY = p.y;
addTrail(p.x, p.y);
}
}
function onPointerUp(e) {
mouseDown = false;
finishSwipeCombo();
}
canvas.addEventListener('mousedown', onPointerDown);
canvas.addEventListener('mousemove', onPointerMove);
canvas.addEventListener('mouseup', onPointerUp);
canvas.addEventListener('mouseleave', onPointerUp);
canvas.addEventListener('touchstart', onPointerDown, { passive: false });
canvas.addEventListener('touchmove', onPointerMove, { passive: false });
canvas.addEventListener('touchend', onPointerUp, { passive: false });
function addTrail(x, y) {
trailPoints.push({ x, y, life: 0.4 });
if (trailPoints.length > MAX_TRAIL) trailPoints.shift();
}
/* ===== SLICE LOGIC ===== */
function handleSlice(x1, y1, x2, y2) {
const now = performance.now();
if (now - lastSwipeTime > 500) {
slicesThisSwipe = [];
}
lastSwipeTime = now;
const angle = Math.atan2(y2 - y1, x2 - x1);
for (let i = objects.length - 1; i >= 0; i--) {
const obj = objects[i];
if (!obj.alive || obj.sliced) continue;
if (obj.hitTest(x1, y1, x2, y2)) {
sliceObject(obj, angle, x2, y2);
slicesThisSwipe.push(obj);
}
}
}
function finishSwipeCombo() {
// If multiple fruits sliced rapidly, give combo bonus
if (slicesThisSwipe.length >= 2) {
comboCount = slicesThisSwipe.length;
showCombo(comboCount);
}
slicesThisSwipe = [];
}
function sliceObject(obj, angle, px, py) {
if (obj.isBomb) {
// Bomb sliced! Lose life
obj.sliced = true;
obj.alive = false;
// Big explosion
for (let i = 0; i < 30; i++) {
const ang = rand(0, Math.PI*2);
const sp = rand(200, 600);
particles.push(new SparkParticle(obj.x, obj.y, '#ff6b3b',
Math.cos(ang) * sp, Math.sin(ang) * sp, rand(0.5, 1.0)));
}
for (let i = 0; i < 15; i++) {
const ang = rand(0, Math.PI*2);
const sp = rand(100, 300);
particles.push(new SparkParticle(obj.x, obj.y, '#ffd93d',
Math.cos(ang) * sp, Math.sin(ang) * sp, rand(0.4, 0.8)));
}
shakeAmount = 25;
loseLife();
scorePopups.push(new ScorePopup(obj.x, obj.y - 20, '💥 BOOM!', '#ff3b3b', true));
return;
}
if (obj.isFruit) {
obj.sliced = true;
setTimeout(() => { if (obj._remove) obj.alive = false; }, 100);
obj._remove = true;
// Juice particles
const color = obj.type.rCol2;
for (let i = 0; i < 12; i++) {
const ang = angle + rand(-1, 1);
const sp = rand(80, 350);
particles.push(new JuiceParticle(obj.x, obj.y, color,
Math.cos(ang) * sp, Math.sin(ang) * sp - 60, rand(3, 8), rand(0.5, 1.2)));
}
// Score bump
let pts = 10;
if (superTimer > 0) pts *= 2;
score += pts;
comboHits.push({ time: performance.now(), pts });
scorePopups.push(new ScorePopup(obj.x, obj.y, '+' + pts,
superTimer > 0 ? '#ffd93d' : '#fff5e6', false));
updateHUD();
return;
}
// Specials
if (obj.isSpecial) {
obj.alive = false;
if (obj.type.name === 'freeze') { freezeTimer = 4.0; }
else if (obj.type.name === 'super') { superTimer = 6.0; }
else if (obj.type.name === 'rain') { rainTimer = 5.0; }
// Spark particles
for (let i = 0; i < 25; i++) {
const ang = rand(0, Math.PI*2);
const sp = rand(100, 400);
particles.push(new SparkParticle(obj.x, obj.y, obj.type.glow,
Math.cos(ang) * sp, Math.sin(ang) * sp, rand(0.5, 1.0)));
}
scorePopups.push(new ScorePopup(obj.x, obj.y, obj.type.name.toUpperCase() + '!',
obj.type.glow, true));
updatePowerBadges();
return;
}
}
/* ===== COMBO ===== */
function showCombo(n) {
const el = document.getElementById('comboPopup');
let label = n + 'x COMBO!';
let color = '#ffd93d';
if (n >= 4) { label = '🔥 ' + n + 'x BLADE MASTER!'; color = '#ff5e3a'; }
else if (n >= 3) { label = n + 'x COMBO!'; color = '#ff9f43'; }
el.textContent = label;
el.style.color = color;
el.style.left = (W/2 - 80) + 'px';
el.style.top = '40%';
el.style.transform = 'scale(1.3)';
el.style.opacity = '1';
// bonus
const bonus = n * 5;
score += bonus;
updateHUD();
scorePopups.push(new ScorePopup(W/2, H * 0.45, '+' + bonus + ' BONUS', color, true));
setTimeout(() => {
el.style.opacity = '0';
el.style.transform = 'scale(1)';
}, 800);
}
/* ===== LIFE ===== */
function loseLife() {
lives--;
shakeAmount = 20;
updateHUD();
if (lives <= 0) {
gameOver();
}
}
/* ===== HUD ===== */
function updateHUD() {
document.getElementById('scoreEl').textContent = score;
for (let i = 1; i <= 3; i++) {
const icon = document.querySelector(`.life-icon[data-life="${i}"]`);
if (i <= lives) icon.classList.remove('lost');
else icon.classList.add('lost');
}
if (score > best) {
best = score;
localStorage.setItem('fruitNinjaBest', best);
document.getElementById('bestEl').textContent = best;
}
}
function updatePowerBadges() {
document.getElementById('pb-freeze').classList.toggle('active', freezeTimer > 0);
document.getElementById('pb-super').classList.toggle('active', superTimer > 0);
document.getElementById('pb-rain').classList.toggle('active', rainTimer > 0);
}
/* ===== SPAWNING ===== */
let bombChance = 0.10; // 10% chance per spawn
let specialChance = 0.06;
function spawnWave(dt) {
spawnTimer += dt * 1000;
let interval = spawnInterval / difficulty;
if (rainTimer > 0) interval *= 0.4; // fruit rain spawns faster
if (spawnTimer >= interval) {
spawnTimer = 0;
const batch = rainTimer > 0 ? randInt(2, 4) : (Math.random() < 0.25 ? 2 : 1);
for (let i = 0; i < batch; i++) spawnObject();
}
}
function spawnObject() {
const roll = Math.random();
if (roll < bombChance * difficulty * 0.5) {
// Bomb
objects.push(new GameObject(BOMB, false, false, true));
} else if (roll < bombChance + specialChance) {
// Special
const st = choose(SPECIAL_TYPES);
objects.push(new GameObject(st, false, true, false));
} else {
// Regular fruit
const ft = choose(FRUIT_TYPES);
objects.push(new GameObject(ft, true, false, false));
}
}
/* ===== GAME LOOP ===== */
let lastT = 0;
function loop(ts) {
if (!lastT) lastT = ts;
const dt = Math.min(0.05, (ts - lastT) / 1000);
lastT = ts;
timeAccumulator += dt;
// Clear
ctx.fillStyle = '#1a0a14';
ctx.fillRect(0, 0, W, H);
// Dojo bg pattern
drawBackground();
if (gameState === State.PLAYING) {
// Decrease power-up timers
if (freezeTimer > 0) freezeTimer -= dt;
if (superTimer > 0) superTimer -= dt;
if (rainTimer > 0) rainTimer -= dt;
if (freezeTimer < 0) freezeTimer = 0;
if (superTimer < 0) superTimer = 0;
if (rainTimer < 0) rainTimer = 0;
updatePowerBadges();
// Difficulty ramps with time
difficulty = 1 + timeAccumulator / 80;
// Update combo timer
if (performance.now() - lastSwipeTime > 1500) {
// reset combo
}
// Spawn
spawnWave(dt);
// Update objects
for (let i = objects.length - 1; i >= 0; i--) {
objects[i].update(dt, freezeTimer > 0);
if (!objects[i].alive) objects.splice(i, 1);
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update(dt, freezeTimer > 0);
if (!particles[i].alive) particles.splice(i, 1);
}
// Update score popups
for (let i = scorePopups.length - 1; i >= 0; i--) {
scorePopups[i].update(dt);
if (!scorePopups[i].alive) scorePopups.splice(i, 1);
}
// Update trail
for (let i = trailPoints.length - 1; i >= 0; i--) {
trailPoints[i].life -= dt;
if (trailPoints[i].life <= 0) trailPoints.splice(i, 1);
}
}
// Shake offset
let sx = 0, sy = 0;
if (shakeAmount > 0) {
sx = rand(-shakeAmount, shakeAmount) * 0.5;
sy = rand(-shakeAmount, shakeAmount) * 0.5;
shakeAmount *= 0.88;
if (shakeAmount < 0.3) shakeAmount = 0;
}
ctx.save();
ctx.translate(sx, sy);
// Draw objects
for (const obj of objects) obj.draw(ctx);
// Draw particles
for (const p of particles) p.draw(ctx);
// Score popups
for (const p of scorePopups) p.draw(ctx);
// Slice trail
if (gameState === State.PLAYING) drawTrail();
ctx.restore();
// Freeze overlay
if (freezeTimer > 0) {
ctx.fillStyle = `rgba(79,195,247,${0.08 * Math.min(1, freezeTimer)})`;
ctx.fillRect(0, 0, W, H);
}
requestAnimationFrame(loop);
}
/* ===== BACKGROUND ===== */
function drawBackground() {
// Vertical gradient
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#231019');
g.addColorStop(0.5, '#1e0d14');
g.addColorStop(1, '#15080f');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// Subtle radial glow
const rg = ctx.createRadialGradient(W * 0.5, H * 0.4, 80, W * 0.5, H * 0.4, Math.max(W, H) * 0.7);
rg.addColorStop(0, 'rgba(255,94,58,0.04)');
rg.addColorStop(1, 'rgba(255,94,58,0)');
ctx.fillStyle = rg;
ctx.fillRect(0, 0, W, H);
// Decorative concentric arcs
ctx.strokeStyle = 'rgba(255,94,58,0.03)';
ctx.lineWidth = 1;
for (let i = 0; i < 6; i++) {
ctx.beginPath();
ctx.arc(W * 0.5, H * 0.9, 120 + i * 90, Math.PI, 0);
ctx.stroke();
}
// Lantern particles (floating background dots)
for (let i = 0; i < 25; i++) {
const x = (i * 137.5) % W;
const y = (i * 89.3 + timeAccumulator * 15) % (H + 40) - 20;
const a = 0.04 + Math.sin(timeAccumulator + i) * 0.02;
ctx.fillStyle = `rgba(255,159,67,${a})`;
ctx.beginPath();
ctx.arc(x, y, 1.5, 0, Math.PI * 2);
ctx.fill();
}
}
/* ===== SLICE TRAIL ===== */
function drawTrail() {
if (trailPoints.length < 2) return;
ctx.save();
// Outer glow trail
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
ctx.lineWidth = 20;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(trailPoints[0].x, trailPoints[0].y);
for (let i = 1; i < trailPoints.length; i++) ctx.lineTo(trailPoints[i].x, trailPoints[i].y);
ctx.stroke();
// Core white trail
ctx.strokeStyle = '#fff';
ctx.lineWidth = 5;
ctx.shadowColor = '#ff5e3a';
ctx.shadowBlur = 16;
ctx.beginPath();
ctx.moveTo(trailPoints[0].x, trailPoints[0].y);
for (let i = 1; i < trailPoints.length; i++) ctx.lineTo(trailPoints[i].x, trailPoints[i].y);
ctx.stroke();
ctx.restore();
}
/* ===== SCREEN MANAGEMENT ===== */
function startGame() {
gameState = State.PLAYING;
score = 0;
lives = 3;
difficulty = 1;
spawnTimer = 0;
timeAccumulator = 0;
objects = [];
particles.length = 0;
scorePopups.length = 0;
trailPoints.length = 0;
freezeTimer = 0;
superTimer = 0;
rainTimer = 0;
comboCount = 0;
slicesThisSwipe = [];
shakeAmount = 0;
document.getElementById('screen').classList.add('hidden');
updateHUD();
updatePowerBadges();
}
function gameOver() {
gameState = State.OVER;
if (score > best) {
best = score;
localStorage.setItem('fruitNinjaBest', best);
}
// Show game over screen
setTimeout(() => {
const screen = document.getElementById('screen');
screen.classList.remove('hidden');
document.getElementById('screenTitle').innerHTML =
'Game Over<span class="sub">You scored ' + score + '</span>';
document.getElementById('screenTagline').style.display = 'none';
document.getElementById('featRow').style.display = 'none';
document.getElementById('howto').style.display = 'none';
document.getElementById('startBtn').textContent = 'Play Again';
// Add score display
let scoreDisplay = document.getElementById('scoreDisplay');
if (!scoreDisplay) {
scoreDisplay = document.createElement('div');
scoreDisplay.id = 'scoreDisplay';
scoreDisplay.className = 'score-row';
screen.insertBefore(scoreDisplay, document.getElementById('startBtn'));
}
scoreDisplay.innerHTML = `
<div class="stat"><div class="num">${score}</div><div class="lbl">Score</div></div>
<div class="stat"><div class="num">${best}</div><div class="lbl">Best</div></div>
`;
}, 600);
}
document.getElementById('startBtn').addEventListener('click', () => {
document.getElementById('screenTagline').style.display = '';
document.getElementById('featRow').style.display = '';
document.getElementById('howto').style.display = '';
const sd = document.getElementById('scoreDisplay');
if (sd) sd.remove();
startGame();
});
// Start the loop
requestAnimationFrame(loop);
</script>
</body>
</html>