canvas面试题-图像截取

之前发两篇关于canvas学习笔记,发的这几篇文档主要为了记录面试需求如何实现,下面是面试官提出的实现效果,上传一张图片,然后可以分区域选择头发,衣服等,通过画笔橡皮擦进行修改,最终将选择区域导出成为单独图片。

在博客上,基于上面需求做简化demo,主要记录核心功能,选区、画笔、橡皮擦、导出

项目素材

最后一张是用户上传图片source.png,前两张是上传后端返回两张图层mask1.pngmask2.png,后续操作是基于蒙版进行选区、修改导出操作。

前置条件

在实现相关功能得先学习一些基础canvas学习笔记,实现刮刮卡、在线签名、图片涂抹裁切 - 掘金 (juejin.cn)或者其他学习资料,了解画笔,画布相关概念,和canvas与图片像素点之间关系即可。

先创建一个index.html,实际上img图片和canvas绘画区域是分开的两部分内容,取消相关style可以从下图中看相关内容。

js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #prec{
            position: relative;
            border: 1px solid #000;
            width: 302px;
            height:403px;
        }
        #prec #lodCanvas{
            position: absolute;
            top: 0;
            left: 0;
             border: 1px solid #000;
        }
        #newCanvas{
            position: absolute;
            top: 0;
            left: 350px;
            border: 1px solid #000;
            
            width: 302px;
            height:403px;
        }
    </style>
</head>
<body>
    <button id="addIscoordte">添加选区</button>
    <button id="cancelIscoordte">取消选区</button>
    <button id="brush">画笔</button>
    <button id="eraser">橡皮擦</button>
    <button id="export">导出</button>
    <div id="prec">
        <canvas id="newCanvas"></canvas>
    </div>

    <script src="script.js"></script>
</body>
</html>

初始化创建

js 复制代码
const width = 302;
const height = 403;
const brushSize = 10;
//是否添加选区
var iscoordte = false;
//是否删除选区
var canceliscoordte = false;
//是否开启签名状态
var isSign = false;
//是否开启橡皮擦
var isEraser = false;
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
canvas.style.border = "1px solid #ccc";
const ctx = canvas.getContext("2d");

canvas.id = "lodCanvas";
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = brushSize;
ctx.strokeStyle =  "rgb(255 0 0/ 39%)"; //设置透明渡的颜色选取内容

var temp = document.getElementById("prec");
temp.appendChild(canvas);

//渲染背景图
ccreateimg();
//添加背景
function ccreateimg(){
    const img = document.createElement("img");
    img.src = "./test/source.png";
    img.classList.add("bg");
    img.width =width;
    img.height = height;
    document.getElementById("prec").appendChild(img);

}

因为要实现点击canvas对应区域展示该图层信息,所以需要将任务拆分

  1. 将所有图片都封装promise,并等待所有图片加载完成;
  2. 监听点击事件canvas点击位置的坐标event;
  3. 根据点击canvas对应位置的坐标event,查询那个图层有内容;
  4. 在canvas画布上绘制该图层有内容的区域;

简单绘制整个流程

根据img数组地址imgArray,封装img为promise,通过Promise.all静态方法等待所有img拿到数据imagesarr,并在获取所有图片后,通过for循环将所有图片对其进行转换canvas数组图层。

js 复制代码
/**
 * 根据点击位置判断是否绘制图层
 * 1、将多个图层设置为数组;
 * 2、循环每个图层
 * 3、创建一个临时画布
 * 4、根据 img图层/ 判断当前点位rgb是否透明
 * 5、如果不透明,则清空原画布所有内容,在canvas根据img图层绘制
 * 5.1画笔设置为半透明
 */
// 假设img数组包含了需要加载的图片URL
const imgArray = [ "./test/mask1.png","./test/mask2.png"];
// 创建一个Promise数组,每个Promise完成时都会resolve对应的图片元素
const imgpromises = imgArray.map((imgUrl) => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = (error) => reject(error);
    img.src = imgUrl;
  });
});
// 使用Promise.all()等待所有图片加载完成
 const imagesarr = Promise.all(imgpromises)
  .then((images) => {
    // 所有图片加载完成后执行的操作
    // console.log(images); // 可以在这里处理加载完成的图片数组
    let arrData = [];
    //将图片转换为canvas
    for(let i = 0; i < images.length; i++){
        const img = images[i];
        const imgcanvas = document.createElement("canvas");
        imgcanvas.width = img.width;
        imgcanvas.height = img.height;
        const imgctx = imgcanvas.getContext("2d");
        imgctx.drawImage(img, 0, 0,width,height);
        const imgData = imgctx.getImageData(0, 0, width, height);
        arrData.push(imgData);
    }
    return arrData;
  })

