Please bookmark this page to avoid losing your image tool!

Image Military Insignia Patch Template Creator

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Military Insignia Patch Template Creator is a versatile tool designed to assist in the creation of custom military insignia patches. Users can upload an image and customize it into various shapes such as circles, ovals, rounded rectangles, or shields. Additional features allow for the adjustment of border colors and widths, background colors, and the inclusion of customized text on the top and bottom of the patch, with options for text color and font type. This tool is ideal for military personnel, veterans, or hobbyists who wish to design unique patches for uniforms, events, or personal collections.

Leave a Reply

Your email address will not be published. Required fields are marked *