mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 23:19:14 +00:00
Sun rays: brighter, and a hard 3 degree threshold with a 2 s fade
Owner 2026-08-03: «лучи поярче, иногда плохо видны. Убрать плавное затухание — появляться и исчезать анимацией в 2 секунды при переходе через 3 градуса над горизонтом». RAY_MAX_ALPHA 0.18 -> 0.30: checked against both hard cases, a daylight sun on white paper and a low sun over the dark glow canvas (demo/shot_sun_bright.mjs writes the pair). The gradual ramp-in over the first ~2 degrees is gone. rayAlpha() is now a threshold: 0 below RAY_ELEVATION_MIN (3), rayPeakAlpha(cloud) at or above it — cloud cover stays the only multiplier. Crossing it animates the LAYER, never the geometry: <g class='sunlayer'> fades in/out over exactly RAY_FADE_MS = 2 s (hp-sunfade-in / hp-sunfade-out), and the card keeps the group mounted with .out for those two seconds so the dissolve can play at all. prefers-reduced-motion skips it. Every other reason to drop the wedges — editor, feature off, night, rain — stays instant. Units: rayAlpha rewritten (ramp tests dropped), raysVisible/rayPeakAlpha/ RAY_MAX_ALPHA covered. Smoke: smoke_sun gains a threshold section (8 of its checks fail on the previous build). docs/SUN.md + TESTING.md updated.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// Brightness check for the wedges (owner 2026-08-03: «поярче, иногда плохо
|
||||
// видны»). Two hardest cases side by side: a DAYLIGHT sun on white paper (the
|
||||
// wedge colour is nearly neutral there) and a low sun over the dark glow
|
||||
// canvas. Usage: node demo/shot_sun_bright.mjs <outdir> [suffix]
|
||||
import { launch } from './serve.mjs';
|
||||
const outDir = process.argv[2] || '/tmp';
|
||||
const tag = process.argv[3] ? '_' + process.argv[3] : '';
|
||||
const { page, browser } = await launch({ width: 900, height: 860 }, 2);
|
||||
// the fade is a 2 s animation; reduced motion pins the layer at full opacity
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
|
||||
const setup = (fill, azimuth, elevation, ceiling) => page.evaluate(async ([fill, azimuth, elevation, ceiling]) => {
|
||||
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: fill };
|
||||
c._serverCfg.settings = { ...(c._serverCfg.settings || {}),
|
||||
north_deg: 0, sun_rays: true, bg_mode: 'daynight' };
|
||||
c._cfgEpoch++;
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states,
|
||||
'light.ceiling': { ...c.hass.states['light.ceiling'], state: ceiling },
|
||||
'sun.sun': { entity_id: 'sun.sun', state: 'above_horizon', attributes: { azimuth, elevation } } } };
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
}, [fill, azimuth, elevation, ceiling]);
|
||||
|
||||
const shoot = async (name) => {
|
||||
await page.waitForTimeout(400);
|
||||
const stage = await page.evaluateHandle(() => window.__card.shadowRoot.querySelector('.stage'));
|
||||
await stage.asElement().screenshot({ path: `${outDir}/${name}${tag}.png` });
|
||||
};
|
||||
|
||||
// 1) plain daylight on white paper: neutral wedge colour, brightest canvas
|
||||
await setup('none', 265, 24, 'off');
|
||||
await shoot('sun_bright_paper');
|
||||
// 2) a low warm sun over the dark glow canvas
|
||||
await setup('glow', 265, 4, 'off');
|
||||
await shoot('sun_bright_glow');
|
||||
await browser.close();
|
||||
console.log('shots written to ' + outDir);
|
||||
+55
-2
@@ -133,10 +133,10 @@ const res = await page.evaluate(async () => {
|
||||
await setWeather('cloudy');
|
||||
const stopCloudy = Number(sr().querySelector('.sunlayer ~ * stop, defs stop')?.getAttribute('stop-opacity')
|
||||
|| sr().querySelector('stop')?.getAttribute('stop-opacity'));
|
||||
out.cloudyFades = domPolys().length > 0 && Math.abs(stopCloudy - 0.18 * 0.4) < 1e-6;
|
||||
out.cloudyFades = domPolys().length > 0 && Math.abs(stopCloudy - 0.3 * 0.4) < 1e-6;
|
||||
await setWeather('sunny');
|
||||
const stopClear = Number(sr().querySelector('stop')?.getAttribute('stop-opacity'));
|
||||
out.clearFullAlpha = Math.abs(stopClear - 0.18) < 1e-6;
|
||||
out.clearFullAlpha = Math.abs(stopClear - 0.3) < 1e-6;
|
||||
delete cfg().settings.weather_entity;
|
||||
touchCfg();
|
||||
|
||||
@@ -290,4 +290,57 @@ const final = await rayInfo();
|
||||
check('b701_write_landed', final.rev !== base.rev, true);
|
||||
check('b701_ray_survives_ack', Math.abs(final.midY - final.wEy * 1000) < 1, true);
|
||||
|
||||
// 15) the 3° threshold + the 2 s dissolve (owner 2026-08-03). Runs LAST and in
|
||||
// its own evaluate: it deliberately waits out a two-second fade, and the b701
|
||||
// regression above measures a debounced config write against wall-clock time.
|
||||
const thr = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const upd = async () => { c.requestUpdate(); await c.updateComplete; };
|
||||
const setSun = async (azimuth, elevation) => {
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states, 'sun.sun': {
|
||||
entity_id: 'sun.sun', state: elevation > 0 ? 'above_horizon' : 'below_horizon',
|
||||
attributes: { azimuth, elevation },
|
||||
} } };
|
||||
await upd();
|
||||
};
|
||||
const domPolys = () => [...sr().querySelectorAll('.sunlayer polygon')];
|
||||
const layer = () => sr().querySelector('.sunlayer');
|
||||
const stopAlpha = () => Number(sr().querySelector('stop')?.getAttribute('stop-opacity'));
|
||||
|
||||
// the old build ramped the alpha in over the first ~2°; now the layer is
|
||||
// either absent or at FULL strength, and crossing 3° fades the whole LAYER
|
||||
// (never the geometry) in or out over exactly two seconds
|
||||
await setSun(90, 10);
|
||||
out.aboveThresholdDrawn = domPolys().length > 0 && !layer().classList.contains('out');
|
||||
{
|
||||
const cs = getComputedStyle(layer());
|
||||
out.fadeInTakesTwoSeconds = cs.animationName === 'hp-sunfade-in' && cs.animationDuration === '2s';
|
||||
}
|
||||
out.fullAlphaWellAboveThreshold = Math.abs(stopAlpha() - 0.3) < 1e-6;
|
||||
await setSun(90, 3.2);
|
||||
out.fullAlphaJustAboveThreshold = Math.abs(stopAlpha() - 0.3) < 1e-6;
|
||||
|
||||
// below 3°: the layer stays for exactly the dissolve, then leaves the DOM
|
||||
await setSun(90, 2);
|
||||
{
|
||||
const dying = layer();
|
||||
const cs = dying && getComputedStyle(dying);
|
||||
out.belowThresholdDissolves = !!dying && dying.classList.contains('out')
|
||||
&& cs.animationName === 'hp-sunfade-out' && cs.animationDuration === '2s';
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 2300));
|
||||
await upd();
|
||||
out.belowThresholdGoneAfterFade = !layer() && domPolys().length === 0;
|
||||
|
||||
// a sun that never crossed the threshold draws nothing at all — no ghost
|
||||
await setSun(90, 1);
|
||||
out.coldStartBelowThresholdEmpty = domPolys().length === 0 && !layer();
|
||||
await setSun(90, 5);
|
||||
out.backAboveThreshold = domPolys().length > 0 && !layer().classList.contains('out');
|
||||
return out;
|
||||
});
|
||||
Object.assign(res, thr);
|
||||
|
||||
await finish(browser, checkAll(res));
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user