📜 ⬆️ ⬇️

Visual windows configurator written in one hour

Solved an interesting task - 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 they look out of the house
')
Thanks for the feedback!


Business requirements


Interview 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 the editing mode, the program should allow you to specify the way of opening openings in the windows, five options: no opening, left, right, left and leans back, right and leans.
4. In the display mode, the program should display the window configuration in 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 colored and understandable. ESKD in this case is not in the business.
6. Should not be buggy, stupid, should be cross-browser, should work on tablet PC browsers and smartphones, etc.

At this stage, we, together with the customer, search Google for images on the interface of similar products. Searching sites we find window sellers, and we visit dozens of sites to look at the interface of online configurators and in general an assortment of window configurations. We are discussing what we should have and what should not be.

TU and TZ


Now we supplement the business requirements with technical specifications in order to finally form a technical task.
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 - HTML5 canvas.
2. Obviously, there should be two modes: edit mode and display mode.
3. In edit mode, the data should be saved to input type = hidden. I will not make changes to the CMS - why do I need extra smut? 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 action, if you don’t, then it probably makes sense to revise the structure of the program).
4. In the edit mode, the previously created visual configuration of the window should be restored from the data located and automatically inserted into the input type = hidden field.
5. In the display mode, CMS will give the data as a property of some div, and my program should have this data: a) detect, b) draw a window using it.
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 at the moment, so I will start the implementation immediately.

Development


Harsh programmer reality: I do not want to complicate my life, and therefore I initially create scalable and accompanied solutions. Therefore DRY, therefore abstractions and layers - right away, by default.

When I looked through the varieties of windows, I sketched a small catalog in my notebook with my pencil in order to understand what was to be drawn. When I made these sketches, it came to be understood that I did not want to do it on CSS (probably in vain), and continue to work with <canvas />.
I'm going to look for a library to work with canvas. I find calebevans.me/projects/jcanvas , skim through the documentation, evaluate the quality of the source code 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 documentation functions, find examples online in the sandbox. Everything works, everything suits.

Start drawing


I will create a function-basis for drawing the window.
function windows_init(selector) { window_canvas = $('<canvas></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 the functions are variables.
At that moment, conscience did not wake up, so they are in the global scope. If she wakes up - just put everything in the classroom. If I wake up simultaneously with laziness (or common sense), I will write in CoffeeScript. Now the stars have risen to a certain position, and there is some understanding that the final product will be a small program consisting of a dozen jQuery functions, and therefore the expediency of such actions is simply not considered at the moment. First to make it work. Refactoring - then.
Looking at my sketches, I see that I can draw window openings, like rectangles, and mark the 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 - lines denoting the opening. Left - to the left, right - to the right, tilt - tilting. There is no case with transom down (I asked again when I interviewed the customer), so I won't bother now. If the need arises, then it will be easy to 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 move on.

Types of windows


Actually, by the configuration of the openings all windows can be divided into “vertical” (as they usually do in apartments), T-shaped. Less common are "horizontal" - in the hallways and in institutions.
First 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 small debugging and a series of small tests I bring the function to the working form.
I hand over the parameters and call the functions that draw the opening - in order to display broken lines from above.
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, working hard.
A beautiful proportion is 1 to 2. Since there are instructions in the business requirements not to bother with the proportions, for a T-shaped window I will make this 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 jerks.

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 content was added later. Just ignore him now.
And I will add to the function, those responsible for drawing the casement window, callback per click. The intermediate version of the code was not preserved - by taking a good overclocking, I forgot to do frequent commits, so I’ll 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 sash of a large window or a small window in the 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 I did not find any reasons for doing extra work.
Added 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 switch


The opening of the leaves will switch by clicking. What could be easier?
I will save the list of valves in the array, and define the possibilities for opening them in the second array.
 // 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]); } } 


At the click, the opening of the sash should change. In the cycle of the possibilities of opening: 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 will do 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 = $('<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; } } 


When to draw?


The program must somehow understand that it is time to draw windows.
Based on the TK, there are two options - the form field and <div /> in an arbitrary 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()) }); } 


Perhaps input [name = "window_type"] is not the best solution. Just for this moment I had a goal to launch the program into work, and I didn’t want to modify CMS at all - so I taught the plugin to search for its field by its name: windows_type.

If you make a library out of this program, you need to put the selector in a variable. And be sure to wrap it in a class to close the namespace, etc.

Total


Here is the revised code. This is beta, and she went into production without any changes.
 $(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(); } 


What is not shown in the article. The windows_handler function is triggered by another JS component, based on two events: document.ready and successful loading of the Ajax data. Thus, the windows are drawn immediately after the page loads, and redrawn if interactive data refresh occurs (“live mode”).
All user cases are performed. I made a simple test with a lot of redrawing without rebooting, left the car running with chrome and mobile for a while - the memory is not burning. I drove the same test for several hours in chrome and in a safari on the iPad and MacBook. No problems detected.

Screenshots


A small picture is created on the client on the fly (it prints perfectly)


Big picture. Dimensions can be adjusted, someday.


In edit mode. Clicking on a small window in the directory changes the configuration of a large (and immediately the data in input type = hidden).


Clicking on the sash of a large window changes the sash opening.


Beauty!


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

At the moment, thanks to this decision, a lot of new windows have been sold, ordered and installed.

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

Report remarks in lichku.

Thank!

Source: https://habr.com/ru/post/238065/


All Articles