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:
@@ -0,0 +1,46 @@
|
||||
// Before/after shots for the 2026-08-04 ray report: 30% shorter shafts that
|
||||
// always dissolve to nothing (docs/SUN.md). Usage:
|
||||
// node demo/shot_sun_short.mjs <outdir> <suffix>
|
||||
import { launch } from './serve.mjs';
|
||||
const outDir = process.argv[2] || '/tmp';
|
||||
const tag = process.argv[3] || 'short';
|
||||
const { page, browser } = await launch({ width: 900, height: 860 }, 2);
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' }); // still shots, no 45 s sky glide
|
||||
|
||||
const setup = async (az, el) => {
|
||||
await page.evaluate(async ([azimuth, elevation]) => {
|
||||
const c = window.__card;
|
||||
const sp = c._serverCfg.spaces.find((s) => s.id === 'f1');
|
||||
sp.openings = [
|
||||
{ id: 'wN', type: 'window', x: 0.30, y: 0.14, angle: 0, length: 0.08 },
|
||||
{ id: 'wE', type: 'window', x: 0.96, y: 0.60, angle: 90, length: 0.08 },
|
||||
{ id: 'wS', type: 'window', x: 0.30, y: 0.86, angle: 0, length: 0.08 },
|
||||
{ id: 'wW', type: 'window', x: 0.04, y: 0.30, angle: 90, length: 0.08 },
|
||||
{ id: 'wW2', type: 'window', x: 0.04, y: 0.72, angle: 90, length: 0.08 },
|
||||
];
|
||||
sp.settings = { ...(sp.settings || {}), show_borders: true, fill_mode: 'glow' };
|
||||
c._serverCfg.settings = { ...(c._serverCfg.settings || {}),
|
||||
north_deg: 0, sun_rays: true, bg_mode: 'daynight' };
|
||||
c._cfgEpoch = (c._cfgEpoch || 0) + 1;
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states,
|
||||
'light.ceiling': { ...c.hass.states['light.ceiling'], state: 'off' },
|
||||
'sun.sun': { entity_id: 'sun.sun', state: 'above_horizon',
|
||||
attributes: { azimuth, elevation } } } };
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
}, [az, el]);
|
||||
await page.waitForTimeout(350);
|
||||
};
|
||||
|
||||
// low sun in the west — the long shafts, the case that hit the far walls
|
||||
await setup(265, 5);
|
||||
let stage = await page.evaluateHandle(() => window.__card.shadowRoot.querySelector('.stage'));
|
||||
await stage.asElement().screenshot({ path: `${outDir}/sun_rays_${tag}_low.png` });
|
||||
|
||||
// high southern sun — the short noon pools
|
||||
await setup(180, 55);
|
||||
stage = await page.evaluateHandle(() => window.__card.shadowRoot.querySelector('.stage'));
|
||||
await stage.asElement().screenshot({ path: `${outDir}/sun_rays_${tag}_high.png` });
|
||||
|
||||
await browser.close();
|
||||
console.log('shots written to ' + outDir);
|
||||
@@ -0,0 +1,76 @@
|
||||
// Owner 2026-08-04: «лучи от солнца сделать короче на 30%, проверить, чтобы они
|
||||
// всегда плавно рассеивались (сейчас есть ощущение, что они упираются во что-то
|
||||
// невидимое)». The wedge must be 70 % of the old reach AND must never show a
|
||||
// straight kerb: the gradient dies before the far edge, the shaft is feathered
|
||||
// and only then clipped by the room (docs/SUN.md).
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 900, height: 900 }, 1);
|
||||
|
||||
const res = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const cfg = c._serverCfg;
|
||||
const sp = cfg.spaces.find((s) => s.id === 'f1');
|
||||
sp.openings = [
|
||||
{ id: 'wW', type: 'window', x: 0.04, y: 0.30, angle: 90, length: 0.08 },
|
||||
{ id: 'wS', type: 'window', x: 0.30, y: 0.86, angle: 0, length: 0.08 },
|
||||
];
|
||||
cfg.settings = { ...(cfg.settings || {}), north_deg: 0, bg_mode: 'static', sun_rays: true };
|
||||
c._cfgEpoch++;
|
||||
const setSun = async (az, el) => {
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states, 'sun.sun': {
|
||||
entity_id: 'sun.sun', state: 'above_horizon', attributes: { azimuth: az, elevation: el } } } };
|
||||
c.requestUpdate(); await c.updateComplete;
|
||||
};
|
||||
const grads = () => [...sr().querySelectorAll('linearGradient[id^=hp-sun-]')];
|
||||
const stopsOf = (g) => [...g.querySelectorAll('stop')].map((s) => [
|
||||
parseFloat(s.getAttribute('offset')), Number(s.getAttribute('stop-opacity'))]);
|
||||
|
||||
// ---- 1) 30 % shorter: the wedge reach in window lengths ----------------
|
||||
await setSun(270, 5); // low western sun into the west window
|
||||
const oldK = (e) => 0.8 + 1.7 * Math.pow(1 - e / 90, 1.6);
|
||||
const winLen = 0.08 * 1000;
|
||||
const low = c._sunRaysCache.rays[0].len;
|
||||
out.lowSunIs70Percent = Math.abs(low - oldK(5) * 0.7 * winLen) < 1e-6;
|
||||
await setSun(180, 60);
|
||||
const high = c._sunRaysCache.rays[0].len;
|
||||
out.highSunIs70Percent = Math.abs(high - oldK(60) * 0.7 * winLen) < 1e-6;
|
||||
out.lowStillReachesFurther = low > high;
|
||||
|
||||
// ---- 2) the shaft always dissolves BEFORE its own far edge -------------
|
||||
await setSun(270, 5);
|
||||
const gs = grads();
|
||||
out.gradientsDrawn = gs.length > 0;
|
||||
out.gradientSpansWholeWedge = gs.every((g) => {
|
||||
const st = stopsOf(g);
|
||||
// the gradient axis is the FULL wedge length (x1,y1 → x2,y2 = len away)
|
||||
const dx = +g.getAttribute('x2') - +g.getAttribute('x1');
|
||||
const dy = +g.getAttribute('y2') - +g.getAttribute('y1');
|
||||
return Math.abs(Math.hypot(dx, dy) - c._sunRaysCache.rays[0].len) < 1e-6 && st.length > 2;
|
||||
});
|
||||
out.deadWellBeforeTheEnd = gs.every((g) => {
|
||||
const st = stopsOf(g);
|
||||
const firstZero = st.find(([, a]) => a === 0);
|
||||
return !!firstZero && firstZero[0] <= 85.001; // %
|
||||
});
|
||||
out.lastStopIsZero = gs.every((g) => stopsOf(g).slice(-1)[0][1] === 0);
|
||||
out.brightAtTheGlass = gs.every((g) => stopsOf(g)[0][1] > 0.2);
|
||||
out.neverBrightensInward = gs.every((g) => {
|
||||
const st = stopsOf(g);
|
||||
return st.every(([, a], i) => i === 0 || a <= st[i - 1][1] + 1e-9);
|
||||
});
|
||||
|
||||
// ---- 3) feathered edges, and still no light through a wall -------------
|
||||
const wraps = [...sr().querySelectorAll('.sunlayer > g')];
|
||||
out.everyWedgeFeathered = wraps.length > 0
|
||||
&& wraps.every((g) => /hp-sunsoft-/.test(g.getAttribute('filter') || ''));
|
||||
out.everyWedgeClippedToItsRoom = wraps.every((g) => /hp-sunclip-/.test(g.getAttribute('clip-path') || ''));
|
||||
out.blurIsProportional = [...sr().querySelectorAll('filter[id^=hp-sunsoft-] feGaussianBlur')]
|
||||
.every((b) => { const s = Number(b.getAttribute('stdDeviation')); return s >= 3 && s <= 18; });
|
||||
// the clip really is the room outline, so a wall still stops the light
|
||||
out.clipIsTheRoomOutline = [...sr().querySelectorAll('clipPath[id^=hp-sunclip-] polygon')]
|
||||
.every((p) => (p.getAttribute('points') || '').split(' ').length >= 3);
|
||||
return out;
|
||||
});
|
||||
await finish(browser, checkAll(res));
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user