საერთო კოდი ბრაუზერსა და node.js აპლკაციაში

წინა პოსტებში ვახსენე რომ ჯავასკრიპტზე მომიწია ანიმაციის ლოგიკის წერა. იგივე კოდი (მათ შორის გეომეტრიაც) ბექშიც მჭირდებოდა, ამიტომ ვიფიქრე – რატომ გადავწერო.. ბექს node.js-ით გავაკეთებ და იგივე კოდს გამოვიყენებ. შესაბამისად თავსებადობის ამოცანა გამიჩნდა.

ცხადია, პირველ რიგში საერთო ბიბლიოთეკებიდან უნდა გამომეყო ბრაუზერთან დაკავშირებული კოდი (DOM-ის მანიპულაცია, ვორკერების ურთიერთობა, ა.შ.). მეორე რიგში node.js-ს ცოტა განსხვავებული მეთოდი აქვს სკრიპტების იმპორტისთვის. თუ ბრაუზერში <script> ტეგს ვიყენებთ, node.js-ში მოდულები უნდა გავაკეთოთ და require მეთოდით ჩავტვირთოთ.

მაგალითად, მარტივი მოდული ასე გამოიყურება (ფაილის სახელია Square.js):

exports.Square = function (side){
	this.side = side;
	this.getArea = function(){
		return side * side;
	}
}

module.exports ან პირდაპირ exports მეთოდს ვიყენებთ იმისთვის რომ ობიექტები გარედან ხელმისაწვდომი გავხადოთ. ამ ფაილში აღწერილი სხვა დანარჩენი ფუნქციები და ცვლადები private იქნება გარე სამყაროსთვის.

მოდულის გამოყენების მაგალითი სხვა ფაილში:

var module = require('./Square');
var square = new module.Square(10);
console.log(square.getArea());

Square.js ფაილი ბრაუზერისთვის თავსებადი რომ გავხადოთ, exports მეთოდს რამე უნდა მოვუხერხოთ.
მაგალითად, შეიძლება შემოწმების ჩამატება:

var Square = function (side){
	this.side = side;
	this.getArea = function(){
		return side * side;
	}
}

if (typeof exports !== 'undefined') {
	exports.Square = Square;
}

აქ კიდევ ერთი მომენტია. როგორც ზევით აღვნიშნეთ, node.js-ში exports-ის გარეთ რაც რჩება, ხელმისაწვდომი არ არის გარედან. ბრაუზერის შემთხვევაში კი Square გლობალური ცვლადი გამოდის. სწორი იქნებოდა, რომ მისი წვდომაც შეგვეზღუდა. მაგალითად ასე:

(function(exports){
	var Square = function (side){
		this.side = side;
		this.getArea = function(){
			return side * side;
		}
	}

	if (typeof exports !== 'undefined') {
		exports.Square = Square;
	}	
})(typeof exports !== 'undefined' ? exports : this['module'] = {});

 

RequireJS + node

არის კიდევ ერთი გზა. ყოველთვის მინდოდა Require.js ბიბლიოთეკის დატესტვა. როგორც აღმოჩნდა მაგას ადაპტერიც აქვს nodejs და მისნაირებისთვის. ასე რომ, ავდექი და Require-ზე გადავაწყე პროექტი. შედეგად ერთნაირი ფორმატით შემიძლია ვწერო მოდულები ფრონტისთვისაც და ბექსითვისაც. თვითონ ბიბლიოთეკაც საკმაოდ მომეწონა. ძალიან მარტივად გადაეწყო ჩემი კოდი და პირველივე გაშვებაზე იმუშავა რაც არ უნდა უცნაური იყოს.

ზევით აღწერილი მოდულის მაგალითი Require.js-ით:

define(function(){
	return function Square(side){
		this.side = side;
		this.getArea = function(){
			return side * side;
		}
	}
});

მოდულის გამოყენების მაგალითი:

requirejs(['Square'], function(Square) {
    var square = new Square(10);
	console.log(square.getArea());
});

JavaScript animation / game loop (ნაწილი 2)

