Files
kolaytercih/src/components/use-pixel-heat.ts
bilalgursen c60e3ab409
All checks were successful
Deploy / deploy (push) Successful in 4m34s
fix(tasarim): şehir haritası kartı arka plan gibi kaplar, kenarlar erir; pikseller azaldı
- Izgara 72 → 56 sütun, serpinti kaldırıldı (Bilal: "piksel detaylarını azalt").
- Harita `doldur` (slice) ile SehirYuzeyi kartını kaplar, metin üstünde; iki
  katlı mask ile hiçbir kenarda kesik kare kalmaz (Bilal: "canvas net bir
  şekilde kesiliyor"). Telefonda altta, lg'de sağ yarıda belirir.
- Yakınlaşma kutusu kabın oranında kurulur, il `odakHiza` ile kabın istenen
  noktasına gelir (şehir sayfası: lg sağ orta, telefon alt orta; konum kartı orta).
- usePixelHeat meet/slice + ortalama ofsetini hesaba katar.
- Üniversite konum kartı: kartı doldurur, radyal erime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 15:10:44 +03:00

180 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<SVGRectElement>;
controls: Map<SVGRectElement, AnimationPlaybackControls>;
};
const instances = new Set<HeatInstance>();
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<SVGSVGElement | null>,
{
/** 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<SVGRectElement>("[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]);
}