在线绘图小工具

在线绘图小工具

    • 文章说明
    • 程序源码
    • 功能展示
    • 源码下载

文章说明

本文主要是在看了袁老师的canvas绘图小视频后所写,记录一个简单的canvas绘图功能,并学习一下较为传统的JavaScript事件写法,同时了解一下拖拽事件的更合理写法,等待后续将头像上传功能进行优化。

参考教程

程序源码

主要为核心的绘图类Shape.js,以及页面和事件,在App.vue里面

Shape.js

export class Shape {
    constructor(color, startX, startY, dpr) {
        this.endX = this.startX;
        this.endY = this.startY;
        this.color = color;
        this.startX = startX;
        this.startY = startY;
        this.dpr = dpr;
    }

    get minX() {
        return Math.min(this.startX, this.endX);
    }

    get maxX() {
        return Math.max(this.startX, this.endX);
    }

    get minY() {
        return Math.min(this.startY, this.endY);
    }

    get maxY() {
        return Math.max(this.startY, this.endY);
    }

    draw(ctx) {
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.strokeStyle = "#fff";
        ctx.lineWidth = 3 * this.dpr;
        ctx.lineCap = "square";
        ctx.stroke();
    }

    drawDashed(ctx) {
        ctx.strokeStyle = "#fff";
        ctx.lineWidth = 1 * this.dpr;
        ctx.lineCap = "square";
        ctx.stroke();
    }

    inSide(x, y) {
        return (this.minX <= x && this.maxX >= x) && (this.minY <= y && this.maxY >= y);
    }

    // 上1、下2、左4、右8
    // 得到上1、下2、左4、左上5、左下6、右8、右上9、右下10
    inGap(gap, x, y) {
        let result = 0;
        if (Math.abs(this.minY - y) <= gap && (this.minX - gap <= x && this.maxX + gap >= x)) {
            result += 1;
        }
        if (Math.abs(this.maxY - y) <= gap && (this.minX - gap <= x && this.maxX + gap >= x)) {
            result += 2;
        }
        if (Math.abs(this.minX - x) <= gap && (this.minY - gap <= y && this.maxY + gap >= y)) {
            result += 4;
        }
        if (Math.abs(this.maxX - x) <= gap && (this.minY - gap <= y && this.maxY + gap >= y)) {
            result += 8;
        }
        return result;
    }

    mouseMoveCreate(e, rect) {
        const clickX = e.clientX - rect.left;
        const clickY = e.clientY - rect.top;
        this.endX = clickX;
        this.endY = clickY;
        this.startX = this.minX;
        this.endX = this.maxX;
        this.startY = this.minY;
        this.endY = this.maxY;
    }

    mouseMoveChangePos(e, rect, startX, startY, endX, endY, clickX, clickY, canvas) {
        const disX = e.clientX - rect.left - clickX;
        const disY = e.clientY - rect.top - clickY;
        this.startX = startX + disX;
        this.startY = startY + disY;
        this.endX = endX + disX;
        this.endY = endY + disY;
        canvas.style.cursor = "move";
    }

    mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, canvas) {
        const disX = e.clientX - rect.left - clickX;
        const disY = e.clientY - rect.top - clickY;
        if (endX + disX < startX || endY + disY < startY || startX + disX > endX || startY + disY > endY) {
            return;
        }

        switch (inGap) {
            case 1:
                canvas.style.cursor = "n-resize";
                this.startY = startY + disY;
                break;
            case 2:
                canvas.style.cursor = "s-resize";
                this.endY = endY + disY;
                break;
            case 4:
                canvas.style.cursor = "w-resize";
                this.startX = startX + disX;
                break;
            case 5:
                canvas.style.cursor = "nw-resize";
                this.startX = startX + disX;
                this.startY = startY + disY;
                break;
            case 6:
                canvas.style.cursor = "sw-resize";
                this.startX = startX + disX;
                this.endY = endY + disY;
                break;
            case 8:
                canvas.style.cursor = "e-resize";
                this.endX = endX + disX;
                break;
            case 9:
                canvas.style.cursor = "ne-resize";
                this.endX = endX + disX;
                this.startY = startY + disY;
                break;
            case 10:
                canvas.style.cursor = "se-resize";
                this.endX = endX + disX;
                this.endY = endY + disY;
                break;
            default:
                canvas.style.cursor = "default";
                break;
        }
    }
}

