Files
kolaytercih/src/components/pixel-decor.tsx
bilalgursen 78d7c51a7a
Some checks failed
Deploy / deploy (push) Has been cancelled
perf: deterministik dekor memo'ları ve küçük render süpürmeleri (Faz 7)
- pixel-decor: PixelField/PixelDivider piksel üretimi useMemo([seed]) —
  504 iterasyon + ~150 element çekmece reorder'ının her karesinde
  yeniden üretiliyordu
- rapor-listesi: acikSira effect'i önceki-prop desenine döndü (çift render
  ve eslint-disable kalktı), Set lazy init, tekIl useMemo
- program-tablosu: 3 dilimin spread'i useMemo
- typing-animation: displayedText state yerine türetme; kaynak reset'i
  render sırasında — tick başına state yazımı ve effect bağımlılığı azaldı
- site-top-banner: saniyelik geri sayım tiki startTransition'da
- program-liste-verileri: sparkline 6 dizi geçişi tek döngüde
- secimlerim-paneli: SecimSatiri profili prop'tan alır + memo (24 ayrı
  store aboneliği kalktı)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 14:29:14 +03:00

293 lines
7.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.
"use client";
import { useMemo, useRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { usePixelHeat } from "@/components/use-pixel-heat";
/**
* Hero'daki dot-matrix dilini (TurkeyPixelMap) bölüm ayraçlarına ve
* dekorlara taşıyan piksel süsleri. Rastgelelik yerine deterministik
* hash kullanılır ki sunucu ve istemci aynı deseni üretsin.
*
* Hepsi hero'daki imleç ısı fırçasına katılır (usePixelHeat): imleç
* yaklaşınca pikseller turuncuya ısınır, uzaklaşınca yavaşça soğur.
* Turuncu vurgu (accent) pikselleri hero'daki şehirler gibi fırçaya
* katılmaz.
*/
// Hero ile aynı ızgara ölçüleri
const CELL = 10;
const DOT = 7.4;
const INSET = (CELL - DOT) / 2;
// Hero ile aynı doku paleti
const OPACITIES = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19];
const opacityFor = (x: number, y: number) =>
OPACITIES[(x * 7 + y * 11) % OPACITIES.length];
// Deterministik sözde-rastgele: aynı girdi her ortamda aynı deseni verir
const hash = (x: number, y: number, seed: number) => {
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
return n - Math.floor(n);
};
function pixelRect(
x: number,
y: number,
extra: { key: string; accent?: boolean; accentDelay?: number },
) {
const shared = {
x: x * CELL + INSET,
y: y * CELL + INSET,
width: DOT,
height: DOT,
rx: 2,
};
if (extra.accent) {
return (
<rect
key={extra.key}
{...shared}
className="animate-pulse fill-orange-500/70 motion-reduce:animate-none"
style={{
animationDuration: "3.6s",
animationDelay: `${extra.accentDelay ?? 0}s`,
}}
/>
);
}
const op = opacityFor(x, y);
return (
<rect
key={extra.key}
{...shared}
data-px=""
data-cx={x * CELL + CELL / 2}
data-cy={y * CELL + CELL / 2}
data-op={op}
fill="currentColor"
fillOpacity={op}
/>
);
}
/**
* Geniş alanlar için seyrek piksel zemini (ör. koyu CTA bandı).
* İçeriğin oturduğu orta bölge tamamen boş bırakılır; pikseller
* kenarlara doğru yoğunlaşır.
*/
export function PixelField({
seed = 7,
className,
}: {
seed?: number;
className?: string;
}) {
const svgRef = useRef<SVGSVGElement>(null);
usePixelHeat(svgRef);
const COLS = 42;
const ROWS = 12;
// Çıktı seed'e göre deterministik; 504 iterasyon + ~150 element üretimini
// her render'da (ör. çekmece reorder'ında) tekrarlama (rendering-hoist-jsx)
const pixels = useMemo(() => {
const cx = (COLS - 1) / 2;
const cy = (ROWS - 1) / 2;
const maxD = Math.sqrt(cx * cx + cy * cy);
const sonuc: ReactNode[] = [];
let accentIdx = 0;
// Hero serpintisiyle aynı kural: içeriğin oturduğu orta kolon
// (genişliğin %2278 bandı) tüm yükseklik boyunca tamamen boş kalır
const BAND_MIN = COLS * 0.22;
const BAND_MAX = COLS * 0.78;
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (x > BAND_MIN && x < BAND_MAX) continue;
// Kenarlara doğru artan doluluk
const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) / maxD;
const density = 0.1 + 0.42 * d * d;
if (hash(x, y, seed) > density) continue;
const accent = hash(x, y, seed + 99) > 0.96;
sonuc.push(
pixelRect(x, y, {
key: `${x}-${y}`,
accent,
accentDelay: accent ? (accentIdx++ % 8) * 0.45 : 0,
}),
);
}
}
return sonuc;
}, [seed]);
return (
<svg
ref={svgRef}
viewBox={`0 0 ${COLS * CELL} ${ROWS * CELL}`}
preserveAspectRatio="xMidYMid slice"
aria-hidden="true"
focusable="false"
className={cn("h-full w-full", className)}
>
{pixels}
</svg>
);
}
/**
* Bölümler arasında sert border çizgileri yerine kullanılan, ortada
* yoğunlaşıp kenarlara doğru dağılan piksel ayraç. Hero'nun dot-matrix
* dilini bölüm geçişlerine taşır.
*/
export function PixelDivider({
seed = 5,
className,
}: {
seed?: number;
className?: string;
}) {
const svgRef = useRef<SVGSVGElement>(null);
usePixelHeat(svgRef);
const COLS = 12;
const ROWS = 2;
// Deterministik çıktı — render başına yeniden üretme (PixelField ile aynı)
const pixels = useMemo(() => {
const cx = (COLS - 1) / 2;
const sonuc: ReactNode[] = [];
let accentPlaced = false;
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
// Az sayıda, iri piksel: ortada yoğun, kenara doğru seyrelen küçük küme
const dx = Math.abs(x - cx) / cx;
const rowFalloff = y === 0 ? 0.9 : 0.35;
const density = (1 - dx) ** 1.1 * rowFalloff;
if (hash(x, y, seed) > density) continue;
const accent = !accentPlaced && hash(x, y, seed + 99) > 0.8;
if (accent) accentPlaced = true;
sonuc.push(
pixelRect(x, y, {
key: `${x}-${y}`,
accent,
}),
);
}
}
return sonuc;
}, [seed]);
return (
<svg
ref={svgRef}
viewBox={`0 0 ${COLS * CELL} ${ROWS * CELL}`}
aria-hidden="true"
focusable="false"
className={cn("h-auto w-full text-primary", className)}
>
{pixels}
</svg>
);
}
/** Alt sayfa başlıklarını içerikten ayıran kompakt piksel imzası. */
export function PagePixelDivider({
seed = 5,
className,
}: {
seed?: number;
className?: string;
}) {
return (
<div aria-hidden="true" className={cn("w-40", className)}>
<PixelDivider seed={seed} />
</div>
);
}
/** Küçük sabit süslerde (mark/korner) data-px öznitelikli rect. */
function fixedPixel(x: number, y: number, opacity: number) {
return (
<rect
x={x + INSET}
y={y + INSET}
width={DOT}
height={DOT}
rx={2}
data-px=""
data-cx={x + CELL / 2}
data-cy={y + CELL / 2}
data-op={opacity}
fill="currentColor"
fillOpacity={opacity}
/>
);
}
/** Eyebrow rozetlerinde kullanılan 2×2'lik minik piksel imzası. */
export function PixelMark({ className }: { className?: string }) {
const svgRef = useRef<SVGSVGElement>(null);
usePixelHeat(svgRef, { radius: 24 });
return (
<svg
ref={svgRef}
viewBox="0 0 20 20"
aria-hidden="true"
focusable="false"
className={cn("size-3.5 shrink-0", className)}
>
{fixedPixel(0, 0, 0.35)}
<rect x={11.3} y={1.3} width={7.4} height={7.4} rx={2} className="fill-orange-500" fillOpacity={0.85} />
{fixedPixel(0, 10, 0.6)}
{fixedPixel(10, 10, 0.18)}
</svg>
);
}
/** Kart köşelerine iliştirilen sessiz piksel kümesi. */
export function PixelCorner({ className }: { className?: string }) {
const svgRef = useRef<SVGSVGElement>(null);
usePixelHeat(svgRef, { radius: 28 });
return (
<svg
ref={svgRef}
viewBox="0 0 30 30"
aria-hidden="true"
focusable="false"
className={cn("size-7 text-primary", className)}
>
{fixedPixel(20, 0, 0.5)}
{fixedPixel(10, 0, 0.14)}
{fixedPixel(20, 10, 0.26)}
</svg>
);
}
/** Bölüm başlıklarının üstünde duran piksel imzalı eyebrow rozeti. */
export function SectionEyebrow({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<span
className={cn(
"inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-1.5 text-xs font-semibold uppercase tracking-[0.12em] text-primary",
className,
)}
>
<PixelMark />
{children}
</span>
);
}