fix(tasarim): şehir haritası kartı arka plan gibi kaplar, kenarlar erir; pikseller azaldı
All checks were successful
Deploy / deploy (push) Successful in 4m34s
All checks were successful
Deploy / deploy (push) Successful in 4m34s
- 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>
This commit is contained in:
@@ -5,9 +5,10 @@
|
||||
* 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 72 sütun (her il en az 2×2 kare) ve
|
||||
* bulunulan ilin kareleri turuncu, hücrenin %88'i kadar iri (Bilal, 24 Eyl
|
||||
* 2026: "C'deki gibi piksel olabilir"), sırayla belirir ve nabız atar.
|
||||
* (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
|
||||
@@ -16,8 +17,12 @@
|
||||
* 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, SVG ile aynı oranda) güçlü ease-out ile kayar — kareler
|
||||
* büyür, il kadraja gelir. Hareketi azalt açıkken kutuya doğrudan atlanır.
|
||||
* 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";
|
||||
@@ -38,28 +43,28 @@ 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;
|
||||
// Güney kıyının altına seyrelerek dağılan serpinti derinliği (hücre)
|
||||
const SERPINTI = 5;
|
||||
const VIEW_W = SUTUN * CELL;
|
||||
const VIEW_H = (SATIR + SERPINTI) * 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)
|
||||
const ODAK_GECIKME = 0.7;
|
||||
const ODAK_SURE = 1.1;
|
||||
const ODAK_ZOOM = 2.4;
|
||||
const ODAK_PAY = 3;
|
||||
const ODAK_PAY = 2;
|
||||
const ODAK_EASE = [0.23, 1, 0.32, 1] as const;
|
||||
|
||||
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];
|
||||
|
||||
/**
|
||||
* Vurgu hücrelerinin çevresine, SVG ile aynı oranda ve en çok ODAK_ZOOM kat
|
||||
* büyüten odak kutusu; harita dışına taşmayacak şekilde kaydırılır.
|
||||
*/
|
||||
function odakKutusu(hucreler: readonly IzgaraHucre[], zoom = ODAK_ZOOM): Kutu {
|
||||
if (hucreler.length === 0) return TAM_KUTU;
|
||||
/** 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;
|
||||
@@ -67,9 +72,21 @@ function odakKutusu(hucreler: readonly IzgaraHucre[], zoom = ODAK_ZOOM): Kutu {
|
||||
if (r < r0) r0 = r;
|
||||
if (r > r1) r1 = r;
|
||||
}
|
||||
const x0 = (q0 - ODAK_PAY) * CELL, x1 = (q1 + 1 + ODAK_PAY) * CELL;
|
||||
const y0 = (r0 - ODAK_PAY) * CELL, y1 = (r1 + 1 + ODAK_PAY) * CELL;
|
||||
const oran = VIEW_W / VIEW_H;
|
||||
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) {
|
||||
@@ -77,9 +94,7 @@ function odakKutusu(hucreler: readonly IzgaraHucre[], zoom = ODAK_ZOOM): Kutu {
|
||||
w = h * oran;
|
||||
}
|
||||
const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
|
||||
const x = Math.min(Math.max(cx - w / 2, 0), VIEW_W - w);
|
||||
const y = Math.min(Math.max(cy - h / 2, 0), VIEW_H - h);
|
||||
return [x, y, w, h];
|
||||
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];
|
||||
@@ -89,12 +104,6 @@ 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];
|
||||
|
||||
// Deterministik sözde-rastgele (hero ile aynı): server ve client aynı deseni üretir
|
||||
const hash = (x: number, y: number, seed = 0) => {
|
||||
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
|
||||
return n - Math.floor(n);
|
||||
};
|
||||
|
||||
const f = (n: number) => n.toFixed(2);
|
||||
|
||||
function zeminKare(q: number, r: number, op: number) {
|
||||
@@ -116,25 +125,9 @@ function markupUret(
|
||||
): Harita {
|
||||
const parts: string[] = [];
|
||||
const vurguSet = new Set(vurgu.map(([q, r]) => `${q},${r}`));
|
||||
const enAlt: number[] = Array(SUTUN).fill(-1);
|
||||
for (const [q, r] of zemin) {
|
||||
if (vurguSet.has(`${q},${r}`)) continue;
|
||||
parts.push(zeminKare(q, r, opacityFor(q, r)));
|
||||
if (r > enAlt[q]) enAlt[q] = r;
|
||||
}
|
||||
for (const [q, r] of vurgu) if (r > enAlt[q]) enAlt[q] = r;
|
||||
// Kıyının altına serpinti (hero ile aynı kural): kıyıdan uzaklaştıkça
|
||||
// seyrelen, derinlikle sönen kareler; ısı fırçasına katılırlar.
|
||||
for (let q = 0; q < SUTUN; q++) {
|
||||
if (enAlt[q] < 0) continue;
|
||||
for (let d = 1; d <= SERPINTI; d++) {
|
||||
const r = enAlt[q] + d;
|
||||
if (r >= SATIR + SERPINTI) break;
|
||||
const density = 0.05 + 0.4 * (1 - (d - 1) / SERPINTI) ** 2;
|
||||
if (hash(q, r) > density) continue;
|
||||
const op = Math.round(opacityFor(q, r) * (1 - d / (SERPINTI + 1)) * 100) / 100;
|
||||
parts.push(zeminKare(q, r, op));
|
||||
}
|
||||
}
|
||||
// Vurgu: sırayla belirir (kt-piksel-belir), grup hâlinde nabız atar
|
||||
parts.push(
|
||||
@@ -151,12 +144,12 @@ function markupUret(
|
||||
parts.push("</g>");
|
||||
// Odak: il kareleri; ülke haritasında şehir işareti (2×2) ya da ülkenin kendisi
|
||||
const odak = isaret
|
||||
? odakKutusu([isaret, [isaret[0] + 1, isaret[1] + 1]], 1.8)
|
||||
: odakKutusu(vurgu);
|
||||
return { html: parts.join(""), odak };
|
||||
? { 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; odak: Kutu };
|
||||
type Harita = { html: string; sinir: Sinir | null; zoom: number };
|
||||
|
||||
const onbellek = new Map<string, Harita>();
|
||||
function markup(anahtar: string, uret: () => Harita): Harita {
|
||||
@@ -170,24 +163,37 @@ function markup(anahtar: string, uret: () => Harita): Harita {
|
||||
|
||||
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,
|
||||
}: {
|
||||
harita: Harita;
|
||||
etiket: string;
|
||||
className?: string;
|
||||
}) {
|
||||
doldur = false,
|
||||
odakHiza,
|
||||
}: HaritaProps & { harita: Harita }) {
|
||||
const svgRef = useRef<SVGSVGElement>(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 { odak } = harita;
|
||||
const { sinir, zoom } = harita;
|
||||
const hizaLg = odakHiza?.lg ?? ORTA;
|
||||
const hizaDar = odakHiza?.dar ?? ORTA;
|
||||
useEffect(() => {
|
||||
const svg = svgRef.current;
|
||||
if (!svg || odak === TAM_KUTU) return;
|
||||
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);
|
||||
@@ -210,13 +216,14 @@ function PikselSvg({
|
||||
kontrol.stop();
|
||||
yaz(TAM_KUTU);
|
||||
};
|
||||
}, [odak]);
|
||||
}, [sinir, zoom, hizaLg, hizaDar]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={svgRef}
|
||||
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
||||
className={cn("h-auto w-full text-primary", className)}
|
||||
preserveAspectRatio={doldur ? "xMidYMid slice" : "xMidYMid meet"}
|
||||
className={cn(doldur ? "h-full w-full" : "h-auto w-full", "text-primary", className)}
|
||||
role="img"
|
||||
aria-label={etiket}
|
||||
// Statik, deterministik markup — her render'da yeniden kurulmaz
|
||||
@@ -230,17 +237,12 @@ const TUM_IL_HUCRELERI = Object.values(IL_HUCRELERI).flat();
|
||||
/** Türkiye; `il` (DB biçimi, ör. "İZMİR") turuncu karelerle yanar. */
|
||||
export function TurkiyePikselHaritasi({
|
||||
il,
|
||||
etiket,
|
||||
className,
|
||||
}: {
|
||||
il: string;
|
||||
etiket: string;
|
||||
className?: string;
|
||||
}) {
|
||||
...props
|
||||
}: HaritaProps & { il: string }) {
|
||||
const harita = markup(`tr:${il}`, () =>
|
||||
markupUret(TUM_IL_HUCRELERI, IL_HUCRELERI[il] ?? [], null),
|
||||
);
|
||||
return <PikselSvg harita={harita} etiket={etiket} className={className} />;
|
||||
return <PikselSvg harita={harita} {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,14 +252,11 @@ export function TurkiyePikselHaritasi({
|
||||
export function UlkePikselHaritasi({
|
||||
ulke,
|
||||
sehir,
|
||||
etiket,
|
||||
className,
|
||||
}: {
|
||||
...props
|
||||
}: HaritaProps & {
|
||||
ulke: UlkeKodu;
|
||||
/** viewBox koordinatı (harita-ulkeler'deki proje ile) */
|
||||
sehir: { x: number; y: number } | null;
|
||||
etiket: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const harita = markup(
|
||||
`ulke:${ulke}:${sehir ? `${Math.round(sehir.x)},${Math.round(sehir.y)}` : "-"}`,
|
||||
@@ -271,27 +270,26 @@ export function UlkePikselHaritasi({
|
||||
return markupUret(h.baglam, h.ulke, null);
|
||||
},
|
||||
);
|
||||
return <PikselSvg harita={harita} etiket={etiket} className={className} />;
|
||||
return <PikselSvg harita={harita} {...props} />;
|
||||
}
|
||||
|
||||
/** Şehir sayfası: il değerinden Türkiye ya da ülke haritasını seçer. */
|
||||
export function SehirPikselHaritasi({
|
||||
il,
|
||||
ad,
|
||||
className,
|
||||
}: {
|
||||
...props
|
||||
}: Omit<HaritaProps, "etiket"> & {
|
||||
/** DB biçiminde il ("İSTANBUL", "LEFKOŞA", "KIBRIS") */
|
||||
il: string;
|
||||
/** Görünen ad (etiket için) */
|
||||
ad: string;
|
||||
className?: string;
|
||||
}) {
|
||||
if (IL_HUCRELERI[il]) {
|
||||
return (
|
||||
<TurkiyePikselHaritasi
|
||||
il={il}
|
||||
etiket={`${ad} Türkiye haritasında`}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -302,7 +300,7 @@ export function SehirPikselHaritasi({
|
||||
ulke={ulke.ulke}
|
||||
sehir={ulke.sehir ? { x: ulke.sehir.x, y: ulke.sehir.y } : null}
|
||||
etiket={`${ad} ${ulke.ulkeAd} haritasında`}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user