v1-beta-tema #9
@@ -133,11 +133,13 @@ Seçici `src/components/tema-secici.tsx` (footer'da), sağlayıcı `src/componen
|
|||||||
(`next-themes`, `<html class="dark">`). Renklerin tek kaynağı `src/app/globals.css` içindeki
|
(`next-themes`, `<html class="dark">`). Renklerin tek kaynağı `src/app/globals.css` içindeki
|
||||||
`.dark` bloğu.
|
`.dark` bloğu.
|
||||||
|
|
||||||
**Geçiş ve kısayol (7 Eki 2026, 9 Eki'de sadeleşti):** tema değişimi anlık değildir;
|
**Geçiş ve kısayol (7 Eki 2026; son hâli 9 Eki):** tema değişimi anlık değildir; yeni
|
||||||
tüm sayfa tek parça, 640 ms ease-in-out crossfade ile yeni temaya geçer (View
|
tema header'daki logodan başlar ve ekrana **piksel piksel** yayılır — piksel imzasının
|
||||||
Transitions: `src/lib/tema-gecisi.ts`, `globals.css` `.kt-tema-gecis`; hareket
|
(§1, `PixelMark`/`PixelDivider`) hareketli hâli. Kareler sırası gelince yumuşakça belirir,
|
||||||
azaltılmışsa düz değişir). Logo maskeli yayılma denendi ve kaldırıldı (Bilal'in
|
dalganın ön yüzü dağınık bir piksel kenarıdır; 900 ms (`--kt-tema-sure`). View Transitions
|
||||||
kararı, 9 Eki 2026): mursela-nextjs'teki sade crossfade yeter.
|
+ tuvalden üretilen kare ızgarası maskesi: `src/lib/tema-gecisi.ts`, `globals.css`
|
||||||
|
`.kt-tema-gecis`; hareket azaltılmışsa düz değişir. Logo biçimli maske ve düz crossfade
|
||||||
|
denendi, bu seçildi (Bilal işi tasarıma bıraktı, 9 Eki 2026).
|
||||||
`D` tuşu açık ↔ koyu geçişi yapar (`src/components/tema-kisayolu.tsx`); ipucu seçicinin
|
`D` tuşu açık ↔ koyu geçişi yapar (`src/components/tema-kisayolu.tsx`); ipucu seçicinin
|
||||||
altında `Kbd` ile durur, başka yere kopyalanmaz. Header'daki Beta rozeti `/surum-notlari`na
|
altında `Kbd` ile durur, başka yere kopyalanmaz. Header'daki Beta rozeti `/surum-notlari`na
|
||||||
bağlantıdır.
|
bağlantıdır.
|
||||||
|
|||||||
@@ -477,47 +477,43 @@
|
|||||||
background: var(--color-slate-700);
|
background: var(--color-slate-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): tema değişirken tüm
|
/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): yeni tema header'daki
|
||||||
sayfa tek parça crossfade yapar — eski görüntü solar, yeni belirir.
|
logodan başlar ve ekrana piksel piksel yayılır — sitenin piksel imzasının
|
||||||
mursela-nextjs'teki tema geçişiyle aynı: 640 ms, ease-in-out. Sınıf
|
hareketli hâli. Eski görüntü yerinde durur; yeni görüntü, JS'in kare kare
|
||||||
(`kt-tema-gecis`) geçiş süresince <html>'de durur; stil yalnız tema
|
bastığı kare ızgarası maskesiyle (--kt-tema-maske, küçük bir tuvalden
|
||||||
değişimine uygulanır. Hareket azaltılmışsa JS geçişi hiç başlatmaz,
|
üretilir; --kt-tema-en/boy ızgaranın ekrandaki ölçüsü) üstünde açılır.
|
||||||
tema düz değişir. */
|
`pixelated`: küçük maske büyütülürken kareler bulanmaz, keskin kalır.
|
||||||
|
Sürenin tek kaynağı --kt-tema-sure (JS buradan okur). kt-tema-omur değeri
|
||||||
|
değişmeyen ama gerçek bir animasyondur: view transition, üzerindeki
|
||||||
|
animasyon bitene kadar kalkmaz; maskeyi JS sürerken ömrü bu tutar.
|
||||||
|
Hareket azaltılmışsa JS geçişi hiç başlatmaz, tema düz değişir. */
|
||||||
|
:root {
|
||||||
|
--kt-tema-sure: 900ms;
|
||||||
|
}
|
||||||
.kt-tema-gecis {
|
.kt-tema-gecis {
|
||||||
view-transition-name: root;
|
view-transition-name: root;
|
||||||
--kt-tema-sure: 640ms;
|
|
||||||
--kt-tema-egri: cubic-bezier(0.6, 0, 0.2, 1);
|
|
||||||
}
|
}
|
||||||
.kt-tema-gecis::view-transition-old(root),
|
.kt-tema-gecis::view-transition-old(root),
|
||||||
.kt-tema-gecis::view-transition-new(root) {
|
.kt-tema-gecis::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
mix-blend-mode: normal;
|
mix-blend-mode: normal;
|
||||||
will-change: opacity;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
}
|
|
||||||
.kt-tema-gecis::view-transition-group(root) {
|
|
||||||
animation-duration: var(--kt-tema-sure);
|
|
||||||
animation-timing-function: var(--kt-tema-egri);
|
|
||||||
}
|
}
|
||||||
.kt-tema-gecis::view-transition-old(root) {
|
.kt-tema-gecis::view-transition-old(root) {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
animation: kt-tema-cikis var(--kt-tema-sure) var(--kt-tema-egri) both;
|
|
||||||
}
|
}
|
||||||
.kt-tema-gecis::view-transition-new(root) {
|
.kt-tema-gecis::view-transition-new(root) {
|
||||||
z-index: 0;
|
z-index: 2;
|
||||||
animation: kt-tema-giris var(--kt-tema-sure) var(--kt-tema-egri) both;
|
mask-image: var(--kt-tema-maske);
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: 0 0;
|
||||||
|
mask-size: var(--kt-tema-en) var(--kt-tema-boy);
|
||||||
|
image-rendering: pixelated;
|
||||||
|
animation: kt-tema-omur calc(var(--kt-tema-sure) + 80ms) linear both;
|
||||||
}
|
}
|
||||||
@keyframes kt-tema-cikis {
|
@keyframes kt-tema-omur {
|
||||||
from {
|
from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes kt-tema-giris {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function yaziAlanindaMi(hedef: EventTarget | null) {
|
|||||||
* Klavye kısayolu: `D` açık ↔ koyu temayı değiştirir (ipucu footer'daki
|
* Klavye kısayolu: `D` açık ↔ koyu temayı değiştirir (ipucu footer'daki
|
||||||
* TemaSecici'nin altında, Kbd ile). Değiştirici tuşla (⌘/Ctrl/Alt) ya da yazı
|
* TemaSecici'nin altında, Kbd ile). Değiştirici tuşla (⌘/Ctrl/Alt) ya da yazı
|
||||||
* alanındayken çalışmaz; "sistem" seçiliyken o anki çözümlenmiş temanın
|
* alanındayken çalışmaz; "sistem" seçiliyken o anki çözümlenmiş temanın
|
||||||
* tersine, açık bir seçime geçer. Geçiş sayfa geneli crossfade ile oynar.
|
* tersine, açık bir seçime geçer. Geçiş logodan piksel piksel yayılır.
|
||||||
*/
|
*/
|
||||||
export function TemaKisayolu() {
|
export function TemaKisayolu() {
|
||||||
const { resolvedTheme, setTheme } = useTheme();
|
const { resolvedTheme, setTheme } = useTheme();
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function AdresCubuguRengi() {
|
|||||||
* globals.css'teki `.dark` token eşlemesinden gelir. Varsayılan açık tema;
|
* globals.css'teki `.dark` token eşlemesinden gelir. Varsayılan açık tema;
|
||||||
* kullanıcı footer'daki TemaSecici ile koyu ya da "sistem"e geçer, seçim
|
* kullanıcı footer'daki TemaSecici ile koyu ya da "sistem"e geçer, seçim
|
||||||
* tarayıcıda saklanır; `D` tuşu açık ↔ koyu geçişi yapar (TemaKisayolu).
|
* tarayıcıda saklanır; `D` tuşu açık ↔ koyu geçişi yapar (TemaKisayolu).
|
||||||
* Tema değişimi sayfa geneli crossfade ile oynar (lib/tema-gecisi.ts).
|
* Tema değişimi logodan piksel piksel yayılır (lib/tema-gecisi.ts).
|
||||||
*/
|
*/
|
||||||
export function TemaSaglayici({ children }: { children: React.ReactNode }) {
|
export function TemaSaglayici({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ const aboneOlma = () => () => {};
|
|||||||
/**
|
/**
|
||||||
* Tema seçici: açık / koyu / sistem (DESIGN.md §2.3). Seçili tema yalnızca
|
* Tema seçici: açık / koyu / sistem (DESIGN.md §2.3). Seçili tema yalnızca
|
||||||
* tarayıcıda bilinir; sunucu HTML'inde hiçbir seçenek işaretli gelmez,
|
* tarayıcıda bilinir; sunucu HTML'inde hiçbir seçenek işaretli gelmez,
|
||||||
* hydration sonrası işaretlenir (yerleşim oynamaz). Seçim sayfa geneli crossfade
|
* hydration sonrası işaretlenir (yerleşim oynamaz). Seçim logodan piksel piksel
|
||||||
* ile uygulanır (lib/tema-gecisi.ts); altında `D` kısayolu ipucu durur.
|
* yayılarak uygulanır (lib/tema-gecisi.ts); altında `D` kısayolu ipucu durur.
|
||||||
*/
|
*/
|
||||||
export function TemaSecici({ className }: { className?: string }) {
|
export function TemaSecici({ className }: { className?: string }) {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|||||||
@@ -1,16 +1,106 @@
|
|||||||
// 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; tüm sayfa tek parça, yumuşak bir crossfade ile yeni temaya geçer
|
// çevirmez; yeni tema header'daki KolayTercih işaretinden başlar ve ekrana
|
||||||
// (View Transitions API; süre ve eğri globals.css `.kt-tema-gecis`
|
// PİKSEL PİKSEL yayılır — sitenin piksel imzasının (PixelMark, PixelDivider)
|
||||||
// bloğunda, mursela-nextjs'teki tema geçişiyle aynı: 640 ms ease-in-out).
|
// hareketli hâli. Her kare kendi sırası gelince yumuşakça belirir; dalganın
|
||||||
// Tarayıcı View Transitions bilmiyorsa ya da kullanıcı hareketi azaltmışsa
|
// ön yüzü düz bir çizgi değil, karelerin rastgele öne-arkaya düştüğü
|
||||||
// tema düz değişir.
|
// dağınık bir kenardır.
|
||||||
|
//
|
||||||
|
// Nasıl: View Transitions API eski/yeni görüntü çiftini verir; yeni görüntü
|
||||||
|
// küçük bir tuvalde üretilen kare ızgarası maskesiyle açılır (her hücre bir
|
||||||
|
// kare, alfası o karenin ne kadar belirdiği). Kareler önceden üretilir ve
|
||||||
|
// kare kare maskeye basılır (globals.css `.kt-tema-gecis`). Tarayıcı View
|
||||||
|
// Transitions bilmiyorsa ya da kullanıcı hareketi azaltmışsa tema düz değişir.
|
||||||
|
|
||||||
/** Geçiş sırasında <html>'de duran sınıf: crossfade stili yalnız tema
|
/** Geçiş sırasında <html>'de duran sınıf (globals.css). */
|
||||||
* değişimine uygulanır, başka view transition'lara karışmaz. */
|
|
||||||
const GECIS_SINIFI = "kt-tema-gecis";
|
const GECIS_SINIFI = "kt-tema-gecis";
|
||||||
|
|
||||||
|
// Maske karesi sayısı: ~30 kare/sn; piksel dilinde akıcı, üretimi ucuz.
|
||||||
|
const KARE_SAYISI = 28;
|
||||||
|
// Tuvalde bir hücrenin piksel kenarı: büyütülünce kare kenarı keskin kalsın.
|
||||||
|
const HUCRE_COZUNURLUK = 4;
|
||||||
|
// Dalga: karenin sırası = merkeze uzaklık (DALGA) + rastgele pay (SACILMA);
|
||||||
|
// sırası gelen kare BELIRME kadar sürede 0 → 1 alfaya çıkar (toplam 1).
|
||||||
|
const DALGA = 0.66;
|
||||||
|
const SACILMA = 0.19;
|
||||||
|
const BELIRME = 0.15;
|
||||||
|
|
||||||
|
type Maske = { anahtar: string; kareler: string[]; en: number; boy: number };
|
||||||
|
let onbellek: Maske | null = null;
|
||||||
let suruyor = false;
|
let suruyor = false;
|
||||||
|
|
||||||
|
/** Ekrandaki bir karenin kenarı (px): ekranla ölçeklenir, sınırlar içinde. */
|
||||||
|
function hucreKenari(en: number, boy: number) {
|
||||||
|
return Math.max(28, Math.min(56, Math.round(Math.min(en, boy) / 16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** easeInOutCubic: dalga yumuşak kalkar, yumuşak biter. */
|
||||||
|
function yumusat(t: number) {
|
||||||
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maske karelerini üretir: `sutun × satir` hücrelik ızgara, dalga (mx, my)
|
||||||
|
* hücresinden yayılır. Aynı ızgara ve merkez için sonuç önbellekten gelir —
|
||||||
|
* ikinci basışta üretim bedeli yoktur, görseller de çözülmüş olur.
|
||||||
|
*/
|
||||||
|
function maskeUret(en: number, boy: number, merkezX: number, merkezY: number): Maske | null {
|
||||||
|
const kenar = hucreKenari(en, boy);
|
||||||
|
const sutun = Math.ceil(en / kenar);
|
||||||
|
const satir = Math.ceil(boy / kenar);
|
||||||
|
const mx = Math.floor(merkezX / kenar);
|
||||||
|
const my = Math.floor(merkezY / kenar);
|
||||||
|
const anahtar = `${sutun}x${satir}@${mx},${my}`;
|
||||||
|
if (onbellek?.anahtar === anahtar) return onbellek;
|
||||||
|
|
||||||
|
const tuval = document.createElement("canvas");
|
||||||
|
tuval.width = sutun * HUCRE_COZUNURLUK;
|
||||||
|
tuval.height = satir * HUCRE_COZUNURLUK;
|
||||||
|
const cizim = tuval.getContext("2d");
|
||||||
|
if (!cizim) return null;
|
||||||
|
|
||||||
|
// Her karenin sırası: merkeze uzaklık (en uzak köşeye oranla) + saçılma.
|
||||||
|
const enUzak = Math.hypot(Math.max(mx, sutun - 1 - mx), Math.max(my, satir - 1 - my)) || 1;
|
||||||
|
const sira = new Float32Array(sutun * satir);
|
||||||
|
for (let y = 0; y < satir; y++) {
|
||||||
|
for (let x = 0; x < sutun; x++) {
|
||||||
|
const uzaklik = Math.hypot(x - mx, y - my) / enUzak;
|
||||||
|
sira[y * sutun + x] = uzaklik * DALGA + Math.random() * SACILMA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kareler: string[] = [];
|
||||||
|
for (let k = 0; k < KARE_SAYISI; k++) {
|
||||||
|
// İlk kare boş değil: geçiş ilk karede başlamış görünsün; son kare tam dolu.
|
||||||
|
const t = yumusat((k + 1) / KARE_SAYISI);
|
||||||
|
cizim.clearRect(0, 0, tuval.width, tuval.height);
|
||||||
|
for (let y = 0; y < satir; y++) {
|
||||||
|
for (let x = 0; x < sutun; x++) {
|
||||||
|
const alfa = (t - sira[y * sutun + x]) / BELIRME;
|
||||||
|
if (alfa <= 0) continue;
|
||||||
|
cizim.fillStyle = `rgba(0,0,0,${Math.min(1, alfa)})`;
|
||||||
|
cizim.fillRect(
|
||||||
|
x * HUCRE_COZUNURLUK,
|
||||||
|
y * HUCRE_COZUNURLUK,
|
||||||
|
HUCRE_COZUNURLUK,
|
||||||
|
HUCRE_COZUNURLUK,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kareler.push(`url("${tuval.toDataURL("image/png")}")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onbellek = { anahtar, kareler, en: sutun * kenar, boy: satir * kenar };
|
||||||
|
return onbellek;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CSS'teki `--kt-tema-sure` (ms ya da s) — sürenin tek kaynağı globals.css. */
|
||||||
|
function gecisSuresi(kok: HTMLElement) {
|
||||||
|
const ham = getComputedStyle(kok).getPropertyValue("--kt-tema-sure").trim();
|
||||||
|
const sayi = parseFloat(ham);
|
||||||
|
if (!Number.isFinite(sayi)) return 900;
|
||||||
|
return ham.endsWith("ms") ? sayi : sayi * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `uygula` temayı değiştiren çağrıdır (ör. `setTheme("dark")`). Geçiş
|
* `uygula` temayı değiştiren çağrıdır (ör. `setTheme("dark")`). Geçiş
|
||||||
* sürerken gelen istek yok sayılır: art arda D'ye basınca ekrandaki görüntü
|
* sürerken gelen istek yok sayılır: art arda D'ye basınca ekrandaki görüntü
|
||||||
@@ -29,17 +119,60 @@ export function temayiGecisle(uygula: () => void) {
|
|||||||
if (suruyor) return;
|
if (suruyor) return;
|
||||||
|
|
||||||
const kok = document.documentElement;
|
const kok = document.documentElement;
|
||||||
|
// Dalganın merkezi header'daki logo; krom gizli sayfalarda sol üst köşe.
|
||||||
|
const logo = document
|
||||||
|
.querySelector('header a[aria-label="KolayTercih"]')
|
||||||
|
?.getBoundingClientRect();
|
||||||
|
const gorunur = logo && logo.width > 0 && logo.bottom > 0;
|
||||||
|
const merkezX = gorunur ? logo.left + logo.height / 2 : 24;
|
||||||
|
const merkezY = gorunur ? logo.top + logo.height / 2 : 24;
|
||||||
|
|
||||||
|
const maske = maskeUret(window.innerWidth, window.innerHeight, merkezX, merkezY);
|
||||||
|
if (!maske) {
|
||||||
|
uygula();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sure = gecisSuresi(kok);
|
||||||
|
kok.style.setProperty("--kt-tema-maske", maske.kareler[0]);
|
||||||
|
kok.style.setProperty("--kt-tema-en", `${maske.en}px`);
|
||||||
|
kok.style.setProperty("--kt-tema-boy", `${maske.boy}px`);
|
||||||
kok.classList.add(GECIS_SINIFI);
|
kok.classList.add(GECIS_SINIFI);
|
||||||
suruyor = true;
|
suruyor = true;
|
||||||
|
|
||||||
|
let cerceve = 0;
|
||||||
|
let baslangic = 0;
|
||||||
|
let sonKare = 0;
|
||||||
|
const ilerle = (simdi: number) => {
|
||||||
|
if (!baslangic) baslangic = simdi;
|
||||||
|
const kare = Math.min(
|
||||||
|
KARE_SAYISI - 1,
|
||||||
|
Math.floor(((simdi - baslangic) / sure) * KARE_SAYISI),
|
||||||
|
);
|
||||||
|
if (kare !== sonKare) {
|
||||||
|
sonKare = kare;
|
||||||
|
kok.style.setProperty("--kt-tema-maske", maske.kareler[kare]);
|
||||||
|
}
|
||||||
|
if (kare < KARE_SAYISI - 1) cerceve = requestAnimationFrame(ilerle);
|
||||||
|
};
|
||||||
|
|
||||||
const gecis = document.startViewTransition(uygula);
|
const gecis = document.startViewTransition(uygula);
|
||||||
|
// Görüntü çifti hazır olunca saat başlar: yakalama süresi dalgadan yemesin.
|
||||||
// Sekme gizliyken / yeniden boyutlanırken tarayıcı geçişi iptal eder:
|
// Sekme gizliyken / yeniden boyutlanırken tarayıcı geçişi iptal eder:
|
||||||
// `ready` reddedilir, tema yine uygulanmıştır; konsola hata düşmesin.
|
// `ready` reddedilir, tema yine uygulanmıştır; konsola hata düşmesin.
|
||||||
gecis.ready.catch(() => {});
|
gecis.ready
|
||||||
|
.then(() => {
|
||||||
|
cerceve = requestAnimationFrame(ilerle);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
gecis.finished
|
gecis.finished
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
cancelAnimationFrame(cerceve);
|
||||||
kok.classList.remove(GECIS_SINIFI);
|
kok.classList.remove(GECIS_SINIFI);
|
||||||
|
kok.style.removeProperty("--kt-tema-maske");
|
||||||
|
kok.style.removeProperty("--kt-tema-en");
|
||||||
|
kok.style.removeProperty("--kt-tema-boy");
|
||||||
suruyor = false;
|
suruyor = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user