Pull to refresh

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

Website developmentJavaScript
Однажды автор этого поста работал над одним заказом по разработке простенько сайта и тогда появилась идея — придать всем страницам некой уникальности и запоминаемости — использовать уникальные фоновые текстуры или элементы дизайна (активно использовался 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 />). Буду рад, если кому-то помог, или навел на другую стоящую мысль.
Tags:javascripttumblrimgjquery404UIjson
Hubs: Website development JavaScript
Rating +29
Views 32.2k Add to bookmarks 324
Comments
Comments 19
UX/UI дизайнер
March 17, 2021104,900 ₽Нетология
JavaScript Developer. Professional
March 29, 202172,500 ₽OTUS
UI-дизайнер
March 31, 202159,900 ₽Нетология
Node.js: серверный JavaScript
April 5, 202127,000 ₽Loftschool
Комплексное обучение JavaScript
April 19, 202127,000 ₽Loftschool