添加选区

执行点击事件获取,坐标和canvas图层数组信息,根据条件执行方法replaceRgb(下标)

js 复制代码
//选区事件
  canvas.onmousedown = function(e){
    selectRegion(e);
}

//默认开启选区事件,当用户点击画笔、橡皮擦、导出则不调用该方法
async function  selectRegion(e){
  // console.log("点击事件",e.offsetX,e.offsetY);
  var tempi = await iscoordinate(e.offsetX,e.offsetY,imagesarr);
  console.log(tempi);
  if(tempi ==-1){
      //选中区域没有图层
  }else if(canceliscoordte){
      //如果取消选取为false,并且ctx已经绘画图层则取消盖图层内容,不然添加该图层内容
      //判断坐标ctx是否有颜色,如果有颜色则按照图片坐标将图层改为透明
      console.log("是否进入这里");
      cancel(tempi)
  }else{
      //添加选取,可以选择多个图层
      replaceRgb(tempi)
  }
}

接下来就是进行页面绘制工作, 首先判断是选多个图层还是单个图层,如果是多个图层,则是叠加而不需要清空画布,否则调用closeCanvas函数先清空画布,然后进行绘制。

然后获取imageData图层所有坐标,通过循环每个像素点(每个像素点有rbga四个值,其中a表示是否透明),如果透明则不做操作,如果不是0,则在页面上把对应像素的绘制上(在rbg不同通道上设置固定值)。 然后将处理后的imageData通过putImageData更新到页面上。

js 复制代码
/**
 * 传入图层下标
 * 将该图层颜色替换红色
 * 绘制在canvas上
 */
function replaceRgb(index){
    //根据状态是否添加选取,iscoordte 判断是否选择清空画布
    if(!iscoordte ){
        closeCanvas();
    }

    let preimg = new Image();
    preimg.src = imgArray[index];
    preimg.onload = function(){
    ctx.drawImage(preimg,0,0,width,height);
    //  console.log("绘制成功",canvas.toDataURL());
      // 获取canvas上的像素数据
    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var pixels = imageData.data;
    var targetColor = [255, 0, 0]; // 自定义颜色,这里使用红色
    for (var i = 0; i < pixels.length; i += 4) {
        var r = pixels[i];
        var g = pixels[i + 1];
        var b = pixels[i + 2];
        var a = pixels[i + 3];
    
        // 判断当前像素是否在主题区域内
        if (r === 255 && g === 255 && b === 255  &&  a >0) {
        // 替换为自定义颜色
        pixels[i] = targetColor[0];
        pixels[i + 1] = targetColor[1];
        pixels[i + 2] = targetColor[2];
        pixels[i + 3] = 100;
        }
    }
    // 将修改后的像素数据重新绘制到canvas上
    ctx.putImageData(imageData, 0, 0);
    }
}

//清空画布
function closeCanvas(){
    //清空画布先结束之前操作
    ctx.closePath()
    ctx.clearRect(0,0,width,height);
    ctx.beginPath();
    //设置橡皮擦
    // isErase = false;
    // daubEvent();
}

添加点击事件

js 复制代码
/**
 * 点击事件
 */
document.getElementById("addIscoordte").onclick = function(){
    iscoordte = !iscoordte;
    canceliscoordte = false
    isEraser = false;
    isSign = false;
    canvas.onmousedown = null;
    canvas.onmousemove = null;
    canvas.onmouseup = null;
    
    console.log("添加选取");
    canvas.onmousedown = function (e){
        selectRegion(e);
    };

}

这样实现以下效果,然后我可以控制添加选区iscoordte控制是否加载清空,页面是否叠加

取消选区

取消选区的点击事件

