company_banner

Запускаем фрактальные снежинки на HTML5 Canvas

    Предновогоднее развлечение на HTML5 Canvas по украшению сайта снежинками (ну и просто интресный пример посмотреть, как работает Canvas).

    В своем рассказе я буду отталкиваться от кода Giorgio Sardo, который в свою очередь базируется на коде David Flanagan.



    Все, что описано ниже, вы можете попробовать непосредственно здесь, на Хабре в любом современном браузере со средствами разработки, просто запустив консоль JavaScript. В IE9 достаточно нажать F12 и, если вы хотите тестировать прямо на этой странице, не забудьте перевести браузер в режим Internet Explorer 9 Standards (Alt + 9), т.к. по умолчанию Хабр требует режима IE8.

    Проверка поддержки Canvas



    Прежде всего, надо начать с того, что нужно убедиться, что браузер поддерживает Canvas, для этого нужно создать элемент Canvas и попробовать добраться до контекста работы:

    if (document.createElement('canvas').getContext) {
      ...
    }
    else {
      ...
    }


    В первом случае можно двигаться дальше и запускать снежинки.

    Создаем холст



    Для отрисовки снежинок мы создатим холст (Canvas) на весь экран:

    var canvas = document.createElement('canvas');
    canvas.style.position = 'fixed';
    canvas.style.top = '0px';
    canvas.style.left = '0px';
    canvas.style.zIndex = '-10';
    canvas.width = document.body.offsetWidth;
    canvas.height = window.innerHeight;

    document.body.insertBefore(canvas, document.body.firstChild);


    В данном случае мы создаем новый элемент Canvas и задаем ему фиксированное расположение, старясь разместить его так, чтобы он не мещал остальным элементам.

    Далее мы получаем контекст для отрисовки:

    var sky = canvas.getContext('2d');


    Снежинка Коха



    Думаю, хабраюзерам, должно быть хорошо известно, что такое Снежинка Коха, поэтому ограничусь картинкой:



    Штука эта фрактальная и удобно рисуется рекурсивно. Чтобы отрисовать треугольник, нужно к каждому из его ребер применить последовательно один и тот же паттерн отрисовки:



    Давайте начнем с того, что попробуем отрисовать одну линию. При отрисовке, что удобно, мы можем применять трансформации (масштабирование и повороты), при которых каждая локальная отрисовка будет выглядеть как отрисовка прямой горизонтальной линии. То есть мы масштабируем и поворачиваем контекст (меняем матрицу трансформации) вместо поворота отрисовки линии.

    Для сохранения и восстановления состояния матрицы трансформации используются соответственно функции save() и restore().

    По ходу работы нам понадобится конвертировать градусы в радианы (хотя при желании можно и сразу в радианах писать):

    var deg = Math.PI / 180;


    Рекурсивная функция для отрисовки одного ребра выглядит так:

    function leg(n, len) {
      sky.save();       // Сохраняем текущую трансформацию
      if (n == 0) {      // Нерекурсивный случай - отрисовываем линию
        sky.lineTo(len, 0);    }
      else {
        sky.scale(1 / 3, 1 / 3);  // Уменьшаем масштаб в 3 раза
            leg(n – 1, len);             sky.rotate(60 * deg);
            leg(n – 1, len);              sky.rotate(-120 * deg);
            leg(n – 1, len);              sky.rotate(60 * deg);          leg(n – 1, len);        }
      sky.restore();      // Восстанавливаем трансформацию
      sky.translate(len, 0); // переходим в конец ребра
    }


    Для запуска отрисовки можно использовать такую функцию:

    function drawFlake(x, y, len, n, stroke, fill) {
      sky.save();    sky.strokeStyle = stroke;
      sky.fillStyle = fill;
      sky.beginPath();
      sky.translate(x, y);
      sky.moveTo(0, 0);    leg(n, len);         sky.closePath();
      sky.fill();
      sky.stroke();
      sky.restore();
    }


    Обратите внимание, что для отрисовки нужно запустить создание пути, если нужно, закрыть его и только потом сказать, что нужно сделать закраску областей и прорисовку линий. Результат:



    Если добавить еще несколько ребер с соответствующими поворотами, получим снежинку:

    function drawFlake(x, y, len, n, stroke, fill) {
      sky.save();        sky.strokeStyle = stroke;
      sky.fillStyle = fill;
      sky.beginPath();
      sky.translate(x, y);
      sky.moveTo(0, 0);    leg(n, len);         sky.rotate(-120 * deg);
      leg(n, len);         sky.rotate(-120 * deg);
      leg(n, len);
             sky.closePath();
      sky.fill();
      sky.stroke();
      sky.restore();
    }


    Результат:



    Создание и перемещение снежинок



    Дальше идея довольно прозрачная: 1) создаем пул снежинок, повесив добавление снежинок на таймер, 2) по таймеру меняем положение снежинок и делаем отрисовку.

    Добавление снежинок


    Дополнительная функция для случайных значений, массив снежинок и максимальное количество. Задаем таймер:

    var rand = function (n) { return Math.floor(n * Math.random()); }
    var flakes = [];       var maxflakes = 20;
    var snowspeed = 500;
    var snowingTimer = setInterval(createSnowflake, snowspeed);


    И собственно само создание снежинок (в нужный момент создание новых снежинок останавливается отчисткой таймера):

    function createSnowflake() {
      var order = 3;
      var size = 10 + rand(50);
      var x = rand(document.body.offsetWidth);
      var y = window.pageYOffset;

      flakes.push({ x: x, y: y, vx: 0, vy: 3 + rand(3), size: size, order: order, stroke: "#99f", fill: "transparent" });
      if (flakes.length > maxflakes) clearInterval(snowingTimer);
    }


    Перемещение снежинок


    Тут появляется дополнительная переменная invalidateMeasure, которая устанавливается в true при изменении размера экрана. Ставим таймер на обновление положения и собственно функция перемещения (очищаем экран, обновляем положение –> рисуем снежинки).

    var scrollspeed = 64;
    setInterval(moveSnowflakes, scrollspeed);

    function moveSnowflakes() {
      sky.clearRect(0, 0, canvas.width, canvas.height);

      var maxy = canvas.height;

      for (var i = 0; i < flakes.length; i++) {
       var flake = flakes[i];
       flake.y += flake.vy;
       flake.x += flake.vx;

       if (flake.y > maxy) flake.y = 0;
       if (invalidateMeasure) {
         flake.x = rand(canvas.width);
       }

       drawFlake(flake.x, flake.y, flake.size, flake.order, flake.stroke, flake.fill);

       // Иногда меняем боковой ветер
       if (rand(4) == 1) flake.vx += (rand(11) - 5) / 10;
       if (flake.vx > 2) flake.vx = 2;
       if (flake.vx < -2) flake.vx = -2;
      }
      if (invalidateMeasure) invalidateMeasure = false;
    }


    Финальный код



    Дальше можно добавить еще несколько дополнительных деталей: случайный поворот снежинки и случайный цвет снежинки + детализация в зависимости от размера:

    (function () {
      if (document.createElement('canvas').getContext) {
        if (document.readyState === 'complete')
          snow();
        else
          window.addEventListener('DOMContentLoaded', snow, false);
      }
      else {
      return;
      }

      var deg = Math.PI / 180;
      var maxflakes = 20;     var flakes = [];         var scrollspeed = 64;    var snowspeed = 500;  
      var canvas, sky;
      var snowingTimer;
      var invalidateMeasure = false;

         var strokes = ["#6cf", "#9cf", "#99f", "#ccf", "#66f", "#3cf"];

      function rand (n) {
        return Math.floor(n * Math.random());
      }

      // Запуск снегопада
      function snow() {
        canvas = document.createElement('canvas');
        canvas.style.position = 'fixed';
        canvas.style.top = '0px';
        canvas.style.left = '0px';
        canvas.style.zIndex = '-10';

        document.body.insertBefore(canvas, document.body.firstChild);
        sky = canvas.getContext('2d');

        ResetCanvas();

        snowingTimer = setInterval(createSnowflake, snowspeed);
        setInterval(moveSnowflakes, scrollspeed);
        window.addEventListener('resize', ResetCanvas, false);
      }

      // Сброс размеров Canvas
      function ResetCanvas() {
        invalidateMeasure = true;
        canvas.width = document.body.offsetWidth;
        canvas.height = window.innerHeight;
      }

      // Отрисовка кривой Коха
      function leg(n, len) {
        sky.save();       // Сохраняем текущую трансформацию
        if (n == 0) {      // Нерекурсивный случай - отрисовываем линию
          sky.lineTo(len, 0);      }
        else {
          sky.scale(1 / 3, 1 / 3);  // Уменьшаем масштаб в 3 раза
            leg(n - 1, len);            sky.rotate(60 * deg);
            leg(n - 1, len);              sky.rotate(-120 * deg);
            leg(n - 1, len);              sky.rotate(60 * deg);          leg(n - 1, len);          }
        sky.restore();      // Восстанавливаем трансформацию
        sky.translate(len, 0); // переходим в конец ребра
      }

      // Отрисовка снежинки Коха
      function drawFlake(x, y, angle, len, n, stroke, fill) {
        sky.save();      sky.strokeStyle = stroke;
        sky.fillStyle = fill;
        sky.beginPath();
        sky.translate(x, y);
        sky.moveTo(0, 0);      sky.rotate(angle);
        leg(n, len);
        sky.rotate(-120 * deg);
        leg(n, len);           sky.rotate(-120 * deg);
        leg(n, len);           sky.closePath();
        sky.fill();
        sky.stroke();
        sky.restore();
      }

      // Создание пула снежинок
      function createSnowflake() {
        var order = 2+rand(2);
        var size = 10*order+rand(10);
        var x = rand(document.body.offsetWidth);
        var y = window.pageYOffset;
        var stroke = strokes[rand(strokes.length)];

        flakes.push({ x: x, y: y, vx: 0, vy: 3 + rand(3), angle:0, size: size, order: order, stroke: stroke, fill: 'transparent' });

        if (flakes.length > maxflakes) clearInterval(snowingTimer);
      }

      // Перемещение снежинок
      function moveSnowflakes() {
        sky.clearRect(0, 0, canvas.width, canvas.height);

        var maxy = canvas.height;

        for (var i = 0; i < flakes.length; i++) {
         var flake = flakes[i];

         flake.y += flake.vy;
         flake.x += flake.vx;

         if (flake.y > maxy) flake.y = 0;
         if (invalidateMeasure) {
           flake.x = rand(canvas.width);
         }

         drawFlake(flake.x, flake.y, flake.angle, flake.size, flake.order, flake.stroke, flake.fill);

         // Иногда меняем боковой ветер
         if (rand(4) == 1) flake.vx += (rand(11) - 5) / 10;
         if (flake.vx > 2) flake.vx = 2;
         if (flake.vx < -2) flake.vx = -2;
         if (rand(3) == 1) flake.angle = (rand(13) - 6) / 271;
        }
        if (invalidateMeasure) invalidateMeasure = false;
      }
    } ());


    * This source code was highlighted with Source Code Highlighter.


    Копируйте код, запускайте из консоли и получайте снегопад на сайте.
    Microsoft
    280,48
    Microsoft — мировой лидер в области ПО и ИТ-услуг
    Поделиться публикацией

    Комментарии 55

    • НЛО прилетело и опубликовало эту надпись здесь
        +7
        так напомнило сайты 1996 го года с зачатками JS и поддержкой прозрачных GIF
        • НЛО прилетело и опубликовало эту надпись здесь
          • НЛО прилетело и опубликовало эту надпись здесь
              +1
              Да да и еще флешовые часики — оч модна было.
            +13
            Может быть, вы еще и в Деда Мороза не верите? :)

            Это просто интересный пример, а не призыв украшать сайты елочками и падающими снежинками ;) Что касается древности, то лично у меня такое ощущение, что в ближайшие год-два html5 c canvas, svg и javascript предстоит быстрыми темпами пройти путь от таких простых поделок до чего-то более мощного (см. например, предыдущий пост).

            Поэтому мы еще неоднократно увидим примеры с генерацией графики в различных вариациях. Лет 5-7 назад было много примеров по генерации плазмы, огня и прочих эффектов на flash, а еще сильно раньше различные такие эффекты писали на native code (c/c++) и работали с видео-карточками через прерывания. «HTML5» предстоит быстро пройти аналогичный путь.
              0
              А вот, кстати, те эффекты, про которые вы говорите.

              Нужен webGL (у меня работает с Chome) + аппаратная графика.

              Кстати, подобные эффекты в HTML5 пока тормозят из-за очень долгой работы canvas.putImageData в некоторых браузерах.
              +8
              вы что?! даже у гугла снежинки — bit.ly/eZ2KEQ
              • НЛО прилетело и опубликовало эту надпись здесь
              +5
              Это программистский прикол, не всем понятно.
                0
                Имхо в createSnowflake надо вместо
                var y = window.pageYOffset;
                написать
                var y = window.pageYOffset — rand(window.innerHeight);
                А то сразу после старта они как-то неравномерно падают…
                  +1
                  Можно увеличить таймер на запуск новых снежинок.
                  0
                  Пожалуйста, ненадо.
                    0
                    Снежинки — не сильно эффектный пример…
                      0
                      А что было бы интересно вам?
                        0
                        Например, статья о создании векторного редактора на HTML5. Это было бы куда более полезней…
                          +1
                          Добавить туда самолетик, который будет сбивать снежинки =)
                        0
                        [offtop]Вкладки IE ужасны.[/offtop]
                          –2
                          кому как
                          0
                          а это так задумано?
                            0
                            снежинки кстати и под эту картинку залетают и под форму ввода
                              0
                              Да, я в коде прописал canvas.style.zIndex = '-10'; соответственно canvas располагается внизу, иначе он будет перекрывать все другие элементы (и события на них).
                                0
                                Неужели с этим нельзя ничего сделать? :)
                                  0
                                  В данной реализации это аналогично тому, что вы бы поверх всей страницы разместили обычную картинку или flash.
                                    0
                                    Обидно.

                                    Эффект красивые, не вижу причин для всеобщего возмущения, который наблюдается :)

                                    Есть досадный баг с определением загрузки в первом вложенном условии, по крайней мере в FF и Crome, при загрузке скрипта через
                                    <script type="text/javascript" async="true" src="http://static.example.com/js/fractal_snow.js"></script>

                                    Попробую пофиксить :)
                                      +2
                                      Итак, в случае использования загрузки скриптов с помощью async=«true» приходится использовать событие «load», так как DOMContentLoaded уже успевает пройти.

                                      В начало функции snow() добавляем:
                                          window.removeEventListener('DOMContentLoaded', snow, false);
                                          window.removeEventListener('load', snow, false);


                                      А условие в начале скрипта заменяем на
                                        if (document.createElement('canvas').getContext) {
                                          if (document.readyState === 'complete') {
                                            snow();
                                          } else {
                                            window.addEventListener('DOMContentLoaded', snow, false);
                                            window.addEventListener('load', snow, false);
                                          }
                                        } else {
                                          return;
                                        }
                                      0
                                      почему бы не запускать много маленьких канвасиков?
                                        0
                                        Можно и так :)
                                          0
                                          на самом деле, подозреваю, это единственный правильный вариант в данном случае. но да, рисовать надо скриптом.
                                0
                                Да, z-index: -10;
                                +5
                                Это такой завуалированный подарок энергетикам на новый год?
                                  +7
                                  Вы про 100%-ю загрузку процессора? :)
                                    +4
                                    Про нее самую.
                                  +2
                                  Раньше дети в садах снежинки из салфеток вырезали и наклеивали иногда на окна…

                                  Прошло немного лет.
                                  Теперь они выросли, и стали делать снежинки из кода… в блоге конторы, которая делает ОС «окна»
                                    +1
                                    Chromium 10.0.613.0 (69331) на Ubuntu 10.10 падает при попытке выполнить код.
                                      +2
                                      Подозреваю, что это баг. Браузер не должен падать при выполнении js-кода, в крайнем случае должна выдаваться ошибка кода, либо если проблема с производительностью, браузер должен предлагать остановить код/убить вкладку.
                                        +1
                                        Выпадает только один процесс — в котором выполняется эта вкладка. Возможно, падение связано с тем, что я выполняю код из консоли — яхз. Вот скриншот.
                                          +1
                                          Думаю, самое правильное: отправить в Google баг. Дополнительно можно попытаться локализовать в коде, что именно вызывает ошибку.
                                            0
                                            У вас в последней строке ошибка со скобками, может быть из-за неё?
                                            --- } ()); 
                                            +++ })(); 

                                              0
                                              Если выкинуть все содержимое, будет вот так:

                                              (function () {
                                              } ());
                                                0
                                                Вы правы, не знал что так можно.
                                        0
                                        Chrome 8.0.552.224 beta на openSUSE 11.3 летит нормально :)
                                        +2
                                        С наступающими!
                                          +3
                                          я так понимаю начинается предновогодняя тематика…
                                          Авторы — пишите! Чем больше напишете — тем больше шанс что это сможет повторить большее количество пытающихся))
                                          фрактальная снежинка — рулез ( хотя я бы предпочел скрещивающиеся мечи или морской штурвал )
                                          Народ — не скупитесь на творчество и проявите проф снисходительность — праздник же!
                                            0
                                            Если долго смотреть на снежинки их становится все меньше. Походу тут нет проверки за выход их вправо-влево, и со временем они все уходят и не возвращаются.

                                            Если после этого обновить размеры экрана — они все снова вываливаются как тут и были.

                                            Предлагаю добавить как-то так:
                                            if (flake.x > canvas.width+flake.size || flake.x < -flake.size) {
                                                flake.y = 0;
                                                flake.x = rand(canvas.width);
                                            }
                                            
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                                +1
                                                ctrl+shift+i
                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                +2
                                                А у yahoo.com до сих пор снежинки на логотипе флешем отрисовываются)
                                                  +2
                                                  Ждём массовой миграции сайтов со снежинками на народе и укозе на HTML5
                                                    +2
                                                    Со времен плавающих рыбок, мой кот не занимал компьютера на такое долгое время.
                                                      0
                                                      Хех, как запустить в Chrome, что разобраться не могу…
                                                        0
                                                        ctrl+shift+i, дальше консоль
                                                        –1
                                                        Нененененененененененененененененененеееееееет!!! Не надо! Убейте их!!! Сукка… Лихие 90-ые возвращаются!
                                                          0
                                                          а мне например интересен чисто код,
                                                          для повышения скилла

                                                          да и фракталы люблю, как идею сделать красивой зависит уже от вас самих.

                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                          Самое читаемое