feat: KolayTercih lansman videosu (Remotion, v3)

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

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

236
src/eski/imlec.tsx Normal file
View File

@@ -0,0 +1,236 @@
import { useLayoutEffect, useRef } from "react";
import { Easing, continueRender, delayRender, interpolateColors, useCurrentFrame, useVideoConfig } from "remotion";
import { R, hash } from "./tema";
import ZAMAN from "./zaman.json";
/**
* Filmin baş karakteri: tek bir imleç. Sahneler arasında kesintisiz dolaşır
* ve hedeflerine **motion tracking** ile kilitlenir: hedef, sahnedeki bir
* öğenin `data-iz` adıdır ve konumu her karede DOM'dan ölçülür
* (getBoundingClientRect). Böylece kamera yaklaşsa, telefon 3B dönse, kart
* uçuşsa da imleç öğenin üstünde kalır.
*
* Geçtiği yerde sitedeki ısı fırçası gibi turuncu pikseller bırakır (hızla
* orantılı, 0.7 sn'de soğur); tıkladığı yerde halka + piksel patlaması.
* Konum hesabı ölçüme bağlı olduğu için çizim, React render'ı yerine
* useLayoutEffect içinde doğrudan DOM'a yazılır (ekran görüntüsünden önce).
*/
type Hedef = string | [number, number] | { dikey: [number, number]; yatay: [number, number] };
type Nokta = { s: string; f: number; h: Hedef; dx?: number; dy?: number; g?: number; tik?: boolean };
type Durak = { F: number; h: Hedef; dx: number; dy: number; g: number; tik: boolean };
const BAS: Record<string, number> = Object.fromEntries(ZAMAN.sahneler.map((s) => [s.id, s.bas]));
export const DURAKLAR: Durak[] = (ZAMAN.imlec as Nokta[])
.map((n) => ({ F: BAS[n.s] + n.f, h: n.h, dx: n.dx ?? 0, dy: n.dy ?? 0, g: n.g ?? 14, tik: !!n.tik }))
.sort((a, b) => a.F - b.F);
const TIKLAR = DURAKLAR.filter((d) => d.tik);
const yolEgri = Easing.bezier(0.33, 0, 0.12, 1); // insan eli: hızlı kalkış, yumuşak varış
export function IzleyenImlec() {
const frame = useCurrentFrame();
const { width: W, height: H } = useVideoConfig();
const dikey = H > W;
const kokRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const imlecRefs = useRef<(HTMLDivElement | null)[]>([]);
const BOY = dikey ? 72 : 60;
useLayoutEffect(() => {
// Remotion kompozisyonu önce ekran dışındaki sıfır genişlikli bir kapta
// çizer, sonra görünür tuvale taşır. Ölçüm ancak taşındıktan sonra
// doğrudur; o ana kadar kare tutulur (delayRender).
if (ciz()) return;
const tutamac = delayRender("İmleç: yerleşim bekleniyor");
let iptal = false;
const dene = () => {
if (iptal) return;
if (ciz()) continueRender(tutamac);
else requestAnimationFrame(dene);
};
requestAnimationFrame(dene);
return () => {
iptal = true;
continueRender(tutamac);
};
function ciz(): boolean {
const kok = kokRef.current?.parentElement;
if (!kok) return false;
const kr = kok.getBoundingClientRect();
if (kr.width < 1) return false;
const olcek = kr.width / W;
const onbellek = new Map<string, [number, number] | null>();
const olc = (h: Hedef): [number, number] | null => {
if (Array.isArray(h)) return [h[0] * W, h[1] * H];
if (typeof h === "object") {
const p = dikey ? h.dikey : h.yatay;
return [p[0] * W, p[1] * H];
}
if (onbellek.has(h)) return onbellek.get(h)!;
const el = kok.querySelector(`[data-iz="${h}"]`);
let sonuc: [number, number] | null = null;
if (el) {
const r = el.getBoundingClientRect();
if (r.width > 0 || r.height > 0) {
sonuc = [(r.left + r.width / 2 - kr.left) / olcek, (r.top + r.height / 2 - kr.top) / olcek];
}
}
onbellek.set(h, sonuc);
return sonuc;
};
const durakNoktasi = (i: number): [number, number] | null => {
const d = DURAKLAR[i];
const p = olc(d.h);
return p ? [p[0] + d.dx, p[1] + d.dy] : null;
};
// Hedef ölçülemezse (öğe henüz sahnede yoksa) bir önceki durağa düş
const guvenli = (i: number): [number, number] => {
for (let k = i; k >= 0; k--) {
const p = durakNoktasi(k);
if (p) return p;
}
return [W / 2, H / 2];
};
const konum = (t: number): [number, number] => {
let j = DURAKLAR.findIndex((d) => d.F > t);
if (j === -1) return guvenli(DURAKLAR.length - 1);
if (j === 0) return guvenli(0);
const i = j - 1;
const a = guvenli(i);
const d = DURAKLAR[j];
const kalkis = Math.max(DURAKLAR[i].F, d.F - d.g);
if (t <= kalkis) return a;
const b = durakNoktasi(j) ?? a;
const p = yolEgri(Math.min(1, (t - kalkis) / (d.F - kalkis)));
// Hafif yay: düz çizgi değil, el hareketi gibi kavis
const mx = b[0] - a[0];
const my = b[1] - a[1];
const uz = Math.hypot(mx, my);
const kavis = Math.sin(Math.PI * p) * Math.min(90, uz * 0.12);
const nx = uz ? -my / uz : 0;
const ny = uz ? mx / uz : 0;
return [a[0] + mx * p + nx * kavis, a[1] + my * p + ny * kavis];
};
const [x, y] = konum(frame);
// Durağan anlarda nefes gibi minik salınım
const sx = Math.sin(frame / 13) * 1.6;
const sy = Math.cos(frame / 17) * 1.4;
// Basış
let bas = 1;
for (const t of TIKLAR) {
const d = frame - t.F;
if (d >= -2 && d <= 2) bas = Math.min(bas, 1 - 0.16 * ((d + 2) / 4));
else if (d > 2 && d <= 9) bas = Math.min(bas, 0.84 + 0.16 * ((d - 2) / 7));
}
// İmleç + hareket bulanıklığı hayaletleri
const [px, py] = konum(frame - 1);
const hiz = Math.hypot(x - px, y - py);
const tipX = (4.04 / 24) * BOY;
const tipY = (4.04 / 24) * BOY;
imlecRefs.current.forEach((el, k) => {
if (!el) return;
if (k === 0) {
el.style.transform = `translate(${x + sx - tipX}px, ${y + sy - tipY}px) scale(${bas})`;
el.style.opacity = "1";
return;
}
const [gx, gy] = konum(frame - k * 0.33);
const op = hiz > 22 ? [0, 0.28, 0.16, 0.08][k] * Math.min(1, (hiz - 22) / 30) : 0;
el.style.transform = `translate(${gx + sx - tipX}px, ${gy + sy - tipY}px)`;
el.style.opacity = String(op);
});
// Piksel izi (ısı fırçası): hızla orantılı, ızgaraya oturan kareler
const G = dikey ? 26 : 24;
const hucreler = new Map<string, number>();
const IZ = 21;
for (let k = 1; k <= IZ; k++) {
const [ax, ay] = konum(frame - k);
const [bx, by] = konum(frame - k - 1);
const v = Math.hypot(ax - bx, ay - by);
if (v < 5) continue;
const isi = (1 - k / IZ) * Math.min(1, v / 24);
// Hızlı harekette ara noktaları da doldur ki iz kopuk olmasın
const adim = Math.max(1, Math.ceil(v / G));
for (let s = 0; s < adim; s++) {
const qx = ax + ((bx - ax) * s) / adim;
const qy = ay + ((by - ay) * s) / adim;
const anahtar = `${Math.floor(qx / G)},${Math.floor(qy / G)}`;
hucreler.set(anahtar, Math.max(hucreler.get(anahtar) ?? 0, isi));
}
}
const dot = G * 0.74;
let svg = "";
hucreler.forEach((isi, anahtar) => {
const [cx, cy] = anahtar.split(",").map(Number);
const renk = interpolateColors(isi, [0, 1], [R.primary, R.turuncu]);
svg += `<rect x="${cx * G + (G - dot) / 2}" y="${cy * G + (G - dot) / 2}" width="${dot}" height="${dot}" rx="${dot * 0.27}" fill="${renk}" fill-opacity="${(0.2 + 0.65 * isi).toFixed(3)}"/>`;
});
// Tık: halka + piksel patlaması
for (const t of TIKLAR) {
const d = frame - t.F;
if (d < 0 || d > 24) continue;
const [cx, cy] = konum(t.F);
const p = Easing.out(Easing.cubic)(Math.min(1, d / 18));
const r = 12 + (dikey ? 78 : 64) * p;
svg += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${R.turuncu}" stroke-width="${(5 * (1 - p)).toFixed(2)}" stroke-opacity="${(0.8 * (1 - p)).toFixed(3)}"/>`;
const KARE = 12;
const Gp = dikey ? 14 : 12;
for (let n = 0; n < 10; n++) {
const aci = (n / 10) * Math.PI * 2 + hash(t.F, n, 3) * 0.6;
const uz = (18 + (dikey ? 96 : 80) * (0.6 + 0.4 * hash(t.F, n, 5))) * p;
// Konum ızgaraya oturur: piksel adımlarıyla dağılan kareler
const qx = Math.round((cx + Math.cos(aci) * uz) / Gp) * Gp;
const qy = Math.round((cy + Math.sin(aci) * uz) / Gp) * Gp;
const boy = KARE * (1 - d / 26);
const renk = [R.turuncu, R.primary, R.logoSeftali][n % 3];
svg += `<rect x="${qx - boy / 2}" y="${qy - boy / 2}" width="${boy}" height="${boy}" rx="${boy * 0.27}" fill="${renk}" fill-opacity="${(1 - d / 24).toFixed(3)}"/>`;
}
}
if (svgRef.current) svgRef.current.innerHTML = svg;
return true;
}
}, [frame, W, H, dikey, BOY]);
return (
<div ref={kokRef} style={{ position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" }}>
<svg ref={svgRef} width={W} height={H} style={{ position: "absolute", inset: 0 }} />
{[0, 1, 2, 3].map((k) => (
<div
key={k}
ref={(el) => {
imlecRefs.current[k] = el;
}}
style={{
position: "absolute",
left: 0,
top: 0,
width: BOY,
height: BOY,
transformOrigin: `${(4.04 / 24) * 100}% ${(4.04 / 24) * 100}%`,
filter: k === 0 ? "drop-shadow(0 8px 14px rgba(15,23,42,0.28))" : undefined,
opacity: 0,
}}
>
<svg width={BOY} height={BOY} viewBox="0 0 24 24">
<path
d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"
fill={R.slate900}
stroke="#fff"
strokeWidth={1.5}
strokeLinejoin="round"
/>
</svg>
</div>
))}
</div>
);
}