492 lines
14 KiB
JavaScript
492 lines
14 KiB
JavaScript
import React, { useEffect, useRef, useState } from "react";
|
|
import Modal from "./Modal";
|
|
import FormComponent from "./FormComponent";
|
|
|
|
const STORAGE_KEY = "camera_canvas_gallery";
|
|
|
|
const CameraCanvas = () => {
|
|
const videoRef = useRef(null);
|
|
const canvasRef = useRef(null);
|
|
const hiddenCanvasRef = useRef(null);
|
|
const [capturedImage, setCapturedImage] = useState(null);
|
|
const [galleryImages, setGalleryImages] = useState([]);
|
|
const [fileTemp, setFileTemp] = useState(null);
|
|
const [isFreeze, setIsFreeze] = useState(false);
|
|
const freezeFrameRef = useRef(null);
|
|
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const fileInputRef = useRef(null);
|
|
|
|
const triggerFileSelect = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
const rectRef = useRef({
|
|
x: 0,
|
|
y: 0,
|
|
width: 0,
|
|
height: 0,
|
|
radius: 20,
|
|
});
|
|
|
|
const drawRoundedRect = (ctx, x, y, width, height, radius) => {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + radius, y);
|
|
ctx.lineTo(x + width - radius, y);
|
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
ctx.lineTo(x + width, y + height - radius);
|
|
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
ctx.lineTo(x + radius, y + height);
|
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
ctx.lineTo(x, y + radius);
|
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
ctx.closePath();
|
|
ctx.strokeStyle = "red";
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
};
|
|
|
|
const fillOutsideRect = (ctx, rect, canvasWidth, canvasHeight) => {
|
|
ctx.save();
|
|
const { x, y, width, height, radius } = rect;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + radius, y);
|
|
ctx.lineTo(x + width - radius, y);
|
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
ctx.lineTo(x + width, y + height - radius);
|
|
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
ctx.lineTo(x + radius, y + height);
|
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
ctx.lineTo(x, y + radius);
|
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
ctx.closePath();
|
|
ctx.rect(0, 0, canvasWidth, canvasHeight);
|
|
ctx.fillStyle = "rgba(173, 173, 173, 1)";
|
|
ctx.fill("evenodd");
|
|
ctx.restore();
|
|
};
|
|
|
|
useEffect(() => {
|
|
const savedGallery = localStorage.getItem(STORAGE_KEY);
|
|
if (savedGallery) setGalleryImages(JSON.parse(savedGallery));
|
|
|
|
const getCameraStream = async () => {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: { facingMode: { ideal: "environment" } },
|
|
audio: false,
|
|
});
|
|
|
|
if (videoRef.current) {
|
|
videoRef.current.srcObject = stream;
|
|
|
|
videoRef.current.onloadedmetadata = () => {
|
|
videoRef.current.play();
|
|
const video = videoRef.current;
|
|
const canvas = canvasRef.current;
|
|
const hiddenCanvas = hiddenCanvasRef.current;
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
canvas.style.maxWidth = "100%";
|
|
canvas.style.height = "auto";
|
|
|
|
hiddenCanvas.width = video.videoWidth;
|
|
hiddenCanvas.height = video.videoHeight;
|
|
|
|
const rectWidth = canvas.width * 0.9;
|
|
const rectHeight = (53.98 / 85.6) * rectWidth;
|
|
const rectX = (canvas.width - rectWidth) / 2;
|
|
const rectY = (canvas.height - rectHeight) / 2;
|
|
|
|
rectRef.current = {
|
|
x: rectX,
|
|
y: rectY,
|
|
width: rectWidth,
|
|
height: rectHeight,
|
|
radius: 20,
|
|
};
|
|
|
|
const drawToCanvas = () => {
|
|
if (video.readyState === 4) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
if (isFreeze && freezeFrameRef.current) {
|
|
ctx.putImageData(freezeFrameRef.current, 0, 0);
|
|
} else {
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
}
|
|
drawRoundedRect(
|
|
ctx,
|
|
rectRef.current.x,
|
|
rectRef.current.y,
|
|
rectRef.current.width,
|
|
rectRef.current.height,
|
|
rectRef.current.radius
|
|
);
|
|
if (isFreeze) {
|
|
fillOutsideRect(
|
|
ctx,
|
|
rectRef.current,
|
|
canvas.width,
|
|
canvas.height
|
|
);
|
|
}
|
|
}
|
|
requestAnimationFrame(drawToCanvas);
|
|
};
|
|
|
|
drawToCanvas();
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.error("Gagal mendapatkan kamera:", err);
|
|
}
|
|
};
|
|
|
|
getCameraStream();
|
|
}, [isFreeze]);
|
|
|
|
const shootImage = () => {
|
|
const video = videoRef.current;
|
|
const { x, y, width, height } = rectRef.current;
|
|
const hiddenCanvas = hiddenCanvasRef.current;
|
|
const hiddenCtx = hiddenCanvas.getContext("2d");
|
|
const visibleCtx = canvasRef.current.getContext("2d");
|
|
|
|
freezeFrameRef.current = visibleCtx.getImageData(
|
|
0,
|
|
0,
|
|
canvasRef.current.width,
|
|
canvasRef.current.height
|
|
);
|
|
setIsFreeze(true);
|
|
|
|
hiddenCtx.drawImage(video, 0, 0, hiddenCanvas.width, hiddenCanvas.height);
|
|
|
|
const cropCanvas = document.createElement("canvas");
|
|
cropCanvas.width = Math.floor(width);
|
|
cropCanvas.height = Math.floor(height);
|
|
const cropCtx = cropCanvas.getContext("2d");
|
|
|
|
cropCtx.drawImage(
|
|
hiddenCanvas,
|
|
Math.floor(x),
|
|
Math.floor(y),
|
|
Math.floor(width),
|
|
Math.floor(height),
|
|
0,
|
|
0,
|
|
Math.floor(width),
|
|
Math.floor(height)
|
|
);
|
|
|
|
const imageDataUrl = cropCanvas.toDataURL("image/png", 1.0);
|
|
setCapturedImage(imageDataUrl);
|
|
};
|
|
|
|
const ReadImage = async (capturedImage) => {
|
|
try {
|
|
setLoading(true);
|
|
setModalOpen(true);
|
|
|
|
let res = await fetch(
|
|
"https://bot.kediritechnopark.com/webhook/mastersnapper/read",
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ image: capturedImage }),
|
|
}
|
|
);
|
|
|
|
setLoading(false);
|
|
|
|
const data = await res.json();
|
|
console.log(data);
|
|
|
|
setFileTemp(data);
|
|
} catch (error) {
|
|
console.error("Failed to read image:", error);
|
|
}
|
|
};
|
|
|
|
const handleSaveTemp = async (correctedData) => {
|
|
try {
|
|
await fetch(
|
|
"https://bot.kediritechnopark.com/webhook/mastersnapper/save",
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ correctedData }),
|
|
}
|
|
);
|
|
|
|
const updatedGallery = [fileTemp, ...galleryImages];
|
|
setGalleryImages(updatedGallery);
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedGallery));
|
|
setFileTemp(null);
|
|
} catch (err) {
|
|
console.error("Gagal menyimpan ke server:", err);
|
|
}
|
|
};
|
|
|
|
const handleDeleteTemp = async () => {
|
|
try {
|
|
await fetch(
|
|
"https://bot.kediritechnopark.com/webhook/mastersnapper/delete",
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ fileTemp }),
|
|
}
|
|
);
|
|
|
|
setFileTemp(null);
|
|
} catch (err) {
|
|
console.error("Gagal menghapus dari server:", err);
|
|
}
|
|
};
|
|
|
|
const removeImage = (index) => {
|
|
const newGallery = [...galleryImages];
|
|
newGallery.splice(index, 1);
|
|
setGalleryImages(newGallery);
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(newGallery));
|
|
};
|
|
|
|
const aspectRatio = 53.98 / 85.6;
|
|
|
|
const handleManualUpload = async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
const imageDataUrl = reader.result;
|
|
setCapturedImage(imageDataUrl);
|
|
setIsFreeze(true);
|
|
|
|
// Create an image object from the uploaded file
|
|
const image = new Image();
|
|
image.onload = () => {
|
|
// Get the width of the rounded rectangle from rectRef
|
|
const rectWidth = rectRef.current.width;
|
|
const rectHeight = rectRef.current.height;
|
|
|
|
// Create a canvas to draw the uploaded image
|
|
const canvas = canvasRef.current;
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
// Set the scale factor based on the rectangle width
|
|
const scaleFactor = rectWidth / image.width;
|
|
|
|
// Calculate the new height based on the aspect ratio
|
|
const newHeight = image.height * scaleFactor;
|
|
|
|
// Clear the canvas and draw the video or freeze frame
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
if (isFreeze && freezeFrameRef.current) {
|
|
ctx.putImageData(freezeFrameRef.current, 0, 0);
|
|
} else {
|
|
const video = videoRef.current;
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
// Draw the rounded rectangle
|
|
drawRoundedRect(
|
|
ctx,
|
|
rectRef.current.x,
|
|
rectRef.current.y,
|
|
rectRef.current.width,
|
|
rectRef.current.height,
|
|
rectRef.current.radius
|
|
);
|
|
|
|
// Draw the image inside the rounded rectangle
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.moveTo(
|
|
rectRef.current.x + rectRef.current.radius,
|
|
rectRef.current.y
|
|
);
|
|
ctx.lineTo(
|
|
rectRef.current.x + rectRef.current.width - rectRef.current.radius,
|
|
rectRef.current.y
|
|
);
|
|
ctx.quadraticCurveTo(
|
|
rectRef.current.x + rectRef.current.width,
|
|
rectRef.current.y,
|
|
rectRef.current.x + rectRef.current.width,
|
|
rectRef.current.y + rectRef.current.radius
|
|
);
|
|
ctx.lineTo(
|
|
rectRef.current.x + rectRef.current.width,
|
|
rectRef.current.y + rectRef.current.height - rectRef.current.radius
|
|
);
|
|
ctx.quadraticCurveTo(
|
|
rectRef.current.x + rectRef.current.width,
|
|
rectRef.current.y + rectRef.current.height,
|
|
rectRef.current.x + rectRef.current.width - rectRef.current.radius,
|
|
rectRef.current.y + rectRef.current.height
|
|
);
|
|
ctx.lineTo(
|
|
rectRef.current.x + rectRef.current.radius,
|
|
rectRef.current.y + rectRef.current.height
|
|
);
|
|
ctx.quadraticCurveTo(
|
|
rectRef.current.x,
|
|
rectRef.current.y + rectRef.current.height,
|
|
rectRef.current.x,
|
|
rectRef.current.y + rectRef.current.height - rectRef.current.radius
|
|
);
|
|
ctx.lineTo(
|
|
rectRef.current.x,
|
|
rectRef.current.y + rectRef.current.radius
|
|
);
|
|
ctx.quadraticCurveTo(
|
|
rectRef.current.x,
|
|
rectRef.current.y,
|
|
rectRef.current.x + rectRef.current.radius,
|
|
rectRef.current.y
|
|
);
|
|
ctx.closePath();
|
|
ctx.clip(); // Clip the image within the rounded rectangle
|
|
|
|
// Draw the uploaded image inside the clipped region
|
|
ctx.drawImage(
|
|
image,
|
|
rectRef.current.x,
|
|
rectRef.current.y,
|
|
rectWidth,
|
|
newHeight // Height is scaled based on the image's aspect ratio
|
|
);
|
|
|
|
ctx.restore();
|
|
|
|
// Save the image data into the freeze frame reference
|
|
freezeFrameRef.current = ctx.getImageData(
|
|
0,
|
|
0,
|
|
canvas.width,
|
|
canvas.height
|
|
);
|
|
};
|
|
image.src = imageDataUrl;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
padding: "12px 16px",
|
|
backgroundColor: "#f5f5f5",
|
|
fontFamily: "sans-serif",
|
|
fontSize: "16px",
|
|
fontWeight: "bold",
|
|
}}
|
|
>
|
|
<button
|
|
style={{
|
|
marginRight: "12px",
|
|
fontSize: "18px",
|
|
background: "none",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<
|
|
</button>
|
|
<div>Scan KTP atau unggah</div>
|
|
</div>
|
|
<video
|
|
ref={videoRef}
|
|
autoPlay
|
|
playsInline
|
|
muted
|
|
style={{ display: "none" }}
|
|
/>
|
|
<canvas ref={canvasRef} style={{ maxWidth: "100%", height: "auto" }} />
|
|
<canvas ref={hiddenCanvasRef} style={{ display: "none" }} />
|
|
|
|
<div
|
|
style={{
|
|
backgroundColor: "white",
|
|
borderRadius: 16,
|
|
textAlign: "center",
|
|
top: "-17px",
|
|
position: "relative",
|
|
padding: "20px",
|
|
}}
|
|
>
|
|
<h2 style={{ marginTop: 0 }}>Data Verification</h2>
|
|
{!isFreeze ? (
|
|
<>
|
|
<div
|
|
style={{
|
|
padding: 10,
|
|
backgroundColor: "#ff6d6d",
|
|
borderRadius: 15,
|
|
color: "white",
|
|
fontWeight: "bold",
|
|
}}
|
|
onClick={shootImage}
|
|
>
|
|
Ambil Gambar
|
|
</div>
|
|
<div>atau</div>
|
|
<div
|
|
style={{
|
|
padding: 10,
|
|
backgroundColor: "#ff6d6d",
|
|
borderRadius: 15,
|
|
color: "white",
|
|
fontWeight: "bold",
|
|
}}
|
|
onClick={triggerFileSelect}
|
|
>
|
|
Upload Gambar
|
|
</div>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={(e) => handleManualUpload(e)}
|
|
style={{ marginRight: 10, display: "none" }}
|
|
/>
|
|
</>
|
|
) : (
|
|
<div
|
|
style={{
|
|
padding: 10,
|
|
backgroundColor: "#ff6d6d",
|
|
borderRadius: 15,
|
|
color: "white",
|
|
fontWeight: "bold",
|
|
}}
|
|
onClick={() => ReadImage(capturedImage)}
|
|
>
|
|
Scan KTP
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Modal
|
|
isOpen={modalOpen}
|
|
onClose={() => setModalOpen(false)}
|
|
loading={loading}
|
|
fileTemp={fileTemp}
|
|
onSave={handleSaveTemp}
|
|
onDelete={handleDeleteTemp}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CameraCanvas;
|