You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
specimenName = "Unknown Creature",
locationFound = "Undisclosed Location",
dateObserved = "Date Unknown",
classification = "Cryptid Exemplar",
observerName = "Dr. E. Blackwood",
notes = "Further study required. Handle with extreme caution. Sightings are rare and often disputed, contributing to its mythical status. The specimen exhibits unusual biological markers not consistent with known terrestrial fauna.",
cardTitle = "CRYPTOZOOLOGICAL SPECIMEN RECORD",
specimenId = "", // If empty, will be auto-generated
stampText = "CLASSIFIED"
) {
const FONT_FAMILY_NAME = 'Special Elite';
const FONT_URL = 'https://fonts.gstatic.com/s/specialelite/v19/XLYgIZbkc4JPVQnNIAb0bHdSFM3s_3so_Pef.woff2';
const FONT_STRING_WITH_FALLBACK = `'${FONT_FAMILY_NAME}', 'Courier New', 'Courier', monospace`;
async function ensureFontIsLoaded(fontFamilyName, fontUrl) {
if (document.fonts && document.fonts.check && document.fonts.check(`1em ${fontFamilyName}`)) {
return true;
}
if (typeof FontFace === 'function') {
const fontFace = new FontFace(fontFamilyName, `url(${fontUrl}) format('woff2')`);
try {
await fontFace.load();
document.fonts.add(fontFace);
await new Promise(resolve => setTimeout(resolve, 50)); // Brief pause
return document.fonts.check(`1em ${fontFamilyName}`);
} catch (e) {
console.error(`FontFace API: Font ${fontFamilyName} failed to load:`, e);
}
}
// Fallback for older systems or if FontFace API fails: inject @font-face
const styleId = `font-style-${fontFamilyName.replace(/\s+/g, '-')}`;
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `@font-face { font-family: '${fontFamilyName}'; src: url('${fontUrl}') format('woff2'); font-display: swap; }`;
document.head.appendChild(style);
await new Promise(resolve => setTimeout(resolve, 200)); // Wait longer
if (document.fonts && document.fonts.check && document.fonts.check(`1em ${fontFamilyName}`)) return true;
}
console.warn(`Font ${fontFamilyName} may not be loaded. Using fallback.`);
return false;
}
const fontLoaded = await ensureFontIsLoaded(FONT_FAMILY_NAME, FONT_URL);
const currentFontFamily = fontLoaded ? FONT_STRING_WITH_FALLBACK : `'Courier New', 'Courier', monospace`;
const canvas = document.createElement('canvas');
const canvasWidth = 800;
const canvasHeight = 600;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
const bgColor = '#f5e8d0';
const textColor = '#3D2B1F';
const labelColor = '#4a3b32';
const borderColor = '#5A3A22';
const stampColor = '#A03030';
const padding = 30;
const baseLineHeight = 22;
const detailFontSize = "15px";
const labelFontSize = "bold 15px";
const titleFontSize = "bold 26px";
const notesTitleFontSize = "bold 16px"; // Adjusted size for notes title
function drawWrappedText(context, text, x, y, maxWidth, lineHeight, fontStyle) {
context.font = fontStyle;
const words = String(text).split(' ');
let lineContent = '';
let currentLineY = y;
let linesOutput = 0;
if (String(text).trim() === '') return y;
for (let i = 0; i < words.length; i++) {
const testLine = lineContent + words[i] + ' ';
if (context.measureText(testLine).width > maxWidth && i > 0 && lineContent !== '') {
context.fillText(lineContent.trim(), x, currentLineY);
lineContent = words[i] + ' ';
currentLineY += lineHeight;
linesOutput++;
} else {
lineContent = testLine;
}
}
if (lineContent.trim() !== '') {
context.fillText(lineContent.trim(), x, currentLineY);
linesOutput++;
}
return y + (linesOutput * lineHeight);
}
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.strokeStyle = borderColor;
ctx.lineWidth = 3; ctx.strokeRect(5, 5, canvasWidth - 10, canvasHeight - 10);
ctx.lineWidth = 1; ctx.strokeRect(15, 15, canvasWidth - 30, canvasHeight - 30);
let currentY = padding + 20;
ctx.textAlign = 'center';
ctx.font = `${titleFontSize} ${currentFontFamily}`;
ctx.fillStyle = textColor;
ctx.fillText(cardTitle.toUpperCase(), canvasWidth / 2, currentY);
currentY += 35;
const finalSpecimenId = specimenId || `CSX-${String(Math.floor(Math.random() * 900000) + 100000).padStart(6, '0')}`;
ctx.textAlign = 'right';
ctx.font = `${detailFontSize} ${currentFontFamily}`;
ctx.fillStyle = labelColor;
ctx.fillText(`ID: ${finalSpecimenId}`, canvasWidth - padding - 10, currentY);
currentY += 25;
ctx.beginPath();
ctx.moveTo(padding + 10, currentY); ctx.lineTo(canvasWidth - padding - 10, currentY);
ctx.lineWidth = 1.5; ctx.strokeStyle = borderColor; ctx.stroke();
currentY += 25;
const mainContentStartY = currentY;
const imageSectionX = padding + 10;
const imageSectionMaxWidth = canvasWidth * 0.42;
const imageSectionMaxHeight = canvasHeight * 0.48;
let imageSectionActualHeight = 0;
if (originalImg && originalImg.width > 0 && originalImg.height > 0 && originalImg.complete) {
const ar = originalImg.width / originalImg.height;
let dw = imageSectionMaxWidth, dh = dw / ar;
if (dh > imageSectionMaxHeight) { dh = imageSectionMaxHeight; dw = dh * ar; }
if (dw > imageSectionMaxWidth) { dw = imageSectionMaxWidth; dh = dw / ar; }
const ix = imageSectionX + (imageSectionMaxWidth - dw) / 2, iy = mainContentStartY;
ctx.drawImage(originalImg, ix, iy, dw, dh);
imageSectionActualHeight = dh;
ctx.strokeStyle = labelColor; ctx.lineWidth = 2;
ctx.strokeRect(ix - 2, iy - 2, dw + 4, dh + 4);
} else {
const phX = imageSectionX, phY = mainContentStartY, phW = imageSectionMaxWidth, phH = imageSectionMaxHeight;
ctx.strokeStyle = labelColor; ctx.lineWidth = 1;
ctx.strokeRect(phX, phY, phW, phH);
ctx.textAlign = 'center';
drawWrappedText(ctx, "[Image Unavailable]", phX + phW / 2, phY + phH / 2 - baseLineHeight / 2, phW - 10, baseLineHeight, `${detailFontSize} ${currentFontFamily}`);
imageSectionActualHeight = phH;
}
const imageSectionEndY = mainContentStartY + imageSectionActualHeight;
const textFieldsX = imageSectionX + imageSectionMaxWidth + 20; // Reduced gap
const textFieldsWidth = canvasWidth - textFieldsX - padding - 10;
let currentTextY = mainContentStartY;
const fieldsData = [
{ label: "SPECIMEN NAME:", value: specimenName }, { label: "CLASSIFICATION:", value: classification },
{ label: "LOCATION:", value: locationFound }, { label: "DATE OBSERVED:", value: dateObserved },
{ label: "FILED BY:", value: observerName }
];
const fixedLabelWidth = 130;
const fieldItemSpacing = baseLineHeight * 0.5;
for (const field of fieldsData) {
ctx.fillStyle = labelColor; ctx.textAlign = 'left';
const labelEndY = drawWrappedText(ctx, field.label, textFieldsX, currentTextY, fixedLabelWidth - 5, baseLineHeight, `${labelFontSize} ${currentFontFamily}`);
ctx.fillStyle = textColor;
const valueX = textFieldsX + fixedLabelWidth;
const valueMaxWidth = textFieldsWidth - fixedLabelWidth;
const valueEndY = drawWrappedText(ctx, field.value, valueX, currentTextY, valueMaxWidth, baseLineHeight, `${detailFontSize} ${currentFontFamily}`);
currentTextY = Math.max(labelEndY, valueEndY) + fieldItemSpacing;
}
const textFieldsEndY = currentTextY - fieldItemSpacing;
currentY = Math.max(imageSectionEndY, textFieldsEndY) + 15; // Reduced spacing
const notesSectionMaxY = canvasHeight - padding - (stampText && stampText.trim() !== "" ? 45 : 10); // Max Y before stamp/bottom padding
if (currentY > notesSectionMaxY - 60) { // Ensure minimum space for notes title and a bit of text
currentY = Math.max(imageSectionEndY, textFieldsEndY) + 10; // Minimal spacing if already too cramped
if (currentY > notesSectionMaxY - 60) currentY = notesSectionMaxY - 60; // Absolute cap
}
ctx.beginPath();
ctx.moveTo(padding + 10, currentY); ctx.lineTo(canvasWidth - padding - 10, currentY);
ctx.lineWidth = 1.5; ctx.strokeStyle = borderColor; ctx.stroke();
currentY += 20;
ctx.textAlign = 'left'; ctx.fillStyle = labelColor;
const notesTitleEndY = drawWrappedText(ctx, "ADDITIONAL NOTES:", padding + 10, currentY, textFieldsWidth, baseLineHeight, `${notesTitleFontSize} ${currentFontFamily}`);
currentY = notesTitleEndY + 5; // Space after notes title
ctx.fillStyle = textColor;
const notesX = padding + 10;
const notesMaxWidth = canvasWidth - 2 * (padding + 10);
// Draw notes, but respect the notesSectionMaxY
// This simple drawWrappedText doesn't truncate; it will draw all lines.
// For trucation, drawWrappedText would need to accept a max_height or max_lines.
let finalNotesY = drawWrappedText(ctx, notes, notesX, currentY, notesMaxWidth, baseLineHeight, `${detailFontSize} ${currentFontFamily}`);
if (finalNotesY > notesSectionMaxY) {
// Basic handling: if notes overflow, they will be clipped by the canvas bottom or stamp.
// To implement "..." if truncated, drawWrappedText would need modification.
}
if (stampText && stampText.trim() !== "") {
const stampW = 110, stampH = 30;
const stampPosX = canvasWidth - padding - stampW - 10;
const stampPosY = canvasHeight - padding - stampH - 5;
ctx.save();
ctx.translate(stampPosX + stampW / 2, stampPosY + stampH / 2);
ctx.rotate(-10 * Math.PI / 180);
ctx.strokeStyle = stampColor; ctx.lineWidth = 2;
ctx.strokeRect(-stampW / 2, -stampH / 2, stampW, stampH);
ctx.font = `bold 16px ${currentFontFamily}`;
ctx.fillStyle = stampColor; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(stampText.toUpperCase(), 0, 1); // Small Y offset for better visual centering
ctx.restore();
}
return canvas;
}
Apply Changes