ამ პოსტის წინა ნაწილში requestAnimationFrame მეთოდზე ვისაუბრე, რომლის საშუალებით ბრაუზერის რენდერინგს შეგვიძლია ჩვენი ქოლბექი მივაბათ და შესაბამისად მაგ დროს განვაახლოთ ანიმაციის ან თამაშის მდგომარეობა.

მინდა აღვნიშნო, რომ ამ მეთოდში ჯობია მხოლოდ გადახატვის ლოგიკა იყოს, ხოლო ანიმაციის ან თამაშის ლოგიკის (მდგომარეობების გამოთვლა, ა.შ.) ადგილი იქ ცალსახად არ არის. ამას ბევრი მიზეზი აქვს: ბრაუზერი გვერდის რენდერის დროს სხვადასხვა ფაქტორს ითვალისწინებს – მაგალითად ტაბი აქტიურია თუ არა, ლეპტოპის აკუმულატორი რა დონეზეა, მონიტორი რა სიხშირით ახლდება, ა.შ. შესაბამისად შეიძლება ჩვენი ქოლბექების გამოძახების რაოდენობა შეამციროს ან გაზარდოს დროის კონკრეტულ პერიოდში.

აქედან გამომდინარე, უმჯობესია რომ ანიმაციის ან თამაშის მდგომარეობები ცალკე გამოითვალოს / დაგენერირდეს და requestAnimationFrame-ის ქოლბექში მხოლოდ მათი რენდერის კოდი ჩაიწეროს.

ცალკე გენერაციაც ბევრნაირად შეიძლება. რადგან ეგ მნიშვნელოვნად განსაზღვრავს ჩვენი ანიმაციის წარმადობას, ბევრი ვიფიქრე როგორ გამეკეთებინა და ახლაც არ ვარ დარწუნებული ჩემი კოდის სისწორეში. სხვის მიდგომებს მოვიყვან მაგალითად:

1. შეიძლება setInterval და setTimeout მეთოდებით დაგენერირდეს მდგომარეობები, ხოლო requestAnimationFrame-ის ქოლბექმა აარჩიოს იქიდან შესაბამისი მდგომარეობა და ის დაარენდეროს.

2. პირველი პუნქტის ანალოგიურად მდგომარეობები დროის რაღაც ინტერვალში დაგენერირდეს, თუმცა ეს ლოგიკა ცალკე ნაკადში – მუდმივად გაშვებულ web worker-ში იყოს გატანილი, რომ მთავარი ნაკადი ძალიან არ დაიტვირთოს (ჩემი აზრით ეს უკეთესია). requestAnimationFrame-მა კი worker-დან მიღებული მდგომარეობებიდან შეარჩიოს დროის შესაბამისად და დაარენდეროს გარემო.

3. ეს მეორე პუნქტის მსგავსია, თუმცა setInterval-ის ნაცვლად ინიციატორი requestAnimationFrame-ის ქოლბექი იყოს. მის გამოძახებაში დამატებით იყოს worker-თან შეტყობინების გაგზავნა, რომ ვორკერმა მომდევნო X რაოდენობის მდგომარეობა დააგენერიროს. ასეთ შემთხვევაში, თუ ბრაუზერი არ არენდერებს არაფერს, ჩვენი ნაკადი ტყუილად არ იქნება მუდმივად გაშვებული თავისი მძიმე კალკულაციებით.

ჩემს შემთხვევაში საქმე უფრო ანიმაციასთან მაქვს და არა real-time თამაშთან. მდგომარეობები შემიძლია წინასწარვე გამოვთვალო, ამიტომ ასეთი გზა ავირჩიე: worker-ს ვუშვებ რომელიც მთლიადანვე აგენერირებს ყველა მდგომარეობას და თითოეულ მათგანს უკან აბრუნებს მთავარ ნაკადთან. requestAnimationFrame-ის ქოლბექი ტაიმსტემპს უყურებს და მაგის შესაბამისად გავლილ მდგომარეობებს არენდერებს.

