Here it is.
To save the annotated file, you need to add a basic column as an Image placeholder and a Row ID column. For the JavaScript column I provided to work, target p1 to the Image placeholder column and p2 to your Row ID column.
Additionally, you need to insert your own API data into the code at the end of the provided script (read the comments in the script).
Script
function generateAnnotatedImageURI(imageSource, rowID) {
const htmlContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Annotated Image</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
:root {
--text-color-light: #1f2937;
--text-color-dark: #cccccc;
}
body {
color: var(--text-color-light);
transition: background-color 0.5s, color 0.5s;
}
body.dark {
color: var(--text-color-dark);
}
label, .color-option {
color: inherit;
transition: color 0.5s;
}
body.dark label,
body.dark .color-option {
color: var(--text-color-dark);
}
canvas {
border-radius: 0.5rem;
display: block;
margin: 0 auto;
width: 100%;
height: auto;
}
.color-option {
border: 3px solid transparent;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
display: inline-block;
}
.color-option.selected {
border-color: white;
}
#save:hover {
opacity: 0.9;
}
</style>
<body class="flex justify-center items-center min-h-screen m-0">
<div class="relative text-center w-full">
<div class="canvas-container" style="margin-bottom: 20px; width: 100%;">
<canvas id="canvas" style="width: 100%; height: auto;"></canvas>
</div>
<div class="controls" style="display: grid; grid-template-columns: 1fr auto; gap: 10px; width: 100%;">
<div class="control-content">
<div class="flex flex-col md:flex-row items-start mt-2 gap-2">
<div class="flex items-center mb-2 md:mb-0">
<label class="mr-2">Size:</label>
<input type="range" id="sizeRange" min="1" max="50" value="5" class="w-32 h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer" style="outline:none">
<span id="sizeValue" class="ml-2" style="font-size: 0.825rem; width: 20px; text-align: center; display: inline-block;">5</span>
</div>
<div class="flex items-center mb-2 md:mt-0">
<label class="mr-2">Color:</label>
<div class="color-option cursor-pointer" data-color="black" style="background-color: #323232;"></div>
<div class="color-option cursor-pointer ml-1" data-color="red" style="background-color: red;"></div>
<div class="color-option cursor-pointer ml-1" data-color="blue" style="background-color: blue;"></div>
<div class="color-option cursor-pointer ml-1" data-color="green" style="background-color: green;"></div>
<div class="color-option cursor-pointer ml-1" data-color="yellow" style="background-color: yellow;"></div>
</div>
</div>
<div class="flex justify-start mb-2 mt-2 gap-2">
<button id="clear" class="border rounded px-3 py-1 bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500">Clear</button>
<button id="save" class="border rounded px-3 py-1" style="background-color: orange; color: white;">Save</button>
<button id="download" class="border rounded px-3 py-1 bg-gray-200 hover:bg-gray-300" title="Download">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path d="M3 12V17C3 19.2091 4.79086 21 7 21H17C19.2091 21 21 19.2091 21 17V12M7 11L12 16L17 11M12 3L12 15.5" stroke="black" stroke-width="1.5" vector-effect="non-scaling-stroke"></path>
</svg>
</button>
</div>
</div>
<div class="flex gap-2 justify-self-end align-self-start items-start" style="position: relative; top: 0;">
<button id="undo" class="icon-button p-2 bg-white rounded hover:opacity-70 text-gray-800 dark:text-white" title="Undo" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
</svg>
</button>
<button id="redo" class="icon-button p-2 bg-white rounded hover:opacity-70 text-gray-800 dark:text-white" title="Redo" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6"></path>
</svg>
</button>
</div>
</div>
</div>
<script>
// Detect and apply theme
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
if (prefersDarkScheme.matches) {
document.body.classList.add("dark");
}
prefersDarkScheme.addEventListener("change", (e) => {
if (e.matches) {
document.body.classList.add("dark");
} else {
document.body.classList.remove("dark");
}
});
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.crossOrigin = "Anonymous";
img.src = "${imageSource}";
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
saveState();
};
let color = "black";
let size = document.getElementById("sizeRange").value;
let isDrawing = false;
let history = [];
let historyStep = -1;
document.getElementById("sizeRange").oninput = (e) => {
size = e.target.value;
document.getElementById("sizeValue").textContent = size;
};
document.querySelectorAll(".color-option").forEach((colorOption) => {
colorOption.onclick = () => {
color = colorOption.getAttribute('data-color');
document.querySelectorAll(".color-option").forEach(opt => opt.classList.remove('selected'));
colorOption.classList.add('selected');
};
});
document.querySelectorAll(".color-option")[0].classList.add('selected');
document.getElementById("clear").onclick = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, img.width, img.height);
saveState();
};
function getMousePos(canvas, evt) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY
};
}
canvas.onmousedown = (e) => {
isDrawing = true;
ctx.beginPath();
ctx.lineWidth = size;
ctx.strokeStyle = color;
ctx.lineJoin = "round";
ctx.lineCap = "round";
const pos = getMousePos(canvas, e);
ctx.moveTo(pos.x, pos.y);
};
canvas.onmousemove = (e) => {
if (isDrawing) {
const pos = getMousePos(canvas, e);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}
};
canvas.onmouseup = () => {
isDrawing = false;
saveState();
};
function saveState() {
if (historyStep < history.length - 1) {
history = history.slice(0, historyStep + 1);
}
history.push(canvas.toDataURL());
historyStep++;
updateUndoRedo();
}
function updateUndoRedo() {
document.getElementById("undo").disabled = historyStep <= 0;
document.getElementById("redo").disabled = historyStep >= history.length - 1;
}
document.getElementById("undo").onclick = () => {
if (historyStep > 0) {
historyStep--;
const imgData = new Image();
imgData.src = history[historyStep];
imgData.onload = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imgData, 0, 0, imgData.width, imgData.height);
};
}
updateUndoRedo();
};
document.getElementById("redo").onclick = () => {
if (historyStep < history.length - 1) {
historyStep++;
const imgData = new Image();
imgData.src = history[historyStep];
imgData.onload = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imgData, 0, 0, imgData.width, imgData.height);
};
}
updateUndoRedo();
};
document.getElementById("save").onclick = () => {
const dataURL = canvas.toDataURL("image/png");
document.getElementById("save").innerText = "Saving...";
document.getElementById("save").disabled = true;
fetch('https://api.glideapp.io/api/function/mutateTables', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer [YOUR_API_KEY]' // Replace this with your API key
},
body: JSON.stringify({
appID: "[YOUR_APP_ID]", // Replace this with your Glide app ID
mutations: [
{
kind: "set-columns-in-row",
tableName: "[YOUR_TABLE_NAME]", // Replace this with the table name in your Glide app
columnValues: {
"[COLUMN_NAME]": dataURL // Replace [COLUMN_NAME] with the name of the column where you want to store the image URL
},
rowID: "${rowID}"
}
]
})
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
document.getElementById("save").innerText = "Save";
document.getElementById("save").disabled = false;
})
.catch((error) => {
console.error('Error:', error);
alert("Error saving image. Please try again.");
document.getElementById("save").innerText = "Save";
document.getElementById("save").disabled = false;
});
};
document.getElementById("download").onclick = () => {
const link = document.createElement("a");
link.href = canvas.toDataURL();
link.download = "image.png";
link.click();
};
</script>
</body>
</html>
`;
return `data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`;
}
const imageURI = generateAnnotatedImageURI(p1, p2);
p1 = imageURI;
return imageURI;
As an additional note, to make your web-embed component accept background transparency, use the following CSS with the class name color-scheme
.
.color-scheme {
color-scheme: auto;
}