js 复制代码
//去除选取
document.getElementById("cancelIscoordte").onclick = function(){
    iscoordte =false;
    canceliscoordte = true;
    //重写点击事件
    isEraser = false;
    isSign = false;
    
    canvas.onmousedown = null;
    canvas.onmousemove = null;
    canvas.onmouseup = null;
    canvas.onmousedown = function (e){
        selectRegion(e);
    };
}

取消选区通过调用selectRegion方法,确定用户点击是那个模块,它进入第二个if调用cancel方法实现取消权限功能。

cancel方法中主要进行下面几个步骤

  1. 创建一个临时画布,图片绘制到临时画布上,根据传入下标获取图层图片,获取imageData.data(像素点位置)
  2. 获取canvas目前let ctxData = ctx.getImageData(0, 0, canvas.width, canvas.height);像素点位置
  3. 循环临时画布的每个点位,判断临时画布不为空点位,对应修改ctx画布点位为透明
  4. 将修改后的像素数据重新绘制到canvas上ctx.putImageData(ctxData, 0, 0);
js 复制代码
/**
 * 取消选取
 * 判断坐标ctx是否有颜色,如果有颜色则按照图片坐标将图层改为透明
 * 1、创建一个临时画布
 * 2、将图片绘制到临时画布上
 * 3、循环临时画布的每个点位
 * 4、判断临时画布不为空点位,对应修改ctx画布点位为透明
 */

function cancel(item){
    let tempCanvas = document.createElement("canvas");
    tempCanvas.width = width;
    tempCanvas.height = height;
    let tempctx = tempCanvas.getContext("2d");
    // 声明一张图片
    let tempImg = new Image();
    tempImg.src = imgArray[item];
    tempImg.onload = function(){
        //图片添加到canvas上
        tempctx.drawImage(tempImg, 0, 0,width,height);
        //获取画布数据
        let imageData = tempctx.getImageData(0, 0, canvas.width, canvas.height);
        let pixels = imageData.data;
        // 获取canvas上的像素数据
        let ctxData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        let ctxpixels = ctxData.data;
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
            const index = (y * width + x) * 4;
            const text4a = pixels[index + 3];
                // 判断像素是否为主体区域(透明度大于0)
                if (text4a > 0 ) {
                    ctxpixels[index] = 0;   // 红色通道
                    ctxpixels[index + 1] = 0; // 绿色通道
                    ctxpixels[index + 2] = 0; // 蓝色通道
                    ctxpixels[index + 3] = 0; // 透明度
                }
            }
        }
        // 将修改后的像素数据重新绘制到canvas上
        ctx.putImageData(ctxData, 0, 0);
    }
}

画笔橡皮擦

画笔和橡皮擦点击事件,先清空已有的点击事件canvas.onmousedown = null;,调用daubEvent方法,重新赋值点击事件

js 复制代码
//画笔事件
document.getElementById("brush").onclick = function(){
    iscoordte =false;
    canceliscoordte = false;
    isSign = true;
    isEraser = false;
    //重写点击事件
    console.log("点击画笔");
    canvas.onmousedown = null;
    daubEvent();
    ctx.beginPath();
}
//橡皮擦事件
document.getElementById("eraser").onclick = function(){
    iscoordte =false;
    canceliscoordte = false;
    isSign = true;
    isEraser = true;
    //重写点击事件
    console.log("点击橡皮擦");
    canvas.onmousedown = null;
    daubEvent();
    ctx.beginPath();
}

通过ctx.globalCompositeOperation判断是橡皮擦还是画笔事件,destination-out 将两个图层重叠部分设置为null,source-over属性是在原图层基础上添加内容,通过重写了鼠标按下事件,实现选区和画笔两个功能的分割独立,当用户点击对应按钮,对应的鼠标按下事件将会不同。

js 复制代码
/**
 * 撰写画笔事件
 */
function daubEvent(){
    console.log("开始绘制");
    // 鼠标按下事件
    canvas.onmousedown = function (event) {
        if (!isSign) return;
        if (isEraser) {
            ctx.globalCompositeOperation = 'destination-out';
        }else{
            ctx.globalCompositeOperation = 'source-over';
        } 
        ctx.beginPath();
        ctx.moveTo(event.offsetX,event.offsetY);
        canvas.onmousemove = function(event) {
            if (isEraser) {
                // 设置橡皮擦粗细
                ctx.lineWidth = brushSize; 
                ctx.strokeStyle = "rgba(0, 0, 0, 1)";
                ctx.globalAlpha = 1;
            }else{
                ctx.strokeStyle =   "rgb(255 0 0/ 10%)"; 
                ctx.globalAlpha = 0.05;
            }
            ctx.lineTo(event.offsetX,event.offsetY);
            ctx.stroke();
        }
        canvas.onmouseup = function(event) {
            canvas.onmousemove = null;
            canvas.onmouseup = null;
        }
    }
}

实现效果

此时项目整体流程如下:

导出功能

导出功能点击事件,调用contrast方法将图片绘制新的canvas上面。

js 复制代码
//导出事件
document.getElementById("export").onclick = function(){
    console.log("点击导出");
    contrast()
}

导出功能:

  1. 先获取旧的canvas已经绘制图层(canvas绘制是红色)
  2. text4data 旧图层的内容 (用户涂抹所有内容,即rgb不透明)获取所有像素的
  3. 创建新的图层newCanvas, 新图层画笔newcontext
  4. 先将图片添加新图层中newcontext.drawImage(newimg, 0, 0,width,height);
  5. 循环旧的图层canvas每个像素点,计算是否透明const text4a = text4data[index + 3];
  6. 若是旧图层canvas像素点是透明,则将新图层newcanvas对应区域也设置为透明
  7. 将新图层最终结果渲染到页面上
js 复制代码
/**
 * 根据图层点位,将图片内容绘制到新的canvas上
 * text4data 旧图层的内容 (用户涂抹所有内容,即rgb不透明)获取所有像素的
 * newCanvas 创建新的图层
 * newcontext 新图层画笔
 * 1、新图层newcontext引入需要裁剪的图片
 * 2、图片加载完成,循环旧图层每个像素点,
 * 3、判断text4data旧图层像素的是否透明,如果透明则将新图层对应内容区域也修改透明
 */
function contrast() {
    const text4data = ctx.getImageData(0, 0, width, height).data;
    // 创建一个新的canvas图层
    const newCanvas = document.getElementById("newCanvas");
    const newcontext = newCanvas.getContext('2d');
    // 设置Canvas的大小与图片一致
    newCanvas.width = width;
    newCanvas.height = height;
    const newimg  = new Image();
    newimg.src = "./test/source.png";
    // 将图片绘制到Canvas上
    newcontext.drawImage(newimg, 0, 0,width,height);
    const imageData   = newcontext.getImageData(0, 0, width, height);
    const newdata = imageData.data;
    
    newimg.onload = function () {
        //循环每个像素点,如果是白色则  将其设为透明
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
            const index = (y * width + x) * 4;
    
            const text4a = text4data[index + 3];
    
                // 判断像素是否为主体区域(透明度大于0)
                if (text4a > 0 ) {
                    // const r = newdata[index];
                    // const g = newdata[index + 1];    
                    // const b = newdata[index + 2];
                    // const a = newdata[index + 3];
                }else{
                    newdata[index] = 0;   // 红色通道
                    newdata[index + 1] = 0; // 绿色通道
                    newdata[index + 2] = 0; // 蓝色通道
                    newdata[index + 3] = 0; // 透明度
                }
            }
        }
        newcontext.putImageData(imageData, 0, 0);
    }
  }

最终流程(草图)

最终效果

相关推荐
yunvwugua__12 分钟前
Python训练营打卡 Day26
前端·javascript·python
满怀101520 分钟前
【Django全栈开发实战】从零构建企业级Web应用
前端·python·django·orm·web开发·前后端分离
Darling02zjh1 小时前
GUI图形化演示
前端
Channing Lewis1 小时前
如何判断一个网站后端是用什么语言写的
前端·数据库·python
互联网搬砖老肖1 小时前
Web 架构之状态码全解
前端·架构
showmethetime1 小时前
matlab提取脑电数据的五种频域特征指标数值
前端·人工智能·matlab
左钦杨3 小时前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
NaclarbCSDN3 小时前
Java集合框架
java·开发语言·前端
进取星辰4 小时前
28、动画魔法圣典:Framer Motion 时空奥义全解——React 19 交互动效
前端·react.js·交互
不爱吃饭爱吃菜4 小时前
uniapp微信小程序-长按按钮百度语音识别回显文字
前端·javascript·vue.js·百度·微信小程序·uni-app·语音识别