/**
* Чистая математика теней — без Pixi, тестируется в Vitest.
* Солнце ходит по дуге в пределах дня из DayNightSpec (dawn..night): тень
* утром «на запад экрана», вечером «на восток», в полдень короткая. Мир
* пепельный — свет рассеянный, тени мягкие: альфа скромная, края ступенчатые
* (рисует makeShadowTexture), геометрия — вытянутый эллипс от базы объекта.
*/
import { type DayNightSpec, DEFAULT_DAY_NIGHT } from './lightSim';
/** Цвет тени — нейтральный тёмно-серый (пепельная гамма, не чёрный). */
export const SHADOW_COLOR = 0x2f2f38;
/** Гладкая ступень 0..1 (нулевой наклон на краях — без рывков альфы). */
function smooth(t: number): number {
const x = Math.max(0, Math.min(1, t));
return x * x * (3 - 2 * x);
}
/** Состояние солнца для теней: направление тени (экран), длина, альфа. */
export interface SunShadow {
/** Единичный вектор тени на экране (dx вправо, dy вниз). */
dirX: number;
dirY: number;
/** Длина тени на единицу высоты объекта (короткая в полдень). */
len: number;
/** Сила тени 0..1 (пепельный мир — солнце рассеянное). */
alpha: number;
}
/**
* Солнце по часам суток: null — ночи (тьма рисует darkness-слой, тени от
* солнца не видны). Границы и разгон альфы — из DayNightSpec: подъём
* dawn..day, спад dusk..night; направление качается от «на запад экрана»
* утром до «на восток» вечером.
*/
export function sunShadow(hours: number, spec: DayNightSpec = DEFAULT_DAY_NIGHT): SunShadow | null {
const t = ((hours % 24) + 24) % 24;
const rise = spec.dawn;
const set = spec.night;
if (t < rise || t >= set) return null;
const up = smooth((t - rise) / Math.max(0.001, spec.day - rise));
const down = 1 - smooth((t - spec.dusk) / Math.max(0.001, set - spec.dusk));
const k = (t - rise) / (set - rise); // 0..1 — путь солнца за день
const dx = -1 + 2 * k;
const dy = 0.6; // свет всегда «сверху» экрана — тень чуть вниз
const n = Math.hypot(dx, dy);
const len = 0.55 + 1.05 * Math.pow(Math.abs(k - 0.5) * 2, 1.4);
return { dirX: dx / n, dirY: dy / n, len, alpha: 0.34 * Math.min(up, down) };
}
/** Размещение блоба-тени относительно базы объекта (в px виртуального экрана). */
export interface BlobPlacement {
/** Центр эллипса относительно точки базы (ноги/основания). */
x: number;
y: number;
/** Поворот эллипса — вдоль направления тени. */
rotation: number;
/** Полуоси эллипса, px. */
scaleX: number;
scaleY: number;
/** Прозрачность — сила солнца. */
alpha: number;
}
/**
* Эллипс тени для объекта высотой heightPx с «толщиной» radiusPx: тень
* начинается у базы и тянется вдоль направления на heightPx·len, ширина
* поперёк почти не меняется от длины.
*/
export function blobPlacement(radiusPx: number, heightPx: number, sun: SunShadow): BlobPlacement {
const reach = heightPx * sun.len;
const tip = reach + radiusPx * 0.8; // дальний край тени от базы
const a = tip / 2;
const b = radiusPx * (0.7 + 0.3 * sun.len);
return {
x: sun.dirX * a,
y: sun.dirY * a,
rotation: Math.atan2(sun.dirY, sun.dirX),
scaleX: a,
scaleY: b,
alpha: sun.alpha
};
}