Lansman videosu (Remotion) projesi eklendi

video/ ayrı bir Remotion projesi; Docker imajı, ESLint ve tsc kapsamı dışında.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-04 14:00:33 +03:00
parent 9c02b810e7
commit d2359d3f3a
55 changed files with 7492 additions and 1 deletions

View File

@@ -27,3 +27,5 @@ data/
!data/yokatlas.db
!data/yokatlas.db-wal
!data/yokatlas.db-shm
# Lansman videosu (Remotion) imaja girmez
video/

View File

@@ -12,6 +12,8 @@ const eslintConfig = defineConfig([
"out/**",
"build/**",
"next-env.d.ts",
// Lansman videosu ayrı bir Remotion projesi (kendi package.json'ı var)
"video/**",
]),
]);

View File

@@ -30,5 +30,5 @@
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
"exclude": ["node_modules", "video"]
}

2
video/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
out

30
video/REMOCN-NOTU.md Normal file
View File

@@ -0,0 +1,30 @@
# remocn kurulum notu
remocn bileşenleri shadcn registry'sinden kurulur:
```sh
pnpm dlx shadcn@latest add @remocn/<bileşen>
```
**Dikkat:** shadcn CLI kurulumda string literal'leri Tailwind sınıf listesi gibi
işleyip tekrar eden "sınıfları" siliyor. Bu, boşlukla ayrılmış ve içinde
yinelenen değer olan her string'i bozar: SVG path'leri, `viewBox="0 0 24 24"`,
`transformOrigin: "0 0"`, `oklch(1 0 0 / 10%)`. İlk kurulumda `cursor.tsx`
(el ve ok imleci path'leri, viewBox) ve `lib/remocn-ui/theme.ts` (iki oklch
değeri) böyle bozuldu ve registry JSON'undan (`https://remocn.dev/r/<ad>.json`)
geri yüklendi.
Yeni bir bileşen kurduktan sonra, içindeki boşluklu string'leri registry
JSON'uyla karşılaştır.
## Bu projede yerel olarak değiştirilen bileşenler
shadcn mantığı: kod bizim. Marka yazı tiplerini ve sahne ihtiyaçlarını
karşılamak için şu dosyalar değiştirildi (yeniden `add --overwrite` yapılırsa
değişiklikler gider):
- `tracking-in.tsx`: `fontFamily`, `background` (şeffaf), `endTracking`, ReactNode metin
- `kinetic-center-build.tsx`: gerçek yazı tipiyle ölçüm, `birim` (harf/kelime), `vurgu`, `gap`
- `focus-blur-resolve.tsx`: `fontFamily`
- `slot-machine-roll.tsx`: `inline`, `fontFamily`, `stagger`, karaktere göre sütun genişliği
- `push-through.tsx`: `origin` (itmenin tıklanan noktadan yapılması)

17
video/components.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": { "config": "", "css": "", "baseColor": "slate", "cssVariables": false },
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@remocn": "https://remocn.dev/r/{name}.json"
}
}

32
video/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "kolaytercih-video",
"version": "0.1.0",
"private": true,
"description": "KolayTercih lansman videosu (Remotion + remocn)",
"scripts": {
"studio": "remotion studio src/index.ts",
"render": "remotion render src/index.ts Lansman out/kolaytercih-lansman.mp4",
"render:dikey": "remotion render src/index.ts LansmanDikey out/kolaytercih-lansman-dikey.mp4",
"kare": "remotion still src/index.ts Lansman",
"tip": "tsc --noEmit"
},
"dependencies": {
"@remotion/cli": "4.0.531",
"@remotion/fonts": "4.0.531",
"@remotion/google-fonts": "4.0.531",
"@remotion/transitions": "4.0.531",
"culori": "^4.0.2",
"lucide-react": "^1.49.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"remotion": "4.0.531",
"zod": "4.5.4"
},
"devDependencies": {
"@types/culori": "^4.0.1",
"@types/node": "22",
"@types/react": "19",
"@types/react-dom": "19",
"typescript": "^5.9.3"
}
}

2811
video/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

19
video/remotion.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import path from "node:path";
import { Config } from "@remotion/cli/config";
Config.setVideoImageFormat("jpeg");
Config.setJpegQuality(92);
Config.setOverwriteOutput(true);
Config.setConcurrency(4);
// remocn bileşenleri "@/..." yollarıyla import eder (shadcn alias'ı)
Config.overrideWebpackConfig((config) => ({
...config,
resolve: {
...config.resolve,
alias: {
...(config.resolve?.alias ?? {}),
"@": path.join(process.cwd(), "src"),
},
},
}));

100
video/src/Lansman.tsx Normal file
View File

@@ -0,0 +1,100 @@
import { linearTiming, TransitionSeries } from "@remotion/transitions";
import { AbsoluteFill, useCurrentFrame } from "remotion";
import { pushThrough } from "@/components/remocn/push-through";
import { Gren } from "./bilesenler/Gren";
import "./fontlar";
import { useFontlarHazir } from "./fontlar-hazir";
import { pikselDalga } from "./gecisler/piksel-dalga";
import { R } from "./marka";
import { S1Gurultu } from "./sahneler/S1Gurultu";
import { haritaTiklama, S2Harita } from "./sahneler/S2Harita";
import { S3Siralama, siralamaTiklama } from "./sahneler/S3Siralama";
import { havuzTiklama, S4Havuz } from "./sahneler/S4Havuz";
import { danismanDalgaNoktasi, S5Danisman } from "./sahneler/S5Danisman";
import { degerDalgaNoktasi, S6Deger } from "./sahneler/S6Deger";
import { S7Kapanis } from "./sahneler/S7Kapanis";
import { GECIS, SAHNE } from "./zaman";
// Sahnelerin küresel başlangıç kareleri (geçişler üst üste biner)
const SIRA = [
[SAHNE.gurultu, GECIS.g1],
[SAHNE.harita, GECIS.g2],
[SAHNE.siralama, GECIS.g3],
[SAHNE.havuz, GECIS.g4],
[SAHNE.danisman, GECIS.g5],
[SAHNE.deger, GECIS.g6],
] as const;
const BASLAR = SIRA.reduce<number[]>((acc, [s, g]) => [...acc, acc[acc.length - 1] + s - g], [0]);
const KAPANIS_BASI = BASLAR[6];
export const Lansman: React.FC = () => {
useFontlarHazir();
const f = useCurrentFrame();
// Koyu sahnelerde gren daha belirgin ve açık zerreli
const koyu = f < SAHNE.gurultu - 14 || f > KAPANIS_BASI + 14;
return (
<AbsoluteFill style={{ background: R.slate50 }}>
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={SAHNE.gurultu}>
<S1Gurultu />
</TransitionSeries.Sequence>
{/* Tek piksel patlar: koyudan açığa, merkezden */}
<TransitionSeries.Transition
presentation={pikselDalga({ bant: 420, hucre: 64, renkler: [R.turuncuLogo, R.seftali, R.mavi] })}
timing={linearTiming({ durationInFrames: GECIS.g1 })}
/>
<TransitionSeries.Sequence durationInFrames={SAHNE.harita}>
<S2Harita />
</TransitionSeries.Sequence>
{/* İmleç logonun turuncu karesine tıklar → dalga oradan */}
<TransitionSeries.Transition
presentation={pikselDalga({ origin: haritaTiklama(), bant: 380, hucre: 60, tohum: 5 })}
timing={linearTiming({ durationInFrames: GECIS.g2 })}
/>
<TransitionSeries.Sequence durationInFrames={SAHNE.siralama}>
<S3Siralama />
</TransitionSeries.Sequence>
{/* CTA'ya basılır → sahne butonun içinden geçilerek terk edilir */}
<TransitionSeries.Transition
presentation={pushThrough({ origin: siralamaTiklama(), zoom: 3.2, blur: 16 })}
timing={linearTiming({ durationInFrames: GECIS.g3 })}
/>
<TransitionSeries.Sequence durationInFrames={SAHNE.havuz}>
<S4Havuz />
</TransitionSeries.Sequence>
{/* Listedeki satıra tıklanır → satırın içinden danışmana */}
<TransitionSeries.Transition
presentation={pushThrough({ origin: havuzTiklama(), zoom: 2.8, blur: 14 })}
timing={linearTiming({ durationInFrames: GECIS.g4 })}
/>
<TransitionSeries.Sequence durationInFrames={SAHNE.danisman}>
<S5Danisman />
</TransitionSeries.Sequence>
{/* Trendin son (2026) pikselinden dalga */}
<TransitionSeries.Transition
presentation={pikselDalga({ origin: danismanDalgaNoktasi(), bant: 400, hucre: 60, tohum: 8 })}
timing={linearTiming({ durationInFrames: GECIS.g5 })}
/>
<TransitionSeries.Sequence durationInFrames={SAHNE.deger}>
<S6Deger />
</TransitionSeries.Sequence>
{/* Son gösterge karesinden koyu dalga: kapanışın CTA bandına */}
<TransitionSeries.Transition
presentation={pikselDalga({
origin: degerDalgaNoktasi(),
bant: 440,
hucre: 64,
tohum: 11,
renkler: [R.green600, R.slate700, R.turuncu],
})}
timing={linearTiming({ durationInFrames: GECIS.g6 })}
/>
<TransitionSeries.Sequence durationInFrames={SAHNE.kapanis}>
<S7Kapanis />
</TransitionSeries.Sequence>
</TransitionSeries>
<Gren guc={koyu ? 0.24 : 0.17} koyu={koyu} />
</AbsoluteFill>
);
};

15
video/src/Root.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { Composition } from "remotion";
import { Lansman } from "./Lansman";
import { FPS, H, W } from "./marka";
import { TOPLAM } from "./zaman";
export const RemotionRoot: React.FC = () => (
<Composition
id="Lansman"
component={Lansman}
durationInFrames={TOPLAM}
fps={FPS}
width={W}
height={H}
/>
);

View File

@@ -0,0 +1,60 @@
import { AbsoluteFill, useCurrentFrame } from "remotion";
import { H, W } from "../marka";
/**
* Film greni: her karede tohumu değişen fraktal gürültü (deterministik —
* aynı kare her render'da aynı greni verir). Tüm videonun üstünde durur;
* `guc` koyu sahnelerde artırılır.
*/
export const Gren: React.FC<{ guc: number; koyu?: boolean }> = ({
guc,
koyu = false,
}) => {
const frame = useCurrentFrame();
if (guc <= 0) return null;
const id = `gren-${frame}`;
return (
<AbsoluteFill
style={{
pointerEvents: "none",
mixBlendMode: koyu ? "screen" : "multiply",
opacity: guc,
}}
>
<svg width={W} height={H} viewBox={`0 0 ${W / 1.5} ${H / 1.5}`}>
<filter id={id} x="0" y="0" width="100%" height="100%">
<feTurbulence
type="fractalNoise"
baseFrequency="0.78"
numOctaves={2}
seed={frame % 997}
stitchTiles="stitch"
/>
{/* Tek kanallı gren: renkli gürültü değil */}
<feColorMatrix
type="matrix"
values="0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0 0 0 0 1"
/>
{/* Açık sahnede yalnız koyu zerreler (multiply), koyuda yalnız açık
zerreler (screen) kalsın diye ortalama kaydırılıp kontrast verilir */}
<feComponentTransfer>
<feFuncR type="linear" slope={2.6} intercept={koyu ? -1.25 : -0.35} />
<feFuncG type="linear" slope={2.6} intercept={koyu ? -1.25 : -0.35} />
<feFuncB type="linear" slope={2.6} intercept={koyu ? -1.25 : -0.35} />
</feComponentTransfer>
</filter>
<rect width="100%" height="100%" filter={`url(#${id})`} />
</svg>
</AbsoluteFill>
);
};
/** Koyu sahnelerde kenar kararması */
export const Vinyet: React.FC<{ guc?: number }> = ({ guc = 0.55 }) => (
<AbsoluteFill
style={{
pointerEvents: "none",
background: `radial-gradient(ellipse 75% 70% at 50% 50%, rgba(0,0,0,0) 45%, rgba(2,6,23,${guc}) 100%)`,
}}
/>
);

View File

@@ -0,0 +1,84 @@
import { useCurrentFrame } from "remotion";
import { Cursor } from "@/components/remocn/cursor";
import {
cursorPathAt,
type CursorWaypoint,
} from "@/components/remocn/use-cursor-path";
import { R } from "../marka";
import { aralik } from "../yardimci";
export type Yol = CursorWaypoint[];
/**
* remocn Cursor + useCursorPath, sitenin diline göre: koyu ok, beyaz kontur,
* tıklamada turuncu halka. Tıklanabilir bir şeyin üstündeyken el imlecine
* döner (sitede butonlar `cursor-pointer`).
*/
export const Imlec: React.FC<{
yol: Yol;
/** [görünmeye başladığı kare, kaybolduğu kare] */
gorunur?: [number, number];
/** El imleci gösterilecek kare aralıkları */
el?: [number, number][];
boy?: number;
}> = ({ yol, gorunur = [0, 1e9], el = [], boy = 46 }) => {
const frame = useCurrentFrame();
const st = cursorPathAt(yol, frame);
const op =
aralik(frame, gorunur[0], gorunur[0] + 8) *
(1 - aralik(frame, gorunur[1] - 8, gorunur[1]));
const elMi = el.some(([a, b]) => frame >= a && frame <= b);
if (op <= 0) return null;
// El imlecinin sıcak noktası parmak ucunda (viewBox 24'te x≈9.5): tıklama
// noktası ok ucuyla aynı kalsın diye kaydırılır.
const kaydir = elMi ? (9.5 / 24) * boy : (1 / 24) * boy;
return (
<div style={{ position: "absolute", inset: 0, opacity: op, pointerEvents: "none" }}>
<Cursor
style={{ ...st, x: st.x - kaydir, y: st.y - (1 / 24) * boy }}
variant={elMi ? "pointer" : "arrow"}
size={boy}
theme={{ foreground: R.slate900, background: R.beyaz, primary: R.turuncu }}
rippleColor={R.turuncu}
/>
</div>
);
};
export type IsiOrnek = { x: number; y: number; w: number };
/**
* Sitenin imleç ısı fırçası (use-pixel-heat.ts) video karşılığı: imleç
* yaklaşınca pikseller anında ısınır, uzaklaşınca yavaş soğur. İmleç yolu saf
* bir fonksiyon olduğu için geçmiş kareler yeniden hesaplanır — iz
* deterministiktir.
*/
export function isiIzi(
yol: Yol,
frame: number,
gorunur: [number, number] = [0, 1e9],
iz = 26,
): IsiOrnek[] {
const out: IsiOrnek[] = [];
for (let k = 0; k <= iz; k += 2) {
const f = frame - k;
if (f < gorunur[0] || f > gorunur[1]) continue;
const p = cursorPathAt(yol, f);
out.push({ x: p.x, y: p.y, w: k === 0 ? 1 : (1 - k / (iz + 2)) ** 1.7 });
}
return out;
}
export function isi(x: number, y: number, izler: IsiOrnek[], r: number) {
let h = 0;
const r2 = r * r;
for (const o of izler) {
const dx = x - o.x;
const dy = y - o.y;
const v = Math.exp(-(dx * dx + dy * dy) / r2) * o.w;
if (v > h) h = v;
}
return h;
}
export { cursorPathAt };

View File

@@ -0,0 +1,48 @@
import { useCurrentFrame } from "remotion";
import { aralik, ease } from "../yardimci";
/**
* Metin çıkışı (remocn blur-out-up'ın sarmalayıcı hâli): içerik
* [bas, bas+sure] arasında yukarı kayar, bulanıklaşır ve söner.
* `giris` verilirse aynı yoldan tersine belirir.
*/
export const Cikis: React.FC<{
bas: number;
sure?: number;
giris?: [number, number];
yol?: number;
children: React.ReactNode;
style?: React.CSSProperties;
}> = ({ bas, sure = 10, giris, yol = 34, children, style }) => {
const f = useCurrentFrame();
const c = aralik(f, bas, bas + sure, ease.in);
const g = giris ? aralik(f, giris[0], giris[1], ease.out) : 1;
return (
<div
style={{
position: "absolute",
inset: 0,
opacity: g * (1 - c),
translate: `0 ${(1 - g) * yol * 0.6 - c * yol}px`,
filter: `blur(${(1 - g) * 10 + c * 12}px)`,
...style,
}}
>
{children}
</div>
);
};
/** Mutlak konumlu kutu: kinetik bileşenler (inset:0 ortalar) bu kutuya göre ortalanır */
export const Kutu: React.FC<{
x?: number;
y: number;
w?: number;
h: number;
children: React.ReactNode;
style?: React.CSSProperties;
}> = ({ x = 0, y, w = 1920, h, children, style }) => (
<div style={{ position: "absolute", left: x, top: y, width: w, height: h, ...style }}>
{children}
</div>
);

View File

@@ -0,0 +1,94 @@
import { LOGO_KARELER, R } from "../marka";
import { FONT } from "../fontlar";
/**
* Logo işareti: üç kare (public/kolay-tercih-mark.svg). `kareStil` ile her
* kare ayrı ayrı oynatılabilir (animasyonlu-mark.tsx'teki "rota"/"nabız").
*/
export const LogoIsaret: React.FC<{
boy: number;
kareStil?: (i: number) => { dx?: number; dy?: number; olcek?: number; opaklik?: number };
style?: React.CSSProperties;
}> = ({ boy, kareStil, style }) => (
<svg width={boy} height={boy} viewBox="0 0 180 180" style={{ overflow: "visible", ...style }}>
{LOGO_KARELER.map((k, i) => {
const s = kareStil?.(i) ?? {};
const cx = k.x + k.boy / 2 + (s.dx ?? 0);
const cy = k.y + k.boy / 2 + (s.dy ?? 0);
const o = s.olcek ?? 1;
return (
<rect
key={i}
x={cx - (k.boy * o) / 2}
y={cy - (k.boy * o) / 2}
width={k.boy * o}
height={k.boy * o}
rx={13 * o}
fill={k.renk}
opacity={s.opaklik ?? 1}
/>
);
})}
</svg>
);
/** "Kolay" Bricolage 300 + "Tercih" 600, tracking -0.05em (logo svg'si) */
export const Kelime: React.FC<{ boy: number; renk?: string }> = ({
boy,
renk = "#0a0a0a",
}) => (
<span
style={{
fontFamily: FONT.vurgu,
fontSize: boy,
letterSpacing: "-0.05em",
color: renk,
whiteSpace: "nowrap",
lineHeight: 1,
}}
>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</span>
);
/** Rozet ikonu PixelMark (pixel-decor.tsx): 2×2 piksel, sağ üst turuncu */
export const PikselIsaret: React.FC<{ boy: number }> = ({ boy }) => (
<svg width={boy} height={boy} viewBox="0 0 20 20" style={{ flexShrink: 0 }}>
<rect x={1.3} y={1.3} width={7.4} height={7.4} rx={2} fill={R.primary} fillOpacity={0.35} />
<rect x={11.3} y={1.3} width={7.4} height={7.4} rx={2} fill={R.turuncu} fillOpacity={0.85} />
<rect x={11.3} y={11.3} width={7.4} height={7.4} rx={2} fill={R.primary} fillOpacity={0.18} />
<rect x={1.3} y={11.3} width={7.4} height={7.4} rx={2} fill={R.primary} fillOpacity={0.6} />
</svg>
);
/**
* SectionEyebrow'un video karşılığı (2×): piksel imzalı rozet.
* `inline-flex items-center gap-2 rounded-full border border-slate-200
* bg-white px-3.5 py-1.5 text-xs font-semibold text-primary`
*/
export const Rozet: React.FC<{ children: React.ReactNode; style?: React.CSSProperties }> = ({
children,
style,
}) => (
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 16,
borderRadius: 999,
border: `2px solid ${R.slate200}`,
background: R.beyaz,
padding: "12px 28px",
fontFamily: FONT.govde,
fontSize: 26,
fontWeight: 600,
color: R.primary,
whiteSpace: "nowrap",
...style,
}}
>
<PikselIsaret boy={28} />
{children}
</span>
);

View File

@@ -0,0 +1,55 @@
import { useCurrentFrame } from "remotion";
import { H, PIKSEL, R, W, opaklikFor } from "../marka";
import { clamp01, hash, karis } from "../yardimci";
import { isi, type IsiOrnek } from "./Imlec";
/**
* Sahne kenarlarında hero dokusu (PikselZemin'in video hâli): ortası boş,
* kenara yaklaştıkça sıklaşan noktalar. İmleç izi verilirse pikseller
* turuncuya ısınır. Yavaş kayma (parallax) derinlik verir.
*/
export const PikselZemin: React.FC<{
hucre?: number;
tohum?: number;
renk?: string;
/** Kenar bandı: kısa kenarın oranı */
kenar?: number;
izler?: IsiOrnek[];
kayma?: number;
guc?: number;
}> = ({ hucre = 40, tohum = 7, renk = R.primary, kenar = 0.26, izler = [], kayma = 0.25, guc = 1 }) => {
const f = useCurrentFrame();
const bant = Math.min(W, H) * kenar;
const dy = -f * kayma;
const sutun = Math.ceil(W / hucre);
const satir = Math.ceil(H / hucre) + 2;
const rects: React.ReactNode[] = [];
for (let j = 0; j < satir; j++) {
for (let i = 0; i < sutun; i++) {
const cx = i * hucre + hucre / 2;
const cy = j * hucre + hucre / 2 + (dy % hucre);
const kenarUz = Math.min(cx, W - cx, cy, H - cy);
const yogunluk = clamp01(1 - kenarUz / bant) ** 1.6;
if (hash(i, j, tohum) > yogunluk * 0.9) continue;
const h = izler.length ? isi(cx, cy, izler, 110) : 0;
const boy = hucre * PIKSEL.nokta;
rects.push(
<rect
key={`${i}-${j}`}
x={cx - boy / 2}
y={cy - boy / 2}
width={boy}
height={boy}
rx={boy * PIKSEL.rx}
fill={karis(renk, R.turuncu, h)}
fillOpacity={(opaklikFor(i, j) * 0.9 + h * 0.6) * guc}
/>,
);
}
}
return (
<svg width={W} height={H} style={{ position: "absolute", inset: 0 }}>
{rects}
</svg>
);
};

