如何使用h5 canvs实现一个模拟时钟

注: 本例使用es6语法编写,源码采用typescript编写。

本例效果图

思路

1、性能节省: 将模拟时钟分成两个部分,表盘和指针,要使时钟“动”起来,需要每隔1秒重新绘制一次,但真正“动”的只有指针,所以使用两个canvas对象,一个用来绘制缓存表盘,且只需要绘制1次,另一个则用来绘制缓存好的表盘加指针。
2、坐标问题: canvas默认坐标轴原点在画布左上角,x轴水平向右,y轴竖直向下。为方便计算,可将坐标轴原点平移到画布中心。不管是时钟表盘的刻度或者是指针,都与角度有关,所以采用极坐标的方式更方便处理问题,这里的极坐标的极点在画布中心,极轴竖直向上(12点方向),角度正方向取顺时针。接下来,只需要提供一个极坐标上的点坐标转canvas平移后的坐标轴上的点的函数即可。

代码实现

  • 实现坐标轴的转换方法.
	/**
	 * 极坐标转平移后画布坐标
	 * 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 };
	}
  • 全部代码如下:

定义类Clock,构造方法参数有两个,第一个参数接收HTMLCanvasElement对象或id,用来作为显示模拟时钟的容器,第二个参数接收模拟时钟的一些属性,方便第三方使用者调用。并提供run、stop、show、setOptions方法,其中run方法让模拟时钟“动起来”,stop方法停止一个正在运行的模拟时钟,show方法显示一个时间(不会动),setOptions方法则是更新模拟时钟的一些属性。

class Clock{
	constructor(canvas, options = {}) {
	    if (!canvas) {//参数为空验证
            throw new Error("请传入canvas参数!");
        }
        let container = canvas;
        if ("string" == typeof canvas) {
            //如果是字符串,那么通过getElementById获取dom对象
            container = document.getElementById(canvas);
        }
        if (!(container instanceof HTMLCanvasElement)) {//验证是否是HTMLCanvasElement对象
            throw new Error("传入的canvas参数不是一个HTMLCanvasElement对象!");
        }
        /**默认选项 */
        this.options = {
            	size: 300,//模拟时钟尺寸(px)
			    padding: 5,//内边距
			    borderWidth: 15,//边框宽度
			    borderColor: "black",//边框颜色
			    borderImage: undefined,//边框图,优先级高于borderColor
			    scaleType: "arabic",//刻度值类型(arabic、roman、none),arabic:阿拉伯数字;roman:罗马数字; none:不显示;
			    scaleColor: "#666",//刻度线颜色
			    hourColor: "#666",//刻度值颜色
			    backgroundColor: "white",//背景色
			    backgroundImage: undefined,//背景图,优先级高于backgroundColor
			    secondHandColor: "red",//秒针颜色
			    minuteHandColor: "#666",//分针颜色
			    hourHandColor: "black",//时针颜色
			    backgroundMode: "full",//背景图显示模式
			    backgroundAlpha: 0.5,//背景色透明度
			    showShadow: true,//是否显示阴影
			    onload: undefined,//图片加载完成回调,回调参数当前Clock对象
        };
        //用来缓存表盘的canvas对象
        this.dialCanvas = document.createElement("canvas");
        //这里获取下dialCanvas的上下文,方便在其他方法里使用
        this.dialCtx = this.dialCanvas.getContext("2d");
        this.container = container;
        //同上,获取容器的context,方便在其他方法中用到
        this.ctx = container.getContext("2d");
        //设置模拟时钟属性
        this.setOptions(options);
    }
    
    //提供此方法,方便使用者更新模拟时钟属性
	setOptions(options = {}) {
        let opts = {};
        Object.keys(options).forEach(key => {
            const val = options[key];
            if (val !== undefined) { //过滤掉值为undefined的key
                opts[key] = val;
            }
        });
        //合并覆盖默认属性
        this.options = Object.assign({}, this.options, opts);
        //初始化操作
        this.init();
    }

	/**
     * 极坐标转平移后画布坐标
     * 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 };
    }
	
	//加载一张图片,并得到Image对象
	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;
        });
    }
    
    async init() {
        const { size, borderWidth, borderImage, padding, scaleType = "arabic", backgroundImage, onload } = this.options;
        this.halfSize = size * 0.5;//画布尺寸的一半,多处地方有用到,故提出来
        //设置两个画布的宽高均为size
        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) {//用户有设置边框背景图
            //使用es6语法糖async await比较方便,不然要写函数回调。
            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);//若用户有定义onload回调函数,那么就回调一下
        }
    }
    
	//绘制表盘
    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();
        }
    }

	//绘制刻度值
    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();
    }	

	//绘制时针、分针、秒针
    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
        */
        const radHour = time.getHours() * Math.PI / 6 + time.getMinutes() * Math.PI / 360;
        //绘制时针
        this.drawNeedle(ctx, radHour , 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) {
        const start = this.polarCoordinates2canvasCoordinates(-this.headLen, radian);
        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();
    }
	
	//显示一个时间
	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)) {//正则表达式匹配格式hh:mm:ss
                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;
        }
    }
	
}
  • 使用
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>例子</title>
    <!--[if lt IE 10]>
    <script src="https://as.alipayobjects.com/g/component/??console-polyfill/0.2.2/index.js,es5-shim/4.5.7/es5-shim.min.js,es5-shim/4.5.7/es5-sham.min.js,es6-shim/0.35.1/es6-sham.min.js,es6-shim/0.35.1/es6-shim.min.js,html5shiv/3.7.2/html5shiv.min.js,media-match/2.0.2/media.match.min.js"></script>
	<![endif]-->
    <script src="https://as.alipayobjects.com/g/component/??es6-shim/0.35.1/es6-sham.min.js,es6-shim/0.35.1/es6-shim.min.js"></script>
    <script type="text/javascript" src="clock.js"></script>
    <style>
        html,body{
            padding: 0;
            margin: 0;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
        }
    </style>
</head>
<body>
    <canvas id="demo1"></canvas>
    <canvas id="demo2"></canvas>
    <canvas id="demo3"></canvas>
    <script type="text/javascript">
        //参数1可传入dom对象,options可不传
        new Clock(document.getElementById("demo1")).run();

        //参数1可传入dom id值,参数2可传入自己想要的样式
        new Clock("demo2", {
            scaleType: "roman",//显示罗马数字
            borderColor: "brown",//边框颜色
            backgroundColor: "black",//表盘背景色
            hourHandColor: "white",//时针颜色
            minuteHandColor: "white",//分针颜色
            hourColor: "white",//小时数字颜色
            scaleColor: "yellow"//刻度线颜色
        }).run();
		
		//使用边框图
        new Clock("demo3", {
            borderImage: "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1545553805386&di=ec656215a2958d617ef30631e96304e0&imgtype=0&src=http%3A%2F%2Fimg1.ali213.net%2Fshouyou%2Fupload%2Fimage%2F2018%2F07%2F09%2F584_2018070952816881.png",
            backgroundImage: "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1545553773235&di=1c768f80fc088c2edc20fa75af77c515&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201607%2F03%2F20160703164252_2WySB.jpeg"
        }).run();
    </script>
</body>

</html>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值