You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
journalDateStr = "AUTO_DATE",
journalLocation = "At Sea, Unknown Longitude & Latitude",
entryText = "A most peculiar day. The sea, usually a turbulent mistress, was calm as a millpond. Observed a school of luminous fish unlike any recorded in the charts. The men are uneasy, whispering tales of the deep. I maintain a stoic front, though a sense of wonder, and perhaps a little dread, touches even this old captain's heart. We mark our position and pray for fair winds tomorrow.",
captainName = "Captain [Your Name]",
fontNameParam = "IM Fell English SC",
fontSize = 18,
textColor = "#3A2A1D", // Dark Sepia
paperColor = "#F5F5DC", // Beige
imagePlacement = "top-left", // "none", "top-left", "top-right"
imageScale = 0.25,
canvasWidth = 800,
canvasHeight = 1000
) {
const fontName = `'${fontNameParam}', serif`; // Add generic fallback
// Helper function to get current date string
function getLocalCurrentDate() {
const d = new Date();
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
}
if (journalDateStr === "AUTO_DATE") {
journalDateStr = getLocalCurrentDate();
}
// Helper function to load font
async function loadFont(fontFamily, fontUrl) {
const styleId = `font-style-${fontFamily.replace(/\s+/g, '-')}`;
if (document.getElementById(styleId)) {
try {
await document.fonts.load(`1em "${fontFamily}"`); // Ensure quotes for font names with spaces
return;
} catch (e) {
console.warn(`Re-checking loaded font ${fontFamily} failed, will try to load via CSS. Error:`, e);
}
}
if (document.fonts && typeof document.fonts.check === 'function' && document.fonts.check(`1em "${fontFamily}"`)) {
return; // Font already available
}
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.id = styleId;
link.rel = 'stylesheet';
link.href = fontUrl;
link.onload = () => {
document.fonts.load(`1em "${fontFamily}"`).then(resolve).catch(e => {
console.warn(`Failed to load ${fontFamily} via document.fonts.load after CSS link. Error:`, e);
// Fallback to a timeout if document.fonts.load is problematic for some reason
setTimeout(resolve, 500); // Wait a bit for browser to apply CSS
});
};
link.onerror = (err) => {
console.error(`Failed to load font CSS from ${fontUrl}`, err);
reject(err);
};
document.head.appendChild(link);
});
}
// Helper function for text wrapping - returns one line and remaining text
function getNextLine(context, text, maxWidth, currentFont) {
context.font = currentFont; // Ensure context has the correct font for measurement
const words = text.split(' ');
let currentLine = words[0] || "";
let remainingText = words.slice(1).join(" ");
if (!words[0]) return { line: "", remainingText: "" };
// Check if the first word itself is too long
if (context.measureText(currentLine).width > maxWidth) {
// Handle very long first word: break it by character (simple approach)
let fittedChars = "";
for (let char of currentLine) {
if (context.measureText(fittedChars + char).width > maxWidth) {
break;
}
fittedChars += char;
}
if (fittedChars === "") { // Cannot even fit one character
return { line: currentLine[0] || "", remainingText: currentLine.substring(1) + (remainingText ? " " + remainingText : "") }; // return first char
}
remainingText = currentLine.substring(fittedChars.length) + (remainingText ? " " + remainingText : "");
currentLine = fittedChars;
return { line: currentLine, remainingText: remainingText.trim() };
}
for (let i = 1; i < words.length; i++) {
const word = words[i];
const testLine = currentLine + " " + word;
if (context.measureText(testLine).width <= maxWidth) {
currentLine = testLine;
remainingText = words.slice(i + 1).join(" ");
} else {
break;
}
}
return { line: currentLine, remainingText: remainingText.trim() };
}
// Helper for paper texture (subtle noise)
function drawPaperTexture(ctx, width, height) {
const intensity = 25;
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
const imageData = tempCtx.createImageData(width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const rand = (Math.random() - 0.5) * intensity;
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = Math.max(0, Math.min(255, 10 + rand));
}
tempCtx.putImageData(imageData, 0, 0);
ctx.save();
ctx.globalCompositeOperation = 'multiply';
ctx.globalAlpha = 0.15;
ctx.drawImage(tempCanvas, 0, 0);
ctx.restore();
}
// 1. Load custom font if specified
if (fontNameParam === "IM Fell English SC") {
try {
await loadFont("IM Fell English SC", "https://fonts.googleapis.com/css2?family=IM+Fell+English+SC&display=swap");
} catch (e) {
console.warn("IM Fell English SC font failed to load, fallback to serif will be used.", e);
}
}
// 2. Canvas Setup
const canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
// 3. Draw Background Paper
ctx.fillStyle = paperColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawPaperTexture(ctx, canvas.width, canvas.height);
// 4. Define layout constants
const padding = Math.max(20, Math.min(canvasWidth, canvasHeight) * 0.05); // Responsive padding
const lineHeight = fontSize * 1.6; // Increased for script font
const headerFontSize = fontSize * 0.9;
const signatureFontSize = fontSize * 1.1;
let currentY = padding + headerFontSize; // Start Y for header text
// 5. Draw Date and Location (Header)
ctx.fillStyle = textColor;
ctx.font = `${headerFontSize}px ${fontName}`;
ctx.textAlign = 'right';
ctx.fillText(journalDateStr, canvas.width - padding, currentY);
currentY += lineHeight * 0.9;
ctx.fillText(journalLocation, canvas.width - padding, currentY);
currentY += lineHeight * 1.5; // Extra space after header
// 6. Handle Original Image and Main Text Flow
ctx.textAlign = 'left'; // Reset alignment for main text
let textCursorY = currentY;
let remainingTextContent = entryText;
let imgActualX = 0, imgActualY = 0, imgActualWidth = 0, imgActualHeight = 0;
let hasSideImage = false;
const imageRendered = originalImg && originalImg.width > 0 && originalImg.height > 0 && imagePlacement !== "none" && imageScale > 0;
if (imageRendered) {
const scaledWidth = originalImg.width * imageScale;
const scaledHeight = originalImg.height * imageScale;
if (imagePlacement === "top-left") {
let potentialTextWidth = canvas.width - (padding + scaledWidth + padding / 2) - padding;
if (potentialTextWidth >= 100) {
imgActualX = padding;
imgActualY = textCursorY;
imgActualWidth = scaledWidth;
imgActualHeight = scaledHeight;
ctx.drawImage(originalImg, imgActualX, imgActualY, imgActualWidth, imgActualHeight);
hasSideImage = true;
} else {
ctx.drawImage(originalImg, (canvas.width - scaledWidth) / 2, textCursorY, scaledWidth, scaledHeight);
textCursorY += scaledHeight + lineHeight;
}
} else if (imagePlacement === "top-right") {
let potentialTextWidth = (canvas.width - padding - scaledWidth - padding / 2) - padding;
if (potentialTextWidth >= 100) {
imgActualX = canvas.width - padding - scaledWidth;
imgActualY = textCursorY;
imgActualWidth = scaledWidth;
imgActualHeight = scaledHeight;
ctx.drawImage(originalImg, imgActualX, imgActualY, imgActualWidth, imgActualHeight);
hasSideImage = true;
} else {
ctx.drawImage(originalImg, (canvas.width - scaledWidth) / 2, textCursorY, scaledWidth, scaledHeight);
textCursorY += scaledHeight + lineHeight;
}
}
}
const imageBottomY = hasSideImage ? (imgActualY + imgActualHeight) : textCursorY;
const mainTextFont = `${fontSize}px ${fontName}`;
ctx.font = mainTextFont; // Set font once for main text block initial setup
while (remainingTextContent.trim() !== "" && textCursorY < canvas.height - padding - lineHeight * 2) {
let currentLineMaxWidth;
let currentLineX;
if (hasSideImage && textCursorY < imageBottomY) {
if (imagePlacement === "top-left") {
currentLineX = imgActualX + imgActualWidth + padding / 2;
currentLineMaxWidth = canvas.width - currentLineX - padding;
} else { // top-right
currentLineX = padding;
currentLineMaxWidth = imgActualX - padding / 2 - padding; // Width available left of image
}
if (currentLineMaxWidth < 50) { // Not enough space beside image for meaningful text
textCursorY = imageBottomY + lineHeight * 0.2; // Move below image
currentLineX = padding;
currentLineMaxWidth = canvas.width - 2 * padding;
}
} else {
currentLineX = padding;
currentLineMaxWidth = canvas.width - 2 * padding;
if(hasSideImage && textCursorY >= imageBottomY && textCursorY < imageBottomY + lineHeight){
// Ensure text starts cleanly below the image
textCursorY = imageBottomY + lineHeight * 0.2;
}
}
if (currentLineMaxWidth <=0) { // No space to draw
console.warn("Calculated text width is zero or negative. Stopping text rendering.");
break;
}
const lineResult = getNextLine(ctx, remainingTextContent, currentLineMaxWidth, mainTextFont);
if (lineResult.line.trim() === "" && remainingTextContent.trim() !== "") {
console.warn("Cannot fit more text or stuck. Remaining:", remainingTextContent.substring(0,30));
break;
}
if (lineResult.line.trim() !== "") {
ctx.fillText(lineResult.line, currentLineX, textCursorY);
}
remainingTextContent = lineResult.remainingText;
textCursorY += lineHeight;
if (textCursorY > canvas.height - padding - (lineHeight * 3)) break; // Safety break near bottom before captain name
}
currentY = textCursorY; // Update overall Y cursor
// 8. Draw Captain's Signature
ctx.font = `italic ${signatureFontSize}px ${fontName}`;
ctx.textAlign = 'right';
// Ensure signature is on page, potentially adjusting currentY if too low
let signatureY = Math.min(canvas.height - padding, currentY + signatureFontSize);
if (signatureY < padding + signatureFontSize*2) signatureY = padding + signatureFontSize*2; // Ensure it is not on top
ctx.fillText(captainName, canvas.width - padding, signatureY);
return canvas;
}
Apply Changes