ამდენი მდგომარეობების გენერაცია, აქეთ იქით გზავნა და შენახვა დიდად არ მომწონს, რადგან აზრზე არ ვარ ობიექტების რაოდენობის ზრდასთან ერთად რა მოხდება, თუმცა ჯერ ჯერობით უკეთესი გზა არ ვიცი.

JavaScript animation / game loop (ნაწილი 1)

იმ დღეს ერთ-ერთ ვებ აპლიკაციაში არც თუ ისე მარტივი ანიმაციის გაკეთება მომინდა და ბოლოს ჯავასკრიპტამდე მივედი. თუმცა რამდენიმე გზის გამოკვლევა დამჭირდა და გადავწყვიტე ამ თემაზე პოსტი კარგი იქნებოდა. იქნებ ვინმეს უკეთესი იდეა ჰქონდეს…

ჩემი პირველი ნაბიჯი CSS3-ის გამოცდა იყო. მაგის ანიმაციების მოდული ძალიან მომწონს და ვიფიქრე იქნებ როგორმე მის ფარგლებში ჩავმჯდარიყავი, მაგრამ როცა ანიმირებულ ელემენტებს შორის კოლიზიების დადგენა დამჭირდა, გავიჭედე :)

კოლიზიის შესახებ ინფორმაციას ბრაუზერი ვერ მაძლევს css ანიმაციის დროს (და არც მაქვს მაგის მოთხოვნის უფლება). შესაბამისად, როგორღაც მოძრავი ობიექტების ადგილსამყოფელის გაკონტროლება მჭირდებოდა. ეს ნიშნავს რომ რაღაც ინტერვალით ვამოწმო თითოეული მათგანის კოორდინატი და თანაკვეთა შევამოწმო, ამასთან ინტერვალებში არ გადავახტე კოლიზიის მომენტს.

როგორც აღმოჩნდა, მოძრავი ობიექტის კოორდინატებიც არ არის ზედაპირზე. თუ ვინმეს დაგჭირდებათ, ვებკიტში არის ერთი ადგილი საიდანაც მათი აღება შეიძლება – ეს არის ტრანსფორმაციის მატრიცა, რომელიც ახალი მდგომარეობის გამოსათვლელად გამოიყენება. ეს მატრიცა შეიცავს მიმდინარე კოორდინატების ინფორმაციასაც. მაგალითად:

var animatedElement = document.getElementById("animatedElement");
var transform = window.getComputedStyle(animatedElement).webkitTransform;
var curTransform = new WebKitCSSMatrix(transform);
console.log(animatedElement.offsetLeft + curTransform.m41);
console.log(animatedElement.offsetTop + curTransform.m42);

ამიტომ გადავწყვიტე ჩემს შემთხვევაში მხოლოდ CSS3-ის იმედზე ვერ ვიქნებოდი და ისევ ჯავასკრიპტს მოვკიდე ხელი. იდეაში საჭიროა შემდეგი: უნდა გავაკეთო კადრების ციკლი. დროის პატარა ინტერვალებში უნდა მოხდეს ელემენტების მდგომარეობის / მდებარეობის განახლება და შესაბამისად გადახატვა. კოლიზიების შემოწმებაც მანდ შეიძლება – მდგომარეობის გენერაციის დროს.

წინათ ჯავასკრიპტის ანიმაცია რომ გაგეკეთებინათ, ერთმანეთში ჩადგმული და გადაბმული setTimeout ან setInteval მეთოდებით მოგიწევდათ ანიმაციის ციკლის შეკვრა. მაგალითად, ასე:

function draw(){
    console.log('timestamp ' + +new Date());
    // update positions and redraw frame
}
setInterval(draw, 10);

ყოველ 10 მილიწამში მოხდება draw მეთოდის გამოძახება და მაგ მეთოდში იქნებოდა ანიმაციის ახალი მდგომარეობის გენერაცია.

ზოგი ახლაც იყენებს ამ გზას მარტივი ამოცანების გადასაჭრელად, თუმცა გასათვალისწინებელია მათი პრობლემები:

