Skip to content

Instantly share code, notes, and snippets.

@Leibinger015
Created November 23, 2025 18:00
Show Gist options
  • Select an option

  • Save Leibinger015/0558f39fe73be6d59d7c1da73729bedf to your computer and use it in GitHub Desktop.

Select an option

Save Leibinger015/0558f39fe73be6d59d7c1da73729bedf to your computer and use it in GitHub Desktop.
WebApp: Für Smartphones und Tablet
<!-- Script by anb030.de / v1.8 (20251123) -->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>SuperBall Touch</title>
<link rel="apple-touch-icon" href="img/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover">
<style>
@font-face {
font-family: 'DigiLCD';
src: url('data/digi_lcd.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
* {
box-sizing: border-box;
-webkit-user-select: none;
user-select: none;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: #000;
color: #eee;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
touch-action: none;
}
.app {
height: 100vh;
display: flex;
flex-direction: column;
max-width: 500px;
margin: 0 auto;
background: #000;
}
.game-area {
height: calc(100vh - 260px);
flex: none;
position: relative;
}
canvas {
width: 100%;
height: 100%;
display: block;
background: #000;
}
.controls {
min-height: 260px;
flex: none;
background: #111;
display: grid;
grid-template-columns: 1.3fr 0.8fr 0.8fr 1.3fr;
grid-template-rows: auto 1fr;
gap: 10px;
align-items: center;
padding: 0 10px calc(26px + env(safe-area-inset-bottom)) 10px;
}
.btn {
border: none;
border-radius: 12px;
background: #222;
color: #eee;
font-weight: 600;
font-size: clamp(14px, 3.2vw, 18px);
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 8px rgba(0,0,0,0.6);
}
.btn:active {
transform: scale(0.97);
background: #333;
}
.btn-round {
border-radius: 50%;
aspect-ratio: 1 / 1;
width: 90%;
justify-self: center;
font-size: clamp(22px, 5vw, 30px);
padding: 0;
}
.btn-left {
background: #263238;
}
.btn-right {
background: #263238;
}
.btn-start,
.btn-pause {
font-size: clamp(12px, 2.8vw, 16px);
padding: 6px;
margin-top: 15px;
border-radius: 10px;
}
.btn-start {
background: #1b5e20;
}
.btn-pause {
background: #b71c1c;
}
.btn-label {
pointer-events: none;
}
.game-logo-container {
grid-column: 1 / -1;
grid-row: 2;
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 10px;
padding-bottom: 10px;
min-height: 60px;
}
.game-logo {
max-width: 80%;
max-height: 80px;
height: auto;
object-fit: contain;
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.4));
}
.orientation-warning {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
color: #fff;
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
z-index: 99999;
font-size: 1.5rem;
padding: 20px;
}
.landscape-image {
max-height: 50vh;
width: auto;
max-width: 50vw;
height: auto;
display: block;
margin-bottom: 20px;
}
@media (orientation: landscape) {
.orientation-warning {
display: flex;
}
.app {
display: none !important;
}
}
</style>
</head>
<body>
<div class="orientation-warning">
<img src="img/no_landscape.png" alt="Bitte in den Hochformat-Modus drehen" class="landscape-image">
<p>Dieses Spiel kann nur im <br>Porträtformat gespielt werden.</p>
</div>
<div class="app">
<div class="game-area">
<canvas id="game" width="360" height="540"></canvas>
</div>
<div class="controls">
<button id="btn-left" class="btn btn-round btn-left">
<span class="btn-label">L</span>
</button>
<button id="btn-start" class="btn btn-start">
<span class="btn-label">Start</span>
</button>
<button id="btn-pause" class="btn btn-pause">
<span class="btn-label" id="pause-label">Pause</span>
</button>
<button id="btn-right" class="btn btn-round btn-right">
<span class="btn-label">R</span>
</button>
<div class="game-logo-container">
</div>
</div>
</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const WIDTH = canvas.width;
const HEIGHT = canvas.height;
const ballImage = new Image();
ballImage.src = "img/s_ball.png";
const obstacleImage = new Image();
obstacleImage.src = "img/x_ball.png";
const gameLogoImage = new Image();
gameLogoImage.src = "img/game_logo.png";
const PLAYFIELD_WIDTH = WIDTH * 0.55;
const PLAYFIELD_X = (WIDTH - PLAYFIELD_WIDTH) / 2;
const PLAYFIELD_Y = 40;
const PLAYFIELD_HEIGHT = HEIGHT - 100;
const stars = [];
for (let i = 0; i < 60; i++) {
stars.push({
x: Math.random() * WIDTH,
y: Math.random() * HEIGHT,
r: Math.random() * 1.2 + 0.3
});
}
const STATE_TITLE = 0;
const STATE_RUNNING = 1;
const STATE_GAMEOVER= 2;
const STATE_PAUSED = 3;
let gameState = STATE_TITLE;
let level = 1;
const LEVEL_DURATION = 20;
let elapsedTime = 0;
let highscoreLevel = 0;
let highscoreTime = 0.0;
const ball = {
x: WIDTH / 2,
y: PLAYFIELD_Y + PLAYFIELD_HEIGHT - 40,
radius: 16,
speedX: 230
};
let obstacles = [];
const BASE_OBSTACLE_SPEED = 120;
let obstacleSpeed = BASE_OBSTACLE_SPEED;
let spawnInterval = 0.9;
let spawnTimer = 0;
const input = {
left: false,
right: false
};
let lastTimestamp = performance.now();
let infoLinkArea = null;
function loadHighscores() {
const storedLevel = localStorage.getItem('superball_highscore_level');
const storedTime = localStorage.getItem('superball_highscore_time');
highscoreLevel = storedLevel ? parseInt(storedLevel, 10) : 0;
highscoreTime = storedTime ? parseFloat(storedTime) : 0.0;
}
function saveHighscore(currentLevel, currentTime) {
let updateNeeded = false;
if (currentLevel > highscoreLevel) {
highscoreLevel = currentLevel;
highscoreTime = currentTime;
updateNeeded = true;
}
else if (currentLevel > 0 && currentLevel === highscoreLevel && currentTime > highscoreTime) {
highscoreTime = currentTime;
updateNeeded = true;
}
if (updateNeeded) {
localStorage.setItem('superball_highscore_level', highscoreLevel);
localStorage.setItem('superball_highscore_time', highscoreTime.toFixed(1));
}
}
function resetDifficulty() {
level = 1;
elapsedTime = 0;
obstacleSpeed = BASE_OBSTACLE_SPEED;
spawnInterval = 0.9;
spawnTimer = 0;
}
function applyLevelSettings() {
obstacleSpeed = BASE_OBSTACLE_SPEED + (level - 1) * 40;
spawnInterval = Math.max(0.4, 0.9 - (level - 1) * 0.08);
}
function resetGame() {
ball.x = WIDTH / 2;
ball.y = PLAYFIELD_Y + PLAYFIELD_HEIGHT - 40;
obstacles = [];
resetDifficulty();
gameState = STATE_RUNNING;
}
function spawnObstacle() {
const minR = 10;
const maxR = 18;
const r = minR + Math.random() * (maxR - minR);
const x = PLAYFIELD_X + r + Math.random() * (PLAYFIELD_WIDTH - 2 * r);
obstacles.push({
x: x,
y: PLAYFIELD_Y - r,
r: r,
speed: obstacleSpeed + Math.random() * 25
});
}
function update(dt) {
if (gameState !== STATE_RUNNING) return;
elapsedTime += dt;
const newLevel = Math.floor(elapsedTime / LEVEL_DURATION) + 1;
if (newLevel > level) {
level = newLevel;
applyLevelSettings();
}
let dir = 0;
if (input.left) dir -= 1;
if (input.right) dir += 1;
ball.x += dir * ball.speedX * dt;
const leftLimit = PLAYFIELD_X + ball.radius + 4;
const rightLimit = PLAYFIELD_X + PLAYFIELD_WIDTH - ball.radius - 4;
if (ball.x < leftLimit) ball.x = leftLimit;
if (ball.x > rightLimit) ball.x = rightLimit;
spawnTimer += dt;
if (spawnTimer >= spawnInterval) {
spawnTimer -= spawnInterval;
spawnObstacle();
}
for (let i = obstacles.length - 1; i >= 0; i--) {
const o = obstacles[i];
o.y += o.speed * dt;
if (o.y - o.r > PLAYFIELD_Y + PLAYFIELD_HEIGHT) {
obstacles.splice(i, 1);
}
}
for (const o of obstacles) {
const dx = ball.x - o.x;
const dy = ball.y - o.y;
const rSum = ball.radius + o.r;
if (dx * dx + dy * dy <= rSum * rSum) {
gameOver();
break;
}
}
}
function gameOver() {
saveHighscore(level, elapsedTime);
gameState = STATE_GAMEOVER;
}
function drawBackground() {
const grd = ctx.createLinearGradient(0, 0, 0, HEIGHT);
grd.addColorStop(0, "#001b3d");
grd.addColorStop(1, "#000014");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
ctx.fillStyle = "#ffffff";
for (const s of stars) {
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = "#123c8c";
ctx.fillRect(PLAYFIELD_X - 6, PLAYFIELD_Y - 6, PLAYFIELD_WIDTH + 12, PLAYFIELD_HEIGHT + 12);
ctx.fillStyle = "#001a33";
ctx.fillRect(PLAYFIELD_X, PLAYFIELD_Y, PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT);
ctx.strokeStyle = "#4f7fbf";
ctx.lineWidth = 1;
const gridSpacing = 24;
for (let x = PLAYFIELD_X; x <= PLAYFIELD_X + PLAYFIELD_WIDTH; x += gridSpacing) {
ctx.beginPath();
ctx.moveTo(x + 0.5, PLAYFIELD_Y);
ctx.lineTo(x + 0.5, PLAYFIELD_Y + PLAYFIELD_HEIGHT);
ctx.stroke();
}
for (let y = PLAYFIELD_Y; y <= PLAYFIELD_Y + PLAYFIELD_HEIGHT; y += gridSpacing) {
ctx.beginPath();
ctx.moveTo(PLAYFIELD_X, y + 0.5);
ctx.lineTo(PLAYFIELD_X + PLAYFIELD_WIDTH, y + 0.5);
ctx.stroke();
}
}
function drawBall() {
const diameter = ball.radius * 2;
const drawX = ball.x - ball.radius;
const drawY = ball.y - ball.radius;
if (ballImage.complete && ballImage.naturalWidth > 0) {
ctx.drawImage(ballImage, drawX, drawY, diameter, diameter);
} else {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = "#ffcc00";
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = "#ff9900";
ctx.stroke();
}
}
function drawObstacles() {
for (const o of obstacles) {
const diameter = o.r * 2;
const drawX = o.x - o.r;
const drawY = o.y - o.r;
if (obstacleImage.complete && obstacleImage.naturalWidth > 0) {
ctx.drawImage(obstacleImage, drawX, drawY, diameter, diameter);
} else {
ctx.beginPath();
ctx.arc(o.x, o.y, o.r, 0, Math.PI * 2);
ctx.fillStyle = "#5cff4a";
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = "#2cbf1e";
ctx.stroke();
}
}
}
function drawHUD() {
const timeSec = elapsedTime.toFixed(1);
ctx.textAlign = "right";
const fontPrimary = 'DigiLCD, system-ui, sans-serif';
const fontSecondary = 'system-ui, sans-serif';
ctx.fillStyle = "#ffffff";
ctx.font = "bold 22px " + fontPrimary;
ctx.fillText(timeSec + " s", WIDTH - 8, 28);
ctx.fillStyle = "#a6ff4d";
ctx.font = "14px " + fontSecondary;
ctx.fillText("Level", WIDTH - 8, 56);
ctx.font = "bold 32px " + fontPrimary;
ctx.fillText(level.toString(), WIDTH - 8, 88);
ctx.fillStyle = "#ffffff";
ctx.font = "14px " + fontSecondary;
ctx.fillText("Rekord", WIDTH - 8, 116);
ctx.fillStyle = "#ffcc00";
ctx.font = "bold 24px " + fontPrimary;
ctx.fillText(highscoreLevel.toString(), WIDTH - 8, 144);
if (highscoreLevel > 0) {
const timeText = "(" + highscoreTime.toFixed(1) + " s)";
ctx.fillStyle = "#aaaaaa";
ctx.font = "12px " + fontSecondary;
ctx.fillText(timeText, WIDTH - 8, 162);
}
}
function drawTitleScreen() {
drawBackground();
drawBall();
drawHUD();
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
if (gameLogoImage.complete && gameLogoImage.naturalWidth > 0) {
const logoWidth = WIDTH * 0.8;
const logoHeight = (gameLogoImage.naturalHeight / gameLogoImage.naturalWidth) * logoWidth;
const logoX = (WIDTH - logoWidth) / 2;
const logoY = HEIGHT / 2 - logoHeight / 2 - 60;
ctx.drawImage(gameLogoImage, logoX, logoY, logoWidth, logoHeight);
} else {
ctx.fillStyle = "#ffcc00";
ctx.font = "bold 26px DigiLCD, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillText("SUPERBALL TOUCH", WIDTH / 2, HEIGHT / 2 - 40);
}
ctx.fillStyle = "#ffffff";
ctx.font = "16px system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillText("Weiche den Kugeln aus!", WIDTH / 2, HEIGHT / 2 + 30);
ctx.font = "20px system-ui, sans-serif";
ctx.fillText("Tippe unten auf START, um zu beginnen.", WIDTH / 2, HEIGHT / 2 + 60);
const infoText = "ⓘ Spielregeln & FAQ";
const infoFont = "16px system-ui, sans-serif";
ctx.font = infoFont;
ctx.fillStyle = "#ffffff";
const infoY = HEIGHT / 2 + 86;
ctx.fillText(infoText, WIDTH / 2, infoY);
const metrics = ctx.measureText(infoText);
const textWidth = metrics.width;
const boxPaddingX = 10;
const boxPaddingY = 6;
infoLinkArea = {
x: WIDTH / 2 - textWidth / 2 - boxPaddingX,
y: infoY - 12 - boxPaddingY,
w: textWidth + boxPaddingX * 2,
h: 24 + boxPaddingY * 2
};
ctx.strokeStyle = "rgba(187,187,187,0.5)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(WIDTH / 2 - textWidth / 2, infoY + 4);
ctx.lineTo(WIDTH / 2 + textWidth / 2, infoY + 4);
ctx.stroke();
}
function drawGameOverScreen() {
ctx.fillStyle = "rgba(0,0,0,0.65)";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
ctx.fillStyle = "#ff4444";
ctx.font = "bold 26px DigiLCD, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillText("GAME OVER", WIDTH / 2, HEIGHT / 2 - 40);
ctx.fillStyle = "#ffffff";
ctx.font = "16px system-ui, sans-serif";
ctx.fillText("Zeit: " + elapsedTime.toFixed(1) + " s", WIDTH / 2, HEIGHT / 2);
ctx.fillText("Level: " + level, WIDTH / 2, HEIGHT / 2 + 26);
ctx.fillStyle = "#ffcc00";
ctx.font = "16px system-ui, sans-serif";
ctx.fillText("Rekord: " + highscoreLevel + " (" + highscoreTime.toFixed(1) + " s)", WIDTH / 2, HEIGHT / 2 + 52);
ctx.fillStyle = "#ffffff";
ctx.font = "14px system-ui, sans-serif";
ctx.fillText("Tippe auf START für einen neuen Versuch.", WIDTH / 2, HEIGHT / 2 + 80);
}
function drawPauseOverlay() {
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
ctx.fillStyle = "#ffeb3b";
ctx.font = "bold 24px DigiLCD, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillText("PAUSE", WIDTH / 2, HEIGHT / 2);
}
function render() {
drawBackground();
drawObstacles();
drawBall();
drawHUD();
if (gameState === STATE_TITLE) {
drawTitleScreen();
} else if (gameState === STATE_GAMEOVER) {
drawGameOverScreen();
} else if (gameState === STATE_PAUSED) {
drawPauseOverlay();
}
}
function loop(timestamp) {
let dt = (timestamp - lastTimestamp) / 1000;
lastTimestamp = timestamp;
if (dt > 0.05) dt = 0.05;
if (gameState === STATE_RUNNING) {
update(dt);
}
render();
requestAnimationFrame(loop);
}
const btnLeft = document.getElementById("btn-left");
const btnRight = document.getElementById("btn-right");
const btnStart = document.getElementById("btn-start");
const btnPause = document.getElementById("btn-pause");
const pauseLabel = document.getElementById("pause-label");
function bindHoldButton(elem, onDown, onUp) {
elem.addEventListener("pointerdown", (e) => {
e.preventDefault();
onDown();
});
["pointerup", "pointercancel", "pointerleave"].forEach(ev => {
elem.addEventListener(ev, (e) => {
e.preventDefault();
if (onUp) onUp();
});
});
}
bindHoldButton(btnLeft,
() => { input.left = true; input.right = false; },
() => { input.left = false; }
);
bindHoldButton(btnRight,
() => { input.right = true; input.left = false; },
() => { input.right = false; }
);
btnStart.addEventListener("pointerdown", (e) => {
e.preventDefault();
resetGame();
pauseLabel.textContent = "Pause";
});
btnPause.addEventListener("pointerdown", (e) => {
e.preventDefault();
if (gameState === STATE_RUNNING) {
gameState = STATE_PAUSED;
pauseLabel.textContent = "Weiter";
} else if (gameState === STATE_PAUSED) {
gameState = STATE_RUNNING;
pauseLabel.textContent = "Pause";
}
});
canvas.addEventListener("pointerdown", (e) => {
if (gameState !== STATE_TITLE || !infoLinkArea) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
if (
x >= infoLinkArea.x &&
x <= infoLinkArea.x + infoLinkArea.w &&
y >= infoLinkArea.y &&
y <= infoLinkArea.y + infoLinkArea.h
) {
e.preventDefault();
window.location.href = "data/info.html";
}
});
window.addEventListener("keydown", (e) => {
const key = e.key;
if (key === "ArrowLeft") {
e.preventDefault();
input.left = true;
input.right = false;
} else if (key === "ArrowRight") {
e.preventDefault();
input.right = true;
input.left = false;
} else if (key === " " || key === "Spacebar") {
e.preventDefault();
resetGame();
pauseLabel.textContent = "Pause";
} else if (key === "p" || key === "P") {
e.preventDefault();
if (gameState === STATE_RUNNING) {
gameState = STATE_PAUSED;
pauseLabel.textContent = "Weiter";
} else if (gameState === STATE_PAUSED) {
gameState = STATE_RUNNING;
pauseLabel.textContent = "Pause";
}
}
});
window.addEventListener("keyup", (e) => {
const key = e.key;
if (key === "ArrowLeft") {
e.preventDefault();
input.left = false;
} else if (key === "ArrowRight") {
e.preventDefault();
input.right = false;
}
});
loadHighscores();
lastTimestamp = performance.now();
requestAnimationFrame(loop);
</script>
</body>
</html>
@Leibinger015
Copy link
Author

Leibinger015 commented Nov 23, 2025

Artikel zum Spiel: https://anb030.de/a/webapp-superball-touch/

index.html

  • data
  • img

App-Icon:

apple-touch-icon

Logo:

game_logo

Ball‘s:

x_ball s_ball

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment