Создание конструктора кирпичной кладки для сайта

Компания Сиджеко занимается поддержкой сайта организации Реконстрой, которая продаёт и доставляет кирпич, черепицу, архитектурный декор и многие другие строительные материалы в Центральном Черноземье.

В процессе работы над сайтом возникла идея конструктора кладки.

У немецкого концерна «Feldhaus Klinker» существуют модельные ряды кирпича «Vascu Mix» и «Sintra Mix», которые специально предназначены для смешивания в разных пропорциях и создания неповторимого рисунка кладки. К ним существует ряд замазок «Quick Mix», применяемых при замазывании швов кладки между кирпичами. Для демонстрации этого подхода мы решили сделать конструктор кирпичной кладки, аналогов которому в рунете я пока не видел (буду рад примерам).

Конструктор кирпичной кладки



На кирпичи и швы накладываются тени и царапинки, чтобы максимально приблизить рисунок к жизни. Переключатели решили стилизовать под тач-устройства. Для конструктора были обработаны сотни изображений: фотографии кладок, кирпичей, замазок для швов и т. п.

Режим «Пьяный мастер» — потехи ради:

Режим «Пьяный мастер»



Процесс


Оставляем дизайнера с картинками (на самом деле, объём работы там несоизмеримо больше программистской стороны дела), фронтендер Дима делает красиво на клиентской стороне, я стараюсь дотянуть до его мастерства на бекенде.

Итак…

Основные шаги


Берём все возможные фотографии кладок, которые только смогли найти в рекламных материалах компании, добавляем свои фото.

Вырезаем кирпичи, производим их обтравку с прозрачными полями.

Приводим изображения кирпичей к одному размеру. От каждой модели нам нужно несколько реальных фоток, чтобы в результате было нескучно.

Кирпичи

Виртуальная кладка строится не в той же последовательности, что и реальная :-)
Так, сначала всё поле заполняем картинкой шва, размножив её до нужного размера картинки.

image

Затем заполняем слой кирпичей, в котором разные модели линейки смешаны в нужной пропорции. Чётные ряды смещаем по горизонтали на полкирпича от нечётных. (Существуют и другие рисунки кладок для любителей дела, но пока было решено остановиться на классическом, чтобы уложиться в сроки.)

image

Можно было бы и остановиться, но выглядит слишком рафинированно. Реальность всегда лучше: потёртости, царапины — внешний вид каждой вещи отражает её историю. Нужно искусственно «состарить» изображение шумами.

Можно нанести шум просто рандомно изменив каждый пиксель изображения. Но это работает долго, да и выглядит не очень: мы, всё-таки, хотим эмулировать реальную фактуру материалов, а не ISO-шумы фотоаппарата.

// Шум (долго)
for ($x = 0; $x < $allWidth; $x++)
	for ($y = 0; $y < $allHeight; $y++)
		imagesetpixel($image, $x, $y, imagecolorallocatealpha($image, 0xff, 0xff, 0xff, MyRand::rand(115, 127)));

Случайный шум

Пойдём более интересным путём (который, к тому же, сильно сэкономит время генерации картинки) — нанесём «царапины».

Царапины — это, по сути, линии белого и чёрного цвета со случайной длиной, наклоном и прозрачностью. Если верхняя часть линии светлая, а нижняя тёмная, на изображении это выглядит как выпуклость, если верхняя часть тёмная, а нижняя светлая — как царапина (впуклость, да).

// Выборочный шум (быстро)
// Царапинки и выпуклости
for ($x = 0; $x < 5000; $x++) {
	$dotX = MyRand::rand(0, $allWidth);
	$dotY = MyRand::rand(0, $allHeight);
	$scratchWidth = MyRand::rand(1, 8);
	// 80% впадинок-царапин (+1) и 20% выпуклостей (-1)
	$scratchAdj = MyRand::rand(0, 10) > 8 ? -1 : +1;

	for ($i = 0; $i < $scratchWidth; $i++) {
		imagesetpixel($image, $dotX+$i, $dotY+$scratchAdj, imagecolorallocatealpha($image, 0xff, 0xff, 0xff, MyRand::rand(95, 110)));
		imagesetpixel($image, $dotX+$i, $dotY, imagecolorallocatealpha($image, 0x00, 0x00, 0x00, MyRand::rand(95, 110)));
	}
}

Выборочный шум

Без шумов и с ними:

