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);
    }
  }

最终流程(草图)

最终效果

相关推荐
Summer不秃2 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰6 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye13 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm15 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
乐闻x41 分钟前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚43 分钟前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
Amd7941 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You1 小时前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生1 小时前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
baiduopenmap2 小时前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图