The tutorial was borrowing a court tile, the in-game how-to icon and a weapon icon, none of which were drawn to be looked at. Four purpose-made 2:1 panels now: the serve with a tapping hand, the slingshot pull with chevrons into a zombie, the weapon rack, and the floodlit duel. They get the full card width above Marvin instead of being squeezed into the text column beside him. The menu wears the title logo instead of a Silkscreen headline, and the two buttons wear generated plates — START GAME bloodied, TUTORIAL nearly clean. The lettering stays Silkscreen on top: generated type is a lottery and would not match the rest of the HUD. Two things the art needed. Quantise the panels at 128 colours, not 64 — big flat fields with saturated accents is where a small palette shows, and 64 turned the tennis ball beige. And find a plate by its BRIGHT BORDER, never by the corner colour: one plate came back on a near-black margin, the other on a transparency checkerboard that differs from itself everywhere and matched the whole canvas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
186 lines
12 KiB
HTML
186 lines
12 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; } }
|
|
/* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame
|
|
alive across every screen is the point — a screen that unmounts the whole overlay flashes the
|
|
court through between menus. */
|
|
#splash, #menu, #courts, #tutorial { display: none; }
|
|
#intro[data-stage="splash"] #splash { display: block; }
|
|
#intro[data-stage="menu"] #menu { display: block; animation: drop 0.3s ease-out; }
|
|
#intro[data-stage="courts"] #courts { display: block; animation: drop 0.3s ease-out; }
|
|
#intro[data-stage="tutorial"] #tutorial { display: block; animation: drop 0.3s ease-out; }
|
|
/* headline: taller than the body text, so a screen reads as a screen and not as a list */
|
|
#intro h2 { margin: 0 0 20px; font: 400 15px Silkscreen, monospace; color: #e0cc72;
|
|
letter-spacing: 3px; line-height: 1.5; }
|
|
/* one button look for every menu screen */
|
|
.btn { display: block; width: min(340px, 84vw); margin: 0 auto 10px; padding: 12px 10px;
|
|
background: rgba(255,255,255,0.04); border: 2px solid #3b4354; color: #dfe6f2;
|
|
font: 400 12px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent; }
|
|
.btn.go { border-color: #e0cc72; color: #e0cc72; }
|
|
/* The two menu buttons wear generated plates. The LETTERING stays Silkscreen on top rather than
|
|
being drawn into the art: generated type is a lottery and would not match the rest of the HUD.
|
|
A text-shadow is what keeps it readable where a drip runs behind it. */
|
|
.btn.plate, .btn.plate2 { border: none; padding: 16px 10px; background-size: 100% 100%;
|
|
background-repeat: no-repeat; image-rendering: pixelated;
|
|
text-shadow: 0 2px 0 rgba(0,0,0,0.85); }
|
|
.btn.plate { background-image: url('assets/btn_start.png?v=92'); color: #f4e6a6; }
|
|
.btn.plate2 { background-image: url('assets/btn_tut.png?v=92'); color: #cfd8e6; }
|
|
#menu .logo { width: min(280px, 70vw); margin-bottom: 22px; }
|
|
.btn.back { font-size: 10px; padding: 9px 10px; color: #8a96ab; }
|
|
/* 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.court { 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.court.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; }
|
|
#courts .acts { margin-top: 16px; }
|
|
/* tutorial: Marvin on the left, his line and the picture on the right */
|
|
#tutorial .page { display: flex; align-items: flex-end; gap: 14px; width: min(360px, 88vw);
|
|
margin: 0 auto; text-align: left; min-height: 190px; }
|
|
#tutorial .marvin { width: 84px; flex: none; image-rendering: pixelated; align-self: flex-end; }
|
|
#tutorial .say { flex: 1; }
|
|
#tutorial .say p { margin: 0 0 10px; font: 400 10px Silkscreen, monospace; color: #dfe6f2; line-height: 1.9; }
|
|
#tutorial .say em { color: #e0cc72; font-style: normal; }
|
|
/* the panel is 2:1 and it is the point of the page, so it gets the full card width above the
|
|
Marvin row rather than being squeezed into the text column beside him */
|
|
#tutorial .shot { display: block; width: min(360px, 88vw); margin: 0 auto 12px;
|
|
image-rendering: pixelated; border: 2px solid #2a3140; }
|
|
#tutorial .dots { margin: 14px 0 10px; font: 400 10px Silkscreen, monospace; color: #5a6270; letter-spacing: 3px; }
|
|
#tutorial .acts { display: flex; gap: 8px; width: min(340px, 84vw); margin: 0 auto; }
|
|
#tutorial .acts .btn { margin: 0; flex: 1; }
|
|
</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" data-stage="splash">
|
|
<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>
|
|
<div id="menu">
|
|
<!-- the logo IS the menu's headline; the title screen's own art, not a word set in Silkscreen -->
|
|
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
|
|
<button class="btn go plate" id="mStart">START GAME</button>
|
|
<button class="btn plate2" id="mTut">TUTORIAL</button>
|
|
</div>
|
|
<!-- court 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 id="courtList"></div>
|
|
<div class="acts">
|
|
<button class="btn go" id="cStart">START GAME</button>
|
|
<button class="btn back" id="cBack">BACK</button>
|
|
</div>
|
|
</div>
|
|
<!-- tutorial pages are built from a table in main.js -->
|
|
<div id="tutorial">
|
|
<h2>HOW IT WORKS</h2>
|
|
<img class="shot" id="tShot" alt="">
|
|
<div class="page">
|
|
<img class="marvin" id="tMarvin" src="assets/marvin1.png" alt="">
|
|
<div class="say" id="tSay"></div>
|
|
</div>
|
|
<p class="dots" id="tDots"></p>
|
|
<div class="acts">
|
|
<button class="btn back" id="tBack">BACK</button>
|
|
<button class="btn go" id="tNext">NEXT</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script type="module" src="main.js?v=93"></script>
|
|
</body>
|
|
</html>
|