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>

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

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

相关推荐
字节架构前端3 分钟前
媒体采集标准草案 与 Chromium 音频采集实现简介
前端·chrome·音视频开发
奋斗的小青年!!4 分钟前
Flutter在OpenHarmony上实现渐变文字动画的深度优化实践
前端·flutter·harmonyos·鸿蒙
梦6508 分钟前
Vue3 计算属性 (computed) 与监听属性 (watch)
前端·javascript·vue.js
六月June June22 分钟前
leaflet L.popup().setContent中挂载vue组件
前端·javascript·vue.js
软件开发技术深度爱好者25 分钟前
JavaScript的p5.js库使用详解(上)
开发语言·javascript
首席拯救HMI官25 分钟前
【拯救HMI】HMI容错设计:如何减少操作失误并快速纠错?
大数据·运维·前端·javascript·网络·学习
深蓝电商API28 分钟前
Scrapy与Splash结合爬取JavaScript渲染页面
javascript·爬虫·python·scrapy
m0_7482546632 分钟前
Vue.js 模板语法基础
前端·vue.js·flutter
donecoding34 分钟前
AI时代程序员的护城河:让AI做创意组合,用标准化工具守住质量底线
javascript·架构·代码规范
PBitW39 分钟前
和AI浅聊了一下SEO —— 真神Astro
前端·seo