Canvas 用最土的办法画了一个掘金的 logo

421 阅读3分钟

Canvas 应用之掘金 logo

多边形

moveTo,lineTo 方法的应用

通过前面的学习我们已经知道,moveTo,lineTo 方法可以用来画三角形,正方形,自然而然多边形也是用这两个方法来绘制。

绘制一个多边形箭头
<template>
  <div ref="mainContent" class="polygon">
    <canvas ref="cnv" width="200" height="150" style="border: 1px dashed gray"></canvas>
  </div>
</template>

<script setup>
import {ref, onMounted} from "vue";
const cnv = ref();
const drawPolygon = (ctx) => {
  // 确定顶点
  ctx.moveTo(100, 20);
  ctx.lineTo(150, 50);
  ctx.lineTo(120, 50);
  ctx.lineTo(120, 130);
  ctx.lineTo(80, 130);
  ctx.lineTo(80, 50);
  ctx.lineTo(50, 50);
  ctx.lineTo(100, 20);

  ctx.stroke();
}
onMounted(() => {
  const ctx = cnv.value.getContext('2d');
  drawPolygon(ctx);
});
</script>

这样我们就画出了一个向上的箭头,但是,是否在上述的代码中发现,lineTo 这个方法是否一直在重复的执行,对于一个程序员来说,这估计内心难以容忍,选择将 drawPolygon 方法进行封装一下。

const drawPolygon = (ctx) => {
  const points = [ 100, 20, 150, 50, 120, 50, 120, 130, 80, 130, 80, 50, 50, 50 ];

  points.forEach((point, index) => {
    if (index && index % 2 === 1) {
      if (index === 1) {
        ctx.moveTo(points[index - 1], point);
      } else {
        ctx.lineTo(points[index - 1], point);
      }
    }
  });
  // 形成一个多边形的必要条件就是起点与终点相同,也即是首尾相连
  ctx.lineTo(points.at(0), points.at(1));

  ctx.stroke();
}

这样封装一下是否感觉要好多了,但是 forEach 里边的计算函数的判断还是太复杂了,看来需要介绍新的方法了,ctx.beginPath(),ctx.closePath()。

  • ctx.beginPath():用于开始一条新路径
  • ctx.closePath():用于关闭路径

我们再次来封装一下绘制一个多边形,这次附上一套完整代码来看一看。

<template>
  <div ref="mainContent" class="polygon">
    <canvas ref="cnv" width="200" height="150" style="border: 1px dashed gray"></canvas>
  </div>
</template>

<script setup>
import {ref, onMounted} from "vue";
const cnv = ref();

/**
 * 根据顶点列表绘制多边形
 * @param ctx
 * @param points
 */
const drawPolygon = (ctx, points) => {
	try {
    if (!ctx) {
      throw new Error('未获取到上下文');
    }
    if (points.length % 2 === 1) {
      throw new Error('顶点列表不符合规范');
    }

    ctx.beginPath();
    points.forEach((point, index) => {
      if (index && index % 2 === 1) {
        ctx.lineTo(points[index - 1], point);
      }
    });
    ctx.closePath();

    ctx.stroke();
  } catch (error) {
    console.error(error);
  }
}
onMounted(() => {
  const ctx = cnv.value.getContext('2d');
  const points = [ 100, 20, 150, 50, 120, 50, 120, 130, 80, 130, 80, 50, 50, 50 ];
  
  drawPolygon(ctx, points);
});
</script>

对于多边形的绘制,归根结底就是一个寻找点坐标的问题,只要把点找对,找齐,那么就可以画出多边形来。

绘制一个掘金的 Logo

根据现有的直线这一章的学习,我们可以绘制出基础的多边形,那我们来看一看怎么来绘制一个掘金的 Logo。

因为不知道掘金 Logo 的尺寸及比例,所以只能用最笨的办法,就是把图下载下来,进行粗糙的测量,肯定会有误差,但是不影响,只是做一个练习,测量用到的软件 《Geogebra》

我们来看看在 Geogebra 中画的的测量图:

