Canvas绘制老友记时钟
前言
一直做3D/2D可视化,Canvas API和三角函数,空间几何是基础。在官网上看了一遍Canvas API之后,决定绘制一个老友记时钟来巩固知识点,本文用实际代码讲解绘制过程。
代码
HTML
<canvas id="myCanvas" width="300" height="300"></canvas>
Javascript
const canvas = document.getElementById("myCanvas");
const bgImage = new Image();
const ctx = canvas.getContext("2d");
bgImage.src = "https://thumbnail1.baidupcs.com/thumbnail/cc3e81310m71ea6cdb2c5755bda0dc0c?fid=1099650259173-250528-406088947420032&rt=pr&sign=FDTAER-DCb740ccc5511e5e8fedcff06b081203-VVyEw%2bgld39Yr5Tjp%2f1KbwKqa4M%3d&expires=8h&chkbd=0&chkv=0&dp-logid=409019635711974061&dp-callid=0&time=1718445600&size=c1512_u982&quality=90&vuk=1099650259173&ft=image&autopolicy=1";
// 时钟半径
const r = 100
bgImage.onload = function () {
render();
setInterval(function(){
render();
}, 1000);
// 每一帧都先用canvas.clearRect(x,y,w,h)擦掉画布上的像素,否则会造成当前像素和之前的像素叠加的问题。将画布的原点移到画布的中心,有助于绘制刻度和以中心为基点旋转的指针,在之前得保存平移之前的环境状态。
function render() {
drawClockBackGround();
drawHourTicks();
drawTime();
ctx.restore();
}
// 时针、分针、秒针的做法是一致的,使用canvas.rotate()绕原点旋转,旋转之前都要canvas.save()保存当前状态(指针的每一帧动作都是让画布旋转特定的角度,所以画完一次要摆正一次画布,否则秒针旋转一次,分针会在此基础上旋转)
function drawTime(){
var now = new Date();
h = now.getHours();
m = now.getMinutes();
s = now.getSeconds();
ctx.strokeStyle = 'black';
drawHour(h,m);
drawMinute(m,s);
drawSecond(s);
}
function drawHour(h, m) {
const hour = h + m/60;
ctx.save();
ctx.beginPath();
ctx.rotate(hour * 2 * Math.PI / 12); // 时针旋转一周是12小时
ctx.lineWidth = 4;
ctx.moveTo(0, 0.2 * 0.4 * r);
ctx.lineTo(0, -0.8 * 0.4 * r);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
function drawMinute(m, s) {
const minute = m + s/60;
ctx.save();
ctx.beginPath();
ctx.rotate(minute * 2 * Math.PI / 60); // 分针旋转一周是60分钟
ctx.lineWidth = 2;
ctx.moveTo(0, 0.2 * 0.6 * r);
ctx.lineTo(0, -0.8 * 0.6 * r);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
function drawSecond(s) {
ctx.save();
ctx.beginPath();
ctx.rotate(s * 2 * Math.PI / 60); // 秒针旋转一周是60秒
ctx.lineWidth = 2;
ctx.moveTo(0, 0.2 * 0.8 * r);
ctx.lineTo(0, -0.8 * 0.8 * r);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
function drawHourTicks() {
const hourTickLength = 5; // 刻度的长度
const hourTickColor = "yellow";
const gap = 10; // 刻度起始位置距离表盘边缘的间隔
for (let i = 0; i * Math.PI / 6 < 2 * Math.PI; i ++) {
const angle = i * Math.PI / 6;
ctx.beginPath();
ctx.moveTo((r - gap) * Math.cos(angle), (r - gap) * Math.sin(angle));
ctx.lineTo((r - gap + hourTickLength) * Math.cos(angle), (r - gap + hourTickLength) * Math.sin(angle));
ctx.strokeStyle = hourTickColor;
ctx.stroke();
ctx.closePath();
}
}
function drawClockBackGround() {
// 清除canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
// 将坐标系原点平移到画布中心位置
ctx.translate(canvas.width / 2, canvas.height / 2);
// 绘制圆形遮罩(实际上绘制一个圆形,并用白色填充)
ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.closePath();
// 表盘背景图片
drawBGImage();
// 指针交汇处的圆形
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.arc(0, 0, 5, 0, Math.PI * 2, false);
ctx.fill();
ctx.closePath();
// 描边表盘轮廓
ctx.beginPath();
ctx.lineWidth = 2;
ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.strokeStyle = 'black';
ctx.stroke();
ctx.closePath();
}
function drawBGImage() {
ctx.fillStyle = 'white'; // 遮罩颜色,通常与背景色相同
ctx.fill();
// 设置globalCompositeOperation为'source-in',这样接下来的绘制只会在遮罩区域内显示
ctx.globalCompositeOperation = 'source-in';
// 绘制背景图片,它现在只会在圆形区域内显示
const scale = 1;
const scaledWidth = bgImage.width * scale;
const scaledHeight = bgImage.height * scale;
ctx.drawImage(bgImage, 0, 0, scaledWidth, scaledHeight, - canvas.width / 2, - canvas.height / 2, canvas.width, canvas.height);
// 重置globalCompositeOperation以便后续绘制不受影响
ctx.globalCompositeOperation = 'source-over';
}
};
效果
链接
在线演练请参考: CodePen