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>
239 lines
8.0 KiB
TypeScript
239 lines
8.0 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef } from "react";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı.
|
||
// İki varyant:
|
||
//
|
||
// - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi →
|
||
// şeftalinin yeri, şeftali → turuncunun, turuncu → mavinin), üç adımda
|
||
// herkes başlangıç formuna döner. Uçuş sırasında kare hafifçe küçülüp
|
||
// solar, varışta toparlanır. Site header'daki logo hover'da bunu oynatır.
|
||
// - "nabiz": kareler yerlerinden ayrılmaz; 120° faz farkıyla dalga gibi
|
||
// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli).
|
||
//
|
||
// Performans notu: kareler SVG <rect> değil HTML <span>, animasyon da Web
|
||
// Animations API ile yalnızca `translate` / `scale` / `opacity` üzerinden
|
||
// yürür. Bu üçü compositor'da (GPU) oynar: ana iş parçacığı meşgulken
|
||
// (hidrasyon, sayfa geçişi, kaydırma) bile kare düşmez ve hover React'i
|
||
// yeniden render etmez. SVG iç elemanlarının transform'u her karede yeniden
|
||
// boyanır; o yüzden buraya geri dönme.
|
||
|
||
const KUTU = 180; // kaynak SVG'nin viewBox kenarı
|
||
|
||
const POZISYONLAR = [
|
||
[27, 56],
|
||
[91, 29],
|
||
[91, 95],
|
||
] as const;
|
||
|
||
const KARELER = [
|
||
{ boyut: 57, renk: "#3b82f6", baslangic: 0 },
|
||
{ boyut: 56, renk: "#fdc7a7", baslangic: 1 },
|
||
{ boyut: 56, renk: "#ff5a00", baslangic: 2 },
|
||
] as const;
|
||
|
||
// Rota: tam tur 3.6s — 3 etap × (%25 yol = 0.9s + %8.3 varışta soluklanma).
|
||
// 0.9s'lik yolculuk, nabız varyantının küçül-büyü temposuyla aynı hızda.
|
||
const ROTA_SURE = 3600;
|
||
// Kalkışlar kademeli: kareler eş zamanlı değil, danışman başlığındaki gibi
|
||
// sırayla (mavi → şeftali → turuncu) yola çıkıp küçülür. Gecikme yalnızca ilk
|
||
// turdan önce uygulanır, sonraki turlarda faz farkı kendiliğinden korunur.
|
||
const ROTA_GECIKME_ADIMI = 200;
|
||
const KONUM_ZAMANLARI = [0, 0.25, 1 / 3, 0.583, 2 / 3, 0.917, 1];
|
||
// Nabız (rota içi): her yolculuğun ortasında dibe vurur, varışta tam form.
|
||
const ROTA_NABIZ_ZAMANLARI = [
|
||
0, 0.125, 0.25, 1 / 3, 0.458, 0.583, 2 / 3, 0.792, 0.917, 1,
|
||
];
|
||
const ROTA_NABIZ_SCALE = [1, 0.84, 1, 1, 0.84, 1, 1, 0.84, 1, 1];
|
||
const ROTA_NABIZ_OPACITY = [1, 0.6, 1, 1, 0.6, 1, 1, 0.6, 1, 1];
|
||
|
||
// Yerinde nabız: kesintisiz tam-sinüs (dinlenme yok), delay'ler döngüyü üçe
|
||
// böler — biri büyürken sıradaki küçülür, hiçbir kare beklemez.
|
||
const NABIZ_SURE = 1800;
|
||
const NABIZ_GECIKMELER = [0, 600, 1200] as const;
|
||
|
||
const SINE = "cubic-bezier(0.37, 0, 0.63, 1)";
|
||
// Yolculuk: yumuşak kalkış, uzun ve yumuşak iniş (easeInOutCubic).
|
||
const YOL = "cubic-bezier(0.65, 0, 0.35, 1)";
|
||
// Hover bitince yerine oturma (easeOutQuint): hızlı toparlanır, sarsmadan durur.
|
||
const DONUS = "cubic-bezier(0.22, 1, 0.36, 1)";
|
||
const DONUS_SURE = 500;
|
||
|
||
/** Karenin `translate` değeri: yüzdeler karenin kendi boyutuna göredir, bu
|
||
* yüzden işaret hangi boyutta çizilirse çizilsin aynı oran korunur. */
|
||
function yer(pozisyon: number, boyut: number): string {
|
||
const [x, y] = POZISYONLAR[pozisyon % 3];
|
||
return `${(x / boyut) * 100}% ${(y / boyut) * 100}%`;
|
||
}
|
||
|
||
/** Kare için durak koordinatları: kendi yeri → sıradaki → ondan sonraki →
|
||
* kendi yeri. Duraklarda bekleme için her hedef iki kez tekrarlanır
|
||
* (KONUM_ZAMANLARI ile hizalı). */
|
||
function rota(baslangic: number, boyut: number): string[] {
|
||
const [p0, p1, p2] = [0, 1, 2].map((adim) => yer(baslangic + adim, boyut));
|
||
return [p0, p1, p1, p2, p2, p0, p0];
|
||
}
|
||
|
||
function donguyuBaslat(
|
||
el: HTMLElement,
|
||
i: number,
|
||
varyant: "rota" | "nabiz",
|
||
): Animation[] {
|
||
const k = KARELER[i];
|
||
if (varyant === "nabiz") {
|
||
return [
|
||
el.animate(
|
||
{ scale: ["1", "0.82", "1"], opacity: [1, 0.55, 1], easing: SINE },
|
||
{
|
||
duration: NABIZ_SURE,
|
||
delay: NABIZ_GECIKMELER[i],
|
||
iterations: Infinity,
|
||
},
|
||
),
|
||
];
|
||
}
|
||
const secenek = {
|
||
duration: ROTA_SURE,
|
||
delay: i * ROTA_GECIKME_ADIMI,
|
||
iterations: Infinity,
|
||
};
|
||
// Konum ve nabız ayrı animasyonlar: konum etabı tek parça eğriyle akar,
|
||
// ortasındaki küçülme onu ikiye bölüp duraksatmaz.
|
||
return [
|
||
el.animate(
|
||
{
|
||
translate: rota(k.baslangic, k.boyut),
|
||
offset: KONUM_ZAMANLARI,
|
||
easing: YOL,
|
||
},
|
||
secenek,
|
||
),
|
||
el.animate(
|
||
{
|
||
scale: ROTA_NABIZ_SCALE.map(String),
|
||
opacity: ROTA_NABIZ_OPACITY,
|
||
offset: ROTA_NABIZ_ZAMANLARI,
|
||
easing: SINE,
|
||
},
|
||
secenek,
|
||
),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Animasyonlu KolayTercih işareti. Varsayılanda koreografi sürekli döner;
|
||
* `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site
|
||
* header'daki logo linki) ve hover bitince kareler yumuşakça yerine oturur.
|
||
*/
|
||
export function AnimasyonluMark({
|
||
className,
|
||
sadeceHover = false,
|
||
varyant = "rota",
|
||
renk = "marka",
|
||
}: {
|
||
className?: string;
|
||
sadeceHover?: boolean;
|
||
varyant?: "rota" | "nabiz";
|
||
/** "marka": logodaki üç renk; "turuncu": tüm kareler sitenin CTA turuncusu
|
||
* (orange-500) — ör. danışman panel başlığı. */
|
||
renk?: "marka" | "turuncu";
|
||
}) {
|
||
const kokRef = useRef<HTMLSpanElement>(null);
|
||
|
||
useEffect(() => {
|
||
const kok = kokRef.current;
|
||
if (!kok || typeof kok.animate !== "function") return;
|
||
const azHareket = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||
const kareler = Array.from(kok.children) as HTMLElement[];
|
||
|
||
let dongu: Animation[] = [];
|
||
let donus: Animation[] = [];
|
||
let istekli = false;
|
||
|
||
const baslat = () => {
|
||
if (istekli) return;
|
||
istekli = true;
|
||
if (azHareket.matches) return;
|
||
// Kareler hâlâ yerine dönüyorsa (hızlı çık-gir) dönüşün bitmesi
|
||
// beklenir; döngü hep yerinden başlar, sıçrama olmaz.
|
||
Promise.all(donus.map((a) => a.finished))
|
||
.then(() => {
|
||
if (!istekli || dongu.length > 0) return;
|
||
dongu = kareler.flatMap((el, i) => donguyuBaslat(el, i, varyant));
|
||
})
|
||
.catch(() => {});
|
||
};
|
||
|
||
const durdur = () => {
|
||
istekli = false;
|
||
if (dongu.length === 0) return;
|
||
// Kareler o an neredeyse oradan yerlerine süzülür.
|
||
const anlik = kareler.map((el) => {
|
||
const cs = getComputedStyle(el);
|
||
return { translate: cs.translate, scale: cs.scale, opacity: cs.opacity };
|
||
});
|
||
dongu.forEach((a) => a.cancel());
|
||
dongu = [];
|
||
donus = kareler.map((el, i) =>
|
||
el.animate(
|
||
[
|
||
anlik[i],
|
||
{
|
||
translate: yer(KARELER[i].baslangic, KARELER[i].boyut),
|
||
scale: "1",
|
||
opacity: 1,
|
||
},
|
||
],
|
||
{ duration: DONUS_SURE, easing: DONUS },
|
||
),
|
||
);
|
||
};
|
||
|
||
if (!sadeceHover) {
|
||
baslat();
|
||
return () => {
|
||
dongu.forEach((a) => a.cancel());
|
||
};
|
||
}
|
||
|
||
// Hover hedefi işaretin kendisi değil, en yakın `group` ebeveyni (site
|
||
// header'da logo + "KolayTercih" yazısını saran Link): yazının üzerine
|
||
// gelindiğinde de animasyon başlar. `group`'suz kullanımda köke düşer.
|
||
const hedef = kok.closest(".group") ?? kok;
|
||
hedef.addEventListener("mouseenter", baslat);
|
||
hedef.addEventListener("mouseleave", durdur);
|
||
return () => {
|
||
hedef.removeEventListener("mouseenter", baslat);
|
||
hedef.removeEventListener("mouseleave", durdur);
|
||
[...dongu, ...donus].forEach((a) => a.cancel());
|
||
};
|
||
}, [sadeceHover, varyant]);
|
||
|
||
return (
|
||
<span
|
||
ref={kokRef}
|
||
aria-hidden="true"
|
||
className={cn("relative block shrink-0", className)}
|
||
>
|
||
{KARELER.map((k) => (
|
||
<span
|
||
key={k.renk}
|
||
className={cn(
|
||
"absolute top-0 left-0 block",
|
||
renk === "turuncu" && "bg-orange-500",
|
||
)}
|
||
style={{
|
||
width: `${(k.boyut / KUTU) * 100}%`,
|
||
height: `${(k.boyut / KUTU) * 100}%`,
|
||
borderRadius: `${(13 / k.boyut) * 100}%`,
|
||
backgroundColor: renk === "marka" ? k.renk : undefined,
|
||
translate: yer(k.baslangic, k.boyut),
|
||
}}
|
||
/>
|
||
))}
|
||
</span>
|
||
);
|
||
}
|