[RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경
- 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭 - scale: max(dpr, 2)로 화질 2배 향상 - 캡처 준비 중 wait 커서 표시 - 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
드래그하여 캡처 영역을 선택하세요 · ESC로 취소
|
||||
{ready ? "드래그하여 캡처 영역을 선택하세요" : "캡처 준비 중..."} · 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"
|
||||
|
||||
Reference in New Issue
Block a user