HTML5实例教程:圣诞彩蛋(五)

添加Alpha值和Lock锁以完成动画效果

要完成淡入淡出的动画,需要使用到Alpha值来修改透明度,使用Lock来”锁”住动画时鼠标移动触发的事件.

在第三节中我们已经实现了512px圆到256px圆的动画,现在我们要将它移植到新的通用函数上.

首先我们要修改circleData的格式:

function circleData(cX, cY, cR, Px, Py, r2, a, lock) {  
    return {
        'circle': {
            'x': cX,
            'y': cY,
            'r': cR
        },
        'point': {
            'x': Px,
            'y': Py
        },
        'r2': r2,
        'alpha': a == undefined ? 100 : a,
        'lock': lock || false
    }
}

考虑到兼容先前写的代码以及alpha的值可能为0的情况,改用三元运算替代||进行赋值时的选择.

下面的代码用来完成动画效果.

(function() {
    var j = i;
    var old = data[j];
    (function() {
        if(old.alpha == 0) {
            for(var i = newC.length; i--;) {
                var c = newC[i];
                c.lock = false;
            }
            delete data[j];
            draw(true);
        } else {
            for(var i = newC.length; i--;) {
                newC[i].alpha += 25;
            }
            old.alpha -= 25;

            draw(true);
            setTimeout(arguments.callee, 50);
        }
    })();
})()

因为使用数组循环方便,所以在代码中使用到了一个名称为newC的数组变量.

我们在使用之前要先给newC赋值,把这段代码写在前面:

var newC = [newC1, newC2, newC3, newC4];  

因为使用到alpha和lock,还必须修改之前newC1,newC2,newC3,newC4的创建代码.

var newC1 = circleData(cX - r2 / 2, cY - r2 / 2, r2 / 2, pX * 2, pY * 2, r2, alpha, isLock),  
        newC2 = circleData(cX + r2 / 2, cY - r2 / 2, r2 / 2, pX * 2 + 1, pY * 2, r2, alpha, isLock),
        newC3 = circleData(cX - r2 / 2, cY + r2 / 2, r2 / 2, pX * 2, pY * 2 + 1, r2, alpha, isLock),
        newC4 = circleData(cX + r2 / 2, cY + r2 / 2, r2 / 2, pX * 2 + 1, pY * 2 + 1, r2, alpha, isLock);

在这之段代码的前面为alpha和isLock赋值.

var alpha, isLock;  
if(r2 <= 64) {  
    alpha = 100;
    isLock = false;
} else {
    alpha = 0;
    isLock = true;
}
c.lock = true;  

由于128px以下的圆都没有动画效果(因为数量太多了,影响效率),所以在为alpha和isLock赋值的时候进行了判断.

在这之前还有一处需要修改,我们需要为continue添加一个新的条件——当前圆被锁,这样就避免了被锁的圆执行后面的代码.

if(!c || c.r2 == 4 || c.lock)  

到了这步,动画效果算是做完了.

不响应逻辑

如果你试过去执行前面代码所构建出的动画,你会发现一个问题.

当你将鼠标移动到一个圆上时,该圆以及后续的圆会一直分离直到分离为4px圆,也就是说我们只需要动几下鼠标就会自动连续分离好多个圆,原版显然不是这样.

为了改变这种现象,我们必须要防止当鼠标停留在一个新圆时就进行分离.

于是就有了新的不响应逻辑,它的原理是这样的:

设置一个变量last,记录下分离时鼠标位置产生的新圆,如果鼠标位置没有新圆产生,则给last赋值为null.在isPointInCircle返回true之后执行的逻辑中,比较last和当前鼠标位置的圆,如果相同则不响应,否则执行响应并为last设置新值.如果鼠标不处在圆上,则也为last设置null.

基于上述原理,将代码改成这样:

