v1.5.3 β mobile vpad bottom anchor + top bar tightening
Changelog
All notable changes to tetris will be documented here. Format inspired by Keep a Changelog, versioning follows SemVer.
[1.5.3] β 2026-06-14
Fixed
- Mobile: vpad now actually anchors to the bottom of the screen. On iOS Safari with
html, body { position: fixed; height: 100% }, the body locks to the layout viewport (the larger one that ignores Safari's bottom chrome) soposition: fixed; bottom: 0was painting ~200 px above the visible bottom edge of the screen. Switched the lock toheight: 100dvhso the body tracks the visual viewport, andbottom: env(safe-area-inset-bottom) + 2pxnow lands the vpad just above the home-indicator as intended.
Changed
- Mobile top bar tucked tighter to the status bar. Dropped the extra
+ 6pxpadding above the safe-area inset β the chip now starts atmax(4px, env(safe-area-inset-top)), recovering ~14 px of vertical room for the board.
[1.5.2] β 2026-06-14
Changed
- Vpad sits at the actual viewport bottom and reads as a near-invisible hint. Default opacity dropped from 0.18 to 0.08 (still a hint, but no longer competing with the board), grid centered with
left: 50%; transform: translateX(-50%), width capped atmin(360px, 100vw - 16px)so it doesn't span the whole row, and button heights tightened toclamp(38px, 6dvh, 50px). The pad now occupies a thumb-reach footprint instead of a full-width strip.
Fixed
- Page no longer rubber-band-scrolls when you swipe down on the board. Three changes work together: (1)
touch-action: noneon.board-frametells iOS we handle every gesture, (2) thetouchstart/touchmovelisteners switched from{ passive: true }to{ passive: false }soe.preventDefault()actually takes effect on each move event, and (3)html, bodygetposition: fixed; overflow: hidden; touch-action: noneon mobile so the document is removed from the scroll container entirely. Pull-to-refresh and the rubber-band overscroll are both gone.
[1.5.1] β 2026-06-14
Fixed
- Mobile black-screen regression (critical). v1.5.0 added mirrored HOLD/NEXT mini-canvases to the floating mobile bar (36 Γ 36 CSS px). At those tiny sizes the renderer's inset math for the glass "sheen" highlight produced a negative width, which made
roundRect()'s polyfill callarcTo()with a negative radius. Every Tetris frame then threwFailed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-2.66667) is negativeβ the main board never painted, leaving a fully black playfield on every iPhone. Two defenses:roundRect()now short-circuits when width or height is non-positive and clamps the radius to>= 0;drawMini()clamps the per-cell size to a>= 2 pxfloor so the mobile chips degrade to flat tiles instead of crashing the frame. - Desktop regression. v1.5.0's
.mobile-barHTML element fell back to its default block layout on desktop because every styling rule lived inside the@media (max-width: 760px)query β so HOLD/score/lvl/lines/time/NEXT and the two mini-canvases leaked into the top-left of the desktop layout. Added a default.mobile-bar { display: none }rule outside the media query; the mobile query re-enables it withdisplay: flex. Desktop is back to its pre-v1.5.0 chrome.
[1.5.0] β 2026-06-14
Added
- Mobile: complete rebuild around gestures. The board now fills the entire viewport edge-to-edge β no chrome, no padding, no rounded frame. Five classic mobile Tetris gestures: tap = rotate, drag left/right = move (one cell per 22 px), drag down = soft drop, swipe up = hard drop, long-press = hold. The mapping flips v1.4.x's "swipe down = hard drop" to match the convention every modern Tetris mobile client uses.
- Floating mobile bar. A single horizontal chip at the top of the viewport (HOLD Β· score/level/lines/time Β· NEXT Β· lightΒ·dark / settings) replaces the side HUDs. Backdrop-blurred over the board so the playfield underneath is still visible β the chip floats, doesn't displace.
- Discovery hint vpad. The virtual D-pad is now always present at 18 % opacity so new players notice it exists. Any ambiguous touch (a release that wasn't a tap, swipe, drag, or long-press) reveals the pad at full opacity for 3 seconds. Tap a button to act β the timer resets so you can keep using it. Tap the board to dismiss early.
- Renderer mirroring.
Renderernow accepts optionalholdCanvasMobile/nextCanvasMobileand mirrors the HOLD and NEXT previews into them every frame. Desktop HUD canvases are unchanged.
Changed
availableBoardHeight()rewritten for mobile: budget is100dvh β floating-mobile-bar.bottom β safe-area-bottom β breathing room. The mobile-bar floatsposition:fixedso the old sibling-subtraction loop missed it.availableBoardWidth()on mobile is now fullwindow.innerWidthβ board-frame has zero padding and zero border, so the canvas can use every pixel.bindTouch()adds long-press detection (500 ms), swipe-up = hard drop, ambiguous-touch β vpad reveal, and atouchcancelcleanup path. Long-press fires once and consumes the trailing touchend so it doesn't also rotate.- Vpad pointer-events default to
none(so touches pass through to the board) and flip toautoonly whilebody.vpad-revealis set. - Service worker cache bumped to
tetris-v1.5.0. - README badge β 1.5.0.
Fixed
- Mobile no longer shows a black playground. The board fills the viewport at all phone sizes from iPhone SE to iPhone 16 Pro Max, and the overlay (title / pause / game-over) covers the full screen with a solid background.
[1.4.3] β 2026-06-14
Fixed
- Desktop: blocks bleeding past the right edge of the board frame. Global
box-sizing: border-box(set on*) meant.board-frame's 8px padding + 1px border ate 18px off its content area. The canvas was sized to the full--board-w, so its right column rendered ~18px outside the frame. Fix:.board-frame { width: max-content; overflow: hidden }so the frame shrink-wraps the canvas, and the desktop grid's middle column is nowautoinstead ofvar(--board-w). As a safety net,availableBoardWidth()now also subtracts the frame's horizontal padding + border from the width budget (mirroring whatavailableBoardHeight()already did). - Mobile: empty playground on iPhone. Two compounding bugs. (1)
.game-wrapusedgrid-template-columns: 1fr, stretching the frame to full stage width while the canvas (sized from JS to the integer cell) stayed much smaller β most of the frame was empty space. (2) The overlay usedcolor-mix()+backdrop-filter: blur(), both of which can fail silently on iOS Safari, leaving the title screen invisible. Fix: mobile grid is nowauto+justify-items: centerso the frame shrink-wraps the canvas, and the overlay uses a solidrgba(0,0,0,.65)background withbackdrop-filterdisabled on mobile. - Mobile: vpad too tall, eating board height. Buttons were
clamp(52px, 9.5dvh, 72px)with 8px gap β ~152px of two-row vpad. Trimmed toclamp(40px, 7dvh, 56px)with 6px gap β ~118px, giving the board ~34 extra pixels of vertical room (more cells, larger pieces).
Changed
- Service worker cache bumped to
tetris-v1.4.3. - README badge β 1.4.3.
[1.4.2] β 2026-06-14
Added
- HOLD ghost preview. When the HOLD slot is empty, the panel now shows a low-opacity (18%) ghost of the CURRENT piece instead of empty space. Hints at what hitting C will store and gives the HUD a calmer, more intentional look on desktop. The on-board ghost system was already there β this just reuses
drawCell({ ghost: true })inside the HOLD mini-canvas, no new rendering code.
Changed
- Service worker cache bumped to
tetris-v1.4.2. - README badge β 1.4.2.
[1.4.1] β 2026-06-14
Fixed
- Bottom-of-frame empty band on desktop and mobile. v1.4.0 set
--board-hfrom JS and derived--board-wvia CSScalc(--board-h / 2), with a 200 ms transition on the property. After a zoom or resize, the renderer (called via double-rAF) measured the canvas mid-tween: width was still catching up while the height kept growing toward its final value. The renderer locked in a cell size based on the intermediate width, so the play grid stopped short of the bottom of the frame β the empty rectangle visible in the screenshot. - New sizing model.
cell(in integer CSS pixels) is now the single source of truth. JS computes the largest integer cell that fits both the height and width budgets, then sets BOTH--board-h = cell * 20AND--board-w = cell * 10explicitly on.game-wrap. CSS no longer derives one dimension from the other. - No CSS transition on board dims. The instant snap eliminates the mid-tween mismatch entirely. Zoom and resize still feel smooth because the per-step delta is small; what users gain is pixel-exactness at every step.
- Mobile budget unchanged but now correct. Same chip-strip + vpad chrome math as v1.4.0, but the board itself is now guaranteed to fill the frame to the pixel on every iPhone-class device, including SE.
Changed
- Service worker cache bumped to
tetris-v1.4.1. - README badge β 1.4.1.
[1.4.0] β 2026-06-14
Added
- Real mobile layout. A dedicated single-screen design under 760 px wide: slim topbar, HOLD/STATS/NEXT chip strip above the board, board flush-centered, virtual pad pinned to the bottom with big touch targets. No more scrolling, no more wasted vertical space, no more tiny board.
- Hero hard-drop button. The
‡button in the vpad is accent-tinted and weight-bumped β the most important touch action is now the most prominent. - Landscape phone layout. When the device is < 900 px wide AND < 500 px tall, the vpad moves next to the board (right side) instead of stacking below it, so the board can still use the full viewport height.
- Extra-small breakpoint (β€ 360 px). Tighter chip padding, smaller HOLD/NEXT icons, and shorter vpad buttons so the game fits cleanly on iPhone SE-sized screens.
Changed
- Mobile topbar hides zoom buttons (the board auto-fits the viewport β manual zoom is a desktop windowing affordance).
- Mobile hides the footer (single-screen rule). The version badge in the topbar still links to the GitHub release.
- HOLD / NEXT canvases are now icon-sized chips on mobile, not full panels.
- Vpad: 6 buttons in a 3Γ2 grid (β β» βΆ / H ⼠‡),
clamp(52px, 9.5dvh, 72px)tall. - Service worker cache bumped to
tetris-v1.4.0. - README badge β 1.4.0.
Fixed
computeFittedHeight()now also subtracts the board-frame's own padding/border on both desktop and mobile, so the play grid lands flush against the frame inner edge.
[1.3.0] β 2026-06-14
Fixed
- Pixel-perfect zoom across every device. The play grid (10 cols Γ 20 rows) now stays exactly inside the board frame at every zoom level. Previous versions had two failure modes when zooming: the bottom row could either clip below the frame, or leave an empty strip under it β caused by
--board-htaking a non-integer-times-20 value, so the renderer's cell size (canvas.width / 10) had a fractional residual. - Grid-locked sizing.
--board-his now always snapped to a multiple ofROWS(= 20). That makes each cell an integer number of CSS pixels, so 20Γcell lands precisely on the bottom edge of the canvas regardless of viewport or zoom multiplier. - Honest mobile reservation. Vertical budget on mobile is measured from the actual rendered HUD + vpad heights via DOM rects, instead of guessing with a hardcoded reserve. Phones, tablets, and shrunken windows all converge on the largest grid-aligned board that fits.
- No more independent mobile clamps. Mobile no longer applies separate
min(width, 80vw)andmin(height, 160vw)to the canvas β those broke the 1:2 ratio when one axis clamped but the other didn't. JS sizes the board to fit the viewport, CSS just inherits. - Renderer bitmap stays in lockstep. After every
applyBoardSize(), the renderer re-syncs its DPR-scaled bitmap on a doublerequestAnimationFrameso layout has fully flushed beforegetBoundingClientRect()is read.
Changed
BOARD_MIN_H = 280(was 360) so very tight viewports can still show 14 px cells.- Resize/orientationchange handlers are now debounced via rAF.
- Service worker cache bumped to
tetris-v1.3.0. - README badge β 1.3.0.
[1.2.2] β 2026-06-14
Fixed
- Sustainable zoom/scaling rewrite. Replaced the legacy
transform: scale()+ clamp-on-canvas combo with a single CSS variable,--board-h, that drives every dependent dimension (canvas size, HUD widths, gap). The canvas grows and shrinks in real CSS pixels, so the bitmap (canvas.width Γ DPR) is always in lockstep withgetBoundingClientRect()β no smear trails on zoom-out, no empty space below the board on certain viewport heights, no aspect-ratio glitches. - fitToScreen is now device-aware. It computes the available height/width budget from the actual stage rect, accounting for mobile stacking and the virtual pad, then clamps the result to
[360px, 920px]. Manual zoom (60β160%) is a multiplier on top of the fitted base, so "Fit" then "+"/"β" composes correctly. - Viewport resilience. Listens to
resizeandorientationchangeso the board re-fits when the keyboard appears, an iOS browser bar collapses, or the user rotates the device.
Changed
- Service worker cache bumped to
tetris-v1.2.6. - README badge β 1.2.2.
[1.2.1] β 2026-06-14
Added
- Hold-box hint. The HOLD panel now shows
C or Shifton desktop andtap Hon touch devices, so players discover the swap mechanic without reading docs. - Brand reload + version link. Clicking the
tetrislogo reloads the page; thevX.Y.Zbadge links to that exact GitHub release in a new tab.
Fixed
- Canvas trail / smear on zoom-out. When the board was scaled down via CSS
transform: scale(),drawBoard()cleared in CSS-rect coordinates while the bitmap kept its larger dimensions β leaving stale pixels in the un-cleared region, visible as diagonal trails below the lowest blocks. The board now wipes the FULL bitmap each frame (same pattern already used bydrawMinifor HOLD / NEXT), and manual zoom triggersrenderer.resize()so the bitmap matches the new CSS size. - Zoom +/β buttons now stick. Clicking βFit to screenβ had set a
fit: trueflag thatapplySettings()honored on every subsequent action, immediately overwriting any manual zoom. Manual zoom now clearsfitso the user's choice persists. - Skip button in the score modal closes the dialog. It was silently swallowed by HTML5 form validation on the
requiredName input;formnovalidatelets Skip bypass validation.
Changed
- Score-save modal status line is friendlier and clearer: "Your score will be PGP signed upon download or submission." followed by current server reachability.
- Modal placeholders refreshed:
John Doe(name),I'm a Tetris fan(tagline). - Service worker cache bumped to
tetris-v1.2.4. - README badge β 1.2.1.
[1.2.0] β 2026-06-14
Added
- Real music! Ships a bundled CC0 chiptune MP3 (βHappy Adventure (Loop)β by Bart Kelsey, public-domain, ~620 KB) played via a plain
<audio loop>element. Replaces the v1.1.x procedural Korobeiniki Web Audio synth, which silently failed on many browsers due to autoplay restrictions and tab throttling. Toggling Music in Settings now reliably plays / pauses the loop.
Fixed
- Signed scoreboard receipts now actually sign. The PHP backend resolved the private-key directory to
<webroot>/../private/, which on Siteground's per-domain layout is~/tetris.rocks/private/β a different directory than the FTP-account-root~/private/where the keys lived. PHP now probes several candidate locations and uses whichever contains the key file. The deploy script also uploads keys to both locations as belt-and-suspenders. As a result the downloaded.txtis a real PGP cleartext-signed receipt, verifiable withgpg --verify. /api/healthnow reportsprivate_dirandpriv_key_presentso future key-misplacement issues are immediately visible.
Changed
- Service worker cache bumped to
tetris-v1.2.0and now precachesassets/music.mp3so the music plays offline too. - README version badge β 1.2.0.
[1.1.3] β 2026-06-14
Changed
- Press Start CTA animation swapped from "heartbeat scale" to a shake β a symmetric left/right wobble (Β±4px) with a long rest phase. Zero net displacement; the button starts and ends at exactly the same X.
- Music is on by default. New users get Korobeiniki the moment they hit Press Start. Existing users whose
musicpreference is already saved keep their choice.
Fixed
- Music wasn't actually starting for most users. The Settings dialog now toggles music live the moment the checkbox changes (instead of only after pressing Save). Clicking the checkbox is itself a user gesture, which is what browsers require to unlock the AudioContext.
- Same live-toggle now applies to the SFX checkbox.
[1.1.2] β 2026-06-14
Added
- Music! Procedural Korobeiniki (the classic Game Boy Tetris theme β public-domain Russian folk song from 1861) rendered live via Web Audio. Two-voice arrangement: square-wave lead + triangle-wave bass. Toggle in Settings β Music. Zero KB shipped β generated entirely from a note-array at runtime.
- New Modern-theme animations, all strictly in-place (no horizontal drift):
cta-heartbeaton the Press Start button: cardiac two-beat pulse (scale + glow).brand-breatheon the logo: gentle breathing replaces the previous full 360Β° rotation.aurora-breathewash behind the board.stats-glowstaggered pulse on score/lines/level/time values.hud-underline-pulseanimated accent under each HUD label.overlay-glowsubtle vignette pulse on the Press Start / Pause / Game Over card.
Fixed
- Name input swallowed letters: typing βpβ, βcβ, βxβ, βzβ, etc. in the score-submit form was triggering pause / hold / rotate because the global keyboard handler intercepted every keydown. Input now bails out when focus is on a text field.
- Press-Start animation slid off to the right (
translateXshimmer). Replaced with a contained heartbeat scale; nothing leaves the button bounds anymore. - βServer unreachableβ on score submit: Sitegroundβs
sgcaptchabot-protection layer was serving HTML CAPTCHA pages in response to JSON POSTs from new IPs. Added explicitAccept: application/json+X-Requested-With: XMLHttpRequestheaders, a content-type check that throws clean on HTML responses, and an.htaccessdirective to mark/api/*as already-challenged.
Changed
- README version badge now links to the matching GitHub release.
[1.1.0] β 2026-06-14
Added
- PHP scoreboard backend in
server-php/. A single-file API (api/index.php) that runs on any shared PHP host (PHP 7.4+) using either thegnupgextension or thegpgbinary. Private key lives above the webroot. This is the backend running in production on tetris.rocks. - Same-origin
/apiauto-detection in the frontend: when the game is served over HTTPS, the scoreboard client now defaults to the same origin, so deploying to a domain with the PHP backend co-located just works β no settings needed. - Production deployment + docs for Siteground / shared FTP hosts.
Changed
- Renamed
server/βserver-node/to reflect that there are now two interchangeable backends. README.mdandINSTALL.mdnow featuretetris.rocksand document the PHP path as primary, with Fly.io as alternative.
[1.0.0] β 2026-06-14
First public release.
Added
- Pure-JS Tetris engine: 7-bag random generator, SRS-style wall kicks, hold, ghost piece, lock delay.
- Three themes β Classic (B&W, Game Boy / terminal), Color (GBC pixel art), Modern (glass with animations) β each with dark and light modes.
- Keyboard controls (arrow keys,
X/Z,C,Space,P). - Touch controls β drag to move, tap to rotate, fast swipe-down for hard drop.
- On-screen virtual D-pad (auto-shown on touch devices, configurable).
- Zoom in / out / fit-to-screen with localStorage persistence.
- Animated tetromino background, theme-aware.
- HUD: live score, lines, level, time, hold slot, next-4 queue.
- Procedural SFX via Web Audio API.
- Service worker for offline play (PWA, installable).
- Game-over flow with name / tagline / email form.
- Signed-
.txtscore receipt β downloadable and PGP-signed by the configured server. - Local-only fallback: when no server is configured, the game stores scores in
localStorageand produces an unsigned receipt. - Public scoreboard backend (Fly.io + SQLite + Fastify + openpgp).
keygenscript to generate an Ed25519 PGP keypair for the server.- Full README, INSTALL guide, verification docs, MIT license.
Full Changelog: v1.5.2...v1.5.3