今日分享: 使用OffscreenCanvas + Worker 实现时钟动画
思路:使用OffscreenCanvas(离屏canvas) 和 Worker 多线程
代码:
clock.js
// 创建canvas对象
function CreateClockCanvas({container, width=400, height=400 }){
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
container.appendChild(canvas);
const ctx = canvas.getContext('2d');
this.ctx = ctx;
this.width = width;
this.height = height;
}
CreateClockCanvas.prototype = {
drawImage(imageBitmap){
let {ctx, width, height} = this;
ctx.clearRect(0, 0, width, height);
ctx.drawImage(imageBitmap,0,0);
}
}
// 背景canvas
let clockBg = new CreateClockCanvas({
container: document.querySelector("#clockBg"),
});
// 指针canvas
let clock = new CreateClockCanvas({
container: document.querySelector("#clock"),
});
const worker = new Worker('./offscreenCanvas/clockWorker.js');
worker.postMessage({msg: 'start'});
worker.onmessage= (e)=>{
switch(e.data.type){
case 'clock':
clockBg.drawImage(e.data.imageBitmap);
break;
case 'run':
clock.drawImage(e.data.imageBitmap);
break;
}
}
clockWorker.js
onmessage = (e) => {
if (e.data.msg == 'start') {
let clock = new CreateClockOffScreenCanvas({});
// 刻画时钟
clock.clockDraw();
requestAnimationFrame(clock.run.bind(clock))
}
}
function CreateClockOffScreenCanvas({ width = 512, height = 512 }) {
const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext('2d')
this.offscreen = offscreen;
this.ctx = ctx;
this.width = width;
this.height = height;
this.zeroX = 200;
this.zeroY = 200;
this.radius = 100;
// 时针 分针 秒针的长度初始化
this.hourHand = this.radius * 0.3;
this.minuteHand = this.radius * 0.5;
this.secondHand = this.radius * 0.7;
// 刻度长度
this.scaleRadius = this.radius * 0.05;
this.data = null;
// 0-59刻度的角度收集
this.angles = null;
this.timeTable = null;
this.initData();
}
CreateClockOffScreenCanvas.prototype = {
// 数据初始化
initData() {
let { radius, scaleRadius, zeroX, zeroY } = this;
console.log(scaleRadius, radius);
let data = [];
let moreR = 0;
let angles = [];
let timeTable = [];
for (let i = 0; i < 360; i += 6) {
let angle = (Math.PI * 2 * i) / 360;
angles.push(angle - Math.PI / 2);
let x = Math.cos(angle) * radius;
let y = Math.sin(angle) * radius;
if (i % (360 / 12) === 0) {
moreR = scaleRadius + 3;
} else {
moreR = 0;
}
let scaleX = Math.cos(angle) * (scaleRadius + moreR);
let scaleY = Math.sin(angle) * (scaleRadius + moreR);
data.push({
start: [x + zeroX, y + zeroY],
end: [x + zeroX - scaleX, y + zeroY - scaleY],
});
}
this.data = data;
this.angles = angles;
for (let i = 0; i < 12; i++) {
let angle = (Math.PI * 2 * i) / 12 - Math.PI / 3;
timeTable.push({
time: i + 1,
point: [
Math.cos(angle) * radius * 0.79 + zeroX - 6,
Math.sin(angle) * radius * 0.79 + zeroY + 6
],
})
}
this.timeTable = timeTable;
},
// 时钟刻画
clockDraw() {
let {
ctx, width, height,
data, offscreen, zeroY, zeroX, radius,
timeTable,
} = this;
// 清除画布
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = 'white';
ctx.strokeStyle = '#000'
ctx.beginPath();
ctx.arc(zeroX, zeroY, radius, 0, Math.PI * 2, false);
ctx.fill();
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = '#000';
ctx.beginPath();
for (let index = 0; index < data.length; index++) {
ctx.moveTo(...data[index].start);
ctx.lineTo(...data[index].end);
}
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = '#000';
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(zeroX, zeroY, 5, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillStyle = '#000'
// 字体样式
ctx.font = "16px Verdana"
for (let i = 0; i < timeTable.length; i++) {
ctx.fillText(timeTable[i].time, ...timeTable[i].point);
}
ctx.stroke();
const imageBitmap = offscreen.transferToImageBitmap();
postMessage({ type: 'clock', imageBitmap: imageBitmap });
},
// 时钟开始工作
run() {
let {
angles, ctx, minuteHand,
hourHand, secondHand, zeroX, zeroY, offscreen
} = this;
let date = new Date();
let hour = date.getHours();
let minute = date.getMinutes();
let second = date.getSeconds();
let times = {
seconds: {
start: [zeroX, zeroY],
end: [
(zeroX + Math.cos(angles[second]) * secondHand),
(zeroY + Math.sin(angles[second]) * secondHand),
]
},
minutes: {
start: [
Math.cos((Math.PI * 2 / 60) * minute) + zeroX,
Math.cos((Math.PI * 2 / 60) * minute) + zeroY
],
end: [
zeroX + Math.cos(angles[minute]) * minuteHand,
zeroY + Math.sin(angles[minute]) * minuteHand,
]
},
hours: {
start: [
Math.cos((Math.PI * 2 / 60) * hour) + zeroX,
Math.cos((Math.PI * 2 / 60) * hour) + zeroY
],
end: [
zeroX + Math.cos(angles[hour]) * hourHand,
zeroY + Math.sin(angles[hour]) * hourHand,
]
},
}
// 时针
ctx.beginPath();
ctx.strokeStyle = '#000';
ctx.moveTo(...times.hours.end)
ctx.lineTo(...times.hours.start);
ctx.closePath();
ctx.stroke()
// 分针
ctx.beginPath();
ctx.strokeStyle = '#000';
ctx.moveTo(...times.minutes.end)
ctx.lineTo(...times.minutes.start);
ctx.closePath();
ctx.stroke()
// 秒针
ctx.beginPath();
ctx.strokeStyle = 'red';
ctx.moveTo(...times.seconds.end)
ctx.lineTo(...times.seconds.start);
ctx.closePath();
ctx.stroke()
const imageBitmap = offscreen.transferToImageBitmap();
postMessage({ type: 'run', imageBitmap: imageBitmap });
requestAnimationFrame(this.run.bind(this))
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
}
.container{
position: relative;
}
.clock-canvas{
position: absolute;
}
#clockBg{
background-color: #282828;
z-index: 100;
}
#clock{
z-index: 200;
}
</style>
</head>
<body>
<div class="container">
<div id="clockBg" class="clock-canvas"></div>
<div id="clock" class="clock-canvas"></div>
</div>
<script type="module">
import './clock.js'
</script>
</body>
</html>