搬运一个前端锻炼面向对象思维的小案例

使用canvas实现一个简单的绘图板:

实际实现效果
使用vite+vue3,组件使用部分antdv的

第一步:简单布局

主要布局内容:

1.颜色选择器

2.形状选择器(这里只做2种形状的)

3.canvas图层

js 复制代码
<template>
  <div ref="wrapRef" class="canvas_wrap">
    <div>
      <span>颜色选择: </span>
      <a-input
        v-model:value="colorVal"
        style="width: 200px"
        type="color"
      ></a-input>
      <span style="margin-left: 20px">形状选择: </span>
      <a-select v-model:value="drawType">
        <a-select-option value="rect">方形</a-select-option>
        <a-select-option value="circle">圆形</a-select-option>
      </a-select>
    </div>
    <canvas
      ref="canvasRef"
      :style="{
        width: canvasStyleWidth + 'px',
        height: canvasStyleHeight + 'px',
      }"
    ></canvas>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const canvasStyleWidth = 1200;
const canvasStyleHeight = 480;
const colorVal = ref<string>("#000");
const drawType = ref<string>("rect");
</script>

<style scoped lang="scss">
.canvas_wrap {
  padding: 30px;
}
canvas {
  margin-top: 30px;
  border-radius: 10px;
  background: #f0f0f0;
}
</style>
布局完

第二步:画矩形

在canvas中画一个矩形只需要使用canvas中strokeRect方法以及提供4个参数分别是起始点x、y坐标以及矩形的宽高。

strokeRect文档参考

这里我们新建一个矩形的类RectPolygon,由上面绘制所需肯定就需要定义4个变量startX、startY、endX、endY以及绘制颜色的变量
typescript 复制代码
export class RectPolygon {
  public startX: number;
  public startY: number;
  public endX: number;
  public endY: number;
  public drawColor: string;
  public ctx: CanvasRenderingContext2D;
  public id: number;
  constructor(
    startX: number,
    startY: number,
    drawColor: string,
    ctx: CanvasRenderingContext2D
  ) {
    this.startX = startX;
    this.startY = startY;
    this.drawColor = drawColor;
    this.endX = startX;
    this.endY = startY;
    this.ctx = ctx;
    this.id = Date.now();
  }
}

如何确定起始点的坐标呢?

当鼠标按下的时候肯定可以确认1个点(但不一定就是起始点坐标),当鼠标抬起来的时候可以确认另外1个点;

这样画时,鼠标按下的点就是起始点的坐标,鼠标移动后抬起的点就是结束点坐标

但是这样画时,鼠标按下的点在canvas中绘制矩形时相当于结束坐标,鼠标移动后抬起的点是起始坐标

但是可以看出起始坐标就是x、y相对于较小的坐标,所以在RectPolygon类中新增几个getter分别获取起始/结束的x、y坐标
typescript 复制代码
export class RectPolygon {
  public startX: number;
  public startY: number;
  public endX: number;
  public endY: number;
  public drawColor: string;
  public ctx: CanvasRenderingContext2D;
  public id: number;
  constructor(
    startX: number,
    startY: number,
    drawColor: string,
    ctx: CanvasRenderingContext2D
  ) {
    this.startX = startX;
    this.startY = startY;
    this.drawColor = drawColor;
    this.endX = startX;
    this.endY = startY;
    this.ctx = ctx;
    this.id = Date.now();
  }
  get StartX() {
    return Math.min(this.startX, this.endX);
  }
  get StartY() {
    return Math.min(this.startY, this.endY);
  }
  get EndX() {
    return Math.max(this.startX, this.endX);
  }
  get EndY() {
    return Math.max(this.startY, this.endY);
  }
  change(x: number, y: number) {
    this.endX = x;
    this.endY = y;
  }
  draw() {
    this.ctx.fillStyle = this.drawColor;
    this.ctx.fillRect(
      this.StartX,
      this.StartY,
      this.EndX - this.StartX,
      this.EndY - this.StartY
    );
  }
}
当我们鼠标按下时就创建了一个矩形,这个时候起始坐标和结束坐标相当于重叠的,矩形还没有宽高,在鼠标移动的时候实际就是修改矩形的结束坐标的x、y,添加一个change方法用于修改结束点的x、y坐标

