Files
kolaytercih-post/src/eski/imlec.tsx
bilalgursen dc5c87b6aa 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>
2026-09-30 20:22:38 +03:00

237 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}