mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +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:
File diff suppressed because one or more lines are too long
@@ -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
Vendored
+45
-21
File diff suppressed because one or more lines are too long
+27
-3
@@ -111,9 +111,33 @@ room's polygon (`polyclip` intersection, the same dependency
|
||||
lengths: ~2.5 at sunrise/sunset tapering to ~0.8 at the zenith
|
||||
(`0.8 + 1.7·(1 − elevation/90)^1.6`). A linear gradient runs bright at
|
||||
the window and dissolves inward; the color is warm orange while
|
||||
`elevation < 10°` and neutral by day; peak opacity is modest (~0.18 —
|
||||
two overlapping wedges never exceed a readable ceiling). Near the
|
||||
horizon the opacity ramps in over the first ~2° so wedges never pop.
|
||||
`elevation < 10°` and neutral by day; peak opacity is `RAY_MAX_ALPHA`
|
||||
= 0.30 (owner 2026-08-03: «лучи поярче, иногда плохо видны» — raised
|
||||
from 0.18; two overlapping wedges still stay under a readable ceiling
|
||||
on white paper and on the dark glow canvas alike).
|
||||
|
||||
### The 3° threshold and the 2-second fade
|
||||
|
||||
Wedge opacity does NOT depend on elevation any more — the old ramp-in
|
||||
over the first ~2° is gone. The contract (owner 2026-08-03) is a hard
|
||||
threshold:
|
||||
|
||||
- `elevation < 3°` → NO rays at all;
|
||||
- `elevation ≥ 3°` → rays at full strength (`rayPeakAlpha`, cloud cover
|
||||
being the only multiplier).
|
||||
|
||||
Crossing the threshold is animated, but on the LAYER, never on the
|
||||
geometry: the `<g class="sunlayer">` fades in with `hp-sunfade-in` and
|
||||
out with `hp-sunfade-out`, both exactly 2 s (`RAY_FADE_MS` in
|
||||
`src/sun.ts` must stay in sync with `styles.ts`). To let the fade-out
|
||||
play at all, the card keeps the layer mounted with `.out` for those two
|
||||
seconds and only then drops it. `prefers-reduced-motion: reduce` skips
|
||||
the animation entirely — the rays are simply there or simply gone.
|
||||
|
||||
Everything else that removes wedges — leaving view mode, switching the
|
||||
feature off, night (`elevation ≤ 0`), rain — is instant: those are not
|
||||
threshold crossings, and a wedge lingering while you enter the editor
|
||||
would just be a bug.
|
||||
|
||||
Layer order: ABOVE room fills (and the glow layer), BELOW devices and
|
||||
labels (those live in the HTML `devlayer` anyway). Night
|
||||
|
||||
@@ -931,6 +931,15 @@ require hands on real hardware — they remain for the human pass.
|
||||
- [ ] Wedge direction follows the compass; length grows toward
|
||||
sunrise/sunset and shrinks toward noon; every wedge is clipped by its
|
||||
room polygon; night = no wedges at all
|
||||
- [ ] Brightness + the 3° threshold (2026-08-03): wedges are visibly brighter
|
||||
(peak alpha 0.30, was 0.18) yet still readable over white paper AND the
|
||||
dark glow canvas; there is NO gradual ramp near the horizon — below 3°
|
||||
no rays at all, at/above 3° full strength; crossing the threshold fades
|
||||
the whole layer in/out over exactly 2 s (CSS on `.sunlayer`, the
|
||||
geometry never moves), and `prefers-reduced-motion` makes it instant.
|
||||
Every other way of losing the wedges (editor, feature off, night, rain)
|
||||
stays instant [auto: smoke_sun «the 3° threshold» + unit rayAlpha/
|
||||
raysVisible/rayPeakAlpha; shots: demo/shot_sun_bright.mjs]
|
||||
- [ ] Weather entity (optional, global): cloudy fades the wedges, rain/snow
|
||||
removes them, a dead/unknown weather sensor changes nothing
|
||||
- [ ] Sun geometry recomputes ONLY when the sun attributes or the config
|
||||
|
||||
+42
-6
@@ -31,7 +31,7 @@ import {
|
||||
} from './resize';
|
||||
import {
|
||||
computeSunRays, dayPhase, northDegOf, bgModeOf, sunRaysOn, weatherEntityOf,
|
||||
sunStateOf, cloudFactor, rayAlpha, rayColor, type SunRay,
|
||||
sunStateOf, cloudFactor, rayPeakAlpha, raysVisible, rayColor, RAY_FADE_MS, type SunRay,
|
||||
} from './sun';
|
||||
import { ContentSigner } from './signing';
|
||||
import { mdiHomeCityOutline } from '@mdi/js';
|
||||
@@ -4655,14 +4655,36 @@ class HouseplanCard extends LitElement {
|
||||
*/
|
||||
private _renderSunRays(space: SpaceModel): TemplateResult {
|
||||
const empty = svg`` as unknown as TemplateResult;
|
||||
if (this._editing || !this._effSunRays()) return empty;
|
||||
// HARD gates — the feature is simply not on: leaving an editor, a space
|
||||
// with rays off, night, rain. Those never fade, they just are not there.
|
||||
if (this._editing || !this._effSunRays()) { this._sunFadeReset(); return empty; }
|
||||
const north = this._effNorth();
|
||||
const sun = north !== null ? sunStateOf(this.hass) : null;
|
||||
if (!sun || sun.elevation <= 0) return empty;
|
||||
if (!sun || sun.elevation <= 0) { this._sunFadeReset(); return empty; }
|
||||
const weather = weatherEntityOf(this._sunGlobal());
|
||||
const cloud = cloudFactor(weather ? this.hass?.states?.[weather]?.state : null);
|
||||
const alpha = rayAlpha(sun.elevation, cloud);
|
||||
if (alpha <= 0) return empty;
|
||||
const alpha = rayPeakAlpha(cloud);
|
||||
if (alpha <= 0) { this._sunFadeReset(); return empty; }
|
||||
// The ONE thing that fades (owner 2026-08-03): the 3° threshold. Above it
|
||||
// the layer is there at full strength, below it gone — with a 2 s CSS
|
||||
// fade either way. The keep-alive below is what lets the fade-OUT play at
|
||||
// all: without it the layer would leave the DOM in the same frame.
|
||||
if (raysVisible(sun.elevation)) {
|
||||
if (this._sunOutTimer) { clearTimeout(this._sunOutTimer); this._sunOutTimer = 0; }
|
||||
this._sunOut = false;
|
||||
this._sunShown = true;
|
||||
} else {
|
||||
if (!this._sunShown) return empty; // never lit: nothing to dissolve
|
||||
if (!this._sunOut) {
|
||||
this._sunOut = true;
|
||||
this._sunOutTimer = window.setTimeout(() => {
|
||||
this._sunOutTimer = 0;
|
||||
this._sunShown = false;
|
||||
this._sunOut = false;
|
||||
this.requestUpdate();
|
||||
}, RAY_FADE_MS);
|
||||
}
|
||||
}
|
||||
// DEV-B701-01: the geometry signal must be _cfgEpoch, not _cfgRev.
|
||||
// Every local mutation ends in _saveConfig(), which bumps the epoch
|
||||
// SYNCHRONOUSLY; _cfgRev only moves after the debounced WS write is
|
||||
@@ -4692,12 +4714,26 @@ class HouseplanCard extends LitElement {
|
||||
</linearGradient>`;
|
||||
})}
|
||||
</defs>
|
||||
<g class="sunlayer">
|
||||
<g class="sunlayer ${this._sunOut ? 'out' : ''}">
|
||||
${rays.map((r, i) => r.polys.map((p) => svg`<polygon
|
||||
points="${p.map((q) => q[0] + ',' + q[1]).join(' ')}" fill="url(#hp-sun-${i})"></polygon>`))}
|
||||
</g>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
/** Sun-ray layer keep-alive: `shown` = in the DOM, `out` = playing the
|
||||
* 2 s dissolve before it leaves (plain fields — the timer requests the
|
||||
* update, render just reads them). */
|
||||
private _sunShown = false;
|
||||
private _sunOut = false;
|
||||
private _sunOutTimer = 0;
|
||||
|
||||
/** Drop the layer at once: used by every gate that is NOT the 3° threshold. */
|
||||
private _sunFadeReset(): void {
|
||||
if (this._sunOutTimer) { clearTimeout(this._sunOutTimer); this._sunOutTimer = 0; }
|
||||
this._sunShown = false;
|
||||
this._sunOut = false;
|
||||
}
|
||||
|
||||
/** One drag/click sample on the compass dial → dialog north_deg. */
|
||||
private _compassPoint(ev: PointerEvent): void {
|
||||
const el = ev.currentTarget as SVGSVGElement;
|
||||
|
||||
@@ -1111,6 +1111,30 @@ export const cardStyles = css`
|
||||
sense rings, the vacuum puck's 2.2s period, and a moderate opacity so
|
||||
two curtains travelling at once never turn into a strobe. The plate
|
||||
itself stays neutral — yellow means «включено», nothing else. */
|
||||
/* Sun wedges (docs/SUN.md). The layer is present ONLY above the 3°
|
||||
threshold; crossing it fades the whole layer in or out over EXACTLY
|
||||
2 s (owner 2026-08-03 — RAY_FADE_MS in src/sun.ts must match). The
|
||||
geometry is untouched: this is a plain opacity animation on the group,
|
||||
so overlapping wedges keep their own blending while it plays. */
|
||||
.sunlayer {
|
||||
animation: hp-sunfade-in 2s linear both;
|
||||
}
|
||||
.sunlayer.out {
|
||||
animation: hp-sunfade-out 2s linear both;
|
||||
}
|
||||
@keyframes hp-sunfade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes hp-sunfade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* no fade at all: the rays are simply there or simply gone */
|
||||
.sunlayer, .sunlayer.out { animation: none; }
|
||||
.sunlayer.out { opacity: 0; }
|
||||
}
|
||||
.dev.covermove::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
+33
-5
@@ -220,13 +220,41 @@ export function computeSunRays(
|
||||
|
||||
// ---------------- wedge dressing ----------------
|
||||
|
||||
/** Peak wedge opacity; two overlapping wedges stay readable (docs/SUN.md). */
|
||||
export const RAY_MAX_ALPHA = 0.18;
|
||||
/**
|
||||
* Peak wedge opacity (owner 2026-08-03: «лучи поярче, иногда плохо видны» —
|
||||
* raised from 0.18). Two overlapping wedges still stay under a readable
|
||||
* ceiling on white paper AND on the dark glow canvas (docs/SUN.md).
|
||||
*/
|
||||
export const RAY_MAX_ALPHA = 0.3;
|
||||
|
||||
/** Wedge opacity: ramps in over the first ~2° so sunrise never pops. */
|
||||
/**
|
||||
* The elevation threshold, degrees. Below it there are NO rays at all, above
|
||||
* it they are at FULL strength — the owner's 2026-08-03 contract replacing
|
||||
* the old gradual ramp-in. The switch itself is not instant: the card fades
|
||||
* the whole layer in/out over RAY_FADE_MS (CSS, not geometry).
|
||||
*/
|
||||
export const RAY_ELEVATION_MIN = 3;
|
||||
|
||||
/** Duration of that fade, ms — «ровно 2 секунды» (mirrored in styles.ts). */
|
||||
export const RAY_FADE_MS = 2000;
|
||||
|
||||
/** Are the wedges present at this elevation at all? (The hard 3° threshold.) */
|
||||
export function raysVisible(elevation: number): boolean {
|
||||
return Number(elevation) >= RAY_ELEVATION_MIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-strength wedge opacity for the current cloud cover — elevation plays
|
||||
* no part. The card feeds this into the gradient and lets CSS fade the layer,
|
||||
* so a wedge dissolving at the threshold keeps its colour while it goes.
|
||||
*/
|
||||
export function rayPeakAlpha(cloud = 1): number {
|
||||
return RAY_MAX_ALPHA * clamp01(cloud);
|
||||
}
|
||||
|
||||
/** Wedge opacity: nothing below the threshold, full strength above it. */
|
||||
export function rayAlpha(elevation: number, cloud = 1): number {
|
||||
if (!(elevation > 0)) return 0;
|
||||
return RAY_MAX_ALPHA * Math.min(1, elevation / 2) * clamp01(cloud);
|
||||
return raysVisible(elevation) ? rayPeakAlpha(cloud) : 0;
|
||||
}
|
||||
|
||||
/** Wedge color: warm orange at the horizon → neutral daylight. */
|
||||
|
||||
+28
-3
@@ -5,6 +5,7 @@ import {
|
||||
isExteriorWall, windowWallInfo, windowLit,
|
||||
rayLength, rayQuad, clipToRoom, computeSunRays,
|
||||
rayAlpha, rayColor, cloudFactor, RAY_MAX_ALPHA,
|
||||
raysVisible, rayPeakAlpha, RAY_ELEVATION_MIN, RAY_FADE_MS,
|
||||
northDegOf, bgModeOf, sunRaysOn, weatherEntityOf, sunStateOf,
|
||||
} from '../test-build/sun.js';
|
||||
|
||||
@@ -163,15 +164,39 @@ test('computeSunRays: rotating the compass swings the light to another window',
|
||||
}
|
||||
});
|
||||
|
||||
test('rayAlpha: capped, ramps in near the horizon, scaled by clouds', () => {
|
||||
assert.equal(rayAlpha(0), 0);
|
||||
test('rayAlpha: nothing below 3°, full strength above (owner 2026-08-03)', () => {
|
||||
// the old gradual ramp-in is gone: it is a threshold, not a fade
|
||||
assert.equal(rayAlpha(-3), 0);
|
||||
assert.ok(near(rayAlpha(1), RAY_MAX_ALPHA / 2));
|
||||
assert.equal(rayAlpha(0), 0);
|
||||
assert.equal(rayAlpha(1), 0);
|
||||
assert.equal(rayAlpha(2.99), 0);
|
||||
assert.ok(near(rayAlpha(3), RAY_MAX_ALPHA)); // exactly at the threshold: on
|
||||
assert.ok(near(rayAlpha(3.1), RAY_MAX_ALPHA));
|
||||
assert.ok(near(rayAlpha(30), RAY_MAX_ALPHA));
|
||||
assert.ok(near(rayAlpha(89), RAY_MAX_ALPHA)); // no elevation shaping at all
|
||||
// clouds still scale it, rain still kills it
|
||||
assert.ok(near(rayAlpha(30, 0.25), RAY_MAX_ALPHA * 0.25));
|
||||
assert.equal(rayAlpha(30, 0), 0);
|
||||
});
|
||||
|
||||
test('raysVisible / rayPeakAlpha: the threshold and the cloud-only ceiling', () => {
|
||||
assert.equal(RAY_ELEVATION_MIN, 3);
|
||||
assert.equal(RAY_FADE_MS, 2000); // «ровно 2 секунды», mirrored in styles.ts
|
||||
assert.equal(raysVisible(2.9), false);
|
||||
assert.equal(raysVisible(3), true);
|
||||
assert.equal(raysVisible(45), true);
|
||||
assert.equal(raysVisible(-10), false);
|
||||
// the peak is what the gradient uses while the layer fades — cloud only
|
||||
assert.ok(near(rayPeakAlpha(), RAY_MAX_ALPHA));
|
||||
assert.ok(near(rayPeakAlpha(1), RAY_MAX_ALPHA));
|
||||
assert.ok(near(rayPeakAlpha(0.4), RAY_MAX_ALPHA * 0.4));
|
||||
assert.equal(rayPeakAlpha(0), 0);
|
||||
});
|
||||
|
||||
test('RAY_MAX_ALPHA is the brighter 0.3 ceiling (owner 2026-08-03)', () => {
|
||||
assert.equal(RAY_MAX_ALPHA, 0.3);
|
||||
});
|
||||
|
||||
test('rayColor: warm at the horizon, neutral by day', () => {
|
||||
assert.equal(rayColor(1), '#ff9a45');
|
||||
assert.equal(rayColor(0), '#ffe9c2');
|
||||
|
||||
Reference in New Issue
Block a user