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>

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

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

相关推荐
程序员龙语18 小时前
CSS 文本样式与阴影属性
前端·css
LYFlied18 小时前
【每日算法】LeetCode 152. 乘积最大子数组(动态规划)
前端·算法·leetcode·动态规划
狼与自由18 小时前
excel 导入 科学计数法问题处理
java·前端·excel
TAEHENGV18 小时前
导入导出模块 Cordova 与 OpenHarmony 混合开发实战
android·javascript·数据库
小徐_233319 小时前
不如摸鱼去的 2025 年终总结,今年的关键词是直面天命
前端·年终总结
GISer_Jing19 小时前
交互式圣诞树粒子效果:手势控制+图片上传
前端·javascript
38242782719 小时前
CSS 选择器(CSS Selectors) 的完整规则汇总
前端·css
放逐者-保持本心,方可放逐19 小时前
PDFObject 在 Vue 项目中的应用实例详解
前端·javascript·vue.js
龙仔CLL19 小时前
vue2项目使用zoom解决pc端浏览器百分比缩放,布局样式不兼容问题
vue.js·html·zoom
一 乐19 小时前
养老院信息|基于springboot + vue养老院信息管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端