From 504c5c85d9bb43d8799a947162ac1cdf78399894 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Sun, 4 Oct 2026 14:00:34 +0300 Subject: [PATCH] =?UTF-8?q?Piksel=20harita=20ve=20logo=20animasyonu=20iyil?= =?UTF-8?q?e=C5=9Ftirmeleri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Şehir/Türkiye piksel haritası görünüme girince oynar; animasyonlu-mark, console-asr, use-pixel-heat ve cta-sira-form güncellendi. Co-Authored-By: Claude Fable 5.1 --- src/app/globals.css | 5 + src/components/animasyonlu-mark.tsx | 287 ++++++++++-------- src/components/console-asr.tsx | 33 +- src/components/use-pixel-heat.ts | 78 ++++- .../components/sehir-piksel-haritasi.tsx | 77 +++-- .../pazarlama/components/turkey-pixel-map.tsx | 7 +- .../sihirbaz/components/cta-sira-form.tsx | 22 +- 7 files changed, 329 insertions(+), 180 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 56dc64b..d213829 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -341,6 +341,11 @@ animation: kt-piksel-belir 0.42s cubic-bezier(0.23, 1, 0.32, 1) both; } +/* Harita görünüme girene kadar bekler (PikselSvg data-gorundu yazar). */ +.kt-piksel-harita:not([data-gorundu]) .kt-piksel-belir { + animation-play-state: paused; +} + @media (prefers-reduced-motion: reduce) { .kt-piksel-belir { animation: none; diff --git a/src/components/animasyonlu-mark.tsx b/src/components/animasyonlu-mark.tsx index fb6ff7a..3289bdf 100644 --- a/src/components/animasyonlu-mark.tsx +++ b/src/components/animasyonlu-mark.tsx @@ -1,10 +1,9 @@ "use client"; -import { useEffect, useRef, useState, type CSSProperties } from "react"; -import { motion, useReducedMotion } from "motion/react"; +import { useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; -// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı (Motion). +// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı. // İki varyant: // // - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi → @@ -14,9 +13,14 @@ import { cn } from "@/lib/utils"; // - "nabiz": kareler yerlerinden ayrılmaz; 120° faz farkıyla dalga gibi // küçülüp büyür (danışman panel başlığının sakin, sürekli hâli). // -// Not: motion.rect'te `x`/`y` transform değeridir (attribute değil) — kareler -// attribute yerine translate ile konumlanır; scale, transform-box: fill-box -// sayesinde karenin kendi merkezinden uygulanır. +// Performans notu: kareler SVG değil HTML , animasyon da Web +// Animations API ile yalnızca `translate` / `scale` / `opacity` üzerinden +// yürür. Bu üçü compositor'da (GPU) oynar: ana iş parçacığı meşgulken +// (hidrasyon, sayfa geçişi, kaydırma) bile kare düşmez ve hover React'i +// yeniden render etmez. SVG iç elemanlarının transform'u her karede yeniden +// boyanır; o yüzden buraya geri dönme. + +const KUTU = 180; // kaynak SVG'nin viewBox kenarı const POZISYONLAR = [ [27, 56], @@ -32,10 +36,11 @@ const KARELER = [ // Rota: tam tur 3.6s — 3 etap × (%25 yol = 0.9s + %8.3 varışta soluklanma). // 0.9s'lik yolculuk, nabız varyantının küçül-büyü temposuyla aynı hızda. -const ROTA_SURE = 3.6; +const ROTA_SURE = 3600; // Kalkışlar kademeli: kareler eş zamanlı değil, danışman başlığındaki gibi -// sırayla (mavi → şeftali → turuncu) yola çıkıp küçülür. -const ROTA_GECIKME_ADIMI = 0.2; +// sırayla (mavi → şeftali → turuncu) yola çıkıp küçülür. Gecikme yalnızca ilk +// turdan önce uygulanır, sonraki turlarda faz farkı kendiliğinden korunur. +const ROTA_GECIKME_ADIMI = 200; const KONUM_ZAMANLARI = [0, 0.25, 1 / 3, 0.583, 2 / 3, 0.917, 1]; // Nabız (rota içi): her yolculuğun ortasında dibe vurur, varışta tam form. const ROTA_NABIZ_ZAMANLARI = [ @@ -46,20 +51,77 @@ const ROTA_NABIZ_OPACITY = [1, 0.6, 1, 1, 0.6, 1, 1, 0.6, 1, 1]; // Yerinde nabız: kesintisiz tam-sinüs (dinlenme yok), delay'ler döngüyü üçe // böler — biri büyürken sıradaki küçülür, hiçbir kare beklemez. -const NABIZ_SURE = 1.8; -const NABIZ_GECIKMELER = [0, 0.6, 1.2] as const; -const SINE = [0.37, 0, 0.63, 1] as const; +const NABIZ_SURE = 1800; +const NABIZ_GECIKMELER = [0, 600, 1200] as const; -/** Kare i için durak koordinatları: kendi yeri → sıradaki → ondan sonraki → +const SINE = "cubic-bezier(0.37, 0, 0.63, 1)"; +// Yolculuk: yumuşak kalkış, uzun ve yumuşak iniş (easeInOutCubic). +const YOL = "cubic-bezier(0.65, 0, 0.35, 1)"; +// Hover bitince yerine oturma (easeOutQuint): hızlı toparlanır, sarsmadan durur. +const DONUS = "cubic-bezier(0.22, 1, 0.36, 1)"; +const DONUS_SURE = 500; + +/** Karenin `translate` değeri: yüzdeler karenin kendi boyutuna göredir, bu + * yüzden işaret hangi boyutta çizilirse çizilsin aynı oran korunur. */ +function yer(pozisyon: number, boyut: number): string { + const [x, y] = POZISYONLAR[pozisyon % 3]; + return `${(x / boyut) * 100}% ${(y / boyut) * 100}%`; +} + +/** Kare için durak koordinatları: kendi yeri → sıradaki → ondan sonraki → * kendi yeri. Duraklarda bekleme için her hedef iki kez tekrarlanır * (KONUM_ZAMANLARI ile hizalı). */ -function rota(baslangic: number, eksen: 0 | 1): number[] { - const [p0, p1, p2] = [0, 1, 2].map( - (adim) => POZISYONLAR[(baslangic + adim) % 3][eksen], - ); +function rota(baslangic: number, boyut: number): string[] { + const [p0, p1, p2] = [0, 1, 2].map((adim) => yer(baslangic + adim, boyut)); return [p0, p1, p1, p2, p2, p0, p0]; } +function donguyuBaslat( + el: HTMLElement, + i: number, + varyant: "rota" | "nabiz", +): Animation[] { + const k = KARELER[i]; + if (varyant === "nabiz") { + return [ + el.animate( + { scale: ["1", "0.82", "1"], opacity: [1, 0.55, 1], easing: SINE }, + { + duration: NABIZ_SURE, + delay: NABIZ_GECIKMELER[i], + iterations: Infinity, + }, + ), + ]; + } + const secenek = { + duration: ROTA_SURE, + delay: i * ROTA_GECIKME_ADIMI, + iterations: Infinity, + }; + // Konum ve nabız ayrı animasyonlar: konum etabı tek parça eğriyle akar, + // ortasındaki küçülme onu ikiye bölüp duraksatmaz. + return [ + el.animate( + { + translate: rota(k.baslangic, k.boyut), + offset: KONUM_ZAMANLARI, + easing: YOL, + }, + secenek, + ), + el.animate( + { + scale: ROTA_NABIZ_SCALE.map(String), + opacity: ROTA_NABIZ_OPACITY, + offset: ROTA_NABIZ_ZAMANLARI, + easing: SINE, + }, + secenek, + ), + ]; +} + /** * Animasyonlu KolayTercih işareti. Varsayılanda koreografi sürekli döner; * `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site @@ -78,116 +140,99 @@ export function AnimasyonluMark({ * (orange-500) — ör. danışman panel başlığı. */ renk?: "marka" | "turuncu"; }) { - const [hover, setHover] = useState(false); - const svgRef = useRef(null); - const azHareket = useReducedMotion(); - const aktif = !azHareket && (!sadeceHover || hover); + const kokRef = useRef(null); - // Hover hedefi işaretin kendisi değil, en yakın `group` ebeveyni (site - // header'da logo + "KolayTercih" yazısını saran Link): yazının üzerine - // gelindiğinde de animasyon başlar. `group`'suz kullanımda svg'ye düşer. useEffect(() => { - if (!sadeceHover) return; - const hedef = svgRef.current?.closest(".group") ?? svgRef.current; - if (!hedef) return; - const ac = () => setHover(true); - const kapat = () => setHover(false); - hedef.addEventListener("mouseenter", ac); - hedef.addEventListener("mouseleave", kapat); - return () => { - hedef.removeEventListener("mouseenter", ac); - hedef.removeEventListener("mouseleave", kapat); + const kok = kokRef.current; + if (!kok || typeof kok.animate !== "function") return; + const azHareket = window.matchMedia("(prefers-reduced-motion: reduce)"); + const kareler = Array.from(kok.children) as HTMLElement[]; + + let dongu: Animation[] = []; + let donus: Animation[] = []; + let istekli = false; + + const baslat = () => { + if (istekli) return; + istekli = true; + if (azHareket.matches) return; + // Kareler hâlâ yerine dönüyorsa (hızlı çık-gir) dönüşün bitmesi + // beklenir; döngü hep yerinden başlar, sıçrama olmaz. + Promise.all(donus.map((a) => a.finished)) + .then(() => { + if (!istekli || dongu.length > 0) return; + dongu = kareler.flatMap((el, i) => donguyuBaslat(el, i, varyant)); + }) + .catch(() => {}); }; - }, [sadeceHover]); + + const durdur = () => { + istekli = false; + if (dongu.length === 0) return; + // Kareler o an neredeyse oradan yerlerine süzülür. + const anlik = kareler.map((el) => { + const cs = getComputedStyle(el); + return { translate: cs.translate, scale: cs.scale, opacity: cs.opacity }; + }); + dongu.forEach((a) => a.cancel()); + dongu = []; + donus = kareler.map((el, i) => + el.animate( + [ + anlik[i], + { + translate: yer(KARELER[i].baslangic, KARELER[i].boyut), + scale: "1", + opacity: 1, + }, + ], + { duration: DONUS_SURE, easing: DONUS }, + ), + ); + }; + + if (!sadeceHover) { + baslat(); + return () => { + dongu.forEach((a) => a.cancel()); + }; + } + + // Hover hedefi işaretin kendisi değil, en yakın `group` ebeveyni (site + // header'da logo + "KolayTercih" yazısını saran Link): yazının üzerine + // gelindiğinde de animasyon başlar. `group`'suz kullanımda köke düşer. + const hedef = kok.closest(".group") ?? kok; + hedef.addEventListener("mouseenter", baslat); + hedef.addEventListener("mouseleave", durdur); + return () => { + hedef.removeEventListener("mouseenter", baslat); + hedef.removeEventListener("mouseleave", durdur); + [...dongu, ...donus].forEach((a) => a.cancel()); + }; + }, [sadeceHover, varyant]); return ( - + {KARELER.map((k) => ( + + ))} + ); } diff --git a/src/components/console-asr.tsx b/src/components/console-asr.tsx index acf4f07..a2af0b1 100644 --- a/src/components/console-asr.tsx +++ b/src/components/console-asr.tsx @@ -97,24 +97,22 @@ function mesaj(): string[] { bicim += "\n"; }); + // Bloklar arasında birer boş satır; her blokta tek punto, tek renk. + const arapca = `font-size:18px;line-height:2.1;font-family:${ARAPCA_YAZI_TIPI}`; + ekle(`\n${BESMELE}\n`, `${arapca};color:${SILIK}`); + ekle(`${ASR.join("\n")}\n`, arapca); + ekle(`\n${MEAL[0]}\n`, `font-size:12px;line-height:1.8;color:${SILIK}`); + ekle(`${MEAL.slice(2).join("\n")}\n`, "font-size:12px;line-height:1.8"); + ekle("Asr Sûresi (103) · Diyanet İşleri meali\n", `font-size:11px;line-height:2.2;color:${SILIK}`); ekle( - `\n${BESMELE}\n${ASR.join("\n")}\n`, - `font-size:22px;line-height:2;font-family:${ARAPCA_YAZI_TIPI}`, - ); - ekle(`\n${MEAL.join("\n")}\n`, "font-size:13px;line-height:1.7"); - ekle("Asr Sûresi (103) · Diyanet İşleri meali\n\n", `font-size:11px;color:${SILIK}`); - ekle( - "«Vakit bir kılıçtır; sen onu kesmezsen o seni keser.»\n", - `font-size:13px;font-weight:600;line-height:1.8;color:${TURUNCU}`, + "\n«Vakit bir kılıçtır; sen onu kesmezsen o seni keser.»\n", + `font-size:12px;font-style:italic;line-height:1.8;color:${TURUNCU}`, ); ekle( - "İmam Şâfiî’nin sûfîlerden öğrendiğini söylediği söz\n\n", - `font-size:11px;color:${SILIK}`, - ); - ekle( - `Meraklısına: ${window.location.origin}/meraklisina`, - `font-size:11px;color:${SILIK}`, + "İmam Şâfiî’nin sûfîlerden öğrendiğini söylediği söz\n", + `font-size:11px;line-height:2.2;color:${SILIK}`, ); + ekle(`\n${window.location.origin}/meraklisina`, `font-size:11px;color:${SILIK}`); return [bicim, ...stiller]; } @@ -126,7 +124,12 @@ export function ConsoleAsr() { // Hidrasyon ve ilk etkileşimle yarışmasın: tarayıcı boşa çıkınca bas. // Safari'de requestIdleCallback yok, orada kısa bir gecikme yeter. - const bas = () => console.log(...mesaj()); + // Önce konsolu temizle: eklenti uyarıları ve çerçeve satırları sürprizi + // aşağı itmesin, konsolu açan yalnız bunu görsün. + const bas = () => { + console.clear(); + console.log(...mesaj()); + }; if (typeof window.requestIdleCallback === "function") { window.requestIdleCallback(bas, { timeout: 3000 }); } else { diff --git a/src/components/use-pixel-heat.ts b/src/components/use-pixel-heat.ts index 75aefdb..5701bc0 100644 --- a/src/components/use-pixel-heat.ts +++ b/src/components/use-pixel-heat.ts @@ -26,6 +26,12 @@ 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 +// Kırpışma: imleç olmadan, rastgele tek piksel ısınıp söner. Yormasın diye +// seyrek ve yumuşak — aynı anda ekranda bir-iki sıcak piksel olur. +const KIRPISMA_BEKLEME = 1500; // ms; sayfa otursun +const KIRPISMA_ARALIK = [500, 1200]; // ms; iki kırpışma arası (rastgele) +const KIRPISMA_ISINMA = 0.5; // imleçtekinden yavaş: göz kırpar, çakmaz +const KIRPISMA_TUTMA = 900; // ms; ısınma + kısa bekleyiş, sonra soğur type HeatTarget = { el: SVGRectElement; @@ -139,18 +145,74 @@ function ayril(inst: HeatInstance) { } } +/** Rastgele pikselleri seyrek aralıklarla ısıtıp soğutur; durdurucu döner. + * Fırçayla aynı `calistir`/`hot` üzerinden çalışır: imlecin altındaki + * piksele dokunmaz, fırça da kırpışan pikseli devralabilir. Harita ekran + * dışındayken ya da sekme arka plandayken bekler. */ +function kirpismaBaslat(inst: HeatInstance) { + const yanan = new Set(); + const bekleyen = new Set(); + let gorunur = true; + const io = new IntersectionObserver(([g]) => { + gorunur = g.isIntersecting; + }); + io.observe(inst.svg); + + let sayac = 0; + const tik = () => { + const [enAz, enCok] = KIRPISMA_ARALIK; + sayac = window.setTimeout(tik, enAz + Math.random() * (enCok - enAz)); + if (!gorunur || document.hidden) return; + const p = inst.pixels[Math.floor(Math.random() * inst.pixels.length)]; + if (inst.hot.has(p.el) || yanan.has(p.el)) return; + yanan.add(p.el); + calistir( + inst, + p.el, + { fill: HOT_COLOR, fillOpacity: HOT_OPACITY }, + KIRPISMA_ISINMA, + ); + const s = window.setTimeout(() => { + bekleyen.delete(s); + yanan.delete(p.el); + if (inst.hot.has(p.el)) return; + calistir( + inst, + p.el, + { fill: inst.baseColor, fillOpacity: p.baseOpacity }, + COOL_DURATION, + ); + }, KIRPISMA_TUTMA); + bekleyen.add(s); + }; + sayac = window.setTimeout(tik, KIRPISMA_BEKLEME); + + return () => { + window.clearTimeout(sayac); + bekleyen.forEach((s) => window.clearTimeout(s)); + io.disconnect(); + }; +} + export function usePixelHeat( svgRef: RefObject, { /** Fırça yarıçapı, SVG (viewBox) biriminde */ radius = 34, - }: { radius?: number } = {}, + /** İmleç olmadan da rastgele pikseller seyrekçe ısınıp sönsün (hero) */ + kirpisma = false, + }: { radius?: number; kirpisma?: boolean } = {}, ) { 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; + // Dokunmatik cihazlarda hover yanlış tetiklenir; fırça imleçli cihazlara + // saklanır. Kırpışma imleç istemez ama "hareketi azalt"ta kapalıdır. + const imlecli = window.matchMedia(FINE_POINTER_QUERY).matches; + const kirp = + kirpisma && + !window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (!imlecli && !kirp) return; if (!svg.viewBox.baseVal.width) return; @@ -173,7 +235,11 @@ export function usePixelHeat( hot: new Set(), controls: new Map(), }; - kaydol(inst); - return () => ayril(inst); - }, [svgRef, radius]); + if (imlecli) kaydol(inst); + const kirpismaDur = kirp ? kirpismaBaslat(inst) : null; + return () => { + kirpismaDur?.(); + ayril(inst); + }; + }, [svgRef, radius, kirpisma]); } diff --git a/src/features/katalog/components/sehir-piksel-haritasi.tsx b/src/features/katalog/components/sehir-piksel-haritasi.tsx index d05cded..c35b873 100644 --- a/src/features/katalog/components/sehir-piksel-haritasi.tsx +++ b/src/features/katalog/components/sehir-piksel-haritasi.tsx @@ -195,40 +195,61 @@ function PikselSvg({ const svgRef = useRef(null); usePixelHeat(svgRef, { radius: ISI_YARICAPI }); - // Yüklenince gecikmeyle odağa yakınlaş. viewBox her karede yazılır; ısı - // fırçası viewBox'ı canlı okuduğu için imleç hep doğru kareyi ısıtır. + // Harita görünüme girince (yüklenince değil) gecikmeyle odağa yakınlaş; + // il karelerinin sırayla belirmesi de o ana kadar bekler (globals.css: + // data-gorundu yokken kt-piksel-belir duraklatılır). Ekranın altında kalan + // harita, kullanıcı oraya inmeden animasyonunu harcamasın. viewBox her + // karede yazılır; ısı fırçası viewBox'ı canlı okuduğu için imleç hep doğru + // kareyi ısıtır. const { sinir, zoom } = harita; const hizaLg = odakHiza?.lg ?? ORTA; const hizaDar = odakHiza?.dar ?? ORTA; useEffect(() => { const svg = svgRef.current; - if (!svg || !sinir) return; - const rect = svg.getBoundingClientRect(); - const oran = rect.width && rect.height ? rect.width / rect.height : VIEW_W / VIEW_H; - const genis = window.matchMedia("(min-width: 1024px)").matches; - const odak = odakKutusu(sinir, oran, zoom, genis ? hizaLg : hizaDar); + if (!svg) return; const yaz = (k: Kutu) => svg.setAttribute("viewBox", kutuMetni(k)); - if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - yaz(odak); - return () => yaz(TAM_KUTU); - } - const bas = TAM_KUTU; - const kontrol = animate(0, 1, { - type: "spring", - bounce: 0, - delay: ODAK_GECIKME, - duration: ODAK_SURE, - onUpdate: (t) => - yaz([ - bas[0] + (odak[0] - bas[0]) * t, - bas[1] + (odak[1] - bas[1]) * t, - bas[2] + (odak[2] - bas[2]) * t, - bas[3] + (odak[3] - bas[3]) * t, - ]), - }); + let kontrol: { stop: () => void } | null = null; + const basla = () => { + svg.setAttribute("data-gorundu", ""); + if (!sinir) return; + const rect = svg.getBoundingClientRect(); + const oran = rect.width && rect.height ? rect.width / rect.height : VIEW_W / VIEW_H; + const genis = window.matchMedia("(min-width: 1024px)").matches; + const odak = odakKutusu(sinir, oran, zoom, genis ? hizaLg : hizaDar); + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + yaz(odak); + return; + } + const bas = TAM_KUTU; + kontrol = animate(0, 1, { + type: "spring", + bounce: 0, + delay: ODAK_GECIKME, + duration: ODAK_SURE, + onUpdate: (t) => + yaz([ + bas[0] + (odak[0] - bas[0]) * t, + bas[1] + (odak[1] - bas[1]) * t, + bas[2] + (odak[2] - bas[2]) * t, + bas[3] + (odak[3] - bas[3]) * t, + ]), + }); + }; + // Haritanın dörtte biri, ekranın alt %15'lik şeridinin üstüne çıkınca + const gozlemci = new IntersectionObserver( + (girdiler) => { + if (!girdiler.some((g) => g.isIntersecting)) return; + gozlemci.disconnect(); + basla(); + }, + { threshold: 0.25, rootMargin: "0px 0px -15% 0px" }, + ); + gozlemci.observe(svg); return () => { - kontrol.stop(); - yaz(TAM_KUTU); + gozlemci.disconnect(); + kontrol?.stop(); + svg.removeAttribute("data-gorundu"); + if (sinir) yaz(TAM_KUTU); }; }, [sinir, zoom, hizaLg, hizaDar]); @@ -237,7 +258,7 @@ function PikselSvg({ ref={svgRef} viewBox={`0 0 ${VIEW_W} ${VIEW_H}`} preserveAspectRatio={doldur ? "xMidYMid slice" : "xMidYMid meet"} - className={cn(doldur ? "h-full w-full" : "h-auto w-full", "text-primary", className)} + className={cn(doldur ? "h-full w-full" : "h-auto w-full", "kt-piksel-harita text-primary", className)} role="img" aria-label={etiket} // Statik, deterministik markup — her render'da yeniden kurulmaz diff --git a/src/features/pazarlama/components/turkey-pixel-map.tsx b/src/features/pazarlama/components/turkey-pixel-map.tsx index bfa9e86..659735b 100644 --- a/src/features/pazarlama/components/turkey-pixel-map.tsx +++ b/src/features/pazarlama/components/turkey-pixel-map.tsx @@ -16,6 +16,9 @@ * imleç uzaklaşınca yavaşça kendi mavi tonuna soğur (asimetrik zamanlama). * Fırça mantığı usePixelHeat hook'unda ortaklaştırıldı; DOM'a * querySelectorAll ile bağlandığı için statik markup'la sorunsuz çalışır. + * + * İmleç gelmese de rastgele pikseller tek tük, seyrek aralıklarla turuncuya + * ısınıp söner (usePixelHeat `kirpisma`); "hareketi azalt"ta kapalıdır. */ import { useRef } from "react"; @@ -139,7 +142,7 @@ const MAP_MARKUP = (() => { ); } } - // Şehir pikselleri: turuncu, hafif nabız; hover fırçasına katılmazlar + // Şehir pikselleri: turuncu, hafif nabız; hover fırçasına katılmazlar. CITIES.forEach((city, i) => { parts.push( ``, @@ -150,7 +153,7 @@ const MAP_MARKUP = (() => { export function TurkeyPixelMap({ className }: { className?: string }) { const svgRef = useRef(null); - usePixelHeat(svgRef); + usePixelHeat(svgRef, { kirpisma: true }); return ( - + {/* Gölge yok (§6: derinlik gölgeyle verilmez), hover'da renk + değişmez (iri dolu yüzeyde orange-600 kırmızımsı okunuyor — + Bilal, 24 Eyl 2026); tepki: ok kayar, basınca küçülür. */} + + +