The aim is a mark on the grass, not a line drawn over the game
The live flick readout was a ring on a ray from the player's feet — a HUD element laid across the court, and read as one: "pretty bare bones... the line must go away, it is distracting". It is a soft cloud on the ground now, in the register the dust and the blood already use: it sits where the ball is being asked to land, grows with the length of the swipe, and warms from pale lime through gold to red as that ask approaches the width no timing can keep in. The threshold stopped being a flip, so too hard is something you watch coming. Three things it took to make it read, kept in the comments so the next pass does not re-learn them: it is sized in METRES off the projection, or it is an eight-pixel smudge at the far corner where it actually lives; it is a stain and not an additive glow, which turned the red end yellow over green grass; and the ramp starts pale rather than green, because green on green showed nothing at all across the whole safe half of the range. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d5a069a90c
commit
1b16b422df
3 files changed
+49
-20
No files matched your search
+17
-4
@@ -524,10 +524,23 @@ depth only and holding your own artillery lane is the point.
|
||||
**THE AIM IS DRAWN WHILE THE THUMB IS STILL MOVING.** Oliver asked to *"visualize the strength of my
|
||||
flick at flick-time"*, and he was asking for the half the placement ring cannot give: the ring is a
|
||||
RECEIPT, it arrives with the ball already gone. `G.swipe` (set by input.js during the drag, cleared
|
||||
on release) draws the same target ring live, plus a RAY from the player's feet — the ring alone sits
|
||||
deep in perspective and 300 ms is not long enough to go looking for it. Cream while the ask is safe,
|
||||
`UI.hot` once the sway crosses that derived threshold: the colour is the answer to "was I too hard".
|
||||
Only inside the window where a release would still BE a flick (`FLICK_MS`/`TUNE.flickMin`, the same
|
||||
on release) marks where the ball is being ASKED to land.
|
||||
|
||||
**It is a cloud, not a diagram.** The first cut was a ring on a ray from the player's feet and it
|
||||
was rejected on sight (*"pretty bare bones... the line from the player to the indicator must go
|
||||
away, it is distracting"*): a line drawn across the court is a HUD element laid over the game.
|
||||
What is there now is a soft stain on the ground in the same procedural register as the dust and the
|
||||
blood — it GROWS with the length of the swipe and it WARMS as that ask approaches the width no
|
||||
timing can keep in. Three things were learned making it read, all of them the hard way:
|
||||
* **metres, not pixels** — sized off `p.s` so perspective shrinks it with distance. A pixel radius
|
||||
looked fine at the baseline and was an 8-px smudge at the far corner, which is where it lives.
|
||||
* **a stain, not a glow** — `globalCompositeOperation = 'lighter'` turned the red end YELLOW over
|
||||
green grass, and the hue is the entire message.
|
||||
* **the ramp is pale lime -> gold -> red, not green -> red** — green on green is invisible, so the
|
||||
obvious ramp showed nothing across the whole safe half of its range. The cool end carries its
|
||||
contrast in LIGHTNESS, which is what makes it read on grass, clay and slab alike.
|
||||
The threshold is not a flip: the colour is already gold before you reach it, so "too hard" is
|
||||
something you watch coming. Only inside the window where a release would still BE a flick (`FLICK_MS`/`TUNE.flickMin`, the same
|
||||
gate `endTouch` uses) — past that the gesture is a run and a target hanging off it would be a lie.
|
||||
Rally modes only (`rallyFeel`); gore's flick free-aims at a body, not at a lane. The placement ring
|
||||
turns `UI.bad` when the shot really is out, so the two rings together ARE the lesson: what you asked
|
||||
|
||||
+1
-1
@@ -659,6 +659,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=282"></script>
|
||||
<script type="module" src="src/main.js?v=287"></script>
|
||||
</body>
|
||||
</html>
|
||||
+31
-15
@@ -16,6 +16,14 @@ const hexLerp = (a, b, t) => {
|
||||
const [ar, ag, ab] = c(a), [br, bg, bb] = c(b);
|
||||
return `rgb(${Math.round(ar + (br - ar) * t)},${Math.round(ag + (bg - ag) * t)},${Math.round(ab + (bb - ab) * t)})`;
|
||||
};
|
||||
// The live aim cloud's ramp, walked in TWO steps: pale lime -> gold -> red. A straight green-to-red
|
||||
// lerp was the obvious version and it failed on the only court that matters — green on green is
|
||||
// invisible, so the whole safe half of the range showed nothing. The cool end carries its contrast
|
||||
// in LIGHTNESS instead of hue, which is what makes it read on grass, clay and slab alike, and gold
|
||||
// in the middle keeps every step of the ramp distinct from the two beside it.
|
||||
const AIM_COOL = '#d8eaae', AIM_MID = '#e0cc72', AIM_HOT = '#d64a3a';
|
||||
const aimHeat = t => t < 0.5 ? hexLerp(AIM_COOL, AIM_MID, t * 2) : hexLerp(AIM_MID, AIM_HOT, (t - 0.5) * 2);
|
||||
|
||||
// how far into the rot we are, 0 (tennis) .. 1 (splatter) — duels lerp it back home
|
||||
function paletteT() {
|
||||
return G.mode === 'tennis' ? 0 : G.mode === 'splatter' ? 1 - G.duelLerp : Math.min(1, G.transT / 6);
|
||||
@@ -2290,21 +2298,29 @@ export function render() {
|
||||
const sway = swayOf(G.swipe);
|
||||
const x = sway * TUNE.flickWidth;
|
||||
const z = G.swipe.fy < -0.38 ? SHOT_DEPTH.deep : G.swipe.fy > 0.38 ? SHOT_DEPTH.short : SHOT_DEPTH.mid;
|
||||
// ...and it goes HOT exactly where the shot stops being safe. Below this much sway the worst
|
||||
// mistiming in the game cannot push the scatter past the sideline; above it, it can — so the
|
||||
// colour is the honest answer to "was I too hard?", and it is DERIVED from the same three
|
||||
// numbers doHit spends, never a hand-picked threshold that would rot the moment one moved.
|
||||
const safe = (COURT.W / 2 - TUNE.aimError) / (TUNE.flickWidth * TUNE.flickReach);
|
||||
const col = Math.abs(sway) > safe ? UI.hot : UI.cream;
|
||||
ctx.globalAlpha = 0.35 + 0.45 * Math.abs(sway); // the size of the ask is the brightness
|
||||
// the ray is what makes the ring findable: the target sits deep in perspective and is small,
|
||||
// and 300 ms is not long enough to go looking for it. It leaves from your own feet, so the
|
||||
// gesture, the man and the corner are one line — and its length IS the strength you swiped.
|
||||
const a = proj(P.x, 0, P.z), b = proj(x, 0, z);
|
||||
ctx.strokeStyle = col; ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.moveTo(Math.round(a.x), Math.round(a.y)); ctx.lineTo(Math.round(b.x), Math.round(b.y)); ctx.stroke();
|
||||
drawRing(x, z, col, 0.9);
|
||||
ctx.globalAlpha = 1;
|
||||
// A CLOUD, NOT A DIAGRAM. The first cut was a ring on a ray from the player's feet, and a line
|
||||
// drawn across the court is a HUD element laid over the game — Oliver: "pretty bare bones...
|
||||
// the line must go away, it is distracting". This is a soft mark on the grass instead, in the
|
||||
// same procedural register as the dust and the blood: it sits where the ball is being asked to
|
||||
// land, it GROWS with the length of the swipe, and it warms from green to red as that ask
|
||||
// approaches the width no timing can keep in. The threshold is not a flip — the colour is
|
||||
// already amber before you reach it, so "too hard" is something you watch coming.
|
||||
const a = Math.abs(sway), p = proj(x, 0, z);
|
||||
if (p.s > 0) {
|
||||
const safe = (COURT.W / 2 - TUNE.aimError) / (TUNE.flickWidth * TUNE.flickReach);
|
||||
const col = aimHeat(Math.min(1, a / safe));
|
||||
const rx = p.s * (0.55 + 1.35 * a), ry = rx * 0.4; // metres of ground, so perspective shrinks it with distance
|
||||
ctx.fillStyle = col;
|
||||
// NOT additive: 'lighter' turned the red end yellow over green grass, and the hue IS the
|
||||
// message here. A soft stain keeps green green and red red on every court in the table.
|
||||
// stacked rather than a gradient: three soft passes build a chunky core the way the dust and
|
||||
// the piles do, and a smooth radial ramp would be the one un-pixelated thing on the court
|
||||
for (let i = 3; i >= 1; i--) {
|
||||
ctx.globalAlpha = 0.13 + 0.10 * a;
|
||||
ctx.beginPath(); ctx.ellipse(Math.round(p.x), Math.round(p.y), rx * i / 3, ry * i / 3, 0, 0, 7); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
// landing ring: white = incoming ball (a PREDICTION, gone the moment the ball touches down —
|
||||
// the ring that lingered where the ball had already landed confused testers and is removed)
|
||||
|
||||
Reference in new issue
Block a user