200
video/src/bilesenler/UI.tsx Normal file
View File

@@ -0,0 +1,200 @@
import { ArrowRight } from "lucide-react";
import { Caret } from "@/components/remocn/caret";
import { FONT } from "../fontlar";
import { R } from "../marka";
import { DILIM_ETIKET, type Dilim } from "../veri";
import { karis } from "../yardimci";
// Arayüz parçaları sitenin 2 katı ölçüde (DESIGN.md §4). Durumlar (hover,
// basma, seçim) 0–1 arası sayı alır ki kareler arasında yumuşak geçsin.
/** Turuncu funnel CTA (§4.1): metin önce, ArrowRight sonda, yıldız yok */
export const CtaButon: React.FC<{
children: React.ReactNode;
hover?: number;
bas?: number;
yukseklik?: number;
style?: React.CSSProperties;
}> = ({ children, hover = 0, bas = 0, yukseklik = 104, style }) => (
<div
style={{
height: yukseklik,
padding: `0 ${yukseklik * 0.52}px`,
borderRadius: 999,
background: karis(R.turuncu, R.turuncu600, hover),
color: R.beyaz,
display: "inline-flex",
alignItems: "center",
gap: 16,
fontFamily: FONT.govde,
fontSize: yukseklik * 0.31,
fontWeight: 600,
whiteSpace: "nowrap",
scale: `${1 - 0.03 * bas}`,
boxShadow: "0 2px 6px rgba(15,23,42,0.08)",
...style,
}}
>
{children}
<ArrowRight size={yukseklik * 0.31} strokeWidth={2.4} aria-hidden />
</div>
);
/** Form alanı hap (§4.6): 48 px → 104 px, tam yuvarlak, slate-200 kenarlık */
export const GirisHap: React.FC<{
genislik: number;
deger: string;
placeholder: string;
odak?: number;
yaziyor?: boolean;
ikon?: React.ReactNode;
yukseklik?: number;
}> = ({ genislik, deger, placeholder, odak = 0, yaziyor = false, ikon, yukseklik = 104 }) => (
<div
style={{
width: genislik,
height: yukseklik,
borderRadius: 999,
border: `2px solid ${karis(R.slate200, R.primary, odak)}`,
boxShadow: `0 0 0 ${6 * odak}px color-mix(in oklab, ${R.primary} 22%, transparent)`,
background: R.beyaz,
display: "flex",
alignItems: "center",
gap: 20,
padding: `0 ${yukseklik * 0.4}px`,
fontFamily: FONT.govde,
fontSize: yukseklik * 0.31,
color: deger ? R.slate900 : R.slate400,
fontWeight: deger ? 600 : 400,
boxSizing: "border-box",
whiteSpace: "nowrap",
overflow: "hidden",
}}
>
{ikon}
<span style={{ display: "inline-flex", alignItems: "center" }}>
{/* Boş alanda imleç placeholder'ın başında, doluyken metnin sonunda */}
{odak > 0.5 && !deger && (
<Caret color={R.primary} width={3} height={yukseklik * 0.36} blink blinkPerSecond={1.6} />
)}
{deger || placeholder}
{odak > 0.5 && deger && (
<Caret
color={R.primary}
width={3}
height={yukseklik * 0.36}
blink={!yaziyor}
blinkPerSecond={1.6}
marginLeft={4}
/>
)}
</span>
</div>
);
/** Etkileşimli çip (§4.17): pasif beyaz, seçili mavi dolu */
export const Cip: React.FC<{
children: React.ReactNode;
secili?: number;
hover?: number;
bas?: number;
ikon?: React.ReactNode;
boy?: number;
style?: React.CSSProperties;
}> = ({ children, secili = 0, hover = 0, bas = 0, ikon, boy = 80, style }) => {
const pasifKenar = karis(R.slate200, R.turuncu, hover);
const pasifYazi = karis(R.slate700, R.turuncu, hover);
return (
<span
style={{
height: boy,
display: "inline-flex",
alignItems: "center",
gap: boy * 0.15,
padding: `0 ${boy * 0.35}px`,
borderRadius: 999,
border: `2px solid ${karis(pasifKenar, R.primary, secili)}`,
background: karis(karis(R.beyaz, R.turuncu50, hover), R.primary, secili),
color: karis(pasifYazi, R.beyaz, secili),
fontFamily: FONT.govde,
fontSize: boy * 0.35,
fontWeight: 600,
whiteSpace: "nowrap",
scale: `${1 - 0.03 * bas}`,
boxSizing: "border-box",
...style,
}}
>
{ikon}
{children}
</span>
);
};
/** Dilim etiketi (§4.5 düzeni): nokta + metin; renk tek başına anlam taşımaz */
export const DILIM_RENK: Record<Dilim, { nokta: string; yazi: string }> = {
hayal: { nokta: R.red500, yazi: R.red600 },
dengeli: { nokta: R.amber500, yazi: R.amber600 },
garanti: { nokta: R.emerald500, yazi: R.emerald600 },
};
export const DilimEtiketi: React.FC<{ dilim: Dilim; boy?: number }> = ({ dilim, boy = 26 }) => (
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: boy * 0.4,
fontFamily: FONT.govde,
fontSize: boy,
fontWeight: 600,
color: DILIM_RENK[dilim].yazi,
whiteSpace: "nowrap",
}}
>
<span
style={{
width: boy * 0.62,
height: boy * 0.62,
borderRadius: 999,
background: DILIM_RENK[dilim].nokta,
}}
/>
{DILIM_ETIKET[dilim]}
</span>
);
/**
* Piksel trend: 6 yılın taban sırası, her yıl bir piksel. Üstte = daha iyi
* (küçük) sıra. Son yıl turuncu yanar (PixelSira'nın dili).
*/
export const PikselTrend: React.FC<{
sira: number[];
hucre?: number;
yukseklik?: number;
ilerleme?: number;
}> = ({ sira, hucre = 18, yukseklik = 56, ilerleme = 1 }) => {
const min = Math.min(...sira);
const max = Math.max(...sira);
const boy = hucre * 0.74;
return (
<svg width={sira.length * hucre} height={yukseklik + boy} style={{ overflow: "visible" }}>
{sira.map((s, i) => {
const t = max === min ? 0.5 : (s - min) / (max - min);
const gorunur = ilerleme * sira.length > i;
const son = i === sira.length - 1;
return (
<rect
key={i}
x={i * hucre + (hucre - boy) / 2}
y={t * yukseklik}
width={boy}
height={boy}
rx={boy * 0.27}
fill={son ? R.turuncu : R.primary}
fillOpacity={gorunur ? (son ? 0.9 : 0.35 + 0.1 * i) : 0}
/>
);
})}
</svg>
);
};

View File

@@ -0,0 +1,69 @@
"use client";
import type { CSSProperties } from "react";
import { useCurrentFrame, useVideoConfig } from "remotion";
export interface CaretProps {
color?: string;
width?: number;
height?: number;
radius?: number;
opacity?: number;
blink?: boolean;
blinkPerSecond?: number;
speed?: number;
marginLeft?: number;
className?: string;
style?: CSSProperties;
}
export function caretBlinkOpacity(
frame: number,
opts: { fps: number; blinkPerSecond: number; speed: number },
): number {
const cycles = opts.blinkPerSecond <= 0 ? 1 : opts.blinkPerSecond;
const halfPeriod = opts.fps / cycles / 2;
if (halfPeriod <= 0) return 1;
return Math.floor((frame * opts.speed) / halfPeriod) % 2 === 0 ? 1 : 0;
}
export function Caret({
color = "currentColor",
width = 2,
height = 18,
radius = 1,
opacity,
blink = false,
blinkPerSecond = 1,
speed = 1,
marginLeft = 0,
className,
style,
}: CaretProps) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const resolvedOpacity =
opacity !== undefined
? opacity
: blink
? caretBlinkOpacity(frame, { fps, blinkPerSecond, speed })
: 1;
return (
<span
className={className}
style={{
display: "inline-block",
flexShrink: 0,
width,
height,
borderRadius: radius,
background: color,
opacity: resolvedOpacity,
marginLeft,
...style,
}}
/>
);
}

View File

@@ -0,0 +1,115 @@
"use client";
import { type RemocnTheme, useRemocnTheme } from "@/lib/remocn-ui";
export type CursorVariant = "arrow" | "pointer";
export interface CursorStyle {
x: number;
y: number;
scale: number;
rippleOpacity: number;
rippleScale: number;
pressScale?: number;
}
export interface CursorProps {
style?: CursorStyle;
variant?: CursorVariant;
size?: number;
theme?: Partial<RemocnTheme>;
rippleColor?: string;
className?: string;
}
const REST: CursorStyle = {
x: 0,
y: 0,
scale: 1,
rippleOpacity: 0,
rippleScale: 0,
};
const ARROW_PATH =
"M1 1 L1 18.5 L5.6 14.4 L8.7 21.6 L11.6 20.4 L8.5 13.2 L14.5 13.2 Z";
const POINTER_PATH =
"M8 2.2 C8 1.3 8.7 0.7 9.5 0.7 C10.3 0.7 11 1.3 11 2.2 L11 9 " +
"C11 9 11.3 8.4 12.2 8.4 C13 8.4 13.4 9 13.4 9.6 " +
"C13.4 9.6 13.9 9.1 14.7 9.1 C15.5 9.1 15.9 9.7 15.9 10.3 " +
"C15.9 10.3 16.4 9.9 17.1 9.9 C17.9 9.9 18.3 10.5 18.3 11.2 " +
"L18.3 16.8 C18.3 19.8 16.3 22.3 12.8 22.3 L11.4 22.3 " +
"C9 22.3 7.9 21.2 6.6 19.2 L4 15.2 C3.5 14.4 3.7 13.4 4.5 12.9 " +
"C5.1 12.5 5.9 12.6 6.4 13.2 L8 15 Z";
export function Cursor({
style,
variant = "arrow",
size = 28,
theme: themeOverride,
rippleColor,
className,
}: CursorProps) {
const theme = useRemocnTheme(themeOverride, "light");
const v = style ?? REST;
const press = v.pressScale ?? 1;
const pressedScale = v.scale * (0.9 + 0.1 * press);
const ring = rippleColor ?? theme.primary;
const fill = theme.foreground;
const stroke = theme.background;
const rippleSize = size * 1.8;
return (
<div
className={className}
style={{
position: "absolute",
left: 0,
top: 0,
transform: `translate(${v.x}px, ${v.y}px)`,
pointerEvents: "none",
}}
>
{}
<div
style={{
position: "absolute",
left: -rippleSize / 2,
top: -rippleSize / 2,
width: rippleSize,
height: rippleSize,
borderRadius: "50%",
border: `2px solid ${ring}`,
opacity: v.rippleOpacity,
transform: `scale(${v.rippleScale})`,
transformOrigin: "center",
}}
/>
{}
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
style={{
position: "absolute",
left: 0,
top: 0,
transform: `scale(${pressedScale})`,
transformOrigin: "0 0",
filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.3))",
overflow: "visible",
}}
>
<path
d={variant === "pointer" ? POINTER_PATH : ARROW_PATH}
fill={fill}
stroke={stroke}
strokeWidth={1.4}
strokeLinejoin="round"
/>
</svg>
</div>
);
}

View File

@@ -0,0 +1,115 @@
"use client";
import { Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
export interface FocusBlurResolveProps {
text: string;
fontFamily?: string;
blur?: number;
fontSize?: number;
color?: string;
fontWeight?: number;
speed?: number;
className?: string;
}
export function FocusBlurResolve({
text,
blur = 14,
fontSize = 72,
color = "#171717",
fontWeight = 600,
speed = 1,
className,
fontFamily = "sans-serif",
}: FocusBlurResolveProps) {
const frame = useCurrentFrame() * speed;
const { durationInFrames } = useVideoConfig();
const enterDur = 23;
const exitDur = 16;
const exitStart = Math.max(enterDur, durationInFrames - exitDur);
const enterEasing = Easing.bezier(0.22, 1, 0.36, 1);
const exitEasing = Easing.bezier(0.64, 0, 0.78, 0);
const enterP = interpolate(frame, [0, enterDur], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: enterEasing,
});
const exitP = interpolate(frame, [exitStart, durationInFrames], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: exitEasing,
});
const opacity = enterP * (1 - exitP);
const yEnter = interpolate(frame, [0, enterDur], [14, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: enterEasing,
});
const yExit = interpolate(frame, [exitStart, durationInFrames], [0, -10], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: exitEasing,
});
const y = yEnter + yExit;
const blurEnter = interpolate(frame, [0, enterDur], [blur, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: enterEasing,
});
const blurExit = interpolate(frame, [exitStart, durationInFrames], [0, 10], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: exitEasing,
});
const blurVal = blurEnter + blurExit;
const scale = interpolate(frame, [0, enterDur], [1.01, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: enterEasing,
});
return (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "transparent",
}}
>
<span
className={className}
style={{
fontSize,
fontWeight,
color,
letterSpacing: "-0.03em",
fontFamily,
display: "inline-block",
transformOrigin: "50%",
opacity,
translate: `0 ${y}px`,
scale: `${scale}`,
filter: `blur(${blurVal}px)`,
}}
>
{text}
</span>
</div>
);
}

View File

@@ -0,0 +1,243 @@
"use client";
import { type CSSProperties, useMemo } from "react";
import { Easing, interpolate, useCurrentFrame } from "remotion";
export interface KineticCenterBuildProps {
text: string;
entryOffset?: number;
fontSize?: number;
color?: string;
fontWeight?: number;
speed?: number;
className?: string;
fontFamily?: string;
letterSpacing?: number;
gap?: number;
/** "harf": harfler sağdan girip satırı iter (upstream davranışı);
* "kelime": uzun cümlelerde kelime kelime */
birim?: "harf" | "kelime";
/** Vurgulanan kelimeler (kelime indeksi, iki modda da) ve yazı stili */
vurgu?: { indices: number[]; style: KineticWordStyle };
}
export interface KineticWordStyle {
fontFamily?: string;
fontWeight?: number;
fontStyle?: "normal" | "italic";
color?: string;
letterSpacing?: number;
}
// Ölçüm, kelimenin gerçekte basılacağı yazı tipiyle yapılır (Cal Sans,
// Bricolage…); aksi hâlde itme sırasında kelimeler üst üste biner.
function measureWidths(
words: string[],
fontSize: number,
styleFor: (i: number) => Required<KineticWordStyle>,
): number[] {
if (typeof document === "undefined") {
return words.map((w) => w.length * fontSize * 0.55);
}
const ctx = document.createElement("canvas").getContext("2d");
if (!ctx) return words.map((w) => w.length * fontSize * 0.55);
return words.map((w, i) => {
const st = styleFor(i);
ctx.font = `${st.fontStyle} ${st.fontWeight} ${fontSize}px "${st.fontFamily}"`;
return ctx.measureText(w).width + st.letterSpacing * fontSize * Array.from(w).length;
});
}
export function KineticCenterBuild({
text,
entryOffset = 88,
fontSize = 72,
color = "#171717",
fontWeight = 600,
speed = 1,
className,
fontFamily = "sans-serif",
letterSpacing = -0.03,
gap: gapProp,
vurgu,
birim = "harf",
}: KineticCenterBuildProps) {
const frame = useCurrentFrame() * speed;
const words = useMemo(
() => (birim === "kelime" ? text.split(" ") : Array.from(text)),
[text, birim],
);
// Her birimin ait olduğu kelime (vurgu kelime indeksiyle verilir)
const kelimeOf = useMemo(() => {
if (birim === "kelime") return words.map((_, i) => i);
let w = 0;
return words.map((ch) => {
const k = w;
if (ch === " ") w++;
return k;
});
}, [words, birim]);
const styleFor = (i: number): Required<KineticWordStyle> => {
const base = { fontFamily, fontWeight, fontStyle: "normal" as const, color, letterSpacing };
return vurgu?.indices.includes(kelimeOf[i]) ? { ...base, ...vurgu.style } : base;
};
const widths = useMemo(
() => measureWidths(words, fontSize, styleFor),
// eslint-disable-next-line react-hooks/exhaustive-deps
[words, fontSize, fontWeight, fontFamily, letterSpacing, vurgu, kelimeOf],
);
const gap = gapProp ?? (birim === "kelime" ? fontSize * 0.26 : 0);
const firstDur = 10;
const pushDur = 13;
const travelDur = 7;
const entryScale = 0.992;
const entryBlur = 3.5;
const reflowBlur = 0.8;
const firstWordY = 6;
const easing = Easing.bezier(0.2, 0.8, 0.2, 1);
const positionsAt = useMemo(() => {
const out: number[][] = [];
for (let k = 1; k <= words.length; k++) {
let total = gap * (k - 1);
for (let j = 0; j < k; j++) total += widths[j];
let cursor = -total / 2;
const xs: number[] = [];
for (let j = 0; j < k; j++) {
xs.push(cursor + widths[j] / 2);
cursor += widths[j] + gap;
}
out.push(xs);
}
return out;
}, [words, widths]);
const n = words.length;
return (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "transparent",
}}
>
<div
className={className}
style={{
position: "relative",
fontSize,
fontWeight,
color,
letterSpacing: `${letterSpacing}em`,
fontFamily,
whiteSpace: "nowrap",
}}
>
{words.map((word, j) => {
const entryStart = j === 0 ? 0 : firstDur + (j - 1) * pushDur;
const entryEnd = entryStart + (j === 0 ? firstDur : pushDur);
const targetX = positionsAt[j][j];
const xFrom = j === 0 ? 0 : targetX + entryOffset;
let x = targetX;
let opacity = 1;
let scale = 1;
let blur = 0;
let y = 0;
if (frame < entryStart) {
opacity = 0;
x = xFrom;
scale = entryScale;
blur = entryBlur;
y = j === 0 ? firstWordY : 0;
} else if (frame <= entryEnd) {
const range: [number, number] = [entryStart, entryEnd];
const travelRange: [number, number] = [
entryStart,
Math.min(entryStart + travelDur, entryEnd),
];
const opts = {
extrapolateLeft: "clamp" as const,
extrapolateRight: "clamp" as const,
easing,
};
x = interpolate(frame, travelRange, [xFrom, targetX], opts);
opacity = interpolate(frame, range, [0, 1], opts);
scale = interpolate(frame, range, [entryScale, 1], opts);
blur = interpolate(frame, range, [entryBlur, 0], opts);
y =
j === 0
? interpolate(frame, travelRange, [firstWordY, 0], opts)
: 0;
} else {
for (let w = j + 1; w < n; w++) {
const pushStart = firstDur + (w - 1) * pushDur;
const pushEnd = pushStart + pushDur;
const fromX = positionsAt[w - 1][j];
const toX = positionsAt[w][j];
if (frame >= pushEnd) {
x = toX;
} else if (frame >= pushStart) {
x = interpolate(
frame,
[pushStart, Math.min(pushStart + travelDur, pushEnd)],
[fromX, toX],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing,
},
);
blur = interpolate(
frame,
[pushStart, (pushStart + pushEnd) / 2, pushEnd],
[0, reflowBlur, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
break;
} else {
x = fromX;
break;
}
}
}
const st = styleFor(j);
const wordStyle: CSSProperties = {
fontFamily: st.fontFamily,
fontWeight: st.fontWeight,
fontStyle: st.fontStyle,
color: st.color,
letterSpacing: `${st.letterSpacing}em`,
};
return (
<span
key={j}
style={{
...wordStyle,
position: "absolute",
left: "50%",
top: "50%",
whiteSpace: "nowrap",
backfaceVisibility: "hidden",
transform: `translate(-50%, -50%) translate3d(${x}px, ${y}px, 0) scale(${scale})`,
filter: `blur(${blur}px)`,
opacity,
}}
>
{word}
</span>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,86 @@
"use client";
import { Easing, interpolate, useCurrentFrame } from "remotion";
export interface PerCharacterRiseProps {
text: string;
distance?: number;
fontSize?: number;
color?: string;
fontWeight?: number;
speed?: number;
className?: string;
}
export function PerCharacterRise({
text,
distance = 32,
fontSize = 72,
color = "#171717",
fontWeight = 600,
speed = 1,
className,
}: PerCharacterRiseProps) {
const frame = useCurrentFrame() * speed;
const chars = Array.from(text);
const charDurationFrames = 21;
const charTravelFrames = 10;
const staggerFrames = 1;
return (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "transparent",
}}
>
<span
className={className}
style={{
fontSize,
fontWeight,
color,
letterSpacing: "-0.05em",
fontFamily:
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
}}
>
{chars.map((char, i) => {
const local = frame - i * staggerFrames;
const easing = Easing.bezier(0.2, 0.8, 0.2, 1);
const travelEasing = Easing.bezier(0.2, 0.8, 0.6, 0.85);
const opacity = interpolate(local, [0, charDurationFrames], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing,
});
const y = interpolate(local, [0, charTravelFrames], [distance, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: travelEasing,
});
return (
<span
key={i}
style={{
display: "inline-block",
whiteSpace: "pre",
backfaceVisibility: "hidden",
transformOrigin: "50% 55%",
opacity,
translate: `0 ${y}px`,
}}
>
{char}
</span>
);
})}
</span>
</div>
);
}

View File

@@ -0,0 +1,72 @@
"use client";
import type {
TransitionPresentation,
TransitionPresentationComponentProps,
} from "@remotion/transitions";
import type React from "react";
import { AbsoluteFill, Easing, interpolate } from "remotion";
const clampOpts = {
extrapolateLeft: "clamp" as const,
extrapolateRight: "clamp" as const,
};
export type PushThroughProps = {
zoom?: number;
blur?: number;
/** İtmenin merkezi (px). İmlecin tıkladığı nokta verilir: sahne o noktanın
* içinden geçilerek terk edilir, yenisi aynı noktadan açılır. */
origin?: { x: number; y: number };
};
const PushThroughPresentation: React.FC<
TransitionPresentationComponentProps<PushThroughProps>
> = ({
children,
presentationProgress,
presentationDirection,
passedProps,
}) => {
const { zoom = 2.4, blur = 14, origin } = passedProps;
const transformOrigin = origin ? `${origin.x}px ${origin.y}px` : "50% 50%";
const entering = presentationDirection === "entering";
const p = presentationProgress;
if (!entering) {
const exitStyle: React.CSSProperties = {
transformOrigin,
opacity: interpolate(p, [0.4, 0.62], [1, 0], {
...clampOpts,
easing: Easing.bezier(0.42, 0, 0.58, 1),
}),
scale: `${interpolate(p, [0, 0.65], [1, zoom], {
...clampOpts,
easing: Easing.in(Easing.cubic),
})}`,
filter: `blur(${interpolate(p, [0.15, 0.6], [0, blur], clampOpts)}px)`,
};
return <AbsoluteFill style={exitStyle}>{children}</AbsoluteFill>;
}
const childStyle: React.CSSProperties = {
transformOrigin,
opacity: interpolate(p, [0.3, 0.5], [0, 1], clampOpts),
scale: `${interpolate(p, [0.3, 0.88, 1], [0.68, 1.02, 1], {
...clampOpts,
easing: Easing.out(Easing.cubic),
})}`,
filter: `blur(${interpolate(p, [0.3, 0.75], [blur * 0.7, 0], clampOpts)}px)`,
};
return <AbsoluteFill style={childStyle}>{children}</AbsoluteFill>;
};
export function pushThrough(
props: PushThroughProps = {},
): TransitionPresentation<PushThroughProps> {
return {
component: PushThroughPresentation,
props,
};
}

View File

@@ -0,0 +1,112 @@
"use client";
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
export interface SlotMachineRollProps {
from?: string;
to?: string;
fontSize?: number;
color?: string;
fontWeight?: number;
speed?: number;
className?: string;
fontFamily?: string;
/** true: tam kare sarmalayıcı ve beyaz zemin olmadan satır içi basar */
inline?: boolean;
/** Sütunlar arası kademe (kare) */
stagger?: number;
}
// Nokta, virgül ve boşluk rakam kadar yer kaplamasın
const charWidth = (ch: string) =>
ch === "." || ch === "," ? "0.3em" : ch === " " ? "0.25em" : ch === "%" ? "0.8em" : "0.62em";
export function SlotMachineRoll({
from = "$99",
to = "$199",
fontSize = 120,
color = "#171717",
fontWeight = 700,
speed = 1,
className,
fontFamily = "sans-serif",
inline = false,
stagger = 4,
}: SlotMachineRollProps) {
const frame = useCurrentFrame() * speed;
const { fps } = useVideoConfig();
const len = Math.max(from.length, to.length);
const paddedFrom = from.padStart(len, " ");
const paddedTo = to.padStart(len, " ");
const columns = [];
for (let i = 0; i < len; i++) {
const raw = spring({
frame: frame - i * stagger,
fps,
config: { damping: 14 },
});
// Değişmeyen karakter (ör. "6 yıl"daki "yıl") yerinde durur
const sabit = paddedFrom[i] === paddedTo[i];
const t = sabit ? 0 : Math.max(0, Math.min(1, raw));
columns.push(
<span
key={i}
style={{
display: "inline-block",
overflow: "hidden",
height: "1.1em",
lineHeight: "1.1em",
verticalAlign: "top",
width: paddedFrom[i] === paddedTo[i] && /[^\d\s]/.test(paddedTo[i]) ? "auto" : charWidth(paddedTo[i]),
textAlign: "center",
}}
>
<span
style={{
display: "flex",
flexDirection: "column",
translate: `0 ${-t * 1.1}em`,
}}
>
<span>{paddedFrom[i]}</span>
<span>{paddedTo[i]}</span>
</span>
</span>,
);
}
const metin = (
<span
className={className}
style={{
fontSize,
fontWeight,
color,
letterSpacing: "-0.03em",
fontFamily,
lineHeight: 1.1,
whiteSpace: "nowrap",
}}
>
{columns}
</span>
);
if (inline) return metin;
return (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "white",
}}
>
{metin}
</div>
);
}

