Search code examples
javascriptjqueryhtmlcanvaszooming

Zoom the user uploaded image onclick button


Here is the basic code for Zoom in & Zoom out: https://codepen.io/kidsdial/pen/ROBpgM,

Now I tried to integrate that code in my Project.

Background:

  1. User clicks on Mask & upload their own image on the mask.

  2. Once image uploaded, Edit Text is displaying on the image.

  3. Once user click on Edit Text, we are displaying pop up box.

  4. Zoom in & Zoom out button displaying.

Issue:

When we click on that button, zoom is not happening for user uploaded image.

enter image description here

Video link

Codepen : https://codepen.io/kidsdial/pen/PgxegO

Fiddle : https://jsfiddle.net/kidsdial1/nhswfjr3/

var target;
const imageUrl = "https://i.imgur.com/RzEm1WK.png";

let jsonData = {
    "layers": [{
        "x": 0,
        "height": 612,
        "layers": [{
                "x": 160,
                "src": "ax0HVTs.png",
                "y": 291,
                "height": 296,
                "width": 429,
                "name": "mask_1"
            },
            {
                "x": 25,
                "src": "hEM2kEP.png",
                "height": 324,
                "width": 471,
                "y": 22,
                "name": "mask_2"
            }
        ],
        "y": 0,
        "width": 612
    }]
};

const containerElement = $('#container');
const fileUp = $('#fileup');

$(function() {

    // Upload image onclick mask image 

    containerElement.click(function(e) {
        var res = e.target;
        target = res.id;
        console.log(target);
        if (e.target.getContext) {
            // click only inside Non Transparent part
            var pixel = e.target.getContext('2d').getImageData(e.offsetX, e.offsetY, 1, 1).data;
            if (pixel[3] === 255) {
                setTimeout(() => {
                    $('#fileup').click();
                }, 20);
            }
        }
    });

    // Fetch mask images from json file - IGNORE this code

    function getAllSrc(layers) {
        let arr = [];
        layers.forEach(layer => {
            if (layer.src) {
                arr.push({
                    src: layer.src,
                    x: layer.x,
                    y: layer.y,
                    height: layer.height,
                    width: layer.width,
                    name: layer.name
                });
            } else if (layer.layers) {
                let newArr = getAllSrc(layer.layers);
                if (newArr.length > 0) {
                    newArr.forEach(({
                        src,
                        x,
                        y,
                        height,
                        width,
                        name
                    }) => {
                        arr.push({
                            src,
                            x: (layer.x + x),
                            y: (layer.y + y),
                            height,
                            width,
                            name: (name)
                        });
                    });
                }
            }
        });
        return arr;
    }

    function json(data)

    {
        var width = 0;
        var height = 0;

        let arr = getAllSrc(data.layers);

        let layer1 = data.layers;
        width = layer1[0].width;
        height = layer1[0].height;
        let counter = 0;
        let table = [];

        // container dimensions
        containerElement.css('width', width + "px").css('height', height + "px").addClass('temp');
        //end

        for (let {
                src,
                x,
                y,
                name
            } of arr) {

            //Get Height and width of mask image [ edit button ]
            var ImagePosition = arr;
            //code end

            var mask = $(".container").mask({
                imageUrl: imageUrl,

                // Fetch Mask images
                maskImageUrl: 'https://i.imgur.com/' + src,
                // end

                onMaskImageCreate: function(img) {
                    // Mask image positions
                    img.css({
                        "position": "absolute",
                        "left": x + "px",
                        "top": y + "px"
                    });
                    // end

                },
                id: counter
            });
            table.push(mask);
            fileup.onchange = function() {

                let mask2 = table[target];
                const newImageLoadedId = mask2.loadImage(URL.createObjectURL(fileup.files[0]));
                document.getElementById('fileup').value = "";

                //  Edit image - IGNORE this code

                if ($(".masked-img" + newImageLoadedId).length === 1) {
                    $("<span class=\"pip pip" + newImageLoadedId + "\">" +
                        "<a onclick='document.getElementById(\"dark" + newImageLoadedId + "\").style.display=\"block\";'><span class=\"edit edit" + newImageLoadedId + "\" >Edit </span></a>" +
                        "</span>").insertAfter(".masked-img" + newImageLoadedId).css({
                        "left": ImagePosition[newImageLoadedId].x + (ImagePosition[newImageLoadedId].width / 2) + "px",
                        "top": ImagePosition[newImageLoadedId].y + (ImagePosition[newImageLoadedId].height / 2) + "px"
                    });;
                    $("<div id=\'dark" + newImageLoadedId + "\' class=\'dark_content\'>" +
                        $('#demoTemplate').html() +
                        "<a href=\"javascript:void(0)\" onclick=\"document.getElementById(\'dark" + newImageLoadedId + "\').style.display=\'none\'\">Close</a>" + "</div>").appendTo(".pip" + newImageLoadedId).css({
                        "left": $('.edit' + newImageLoadedId).width() + 2 + "px",
                        "top": "0px"
                    });
                }
                //  end                
            };
            counter++;
        }
    }
    json(jsonData);
}); // end of function

