Решал интересную задачу – сделать визуальный редактор-конфигуратор окон.
Подробностями процесса разработки я с вами, коллеги, и поделюсь.
UPD. Добавил скриншоты.
UPD2. Речь идет об окнах оффлайновых, застекленных, деревянных или пластиковых — через которые на улицу из дома смотрят
Спасибо за отклики!
Интервьюирую заказчика.
1. Это модуль для сайта, который должен работать в произвольных популярных кейсах.
2. В режиме редактирования программа должна позволять указывать количество и расположение проемов в окнах.
3. В режиме редактирования программа должна позволять указывать способ открывания проемов в окнах, пять вариантов: нет открывания, налево, направо, налево и откидывается, направо и откидывается.
4. В режиме отображения программа должна картинкой в произвольном масштабе отображать конфигурацию окна.
5. Не нужно хранить и работать со сведениями о размере, пропорциях, цвете и других характеристиках окна. Картинки должны быть цветными и понятными. ЕСКД в данном случае не при делах.
6. Не должно глючить, тупить, должно быть кроссбраузерно, должно работать на в браузерах планшетных ПК и на смартфонах и т.д.
На этом этапе мы совместно с заказчиком поиском по картинкам Google просматриваем интерфейс аналогичных продуктов. Поиском по сайтам находим продавцов окон, и посещаем десяток сайтов, чтобы посмотреть на интерфейс онлайн-конфигураторов и вообще ассортимент конфигураций окон. Обсуждаем, что у нас должно быть, и чего, быть не должно.
Теперь дополняем бизнес-требования техническими условиями, для того, чтобы в итоге сформировать техническое задание.
1. Изходя из требования произвольного масштабирования – возникает понимание, что графика должна быть векторной. Кроссбраузерное решение, которое удовлетворит – HTML5 canvas.
2. Очевидно, должно быть два режима: режим редактирования и режим отображения.
3. В режиме редактирования данные должны сохраняться в input type=hidden. Я не буду вносить изменений в CMS – зачем мне лишние головняки? Просто добавлю одно поле в формы для добавления и редактирования, в СУБД и в соответствующие модели (у меня реально это происходит одним действием, если у вас нет – вероятно имеет смысл пересмотреть структуру программы).
4. В режиме редактирования ранее созданная визуальная конфигурация окна должна восстанавливаться из данных, находящихся и подставленных автоматически в поле input type=hidden.
5. В режиме отображения CMSка отдаст данные, как свойство какого-нибудь div, и моя программа должна эти данные: а) обнаружить, б) нарисовать по ним окно.
В данном случае спецификацию я делать не буду, а пойду по пути наименьшего сопротивления. Хорошая часть видения решения присутствует уже на данный момент, поэтому я начну реализацию немедленно.
Суровая программисткая реальность: не хочу усложнять себе жизнь, и поэтому изначально создаю масштабируемые и сопровождаемые решения. Поэтому DRY, поэтому абстракции и слои – сразу, по умолчанию.
Когда просматривал разновидности окон, зарисовал в тетрадке карандашом небольшой каталог, чтобы понять, что предстоит рисовать. Когда я делал эти зарисовки, пришло понимание, что я не хочу делать это на CSS (вероятно зря), и продолжать работать с <canvas />.
Иду искать библиотеку для работы с canvas. Нахожу calebevans.me/projects/jcanvas, бегло просматриваю документацию, оцениваю качество исходников и понимаю, что это то, что мне нужно сейчас.
Понимаю, что рисование будет самой низкоуровневой функцией. И вообще, давно хочется порисовать. Пробую несколько функций по документации, нахожу примеры онлайн в песочнице. Все работает, все устраивает.
Создам функцию-основу для рисования окна.
Естественно, функции не хранят параметры (это называется данными). Внутри функций – переменные.
В тот момент совесть не просыпалась, поэтому они в глобальной области видимости. Если она проснется – просто положу все в класс. Если проснется одновременно с ленью (или здравым смыслом) – буду писать на CoffeeScript. Сейчас звезды встали в определенное положение, и есть некоторое понимание того, что конечный продукт будет маленькой программой, состоящей из десятка фунций jQuery, в связи с чем целесообразность подобных действий в настоящий момент просто не рассматривается. Сначала сделать, чтобы работало. Рефакторинг – потом.
Глядя на свои зарисовки, вижу, что я могу рисовать оконные проемы, как прямоугольники, и обозначать открывание с помощью ровных ломаных линий внутри них.
Теперь – линии, обозначающие открывание. Left — налево, right – направо, tilt – откидывание. Кейса с фрамугой вниз нет (переспрашивал, когда интервьюировал заказчика), поэтому и заморачиваться сейчас не буду. Если возникнет потребность – потом можно будет легко его добавить.
Пишу несколько очень быстрых тестов, чтобы попробовать это. Все работает, поэтому перехожу дальше.
Собственно, по конфигурации проемов все окна можно поделить на “вертикальные” (как обычно делают в квартирах), Т-образные. Реже встречаются “горизонтальные” — в подъездах и в учреждениях.
Сначала нарисую что-нибудь попроще. Параметр leafs – количество проемов.
Посредством небольшой отладки и серии мелких тестов привожу функцию в рабочий вид.
Руками передаю параметры и вызываю функции, рисующие открывание – для того, чтобы сверху отображались ломанные линии.
Поворачиваю на 90 градусов, и получаю “горизонтальное” окно.
Тестирую, добиваюсь работоспособности.
Красивая пропорция – 1 к 2. Так как в бизнес-требованиях есть указание не заморачиваться с пропорциями, для Т-образного окна сделаю вот такой дизайн.
Делаю тесты, заставляю все работать ровно, без рывков.
Нарисую все виды окон, с которыми должна работать программа.
Седьмой параметр и понимание его содержание добавились позднее. Просто не обращайте на него внимание сейчас.
И добавлю в функцию, ответственные за рисование створки окна, коллбек на клик. Промежуточная версия кода не сохранилась – взяв хороший разгон, я позабыл делать частые комиты, поэтому покажу окончательную версию.
И функция, которая ловит клик по створке большого окна или маленькому окну в каталоге.
Была мысль сделать раздельные коллбеки, но в процессе причин для совершения лишней работы не нашел.
Добавил функцию-диспетчер, для удобства.
Открывание створок будет переключаться щелчком. Что может быть проще?
Сохраню в массиве список створок, и определю во втором массиве возможности по их открыванию.
Заполню массив данными по умолчанию. Не лучший вариант, но на момент написания думал о другом – о вероятном сохранении данных.
По щелчку должно меняться открывание створки. В цикле по возможностям открывания: нет, налево, направо, налево и откидывается, направо и откидывается.
И тут же, не уходя далеко…
Данные после редактирования нужно сохранять.
Сделаю сериализацию от руки.
И, теперь никто не мешает рисовать окна из сохраненных данных.
Конфигурация окон может отрисовываться в списках заказов, это очень удобно. Маленькие картинки.
Программа должна каким-то образом понимать, что настало время рисовать окна.
Исходя из ТЗ, есть два варианта – поле формы и <div /> в произвольном месте.
Пожалуй, input[name=«window_type»] – не лучшее решение. Просто на этот момент у меня была цель запустить программу в работу, и я совсем не хотел модифицировать CMSку — поэтому обучил плагин искать свое поле по его имени: windows_type.
Если делать из этой программы библиотеку, нужно положить селектор в переменную. И обязательно завернуть это в класс, чтобы закрыть пространство имен, и т.д.
Вот переработанный код целиком. Это бета, и она же пошла в продакшн без изменений.
Что не показано в статье. Функция windows_handler запускается другим JS-компонентом, по двум событиям: document.ready и успешной загрузке аяксовых данных. Таким образом, окна отрисовываются немедленно после загрузки страницы, и перерисовываются, если происходит интерактивное обновление данных (“живой режим”).
Все юзкейсы выполняются. Сделал простой тест с большим количеством перерисовываний без перезагрузок, оставил машину с запущенными хромом и мозилой на некоторое время – память не жрется. Погонял этот же тест несколько часов в хроме и в сафари на айпаде и макбуке. Проблем не обнаружено.
Маленькая картинка, создается на клиенте на лету (распечатывается великолепно)