var last;  
canvas.addEventListener('mousemove', function(e) {  
    var mouseX = e.pageX - canvas.offsetLeft,
            mouseY = e.pageY - canvas.offsetTop;
    for(var i = data.length; i--;) {
        var c = data[i];
        if(!c || c.r2 == 4 || c.lock) {
            if(c && c.r2 >= 128) {
                return;
            }
            continue;
        }
        if(isPointInCircle(c.circle.x, c.circle.y, c.circle.r, mouseX, mouseY)) {
            if(last == c) {
                return;
            } else {
                last = null;
            }
            var r2 = c.r2 / 2;
            var rData = r[r2];
            var cX = c.circle.x,
                    cY = c.circle.y,
                    pX = c.point.x,
                    pY = c.point.y;
            var alpha, isLock;
            if(r2 <= 64) {
                alpha = 100;
                isLock = false;
            } else {
                alpha = 0;
                isLock = true;
            }
            var newC1 = circleData(cX - r2 / 2, cY - r2 / 2, r2 / 2, pX * 2, pY * 2, r2, alpha, isLock),
                    newC2 = circleData(cX + r2 / 2, cY - r2 / 2, r2 / 2, pX * 2 + 1, pY * 2, r2, alpha, isLock),
                    newC3 = circleData(cX - r2 / 2, cY + r2 / 2, r2 / 2, pX * 2, pY * 2 + 1, r2, alpha, isLock),
                    newC4 = circleData(cX + r2 / 2, cY + r2 / 2, r2 / 2, pX * 2 + 1, pY * 2 + 1, r2, alpha, isLock);
            data.push(newC1, newC2, newC3, newC4);
            c.lock = true;
            var newC = [newC1, newC2, newC3, newC4];
            for(var m = newC.length; m--;) {
                var c = newC[m];
                if(isPointInCircle(c.circle.x, c.circle.y, c.circle.r, mouseX, mouseY)) {
                    last = c;
                }
            }
            if(c.r2 >= 128) {
                (function() {
                    var j = i;
                    var old = data[j];
                    (function() {
                        if(old.alpha == 0) {
                            for(var i = newC.length; i--;) {
                                var c = newC[i];
                                c.lock = false;
                            }
                            delete data[j];
                            draw(true);
                        } else {
                            for(var i = newC.length; i--;) {
                                newC[i].alpha += 25;
                            }
                            old.alpha -= 25;
                            draw(true);
                            setTimeout(arguments.callee, 50);
                        }
                    })();
                })()
            } else {
                delete data[i];
                draw(true);
            }
            return;
        }
    }
    last = null;
});

因为last设置时的位置过于零碎,所以一次性放出了完整的代码.

代码优化

现在所有有关Canvas的行为都制作完了,我们来看看完整的代码吧.

<!DOCTYPE html>  
<html>  
<head>  
    <meta charset='UTF-8'/>
    <title>圣诞彩蛋</title>
<style>  
    body{
        text-align:center;
        background:#E6E7E8;
    }
