ok
This commit is contained in:
@@ -20,6 +20,7 @@ const SPINNER_ANIMATION_STYLE = `
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const cameraModalStyles = {
|
||||
modalOverlay: {
|
||||
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 guideRectRef = useRef({ x: 0, y: 0, width: 0, height: 0, radius: 20 });
|
||||
const animationFrameId = useRef(null);
|
||||
@@ -218,15 +219,25 @@ const useCamera = ({ videoRef, canvasRef }) => {
|
||||
ctx.scale(pixelRatio, pixelRatio);
|
||||
|
||||
// Calculate guide rectangle (portrait ratio for ID cards)
|
||||
const portraitRatio = 0.63; // Width/Height ratio for ID cards
|
||||
let rectWidth = viewportWidth * 0.85;
|
||||
let rectHeight = rectWidth / portraitRatio;
|
||||
// hitung ratio sesuai mode
|
||||
const portraitRatio = 0.63; // KTP (lebih tinggi)
|
||||
const landscapeRatio = 1.58; // selalu horizontal
|
||||
|
||||
let rectWidth, rectHeight;
|
||||
if (mode === "portrait") {
|
||||
rectWidth = viewportWidth * 0.85;
|
||||
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);
|
||||
}
|
||||
|
||||
// Ensure it fits in viewport
|
||||
if (rectHeight > viewportHeight * 0.7) {
|
||||
rectHeight = viewportHeight * 0.7;
|
||||
rectWidth = rectHeight * portraitRatio;
|
||||
}
|
||||
|
||||
guideRectRef.current = {
|
||||
x: (viewportWidth - rectWidth) / 2,
|
||||
@@ -249,7 +260,7 @@ const useCamera = ({ videoRef, canvasRef }) => {
|
||||
};
|
||||
|
||||
drawLoop();
|
||||
}, [videoRef, canvasRef]);
|
||||
}, [videoRef, canvasRef, mode]);
|
||||
|
||||
const startCamera = useCallback(async () => {
|
||||
console.log("Starting camera...");
|
||||
@@ -322,80 +333,80 @@ const useCamera = ({ videoRef, canvasRef }) => {
|
||||
}
|
||||
}, [videoRef, stopCameraStream, setupCanvasAndDrawLoop]);
|
||||
|
||||
const captureImage = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
const captureImage = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!video || video.readyState < 2) {
|
||||
console.log("Video not ready for capture");
|
||||
return null;
|
||||
}
|
||||
if (!video || video.readyState < 2) {
|
||||
console.log("Video not ready for capture");
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("Capturing and cropping image...");
|
||||
console.log("Capturing and cropping image...");
|
||||
|
||||
const guide = guideRectRef.current;
|
||||
if (!canvas) return null;
|
||||
const guide = guideRectRef.current;
|
||||
if (!canvas) return null;
|
||||
|
||||
// Get actual dimensions
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const videoWidth = video.videoWidth;
|
||||
const videoHeight = video.videoHeight;
|
||||
// Get actual dimensions
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const videoWidth = video.videoWidth;
|
||||
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);
|
||||
const videoAspectRatio = videoWidth / videoHeight;
|
||||
const canvasAspectRatio = canvasRect.width / canvasRect.height;
|
||||
|
||||
// Calculate how the video is displayed (object-fit: cover)
|
||||
const videoAspectRatio = videoWidth / videoHeight;
|
||||
const canvasAspectRatio = canvasRect.width / canvasRect.height;
|
||||
let displayWidth, displayHeight, offsetX = 0, offsetY = 0;
|
||||
let scaleX, scaleY;
|
||||
|
||||
let displayWidth, displayHeight, offsetX = 0, offsetY = 0;
|
||||
let scaleX, scaleY;
|
||||
if (videoAspectRatio > canvasAspectRatio) {
|
||||
displayHeight = canvasRect.height;
|
||||
displayWidth = displayHeight * videoAspectRatio;
|
||||
offsetX = (canvasRect.width - displayWidth) / 2;
|
||||
} else {
|
||||
displayWidth = canvasRect.width;
|
||||
displayHeight = displayWidth / videoAspectRatio;
|
||||
offsetY = (canvasRect.height - displayHeight) / 2;
|
||||
}
|
||||
|
||||
if (videoAspectRatio > canvasAspectRatio) {
|
||||
// Video is wider - it will be cropped horizontally
|
||||
displayHeight = canvasRect.height;
|
||||
displayWidth = displayHeight * videoAspectRatio;
|
||||
offsetX = (canvasRect.width - displayWidth) / 2;
|
||||
offsetY = 0;
|
||||
} else {
|
||||
// Video is taller - it will be cropped vertically
|
||||
displayWidth = canvasRect.width;
|
||||
displayHeight = displayWidth / videoAspectRatio;
|
||||
offsetX = 0;
|
||||
offsetY = (canvasRect.height - displayHeight) / 2;
|
||||
}
|
||||
scaleX = videoWidth / displayWidth;
|
||||
scaleY = videoHeight / displayHeight;
|
||||
|
||||
// Calculate scale factors from displayed video to actual video
|
||||
scaleX = videoWidth / displayWidth;
|
||||
scaleY = videoHeight / displayHeight;
|
||||
const cropX = Math.max(0, (guide.x - offsetX) * scaleX);
|
||||
const cropY = Math.max(0, (guide.y - offsetY) * scaleY);
|
||||
const cropWidth = Math.min(videoWidth - cropX, guide.width * scaleX);
|
||||
const cropHeight = Math.min(videoHeight - cropY, guide.height * scaleY);
|
||||
|
||||
// Calculate crop coordinates in video space
|
||||
const cropX = Math.max(0, (guide.x - offsetX) * scaleX);
|
||||
const cropY = Math.max(0, (guide.y - offsetY) * scaleY);
|
||||
const cropWidth = Math.min(videoWidth - cropX, guide.width * scaleX);
|
||||
const cropHeight = Math.min(videoHeight - cropY, guide.height * scaleY);
|
||||
// --- Crop canvas ---
|
||||
const cropCanvas = document.createElement("canvas");
|
||||
cropCanvas.width = Math.round(cropWidth);
|
||||
cropCanvas.height = Math.round(cropHeight);
|
||||
const cropCtx = cropCanvas.getContext("2d");
|
||||
|
||||
console.log("Crop coordinates:", { cropX, cropY, cropWidth, cropHeight });
|
||||
console.log("Scale factors:", { scaleX, scaleY });
|
||||
console.log("Display info:", { displayWidth, displayHeight, offsetX, offsetY });
|
||||
cropCtx.drawImage(
|
||||
video,
|
||||
Math.round(cropX), Math.round(cropY), Math.round(cropWidth), Math.round(cropHeight),
|
||||
0, 0, Math.round(cropWidth), Math.round(cropHeight)
|
||||
);
|
||||
|
||||
// Create crop canvas
|
||||
const cropCanvas = document.createElement("canvas");
|
||||
cropCanvas.width = Math.round(cropWidth);
|
||||
cropCanvas.height = 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(() => {
|
||||
return () => {
|
||||
@@ -414,8 +425,13 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
|
||||
const [cameraReady, setCameraReady] = useState(false);
|
||||
const videoRef = 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
|
||||
useEffect(() => {
|
||||
@@ -514,6 +530,25 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
|
||||
<div style={cameraModalStyles.modalContent}>
|
||||
{step === "camera" && (
|
||||
<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
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
@@ -521,6 +556,7 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
|
||||
muted
|
||||
style={cameraModalStyles.video}
|
||||
/>
|
||||
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={cameraModalStyles.canvas}
|
||||
@@ -534,9 +570,7 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
|
||||
×
|
||||
</button>
|
||||
|
||||
<div style={cameraModalStyles.instructionText}>
|
||||
Posisikan dokumen dalam area hijau dan tekan tombol untuk mengambil gambar
|
||||
</div>
|
||||
|
||||
|
||||
<div style={cameraModalStyles.cameraControls}>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user