Пишем Photobooth на PHP, jQuery и CSS3

В этой статье мы будем писать WEB приложение Photobooth на jQuery и PHP. Оно позволит посетителям сделать снимок с веб-камеры и загрузить его в галерею, доступную для просмостра всем остальным.

Как вы знаете, с помощью JavaScript не возможно получить доступ к веб-камере и к другим периферийным устройствам. Однако есть решение – мы можем использовать flash. Flash имеет полный доступ к веб-камере, а так же установлен на большинство компьютеров.

Решать данную задачу мы будем с помощью библиотеки webcam.js. Это JavaScript-оболочка для flash API, что даст нам контроль над веб-камерой пользователя.

HTML

Первым шагом к построению нашего Photobooth приложения будет написание HTML структуры страницы. Поэтому создадим простой HTML файл index.html со следующим содержимым:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>

    <title>Пишем Photobooth на PHP, jQuery и CSS3</title>

    <link rel="stylesheet" type="text/css" href="assets/fancybox/jquery.fancybox.css"/>
    <link rel="stylesheet" type="text/css" href="assets/css/buttons.css"/>
    <link rel="stylesheet" type="text/css" href="assets/css/styles.css"/>
</head>
<body>

<a href="http://nikitakiselev.ru/post/20" class="ref-link" title="Вернуться к статье">
    Назад к статье
</a>

<div id="photos"></div>

<div id="camera">
    <div id="screen"></div>
    <div id="buttons">
        <div class="buttonPane">
            <a id="shootButton" href="" class="blueButton">Фото!</a>
        </div>
        <div class="buttonPane hidden">
            <a id="cancelButton" href="" class="red">Отмена</a> <a id="uploadButton" href="" class="green">Загрузить!</a>
        </div>
    </div>

    <span class="settings"></span>
    <span class="camTop"></span>
    <span class="tooltip"></span>
</div>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.3/jquery.easing.min.js"></script>
<script src="assets/fancybox/jquery.fancybox.pack.js"></script>
<script src="assets/jpegcam/webcam.js"></script>
<script src="assets/js/script.js"></script>

</body>
</html>

На странице у нас 3 DIV элемента:

  1. #topBar отображает заголовки.
  2. #photos куда изображения будут добавляться с помощью jQuery и метода $.getJSON.
  3. #camera в этом блоке находится флеш ролик webcam.swf, который мы будем использовать для доступа к камере пользователя. Так же в этом блоке находятся управляющие элементы: кнопка фотографирования и загрузки в галерею.

Управляющие элементы разделены на 2 div'a с классами .buttonPane. С помощью jQuery мы будем переключаться между копкой фотографирования и кнопкой загрузки фото в галерею.

Перед закрывающимся тегом </body> мы подключаем несколько javascript библиотек:

  • библиотека jQuery.
  • jQuery плагин fancybox для отображения фотографий во всплывающем окне.
  • easing plugin добавляет новые анимации для jQuery.
  • webcam.js

Если данное WEB приложение планируется использовать на высоконагруженном сайте, то есть смысл объединить все js в один и сжать.

PHP

Напишем PHP скрипт, который будет получать фото от камеры (upload.php), а так же скрипт для получения списока уже существующих изображений (browse.php)

upload.php

<?php

/*
 * Этот файл получает JPEG снимок от "/assets/jpegcam/webcam.swf"
 * который передаётся $_POST запросом
*/

// Скрипт работает только тогда, когда получает POST запрос
if (strtolower($_SERVER['REQUEST_METHOD']) != 'post') {
    exit;
}

$folder = 'uploads/';
$filename = md5($_SERVER['REMOTE_ADDR'] . rand()) . '.jpg';

$original = $folder . $filename;

// Получаем JPEG снимок:
$input = file_get_contents('php://input');

if (md5($input) == '7d4df9cc423720b7f1f3d672b89362be') {
// Пустое изображение
    exit;
}

$result = file_put_contents($original, $input);
if (!$result) {
    echo '{
"error"        : 1,
"message"   : "Ошибка при сохранении изображения. Убедитесь, что у Вас есть доступ к папке ' . $folder . '
}';
    exit;
}

