Псевдо-случайное изображение (на примере страницы 404-й ошибки)

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

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



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

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

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

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

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



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

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


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

    <!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&subset=latin,cyrillic" rel="stylesheet" type="text/css" />
        <style type="text/css">
    

    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: 0.9;
                opacity: 0.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: 0px 0px 42px rgba(0, 0, 0, 1);
            }
            #content h3 {
                font-size: 5.4em;
                position: relative;
                top: -0.9em;
                text-shadow: 0px 0px 22px rgba(0, 0, 0, 1);
                -moz-opacity: 0.9;
                opacity: 0.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 30px;
                    background-color: #d63a0a;
                    color: #fff;
                    text-decoration: none;
                    
                    -webkit-box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.6);
                       -moz-box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.6);
                            box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.6);
                }
                    #content a:hover {
                        top: -1px;
                    }
                    #content a:active {
                        top: +2px !important;
                    }
                #content a.home {}
    
            @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('awesomepeoplehangingouttogether', imagesArray, 10, 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. Если по завершению прохода нужное количество изображений не собрано — рекурсивно запускаемся снова, но с новым отступом.


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

    404 Pages Slide Show


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


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

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


    Просмотреть демонстрацию


    Эпилог


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

    Comments 19

      +15
      dat ass!
      +9
      Где взять скрипт, который будет получать права на изображения?
        +1
        Может с flickr-а брать? Там вроде все строго с правами и наверняка через API можно получать свободные экземпляры под нужной лицензией.
          +1
          Спасибо, совсем он вылетел из головы, обязательно посмотрю
            0
            Не за что. В API вот сходу нашел подобную функцию, да и в Useful values можно найти что-то полезное, типа:
            Popular public group IDs:
            16978849@N00 — Black and White
            1577604@N20 — Group with Ҽxperience
            34427469792@N01 — FlickrCentral
              0
              За одно расскажите можно ли там получать изображения по определенной тематике (или ключевику)…

              Еще есть 500px.com и у него, как и у фликра, есть какое-то API.

              p.s. Кто этим вопросом занимался? Расскажите про лицензионный тонкости. А то я не уверен, можно ли просто так взять фото и отображать на своем сайте.
          +1
          «-khtml-opacity: 0.9» вам зачем?
            0
            Спасибо за замечание, исправил
            +2
            Сейчас в этом блоге вот что :)

            image
              –3
              Нехотя, но тоже исправил :)
              +1
              Зашел на демо-страницу, обновил пять раз — 3 раза из 5 была одна и та же фото с пожилыми людьми. Вот уж и вправду «псевдо»… Да и грузится фон не сразу — видны запросы к API tumblr, пользователь с такой страницы, имхо, раньше уйдет чем загрузки фона дождется.
                0
                Если бы я писал для вас гороскоп, то на сегодня точно бы не забыл строчку «Сегодня явно ваш день!» :)
                Необходимо некоторое время для получения json ответа, плюс прелоад картинки, чтоб не получилось «я плавно появляюсь, но пока до конца не загрузилась».
                Избежать этого можно, но путем переноса кода на сервер. Переписать на любой современный ЯП труда не составит, а если сервер ещё и на толстом канале — то задержка сведется к минимуму.
                +1
                А есть какая-то возможность прикрутить туда возрастные ограничения? Если я, например, не хочу, чтобы у меня в выборку попадали фото 18+
                  0
                  В UI на сайте — можно (этот функционал ещё под большим вопросом), а вот в апи — врятли… Только выбрать другой источник, скорее всего
                  +2
                  Можно из хлеба сделать трамвай, но зачем?
                  Как аргумент в пользу одной, но оригинальной и подходящей картинки. Довольно сложно понять, что картинки меняются, да и вообще редко попадаешь на 404. Мало, кто оценит старания, а вот притормаживающую загрузку с flickr заметить проще, на мой взгляд.
                    +1
                    Эсквайр такое делал c помошью фликра esquire.ru/randomness

                    Only users with full accounts can post comments. Log in, please.