Compare commits
2 Commits
9c02b810e7
...
504c5c85d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
504c5c85d9 | ||
|
|
d2359d3f3a |
@@ -27,3 +27,5 @@ data/
|
|||||||
!data/yokatlas.db
|
!data/yokatlas.db
|
||||||
!data/yokatlas.db-wal
|
!data/yokatlas.db-wal
|
||||||
!data/yokatlas.db-shm
|
!data/yokatlas.db-shm
|
||||||
|
# Lansman videosu (Remotion) imaja girmez
|
||||||
|
video/
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const eslintConfig = defineConfig([
|
|||||||
"out/**",
|
"out/**",
|
||||||
"build/**",
|
"build/**",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
// Lansman videosu ayrı bir Remotion projesi (kendi package.json'ı var)
|
||||||
|
"video/**",
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -341,6 +341,11 @@
|
|||||||
animation: kt-piksel-belir 0.42s cubic-bezier(0.23, 1, 0.32, 1) both;
|
animation: kt-piksel-belir 0.42s cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Harita görünüme girene kadar bekler (PikselSvg data-gorundu yazar). */
|
||||||
|
.kt-piksel-harita:not([data-gorundu]) .kt-piksel-belir {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.kt-piksel-belir {
|
.kt-piksel-belir {
|
||||||
animation: none;
|
animation: none;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { motion, useReducedMotion } from "motion/react";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı (Motion).
|
// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı.
|
||||||
// İki varyant:
|
// İki varyant:
|
||||||
//
|
//
|
||||||
// - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi →
|
// - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi →
|
||||||
@@ -14,9 +13,14 @@ import { cn } from "@/lib/utils";
|
|||||||
// - "nabiz": kareler yerlerinden ayrılmaz; 120° faz farkıyla dalga gibi
|
// - "nabiz": kareler yerlerinden ayrılmaz; 120° faz farkıyla dalga gibi
|
||||||
// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli).
|
// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli).
|
||||||
//
|
//
|
||||||
// Not: motion.rect'te `x`/`y` transform değeridir (attribute değil) — kareler
|
// Performans notu: kareler SVG <rect> değil HTML <span>, animasyon da Web
|
||||||
// attribute yerine translate ile konumlanır; scale, transform-box: fill-box
|
// Animations API ile yalnızca `translate` / `scale` / `opacity` üzerinden
|
||||||
// sayesinde karenin kendi merkezinden uygulanır.
|
// yürür. Bu üçü compositor'da (GPU) oynar: ana iş parçacığı meşgulken
|
||||||
|
// (hidrasyon, sayfa geçişi, kaydırma) bile kare düşmez ve hover React'i
|
||||||
|
// yeniden render etmez. SVG iç elemanlarının transform'u her karede yeniden
|
||||||
|
// boyanır; o yüzden buraya geri dönme.
|
||||||
|
|
||||||
|
const KUTU = 180; // kaynak SVG'nin viewBox kenarı
|
||||||
|
|
||||||
const POZISYONLAR = [
|
const POZISYONLAR = [
|
||||||
[27, 56],
|
[27, 56],
|
||||||
@@ -32,10 +36,11 @@ const KARELER = [
|
|||||||
|
|
||||||
// Rota: tam tur 3.6s — 3 etap × (%25 yol = 0.9s + %8.3 varışta soluklanma).
|
// Rota: tam tur 3.6s — 3 etap × (%25 yol = 0.9s + %8.3 varışta soluklanma).
|
||||||
// 0.9s'lik yolculuk, nabız varyantının küçül-büyü temposuyla aynı hızda.
|
// 0.9s'lik yolculuk, nabız varyantının küçül-büyü temposuyla aynı hızda.
|
||||||
const ROTA_SURE = 3.6;
|
const ROTA_SURE = 3600;
|
||||||
// Kalkışlar kademeli: kareler eş zamanlı değil, danışman başlığındaki gibi
|
// Kalkışlar kademeli: kareler eş zamanlı değil, danışman başlığındaki gibi
|
||||||
// sırayla (mavi → şeftali → turuncu) yola çıkıp küçülür.
|
// sırayla (mavi → şeftali → turuncu) yola çıkıp küçülür. Gecikme yalnızca ilk
|
||||||
const ROTA_GECIKME_ADIMI = 0.2;
|
// turdan önce uygulanır, sonraki turlarda faz farkı kendiliğinden korunur.
|
||||||
|
const ROTA_GECIKME_ADIMI = 200;
|
||||||
const KONUM_ZAMANLARI = [0, 0.25, 1 / 3, 0.583, 2 / 3, 0.917, 1];
|
const KONUM_ZAMANLARI = [0, 0.25, 1 / 3, 0.583, 2 / 3, 0.917, 1];
|
||||||
// Nabız (rota içi): her yolculuğun ortasında dibe vurur, varışta tam form.
|
// Nabız (rota içi): her yolculuğun ortasında dibe vurur, varışta tam form.
|
||||||
const ROTA_NABIZ_ZAMANLARI = [
|
const ROTA_NABIZ_ZAMANLARI = [
|
||||||
@@ -46,20 +51,77 @@ const ROTA_NABIZ_OPACITY = [1, 0.6, 1, 1, 0.6, 1, 1, 0.6, 1, 1];
|
|||||||
|
|
||||||
// Yerinde nabız: kesintisiz tam-sinüs (dinlenme yok), delay'ler döngüyü üçe
|
// Yerinde nabız: kesintisiz tam-sinüs (dinlenme yok), delay'ler döngüyü üçe
|
||||||
// böler — biri büyürken sıradaki küçülür, hiçbir kare beklemez.
|
// böler — biri büyürken sıradaki küçülür, hiçbir kare beklemez.
|
||||||
const NABIZ_SURE = 1.8;
|
const NABIZ_SURE = 1800;
|
||||||
const NABIZ_GECIKMELER = [0, 0.6, 1.2] as const;
|
const NABIZ_GECIKMELER = [0, 600, 1200] as const;
|
||||||
const SINE = [0.37, 0, 0.63, 1] as const;
|
|
||||||
|
|
||||||
/** Kare i için durak koordinatları: kendi yeri → sıradaki → ondan sonraki →
|
const SINE = "cubic-bezier(0.37, 0, 0.63, 1)";
|
||||||
|
// Yolculuk: yumuşak kalkış, uzun ve yumuşak iniş (easeInOutCubic).
|
||||||
|
const YOL = "cubic-bezier(0.65, 0, 0.35, 1)";
|
||||||
|
// Hover bitince yerine oturma (easeOutQuint): hızlı toparlanır, sarsmadan durur.
|
||||||
|
const DONUS = "cubic-bezier(0.22, 1, 0.36, 1)";
|
||||||
|
const DONUS_SURE = 500;
|
||||||
|
|
||||||
|
/** Karenin `translate` değeri: yüzdeler karenin kendi boyutuna göredir, bu
|
||||||
|
* yüzden işaret hangi boyutta çizilirse çizilsin aynı oran korunur. */
|
||||||
|
function yer(pozisyon: number, boyut: number): string {
|
||||||
|
const [x, y] = POZISYONLAR[pozisyon % 3];
|
||||||
|
return `${(x / boyut) * 100}% ${(y / boyut) * 100}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kare için durak koordinatları: kendi yeri → sıradaki → ondan sonraki →
|
||||||
* kendi yeri. Duraklarda bekleme için her hedef iki kez tekrarlanır
|
* kendi yeri. Duraklarda bekleme için her hedef iki kez tekrarlanır
|
||||||
* (KONUM_ZAMANLARI ile hizalı). */
|
* (KONUM_ZAMANLARI ile hizalı). */
|
||||||
function rota(baslangic: number, eksen: 0 | 1): number[] {
|
function rota(baslangic: number, boyut: number): string[] {
|
||||||
const [p0, p1, p2] = [0, 1, 2].map(
|
const [p0, p1, p2] = [0, 1, 2].map((adim) => yer(baslangic + adim, boyut));
|
||||||
(adim) => POZISYONLAR[(baslangic + adim) % 3][eksen],
|
|
||||||
);
|
|
||||||
return [p0, p1, p1, p2, p2, p0, p0];
|
return [p0, p1, p1, p2, p2, p0, p0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function donguyuBaslat(
|
||||||
|
el: HTMLElement,
|
||||||
|
i: number,
|
||||||
|
varyant: "rota" | "nabiz",
|
||||||
|
): Animation[] {
|
||||||
|
const k = KARELER[i];
|
||||||
|
if (varyant === "nabiz") {
|
||||||
|
return [
|
||||||
|
el.animate(
|
||||||
|
{ scale: ["1", "0.82", "1"], opacity: [1, 0.55, 1], easing: SINE },
|
||||||
|
{
|
||||||
|
duration: NABIZ_SURE,
|
||||||
|
delay: NABIZ_GECIKMELER[i],
|
||||||
|
iterations: Infinity,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const secenek = {
|
||||||
|
duration: ROTA_SURE,
|
||||||
|
delay: i * ROTA_GECIKME_ADIMI,
|
||||||
|
iterations: Infinity,
|
||||||
|
};
|
||||||
|
// Konum ve nabız ayrı animasyonlar: konum etabı tek parça eğriyle akar,
|
||||||
|
// ortasındaki küçülme onu ikiye bölüp duraksatmaz.
|
||||||
|
return [
|
||||||
|
el.animate(
|
||||||
|
{
|
||||||
|
translate: rota(k.baslangic, k.boyut),
|
||||||
|
offset: KONUM_ZAMANLARI,
|
||||||
|
easing: YOL,
|
||||||
|
},
|
||||||
|
secenek,
|
||||||
|
),
|
||||||
|
el.animate(
|
||||||
|
{
|
||||||
|
scale: ROTA_NABIZ_SCALE.map(String),
|
||||||
|
opacity: ROTA_NABIZ_OPACITY,
|
||||||
|
offset: ROTA_NABIZ_ZAMANLARI,
|
||||||
|
easing: SINE,
|
||||||
|
},
|
||||||
|
secenek,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animasyonlu KolayTercih işareti. Varsayılanda koreografi sürekli döner;
|
* Animasyonlu KolayTercih işareti. Varsayılanda koreografi sürekli döner;
|
||||||
* `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site
|
* `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site
|
||||||
@@ -78,116 +140,99 @@ export function AnimasyonluMark({
|
|||||||
* (orange-500) — ör. danışman panel başlığı. */
|
* (orange-500) — ör. danışman panel başlığı. */
|
||||||
renk?: "marka" | "turuncu";
|
renk?: "marka" | "turuncu";
|
||||||
}) {
|
}) {
|
||||||
const [hover, setHover] = useState(false);
|
const kokRef = useRef<HTMLSpanElement>(null);
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
|
||||||
const azHareket = useReducedMotion();
|
|
||||||
const aktif = !azHareket && (!sadeceHover || hover);
|
|
||||||
|
|
||||||
// Hover hedefi işaretin kendisi değil, en yakın `group` ebeveyni (site
|
|
||||||
// header'da logo + "KolayTercih" yazısını saran Link): yazının üzerine
|
|
||||||
// gelindiğinde de animasyon başlar. `group`'suz kullanımda svg'ye düşer.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sadeceHover) return;
|
const kok = kokRef.current;
|
||||||
const hedef = svgRef.current?.closest(".group") ?? svgRef.current;
|
if (!kok || typeof kok.animate !== "function") return;
|
||||||
if (!hedef) return;
|
const azHareket = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||||
const ac = () => setHover(true);
|
const kareler = Array.from(kok.children) as HTMLElement[];
|
||||||
const kapat = () => setHover(false);
|
|
||||||
hedef.addEventListener("mouseenter", ac);
|
let dongu: Animation[] = [];
|
||||||
hedef.addEventListener("mouseleave", kapat);
|
let donus: Animation[] = [];
|
||||||
return () => {
|
let istekli = false;
|
||||||
hedef.removeEventListener("mouseenter", ac);
|
|
||||||
hedef.removeEventListener("mouseleave", kapat);
|
const baslat = () => {
|
||||||
|
if (istekli) return;
|
||||||
|
istekli = true;
|
||||||
|
if (azHareket.matches) return;
|
||||||
|
// Kareler hâlâ yerine dönüyorsa (hızlı çık-gir) dönüşün bitmesi
|
||||||
|
// beklenir; döngü hep yerinden başlar, sıçrama olmaz.
|
||||||
|
Promise.all(donus.map((a) => a.finished))
|
||||||
|
.then(() => {
|
||||||
|
if (!istekli || dongu.length > 0) return;
|
||||||
|
dongu = kareler.flatMap((el, i) => donguyuBaslat(el, i, varyant));
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
}, [sadeceHover]);
|
|
||||||
|
const durdur = () => {
|
||||||
|
istekli = false;
|
||||||
|
if (dongu.length === 0) return;
|
||||||
|
// Kareler o an neredeyse oradan yerlerine süzülür.
|
||||||
|
const anlik = kareler.map((el) => {
|
||||||
|
const cs = getComputedStyle(el);
|
||||||
|
return { translate: cs.translate, scale: cs.scale, opacity: cs.opacity };
|
||||||
|
});
|
||||||
|
dongu.forEach((a) => a.cancel());
|
||||||
|
dongu = [];
|
||||||
|
donus = kareler.map((el, i) =>
|
||||||
|
el.animate(
|
||||||
|
[
|
||||||
|
anlik[i],
|
||||||
|
{
|
||||||
|
translate: yer(KARELER[i].baslangic, KARELER[i].boyut),
|
||||||
|
scale: "1",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ duration: DONUS_SURE, easing: DONUS },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!sadeceHover) {
|
||||||
|
baslat();
|
||||||
|
return () => {
|
||||||
|
dongu.forEach((a) => a.cancel());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover hedefi işaretin kendisi değil, en yakın `group` ebeveyni (site
|
||||||
|
// header'da logo + "KolayTercih" yazısını saran Link): yazının üzerine
|
||||||
|
// gelindiğinde de animasyon başlar. `group`'suz kullanımda köke düşer.
|
||||||
|
const hedef = kok.closest(".group") ?? kok;
|
||||||
|
hedef.addEventListener("mouseenter", baslat);
|
||||||
|
hedef.addEventListener("mouseleave", durdur);
|
||||||
|
return () => {
|
||||||
|
hedef.removeEventListener("mouseenter", baslat);
|
||||||
|
hedef.removeEventListener("mouseleave", durdur);
|
||||||
|
[...dongu, ...donus].forEach((a) => a.cancel());
|
||||||
|
};
|
||||||
|
}, [sadeceHover, varyant]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<span
|
||||||
ref={svgRef}
|
ref={kokRef}
|
||||||
viewBox="0 0 180 180"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
focusable="false"
|
className={cn("relative block shrink-0", className)}
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
>
|
>
|
||||||
{KARELER.map((k, i) => {
|
{KARELER.map((k) => (
|
||||||
const [px, py] = POZISYONLAR[k.baslangic];
|
<span
|
||||||
const duruk = { x: px, y: py, scale: 1, opacity: 1 };
|
key={k.renk}
|
||||||
|
className={cn(
|
||||||
const animate = !aktif
|
"absolute top-0 left-0 block",
|
||||||
? duruk
|
renk === "turuncu" && "bg-orange-500",
|
||||||
: varyant === "rota"
|
)}
|
||||||
? {
|
style={{
|
||||||
x: rota(k.baslangic, 0),
|
width: `${(k.boyut / KUTU) * 100}%`,
|
||||||
y: rota(k.baslangic, 1),
|
height: `${(k.boyut / KUTU) * 100}%`,
|
||||||
scale: ROTA_NABIZ_SCALE,
|
borderRadius: `${(13 / k.boyut) * 100}%`,
|
||||||
opacity: ROTA_NABIZ_OPACITY,
|
backgroundColor: renk === "marka" ? k.renk : undefined,
|
||||||
}
|
translate: yer(k.baslangic, k.boyut),
|
||||||
: {
|
}}
|
||||||
x: px,
|
/>
|
||||||
y: py,
|
))}
|
||||||
scale: [1, 0.82, 1],
|
</span>
|
||||||
opacity: [1, 0.55, 1],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Kademeli kalkış: delay yalnızca ilk turdan önce uygulanır, sonraki
|
|
||||||
// turlarda faz farkı kendiliğinden korunur.
|
|
||||||
const rotaGecikme = i * ROTA_GECIKME_ADIMI;
|
|
||||||
const konumGecis = {
|
|
||||||
duration: ROTA_SURE,
|
|
||||||
times: KONUM_ZAMANLARI,
|
|
||||||
ease: "easeInOut" as const,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: rotaGecikme,
|
|
||||||
};
|
|
||||||
const rotaNabizGecis = {
|
|
||||||
duration: ROTA_SURE,
|
|
||||||
times: ROTA_NABIZ_ZAMANLARI,
|
|
||||||
ease: "easeInOut" as const,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: rotaGecikme,
|
|
||||||
};
|
|
||||||
const nabizGecis = {
|
|
||||||
duration: NABIZ_SURE,
|
|
||||||
times: [0, 0.5, 1],
|
|
||||||
ease: SINE as unknown as [number, number, number, number],
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: NABIZ_GECIKMELER[i],
|
|
||||||
};
|
|
||||||
|
|
||||||
const transition = !aktif
|
|
||||||
? { duration: 0.4, ease: "easeOut" as const }
|
|
||||||
: varyant === "rota"
|
|
||||||
? {
|
|
||||||
x: konumGecis,
|
|
||||||
y: konumGecis,
|
|
||||||
scale: rotaNabizGecis,
|
|
||||||
opacity: rotaNabizGecis,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
x: { duration: 0 },
|
|
||||||
y: { duration: 0 },
|
|
||||||
scale: nabizGecis,
|
|
||||||
opacity: nabizGecis,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.rect
|
|
||||||
key={k.renk}
|
|
||||||
width={k.boyut}
|
|
||||||
height={k.boyut}
|
|
||||||
rx={13}
|
|
||||||
fill={k.renk}
|
|
||||||
className={renk === "turuncu" ? "fill-orange-500" : undefined}
|
|
||||||
animate={animate}
|
|
||||||
transition={transition}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
transformBox: "fill-box",
|
|
||||||
transformOrigin: "center",
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</svg>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,24 +97,22 @@ function mesaj(): string[] {
|
|||||||
bicim += "\n";
|
bicim += "\n";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bloklar arasında birer boş satır; her blokta tek punto, tek renk.
|
||||||
|
const arapca = `font-size:18px;line-height:2.1;font-family:${ARAPCA_YAZI_TIPI}`;
|
||||||
|
ekle(`\n${BESMELE}\n`, `${arapca};color:${SILIK}`);
|
||||||
|
ekle(`${ASR.join("\n")}\n`, arapca);
|
||||||
|
ekle(`\n${MEAL[0]}\n`, `font-size:12px;line-height:1.8;color:${SILIK}`);
|
||||||
|
ekle(`${MEAL.slice(2).join("\n")}\n`, "font-size:12px;line-height:1.8");
|
||||||
|
ekle("Asr Sûresi (103) · Diyanet İşleri meali\n", `font-size:11px;line-height:2.2;color:${SILIK}`);
|
||||||
ekle(
|
ekle(
|
||||||
`\n${BESMELE}\n${ASR.join("\n")}\n`,
|
"\n«Vakit bir kılıçtır; sen onu kesmezsen o seni keser.»\n",
|
||||||
`font-size:22px;line-height:2;font-family:${ARAPCA_YAZI_TIPI}`,
|
`font-size:12px;font-style:italic;line-height:1.8;color:${TURUNCU}`,
|
||||||
);
|
|
||||||
ekle(`\n${MEAL.join("\n")}\n`, "font-size:13px;line-height:1.7");
|
|
||||||
ekle("Asr Sûresi (103) · Diyanet İşleri meali\n\n", `font-size:11px;color:${SILIK}`);
|
|
||||||
ekle(
|
|
||||||
"«Vakit bir kılıçtır; sen onu kesmezsen o seni keser.»\n",
|
|
||||||
`font-size:13px;font-weight:600;line-height:1.8;color:${TURUNCU}`,
|
|
||||||
);
|
);
|
||||||
ekle(
|
ekle(
|
||||||
"İmam Şâfiî’nin sûfîlerden öğrendiğini söylediği söz\n\n",
|
"İmam Şâfiî’nin sûfîlerden öğrendiğini söylediği söz\n",
|
||||||
`font-size:11px;color:${SILIK}`,
|
`font-size:11px;line-height:2.2;color:${SILIK}`,
|
||||||
);
|
|
||||||
ekle(
|
|
||||||
`Meraklısına: ${window.location.origin}/meraklisina`,
|
|
||||||
`font-size:11px;color:${SILIK}`,
|
|
||||||
);
|
);
|
||||||
|
ekle(`\n${window.location.origin}/meraklisina`, `font-size:11px;color:${SILIK}`);
|
||||||
|
|
||||||
return [bicim, ...stiller];
|
return [bicim, ...stiller];
|
||||||
}
|
}
|
||||||
@@ -126,7 +124,12 @@ export function ConsoleAsr() {
|
|||||||
|
|
||||||
// Hidrasyon ve ilk etkileşimle yarışmasın: tarayıcı boşa çıkınca bas.
|
// Hidrasyon ve ilk etkileşimle yarışmasın: tarayıcı boşa çıkınca bas.
|
||||||
// Safari'de requestIdleCallback yok, orada kısa bir gecikme yeter.
|
// Safari'de requestIdleCallback yok, orada kısa bir gecikme yeter.
|
||||||
const bas = () => console.log(...mesaj());
|
// Önce konsolu temizle: eklenti uyarıları ve çerçeve satırları sürprizi
|
||||||
|
// aşağı itmesin, konsolu açan yalnız bunu görsün.
|
||||||
|
const bas = () => {
|
||||||
|
console.clear();
|
||||||
|
console.log(...mesaj());
|
||||||
|
};
|
||||||
if (typeof window.requestIdleCallback === "function") {
|
if (typeof window.requestIdleCallback === "function") {
|
||||||
window.requestIdleCallback(bas, { timeout: 3000 });
|
window.requestIdleCallback(bas, { timeout: 3000 });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ const HOT_COLOR = "#f97316"; // orange-500
|
|||||||
const HOT_OPACITY = 0.85;
|
const HOT_OPACITY = 0.85;
|
||||||
const HEAT_DURATION = 0.15; // ısınma: anlık geri bildirim
|
const HEAT_DURATION = 0.15; // ısınma: anlık geri bildirim
|
||||||
const COOL_DURATION = 1.4; // soğuma: iz gibi yavaşça söner
|
const COOL_DURATION = 1.4; // soğuma: iz gibi yavaşça söner
|
||||||
|
// Kırpışma: imleç olmadan, rastgele tek piksel ısınıp söner. Yormasın diye
|
||||||
|
// seyrek ve yumuşak — aynı anda ekranda bir-iki sıcak piksel olur.
|
||||||
|
const KIRPISMA_BEKLEME = 1500; // ms; sayfa otursun
|
||||||
|
const KIRPISMA_ARALIK = [500, 1200]; // ms; iki kırpışma arası (rastgele)
|
||||||
|
const KIRPISMA_ISINMA = 0.5; // imleçtekinden yavaş: göz kırpar, çakmaz
|
||||||
|
const KIRPISMA_TUTMA = 900; // ms; ısınma + kısa bekleyiş, sonra soğur
|
||||||
|
|
||||||
type HeatTarget = {
|
type HeatTarget = {
|
||||||
el: SVGRectElement;
|
el: SVGRectElement;
|
||||||
@@ -139,18 +145,74 @@ function ayril(inst: HeatInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rastgele pikselleri seyrek aralıklarla ısıtıp soğutur; durdurucu döner.
|
||||||
|
* Fırçayla aynı `calistir`/`hot` üzerinden çalışır: imlecin altındaki
|
||||||
|
* piksele dokunmaz, fırça da kırpışan pikseli devralabilir. Harita ekran
|
||||||
|
* dışındayken ya da sekme arka plandayken bekler. */
|
||||||
|
function kirpismaBaslat(inst: HeatInstance) {
|
||||||
|
const yanan = new Set<SVGRectElement>();
|
||||||
|
const bekleyen = new Set<number>();
|
||||||
|
let gorunur = true;
|
||||||
|
const io = new IntersectionObserver(([g]) => {
|
||||||
|
gorunur = g.isIntersecting;
|
||||||
|
});
|
||||||
|
io.observe(inst.svg);
|
||||||
|
|
||||||
|
let sayac = 0;
|
||||||
|
const tik = () => {
|
||||||
|
const [enAz, enCok] = KIRPISMA_ARALIK;
|
||||||
|
sayac = window.setTimeout(tik, enAz + Math.random() * (enCok - enAz));
|
||||||
|
if (!gorunur || document.hidden) return;
|
||||||
|
const p = inst.pixels[Math.floor(Math.random() * inst.pixels.length)];
|
||||||
|
if (inst.hot.has(p.el) || yanan.has(p.el)) return;
|
||||||
|
yanan.add(p.el);
|
||||||
|
calistir(
|
||||||
|
inst,
|
||||||
|
p.el,
|
||||||
|
{ fill: HOT_COLOR, fillOpacity: HOT_OPACITY },
|
||||||
|
KIRPISMA_ISINMA,
|
||||||
|
);
|
||||||
|
const s = window.setTimeout(() => {
|
||||||
|
bekleyen.delete(s);
|
||||||
|
yanan.delete(p.el);
|
||||||
|
if (inst.hot.has(p.el)) return;
|
||||||
|
calistir(
|
||||||
|
inst,
|
||||||
|
p.el,
|
||||||
|
{ fill: inst.baseColor, fillOpacity: p.baseOpacity },
|
||||||
|
COOL_DURATION,
|
||||||
|
);
|
||||||
|
}, KIRPISMA_TUTMA);
|
||||||
|
bekleyen.add(s);
|
||||||
|
};
|
||||||
|
sayac = window.setTimeout(tik, KIRPISMA_BEKLEME);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(sayac);
|
||||||
|
bekleyen.forEach((s) => window.clearTimeout(s));
|
||||||
|
io.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function usePixelHeat(
|
export function usePixelHeat(
|
||||||
svgRef: RefObject<SVGSVGElement | null>,
|
svgRef: RefObject<SVGSVGElement | null>,
|
||||||
{
|
{
|
||||||
/** Fırça yarıçapı, SVG (viewBox) biriminde */
|
/** Fırça yarıçapı, SVG (viewBox) biriminde */
|
||||||
radius = 34,
|
radius = 34,
|
||||||
}: { radius?: number } = {},
|
/** İmleç olmadan da rastgele pikseller seyrekçe ısınıp sönsün (hero) */
|
||||||
|
kirpisma = false,
|
||||||
|
}: { radius?: number; kirpisma?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const svg = svgRef.current;
|
const svg = svgRef.current;
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
// Dokunmatik cihazlarda hover yanlış tetiklenir; efekti imleçli cihazlara sakla
|
// Dokunmatik cihazlarda hover yanlış tetiklenir; fırça imleçli cihazlara
|
||||||
if (!window.matchMedia(FINE_POINTER_QUERY).matches) return;
|
// saklanır. Kırpışma imleç istemez ama "hareketi azalt"ta kapalıdır.
|
||||||
|
const imlecli = window.matchMedia(FINE_POINTER_QUERY).matches;
|
||||||
|
const kirp =
|
||||||
|
kirpisma &&
|
||||||
|
!window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
if (!imlecli && !kirp) return;
|
||||||
|
|
||||||
if (!svg.viewBox.baseVal.width) return;
|
if (!svg.viewBox.baseVal.width) return;
|
||||||
|
|
||||||
@@ -173,7 +235,11 @@ export function usePixelHeat(
|
|||||||
hot: new Set(),
|
hot: new Set(),
|
||||||
controls: new Map(),
|
controls: new Map(),
|
||||||
};
|
};
|
||||||
kaydol(inst);
|
if (imlecli) kaydol(inst);
|
||||||
return () => ayril(inst);
|
const kirpismaDur = kirp ? kirpismaBaslat(inst) : null;
|
||||||
}, [svgRef, radius]);
|
return () => {
|
||||||
|
kirpismaDur?.();
|
||||||
|
ayril(inst);
|
||||||
|
};
|
||||||
|
}, [svgRef, radius, kirpisma]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,40 +195,61 @@ function PikselSvg({
|
|||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
usePixelHeat(svgRef, { radius: ISI_YARICAPI });
|
usePixelHeat(svgRef, { radius: ISI_YARICAPI });
|
||||||
|
|
||||||
// Yüklenince gecikmeyle odağa yakınlaş. viewBox her karede yazılır; ısı
|
// Harita görünüme girince (yüklenince değil) gecikmeyle odağa yakınlaş;
|
||||||
// fırçası viewBox'ı canlı okuduğu için imleç hep doğru kareyi ısıtır.
|
// il karelerinin sırayla belirmesi de o ana kadar bekler (globals.css:
|
||||||
|
// data-gorundu yokken kt-piksel-belir duraklatılır). Ekranın altında kalan
|
||||||
|
// harita, kullanıcı oraya inmeden animasyonunu harcamasın. viewBox her
|
||||||
|
// karede yazılır; ısı fırçası viewBox'ı canlı okuduğu için imleç hep doğru
|
||||||
|
// kareyi ısıtır.
|
||||||
const { sinir, zoom } = harita;
|
const { sinir, zoom } = harita;
|
||||||
const hizaLg = odakHiza?.lg ?? ORTA;
|
const hizaLg = odakHiza?.lg ?? ORTA;
|
||||||
const hizaDar = odakHiza?.dar ?? ORTA;
|
const hizaDar = odakHiza?.dar ?? ORTA;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const svg = svgRef.current;
|
const svg = svgRef.current;
|
||||||
if (!svg || !sinir) return;
|
if (!svg) return;
|
||||||
const rect = svg.getBoundingClientRect();
|
|
||||||
const oran = rect.width && rect.height ? rect.width / rect.height : VIEW_W / VIEW_H;
|
|
||||||
const genis = window.matchMedia("(min-width: 1024px)").matches;
|
|
||||||
const odak = odakKutusu(sinir, oran, zoom, genis ? hizaLg : hizaDar);
|
|
||||||
const yaz = (k: Kutu) => svg.setAttribute("viewBox", kutuMetni(k));
|
const yaz = (k: Kutu) => svg.setAttribute("viewBox", kutuMetni(k));
|
||||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
let kontrol: { stop: () => void } | null = null;
|
||||||
yaz(odak);
|
const basla = () => {
|
||||||
return () => yaz(TAM_KUTU);
|
svg.setAttribute("data-gorundu", "");
|
||||||
}
|
if (!sinir) return;
|
||||||
const bas = TAM_KUTU;
|
const rect = svg.getBoundingClientRect();
|
||||||
const kontrol = animate(0, 1, {
|
const oran = rect.width && rect.height ? rect.width / rect.height : VIEW_W / VIEW_H;
|
||||||
type: "spring",
|
const genis = window.matchMedia("(min-width: 1024px)").matches;
|
||||||
bounce: 0,
|
const odak = odakKutusu(sinir, oran, zoom, genis ? hizaLg : hizaDar);
|
||||||
delay: ODAK_GECIKME,
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||||
duration: ODAK_SURE,
|
yaz(odak);
|
||||||
onUpdate: (t) =>
|
return;
|
||||||
yaz([
|
}
|
||||||
bas[0] + (odak[0] - bas[0]) * t,
|
const bas = TAM_KUTU;
|
||||||
bas[1] + (odak[1] - bas[1]) * t,
|
kontrol = animate(0, 1, {
|
||||||
bas[2] + (odak[2] - bas[2]) * t,
|
type: "spring",
|
||||||
bas[3] + (odak[3] - bas[3]) * t,
|
bounce: 0,
|
||||||
]),
|
delay: ODAK_GECIKME,
|
||||||
});
|
duration: ODAK_SURE,
|
||||||
|
onUpdate: (t) =>
|
||||||
|
yaz([
|
||||||
|
bas[0] + (odak[0] - bas[0]) * t,
|
||||||
|
bas[1] + (odak[1] - bas[1]) * t,
|
||||||
|
bas[2] + (odak[2] - bas[2]) * t,
|
||||||
|
bas[3] + (odak[3] - bas[3]) * t,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// Haritanın dörtte biri, ekranın alt %15'lik şeridinin üstüne çıkınca
|
||||||
|
const gozlemci = new IntersectionObserver(
|
||||||
|
(girdiler) => {
|
||||||
|
if (!girdiler.some((g) => g.isIntersecting)) return;
|
||||||
|
gozlemci.disconnect();
|
||||||
|
basla();
|
||||||
|
},
|
||||||
|
{ threshold: 0.25, rootMargin: "0px 0px -15% 0px" },
|
||||||
|
);
|
||||||
|
gozlemci.observe(svg);
|
||||||
return () => {
|
return () => {
|
||||||
kontrol.stop();
|
gozlemci.disconnect();
|
||||||
yaz(TAM_KUTU);
|
kontrol?.stop();
|
||||||
|
svg.removeAttribute("data-gorundu");
|
||||||
|
if (sinir) yaz(TAM_KUTU);
|
||||||
};
|
};
|
||||||
}, [sinir, zoom, hizaLg, hizaDar]);
|
}, [sinir, zoom, hizaLg, hizaDar]);
|
||||||
|
|
||||||
@@ -237,7 +258,7 @@ function PikselSvg({
|
|||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
||||||
preserveAspectRatio={doldur ? "xMidYMid slice" : "xMidYMid meet"}
|
preserveAspectRatio={doldur ? "xMidYMid slice" : "xMidYMid meet"}
|
||||||
className={cn(doldur ? "h-full w-full" : "h-auto w-full", "text-primary", className)}
|
className={cn(doldur ? "h-full w-full" : "h-auto w-full", "kt-piksel-harita text-primary", className)}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={etiket}
|
aria-label={etiket}
|
||||||
// Statik, deterministik markup — her render'da yeniden kurulmaz
|
// Statik, deterministik markup — her render'da yeniden kurulmaz
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
* imleç uzaklaşınca yavaşça kendi mavi tonuna soğur (asimetrik zamanlama).
|
* imleç uzaklaşınca yavaşça kendi mavi tonuna soğur (asimetrik zamanlama).
|
||||||
* Fırça mantığı usePixelHeat hook'unda ortaklaştırıldı; DOM'a
|
* Fırça mantığı usePixelHeat hook'unda ortaklaştırıldı; DOM'a
|
||||||
* querySelectorAll ile bağlandığı için statik markup'la sorunsuz çalışır.
|
* querySelectorAll ile bağlandığı için statik markup'la sorunsuz çalışır.
|
||||||
|
*
|
||||||
|
* İmleç gelmese de rastgele pikseller tek tük, seyrek aralıklarla turuncuya
|
||||||
|
* ısınıp söner (usePixelHeat `kirpisma`); "hareketi azalt"ta kapalıdır.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
@@ -139,7 +142,7 @@ const MAP_MARKUP = (() => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Şehir pikselleri: turuncu, hafif nabız; hover fırçasına katılmazlar
|
// Şehir pikselleri: turuncu, hafif nabız; hover fırçasına katılmazlar.
|
||||||
CITIES.forEach((city, i) => {
|
CITIES.forEach((city, i) => {
|
||||||
parts.push(
|
parts.push(
|
||||||
`<rect ${rectAttrs(city.x, city.y)} class="animate-pulse fill-orange-500/70 motion-reduce:animate-none" style="animation-duration:3.6s;animation-delay:${i * 0.45}s"/>`,
|
`<rect ${rectAttrs(city.x, city.y)} class="animate-pulse fill-orange-500/70 motion-reduce:animate-none" style="animation-duration:3.6s;animation-delay:${i * 0.45}s"/>`,
|
||||||
@@ -150,7 +153,7 @@ const MAP_MARKUP = (() => {
|
|||||||
|
|
||||||
export function TurkeyPixelMap({ className }: { className?: string }) {
|
export function TurkeyPixelMap({ className }: { className?: string }) {
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
usePixelHeat(svgRef);
|
usePixelHeat(svgRef, { kirpisma: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -442,15 +442,21 @@ export function CtaSiraForm({
|
|||||||
href={sonucHref()}
|
href={sonucHref()}
|
||||||
aria-label="Sıralamana uygun programları gör"
|
aria-label="Sıralamana uygun programları gör"
|
||||||
title="Sıralamana uygun programları gör"
|
title="Sıralamana uygun programları gör"
|
||||||
// Gölge yok (§6: derinlik gölgeyle verilmez), hover'da renk
|
// Gerilmiş link (after:inset-0) bandın tamamını kaplar. Ölçek
|
||||||
// değişmez (iri dolu yüzeyde orange-600 kırmızımsı okunuyor —
|
// linkin KENDİSİNE verilmez: transform, ::after için kapsayan
|
||||||
// Bilal, 24 Eyl 2026); tepki: ok kayar, basınca küçülür.
|
// blok olur, alan 48 px'lik daireye çöker, imleç linkten düşer
|
||||||
className="group flex size-12 shrink-0 items-center justify-center rounded-full bg-orange-500 text-white transition-transform duration-200 after:absolute after:inset-0 hover:scale-[1.04] active:scale-[0.95] sm:size-14 lg:order-3"
|
// ve hover açılıp kapanarak titrer. Büyüyen içteki dairedir.
|
||||||
|
className="group flex shrink-0 lg:order-3 after:absolute after:inset-0"
|
||||||
>
|
>
|
||||||
<ArrowRight
|
{/* Gölge yok (§6: derinlik gölgeyle verilmez), hover'da renk
|
||||||
className="size-6 transition-transform duration-200 group-hover:translate-x-0.5 sm:size-7"
|
değişmez (iri dolu yüzeyde orange-600 kırmızımsı okunuyor —
|
||||||
aria-hidden
|
Bilal, 24 Eyl 2026); tepki: ok kayar, basınca küçülür. */}
|
||||||
/>
|
<span className="flex size-12 items-center justify-center rounded-full bg-orange-500 text-white transition-transform duration-200 group-hover:scale-[1.04] group-active:scale-[0.95] sm:size-14">
|
||||||
|
<ArrowRight
|
||||||
|
className="size-6 transition-transform duration-200 group-hover:translate-x-0.5 sm:size-7"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,5 +30,5 @@
|
|||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "video"]
|
||||||
}
|
}
|
||||||
|
|||||||
2
video/.gitignore
vendored
Normal file
2
video/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
out
|
||||||
30
video/REMOCN-NOTU.md
Normal file
30
video/REMOCN-NOTU.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# remocn kurulum notu
|
||||||
|
|
||||||
|
remocn bileşenleri shadcn registry'sinden kurulur:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm dlx shadcn@latest add @remocn/<bileşen>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dikkat:** shadcn CLI kurulumda string literal'leri Tailwind sınıf listesi gibi
|
||||||
|
işleyip tekrar eden "sınıfları" siliyor. Bu, boşlukla ayrılmış ve içinde
|
||||||
|
yinelenen değer olan her string'i bozar: SVG path'leri, `viewBox="0 0 24 24"`,
|
||||||
|
`transformOrigin: "0 0"`, `oklch(1 0 0 / 10%)`. İlk kurulumda `cursor.tsx`
|
||||||
|
(el ve ok imleci path'leri, viewBox) ve `lib/remocn-ui/theme.ts` (iki oklch
|
||||||
|
değeri) böyle bozuldu ve registry JSON'undan (`https://remocn.dev/r/<ad>.json`)
|
||||||
|
geri yüklendi.
|
||||||
|
|
||||||
|
Yeni bir bileşen kurduktan sonra, içindeki boşluklu string'leri registry
|
||||||
|
JSON'uyla karşılaştır.
|
||||||
|
|
||||||
|
## Bu projede yerel olarak değiştirilen bileşenler
|
||||||
|
|
||||||
|
shadcn mantığı: kod bizim. Marka yazı tiplerini ve sahne ihtiyaçlarını
|
||||||
|
karşılamak için şu dosyalar değiştirildi (yeniden `add --overwrite` yapılırsa
|
||||||
|
değişiklikler gider):
|
||||||
|
|
||||||
|
- `tracking-in.tsx`: `fontFamily`, `background` (şeffaf), `endTracking`, ReactNode metin
|
||||||
|
- `kinetic-center-build.tsx`: gerçek yazı tipiyle ölçüm, `birim` (harf/kelime), `vurgu`, `gap`
|
||||||
|
- `focus-blur-resolve.tsx`: `fontFamily`
|
||||||
|
- `slot-machine-roll.tsx`: `inline`, `fontFamily`, `stagger`, karaktere göre sütun genişliği
|
||||||
|
- `push-through.tsx`: `origin` (itmenin tıklanan noktadan yapılması)
|
||||||
17
video/components.json
Normal file
17
video/components.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": { "config": "", "css": "", "baseColor": "slate", "cssVariables": false },
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {
|
||||||
|
"@remocn": "https://remocn.dev/r/{name}.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
video/package.json
Normal file
32
video/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "kolaytercih-video",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "KolayTercih lansman videosu (Remotion + remocn)",
|
||||||
|
"scripts": {
|
||||||
|
"studio": "remotion studio src/index.ts",
|
||||||
|
"render": "remotion render src/index.ts Lansman out/kolaytercih-lansman.mp4",
|
||||||
|
"render:dikey": "remotion render src/index.ts LansmanDikey out/kolaytercih-lansman-dikey.mp4",
|
||||||
|
"kare": "remotion still src/index.ts Lansman",
|
||||||
|
"tip": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@remotion/cli": "4.0.531",
|
||||||
|
"@remotion/fonts": "4.0.531",
|
||||||
|
"@remotion/google-fonts": "4.0.531",
|
||||||
|
"@remotion/transitions": "4.0.531",
|
||||||
|
"culori": "^4.0.2",
|
||||||
|
"lucide-react": "^1.49.0",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"remotion": "4.0.531",
|
||||||
|
"zod": "4.5.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/culori": "^4.0.1",
|
||||||
|
"@types/node": "22",
|
||||||
|
"@types/react": "19",
|
||||||
|
"@types/react-dom": "19",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
2811
video/pnpm-lock.yaml
generated
Normal file
2811
video/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
video/public/fonts/ClarityCity-Bold.woff2
Normal file
BIN
video/public/fonts/ClarityCity-Bold.woff2
Normal file
Binary file not shown.
BIN
video/public/fonts/ClarityCity-ExtraBold.woff2
Normal file
BIN
video/public/fonts/ClarityCity-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
video/public/fonts/ClarityCity-Medium.woff2
Normal file
BIN
video/public/fonts/ClarityCity-Medium.woff2
Normal file
Binary file not shown.
BIN
video/public/fonts/ClarityCity-Regular.woff2
Normal file
BIN
video/public/fonts/ClarityCity-Regular.woff2
Normal file
Binary file not shown.
BIN
video/public/fonts/ClarityCity-SemiBold.woff2
Normal file
BIN
video/public/fonts/ClarityCity-SemiBold.woff2
Normal file
Binary file not shown.
19
video/remotion.config.ts
Normal file
19
video/remotion.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { Config } from "@remotion/cli/config";
|
||||||
|
|
||||||
|
Config.setVideoImageFormat("jpeg");
|
||||||
|
Config.setJpegQuality(92);
|
||||||
|
Config.setOverwriteOutput(true);
|
||||||
|
Config.setConcurrency(4);
|
||||||
|
|
||||||
|
// remocn bileşenleri "@/..." yollarıyla import eder (shadcn alias'ı)
|
||||||
|
Config.overrideWebpackConfig((config) => ({
|
||||||
|
...config,
|
||||||
|
resolve: {
|
||||||
|
...config.resolve,
|
||||||
|
alias: {
|
||||||
|
...(config.resolve?.alias ?? {}),
|
||||||
|
"@": path.join(process.cwd(), "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
100
video/src/Lansman.tsx
Normal file
100
video/src/Lansman.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { linearTiming, TransitionSeries } from "@remotion/transitions";
|
||||||
|
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
||||||
|
import { pushThrough } from "@/components/remocn/push-through";
|
||||||
|
import { Gren } from "./bilesenler/Gren";
|
||||||
|
import "./fontlar";
|
||||||
|
import { useFontlarHazir } from "./fontlar-hazir";
|
||||||
|
import { pikselDalga } from "./gecisler/piksel-dalga";
|
||||||
|
import { R } from "./marka";
|
||||||
|
import { S1Gurultu } from "./sahneler/S1Gurultu";
|
||||||
|
import { haritaTiklama, S2Harita } from "./sahneler/S2Harita";
|
||||||
|
import { S3Siralama, siralamaTiklama } from "./sahneler/S3Siralama";
|
||||||
|
import { havuzTiklama, S4Havuz } from "./sahneler/S4Havuz";
|
||||||
|
import { danismanDalgaNoktasi, S5Danisman } from "./sahneler/S5Danisman";
|
||||||
|
import { degerDalgaNoktasi, S6Deger } from "./sahneler/S6Deger";
|
||||||
|
import { S7Kapanis } from "./sahneler/S7Kapanis";
|
||||||
|
import { GECIS, SAHNE } from "./zaman";
|
||||||
|
|
||||||
|
// Sahnelerin küresel başlangıç kareleri (geçişler üst üste biner)
|
||||||
|
const SIRA = [
|
||||||
|
[SAHNE.gurultu, GECIS.g1],
|
||||||
|
[SAHNE.harita, GECIS.g2],
|
||||||
|
[SAHNE.siralama, GECIS.g3],
|
||||||
|
[SAHNE.havuz, GECIS.g4],
|
||||||
|
[SAHNE.danisman, GECIS.g5],
|
||||||
|
[SAHNE.deger, GECIS.g6],
|
||||||
|
] as const;
|
||||||
|
const BASLAR = SIRA.reduce<number[]>((acc, [s, g]) => [...acc, acc[acc.length - 1] + s - g], [0]);
|
||||||
|
const KAPANIS_BASI = BASLAR[6];
|
||||||
|
|
||||||
|
export const Lansman: React.FC = () => {
|
||||||
|
useFontlarHazir();
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
// Koyu sahnelerde gren daha belirgin ve açık zerreli
|
||||||
|
const koyu = f < SAHNE.gurultu - 14 || f > KAPANIS_BASI + 14;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate50 }}>
|
||||||
|
<TransitionSeries>
|
||||||
|
<TransitionSeries.Sequence durationInFrames={SAHNE.gurultu}>
|
||||||
|
<S1Gurultu />
|
||||||
|
</TransitionSeries.Sequence>
|
||||||
|
{/* Tek piksel patlar: koyudan açığa, merkezden */}
|
||||||
|
<TransitionSeries.Transition
|
||||||
|
presentation={pikselDalga({ bant: 420, hucre: 64, renkler: [R.turuncuLogo, R.seftali, R.mavi] })}
|
||||||
|
timing={linearTiming({ durationInFrames: GECIS.g1 })}
|
||||||
|
/>
|
||||||
|
<TransitionSeries.Sequence durationInFrames={SAHNE.harita}>
|
||||||
|
<S2Harita />
|
||||||
|
</TransitionSeries.Sequence>
|
||||||
|
{/* İmleç logonun turuncu karesine tıklar → dalga oradan */}
|
||||||
|
<TransitionSeries.Transition
|
||||||
|
presentation={pikselDalga({ origin: haritaTiklama(), bant: 380, hucre: 60, tohum: 5 })}
|
||||||
|
timing={linearTiming({ durationInFrames: GECIS.g2 })}
|
||||||
|
/>
|
||||||
|
<TransitionSeries.Sequence durationInFrames={SAHNE.siralama}>
|
||||||
|
<S3Siralama />
|
||||||
|
</TransitionSeries.Sequence>
|
||||||
|
{/* CTA'ya basılır → sahne butonun içinden geçilerek terk edilir */}
|
||||||
|
<TransitionSeries.Transition
|
||||||
|
presentation={pushThrough({ origin: siralamaTiklama(), zoom: 3.2, blur: 16 })}
|
||||||
|
timing={linearTiming({ durationInFrames: GECIS.g3 })}
|
||||||
|
/>
|
||||||
|
<TransitionSeries.Sequence durationInFrames={SAHNE.havuz}>
|
||||||
|
<S4Havuz />
|
||||||
|
</TransitionSeries.Sequence>
|
||||||
|
{/* Listedeki satıra tıklanır → satırın içinden danışmana */}
|
||||||
|
<TransitionSeries.Transition
|
||||||
|
presentation={pushThrough({ origin: havuzTiklama(), zoom: 2.8, blur: 14 })}
|
||||||
|
timing={linearTiming({ durationInFrames: GECIS.g4 })}
|
||||||
|
/>
|
||||||
|
<TransitionSeries.Sequence durationInFrames={SAHNE.danisman}>
|
||||||
|
<S5Danisman />
|
||||||
|
</TransitionSeries.Sequence>
|
||||||
|
{/* Trendin son (2026) pikselinden dalga */}
|
||||||
|
<TransitionSeries.Transition
|
||||||
|
presentation={pikselDalga({ origin: danismanDalgaNoktasi(), bant: 400, hucre: 60, tohum: 8 })}
|
||||||
|
timing={linearTiming({ durationInFrames: GECIS.g5 })}
|
||||||
|
/>
|
||||||
|
<TransitionSeries.Sequence durationInFrames={SAHNE.deger}>
|
||||||
|
<S6Deger />
|
||||||
|
</TransitionSeries.Sequence>
|
||||||
|
{/* Son gösterge karesinden koyu dalga: kapanışın CTA bandına */}
|
||||||
|
<TransitionSeries.Transition
|
||||||
|
presentation={pikselDalga({
|
||||||
|
origin: degerDalgaNoktasi(),
|
||||||
|
bant: 440,
|
||||||
|
hucre: 64,
|
||||||
|
tohum: 11,
|
||||||
|
renkler: [R.green600, R.slate700, R.turuncu],
|
||||||
|
})}
|
||||||
|
timing={linearTiming({ durationInFrames: GECIS.g6 })}
|
||||||
|
/>
|
||||||
|
<TransitionSeries.Sequence durationInFrames={SAHNE.kapanis}>
|
||||||
|
<S7Kapanis />
|
||||||
|
</TransitionSeries.Sequence>
|
||||||
|
</TransitionSeries>
|
||||||
|
<Gren guc={koyu ? 0.24 : 0.17} koyu={koyu} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
15
video/src/Root.tsx
Normal file
15
video/src/Root.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Composition } from "remotion";
|
||||||
|
import { Lansman } from "./Lansman";
|
||||||
|
import { FPS, H, W } from "./marka";
|
||||||
|
import { TOPLAM } from "./zaman";
|
||||||
|
|
||||||
|
export const RemotionRoot: React.FC = () => (
|
||||||
|
<Composition
|
||||||
|
id="Lansman"
|
||||||
|
component={Lansman}
|
||||||
|
durationInFrames={TOPLAM}
|
||||||
|
fps={FPS}
|
||||||
|
width={W}
|
||||||
|
height={H}
|
||||||
|
/>
|
||||||
|
);
|
||||||
60
video/src/bilesenler/Gren.tsx
Normal file
60
video/src/bilesenler/Gren.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
||||||
|
import { H, W } from "../marka";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Film greni: her karede tohumu değişen fraktal gürültü (deterministik —
|
||||||
|
* aynı kare her render'da aynı greni verir). Tüm videonun üstünde durur;
|
||||||
|
* `guc` koyu sahnelerde artırılır.
|
||||||
|
*/
|
||||||
|
export const Gren: React.FC<{ guc: number; koyu?: boolean }> = ({
|
||||||
|
guc,
|
||||||
|
koyu = false,
|
||||||
|
}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
if (guc <= 0) return null;
|
||||||
|
const id = `gren-${frame}`;
|
||||||
|
return (
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
pointerEvents: "none",
|
||||||
|
mixBlendMode: koyu ? "screen" : "multiply",
|
||||||
|
opacity: guc,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width={W} height={H} viewBox={`0 0 ${W / 1.5} ${H / 1.5}`}>
|
||||||
|
<filter id={id} x="0" y="0" width="100%" height="100%">
|
||||||
|
<feTurbulence
|
||||||
|
type="fractalNoise"
|
||||||
|
baseFrequency="0.78"
|
||||||
|
numOctaves={2}
|
||||||
|
seed={frame % 997}
|
||||||
|
stitchTiles="stitch"
|
||||||
|
/>
|
||||||
|
{/* Tek kanallı gren: renkli gürültü değil */}
|
||||||
|
<feColorMatrix
|
||||||
|
type="matrix"
|
||||||
|
values="0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0 0 0 0 1"
|
||||||
|
/>
|
||||||
|
{/* Açık sahnede yalnız koyu zerreler (multiply), koyuda yalnız açık
|
||||||
|
zerreler (screen) kalsın diye ortalama kaydırılıp kontrast verilir */}
|
||||||
|
<feComponentTransfer>
|
||||||
|
<feFuncR type="linear" slope={2.6} intercept={koyu ? -1.25 : -0.35} />
|
||||||
|
<feFuncG type="linear" slope={2.6} intercept={koyu ? -1.25 : -0.35} />
|
||||||
|
<feFuncB type="linear" slope={2.6} intercept={koyu ? -1.25 : -0.35} />
|
||||||
|
</feComponentTransfer>
|
||||||
|
</filter>
|
||||||
|
<rect width="100%" height="100%" filter={`url(#${id})`} />
|
||||||
|
</svg>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Koyu sahnelerde kenar kararması */
|
||||||
|
export const Vinyet: React.FC<{ guc?: number }> = ({ guc = 0.55 }) => (
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
pointerEvents: "none",
|
||||||
|
background: `radial-gradient(ellipse 75% 70% at 50% 50%, rgba(0,0,0,0) 45%, rgba(2,6,23,${guc}) 100%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
84
video/src/bilesenler/Imlec.tsx
Normal file
84
video/src/bilesenler/Imlec.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { useCurrentFrame } from "remotion";
|
||||||
|
import { Cursor } from "@/components/remocn/cursor";
|
||||||
|
import {
|
||||||
|
cursorPathAt,
|
||||||
|
type CursorWaypoint,
|
||||||
|
} from "@/components/remocn/use-cursor-path";
|
||||||
|
import { R } from "../marka";
|
||||||
|
import { aralik } from "../yardimci";
|
||||||
|
|
||||||
|
export type Yol = CursorWaypoint[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remocn Cursor + useCursorPath, sitenin diline göre: koyu ok, beyaz kontur,
|
||||||
|
* tıklamada turuncu halka. Tıklanabilir bir şeyin üstündeyken el imlecine
|
||||||
|
* döner (sitede butonlar `cursor-pointer`).
|
||||||
|
*/
|
||||||
|
export const Imlec: React.FC<{
|
||||||
|
yol: Yol;
|
||||||
|
/** [görünmeye başladığı kare, kaybolduğu kare] */
|
||||||
|
gorunur?: [number, number];
|
||||||
|
/** El imleci gösterilecek kare aralıkları */
|
||||||
|
el?: [number, number][];
|
||||||
|
boy?: number;
|
||||||
|
}> = ({ yol, gorunur = [0, 1e9], el = [], boy = 46 }) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const st = cursorPathAt(yol, frame);
|
||||||
|
const op =
|
||||||
|
aralik(frame, gorunur[0], gorunur[0] + 8) *
|
||||||
|
(1 - aralik(frame, gorunur[1] - 8, gorunur[1]));
|
||||||
|
const elMi = el.some(([a, b]) => frame >= a && frame <= b);
|
||||||
|
if (op <= 0) return null;
|
||||||
|
// El imlecinin sıcak noktası parmak ucunda (viewBox 24'te x≈9.5): tıklama
|
||||||
|
// noktası ok ucuyla aynı kalsın diye kaydırılır.
|
||||||
|
const kaydir = elMi ? (9.5 / 24) * boy : (1 / 24) * boy;
|
||||||
|
return (
|
||||||
|
<div style={{ position: "absolute", inset: 0, opacity: op, pointerEvents: "none" }}>
|
||||||
|
<Cursor
|
||||||
|
style={{ ...st, x: st.x - kaydir, y: st.y - (1 / 24) * boy }}
|
||||||
|
variant={elMi ? "pointer" : "arrow"}
|
||||||
|
size={boy}
|
||||||
|
theme={{ foreground: R.slate900, background: R.beyaz, primary: R.turuncu }}
|
||||||
|
rippleColor={R.turuncu}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IsiOrnek = { x: number; y: number; w: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sitenin imleç ısı fırçası (use-pixel-heat.ts) video karşılığı: imleç
|
||||||
|
* yaklaşınca pikseller anında ısınır, uzaklaşınca yavaş soğur. İmleç yolu saf
|
||||||
|
* bir fonksiyon olduğu için geçmiş kareler yeniden hesaplanır — iz
|
||||||
|
* deterministiktir.
|
||||||
|
*/
|
||||||
|
export function isiIzi(
|
||||||
|
yol: Yol,
|
||||||
|
frame: number,
|
||||||
|
gorunur: [number, number] = [0, 1e9],
|
||||||
|
iz = 26,
|
||||||
|
): IsiOrnek[] {
|
||||||
|
const out: IsiOrnek[] = [];
|
||||||
|
for (let k = 0; k <= iz; k += 2) {
|
||||||
|
const f = frame - k;
|
||||||
|
if (f < gorunur[0] || f > gorunur[1]) continue;
|
||||||
|
const p = cursorPathAt(yol, f);
|
||||||
|
out.push({ x: p.x, y: p.y, w: k === 0 ? 1 : (1 - k / (iz + 2)) ** 1.7 });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isi(x: number, y: number, izler: IsiOrnek[], r: number) {
|
||||||
|
let h = 0;
|
||||||
|
const r2 = r * r;
|
||||||
|
for (const o of izler) {
|
||||||
|
const dx = x - o.x;
|
||||||
|
const dy = y - o.y;
|
||||||
|
const v = Math.exp(-(dx * dx + dy * dy) / r2) * o.w;
|
||||||
|
if (v > h) h = v;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { cursorPathAt };
|
||||||
48
video/src/bilesenler/Kinetik.tsx
Normal file
48
video/src/bilesenler/Kinetik.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useCurrentFrame } from "remotion";
|
||||||
|
import { aralik, ease } from "../yardimci";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metin çıkışı (remocn blur-out-up'ın sarmalayıcı hâli): içerik
|
||||||
|
* [bas, bas+sure] arasında yukarı kayar, bulanıklaşır ve söner.
|
||||||
|
* `giris` verilirse aynı yoldan tersine belirir.
|
||||||
|
*/
|
||||||
|
export const Cikis: React.FC<{
|
||||||
|
bas: number;
|
||||||
|
sure?: number;
|
||||||
|
giris?: [number, number];
|
||||||
|
yol?: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}> = ({ bas, sure = 10, giris, yol = 34, children, style }) => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const c = aralik(f, bas, bas + sure, ease.in);
|
||||||
|
const g = giris ? aralik(f, giris[0], giris[1], ease.out) : 1;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
opacity: g * (1 - c),
|
||||||
|
translate: `0 ${(1 - g) * yol * 0.6 - c * yol}px`,
|
||||||
|
filter: `blur(${(1 - g) * 10 + c * 12}px)`,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Mutlak konumlu kutu: kinetik bileşenler (inset:0 ortalar) bu kutuya göre ortalanır */
|
||||||
|
export const Kutu: React.FC<{
|
||||||
|
x?: number;
|
||||||
|
y: number;
|
||||||
|
w?: number;
|
||||||
|
h: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}> = ({ x = 0, y, w = 1920, h, children, style }) => (
|
||||||
|
<div style={{ position: "absolute", left: x, top: y, width: w, height: h, ...style }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
94
video/src/bilesenler/Logo.tsx
Normal file
94
video/src/bilesenler/Logo.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { LOGO_KARELER, R } from "../marka";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logo işareti: üç kare (public/kolay-tercih-mark.svg). `kareStil` ile her
|
||||||
|
* kare ayrı ayrı oynatılabilir (animasyonlu-mark.tsx'teki "rota"/"nabız").
|
||||||
|
*/
|
||||||
|
export const LogoIsaret: React.FC<{
|
||||||
|
boy: number;
|
||||||
|
kareStil?: (i: number) => { dx?: number; dy?: number; olcek?: number; opaklik?: number };
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}> = ({ boy, kareStil, style }) => (
|
||||||
|
<svg width={boy} height={boy} viewBox="0 0 180 180" style={{ overflow: "visible", ...style }}>
|
||||||
|
{LOGO_KARELER.map((k, i) => {
|
||||||
|
const s = kareStil?.(i) ?? {};
|
||||||
|
const cx = k.x + k.boy / 2 + (s.dx ?? 0);
|
||||||
|
const cy = k.y + k.boy / 2 + (s.dy ?? 0);
|
||||||
|
const o = s.olcek ?? 1;
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={i}
|
||||||
|
x={cx - (k.boy * o) / 2}
|
||||||
|
y={cy - (k.boy * o) / 2}
|
||||||
|
width={k.boy * o}
|
||||||
|
height={k.boy * o}
|
||||||
|
rx={13 * o}
|
||||||
|
fill={k.renk}
|
||||||
|
opacity={s.opaklik ?? 1}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** "Kolay" Bricolage 300 + "Tercih" 600, tracking -0.05em (logo svg'si) */
|
||||||
|
export const Kelime: React.FC<{ boy: number; renk?: string }> = ({
|
||||||
|
boy,
|
||||||
|
renk = "#0a0a0a",
|
||||||
|
}) => (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: FONT.vurgu,
|
||||||
|
fontSize: boy,
|
||||||
|
letterSpacing: "-0.05em",
|
||||||
|
color: renk,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontWeight: 300 }}>Kolay</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>Tercih</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Rozet ikonu PixelMark (pixel-decor.tsx): 2×2 piksel, sağ üst turuncu */
|
||||||
|
export const PikselIsaret: React.FC<{ boy: number }> = ({ boy }) => (
|
||||||
|
<svg width={boy} height={boy} viewBox="0 0 20 20" style={{ flexShrink: 0 }}>
|
||||||
|
<rect x={1.3} y={1.3} width={7.4} height={7.4} rx={2} fill={R.primary} fillOpacity={0.35} />
|
||||||
|
<rect x={11.3} y={1.3} width={7.4} height={7.4} rx={2} fill={R.turuncu} fillOpacity={0.85} />
|
||||||
|
<rect x={11.3} y={11.3} width={7.4} height={7.4} rx={2} fill={R.primary} fillOpacity={0.18} />
|
||||||
|
<rect x={1.3} y={11.3} width={7.4} height={7.4} rx={2} fill={R.primary} fillOpacity={0.6} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SectionEyebrow'un video karşılığı (2×): piksel imzalı rozet.
|
||||||
|
* `inline-flex items-center gap-2 rounded-full border border-slate-200
|
||||||
|
* bg-white px-3.5 py-1.5 text-xs font-semibold text-primary`
|
||||||
|
*/
|
||||||
|
export const Rozet: React.FC<{ children: React.ReactNode; style?: React.CSSProperties }> = ({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}) => (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 16,
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `2px solid ${R.slate200}`,
|
||||||
|
background: R.beyaz,
|
||||||
|
padding: "12px 28px",
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: R.primary,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PikselIsaret boy={28} />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
55
video/src/bilesenler/PikselZemin.tsx
Normal file
55
video/src/bilesenler/PikselZemin.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useCurrentFrame } from "remotion";
|
||||||
|
import { H, PIKSEL, R, W, opaklikFor } from "../marka";
|
||||||
|
import { clamp01, hash, karis } from "../yardimci";
|
||||||
|
import { isi, type IsiOrnek } from "./Imlec";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sahne kenarlarında hero dokusu (PikselZemin'in video hâli): ortası boş,
|
||||||
|
* kenara yaklaştıkça sıklaşan noktalar. İmleç izi verilirse pikseller
|
||||||
|
* turuncuya ısınır. Yavaş kayma (parallax) derinlik verir.
|
||||||
|
*/
|
||||||
|
export const PikselZemin: React.FC<{
|
||||||
|
hucre?: number;
|
||||||
|
tohum?: number;
|
||||||
|
renk?: string;
|
||||||
|
/** Kenar bandı: kısa kenarın oranı */
|
||||||
|
kenar?: number;
|
||||||
|
izler?: IsiOrnek[];
|
||||||
|
kayma?: number;
|
||||||
|
guc?: number;
|
||||||
|
}> = ({ hucre = 40, tohum = 7, renk = R.primary, kenar = 0.26, izler = [], kayma = 0.25, guc = 1 }) => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const bant = Math.min(W, H) * kenar;
|
||||||
|
const dy = -f * kayma;
|
||||||
|
const sutun = Math.ceil(W / hucre);
|
||||||
|
const satir = Math.ceil(H / hucre) + 2;
|
||||||
|
const rects: React.ReactNode[] = [];
|
||||||
|
for (let j = 0; j < satir; j++) {
|
||||||
|
for (let i = 0; i < sutun; i++) {
|
||||||
|
const cx = i * hucre + hucre / 2;
|
||||||
|
const cy = j * hucre + hucre / 2 + (dy % hucre);
|
||||||
|
const kenarUz = Math.min(cx, W - cx, cy, H - cy);
|
||||||
|
const yogunluk = clamp01(1 - kenarUz / bant) ** 1.6;
|
||||||
|
if (hash(i, j, tohum) > yogunluk * 0.9) continue;
|
||||||
|
const h = izler.length ? isi(cx, cy, izler, 110) : 0;
|
||||||
|
const boy = hucre * PIKSEL.nokta;
|
||||||
|
rects.push(
|
||||||
|
<rect
|
||||||
|
key={`${i}-${j}`}
|
||||||
|
x={cx - boy / 2}
|
||||||
|
y={cy - boy / 2}
|
||||||
|
width={boy}
|
||||||
|
height={boy}
|
||||||
|
rx={boy * PIKSEL.rx}
|
||||||
|
fill={karis(renk, R.turuncu, h)}
|
||||||
|
fillOpacity={(opaklikFor(i, j) * 0.9 + h * 0.6) * guc}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<svg width={W} height={H} style={{ position: "absolute", inset: 0 }}>
|
||||||
|
{rects}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
200
video/src/bilesenler/UI.tsx
Normal file
200
video/src/bilesenler/UI.tsx
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import { Caret } from "@/components/remocn/caret";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
import { R } from "../marka";
|
||||||
|
import { DILIM_ETIKET, type Dilim } from "../veri";
|
||||||
|
import { karis } from "../yardimci";
|
||||||
|
|
||||||
|
// Arayüz parçaları sitenin 2 katı ölçüde (DESIGN.md §4). Durumlar (hover,
|
||||||
|
// basma, seçim) 0–1 arası sayı alır ki kareler arasında yumuşak geçsin.
|
||||||
|
|
||||||
|
/** Turuncu funnel CTA (§4.1): metin önce, ArrowRight sonda, yıldız yok */
|
||||||
|
export const CtaButon: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
hover?: number;
|
||||||
|
bas?: number;
|
||||||
|
yukseklik?: number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}> = ({ children, hover = 0, bas = 0, yukseklik = 104, style }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: yukseklik,
|
||||||
|
padding: `0 ${yukseklik * 0.52}px`,
|
||||||
|
borderRadius: 999,
|
||||||
|
background: karis(R.turuncu, R.turuncu600, hover),
|
||||||
|
color: R.beyaz,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 16,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: yukseklik * 0.31,
|
||||||
|
fontWeight: 600,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
scale: `${1 - 0.03 * bas}`,
|
||||||
|
boxShadow: "0 2px 6px rgba(15,23,42,0.08)",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ArrowRight size={yukseklik * 0.31} strokeWidth={2.4} aria-hidden />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Form alanı hap (§4.6): 48 px → 104 px, tam yuvarlak, slate-200 kenarlık */
|
||||||
|
export const GirisHap: React.FC<{
|
||||||
|
genislik: number;
|
||||||
|
deger: string;
|
||||||
|
placeholder: string;
|
||||||
|
odak?: number;
|
||||||
|
yaziyor?: boolean;
|
||||||
|
ikon?: React.ReactNode;
|
||||||
|
yukseklik?: number;
|
||||||
|
}> = ({ genislik, deger, placeholder, odak = 0, yaziyor = false, ikon, yukseklik = 104 }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: genislik,
|
||||||
|
height: yukseklik,
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `2px solid ${karis(R.slate200, R.primary, odak)}`,
|
||||||
|
boxShadow: `0 0 0 ${6 * odak}px color-mix(in oklab, ${R.primary} 22%, transparent)`,
|
||||||
|
background: R.beyaz,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 20,
|
||||||
|
padding: `0 ${yukseklik * 0.4}px`,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: yukseklik * 0.31,
|
||||||
|
color: deger ? R.slate900 : R.slate400,
|
||||||
|
fontWeight: deger ? 600 : 400,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ikon}
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center" }}>
|
||||||
|
{/* Boş alanda imleç placeholder'ın başında, doluyken metnin sonunda */}
|
||||||
|
{odak > 0.5 && !deger && (
|
||||||
|
<Caret color={R.primary} width={3} height={yukseklik * 0.36} blink blinkPerSecond={1.6} />
|
||||||
|
)}
|
||||||
|
{deger || placeholder}
|
||||||
|
{odak > 0.5 && deger && (
|
||||||
|
<Caret
|
||||||
|
color={R.primary}
|
||||||
|
width={3}
|
||||||
|
height={yukseklik * 0.36}
|
||||||
|
blink={!yaziyor}
|
||||||
|
blinkPerSecond={1.6}
|
||||||
|
marginLeft={4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Etkileşimli çip (§4.17): pasif beyaz, seçili mavi dolu */
|
||||||
|
export const Cip: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
secili?: number;
|
||||||
|
hover?: number;
|
||||||
|
bas?: number;
|
||||||
|
ikon?: React.ReactNode;
|
||||||
|
boy?: number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}> = ({ children, secili = 0, hover = 0, bas = 0, ikon, boy = 80, style }) => {
|
||||||
|
const pasifKenar = karis(R.slate200, R.turuncu, hover);
|
||||||
|
const pasifYazi = karis(R.slate700, R.turuncu, hover);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
height: boy,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: boy * 0.15,
|
||||||
|
padding: `0 ${boy * 0.35}px`,
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `2px solid ${karis(pasifKenar, R.primary, secili)}`,
|
||||||
|
background: karis(karis(R.beyaz, R.turuncu50, hover), R.primary, secili),
|
||||||
|
color: karis(pasifYazi, R.beyaz, secili),
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: boy * 0.35,
|
||||||
|
fontWeight: 600,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
scale: `${1 - 0.03 * bas}`,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ikon}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Dilim etiketi (§4.5 düzeni): nokta + metin; renk tek başına anlam taşımaz */
|
||||||
|
export const DILIM_RENK: Record<Dilim, { nokta: string; yazi: string }> = {
|
||||||
|
hayal: { nokta: R.red500, yazi: R.red600 },
|
||||||
|
dengeli: { nokta: R.amber500, yazi: R.amber600 },
|
||||||
|
garanti: { nokta: R.emerald500, yazi: R.emerald600 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DilimEtiketi: React.FC<{ dilim: Dilim; boy?: number }> = ({ dilim, boy = 26 }) => (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: boy * 0.4,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: boy,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: DILIM_RENK[dilim].yazi,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: boy * 0.62,
|
||||||
|
height: boy * 0.62,
|
||||||
|
borderRadius: 999,
|
||||||
|
background: DILIM_RENK[dilim].nokta,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{DILIM_ETIKET[dilim]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Piksel trend: 6 yılın taban sırası, her yıl bir piksel. Üstte = daha iyi
|
||||||
|
* (küçük) sıra. Son yıl turuncu yanar (PixelSira'nın dili).
|
||||||
|
*/
|
||||||
|
export const PikselTrend: React.FC<{
|
||||||
|
sira: number[];
|
||||||
|
hucre?: number;
|
||||||
|
yukseklik?: number;
|
||||||
|
ilerleme?: number;
|
||||||
|
}> = ({ sira, hucre = 18, yukseklik = 56, ilerleme = 1 }) => {
|
||||||
|
const min = Math.min(...sira);
|
||||||
|
const max = Math.max(...sira);
|
||||||
|
const boy = hucre * 0.74;
|
||||||
|
return (
|
||||||
|
<svg width={sira.length * hucre} height={yukseklik + boy} style={{ overflow: "visible" }}>
|
||||||
|
{sira.map((s, i) => {
|
||||||
|
const t = max === min ? 0.5 : (s - min) / (max - min);
|
||||||
|
const gorunur = ilerleme * sira.length > i;
|
||||||
|
const son = i === sira.length - 1;
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={i}
|
||||||
|
x={i * hucre + (hucre - boy) / 2}
|
||||||
|
y={t * yukseklik}
|
||||||
|
width={boy}
|
||||||
|
height={boy}
|
||||||
|
rx={boy * 0.27}
|
||||||
|
fill={son ? R.turuncu : R.primary}
|
||||||
|
fillOpacity={gorunur ? (son ? 0.9 : 0.35 + 0.1 * i) : 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
69
video/src/components/remocn/caret.tsx
Normal file
69
video/src/components/remocn/caret.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { CSSProperties } from "react";
|
||||||
|
import { useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export interface CaretProps {
|
||||||
|
color?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
radius?: number;
|
||||||
|
opacity?: number;
|
||||||
|
blink?: boolean;
|
||||||
|
blinkPerSecond?: number;
|
||||||
|
speed?: number;
|
||||||
|
marginLeft?: number;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function caretBlinkOpacity(
|
||||||
|
frame: number,
|
||||||
|
opts: { fps: number; blinkPerSecond: number; speed: number },
|
||||||
|
): number {
|
||||||
|
const cycles = opts.blinkPerSecond <= 0 ? 1 : opts.blinkPerSecond;
|
||||||
|
const halfPeriod = opts.fps / cycles / 2;
|
||||||
|
if (halfPeriod <= 0) return 1;
|
||||||
|
return Math.floor((frame * opts.speed) / halfPeriod) % 2 === 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Caret({
|
||||||
|
color = "currentColor",
|
||||||
|
width = 2,
|
||||||
|
height = 18,
|
||||||
|
radius = 1,
|
||||||
|
opacity,
|
||||||
|
blink = false,
|
||||||
|
blinkPerSecond = 1,
|
||||||
|
speed = 1,
|
||||||
|
marginLeft = 0,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
}: CaretProps) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
const resolvedOpacity =
|
||||||
|
opacity !== undefined
|
||||||
|
? opacity
|
||||||
|
: blink
|
||||||
|
? caretBlinkOpacity(frame, { fps, blinkPerSecond, speed })
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
flexShrink: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
borderRadius: radius,
|
||||||
|
background: color,
|
||||||
|
opacity: resolvedOpacity,
|
||||||
|
marginLeft,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
video/src/components/remocn/cursor.tsx
Normal file
115
video/src/components/remocn/cursor.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { type RemocnTheme, useRemocnTheme } from "@/lib/remocn-ui";
|
||||||
|
|
||||||
|
export type CursorVariant = "arrow" | "pointer";
|
||||||
|
|
||||||
|
export interface CursorStyle {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
scale: number;
|
||||||
|
rippleOpacity: number;
|
||||||
|
rippleScale: number;
|
||||||
|
pressScale?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CursorProps {
|
||||||
|
style?: CursorStyle;
|
||||||
|
variant?: CursorVariant;
|
||||||
|
size?: number;
|
||||||
|
theme?: Partial<RemocnTheme>;
|
||||||
|
rippleColor?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REST: CursorStyle = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
rippleOpacity: 0,
|
||||||
|
rippleScale: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ARROW_PATH =
|
||||||
|
"M1 1 L1 18.5 L5.6 14.4 L8.7 21.6 L11.6 20.4 L8.5 13.2 L14.5 13.2 Z";
|
||||||
|
const POINTER_PATH =
|
||||||
|
"M8 2.2 C8 1.3 8.7 0.7 9.5 0.7 C10.3 0.7 11 1.3 11 2.2 L11 9 " +
|
||||||
|
"C11 9 11.3 8.4 12.2 8.4 C13 8.4 13.4 9 13.4 9.6 " +
|
||||||
|
"C13.4 9.6 13.9 9.1 14.7 9.1 C15.5 9.1 15.9 9.7 15.9 10.3 " +
|
||||||
|
"C15.9 10.3 16.4 9.9 17.1 9.9 C17.9 9.9 18.3 10.5 18.3 11.2 " +
|
||||||
|
"L18.3 16.8 C18.3 19.8 16.3 22.3 12.8 22.3 L11.4 22.3 " +
|
||||||
|
"C9 22.3 7.9 21.2 6.6 19.2 L4 15.2 C3.5 14.4 3.7 13.4 4.5 12.9 " +
|
||||||
|
"C5.1 12.5 5.9 12.6 6.4 13.2 L8 15 Z";
|
||||||
|
|
||||||
|
export function Cursor({
|
||||||
|
style,
|
||||||
|
variant = "arrow",
|
||||||
|
size = 28,
|
||||||
|
theme: themeOverride,
|
||||||
|
rippleColor,
|
||||||
|
className,
|
||||||
|
}: CursorProps) {
|
||||||
|
const theme = useRemocnTheme(themeOverride, "light");
|
||||||
|
const v = style ?? REST;
|
||||||
|
|
||||||
|
const press = v.pressScale ?? 1;
|
||||||
|
const pressedScale = v.scale * (0.9 + 0.1 * press);
|
||||||
|
|
||||||
|
const ring = rippleColor ?? theme.primary;
|
||||||
|
const fill = theme.foreground;
|
||||||
|
const stroke = theme.background;
|
||||||
|
const rippleSize = size * 1.8;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: `translate(${v.x}px, ${v.y}px)`,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: -rippleSize / 2,
|
||||||
|
top: -rippleSize / 2,
|
||||||
|
width: rippleSize,
|
||||||
|
height: rippleSize,
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: `2px solid ${ring}`,
|
||||||
|
opacity: v.rippleOpacity,
|
||||||
|
transform: `scale(${v.rippleScale})`,
|
||||||
|
transformOrigin: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{}
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: `scale(${pressedScale})`,
|
||||||
|
transformOrigin: "0 0",
|
||||||
|
filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.3))",
|
||||||
|
overflow: "visible",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d={variant === "pointer" ? POINTER_PATH : ARROW_PATH}
|
||||||
|
fill={fill}
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={1.4}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
video/src/components/remocn/focus-blur-resolve.tsx
Normal file
115
video/src/components/remocn/focus-blur-resolve.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export interface FocusBlurResolveProps {
|
||||||
|
text: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
blur?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FocusBlurResolve({
|
||||||
|
text,
|
||||||
|
blur = 14,
|
||||||
|
fontSize = 72,
|
||||||
|
color = "#171717",
|
||||||
|
fontWeight = 600,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
fontFamily = "sans-serif",
|
||||||
|
}: FocusBlurResolveProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
const { durationInFrames } = useVideoConfig();
|
||||||
|
|
||||||
|
const enterDur = 23;
|
||||||
|
const exitDur = 16;
|
||||||
|
const exitStart = Math.max(enterDur, durationInFrames - exitDur);
|
||||||
|
|
||||||
|
const enterEasing = Easing.bezier(0.22, 1, 0.36, 1);
|
||||||
|
const exitEasing = Easing.bezier(0.64, 0, 0.78, 0);
|
||||||
|
|
||||||
|
const enterP = interpolate(frame, [0, enterDur], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: enterEasing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitP = interpolate(frame, [exitStart, durationInFrames], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: exitEasing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const opacity = enterP * (1 - exitP);
|
||||||
|
|
||||||
|
const yEnter = interpolate(frame, [0, enterDur], [14, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: enterEasing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const yExit = interpolate(frame, [exitStart, durationInFrames], [0, -10], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: exitEasing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const y = yEnter + yExit;
|
||||||
|
|
||||||
|
const blurEnter = interpolate(frame, [0, enterDur], [blur, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: enterEasing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blurExit = interpolate(frame, [exitStart, durationInFrames], [0, 10], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: exitEasing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blurVal = blurEnter + blurExit;
|
||||||
|
|
||||||
|
const scale = interpolate(frame, [0, enterDur], [1.01, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: enterEasing,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing: "-0.03em",
|
||||||
|
fontFamily,
|
||||||
|
display: "inline-block",
|
||||||
|
transformOrigin: "50%",
|
||||||
|
opacity,
|
||||||
|
translate: `0 ${y}px`,
|
||||||
|
scale: `${scale}`,
|
||||||
|
filter: `blur(${blurVal}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
243
video/src/components/remocn/kinetic-center-build.tsx
Normal file
243
video/src/components/remocn/kinetic-center-build.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { type CSSProperties, useMemo } from "react";
|
||||||
|
import { Easing, interpolate, useCurrentFrame } from "remotion";
|
||||||
|
|
||||||
|
export interface KineticCenterBuildProps {
|
||||||
|
text: string;
|
||||||
|
entryOffset?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
letterSpacing?: number;
|
||||||
|
gap?: number;
|
||||||
|
/** "harf": harfler sağdan girip satırı iter (upstream davranışı);
|
||||||
|
* "kelime": uzun cümlelerde kelime kelime */
|
||||||
|
birim?: "harf" | "kelime";
|
||||||
|
/** Vurgulanan kelimeler (kelime indeksi, iki modda da) ve yazı stili */
|
||||||
|
vurgu?: { indices: number[]; style: KineticWordStyle };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KineticWordStyle {
|
||||||
|
fontFamily?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
fontStyle?: "normal" | "italic";
|
||||||
|
color?: string;
|
||||||
|
letterSpacing?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Ölçüm, kelimenin gerçekte basılacağı yazı tipiyle yapılır (Cal Sans,
|
||||||
|
// Bricolage…); aksi hâlde itme sırasında kelimeler üst üste biner.
|
||||||
|
function measureWidths(
|
||||||
|
words: string[],
|
||||||
|
fontSize: number,
|
||||||
|
styleFor: (i: number) => Required<KineticWordStyle>,
|
||||||
|
): number[] {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
return words.map((w) => w.length * fontSize * 0.55);
|
||||||
|
}
|
||||||
|
const ctx = document.createElement("canvas").getContext("2d");
|
||||||
|
if (!ctx) return words.map((w) => w.length * fontSize * 0.55);
|
||||||
|
return words.map((w, i) => {
|
||||||
|
const st = styleFor(i);
|
||||||
|
ctx.font = `${st.fontStyle} ${st.fontWeight} ${fontSize}px "${st.fontFamily}"`;
|
||||||
|
return ctx.measureText(w).width + st.letterSpacing * fontSize * Array.from(w).length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KineticCenterBuild({
|
||||||
|
text,
|
||||||
|
entryOffset = 88,
|
||||||
|
fontSize = 72,
|
||||||
|
color = "#171717",
|
||||||
|
fontWeight = 600,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
fontFamily = "sans-serif",
|
||||||
|
letterSpacing = -0.03,
|
||||||
|
gap: gapProp,
|
||||||
|
vurgu,
|
||||||
|
birim = "harf",
|
||||||
|
}: KineticCenterBuildProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
|
||||||
|
const words = useMemo(
|
||||||
|
() => (birim === "kelime" ? text.split(" ") : Array.from(text)),
|
||||||
|
[text, birim],
|
||||||
|
);
|
||||||
|
// Her birimin ait olduğu kelime (vurgu kelime indeksiyle verilir)
|
||||||
|
const kelimeOf = useMemo(() => {
|
||||||
|
if (birim === "kelime") return words.map((_, i) => i);
|
||||||
|
let w = 0;
|
||||||
|
return words.map((ch) => {
|
||||||
|
const k = w;
|
||||||
|
if (ch === " ") w++;
|
||||||
|
return k;
|
||||||
|
});
|
||||||
|
}, [words, birim]);
|
||||||
|
const styleFor = (i: number): Required<KineticWordStyle> => {
|
||||||
|
const base = { fontFamily, fontWeight, fontStyle: "normal" as const, color, letterSpacing };
|
||||||
|
return vurgu?.indices.includes(kelimeOf[i]) ? { ...base, ...vurgu.style } : base;
|
||||||
|
};
|
||||||
|
const widths = useMemo(
|
||||||
|
() => measureWidths(words, fontSize, styleFor),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[words, fontSize, fontWeight, fontFamily, letterSpacing, vurgu, kelimeOf],
|
||||||
|
);
|
||||||
|
|
||||||
|
const gap = gapProp ?? (birim === "kelime" ? fontSize * 0.26 : 0);
|
||||||
|
const firstDur = 10;
|
||||||
|
const pushDur = 13;
|
||||||
|
const travelDur = 7;
|
||||||
|
const entryScale = 0.992;
|
||||||
|
const entryBlur = 3.5;
|
||||||
|
const reflowBlur = 0.8;
|
||||||
|
const firstWordY = 6;
|
||||||
|
const easing = Easing.bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
|
||||||
|
const positionsAt = useMemo(() => {
|
||||||
|
const out: number[][] = [];
|
||||||
|
for (let k = 1; k <= words.length; k++) {
|
||||||
|
let total = gap * (k - 1);
|
||||||
|
for (let j = 0; j < k; j++) total += widths[j];
|
||||||
|
let cursor = -total / 2;
|
||||||
|
const xs: number[] = [];
|
||||||
|
for (let j = 0; j < k; j++) {
|
||||||
|
xs.push(cursor + widths[j] / 2);
|
||||||
|
cursor += widths[j] + gap;
|
||||||
|
}
|
||||||
|
out.push(xs);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}, [words, widths]);
|
||||||
|
|
||||||
|
const n = words.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing: `${letterSpacing}em`,
|
||||||
|
fontFamily,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{words.map((word, j) => {
|
||||||
|
const entryStart = j === 0 ? 0 : firstDur + (j - 1) * pushDur;
|
||||||
|
const entryEnd = entryStart + (j === 0 ? firstDur : pushDur);
|
||||||
|
const targetX = positionsAt[j][j];
|
||||||
|
const xFrom = j === 0 ? 0 : targetX + entryOffset;
|
||||||
|
|
||||||
|
let x = targetX;
|
||||||
|
let opacity = 1;
|
||||||
|
let scale = 1;
|
||||||
|
let blur = 0;
|
||||||
|
let y = 0;
|
||||||
|
|
||||||
|
if (frame < entryStart) {
|
||||||
|
opacity = 0;
|
||||||
|
x = xFrom;
|
||||||
|
scale = entryScale;
|
||||||
|
blur = entryBlur;
|
||||||
|
y = j === 0 ? firstWordY : 0;
|
||||||
|
} else if (frame <= entryEnd) {
|
||||||
|
const range: [number, number] = [entryStart, entryEnd];
|
||||||
|
const travelRange: [number, number] = [
|
||||||
|
entryStart,
|
||||||
|
Math.min(entryStart + travelDur, entryEnd),
|
||||||
|
];
|
||||||
|
const opts = {
|
||||||
|
extrapolateLeft: "clamp" as const,
|
||||||
|
extrapolateRight: "clamp" as const,
|
||||||
|
easing,
|
||||||
|
};
|
||||||
|
x = interpolate(frame, travelRange, [xFrom, targetX], opts);
|
||||||
|
opacity = interpolate(frame, range, [0, 1], opts);
|
||||||
|
scale = interpolate(frame, range, [entryScale, 1], opts);
|
||||||
|
blur = interpolate(frame, range, [entryBlur, 0], opts);
|
||||||
|
y =
|
||||||
|
j === 0
|
||||||
|
? interpolate(frame, travelRange, [firstWordY, 0], opts)
|
||||||
|
: 0;
|
||||||
|
} else {
|
||||||
|
for (let w = j + 1; w < n; w++) {
|
||||||
|
const pushStart = firstDur + (w - 1) * pushDur;
|
||||||
|
const pushEnd = pushStart + pushDur;
|
||||||
|
const fromX = positionsAt[w - 1][j];
|
||||||
|
const toX = positionsAt[w][j];
|
||||||
|
if (frame >= pushEnd) {
|
||||||
|
x = toX;
|
||||||
|
} else if (frame >= pushStart) {
|
||||||
|
x = interpolate(
|
||||||
|
frame,
|
||||||
|
[pushStart, Math.min(pushStart + travelDur, pushEnd)],
|
||||||
|
[fromX, toX],
|
||||||
|
{
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
blur = interpolate(
|
||||||
|
frame,
|
||||||
|
[pushStart, (pushStart + pushEnd) / 2, pushEnd],
|
||||||
|
[0, reflowBlur, 0],
|
||||||
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
x = fromX;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const st = styleFor(j);
|
||||||
|
const wordStyle: CSSProperties = {
|
||||||
|
fontFamily: st.fontFamily,
|
||||||
|
fontWeight: st.fontWeight,
|
||||||
|
fontStyle: st.fontStyle,
|
||||||
|
color: st.color,
|
||||||
|
letterSpacing: `${st.letterSpacing}em`,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={j}
|
||||||
|
style={{
|
||||||
|
...wordStyle,
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
top: "50%",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
backfaceVisibility: "hidden",
|
||||||
|
transform: `translate(-50%, -50%) translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||||
|
filter: `blur(${blur}px)`,
|
||||||
|
opacity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
video/src/components/remocn/per-character-rise.tsx
Normal file
86
video/src/components/remocn/per-character-rise.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Easing, interpolate, useCurrentFrame } from "remotion";
|
||||||
|
|
||||||
|
export interface PerCharacterRiseProps {
|
||||||
|
text: string;
|
||||||
|
distance?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PerCharacterRise({
|
||||||
|
text,
|
||||||
|
distance = 32,
|
||||||
|
fontSize = 72,
|
||||||
|
color = "#171717",
|
||||||
|
fontWeight = 600,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
}: PerCharacterRiseProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
|
||||||
|
const chars = Array.from(text);
|
||||||
|
const charDurationFrames = 21;
|
||||||
|
const charTravelFrames = 10;
|
||||||
|
const staggerFrames = 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing: "-0.05em",
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chars.map((char, i) => {
|
||||||
|
const local = frame - i * staggerFrames;
|
||||||
|
const easing = Easing.bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
const travelEasing = Easing.bezier(0.2, 0.8, 0.6, 0.85);
|
||||||
|
const opacity = interpolate(local, [0, charDurationFrames], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing,
|
||||||
|
});
|
||||||
|
const y = interpolate(local, [0, charTravelFrames], [distance, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: travelEasing,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
whiteSpace: "pre",
|
||||||
|
backfaceVisibility: "hidden",
|
||||||
|
transformOrigin: "50% 55%",
|
||||||
|
opacity,
|
||||||
|
translate: `0 ${y}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
video/src/components/remocn/push-through.tsx
Normal file
72
video/src/components/remocn/push-through.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TransitionPresentation,
|
||||||
|
TransitionPresentationComponentProps,
|
||||||
|
} from "@remotion/transitions";
|
||||||
|
import type React from "react";
|
||||||
|
import { AbsoluteFill, Easing, interpolate } from "remotion";
|
||||||
|
|
||||||
|
const clampOpts = {
|
||||||
|
extrapolateLeft: "clamp" as const,
|
||||||
|
extrapolateRight: "clamp" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PushThroughProps = {
|
||||||
|
zoom?: number;
|
||||||
|
blur?: number;
|
||||||
|
/** İtmenin merkezi (px). İmlecin tıkladığı nokta verilir: sahne o noktanın
|
||||||
|
* içinden geçilerek terk edilir, yenisi aynı noktadan açılır. */
|
||||||
|
origin?: { x: number; y: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
const PushThroughPresentation: React.FC<
|
||||||
|
TransitionPresentationComponentProps<PushThroughProps>
|
||||||
|
> = ({
|
||||||
|
children,
|
||||||
|
presentationProgress,
|
||||||
|
presentationDirection,
|
||||||
|
passedProps,
|
||||||
|
}) => {
|
||||||
|
const { zoom = 2.4, blur = 14, origin } = passedProps;
|
||||||
|
const transformOrigin = origin ? `${origin.x}px ${origin.y}px` : "50% 50%";
|
||||||
|
const entering = presentationDirection === "entering";
|
||||||
|
const p = presentationProgress;
|
||||||
|
|
||||||
|
if (!entering) {
|
||||||
|
const exitStyle: React.CSSProperties = {
|
||||||
|
transformOrigin,
|
||||||
|
opacity: interpolate(p, [0.4, 0.62], [1, 0], {
|
||||||
|
...clampOpts,
|
||||||
|
easing: Easing.bezier(0.42, 0, 0.58, 1),
|
||||||
|
}),
|
||||||
|
scale: `${interpolate(p, [0, 0.65], [1, zoom], {
|
||||||
|
...clampOpts,
|
||||||
|
easing: Easing.in(Easing.cubic),
|
||||||
|
})}`,
|
||||||
|
filter: `blur(${interpolate(p, [0.15, 0.6], [0, blur], clampOpts)}px)`,
|
||||||
|
};
|
||||||
|
return <AbsoluteFill style={exitStyle}>{children}</AbsoluteFill>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childStyle: React.CSSProperties = {
|
||||||
|
transformOrigin,
|
||||||
|
opacity: interpolate(p, [0.3, 0.5], [0, 1], clampOpts),
|
||||||
|
scale: `${interpolate(p, [0.3, 0.88, 1], [0.68, 1.02, 1], {
|
||||||
|
...clampOpts,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
})}`,
|
||||||
|
filter: `blur(${interpolate(p, [0.3, 0.75], [blur * 0.7, 0], clampOpts)}px)`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AbsoluteFill style={childStyle}>{children}</AbsoluteFill>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pushThrough(
|
||||||
|
props: PushThroughProps = {},
|
||||||
|
): TransitionPresentation<PushThroughProps> {
|
||||||
|
return {
|
||||||
|
component: PushThroughPresentation,
|
||||||
|
props,
|
||||||
|
};
|
||||||
|
}
|
||||||
112
video/src/components/remocn/slot-machine-roll.tsx
Normal file
112
video/src/components/remocn/slot-machine-roll.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export interface SlotMachineRollProps {
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
/** true: tam kare sarmalayıcı ve beyaz zemin olmadan satır içi basar */
|
||||||
|
inline?: boolean;
|
||||||
|
/** Sütunlar arası kademe (kare) */
|
||||||
|
stagger?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nokta, virgül ve boşluk rakam kadar yer kaplamasın
|
||||||
|
const charWidth = (ch: string) =>
|
||||||
|
ch === "." || ch === "," ? "0.3em" : ch === " " ? "0.25em" : ch === "%" ? "0.8em" : "0.62em";
|
||||||
|
|
||||||
|
export function SlotMachineRoll({
|
||||||
|
from = "$99",
|
||||||
|
to = "$199",
|
||||||
|
fontSize = 120,
|
||||||
|
color = "#171717",
|
||||||
|
fontWeight = 700,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
fontFamily = "sans-serif",
|
||||||
|
inline = false,
|
||||||
|
stagger = 4,
|
||||||
|
}: SlotMachineRollProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
const len = Math.max(from.length, to.length);
|
||||||
|
const paddedFrom = from.padStart(len, " ");
|
||||||
|
const paddedTo = to.padStart(len, " ");
|
||||||
|
|
||||||
|
const columns = [];
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const raw = spring({
|
||||||
|
frame: frame - i * stagger,
|
||||||
|
fps,
|
||||||
|
config: { damping: 14 },
|
||||||
|
});
|
||||||
|
// Değişmeyen karakter (ör. "6 yıl"daki "yıl") yerinde durur
|
||||||
|
const sabit = paddedFrom[i] === paddedTo[i];
|
||||||
|
const t = sabit ? 0 : Math.max(0, Math.min(1, raw));
|
||||||
|
columns.push(
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
overflow: "hidden",
|
||||||
|
height: "1.1em",
|
||||||
|
lineHeight: "1.1em",
|
||||||
|
verticalAlign: "top",
|
||||||
|
width: paddedFrom[i] === paddedTo[i] && /[^\d\s]/.test(paddedTo[i]) ? "auto" : charWidth(paddedTo[i]),
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
translate: `0 ${-t * 1.1}em`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{paddedFrom[i]}</span>
|
||||||
|
<span>{paddedTo[i]}</span>
|
||||||
|
</span>
|
||||||
|
</span>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metin = (
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing: "-0.03em",
|
||||||
|
fontFamily,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{columns}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
if (inline) return metin;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{metin}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
video/src/components/remocn/tracking-in.tsx
Normal file
77
video/src/components/remocn/tracking-in.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export interface TrackingInProps {
|
||||||
|
text: ReactNode;
|
||||||
|
fontFamily?: string;
|
||||||
|
background?: string;
|
||||||
|
endTracking?: number;
|
||||||
|
startTracking?: number;
|
||||||
|
startBlur?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrackingIn({
|
||||||
|
text,
|
||||||
|
startTracking = 0.5,
|
||||||
|
startBlur = 12,
|
||||||
|
fontSize = 96,
|
||||||
|
color = "#171717",
|
||||||
|
fontWeight = 700,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
fontFamily = "sans-serif",
|
||||||
|
background = "transparent",
|
||||||
|
endTracking = -0.03,
|
||||||
|
}: TrackingInProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
const t = spring({
|
||||||
|
frame,
|
||||||
|
fps,
|
||||||
|
config: { damping: 18, stiffness: 90 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const letterSpacing = `${interpolate(t, [0, 1], [startTracking, endTracking])}em`;
|
||||||
|
const blurAmount = interpolate(t, [0, 1], [startBlur, 0]);
|
||||||
|
const opacity = interpolate(frame, [0, 15], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing,
|
||||||
|
opacity,
|
||||||
|
filter: `blur(${blurAmount}px)`,
|
||||||
|
fontFamily,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
video/src/components/remocn/typewriter.tsx
Normal file
72
video/src/components/remocn/typewriter.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Caret } from "@/components/remocn/caret";
|
||||||
|
import { useTypewriter } from "@/lib/remocn-ui";
|
||||||
|
|
||||||
|
export interface TypewriterProps {
|
||||||
|
text: string;
|
||||||
|
cursor?: boolean;
|
||||||
|
charsPerSecond?: number;
|
||||||
|
speed?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
cursorColor?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Typewriter({
|
||||||
|
text,
|
||||||
|
cursor = true,
|
||||||
|
charsPerSecond = 22,
|
||||||
|
speed = 1,
|
||||||
|
fontSize = 48,
|
||||||
|
color = "#171717",
|
||||||
|
cursorColor = "#171717",
|
||||||
|
fontWeight = 600,
|
||||||
|
className,
|
||||||
|
}: TypewriterProps) {
|
||||||
|
const tw = useTypewriter(text, { cps: charsPerSecond, speed });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing: "-0.03em",
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
|
||||||
|
whiteSpace: "pre",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tw.text}
|
||||||
|
{cursor && (
|
||||||
|
<Caret
|
||||||
|
color={cursorColor}
|
||||||
|
blink={!tw.typing}
|
||||||
|
speed={speed}
|
||||||
|
radius={0}
|
||||||
|
style={{
|
||||||
|
width: "0.08em",
|
||||||
|
height: "1em",
|
||||||
|
marginLeft: "0.04em",
|
||||||
|
verticalAlign: "text-bottom",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
video/src/components/remocn/use-cursor-path.ts
Normal file
133
video/src/components/remocn/use-cursor-path.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCurrentFrame } from "remotion";
|
||||||
|
import type { CursorStyle } from "@/components/remocn/cursor";
|
||||||
|
import { clamp01, type EasingName, easings } from "@/lib/remocn-ui";
|
||||||
|
|
||||||
|
export interface CursorWaypoint {
|
||||||
|
at: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
duration?: number;
|
||||||
|
click?: boolean;
|
||||||
|
press?: boolean;
|
||||||
|
easing?: EasingName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_DURATION = 24;
|
||||||
|
|
||||||
|
export const CLICK_FRAMES = 16;
|
||||||
|
|
||||||
|
export const PRESS_FRAMES = 8;
|
||||||
|
|
||||||
|
export interface CursorPathOptions {
|
||||||
|
speed?: number;
|
||||||
|
defaultDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lerp(a: number, b: number, t: number): number {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ripplePhase(framesSinceClick: number): {
|
||||||
|
rippleOpacity: number;
|
||||||
|
rippleScale: number;
|
||||||
|
} {
|
||||||
|
if (framesSinceClick < 0 || framesSinceClick >= CLICK_FRAMES) {
|
||||||
|
return { rippleOpacity: 0, rippleScale: 0 };
|
||||||
|
}
|
||||||
|
const p = clamp01(framesSinceClick / CLICK_FRAMES);
|
||||||
|
return {
|
||||||
|
rippleOpacity: 0.5 * (1 - p),
|
||||||
|
rippleScale: 2.5 * easings.out(p),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clickPress(framesSinceClick: number): number {
|
||||||
|
if (framesSinceClick < 0 || framesSinceClick >= PRESS_FRAMES) return 1;
|
||||||
|
const half = PRESS_FRAMES / 2;
|
||||||
|
const p =
|
||||||
|
framesSinceClick < half
|
||||||
|
? framesSinceClick / half
|
||||||
|
: 1 - (framesSinceClick - half) / half;
|
||||||
|
return 1 - clamp01(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCursorPath(
|
||||||
|
waypoints: CursorWaypoint[],
|
||||||
|
opts: CursorPathOptions = {},
|
||||||
|
): CursorStyle {
|
||||||
|
const raw = useCurrentFrame();
|
||||||
|
return cursorPathAt(waypoints, raw, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cursorPathAt(
|
||||||
|
waypoints: CursorWaypoint[],
|
||||||
|
raw: number,
|
||||||
|
opts: CursorPathOptions = {},
|
||||||
|
): CursorStyle {
|
||||||
|
const { speed = 1, defaultDuration = DEFAULT_DURATION } = opts;
|
||||||
|
const effectiveFrame = raw * speed;
|
||||||
|
|
||||||
|
if (waypoints.length === 0) {
|
||||||
|
return { x: 0, y: 0, scale: 1, rippleOpacity: 0, rippleScale: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = waypoints[0];
|
||||||
|
|
||||||
|
if (effectiveFrame <= first.at) {
|
||||||
|
return {
|
||||||
|
x: first.x,
|
||||||
|
y: first.y,
|
||||||
|
scale: 1,
|
||||||
|
rippleOpacity: 0,
|
||||||
|
rippleScale: 0,
|
||||||
|
pressScale: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let toIndex = waypoints.length - 1;
|
||||||
|
for (let i = 1; i < waypoints.length; i++) {
|
||||||
|
if (waypoints[i].at > effectiveFrame) {
|
||||||
|
toIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pastLast = effectiveFrame >= waypoints[waypoints.length - 1].at;
|
||||||
|
const to = pastLast ? waypoints[waypoints.length - 1] : waypoints[toIndex];
|
||||||
|
const from = pastLast
|
||||||
|
? waypoints[waypoints.length - 1]
|
||||||
|
: waypoints[toIndex - 1];
|
||||||
|
|
||||||
|
const dur = to.duration ?? defaultDuration;
|
||||||
|
const ease = easings[to.easing ?? "inOut"];
|
||||||
|
const start = to.at - dur;
|
||||||
|
const t =
|
||||||
|
pastLast || dur <= 0 ? 1 : ease(clamp01((effectiveFrame - start) / dur));
|
||||||
|
const x = lerp(from.x, to.x, t);
|
||||||
|
const y = lerp(from.y, to.y, t);
|
||||||
|
|
||||||
|
let lastClickAt = -Infinity;
|
||||||
|
for (const wp of waypoints) {
|
||||||
|
if (wp.click && wp.at <= effectiveFrame && wp.at > lastClickAt)
|
||||||
|
lastClickAt = wp.at;
|
||||||
|
}
|
||||||
|
const sinceClick =
|
||||||
|
lastClickAt === -Infinity ? -1 : effectiveFrame - lastClickAt;
|
||||||
|
const ripple = ripplePhase(sinceClick);
|
||||||
|
const clickDip = clickPress(sinceClick);
|
||||||
|
|
||||||
|
const holdWp = pastLast ? waypoints[waypoints.length - 1] : from;
|
||||||
|
const heldPress = holdWp.press ? 0 : 1;
|
||||||
|
|
||||||
|
const pressScale = Math.min(clickDip, heldPress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
scale: 1,
|
||||||
|
rippleOpacity: ripple.rippleOpacity,
|
||||||
|
rippleScale: ripple.rippleScale,
|
||||||
|
pressScale,
|
||||||
|
};
|
||||||
|
}
|
||||||
22
video/src/fontlar-hazir.ts
Normal file
22
video/src/fontlar-hazir.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { continueRender, delayRender } from "remotion";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kinetik yerleşimler metni gerçek yazı tipiyle ölçer. Yazı tipleri
|
||||||
|
* yüklenmeden ölçülürse kelimeler kayar; bu kanca yazı tipleri hazır olana
|
||||||
|
* kadar render'ı bekletir ve hazır olunca ağacı yeniden çizdirir.
|
||||||
|
*/
|
||||||
|
export function useFontlarHazir() {
|
||||||
|
const [hazir, setHazir] = useState(
|
||||||
|
() => typeof document !== "undefined" && document.fonts.status === "loaded",
|
||||||
|
);
|
||||||
|
const [handle] = useState(() => (hazir ? null : delayRender("yazı tipleri")));
|
||||||
|
useEffect(() => {
|
||||||
|
if (hazir) return;
|
||||||
|
document.fonts.ready.then(() => {
|
||||||
|
setHazir(true);
|
||||||
|
if (handle !== null) continueRender(handle);
|
||||||
|
});
|
||||||
|
}, [hazir, handle]);
|
||||||
|
return hazir;
|
||||||
|
}
|
||||||
38
video/src/fontlar.ts
Normal file
38
video/src/fontlar.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { loadFont as yerelFont } from "@remotion/fonts";
|
||||||
|
import { loadFont as bricolageYukle } from "@remotion/google-fonts/BricolageGrotesque";
|
||||||
|
import { loadFont as calSansYukle } from "@remotion/google-fonts/CalSans";
|
||||||
|
import { staticFile } from "remotion";
|
||||||
|
|
||||||
|
// Sitenin dört yazı tipinden üçü (DESIGN.md §3): Cal Sans başlık ve iri sayı,
|
||||||
|
// Clarity City gövde/buton, Bricolage vurgu. Geist Mono arayüzde yok.
|
||||||
|
const cal = calSansYukle("normal", {
|
||||||
|
weights: ["400"],
|
||||||
|
subsets: ["latin", "latin-ext"],
|
||||||
|
});
|
||||||
|
const bricolage = bricolageYukle("normal", {
|
||||||
|
weights: ["300", "400", "600", "700", "800"],
|
||||||
|
subsets: ["latin", "latin-ext"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const CLARITY = "Clarity City";
|
||||||
|
for (const [dosya, agirlik] of [
|
||||||
|
["Regular", "400"],
|
||||||
|
["Medium", "500"],
|
||||||
|
["SemiBold", "600"],
|
||||||
|
["Bold", "700"],
|
||||||
|
["ExtraBold", "800"],
|
||||||
|
] as const) {
|
||||||
|
yerelFont({
|
||||||
|
family: CLARITY,
|
||||||
|
url: staticFile(`fonts/ClarityCity-${dosya}.woff2`),
|
||||||
|
weight: agirlik,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FONT = {
|
||||||
|
/** Cal Sans yalnız 400; site `font-bold` ile sentetik kalınlık kullanıyor,
|
||||||
|
* video da aynı görünsün diye başlıklarda fontWeight 700 verilir. */
|
||||||
|
baslik: cal.fontFamily,
|
||||||
|
govde: CLARITY,
|
||||||
|
vurgu: bricolage.fontFamily,
|
||||||
|
} as const;
|
||||||
137
video/src/gecisler/piksel-dalga.tsx
Normal file
137
video/src/gecisler/piksel-dalga.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import type {
|
||||||
|
TransitionPresentation,
|
||||||
|
TransitionPresentationComponentProps,
|
||||||
|
} from "@remotion/transitions";
|
||||||
|
import { AbsoluteFill, Easing, interpolate } from "remotion";
|
||||||
|
import { H, LOGO_KARELER, PIKSEL, W } from "../marka";
|
||||||
|
import { clamp01, hash } from "../yardimci";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Piksel dalgası: yeni sahne, bir noktadan (çoğunlukla imlecin tıkladığı
|
||||||
|
* yer) yayılan iri piksellerle açılır. Dalganın önünde logonun üç rengi
|
||||||
|
* (mavi, şeftali, turuncu) yanıp söner — hero haritasındaki ısınan
|
||||||
|
* piksellerin sahne ölçeğindeki hâli. Hücreler sitenin nokta oranını
|
||||||
|
* (%74, rx %20) taşır; dalga geçince birleşip kesintisiz yüzeye döner.
|
||||||
|
*/
|
||||||
|
export type PikselDalgaProps = {
|
||||||
|
origin?: { x: number; y: number };
|
||||||
|
/** Hücre kenarı (px) */
|
||||||
|
hucre?: number;
|
||||||
|
/** Dalga önünün kalınlığı (px) */
|
||||||
|
bant?: number;
|
||||||
|
/** Dalga önündeki piksellerin renkleri */
|
||||||
|
renkler?: string[];
|
||||||
|
/** Çıkan sahne hafifçe geri çekilsin (derinlik) */
|
||||||
|
geriCekil?: number;
|
||||||
|
tohum?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VARSAYILAN_RENKLER = LOGO_KARELER.map((k) => k.renk);
|
||||||
|
|
||||||
|
const koseUzakligi = (o: { x: number; y: number }) =>
|
||||||
|
Math.max(
|
||||||
|
Math.hypot(o.x, o.y),
|
||||||
|
Math.hypot(W - o.x, o.y),
|
||||||
|
Math.hypot(o.x, H - o.y),
|
||||||
|
Math.hypot(W - o.x, H - o.y),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dalga önünün yarıçapı. Gelen sahne kendi öğelerini dalgayla eş zamanlı
|
||||||
|
* belirtmek isterse (ör. haritanın noktaları) aynı fonksiyonu kullanır.
|
||||||
|
*/
|
||||||
|
export const dalgaYaricapi = (
|
||||||
|
p: number,
|
||||||
|
origin: { x: number; y: number } = { x: W / 2, y: H / 2 },
|
||||||
|
bant = 420,
|
||||||
|
) =>
|
||||||
|
interpolate(clamp01(p), [0, 1], [0, koseUzakligi(origin) + bant * 1.25], {
|
||||||
|
easing: Easing.bezier(0.45, 0.05, 0.4, 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const PikselDalgaSunum: React.FC<
|
||||||
|
TransitionPresentationComponentProps<PikselDalgaProps>
|
||||||
|
> = ({ children, presentationProgress, presentationDirection, passedProps }) => {
|
||||||
|
const {
|
||||||
|
origin = { x: W / 2, y: H / 2 },
|
||||||
|
hucre = 64,
|
||||||
|
bant = 420,
|
||||||
|
renkler = VARSAYILAN_RENKLER,
|
||||||
|
geriCekil = 0.05,
|
||||||
|
tohum = 3,
|
||||||
|
} = passedProps;
|
||||||
|
const p = presentationProgress;
|
||||||
|
|
||||||
|
if (presentationDirection === "exiting") {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
transformOrigin: `${origin.x}px ${origin.y}px`,
|
||||||
|
scale: `${1 - geriCekil * Easing.in(Easing.cubic)(p)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sutun = Math.ceil(W / hucre);
|
||||||
|
const satir = Math.ceil(H / hucre);
|
||||||
|
const r = dalgaYaricapi(p, origin, bant);
|
||||||
|
|
||||||
|
let maske = "";
|
||||||
|
let renkli = "";
|
||||||
|
for (let j = 0; j < satir; j++) {
|
||||||
|
for (let i = 0; i < sutun; i++) {
|
||||||
|
const cx = i * hucre + hucre / 2;
|
||||||
|
const cy = j * hucre + hucre / 2;
|
||||||
|
const d = Math.hypot(cx - origin.x, cy - origin.y) + (hash(i, j, tohum) - 0.5) * bant * 0.55;
|
||||||
|
const t = clamp01((r - d) / bant);
|
||||||
|
if (t <= 0) continue;
|
||||||
|
// Önde renkli piksel belirip söner…
|
||||||
|
if (t < 0.72) {
|
||||||
|
const c = Math.sin(Math.PI * (t / 0.72));
|
||||||
|
const boy = hucre * PIKSEL.nokta * c;
|
||||||
|
const renk = renkler[Math.floor(hash(i, j, tohum + 9) * renkler.length)];
|
||||||
|
renkli += `<rect x="${cx - boy / 2}" y="${cy - boy / 2}" width="${boy}" height="${boy}" rx="${boy * PIKSEL.rx}" fill="${renk}"/>`;
|
||||||
|
}
|
||||||
|
// …arkasından yeni sahne piksel piksel açılır
|
||||||
|
const m = Easing.bezier(0.22, 1, 0.36, 1)(clamp01((t - 0.38) / 0.62));
|
||||||
|
if (m > 0) {
|
||||||
|
const boy = hucre * (PIKSEL.nokta + 0.3 * m);
|
||||||
|
const rx = hucre * PIKSEL.rx * (1 - m);
|
||||||
|
maske += `<rect x="${cx - boy / 2}" y="${cy - boy / 2}" width="${boy}" height="${boy}" rx="${rx}" fill="white"/>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskeUrl = `url("data:image/svg+xml;utf8,${encodeURIComponent(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">${maske}</svg>`,
|
||||||
|
)}")`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill>
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
maskImage: maskeUrl,
|
||||||
|
WebkitMaskImage: maskeUrl,
|
||||||
|
maskSize: "100% 100%",
|
||||||
|
WebkitMaskSize: "100% 100%",
|
||||||
|
maskRepeat: "no-repeat",
|
||||||
|
WebkitMaskRepeat: "no-repeat",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AbsoluteFill>
|
||||||
|
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
||||||
|
<svg width={W} height={H} dangerouslySetInnerHTML={{ __html: renkli }} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pikselDalga(
|
||||||
|
props: PikselDalgaProps = {},
|
||||||
|
): TransitionPresentation<PikselDalgaProps> {
|
||||||
|
return { component: PikselDalgaSunum, props };
|
||||||
|
}
|
||||||
4
video/src/index.ts
Normal file
4
video/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { registerRoot } from "remotion";
|
||||||
|
import { RemotionRoot } from "./Root";
|
||||||
|
|
||||||
|
registerRoot(RemotionRoot);
|
||||||
87
video/src/lib/remocn-ui/color.ts
Normal file
87
video/src/lib/remocn-ui/color.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
clampChroma,
|
||||||
|
converter,
|
||||||
|
formatRgb,
|
||||||
|
interpolate,
|
||||||
|
type Oklch,
|
||||||
|
parse,
|
||||||
|
type Rgb,
|
||||||
|
} from "culori";
|
||||||
|
|
||||||
|
const toRgb = converter("rgb");
|
||||||
|
const toOklch = converter("oklch");
|
||||||
|
|
||||||
|
const BLACK: Rgb = { mode: "rgb", r: 0, g: 0, b: 0, alpha: 1 };
|
||||||
|
|
||||||
|
export function parseColor(c: string): Rgb {
|
||||||
|
const s = c.trim();
|
||||||
|
|
||||||
|
if (s.startsWith("var(")) {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
console.warn(
|
||||||
|
`[remocn-ui] parseColor cannot resolve CSS variable "${s}" under Remotion's per-frame render. ` +
|
||||||
|
"Animated colors must be concrete oklch/hex/rgb values supplied via the theme." +
|
||||||
|
"Falling back to the JS default.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { ...BLACK };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rgb = toRgb(parse(s));
|
||||||
|
if (!rgb) {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
console.warn(
|
||||||
|
`[remocn-ui] parseColor could not parse "${s}"; using black.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { ...BLACK };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mode: "rgb", r: rgb.r, g: rgb.g, b: rgb.b, alpha: rgb.alpha ?? 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function oklchToRgb(l: number, c: number, h: number): Rgb {
|
||||||
|
const mapped = clampChroma({ mode: "oklch", l, c, h }, "oklch", "rgb");
|
||||||
|
return toRgb(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbToOklch(rgb: Rgb): Oklch {
|
||||||
|
const { l, c, h } = toOklch(rgb);
|
||||||
|
return { mode: "oklch", l, c, h: Number.isFinite(h) ? h : 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveColorString(s: string): string {
|
||||||
|
const trimmed = s.trim();
|
||||||
|
if (trimmed.startsWith("var(")) {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
console.warn(
|
||||||
|
`[remocn-ui] mixOklch cannot resolve CSS variable "${trimmed}" under Remotion's per-frame render. ` +
|
||||||
|
"Animated colors must be concrete oklch/hex/rgb values supplied via the theme." +
|
||||||
|
"Falling back to the JS default.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return "#000";
|
||||||
|
}
|
||||||
|
if (!parse(trimmed)) {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
console.warn(
|
||||||
|
`[remocn-ui] mixOklch could not parse "${trimmed}"; using black.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return "#000";
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mixOklch(a: string, b: string, t: number): string {
|
||||||
|
const mixed = clampChroma(
|
||||||
|
interpolate([resolveColorString(a), resolveColorString(b)], "oklch")(t),
|
||||||
|
"oklch",
|
||||||
|
"rgb",
|
||||||
|
);
|
||||||
|
return toCss(toRgb(mixed));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toCss(color: Rgb): string {
|
||||||
|
return formatRgb(color);
|
||||||
|
}
|
||||||
27
video/src/lib/remocn-ui/index.ts
Normal file
27
video/src/lib/remocn-ui/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export {
|
||||||
|
mixOklch,
|
||||||
|
oklchToRgb,
|
||||||
|
parseColor,
|
||||||
|
rgbToOklch,
|
||||||
|
toCss,
|
||||||
|
} from "./color";
|
||||||
|
export type { EasingName, SpringName } from "./motion";
|
||||||
|
export { easings, springs } from "./motion";
|
||||||
|
export type { RemocnTheme, RemocnUIProviderProps } from "./theme";
|
||||||
|
export {
|
||||||
|
defaultDarkTheme,
|
||||||
|
defaultLightTheme,
|
||||||
|
RemocnUIProvider,
|
||||||
|
useRemocnTheme,
|
||||||
|
} from "./theme";
|
||||||
|
export type { TypewriterOptions, TypewriterState } from "./timeline";
|
||||||
|
export {
|
||||||
|
clamp01,
|
||||||
|
framesFor,
|
||||||
|
revealCount,
|
||||||
|
revealedText,
|
||||||
|
useCurrentState,
|
||||||
|
useStateTransition,
|
||||||
|
useTypewriter,
|
||||||
|
} from "./timeline";
|
||||||
|
export type { Step } from "./types";
|
||||||
17
video/src/lib/remocn-ui/motion.ts
Normal file
17
video/src/lib/remocn-ui/motion.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const easings = {
|
||||||
|
linear: (t: number): number => t,
|
||||||
|
out: (t: number): number => 1 - (1 - t) ** 3,
|
||||||
|
in: (t: number): number => t * t * t,
|
||||||
|
inOut: (t: number): number =>
|
||||||
|
t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type EasingName = keyof typeof easings;
|
||||||
|
|
||||||
|
export const springs = {
|
||||||
|
snappy: { damping: 18, stiffness: 220, mass: 0.7 },
|
||||||
|
soft: { damping: 14, stiffness: 120, mass: 0.9 },
|
||||||
|
bouncy: { damping: 10, stiffness: 180, mass: 0.8 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SpringName = keyof typeof springs;
|
||||||
108
video/src/lib/remocn-ui/theme.ts
Normal file
108
video/src/lib/remocn-ui/theme.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { createContext, createElement, useContext } from "react";
|
||||||
|
|
||||||
|
export interface RemocnTheme {
|
||||||
|
background: string;
|
||||||
|
foreground: string;
|
||||||
|
card: string;
|
||||||
|
cardForeground: string;
|
||||||
|
popover: string;
|
||||||
|
popoverForeground: string;
|
||||||
|
primary: string;
|
||||||
|
primaryForeground: string;
|
||||||
|
secondary: string;
|
||||||
|
secondaryForeground: string;
|
||||||
|
muted: string;
|
||||||
|
mutedForeground: string;
|
||||||
|
accent: string;
|
||||||
|
accentForeground: string;
|
||||||
|
destructive: string;
|
||||||
|
destructiveForeground: string;
|
||||||
|
border: string;
|
||||||
|
input: string;
|
||||||
|
ring: string;
|
||||||
|
radius: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultLightTheme: RemocnTheme = {
|
||||||
|
background: "oklch(1 0 0)",
|
||||||
|
foreground: "oklch(0.145 0 0)",
|
||||||
|
card: "oklch(1 0 0)",
|
||||||
|
cardForeground: "oklch(0.145 0 0)",
|
||||||
|
popover: "oklch(1 0 0)",
|
||||||
|
popoverForeground: "oklch(0.145 0 0)",
|
||||||
|
primary: "oklch(0.205 0 0)",
|
||||||
|
primaryForeground: "oklch(0.985 0 0)",
|
||||||
|
secondary: "oklch(0.97 0 0)",
|
||||||
|
secondaryForeground: "oklch(0.205 0 0)",
|
||||||
|
muted: "oklch(0.97 0 0)",
|
||||||
|
mutedForeground: "oklch(0.556 0 0)",
|
||||||
|
accent: "oklch(0.97 0 0)",
|
||||||
|
accentForeground: "oklch(0.205 0 0)",
|
||||||
|
destructive: "oklch(0.577 0.245 27.325)",
|
||||||
|
destructiveForeground: "oklch(0.985 0 0)",
|
||||||
|
border: "oklch(0.922 0 0)",
|
||||||
|
input: "oklch(0.922 0 0)",
|
||||||
|
ring: "oklch(0.708 0 0)",
|
||||||
|
radius: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultDarkTheme: RemocnTheme = {
|
||||||
|
background: "oklch(0.145 0 0)",
|
||||||
|
foreground: "oklch(0.985 0 0)",
|
||||||
|
card: "oklch(0.205 0 0)",
|
||||||
|
cardForeground: "oklch(0.985 0 0)",
|
||||||
|
popover: "oklch(0.205 0 0)",
|
||||||
|
popoverForeground: "oklch(0.985 0 0)",
|
||||||
|
primary: "oklch(0.922 0 0)",
|
||||||
|
primaryForeground: "oklch(0.205 0 0)",
|
||||||
|
secondary: "oklch(0.269 0 0)",
|
||||||
|
secondaryForeground: "oklch(0.985 0 0)",
|
||||||
|
muted: "oklch(0.269 0 0)",
|
||||||
|
mutedForeground: "oklch(0.708 0 0)",
|
||||||
|
accent: "oklch(0.269 0 0)",
|
||||||
|
accentForeground: "oklch(0.985 0 0)",
|
||||||
|
destructive: "oklch(0.704 0.191 22.216)",
|
||||||
|
destructiveForeground: "oklch(0.985 0 0)",
|
||||||
|
border: "oklch(1 0 0 / 10%)",
|
||||||
|
input: "oklch(1 0 0 / 15%)",
|
||||||
|
ring: "oklch(0.556 0 0)",
|
||||||
|
radius: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RemocnThemeContextValue {
|
||||||
|
theme?: Partial<RemocnTheme>;
|
||||||
|
mode?: "light" | "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
const RemocnThemeContext = createContext<RemocnThemeContextValue>({});
|
||||||
|
|
||||||
|
export interface RemocnUIProviderProps {
|
||||||
|
theme?: Partial<RemocnTheme>;
|
||||||
|
mode?: "light" | "dark";
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemocnUIProvider({
|
||||||
|
theme,
|
||||||
|
mode,
|
||||||
|
children,
|
||||||
|
}: RemocnUIProviderProps) {
|
||||||
|
return createElement(
|
||||||
|
RemocnThemeContext.Provider,
|
||||||
|
{ value: { theme, mode } },
|
||||||
|
children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRemocnTheme(
|
||||||
|
override?: Partial<RemocnTheme>,
|
||||||
|
modeOverride?: "light" | "dark",
|
||||||
|
): RemocnTheme {
|
||||||
|
const ctx = useContext(RemocnThemeContext);
|
||||||
|
const mode = modeOverride ?? ctx.mode ?? "light";
|
||||||
|
const base = mode === "dark" ? defaultDarkTheme : defaultLightTheme;
|
||||||
|
return { ...base, ...ctx.theme, ...override };
|
||||||
|
}
|
||||||
96
video/src/lib/remocn-ui/timeline.ts
Normal file
96
video/src/lib/remocn-ui/timeline.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
import type { Step } from "./types";
|
||||||
|
|
||||||
|
export function framesFor(
|
||||||
|
d: number | { seconds: number },
|
||||||
|
fps: number,
|
||||||
|
): number {
|
||||||
|
return typeof d === "number" ? d : Math.round(d.seconds * fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function revealCount(
|
||||||
|
localFrame: number,
|
||||||
|
fps: number,
|
||||||
|
len: number,
|
||||||
|
cps: number,
|
||||||
|
): number {
|
||||||
|
const over = (len / cps) * fps;
|
||||||
|
if (over <= 0) return len;
|
||||||
|
return Math.max(0, Math.min(len, Math.floor((localFrame / over) * len)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clamp01(t: number): number {
|
||||||
|
return Math.max(0, Math.min(1, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function revealedText(full: string, count: number): string {
|
||||||
|
const c = Math.max(0, Math.min(full.length, Math.floor(count)));
|
||||||
|
return full.slice(0, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypewriterOptions {
|
||||||
|
cps?: number;
|
||||||
|
speed?: number;
|
||||||
|
startFrame?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypewriterState {
|
||||||
|
text: string;
|
||||||
|
count: number;
|
||||||
|
done: boolean;
|
||||||
|
typing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTypewriter(
|
||||||
|
full: string,
|
||||||
|
options: TypewriterOptions = {},
|
||||||
|
): TypewriterState {
|
||||||
|
const { cps = 20, speed = 1, startFrame = 0 } = options;
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const local = frame * speed - startFrame;
|
||||||
|
const count = local <= 0 ? 0 : revealCount(local, fps, full.length, cps);
|
||||||
|
return {
|
||||||
|
text: revealedText(full, count),
|
||||||
|
count,
|
||||||
|
done: count >= full.length,
|
||||||
|
typing: count > 0 && count < full.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCurrentState<S extends string>(
|
||||||
|
steps: Step<S>[],
|
||||||
|
defaultState: S,
|
||||||
|
speed = 1,
|
||||||
|
): S {
|
||||||
|
const effectiveFrame = useCurrentFrame() * speed;
|
||||||
|
let current = defaultState;
|
||||||
|
let bestAt = -Infinity;
|
||||||
|
steps.forEach((step) => {
|
||||||
|
if (step.at <= effectiveFrame && step.at >= bestAt) {
|
||||||
|
bestAt = step.at;
|
||||||
|
current = step.state;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStateTransition<S extends string>(
|
||||||
|
steps: Step<S>[],
|
||||||
|
defaultState: S,
|
||||||
|
speed = 1,
|
||||||
|
defaultDuration = 8,
|
||||||
|
): { from: S; to: S; progress: number } {
|
||||||
|
const effectiveFrame = useCurrentFrame() * speed;
|
||||||
|
const started = steps
|
||||||
|
.map((step, index) => ({ step, index }))
|
||||||
|
.sort((a, b) => a.step.at - b.step.at || a.index - b.index)
|
||||||
|
.filter((e) => e.step.at <= effectiveFrame);
|
||||||
|
if (started.length === 0)
|
||||||
|
return { from: defaultState, to: defaultState, progress: 1 };
|
||||||
|
const to = started[started.length - 1].step;
|
||||||
|
const from = started.length >= 2 ? started[started.length - 2].step : null;
|
||||||
|
const dur = to.duration ?? defaultDuration;
|
||||||
|
const progress = dur > 0 ? clamp01((effectiveFrame - to.at) / dur) : 1;
|
||||||
|
return { from: from ? from.state : defaultState, to: to.state, progress };
|
||||||
|
}
|
||||||
5
video/src/lib/remocn-ui/types.ts
Normal file
5
video/src/lib/remocn-ui/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface Step<S extends string = string> {
|
||||||
|
at: number;
|
||||||
|
state: S;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
67
video/src/marka.ts
Normal file
67
video/src/marka.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Lansman videosunun görsel dili — docs/tasarim/DESIGN.md'nin video karşılığı.
|
||||||
|
* Renkler sitenin Tailwind v4 tonlarıyla birebir (oklch); logo renkleri
|
||||||
|
* public/kolay-tercih-mark.svg'den. Video 1080p olduğu için arayüz ölçüleri
|
||||||
|
* sitenin 2 katıdır (h-12 = 48 px → 96–104 px).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const W = 1920;
|
||||||
|
export const H = 1080;
|
||||||
|
export const FPS = 30;
|
||||||
|
|
||||||
|
export const R = {
|
||||||
|
// Bilgi mavisi (--primary) ve logo mavisi
|
||||||
|
primary: "oklch(0.623 0.188 259.8)",
|
||||||
|
mavi: "#3b82f6",
|
||||||
|
mavi400: "oklch(0.707 0.165 254.624)",
|
||||||
|
// Logo: şeftali + turuncu kare
|
||||||
|
seftali: "#fdc7a7",
|
||||||
|
turuncuLogo: "#ff5a00",
|
||||||
|
// Aksiyon turuncusu (orange-500) ve hover'ı
|
||||||
|
turuncu: "oklch(0.705 0.213 47.604)",
|
||||||
|
turuncu600: "oklch(0.646 0.222 41.116)",
|
||||||
|
turuncu50: "oklch(0.98 0.016 73.684)",
|
||||||
|
|
||||||
|
slate50: "oklch(0.984 0.003 247.858)",
|
||||||
|
slate100: "oklch(0.968 0.007 247.896)",
|
||||||
|
slate200: "oklch(0.929 0.013 255.508)",
|
||||||
|
slate300: "oklch(0.869 0.022 252.894)",
|
||||||
|
slate400: "oklch(0.704 0.04 256.788)",
|
||||||
|
slate500: "oklch(0.554 0.046 257.417)",
|
||||||
|
slate600: "oklch(0.446 0.043 257.281)",
|
||||||
|
slate700: "oklch(0.372 0.044 257.287)",
|
||||||
|
slate800: "oklch(0.279 0.041 260.031)",
|
||||||
|
slate900: "oklch(0.208 0.042 265.755)",
|
||||||
|
slate950: "oklch(0.129 0.042 264.695)",
|
||||||
|
beyaz: "#ffffff",
|
||||||
|
|
||||||
|
// Risk / dilim renkleri — her zaman metin etiketiyle birlikte
|
||||||
|
emerald500: "oklch(0.696 0.17 162.48)",
|
||||||
|
emerald600: "oklch(0.596 0.145 163.225)",
|
||||||
|
amber500: "oklch(0.769 0.188 70.08)",
|
||||||
|
amber600: "oklch(0.666 0.179 58.318)",
|
||||||
|
red500: "oklch(0.637 0.237 25.331)",
|
||||||
|
red600: "oklch(0.577 0.245 27.325)",
|
||||||
|
|
||||||
|
// Gösterge kareleri (§4.11)
|
||||||
|
indigo500: "oklch(0.585 0.233 277.117)",
|
||||||
|
green600: "oklch(0.627 0.194 149.214)",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Piksel imzası: hero ızgarası (hücre 10, nokta 7.4, rx 2) oranları */
|
||||||
|
export const PIKSEL = {
|
||||||
|
nokta: 0.74,
|
||||||
|
rx: 0.2,
|
||||||
|
// Hero doku paleti (turkey-pixel-map.tsx)
|
||||||
|
opakliklar: [0.3, 0.16, 0.24, 0.12, 0.27, 0.19],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const opaklikFor = (x: number, y: number) =>
|
||||||
|
PIKSEL.opakliklar[(x * 7 + y * 11) % PIKSEL.opakliklar.length];
|
||||||
|
|
||||||
|
/** Logo işareti (viewBox 180) */
|
||||||
|
export const LOGO_KARELER = [
|
||||||
|
{ x: 27, y: 56, boy: 57, renk: "#3b82f6" },
|
||||||
|
{ x: 91, y: 29, boy: 56, renk: "#fdc7a7" },
|
||||||
|
{ x: 91, y: 95, boy: 56, renk: "#ff5a00" },
|
||||||
|
] as const;
|
||||||
183
video/src/sahneler/S1Gurultu.tsx
Normal file
183
video/src/sahneler/S1Gurultu.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
|
||||||
|
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
|
||||||
|
import { TrackingIn } from "@/components/remocn/tracking-in";
|
||||||
|
import { Vinyet } from "../bilesenler/Gren";
|
||||||
|
import { Cikis } from "../bilesenler/Kinetik";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
import { H, PIKSEL, R, W } from "../marka";
|
||||||
|
import { GURULTU_SAYILARI } from "../veri";
|
||||||
|
import { aralik, clamp01, ease, hash, lerp } from "../yardimci";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1 · Gürültü. Koyu zemin, derinlikten üstüne gelen gerçek taban sıraları
|
||||||
|
* (tercih dönemindeki tablo kalabalığı). Üç kinetik satır; sonunda bütün
|
||||||
|
* sayılar merkeze çöker ve geriye **tek bir turuncu piksel** kalır. Bir
|
||||||
|
* sonraki geçiş o pikselden yayılan dalgadır.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ADET = 60;
|
||||||
|
const DERINLIK = 3200;
|
||||||
|
const YAKIN = 70;
|
||||||
|
const ODAK = 720;
|
||||||
|
const COKUS: [number, number] = [104, 122];
|
||||||
|
|
||||||
|
const tunel = Array.from({ length: ADET }, (_, i) => {
|
||||||
|
let x = (hash(i, 1) - 0.5) * 2800;
|
||||||
|
let y = (hash(i, 2) - 0.5) * 1600;
|
||||||
|
// Başlık alanının içinden geçmesin: merkeze yakın olanlar dışarı itilir
|
||||||
|
if (Math.abs(x) < 520 && Math.abs(y) < 260) {
|
||||||
|
x += Math.sign(x || 1) * 520;
|
||||||
|
y += Math.sign(y || 1) * 180;
|
||||||
|
}
|
||||||
|
const r = hash(i, 4);
|
||||||
|
return {
|
||||||
|
metin: GURULTU_SAYILARI[i % GURULTU_SAYILARI.length],
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z0: hash(i, 3) * DERINLIK,
|
||||||
|
renk: r > 0.9 ? R.turuncu : r > 0.78 ? R.mavi400 : R.slate400,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tünelde kat edilen yol: sabit hız, çöküşe doğru hızlanır
|
||||||
|
const yol = (f: number) => f * 24 + Math.max(0, f - 70) ** 2 * 0.9;
|
||||||
|
|
||||||
|
export const S1Gurultu: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const cokus = aralik(f, COKUS[0], COKUS[1], ease.in);
|
||||||
|
const merkezPiksel = aralik(f, 116, 122, ease.geriTep);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate950, overflow: "hidden" }}>
|
||||||
|
{/* Derinlik tüneli */}
|
||||||
|
{tunel.map((t, i) => {
|
||||||
|
const z = YAKIN + ((((t.z0 - yol(f)) % DERINLIK) + DERINLIK) % DERINLIK);
|
||||||
|
const s = ODAK / z;
|
||||||
|
let X = W / 2 + t.x * s;
|
||||||
|
let Y = H / 2 + t.y * s;
|
||||||
|
let olcek = s;
|
||||||
|
// Çöküş: her sayı merkeze çekilip piksele küçülür
|
||||||
|
X = lerp(X, W / 2, cokus);
|
||||||
|
Y = lerp(Y, H / 2, cokus);
|
||||||
|
olcek = lerp(olcek, 0.02, cokus);
|
||||||
|
const uzak = aralik(z, DERINLIK, DERINLIK * 0.72);
|
||||||
|
const yakin = aralik(z, YAKIN, 220);
|
||||||
|
const op = uzak * yakin * 0.7 * (1 - aralik(f, COKUS[1] - 4, COKUS[1]));
|
||||||
|
const bulanik = Math.min(7, Math.abs(z - ODAK) / 420) + (1 - yakin) * 6;
|
||||||
|
if (op <= 0.01) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: X,
|
||||||
|
top: Y,
|
||||||
|
transform: `translate(-50%, -50%) scale(${olcek})`,
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 64,
|
||||||
|
color: t.renk,
|
||||||
|
opacity: op,
|
||||||
|
filter: `blur(${bulanik}px)`,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
zIndex: Math.round(10000 - z),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.metin}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Başlığın arkasını sakinleştiren karartma */}
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(ellipse 34% 26% at 50% 50%, ${R.slate950} 30%, rgba(2,6,23,0) 100%)`,
|
||||||
|
opacity: 0.9,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Kinetik satırlar */}
|
||||||
|
<Sequence from={6} durationInFrames={46}>
|
||||||
|
<Cikis bas={36}>
|
||||||
|
<TrackingIn
|
||||||
|
text="24 tercih."
|
||||||
|
fontFamily={FONT.baslik}
|
||||||
|
fontSize={190}
|
||||||
|
color={R.beyaz}
|
||||||
|
startTracking={1.1}
|
||||||
|
endTracking={0.015}
|
||||||
|
startBlur={16}
|
||||||
|
/>
|
||||||
|
</Cikis>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={48} durationInFrames={50}>
|
||||||
|
<Cikis bas={42}>
|
||||||
|
<KineticCenterBuild
|
||||||
|
text="Binlerce ihtimal."
|
||||||
|
fontFamily={FONT.baslik}
|
||||||
|
fontWeight={700}
|
||||||
|
fontSize={176}
|
||||||
|
color={R.beyaz}
|
||||||
|
letterSpacing={0.015}
|
||||||
|
entryOffset={140}
|
||||||
|
speed={5.5}
|
||||||
|
vurgu={{ indices: [1], style: { color: R.slate400 } }}
|
||||||
|
/>
|
||||||
|
</Cikis>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={94} durationInFrames={30}>
|
||||||
|
<HangisiSenin />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Geriye kalan tek piksel — dalga buradan doğar */}
|
||||||
|
{merkezPiksel > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: W / 2,
|
||||||
|
top: H / 2,
|
||||||
|
width: 44 * PIKSEL.nokta,
|
||||||
|
height: 44 * PIKSEL.nokta,
|
||||||
|
borderRadius: 44 * PIKSEL.nokta * PIKSEL.rx,
|
||||||
|
background: R.turuncu,
|
||||||
|
transform: `translate(-50%, -50%) scale(${merkezPiksel})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Vinyet guc={0.6} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** "Hangisi senin?" — belirir, sonra harf aralığı sıfırın altına çöküp piksele küçülür */
|
||||||
|
const HangisiSenin: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const cok = aralik(f, 12, 26, ease.in);
|
||||||
|
return (
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
transform: `scale(${lerp(1, 0.04, cok)})`,
|
||||||
|
opacity: 1 - clamp01((f - 22) / 4),
|
||||||
|
filter: `blur(${cok * 6}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrackingIn
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
Hangisi{" "}
|
||||||
|
<span style={{ fontFamily: FONT.vurgu, fontStyle: "italic", color: R.turuncu, letterSpacing: "-0.04em" }}>
|
||||||
|
senin?
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
fontFamily={FONT.baslik}
|
||||||
|
fontSize={176}
|
||||||
|
color={R.beyaz}
|
||||||
|
startTracking={0.7}
|
||||||
|
endTracking={lerp(0.015, -0.2, cok)}
|
||||||
|
startBlur={12}
|
||||||
|
speed={1.4}
|
||||||
|
/>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
355
video/src/sahneler/S2Harita.tsx
Normal file
355
video/src/sahneler/S2Harita.tsx
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
|
||||||
|
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
|
||||||
|
import { TrackingIn } from "@/components/remocn/tracking-in";
|
||||||
|
import { Imlec, isi, isiIzi, type Yol } from "../bilesenler/Imlec";
|
||||||
|
import { Cikis, Kutu } from "../bilesenler/Kinetik";
|
||||||
|
import { LogoIsaret } from "../bilesenler/Logo";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
import { dalgaYaricapi } from "../gecisler/piksel-dalga";
|
||||||
|
import { H, LOGO_KARELER, PIKSEL, R, W, opaklikFor } from "../marka";
|
||||||
|
import { IZGARA_SATIR, IZGARA_SUTUN, SEHIRLER, TURKIYE_HUCRELER } from "../turkiye";
|
||||||
|
import { IL_PROGRAM, TOPLAM_PROGRAM } from "../veri";
|
||||||
|
import { aralik, clamp01, ease, hash, karis, lerp, olc, tr } from "../yardimci";
|
||||||
|
import { GECIS, SAHNE, gecisBasi } from "../zaman";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2 · Harita. Dalga geçerken hero'nun dot-matrix Türkiye'si piksel piksel
|
||||||
|
* belirir; harita derinlikte yatık durur, illerin program sayısı kadar
|
||||||
|
* piksel kuleleri yükselir. İmleç haritanın üstünden geçer, sitedeki gibi
|
||||||
|
* pikseller turuncuya ısınıp yavaş soğur. Sonra bütün pikseller logonun üç
|
||||||
|
* karesine toplanır; "KolayTercih" harf aralığı daralarak (tracking) yerine
|
||||||
|
* oturur. İmleç turuncu kareye tıklar → bir sonraki dalga oradan.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HUCRE = 24;
|
||||||
|
const PERSPEKTIF = 2600;
|
||||||
|
const DALGA_BANT = 420;
|
||||||
|
const MORF: [number, number] = [110, 146];
|
||||||
|
const LOGO_BOY = 260;
|
||||||
|
const LOGO_MERKEZ_Y = 470;
|
||||||
|
|
||||||
|
// Kelime markanın logo svg'sindeki oranlarıyla: yazı 46.7/44 işaret boyu
|
||||||
|
// sağda başlar, punto işaret boyunun ~0.63'ü
|
||||||
|
const KELIME_BOY = 164;
|
||||||
|
const KELIME_OFSET = (46.7 / 44) * LOGO_BOY;
|
||||||
|
|
||||||
|
/** Logonun son yerleşimi (işaret + kelime ortalı) */
|
||||||
|
export const logoYerlesimi = () => {
|
||||||
|
const ww =
|
||||||
|
olc("Kolay", { family: FONT.vurgu, size: KELIME_BOY, weight: 300, tracking: -0.05 }) +
|
||||||
|
olc("Tercih", { family: FONT.vurgu, size: KELIME_BOY, weight: 600, tracking: -0.05 });
|
||||||
|
const toplam = KELIME_OFSET + ww;
|
||||||
|
const sol = W / 2 - toplam / 2;
|
||||||
|
return { sol, ww, ust: LOGO_MERKEZ_Y - LOGO_BOY / 2 };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** İmlecin tıkladığı nokta: turuncu karenin merkezi */
|
||||||
|
export const haritaTiklama = () => {
|
||||||
|
const { sol, ust } = logoYerlesimi();
|
||||||
|
const k = LOGO_KARELER[2];
|
||||||
|
return {
|
||||||
|
x: sol + ((k.x + k.boy / 2) / 180) * LOGO_BOY,
|
||||||
|
y: ust + ((k.y + k.boy / 2) / 180) * LOGO_BOY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIKLAMA_KARESI = gecisBasi(SAHNE.harita, GECIS.g2);
|
||||||
|
|
||||||
|
// ——— Harita noktaları ve morf hedefleri (bir kez hesaplanır) ———
|
||||||
|
|
||||||
|
const bolgeOf = (x: number, y: number) => (x < 30 ? 0 : y < 12 ? 1 : 2);
|
||||||
|
|
||||||
|
const noktalar = (() => {
|
||||||
|
const ham = TURKIYE_HUCRELER.map(({ x, y }) => ({
|
||||||
|
gx: x,
|
||||||
|
gy: y,
|
||||||
|
mx: (x - IZGARA_SUTUN / 2 + 0.5) * HUCRE,
|
||||||
|
my: (y - IZGARA_SATIR / 2 + 0.5) * HUCRE,
|
||||||
|
op: 0.2 + opaklikFor(x, y) * 1.7,
|
||||||
|
bolge: bolgeOf(x, y),
|
||||||
|
}));
|
||||||
|
const sinir = [0, 1, 2].map((b) => {
|
||||||
|
const n = ham.filter((p) => p.bolge === b);
|
||||||
|
return {
|
||||||
|
n: n.length,
|
||||||
|
x0: Math.min(...n.map((p) => p.gx)),
|
||||||
|
x1: Math.max(...n.map((p) => p.gx)),
|
||||||
|
y0: Math.min(...n.map((p) => p.gy)),
|
||||||
|
y1: Math.max(...n.map((p) => p.gy)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const mSol = W / 2 - LOGO_BOY / 2;
|
||||||
|
const mUst = LOGO_MERKEZ_Y - LOGO_BOY / 2;
|
||||||
|
return ham.map((p, i) => {
|
||||||
|
const s = sinir[p.bolge];
|
||||||
|
const k = LOGO_KARELER[p.bolge];
|
||||||
|
const u = (p.gx - s.x0) / Math.max(1, s.x1 - s.x0);
|
||||||
|
const v = (p.gy - s.y0) / Math.max(1, s.y1 - s.y0);
|
||||||
|
const kk = Math.ceil(Math.sqrt(s.n));
|
||||||
|
const kenar = (k.boy / 180) * LOGO_BOY;
|
||||||
|
const hc = kenar / kk;
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
hedefX: mSol + (k.x / 180) * LOGO_BOY + (Math.min(kk - 1, Math.floor(u * kk)) + 0.5) * hc,
|
||||||
|
hedefY: mUst + (k.y / 180) * LOGO_BOY + (Math.min(kk - 1, Math.floor(v * kk)) + 0.5) * hc,
|
||||||
|
hedefBoy: hc * 1.08,
|
||||||
|
hedefRenk: k.renk,
|
||||||
|
// Soldan sağa süpürülen toplanma + biraz dağınıklık
|
||||||
|
gecikme: (p.gx / IZGARA_SUTUN) * 10 + hash(i, 5) * 6,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
const kuleler = SEHIRLER.filter((s) => IL_PROGRAM[s.name]).map((s, i) => ({
|
||||||
|
...s,
|
||||||
|
mx: (s.x - IZGARA_SUTUN / 2 + 0.5) * HUCRE,
|
||||||
|
my: (s.y - IZGARA_SATIR / 2 + 0.5) * HUCRE,
|
||||||
|
n: IL_PROGRAM[s.name],
|
||||||
|
yukseklik: 16 + 170 * Math.sqrt(IL_PROGRAM[s.name] / IL_PROGRAM["İstanbul"]),
|
||||||
|
i,
|
||||||
|
}));
|
||||||
|
const ETIKETLI = ["İstanbul", "Ankara", "İzmir"];
|
||||||
|
|
||||||
|
// ——— Kamera ———
|
||||||
|
|
||||||
|
type Kamera = { egim: number; donus: number; zoom: number; cx: number; cy: number };
|
||||||
|
|
||||||
|
const kameraAt = (f: number): Kamera => {
|
||||||
|
const t = aralik(f, 0, 130, ease.inOut);
|
||||||
|
return {
|
||||||
|
egim: (lerp(56, 44, t) * Math.PI) / 180,
|
||||||
|
donus: (lerp(-9, -3, t) * Math.PI) / 180,
|
||||||
|
zoom: lerp(0.94, 1.06, t),
|
||||||
|
cx: W / 2,
|
||||||
|
cy: lerp(560, 540, t),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Harita uzayından ekrana: Z çevresinde dönüş, X çevresinde eğim, perspektif */
|
||||||
|
const izdus = (mx: number, my: number, mz: number, k: Kamera) => {
|
||||||
|
const x1 = mx * Math.cos(k.donus) - my * Math.sin(k.donus);
|
||||||
|
const y1 = mx * Math.sin(k.donus) + my * Math.cos(k.donus);
|
||||||
|
const y2 = y1 * Math.cos(k.egim) - mz * Math.sin(k.egim);
|
||||||
|
const z2 = y1 * Math.sin(k.egim) + mz * Math.cos(k.egim);
|
||||||
|
const s = (PERSPEKTIF / (PERSPEKTIF - z2)) * k.zoom;
|
||||||
|
return { x: k.cx + x1 * s, y: k.cy + y2 * s, s };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ——— İmleç ———
|
||||||
|
|
||||||
|
const tik = () => haritaTiklama();
|
||||||
|
const imlecYolu = (): Yol => {
|
||||||
|
const t = tik();
|
||||||
|
return [
|
||||||
|
{ at: 44, x: 1860, y: 1010 },
|
||||||
|
{ at: 72, x: 1340, y: 700, duration: 28 },
|
||||||
|
{ at: 96, x: 920, y: 540, duration: 24 },
|
||||||
|
{ at: 120, x: 520, y: 620, duration: 24 },
|
||||||
|
{ at: 156, x: 1240, y: 880, duration: 36 },
|
||||||
|
{ at: TIKLAMA_KARESI - 4, x: t.x, y: t.y, duration: 34 },
|
||||||
|
{ at: TIKLAMA_KARESI, x: t.x, y: t.y, click: true, duration: 4 },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
const IMLEC_GORUNUR: [number, number] = [42, SAHNE.harita];
|
||||||
|
|
||||||
|
export const S2Harita: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const kam = kameraAt(f);
|
||||||
|
const yol = imlecYolu();
|
||||||
|
const izler = isiIzi(yol, f, IMLEC_GORUNUR);
|
||||||
|
const r = dalgaYaricapi(f / GECIS.g1, { x: W / 2, y: H / 2 }, DALGA_BANT);
|
||||||
|
const cos = Math.cos(kam.egim);
|
||||||
|
|
||||||
|
const kuleCokus = 1 - aralik(f, MORF[0] - 10, MORF[0] + 6, ease.in);
|
||||||
|
const noktaSonus = 1 - aralik(f, MORF[1] + 2, MORF[1] + 10);
|
||||||
|
const logoBelir = aralik(f, MORF[1] - 4, MORF[1] + 6, ease.out);
|
||||||
|
|
||||||
|
const { sol, ww } = logoYerlesimi();
|
||||||
|
const kay = aralik(f, 150, 178, ease.inOut);
|
||||||
|
const logoSol = lerp(W / 2 - LOGO_BOY / 2, sol, kay);
|
||||||
|
|
||||||
|
// Noktalar
|
||||||
|
const rects: React.ReactNode[] = [];
|
||||||
|
if (noktaSonus > 0) {
|
||||||
|
noktalar.forEach((p, i) => {
|
||||||
|
const e = ease.inOut(clamp01((f - MORF[0] - p.gecikme) / 22));
|
||||||
|
const q = izdus(p.mx, p.my, 0, kam);
|
||||||
|
const d = Math.hypot(q.x - W / 2, q.y - H / 2);
|
||||||
|
const pop = ease.geriTep(clamp01((r - d - 0.36 * DALGA_BANT) / (0.42 * DALGA_BANT)));
|
||||||
|
if (pop <= 0) return;
|
||||||
|
const h = e < 1 ? isi(q.x, q.y, izler, 130) * (1 - e) : 0;
|
||||||
|
const boy0 = HUCRE * PIKSEL.nokta * q.s * pop;
|
||||||
|
const x = lerp(q.x, p.hedefX, e);
|
||||||
|
const y = lerp(q.y, p.hedefY, e);
|
||||||
|
const w = lerp(boy0, p.hedefBoy, e);
|
||||||
|
const hh = lerp(boy0 * cos, p.hedefBoy, e);
|
||||||
|
const renk = e > 0 ? karis(karis(R.primary, R.turuncu, h), p.hedefRenk, e) : karis(R.primary, R.turuncu, h);
|
||||||
|
const op = lerp(lerp(p.op, 0.95, h), 1, e) * noktaSonus;
|
||||||
|
rects.push(
|
||||||
|
<rect
|
||||||
|
key={i}
|
||||||
|
x={x - w / 2}
|
||||||
|
y={y - hh / 2}
|
||||||
|
width={w}
|
||||||
|
height={hh}
|
||||||
|
rx={Math.min(w, hh) * PIKSEL.rx}
|
||||||
|
fill={renk}
|
||||||
|
fillOpacity={op}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Piksel kuleleri: il başına program sayısı kadar yükselen plakalar
|
||||||
|
const kuleRects: React.ReactNode[] = [];
|
||||||
|
const etiketler: React.ReactNode[] = [];
|
||||||
|
if (kuleCokus > 0) {
|
||||||
|
kuleler.forEach((k) => {
|
||||||
|
const buyu = aralik(f, 30 + k.i * 3, 70 + k.i * 3, ease.out) * kuleCokus;
|
||||||
|
const yuk = k.yukseklik * buyu;
|
||||||
|
const adim = HUCRE * 0.55;
|
||||||
|
const adet = Math.floor(yuk / adim);
|
||||||
|
for (let j = 0; j <= adet; j++) {
|
||||||
|
const q = izdus(k.mx, k.my, j * adim, kam);
|
||||||
|
const boy = HUCRE * PIKSEL.nokta * q.s;
|
||||||
|
const ust = j === adet;
|
||||||
|
kuleRects.push(
|
||||||
|
<rect
|
||||||
|
key={`${k.name}-${j}`}
|
||||||
|
x={q.x - boy / 2}
|
||||||
|
y={q.y - (boy * cos) / 2}
|
||||||
|
width={boy}
|
||||||
|
height={boy * cos}
|
||||||
|
rx={boy * PIKSEL.rx}
|
||||||
|
fill={R.turuncu}
|
||||||
|
fillOpacity={ust ? 0.95 : 0.3 + 0.45 * (j / Math.max(1, adet))}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (ETIKETLI.includes(k.name)) {
|
||||||
|
const op = aralik(f, 62 + k.i * 5, 72 + k.i * 5) * (1 - aralik(f, 100, 110));
|
||||||
|
if (op > 0) {
|
||||||
|
const q = izdus(k.mx, k.my, yuk, kam);
|
||||||
|
etiketler.push(
|
||||||
|
<div
|
||||||
|
key={k.name}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: q.x,
|
||||||
|
top: q.y - 34,
|
||||||
|
transform: `translate(-50%, -100%) translateY(${(1 - op) * 12}px)`,
|
||||||
|
opacity: op,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
padding: "10px 22px",
|
||||||
|
borderRadius: 999,
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `2px solid ${R.slate200}`,
|
||||||
|
boxShadow: "0 2px 8px rgba(15,23,42,0.06)",
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: R.slate800,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{k.name}
|
||||||
|
<span style={{ color: R.slate400, fontWeight: 500 }}>·</span>
|
||||||
|
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, color: R.slate900 }}>{tr(k.n)}</span>
|
||||||
|
<span style={{ color: R.slate500, fontWeight: 500 }}>program</span>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(ellipse 60% 55% at 50% 45%, ${R.beyaz} 20%, rgba(255,255,255,0) 75%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<svg width={W} height={H} style={{ position: "absolute", inset: 0 }}>
|
||||||
|
<g>{rects}</g>
|
||||||
|
<g>{kuleRects}</g>
|
||||||
|
</svg>
|
||||||
|
{etiketler}
|
||||||
|
|
||||||
|
{/* Logo: morfun sonunda katı kareler, sonra sola kayar */}
|
||||||
|
{logoBelir > 0 && (
|
||||||
|
<LogoIsaret
|
||||||
|
boy={LOGO_BOY}
|
||||||
|
style={{ position: "absolute", left: logoSol, top: LOGO_MERKEZ_Y - LOGO_BOY / 2, opacity: logoBelir }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Kelime marka: harf aralığı genişten daralarak yerine oturur */}
|
||||||
|
<Sequence from={154}>
|
||||||
|
<Kutu x={sol + KELIME_OFSET} w={ww} y={LOGO_MERKEZ_Y - 100} h={200}>
|
||||||
|
<TrackingIn
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
<span style={{ fontWeight: 300 }}>Kolay</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>Tercih</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
fontFamily={FONT.vurgu}
|
||||||
|
fontSize={KELIME_BOY}
|
||||||
|
fontWeight={300}
|
||||||
|
color="#0a0a0a"
|
||||||
|
startTracking={0.55}
|
||||||
|
endTracking={-0.05}
|
||||||
|
startBlur={14}
|
||||||
|
/>
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Alt yazı: harita beliren veriyi söyler */}
|
||||||
|
<Sequence from={30} durationInFrames={82}>
|
||||||
|
<Kutu y={850} h={140}>
|
||||||
|
<Cikis bas={68}>
|
||||||
|
<KineticCenterBuild
|
||||||
|
text={`${tr(TOPLAM_PROGRAM)} program, 6 yıllık veri.`}
|
||||||
|
fontFamily={FONT.baslik}
|
||||||
|
fontWeight={700}
|
||||||
|
fontSize={80}
|
||||||
|
color={R.slate900}
|
||||||
|
letterSpacing={0.015}
|
||||||
|
speed={1.25}
|
||||||
|
birim="kelime"
|
||||||
|
vurgu={{
|
||||||
|
indices: [2, 3, 4],
|
||||||
|
style: { fontFamily: FONT.vurgu, fontStyle: "italic", color: R.primary, letterSpacing: -0.05 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Cikis>
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Slogan (hero h1) */}
|
||||||
|
<Sequence from={160}>
|
||||||
|
<Kutu y={650} h={130}>
|
||||||
|
<KineticCenterBuild
|
||||||
|
text="Tercih listeni gerçek veriyle kur, panikle değil."
|
||||||
|
fontFamily={FONT.baslik}
|
||||||
|
fontWeight={700}
|
||||||
|
fontSize={66}
|
||||||
|
color={R.slate900}
|
||||||
|
letterSpacing={0.015}
|
||||||
|
speed={1.9}
|
||||||
|
birim="kelime"
|
||||||
|
vurgu={{
|
||||||
|
indices: [2, 3],
|
||||||
|
style: { fontFamily: FONT.vurgu, fontStyle: "italic", color: R.primary, letterSpacing: -0.05 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
<Imlec yol={yol} gorunur={IMLEC_GORUNUR} el={[[TIKLAMA_KARESI - 10, SAHNE.harita]]} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
155
video/src/sahneler/S3Siralama.tsx
Normal file
155
video/src/sahneler/S3Siralama.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { Trophy } from "lucide-react";
|
||||||
|
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
|
||||||
|
import { TrackingIn } from "@/components/remocn/tracking-in";
|
||||||
|
import { useTypewriter } from "@/lib/remocn-ui";
|
||||||
|
import { Imlec, isiIzi, type Yol } from "../bilesenler/Imlec";
|
||||||
|
import { Kutu } from "../bilesenler/Kinetik";
|
||||||
|
import { Rozet } from "../bilesenler/Logo";
|
||||||
|
import { PikselZemin } from "../bilesenler/PikselZemin";
|
||||||
|
import { Cip, CtaButon, GirisHap } from "../bilesenler/UI";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
import { R, W } from "../marka";
|
||||||
|
import { ORNEK_SIRA_METNI } from "../veri";
|
||||||
|
import { aralik, ease, lerp, olc } from "../yardimci";
|
||||||
|
import { GECIS, SAHNE, gecisBasi } from "../zaman";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3 · Sıralamanı gir. Hero formunun kendisi (hero-form.tsx): puan türü
|
||||||
|
* çipleri, sıralama alanı, turuncu CTA. Sahne dalgayla açılırken form
|
||||||
|
* yatık bir düzlemden doğrulur (derinlik). İmleç alana tıklar, sıralama
|
||||||
|
* yazılır, imleç CTA'ya gider ve basar → sahne butonun içinden geçilerek
|
||||||
|
* terk edilir.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CTA_METNI = "Yapay Zeka listemi kur";
|
||||||
|
const FORM_Y = 610;
|
||||||
|
const FORM_H = 104;
|
||||||
|
const GIRIS_W = 860;
|
||||||
|
const ARA = 24;
|
||||||
|
const PUAN_TURLERI = ["Sayısal", "Eşit Ağırlık", "Sözel", "Dil", "TYT"];
|
||||||
|
|
||||||
|
const ctaGenislik = () =>
|
||||||
|
2 * 0.52 * FORM_H +
|
||||||
|
olc(CTA_METNI, { family: FONT.govde, size: FORM_H * 0.31, weight: 600 }) +
|
||||||
|
16 +
|
||||||
|
FORM_H * 0.31;
|
||||||
|
|
||||||
|
const formSol = () => W / 2 - (GIRIS_W + ARA + ctaGenislik()) / 2;
|
||||||
|
|
||||||
|
export const siralamaTiklama = () => ({
|
||||||
|
x: formSol() + GIRIS_W + ARA + ctaGenislik() / 2,
|
||||||
|
y: FORM_Y + FORM_H / 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const TIK = gecisBasi(SAHNE.siralama, GECIS.g3);
|
||||||
|
const YAZ_BAS = 34;
|
||||||
|
|
||||||
|
export const S3Siralama: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const cta = siralamaTiklama();
|
||||||
|
const sol = formSol();
|
||||||
|
const girisX = sol + 150;
|
||||||
|
const yol: Yol = [
|
||||||
|
{ at: 2, x: 1620, y: 980 },
|
||||||
|
{ at: 28, x: girisX, y: cta.y + 6, duration: 26 },
|
||||||
|
{ at: 30, x: girisX, y: cta.y + 6, click: true, duration: 2 },
|
||||||
|
{ at: TIK - 10, x: cta.x - 40, y: cta.y + 4, duration: 30 },
|
||||||
|
{ at: TIK, x: cta.x - 40, y: cta.y + 4, click: true, duration: 10 },
|
||||||
|
];
|
||||||
|
const izler = isiIzi(yol, f);
|
||||||
|
const yazi = useTypewriter(ORNEK_SIRA_METNI, { cps: 10, startFrame: YAZ_BAS });
|
||||||
|
const odak = aralik(f, 30, 36);
|
||||||
|
const hover = aralik(f, TIK - 18, TIK - 10);
|
||||||
|
const bas = f >= TIK && f < TIK + 8 ? 1 - Math.abs(f - TIK - 4) / 4 : 0;
|
||||||
|
|
||||||
|
// Form yatık düzlemden doğrulur
|
||||||
|
const dogrul = aralik(f, 0, 30, ease.out);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
|
||||||
|
<PikselZemin tohum={23} izler={izler} />
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(ellipse 50% 45% at 50% 52%, ${R.slate50} 40%, rgba(248,250,252,0) 100%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AbsoluteFill style={{ perspective: 1800 }}>
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
transformOrigin: "50% 70%",
|
||||||
|
transform: `rotateX(${lerp(24, 0, dogrul)}deg) translateY(${lerp(60, 0, dogrul)}px) scale(${lerp(0.92, 1, dogrul)})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Kutu y={236} h={80} style={{ display: "flex", justifyContent: "center", opacity: aralik(f, 4, 14) }}>
|
||||||
|
<Rozet>Nasıl çalışır? · 1/3</Rozet>
|
||||||
|
</Kutu>
|
||||||
|
<Sequence from={6}>
|
||||||
|
<Kutu y={316} h={180}>
|
||||||
|
<TrackingIn
|
||||||
|
text="Sıralamanı gir."
|
||||||
|
fontFamily={FONT.baslik}
|
||||||
|
fontSize={124}
|
||||||
|
color={R.slate900}
|
||||||
|
startTracking={0.6}
|
||||||
|
endTracking={0.015}
|
||||||
|
startBlur={14}
|
||||||
|
/>
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Puan türü çipleri: Sayısal seçili */}
|
||||||
|
<Kutu y={512} h={60} style={{ display: "flex", justifyContent: "center", gap: 12 }}>
|
||||||
|
{PUAN_TURLERI.map((t, i) => (
|
||||||
|
<div key={t} style={{ opacity: aralik(f, 12 + i * 2, 20 + i * 2), translate: `0 ${(1 - aralik(f, 12 + i * 2, 22 + i * 2, ease.out)) * 16}px` }}>
|
||||||
|
<Cip boy={60} secili={i === 0 ? 1 : 0}>{t}</Cip>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Kutu>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: sol,
|
||||||
|
top: FORM_Y,
|
||||||
|
display: "flex",
|
||||||
|
gap: ARA,
|
||||||
|
opacity: aralik(f, 14, 24),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GirisHap
|
||||||
|
genislik={GIRIS_W}
|
||||||
|
deger={yazi.text}
|
||||||
|
yaziyor={yazi.typing}
|
||||||
|
placeholder="YKS başarı sıralaman (ör. 85.000)"
|
||||||
|
odak={odak}
|
||||||
|
ikon={<Trophy size={30} color={R.slate400} strokeWidth={2} aria-hidden />}
|
||||||
|
/>
|
||||||
|
<CtaButon hover={hover} bas={bas}>
|
||||||
|
{CTA_METNI}
|
||||||
|
</CtaButon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Kutu y={FORM_Y + FORM_H + 40} h={60} style={{ display: "flex", justifyContent: "center", opacity: aralik(f, 22, 32) }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
padding: "12px 24px",
|
||||||
|
borderRadius: 16,
|
||||||
|
border: `2px solid ${R.slate200}`,
|
||||||
|
background: R.beyaz,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: 24,
|
||||||
|
color: R.slate700,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<b style={{ color: R.slate900, fontWeight: 600 }}>Veri: YÖK Atlas</b> · 2026 yerleştirme sonuçları
|
||||||
|
</span>
|
||||||
|
</Kutu>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</AbsoluteFill>
|
||||||
|
<Imlec yol={yol} el={[[24, 34], [TIK - 16, SAHNE.siralama]]} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
707
video/src/sahneler/S4Havuz.tsx
Normal file
707
video/src/sahneler/S4Havuz.tsx
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
import { Building2, Landmark, Scale } from "lucide-react";
|
||||||
|
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
|
||||||
|
import { FocusBlurResolve } from "@/components/remocn/focus-blur-resolve";
|
||||||
|
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
|
||||||
|
import { TrackingIn } from "@/components/remocn/tracking-in";
|
||||||
|
import { Imlec, type Yol } from "../bilesenler/Imlec";
|
||||||
|
import { Cikis, Kutu } from "../bilesenler/Kinetik";
|
||||||
|
import { Rozet } from "../bilesenler/Logo";
|
||||||
|
import { Cip, DILIM_RENK, DilimEtiketi, PikselTrend } from "../bilesenler/UI";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
import { PIKSEL, R, W, opaklikFor } from "../marka";
|
||||||
|
import { TURKIYE_HUCRELER } from "../turkiye";
|
||||||
|
import { DILIM_ETIKET, HUNI, ISKELET, LISTE, SORULAN, type Dilim } from "../veri";
|
||||||
|
import { aralik, clamp01, ease, hash, karis, lerp, olc, tr } from "../yardimci";
|
||||||
|
import { GECIS, SAHNE, gecisBasi } from "../zaman";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4 · Havuz → 24'lük liste.
|
||||||
|
*
|
||||||
|
* A) Sihirbaz (alan → il → devlet/vakıf). Sağda aday havuzu: sayı her
|
||||||
|
* seçimde küçülür, altındaki piksel alanından o kadar piksel düşer.
|
||||||
|
* Son adımda kalan her piksel **bir gerçek programdır** (179).
|
||||||
|
* B) Kart derinliğe çekilir; 179 piksel havuz sınırının içine toplanır.
|
||||||
|
* Dışarıdaki gri pikseller sınıra çarpıp geri döner (yapay zekâ havuzun
|
||||||
|
* dışına çıkamaz). 24 piksel seçilir, dilim renklerini alır ve soldaki
|
||||||
|
* 24'lük şeride uçar; sağda liste yatık bir düzlemden doğrulur. İmleç
|
||||||
|
* bir satıra tıklar → sahne o satırın içinden terk edilir.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TIK = gecisBasi(SAHNE.havuz, GECIS.g4);
|
||||||
|
|
||||||
|
// ——— Sihirbaz kartı yerleşimi ———
|
||||||
|
const KART = { x: 150, y: 170, w: 860, h: 740, ic: 64 };
|
||||||
|
const ICERIK_X = KART.x + KART.ic;
|
||||||
|
const ICERIK_W = KART.w - KART.ic * 2;
|
||||||
|
const CIP_BOY = 72;
|
||||||
|
|
||||||
|
const ALANLAR = [
|
||||||
|
"Mühendislik & Teknoloji",
|
||||||
|
"Sağlık & Tıp",
|
||||||
|
"Fen & Temel Bilimler",
|
||||||
|
"Mimarlık & Tasarım",
|
||||||
|
"İktisat & İşletme",
|
||||||
|
"Eğitim & Öğretmenlik",
|
||||||
|
"Hukuk & Siyasal",
|
||||||
|
"Tarım & Doğa",
|
||||||
|
];
|
||||||
|
const ILLER = ["İstanbul", "Ankara", "İzmir", "Bursa", "Antalya", "Eskişehir"];
|
||||||
|
const IL_HUCRE: Record<string, [number, number]> = {
|
||||||
|
İstanbul: [8, 4],
|
||||||
|
Ankara: [23, 9],
|
||||||
|
İzmir: [4, 16],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Çipleri kart genişliğinde satırlara dizer (gerçek yazı genişliğiyle) */
|
||||||
|
const cipDiz = (metinler: string[], x0: number, y0: number, ikonlu = false) => {
|
||||||
|
const ara = 16;
|
||||||
|
let x = x0;
|
||||||
|
let y = y0;
|
||||||
|
return metinler.map((m) => {
|
||||||
|
const w =
|
||||||
|
CIP_BOY * 0.7 +
|
||||||
|
olc(m, { family: FONT.govde, size: CIP_BOY * 0.35, weight: 600 }) +
|
||||||
|
(ikonlu ? CIP_BOY * 0.35 + CIP_BOY * 0.15 : 0);
|
||||||
|
if (x + w > x0 + ICERIK_W) {
|
||||||
|
x = x0;
|
||||||
|
y += CIP_BOY + ara;
|
||||||
|
}
|
||||||
|
const k = { metin: m, x, y, w, cx: x + w / 2, cy: y + CIP_BOY / 2 };
|
||||||
|
x += w + ara;
|
||||||
|
return k;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adım pencereleri: [giriş, çıkış]
|
||||||
|
const ADIM: [number, number][] = [
|
||||||
|
[-10, 88],
|
||||||
|
[86, 154],
|
||||||
|
[152, 214],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Tıklama kareleri
|
||||||
|
const T_ALAN = 46;
|
||||||
|
const T_IL = [106, 120, 134];
|
||||||
|
const T_TUR = 174;
|
||||||
|
|
||||||
|
// ——— Havuz sayacı ve piksel alanı ———
|
||||||
|
const SAYAC: { a: number; b: number; n: number }[] = [
|
||||||
|
{ a: 8, b: 34, n: HUNI[1].n },
|
||||||
|
{ a: 48, b: 70, n: HUNI[2].n },
|
||||||
|
{ a: 136, b: 158, n: HUNI[3].n },
|
||||||
|
{ a: 176, b: 198, n: HUNI[4].n },
|
||||||
|
];
|
||||||
|
const sayacAt = (f: number) => {
|
||||||
|
let n: number = HUNI[0].n;
|
||||||
|
let adim = 0;
|
||||||
|
for (const [i, s] of SAYAC.entries()) {
|
||||||
|
if (f >= s.a) {
|
||||||
|
n = lerp(n, s.n, aralik(f, s.a, s.b, ease.out));
|
||||||
|
adim = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { n, adim };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALAN = { x: 1100, y: 560, sutun: 30, satir: 14, hucre: 22 };
|
||||||
|
const HAVUZ = HUNI[4].n; // 179
|
||||||
|
const EN_COK = ALAN.sutun * ALAN.satir; // 420
|
||||||
|
const yanan = (n: number) =>
|
||||||
|
HAVUZ + (EN_COK - HAVUZ) * (Math.log10(n / HAVUZ) / Math.log10(HUNI[0].n / HAVUZ));
|
||||||
|
|
||||||
|
// Kümelenme (B): havuz sınırı içinde 17×11
|
||||||
|
const KUME = { sutun: 17, hucre: 34, cx: 960, cy: 590 };
|
||||||
|
const KUME_SATIR = Math.ceil(HAVUZ / KUME.sutun);
|
||||||
|
const KUME_X0 = KUME.cx - (KUME.sutun * KUME.hucre) / 2;
|
||||||
|
const KUME_Y0 = KUME.cy - (KUME_SATIR * KUME.hucre) / 2;
|
||||||
|
const SINIR = {
|
||||||
|
x: KUME_X0 - 30,
|
||||||
|
y: KUME_Y0 - 30,
|
||||||
|
w: KUME.sutun * KUME.hucre + 60,
|
||||||
|
h: KUME_SATIR * KUME.hucre + 60,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Şerit (B4): seçilen 24 piksel
|
||||||
|
const SERIT = { x: 140, y: 700, hucre: 30 };
|
||||||
|
|
||||||
|
const pikseller = (() => {
|
||||||
|
const hucreler = Array.from({ length: EN_COK }, (_, i) => ({
|
||||||
|
i,
|
||||||
|
gx: i % ALAN.sutun,
|
||||||
|
gy: Math.floor(i / ALAN.sutun),
|
||||||
|
h: hash(i % ALAN.sutun, Math.floor(i / ALAN.sutun), 41),
|
||||||
|
}));
|
||||||
|
const sirali = [...hucreler].sort((a, b) => a.h - b.h);
|
||||||
|
const rutbe = new Map(sirali.map((c, r) => [c.i, r]));
|
||||||
|
const havuzdakiler = hucreler.filter((c) => (rutbe.get(c.i) ?? 0) < HAVUZ); // alan sırasına göre
|
||||||
|
const havuzSira = new Map(havuzdakiler.map((c, p) => [c.i, p]));
|
||||||
|
// 24 seçim: havuzdan dağınık
|
||||||
|
const secimSirasi = [...havuzdakiler].sort((a, b) => hash(a.i, 3, 77) - hash(b.i, 3, 77)).slice(0, 24);
|
||||||
|
const dilimler: Dilim[] = ISKELET.flatMap((d) => Array<Dilim>(d.adet).fill(d.dilim));
|
||||||
|
const secim = new Map(secimSirasi.map((c, k) => [c.i, { k, dilim: dilimler[k] }]));
|
||||||
|
return hucreler.map((c) => ({
|
||||||
|
...c,
|
||||||
|
rutbe: rutbe.get(c.i) ?? 0,
|
||||||
|
p: havuzSira.get(c.i),
|
||||||
|
secim: secim.get(c.i),
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Havuz dışındaki "hayalet" programlar: sınıra çarpıp geri döner
|
||||||
|
const HAYALETLER = [
|
||||||
|
{ bx: SINIR.x, by: SINIR.y + 90, nx: -1, ny: 0 },
|
||||||
|
{ bx: SINIR.x, by: SINIR.y + 300, nx: -1, ny: 0 },
|
||||||
|
{ bx: SINIR.x + SINIR.w, by: SINIR.y + 150, nx: 1, ny: 0 },
|
||||||
|
{ bx: SINIR.x + SINIR.w, by: SINIR.y + 340, nx: 1, ny: 0 },
|
||||||
|
{ bx: SINIR.x + 180, by: SINIR.y + SINIR.h, nx: 0, ny: 1 },
|
||||||
|
{ bx: SINIR.x + 420, by: SINIR.y, nx: 0, ny: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ——— Liste (B4) ———
|
||||||
|
const LISTE_X = 950;
|
||||||
|
const LISTE_W = 860;
|
||||||
|
const SATIR_H = 84;
|
||||||
|
const BASLIK_H = 58;
|
||||||
|
const DAHA_H = 42;
|
||||||
|
const BOLUMLER = ISKELET.map((d) => ({
|
||||||
|
...d,
|
||||||
|
satirlar: LISTE.map((s, i) => ({ s, i })).filter(({ s }) => s.dilim === d.dilim),
|
||||||
|
}));
|
||||||
|
const listeUst = (f: number) => lerp(150, 12, aralik(f, 330, 392, ease.inOut));
|
||||||
|
// Tıklanan satırın liste içindeki konumu
|
||||||
|
const HEDEF_SATIR = LISTE.indexOf(SORULAN);
|
||||||
|
const hedefOfset = (() => {
|
||||||
|
let y = 0;
|
||||||
|
for (const b of BOLUMLER) {
|
||||||
|
y += BASLIK_H;
|
||||||
|
for (const { i } of b.satirlar) {
|
||||||
|
if (i === HEDEF_SATIR) return y;
|
||||||
|
y += SATIR_H;
|
||||||
|
}
|
||||||
|
y += DAHA_H;
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const havuzTiklama = () => ({
|
||||||
|
x: LISTE_X + LISTE_W * 0.42,
|
||||||
|
y: listeUst(TIK) + hedefOfset + SATIR_H / 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const S4Havuz: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
|
||||||
|
const alanCip = cipDiz(ALANLAR, ICERIK_X, 430);
|
||||||
|
const ilCip = cipDiz(ILLER, ICERIK_X, 744);
|
||||||
|
const turCip = cipDiz(["Devlet", "Vakıf", "Fark etmez"], ICERIK_X, 494, true);
|
||||||
|
const hedef = havuzTiklama();
|
||||||
|
|
||||||
|
const yol: Yol = [
|
||||||
|
{ at: 16, x: 1480, y: 1010 },
|
||||||
|
{ at: T_ALAN, x: alanCip[0].cx, y: alanCip[0].cy, duration: 26, click: true },
|
||||||
|
{ at: T_IL[0], x: ilCip[0].cx, y: ilCip[0].cy, duration: 30, click: true },
|
||||||
|
{ at: T_IL[1], x: ilCip[1].cx, y: ilCip[1].cy, duration: 12, click: true },
|
||||||
|
{ at: T_IL[2], x: ilCip[2].cx, y: ilCip[2].cy, duration: 12, click: true },
|
||||||
|
{ at: T_TUR, x: turCip[0].cx, y: turCip[0].cy, duration: 28, click: true },
|
||||||
|
{ at: 206, x: 1180, y: 1060, duration: 26 },
|
||||||
|
{ at: 350, x: 1880, y: 1040, duration: 4 },
|
||||||
|
{ at: 380, x: 1560, y: 760, duration: 28 },
|
||||||
|
{ at: TIK - 2, x: hedef.x, y: hedef.y, duration: 16 },
|
||||||
|
{ at: TIK, x: hedef.x, y: hedef.y, click: true, duration: 2 },
|
||||||
|
];
|
||||||
|
const imlecGorunur = f < 230 ? ([12, 228] as [number, number]) : ([352, SAHNE.havuz] as [number, number]);
|
||||||
|
|
||||||
|
const { n, adim } = sayacAt(f);
|
||||||
|
const yananF = yanan(n);
|
||||||
|
|
||||||
|
// ——— Faz geçişleri ———
|
||||||
|
const kartCikis = aralik(f, 198, 232, ease.in);
|
||||||
|
const panelCikis = aralik(f, 198, 214);
|
||||||
|
const kume = (p: number) => ease.inOut(clamp01((f - 204 - p * 0.1) / 26));
|
||||||
|
const secimBas = 262;
|
||||||
|
const seritBas = 300;
|
||||||
|
const sinirOp = aralik(f, 222, 238) * (1 - aralik(f, 300, 316));
|
||||||
|
|
||||||
|
// ——— Pikseller ———
|
||||||
|
const rects: React.ReactNode[] = [];
|
||||||
|
for (const c of pikseller) {
|
||||||
|
let x = ALAN.x + c.gx * ALAN.hucre + ALAN.hucre / 2;
|
||||||
|
let y = ALAN.y + c.gy * ALAN.hucre + ALAN.hucre / 2;
|
||||||
|
let boy = ALAN.hucre * PIKSEL.nokta;
|
||||||
|
let renk: string = R.primary;
|
||||||
|
let op = 0.25 + opaklikFor(c.gx, c.gy) * 1.9;
|
||||||
|
// Belirme (kart ile birlikte)
|
||||||
|
const belir = aralik(f, 4 + (c.gx + c.gy) * 0.35, 14 + (c.gx + c.gy) * 0.35, ease.out);
|
||||||
|
op *= belir;
|
||||||
|
// Havuzdan düşenler: sayaç indikçe sırası gelen piksel griye döner ve düşer
|
||||||
|
if (c.rutbe >= yananF) {
|
||||||
|
const dus = clamp01((c.rutbe + 1 - yananF) / 20);
|
||||||
|
if (dus >= 1) continue;
|
||||||
|
y += dus * dus * 150;
|
||||||
|
renk = karis(R.primary, R.slate300, clamp01(dus * 3));
|
||||||
|
op *= 1 - dus;
|
||||||
|
}
|
||||||
|
if (c.p !== undefined) {
|
||||||
|
// B: kümeye toplanma
|
||||||
|
const e = kume(c.p);
|
||||||
|
const kx = KUME_X0 + (c.p % KUME.sutun) * KUME.hucre + KUME.hucre / 2;
|
||||||
|
const ky = KUME_Y0 + Math.floor(c.p / KUME.sutun) * KUME.hucre + KUME.hucre / 2;
|
||||||
|
x = lerp(x, kx, e);
|
||||||
|
y = lerp(y, ky, e);
|
||||||
|
boy = lerp(boy, KUME.hucre * PIKSEL.nokta, e);
|
||||||
|
op = lerp(op, 0.55 + opaklikFor(c.gx, c.gy), e);
|
||||||
|
if (c.secim) {
|
||||||
|
const s = aralik(f, secimBas + c.secim.k * 1.3, secimBas + 8 + c.secim.k * 1.3, ease.out);
|
||||||
|
renk = karis(renk, DILIM_RENK[c.secim.dilim].nokta, s);
|
||||||
|
op = lerp(op, 1, s);
|
||||||
|
// seçilirken kısa bir halka
|
||||||
|
const halka = Math.sin(Math.PI * s);
|
||||||
|
if (halka > 0.02) {
|
||||||
|
const hb = boy * (1 + 0.7 * halka);
|
||||||
|
rects.push(
|
||||||
|
<rect key={`h${c.i}`} x={x - hb / 2} y={y - hb / 2} width={hb} height={hb} rx={hb * PIKSEL.rx} fill="none" stroke={DILIM_RENK[c.secim.dilim].nokta} strokeWidth={3} strokeOpacity={halka * 0.8} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// şeride uçuş
|
||||||
|
const u = ease.inOut(clamp01((f - seritBas - c.secim.k * 0.9) / 26));
|
||||||
|
x = lerp(x, SERIT.x + c.secim.k * SERIT.hucre + SERIT.hucre / 2, u);
|
||||||
|
y = lerp(y, SERIT.y + SERIT.hucre / 2, u);
|
||||||
|
boy = lerp(boy, SERIT.hucre * PIKSEL.nokta, u);
|
||||||
|
} else {
|
||||||
|
op *= 1 - 0.72 * aralik(f, secimBas, secimBas + 26);
|
||||||
|
op *= 1 - aralik(f, seritBas, seritBas + 14);
|
||||||
|
if (op <= 0.01) continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rects.push(
|
||||||
|
<rect key={c.i} x={x - boy / 2} y={y - boy / 2} width={boy} height={boy} rx={boy * PIKSEL.rx} fill={renk} fillOpacity={op} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hayaletler
|
||||||
|
const hayaletler = HAYALETLER.map((g, i) => {
|
||||||
|
const u = (f - (240 + i * 5)) / 26;
|
||||||
|
if (u < 0 || f > 316) return null;
|
||||||
|
const bosluk =
|
||||||
|
u < 1
|
||||||
|
? lerp(170, 10, ease.in(u))
|
||||||
|
: 10 + 46 * Math.abs(Math.sin((u - 1) * Math.PI * 1.2)) * Math.exp(-(u - 1) * 2.4);
|
||||||
|
const boy = KUME.hucre * PIKSEL.nokta;
|
||||||
|
const x = g.bx + g.nx * (bosluk + boy / 2);
|
||||||
|
const y = g.by + g.ny * (bosluk + boy / 2);
|
||||||
|
const op = aralik(u, 0, 0.3) * (1 - aralik(f, 300, 316));
|
||||||
|
return (
|
||||||
|
<rect key={`g${i}`} x={x - boy / 2} y={y - boy / 2} width={boy} height={boy} rx={boy * PIKSEL.rx} fill={R.slate100} stroke={R.slate300} strokeWidth={2.5} opacity={op} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Çarpma anında sınır kısa süre turuncuya döner
|
||||||
|
const carpma = Math.max(
|
||||||
|
...HAYALETLER.map((_, i) => {
|
||||||
|
const u = (f - (240 + i * 5)) / 26 - 1;
|
||||||
|
return u >= 0 && u < 0.4 ? 1 - u / 0.4 : 0;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kartın adımları
|
||||||
|
const adimStil = (k: number): React.CSSProperties => {
|
||||||
|
const [a, b] = ADIM[k];
|
||||||
|
const g = aralik(f, a, a + 10, ease.out);
|
||||||
|
const c = aralik(f, b - 8, b, ease.in);
|
||||||
|
return {
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
opacity: g * (1 - c),
|
||||||
|
translate: `${(1 - g) * 90 - c * 90}px 0`,
|
||||||
|
filter: `blur(${(1 - g) * 6 + c * 6}px)`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const secildi = (t: number) => aralik(f, t, t + 5);
|
||||||
|
const uzerinde = (t: number) => aralik(f, t - 7, t - 2) * (1 - aralik(f, t, t + 3));
|
||||||
|
const aktifAdim = f < ADIM[1][0] + 4 ? 0 : f < ADIM[2][0] + 4 ? 1 : 2;
|
||||||
|
|
||||||
|
const basliklar = ["Hangi alanlara ilgi duyuyorsun?", "Nerede okumak istersin?", "Üniversite tipi & öncelikler"];
|
||||||
|
const aciklamalar = [
|
||||||
|
"Sıralamanla ulaşabileceğin alanlar. En az bir tane seç.",
|
||||||
|
"En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.",
|
||||||
|
"Bunlar listenin dengesini belirler.",
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
|
||||||
|
{/* ——— A: sihirbaz kartı (derinliğe çekilerek çıkar) ——— */}
|
||||||
|
<AbsoluteFill style={{ perspective: 1600 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: KART.x,
|
||||||
|
top: KART.y,
|
||||||
|
width: KART.w,
|
||||||
|
height: KART.h,
|
||||||
|
borderRadius: 48,
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `2px solid ${R.slate200}`,
|
||||||
|
boxShadow: "0 24px 60px -30px rgba(15,23,42,0.25)",
|
||||||
|
transformOrigin: "0% 50%",
|
||||||
|
transform: `translateZ(${-700 * kartCikis}px) rotateY(${24 * kartCikis}deg) translateX(${-120 * kartCikis}px)`,
|
||||||
|
opacity: aralik(f, 0, 8) * (1 - aralik(f, 214, 232)),
|
||||||
|
filter: `blur(${kartCikis * 10}px)`,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: "absolute", left: 0, top: 0, width: KART.w, height: KART.h, translate: `${-KART.x}px ${-KART.y}px` }}>
|
||||||
|
{/* İlerleme */}
|
||||||
|
<div style={{ position: "absolute", left: ICERIK_X, top: KART.y + 58, display: "flex", gap: 10 }}>
|
||||||
|
{[0, 1, 2].map((k) => (
|
||||||
|
<div key={k} style={{ width: 72, height: 10, borderRadius: 99, background: k <= aktifAdim ? R.primary : R.slate200 }} />
|
||||||
|
))}
|
||||||
|
<span style={{ marginLeft: 16, marginTop: -9, fontFamily: FONT.govde, fontSize: 24, fontWeight: 600, color: R.slate500 }}>
|
||||||
|
{aktifAdim + 1} / 3
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{[0, 1, 2].map((k) => (
|
||||||
|
<div key={k} style={adimStil(k)}>
|
||||||
|
<div style={{ position: "absolute", left: ICERIK_X, top: KART.y + 110, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 46, color: R.slate900, whiteSpace: "nowrap" }}>
|
||||||
|
{basliklar[k]}
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", left: ICERIK_X, top: KART.y + 180, fontFamily: FONT.govde, fontSize: 26, color: R.slate500, whiteSpace: "nowrap" }}>
|
||||||
|
{aciklamalar[k]}
|
||||||
|
</div>
|
||||||
|
{k === 0 &&
|
||||||
|
alanCip.map((c, i) => (
|
||||||
|
<Cip key={c.metin} boy={CIP_BOY} secili={i === 0 ? secildi(T_ALAN) : 0} hover={i === 0 ? uzerinde(T_ALAN) : 0} style={{ position: "absolute", left: c.x, top: c.y }}>
|
||||||
|
{c.metin}
|
||||||
|
</Cip>
|
||||||
|
))}
|
||||||
|
{k === 1 && (
|
||||||
|
<>
|
||||||
|
<MiniHarita x={ICERIK_X} y={KART.y + 262} f={f} />
|
||||||
|
{ilCip.map((c, i) => (
|
||||||
|
<Cip key={c.metin} boy={CIP_BOY} secili={i < 3 ? secildi(T_IL[i]) : 0} hover={i < 3 ? uzerinde(T_IL[i]) : 0} style={{ position: "absolute", left: c.x, top: c.y }}>
|
||||||
|
{c.metin}
|
||||||
|
</Cip>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{k === 2 && (
|
||||||
|
<>
|
||||||
|
<div style={{ position: "absolute", left: ICERIK_X, top: 440, fontFamily: FONT.govde, fontSize: 26, fontWeight: 600, color: R.slate700 }}>
|
||||||
|
Üniversite tipi
|
||||||
|
</div>
|
||||||
|
{turCip.map((c, i) => {
|
||||||
|
const Ikon = [Landmark, Building2, Scale][i];
|
||||||
|
const sec = i === 0 ? secildi(T_TUR) : 0;
|
||||||
|
return (
|
||||||
|
<Cip
|
||||||
|
key={c.metin}
|
||||||
|
boy={CIP_BOY}
|
||||||
|
secili={sec}
|
||||||
|
hover={i === 0 ? uzerinde(T_TUR) : 0}
|
||||||
|
ikon={<Ikon size={CIP_BOY * 0.35} color={karis(R.primary, R.beyaz, sec)} strokeWidth={2.2} aria-hidden />}
|
||||||
|
style={{ position: "absolute", left: c.x, top: c.y }}
|
||||||
|
>
|
||||||
|
{c.metin}
|
||||||
|
</Cip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div style={{ position: "absolute", left: ICERIK_X, top: 620, fontFamily: FONT.govde, fontSize: 26, fontWeight: 600, color: R.slate700 }}>
|
||||||
|
Öncelikler
|
||||||
|
</div>
|
||||||
|
{cipDiz(["Burslu", "Öğretim dili", "Evime yakın"], ICERIK_X, 672).map((c) => (
|
||||||
|
<Cip key={c.metin} boy={CIP_BOY} style={{ position: "absolute", left: c.x, top: c.y }}>
|
||||||
|
{c.metin}
|
||||||
|
</Cip>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
|
||||||
|
{/* ——— A: aday havuzu paneli ——— */}
|
||||||
|
<div style={{ opacity: aralik(f, 6, 16) * (1 - panelCikis) }}>
|
||||||
|
<Kutu x={ALAN.x} w={660} y={186} h={50}>
|
||||||
|
<Rozet>Nasıl çalışır? · 2/3</Rozet>
|
||||||
|
</Kutu>
|
||||||
|
<div style={{ position: "absolute", left: ALAN.x, top: 262, fontFamily: FONT.govde, fontSize: 30, fontWeight: 600, color: R.slate500 }}>
|
||||||
|
Aday havuzu
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", left: ALAN.x - 6, top: 300, display: "flex", alignItems: "baseline", gap: 18 }}>
|
||||||
|
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 176, lineHeight: 1, letterSpacing: "-0.01em", color: R.slate900 }}>
|
||||||
|
{tr(n)}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontFamily: FONT.govde, fontSize: 38, fontWeight: 600, color: R.slate400 }}>program</span>
|
||||||
|
</div>
|
||||||
|
{HUNI.map((h, i) => {
|
||||||
|
const op = i === adim ? aralik(f, i === 0 ? 0 : SAYAC[i - 1].a, (i === 0 ? 0 : SAYAC[i - 1].a) + 8) : 0;
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ position: "absolute", left: ALAN.x, top: 492, opacity: op, translate: `0 ${(1 - op) * 10}px`, fontFamily: FONT.govde, fontSize: 28, fontWeight: 600, color: i === 4 ? R.primary : R.slate600, whiteSpace: "nowrap" }}>
|
||||||
|
{h.etiket}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ——— B: havuz sınırı ——— */}
|
||||||
|
{sinirOp > 0 && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: SINIR.x,
|
||||||
|
top: SINIR.y,
|
||||||
|
width: SINIR.w,
|
||||||
|
height: SINIR.h,
|
||||||
|
borderRadius: 40,
|
||||||
|
border: `3px dashed ${karis(R.slate300, R.turuncu, carpma)}`,
|
||||||
|
background: "rgba(255,255,255,0.7)",
|
||||||
|
opacity: sinirOp,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ position: "absolute", left: SINIR.x + 8, top: SINIR.y - 52, opacity: sinirOp, fontFamily: FONT.govde, fontSize: 28, fontWeight: 600, color: R.slate600, whiteSpace: "nowrap" }}>
|
||||||
|
Aday havuzu · <span style={{ fontFamily: FONT.baslik, fontWeight: 700, color: R.slate900 }}>179</span> gerçek program
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<svg width={W} height={1080} style={{ position: "absolute", inset: 0 }}>
|
||||||
|
{hayaletler}
|
||||||
|
{rects}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<Sequence from={230} durationInFrames={70}>
|
||||||
|
<Kutu y={130} h={140}>
|
||||||
|
<Cikis bas={60}>
|
||||||
|
<KineticCenterBuild
|
||||||
|
text="Yapay zekâ yalnızca bu havuzdan seçer."
|
||||||
|
birim="kelime"
|
||||||
|
fontFamily={FONT.baslik}
|
||||||
|
fontWeight={700}
|
||||||
|
fontSize={72}
|
||||||
|
color={R.slate900}
|
||||||
|
letterSpacing={0.015}
|
||||||
|
speed={1.5}
|
||||||
|
vurgu={{ indices: [2], style: { fontFamily: FONT.vurgu, fontStyle: "italic", color: R.primary, letterSpacing: -0.05 } }}
|
||||||
|
/>
|
||||||
|
</Cikis>
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={262} durationInFrames={48}>
|
||||||
|
<Kutu y={880} h={80}>
|
||||||
|
<FocusBlurResolve text="Havuzda olmayan bir bölümü listene yazamaz." fontFamily={FONT.govde} fontSize={36} fontWeight={600} color={R.slate500} />
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* ——— B4: sol başlık + 24'lük şerit + dilimler ——— */}
|
||||||
|
{f >= 316 && <ListeBasligi f={f} />}
|
||||||
|
|
||||||
|
{/* ——— B4: liste ——— */}
|
||||||
|
{f >= 318 && <Liste f={f} hover={aralik(f, TIK - 10, TIK - 4)} />}
|
||||||
|
|
||||||
|
<Imlec yol={yol} gorunur={imlecGorunur} el={[[T_ALAN - 8, T_TUR + 4], [TIK - 14, SAHNE.havuz]]} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** İl adımındaki dot-matrix harita; seçilen iller turuncu yanar */
|
||||||
|
const MiniHarita: React.FC<{ x: number; y: number; f: number }> = ({ x, y, f }) => {
|
||||||
|
const hucre = 10.6;
|
||||||
|
const boy = hucre * PIKSEL.nokta;
|
||||||
|
const secim = Object.entries(IL_HUCRE).map(([ad, [cx, cy]], i) => ({ ad, cx, cy, s: aralik(f, T_IL[i], T_IL[i] + 8, ease.geriTep) }));
|
||||||
|
return (
|
||||||
|
<svg width={64 * hucre} height={26 * hucre} style={{ position: "absolute", left: x, top: y, overflow: "visible" }}>
|
||||||
|
{TURKIYE_HUCRELER.map(({ x: gx, y: gy }) => {
|
||||||
|
let isi = 0;
|
||||||
|
for (const s of secim) {
|
||||||
|
const d = Math.hypot(gx - s.cx, gy - s.cy);
|
||||||
|
if (d <= 1.5) isi = Math.max(isi, s.s * (d < 0.5 ? 1 : 0.75));
|
||||||
|
}
|
||||||
|
const b = boy * (1 + 0.25 * isi);
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={`${gx}-${gy}`}
|
||||||
|
x={gx * hucre + (hucre - b) / 2}
|
||||||
|
y={gy * hucre + (hucre - b) / 2}
|
||||||
|
width={b}
|
||||||
|
height={b}
|
||||||
|
rx={b * PIKSEL.rx}
|
||||||
|
fill={karis(R.primary, R.turuncu, clamp01(isi))}
|
||||||
|
fillOpacity={lerp(0.2 + opaklikFor(gx, gy) * 1.5, 0.95, clamp01(isi))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SolaYasli: React.FC<{ x: number; y: number; metin: React.ReactNode; genislik: number; bas: number; boy: number; family: string; renk?: string }> = ({ x, y, metin, genislik, bas, boy, family, renk = R.slate900 }) => (
|
||||||
|
<Sequence from={bas} layout="none">
|
||||||
|
<Kutu x={x - 40} y={y} w={genislik + 80} h={boy * 1.2}>
|
||||||
|
<TrackingIn text={metin} fontFamily={family} fontSize={boy} color={renk} startTracking={0.5} endTracking={0.015} startBlur={12} />
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ListeBasligi: React.FC<{ f: number }> = ({ f }) => {
|
||||||
|
const boy = 118;
|
||||||
|
const w1 = olc("Dengeli", { family: FONT.baslik, size: boy, weight: 700, tracking: 0.015 });
|
||||||
|
const w2 = olc("24'lük", { family: FONT.vurgu, size: boy, weight: 700, italic: true, tracking: -0.05 });
|
||||||
|
const w3 = olc("listen hazır.", { family: FONT.baslik, size: boy, weight: 700, tracking: 0.015 });
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Kutu x={140} w={600} y={176} h={60} style={{ opacity: aralik(f, 318, 328) }}>
|
||||||
|
<Rozet>Nasıl çalışır? · 3/3</Rozet>
|
||||||
|
</Kutu>
|
||||||
|
<SolaYasli x={140} y={250} metin="Dengeli" genislik={w1} bas={320} boy={boy} family={FONT.baslik} />
|
||||||
|
<SolaYasli
|
||||||
|
x={140}
|
||||||
|
y={380}
|
||||||
|
genislik={w2}
|
||||||
|
bas={328}
|
||||||
|
boy={boy}
|
||||||
|
family={FONT.vurgu}
|
||||||
|
metin={<span style={{ fontStyle: "italic", color: R.primary, letterSpacing: "-0.05em" }}>24'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)`,
|
||||||
|
};
|
||||||
|
};
|
||||||
242
video/src/sahneler/S5Danisman.tsx
Normal file
242
video/src/sahneler/S5Danisman.tsx
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import { ArrowUp } from "lucide-react";
|
||||||
|
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
||||||
|
import { useTypewriter } from "@/lib/remocn-ui";
|
||||||
|
import { Imlec, type Yol } from "../bilesenler/Imlec";
|
||||||
|
import { LogoIsaret } from "../bilesenler/Logo";
|
||||||
|
import { PikselZemin } from "../bilesenler/PikselZemin";
|
||||||
|
import { DilimEtiketi, GirisHap, PikselTrend } from "../bilesenler/UI";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
import { R } from "../marka";
|
||||||
|
import { SORULAN, ORNEK_SIRA_METNI } from "../veri";
|
||||||
|
import { aralik, ease, karis, lerp, olc, tr } from "../yardimci";
|
||||||
|
import { GECIS, SAHNE, gecisBasi } from "../zaman";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5 · Danışman. Tıklanan satır sohbetin konusu olur. Soru yazılır, gönder
|
||||||
|
* düğmesine basılır; yanıt beklenirken logonun üç karesi nabız atar
|
||||||
|
* (animasyonlu-mark.tsx "nabız"). Yanıt yalnızca gerçek taban geçmişini
|
||||||
|
* söyler ve altında 6 yılın piksel trendi belirir; son yılın turuncu pikseli
|
||||||
|
* bir sonraki dalganın çıkış noktasıdır.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PANEL = { x: 330, y: 64, w: 1260, h: 952 };
|
||||||
|
const SORU = "Bu tercih neden dengeli dilimde?";
|
||||||
|
const YANIT = `2026'da taban sırası ${tr(SORULAN.sira[5])}, seninki ${ORNEK_SIRA_METNI}. Ama son 6 yılda taban ${tr(Math.min(...SORULAN.sira))} ile ${tr(Math.max(...SORULAN.sira))} arasında gidip geldi — bu kadar dalgalı olduğu için dengeli dilimde.`;
|
||||||
|
|
||||||
|
const YAZ_BAS = 12;
|
||||||
|
const GONDER = 58;
|
||||||
|
const BEKLE: [number, number] = [64, 86];
|
||||||
|
const TREND_BAS = 150;
|
||||||
|
|
||||||
|
const GIRIS_Y = PANEL.y + PANEL.h - 132;
|
||||||
|
const GONDER_X = PANEL.x + PANEL.w - 64 - 42;
|
||||||
|
const GONDER_Y = GIRIS_Y + 52;
|
||||||
|
|
||||||
|
// Yanıt balonu ve içindeki trend (satır sayısı gerçek yazı genişliğiyle)
|
||||||
|
const BALON = { x: PANEL.x + 56, y: PANEL.y + 330, w: 960, pad: 40, ust: 30, punto: 32, satir: 48 };
|
||||||
|
const TREND_H = { hucre: 34, yukseklik: 90 };
|
||||||
|
const satirSayisi = () => {
|
||||||
|
const bosluk = olc(" ", { family: FONT.govde, size: BALON.punto });
|
||||||
|
let n = 1;
|
||||||
|
let x = 0;
|
||||||
|
for (const k of YANIT.split(" ")) {
|
||||||
|
const w = olc(k, { family: FONT.govde, size: BALON.punto });
|
||||||
|
if (x > 0 && x + bosluk + w > BALON.w - BALON.pad * 2) {
|
||||||
|
n++;
|
||||||
|
x = w;
|
||||||
|
} else x += (x > 0 ? bosluk : 0) + w;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
};
|
||||||
|
const trendKonumu = () => ({
|
||||||
|
x: BALON.x + BALON.pad,
|
||||||
|
y: BALON.y + BALON.ust + satirSayisi() * BALON.satir + 28,
|
||||||
|
});
|
||||||
|
/** Trendin son (2026) pikseli: bir sonraki dalga buradan yayılır */
|
||||||
|
export const danismanDalgaNoktasi = () => {
|
||||||
|
const t = trendKonumu();
|
||||||
|
const son = SORULAN.sira[5];
|
||||||
|
const min = Math.min(...SORULAN.sira);
|
||||||
|
const max = Math.max(...SORULAN.sira);
|
||||||
|
return {
|
||||||
|
x: t.x + 5 * TREND_H.hucre + TREND_H.hucre / 2,
|
||||||
|
y: t.y + ((son - min) / (max - min)) * TREND_H.yukseklik + (TREND_H.hucre * 0.74) / 2,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const S5Danisman: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const soru = useTypewriter(SORU, { cps: 24, startFrame: YAZ_BAS });
|
||||||
|
const yanit = useTypewriter(YANIT, { cps: 70, startFrame: BEKLE[1] });
|
||||||
|
const gonderildi = f >= GONDER;
|
||||||
|
|
||||||
|
const yol: Yol = [
|
||||||
|
{ at: 8, x: 1700, y: 1050 },
|
||||||
|
{ at: GONDER - 4, x: GONDER_X, y: GONDER_Y, duration: 30 },
|
||||||
|
{ at: GONDER, x: GONDER_X, y: GONDER_Y, click: true, duration: 4 },
|
||||||
|
{ at: 100, x: 1740, y: 1010, duration: 30 },
|
||||||
|
];
|
||||||
|
const bas = f >= GONDER && f < GONDER + 8 ? 1 - Math.abs(f - GONDER - 4) / 4 : 0;
|
||||||
|
const soruBalon = aralik(f, GONDER + 2, GONDER + 14, ease.geriTep);
|
||||||
|
const bekliyor = f >= BEKLE[0] && f < BEKLE[1] + 4;
|
||||||
|
const yanitBalon = aralik(f, BEKLE[1] - 4, BEKLE[1] + 8, ease.out);
|
||||||
|
const trend = trendKonumu();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
|
||||||
|
<PikselZemin tohum={51} kenar={0.2} guc={0.8} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: PANEL.x,
|
||||||
|
top: PANEL.y,
|
||||||
|
width: PANEL.w,
|
||||||
|
height: PANEL.h,
|
||||||
|
borderRadius: 48,
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `2px solid ${R.slate200}`,
|
||||||
|
boxShadow: "0 30px 80px -40px rgba(15,23,42,0.35)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Başlık */}
|
||||||
|
<div style={{ position: "absolute", left: 56, top: 40, display: "flex", alignItems: "center", gap: 22 }}>
|
||||||
|
<LogoIsaret
|
||||||
|
boy={68}
|
||||||
|
kareStil={(i) => {
|
||||||
|
const faz = ((f / 30) * 2 * Math.PI) / 1.8 - (i * 2 * Math.PI) / 3;
|
||||||
|
const nabiz = bekliyor ? 0.5 + 0.5 * Math.sin(faz) : 1;
|
||||||
|
return { olcek: lerp(0.78, 1, nabiz) };
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 40, color: R.slate900 }}>Danışman</div>
|
||||||
|
<div style={{ fontFamily: FONT.govde, fontSize: 24, color: R.slate500, marginTop: 4 }}>
|
||||||
|
{SORULAN.program} · {SORULAN.universite}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", right: 56, top: 58 }}>
|
||||||
|
<DilimEtiketi dilim={SORULAN.dilim} boy={28} />
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", left: 0, right: 0, top: 150, height: 2, background: R.slate100 }} />
|
||||||
|
|
||||||
|
{/* Soru balonu */}
|
||||||
|
{soruBalon > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: 56,
|
||||||
|
top: 210,
|
||||||
|
maxWidth: 820,
|
||||||
|
padding: "24px 34px",
|
||||||
|
borderRadius: 32,
|
||||||
|
background: R.primary,
|
||||||
|
color: R.beyaz,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: 500,
|
||||||
|
transformOrigin: "100% 100%",
|
||||||
|
scale: `${lerp(0.6, 1, soruBalon)}`,
|
||||||
|
opacity: Math.min(1, soruBalon * 1.4),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{SORU}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bekleme: üç kare nabız */}
|
||||||
|
{bekliyor && (
|
||||||
|
<div style={{ position: "absolute", left: 56, top: 330, padding: "26px 30px", borderRadius: 32, background: R.slate100, display: "flex", gap: 12, opacity: aralik(f, BEKLE[0], BEKLE[0] + 4) }}>
|
||||||
|
{[0, 1, 2].map((i) => {
|
||||||
|
const s = 0.55 + 0.45 * Math.sin((f / 30) * 2 * Math.PI * 1.4 - i * 1.1);
|
||||||
|
return <div key={i} style={{ width: 22, height: 22, borderRadius: 5, background: [R.mavi, R.seftali, R.turuncuLogo][i], scale: `${lerp(0.6, 1, s)}` }} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Yanıt balonu */}
|
||||||
|
{yanitBalon > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: BALON.x - PANEL.x,
|
||||||
|
top: BALON.y - PANEL.y,
|
||||||
|
width: BALON.w,
|
||||||
|
padding: `${BALON.ust}px ${BALON.pad}px 36px`,
|
||||||
|
borderRadius: 32,
|
||||||
|
background: R.slate100,
|
||||||
|
color: R.slate800,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: BALON.punto,
|
||||||
|
lineHeight: `${BALON.satir}px`,
|
||||||
|
opacity: yanitBalon,
|
||||||
|
translate: `0 ${(1 - yanitBalon) * 16}px`,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Yer tutucu: balon baştan tam boyda dursun, metin akarken zıplamasın */}
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<span style={{ visibility: "hidden" }}>{YANIT}</span>
|
||||||
|
<span style={{ position: "absolute", inset: 0 }}>{yanit.text}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ height: TREND_H.yukseklik + 60 }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trend: 6 yılın taban sırası piksel piksel */}
|
||||||
|
<div style={{ position: "absolute", left: trend.x, top: trend.y, opacity: aralik(f, TREND_BAS - 4, TREND_BAS + 4) }}>
|
||||||
|
<PikselTrend sira={SORULAN.sira} hucre={TREND_H.hucre} yukseklik={TREND_H.yukseklik} ilerleme={aralik(f, TREND_BAS, TREND_BAS + 14)} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: trend.x + 6 * TREND_H.hucre + 34,
|
||||||
|
top: trend.y + 18,
|
||||||
|
opacity: aralik(f, TREND_BAS + 8, TREND_BAS + 16),
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: 24,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: R.slate500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<b style={{ color: R.slate900, fontWeight: 600 }}>Taban sırası, 2021–2026</b>
|
||||||
|
</div>
|
||||||
|
<div>Kaynak: YÖK Atlas · yapay zekâ yorumu değil, gerçek geçmiş</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Giriş */}
|
||||||
|
<div style={{ position: "absolute", left: PANEL.x + 56, top: GIRIS_Y, display: "flex", alignItems: "center", gap: 20 }}>
|
||||||
|
<GirisHap
|
||||||
|
genislik={PANEL.w - 112 - 124}
|
||||||
|
yukseklik={104}
|
||||||
|
deger={gonderildi ? "" : soru.text}
|
||||||
|
yaziyor={soru.typing}
|
||||||
|
odak={aralik(f, 6, 10)}
|
||||||
|
placeholder="Listen hakkında bir şey sor…"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 104,
|
||||||
|
height: 104,
|
||||||
|
borderRadius: 999,
|
||||||
|
background: karis(R.slate200, R.primary, aralik(f, YAZ_BAS, YAZ_BAS + 6)),
|
||||||
|
color: R.beyaz,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
scale: `${1 - 0.06 * bas}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowUp size={40} strokeWidth={2.6} aria-hidden />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Imlec yol={yol} gorunur={[6, 130]} el={[[GONDER - 12, GONDER + 10]]} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DANISMAN_DALGA_BASI = gecisBasi(SAHNE.danisman, GECIS.g5);
|
||||||
120
video/src/sahneler/S6Deger.tsx
Normal file
120
video/src/sahneler/S6Deger.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
|
||||||
|
import { FocusBlurResolve } from "@/components/remocn/focus-blur-resolve";
|
||||||
|
import { SlotMachineRoll } from "@/components/remocn/slot-machine-roll";
|
||||||
|
import { TrackingIn } from "@/components/remocn/tracking-in";
|
||||||
|
import { Kutu } from "../bilesenler/Kinetik";
|
||||||
|
import { Rozet } from "../bilesenler/Logo";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
import { R } from "../marka";
|
||||||
|
import { aralik, ease, lerp, olc } from "../yardimci";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 6 · Değer. Solda "Tahmin değil, veri." ve dürüstlük notu; sağda sitenin
|
||||||
|
* gösterge kareleri (DESIGN.md §4.11): logodaki üç kare gibi iki sütun,
|
||||||
|
* sağdaki aşağı kaymış, boylar farklı. Kareler derinlikten gelip oturur,
|
||||||
|
* sayılar slot makinesi gibi yerine döner.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Kare = { renk: string; sayi: [string, string]; etiket: string; x: number; y: number; w: number; h: number; bas: number };
|
||||||
|
|
||||||
|
const SUTUN_W = 400;
|
||||||
|
const SOL_X = 990;
|
||||||
|
const SAG_X = SOL_X + SUTUN_W + 36;
|
||||||
|
const UST = 96;
|
||||||
|
|
||||||
|
// Renk sırası: en önemli turuncu, sonra mavi, indigo, yeşil (§4.11)
|
||||||
|
export const KARELER: Kare[] = [
|
||||||
|
{ renk: R.turuncu, sayi: ["00.000+", "21.000+"], etiket: "program tarandı", x: SOL_X, y: UST, w: SUTUN_W, h: 420, bas: 8 },
|
||||||
|
{ renk: R.primary, sayi: ["0 yıl", "6 yıl"], etiket: "taban sıralaması trendi", x: SAG_X, y: UST + 130, w: SUTUN_W, h: 330, bas: 15 },
|
||||||
|
{ renk: R.indigo500, sayi: ["%000", "%100"], etiket: "resmî YÖK Atlas verisi", x: SOL_X, y: UST + 460, w: SUTUN_W, h: 300, bas: 22 },
|
||||||
|
{ renk: R.green600, sayi: ["00", "24"], etiket: "tercihlik dengeli liste", x: SAG_X, y: UST + 500, w: SUTUN_W, h: 390, bas: 29 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Son karenin merkezi: kapanışa giden koyu dalga buradan */
|
||||||
|
export const degerDalgaNoktasi = () => {
|
||||||
|
const k = KARELER[3];
|
||||||
|
return { x: k.x + k.w / 2, y: k.y + k.h / 2 };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const S6Deger: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const boy = 132;
|
||||||
|
const w1 = olc("Tahmin değil,", { family: FONT.baslik, size: boy, weight: 700, tracking: 0.015 });
|
||||||
|
const w2 = olc("veri.", { family: FONT.vurgu, size: boy, weight: 700, italic: true, tracking: -0.05 });
|
||||||
|
const n1 = "Yerleşme garantisi vermiyoruz.";
|
||||||
|
const n2 = "Riski satır satır gösteriyoruz.";
|
||||||
|
const nw1 = olc(n1, { family: FONT.govde, size: 38, weight: 600 });
|
||||||
|
const nw2 = olc(n2, { family: FONT.govde, size: 38, weight: 600 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate50, overflow: "hidden" }}>
|
||||||
|
<Kutu x={140} w={700} y={272} h={60} style={{ opacity: aralik(f, 4, 14) }}>
|
||||||
|
<Rozet>Neden KolayTercih?</Rozet>
|
||||||
|
</Kutu>
|
||||||
|
<Sequence from={6} layout="none">
|
||||||
|
<Kutu x={100} w={w1 + 80} y={350} h={170}>
|
||||||
|
<TrackingIn text="Tahmin değil," fontFamily={FONT.baslik} fontSize={boy} color={R.slate900} startTracking={0.5} endTracking={0.015} />
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={16} layout="none">
|
||||||
|
<Kutu x={100} w={w2 + 80} y={500} h={170}>
|
||||||
|
<TrackingIn
|
||||||
|
text={<span style={{ fontStyle: "italic", color: R.primary, letterSpacing: "-0.05em" }}>veri.</span>}
|
||||||
|
fontFamily={FONT.vurgu}
|
||||||
|
fontSize={boy}
|
||||||
|
color={R.primary}
|
||||||
|
startTracking={0.9}
|
||||||
|
endTracking={-0.05}
|
||||||
|
/>
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={44} layout="none">
|
||||||
|
<Kutu x={140} w={nw1} y={700} h={60}>
|
||||||
|
<FocusBlurResolve text={n1} fontFamily={FONT.govde} fontSize={38} fontWeight={600} color={R.slate700} />
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={54} layout="none">
|
||||||
|
<Kutu x={140} w={nw2} y={756} h={60}>
|
||||||
|
<FocusBlurResolve text={n2} fontFamily={FONT.govde} fontSize={38} fontWeight={600} color={R.slate500} />
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Gösterge kareleri — derinlikten gelip oturur */}
|
||||||
|
<AbsoluteFill style={{ perspective: 1800, perspectiveOrigin: "70% 50%" }}>
|
||||||
|
{KARELER.map((k, i) => {
|
||||||
|
const g = aralik(f, k.bas, k.bas + 22, ease.out);
|
||||||
|
const yuz = Math.sin((f + i * 17) / 22) * 6 * g;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: k.x,
|
||||||
|
top: k.y,
|
||||||
|
width: k.w,
|
||||||
|
height: k.h,
|
||||||
|
borderRadius: 48,
|
||||||
|
background: k.renk,
|
||||||
|
color: R.beyaz,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 14,
|
||||||
|
opacity: aralik(f, k.bas, k.bas + 8),
|
||||||
|
transform: `translateZ(${lerp(-900, 0, g)}px) rotateX(${lerp(38, 0, g)}deg) rotateY(${lerp(-24, 0, g)}deg) translateY(${yuz}px)`,
|
||||||
|
filter: `blur(${(1 - g) * 16}px)`,
|
||||||
|
boxShadow: `0 30px 60px -30px rgba(15,23,42,${0.35 * g})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sequence from={k.bas + 10} layout="none">
|
||||||
|
<SlotMachineRoll inline from={k.sayi[0]} to={k.sayi[1]} fontFamily={FONT.baslik} fontSize={92} color={R.beyaz} stagger={3} />
|
||||||
|
</Sequence>
|
||||||
|
<span style={{ fontFamily: FONT.govde, fontSize: 30, fontWeight: 700, color: "rgba(255,255,255,0.9)" }}>{k.etiket}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AbsoluteFill>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
205
video/src/sahneler/S7Kapanis.tsx
Normal file
205
video/src/sahneler/S7Kapanis.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
|
||||||
|
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
|
||||||
|
import { TrackingIn } from "@/components/remocn/tracking-in";
|
||||||
|
import { Vinyet } from "../bilesenler/Gren";
|
||||||
|
import { Imlec, isi, isiIzi, type Yol } from "../bilesenler/Imlec";
|
||||||
|
import { Kutu } from "../bilesenler/Kinetik";
|
||||||
|
import { LogoIsaret } from "../bilesenler/Logo";
|
||||||
|
import { CtaButon } from "../bilesenler/UI";
|
||||||
|
import { FONT } from "../fontlar";
|
||||||
|
import { dalgaYaricapi } from "../gecisler/piksel-dalga";
|
||||||
|
import { H, LOGO_KARELER, PIKSEL, R, W } from "../marka";
|
||||||
|
import { aralik, clamp01, ease, hash, karis, lerp, olc } from "../yardimci";
|
||||||
|
import { GECIS } from "../zaman";
|
||||||
|
import { degerDalgaNoktasi } from "./S6Deger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 7 · Kapanış. Sitenin tek koyu yüzeyi olan CTA bandının dili: kenarlarda
|
||||||
|
* büyüklü küçüklü, hafif kaymış kareler (PixelMasonry). Logo kareleri
|
||||||
|
* "rota" hareketiyle yer değiştirip oturur, kelime marka harf aralığı
|
||||||
|
* daralarak gelir, slogan kinetik kurulur. İmleç CTA'ya gelirken yolundaki
|
||||||
|
* kareler turuncuya ısınır; tıklar.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const LOGO_BOY = 200;
|
||||||
|
const LOGO_Y = 330;
|
||||||
|
const KELIME_BOY = 126;
|
||||||
|
const KELIME_OFSET = (46.7 / 44) * LOGO_BOY;
|
||||||
|
const CTA_Y = 690;
|
||||||
|
const CTA_H = 112;
|
||||||
|
const TIK = 138;
|
||||||
|
|
||||||
|
// PixelMasonry: 4×4 hücrelik makro hücrelere 2–4 hücrelik tek kare, kenar bandında
|
||||||
|
const MASONRY = (() => {
|
||||||
|
const hucre = 22;
|
||||||
|
const makro = hucre * 4;
|
||||||
|
const bant = Math.min(W, H) * 0.3;
|
||||||
|
const out: { x: number; y: number; boy: number; op: number; vurgu: string | null }[] = [];
|
||||||
|
for (let j = 0; j * makro < H; j++) {
|
||||||
|
for (let i = 0; i * makro < W; i++) {
|
||||||
|
const cx = i * makro + makro / 2;
|
||||||
|
const cy = j * makro + makro / 2;
|
||||||
|
const kenar = Math.min(cx, W - cx, cy, H - cy);
|
||||||
|
const yog = clamp01(1 - kenar / bant);
|
||||||
|
if (hash(i, j, 7) > yog * 0.95) continue;
|
||||||
|
const n = 2 + Math.floor(hash(i, j, 8) * 3);
|
||||||
|
const boy = n * hucre;
|
||||||
|
const bos = makro - boy;
|
||||||
|
const r = hash(i, j, 9);
|
||||||
|
out.push({
|
||||||
|
x: i * makro + hash(i, j, 10) * bos + boy / 2,
|
||||||
|
y: j * makro + hash(i, j, 11) * bos + boy / 2,
|
||||||
|
boy: boy * 0.86,
|
||||||
|
op: 0.05 + hash(i, j, 12) * 0.08,
|
||||||
|
vurgu: r > 0.93 ? R.turuncu : r > 0.87 ? R.mavi : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const S7Kapanis: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const kok = degerDalgaNoktasi();
|
||||||
|
const r = dalgaYaricapi(f / GECIS.g6, kok, 440);
|
||||||
|
|
||||||
|
const ww =
|
||||||
|
olc("Kolay", { family: FONT.vurgu, size: KELIME_BOY, weight: 300, tracking: -0.05 }) +
|
||||||
|
olc("Tercih", { family: FONT.vurgu, size: KELIME_BOY, weight: 600, tracking: -0.05 });
|
||||||
|
const sol = W / 2 - (KELIME_OFSET + ww) / 2;
|
||||||
|
const ctaW =
|
||||||
|
2 * 0.52 * CTA_H + olc("kolaytercih.com", { family: FONT.govde, size: CTA_H * 0.31, weight: 600 }) + 16 + CTA_H * 0.31;
|
||||||
|
const cta = { x: W / 2, y: CTA_Y + CTA_H / 2 };
|
||||||
|
|
||||||
|
const yol: Yol = [
|
||||||
|
{ at: 96, x: 1860, y: 1050 },
|
||||||
|
{ at: 120, x: 1500, y: 900, duration: 24 },
|
||||||
|
{ at: TIK - 2, x: cta.x + ctaW * 0.28, y: cta.y + 8, duration: 16 },
|
||||||
|
{ at: TIK, x: cta.x + ctaW * 0.28, y: cta.y + 8, click: true, duration: 2 },
|
||||||
|
];
|
||||||
|
const izler = isiIzi(yol, f, [96, 999]);
|
||||||
|
|
||||||
|
// Logo "rota": her kare sıradakinin yerine süzülür, bir tur sonra evine döner
|
||||||
|
const rota = (i: number) => {
|
||||||
|
const konum = (k: number) => {
|
||||||
|
const q = LOGO_KARELER[(i + k) % 3];
|
||||||
|
const ev = LOGO_KARELER[i];
|
||||||
|
return { dx: q.x + q.boy / 2 - (ev.x + ev.boy / 2), dy: q.y + q.boy / 2 - (ev.y + ev.boy / 2) };
|
||||||
|
};
|
||||||
|
const adim = [aralik(f, 14 + i * 3, 30 + i * 3, ease.inOut), aralik(f, 34 + i * 3, 50 + i * 3, ease.inOut), aralik(f, 54 + i * 3, 70 + i * 3, ease.inOut)];
|
||||||
|
const a = konum(0);
|
||||||
|
const b = konum(1);
|
||||||
|
const c = konum(2);
|
||||||
|
let dx = lerp(a.dx, b.dx, adim[0]);
|
||||||
|
let dy = lerp(a.dy, b.dy, adim[0]);
|
||||||
|
dx = lerp(dx, c.dx, adim[1]);
|
||||||
|
dy = lerp(dy, c.dy, adim[1]);
|
||||||
|
dx = lerp(dx, 0, adim[2]);
|
||||||
|
dy = lerp(dy, 0, adim[2]);
|
||||||
|
const ucus = Math.max(...adim.map((t) => Math.sin(Math.PI * t)));
|
||||||
|
return { dx, dy, olcek: 1 - 0.16 * ucus, opaklik: 1 - 0.4 * ucus };
|
||||||
|
};
|
||||||
|
const kay = aralik(f, 66, 90, ease.inOut);
|
||||||
|
const logoSol = lerp(W / 2 - LOGO_BOY / 2, sol, kay);
|
||||||
|
const hover = aralik(f, TIK - 10, TIK - 4);
|
||||||
|
const bas = f >= TIK && f < TIK + 8 ? 1 - Math.abs(f - TIK - 4) / 4 : 0;
|
||||||
|
const son = aralik(f, 168, 180);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate900, overflow: "hidden" }}>
|
||||||
|
<svg width={W} height={H} style={{ position: "absolute", inset: 0 }}>
|
||||||
|
{MASONRY.map((m, i) => {
|
||||||
|
const d = Math.hypot(m.x - kok.x, m.y - kok.y);
|
||||||
|
const pop = ease.geriTep(clamp01((r - d - 0.3 * 440) / (0.4 * 440)));
|
||||||
|
if (pop <= 0) return null;
|
||||||
|
const h = isi(m.x, m.y, izler, 150);
|
||||||
|
const boy = m.boy * pop;
|
||||||
|
const taban = m.vurgu ?? R.beyaz;
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={i}
|
||||||
|
x={m.x - boy / 2}
|
||||||
|
y={m.y - boy / 2}
|
||||||
|
width={boy}
|
||||||
|
height={boy}
|
||||||
|
rx={boy * PIKSEL.rx}
|
||||||
|
fill={karis(taban, R.turuncu, h)}
|
||||||
|
fillOpacity={lerp(m.vurgu ? 0.55 : m.op, 0.9, h)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
<AbsoluteFill style={{ background: `radial-gradient(ellipse 46% 42% at 50% 48%, ${R.slate900} 35%, rgba(15,23,42,0) 100%)` }} />
|
||||||
|
|
||||||
|
<LogoIsaret
|
||||||
|
boy={LOGO_BOY}
|
||||||
|
kareStil={rota}
|
||||||
|
style={{ position: "absolute", left: logoSol, top: LOGO_Y - LOGO_BOY / 2, opacity: aralik(f, 4, 12), scale: `${lerp(0.6, 1, aralik(f, 4, 18, ease.geriTep))}` }}
|
||||||
|
/>
|
||||||
|
<Sequence from={70} layout="none">
|
||||||
|
<Kutu x={sol + KELIME_OFSET} w={ww} y={LOGO_Y - 80} h={160}>
|
||||||
|
<TrackingIn
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
<span style={{ fontWeight: 300 }}>Kolay</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>Tercih</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
fontFamily={FONT.vurgu}
|
||||||
|
fontSize={KELIME_BOY}
|
||||||
|
fontWeight={300}
|
||||||
|
color={R.beyaz}
|
||||||
|
startTracking={0.55}
|
||||||
|
endTracking={-0.05}
|
||||||
|
/>
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={88} layout="none">
|
||||||
|
<Kutu y={500} h={130}>
|
||||||
|
<KineticCenterBuild
|
||||||
|
text="Tercih listeni gerçek veriyle kur, panikle değil."
|
||||||
|
birim="kelime"
|
||||||
|
fontFamily={FONT.baslik}
|
||||||
|
fontWeight={700}
|
||||||
|
fontSize={68}
|
||||||
|
color={R.beyaz}
|
||||||
|
letterSpacing={0.015}
|
||||||
|
speed={1.8}
|
||||||
|
vurgu={{ indices: [2, 3], style: { fontFamily: FONT.vurgu, fontStyle: "italic", color: R.mavi400, letterSpacing: -0.05 } }}
|
||||||
|
/>
|
||||||
|
</Kutu>
|
||||||
|
</Sequence>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: W / 2 - ctaW / 2,
|
||||||
|
top: CTA_Y,
|
||||||
|
opacity: aralik(f, 112, 120),
|
||||||
|
scale: `${lerp(0.7, 1, aralik(f, 112, 126, ease.geriTep))}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CtaButon yukseklik={CTA_H} hover={hover} bas={bas}>
|
||||||
|
kolaytercih.com
|
||||||
|
</CtaButon>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 980,
|
||||||
|
textAlign: "center",
|
||||||
|
opacity: aralik(f, 124, 136) * 0.9,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontSize: 22,
|
||||||
|
color: R.slate400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bağımsız bir karar destek aracıdır. ÖSYM ya da YÖK ile bağlantılı değildir; yerleşme garantisi vermez.
|
||||||
|
</div>
|
||||||
|
<Imlec yol={yol} gorunur={[96, 170]} el={[[TIK - 12, 170]]} />
|
||||||
|
<Vinyet guc={0.45} />
|
||||||
|
<AbsoluteFill style={{ background: R.slate950, opacity: son }} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
51
video/src/turkiye.ts
Normal file
51
video/src/turkiye.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Dot-matrix Türkiye silüeti — src/features/pazarlama/components/turkey-pixel-map.tsx
|
||||||
|
* ile aynı ızgara (64×26 hücre, gerçek sınır poligonlarından rasterize).
|
||||||
|
*/
|
||||||
|
export const TURKIYE_IZGARA = [
|
||||||
|
"..######..............##########................................",
|
||||||
|
"..#######...........##############..............................",
|
||||||
|
"..#######.........#################................#####........",
|
||||||
|
".########...###########################...........########......",
|
||||||
|
".######.....###############################################.....",
|
||||||
|
".###........###############################################.....",
|
||||||
|
".##.......##################################################....",
|
||||||
|
".###########################################################....",
|
||||||
|
".#############################################################..",
|
||||||
|
"..#############################################################.",
|
||||||
|
"...############################################################.",
|
||||||
|
"...##########################################################...",
|
||||||
|
"...##########################################################...",
|
||||||
|
"...###########################################################..",
|
||||||
|
"..############################################################..",
|
||||||
|
"..############################################################..",
|
||||||
|
"...###########################################################..",
|
||||||
|
"....##########################################################..",
|
||||||
|
"....##########################################################..",
|
||||||
|
".....#########################################################..",
|
||||||
|
".....###################################################..##....",
|
||||||
|
".....############################################...............",
|
||||||
|
"......##########..######################...###..................",
|
||||||
|
"...........#####....#########....#####..........................",
|
||||||
|
"............###.......#####......####...........................",
|
||||||
|
".................................###............................",];
|
||||||
|
|
||||||
|
/** Hero'daki vurgu şehirleri (sütun, satır) */
|
||||||
|
export const SEHIRLER = [
|
||||||
|
{ name: "İstanbul", x: 8, y: 4 },
|
||||||
|
{ name: "Ankara", x: 23, y: 9 },
|
||||||
|
{ name: "İzmir", x: 4, y: 16 },
|
||||||
|
{ name: "Antalya", x: 16, y: 22 },
|
||||||
|
{ name: "Kayseri", x: 32, y: 14 },
|
||||||
|
{ name: "Trabzon", x: 46, y: 5 },
|
||||||
|
{ name: "Diyarbakır", x: 47, y: 18 },
|
||||||
|
{ name: "Van", x: 58, y: 15 },];
|
||||||
|
|
||||||
|
export const IZGARA_SUTUN = TURKIYE_IZGARA[0].length;
|
||||||
|
export const IZGARA_SATIR = TURKIYE_IZGARA.length;
|
||||||
|
|
||||||
|
/** Dolu hücreler (x, y) */
|
||||||
|
export const TURKIYE_HUCRELER: { x: number; y: number }[] = TURKIYE_IZGARA.flatMap(
|
||||||
|
(satir, y) =>
|
||||||
|
Array.from(satir).flatMap((c, x) => (c === "#" ? [{ x, y }] : [])),
|
||||||
|
);
|
||||||
88
video/src/veri.ts
Normal file
88
video/src/veri.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Videodaki örnek öğrenci ve liste — data/yokatlas.db'den (2026 kılavuzu)
|
||||||
|
* alınmış gerçek kayıtlar, uydurma satır yok.
|
||||||
|
*
|
||||||
|
* Örnek: SAY 45.000 · Mühendislik · İstanbul, Ankara, İzmir · Devlet.
|
||||||
|
*
|
||||||
|
* Havuz sayıları sadeleştirilmiş bir sorgudan (taban sırası sıralamanın
|
||||||
|
* 0,7–3 katı arasındaki SAY programları); ürünün gerçek havuz kuralı daha
|
||||||
|
* ayrıntılıdır. Sorgu:
|
||||||
|
* son_kilavuz_yili=2026 AND tur='SAYISAL'
|
||||||
|
* AND COALESCE(sira2026,sira2025) BETWEEN 31500 AND 135000
|
||||||
|
* → 1.443 · +isim LIKE '%Mühendisliği%' → 704
|
||||||
|
* · +il IN (İSTANBUL, ANKARA, İZMİR) → 484 · +unitur='DEVLET' → 179
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ORNEK_SIRA = 45000;
|
||||||
|
export const ORNEK_SIRA_METNI = "45.000";
|
||||||
|
|
||||||
|
/** 2026 kılavuzundaki toplam program (lisans + önlisans) */
|
||||||
|
export const TOPLAM_PROGRAM = 21493;
|
||||||
|
|
||||||
|
export const HUNI = [
|
||||||
|
{ n: TOPLAM_PROGRAM, etiket: "Tüm programlar" },
|
||||||
|
{ n: 1443, etiket: "Sıralamanla açılan · Sayısal" },
|
||||||
|
{ n: 704, etiket: "Mühendislik & Teknoloji" },
|
||||||
|
{ n: 484, etiket: "İstanbul · Ankara · İzmir" },
|
||||||
|
{ n: 179, etiket: "Devlet üniversiteleri" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type Dilim = "hayal" | "dengeli" | "garanti";
|
||||||
|
|
||||||
|
export interface Satir {
|
||||||
|
program: string;
|
||||||
|
universite: string;
|
||||||
|
il: string;
|
||||||
|
dilim: Dilim;
|
||||||
|
/** Taban başarı sırası 2021 → 2026 */
|
||||||
|
sira: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Görünen dilim adı "Güvenli"; iç anahtar "garanti" (src/lib/risk.ts DILIM_ETIKET)
|
||||||
|
export const DILIM_ETIKET: Record<Dilim, string> = {
|
||||||
|
hayal: "Hayal",
|
||||||
|
dengeli: "Dengeli",
|
||||||
|
garanti: "Güvenli",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LISTE: Satir[] = [
|
||||||
|
{ program: "Elektrik-Elektronik Mühendisliği", universite: "Dokuz Eylül Üniversitesi", il: "İzmir", dilim: "hayal", sira: [45295, 41759, 36137, 40164, 39190, 38626] },
|
||||||
|
{ program: "Elektrik-Elektronik Mühendisliği", universite: "Ankara Yıldırım Beyazıt Üniversitesi", il: "Ankara", dilim: "hayal", sira: [53586, 47520, 40031, 45027, 42086, 40221] },
|
||||||
|
{ program: "Elektrik-Elektronik Mühendisliği", universite: "İstanbul Üniversitesi-Cerrahpaşa", il: "İstanbul", dilim: "hayal", sira: [58073, 54911, 49623, 52063, 47099, 42633] },
|
||||||
|
{ program: "Endüstri Mühendisliği", universite: "İstanbul Üniversitesi-Cerrahpaşa", il: "İstanbul", dilim: "dengeli", sira: [51496, 46297, 43436, 46957, 46877, 45099] },
|
||||||
|
{ program: "Bilgisayar Mühendisliği", universite: "Dokuz Eylül Üniversitesi", il: "İzmir", dilim: "dengeli", sira: [26098, 15816, 14246, 23271, 36435, 52071] },
|
||||||
|
{ program: "Endüstri Mühendisliği", universite: "Dokuz Eylül Üniversitesi", il: "İzmir", dilim: "dengeli", sira: [58559, 52933, 49684, 54049, 54049, 53587] },
|
||||||
|
{ program: "Elektrik-Elektronik Mühendisliği", universite: "İzmir Katip Çelebi Üniversitesi", il: "İzmir", dilim: "dengeli", sira: [71760, 64426, 59156, 68341, 60857, 53699] },
|
||||||
|
{ program: "Bilgisayar Mühendisliği", universite: "Ankara Yıldırım Beyazıt Üniversitesi", il: "Ankara", dilim: "dengeli", sira: [37831, 27534, 21602, 30797, 42108, 55158] },
|
||||||
|
{ program: "Elektrik-Elektronik Mühendisliği", universite: "İstanbul Medeniyet Üniversitesi", il: "İstanbul", dilim: "garanti", sira: [76034, 74632, 70514, 74989, 67761, 59417] },
|
||||||
|
{ program: "Endüstri Mühendisliği", universite: "İstanbul Medeniyet Üniversitesi", il: "İstanbul", dilim: "garanti", sira: [79628, 73208, 67215, 70600, 68739, 65169] },
|
||||||
|
{ program: "Bilgisayar Mühendisliği", universite: "İstanbul Medeniyet Üniversitesi", il: "İstanbul", dilim: "garanti", sira: [50828, 43992, 38865, 48481, 56616, 66119] },
|
||||||
|
{ program: "Bilgisayar Mühendisliği", universite: "İzmir Katip Çelebi Üniversitesi", il: "İzmir", dilim: "garanti", sira: [55869, 41281, 36708, 47803, 56980, 71833] },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Danışman sahnesinde sorulan satır: DEÜ Bilgisayar Mühendisliği */
|
||||||
|
export const SORULAN = LISTE[4];
|
||||||
|
|
||||||
|
/** 24'lük iskelet hedefi (huni-semasi.tsx: hayal 5 · dengeli 13 · güvenli 6) */
|
||||||
|
export const ISKELET: { dilim: Dilim; adet: number }[] = [
|
||||||
|
{ dilim: "hayal", adet: 5 },
|
||||||
|
{ dilim: "dengeli", adet: 13 },
|
||||||
|
{ dilim: "garanti", adet: 6 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Açılıştaki "gürültü" tüneli: listedeki gerçek taban sıraları */
|
||||||
|
export const GURULTU_SAYILARI = [
|
||||||
|
...new Set(LISTE.flatMap((s) => s.sira)),
|
||||||
|
].map((n) => n.toLocaleString("tr-TR"));
|
||||||
|
|
||||||
|
/** İl program sayıları (2026 kılavuzu) — harita kuleleri */
|
||||||
|
export const IL_PROGRAM: Record<string, number> = {
|
||||||
|
İstanbul: 6442,
|
||||||
|
Ankara: 1831,
|
||||||
|
İzmir: 793,
|
||||||
|
Antalya: 427,
|
||||||
|
Trabzon: 317,
|
||||||
|
Kayseri: 235,
|
||||||
|
Van: 123,
|
||||||
|
Diyarbakır: 111,
|
||||||
|
};
|
||||||
53
video/src/yardimci.ts
Normal file
53
video/src/yardimci.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Easing } from "remotion";
|
||||||
|
|
||||||
|
export const clamp01 = (t: number) => Math.max(0, Math.min(1, t));
|
||||||
|
export const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
|
||||||
|
|
||||||
|
/** Sitedeki pixel-decor.tsx ile aynı deterministik sözde-rastgele */
|
||||||
|
export const hash = (x: number, y: number, seed = 0) => {
|
||||||
|
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
|
||||||
|
return n - Math.floor(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ease = {
|
||||||
|
out: Easing.bezier(0.22, 1, 0.36, 1),
|
||||||
|
inOut: Easing.bezier(0.65, 0, 0.35, 1),
|
||||||
|
in: Easing.bezier(0.64, 0, 0.78, 0),
|
||||||
|
// Sitenin yay hissi: bounce 0 (Apple damping 1)
|
||||||
|
yay: Easing.bezier(0.2, 0.8, 0.2, 1),
|
||||||
|
geriTep: Easing.bezier(0.34, 1.56, 0.64, 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** f, [a, b] aralığında 0→1 ilerleme (kırpılmış, isteğe bağlı eğri) */
|
||||||
|
export const aralik = (
|
||||||
|
f: number,
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
egri: (t: number) => number = (t) => t,
|
||||||
|
) => egri(clamp01((f - a) / (b - a)));
|
||||||
|
|
||||||
|
/** İki CSS rengi arasında oklab karışımı (oklch token'larla çalışır) */
|
||||||
|
export const karis = (a: string, b: string, t: number) => {
|
||||||
|
const p = Math.round(clamp01(t) * 1000) / 10;
|
||||||
|
if (p <= 0) return a;
|
||||||
|
if (p >= 100) return b;
|
||||||
|
return `color-mix(in oklab, ${b} ${p}%, ${a})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Sayıyı Türkçe binlik ayraçla yazar: 45000 → "45.000" */
|
||||||
|
export const tr = (n: number) => Math.round(n).toLocaleString("tr-TR");
|
||||||
|
|
||||||
|
/** Metin ölçümü: kinetik yerleşimler için gerçek yazı tipiyle genişlik */
|
||||||
|
export const olc = (
|
||||||
|
metin: string,
|
||||||
|
font: { family: string; size: number; weight?: number; italic?: boolean; tracking?: number },
|
||||||
|
) => {
|
||||||
|
if (typeof document === "undefined") return metin.length * font.size * 0.55;
|
||||||
|
const ctx = document.createElement("canvas").getContext("2d");
|
||||||
|
if (!ctx) return metin.length * font.size * 0.55;
|
||||||
|
ctx.font = `${font.italic ? "italic " : ""}${font.weight ?? 400} ${font.size}px "${font.family}"`;
|
||||||
|
return (
|
||||||
|
ctx.measureText(metin).width +
|
||||||
|
(font.tracking ?? 0) * font.size * Array.from(metin).length
|
||||||
|
);
|
||||||
|
};
|
||||||
41
video/src/zaman.ts
Normal file
41
video/src/zaman.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Zaman çizelgesi (30 fps). Sahneler TransitionSeries'te art arda; geçişler
|
||||||
|
* iki sahneyi üst üste bindirir, yani toplam süre = sahneler − geçişler.
|
||||||
|
*
|
||||||
|
* Storyboard omurgası: her geçiş ya bir **piksel** (dalga, tek piksele
|
||||||
|
* çöküş, havuzdan seçilen 24 piksel) ya da **imlecin tıkladığı nokta**
|
||||||
|
* (o noktanın içinden itme / o noktadan yayılan piksel dalgası) üstüne
|
||||||
|
* kurulu. Geçişin başladığı kare, imlecin tıkladığı karedir.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SAHNE = {
|
||||||
|
gurultu: 150,
|
||||||
|
harita: 250,
|
||||||
|
siralama: 126,
|
||||||
|
havuz: 420,
|
||||||
|
danisman: 200,
|
||||||
|
deger: 170,
|
||||||
|
kapanis: 180,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const GECIS = {
|
||||||
|
/** gürültü → harita: tek piksel patlar, koyudan açığa piksel dalgası */
|
||||||
|
g1: 30,
|
||||||
|
/** harita → sıralama: imleç logonun turuncu karesine tıklar, dalga oradan */
|
||||||
|
g2: 26,
|
||||||
|
/** sıralama → havuz: CTA'ya tıklanır, sahne butonun içinden geçer */
|
||||||
|
g3: 22,
|
||||||
|
/** havuz → danışman: listedeki satıra tıklanır, satırın içinden geçilir */
|
||||||
|
g4: 22,
|
||||||
|
/** danışman → değer: gönder düğmesinden piksel dalgası */
|
||||||
|
g5: 26,
|
||||||
|
/** değer → kapanış: son gösterge karesinden koyu piksel dalgası */
|
||||||
|
g6: 28,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Bir sahnenin, kendisinden sonraki geçişin başladığı yerel karesi */
|
||||||
|
export const gecisBasi = (sahne: number, gecis: number) => sahne - gecis;
|
||||||
|
|
||||||
|
export const TOPLAM =
|
||||||
|
Object.values(SAHNE).reduce((a, b) => a + b, 0) -
|
||||||
|
Object.values(GECIS).reduce((a, b) => a + b, 0);
|
||||||
18
video/tsconfig.json
Normal file
18
video/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": { "@/*": ["./src/*"] }
|
||||||
|
},
|
||||||
|
"include": ["src", "remotion.config.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user