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

742
src/eski/sahneler-urun.tsx Normal file
View File

@@ -0,0 +1,742 @@
import type { ReactNode } from "react";
import { AbsoluteFill, Easing, useCurrentFrame } from "remotion";
import { YagmurHaritasi } from "./harita";
import { Ikon, type IkonAdi } from "./ikonlar";
import { LogoMark } from "./logo";
import { BaslikBlogu, Kutu, RiskEtiketi, Sahne, TuruncuCta, useDuzen } from "./ortak";
import { Eyebrow, PixelCorner, Wordmark } from "./piksel";
import { Kamera, MaskeSatir, OdakDisiPikseller, degerAl, type Anahtar } from "./sinema";
import { Telefon } from "./telefon";
import { FONT, R, ara, trSayi, yay, zipla } from "./tema";
import { VURUSLAR } from "./vurus";
const vurguStil = {
fontFamily: FONT.vurgu,
fontStyle: "italic" as const,
fontWeight: 800,
letterSpacing: "-0.04em",
color: R.primary,
};
/* ------------------------------------------------------------------ */
/* Adım 1 — 3B telefonda sihirbaz: sıralama, çipler, CTA */
/* ------------------------------------------------------------------ */
const CIPLER = ["Mühendislik", "Bilgisayar", "İstanbul", "Ankara", "İzmir", "Devlet"];
// Seçim kareleri imleç tıklarıyla aynı (zaman.json)
const SECIM: Record<number, number> = { 0: 58, 1: 66, 2: 74, 3: 82, 5: 92 };
function SihirbazEkrani() {
const frame = useCurrentFrame();
const metin = "45.320";
const yazilan = metin.slice(0, Math.max(0, Math.floor((frame - 22) / 4)));
const odak = frame >= 20;
const imlecGorunur = Math.floor(frame / 8) % 2 === 0 || yazilan.length < metin.length;
const cta = zipla(frame, 96);
const bas = ara(frame, [122, 125], [0, 1]) * (1 - ara(frame, [129, 134], [0, 1]));
const sayfa = yay(frame, 128, 22);
return (
<div style={{ padding: "0 20px", fontFamily: FONT.govde, color: R.slate900 }}>
{/* Site üst çubuğu */}
<div style={{ height: 56, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
<LogoMark boy={28} varyant="duruk" girisYok />
<Wordmark boy={21} />
</span>
<span style={{ width: 40, height: 40, borderRadius: 999, border: `1px solid ${R.slate200}`, display: "flex", alignItems: "center", justifyContent: "center" }}>
<Ikon ad="kullanici" boy={16} renk={R.slate600} />
</span>
</div>
<div style={{ marginTop: 18 }}>
<Eyebrow olcek={1}>Adım 1 / 3</Eyebrow>
<div style={{ marginTop: 14, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 30, lineHeight: 1.1 }}>Sıralamanı gir</div>
<div style={{ marginTop: 8, fontSize: 16, lineHeight: 1.5, color: R.slate600 }}>Sana uyan programları bununla bulacağız.</div>
<div style={{ marginTop: 22, fontSize: 14, fontWeight: 600, color: R.slate700 }}>Başarı sıralaman</div>
<div
data-iz="a1-girdi"
style={{
marginTop: 8,
height: 48,
borderRadius: 999,
border: `1px solid ${odak ? R.primary : R.slate200}`,
boxShadow: odak ? "0 0 0 3px rgba(59,130,246,0.25)" : undefined,
display: "flex",
alignItems: "center",
gap: 12,
padding: "0 20px",
fontSize: 16,
}}
>
<Ikon ad="kupa" boy={16} renk={R.slate400} />
<span style={{ fontWeight: 600 }}>
{yazilan}
{odak && imlecGorunur ? <span style={{ color: R.primary, fontWeight: 400 }}>|</span> : null}
{!yazilan && !odak ? <span style={{ color: R.slate400, fontWeight: 400 }}>Örn. 45.320</span> : null}
</span>
</div>
<div style={{ marginTop: 20, fontSize: 14, fontWeight: 600, color: R.slate700 }}>Neyi seviyorsun, nerede okumak istersin?</div>
<div style={{ marginTop: 10, display: "flex", flexWrap: "wrap", gap: 8 }}>
{CIPLER.map((c, i) => {
const an = SECIM[i];
const secili = an != null && frame >= an;
const tik = an != null ? ara(frame, [an, an + 2], [0, 1]) * (1 - ara(frame, [an + 3, an + 7], [0, 1])) : 0;
return (
<span
key={c}
data-iz={`a1-cip-${i}`}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
height: 40,
padding: "0 14px",
borderRadius: 999,
border: `1px solid ${secili ? R.primary : R.slate200}`,
background: secili ? R.primary : R.beyaz,
color: secili ? R.beyaz : R.slate700,
fontSize: 14,
fontWeight: 600,
transform: `scale(${1 - 0.04 * tik})`,
}}
>
{i >= 2 && i <= 4 ? <Ikon ad="konum" boy={14} /> : i === 5 ? <Ikon ad="devlet" boy={14} /> : null}
{c}
</span>
);
})}
</div>
<div style={{ marginTop: 26, opacity: Math.min(1, Math.max(0, cta)), transform: `scale(${0.85 + 0.15 * cta})` }}>
<span data-iz="a1-cta" style={{ display: "block" }}>
<TuruncuCta metin="Listemi oluştur" boy={16 / 40} basili={bas} style={{ width: "100%", height: 48 }} />
</span>
</div>
</div>
{/* CTA'dan sonra: liste hazırlanıyor paneli (3. adıma köprü) */}
<div
style={{
position: "absolute",
left: 12,
right: 12,
bottom: 16,
padding: 18,
borderRadius: 24,
background: R.beyaz,
border: `1px solid ${R.slate200}`,
boxShadow: "0 -10px 30px rgba(15,23,42,0.10)",
transform: `translateY(${(1 - sayfa) * 260}px)`,
}}
>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 20 }}>Listen hazırlanıyor</div>
<div style={{ marginTop: 4, fontSize: 14, color: R.slate500 }}>6 yılın verisi taranıyor…</div>
<div style={{ marginTop: 12, display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap: 5 }}>
{Array.from({ length: 24 }, (_, i) => {
const on = frame >= 134 + i * 1.2;
const renk = i < 5 ? R.riskli : i < 18 ? R.azRiskli : R.guvenli;
return <span key={i} style={{ aspectRatio: "1", borderRadius: 5, background: on ? renk : R.slate100 }} />;
})}
</div>
</div>
</div>
);
}
export function Adim1() {
const frame = useCurrentFrame();
const { dikey, w, h } = useDuzen();
const telGen = dikey ? 640 : 470;
const telDon = [
{ f: 0, s: -28, x: 12, r: -3 },
{ f: 34, s: -10, x: 5, r: -1 },
{ f: 70, s: 0, x: 0, r: 0 },
{ f: 185, s: 9, x: 4, r: 1 },
].map((a) => ({ f: a.f, s: a.s, x: a.x, r: a.r }));
const donY = degerAl(telDon as Anahtar[], frame, "s", 0);
const donX = degerAl(telDon as Anahtar[], frame, "x", 0);
const donZ = degerAl(telDon as Anahtar[], frame, "r", 0);
const kamera: Anahtar[] = dikey
? [
{ f: 0, s: 0.95, x: 0, y: 60 },
{ f: 24, s: 1.4, x: 0, y: -40 },
{ f: 50, s: 1.4, x: 0, y: -40 },
{ f: 64, s: 1.3, x: 0, y: 140 },
{ f: 100, s: 1.3, x: 0, y: 150 },
{ f: 118, s: 1.42, x: 0, y: 290 },
{ f: 140, s: 1.3, x: 0, y: 420 },
{ f: 185, s: 1, x: 0, y: 60 },
]
: [
{ f: 0, s: 1, x: 0, y: 0 },
{ f: 24, s: 1.6, x: 384, y: -140 },
{ f: 50, s: 1.6, x: 384, y: -140 },
{ f: 64, s: 1.55, x: 384, y: -10 },
{ f: 100, s: 1.55, x: 384, y: 0 },
{ f: 118, s: 1.7, x: 384, y: 90 },
{ f: 140, s: 1.5, x: 384, y: 250 },
{ f: 185, s: 1, x: 0, y: 0 },
];
return (
<Sahne>
<OdakDisiPikseller seed={11} adet={8} />
<Kamera anahtarlar={kamera} vuruslar={VURUSLAR.adim1}>
<AbsoluteFill>
<div
style={{
position: "absolute",
...(dikey
? { left: 70, right: 70, top: 130, textAlign: "center" as const, alignItems: "center" }
: { left: 140, width: 760, top: 0, bottom: 0, justifyContent: "center", alignItems: "flex-start" }),
display: "flex",
flexDirection: "column",
gap: 28,
// Kamera telefona dalınca kenarda yarım yazı kalmasın
opacity: dikey ? 1 : 1 - ara(frame, [14, 26], [0, 1]) + ara(frame, [150, 172], [0, 1]),
}}
>
{!dikey ? (
<MaskeSatir gecikme={4}>
<Eyebrow olcek={2.3}>Adım 1</Eyebrow>
</MaskeSatir>
) : null}
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 84 : 96, lineHeight: 1.05 }}>
<MaskeSatir gecikme={6}>Form doldurmaktan</MaskeSatir>
<MaskeSatir gecikme={12}>
<span style={vurguStil}>kısa</span> sürer.
</MaskeSatir>
</div>
{!dikey ? (
<MaskeSatir gecikme={20}>
<span style={{ display: "block", fontSize: 36, lineHeight: 1.45, color: R.slate600 }}>
Sıralamanı yaz, sevdiğin alanları ve şehirleri seç. Gerisini bir butona bırak.
</span>
</MaskeSatir>
) : null}
</div>
<div
style={{
position: "absolute",
left: (dikey ? w / 2 : w * 0.7) - telGen / 2,
top: (dikey ? h * 0.585 : h / 2) - (telGen * 2.05) / 2,
}}
>
<Telefon genislik={telGen} donX={donX} donY={donY} donZ={donZ}>
<SihirbazEkrani />
</Telefon>
</div>
</AbsoluteFill>
</Kamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Adım 2 — hero haritası dökülür, imleç ısı fırçasıyla tarar */
/* ------------------------------------------------------------------ */
const ISTATISTIK = [
{ deger: "21.000+", etiket: "Taranan program" },
{ deger: "6 yıl", etiket: "Taban sıralaması trendi" },
{ deger: "%100", etiket: "Resmî YÖK Atlas kaynağı" },
];
export function Adim2() {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
const kilit = yay(frame, 150, 20);
const haritaY = dikey ? -70 : -40;
const kamera: Anahtar[] = [
{ f: 0, s: 1.42, x: 0, y: haritaY },
{ f: 58, s: 1.3, x: 0, y: haritaY },
{ f: 118, s: 1.22, x: 0, y: haritaY },
{ f: 138, s: 1, x: 0, y: 0 },
{ f: 200, s: 1.04, x: 0, y: 0 },
];
return (
<Sahne zemin={R.beyaz}>
<OdakDisiPikseller seed={21} adet={6} guc={0.7} />
<Kamera anahtarlar={kamera} vuruslar={VURUSLAR.adim2}>
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: dikey ? 64 : 30 }}>
<BaslikBlogu
rozet="Adım 2"
baslik="6 yılın verisi, tek taramada"
aciklama={dikey ? "Taban sıralaması, kontenjan, doluluk. Hepsi resmî YÖK Atlas'tan." : undefined}
genis
/>
<YagmurHaritasi genislik={dikey ? 1000 : 860} baslangic={4} dokulme={42} fircaBas={56} fircaSure={64} iz="a2-firca" />
<div style={{ display: "flex", gap: dikey ? 18 : 28 }}>
{ISTATISTIK.map((s, i) => {
const g = yay(frame, 124 + i * 8, 20);
return (
<Kutu
key={s.etiket}
style={{ width: dikey ? 300 : 380, padding: dikey ? "30px 18px" : "22px 24px", textAlign: "center", opacity: g, transform: `translateY(${(1 - g) * 40}px)` }}
>
<div data-iz={`a2-stat-${i}`} style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 74 : 64, lineHeight: 1, color: R.primary }}>
{s.deger}
</div>
<div style={{ marginTop: 14, fontSize: 26, fontWeight: 600, color: R.slate500, lineHeight: 1.3 }}>{s.etiket}</div>
</Kutu>
);
})}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 22, opacity: kilit, transform: `translateY(${(1 - kilit) * 20}px)`, maxWidth: dikey ? 920 : 1400 }}>
<div style={{ width: 84, height: 84, borderRadius: 24, background: R.primary, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
<Ikon ad="kalkan" boy={44} renk={R.beyaz} />
</div>
<div style={{ fontSize: 36, fontWeight: 600, color: R.slate800, lineHeight: 1.3 }}>
Yapay zekâ bu havuzun dışına çıkamaz. <span style={{ color: R.primary }}>Uydurmak yok.</span>
</div>
</div>
</AbsoluteFill>
</Kamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Adım 3 — 24 kare dolar, imleç her yeni kareyi takip eder */
/* ------------------------------------------------------------------ */
const DILIMLER = [
{ ad: "Hayal", adet: 5, renk: R.riskli },
{ ad: "Dengeli", adet: 13, renk: R.azRiskli },
{ ad: "Güvenli", adet: 6, renk: R.guvenli },
] as const;
const KARELER = DILIMLER.flatMap((d) => Array.from({ length: d.adet }, () => d));
// Gerçek YÖK Atlas 2026 taban sıralamaları (data/yokatlas.db, SAYISAL);
// örnek aday 45.320. — risk riskHesapla() eşikleriyle birebir.
const SATIRLAR = [
{ sira: 3, ad: "Elektrik-Elektronik Mühendisliği", uni: "Dokuz Eylül Üniversitesi", il: "İzmir", taban: 38626, risk: "riskli" as const },
{ sira: 11, ad: "Bilgisayar Mühendisliği", uni: "Ankara Yıldırım Beyazıt Üniversitesi", il: "Ankara", taban: 55158, risk: "az-riskli" as const },
{ sira: 20, ad: "Bilgisayar Mühendisliği", uni: "Kocaeli Üniversitesi", il: "Kocaeli", taban: 70832, risk: "guvenli" as const },
];
export function Adim3() {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
const kare = dikey ? 112 : 92;
const arali = dikey ? 16 : 12;
const sut = dikey ? 6 : 8;
const BAS = 24;
// İmlecin takip ettiği nokta: dolmakta olan kare
const t = Math.min(23, Math.max(0, (frame - BAS) / 2));
const kareMerkez = (i: number) => [(i % sut) * (kare + arali) + kare / 2, Math.floor(i / sut) * (kare + arali) + kare / 2];
const a = kareMerkez(Math.floor(t));
const b = kareMerkez(Math.min(23, Math.floor(t) + 1));
const f = t - Math.floor(t);
const takip = [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
const kamera: Anahtar[] = dikey
? [
{ f: 0, s: 1.16, x: 0, y: -190 },
{ f: 80, s: 1.1, x: 0, y: -160 },
{ f: 112, s: 1.15, x: 0, y: 500 },
{ f: 150, s: 1.1, x: 0, y: 520 },
{ f: 185, s: 1, x: 0, y: 0 },
]
: [
{ f: 0, s: 1.45, x: -435, y: 90 },
{ f: 80, s: 1.3, x: -420, y: 110 },
{ f: 112, s: 1.45, x: 455, y: 120 },
{ f: 150, s: 1.35, x: 455, y: 130 },
{ f: 185, s: 1, x: 0, y: 0 },
];
const izgara = (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 36 }}>
<div style={{ position: "relative", display: "grid", gridTemplateColumns: `repeat(${sut}, ${kare}px)`, gap: arali }}>
{KARELER.map((d, i) => {
const s = zipla(frame, BAS + i * 2);
return (
<div
key={i}
style={{
width: kare,
height: kare,
borderRadius: kare * 0.26,
background: d.renk,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: FONT.baslik,
fontWeight: 700,
fontSize: kare * 0.38,
color: R.beyaz,
transform: `scale(${Math.max(0, s)})`,
}}
>
{i + 1}
</div>
);
})}
<div data-iz="a3-takip" style={{ position: "absolute", left: takip[0], top: takip[1], width: 2, height: 2 }} />
</div>
<div style={{ display: "flex", gap: 22 }}>
{DILIMLER.map((d, i) => {
const g = yay(frame, 78 + i * 6, 16);
return (
<span
key={d.ad}
style={{
display: "inline-flex",
alignItems: "center",
gap: 14,
height: 76,
padding: "0 28px",
borderRadius: 999,
border: `3px solid ${R.slate200}`,
background: R.beyaz,
fontSize: 32,
fontWeight: 600,
color: R.slate800,
opacity: g,
transform: `translateY(${(1 - g) * 16}px)`,
}}
>
<span style={{ width: 20, height: 20, borderRadius: 6, background: d.renk }} />
{d.ad}
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, color: R.slate900 }}>{d.adet}</span>
</span>
);
})}
</div>
</div>
);
const satirlar = (
<div style={{ display: "flex", flexDirection: "column", gap: 18, width: dikey ? 940 : 780 }}>
{SATIRLAR.map((s, i) => {
const g = yay(frame, 96 + i * 9, 20);
const hover = (i === 1 && frame >= 100 && frame < 132) || (i === 2 && frame >= 130 && frame < 158);
return (
<Kutu
key={s.uni}
style={{
display: "flex",
alignItems: "center",
gap: 26,
padding: "26px 30px",
opacity: g,
transform: `translateX(${(1 - g) * 60}px) scale(${hover ? 1.02 : 1})`,
borderColor: hover ? R.turuncu : R.slate200,
background: hover ? "#fff7ed" : R.beyaz,
}}
>
<div style={{ width: 70, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 44, color: hover ? R.turuncu : R.slate400, textAlign: "center", flexShrink: 0 }}>
{s.sira}
</div>
<div data-iz={`a3-satir-${i}`} style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 34, fontWeight: 600, lineHeight: 1.2, color: R.slate900 }}>{s.ad}</div>
<div style={{ marginTop: 6, fontSize: 26, color: R.slate500, lineHeight: 1.3 }}>
{s.uni} · {s.il}
</div>
<div style={{ marginTop: 12, display: "flex", alignItems: "center", gap: 20 }}>
<RiskEtiketi risk={s.risk} olcek={2.3} />
<span style={{ fontSize: 26, color: R.slate500 }}>
Taban <b style={{ color: R.slate900, fontWeight: 600 }}>{trSayi(s.taban)}</b>
</span>
</div>
</div>
</Kutu>
);
})}
<div style={{ fontSize: 22, color: R.slate400, textAlign: dikey ? "center" : "left", opacity: yay(frame, 128, 16) }}>
Örnek: sayısal, 45.320. sıra · YÖK Atlas 2026 taban sıralamaları
</div>
</div>
);
return (
<Sahne>
<OdakDisiPikseller seed={31} adet={6} guc={0.7} />
<Kamera anahtarlar={kamera} vuruslar={VURUSLAR.adim3}>
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: dikey ? 60 : 56, padding: dikey ? "0 60px" : "0 100px" }}>
<BaslikBlogu rozet="Adım 3" baslik="Dengeli 24'lük listen hazır" genis />
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", alignItems: "center", gap: dikey ? 56 : 90 }}>
{izgara}
{satirlar}
</div>
</AbsoluteFill>
</Kamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Paket içinde — trend, PDF, revizyon (satır imleçle sürüklenir) */
/* ------------------------------------------------------------------ */
// Ankara Yıldırım Beyazıt Ü. Bilgisayar Müh. taban sırası, 2021–2026
const TREND = [37831, 27534, 21602, 30797, 42108, 55158];
const REVIZYON = [
{ ad: "Bilgisayar Müh. · Ege", renk: R.riskli },
{ ad: "Bilgisayar Müh. · Dokuz Eylül", renk: R.azRiskli },
{ ad: "Bilgisayar Müh. · Katip Çelebi", renk: R.guvenli },
];
function OzellikKarti({
ikon,
baslik,
aciklama,
gecikme,
children,
dikey,
}: {
ikon: IkonAdi;
baslik: string;
aciklama: string;
gecikme: number;
children: ReactNode;
dikey: boolean;
}) {
const frame = useCurrentFrame();
const g = yay(frame, gecikme, 22);
return (
<Kutu
style={{
position: "relative",
overflow: "visible",
width: dikey ? 940 : 520,
height: dikey ? 390 : 600,
padding: dikey ? "36px 40px" : "44px 40px",
display: "flex",
flexDirection: dikey ? "row" : "column",
gap: dikey ? 36 : 0,
opacity: g,
transform: `translateY(${(1 - g) * 80}px)`,
borderColor: "rgba(226,232,240,0.7)",
}}
>
<div style={{ position: "absolute", right: 30, top: 30 }}>
<PixelCorner boy={60} />
</div>
<div style={{ flex: dikey ? 1 : undefined }}>
<div style={{ width: 96, height: 96, borderRadius: 26, background: "rgba(59,130,246,0.1)", display: "flex", alignItems: "center", justifyContent: "center" }}>
<Ikon ad={ikon} boy={46} renk={R.primary} />
</div>
<div style={{ marginTop: 26, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 48, lineHeight: 1.1 }}>{baslik}</div>
<div style={{ marginTop: 12, fontSize: 30, lineHeight: 1.4, color: R.slate600 }}>{aciklama}</div>
</div>
<div style={{ flex: 1, display: "flex", alignItems: dikey ? "center" : "flex-end", justifyContent: "center", marginTop: dikey ? 0 : 24 }}>
{children}
</div>
</Kutu>
);
}
export function Ozellikler() {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
const gW = dikey ? 380 : 420;
const gH = dikey ? 200 : 170;
const min = 15000;
const max = 60000;
const noktalar = TREND.map((v, i) => [(i / (TREND.length - 1)) * gW, ((v - min) / (max - min)) * gH]);
const cizgi = ara(frame, [26, 58], [0, 1]);
const yol = noktalar.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join(" ");
// Sürükle-bırak: imleç 58'de ilk satırı tutar, 92'de en alta bırakır
const tut = ara(frame, [58, 92], [0, 1], Easing.inOut(Easing.cubic));
const tutuldu = frame >= 58 && frame < 94;
const SATIR = 74;
return (
<Sahne>
<OdakDisiPikseller seed={41} adet={7} guc={0.8} />
<Kamera anahtarlar={[{ f: 0, s: 1.18, x: 0, y: 60 }, { f: 50, s: 1.04, x: 0, y: 0 }, { f: 140, s: 1.0, x: 0, y: 0 }]} vuruslar={VURUSLAR.ozellikler}>
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: dikey ? 60 : 64 }}>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 36, textAlign: "center" }}>
<MaskeSatir gecikme={2}>
<Eyebrow olcek={dikey ? 2.6 : 2.3}>Paket içinde</Eyebrow>
</MaskeSatir>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 88 : 88, lineHeight: 1.08, maxWidth: dikey ? 940 : 1600 }}>
<MaskeSatir gecikme={6}>
Her tercihin bir <span style={vurguStil}>“neden”i</span> var.
</MaskeSatir>
</div>
</div>
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", gap: dikey ? 28 : 36 }}>
<OzellikKarti dikey={dikey} ikon="trend" baslik="6 yıllık trend" aciklama="Her tercihin taban seyri tek bakışta." gecikme={8}>
<svg data-iz="oz-trend" width={gW + 20} height={gH + 20} viewBox={`-10 -10 ${gW + 20} ${gH + 20}`}>
<path d={yol} fill="none" stroke={R.primary} strokeWidth={6} strokeLinecap="round" strokeLinejoin="round" pathLength={1} strokeDasharray={1} strokeDashoffset={1 - cizgi} />
{noktalar.map((p, i) => (
<rect key={i} x={p[0] - 9} y={p[1] - 9} width={18} height={18} rx={5} fill={i === TREND.length - 1 ? R.turuncu : R.primary} opacity={cizgi >= i / (TREND.length - 1) ? 1 : 0} />
))}
</svg>
</OzellikKarti>
<OzellikKarti dikey={dikey} ikon="belge" baslik="PDF rapor" aciklama="Aile toplantısına hazır." gecikme={14}>
<div style={{ position: "relative", width: 230, height: 230 }}>
{[2, 1, 0].map((k) => {
const s = yay(frame, 34 + (2 - k) * 6, 18);
return (
<div
key={k}
style={{
position: "absolute",
left: 40 + k * 18,
top: 10 + k * 14,
width: 150,
height: 196,
borderRadius: 20,
background: R.beyaz,
border: `3px solid ${R.slate200}`,
boxShadow: "0 10px 24px rgba(15,23,42,0.06)",
transform: `rotate(${(k - 1) * 6 * s}deg)`,
padding: 20,
display: "flex",
flexDirection: "column",
gap: 12,
}}
>
{k === 0 ? (
<>
<div style={{ display: "flex", gap: 6 }}>
{[R.riskli, R.azRiskli, R.guvenli].map((c) => (
<span key={c} style={{ width: 18, height: 18, borderRadius: 5, background: c }} />
))}
</div>
{[1, 0.8, 0.9, 0.6, 0.75].map((wd, j) => (
<span key={j} style={{ height: 10, width: `${wd * 100}%`, borderRadius: 5, background: R.slate200 }} />
))}
</>
) : null}
</div>
);
})}
</div>
</OzellikKarti>
<OzellikKarti dikey={dikey} ikon="yenile" baslik="2 revizyon hakkı" aciklama="Fikrin değişir, listen de." gecikme={20}>
<div style={{ position: "relative", width: dikey ? 420 : 400, height: 3 * SATIR }}>
{REVIZYON.map((r, i) => {
// 0. satır sürüklenir; diğerleri yukarı kayarak yer açar
// Sürüklenen satır i'nin üstünden geçince o satır bir basamak yukarı kayar
const gecis = i === 1 ? (2 * tut - 0.5) / 0.8 : (2 * tut - 1.3) / 0.7;
const kay = Easing.inOut(Easing.cubic)(Math.min(1, Math.max(0, gecis)));
const y = i === 0 ? tut * 2 * SATIR : (i - kay) * SATIR;
const kalkis = i === 0 && tutuldu ? 1 : 0;
return (
<div
key={r.ad}
style={{
position: "absolute",
left: kalkis * 18,
top: y,
width: "100%",
height: 62,
borderRadius: 18,
border: `3px solid ${kalkis ? R.turuncu : R.slate200}`,
background: kalkis ? "#fff7ed" : R.beyaz,
display: "flex",
alignItems: "center",
gap: 16,
padding: "0 20px",
fontSize: 24,
fontWeight: 600,
color: R.slate800,
boxShadow: kalkis ? "0 18px 36px rgba(15,23,42,0.16)" : undefined,
transform: `scale(${1 + kalkis * 0.04}) rotate(${kalkis * -1.5}deg)`,
zIndex: i === 0 ? 2 : 1,
}}
>
<span style={{ width: 14, height: 14, borderRadius: 4, background: r.renk, flexShrink: 0 }} />
<span style={{ flex: 1, whiteSpace: "nowrap", overflow: "hidden" }}>{r.ad}</span>
{i === 0 ? (
<span data-iz="oz-rev-tut" style={{ display: "grid", gridTemplateColumns: "repeat(2, 6px)", gap: 4 }}>
{Array.from({ length: 6 }, (_, k) => (
<span key={k} style={{ width: 6, height: 6, borderRadius: 2, background: R.slate400 }} />
))}
</span>
) : null}
</div>
);
})}
</div>
</OzellikKarti>
</div>
</AbsoluteFill>
</Kamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Dürüstlük notu */
/* ------------------------------------------------------------------ */
export function Durustluk() {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
const kalkan = zipla(frame, 0);
const alt = yay(frame, 64, 18);
return (
<Sahne zemin={R.beyaz}>
<OdakDisiPikseller seed={51} adet={6} guc={0.6} />
<Kamera anahtarlar={[{ f: 0, s: 1.02 }, { f: 140, s: 1.16 }]} vuruslar={VURUSLAR.durustluk}>
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center", gap: 46, padding: "0 70px" }}>
<div style={{ width: 150, height: 150, borderRadius: 40, background: R.primary, display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${Math.max(0, kalkan)})` }}>
<Ikon ad="kalkan" boy={80} renk={R.beyaz} />
</div>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 104 : 112, lineHeight: 1.06, maxWidth: dikey ? 920 : 1600 }}>
<MaskeSatir gecikme={8}>Yerleşme garantisi</MaskeSatir>
<MaskeSatir gecikme={16}>vermiyoruz.</MaskeSatir>
</div>
<div style={{ ...vurguStil, fontSize: dikey ? 92 : 96, lineHeight: 1.1 }}>
<MaskeSatir gecikme={36}>Çünkü kimse veremez.</MaskeSatir>
</div>
<div style={{ opacity: alt, transform: `translateY(${(1 - alt) * 24}px)`, display: "flex", flexDirection: "column", alignItems: "center", gap: 26, marginTop: 16 }}>
<div style={{ fontSize: dikey ? 40 : 40, color: R.slate600, fontWeight: 600, maxWidth: dikey ? 900 : 1400, 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} data-iz={`du-risk-${i}`} style={{ transform: `scale(${Math.max(0, zipla(frame, 76 + i * 5))})` }}>
<RiskEtiketi risk={r} olcek={3.2} />
</span>
))}
</div>
</div>
</AbsoluteFill>
</Kamera>
</Sahne>
);
}
/* ------------------------------------------------------------------ */
/* Kapanış — yalnız logo, tipografi, buton. Tıkla → her şey imlecin */
/* içine çöker → ilk kare: döngü */
/* ------------------------------------------------------------------ */
export function Kapanis() {
const frame = useCurrentFrame();
const { dikey } = useDuzen();
const buton = zipla(frame, 40);
const bas = ara(frame, [118, 121], [0, 1]) * (1 - ara(frame, [125, 130], [0, 1]));
// Çöküş: tıktan sonra içerik butonun olduğu noktaya büzülür
const cok = ara(frame, [124, 150], [0, 1], Easing.bezier(0.7, 0, 0.84, 0));
return (
<Sahne>
<Kamera anahtarlar={[{ f: 0, s: 1.12 }, { f: 100, s: 1.0 }]} vuruslar={VURUSLAR.kapanis}>
<AbsoluteFill
style={{
alignItems: "center",
justifyContent: "center",
textAlign: "center",
gap: dikey ? 70 : 56,
transformOrigin: "50% 66%",
transform: `scale(${1 - cok})`,
opacity: 1 - ara(frame, [140, 150], [0, 1]),
filter: `blur(${cok * 10}px)`,
}}
>
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", alignItems: "center", gap: dikey ? 18 : 28 }}>
<LogoMark boy={dikey ? 220 : 180} giris={4} rotaBas={22} />
<div style={{ opacity: yay(frame, 12, 16) }}>
<Wordmark boy={dikey ? 120 : 120} />
</div>
</div>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 100 : 108, lineHeight: 1.06, maxWidth: dikey ? 920 : 1600 }}>
<MaskeSatir gecikme={20}>Listeni kur,</MaskeSatir>
<MaskeSatir gecikme={28}>
<span style={vurguStil}>panikle</span> değil.
</MaskeSatir>
</div>
<div data-iz="ka-buton" style={{ transform: `scale(${Math.max(0, buton)})` }}>
<TuruncuCta metin="kolaytercih.com" boy={dikey ? 1.2 : 1.1} basili={bas} />
</div>
</AbsoluteFill>
</Kamera>
</Sahne>
);
}