前端canvas实现图片涂鸦(Vue2、Vue3都支持)

先看一下效果图吧

代码组成:画笔大小、颜色、工具按钮都是组件,通俗易懂,可以按照自己的需求调整。

主要代码App.vue
<template><div class="page"><div class="main"><div id="canvas_panel"><canvas id="canvas" :style="{ backgroundImage: `url(${backgroundImage})`, backgroundSize: 'cover', backgroundPosition: 'center' }">当前浏览器不支持canvas。</canvas></div></div><div class="footer"><BrushSize :size="brushSize" @change-size="onChangeSize" /><ColorPicker :color="brushColor" @change-color="onChangeColor" /><ToolBtns :tool="brushTool" @change-tool="onChangeTool" /></div></div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import BrushSize from './components/BrushSize.vue';
import ColorPicker from './components/ColorPicker.vue';
import ToolBtns from './components/ToolBtns.vue';let canvas = null;
let context = null;
let painting = false;
const historyData = []; // 存储历史数据,用于撤销
const brushSize = ref(5); // 笔刷大小
const brushColor = ref('#000000'); // 笔刷颜色
const brushTool = ref('brush');
// canvas相对于(0, 0)的偏移,用于计算鼠标相对于canvas的坐标
const canvasOffset = {left: 0,top: 0,
};
const backgroundImage = ref('https://t7.baidu.com/it/u=1819248061,230866778&fm=193&f=GIF'); // 默认背景图为空function changeBackground(imgUrl) {backgroundImage.value = imgUrl;
}
function initCanvas() {function resetCanvas() {const elPanel = document.getElementById('canvas_panel');canvas.width = elPanel.clientWidth;canvas.height = elPanel.clientHeight;context = canvas.getContext('2d', { willReadFrequently: true }); // 添加这一行context.fillStyle = 'white';context.fillRect(0, 0, canvas.width, canvas.height);context.fillStyle = 'black';getCanvasOffset(); // 更新画布位置}resetCanvas();window.addEventListener('resize', resetCanvas);
}
// 获取canvas的偏移值
function getCanvasOffset() {const rect = canvas.getBoundingClientRect();canvasOffset.left = rect.left * (canvas.width / rect.width); // 兼容缩放场景canvasOffset.top = rect.top * (canvas.height / rect.height);
}
// 计算当前鼠标相对于canvas的坐标
function calcRelativeCoordinate(x, y) {return {x: x - canvasOffset.left,y: y - canvasOffset.top,};
}function downCallback(event) {// 先保存之前的数据,用于撤销时恢复(绘制前保存,不是绘制后再保存)const data = context.getImageData(0, 0, canvas.width, canvas.height);saveData(data);const { clientX, clientY } = event;const { x, y } = calcRelativeCoordinate(clientX, clientY);context.beginPath();context.moveTo(x, y);context.lineWidth = brushSize.value;context.strokeStyle = brushTool.value === 'eraser' ? '#FFFFFF' : brushColor.value;painting = true;
}
function moveCallback(event) {if (!painting) {return;}const { clientX, clientY } = event;const { x, y } = calcRelativeCoordinate(clientX, clientY);context.lineTo(x, y);context.stroke();
}
function closePaint() {painting = false;
}
function updateCanvasOffset() {getCanvasOffset(); // 重新计算画布的偏移值
}
onMounted(() => {canvas = document.getElementById('canvas');if (canvas.getContext) {context = canvas.getContext('2d', { willReadFrequently: true });initCanvas();// window.addEventListener('resize', updateCanvasPosition);window.addEventListener('scroll', updateCanvasOffset); // 添加滚动条滚动事件监听器getCanvasOffset();context.lineGap = 'round';context.lineJoin = 'round';canvas.addEventListener('mousedown', downCallback);canvas.addEventListener('mousemove', moveCallback);canvas.addEventListener('mouseup', closePaint);canvas.addEventListener('mouseleave', closePaint);}toolClear()
});function onChangeSize(size) {brushSize.value = size;
}
function onChangeColor(color) {brushColor.value = color;
}
function onChangeTool(tool) {brushTool.value = tool;switch (tool) {case 'clear':toolClear();break;case 'undo':toolUndo();break;case 'save':toolSave();break;}
}
function toolClear() {context.clearRect(0, 0, canvas.width, canvas.height);resetToolActive();
}
function toolSave() {const imageDataUrl = canvas.toDataURL('image/png');console.log(imageDataUrl)// const imgUrl = canvas.toDataURL('image/png');// const el = document.createElement('a');// el.setAttribute('href', imgUrl);// el.setAttribute('target', '_blank');// el.setAttribute('download', `graffiti-${Date.now()}`);// document.body.appendChild(el);// el.click();// document.body.removeChild(el);// resetToolActive();
}
function toolUndo() {if (historyData.length <= 0) {resetToolActive();return;}const lastIndex = historyData.length - 1;context.putImageData(historyData[lastIndex], 0, 0);historyData.pop();resetToolActive();
}
// 存储数据
function saveData(data) {historyData.length >= 50 && historyData.shift(); // 设置储存上限为50步historyData.push(data);
}
// 清除、撤销、保存状态不需要保持,操作完后恢复笔刷状态
function resetToolActive() {setTimeout(() => {brushTool.value = 'brush';}, 1000);
}
</script><style scoped>
.page {display: flex;flex-direction: column;width: 1038px;height: 866px;
}.main {flex: 1;
}.footer {display: flex;justify-content: space-around;align-items: center;height: 88px;background-color: #fff;
}#canvas_panel {margin: 12px;height: calc(100% - 24px);/* 消除空格影响 */font-size: 0;background-color: #fff;box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
}#canvas {cursor: crosshair;/* background: url('https://t7.baidu.com/it/u=1819248061,230866778&fm=193&f=GIF') no-repeat !important; */}
</style>
接下来就是三个组件
BrushSize.vue(画笔大小)
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({size: {type: Number,default: 5,},
});
const brushSize = computed(() => props.size);
</script><template><div class="wrap-range"><!-- 为了不在子组件中变更值,不用v-model --><inputtype="range":value="brushSize"min="1"max="30"title="调整笔刷粗细"@change="event => $emit('change-size', +event.target.value)"/></div>
</template><style scoped>
.wrap-range input {width: 150px;height: 20px;margin: 0;transform-origin: 75px 75px;border-radius: 15px;-webkit-appearance: none;appearance: none;outline: none;position: relative;
}.wrap-range input::after {display: block;content: '';width: 0;height: 0;border: 5px solid transparent;border-right: 150px solid #00ccff;border-left-width: 0;position: absolute;left: 0;top: 5px;border-radius: 15px;z-index: 0;
}.wrap-range input[type='range']::-webkit-slider-thumb,
.wrap-range input[type='range']::-moz-range-thumb {-webkit-appearance: none;
}.wrap-range input[type='range']::-webkit-slider-runnable-track,
.wrap-range input[type='range']::-moz-range-track {height: 10px;border-radius: 10px;box-shadow: none;
}.wrap-range input[type='range']::-webkit-slider-thumb {-webkit-appearance: none;height: 20px;width: 20px;margin-top: -1px;background: #ffffff;border-radius: 50%;box-shadow: 0 0 8px #00ccff;position: relative;z-index: 999;
}
</style>
ColorPicker.vue(颜色)
<script setup>
import { ref, computed } from 'vue';const props = defineProps(['color']);
const emit = defineEmits(['change-color']);const colorList = ref(['#000000', '#808080', '#FF3333', '#0066FF', '#FFFF33', '#33CC66']);const colorSelected = computed(() => props.color);function onChangeColor(color) {emit('change-color', color);
}
</script><template><div><spanv-for="(color, index) of colorList"class="color-item":class="{ active: colorSelected === color }":style="{ backgroundColor: color }":key="index"@click="onChangeColor(color)"></span></div>
</template><style scoped>
.color-item {display: inline-block;width: 32px;height: 32px;margin: 0 4px;box-sizing: border-box;border: 4px solid white;box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);cursor: pointer;transition: 0.3s;
}
.color-item.active {box-shadow: 0 0 15px #00ccff;
}
</style>
ToolBtns.vue(按钮)
<script setup>
import { ref, computed } from 'vue';const props = defineProps({tool: {type: String,default: 'brush',},
});
const emit = defineEmits(['change-tool']);const toolSelected = computed(() => props.tool);const toolList = ref([{ name: 'brush', title: '画笔', icon: 'icon-qianbi' },{ name: 'eraser', title: '橡皮擦', icon: 'icon-xiangpi' },{ name: 'clear', title: '清空', icon: 'icon-qingchu' },{ name: 'undo', title: '撤销', icon: 'icon-chexiao' },{ name: 'save', title: '保存', icon: 'icon-fuzhi' },
]);function onChangeTool(tool) {emit('change-tool', tool);
}
</script><template><div class="tools"><buttonv-for="item of toolList":class="{ active: toolSelected === item.name }":title="item.title"@click="onChangeTool(item.name)"><i :class="['iconfont', item.icon]"></i></button></div>
</template><style scoped>
.tools button {/* border-radius: 50%; */width: 32px;height: 32px;background-color: rgba(255, 255, 255, 0.7);border: 1px solid #eee;outline: none;cursor: pointer;box-sizing: border-box;margin: 0 8px;padding: 0;text-align: center;color: #ccc;box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);transition: 0.3s;
}.tools button.active,
.tools button:active {/* box-shadow: 0 0 15px #00CCFF; */color: #00ccff;
}.tools button i {font-size: 20px;
}
</style>

