An interactive, browser-based 2D Beyblade battle game built with vanilla HTML5 Canvas, CSS, and JavaScript. Customize your spinning top, power-up your launch, and battle an AI rival in a physics-driven stadium.
bladium/
├── index.html # Entry point — all UI screens, canvas, button markup
├── css/
│ └── style.css # All styles, animations, overlays, HUD, responsive layout
└── js/
├── parts.js # Part definitions (15 parts: 5 top / 5 mid / 5 bot)
├── beyblade.js # Beyblade class — physics, rendering, collision, stat modifiers
├── particles.js # Particle & ParticleSystem classes for VFX
└── game.js # Main controller — game loop, state machine, AI, input, rendering
No dependencies, no build step. Open index.html in any browser to play.
MENU ──[START]──▶ CUSTOMIZE ──[READY]──▶ LAUNCH ──[LAUNCH!]──▶ BATTLE ──[end]──▶ RESULT
▲ ▲ │
│ └────────────────────────[RECUSTOMIZE]────────────────────────────────┘
└──────────────────────────────────[REMATCH]──────────────────────────────────────────────┘
| State | Screen | What happens |
|---|---|---|
MENU |
#menuScreen |
Title, START button |
CUSTOMIZE |
#customizeScreen |
Swap top/mid/bot parts, live preview, stat bars |
LAUNCH |
#launchScreen |
Tap/click to fill power meter, LAUNCH button |
BATTLE |
Canvas + #battleHud |
Beys fight in stadium, mouse-driven, HUD visible |
RESULT |
#resultScreen |
Winner, collision stats, rematch/recustomize buttons |
Managed by showScreen(state) in game.js — hides all overlays, shows the target one.
Three slot types, each with 5 parts of varying rarity (common → epic):
| Slot | Role | Stat focus | Array |
|---|---|---|---|
| Top (Attack Ring) | Primary ATK contributor | ATK | TOP_PARTS |
| Middle (Forge Disc) | Primary DEF contributor | DEF | MIDDLE_PARTS |
| Bottom (Performance Tip) | Primary SPD contributor | SPD | BOTTOM_PARTS |
Each part object:
{ id: 'flame', name: 'Flame Ring', atk: 3, def: 1, spd: 1,
color: '#ff4444', accentColor: '#ff8866', rarity: 'common' }Key functions:
calculateStats(top, mid, bot)→{ atk, def, spd, stamina }— sums contributions, stamina derived from DEF + SPDgetPart(type, index)/getPartIndex(type, partId)— accessor helpers
Selection state in game.js: selectedTopIdx, selectedMidIdx, selectedBotIdx.
class Beyblade(x, y, topPart, midPart, botPart, isPlayer)
| Property | Type | Description |
|---|---|---|
x, y |
float | Position in canvas space |
vx, vy |
float | Velocity vector |
targetX, targetY |
float | Desired position (set via setTarget()) |
radius |
number | Collision/draw radius (default 28) |
spinSpeed |
0–100 | Current spin percentage — decays over time, determines movement capability |
angle |
radians | Current rotation angle for visual spinning |
baseStats |
{atk,def,spd,stamina} |
Calculated from parts, never changes |
currentStats |
{atk,def,spd,stamina} |
Base stats (can be modified temporarily) |
statModifiers |
{atk,def,spd} |
Temporary ± modifiers from collision RNG |
statModifierTimers |
{atk,def,spd} |
Remaining frames for each modifier |
| Method | Purpose |
|---|---|
getEffectiveAtk() |
currentStats.atk + statModifiers.atk, clamped ≥ 0 |
getEffectiveDef() |
Same pattern for DEF |
getEffectiveSpd() |
Same pattern for SPD |
getSpeed() |
Magnitude of velocity vector |
isAlive() |
spinSpeed > 5 |
launch(power) |
Sets spinSpeed from power (clamped 20–100), gives initial velocity. Overcharge (>85) adds wobble |
setTarget(tx, ty) |
Sets target position for movement |
update(dt, stadiumRadius, cx, cy) |
Full physics tick — spin decay, movement toward target, friction, speed clamp, stadium boundary bounce, trail |
collide(other) → result | null |
Checks overlap, calculates power/defense with RNG, applies knockback + spin damage + stat changes, returns collision data |
applyStatMod(stat, amount, duration) |
Adds temporary stat modifier for N frames |
applyRngStatChange() → {stat, amount, isBuff} | null |
25% buff / 20% debuff on random stat for ~3s |
draw(ctx) |
Full beyblade render — trail, three spinning layers, center dot, flash, modifier indicators |
drawTopPart(ctx) |
8-spike star ring using topPart.color |
drawMiddlePart(ctx) |
Radial gradient disc using midPart colors |
drawBottomPart(ctx) |
Small center circle with cross using botPart.color |
- Bey accelerates toward
targetX/Ywith force proportional toSPD × spinSpeed% - Max speed clamped to
SPD × 0.055 × spinSpeed% - Friction of 0.98 applied each frame
- Stadium boundary bounce (elastic reflection, 0.7 damping, −2 spin)
thisPower = ATK_eff × (spinSpeed/100) × (moveSpeed + 1)
otherPower = ATK_eff × (spinSpeed/100) × (moveSpeed + 1)
thisDef = DEF_eff × (spinSpeed/100)
rng = random(0.75, 1.25)
thisImpact = (thisPower - otherDef) × rng
otherImpact = (otherPower - thisDef) × rng
winner = thisImpact > otherImpact ? this : other
loserSpinLoss = intensity × 0.08
winnerSpinLoss = intensity × 0.02
| Class | Purpose |
|---|---|
Particle |
Single particle with position, velocity, gravity, life, color, size |
ParticleSystem |
Manages array of particles |
ParticleSystem methods:
burst(x, y, count, colors, intensity)— radial explosionspark(x, y, count, color)— fast small sparksring(x, y, radius, count, color)— expanding ringupdate()/draw(ctx)/clear()
Used for: launch taps (sparks), collision impacts (burst + ring).
| Variable | Type | Purpose |
|---|---|---|
gameState |
GameState enum |
Current screen |
selectedTopIdx/MidIdx/BotIdx |
int | Part selection indices |
launchPower |
0–100 | Current charge level |
launchCharging |
bool | Whether still accepting taps |
playerBey, rivalBey |
Beyblade |
Active beyblade instances |
particles |
ParticleSystem |
Global particle manager |
stadiumCenterX/Y, stadiumRadius |
float | Computed from canvas size |
mouseX/Y, mouseActive |
float, bool | Cursor tracking for steering |
screenShake |
float | Decaying shake intensity |
aiBehavior |
string | `'patrol' |
| Function | Purpose |
|---|---|
resizeCanvas() |
Sets canvas to window size, recalculates stadium |
showScreen(state) |
Hides all overlays, shows target, resets relevant state |
updateCustomizeUI() |
Refreshes part names, stat bars, totals from selection indices |
drawPreviewBey() |
Renders a spinning preview on #previewCanvas using temp Beyblade |
startPreviewAnimation() |
RAF loop for preview — auto-cancels on leaving CUSTOMIZE |
resetLaunchState() |
Resets power meter to 0 |
handleLaunchTap() |
+7 power per tap, clamps to 100, spawns spark particles |
updateLaunchUI() |
Updates power bar fill, marker, text, button state |
startBattle() |
Creates both beys, applies launch, resets battle state |
updateBattleHud() |
Syncs spin bars, labels, timer from bey state |
updateAI() |
AI behavior decision tree + target position update |
showToast(msg, isBuff) |
Shows floating stat-change notification |
endBattle() |
Determines winner, populates result screen |
drawStadium() |
Renders the circular arena |
renderBattle() |
Full frame — background, stadium, beys, particles, screen shake |
gameLoop(timestamp) |
Main RAF loop — handles all state updates and rendering |
Every N frames:
if rival.spinSpeed < 30 → defensive (flee)
else if close to player + 50% → charge (rush)
else if roll < 0.35 → aggressive (pursue)
else if roll < 0.70 → patrol (circle)
else → defensive (keep distance)
| Event | Action |
|---|---|
mousemove / touchmove |
Track cursor for steering + launch tap location |
mouseleave / touchend |
Set mouseActive = false |
click / touchstart |
Trigger handleLaunchTap() during LAUNCH state |
Keyboard Space |
Tap to charge during LAUNCH |
Keyboard L |
Instant launch if power ≥ 20 |
- Each tap adds 7 power (max 100)
- Power decays at 0.15/frame after 400ms of inactivity
- Green zone (60–85%): +10 bonus to initial spin speed
- Overcharge (>85%): +5 bonus but 2 seconds of wobble (erratic movement)
- Launch button disabled until power ≥ 20
| Element | Technique |
|---|---|
| Beyblade layers | 3 concentric rotating canvases with spokes, gradients, star-spike attack ring |
| Spin trails | Fading position history drawn as translucent circles |
| Collision flash | White ring overlay fading over 10 frames |
| Screen shake | Random ctx.translate offset decaying at 0.9/frame |
| Particles | Burst + expanding ring on collision; sparks on launch tap |
| Stat modifiers | Floating colored text orbiting the bey (+2 ATK, -1 DEF, etc.) |
| Power meter | Gradient fill bar with zone labels (Weak/Good/Perfect/Over) and animated marker |
| HUD | Semi-transparent panels with gradient spin bars, timer in gold |
- Canvas fills viewport:
canvas.width/height = window.innerWidth/Height - Stadium radius:
min(canvas.width, canvas.height) × 0.38 - Stadium center:
(canvas.width/2, canvas.height/2) - Beyblade radius: 28px (preview: 70px)
- All coordinates in canvas pixel space
- Screen shake offsets translation matrix temporarily
Edit js/parts.js — add object to TOP_PARTS, MIDDLE_PARTS, or BOTTOM_PARTS array with id, name, atk, def, spd, color, accentColor, rarity.
Key constants in js/game.js:
TAP_POWER_GAIN(7) — power per tapLAUNCH_DECAY_RATE(0.15) — power loss per framelaunchBonusinstartBattle()— perfect/overcharge bonuses
Key constants in js/beyblade.js:
spinDecayRate(0.015) — base spin loss per framefriction(0.98) — velocity damping- Collision:
loserSpinLoss = intensity × 0.08,winnerSpinLoss = intensity × 0.02 - Collision: RNG range
0.75 + random() × 0.5(±25%) applyRngStatChange(): 25% buff chance, 20% debuff chance
Edit startBattle() → rivalPower formula, or updateAI() behavior probabilities/timers.
- Add state to
GameStateenum - Add overlay HTML in
index.html - Add case in
showScreen() - Handle in
gameLoop()if needed
- Works on all modern browsers (Chrome, Firefox, Safari, Edge)
- Touch support for mobile devices
- No server required — open
index.htmldirectly - Google Fonts loaded via CDN (Orbitron + Rajdhani)
- Uses CSS
backdrop-filter(gracefully degrades)