# canvas效果

# 粒子系统

在粒子系统中,最长出现的概念就是向量,粒子,粒子系统。我们可以把它们各封装成一个类,但根据不同场景也可以把其中一个简化为对象直接作为另一种的一个变量。

向量是每个粒子位置的关键,他定义了粒子坐标位置,以及对坐标位置的操作的方法。粒子是最小的移动元素,它定义了自己的在坐标系中的位置,速度,生命及大小颜色等属性,当然还有绘制规则。粒子系统,在他里面会定义粒子集,并且定了他们的行动和杀死指令,控制他们的渲染

常见的粒子系统封装如下

//向量
Vector = function (x, y) { 
    this.x = x; 
    this.y = y; 
    this.add=function (v) { return new Vector(this.x + v.x, this.y + v.y); }
};
//粒子
Particle = function(position, velocity, life, color, size) {
    this.position = position;
    this.velocity = velocity;
    this.age = 0;
    this.life = life;
    this.color = color;
    this.size = size;
};
//粒子系统
function ParticleSystem() {
    // Private fields
    var particles = new Array();
 
    // Public fields
}
# 常用到的方法

canvas上的方法

  • toDataURL("image/png"):将画布内容转化为base64格式的图片,可直接赋值给image对象的src

context上的方法

  • getImageData(0,0,canvas.width,canvas.height):将范围内的图画数据取出,对象中包括width、height、data。data数据格式是每个像素的rgba
  • ctx.putImageData(imgData,0,0):将值重新付给canvas绘制

以下是方法演示

var imgData=ctx.getImageData(0,0,c.width,c.height);
for (var i=0;i<imgData.data.length;i+=4)
  {
  imgData.data[i]=255-imgData.data[i];
  imgData.data[i+1]=255-imgData.data[i+1];
  imgData.data[i+2]=255-imgData.data[i+2];
  imgData.data[i+3]=255;
  }
ctx.putImageData(imgData,0,0);
# 利用image每格像素

我们将实现一个像素零散聚合成自己名字的动画

这个动画我们使用面向过程的思想去完成,那么他的实现过程如下

  • 画出目标图像
  • 筛选出需要移动的像素
  • 动画

在这种方法下,我们主要存储的数据是有颜色的格子,因为如果存储所有的格子将极其消耗内存。我们将这些有颜色的像素格子看作是一个粒子,整个画布是粒子系统。

以下为零散聚合示例

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        body {
            color: aqua;
        }
    </style>
</head>

<body>
    <canvas id="canvas"></canvas>
    <script>
        var canvas = document.getElementById('canvas')
        var ctx = canvas.getContext('2d')
        var data = []
        var dataObj = []
        var dataScreen = []
        canvas.width = 800
        canvas.height = 400
        var id = 0;
        init()
        choose(0.3)
        animate(id, 20)

        //画出字样、收集数据
        function init() {
            ctx.font = '200px Arial'
            ctx.fillStyle = 'aqua'
            ctx.textBaseline = 'top'
            ctx.fillText('夏炜轩', 0, canvas.height / 3)
            data = ctx.getImageData(0, 0, canvas.width, canvas.height).data
            var i = 0, n = 0;
            var index=0;
            for (var row = 0; row < canvas.height; row++) {
                for (var col = 0; col < canvas.width; col++) {
                    n = row * canvas.width + col//第几个点
                    i = n * 4//data中对应位置
                    if (data[i + 3] > 0) {
                        dataObj[index++] = {
                            x: col,
                            y: row,
                            color: `rgba(${data[i]},${data[i + 1]},${data[i + 2]},${data[i + 3]})`,
                            red: data[i],
                            green: data[i + 1],
                            blue: data[i + 2],
                            randomX: Math.floor(Math.random() * canvas.height),
                            randomY: Math.floor(Math.random() * canvas.width),
                        }
                    }
                }
            }
        }

        //筛选数据
        function choose(op) {
            var n = Math.floor(dataObj.length * op);
            for (var i = 0; i < n; i++) {
                dataScreen.push(dataObj.splice(Math.floor(Math.random() * dataObj.length), 1)[0])
            }
        }

        //聚合动画
        function animate( id, n) {
            id = setInterval(function () {
                ctx.clearRect(0, 0, canvas.width, canvas.height)
                for (var i = 0; i < dataScreen.length; i++) {
                    var temp = dataScreen[i];
                    var x0 = temp.randomX;
                    var y0 = temp.randomY;
                    var disX = temp.x - temp.randomX;
                    var disY = temp.y - temp.randomY;
                    // console.log(temp)
                    ctx.fillStyle=temp.color
                    ctx.fillRect(x0 + disX / n, y0 + disY / n, 1, 1);
                }
                n--;
                if (n === 0) {
                    clearInterval(id);
                } else {
                    setInterval( id, n);
                }
            }, 60)
        }



    </script>
