feat: add Night Shift layer (boot intro, ops strip, headlight, chapter numerals, film grain)

This commit is contained in:
2026-10-10 14:13:26 +03:00
parent 475e494675
commit d16ce88639
8 changed files with 389 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
import { onMounted, onUnmounted, type Ref } from 'vue';
const SURFACES = '.project-case-study, .project-card, .contact-destination, .infrastructure-flow-figure';
/**
* Pointer light for fine pointers only. Writes `--hx/--hy` (px) on the root for the
* headlight beam and `--mx/--my` (%) on the hovered surface for its spotlight.
*/
export function useHeadlight(enabled: Ref<boolean>): void {
let frame = 0;
let pending: PointerEvent | undefined;
let active = false;
const flush = (): void => {
frame = 0;
const event = pending;
pending = undefined;
if (event === undefined) return;
const root = document.documentElement;
root.style.setProperty('--hx', `${event.clientX}px`);
root.style.setProperty('--hy', `${event.clientY}px`);
const surface = (event.target as Element | null)?.closest<HTMLElement>(SURFACES);
if (surface !== null && surface !== undefined) {
const box = surface.getBoundingClientRect();
surface.style.setProperty('--mx', `${((event.clientX - box.left) / box.width) * 100}%`);
surface.style.setProperty('--my', `${((event.clientY - box.top) / box.height) * 100}%`);
}
};
const onMove = (event: PointerEvent): void => {
if (!enabled.value || event.pointerType === 'touch') return;
if (!active) {
active = true;
document.documentElement.dataset.headlight = 'on';
}
pending = event;
if (frame === 0) frame = requestAnimationFrame(flush);
};
onMounted(() => {
if (!window.matchMedia('(pointer: fine)').matches) return;
window.addEventListener('pointermove', onMove, { passive: true });
});
onUnmounted(() => {
window.removeEventListener('pointermove', onMove);
if (frame !== 0) cancelAnimationFrame(frame);
delete document.documentElement.dataset.headlight;
});
}