Javascript - 使用原生动画与canvas制作抽奖圆盘

目的

HTML5中的canvas提供了使用JS脚本在网页上绘制图形的能力,传统的HTML对象往往只能作为矩形或者圆形容器存在,而直接使用图片又比较复杂,且需要适配不同分辨率。因此,本着试一试也不亏想法,作者使用canvas创建了一个抽奖圆盘,配合动画实现抽奖地效果,整个转盘封装成了一个类,并且提供了抽奖方法。效果图如下,感兴趣地可以看一看:

在这里插入图片描述

过程

由于对API不是非常熟悉,因此实际的过程比较凌乱,大致来说分为动画实现转盘绘制文字绘制三步。参考的网站主要就是MDN:

动画实现

HTML提供了基础的CSS动画keyframes,通过关键帧确定某几个时刻的属性值,然后根据timing-function自动绘制属性的过渡变化过程。例如如下代码表示一个从背景色紫色,旋转0度变化到背景色蓝色,旋转720度的一个动画,并将其应用到#canvas元素上,设置为2s:

@keyframes rot {
    0% {
        transform: rotate(0deg);
        background-color: rebeccapurple;
    }
    100% {
        transform: rotate(720deg);
        background-color: dodgerblue;
    }
}
#canvas {
    animation: rot 2s ease, forwards;
}

不过CSS动画是已经写好的,因此需要使用JS编写动画:

ani = canvas.animate([{ transform: 'rotate(720deg)' }], {
        duration: 2 * 1000,
        fill: "forwards",
        easing: "ease"
    });

如果需要多次开启动画,可以使用这里获取的ani.cancel()方法取消动画,然后再调用animate就可以了。

转盘实现

转盘包括两部分,地盘部分和上面指针部分。目的是让地盘旋转,因此上面的指针需要在设置一个canvas元素。这里使用position: absolute就能达到堆叠的效果了。

  • 指针绘制 - 先画个三角形再画个圆形,非常easy

    drawPointer() {
        let context = this.context, radius = this.radius;
        // 指针下部 - 圆形
        context.fillStyle = "#ff2233";
        context.beginPath();
        // 圆弧,这里直接就是个圆了
        context.arc(radius, radius, radius / 10, 0, Math.PI * 2);
        context.closePath();
        context.fill();
        // 指针头 - 三角形
        context.beginPath();
        // 右下角
        context.moveTo(radius + radius / 15, radius);
        // 顶角
        context.lineTo(radius, radius / 6);
        // 左下角
        context.lineTo(radius - radius / 15, radius);
        context.closePath();
        context.fill();
    }
    
  • 圆盘绘制

    圆盘的绘制需要等分圆盘,然后每一部分填充个扇形。这里使用ctx.arc()配合ctx.closePath()即可快速绘制。绘制的时候参数是弧度,需要做一下转换。

    // 绘制一个扇形并使用颜色填充
    fillSector(fromDeg, toDeg, color) {
        const context = this.context;
        const radius = this.radius;
        context.beginPath();
        context.fillStyle = color;
        context.moveTo(radius, radius);
        context.arc(radius, radius, radius, fromDeg * Math.PI / 180, toDeg * Math.PI / 180);
        context.closePath();
        context.fill();
    }
    // 将圆等分并给每一块填上不同颜色
    divideCircle(n, colors) {
        const degWidth = 360 / n;
        for (let i = 0; i < n; i++) {
            const fromDeg = degWidth * i;
            this.fillSector(degWidth * i, degWidth * (i + 1), colors[i]);
        }
    }
    

转盘文字绘制

文字绘制本身很简单,但由于需要在转盘的不同位置绘制,所以会牵涉到canvas坐标的旋转变换。

所谓的坐标变化,就是对canvas的整个坐标系进行平移、旋转等操作。变换之后,画布上的绘制操作如绘制线条、矩形、绘制文字、圆弧等都会以变换后的坐标系为基准。以MDN的例子来说,灰色矩形为坐标轴变化前绘制的,红色矩形为以左上角为圆心,顺时针旋转45°后绘制的,如下。

在这里插入图片描述

// 为旋转,正常坐标系的灰色矩形
ctx.fillStyle = 'gray';
ctx.fillRect(100, 0, 80, 20);

// 旋转坐标系后的红色矩形
ctx.rotate(45 * Math.PI / 180); // 旋转坐标系
ctx.fillStyle = 'red';
ctx.fillRect(100, 0, 80, 20)

无论做了什么变换,都可以通过ctx.setTransform(1, 0, 0, 1, 0, 0)重置坐标系。

完整代码

