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:
@@ -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.
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user