View File

@@ -0,0 +1,77 @@
"use client";
import type { ReactNode } from "react";
import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
export interface TrackingInProps {
text: ReactNode;
fontFamily?: string;
background?: string;
endTracking?: number;
startTracking?: number;
startBlur?: number;
fontSize?: number;
color?: string;
fontWeight?: number;
speed?: number;
className?: string;
}
export function TrackingIn({
text,
startTracking = 0.5,
startBlur = 12,
fontSize = 96,
color = "#171717",
fontWeight = 700,
speed = 1,
className,
fontFamily = "sans-serif",
background = "transparent",
endTracking = -0.03,
}: TrackingInProps) {
const frame = useCurrentFrame() * speed;
const { fps } = useVideoConfig();
const t = spring({
frame,
fps,
config: { damping: 18, stiffness: 90 },
});
const letterSpacing = `${interpolate(t, [0, 1], [startTracking, endTracking])}em`;
const blurAmount = interpolate(t, [0, 1], [startBlur, 0]);
const opacity = interpolate(frame, [0, 15], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background,
}}
>
<span
className={className}
style={{
fontSize,
fontWeight,
color,
letterSpacing,
opacity,
filter: `blur(${blurAmount}px)`,
fontFamily,
whiteSpace: "nowrap",
}}
>
{text}
</span>
</div>
);
}

View File

@@ -0,0 +1,72 @@
"use client";
import { Caret } from "@/components/remocn/caret";
import { useTypewriter } from "@/lib/remocn-ui";
export interface TypewriterProps {
text: string;
cursor?: boolean;
charsPerSecond?: number;
speed?: number;
fontSize?: number;
color?: string;
cursorColor?: string;
fontWeight?: number;
className?: string;
}
export function Typewriter({
text,
cursor = true,
charsPerSecond = 22,
speed = 1,
fontSize = 48,
color = "#171717",
cursorColor = "#171717",
fontWeight = 600,
className,
}: TypewriterProps) {
const tw = useTypewriter(text, { cps: charsPerSecond, speed });
return (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "white",
}}
>
<span
className={className}
style={{
fontSize,
fontWeight,
color,
letterSpacing: "-0.03em",
fontFamily:
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
whiteSpace: "pre",
}}
>
{tw.text}
{cursor && (
<Caret
color={cursorColor}
blink={!tw.typing}
speed={speed}
radius={0}
style={{
width: "0.08em",
height: "1em",
marginLeft: "0.04em",
verticalAlign: "text-bottom",
}}
/>
)}
</span>
</div>
);
}

View File

@@ -0,0 +1,133 @@
"use client";
import { useCurrentFrame } from "remotion";
import type { CursorStyle } from "@/components/remocn/cursor";
import { clamp01, type EasingName, easings } from "@/lib/remocn-ui";
export interface CursorWaypoint {
at: number;
x: number;
y: number;
duration?: number;
click?: boolean;
press?: boolean;
easing?: EasingName;
}
export const DEFAULT_DURATION = 24;
export const CLICK_FRAMES = 16;
export const PRESS_FRAMES = 8;
export interface CursorPathOptions {
speed?: number;
defaultDuration?: number;
}
function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
export function ripplePhase(framesSinceClick: number): {
rippleOpacity: number;
rippleScale: number;
} {
if (framesSinceClick < 0 || framesSinceClick >= CLICK_FRAMES) {
return { rippleOpacity: 0, rippleScale: 0 };
}
const p = clamp01(framesSinceClick / CLICK_FRAMES);
return {
rippleOpacity: 0.5 * (1 - p),
rippleScale: 2.5 * easings.out(p),
};
}
export function clickPress(framesSinceClick: number): number {
if (framesSinceClick < 0 || framesSinceClick >= PRESS_FRAMES) return 1;
const half = PRESS_FRAMES / 2;
const p =
framesSinceClick < half
? framesSinceClick / half
: 1 - (framesSinceClick - half) / half;
return 1 - clamp01(p);
}
export function useCursorPath(
waypoints: CursorWaypoint[],
opts: CursorPathOptions = {},
): CursorStyle {
const raw = useCurrentFrame();
return cursorPathAt(waypoints, raw, opts);
}
export function cursorPathAt(
waypoints: CursorWaypoint[],
raw: number,
opts: CursorPathOptions = {},
): CursorStyle {
const { speed = 1, defaultDuration = DEFAULT_DURATION } = opts;
const effectiveFrame = raw * speed;
if (waypoints.length === 0) {
return { x: 0, y: 0, scale: 1, rippleOpacity: 0, rippleScale: 0 };
}
const first = waypoints[0];
if (effectiveFrame <= first.at) {
return {
x: first.x,
y: first.y,
scale: 1,
rippleOpacity: 0,
rippleScale: 0,
pressScale: 1,
};
}
let toIndex = waypoints.length - 1;
for (let i = 1; i < waypoints.length; i++) {
if (waypoints[i].at > effectiveFrame) {
toIndex = i;
break;
}
}
const pastLast = effectiveFrame >= waypoints[waypoints.length - 1].at;
const to = pastLast ? waypoints[waypoints.length - 1] : waypoints[toIndex];
const from = pastLast
? waypoints[waypoints.length - 1]
: waypoints[toIndex - 1];
const dur = to.duration ?? defaultDuration;
const ease = easings[to.easing ?? "inOut"];
const start = to.at - dur;
const t =
pastLast || dur <= 0 ? 1 : ease(clamp01((effectiveFrame - start) / dur));
const x = lerp(from.x, to.x, t);
const y = lerp(from.y, to.y, t);
let lastClickAt = -Infinity;
for (const wp of waypoints) {
if (wp.click && wp.at <= effectiveFrame && wp.at > lastClickAt)
lastClickAt = wp.at;
}
const sinceClick =
lastClickAt === -Infinity ? -1 : effectiveFrame - lastClickAt;
const ripple = ripplePhase(sinceClick);
const clickDip = clickPress(sinceClick);
const holdWp = pastLast ? waypoints[waypoints.length - 1] : from;
const heldPress = holdWp.press ? 0 : 1;
const pressScale = Math.min(clickDip, heldPress);
return {
x,
y,
scale: 1,
rippleOpacity: ripple.rippleOpacity,
rippleScale: ripple.rippleScale,
pressScale,
};
}

View File

@@ -0,0 +1,22 @@
import { useEffect, useState } from "react";
import { continueRender, delayRender } from "remotion";
/**
* Kinetik yerleşimler metni gerçek yazı tipiyle ölçer. Yazı tipleri
* yüklenmeden ölçülürse kelimeler kayar; bu kanca yazı tipleri hazır olana
* kadar render'ı bekletir ve hazır olunca ağacı yeniden çizdirir.
*/
export function useFontlarHazir() {
const [hazir, setHazir] = useState(
() => typeof document !== "undefined" && document.fonts.status === "loaded",
);
const [handle] = useState(() => (hazir ? null : delayRender("yazı tipleri")));
useEffect(() => {
if (hazir) return;
document.fonts.ready.then(() => {
setHazir(true);
if (handle !== null) continueRender(handle);
});
}, [hazir, handle]);
return hazir;
}

38
video/src/fontlar.ts Normal file
View File

@@ -0,0 +1,38 @@
import { loadFont as yerelFont } from "@remotion/fonts";
import { loadFont as bricolageYukle } from "@remotion/google-fonts/BricolageGrotesque";
import { loadFont as calSansYukle } from "@remotion/google-fonts/CalSans";
import { staticFile } from "remotion";
// Sitenin dört yazı tipinden üçü (DESIGN.md §3): Cal Sans başlık ve iri sayı,
// Clarity City gövde/buton, Bricolage vurgu. Geist Mono arayüzde yok.
const cal = calSansYukle("normal", {
weights: ["400"],
subsets: ["latin", "latin-ext"],
});
const bricolage = bricolageYukle("normal", {
weights: ["300", "400", "600", "700", "800"],
subsets: ["latin", "latin-ext"],
});
const CLARITY = "Clarity City";
for (const [dosya, agirlik] of [
["Regular", "400"],
["Medium", "500"],
["SemiBold", "600"],
["Bold", "700"],
["ExtraBold", "800"],
] as const) {
yerelFont({
family: CLARITY,
url: staticFile(`fonts/ClarityCity-${dosya}.woff2`),
weight: agirlik,
});
}
export const FONT = {
/** Cal Sans yalnız 400; site `font-bold` ile sentetik kalınlık kullanıyor,
* video da aynı görünsün diye başlıklarda fontWeight 700 verilir. */
baslik: cal.fontFamily,
govde: CLARITY,
vurgu: bricolage.fontFamily,
} as const;

View File

@@ -0,0 +1,137 @@
import type {
TransitionPresentation,
TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { AbsoluteFill, Easing, interpolate } from "remotion";
import { H, LOGO_KARELER, PIKSEL, W } from "../marka";
import { clamp01, hash } from "../yardimci";
/**
* Piksel dalgası: yeni sahne, bir noktadan (çoğunlukla imlecin tıkladığı
* yer) yayılan iri piksellerle açılır. Dalganın önünde logonun üç rengi
* (mavi, şeftali, turuncu) yanıp söner — hero haritasındaki ısınan
* piksellerin sahne ölçeğindeki hâli. Hücreler sitenin nokta oranını
* (%74, rx %20) taşır; dalga geçince birleşip kesintisiz yüzeye döner.
*/
export type PikselDalgaProps = {
origin?: { x: number; y: number };
/** Hücre kenarı (px) */
hucre?: number;
/** Dalga önünün kalınlığı (px) */
bant?: number;
/** Dalga önündeki piksellerin renkleri */
renkler?: string[];
/** Çıkan sahne hafifçe geri çekilsin (derinlik) */
geriCekil?: number;
tohum?: number;
};
const VARSAYILAN_RENKLER = LOGO_KARELER.map((k) => k.renk);
const koseUzakligi = (o: { x: number; y: number }) =>
Math.max(
Math.hypot(o.x, o.y),
Math.hypot(W - o.x, o.y),
Math.hypot(o.x, H - o.y),
Math.hypot(W - o.x, H - o.y),
);
/**
* Dalga önünün yarıçapı. Gelen sahne kendi öğelerini dalgayla eş zamanlı
* belirtmek isterse (ör. haritanın noktaları) aynı fonksiyonu kullanır.
*/
export const dalgaYaricapi = (
p: number,
origin: { x: number; y: number } = { x: W / 2, y: H / 2 },
bant = 420,
) =>
interpolate(clamp01(p), [0, 1], [0, koseUzakligi(origin) + bant * 1.25], {
easing: Easing.bezier(0.45, 0.05, 0.4, 1),
});
const PikselDalgaSunum: React.FC<
TransitionPresentationComponentProps<PikselDalgaProps>
> = ({ children, presentationProgress, presentationDirection, passedProps }) => {
const {
origin = { x: W / 2, y: H / 2 },
hucre = 64,
bant = 420,
renkler = VARSAYILAN_RENKLER,
geriCekil = 0.05,
tohum = 3,
} = passedProps;
const p = presentationProgress;
if (presentationDirection === "exiting") {
return (
<AbsoluteFill
style={{
transformOrigin: `${origin.x}px ${origin.y}px`,
scale: `${1 - geriCekil * Easing.in(Easing.cubic)(p)}`,
}}
>
{children}
</AbsoluteFill>
);
}
const sutun = Math.ceil(W / hucre);
const satir = Math.ceil(H / hucre);
const r = dalgaYaricapi(p, origin, bant);
let maske = "";
let renkli = "";
for (let j = 0; j < satir; j++) {
for (let i = 0; i < sutun; i++) {
const cx = i * hucre + hucre / 2;
const cy = j * hucre + hucre / 2;
const d = Math.hypot(cx - origin.x, cy - origin.y) + (hash(i, j, tohum) - 0.5) * bant * 0.55;
const t = clamp01((r - d) / bant);
if (t <= 0) continue;
// Önde renkli piksel belirip söner…
if (t < 0.72) {
const c = Math.sin(Math.PI * (t / 0.72));
const boy = hucre * PIKSEL.nokta * c;
const renk = renkler[Math.floor(hash(i, j, tohum + 9) * renkler.length)];
renkli += `<rect x="${cx - boy / 2}" y="${cy - boy / 2}" width="${boy}" height="${boy}" rx="${boy * PIKSEL.rx}" fill="${renk}"/>`;
}
// …arkasından yeni sahne piksel piksel açılır
const m = Easing.bezier(0.22, 1, 0.36, 1)(clamp01((t - 0.38) / 0.62));
if (m > 0) {
const boy = hucre * (PIKSEL.nokta + 0.3 * m);
const rx = hucre * PIKSEL.rx * (1 - m);
maske += `<rect x="${cx - boy / 2}" y="${cy - boy / 2}" width="${boy}" height="${boy}" rx="${rx}" fill="white"/>`;
}
}
}
const maskeUrl = `url("data:image/svg+xml;utf8,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">${maske}</svg>`,
)}")`;
return (
<AbsoluteFill>
<AbsoluteFill
style={{
maskImage: maskeUrl,
WebkitMaskImage: maskeUrl,
maskSize: "100% 100%",
WebkitMaskSize: "100% 100%",
maskRepeat: "no-repeat",
WebkitMaskRepeat: "no-repeat",
}}
>
{children}
</AbsoluteFill>
<AbsoluteFill style={{ pointerEvents: "none" }}>
<svg width={W} height={H} dangerouslySetInnerHTML={{ __html: renkli }} />
</AbsoluteFill>
</AbsoluteFill>
);
};
export function pikselDalga(
props: PikselDalgaProps = {},
): TransitionPresentation<PikselDalgaProps> {
return { component: PikselDalgaSunum, props };
}

4
video/src/index.ts Normal file
View File

@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);

View File

@@ -0,0 +1,87 @@
import {
clampChroma,
converter,
formatRgb,
interpolate,
type Oklch,
parse,
type Rgb,
} from "culori";
const toRgb = converter("rgb");
const toOklch = converter("oklch");
const BLACK: Rgb = { mode: "rgb", r: 0, g: 0, b: 0, alpha: 1 };
export function parseColor(c: string): Rgb {
const s = c.trim();
if (s.startsWith("var(")) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`[remocn-ui] parseColor cannot resolve CSS variable "${s}" under Remotion's per-frame render. ` +
"Animated colors must be concrete oklch/hex/rgb values supplied via the theme." +
"Falling back to the JS default.",
);
}
return { ...BLACK };
}
const rgb = toRgb(parse(s));
if (!rgb) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`[remocn-ui] parseColor could not parse "${s}"; using black.`,
);
}
return { ...BLACK };
}
return { mode: "rgb", r: rgb.r, g: rgb.g, b: rgb.b, alpha: rgb.alpha ?? 1 };
}
export function oklchToRgb(l: number, c: number, h: number): Rgb {
const mapped = clampChroma({ mode: "oklch", l, c, h }, "oklch", "rgb");
return toRgb(mapped);
}
export function rgbToOklch(rgb: Rgb): Oklch {
const { l, c, h } = toOklch(rgb);
return { mode: "oklch", l, c, h: Number.isFinite(h) ? h : 0 };
}
function resolveColorString(s: string): string {
const trimmed = s.trim();
if (trimmed.startsWith("var(")) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`[remocn-ui] mixOklch cannot resolve CSS variable "${trimmed}" under Remotion's per-frame render. ` +
"Animated colors must be concrete oklch/hex/rgb values supplied via the theme." +
"Falling back to the JS default.",
);
}
return "#000";
}
if (!parse(trimmed)) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`[remocn-ui] mixOklch could not parse "${trimmed}"; using black.`,
);
}
return "#000";
}
return trimmed;
}
export function mixOklch(a: string, b: string, t: number): string {
const mixed = clampChroma(
interpolate([resolveColorString(a), resolveColorString(b)], "oklch")(t),
"oklch",
"rgb",
);
return toCss(toRgb(mixed));
}
export function toCss(color: Rgb): string {
return formatRgb(color);
}

View File

