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>
This commit is contained in:
1 parent
f32cac6a70
commit
f3dcf0b16f
5 files changed
+45
-21
No files matched your search
@@ -417,24 +417,29 @@ export function render() {
|
||||
const a = proj(P.x, 1.4, P.z), b = proj(tx, 0.35, tz);
|
||||
const dx = b.x - a.x, dy = b.y - a.y, len = Math.hypot(dx, dy) || 1;
|
||||
const ux = dx / len, uy = dy / len, px = -uy, py = ux; // unit along the lane, and its normal
|
||||
// chevrons pointing at the target: they multiply and grow as the draw gets stronger, so
|
||||
// the trail reads as pressure building up rather than as a static line
|
||||
const n = Math.round(3 + ft.f * 5), size = 2 + ft.f * 2.5;
|
||||
// (proj is a true projective map, so a straight world line stays straight on screen — one
|
||||
// screen direction is correct for every chevron along it, only the SIZE has to change)
|
||||
const chevron = (c, size, alpha) => {
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c.x + ux * size, c.y + uy * size);
|
||||
ctx.lineTo(c.x - ux * size * 0.4 + px * size, c.y - uy * size * 0.4 + py * size);
|
||||
ctx.lineTo(c.x - ux * size * 0.4 - px * size, c.y - uy * size * 0.4 - py * size);
|
||||
ctx.fill();
|
||||
};
|
||||
// Chevrons stepped along the lane in WORLD space, each sized by its own projection scale,
|
||||
// so they shrink into the distance like everything else on the court. They also fade IN
|
||||
// toward the target: faint at your racket, solid on the body, which walks the eye up the
|
||||
// lane instead of leaving a uniform stripe. More of them, and fatter, the harder you draw.
|
||||
const n = Math.round(3 + ft.f * 5);
|
||||
ctx.fillStyle = col;
|
||||
for (let i = 1; i <= n; i++) {
|
||||
const d = len * (i / (n + 1)), cx = a.x + ux * d, cy = a.y + uy * d;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx + ux * size, cy + uy * size);
|
||||
ctx.lineTo(cx - ux * size * 0.4 + px * size, cy - uy * size * 0.4 + py * size);
|
||||
ctx.lineTo(cx - ux * size * 0.4 - px * size, cy - uy * size * 0.4 - py * size);
|
||||
ctx.fill();
|
||||
const u = i / (n + 1);
|
||||
const c = proj(P.x + (tx - P.x) * u, 1.4 + (0.35 - 1.4) * u, P.z + (tz - P.z) * u);
|
||||
chevron(c, Math.max(1.5, c.s * (0.07 + ft.f * 0.09)), 0.3 + 0.7 * u);
|
||||
}
|
||||
const head = 3 + ft.f * 3.5; // the arrowhead lands ON the target, bigger the harder you pull
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(b.x + ux * head, b.y + uy * head);
|
||||
ctx.lineTo(b.x - ux * head + px * head * 0.8, b.y - uy * head + py * head * 0.8);
|
||||
ctx.lineTo(b.x - ux * head - px * head * 0.8, b.y - uy * head - py * head * 0.8);
|
||||
ctx.fill();
|
||||
chevron(b, Math.max(2.5, b.s * (0.10 + ft.f * 0.11)), 1); // the arrowhead lands ON the target
|
||||
ctx.globalAlpha = 1;
|
||||
drawRing(tx, tz, col);
|
||||
}
|
||||
// the draw meter: it fills as you pull and the notch marks the kill line. Anchored on the
|
||||
|
||||
Reference in new issue
Block a user