[RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경

- 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭
- scale: max(dpr, 2)로 화질 2배 향상
- 캡처 준비 중 wait 커서 표시
- 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표)
This commit is contained in:
2026-03-31 15:18:36 +09:00
parent a8f96e7b39
commit 2701edf87a
2 changed files with 52 additions and 44 deletions

View File

@@ -189,7 +189,7 @@ export function MessengerModal() {
{/* Header */}
<div
className="flex items-center justify-between px-4 py-2 border-b bg-muted/30 select-none cursor-grab active:cursor-grabbing"
className="flex items-center justify-between px-4 py-2 border-b bg-muted/30 select-none cursor-move"
onMouseDown={onHeaderMouseDown}
>
<h2 className="text-sm font-semibold"></h2>

View File

@@ -16,10 +16,37 @@ interface ScreenCaptureProps {
export function ScreenCapture({ onCapture, onCancel }: ScreenCaptureProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const capturedImgRef = useRef<HTMLImageElement | null>(null);
const [ready, setReady] = useState(false);
const [selecting, setSelecting] = useState(false);
const startRef = useRef<{ x: number; y: number } | null>(null);
const [rect, setRect] = useState<Rect | null>(null);
// Pre-capture on mount so mouseup is instant
useEffect(() => {
let cancelled = false;
(async () => {
try {
const { domToPng } = await import("modern-screenshot");
const scale = Math.max(window.devicePixelRatio || 1, 2);
const dataUrl = await domToPng(document.body, {
width: window.innerWidth,
height: window.innerHeight,
scale,
});
if (cancelled) return;
const img = new Image();
img.src = dataUrl;
await new Promise((res) => { img.onload = res; });
capturedImgRef.current = img;
setReady(true);
} catch {
if (!cancelled) onCancel();
}
})();
return () => { cancelled = true; };
}, [onCancel]);
// ESC to cancel
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@@ -47,70 +74,51 @@ export function ScreenCapture({ onCapture, onCancel }: ScreenCaptureProps) {
setRect(getRect(startRef.current.x, startRef.current.y, e.clientX, e.clientY));
};
const handleMouseUp = async (e: React.MouseEvent) => {
const handleMouseUp = (e: React.MouseEvent) => {
if (!selecting || !startRef.current) return;
setSelecting(false);
const r = getRect(startRef.current.x, startRef.current.y, e.clientX, e.clientY);
startRef.current = null;
if (r.w < 4 || r.h < 4) {
onCancel();
return;
}
if (r.w < 4 || r.h < 4) { onCancel(); return; }
// Capture via modern-screenshot
try {
const { domToPng } = await import("modern-screenshot");
const dataUrl = await domToPng(document.body, {
width: window.innerWidth,
height: window.innerHeight,
});
const img = capturedImgRef.current;
if (!img) { onCancel(); return; }
// Crop the captured region — image is at CSS pixel scale
const img = new Image();
img.src = dataUrl;
await new Promise((res) => { img.onload = res; });
const scaleX = img.naturalWidth / window.innerWidth;
const scaleY = img.naturalHeight / window.innerHeight;
// Scale factor between actual image size and CSS pixels
const scaleX = img.naturalWidth / window.innerWidth;
const scaleY = img.naturalHeight / window.innerHeight;
const canvas = canvasRef.current!;
canvas.width = r.w * scaleX;
canvas.height = r.h * scaleY;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(
img,
r.x * scaleX, r.y * scaleY, r.w * scaleX, r.h * scaleY,
0, 0, r.w * scaleX, r.h * scaleY,
);
const canvas = canvasRef.current!;
canvas.width = r.w * scaleX;
canvas.height = r.h * scaleY;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(
img,
r.x * scaleX, r.y * scaleY, r.w * scaleX, r.h * scaleY,
0, 0, r.w * scaleX, r.h * scaleY,
);
canvas.toBlob((blob) => {
if (!blob) { onCancel(); return; }
const file = new File([blob], `capture-${Date.now()}.png`, { type: "image/png" });
onCapture(file);
}, "image/png");
} catch {
onCancel();
}
canvas.toBlob((blob) => {
if (!blob) { onCancel(); return; }
const file = new File([blob], `capture-${Date.now()}.png`, { type: "image/png" });
onCapture(file);
}, "image/png");
};
return (
<>
<canvas ref={canvasRef} className="hidden" />
<div
className="fixed inset-0 bg-black/40 cursor-crosshair select-none"
style={{ zIndex: 99999 }}
onMouseDown={handleMouseDown}
className="fixed inset-0 bg-black/40 select-none"
style={{ zIndex: 99999, cursor: ready ? "crosshair" : "wait" }}
onMouseDown={ready ? handleMouseDown : undefined}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
{/* instruction */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-black/70 text-white text-sm px-4 py-2 rounded-full pointer-events-none">
&nbsp;·&nbsp; ESC로
{ready ? "드래그하여 캡처 영역을 선택하세요" : "캡처 준비 중..."}&nbsp;·&nbsp;ESC로
</div>
{/* selection rect */}
{rect && rect.w > 0 && rect.h > 0 && (
<div
className="absolute border-2 border-blue-400 bg-blue-400/10 pointer-events-none"