@@ -0,0 +1,27 @@
export {
mixOklch,
oklchToRgb,
parseColor,
rgbToOklch,
toCss,
} from "./color";
export type { EasingName, SpringName } from "./motion";
export { easings, springs } from "./motion";
export type { RemocnTheme, RemocnUIProviderProps } from "./theme";
export {
defaultDarkTheme,
defaultLightTheme,
RemocnUIProvider,
useRemocnTheme,
} from "./theme";
export type { TypewriterOptions, TypewriterState } from "./timeline";
export {
clamp01,
framesFor,
revealCount,
revealedText,
useCurrentState,
useStateTransition,
useTypewriter,
} from "./timeline";
export type { Step } from "./types";

View File

@@ -0,0 +1,17 @@
export const easings = {
linear: (t: number): number => t,
out: (t: number): number => 1 - (1 - t) ** 3,
in: (t: number): number => t * t * t,
inOut: (t: number): number =>
t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2,
} as const;
export type EasingName = keyof typeof easings;
export const springs = {
snappy: { damping: 18, stiffness: 220, mass: 0.7 },
soft: { damping: 14, stiffness: 120, mass: 0.9 },
bouncy: { damping: 10, stiffness: 180, mass: 0.8 },
} as const;
export type SpringName = keyof typeof springs;

View File

@@ -0,0 +1,108 @@
"use client";
import type { ReactNode } from "react";
import { createContext, createElement, useContext } from "react";
export interface RemocnTheme {
background: string;
foreground: string;
card: string;
cardForeground: string;
popover: string;
popoverForeground: string;
primary: string;
primaryForeground: string;
secondary: string;
secondaryForeground: string;
muted: string;
mutedForeground: string;
accent: string;
accentForeground: string;
destructive: string;
destructiveForeground: string;
border: string;
input: string;
ring: string;
radius: number;
}
export const defaultLightTheme: RemocnTheme = {
background: "oklch(1 0 0)",
foreground: "oklch(0.145 0 0)",
card: "oklch(1 0 0)",
cardForeground: "oklch(0.145 0 0)",
popover: "oklch(1 0 0)",
popoverForeground: "oklch(0.145 0 0)",
primary: "oklch(0.205 0 0)",
primaryForeground: "oklch(0.985 0 0)",
secondary: "oklch(0.97 0 0)",
secondaryForeground: "oklch(0.205 0 0)",
muted: "oklch(0.97 0 0)",
mutedForeground: "oklch(0.556 0 0)",
accent: "oklch(0.97 0 0)",
accentForeground: "oklch(0.205 0 0)",
destructive: "oklch(0.577 0.245 27.325)",
destructiveForeground: "oklch(0.985 0 0)",
border: "oklch(0.922 0 0)",
input: "oklch(0.922 0 0)",
ring: "oklch(0.708 0 0)",
radius: 10,
};
export const defaultDarkTheme: RemocnTheme = {
background: "oklch(0.145 0 0)",
foreground: "oklch(0.985 0 0)",
card: "oklch(0.205 0 0)",
cardForeground: "oklch(0.985 0 0)",
popover: "oklch(0.205 0 0)",
popoverForeground: "oklch(0.985 0 0)",
primary: "oklch(0.922 0 0)",
primaryForeground: "oklch(0.205 0 0)",
secondary: "oklch(0.269 0 0)",
secondaryForeground: "oklch(0.985 0 0)",
muted: "oklch(0.269 0 0)",
mutedForeground: "oklch(0.708 0 0)",
accent: "oklch(0.269 0 0)",
accentForeground: "oklch(0.985 0 0)",
destructive: "oklch(0.704 0.191 22.216)",
destructiveForeground: "oklch(0.985 0 0)",
border: "oklch(1 0 0 / 10%)",
input: "oklch(1 0 0 / 15%)",
ring: "oklch(0.556 0 0)",
radius: 10,
};
interface RemocnThemeContextValue {
theme?: Partial<RemocnTheme>;
mode?: "light" | "dark";
}
const RemocnThemeContext = createContext<RemocnThemeContextValue>({});
export interface RemocnUIProviderProps {
theme?: Partial<RemocnTheme>;
mode?: "light" | "dark";
children: ReactNode;
}
export function RemocnUIProvider({
theme,
mode,
children,
}: RemocnUIProviderProps) {
return createElement(
RemocnThemeContext.Provider,
{ value: { theme, mode } },
children,
);
}
export function useRemocnTheme(
override?: Partial<RemocnTheme>,
modeOverride?: "light" | "dark",
): RemocnTheme {
const ctx = useContext(RemocnThemeContext);
const mode = modeOverride ?? ctx.mode ?? "light";
const base = mode === "dark" ? defaultDarkTheme : defaultLightTheme;
return { ...base, ...ctx.theme, ...override };
}

View File

@@ -0,0 +1,96 @@
import { useCurrentFrame, useVideoConfig } from "remotion";
import type { Step } from "./types";
export function framesFor(
d: number | { seconds: number },
fps: number,
): number {
return typeof d === "number" ? d : Math.round(d.seconds * fps);
}
export function revealCount(
localFrame: number,
fps: number,
len: number,
cps: number,
): number {
const over = (len / cps) * fps;
if (over <= 0) return len;
return Math.max(0, Math.min(len, Math.floor((localFrame / over) * len)));
}
export function clamp01(t: number): number {
return Math.max(0, Math.min(1, t));
}
export function revealedText(full: string, count: number): string {
const c = Math.max(0, Math.min(full.length, Math.floor(count)));
return full.slice(0, c);
}
export interface TypewriterOptions {
cps?: number;
speed?: number;
startFrame?: number;
}
export interface TypewriterState {
text: string;
count: number;
done: boolean;
typing: boolean;
}
export function useTypewriter(
full: string,
options: TypewriterOptions = {},
): TypewriterState {
const { cps = 20, speed = 1, startFrame = 0 } = options;
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const local = frame * speed - startFrame;
const count = local <= 0 ? 0 : revealCount(local, fps, full.length, cps);
return {
text: revealedText(full, count),
count,
done: count >= full.length,
typing: count > 0 && count < full.length,
};
}
export function useCurrentState<S extends string>(
steps: Step<S>[],
defaultState: S,
speed = 1,
): S {
const effectiveFrame = useCurrentFrame() * speed;
let current = defaultState;
let bestAt = -Infinity;
steps.forEach((step) => {
if (step.at <= effectiveFrame && step.at >= bestAt) {
bestAt = step.at;
current = step.state;
}
});
return current;
}
export function useStateTransition<S extends string>(
steps: Step<S>[],
defaultState: S,
speed = 1,
defaultDuration = 8,
): { from: S; to: S; progress: number } {
const effectiveFrame = useCurrentFrame() * speed;
const started = steps
.map((step, index) => ({ step, index }))
.sort((a, b) => a.step.at - b.step.at || a.index - b.index)
.filter((e) => e.step.at <= effectiveFrame);
if (started.length === 0)
return { from: defaultState, to: defaultState, progress: 1 };
const to = started[started.length - 1].step;
const from = started.length >= 2 ? started[started.length - 2].step : null;
const dur = to.duration ?? defaultDuration;
const progress = dur > 0 ? clamp01((effectiveFrame - to.at) / dur) : 1;
return { from: from ? from.state : defaultState, to: to.state, progress };
}

View File

@@ -0,0 +1,5 @@
export interface Step<S extends string = string> {
at: number;
state: S;
duration?: number;
}

67
video/src/marka.ts Normal file
View File

@@ -0,0 +1,67 @@
/**
* Lansman videosunun görsel dili — docs/tasarim/DESIGN.md'nin video karşılığı.
* Renkler sitenin Tailwind v4 tonlarıyla birebir (oklch); logo renkleri
* public/kolay-tercih-mark.svg'den. Video 1080p olduğu için arayüz ölçüleri
* sitenin 2 katıdır (h-12 = 48 px → 96–104 px).
*/
export const W = 1920;
export const H = 1080;
export const FPS = 30;
export const R = {
// Bilgi mavisi (--primary) ve logo mavisi
primary: "oklch(0.623 0.188 259.8)",
mavi: "#3b82f6",
mavi400: "oklch(0.707 0.165 254.624)",
// Logo: şeftali + turuncu kare
seftali: "#fdc7a7",
turuncuLogo: "#ff5a00",
// Aksiyon turuncusu (orange-500) ve hover'ı
turuncu: "oklch(0.705 0.213 47.604)",
turuncu600: "oklch(0.646 0.222 41.116)",
turuncu50: "oklch(0.98 0.016 73.684)",
slate50: "oklch(0.984 0.003 247.858)",
slate100: "oklch(0.968 0.007 247.896)",
slate200: "oklch(0.929 0.013 255.508)",
slate300: "oklch(0.869 0.022 252.894)",
slate400: "oklch(0.704 0.04 256.788)",
slate500: "oklch(0.554 0.046 257.417)",
slate600: "oklch(0.446 0.043 257.281)",
slate700: "oklch(0.372 0.044 257.287)",
slate800: "oklch(0.279 0.041 260.031)",
slate900: "oklch(0.208 0.042 265.755)",
slate950: "oklch(0.129 0.042 264.695)",
beyaz: "#ffffff",
// Risk / dilim renkleri — her zaman metin etiketiyle birlikte
emerald500: "oklch(0.696 0.17 162.48)",
emerald600: "oklch(0.596 0.145 163.225)",
amber500: "oklch(0.769 0.188 70.08)",
amber600: "oklch(0.666 0.179 58.318)",
red500: "oklch(0.637 0.237 25.331)",
red600: "oklch(0.577 0.245 27.325)",
// Gösterge kareleri (§4.11)
indigo500: "oklch(0.585 0.233 277.117)",
green600: "oklch(0.627 0.194 149.214)",
} as const;
/** Piksel imzası: hero ızgarası (hücre 10, nokta 7.4, rx 2) oranları */
export const PIKSEL = {
nokta: 0.74,
rx: 0.2,
// Hero doku paleti (turkey-pixel-map.tsx)
opakliklar: [0.3, 0.16, 0.24, 0.12, 0.27, 0.19],
} as const;
export const opaklikFor = (x: number, y: number) =>
PIKSEL.opakliklar[(x * 7 + y * 11) % PIKSEL.opakliklar.length];
/** Logo işareti (viewBox 180) */
export const LOGO_KARELER = [
{ x: 27, y: 56, boy: 57, renk: "#3b82f6" },
{ x: 91, y: 29, boy: 56, renk: "#fdc7a7" },
{ x: 91, y: 95, boy: 56, renk: "#ff5a00" },
] as const;

View File

