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
+10
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user