上一章讲解了如何使用 canvas 实现大转盘抽奖点击回顾;但有些地方并无讲清楚,好比上一章实现的大转盘,奖品选项只能填文字,而不能放图片上去。 这一次,咱们用 canvas 来实现九宫格抽奖(我已沉迷抽奖没法自拔~),顺便将渲染图片功能也给你们过一遍。 本章涉及到的知识点,主要有:javascript
context.drawImage()
方法渲染图片context.isPointInPath()
方法,在画布中制做按钮setTimeout()
方法,来作逐帧动画由于本章代码比较繁杂,我不会所有贴出来;建议进入个人 Github 仓库,找到 test 文件下的 sudoku文件夹下载,本章讲解的代码都在里面啦。java
|--- js
|--- | --- variable.js # 包含了全部全局变量
|--- | --- global.js # 包含了本项目所用到的公用方法
|--- | --- index.js # 九宫格主体逻辑代码
|--- index.html
复制代码
首先,咱们须要绘制出一个九宫格,你们都知道九宫格长什么样子哈,简单的排9个方块,不就搞定了么?git
不不不,做为一名合格的搬砖工,咱们须要严于律己,写代码要抽象,要能重用,要... 假如哪天产品大大说,我要12宫格儿的,15的,20的,你咋办,一个个从新算额~ 因此,咱们得作成图1这样的:github
敲敲数字,鸟枪变大炮。无论你9宫仍是12宫仍是自宫,哥都不怕。算法
如下是个人实现方法,若是你们有更简单的方法,请告诉我,请告诉我,请告诉我,学美术出生的我数学真的很烂~gulp
咱们将九宫格看作一个完整的矩形,矩形有四个顶点; 假设每一行每一列,咱们只显示3个小方块(也就是传统的九宫格),那么四个顶点上的小方块序号分别是,0, 2, 4, 6
; 假设每一行每一列,咱们显示4个小方块,那么四个顶点上的小方块序号分别是,0, 3, 6, 9
; 以此类推,每行每列显示5个小方块,就是 0, 4, 8, 12
;canvas
每行每列小方块数量 | 左上角 | 右上角 | 右下角 | 左下角 |
---|---|---|---|---|
3个 | 0 | 2 | 4 | 6 |
4个 | 0 | 3 | 6 | 9 |
5个 | 0 | 4 | 8 | 12 |
如图2:跨域
聪明的小伙伴们应该已经发现规律了,在图1中,咱们使用的神秘变量 AWARDS_ROW_LEN
,它的做用就是指定九宫格每行每列显示多少个小方块;数组
接着,咱们绘制的原理是:分红四步,从每个顶点开始绘制小方块,直到碰到下一个顶点为止;
咱们会发现,当 AWARDS_ROW_LEN = 3
时,咱们从 0 ~ 1
,从 2 ~ 3
... ,每一次绘制两个小方块;
当 AWARDS_ROW_LEN = 4
时,咱们从0 ~ 2
,从 3 ~ 5
,每一次绘制三个小方块,绘制的步数恰好是 AWARDS_ROW_LEN - 1
;如图3:
AWARDS_TOP_DRAW_LEN
,来表示不一样状况下,每一个顶点绘制的步数;
咱们经过 AWARDS_TOP_DRAW_LEN
这个变量,又能够推算出,任何状况下,矩形四个顶点所在的小方块的下标:
你能够列举多种状况,来验证该公式的正确性
LETF_TOP_POINT = 0,
RIGHT_TOP_POINT = AWARDS_TOP_DRAW_LEN,
RIGHT_BOTTOM_POINT = AWARDS_TOP_DRAW_LEN * 2,
LEFT_BOTTOM_POINT = AWARDS_TOP_DRAW_LEN * 2 + AWARDS_TOP_DRAW_LEN,
复制代码
获得了每一个顶点的下标,那就意味着咱们知道了一个顶点距离另外一个顶点之间,有多少个小方块,那么接下来就很是好办了,
AWARDS_TOP_DRAW_LEN
乘以4,来获取总的奖品个数,做为循环条件(AWARDS_LEN
);SUDOKU_SIZE
);SUDOKU_ITEM_MARGIN
);SUDOKU_ITEM_SIZE
)。变量有点多·若是你感受有点懵逼,请仔细查阅源码
variable.js
中的变量,搞懂每一个变量的表明的意义。
咱们已经拿到全部绘制的条件,接下来只须要写个循环,轻松搞定!
function drawSudoku() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < AWARDS_LEN; i ++) {
// 顶点的坐标
let max_position = AWARDS_TOP_DRAW_LEN * SUDOKU_ITEM_SIZE + AWARDS_TOP_DRAW_LEN * SUDOKU_ITEM_MARGIN;
// ----- 左上顶点
if (i >= LETF_TOP_POINT && i < RIGHT_TOP_POINT) {
let row = i,
x = row * SUDOKU_ITEM_SIZE + row * SUDOKU_ITEM_MARGIN,
y = 0;
// 记录每个方块的坐标
positions.push({x, y});
// 绘制方块
drawSudokuItem(
x, y, SUDOKU_ITEM_SIZE, SUDOKU_ITEM_RADIUS,
awards[i], SUDOKU_ITEM_TXT_SIZE, SUDOKU_ITEM_UNACTIVE_TXT_COLOR,
SUDOKU_ITEM_UNACTIVE_COLOR,
SUDOKU_ITEM_SHADOW_COLOR
);
};
// -----
// ----- 右上顶点
if (i >= RIGHT_TOP_POINT && i < RIGHT_BOTTOM_POINT) {
// ...
};
// -----
// ----- 右下顶点
if (i >= RIGHT_BOTTOM_POINT && i < LEFT_BOTTOM_POINT) {
// ...
}
// -----
// ----- 左下顶点
if (i >= LEFT_BOTTOM_POINT) {
// ...
}
// -----
};
}
复制代码
在绘制九宫格的 drawSudoku()
函数方法中,你会发现,咱们每一步绘制,都将当前小方块的坐标推到了一个 positions
的全局变量中;
这个变量会记录全部小方块的坐标,以及他们的下标;
以后咱们在绘制轮跳的小方块时,就可以经过 setTimeout()
定时器,规定每隔一段时间,经过下标值 jump_index
取出 positions
变量中的某一组坐标信息,并经过该信息中的坐标绘制一个新的小方块,覆盖到原来的小方块上,结束绘制后,jump_index
的值递增;
这便实现了九宫格的轮跳效果。
而绘制这些小方块,咱们封装了一个公共的方法:drawSudokuItem()
;
/** * 绘制单个小方块 * @param {Num} x 坐标 * @param {Num} y 坐标 * @param {Num} size 小方块的尺寸 * @param {Num} radius 小方块的圆角大小 * @param {Str} text 文字内容 * @param {Str} txtSize 文字大小样式 * @param {Str} txtColor 文字颜色 * @param {Str} bgColor 背景颜色 * @param {Str} shadowColor 底部厚度颜色 */
function drawSudokuItem(x, y, size, radius, text, txtSize, txtColor, bgColor, shadowColor) {
// ----- 绘制方块
context.save();
context.fillStyle = bgColor;
context.shadowOffsetX = 0;
context.shadowOffsetY = 4;
context.shadowBlur = 0;
context.shadowColor = shadowColor;
context.beginPath();
roundedRect(
x, y,
size, size,
radius
);
context.fill();
context.restore();
// -----
// ----- 绘制图片与文字
if (text) {
if (text.substr(0, 3) === 'img') {
let textFormat = text.replace('img-', ''),
image = new Image();
image.src = textFormat;
function drawImage() {
context.drawImage(
image,
x + (size * .2 / 2), y + (size * .2 / 2),
size * .8, size * .8
);
};
// ----- 若是图片没有加载,则加载,如已加载,则直接绘制
if (!image.complete) {
image.onload = function (e) {
drawImage();
}
} else {
drawImage();
}
// -----
}
else {
context.save();
context.fillStyle = txtColor;
context.font = txtSize;
context.translate(
x + SUDOKU_ITEM_SIZE / 2 - context.measureText(text).width / 2,
y + SUDOKU_ITEM_SIZE / 2 + 6
);
context.fillText(text, 0, 0);
context.restore();
}
}
// -----
}
复制代码
该方法是一个公共的绘制小方块的方法,它能在初始化时绘制全部“底层”小方块,在动画轮跳是,绘制那个移动中的小方块。
drawSudokuItem() 实现了哪些功能?
global.js
中的一个 roundedRect()
方法,绘制了一个圆角矩形;(本章暂不讨论圆角矩形的绘制方法,若是你感兴趣,能够查看源码,或者 GG 一下)awards
数组来存储奖品信息,若是值是普通的字符串,则在小方块的正中绘制该字符串文字,若是值带有前缀 img-
咱们就将该字符串中的 url 地址,做为图片的地址,渲染到小方块上。绘制方块没啥好讲的,若是你不想用 roudedRect()
方法,你能够直接把它替换成 context.rect()
,除了不是圆角,效果彻底同样。
context.drawImage()
这个方法:先清楚一个概念:
source image
;destination canvas
。语法:
context.drawImage(
HTMLImageElement $image,
int $sourceX, int $sourceY [ , int $sourceW, int $sourceH,
int $destinationX, int $destinationY, int $destinationW, int $destinationH ]
)
复制代码
参数有点多哈,但本章用到的也就前五个,其中前三个是必选,后两个是可选参数:
$image # 能够是 HTMLImageElement 类型的图像对象,
# 也能够是 HTMLCanvasElement 类型的 canvas 对象,
# 或 HTMLVideoElement 类型的视频对象
# 也就是说,它能够将指定 图片,canvas,视频 绘制到指定的 canvas 画布上。
# 能够看到,该方法能够绘制另外一个 canvas,
# 咱们能够经过这个特性实现 离屏canvas;在之后的章节中我会详细的讲解。
$sourceX / Y # 源图像的坐标,用这两个参数控制图片的坐标位置。
$sourceW / H # 源图像的宽高,用这两个参数控制图片的宽度与高度。
复制代码
⚠️ 这个方法有两个坑:
image.onload = function(e) {...}
回调中调用 context.drawImage()
。若是你不知道怎么创建本地服务器的话,我...,愤怒的我当场百度了一篇最简单搭建服务器的教程,童叟无欺!gulp 搭建本地服务器教程
咱们来看如下代码:
if (text.substr(0, 3) === 'img') {
let textFormat = text.replace('img-', ''),
image = new Image();
image.src = textFormat;
function drawImage() {
context.drawImage(
image,
x + (size * .2 / 2), y + (size * .2 / 2),
size * .8, size * .8
);
};
// ----- 若是图片没有加载,则加载,如已加载,则直接绘制
if (!image.complete) {
image.onload = function (e) {
drawImage();
}
} else {
drawImage();
}
// -----
}
复制代码
img
,若是有,便开始绘制图片;image
对象,将该对象的 src
属性赋值;drawImage()
函数方法,在该方法里面,使用 context.drawImage()
方法渲染刚刚定义的 image
对象,并指定相应的图片大小,和尺寸;image.complete
来判断图片是否已加载完成,若是未加载,则先初始化,在 image.onload = function(e) {...}
的回调中调用 drawImage()
方法;若是已经加载完毕,则直接调用 drawImage()
方法。以上,图片就这样渲染完成了,渲染普通文本就不用说了哈,就是普通的 context.fillText()
方法。
咱们已经将外围的小方块绘制完成了,接下来来制做中间的按钮。
按钮的绘制很简单,你们看看源码, 就能轻松理解;
可是这个按钮在 canvas 中,只不过就是一堆像素组成的色块,它不能像 html
中定义的按钮那样,具备点击,鼠标移动等交互功能;
若是咱们想在 canvas 中实现一个按钮,那咱们只能规定当咱们点击 canvas 画布中的某一个区域时,给予用户反馈;
🎉 这里引入一个新的方法,
context.isPointInPath()
; 人如其名,该方法会判断:当前坐标点,是否在当前路径中,若是在,返回 true,不然返回 false。
语法:
context.isPointInPath(int $currentX, int $currentY)
复制代码
两个参数就表明须要进行判断的坐标点。
经过这个方法,咱们能够判断:当前用户点击的位置的坐标,是否位于按钮的路径中,若是返回 true,则执行抽奖动画。
⚠️ 值得注意的是,判断的路径,必须是当前路径,也就是说,咱们在执行判断以前须要从新绘制一遍按钮的路径;源码中的
createButtonPath()
就是为了作这件事情存在的。
咱们来作一个简单的小测试,测试效果如图4:
var canvas = document.getElementById('canvas'),
context = canvas.getContext('2d');
function windowToCanvas(e) {
var bbox = canvas.getBoundingClientRect(),
x = e.clientX,
y = e.clientY;
return {
x: x - bbox.left,
y: y - bbox.top
}
}
context.beginPath();
context.rect(100, 100, 100, 100);
context.stroke();
canvas.addEventListener('click', function (e) {
var loc = windowToCanvas(e);
if (context.isPointInPath(loc.x, loc.y)) {
alert('🎉')
}
});
复制代码
怎么样?炒鸡简单对吧?在咱们这个项目中也是同样的:
button_position
这个变量中;当前路径
,咱们将点击事件 click
中获取的坐标信息传给 context.isPointInPath()
方法,就能够判断,当前的位置,是否在按钮的路径中。['mousedown', 'touchstart'].forEach((event) => {
canvas.addEventListener(event, (e) => {
let loc = windowToCanvas(e);
// 建立一段新的按钮路径,
createButtonPath();
// 判断当前鼠标点击 canvas 的位置,是否在当前路径中,
// 若是为 true,则开始抽奖
if (context.isPointInPath(loc.x, loc.y) && !is_animate) {
// ...
}
})
});
复制代码
咱们将经过点击按钮,来调用 animate()
方法,该方法实现了九宫格抽奖的动画效果。
在点击按钮时,咱们会初始化三个全局变量,jumping_time, jumping_total_time, jumping_change
;
它们分别表示:动画当前时间计时;动画花费的时间总长;动画速率改变的峰值(使用 easeOut
函数方法,单位时间内会将速率由0提高到峰值);
最后咱们将调用 animate()
函数方法,如下是该方法的代码:
function animate() {
is_animate = true;
if (jump_index < AWARDS_LEN - 1) jump_index ++;
else if (jump_index >= AWARDS_LEN -1 ) jump_index = 0;
jumping_time += 100; // 每一帧执行 setTimeout 方法所消耗的时间
// 当前时间大于时间总量后,退出动画,清算奖品
if (jumping_time >= jumping_total_time) {
is_animate = false;
if (jump_index != 0) alert(`🎉恭喜您中得:${awards[jump_index - 1]}`)
else if (jump_index === 0) alert(`🎉恭喜您中得:${awards[AWARDS_LEN - 1]}`);
return;
};
// ----- 绘制轮跳方块
drawSudoku();
drawSudokuItem(
positions[jump_index].x, positions[jump_index].y,
SUDOKU_ITEM_SIZE, SUDOKU_ITEM_RADIUS,
awards[jump_index], SUDOKU_ITEM_TXT_SIZE, SUDOKU_ITEM_ACTIVE_TXT_COLOR,
SUDOKU_ITEM_ACTIVE_COLOR,
SUDOKU_ITEM_SHADOW_COLOR
);
// -----
setTimeout(animate, easeOut(jumping_time, 0, jumping_change, jumping_total_time))
}
复制代码
animate() 函数方法:
is_animate
,该变量用来阻止用户在动画进行时反复点击按钮,使动画不断被调用;该变量初始值为 false
,仅当该变量为 false
时,点击按钮才会进入 animate()
函数;当进入 animate()
函数后,该变量被设置为 true
,结束动画时,又被重置为 false
;jump_index
全局变量的初始值是一个小于等于奖品总数的随机正整数;随着每一帧动画的执行递增,但当他等于奖品总数时,又会被重置为 0,以此循环;咱们使用该变量,来绘制轮跳的小方块;jumping_time
全局变量初始值为0,随着每一帧动画的执行递增,以此来记录动画当前时间点,当这个值大于等于时间总量 jumping_total_time
时,就能够结束动画,并将当前的 jump_index
取出,做为抽中的奖品了;drawSudoku()
方法中第一句代码就是:context.clearRect(0, 0 , canvas.width, canvas.height)
;它用于清理整个画板,并将九宫格重绘出来;drawSudokuItem()
咱们使用这个函数方法,来绘制轮跳的小方块;前面说过,咱们将 jump_index
作为下标,那么咱们就能够在 positions
变量中找到坐标信息,从 awards
变量中,找到奖品信息;setTimeout()
方法,来实现小方块的动画;该方法调用 animate()
方法自己,它的第二个参数,咱们使用了上一章介绍过的缓动函数来定义,这会使动画看上去由快到慢;缓动函数的源码能够在 global.js
中找到。O 啦~全部代码讲解完毕,你的九宫格是否也动起来了?😍
canvas 实现动画的方式不外乎就是清除画板,再从新绘制一个 动做
,理解了它,不管你是用 window.requestAnimateFrame()
仍是 setTimeout() 和 setInterval()
来作动画,都是同样的原理;
九宫格的实现很简单,惟一复杂点的,就须要一系列计算,来绘制一个灵活的九宫格;
九宫格不只能够用来抽奖,也能够用来作一些小游戏,还记得小时候玩过的老虎机么?如图5:
改改样式,找点图片,把值取出来作下分数规则判断,分分钟搞定呢!