@@ -0,0 +1,183 @@
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
import { TrackingIn } from "@/components/remocn/tracking-in";
import { Vinyet } from "../bilesenler/Gren";
import { Cikis } from "../bilesenler/Kinetik";
import { FONT } from "../fontlar";
import { H, PIKSEL, R, W } from "../marka";
import { GURULTU_SAYILARI } from "../veri";
import { aralik, clamp01, ease, hash, lerp } from "../yardimci";
/**
* 1 · Gürültü. Koyu zemin, derinlikten üstüne gelen gerçek taban sıraları
* (tercih dönemindeki tablo kalabalığı). Üç kinetik satır; sonunda bütün
* sayılar merkeze çöker ve geriye **tek bir turuncu piksel** kalır. Bir
* sonraki geçiş o pikselden yayılan dalgadır.
*/
const ADET = 60;
const DERINLIK = 3200;
const YAKIN = 70;
const ODAK = 720;
const COKUS: [number, number] = [104, 122];
const tunel = Array.from({ length: ADET }, (_, i) => {
let x = (hash(i, 1) - 0.5) * 2800;
let y = (hash(i, 2) - 0.5) * 1600;
// Başlık alanının içinden geçmesin: merkeze yakın olanlar dışarı itilir
if (Math.abs(x) < 520 && Math.abs(y) < 260) {
x += Math.sign(x || 1) * 520;
y += Math.sign(y || 1) * 180;
}
const r = hash(i, 4);
return {
metin: GURULTU_SAYILARI[i % GURULTU_SAYILARI.length],
x,
y,
z0: hash(i, 3) * DERINLIK,
renk: r > 0.9 ? R.turuncu : r > 0.78 ? R.mavi400 : R.slate400,
};
});
// Tünelde kat edilen yol: sabit hız, çöküşe doğru hızlanır
const yol = (f: number) => f * 24 + Math.max(0, f - 70) ** 2 * 0.9;
export const S1Gurultu: React.FC = () => {
const f = useCurrentFrame();
const cokus = aralik(f, COKUS[0], COKUS[1], ease.in);
const merkezPiksel = aralik(f, 116, 122, ease.geriTep);
return (
<AbsoluteFill style={{ background: R.slate950, overflow: "hidden" }}>
{/* Derinlik tüneli */}
{tunel.map((t, i) => {
const z = YAKIN + ((((t.z0 - yol(f)) % DERINLIK) + DERINLIK) % DERINLIK);
const s = ODAK / z;
let X = W / 2 + t.x * s;
let Y = H / 2 + t.y * s;
let olcek = s;
// Çöküş: her sayı merkeze çekilip piksele küçülür
X = lerp(X, W / 2, cokus);
Y = lerp(Y, H / 2, cokus);
olcek = lerp(olcek, 0.02, cokus);
const uzak = aralik(z, DERINLIK, DERINLIK * 0.72);
const yakin = aralik(z, YAKIN, 220);
const op = uzak * yakin * 0.7 * (1 - aralik(f, COKUS[1] - 4, COKUS[1]));
const bulanik = Math.min(7, Math.abs(z - ODAK) / 420) + (1 - yakin) * 6;
if (op <= 0.01) return null;
return (
<div
key={i}
style={{
position: "absolute",
left: X,
top: Y,
transform: `translate(-50%, -50%) scale(${olcek})`,
fontFamily: FONT.baslik,
fontWeight: 700,
fontSize: 64,
color: t.renk,
opacity: op,
filter: `blur(${bulanik}px)`,
whiteSpace: "nowrap",
zIndex: Math.round(10000 - z),
}}
>
{t.metin}
</div>
);
})}
{/* Başlığın arkasını sakinleştiren karartma */}
<AbsoluteFill
style={{
background: `radial-gradient(ellipse 34% 26% at 50% 50%, ${R.slate950} 30%, rgba(2,6,23,0) 100%)`,
opacity: 0.9,
}}
/>
{/* Kinetik satırlar */}
<Sequence from={6} durationInFrames={46}>
<Cikis bas={36}>
<TrackingIn
text="24 tercih."
fontFamily={FONT.baslik}
fontSize={190}
color={R.beyaz}
startTracking={1.1}
endTracking={0.015}
startBlur={16}
/>
</Cikis>
</Sequence>
<Sequence from={48} durationInFrames={50}>
<Cikis bas={42}>
<KineticCenterBuild
text="Binlerce ihtimal."
fontFamily={FONT.baslik}
fontWeight={700}
fontSize={176}
color={R.beyaz}
letterSpacing={0.015}
entryOffset={140}
speed={5.5}
vurgu={{ indices: [1], style: { color: R.slate400 } }}
/>
</Cikis>
</Sequence>
<Sequence from={94} durationInFrames={30}>
<HangisiSenin />
</Sequence>
{/* Geriye kalan tek piksel — dalga buradan doğar */}
{merkezPiksel > 0 && (
<div
style={{
position: "absolute",
left: W / 2,
top: H / 2,
width: 44 * PIKSEL.nokta,
height: 44 * PIKSEL.nokta,
borderRadius: 44 * PIKSEL.nokta * PIKSEL.rx,
background: R.turuncu,
transform: `translate(-50%, -50%) scale(${merkezPiksel})`,
}}
/>
)}
<Vinyet guc={0.6} />
</AbsoluteFill>
);
};
/** "Hangisi senin?" — belirir, sonra harf aralığı sıfırın altına çöküp piksele küçülür */
const HangisiSenin: React.FC = () => {
const f = useCurrentFrame();
const cok = aralik(f, 12, 26, ease.in);
return (
<AbsoluteFill
style={{
transform: `scale(${lerp(1, 0.04, cok)})`,
opacity: 1 - clamp01((f - 22) / 4),
filter: `blur(${cok * 6}px)`,
}}
>
<TrackingIn
text={
<>
Hangisi{" "}
<span style={{ fontFamily: FONT.vurgu, fontStyle: "italic", color: R.turuncu, letterSpacing: "-0.04em" }}>
senin?
</span>
</>
}
fontFamily={FONT.baslik}
fontSize={176}
color={R.beyaz}
startTracking={0.7}
endTracking={lerp(0.015, -0.2, cok)}
startBlur={12}
speed={1.4}
/>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,355 @@
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
import { TrackingIn } from "@/components/remocn/tracking-in";
import { Imlec, isi, isiIzi, type Yol } from "../bilesenler/Imlec";
import { Cikis, Kutu } from "../bilesenler/Kinetik";
import { LogoIsaret } from "../bilesenler/Logo";
import { FONT } from "../fontlar";
import { dalgaYaricapi } from "../gecisler/piksel-dalga";
import { H, LOGO_KARELER, PIKSEL, R, W, opaklikFor } from "../marka";
import { IZGARA_SATIR, IZGARA_SUTUN, SEHIRLER, TURKIYE_HUCRELER } from "../turkiye";
import { IL_PROGRAM, TOPLAM_PROGRAM } from "../veri";
import { aralik, clamp01, ease, hash, karis, lerp, olc, tr } from "../yardimci";
import { GECIS, SAHNE, gecisBasi } from "../zaman";
/**
* 2 · Harita. Dalga geçerken hero'nun dot-matrix Türkiye'si piksel piksel
* belirir; harita derinlikte yatık durur, illerin program sayısı kadar
* piksel kuleleri yükselir. İmleç haritanın üstünden geçer, sitedeki gibi
* pikseller turuncuya ısınıp yavaş soğur. Sonra bütün pikseller logonun üç
* karesine toplanır; "KolayTercih" harf aralığı daralarak (tracking) yerine
* oturur. İmleç turuncu kareye tıklar → bir sonraki dalga oradan.
*/
const HUCRE = 24;
const PERSPEKTIF = 2600;
const DALGA_BANT = 420;
const MORF: [number, number] = [110, 146];
const LOGO_BOY = 260;
const LOGO_MERKEZ_Y = 470;
// Kelime markanın logo svg'sindeki oranlarıyla: yazı 46.7/44 işaret boyu
// sağda başlar, punto işaret boyunun ~0.63'ü
const KELIME_BOY = 164;
const KELIME_OFSET = (46.7 / 44) * LOGO_BOY;
/** Logonun son yerleşimi (işaret + kelime ortalı) */
export const logoYerlesimi = () => {
const ww =
olc("Kolay", { family: FONT.vurgu, size: KELIME_BOY, weight: 300, tracking: -0.05 }) +
olc("Tercih", { family: FONT.vurgu, size: KELIME_BOY, weight: 600, tracking: -0.05 });
const toplam = KELIME_OFSET + ww;
const sol = W / 2 - toplam / 2;
return { sol, ww, ust: LOGO_MERKEZ_Y - LOGO_BOY / 2 };
};
/** İmlecin tıkladığı nokta: turuncu karenin merkezi */
export const haritaTiklama = () => {
const { sol, ust } = logoYerlesimi();
const k = LOGO_KARELER[2];
return {
x: sol + ((k.x + k.boy / 2) / 180) * LOGO_BOY,
y: ust + ((k.y + k.boy / 2) / 180) * LOGO_BOY,
};
};
const TIKLAMA_KARESI = gecisBasi(SAHNE.harita, GECIS.g2);
// ——— Harita noktaları ve morf hedefleri (bir kez hesaplanır) ———
const bolgeOf = (x: number, y: number) => (x < 30 ? 0 : y < 12 ? 1 : 2);
const noktalar = (() => {
const ham = TURKIYE_HUCRELER.map(({ x, y }) => ({
gx: x,
gy: y,
mx: (x - IZGARA_SUTUN / 2 + 0.5) * HUCRE,
my: (y - IZGARA_SATIR / 2 + 0.5) * HUCRE,
op: 0.2 + opaklikFor(x, y) * 1.7,
bolge: bolgeOf(x, y),
}));
const sinir = [0, 1, 2].map((b) => {
const n = ham.filter((p) => p.bolge === b);
return {
n: n.length,
x0: Math.min(...n.map((p) => p.gx)),
x1: Math.max(...n.map((p) => p.gx)),
y0: Math.min(...n.map((p) => p.gy)),
y1: Math.max(...n.map((p) => p.gy)),
};
});
const mSol = W / 2 - LOGO_BOY / 2;
const mUst = LOGO_MERKEZ_Y - LOGO_BOY / 2;
return ham.map((p, i) => {
const s = sinir[p.bolge];
const k = LOGO_KARELER[p.bolge];
const u = (p.gx - s.x0) / Math.max(1, s.x1 - s.x0);
const v = (p.gy - s.y0) / Math.max(1, s.y1 - s.y0);
const kk = Math.ceil(Math.sqrt(s.n));
const kenar = (k.boy / 180) * LOGO_BOY;
const hc = kenar / kk;
return {
...p,
hedefX: mSol + (k.x / 180) * LOGO_BOY + (Math.min(kk - 1, Math.floor(u * kk)) + 0.5) * hc,
hedefY: mUst + (k.y / 180) * LOGO_BOY + (Math.min(kk - 1, Math.floor(v * kk)) + 0.5) * hc,
hedefBoy: hc * 1.08,
hedefRenk: k.renk,
// Soldan sağa süpürülen toplanma + biraz dağınıklık
gecikme: (p.gx / IZGARA_SUTUN) * 10 + hash(i, 5) * 6,
};
});
})();
const kuleler = SEHIRLER.filter((s) => IL_PROGRAM[s.name]).map((s, i) => ({
...s,
mx: (s.x - IZGARA_SUTUN / 2 + 0.5) * HUCRE,
my: (s.y - IZGARA_SATIR / 2 + 0.5) * HUCRE,
n: IL_PROGRAM[s.name],
yukseklik: 16 + 170 * Math.sqrt(IL_PROGRAM[s.name] / IL_PROGRAM["İstanbul"]),
i,
}));
const ETIKETLI = ["İstanbul", "Ankara", "İzmir"];
// ——— Kamera ———
type Kamera = { egim: number; donus: number; zoom: number; cx: number; cy: number };
const kameraAt = (f: number): Kamera => {
const t = aralik(f, 0, 130, ease.inOut);
return {
egim: (lerp(56, 44, t) * Math.PI) / 180,
donus: (lerp(-9, -3, t) * Math.PI) / 180,
zoom: lerp(0.94, 1.06, t),
cx: W / 2,
cy: lerp(560, 540, t),
};
};
/** Harita uzayından ekrana: Z çevresinde dönüş, X çevresinde eğim, perspektif */
const izdus = (mx: number, my: number, mz: number, k: Kamera) => {
const x1 = mx * Math.cos(k.donus) - my * Math.sin(k.donus);
const y1 = mx * Math.sin(k.donus) + my * Math.cos(k.donus);
const y2 = y1 * Math.cos(k.egim) - mz * Math.sin(k.egim);
const z2 = y1 * Math.sin(k.egim) + mz * Math.cos(k.egim);
const s = (PERSPEKTIF / (PERSPEKTIF - z2)) * k.zoom;
return { x: k.cx + x1 * s, y: k.cy + y2 * s, s };
};
// ——— İmleç ———
const tik = () => haritaTiklama();
const imlecYolu = (): Yol => {
const t = tik();
return [
{ at: 44, x: 1860, y: 1010 },
{ at: 72, x: 1340, y: 700, duration: 28 },
{ at: 96, x: 920, y: 540, duration: 24 },
{ at: 120, x: 520, y: 620, duration: 24 },
{ at: 156, x: 1240, y: 880, duration: 36 },
{ at: TIKLAMA_KARESI - 4, x: t.x, y: t.y, duration: 34 },
{ at: TIKLAMA_KARESI, x: t.x, y: t.y, click: true, duration: 4 },
];
};
const IMLEC_GORUNUR: [number, number] = [42, SAHNE.harita];
export const S2Harita: React.FC = () => {
const f = useCurrentFrame();
const kam = kameraAt(f);
const yol = imlecYolu();
const izler = isiIzi(yol, f, IMLEC_GORUNUR);
const r = dalgaYaricapi(f / GECIS.g1, { x: W / 2, y: H / 2 }, DALGA_BANT);
const cos = Math.cos(kam.egim);
const kuleCokus = 1 - aralik(f, MORF[0] - 10, MORF[0] + 6, ease.in);
const noktaSonus = 1 - aralik(f, MORF[1] + 2, MORF[1] + 10);
const logoBelir = aralik(f, MORF[1] - 4, MORF[1] + 6, ease.out);
const { sol, ww } = logoYerlesimi();
const kay = aralik(f, 150, 178, ease.inOut);
const logoSol = lerp(W / 2 - LOGO_BOY / 2, sol, kay);
// Noktalar
const rects: React.ReactNode[] = [];
if (noktaSonus > 0) {
noktalar.forEach((p, i) => {
const e = ease.inOut(clamp01((f - MORF[0] - p.gecikme) / 22));
const q = izdus(p.mx, p.my, 0, kam);
const d = Math.hypot(q.x - W / 2, q.y - H / 2);
const pop = ease.geriTep(clamp01((r - d - 0.36 * DALGA_BANT) / (0.42 * DALGA_BANT)));
if (pop <= 0) return;
const h = e < 1 ? isi(q.x, q.y, izler, 130) * (1 - e) : 0;
const boy0 = HUCRE * PIKSEL.nokta * q.s * pop;
const x = lerp(q.x, p.hedefX, e);
const y = lerp(q.y, p.hedefY, e);
const w = lerp(boy0, p.hedefBoy, e);
const hh = lerp(boy0 * cos, p.hedefBoy, e);
const renk = e > 0 ? karis(karis(R.primary, R.turuncu, h), p.hedefRenk, e) : karis(R.primary, R.turuncu, h);
const op = lerp(lerp(p.op, 0.95, h), 1, e) * noktaSonus;
rects.push(
<rect
key={i}
x={x - w / 2}
y={y - hh / 2}
width={w}
height={hh}
rx={Math.min(w, hh) * PIKSEL.rx}
fill={renk}
fillOpacity={op}
/>,
);
});
}
// Piksel kuleleri: il başına program sayısı kadar yükselen plakalar
const kuleRects: React.ReactNode[] = [];
const etiketler: React.ReactNode[] = [];
if (kuleCokus > 0) {
kuleler.forEach((k) => {
const buyu = aralik(f, 30 + k.i * 3, 70 + k.i * 3, ease.out) * kuleCokus;
const yuk = k.yukseklik * buyu;
const adim = HUCRE * 0.55;
const adet = Math.floor(yuk / adim);
for (let j = 0; j <= adet; j++) {
const q = izdus(k.mx, k.my, j * adim, kam);
const boy = HUCRE * PIKSEL.nokta * q.s;
const ust = j === adet;
kuleRects.push(
<rect
key={`${k.name}-${j}`}
x={q.x - boy / 2}
y={q.y - (boy * cos) / 2}
width={boy}
height={boy * cos}
rx={boy * PIKSEL.rx}
fill={R.turuncu}
fillOpacity={ust ? 0.95 : 0.3 + 0.45 * (j / Math.max(1, adet))}
/>,
);
}
if (ETIKETLI.includes(k.name)) {
const op = aralik(f, 62 + k.i * 5, 72 + k.i * 5) * (1 - aralik(f, 100, 110));
if (op > 0) {
const q = izdus(k.mx, k.my, yuk, kam);
etiketler.push(
<div
key={k.name}
style={{
position: "absolute",
left: q.x,
top: q.y - 34,
transform: `translate(-50%, -100%) translateY(${(1 - op) * 12}px)`,
opacity: op,
display: "flex",
alignItems: "center",
gap: 10,
padding: "10px 22px",
borderRadius: 999,
background: R.beyaz,
border: `2px solid ${R.slate200}`,
boxShadow: "0 2px 8px rgba(15,23,42,0.06)",
fontFamily: FONT.govde,
fontSize: 28,
fontWeight: 600,
color: R.slate800,
whiteSpace: "nowrap",
}}
>
{k.name}
<span style={{ color: R.slate400, fontWeight: 500 }}>·</span>
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, color: R.slate900 }}>{tr(k.n)}</span>
<span style={{ color: R.slate500, fontWeight: 500 }}>program</span>
</div>,
);
}
}
});
}
return (
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
<AbsoluteFill
style={{
background: `radial-gradient(ellipse 60% 55% at 50% 45%, ${R.beyaz} 20%, rgba(255,255,255,0) 75%)`,
}}
/>
<svg width={W} height={H} style={{ position: "absolute", inset: 0 }}>
<g>{rects}</g>
<g>{kuleRects}</g>
</svg>
{etiketler}
{/* Logo: morfun sonunda katı kareler, sonra sola kayar */}
{logoBelir > 0 && (
<LogoIsaret
boy={LOGO_BOY}
style={{ position: "absolute", left: logoSol, top: LOGO_MERKEZ_Y - LOGO_BOY / 2, opacity: logoBelir }}
/>
)}
{/* Kelime marka: harf aralığı genişten daralarak yerine oturur */}
<Sequence from={154}>
<Kutu x={sol + KELIME_OFSET} w={ww} y={LOGO_MERKEZ_Y - 100} h={200}>
<TrackingIn
text={
<>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</>
}
fontFamily={FONT.vurgu}
fontSize={KELIME_BOY}
fontWeight={300}
color="#0a0a0a"
startTracking={0.55}
endTracking={-0.05}
startBlur={14}
/>
</Kutu>
</Sequence>
{/* Alt yazı: harita beliren veriyi söyler */}
<Sequence from={30} durationInFrames={82}>
<Kutu y={850} h={140}>
<Cikis bas={68}>
<KineticCenterBuild
text={`${tr(TOPLAM_PROGRAM)} program, 6 yıllık veri.`}
fontFamily={FONT.baslik}
fontWeight={700}
fontSize={80}
color={R.slate900}
letterSpacing={0.015}
speed={1.25}
birim="kelime"
vurgu={{
indices: [2, 3, 4],
style: { fontFamily: FONT.vurgu, fontStyle: "italic", color: R.primary, letterSpacing: -0.05 },
}}
/>
</Cikis>
</Kutu>
</Sequence>
{/* Slogan (hero h1) */}
<Sequence from={160}>
<Kutu y={650} h={130}>
<KineticCenterBuild
text="Tercih listeni gerçek veriyle kur, panikle değil."
fontFamily={FONT.baslik}
fontWeight={700}
fontSize={66}
color={R.slate900}
letterSpacing={0.015}
speed={1.9}
birim="kelime"
vurgu={{
indices: [2, 3],
style: { fontFamily: FONT.vurgu, fontStyle: "italic", color: R.primary, letterSpacing: -0.05 },
}}
/>
</Kutu>
</Sequence>
<Imlec yol={yol} gorunur={IMLEC_GORUNUR} el={[[TIKLAMA_KARESI - 10, SAHNE.harita]]} />
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,155 @@
import { Trophy } from "lucide-react";
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
import { TrackingIn } from "@/components/remocn/tracking-in";
import { useTypewriter } from "@/lib/remocn-ui";
import { Imlec, isiIzi, type Yol } from "../bilesenler/Imlec";
import { Kutu } from "../bilesenler/Kinetik";
import { Rozet } from "../bilesenler/Logo";
import { PikselZemin } from "../bilesenler/PikselZemin";
import { Cip, CtaButon, GirisHap } from "../bilesenler/UI";
import { FONT } from "../fontlar";
import { R, W } from "../marka";
import { ORNEK_SIRA_METNI } from "../veri";
import { aralik, ease, lerp, olc } from "../yardimci";
import { GECIS, SAHNE, gecisBasi } from "../zaman";
/**
* 3 · Sıralamanı gir. Hero formunun kendisi (hero-form.tsx): puan türü
* çipleri, sıralama alanı, turuncu CTA. Sahne dalgayla açılırken form
* yatık bir düzlemden doğrulur (derinlik). İmleç alana tıklar, sıralama
* yazılır, imleç CTA'ya gider ve basar → sahne butonun içinden geçilerek
* terk edilir.
*/
const CTA_METNI = "Yapay Zeka listemi kur";
const FORM_Y = 610;
const FORM_H = 104;
const GIRIS_W = 860;
const ARA = 24;
const PUAN_TURLERI = ["Sayısal", "Eşit Ağırlık", "Sözel", "Dil", "TYT"];
const ctaGenislik = () =>
2 * 0.52 * FORM_H +
olc(CTA_METNI, { family: FONT.govde, size: FORM_H * 0.31, weight: 600 }) +
16 +
FORM_H * 0.31;
const formSol = () => W / 2 - (GIRIS_W + ARA + ctaGenislik()) / 2;
export const siralamaTiklama = () => ({
x: formSol() + GIRIS_W + ARA + ctaGenislik() / 2,
y: FORM_Y + FORM_H / 2,
});
const TIK = gecisBasi(SAHNE.siralama, GECIS.g3);
const YAZ_BAS = 34;
export const S3Siralama: React.FC = () => {
const f = useCurrentFrame();
const cta = siralamaTiklama();
const sol = formSol();
const girisX = sol + 150;
const yol: Yol = [
{ at: 2, x: 1620, y: 980 },
{ at: 28, x: girisX, y: cta.y + 6, duration: 26 },
{ at: 30, x: girisX, y: cta.y + 6, click: true, duration: 2 },
{ at: TIK - 10, x: cta.x - 40, y: cta.y + 4, duration: 30 },
{ at: TIK, x: cta.x - 40, y: cta.y + 4, click: true, duration: 10 },
];
const izler = isiIzi(yol, f);
const yazi = useTypewriter(ORNEK_SIRA_METNI, { cps: 10, startFrame: YAZ_BAS });
const odak = aralik(f, 30, 36);
const hover = aralik(f, TIK - 18, TIK - 10);
const bas = f >= TIK && f < TIK + 8 ? 1 - Math.abs(f - TIK - 4) / 4 : 0;
// Form yatık düzlemden doğrulur
const dogrul = aralik(f, 0, 30, ease.out);
return (
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
<PikselZemin tohum={23} izler={izler} />
<AbsoluteFill
style={{
background: `radial-gradient(ellipse 50% 45% at 50% 52%, ${R.slate50} 40%, rgba(248,250,252,0) 100%)`,
}}
/>
<AbsoluteFill style={{ perspective: 1800 }}>
<AbsoluteFill
style={{
transformOrigin: "50% 70%",
transform: `rotateX(${lerp(24, 0, dogrul)}deg) translateY(${lerp(60, 0, dogrul)}px) scale(${lerp(0.92, 1, dogrul)})`,
}}
>
<Kutu y={236} h={80} style={{ display: "flex", justifyContent: "center", opacity: aralik(f, 4, 14) }}>
<Rozet>Nasıl çalışır? · 1/3</Rozet>
</Kutu>
<Sequence from={6}>
<Kutu y={316} h={180}>
<TrackingIn
text="Sıralamanı gir."
fontFamily={FONT.baslik}
fontSize={124}
color={R.slate900}
startTracking={0.6}
endTracking={0.015}
startBlur={14}
/>
</Kutu>
</Sequence>
{/* Puan türü çipleri: Sayısal seçili */}
<Kutu y={512} h={60} style={{ display: "flex", justifyContent: "center", gap: 12 }}>
{PUAN_TURLERI.map((t, i) => (
<div key={t} style={{ opacity: aralik(f, 12 + i * 2, 20 + i * 2), translate: `0 ${(1 - aralik(f, 12 + i * 2, 22 + i * 2, ease.out)) * 16}px` }}>
<Cip boy={60} secili={i === 0 ? 1 : 0}>{t}</Cip>
</div>
))}
</Kutu>
<div
style={{
position: "absolute",
left: sol,
top: FORM_Y,
display: "flex",
gap: ARA,
opacity: aralik(f, 14, 24),
}}
>
<GirisHap
genislik={GIRIS_W}
deger={yazi.text}
yaziyor={yazi.typing}
placeholder="YKS başarı sıralaman (ör. 85.000)"
odak={odak}
ikon={<Trophy size={30} color={R.slate400} strokeWidth={2} aria-hidden />}
/>
<CtaButon hover={hover} bas={bas}>
{CTA_METNI}
</CtaButon>
</div>
<Kutu y={FORM_Y + FORM_H + 40} h={60} style={{ display: "flex", justifyContent: "center", opacity: aralik(f, 22, 32) }}>
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 10,
padding: "12px 24px",
borderRadius: 16,
border: `2px solid ${R.slate200}`,
background: R.beyaz,
fontFamily: FONT.govde,
fontSize: 24,
color: R.slate700,
}}
>
<b style={{ color: R.slate900, fontWeight: 600 }}>Veri: YÖK Atlas</b> · 2026 yerleştirme sonuçları
</span>
</Kutu>
</AbsoluteFill>
</AbsoluteFill>
<Imlec yol={yol} el={[[24, 34], [TIK - 16, SAHNE.siralama]]} />
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,707 @@
import { Building2, Landmark, Scale } from "lucide-react";
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
import { FocusBlurResolve } from "@/components/remocn/focus-blur-resolve";
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
import { TrackingIn } from "@/components/remocn/tracking-in";
import { Imlec, type Yol } from "../bilesenler/Imlec";
import { Cikis, Kutu } from "../bilesenler/Kinetik";
import { Rozet } from "../bilesenler/Logo";
import { Cip, DILIM_RENK, DilimEtiketi, PikselTrend } from "../bilesenler/UI";
import { FONT } from "../fontlar";
import { PIKSEL, R, W, opaklikFor } from "../marka";
import { TURKIYE_HUCRELER } from "../turkiye";
import { DILIM_ETIKET, HUNI, ISKELET, LISTE, SORULAN, type Dilim } from "../veri";
import { aralik, clamp01, ease, hash, karis, lerp, olc, tr } from "../yardimci";
import { GECIS, SAHNE, gecisBasi } from "../zaman";
/**
* 4 · Havuz → 24'lük liste.
*
* A) Sihirbaz (alan → il → devlet/vakıf). Sağda aday havuzu: sayı her
* seçimde küçülür, altındaki piksel alanından o kadar piksel düşer.
* Son adımda kalan her piksel **bir gerçek programdır** (179).
* B) Kart derinliğe çekilir; 179 piksel havuz sınırının içine toplanır.
* Dışarıdaki gri pikseller sınıra çarpıp geri döner (yapay zekâ havuzun
* dışına çıkamaz). 24 piksel seçilir, dilim renklerini alır ve soldaki
* 24'lük şeride uçar; sağda liste yatık bir düzlemden doğrulur. İmleç
* bir satıra tıklar → sahne o satırın içinden terk edilir.
*/
const TIK = gecisBasi(SAHNE.havuz, GECIS.g4);
// ——— Sihirbaz kartı yerleşimi ———
const KART = { x: 150, y: 170, w: 860, h: 740, ic: 64 };
const ICERIK_X = KART.x + KART.ic;
const ICERIK_W = KART.w - KART.ic * 2;
const CIP_BOY = 72;
const ALANLAR = [
"Mühendislik & Teknoloji",
"Sağlık & Tıp",
"Fen & Temel Bilimler",
"Mimarlık & Tasarım",
"İktisat & İşletme",
"Eğitim & Öğretmenlik",
"Hukuk & Siyasal",
"Tarım & Doğa",
];
const ILLER = ["İstanbul", "Ankara", "İzmir", "Bursa", "Antalya", "Eskişehir"];
const IL_HUCRE: Record<string, [number, number]> = {
İstanbul: [8, 4],
Ankara: [23, 9],
İzmir: [4, 16],
};
/** Çipleri kart genişliğinde satırlara dizer (gerçek yazı genişliğiyle) */
const cipDiz = (metinler: string[], x0: number, y0: number, ikonlu = false) => {
const ara = 16;
let x = x0;
let y = y0;
return metinler.map((m) => {
const w =
CIP_BOY * 0.7 +
olc(m, { family: FONT.govde, size: CIP_BOY * 0.35, weight: 600 }) +
(ikonlu ? CIP_BOY * 0.35 + CIP_BOY * 0.15 : 0);
if (x + w > x0 + ICERIK_W) {
x = x0;
y += CIP_BOY + ara;
}
const k = { metin: m, x, y, w, cx: x + w / 2, cy: y + CIP_BOY / 2 };
x += w + ara;
return k;
});
};
// Adım pencereleri: [giriş, çıkış]
const ADIM: [number, number][] = [
[-10, 88],
[86, 154],
[152, 214],
];
// Tıklama kareleri
const T_ALAN = 46;
const T_IL = [106, 120, 134];
const T_TUR = 174;
// ——— Havuz sayacı ve piksel alanı ———
const SAYAC: { a: number; b: number; n: number }[] = [
{ a: 8, b: 34, n: HUNI[1].n },
{ a: 48, b: 70, n: HUNI[2].n },
{ a: 136, b: 158, n: HUNI[3].n },
{ a: 176, b: 198, n: HUNI[4].n },
];
const sayacAt = (f: number) => {
let n: number = HUNI[0].n;
let adim = 0;
for (const [i, s] of SAYAC.entries()) {
if (f >= s.a) {
n = lerp(n, s.n, aralik(f, s.a, s.b, ease.out));
adim = i + 1;
}
}
return { n, adim };
};
const ALAN = { x: 1100, y: 560, sutun: 30, satir: 14, hucre: 22 };
const HAVUZ = HUNI[4].n; // 179
const EN_COK = ALAN.sutun * ALAN.satir; // 420
const yanan = (n: number) =>
HAVUZ + (EN_COK - HAVUZ) * (Math.log10(n / HAVUZ) / Math.log10(HUNI[0].n / HAVUZ));
// Kümelenme (B): havuz sınırı içinde 17×11
const KUME = { sutun: 17, hucre: 34, cx: 960, cy: 590 };
const KUME_SATIR = Math.ceil(HAVUZ / KUME.sutun);
const KUME_X0 = KUME.cx - (KUME.sutun * KUME.hucre) / 2;
const KUME_Y0 = KUME.cy - (KUME_SATIR * KUME.hucre) / 2;
const SINIR = {
x: KUME_X0 - 30,
y: KUME_Y0 - 30,
w: KUME.sutun * KUME.hucre + 60,
h: KUME_SATIR * KUME.hucre + 60,
};
// Şerit (B4): seçilen 24 piksel
const SERIT = { x: 140, y: 700, hucre: 30 };
const pikseller = (() => {
const hucreler = Array.from({ length: EN_COK }, (_, i) => ({
i,
gx: i % ALAN.sutun,
gy: Math.floor(i / ALAN.sutun),
h: hash(i % ALAN.sutun, Math.floor(i / ALAN.sutun), 41),
}));
const sirali = [...hucreler].sort((a, b) => a.h - b.h);
const rutbe = new Map(sirali.map((c, r) => [c.i, r]));
const havuzdakiler = hucreler.filter((c) => (rutbe.get(c.i) ?? 0) < HAVUZ); // alan sırasına göre
const havuzSira = new Map(havuzdakiler.map((c, p) => [c.i, p]));
// 24 seçim: havuzdan dağınık
const secimSirasi = [...havuzdakiler].sort((a, b) => hash(a.i, 3, 77) - hash(b.i, 3, 77)).slice(0, 24);
const dilimler: Dilim[] = ISKELET.flatMap((d) => Array<Dilim>(d.adet).fill(d.dilim));
const secim = new Map(secimSirasi.map((c, k) => [c.i, { k, dilim: dilimler[k] }]));
return hucreler.map((c) => ({
...c,
rutbe: rutbe.get(c.i) ?? 0,
p: havuzSira.get(c.i),
secim: secim.get(c.i),
}));
})();
// Havuz dışındaki "hayalet" programlar: sınıra çarpıp geri döner
const HAYALETLER = [
{ bx: SINIR.x, by: SINIR.y + 90, nx: -1, ny: 0 },
{ bx: SINIR.x, by: SINIR.y + 300, nx: -1, ny: 0 },
{ bx: SINIR.x + SINIR.w, by: SINIR.y + 150, nx: 1, ny: 0 },
{ bx: SINIR.x + SINIR.w, by: SINIR.y + 340, nx: 1, ny: 0 },
{ bx: SINIR.x + 180, by: SINIR.y + SINIR.h, nx: 0, ny: 1 },
{ bx: SINIR.x + 420, by: SINIR.y, nx: 0, ny: -1 },
];
// ——— Liste (B4) ———
const LISTE_X = 950;
const LISTE_W = 860;
const SATIR_H = 84;
const BASLIK_H = 58;
const DAHA_H = 42;
const BOLUMLER = ISKELET.map((d) => ({
...d,
satirlar: LISTE.map((s, i) => ({ s, i })).filter(({ s }) => s.dilim === d.dilim),
}));
const listeUst = (f: number) => lerp(150, 12, aralik(f, 330, 392, ease.inOut));
// Tıklanan satırın liste içindeki konumu
const HEDEF_SATIR = LISTE.indexOf(SORULAN);
const hedefOfset = (() => {
let y = 0;
for (const b of BOLUMLER) {
y += BASLIK_H;
for (const { i } of b.satirlar) {
if (i === HEDEF_SATIR) return y;
y += SATIR_H;
}
y += DAHA_H;
}
return y;
})();
export const havuzTiklama = () => ({
x: LISTE_X + LISTE_W * 0.42,
y: listeUst(TIK) + hedefOfset + SATIR_H / 2,
});
export const S4Havuz: React.FC = () => {
const f = useCurrentFrame();
const alanCip = cipDiz(ALANLAR, ICERIK_X, 430);
const ilCip = cipDiz(ILLER, ICERIK_X, 744);
const turCip = cipDiz(["Devlet", "Vakıf", "Fark etmez"], ICERIK_X, 494, true);
const hedef = havuzTiklama();
const yol: Yol = [
{ at: 16, x: 1480, y: 1010 },
{ at: T_ALAN, x: alanCip[0].cx, y: alanCip[0].cy, duration: 26, click: true },
{ at: T_IL[0], x: ilCip[0].cx, y: ilCip[0].cy, duration: 30, click: true },
{ at: T_IL[1], x: ilCip[1].cx, y: ilCip[1].cy, duration: 12, click: true },
{ at: T_IL[2], x: ilCip[2].cx, y: ilCip[2].cy, duration: 12, click: true },
{ at: T_TUR, x: turCip[0].cx, y: turCip[0].cy, duration: 28, click: true },
{ at: 206, x: 1180, y: 1060, duration: 26 },
{ at: 350, x: 1880, y: 1040, duration: 4 },
{ at: 380, x: 1560, y: 760, duration: 28 },
{ at: TIK - 2, x: hedef.x, y: hedef.y, duration: 16 },
{ at: TIK, x: hedef.x, y: hedef.y, click: true, duration: 2 },
];
const imlecGorunur = f < 230 ? ([12, 228] as [number, number]) : ([352, SAHNE.havuz] as [number, number]);
const { n, adim } = sayacAt(f);
const yananF = yanan(n);
// ——— Faz geçişleri ———
const kartCikis = aralik(f, 198, 232, ease.in);
const panelCikis = aralik(f, 198, 214);
const kume = (p: number) => ease.inOut(clamp01((f - 204 - p * 0.1) / 26));
const secimBas = 262;
const seritBas = 300;
const sinirOp = aralik(f, 222, 238) * (1 - aralik(f, 300, 316));
// ——— Pikseller ———
const rects: React.ReactNode[] = [];
for (const c of pikseller) {
let x = ALAN.x + c.gx * ALAN.hucre + ALAN.hucre / 2;
let y = ALAN.y + c.gy * ALAN.hucre + ALAN.hucre / 2;
let boy = ALAN.hucre * PIKSEL.nokta;
let renk: string = R.primary;
let op = 0.25 + opaklikFor(c.gx, c.gy) * 1.9;
// Belirme (kart ile birlikte)
const belir = aralik(f, 4 + (c.gx + c.gy) * 0.35, 14 + (c.gx + c.gy) * 0.35, ease.out);
op *= belir;
// Havuzdan düşenler: sayaç indikçe sırası gelen piksel griye döner ve düşer
if (c.rutbe >= yananF) {
const dus = clamp01((c.rutbe + 1 - yananF) / 20);
if (dus >= 1) continue;
y += dus * dus * 150;
renk = karis(R.primary, R.slate300, clamp01(dus * 3));
op *= 1 - dus;
}
if (c.p !== undefined) {
// B: kümeye toplanma
const e = kume(c.p);
const kx = KUME_X0 + (c.p % KUME.sutun) * KUME.hucre + KUME.hucre / 2;
const ky = KUME_Y0 + Math.floor(c.p / KUME.sutun) * KUME.hucre + KUME.hucre / 2;
x = lerp(x, kx, e);
y = lerp(y, ky, e);
boy = lerp(boy, KUME.hucre * PIKSEL.nokta, e);
op = lerp(op, 0.55 + opaklikFor(c.gx, c.gy), e);
if (c.secim) {
const s = aralik(f, secimBas + c.secim.k * 1.3, secimBas + 8 + c.secim.k * 1.3, ease.out);
renk = karis(renk, DILIM_RENK[c.secim.dilim].nokta, s);
op = lerp(op, 1, s);
// seçilirken kısa bir halka
const halka = Math.sin(Math.PI * s);
if (halka > 0.02) {
const hb = boy * (1 + 0.7 * halka);
rects.push(
<rect key={`h${c.i}`} x={x - hb / 2} y={y - hb / 2} width={hb} height={hb} rx={hb * PIKSEL.rx} fill="none" stroke={DILIM_RENK[c.secim.dilim].nokta} strokeWidth={3} strokeOpacity={halka * 0.8} />,
);
}
// şeride uçuş
const u = ease.inOut(clamp01((f - seritBas - c.secim.k * 0.9) / 26));
x = lerp(x, SERIT.x + c.secim.k * SERIT.hucre + SERIT.hucre / 2, u);
y = lerp(y, SERIT.y + SERIT.hucre / 2, u);
boy = lerp(boy, SERIT.hucre * PIKSEL.nokta, u);
} else {
op *= 1 - 0.72 * aralik(f, secimBas, secimBas + 26);
op *= 1 - aralik(f, seritBas, seritBas + 14);
if (op <= 0.01) continue;
}
}
rects.push(
<rect key={c.i} x={x - boy / 2} y={y - boy / 2} width={boy} height={boy} rx={boy * PIKSEL.rx} fill={renk} fillOpacity={op} />,
);
}
// Hayaletler
const hayaletler = HAYALETLER.map((g, i) => {
const u = (f - (240 + i * 5)) / 26;
if (u < 0 || f > 316) return null;
const bosluk =
u < 1
? lerp(170, 10, ease.in(u))
: 10 + 46 * Math.abs(Math.sin((u - 1) * Math.PI * 1.2)) * Math.exp(-(u - 1) * 2.4);
const boy = KUME.hucre * PIKSEL.nokta;
const x = g.bx + g.nx * (bosluk + boy / 2);
const y = g.by + g.ny * (bosluk + boy / 2);
const op = aralik(u, 0, 0.3) * (1 - aralik(f, 300, 316));
return (
<rect key={`g${i}`} x={x - boy / 2} y={y - boy / 2} width={boy} height={boy} rx={boy * PIKSEL.rx} fill={R.slate100} stroke={R.slate300} strokeWidth={2.5} opacity={op} />
);
});
// Çarpma anında sınır kısa süre turuncuya döner
const carpma = Math.max(
...HAYALETLER.map((_, i) => {
const u = (f - (240 + i * 5)) / 26 - 1;
return u >= 0 && u < 0.4 ? 1 - u / 0.4 : 0;
}),
);
// Kartın adımları
const adimStil = (k: number): React.CSSProperties => {
const [a, b] = ADIM[k];
const g = aralik(f, a, a + 10, ease.out);
const c = aralik(f, b - 8, b, ease.in);
return {
position: "absolute",
inset: 0,
opacity: g * (1 - c),
translate: `${(1 - g) * 90 - c * 90}px 0`,
filter: `blur(${(1 - g) * 6 + c * 6}px)`,
};
};
const secildi = (t: number) => aralik(f, t, t + 5);
const uzerinde = (t: number) => aralik(f, t - 7, t - 2) * (1 - aralik(f, t, t + 3));
const aktifAdim = f < ADIM[1][0] + 4 ? 0 : f < ADIM[2][0] + 4 ? 1 : 2;
const basliklar = ["Hangi alanlara ilgi duyuyorsun?", "Nerede okumak istersin?", "Üniversite tipi & öncelikler"];
const aciklamalar = [
"Sıralamanla ulaşabileceğin alanlar. En az bir tane seç.",
"En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.",
"Bunlar listenin dengesini belirler.",
];
return (
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
{/* ——— A: sihirbaz kartı (derinliğe çekilerek çıkar) ——— */}
<AbsoluteFill style={{ perspective: 1600 }}>
<div
style={{
position: "absolute",
left: KART.x,
top: KART.y,
width: KART.w,
height: KART.h,
borderRadius: 48,
background: R.beyaz,
border: `2px solid ${R.slate200}`,
boxShadow: "0 24px 60px -30px rgba(15,23,42,0.25)",
transformOrigin: "0% 50%",
transform: `translateZ(${-700 * kartCikis}px) rotateY(${24 * kartCikis}deg) translateX(${-120 * kartCikis}px)`,
opacity: aralik(f, 0, 8) * (1 - aralik(f, 214, 232)),
filter: `blur(${kartCikis * 10}px)`,
overflow: "hidden",
}}
>
<div style={{ position: "absolute", left: 0, top: 0, width: KART.w, height: KART.h, translate: `${-KART.x}px ${-KART.y}px` }}>
{/* İlerleme */}
<div style={{ position: "absolute", left: ICERIK_X, top: KART.y + 58, display: "flex", gap: 10 }}>
{[0, 1, 2].map((k) => (
<div key={k} style={{ width: 72, height: 10, borderRadius: 99, background: k <= aktifAdim ? R.primary : R.slate200 }} />
))}
<span style={{ marginLeft: 16, marginTop: -9, fontFamily: FONT.govde, fontSize: 24, fontWeight: 600, color: R.slate500 }}>
{aktifAdim + 1} / 3
</span>
</div>
{[0, 1, 2].map((k) => (
<div key={k} style={adimStil(k)}>
<div style={{ position: "absolute", left: ICERIK_X, top: KART.y + 110, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 46, color: R.slate900, whiteSpace: "nowrap" }}>
{basliklar[k]}
</div>
<div style={{ position: "absolute", left: ICERIK_X, top: KART.y + 180, fontFamily: FONT.govde, fontSize: 26, color: R.slate500, whiteSpace: "nowrap" }}>
{aciklamalar[k]}
</div>
{k === 0 &&
alanCip.map((c, i) => (
<Cip key={c.metin} boy={CIP_BOY} secili={i === 0 ? secildi(T_ALAN) : 0} hover={i === 0 ? uzerinde(T_ALAN) : 0} style={{ position: "absolute", left: c.x, top: c.y }}>
{c.metin}
</Cip>
))}
{k === 1 && (
<>
<MiniHarita x={ICERIK_X} y={KART.y + 262} f={f} />
{ilCip.map((c, i) => (
<Cip key={c.metin} boy={CIP_BOY} secili={i < 3 ? secildi(T_IL[i]) : 0} hover={i < 3 ? uzerinde(T_IL[i]) : 0} style={{ position: "absolute", left: c.x, top: c.y }}>
{c.metin}
</Cip>
))}
</>
)}
{k === 2 && (
<>
<div style={{ position: "absolute", left: ICERIK_X, top: 440, fontFamily: FONT.govde, fontSize: 26, fontWeight: 600, color: R.slate700 }}>
Üniversite tipi
</div>
{turCip.map((c, i) => {
const Ikon = [Landmark, Building2, Scale][i];
const sec = i === 0 ? secildi(T_TUR) : 0;
return (
<Cip
key={c.metin}
boy={CIP_BOY}
secili={sec}
hover={i === 0 ? uzerinde(T_TUR) : 0}
ikon={<Ikon size={CIP_BOY * 0.35} color={karis(R.primary, R.beyaz, sec)} strokeWidth={2.2} aria-hidden />}
style={{ position: "absolute", left: c.x, top: c.y }}
>
{c.metin}
</Cip>
);
})}
<div style={{ position: "absolute", left: ICERIK_X, top: 620, fontFamily: FONT.govde, fontSize: 26, fontWeight: 600, color: R.slate700 }}>
Öncelikler
</div>
{cipDiz(["Burslu", "Öğretim dili", "Evime yakın"], ICERIK_X, 672).map((c) => (
<Cip key={c.metin} boy={CIP_BOY} style={{ position: "absolute", left: c.x, top: c.y }}>
{c.metin}
</Cip>
))}
</>
)}
</div>
))}
</div>
</div>
</AbsoluteFill>
{/* ——— A: aday havuzu paneli ——— */}
<div style={{ opacity: aralik(f, 6, 16) * (1 - panelCikis) }}>
<Kutu x={ALAN.x} w={660} y={186} h={50}>
<Rozet>Nasıl çalışır? · 2/3</Rozet>
</Kutu>
<div style={{ position: "absolute", left: ALAN.x, top: 262, fontFamily: FONT.govde, fontSize: 30, fontWeight: 600, color: R.slate500 }}>
Aday havuzu
</div>
<div style={{ position: "absolute", left: ALAN.x - 6, top: 300, display: "flex", alignItems: "baseline", gap: 18 }}>
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 176, lineHeight: 1, letterSpacing: "-0.01em", color: R.slate900 }}>
{tr(n)}
</span>
<span style={{ fontFamily: FONT.govde, fontSize: 38, fontWeight: 600, color: R.slate400 }}>program</span>
</div>
{HUNI.map((h, i) => {
const op = i === adim ? aralik(f, i === 0 ? 0 : SAYAC[i - 1].a, (i === 0 ? 0 : SAYAC[i - 1].a) + 8) : 0;
return (
<div key={i} style={{ position: "absolute", left: ALAN.x, top: 492, opacity: op, translate: `0 ${(1 - op) * 10}px`, fontFamily: FONT.govde, fontSize: 28, fontWeight: 600, color: i === 4 ? R.primary : R.slate600, whiteSpace: "nowrap" }}>
{h.etiket}
</div>
);
})}
</div>
{/* ——— B: havuz sınırı ——— */}
{sinirOp > 0 && (
<>
<div
style={{
position: "absolute",
left: SINIR.x,
top: SINIR.y,
width: SINIR.w,
height: SINIR.h,
borderRadius: 40,
border: `3px dashed ${karis(R.slate300, R.turuncu, carpma)}`,
background: "rgba(255,255,255,0.7)",
opacity: sinirOp,
}}
/>
<div style={{ position: "absolute", left: SINIR.x + 8, top: SINIR.y - 52, opacity: sinirOp, fontFamily: FONT.govde, fontSize: 28, fontWeight: 600, color: R.slate600, whiteSpace: "nowrap" }}>
Aday havuzu · <span style={{ fontFamily: FONT.baslik, fontWeight: 700, color: R.slate900 }}>179</span> gerçek program
</div>
</>
)}
<svg width={W} height={1080} style={{ position: "absolute", inset: 0 }}>
{hayaletler}
{rects}
</svg>
<Sequence from={230} durationInFrames={70}>
<Kutu y={130} h={140}>
<Cikis bas={60}>
<KineticCenterBuild
text="Yapay zekâ yalnızca bu havuzdan seçer."
birim="kelime"
fontFamily={FONT.baslik}
fontWeight={700}
fontSize={72}
color={R.slate900}
letterSpacing={0.015}
speed={1.5}
vurgu={{ indices: [2], style: { fontFamily: FONT.vurgu, fontStyle: "italic", color: R.primary, letterSpacing: -0.05 } }}
/>
</Cikis>
</Kutu>
</Sequence>
<Sequence from={262} durationInFrames={48}>
<Kutu y={880} h={80}>
<FocusBlurResolve text="Havuzda olmayan bir bölümü listene yazamaz." fontFamily={FONT.govde} fontSize={36} fontWeight={600} color={R.slate500} />
</Kutu>
</Sequence>
{/* ——— B4: sol başlık + 24'lük şerit + dilimler ——— */}
{f >= 316 && <ListeBasligi f={f} />}
{/* ——— B4: liste ——— */}
{f >= 318 && <Liste f={f} hover={aralik(f, TIK - 10, TIK - 4)} />}
<Imlec yol={yol} gorunur={imlecGorunur} el={[[T_ALAN - 8, T_TUR + 4], [TIK - 14, SAHNE.havuz]]} />
</AbsoluteFill>
);
};
/** İl adımındaki dot-matrix harita; seçilen iller turuncu yanar */
const MiniHarita: React.FC<{ x: number; y: number; f: number }> = ({ x, y, f }) => {
const hucre = 10.6;
const boy = hucre * PIKSEL.nokta;
const secim = Object.entries(IL_HUCRE).map(([ad, [cx, cy]], i) => ({ ad, cx, cy, s: aralik(f, T_IL[i], T_IL[i] + 8, ease.geriTep) }));
return (
<svg width={64 * hucre} height={26 * hucre} style={{ position: "absolute", left: x, top: y, overflow: "visible" }}>
{TURKIYE_HUCRELER.map(({ x: gx, y: gy }) => {
let isi = 0;
for (const s of secim) {
const d = Math.hypot(gx - s.cx, gy - s.cy);
if (d <= 1.5) isi = Math.max(isi, s.s * (d < 0.5 ? 1 : 0.75));
}
const b = boy * (1 + 0.25 * isi);
return (
<rect
key={`${gx}-${gy}`}
x={gx * hucre + (hucre - b) / 2}
y={gy * hucre + (hucre - b) / 2}
width={b}
height={b}
rx={b * PIKSEL.rx}
fill={karis(R.primary, R.turuncu, clamp01(isi))}
fillOpacity={lerp(0.2 + opaklikFor(gx, gy) * 1.5, 0.95, clamp01(isi))}
/>
);
})}
</svg>
);
};
const SolaYasli: React.FC<{ x: number; y: number; metin: React.ReactNode; genislik: number; bas: number; boy: number; family: string; renk?: string }> = ({ x, y, metin, genislik, bas, boy, family, renk = R.slate900 }) => (
<Sequence from={bas} layout="none">
<Kutu x={x - 40} y={y} w={genislik + 80} h={boy * 1.2}>
<TrackingIn text={metin} fontFamily={family} fontSize={boy} color={renk} startTracking={0.5} endTracking={0.015} startBlur={12} />
</Kutu>
</Sequence>
);
const ListeBasligi: React.FC<{ f: number }> = ({ f }) => {
const boy = 118;
const w1 = olc("Dengeli", { family: FONT.baslik, size: boy, weight: 700, tracking: 0.015 });
const w2 = olc("24'lük", { family: FONT.vurgu, size: boy, weight: 700, italic: true, tracking: -0.05 });
const w3 = olc("listen hazır.", { family: FONT.baslik, size: boy, weight: 700, tracking: 0.015 });
return (
<>
<Kutu x={140} w={600} y={176} h={60} style={{ opacity: aralik(f, 318, 328) }}>
<Rozet>Nasıl çalışır? · 3/3</Rozet>
</Kutu>
<SolaYasli x={140} y={250} metin="Dengeli" genislik={w1} bas={320} boy={boy} family={FONT.baslik} />
<SolaYasli
x={140}
y={380}
genislik={w2}
bas={328}
boy={boy}
family={FONT.vurgu}
metin={<span style={{ fontStyle: "italic", color: R.primary, letterSpacing: "-0.05em" }}>24&apos;lük</span>}
/>
<SolaYasli x={140} y={510} metin="listen hazır." genislik={w3} bas={336} boy={boy} family={FONT.baslik} />
<div style={{ position: "absolute", left: SERIT.x, top: 760, display: "flex", gap: 30, opacity: aralik(f, 336, 350), translate: `0 ${(1 - aralik(f, 336, 350, ease.out)) * 12}px` }}>
{ISKELET.map((d) => (
<span key={d.dilim} style={{ display: "inline-flex", alignItems: "baseline", gap: 10 }}>
<DilimEtiketi dilim={d.dilim} boy={28} />
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 30, color: R.slate900 }}>{d.adet}</span>
</span>
))}
</div>
<div style={{ position: "absolute", left: SERIT.x, top: 830, width: 720, opacity: aralik(f, 344, 358), fontFamily: FONT.govde, fontSize: 22, lineHeight: 1.45, color: R.slate400 }}>
Örnek: SAY 45.000 · Mühendislik & Teknoloji · İstanbul, Ankara, İzmir · Devlet.
Satırlar 2026 YÖK Atlas kayıtlarıdır.
</div>
</>
);
};
const Liste: React.FC<{ f: number; hover: number }> = ({ f, hover }) => {
const duz = aralik(f, 330, 390, ease.inOut);
const ust = listeUst(f);
let y = 0;
let sira = 0;
const ogeler: React.ReactNode[] = [];
BOLUMLER.forEach((b, bi) => {
const by = y;
ogeler.push(
<div key={`b${bi}`} style={{ position: "absolute", left: 0, top: by, height: BASLIK_H, display: "flex", alignItems: "center", gap: 14, ...giris(f, 322 + bi * 10) }}>
<DilimEtiketi dilim={b.dilim} boy={28} />
<span style={{ fontFamily: FONT.govde, fontSize: 24, color: R.slate400 }}>· {b.adet} tercih</span>
</div>,
);
y += BASLIK_H;
b.satirlar.forEach(({ s, i }) => {
sira++;
const hedef = i === HEDEF_SATIR;
const sy = y;
ogeler.push(
<div
key={`s${i}`}
style={{
position: "absolute",
left: 0,
top: sy,
width: LISTE_W,
height: SATIR_H,
display: "flex",
alignItems: "center",
gap: 20,
borderBottom: `2px solid ${R.slate100}`,
borderRadius: hedef ? 20 : 0,
background: hedef ? `color-mix(in oklab, ${R.turuncu50} ${hover * 100}%, rgba(255,255,255,0))` : "transparent",
padding: "0 12px",
boxSizing: "border-box",
...giris(f, 324 + sira * 2.5 + bi * 4),
}}
>
<span style={{ width: 44, height: 44, flexShrink: 0, borderRadius: 99, background: R.slate100, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: FONT.govde, fontWeight: 600, fontSize: 22, color: R.slate600 }}>
{LISTE_SIRASI[i]}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontFamily: FONT.govde,
fontWeight: 600,
fontSize: 28,
lineHeight: 1.2,
color: hedef ? karis(R.slate900, R.primary, hover) : R.slate900,
textDecoration: hedef && hover > 0.5 ? "underline" : "none",
textUnderlineOffset: 5,
whiteSpace: "nowrap",
}}
>
{s.program}
</div>
<div style={{ fontFamily: FONT.govde, fontSize: 22, color: R.slate500, whiteSpace: "nowrap", marginTop: 4 }}>
{s.universite} · {s.il}
</div>
</div>
<PikselTrend sira={s.sira} hucre={14} yukseklik={30} ilerleme={aralik(f, 340 + sira * 2, 356 + sira * 2)} />
<div style={{ width: 118, textAlign: "right", flexShrink: 0 }}>
<div style={{ fontFamily: FONT.govde, fontWeight: 600, fontSize: 28, color: R.slate900, fontVariantNumeric: "tabular-nums" }}>{tr(s.sira[5])}</div>
<div style={{ fontFamily: FONT.govde, fontSize: 18, color: R.slate400 }}>taban 2026</div>
</div>
</div>,
);
y += SATIR_H;
});
const kalan = b.adet - b.satirlar.length;
ogeler.push(
<div key={`d${bi}`} style={{ position: "absolute", left: 76, top: y + 8, fontFamily: FONT.govde, fontSize: 22, color: R.slate400, ...giris(f, 334 + bi * 12) }}>
+{kalan} {DILIM_ETIKET[b.dilim].toLocaleLowerCase("tr-TR")} tercih daha
</div>,
);
y += DAHA_H;
});
return (
<AbsoluteFill
style={{
perspective: 2200,
maskImage: "linear-gradient(to bottom, transparent 0, black 60px, black 980px, transparent 1080px)",
WebkitMaskImage: "linear-gradient(to bottom, transparent 0, black 60px, black 980px, transparent 1080px)",
}}
>
<div
style={{
position: "absolute",
left: LISTE_X,
top: ust,
width: LISTE_W,
height: y,
transformOrigin: "30% 40%",
transformStyle: "preserve-3d",
transform: `rotateY(${lerp(-22, 0, duz)}deg) rotateX(${lerp(14, 0, duz)}deg) translateZ(${lerp(-160, 0, duz)}px)`,
}}
>
{ogeler}
</div>
</AbsoluteFill>
);
};
// Listede her satırın 24'lük listedeki sırası (hayal 1–5, dengeli 6–18, güvenli 19–24)
const LISTE_SIRASI: Record<number, number> = (() => {
const out: Record<number, number> = {};
let bas = 1;
for (const b of BOLUMLER) {
b.satirlar.forEach(({ i }, k) => (out[i] = bas + k));
bas += b.adet;
}
return out;
})();
/** Satır girişi: derinlikten öne, bulanıktan nete */
const giris = (f: number, bas: number): React.CSSProperties => {
const g = aralik(f, bas, bas + 14, ease.out);
return {
opacity: g,
transform: `translateZ(${(1 - g) * -260}px) translateY(${(1 - g) * 24}px)`,
filter: `blur(${(1 - g) * 8}px)`,
};
};

