Make the plane solid, and take the posed scene back out

The cone is fully opaque now: solid colour, deep blue at the edges and near-white
down the core, dimming as it runs away. Depth is carried by BRIGHTNESS instead of
by alpha, so it still reads as receding without anything showing through.

That settles an argument the translucent version could not win. Its opacity and
how well you could tell above-the-plane from below-the-plane were the same dial
pulling opposite ways: readable enough to show the split and it veiled the whole
cone, subtle enough to look right and there was nothing to see. Solid, a body
above the plane is drawn on top of it and a body below is hidden behind it. The
trade is that you cannot see the crowd through it, which is the thing to judge on
the phone.

The `alpha` parameter went with it rather than being left in the signature lying
about what it does.

laserScene is gone. T.pause(true) stays -- it freezes the world but keeps drawing,
which is worth having whenever a still is what you need to look at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-03 17:21:21 +02:00
1 parent 44419d9ce1
commit 601aa934ad
4 files changed
+43 -81

No files matched your search

+29 -24
View File
@@ -589,16 +589,16 @@ function drawShotBand(a, u0, flying) {
ctx.globalAlpha = 1; ctx.lineWidth = 1;
}
// The laser's plane on the ground, shared by the aim PREVIEW and the fired shot. `alpha` dims the
// whole thing (the preview is faint, the shot is not) and `from` is the erase front: 0 draws the
// whole wedge, 0.5 draws only its far half, 1 draws nothing. Drawn under the bodies, so the ones
// standing in it are lit from beneath instead of being covered by it.
// The laser's plane: ONE flat translucent triangle, apex at the strings, opening out down the court
// and fading with distance. It was four nested wedges composited `lighter` — and that layering WAS
// the banding, additive light blowing out to white where the layers overlapped. Flat alpha also
// means a body standing under it is TINTED by it rather than brightened, which is what makes the
// split below read. Returns the length it drew, so the split can use the same geometry.
function drawBeamWedge(dirX, dirZ, ox, oy, oz, alpha, from) {
// The laser's plane, shared by the aim PREVIEW and the fired shot. ONE triangle, apex at the
// player's chest, opening out down the court. It is FULLY OPAQUE — solid colour, deep blue at the
// edges and near-white down the core, dimming with distance. Depth is carried by BRIGHTNESS, not by
// transparency, so nothing shows through it: a body above the plane is drawn on top of it and a body
// below is simply hidden behind it, which is as unambiguous as the split can be. (It was a
// translucent veil; at a readable opacity it washed everything out, and at a subtle one you could
// not tell above from below at all — the two pulled against each other. Opaque settles it.)
// `from` is the erase front: 0 draws the whole wedge, 0.5 only its far half, 1 nothing.
// Returns the length it drew, so the split can use the same geometry.
function drawBeamWedge(dirX, dirZ, ox, oy, oz, from) {
const d = Math.hypot(dirX, dirZ) || 1, ux = dirX / d, uz = dirZ / d;
const px = -uz, pz = ux; // the lane's perpendicular, on the ground
// How far it runs is solved, not tuned: walk out along the lane until the plane's own projection
@@ -618,7 +618,6 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, alpha, from) {
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);
const u0 = Math.max(0, from), w0 = wAt(u0), w1 = wAt(1);
const [r, g_, b] = RGB(WEAPONS.laser.col);
// 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
// no 2D gradient, so the wedge is filled in a handful of slices along its length, each with its
@@ -631,16 +630,18 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, alpha, from) {
const wa = wAt(ua), wb = wAt(ub), wm = wAt(um);
const a1 = corner(ua, -wa), a2 = corner(ua, wa), b2 = corner(ub, wb), b1 = corner(ub, -wb);
const el = corner(um, -wm), er = corner(um, wm); // the across axis, at this slice's width
// Lower opacity and BLUER. It was near-white down the core at almost double this alpha, which
// veiled the whole cone evenly — and once everything under it is equally washed out, the split
// stops reading: you cannot tell the part above the plane from the part below.
const k = (0.19 - 0.13 * um) * alpha; // depth: strongest at the strings
// FULLY OPAQUE. Depth is carried by BRIGHTNESS, not by alpha — the plane is solid colour, dark
// blue at the edges and near-white down the core, dimming as it runs away from you. Nothing under
// it shows through, so "above the plane" and "below the plane" stop being a matter of degree:
// the part above is simply there and the part below is simply gone behind it.
const f = 1 - 0.5 * um; // depth: full at the strings, half at the ads
const mix = (c, m) => `rgb(${Math.round(c[0] * m)},${Math.round(c[1] * m)},${Math.round(c[2] * m)})`;
const g = ctx.createLinearGradient(el.x, el.y, er.x, er.y);
g.addColorStop(0, `rgba(${r},${g_},${b},${k * 0.5})`); // edge: the weapon's blue
g.addColorStop(0.34, `rgba(${r},${g_},${b},${k})`);
g.addColorStop(0.5, `rgba(118,188,255,${k * 1.35})`); // core: a LIT blue, not white
g.addColorStop(0.66, `rgba(${r},${g_},${b},${k})`);
g.addColorStop(1, `rgba(${r},${g_},${b},${k * 0.5})`);
g.addColorStop(0, mix(BEAM_EDGE, f * 0.8)); // edge: deep blue
g.addColorStop(0.30, mix(BEAM_MID, f));
g.addColorStop(0.5, mix(BEAM_CORE, f)); // core: the hot line down the middle
g.addColorStop(0.70, mix(BEAM_MID, f));
g.addColorStop(1, mix(BEAM_EDGE, f * 0.8));
ctx.fillStyle = g;
ctx.beginPath(); ctx.moveTo(a1.x, a1.y); ctx.lineTo(a2.x, a2.y); ctx.lineTo(b2.x, b2.y); ctx.lineTo(b1.x, b1.y);
ctx.closePath(); ctx.fill();
@@ -648,14 +649,14 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, alpha, from) {
// ...and the OUTER LINES. The reference reads as a plane because its edges are drawn, not just
// implied by where the fill stops.
const c1 = corner(u0, -w0), c2 = corner(u0, w0), c3 = corner(1, w1), c4 = corner(1, -w1);
ctx.strokeStyle = `rgba(120,190,255,${0.45 * alpha})`;
ctx.strokeStyle = 'rgb(210,238,255)'; // the outer lines, solid like the rest of it
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(c2.x, c2.y); ctx.lineTo(c3.x, c3.y);
ctx.moveTo(c1.x, c1.y); ctx.lineTo(c4.x, c4.y);
ctx.stroke();
if (from > 0) { // the erase front: a bright bar racing away down the lane
ctx.strokeStyle = `rgba(255,255,255,${0.85 * alpha})`; ctx.lineWidth = 1.5;
ctx.strokeStyle = 'rgb(255,255,255)'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(c1.x, c1.y); ctx.lineTo(c2.x, c2.y); ctx.stroke();
}
ctx.lineWidth = 1;
@@ -665,6 +666,10 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, alpha, from) {
// The cut line: BLUEISH and faint. Near-white and bold, it read as a HUD marker stamped on the
// sprite rather than as light crossing a body — it has to whisper, not label.
const CUT_COL = 'rgba(58,175,255,0.85)'; // the laser icon's blue (#1d99e8), lifted so a flat bar reads
// The plane's three colours, edge -> core. Opaque: see the gradient in drawBeamWedge.
const BEAM_EDGE = [26, 62, 178];
const BEAM_MID = [63, 116, 255];
const BEAM_CORE = [206, 238, 255];
const CUT_MAX = 8; // bodies that get a cut line in one frame (nearest first)
const CUT_MIN_PX = 14; // ...and only if it is drawn at least this tall
// NO ctx.filter ANYWHERE. The cut line used to be the sprite redrawn clipped to a band and flattened
@@ -1218,10 +1223,10 @@ export function render() {
// this is light in the air. Then the bodies standing in it are re-cut on top of it.
if (G.beam) {
const b = G.beam, k = Math.max(0, b.t / TUNE.beamT); // 1 at the flash, 0 as it dies
const L = drawBeamWedge(b.ux, b.uz, b.x, b.y, b.z, 1, 1 - k);
const L = drawBeamWedge(b.ux, b.uz, b.x, b.y, b.z, 1 - k);
drawBeamCuts(b.ux, b.uz, b.x, b.z, L);
} else if (laserAim) {
const L = drawBeamWedge(laserAim.v.vx, laserAim.v.vz, P.x, TUNE.beamOriginY, P.z, 0.8, 0); // faint plane, so the preview leans on more of it
const L = drawBeamWedge(laserAim.v.vx, laserAim.v.vz, P.x, TUNE.beamOriginY, P.z, 0); // the preview is the same solid plane
drawBeamCuts(laserAim.v.vx, laserAim.v.vz, P.x, P.z, L); // "already while aiming"
}
if (band && !laserAim) drawAimMark(band, bandU0, flying); // ...now that there is a body to mark