1. არაზუსტი დრო. დაყოვნება, რომელსაც ამ მეთოდებს (setTimeout ან setInteval) მილიწამების სახით გადავცემთ არგუმენტად, მხოლოდ იმას ნიშნავს, რომ ჩვენი ფუნქციები ამ დროის შემდეგ ბრაუზერის UI thread queue-ში ჩაემატებიან და თავის რიგს დაელოდებიან შესასრულებლად. შესაბამისად თუ UI ნაკადი სხვა საქმითაა დაკავებული, ჩვენი ანიმაციის ფრაგმენტი ზუსტად საჭირო დროს ვერ შესრულდება.

2. Browser timer resolution. მისი ქართული შესატყვისი ვერ მოვიფიქრე – ეს არის დრო, რამდენ ხანში ერთხელაც ახლდება საათი. მაგალითად, წინათ ბრაუზერები OS-ის სისტემურ დროს უყურებდნენ, რომელსაც ეს რეზოლუცია გაცილებით დიდი ჰქონდა (მაგალითად 10-15 მილიწამი), შესაბამისად რამდენიმე მილიწამის მითითების შემთხვევაში, შედეგის განსაზღვრა ვერ ხდებოდა. დღეს მთლად ასე აღარ არის, მაგრამ ამაზე ლაპარაკი შორს წამიყვანს.

მოკლედ რომ ვთქვათ, არ იყო გარანტია თუ როდის გადახატავდა ბრაუზერი ეკრანს.
შემდეგ მოზილას ერთ-ერთი თანამშრომელი მივიდა ახალ იდეამდე და Firefox-ში ჩაემატა მეთოდი mozRequestAnimationFrame, რომელიც შემდეგ სხვა ბრაუზერებმაც გადაიღეს და სპეციფიკაციაში requestAnimationFrame სახელით შევიდა.

ამ მეთოდს მხოლოდ ერთი პარამეტრი აქვს – თქვენი ფუნქცია, რომელსაც ბრაუზერი ეკრანის გადახატვამდე გამოიძახებს. ხოლო თქვენს ქოლბექს გამოძახების დროს გადმოეცემა მაღალი სიზუსტის ტაიმსტემპი – დრო როცა requestAnimationFrame-მა ქოლბექების გამოძახება დაიწყო მიმდინარე კადრისთვის.

მაგალითად:

function step(timestamp) {
    console.log(timestamp);
    requestAnimationFrame(step);
}
requestAnimationFrame(step);

requestAnimationFrame-ს გამოძახებით მხოლოდ მომდევნო გადახატვაზე გამოიძახება ქოლბექი. რადგან ჩვენ ანიმაციის ციკლი გვჭირდება, მოგვიწევს რომ ქოლბექში შიგნით ხელახლა მივებათ ბრაუზერის მოვლენას.

ეს მაღალი სიზუსტის ტაიმსტემპი არის DOMHighResTimeStamp ობიექტი და განსხვავდება ჩვეულებრივი Date.now() ფუნქციის დასაბრუნებელი მნიშვნელობისგან. თუ მაგ ფორმატის მიმდინარე ტაიმსტემპი გჭირდებათ, მისი მიღება შეიძლება performance.now() მეთოდით.

CNC მანქანების დაპროგრამება

ორი დღის წინ თსუ-ში მაგისტრატურა დავამთავრე. ჩემი ბოლო სემესტრში შესრულებული კვლევა CNC მანქანებს და მათ დაპროგრამებას ეხებოდა. თემა არც ისე ახლოს არის ჩემს საქმიანობასთან, მაგრამ მაინც საინტერესო იყო ჩემთვის. განსაკუთრებით კი ექსპერიმენტები ამ მანქანების ახალი პროგრამირების ენით, რომლის შესახებაც უფრო დაწვრილებით თემაში მიწერია.

რამდენადაც ვიცი, საქართველოშიც არის პატარა საწარმოები, რომლებიც ასეთი ტიპის მანქანებს იყენებენ, თუმცა პასუხი ვერ მივიღე მათგან.

შემთხვევით ვინმეს ხომ არ ჩაგიტარებიან ექსპერიმენტები ან სახლში აგიწყვიათ მსგავის მანქანა? თემის ბლოგზე გამოქვეყნება მე მგონი შეიძლება, რადგან თსუ-ს კონფერენციის საიტზე სახალხოდ დევს. თუ დაგაინტერესებთ ჩემი მიმოხილვა, ფაილი ამ ბმულზეა :)

