Refactor code structure for improved readability and maintainability
All checks were successful
Deploy / deploy (push) Successful in 10m26s
All checks were successful
Deploy / deploy (push) Successful in 10m26s
This commit is contained in:
256
src/components/pixel-decor.tsx
Normal file
256
src/components/pixel-decor.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
"use client";
|
||||
|
||||
import { 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;
|
||||
const cx = (COLS - 1) / 2;
|
||||
const cy = (ROWS - 1) / 2;
|
||||
const maxD = Math.sqrt(cx * cx + cy * cy);
|
||||
|
||||
const pixels: ReactNode[] = [];
|
||||
let accentIdx = 0;
|
||||
|
||||
// Hero serpintisiyle aynı kural: içeriğin oturduğu orta kolon
|
||||
// (genişliğin %22–78 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;
|
||||
pixels.push(
|
||||
pixelRect(x, y, {
|
||||
key: `${x}-${y}`,
|
||||
accent,
|
||||
accentDelay: accent ? (accentIdx++ % 8) * 0.45 : 0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const cx = (COLS - 1) / 2;
|
||||
|
||||
const pixels: 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;
|
||||
pixels.push(
|
||||
pixelRect(x, y, {
|
||||
key: `${x}-${y}`,
|
||||
accent,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="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">
|
||||
<PixelMark />
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user