简单实现了矩形和椭圆形的绘制相关功能

import {Shape} from "@/Shape";

export class Rectangle extends Shape {
    constructor(color, startX, startY, dpr) {
        super(color, startX, startY, dpr);
    }

    draw(ctx) {
        ctx.beginPath();
        ctx.setLineDash([]);
        ctx.moveTo(this.minX, this.minY);
        ctx.lineTo(this.maxX, this.minY);
        ctx.lineTo(this.maxX, this.maxY);
        ctx.lineTo(this.minX, this.maxY);
        ctx.lineTo(this.minX, this.minY);
        super.draw(ctx);
    }
}
import {Shape} from "@/Shape";

export class Circle extends Shape {
    constructor(color, startX, startY, dpr) {
        super(color, startX, startY, dpr);
    }

    draw(ctx) {
        ctx.beginPath();
        ctx.setLineDash([]);
        ctx.ellipse(
            (this.maxX + this.minX) / 2,
            (this.maxY + this.minY) / 2,
            (this.maxX - this.minX) / 2,
            (this.maxY - this.minY) / 2,
            0, 0, Math.PI * 2
        );
        super.draw(ctx);

        ctx.beginPath();
        ctx.moveTo(this.minX, this.minY);
        ctx.setLineDash([1, 5]);
        ctx.lineTo(this.maxX, this.minY);
        ctx.lineTo(this.maxX, this.maxY);
        ctx.lineTo(this.minX, this.maxY);
        ctx.lineTo(this.minX, this.minY);
        super.drawDashed(ctx);
    }
}

App.vue

<script setup>
import {onMounted, reactive} from "vue";
import {Rectangle} from "@/Rectangle";
import {Circle} from "@/Circle";

let canvas;
let rect;
let ctx;
let color;
let dpr;
let shapes = [];
let change = true;
let gap;

function setCanvas() {
  canvas.width = 1000 * dpr;
  canvas.height = 500 * dpr;
  rect = canvas.getBoundingClientRect();
}

function resizeShape(ratio) {
  for (let i = 0; i < shapes.length; i++) {
    shapes[i].startX *= ratio;
    shapes[i].startY *= ratio;
    shapes[i].endX *= ratio;
    shapes[i].endY *= ratio;
  }
}

function draw() {
  if (change) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (let i = 0; i < shapes.length; i++) {
      shapes[i].draw(ctx);
    }
    change = false;
  }
  requestAnimationFrame(draw);
}

onMounted(() => {
  canvas = document.getElementById("canvas");
  ctx = canvas.getContext("2d");
  color = document.getElementById("color");
  dpr = window.devicePixelRatio;
  gap = 10 * dpr;
  setCanvas(canvas);
  draw();

  window.onresize = () => {
    const oldRatio = dpr;
    dpr = window.devicePixelRatio;
    gap = 10 * dpr;
    setCanvas();
    resizeShape(dpr / oldRatio);
    change = true;
    draw();
  };

  window.onkeydown = (e) => {
    if (e.ctrlKey && e.code === "KeyZ") {
      shapes.pop();
      change = true;
    }
  }

  canvas.onmousedown = mouseDown;

  canvas.ondblclick = (e) => {
    const clickX = e.clientX - rect.left;
    const clickY = e.clientY - rect.top;
    let shape;
    for (let i = shapes.length - 1; i >= 0; i--) {
      if (shapes[i].inSide(clickX, clickY)) {
        shapes[i].color = color.value;
        shape = shapes[i];
        shapes.splice(i, 1);
        break;
      }
    }
    if (shape) {
      shapes.push(shape);
    }
    change = true;
  };

  canvas.onmousemove = (e) => {
    changeSize(e);
  };
});

