60分前端之canvas

前言

最近在等前端对接,没事干就折腾一下前端。写了这么多年服务端,一直都是写api接口,还没好好学过前端呢。前端框架那么多,什么Vue、React,还分移动端、PC端,还有什么响应式、自适应,各种花里胡哨的东西,也没那么多精力去折腾,直接上手小程序吧。

小程序开发虽然简单,但基本的CSS样式还是得学一下吧:

在国内,微信小程序开发,应该是每个前端程序员必会的技术了吧。

话不多说,直接开干。

写了这么多年的hello world了,学一门"新"的技术还是很简单的。而且咱要求也不高,毕竟不是专门写前端的,所以对于前端的技术,只要做到60分就够了。

所以,下面很多东西,我们都是能忽略就忽略,主打一个囫囵吞枣、不求甚解,以解决问题为主。

微信小程序类目的坑

微信小程序文档

学习任何技术,官方文档都是最好的入门方式之一。

查看微信小程序官方文档,照着文档一步一步往下走,有个两年开发经验的人,基本都能搞定,没什么难度。

但是问题来了,在配置小程序时,需要设置小程序类目。这里提前说一下,因为是练手的项目,所以我写的这个小程序是个大杂烩,既有数独小游戏,又有背单词。由于微信小程序类目可以选择多个,所以我先选择了小游戏......

问题来了!

微信小程序类目,选择了游戏之后,就不能选择其他的了!

而且还改不了!!

改不了就算了,更坑的是,小游戏的代码结构和小程序还不一样,没办法通用。选择小游戏,就必须用小游戏的代码结构,没办法和小程序混用。

练手的几个小游戏,我都是用小程序实现的,没办法在小游戏模式下运行。

网上搜了一下,发现很多人都遇到过这样的问题,也都向官方反映过了,但官方都没有给回复。顺带一提,好像社区里很多问题,官方基本都不回复的。

没办法,只能重新注册一个号了。或者注销之前的号,然后重新注册也行。嫌麻烦,干脆重新注册一个号吧。

数独81宫格实现过程

数独游戏估计很多人都只听过,但没实际玩过,这里就不详细介绍数独游戏了,我们只需要知道一点,数独游戏需要有一个81宫格。

可以看到,数独的81宫格最外层边框加粗,里面每三列的右边框每三行的下边框也加粗,相当于是9个9宫格合并在一起。

作为前端的初学者,想要实现一个样式,第一反应肯定是用CSS来实现。

CSS选择器

先来把基本的框给实现了。

css 复制代码
.sudoku {
    box-sizing: border-box;
    width: 700rpx;
    min-height: 730rpx;
    display: grid;
    grid-template-columns: repeat(9, 1fr);
    grid-template-rows: repeat(9, 1fr);
    gap: 0;
    border: 2px solid #000000;
}
.cell {
    background-color: #FFFFFF;
    border: 1px solid #000000;
    border-right-width: 0; 
    border-bottom-width: 0;
}

实现效果是这样的:

不知道大家有没有注意到这段样式min-height: 730rpx;,设置最小高度。

一开始设置height: 700rpx;,宽高都是700rpx,在iPhone12/13 Pro Max机型下没问题,可是在iPhone12/13 Pro机型下,最下面一行会少一截,高度不够了!

前端的同学都知道CSS的一个概念,叫盒模型。这里高度不够是因为每个小格子的边框也有个宽度,实际总高度应该是每个格子的高度加上格子边框的宽度之和。宽度也是一样的。

这里有一个问题,就算宽高要加上每个小格子边框的宽度,最终的宽高也应该一样的才对啊?!为什么高度反而比宽度还大了呢?!

不理解!算了,毕竟是写服务端的,前端只要做到60分就够了。解决问题就行。高度不够的话,给它加高不就行了。/doge

加多少呢?试呗。😁

好了,在iPhone12/13 Pro机型下问题是解决了,但其他机型下呢?

这个好办,既然知道高度可能不够,继续加高的话,怕影响其他机型下的效果,那干脆给一个最低高度,超过这个高度就让它自己加高,简单粗暴。