</body>

</html>

上面得效果比较简单,我们使用的是面向过程得思想,当项目复杂得时候我们可以使用面向对象得方式将他们抽象成类。

# 手动创建粒子系统

接下来我们制作粒子随鼠标移动的经典案例

动画原理:这样的动画,我们的思路为不断刷新绘制,于此同时使用另一个计时器按照时间改变粒子对象的属性,这样就可以呈现出粒子动的效果

效果原理:我们会在刚开始将满屏画满粒子,但是全部设置为透明,接近鼠标的改变颜色属性

//粒子定义

         function Particle() {
            this.x = 0;//粒子现在的x坐标
            this.y = 0;
            this.originX = 0;//粒子原始的x坐标
            this.originY = 0;
            this.radius = 0;//粒子的半径
            this.color = 0;//粒子颜色
            this.closest = []//本粒子相离最近的五个粒子
            this.active = 0;//粒子状态
            this.circleActive = 0;//绘制出圆球的状态
            this.draw = function (ctx) {//画出粒子球
                if (!this.active) return;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
                ctx.fillStyle = 'rgba(156,217,249,' + this.active + ')';
                ctx.fill();
            }
            this.drawLine = function (ctx) {//画出粒子间的线
                if (!this.active) return;
                for (var i in this.closest) {
                    ctx.beginPath();
                    ctx.moveTo(this.x, this.y);
                    ctx.lineTo(this.closest[i].x, this.closest[i].y);
                    ctx.strokeStyle = 'rgba(156,217,249,' + this.active + ')';
                    ctx.stroke();
                }
            }
        }

接下来我们来定义粒子系统,它包含了粒子的集合,以及他们的渲染的动作

        function System(canvas) {
            this.width = window.innerWidth;
            this.height = window.innerHeight;
            this.largeHeader;
            this.canvas = canvas;
            this.ctx = canvas.getContext('2d');
            this.points = [];
            this.target = {//鼠标的位置
                x: canvas.width / 2,
                y: canvas.height / 2
            };
            this.animateHeader = true;

            canvas.width = this.width;
            canvas.height = this.height;

        }
		System.prototype.initHeader=function(){};//初始化每个粒子的属性
		System.prototype.initAnimation=function(){};//动画
		System.prototype.addListeners=function(){};//根据鼠标的移动而产生变化

我们逐个实现,初始化每个粒子的属性

        System.prototype.initHeader = function () {
            //初始化点线
            points = this.points;
            width = this.width;
            height = this.height;
            //定义一定数量的点
            for (var x = 0; x < width; x = x + width / 20) {
                for (var y = 0; y < height; y = y + height / 20) {
                    var px = x + Math.random() * width / 20;
                    var py = y + Math.random() * height / 20;
                    var p = new Particle()
                    p.x = px;
                    p.y = py;
                    p.originX = px;
                    p.originY = py;
                    points.push(p);
                }
            }

            // 找到每个点旁边最近的五个点
            for (var i = 0; i < points.length; i++) {
                var closest = [];
                var p1 = points[i];
                for (var j = 0; j < points.length; j++) {
                    var p2 = points[j]
                    if (!(p1 == p2)) {
                        var placed = false;
                        for (var k = 0; k < 5; k++) {
                            if (!placed) {
                                if (closest[k] == undefined) {
                                    closest[k] = p2;
                                    placed = true;
                                }
                            }
                        }

                        for (var k = 0; k < 5; k++) {
                            if (!placed) {
                                if (this._getDistance(p1, p2) < this._getDistance(p1, closest[k])) {
                                    closest[k] = p2;
                                    placed = true;
                                }
                            }
                        }
                    }
                }
                p1.closest = closest;
            }

            // 为每个粒子添加圆的属性
            for (var i in points) {
                points[i].color = 'rgba(255,255,255,0.3)'
                points[i].radius = 2 + Math.random() * 2
 			}
        };           

