fix(tasarim): piksel zemin yalnız CTA yüzeylerinde, kabın ölçüsüne göre, küçük kare yok
Bilal (24 Eyl 2026): ana sayfa CTA'sında doku hiç görünmüyordu, bazı kartlarda çok azdı; sonra "çok fazla piksel, küçük olanlar hiç olmasın" ve "/sonuc'taki kartın arkasına da piksel koymuşsun; pikseller CTA'larda olacak sadece". - Kök neden: PixelMasonry sabit 420×120 çizimi kaplayacak şekilde ölçekleniyordu; yüksek kartlarda görünen kısım boş banda düşüyordu. Artık kabın gerçek ölçüsüne göre üretiliyor (ResizeObserver), yalnız kenar çerçevesinde, orta boş; `haric` verilen öğenin (24'lük piksel tablosu) çevresi her genişlikte boş. Sunucuda boş, ölçülünce belirir. - Küçük (tek hücre) kare yok: 4×4 makro hücrede 2–4 hücrelik kareler, iri olan seyrek ve silik. Ölçüm: kapanış kartı 1280 px'te 58 kare (tabloya binen 0), 375 px'te 27 kare. - Beyaz içerik kartlarından zemin geri alındı (tadımlık, /listem üretim ve kilit kartları, boş durum, sıralama-yok, meraklısına, paket kartları, kaydet bandı, katalog üstü kompakt satır, /sonuc "listen hazır"). Kalanlar: CtaBandi, PikselCtaYuzeyi, arama penceresi kartı. PikselZemin'in açık tonu kaldırıldı. DESIGN.md §10.8 güncellendi. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useRef, type CSSProperties, type ReactNode } from "react";
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePixelHeat } from "@/components/use-pixel-heat";
|
||||
|
||||
@@ -74,41 +82,86 @@ function pixelRect(
|
||||
/**
|
||||
* CTA yüzeylerinin piksel zemini: büyüklü küçüklü kareler, masonry gibi
|
||||
* hafif kaymış (Bilal, 24 Eyl 2026: "arka planda büyüklü küçüklü, masonry
|
||||
* şeklinde"). İçeriğin oturduğu orta bant boş kalır; kareler kenarlara doğru
|
||||
* yoğunlaşır. 3×3 hücrelik makro hücrelere 1–3 hücrelik tek kare düşer ve
|
||||
* makro hücre içinde kaydırılır: kareler çakışmaz ama hizaya da girmez
|
||||
* (§1.1 "doğal duruş"). Durağan zemin: imleç ısı fırçasına katılmaz
|
||||
* (Bilal, 24 Eyl 2026: "üzerine gelindiğinde bir şey olmasın").
|
||||
* şeklinde"). Kabın **gerçek ölçüsüne** göre üretilir (ResizeObserver):
|
||||
* sabit bir çizimi kaplayacak şekilde ölçeklemek yüksek kartlarda görünen
|
||||
* kısmı boş banda düşürüyordu ("hiç doku yok"). Yalnız kenar çerçevesinde,
|
||||
* orta tamamen boş; `haric` verilen öğenin (ör. 24'lük piksel tablosu)
|
||||
* çevresi de boş kalır. Küçük (tek hücrelik) kare **yok** (Bilal, 24 Eyl
|
||||
* 2026: "çok fazla piksel, küçük olanlar hiç olmasın"): 4×4 hücrelik makro
|
||||
* hücrelere 2–4 hücrelik tek kare düşer ve makro hücre içinde kaydırılır:
|
||||
* çakışmaz ama hizaya da girmez (§1.1 "doğal duruş"). Durağan zemin:
|
||||
* imleç ısı fırçasına katılmaz.
|
||||
* Sunucuda boş çizilir, ölçülünce belirir (dekor; aria-hidden).
|
||||
*/
|
||||
// Varsayılan boş bant modül sabiti: her render'da yeni dizi olup useMemo'yu
|
||||
// boşa düşürmesin (rendering-hoist-jsx)
|
||||
const BOS_BANT_VARSAYILAN: [number, number] = [0.22, 0.78];
|
||||
|
||||
export function PixelMasonry({
|
||||
seed = 7,
|
||||
bosBant = BOS_BANT_VARSAYILAN,
|
||||
yogunluk = 1,
|
||||
kenar = 0.3,
|
||||
haric,
|
||||
className,
|
||||
}: {
|
||||
seed?: number;
|
||||
/** Genişliğin bu oranları arasındaki bant boş kalır (içerik orada) */
|
||||
bosBant?: [number, number];
|
||||
/** Doluluk çarpanı; 1 varsayılan, 0.6 seyrek */
|
||||
yogunluk?: number;
|
||||
/** Kenar çerçevesinin genişliği: kısa kenarın oranı (0.3 = %30) */
|
||||
kenar?: number;
|
||||
/** Çevresi boş bırakılacak öğe (kreatif, iri sayı) */
|
||||
haric?: RefObject<HTMLElement | null>;
|
||||
className?: string;
|
||||
}) {
|
||||
const COLS = 42;
|
||||
const ROWS = 12;
|
||||
const MAKRO = 3;
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const [olcu, setOlcu] = useState<{
|
||||
w: number;
|
||||
h: number;
|
||||
hx: number;
|
||||
hy: number;
|
||||
hw: number;
|
||||
hh: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const svg = svgRef.current;
|
||||
if (!svg) return;
|
||||
const olc = () => {
|
||||
const r = svg.getBoundingClientRect();
|
||||
const hr = haric?.current?.getBoundingClientRect();
|
||||
const sonraki = {
|
||||
w: Math.round(r.width),
|
||||
h: Math.round(r.height),
|
||||
hx: hr ? Math.round(hr.left - r.left) : 0,
|
||||
hy: hr ? Math.round(hr.top - r.top) : 0,
|
||||
hw: hr ? Math.round(hr.width) : 0,
|
||||
hh: hr ? Math.round(hr.height) : 0,
|
||||
};
|
||||
setOlcu((onceki) =>
|
||||
onceki &&
|
||||
onceki.w === sonraki.w &&
|
||||
onceki.h === sonraki.h &&
|
||||
onceki.hx === sonraki.hx &&
|
||||
onceki.hy === sonraki.hy &&
|
||||
onceki.hw === sonraki.hw &&
|
||||
onceki.hh === sonraki.hh
|
||||
? onceki
|
||||
: sonraki,
|
||||
);
|
||||
};
|
||||
olc();
|
||||
const ro = new ResizeObserver(olc);
|
||||
ro.observe(svg);
|
||||
const h = haric?.current;
|
||||
if (h) ro.observe(h);
|
||||
return () => ro.disconnect();
|
||||
}, [haric]);
|
||||
|
||||
const MAKRO = 4;
|
||||
const PAY = 14; // hariç öğenin çevresinde bırakılan boşluk (px)
|
||||
|
||||
// Çıktı seed'e göre deterministik; render başına yeniden üretme
|
||||
const pixels = useMemo(() => {
|
||||
const cx = (COLS - 1) / 2;
|
||||
const cy = (ROWS - 1) / 2;
|
||||
const maxD = Math.sqrt(cx * cx + cy * cy);
|
||||
const BAND_MIN = COLS * bosBant[0];
|
||||
const BAND_MAX = COLS * bosBant[1];
|
||||
|
||||
if (!olcu || olcu.w === 0 || olcu.h === 0) return null;
|
||||
const { w, h, hx, hy, hw, hh } = olcu;
|
||||
const cols = Math.ceil(w / CELL);
|
||||
const rows = Math.ceil(h / CELL);
|
||||
const kisa = Math.min(w, h);
|
||||
const sonuc: ReactNode[] = [];
|
||||
let accentIdx = 0;
|
||||
|
||||
@@ -141,55 +194,57 @@ export function PixelMasonry({
|
||||
);
|
||||
}
|
||||
// İri kare daha silik: yazının yanında bağırmasın
|
||||
const op = opacityFor(x, y) * (n === 1 ? 1 : n === 2 ? 0.8 : 0.6);
|
||||
const op = opacityFor(x, y) * (n === 2 ? 0.9 : n === 3 ? 0.7 : 0.55);
|
||||
return (
|
||||
<rect key={key} {...shared} fill="currentColor" fillOpacity={op} />
|
||||
);
|
||||
};
|
||||
|
||||
for (let my = 0; my < ROWS / MAKRO; my++) {
|
||||
for (let mx = 0; mx < COLS / MAKRO; mx++) {
|
||||
const haricte = (px: number, py: number) =>
|
||||
hw > 0 &&
|
||||
px >= hx - PAY &&
|
||||
px <= hx + hw + PAY &&
|
||||
py >= hy - PAY &&
|
||||
py <= hy + hh + PAY;
|
||||
|
||||
for (let my = 0; my < rows / MAKRO; my++) {
|
||||
for (let mx = 0; mx < cols / MAKRO; mx++) {
|
||||
const ox = mx * MAKRO;
|
||||
const oy = my * MAKRO;
|
||||
const mcx = ox + MAKRO / 2;
|
||||
if (mcx > BAND_MIN && mcx < BAND_MAX) continue;
|
||||
// Kenarlara doğru artan doluluk
|
||||
const d = Math.sqrt((mcx - cx) ** 2 + (oy + MAKRO / 2 - cy) ** 2) / maxD;
|
||||
const density = (0.3 + 0.6 * d * d) * yogunluk;
|
||||
if (hash(mx, my, seed) > density) {
|
||||
// Boş makro hücrede ara sıra tek küçük piksel: doku kopmasın
|
||||
if (hash(mx, my, seed + 3) < 0.25 * yogunluk) {
|
||||
const px = ox + Math.floor(hash(mx, my, seed + 4) * MAKRO);
|
||||
const py = oy + Math.floor(hash(mx, my, seed + 5) * MAKRO);
|
||||
sonuc.push(kare(px, py, 1, `k-${mx}-${my}`, false));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const cxPx = (ox + MAKRO / 2) * CELL;
|
||||
const cyPx = (oy + MAKRO / 2) * CELL;
|
||||
if (haricte(cxPx, cyPx)) continue;
|
||||
// En yakın kenara uzaklık, kısa kenarın oranı olarak (0 kenar, 0.5 orta)
|
||||
const k = Math.min(cxPx, w - cxPx, cyPx, h - cyPx) / kisa;
|
||||
// Yalnız çerçevede, kenara doğru artan doluluk; orta boş
|
||||
if (k >= kenar) continue;
|
||||
const density = (0.06 + 0.5 * (1 - k / kenar) ** 1.6) * yogunluk;
|
||||
if (hash(mx, my, seed) > density) continue;
|
||||
const r = hash(mx, my, seed + 7);
|
||||
const n = r < 0.5 ? 1 : r < 0.85 ? 2 : 3;
|
||||
// 2–4 hücre: orta boy sık, iri seyrek
|
||||
const n = r < 0.5 ? 2 : r < 0.85 ? 3 : 4;
|
||||
const serbest = MAKRO - n;
|
||||
const kx = ox + Math.floor(hash(mx, my, seed + 11) * (serbest + 1));
|
||||
const ky = oy + Math.floor(hash(mx, my, seed + 13) * (serbest + 1));
|
||||
const accent = n < 3 && hash(mx, my, seed + 99) > 0.94;
|
||||
const accent = n === 2 && hash(mx, my, seed + 99) > 0.95;
|
||||
sonuc.push(kare(kx, ky, n, `m-${mx}-${my}`, accent));
|
||||
// Büyük karenin yanına küçük bir eş: büyüklü küçüklü ritim
|
||||
if (n < 3 && hash(mx, my, seed + 17) < 0.45) {
|
||||
const ex = kx + n < ox + MAKRO ? kx + n : kx - 1;
|
||||
const ey = ky + n < oy + MAKRO ? ky + n : ky - 1;
|
||||
if (ex >= ox && ey >= oy) sonuc.push(kare(ex, ey, 1, `e-${mx}-${my}`, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return sonuc;
|
||||
}, [seed, bosBant, yogunluk]);
|
||||
}, [olcu, seed, yogunluk, kenar]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${COLS * CELL} ${ROWS * CELL}`}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
ref={svgRef}
|
||||
viewBox={olcu ? `0 0 ${olcu.w} ${olcu.h}` : undefined}
|
||||
preserveAspectRatio="xMinYMin slice"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className={cn("h-full w-full", className)}
|
||||
className={cn(
|
||||
"h-full w-full transition-opacity duration-500 motion-reduce:transition-none",
|
||||
pixels ? "opacity-100" : "opacity-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{pixels}
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user