它已经是个成熟的高度了,已经学会了自己长高。

还有一种更直接的办法,就是不设置高度,让它自己撑开。但是这样有一个不好的点,高度在撑开的过程中,会很明显看到页面抖动。设置一个最低高度的话,抖动就不明显了。

只要我看不到它抖动,它就不存在。

基本的形状已经有了,下面开始对内部指定的边框进行加粗。先来对每三列的右边框加粗,看过上面的CSS教程的话,稍微学点CSS知识的人都知道,CSS有个伪类选择器,我们用选择器nth-child来实现:

css 复制代码
.cell:nth-child(3n+1) {
    border-left-width: 2px;
}

:nth-child(n) 选择器匹配父元素中的第 n 个子元素,元素类型没有限制。

n 可以是一个数字,一个关键字,或者一个公式。

这里,我们用的是公式的方式。

使用公式(an+ b)a代表一个循环的大小,n是一个计数器(从0开始),以及b是偏移量。ab都必须是整数,an 必须写在 b 的前面,不能写成 b+an 的形式。

第一列左边框看起来太粗了,虽然样式上看起来边框的宽度是2px,但视觉上却像是4px也不知道是什么原因?

不管它什么原因了,毕竟我们不是专业的前端,就像上面说的那样,我们只需要做到60分就够了。

既然视觉上看起来像是4px,那就让它在视觉上看起来像2px

css 复制代码
.cell:nth-child(9n+1) {
    border-left-width: 1px;
}

现在看起来顺眼了很多。

宽度明明设置的是1px,为什么视觉上看起来像2px,不知道什么原因?老规矩,不管!😁

每三列的左边框加粗 ,这个已经实现了,下面开始实现每三行的下边框加粗

可是问题来了,使用:nth-child选择器貌似不能选中连续的子元素,网上搜了一堆,都没有解决方案,后来问了一下ChatGPT,给的回复是这样的:

css 复制代码
/* 设置第3行和第6行的下边框宽度为2px */
.cell:nth-child(3n+1):nth-child(n+7),
.cell:nth-child(3n+2):nth-child(n+8),
.cell:nth-child(3n+3):nth-child(n+9) {
  border-bottom-width: 2px;
}

还亲切地给了注释。可惜根本用不了。

除了第一行前6个的下边框没加粗外,其他元素的下边框全加粗了!

题外话:ChatGPT对CSS的问题,一本正经地胡说八道,给的答案基本都用不了。

折腾了好久,都没折腾出怎么用CSS选择器实现每三行下边框加粗这个效果,算了,换种方式吧。

对象模型

数独游戏不但要有这个宫格,相应的宫格里还得要有数字的。最终的效果应该像这样:

既然没办法自动计算出每三列和每三行的元素,那我们就用笨办法,手动给它们全标记出来。

定义对象模型:

Typescript 复制代码
interface SudokuItem {
  numerical: number, // 数值
  tower: boolean, // 左边框是否需要加粗
  floor: boolean // 下边框是否需要加粗
}

初始化对象模型:

typescript 复制代码
Page({
  data: {
      sudoku: <SudokuItem[]>[
          {numerical: 0, tower: false, floor: false},
          {numerical: 0, tower: false, floor: false},
          {numerical: 0, tower: true, floor: false},
          ...
      ]
  }
})

实际初始化的时候,通过JS代码初始化sudoku,并计算哪个元素的towertrue,哪个floortrue

页面渲染的时候:

html 复制代码
 <view class="sudoku">
    <view wx:for="{{mission}}" wx:for-index="idx" wx:key="idx" wx:for-item="item"
      class="cell-item {{item.tower ? 'tower' : ''}} {{item.floor ? 'floor' : ''}}"
      bindtap="focusTheCell"
      data-index="{{idx}}"
    >
      {{item.numerical == 0 ? undefined : item.numerical}}
    </view>
  </view>
css 复制代码
.sudoku .floor {
  border-top-width: 2px;
}

.sudoku .tower {
  border-right-width: 2px;
}

