Canvas蒙版图片上传缩放异常修复:替换内部图片需求
Fix: Image Duplication & Zoom Sync Issues
我来帮你解决这个图片重复显示和缩放不同步的问题!核心问题出在代码里额外插入了一个独立的<img>元素(#renderImage),而缩放操作只作用于这个外部元素,没有同步到蒙版内部的canvas渲染逻辑。同时这个额外的img也导致了图片重复显示的问题。
关键修复点:
- 移除重复的图片元素:删掉手动添加
#renderImage的代码,蒙版插件本身已经通过canvas渲染了上传的图片,不需要额外插入img。 - 让缩放操作作用于蒙版内部的图片:修改缩放函数,直接更新蒙版实例的缩放比例,然后触发重新渲染,确保缩放同步生效。
- 为每个蒙版实例单独保存缩放状态:避免全局
scale变量导致多个蒙版互相干扰。
修复后的完整代码:
JavaScript
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'); // 存储每个mask实例的缩放状态 const maskScales = {}; $(function() { // Upload image onclick 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; //初始化当前mask的缩放比例 maskScales[counter] = 1; //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, scale: maskScales[counter] }); table.push(mask); fileup.onchange = function() { let mask2 = table[target]; const imgView = URL.createObjectURL(fileup.files[0]); const newImageLoadedId = mask2.loadImage(URL.createObjectURL(fileup.files[0])); document.getElementById('fileup').value = ""; // --- 移除了添加#renderImage的代码 --- // 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; }; } // 新增方法:更新缩放比例并重新渲染 container.updateScale = function(newScale) { settings.scale = newScale; renderInnerImage(); }; // change the draggable image container.loadImage = function(imageUrl) { settings.y = startY; settings.x = startX; prevX = prevY = 0; settings.imageUrl = imageUrl; initImage = true; container.updateStyle().then(renderInnerImage); 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('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 function zoom_in(data) { var getParent = data.parentElement.parentElement.parentElement; var getId = parseInt(getParent.id.substring(getParent.id.length - 1)); // 更新对应mask的缩放比例 maskScales[getId] += 0.25; // 找到对应的mask实例并更新缩放 const targetMask = JQmasks.find(m => m.id === getId.toString()).item; targetMask.updateScale(maskScales[getId]); } function zoom_out(data) { var getParent = data.parentElement.parentElement.parentElement; var getId = parseInt(getParent.id.substring(getParent.id.length - 1)); // 确保缩放比例不小于0.25 if (maskScales[getId] > 0.25) { maskScales[getId] -= 0.25; // 找到对应的mask实例并更新缩放 const targetMask = JQmasks.find(m => m.id === getId.toString()).item; targetMask.updateScale(maskScales[getId]); } }
CSS
.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; }
HTML
<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> </div> <img id="image" src ="" style ="display:none"> </span> </template>
修复说明:
- 移除了手动添加
#renderImage的代码,解决了图片重复显示的问题; - 新增了
maskScales对象来存储每个蒙版实例的缩放比例,避免全局变量冲突; - 在蒙版插件中添加了
updateScale方法,用于更新缩放比例并重新渲染图片; - 修改了
zoom_in和zoom_out函数,现在它们会找到对应的蒙版实例,更新其内部缩放比例并触发重新渲染,确保缩放操作正确作用在蒙版内的图片上。
内容的提问来源于stack exchange,提问作者user8444404




