• 利用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
            };
        }
    }

    资源搜索网站大全 http://www.szhdn.com 广州VI设计公司https://www.houdianzi.com

    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,
                 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,
                 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,
                         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,
                     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;
            }
        }
    }
  • 相关阅读:
    汽车档位作用
    大家免着惊
    出头天 闽南语歌词
    Configuration所有配置简介
    android图片缓存框架Android-Universal-Image-Loader
    java泛型 之 入门(interface)
    broadcom6838开发环境实现函数栈追踪
    win 开机 Microsoft corparation 滚动栏
    Linux中下载,压缩,解压等命令
    python实现人人网用户数据爬取及简单分析
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/14010616.html
Copyright © 2020-2023  润新知