Piano Fire · your music
by SonicBear35607 lines17.0 KB
<!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>Piano Fire · your music</title>
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at 20% 30%, #1a0f1e, #0c0712);
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
padding: 12px;
}
.game-container {
background: #1e1528;
border-radius: 48px 48px 32px 32px;
box-shadow: 0 20px 40px rgba(0,0,0,0.8), 0 0 0 1px #5a3a5e;
padding: 20px 16px 28px;
max-width: 440px;
width: 100%;
transition: 0.2s;
}
.screen {
background: #120b1a;
border-radius: 28px;
padding: 14px 10px 10px;
box-shadow: inset 0 8px 14px rgba(0,0,0,0.7);
margin-bottom: 14px;
}
.canvas-wrapper {
position: relative;
border-radius: 20px;
overflow: hidden;
background: #0f0818;
box-shadow: 0 0 0 1px #6a4a6e;
}
#gameCanvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 360 / 620;
background: #0f0818;
touch-action: none;
cursor: pointer;
}
.ui-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin: 12px 4px 6px;
color: #f0d6ff;
font-weight: 600;
}
.score-box {
background: #21162b;
padding: 6px 18px;
border-radius: 40px;
font-size: 1.3rem;
font-weight: 700;
color: #ffb3f0;
box-shadow: inset 0 2px 8px #0a0610, 0 2px 0 #6a3e6e;
letter-spacing: 0.5px;
}
.stars-box {
display: flex;
gap: 4px;
background: #1d1225;
padding: 4px 16px;
border-radius: 40px;
border: 1px solid #6a456e;
font-size: 1.5rem;
line-height: 1.4;
}
.control-group {
display: flex;
gap: 8px;
}
.ctrl-btn {
background: #32223b;
border: none;
color: #f2dcff;
font-size: 1.2rem;
font-weight: 700;
padding: 6px 16px;
border-radius: 40px;
box-shadow: 0 4px 0 #140e1a, 0 2px 10px rgba(0,0,0,0.6);
transition: 0.06s linear;
cursor: pointer;
}
.ctrl-btn:active {
transform: translateY(3px);
box-shadow: 0 1px 0 #140e1a;
}
.song-panel {
background: #1b1225;
border-radius: 40px;
padding: 14px 12px;
margin-top: 12px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px 8px;
border: 1px solid #4d3152;
}
.file-label {
background: #5d3a63;
padding: 8px 22px;
border-radius: 50px;
color: white;
font-weight: 600;
font-size: 0.9rem;
box-shadow: 0 3px 0 #2f1c33;
transition: 0.06s;
cursor: pointer;
}
.file-label:active {
transform: translateY(2px);
box-shadow: 0 1px 0 #2f1c33;
}
.song-name {
color: #dbb6f0;
background: #100a18;
padding: 6px 16px;
border-radius: 30px;
border: 1px solid #583d5e;
font-size: 0.8rem;
max-width: 140px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.timer-box {
background: #1d1225;
padding: 4px 16px;
border-radius: 40px;
color: #ffc8f0;
font-weight: 700;
font-size: 1.2rem;
border: 1px solid #7a4d7a;
}
input[type="file"] { display: none; }
.hint { color: #9a7a9e; font-size: 0.7rem; margin-left: 4px; }
</style>
</head>
<body>
<div class="game-container">
<div class="screen">
<div class="canvas-wrapper">
<canvas id="gameCanvas" width="360" height="620"></canvas>
</div>
<div class="ui-bar">
<div class="score-box">🪙 <span id="scoreDisplay">0</span></div>
<div class="stars-box" id="starsDisplay">☆☆☆</div>
<div class="timer-box" id="timerDisplay">30s</div>
</div>
</div>
<div class="song-panel">
<label>
<input type="file" id="musicFileInput" accept=".json,.txt,.music">
<span class="file-label" id="fileUploadLabel">🎵 upload your music</span>
</label>
<span class="song-name" id="currentSongLabel">no song</span>
<span class="hint">(only you)</span>
<button class="ctrl-btn" id="restartBtn" style="padding:4px 18px;">⟳</button>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
const starsSpan = document.getElementById('starsDisplay');
const timerSpan = document.getElementById('timerDisplay');
const songLabel = document.getElementById('currentSongLabel');
const W = 360, H = 620;
canvas.width = W; canvas.height = H;
const LANE_COUNT = 4;
const LANE_W = W / LANE_COUNT;
const TILE_H = 68;
const TILE_SPEED = 2.9;
const HIT_ZONE_Y = 490;
const HIT_ZONE_H = 44;
// game state
let tiles = [];
let score = 0;
let stars = 0;
let timeLeft = 30; // seconds
let gameActive = false;
let gameOverFlag = false;
let gameFinished = false; // reached 3 stars
let animId = null;
let frameCount = 0;
// music data
let musicNotes = [];
let loadedSongName = 'no song';
let songStartTime = 0;
let currentPlayhead = 0;
let songPaused = true;
// default demo song (so it works out of box)
function generateDemo() {
const notes = [];
for (let i = 0; i < 40; i++) {
const lane = i % 4;
const time = i * 220 + 60;
notes.push({ lane, time });
}
for (let i = 0; i < 18; i++) {
const lane = (i + 2) % 4;
const time = 1400 + i * 190;
notes.push({ lane, time });
}
notes.sort((a,b) => a.time - b.time);
return notes;
}
musicNotes = generateDemo();
loadedSongName = 'demo';
songLabel.textContent = loadedSongName;
// reset flags for notes
function resetNoteSpawned() {
musicNotes.forEach(n => n._spawned = false);
}
resetNoteSpawned();
// UI updates
function updateScoreDisplay() {
scoreSpan.textContent = score;
}
function updateStars() {
stars = 0;
if (score >= 10) stars = 1;
if (score >= 25) stars = 2;
if (score >= 45) stars = 3;
let str = '';
for (let i=0; i<3; i++) str += (i < stars) ? '★' : '☆';
starsSpan.textContent = str;
if (stars >= 3 && gameActive && !gameOverFlag && !gameFinished) {
// finished with 3 stars
gameFinished = true;
gameActive = false;
songPaused = true;
gameOverFlag = false; // show win state
}
}
function updateTimer() {
timerSpan.textContent = Math.ceil(timeLeft) + 's';
}
// spawn tiles based on playhead
function spawnTiles(playheadMs) {
if (!gameActive && !gameOverFlag && !gameFinished) return;
const margin = 60;
for (let note of musicNotes) {
if (!note._spawned && note.time <= playheadMs + margin) {
note._spawned = true;
const lane = note.lane;
// avoid stacking same lane too close
const blocked = tiles.some(t => t.lane === lane && t.y < TILE_H + 20);
if (!blocked) {
tiles.push({
lane: lane,
y: -TILE_H - 10,
time: note.time,
active: true
});
}
}
}
}
function updateTiles() {
if (!gameActive) return;
for (let i = tiles.length-1; i>=0; i--) {
const t = tiles[i];
t.y += TILE_SPEED;
if (t.y > H + 30) {
tiles.splice(i,1);
// miss -> game over (unless finished)
if (gameActive && !gameOverFlag && !gameFinished) {
gameOver();
return;
}
}
}
}
function gameOver() {
if (gameFinished) return;
gameActive = false;
gameOverFlag = true;
songPaused = true;
}
// hit
function handleTap(laneIndex) {
if (!gameActive || gameOverFlag || gameFinished) return;
// find closest tile in lane (lowest y)
let target = null;
let targetIdx = -1;
for (let i = tiles.length-1; i>=0; i--) {
if (tiles[i].lane === laneIndex && tiles[i].active) {
target = tiles[i];
targetIdx = i;
break;
}
}
if (!target) {
gameOver();
return;
}
const y = target.y;
const hitLow = HIT_ZONE_Y - HIT_ZONE_H/2;
const hitHigh = HIT_ZONE_Y + HIT_ZONE_H/2;
if (y >= hitLow && y <= hitHigh) {
tiles.splice(targetIdx, 1);
score++;
updateScoreDisplay();
updateStars();
// coin collect effect (visual only)
} else {
gameOver();
}
}
// ----- drawing (pink theme) -----
function draw() {
ctx.clearRect(0, 0, W, H);
// lanes
for (let i=0; i<LANE_COUNT; i++) {
const x = i*LANE_W;
ctx.fillStyle = '#1b1124';
ctx.fillRect(x, 0, LANE_W-1, H);
ctx.fillStyle = '#281d30';
ctx.fillRect(x+1, 0, LANE_W-2, H);
ctx.strokeStyle = '#4d2f55';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x+LANE_W, 0);
ctx.lineTo(x+LANE_W, H);
ctx.stroke();
}
// hit zone (glowing pink)
const hitY = HIT_ZONE_Y;
ctx.shadowColor = '#ff66c0';
ctx.shadowBlur = 24;
ctx.fillStyle = 'rgba(255, 80, 180, 0.15)';
ctx.fillRect(0, hitY - HIT_ZONE_H/2, W, HIT_ZONE_H);
ctx.shadowBlur = 0;
ctx.strokeStyle = '#ff7ac8';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(0, hitY - HIT_ZONE_H/2);
ctx.lineTo(W, hitY - HIT_ZONE_H/2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, hitY + HIT_ZONE_H/2);
ctx.lineTo(W, hitY + HIT_ZONE_H/2);
ctx.stroke();
// tiles (pink)
for (let t of tiles) {
const x = t.lane * LANE_W;
const y = t.y;
const grad = ctx.createLinearGradient(x, y, x+LANE_W, y+TILE_H);
grad.addColorStop(0, '#ff77c8');
grad.addColorStop(0.5, '#e84393');
grad.addColorStop(1, '#b8216b');
ctx.fillStyle = grad;
ctx.shadowColor = '#ff66b0';
ctx.shadowBlur = 22;
ctx.beginPath();
ctx.roundRect(x+5, y, LANE_W-10, TILE_H-3, 12);
ctx.fill();
ctx.shadowBlur = 0;
// inner glow
ctx.fillStyle = 'rgba(255,200,230,0.3)';
ctx.beginPath();
ctx.roundRect(x+10, y+5, LANE_W-20, 12, 6);
ctx.fill();
// note
ctx.fillStyle = '#fff0f5';
ctx.font = 'bold 28px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('♩', x + LANE_W/2, y + TILE_H/2 + 2);
}
// win / game over overlay
if (gameFinished) {
ctx.fillStyle = 'rgba(20,6,30,0.7)';
ctx.fillRect(0,0,W,H);
ctx.fillStyle = '#ffb3e6';
ctx.font = 'bold 38px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('✨ 3 stars!', W/2, H/2 - 30);
ctx.fillStyle = '#f0c0e0';
ctx.font = '20px sans-serif';
ctx.fillText('🎵 perfect!', W/2, H/2 + 30);
} else if (gameOverFlag) {
ctx.fillStyle = 'rgba(10,4,16,0.7)';
ctx.fillRect(0,0,W,H);
ctx.fillStyle = '#ff99cc';
ctx.font = 'bold 36px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('💔 GAME OVER', W/2, H/2 - 20);
ctx.fillStyle = '#cc99bb';
ctx.font = '18px sans-serif';
ctx.fillText('restart ↺', W/2, H/2 + 40);
} else if (!gameActive && !gameOverFlag && !gameFinished) {
ctx.fillStyle = 'rgba(12,6,18,0.6)';
ctx.fillRect(0,0,W,H);
ctx.fillStyle = '#dbb0e0';
ctx.font = '22px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🎹 upload your music', W/2, H/2 - 10);
ctx.fillStyle = '#a07aae';
ctx.font = '16px sans-serif';
ctx.fillText('then tap play (restart)', W/2, H/2 + 40);
}
// song name
ctx.fillStyle = '#a080aa';
ctx.font = '12px monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText(loadedSongName || '—', W-10, H-8);
}
// roundRect
CanvasRenderingContext2D.prototype.roundRect = function (x,y,w,h,r) {
if (w<2*r) r=w/2; if (h<2*r) r=h/2;
this.moveTo(x+r,y);
this.lineTo(x+w-r,y);
this.quadraticCurveTo(x+w,y,x+w,y+r);
this.lineTo(x+w,y+h-r);
this.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
this.lineTo(x+r,y+h);
this.quadraticCurveTo(x,y+h,x,y+h-r);
this.lineTo(x,y+r);
this.quadraticCurveTo(x,y,x+r,y);
this.closePath();
return this;
};
// ----- game loop -----
let lastTimestamp = 0;
function gameLoop(ts) {
if (!lastTimestamp) lastTimestamp = ts;
const delta = Math.min(ts - lastTimestamp, 30);
lastTimestamp = ts;
if (gameActive && !songPaused) {
currentPlayhead += delta;
spawnTiles(currentPlayhead);
updateTiles();
// timer
if (gameActive) {
timeLeft -= delta / 1000;
if (timeLeft <= 0) {
timeLeft = 0;
updateTimer();
// time up -> game over if not finished
if (!gameFinished && gameActive) {
gameOver();
}
} else {
updateTimer();
}
}
updateStars();
}
draw();
animId = requestAnimationFrame(gameLoop);
}
// ----- reset / start -----
function resetGame() {
tiles = [];
score = 0;
stars = 0;
timeLeft = 30;
gameFinished = false;
gameOverFlag = false;
gameActive = true;
songPaused = false;
currentPlayhead = 0;
resetNoteSpawned();
songStartTime = performance.now();
updateScoreDisplay();
updateStars();
updateTimer();
// spawn initial tiles
spawnTiles(0);
}
function stopAndReset() {
gameActive = false;
gameOverFlag = false;
gameFinished = false;
songPaused = true;
tiles = [];
score = 0;
stars = 0;
timeLeft = 30;
currentPlayhead = 0;
resetNoteSpawned();
updateScoreDisplay();
updateStars();
updateTimer();
draw();
}
// load music from file (only you)
function loadMusicFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
let parsed = JSON.parse(content);
if (Array.isArray(parsed) && parsed.length>0 && parsed[0].lane!==undefined && parsed[0].time!==undefined) {
musicNotes = parsed.map(n => ({ lane: Number(n.lane), time: Number(n.time) }));
musicNotes.sort((a,b) => a.time - b.time);
resetNoteSpawned();
loadedSongName = file.name.replace(/\.[^.]+$/, '');
songLabel.textContent = loadedSongName;
stopAndReset();
// auto start after load?
resetGame();
} else {
alert('Format: array of {lane, time}');
}
} catch(err) {
alert('Invalid JSON file. Use [{"lane":0,"time":100}, ...]');
}
};
reader.readAsText(file);
}
// ----- event binding -----
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const x = (e.clientX - rect.left) * scaleX;
if (x<0 || x>W) return;
const lane = Math.floor(x / LANE_W);
if (lane>=0 && lane<LANE_COUNT) handleTap(lane);
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const touch = e.touches[0];
const scaleX = canvas.width / rect.width;
const x = (touch.clientX - rect.left) * scaleX;
if (x<0 || x>W) return;
const lane = Math.floor(x / LANE_W);
if (lane>=0 && lane<LANE_COUNT) handleTap(lane);
}, { passive: false });
document.getElementById('restartBtn').addEventListener('click', () => {
stopAndReset();
resetGame();
});
document.getElementById('musicFileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) loadMusicFile(file);
e.target.value = '';
});
// start loop
animId = requestAnimationFrame(gameLoop);
// auto start with demo
setTimeout(() => { resetGame(); }, 300);
// ensure timer updates even when idle
setInterval(() => {
if (!gameActive && !gameOverFlag && !gameFinished) {
updateTimer();
}
}, 500);
})();
</script>
</body>
</html>Game Source: Piano Fire · your music
Creator: SonicBear35
Libraries: none
Complexity: complex (607 lines, 17.0 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: piano-fire-your-music-sonicbear35" to link back to the original. Then publish at arcadelab.ai/publish.