"use client"; import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode, type RefObject, } from "react"; import { cn } from "@/lib/utils"; import { usePixelHeat } from "@/components/use-pixel-heat"; /** * Hero'daki dot-matrix dilini (TurkeyPixelMap) bölüm ayraçlarına ve * dekorlara taşıyan piksel süsleri. Rastgelelik yerine deterministik * hash kullanılır ki sunucu ve istemci aynı deseni üretsin. * * Hepsi hero'daki imleç ısı fırçasına katılır (usePixelHeat): imleç * yaklaşınca pikseller turuncuya ısınır, uzaklaşınca yavaşça soğur. * Turuncu vurgu (accent) pikselleri hero'daki şehirler gibi fırçaya * katılmaz. */ // Hero ile aynı ızgara ölçüleri const CELL = 10; const DOT = 7.4; const INSET = (CELL - DOT) / 2; // Hero ile aynı doku paleti 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]; // Deterministik sözde-rastgele: aynı girdi her ortamda aynı deseni verir const hash = (x: number, y: number, seed: number) => { const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453; return n - Math.floor(n); }; function pixelRect( x: number, y: number, extra: { key: string; accent?: boolean; accentDelay?: number }, ) { const shared = { x: x * CELL + INSET, y: y * CELL + INSET, width: DOT, height: DOT, rx: 2, }; if (extra.accent) { return ( ); } const op = opacityFor(x, y); return ( ); } /** * CTA yüzeylerinin piksel zemini: büyüklü küçüklü kareler, masonry gibi * hafif kaymış (Bilal, 24 Eyl 2026: "arka planda büyüklü küçüklü, masonry * şeklinde"). Kabın **gerçek ölçüsüne** göre üretilir (ResizeObserver): * sabit bir çizimi kaplayacak şekilde ölçeklemek yüksek kartlarda görünen * kısmı boş banda düşürüyordu ("hiç doku yok"). Yalnız kenar çerçevesinde, * orta tamamen boş; `haric` verilen öğenin (ör. 24'lük piksel tablosu) * çevresi de boş kalır. Küçük (tek hücrelik) kare **yok** (Bilal, 24 Eyl * 2026: "çok fazla piksel, küçük olanlar hiç olmasın"): 4×4 hücrelik makro * hücrelere 2–4 hücrelik tek kare düşer ve makro hücre içinde kaydırılır: * çakışmaz ama hizaya da girmez (§1.1 "doğal duruş"). Durağan zemin: * imleç ısı fırçasına katılmaz. * Sunucuda boş çizilir, ölçülünce belirir (dekor; aria-hidden). */ export function PixelMasonry({ seed = 7, yogunluk = 1, kenar = 0.3, haric, className, }: { seed?: number; /** Doluluk çarpanı; 1 varsayılan, 0.6 seyrek */ yogunluk?: number; /** Kenar çerçevesinin genişliği: kısa kenarın oranı (0.3 = %30) */ kenar?: number; /** Çevresi boş bırakılacak öğe (kreatif, iri sayı) */ haric?: RefObject; className?: string; }) { const svgRef = useRef(null); const [olcu, setOlcu] = useState<{ w: number; h: number; hx: number; hy: number; hw: number; hh: number; } | null>(null); useEffect(() => { const svg = svgRef.current; if (!svg) return; const olc = () => { const r = svg.getBoundingClientRect(); const hr = haric?.current?.getBoundingClientRect(); const sonraki = { w: Math.round(r.width), h: Math.round(r.height), hx: hr ? Math.round(hr.left - r.left) : 0, hy: hr ? Math.round(hr.top - r.top) : 0, hw: hr ? Math.round(hr.width) : 0, hh: hr ? Math.round(hr.height) : 0, }; setOlcu((onceki) => onceki && onceki.w === sonraki.w && onceki.h === sonraki.h && onceki.hx === sonraki.hx && onceki.hy === sonraki.hy && onceki.hw === sonraki.hw && onceki.hh === sonraki.hh ? onceki : sonraki, ); }; olc(); const ro = new ResizeObserver(olc); ro.observe(svg); const h = haric?.current; if (h) ro.observe(h); return () => ro.disconnect(); }, [haric]); const MAKRO = 4; const PAY = 14; // hariç öğenin çevresinde bırakılan boşluk (px) const pixels = useMemo(() => { if (!olcu || olcu.w === 0 || olcu.h === 0) return null; const { w, h, hx, hy, hw, hh } = olcu; const cols = Math.ceil(w / CELL); const rows = Math.ceil(h / CELL); const kisa = Math.min(w, h); const sonuc: ReactNode[] = []; let accentIdx = 0; const kare = ( x: number, y: number, n: number, key: string, accent: boolean, ) => { const boy = n * CELL - 2 * INSET; const shared = { x: x * CELL + INSET, y: y * CELL + INSET, width: boy, height: boy, rx: 2 * n, }; if (accent) { return ( ); } // İri kare daha silik: yazının yanında bağırmasın const op = opacityFor(x, y) * (n === 2 ? 0.9 : n === 3 ? 0.7 : 0.55); return ( ); }; const haricte = (px: number, py: number) => hw > 0 && px >= hx - PAY && px <= hx + hw + PAY && py >= hy - PAY && py <= hy + hh + PAY; for (let my = 0; my < rows / MAKRO; my++) { for (let mx = 0; mx < cols / MAKRO; mx++) { const ox = mx * MAKRO; const oy = my * MAKRO; const cxPx = (ox + MAKRO / 2) * CELL; const cyPx = (oy + MAKRO / 2) * CELL; if (haricte(cxPx, cyPx)) continue; // En yakın kenara uzaklık, kısa kenarın oranı olarak (0 kenar, 0.5 orta) const k = Math.min(cxPx, w - cxPx, cyPx, h - cyPx) / kisa; // Yalnız çerçevede, kenara doğru artan doluluk; orta boş if (k >= kenar) continue; const density = (0.06 + 0.5 * (1 - k / kenar) ** 1.6) * yogunluk; if (hash(mx, my, seed) > density) continue; const r = hash(mx, my, seed + 7); // 2–4 hücre: orta boy sık, iri seyrek const n = r < 0.5 ? 2 : r < 0.85 ? 3 : 4; const serbest = MAKRO - n; const kx = ox + Math.floor(hash(mx, my, seed + 11) * (serbest + 1)); const ky = oy + Math.floor(hash(mx, my, seed + 13) * (serbest + 1)); const accent = n === 2 && hash(mx, my, seed + 99) > 0.95; sonuc.push(kare(kx, ky, n, `m-${mx}-${my}`, accent)); } } return sonuc; }, [olcu, seed, yogunluk, kenar]); return ( ); } /** * Bölümler arasında sert border çizgileri yerine kullanılan, ortada * yoğunlaşıp kenarlara doğru dağılan piksel ayraç. Hero'nun dot-matrix * dilini bölüm geçişlerine taşır. */ export function PixelDivider({ seed = 5, className, }: { seed?: number; className?: string; }) { const svgRef = useRef(null); usePixelHeat(svgRef); const COLS = 12; const ROWS = 2; // Deterministik çıktı — render başına yeniden üretme (PixelMasonry ile aynı) const pixels = useMemo(() => { const cx = (COLS - 1) / 2; const sonuc: ReactNode[] = []; let accentPlaced = false; for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { // Az sayıda, iri piksel: ortada yoğun, kenara doğru seyrelen küçük küme const dx = Math.abs(x - cx) / cx; const rowFalloff = y === 0 ? 0.9 : 0.35; const density = (1 - dx) ** 1.1 * rowFalloff; if (hash(x, y, seed) > density) continue; const accent = !accentPlaced && hash(x, y, seed + 99) > 0.8; if (accent) accentPlaced = true; sonuc.push( pixelRect(x, y, { key: `${x}-${y}`, accent, }), ); } } return sonuc; }, [seed]); return ( ); } /** Alt sayfa başlıklarını içerikten ayıran kompakt piksel imzası. */ export function PagePixelDivider({ seed = 5, className, }: { seed?: number; className?: string; }) { return ( ); } /** Küçük sabit süslerde (mark/korner) data-px öznitelikli rect. */ function fixedPixel(x: number, y: number, opacity: number) { return ( ); } /** PixelMark pikseli: kendi kendine, sırası geldiğinde ısınıp sönen rect. */ function markPixel(x: number, y: number, opacity: number, delay: number) { return ( ); } /** * Eyebrow rozetlerinde kullanılan 2×2'lik minik piksel imzası. * Pikseller imleç fırçasına katılmak yerine saat yönünde sırayla * ısınıp söner; ikisi aynı fill/fillOpacity'yi sürdüğü için çakışır. */ export function PixelMark({ className }: { className?: string }) { return ( ); } /** Kart köşelerine iliştirilen sessiz piksel kümesi. */ export function PixelCorner({ className }: { className?: string }) { const svgRef = useRef(null); usePixelHeat(svgRef, { radius: 28 }); return ( ); } /** * Bir sıranın yerini tek satır piksel şeridinde gösteren sessiz gösterge * (ör. URAP'ta 198 üniversite içinde 5.). Şerit soldan (1.) sağa * (sonuncu) dilimlere bölünür; sıranın düştüğü dilim hero'daki şehirler * gibi turuncu yanar, geri kalanı hero dokusunda durur ve imleç fırçasına * katılır. Dekoratiftir: anlamı yanındaki metin taşır (renk tek başına * anlam taşımaz), bu yüzden aria-hidden. */ export function PixelSira({ sira, toplam, hucre = 8, zemin = "acik", className, }: { sira: number; toplam: number; hucre?: number; /** "koyu": renkli dolu zemin üstünde (ör. turuncu gösterge karesi) — * işaret beyaz yanar, doku zeminin üstünde beyaz kalır. */ zemin?: "acik" | "koyu"; className?: string; }) { const svgRef = useRef(null); // Isı fırçası turuncuya ısıtır; turuncu zeminde görünmez, orada kapalı. usePixelHeat(svgRef, { radius: zemin === "koyu" ? 0 : 28 }); const yer = Math.min( hucre - 1, Math.max(0, Math.floor(((sira - 1) / toplam) * hucre)), ); return ( ); } /** * Piksel sahne (DESIGN.md §4.31): hero'daki dot-matrix Türkiye'nin dilini * küçük resimlere taşır. Harita gibi satır satır bir ızgaradan çizilir: * `#` doku pikseli, `-` silik piksel, `o` turuncu vurgu (hero'daki şehirler * gibi nabız atar), başka her karakter boş. Doku pikselleri imleç ısı * fırçasına katılır. Resim kartın söylediğini gösterir (süs değil); anlamı * yanındaki başlık taşır, bu yüzden aria-hidden. */ export function PixelSahne({ izgara, ton = "doku", yaricap = 26, className, }: { izgara: readonly string[]; /** "doku": hero haritasının silik mavisi. "dolgun": aynı doku koyu * tonda (0,64–1) — rakam gibi okunması gereken şekiller için. */ ton?: "doku" | "dolgun"; /** Isı fırçasının yarıçapı (viewBox birimi; hücre 10) */ yaricap?: number; className?: string; }) { const svgRef = useRef(null); usePixelHeat(svgRef, { radius: yaricap }); const cols = Math.max(...izgara.map((satir) => satir.length)); const pixels = useMemo(() => { const opaklik = (x: number, y: number) => ton === "dolgun" ? 0.4 + opacityFor(x, y) * 2 : opacityFor(x, y); const sonuc: ReactNode[] = []; let vurgu = 0; izgara.forEach((satir, y) => { for (let x = 0; x < satir.length; x++) { const c = satir[x]; if (c === "o") { sonuc.push( pixelRect(x, y, { key: `${x}-${y}`, accent: true, accentDelay: (vurgu++ % 8) * 0.45, }), ); } else if (c === "#" || c === "-") { const op = Math.round(opaklik(x, y) * (c === "-" ? 0.5 : 1) * 100) / 100; sonuc.push( , ); } } }); return sonuc; }, [izgara, ton]); return ( ); } // 5×7 dot-matrix rakamlar. Yeni bir rakam gerekirse ızgarası buraya eklenir. const RAKAMLAR = { 1: ["..#..", ".##..", "..#..", "..#..", "..#..", "..#..", ".###."], 2: [".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####"], 3: [".###.", "#...#", "....#", "..##.", "....#", "#...#", ".###."], } as const; /** Hero'nun piksel diliyle yazılmış iri rakam (ana sayfa adım numaraları). */ export function PixelRakam({ rakam, className, }: { rakam: keyof typeof RAKAMLAR; className?: string; }) { return ( ); } /** Bölüm başlıklarının üstünde duran piksel imzalı eyebrow rozeti. */ export function SectionEyebrow({ children, className, }: { children: ReactNode; className?: string; }) { return ( {children} ); }