You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Canvas加载可缩放平移图片后保存画布内容及本地文件选择实现问题

问题原因

你调用toDataURL报错确实是跨源资源导致的:当Canvas加载了未配置跨域许可的外域图片后,会被浏览器标记为「污染画布」,禁止调用导出类API,这是浏览器的默认安全限制。

实现方案

以下是同时满足两个需求的完整可运行修改版代码:

修改后的HTML代码

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p>演示如何使用HTML5 Canvas Context的变换方法实现选择性缩放:拖拽平移,点击放大对应位置,Shift+点击缩小对应位置,鼠标滚轮控制对应位置缩放。</p>
<!-- 新增本地图片选择控件 -->
<input type="file" id="imgUpload" accept="image/*">
<canvas></canvas>
<button onclick="saveImg()">SAVE IMG</button>

修改后的CSS代码

body { background:#eee; margin:1em; text-align:center; }
canvas { border-radius:25%;display:block; margin:1em auto; background:#fff; border:1px solid #ccc }
#imgUpload { margin: 10px 0; }

修改后的JavaScript代码

var canvas = document.getElementsByTagName('canvas')[0];
canvas.width = 200;
canvas.height = 200;
var ctx = canvas.getContext('2d');
var gkhead = new Image;
// 图片加载状态标记
var imgLoaded = false;

// 保存图片逻辑
function saveImg() {
    if (!imgLoaded) {
        alert('请先上传图片再保存');
        return;
    }
    try {
        var fullQuality = canvas.toDataURL('image/jpeg', 1.0);
        // 自动触发下载
        var link = document.createElement('a');
        link.download = '调整后图片.jpg';
        link.href = fullQuality;
        link.click();
    } catch (e) {
        alert('保存失败,请使用本地上传的图片操作');
    }
}

// 本地图片读取逻辑
document.getElementById('imgUpload').addEventListener('change', function(e) {
    var file = e.target.files[0];
    if (!file) return;
    var reader = new FileReader();
    reader.onload = function(evt) {
        gkhead.src = evt.target.result;
    }
    reader.readAsDataURL(file);
})

// 图片加载完成后初始化交互逻辑
gkhead.onload = function() {
    imgLoaded = true;
    trackTransforms(ctx);
    redraw();

    var lastX=canvas.width/2, lastY=canvas.height/2;
    var dragStart,dragged;

    canvas.addEventListener('mousedown',function(evt){
        document.body.style.mozUserSelect = document.body.style.webkitUserSelect = document.body.style.userSelect = 'none';
        lastX = evt.offsetX || (evt.pageX - canvas.offsetLeft);
        lastY = evt.offsetY || (evt.pageY - canvas.offsetTop);
        dragStart = ctx.transformedPoint(lastX,lastY);
        dragged = false;
    },false);

    canvas.addEventListener('mousemove',function(evt){
        lastX = evt.offsetX || (evt.pageX - canvas.offsetLeft);
        lastY = evt.offsetY || (evt.pageY - canvas.offsetTop);
        dragged = true;
        if (dragStart){
          var pt = ctx.transformedPoint(lastX,lastY);
          ctx.translate(pt.x-dragStart.x,pt.y-dragStart.y);
          redraw();
        }
    },false);

    canvas.addEventListener('mouseup',function(evt){
        dragStart = null;
        if (!dragged) zoom(evt.shiftKey ? -1 : 1 );
    },false);

    var scaleFactor = 1.1;

    var zoom = function(clicks){
        var pt = ctx.transformedPoint(lastX,lastY);
        ctx.translate(pt.x,pt.y);
        var factor = Math.pow(scaleFactor,clicks);
        ctx.scale(factor,factor);
        ctx.translate(-pt.x,-pt.y);
        redraw();
    }

    var handleScroll = function(evt){
        var delta = evt.wheelDelta ? evt.wheelDelta/40 : evt.detail ? -evt.detail : 0;
        if (delta) zoom(delta);
        return evt.preventDefault() && false;
    };
  
    canvas.addEventListener('DOMMouseScroll',handleScroll,false);
    canvas.addEventListener('mousewheel',handleScroll,false);
}

function redraw(){
    if (!imgLoaded) return;
    // 清空画布
    var p1 = ctx.transformedPoint(0,0);
    var p2 = ctx.transformedPoint(canvas.width,canvas.height);
    ctx.clearRect(p1.x,p1.y,p2.x-p1.x,p2.y-p1.y);

    ctx.save();
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.restore();

    ctx.drawImage(gkhead,0,0);
}

// 坐标变换工具方法
function trackTransforms(ctx){
  var svg = document.createElementNS("http://www.w3.org/2000/svg",'svg');
  var xform = svg.createSVGMatrix();
  ctx.getTransform = function(){ return xform; };

  var savedTransforms = [];
  var save = ctx.save;
  ctx.save = function(){
      savedTransforms.push(xform.translate(0,0));
      return save.call(ctx);
  };

  var restore = ctx.restore;
  ctx.restore = function(){
    xform = savedTransforms.pop();
    return restore.call(ctx);
          };

  var scale = ctx.scale;
  ctx.scale = function(sx,sy){
    xform = xform.scaleNonUniform(sx,sy);
    return scale.call(ctx,sx,sy);
          };

  var rotate = ctx.rotate;
  ctx.rotate = function(radians){
      xform = xform.rotate(radians*180/Math.PI);
      return rotate.call(ctx,radians);
  };

  var translate = ctx.translate;
  ctx.translate = function(dx,dy){
      xform = xform.translate(dx,dy);
      return translate.call(ctx,dx,dy);
  };

  var transform = ctx.transform;
  ctx.transform = function(a,b,c,d,e,f){
      var m2 = svg.createSVGMatrix();
      m2.a=a; m2.b=b; m2.c=c; m2.d=d; m2.e=e; m2.f=f;
      xform = xform.multiply(m2);
      return transform.call(ctx,a,b,c,d,e,f);
  };

  var setTransform = ctx.setTransform;
  ctx.setTransform = function(a,b,c,d,e,f){
      xform.a = a;
      xform.b = b;
      xform.c = c;
      xform.d = d;
      xform.e = e;
      xform.f = f;
      return setTransform.call(ctx,a,b,c,d,e,f);
  };

  var pt  = svg.createSVGPoint();
  ctx.transformedPoint = function(x,y){
      pt.x=x; pt.y=y;
      return pt.matrixTransform(xform.inverse());
  }
}

核心修改说明

  1. 新增本地图片上传能力,通过FileReader读取本地图片为base64格式加载,完全规避跨域问题,无需写死图片路径
  2. 优化保存逻辑,导出成功后自动触发浏览器下载,增加异常捕获和状态判断,避免无效操作报错
  3. 调整初始化逻辑,仅在图片加载完成后绑定交互事件,避免空操作报错

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 08:54:07