利用canvas实现转盘抽奖

更新日期: 2019-07-24阅读: 2.8k标签: canvas

最近工作中重构了抽奖转盘,给大家提供一个开发转盘抽奖的思路


需求

1、转盘根据奖品数量不同而有变化 

2、canvas 

 

目录结构


由于业务需要所以开发了两个版本抽奖,dom和canvas,不过editor.js部分只能替换图片,没有功能逻辑。

需要注意的是此目录隐藏了一个动态数据类(dataStore),因为集成在项目里了,所以没有体现。


Spirts

精灵类生成实例,会包括基础属性:width、height、x、y和方法:setOpacity、drawCircular、setRotate、draw

下面是几个重要的精灵构造器:背景、转盘背景和每一个奖品

/*
 * 精灵核心 基类
 * */
class Spirt {
    constructor({}) {}
    // 精灵透明度调节
    setOpacity(opy, callback) {}
    // 画圆形图片
    drawCircular(fn) {}
    // 精灵旋转调节
    setRotate() {}
    // 画精灵
    draw() {}
}
// 背景
class Bg extends Spirt {
    constructor({ ...args }) {
        super({ ...args });
        if (args.height == '100%') {
            this.height = this.canvas.height;
        }
    }
}
// 转盘背景
class Turn extends Spirt {
    constructor({ ...args }) {
        super({ ...args });
    }
    draw() {
        this.drawCircular(() => {
            super.draw();
        });
    }
}
// 每一个奖品
class Item extends Spirt {
    constructor({ ...args }, rid) {
        super({ ...args });
        this.rid = rid;
    }
    draw(angle, x, y) {
        this.setRotate(angle, () => super.draw(), x, y);
    }
}


Config

基础数据类,包括基础数据:转盘分块、角度、半径、每一块对应奖品、旋转总时长、旋转速度等

主要说一下转盘分块:如果符合规律,就用函数代替,如果不符合规律就用映射

let layout = {
    1: [1, 10, 1, 10, 1, 10],
    2: [1, 2, 10, 1, 2, 10],
    3: [1, 10, 2, 10, 3, 10],
    4: [2, 10, 3, 10, 4, 10, 1, 10],
    5: [2, 3, 4, 10, 5, 10, 1, 10],
    6: [2, 3, 4, 10, 5, 6, 1, 10],
    7: [3, 4, 10, 5, 6, 7, 10, 1, 2, 10],
    8: [3, 4, 10, 5, 6, 7, 8, 10, 1, 2]
};

下面为部分代码

class Config {
    constructor(prize = new Array(3), resImg) {
        this.awards_len = prize.length >= 7 ? 10 : prize.length >= 4 ? 8 : 6;
        this.awards_angle = 360 / this.awards_len;

        this.awards_r = 320;
        this.awards_cir = 2 * Math.PI * this.awards_r;

        let nums = {
            6: 2.5,
            8: 2,
            10: 2
        };

        this.awards_item_margin = 40;
        this.award_item_size =
            this.awards_cir / this.awards_len / nums[this.awards_len];

        this.duration = 2000;

        // 奖品详情
        this.awards = getAwards(resImg, prize.length);
    }
}
/**
 * 获取奖品列表
 * @param {*} num
 */
function getAwards(resImg, num) {
    let arr = layout[num];
    return arr.map(rid => {
        let res = resImg[mapAwards[rid]];
        return { rid, res, className: mapAwards[rid] };
    });
}


Res

资源类主要做一些图片初始化的操作

// 获取游戏资源
class Res extends Resource {
    constructor(dataStore) {
        super({ dataStore });
        let { gameJson } = dataStore;

        this.res = {
            ...gameJson.staticSpirts.BG
        };

        this.dataStore = dataStore;
    }
    // 编辑页面改变页面图片能力。
    setImg(data) {
        this.res[data.num].imgUrl = data.imgUrl;
        if (['BG', 'TITLE', 'TURNTABLE_BG', 'PLAYBTN'].includes(data.num)) {
            $(`.turnTableNew_${data.num}`).css(
                'background-image',
                `url('${HOST.FILE + data.imgUrl}')`
            );
        } else {
            $(`.turnTableNew_${data.num}`).attr(
                'src',
                `${HOST.FILE + data.imgUrl}`
            );
        }
        return {
            staticSpirts: this.res
        };
    }
}


Director

导演类,主要操作的是转盘动画的逻辑
主要逻辑是:
1、addCLick: canvas添加点击事件
2、drawStatic:画静态元素
3、drawZhuanPan:这个为单独canvas,group内部包括画转盘,奖品
4、drawPlayBtn: 画按钮
5、当点击抽奖按钮执行updatedRotate函数让单独转盘canvas旋转即可
6、当旋转角度和获取奖品角度一致时停止

class turnTable extends Director {
    constructor(dataStore) {
        let { gameManager } = dataStore;
        super(gameManager);

        // 从仓库中获取基础数据,canvas和config总配置
        this.dataStore = dataStore;
        this.canvas = dataStore.canvas;
        this.config = dataStore.$gameConfig;
        
        // 当前抽奖的一些基础数据
        this.angle = 0;
        this.isAnimate = true;
        this.lastTime = 0;
        this.num = 0;

        this.addCLick();
    }