$info = getimagesize($original);
if ($info['mime'] != 'image/jpeg') {
    unlink($original);
    exit;
}

// Переносим полученное фото в папку с полноразмерными изображениями
rename($original, 'uploads/original/' . $filename);
$original = 'uploads/original/' . $filename;

// Используем GD library для изменения размера фото,
// чтобы получить миниатюру

$origImage = imagecreatefromjpeg($original);
$newImage = imagecreatetruecolor(154, 110);
imagecopyresampled($newImage, $origImage, 0, 0, 0, 0, 154, 110, 520, 370);

imagejpeg($newImage, 'uploads/thumbs/' . $filename);

echo '{"status":1,"message":"Success!","filename":"' . $filename . '"}';

Как я упоминал ранее, мы не можем общаться с веб-камерой пользователя непосредственно из JavaScript. Вот почему нам нужен flash, который имеет полный доступ к камере. Он чтобы выступать в качестве промежуточного слоя. Получить изображение от веб-камеры можно двумя путями:

  1. Мы можем передавать изображения напрямую из флеш к javascript (медленно и неоптимально).
  2. Передавать изображение напрямую в php скрипт с помощью POST.

Мы используем второй способ и передаём снимок напрямую в PHP скрипт. Так же снимок получается в формате JPEG, что означает, что мы можем сохранить его в файл напрямую из PHP, без его преобразования.

В PHP скрипте upload.php мы получаем JPEG изображение и сохраняем его в файл в папку uploads/original/. Затем генерируем миниатюру размером 154x110 px в папку uploads/thumbs. Я выбрал этот размер эскизов из соображений удобства, как он сохраняет пропорции исходного изображения (520x370 px).

browse.php

<?php
/*
 * Этот скрипт сканирует папки с миниатюрами и позвращает
 * их в JSON объекте. Эти миниатюры будут использоваться
 * для отображения всех фото на странице, сделанных
 * пользователями.
*/

// Заголовок для json:
header('Content-type: application/json');

$perPage = 24;

// Сканируем папку с миниатюрами:
$g = glob('uploads/thumbs/*.jpg');

if (!$g) {
    $g = array();
}

$names = array();
$modified = array();

// Запускаем цикл от 0 до количества найденных с помощью glob() изображений,
// получаем в массив $names имена файлов, а в массив $modified время создания файла

for ($i = 0, $z = count($g); $i < $z; $i++) {
    $path = explode('/', $g[$i]);

    $names[$i] = array_pop($path);
    $modified[$i] = filemtime($g[$i]);
}

// Функция array_multisort() сортирует массив с именами изображений $names
// в соответствии с датой создания этого изображения

array_multisort($modified, SORT_DESC, $names);

$start = 0;

// browse.php так же может разбивать результат постранично
// с помощью параметра GET, в котором хранится имя изображения,
// с которого начинать отображение галереи

if (isset($_GET['start']) && strlen($_GET['start']) > 1) {
    $_GET['start'] = trim($_GET['start']);
    $_GET['start'] = htmlspecialchars($_GET['start']);

    $start = array_search($_GET['start'], $names);

    if ($start === false) {
        // Не нашёл изображение с нужным именем
        $start = 0;
    }
}

// В переменной nextStart будем хранить имя изображения,
// которое будет являться первым на следующей странице галереи
//
// Скрипт будет передавать это значение в $_GET['start'],
// если пользователь нажан на кнопку "Далее" в галерее.

$nextStart = '';

if ($names[$start + $perPage]) {
    $nextStart = $names[$start + $perPage];
}

$names = array_slice($names, $start, $perPage);

// Возвращаем результат как JSON объект:

echo json_encode(array(
    'files' => $names,
    'nextStart' => $nextStart
));

Скрипт browse.php, возвращает содержимое папки с изображениями в формате JSON. С помощью функции glob() мы сканируем папку и возвращает массив с именами файлов. Затем мы сортируем массив по дате функцией array_multisort(), после чего "отрезаем" 24 фотографии с помощью array_slice().

