You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Canvas蒙版图片上传缩放异常修复:替换内部图片需求

Fix: Image Duplication & Zoom Sync Issues

我来帮你解决这个图片重复显示和缩放不同步的问题!核心问题出在代码里额外插入了一个独立的<img>元素(#renderImage),而缩放操作只作用于这个外部元素,没有同步到蒙版内部的canvas渲染逻辑。同时这个额外的img也导致了图片重复显示的问题。

关键修复点:

  1. 移除重复的图片元素:删掉手动添加#renderImage的代码,蒙版插件本身已经通过canvas渲染了上传的图片,不需要额外插入img。
  2. 让缩放操作作用于蒙版内部的图片:修改缩放函数,直接更新蒙版实例的缩放比例,然后触发重新渲染,确保缩放同步生效。
  3. 为每个蒙版实例单独保存缩放状态:避免全局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_inzoom_out函数,现在它们会找到对应的蒙版实例,更新其内部缩放比例并触发重新渲染,确保缩放操作正确作用在蒙版内的图片上。

内容的提问来源于stack exchange,提问作者user8444404

火山引擎 最新活动