这两天在重读《JavaScript 高级程序设计第 4 版》,感觉有了一些新的认识和发现,收获还是挺多的。今天刚刚好阅读到动画和canvas这部分,书中给的一个小示例是一个闹钟
当时觉得挺有意思的,就想着自己着手也实现一个类似的特效(本来准备写论文的,前两天感冒了 ,有点不舒服,进度有点迟缓,不过这不影响写代码(▽))
效果图
实现思路
闹钟的实现思路也比较容易,简单介绍一下思路。先创建一个大圆(闹钟的外层),闹钟的刻度是单个线条,实现单个线条也比较简单,主要就是moveTo和lineTo的使用,结合一些基础的数学知识,获取相应的坐标即可;接着是时针和分针的编写,原理和刻度一样。至于,让指针走起来,我这里使用的是setInterval定时器,每隔一秒刷新一次时间,并且清空画布,重新绘制。文字没有什么好说的,就是简单的api调用,颜色的话,还是有些地方需要小心的,比如怎么让不同的内容拥有不同的颜色,这里不多解释了,想要了解的小伙伴可以点击在同一个 canvas 元素中绘制不同颜色的图形查看。
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
window.onload = function () {
//写逻辑
let canvas = document.getElementById('canvas');
let cxt = canvas.getContext('2d');
const width = 400;
const height = 400;
canvas.width = width;
canvas.height = height;
let x;
let y;
let hour;
let minute;
let second;
cxt.font = "bold 14px Arial";
cxt.textAlign = "center";
cxt.textBaseLine = "middle";
setInterval(() => {
cxt.clearRect(0, 0, width, height);
// 绘制闹钟外壳
cxt.beginPath();
cxt.fillStyle = "#d9ddd8";
cxt.strokeStyle = "#e5d4a8";
cxt.lineWidth = 3;
cxt.arc(200, 200, 200, 0, 2 * Math.PI, false);
cxt.stroke();
cxt.closePath();
cxt.beginPath();
cxt.strokeStyle = "#84c2b3";
// 绘制小时刻度并添加文字
for (let i = 3; i >= -8; i--) {
cxt.lineWidth = 4;
x = 200 * Math.sin(Math.PI / 6 * i);
y = 200 * Math.cos(Math.PI / 6 * i);
cxt.moveTo(200 + y, 200 - x);
cxt.lineTo(200 + y - y / 10, 200 - x + x / 10);
cxt.fillStyle = "black";
if (i >= 1 || (i >= -8 && i <= -7)) {
if (i == 3) {
cxt.fillText("12", 200 + y - y / 10, 200 - x + x / 10 + 15);
}
else {
cxt.fillText(String(3 - i), 200 + y - y / 10, 200 - x + x / 10 + 15);
}
}
else if (i == 0) {
cxt.fillText("3", 200 + y - y / 10 - 10, 200 - x + x / 10 + 5);
}
else if (i == -6) {
cxt.fillText("9", 200 + y - y / 10 + 10, 200 - x + x / 10 + 5);
}
else {
cxt.fillText(String(3 - i), 200 + y - y / 10, 200 - x + x / 10 - 6);
}
cxt.fill();
}
cxt.stroke();
cxt.closePath();
// 绘制分钟刻度
cxt.beginPath();
for (let i = 1; i <= 59; i++) {
cxt.lineWidth = 2;
x = 200 * Math.sin(Math.PI / 30 * i);
y = 200 * Math.cos(Math.PI / 30 * i);
if (i % 5 != 0) {
cxt.moveTo(200 + x, 200 - y);
cxt.lineTo(200 + x - x / 20, 200 - y + y / 20);
}
}
cxt.stroke();
cxt.closePath();
// 绘制秒刻度
cxt.beginPath();
for (let i = 1; i <= 59; i++) {
cxt.lineWidth = 2;
x = 40 * Math.sin(Math.PI / 6 * i);
y = 40 * Math.cos(Math.PI / 6 * i);
cxt.moveTo(130 + x, 280 - y);
cxt.lineTo(130 + x - x / 10, 280 - y + y / 10);
}
cxt.stroke();
cxt.closePath();
hour = new Date().getHours();
minute = new Date().getMinutes();
second = new Date().getSeconds();
// 绘制秒外壳
cxt.beginPath();
cxt.strokeStyle = "#898e8b";
cxt.arc(130, 280, 40, 0, 2 * Math.PI, false);
cxt.stroke();
cxt.closePath();
// 秒针
cxt.beginPath();
cxt.lineCap = 'round';
cxt.lineWidth = 5;
cxt.strokeStyle = "#7c817e";
x = 30 * Math.sin(Math.PI / 30 * second);
y = 30 * Math.cos(Math.PI / 30 * second);
cxt.moveTo(130, 280);
cxt.lineTo(130 + x, 280 - y);
cxt.stroke();
cxt.closePath();
// 分针
cxt.beginPath();
cxt.lineCap = 'round';
cxt.lineWidth = 5;
cxt.strokeStyle = "rgb(227, 140, 99)";
x = 100 * Math.sin(Math.PI / 6 * hour);
y = 100 * Math.cos(Math.PI / 6 * hour);
cxt.moveTo(200, 200);
cxt.lineTo(200 + x, 200 - y);
cxt.stroke();
cxt.closePath();
// 时针
cxt.beginPath();
cxt.lineCap = 'round';
cxt.lineWidth = 5;
cxt.strokeStyle = "rgb(227, 140, 99)";
x = 150 * Math.sin(Math.PI / 30 * minute);
y = 150 * Math.cos(Math.PI / 30 * minute);
cxt.moveTo(200, 200);
cxt.lineTo(200 + x, 200 - y);
cxt.stroke();
cxt.closePath();
}, 1000)
}
</script>
</html>
第二个时钟特效
一些说明
这个时钟特效实现起来更简单,不过就是有点麻烦。参考特效的链接是炫酷时钟特效,感兴趣的掘友可以看看。在前期思考的过程中,关于如何让外层的一圈文字转起来,首先我想到的是变换固定位置的内容,这种思路的缺点就是看起来不太像是转起来的,第二个是使用rotate,不过这个有更致命的缺点
细心的掘友已经看到了,文字也会随着转,目前,还没有能力去解决这个问题。于是,我就选用了第一种思路。
代码
有些地方并没有抽公共函数,见谅。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas id="canvas" style="background-color: black;"></canvas>
</body>
<script>
function getArr(num, benchmark) {
let arr = [];
for (let i = num; i <= benchmark; i++) {
arr.push(i);
}
for (let i = 1; i <= num - 1; i++) {
arr.push(i);
}
return arr;
}
window.onload = function () {
const canvas = document.getElementById('canvas');
const cxt = canvas.getContext('2d');
let x;
let y;
let second;
let secondArr = [];
let minute;
let minuteArr = [];
let hour;
let hourArr = [];
let month;
let monthArr = [];
let day = [];
let dayArr = [];
let year;
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width;
canvas.height = height;
cxt.font = "bold 11px Arial";
setInterval(() => {
cxt.clearRect(0, 0, width, height);
second = new Date().getSeconds() + 1;
minute = new Date().getMinutes() + 1;
hour = new Date().getHours() + 1;
day = new Date().getDate();
month = new Date().getMonth() + 1;
year = new Date().getFullYear();
secondArr = getArr(second,60);
minuteArr = getArr(minute,60);
hourArr = getArr(hour,24);
dayArr = getArr(day+1,31);
monthArr = getArr(month+1,12);
for (let i = 1; i <= 60; i++) {
x = 290 * Math.sin(Math.PI / 30 * i);
y = 290 * Math.cos(Math.PI / 30 * i);
cxt.beginPath();
if (i == 60) {
cxt.fillStyle = '#930974';
if(secondArr[i - 1] <= 9){
cxt.fillText('0'+ secondArr[i - 1]+ '秒', width / 2 + x, height / 2 - y);
}
else{
cxt.fillText(secondArr[i - 1]+ '秒', width / 2 + x, height / 2 - y);
}
}
else {
if(secondArr[i - 1] <= 9){
cxt.fillStyle = '#b2bcc5';
cxt.fillText('0'+ secondArr[i - 1] + '秒', width / 2 + x, height / 2 - y);
}
else{
cxt.fillStyle = '#b2bcc5';
cxt.fillText(secondArr[i - 1] + '秒', width / 2 + x, height / 2 - y);
}
}
cxt.fill();
cxt.closePath();
}
for (let i = 1; i <= 60; i++) {
x = 250 * Math.sin(Math.PI / 30 * i);
y = 250 * Math.cos(Math.PI / 30 * i);
cxt.beginPath();
if (i == 60) {
cxt.fillStyle = '#930974';
if(minuteArr[i - 1] <= 9){
cxt.fillText('0'+ minuteArr[i - 1]+ '分', width / 2 + x, height / 2 - y);
}
else{
cxt.fillText(minuteArr[i - 1]+ '分', width / 2 + x, height / 2 - y);
}
}
else {
if(minuteArr[i - 1] <= 9){
cxt.fillStyle = '#b2bcc5';
cxt.fillText('0'+ minuteArr[i - 1]+ '分', width / 2 + x, height / 2 - y);
}
else{
cxt.fillStyle = '#b2bcc5';
cxt.fillText(minuteArr[i - 1]+ '分', width / 2 + x, height / 2 - y);
}
}
cxt.fill();
cxt.closePath();
}
for (let i = 1; i <= 24; i++) {
x = 200 * Math.sin(Math.PI / 12 * i);
y = 200 * Math.cos(Math.PI / 12 * i);
cxt.beginPath();
if (i == 24) {
cxt.fillStyle = '#930974';
if(hourArr[i - 1] <= 9){
cxt.fillText('0'+ hourArr[i - 1]+ '时', width / 2 + x, height / 2 - y);
}
else{
cxt.fillText(hourArr[i - 1]+ '时', width / 2 + x, height / 2 - y);
}
}
else {
if(hourArr[i - 1] <= 9){
cxt.fillStyle = '#b2bcc5';
cxt.fillText('0'+ hourArr[i - 1] + '时', width / 2 + x, height / 2 - y);
}
else{
cxt.fillStyle = '#b2bcc5';
cxt.fillText(hourArr[i - 1] + '时', width / 2 + x, height / 2 - y);
}
}
cxt.fill();
cxt.closePath();
}
for (let i = 1; i <= 12; i++) {
x = 80 * Math.sin(Math.PI / 6 * i);
y = 80 * Math.cos(Math.PI / 6 * i);
cxt.beginPath();
if (i == 12) {
cxt.fillStyle = '#930974';
if(monthArr[i - 1] <= 9){
cxt.fillText('0'+ monthArr[i - 1] + '月', width / 2 + x, height / 2 - y);
}
else{
cxt.fillText(monthArr[i - 1] + '月', width / 2 + x, height / 2 - y);
}
}
else {
if(monthArr[i - 1] <= 9){
cxt.fillStyle = '#b2bcc5';
cxt.fillText('0'+ monthArr[i - 1] + '月', width / 2 + x, height / 2 - y);
}
else{
cxt.fillStyle = '#b2bcc5';
cxt.fillText(monthArr[i - 1] + '月', width / 2 + x, height / 2 - y);
}
}
cxt.fill();
cxt.closePath();
}
for (let i = 1; i <= 31; i++) {
x = 150 * Math.sin(2 * Math.PI / 31 * i);
y = 150 * Math.cos(2 * Math.PI / 31 * i);
cxt.beginPath();
if (i == 31) {
cxt.fillStyle = '#930974';
if(dayArr[i - 1] <= 9){
cxt.fillText('0'+ dayArr[i - 1] + '日', width / 2 + x, height / 2 - y);
}
else{
cxt.fillText(dayArr[i - 1]+ '日', width / 2 + x, height / 2 - y);
}
}
else {
if(dayArr[i - 1] <= 9){
cxt.fillStyle = '#b2bcc5';
cxt.fillText('0'+ dayArr[i - 1]+ '日', width / 2 + x, height / 2 - y);
}
else{
cxt.fillStyle = '#b2bcc5';
cxt.fillText(dayArr[i - 1]+ '日', width / 2 + x, height / 2 - y);
}
}
cxt.fill();
cxt.closePath();
}
cxt.beginPath();
cxt.fillStyle = '#930974';
cxt.fillText(year + '年',width / 2 - 5,height / 2 - 5);
cxt.closePath();
secondArr.length = 0;
minuteArr.length = 0;
hourArr.length = 0;
monthArr.length = 0;
dayArr.length = 0;
}, 1000)
}
</script>
</html>