You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg,
patchShape = "circle",
patchWidth = 300,
patchHeight = 300,
borderColor = "gold",
borderWidth = 15,
backgroundColor = "navy",
topText = "",
bottomText = "",
textColor = "white",
fontFamily = "Impact, Arial Black, sans-serif",
fontSize = 24,
imageFit = "cover",
cornerRadius = 20) {
const canvas = document.createElement('canvas');
canvas.width = patchWidth;
canvas.height = patchHeight;
const ctx = canvas.getContext('2d');
// Helper function to draw rounded rectangle path
function drawRoundedRectPath(currentCtx, x, y, w, h, r) {
r = Math.max(0, r); // Ensure radius is not negative
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
currentCtx.beginPath();
currentCtx.moveTo(x + r, y);
currentCtx.lineTo(x + w - r, y);
currentCtx.arcTo(x + w, y, x + w, y + r, r);
currentCtx.lineTo(x + w, y + h - r);
currentCtx.arcTo(x + w, y + h, x + w - r, y + h, r);
currentCtx.lineTo(x + r, y + h);
currentCtx.arcTo(x, y + h, x, y + h - r, r);
currentCtx.lineTo(x, y + r);
currentCtx.arcTo(x, y, x + r, y, r);
currentCtx.closePath();
}
// Helper function for shield path (simplified heater shield)
function drawShieldPath(currentCtx, x, y, width, height, C_radius) {
const topPartFraction = 0.4; // How much of height is the "rectangular" top part
const topPartHeightAbs = height * topPartFraction;
// Ensure corner radius is not too large for the segments, and non-negative
const effectiveCornerRadius = Math.min(Math.max(0, C_radius), width / 2, topPartHeightAbs);
currentCtx.beginPath();
currentCtx.moveTo(x + effectiveCornerRadius, y);
currentCtx.lineTo(x + width - effectiveCornerRadius, y); // Top edge
currentCtx.arcTo(x + width, y, x + width, y + effectiveCornerRadius, effectiveCornerRadius); // Top-right corner
currentCtx.lineTo(x + width, y + topPartHeightAbs); // Right vertical edge (before tapering)
currentCtx.lineTo(x + width / 2, y + height); // Bottom-right slant to point
currentCtx.lineTo(x, y + topPartHeightAbs); // Bottom-left slant from point
currentCtx.lineTo(x, y + effectiveCornerRadius); // Left vertical edge
currentCtx.arcTo(x, y, x + effectiveCornerRadius, y, effectiveCornerRadius); // Top-left corner
currentCtx.closePath();
}
// --- Drawing STARTS ---
const cx = patchWidth / 2;
const cy = patchHeight / 2;
const bw = Math.max(0, borderWidth); // Ensure borderWidth is non-negative
// 1. Draw Border Color Layer (Outer Shape)
// This is only drawn if borderWidth > 0. If borderWidth is 0, this step is skipped,
// and the backgroundColor will fill the entire shape.
if (bw > 0) {
ctx.fillStyle = borderColor;
if (patchShape === "circle") {
const rOuter = Math.min(patchWidth, patchHeight) / 2;
ctx.beginPath();
ctx.arc(cx, cy, rOuter, 0, Math.PI * 2);
ctx.fill();
} else if (patchShape === "oval") {
ctx.beginPath();
ctx.ellipse(cx, cy, patchWidth / 2, patchHeight / 2, 0, 0, Math.PI * 2);
ctx.fill();
} else if (patchShape === "roundedRectangle") {
drawRoundedRectPath(ctx, 0, 0, patchWidth, patchHeight, cornerRadius);
ctx.fill();
} else if (patchShape === "shield") {
drawShieldPath(ctx, 0, 0, patchWidth, patchHeight, cornerRadius);
ctx.fill();
} else { // Default to rectangle
ctx.fillRect(0, 0, patchWidth, patchHeight);
}
}
// 2. Define Inner Area for Background/Image and Draw Background Color Layer
ctx.fillStyle = backgroundColor;
let imageArea = { x: bw, y: bw, width: patchWidth - 2 * bw, height: patchHeight - 2 * bw };
let shapeForClippingAndImage = { type: "", params: {} }; // To store path data for clipping
if (bw === 0) { // No border, background fills the whole shape, image uses full dimensions
imageArea = { x: 0, y: 0, width: patchWidth, height: patchHeight };
if (patchShape === "circle") {
const rFull = Math.min(patchWidth, patchHeight) / 2;
ctx.beginPath(); ctx.arc(cx, cy, rFull, 0, Math.PI * 2); ctx.fill();
shapeForClippingAndImage = { type: "circle", params: { cx, cy, r: rFull }};
} else if (patchShape === "oval") {
ctx.beginPath(); ctx.ellipse(cx, cy, patchWidth / 2, patchHeight / 2, 0, 0, Math.PI * 2); ctx.fill();
shapeForClippingAndImage = { type: "oval", params: { cx, cy, rx: patchWidth / 2, ry: patchHeight / 2 }};
} else if (patchShape === "roundedRectangle") {
drawRoundedRectPath(ctx, 0, 0, patchWidth, patchHeight, cornerRadius); ctx.fill();
shapeForClippingAndImage = { type: "roundedRectangle", params: { x:0, y:0, w:patchWidth, h:patchHeight, r:cornerRadius }};
} else if (patchShape === "shield") {
drawShieldPath(ctx, 0, 0, patchWidth, patchHeight, cornerRadius); ctx.fill();
shapeForClippingAndImage = { type: "shield", params: { x:0, y:0, w:patchWidth, h:patchHeight, r:cornerRadius }};
} else {
ctx.fillRect(0, 0, patchWidth, patchHeight);
shapeForClippingAndImage = { type: "rectangle", params: { x:0, y:0, w:patchWidth, h:patchHeight }};
}
} else { // Has border, background is for the inner area
if (patchShape === "circle") {
const rInner = Math.max(0, Math.min(patchWidth, patchHeight) / 2 - bw);
if (rInner > 0) { ctx.beginPath(); ctx.arc(cx, cy, rInner, 0, Math.PI * 2); ctx.fill(); }
shapeForClippingAndImage = { type: "circle", params: { cx, cy, r: rInner }};
} else if (patchShape === "oval") {
const rxInner = Math.max(0, patchWidth / 2 - bw);
const ryInner = Math.max(0, patchHeight / 2 - bw);
if (rxInner > 0 && ryInner > 0) { ctx.beginPath(); ctx.ellipse(cx, cy, rxInner, ryInner, 0, 0, Math.PI * 2); ctx.fill(); }
shapeForClippingAndImage = { type: "oval", params: { cx, cy, rx: rxInner, ry: ryInner }};
} else if (patchShape === "roundedRectangle") {
if (imageArea.width > 0 && imageArea.height > 0) {
const innerCR = Math.max(0, cornerRadius - bw);
drawRoundedRectPath(ctx, bw, bw, imageArea.width, imageArea.height, innerCR); ctx.fill();
shapeForClippingAndImage = { type: "roundedRectangle", params: { x:bw, y:bw, w:imageArea.width, h:imageArea.height, r:innerCR }};
} else { shapeForClippingAndImage = { type: "roundedRectangle", params: { x:bw, y:bw, w:0, h:0, r:0 }}; }
} else if (patchShape === "shield") {
if (imageArea.width > 0 && imageArea.height > 0) {
const innerCR = Math.max(0, cornerRadius - bw);
drawShieldPath(ctx, bw, bw, imageArea.width, imageArea.height, innerCR); ctx.fill();
shapeForClippingAndImage = { type: "shield", params: { x:bw, y:bw, w:imageArea.width, h:imageArea.height, r:innerCR }};
} else { shapeForClippingAndImage = { type: "shield", params: {x:bw, y:bw, w:0, h:0, r:0 }}; }
} else { // Rectangle (default)
if (imageArea.width > 0 && imageArea.height > 0) { ctx.fillRect(bw, bw, imageArea.width, imageArea.height); }
shapeForClippingAndImage = { type: "rectangle", params: { x:bw, y:bw, w:imageArea.width, h:imageArea.height }};
}
}
// 3. Clip to Inner Area (or full area if no border) and Draw Image
if (originalImg && originalImg.width > 0 && originalImg.height > 0) {
ctx.save();
// Redefine clipping path using shapeForClippingAndImage data
const p = shapeForClippingAndImage.params;
if (shapeForClippingAndImage.type === "circle" && p.r > 0) {
ctx.beginPath(); ctx.arc(p.cx, p.cy, p.r, 0, Math.PI * 2); ctx.clip();
} else if (shapeForClippingAndImage.type === "oval" && p.rx > 0 && p.ry > 0) {
ctx.beginPath(); ctx.ellipse(p.cx, p.cy, p.rx, p.ry, 0, 0, Math.PI * 2); ctx.clip();
} else if (shapeForClippingAndImage.type === "roundedRectangle" && p.w > 0 && p.h > 0) {
drawRoundedRectPath(ctx, p.x, p.y, p.w, p.h, p.r); ctx.clip();
} else if (shapeForClippingAndImage.type === "shield" && p.w > 0 && p.h > 0) {
drawShieldPath(ctx, p.x, p.y, p.w, p.h, p.r); ctx.clip();
} else if (shapeForClippingAndImage.type === "rectangle" && p.w > 0 && p.h > 0) {
ctx.beginPath(); ctx.rect(p.x, p.y, p.w, p.h); ctx.clip();
}
let targetBox;
if (shapeForClippingAndImage.type === "circle") {
targetBox = { x: p.cx - p.r, y: p.cy - p.r, width: 2 * p.r, height: 2 * p.r };
} else if (shapeForClippingAndImage.type === "oval") {
targetBox = { x: p.cx - p.rx, y: p.cy - p.ry, width: 2 * p.rx, height: 2 * p.ry };
} else { // rect, shield, roundedRectangle
targetBox = { x: p.x, y: p.y, width: p.w, height: p.h };
}
if (targetBox.width > 0 && targetBox.height > 0) {
const imgRatio = originalImg.width / originalImg.height;
const boxRatio = targetBox.width / targetBox.height;
let drawWidth, drawHeight, drawX, drawY;
if (imageFit === "cover") {
if (imgRatio > boxRatio) { drawHeight = targetBox.height; drawWidth = drawHeight * imgRatio; }
else { drawWidth = targetBox.width; drawHeight = drawWidth / imgRatio; }
} else { // "contain"
if (imgRatio > boxRatio) { drawWidth = targetBox.width; drawHeight = drawWidth / imgRatio; }
else { drawHeight = targetBox.height; drawWidth = drawHeight * imgRatio; }
}
drawX = targetBox.x + (targetBox.width - drawWidth) / 2;
drawY = targetBox.y + (targetBox.height - drawHeight) / 2;
ctx.drawImage(originalImg, drawX, drawY, drawWidth, drawHeight);
}
ctx.restore();
}
// 4. Draw Text
ctx.fillStyle = textColor;
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.textAlign = 'center';
function drawCurvedText(currentCtx, text, textCx, textCy, radius, startAngleDeg, sweepAngleDeg, isBottom) {
if (!text || text.length === 0 || radius <=0 ) return;
currentCtx.save();
currentCtx.translate(textCx, textCy);
const numChars = text.length;
const actualStartAngleRad = startAngleDeg * Math.PI / 180;
const actualSweepRad = sweepAngleDeg * Math.PI / 180;
let angleStepRad = 0;
if (numChars > 1) {
angleStepRad = actualSweepRad / (numChars - 1);
}
for (let i = 0; i < numChars; i++) {
currentCtx.save();
const char = text[i];
let currentAngleRad = actualStartAngleRad + (i * angleStepRad);
// For a single character, center it in the provided sweep (or at startAngle if sweep is 0)
if (numChars === 1) {
currentAngleRad = actualStartAngleRad + actualSweepRad / 2;
}
currentCtx.rotate(currentAngleRad);
if (isBottom) {
currentCtx.textBaseline = 'top';
currentCtx.fillText(char, 0, radius);
} else {
currentCtx.textBaseline = 'bottom';
currentCtx.fillText(char, 0, -radius);
}
currentCtx.restore();
}
currentCtx.restore();
}
const typicalTextHeight = fontSize;
if (patchShape === "circle" || patchShape === "oval") {
let textPlacementRadius;
if (patchShape === "circle") {
textPlacementRadius = Math.min(patchWidth, patchHeight) / 2 - bw - typicalTextHeight * 0.5 - (bw > 0 ? 2 : 0) ;
} else {
textPlacementRadius = Math.min(patchWidth/2, patchHeight/2) - bw - typicalTextHeight * 0.5 - (bw > 0 ? 2 : 0);
}
textPlacementRadius = Math.max(typicalTextHeight * 0.25, textPlacementRadius);
const maxSweepAngleDefault = 120; // degrees
if (topText && textPlacementRadius > 0) {
let charAngularWidth = (typicalTextHeight * 0.7 / textPlacementRadius) * (180/Math.PI); // Heuristic
let estimatedSweep = Math.min(maxSweepAngleDefault, topText.length * charAngularWidth);
if(topText.length ===1) estimatedSweep = 0;
drawCurvedText(ctx, topText, cx, cy, textPlacementRadius, -90 - estimatedSweep / 2, estimatedSweep, false);
}
if (bottomText && textPlacementRadius > 0) {
let charAngularWidth = (typicalTextHeight * 0.7 / textPlacementRadius) * (180/Math.PI);
let estimatedSweep = Math.min(maxSweepAngleDefault, bottomText.length * charAngularWidth);
if(bottomText.length ===1) estimatedSweep = 0;
drawCurvedText(ctx, bottomText, cx, cy, textPlacementRadius, 90 - estimatedSweep / 2, estimatedSweep, true);
}
} else {
ctx.textBaseline = 'middle';
const textMarginFromEdge = (bw > 0 ? bw : 0) + typicalTextHeight * 0.6 + _getVerticalPaddingForShape(patchShape);
if (topText) {
ctx.fillText(topText, cx, textMarginFromEdge);
}
if (bottomText) {
ctx.fillText(bottomText, cx, patchHeight - textMarginFromEdge);
}
}
function _getVerticalPaddingForShape(shape) {
// Provide a little extra padding for pointy shapes if text is straight
if (shape === 'shield') return 5;
return 2; // Default small padding
}
return canvas;
}
Apply Changes