Draw the laser like the ball's aim band, and let the two be compared

TUNE.beamBand flips between the two looks live from the T panel. 1, the new
default, draws the laser the way the BALL's aim trajectory is drawn: a path down
the lane whose width comes from each point's own projection scale, so it narrows
into the distance like everything else on the court, with a shadow on the lawn to
pin it there. 0 is the solid widening cone.

The cone was a true projection, which is why it was not obviously wrong to me --
but it GROWS in world width, so on screen it opens out while the court recedes,
and the eye reads that as a shape stuck to the screen rather than a plane lying
in the world.

The kill follows whichever look is on, in both fireBeam and drawBeamCuts.
Otherwise the comparison would be a lie: a narrow band that deletes a cone-wide
crowd tells you nothing about how the narrow band plays. Measured down one lane,
the cone takes 9 of 9 and the band 5 of 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-03 17:26:18 +02:00
1 parent 601aa934ad
commit ac1d5fbccd
5 files changed
+63 -3

No files matched your search

+10
View File
@@ -443,6 +443,16 @@ that holds that height all the way out instead of sloping to the ground — that
makes the rest possible. Sloping, it lay flat on the grass across the whole far half, so there was
nothing for it to cut exactly where the crowd stands.
- **`TUNE.beamBand` is a live A/B between two looks**, flippable from the T panel on the phone.
**1 (default) = the BAND**: drawn the way the ball's aim trajectory is drawn — a path down the lane
whose width comes from each point's OWN projection scale, so it narrows into the distance like
everything else on the court, with a shadow on the lawn to pin it there. **0 = the CONE**: the solid
widening triangle. The cone IS a true projection, but it grows in world width (0.06 m -> `beamW1`),
so on screen it opens out while the court recedes — and the eye reads that as a flat shape stuck to
the screen. Oliver: "it does not look correct perspective wise".
**The KILL follows whichever look is on** (`beamBandW * 2` vs the cone's taper) in BOTH `fireBeam`
and `drawBeamCuts`, or the comparison would be a lie — measured, the cone takes 9 of 9 bodies in a
lane and the band 5 of 9.
- **THE PLANE IS OPAQUE, and that is what settled it.** While it was translucent, its opacity and
the over/under read were the same dial pulling in opposite directions: readable enough to show the
split and it veiled the whole cone, subtle enough to look right and you could not tell above from
+7
View File
@@ -255,6 +255,13 @@ export const TUNE = {
beamW0: 0.06, // m half-width at the racket — a POINT. At 0.35 it left the racket as a 0.7 m
// bar hanging in the air rather than as something emitted from the strings.
beamW1: 5.0, // ...and at the far end. It is a CONE: the spread is the point of it
// A/B, live from the T panel on the phone. 1 = draw the laser the way the BALL's aim band is
// drawn: a path down the lane whose width comes from each point's OWN projection scale, so it
// tapers into the distance and sits in the world. 0 = the solid widening cone.
// The cone is a real projection, but it GROWS in world width (0.06 m -> beamW1), so on screen it
// opens out while everything else recedes — which is what reads as a flat sticker.
beamBand: 1,
beamBandW: 0.30, // world half-width of that band — CONSTANT, which is what makes it taper
beamT: 0.42, // s the wedge stays drawn
dmgBeam: 8, // what it takes off a BOSS. An ordinary body is not damaged by the beam, it is
// DELETED by it (see fireBeam); bosses "must suffer severely" but survive.
+1 -1
View File
@@ -179,6 +179,6 @@
</div>
</div>
</div>
<script type="module" src="main.js?v=108"></script>
<script type="module" src="main.js?v=109"></script>
</body>
</html>
+41 -1
View File
@@ -617,6 +617,7 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, from) {
const yAt = u => oy + (TUNE.beamY - oy) * Math.min(1, u / TUNE.beamRise);
const wAt = u => TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * u;
const corner = (u, w) => proj(ox + ux * (u * L) + px * w, yAt(u), oz + uz * (u * L) + pz * w);
if (TUNE.beamBand) { drawBeamBand(ux, uz, ox, oy, oz, L, from); return L; }
const u0 = Math.max(0, from), w0 = wAt(u0), w1 = wAt(1);
// TWO gradients, because the reference has both: a HORIZONTAL one across the width (near-white
// down the centre line, falling to blue at the edges) and a DEPTH one along the length. Canvas has
@@ -676,6 +677,44 @@ const CUT_MIN_PX = 14; // ...and only if it is drawn at least this tall
// to one colour by a canvas filter, which put it exactly on the silhouette — and on an iPhone the
// line did not draw AT ALL and bodies blinked in and out of existence, which is what a filter
// leaking past its restore() looks like. Not worth it at any price. Plain bars from here.
// THE BAND VARIANT (TUNE.beamBand = 1). Drawn the way the BALL's aim trajectory is drawn: a path
// down the lane whose width comes from each point's OWN projection scale, so it narrows into the
// distance exactly like everything else on the court. That is the difference — the cone is a real
// projection too, but it GROWS in world width, so on screen it opens out while the court recedes,
// and the eye reads that as a flat shape stuck to the screen rather than a plane lying in the world.
function drawBeamBand(ux, uz, ox, oy, oz, L, from) {
const N = 20;
const yAt = u => oy + (TUNE.beamY - oy) * Math.min(1, u / TUNE.beamRise);
const at = u => ({ x: ox + ux * (u * L), y: yAt(u), z: oz + uz * (u * L) });
const u0 = Math.max(0, from);
if (u0 >= 1) return;
const uAt = i => u0 + (1 - u0) * (i / N);
// the shadow on the lawn first, same as the ball's band: an arc hanging in the air is ambiguous
// about where it lies until something pins it to the ground
for (let i = 0; i <= N; i++) {
const w = at(uAt(i)), p = proj(w.x, 0, w.z);
ctx.fillStyle = `rgba(0,0,0,${0.20 * (1 - uAt(i) * 0.6)})`;
ctx.beginPath(); ctx.ellipse(p.x, p.y, Math.max(0.8, p.s * TUNE.beamBandW * 0.9),
Math.max(0.4, p.s * TUNE.beamBandW * 0.3), 0, 0, 7); ctx.fill();
}
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
// two passes: a wide soft body and a hot core down the middle, both scaled per point
for (const [wf, col] of [[2.0, BEAM_MID], [0.7, BEAM_CORE]]) {
let prev = null;
for (let i = 0; i <= N; i++) {
const u = uAt(i), w = at(u), p = proj(w.x, w.y, w.z);
if (prev) {
const f = 1 - 0.45 * u; // depth by brightness, as the cone does
ctx.strokeStyle = `rgb(${Math.round(col[0] * f)},${Math.round(col[1] * f)},${Math.round(col[2] * f)})`;
ctx.lineWidth = Math.max(1, p.s * TUNE.beamBandW * wf);
ctx.beginPath(); ctx.moveTo(prev.x, prev.y); ctx.lineTo(p.x, p.y); ctx.stroke();
}
prev = p;
}
}
ctx.lineWidth = 1;
}
// THE CUT. The beam is a plane at TUNE.beamY, so a body standing in it is sliced at the height that
// plane passes through it: everything below stays where it was drawn (and is tinted by the
// translucent beam laid over it), and everything above is redrawn on TOP of the beam, clipped.
@@ -693,7 +732,8 @@ function drawBeamCuts(dirX, dirZ, ox, oz, L) {
const along = rx * ux + rz * uz;
if (along < 0.5 || along > L) continue; // behind him, or past the end
const off = Math.abs(rx * uz - rz * ux);
const halfW = TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * (along / L);
const halfW = TUNE.beamBand ? TUNE.beamBandW * 2 // must match fireBeam's test, or the cut
: TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * (along / L); // marks bodies the shot will miss
if (off > halfW + 0.8) continue;
const gp = proj(zb.x, 0, zb.z), h = zombieH(zb);
// whole pixels: a clip edge on a fractional y crawls under nearest-neighbour scaling
+4 -1
View File
@@ -313,7 +313,10 @@ export function fireBeam(dirX, dirZ) {
const along = rx * ux + rz * uz; // how far down the lane it stands
if (along < 0 || along > TUNE.beamLen) continue; // behind you, or past the end of the beam
const off = Math.abs(rx * uz - rz * ux); // lateral offset from the lane
const halfW = TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * (along / TUNE.beamLen);
// the kill follows whichever LOOK is on, or the A/B would be a lie: a narrow band that deletes a
// cone-wide crowd tells you nothing about how the narrow band plays
const halfW = TUNE.beamBand ? TUNE.beamBandW * 2
: TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * (along / TUNE.beamLen);
if (off > halfW + hitR(zb)) continue;
// THE BEAM DELETES ORDINARY BODIES. hp does not enter into it: a plane of light that left a
// roller man standing was the thing that made it feel like a worse ball rather than a weapon.