<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>Jogo do Quadrado - Canvas</title>
<style>
/* Centraliza o canvas na tela e define fundo da página */
body { background: #f0f0f0; display: flex; justify-content: center; align-items: center; height: 100vh; }
/* Estilo do canvas: fundo branco e borda preta */
canvas { background: #fff; border: 2px solid #222; }
</style>
</head>
<body>
<!-- Área do jogo: 400px de largura, 600px de altura -->
<canvas id="game" width="400" height="600"></canvas>
<script>
// Pega o elemento canvas e o contexto 2D para desenhar
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// Define o jogador como um quadrado azul com posição inicial, tamanho e velocidade
const player = { x: 180, y: 500, w: 40, h: 40, speed: 6 };
// Array para armazenar obstáculos
let obstacles = [];
// Controle do estado do jogo e pontuação
let gameOver = false;
let score = 0;
// Função para adicionar novos obstáculos com largura e posição aleatória no topo da tela
function addObstacle() {
const w = 40 + Math.random() * 60; // largura entre 40 e 100
const x = Math.random() * (canvas.width - w); // posição X aleatória
// Cria o obstáculo logo acima da tela e velocidade aleatória
obstacles.push({ x, y: -50, w, h: 30, speed: 3 + Math.random() * 3 });
}
// Desenha o jogador (quadrado azul)
function drawPlayer() {
ctx.fillStyle = '#2196f3';
ctx.fillRect(player.x, player.y, player.w, player.h);
}
// Desenha todos os obstáculos (retângulos vermelhos)
function drawObstacles() {
ctx.fillStyle = '#e53935';
obstacles.forEach(obs => {
ctx.fillRect(obs.x, obs.y, obs.w, obs.h);
});
}
// Move todos os obstáculos para baixo e remove os que já passaram
function moveObstacles() {
obstacles.forEach(obs => {
obs.y += obs.speed; // move cada obstáculo para baixo
});
// Remove obstáculos que passaram do final da tela e aumenta score
obstacles = obstacles.filter(obs => {
if (obs.y > canvas.height) {
score += 1; // Ganha ponto se desviar do obstáculo
return false; // Remove do array
}
return true;
});
}
// Verifica colisão entre o jogador e qualquer obstáculo
function checkCollision() {
for (const obs of obstacles) {
if (
player.x < obs.x + obs.w &&
player.x + player.w > obs.x &&
player.y < obs.y + obs.h &&
player.y + player.h > obs.y
) {
gameOver = true; // Se colidir, fim de jogo!
break;
}
}
}
// Escuta as teclas de seta para mover o jogador, se o jogo não acabou
document.addEventListener('keydown', e => {
if (gameOver) return;
if (e.key === 'ArrowLeft' && player.x > 0) player.x -= player.speed;
if (e.key === 'ArrowRight' && player.x + player.w < canvas.width) player.x += player.speed;
if (e.key === 'ArrowUp' && player.y > 0) player.y -= player.speed;
if (e.key === 'ArrowDown' && player.y + player.h < canvas.height) player.y += player.speed;
});
// Função principal do jogo (loop): desenha, move, verifica colisão, pontua
function loop() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // Limpa tela
drawPlayer(); // Desenha o jogador
drawObstacles(); // Desenha obstáculos
moveObstacles(); // Move obstáculos
checkCollision(); // Verifica colisão
// Exibe a pontuação no canto superior esquerdo
ctx.fillStyle = '#222';
ctx.font = '20px Arial';
ctx.fillText('Score: ' + score, 10, 25);
// Se perdeu, exibe mensagem de fim de jogo
if (gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.font = '40px Arial';
ctx.fillText('Game Over', 100, 300);
ctx.font = '24px Arial';
ctx.fillText('Score: ' + score, 150, 340);
ctx.fillText('Recarregue a página para jogar de novo', 35, 390);
return; // Não continua o loop
}
// Continua o jogo chamando o próximo quadro de animação
requestAnimationFrame(loop);
}
// Cria novos obstáculos automaticamente a cada segundo
setInterval(() => {
if (!gameOver) addObstacle();
}, 1000);
// Inicia o jogo chamando o loop
loop();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8" />
<title>Snake Game — Canvas</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
/>
<style>
:root {
--bg1: #0f0f13;
--bg2: #1a1a22;
--panel: #101018cc;
--accent: #34d399; /* verde menta */
--accent-dark: #10b981;
--danger: #ef4444;
--text: #e5e7eb;
--muted: #9ca3af;
--shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
--radius: 16px;
--gap: 12px;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
}
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu,
"Helvetica Neue", Arial;
color: var(--text);
background: radial-gradient(
1200px 1200px at 10% 10%,
#232335 0%,
transparent 60%
),
radial-gradient(1200px 1200px at 90% 20%, #1b2a2a 0%, transparent 55%),
linear-gradient(160deg, var(--bg1), var(--bg2) 60%);
display: flex;
align-items: center;
justify-content: center;
/* paddings mínimos, mas considerados no cálculo do JS */
padding: max(8px, env(safe-area-inset-top))
max(8px, env(safe-area-inset-right))
max(8px, env(safe-area-inset-bottom))
max(8px, env(safe-area-inset-left));
}
.wrap {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--gap);
width: min(92vw, 820px);
/* importante: o JS vai cuidar da altura do stage para caber em 100% do viewport */
}
.panel {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
background: var(--panel);
border: 1px solid #232334;
border-radius: var(--radius);
padding: 10px 12px;
backdrop-filter: blur(8px);
box-shadow: var(--shadow);
}
.title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
letter-spacing: 0.3px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
box-shadow: 0 0 14px #34d39999;
}
.stats {
display: flex;
gap: 14px;
align-items: center;
font-variant-numeric: tabular-nums;
}
.badge {
background: #151521;
border: 1px solid #2a2a3a;
border-radius: 999px;
padding: 6px 10px;
font-size: 13px;
color: var(--muted);
}
.btn {
appearance: none;
border: 1px solid #2a2a3a;
background: #151521;
color: var(--text);
border-radius: 10px;
padding: 8px 12px;
cursor: pointer;
}
.btn:hover {
background: #1b1b2a;
}
.btn-accent {
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
border: none;
color: #042016;
font-weight: 700;
}
.btn-danger {
background: #2a1214;
border: 1px solid #4d1c20;
color: #fda4a4;
}
.stage {
position: relative;
/* width/height serão definidos via JS para caber 100% do alto */
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
border: 1px solid #24243a;
background: radial-gradient(
1200px 800px at 50% 10%,
#151826 0%,
#10131f 70%
);
}
canvas {
width: 100%;
height: 100%;
display: block;
}
/* Overlay */
.overlay {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(6px);
}
.overlay.show {
display: flex;
}
.card {
width: min(92%, 420px);
background: #0f1222ee;
border: 1px solid #272a45;
border-radius: 16px;
padding: 18px;
text-align: center;
box-shadow: var(--shadow);
}
.card h2 {
margin: 6px 0 8px 0;
font-size: 28px;
}
.card p {
margin: 6px 0;
color: var(--muted);
}
.row {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 12px;
}
/* D-Pad Mobile */
.dpad {
position: absolute;
right: 12px;
bottom: 12px;
display: grid;
grid-template-columns: 60px 60px 60px;
grid-template-rows: 60px 60px 60px;
gap: 8px;
opacity: 0.92;
user-select: none;
}
.dpad button {
border: none;
border-radius: 12px;
background: #0f1424aa;
color: #e8fdf5;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35), inset 0 0 0 1px #2a2f4a;
backdrop-filter: blur(6px);
font-size: 20px;
line-height: 60px;
text-align: center;
padding: 0;
cursor: pointer;
touch-action: manipulation;
}
.dpad .ghost {
visibility: hidden;
}
.dpad button:active {
outline: 2px solid #34d39966;
}
@media (max-width: 480px) {
.dpad {
grid-template-columns: 56px 56px 56px;
grid-template-rows: 56px 56px 56px;
}
.dpad button {
line-height: 56px;
}
}
.hint {
font-size: 12px;
color: var(--muted);
text-align: center;
opacity: 0.8;
}
</style>
</head>
<body>
<div class="wrap">
<div class="panel" id="panel">
<div class="title">
<span class="dot"></span> <span>Snake — Edição Canvas</span>
</div>
<div class="stats">
<span class="badge">Score: <b id="uiScore">0</b></span>
<span class="badge">Recorde: <b id="uiBest">0</b></span>
<button id="btnPause" class="btn">Pausar</button>
<button id="btnRestart" class="btn btn-accent">Reiniciar</button>
</div>
</div>
<div class="stage" id="stage">
<canvas id="snake"></canvas>
<!-- Overlay -->
<div id="overlay" class="overlay">
<div class="card">
<h2 id="ovTitle">Game Over</h2>
<p id="ovScore">Score: 0</p>
<p id="ovTip">Dica: mantenha o controle e antecipe as curvas!</p>
<div class="row">
<button class="btn btn-accent" id="ovPlay">Jogar de novo</button>
<button class="btn btn-danger" id="ovClose">Fechar</button>
</div>
</div>
</div>
<!-- D-Pad Mobile -->
<div class="dpad" id="dpad" aria-label="Controles de toque">
<button class="ghost"></button>
<button data-dir="UP">↑</button>
<button class="ghost"></button>
<button data-dir="LEFT">←</button>
<button class="ghost"></button>
<button data-dir="RIGHT">→</button>
<button class="ghost"></button>
<button data-dir="DOWN">↓</button>
<button class="ghost"></button>
</div>
</div>
<div class="hint" id="hint">
Desktop: setas do teclado • Mobile: botões / gesto de deslizar
</div>
</div>
<script>
// ========= Medição de layout para ocupar 100% da vertical =========
const wrap = document.querySelector(".wrap");
const panel = document.getElementById("panel");
const stage = document.getElementById("stage");
const hint = document.getElementById("hint");
function getViewportHeight() {
// Usa visualViewport em mobile p/ considerar barras dinâmicas
return Math.floor(
(window.visualViewport && window.visualViewport.height) ||
window.innerHeight
);
}
function getBodyPadding() {
const cs = getComputedStyle(document.body);
return {
top: parseFloat(cs.paddingTop) || 0,
bottom: parseFloat(cs.paddingBottom) || 0,
left: parseFloat(cs.paddingLeft) || 0,
right: parseFloat(cs.paddingRight) || 0,
};
}
function fitStageBox() {
const vh = getViewportHeight();
const pad = getBodyPadding();
const wrapWidth = wrap.clientWidth;
const panelH = panel.offsetHeight;
const hintH = hint.offsetHeight;
const gaps = parseFloat(getComputedStyle(wrap).rowGap) || 12; // entre panel-stage e stage-hint
// Altura disponível = viewport - paddings - painel - dica - gaps duplos
const availH = vh - pad.top - pad.bottom - panelH - hintH - gaps * 2;
const availW = wrapWidth; // já limitado por 92vw/820px no CSS
const size = Math.max(120, Math.floor(Math.min(availW, availH))); // mínimo seguro
stage.style.width = size + "px";
stage.style.height = size + "px";
}
// ========= Canvas e jogo =========
const canvas = document.getElementById("snake");
const ctx = canvas.getContext("2d");
const overlay = document.getElementById("overlay");
const uiScore = document.getElementById("uiScore");
const uiBest = document.getElementById("uiBest");
const btnPause = document.getElementById("btnPause");
const btnRestart = document.getElementById("btnRestart");
const ovPlay = document.getElementById("ovPlay");
const ovClose = document.getElementById("ovClose");
const dpad = document.getElementById("dpad");
const DPR = Math.max(1, Math.floor(window.devicePixelRatio || 1));
const COLS = 20,
ROWS = 20;
let BOX = 20;
function fitCanvas() {
const rect = stage.getBoundingClientRect();
const size = Math.floor(Math.min(rect.width, rect.height));
BOX = Math.floor(size / COLS);
const cssSize = BOX * COLS; // garante múltiplo exato
canvas.style.width = cssSize + "px";
canvas.style.height = cssSize + "px";
canvas.width = cssSize * DPR;
canvas.height = cssSize * DPR;
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
function fitAll() {
fitStageBox();
fitCanvas();
}
// Fit inicial
fitAll();
// Refit em resize/scroll (mobile muda viewport ao mostrar/ocultar barras)
let resizeTimeout;
["resize", "orientationchange", "scroll"].forEach((evt) => {
window.addEventListener(
evt,
() => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
const oldDir = direction;
fitAll();
restart(); // recomeça mantendo tudo visível (mantém simples)
direction = nextDirection = oldDir || "RIGHT";
}, 100);
},
{ passive: true }
);
});
// ========= Estado do jogo =========
const DIRS = { LEFT: [-1, 0], RIGHT: [1, 0], UP: [0, -1], DOWN: [0, 1] };
let snake,
direction,
nextDirection,
food,
score,
best,
alive,
paused,
timer,
speed;
function init() {
snake = [{ x: Math.floor(COLS / 2) - 1, y: Math.floor(ROWS / 2) }];
direction = "RIGHT";
nextDirection = "RIGHT";
score = 0;
alive = true;
paused = false;
speed = 110;
spawnFood();
uiScore.textContent = score;
best = Number(localStorage.getItem("snake_best") || 0);
uiBest.textContent = best;
overlay.classList.remove("show");
loopStart();
draw();
}
function restart() {
clearInterval(timer);
init();
}
function spawnFood() {
while (true) {
const x = Math.floor(Math.random() * COLS);
const y = Math.floor(Math.random() * ROWS);
if (!snake.some((seg) => seg.x === x && seg.y === y)) {
food = { x, y };
return;
}
}
}
// ========= Controles =========
document.addEventListener("keydown", (e) => {
const k = e.key;
if (k === " ") {
togglePause();
return;
}
if (k === "ArrowLeft" && direction !== "RIGHT") nextDirection = "LEFT";
if (k === "ArrowRight" && direction !== "LEFT") nextDirection = "RIGHT";
if (k === "ArrowUp" && direction !== "DOWN") nextDirection = "UP";
if (k === "ArrowDown" && direction !== "UP") nextDirection = "DOWN";
});
dpad.addEventListener(
"touchstart",
(e) => {
const btn = e.target.closest("button[data-dir]");
if (!btn) return;
setDir(btn.dataset.dir);
},
{ passive: true }
);
function setDir(dir) {
if (dir === "LEFT" && direction !== "RIGHT") nextDirection = "LEFT";
if (dir === "RIGHT" && direction !== "LEFT") nextDirection = "RIGHT";
if (dir === "UP" && direction !== "DOWN") nextDirection = "UP";
if (dir === "DOWN" && direction !== "UP") nextDirection = "DOWN";
}
// Swipe
let touchStartX = 0,
touchStartY = 0,
touching = false;
canvas.addEventListener(
"touchstart",
(e) => {
if (!e.touches[0]) return;
touching = true;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
},
{ passive: true }
);
canvas.addEventListener(
"touchend",
(e) => {
if (!touching) return;
touching = false;
const dx =
(e.changedTouches[0]?.clientX || touchStartX) - touchStartX;
const dy =
(e.changedTouches[0]?.clientY || touchStartY) - touchStartY;
if (Math.abs(dx) < 20 && Math.abs(dy) < 20) return;
if (Math.abs(dx) > Math.abs(dy)) {
setDir(dx > 0 ? "RIGHT" : "LEFT");
} else {
setDir(dy > 0 ? "DOWN" : "UP");
}
},
{ passive: true }
);
btnPause.addEventListener("click", togglePause);
btnRestart.addEventListener("click", restart);
ovPlay.addEventListener("click", restart);
ovClose.addEventListener("click", () => overlay.classList.remove("show"));
function togglePause() {
if (!alive) return;
paused = !paused;
btnPause.textContent = paused ? "Retomar" : "Pausar";
clearInterval(timer);
if (!paused) {
overlay.classList.remove("show");
loopStart();
} else
showOverlay(
"Pausado",
`Score: ${score}`,
"Toque em Retomar ou use Espaço"
);
}
// ========= Loop =========
function loopStart() {
overlay.classList.remove("show");
timer = setInterval(step, speed);
}
function step() {
if (!alive || paused) return;
direction = nextDirection;
const head = snake[0];
const [dx, dy] = DIRS[direction];
const nx = head.x + dx;
const ny = head.y + dy;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) return gameOver();
for (let i = 0; i < snake.length; i++) {
if (snake[i].x === nx && snake[i].y === ny) return gameOver();
}
snake.unshift({ x: nx, y: ny });
if (nx === food.x && ny === food.y) {
score++;
uiScore.textContent = score;
if (score % 5 === 0 && speed > 70) {
speed -= 5;
clearInterval(timer);
loopStart();
}
spawnFood();
} else {
snake.pop();
}
draw();
}
// ========= Render =========
function draw() {
const W = COLS * BOX,
H = ROWS * BOX;
ctx.clearRect(0, 0, W, H);
drawGrid(W, H, BOX);
drawFood(food.x * BOX, food.y * BOX, BOX);
for (let i = snake.length - 1; i >= 0; i--) {
const seg = snake[i];
drawSegment(seg.x * BOX, seg.y * BOX, BOX, i === 0);
}
// HUD
ctx.save();
ctx.fillStyle = "rgba(0,0,0,.35)";
roundRect(8, 8, 120, 28, 8);
ctx.fill();
ctx.fillStyle = "#e6fff6";
ctx.font = "14px system-ui, -apple-system, Segoe UI, Roboto, Arial";
ctx.fillText("Score: " + score, 16, 27);
ctx.restore();
}
function drawGrid(W, H, S) {
ctx.save();
ctx.fillStyle = "#0f1322";
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "rgba(255,255,255,.04)";
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += S) {
line(x, 0, x, H);
}
for (let y = 0; y <= H; y += S) {
line(0, y, W, y);
}
ctx.restore();
}
function drawFood(x, y, s) {
ctx.save();
const r = Math.floor(s * 0.38);
const cx = x + s / 2,
cy = y + s / 2;
const g = ctx.createRadialGradient(cx, cy, r * 0.3, cx, cy, r * 1.2);
g.addColorStop(0, "#fca5a5");
g.addColorStop(1, "#ef444466");
ctx.fillStyle = g;
circle(cx, cy, r);
ctx.fill();
ctx.fillStyle = "#ef4444";
circle(cx, cy, r * 0.75);
ctx.fill();
ctx.restore();
}
function drawSegment(x, y, s, isHead) {
ctx.save();
ctx.shadowColor = isHead ? "#34d39966" : "#10b98133";
ctx.shadowBlur = isHead ? 18 : 10;
ctx.fillStyle = isHead ? "#34d399" : "#10b981";
roundRect(x + 2, y + 2, s - 4, s - 4, Math.floor(s * 0.25));
ctx.fill();
ctx.globalAlpha = 0.18;
ctx.fillStyle = "#ffffff";
roundRect(
x + 4,
y + 4,
s - 8,
Math.floor((s - 8) / 3),
Math.floor(s * 0.18)
);
ctx.fill();
ctx.restore();
}
function gameOver() {
alive = false;
clearInterval(timer);
best = Math.max(Number(localStorage.getItem("snake_best") || 0), score);
localStorage.setItem("snake_best", String(best));
uiBest.textContent = best;
showOverlay(
"Game Over",
`Score: ${score}`,
"Toque em “Jogar de novo” para reiniciar"
);
btnPause.textContent = "Pausar";
}
function showOverlay(title, line1, tip) {
document.getElementById("ovTitle").textContent = title;
document.getElementById("ovScore").textContent = line1;
document.getElementById("ovTip").textContent = tip || "";
overlay.classList.add("show");
}
// helpers
function line(x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1 + 0.5, y1 + 0.5);
ctx.lineTo(x2 + 0.5, y2 + 0.5);
ctx.stroke();
}
function circle(cx, cy, r) {
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.closePath();
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
// start
init();
</script>
</body>
</html>