开始绘制

注册事件把矩形类应用上,然后调用draw方法绘制,在鼠标移动时调用change方法修改结束点坐标位置
js 复制代码
<template>
  <div ref="wrapRef" class="canvas_wrap">
    <div>
      <span>颜色选择: </span>
      <a-input
        v-model:value="colorVal"
        style="width: 200px"
        type="color"
      ></a-input>
      <span style="margin-left: 20px">形状选择: </span>
      <a-select v-model:value="drawType">
        <a-select-option value="rect">方形</a-select-option>
        <a-select-option value="circle">圆形</a-select-option>
      </a-select>
    </div>
    <canvas
      ref="canvasRef"
      :style="{
        width: canvasStyleWidth + 'px',
        height: canvasStyleHeight + 'px',
      }"
    ></canvas>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { RectPolygon } from "./utils";
type GeometryType = RectPolygon;
const canvasStyleWidth = 1200; //canvas样式宽度
const canvasStyleHeight = 480; //canvas样式高度
const colorVal = ref<string>("#000");
const drawType = ref<string>("rect");
const ctx = ref<CanvasRenderingContext2D>();
const canvasRef = ref<HTMLCanvasElement>();
const currentPolygon = ref<GeometryType>(); //当前绘制的矩形
const drawData = ref<GeometryType[]>([]); //当前画布上所有的矩形数据源
const isDrawing = ref<boolean>(false); //是否正在绘制
let canvasLeft: number, canvasTop: number;
onMounted(() => {
  initCanvas();
  const { x, y } = canvasRef.value.getBoundingClientRect();
  canvasLeft = x;
  canvasTop = y;
});
const initCanvas = () => {
  canvasRef.value.width = canvasStyleWidth * window.devicePixelRatio;
  canvasRef.value.height = canvasStyleHeight * window.devicePixelRatio;
  ctx.value = canvasRef.value.getContext("2d");
  canvasRef.value.addEventListener("mousedown", canvasMouseDown);
};
const canvasMouseDown = (e: MouseEvent) => {
  //计算按下的起始坐标
  const startX = e.clientX - canvasLeft;
  const startY = e.clientY - canvasTop;
  const poylogn = new RectPolygon(startX, startY, colorVal.value, ctx.value);
  currentPolygon.value = poylogn;
  drawData.value.push(currentPolygon.value);
  window.onmousemove = (e: MouseEvent) => {
    const endX = e.clientX - canvasLeft;
    const endY = e.clientY - canvasTop;
    ctx.value.clearRect(
      0,
      0,
      1200,
      480
    );
    if (currentPolygon.value) {
      currentPolygon.value.change(endX, endY);
    }
    for (const item of drawData.value) {
      item.draw();
    }
  };
  window.onmouseup = () => {
    currentPolygon.value = null;
    window.onmousemove = null;
    window.onmouseup = null;
  };
};
</script>

<style scoped lang="scss">
.canvas_wrap {
  padding: 30px;
}
canvas {
  margin-top: 30px;
  border-radius: 10px;
  background: #f0f0f0;
}
</style>
成功绘制
现在我们想如果鼠标在当前已经绘制的矩形内是选中矩形拖动,而不是又重新绘制一个矩形,所以在矩形类新增一个方法isPointIn用于判断一个坐标是否位于当前图形内,并且添加一个move方法用于图形移动
js 复制代码
  isPointIn(x: number, y: number) {
    if (
      x >= this.StartX &&
      x < this.EndX &&
      y >= this.StartY &&
      y <= this.EndY
    ) {
      return true;
    }
    return false;
  }
  
    move(x: number, y: number) {
    this.startX += x;
    this.startY += y;
    this.endX += x;
    this.endY += y;
  }
