v1-beta-tema #9

Merged
bilal merged 19 commits from v1-beta-tema into main 2026-10-09 02:30:44 +03:00
3 changed files with 107 additions and 18 deletions
Showing only changes of commit 8c48303051 - Show all commits

View File

@@ -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 **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 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 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 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 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. ızgarası (sert bulundu). Kural: bu geçişte sert kenar ve ikinci bir şekil yok.

View File

@@ -98,6 +98,37 @@ function yer(pozisyon: number, boyut: number): string {
return `${(x / boyut) * 100}% ${(y / boyut) * 100}%`; 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 → /** 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ı). */
@@ -185,6 +216,7 @@ export function AnimasyonluMark({
const kokRef = useRef<HTMLSpanElement>(null); const kokRef = useRef<HTMLSpanElement>(null);
const kontrolRef = useRef<{ const kontrolRef = useRef<{
ayarla: (hedef: Varyant | null, cevik?: boolean) => void; ayarla: (hedef: Varyant | null, cevik?: boolean) => void;
temaSekli: (koyu: boolean) => void;
} | null>(null); } | null>(null);
// Animasyonların sahibi: bileşen yaşadıkça bir kez kurulur. Prop değişimi // 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 dongu: Animation[] = [];
let donus: 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). */ /** Şu an dönen döngü ve istenen döngü (`null`: durağan). */
let aktif: Varyant | null = null; let aktif: Varyant | null = null;
let hedef: Varyant | null = null; let hedef: Varyant | null = null;
@@ -237,6 +271,11 @@ export function AnimasyonluMark({
const ayarla = (yeni: Varyant | null, cevik = false) => { const ayarla = (yeni: Varyant | null, cevik = false) => {
if (yeni === hedef) return; if (yeni === hedef) return;
hedef = yeni; 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 (aktif !== null) yerles(cevik);
if (yeni === null || azHareket.matches) return; if (yeni === null || azHareket.matches) return;
// Kareler hâlâ yerine dönüyorsa dönüşün bitmesi beklenir; döngü hep // Kareler hâlâ yerine dönüyorsa dönüşün bitmesi beklenir; döngü hep
@@ -250,12 +289,65 @@ export function AnimasyonluMark({
.catch(() => {}); .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; kontrolRef.current = kontrol;
return () => { return () => {
sokuldu = true; sokuldu = true;
if (kontrolRef.current === kontrol) kontrolRef.current = null; 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); const cik = () => kontrolRef.current?.ayarla(null, true);
hoverHedefi.addEventListener("mouseenter", gir); hoverHedefi.addEventListener("mouseenter", gir);
hoverHedefi.addEventListener("mouseleave", cik); hoverHedefi.addEventListener("mouseleave", cik);
// Tema geçişi başlarken (lib/tema-gecisi.ts) kareler bir etap yürür ve // Tema geçişi başlarken (lib/tema-gecisi.ts) kareler yeni temanın
// yerine oturur: geçiş logodan yayıldığı için onu logo başlatmış olur. // biçimine dönüşür: koyuya geçişte ay ve yıldızlar, açığa geçişte güneş.
// İmleç zaten üzerindeyse hover döngüsü sürer, dokunulmaz. // Olay anında <html> yeni temanın sınıfını taşır.
let temaZamanlayici: number | undefined; const temaGecti = () =>
const temaGecti = () => { kontrolRef.current?.temaSekli(
if (hoverHedefi.matches(":hover")) return; document.documentElement.classList.contains("dark"),
gir(); );
window.clearTimeout(temaZamanlayici);
temaZamanlayici = window.setTimeout(() => {
if (!hoverHedefi.matches(":hover")) cik();
}, 1000);
};
window.addEventListener(TEMA_GECIS_OLAYI, temaGecti); window.addEventListener(TEMA_GECIS_OLAYI, temaGecti);
return () => { return () => {
hoverHedefi.removeEventListener("mouseenter", gir); hoverHedefi.removeEventListener("mouseenter", gir);
hoverHedefi.removeEventListener("mouseleave", cik); hoverHedefi.removeEventListener("mouseleave", cik);
window.removeEventListener(TEMA_GECIS_OLAYI, temaGecti); window.removeEventListener(TEMA_GECIS_OLAYI, temaGecti);
window.clearTimeout(temaZamanlayici);
}; };
}, [oynat, sadeceHover, varyant]); }, [oynat, sadeceHover, varyant]);

View File

@@ -1,7 +1,8 @@
// Tema geçişi (DESIGN.md §2.3): açık ↔ koyu değişimi ekranı bir anda // 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 // ç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. // 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 // Markanın yumuşak, oyunsu diline uysun diye sert kenar, piksel ya da ikinci
// bir şekil yok (9 Eki 2026). // bir şekil yok (9 Eki 2026).
// //
@@ -14,7 +15,7 @@
const GECIS_SINIFI = "kt-tema-gecis"; const GECIS_SINIFI = "kt-tema-gecis";
/** Geçiş başlarken pencereye atılan olay; header'daki logo bunu dinleyip /** 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"; export const TEMA_GECIS_OLAYI = "kt-tema-gecis";
// Maskenin tam opak çekirdeği yarıçapın bu oranında biter, kalanı tüydür // Maskenin tam opak çekirdeği yarıçapın bu oranında biter, kalanı tüydür