    // 抽奖结束,需要初始化抽奖
    initGame() {
        this.state = this.START;
        this.angle = 0;
        this.num = 0;
        this.prizeId = null;
        this.isAnimate = true;
        this.turnAudio.pause();
        this.drawAllElements(this.res, this.set);
    }
    /**
     * 画所有元素
     * @param {*} store
     * @param {*} res
     */
    drawAllElements(res, set) {
        this.res = res;
        this.set = set;
        this.drawStatic(res);
        this.drawZhuanPan(this.angle);
        this.drawPlayBtn(this.canvas, res);
    }
    /**
     * 画静态元素
     */
    drawStatic(res) {
        ['BG', 'TITLE'].forEach(item => {
            let str = item.toLowerCase();
            str = str.replace(str[0], str[0].toUpperCase());
            let ele = new Spirts[str]({
                canvas: this.canvas,
                ...res[item]
            });
            ele.draw();
        });
    }
    // 画转盘
    drawZhuanPan(angle) {
        this.group = new Spirts['Group']({
            canvas: this.canvas,
            ...this.res['TURNTABLE_BG']
        });
        this.items = this.drawDynamic(this.group.group_canvas, this.res);
        this.group.draw(
            angle,
            +this.res['TURNTABLE_BG'].x + +this.res['TURNTABLE_BG'].width / 2,
            +this.res['TURNTABLE_BG'].y + +this.res['TURNTABLE_BG'].height / 2
        );
    }
    // 画动态元素
    drawDynamic(canvas, res) {
        let set = this.set;
        let items = [];
        // 转盘背景1,装饰物
        let turnBg = new Spirts['Turn']({
            canvas,
            img: res['TURNTABLE_BG'].img,
            width: res['TURNTABLE_BG'].width,
            height: res['TURNTABLE_BG'].height,
            x: 0,
            y: 0
        });
        turnBg.draw();
        // 转盘背景2,盘面
        let turnPan = new Spirts['Turn']({
            canvas,
            img: res['TURNTABLE_PAN'].img,
            width: res['TURNTABLE_PAN'].width,
            height: res['TURNTABLE_PAN'].height,
            x: (res['TURNTABLE_BG'].width - res['TURNTABLE_PAN'].width) / 2,
            y: (res['TURNTABLE_BG'].height - res['TURNTABLE_PAN'].height) / 2
        });
        turnPan.draw();

        for (let i = 0; i < set.awards_len; i++) {
            // 每一个奖品
            let item = new Spirts['Item'](
                {
                    canvas,
                    img: set.awards[i].res.img,
                    width: set.award_item_size,
                    height: set.award_item_size,
                    x: turnBg.width / 2 - set.award_item_size / 2,
                    y:
                        (turnBg.height - turnPan.height) / 2 +
                        set.awards_item_margin
                },
                set.awards[i].rid
            );
            item.draw(
                set.awards_angle / 2 + set.awards_angle * i,
                turnBg.width / 2,
                turnBg.height / 2
            );
            // 画线
            let line = new Spirts['Item']({
                canvas,
                img: res['LINE'].img,
                width: res['LINE'].width,
                height: res['LINE'].height,
                x: turnBg.width / 2 - res['LINE'].width / 2,
                y: (turnBg.height - turnPan.height) / 2
            });
            line.draw(
                set.awards_angle * i,
                turnBg.width / 2,
                turnBg.height / 2
            );
            // 放到items数组内,后期转盘停止校验用
            items.push(item);
        }
        return items;
    }
    // 画按钮
    drawPlayBtn(canvas, res) {
        let playBtn = new Spirts['PlayBtn']({
            canvas,
            ...res['PLAYBTN']
        });
        playBtn.draw();
        this.playBtn = playBtn;
    }
    // 点击事件
    addCLick() {
        let initX,
            isClickState,
            cScale = this.config['cScale'] || 1;
        this.canvas.addEventListener(tapstart, event => {
            initX = event.targetTouches
                ? event.targetTouches[0].clientX
                : event.offsetX / cScale;
            let y = event.targetTouches
                ? event.targetTouches[0].clientY
                : event.offsetY / cScale;
            isClickState = isCheck.call(this.playBtn, initX, y);
            // 点击回调
            if (isClickState && this.isAnimate) {
                 /**
                 * 按钮不可点击
                 * 初始化总时长
                 * 初始化速度
                 * 初始化当前时间
                 */
                this.isAnimate = false;
                this.set.is_animate = true;
                this.set.jumping_total_time =
                    Math.random() * 1000 + this.set.duration;
                this.set.speed = (this.set.jumping_total_time / 2000) * 10;
                this.lastTime = new Date().getTime();
                this.run();
                this.getPrize()
                    .then(res => {
                        if (!res) {
                            this.prizeId = 10;
                            return;
                        }
                        this.prizeId = +res.prizeLevel + 1;
                    })
                    .catch(_ => {
                        this.prizeId = 10;
                        this.initGame();
                        this.state = this.END;
                    });
            }
        });
    }
    updatedRotate() {
        let curTime = new Date().getTime(),
            set = this.set,
            speed = 1;
        /**
         * 转盘停止,需要满足一下条件
         * 1.大于总时间
         * 2.有奖品id
         * 3.速度降为1
         * 4.转盘角度对应奖品id位置
         * 角度做了容错处理,当前角度范围中心位置,偏移量为5
         * 公式:通过旋转角度计算当前奖品index
         * 通过items奖品列表计算当前奖品rid
         * rid和prizeId对比,如果结束抽奖
         */
        if (
            curTime - this.lastTime >= set.jumping_total_time &&
            this.prizeId &&
            speed == 1
        ) {
            let resultAngle = 360 - (this.angle % 360);
            let index = (resultAngle / set.awards_angle) >> 0;
            let centerAngle = set.awards_angle * (index + 0.5);
            if (
                this.items[index].rid == this.prizeId &&
                (resultAngle > centerAngle - 5) &
                    (resultAngle < centerAngle + 5)
            ) {
                this.comAudio.play();
                this.state = this.PAUSE;
            }
        }
        this.num++;
        speed = Math.max(
            set.speed -
                (18 * this.num * (set.speed - 1)) / set.jumping_total_time,
            1
        );
        this.angle += speed;
        this.drawAllElements(this.res, this.set);
    }

