Pull to refresh

Делаем калейдоскоп на CSS+JS

Reading time 9 min
Views 4.9K
imageЯ как-то раньше никогда не задумывался над такой штукой, как калейдоскоп на странице. Видел их как-то раньше, но не обращал особо внимания. А тут увидел у Лебедева в портфолио калейдоскоп на флеше, покрутил по нему мышкой, понял принцип работы и подумал «ёлки, это же не сложно!».

Конечно, нельзя сказать, что это действительно очень просто. И нельзя сказать, что получилось совсем всё, что хотелось. Но есть на что посмотреть, за чем приглашаю под кат.

UPD
В посте добавил улучшенный вариант от хабрачеловека hlomzik, который не работает в ИЕ, но с меньшим кол-вом кода и с более правильным поведением.


В самом начале, я надеялся, что он будет работать совсем везде и хорошо. Не совсем получилось. В результате:
  • Firefox / Safari / Chrome — Работает идеально (кто бы сомневался).
  • IE8 — работает, но только если калейдоскоп небольшой (до 300х300). Если больше — начинает серьезно тормозить.
  • IE7 — будет работать, если добавить для него отдельный CSS в котором поправить позиционирование блоков.
  • Opera — ничерта не работает. Это отдельная песня. Сразу обращаюсь к ребятам из Оперы, которые это прочтут — посмотрите на демо и оформите это как багрепорт. В «самом быстром браузере на земле»© CSS-transform тормозит на столько безбожно, что это не лезет ни в какие ворота. И опять оперная вечная проблема — полная лажа при использовании overflow:hidden и не static-позиционированных элементах. СДЕЛАЙТЕ С ЭТИМ ЧТО-ТО!


Сразу дам ссылку на рабочий пример, чтоб было понятно, о чем вообще речь: kaleidoscope.terion.name
Итак, поехали.

Задача 1: понять, как вообще это сделать.


Калейдоскоп состоит из 12 секторов, собранных в диск. Соответственно возникает вопрос: как сделать сектор? На самом деле, это очень просто. Нам понадобится 4 блока и немного CSS. Выглядеть это должно так:
image
Блок-контейнер, один блок повернутый на -15°, внутри блок повернутый на 30°, внутри еще один повернутый на -15°.
У сектора установлена фоновая картинка, которую мы будем смещать.

Задача 2: собственно, собрать это все.


Код, соответственно очень простой:
<div class="sc s1">
  <div class="rl">
   <div class="rr">
     <div class="sv">
     </div>
   </div>
  </div>
</div>


Да, мы сразу предполагаем, что на странице может быть несколько калейдоскопов, поэтому используем только классы.
Важное условие: размер калейдоскопа должен меняться путем изменения размеров только контейнера, соответственно внутри всё должно быть в процентах.
Сразу отмечу, что значения в процентах были подобраны методом научного тыка, т.к. мои знания тригонометрии не столь глубоки, чтобы рассчитать всё это :)

  1. .scope_container .sc {
  2.   width:50%;
  3.   height:50%;
  4.   -webkit-transform-origin: top center;
  5.   -moz-transform-origin: top center;
  6.   -o-transform-origin:top center;
  7.   transform-origin:top center;
  8.   position:absolute;
  9.   top:50%;
  10.   left:25%;
  11.   z-index:-1;
  12. }
  13.  
  14. .scope_container .sc div {
  15.   overflow:hidden}
  16.  
  17. .scope_container .rl {
  18.   height:110%;
  19.   width:60%;
  20.   -webkit-transform: rotate(-15deg);
  21.   -moz-transform: rotate(-15deg);
  22.   -o-transform: rotate(-15deg);
  23.   transform: rotate(-15deg);
  24.   position:relative;
  25.   top:1.5%;
  26.   left:4.5%;
  27.   
  28. }
  29.  
  30. .scope_container .rr {
  31.   height:100%;
  32.   width:100%;
  33.   -webkit-transform: rotate(30deg);
  34.   -moz-transform: rotate(30deg);
  35.   -o-transform: rotate(30deg);
  36.   transform: rotate(30deg);
  37.   position:relative;
  38.   top:7%;
  39.   left:51%;
  40. }
  41.  
  42. .scope_container .sv {
  43.   width:105%;
  44.   height:105%;
  45.   -webkit-transform: rotate(-15deg);
  46.   -moz-transform: rotate(-15deg);
  47.   -o-transform: rotate(-15deg);
  48.   transform: rotate(-15deg);
  49.   position:relative;
  50.   top:-2%;
  51.   left:-29%;
  52. }


