Draw the tutorial properly, and put the logo and gore on the menu

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>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-01 13:43:43 +02:00
1 parent 9a65a50cc4
commit 4a034773aa
9 files changed
+46 -13

No files matched your search

+18
View File
@@ -460,6 +460,24 @@ screen that unmounts the overlay flashes the live court through between menus. C
only; `START GAME` and `BACK` are their own buttons. The tutorial is a data table (`TUT` in main.js) only; `START GAME` and `BACK` are their own buttons. The tutorial is a data table (`TUT` in main.js)
of Marvin poses, an optional picture already in `assets/`, and his lines — adding a page is a row. of Marvin poses, an optional picture already in `assets/`, and his lines — adding a page is a row.
**Menu and tutorial art is generated DIRECTLY, not through `_regen.py`.** That pipeline keys magenta
and slices figures out of a sheet; these are whole illustrated panels with backgrounds, so they go
through `generate_image.py` and a downscale/quantise pass, the same route the court tiles took. Two
things learned making them:
- **Quantise these at 128 colours, not 64.** The panels are big flat fields with a few saturated
accents (the ball, fire, blood), which is exactly where a small palette shows — 64 turned the
tennis ball beige.
- **Find a generated plate by its BRIGHT BORDER, never by the corner colour.** One button plate came
back on a near-black margin and the other on a transparency CHECKERBOARD, which differs from
itself everywhere and matched the entire canvas. The threshold has to adapt per image too: the
checkerboard's own greys are brighter than the darker plate's border.
The button plates are blank on purpose — **the lettering stays Silkscreen on top of the art**.
Generated type is a lottery and would not match the rest of the HUD. `btn_start` is a BOTTOM band of
a taller plate: by there the drips have run out to the ends and the centre is clear enough to print
over, and the plate's own bottom border comes with it.
**The menu theme starts on the FIRST TAP, never at boot** (`go()` calls `menuMusic`). A title screen **The menu theme starts on the FIRST TAP, never at boot** (`go()` calls `menuMusic`). A title screen
that plays at you before you touch it is rude, and on a phone the autoplay policy would have held it that plays at you before you touch it is rude, and on a phone the autoplay policy would have held it
silent until that tap regardless. silent until that tap regardless.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+19 -6
View File
@@ -93,6 +93,15 @@
font: 400 12px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer; font: 400 12px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer;
-webkit-tap-highlight-color: transparent; } -webkit-tap-highlight-color: transparent; }
.btn.go { border-color: #e0cc72; color: #e0cc72; } .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; } .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 /* 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. */ reads at this size. -webkit-tap-highlight-color kills iOS's blue flash over the pixel art. */
@@ -112,8 +121,10 @@
#tutorial .say { flex: 1; } #tutorial .say { flex: 1; }
#tutorial .say p { margin: 0 0 10px; font: 400 10px Silkscreen, monospace; color: #dfe6f2; line-height: 1.9; } #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; } #tutorial .say em { color: #e0cc72; font-style: normal; }
#tutorial .shot { display: block; max-width: 100%; max-height: 108px; margin: 0 0 8px; /* the panel is 2:1 and it is the point of the page, so it gets the full card width above the
image-rendering: pixelated; } 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 .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 { display: flex; gap: 8px; width: min(340px, 84vw); margin: 0 auto; }
#tutorial .acts .btn { margin: 0; flex: 1; } #tutorial .acts .btn { margin: 0; flex: 1; }
@@ -139,9 +150,10 @@
<p id="build"></p> <p id="build"></p>
</div> </div>
<div id="menu"> <div id="menu">
<h2>SPLATTER TENNIS</h2> <!-- the logo IS the menu's headline; the title screen's own art, not a word set in Silkscreen -->
<button class="btn go" id="mStart">START GAME</button> <img class="logo" src="assets/logo.png" alt="Splatter Tennis">
<button class="btn" id="mTut">TUTORIAL</button> <button class="btn go plate" id="mStart">START GAME</button>
<button class="btn plate2" id="mTut">TUTORIAL</button>
</div> </div>
<!-- court rows are built from config.COURTS in main.js: adding a court is a table row, never markup --> <!-- court rows are built from config.COURTS in main.js: adding a court is a table row, never markup -->
<div id="courts"> <div id="courts">
@@ -155,6 +167,7 @@
<!-- tutorial pages are built from a table in main.js --> <!-- tutorial pages are built from a table in main.js -->
<div id="tutorial"> <div id="tutorial">
<h2>HOW IT WORKS</h2> <h2>HOW IT WORKS</h2>
<img class="shot" id="tShot" alt="">
<div class="page"> <div class="page">
<img class="marvin" id="tMarvin" src="assets/marvin1.png" alt=""> <img class="marvin" id="tMarvin" src="assets/marvin1.png" alt="">
<div class="say" id="tSay"></div> <div class="say" id="tSay"></div>
@@ -167,6 +180,6 @@
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="main.js?v=91"></script> <script type="module" src="main.js?v=93"></script>
</body> </body>
</html> </html>
+9 -7
View File
@@ -78,22 +78,22 @@ const TUT = [
{ pose: 1, say: ["I'm <em>MARVIN</em>. I mop the court.", { pose: 1, say: ["I'm <em>MARVIN</em>. I mop the court.",
"Lately there is a great deal more to mop.", "Lately there is a great deal more to mop.",
"Let me show you how this goes."] }, "Let me show you how this goes."] },
{ pose: 2, shot: 'court_grass', say: ["First it is just <em>TENNIS</em>.", { pose: 2, shot: 'tut_serve', say: ["First it is just <em>TENNIS</em>.",
"<em>TAP</em> once to toss, tap again to serve.", "<em>TAP</em> once to toss, tap again to serve.",
"<em>FLICK</em> toward where you want it.", "<em>FLICK</em> toward where you want it.",
"<em>DRAG</em> to run — though he mostly runs himself."] }, "<em>DRAG</em> to run — though he mostly runs himself."] },
{ pose: 4, say: ["Then, at some point, the other fellow...", { pose: 4, say: ["Then, at some point, the other fellow...",
"stops being the other fellow.", "stops being the other fellow.",
"Nobody stops the match. Nobody ever does."] }, "Nobody stops the match. Nobody ever does."] },
{ pose: 2, shot: 'x_howto', say: ["Now you do not rally. You <em>AIM</em>.", { pose: 2, shot: 'tut_aim', say: ["Now you do not rally. You <em>AIM</em>.",
"<em>PULL BACK</em> from your man and let go.", "<em>PULL BACK</em> from your man and let go.",
"The further you pull, the harder it lands.", "The further you pull, the harder it lands.",
"A soft one only makes them cross."] }, "A soft one only makes them cross."] },
{ pose: 2, shot: 'w_bomb', say: ["I bring tins out to the rack down there.", { pose: 2, shot: 'tut_rack', say: ["I bring tins out to the rack down there.",
"<em>BOMBS</em>, <em>TRIPLES</em>, <em>FIRE</em>, and the <em>GUN</em>.", "<em>BOMBS</em>, <em>TRIPLES</em>, <em>FIRE</em>, and the <em>GUN</em>.",
"Tap the rack to load one, then serve it.", "Tap the rack to load one, then serve it.",
"Do try not to hit me with them."] }, "Do try not to hit me with them."] },
{ pose: 3, say: ["Sometimes the umpire calls order back.", { pose: 3, shot: 'tut_duel', say: ["Sometimes the umpire calls order back.",
"One point, tennis rules, against one of them.", "One point, tennis rules, against one of them.",
"The lines are live again. Do not miss."] }, "The lines are live again. Do not miss."] },
{ pose: 1, say: ["That is the lot. Mind the deep ones.", { pose: 1, say: ["That is the lot. Mind the deep ones.",
@@ -102,12 +102,14 @@ const TUT = [
]; ];
let tutPage = 0; let tutPage = 0;
const tSay = document.getElementById('tSay'), tMarvin = document.getElementById('tMarvin'); const tSay = document.getElementById('tSay'), tMarvin = document.getElementById('tMarvin');
const tShot = document.getElementById('tShot');
const tDots = document.getElementById('tDots'), tNext = document.getElementById('tNext'); const tDots = document.getElementById('tDots'), tNext = document.getElementById('tNext');
function drawTut() { function drawTut() {
const p = TUT[tutPage]; const p = TUT[tutPage];
tMarvin.src = `assets/marvin${p.pose}.png?v=91`; tMarvin.src = `assets/marvin${p.pose}.png?v=92`;
tSay.innerHTML = (p.shot ? `<img class="shot" src="assets/${p.shot}.png?v=91" alt="">` : '') tShot.style.display = p.shot ? 'block' : 'none';
+ p.say.map(l => `<p>${l}</p>`).join(''); if (p.shot) tShot.src = `assets/${p.shot}.png?v=93`;
tSay.innerHTML = p.say.map(l => `<p>${l}</p>`).join('');
tDots.textContent = TUT.map((_, i) => i === tutPage ? '●' : '·').join(''); tDots.textContent = TUT.map((_, i) => i === tutPage ? '●' : '·').join('');
tNext.textContent = tutPage === TUT.length - 1 ? 'DONE' : 'NEXT'; tNext.textContent = tutPage === TUT.length - 1 ? 'DONE' : 'NEXT';
} }