function changeSize(e) {
  const clickX = e.clientX - rect.left;
  const clickY = e.clientY - rect.top;
  for (let i = shapes.length - 1; i >= 0; i--) {
    const inGap = shapes[i].inGap(gap, clickX, clickY);
    const {startX, startY, endX, endY} = shapes[i];
    shapes[i].mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, canvas);
    if (inGap > 0) {
      break;
    }
  }
}

function getShape(clickX, clickY) {
  for (let i = shapes.length - 1; i >= 0; i--) {
    const inGap = shapes[i].inGap(gap, clickX, clickY);
    if (inGap > 0) {
      return shapes[i];
    }
    if (shapes[i].inSide(clickX, clickY)) {
      return shapes[i];
    }
  }
  return null;
}

function mouseDown(e) {
  const clickX = e.clientX - rect.left;
  const clickY = e.clientY - rect.top;

  let shape = getShape(clickX, clickY);
  if (!shape) {
    if (data.checkType === "矩形") {
      shape = new Rectangle(color.value, clickX, clickY, dpr);
    } else if (data.checkType === "椭圆形") {
      shape = new Circle(color.value, clickX, clickY, dpr);
    }
    shapes.push(shape);
    canvas.onmousemove = (e) => {
      shape.mouseMoveCreate(e, rect);
      change = true;
    };
  } else {
    const {startX, startY, endX, endY} = shape;
    const inGap = shape.inGap(gap, clickX, clickY);
    if (inGap > 0) {
      canvas.onmousemove = (e) => {
        shape.mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, canvas);
        change = true;
      };
    } else {
      canvas.onmousemove = (e) => {
        shape.mouseMoveChangePos(e, rect, startX, startY, endX, endY, clickX, clickY, canvas);
        change = true;
      };
    }
  }

  window.onmouseup = () => {
    canvas.onmousemove = null;
    canvas.onmousemove = (e) => {
      changeSize(e);
    };
  };
}

const data = reactive({
  typeList: [
    {
      id: 1,
      name: "矩形",
    },
    {
      id: 2,
      name: "椭圆形",
    },
  ],
  checkType: "矩形",
});

function select(item) {
  data.checkType = item.name;
}
</script>

<template>
  <div style="margin: 100px auto; display: flex; align-items: center; justify-content: center; flex-direction: column">
    <ul class="type">
      <li v-for="item in data.typeList" :key="item.id" :class="data.checkType === item.name ? 'selected' : ''"
          @click="select(item)">
        {{ item.name }}
      </li>
    </ul>
    <input id="color" style="margin-bottom: 20px" type="color"/>
    <canvas id="canvas"></canvas>
  </div>
</template>

<style>
* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

.type {
  list-style: none;

  li {
    padding: 20px;
    float: left;

    &:hover {
      color: #ff0000aa;
      cursor: pointer;
    }
  }

  .selected {
    color: #ff0000;
  }
}

#canvas {
  background-color: coral;
}
</style>

功能展示

绘制矩形和椭圆形
在这里插入图片描述

拖动矩形和椭圆形
在这里插入图片描述

放大缩小
在这里插入图片描述

双击切换颜色并让其在最上层
在这里插入图片描述

源码下载

在线绘图小工具

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/792703.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

推荐系统三十六式学习笔记:原理篇.深度学习20|用RNN构建个性化音乐榜单

目录 时间的重要性循环神经网络榜单生成1.数据2.建模 总结 时间是一个客观存在的物理属性&#xff0c;很多数据都有时间属性&#xff0c;只不过大多时候都把它忽略了。前面讲到的绝大多数推荐算法&#xff0c;也都没有考虑“用户在产品上作出任何行为”都是有时间先后的。 正是…

