The whites take a wash, the friend brings their own colours, and the menu shows the man
Three things off the back of the picker. THE LIVE PLAYER on the setup screen is the real sprite through the real tint, not a mock-up of the pick — so the menu cannot promise a colour the court will not keep. It redraws on every tap of a tour, a kit or a skin. THE WOMEN'S TOUR IS NO LONGER SAT OUT. Her set is drawn head to toe in whites, so there was no hue to rotate — the answer is not a dye but a WASH: the pick laid over her whites at a low saturation with the lightness kept, which is how you dye a white kit in life and gives rose, powder blue, mint, lilac. Her hair and skin fall out of it on lightness alone, which is exactly what a dye could not manage. THE FRIEND ACROSS THE NET now wears what they picked on their own phone. Their look crosses on pairing, and since the opponent art is red rather than blue the kit rides its own hue arc — safe there for the one reason it would not be on the player's own sheets: the opponent sprites carry no drawn blood. An older build simply never sends it and the opponent draws as before; the match still plays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1daa3e9d6a
commit
b5838b2b24
6 files changed
+194
-64
No files matched your search
+60
-28
@@ -1538,18 +1538,28 @@ const inArc = (h, [a, b]) => a <= b ? h >= a && h < b : h >= a || h < b;
|
||||
// interesting claim in this feature is "the kit can move without the skin moving", and that claim
|
||||
// lives entirely here; test.js checks it against the colours actually measured off p_idle1.png,
|
||||
// with no DOM in the way. Returns null for "leave this pixel exactly as it is".
|
||||
export function tintPx(r, g, b, k, sk) {
|
||||
export function tintPx(r, g, b, k, sk, mode = 'dye', arc = TINT.kit) {
|
||||
const [h, s, l] = rgb2hsl(r, g, b);
|
||||
// each bucket carries its own fence — see TINT. The kit only needs a low saturation floor (nothing
|
||||
// else on the sheet is blue, and a low floor catches its soft edges); the skin needs a floor AND a
|
||||
// lightness ceiling, because the shirt's warm highlights sit above every real skin tone.
|
||||
const spec = inArc(h, TINT.kit) && s >= TINT.satKit ? k
|
||||
: inArc(h, TINT.skin) && s >= TINT.satSkin && l <= TINT.lumSkin ? sk : null;
|
||||
if (!spec || spec.h == null) return null; // "as drawn" entries cost one branch and exit
|
||||
return hsl2rgb(spec.h, Math.min(1, s * (spec.s ?? 1)), Math.min(1, l * (spec.l ?? 1)));
|
||||
// THE KIT, two ways. 'dye' rotates a coloured kit's hue and needs only a low saturation floor
|
||||
// (nothing else on those sheets is blue, and a low floor catches the soft edges). 'wash' lays the
|
||||
// pick over a WHITE kit — bright, barely saturated — which is a different test entirely and one
|
||||
// that would swallow the men's shirt, which is why it belongs to the tour and not to the pixel.
|
||||
const isKit = mode === 'wash' ? (l >= TINT.whiteLum && s <= TINT.whiteSat)
|
||||
: (inArc(h, arc) && s >= TINT.satKit);
|
||||
if (isKit) {
|
||||
if (!k || k.h == null) return null;
|
||||
return mode === 'wash'
|
||||
// lightness kept (times any darkening the pick asks for), the hue laid on at a pastel strength
|
||||
? hsl2rgb(k.h, Math.min(1, TINT.washS * (k.s ?? 1)), Math.min(1, l * TINT.washL * (k.l ?? 1)))
|
||||
: hsl2rgb(k.h, Math.min(1, s * (k.s ?? 1)), Math.min(1, l * (k.l ?? 1)));
|
||||
}
|
||||
// the skin needs a floor AND a ceiling: below the floor is grey, above the ceiling is shirt
|
||||
if (!(inArc(h, TINT.skin) && s >= TINT.satSkin && l <= TINT.lumSkin)) return null;
|
||||
if (!sk || sk.h == null) return null; // "as drawn" entries cost one branch and exit
|
||||
return hsl2rgb(sk.h, Math.min(1, s * (sk.s ?? 1)), Math.min(1, l * (sk.l ?? 1)));
|
||||
}
|
||||
|
||||
function tint(img, k, sk) {
|
||||
function tint(img, k, sk, mode, arc) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width; c.height = img.height;
|
||||
const g = c.getContext('2d');
|
||||
@@ -1557,34 +1567,56 @@ function tint(img, k, sk) {
|
||||
const d = g.getImageData(0, 0, c.width, c.height), px = d.data;
|
||||
for (let i = 0; i < px.length; i += 4) {
|
||||
if (px[i + 3] < 8) continue; // fully transparent: nothing to recolour
|
||||
const out = tintPx(px[i], px[i + 1], px[i + 2], k, sk);
|
||||
const out = tintPx(px[i], px[i + 1], px[i + 2], k, sk, mode, arc);
|
||||
if (out) { px[i] = out[0]; px[i + 1] = out[1]; px[i + 2] = out[2]; }
|
||||
}
|
||||
g.putImageData(d, 0, 0);
|
||||
return c;
|
||||
}
|
||||
|
||||
// Recoloured sheets are cached PER FRAME NAME and built on first draw, not on the pick: a swatch tap
|
||||
// costs nothing, and each frame pays its ~1 ms once, the first time it is actually on screen. The
|
||||
// cache is keyed on the pick, so changing it mid-run (the setup screen is reachable between runs,
|
||||
// and the bench can drive it live) drops the old bodies instead of showing stale ones.
|
||||
// Recoloured sheets are built on FIRST DRAW, not on the pick: a swatch tap costs nothing, and each
|
||||
// frame pays its ~1 ms once, the first time it is actually on screen. The cache key carries the
|
||||
// whole look, so a changed pick simply misses and rebuilds — there is no invalidation to forget.
|
||||
// The cap is a runaway guard for the sprite bench, which can cycle looks all afternoon.
|
||||
const TINTC = new Map();
|
||||
let tintKey = '';
|
||||
export function tinted(n, img, k, sk, mode, arc) {
|
||||
if ((!k || k.h == null) && (!sk || sk.h == null)) return img; // "as drawn" both ways: no pass
|
||||
const key = n + '|' + (k ? k.id : '-') + '|' + (sk ? sk.id : '-') + '|' + mode;
|
||||
let out = TINTC.get(key);
|
||||
if (!out) {
|
||||
if (TINTC.size > 96) TINTC.clear();
|
||||
TINTC.set(key, out = tint(img, k, sk, mode, arc));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function PS(name) {
|
||||
const n = SK(name), img = SPR[n];
|
||||
// YOUR body only. drawChar draws both sides through this one lookup, and the man across the net
|
||||
// wears the same blue out of the same sheets — recolouring him too would mean picking the
|
||||
// opponent's kit as a side effect of picking your own.
|
||||
if (n[0] !== 'p' || n[1] !== '_') return img;
|
||||
// a tour whose art has no kit to dye (see TOURS) sits the kit pick out entirely — on her sheets
|
||||
// the only thing in the kit's hue arc is her hair
|
||||
const k = tour.kit === false ? null : kit;
|
||||
const key = (k ? k.id : '-') + '/' + skin.id;
|
||||
if (key !== tintKey) { TINTC.clear(); tintKey = key; }
|
||||
if (!img || ((k == null || k.h == null) && skin.h == null)) return img; // nothing to do: no pass
|
||||
let out = TINTC.get(n);
|
||||
if (!out) TINTC.set(n, out = tint(img, k, skin));
|
||||
return out;
|
||||
if (!img) return img;
|
||||
// YOUR body, dyed or washed depending on what this tour is dressed in (see TOURS.kit)
|
||||
if (n[0] === 'p' && n[1] === '_') return tinted(n, img, kit, skin, tour.kit, TINT.kit);
|
||||
// ...AND THE FRIEND ACROSS THE NET, wearing what THEY picked on their own phone (netplay only —
|
||||
// the roster CPU is not a person and stays exactly as drawn). His sheets are red, not blue, so
|
||||
// the kit rides its own arc; his shirt is always a dye, whatever tour the art came from.
|
||||
if (n[0] === 'o' && n[1] === '_' && G.net && G.net.look)
|
||||
return tinted(n, img, G.net.look.kit, G.net.look.skin, 'dye', TINT.oKit);
|
||||
return img;
|
||||
}
|
||||
|
||||
// THE SETUP SCREEN'S LIVE PLAYER. Not a mock-up of the pick — the pick, run through the same tint
|
||||
// and the same sheet the court will draw, so what the menu shows cannot drift from what walks on.
|
||||
// Returns false while the sprites are still loading, so the caller knows to come back.
|
||||
export function drawLook(cv2, t, k, sk) {
|
||||
const img = SPR['p_idle1' + (t.sfx || '')] || SPR.p_idle1;
|
||||
if (!img) return false;
|
||||
const out = tinted('look' + (t.sfx || ''), img, k, sk, t.kit, TINT.kit);
|
||||
const g = cv2.getContext('2d');
|
||||
g.clearRect(0, 0, cv2.width, cv2.height);
|
||||
g.imageSmoothingEnabled = false; // it is pixel art; let it be pixel art
|
||||
const s = Math.min(cv2.width / out.width, cv2.height / out.height);
|
||||
const w = Math.round(out.width * s), h = Math.round(out.height * s);
|
||||
g.drawImage(out, Math.round((cv2.width - w) / 2), cv2.height - h, w, h);
|
||||
return true;
|
||||
}
|
||||
|
||||
// BOUNCE_IDLE.dx/apex are measured off the DRAWN PALM (see config), so a tour whose player reaches
|
||||
|
||||
Reference in new issue
Block a user