js实现任意布局照片墙

2,408 阅读8分钟
原文链接: mp.weixin.qq.com

最近厂里面在做大促,所以公众号的更新被无情地拖后了很久很久。欣仔总觉得有了个公众号,就像有了个孩子,或者有了只猫一样,觉得不能对他不管不顾,思前想后地琢磨着得有一篇文章谢谢。

这篇文章的来自于生活里一处细节的思索:生活中有一种随处可见的照片墙,就像这样:

(PS:这绝对不是一篇说装修的文章)

大概2年前,每当有看到这样的照片墙,都会想着该去如何实现这样的布局效果。但大概由于自己的懒惰,也觉得即便实现了也是无用的,因为也不会用到项目中去,这种利益至上的心理直接阻碍了这个功能的出现。但也庆幸2年前没有这样的需求,说不定以两年前的阅历来看,还未必能有效的解决这样的问题。。

直到现在。。当然也是另外一种利益至上的思维导致的。

这篇文章的内容也不是很多。当我有了要实现他的想法,我大概花了一个晚上的时间用来游戏和休息,然后又一个上午的时间继续休息,然后一个下午的时间看了部电影,一个傍晚的时间花了点时间缕清了思路。最后用了一个通宵的时间把这个功能做了出来。当然我在休息和娱乐的时候还是心系与此的啊,以至于我的王者荣耀都掉星掉到钻石了。。

我们要的最终的实现效果是这样

这个需求始终要克服的一个问题就是:所有的元素往画面中某处随机聚集,并且最终要两两不重叠。

在日常的前端开发过程中,大部分人其实都习惯了按照既定的元素大小,固定排列。因为这种需求我们可以通过很简单的行和列的关系去布局元素。因为所有的元素的又都是大小一致的,所以就不会出现重叠的现象。

在这样场景的布局中,假设欣仔用x和y表示元素的横坐标和纵坐标,宽高各为100的情况,布局在3*3的网格内。用几行简单的代码就可以实现,就像这样:

  1. //offset 表示元素之间的间隔

  2. const offset=10;

  3. for (let i =0;i< 9;i++)

  4. {

  5. let cell= new cell();

  6. cell.x=( 100+offset)*(i%3 );

  7. cell.y=( 100+offset)*((i/3 )>>0);

  8. }

最终可以达到这样的一个展示:

但是当元素的大小不受控制,布局不受控制的时候,即:对应的属性在有限的范围内具有随机性,那布局可能就会是这样:

中间有几个元素重叠在一起了。。。很显然这不是欣仔的初衷,我希望他们任意排列,但是不重叠。所以我就想着应该在这个时候去移动碰撞在一起的元素。但是移动的时候有要保证不去碰撞其他之前与之不重叠的元素,每个元素实际上是相互关联的,一旦移动,就得考虑所有其他元素与之的关系,然后就要想着是否去移动其他的元素。移动其他的元素又得重新考虑这个问题,这就像西西弗斯推石头一样,似乎是无穷无尽的处境,也许会有偶尔一次所有的元素不重叠在一起了,但是这并不能解决所有的情况。

解决方案要从另外一个角度去思考。

当我们在处理第一种所有尺寸和位置都既定的情况下,已经形成了一种概念:秩序。

当秩序都非常规范的时候,我们甚至都不用去做规划,拿到什么就直接往里面填坑即可。

当我们处理的是第二种所有的尺寸和位置都不既定的情况下,就失去了秩序,我们就很难那之前的秩序的逻辑去套用。这个时候就得建立规划新的秩序。

   如何判断两个矩形相交?

本偏文章会省略数据结构构造的部分,并确认聪明的你已经掌握了如何使用es6 以及相关的OOP编程思维。

这是一个很简单但也有时候会让人一下子陷入复杂计算的问题。

欣仔在计算的时候的判断依据只是:

两个矩形的中心点的 x坐标的距离,小于两个矩形的宽度之和的一半。

同时

两个矩形的中心点的 y坐标的距离,小于两个矩形的高度之和的一半。

两个条件同时满足即判断两个矩形相交。

  1. //比较两个矩形是否有重叠

  2. static checkIsAttach(rc1,rc2){

  3. let p1=rc1.center;

  4. let p2=rc2.center;

  5. let discenterx= Math.abs(p1.x-p2.x);

  6. let discentery= Math.abs(p1.y-p2.y);

  7. if(discenterx<=(rc1.width+rc2.width)/ 2

  8. &&

  9. discentery <= (rc1.height+rc2.height)/ 2)

  10. {

  11. return true ;

  12. }

  13. return false ;

  14. }

这个方法作为一个静态函数写在了一个类里面。

那我们该如何实现布局呢?

欣仔是这么处理的:

  • step1:画面中央任意位置放置一个任意大小的矩形A。

  • step2:画面之外任意的位置出现新建一个矩形。并以第一个矩形A为目标规划路径,并检测在此路径上是否会碰到其他矩形。如果碰到其他矩形,则以碰到的这个矩形为目标移动。直到最终不会再有其他的矩形处在路径上与之碰撞。

  • step3:让新建的矩形以目标无限靠近。

实现step2这一步利用一次递归,因为首先所有的元素都是以第一个元素为中心位置移动的,然后逐步再更新至最新到最终的目标元素。