数据结构:树状数组

树状数组 基本操作&#xff1a;1.快速求前缀和 2.修改一个数。 基本图示&#xff1a; lowbit&#xff1a;求出一个数字二进制最后一个1的位置&#xff1b; 原理&#xff1a; 我们发现&#xff0c;除了最后一个1&#xff0c;以及其后面的0&#xff0c;其余位置都是反&#xf…

翻牌器单独设置前后缀样式

翻牌器单独设置前后缀样式 <template><div :style"[fontStyle,styleBackGroundColor]"><!-- <span style"color: #1d1d1d"> {{optionData}}</span>--><!-- 设置前缀样式 --><span class"prefix" …

【全面介绍Oracle】

🌈个人主页: 程序员不想敲代码啊 🏆CSDN优质创作者,CSDN实力新星,CSDN博客专家 👍点赞⭐评论⭐收藏 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步! 目录 🎥前言🎥基本概念和安装🎥SQL语言🎥PL/SQL编程🎥数据库…

【UML用户指南】-34-应用UML

目录 1、事物 1.1、结构事物 1.2、行为事物 1.3、成组事物 1.4、注释事物 2、关系 2.1、依赖 2.2、关联 2.3、泛化 3、可扩展性 4、图 4.1、结构图 4.2、行为图 5、统一过程Rational 5.1、四个阶段 5.2、九个任务 5.3、制品 5.3.1、模型 5.3.2、其他制品 利…

CACTER直播预告:SMC2全面焕新——您的邮件系统专属安全管家

在数字化的浪潮中&#xff0c;科技革命和产业变革正重塑着企业的发展轨迹。邮箱作为企业内部&#xff0c;企业和企业之间沟通的桥梁&#xff0c;其安全性和效率性是保障企业顺畅运作和信息安全的基石。 随着网络攻击手段的不断翻新&#xff0c;邮件系统所面临的安全威胁日益加剧…

医院门诊预约挂号小程序模板源码

医院门诊预约挂号小程序模板源码,主要有&#xff1a;绿色的医院住院办理&#xff0c;门诊预约挂号微信小程序页面模板。包含&#xff1a;办卡绑定、快速办理预约挂号、门诊缴费、住院服务、医院信息、个人中心、添加就诊人、找医生等等。 医院门诊预约挂号小程序模板源码

vue 画二维码及长按保存

需求 想要做如下图的二维码带文字&#xff0c;且能够长按保存 前期准备 一个canvas安装qrcode&#xff08;命令&#xff1a;npm i qrcode&#xff09; 画二维码及文字 初始化画布 <template><div><canvas ref"canvas" width"300" he…

Qt常用基础控件总结—输入部件(QComboBox类和QLineEdit)

输入部件 下拉列表控件QComboBox 类 QComboBox 类是 QWidget 类的直接子类,该类实现了一个下拉列表(组合框)。 QComboBox 类中的属性函数 1)count:const int 访问函数:int count() const; 获取组合框中的项目数量,默认情况下,对于空组合框或未设置当前项目的组合框,…

4-2 文本向量化

4-2 文本向量化 文本向量化是自然语言处理&#xff08;NLP&#xff09;中的一个关键步骤&#xff0c;通过将文本数据转化为数值向量&#xff0c;使计算机能够理解和处理自然语言。本文将深入探讨文本向量化的各种方法&#xff0c;包括词袋模型&#xff08;Bag of Words&#x…

生物素-十一聚乙二醇-沙利度胺;Biotin-PEG11-Thalidomide

Biotin-PEG11-Thalidomide&#xff0c;即生物素-十一聚乙二醇-沙利度胺&#xff0c;是一种结合了生物素、十一聚乙二醇&#xff08;PEG11&#xff09;和沙利度胺的复杂化合物。以下是对该化合物的详细分析&#xff1a; 一、组成成分及特性 生物素&#xff08;Biotin&#xff09…

