feat(tasarim): şehir sayfasına hero dilinde piksel harita; üniversite konum kartı da aynı harita
All checks were successful
Deploy / deploy (push) Successful in 4m39s

- scripts/il-izgara-uret.ts → src/lib/il-izgara-verisi.ts: 81 il ve 7 ülke
  kontürü hero'nun dot-matrix ızgarasına (72×31) rasterize; küçük iller 2×2 garanti.
- SehirPikselHaritasi: iri kareler, altı opaklık, kıyı serpintisi, usePixelHeat
  ısı fırçası; bulunulan il iri turuncu karelerle sırayla belirir (kt-piksel-belir)
  ve nabız atar; KKTC/yurt dışında şehir merkezinde 2×2 iri kare.
- Şehir sayfası başlığı beyaz piksel dokulu yüzeyde (SehirYuzeyi), harita sağda/altta.
- UniversiteKonumHaritasi düz SVG yerine aynı piksel haritayı kullanır.
- DESIGN.md §4.12/§4.17/§10.8 güncellendi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 14:56:11 +03:00
parent 259534fa9c
commit eefac56659
8 changed files with 671 additions and 108 deletions

170
scripts/il-izgara-uret.ts Normal file
View File

@@ -0,0 +1,170 @@
// Şehir haritası (DESIGN.md §4.17) için il ve ülke kontürlerini hero'nun
// dot-matrix ızgarasına rasterize eder. Hero'daki TurkeyPixelMap 64 sütun;
// burada 72 sütun ki her il en az birkaç kare alsın (Yalova, Kilis gibi
// küçük iller için 2×2 blok garantisi vardır). Çıktı
// src/lib/il-izgara-verisi.ts dosyasına yazılır ve commit'lenir.
//
// Kaynak: turkey-map-react il path'leri (viewBox 1050×585, üstten 138 kırpık)
// ve harita-ulkeler-verisi.ts ülke kontürleri (1050×447). Hücre merkezi
// poligonun içindeyse hücre o ile aittir (ray casting; Bézier eğrileri uç
// noktalarıyla düzleştirilir — hücre ölçeğinde fark etmez).
//
// Çalıştırma: pnpm tsx scripts/il-izgara-uret.ts
import fs from "node:fs";
import path from "node:path";
import { cities } from "turkey-map-react/lib/data";
import { normalizeIlAdi, HARITA_UST_KIRPMA, HARITA_GENISLIK, HARITA_YUKSEKLIK } from "../src/lib/harita";
import { ULKE_HARITALARI } from "../src/lib/harita-ulkeler-verisi";
const SUTUN = 72;
const HUCRE = HARITA_GENISLIK / SUTUN;
const YUKSEKLIK = HARITA_YUKSEKLIK - HARITA_UST_KIRPMA; // 447
const SATIR = Math.ceil(YUKSEKLIK / HUCRE);
type Nokta = [number, number];
type Hucre = [number, number];
function poligonlar(d: string): Nokta[][] {
const toks = d.match(/[MLHVCZmlhvcz]|-?\d*\.?\d+(?:e-?\d+)?/g) ?? [];
const polys: Nokta[][] = [];
let cur: Nokta[] = [];
let x = 0;
let y = 0;
let cmd = "";
let i = 0;
const num = () => Number(toks[i++]);
const kapat = () => {
if (cur.length > 2) polys.push(cur);
cur = [];
};
while (i < toks.length) {
const t = toks[i];
if (/[A-Za-z]/.test(t)) {
cmd = t;
i++;
if (cmd === "Z" || cmd === "z") kapat();
continue;
}
switch (cmd) {
case "M": x = num(); y = num(); kapat(); cur = [[x, y]]; cmd = "L"; break;
case "m": x += num(); y += num(); kapat(); cur = [[x, y]]; cmd = "l"; break;
case "L": x = num(); y = num(); cur.push([x, y]); break;
case "l": x += num(); y += num(); cur.push([x, y]); break;
case "H": x = num(); cur.push([x, y]); break;
case "h": x += num(); cur.push([x, y]); break;
case "V": y = num(); cur.push([x, y]); break;
case "v": y += num(); cur.push([x, y]); break;
case "C": num(); num(); num(); num(); x = num(); y = num(); cur.push([x, y]); break;
case "c": num(); num(); num(); num(); x += num(); y += num(); cur.push([x, y]); break;
default: i++;
}
}
kapat();
return polys;
}
function icinde(px: number, py: number, poly: Nokta[]): boolean {
let ic = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const [xi, yi] = poly[i];
const [xj, yj] = poly[j];
if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) ic = !ic;
}
return ic;
}
/** Her hücre merkezini ilk eşleşen bölgeye verir; sıra öncelik demektir. */
function rasterize(
bolgeler: { ad: string; polys: Nokta[][] }[],
ustKirpma: number,
): Map<string, Hucre[]> {
const sonuc = new Map<string, Hucre[]>();
for (let r = 0; r < SATIR; r++) {
for (let q = 0; q < SUTUN; q++) {
const px = (q + 0.5) * HUCRE;
const py = ustKirpma + (r + 0.5) * HUCRE;
const b = bolgeler.find((b) => b.polys.some((p) => icinde(px, py, p)));
if (!b) continue;
const liste = sonuc.get(b.ad) ?? [];
liste.push([q, r]);
sonuc.set(b.ad, liste);
}
}
return sonuc;
}
// ---- Türkiye illeri
const iller = cities.map((c) => ({ ad: normalizeIlAdi(c.name), polys: poligonlar(c.path) }));
const ilHucre = rasterize(iller, HARITA_UST_KIRPMA);
// Küçük il garantisi: 4 hücreden azı olan il bbox merkezinde 2×2 blok alır,
// o hücreler komşulardan düşer.
for (const il of iller) {
const mevcut = ilHucre.get(il.ad) ?? [];
if (mevcut.length >= 4) continue;
const pts = il.polys.flat();
const cx = (Math.min(...pts.map((p) => p[0])) + Math.max(...pts.map((p) => p[0]))) / 2;
const cy = (Math.min(...pts.map((p) => p[1])) + Math.max(...pts.map((p) => p[1]))) / 2;
const q0 = Math.min(SUTUN - 2, Math.max(0, Math.round(cx / HUCRE) - 1));
const r0 = Math.min(SATIR - 2, Math.max(0, Math.round((cy - HARITA_UST_KIRPMA) / HUCRE) - 1));
const blok: Hucre[] = [[q0, r0], [q0 + 1, r0], [q0, r0 + 1], [q0 + 1, r0 + 1]];
const anahtar = new Set(blok.map(([q, r]) => `${q},${r}`));
for (const [ad, liste] of ilHucre) {
if (ad === il.ad) continue;
ilHucre.set(ad, liste.filter(([q, r]) => !anahtar.has(`${q},${r}`)));
}
const birlesik = new Map(mevcut.map((h) => [`${h[0]},${h[1]}`, h]));
for (const h of blok) birlesik.set(`${h[0]},${h[1]}`, h);
ilHucre.set(il.ad, [...birlesik.values()]);
}
// ---- Ülkeler (KKTC, yurt dışı): ülke öncelikli, bağlam ikincil
const ulkeHucre: Record<string, { ulke: Hucre[]; baglam: Hucre[] }> = {};
for (const [kod, harita] of Object.entries(ULKE_HARITALARI)) {
const r = rasterize(
[
{ ad: "ulke", polys: harita.ulke.flatMap(poligonlar) },
{ ad: "baglam", polys: harita.baglam.flatMap(poligonlar) },
],
0,
);
ulkeHucre[kod] = { ulke: r.get("ulke") ?? [], baglam: r.get("baglam") ?? [] };
}
const enKucuk = [...ilHucre.entries()].sort((a, b) => a[1].length - b[1].length).slice(0, 5);
console.log(
`${SUTUN}×${SATIR}, hücre ${HUCRE.toFixed(3)}; kara ${[...ilHucre.values()].reduce((a, b) => a + b.length, 0)}; en küçük: ${enKucuk.map(([a, l]) => `${a}:${l.length}`).join(" ")}`,
);
const satirlar = [...ilHucre.entries()]
.sort((a, b) => a[0].localeCompare(b[0], "tr-TR"))
.map(([ad, liste]) => ` ${JSON.stringify(ad)}: ${JSON.stringify(liste)},`);
const ulkeSatirlar = Object.entries(ulkeHucre).map(
([kod, v]) => ` ${JSON.stringify(kod)}: { ulke: ${JSON.stringify(v.ulke)}, baglam: ${JSON.stringify(v.baglam)} },`,
);
const cikti = `// Bu dosya scripts/il-izgara-uret.ts tarafından üretildi — elle düzenleme.
// Hero'nun dot-matrix ızgarasında (${SUTUN} sütun × ${SATIR} satır, hücre ${HUCRE.toFixed(4)}
// viewBox birimi) her ilin ve ülkenin kapladığı hücreler [sütun, satır].
import type { UlkeKodu } from "./harita-ulkeler-verisi";
export const IL_IZGARA = {
sutun: ${SUTUN},
satir: ${SATIR},
hucre: ${HUCRE.toFixed(4)},
} as const;
export type IzgaraHucre = readonly [number, number];
/** Anahtar DB biçiminde il adı (normalizeIlAdi). */
export const IL_HUCRELERI: Record<string, readonly IzgaraHucre[]> = {
${satirlar.join("\n")}
};
export const ULKE_HUCRELERI: Record<UlkeKodu, { ulke: readonly IzgaraHucre[]; baglam: readonly IzgaraHucre[] }> = {
${ulkeSatirlar.join("\n")}
};
`;
const hedef = path.join(process.cwd(), "src/lib/il-izgara-verisi.ts");
fs.writeFileSync(hedef, cikti);
console.log("yazıldı:", hedef, `${(cikti.length / 1024).toFixed(0)} KB`);