Simple Vary Region (inpaint) mask editor

November 22, 2024

Midjourney inpainting Vary Region and Vary Region + Remix button require a base64-encoded black and white mask image. The generated base64 mask value is used by the POST jobs/button button Vary (Region) param mask.

Source code

Copy the code provided below into a <select desired file name>.html and open it in your browser.


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Vary Region (inpaint) mask editor</title>

<style>
    #canvasContainer {
        position: relative;
        max-width: 100vw;
        max-height: 100vh;
        /* overflow: hidden; */
        margin: 0 auto;
    }
    #imageCanvas, #maskCanvas {
        display: block;
    }
    #imageCanvas {
        border: 1px solid #ccc;
    }
    #maskCanvas {
        position: absolute;
        left: 0;
        top: 0;
        cursor: crosshair;
    }
    #maskBase64 {
        width: 100%;
        height: 100px;
        margin-top: 10px;
    }
    #copyMaskButton {
        margin-top: 10px;
    }
</style>

<input type="file" id="imageLoader" name="imageLoader"/>
<div id="canvasContainer">
    <canvas id="imageCanvas"></canvas>
    <canvas id="maskCanvas"></canvas>
</div>
<button id="copyMaskButton">Copy Base64 to Clipboard</button>
<textarea id="maskBase64" readonly></textarea>

<script>
    function getMousePos(canvas, evt) {
        var rect = canvas.getBoundingClientRect();
        var scaleX = canvas.width / rect.width;
        var scaleY = canvas.height / rect.height;
        return {
            x: (evt.clientX - rect.left) * scaleX,
            y: (evt.clientY - rect.top) * scaleY
        }
    }

    document.addEventListener('DOMContentLoaded', () => {
        var isDrawing = false;
        var rect = {};
        var img = new Image();
        var base64Mask = ''; 

        var imageLoader = document.getElementById('imageLoader');
        var canvasContainer = document.getElementById('canvasContainer'); 
        var copyMaskButton = document.getElementById('copyMaskButton');
        var maskBase64 = document.getElementById('maskBase64');
        var imageCanvas = document.getElementById('imageCanvas');
        var maskCanvas = document.getElementById('maskCanvas');
        var imageCtx = imageCanvas.getContext('2d');
        var maskCtx = maskCanvas.getContext('2d');

        var viewportWidth = window.innerWidth;
        var rectCanvasContainer = canvasContainer.getBoundingClientRect();
        var viewportHeight = window.innerHeight - rectCanvasContainer.top + window.scrollY;
        
        imageLoader.addEventListener('change', function(e) {
            var reader = new FileReader();
            reader.onload = function(event) {
                img.onload = function() {
                    var widthScale = viewportWidth / img.width;
                    var heightScale = viewportHeight / img.height;
                    var scale = Math.min(widthScale, heightScale, 1);        
                    var scaledWidth = img.width * scale;
                    var scaledHeight = img.height * scale;

                    imageCanvas.width = img.width;
                    imageCanvas.height = img.height;
                    maskCanvas.width = img.width;
                    maskCanvas.height = img.height;

                    imageCanvas.style.width = scaledWidth + 'px';
                    imageCanvas.style.height = scaledHeight + 'px';
                    maskCanvas.style.width = scaledWidth + 'px';
                    maskCanvas.style.height = scaledHeight + 'px';

                    imageCtx.drawImage(img, 0, 0);
                    maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);

                    maskBase64.value = '';
                    base64Mask = '';

                    isDrawing = false;
                    rect = {};
                };
                
                img.src = event.target.result;

            };

            reader.readAsDataURL(e.target.files[0]);

        }, false);

        maskCanvas.addEventListener('mousedown', function(e) {
            isDrawing = true;
            rect = {};
            var pos = getMousePos(maskCanvas, e);
            rect.startX = pos.x;
            rect.startY = pos.y;
            rect.w = 0;
            rect.h = 0;
            maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
        });

        maskCanvas.addEventListener('mousemove', function(e) {
            if (isDrawing) {
                var pos = getMousePos(maskCanvas, e);
                rect.w = pos.x - rect.startX;
                rect.h = pos.y - rect.startY;
                maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
                maskCtx.strokeStyle = 'red';
                maskCtx.lineWidth = 2;
                maskCtx.strokeRect(rect.startX, rect.startY, rect.w, rect.h);
            }
        });

        maskCanvas.addEventListener('mouseup', function(e) {
            if (isDrawing) {
                isDrawing = false;
                var rectWidth = Math.abs(rect.w);
                var rectHeight = Math.abs(rect.h);

                if (rectWidth > 1 && rectHeight > 1) {
                    var maskImage = document.createElement('canvas');
                    maskImage.width = maskCanvas.width;
                    maskImage.height = maskCanvas.height;
                    var maskImageCtx = maskImage.getContext('2d');
                    maskImageCtx.fillStyle = '#000';
                    maskImageCtx.fillRect(0, 0, maskImage.width, maskImage.height);
                    var drawX = rect.w >= 0 ? rect.startX : rect.startX + rect.w;
                    var drawY = rect.h >= 0 ? rect.startY : rect.startY + rect.h;
                    maskImageCtx.fillStyle = '#fff';
                    maskImageCtx.fillRect(drawX, drawY, rectWidth, rectHeight);
                    var base64String = maskImage.toDataURL('image/png');
                    base64Mask = base64String.replace(/^data:image\/png;base64,/, '');
                    maskBase64.value = base64Mask;
                } else {
                    maskBase64.value = '';
                    base64Mask = '';
                    maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
                }
            }
        });

        copyMaskButton.addEventListener('click', function() {
            if (base64Mask) {
                maskBase64.select();
                maskBase64.setSelectionRange(0, maskBase64.value.length);
                try {
                    var successful = document.execCommand('copy');
                    if (successful) {
                        alert('✅ Base64 mask copied to clipboard!');
                    } else {
                        alert('🛑 Failed to copy base64 mask.');
                    }
                } catch (err) {
                    alert('🛑 Oops, unable to copy');
                }
            } else {
                alert('🛑 No base64 mask to copy. Please select a valid rectangle.');
            }
        });
    });
</script>

</body>
</html>