Итак, у нас есть код для сектора. Теперь этих секторов должно быть 12 и ни должны быть завернуты в 2 контейнера, т.к. внутренний используется для позиционирования элементов, а внешний — для задания размеров и обрезки лишнего.

В итоге весь хтмл-код будет таким:

  1. <div class="parent">
  2. <div class="scope_container pattern">
  3.  
  4.  <div class="sc s1"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  5.  <div class="sc s2"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  6.  <div class="sc s3"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  7.  <div class="sc s4"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  8.  <div class="sc s5"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  9.  <div class="sc s6"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  10.  <div class="sc s7"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  11.  <div class="sc s8"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  12.  <div class="sc s9"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  13.  <div class="sc s10"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  14.  <div class="sc s11"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  15.  <div class="sc s12"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  16.  
  17. </div>
  18. </div>


Классы s1 — s12 для определения положения каждого сектора.
Класс pattern определяет, какая картинка будет использована (спасибо eto_moy_nick за паттерн :) )

Расставляем блоки по местам:
  1. .scope_container .s1 {
  2.   -webkit-transform: rotate(0deg);
  3.   -moz-transform: rotate(0deg);
  4.   -o-transform: rotate(0deg);
  5.   transform: rotate(0deg);
  6. }
  7. .scope_container .s2 {
  8.   -webkit-transform: rotate(30deg);
  9.   -moz-transform: rotate(30deg);
  10.   -o-transform: rotate(30deg);
  11.   transform: rotate(30deg);
  12. }
И так до 12го с шагом в 30 градусов.

В общем-то и всё.
Но ведь есть ИЕ, который всего этого не понимает! Но для ИЕ есть фильтры filter: progid:DXImageTransform.Microsoft.Matrix(...).
Поэтому, в условный комментарий [if IE 8] добавляем следующий код:

  1. .scope_container .s1 {
  2.   -webkit-transform: rotate(0deg);
  3.   -moz-transform: rotate(0deg);
  4.   -o-transform: rotate(0deg);
  5.   transform: rotate(0deg);
  6. }
  7. .scope_container .s2 {
  8.   -webkit-transform: rotate(30deg);
  9.   -moz-transform: rotate(30deg);
  10.   -o-transform: rotate(30deg);
  11.   transform: rotate(30deg);
  12. }
Как показали опыты, с абсолютным позиционированием и матрицей у ИЕ беда. Поэтому пришлось такие костыли городить.

Далее:
  1. .s2 {
  2.   filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.86602540, M12=-0.50000000, M21=0.50000000, M22=0.86602540,SizingMethod='auto expand',FilterType='nearest neighbor');
  3.   margin:-64.5% 0 0 -25%;
  4.   }
  5. .s3 {
  6.   filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.50000000, M12=-0.86602540, M21=0.86602540, M22=0.50000000,SizingMethod='auto expand',FilterType='nearest neighbor');
  7.   margin:-60.5% 0 0 -32.8%;
  8.   }
И т.п.
К моему удивлению, FilterType='nearest neighbor' не принес результата (без бикубического сглаживания, скорее всего, не было бы таких тормозов).
Для расчетов матрицы я пользовался вот этим инструментом, просто спасшим мне жизнь: www.boogdesign.com/examples/transforms/matrix-calculator.html
Маржинами были вручную расставлены блоки. Если есть желание заставить это все работать в ИЕ7 — нужно лишь для него переписать эти самые маржины.