然后为window添加一个mousemove监听器,用于检测当前鼠标移动的点是否在矩形内
js 复制代码
 window.addEventListener("mousemove", checkPolygonAtPixel);
 const checkPolygonAtPixel = (e: MouseEvent) => {
 if (isDrawing.value) return;
  const checkX = e.clientX - canvasLeft;
  const chexkY = e.clientY - canvasTop;
  ctx.value.clearRect(
    0,
    0,
    1200 * window.devicePixelRatio,
    480 * window.devicePixelRatio
  );
  for (const item of drawData.value) {
    if (!item.isPointIn(checkX, chexkY)) {
      document.body.style.cursor = "default";
      currentPolygon.value = null;
    } else {
      const index = drawData.value.findIndex((items) => items === item);
      drawData.value.splice(index, 1);
      drawData.value.push(item);
      document.body.style.cursor = "pointer";
      currentPolygon.value = item;
    }
    item.draw();
  }
};
并且在我们鼠标按下后要修改对应逻辑,判断当前鼠标位置是否有图形,有的话鼠标移动就是调用图形的move方法,没有则是新建一个图形,在移动时调用其change方法
js 复制代码
const canvasMouseDown = (e: MouseEvent) => {
  //计算按下的起始坐标
  isDrawing.value = true;
  const startX = e.clientX - canvasLeft;
  const startY = e.clientY - canvasTop;
  if (currentPolygon.value) {
    window.onmousemove = (e: MouseEvent) => {
      currentPolygon.value.move(e.movementX, e.movementY);
      ctx.value.clearRect(
        0,
        0,
        1200 * window.devicePixelRatio,
        480 * window.devicePixelRatio
      );
      for (const item of drawData.value) {
        item.draw();
      }
    };
  } else {
    const poylogn = new RectPolygon(startX, startY, colorVal.value, ctx.value);
    currentPolygon.value = poylogn;
    drawData.value.push(poylogn);
    window.onmousemove = (e: MouseEvent) => {
      const endX = e.clientX - canvasLeft;
      const endY = e.clientY - canvasTop;
      ctx.value.clearRect(
        0,
        0,
        1200 * window.devicePixelRatio,
        480 * window.devicePixelRatio
      );
      if (currentPolygon.value) {
        currentPolygon.value.change(endX, endY);
      }
      for (const item of drawData.value) {
        item.draw();
      }
    };
  }

  window.onmouseup = () => {
    currentPolygon.value = null;
    window.onmousemove = null;
    window.onmouseup = null;
    isDrawing.value = false;
  };
};
至此就实现了矩形的绘制和移动功能

实现一种图形的绘制和移动后,其他图形就可以依葫芦画瓢了,比如画圆形,新建CirclePolygon类实现其核心方法draw、move、change即可

全部代码

index.vue

js 复制代码
<template>
  <div ref="wrapRef" class="canvas_wrap">
    <div>
      <span>颜色选择: </span>
      <a-input
        v-model:value="colorVal"
        style="width: 200px"
        type="color"
      ></a-input>
      <span style="margin-left: 20px">形状选择: </span>
      <a-select v-model:value="drawType">
        <a-select-option value="rect">方形</a-select-option>
        <a-select-option value="circle">圆形</a-select-option>
      </a-select>
    </div>
    <canvas
      ref="canvasRef"
      :style="{
        width: canvasStyleWidth + 'px',
        height: canvasStyleHeight + 'px',
      }"
    ></canvas>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { CirclePolygon, RectPolygon } from "./utils";
