HTML5 刮刮卡效果source-atop实现

2年前我写了《HTML5 Canvas 可刮涂层效果》, 现在才知道, 我不经意间写的代码竟然有不少人在借鉴和使用, 这还是我今天搜索怎么实现刮刮卡效果的时候发现的……由于是我自己的代码, 一些分析我代码的帖子虽然把代码改了一部分, 但我当时的编码习惯还是很容易辨认出来.

今天写这篇文章的目的, 是为了终结我2年前写的destination-out刮刮卡实现方法, 因为有不少人反映三星手机不能使用, 可我又没有三星手机, 怎么调试? 后来一位游客给了我一个他说可以用的代码, 我看了看, 是使用source-atop合成多个canvas实现的.

今天我用自己的代码把source-atop的实现方法写一下. 也避免各位新读者在文章里留言问我要源码, 如果在部分三星手机中还是不能实现, 那我也没辙了.

当然, 我并不认为source-atop的方法比destination-out要高明, 因为source-atop需要使用3个canvas才能实现的功能, destination-out只用一个canvas就能实现了, 放弃destination-out仅仅是因为兼容问题.

在线DEMO

这次的源码在刮刮卡的基本逻辑之外额外实现了:

  • 显示完整刮刮卡背景的动画效果(通过requestAnimateFrame实现).
  • 刮卡范围的计算(通过getImageData和事件委托实现).

关于怎么使用, 相信阅读完源码你就会明白, 如果还是有哪里不清楚, 请在文章下方的评论框中留言吧.

源码(在一加手机上表现很好):

