Canvas艺术之旅:探索锚点抠图的无限可能

说在前面

在日常的图片处理中,我们经常会遇到需要抠图的情况,无论是为了美化照片、制作海报,还是进行图片合成。抠图对于我们来说也是一种很常用的功能了,今天就让我们一起来看下怎么使用canvas来实现一个锚点抠图功能。

效果展示

在这里插入图片描述

体验地址

http://jyeontu.xyz/JDemo/#/imgCut

代码实现

一、图片上传

想要进行抠图的话我们得先有图片是吧,所以要有个图片上传的功能。

1、本地图片上传

这里我们使用简单的点击按钮上传,前面也有文章介绍过了拖拽上传功能的实现,这里就不赘述了,有兴趣的可以看下这篇文章:《文件拖拽上传功能已经烂大街了,你还不会吗?》

这里我们直接使用input标签来实现上传功能即可:

<label for="file-upload" class="custom-file-upload"><i class="fas fa-cloud-upload-alt"></i> 选择文件
</label>
<inputv-show="false"id="file-upload"type="file"accept="image/*"@change="handleFileUpload"
/>

image.png

handleFileUpload(e) {let file = e.target.files[0];if (!file) return;this.srcLink = "";const reader = new FileReader();reader.onload = event => {const img = new Image();img.onload = () => {this.image = img;this.width = img.width;this.height = img.height;this.originWidth = img.width;this.originHeight = img.height;this.drawCanvas();};img.src = event.target.result;};reader.readAsDataURL(file);
}
2、在线链接图片

使用Input输入在线图片链接:

<inputtype="input"@change="inputSrc"placeholder="输入图片在线地址"v-model="srcLink"class="input-style"style="width: 100%;"
/>

image.png