🐱 个人主页:TechCodeAI启航,公众号:SHOW科技

🙋‍♂️ 作者简介:2020参加工作,专注于前端各领域技术,共同学习共同进步,一起加油呀!

💫 优质专栏:前端主流技术分享

📢 资料领取:前端进阶资料可以找我免费领取

🔥 摸鱼学习交流:我们的宗旨是在「工作中摸鱼,摸鱼中进步」,期待大佬一起来摸鱼!

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

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

相关文章

动态规划part01 Day41

动态规划算法解题步骤 确定dp数组&#xff08;dp table&#xff09;以及下标的含义确定递推公式dp数组如何初始化确定遍历顺序举例推导dp数组 LC509斐波那契数 LC70爬楼梯 LC746使用最小花费爬楼梯 dp[]含义&#xff1a;爬到第i层楼梯的最小花费

B站尚硅谷git学习记录

文章目录 一、Git概述1.何为版本控制2.为什么需要版本控制3.版本控制工具 二、Git常用命令1.设置用户签名1.1 基本语法1.2 案例实操 2.初始化本地库2.1 基本语法2.2 案例实操 3.查看本地库状态3.1基本语法3.2 案例实操&#xff08;1&#xff09;首次查看&#xff08;工作区没有…

TinyEngine 低代码引擎:带你5分钟高效构建游戏登录界面