// Image code

(function($) {
    var JQmasks = [];
    $.fn.mask = function(options) {
        // This is the easiest way to have default options.
        var settings = $.extend({
            // These are the defaults.
            maskImageUrl: undefined,
            imageUrl: undefined,
            scale: 1,
            id: new Date().getUTCMilliseconds().toString(),
            x: 0, // image start position
            y: 0, // image start position
            onMaskImageCreate: function(div) {},
        }, options);


        var container = $(this);

        let prevX = 0,
            prevY = 0,
            draggable = false,
            img,
            canvas,
            context,
            image,
            timeout,
            initImage = false,
            startX = settings.x,
            startY = settings.y,
            div;

        container.mousePosition = function(event) {
            return {
                x: event.pageX || event.offsetX,
                y: event.pageY || event.offsetY
            };
        }

        container.selected = function(ev) {
            var pos = container.mousePosition(ev);
            var item = $(".masked-img canvas").filter(function() {
                var offset = $(this).offset()
                var x = pos.x - offset.left;
                var y = pos.y - offset.top;
                var d = this.getContext('2d').getImageData(x, y, 1, 1).data;
                return d[0] > 0
            });

            JQmasks.forEach(function(el) {
                var id = item.length > 0 ? $(item).attr("id") : "";
                if (el.id == id)
                    el.item.enable();
                else el.item.disable();
            });
        };

        container.enable = function() {
            draggable = true;
            $(canvas).attr("active", "true");
            div.css({
                "z-index": 2
            });
        }

        container.disable = function() {
            draggable = false;
            $(canvas).attr("active", "false");
            div.css({
                "z-index": 1
            });
        }

        container.getImagePosition = function() {
            return {
                x: settings.x,
                y: settings.y,
                scale: settings.scale
            };
        };

        container.updateStyle = function() {
            return new Promise((resolve, reject) => {
                context.beginPath();
                context.globalCompositeOperation = "source-over";
                image = new Image();
                image.setAttribute('crossOrigin', 'anonymous');
                image.src = settings.maskImageUrl;
                image.onload = function() {
                    canvas.width = image.width;
                    canvas.height = image.height;
                    context.drawImage(image, 0, 0, image.width, image.height);
                    div.css({
                        "width": image.width,
                        "height": image.height
                    });
                    resolve();
                };
            });
        };

        function renderInnerImage() {
            img = new Image();
            img.setAttribute('crossOrigin', 'anonymous');
            img.src = settings.imageUrl;
            img.onload = function() {
                settings.x = settings.x == 0 && initImage ? (canvas.width - (img.width * settings.scale)) / 2 : settings.x;
                settings.y = settings.y == 0 && initImage ? (canvas.height - (img.height * settings.scale)) / 2 : settings.y;
                context.globalCompositeOperation = 'source-atop';
                context.drawImage(img, settings.x, settings.y, img.width * settings.scale, img.height * settings.scale);
                initImage = false;
            };
        }

        // change the draggable image

        container.loadImage = function(imageUrl) {
            console.log("load");
            settings.y = startY;
            settings.x = startX;
            prevX = prevY = 0;
            settings.imageUrl = imageUrl;
            initImage = true;
            container.updateStyle().then(renderInnerImage);
            // sirpepole  Add this
            return settings.id;
        };

        container.loadMaskImage = function(imageUrl, from) {
            canvas = document.createElement("canvas");
            context = canvas.getContext('2d');
            canvas.setAttribute("draggable", "true");
            canvas.setAttribute("id", settings.id);
            settings.maskImageUrl = imageUrl;
            div = $("<div/>", {
                "class": "masked-img"
            }).append(canvas);

            // div.find("canvas").on('touchstart mousedown', function(event)
            div.find("canvas").on('dragstart', function(event) {
                if (event.handled === false) return;
                event.handled = true;
                container.onDragStart(event);
            });

            div.find("canvas").on('touchend mouseup', function(event) {
                if (event.handled === false) return;
                event.handled = true;
                container.selected(event);
            });

            div.find("canvas").bind("dragover", container.onDragOver);
            container.append(div);
            if (settings.onMaskImageCreate)
                settings.onMaskImageCreate(div);
            container.loadImage(settings.imageUrl);
        };
        container.loadMaskImage(settings.maskImageUrl);
        JQmasks.push({
            item: container,
            id: settings.id
        })
        // Edit image
        div.addClass('masked-img' + settings.id);
        // end
        return container;
    };
}(jQuery));