View File

@@ -0,0 +1,242 @@
import { ArrowUp } from "lucide-react";
import { AbsoluteFill, useCurrentFrame } from "remotion";
import { useTypewriter } from "@/lib/remocn-ui";
import { Imlec, type Yol } from "../bilesenler/Imlec";
import { LogoIsaret } from "../bilesenler/Logo";
import { PikselZemin } from "../bilesenler/PikselZemin";
import { DilimEtiketi, GirisHap, PikselTrend } from "../bilesenler/UI";
import { FONT } from "../fontlar";
import { R } from "../marka";
import { SORULAN, ORNEK_SIRA_METNI } from "../veri";
import { aralik, ease, karis, lerp, olc, tr } from "../yardimci";
import { GECIS, SAHNE, gecisBasi } from "../zaman";
/**
* 5 · Danışman. Tıklanan satır sohbetin konusu olur. Soru yazılır, gönder
* düğmesine basılır; yanıt beklenirken logonun üç karesi nabız atar
* (animasyonlu-mark.tsx "nabız"). Yanıt yalnızca gerçek taban geçmişini
* söyler ve altında 6 yılın piksel trendi belirir; son yılın turuncu pikseli
* bir sonraki dalganın çıkış noktasıdır.
*/
const PANEL = { x: 330, y: 64, w: 1260, h: 952 };
const SORU = "Bu tercih neden dengeli dilimde?";
const YANIT = `2026'da taban sırası ${tr(SORULAN.sira[5])}, seninki ${ORNEK_SIRA_METNI}. Ama son 6 yılda taban ${tr(Math.min(...SORULAN.sira))} ile ${tr(Math.max(...SORULAN.sira))} arasında gidip geldi — bu kadar dalgalı olduğu için dengeli dilimde.`;
const YAZ_BAS = 12;
const GONDER = 58;
const BEKLE: [number, number] = [64, 86];
const TREND_BAS = 150;
const GIRIS_Y = PANEL.y + PANEL.h - 132;
const GONDER_X = PANEL.x + PANEL.w - 64 - 42;
const GONDER_Y = GIRIS_Y + 52;
// Yanıt balonu ve içindeki trend (satır sayısı gerçek yazı genişliğiyle)
const BALON = { x: PANEL.x + 56, y: PANEL.y + 330, w: 960, pad: 40, ust: 30, punto: 32, satir: 48 };
const TREND_H = { hucre: 34, yukseklik: 90 };
const satirSayisi = () => {
const bosluk = olc(" ", { family: FONT.govde, size: BALON.punto });
let n = 1;
let x = 0;
for (const k of YANIT.split(" ")) {
const w = olc(k, { family: FONT.govde, size: BALON.punto });
if (x > 0 && x + bosluk + w > BALON.w - BALON.pad * 2) {
n++;
x = w;
} else x += (x > 0 ? bosluk : 0) + w;
}
return n;
};
const trendKonumu = () => ({
x: BALON.x + BALON.pad,
y: BALON.y + BALON.ust + satirSayisi() * BALON.satir + 28,
});
/** Trendin son (2026) pikseli: bir sonraki dalga buradan yayılır */
export const danismanDalgaNoktasi = () => {
const t = trendKonumu();
const son = SORULAN.sira[5];
const min = Math.min(...SORULAN.sira);
const max = Math.max(...SORULAN.sira);
return {
x: t.x + 5 * TREND_H.hucre + TREND_H.hucre / 2,
y: t.y + ((son - min) / (max - min)) * TREND_H.yukseklik + (TREND_H.hucre * 0.74) / 2,
};
};
export const S5Danisman: React.FC = () => {
const f = useCurrentFrame();
const soru = useTypewriter(SORU, { cps: 24, startFrame: YAZ_BAS });
const yanit = useTypewriter(YANIT, { cps: 70, startFrame: BEKLE[1] });
const gonderildi = f >= GONDER;
const yol: Yol = [
{ at: 8, x: 1700, y: 1050 },
{ at: GONDER - 4, x: GONDER_X, y: GONDER_Y, duration: 30 },
{ at: GONDER, x: GONDER_X, y: GONDER_Y, click: true, duration: 4 },
{ at: 100, x: 1740, y: 1010, duration: 30 },
];
const bas = f >= GONDER && f < GONDER + 8 ? 1 - Math.abs(f - GONDER - 4) / 4 : 0;
const soruBalon = aralik(f, GONDER + 2, GONDER + 14, ease.geriTep);
const bekliyor = f >= BEKLE[0] && f < BEKLE[1] + 4;
const yanitBalon = aralik(f, BEKLE[1] - 4, BEKLE[1] + 8, ease.out);
const trend = trendKonumu();
return (
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
<PikselZemin tohum={51} kenar={0.2} guc={0.8} />
<div
style={{
position: "absolute",
left: PANEL.x,
top: PANEL.y,
width: PANEL.w,
height: PANEL.h,
borderRadius: 48,
background: R.beyaz,
border: `2px solid ${R.slate200}`,
boxShadow: "0 30px 80px -40px rgba(15,23,42,0.35)",
overflow: "hidden",
}}
>
{/* Başlık */}
<div style={{ position: "absolute", left: 56, top: 40, display: "flex", alignItems: "center", gap: 22 }}>
<LogoIsaret
boy={68}
kareStil={(i) => {
const faz = ((f / 30) * 2 * Math.PI) / 1.8 - (i * 2 * Math.PI) / 3;
const nabiz = bekliyor ? 0.5 + 0.5 * Math.sin(faz) : 1;
return { olcek: lerp(0.78, 1, nabiz) };
}}
/>
<div>
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 40, color: R.slate900 }}>Danışman</div>
<div style={{ fontFamily: FONT.govde, fontSize: 24, color: R.slate500, marginTop: 4 }}>
{SORULAN.program} · {SORULAN.universite}
</div>
</div>
</div>
<div style={{ position: "absolute", right: 56, top: 58 }}>
<DilimEtiketi dilim={SORULAN.dilim} boy={28} />
</div>
<div style={{ position: "absolute", left: 0, right: 0, top: 150, height: 2, background: R.slate100 }} />
{/* Soru balonu */}
{soruBalon > 0 && (
<div
style={{
position: "absolute",
right: 56,
top: 210,
maxWidth: 820,
padding: "24px 34px",
borderRadius: 32,
background: R.primary,
color: R.beyaz,
fontFamily: FONT.govde,
fontSize: 32,
fontWeight: 500,
transformOrigin: "100% 100%",
scale: `${lerp(0.6, 1, soruBalon)}`,
opacity: Math.min(1, soruBalon * 1.4),
}}
>
{SORU}
</div>
)}
{/* Bekleme: üç kare nabız */}
{bekliyor && (
<div style={{ position: "absolute", left: 56, top: 330, padding: "26px 30px", borderRadius: 32, background: R.slate100, display: "flex", gap: 12, opacity: aralik(f, BEKLE[0], BEKLE[0] + 4) }}>
{[0, 1, 2].map((i) => {
const s = 0.55 + 0.45 * Math.sin((f / 30) * 2 * Math.PI * 1.4 - i * 1.1);
return <div key={i} style={{ width: 22, height: 22, borderRadius: 5, background: [R.mavi, R.seftali, R.turuncuLogo][i], scale: `${lerp(0.6, 1, s)}` }} />;
})}
</div>
)}
{/* Yanıt balonu */}
{yanitBalon > 0 && (
<div
style={{
position: "absolute",
left: BALON.x - PANEL.x,
top: BALON.y - PANEL.y,
width: BALON.w,
padding: `${BALON.ust}px ${BALON.pad}px 36px`,
borderRadius: 32,
background: R.slate100,
color: R.slate800,
fontFamily: FONT.govde,
fontSize: BALON.punto,
lineHeight: `${BALON.satir}px`,
opacity: yanitBalon,
translate: `0 ${(1 - yanitBalon) * 16}px`,
boxSizing: "border-box",
}}
>
{/* Yer tutucu: balon baştan tam boyda dursun, metin akarken zıplamasın */}
<div style={{ position: "relative" }}>
<span style={{ visibility: "hidden" }}>{YANIT}</span>
<span style={{ position: "absolute", inset: 0 }}>{yanit.text}</span>
</div>
<div style={{ height: TREND_H.yukseklik + 60 }} />
</div>
)}
</div>
{/* Trend: 6 yılın taban sırası piksel piksel */}
<div style={{ position: "absolute", left: trend.x, top: trend.y, opacity: aralik(f, TREND_BAS - 4, TREND_BAS + 4) }}>
<PikselTrend sira={SORULAN.sira} hucre={TREND_H.hucre} yukseklik={TREND_H.yukseklik} ilerleme={aralik(f, TREND_BAS, TREND_BAS + 14)} />
</div>
<div
style={{
position: "absolute",
left: trend.x + 6 * TREND_H.hucre + 34,
top: trend.y + 18,
opacity: aralik(f, TREND_BAS + 8, TREND_BAS + 16),
fontFamily: FONT.govde,
fontSize: 24,
lineHeight: 1.5,
color: R.slate500,
}}
>
<div>
<b style={{ color: R.slate900, fontWeight: 600 }}>Taban sırası, 2021–2026</b>
</div>
<div>Kaynak: YÖK Atlas · yapay zekâ yorumu değil, gerçek geçmiş</div>
</div>
{/* Giriş */}
<div style={{ position: "absolute", left: PANEL.x + 56, top: GIRIS_Y, display: "flex", alignItems: "center", gap: 20 }}>
<GirisHap
genislik={PANEL.w - 112 - 124}
yukseklik={104}
deger={gonderildi ? "" : soru.text}
yaziyor={soru.typing}
odak={aralik(f, 6, 10)}
placeholder="Listen hakkında bir şey sor…"
/>
<div
style={{
width: 104,
height: 104,
borderRadius: 999,
background: karis(R.slate200, R.primary, aralik(f, YAZ_BAS, YAZ_BAS + 6)),
color: R.beyaz,
display: "flex",
alignItems: "center",
justifyContent: "center",
scale: `${1 - 0.06 * bas}`,
}}
>
<ArrowUp size={40} strokeWidth={2.6} aria-hidden />
</div>
</div>
<Imlec yol={yol} gorunur={[6, 130]} el={[[GONDER - 12, GONDER + 10]]} />
</AbsoluteFill>
);
};
export const DANISMAN_DALGA_BASI = gecisBasi(SAHNE.danisman, GECIS.g5);