实际上转盘旋转的过程也需要思考一下,但是因为是基本的数学知识,所以也不再赘述了,完整代码如下(只有一个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>
        body,
        html {
            width: 100%;
            height: 100%;
            background-color: white;
            margin: 0px;
        }

        @keyframes rot {
            0% {
                transform: rotate(0deg);
                background-color: rebeccapurple;
            }

            100% {
                transform: rotate(720deg);
                background-color: dodgerblue;
            }
        }

        #plate {
            width: 30vw;
            height: 30vw;
            margin: auto;
            border: solid rgb(247, 67, 67) 12px;
            border-radius: 50%;
            background-color: #fff;
        }

        #canvas,
        #canvas2 {
            position: absolute;
            margin: auto;
            /* animation: rot 2s ease, forwards; */
            border-radius: 50%;
        }

        #btn,
        input {
            width: 60%;
            margin: 5px 100px;
        }
    </style>
    <script>

        class DrawingContext {
            defaultSec = 4;
            defaultDegBonus = 360 * 3;
            defaultDeg = 90;
            ani = null;
            timeoutTask = null;

            shuffle(a) {
                for (let i = a.length - 1; i > 0; i--) {
                    const j = Math.floor(Math.random() * (i + 1));
                    var x = a[i];
                    a[i] = a[j];
                    a[j] = x;
                }
                return a;
            }

            constructor(plate, canvas) {
                this.plate = plate;
                this.canvas = canvas;
                this.width = plate.clientWidth;
                this.height = plate.clientHeight;
                this.radius = this.width / 2;
                canvas.setAttribute("width", this.width);
                canvas.setAttribute("height", this.width);
                this.context = this.canvas.getContext("2d");
                this.canvas.onanimationend = (ev) => this.canvas.style.animation = "";
            }
            // 重设动画
            resetAni() {
                if (this.timeoutTask) {
                    clearTimeout(this.timeoutTask);
                }
                if (this.ani) {
                    this.ani.cancel();
                }
                this.timeoutTask = null;
                this.ani = null;
            }
            // 重设动画,重绘圆盘
            redraw(n, colors, texts) {
                this.resetAni();
                this.drawLottery(n, colors, texts);
            }
            // 重设动画,重绘圆盘,开启抽奖,返回中奖目标
            letsGoLottery(n, colors, texts) {
                this.redraw(n, this.shuffle(colors), texts);
                const who = Math.floor(Math.random() * n);
                this.timeoutTask = setTimeout(() => {
                    const degWidth = 360 / n;
                    var degToGo = (2 * n - who - Math.ceil(n / 4)) % n * degWidth - (n % 4 == 0 ? degWidth / 2 : 0);
                    console.log(degToGo);
                    this.setAnimation(degToGo);
                }, 800);
                return who;
            }
            // 启动一个旋转的动画
            setAnimation(deg, sec) {
                if (!deg) deg = this.defaultDeg;
                if (!sec) sec = this.defaultSec;
                deg += this.defaultDegBonus;
                this.ani = this.canvas.animate(
                    [{ transform: 'rotate(' + deg + 'deg)' }],
                    {
                        duration: sec * 1000,
                        fill: "forwards",
                        easing: "ease"
                    });
            }
            // 绘制一个扇形并使用颜色填充
            fillSector(fromDeg, toDeg, color) {
                const context = this.context;
                const radius = this.radius;
                context.beginPath();
                context.fillStyle = color;
                context.moveTo(radius, radius);
                context.arc(radius, radius, radius, fromDeg * Math.PI / 180, toDeg * Math.PI / 180);
                context.closePath();
                context.fill();
            }
            // 将圆等分并给每一块填上不同颜色
            divideCircle(n, colors) {
                const degWidth = 360 / n;
                for (let i = 0; i < n; i++) {
                    const fromDeg = degWidth * i;
                    this.fillSector(degWidth * i, degWidth * (i + 1), colors[i]);
                }
            }
            // 画n个均匀分布在园上的文字
            drawText(n, texts) {
                var context = this.context;
                context.font = "20px '等线'";
                context.fillStyle = "#000";
                context.textAlign = "center";
                context.translate(this.radius, this.radius);
                const deg = 360 / n * Math.PI / 180;
                context.rotate(Math.PI / 2 - deg / 2);
                for (let i = 1; i <= n; i++) {
                    context.rotate(deg);
                    context.fillText(texts[i - 1], 0, -this.radius * 4 / 5, deg * this.radius * 4 / 5);
                }
                context.setTransform(1, 0, 0, 1, 0, 0);
            }
            // 抽奖转盘 - 园等分成n个扇形,每个扇形有文字
            drawLottery(n, colors, texts) {
                this.divideCircle(n, colors);
                this.drawText(n, texts);
            }
            // 绘制圆盘指针
            drawPointer() {
                let context = this.context, radius = this.radius;
                // 指针下部
                context.fillStyle = "#ff2233";
                context.beginPath();
                context.arc(radius, radius, radius / 10, 0, Math.PI * 2);
                context.closePath();
                context.fill();
                // 指针头
                context.beginPath();
                context.moveTo(radius + radius / 15, radius);
                context.lineTo(radius, radius / 6);
                context.lineTo(radius - radius / 15, radius);
                context.closePath();
                context.fill();
            }
        };

        function randColor() {
            const cl = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
            var res = "#";
            for (let i = 0; i < 6; i++) res += cl[Math.floor(Math.random() * (cl.length))];
            return res;
        };

        window.onload = function () {
            var colors = ["pink", "purple", "cyan", "green", "gold", "brown", "crimson", "dodgerBlue", "tomato", "MediumAquaMarine"];
            var n = 10, texts = colors;

            var drawContext = new DrawingContext(document.getElementById("plate"), document.getElementById("canvas"));
            var drawContext2 = new DrawingContext(document.getElementById("plate"), document.getElementById("canvas2"));

            drawContext.divideCircle(n, colors);
            drawContext2.drawPointer();

            document.getElementById("btn").onclick = function (event) {
                const who = drawContext.letsGoLottery(n, colors, texts);
                alert(texts[who]);
            }
        }
    </script>
</head>
<body>
    <div id="plate">
        <canvas id="canvas"></canvas>
        <canvas id="canvas2"></canvas>
    </div>
    <div>
        <button id="btn">点啊</button>
        <!-- <input type="number" name="helo" placeholder="num" id="inputN"> -->
        <!-- <input type="number" name="halo" placeholder="degree" id="inputDeg">
        <input type="number" name="hhlo" placeholder="time" id="inputTime"> -->
    </div>
</body>

</html>

总结

边查边写,边写便理解。如有纰漏,还望指正。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页