好了,基本的样式是实现了,虽然过程很简单粗暴,但咱毕竟是写服务端的,对前端的要求也不高,能做到60分就够了。

继续折腾,上面是用CSS样式实现的81宫格,还有其他办法吗?

必须有啊,前端的Canvas技术也很火啊,这个必须得折腾一下啊。

而且貌似还有很多前端不会Canvas的哦,等我们学会了,就去找前端嘚瑟一下。

Canvas

MDN官方教程

先看Canvas官方教程,嗯......,东西挺多的,好像有点复杂,几十个API,看得人眼花。没关系,毕竟我们是带着问题来学习的。这里,我们只需要几个API就行了。

什么问题?使用Canvas画出一个数独的81宫格。

我在学习一个新技术的时候,都是先上手再深入,不懂的地方暂时先跳过去,前面说过的,主打一个囫囵吞枣、不求甚解。先把问题解决了再说。

我们先暂时抛开小程序,只专注于Canvas。

直接开干。

首页,创建一个Canvas画布。

html 复制代码
<canvas id="sudoku" width="460" height="460"></canvas>
javascript 复制代码
const canvas = document.getElementById("sudoku");
const ctx = canvas.getContext("2d");

好了,一个画布就创建好了。是不是很简单。而且是固定套路,可以不理解,记住就行了。下次用的时候,直接复制粘贴,再简单改改就行了。

id:和HTML标签的id属性一样。

widthheight:画布的宽高,可以通过CSS属性调整。这里的宽高类似于图片的原始宽高,CSS调整后的宽高就和修改页面图片的宽高一样,实际是对图片进行缩放。

canvas.getContext("2d"):接受一个参数,即上下文的类型。可以传入2dwebglwebgl2bitmaprenderer,用于创建不同类型的上下文。

这里我们传入2d,创建一个二维渲染上下文。

画布创建好了,下面该画画了。

想想看,如果让你在现实中用笔画一个81宫格,你应该怎么做?

首先,得有张白纸,然后提笔,之后开始画。

在Canvas中也一样,白纸我们已经有了,上面创建的画布就是白纸。

下面,该考虑怎么画了。初中数学老师告诉我们,两点确定一条直线,从A点到B点,画一条直线。

Canvas通过路径和填充的方式来绘制图画的,填充我们暂时不管,路径可以简单理解为线段,直线、曲线这些。

每条线段就是一条路径。

生成路径的第一步叫做 beginPath()。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。

不理解?记住就行了,都是固定套路。先记住怎么用,等bug写多了,自然就理解了。别管代码是不是写的垃圾,先敲出来再说,别只光在脑子里想。

ctx.beginPath():清空子路径列表开始一个新路径。可以简单理解成"提笔"。

ctx.moveTo(x, y):将一个新的子路径的起始点移动到 (x,y) 坐标。可以理解成上面的"从A点......"

ctx.lineTo(x, y):使用直线连接子路径的终点到(x,y)坐标。可以理解成上面的"到B点,画一条直线"

上面的(x, y)为坐标,页面坐标这个东西,相信很多人应该都已经知道了。

这里的坐标为画布中的坐标,坐标原点(0, 0)为画布的左上角。

javascript 复制代码
const canvas = document.getElementById("sudoku");
// 创建二维上下文
const ctx = canvas.getContext("2d");
// 从A点
ctx.moveTo(100, 100);
// 到B点
ctx.lineTo(200, 100);

刷新一下页面,什么都没有!

别急,这就和我们现实中画画一样,先要在脑中构思,构思完了,才开始落笔绘画。

同样的道理,上面的那些相当于是在构思,构思完了后,我们得要落笔绘制。

ctx.stroke():根据当前的画线样式,绘制当前或已经存在的路径。

所以,完整的代码应该是这样的:

javascript 复制代码
const canvas = document.getElementById("sudoku");
// 创建二维上下文
const ctx = canvas.getContext("2d");
ctx.beginPath();
// 从A点
ctx.moveTo(100, 100);
// 到B点
ctx.lineTo(200, 100);
// 画线
ctx.stroke();