本文由体验技术团队 TinyEngine 项目成员李旭宏创作&#xff0c;欢迎大家实操体验&#xff0c;本体验项目基于 TinyEngine 低代码引擎提供的环境&#xff0c;通过体验简单拖、拉、拽的形式帮助开发者快速了解低代码引擎的使用流程&#xff0c;达到快速开发游戏登录界面的效果。…

143.栈和队列:用队列实现栈(力扣)

题目描述 代码解决 class MyStack { public:queue<int> que; // 定义一个队列用于实现栈// 构造函数&#xff0c;初始化队列MyStack() {}// 向栈中推入元素 xvoid push(int x) {que.push(x); // 使用队列的 push 方法将元素 x 添加到队列尾部}// 从栈中弹出并返回栈顶元…

【MIT 6.5840(6.824)学习笔记】GFS

1 分布式存储系统难点 在设计大型分布式系统或存储系统时&#xff0c;初衷通常是为了获得显著的性能提升&#xff0c;通过数百台计算机的资源来并行完成大量工作。因此&#xff0c;性能问题成为最初的关注点。一个自然的想法是将数据分片&#xff08;Sharding&#xff09;&…

《数字图像处理》笔记/期末复习资料

目录 1 简述二值图像、灰度图像与彩色图像间的区别。 2 图像量化时&#xff0c;如果量化级比较小会出现什么现象&#xff1f;为什么&#xff1f; 3 图像增强的目的是什么&#xff1f; 4 什么是中值滤波&#xff0c;有何特点&#xff1f; 5 叙述高通滤波、低通滤波、带通滤…

区块链技术引领:Web3时代的新网络革命

随着区块链技术的快速发展和不断成熟&#xff0c;人们已经开始意识到它所带来的潜在影响&#xff0c;尤其是在构建一个更加去中心化、安全和透明的互联网时。这个新的互联网时代被称为Web3&#xff0c;它将不再受制于传统的中心化平台&#xff0c;而是更多地依赖于去中心化的网…

Rust最新版安装(v1.78.0+)

系统&#xff1a;Windows 11 专业版 23H2rustc&#xff1a;1.78.0 配置环境变量和设置配置文件 新建文件夹“C:\Rust\Rustup”和“C:\Rust\Cargo”。【以管理员身份运行】打开CMD 设置系统环境变量&#xff0c;如下设置RUSTUP_DIST_SERVER&#xff0c;其余同理 C:\Windows\S…

Golang | Leetcode Golang题解之第103题二叉树的锯齿形层序遍历

题目&#xff1a; 题解&#xff1a; func zigzagLevelOrder(root *TreeNode) (ans [][]int) {if root nil {return}queue : []*TreeNode{root}for level : 0; len(queue) > 0; level {vals : []int{}q : queuequeue nilfor _, node : range q {vals append(vals, node.V…

物联网架构实例—Ubuntu 安装MySQL

1.ubuntu安装mysql apt-get upgrade apt-get update 安装mysql apt-get install mysql-server Y执行安装后&#xff0c;会来到软件包设置界面. 再次确认设置的密码. 开启mysql的服务 service mysql start 确认是否成功开启mysql service mysql status 确认是否启动成功&a…

Java Object类方法介绍

Object作为顶级类&#xff0c;所有的类都实现了该类的方法&#xff0c;包括数组。 查询Java文档&#xff1a; 1、object.eauqls(): 其作用与 有些类似。 &#xff1a; 是一个比较运算符&#xff0c;而不是一个方法。 ①可以判断基本类型&#xff0c;也可以判断引用类型。 ②若…

gitLab 使用tortoiseGit 克隆新项目 一直提示tortoiseGitPlink输入密码 输完也不生效

问题描述&#xff1a;准备用TortoiseGit拉取gitlab上一个新项目代码&#xff0c;出现tortoiseGitPlink提示让输入密码&#xff0c;输入后又弹出&#xff0c;反复几次&#xff0c;无法down下来代码。 解决方案&#xff1a; 1.找到PuTTYgen工具&#xff0c;打开 2. 点击load 按钮…

山脉数组的峰顶索引 ---- 二分查找

题目链接 题目: 分析: 我们很明显, 可以从峰值位置将数组分成两段, 具有"二段性", 所以可以用二分查找因为arr是山峰数组, 不存在相等的情况如果arr[mid] > arr[mid 1], 说明mid的位置可能是峰值, 移动right mid如果arr[mid] < arr[mid 1], 说明mid的位置…

神奇的一万

在代码界&#xff0c;有个神奇的存在&#xff0c;它叫一万&#xff1a;eval&#xff08;&#xff09;。 这个神奇的一万&#xff0c;在python和JavaScript中都存在&#xff0c;作用也是基本相同的。 Python中的eval函数能将字符串str当成有效的表达式来求值并返回计算结果。 …

vue contextPath的思考

先说我这边的情况&#xff0c;目前项目都是前后端分离开发的&#xff0c;上线有种部署方式&#xff0c;常见的就是前后端分开部署&#xff0c;这是比较常见的&#xff0c;我这边因客户原因&#xff0c;打包一起进行部署比较简单&#xff0c;交付技术运维部方便后期其他现场部署…

docker ps显示的参数具体是什么意思

1&#xff0c;运行一个容器 docker run -d ubuntu:15.10 /bin/sh -c "while true; do echo hello world; sleep 1; done"这段命令的作用是使用 docker run 命令运行一个基于 ubuntu:15.10 镜像的 Docker 容器&#xff0c;并在容器中执行一个无限循环的命令。 具体解…

10Django项目--用户管理系统--改

对应视频链接点击直达 10Django项目--用户管理系统--改 对应视频链接点击直达改a&#xff0c;本质b&#xff0c;修改(更新) 页面相关a&#xff0c;index页面新增操作按钮b&#xff0c;修改(更新)页面c&#xff0c;路由和函数 OVER&#xff0c;不会有人不会吧不会的加Q139400651…

基于jeecgboot-vue3的Flowable流程-我的任务(一)

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 1、首先可以用现成生成代码的前端来做这个&#xff0c;只要做一些调整就可以了&#xff0c;这样利用现有的一些模板可以快速构建我的任务&#xff0c;否则vue2与vue3相差太大&#xff0c;移…

质量人,你还在等什么?快来六西格玛培训公司充电吧!——张驰咨询

在竞争激烈的商业环境中&#xff0c;质量成为了企业生存和发展的关键。而六西格玛&#xff0c;作为一种全球公认的质量管理方法论&#xff0c;正在成为越来越多企业追求品质革命的重要工具。而六西格玛培训公司&#xff0c;则成为了这场品质革命中&#xff0c;质量人不可或缺的…

菊花链通信技术整理

目录 一、菊花链简介 二、菊花链与CAN通信的区别 三、常见的菊花链AFE芯片 四、菊花链数据结构 五、菊花链方案介绍 一、菊花链简介 首先简单的说一下菊花链以及菊花链的应用&#xff0c;在目前国内的BMS开发中&#xff0c;我们应用最广泛的目前还还是分布式&#xff0c;…