zombie-mode: 5.9 MB of 24-bit WAV down to 287 KB. Its master is 982597 frames at 48 kHz, so loopEnd is the un-round 20.47077 — the decoder hands back 20.4787, and those 7.9 ms of AAC padding would tick once a loop. Nothing in assets/audio/ is a multi-megabyte WAV any more. o_run1/o_run2 had no racket in either hand. The cause was the batch prompt: opponent_run never said "racket", and its reference sheet was built from the racket-less frames it was replacing, so there was nothing to copy one from. It is a ref_only batch off o_idle1 now — same man, front-facing, racket in his right hand — with the subject spelling out that he still carries it mid-stride. The stride reads as well as the old one did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
8.1 KiB
HTML
123 lines
8.1 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; } }
|
|
/* court select — the intro's second stage. One overlay, two panels, .picking swaps them, so the
|
|
backdrop and the card frame never flash between screens. */
|
|
#courts { display: none; }
|
|
#intro.picking #splash { display: none; }
|
|
#intro.picking #courts { display: block; animation: drop 0.3s ease-out; }
|
|
#courts h2 { margin: 0 0 14px; font: 400 11px Silkscreen, monospace; color: #e0cc72; letter-spacing: 2px; }
|
|
/* a row, not a stacked tile: three 3:2 tiles stacked fill a phone twice over, and the art still
|
|
reads at this size. -webkit-tap-highlight-color kills iOS's blue flash over the pixel art. */
|
|
#courts button { display: flex; align-items: center; gap: 12px; width: min(340px, 84vw);
|
|
margin: 0 auto 8px; padding: 6px; background: rgba(255,255,255,0.04);
|
|
border: 2px solid #3b4354; color: inherit; font: inherit; text-align: left;
|
|
cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
#courts button.on { border-color: #e0cc72; background: rgba(224,204,114,0.10); }
|
|
#courts img { width: 108px; height: 72px; display: block; image-rendering: pixelated; flex: none; }
|
|
#courts b { display: block; font: 400 11px Silkscreen, monospace; color: #dfe6f2; letter-spacing: 0.5px; }
|
|
#courts i { display: block; margin-top: 5px; font: 400 9px Silkscreen, monospace; color: #8a96ab; font-style: normal; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="c" width="480" height="270"></canvas>
|
|
<div id="safe"></div>
|
|
<div id="hint">move: WASD/arrows · serve/swing: SPACE · aim: hold a direction while swinging · T: tuning panel</div>
|
|
<div id="intro">
|
|
<div class="card">
|
|
<div id="splash">
|
|
<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>
|
|
<!-- rows are built from config.COURTS in main.js: adding a court is a table row, never markup -->
|
|
<div id="courts"><h2>CHOOSE YOUR COURT</h2></div>
|
|
</div>
|
|
</div>
|
|
<script type="module" src="main.js?v=87"></script>
|
|
</body>
|
|
</html>
|