Псевдо-випадкове зображення (на прикладі сторінки 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;
        -khtml-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;
            -khtml-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;
            -khtml-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;
                -khtml-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>

 
Алгоритм роботи функції наступний:
 
     
Формуємо і відправляємо Ajax-запит до API Tumblr-a ;
 Перевіряємо статус відповіді і проходимся по кожному посту;
 Якщо це фото-пост, то проходимся по кожному зображенню;
 Якщо зображення нам підходить (наприклад — тип, мінімальний розмір, співвідношення сторін), то додаємо його в підсумковий масив;
 Якщо по завершенню проходу потрібну кількість зображень не зібрав — рекурсивно запускаємо знову, але з новим відступом.
 
 
Результат роботи прикладу виглядає наступним чином (одне зображення — один показ):
 
 404 Pages Slide Show
 
І кілька слів про те, в якому вигляді у нас повертаються дані:
 
 
Плюси даної реалізації:
 
     
Якщо захочеться використовувати gif-зображення — змінюємо шукане розширення (рядок ~ 178) і переглядаємо перевірку розмірів зображень;
 Щоб змінити джерело зображень — необхідно змінити один виклик функції;
 При відключеному JavaScript — виведемо зображення із заготовки (див. <noscript>… </ noscript>);
 Доступні різні розміри зображень;
 Працює навіть в IE6 (при вимкненому 'debug' — режимі, рядок ~ 153);
 Легко «допив» під себе.
 
І мінуси:
 
     
В середньому отримання і розбір даних (виходило 1… 2 запиту, 10 зображень) під час тестів займав близько 0,4… 1 секунди, що досить довго;
 Необхідність тягати JQuery.
 
 
 ÐŸÑ€Ð¾ÑÐ¼Ð¾Ñ‚реть демонстрацию
 
 

Епілог

Даний метод може чудово вписатися в невеликі сайти, портфоліо, студії, блоги. Чи не потребує підтримки, легко інтегрується в готові рішення, що не навантажує сервер. Цілком реально використовувати в шаблонах для наповнення тестовим контентом (кілька рядків на jQuery по заміні 'src' у <img />). Буду радий, якщо комусь допоміг, або навів на іншу вартісну думку.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.