Files
splatter-tennis/index.html
T
wuwiandClaude Opus 5 6378ff2a1b Cut the serve down to three poses
Oliver, after playing the four-stage version on the phone: idle, then the racket
dropping on the pull, then the drive on release — "that's all, forget all the
others". At thumb speed the middle stages are never read, so they are deleted
rather than kept around: p_pull is the wind-up, p_smash is the drive, and both
come out of the photo-referenced sheet, so the whole serve is one drawing style.

The dead batches and the arm_left guard go with them. assets/_pose_ref_serve.png
is kept as the recipe: his photo sequence over our own sprites, which is what
turned a fifteen-generation prose argument into one generation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 01:09:57 +02:00

103 lines
6.5 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Splatter Tennis</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<!-- save-to-homescreen: a bloody tennis ball. iOS needs the apple-* tags (it ignores the
manifest's display/name); Android reads the manifest. -->
<link rel="icon" type="image/png" href="assets/favicon.png">
<link rel="apple-touch-icon" href="assets/icon-180.png">
<link rel="manifest" href="manifest.webmanifest">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="Splatter">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#2b2735">
<style>
/* pixel HUD font — bundled so it renders offline; used by the canvas scoreboard + rack */
@font-face { font-family: 'Silkscreen'; font-weight: 400; font-display: block;
src: url('assets/fonts/Silkscreen-Regular.ttf') format('truetype'); }
@font-face { font-family: 'Silkscreen'; font-weight: 700; font-display: block;
src: url('assets/fonts/Silkscreen-Bold.ttf') format('truetype'); }
/* html needs a real HEIGHT: the root element's background paints the viewport, but a GRADIENT is
laid out in the root's own box — and with the canvas position:fixed the root has no content and
collapses, so the gradient render.js sets had nowhere to go. */
html { height: 100%; height: 100dvh; }
html, body { margin: 0; padding: 0; background: #10141c; overflow: hidden; }
/* invisible probe: env() is only readable from CSS, so park the insets on an element and let
render.js measure them. The canvas stays FULL-BLEED (the stands under the notch look good) —
it is the HUD that has to move, see SAFE in render.js. */
#safe { position: fixed; top: 0; left: 0; width: 0; height: 0; pointer-events: none; visibility: hidden;
padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); }
/* The canvas STRETCHES to the viewport and render.js derives its logical height from the box it
actually got. Sizing it the other way round (compute a scale from innerWidth/innerHeight, then set
explicit px) meant it was laid out against whatever innerHeight reported at load — before iOS
Safari's toolbars had settled — and it stayed a strip short of the bottom forever after.
100dvh is the dynamic viewport height, so it tracks those toolbars. */
/* TOP inset only. The status bar is opaque UI and the scoreboard must clear it, so the canvas starts
below it — but the home indicator is a slim translucent bar, so the canvas runs all the way to the
PHYSICAL bottom edge and the court art fills it. Only the weapon RACK lifts clear of the indicator,
via SAFE.bottom in render.js. Reserving the bottom inset here instead just left a dead band.
100dvh is the dynamic viewport height, so it tracks iOS Safari's toolbars appearing/hiding. */
canvas { image-rendering: pixelated; display: block; position: fixed; touch-action: none;
top: env(safe-area-inset-top, 0px);
left: env(safe-area-inset-left, 0px);
width: calc(100vw - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
height: calc(100vh - env(safe-area-inset-top, 0px));
height: calc(100dvh - env(safe-area-inset-top, 0px)); }
/* ...but NOT installed. Launched from the homescreen, WebKit does not initialise the DYNAMIC
viewport until the geometry is "exercised" — 100dvh stays provisional until you rotate the
phone, which is exactly the bug. Standalone has no toolbars, so 100vh is both correct and
stable from the first frame; dvh only exists for the browser's disappearing toolbars. */
@media (display-mode: standalone) {
html { height: 100vh; }
canvas { height: calc(100vh - env(safe-area-inset-top, 0px)); }
}
/* desktop key hint — up in the empty top-right corner now that the rack owns the bottom edge */
#hint { position: fixed; top: 6px; right: 10px; text-align: right;
font: 400 10px Silkscreen, monospace; color: #6b7688; }
@media (pointer: coarse) { #hint, #intro .kb { display: none; } }
#intro { position: fixed; inset: 0; z-index: 10; touch-action: none; display: grid; place-items: center;
background: radial-gradient(circle at 50% 34%, rgba(34,16,20,0.92), rgba(8,10,16,0.96)); }
#intro .card { font: 400 11px Silkscreen, monospace; color: #dfe6f2; text-align: center; line-height: 1.9;
padding: 18px 26px; max-width: 92vw; animation: drop 0.5s ease-out; }
@keyframes drop { from { transform: translateY(-14px); opacity: 0; } }
/* the logo is generated pixel art now (assets/logo.png), not stacked CSS text */
#intro .logo { display: block; width: min(320px, 78vw); height: auto; margin: 0 auto 8px;
image-rendering: pixelated; filter: drop-shadow(0 3px 0 rgba(0,0,0,0.45)); }
#intro .slogan { margin: 8px 0 16px; font-size: 12px; color: #b6a0a4; letter-spacing: 0.5px; }
#intro .slogan b { color: #c9564a; }
#intro .how { display: inline-block; text-align: left; color: #cbd4e2; line-height: 2; }
#intro .how p { margin: 0; }
#intro .how span { display: inline-block; width: 30px; }
#intro em { color: #8a96ab; font-style: normal; font-size: 12px; }
/* which build is served — filled in by main.js, empty (and invisible) if the header is missing */
#build { margin: 10px 0 0; font-size: 9px; color: #5a6270; letter-spacing: 0.5px; }
#intro .start { margin-top: 18px; color: #e0cc72; font-weight: bold; letter-spacing: 1px; animation: blink 1.1s step-end infinite; }
@keyframes blink { 50% { opacity: 0.25; } }
</style>
</head>
<body>
<canvas id="c" width="480" height="270"></canvas>
<div id="safe"></div>
<div id="hint">move: WASD/arrows &nbsp;·&nbsp; serve/swing: SPACE &nbsp;·&nbsp; aim: hold a direction while swinging &nbsp;·&nbsp; T: tuning panel</div>
<div id="intro">
<div class="card">
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
<div class="how">
<p><span>👆</span> tap — serve + swing</p>
<p><span>👉</span> flick — aimed shot</p>
<p><span>✋</span> drag — run <em>(auto-run is on)</em></p>
<p class="kb"><span>⌨️</span> WASD / arrows + SPACE</p>
</div>
<p class="start">TAP TO START</p>
<p id="build"></p>
</div>
</div>
<script type="module" src="main.js?v=80"></script>
</body>
</html>