判断的过程要尽量覆盖当前路径上的所有点,寻找路径上第一个能够碰撞的矩形的方式如下:

   STEP2 CODE  

  1. function findTheFistAttachRect(){

  2.             //newer 为当前新元素

  3. temprect=newer.clone();

  4. let mmx=disx/ 100;

  5. let mmy=disy/ 100;

  6. let num= 100;

  7. while(1 )

  8. {

  9. //break;

  10. temprect.x+=mmx;

  11. temprect.y+=mmy;

  12. let isca= false;

  13. for(var i=targetarr.length-1 ;i>=0;i--)

  14. {

  15. if(Rect .checkIsAttach(targetarr[i],temprect))

  16. {

  17. if(targetarr[i]==newer) continue

  18. target=targetarr[i];//更新目标元素

  19. targetCenter=target.center;

  20. disx=targetCenter.x-newerCenter.x;

  21. disy=targetCenter.y-newerCenter.y;

  22. isca= true;

  23. return target;

  24. break;

  25. }

  26. }

  27. num--;

  28. if(num==0 ) break;

  29. }

  30. return targetarr[0];

  31. }

判断是否路径上没有其他的矩形来碰撞,需连续两次执行这个方法。

然后判断两次执行的结果是否一致,如果一致,则表示路径上已经没有其他矩形了;如果不一致,则表示很有可能路径上还会有其他矩形,代码如下。

  1. while(1)

  2. {

  3. let fp=findTheFistAttachRect();

  4. let fp2=findTheFistAttachRect();

  5. if(fp==fp2) break;

  6. }

为何要有这一步?

欣仔在这一步上也花了相当多的时间啊,因为一开始的时候并没有这步判断,直接就导致了后期的矩形发生了大规模的碰撞。为何?

findTheFistAttachRect()

这个方法会更新目标点,每次目标点的更新就导致了移动路径的变化。路径一旦变化,路径上就有可能会出现其他的矩形进而引发碰撞导致两个矩形的重叠。

第三部处理一个矩形不断靠近另一个矩形,直到零界点碰撞的时候出现便停止,但他们永远也碰不到一起,就像“阿基米德与龟”,即便最终无限接近,但也没有最终阿基米德也没有追上龟一样。

利用循环追加坐标,每次循环追加距离的1/2,

由于矩形是边角的形状,所以这样简单的距离相加,最终还是会存在两个矩形相交的情况,除非两矩形的中心点之间的角度是90度或者0度。

  STEP3 CODE  

  1. //cumn 执行次数

  2. //newer 需要移动的矩形

  3. //target 目标三角形,通常为第一个添加到舞台上的三角形

  4. //disx为两者之间的x坐标距离。disy为两者之间的y坐标的距离。

  5. //newer.clone() 目的为创建一个副本,以免位移判断影响到newer 这个矩形

  6. let cumn= 7;

  7. disx=disx>> 1;

  8. disy=disy>> 1;

  9. while( 1)

  10. {

  11. cumn--;

  12. if(cumn<= 0) break;

  13. temprect=newer.clone();

  14. temprect.x+=disx;

  15. temprect.y+=disy;

  16. //如果两个矩形相交

  17. if( Rect.checkIsAttach(target,temprect))

  18. {//此处可以做优化的处理

  19. }

  20. else{

  21. newer.x+=disx;

  22. newer.y+=disy;

  23. disx=disx>> 1;

  24. disy=disy>> 1;

  25. }

  26. }

以上所有这些代码被写在一个方法里面,这个方法返回一个当前元素在画面上的最终坐标,最终的代码结构大致如下:

  1. class core{

  2. static moveToRect(targetarr,newer){

  3. let target=targetarr[ 0];

  4. let targetCenter=target.center;

  5. let newerCenter=newer.center;

  6. let disx=targetCenter.x-newerCenter.x;

  7. let disy=targetCenter.y-newerCenter.y;

  8. while( 1)

  9. {

  10. let fp=findTheFistAttachRect();

  11. let fp2=findTheFistAttachRect();

  12. if(fp==fp2) break;

  13. }

  14. //找到第一个碰撞的矩形

  15. function findTheFistAttachRect(){

  16. //step2 code

  17. }

  18. //step3 code

  19. return newer;

  20. }

  21. }

  22. export {core}

外部的实现方式如下:

  1. //Rect 为一个矩形类,内部封装了一些矩形的基本属性,以及判断是否相撞的方法

  2. //申明一个x,y,width,height 都随机的Rect,即矩形,构造函数中为x,y 为初始坐标。

  3. let a= Math.random()* Math.PI* 2;

  4. let rect= new Rect(

  5. Math.cos(a)* 500, //x

  6. Math.sin(a)* 800, //y

  7. Math.random()* 30+ 50, //width

  8. Math.random()* 30+ 50); //height

  9. //core.moveToRect 用于获得矩形的最终的坐标

  10. //recttemp 为矩形数据,作为判断是否重叠的依据

  11. core.moveToRect( this.recttemp,rect);

  12. //rect 在moveToRect 方法中得到最新的位置,而后入栈,作为其他新申明矩形的碰撞依据。。。

  13. this .recttemp.push(rect);

结合欣仔之前写过的一篇文章,VUE的动画~

欣仔已为导出视频。

播放

当前案例已经更新到我的github上去了,欢迎大家fork,并提供宝贵的意见~

github地址:

https://github.com/shininter/putyourphotos

 

欣仔活动,互动世界~

戳我戳我