Piksel harita ve logo animasyonu iyileştirmeleri
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Ş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:
@@ -341,6 +341,11 @@
|
|||||||
animation: kt-piksel-belir 0.42s cubic-bezier(0.23, 1, 0.32, 1) both;
|
animation: kt-piksel-belir 0.42s cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Harita görünüme girene kadar bekler (PikselSvg data-gorundu yazar). */
|
||||||
|
.kt-piksel-harita:not([data-gorundu]) .kt-piksel-belir {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.kt-piksel-belir {
|
.kt-piksel-belir {
|
||||||
animation: none;
|
animation: none;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { motion, useReducedMotion } from "motion/react";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı (Motion).
|
// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı.
|
||||||
// İki varyant:
|
// İki varyant:
|
||||||
//
|
//
|
||||||
// - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi →
|
// - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi →
|
||||||
@@ -14,9 +13,14 @@ import { cn } from "@/lib/utils";
|
|||||||
// - "nabiz": kareler yerlerinden ayrılmaz; 120° faz farkıyla dalga gibi
|
// - "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).
|
// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli).
|
||||||
//
|
//
|
||||||
// Not: motion.rect'te `x`/`y` transform değeridir (attribute değil) — kareler
|
// Performans notu: kareler SVG <rect> değil HTML <span>, animasyon da Web
|
||||||
// attribute yerine translate ile konumlanır; scale, transform-box: fill-box
|
// Animations API ile yalnızca `translate` / `scale` / `opacity` üzerinden
|
||||||
// sayesinde karenin kendi merkezinden uygulanır.
|
// 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 = [
|
const POZISYONLAR = [
|
||||||
[27, 56],
|
[27, 56],
|
||||||
@@ -32,10 +36,11 @@ const KARELER = [
|
|||||||
|
|
||||||
// Rota: tam tur 3.6s — 3 etap × (%25 yol = 0.9s + %8.3 varışta soluklanma).
|
// 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.
|
// 0.9s'lik yolculuk, nabız varyantının küçül-büyü temposuyla aynı hızda.
|
||||||
const ROTA_SURE = 3.6;
|
const ROTA_SURE = 3600;
|
||||||
// Kalkışlar kademeli: kareler eş zamanlı değil, danışman başlığındaki gibi
|
// 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.
|
// sırayla (mavi → şeftali → turuncu) yola çıkıp küçülür. Gecikme yalnızca ilk
|
||||||
const ROTA_GECIKME_ADIMI = 0.2;
|
// 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];
|
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.
|
// Nabız (rota içi): her yolculuğun ortasında dibe vurur, varışta tam form.
|
||||||
const ROTA_NABIZ_ZAMANLARI = [
|
const ROTA_NABIZ_ZAMANLARI = [
|
||||||
@@ -46,20 +51,77 @@ 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
|
// 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.
|
// böler — biri büyürken sıradaki küçülür, hiçbir kare beklemez.
|
||||||
const NABIZ_SURE = 1.8;
|
const NABIZ_SURE = 1800;
|
||||||
const NABIZ_GECIKMELER = [0, 0.6, 1.2] as const;
|
const NABIZ_GECIKMELER = [0, 600, 1200] as const;
|
||||||
const SINE = [0.37, 0, 0.63, 1] as const;
|
|
||||||
|
|
||||||
/** Kare i için durak koordinatları: kendi yeri → sıradaki → ondan sonraki →
|
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
|
* kendi yeri. Duraklarda bekleme için her hedef iki kez tekrarlanır
|
||||||
* (KONUM_ZAMANLARI ile hizalı). */
|
* (KONUM_ZAMANLARI ile hizalı). */
|
||||||
function rota(baslangic: number, eksen: 0 | 1): number[] {
|
function rota(baslangic: number, boyut: number): string[] {
|
||||||
const [p0, p1, p2] = [0, 1, 2].map(
|
const [p0, p1, p2] = [0, 1, 2].map((adim) => yer(baslangic + adim, boyut));
|
||||||
(adim) => POZISYONLAR[(baslangic + adim) % 3][eksen],
|
|
||||||
);
|
|
||||||
return [p0, p1, p1, p2, p2, p0, p0];
|
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;
|
* Animasyonlu KolayTercih işareti. Varsayılanda koreografi sürekli döner;
|
||||||
* `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site
|
* `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site
|
||||||
@@ -78,116 +140,99 @@ export function AnimasyonluMark({
|
|||||||
* (orange-500) — ör. danışman panel başlığı. */
|
* (orange-500) — ör. danışman panel başlığı. */
|
||||||
renk?: "marka" | "turuncu";
|
renk?: "marka" | "turuncu";
|
||||||
}) {
|
}) {
|
||||||
const [hover, setHover] = useState(false);
|
const kokRef = useRef<HTMLSpanElement>(null);
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
|
||||||
const azHareket = useReducedMotion();
|
|
||||||
const aktif = !azHareket && (!sadeceHover || hover);
|
|
||||||
|
|
||||||
// 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 svg'ye düşer.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sadeceHover) return;
|
const kok = kokRef.current;
|
||||||
const hedef = svgRef.current?.closest(".group") ?? svgRef.current;
|
if (!kok || typeof kok.animate !== "function") return;
|
||||||
if (!hedef) return;
|
const azHareket = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||||
const ac = () => setHover(true);
|
const kareler = Array.from(kok.children) as HTMLElement[];
|
||||||
const kapat = () => setHover(false);
|
|
||||||
hedef.addEventListener("mouseenter", ac);
|
let dongu: Animation[] = [];
|
||||||
hedef.addEventListener("mouseleave", kapat);
|
let donus: Animation[] = [];
|
||||||
return () => {
|
let istekli = false;
|
||||||
hedef.removeEventListener("mouseenter", ac);
|
|
||||||
hedef.removeEventListener("mouseleave", kapat);
|
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(() => {});
|
||||||
};
|
};
|
||||||
}, [sadeceHover]);
|
|
||||||
|
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 (
|
return (
|
||||||
<svg
|
<span
|
||||||
ref={svgRef}
|
ref={kokRef}
|
||||||
viewBox="0 0 180 180"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
focusable="false"
|
className={cn("relative block shrink-0", className)}
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
>
|
>
|
||||||
{KARELER.map((k, i) => {
|
{KARELER.map((k) => (
|
||||||
const [px, py] = POZISYONLAR[k.baslangic];
|
<span
|
||||||
const duruk = { x: px, y: py, scale: 1, opacity: 1 };
|
key={k.renk}
|
||||||
|
className={cn(
|
||||||
const animate = !aktif
|
"absolute top-0 left-0 block",
|
||||||
? duruk
|
renk === "turuncu" && "bg-orange-500",
|
||||||
: varyant === "rota"
|
)}
|
||||||
? {
|
style={{
|
||||||
x: rota(k.baslangic, 0),
|
width: `${(k.boyut / KUTU) * 100}%`,
|
||||||
y: rota(k.baslangic, 1),
|
height: `${(k.boyut / KUTU) * 100}%`,
|
||||||
scale: ROTA_NABIZ_SCALE,
|
borderRadius: `${(13 / k.boyut) * 100}%`,
|
||||||
opacity: ROTA_NABIZ_OPACITY,
|
backgroundColor: renk === "marka" ? k.renk : undefined,
|
||||||
}
|
translate: yer(k.baslangic, k.boyut),
|
||||||
: {
|
}}
|
||||||
x: px,
|
/>
|
||||||
y: py,
|
))}
|
||||||
scale: [1, 0.82, 1],
|
</span>
|
||||||
opacity: [1, 0.55, 1],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Kademeli kalkış: delay yalnızca ilk turdan önce uygulanır, sonraki
|
|
||||||
// turlarda faz farkı kendiliğinden korunur.
|
|
||||||
const rotaGecikme = i * ROTA_GECIKME_ADIMI;
|
|
||||||
const konumGecis = {
|
|
||||||
duration: ROTA_SURE,
|
|
||||||
times: KONUM_ZAMANLARI,
|
|
||||||
ease: "easeInOut" as const,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: rotaGecikme,
|
|
||||||
};
|
|
||||||
const rotaNabizGecis = {
|
|
||||||
duration: ROTA_SURE,
|
|
||||||
times: ROTA_NABIZ_ZAMANLARI,
|
|
||||||
ease: "easeInOut" as const,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: rotaGecikme,
|
|
||||||
};
|
|
||||||
const nabizGecis = {
|
|
||||||
duration: NABIZ_SURE,
|
|
||||||
times: [0, 0.5, 1],
|
|
||||||
ease: SINE as unknown as [number, number, number, number],
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: NABIZ_GECIKMELER[i],
|
|
||||||
};
|
|
||||||
|
|
||||||
const transition = !aktif
|
|
||||||
? { duration: 0.4, ease: "easeOut" as const }
|
|
||||||
: varyant === "rota"
|
|
||||||
? {
|
|
||||||
x: konumGecis,
|
|
||||||
y: konumGecis,
|
|
||||||
scale: rotaNabizGecis,
|
|
||||||
opacity: rotaNabizGecis,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
x: { duration: 0 },
|
|
||||||
y: { duration: 0 },
|
|
||||||
scale: nabizGecis,
|
|
||||||
opacity: nabizGecis,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.rect
|
|
||||||
key={k.renk}
|
|
||||||
width={k.boyut}
|
|
||||||
height={k.boyut}
|
|
||||||
rx={13}
|
|
||||||
fill={k.renk}
|
|
||||||
className={renk === "turuncu" ? "fill-orange-500" : undefined}
|
|
||||||
animate={animate}
|
|
||||||
transition={transition}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
transformBox: "fill-box",
|
|
||||||
transformOrigin: "center",
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</svg>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,24 +97,22 @@ function mesaj(): string[] {
|
|||||||
bicim += "\n";
|
bicim += "\n";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bloklar arasında birer boş satır; her blokta tek punto, tek renk.
|
||||||
|
const arapca = `font-size:18px;line-height:2.1;font-family:${ARAPCA_YAZI_TIPI}`;
|
||||||
|
ekle(`\n${BESMELE}\n`, `${arapca};color:${SILIK}`);
|
||||||
|
ekle(`${ASR.join("\n")}\n`, arapca);
|
||||||
|
ekle(`\n${MEAL[0]}\n`, `font-size:12px;line-height:1.8;color:${SILIK}`);
|
||||||
|
ekle(`${MEAL.slice(2).join("\n")}\n`, "font-size:12px;line-height:1.8");
|
||||||
|
ekle("Asr Sûresi (103) · Diyanet İşleri meali\n", `font-size:11px;line-height:2.2;color:${SILIK}`);
|
||||||
ekle(
|
ekle(
|
||||||
`\n${BESMELE}\n${ASR.join("\n")}\n`,
|
"\n«Vakit bir kılıçtır; sen onu kesmezsen o seni keser.»\n",
|
||||||
`font-size:22px;line-height:2;font-family:${ARAPCA_YAZI_TIPI}`,
|
`font-size:12px;font-style:italic;line-height:1.8;color:${TURUNCU}`,
|
||||||
);
|
|
||||||
ekle(`\n${MEAL.join("\n")}\n`, "font-size:13px;line-height:1.7");
|
|
||||||
ekle("Asr Sûresi (103) · Diyanet İşleri meali\n\n", `font-size:11px;color:${SILIK}`);
|
|
||||||
ekle(
|
|
||||||
"«Vakit bir kılıçtır; sen onu kesmezsen o seni keser.»\n",
|
|
||||||
`font-size:13px;font-weight:600;line-height:1.8;color:${TURUNCU}`,
|
|
||||||
);
|
);
|
||||||
ekle(
|
ekle(
|
||||||
"İmam Şâfiî’nin sûfîlerden öğrendiğini söylediği söz\n\n",
|
"İmam Şâfiî’nin sûfîlerden öğrendiğini söylediği söz\n",
|
||||||
`font-size:11px;color:${SILIK}`,
|
`font-size:11px;line-height:2.2;color:${SILIK}`,
|
||||||
);
|
|
||||||
ekle(
|
|
||||||
`Meraklısına: ${window.location.origin}/meraklisina`,
|
|
||||||
`font-size:11px;color:${SILIK}`,
|
|
||||||
);
|
);
|
||||||
|
ekle(`\n${window.location.origin}/meraklisina`, `font-size:11px;color:${SILIK}`);
|
||||||
|
|
||||||
return [bicim, ...stiller];
|
return [bicim, ...stiller];
|
||||||
}
|
}
|
||||||
@@ -126,7 +124,12 @@ export function ConsoleAsr() {
|
|||||||
|
|
||||||
// Hidrasyon ve ilk etkileşimle yarışmasın: tarayıcı boşa çıkınca bas.
|
// Hidrasyon ve ilk etkileşimle yarışmasın: tarayıcı boşa çıkınca bas.
|
||||||
// Safari'de requestIdleCallback yok, orada kısa bir gecikme yeter.
|
// Safari'de requestIdleCallback yok, orada kısa bir gecikme yeter.
|
||||||
const bas = () => console.log(...mesaj());
|
// Önce konsolu temizle: eklenti uyarıları ve çerçeve satırları sürprizi
|
||||||
|
// aşağı itmesin, konsolu açan yalnız bunu görsün.
|
||||||
|
const bas = () => {
|
||||||
|
console.clear();
|
||||||
|
console.log(...mesaj());
|
||||||
|
};
|
||||||
if (typeof window.requestIdleCallback === "function") {
|
if (typeof window.requestIdleCallback === "function") {
|
||||||
window.requestIdleCallback(bas, { timeout: 3000 });
|
window.requestIdleCallback(bas, { timeout: 3000 });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ const HOT_COLOR = "#f97316"; // orange-500
|
|||||||
const HOT_OPACITY = 0.85;
|
const HOT_OPACITY = 0.85;
|
||||||
const HEAT_DURATION = 0.15; // ısınma: anlık geri bildirim
|
const HEAT_DURATION = 0.15; // ısınma: anlık geri bildirim
|
||||||
const COOL_DURATION = 1.4; // soğuma: iz gibi yavaşça söner
|
const COOL_DURATION = 1.4; // soğuma: iz gibi yavaşça söner
|
||||||
|
// Kırpışma: imleç olmadan, rastgele tek piksel ısınıp söner. Yormasın diye
|
||||||
|
// seyrek ve yumuşak — aynı anda ekranda bir-iki sıcak piksel olur.
|
||||||
|
const KIRPISMA_BEKLEME = 1500; // ms; sayfa otursun
|
||||||
|
const KIRPISMA_ARALIK = [500, 1200]; // ms; iki kırpışma arası (rastgele)
|
||||||
|
const KIRPISMA_ISINMA = 0.5; // imleçtekinden yavaş: göz kırpar, çakmaz
|
||||||
|
const KIRPISMA_TUTMA = 900; // ms; ısınma + kısa bekleyiş, sonra soğur
|
||||||
|
|
||||||
type HeatTarget = {
|
type HeatTarget = {
|
||||||
el: SVGRectElement;
|
el: SVGRectElement;
|
||||||
@@ -139,18 +145,74 @@ function ayril(inst: HeatInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rastgele pikselleri seyrek aralıklarla ısıtıp soğutur; durdurucu döner.
|
||||||
|
* Fırçayla aynı `calistir`/`hot` üzerinden çalışır: imlecin altındaki
|
||||||
|
* piksele dokunmaz, fırça da kırpışan pikseli devralabilir. Harita ekran
|
||||||
|
* dışındayken ya da sekme arka plandayken bekler. */
|
||||||
|
function kirpismaBaslat(inst: HeatInstance) {
|
||||||
|
const yanan = new Set<SVGRectElement>();
|
||||||
|
const bekleyen = new Set<number>();
|
||||||
|
let gorunur = true;
|
||||||
|
const io = new IntersectionObserver(([g]) => {
|
||||||
|
gorunur = g.isIntersecting;
|
||||||
|
});
|
||||||
|
io.observe(inst.svg);
|
||||||
|
|
||||||
|
let sayac = 0;
|
||||||
|
const tik = () => {
|
||||||
|
const [enAz, enCok] = KIRPISMA_ARALIK;
|
||||||
|
sayac = window.setTimeout(tik, enAz + Math.random() * (enCok - enAz));
|
||||||
|
if (!gorunur || document.hidden) return;
|
||||||
|
const p = inst.pixels[Math.floor(Math.random() * inst.pixels.length)];
|
||||||
|
if (inst.hot.has(p.el) || yanan.has(p.el)) return;
|
||||||
|
yanan.add(p.el);
|
||||||
|
calistir(
|
||||||
|
inst,
|
||||||
|
p.el,
|
||||||
|
{ fill: HOT_COLOR, fillOpacity: HOT_OPACITY },
|
||||||
|
KIRPISMA_ISINMA,
|
||||||
|
);
|
||||||
|
const s = window.setTimeout(() => {
|
||||||
|
bekleyen.delete(s);
|
||||||
|
yanan.delete(p.el);
|
||||||
|
if (inst.hot.has(p.el)) return;
|
||||||
|
calistir(
|
||||||
|
inst,
|
||||||
|
p.el,
|
||||||
|
{ fill: inst.baseColor, fillOpacity: p.baseOpacity },
|
||||||
|
COOL_DURATION,
|
||||||
|
);
|
||||||
|
}, KIRPISMA_TUTMA);
|
||||||
|
bekleyen.add(s);
|
||||||
|
};
|
||||||
|
sayac = window.setTimeout(tik, KIRPISMA_BEKLEME);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(sayac);
|
||||||
|
bekleyen.forEach((s) => window.clearTimeout(s));
|
||||||
|
io.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function usePixelHeat(
|
export function usePixelHeat(
|
||||||
svgRef: RefObject<SVGSVGElement | null>,
|
svgRef: RefObject<SVGSVGElement | null>,
|
||||||
{
|
{
|
||||||
/** Fırça yarıçapı, SVG (viewBox) biriminde */
|
/** Fırça yarıçapı, SVG (viewBox) biriminde */
|
||||||
radius = 34,
|
radius = 34,
|
||||||
}: { radius?: number } = {},
|
/** İmleç olmadan da rastgele pikseller seyrekçe ısınıp sönsün (hero) */
|
||||||
|
kirpisma = false,
|
||||||
|
}: { radius?: number; kirpisma?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const svg = svgRef.current;
|
const svg = svgRef.current;
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
// Dokunmatik cihazlarda hover yanlış tetiklenir; efekti imleçli cihazlara sakla
|
// Dokunmatik cihazlarda hover yanlış tetiklenir; fırça imleçli cihazlara
|
||||||
if (!window.matchMedia(FINE_POINTER_QUERY).matches) return;
|
// saklanır. Kırpışma imleç istemez ama "hareketi azalt"ta kapalıdır.
|
||||||
|
const imlecli = window.matchMedia(FINE_POINTER_QUERY).matches;
|
||||||
|
const kirp =
|
||||||
|
kirpisma &&
|
||||||
|
!window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
if (!imlecli && !kirp) return;
|
||||||
|
|
||||||
if (!svg.viewBox.baseVal.width) return;
|
if (!svg.viewBox.baseVal.width) return;
|
||||||
|
|
||||||
@@ -173,7 +235,11 @@ export function usePixelHeat(
|
|||||||
hot: new Set(),
|
hot: new Set(),
|
||||||
controls: new Map(),
|
controls: new Map(),
|
||||||
};
|
};
|
||||||
kaydol(inst);
|
if (imlecli) kaydol(inst);
|
||||||
return () => ayril(inst);
|
const kirpismaDur = kirp ? kirpismaBaslat(inst) : null;
|
||||||
}, [svgRef, radius]);
|
return () => {
|
||||||
|
kirpismaDur?.();
|
||||||
|
ayril(inst);
|
||||||
|
};
|
||||||
|
}, [svgRef, radius, kirpisma]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,40 +195,61 @@ function PikselSvg({
|
|||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
usePixelHeat(svgRef, { radius: ISI_YARICAPI });
|
usePixelHeat(svgRef, { radius: ISI_YARICAPI });
|
||||||
|
|
||||||
// Yüklenince gecikmeyle odağa yakınlaş. viewBox her karede yazılır; ısı
|
// Harita görünüme girince (yüklenince değil) gecikmeyle odağa yakınlaş;
|
||||||
// fırçası viewBox'ı canlı okuduğu için imleç hep doğru kareyi ısıtır.
|
// 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 { sinir, zoom } = harita;
|
||||||
const hizaLg = odakHiza?.lg ?? ORTA;
|
const hizaLg = odakHiza?.lg ?? ORTA;
|
||||||
const hizaDar = odakHiza?.dar ?? ORTA;
|
const hizaDar = odakHiza?.dar ?? ORTA;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const svg = svgRef.current;
|
const svg = svgRef.current;
|
||||||
if (!svg || !sinir) return;
|
if (!svg) 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));
|
const yaz = (k: Kutu) => svg.setAttribute("viewBox", kutuMetni(k));
|
||||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
let kontrol: { stop: () => void } | null = null;
|
||||||
yaz(odak);
|
const basla = () => {
|
||||||
return () => yaz(TAM_KUTU);
|
svg.setAttribute("data-gorundu", "");
|
||||||
}
|
if (!sinir) return;
|
||||||
const bas = TAM_KUTU;
|
const rect = svg.getBoundingClientRect();
|
||||||
const kontrol = animate(0, 1, {
|
const oran = rect.width && rect.height ? rect.width / rect.height : VIEW_W / VIEW_H;
|
||||||
type: "spring",
|
const genis = window.matchMedia("(min-width: 1024px)").matches;
|
||||||
bounce: 0,
|
const odak = odakKutusu(sinir, oran, zoom, genis ? hizaLg : hizaDar);
|
||||||
delay: ODAK_GECIKME,
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||||
duration: ODAK_SURE,
|
yaz(odak);
|
||||||
onUpdate: (t) =>
|
return;
|
||||||
yaz([
|
}
|
||||||
bas[0] + (odak[0] - bas[0]) * t,
|
const bas = TAM_KUTU;
|
||||||
bas[1] + (odak[1] - bas[1]) * t,
|
kontrol = animate(0, 1, {
|
||||||
bas[2] + (odak[2] - bas[2]) * t,
|
type: "spring",
|
||||||
bas[3] + (odak[3] - bas[3]) * t,
|
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 () => {
|
return () => {
|
||||||
kontrol.stop();
|
gozlemci.disconnect();
|
||||||
yaz(TAM_KUTU);
|
kontrol?.stop();
|
||||||
|
svg.removeAttribute("data-gorundu");
|
||||||
|
if (sinir) yaz(TAM_KUTU);
|
||||||
};
|
};
|
||||||
}, [sinir, zoom, hizaLg, hizaDar]);
|
}, [sinir, zoom, hizaLg, hizaDar]);
|
||||||
|
|
||||||
@@ -237,7 +258,7 @@ function PikselSvg({
|
|||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
||||||
preserveAspectRatio={doldur ? "xMidYMid slice" : "xMidYMid meet"}
|
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"
|
role="img"
|
||||||
aria-label={etiket}
|
aria-label={etiket}
|
||||||
// Statik, deterministik markup — her render'da yeniden kurulmaz
|
// Statik, deterministik markup — her render'da yeniden kurulmaz
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
* imleç uzaklaşınca yavaşça kendi mavi tonuna soğur (asimetrik zamanlama).
|
* 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
|
* 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.
|
* 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";
|
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) => {
|
CITIES.forEach((city, i) => {
|
||||||
parts.push(
|
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"/>`,
|
`<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 }) {
|
export function TurkeyPixelMap({ className }: { className?: string }) {
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
usePixelHeat(svgRef);
|
usePixelHeat(svgRef, { kirpisma: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -442,15 +442,21 @@ export function CtaSiraForm({
|
|||||||
href={sonucHref()}
|
href={sonucHref()}
|
||||||
aria-label="Sıralamana uygun programları gör"
|
aria-label="Sıralamana uygun programları gör"
|
||||||
title="Sıralamana uygun programları gör"
|
title="Sıralamana uygun programları gör"
|
||||||
// Gölge yok (§6: derinlik gölgeyle verilmez), hover'da renk
|
// Gerilmiş link (after:inset-0) bandın tamamını kaplar. Ölçek
|
||||||
// değişmez (iri dolu yüzeyde orange-600 kırmızımsı okunuyor —
|
// linkin KENDİSİNE verilmez: transform, ::after için kapsayan
|
||||||
// Bilal, 24 Eyl 2026); tepki: ok kayar, basınca küçülür.
|
// blok olur, alan 48 px'lik daireye çöker, imleç linkten düşer
|
||||||
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"
|
// 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
|
{/* Gölge yok (§6: derinlik gölgeyle verilmez), hover'da renk
|
||||||
className="size-6 transition-transform duration-200 group-hover:translate-x-0.5 sm:size-7"
|
değişmez (iri dolu yüzeyde orange-600 kırmızımsı okunuyor —
|
||||||
aria-hidden
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user