Files
splatter-tennis/index.html
T
wuwiandClaude Opus 5 f3dcf0b16f Splatter: chevrons in perspective, and a smash that actually flies
The chevrons were stepped along the SCREEN line at a fixed size, so they read as
a flat sticker over a 3D court. They now step through world space and take their
size from their own projection scale — near ones roughly twice the far ones —
and they fade IN toward the target so the eye is walked up the lane instead of
being handed a uniform stripe. The arrowhead scales with the target's own depth
too. (proj is a true projective map, so the screen direction stays constant
along the line; only size and alpha needed to vary.)

Hard shots now fly hard: serve pace is weighted onto quality (19 base + 22×q,
so 21 -> 41 m/s) instead of 21 -> 28. Two things had to give way for it.

Serve topspin was a fixed 1 + 0.35q, which meant a fast flat ball could not
both clear the tape and land on a target, so the net-loft loop throttled every
smash back to walking pace — measured 21.8 m/s whether you drew 60px or 95px.
TUNE.serveSpin 1.3 lets it leave flat and DIVE, which is what a smash is.

And the loft loop's margin was doHit's quality cushion, which taxed exactly the
shots meant to hurt. It's now sized to the real error it exists to cover: the
sim samples the net plane once a frame, so a diving ball can test below the tape
when the closed form clears it. Margin = descent rate × the dt clamp. A flat
drive pays almost nothing, a lob pays what it must.

Measured, target 20m out: 24.7 / 32.2 / 41 m/s for weak / mid / full draw. At
the net it's 9.6 / 11.2 / 11.6 — still the slow floaty lob, as it should be.

Ball-vs-shambler contact gets a crude sweep (radius + half a frame's travel),
because a 41 m/s ball steps 0.7m per frame at 60fps and 1.6m at main.js's dt
clamp, both wider than a shambler. Verified the full smash connects at both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:36:06 +03:00

58 lines
3.3 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">
<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, body { margin: 0; height: 100%; background: #10141c; display: grid; place-items: center; }
canvas { image-rendering: pixelated; width: min(96vw, calc(96vh * 16 / 9)); aspect-ratio: 16/9; touch-action: none; }
#hint { position: fixed; bottom: 8px; width: 100%; text-align: center;
font: 12px 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: 13px 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; } }
#intro .logo { line-height: 0.9; margin-bottom: 8px; }
#intro .logo span { display: block; font-weight: 900; }
#intro .l1 { font-size: 46px; letter-spacing: 3px; color: #e0231a;
text-shadow: 0 2px 0 #7a0f0a, 0 4px 0 #550906, 0 6px 1px rgba(0,0,0,0.4), 0 0 20px rgba(224,35,26,0.35); }
#intro .l2 { font-size: 30px; letter-spacing: 9px; color: #eef23a; margin-top: 2px;
text-shadow: 0 2px 0 #59610f, 0 0 14px rgba(238,242,58,0.3); }
#intro .slogan { margin: 8px 0 16px; font-size: 13px; font-style: italic; color: #b6a0a4; letter-spacing: 0.5px; }
#intro .slogan b { color: #e0231a; font-style: normal; }
#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; }
#intro .start { margin-top: 18px; color: #f8e048; 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="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">
<div class="logo"><span class="l1">SPLATTER</span><span class="l2">TENNIS</span></div>
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
<div class="how">
<p><span>👆</span> tap — serve &amp; 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>
</div>
</div>
<script type="module" src="main.js?v=19"></script>
</body>
</html>