type GeometryType = RectPolygon | CirclePolygon;
const canvasStyleWidth = 1200; //canvas样式宽度
const canvasStyleHeight = 480; //canvas样式高度
const colorVal = ref<string>("#000");
const drawType = ref<string>("rect");
const ctx = ref<CanvasRenderingContext2D>();
const canvasRef = ref<HTMLCanvasElement>();
const currentPolygon = ref<GeometryType>(); //当前绘制的矩形
const drawData = ref<GeometryType[]>([]); //当前画布上所有的矩形数据源
const isDrawing = ref<boolean>(false); //是否正在绘制
let canvasLeft: number, canvasTop: number;
onMounted(() => {
  initCanvas();
  const { x, y } = canvasRef.value.getBoundingClientRect();
  canvasLeft = x;
  canvasTop = y;
});
const initCanvas = () => {
  canvasRef.value.width = canvasStyleWidth * window.devicePixelRatio;
  canvasRef.value.height = canvasStyleHeight * window.devicePixelRatio;
  ctx.value = canvasRef.value.getContext("2d");
  canvasRef.value.addEventListener("mousedown", canvasMouseDown);
  window.addEventListener("mousemove", checkPolygonAtPixel);
};
const checkPolygonAtPixel = (e: MouseEvent) => {
  if (isDrawing.value) return;
  const checkX = e.clientX - canvasLeft;
  const chexkY = e.clientY - canvasTop;
  ctx.value.clearRect(
    0,
    0,
    1200 * window.devicePixelRatio,
    480 * window.devicePixelRatio
  );
  for (const item of drawData.value) {
    if (!item.isPointIn(checkX, chexkY)) {
      //如果当前鼠标点不在该矩形范围内
      item.changeIsAcative(false);
      item.draw();
      document.body.style.cursor = "default";
      currentPolygon.value = null;
    } else {
      //如果当前鼠标点在该矩形范围内
      const index = drawData.value.findIndex((items) => items === item); //找到图形数据index
      const hasActive = drawData.value
        .filter((items) => items !== item)
        .some((i) => i.isActive);
      if (!hasActive) {
        //这里主要是处理一下如果2个图形有遮挡,在激活时保证激活的图形在最上面
        item.changeIsAcative(true);
        item.drawActive();
        document.body.style.cursor = "pointer";
        drawData.value.splice(index, 1);
        drawData.value.push(item);
        item.changeIsAcative(true);
        item.drawActive();
        currentPolygon.value = item;
        document.body.style.cursor = "pointer";
      } else {
        item.changeIsAcative(false);
        item.draw();
        document.body.style.cursor = "default";
        currentPolygon.value = null;
      }
    }
  }
};
const canvasMouseDown = (e: MouseEvent) => {
  //计算按下的起始坐标
  isDrawing.value = true;
  const startX = e.clientX - canvasLeft;
  const startY = e.clientY - canvasTop;
  if (currentPolygon.value) {
    window.onmousemove = (e: MouseEvent) => {
      currentPolygon.value.move(e.movementX, e.movementY);
      ctx.value.clearRect(
        0,
        0,
        1200 * window.devicePixelRatio,
        480 * window.devicePixelRatio
      );
      for (const item of drawData.value) {
        if (item.id === currentPolygon.value.id) {
          item.drawActive();
        } else {
          item.draw();
        }
      }
    };
  } else {
    let poylogn: GeometryType;
    if (drawType.value === "rect") {
      poylogn = new RectPolygon(startX, startY, colorVal.value, ctx.value);
    } else {
      poylogn = new CirclePolygon(startX, startY, colorVal.value, ctx.value);
    }
    currentPolygon.value = poylogn;
    drawData.value.push(poylogn);
    window.onmousemove = (e: MouseEvent) => {
      const endX = e.clientX - canvasLeft;
      const endY = e.clientY - canvasTop;
      ctx.value.clearRect(
        0,
        0,
        1200 * window.devicePixelRatio,
        480 * window.devicePixelRatio
      );
      if (currentPolygon.value) {
        currentPolygon.value.change(endX, endY);
      }
      for (const item of drawData.value) {
        item.draw();
      }
    };
  }

  window.onmouseup = () => {
    currentPolygon.value = null;
    window.onmousemove = null;
    window.onmouseup = null;
    isDrawing.value = false;
  };
};
</script>

<style scoped lang="scss">
.canvas_wrap {
  padding: 30px;
}
canvas {
  margin-top: 30px;
  border-radius: 10px;
  background: #f0f0f0;
}
</style>

utils.ts

