vue+canvas实现按下鼠标绘制箭头

需要实现的效果:

直接上代码:

html 复制代码
<template>
    <div class="arrow-canvas-container">
        <canvas 
            ref="canvas"
            class="arrow-canvas"
            :width="canvasWidth"
            :height="canvasHeight"
            @mousedown="handleMouseDown"
            @mousemove="handleMouseMove"
            @mouseup="handleMouseUp"
            @touchstart="handleTouchStart"
            @touchmove="handleTouchMove"
            @touchend="handleTouchEnd"
        ></canvas>
    </div>
</template>

<script>
export default {
    name: 'ArrowCanvas',
    emits: ['arrow-drawn', 'canvas-cleared'],
    props: {
        width: {
            type: Number,
            default: 800
        },
        height: {
            type: Number,
            default: 600
        },
        lineColor: {
            type: String,
            default: '#3b82f6'
        },
        lineWidth: {
            type: Number,
            default: 3
        },
        arrowStyle: {
            type: String,
            default: 'single',
            validator: (value) => ['single', 'double'].includes(value)
        },
        showGrid: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            canvas: null,
            ctx: null,
            isDrawing: false,
            startPoint: null,
            endPoint: null,
            arrows: []
        }
    },
    computed: {
        canvasWidth() {
            return this.width;
        },
        canvasHeight() {
            return this.height;
        }
    },
    mounted() {
        this.canvas = this.$refs.canvas;
        this.ctx = this.canvas.getContext('2d');
        this.initCanvas();
    },
    methods: {
        initCanvas() {
            this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
            if (this.showGrid) {
                this.drawGrid();
            }
        },

        handleMouseDown(event) {
            this.startDrawing(this.getCanvasCoordinates(event));
        },

        handleMouseMove(event) {
            this.drawing(this.getCanvasCoordinates(event));
        },

        handleMouseUp(event) {
            this.stopDrawing(this.getCanvasCoordinates(event));
        },

        handleTouchStart(event) {
            event.preventDefault();
            const touch = event.touches[0];
            this.startDrawing(this.getCanvasCoordinates(touch));
        },

        handleTouchMove(event) {
            event.preventDefault();
            const touch = event.touches[0];
            this.drawing(this.getCanvasCoordinates(touch));
        },

        handleTouchEnd(event) {
            event.preventDefault();
            this.stopDrawing();
        },

        getCanvasCoordinates(event) {
            const rect = this.canvas.getBoundingClientRect();
            return {
                x: event.clientX - rect.left,
                y: event.clientY - rect.top
            };
        },

        startDrawing(point) {
            this.isDrawing = true;
            this.startPoint = point;
            this.endPoint = point;
        },

        drawing(point) {
            if (!this.isDrawing || !this.startPoint) return;

            this.endPoint = point;
            this.redraw();
        },

        stopDrawing() {
            if (this.isDrawing && this.startPoint && this.endPoint) {
                const arrow = {
                    start: { ...this.startPoint },
                    end: { ...this.endPoint },
                    color: this.lineColor,
                    width: this.lineWidth,
                    style: this.arrowStyle,
                    timestamp: Date.now()
                };

                this.arrows.push(arrow);
                this.$emit('arrow-drawn', arrow);
            }

            this.isDrawing = false;
            this.startPoint = null;
            this.endPoint = null;
        },

        redraw() {
            this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
            
            if (this.showGrid) {
                this.drawGrid();
            }
            
            this.arrows.forEach(arrow => {
                this.drawSingleArrow(arrow);
            });
            
            if (this.isDrawing && this.startPoint && this.endPoint) {
                this.drawSingleArrow({
                    start: this.startPoint,
                    end: this.endPoint,
                    color: this.lineColor,
                    width: this.lineWidth,
                    style: this.arrowStyle
                });
            }
        },

        drawSingleArrow(arrow) {
            const { start, end, color, width, style } = arrow;
            const headLength = 15;
            const angle = Math.atan2(end.y - start.y, end.x - start.x);
            
            this.ctx.save();
            this.ctx.strokeStyle = color;
            this.ctx.fillStyle = color;
            this.ctx.lineWidth = width;
            this.ctx.lineCap = 'round';
            
            // 绘制线条
            this.ctx.beginPath();
            this.ctx.moveTo(start.x, start.y);
            this.ctx.lineTo(end.x, end.y);
            this.ctx.stroke();
            
            // 绘制箭头头部
            this.ctx.beginPath();
            this.ctx.moveTo(end.x, end.y);
            this.ctx.lineTo(
                end.x - headLength * Math.cos(angle - Math.PI / 6),
                end.y - headLength * Math.sin(angle - Math.PI / 6)
            );
            this.ctx.lineTo(
                end.x - headLength * Math.cos(angle + Math.PI / 6),
                end.y - headLength * Math.sin(angle + Math.PI / 6)
            );
            this.ctx.closePath();
            this.ctx.fill();
            
            // 双箭头样式
            if (style === 'double') {
                this.ctx.beginPath();
                this.ctx.moveTo(start.x, start.y);
                this.ctx.lineTo(
                    start.x + headLength * Math.cos(angle - Math.PI / 6),
                    start.y + headLength * Math.sin(angle - Math.PI / 6)
                );
                this.ctx.lineTo(
                    start.x + headLength * Math.cos(angle + Math.PI / 6),
                    start.y + headLength * Math.sin(angle + Math.PI / 6)
                );
                this.ctx.closePath();
                this.ctx.fill();
            }
            
            this.ctx.restore();
        },

        drawGrid() {
            this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.08)';
            this.ctx.lineWidth = 1;
            
            const gridSize = 20;
            
            // 垂直线
            for (let x = 0; x <= this.canvasWidth; x += gridSize) {
                this.ctx.beginPath();
                this.ctx.moveTo(x, 0);
                this.ctx.lineTo(x, this.canvasHeight);
                this.ctx.stroke();
            }
            
            // 水平线
            for (let y = 0; y <= this.canvasHeight; y += gridSize) {
                    this.ctx.beginPath();
                    this.ctx.moveTo(0, y);
                    this.ctx.lineTo(this.canvasWidth, y);
                    this.ctx.stroke();
                }
            },

            clearCanvas() {
                this.arrows = [];
                this.initCanvas();
                this.$emit('canvas-cleared');
        },

        undo() {
            if (this.arrows.length > 0) {
                this.arrows.pop();
                this.redraw();
            }
        },

        exportImage() {
                return this.canvas.toDataURL('image/png');
            }
        },

        watch: {
            lineColor() {
                this.redraw();
            },
            lineWidth() {
                this.redraw();
            },
            arrowStyle() {
                this.redraw();
            },
            showGrid() {
                this.redraw();
            }
        }
    };
</script>

<style scoped>
.arrow-canvas-container {
    position: relative;
    width: 100%;
    height: 100%;
}

.arrow-canvas {
    border: 2px dashed #d1d5db;
    border-radius: 12px;
    background-color: white;
    cursor: crosshair;
    transition: all 0.3s ease;
}

.arrow-canvas:hover {
    border-color: #3b82f6;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
</style>

以上代码可以直接复制使用~

如有问题,欢迎留言讨论~

相关推荐
hpoenixf1 天前
2026 年前端面试问什么
前端·面试
还是大剑师兰特1 天前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷1 天前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian1 天前
前端node常用配置
前端
华洛1 天前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq1 天前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A1 天前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常1 天前
被EdgeToEdge适配折磨疯了,谁懂!
前端
小码哥_常1 天前
从Groovy到KTS:Android Gradle脚本的华丽转身
前端
灵感__idea1 天前
Hello 算法:复杂问题的应对策略
前端·javascript·算法