Без шумов и с ними

Осталось нанести копирайт. Подгрузим шрифт (Убунту подойдёт).

imagettftext($image, 10, 90, $allWidth - 7, $allHeight - 7, imagecolorallocatealpha($image, 0, 0, 0, 90), 'font/ubuntu-r.ttf', 'Sijeko Brick Generator ' . GENERATOR_VERSION);

Копирайт

Готово!

Конструктор на выходе может выдавать и JPEG, и PNG (все исходные картинки в PNG24), но ввиду большого размера результирующего PNG мы остановились на JPEG.

Кеширование и урлы

Второй раз строить изображение при тех же входных параметрах не нужно, пощадим процессор и пользователя; сохраняем картинку при первом обращении и выдаём её из кеша при последующих.

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

// Сортируем ключи (чтобы идентификатор кеша всегда был одинаков при одинаковом наборе ключей)
ksort($_GET);

Затем получаем всю остальную часть идентификатора:

$fileIdentifier = 'images/cache/' . $randSeed;

$fileIdentifier .= sprintf(
	'/lineup-%s_color-%d_drunk-%s_',
	$lineup,
	$backFile,
	$isDrunk ? 'on' : 'off'
);

foreach ($_GET as $key => $value) {
	if (!empty($value))
		$fileIdentifier .= preg_replace('/[^a-z0-9_-]/ui', '', $key) . '-' . preg_replace('/[^a-z0-9_-]/ui', '', $value) . '_';
}
$fileIdentifier = rtrim($fileIdentifier, '_');
$fileIdentifier .= '.' .  OUTPUT_FORMAT;


Не забудем и про HTTP-заголовки, связанные с кешированием:

header('Cache-Control: Public');
header('Pragma: Public');
header('Last-Modified: '     . gmdate('D, d M Y H:i:s', $time) . ' GMT');
header('Expires: '           . gmdate('D, d M Y H:i:s', $time + 30*24*60*60) . ' GMT');


Генератор случайных чисел

Чтобы полностью следовать философии урлов, для каждой ссылки всегда должна выдаваться одна и та же картинка. Но у нас в системе много случайных факторов (шумы, неровности). Если быть предельно точным, нам нужен не генератор псевдослучайных чисел (ГПСЧ), а генератор псевдослучайных последовательностей (ГПСП). Каждая последовательность, определяемая неким идентификатором (seed), должна быть уникальной и повторяемой.

В новых версиях PHP (≥5.2.1), генератор mt_rand() перестал выдавать одинаковые последовательности при одинаковых значениях параметра mt_srand($seed) (по крайней мере, рекомендуется на это поведение не рассчитывать). Напишем свой велосипед. Поскольку криптографической безопасности от генератора нам не требуется, воспользуемся одним из самых популярных и простых методов — умножением с переносом.

/**
 * Получение следующего псевдослучайного целого числа в заданном диапазоне.
 * @param integer $min
 * @param integer $max
 * @return integer
 */
public static function rand($min = 0, $max = 0xffffffff)
{
	self::$m_z = 36969 * (self::$m_z & 65535) + (self::$m_z >> 16);
	self::$m_w = 18000 * (self::$m_w & 65535) + (self::$m_w >> 16);

	$res = ((self::$m_z << 16) + self::$m_w) & 0xffffffff;
	$res = $res > 0 ? $res : ~$res;
	//var_dump($res, PHP_INT_MAX, 0xffffffff); die;
	return intval($min + floor(($res / doubleval(0xffffffff)) * ($max - $min + 1)));
}

Где self::$m_z и self::$m_w — статические поля текущего состояния генератора. При одном и том же заданном наборе self::$m_z и self::$m_w генератор будет выдавать одинаковые псевдослучайные последовательности целых чисел.

Режим «Пьяный мастер»

Повернём каждый кирпич на случайный угол в узком диапазоне наклона, и выпивший мастер уже работает над вашей стеной.

Варьируя диапазон наклона в градусах, можно менять количество и градус выпитого (простите за каламбур). Я остановился на ±3,2°.

Ссылки


