前端canvas实现图片涂鸦(Vue2、Vue3都支持)
TechCodeAI启航 2024-07-30 08:33:01 阅读 71
先看一下效果图吧
Gitee地址
代码组成:画笔大小、颜色、工具按钮都是组件,通俗易懂,可以按照自己的需求调整。
主要代码App.vue
<code><template>
<div class="page">code>
<div class="main">code>
<div id="canvas_panel">code>
<canvas id="canvas" :style="{ backgroundImage: `url(${backgroundImage})`, backgroundSize: 'cover', backgroundPosition: 'center' }">当前浏览器不支持canvas。</canvas>code>
</div>
</div>
<div class="footer">code>
<BrushSize :size="brushSize" @change-size="onChangeSize" />code>
<ColorPicker :color="brushColor" @change-color="onChangeColor" />code>
<ToolBtns :tool="brushTool" @change-tool="onChangeTool" />code>
</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">code>
<!-- 为了不在子组件中变更值,不用v-model -->
<input
type="range"code>
:value="brushSize"code>
min="1"code>
max="30"code>
title="调整笔刷粗细"code>
@change="event => $emit('change-size', +event.target.value)"code>
/>
</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,code>
.wrap-range input[type='range']::-moz-range-thumb {code>
-webkit-appearance: none;
}
.wrap-range input[type='range']::-webkit-slider-runnable-track,code>
.wrap-range input[type='range']::-moz-range-track {code>
height: 10px;
border-radius: 10px;
box-shadow: none;
}
.wrap-range input[type='range']::-webkit-slider-thumb {code>
-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>
<span
v-for="(color, index) of colorList"code>
class="color-item"code>
:class="{ active: colorSelected === color }"code>
:style="{ backgroundColor: color }"code>
:key="index"code>
@click="onChangeColor(color)"code>
></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">code>
<button
v-for="item of toolList"code>
:class="{ active: toolSelected === item.name }"code>
:title="item.title"code>
@click="onChangeTool(item.name)"code>
>
<i :class="['iconfont', item.icon]"></i>code>
</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参加工作,专注于前端各领域技术,共同学习共同进步,一起加油呀!
💫 优质专栏:前端主流技术分享
📢 资料领取:前端进阶资料可以找我免费领取
🔥 摸鱼学习交流:我们的宗旨是在「工作中摸鱼,摸鱼中进步」,期待大佬一起来摸鱼!
上一篇: Nuxt.js必读:轻松掌握运行时配置与 useRuntimeConfig
下一篇: SpringBoo+vue3整合讯飞星火3.5通过webscoket实现聊天功能(全网首发)附带展示效果
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。