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 = 100bgImage.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