具体可以看一下MDN的示例,使用canvas来绘制图形

好了,一条直线就绘制出来了。

可以了,能画出一条直线,我们可以开始画81宫格了。81宫格就是10条横线、10条竖线组成的。只要计算好每个点的坐标,然后画线就行了。

计算点的坐标,这是数学的范畴,就不过多赘述了,而且点的坐标计算也不难。

javascript 复制代码
const canvas = document.getElementById("sudoku");
const ctx = canvas.getContext("2d");
// 单元格的宽高
const cellSize = 50;

for (let i = 0; i < 10; i++) {
    // 画横线,留5px的间距
    ctx.beginPath();
    ctx.moveTo(5, i * cellSize + 5);
    ctx.lineTo(455, i * cellSize + 5);
    ctx.stroke();

    // 画竖线
    ctx.beginPath();
    ctx.moveTo(i * cellSize + 5, 5);
    ctx.lineTo(i * cellSize + 5, 455);
    ctx.stroke();
}

81宫格是画出来了,可还没法用,因为数独中的81宫格,要求最外围边框加粗,里面每三列左边框加粗、每三行下边框加粗。

ctx.lineWidth:设置线段的宽度

还记得上面我们通过CSS选择器给边框加粗时遇到的痛苦吗,现在就简单多了,只需要简单的数学计算就可以了。

javascript 复制代码
// 设置画笔宽度
if (i % 3 == 0) {
        ctx.lineWidth = 3;
} else {
        ctx.lineWidth = 1;
}

设置完后我们,看一下效果

下面是完整的代码:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Canvas 练习</title>
</head>
<body>
    <canvas id="sudoku" width="460px" height="460px"></canvas>
    <script type="text/javascript">
        const canvas = document.getElementById("sudoku");
        const ctx = canvas.getContext("2d");
        // 单元格的宽高
        const cellSize = 50;

        for (let i = 0; i < 10; i++) {
            // 设置画笔宽度
            ctx.lineWidth = i % 3 == 0 ? 3 : 1;

            ctx.beginPath();
            // 画横线,留5px的间距
            ctx.moveTo(5, i * cellSize + 5);
            ctx.lineTo(455, i * cellSize + 5);
            ctx.stroke();

            ctx.beginPath();
            // 画竖线
            ctx.moveTo(i * cellSize + 5, 5);
            ctx.lineTo(i * cellSize + 5, 455);
            ctx.stroke();
        }
    </script>
</body>
</html>

之后只要根据小程序的规则,将代码移植到小程序里就行了。至于怎么往数独里填充数字,这个之后再说吧,我们一点一点的来。这里我们只介绍这么画直线。

学一门新技术,一定要多练。数独的81宫格画完了,还可以画点其他的啊。比如画一个象棋的棋盘,或者画一个围棋的棋盘。

多练手,遇到的问题多了,自然就会了。

最后

虽然是一个练手的项目,但是既然已经做出来了,干脆就上线算了。

相关推荐
老华带你飞4 分钟前
酒店预约|基于springboot 酒店预约系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
dob4 分钟前
为什么你的if-else越写越乱?聊聊状态机
后端
古城小栈7 分钟前
Spring Boot + 边缘 GenAI:智能座舱应用开发实战
java·spring boot·后端
twl10 分钟前
注意力机制在Code Agent的应用
前端
开心就好202526 分钟前
使用 Ipa Guard 应对 App Store 4.3 风险的一些实践
后端
涔溪27 分钟前
如何使用 CSS Grid 实现响应式布局?
前端·css
想用offer打牌29 分钟前
一站式了解数据库三大范式(库表设计基础)
数据库·后端·面试
2501_9419820534 分钟前
Go 进阶:发送文件/图片消息的流程与实现
开发语言·后端·golang
未来读啥科教资讯34 分钟前
2026年深圳国际户外用品展览会参展效果如何?影响力如何?
前端
码农胖大海38 分钟前
浏览器及标签页关闭时登出的解决方案
前端·浏览器