前端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参加工作,专注于前端各领域技术,共同学习共同进步,一起加油呀!

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

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

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



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。