接下来实现动画,动画要做的有两点,首先是动画循环渲染,可以使用计时器间隔1/60秒渲染一次,也可以使用h5requestAnimationFrame方法。这里的定时器负责每个相应的时间渲染,他根据每个粒子的x,y属性循环渲染所有粒子。于此同时我们还应该需要一个计时器来定时改变每个粒子的x,y,当然,时间也需要足够短,让他实现渐变的效果。对于改变对象属性,我们可以使用现在存在的一些优秀动画库去实现,此处使用的是TweenLite,所以实现如下

        System.prototype.initAnimation = function () {
            this._animate();
            for (var i in this.points) {
                this._shiftPoint(this.points[i]);
            }
        };
        System.prototype._animate = function () {
            var points = this.points
            var target = this.target
            if (this.animateHeader) {
                //清屏、渲染重复
                this.ctx.clearRect(0, 0, this.width, this.height);
                for (var i in points) {
                    if (Math.abs(this._getDistance(target, points[i])) < 4000) {
                        points[i].active = 0.3;
                        points[i].circleActive = 0.6;
                    } else if (Math.abs(this._getDistance(target, points[i])) < 20000) {
                        points[i].active = 0.1;
                        points[i].circleActive = 0.3;
                    } else if (Math.abs(this._getDistance(target, points[i])) < 40000) {
                        points[i].active = 0.02;
                        points[i].circleActive = 0.1;
                    } else {
                        points[i].active = 0;
                        points[i].circleActive = 0;
                    }

                    points[i].drawLine(this.ctx);
                    points[i].draw(this.ctx);
                }
            }
            requestAnimationFrame(this._animate.bind(this));
        }
        System.prototype._getDistance = function (p1, p2) {
            return Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
        }
        System.prototype._shiftPoint = function (p) {
            //改变对象属性
            var that = this;
            TweenLite.to(p, 1 + 1 * Math.random(), {
                x: p.originX - 50 + Math.random() * 100,
                y: p.originY - 50 + Math.random() * 100,
                onComplete: function () {
                    that._shiftPoint(p);
                }
            });
        }

这时,动画已经可以在屏幕中显示了,我们还需要添加跟随鼠标移动的时间

        System.prototype.addListeners = function () {
            if (!('ontouchstart' in window)) {
                window.addEventListener('mousemove', this._mouseMove.bind(this));
            }
            window.addEventListener('scroll', this._scrollCheck.bind(this));
            window.addEventListener('resize', this._resize.bind(this));
        };
        System.prototype._mouseMove = function (e) {
            var posx = posy = 0;
            if (e.pageX || e.pageY) {
                posx = e.pageX;
                posy = e.pageY;
            } else if (e.clientX || e.clientY) {
                posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
                posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
            }
            this.target.x = posx;
            this.target.y = posy;
        }
        System.prototype._scrollCheck = function () {
            if (document.body.scrollTop > height) this.animateHeader = false;
            else this.animateHeader = true;
        }
        System.prototype._resize = function () {
            this.width = window.innerWidth;
            this.height = window.innerHeight;
            this.canvas.width = this.width;
            this.canvas.height = this.height;
        }

至此我们完成了所有效果,现在只需要去调用这个我们定义好的类

<!DOCTYPE html>
<html>

