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:
1 parent
601aa934ad
commit
ac1d5fbccd
5 files changed
+63
-3
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user