juejinIcon.png

由于,这个软件是数学几何软件,在绘制的时候,尝试了转换为 W3C 坐标系,但是失败了(如果有知道比较好的图形绘制辅助软件,还请 jym 评论告知一下),但是这并不影响我们进行测量,测量数据都不是精确值,难免有些误差,还请见谅。

<template>
  <canvas ref="cnv" width="200" height="150" style="border: 1px dashed gray"></canvas>
</template>

<script setup>
import {ref, onMounted} from "vue";
const cnv = ref();

function getPoints(ANGLE, pointGroups, basePoint) {
  const [baseX, baseY] = basePoint;
  return pointGroups.map(group => {
    return group.map(points => {
      let pointX;
      if (points.onLine === 'middle') {
        pointX = baseX;
      } else {
        const width = points.height * Math.tan(ANGLE);
        pointX = points.onLine === 'left' ? baseX - width : baseX + width;
      }
      const pointY = baseY + points.height;
      return [pointX, pointY];
    })
  })
}

function drawLogo(ctx, ANGLE, pointGroups, basePoint) {
  const shouldDrawPointGroups = getPoints(ANGLE, pointGroups, basePoint);

  shouldDrawPointGroups.forEach(group => {
    group.forEach((point, index) => {
      if (index === 0) {
        ctx.moveTo(point[0], point[1]);
      } else  {
        ctx.lineTo(point[0], point[1]);
      }
    });
  });

  ctx.fillStyle = "#1f80ff";
  ctx.fill();
}

onMounted(() => {
  const ctx = cnv.value.getContext('2d');
  const ANGLE = Math.PI / 180 * (90 - 39);
  const basePoint = [100, 10];
  const pointGroups = [
    // 第一组顶点关系
    [
      { height: 0, onLine: 'middle' },
      { height: 15, onLine: 'left'},
      { height: 30, onLine: 'middle' },
      { height: 15, onLine: 'right'},
      { height: 0, onLine: 'middle' }
    ],
    // 第二组顶点关系
    [
      { height: 59, onLine: 'middle' },
      { height: 30, onLine: 'left'},
      { height: 40, onLine: 'left'},
      { height: 80, onLine: 'middle' },
      { height: 40, onLine: 'right'},
      { height: 30, onLine: 'right'},
      { height: 59, onLine: 'middle' }
    ],
    // 第三组顶点关系
    [
      { height: 106, onLine: 'middle' },
      { height: 54, onLine: 'left'},
      { height: 65, onLine: 'left'},
      { height: 128, onLine: 'middle' },
      { height: 65, onLine: 'right'},
      { height: 54, onLine: 'right'},
      { height: 106, onLine: 'middle' }
    ]
  ];
  drawLogo(ctx, ANGLE, pointGroups, basePoint);
});
</script>

我们先来看看画出来的 logo 效果:

juejinLogo.png

因为坐标系的缘故,所以在绘制时,需要自行把视图倒过来看,为了将视图安排的尽量美观,所有不是在x轴上开始画图,而是以顶点 E(100, 10)开始向下绘制,如图,已知角度 ∠DEx 为 39°,那么 ∠DER 即为 90° - 39° = 51°,已知线段 ER = 15px,那么线段 DR 的长度就能通过三角函数求出,在求解三角函数时,需要把角度制转为弧度制,再进行计算,我们用同样的方式,可以测出线段 AE、SE、TE、HE、UE、KE、ME、PE 的长度,可以通过三角函数直接求出对应的点的x坐标,或者利用相似三角形等比属性计算也是可以的。这个思路总结一下就是,通过辅助线把所有点的纵坐标测出来,利用三角函数将对应点的横坐标算出来,得到所有点的坐标,最后再进行绘制。绘制时,一定要记住绘制的先后顺序,可以选择顺时针,也可以逆时针方向,但得一直保持一个方向。

我们在学习的时候,想着法的让自己能把学到的东西及时的进行练习,把学到的知识点巩固起来,jym们一起加油吧!