jQuery

Плагин webcam.js предоставляет простое API, доступное как глобальный объект с именем webcam. Он дает нам методы для получения и загрузки фотографий, а так же методы для работы с SWF-файлом.

В script.js мы будем использовать это api. Сначала мы определим некоторые переменные и наиболее часто используемые jQuery селекторы для повышения производительности:

assets/js/script.js – Часть 1

$(document).ready(function(){

    var camera = $('#camera'),
        photos = $('#photos'),
        screen =  $('#screen');

    var template = '<a href="uploads/original/{src}" rel="cam" '
        +'style="background-image:url(uploads/thumbs/{src})"></a>';

    /*----------------------------------
     Установки WEB камеры
     ----------------------------------*/

    webcam.set_swf_url('assets/webcam/webcam.swf');
    webcam.set_api_url('upload.php');    // Скрипт созранения фото
    webcam.set_quality(80);             // качество фото
    webcam.set_shutter_sound(true, 'assets/jpegcam/shutter.mp3');

    // Генерируем HTML код для области с камерой:
    screen.html(
        webcam.get_html(screen.width(), screen.height())
    );

Переменная template в коде выше содержит HTML разметку, которая создается для каждой фотографии. Это обычная гиперссылка, у которой в качестве фона используется миниатюра загруженного изображения. Эта ссылка так же ведёт на полноразмерную версию этого изображения. Атрибут {src} мы заменим на имя файла фотографии (имена файлов генерируются автоматически в upload.php из раздела про PHP).

Далее мы будем привязывать события для кнопок управления. Обратите внимание на использование методов webcam.freeze() и webcam.upload(). Это дает пользователю сфотографиговать и решить, следует ли загрузить фото позднее. Webcam.Reset() подготавливает веб-камеру для следующего фотографирования.

assets/js/script.js - Часть 2

/*----------------------------------
     Привязываем события для кнопок управления
     ----------------------------------*/

    var shootEnabled = false;

    $('#shootButton').click(function(){

        if(!shootEnabled){
            return false;
        }

        webcam.freeze();
        togglePane();
        return false;
    });

    $('#cancelButton').click(function(){
        webcam.reset();
        togglePane();
        return false;
    });

    $('#uploadButton').click(function(){
        webcam.upload();
        webcam.reset();
        togglePane();
        return false;
    });

    camera.find('.settings').click(function(){
        if(!shootEnabled){
            return false;
        }

        webcam.configure('camera');
    });

    // Показываем/Скрываем панель с камерой:

    var shown = false;
    $('.camTop').click(function(){

        $('.tooltip').fadeOut('fast');

        if(shown){
            camera.animate({
                bottom:-466
            });
        }
        else {
            camera.animate({
                bottom:-5
            },{easing:'easeOutExpo',duration:'slow'});
        }

        shown = !shown;
    });

    $('.tooltip').mouseenter(function(){
        $(this).fadeOut('fast');
    });

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

assets/js/script.js- Часть 3

 /*----------------------
     Callbacks
     ----------------------*/

    webcam.set_hook('onLoad',function(){
        // Когда flash загружен, делаем доступными
        // кнопки фотографирования и настройки камеры:
        shootEnabled = true;
    });

    webcam.set_hook('onComplete', function(msg){

        // Объект JSON, возвращаемый скриптом upload.php,
        // в котором содержится имя снятой фотографии:

        msg = $.parseJSON(msg);

        if(msg.error) {
            alert(msg.message);
        }
        else {
            // Добавим изображение к галерее
            photos.prepend(templateReplace(template,{src:msg.filename}));
            initFancyBox();
        }
    });

    webcam.set_hook('onError',function(e){
        screen.html(e);
    });

Здесь мы завершаем работу с WEB камерой. Далее нужно показать список последних фотографий, чтобы пользователи могли их просматривать. Мы будем делать это с помощью пользовательской функции под названием loadPics(), которая будет общаться к browse.php:

assets/js/script.js - Часть 4

/*-------------------------------------
     Получаем уже существующие фото
     -------------------------------------*/

    var start = '';

    function loadPics(){

        // Возвращает true, когда фотки загрузились:

        if(this != window){
            if($(this).html() == 'Загрузка..') {
                // Ничего не делаем пока "Загрузка..."
                // чтобы защиться от нескольких нажатий подряд
                // на кнопку Далее в галерее
            }
                return false;
            }
            $(this).html('Загрузка..');
        }

        // AJAX запрос. В параметре "start" мы указываем
        // с какой картинки отображать галерею.
        // Используется для постраничной навигации

        $.getJSON('browse.php',{'start':start},function(r){

            photos.find('a').show();

            // detach() удаляет элемент #loadMore
            // при использовании detach, jQuery
            // не удаляет информацию о элементе и поэтому
            // он может быть восстановлен
            var loadMore = $('#loadMore').detach();

            if(!loadMore.length){
                loadMore = $('<span>',{
                    id          : 'loadMore',
                    html        : 'Далее',
                    click       : loadPics
                });
            }

            $.each(r.files,function(i,filename){
                photos.append(templateReplace(template,{src:filename}));
            });

            // Если есть следующас страница с фотками:
            if(r.nextStart){

                start = r.nextStart;
                photos.find('a:last').hide();
                photos.append(loadMore.html('Далее'));
            }

            // Переинициализируем Fancy Box каждый раз,
            // когда изменяются фотки на странице:

            initFancyBox();
        });

        return false;
    }

    // Вызывает loadPics() при загрузке страницы
    loadPics();

Так как loadPics() привязана еще и к событию click для кнопки загрузки дополнительнх фотографий "Далее", эта функция может быть вызвана двумя способами: нормальный и качестве обратного вызова. Разница заключается в том, что объект this будет window или DOM-элементом. Мы проверяем это и создаём защиту от многократных кликов подряд, чтобы небыло множественных запросов к browse.php.

И наконец мы напишем вспомогательные функции, используемые в остальной части кода.

assets/js/script.js - Часть 5

    /*----------------------
     Вспомогательные функции
     ------------------------*/

// Инициализируем fancybox скрипт

    function initFancyBox(filename) {
        photos.find('a:visible').fancybox({
            'transitionIn': 'elastic',
            'transitionOut': 'elastic',
            'overlayColor': '#111'
        });
    }

// Функция переключает режимы отображения панелей .buttonPane

    function togglePane() {
        var visible = $('#camera .buttonPane:visible:first');
        var hidden = $('#camera .buttonPane:hidden:first');

        visible.fadeOut('fast', function () {
            hidden.show();
        });
    }

// Функция-парсер, заменяет шаблонные значения "{KEYWORD}"
// их реальными значениями

    function templateReplace(template, data) {
        return template.replace(/{([^}]+)}/g, function (match, group) {
            return data[group.toLowerCase()];
        });
    }
});

Теперь, когда мы разобрали весь код, давайте напишем CSS-стили.

CSS

С внедрением поддержки CSS3 в Firefox 4 эффекты переходов можно, наконец то, использовать на всю катушку.

assets/css/styles.css

/*-------------------
    Photo area
--------------------*/

#photos{
    margin: 60px auto 100px;
    overflow: hidden;
    width: 880px;
}

#photos:hover a{
    opacity:0.5;
}

#photos a,
#loadMore{
    background-position: center center;
    background-color: rgba(14, 14, 14, 0.3);
    float: left;
    height: 110px;
    margin: 1px 1px 0 0;
    overflow: hidden;
    width: 145px;

    -moz-transition:0.25s;
    -webkit-transition:0.25s;
    -o-transition:0.25s;
    transition:0.25s;
}

#photos a:hover{
    opacity:1;
}

#loadMore{
    cursor: pointer;
    line-height: 110px;
    text-align: center;
    text-transform: uppercase;
    font-size:10px;
}

#loadMore:hover{
    color:#fff;
    text-shadow:0 0 4px #fff;
}

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

jquery-photobooth-hover.jpg

Заключение

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