getImageBase64FromURL(url, callback) {return new Promise(resove => {const xhr = new XMLHttpRequest();xhr.onload = function() {const reader = new FileReader();reader.onloadend = function() {resove(reader.result);};reader.readAsDataURL(xhr.response);};xhr.open("GET", url);xhr.responseType = "blob";xhr.send();});
},
async inputSrc() {const src = await this.getImageBase64FromURL(this.srcLink);const img = new Image();img.onload = () => {this.image = img;this.width = img.width;this.height = img.height;this.drawCanvas();};img.src = src;
}
3、将上传的图片绘制到canvas中
drawCanvas() {setTimeout(() => {if (!this.image || !this.ctx) {return;}this.ctx.clearRect(0, 0, this.width, this.height);this.ctx.save();this.ctx.translate(this.width / 2, this.height / 2);this.ctx.drawImage(this.image,-this.width / 2,-this.height / 2,this.width,this.height);this.ctx.restore();this.realPoints.forEach(point => {this.drawPoint(point.x, point.y);});this.connectPoints(); // 每次绘制canvas后连接所有点}, 100);
}

使用ctx.clearRect()方法清除整个画布,以便在重新绘制之前清空之前的内容。然后,使用ctx.save()方法保存当前的绘图状态。

通过ctx.translate()方法将绘图原点移动到画布的中心位置(this.width / 2, this.height / 2),这样可以方便地绘制图像和点的坐标。

使用ctx.drawImage()方法绘制图像,参数分别为图像对象this.image、图像左上角的x和y坐标(-this.width / 2, -this.height / 2),以及图像的宽度和高度(this.width, this.height)。这样就在画布上绘制了图像。

接着使用ctx.restore()方法恢复之前保存的绘图状态。

然后,通过forEach循环遍历this.realPoints数组中的每个点,调用this.drawPoint()方法绘制每个点。

最后,调用this.connectPoints()方法连接所有的点,以绘制线条。

二、锚点选择与撤销

1、监听鼠标点击

这里我们使用canvas来展示图片:

<canvasref="canvas"id="example-canvas":width="width":height="height"@click="canvasClick"tabindex="0"
></canvas>

image.png

监听canvas的点击事件并保存点击坐标

canvasClick(event) {if (!this.image || !this.ctx) {return;}const x = event.offsetX / (this.width / this.originWidth);const y = event.offsetY / (this.height / this.originHeight);this.points.push({ x, y }); // 将坐标添加到数组中const point = this.tranPoint({ x, y });this.drawPoint(point.x, point.y);
},
2、绘制锚点

前面我们获取到点击坐标了,这里我们需要在该坐标上绘制上锚点:

drawPoint(x, y) {// 绘制一个小圆点this.ctx.beginPath();this.ctx.arc(x, y, 4, 0, 2 * Math.PI);this.ctx.fillStyle = "red";this.ctx.fill();this.ctx.closePath();this.connectPoints(); // 每次点击后连接所有点
},

使用beginPath()方法创建路径,然后使用arc()方法绘制圆形,参数解释如下:

  • x: 圆心的x轴坐标
  • y: 圆心的y轴坐标
  • 4: 圆的半径
  • 0, 2 * Math.PI: 圆弧的起始角度和结束角度,这里表示绘制一个完整的圆

接下来设置fillStyle属性为红色,使用fill()方法填充圆形区域,并使用closePath()方法关闭路径。

3、连接锚点

用虚线将所有锚点按顺序连接起来:

connectPoints() {if (this.realPoints.length <= 1) {return;}this.ctx.beginPath();this.ctx.moveTo(this.realPoints[0].x, this.realPoints[0].y);for (let i = 1; i < this.realPoints.length; i++) {this.ctx.lineTo(this.realPoints[i].x, this.realPoints[i].y);}this.ctx.setLineDash([5, 5]);this.ctx.strokeStyle = "blue";this.ctx.lineWidth = 2;this.ctx.stroke();this.ctx.closePath();
}

如果realPoints数组长度大于1,接着使用beginPath()方法开始创建新的路径,并通过moveTo()方法将画笔移动到第一个点的位置(this.realPoints[0].x, this.realPoints[0].y)。随后使用for循环遍历realPoints数组中的每个点,使用lineTo()方法将画笔移动到下一个点的位置(this.realPoints[i].x, this.realPoints[i].y),从而连接所有的点。

在绘制线条之前,通过setLineDash()方法设置虚线的样式,这里是一个5像素的实线和5像素的空白,表示虚线的样式。然后设置线条的颜色为蓝色,线宽为2像素,最后通过stroke()方法绘制连接线条。最后使用closePath()方法关闭路径。

4、锚点撤销功能

平时我们都习惯了通过Ctrl+Z来撤销上一步操作,这里我们也加上,通过监听键盘按键事件来实现当用户按下Ctrl+Z组合键时,撤销最后一步锚点操作,也就是将锚点列表的最后一个删除即可:

document.addEventListener("keydown", event => {if (event.ctrlKey && event.key === "z") {event.preventDefault();that.undoPoint();}
});
undoPoint() {if (this.points.length > 0) {this.points.pop();this.drawCanvas();}
},
5、获取锚点集合

这里我们在右边预留了一个展示锚点列表的文本域

<textarea v-model="pointsStr" class="points-list"></textarea>
computed: {pointsStr() {return JSON.stringify(this.realPoints);}
}

image.png

image.png

大家觉得这里输出锚点集合可以做什么?这里先卖个关子,下一篇博客就会需要用到这里的锚点集合了。

三、尺寸修改

页面上我们可以对图片尺寸进行修改,便于获取不同比例下的锚点集:

1、页面图片尺寸修改
<label class="label-style"></label>
<inputtype="number"v-model="width"@input="resizeImage($event, 'width')"@keydown.ctrl.z.preventclass="input-style"
/>
<label class="label-style"></label>
<inputtype="number"v-model="height"@input="resizeImage($event, 'height')"@keydown.ctrl.z.preventclass="input-style"
/>
<label class="label-style">按比例缩放</label>
<input type="checkbox" v-model="aspectRatio" class="checkbox-style" />
resizeImageByWidth(event) {this.width = event.target.value ? parseInt(event.target.value) : null;if (this.aspectRatio && this.width) {this.height = Math.round((this.width / this.originWidth) * this.originHeight);}
},
resizeImageByHeight(event) {this.height = event.target.value ? parseInt(event.target.value) : null;if (this.aspectRatio && this.height) {this.width = Math.round((this.height / this.originHeight) * this.originWidth);}
},
resizeImage(event, dimension) {if (!this.image) {return;}if (dimension === "width") {this.resizeImageByWidth(event);} else if (dimension === "height") {this.resizeImageByHeight(event);}if (this.aspectRatio &&(!event || event.target !== document.activeElement)) {const aspectRatio = this.originWidth / this.originHeight;if (this.width && !this.height) {this.height = Math.round(this.originWidth / aspectRatio);} else if (!this.width && this.height) {this.width = Math.round(this.originHeight * aspectRatio);} else if (this.width / aspectRatio < this.height) {this.width = Math.round(this.originHeight * aspectRatio);} else {this.height = Math.round(this.originWidth / aspectRatio);}}this.$refs.canvas.width = this.width ? this.width : null;this.$refs.canvas.height = this.height ? this.height : null;this.image.width = this.width;this.image.height = this.height;this.drawCanvas();
}

根据 dimension 的值(可能是 “width” 或 “height”),调用相应的方法来调整图像的宽度或高度。

resizeImageByWidth(event) 方法用于根据给定的宽度调整图像的大小。它首先将 event.target.value 转换为整数,并将结果赋值给 this.width。然后,如果启用了纵横比 (this.aspectRatio) 并且 this.width 有值,则计算出相应的高度,使得调整后的图像与原始图像保持相同的纵横比。

resizeImageByHeight(event) 方法用于根据给定的高度调整图像的大小。它的逻辑与 resizeImageByWidth(event) 类似,只是操作的是 this.height 和宽高比的计算方式不同。

接下来,如果启用了纵横比 (this.aspectRatio) 并且没有通过键盘事件触发该方法,则根据原始图像的宽高比 (this.originWidth / this.originHeight) 进行额外的调整。具体的调整逻辑如下:

  • 如果只设置了宽度 (this.width) 而没有设置高度 (this.height),则根据原始图像的宽高比计算出相应的高度。
  • 如果只设置了高度 (this.height) 而没有设置宽度 (this.width),则根据原始图像的宽高比计算出相应的宽度。
  • 如果设置了宽度和高度,并且根据当前的宽高比计算出的宽度小于当前的高度,则根据原始图像的宽高比计算出相应的宽度。
  • 否则,根据原始图像的宽高比计算出相应的高度。

最后,根据调整后的宽度和高度,更新画布(this.$refs.canvas.widththis.$refs.canvas.height),以及图像的宽度和高度 (this.image.widththis.image.height)。然后调用 drawCanvas() 方法重新绘制画布。

2、锚点根据缩放比例进行修改

图片缩放之后,锚点位置也要进行对应的缩放。

tranPoint(point) {let { x, y } = point;x = x * (this.width / this.originWidth);y = y * (this.height / this.originHeight);return { x, y };
}

四、抠图预览

1、图片预览组件

这里我们简单编写一个图片预览弹窗组件:

<template><div><div class="preview-overlay" @click="hidePreview"><img :src="currentImage" alt="preview image" class="preview-image" /><div class="export-button" @click.stop="handleExport"><span>导出图片</span><span class="shine"></span></div></div></div>
</template>
<script>
export default {name: "previewImg",props: {imageList: {type: Array,default: () => []},currentImage: {type: String,default: ""}},data() {return {};},methods: {hidePreview() {this.$emit("close");},handleExport() {this.$emit("export", this.currentImage);}}
};
</script>
<style>
.preview-overlay {position: fixed;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.8);display: flex;justify-content: center;align-items: center;z-index: 999;
}
.preview-image {max-width: 80%;max-height: 80%;object-fit: contain;
}
.export-button {position: absolute;bottom: 20px;padding: 10px;background-color: #00aaff;color: white;border-radius: 5px;cursor: pointer;display: flex;justify-content: center;align-items: center;font-size: 16px;font-weight: bold;text-align: center;box-shadow: 0 0 10px #00aaff;overflow: hidden;
}
.export-button:hover {background-color: #00e5ff;
}
.shine {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background-image: linear-gradient(45deg,#ffffff 10%,rgba(255, 255, 255, 0) 50%,rgba(255, 255, 255, 0) 100%);animation: exportButtonShine 2s linear infinite;
}
@keyframes exportButtonShine {from {transform: rotate(0deg);}to {transform: rotate(360deg);}
}
</style>

模板部分包含了一个遮罩层和图片预览,以及一个导出按钮。当用户点击遮罩层时,会触发 hidePreview 方法,关闭预览。图片预览部分使用了动态绑定的 :src 属性来显示当前的图片,而导出按钮则绑定了 handleExport 方法,在点击时会触发导出操作。

脚本部分定义了名为 “previewImg” 的组件,其中包括了两个属性 imageListcurrentImage,分别用于接收图片列表和当前显示的图片。在方法部分,定义了 hidePreview 方法用于关闭预览,并通过 $emit 向父组件发送 “close” 事件,以通知父组件关闭预览。另外还有 handleExport 方法,用于处理导出操作,并通过 $emit 向父组件发送 “export” 事件,并传递当前图片的路径。

2、抠图操作
cutImg() {const canvas = document.createElement("canvas");const ctx = canvas.getContext("2d");if (!this.image || !ctx) {return;}const image = this.image;canvas.width = image.width;canvas.height = image.height;// 定义剪切路径const cutPath = this.realPoints;ctx.beginPath();ctx.moveTo(cutPath[0].x, cutPath[0].y);for (let i = 1; i < cutPath.length; i++) {ctx.lineTo(cutPath[i].x, cutPath[i].y);}ctx.closePath();ctx.clip();// 绘制图片ctx.drawImage(image, 0, 0, this.width, this.height);// 将Canvas元素转换为PNG图像const imgData = canvas.toDataURL("image/png");this.currentImage = imgData;this.showImg = true;
}

获取要剪切的图片对象,并根据该图片的宽度和高度设置 <canvas> 的宽度和高度。

然后,定义剪切路径,通过遍历 cutPath 数组中的点坐标,使用 ctx.lineTo() 方法绘制路径。最后使用 ctx.closePath() 方法闭合路径,并调用 ctx.clip() 方法将剪切路径应用于上下文。

接着,使用 ctx.drawImage() 方法绘制剪切后的图片。传入的参数包括原始图片对象、剪切后的起始点坐标以及剪切后的宽度和高度。

最后,使用 canvas.toDataURL() 方法将 <canvas> 元素转换为 base64 编码的 PNG 图像数据,并将该数据赋值给 imgData 变量。然后将 imgData 赋值给 currentImage 属性,将剪切后的图片显示出来(通过在模板中绑定 currentImage)。

五、导出抠图图片

downloadImg(imgData) {// 创建一个链接元素,将图像数据作为URL设置给它const link = document.createElement("a");link.download = "myImage.png";link.href = imgData;// 触发链接的下载事件link.click();
}

首先,通过 document.createElement("a") 创建一个 <a> 元素,并将该元素赋值给 link 变量。

然后,将要下载的图片的文件名设置为 “myImage.png”,可以根据实际需要修改。

接下来,将图片数据 imgData 设置为链接元素的 href 属性,这样点击链接时会下载该图片。

最后,通过调用 link.click() 方法触发链接的点击事件,从而触发下载操作。

image.png

image.png

image.png

源码地址

gitee

https://gitee.com/zheng_yongtao/jyeontu-vue-demo.git

公众号

关注公众号『前端也能这么有趣』发送 vueDemo即可获取源码。

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/218427.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【云备份】文件操作实用工具类设计

文章目录 为什么要单独设计文件工具类&#xff1f;整体实现Filesize ——文件大小stat接口 LastMTime ——最后一次修改时间LastATime —— 最后一次访问时间FileName —— 文件名称GetPostLen ——获取文件指定位置 指定长度的数据GetContnet —— 读取文件数据SetContent ——…

搜索 C. Tic-tac-toe

Problem - C - Codeforces 思路&#xff1a;搜索&#xff0c;判断合法性。从起始态用搜索进行模拟&#xff0c;这样可以避免后面判断合法性这一繁琐的步骤。用一个map进行映射当前态及对应的结果。剪枝&#xff1a;如果当前字符串已经被搜索过&#xff0c;则直接跳过去。 代码…

交换机的VRRP主备配置例子

拓朴如下&#xff1a; 主要配置如下&#xff1a; [S1] vlan batch 10 20 # interface Vlanif10ip address 10.1.1.1 255.255.255.0vrrp vrid 1 virtual-ip 10.1.1.254vrrp vrid 1 priority 200vrrp vrid 1 preempt-mode timer delay 20 # interface Vlanif20ip address 13.1.1…

最新AI创作系统ChatGPT网站运营源码、支持GPT-4-Turbo模型,图片对话识图理解,支持DALL-E3文生图

一、AI创作系统 SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧&#xff01;本系统使用NestjsVueTypescript框架技术&#xff0c;持续集成AI能力到本系统。支持OpenAI DALL-E3文生图&#xff0c;…

网页设计作业-音乐网站首页

效果图 网盘链接 链接&#xff1a;https://pan.baidu.com/s/1CO4jAOY0zk1AWTx_pC3UmA?pwdfuck 提取码&#xff1a;fuck

macos安装小软件 cmake

一&#xff0c;cmake下载主页 Download CMake 二&#xff0c;下载&#xff0c;解压&#xff0c;配置&#xff0c;编译&#xff0c;安装 0. 假设macos中已经存在了 clang和make工具 1. 通过网页下载最新的稳定版 cmake***.tar.gz 源代码 2. tar zxf cmake***.tar 3. cd cmake***…

linux的netstat命令和ss命令

1. 网络状态 State状态LISTENING监听中&#xff0c;服务端需要打开一个socket进行监听&#xff0c;侦听来自远方TCP端口的连接请求ESTABLISHED已连接&#xff0c;代表一个打开的连接&#xff0c;双方可以进行或已经在数据交互了SYN_SENT客户端通过应用程序调用connect发送一个…

雅可比矩阵(Jacobian Matrix)

假设给定一个从n维欧式空间到m维欧式空间的变换: 雅可比矩阵就是将一阶偏导数排列成一个m行、n列形式的矩阵&#xff0c;记作&#xff1a; 举一个例子&#xff1a; 雅可比矩阵等于&#xff1a;

解决视口动画插件jquery.aniview.js使用animate.css时无效的问题(最新版本网页视口动画插件的使用及没作用、没反应)

当网站页面元素进入视口时自动应用过渡效果。CSS过渡效果可以为网页添加动画效果&#xff0c;并提供了一种平滑的转换方式&#xff0c;使元素的变化更加流畅和生动。而通过jQuery插件来获取页面滚动位置决定合适调用动画效果。 一、官网 animate.css官网 一款强大的预设css3动…

Mybatis-Plus 租户使用

Mybatis-Plus 租户使用 文章目录 Mybatis-Plus 租户使用一. 前言1.1 租户存在的意义1.2 租户框架 二. Mybatis-plus 租户2.1 租户处理器2.2 前置准备1. 依赖2. 表及数据准备3. 代码生成器 2.3 使用 三. 深入使用3.1 前言3.2 租户主体设值&#xff0c;取值3.3 部分表全量db操作3…

基于 STM32 的温度测量与控制系统设计

本文介绍了如何基于 STM32 微控制器设计一款温度测量与控制系统。首先&#xff0c;我们将简要介绍 STM32 微控制器的特点和能力。接下来&#xff0c;我们将详细讨论温度传感器的选择与接口。然后&#xff0c;我们将介绍如何使用 STM32 提供的开发工具和相关库来进行温度测量和控…

PostgreSQL 分区表插入数据及报错:子表明明存在却报不存在以及column “xxx“ does not exist 解决方法

PostgreSQL 分区表插入数据及报错&#xff1a;子表明明存在却报不存在以及column “xxx“ does not exist 解决方法 问题1. 分区表需要先创建子表在插入&#xff0c;创建子表立马插入后可能会报错子表不存在&#xff1b;解决&#xff1a; 创建子表及索引后&#xff0c;sleep10毫…