Skip to content

Commit

Permalink
(feat)O3-2365:Use fabricjs to build a custom editor
Browse files Browse the repository at this point in the history
  • Loading branch information
jona42-ui committed Sep 25, 2023
1 parent 6846401 commit 1ac42f8
Show file tree
Hide file tree
Showing 7 changed files with 1,438 additions and 9,019 deletions.
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,8 @@
"@openmrs/esm-patient-common-lib": "^5.0.0",
"@openmrs/openmrs-form-engine-lib": "latest",
"canvas-to-image": "^2.0.3",
"canvg": "^4.0.1",
"html2canvas": "^1.4.1",
"lodash-es": "^4.17.21",
"react-image-annotate": "^1.8.0"
"fabric": "^5.3.0",
"lodash-es": "^4.17.21"
},
"peerDependencies": {
"@openmrs/esm-framework": "*",
Expand Down
264 changes: 264 additions & 0 deletions src/components/custom-annotate.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import React, { useEffect, useRef, useState } from "react";
import { fabric } from "fabric";
import "./custom-annotate.style.css"; // Import your CSS file for styling

import { Button } from "@carbon/react";
import { createAttachment } from "../attachments/attachments.resource";
import { readFileAsString } from "../utils";

const SvgEditor = () => {
const canvasRef = useRef(null);
const [canvas, setCanvas] = useState(null);
const [drawingMode, setDrawingMode] = useState("rectangle");
const [stateHistory, setStateHistory] = useState([]);
const [currentStatePointer, setCurrentStatePointer] = useState(-1);
const [isDrawing, setIsDrawing] = useState(false);
const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 });
const [imageObject, setImageObject] = useState(null);
const [originalImage, setOriginalImage] = useState(null); // State to store the original image object
useEffect(() => {
const options = {
width: window.innerWidth, // Set canvas width to window width
height: window.innerHeight, // Set canvas height to window height
};
const newCanvas = new fabric.Canvas(canvasRef.current, options);
setCanvas(newCanvas);

newCanvas.on("mouse:down", handleMouseDown);
newCanvas.on("mouse:move", handleMouseMove);
newCanvas.on("mouse:up", handleMouseUp);

saveCanvasState();

return () => {
newCanvas.dispose();
};
}, []);

Check warning on line 36 in src/components/custom-annotate.component.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has missing dependencies: 'handleMouseDown', 'handleMouseMove', 'handleMouseUp', and 'saveCanvasState'. Either include them or remove the dependency array

const selectDrawingMode = (mode) => {
setDrawingMode(mode);
canvas.isDrawingMode = mode === "freehand";
};

const addShape = () => {
let shape;
if (drawingMode === "rectangle") {
shape = new fabric.Rect({
width: 100,
height: 50,
fill: "transparent",
stroke: "blue",
strokeWidth: 2,
left: 100,
top: 100,
});
} else if (drawingMode === "circle") {
shape = new fabric.Circle({
radius: 25,
fill: "transparent",
stroke: "red",
strokeWidth: 2,
left: 200,
top: 200,
});
}
if (shape) {
canvas.add(shape);
// Ensure the shape is always at the front
shape.bringToFront();
saveCanvasState();
}
};

const addText = () => {
const text = new fabric.IText("Type your text here", {
left: 300,
top: 300,
fill: "black",
});
canvas.add(text);
// Ensure the text is always at the front
text.bringToFront();
saveCanvasState();
};

const changeColor = (color) => {
const activeObject = canvas.getActiveObject();
if (activeObject) {
activeObject.set({ fill: color });
canvas.renderAll();
saveCanvasState();
}
};

const handleMouseDown = (event) => {
if (drawingMode === "freehand") {
setIsDrawing(true);
const { offsetX, offsetY } = event.e;
setLastPosition({ x: offsetX, y: offsetY });
}
};

const handleMouseMove = (event) => {
if (isDrawing) {
const { offsetX, offsetY } = event.e;
const path = new fabric.Path(
`M ${lastPosition.x} ${lastPosition.y} L ${offsetX} ${offsetY}`,
{
stroke: "blue",
strokeWidth: 2,
fill: "transparent",
}
);
canvas.add(path);
setLastPosition({ x: offsetX, y: offsetY });
}
};

const handleMouseUp = () => {
setIsDrawing(false);
saveCanvasState();
};

const saveCanvasState = () => {
const canvasState = JSON.stringify(canvas);
const newHistory = [
...stateHistory.slice(0, currentStatePointer + 1),
canvasState,
];
setStateHistory(newHistory);
setCurrentStatePointer(newHistory.length - 1);
};

