"use client"; /** * Şehri temsil eden piksel harita (DESIGN.md §4.17) — hero'daki dot-matrix * Türkiye'nin (turkey-pixel-map.tsx) şehir sayfası ve üniversite konum kartı * için genellenmiş hâli. Aynı dil: iri kareler, altı farklı opaklık, kıyının * altına seyrelen serpinti, imleç yaklaşınca turuncuya ısınan pikseller * (usePixelHeat). Fark: ızgara 56 sütun (her il en az 2×2 kare; Bilal, 24 * Eyl 2026: "piksel detaylarını azalt"), serpinti yok, bulunulan ilin * kareleri turuncu, hücrenin %88'i kadar iri ("C'deki gibi"), sırayla * belirir ve nabız atar. * * Markup hero'daki gibi deterministik string olarak üretilir ve * dangerouslySetInnerHTML ile basılır: ~1300 rect React elemanı olarak * hydrate edilmez; server ve client aynı çıktıyı verir. * * Yakınlaşma (Bilal, 24 Eyl 2026: "ilin bulunduğu yere doğru harita büyüsün, * sayfa yüklendikten sonra delay ile"): SSR tüm ülkeyi basar; yüklenince * ODAK_GECIKME sonra viewBox, ilin çevresindeki kutuya doğru (en çok * ODAK_ZOOM kat) güçlü ease-out ile kayar — kareler büyür, il kadraja * gelir. Kutu kabın o anki oranında kurulur ve il `odakHiza` ile kabın * istenen noktasına (ör. sağ orta, metnin boş bıraktığı yere) getirilir. * Harita bir arka plan gibi kabı doldurur (`doldur`: slice); kesik kenar * kalmasın diye çağıran kenarları mask ile eritir (Bilal: "canvas net bir * şekilde kesiliyor, çok kötü"). Hareketi azalt açıkken kutuya doğrudan atlanır. */ import { useEffect, useRef } from "react"; import { animate } from "motion"; import { usePixelHeat } from "@/components/use-pixel-heat"; import { ilUlkeHaritasi, type UlkeKodu } from "@/lib/harita-ulkeler"; import { IL_HUCRELERI, IL_IZGARA, ULKE_HUCRELERI, type IzgaraHucre, } from "@/lib/il-izgara-verisi"; import { cn } from "@/lib/utils"; const { sutun: SUTUN, satir: SATIR, hucre: CELL } = IL_IZGARA; const DOT = CELL * 0.74; const INSET = (CELL - DOT) / 2; // Vurgu kareleri iri ve dolu (C seçeneğinin hissi) const DOT_VURGU = CELL * 0.88; const INSET_VURGU = (CELL - DOT_VURGU) / 2; const VIEW_W = SUTUN * CELL; const VIEW_H = SATIR * CELL; // Hero'nun ısı fırçası 640 birimlik viewBox'ta 34; burada aynı oran. const ISI_YARICAPI = 34 * (VIEW_W / 640); // Yakınlaşma: gecikme (s), süre (s), en çok büyütme, ilin çevresinde pay (hücre) // Sıçramasız yay (Apple: damping 1): hızlı başlayan ease-out "sert" // duruyordu (Bilal, 24 Eyl 2026: "geçişler sert"); yay sıfır hızla başlar, // yumuşak ivmelenir ve sessizce oturur. const ODAK_GECIKME = 0.6; const ODAK_SURE = 1.6; const ODAK_ZOOM = 2.4; const ODAK_PAY = 2; type Kutu = readonly [number, number, number, number]; const TAM_KUTU: Kutu = [0, 0, VIEW_W, VIEW_H]; /** Vurgunun viewBox'taki sınır kutusu [x0, y0, x1, y1] */ type Sinir = readonly [number, number, number, number]; /** Odak konumu: ilin kabın neresine geleceği (0–1, sol/üst = 0). */ export type OdakHiza = { readonly x: number; readonly y: number }; const ORTA: OdakHiza = { x: 0.5, y: 0.5 }; function sinirKutusu(hucreler: readonly IzgaraHucre[]): Sinir | null { if (hucreler.length === 0) return null; let q0 = Infinity, q1 = -Infinity, r0 = Infinity, r1 = -Infinity; for (const [q, r] of hucreler) { if (q < q0) q0 = q; if (q > q1) q1 = q; if (r < r0) r0 = r; if (r > r1) r1 = r; } return [ (q0 - ODAK_PAY) * CELL, (r0 - ODAK_PAY) * CELL, (q1 + 1 + ODAK_PAY) * CELL, (r1 + 1 + ODAK_PAY) * CELL, ]; } /** * Odak kutusu: kabın oranında (kesme olmasın), en çok `zoom` kat büyüten, * ilin merkezini kabın (hiza.x, hiza.y) noktasına getiren viewBox. Harita * dışına taşabilir; kenarlar çağıranın mask'ıyla eridiği için sorun olmaz. */ function odakKutusu(sinir: Sinir, oran: number, zoom: number, hiza: OdakHiza): Kutu { const [x0, y0, x1, y1] = sinir; let w = Math.max(x1 - x0, VIEW_W / zoom); let h = w / oran; if (h < y1 - y0) { h = y1 - y0; w = h * oran; } const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2; return [cx - w * hiza.x, cy - h * hiza.y, w, h]; } const OPACITIES = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19]; const opacityFor = (x: number, y: number) => OPACITIES[(x * 7 + y * 11) % OPACITIES.length]; const VURGU_OPACITIES = [1, 0.9, 0.82, 0.95]; const vurguOpacityFor = (x: number, y: number) => VURGU_OPACITIES[(x * 3 + y * 5) % VURGU_OPACITIES.length]; const f = (n: number) => n.toFixed(2); function zeminKare(q: number, r: number, op: number) { return ``; } function vurguKare(q: number, r: number, i: number) { return ``; } /** * Zemin (mavi doku) + vurgu (turuncu) hücrelerinden statik SVG içi markup. * `isaret`: ülke haritasında şehir merkezi — 2×2 hücrelik tek iri kare. */ function markupUret( zemin: readonly IzgaraHucre[], vurgu: readonly IzgaraHucre[], isaret: IzgaraHucre | null, ): Harita { const parts: string[] = []; const vurguSet = new Set(vurgu.map(([q, r]) => `${q},${r}`)); for (const [q, r] of zemin) { if (vurguSet.has(`${q},${r}`)) continue; parts.push(zeminKare(q, r, opacityFor(q, r))); } // Vurgu: sırayla belirir (kt-piksel-belir), grup hâlinde nabız atar parts.push( ``, ); vurgu.forEach(([q, r], i) => parts.push(vurguKare(q, r, i))); if (isaret) { const [q, r] = isaret; const boy = 2 * CELL - 2 * INSET_VURGU; parts.push( ``, ); } parts.push(""); // Odak: il kareleri; ülke haritasında şehir işareti (2×2) ya da ülkenin kendisi const odak = isaret ? { sinir: sinirKutusu([isaret, [isaret[0] + 1, isaret[1] + 1]]), zoom: 1.8 } : { sinir: sinirKutusu(vurgu), zoom: ODAK_ZOOM }; return { html: parts.join(""), ...odak }; } type Harita = { html: string; sinir: Sinir | null; zoom: number }; const onbellek = new Map(); function markup(anahtar: string, uret: () => Harita): Harita { let m = onbellek.get(anahtar); if (!m) { m = uret(); onbellek.set(anahtar, m); } return m; } const kutuMetni = (k: Kutu) => k.map((n) => n.toFixed(1)).join(" "); type HaritaProps = { etiket: string; className?: string; /** Kabı doldur (preserveAspectRatio slice) — arka plan kullanımı */ doldur?: boolean; /** İlin kabın neresine geleceği; `lg` geniş ekranda, `dar` altında */ odakHiza?: { lg: OdakHiza; dar: OdakHiza }; }; function PikselSvg({ harita, etiket, className, doldur = false, odakHiza, }: HaritaProps & { harita: Harita }) { 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. 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); 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, ]), }); return () => { kontrol.stop(); yaz(TAM_KUTU); }; }, [sinir, zoom, hizaLg, hizaDar]); return ( ); } const TUM_IL_HUCRELERI = Object.values(IL_HUCRELERI).flat(); /** * Türkiye; `il` (DB biçimi, ör. "İZMİR") turuncu karelerle yanar. `iller` * verilirse hepsi yanar (tür sayfası: devlet üniversitesi olan her il) ve * yakınlaşma yapılmaz — ülkenin tamamı görünür. */ export function TurkiyePikselHaritasi({ il, iller, ...props }: HaritaProps & { il?: string; iller?: readonly string[] }) { const liste = iller ?? (il ? [il] : []); const anahtar = `tr:${liste.join("|")}`; const harita = markup(anahtar, () => { const vurgu = liste.flatMap((ad) => IL_HUCRELERI[ad] ?? []); const h = markupUret(TUM_IL_HUCRELERI, vurgu, null); return iller ? { ...h, sinir: null } : h; }); return ; } /** * Ülke (KKTC, yurt dışı). Şehir konumu verilirse ülke mavi kalır ve şehirde * 2×2 hücrelik iri turuncu kare durur; verilmezse ülkenin tamamı turuncu. */ export function UlkePikselHaritasi({ ulke, sehir, ...props }: HaritaProps & { ulke: UlkeKodu; /** viewBox koordinatı (harita-ulkeler'deki proje ile) */ sehir: { x: number; y: number } | null; }) { const harita = markup( `ulke:${ulke}:${sehir ? `${Math.round(sehir.x)},${Math.round(sehir.y)}` : "-"}`, () => { const h = ULKE_HUCRELERI[ulke]; if (sehir) { const q = Math.min(SUTUN - 2, Math.max(0, Math.round(sehir.x / CELL) - 1)); const r = Math.min(SATIR - 2, Math.max(0, Math.round(sehir.y / CELL) - 1)); return markupUret([...h.baglam, ...h.ulke], [], [q, r]); } return markupUret(h.baglam, h.ulke, null); }, ); return ; } /** Şehir sayfası: il değerinden Türkiye ya da ülke haritasını seçer. */ export function SehirPikselHaritasi({ il, ad, ...props }: Omit & { /** DB biçiminde il ("İSTANBUL", "LEFKOŞA", "KIBRIS") */ il: string; /** Görünen ad (etiket için) */ ad: string; }) { if (IL_HUCRELERI[il]) { return ( ); } const ulke = ilUlkeHaritasi(il); if (!ulke) return null; return ( ); }