// Zoom 

//document.getElementById("img").src = json(data);

var angle = 0;
var scale = 1;
var $img = $('#image');

$img.on('transform', function() {
    $img.css('transform', `rotate(${angle}deg) scale(${scale})`);
});

$('.js-zoom-in').on('click', function() {
    scale += 0.25;
    if (scale == 2.25) {
        scale = 2;
    };
    $img.trigger('transform');
});

$('.js-zoom-out').on('click', function() {
    scale -= 0.25;
    if (scale == 0) {
        scale = 0.25;
    }
    $img.trigger('transform');
});
.container {
	background: silver;
	position: relative;
	
}

.container img {
	position: absolute;
	top: 0;
	bottom: 250px;
	left: 0;
	right: 0;
	margin: auto;
	z-index: 999;
}

.masked-img {
	overflow: hidden;
	position: relative;
}

.pip {
	display: inline-block;
	margin: 0;
	position: absolute;
}

.edit {
	display: block;
	background: #444;
	border: 1px solid black;
	color: white;
	text-align: center;
	cursor: pointer;
	position: absolute;
	z-index: 3;
}

.edit:hover {
	background: white;
	color: black;
	position: absolute;
	z-index: 3;
}

.dark_content {
	display: none;
	position: relative;
	top: 25%;
	left: 25%;
	width: 250px;
	height: 250px;
	padding: 16px;
	border: 16px solid orange;
	background-color: white;
	z-index: 1002;
	overflow: auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js">
</script>

<input id="fileup" name="fileup" type="file" style="display:none" >

<div id="container"class="container">
</div>

<template id='demoTemplate'>
   <span>
      <div class="btn-group">
         <button type="button" class="js-zoom-in">Zoom In</button>
         <button type="button" class="js-zoom-out">Zoom Out</button>         
      </div>
      <img id="image" src ="" style ="display:none">             
   </span>
</template>

Note: I will give 150 bounty points if someone gives me a solution.


Solution

  • Have a try with this code. @PranavNutalapati helped me figure this out.

    var target;
    const imageUrl = "https://i.imgur.com/RzEm1WK.png";
    
    let jsonData = {
        "layers": [{
            "x": 0,
            "height": 612,
            "layers": [{
                "x": 160,
                "src": "ax0HVTs.png",
                "y": 291,
                "height": 296,
                "width": 429,
                "name": "mask_1"
            }, {
                "x": 25,
                "src": "hEM2kEP.png",
                "height": 324,
                "width": 471,
                "y": 22,
                "name": "mask_2"
            }],
            "y": 0,
            "width": 612
        }]
    };
    
    const containerElement = $('#container');
    const fileUp = $('#fileup');
    let mask;
    
    $(function () {
    
        // Upload image on the click mask image
    
        containerElement.click(function (e) {
            var res = e.target;
            target = res.id;
            if (e.target.getContext) {
                // click only inside Non Transparent part
                var pixel = e.target.getContext('2d').getImageData(e.offsetX, e.offsetY, 1, 1).data;
                if (pixel[3] === 255) {
                    setTimeout(() => {
                        $('#fileup').click();
                    }, 20);
                }
            }
        });
    
        // Fetch mask images from JSON file - IGNORE this code
    
        function getAllSrc(layers) {
            let arr = [];
            layers.forEach(layer => {
                if (layer.src) {
                    arr.push({
                        src: layer.src,
                        x: layer.x,
                        y: layer.y,
                        height: layer.height,
                        width: layer.width,
                        name: layer.name
                    });
                } else if (layer.layers) {
                    let newArr = getAllSrc(layer.layers);
                    if (newArr.length > 0) {
                        newArr.forEach(({
                            src,
                            x,
                            y,
                            height,
                            width,
                            name
                        }) => {
                            arr.push({
                                src,
                                x: (layer.x + x),
                                y: (layer.y + y),
                                height,
                                width,
                                name: (name)
                            });
                        });
                    }
                }
            });
            return arr;
        }
    
        function json(data) {
            var width = 0;
            var height = 0;
    
            let arr = getAllSrc(data.layers);
            let layer1 = data.layers;
            width = layer1[0].width;
            height = layer1[0].height;
            let counter = 0;
            let table = [];
    
            // container dimensions
            containerElement.css('width', width + "px").css('height', height + "px").addClass('temp');
            //end
    
            for (let {
                    src,
                    x,
                    y,
                    name
                } of arr) {
    
                //Get Height and width of mask image [ edit button ]
                var ImagePosition = arr;
                //code end
    
                var mask = $(".container").mask({
                    imageUrl: imageUrl,
    
                    // Fetch Mask images
                    maskImageUrl: 'http://i.imgur.com/' + src,
                    // end
    
                    onMaskImageCreate: function (img) {
                        // Mask image positions
                        img.css({
                            "position": "absolute",
                            "left": x + "px",
                            "top": y + "px"
                        });
                        // end
    
                    },
                    id: counter
                });
                // here
                table.push(mask);
                fileup.onchange = function () {
    
                    let mask2 = table[target];
                    const newImageLoadedId = mask2.loadImage(URL.createObjectURL(fileup.files[0]));
                    document.getElementById('fileup').value = "";
    
                    // Edit image - IGNORE this code
    
                    if ($(".masked-img" + newImageLoadedId).length === 1) {
                        const span = $("<span class=\"pip pip" + newImageLoadedId + "\">" +
                            "<a onclick='document.getElementById(\"dark" + newImageLoadedId +
                            "\").style.display=\"block\";'><span class=\"edit edit" +
                            newImageLoadedId + "\" >Edit </span></a>" +
                            "</span>").insertAfter(".masked-img" + newImageLoadedId).css({
                            "left": ImagePosition[newImageLoadedId].x + (ImagePosition[
                                newImageLoadedId].width / 2) + "px",
                            "top": ImagePosition[newImageLoadedId].y + (ImagePosition[
                                newImageLoadedId].height / 2) + "px"
                        });
                        span.attr('data-id', newImageLoadedId)
                        $("<div id=\'dark" + newImageLoadedId + "\' class=\'dark_content\'>" +
                                $('#demoTemplate').html() +
                                "<a href=\"javascript:void(0)\" onclick=\"document.getElementById(\'dark" +
                                newImageLoadedId + "\').style.display=\'none\'\">Close</a>" + "</div>")
                            .appendTo(".pip" + newImageLoadedId).css({
                                "left": $('.edit' + newImageLoadedId).width() + 2 + "px",
                                "top": "0px"
                            });
                    }
                    // end
                };
                counter++;
            }
            return mask;
        }
        mask = json(jsonData);
    }); // end of function
    
    // Image code
    
    (function ($) {
        window.JQmasks = [];
        $.fn.mask = function (options) {
            // This is the easiest way to have default options.
            var settings = $.extend({
                // These are the defaults.
                maskImageUrl: undefined,
                imageUrl: undefined,
                scale: 1,
                id: new Date().getUTCMilliseconds().toString(),
                x: 0, // image start position
                y: 0, // image start position
                onMaskImageCreate: function (div) {},
                rotate: 0,
            }, options);
    
            // Create the image properties
            settings.maskImage = new Image
            settings.image = new Image
    
            // set the cross-origin attributes
            settings.maskImage.setAttribute('crossOrigin', 'anonymous');
            settings.image.setAttribute('crossOrigin', 'anonymous');
    
            settings.maskImage.onload = function () {
                // once the mask is loaded, load the image
                container.loadImage(settings.imageUrl)
            }
    
            settings.image.onload = function () {
                // once the image is loaded, render to canvas
                container.drawMask()
                container.drawImage()
            }
    
            var container = $(this);
    
            let prevX = 0,
                prevY = 0,
                draggable = false,
                img,
                canvas,
                context,
                image,
                timeout,
                initImage = false,
                startX = settings.x,
                startY = settings.y,
                scale = settings.scale,
                div;
    
            container.mousePosition = function (event) {
                return {
                    x: event.pageX || event.offsetX,
                    y: event.pageY || event.offsetY
                };
            }
    
            container.selected = function (ev) {
                var pos = container.mousePosition(ev);
                var item = $(".masked-img canvas").filter(function () {
                    var offset = $(this).offset()
                    var x = pos.x - offset.left;
                    var y = pos.y - offset.top;
                    var d = this.getContext('2d').getImageData(x, y, 1, 1).data;
                    return d[0] > 0
                });
    
                JQmasks.forEach(function (el) {
                    var id = item.length > 0 ? $(item).attr("id") : "";
                    if (el.id == id)
                        el.item.enable();
                    else el.item.disable();
                });
            };
    
            container.enable = function () {
                draggable = true;
                $(canvas).attr("active", "true");
                div.css({
                    "z-index": 2
                });
            }
    
            container.disable = function () {
                draggable = false;
                $(canvas).attr("active", "false");
                div.css({
                    "z-index": 1
                });
            }
    
            container.getImagePosition = function () {
                return {
                    x: settings.x,
                    y: settings.y,
                    scale: settings.scale
                };
            };
    
            container.zoom = function (delta) {
                settings.scale += delta;
                settings.scale = Math.min(2, Math.max(0, settings.scale))
                context.clearRect(0, 0, canvas.width, canvas.height);
                container.drawMask()
                container.drawImage();
            }
    
            container.rotate = function (rotation) {
                settings.rotate += rotation * Math.PI / 180;
                console.log('Rotation', settings.rotate);
                context.clearRect(0, 0, canvas.width, canvas.height);
                container.drawMask()
                container.drawImage();
            }
    
            container.drawMask = function () {
                canvas.width = settings.maskImage.width;
                canvas.height = settings.maskImage.height;
                context.save();
                context.beginPath();
                context.globalCompositeOperation = "source-over";
                // draw the masked image after scaling
                context.drawImage(settings.maskImage, 0, 0, settings.maskImage.width, settings.maskImage
                    .height);
                context.restore()
            };
    
            container.drawImage = function () {
                const img = settings.image
    
                settings.x = settings.x == 0 && initImage ? (canvas.width - (img.width * settings.scale)) / 2 : settings.x;
                settings.y = settings.y == 0 && initImage ? (canvas.height - (img.height * settings.scale)) / 2 : settings.y;
    
                context.globalCompositeOperation = 'source-atop';
                context.save();
                context.translate(settings.x + img.width / 2, settings.y + img.height / 2);
                context.rotate(settings.rotate);
                context.scale(settings.scale, settings.scale);
                context.translate(-(settings.x + img.width / 2), -(settings.y + img.height / 2));
                let width = img.width, height = img.height;
                context.drawImage(img, settings.x, settings.y, width, height);
                context.restore();
                initImage = false;
            }
    
            // change the draggable image
    
            container.loadImage = function (imageUrl) {
                console.log("selected image, loading");
                settings.y = startY;
                settings.x = startX;
                prevX = prevY = 0;
    
                initImage = true;
    
                settings.image.src = imageUrl; // CHANGED
    
                // sirpepole Add this
                return settings.id;
            };
    
            container.onDragStart = function(evt) {
                console.log('Draw started');
                if (evt.target.getContext) {
                    var pixel = evt.target.getContext('2d').getImageData(evt.offsetX, evt.offsetY, 1, 1).data;
    
                    $(canvas).attr("active", "true");
                    container.selected(evt);
                    prevX = evt.clientX;
                    prevY = evt.clientY;
                    var img = new Image();
                    evt.originalEvent.dataTransfer.setDragImage(img, 10, 10);
                    evt.originalEvent.dataTransfer.setData('text/plain', 'anything');
    
                }
            };
    
            container.onDragOver = function(evt) {
                console.log('Drag over');
    
                if (evt.target.getContext) {
                    var pixel = evt.target.getContext('2d').getImageData(evt.offsetX, evt.offsetY, 1, 1).data;
                    if (pixel[3] === 255) {
                        if (draggable && $(canvas).attr("active") === "true") {
                            var x = settings.x + evt.clientX - prevX;
                            var y = settings.y + evt.clientY - prevY;
                            if (x == settings.x && y == settings.y)
                                return; // position has not changed
                            settings.x += evt.clientX - prevX;
                            settings.y += evt.clientY - prevY;
                            prevX = evt.clientX;
                            prevY = evt.clientY;
                            clearTimeout(timeout);
                            timeout = setTimeout(function() {
                                container.drawMask();
                                container.drawImage();
                            }, 1);
                        }
                    } else {
                        evt.stopPropagation();
                        return false;
                    }
                }
            };
    
            container.loadMaskImage = function (imageUrl, from) {
                console.log('loading mask image from', imageUrl, from)
                canvas = document.createElement("canvas");
                context = canvas.getContext('2d');
                canvas.setAttribute("draggable", "true");
                canvas.setAttribute("id", settings.id);
                // settings.maskImageUrl = imageUrl;
                settings.maskImage.src = imageUrl // CHANGED
    
                div = $("<div/>", {
                    "class": "masked-img"
                }).append(canvas);
    
                // div.find("canvas").on('touchstart mousedown', function(event)
                div.find("canvas").on('dragstart', function (event) {
                    if (event.handled === false) return;
                    event.handled = true;
                    container.onDragStart(event);
                });
    
                div.find("canvas").on('touchend mouseup', function (event) {
                    if (event.handled === false) return;
                    event.handled = true;
                    container.selected(event);
                });
    
                div.find("canvas").bind("dragover", container.onDragOver);
    
                container.append(div);
                if (settings.onMaskImageCreate)
                    settings.onMaskImageCreate(div);
    
                // container.loadImage(settings.imageUrl);
                // Moved this to the settings.maskImage.onload
            };
            container.loadMaskImage(settings.maskImageUrl);
            JQmasks.push({
                item: container,
                id: settings.id
            })
            // Edit image
            div.addClass('masked-img' + settings.id);
            div.attr('data-id', settings.id);
            // ends
            return container;
        };
    }(jQuery));
    
    // Zoom
    
    //document.getElementById("img").src = json(data);
    
    function zoom_in(button) {
        const id = $(button).parents('.pip').attr('data-id')
        JQmasks[id].item.zoom(0.1);
    }
    
    function zoom_out(button) {
        const id = $(button).parents('.pip').attr('data-id')
        JQmasks[id].item.zoom(-0.1);
    }
    
    // Rotate
    
    function rotate_right(button) {
        const id = $(button).parents('.pip').attr('data-id')
        JQmasks[id].item.rotate(20);
    }
    
    function rotate_left(button) {
        const id = $(button).parents('.pip').attr('data-id')
        JQmasks[id].item.rotate(-20);
    }
    .container {
        background: silver;
        position: relative;
    }
    
    .container img {
        position: absolute;
        top: 0;
        bottom: 250px;
        left: 0;
        right: 0;
        margin: auto;
        z-index: 999;
    }
    
    .masked-img {
        overflow: hidden;
        position: relative;
    }
    
    .pip {
        display: inline-block;
        margin: 0;
        position: absolute;
    }
    
    .edit {
        display: block;
        background: #444;
        border: 1px solid black;
        color: white;
        text-align: center;
        cursor: pointer;
        position: absolute;
        z-index: 3;
    }
    
    .edit:hover {
        background: white;
        color: black;
        position: absolute;
        z-index: 3;
    }
    
    .dark_content {
        display: none;
        position: relative;
        top: 25%;
        left: 25%;
        width: 250px;
        height: 250px;
        padding: 16px;
        border: 16px solid orange;
        background-color: white;
        z-index: 1002;
        overflow: auto;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    
    <input id="fileup" name="fileup" type="file" style="display:none">
    
    <div id="container" class="container"></div>
    
    <template id='demoTemplate'>
        <span>
            <div class="btn-group">
                <button type="button" class="js-zoom-in" onclick="zoom_in(this)">Zoom In</button>
                <button type="button" class="js-zoom-out" onclick="zoom_out(this)">Zoom Out</button>
                <button type="button" class="js-rotate-right" onclick="rotate_right(this)">Rotate Right</button>
                <button type="button" class="js-rotate-left" onclick="rotate_left(this)">Rotate Left</button>
            </div>
            <img id="image" src="" style="display:none">
        </span>
    </template>

    Explanation

    I have added a data attribute id to differentiate both the image's buttons.

    Then, added a function to handle the button clicks (zoom_in and zoom_out namely).

    function zoom_in(button) {
        const id = $(button).parents('.pip').attr('data-id') // to retrieve the id
        JQmasks[id].item.zoom(0.1); // to zoom the correct image
    }
    
    function zoom_out(button) {
        const id = $(button).parents('.pip').attr('data-id')
        JQmasks[id].item.zoom(-0.1);
    }
    

    Now, we need to handle the zoom functionality. For that, we need to create a new function zoom within the container.

    container.zoom = function (delta) {
        settings.scale += delta; // increase / decrease the scale
        settings.scale = Math.min(2, Math.max(0, settings.scale)) // limiting the range of the scale from 0 to 2
        context.clearRect(0, 0, canvas.width, canvas.height); // clearing the whole canvas
        container.drawMask(); // drawing the bow
        container.drawImage(); // drawing the scaled image
    }
    

    We are only updating the scale property, so we need to make use of it while drawing it. So within the drawImage() function

    container.drawImage = function () {
        const img = settings.image
    
        settings.x = settings.x == 0 && initImage ? (canvas.width - (img.width * settings.scale)) / 2 : settings.x;
        settings.y = settings.y == 0 && initImage ? (canvas.height - (img.height * settings.scale)) / 2 : settings.y;
    
        context.globalCompositeOperation = 'source-atop';
        context.save();
        context.translate(settings.x + img.width / 2, settings.y + img.height / 2); // translating the canvas to center of the image
        context.scale(settings.scale, settings.scale); // scaling the canvas
        context.translate(-(settings.x + img.width / 2), -(settings.y + img.height / 2)); // translating the canvas back to it's original position
        let width = img.width, height = img.height;
        context.drawImage(img, settings.x, settings.y, width, height);
        context.restore();
        initImage = false;
    }
    

    Update 1 - Added functionality for the user to rotate the image

    Explanation

    Starting with handling the button click.

    function rotate_right(button) {
        const id = $(button).parents('.pip').attr('data-id') // getting the id of the image
        JQmasks[id].item.rotate(20); // rotating it by 20deg
    }
    
    function rotate_left(button) {
        const id = $(button).parents('.pip').attr('data-id')
        JQmasks[id].item.rotate(-20);
    }
    

    Now, inside the container, a new function rotate() has been added to handle the rotation

    container.rotate = function (rotation) {
        settings.rotate += rotation * Math.PI / 180; // converting degree to radians and adding it to the previous rotation angle
        console.log('Rotation', settings.rotate);
        context.clearRect(0, 0, canvas.width, canvas.height); // clearing the canvas
        container.drawMask(); // re-drawing the bow
        container.drawImage(); // re-drawing the image
    }
    

    Here we are only updating the rotation property. So, while drawing it we need to use that. For that inside the drawImage() function

    context.save();
    context.translate(settings.x + img.width / 2, settings.y + img.height / 2); // translating the canvas to the center of the image
    context.rotate(settings.rotate); // rotating the image
    context.scale(settings.scale, settings.scale); // scaling the image
    context.translate(-(settings.x + img.width / 2), -(settings.y + img.height / 2)); // translating the image back to it's original position
    let width = img.width, height = img.height;
    context.drawImage(img, settings.x, settings.y, width, height);
    context.restore();