实现移动端刮刮卡

实现移动端刮刮卡

原理说明:在现有想要展示的奖券上,绝对定位覆盖一层canvas,canvas显示内容为设计给的蒙层图片,当用户手机滑动屏幕,监听touchmove事件,获取用户手指相对于canvas左上角的坐标,不停地绘制圆,并且将globalCompositeOperation属性设置为destination-out(表示新绘制的图像和canvas原图像相交的地方变为透明)。如果想实时获取已绘制区域面积占总canvas面积大小,需要获取canvas全部的像素点,然后比对透明度是否为0,算出透明度为零的像素点和总像素点的比例。

1.实现基本的html结构

<!DOCTYPE html>
<html lang="en">
<head>
    
    
    
    <title>Document</title>
    <style>
        html,body {
            margin: 0;
            padding: 0;
        }
        .card {
            width: 200px;
            height: 80px;
            position: relative;
        }
        .coupon {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            z-index: 1;
            background: red;
        }
        #canvas {
            width: 100%;
            height: 100%;
            position: absolute;
            left: 0;
            top: 0;
            z-index: 2;
            display: block;
        }
    </style>
</head>
<body>
    <div class="card">
        <div class="coupon"></div>
        <canvas id="canvas"></canvas>
    </div>
</body>
</html>

2.绘制canvas封面

const img = new Image();
img.src = './lake.jpg';
img.onload = function () {
    config.context.drawImage(img, 0, 0, img.width, img.height, 0, 0, config.width, config.height);
}

context.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);

参数 描述
img 要使用的图片源
sx 开始剪切图片的x坐标位置
sy 开始剪切图片的y坐标位置
swidth 被剪切的图片的宽度
sheight 被剪切的图片的高度
x 在画布上放置图片x坐标位置
y 在画布上放置图片y坐标的位置
width 要使用canvas画布的宽度
height 要使用canvas画布的高度

3.画小圆

config.context.beginPath();
config.context.fillStyle = '#FFFFFF';
config.context.globalCompositeOperation = "destination-out";
config.context.arc(x, y, config.radius, 0, 2 * Math.PI);
config.context.fill();

4.计算已经画的面积

const imageData = config.context.getImageData(0, 0, config.width, config.height).data;
const len = imageData.length;
let transparentPix = 0;
for (let i = 3; i < len; i += 4) {
    if (imageData[i] === 0) {
        transparentPix++;
    }
}
return (4 * transparentPix)/len;

context.getImageData(x,y,width,height);

参数 描述
x 开始获取像素点左上角位置的x坐标
y 开始获取像素点左上角位置的y坐标
width 开始获取像素点矩形区域的宽度
height 开始获取像素点举行区域的高度

getImageData会返回ImageData对象,该对象包含画布指定矩形像素数据和指定举行的宽高,对于ImageData.data中的数据,每四个一组分别表示为RGBA,比如:
第一个像素点的RGBA分别为:

R: ImageData.data[0];

G: ImageData.data[1];

B: ImageData.data[2];

A: ImageData.data[3];

第二个像素点的RGBA分别为:

R: ImageData.data[4];

G: ImageData.data[5];

B: ImageData.data[6];

A: ImageData.data[7];

以此类推

当在canvas上画圆,跟canvas画布重叠的部分由于我们设置了globalCompositeOperation=destination-out,所以重叠部分会变透明,也就是RGBA中的A会变成零,我们只需要找到画布中A=0的像素点,占总像素点的比例,也就找到了已经刮开的面积了

5.性能优化

一个画布的像素点可能会特别多,会有十几万个点,所以比较挨个遍历是比较耗费性能的,我们可以优化一下,目前我个人有两个优化方案:

5.1 节流执行

touchmove事件可能会执行的特别多,我们可以将执行间隔通过节流,控制在500ms左右,

5.2 缩小计算像素点面积

大部分用户刮卡的时候,可能会从中间开始刮,然后范围逐渐扩大,我们可以通过这个特性来进一步优化,把计算像素点点是否透明的范围缩小至用户touchmove的点的最左边最右最上最下来计算。
6.最终代码

<!DOCTYPE html>
<html lang="en">
<head>
    
    
    
    <title>Document</title>
    <style>
        html,body {
            margin: 0;
            padding: 0;
        }
        .con {
            width: 200px;
            height: 80px;
            position: relative;
        }
        .ch {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            z-index: 1;
            background: red;
        }
        #canvas {
            width: 100%;
            height: 100%;
            position: absolute;
            left: 0;
            top: 0;
            z-index: 2;
            display: block;
        }
    </style>
</head>
<body>
    <div class="con">
        <div class="ch"></div>
        <canvas id="canvas"></canvas>
    </div>
    <script>
        const config = {
            context: null,
            canvas: null,
            width: 200,
            height: 80,
            left: 0,
            top: 0,
            radius: 10,
            delay: 500,
            percent: 0.5
        };

        function setInitialInfo() {
            config.canvas = document.getElementById('canvas');
            config.context = config.canvas.getContext('2d');
            const canvasPosition = getCanvasPosition();
            config.left = canvasPosition.x;
            config.top = canvasPosition.y;
        }

        function setCanvasInfo() {
            config.canvas.width = config.width;
            config.canvas.height = config.height;
        }

        function buildCoverMask() {
            const img = new Image();
            img.src = './lake.jpg';
            img.onload = function () {
                config.context.drawImage(img, 0, 0, img.width, img.height, 0, 0, config.width, config.height);
            }
        }

        function drawCircle(x, y) {
            config.context.beginPath();
            config.context.fillStyle = '#FFFFFF';
            config.context.globalCompositeOperation = "destination-out";
            config.context.arc(x, y, config.radius, 0, 2 * Math.PI);
            config.context.fill();
        }

        function bindEvent() {
            config.canvas.addEventListener('touchmove', handleTouchMove);
        }

        function handleTouchMove(e) {
            const x = e.touches[0].pageX;
            const y = e.touches[0].pageY;
            
            drawCircle(x - config.left, y - config.top);
            throttle(handleComputedPercentage, config.delay);
        }

        function handleComputedPercentage() {
            const percent = computedPercentage();
            if (percent >= config.percent) {
                config.canvas.style.display = 'none';
                console.log('开奖了');
            }
        }

        function computedPercentage() {
            const imageData = config.context.getImageData(0, 0, config.width, config.height).data;
            const len = imageData.length;
            let transparentPix = 0;
            for (let i = 3; i < len; i += 4) {
                if (imageData[i] === 0) {
                    transparentPix++;
                }
            }
            return (4 * transparentPix)/len;
        }

        function throttle(func, during, context) {
            if (!func.timer) {
                func.timer = setTimeout(function() {
                    func();
                    func.timer = undefined;
                }, during);
            }
        }

        function getCanvasPosition() {
            const info = config.canvas.getBoundingClientRect();
            return {
                x: info.x,
                y: info.y
            };
        }

        function init() {
            setInitialInfo();
            setCanvasInfo();
            buildCoverMask();
            bindEvent();
        }

        init();
        
    </script>
</body>
</html>
posted @ 2019-03-12 11:13  cococe  阅读(598)  评论(0)    收藏  举报