Files
kolaytercih/src/features/pazarlama/components/program-duvari.tsx
bilalgursen 3c995402c6 feat(ui): Arc bileşenleri, landing bölümleri ve bildirim yığını
Sonner/vaul yerine kendi bildirim yığını ve alt sayfa; çip grubu, kart
destesi, kaydırma eylemi, yazı dönüşümü, bölme tutamacı; landing'e program
duvarı, süzülen haplar ve veri akışı; yaylar tek dosyada (lib/hareket.ts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 17:12:53 +03:00

246 lines
9.0 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.
"use client";
// Program duvarı (DESIGN.md §4.29): "24 tercih, binlerce ihtimal" cümlesinin
// resmi. Küçük bir pencerede piksel kareler satır satır akar — her kare bir
// program; aralarında tam 24 turuncu kare yerinde durur: senin listen.
// İmleç yaklaştığı satırları hızlandırır. Veri taşıyan pikseldir (§1.2);
// anlamı altındaki cümle söyler, renk tek başına değil (§2.2).
//
// Esin: React Bits "Squares Terminal" (yanıp sönen kare ızgarası, imlece
// tepki veren satırlar). Kod bize ait: koyu CRT ekranı, parlama ve tüp eğimi
// alınmadı; açık zemin, hero'nun ızgara ölçüleri (hücre 10, nokta 7.4, köşe 2)
// ve doku opaklıkları kullanılır. Tek <canvas>, yalnız ekrandayken ~30 fps;
// hareketi azalt'ta durağan tek kare.
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
const HUCRE = 10;
const NOKTA = 7.4;
const BOSLUK = (HUCRE - NOKTA) / 2;
// Hero ile aynı doku paleti
const OPAKLIKLAR = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19];
const TERCIH = 24;
/** İmlecin satırları etkilediği dikey uzaklık (px) ve en çok ek hız (hücre/sn). */
const ETKI = 46;
const EK_HIZ = 9;
// Deterministik sözde-rastgele: sunucuda yok ama her ziyarette aynı desen
const karma = (x: number, y: number) => {
const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
return n - Math.floor(n);
};
export function ProgramDuvari({
etiket,
className,
}: {
/** Pencere çubuğundaki kısa yazı, ör. "21.000+ program". */
etiket: string;
className?: string;
}) {
const kapRef = useRef<HTMLDivElement>(null);
const tuvalRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const kap = kapRef.current;
const tuval = tuvalRef.current;
const ctx = tuval?.getContext("2d");
if (!kap || !tuval || !ctx) return;
const azalt = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let en = 0;
let boy = 0;
let sutun = 0;
let satir = 0;
let mavi = "";
let turuncu = "";
/** Satır başına kayma (hücre) ve imleç kaynaklı ek hız. */
let kayma: number[] = [];
let ekHiz: number[] = [];
let tercihler: { c: number; r: number }[] = [];
let imlecY: number | null = null;
let gorunur = false;
let kare = 0;
let onceki = 0;
let basladi = 0;
const renkleriOku = () => {
// Tema değişince palet token'ları değişir (§2.3); renk CSS'ten okunur
mavi = getComputedStyle(tuval).color;
turuncu =
getComputedStyle(document.documentElement).getPropertyValue("--color-orange-500").trim() ||
"#f97316";
};
const olc = () => {
const kutu = kap.getBoundingClientRect();
const oran = Math.min(window.devicePixelRatio || 1, 2);
en = Math.round(kutu.width);
boy = Math.round(kutu.height);
tuval.width = en * oran;
tuval.height = boy * oran;
ctx.setTransform(oran, 0, 0, oran, 0, 0);
sutun = Math.ceil(en / HUCRE) + 1;
satir = Math.floor(boy / HUCRE);
kayma = Array.from({ length: satir }, (_, r) => kayma[r] ?? karma(r, 3) * 40);
ekHiz = Array.from({ length: satir }, () => 0);
// 24 tercih: kenarlardan bir hücre içeride, birbirine değmeyen sabit kareler
tercihler = [];
const dolu = new Set<string>();
for (let i = 0; tercihler.length < TERCIH && i < 4000; i++) {
const c = 1 + Math.floor(karma(i, 17) * Math.max(1, sutun - 3));
const r = 1 + Math.floor(karma(i, 29) * Math.max(1, satir - 2));
const yakin = [-1, 0, 1].some((dx) =>
[-1, 0, 1].some((dy) => dolu.has(`${c + dx}:${r + dy}`)),
);
if (yakin) continue;
dolu.add(`${c}:${r}`);
tercihler.push({ c, r });
}
};
const kareCiz = (x: number, y: number) => {
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(x, y, NOKTA, NOKTA, 2);
else ctx.rect(x, y, NOKTA, NOKTA);
ctx.fill();
};
const ciz = (t: number) => {
ctx.clearRect(0, 0, en, boy);
const ustBosluk = (boy - satir * HUCRE) / 2;
ctx.fillStyle = mavi;
for (let r = 0; r < satir; r++) {
const tam = Math.floor(kayma[r]);
const kesir = kayma[r] - tam;
// Tek satırlar sola, çift satırlar sağa akar
const yon = r % 2 === 0 ? 1 : -1;
for (let c = -1; c < sutun; c++) {
const kimlik = c - yon * tam;
const h = karma(kimlik, r);
// Karelerin üçte biri boş: duvar değil doku
if (h < 0.34) continue;
const taban = OPAKLIKLAR[Math.abs(kimlik * 7 + r * 11) % OPAKLIKLAR.length];
// Yavaş nefes: her kare kendi fazında hafifçe parlar ve söner
const nefes = 0.6 + 0.4 * Math.sin(t * 0.9 + h * 6.283);
ctx.globalAlpha = taban * nefes;
kareCiz((c + yon * kesir) * HUCRE + BOSLUK, ustBosluk + r * HUCRE + BOSLUK);
}
}
// Tercihler: sırayla belirir (110 ms arayla), sonra yerinde kalır
ctx.fillStyle = turuncu;
const gecen = azalt ? Infinity : t - basladi;
tercihler.forEach(({ c, r }, i) => {
const yas = gecen - 0.4 - i * 0.11;
if (yas <= 0) return;
const giris = Math.min(1, yas / 0.3);
const x = c * HUCRE + BOSLUK;
const y = ustBosluk + r * HUCRE + BOSLUK;
// Altındaki akan kareyi sil: turuncu kare tek başına okunur
ctx.globalAlpha = 1;
ctx.clearRect(c * HUCRE, ustBosluk + r * HUCRE, HUCRE, HUCRE);
ctx.globalAlpha = 0.9 * giris;
kareCiz(x, y);
});
ctx.globalAlpha = 1;
};
const adim = (simdi: number) => {
kare = requestAnimationFrame(adim);
// ~30 fps yeter: kareler yavaş akar
if (simdi - onceki < 32) return;
const dt = Math.min(0.1, (simdi - onceki) / 1000);
onceki = simdi;
const ustBosluk = (boy - satir * HUCRE) / 2;
for (let r = 0; r < satir; r++) {
const merkez = ustBosluk + r * HUCRE + HUCRE / 2;
const yakinlik =
imlecY == null ? 0 : Math.max(0, 1 - Math.abs(merkez - imlecY) / ETKI);
// Ek hız hedefe yumuşak yaklaşır; imleç çekilince yavaşça söner
ekHiz[r] += (yakinlik * yakinlik * EK_HIZ - ekHiz[r]) * Math.min(1, dt * 5);
kayma[r] += (0.5 + karma(r, 7) * 1.3 + ekHiz[r]) * dt;
}
ciz(simdi / 1000);
};
const baslat = () => {
if (kare || azalt || !gorunur) return;
onceki = performance.now();
if (!basladi) basladi = onceki / 1000;
kare = requestAnimationFrame(adim);
};
const durdur = () => {
cancelAnimationFrame(kare);
kare = 0;
};
renkleriOku();
olc();
ciz(0);
const boyut = new ResizeObserver(() => {
olc();
ciz(performance.now() / 1000);
});
boyut.observe(kap);
const gorunum = new IntersectionObserver(([kayit]) => {
gorunur = kayit.isIntersecting;
if (gorunur) baslat();
else durdur();
});
gorunum.observe(kap);
const tema = new MutationObserver(() => {
renkleriOku();
ciz(performance.now() / 1000);
});
tema.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
const imlec = (e: PointerEvent) => {
if (e.pointerType !== "mouse") return;
const kutu = kap.getBoundingClientRect();
const icinde =
e.clientX >= kutu.left &&
e.clientX <= kutu.right &&
e.clientY >= kutu.top - ETKI &&
e.clientY <= kutu.bottom + ETKI;
imlecY = icinde ? e.clientY - kutu.top : null;
};
window.addEventListener("pointermove", imlec, { passive: true });
return () => {
durdur();
boyut.disconnect();
gorunum.disconnect();
tema.disconnect();
window.removeEventListener("pointermove", imlec);
};
}, []);
return (
<figure className={cn("mx-auto w-full max-w-3xl", className)}>
{/* Mini pencere: aramadaki tarayıcı penceresiyle aynı dil (§4.20) */}
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-card shadow-sm">
<div className="flex h-9 items-center gap-1.5 border-b border-slate-100 bg-slate-50 px-3.5">
<span className="size-2 rounded-full bg-slate-300" aria-hidden />
<span className="size-2 rounded-full bg-slate-300 opacity-65" aria-hidden />
<span className="size-2 rounded-full bg-slate-300 opacity-35" aria-hidden />
<span className="ml-2 text-xs font-medium text-slate-500">{etiket}</span>
</div>
<div ref={kapRef} className="h-36 sm:h-44">
<canvas
ref={tuvalRef}
role="img"
aria-label="Akan piksel kareler: her kare bir program. Aralarında 24 turuncu kare senin tercih listen."
className="block size-full text-primary"
/>
</div>
</div>
<figcaption className="mt-3 flex items-center justify-center gap-2 text-center text-sm text-slate-500">
<span className="size-2.5 shrink-0 rounded-[3px] bg-orange-500" aria-hidden />
Her kare bir program; turuncular senin 24 tercihin.
</figcaption>
</figure>
);
}