You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
frameBaseColor = '#654321', // Default: SaddleBrown-like color for the main frame
parchmentBgColor = '#F5EACE', // Default: Light Parchment/Aged paper color
compassMainColor = '#5C4033', // Default: Dark Brown for E,S,W compass points
compassNorthColor = '#B22222', // Default: Firebrick Red for North compass point
compassPosition = 'top-right', // Options: "top-left", "top-right", "bottom-left", "bottom-right"
frameScaleFactor = 1.0 // Adjusts frame thickness, e.g., 0.5 (thinner) to 2.0 (thicker)
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// --- Validate & Sanitize Parameters ---
const p_frameScaleFactor = Math.max(0.25, Math.min(3.0, Number(frameScaleFactor) || 1.0));
// Basic color validation helper (accepts hex, common names, rgb/rgba)
const isValidColor = (colorStr) => typeof colorStr === 'string' && (
/^#([0-9A-Fa-f]{3,4}){1,2}$/.test(colorStr) || // hex, hexa
/^[a-zA-Z]+$/.test(colorStr) || // named colors
/^rgb\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*\)$/.test(colorStr) || // rgb
/^rgba\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*,\s*(\d?.?\d+)\s*\)$/.test(colorStr) // rgba
);
const p_frameBaseColor = isValidColor(frameBaseColor) ? frameBaseColor : '#654321';
const p_parchmentBgColor = isValidColor(parchmentBgColor) ? parchmentBgColor : '#F5EACE';
const p_compassMainColor = isValidColor(compassMainColor) ? compassMainColor : '#5C4033';
const p_compassNorthColor = isValidColor(compassNorthColor) ? compassNorthColor : '#B22222';
const p_compassPosition = ['top-left', 'top-right', 'bottom-left', 'bottom-right'].includes(String(compassPosition).toLowerCase()) ? String(compassPosition).toLowerCase() : 'top-right';
// --- Constants derived from parameters or fixed ---
const baseFrameThicknessRatio = 0.1 * p_frameScaleFactor;
const minFrameThickness = 25 * p_frameScaleFactor; // Adjusted min based on scale
const maxFrameThickness = 150 * p_frameScaleFactor; // Adjusted max based on scale
const frameInnerDetailColor = '#B08D57'; // Fixed: Old gold/brass detail for thematic consistency
const compassLabelColor = '#2F1E1A'; // Fixed: Darker brown for compass labels
// --- Calculate dimensions ---
let frameThickness = Math.max(minFrameThickness,
Math.min(maxFrameThickness,
Math.min(originalImg.width, originalImg.height) * baseFrameThicknessRatio));
// Ensure frame thickness isn't excessively large compared to the image itself
frameThickness = Math.min(frameThickness, originalImg.width / 2.5, originalImg.height / 2.5, 200); // Cap at 200 too
canvas.width = originalImg.width + 2 * frameThickness;
canvas.height = originalImg.height + 2 * frameThickness;
// --- 1. Draw Parchment Background for the entire canvas ---
ctx.fillStyle = p_parchmentBgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Add subtle texture to parchment background
ctx.save();
const speckleAreaFactor = Math.min(canvas.width * canvas.height, 1000*800); // Cap area for speckle density
const numSpeckles = Math.floor(speckleAreaFactor / 200); // Adjust density
for (let i = 0; i < numSpeckles; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const radius = Math.random() * 1.3 + 0.2;
const alpha = Math.random() * 0.10 + 0.02; // Very faint speckles
// Using faint black speckles for general applicability over any parchment color
ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
// --- 2. Draw the Frame ---
// This frame is in the frameThickness area, around the image.
ctx.fillStyle = p_frameBaseColor;
ctx.fillRect(0, 0, canvas.width, frameThickness); // Top bar
ctx.fillRect(0, canvas.height - frameThickness, canvas.width, frameThickness); // Bottom bar
ctx.fillRect(0, frameThickness, frameThickness, canvas.height - 2 * frameThickness); // Left bar
ctx.fillRect(canvas.width - frameThickness, frameThickness, frameThickness, canvas.height - 2 * frameThickness); // Right bar
// Inner decorative line(s) for a bit of richness
const detailLineWidth = Math.max(1, Math.min(6, frameThickness * 0.04));
const detailInset1 = frameThickness * 0.2; // Inset from the outer edge of canvas
ctx.strokeStyle = frameInnerDetailColor;
ctx.lineWidth = detailLineWidth;
ctx.strokeRect(detailInset1, detailInset1,
canvas.width - 2 * detailInset1, canvas.height - 2 * detailInset1);
// Optional second inner line, closer to the image edge.
const detailInset2 = frameThickness - (detailLineWidth * 1.8); // Closer to image edge
if (detailInset2 > detailInset1 + detailLineWidth * 2.5) { // Ensure lines are distinct
ctx.lineWidth = Math.max(1, detailLineWidth * 0.65); // Thinner line
ctx.strokeRect(detailInset2, detailInset2,
canvas.width - 2 * detailInset2, canvas.height - 2 * detailInset2);
}
// --- 3. Draw the Original Image ---
ctx.drawImage(originalImg, frameThickness, frameThickness, originalImg.width, originalImg.height);
// --- 4. Draw the Compass Rose ---
// Size calculation for compass rose
const compassRoseOuterAllowRadius = frameThickness * 0.42; // Max space it could occupy
const compassMarginFromFrameEdge = frameThickness * 0.08; // Small margin
// Actual radius for the main arms of the compass
const finalCompassRadius = Math.max(12, // Min sensible radius
Math.min(compassRoseOuterAllowRadius, (frameThickness / 2) - compassMarginFromFrameEdge));
let compassCenterX, compassCenterY;
// Center the compass within the width/height of the frame border
const frameBorderCenterOffset = frameThickness / 2;
switch (p_compassPosition) {
case 'top-left':
compassCenterX = frameBorderCenterOffset;
compassCenterY = frameBorderCenterOffset;
break;
case 'bottom-left':
compassCenterX = frameBorderCenterOffset;
compassCenterY = canvas.height - frameBorderCenterOffset;
break;
case 'bottom-right':
compassCenterX = canvas.width - frameBorderCenterOffset;
compassCenterY = canvas.height - frameBorderCenterOffset;
break;
case 'top-right': // Default
default:
compassCenterX = canvas.width - frameBorderCenterOffset;
compassCenterY = frameBorderCenterOffset;
break;
}
if (frameThickness >= 25 && finalCompassRadius >= 12) { // Only draw if frame is somewhat thick and radius is sensible
_drawOldMapCompassRose(ctx,
compassCenterX, compassCenterY, finalCompassRadius,
p_compassMainColor,
p_compassNorthColor,
compassLabelColor
);
}
return canvas;
}
// Helper function to draw the compass rose (not directly part of the public API)
function _drawOldMapCompassRose(ctx, cx, cy, radius, primaryColor, northColor, labelColor) {
const points = {
N: { angle: -90, label: 'N', color: northColor, main: true },
NE: { angle: -45, label: '', color: primaryColor, main: false },
E: { angle: 0, label: 'E', color: primaryColor, main: true },
SE: { angle: 45, label: '', color: primaryColor, main: false },
S: { angle: 90, label: 'S', color: primaryColor, main: true },
SW: { angle: 135, label: '', color: primaryColor, main: false },
W: { angle: 180, label: 'W', color: primaryColor, main: true },
NW: { angle: 225, label: '', color: primaryColor, main: false }
};
const mainArmLength = radius;
const interArmLength = radius * 0.60; // Intercardinal arms are shorter
const armBaseWidthRatio = 0.22; // Relative width of the triangle base
const armInnerFactorMain = 0.15; // How close main arm bases are to center
const armInnerFactorInter = 0.25; // How close inter arm bases are to center
ctx.save();
ctx.translate(cx, cy);
// Draw arms
for (const key in points) {
const p = points[key];
const angleRad = p.angle * Math.PI / 180;
const armLength = p.main ? mainArmLength : interArmLength;
const baseHalfWidth = armLength * armBaseWidthRatio;
const innerFactor = p.main ? armInnerFactorMain : armInnerFactorInter;
if (armLength < 1) continue; // Skip if arm is too small
ctx.save();
ctx.rotate(angleRad); // Rotate grid for easier drawing of this arm
const innerX = innerFactor * armLength; // X-coord of base points along arm's axis
ctx.beginPath();
ctx.moveTo(armLength, 0); // Tip of the arm
ctx.lineTo(innerX, baseHalfWidth); // Base point 1
ctx.lineTo(innerX, -baseHalfWidth); // Base point 2
ctx.closePath();
ctx.fillStyle = p.color;
ctx.fill();
// Optional subtle outline for arms for better definition on similar backgrounds
// ctx.strokeStyle = 'rgba(0,0,0,0.2)';
// ctx.lineWidth = 0.5;
// ctx.stroke();
ctx.restore(); // Restore rotation context
}
// Decorative circles common in old compass roses
ctx.strokeStyle = primaryColor; // Use primary color for circles for consistency
ctx.lineWidth = Math.max(0.5, radius * 0.025); // Thin lines
if (radius > 10) { // Outer guiding circle
ctx.beginPath();
ctx.arc(0, 0, mainArmLength * 0.92, 0, Math.PI * 2);
ctx.stroke();
}
if (radius > 20) { // Inner guiding circle
const innerCircleRadius = mainArmLength * Math.max(armInnerFactorMain, armInnerFactorInter) * 1.2;
ctx.beginPath();
ctx.arc(0, 0, innerCircleRadius , 0, Math.PI * 2);
ctx.stroke();
}
// Center dot
ctx.beginPath();
ctx.arc(0, 0, Math.max(1, radius * 0.09), 0, Math.PI * 2);
ctx.fillStyle = northColor; // Typically matches North or a contrast jewel color
ctx.fill();
if (radius > 8) { // Small border for center dot if large enough
ctx.strokeStyle = 'rgba(0,0,0,0.4)';
ctx.lineWidth = Math.max(0.5, radius * 0.015);
ctx.stroke();
}
// Labels (N, E, S, W)
const fontSize = Math.max(6, Math.min(16, Math.floor(radius * 0.26)));
if (fontSize >= 6 && radius > 15) { // Check for minimum radius and font size for legibility
ctx.fillStyle = labelColor;
ctx.font = `bold ${fontSize}px serif`; // Serif font often used in old maps
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Position labels slightly outside tips of main arms or adjust based on context
const labelRadius = mainArmLength * 1.05 + fontSize * 0.3;
for (const key in points) {
const p = points[key];
if (p.label && p.main) { // Only for main points that have a label defined
const angleRad = p.angle * Math.PI / 180;
let lx = Math.cos(angleRad) * labelRadius;
let ly = Math.sin(angleRad) * labelRadius;
// Fine-tuning label positions for better aesthetic balance
if (p.label === 'N') ly -= fontSize * 0.05;
if (p.label === 'S') ly += fontSize * 0.1;
if (p.label === 'E') lx += fontSize * 0.05;
if (p.label === 'W') lx -= fontSize * 0.05;
ctx.fillText(p.label, lx, ly);
}
}
}
ctx.restore(); // Restore from translate(cx, cy)
}
Apply Changes