UNPKG

concise-clock

Version:

一个基于h5 canvas的模拟时钟javascript程序

400 lines (399 loc) 15.7 kB
export default class Clock { constructor(canvas, options = {}) { /**默认选项 */ this.options = { size: 300, padding: 5, borderWidth: 15, borderColor: "black", scaleType: "arabic", scaleColor: "#666", hourColor: "#666", backgroundColor: "white", secondHandColor: "red", minuteHandColor: "#666", hourHandColor: "black", backgroundMode: "full", showShadow: true, handType: "triangle", backgroundAlpha: 0.5, }; this.interval = null; this.hours = []; //小时数字 this.largeScale = 0; //大刻度长度 this.smallScale = 0; //小刻度长度 this.hourFontSize = 0; //小时数字字体大小 this.headLen = 0; //针头长度 this.secondHandLen = 0; //秒针长度 this.minuteHandLen = 0; //分针长度 this.hourHandLen = 0; //时针长度 this.borderPattern = null; /**画布大小的一半 */ this.halfSize = 0; /**外面传过来的要显示的canvas */ this.container = null; this.ctx = null; /**表盘 */ this.dialCanvas = document.createElement("canvas"); this.dialCtx = this.dialCanvas.getContext("2d"); if (!canvas) { throw new Error("请传入canvas参数!"); } let container = canvas; if ("string" == typeof canvas) { container = document.getElementById(canvas); } if (!(container instanceof HTMLCanvasElement)) { throw new Error("传入的canvas参数不是一个HTMLCanvasElement对象!"); } this.container = container; this.ctx = container.getContext("2d"); this.setOptions(options); } async init() { const { size, borderWidth, borderImage, padding, scaleType = "arabic", backgroundImage, onload } = this.options; this.halfSize = size * 0.5; this.dialCanvas.width = this.container.width = size; this.dialCanvas.height = this.container.height = size; //大刻度线的长度为内圈半径的十二分之一 this.largeScale = (this.halfSize - padding - borderWidth) / 12; //小刻度线的长度为大刻度线的一半 this.smallScale = this.largeScale * 0.5; this.hourFontSize = this.largeScale * 1.2; this.headLen = this.smallScale * 1.5; this.secondHandLen = this.headLen * 12; this.minuteHandLen = this.headLen * 10; this.hourHandLen = this.headLen * 7; //平移坐标轴,将左上角的(0,0)点平移到画布中心。 this.ctx.translate(this.halfSize, this.halfSize); this.dialCtx.translate(this.halfSize, this.halfSize); if ("roman" == scaleType) { this.hours = ["XII", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI"]; } else if ("arabic" == scaleType) { this.hours = ["12", "1", "2", "3", "4", "5", "6", "7", '8', "9", "10", "11"]; } else { this.hours = []; } if (borderImage) { this.borderPattern = await this.createPattern(this.dialCtx, borderImage, "repeat"); } if (backgroundImage) { this.backgroundImage = await this.createImage(backgroundImage); } this.drawDial(this.dialCtx); if (onload instanceof Function) { onload(this); } } /** * 极坐标转平移后画布坐标 * ps:极坐标极轴水平向上,角度正方向顺时针 * ps:画布坐标是平移后的画布坐标,坐标原点画布中心,x轴水平向右,y轴竖直向下 * @param r 当前点到原点的长度 * @param radian 弧度 */ polarCoordinates2canvasCoordinates(r, radian) { //极轴竖直向上极坐标 转 极轴水平向右极坐标 radian -= Math.PI * 0.5; //角度向右旋转90度即可 //极轴水平向右极坐标转平移后画布坐标(x轴水平向右,y轴竖直向下) let x = r * Math.cos(radian); let y = r * Math.sin(radian); return { x, y }; } /**绘制小时的文字 */ drawHours(ctx, i, hour, end) { ctx.save(); ctx.fillStyle = this.options.hourColor; ctx.font = `${this.hourFontSize}px 微软雅黑`; var w = ctx.measureText(hour).width; var h = this.hourFontSize; var { x, y } = end; //i为 0-11 对应1-12个小时数字(12开始,11结束) var padding = 5; switch (i) { case 0: //12 x -= w * 0.5; y += h; break; case 1: x -= w; y += h; break; case 2: x -= w + padding; y += h - padding; break; case 3: x -= w + padding; y += h * 0.5; break; case 4: x -= w + padding; break; case 5: x -= w; break; case 6: x -= w * 0.5; y -= padding; break; case 8: x += padding; break; case 9: x += padding; y += h * 0.5; break; case 10: x += padding; y += h - padding; break; case 11: y += h; break; } ctx.fillText(hour, x, y); ctx.restore(); } createImage(src) { return new Promise((resolve, reject) => { let img = new Image(); img.onload = () => { resolve(img); }; img.onerror = () => { reject(new Error("图片加载出错!")); this.stop(); //停止 }; img.src = src; }); } createPattern(ctx, src, repetition) { return new Promise((resolve, reject) => { let img = new Image(); img.onload = () => { resolve(ctx.createPattern(img, repetition)); }; img.onerror = () => { reject(new Error("图片加载出错!")); this.stop(); //停止 }; img.src = src; }); } /**绘制表盘 */ drawDial(ctx) { const { padding, borderWidth, borderColor, borderImage, scaleColor, backgroundColor, backgroundImage, backgroundMode, backgroundAlpha, showShadow } = this.options; const hours = this.hours; const halfSize = this.halfSize; const shadowBlur = 10; const shadowOffset = 5; //--------外圈 ctx.save(); const x = 0; const y = 0; const outsideR = halfSize - padding - (showShadow ? shadowBlur + shadowOffset : 0); ctx.arc(x, y, outsideR, 0, 2 * Math.PI, true); if (borderImage && this.borderPattern) { //边框背景图 ctx.fillStyle = this.borderPattern; } else { //边框颜色 ctx.fillStyle = borderColor; } //--------内圈 利用相反缠绕可形成内阴影 const insideR = outsideR - borderWidth; ctx.arc(x, y, insideR, 0, 2 * Math.PI, false); if (showShadow) { ctx.shadowBlur = shadowBlur; ctx.shadowColor = "#666"; ctx.shadowOffsetX = shadowOffset; ctx.shadowOffsetY = shadowOffset; } ctx.fill(); ctx.restore(); //--------内圈的背景图或背景色 ctx.beginPath(); ctx.save(); if (backgroundImage && this.backgroundImage) { //背景图 const { width, height } = this.backgroundImage; const r = "full" == backgroundMode ? insideR : insideR - this.largeScale - this.hourFontSize - 15; ctx.globalAlpha = backgroundAlpha; ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.clip(); //按内圈区域裁剪图片 //最小的一边要刚好能显示完全 ,r * 2直径 const scale = r * 2 / Math.min(width, height); ctx.drawImage(this.backgroundImage, -r, -r, width * scale, height * scale); } else if ("white" != backgroundColor) { //背景色,若背景色是白色,就不必填充,因为原本就是白色,并且不填充可以渲染出内阴影效果 ctx.arc(x, y, insideR, 0, 2 * Math.PI); ctx.fillStyle = backgroundColor; ctx.fill(); } ctx.restore(); //--------刻度线和刻度值 //一圈被分成60份,每一份的度数是360/60=6度,转换为弧度(Math.PI/180)*6=Math.PI/30 const unit = Math.PI / 30; for (let scale = 0; scale < 60; scale++) { //从12点到11点59秒顺时针 const radian = unit * scale; const start = this.polarCoordinates2canvasCoordinates(insideR, radian); const len = 0 == scale % 5 ? this.largeScale : this.smallScale; const end = this.polarCoordinates2canvasCoordinates(insideR - len, radian); ctx.beginPath(); ctx.save(); if (0 == scale % 5) { ctx.lineWidth = 3; if (hours && hours.length == 12) { const hourIndex = scale / 5; this.drawHours(ctx, hourIndex, hours[hourIndex], end); } } else { ctx.lineWidth = 1; } ctx.strokeStyle = scaleColor; ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke(); ctx.restore(); } } /**绘制时针 */ drawHand(ctx, time = new Date()) { let { secondHandColor, minuteHandColor, hourHandColor } = this.options; /* * 一圈被分、秒成分了60份,每一份的度数为:6度 转换成弧度:Math.PI/30 * 一圈被时成了12份,每一份的度数为:30度 转换成弧度:Math.PI/6 * 分针每走完一圈,时针就会慢慢过度到一个大刻度, * 那么分针每走一个小刻度,时针在每个大刻度(大刻度之间的度数为30度)之间过度的角度为:30/60 = 0.5度 转换成弧度:Math.PI/360 */ this.drawNeedle(ctx, time.getHours() * Math.PI / 6 + time.getMinutes() * Math.PI / 360, hourHandColor, this.hourHandLen); this.drawNeedle(ctx, time.getMinutes() * Math.PI / 30, minuteHandColor, this.minuteHandLen); this.drawNeedle(ctx, time.getSeconds() * Math.PI / 30, secondHandColor, this.secondHandLen); } /**绘制指针 */ drawNeedle(ctx, radian, color, len) { let { handType } = this.options; if ("triangle" == handType) { //三角形类型指针 const end = this.polarCoordinates2canvasCoordinates(len, radian); const needleWidth = 6; const left = this.polarCoordinates2canvasCoordinates(needleWidth, radian - 0.5 * Math.PI); const right = this.polarCoordinates2canvasCoordinates(needleWidth, radian + 0.5 * Math.PI); ctx.beginPath(); ctx.save(); ctx.moveTo(end.x, end.y); ctx.lineTo(left.x, left.y); ctx.lineTo(right.x, right.y); ctx.closePath(); ctx.fillStyle = color; ctx.shadowBlur = 5; ctx.shadowColor = "#666"; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; ctx.fill(); if (len == this.secondHandLen) { ctx.beginPath(); //表盘中心圆点 ctx.beginPath(); ctx.fillStyle = "yellow"; ctx.arc(0, 0, needleWidth + 2, 0, 2 * Math.PI); ctx.fill(); } ctx.restore(); } else { //线条类型指针 const start = this.polarCoordinates2canvasCoordinates(this.headLen, radian - Math.PI); const end = this.polarCoordinates2canvasCoordinates(len, radian); ctx.beginPath(); ctx.save(); ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.strokeStyle = color; if (len == this.hourHandLen) { ctx.lineWidth = 3; } else if (len == this.minuteHandLen) { ctx.lineWidth = 2; } ctx.stroke(); if (len == this.secondHandLen) { ctx.beginPath(); ctx.fillStyle = color; //表盘中心圆点 ctx.arc(0, 0, 3, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); //秒针针尾圆点 const { x, y } = this.polarCoordinates2canvasCoordinates(len - 10, radian); ctx.arc(x, y, 2, 0, 2 * Math.PI); ctx.fill(); } ctx.restore(); } } /** * 更新options,调用此方法可更新模拟时钟的一些属性 * @param options */ setOptions(options = {}) { let opts = {}; Object.keys(options).forEach(key => { const val = options[key]; if (val !== undefined) { //过滤掉值为undefined的 opts[key] = val; } }); this.options = Object.assign({}, this.options, opts); this.init(); } /** * 显示一个时间 * @param time 默认值当前时间 */ show(time) { const { size, borderImage, backgroundImage } = this.options; const { ctx, hourFontSize } = this; this.ctx.clearRect(-this.halfSize, -this.halfSize, size, size); if ((borderImage && !this.borderPattern) || (backgroundImage && !this.backgroundImage)) { ctx.save(); ctx.font = `${hourFontSize}px 微软雅黑`; ctx.fillText("loading...", this.halfSize, this.halfSize); ctx.stroke(); return; } //表盘 ctx.drawImage(this.dialCanvas, -this.halfSize, -this.halfSize); if ("string" == typeof time) { if (!/^\d{1,2}(:\d{1,2}){2}$/.test(time)) { throw new Error("参数格式:HH:mm:ss"); } let [h, m, s] = time.split(":").map(o => parseInt(o)); time = new Date(); time.setHours(h); time.setMinutes(m); time.setSeconds(s); } //时针 this.drawHand(ctx, time); return this; } /**运行模拟时钟 */ run() { this.show(); if (!this.interval) { this.interval = setInterval(() => { this.show(); }, 1000); } return this; } /**停止模拟时钟 */ stop() { if (this.interval) { clearInterval(this.interval); this.interval = null; } } } window.Clock = Clock;