ok
This commit is contained in:
@@ -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") {
|
||||||
|
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;
|
rectHeight = viewportHeight * 0.7;
|
||||||
rectWidth = rectHeight * portraitRatio;
|
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,7 +333,7 @@ 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;
|
||||||
|
|
||||||
@@ -341,11 +352,6 @@ const useCamera = ({ videoRef, canvasRef }) => {
|
|||||||
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;
|
||||||
|
|
||||||
@@ -353,49 +359,54 @@ const useCamera = ({ videoRef, canvasRef }) => {
|
|||||||
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;
|
||||||
offsetY = 0;
|
|
||||||
} else {
|
} else {
|
||||||
// Video is taller - it will be cropped vertically
|
|
||||||
displayWidth = canvasRect.width;
|
displayWidth = canvasRect.width;
|
||||||
displayHeight = displayWidth / videoAspectRatio;
|
displayHeight = displayWidth / videoAspectRatio;
|
||||||
offsetX = 0;
|
|
||||||
offsetY = (canvasRect.height - displayHeight) / 2;
|
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 });
|
|
||||||
console.log("Display info:", { displayWidth, displayHeight, offsetX, offsetY });
|
|
||||||
|
|
||||||
// Create crop canvas
|
|
||||||
const cropCanvas = document.createElement("canvas");
|
const cropCanvas = document.createElement("canvas");
|
||||||
cropCanvas.width = Math.round(cropWidth);
|
cropCanvas.width = Math.round(cropWidth);
|
||||||
cropCanvas.height = Math.round(cropHeight);
|
cropCanvas.height = Math.round(cropHeight);
|
||||||
const cropCtx = cropCanvas.getContext("2d");
|
const cropCtx = cropCanvas.getContext("2d");
|
||||||
|
|
||||||
// Draw cropped portion
|
|
||||||
cropCtx.drawImage(
|
cropCtx.drawImage(
|
||||||
video,
|
video,
|
||||||
Math.round(cropX), Math.round(cropY), Math.round(cropWidth), Math.round(cropHeight),
|
Math.round(cropX), Math.round(cropY), Math.round(cropWidth), Math.round(cropHeight),
|
||||||
0, 0, Math.round(cropWidth), Math.round(cropHeight)
|
0, 0, Math.round(cropWidth), Math.round(cropHeight)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("Image cropped successfully");
|
// --- 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);
|
return cropCanvas.toDataURL("image/jpeg", 0.9);
|
||||||
}, [videoRef, canvasRef]);
|
}, [videoRef, canvasRef, mode]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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(() => {
|
||||||
@@ -514,6 +530,25 @@ const CameraModal = ({ isOpen, onClose, onCapture }) => {
|
|||||||
<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 }) => {
|
|||||||
×
|
×
|
||||||
</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
|
||||||
|
|||||||
Reference in New Issue
Block a user