From 8c48303051520d5d7e666531421660459de895e0 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Fri, 9 Oct 2026 00:29:03 +0300 Subject: [PATCH] =?UTF-8?q?feat(tema):=20ge=C3=A7i=C5=9Fte=20logo=20karele?= =?UTF-8?q?ri=20g=C3=BCne=C5=9Fe=20ya=20da=20aya=20d=C3=B6n=C3=BC=C5=9F?= =?UTF-8?q?=C3=BCr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/tasarim/DESIGN.md | 4 +- src/components/animasyonlu-mark.tsx | 116 ++++++++++++++++++++++++---- src/lib/tema-gecisi.ts | 5 +- 3 files changed, 107 insertions(+), 18 deletions(-) diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index 5827ef7..e9b4e27 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -136,7 +136,9 @@ Seçici `src/components/tema-secici.tsx` (footer'da), sağlayıcı `src/componen **Geçiş ve kısayol (7 Eki 2026; son hâli 9 Eki):** tema değişimi anlık değildir; yeni tema header'daki logodan **yumuşak bir halka** gibi açılır: kenarı keskin değil, geniş ve tüylü (ışık yayılır gibi), 1100 ms, yumuşak kalkıp yumuşak durur. Aynı anda logonun kareleri -bir etap yürüyüp yerine oturur; geçişi logo başlatmış olur. View Transitions + radyal degrade +yeni temanın biçimine dönüşür: koyuya geçişte şeftali kare hilale, mavi ve turuncu kareler iki +yıldıza; açığa geçişte turuncu kare güneşe, şeftali kare halesine. Kısa bir an durur, yeniden +logoya toplanır (`animasyonlu-mark.tsx` `temaSekli`, 1,7 sn; yalnız header'daki logo). View Transitions + radyal degrade maskesi: `src/lib/tema-gecisi.ts`, `globals.css` `.kt-tema-gecis`; hareket azaltılmışsa düz değişir. Denenip bırakılanlar: logo biçimli maske ("logo içinde logo"), düz crossfade, piksel ızgarası (sert bulundu). Kural: bu geçişte sert kenar ve ikinci bir şekil yok. diff --git a/src/components/animasyonlu-mark.tsx b/src/components/animasyonlu-mark.tsx index 6161fe2..2108a1b 100644 --- a/src/components/animasyonlu-mark.tsx +++ b/src/components/animasyonlu-mark.tsx @@ -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(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 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]); diff --git a/src/lib/tema-gecisi.ts b/src/lib/tema-gecisi.ts index 7ca82d6..72644ff 100644 --- a/src/lib/tema-gecisi.ts +++ b/src/lib/tema-gecisi.ts @@ -1,7 +1,8 @@ // Tema geçişi (DESIGN.md §2.3): açık ↔ koyu değişimi ekranı bir anda // çevirmez; yeni tema header'daki KolayTercih işaretinden yumuşak bir halka // gibi açılır — kenarı keskin değil, geniş ve tüylü; ışık yayılır gibi. -// Aynı anda logonun kareleri bir tur atar: geçişi logo "başlatmış" olur. +// Aynı anda logonun kareleri yeni temanın biçimine dönüşür (koyuya: ay ve +// yıldızlar; açığa: güneş), kısa bir an durur ve yeniden logoya toplanır. // Markanın yumuşak, oyunsu diline uysun diye sert kenar, piksel ya da ikinci // bir şekil yok (9 Eki 2026). // @@ -14,7 +15,7 @@ const GECIS_SINIFI = "kt-tema-gecis"; /** Geçiş başlarken pencereye atılan olay; header'daki logo bunu dinleyip - * karelerini bir tur oynatır (animasyonlu-mark.tsx). */ + * karelerini güneşe ya da aya dönüştürür (animasyonlu-mark.tsx). */ export const TEMA_GECIS_OLAYI = "kt-tema-gecis"; // Maskenin tam opak çekirdeği yarıçapın bu oranında biter, kalanı tüydür