</style>  
</head>  
<body>  
<canvas width='512' height='512'></canvas>  
<script>  
var canvas = document.querySelector('canvas');  
var ctx = canvas.getContext('2d');  
var img = new Image();  
img.onload = function() {  
    var canvas2 = document.createElement('canvas');
    canvas2.width = canvas2.height = 512;
    var ctx2 = canvas2.getContext('2d');
    ctx2.drawImage(img, 0, 0);
    var data = ctx2.getImageData(0, 0, 512, 512).data;

    function r(r2, data) {
        if(r2 != 512) {
            var rs = r2 * r2;
            var points = 512 * 512 / rs;
            var r = new Uint8ClampedArray(points * 3);
            var size = Math.sqrt(points);
            for(var i = 0; i < points; i++) {
                var red = 0,
                    green = 0,
                    blue = 0;
                var xBegin = i % size * r2,
                    yBegin = Math.floor(i / size) * r2;
                var xEnd = xBegin + r2,
                    yEnd = yBegin + r2;
                for(var x = xBegin; x < xEnd; x++) {
                    for(var y = yBegin; y < yEnd; y++) {
                        var j = (y * 512 + x) * 4;
                        red += data[j];
                        green += data[j + 1];
                        blue += data[j + 2];
                    }
                }
                r[i * 3] = red / rs;
                r[i * 3 + 1] = green / rs;
                r[i * 3 + 2] = blue / rs;
            }
        } else {
            var r = new Uint8ClampedArray(3);
            var red = 0,
                green = 0,
                blue = 0;
            for(var i = data.length; i -= 4;) {
                red += data[i];
                green += data[i + 1];
                blue += data[i + 2];
            }
            r[0] = red / 262144;
            r[1] = green / 262144;
            r[2] = blue / 262144;
        }
        return r;
    }

    function rgb(r, g, b) {
        return 'rgb(' + r + ',' + g + ',' + b + ')';
    }
    var rData = [];
    rData[512] = r(512, data);
    rData[256] = r(256, data);
    rData[128] = r(128, data);
    rData[64] = r(64, data);
    rData[32] = r(32, data);
    rData[16] = r(16, data);
    rData[8] = r(8, data);
    rData[4] = r(4, data);
    rData[2] = r(2, data);

    function drawRound(r, r2) {
        ctx.save();
        var i = 0;
        var end = Math.sqrt((512 * 512) / (r2 * r2));
        for(var y = 0; y < end; y++) {
            for(var x = 0; x < end; x++) {
                ctx.fillStyle = rgb(r[i], r[i + 1], r[i + 2]);
                ctx.beginPath();
                ctx.arc(r2 / 2 + x * r2, r2 / 2 + y * r2, r2 / 2, 0, Math.PI * 2);
                ctx.fill();
                i += 3;
            }
        }
        ctx.restore();
    }

    function drawRoundOne(r, r2, x, y) {
        ctx.save();
        var i = (Math.sqrt((512 * 512) / (r2 * r2)) * y + x) * 3;
        ctx.fillStyle = rgb(r[i], r[i + 1], r[i + 2]);
        ctx.beginPath();
        ctx.arc(r2 / 2 + x * r2, r2 / 2 + y * r2, r2 / 2, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }

    function drawRoundOne2(r, r2, cX, cY, x, y) {
        ctx.save();
        var i = (Math.sqrt((512 * 512) / (r2 * r2)) * y + x) * 3;
        ctx.fillStyle = rgb(r[i], r[i + 1], r[i + 2]);
        ctx.beginPath();
        ctx.arc(cX, cY, r2 / 2, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }

    function isPointInCircle(circleX, circleY, circleRadius, mouseX, mouseY) {
        var distance = Math.pow(mouseX - circleX, 2) + Math.pow(mouseY - circleY, 2);
        return distance <= Math.pow(circleRadius, 2);
    }

    function circleData(cX, cY, cR, Px, Py, r2, a, lock) {
        return {
            'circle': {
                'x': cX,
                'y': cY,
                'r': cR
            },
            'point': {
                'x': Px,
                'y': Py
            },
            'r2': r2,
            'alpha': a == undefined ? 100 : a,
            'lock': lock || false
        }
    }

    var data = [circleData(512 / 2, 512 / 2, 512 / 2, 0, 0, 512)];

    function draw(clear) {
        if(clear) {
            ctx.clearRect(0, 0, 512, 512);
        }
        for(var i = data.length; i--;) {
            var c = data[i];
            if(c) {
                ctx.save();
                ctx.globalAlpha = c.alpha / 100;
                drawRoundOne2(rData, c.r2, c.circle.x, c.circle.y, c.point.x, c.point.y);
                ctx.restore();
            }
        }
    }
    draw();

    var last;
    canvas.addEventListener('mousemove', function(e) {
        var mouseX = e.pageX - canvas.offsetLeft,
                mouseY = e.pageY - canvas.offsetTop;
        for(var i = data.length; i--;) {
            var c = data[i];
            if(!c || c.r2 == 4 || c.lock) {
                if(c && c.r2 >= 128) {
                    return;
                }
                continue;
            }
            if(isPointInCircle(c.circle.x, c.circle.y, c.circle.r, mouseX, mouseY)) {
                if(last == c) {
                    return;
                } else {
                    last = null;
                }
                var r2 = c.r2 / 2;
                var rData = r[r2];
                var cX = c.circle.x,
                        cY = c.circle.y,
                        pX = c.point.x,
                        pY = c.point.y;
                var alpha, isLock;
                if(r2 <= 64) {
                    alpha = 100;
                    isLock = false;
                } else {
                    alpha = 0;
                    isLock = true;
                }
                var newC1 = circleData(cX - r2 / 2, cY - r2 / 2, r2 / 2, pX * 2, pY * 2, r2, alpha, isLock),
                        newC2 = circleData(cX + r2 / 2, cY - r2 / 2, r2 / 2, pX * 2 + 1, pY * 2, r2, alpha, isLock),
                        newC3 = circleData(cX - r2 / 2, cY + r2 / 2, r2 / 2, pX * 2, pY * 2 + 1, r2, alpha, isLock),
                        newC4 = circleData(cX + r2 / 2, cY + r2 / 2, r2 / 2, pX * 2 + 1, pY * 2 + 1, r2, alpha, isLock);
                data.push(newC1, newC2, newC3, newC4);
                c.lock = true;
                var newC = [newC1, newC2, newC3, newC4];
                for(var m = newC.length; m--;) {
                    var c = newC[m];
                    if(isPointInCircle(c.circle.x, c.circle.y, c.circle.r, mouseX, mouseY)) {
                        last = c;
                    }
                }
                if(c.r2 >= 128) {
                    (function() {
                        var j = i;
                        var old = data[j];
                        (function() {
                            if(old.alpha == 0) {
                                for(var i = newC.length; i--;) {
                                    var c = newC[i];
                                    c.lock = false;
                                }
                                delete data[j];
                                draw(true);
                            } else {
                                for(var i = newC.length; i--;) {
                                    newC[i].alpha += 25;
                                }
                                old.alpha -= 25;
                                draw(true);
                                setTimeout(arguments.callee, 50);
                            }
                        })();
                    })()
                } else {
                    delete data[i];
                    draw(true);
                }
                return;
            }
        }
        last = null;
    });
};
img.src = 'image.png';  
</script>  
</body>  
</html>  

足足239行,算是个大家伙了.虽然这段代码已经可以在大多数的现代浏览器中以流畅的速度运行,但是有很多地方实际上是可以修改得更好的.

删掉多余的内容

还记得我们在之前实现了drawRound,drawRoundOne,drawRoundOne2吗?

这三个函数都是用来绘制圆形的,但只有drawRoundOne2陪伴我们走到了最后,现在是时候删掉drawRound和drawRoundOne了.然后我们再把drawRoundOne2的名称索性修改成drawRound.

再借助调试工具,找到之前调用过drawRoundOne2的地方,替换为drawRound.

在浏览器中运行代码,没有任何问题.

这些小动作帮助我们将代码缩减到了213行.

减少新建变量

从浏览器的任务管理器中可以发现,我们的代码在初始化后就要占用50MB左右的内存(这还包括浏览器沙箱,界面缓存等资源),虽然相比大多数的网页占得不多,但执行动画后是什么样呢?

测试后发现,执行完动画后内存膨胀到80MB左右,虽然垃圾回收机制很厉害,但我们还是要去考虑降低内存的占用,这样垃圾回收也的负担也降低了.

以下是修改后的部分:

function isPointInCircle(circleX, circleY, circleRadius, mouseX, mouseY) {  
    return Math.pow(mouseX - circleX, 2) + Math.pow(mouseY - circleY, 2) <= Math.pow(circleRadius, 2);
}
(function() {
    if(old.alpha == 0) {
        for(var i = newC.length; i--;) {
            newC[i].lock = false;
        }
        delete data[j];
        draw(true);
    } else {
        for(var i = newC.length; i--;) {
            newC[i].alpha += 25;
        }
        old.alpha -= 25;
        draw(true);
        setTimeout(arguments.callee, 50);
    }
})();

经过改动,内存占用从80MB缩减到了70MB,效果显著.

更少的兼容性判断

处理兼容性问题是耗费时间的,所以要删掉所有兼容的代码,让所有代码按标准执行.

修改以下几处:

function circleData(cX, cY, cR, Px, Py, r2, a, lock) {  
    return {
        'circle': {
            'x': cX,
            'y': cY,
            'r': cR
        },
        'point': {
            'x': Px,
            'y': Py
        },
        'r2': r2,
        'alpha': a,
        'lock': lock
    }
}
var isLock = !(r2 <= 64);  
var alpha = isLock ? 0 : 100;  
function draw() {  
    ctx.clearRect(0, 0, 512, 512);
    for(var i = data.length; i--;) {
        var c = data[i];
        if(c) {
            ctx.save();
            ctx.globalAlpha = c.alpha / 100;
            drawRound(rData, c.r2, c.circle.x, c.circle.y, c.point.x, c.point.y);
            ctx.restore();
        }
    }
}
算法优化

在算法部分,也有几处可以优化的.

首先是计算圆在画布上的数量,我们之前使用的是(512512)/(r2r2)的方法,现在我们替换成效率更高的Math.pow(512 / r2, 2).

还有几处使用算术表达式计算结果的,可以直接用计算结果替换表达式.

优化后的代码长这样:

<!DOCTYPE html>  
<html>  
<head>  
    <meta charset='UTF-8'/>
    <title>圣诞彩蛋</title>
<style>  
    body{
        text-align:center;
        background:#E6E7E8;
    }
</style>  
</head>  
<body>  
<canvas width='512' height='512'></canvas>  
<script>  
var canvas = document.querySelector('canvas');  
var ctx = canvas.getContext('2d');  
var img = new Image();  
img.onload = function() {  
  function r(r2, data) {
    if(r2 != 512) {
      var rs = r2 * r2;
      var points = Math.pow(512 / r2, 2);
      var r = new Uint8ClampedArray(points * 3);
      var size = Math.sqrt(points);
      for(var i = 0; i < points; i++) {
        var red = 0
          , green = 0
          , blue = 0;
        var xBegin = i % size * r2
          , yBegin = Math.floor(i / size) * r2;
        var xEnd = xBegin + r2
          , yEnd = yBegin + r2;
        for(var x = xBegin; x < xEnd; x++) {
          for(var y = yBegin; y < yEnd; y++) {
            var j = (y * 512 + x) * 4;
            red += data[j];
            green += data[j + 1];
            blue += data[j + 2];
          }
        }
        r[i * 3] = red / rs;
        r[i * 3 + 1] = green / rs;
        r[i * 3 + 2] = blue / rs;
      }
    } else {
      var r = new Uint8ClampedArray(3);
      var red = 0
        , green = 0
        , blue = 0;
      for(var i = data.length; i -= 4;) {
        red += data[i];
        green += data[i + 1];
        blue += data[i + 2];
      }
      r[0] = red / 262144;
      r[1] = green / 262144;
      r[2] = blue / 262144;
    }
    return r;
  }

  function rgb(r, g, b) {
    return 'rgb(' + r + ',' + g + ',' + b + ')';
  }

  function drawRound(r, r2, cX, cY, x, y) {
    ctx.save();
    var i = (512 / r2 * y + x) * 3;
    ctx.fillStyle = rgb(r[i], r[i + 1], r[i + 2]);
    ctx.beginPath();
    ctx.arc(cX, cY, r2 / 2, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  function isPointInCircle(circleX, circleY, circleRadius, mouseX, mouseY) {
    return Math.pow(mouseX - circleX, 2) + Math.pow(mouseY - circleY, 2) <= Math.pow(circleRadius, 2);
  }

  function circleData(cX, cY, cR, Px, Py, r2, a, lock) {
    return {
      'circle': {
        'x': cX
      , 'y': cY
      , 'r': cR
      }
    , 'point': {
        'x': Px
      , 'y': Py
      }
    , 'r2': r2
    , 'alpha': a
    , 'lock': lock
    }
  }

  function draw() {
    ctx.clearRect(0, 0, 512, 512);
    for(var i = data.length; i--;) {
      var c = data[i];
      if(c) {
        ctx.save();
        ctx.globalAlpha = c.alpha / 100;
        drawRound(rData[c.r2], c.r2, c.circle.x, c.circle.y, c.point.x, c.point.y);
        ctx.restore();
      }
    }
  }

  var canvas2 = document.createElement('canvas');
  canvas2.width = canvas2.height = 512;
  var ctx2 = canvas2.getContext('2d');
  ctx2.drawImage(img, 0, 0);
  var data = ctx2.getImageData(0, 0, 512, 512).data;

  var rData = [];
  rData[512] = r(512, data);
  rData[256] = r(256, data);
  rData[128] = r(128, data);
  rData[64] = r(64, data);
  rData[32] = r(32, data);
  rData[16] = r(16, data);
  rData[8] = r(8, data);
  rData[4] = r(4, data);
  rData[2] = r(2, data);

  data = [circleData(256, 256, 256, 0, 0, 512, 100, false)];

  draw();

  var last;
  canvas.addEventListener('mousemove', function(e) {
    var mouseX = e.pageX - canvas.offsetLeft
      , mouseY = e.pageY - canvas.offsetTop;
    for(var i = data.length; i--;) {
      var c = data[i];
      if(!c || c.r2 == 4 || c.lock) {
        if(c && c.r2 >= 128) {
          return;
        }
        continue;
      }
      if(isPointInCircle(c.circle.x, c.circle.y, c.circle.r, mouseX, mouseY)) {
        if(last == c) {
          return;
        } else {
          last = null;
        }
        var r2 = c.r2 / 2;
        var rData = r[r2];
        var cX = c.circle.x
          , cY = c.circle.y
          , pX = c.point.x
          , pY = c.point.y;
        var isLock = !(r2 <= 64);
        var alpha = isLock ? 0 : 100;
        var newC1 = circleData(cX - r2 / 2, cY - r2 / 2, r2 / 2, pX * 2, pY * 2, r2, alpha, isLock)
          , newC2 = circleData(cX + r2 / 2, cY - r2 / 2, r2 / 2, pX * 2 + 1, pY * 2, r2, alpha, isLock)
          , newC3 = circleData(cX - r2 / 2, cY + r2 / 2, r2 / 2, pX * 2, pY * 2 + 1, r2, alpha, isLock)
          , newC4 = circleData(cX + r2 / 2, cY + r2 / 2, r2 / 2, pX * 2 + 1, pY * 2 + 1, r2, alpha, isLock);
        data.push(newC1, newC2, newC3, newC4);
        c.lock = true;
        var newC = [newC1, newC2, newC3, newC4];
        for(var m = newC.length; m--;) {
          var c = newC[m];
          if(isPointInCircle(c.circle.x, c.circle.y, c.circle.r, mouseX, mouseY)) {
            last = c;
          }
        }
        if(c.r2 >= 128) {
          (function() {
            var j = i;
            var old = data[j];
            (function() {
              if(old.alpha == 0) {
                for(var i = newC.length; i--;) {
                  newC[i].lock = false;
                }
                delete data[j];
                draw();
              } else {
                for(var i = newC.length; i--;) {
                  newC[i].alpha += 25;
                }
                old.alpha -= 25;
                draw();
                setTimeout(arguments.callee, 50);
              }
            })();
          })()
        } else {
          delete data[i];
          draw();
        }
        return;
      }
    }
    last = null;
  });
};
img.src = 'image.png';  
</script>  
</body>  
</html>  

事实上,我们还可以修改变量名称来增强开发人员的可读性,不过在这个项目中就暂时忽略这点吧.

NEXT

在下一个教程中,将通过PHP写一个后台程序来完成浏览器上传和下载图片的功能.

HTML5实例教程:圣诞彩蛋(六)