lcamtuf_robot1

ყალიბის გაკეთება რობოტის ნაწილებისთვის. ფოტო აღებულია lcamtuf-ის საიტიდან.

othermill

Othermill კომპანია otherfab-ისგან

მრავალგანზომილებიანი მასივი Java-ში

გუშინ ერთ-ერთ ამოცანაში მეხსიერების ლიმიტს ვაჭარბებდი იმის გამო, რომ არასწორად მქონდა აღწერილი მრავალგანზომილებიანი მასივი. c-ში და c++-ში ამის პრობლემა არ იქნებოდა, თუმცა, იმის გამო, რომ ჯავაში ყველაფერი ობიექტია (პრიმიტიული ტიპების გარდა), მრავალგანზომილებიანი მასივების შექმნისას მეხსიერება განსხვავებული გზით უნდა დავითვალოთ.

მაგალითად ასეთი მასივის ზომა c++-ში 96 ბაიტია (32 ბიტიან სისტემაში):
int arr[2][3][4];
2 * 3 * 4 = 24. თითოეული კიდევ 4 ბაიტიანია int ტიპის გამო და ჯამში 24 * 4 = 96 ბაიტი გამოდის.
int arr[4][3][2] ასეთი აღწერის დროს შედეგი იგივეა.

შევადაროთ Java-ში ეს ორი აღწერა:
int arr[][][] = new int[40000][200][2];
int arr[][][] = new int[2][200][40000];

აზრობრივად განსხვავება არ არის, თუმცა პირველი გზა გაცილებით მეტ მეხსიერებას მოიხმარს. აი, რატომ:
1. ჯავას მასივი თავისთავად ობიექტია. თითოეულ ობიექტში (არა reference-თან, არამედ heap-ში) დამატებით რამდენიმე ბაიტს იკავებს სპეციალური object header ინფორმაცია. object header-ში ინახება ვირტუალური მანქანისთვის საჭირო მონაცემები, რასაც garbage collector-ისთვის და სხვადასხვა მიზნებისთვის იყენებს. როგორც ვიცი, ჩვეულებრივ ეს 8 ბაიტია ხოლმე 32-ბიტიან მანქანაზე და 16 ბაიტი 64-იანზე. ამის გარდა, მასივის ობიექტში ინახება მასივის ზომაც, ანუ დამატებით 4 ბაიტი. კიდევ შეიძება მეხსიერებაში padding-ს დასჭირდეს რამდენიმე ბაიტი. ამიტომ ზუსტად არ დავთვლი და ვთქვათ, ერთი მასივის ობიექტისთვის (ელემენტების გარდა) საჭიროა X ბაიტი.

2. int[a][b] – ჯავაში ეს არის a ცალი b ელემენტიანი მასივის მასივი, ანუ სინამდვილეში ერთის მაგივრად a+1 ობიექტი გვაქვს.
int[a][b][k]სამგანზომილებიანის შემთხვევაში a * b + a + 1 ცალი ობიექტი და ა.შ.

ახლა გამოვთვალოთ და შევადაროთ ზევით ნახსენები ორი მასივის ზომა:
(40,000 * 200 + 40,000 + 1) * X + (40,000 * 200 * 2 * 4)
(2 * 200 + 2 + 1) * X + (40,000 * 200 * 2 * 4)

მეორე ნაწილი რაც int ტიპის ელემენტების ზომას ითვლის, ცხადია, ორივესთვის ერთი და იგივე იქნება.
პირველი ნაწილის მიხედვით კი 8,039,598 ცალი ზედმეტი ობიექტი იქმნება პირველ შემთხვევაში, მეხსიერებაც შესაბამისად გაცილებით მეტი სჭირდება.

ისე, პროფაილერით ვერ ვხედავ სინამდვილეში მართლაც ამდენი ობიექტია თუ არა და იდეა ხომ არ გაქვთ როგორ შეიძლება შევამოწმო?