fix(tema): geçiş maskesinde logo görünmüyordu — SVG katmanları CSS'e gömüldü; alttan uçuş + boşluk kapanışı koreografisi

Chrome, view-transition pseudo elemanında dosyadan url() ile yüklenen ikinci
maske katmanını (boyu 0'dan büyükken) tümden şeffaf çiziyor; iki SVG de
data URI olarak globals.css'e gömüldü. Koreografi (önceki oturum): işaret
ekranın altından ortaya süzülür, merkezden büyür, kareler arası boşluklar
dolu çizimle merkezden dışa kapanır; motion sürer. Geçici console tanıları
kaldırıldı, iptal edilen geçişin ready reddi yakalanıyor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-08 23:33:56 +03:00
parent 5f7ea8bbcd
commit 9f401602a8
6 changed files with 193 additions and 67 deletions

View File

@@ -1,21 +1,48 @@
// Tema geçişi (DESIGN.md §2.3): açık ↔ koyu değişimi ekranı bir anda
// çevirmez; yeni tema, header'daki KolayTercih işaretinin biçiminde logodan
// başlayıp büyüyerek ekrana yayılır (View Transitions API + CSS maskesi,
// globals.css `.kt-tema-gecis`). Tarayıcı View Transitions bilmiyorsa ya da
// kullanıcı hareketi azaltmışsa tema düz değişir.
// çevirmez; yeni tema, KolayTercih işaretinin biçiminde önce ekranın altından
// ortaya süzülür, sonra merkezden büyüyerek ekranı kaplar. View Transitions
// API + CSS maskesi (globals.css `.kt-tema-gecis`) eski/yeni görüntü çiftini
// kurar; maskenin konumu, boyu, boşluk kapanışı ve son dolgu motion ile
// sürülür. Tarayıcı View Transitions bilmiyorsa ya da kullanıcı hareketi
// azaltmışsa tema düz değişir.
/** Header'daki işaretin kök elemanı (site-header.tsx, AnimasyonluMark). */
export const TEMA_ODAK_ID = "kt-tema-odak";
import { animate, type AnimationPlaybackControls } from "motion";
/** Geçiş sırasında <html>'de duran sınıf ve maskenin merkez değişkenleri. */
/** Geçiş sırasında <html>'de duran sınıf (globals.css). */
const GECIS_SINIFI = "kt-tema-gecis";
// Saniye. Faz 1 (uçuş): işaret ekranın altından ortaya gelir — normal,
// boşluklu hâliyle. Faz 2 (kaplama): merkezden büyüyerek ekranı aşar;
// kaplamanın KAPANIS_BASI…KAPANIS_BITIS aralığında dolu çizim merkezden
// dışa büyür ve kareler arası boşlukları kapatır (en büyük hâle ulaşmadan).
// EASE fazlar arasında geçişin eski koreografisiyle aynı hissi verir.
const UCUS_SURE = 0.45;
const KAPLAMA_SURE = 0.6;
const KAPANIS_BASI = 0.55;
const KAPANIS_BITIS = 0.9;
const EASE = [0.65, 0, 0.35, 1] as const;
// CSS'teki kt-tema-omur'un süresi: motion bitmeden pseudo eleman kalkmasın
// diye toplamın biraz üstü (globals.css'te --kt-tema-sure ile okunur).
const GECIS_SURESI = UCUS_SURE + KAPLAMA_SURE + 0.1;
// İşaretin dış kenarları (public/kolay-tercih-mark-dolu.svg, 180'lik viewBox;
// orijinal logonun boşlukları doldurulmuş çizimi, dış uzantılar aynı):
// x 27–147, y 29–151. Merkeze (90) göre oranlar — dolgu, bu sınır kutusu
// ekranı tam kapsadığı andan itibaren dolar; erken dolarsa ekrandaki eski
// zemin topluca solardı.
const KENAR_ORAN = {
sol: (90 - 27) / 180,
sag: (147 - 90) / 180,
ust: (90 - 29) / 180,
alt: (151 - 90) / 180,
} as const;
let suruyor = false;
/**
* `uygula` temayı değiştiren çağrıdır (ör. `setTheme("dark")`). Geçiş
* destekleniyorsa yeni tema işaretin bulunduğu noktadan maskeyle açılır;
* işaret ekranda değilse (krom gizli sayfalar) sol üst köşeden açılır.
* destekleniyorsa işaret ekranın altından ortaya süzülür ve oradan büyüyerek
* yeni temayı açar.
*/
export function temayiGecisle(uygula: () => void) {
if (typeof document === "undefined") {
@@ -23,33 +50,103 @@ export function temayiGecisle(uygula: () => void) {
return;
}
const azHareket = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!document.startViewTransition || azHareket || suruyor) {
if (!document.startViewTransition || azHareket) {
uygula();
return;
}
// Geçiş sürerken gelen istek yok sayılır (uygulanmaz): ara anahtarlamalar
// hem koreografiyi bozar hem de ekrandaki görüntü ile tema durumu birbirine
// girer; D'ye art arda basınca görünen "sıçrama" budur.
if (suruyor) return;
const odak = document.getElementById(TEMA_ODAK_ID)?.getBoundingClientRect();
const en = window.innerWidth;
const boy = window.innerHeight;
const kok = document.documentElement;
const x = odak && odak.width > 0 ? odak.left + odak.width / 2 : 40;
const y = odak && odak.height > 0 ? odak.top + odak.height / 2 : 48;
// Maske merkezden ekranın en uzak köşesine kadar büyümeli.
const uzak = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y),
// Uçuş boyunca işaretin maskeden çıkan kenarı: ekranı kaplamayacak ama
// belirgin bir işaret olacak kadar (min ekran kenarının %22'si; işaretin
// görünür karesi bunun ~%67'si).
const ucusKenar = Math.round(Math.min(en, boy) * 0.22);
// Uçuş ve kaplamanın ortak ekseni ekranın ortası; başlangıçta işaret
// tamamen ekranın altında durur (alttan gelir).
const merkezX = en / 2;
const merkezY = boy / 2;
const baslangicY = boy + ucusKenar / 2;
// Kaplama merkezden ekranın en uzak köşesine kadar büyümeli. İşaretin
// görünür karesi viewBox'ın ~%67'si; kenar bu oranla çarpılır ki en büyük
// hâlde köşe karesi bile ekranı aşsın (eski hesapla aynı çarpan).
const sonKenar = Math.ceil(Math.hypot(merkezX, merkezY) * 3.4);
// Dolgu, işaretin dış kutusu ekranı tam kapsadığı anda başlar (erken
// dolarsa marka henüz ekrana ulaşmadan eski zemin topluca solardı). Alt
// sınır 0.9: kapsama hesaba katılmayan dar bir marjla geç kalırsa bile
// dolgu kaplamanın son ~%10'unda bitsin; aksi hâlde ekranın köşelerinde
// eski zemin belirgin biçimde kalır.
const kapsaKenar = Math.max(
en / 2 / Math.max(KENAR_ORAN.sol, KENAR_ORAN.sag),
boy / 2 / Math.max(KENAR_ORAN.ust, KENAR_ORAN.alt),
);
const dolguBasi = Math.min(
1,
Math.max((kapsaKenar - ucusKenar) / (sonKenar - ucusKenar), 0.9),
);
kok.style.setProperty("--kt-tema-x", `${x}px`);
kok.style.setProperty("--kt-tema-y", `${y}px`);
// İşaretin görünür karesi viewBox'ın ~%67'si; kenar bu oranla çarpılır ki
// en büyük hâlde köşe karesi bile ekranı aşsın.
kok.style.setProperty("--kt-tema-son", `${Math.ceil(uzak * 3.4)}px`);
kok.style.setProperty("--kt-tema-x", `${merkezX}px`);
kok.style.setProperty("--kt-tema-y", `${baslangicY}px`);
kok.style.setProperty("--kt-tema-kenar", `${ucusKenar}px`);
kok.style.setProperty("--kt-tema-kapanis", "0");
kok.style.setProperty("--kt-tema-dolgu", "0");
kok.style.setProperty("--kt-tema-sure", `${GECIS_SURESI}s`);
kok.classList.add(GECIS_SINIFI);
suruyor = true;
const kontroller: AnimationPlaybackControls[] = [
// Faz 1 — uçuş: alttan ortaya, x sabit.
animate(0, 1, {
duration: UCUS_SURE,
ease: EASE,
onUpdate: (p) => {
kok.style.setProperty(
"--kt-tema-y",
`${baslangicY + (merkezY - baslangicY) * p}px`,
);
},
}),
// Faz 2 — kaplama: merkezden ekranı aşana kadar.
animate(ucusKenar, sonKenar, {
duration: KAPLAMA_SURE,
delay: UCUS_SURE,
ease: EASE,
onUpdate: (kenar) => kok.style.setProperty("--kt-tema-kenar", `${kenar}px`),
}),
// Kapanış: dolu çizimin boyu kenar × kapanış — büyümenin ortasından
// sonuna doğru boşluklar merkezden dışa dolar, logo en büyük hâle
// ulaşmadan kapanır. Boyut sürüldüğü için ek bir katmanın alfa
// çakışması gerekmez; maske toplamı (add) değişmez.
animate(0, 1, {
duration: KAPLAMA_SURE * (KAPANIS_BITIS - KAPANIS_BASI),
delay: UCUS_SURE + KAPLAMA_SURE * KAPANIS_BASI,
ease: EASE,
onUpdate: (kapanis) => kok.style.setProperty("--kt-tema-kapanis", `${kapanis}`),
}),
// Dolgu: işaretin dış çentiklerinde kalan eski zemin son anda dolar —
// ekranı kapsayan boşluklar (kareler arası şeritler) maskenin kendisinde
// dolu çizimle kapanmıştır, burada değil.
animate(0, 1, {
duration: KAPLAMA_SURE * (1 - dolguBasi),
delay: UCUS_SURE + KAPLAMA_SURE * dolguBasi,
ease: EASE,
onUpdate: (dolgu) => kok.style.setProperty("--kt-tema-dolgu", `${dolgu}`),
}),
];
const gecis = document.startViewTransition(uygula);
// Sekme gizliyken / yeniden boyutlanırken tarayıcı geçişi iptal eder:
// `ready` reddedilir, tema yine uygulanmıştır; konsola hata düşmesin.
gecis.ready.catch(() => {});
gecis.finished
.catch(() => {})
.finally(() => {
kontroller.forEach((k) => k.stop());
kok.classList.remove(GECIS_SINIFI);
suruyor = false;
});