Piksel harita ve logo animasyonu iyileştirmeleri
All checks were successful
Deploy / deploy (push) Successful in 7m52s

Ş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 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-04 14:00:34 +03:00
parent 9c02b810e7
commit ab0b3a047f
7 changed files with 329 additions and 180 deletions

View File

@@ -195,40 +195,61 @@ function PikselSvg({
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.
// 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

View File

@@ -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(
`<rect ${rectAttrs(city.x, city.y)} class="animate-pulse fill-orange-500/70 motion-reduce:animate-none" style="animation-duration:3.6s;animation-delay:${i * 0.45}s"/>`,
@@ -150,7 +153,7 @@ const MAP_MARKUP = (() => {
export function TurkeyPixelMap({ className }: { className?: string }) {
const svgRef = useRef<SVGSVGElement>(null);
usePixelHeat(svgRef);
usePixelHeat(svgRef, { kirpisma: true });
return (
<svg

View File

@@ -442,15 +442,21 @@ export function CtaSiraForm({
href={sonucHref()}
aria-label="Sıralamana uygun programları gör"
title="Sıralamana uygun programları gör"
// 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.
className="group flex size-12 shrink-0 items-center justify-center rounded-full bg-orange-500 text-white transition-transform duration-200 after:absolute after:inset-0 hover:scale-[1.04] active:scale-[0.95] sm:size-14 lg:order-3"
// Gerilmiş link (after:inset-0) bandın tamamını kaplar. Ölçek
// linkin KENDİSİNE verilmez: transform, ::after için kapsayan
// blok olur, alan 48 px'lik daireye çöker, imleç linkten düşer
// ve hover açılıp kapanarak titrer. Büyüyen içteki dairedir.
className="group flex shrink-0 lg:order-3 after:absolute after:inset-0"
>
<ArrowRight
className="size-6 transition-transform duration-200 group-hover:translate-x-0.5 sm:size-7"
aria-hidden
/>
{/* 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. */}
<span className="flex size-12 items-center justify-center rounded-full bg-orange-500 text-white transition-transform duration-200 group-hover:scale-[1.04] group-active:scale-[0.95] sm:size-14">
<ArrowRight
className="size-6 transition-transform duration-200 group-hover:translate-x-0.5 sm:size-7"
aria-hidden
/>
</span>
</Link>
</div>
</div>