Revert "Lansman videosu (Remotion) projesi eklendi"
Some checks failed
Deploy / deploy (push) Has been cancelled

This reverts commit d2359d3f3a.
This commit is contained in:
bilalgursen
2026-10-04 14:07:47 +03:00
parent 504c5c85d9
commit 50ddd331d7
55 changed files with 1 additions and 7492 deletions

View File

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

View File

@@ -12,8 +12,6 @@ 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", "video"]
"exclude": ["node_modules"]
}

2
video/.gitignore vendored
View File

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

View File

@@ -1,30 +0,0 @@
# 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ı)

View File

@@ -1,17 +0,0 @@
{
"$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"
}
}

View File

@@ -1,32 +0,0 @@
{
"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

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
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"),
},
},
}));

View File

@@ -1,100 +0,0 @@
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>
);
};

View File

@@ -1,15 +0,0 @@
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

@@ -1,60 +0,0 @@
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

@@ -1,84 +0,0 @@
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

@@ -1,48 +0,0 @@
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

@@ -1,94 +0,0 @@
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

@@ -1,55 +0,0 @@
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>
);
};

View File

@@ -1,200 +0,0 @@
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

@@ -1,69 +0,0 @@
"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

@@ -1,115 +0,0 @@
"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

@@ -1,115 +0,0 @@
"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

@@ -1,243 +0,0 @@
"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

@@ -1,86 +0,0 @@
"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

@@ -1,72 +0,0 @@
"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

@@ -1,112 +0,0 @@
"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

@@ -1,77 +0,0 @@
"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

@@ -1,72 +0,0 @@
"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

@@ -1,133 +0,0 @@
"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

@@ -1,22 +0,0 @@
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;
}

View File

@@ -1,38 +0,0 @@
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

@@ -1,137 +0,0 @@
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 };
}

View File

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

View File

@@ -1,87 +0,0 @@
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

@@ -1,27 +0,0 @@
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

@@ -1,17 +0,0 @@
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

@@ -1,108 +0,0 @@
"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

@@ -1,96 +0,0 @@
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

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

View File

@@ -1,67 +0,0 @@
/**
* 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

@@ -1,183 +0,0 @@
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

@@ -1,355 +0,0 @@
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

@@ -1,155 +0,0 @@
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

@@ -1,707 +0,0 @@
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

@@ -1,242 +0,0 @@
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

@@ -1,120 +0,0 @@
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

@@ -1,205 +0,0 @@
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>
);
};

View File

@@ -1,51 +0,0 @@
/**
* 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 }] : [])),
);

View File

@@ -1,88 +0,0 @@
/**
* 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,
};

View File

@@ -1,53 +0,0 @@
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
);
};

View File

@@ -1,41 +0,0 @@
/**
* 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);

View File

@@ -1,18 +0,0 @@
{
"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"]
}