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 = 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(null); const svgRef = useRef(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(); 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(); 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 += ``; }); // 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 += ``; 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 += ``; } } if (svgRef.current) svgRef.current.innerHTML = svg; return true; } }, [frame, W, H, dikey, BOY]); return (
{[0, 1, 2, 3].map((k) => (
{ 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, }} >
))}
); }