<head>
    <title></title>
    <style type="text/css">
        body {
            width: 100%;
            background: #333;
            overflow: hidden;
            background-size: cover;
            background-position: center center;
            z-index: 1;
        }
    </style>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/latest/TweenLite.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/latest/plugins/CSSPlugin.min.js"></script>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script>
    //上面的封装
    </script>
    <script>
        var canvas = document.getElementById('canvas')
        var c = new System(canvas)
        c.initHeader()
        c.initAnimation()
        c.addListeners()
    </script>
        
</body>

</html>
# 动画效果

上面我们将所有的粒子提前画到了canvas上,我们也可以在使用的时候再创建粒子。但主体思想是不变的,粒子对象定义了自己如何渲染以及更新,每个粒子被放在粒子系统中,然后利用动画去不间断渲染粒子系统中每一个粒子

var canvas = document.createElement("canvas");
        canvas.style.zIndex = '1';
        canvas.style.position = 'absolute';
        canvas.style.left = 0;
        canvas.style.top = 0;
        canvas.id = 'canvas';
        document.body.appendChild(canvas);
        var ctx = canvas.getContext("2d");


        var starlist = [];
        function init() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        }
        
        window.onresize = init;

        document.body.addEventListener('mousemove', function (e) {
            starlist.push(new Star(e.clientX, e.clientY));
        }, true)
        /*粒子生成*/
        function random(min, max) {
            return Math.floor((max - min) * Math.random() + min);
        }
        
        function Star(x, y) {
            this.x = x;
            this.y = y;
            this.vx = (Math.random() - 0.5) * 3;
            this.vy = (Math.random() - 0.5) * 3;
            this.color = 'rgba(' + random(0, 256) + ',' + random(0, 256) + ',' + random(0, 256) + ',' + 0.5 + ')';
            this.a = 1;
            this.draw();
        }
        Star.prototype = {
            draw: function () {
                ctx.beginPath();
                ctx.fillStyle = this.color;
                ctx.globalCompositeOperation = 'lighter'
                ctx.globalAlpha = this.a;
                ctx.arc(this.x, this.y, 15, 0, Math.PI * 2, false);
                ctx.fill();
                this.updata();
            },
            updata() {
                this.x += this.vx;
                this.y += this.vy;
                this.a *= 0.98;
            }
        }
        /*画出粒子*/
        function render() {
            ctx.clearRect(0, 0, canvas.width, canvas.height)

            starlist.forEach((item, i) => {
                item.draw();
                if (item.a < 0.05) {
                    starlist.splice(i, 1);
                }
            })

            requestAnimationFrame(render);
        }
        
        init();
        render();

# 视觉差页面

# 常用到的方法
{
	background-attachment: fixed;
}
/*
html元素的data-*属性:为元素起别名,赋予我们在所有 HTML 元素上嵌入自定义 data 属性的能力。
*/
# 背景固定

利用background-attachment: fixed;属性,将图片定位在此处

此属性部分移动端无法支持,iphone暂不支持,可用其他方法实现

body:before {

content: "";

background: url(../img/beiJing3.png) no-repeat;

background-size: 100%;

position: fixed;

top: 1rem;

left: 0;

bottom: 0;

right: 0;

z-index: -1;

}
# 元素滚动速度差异

监听滚动事件,让元素的移动速度和滚轮的滚动速度不同从而展示出视觉差效果

	<section id="first" class="story" data-speed="8" data-type="background">    	
		<div class="smashinglogo" data-type="sprite" data-offsetY="100" data-Xposition="50%" data-speed="-2"></div>		
		<p>
            some废话
        </p>
	</section>


