feat(tema): geçişte logo kareleri güneşe ya da aya dönüşür

Koyuya geçerken şeftali kare hilale (iç gölgeyle; zemin rengine bağlı değil),
mavi ve turuncu kareler iki yıldıza; açığa geçerken turuncu kare güneşe,
şeftali kare halesine dönüşür. Kısa bir an durup logoya toplanır (1,7 sn).
Yalnız header'daki logo; hover döngüsü biçimi keser. DESIGN §2.3 güncel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-09 00:29:03 +03:00
parent 6022319174
commit 8c48303051
3 changed files with 107 additions and 18 deletions

View File

@@ -98,6 +98,37 @@ function yer(pozisyon: number, boyut: number): string {
return `${(x / boyut) * 100}% ${(y / boyut) * 100}%`;
}
/** Karenin merkezini 180'lik kutuda (cx, cy) noktasına taşıyan `translate`. */
function merkezde(cx: number, cy: number, boyut: number): string {
return `${((cx - boyut / 2) / boyut) * 100}% ${((cy - boyut / 2) / boyut) * 100}%`;
}
// Tema geçişi biçimi (DESIGN.md §2.3): geçiş logodan yayılırken kareler
// neye geçildiğini gösterir, kısa bir an durur ve yeniden logoya toplanır.
// - Koyuya: şeftali kare hilale, mavi ve turuncu kareler iki küçük yıldıza.
// - Açığa: turuncu kare güneşe, şeftali kare arkasındaki haleye; mavi söner.
// Toplam süre ve duraklar: %30'da biçim tamam, %70'e kadar durur, sonra döner.
const SEKIL_SURE = 1700;
const SEKIL_DURAKLAR = [0, 0.3, 0.7, 1];
type SekilHedefi = { translate: string; scale: string; opacity: number };
/** Kare `i` için hedef biçim. Kareler sırayla mavi, şeftali, turuncu. */
function sekilHedefi(i: number, koyu: boolean): SekilHedefi {
const b = KARELER[i].boyut;
if (koyu) {
return [
{ translate: merkezde(40, 50, b), scale: "0.3", opacity: 1 }, // yıldız
{ translate: merkezde(100, 90, b), scale: "1.75", opacity: 1 }, // hilal
{ translate: merkezde(56, 132, b), scale: "0.2", opacity: 1 }, // yıldız
][i];
}
return [
{ translate: merkezde(90, 90, b), scale: "0.3", opacity: 0 }, // söner
{ translate: merkezde(90, 90, b), scale: "2.15", opacity: 0.45 }, // hale
{ translate: merkezde(90, 90, b), scale: "1.35", opacity: 1 }, // güneş
][i];
}
/** 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ı). */
@@ -185,6 +216,7 @@ export function AnimasyonluMark({
const kokRef = useRef<HTMLSpanElement>(null);
const kontrolRef = useRef<{
ayarla: (hedef: Varyant | null, cevik?: boolean) => void;
temaSekli: (koyu: boolean) => void;
} | null>(null);
// Animasyonların sahibi: bileşen yaşadıkça bir kez kurulur. Prop değişimi
@@ -198,6 +230,8 @@ export function AnimasyonluMark({
let dongu: Animation[] = [];
let donus: Animation[] = [];
/** Tema geçişi biçimi (güneş/ay) animasyonları; bkz. `temaSekli`. */
let sekil: Animation[] = [];
/** Şu an dönen döngü ve istenen döngü (`null`: durağan). */
let aktif: Varyant | null = null;
let hedef: Varyant | null = null;
@@ -237,6 +271,11 @@ export function AnimasyonluMark({
const ayarla = (yeni: Varyant | null, cevik = false) => {
if (yeni === hedef) return;
hedef = yeni;
// Hover döngüsü biçimin üstüne binmesin: güneş/ay yarıda kesilir.
if (yeni !== null) {
sekil.forEach((a) => a.cancel());
sekil = [];
}
if (aktif !== null) yerles(cevik);
if (yeni === null || azHareket.matches) return;
// Kareler hâlâ yerine dönüyorsa dönüşün bitmesi beklenir; döngü hep
@@ -250,12 +289,65 @@ export function AnimasyonluMark({
.catch(() => {});
};
const kontrol = { ayarla };
/**
* Tema geçişi biçimi: kareler güneşe ya da aya dönüşür, durur, logoya
* döner. Döngü oynarken (hover) ya da hareket azaltılmışken çalışmaz.
* Hilal, kareyi kesen ikinci bir şekille değil karenin kendi iç gölgesiyle
* çizilir (zemin şeffaf, renk iç gölgede): arkadaki zemin ne olursa olsun
* doğru görünür.
*/
const temaSekli = (koyu: boolean) => {
if (aktif !== null || hedef !== null || azHareket.matches) return;
sekil.forEach((a) => a.cancel());
sekil = kareler.flatMap((el, i) => {
const k = KARELER[i];
const ev = {
translate: yer(k.baslangic, k.boyut),
scale: "1",
opacity: 1,
borderRadius: `${(13 / k.boyut) * 100}%`,
};
const bicim = { ...sekilHedefi(i, koyu), borderRadius: "50%" };
const secenek = { duration: SEKIL_SURE };
const animasyonlar = [
el.animate(
[
{ ...ev, easing: DONUS },
{ ...bicim, easing: "linear" },
{ ...bicim, easing: YERLES },
ev,
].map((kare, d) => ({ ...kare, offset: SEKIL_DURAKLAR[d] })),
secenek,
),
];
// Hilal: yalnız şeftali kare, yalnız koyuya geçerken.
if (koyu && i === 1) {
const renk = getComputedStyle(el).backgroundColor;
const w = el.offsetWidth;
const dolu = `inset 0 0 0 ${w}px ${renk}`;
const hilal = `inset ${-0.36 * w}px ${0.1 * w}px 0 0 ${renk}`;
animasyonlar.push(
el.animate(
[
{ boxShadow: dolu, backgroundColor: "transparent", easing: DONUS },
{ boxShadow: hilal, backgroundColor: "transparent", easing: "linear" },
{ boxShadow: hilal, backgroundColor: "transparent", easing: YERLES },
{ boxShadow: dolu, backgroundColor: "transparent" },
].map((kare, d) => ({ ...kare, offset: SEKIL_DURAKLAR[d] })),
secenek,
),
);
}
return animasyonlar;
});
};
const kontrol = { ayarla, temaSekli };
kontrolRef.current = kontrol;
return () => {
sokuldu = true;
if (kontrolRef.current === kontrol) kontrolRef.current = null;
[...dongu, ...donus].forEach((a) => a.cancel());
[...dongu, ...donus, ...sekil].forEach((a) => a.cancel());
};
}, []);
@@ -277,24 +369,18 @@ export function AnimasyonluMark({
const cik = () => kontrolRef.current?.ayarla(null, true);
hoverHedefi.addEventListener("mouseenter", gir);
hoverHedefi.addEventListener("mouseleave", cik);
// Tema geçişi başlarken (lib/tema-gecisi.ts) kareler bir etap yürür ve
// yerine oturur: geçiş logodan yayıldığı için onu logo başlatmış olur.
// İmleç zaten üzerindeyse hover döngüsü sürer, dokunulmaz.
let temaZamanlayici: number | undefined;
const temaGecti = () => {
if (hoverHedefi.matches(":hover")) return;
gir();
window.clearTimeout(temaZamanlayici);
temaZamanlayici = window.setTimeout(() => {
if (!hoverHedefi.matches(":hover")) cik();
}, 1000);
};
// Tema geçişi başlarken (lib/tema-gecisi.ts) kareler yeni temanın
// biçimine dönüşür: koyuya geçişte ay ve yıldızlar, açığa geçişte güneş.
// Olay anında <html> yeni temanın sınıfını taşır.
const temaGecti = () =>
kontrolRef.current?.temaSekli(
document.documentElement.classList.contains("dark"),
);
window.addEventListener(TEMA_GECIS_OLAYI, temaGecti);
return () => {
hoverHedefi.removeEventListener("mouseenter", gir);
hoverHedefi.removeEventListener("mouseleave", cik);
window.removeEventListener(TEMA_GECIS_OLAYI, temaGecti);
window.clearTimeout(temaZamanlayici);
};
}, [oynat, sadeceHover, varyant]);