前置内容
Canvas实现简易数字电子时钟
Canvas实现数字电子时钟(带粒子掉落效果)
效果
逻辑代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>粒子时钟-完整版</title>
<style>
body {
margin: 0;
overflow: hidden
}
#canvas {
background-image: url("./images/back.jpg");
background-size: cover;
background-position: center;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const [width, height] = [window.innerWidth, window.innerHeight];
const canvas = document.getElementById('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
/*1.建立四张图片作为图像*/
const imgH = new Image();
imgH.src = './images/h.png';
const imgM = new Image();
imgM.src = './images/m.png';
const imgS = new Image();
imgS.src = './images/s.png';
const imgO = new Image();
imgO.src = './images/o.png';
/*2.声明钟表的基本数据*/
//钟表数字
const numDt = [
//0
[
1, 1, 1, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 1, 1, 1
],
//1
[
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1
],
//2
[
1, 1, 1, 1,
0, 0, 0, 1,
0, 0, 0, 1,
1, 1, 1, 1,
1, 0, 0, 0,
1, 0, 0, 0,
1, 1, 1, 1
],
//3
[
1, 1, 1, 1,
0, 0, 0, 1,
0, 0, 0, 1,
1, 1, 1, 1,
0, 0, 0, 1,
0, 0, 0, 1,
1, 1, 1, 1
],
//4
[
1, 0, 0, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 1, 1, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1
],
//5
[
1, 1, 1, 1,
1, 0, 0, 0,
1, 0, 0, 0,
1, 1, 1, 1,
0, 0, 0, 1,
0, 0, 0, 1,
1, 1, 1, 1
],
//6
[
1, 1, 1, 1,
1, 0, 0, 0,
1, 0, 0, 0,
1, 1, 1, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 1, 1, 1
],
//7
[
1, 1, 1, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1
],
//8
[
1, 1, 1, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 1, 1, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 1, 1, 1
],
//9
[
1, 1, 1, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 1, 1, 1,
0, 0, 0, 1,
0, 0, 0, 1,
1, 1, 1, 1
],
//:
[
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 1, 0,
0, 0, 0, 0,
0, 0, 1, 0,
0, 0, 0, 0,
0, 0, 0, 0
],
];
//项目数量
const itemNum = 8;
//项目间隙的数量
const itemSpNum = itemNum - 1;
//项目间隙的宽度
const itemSpace = 24;
//粒子尺寸
const partSize = 24;
//项目的列数,行数
const [itemColNum, itemRowNum] = [4, 7];
//项目宽度
const itemWidth = partSize * itemColNum;
//时钟宽度
const clockWidth = itemWidth * itemNum + itemSpace * itemSpNum;
//时钟的高度
const clockHeight = partSize * itemRowNum;
//时钟的位置,放在屏幕中间
const clockPos = {
x: (width - clockWidth) / 2,
y: (height - clockHeight) / 5
};
//所有粒子的边界
const edge = { left: 0, right: width, bottom: height - 50 };
/*粒子对象*/
class Particle {
constructor(width, height, img) {
//尺寸
this.width = width;
this.height = height;
//图像
this.img = img;
//位置
this.x = 0;
this.y = 0;
//1:新生,0:坠落,2:成型
this.state = 1;
//父级
this.parent = null;
//缩放
this.scale = 0;
//速度
this.vx = this.getVx();
this.vy = 0.002;
//重力
this.gravity = 0.03;
//弹力
this.bounce = -0.85;
}
//获取x 轴的速度,避免直上直下的弹动
//vx 取值范围是[-0.5,0.5],但不能在[-0.15,0.15] 之间
getVx() {
let vx = Math.random() - 0.5;
if (Math.abs(vx) < 0.15) {
return this.getVx();
} else {
return vx;
}
}
//更新数据
update(diff) {
//解构状态、尺寸和位置
const { state, width, height, parent } = this;
//解构边界
const { left, right, bottom } = edge;
if (state === 1) {
//如果粒子的状态为新生状态
//开始生长
this.scale += 0.01 * diff;
//长到1 就算成型,不要长了
if (this.scale > 1) {
this.scale = 1;
this.state = 2;
}
} else if (state === 0) {
//如果粒子的状态为坠落状态
this.vy += this.gravity;
//设置粒子位置
this.y += this.vy * diff;
this.x += this.vx * diff;
//底部碰撞检测
if (this.y + height > bottom) {
//将粒子位置调整到底部边界之上
this.y = bottom - height;
//y 值反弹
this.vy *= this.bounce;
}
//左右碰撞检测
if (this.x + width < left || this.x > right) {
//将此元素从父对象的数组中删除
parent.remove(this);
}
}
}
//绘图方法
draw(ctx) {
//解构位置、尺寸、缩放
const { x, y, img, scale } = this;
ctx.save();
//变换坐标系位置
ctx.translate(x, y);
//缩放坐标系
ctx.scale(scale, scale);
//绘图
ctx.drawImage(img, 0, 0);
ctx.restore();
}
}
/*粒子粒子发射器*/
class Gun {
constructor(width, height, img) {
//尺寸
this.width = width;
this.height = height;
//图像
this.img = img;
//位置
this.x = 0;
this.y = 0;
//状态 1:粒子发射器的枪膛中有粒子;0:粒子发射器的枪膛中没有粒子。默认没有粒子。
this._state = 0;
//粒子库
this.children = [];
}
get state() {
return this._state;
}
set state(val) {
//在为state 赋值时,做简单的diff 判断
if (this._state !== val) {
if (val) {
//制造一个粒子
this.createParticle();
} else {
//粒子仓库中的第一个粒子发射
this.children[0].state = 0;
}
this._state = val;
}
}
/*新建粒子*/
createParticle() {
const { x, y, width, height, img, children } = this;
//实例化粒子对象
const part = new Particle(width, height, img);
//粒子位置
part.x = x;
part.y = y;
//粒子父级
part.parent = this;
//粒子状态 1:粒子发射器中要有粒子
part.state = 1;
//将粒子以前置的方式添加到粒子仓库里
children.unshift(part);
}
/*删除粒子
* ele 粒子对象
* */
remove(ele) {
const { children } = this;
const index = children.indexOf(ele);
if (index !== -1) {
children.splice(index, 1);
}
}
/*更新
* diff:以毫秒为单位的时间差
*/
update(diff) {
//遍历粒子仓库中的所有粒子
this.children.forEach((ele) => {
//更新粒子
ele.update(diff);
})
}
//绘制辅助线
drawStroke(ctx) {
const { x, y, width, height } = this;
ctx.save();
ctx.strokeStyle = '#aaa';
ctx.strokeRect(x, y, width, height);
ctx.restore();
}
}
/*items 项目集合
* items=[项目,项目,项目,项目,项目,项目,项目,项目],其中有8个项目
* 项目=[4*7=28个粒子发射器]
* 1个粒子发射器中有0个或多个粒子
* */
const items = [];
//建立八个项目
for (let i = 0; i < itemNum; i++) {
//建立项目
const item = [];
//当前项目x 位置
let curItemX = (itemWidth + itemSpace) * i + clockPos.x;
//当前项目对应的图片
let curImg;
switch (i) {
//小时图
case 0:
case 1:
curImg = imgH;
break;
//冒号图
case 2:
case 5:
curImg = imgO;
break;
//分钟图
case 3:
case 4:
curImg = imgM;
break;
//秒图
case 6:
case 7:
curImg = imgS;
break;
}
//每个项目都是7列,4行。所以遍历列和行,建立粒子发射器。
for (let r = 0; r < itemRowNum; r++) {
//发射器的y 位置
const partY = partSize * r + clockPos.y;
//遍历列
for (let c = 0; c < itemColNum; c++) {
//粒子发射器的x 位置
const partX = partSize * c + curItemX;
//实例粒子发射器对象
const gun = new Gun(partSize, partSize);
//设置粒子发射器的位置
gun.x = partX;
gun.y = partY;
//设置图像
gun.img = curImg;
//粒子发射器绘制辅助线
//part.drawStroke(ctx);
//将粒子发射器添加到项目中
item.push(gun);
}
}
//将项目添加到项目集合里
items.push(item);
}
/*先示配代表了冒号的两个项目*/
fitNum(10, 2);
fitNum(10, 5);
/*基于时钟文字,修改项目中每个粒子发射器的状态的方法
* numInd 数字在numDt数据集合中的索引
* itemInd 项目在项目集合中的索引位置
* */
function fitNum(numInd, itemInd) {
const item = items[itemInd];
numDt[numInd].forEach((ele, ind) => {
item[ind].state = ele;
})
}
/*计时器*/
let time = new Date();
/*updateTime() 获取时间差和时、分、秒的方法
* 此处的时、分、秒是由两个数字组成的数组,如1点=[0,1],10点=[1,0]
* */
function updateTime() {
//获取时间差
const now = new Date();
const diff = now - time;
time = now;
//返回时间差和时、分、秒
return {
diff,
hour: parseTime(time.getHours()),
minute: parseTime(time.getMinutes()),
second: parseTime(time.getSeconds())
};
}
//解析时间,如30分钟,解析为[3,0]
function parseTime(t) {
let arr;
if (t < 10) {
arr = [0, t];
} else {
const str = t.toString();
arr = [parseInt(str[0]), parseInt(str[1])]
}
return arr;
}
/*更新时钟的时间:基于数组类型的时、分、秒更新时钟*/
function updateClock(hour, minute, second) {
//匹配小时
fitNum(hour[0], 0);
fitNum(hour[1], 1);
//匹配分钟
fitNum(minute[0], 3);
fitNum(minute[1], 4);
//匹配秒
fitNum(second[0], 6);
fitNum(second[1], 7);
}
//渲染
!(function render() {
//获取时间差和时分秒
const { diff, hour, minute, second } = updateTime();
//更新时钟的时间
updateClock(hour, minute, second);
//清理画布
ctx.clearRect(0, 0, width, height);
//坠落中的粒子,要最后渲染,避免其被新生的和成型的粒子遮挡
const dropParts = [];
//先渲染state 状态不为0 的元素
items.flat().forEach((part) => {
part.update(diff);
part.children.forEach((ele) => {
if (ele.state) {
ele.draw(ctx);
} else {
dropParts.push(ele);
}
})
});
//后渲染状态为0 的元素
dropParts.forEach((ele) => {
ele.draw(ctx);
});
//请求动画帧
requestAnimationFrame(render);
})()
</script>
</body>
</html>
用到的图片
h.png:
m.png:
s.png:
o.png:
back.jpg: