13 July 2010

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

CSS
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. .scope_container .sc div {
  14. overflow:hidden}
  15. .scope_container .rl {
  16. height:110%;
  17. width:60%;
  18. -webkit-transform: rotate(-15deg);
  19. -moz-transform: rotate(-15deg);
  20. -o-transform: rotate(-15deg);
  21. transform: rotate(-15deg);
  22. position:relative;
  23. top:1.5%;
  24. left:4.5%;
  25. }
  26. .scope_container .rr {
  27. height:100%;
  28. width:100%;
  29. -webkit-transform: rotate(30deg);
  30. -moz-transform: rotate(30deg);
  31. -o-transform: rotate(30deg);
  32. transform: rotate(30deg);
  33. position:relative;
  34. top:7%;
  35. left:51%;
  36. }
  37. .scope_container .sv {
  38. width:105%;
  39. height:105%;
  40. -webkit-transform: rotate(-15deg);
  41. -moz-transform: rotate(-15deg);
  42. -o-transform: rotate(-15deg);
  43. transform: rotate(-15deg);
  44. position:relative;
  45. top:-2%;
  46. left:-29%;
  47. }


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

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

  1. <div class="parent">
  2. <div class="scope_container pattern">
  3. <div class="sc s1"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  4. <div class="sc s2"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  5. <div class="sc s3"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  6. <div class="sc s4"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  7. <div class="sc s5"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  8. <div class="sc s6"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  9. <div class="sc s7"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  10. <div class="sc s8"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  11. <div class="sc s9"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  12. <div class="sc s10"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  13. <div class="sc s11"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  14. <div class="sc s12"><div class="rl"><div class="rr"><div class="sv"></div></div></div></div>
  15. </div>
  16. </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. if(document.getElementsByClassName) {
  4. getElementsByClass = function(classList, node) {
  5. return (node || document).getElementsByClassName(classList)
  6. }
  7. } else {
  8. getElementsByClass = function(classList, node) {
  9. var node = node || document,
  10. list = node.getElementsByTagName('*'),
  11. length = list.length,
  12. classArray = classList.split(/\s+/),
  13. classes = classArray.length,
  14. result = [], i,j
  15. for(i = 0; i < length; i++) {
  16. for(j = 0; j < classes; j++) {
  17. if(list[i].className.search('\\b' + classArray[j] + '\\b') != -1) {
  18. result.push(list[i])
  19. break
  20. }
  21. }
  22. }
  23. return result
  24. }
  25. }
  26. //Получаем координаты мыши
  27. function mousePageXY(e)
  28. {
  29. var x = 0, y = 0;
  30. if (!e) e = window.event;
  31. if (e.pageX || e.pageY)
  32. {
  33. x = e.pageX;
  34. y = e.pageY;
  35. }
  36. else if (e.clientX || e.clientY)
  37. {
  38. x = e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft) - document.documentElement.clientLeft;
  39. y = e.clientY + (document.documentElement.scrollTop || document.body.scrollTop) - document.documentElement.clientTop;
  40. }
  41. return {"x":x, "y":y};
  42. }
  43. window.onload = function() {
  44. var scope_cont = getElementsByClass('scope_container', document);
  45. //Калейдоскопов может быть несколько, учитываем это.
  46. for (i=0;i<scope_cont.length;i++)
  47. {
  48. scope_cont[i].onmouseover = function() {
  49. var sect = getElementsByClass('sv', this);
  50. var curscope = this;
  51. this.onmousemove = function(e){
  52. var mCur = mousePageXY(e);
  53. for (n=0;n<sect.length;n++)
  54. {
  55. //У четных секторов фон двигается в одну сторону
  56. if (n%2) {
  57. sect[n].style.backgroundPosition = mCur.x + 'px ' + mCur.y + 'px';
  58. }
  59. //У нечетных — в другую
  60. else {
  61. sect[n].style.backgroundPosition = mCur.x*(-1) + 'px ' + mCur.y + 'px'
  62. }
  63. }
  64. }
  65. }
  66. scope_cont[i].onmouseout = function() {
  67. //Убираем за собой, чтоб не перегружать браузер
  68. document.onmousemove = null;
  69. }
  70. }
  71. }
  72. </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:cssjsfilterкалейдоскоп
Hubs: CSS
+94
4.1k 78
Comments 102
Popular right now