Задача 3: заставить это двигаться


Javascript. Нужно двигать фон секторов за мышкой. При этом мы помним, что калейдоскоп на странице не один. Код ниже, читаем комментарии:
  1. <script type="text/javascript">
  2.  
  3. //Мы используем классы, но ИЕ не умеет их выбирать. Компенсируем этот недостаток.
  4. if(document.getElementsByClassName) {
  5.   
  6.     getElementsByClass = function(classList, node) { 
  7.       return (node || document).getElementsByClassName(classList)
  8.     }
  9.   
  10.   } else {
  11.   
  12.     getElementsByClass = function(classList, node) {     
  13.       var node = node || document,
  14.       list = node.getElementsByTagName('*'),
  15.       length = list.length,
  16.       classArray = classList.split(/\s+/),
  17.       classes = classArray.length,
  18.       result = [], i,j
  19.       for(i = 0; i < length; i++) {
  20.         for(j = 0; j < classes; j++) {
  21.           if(list[i].className.search('\\b' + classArray[j] + '\\b') != -1) {
  22.             result.push(list[i])
  23.             break
  24.           }
  25.         }
  26.       }
  27.     
  28.       return result
  29.     }
  30.   }
  31.  
  32.  
  33. //Получаем координаты мыши
  34. function mousePageXY(e)
  35. {
  36.  var x = 0, y = 0;
  37.  
  38.  if (!e) e = window.event;
  39.  
  40.  if (e.pageX || e.pageY)
  41.  {
  42.   x = e.pageX;
  43.   y = e.pageY;
  44.  }
  45.  else if (e.clientX || e.clientY)
  46.  {
  47.   x = e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft) - document.documentElement.clientLeft;
  48.   y = e.clientY + (document.documentElement.scrollTop || document.body.scrollTop) - document.documentElement.clientTop;
  49.  }
  50.  
  51.  return {"x":x, "y":y};
  52. }
  53.  
  54.  
  55. window.onload = function() {
  56.  
  57. var scope_cont = getElementsByClass('scope_container', document);
  58.  
  59. //Калейдоскопов может быть несколько, учитываем это.
  60. for (i=0;i<scope_cont.length;i++)
  61. {
  62.   scope_cont[i].onmouseover = function() {
  63.       var sect = getElementsByClass('sv', this);
  64.       var curscope = this;
  65.       
  66.       this.onmousemove = function(e){
  67.         var mCur = mousePageXY(e);
  68.           for (n=0;n<sect.length;n++)
  69.           {
  70.             //У четных секторов фон двигается в одну сторону
  71.             if (n%2) {
  72.               sect[n].style.backgroundPosition = mCur.x + 'px ' + mCur.y + 'px';
  73.             }
  74.             //У нечетных — в другую
  75.             else {
  76.               sect[n].style.backgroundPosition = mCur.x*(-1) + 'px ' + mCur.y + 'px'
  77.             }
  78.           }
  79.           
  80.         }
  81.     }
  82.     scope_cont[i].onmouseout = function() {
  83.       //Убираем за собой, чтоб не перегружать браузер
  84.       document.onmousemove = null;
  85.     }
  86. }
  87. }
  88. </script>


В общем-то, готово.
Opera:
Если блоку .pattern добавить overflow:hidden — пропадает ВСЁ. В итоге пустая страница со скроллами в пустоту на ширину «веера».
Если overflow:hidden убрать, то в «самом быстром браузере на земле»© эта конструкция работает медленнее, чем в ИЕ.
Это полный провал.

Надеюсь, вам было интересно :)

UPD
А вот и улучшенный вариант от хабрачеловека hlomzik:
quaint.su/for/habrahabr/kaleidoscope
Обсуждение, в котором это родилось, вот: habrahabr.ru/blogs/css/99019/?reply_to=3057019#comment_3054307
Tags:
Hubs:
+94
Comments 102
Comments Comments 102

Articles