38 lines
2.5 KiB
TypeScript
38 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
// Genesis (docs/DESIGN.md) — กริดจุดอันตรกิริยา: ลวดลายตกแต่งชิ้นเดียวที่ design
|
|
// system อนุญาต ("the interactive dot grid is the only decorative element") ใช้เป็น
|
|
// พื้นหลังหน้า login — จุดเทาถี่ 24px + ไฮไลต์น้ำเงินจางตามตำแหน่ง pointer
|
|
// ⚠️ ต้องเป็น pointer-events-none เสมอ: ตัว layer เป็น absolute inset-0 ครอบพื้นที่
|
|
// ทั้งหน้า ส่วนการ์ด login เป็น static element — ตัว positioned จะชนะ hit-test ทับ
|
|
// การ์ดจนคลิก/focus ช่องกรอกไม่ได้ (bug report 2026-09-23) จึงฟัง pointermove ที่
|
|
// window แทนของตัวเอง — ไฮไลต์ยังตามเมาส์ได้แม้อยู่เหนือการ์ด + อัปเดตผ่าน CSS
|
|
// variable บน style ตรง ๆ ไม่ setState กัน re-render ทุก mousemove
|
|
// mask ไล่จางขอบให้กลืนกับพื้น canvas แทนการตัดเส้นชัด
|
|
import { useEffect, useRef } from "react";
|
|
|
|
export default function DotGrid() {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const onPointerMove = (e: PointerEvent) => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
const rect = el.getBoundingClientRect();
|
|
el.style.setProperty("--dot-mx", `${e.clientX - rect.left}px`);
|
|
el.style.setProperty("--dot-my", `${e.clientY - rect.top}px`);
|
|
};
|
|
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
|
return () => window.removeEventListener("pointermove", onPointerMove);
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
aria-hidden
|
|
className="pointer-events-none absolute inset-0 -z-10 [background-image:radial-gradient(circle,rgba(10,10,10,0.10)_1px,transparent_1px)] [background-size:24px_24px] [-webkit-mask-image:radial-gradient(72%_58%_at_50%_42%,black,transparent)] [mask-image:radial-gradient(72%_58%_at_50%_42%,black,transparent)]"
|
|
>
|
|
<div className="absolute inset-0 [background-image:radial-gradient(280px_circle_at_var(--dot-mx,50%)_var(--dot-my,36%),rgba(0,51,161,0.07),transparent_72%)]" />
|
|
</div>
|
|
);
|
|
}
|