diff --git a/docs/engine/render.md b/docs/engine/render.md index bd3990d..8663c6c 100644 --- a/docs/engine/render.md +++ b/docs/engine/render.md @@ -118,6 +118,13 @@ игнорируются. Плавный лерп ambient (`setAmbient(color, fadeSec)`) — база для day/night: день/ночь — кейфреймы поверх этого API. +Для непрерывного цикла суток в `lightSim` есть чистая кривая: `dayNightFactor(tHours, +spec?)` — фактор ночи 0..1 по часам (mod 24), линейный на рассвете/закате; границы +задаёт `DayNightSpec` (`DEFAULT_DAY_NIGHT`: рассвет 06–08, закат 18–20). Игра +смешивает дневной и ночной ambient области: `lerpAmbient(base, night, factor)` и +ставит цель через `setAmbient(target, 0)` каждый тик — переход сам растянут кривой +на десятки секунд, отдельный фейд не нужен. + ### Импульсы, виньетка, тёмные пятна Поверх базового API у `Lighting` есть три короткоживущих/локальных эффекта — diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 5f810dc..ce8967d 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -149,9 +149,12 @@ dimColor, pulseEnvelope, pulseTotal, + dayNightFactor, + DEFAULT_DAY_NIGHT, type LightDef, type LightFrame, - type PulseSpec + type PulseSpec, + type DayNightSpec } from './render/lightSim'; export { stepParticle, diff --git a/packages/engine/src/render/__tests__/lightSim.test.ts b/packages/engine/src/render/__tests__/lightSim.test.ts index da21807..3d73a5c 100644 --- a/packages/engine/src/render/__tests__/lightSim.test.ts +++ b/packages/engine/src/render/__tests__/lightSim.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; import { + DEFAULT_DAY_NIGHT, GLOW_BASE_PX, + dayNightFactor, dimColor, flickerFactor, lightFrame, @@ -126,4 +128,53 @@ prev = c; } }); -}); \ No newline at end of file +}); +describe('dayNightFactor', () => { + it('день — 0', () => { + expect(dayNightFactor(8)).toBe(0); + expect(dayNightFactor(12)).toBe(0); + expect(dayNightFactor(17.9)).toBe(0); + }); + + it('закат 18–20 — линейный рост', () => { + expect(dayNightFactor(18)).toBe(0); + expect(dayNightFactor(19)).toBeCloseTo(0.5, 10); + expect(dayNightFactor(20)).toBe(1); + }); + + it('ночь — 1', () => { + expect(dayNightFactor(23)).toBe(1); + expect(dayNightFactor(0)).toBe(1); + expect(dayNightFactor(5.9)).toBe(1); + }); + + it('рассвет 6–8 — линейный спад', () => { + expect(dayNightFactor(6)).toBe(1); + expect(dayNightFactor(7)).toBeCloseTo(0.5, 10); + expect(dayNightFactor(8)).toBe(0); + }); + + it('стык 24/0 непрерывен, отрицательные и большие часы ок', () => { + expect(dayNightFactor(24)).toBe(1); + expect(dayNightFactor(25)).toBe(dayNightFactor(1)); + expect(dayNightFactor(-4)).toBe(1); + expect(dayNightFactor(-16)).toBe(0); // -16 ≡ 8 + }); + + it('монотонность на переходах', () => { + for (let t = 18; t < 20; t += 0.1) { + expect(dayNightFactor(t + 0.1)).toBeGreaterThanOrEqual(dayNightFactor(t)); + } + for (let t = 6; t < 8; t += 0.1) { + expect(dayNightFactor(t + 0.1)).toBeLessThanOrEqual(dayNightFactor(t)); + } + }); + + it('кастомный spec', () => { + const spec = { dawn: 0, day: 1, dusk: 10, night: 12 }; + expect(dayNightFactor(5, spec)).toBe(0); + expect(dayNightFactor(11, spec)).toBeCloseTo(0.5, 10); + expect(dayNightFactor(12, spec)).toBe(1); + expect(DEFAULT_DAY_NIGHT.dusk).toBe(18); // дефолт не тронут + }); +}); diff --git a/packages/engine/src/render/lightSim.ts b/packages/engine/src/render/lightSim.ts index abf96c0..b8ec65d 100644 --- a/packages/engine/src/render/lightSim.ts +++ b/packages/engine/src/render/lightSim.ts @@ -106,4 +106,30 @@ const g = Math.round(((color >> 8) & 0xff) * k); const b = Math.round((color & 0xff) * k); return (r << 16) | (g << 8) | b; +} + +/** + * Границы суток в часах: рассвет dawn..day (ночь → день), + * день day..dusk, закат dusk..night (день → ночь), ночь night..dawn. + */ +export interface DayNightSpec { + dawn: number; + day: number; + dusk: number; + night: number; +} + +/** Дефолтная кривая игры: рассвет 06–08, закат 18–20, ночь 20–06. */ +export const DEFAULT_DAY_NIGHT: DayNightSpec = { dawn: 6, day: 8, dusk: 18, night: 20 }; + +/** + * Фактор ночи 0..1 по времени суток (часы, mod 24): 0 — день, 1 — ночь, + * линейный переход на рассвете и закате. Непрерывен на стыке 24/0. + */ +export function dayNightFactor(tHours: number, spec: DayNightSpec = DEFAULT_DAY_NIGHT): number { + const t = ((tHours % 24) + 24) % 24; + if (t >= spec.night || t < spec.dawn) return 1; + if (t < spec.day) return 1 - (t - spec.dawn) / (spec.day - spec.dawn); + if (t < spec.dusk) return 0; + return (t - spec.dusk) / (spec.night - spec.dusk); } \ No newline at end of file