"use client"; /** * Hero'daki (TurkeyPixelMap) imleç ısı fırçasının ortak hook hali. * `data-px` işaretli rect'ler imleç yaklaşınca anında turuncuya ısınır, * uzaklaşınca yavaşça kendi rengine soğur (asimetrik zamanlama). * * SVG katmanları çoğunlukla pointer-events-none olduğundan imleç window * üzerinden izlenir ve viewBox koordinatına çevrilir. Beklenen veri * öznitelikleri: data-px, data-cx, data-cy, data-op (taban opaklık). * * Landing'de 16 örnek yaşar: her örneğin kendi window dinleyicisi yerine * modül seviyesinde TEK pointermove dinleyicisi + rAF birleştirmesi kullanılır; * kare başına en fazla bir tarama yapılır, imleçten uzak örnekler hiç * piksel iterasyonuna girmez. */ import { useEffect, type RefObject } from "react"; // Bilinçli olarak TAM motor: mini'nin WAAPI animate'i SVG fill/fillOpacity // geçişlerini farklı örnekliyor ve ısınma/soğuma hissi bozuluyor (denendi, // geri alındı). motion/mini'ye çevirme. import { animate, type AnimationPlaybackControls } from "motion"; const FINE_POINTER_QUERY = "(hover: hover) and (pointer: fine)"; const HOT_COLOR = "#f97316"; // orange-500 const HOT_OPACITY = 0.85; const HEAT_DURATION = 0.15; // ısınma: anlık geri bildirim const COOL_DURATION = 1.4; // soğuma: iz gibi yavaşça söner type HeatTarget = { el: SVGRectElement; cx: number; cy: number; baseOpacity: number; }; type HeatInstance = { svg: SVGSVGElement; radius: number; /** preserveAspectRatio "slice" mi (kabı doldurur) */ slice: boolean; baseColor: string; pixels: HeatTarget[]; hot: Set; controls: Map; }; const instances = new Set(); let sonOlay: PointerEvent | null = null; let rafId = 0; function calistir( inst: HeatInstance, el: SVGRectElement, to: { fill: string; fillOpacity: number }, duration: number, ) { inst.controls.get(el)?.stop(); inst.controls.set(el, animate(el, to, { duration, ease: "easeOut" })); } function tara() { rafId = 0; const e = sonOlay; if (!e) return; for (const inst of instances) { const rect = inst.svg.getBoundingClientRect(); if (rect.width === 0) continue; // viewBox her taramada okunur: şehir haritası (sehir-piksel-haritasi.tsx) // yüklendikten sonra ile doğru yakınlaşır; kutunun oranı SVG'ninkiyle // aynı tutulduğu için ölçek = genişlik oranı, orijin = viewBox.x/y. const vb = inst.svg.viewBox.baseVal; if (!vb.width || !vb.height) continue; // xMidYMid meet (varsayılan) ya da slice: ölçek en küçük/en büyük oran, // artan kenar ortalanır. const sx = rect.width / vb.width; const sy = rect.height / vb.height; const scale = inst.slice ? Math.max(sx, sy) : Math.min(sx, sy); const ox = (rect.width - vb.width * scale) / 2; const oy = (rect.height - vb.height * scale) / 2; const pad = inst.radius * scale; // İmleç fırça menzilinin tamamen dışındaysa ve sıcak piksel yoksa // bu örnek için hiç hesap yapma if ( inst.hot.size === 0 && (e.clientX < rect.left - pad || e.clientX > rect.right + pad || e.clientY < rect.top - pad || e.clientY > rect.bottom + pad) ) { continue; } const x = vb.x + (e.clientX - rect.left - ox) / scale; const y = vb.y + (e.clientY - rect.top - oy) / scale; for (const p of inst.pixels) { const d2 = (p.cx - x) ** 2 + (p.cy - y) ** 2; const isHot = d2 <= inst.radius * inst.radius; if (isHot && !inst.hot.has(p.el)) { inst.hot.add(p.el); calistir( inst, p.el, { fill: HOT_COLOR, fillOpacity: HOT_OPACITY }, HEAT_DURATION, ); } else if (!isHot && inst.hot.has(p.el)) { inst.hot.delete(p.el); calistir( inst, p.el, { fill: inst.baseColor, fillOpacity: p.baseOpacity }, COOL_DURATION, ); } } } } function onMove(e: PointerEvent) { sonOlay = e; if (!rafId) rafId = requestAnimationFrame(tara); } function kaydol(inst: HeatInstance) { if (instances.size === 0) { window.addEventListener("pointermove", onMove, { passive: true }); } instances.add(inst); } function ayril(inst: HeatInstance) { instances.delete(inst); inst.controls.forEach((c) => c.stop()); if (instances.size === 0) { window.removeEventListener("pointermove", onMove); if (rafId) cancelAnimationFrame(rafId); rafId = 0; sonOlay = null; } } export function usePixelHeat( svgRef: RefObject, { /** Fırça yarıçapı, SVG (viewBox) biriminde */ radius = 34, }: { radius?: number } = {}, ) { useEffect(() => { const svg = svgRef.current; if (!svg) return; // Dokunmatik cihazlarda hover yanlış tetiklenir; efekti imleçli cihazlara sakla if (!window.matchMedia(FINE_POINTER_QUERY).matches) return; if (!svg.viewBox.baseVal.width) return; const pixels: HeatTarget[] = Array.from( svg.querySelectorAll("[data-px]"), ).map((el) => ({ el, cx: Number(el.dataset.cx), cy: Number(el.dataset.cy), baseOpacity: Number(el.dataset.op), })); if (pixels.length === 0) return; const inst: HeatInstance = { svg, radius, slice: (svg.getAttribute("preserveAspectRatio") ?? "").includes("slice"), baseColor: getComputedStyle(svg).color, pixels, hot: new Set(), controls: new Map(), }; kaydol(inst); return () => ayril(inst); }, [svgRef, radius]); }