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>
237 lines
9.5 KiB
TypeScript
237 lines
9.5 KiB
TypeScript
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>
|
||
);
|
||
}
|