js 复制代码
export class CirclePolygon {
  public startX: number;
  public startY: number;
  public radius: number;
  public ctx: CanvasRenderingContext2D;
  public drawColor: string;
  public id: number;
  public isActive: boolean;
  constructor(
    startX: number,
    startY: number,
    drawColor: string,
    ctx: CanvasRenderingContext2D
  ) {
    this.startX = startX;
    this.startY = startY;
    this.drawColor = drawColor;
    this.isActive = false;
    this.ctx = ctx;
    this.radius = 0;
    this.id = Date.now();
    this.draw();
  }
  draw(color = this.drawColor) {
    this.ctx.fillStyle = color;
    this.ctx.beginPath();
    this.ctx.arc(this.startX, this.startY, this.radius, 0, Math.PI * 2);
    this.ctx.stroke();
    this.ctx.fill();
  }
  changeIsAcative(val: boolean) {
    this.isActive = val;
  }
  drawActive() {
    this.draw("red");
  }
  isPointIn(x: number, y: number) {
    this.ctx.beginPath();
    this.ctx.arc(this.startX, this.startY, this.radius, 0, Math.PI * 2);
    if (this.ctx.isPointInPath(x, y)) {
      return true;
    }
    return false;
  }
  change(x: number, y: number) {
    const rmax = Math.min(this.startX, this.startY);
    this.radius = Math.min(
      rmax,
      Math.sqrt((x - this.startX) ** 2 + (y - this.startY) ** 2)
    );
  }
  move(x: number, y: number) {
    this.startX += x;
    this.startY += y;
  }
}
export class RectPolygon {
  public startX: number;
  public startY: number;
  public endX: number;
  public endY: number;
  public drawColor: string;
  public ctx: CanvasRenderingContext2D;
  public isActive: boolean;
  public id: number;
  constructor(
    startX: number,
    startY: number,
    drawColor: string,
    ctx: CanvasRenderingContext2D
  ) {
    this.startX = startX;
    this.startY = startY;
    this.drawColor = drawColor;
    this.endX = startX;
    this.isActive = false;
    this.endY = startY;
    this.ctx = ctx;
    this.id = Date.now();
  }
  get StartX() {
    return Math.min(this.startX, this.endX);
  }
  get StartY() {
    return Math.min(this.startY, this.endY);
  }
  get EndX() {
    return Math.max(this.startX, this.endX);
  }
  get EndY() {
    return Math.max(this.startY, this.endY);
  }
  changeIsAcative(val: boolean) {
    this.isActive = val;
  }
  drawActive() {
    this.draw("red");
  }
  isPointIn(x: number, y: number) {
    if (
      x >= this.StartX &&
      x <= this.EndX &&
      y >= this.StartY &&
      y <= this.EndY
    ) {
      return true;
    }
    return false;
  }
  move(x: number, y: number) {
    this.startX += x;
    this.startY += y;
    this.endX += x;
    this.endY += y;
  }
  change(x: number, y: number) {
    this.endX = x;
    this.endY = y;
  }
  draw(color = this.drawColor) {
    this.ctx.fillStyle = color;
    this.ctx.fillRect(
      this.StartX,
      this.StartY,
      this.EndX - this.StartX,
      this.EndY - this.StartY
    );
  }
}

总结

虽然在平时普通业务开发时很少遇到需要使用到面向对象的思想,但是一旦复杂度上来了后使用面向对象的的思路往往能解决很多难题,像大多数公共库中源码很多都是面向对象,锻炼这种思维比较重要!
相关推荐
工业甲酰苯胺13 分钟前
TypeScript枚举类型应用:前后端状态码映射的最简方案
javascript·typescript·状态模式
brzhang13 分钟前
我操,终于有人把 AI 大佬们 PUA 程序员的套路给讲明白了!
前端·后端·架构
止观止1 小时前
React虚拟DOM的进化之路
前端·react.js·前端框架·reactjs·react
goms1 小时前
前端项目集成lint-staged
前端·vue·lint-staged
谢尔登1 小时前
【React Natve】NetworkError 和 TouchableOpacity 组件
前端·react.js·前端框架
Lin Hsüeh-ch'in1 小时前
如何彻底禁用 Chrome 自动更新
前端·chrome
augenstern4163 小时前
HTML面试题
前端·html
张可3 小时前
一个KMP/CMP项目的组织结构和集成方式
android·前端·kotlin
G等你下课4 小时前
React 路由懒加载入门:提升首屏性能的第一步
前端·react.js·前端框架
谢尔登4 小时前
【React Native】ScrollView 和 FlatList 组件
javascript·react native·react.js