Большая картинка. Размеры можно и поправить, когда-нибудь.

В режиме редактирования. Щелчок на маленькое окошко в каталоге изменяет конфигурацию большого (и сразу же данные в input type=hidden).

Щелчок на створку большого окна изменяет открывание створки.

Изменений в CMS не было. Окно добавляется и редактируется в скрытом поле, отрисовывается в div. Получается, что конфигуратор окон можно засунуть в произвольный вордпресс — просто подключив этот скрипт.
В настоящий момент благодаря этому решению продано, заказано и установлено уже очень много новых окон.
Хорошо бы засунуть этот код в какую-нибудь песочницу, вместе с тестами. Как вы считаете?
Сообщайте замечания в личку.
Спасибо!
Подробностями процесса разработки я с вами, коллеги, и поделюсь.
UPD. Добавил скриншоты.
UPD2. Речь идет об окнах оффлайновых, застекленных, деревянных или пластиковых — через которые на улицу из дома смотрят
Спасибо за отклики!
Бизнес-требования
Интервьюирую заказчика.
1. Это модуль для сайта, который должен работать в произвольных популярных кейсах.
2. В режиме редактирования программа должна позволять указывать количество и расположение проемов в окнах.
3. В режиме редактирования программа должна позволять указывать способ открывания проемов в окнах, пять вариантов: нет открывания, налево, направо, налево и откидывается, направо и откидывается.
4. В режиме отображения программа должна картинкой в произвольном масштабе отображать конфигурацию окна.
5. Не нужно хранить и работать со сведениями о размере, пропорциях, цвете и других характеристиках окна. Картинки должны быть цветными и понятными. ЕСКД в данном случае не при делах.
6. Не должно глючить, тупить, должно быть кроссбраузерно, должно работать на в браузерах планшетных ПК и на смартфонах и т.д.
На этом этапе мы совместно с заказчиком поиском по картинкам Google просматриваем интерфейс аналогичных продуктов. Поиском по сайтам находим продавцов окон, и посещаем десяток сайтов, чтобы посмотреть на интерфейс онлайн-конфигураторов и вообще ассортимент конфигураций окон. Обсуждаем, что у нас должно быть, и чего, быть не должно.
ТУ и ТЗ
Теперь дополняем бизнес-требования техническими условиями, для того, чтобы в итоге сформировать техническое задание.
1. Изходя из требования произвольного масштабирования – возникает понимание, что графика должна быть векторной. Кроссбраузерное решение, которое удовлетворит – HTML5 canvas.
2. Очевидно, должно быть два режима: режим редактирования и режим отображения.
3. В режиме редактирования данные должны сохраняться в input type=hidden. Я не буду вносить изменений в CMS – зачем мне лишние головняки? Просто добавлю одно поле в формы для добавления и редактирования, в СУБД и в соответствующие модели (у меня реально это происходит одним действием, если у вас нет – вероятно имеет смысл пересмотреть структуру программы).
4. В режиме редактирования ранее созданная визуальная конфигурация окна должна восстанавливаться из данных, находящихся и подставленных автоматически в поле input type=hidden.
5. В режиме отображения CMSка отдаст данные, как свойство какого-нибудь div, и моя программа должна эти данные: а) обнаружить, б) нарисовать по ним окно.
В данном случае спецификацию я делать не буду, а пойду по пути наименьшего сопротивления. Хорошая часть видения решения присутствует уже на данный момент, поэтому я начну реализацию немедленно.
Разработка
Суровая программисткая реальность: не хочу усложнять себе жизнь, и поэтому изначально создаю масштабируемые и сопровождаемые решения. Поэтому DRY, поэтому абстракции и слои – сразу, по умолчанию.
Когда просматривал разновидности окон, зарисовал в тетрадке карандашом небольшой каталог, чтобы понять, что предстоит рисовать. Когда я делал эти зарисовки, пришло понимание, что я не хочу делать это на CSS (вероятно зря), и продолжать работать с <canvas />.
Иду искать библиотеку для работы с canvas. Нахожу calebevans.me/projects/jcanvas, бегло просматриваю документацию, оцениваю качество исходников и понимаю, что это то, что мне нужно сейчас.
Понимаю, что рисование будет самой низкоуровневой функцией. И вообще, давно хочется порисовать. Пробую несколько функций по документации, нахожу примеры онлайн в песочнице. Все работает, все устраивает.
Начинаем рисовать
Создам функцию-основу для рисования окна.
function windows_init(selector)
{
window_canvas = $('<canvas></canvas>').
attr('width',window_width).
attr('height',window_height).
attr('background','blue').
insertAfter(selector);
}
Естественно, функции не хранят параметры (это называется данными). Внутри функций – переменные.
В тот момент совесть не просыпалась, поэтому они в глобальной области видимости. Если она проснется – просто положу все в класс. Если проснется одновременно с ленью (или здравым смыслом) – буду писать на CoffeeScript. Сейчас звезды встали в определенное положение, и есть некоторое понимание того, что конечный продукт будет маленькой программой, состоящей из десятка фунций jQuery, в связи с чем целесообразность подобных действий в настоящий момент просто не рассматривается. Сначала сделать, чтобы работало. Рефакторинг – потом.
Глядя на свои зарисовки, вижу, что я могу рисовать оконные проемы, как прямоугольники, и обозначать открывание с помощью ровных ломаных линий внутри них.
function make_leaf(canvas, x,y, width, height, window)
{
canvas.drawRect({
layer: true,
strokeStyle: window_silver,
fillStyle: window_blue,
strokeWidth: 1,
x: x, y: y,
width: width,
height: height,
fromCenter: false,
});
}
Теперь – линии, обозначающие открывание. Left — налево, right – направо, tilt – откидывание. Кейса с фрамугой вниз нет (переспрашивал, когда интервьюировал заказчика), поэтому и заморачиваться сейчас не буду. Если возникнет потребность – потом можно будет легко его добавить.
// window opening draw
function open_left(canvas, x, y, width, height)
{
canvas.drawLine({
strokeStyle: window_gray,
strokeWidth: 1,
x1: x, y1: y,
x2: x + width, y2: y + (height / 2),
x3: x, y3: y + height,
});
}
function open_right(canvas, x, y, width, height)
{
canvas.drawLine({
strokeStyle: window_gray,
strokeWidth: 1,
x1: x + width, y1: y,
x2: x, y2: y + (height / 2),
x3: x + width, y3: y + height,
});
}
function tilt(canvas, x, y, width, height)
{
canvas.drawLine({
strokeStyle: window_gray,
strokeWidth: 1,
x1: x, y1: y + height,
x2: x + (width / 2), y2: y,
x3: x + width, y3: y + height,
});
}
Пишу несколько очень быстрых тестов, чтобы попробовать это. Все работает, поэтому перехожу дальше.
Виды окон
Собственно, по конфигурации проемов все окна можно поделить на “вертикальные” (как обычно делают в квартирах), Т-образные. Реже встречаются “горизонтальные” — в подъездах и в учреждениях.
Сначала нарисую что-нибудь попроще. Параметр leafs – количество проемов.
function window_vertical(canvas, x, y, width, height, leafs, window)
{
var leaf = width / leafs;
for (var i = 0; i < leafs; i++)
{
var leaf_x = x + (leaf * i);
var leaf_y = y;
var leaf_width = leaf;
var leaf_height = height;
var leaf_num = i;
make_leaf(canvas, leaf_x, leaf_y, leaf_width, leaf_height, window, leaf_num);
}
}
Посредством небольшой отладки и серии мелких тестов привожу функцию в рабочий вид.
Руками передаю параметры и вызываю функции, рисующие открывание – для того, чтобы сверху отображались ломанные линии.
Поворачиваю на 90 градусов, и получаю “горизонтальное” окно.
function window_horisontal(canvas, x, y, width, height, leafs, window)
{
var leaf = height / leafs;
for (var i = 0; i < leafs; i++)
{
var leaf_x = x;
var leaf_y = y + (leaf * i);
var leaf_width = width;
var leaf_height = leaf;
var leaf_num = i;
make_leaf(canvas, leaf_x,leaf_y,leaf_width, leaf_height, window, leaf_num);
}
}
Тестирую, добиваюсь работоспособности.
Красивая пропорция – 1 к 2. Так как в бизнес-требованиях есть указание не заморачиваться с пропорциями, для Т-образного окна сделаю вот такой дизайн.
function window_t(canvas, x,y,width, height,leafs, window)
{
var w = width / leafs;
make_leaf(canvas, x, y, width, height / 3, window, 0);
for (var i = 0; i < leafs; i++)
{
var leaf_x = x + (w * i);
var leaf_y = y + (height / 3 );
var leaf_width = w;
var leaf_height = height * 2 / 3;
var leaf_num = i + 1;
make_leaf(canvas, leaf_x,leaf_y,leaf_width, leaf_height, window, leaf_num);
}
}
Делаю тесты, заставляю все работать ровно, без рывков.
Каталог
Нарисую все виды окон, с которыми должна работать программа.
function windows_catalog()
{
window_horisontal(
window_canvas,
0,
padding,
catalog_height,
catalog_height,
1,
{type: 'single', leafs: 1, from: 'catalog'});
var offset = catalog_height + padding;
for (var i = 2; i < 5; i++)
{
window_vertical(
window_canvas,
offset,
padding,
catalog_height * (i / 2),
catalog_height,
i,
{type: 'vertical', leafs: i, from: 'catalog'});
offset += padding + (catalog_height * (i / 2));
}
window_horisontal(
window_canvas,
offset,
padding,
catalog_height,
catalog_height,
2,
{type: 'horisontal', leafs: 2, from: 'catalog'});
offset += padding + catalog_height;
for (var i = 0; i < 3; i++)
{
window_t(
window_canvas,
offset,
padding,
catalog_height,
catalog_height,
i + 2,
{type: 't', leafs: i + 2, from: 'catalog'});
offset += padding + catalog_height
}
}
Седьмой параметр и понимание его содержание добавились позднее. Просто не обращайте на него внимание сейчас.
И добавлю в функцию, ответственные за рисование створки окна, коллбек на клик. Промежуточная версия кода не сохранилась – взяв хороший разгон, я позабыл делать частые комиты, поэтому покажу окончательную версию.
function make_leaf(canvas, x,y, width, height, window, leaf_num)
{
canvas.drawRect({
layer: true,
strokeStyle: window_silver,
fillStyle: window_blue,
strokeWidth: 1,
x: x, y: y,
width: width,
height: height,
fromCenter: false,
click: function(layer) {
leaf_clicked(window, leaf_num)
}
});
}
И функция, которая ловит клик по створке большого окна или маленькому окну в каталоге.
function leaf_clicked(window, leaf_num)
{
if ( ! window)
{
return;
}
window_canvas.clearCanvas();
windows_catalog();
if (window.size == 'big')
{
trigger_opening(leaf_num);
}
big_window(window.type, window.leafs);
}
Была мысль сделать раздельные коллбеки, но в процессе причин для совершения лишней работы не нашел.
Добавил функцию-диспетчер, для удобства.
function opening(canvas, x, y, width, height, num)
{
switch (window_opening[num])
{
case 'left':
open_left(canvas, x, y, width, height);
break;
case 'left tilt':
open_left(canvas, x, y, width, height);
tilt(canvas, x, y, width, height);
break;
case 'right':
open_right(canvas, x, y, width, height);
break;
case 'right tilt':
open_right(canvas, x, y, width, height);
tilt(canvas, x, y, width, height);
break;
}
}
Переключение открывания створок
Открывание створок будет переключаться щелчком. Что может быть проще?
Сохраню в массиве список створок, и определю во втором массиве возможности по их открыванию.
// window opening
var window_opening = [];
var opening_order = ['none', 'left tilt', 'right tilt', 'left', 'right'];
Заполню массив данными по умолчанию. Не лучший вариант, но на момент написания думал о другом – о вероятном сохранении данных.
function set_opening(leaf_count)
{
for (var i = 0; i < leaf_count; i++)
{
window_opening.push(opening_order[0]);
}
}
По щелчку должно меняться открывание створки. В цикле по возможностям открывания: нет, налево, направо, налево и откидывается, направо и откидывается.
function trigger_opening(num)
{
var current = opening_order.indexOf(window_opening[num]);
if ((current + 2) > opening_order.length)
{
current = 0;
}
else
{
current++;
}
window_opening[num] = opening_order[current];
window_data();
}
И тут же, не уходя далеко…
Сохранение
Данные после редактирования нужно сохранять.
Сделаю сериализацию от руки.
function window_data()
{
var string = order.type + '|' + order.leafs;
for (var i in window_opening)
{
string += '|' + window_opening[i];
}
var select = $('input[name="window_type"]');
select.val(string);
}
И, теперь никто не мешает рисовать окна из сохраненных данных.
function window_from_string(string)
{
if ( ! string.length)
{
return;
}
var data = string.split('|');
for (var i = 0; i < 10; i++)
{
window_opening[i] = data[i + 2];
}
big_window(data[0],data[1]);
}
Конфигурация окон может отрисовываться в списках заказов, это очень удобно. Маленькие картинки.
function small_window_from_string(element, string, width, height)
{
if ( ! string.length)
{
return;
}
var small_canvas = $('<canvas></canvas>').
attr('width',width).
attr('height',height).
appendTo(element);
var data = string.split('|');
for (var i = 0; i < 10; i++)
{
window_opening[i] = data[i + 2];
}
var leafs = data[1];
switch (data[0])
{
case 'single':
window_vertical(small_canvas, 0, 0, width, height, leafs, false);
break;
case 'vertical':
window_vertical(small_canvas, 0, 0, width, height, leafs, false);
break;
case 'horisontal':
window_horisontal(small_canvas, 0, 0, width, height, leafs, false);
break;
case 't':
window_t(small_canvas, 0, 0, width, height, leafs, false);
break;
}
}
Когда же рисовать?
Программа должна каким-то образом понимать, что настало время рисовать окна.
Исходя из ТЗ, есть два варианта – поле формы и <div /> в произвольном месте.
function windows_handler()
{
// add or edit
var select = $('input[name="window_type"]');
if (select.length)
{
select.hide();
windows_init(select);
window_from_string(select.val());
}
// show small window
$('.magic_make_window').each(function() {
small_window_from_string($(this),$(this).attr('window'), $(this).width(), $(this).height())
});
}
Пожалуй, input[name=«window_type»] – не лучшее решение. Просто на этот момент у меня была цель запустить программу в работу, и я совсем не хотел модифицировать CMSку — поэтому обучил плагин искать свое поле по его имени: windows_type.
Если делать из этой программы библиотеку, нужно положить селектор в переменную. И обязательно завернуть это в класс, чтобы закрыть пространство имен, и т.д.
Итого
Вот переработанный код целиком. Это бета, и она же пошла в продакшн без изменений.
$(document).ready(function() {
set_opening(10);
});
function windows_handler()
{
// add or edit
var select = $('input[name="window_type"]');
if (select.length)
{
select.hide();
windows_init(select);
window_from_string(select.val());
}
// show small window
$('.magic_make_window').each(function() {
small_window_from_string($(this),$(this).attr('window'), $(this).width(), $(this).height())
});
}
function small_window_from_string(element, string, width, height)
{
if ( ! string.length)
{
return;
}
var small_canvas = $('<canvas></canvas>').
attr('width',width).
attr('height',height).
appendTo(element);
var data = string.split('|');
for (var i = 0; i < 10; i++)
{
window_opening[i] = data[i + 2];
}
var leafs = data[1];
switch (data[0])
{
case 'single':
window_vertical(small_canvas, 0, 0, width, height, leafs, false);
break;
case 'vertical':
window_vertical(small_canvas, 0, 0, width, height, leafs, false);
break;
case 'horisontal':
window_horisontal(small_canvas, 0, 0, width, height, leafs, false);
break;
case 't':
window_t(small_canvas, 0, 0, width, height, leafs, false);
break;
}
}
function window_from_string(string)
{
if ( ! string.length)
{
return;
}
var data = string.split('|');
for (var i = 0; i < 10; i++)
{
window_opening[i] = data[i + 2];
}
big_window(data[0],data[1]);
}
var window_width = 900;
var window_height = 350;
var catalog_height = window_width / 18;
var padding = 15;
var window_canvas;
var window_blue = '#8CD3EF';
var window_silver = 'white';
var window_gray = 'black';
var order = {type: undefined, leafs: undefined};
function window_data()
{
var string = order.type + '|' + order.leafs;
for (var i in window_opening)
{
string += '|' + window_opening[i];
}
var select = $('input[name="window_type"]');
select.val(string);
}
function windows_init(selector)
{
window_canvas = $('<canvas></canvas>').
attr('width',window_width).
attr('height',window_height).
attr('background','blue').
insertAfter(selector);
windows_catalog();
}
function windows_catalog()
{
window_horisontal(
window_canvas,
0,
padding,
catalog_height,
catalog_height,
1,
{type: 'single', leafs: 1, from: 'catalog'});
var offset = catalog_height + padding;
for (var i = 2; i < 5; i++)
{
window_vertical(
window_canvas,
offset,
padding,
catalog_height * (i / 2),
catalog_height,
i,
{type: 'vertical', leafs: i, from: 'catalog'});
offset += padding + (catalog_height * (i / 2));
}
//~ for (var i = 2; i < 6; i++)
//~ {
window_horisontal(
window_canvas,
offset,
padding,
catalog_height,
catalog_height,
2,
{type: 'horisontal', leafs: 2, from: 'catalog'});
offset += padding + catalog_height;
//~ }
for (var i = 0; i < 3; i++)
{
window_t(
window_canvas,
offset,
padding,
catalog_height,
catalog_height,
i + 2,
{type: 't', leafs: i + 2, from: 'catalog'});
offset += padding + catalog_height
}
}
function window_t(canvas, x,y,width, height,leafs, window)
{
var w = width / leafs;
make_leaf(canvas, x, y, width, height / 3, window, 0);
for (var i = 0; i < leafs; i++)
{
var leaf_x = x + (w * i);
var leaf_y = y + (height / 3 );
var leaf_width = w;
var leaf_height = height * 2 / 3;
var leaf_num = i + 1;
make_leaf(canvas, leaf_x,leaf_y,leaf_width, leaf_height, window, leaf_num);
if (window.from != 'catalog')
{
opening(canvas, leaf_x,leaf_y,leaf_width, leaf_height, leaf_num);
}
}
}
function window_vertical(canvas, x, y, width, height, leafs, window)
{
var leaf = width / leafs;
for (var i = 0; i < leafs; i++)
{
var leaf_x = x + (leaf * i);
var leaf_y = y;
var leaf_width = leaf;
var leaf_height = height;
var leaf_num = i;
make_leaf(canvas, leaf_x, leaf_y, leaf_width, leaf_height, window, leaf_num);
if (window.from != 'catalog')
{
opening(canvas, leaf_x, leaf_y, leaf_width, leaf_height, leaf_num);
}
}
}
function window_horisontal(canvas, x, y, width, height, leafs, window)
{
var leaf = height / leafs;
for (var i = 0; i < leafs; i++)
{
var leaf_x = x;
var leaf_y = y + (leaf * i);
var leaf_width = width;
var leaf_height = leaf;
var leaf_num = i;
make_leaf(canvas, leaf_x,leaf_y,leaf_width, leaf_height, window, leaf_num);
if (window.from != 'catalog')
{
opening(canvas, leaf_x,leaf_y,leaf_width, leaf_height, leaf_num);
}
}
}
function make_leaf(canvas, x,y, width, height, window, leaf_num)
{
canvas.drawRect({
layer: true,
strokeStyle: window_silver,
fillStyle: window_blue,
strokeWidth: 1,
x: x, y: y,
width: width,
height: height,
fromCenter: false,
click: function(layer) {
leaf_clicked(window, leaf_num)
}
});
}
function big_window(window_type, leafs)
{
var padding_top = catalog_height + (padding * 2);
if (window_width > window_height)
{
var segment = window_height - padding_top;
}
//~ else
//~ {
//~ var segment = (window_width - catalog_height - (padding * 3)) / 2;
//~ }
order.type = window_type;
order.leafs = leafs;
window_data();
switch (window_type)
{
case 'single':
window_vertical(
window_canvas,
0,
padding_top,
segment,
segment,
leafs,
{type: 'single', leafs: 1, size: 'big'});
break;
case 'vertical':
window_vertical(
window_canvas,
0,
padding_top,
segment /2 * leafs,
segment,
leafs,
{type: 'vertical', leafs: leafs, size: 'big'});
break;
case 'horisontal':
window_horisontal(
window_canvas,
0,
padding_top,
(segment * 2) / leafs,
segment,
leafs,
{type: 'horisontal', leafs: leafs, size: 'big'});
break;
case 't':
window_t(
window_canvas,
0,
padding_top,
segment,
segment,
leafs,
{type: 't', leafs: leafs, size: 'big'});
break;
}
}
function leaf_clicked(window, leaf_num)
{
if ( ! window)
{
return;
}
window_canvas.clearCanvas();
windows_catalog();
if (window.size == 'big')
{
trigger_opening(leaf_num);
}
big_window(window.type, window.leafs);
}
function opening(canvas, x, y, width, height, num)
{
switch (window_opening[num])
{
case 'left':
open_left(canvas, x, y, width, height);
break;
case 'left tilt':
open_left(canvas, x, y, width, height);
tilt(canvas, x, y, width, height);
break;
case 'right':
open_right(canvas, x, y, width, height);
break;
case 'right tilt':
open_right(canvas, x, y, width, height);
tilt(canvas, x, y, width, height);
break;
}
}
// window opening draw
function open_left(canvas, x, y, width, height)
{
canvas.drawLine({
strokeStyle: window_gray,
strokeWidth: 1,
x1: x, y1: y,
x2: x + width, y2: y + (height / 2),
x3: x, y3: y + height,
});
}
function open_right(canvas, x, y, width, height)
{
canvas.drawLine({
strokeStyle: window_gray,
strokeWidth: 1,
x1: x + width, y1: y,
x2: x, y2: y + (height / 2),
x3: x + width, y3: y + height,
});
}
function tilt(canvas, x, y, width, height)
{
canvas.drawLine({
strokeStyle: window_gray,
strokeWidth: 1,
x1: x, y1: y + height,
x2: x + (width / 2), y2: y,
x3: x + width, y3: y + height,
});
}
// window opening
var window_opening = [];
var opening_order = ['none', 'left tilt', 'right tilt', 'left', 'right'];
function set_opening(leaf_count)
{
for (var i = 0; i < leaf_count; i++)
{
window_opening.push(opening_order[0]);
}
}
function trigger_opening(num)
{
var current = opening_order.indexOf(window_opening[num]);
if ((current + 2) > opening_order.length)
{
current = 0;
}
else
{
current++;
}
window_opening[num] = opening_order[current];
window_data();
}
Что не показано в статье. Функция windows_handler запускается другим JS-компонентом, по двум событиям: document.ready и успешной загрузке аяксовых данных. Таким образом, окна отрисовываются немедленно после загрузки страницы, и перерисовываются, если происходит интерактивное обновление данных (“живой режим”).
Все юзкейсы выполняются. Сделал простой тест с большим количеством перерисовываний без перезагрузок, оставил машину с запущенными хромом и мозилой на некоторое время – память не жрется. Погонял этот же тест несколько часов в хроме и в сафари на айпаде и макбуке. Проблем не обнаружено.
Скриншоты
Маленькая картинка, создается на клиенте на лету (распечатывается великолепно)

Большая картинка. Размеры можно и поправить, когда-нибудь.

В режиме редактирования. Щелчок на маленькое окошко в каталоге изменяет конфигурацию большого (и сразу же данные в input type=hidden).

Щелчок на створку большого окна изменяет открывание створки.

Красота!
Изменений в CMS не было. Окно добавляется и редактируется в скрытом поле, отрисовывается в div. Получается, что конфигуратор окон можно засунуть в произвольный вордпресс — просто подключив этот скрипт.
В настоящий момент благодаря этому решению продано, заказано и установлено уже очень много новых окон.
Хорошо бы засунуть этот код в какую-нибудь песочницу, вместе с тестами. Как вы считаете?
Сообщайте замечания в личку.
Спасибо!