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>

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

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

相关推荐
前端摸鱼匠1 小时前
Vue 3 的v-bind合并行为:讲解v-bind与普通属性合并的规则
前端·javascript·vue.js·前端框架·ecmascript
REDcker2 小时前
浏览器端Web程序性能分析与优化实战 DevTools指标与工程清单
开发语言·前端·javascript·vue·ecmascript·php·js
donecoding3 小时前
一个 sudo 引发的血案:npm 全局包权限错乱彻底修复
前端·node.js·前端工程化
风骏时光牛马3 小时前
Raku正则匹配与数据批量处理实操案例
前端
nbwenren3 小时前
2026实测:Gemini 3 镜像站视觉能力实践——拍照原型图,一键生成 HTML+CSS 代码
前端·css·html
Lee川3 小时前
Prisma 实战指南:像搭积木一样设计古诗词数据库
前端·数据库·后端
Linsk3 小时前
Java和JavaScript的关系真是雷峰和雷峰塔的关系吗?
java·javascript·oracle
当时只道寻常4 小时前
浏览器文本复制到剪贴板:企业级最佳实践
javascript
jinanwuhuaguo4 小时前
(第二十九篇)OpenClaw 实时与具身的跃迁——从异步孤岛到数字世界的“原住民”
前端·网络·人工智能·重构·openclaw