feat: KolayTercih lansman videosu (Remotion, v3)

64 saniyelik, kesintisiz döngü lansman filmi; 9:16 ve 16:9.
- remocn.dev bileşenleri (sheen-slide-in, tracking-in, gradient-scale-cut-text,
  outline-fill-track-text, perspective-marquee, path-ride, zoom-blur, lens-zoom)
  KolayTercih piksel diline uyarlandı (src/kinetik.tsx)
- Sahne içi imleç + imleci takip eden kamera, piksel iz ve tık patlaması
- Sessizlik → BAM kurgusu (lensZoom), punch zoom ve hareket bulanıklığı
- Müzik ve efektler numpy ile sentezlendi (ses/uret.py), zaman.json tek kaynak
- Örnek taban sıralamaları gerçek YÖK Atlas 2026 kayıtları

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-30 20:22:38 +03:00
commit dc5c87b6aa
61 changed files with 11111 additions and 0 deletions

656
src/sahneler-3.tsx Normal file
View File

@@ -0,0 +1,656 @@
import type { ReactNode } from "react";
import { AbsoluteFill, Easing, Sequence, interpolate, useCurrentFrame } from "remotion";
import { Ikon, type IkonAdi } from "./ikonlar";
import { SahneImleci, TakipKamera, imlecKonumu, type Durak } from "./imlec";
import { PikselKonturDolgu, PikselSheen } from "./kinetik";
import { LogoMark } from "./logo";
import { RiskEtiketi, Sahne, TuruncuCta, useDuzen } from "./ortak";
import { Eyebrow, Wordmark } from "./piksel";
import { vurguStil } from "./sahneler-1";
import { Kamera, MaskeSatir, OdakDisiPikseller } from "./sinema";
import { FONT, R, ara, hash, trSayi, yay, zipla } from "./tema";
import { VURUSLAR } from "./vurus";
import ZAMAN from "./zaman.json";
/* ------------------------------------------------------------------ */
/* Off — sessizlik: "Off. Kafan mı karıştı?" imleç kararsız dolanır */
/* ------------------------------------------------------------------ */
export function Off() {
const { dikey, w, h } = useDuzen();
const cx = w / 2 + (dikey ? 170 : 330);
const cy = h / 2 + (dikey ? 160 : 90);
// Kararsız imleç: küçük kıvrımlar, sonra kadrajın ortasına (lens buradan patlar)
const duraklar: Durak[] = [
{ f: 14, x: cx, y: cy },
{ f: 22, x: cx - 40, y: cy + 20, g: 8 },
{ f: 30, x: cx + 26, y: cy - 24, g: 8 },
{ f: 38, x: cx - 18, y: cy + 30, g: 8 },
{ f: 58, x: w / 2, y: h / 2, g: 14 },
];
return (
<Sahne zemin={R.beyaz}>
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center", gap: 20 }}>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 170 : 180, color: R.slate300, lineHeight: 1 }}>
<MaskeSatir gecikme={2} sure={16}>
Off.
</MaskeSatir>
</div>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 76 : 80, color: R.slate500 }}>
<MaskeSatir gecikme={18} sure={18}>
Kafan mı karıştı?
</MaskeSatir>
</div>
</AbsoluteFill>
<SahneImleci duraklar={duraklar} gorunur={[14, 64]} boy={dikey ? 76 : 64} />
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Danışman — BAM: DanışmanlaKonuş */
/* ------------------------------------------------------------------ */
function DanismanlaKonus({ boy }: { boy: number }) {
return (
<span style={{ display: "inline-flex", alignItems: "center", gap: boy * 0.08, fontFamily: FONT.vurgu, fontSize: boy, letterSpacing: "-0.05em", color: R.slate900, whiteSpace: "nowrap" }}>
<LogoMark boy={boy * 1.25} varyant="nabiz" turuncu girisYok />
<span>
<span style={{ fontWeight: 300 }}>Danışmanla</span>
<span style={{ fontWeight: 600 }}>Konuş</span>
</span>
</span>
);
}
export function Danisman() {
const frame = useCurrentFrame();
const { dikey, w, h } = useDuzen();
// BAM (15. kare, lens kesmesi) anında büyükten oturur
const otur = interpolate(frame, [15, 26], [1.3, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) });
// Patlayan pikseller
const kareler: ReactNode[] = [];
const d = frame - 15;
if (d >= 0 && d < 40) {
for (let n = 0; n < 28; n++) {
const aci = (n / 28) * Math.PI * 2 + hash(n, 2, 7);
const uz = (dikey ? 260 : 300) + Easing.out(Easing.cubic)(Math.min(1, d / 30)) * (dikey ? 420 : 620) * (0.5 + hash(n, 3, 1) * 0.5);
const G = 24;
const x = Math.round((w / 2 + Math.cos(aci) * uz) / G) * G;
const y = Math.round((h / 2 - 40 + Math.sin(aci) * uz * 0.7) / G) * G;
const b = 30 * (1 - d / 42);
kareler.push(<rect key={n} x={x - b / 2} y={y - b / 2} width={b} height={b} rx={b * 0.27} fill={[R.turuncu, R.logoSeftali, R.primary][n % 3]} fillOpacity={1 - d / 40} />);
}
}
return (
<Sahne zemin={R.beyaz}>
<OdakDisiPikseller seed={61} adet={8} />
<Kamera anahtarlar={[{ f: 0, s: 1.05 }, { f: 80, s: 1.12 }]} vuruslar={VURUSLAR.danisman}>
<svg width={w} height={h} style={{ position: "absolute", inset: 0 }}>
{kareler}
</svg>
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center", gap: 46 }}>
<div style={{ transform: `scale(${otur})` }}>
<DanismanlaKonus boy={dikey ? 110 : 150} />
</div>
<PikselSheen gecikme={28} fontSize={dikey ? 90 : 100}>
Aklına takılanı sor.
</PikselSheen>
<div style={{ fontSize: dikey ? 40 : 40, color: R.slate500 }}>
<MaskeSatir gecikme={40}>Gece 3'te bile. Sıkılmaz, yargılamaz.</MaskeSatir>
</div>
</AbsoluteFill>
</Kamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Sohbet — tam ekran danışman; imleç yalnız yazarken/gönderirken */
/* ------------------------------------------------------------------ */
// Gerçek YÖK Atlas 2026 verisi: İzmir, İngilizce Bilgisayar Mühendisliği, devlet
const SONUCLAR = [
{ uni: "Ege Üniversitesi", taban: 44589, risk: "riskli" as const },
{ uni: "Dokuz Eylül Üniversitesi", taban: 52071, risk: "az-riskli" as const },
{ uni: "İzmir Katip Çelebi Üniversitesi", taban: 71833, risk: "guvenli" as const },
];
const TS = ZAMAN.tiklar.sohbet; // [girdi1, gönder1, girdi2, gönder2]
const YS = ZAMAN.yazma.sohbet;
type Mesaj =
| { tur: "kullanici"; metin: string; yaz: number[]; gonder: number }
| { tur: "arac"; ad: string; detay: string; bas: number; bitis: number }
| { tur: "cevap"; bas: number; tahmin: number; Icerik: () => ReactNode };
function SonucListesi({ olcek }: { olcek: number }) {
const frame = useCurrentFrame();
return (
<div style={{ display: "flex", flexDirection: "column", gap: 14 * olcek }}>
<div>Sıralamana göre İzmir'de üç devlet seçeneği var:</div>
{SONUCLAR.map((s, i) => {
const g = yay(frame, 100 + i * 7, 16);
return (
<div key={s.uni} style={{ display: "flex", alignItems: "center", gap: 20 * olcek, background: R.beyaz, border: `3px solid ${R.slate200}`, borderRadius: 24 * olcek, padding: `${16 * olcek}px ${22 * olcek}px`, opacity: g, transform: `translateY(${(1 - g) * 14}px)` }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, color: R.slate900 }}>{s.uni}</div>
<div style={{ fontSize: "0.72em", color: R.slate500 }}>Bilgisayar Mühendisliği · Devlet</div>
</div>
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4 }}>
<RiskEtiketi risk={s.risk} olcek={2 * olcek} />
<span style={{ fontSize: "0.72em", color: R.slate500 }}>
Taban <b style={{ color: R.slate900 }}>{trSayi(s.taban)}</b>
</span>
</div>
</div>
);
})}
</div>
);
}
export function Sohbet() {
const frame = useCurrentFrame();
const { dikey, w, h } = useDuzen();
const o = dikey ? 1 : 0.86; // ölçek
const L = dikey
? { x: 50, w: 980, ustY: 80, ustH: 130, altY: 1630, altH: 170 }
: { x: 260, w: 1400, ustY: 30, ustH: 110, altY: 900, altH: 130 };
const gonderX = L.x + L.w - L.altH / 2 - 10;
const altMerkez = L.altY + L.altH / 2;
const SENARYO: Mesaj[] = [
{ tur: "kullanici", metin: "İzmir'de İngilizce bilgisayar mühendisliği var mı?", yaz: YS[0], gonder: TS[1] },
{ tur: "arac", ad: "program_ara", detay: "İzmir · İngilizce", bas: TS[1] + 6, bitis: TS[1] + 32 },
{ tur: "cevap", bas: 90, tahmin: 620 * o, Icerik: () => <SonucListesi olcek={o} /> },
{ tur: "kullanici", metin: "Aslında İzmir dışına çıkmak istemiyorum.", yaz: YS[1], gonder: TS[3] },
{ tur: "arac", ad: "profil_guncelle", detay: "şehir: İzmir", bas: TS[3] + 6, bitis: TS[3] + 22 },
{
tur: "cevap",
bas: 224,
tahmin: 260 * o,
Icerik: () => (
<div style={{ display: "flex", flexDirection: "column", gap: 16, alignItems: "flex-start" }}>
<div>Tamam, profilini güncelledim. Yeni listende yalnız İzmir'deki programlar olacak.</div>
<span style={{ display: "inline-flex", alignItems: "center", gap: 10, height: 64 * o, padding: `0 ${24 * o}px`, borderRadius: 999, background: R.primary, color: R.beyaz, fontWeight: 600, fontSize: "0.85em", transform: `scale(${Math.max(0, zipla(frame, 242))})`, transformOrigin: "left center" }}>
<Ikon ad="konum" boy={28 * o} />
Şehir: İzmir
<Ikon ad="tik" boy={28 * o} kalinlik={3} />
</span>
</div>
),
},
];
const yazilan = (() => {
for (const m of SENARYO) {
if (m.tur !== "kullanici") continue;
if (frame >= m.yaz[0] && frame < m.gonder) {
const n = Math.floor(ara(frame, [m.yaz[0], m.yaz[1]], [0, m.metin.length], (t) => t));
return m.metin.slice(0, n);
}
}
return "";
})();
const basis = TS.some((t, i) => i % 2 === 1 && frame >= t - 2 && frame < t + 3);
const girdiX = L.x + 260;
const imlec1: Durak[] = [
{ f: 8, x: w * 0.72, y: altMerkez - 260 },
{ f: TS[0], x: girdiX, y: altMerkez + 10, g: 12 },
{ f: TS[1], x: gonderX, y: altMerkez, g: 12 },
{ f: 72, x: gonderX - 60, y: altMerkez - 220, g: 12 },
];
const imlec2: Durak[] = [
{ f: 156, x: w * 0.7, y: altMerkez - 300 },
{ f: TS[2], x: girdiX, y: altMerkez + 10, g: 10 },
{ f: TS[3], x: gonderX, y: altMerkez, g: 12 },
{ f: 216, x: gonderX - 80, y: altMerkez - 240, g: 12 },
];
const nokta = (f: number) => (f < 120 ? imlecKonumu(imlec1, f) : imlecKonumu(imlec2, f));
return (
<Sahne>
<TakipKamera
nokta={nokta}
vuruslar={VURUSLAR.sohbet}
anahtarlar={[
{ f: 0, s: 1, t: 0 },
{ f: TS[0], s: 1.3, t: 0.8 },
{ f: TS[1] + 4, s: 1.3, t: 0.8 },
{ f: 80, s: 1.02, t: 0 },
{ f: 150, s: 1.04, t: 0 },
{ f: TS[2], s: 1.3, t: 0.8 },
{ f: TS[3] + 4, s: 1.3, t: 0.8 },
{ f: 214, s: 1.02, t: 0 },
{ f: 300, s: 1.06, t: 0 },
]}
>
{/* Üst şerit */}
<div style={{ position: "absolute", left: L.x, top: L.ustY, width: L.w, height: L.ustH, display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: `3px solid ${R.slate200}` }}>
<DanismanlaKonus boy={dikey ? 52 : 50} />
<span style={{ display: "inline-flex", alignItems: "center", height: 64 * o, padding: `0 ${26 * o}px`, borderRadius: 999, border: `3px solid ${R.slate200}`, fontSize: 28 * o, fontWeight: 600, color: R.slate700 }}>
{frame >= TS[3] ? 58 : frame >= TS[1] ? 59 : 60} soru hakkı
</span>
</div>
{/* Mesajlar */}
<div
style={{
position: "absolute",
left: L.x,
width: L.w,
top: L.ustY + L.ustH + 20,
height: L.altY - (L.ustY + L.ustH + 20) - 20,
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
gap: 26 * o,
overflow: "hidden",
fontSize: 40 * o,
lineHeight: 1.4,
}}
>
{SENARYO.map((m, i) => {
const bas = m.tur === "kullanici" ? m.gonder : m.bas;
if (frame < bas) return null;
const g = yay(frame, bas, 16);
const tahmin = m.tur === "kullanici" ? 200 * o : m.tur === "arac" ? 90 * o : m.tahmin;
const kap = { maxHeight: g >= 0.999 ? undefined : g * tahmin, overflow: "hidden" as const, flexShrink: 0 };
if (m.tur === "kullanici") {
return (
<div key={i} style={{ ...kap, display: "flex", justifyContent: "flex-end" }}>
<div style={{ maxWidth: "82%", background: R.primary, color: R.beyaz, padding: `${22 * o}px ${30 * o}px`, borderRadius: 34 * o, fontWeight: 600, opacity: g, transform: `translateY(${(1 - g) * 20}px)` }}>{m.metin}</div>
</div>
);
}
if (m.tur === "arac") {
const bitti = frame >= m.bitis;
return (
<div key={i} style={{ ...kap, display: "flex", alignItems: "center", gap: 16 }}>
<span style={{ width: 64 * o, height: 64 * o, borderRadius: 999, border: `3px solid ${R.slate200}`, background: R.beyaz, display: "flex", alignItems: "center", justifyContent: "center" }}>
<LogoMark boy={40 * o} varyant={bitti ? "duruk" : "nabiz"} girisYok />
</span>
<span style={{ display: "inline-flex", alignItems: "center", gap: 12, height: 60 * o, padding: `0 ${24 * o}px`, borderRadius: 999, background: R.slate50, border: `3px solid ${R.slate200}`, fontSize: 26 * o, fontWeight: 600, color: R.slate600, opacity: g, whiteSpace: "nowrap" }}>
{bitti ? <Ikon ad="tik" boy={26 * o} renk={R.guvenliYazi} kalinlik={3} /> : <Ikon ad="arama" boy={26 * o} renk={R.primary} />}
<span style={{ fontFamily: FONT.vurgu, color: R.primary }}>{m.ad}</span>
<span>· {m.detay}</span>
{!bitti ? <span style={{ color: R.slate400 }}>· YÖK Atlas'ta arıyor{".".repeat(1 + (Math.floor(frame / 6) % 3))}</span> : null}
</span>
</div>
);
}
return (
<div key={i} style={{ ...kap, paddingLeft: 80 * o }}>
<div style={{ maxWidth: "94%", background: R.slate100, color: R.slate800, padding: `${24 * o}px ${30 * o}px`, borderRadius: 34 * o, opacity: g, transform: `translateY(${(1 - g) * 20}px)` }}>
<m.Icerik />
</div>
</div>
);
})}
</div>
{/* Yazma kutusu */}
<div
style={{
position: "absolute",
left: L.x,
top: L.altY,
width: L.w,
height: L.altH,
display: "flex",
alignItems: "center",
gap: 20,
borderRadius: 48 * o,
border: `4px solid ${yazilan ? R.primary : R.slate200}`,
boxShadow: yazilan ? "0 0 0 10px rgba(59,130,246,0.16)" : undefined,
background: R.beyaz,
padding: `0 16px 0 ${44 * o}px`,
fontSize: 42 * o,
}}
>
<span style={{ flex: 1, color: yazilan ? R.slate900 : R.slate400, whiteSpace: "nowrap", overflow: "hidden" }}>
{yazilan || "Listen hakkında bir şey sor…"}
{yazilan && Math.floor(frame / 8) % 2 === 0 ? <span style={{ color: R.primary }}>|</span> : null}
</span>
<span style={{ width: L.altH - 36, height: L.altH - 36, borderRadius: 999, background: basis ? R.turuncuKoyu : R.turuncu, display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${basis ? 0.93 : 1})`, flexShrink: 0 }}>
<Ikon ad="ok" boy={56 * o} renk={R.beyaz} kalinlik={2.6} style={{ transform: "rotate(-90deg)" }} />
</span>
</div>
<div style={{ position: "absolute", left: 0, width: w, top: dikey ? h - 100 : h - 50, display: "flex", justifyContent: "center", opacity: yay(frame, 262, 16) }}>
<Eyebrow olcek={dikey ? 2.3 : 2}>Havuz dışına çıkamaz, uydurma bölüm önermez</Eyebrow>
</div>
<SahneImleci duraklar={imlec1} tiklar={[TS[0], TS[1]]} gorunur={[8, 74]} boy={dikey ? 76 : 64} />
<SahneImleci duraklar={imlec2} tiklar={[TS[2], TS[3]]} gorunur={[156, 218]} boy={dikey ? 76 : 64} />
</TakipKamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Paket — Apple hızlı kesmeleri: trend, PDF, revizyon */
/* ------------------------------------------------------------------ */
const TREND = [37831, 27534, 21602, 30797, 42108, 55158]; // AYBÜ Bilgisayar, 2021–2026
function PaketKarti({ ikon, baslik, alt, children }: { ikon: IkonAdi; baslik: string; alt: string; children: ReactNode }) {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
return (
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: dikey ? 60 : 40, flexDirection: dikey ? "column" : "row", padding: dikey ? "0 70px" : "0 160px" }}>
<div style={{ display: "flex", flexDirection: "column", alignItems: dikey ? "center" : "flex-start", textAlign: dikey ? "center" : "left", gap: 24, flex: dikey ? undefined : 1 }}>
<div style={{ width: 120, height: 120, borderRadius: 32, background: "rgba(59,130,246,0.1)", display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${Math.max(0, zipla(frame, 0))})` }}>
<Ikon ad={ikon} boy={58} renk={R.primary} />
</div>
<PikselSheen gecikme={2} fontSize={dikey ? 110 : 120}>
{baslik}
</PikselSheen>
<div style={{ fontSize: dikey ? 46 : 46, color: R.slate500, lineHeight: 1.3 }}>
<MaskeSatir gecikme={10}>{alt}</MaskeSatir>
</div>
</div>
<div style={{ flex: dikey ? undefined : 1, display: "flex", justifyContent: "center" }}>{children}</div>
</AbsoluteFill>
);
}
function TrendGorseli() {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
const gW = dikey ? 860 : 760;
const gH = dikey ? 420 : 440;
const noktalar = TREND.map((v, i) => [(i / (TREND.length - 1)) * gW, ((v - 15000) / 45000) * gH]);
const c = ara(frame, [6, 34], [0, 1]);
const yol = noktalar.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join(" ");
return (
<svg width={gW + 40} height={gH + 90} viewBox={`-20 -20 ${gW + 40} ${gH + 90}`}>
<path d={yol} fill="none" stroke={R.primary} strokeWidth={10} strokeLinecap="round" strokeLinejoin="round" pathLength={1} strokeDasharray={1} strokeDashoffset={1 - c} />
{noktalar.map((p, i) => (
<g key={i} opacity={c >= i / (TREND.length - 1) ? 1 : 0}>
<rect x={p[0] - 16} y={p[1] - 16} width={32} height={32} rx={9} fill={i === TREND.length - 1 ? R.turuncu : R.primary} />
<text x={p[0]} y={gH + 50} textAnchor="middle" fontSize={28} fontFamily="Clarity City" fontWeight={600} fill={R.slate400}>
{2021 + i}
</text>
</g>
))}
</svg>
);
}
function PdfGorseli() {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
const k = dikey ? 1.6 : 1.5;
return (
<div style={{ position: "relative", width: 330 * k, height: 380 * k }}>
{[2, 1, 0].map((i) => {
const s = yay(frame, 4 + (2 - i) * 5, 18);
return (
<div
key={i}
style={{
position: "absolute",
left: (60 + i * 30) * k,
top: (20 + i * 22) * k,
width: 220 * k,
height: 290 * k,
borderRadius: 26 * k,
background: R.beyaz,
border: `3px solid ${R.slate200}`,
boxShadow: "0 18px 40px rgba(15,23,42,0.08)",
transform: `rotate(${(i - 1) * 7 * s}deg)`,
padding: 26 * k,
display: "flex",
flexDirection: "column",
gap: 16 * k,
}}
>
{i === 0 ? (
<>
<div style={{ display: "flex", gap: 8 * k }}>
{[R.riskli, R.azRiskli, R.guvenli].map((c) => (
<span key={c} style={{ width: 24 * k, height: 24 * k, borderRadius: 7 * k, background: c }} />
))}
</div>
{[1, 0.8, 0.9, 0.6, 0.75, 0.5].map((wd, j) => (
<span key={j} style={{ height: 14 * k, width: `${wd * 100}%`, borderRadius: 7 * k, background: R.slate200 }} />
))}
</>
) : null}
</div>
);
})}
</div>
);
}
const REVIZYON = [
{ ad: "Ege · Bilgisayar Müh.", renk: R.riskli },
{ ad: "Dokuz Eylül · Bilgisayar Müh.", renk: R.azRiskli },
{ ad: "Katip Çelebi · Bilgisayar Müh.", renk: R.guvenli },
];
function RevizyonGorseli() {
const frame = useCurrentFrame(); // alt dizi içi kare (0–45)
const { dikey } = useDuzen();
const [tut, birak] = ZAMAN.tiklar.paket.map((t) => t - 90);
const SAT = dikey ? 130 : 120;
const g = dikey ? 880 : 760;
const ilerle = ara(frame, [tut, birak], [0, 1], Easing.inOut(Easing.cubic));
const tutuldu = frame >= tut && frame < birak + 2;
return (
<div style={{ position: "relative", width: g, height: 3 * SAT }}>
{REVIZYON.map((r, i) => {
const gecis = i === 1 ? (2 * ilerle - 0.5) / 0.8 : (2 * ilerle - 1.3) / 0.7;
const kay = Easing.inOut(Easing.cubic)(Math.min(1, Math.max(0, gecis)));
const y = i === 0 ? ilerle * 2 * SAT : (i - kay) * SAT;
const kalk = i === 0 && tutuldu ? 1 : 0;
return (
<div
key={r.ad}
style={{
position: "absolute",
left: kalk * 24,
top: y,
width: "100%",
height: SAT - 20,
borderRadius: 30,
border: `4px solid ${kalk ? R.turuncu : R.slate200}`,
background: kalk ? "#fff7ed" : R.beyaz,
display: "flex",
alignItems: "center",
gap: 24,
padding: "0 32px",
fontSize: dikey ? 40 : 38,
fontWeight: 600,
color: R.slate800,
boxShadow: kalk ? "0 24px 48px rgba(15,23,42,0.16)" : undefined,
transform: `scale(${1 + kalk * 0.04}) rotate(${kalk * -1.5}deg)`,
zIndex: i === 0 ? 2 : 1,
}}
>
<span style={{ width: 24, height: 24, borderRadius: 7, background: r.renk }} />
<span style={{ flex: 1 }}>{r.ad}</span>
<span style={{ display: "grid", gridTemplateColumns: "repeat(2, 9px)", gap: 6 }}>
{Array.from({ length: 6 }, (_, k) => (
<span key={k} style={{ width: 9, height: 9, borderRadius: 3, background: R.slate400 }} />
))}
</span>
</div>
);
})}
</div>
);
}
export function Paket() {
const { dikey, w, h } = useDuzen();
const [tut, birak] = ZAMAN.tiklar.paket;
// Revizyon kartında imleç ilk satırı tutup en alta sürükler (kamera takip eder)
const SAT = dikey ? 130 : 120;
const gW = dikey ? 880 : 760;
const kartX = dikey ? (w - gW) / 2 : w * 0.5 + 60 + (w * 0.5 - 220 - gW) / 2;
const kartY = dikey ? h / 2 + 230 : h / 2 - (3 * SAT) / 2;
const tutX = kartX + gW - 50;
const duraklar: Durak[] = [
{ f: 92, x: tutX + 120, y: kartY + 360 },
{ f: tut, x: tutX, y: kartY + (SAT - 20) / 2, g: 8 },
{ f: birak, x: tutX + 24, y: kartY + 2 * SAT + (SAT - 20) / 2, g: birak - tut },
{ f: 134, x: tutX + 90, y: kartY + 2 * SAT + 120, g: 10 },
];
const nokta = (f: number) => imlecKonumu(duraklar, f);
return (
<Sahne>
<OdakDisiPikseller seed={71} adet={6} guc={0.6} />
<TakipKamera
nokta={nokta}
vuruslar={VURUSLAR.paket}
anahtarlar={[
{ f: 0, s: 1.06, t: 0 },
{ f: 44, s: 1.0, t: 0 },
{ f: 45, s: 1.06, t: 0 },
{ f: 89, s: 1.0, t: 0 },
{ f: 90, s: 1.04, t: 0 },
{ f: tut, s: dikey ? 1.15 : 1.08, t: dikey ? 0.35 : 0.12 },
{ f: birak, s: dikey ? 1.15 : 1.08, t: dikey ? 0.35 : 0.12 },
{ f: 135, s: 1.05, t: 0.1 },
]}
>
<Sequence durationInFrames={45}>
<PaketKarti ikon="trend" baslik="6 yıllık trend." alt="Her tercihin geçmişi, tek bakışta.">
<TrendGorseli />
</PaketKarti>
</Sequence>
<Sequence from={45} durationInFrames={45}>
<PaketKarti ikon="belge" baslik="PDF rapor." alt="Aile toplantısına hazır.">
<PdfGorseli />
</PaketKarti>
</Sequence>
<Sequence from={90}>
<PaketKarti ikon="yenile" baslik="2 revizyon." alt="Fikrin değişir, listen de.">
<RevizyonGorseli />
</PaketKarti>
</Sequence>
<SahneImleci duraklar={duraklar} tiklar={[tut, birak]} gorunur={[92, 136]} boy={dikey ? 76 : 64} />
</TakipKamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Dürüstlük — Outline Fill Track: "Çünkü kimse veremez." */
/* ------------------------------------------------------------------ */
export function Durust() {
const frame = useCurrentFrame();
const { dikey, w } = useDuzen();
const alt = yay(frame, 86, 18);
return (
<Sahne zemin={R.beyaz}>
<Kamera anahtarlar={[{ f: 0, s: 1.02 }, { f: 140, s: 1.12 }]} vuruslar={VURUSLAR.durust}>
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center", gap: 40 }}>
<div style={{ width: 140, height: 140, borderRadius: 38, background: R.primary, display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${Math.max(0, zipla(frame, 0))})` }}>
<Ikon ad="kalkan" boy={76} renk={R.beyaz} />
</div>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 104 : 116, lineHeight: 1.06 }}>
<MaskeSatir gecikme={6}>Yerleşme garantisi</MaskeSatir>
<MaskeSatir gecikme={14}>vermiyoruz.</MaskeSatir>
</div>
<PikselKonturDolgu on="Çünkü" deger="kimse veremez." fontSize={dikey ? 84 : 100} gecikme={30} genislik={w} />
<div style={{ opacity: alt, transform: `translateY(${(1 - alt) * 20}px)`, display: "flex", flexDirection: "column", alignItems: "center", gap: 24, marginTop: 10 }}>
<div style={{ fontSize: 40, color: R.slate600, fontWeight: 600, maxWidth: dikey ? 900 : 1500, lineHeight: 1.4 }}>
Veren olursa, uzak dur. Biz riski satır satır gösteririz.
</div>
<div style={{ display: "flex", gap: 44 }}>
{(["guvenli", "az-riskli", "riskli"] as const).map((r, i) => (
<span key={r} style={{ transform: `scale(${Math.max(0, zipla(frame, 94 + i * 5))})` }}>
<RiskEtiketi risk={r} olcek={3.2} />
</span>
))}
</div>
</div>
</AbsoluteFill>
</Kamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Hazır — sessizlik */
/* ------------------------------------------------------------------ */
export function Hazir() {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
const kare = dikey ? 64 : 56;
return (
<Sahne zemin={R.beyaz}>
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: 50 }}>
<div style={{ display: "flex", gap: kare * 0.22 }}>
{[R.logoMavi, R.logoSeftali, R.logoTuruncu].map((c, i) => (
<div key={c} style={{ width: kare, height: kare, borderRadius: kare * 0.23, background: c, transform: `scale(${Math.max(0, zipla(frame, 4 + i * 6))})` }} />
))}
</div>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 70 : 66, color: R.slate500 }}>
<MaskeSatir gecikme={8}>Hazır mısın?</MaskeSatir>
</div>
</AbsoluteFill>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Final — logo + tipografi + buton; tık → çöküş → boş ekran (döngü) */
/* ------------------------------------------------------------------ */
export function Final() {
const frame = useCurrentFrame();
const { dikey, w, h } = useDuzen();
const [tik] = ZAMAN.tiklar.final;
const otur = interpolate(frame, [15, 26], [1.2, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) });
const bas = ara(frame, [tik - 2, tik + 1], [0, 1]) * (1 - ara(frame, [tik + 5, tik + 10], [0, 1]));
const cok = ara(frame, [tik + 4, tik + 30], [0, 1], Easing.bezier(0.7, 0, 0.84, 0));
// Buton kadrajda sabit bir noktada (imleç ve çöküş kökeni)
const butonY = dikey ? h / 2 + 350 : h / 2 + 260;
const duraklar: Durak[] = [
{ f: 84, x: w / 2 + (dikey ? 300 : 520), y: butonY + (dikey ? 380 : 200) },
{ f: tik - 4, x: w / 2 + 60, y: butonY + 8, g: 26 },
{ f: tik, x: w / 2 + 60, y: butonY + 8 },
{ f: 170, x: w / 2, y: h / 2, g: 30 },
];
const nokta = (f: number) => imlecKonumu(duraklar, f);
return (
<Sahne>
<TakipKamera
nokta={nokta}
vuruslar={VURUSLAR.final}
anahtarlar={[
{ f: 0, s: 1.06, t: 0 },
{ f: 90, s: 1, t: 0 },
{ f: tik - 6, s: 1.4, t: 1 },
{ f: tik + 10, s: 1.4, t: 1 },
{ f: tik + 36, s: 1, t: 0 },
]}
>
<AbsoluteFill
style={{
alignItems: "center",
justifyContent: "center",
textAlign: "center",
gap: dikey ? 70 : 54,
transformOrigin: `50% ${(butonY / h) * 100}%`,
transform: `scale(${otur * (1 - cok)})`,
opacity: 1 - ara(frame, [tik + 18, tik + 30], [0, 1]),
filter: `blur(${cok * 10}px)`,
}}
>
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", alignItems: "center", gap: dikey ? 14 : 26 }}>
<LogoMark boy={dikey ? 220 : 180} girisYok rotaBas={24} />
<Wordmark boy={dikey ? 116 : 116} />
</div>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 100 : 108, lineHeight: 1.06 }}>
<MaskeSatir gecikme={20}>Listeni kur,</MaskeSatir>
<MaskeSatir gecikme={28}>
<span style={vurguStil}>panikle</span> değil.
</MaskeSatir>
</div>
<div style={{ transform: `scale(${Math.max(0, zipla(frame, 38))})` }}>
<TuruncuCta metin="kolaytercih.com" boy={dikey ? 1.25 : 1.15} basili={bas} />
</div>
</AbsoluteFill>
<SahneImleci duraklar={duraklar} tiklar={[tik]} gorunur={[84, 176]} boy={dikey ? 80 : 68} />
</TakipKamera>
</Sahne>
);
}