в Разработки

Данная статья является копией публикации на хабре

Однажды автор этого поста работал над одним заказом по разработке простенько сайта и тогда появилась идея — придать всем страницам некой уникальности и запоминаемости — использовать уникальные фоновые текстуры или элементы дизайна (активно использовался parallax-scrolling). Так как в тот момент дедлайн был довольно близок, а идея — в зачаточном состоянии, было реализовано намного проще — простыми заготовками, но идея выброшена не была.

Спустя некоторое время случайно наткнулся на мертвую ссылку, которая вела на несуществующий Tumblr-блог, и страница ошибки сразу привлекла внимание. Обновив страничку фоновое изображение (в виде gif-анимации) сменилось — внимание ещё более усилилось. Почитав исходники стало понятно что все изображения «прописаны» статично, но это натолкнуло на другую идею, о которой вы узнаете под катом.

Идея заключалась в следующем: «Почему бы нам в случае, когда необходимо оформить какую-либо страницу (в частности сервисную — вход, выход, ошибка), или просто получить тематическое изображение для оформления контента, не использовать псевдо-случайные изображения?» (возможно их использовать как элементы дизайна сайта).
Семантически под «псевдо-случайными» я имею в виду изображения определенной тематики (или имеющие между собой какие-либо общие черты), но с течением времени результат «выпадения» был бы в той или иной степени уникальным.

Возможные методы решения:

  • Парсинг результатов поиска (google, yandex) по картинкам;
  • Парсинг хостингов картинок, имеющие деление изображений по тегам или критериям;
  • Инстаграм и сервисы иже с ним;
  • Использовать средства блог-платформ, имеющих акцент на фото-контент.

Парсинг результатов поисковых запросов отпал по причинам встречающейся низкой релевантности, большого количества «мусора», а сами изображения хранятся черт знает где. Хостинг картинок — как-то не сложилось (может быть и зря) сразу. Инстаграм — низкое качество изображений (640х640 точек) и сложность в запросах для получения релевантных ответов. Так и остался крайний вариант — блог-платформы.

Не скажу что выбор был мучительный, так как сам на Tumblr веду пару блогов и в курсе относительно статистики. В том числе — статистики постов:

image

Плюсы данного решения:

  • Изображения в тематических блогах придерживаются своего концепта в 9 из 10 случаев;
  • При наличии корпоративного или личного блога на этом же сервисе изображения можно брать прямо из него, получается довольно прикольно;
  • Нет необходимости беспокоиться об актуальности;
  • Изображения находятся в открытом доступе;
  • Tumblr отлично дружит с ifttt.

Минусы:

  • Если брать контент не у блога с устоявшимся форматом, есть вероятность получить изображение лысого мужика в наколках не соответствующее формату;

