JIT
Grid - Untitled
Self-playing side-view pinball in a single HTML file. Pure CSS/JS, no assets. Features chrome ball physics, shadows, screen shake, and flashing bumpers. Auto-flippers play through 3 balls, updating the score until a 'GAME OVER' fade.
<html> <head> <meta charset="UTF-8"> <title>CSS/JS Pinball</title> <style> *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html, body { width: 100%; height: 100%; background: radial-gradient(circle at top, #111 0%, #050505 45%, #000 100%); color: #f5f5f5; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; overflow: hidden; } body { display: flex; align-items: center; justify-content: center; }

/* Main layout */ .pinball-wrapper { width: min(90vw, 1300px); height: min(90vh, 800px); display: grid; grid-template-columns: 3fr 1fr; gap: 1.5vh; padding: 1.8vh 1.8vw; background: radial-gradient(circle at center, #333 0%, #111 60%, #000 100%); border-radius: 2vh; box-shadow: 0 0 0.8vh rgba(255,255,255,0.15) inset, 0 1.2vh 4vh rgba(0,0,0,0.9); position: relative; }

/* Screen shake */ .shake { animation: screenShake 0.2s ease-out; } @keyframes screenShake { 0% { transform: translate(0,0); } 20% { transform: translate(-4px, 3px); } 40% { transform: translate(3px, -4px); } 60% { transform: translate(-2px, 2px); } 80% { transform: translate(2px, -2px); } 100% { transform: translate(0,0); } }

/* Playfield */ .playfield-container { position: relative; overflow: hidden; border-radius: 2vh; background: linear-gradient(145deg, #083130 0%, #041619 40%, #0b151a 100%); box-shadow: 0 0 0 4px #050809, 0 0 0 7px #14191c, 0 1.5vh 3vh rgba(0,0,0,0.8); }

/* Glass highlight */ .playfield-container::before { content: ""; position: absolute; inset: 0; background: linear-gradient(135deg, rgba(255,255,255,0.12), transparent 40%, transparent 60%, rgba(255,255,255,0.05)); mix-blend-mode: screen; pointer-events: none; }

/* Inner playfield for physics */ .playfield { position: absolute; inset: 3% 4% 8% 4%; border-radius: 2vh; background: radial-gradient(circle at 50% 0%, #0b3b3a 0%, #031214 40%, #020507 100%); overflow: hidden; }

/* Side rails */ .rail { position: absolute; top: 0; bottom: 0; width: 1.2vw; max-width: 12px; background: linear-gradient(180deg, #6f7d86, #252b30); box-shadow: 0 0 1vh rgba(0,0,0,0.7); } .rail.left { left: 0; } .rail.right { right: 0; }

/* Playfield markings */ .playfield-markings { position: absolute; inset: 4% 6% 10% 6%; border-radius: 1.5vh; border: 2px solid rgba(180,220,255,0.12); box-shadow: 0 0 15px rgba(120,210,255,0.2), 0 0 35px rgba(120,210,255,0.1) inset; pointer-events: none; }

/* Ball */ .ball { position: absolute; width: 26px; height: 26px; border-radius: 50%; background: radial-gradient(circle at 30% 25%, #ffffff 0%, #dcdcdc 18%, transparent 40%), radial-gradient(circle at 70% 70%, #999 0%, #444 35%, #111 80%); box-shadow: 0 0 10px rgba(255,255,255,0.3), 0 10px 18px rgba(0,0,0,0.9); transform: translate(-50%, -50%); } .ball::after { content: ""; position: absolute; inset: 18% 10% 30% 35%; border-radius: 50%; background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.7), transparent 60%); mix-blend-mode: screen; }

/* Ball shadow */ .ball-shadow { position: absolute; width: 30px; height: 12px; border-radius: 50%; background: radial-gradient(ellipse at center, rgba(0,0,0,0.7) 0%, transparent 70%); transform: translate(-50%, -50%); filter: blur(2px); pointer-events: none; }

/* Bumpers */ .bumper { position: absolute; width: 70px; height: 70px; border-radius: 50%; background: radial-gradient(circle at 30% 25%, #fff 0%, #ffeecc 18%, #ffb347 45%, #5a0808 100%); box-shadow: 0 0 25px rgba(255, 180, 90, 0.8), 0 0 50px rgba(255, 120, 40, 0.6); display: flex; align-items: center; justify-content: center; } .bumper-core { width: 42%; height: 42%; border-radius: 50%; background: radial-gradient(circle at 30% 20%, #fff 0%, #ffe5c7 25%, #ff7810 85%); box-shadow: 0 0 18px rgba(255,200,120,0.9), 0 0 40px rgba(255,150,70,0.7); } .bumper-flash { animation: bumperFlash 0.25s ease-out; } @keyframes bumperFlash { 0% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.12); filter: brightness(1.8); } 100% { transform: scale(1); filter: brightness(1); } }

/* Flippers */ .flipper { position: absolute; width: 130px; height: 26px; border-radius: 20px; background: linear-gradient(180deg, #cbd3da, #8f9aa5); box-shadow: 0 0 10px rgba(255,255,255,0.15), 0 3px 10px rgba(0,0,0,0.8); transform-origin: 80% 50%; } .flipper.left { left: 38%; bottom: 7%; } .flipper.right { right: 38%; bottom: 7%; transform-origin: 20% 50%; } .flipper-inner { position: absolute; inset: 10% 8%; border-radius: 16px; background: linear-gradient(180deg, #f7f7f7, #d0d6dd); } .flipper.left.auto-up { animation: flipperLeftUp 0.18s ease-out forwards; } .flipper.left.auto-down { animation: flipperLeftDown 0.18s ease-in forwards; } .flipper.right.auto-up { animation: flipperRightUp 0.18s ease-out forwards; } .flipper.right.auto-down { animation: flipperRightDown 0.18s ease-in forwards; } @keyframes flipperLeftUp { from { transform: rotate(0deg); } to { transform: rotate(-26deg); } } @keyframes flipperLeftDown { from { transform: rotate(-26deg); } to { transform: rotate(0deg); } } @keyframes flipperRightUp { from { transform: rotate(0deg); } to { transform: rotate(26deg); } } @keyframes flipperRightDown { from { transform: rotate(26deg); } to { transform: rotate(0deg); } }

/* Drain / Outlane */ .drain { position: absolute; left: 18%; right: 18%; bottom: 2%; height: 18px; border-radius: 999px; background: radial-gradient(circle at 50% 150%, #000 0%, #111 55%, #555 100%); box-shadow: 0 -6px 12px rgba(0,0,0,0.9) inset; }

/* HUD / Side panel */ .hud { position: relative; border-radius: 2vh; background: radial-gradient(circle at 0% 0%, #1e252a 0%, #06090b 60%, #020203 100%); box-shadow: 0 0 0 4px #040708, 0 0 0 6px #151b1f, 0 1.5vh 3vh rgba(0,0,0,0.85); padding: 2.5vh 1.8vw; display: flex; flex-direction: column; justify-content: space-between; }

/* Game title */ .game-title { text-align: center; font-size: 2.1rem; letter-spacing: 0.25em; text-transform: uppercase; color: #fdfdfd; margin-bottom: 1.5vh; text-shadow: 0 0 15px rgba(0,255,255,0.6), 0 0 40px rgba(0,200,255,0.7); } .game-subtitle { text-align: center; font-size: 0.75rem; letter-spacing: 0.25em; text-transform: uppercase; color: rgba(220,240,255,0.6); margin-bottom: 3vh; }

/* Score display */ .score-box { border-radius: 1.2vh; padding: 1.6vh 1.2vw; background: linear-gradient(145deg, #070b0e, #111820); box-shadow: 0 0 0 1px rgba(120,200,255,0.22), 0 0 18px rgba(40,170,255,0.45), 0 10px 20px rgba(0,0,0,0.75); } .score-label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.15em; color: rgba(160,220,255,0.7); margin-bottom: 0.5vh; } .score-value { font-family: "SF Mono", "Consolas", monospace; font-size: 2.4rem; letter-spacing: 0.12em; color: #fefefe; text-shadow: 0 0 12px rgba(140,235,255,0.9), 0 0 30px rgba(60,180,255,0.8); }

/* Balls indicator */ .balls-box { margin-top: 2vh; border-radius: 1vh; padding: 1.2vh 1vw; background: radial-gradient(circle at 0% 0%, #19212a 0%, #05070b 65%); box-shadow: 0 0 0 1px rgba(150, 200, 255, 0.2), 0 10px 16px rgba(0,0,0,0.75); } .balls-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.16em; color: rgba(180,215,240,0.75); margin-bottom: 0.8vh; } .ball-icons { display: flex; gap: 0.6vw; } .ball-icon { width: 20px; height: 20px; border-radius: 50%; background: radial-gradient(circle at 30% 25%, #fff 0%, #ddd 20%, #777 80%); box-shadow: 0 0 8px rgba(255,255,255,0.4), 0 0 18px rgba(120,180,255,0.55); opacity: 0.15; transition: opacity 0.25s ease; } .ball-icon.active { opacity: 1; }

/* Status / hints */ .status-box { margin-top: 3vh; padding-top: 1.2vh; border-top: 1px solid rgba(120,150,180,0.4); font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(200,230,255,0.7); } .status-line { margin-top: 0.6vh; color: rgba(190,220,250,0.6); } .hint { font-size: 0.66rem; margin-top: 1.5vh; color: rgba(140,170,200,0.6); }

/* Game over overlay */ .game-over-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; opacity: 0; transition: opacity 1.5s ease; background: radial-gradient(circle at center, rgba(0,0,0,0) 0%, rgba(0,0,0,0.9) 65%); } .game-over-overlay.visible { opacity: 1; } .game-over-text { font-size: clamp(3rem, 5vw, 4.5rem); letter-spacing: 0.4em; text-transform: uppercase; color: #ff4060; text-shadow: 0 0 18px rgba(255,140,150,0.9), 0 0 40px rgba(255,70,120,0.9); margin-bottom: 1.5vh; } .game-over-sub { font-size: 1rem; letter-spacing: 0.25em; text-transform: uppercase; color: rgba(255, 220, 230, 0.78); }

/* Responsive tweaks */ @media (max-width: 960px) { .pinball-wrapper { grid-template-columns: 1fr; grid-template-rows: 3fr minmax(220px, 1.1fr); } .hud { margin-top: 2vh; } .game-title { font-size: 1.6rem; } .score-value { font-size: 2rem; } } @media (max-width: 640px) { .pinball-wrapper { width: 100vw; height: 100vh; border-radius: 0; padding: 1vh 2vw; } .game-title { font-size: 1.3rem; letter-spacing: 0.16em; } .game-subtitle { letter-spacing: 0.18em; } .score-value { font-size: 1.7rem; } }

</style> </head> <body> <div class="pinball-wrapper" id="pinballRoot"> <div class="playfield-container" id="playfieldContainer"> <div class="playfield" id="playfield"> <div class="rail left"></div> <div class="rail right"></div> <div class="playfield-markings"></div>
  <div class="bumper" id="bumper1">
    <div class="bumper-core"></div>
  </div>
  <div class="bumper" id="bumper2">
    <div class="bumper-core"></div>
  </div>
  <div class="bumper" id="bumper3">
    <div class="bumper-core"></div>
  </div>

  
  <div class="flipper left" id="flipperLeft">
    <div class="flipper-inner"></div>
  </div>
  <div class="flipper right" id="flipperRight">
    <div class="flipper-inner"></div>
  </div>

  
  <div class="drain"></div>

  
  <div class="ball-shadow" id="ballShadow"></div>
  <div class="ball" id="ball"></div>

  
  <div class="game-over-overlay" id="gameOverOverlay">
    <div class="game-over-text">GAME OVER</div>
    <div class="game-over-sub">thanks for playing</div>
  </div>
</div>
</div> <div class="hud"> <div> <div class="game-title">CHROME RUN</div> <div class="game-subtitle">AUTOPLAY PINBALL SIM</div>
  <div class="score-box">
    <div class="score-label">Score</div>
    <div class="score-value" id="scoreDisplay">000000</div>
  </div>

  <div class="balls-box">
    <div class="balls-label">Balls</div>
    <div class="ball-icons">
      <div class="ball-icon active" id="ballIcon1"></div>
      <div class="ball-icon active" id="ballIcon2"></div>
      <div class="ball-icon active" id="ballIcon3"></div>
    </div>
  </div>
</div>

<div class="status-box">
  <div id="statusMain">Autoplay: running</div>
  <div class="status-line" id="statusSub">Watch the chrome ball roll, bump, and flip.</div>
  <div class="hint">This table is fully self‑playing. Sit back and enjoy the light show.</div>
</div>
</div> </div> <script> (function() { const playfield = document.getElementById('playfield'); const container = document.getElementById('playfieldContainer'); const ballEl = document.getElementById('ball'); const ballShadowEl = document.getElementById('ballShadow'); const bumperEls = [ document.getElementById('bumper1'), document.getElementById('bumper2'), document.getElementById('bumper3') ]; const flipperLeftEl = document.getElementById('flipperLeft'); const flipperRightEl = document.getElementById('flipperRight'); const scoreDisplay = document.getElementById('scoreDisplay'); const statusMain = document.getElementById('statusMain'); const statusSub = document.getElementById('statusSub'); const ballIcons = [ document.getElementById('ballIcon1'), document.getElementById('ballIcon2'), document.getElementById('ballIcon3') ]; const gameOverOverlay = document.getElementById('gameOverOverlay'); const rootWrapper = document.getElementById('pinballRoot'); let playRect = playfield.getBoundingClientRect(); let containerRect = container.getBoundingClientRect(); function updateRects() { playRect = playfield.getBoundingClientRect(); containerRect = container.getBoundingClientRect(); } window.addEventListener('resize', updateRects); // Ball physics (simple) const ball = { x: playRect.width * 0.5, y: playRect.height * 0.8, vx: 110, vy: -260, radius: 13, z: 1, // "height" above surface, for shadow vz: 0 }; const physics = { gravity: 480, air: 0.0005, wallRestitution: 0.82, bumperRestitution: 1.2, flipperImpulse: 420, maxSpeed: 720 }; // Bumper model const bumpers = []; function layoutElements() { updateRects(); const w = playRect.width; const h = playRect.height; // approximate placements in relative coordinates const b1 = { x: w * 0.35, y: h * 0.25, r: 35 }; const b2 = { x: w * 0.65, y: h * 0.22, r: 35 }; const b3 = { x: w * 0.50, y: h * 0.36, r: 35 }; const defs = [b1, b2, b3]; bumpers.length = 0; bumperEls.forEach((el, i) => { const d = defs[i]; el.style.left = (d.x - d.r) + 'px'; el.style.top = (d.y - d.r) + 'px'; bumpers.push({ x: d.x, y: d.y, r: d.r, el }); }); // Flippers positioning (approx) const flY = h * 0.82; const flOffset = w * 0.16; const flLeftX = w * 0.5 - flOffset; const flRightX = w * 0.5 + flOffset; flipperLeftEl.style.left = (flLeftX - 80) + 'px'; flipperLeftEl.style.top = (flY - 13) + 'px'; flipperRightEl.style.left = (flRightX - 50) + 'px'; flipperRightEl.style.top = (flY - 13) + 'px'; } layoutElements(); window.addEventListener('resize', layoutElements); // Game state let score = 0; let ballsLeft = 3; let running = true; let gameOver = false; function formatScore(v) { const s = String(v); return s.padStart(6, '0'); } function addScore(points) { score += points; scoreDisplay.textContent = formatScore(score); } function setBallsDisplay() { for (let i = 0; i < 3; i++) { ballIcons[i].classList.toggle('active', i < ballsLeft); } } setBallsDisplay(); // Screen shake let shakeTimeout = null; function triggerShake() { if (shakeTimeout) return; rootWrapper.classList.add('shake'); shakeTimeout = setTimeout(() => { rootWrapper.classList.remove('shake'); shakeTimeout = null; }, 200); } // Bumper flash function flashBumper(el) { el.classList.remove('bumper-flash'); // Force reflow to restart animation void el.offsetWidth; el.classList.add('bumper-flash'); } // Flipper AI let flipperCycle = 0; let flipperState = 'down'; // 'up', 'down' let flipperTimer = 0; function flippersUp() { flipperState = 'up'; flipperTimer = 0; flipperLeftEl.classList.remove('auto-down'); flipperRightEl.classList.remove('auto-down'); flipperLeftEl.classList.add('auto-up'); flipperRightEl.classList.add('auto-up'); } function flippersDown() { flipperState = 'down'; flipperTimer = 0; flipperLeftEl.classList.remove('auto-up'); flipperRightEl.classList.remove('auto-up'); flipperLeftEl.classList.add('auto-down'); flipperRightEl.classList.add('auto-down'); } function aiFlippers(dt) { flipperTimer += dt; const h = playRect.height; const nearFlippers = ball.y > h * 0.63; const centerDist = Math.abs(ball.x - playRect.width * 0.5); // base rhythm flipperCycle += dt; const cycleTime = 1.25; const pulse = (flipperCycle % cycleTime) / cycleTime; const randomOffset = Math.sin(flipperCycle * 2.1 + 0.7) * 0.15; const autoUp = pulse > 0.5 + randomOffset; let shouldFlipUp = false; // if ball near drain and moving downward, flip if (nearFlippers && ball.vy > 40) { shouldFlipUp = true; } // sometimes flip when ball is close to center, for variety if (centerDist < playRect.width * 0.08 && ball.vy > 0 && ball.y > h * 0.55) { shouldFlipUp = true; } // background automatic pulsing if (autoUp && ball.y > h * 0.45) { shouldFlipUp = true; } if (shouldFlipUp && flipperState === 'down' && flipperTimer > 0.18) { flippersUp(); } else if (!shouldFlipUp && flipperState === 'up' && flipperTimer > 0.20) { flippersDown(); } } // Flipper collision (simple zones) function handleFlipperHit() { const h = playRect.height; const w = playRect.width; const yZone = h * 0.77; const yMax = h * 0.9; if (ball.y < yZone || ball.y > yMax) return; const cx = w * 0.5; const leftRegion = cx - w * 0.22; const rightRegion = cx + w * 0.22; if (flipperState === 'up' && ball.vy > -40) { // left side if (ball.x > leftRegion && ball.x < cx) { ball.vy = -physics.flipperImpulse - Math.random() * 120; ball.vx -= 80 + Math.random() * 60; triggerShake(); addScore(150); } // right side if (ball.x < rightRegion && ball.x >= cx) { ball.vy = -physics.flipperImpulse - Math.random() * 120; ball.vx += 80 + Math.random() * 60; triggerShake(); addScore(150); } } } // Initialize ball for a new ball function launchBall() { const w = playRect.width; const h = playRect.height; ball.x = w * 0.5 + (Math.random() * 0.12 - 0.06) * w; ball.y = h * 0.78; ball.vx = (Math.random() * 120 - 60) + (Math.random() < 0.5 ? -80 : 80); ball.vy = - (260 + Math.random() * 80); ball.z = 6; ball.vz = 80; } // Drain handling function handleDrain() { ballsLeft -= 1; setBallsDisplay(); if (ballsLeft <= 0) { running = false; gameOver = true; statusMain.textContent = 'Autoplay complete'; statusSub.textContent = 'Final score locked. GAME OVER.'; setTimeout(() => { gameOverOverlay.classList.add('visible'); }, 600); return; } statusMain.textContent = 'New ball launching'; statusSub.textContent = 'Ball ' + (4 - ballsLeft) + ' of 3 in play.'; setTimeout(() => { launchBall(); }, 600); } // Ball update let lastTime = null; function step(timestamp) { if (!lastTime) lastTime = timestamp; const dt = Math.min(0.035, (timestamp - lastTime) / 1000); lastTime = timestamp; if (running && !gameOver) { // Gravity ball.vy += physics.gravity * dt; ball.vz -= 520 * dt; // quick arc toward table for shadow feel // Air resistance const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); if (speed > 0) { const drag = 1 - physics.air * dt * speed; ball.vx *= drag; ball.vy *= drag; } // Clamp speed const max = physics.maxSpeed; const s2 = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); if (s2 > max) { const k = max / s2; ball.vx *= k; ball.vy *= k; } ball.x += ball.vx * dt; ball.y += ball.vy * dt; ball.z += ball.vz * dt; if (ball.z < 1) { ball.z = 1; ball.vz *= -0.28; } // Walls const r = ball.radius; const left = r + 8; const right = playRect.width - r - 8; const top = r + 6; const bottom = playRect.height - r - 4; if (ball.x < left) { ball.x = left; ball.vx *= -physics.wallRestitution; triggerShake(); } if (ball.x > right) { ball.x = right; ball.vx *= -physics.wallRestitution; triggerShake(); } if (ball.y < top) { ball.y = top; ball.vy *= -physics.wallRestitution; triggerShake(); } // Drain if (ball.y > bottom + 40) { handleDrain(); } // Bumpers collision for (const b of bumpers) { const dx = ball.x - b.x; const dy = ball.y - b.y; const distSq = dx * dx + dy * dy; const minDist = ball.radius + b.r * 0.6; if (distSq < minDist * minDist) { const dist = Math.sqrt(distSq) || 0.0001; const nx = dx / dist; const ny = dy / dist; const overlap = minDist - dist; ball.x += nx * overlap; ball.y += ny * overlap; const dot = ball.vx * nx + ball.vy * ny; if (dot < 0) { const bounce = physics.bumperRestitution; ball.vx -= (1 + bounce) * dot * nx; ball.vy -= (1 + bounce) * dot * ny; } flashBumper(b.el); triggerShake(); addScore(250 + Math.floor(Math.random() * 75)); } } // AI flippers aiFlippers(dt); handleFlipperHit(); } // Update visuals const bx = ball.x; const by = ball.y - ball.z * 0.4; // slight elevation ballEl.style.left = bx + 'px'; ballEl.style.top = by + 'px'; const sx = ball.x + ball.z * 0.35; const sy = ball.y + ball.z * 0.45; ballShadowEl.style.left = sx + 'px'; ballShadowEl.style.top = sy + 'px'; const scale = 1 + ball.z * 0.03; ballShadowEl.style.transform = 'translate(-50%, -50%) scale(' + scale.toFixed(2) + ')'; ballShadowEl.style.opacity = Math.max(0.25, 1.1 - ball.z * 0.1); requestAnimationFrame(step); } // Start statusMain.textContent = 'Autoplay: running'; statusSub.textContent = 'Ball 1 of 3 launched.'; launchBall(); requestAnimationFrame(step); })(); </script> </body> </html>
Updated files
v1