feat(effects): вода, облако яда, луч света; пульт и окна демонстрации
- Поле: вода (сплошная заливка по штриху, превью кистью), туман/огонь/дождь без изменений логики. - Действия: облако яда (частицы, круглая текстура, звук oblako-yada.mp3, длительность как у трека), луч света и заморозка со звуками из public/. - Пульт: инструменты воды и яда, синхрон SFX, тесты панели и ластика. - Окно управления: дочернее от окна просмотра (Z-order). - Типы эффектов, effectsStore prune, hit-test ластика. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
/** Звук и длительность эффекта «Заморозка» (`public/zamorozka.mp3`). */
|
||||
|
||||
const DEFAULT_FREEZE_LIFE_MS = 820;
|
||||
|
||||
const FREEZE_SFX_BASE_VOLUME = 0.88;
|
||||
/** На 25% тише базовой громкости эффекта. */
|
||||
export const FREEZE_SFX_VOLUME = FREEZE_SFX_BASE_VOLUME * 0.75;
|
||||
|
||||
export function freezeEffectSoundUrl(): string {
|
||||
return new URL('zamorozka.mp3', window.location.href).href;
|
||||
}
|
||||
|
||||
let cachedFreezeSfxDurationMs: number | null = null;
|
||||
|
||||
/** Длительность трека в мс (кэш после первого чтения метаданных). */
|
||||
export function getFreezeSfxDurationMs(): Promise<number> {
|
||||
if (cachedFreezeSfxDurationMs !== null) {
|
||||
return Promise.resolve(cachedFreezeSfxDurationMs);
|
||||
}
|
||||
const url = freezeEffectSoundUrl();
|
||||
return new Promise((resolve) => {
|
||||
const a = new Audio();
|
||||
const done = (ms: number): void => {
|
||||
cachedFreezeSfxDurationMs = ms;
|
||||
a.removeAttribute('src');
|
||||
resolve(ms);
|
||||
};
|
||||
a.addEventListener('loadedmetadata', () => {
|
||||
const d = a.duration;
|
||||
done(Number.isFinite(d) && d > 0 ? Math.round(d * 1000) : DEFAULT_FREEZE_LIFE_MS);
|
||||
});
|
||||
a.addEventListener('error', () => done(DEFAULT_FREEZE_LIFE_MS));
|
||||
a.src = url;
|
||||
a.load();
|
||||
});
|
||||
}
|
||||
|
||||
/** Длительность полноэкранной заморозки и льда — по времени звука, с разумными пределами. */
|
||||
export async function getFreezeEffectLifeMs(): Promise<number> {
|
||||
const raw = await getFreezeSfxDurationMs();
|
||||
return Math.min(45_000, Math.max(400, raw));
|
||||
}
|
||||
|
||||
export function playFreezeEffectSound(): void {
|
||||
try {
|
||||
const el = new Audio(freezeEffectSoundUrl());
|
||||
el.volume = FREEZE_SFX_VOLUME;
|
||||
void el.play().catch(() => undefined);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user