Visual window configurator written in one hour

I solved an interesting problem - to make a visual editor-configurator of windows.

I will share the details of the development process with you, colleagues.


UPD Added screenshots.
UPD2. We are talking about offline, glazed, wooden or plastic windows - through which people look at the street from the house.

Thanks for the feedback!


Business requirements


I interview the customer.

1. This is a module for a site that should work in arbitrary popular cases.
2. In editing mode, the program should allow you to specify the number and location of openings in the windows.
3. In editing mode, the program should allow you to specify the method of opening the openings in the windows, five options: no opening, left, right, left and recline, right and recline.
4. In the display mode, the program should display a window configuration at an arbitrary scale.
5. No need to store and work with information about the size, proportions, color and other characteristics of the window. Pictures should be color and clear. ESKD in this case is not the case.
6. It should not be buggy, stupid, it should be cross-browser, it should work on tablet browsers and smartphones, etc.

At this stage, together with the customer, we search through the Google image search interface for similar products. By searching the sites we find sellers of windows, and we visit a dozen sites to look at the interface of online configurators and in general the range of window configurations. We are discussing what should be with us and what should not be.

TU and TK


Now we supplement business requirements with technical conditions in order to formulate the technical task as a result.
1. Based on the requirement of arbitrary scaling - there is an understanding that the graphics should be vector. A cross-browser solution that will satisfy is HTML5 canvas.
2. Obviously, there should be two modes: editing mode and display mode.
3. In edit mode, data should be stored in input type = hidden. I will not make changes to the CMS - why do I need extra bunts? I’ll just add one field to the forms for adding and editing, to the DBMS and to the corresponding models (for me it really happens in one step, if you don’t, it probably makes sense to reconsider the structure of the program).
4. In the editing mode, the previously created visual configuration of the window should be restored from the data located and substituted automatically in the input type = hidden field.
5. In the display mode, the CMSka will give the data as a property of some div, and my program should have this data: a) detect, b) draw a window on them.
In this case, I will not do the specification, but I will follow the path of least resistance. A good part of the vision of the solution is already present, so I will start the implementation immediately.

Development


Harsh programming reality: I don’t want to complicate my life, and therefore I initially create scalable and maintainable solutions. Therefore, DRY, therefore abstractions and layers - right away, by default.

When I looked through window varieties, I sketched a small catalog in a notebook with a pencil in order to understand what was to be drawn. When I did these sketches, the understanding came that I did not want to do this in CSS (probably in vain), and continue to work with.
I'm going to look for a library for working with canvas. I find calebevans.me/projects/jcanvas , quickly go through the documentation, evaluate the quality of the source and understand that this is what I need now.
I understand that drawing will be the lowest level function. And in general, I have long wanted to draw. I try several functions on the documentation, I find examples online in the sandbox. Everything works, everything suits.

Start drawing


I will create a base function for drawing a window.
function windows_init(selector)
{
	window_canvas = $('').
		attr('width',window_width).
		attr('height',window_height).
		attr('background','blue').
		insertAfter(selector);
}

Naturally, functions do not store parameters (this is called data). Inside functions are variables.
At that moment, conscience did not wake up, so they are in the global field of visibility. If she wakes up, I’ll just put everything in class. If I wake up at the same time as laziness (or common sense) - I will write in CoffeeScript. Now the stars are in a certain position, and there is some understanding that the final product will be a small program consisting of a dozen functions jQuery, and therefore the feasibility of such actions is currently not considered. First make it work. Refactoring - then.
Looking at my sketches, I see that I can draw window openings like rectangles, and designate opening with the help of smooth broken lines inside them.

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,
	}); 
}


Now - the lines denoting the opening. Left - left, right - right, tilt - recline. There is no case with the transom down (I asked again when I interviewed the customer), so I won’t bother now. If a need arises, then you can easily add it.
// 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,
	});
}


I am writing some very quick tests to try this. Everything works, so I move on.

Window views


Actually, according to the configuration of the openings, all windows can be divided into “vertical” (as is usually done in apartments), T-shaped. Less common are “horizontal” - in entrances and in institutions.
First, I’ll draw something simpler. The leafs parameter is the number of openings.
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);
	}
}


Through a little debugging and a series of small tests, I bring the function to the working form.
I pass the parameters with my hands and call the functions that draw the opening so that broken lines are displayed on top.
I rotate 90 degrees and get a “horizontal” window.
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);
	}
}


I'm testing, getting performance.
A beautiful proportion is 1 to 2. Since in business requirements there is an instruction not to bother with proportions, for a T-shaped window I will make such a design.
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);
	}
}


I do tests, make everything work smoothly, without jerking.

Catalog


I will draw all kinds of windows with which the program should work.

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
	}
}


The seventh parameter and understanding of its contents were added later. Just ignore him now.
And I will add to the function responsible for drawing the window sash a callback on click. The intermediate version of the code has not been preserved - taking a good overclock, I forgot to make frequent commits, so I will show the final version.

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)
			}
	}); 
}


And a function that catches a click on the shutter of a large window or a small window in a directory.

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);
}


There was an idea to make separate callbacks, but in the process of reasons I did not find any extra work to do.
Added a dispatcher function, for convenience.

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;
	}
}


Sash opening switching


Opening the sash will switch by clicking. What could be easier?
I will save the list of leaves in the array, and I will determine in the second array the possibilities for opening them.
// window opening
var window_opening = [];
var opening_order = ['none', 'left tilt', 'right tilt', 'left', 'right'];

Fill the array with default data. Not the best option, but at the time of writing I was thinking about something else - about the likely preservation of data.
function set_opening(leaf_count)
{
	for (var i = 0; i < leaf_count; i++)
	{
		window_opening.push(opening_order[0]);
	}
}


Clicking should change the opening of the sash. In the cycle of opening possibilities: no, left, right, left and leans back, right and leans back.
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();
}


And then, without going far ...

Preservation


Data after editing must be saved.
I'll do the serialization by hand.
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);
}


And, now no one bothers to draw windows from saved data.

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]);
}


The configuration of windows can be drawn in the lists of orders, it is very convenient. Small pictures.
function small_window_from_string(element, string, width, height)
{
	if ( ! string.length)
	{
		return;
	}
	var small_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;
	}
}


When to draw?


The program must somehow understand that it is time to draw windows.
Based on the TOR, there are two options - the form field and
in any place.
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 = $('').
		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 = $('').
		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).


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


Красота!


There were no changes to the CMS. A window is added and edited in a hidden field, drawn in a div. It turns out that the window configurator can be pushed into an arbitrary wordpress - just by connecting this script.

Currently, thanks to this solution, a lot of new windows have been sold, ordered and installed.

It would be nice to put this code in some sandbox, along with tests. How do you think?

Report comments in PM.

Thanks!

Also popular now: