Sunlight is 30% shorter and always dissolves into nothing

Owner, 2026-08-04: «лучи от солнца сделать короче на 30%, проверить, чтобы они
всегда плавно рассеивались (сейчас есть ощущение, что они упираются во что-то
невидимое)».

SHORTER. `rayLength` is now the v1.56 curve times RAY_LENGTH_K = 0.7 — 1.75
window lengths at sunrise, 0.56 at the zenith. Scaling the whole curve instead
of re-picking the constants keeps the shape the owner approved: a low sun still
reaches three times further than a high one.

WHAT THEY WERE BUMPING INTO. Nothing invisible — the wedge's own outline, in
three places at once.

1. The gradient runs ALONG the sun, so its iso-alpha lines are perpendicular to
   the sun, while the wedge's far edge is parallel to the WALL. The two
   coincide only for a sun hitting the glass dead-on; at any other angle one
   half of that far edge was cut while it still carried colour — a straight
   bright kerb hanging in the middle of the floor. The single `100% → alpha 0`
   stop hid this from the reader of the code and from nobody else.
2. The two SIDES of the wedge had no falloff at all: two razor lines from the
   window into the room, brightest exactly where they are most visible.
3. Where the room outline clips the wedge — the opposite wall, the inner corner
   of an L, and above all an OPEN (virtual) boundary, which has no wall drawn
   at all — the shaft was chopped at whatever alpha it still had.

WHAT IT IS NOW. The gradient still spans the FULL wedge (geometry and gradient
must describe the same shaft), but `rayStops()` eases it to a hard zero at
RAY_FADE_END = 85% of the length, so the last 15% of every wedge is guaranteed
empty and a shaft ending in mid-air has nothing left to draw an edge with. Each
wedge is then drawn inside `<g filter clip-path>`: SVG applies the filter FIRST
and the clip SECOND, so a Gaussian blur of `raySoftness(len)` (7% of the shaft,
clamped 3…18 render units) feathers the sides and the tip and the room outline
cuts that feather off. Light still never crosses a wall — but where it reaches
one, the kerb is a soft ramp that reads as light landing ON the wall.

Clipping by the room is untouched; only its visible edge changed.

Tests: unit — rayLength pinned at exactly 70% of the old curve at ten
elevations, rayStops (monotone, dead at/after 85%, bright at the glass),
raySoftness clamps. Smoke — demo/smoke_sun_soft.mjs, which fails on the
previous tip (lowSunIs70Percent, highSunIs70Percent, gradientSpansWholeWedge,
deadWellBeforeTheEnd, everyWedgeFeathered). Stills: demo/shot_sun_short.mjs.
This commit is contained in:
Matysh
2026-08-04 09:47:34 +03:00
parent 1da1aba625
commit 22b588e116
9 changed files with 342 additions and 41 deletions
+39 -4
View File
@@ -6,6 +6,7 @@ import {
rayLength, rayQuad, clipToRoom, computeSunRays,
rayAlpha, rayColor, cloudFactor, RAY_MAX_ALPHA,
raysVisible, rayPeakAlpha, RAY_ELEVATION_MIN, RAY_FADE_MS,
RAY_LENGTH_K, RAY_FADE_END, rayStops, raySoftness,
northDegOf, bgModeOf, sunRaysOn, weatherEntityOf, sunStateOf,
} from '../test-build/sun.js';
@@ -102,12 +103,46 @@ test('windowLit: above the horizon AND facing the sun', () => {
assert.ok(!windowLit(east, sunDirOnPlan(90, 0), -5)); // night
});
test('rayLength: longest at the horizon, shortest at noon, monotonic', () => {
assert.ok(near(rayLength(0), 2.5, 1e-9));
assert.ok(near(rayLength(90), 0.8, 1e-9));
test('rayLength: 30% shorter than v1.56 (owner 2026-08-04), same shape', () => {
// the old curve, kept here so the -30% stays a fact and not a memory
const before = (e) => 0.8 + 1.7 * Math.pow(1 - Math.min(90, Math.max(0, e)) / 90, 1.6);
assert.equal(RAY_LENGTH_K, 0.7);
assert.ok(near(rayLength(0), 1.75, 1e-9)); // was 2.5
assert.ok(near(rayLength(90), 0.56, 1e-9)); // was 0.8
for (const e of [-5, 0, 3, 10, 30, 45, 60, 89, 90, 120]) {
assert.ok(near(rayLength(e), before(e) * 0.7, 1e-12), 'exactly 70% at ' + e);
}
// the shape survives: a low sun still reaches much further than a high one
assert.ok(rayLength(10) > rayLength(30));
assert.ok(rayLength(30) > rayLength(60));
assert.ok(near(rayLength(-5), 2.5, 1e-9)); // clamped
assert.ok(near(rayLength(-5), 1.75, 1e-9)); // clamped
});
test('rayStops: the shaft is fully dissolved BEFORE its own far edge', () => {
const stops = rayStops();
assert.ok(near(stops[0][0], 0) && near(stops[0][1], 1), 'brightest at the glass');
assert.equal(RAY_FADE_END, 0.85);
// offsets are sorted, alphas never rise, and the tail is a hard zero
for (let i = 1; i < stops.length; i++) {
assert.ok(stops[i][0] > stops[i - 1][0] || stops[i][0] === 1, 'offsets ascend');
assert.ok(stops[i][1] <= stops[i - 1][1], 'alpha never brightens inward');
}
assert.ok(near(stops[stops.length - 1][0], 1), 'the gradient spans the FULL wedge');
for (const [off, k] of stops) {
if (off >= RAY_FADE_END) assert.equal(k, 0, 'nothing left at/after ' + RAY_FADE_END);
else assert.ok(k > 0, 'still lit at ' + off);
}
// half gone well before the middle — the eye must not find a straight edge
const half = stops.find(([, k]) => k <= 0.5);
assert.ok(half[0] <= 0.65, 'past half-dark by two thirds of the way');
});
test('raySoftness: a feather proportional to the shaft, clamped both ends', () => {
assert.equal(raySoftness(0), 3); // a stub of a wedge still gets a kerb
assert.ok(near(raySoftness(100), 7, 1e-9));
assert.ok(near(raySoftness(200), 14, 1e-9));
assert.equal(raySoftness(1e6), 18); // never a smear across the plan
assert.ok(raySoftness(200) > raySoftness(100));
});
test('rayQuad + clipToRoom: the wedge is cut by the room outline', () => {