const undo = () => {
if (currentStatePointer > 0) {
const newPointer = currentStatePointer - 1;
const canvasState = stateHistory[newPointer];
setCurrentStatePointer(newPointer);
canvas.loadFromJSON(canvasState, () => {
canvas.renderAll();
});
}
};

const redo = () => {
if (currentStatePointer < stateHistory.length - 1) {
const newPointer = currentStatePointer + 1;
const canvasState = stateHistory[newPointer];
setCurrentStatePointer(newPointer);
canvas.loadFromJSON(canvasState, () => {
canvas.renderAll();
});
}
};

const handleImageUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
// Remove the original image if it exists
if (originalImage) {
canvas.remove(originalImage);
}
// Load the image and add it to the canvas
fabric.Image.fromURL(e.target.result, (img) => {
// Center the image on the canvas
img.set({
left: canvas.width / 2,
top: canvas.height / 2,
draggable: true,
});

// Add the image to the canvas
canvas.add(img);
setOriginalImage(img);

// Bring the image to the front of the stacking order
img.bringToFront();
// Set the imageObject state
setImageObject(img);
canvas.renderAll();
saveCanvasState();
});
};
reader.readAsDataURL(file);
}
};

const saveAnnotatedImage = async () => {
const patientUuid = "ac64588b-9376-4ef4-b87f-13782647b4c8";
// Check if an image object exists
if (imageObject) {
// Get the original image object
const originalImage = canvas.getObjects("image")[0];

// Capture only the part of the canvas containing the original image and annotations as a data URL
const annotatedCanvasDataUrl = canvas.toDataURL({
format: "png",
left: originalImage.left,
top: originalImage.top,
width: originalImage.width,
height: originalImage.height,
});

// Convert the data URL to a Blob
const blob = await fetch(annotatedCanvasDataUrl).then((res) =>
res.blob()
);

// Create a File from the Blob (you can use the patientUuid as the filename)
const fileName = `${patientUuid}_annotated_image.png`;
const fileType = "image/png";
const fileDescription = "Annotated Image";

const file = new File([blob], fileName, { type: fileType });

// Read the file content as base64
const base64Content = await readFileAsString(file);

// Use createAttachment method to save the annotated image
await createAttachment(patientUuid, {
file,
fileName,
fileType,
fileDescription,
base64Content,
});
}
// TODO: Use the openmrs framework notification
alert("Annotated image saved successfully!");
};

return (
<div className="container">
<div className="side-panel">
<div className="tool-group">
<Button onClick={() => selectDrawingMode("rectangle")}>
Rectangle
</Button>
<Button onClick={() => selectDrawingMode("circle")}>Circle</Button>
<Button onClick={() => selectDrawingMode("freehand")}>
Freehand
</Button>
<Button onClick={addShape}>Add Shape</Button>
<Button onClick={addText}>Add Text</Button>
<input
type="color"
onChange={(e) => changeColor(e.target.value)}
style={{ width: "30px", height: "30px" }}
/>
<Button onClick={undo}>Undo</Button>
<Button onClick={redo}>Redo</Button>
<input type="file" accept="image/*" onChange={handleImageUpload} />
<Button onClick={saveAnnotatedImage}>Save</Button>
</div>
</div>
<div className="canvas-container">
<canvas ref={canvasRef} width="100%" height="100%" />
</div>
</div>
);
};

export default SvgEditor;
42 changes: 42 additions & 0 deletions src/components/custom-annotate.style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* Define styles for the canvas container */
canvas {
border: 2px solid #ccc; /* Add a border to the canvas */
display: block; /* Make the canvas a block-level element */
margin: 0 auto; /* Center the canvas horizontally */
}

/* Define styles for the buttons container */
.buttons {
margin-top: 20px; /* Add some top margin to the buttons */
}

/* Define styles for the drawing buttons */
button {
margin-right: 10px; /* Add right margin between buttons */
padding: 8px 16px; /* Add padding to buttons */
font-size: 16px; /* Set font size */
background-color: #007bff; /* Button background color */
color: #fff; /* Button text color */
border: none; /* Remove button border */
border-radius: 4px; /* Add button border radius */
cursor: pointer; /* Change cursor on hover */
}

button:hover {
background-color: #0056b3; /* Change background color on hover */
}

/* Define styles for the color input */
input[type="color"] {
margin-right: 10px; /* Add right margin to the color input */
}

/* Define styles for the undo and redo buttons */
button.undo, button.redo {
background-color: #6c757d; /* Button background color */
}

/* Define styles for the file input */
input[type="file"] {
display: inline-block; /* Hide the file input */
}
Loading

0 comments on commit 1ac42f8

Please sign in to comment.