<!DOCTYPE html>
<html>
<head>
    <title>Scratcher</title>
    <script>
        var Scratcher = (function(){
            'use strict'

            function index(arr, i){
                return i < 0 ? arr[arr.length + i] : arr[i]
            }

            function range(arr, start, end){
                return arr.slice(start, end + 1)
            }

            function bind(){
                var args = [].slice.call(arguments, 0),
                    element = index(args, 0),
                    events = range(args, 1, -2),
                    handle = index(args, -1)
                for(var i = events.length; i--;){
                    element.addEventListener(events[i], handle)
                }
                return {
                    destory: function(){
                        for(var i = events.length; i--;){
                            element.removeEventListener(events[i], handle)
                        }   
                    }
                }
            }

            var requestAnimationFrame = window.requestAnimationFrame 
            || window.mozRequestAnimationFrame
            || window.webkitRequestAnimationFrame
            || window.msRequestAnimationFrame
            || window.oRequestAnimationFrame
            || function(callback) {
                setTimeout(callback, 1000 / 60)
            }

            return function(obj, back, front, height, width){
                function render(){
                    tempCtx.drawImage(drawCanvas, 0, 0)
                    tempCtx.save()
                    tempCtx.globalCompositeOperation = 'source-atop'
                    tempCtx.drawImage(self.image.back, 0, 0)
                    tempCtx.restore()
                    ctx.drawImage(self.image.front, 0, 0)
                    ctx.drawImage(tempCanvas, 0, 0)
                }

                function moveHandle(e){
                    e.preventDefault()
                    if(mousedown){
                        if(e.changedTouches){
                            e = e.changedTouches[e.changedTouches.length - 1]
                        }
                        var x = (e.clientX + document.body.scrollLeft || e.pageX) - canvas.offsetLeft || 0,
                            y = (e.clientY + document.body.scrollTop || e.pageY) - canvas.offsetTop || 0
                        if(lastPoint){
                            drawCtx.beginPath()
                            drawCtx.moveTo(lastPoint.x, lastPoint.y)
                        }
                        drawCtx.lineTo(x, y)
                        drawCtx.stroke()
                        lastPoint = {
                            'x': x,
                            'y': y
                        }
                        render()
                    }
                }

                function downHandle(e){
                    e.preventDefault()
                    mousedown = true
                    if(e.changedTouches){
                        e = e.changedTouches[e.changedTouches.length - 1]
                    }
                    var x = (e.clientX + document.body.scrollLeft || e.pageX) - canvas.offsetLeft || 0,
                        y = (e.clientY + document.body.scrollTop || e.pageY) - canvas.offsetTop || 0
                    lastPoint = {
                        'x': x,
                        'y': y
                    }
                    render()
                }

                function upHandle(e){
                    e.preventDefault()
                    mousedown = false
                    lastPoint = null

                    var result = (function(){
                        var data = drawCtx.getImageData(0, 0, drawCanvas.width, drawCanvas.height).data
                        for(var i = 0, j = 0; i < data.length; i += 4){
                            if(data[i+3]){
                                j++
                            }
                        }
                        return j / (drawCanvas.width * drawCanvas.height)
                    })()
                    for(var i = eventList.length; i--;){
                        switch(eventList[i].type){
                            case 'change':
                                eventList[i].cb(result)
                                break
                            default:
                        }
                    }
                    render()
                }

                var canvas = (function(obj){
                        if(obj instanceof HTMLCanvasElement){
                            return obj
                        }else if(typeof obj === 'string' && document.querySelector(obj)){
                            return document.querySelector(obj)
                        }else{
                            return document.createElement('canvas')
                        }
                    })(obj),
                    ctx = canvas.getContext('2d'),
                    eventList = [],
                    listener = [
                        bind(document, 'touchmove', 'mousemove', moveHandle),
                        bind(canvas, 'touchstart', 'mousedown', downHandle),
                        bind(document, 'touchend', 'mouseup', upHandle)
                    ],
                    drawCanvas = document.querySelector('#draw'),
                    drawCtx = drawCanvas.getContext('2d'),
                    tempCanvas = document.querySelector('#temp'),
                    tempCtx = tempCanvas.getContext('2d'),
                    mousedown = false,
                    lastPoint = null,
                    self = this

                drawCanvas.height = tempCanvas.height = canvas.height = height
                drawCanvas.width = tempCanvas.width = canvas.width = width

                drawCtx.strokeStyle = 'black'
                drawCtx.lineWidth = 30
                drawCtx.lineCap = ctx.lineJoin = 'round'

                this.on = function(eventName, cb){
                    switch(eventName){
                        case 'change':
                            eventList.push({'type': 'change', 'cb': cb})
                            break
                        default:
                            throw new Error('Event' + eventName + ' does not exist.')
                    }
                }

                this.end = function(ms){
                    function animate(){
                        var pastTime = new Date().getTime() - startTime
                        render()
                        ctx.save()
                        ctx.globalAlpha = pastTime / ms
                        ctx.drawImage(self.image.back, 0, 0)
                        ctx.restore()
                        if(pastTime < ms){
                            requestAnimationFrame(animate)
                        }
                    }

                    for(var i = listener.length; i--;){
                        listener[i].destory()
                    }
                    if(ms){
                        var startTime = new Date().getTime()
                        ctx.globalAlpha = 0
                        requestAnimationFrame(animate)
                    }else{
                        ctx.drawImage(self.image.back, 0, 0)
                    }
                }

                this.canvas = canvas

                this.image = {
                    'back': back,
                    'front': front
                }

                render()
            }
        })()

        function main(){
            'use strict'

            function init(){
                if(++loaded === 2){
                    var s = new Scratcher('#scratcher', back, front, 500, 800)
                    s.on('change', function(e){
                        if(e > 0.4){
                            s.end(2000)
                        }
                    })
                }
            }

            document.body.style.webkitUserSelect = document.body.style.mozUserSelect = 'none'
            var front = new Image(),
                back = new Image(),
                loaded = 0
            front.addEventListener('load', init)
            back.addEventListener('load', init)

            front.src = 'front.png'
            back.src = 'back.png'
        }

        document.addEventListener('DOMContentLoaded', main)
    </script>
</head>
<body>
    <canvas id='scratcher'></canvas>
    <br/>
    <canvas id='temp'></canvas>
    <br/>
    <canvas id='draw'></canvas>
</body>
</html>