    // 渲染画布
    render() {
        switch (this.state) {
            case this.START:
                this.updatedRotate();
                break;
            case this.ERROR:
                break;
            case this.PAUSE:
                this.state = this.END;
                setTimeout(() => {
                    this.showResult();
                    this.initGame();
                }, 1000);
                break;
            case this.END:
                // 打开指定页面
                break;
        }
    }
}

链接: https://www.fly63.com/article/detial/4888

HTML5 Canvas绘图基本使用方法, H5使用Canvas绘图

Canvas 是H5的一部分,允许脚本语言动态渲染图像。Canvas 定义一个区域,可以由html属性定义该区域的宽高,javascript代码可以访问该区域,通过一整套完整的绘图功能(API),在网页上渲染动态效果图。

web图片前端裁剪功能实现_利用html5 canvas技术实现图片裁剪

上传截图很多做法是把图像发送到后端,把裁剪后的结果发送给浏览器,这种方式会增加处理时延。用canvas提供的API实现纯前端的剪切:这里头关键有三步:显示未经处理的图片,得到裁剪区域,显示裁剪后的区域。

原生js使用canvas实现图片格式webp/png/jpeg在线转换

javascript完成图片格式转换: 通过input上传图片,使用FileReader将文件读取到内存中。将图片转换为canvas,canvas.toDataURL()方法设置为我们需要的格式,最后将canvas转换为图片。

离屏Canvas — 使用Web Worker提高你的Canvas运行速度

现在因为有了离屏Canvas,你可以不用在你的主线程中绘制图像了!Canvas 是一个非常受欢迎的表现方式,同时也是WebGL的入口。它能绘制图形,图片,展示动画,甚至是处理视频内容

canvas高效绘制10万图形,你必须知道的高效绘制技巧

最近的一个客户项目中,简化的需求是绘制按照行列绘制很多个圆圈。需求看起来不难,上手就可以做,写两个for循环。,IT行业的知识更新越来越快,能够以不变应万变的人,就是拥有良好的学习力、创造力、判断力和思考力的人。这些能力会让你在变换万千的技术海洋中,屹立不倒,不被淹没。

利用canvas将网页元素生成图片并保存在本地

利用canvas将网页元素生成图片并保存在本地,首先引入三个文件,createElementNS() 方法可创建带有指定命名空间的元素节点。 createElementNS(ns,name) > createElementNS() 方法与 createElement() 方法相似

基于 HTML5 Canvas 的智能安防 SCADA 巡逻模块

随着大数据时代的来临,物联网的日益发展,原先的 SCADA 系统本身也在求新求变,从最开始的专业计算机和操作系统,到通用计算机和相关软件,再到现在基于 HTML5 Canvas 的新型组态开发,其应用的范围也从最初的电力

js用canvas实现简单的粒子运动

在写下合格粒子运动时要先清楚你的思路,不能一开始就盲目的开始写,首先先要确定思路然后在去一步步的实现,在写的过程要注意细节,要思考js有些知识是跟数学知识相关的要注意观察

Canvas 点线动画案例

canvas 画的圆不是圆,是椭圆。不要在style里指定 Canvas 的宽度,Canvas 画布的尺寸的大小和显示的大小是有很大的区别的,在 canvas 里面设置的是才是 Canvas 本身的大小。不要企图通过闭合现有路径来开始一条新路径

Canvas在移动端绘制模糊的原因与解决办法

由于一些移动端的兼容性原因,我们某个项目需要前端将pdf转换成在移动端页面可直接观看的界面。为了方便解决,我们采用了pdf.js这个插件,该插件可以将pdf转换成canvas绘制在页面上

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!