在粒子系统中,最长出现的概念就是向量
,粒子
,粒子系统
。我们可以把它们各封装成一个类,但根据不同场景也可以把其中一个简化为对象直接作为另一种的一个变量。
向量是每个粒子位置的关键,他定义了粒子坐标位置,以及对坐标位置的操作的方法。粒子是最小的移动元素,它定义了自己的在坐标系中的位置,速度,生命及大小颜色等属性,当然还有绘制规则。粒子系统,在他里面会定义粒子集,并且定了他们的行动和杀死指令,控制他们的渲染
常见的粒子系统封装如下
//向量
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);
我们将实现一个像素零散聚合成自己名字的动画
这个动画我们使用面向过程的思想去完成,那么他的实现过程如下
在这种方法下,我们主要存储的数据是有颜色的格子,因为如果存储所有的格子将极其消耗内存。我们将这些有颜色的像素格子看作是一个粒子,整个画布是粒子系统。
以下为零散聚合示例
<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>Documenttitle>
<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秒渲染一次,也可以使用h5
的requestAnimationFrame
方法。这里的定时器负责每个相应的时间渲染,他根据每个粒子的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;
}
至此我们完成了所有效果,现在只需要去调用这个我们定义好的类
<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
动画常用到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;
}