备份及恢复Sonarqube服务数据

基础数据&#xff1a; 源数据机ip&#xff1a;192.*.53 测试机ip&#xff1a;192.*.65 Sonarqube访问地址&#xff1a;http://192.*.65:9000/ 账户名&#xff1a;admin 密码&#xff1a;123456 数据库postgres&#xff1a; 版本&#xff1a;PostgreSQL 15.3 一、数据备份…

厨电,被AI重构的下一个十年|产业特稿

智能化赋能下&#xff0c;厨房从闲人免进的油污重地&#xff0c;到会朋交友的社交空间。随着老板、方太等头部厨电厂商纷纷布局AI&#xff0c;厨电行业的数字化、智能化正逐渐改变了人们和烹饪之间的交互&#xff0c;重塑着厨房固有的属性、定位和职能。 作者|斗斗 编辑|皮爷…

RSA算法java实现

基于RSA算法的Java示例代码&#xff0c;展示了如何进行公钥加密、私钥解密、私钥签名和公钥验签。 非堆成加密公私钥使用学习请查看&#xff1a;非堆成加密公私钥使用-CSDN博客 代码实现 package com.chengxuyuan.demo;import javax.crypto.Cipher; import java.security.*;…

3D互动+AR试戴,赋能珠宝品牌线上营销!

随着电商浪潮的汹涌而至&#xff0c;珠宝这一传统上依赖实体店铺销售的行业&#xff0c;正积极拥抱线上转型的浪潮。然而&#xff0c;面对珠宝商品高客单价及消费者对于亲身体验的强烈需求&#xff0c;线上销售面临诸多挑战&#xff0c;尤其是图片展示难以全面展现珠宝魅力&…

cache 设计

1. cache 概念扫描 简介&#xff1a; cache 是一种小容量的缓存空间&#xff0c;类似于较小的sram 。 它的存在着重解决逻辑访问外部存储&#xff08;ddr &#xff09;的时延。 通过一种预测算法&#xff08;cache 的换入和换出&#xff09;&#xff0c;将逻辑大概率访问的热点…

Milvus核心设计(2)-----TSO机制详解

目录 背景 动机 Timestamp种类及使用场景 Guarantee timestamp Service timestamp Graceful time Timestamp同步机制 主流程 时间戳同步流程 背景 Milvus 在设计上突出了分布式的设计,虽然Chroma 也支持分布式的store 与 query。但是相对Milvus来说,不算非常突出。…

【LangChain系列】【基于Langchain的Pandascsv Agent】

目录 前言一、LangChain1-1、介绍1-2、特点 二、Pandas&csv Agent2-1、安装2-2、Pandas&csv Agent介绍2-3、Pandas&csv Agent使用2-3-1、相关库的导入&#xff1a;2-3-2、设置要调用的模型&#xff08;我这里使用阿里的模型&#xff09;2-3-3、数据读取&展示2-…

华为USG6000V防火墙v1

目录 一、实验拓扑图 二、要求 三、IP地址规划 四、实验配置 1&#x1f923;防火墙FW1web服务配置 2.网络配置 要求1&#xff1a;DMZ区内的服务器&#xff0c;办公区仅能在办公时间内(9:00-18:00)可以访问&#xff0c;生产区的设备全天可以访问 要求2&#xff1a;生产区不…

记一次酣畅淋漓的UDF提权(Linux)

外网打点就不放了&#xff0c;翻了一下具备suid权限的命令&#xff0c;没啥结果。 可疑的命令是/usr/lib/dbus-1.0/dbus-daemon-launch-helper但是没有找到用这个命令提权的资料。 弹shell后翻找一下源码&#xff0c;/app/api.py文件中链接了mysql&#xff0c;事出反常必有妖&…