Теперь остается дело за малым — получить сами картинки. Хочется отдельно выразить благодарность разработчикам этой платформы, так как апи для получения и выборки контента очень прост и качественно реализован. Работу по получению и разбору данных было решено возложить на клиента (что без каких-либо сложностей переписывается на любой серверный язык). В итоге у меня получился следующий пример:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="description" content="404 | Page Not Found" />
    <title>404 | Page Not Found</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <link rel="shortcut icon" href="./blank-favicon.ico" />
    <link href="//fonts.googleapis.com/css?family=PT+Sans+Narrow&amp;subset=latin,cyrillic" rel="stylesheet" type="text/css" />
    <style type="text/css">
      *{margin:0;padding:0}
      html,body{min-height:100%;height:100%;min-width:100%;background-color:#000;overflow:hidden}
      body{position:fixed;font-family:'PT Sans Narrow',Helvetica,Arial,Verdana,sans-serif;visibility:visible;top:0;right:0;left:0;-webkit-font-smoothing:antialiased}
      #bg-fullscreen{position:absolute;-moz-opacity:0;opacity:0;top:0;left:0;width:100%;height:100%;background-size:cover;background-position:50% 50%;-webkit-transition:opacity 2s ease-in-out;-moz-transition:opacity 2s ease-in-out;-ms-transition:opacity 2s ease-in-out;-o-transition:opacity 2s ease-in-out;transition:opacity 2s ease-in-out;-webkit-filter:blur(3px);-moz-filter:blur(3px);-o-filter:blur(3px);-ms-filter:blur(3px);filter:blur(3px)}
      #bg-fullscreen.show{-moz-opacity:.9;opacity:.9}
      #content{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}
      #content *{color:#fff}
      #content h1{font-size:20em;text-shadow:0 0 42px rgba(0,0,0,1)}
      #content h3{font-size:5.4em;position:relative;top:-.9em;text-shadow:0 0 22px rgba(0,0,0,1);-moz-opacity:.9;opacity:.9}
      #content div.link{position:absolute;bottom:80px;text-align:center;width:100%}
      #content div a{display:inline-block;font-size:3em;position:relative;padding:0 30px 5px;background-color:#d63a0a;color:#fff;text-decoration:none;-webkit-box-shadow:0 0 30px 0 rgba(0,0,0,0.6);-moz-box-shadow:0 0 30px 0 rgba(0,0,0,0.6);box-shadow:0 0 30px 0 rgba(0,0,0,0.6)}
      #content a:hover{top:-1px}
      #content a:active{top:2px!important}
      @media only screen and (max-width: 1280px) {
      #content h1{font-size:13em}
      #content h3{font-size:3.8em}
      #content div a{font-size:2em}
      }
      @media only screen and (max-width: 479px) {
      #content h1{font-size:10em}
      #content h3{font-size:2.8em}
      #content div a{font-size:1.4em}
      }
    </style>
    <noscript>
        <style type="text/css">
            #bg-fullscreen {
                -moz-opacity: 0.9;
                opacity: 0.9;
                background-image: url('//habrastorage.org/files/7c1/dfc/c33/7c1dfcc3386347d0aa20b4f3cc1a410a.jpg');
            }
        </style>
    </noscript>
    <script type="text/javascript" src="//code.jquery.com/jquery-latest.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            var imagesArray = [],
                debug = true;

            function getImagesFromTumblr(blogName, imgArr, imgCount, callback, makeOffset) {
                var offsetStep = 20,
                    makeOffset = typeof makeOffset !== 'undefined' ? makeOffset : 0,
                    imgCount = typeof imgCount !== 'undefined' ? imgCount : 5;
                $.ajax({
                    type: 'GET',
                    // https://www.tumblr.com/docs/en/api/v2
                    url: '//api.tumblr.com/v2/blog/' + blogName + '.tumblr.com/posts',
                    dataType: 'jsonp',
                    data: {
                        // https://www.tumblr.com/oauth/apps
                        api_key: 'P1M2xgqzN8Q5V9Oh1eMp2a6V2YceKV5Z7FvlPZlWgDXvPT6AMs',
                        offset: makeOffset

                    },
                    success: function (data) {
                        if (debug) console.log('Makeing request with offset = %d', makeOffset);
                        if (data.meta.status === 200) { // if answer is 'ok'
                            $.each(data.response.posts, function () {
                                if (this.type === 'photo') {
                                    $.each(this.photos, function () {
                                        var ext = this.original_size.url.split('.').pop(); // find image extension
                                        if (
                                            // check image for:
                                            (ext === 'jpg') // 1. type - 'jpg'
                                            && (this.original_size.width >= 640) // 2. minimal width
                                            //&& (this.original_size.width > this.original_size.height) // 2. horizontal
                                        ) {
                                            if (imgArr.length < imgCount) {
                                                imgArr.push(this);
                                            }
                                        }
                                    });
                                }
                            });
                        }
                        // if array not full..
                        if (imgArr.length < imgCount)
                        // ..make a recrussive run
                            getImagesFromTumblr(
                            blogName,
                            imgArr,
                            imgCount,
                            callback, ((makeOffset === 0) ? offsetStep : makeOffset + offsetStep)
                        )
                        else
                        if ($.isFunction(callback)) callback(true);

                    },
                    error: function () {
                        if (debug) console.error('Error try ajax request');
                        if ($.isFunction(callback)) callback(false);
                    }
                });
            }

            // 'womenexcellence' - girls, +18
            // 'life'            - black'n'white photos
            // 'weirdvintage'    - weird vintage
            // 'awesomepeoplehangingouttogether' - awesome people hanging out together
            // 'meiguiceserra'   - space planets

            if (debug) console.time('Getting Tumblr Images Data');
            getImagesFromTumblr('meiguiceserra', imagesArray, 15, function (noerror) {
                if (debug) console.timeEnd('Getting Tumblr Images Data');

                function getArrayItem(arr) {
                    return arr[Math.floor(Math.random() * arr.length)];
                }

                function preloadImg(url, callback) {
                    var pImg = new Image();
                    pImg.onload = function () {
                        if ($.isFunction(callback)) callback(true);
                    }
                    pImg.src = url;
                }

                if (debug) console.log(imagesArray);
                if (imagesArray.length > 0) {

                    var imageUrl = getArrayItem(imagesArray).original_size.url;
                    if (debug) console.log('Random image url: %s', imageUrl);

                    if (debug) console.time('Image downloading');
                    preloadImg(imageUrl, function () {
                        if (debug) console.timeEnd('Image downloading');
                        $('#bg-fullscreen').css({
                            'background-image': 'url(' + imageUrl + ')'
                        }).addClass('show');
                    });
                }
            });
        });
    </script>
</head>

<body>
    <div id="bg-fullscreen"></div>
    <div id="content">
        <h1>404</h1>
        <h3>Not found</h3>
        <div class="link">
            <a href="" class="home">&larr; Main page</a>
        </div>
    </div>
</body>

</html>

Алгоритм работы функции следующий:

  1. Формируем и отправляем Ajax-запрос к API Tumblr-a;
  2. Проверяем статус ответа и проходимся по каждому посту;
  3. Если это фото-пост, то проходимся по каждому изображению;
  4. Если изображение нам подходит (например — тип, минимальный размер, соотношение сторон), то добавляем его в итоговый массив;
  5. Если по завершению прохода нужное количество изображений не собрано — рекурсивно запускаемся снова, но с новым отступом.

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

image

И несколько слов о том, в каком виде у нас возвращаемые данные:
image

Плюсы данной реализации:

  • Если захочется использовать gif-изображение — изменяем искомое расширение (строка ~178) и пересматриваем проверку размеров изображений;
  • Чтобы изменить источник изображений — необходимо изменить один вызов функции;
  • При отключенном JavaScript — выведем изображение из заготовки (см. <noscript>… </noscript>);
  • Доступны различные размеры изображений;
  • Работает даже в IE6 (при выключенном ‘debug’ — режиме, строка ~153);
  • Легко «допилить» под себя.

И минусы:

  • В среднем получение и разбор данных (получалось 1..2 запроса, 10 изображений) во время тестов занимал порядка 0,4..1 секунды, что довольно долго;
  • Необходимость таскать JQuery.

Демо

Эпилог

Данный метод может замечательно вписаться в небольшие сайты, портфолио, студии, блоги. Не нуждается в поддержке, легко интегрируется в готовые решения, не нагружает сервер. Вполне реально использовать в шаблонах для наполнения тестовым контентом (несколько строк на jQuery по замене ‘src’ у <img />). Буду рад, если кому-то помог, или навел на другую стоящую мысль.