This commit is contained in:
Vassshhh
2025-09-29 09:28:18 +07:00
parent f30087d4f2
commit 57c3debfab

View File

@@ -20,6 +20,7 @@ const SPINNER_ANIMATION_STYLE = `
} }
`; `;
const cameraModalStyles = { const cameraModalStyles = {
modalOverlay: { modalOverlay: {
position: "fixed", top: 0, left: 0, right: 0, bottom: 0, position: "fixed", top: 0, left: 0, right: 0, bottom: 0,
@@ -108,7 +109,7 @@ const cameraModalStyles = {
} }
}; };
const useCamera = ({ videoRef, canvasRef }) => { const useCamera = ({ videoRef, canvasRef, mode }) => {
const streamRef = useRef(null); const streamRef = useRef(null);
const guideRectRef = useRef({ x: 0, y: 0, width: 0, height: 0, radius: 20 }); const guideRectRef = useRef({ x: 0, y: 0, width: 0, height: 0, radius: 20 });
const animationFrameId = useRef(null); const animationFrameId = useRef(null);
@@ -218,15 +219,25 @@ const useCamera = ({ videoRef, canvasRef }) => {
ctx.scale(pixelRatio, pixelRatio); ctx.scale(pixelRatio, pixelRatio);
// Calculate guide rectangle (portrait ratio for ID cards) // Calculate guide rectangle (portrait ratio for ID cards)
const portraitRatio = 0.63; // Width/Height ratio for ID cards // hitung ratio sesuai mode
let rectWidth = viewportWidth * 0.85; const portraitRatio = 0.63; // KTP (lebih tinggi)
let rectHeight = rectWidth / portraitRatio; const landscapeRatio = 1.58; // selalu horizontal
// Ensure it fits in viewport let rectWidth, rectHeight;
if (rectHeight > viewportHeight * 0.7) { if (mode === "portrait") {
rectHeight = viewportHeight * 0.7; rectWidth = viewportWidth * 0.85;
rectWidth = rectHeight * portraitRatio; rectHeight = rectWidth / portraitRatio;
} } else {
rectWidth = viewportWidth * 0.85;
rectHeight = rectWidth / landscapeRatio;
}
// pastikan tetap muat di layar
if (rectHeight > viewportHeight * 0.7) {
rectHeight = viewportHeight * 0.7;
rectWidth = rectHeight * (mode === "portrait" ? portraitRatio : landscapeRatio);
}
guideRectRef.current = { guideRectRef.current = {
x: (viewportWidth - rectWidth) / 2, x: (viewportWidth - rectWidth) / 2,
@@ -249,7 +260,7 @@ const useCamera = ({ videoRef, canvasRef }) => {
}; };
drawLoop(); drawLoop();
}, [videoRef, canvasRef]); }, [videoRef, canvasRef, mode]);
const startCamera = useCallback(async () => { const startCamera = useCallback(async () => {
console.log("Starting camera..."); console.log("Starting camera...");
@@ -322,80 +333,80 @@ const useCamera = ({ videoRef, canvasRef }) => {
} }
}, [videoRef, stopCameraStream, setupCanvasAndDrawLoop]); }, [videoRef, stopCameraStream, setupCanvasAndDrawLoop]);
const captureImage = useCallback(() => { const captureImage = useCallback(() => {
const video = videoRef.current; const video = videoRef.current;
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!video || video.readyState < 2) { if (!video || video.readyState < 2) {
console.log("Video not ready for capture"); console.log("Video not ready for capture");
return null; return null;
} }
console.log("Capturing and cropping image..."); console.log("Capturing and cropping image...");
const guide = guideRectRef.current; const guide = guideRectRef.current;
if (!canvas) return null; if (!canvas) return null;
// Get actual dimensions // Get actual dimensions
const canvasRect = canvas.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect();
const videoWidth = video.videoWidth; const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight; const videoHeight = video.videoHeight;
console.log("Video dimensions:", videoWidth, "x", videoHeight);
console.log("Canvas dimensions:", canvasRect.width, "x", canvasRect.height);
console.log("Guide rect:", guide);
// Calculate how the video is displayed (object-fit: cover) const videoAspectRatio = videoWidth / videoHeight;
const videoAspectRatio = videoWidth / videoHeight; const canvasAspectRatio = canvasRect.width / canvasRect.height;
const canvasAspectRatio = canvasRect.width / canvasRect.height;
let displayWidth, displayHeight, offsetX = 0, offsetY = 0; let displayWidth, displayHeight, offsetX = 0, offsetY = 0;
let scaleX, scaleY; let scaleX, scaleY;
if (videoAspectRatio > canvasAspectRatio) { if (videoAspectRatio > canvasAspectRatio) {
// Video is wider - it will be cropped horizontally displayHeight = canvasRect.height;
displayHeight = canvasRect.height; displayWidth = displayHeight * videoAspectRatio;
displayWidth = displayHeight * videoAspectRatio; offsetX = (canvasRect.width - displayWidth) / 2;
offsetX = (canvasRect.width - displayWidth) / 2; } else {
offsetY = 0; displayWidth = canvasRect.width;
} else { displayHeight = displayWidth / videoAspectRatio;
// Video is taller - it will be cropped vertically offsetY = (canvasRect.height - displayHeight) / 2;
displayWidth = canvasRect.width; }
displayHeight = displayWidth / videoAspectRatio;
offsetX = 0;
offsetY = (canvasRect.height - displayHeight) / 2;
}
// Calculate scale factors from displayed video to actual video scaleX = videoWidth / displayWidth;
scaleX = videoWidth / displayWidth; scaleY = videoHeight / displayHeight;
scaleY = videoHeight / displayHeight;
// Calculate crop coordinates in video space const cropX = Math.max(0, (guide.x - offsetX) * scaleX);
const cropX = Math.max(0, (guide.x - offsetX) * scaleX); const cropY = Math.max(0, (guide.y - offsetY) * scaleY);
const cropY = Math.max(0, (guide.y - offsetY) * scaleY); const cropWidth = Math.min(videoWidth - cropX, guide.width * scaleX);
const cropWidth = Math.min(videoWidth - cropX, guide.width * scaleX); const cropHeight = Math.min(videoHeight - cropY, guide.height * scaleY);
const cropHeight = Math.min(videoHeight - cropY, guide.height * scaleY);
console.log("Crop coordinates:", { cropX, cropY, cropWidth, cropHeight }); // --- Crop canvas ---
console.log("Scale factors:", { scaleX, scaleY }); const cropCanvas = document.createElement("canvas");
console.log("Display info:", { displayWidth, displayHeight, offsetX, offsetY }); cropCanvas.width = Math.round(cropWidth);
cropCanvas.height = Math.round(cropHeight);
const cropCtx = cropCanvas.getContext("2d");
// Create crop canvas cropCtx.drawImage(
const cropCanvas = document.createElement("canvas"); video,
cropCanvas.width = Math.round(cropWidth); Math.round(cropX), Math.round(cropY), Math.round(cropWidth), Math.round(cropHeight),
cropCanvas.height = Math.round(cropHeight); 0, 0, Math.round(cropWidth), Math.round(cropHeight)
const cropCtx = cropCanvas.getContext("2d"); );
// --- Jika mode portrait, rotasi ke landscape ---
if (mode === "portrait") {
const rotatedCanvas = document.createElement("canvas");
rotatedCanvas.width = cropCanvas.height; // dibalik
rotatedCanvas.height = cropCanvas.width;
const rctx = rotatedCanvas.getContext("2d");
// Putar 90° CW
rctx.translate(rotatedCanvas.width / 2, rotatedCanvas.height / 2);
rctx.rotate(-90 * Math.PI / 180);
rctx.drawImage(cropCanvas, -cropCanvas.width / 2, -cropCanvas.height / 2);
return rotatedCanvas.toDataURL("image/jpeg", 0.9);
}
return cropCanvas.toDataURL("image/jpeg", 0.9);
}, [videoRef, canvasRef, mode]);
// Draw cropped portion
cropCtx.drawImage(
video,
Math.round(cropX), Math.round(cropY), Math.round(cropWidth), Math.round(cropHeight),
0, 0, Math.round(cropWidth), Math.round(cropHeight)
);
console.log("Image cropped successfully");
return cropCanvas.toDataURL("image/jpeg", 0.9);
}, [videoRef, canvasRef]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -414,8 +425,13 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
const [cameraReady, setCameraReady] = useState(false); const [cameraReady, setCameraReady] = useState(false);
const videoRef = useRef(null); const videoRef = useRef(null);
const canvasRef = useRef(null); const canvasRef = useRef(null);
const [mode, setMode] = useState("portrait");
const toggleMode = () => {
setMode((prev) => (prev === "portrait" ? "landscape" : "portrait"));
};
const { startCamera, stopCameraStream, captureImage } = useCamera({ videoRef, canvasRef, mode });
const { startCamera, stopCameraStream, captureImage } = useCamera({ videoRef, canvasRef });
// Handle modal open/close and step changes // Handle modal open/close and step changes
useEffect(() => { useEffect(() => {
@@ -512,8 +528,27 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
<div style={cameraModalStyles.modalOverlay}> <div style={cameraModalStyles.modalOverlay}>
<style>{SPINNER_ANIMATION_STYLE}</style> <style>{SPINNER_ANIMATION_STYLE}</style>
<div style={cameraModalStyles.modalContent}> <div style={cameraModalStyles.modalContent}>
{step === "camera" && ( {step === "camera" && (
<div style={cameraModalStyles.cameraContainer}> <div style={cameraModalStyles.cameraContainer}>
<button
onClick={toggleMode}
style={{
position: "absolute",
top: "20px",
right: "20px",
backgroundColor: "rgba(0,0,0,0.6)",
color: "white",
border: "none",
borderRadius: "8px",
padding: "8px 12px",
fontSize: "14px",
cursor: "pointer",
zIndex: 11,
}}
>
{mode === "portrait" ? "Mode Horizontal" : "Mode Vertikal"}
</button>
<video <video
ref={videoRef} ref={videoRef}
autoPlay autoPlay
@@ -521,6 +556,7 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
muted muted
style={cameraModalStyles.video} style={cameraModalStyles.video}
/> />
<canvas <canvas
ref={canvasRef} ref={canvasRef}
style={cameraModalStyles.canvas} style={cameraModalStyles.canvas}
@@ -534,9 +570,7 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
&times; &times;
</button> </button>
<div style={cameraModalStyles.instructionText}>
Posisikan dokumen dalam area hijau dan tekan tombol untuk mengambil gambar
</div>
<div style={cameraModalStyles.cameraControls}> <div style={cameraModalStyles.cameraControls}>
<button <button