/**
* Чистая математика теней — без Pixi, тестируется в Vitest.
* Солнце ходит по дуге в пределах дня из DayNightSpec (dawn..night): тень
* утром «влево по экрану», вечером «вправо», в полдень короткая; направление
* проецируется изометрией из плоскости мира — тень всегда лежит на земле.
* Мир пепельный — свет рассеянный, тени мягкие: альфа скромная, края
* ступенчатые (рисует makeShadowTexture), геометрия — эллипс, сидящий на базе.
*/
import { type DayNightSpec, DEFAULT_DAY_NIGHT } from './lightSim';
import { DEFAULT_ISO } from '../math/iso';
/** Цвет тени — нейтральный тёмно-серый (пепельная гамма, не чёрный). */
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 az = (Math.PI / 2) * (1 - k); // мировой юг (утро) → юго-восток (вечер)
const gx = Math.cos(az);
const gy = Math.sin(az);
const px = (gx - gy) * (DEFAULT_ISO.tileW / 2);
const py = (gx + gy) * (DEFAULT_ISO.tileH / 2);
const n = Math.hypot(px, py);
const len = 0.55 + 1.05 * Math.pow(Math.abs(k - 0.5) * 2, 1.4);
return { dirX: px / n, dirY: py / n, len, alpha: 0.5 * Math.min(up, down) };
}
/** Размещение блоба-тени относительно базы объекта (в px виртуального экрана). */
export interface BlobPlacement {
/** Центр эллипса относительно точки базы (ноги/основания). */
x: number;
y: number;
/** Поворот эллипса — вдоль направления тени. */
rotation: number;
/** Полуоси эллипса, px. */
scaleX: number;
scaleY: number;
/** Прозрачность — сила солнца. */
alpha: number;
}
/**
* Эллипс тени для объекта высотой heightPx с «толщиной» radiusPx. Масса
* эллипса сидит у базы: центр лишь чуть съезжает вдоль тени, а ближний край
* заходит за базу — иначе видимая тёмная масса висит в стороне от объекта
* и тот читается «летающим». Вылет вдоль направления растёт с высотой и
* низким солнцем (len), ширина поперёк почти не меняется от длины.
*/
export function blobPlacement(radiusPx: number, heightPx: number, sun: SunShadow): BlobPlacement {
const reach = heightPx * sun.len;
const center = radiusPx * 0.3 + reach * 0.2; // центр массы от базы
const a = center + radiusPx * 0.55 + reach * 0.22; // полуось вдоль тени
const b = radiusPx * (0.75 + 0.25 * Math.min(1, sun.len)); // полуось поперёк
return {
x: sun.dirX * center,
y: sun.dirY * center,
rotation: Math.atan2(sun.dirY, sun.dirX),
scaleX: a,
scaleY: b,
alpha: sun.alpha
};
}