refactor(ui): AcilisSahnesi ortak bileşen (components/ui/acilis-sahnesi.tsx), kartsız logo→içerik modu
akademik-ayrinti.tsx'teki açılış sahnesi src/components/ui/acilis-sahnesi.tsx'e taşındı; davranışı değişmedi (kartlı mod). Eklenenler: `hazir` prop'u (false iken sahne oynamaz, veri gelince oynar), `className` prop'u ve kartsız mod: `data-logo` kabındaki üç `data-logo-kare` logodaki yerinde belirir, durur, scale 0.6 + opacity 0 ile söner, kap sıfır boya iner, `data-sonra` bloklar netleşir. Hareketi azalt: logo gizli, 0,25 s solma. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
308
src/components/ui/acilis-sahnesi.tsx
Normal file
308
src/components/ui/acilis-sahnesi.tsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Alt sayfa açılış sahnesi (DESIGN.md §4.19 açılış koreografisi, §6 hareket):
|
||||||
|
// renkli kareler önce ortada logonun üç karesi gibi belirir, sonra içerik
|
||||||
|
// gelir. İki mod:
|
||||||
|
// - Kartlı (akademik-ayrinti.tsx, gösterge kareleri §4.11): `[data-kare]`
|
||||||
|
// kareler logodan kendi yerine ve boyuna büyür, içleri satır satır
|
||||||
|
// bulanıklıktan netleşir, ardından `[data-sonra]` bloklar.
|
||||||
|
// - Kartsız (program-ayrinti.tsx, §4.19 "kart yok"): `[data-kare]` yok,
|
||||||
|
// `[data-logo]` kabındaki üç `[data-logo-kare]` belirir, kısa durur, sonra
|
||||||
|
// sönerek kaybolur (scale 0.6 + opacity 0; Bilal, 7 Eki 2026) ve
|
||||||
|
// `[data-sonra]` bloklar netleşerek gelir.
|
||||||
|
// "Hareketi azalt"ta logo ve büyüme yok, yalnız 0,25 s solma (§6).
|
||||||
|
|
||||||
|
import { useLayoutEffect, useRef, type ReactNode } from "react";
|
||||||
|
import { animate, useReducedMotion } from "motion/react";
|
||||||
|
|
||||||
|
// Logodaki üç kare (public/kolay-tercih-mark.svg, 180'lik viewBox): solda
|
||||||
|
// mavi, sağ üstte şeftali, sağ altta turuncu. animasyonlu-mark.tsx ile aynı.
|
||||||
|
// Marka rengi; iki temada da sabit kalır.
|
||||||
|
const LOGO = {
|
||||||
|
sol: { x: 27, y: 56, boyut: 57, renk: "#3b82f6" },
|
||||||
|
"sag-ust": { x: 91, y: 29, boyut: 56, renk: "#fdc7a7" },
|
||||||
|
"sag-alt": { x: 91, y: 95, boyut: 56, renk: "#ff5a00" },
|
||||||
|
} as const;
|
||||||
|
const LOGO_RX = 13;
|
||||||
|
const SLOT_SIRASI = ["sol", "sag-ust", "sag-alt"] as const;
|
||||||
|
type Slot = (typeof SLOT_SIRASI)[number];
|
||||||
|
|
||||||
|
// Saniye. Kare: belir (logo) → bekle → yerine büyü; içerik kare oturunca.
|
||||||
|
const BASLANGIC = 0.1;
|
||||||
|
const SLOT_ADIMI = 0.08;
|
||||||
|
const KARE_SURE = 1.15;
|
||||||
|
const KARE_ZAMANLARI = [0, 0.24, 0.52, 1];
|
||||||
|
const ICERIK_BASI = 0.95;
|
||||||
|
const GUCLU_CIKIS = [0.23, 1, 0.32, 1] as const;
|
||||||
|
const YERLESME = [0.32, 0.72, 0, 1] as const;
|
||||||
|
|
||||||
|
// Kartsız mod: logo karesi belir → dur → sön. Belirme ve duruş kartlı
|
||||||
|
// modla aynı anlara denk gelir (KARE_SURE·0.24, ·0.52); sönme 0,4 s.
|
||||||
|
const LOGO_SURE = KARE_SURE * KARE_ZAMANLARI[2] + 0.4;
|
||||||
|
const LOGO_ZAMANLARI = [
|
||||||
|
0,
|
||||||
|
(KARE_SURE * KARE_ZAMANLARI[1]) / LOGO_SURE,
|
||||||
|
(KARE_SURE * KARE_ZAMANLARI[2]) / LOGO_SURE,
|
||||||
|
1,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Kartsız modun logo kareleri, belirme sırasıyla (`data-logo-kare` değerleri). */
|
||||||
|
export const LOGO_SLOTLARI = SLOT_SIRASI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kartsız modun logo karesi stili: logodaki yer ve renk, kabın boyuna ölçekli
|
||||||
|
* (`data-logo` kabı `relative mx-auto h-16 w-16` → 64/180). Kullanım:
|
||||||
|
* `<span data-logo-kare={slot} className="absolute" style={logoKareStili(slot)} />`.
|
||||||
|
* Kareler yalnız sahne sırasında görünür; sahne bitince kap sıfır boya iner,
|
||||||
|
* "hareketi azalt"ta hiç gösterilmez.
|
||||||
|
*/
|
||||||
|
export function logoKareStili(slot: Slot, kapBoyu = 64) {
|
||||||
|
const olcek = kapBoyu / 180;
|
||||||
|
return {
|
||||||
|
left: LOGO[slot].x * olcek,
|
||||||
|
top: LOGO[slot].y * olcek,
|
||||||
|
width: LOGO[slot].boyut * olcek,
|
||||||
|
height: LOGO[slot].boyut * olcek,
|
||||||
|
borderRadius: LOGO_RX * olcek,
|
||||||
|
backgroundColor: LOGO[slot].renk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Çekmece açılınca oynayan sahne.
|
||||||
|
*
|
||||||
|
* Sunucu işaretlemesiyle sözleşme (universite-icerik.tsx, AkademikGostergeler):
|
||||||
|
* `data-sahne` kareleri saran `relative` kap, `data-kare="sol|sag-ust|sag-alt"`
|
||||||
|
* karenin logodaki yeri (aynı yeri paylaşan kareler üst üste başlar, sonra
|
||||||
|
* ayrışır), `data-sonra` karelerden sonra gelen bloklar. Kartsız sözleşme:
|
||||||
|
* `data-logo` kabı (üç `data-logo-kare`, logoKareStili) + `data-sonra` bloklar.
|
||||||
|
*
|
||||||
|
* Kareler yalnız transform ile oynar (yerleşim ölçülür, bozulmaz); içerik o
|
||||||
|
* sırada görünmez olduğu için kare ölçeklenirken yazı ezilmez.
|
||||||
|
*
|
||||||
|
* `hazir`: false iken sahne oynamaz (iskelet görünür kalır), true olunca
|
||||||
|
* oynar — veri bekleyen çekmece için. Varsayılan true (içerik sunucudan
|
||||||
|
* hazır gelen çekmece).
|
||||||
|
*/
|
||||||
|
export function AcilisSahnesi({
|
||||||
|
children,
|
||||||
|
hazir = true,
|
||||||
|
className = "mt-5",
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
hazir?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const kapRef = useRef<HTMLDivElement>(null);
|
||||||
|
const azHareket = useReducedMotion();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const kap = kapRef.current;
|
||||||
|
// hazir false → true tek geçiş: sahne o geçişte bir kez oynar (ref
|
||||||
|
// tutulmaz; Strict Mode'un çift effect'i temizlik sonrası yeniden kurar).
|
||||||
|
if (!kap || !hazir) return;
|
||||||
|
const sahne = kap.querySelector<HTMLElement>("[data-sahne]");
|
||||||
|
const logo = kap.querySelector<HTMLElement>("[data-logo]");
|
||||||
|
const kareler = Array.from(
|
||||||
|
kap.querySelectorAll<HTMLElement>("[data-kare]"),
|
||||||
|
);
|
||||||
|
const logoKareleri = Array.from(
|
||||||
|
kap.querySelectorAll<HTMLElement>("[data-logo-kare]"),
|
||||||
|
);
|
||||||
|
const sonra = Array.from(kap.querySelectorAll<HTMLElement>("[data-sonra]"));
|
||||||
|
const kontroller: { stop: () => void }[] = [];
|
||||||
|
const sayaclar: number[] = [];
|
||||||
|
const dokunulan: HTMLElement[] = [];
|
||||||
|
|
||||||
|
const kartli = !!sahne && kareler.length > 0;
|
||||||
|
const kartsiz = !kartli && !!logo && logoKareleri.length > 0;
|
||||||
|
|
||||||
|
if (azHareket || (!kartli && !kartsiz)) {
|
||||||
|
// Logo kabı sahnesiz anlamsız: yer kaplamasın.
|
||||||
|
if (logo) {
|
||||||
|
logo.style.display = "none";
|
||||||
|
dokunulan.push(logo);
|
||||||
|
}
|
||||||
|
kontroller.push(
|
||||||
|
animate(kap, { opacity: [0, 1] }, { duration: 0.25, ease: "easeOut" }),
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
kontroller.forEach((k) => k.stop());
|
||||||
|
for (const el of dokunulan) el.style.display = "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulanıklıktan netleşerek, hafifçe aşağıdan gelen satır.
|
||||||
|
const netlestir = (el: HTMLElement, gecikme: number) => {
|
||||||
|
el.style.opacity = "0";
|
||||||
|
dokunulan.push(el);
|
||||||
|
const k = animate(
|
||||||
|
el,
|
||||||
|
{
|
||||||
|
opacity: [0, 1],
|
||||||
|
filter: ["blur(10px)", "blur(0px)"],
|
||||||
|
y: [10, 0],
|
||||||
|
},
|
||||||
|
{ duration: 0.5, delay: gecikme, ease: GUCLU_CIKIS },
|
||||||
|
);
|
||||||
|
k.then(() => {
|
||||||
|
el.style.filter = "";
|
||||||
|
});
|
||||||
|
kontroller.push(k);
|
||||||
|
};
|
||||||
|
|
||||||
|
const temizle = () => {
|
||||||
|
kontroller.forEach((k) => k.stop());
|
||||||
|
sayaclar.forEach((s) => window.clearTimeout(s));
|
||||||
|
for (const el of dokunulan) {
|
||||||
|
el.style.opacity = "";
|
||||||
|
el.style.filter = "";
|
||||||
|
el.style.transform = "";
|
||||||
|
el.style.transition = "";
|
||||||
|
el.style.backgroundColor = "";
|
||||||
|
el.style.borderRadius = "";
|
||||||
|
el.style.height = "";
|
||||||
|
el.style.display = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (kartsiz) {
|
||||||
|
// Kartsız: logo kareleri belirir, durur, söner; kap sönerken sıfır boya
|
||||||
|
// iner ki altında boşluk kalmasın; içerik ICERIK_BASI'ndan itibaren gelir.
|
||||||
|
let sonKare = 0;
|
||||||
|
logoKareleri.forEach((el) => {
|
||||||
|
const slot = (el.dataset.logoKare ?? "sol") as Slot;
|
||||||
|
const gecikme = BASLANGIC + SLOT_SIRASI.indexOf(slot) * SLOT_ADIMI;
|
||||||
|
sonKare = Math.max(sonKare, gecikme);
|
||||||
|
el.style.opacity = "0";
|
||||||
|
dokunulan.push(el);
|
||||||
|
kontroller.push(
|
||||||
|
animate(
|
||||||
|
el,
|
||||||
|
{
|
||||||
|
opacity: [0, 1, 1, 0],
|
||||||
|
scale: [0.4, 1, 1, 0.6],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: LOGO_SURE,
|
||||||
|
delay: gecikme,
|
||||||
|
times: LOGO_ZAMANLARI,
|
||||||
|
ease: [GUCLU_CIKIS, "linear", YERLESME],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const sonmeBasi = sonKare + LOGO_SURE * LOGO_ZAMANLARI[2];
|
||||||
|
dokunulan.push(logo!);
|
||||||
|
const kapat = animate(
|
||||||
|
logo!,
|
||||||
|
{ height: [logo!.offsetHeight, 0] },
|
||||||
|
{ duration: 0.4, delay: sonmeBasi, ease: YERLESME },
|
||||||
|
);
|
||||||
|
kapat.then(() => {
|
||||||
|
logo!.style.display = "none";
|
||||||
|
});
|
||||||
|
kontroller.push(kapat);
|
||||||
|
|
||||||
|
sonra.forEach((el, i) => netlestir(el, ICERIK_BASI + i * 0.08));
|
||||||
|
return temizle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kartlı: logo, kare alanının ortasında; 180'lik viewBox'ın ekrandaki boyu.
|
||||||
|
const W = sahne!.offsetWidth;
|
||||||
|
const H = sahne!.offsetHeight;
|
||||||
|
const logoBoy = Math.min(W * 0.5, 200);
|
||||||
|
const olcek = logoBoy / 180;
|
||||||
|
const logoX = (W - logoBoy) / 2;
|
||||||
|
const logoY = (H - logoBoy) / 2;
|
||||||
|
|
||||||
|
let sonKare = 0;
|
||||||
|
kareler.forEach((el, i) => {
|
||||||
|
const slot = LOGO[el.dataset.kare as keyof typeof LOGO]
|
||||||
|
? el.dataset.kare!
|
||||||
|
: "sol";
|
||||||
|
const logoKare = LOGO[slot as keyof typeof LOGO];
|
||||||
|
// offset*: transform'dan etkilenmez (çekmece o sırada kayıyor).
|
||||||
|
let sol = 0;
|
||||||
|
let ust = 0;
|
||||||
|
for (
|
||||||
|
let d: HTMLElement | null = el;
|
||||||
|
d && d !== sahne;
|
||||||
|
d = d.offsetParent as HTMLElement | null
|
||||||
|
) {
|
||||||
|
sol += d.offsetLeft;
|
||||||
|
ust += d.offsetTop;
|
||||||
|
}
|
||||||
|
const w = el.offsetWidth;
|
||||||
|
const h = el.offsetHeight;
|
||||||
|
const boy = logoKare.boyut * olcek;
|
||||||
|
const sx = boy / w;
|
||||||
|
const sy = boy / h;
|
||||||
|
const dx = logoX + logoKare.x * olcek + boy / 2 - (sol + w / 2);
|
||||||
|
const dy = logoY + logoKare.y * olcek + boy / 2 - (ust + h / 2);
|
||||||
|
// Ölçek köşeyi de küçültür; logodaki köşe oranı için ters ölçekli elips.
|
||||||
|
const rx = (LOGO_RX * olcek) / sx;
|
||||||
|
const ry = (LOGO_RX * olcek) / sy;
|
||||||
|
const logoKose = `${rx}px / ${ry}px`;
|
||||||
|
const kose = getComputedStyle(el).borderTopLeftRadius;
|
||||||
|
const gecikme =
|
||||||
|
BASLANGIC +
|
||||||
|
SLOT_SIRASI.indexOf(slot as Slot) * SLOT_ADIMI;
|
||||||
|
sonKare = Math.max(sonKare, gecikme);
|
||||||
|
|
||||||
|
el.style.opacity = "0";
|
||||||
|
el.style.backgroundColor = logoKare.renk;
|
||||||
|
dokunulan.push(el);
|
||||||
|
const k = animate(
|
||||||
|
el,
|
||||||
|
{
|
||||||
|
opacity: [0, 1, 1, 1],
|
||||||
|
x: [dx, dx, dx, 0],
|
||||||
|
y: [dy, dy, dy, 0],
|
||||||
|
scaleX: [sx * 0.4, sx, sx, 1],
|
||||||
|
scaleY: [sy * 0.4, sy, sy, 1],
|
||||||
|
borderRadius: [logoKose, logoKose, logoKose, `${kose} / ${kose}`],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: KARE_SURE,
|
||||||
|
delay: gecikme,
|
||||||
|
times: KARE_ZAMANLARI,
|
||||||
|
ease: [GUCLU_CIKIS, "linear", YERLESME],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
k.then(() => {
|
||||||
|
el.style.borderRadius = "";
|
||||||
|
});
|
||||||
|
kontroller.push(k);
|
||||||
|
// Yerine büyürken logo renginden karenin kendi rengine (sınıftaki renk).
|
||||||
|
sayaclar.push(
|
||||||
|
window.setTimeout(
|
||||||
|
() => {
|
||||||
|
el.style.transition = "background-color 450ms ease-out";
|
||||||
|
el.style.backgroundColor = "";
|
||||||
|
},
|
||||||
|
(gecikme + KARE_SURE * KARE_ZAMANLARI[2]) * 1000,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Array.from(el.children).forEach((satir, j) =>
|
||||||
|
netlestir(
|
||||||
|
satir as HTMLElement,
|
||||||
|
gecikme + ICERIK_BASI + i * 0.06 + j * 0.05,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
sonra.forEach((el, i) =>
|
||||||
|
netlestir(el, sonKare + ICERIK_BASI + 0.3 + i * 0.08),
|
||||||
|
);
|
||||||
|
|
||||||
|
return temizle;
|
||||||
|
}, [azHareket, hazir]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={kapRef} className={className}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useLayoutEffect, useRef, type ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { animate, useReducedMotion } from "motion/react";
|
|
||||||
import { ChevronRight, GraduationCap, X } from "lucide-react";
|
import { ChevronRight, GraduationCap, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
AltSayfa,
|
AltSayfa,
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
AltSayfaBaslik,
|
AltSayfaBaslik,
|
||||||
AltSayfaTetik,
|
AltSayfaTetik,
|
||||||
} from "@/components/ui/alt-sayfa";
|
} from "@/components/ui/alt-sayfa";
|
||||||
|
import { AcilisSahnesi } from "@/components/ui/acilis-sahnesi";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Üniversite ve bölüm sayfasında özet kartlarının altındaki ayrıntı satırı
|
* Üniversite ve bölüm sayfasında özet kartlarının altındaki ayrıntı satırı
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
* önceden söyler. Ayrıntı alttan açılan çekmecede; çekmece ve kapatma
|
* önceden söyler. Ayrıntı alttan açılan çekmecede; çekmece ve kapatma
|
||||||
* çarpısı cta-sira-form.tsx'teki çekmeceyle birebir aynı sınıf setinde.
|
* çarpısı cta-sira-form.tsx'teki çekmeceyle birebir aynı sınıf setinde.
|
||||||
* Kartlar sunucuda üretilip `children` olarak gelir; açılış koreografisini
|
* Kartlar sunucuda üretilip `children` olarak gelir; açılış koreografisini
|
||||||
* AcilisSahnesi oynatır.
|
* AcilisSahnesi (`src/components/ui/acilis-sahnesi.tsx`, kartlı mod) oynatır.
|
||||||
*/
|
*/
|
||||||
export function AkademikAyrinti({
|
export function AkademikAyrinti({
|
||||||
baglam,
|
baglam,
|
||||||
@@ -82,189 +82,3 @@ export function AkademikAyrinti({
|
|||||||
</AltSayfa>
|
</AltSayfa>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logodaki üç kare (public/kolay-tercih-mark.svg, 180'lik viewBox): solda
|
|
||||||
// mavi, sağ üstte şeftali, sağ altta turuncu. animasyonlu-mark.tsx ile aynı.
|
|
||||||
const LOGO = {
|
|
||||||
sol: { x: 27, y: 56, boyut: 57, renk: "#3b82f6" },
|
|
||||||
"sag-ust": { x: 91, y: 29, boyut: 56, renk: "#fdc7a7" },
|
|
||||||
"sag-alt": { x: 91, y: 95, boyut: 56, renk: "#ff5a00" },
|
|
||||||
} as const;
|
|
||||||
const LOGO_RX = 13;
|
|
||||||
const SLOT_SIRASI = ["sol", "sag-ust", "sag-alt"] as const;
|
|
||||||
|
|
||||||
// Saniye. Kare: belir (logo) → bekle → yerine büyü; içerik kare oturunca.
|
|
||||||
const BASLANGIC = 0.1;
|
|
||||||
const SLOT_ADIMI = 0.08;
|
|
||||||
const KARE_SURE = 1.15;
|
|
||||||
const KARE_ZAMANLARI = [0, 0.24, 0.52, 1];
|
|
||||||
const ICERIK_BASI = 0.95;
|
|
||||||
const GUCLU_CIKIS = [0.23, 1, 0.32, 1] as const;
|
|
||||||
const YERLESME = [0.32, 0.72, 0, 1] as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Çekmece açılınca oynayan sahne: renkli kareler önce ortada logonun üç
|
|
||||||
* karesi gibi belirir, sonra kendi yerlerine ve boylarına büyür (renk logo
|
|
||||||
* renginden kendi rengine döner), en son içleri satır satır bulanıklıktan
|
|
||||||
* netleşerek dolar; ardından alttaki not, buton ve kaynaklar gelir.
|
|
||||||
*
|
|
||||||
* Sunucu işaretlemesiyle sözleşme (universite-icerik.tsx, AkademikGostergeler):
|
|
||||||
* `data-sahne` kareleri saran `relative` kap, `data-kare="sol|sag-ust|sag-alt"`
|
|
||||||
* karenin logodaki yeri (aynı yeri paylaşan kareler üst üste başlar, sonra
|
|
||||||
* ayrışır), `data-sonra` karelerden sonra gelen bloklar.
|
|
||||||
*
|
|
||||||
* Kareler yalnız transform ile oynar (yerleşim ölçülür, bozulmaz); içerik o
|
|
||||||
* sırada görünmez olduğu için kare ölçeklenirken yazı ezilmez. "Hareketi
|
|
||||||
* azalt"ta logo ve büyüme yok, yalnız kısa belirme (DESIGN.md §6).
|
|
||||||
*/
|
|
||||||
function AcilisSahnesi({ children }: { children: ReactNode }) {
|
|
||||||
const kapRef = useRef<HTMLDivElement>(null);
|
|
||||||
const azHareket = useReducedMotion();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const kap = kapRef.current;
|
|
||||||
if (!kap) return;
|
|
||||||
const sahne = kap.querySelector<HTMLElement>("[data-sahne]");
|
|
||||||
const kareler = Array.from(
|
|
||||||
kap.querySelectorAll<HTMLElement>("[data-kare]"),
|
|
||||||
);
|
|
||||||
const sonra = Array.from(kap.querySelectorAll<HTMLElement>("[data-sonra]"));
|
|
||||||
const kontroller: { stop: () => void }[] = [];
|
|
||||||
const sayaclar: number[] = [];
|
|
||||||
const dokunulan: HTMLElement[] = [];
|
|
||||||
|
|
||||||
if (azHareket || !sahne || kareler.length === 0) {
|
|
||||||
kontroller.push(
|
|
||||||
animate(kap, { opacity: [0, 1] }, { duration: 0.25, ease: "easeOut" }),
|
|
||||||
);
|
|
||||||
return () => kontroller.forEach((k) => k.stop());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bulanıklıktan netleşerek, hafifçe aşağıdan gelen satır.
|
|
||||||
const netlestir = (el: HTMLElement, gecikme: number) => {
|
|
||||||
el.style.opacity = "0";
|
|
||||||
dokunulan.push(el);
|
|
||||||
const k = animate(
|
|
||||||
el,
|
|
||||||
{
|
|
||||||
opacity: [0, 1],
|
|
||||||
filter: ["blur(10px)", "blur(0px)"],
|
|
||||||
y: [10, 0],
|
|
||||||
},
|
|
||||||
{ duration: 0.5, delay: gecikme, ease: GUCLU_CIKIS },
|
|
||||||
);
|
|
||||||
k.then(() => {
|
|
||||||
el.style.filter = "";
|
|
||||||
});
|
|
||||||
kontroller.push(k);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Logo, kare alanının ortasında; 180'lik viewBox'ın ekrandaki boyu.
|
|
||||||
const W = sahne.offsetWidth;
|
|
||||||
const H = sahne.offsetHeight;
|
|
||||||
const logoBoy = Math.min(W * 0.5, 200);
|
|
||||||
const olcek = logoBoy / 180;
|
|
||||||
const logoX = (W - logoBoy) / 2;
|
|
||||||
const logoY = (H - logoBoy) / 2;
|
|
||||||
|
|
||||||
let sonKare = 0;
|
|
||||||
kareler.forEach((el, i) => {
|
|
||||||
const slot = LOGO[el.dataset.kare as keyof typeof LOGO]
|
|
||||||
? el.dataset.kare!
|
|
||||||
: "sol";
|
|
||||||
const logo = LOGO[slot as keyof typeof LOGO];
|
|
||||||
// offset*: transform'dan etkilenmez (çekmece o sırada kayıyor).
|
|
||||||
let sol = 0;
|
|
||||||
let ust = 0;
|
|
||||||
for (
|
|
||||||
let d: HTMLElement | null = el;
|
|
||||||
d && d !== sahne;
|
|
||||||
d = d.offsetParent as HTMLElement | null
|
|
||||||
) {
|
|
||||||
sol += d.offsetLeft;
|
|
||||||
ust += d.offsetTop;
|
|
||||||
}
|
|
||||||
const w = el.offsetWidth;
|
|
||||||
const h = el.offsetHeight;
|
|
||||||
const boy = logo.boyut * olcek;
|
|
||||||
const sx = boy / w;
|
|
||||||
const sy = boy / h;
|
|
||||||
const dx = logoX + logo.x * olcek + boy / 2 - (sol + w / 2);
|
|
||||||
const dy = logoY + logo.y * olcek + boy / 2 - (ust + h / 2);
|
|
||||||
// Ölçek köşeyi de küçültür; logodaki köşe oranı için ters ölçekli elips.
|
|
||||||
const rx = (LOGO_RX * olcek) / sx;
|
|
||||||
const ry = (LOGO_RX * olcek) / sy;
|
|
||||||
const logoKose = `${rx}px / ${ry}px`;
|
|
||||||
const kose = getComputedStyle(el).borderTopLeftRadius;
|
|
||||||
const gecikme =
|
|
||||||
BASLANGIC +
|
|
||||||
SLOT_SIRASI.indexOf(slot as (typeof SLOT_SIRASI)[number]) * SLOT_ADIMI;
|
|
||||||
sonKare = Math.max(sonKare, gecikme);
|
|
||||||
|
|
||||||
el.style.opacity = "0";
|
|
||||||
el.style.backgroundColor = logo.renk;
|
|
||||||
dokunulan.push(el);
|
|
||||||
const k = animate(
|
|
||||||
el,
|
|
||||||
{
|
|
||||||
opacity: [0, 1, 1, 1],
|
|
||||||
x: [dx, dx, dx, 0],
|
|
||||||
y: [dy, dy, dy, 0],
|
|
||||||
scaleX: [sx * 0.4, sx, sx, 1],
|
|
||||||
scaleY: [sy * 0.4, sy, sy, 1],
|
|
||||||
borderRadius: [logoKose, logoKose, logoKose, `${kose} / ${kose}`],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: KARE_SURE,
|
|
||||||
delay: gecikme,
|
|
||||||
times: KARE_ZAMANLARI,
|
|
||||||
ease: [GUCLU_CIKIS, "linear", YERLESME],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
k.then(() => {
|
|
||||||
el.style.borderRadius = "";
|
|
||||||
});
|
|
||||||
kontroller.push(k);
|
|
||||||
// Yerine büyürken logo renginden karenin kendi rengine (sınıftaki renk).
|
|
||||||
sayaclar.push(
|
|
||||||
window.setTimeout(
|
|
||||||
() => {
|
|
||||||
el.style.transition = "background-color 450ms ease-out";
|
|
||||||
el.style.backgroundColor = "";
|
|
||||||
},
|
|
||||||
(gecikme + KARE_SURE * KARE_ZAMANLARI[2]) * 1000,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Array.from(el.children).forEach((satir, j) =>
|
|
||||||
netlestir(
|
|
||||||
satir as HTMLElement,
|
|
||||||
gecikme + ICERIK_BASI + i * 0.06 + j * 0.05,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
sonra.forEach((el, i) =>
|
|
||||||
netlestir(el, sonKare + ICERIK_BASI + 0.3 + i * 0.08),
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
kontroller.forEach((k) => k.stop());
|
|
||||||
sayaclar.forEach((s) => window.clearTimeout(s));
|
|
||||||
for (const el of dokunulan) {
|
|
||||||
el.style.opacity = "";
|
|
||||||
el.style.filter = "";
|
|
||||||
el.style.transform = "";
|
|
||||||
el.style.transition = "";
|
|
||||||
el.style.backgroundColor = "";
|
|
||||||
el.style.borderRadius = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [azHareket]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={kapRef} className="mt-5">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user