$(document).ready(function () {
	$window = $(window);
	$('[data-type]').each(function () {
		$(this).data('offsetY', parseInt($(this).attr('data-offsetY')));
		$(this).data('Xposition', $(this).attr('data-Xposition'));
		$(this).data('speed', $(this).attr('data-speed'));
	});

	$('section[data-type="background"]').each(function () {

		var $self = $(this),
			offsetCoords = $self.offset(),
			topOffset = offsetCoords.top;

		$(window).scroll(function () {

			if (($window.scrollTop() + $window.height()) > (topOffset) &&
				((topOffset + $self.height()) > $window.scrollTop())) {				
				var yPos = -($window.scrollTop() / $self.data('speed'));
				if ($self.data('offsetY')) {
					yPos += $self.data('offsetY');
				}
				var coords = '50% ' + yPos + 'px';
				$self.css({ backgroundPosition: coords });
				$('[data-type="sprite"]', $self).each(function () {
					var $sprite = $(this);
					var yPos = -($window.scrollTop() / $sprite.data('speed'));
					var coords = $sprite.data('Xposition') + ' ' + (yPos + $sprite.data('offsetY')) + 'px';

					$sprite.css({ backgroundPosition: coords });

				}); 
				$('[data-type="video"]', $self).each(function () {
					var $video = $(this);
					var yPos = -($window.scrollTop() / $video.data('speed'));
					var coords = (yPos + $video.data('offsetY')) + 'px';
					$video.css({ top: coords });

				}); // video	

			}; // in view

		}); // window scroll

	});	// each data-type

}); // document ready

# css3动画

css3动画常用到keyframes创建

/*旋转照片动画定义*/
    @keyframes myfirst {
      from {
        transform: rotateX(10deg);
      }

      to {
        transform: rotateX(50deg);
      }
    }

    .animate_box {
      z-index: 2000;
      position: absolute;
      width: 200px;
      left: 50px;
      top: 150px;
      height: 200px;
      margin: 50px auto;
      perspective: 2800px;
      /*距离屏幕距离*/
      perspective-origin: -100% -100%;
      /*倾斜度*/
      transform-style: preserve-3d;
    }

    .animate_box ul {
      width: 100px;
      height: 100px;
      z-index: 1500;
      position: relative;
      top: 200px;
      margin: 0 auto;
      transform-style: preserve-3d;
      /*子元素保留3d位置*/
    }

    .animate_box ul li {
      z-index: 1000;
      width: 100px;
      height: 100px;
      text-align: center;
      line-height: 100px;
      font-size: 25px;
      color: white;
      position: absolute;
      background: rgba(225, 0, 0, 0.2);
      /* border: 1px solid black; */
      transition: all 1s linear;
      overflow: hidden;
    }

    /* 上面 */
    .animate_box li:nth-child(1) {
      transform: translateY(-50px) rotateX(90deg);
    }

    .animate_box:hover li:nth-child(1) {
      transform: translateY(-100px) rotateX(90deg);
      background: palevioletred;
      border: 0;
      border-radius: 50%;
    }

    /*后面*/
    .animate_box li:nth-child(2) {
      transform: translateX(50px) rotateY(90deg);
    }

    .animate_box:hover li:nth-child(2) {
      transform: translateX(100px) rotateY(90deg);
      background: #92ecae;
      border: 0;
      border-radius: 50%;
    }

    /*下面*/
    .animate_box li:nth-child(3) {
      transform: translateY(50px) rotateX(90deg);
    }

    .animate_box:hover li:nth-child(3) {
      transform: translateY(100px) rotateX(90deg);
      background: #ff916a;
      border: 0;
      border-radius: 50%;
    }

    /*左面*/
    .animate_box li:nth-child(4) {
      transform: translateX(-50px) rotateY(90deg);
    }

    .animate_box:hover li:nth-child(4) {
      transform: translateX(-100px) rotateY(90deg);
      background: greenyellow;
      border: 0;
      border-radius: 50%;
    }

    /*右面*/
    .animate_box li:nth-child(5) {
      transform: translateZ(-50px);
    }

    .animate_box:hover li:nth-child(5) {
      transform: translateZ(-100px);
      background: lightskyblue;
      border: 0;
      border-radius: 50%;
    }

    /*正面*/
    .animate_box li:nth-child(6) {
      transform: translateZ(50px);
    }

    .animate_box:hover li:nth-child(6) {
      transform: translateZ(100px);
      background: #be46d8;
      border: 0;
      border-radius: 50%;
    }

    .animate_box ul:hover {
      transform: rotateX(9000deg) rotateY(5000deg);
      transition: all 100s linear;
    }