[英]Canvas particles, collisions and performance
I'm creating a web app which has an interactive background with particles bouncing around. 我正在创建一个Web应用程序,它具有交互式背景,粒子在弹跳。 At all times there are about 200 circular particles on the screen and at most around 800 particles.
在任何时候屏幕上都有大约200个圆形颗粒,最多约800个颗粒。 Some of the collisions and effects that are being run for the particles are the following prototypes.
正在为粒子运行的一些碰撞和效果是以下原型。 I wonder if I could improve the performance by using web workers to do these calculations?
我想知道我是否可以通过使用网络工作人员来进行这些计算来提高性能?
/**
* Particles
*/
Jarvis.prototype.genForegroundParticles = function(options, count){
count = count || this.logoParticlesNum;
for (var i = 0; i < count; i++) {
this.logoParticles.push(new Particle());
}
}
Jarvis.prototype.genBackgroundParticles = function(options, count){
count = count || this.backgroundParticlesNum;
for (var i = 0; i < count; i++) {
this.backgroundParticles.push(new Particle(options));
}
}
Jarvis.prototype.motion = {
linear : function(particle, pIndex, particles){
particle.x += particle.vx
particle.y += particle.vy
},
normalizeVelocity : function(particle, pIndex, particles){
if (particle.vx - particle.vxInitial > 1) {
particle.vx -= 0.05;
} else if (particle.vx - particle.vxInitial < -1) {
particle.vx += 0.05;
}
if (particle.vy - particle.vyInitial > 1) {
particle.vy -= 0.05;
} else if (particle.vx - particle.vxInitial < -1) {
particle.vy += 0.05;
}
},
explode : function(particle, pIndex, particles) {
if (particle.isBottomOut()) {
particles.splice(pIndex, 1);
} else {
particle.x += particle.vx;
particle.y += particle.vy;
particle.vy += 0.1;
}
if (particles.length === 0){
particles.motion.removeMotion("explode");
this.allowMenu = true;
}
}
}
Jarvis.prototype.collision = {
boundingBox: function(particle, pIndex, particles){
if (particle.y > (this.HEIGHT - particle.radius) || particle.y < particle.radius) {
particle.vy *= -1;
}
if(particle.x > (this.WIDTH - particle.radius) || particle.x < particle.radius) {
particle.vx *= -1;
}
},
boundingBoxGravity: function(particle, pIndex, particles){
// TODO: FIX GRAVITY TO WORK PROPERLY IN COMBINATION WITH FX AND MOTION
if (particle.y > (this.HEIGHT - particle.radius) || particle.y < particle.radius) {
particle.vy *= -1;
particle.vy += 5;
}
if(particle.x > (this.WIDTH - particle.radius) || particle.x < particle.radius) {
particle.vx *= -1;
particle.vx += 5;
}
},
infinity: function(particle, pIndex, particles){
if (particle.x > this.WIDTH){
particle.x = 0;
}
if (particle.x < 0){
particle.x = this.WIDTH;
}
if (particle.y > this.HEIGHT){
particle.y = 0;
}
if (particle.y < 0) {
particle.y = this.HEIGHT;
}
}
}
Jarvis.prototype.fx = {
link : function(particle, pIndex, particles){
for(var j = pIndex + 1; j < particles.length; j++) {
var p1 = particle;
var p2 = particles[j];
var particleDistance = getDistance(p1, p2);
if (particleDistance <= this.particleMinLinkDistance) {
this.backgroundCtx.beginPath();
this.backgroundCtx.strokeStyle = "rgba("+p1.red+", "+p1.green+", "+p1.blue+","+ (p1.opacity - particleDistance / this.particleMinLinkDistance) +")";
this.backgroundCtx.moveTo(p1.x, p1.y);
this.backgroundCtx.lineTo(p2.x, p2.y);
this.backgroundCtx.stroke();
this.backgroundCtx.closePath();
}
}
},
shake : function(particle, pIndex, particles){
if (particle.xInitial - particle.x >= this.shakeAreaThreshold){
particle.xOper = (randBtwn(this.shakeFactorMin, this.shakeFactorMax) * 2) % (this.WIDTH);
} else if (particle.xInitial - particle.x <= -this.shakeAreaThreshold) {
particle.xOper = (randBtwn(-this.shakeFactorMax, this.shakeFactorMin) * 2) % (this.WIDTH);
}
if (particle.yInitial - particle.y >= this.shakeAreaThreshold){
particle.yOper = (randBtwn(this.shakeFactorMin, this.shakeFactorMax) * 2) % (this.HEIGHT);
} else if (particle.yInitial - particle.y <= -this.shakeAreaThreshold) {
particle.yOper = (randBtwn(-this.shakeFactorMax, this.shakeFactorMin) * 2) % (this.HEIGHT);
}
particle.x += particle.xOper;
particle.y += particle.yOper;
},
radialWave : function(particle, pIndex, particles){
var distance = getDistance(particle, this.center);
if (particle.radius >= (this.dim * 0.0085)) {
particle.radiusOper = -0.02;
} else if (particle.radius <= 1) {
particle.radiusOper = 0.02;
}
particle.radius += particle.radiusOper * particle.radius;
},
responsive : function(particle, pIndex, particles){
var newPosX = (this.logoParticles.logoOffsetX + this.logoParticles.particleRadius) + (this.logoParticles.particleDistance + this.logoParticles.particleRadius) * particle.arrPos.x;
var newPosY = (this.logoParticles.logoOffsetY + this.logoParticles.particleRadius) + (this.logoParticles.particleDistance + this.logoParticles.particleRadius) * particle.arrPos.y;
if (particle.xInitial !== newPosX || particle.yInitial !== newPosY){
particle.xInitial = newPosX;
particle.yInitial = newPosY;
particle.x = particle.xInitial;
particle.y = particle.yInitial;
}
},
motionDetect : function(particle, pIndex, particles){
var isClose = false;
var distance = null;
for (var i = 0; i < this.touches.length; i++) {
var t = this.touches[i];
var point = {
x : t.clientX,
y : t.clientY
}
var d = getDistance(point, particle);
if (d <= this.blackhole) {
isClose = true;
if (d <= distance || distance === null) {
distance = d;
}
}
}
if (isClose){
if (particle.radius < (this.dim * 0.0085)) {
particle.radius += 0.25;
}
if (particle.green >= 0 && particle.blue >= 0) {
particle.green -= 10;
particle.blue -= 10;
}
} else {
if (particle.radius > particle.initialRadius) {
particle.radius -= 0.25;
}
if (particle.green <= 255 && particle.blue <= 255) {
particle.green += 10;
particle.blue += 10;
}
}
},
reverseBlackhole : function(particle, pIndex, particles){
for (var i = 0; i < this.touches.length; i++) {
var t = this.touches[i];
var point = {
x : t.clientX,
y : t.clientY
}
var distance = getDistance(point, particle);
if (distance <= this.blackhole){
var diff = getPointsDifference(point, particle);
particle.vx += -diff.x / distance;
particle.vy += -diff.y / distance;
}
}
}
}
Furthermore in case anyone wonders I have 3 canvas layers & I'll add the particles rendering function and the clear function for all canvas layers 此外,万一有人想知道我有3个画布层,我将为所有画布层添加粒子渲染功能和清晰功能
Background which draws a full screen radial gradient & particles 绘制全屏径向渐变和粒子的背景
Menu canvas 菜单画布
Menu button overlay selectors (show which menu is active etc) 菜单按钮覆盖选择器(显示哪个菜单处于活动状态等)
Jarvis.prototype.backgroundDraw = function() {
// particles
var that = this;
this.logoParticles.forEach(function(particle, i){
particle.draw(that.backgroundCtx);
that.logoParticles.motion.forEach(function(motionType, motionIndex){
that.motion[motionType].call(that, particle, i, that.logoParticles, "foregroundParticles");
});
that.logoParticles.fx.forEach(function(fxType, fxIndex){
that.fx[fxType].call(that, particle, i, that.logoParticles, "foregroundParticles");
});
that.logoParticles.collision.forEach(function(collisionType, collisionIndex){
that.collision[collisionType].call(that, particle, i, that.logoParticles, "foregroundParticles");
});
});
this.backgroundParticles.forEach(function(particle, i){
particle.draw(that.backgroundCtx);
that.backgroundParticles.motion.forEach(function(motionType, motionIndex){
that.motion[motionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
});
that.backgroundParticles.fx.forEach(function(fxType, fxIndex){
that.fx[fxType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
});
that.backgroundParticles.collision.forEach(function(collisionType, collisionIndex){
that.collision[collisionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
});
});
}
Jarvis.prototype.clearCanvas = function() {
switch(this.background.type){
case "radial_gradient":
this.setBackgroundRadialGradient(this.background.color1, this.background.color2);
break;
case "plane_color":
this.setBackgroundColor(this.background.red, this.background.green, this.background.blue, this.background.opacity);
break;
default:
this.setBackgroundColor(142, 214, 255, 1);
}
this.foregroundCtx.clearRect(this.clearStartX, this.clearStartY, this.clearDistance, this.clearDistance);
this.middlegroundCtx.clearRect(this.clearStartX, this.clearStartY, this.clearDistance, this.clearDistance);
}
Jarvis.prototype.mainLoop = function() {
this.clearCanvas();
this.backgroundDraw();
this.drawMenu();
window.requestAnimFrame(this.mainLoop.bind(this));
}
Any other optimization tips will be greatly appreciated. 任何其他优化技巧将不胜感激。 I've read a couple of articles but I'm not sure how to optimize this code further.
我已经阅读了几篇文章,但我不确定如何进一步优化此代码。
You can use FabricJS Canvas Library. 您可以使用FabricJS Canvas Library。 FabricJS by default supports interactivity, when you create a new object (circle, rectangle and etc) you can manipulate it by mouse or touchscreen.
FabricJS默认支持交互性,当您创建新对象(圆形,矩形等)时,您可以通过鼠标或触摸屏对其进行操作。
var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect({
width: 10, height: 20,
left: 100, top: 100,
fill: 'yellow',
angle: 30
});
canvas.add(rect);
See, we work there in object oriented way. 看,我们以面向对象的方式在那里工作。
I don't know what major improvement you can do here except switching to a technology that uses hardware acceleration. 除了切换到使用硬件加速的技术之外,我不知道你可以做些什么重大改进。
I hope this helps a bit, though as stated in question's comments WebGL would be really faster. 我希望这会有所帮助,但正如问题所述,WebGL会更快。 If you don't know where to start, here is a good one: webglacademy
如果你不知道从哪里开始,这里是一个很好的: webglacademy
Still I saw some little thingies: 我还是看到了一些小东西:
radialWave : function(particle, pIndex, particles){
// As you don't use distance here remove this line
// it's a really greedy calculus that involves square root
// always avoid if you don't have to use it
// var distance = getDistance(particle, this.center);
if (particle.radius >= (this.dim * 0.0085)) {
particle.radiusOper = -0.02;
} else if (particle.radius <= 1) {
particle.radiusOper = 0.02;
}
particle.radius += particle.radiusOper * particle.radius;
},
Another little thingy: 另一件小事:
Jarvis.prototype.backgroundDraw = function() {
// particles
var that = this;
// Declare callbacks outside of forEach calls
// it will save you a function declaration each time you loop
// Do this for logo particles
var logoMotionCallback = function(motionType, motionIndex){
// Another improvement may be to use a direct function that does not use 'this'
// and instead pass this with a parameter called currentParticle for example
// call and apply are known to be pretty heavy -> see if you can avoid this
that.motion[motionType].call(that, particle, i, that.logoParticles, "foregroundParticles");
};
var logoFxCallback = function(fxType, fxIndex){
that.fx[fxType].call(that, particle, i, that.logoParticles, "foregroundParticles");
};
var logoCollisionCallback = function(collisionType, collisionIndex){
that.collision[collisionType].call(that, particle, i, that.logoParticles, "foregroundParticles");
};
this.logoParticles.forEach(function(particle, i){
particle.draw(that.backgroundCtx);
that.logoParticles.motion.forEach(motionCallback);
that.logoParticles.fx.forEach(fxCallback);
that.logoParticles.collision.forEach(collisionCallback);
});
// Now do the same for background particles
var bgMotionCallback = function(motionType, motionIndex){
that.motion[motionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
};
var bgFxCallback = function(fxType, fxIndex){
that.fx[fxType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
};
var bgCollisionCallback = function(collisionType, collisionIndex){
that.collision[collisionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
};
this.backgroundParticles.forEach(function(particle, i){
particle.draw(that.backgroundCtx);
that.backgroundParticles.motion.forEach(bgMotionCallback);
that.backgroundParticles.fx.forEach(bgFxCallback);
that.backgroundParticles.collision.forEach(bgCollisionCallback);
});
}
If you are looking to speed up code, here are some micro-optimizations: 如果您希望加速代码,这里有一些微优化:
for(var i = 0, l = bla.length; i < l; i++) { ... }
instead of bla.forEach(...)
for(var i = 0, l = bla.length; i < l; i++) { ... }
而不是bla.forEach(...)
radius <= distance
is slow, radius*radius <= distanceSquared
is fast. radius <= distance
慢, radius*radius <= distanceSquared
很快。 reverseBlackhole : function(particle, pIndex, particles) { var blackholeSqr = this.blackhole * this.blackhole, touches = this.touches, fnSqrt = Math.sqrt, t, diffX, diffY, dstSqr; for (var i = 0, l = touches.length; i < l; i++) { t = touches[i]; diffX = particle.x - t.clientX; diffY = particle.y - t.clientY; distSqr = (diffX * diffX + diffY * diffY); // comparing distance without a SQRT needed if (dstSqr <= blackholeSqr){ var dist = Math.sqrt(dstSqr); particle.vx -= diffX / dist; particle.vy -= diffY / dist; } } }
To speed up drawing (or make it lag less during drawing): 为了加快绘图速度(或在绘图过程中减少绘图):
And for the whole animation: 而对于整个动画:
this.backgroundParticles.forEach(..)
: in case of 200 particles, this will do this.backgroundParticles.forEach(..)
:在200个粒子的情况下,这样做
this.backgroundParticles.forEach(
) this.backgroundParticles.forEach(
)
that.backgroundParticles.motion.forEach
) that.backgroundParticles.motion.forEach
) that.backgroundParticles.fx.forEach
) that.backgroundParticles.fx.forEach
) that.backgroundParticles.collision.forEach
) that.backgroundParticles.collision.forEach
) this.foregroundparticles.forEach(..)
this.foregroundparticles.forEach(..)
To get this more performance, remove the OOP stuff and go for ugly spaghetti code, only where it makes sense. 为了获得更高的性能,删除OOP的东西,并只在有意义的地方寻找丑陋的意大利面条代码。
Collision detection can be optimized by not testing every particle against each other. 可以通过不对每个粒子彼此进行测试来优化碰撞检测。 Just look up quadtrees.
只需查看四叉树。 Not that hard to implement, and the basics of it can be used to come up with a custom solution.
并不难实现,并且可以使用它的基础来提出自定义解决方案。
Since you are doing quite some vector math, try out the glmatrix library . 由于你正在做一些矢量数学,试试glmatrix库 。 Optimized vector math :-)
优化的矢量数学:-)
I think you might find that webworker support is about equal to WebGL support: 我想您可能会发现webworker支持与WebGL支持相同:
WebGL Support: http://caniuse.com/#search=webgl WebGL支持: http : //caniuse.com/#search=webgl
WebWorker Support: http://caniuse.com/#search=webworker WebWorker支持: http : //caniuse.com/#search=webworker
On the surface they may appear to be different, but they aren't really. 从表面上看,它们看起来可能不同,但实际上并非如此。 The only thing you'll gain is IE10 support temporarily.
你唯一能获得的是暂时的IE10支持。 IE11 has already surpassed IE10 in market share and the divide will continue to grow.
IE11的市场份额已经超过IE10,并且这一鸿沟将继续增长。 The only thing to note is that webgl support also seems to be based on updated graphics card drivers.
唯一需要注意的是webgl支持似乎也基于更新的显卡驱动程序。
Of course, I don't know your specific needs so maybe this doesn't work. 当然,我不知道你的具体需求,所以这可能不起作用。
Many libraries do this. 许多图书馆都这样做。 Canvas should be usable and just a little cool.
画布应该可以使用,只是有点酷。 WebGL generally has all the cool particle features.
WebGL通常具有所有很酷的粒子特征。
You'll likely need to use a deferred library or create a system that figures out when all the webworkers are done and have a pool of worker threads. 您可能需要使用延迟库或创建一个系统来确定所有Web工作者何时完成并拥有工作线程池。
Some caveats: 一些警告:
Unconfirmed Rumor: I've heard that there's a limited amount of data you can pass through the web worker messages. 未经证实的谣言:我听说您可以通过网络工作人员消息传递的数据量有限。 You should test this since it seems directly applicable to your use case.
您应该对此进行测试,因为它似乎直接适用于您的用例。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.