Refactor code structure for improved readability and maintainability
All checks were successful
Deploy / deploy (push) Successful in 10m26s

This commit is contained in:
bilalgursen
2026-07-30 03:17:12 +03:00
parent 40a1c8ef8e
commit dc6d894a37
33 changed files with 1574 additions and 651 deletions

View File

@@ -0,0 +1,109 @@
"use client";
/**
* Hero'daki (TurkeyPixelMap) imleç ısı fırçasının ortak hook hali.
* `data-px` işaretli rect'ler imleç yaklaşınca anında turuncuya ısınır,
* uzaklaşınca yavaşça kendi rengine soğur (asimetrik zamanlama).
*
* SVG katmanları çoğunlukla pointer-events-none olduğundan imleç window
* üzerinden izlenir ve viewBox koordinatına çevrilir. Beklenen veri
* öznitelikleri: data-px, data-cx, data-cy, data-op (taban opaklık).
*/
import { useEffect, type RefObject } from "react";
import { animate, type AnimationPlaybackControls } from "motion";
const FINE_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
const HOT_COLOR = "#f97316"; // orange-500
const HOT_OPACITY = 0.85;
const HEAT_DURATION = 0.15; // ısınma: anlık geri bildirim
const COOL_DURATION = 1.4; // soğuma: iz gibi yavaşça söner
type HeatTarget = {
el: SVGRectElement;
cx: number;
cy: number;
baseOpacity: number;
};
export function usePixelHeat(
svgRef: RefObject<SVGSVGElement | null>,
{
/** Fırça yarıçapı, SVG (viewBox) biriminde */
radius = 34,
}: { radius?: number } = {},
) {
useEffect(() => {
const svg = svgRef.current;
if (!svg) return;
// Dokunmatik cihazlarda hover yanlış tetiklenir; efekti imleçli cihazlara sakla
if (!window.matchMedia(FINE_POINTER_QUERY).matches) return;
const viewW = svg.viewBox.baseVal.width;
if (!viewW) return;
const baseColor = getComputedStyle(svg).color;
const pixels: HeatTarget[] = Array.from(
svg.querySelectorAll<SVGRectElement>("[data-px]"),
).map((el) => ({
el,
cx: Number(el.dataset.cx),
cy: Number(el.dataset.cy),
baseOpacity: Number(el.dataset.op),
}));
if (pixels.length === 0) return;
const hot = new Set<SVGRectElement>();
const controls = new Map<SVGRectElement, AnimationPlaybackControls>();
const run = (
el: SVGRectElement,
to: { fill: string; fillOpacity: number },
duration: number,
) => {
controls.get(el)?.stop();
controls.set(el, animate(el, to, { duration, ease: "easeOut" }));
};
const onMove = (e: PointerEvent) => {
const rect = svg.getBoundingClientRect();
if (rect.width === 0) return;
const scale = rect.width / viewW;
const pad = radius * scale;
// İmleç fırça menzilinin tamamen dışındaysa ve sıcak piksel yoksa
// hiç hesap yapma (sayfada birden çok örnek dinlediği için önemli)
if (
hot.size === 0 &&
(e.clientX < rect.left - pad ||
e.clientX > rect.right + pad ||
e.clientY < rect.top - pad ||
e.clientY > rect.bottom + pad)
) {
return;
}
const x = (e.clientX - rect.left) / scale;
const y = (e.clientY - rect.top) / scale;
for (const p of pixels) {
const d2 = (p.cx - x) ** 2 + (p.cy - y) ** 2;
const isHot = d2 <= radius * radius;
if (isHot && !hot.has(p.el)) {
hot.add(p.el);
run(p.el, { fill: HOT_COLOR, fillOpacity: HOT_OPACITY }, HEAT_DURATION);
} else if (!isHot && hot.has(p.el)) {
hot.delete(p.el);
run(
p.el,
{ fill: baseColor, fillOpacity: p.baseOpacity },
COOL_DURATION,
);
}
}
};
window.addEventListener("pointermove", onMove, { passive: true });
return () => {
window.removeEventListener("pointermove", onMove);
controls.forEach((c) => c.stop());
};
}, [svgRef, radius]);
}