Поделиться публикацией
Комментарии 25
    +8
    Я бы для таких вещей советовал использовать canvas, думаю он бы тут подошел более чем.
      0
      Ну, мы хотели именно серверный вариант: опыта больше, да и хотелось обеспечить хотя бы минимальную защиту от копирования.
        +2
        А нафига копировать кирпичную кладку? Да и любой, кто хоть чуток владеет фотошопом, вырежет ваш вотермарк и затайлит картинку без проблем.
          +3
          Смысл именно в конструкторе, а не в одном конкретном изображении кладки.
          Многие люди любят кастомизацию: строят дом и им интересно, чтобы выглядело уникально (для того эти модельные ряды и существуют).

          По поводу копирования: на прошлой неделе был прецедент. Одна питерская компания разместила весь клиентский код конструктора на своём сайте без спроса и брала изображения с нашего сервера. Мы им подсунули предупреждение на всю картинку, что воровать нехорошо, и послали официальный запрос, они убрали эту страницу с сайта.
            +9
            А смысл копировать генератор из исходников, если вы выложили статью о нём на Хабре.
              0
              Копировали на прошлой неделе, статьи на Хабре тогда ещё не существовало.
              Отчасти этот факт плагиата был одним из стимулов: раз у нас что-то своровали, значит единица смысла в этом есть, можно и честно́му люду показать.
      +3
      Порадовали.

      В «пьяном мастере» можно поиграться с глубиной тени на разных кирпичах («вылет» тени).
      Особенно правдоподобно будет выглядеть, если рисовать тень с незначительно другим наклоном, чем кирпич.
      А то идеальная плоская стена получается…

      Плюс можно паттерны кирпичей рандомно вращать на 180*rand(0,1), либо расширить количество исходных кирпичей засчет горизонтальных/вертикальных отражений + переворотов.
        +2
        По поводу тени пока нет идей как это сделать.

        По поводу рандомных 180° и отражений — отличная идея, внедрим!

        Спасибо.
        0
        Первый раз открыл сайт с адресом на кириллице.
          0
          Да ладно?! :-)
          Есть минусы: Яндекс.Маркет с русскоязычными доменами пока не работает, и неизвестно, когда начнёт :-(
            0
            Работает Яндекс.Маркет с кириллицей нормально. Вот программа улучшения сниппетов Яндекса (когда цена в поиске показывается) — увы нет.
        +1
        Смотрится неплохо, пока не начинаешь присматриваться к кирпичам. Потом оказывается, что они одинаковые.
          0
          Можно было бы сделать больше вариаций для каждой марки в линейке, согласен.
          Улучшать можно до бесконечности. Вопрос обычно в сроках.

          Может, добавим зеркальное отражение и повороты на 180° в двух направлениях чуть позже.
          0
          Отличный конструктор. Только именно эти кирпичи я в продаже не увидел. Фирму Feldhaus Klinker нашёл, а модельные ряды Vascu Mix и Sinatra Mix нет. То есть, хотелось бы в конструкторе видеть ссылки на эти позиции в каталоге, а ещё было бы неплохо добавить расчёт стоимости стенки по площади или по размерам дома — цена за квадратный метр в каталоге есть. Чтобы узнать, во сколько обойдётся, например, облицевать дом с размерами 10м х 8м и высотой стены 3 метра. Можно без учёта дверей и окон :)
            0
            Очень дельно, спасибо!
            0
            Почему-то ожидал решение на «чистом CSS»…
              +2
              В тридцать строк? :-)
              +1
              А как насчет такого алгоритма?
                0
                Думал про него (обожаю этот пост, иногда перечитываю).
                Пока не очень понятно, как применить к картинкам одного размера, как-то делать прозрачность слоёв хитро.

                Алгоритм можно применить только для изображений самих кирпичей, думаю. (Один дизайнер пока болеет, а второй в отпуске.)
                Для формирования кладки обычного рандома хватит за глаза (потому что всё равно они должны быть одинаковы при одинаковых ссылках).
                0
                Так в той статье как раз и не рандом получяется, а именно очень долгая последовательность. Да и пхп грузить не придётся.
                  0
                  Ну, основная задача (где можно улучшить) не в генерации последовательности с очень большим периодом (с этим справляется почти любой рандомайзер) для рисунка кладки, а в генерации разных рисунков самих кирпичей (вот тут принцип цикады может пригодится, но я пока не знаю, как это сделать красиво).
                  0
                  Для создания кирпичной кладки нужно просто хорошенько напугать верстальщика :-)
                    0
                    Да, и дизайнера: дизайнер выдаст кирпичи, а верстальщик их сложит, как надо.
                    0
                    удалено

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое