mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
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:
+39
-4
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user