View File

@@ -0,0 +1,120 @@
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
import { FocusBlurResolve } from "@/components/remocn/focus-blur-resolve";
import { SlotMachineRoll } from "@/components/remocn/slot-machine-roll";
import { TrackingIn } from "@/components/remocn/tracking-in";
import { Kutu } from "../bilesenler/Kinetik";
import { Rozet } from "../bilesenler/Logo";
import { FONT } from "../fontlar";
import { R } from "../marka";
import { aralik, ease, lerp, olc } from "../yardimci";
/**
* 6 · Değer. Solda "Tahmin değil, veri." ve dürüstlük notu; sağda sitenin
* gösterge kareleri (DESIGN.md §4.11): logodaki üç kare gibi iki sütun,
* sağdaki aşağı kaymış, boylar farklı. Kareler derinlikten gelip oturur,
* sayılar slot makinesi gibi yerine döner.
*/
type Kare = { renk: string; sayi: [string, string]; etiket: string; x: number; y: number; w: number; h: number; bas: number };
const SUTUN_W = 400;
const SOL_X = 990;
const SAG_X = SOL_X + SUTUN_W + 36;
const UST = 96;
// Renk sırası: en önemli turuncu, sonra mavi, indigo, yeşil (§4.11)
export const KARELER: Kare[] = [
{ renk: R.turuncu, sayi: ["00.000+", "21.000+"], etiket: "program tarandı", x: SOL_X, y: UST, w: SUTUN_W, h: 420, bas: 8 },
{ renk: R.primary, sayi: ["0 yıl", "6 yıl"], etiket: "taban sıralaması trendi", x: SAG_X, y: UST + 130, w: SUTUN_W, h: 330, bas: 15 },
{ renk: R.indigo500, sayi: ["%000", "%100"], etiket: "resmî YÖK Atlas verisi", x: SOL_X, y: UST + 460, w: SUTUN_W, h: 300, bas: 22 },
{ renk: R.green600, sayi: ["00", "24"], etiket: "tercihlik dengeli liste", x: SAG_X, y: UST + 500, w: SUTUN_W, h: 390, bas: 29 },
];
/** Son karenin merkezi: kapanışa giden koyu dalga buradan */
export const degerDalgaNoktasi = () => {
const k = KARELER[3];
return { x: k.x + k.w / 2, y: k.y + k.h / 2 };
};
export const S6Deger: React.FC = () => {
const f = useCurrentFrame();
const boy = 132;
const w1 = olc("Tahmin değil,", { family: FONT.baslik, size: boy, weight: 700, tracking: 0.015 });
const w2 = olc("veri.", { family: FONT.vurgu, size: boy, weight: 700, italic: true, tracking: -0.05 });
const n1 = "Yerleşme garantisi vermiyoruz.";
const n2 = "Riski satır satır gösteriyoruz.";
const nw1 = olc(n1, { family: FONT.govde, size: 38, weight: 600 });
const nw2 = olc(n2, { family: FONT.govde, size: 38, weight: 600 });
return (
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
<Kutu x={140} w={700} y={272} h={60} style={{ opacity: aralik(f, 4, 14) }}>
<Rozet>Neden KolayTercih?</Rozet>
</Kutu>
<Sequence from={6} layout="none">
<Kutu x={100} w={w1 + 80} y={350} h={170}>
<TrackingIn text="Tahmin değil," fontFamily={FONT.baslik} fontSize={boy} color={R.slate900} startTracking={0.5} endTracking={0.015} />
</Kutu>
</Sequence>
<Sequence from={16} layout="none">
<Kutu x={100} w={w2 + 80} y={500} h={170}>
<TrackingIn
text={<span style={{ fontStyle: "italic", color: R.primary, letterSpacing: "-0.05em" }}>veri.</span>}
fontFamily={FONT.vurgu}
fontSize={boy}
color={R.primary}
startTracking={0.9}
endTracking={-0.05}
/>
</Kutu>
</Sequence>
<Sequence from={44} layout="none">
<Kutu x={140} w={nw1} y={700} h={60}>
<FocusBlurResolve text={n1} fontFamily={FONT.govde} fontSize={38} fontWeight={600} color={R.slate700} />
</Kutu>
</Sequence>
<Sequence from={54} layout="none">
<Kutu x={140} w={nw2} y={756} h={60}>
<FocusBlurResolve text={n2} fontFamily={FONT.govde} fontSize={38} fontWeight={600} color={R.slate500} />
</Kutu>
</Sequence>
{/* Gösterge kareleri — derinlikten gelip oturur */}
<AbsoluteFill style={{ perspective: 1800, perspectiveOrigin: "70% 50%" }}>
{KARELER.map((k, i) => {
const g = aralik(f, k.bas, k.bas + 22, ease.out);
const yuz = Math.sin((f + i * 17) / 22) * 6 * g;
return (
<div
key={i}
style={{
position: "absolute",
left: k.x,
top: k.y,
width: k.w,
height: k.h,
borderRadius: 48,
background: k.renk,
color: R.beyaz,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 14,
opacity: aralik(f, k.bas, k.bas + 8),
transform: `translateZ(${lerp(-900, 0, g)}px) rotateX(${lerp(38, 0, g)}deg) rotateY(${lerp(-24, 0, g)}deg) translateY(${yuz}px)`,
filter: `blur(${(1 - g) * 16}px)`,
boxShadow: `0 30px 60px -30px rgba(15,23,42,${0.35 * g})`,
}}
>
<Sequence from={k.bas + 10} layout="none">
<SlotMachineRoll inline from={k.sayi[0]} to={k.sayi[1]} fontFamily={FONT.baslik} fontSize={92} color={R.beyaz} stagger={3} />
</Sequence>
<span style={{ fontFamily: FONT.govde, fontSize: 30, fontWeight: 700, color: "rgba(255,255,255,0.9)" }}>{k.etiket}</span>
</div>
);
})}
</AbsoluteFill>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,205 @@
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
import { TrackingIn } from "@/components/remocn/tracking-in";
import { Vinyet } from "../bilesenler/Gren";
import { Imlec, isi, isiIzi, type Yol } from "../bilesenler/Imlec";
import { Kutu } from "../bilesenler/Kinetik";
import { LogoIsaret } from "../bilesenler/Logo";
import { CtaButon } from "../bilesenler/UI";
import { FONT } from "../fontlar";
import { dalgaYaricapi } from "../gecisler/piksel-dalga";
import { H, LOGO_KARELER, PIKSEL, R, W } from "../marka";
import { aralik, clamp01, ease, hash, karis, lerp, olc } from "../yardimci";
import { GECIS } from "../zaman";
import { degerDalgaNoktasi } from "./S6Deger";
/**
* 7 · Kapanış. Sitenin tek koyu yüzeyi olan CTA bandının dili: kenarlarda
* büyüklü küçüklü, hafif kaymış kareler (PixelMasonry). Logo kareleri
* "rota" hareketiyle yer değiştirip oturur, kelime marka harf aralığı
* daralarak gelir, slogan kinetik kurulur. İmleç CTA'ya gelirken yolundaki
* kareler turuncuya ısınır; tıklar.
*/
const LOGO_BOY = 200;
const LOGO_Y = 330;
const KELIME_BOY = 126;
const KELIME_OFSET = (46.7 / 44) * LOGO_BOY;
const CTA_Y = 690;
const CTA_H = 112;
const TIK = 138;
// PixelMasonry: 4×4 hücrelik makro hücrelere 2–4 hücrelik tek kare, kenar bandında
const MASONRY = (() => {
const hucre = 22;
const makro = hucre * 4;
const bant = Math.min(W, H) * 0.3;
const out: { x: number; y: number; boy: number; op: number; vurgu: string | null }[] = [];
for (let j = 0; j * makro < H; j++) {
for (let i = 0; i * makro < W; i++) {
const cx = i * makro + makro / 2;
const cy = j * makro + makro / 2;
const kenar = Math.min(cx, W - cx, cy, H - cy);
const yog = clamp01(1 - kenar / bant);
if (hash(i, j, 7) > yog * 0.95) continue;
const n = 2 + Math.floor(hash(i, j, 8) * 3);
const boy = n * hucre;
const bos = makro - boy;
const r = hash(i, j, 9);
out.push({
x: i * makro + hash(i, j, 10) * bos + boy / 2,
y: j * makro + hash(i, j, 11) * bos + boy / 2,
boy: boy * 0.86,
op: 0.05 + hash(i, j, 12) * 0.08,
vurgu: r > 0.93 ? R.turuncu : r > 0.87 ? R.mavi : null,
});
}
}
return out;
})();
export const S7Kapanis: React.FC = () => {
const f = useCurrentFrame();
const kok = degerDalgaNoktasi();
const r = dalgaYaricapi(f / GECIS.g6, kok, 440);
const ww =
olc("Kolay", { family: FONT.vurgu, size: KELIME_BOY, weight: 300, tracking: -0.05 }) +
olc("Tercih", { family: FONT.vurgu, size: KELIME_BOY, weight: 600, tracking: -0.05 });
const sol = W / 2 - (KELIME_OFSET + ww) / 2;
const ctaW =
2 * 0.52 * CTA_H + olc("kolaytercih.com", { family: FONT.govde, size: CTA_H * 0.31, weight: 600 }) + 16 + CTA_H * 0.31;
const cta = { x: W / 2, y: CTA_Y + CTA_H / 2 };
const yol: Yol = [
{ at: 96, x: 1860, y: 1050 },
{ at: 120, x: 1500, y: 900, duration: 24 },
{ at: TIK - 2, x: cta.x + ctaW * 0.28, y: cta.y + 8, duration: 16 },
{ at: TIK, x: cta.x + ctaW * 0.28, y: cta.y + 8, click: true, duration: 2 },
];
const izler = isiIzi(yol, f, [96, 999]);
// Logo "rota": her kare sıradakinin yerine süzülür, bir tur sonra evine döner
const rota = (i: number) => {
const konum = (k: number) => {
const q = LOGO_KARELER[(i + k) % 3];
const ev = LOGO_KARELER[i];
return { dx: q.x + q.boy / 2 - (ev.x + ev.boy / 2), dy: q.y + q.boy / 2 - (ev.y + ev.boy / 2) };
};
const adim = [aralik(f, 14 + i * 3, 30 + i * 3, ease.inOut), aralik(f, 34 + i * 3, 50 + i * 3, ease.inOut), aralik(f, 54 + i * 3, 70 + i * 3, ease.inOut)];
const a = konum(0);
const b = konum(1);
const c = konum(2);
let dx = lerp(a.dx, b.dx, adim[0]);
let dy = lerp(a.dy, b.dy, adim[0]);
dx = lerp(dx, c.dx, adim[1]);
dy = lerp(dy, c.dy, adim[1]);
dx = lerp(dx, 0, adim[2]);
dy = lerp(dy, 0, adim[2]);
const ucus = Math.max(...adim.map((t) => Math.sin(Math.PI * t)));
return { dx, dy, olcek: 1 - 0.16 * ucus, opaklik: 1 - 0.4 * ucus };
};
const kay = aralik(f, 66, 90, ease.inOut);
const logoSol = lerp(W / 2 - LOGO_BOY / 2, sol, kay);
const hover = aralik(f, TIK - 10, TIK - 4);
const bas = f >= TIK && f < TIK + 8 ? 1 - Math.abs(f - TIK - 4) / 4 : 0;
const son = aralik(f, 168, 180);
return (
<AbsoluteFill style={{ background: R.slate900, overflow: "hidden" }}>
<svg width={W} height={H} style={{ position: "absolute", inset: 0 }}>
{MASONRY.map((m, i) => {
const d = Math.hypot(m.x - kok.x, m.y - kok.y);
const pop = ease.geriTep(clamp01((r - d - 0.3 * 440) / (0.4 * 440)));
if (pop <= 0) return null;
const h = isi(m.x, m.y, izler, 150);
const boy = m.boy * pop;
const taban = m.vurgu ?? R.beyaz;
return (
<rect
key={i}
x={m.x - boy / 2}
y={m.y - boy / 2}
width={boy}
height={boy}
rx={boy * PIKSEL.rx}
fill={karis(taban, R.turuncu, h)}
fillOpacity={lerp(m.vurgu ? 0.55 : m.op, 0.9, h)}
/>
);
})}
</svg>
<AbsoluteFill style={{ background: `radial-gradient(ellipse 46% 42% at 50% 48%, ${R.slate900} 35%, rgba(15,23,42,0) 100%)` }} />
<LogoIsaret
boy={LOGO_BOY}
kareStil={rota}
style={{ position: "absolute", left: logoSol, top: LOGO_Y - LOGO_BOY / 2, opacity: aralik(f, 4, 12), scale: `${lerp(0.6, 1, aralik(f, 4, 18, ease.geriTep))}` }}
/>
<Sequence from={70} layout="none">
<Kutu x={sol + KELIME_OFSET} w={ww} y={LOGO_Y - 80} h={160}>
<TrackingIn
text={
<>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</>
}
fontFamily={FONT.vurgu}
fontSize={KELIME_BOY}
fontWeight={300}
color={R.beyaz}
startTracking={0.55}
endTracking={-0.05}
/>
</Kutu>
</Sequence>
<Sequence from={88} layout="none">
<Kutu y={500} h={130}>
<KineticCenterBuild
text="Tercih listeni gerçek veriyle kur, panikle değil."
birim="kelime"
fontFamily={FONT.baslik}
fontWeight={700}
fontSize={68}
color={R.beyaz}
letterSpacing={0.015}
speed={1.8}
vurgu={{ indices: [2, 3], style: { fontFamily: FONT.vurgu, fontStyle: "italic", color: R.mavi400, letterSpacing: -0.05 } }}
/>
</Kutu>
</Sequence>
<div
style={{
position: "absolute",
left: W / 2 - ctaW / 2,
top: CTA_Y,
opacity: aralik(f, 112, 120),
scale: `${lerp(0.7, 1, aralik(f, 112, 126, ease.geriTep))}`,
}}
>
<CtaButon yukseklik={CTA_H} hover={hover} bas={bas}>
kolaytercih.com
</CtaButon>
</div>
<div
style={{
position: "absolute",
left: 0,
right: 0,
top: 980,
textAlign: "center",
opacity: aralik(f, 124, 136) * 0.9,
fontFamily: FONT.govde,
fontSize: 22,
color: R.slate400,
}}
>
Bağımsız bir karar destek aracıdır. ÖSYM ya da YÖK ile bağlantılı değildir; yerleşme garantisi vermez.
</div>
<Imlec yol={yol} gorunur={[96, 170]} el={[[TIK - 12, 170]]} />
<Vinyet guc={0.45} />
<AbsoluteFill style={{ background: R.slate950, opacity: son }} />
</AbsoluteFill>
);
};

51
video/src/turkiye.ts Normal file
View File

@@ -0,0 +1,51 @@
/**
* Dot-matrix Türkiye silüeti — src/features/pazarlama/components/turkey-pixel-map.tsx
* ile aynı ızgara (64×26 hücre, gerçek sınır poligonlarından rasterize).
*/
export const TURKIYE_IZGARA = [
"..######..............##########................................",
"..#######...........##############..............................",
"..#######.........#################................#####........",
".########...###########################...........########......",
".######.....###############################################.....",
".###........###############################################.....",
".##.......##################################################....",
".###########################################################....",
".#############################################################..",
"..#############################################################.",
"...############################################################.",
"...##########################################################...",
"...##########################################################...",
"...###########################################################..",
"..############################################################..",
"..############################################################..",
"...###########################################################..",
"....##########################################################..",
"....##########################################################..",
".....#########################################################..",
".....###################################################..##....",
".....############################################...............",
"......##########..######################...###..................",
"...........#####....#########....#####..........................",
"............###.......#####......####...........................",
".................................###............................",];
/** Hero'daki vurgu şehirleri (sütun, satır) */
export const SEHIRLER = [
{ name: "İstanbul", x: 8, y: 4 },
{ name: "Ankara", x: 23, y: 9 },
{ name: "İzmir", x: 4, y: 16 },
{ name: "Antalya", x: 16, y: 22 },
{ name: "Kayseri", x: 32, y: 14 },
{ name: "Trabzon", x: 46, y: 5 },
{ name: "Diyarbakır", x: 47, y: 18 },
{ name: "Van", x: 58, y: 15 },];
export const IZGARA_SUTUN = TURKIYE_IZGARA[0].length;
export const IZGARA_SATIR = TURKIYE_IZGARA.length;
/** Dolu hücreler (x, y) */
export const TURKIYE_HUCRELER: { x: number; y: number }[] = TURKIYE_IZGARA.flatMap(
(satir, y) =>
Array.from(satir).flatMap((c, x) => (c === "#" ? [{ x, y }] : [])),
);

88
video/src/veri.ts Normal file
View File

@@ -0,0 +1,88 @@
/**
* Videodaki örnek öğrenci ve liste — data/yokatlas.db'den (2026 kılavuzu)
* alınmış gerçek kayıtlar, uydurma satır yok.
*
* Örnek: SAY 45.000 · Mühendislik · İstanbul, Ankara, İzmir · Devlet.
*
* Havuz sayıları sadeleştirilmiş bir sorgudan (taban sırası sıralamanın
* 0,7–3 katı arasındaki SAY programları); ürünün gerçek havuz kuralı daha
* ayrıntılıdır. Sorgu:
* son_kilavuz_yili=2026 AND tur='SAYISAL'
* AND COALESCE(sira2026,sira2025) BETWEEN 31500 AND 135000
* → 1.443 · +isim LIKE '%Mühendisliği%' → 704
* · +il IN (İSTANBUL, ANKARA, İZMİR) → 484 · +unitur='DEVLET' → 179
*/
export const ORNEK_SIRA = 45000;
export const ORNEK_SIRA_METNI = "45.000";
/** 2026 kılavuzundaki toplam program (lisans + önlisans) */
export const TOPLAM_PROGRAM = 21493;
export const HUNI = [
{ n: TOPLAM_PROGRAM, etiket: "Tüm programlar" },
{ n: 1443, etiket: "Sıralamanla açılan · Sayısal" },
{ n: 704, etiket: "Mühendislik & Teknoloji" },
{ n: 484, etiket: "İstanbul · Ankara · İzmir" },
{ n: 179, etiket: "Devlet üniversiteleri" },
] as const;
export type Dilim = "hayal" | "dengeli" | "garanti";
export interface Satir {
program: string;
universite: string;
il: string;
dilim: Dilim;
/** Taban başarı sırası 2021 → 2026 */
sira: number[];
}
// Görünen dilim adı "Güvenli"; iç anahtar "garanti" (src/lib/risk.ts DILIM_ETIKET)
export const DILIM_ETIKET: Record<Dilim, string> = {
hayal: "Hayal",
dengeli: "Dengeli",
garanti: "Güvenli",
};
export const LISTE: Satir[] = [
{ program: "Elektrik-Elektronik Mühendisliği", universite: "Dokuz Eylül Üniversitesi", il: "İzmir", dilim: "hayal", sira: [45295, 41759, 36137, 40164, 39190, 38626] },
{ program: "Elektrik-Elektronik Mühendisliği", universite: "Ankara Yıldırım Beyazıt Üniversitesi", il: "Ankara", dilim: "hayal", sira: [53586, 47520, 40031, 45027, 42086, 40221] },
{ program: "Elektrik-Elektronik Mühendisliği", universite: "İstanbul Üniversitesi-Cerrahpaşa", il: "İstanbul", dilim: "hayal", sira: [58073, 54911, 49623, 52063, 47099, 42633] },
{ program: "Endüstri Mühendisliği", universite: "İstanbul Üniversitesi-Cerrahpaşa", il: "İstanbul", dilim: "dengeli", sira: [51496, 46297, 43436, 46957, 46877, 45099] },
{ program: "Bilgisayar Mühendisliği", universite: "Dokuz Eylül Üniversitesi", il: "İzmir", dilim: "dengeli", sira: [26098, 15816, 14246, 23271, 36435, 52071] },
{ program: "Endüstri Mühendisliği", universite: "Dokuz Eylül Üniversitesi", il: "İzmir", dilim: "dengeli", sira: [58559, 52933, 49684, 54049, 54049, 53587] },
{ program: "Elektrik-Elektronik Mühendisliği", universite: "İzmir Katip Çelebi Üniversitesi", il: "İzmir", dilim: "dengeli", sira: [71760, 64426, 59156, 68341, 60857, 53699] },
{ program: "Bilgisayar Mühendisliği", universite: "Ankara Yıldırım Beyazıt Üniversitesi", il: "Ankara", dilim: "dengeli", sira: [37831, 27534, 21602, 30797, 42108, 55158] },
{ program: "Elektrik-Elektronik Mühendisliği", universite: "İstanbul Medeniyet Üniversitesi", il: "İstanbul", dilim: "garanti", sira: [76034, 74632, 70514, 74989, 67761, 59417] },
{ program: "Endüstri Mühendisliği", universite: "İstanbul Medeniyet Üniversitesi", il: "İstanbul", dilim: "garanti", sira: [79628, 73208, 67215, 70600, 68739, 65169] },
{ program: "Bilgisayar Mühendisliği", universite: "İstanbul Medeniyet Üniversitesi", il: "İstanbul", dilim: "garanti", sira: [50828, 43992, 38865, 48481, 56616, 66119] },
{ program: "Bilgisayar Mühendisliği", universite: "İzmir Katip Çelebi Üniversitesi", il: "İzmir", dilim: "garanti", sira: [55869, 41281, 36708, 47803, 56980, 71833] },
];
/** Danışman sahnesinde sorulan satır: DEÜ Bilgisayar Mühendisliği */
export const SORULAN = LISTE[4];
/** 24'lük iskelet hedefi (huni-semasi.tsx: hayal 5 · dengeli 13 · güvenli 6) */
export const ISKELET: { dilim: Dilim; adet: number }[] = [
{ dilim: "hayal", adet: 5 },
{ dilim: "dengeli", adet: 13 },
{ dilim: "garanti", adet: 6 },
];
/** Açılıştaki "gürültü" tüneli: listedeki gerçek taban sıraları */
export const GURULTU_SAYILARI = [
...new Set(LISTE.flatMap((s) => s.sira)),
].map((n) => n.toLocaleString("tr-TR"));
/** İl program sayıları (2026 kılavuzu) — harita kuleleri */
export const IL_PROGRAM: Record<string, number> = {
İstanbul: 6442,
Ankara: 1831,
İzmir: 793,
Antalya: 427,
Trabzon: 317,
Kayseri: 235,
Van: 123,
Diyarbakır: 111,
};

53
video/src/yardimci.ts Normal file
View File

@@ -0,0 +1,53 @@
import { Easing } from "remotion";
export const clamp01 = (t: number) => Math.max(0, Math.min(1, t));
export const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
/** Sitedeki pixel-decor.tsx ile aynı deterministik sözde-rastgele */
export const hash = (x: number, y: number, seed = 0) => {
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
return n - Math.floor(n);
};
export const ease = {
out: Easing.bezier(0.22, 1, 0.36, 1),
inOut: Easing.bezier(0.65, 0, 0.35, 1),
in: Easing.bezier(0.64, 0, 0.78, 0),
// Sitenin yay hissi: bounce 0 (Apple damping 1)
yay: Easing.bezier(0.2, 0.8, 0.2, 1),
geriTep: Easing.bezier(0.34, 1.56, 0.64, 1),
};
/** f, [a, b] aralığında 0→1 ilerleme (kırpılmış, isteğe bağlı eğri) */
export const aralik = (
f: number,
a: number,
b: number,
egri: (t: number) => number = (t) => t,
) => egri(clamp01((f - a) / (b - a)));
/** İki CSS rengi arasında oklab karışımı (oklch token'larla çalışır) */
export const karis = (a: string, b: string, t: number) => {
const p = Math.round(clamp01(t) * 1000) / 10;
if (p <= 0) return a;
if (p >= 100) return b;
return `color-mix(in oklab, ${b} ${p}%, ${a})`;
};
/** Sayıyı Türkçe binlik ayraçla yazar: 45000 → "45.000" */
export const tr = (n: number) => Math.round(n).toLocaleString("tr-TR");
/** Metin ölçümü: kinetik yerleşimler için gerçek yazı tipiyle genişlik */
export const olc = (
metin: string,
font: { family: string; size: number; weight?: number; italic?: boolean; tracking?: number },
) => {
if (typeof document === "undefined") return metin.length * font.size * 0.55;
const ctx = document.createElement("canvas").getContext("2d");
if (!ctx) return metin.length * font.size * 0.55;
ctx.font = `${font.italic ? "italic " : ""}${font.weight ?? 400} ${font.size}px "${font.family}"`;
return (
ctx.measureText(metin).width +
(font.tracking ?? 0) * font.size * Array.from(metin).length
);
};

41
video/src/zaman.ts Normal file
View File

@@ -0,0 +1,41 @@
/**
* Zaman çizelgesi (30 fps). Sahneler TransitionSeries'te art arda; geçişler
* iki sahneyi üst üste bindirir, yani toplam süre = sahneler − geçişler.
*
* Storyboard omurgası: her geçiş ya bir **piksel** (dalga, tek piksele
* çöküş, havuzdan seçilen 24 piksel) ya da **imlecin tıkladığı nokta**
* (o noktanın içinden itme / o noktadan yayılan piksel dalgası) üstüne
* kurulu. Geçişin başladığı kare, imlecin tıkladığı karedir.
*/
export const SAHNE = {
gurultu: 150,
harita: 250,
siralama: 126,
havuz: 420,
danisman: 200,
deger: 170,
kapanis: 180,
} as const;
export const GECIS = {
/** gürültü → harita: tek piksel patlar, koyudan açığa piksel dalgası */
g1: 30,
/** harita → sıralama: imleç logonun turuncu karesine tıklar, dalga oradan */
g2: 26,
/** sıralama → havuz: CTA'ya tıklanır, sahne butonun içinden geçer */
g3: 22,
/** havuz → danışman: listedeki satıra tıklanır, satırın içinden geçilir */
g4: 22,
/** danışman → değer: gönder düğmesinden piksel dalgası */
g5: 26,
/** değer → kapanış: son gösterge karesinden koyu piksel dalgası */
g6: 28,
} as const;
/** Bir sahnenin, kendisinden sonraki geçişin başladığı yerel karesi */
export const gecisBasi = (sahne: number, gecis: number) => sahne - gecis;
export const TOPLAM =
Object.values(SAHNE).reduce((a, b) => a + b, 0) -
Object.values(GECIS).reduce((a, b) => a + b, 0);

18
video/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
},
"include": ["src", "remotion.config.ts"]
}