You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, frameSizeRatio = 0.15, primaryMetalColor = "#8C7853", secondaryMetalColor = "#654321", rivetColor = "#707070", gearDensity = 2) {
// --- Helper Functions ---
function _hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 }; // Default to black if parse fails
}
function _rgbToHex(r, g, b) {
r = Math.max(0, Math.min(255, Math.round(r)));
g = Math.max(0, Math.min(255, Math.round(g)));
b = Math.max(0, Math.min(255, Math.round(b)));
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}
function _adjustColor(hex, percent) {
const { r, g, b } = _hexToRgb(hex);
const amount = 255 * (percent / 100); // No Math.round here, allow float for precision with r,g,b
const newR = r + amount;
const newG = g + amount;
const newB = b + amount;
return _rgbToHex(newR, newG, newB); // _rgbToHex will clamp and round
}
function _lighten(hex, percent) { return _adjustColor(hex, percent); }
function _darken(hex, percent) { return _adjustColor(hex, -percent); }
function _drawRivet(ctx, cx, cy, radius, baseColor, highlightOffset = 0.3) {
if (radius <= 0.5) return; // Do not draw tiny rivets
const hr = radius * highlightOffset; // Highlight offset factor
const grad = ctx.createRadialGradient(cx - hr, cy - hr, Math.max(0.1, radius * 0.1), cx, cy, radius);
grad.addColorStop(0, _lighten(baseColor, 40));
grad.addColorStop(0.5, baseColor);
grad.addColorStop(1, _darken(baseColor, 30));
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = _darken(baseColor, 50);
ctx.lineWidth = Math.max(0.5, radius * 0.1);
ctx.stroke();
}
function _drawGear(ctx, cx, cy, outerRadius, numTeeth, toothHeightRatio, gearPrimaryColor) {
if (outerRadius <= 2) return; // Do not draw tiny gears
const innerRadius = outerRadius * (1 - toothHeightRatio);
if (innerRadius <= 0.5) return;
ctx.save();
ctx.translate(cx, cy);
ctx.beginPath();
const angleIncrement = Math.PI / numTeeth;
for (let i = 0; i < numTeeth * 2; i++) {
const angle = i * angleIncrement;
const r = (i % 2 === 0) ? outerRadius : innerRadius;
if (i === 0) {
ctx.moveTo(r * Math.cos(angle), r * Math.sin(angle));
} else {
ctx.lineTo(r * Math.cos(angle), r * Math.sin(angle));
}
}
ctx.closePath();
// Main gear body fill gradient
const grad = ctx.createRadialGradient(-outerRadius * 0.3, -outerRadius * 0.3, outerRadius * 0.1, 0, 0, outerRadius);
grad.addColorStop(0, _lighten(gearPrimaryColor, 20));
grad.addColorStop(0.5, gearPrimaryColor);
grad.addColorStop(1, _darken(gearPrimaryColor, 20));
ctx.fillStyle = grad;
ctx.fill();
// Gear stroke
ctx.strokeStyle = _darken(gearPrimaryColor, 40);
ctx.lineWidth = Math.max(1, outerRadius * 0.03);
ctx.stroke();
// Central hole/decoration
const holeRadius = Math.max(1, innerRadius * 0.5);
if (holeRadius > 1) { // Only draw if reasonably sized
ctx.beginPath();
ctx.arc(0, 0, holeRadius, 0, Math.PI * 2, false);
const holeGrad = ctx.createRadialGradient(holeRadius * 0.2, holeRadius * 0.2, 0, 0, 0, holeRadius);
holeGrad.addColorStop(0, _darken(gearPrimaryColor, 10)); // Inner highlight of hole edge
holeGrad.addColorStop(1, _darken(gearPrimaryColor, 60)); // Darker hole center
ctx.fillStyle = holeGrad;
ctx.fill();
ctx.strokeStyle = _darken(gearPrimaryColor, 30);
ctx.lineWidth = Math.max(0.5, outerRadius * 0.02);
ctx.stroke();
}
ctx.restore();
}
// --- Main Processing ---
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
if (imgWidth === 0 || imgHeight === 0) { // Handle case of invalid/unloaded image
canvas.width = 200; canvas.height = 150;
ctx.fillStyle = "#CCCCCC";
ctx.fillRect(0,0, canvas.width, canvas.height);
ctx.font = "16px Arial";
ctx.fillStyle = "#000000";
ctx.textAlign = "center";
ctx.fillText("Invalid Image Data", canvas.width/2, canvas.height/2);
return canvas;
}
const baseBorderDim = Math.min(imgWidth, imgHeight);
let borderWidth = baseBorderDim * frameSizeRatio;
borderWidth = Math.max(20, borderWidth); // Ensure a minimum border width
canvas.width = imgWidth + 2 * borderWidth;
canvas.height = imgHeight + 2 * borderWidth;
// 1. Parchment Background
ctx.fillStyle = "#F5E8C8"; // Parchment color
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.globalAlpha = 0.6; // Make noise and splotches subtle
// Noise
for (let i = 0; i < (canvas.width * canvas.height) / 150; i++) { // Adjusted density
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const radius = Math.random() * 1.5 + 0.5;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 0, 0, ${0.04 + Math.random() * 0.06})`;
ctx.fill();
}
// Splotches
for (let i = 0; i < 15; i++) { // Adjusted number of splotches
const splotchRadius = Math.random() * (Math.min(canvas.width, canvas.height) * 0.12) + (Math.min(canvas.width, canvas.height) * 0.04);
const sx = Math.random() * (canvas.width - splotchRadius * 2) + splotchRadius;
const sy = Math.random() * (canvas.height - splotchRadius * 2) + splotchRadius;
const grad = ctx.createRadialGradient(sx, sy, 0, sx, sy, splotchRadius);
const splotchBaseOpacity = 0.02 + Math.random() * 0.04;
grad.addColorStop(0, `rgba(160, 130, 90, ${splotchBaseOpacity})`); // Brownish tan
grad.addColorStop(0.7, `rgba(160, 130, 90, ${splotchBaseOpacity * 0.5})`);
grad.addColorStop(1, `rgba(160, 130, 90, 0)`);
ctx.fillStyle = grad;
ctx.fillRect(sx - splotchRadius, sy - splotchRadius, splotchRadius * 2, splotchRadius * 2);
}
ctx.restore();
// 2. Main Metallic Frame Panels
const frameSections = [
{ x: 0, y: 0, w: canvas.width, h: borderWidth }, // Top
{ x: 0, y: canvas.height - borderWidth, w: canvas.width, h: borderWidth }, // Bottom
{ x: 0, y: borderWidth, w: borderWidth, h: imgHeight }, // Left
{ x: canvas.width - borderWidth, y: borderWidth, w: borderWidth, h: imgHeight } // Right
];
frameSections.forEach((panel, index) => {
let grad;
// Consistent light source from top-left
if (panel.h < panel.w) { // Top or Bottom panel (horizontal)
grad = ctx.createLinearGradient(panel.x, panel.y, panel.x, panel.y + panel.h); // Vertical gradient
if (index === 0) { // Top panel
grad.addColorStop(0, _lighten(primaryMetalColor, 20));
grad.addColorStop(0.5, primaryMetalColor);
grad.addColorStop(1, _darken(primaryMetalColor, 15));
} else { // Bottom panel
grad.addColorStop(0, _darken(primaryMetalColor, 15));
grad.addColorStop(0.5, primaryMetalColor);
grad.addColorStop(1, _lighten(primaryMetalColor, 20));
}
} else { // Left or Right panel (vertical)
grad = ctx.createLinearGradient(panel.x, panel.y, panel.x + panel.w, panel.y); // Horizontal gradient
if (index === 2) { // Left panel
grad.addColorStop(0, _lighten(primaryMetalColor, 20));
grad.addColorStop(0.5, primaryMetalColor);
grad.addColorStop(1, _darken(primaryMetalColor, 15));
} else { // Right panel
grad.addColorStop(0, _darken(primaryMetalColor, 15));
grad.addColorStop(0.5, primaryMetalColor);
grad.addColorStop(1, _lighten(primaryMetalColor, 20));
}
}
ctx.fillStyle = grad;
ctx.fillRect(panel.x, panel.y, panel.w, panel.h);
ctx.strokeStyle = _darken(primaryMetalColor, 35);
ctx.lineWidth = Math.max(0.5, borderWidth * 0.01);
ctx.strokeRect(panel.x, panel.y, panel.w, panel.h);
});
// Inner Bevel for frame opening (raised effect)
const bevelWidth = Math.max(1.5, borderWidth * 0.04);
ctx.strokeStyle = _lighten(primaryMetalColor, 30); // Highlight part
ctx.lineWidth = bevelWidth;
ctx.strokeRect(borderWidth - bevelWidth/2, borderWidth - bevelWidth/2, imgWidth + bevelWidth, imgHeight + bevelWidth);
ctx.strokeStyle = _darken(primaryMetalColor, 30); // Shadow part
ctx.lineWidth = bevelWidth;
ctx.strokeRect(borderWidth + bevelWidth/2, borderWidth + bevelWidth/2, imgWidth - bevelWidth, imgHeight - bevelWidth);
// 3. Draw the Original Image
ctx.drawImage(originalImg, borderWidth, borderWidth, imgWidth, imgHeight);
// 4. Add Rivets
const rivetRadius = Math.max(2.5, borderWidth * 0.05);
const effectiveRivetSpacing = Math.max(rivetRadius * 4, 25);
const rivetOffset = borderWidth * 0.5; // Center rivets in border thickness
// Top Edge
for (let x = rivetOffset; x <= canvas.width - rivetOffset; x += effectiveRivetSpacing) {
_drawRivet(ctx, x, rivetOffset, rivetRadius, rivetColor);
}
// Bottom Edge
for (let x = rivetOffset; x <= canvas.width - rivetOffset; x += effectiveRivetSpacing) {
_drawRivet(ctx, x, canvas.height - rivetOffset, rivetRadius, rivetColor);
}
// Left Edge (avoid double-drawing corners)
for (let y = rivetOffset + effectiveRivetSpacing; y <= canvas.height - rivetOffset - effectiveRivetSpacing; y += effectiveRivetSpacing) {
_drawRivet(ctx, rivetOffset, y, rivetRadius, rivetColor);
}
// Right Edge (avoid double-drawing corners)
for (let y = rivetOffset + effectiveRivetSpacing; y <= canvas.height - rivetOffset - effectiveRivetSpacing; y += effectiveRivetSpacing) {
_drawRivet(ctx, canvas.width - rivetOffset, y, rivetRadius, rivetColor);
}
// 5. Add Gears
if (gearDensity > 0) {
const maxGearRadius = borderWidth * 0.40;
const minGearRadius = Math.max(5, borderWidth * 0.18);
let gearCandidates = [];
// Define potential locations and properties for gears
if (gearDensity >= 1) { // Basic corner gears
gearCandidates.push(
{ cx: borderWidth * 0.5, cy: borderWidth * 0.5, sizeFactor: 1.0, metal: primaryMetalColor },
{ cx: canvas.width - borderWidth * 0.5, cy: borderWidth * 0.5, sizeFactor: 1.0, metal: secondaryMetalColor },
{ cx: borderWidth * 0.5, cy: canvas.height - borderWidth * 0.5, sizeFactor: 1.0, metal: secondaryMetalColor },
{ cx: canvas.width - borderWidth * 0.5, cy: canvas.height - borderWidth * 0.5, sizeFactor: 1.0, metal: primaryMetalColor }
);
}
if (gearDensity >= 2) { // Add some offset/smaller gears
gearCandidates.push(
{ cx: borderWidth * 1.1, cy: borderWidth * 0.45, sizeFactor: 0.65, metal: _lighten(primaryMetalColor,10)},
{ cx: canvas.width - borderWidth * 1.1, cy: canvas.height - borderWidth * 0.45, sizeFactor: 0.65, metal: _lighten(secondaryMetalColor,10)},
{ cx: borderWidth * 0.45, cy: canvas.height - borderWidth * 1.1, sizeFactor: 0.75, metal: _lighten(primaryMetalColor,5)},
{ cx: canvas.width - borderWidth * 0.45, cy: borderWidth * 1.1, sizeFactor: 0.75, metal: _lighten(secondaryMetalColor,5)}
);
}
if (gearDensity >= 3) { // Add mid-frame gears
gearCandidates.push(
{ cx: canvas.width * 0.5, cy: borderWidth * 0.4, sizeFactor: 0.85, metal: primaryMetalColor},
{ cx: canvas.width * 0.5, cy: canvas.height - borderWidth * 0.4, sizeFactor: 0.85, metal: secondaryMetalColor},
{ cx: borderWidth * 0.4, cy: canvas.height * 0.5, sizeFactor: 0.70, metal: primaryMetalColor},
{ cx: canvas.width - borderWidth * 0.4, cy: canvas.height * 0.5, sizeFactor: 0.70, metal: secondaryMetalColor}
);
}
// Determine actual gears to draw based on density, avoid over-cluttering
const gearsToDraw = [];
const desiredGearCount = Math.min(gearCandidates.length, Math.max(0, gearDensity * 2 + (gearDensity > 0 ? 2 : 0) )); // e.g., D0:0, D1:4, D2:6, D3:8
// Shuffle candidates to vary placement if desiredGearCount < gearCandidates.length
// For deterministic results, we can take the first `desiredGearCount` items.
for(let i = 0; i < desiredGearCount && i < gearCandidates.length; i++) {
gearsToDraw.push(gearCandidates[i]);
}
gearsToDraw.forEach((loc) => {
const radiusVariance = (0.8 + Math.random() * 0.4); // +/- 20% variance from base sizeFactor
const radius = minGearRadius + (maxGearRadius - minGearRadius) * loc.sizeFactor * radiusVariance;
const numTeeth = Math.floor(5 + Math.random() * 7); // 5 to 11 teeth
const toothHeightRatio = 0.18 + Math.random() * 0.12; // 0.18 to 0.3 tooth height
_drawGear(ctx, loc.cx, loc.cy, radius, numTeeth, toothHeightRatio, loc.metal);
});
}
return canvas;
}
Apply Changes