bugfix : XMLHttpRequest.status est un entier.
[Portfolio.git] / skins / photo_film_viewer.js
1 /*
2 copyright 2008-2014 Benoit Pin - Centre de recherche en informatique - MINES ParisTech
3 http://plinn.org
4 Licence Creative Commons http://creativecommons.org/licenses/by-nc/2.0/
5 */
6
7 var FilmSlider;
8
9 (function(){
10
11 var keyLeft = 37, keyRight = 39;
12 var isTextMime = /^text\/.+/i;
13 var isAddToSelection = /.*\/add_to_selection$/;
14 var imgRequestedSize = /size=(\d+)/;
15 var DEFAULT_IMAGE_SIZES = [500, 600, 800];
16
17 FilmSlider = function(filmBar, slider, ctxInfos, image, toolbar, breadcrumbs) {
18 var thisSlider = this;
19 this.filmBar = filmBar;
20 var film = filmBar.firstChild;
21 if (film.nodeType === 3) { film = film.nextSibling; }
22 this.film = film;
23 this.slider = slider;
24 this.rail = slider.parentNode;
25 this.sliderSpeedRatio = undefined;
26 this.sliderRatio = undefined;
27 this.selectedSlide = undefined;
28 this.selectedSlideInSelection = undefined;
29 this.cartSlide = document.getElementById('cart_slide');
30 this.image = image;
31 this.stretchable = image.parentNode;
32 this.viewMode = 'medium';
33
34 this.buttons = [];
35 this.toolbar = toolbar;
36 if (breadcrumbs) {
37 var bcElements = breadcrumbs.getElementsByTagName('a');
38 this.lastBCElement = bcElements[bcElements.length-1];
39 var imgSrcParts = image.src.split('/');
40 this.lastBCElement.innerHTML = imgSrcParts[imgSrcParts.length-2];
41 this.hasBreadcrumbs = true;
42 }
43 else {
44 this.hasBreadcrumbs = false;
45 }
46
47 var buttons = toolbar.getElementsByTagName('img');
48 var b, name, i;
49 for (i=0 ; i<buttons.length ; i++) {
50 b = buttons[i];
51 name = b.getAttribute('name');
52 if (name) { this.buttons[name] = b; }
53 }
54
55 this.pendingImage = new Image();
56 this.pendingImage.onload = function() {
57 thisSlider.refreshImage();
58 };
59 this.initialized = false;
60
61 this.film.style.left='0';
62 this.film.style.top='0';
63
64 this.filmLength = ctxInfos.filmLength;
65 this.center = ctxInfos.center;
66 this.slideSize = ctxInfos.slideSize;
67 this.ctxUrlTranslation = ctxInfos.ctxUrlTranslation;
68
69 this.ddHandlers = {'down' : function(evt){thisSlider.mouseDownHandler(evt);},
70 'move' : function(evt){thisSlider.mouseMoveHandler(evt);},
71 'up' : function(evt){thisSlider.mouseUpHandler(evt);},
72 'out' : function(evt){thisSlider.mouseOutHandler(evt);}
73 };
74
75 this.resizeSlider();
76 this.addEventListeners();
77 };
78
79
80 FilmSlider.prototype.resizeSlider = function(evt) {
81 var filmBarWidth = getObjectWidth(this.filmBar);
82 if (!filmBarWidth) {
83 var thisSlider = this;
84 addListener(window, 'load', function(evt){thisSlider.resizeSlider(evt);});
85 return;
86 }
87
88 var filmWidth = this.slideSize * this.filmLength;
89 var sliderRatio = this.sliderRatio = filmBarWidth / filmWidth;
90 var sliderWidth = filmBarWidth * sliderRatio;
91 this.rail.style.width = filmBarWidth + 'px';
92 this.rail.style.display = 'block';
93 this.rail.style.visibility = 'visible';
94 if (sliderRatio < 1) {
95 this.slider.style.width = Math.round(sliderWidth) + 'px';
96 this.slider.style.visibility = 'visible';
97 }
98 else {
99 this.slider.style.visibility = 'hidden';
100 }
101
102 this.winSize = {'width' : getWindowWidth(),
103 'height' : getWindowHeight()};
104 this.maxRightPosition = filmBarWidth - sliderWidth;
105 this.sliderSpeedRatio = - (filmBarWidth - sliderWidth) / (filmWidth - filmBarWidth);
106 if (!this.initialized) {
107 this.centerSlide(this.center);
108 this.selectedSlide = this.filmBar.getElementsByTagName('img')[this.center].parentNode;
109 this.initialized = true;
110 }
111 };
112
113 FilmSlider.prototype.fitToScreen = function(evt) {
114 this._fitToScreen();
115 var thisSlider = this;
116 addListener(window, 'resize', function(evt){thisSlider._fitToScreen();});
117 };
118
119 FilmSlider.prototype._fitToScreen = function(evt) {
120 var wh = getWindowHeight();
121 var rb = getObjectTop(this.rail) + getObjectHeight(this.rail); // rail bottom
122 var delta = wh - rb;
123 var sh = getObjectHeight(this.stretchable);
124 var newSize = sh + delta;
125 this.stretchable.style.height = newSize + 'px';
126
127 var ratio = this.image.height / this.image.width;
128 var bestFitSize = this.getBestFitSize(ratio);
129 var currentSize = parseInt(imgRequestedSize.exec(this.image.src)[1], 10);
130 if (currentSize !== bestFitSize) {
131 var src = this.image.src.replace(imgRequestedSize, 'size=' + bestFitSize);
132 this.pendingImage.src = src;
133 }
134 };
135
136 FilmSlider.prototype.getBestFitSize = function(ratio) {
137 var fw = getObjectWidth(this.stretchable) - 1;
138 var fh = getObjectHeight(this.stretchable) - 1;
139
140 var i, irw, irh;
141 if (ratio < 1) {
142 for (i=DEFAULT_IMAGE_SIZES.length -1 ; i>0 ; i--) {
143 irw = DEFAULT_IMAGE_SIZES[i];
144 irh = irw * ratio;
145 if (irw <= fw && irh <= fh) { break; }
146 }
147 }
148 else {
149 for (i=DEFAULT_IMAGE_SIZES.length -1 ; i>0 ; i--) {
150 irh = DEFAULT_IMAGE_SIZES[i];
151 irw = irh / ratio;
152 if (irw <= fw && irh <= fh) { break; }
153 }
154 }
155 return DEFAULT_IMAGE_SIZES[i];
156 };
157
158 FilmSlider.prototype.centerSlide = function(slideIndex) {
159 if (this.sliderRatio > 1) { return; }
160 var filmBarWidth = getObjectWidth(this.filmBar);
161 var x = slideIndex * this.slideSize;
162 x = x - (filmBarWidth - this.slideSize) / 2.0;
163 x = x * this.sliderSpeedRatio;
164 var p = new Point( -x, 0 );
165 this.setSliderPosition(p);
166 };
167
168 FilmSlider.prototype.setSliderPosition = function(point) {
169 if(point.x < 0) { point.x = 0; }
170 if (point.x > this.maxRightPosition) { point.x = this.maxRightPosition; }
171 this.slider.style.left = point.x + 'px';
172 this.setFilmPosition(point);
173 };
174
175 FilmSlider.prototype.setFilmPosition = function(point) {
176 this.film.style.left = point.x / this.sliderSpeedRatio + 'px';
177 };
178
179 FilmSlider.prototype.getSliderPosition = function() {
180 var x = parseInt(this.slider.style.left, 10);
181 var y = parseInt(this.slider.style.top, 10);
182 var p = new Point(x, y);
183 return p;
184 };
185
186 FilmSlider.prototype.getFilmPosition = function() {
187 var x = parseInt(this.film.style.left, 10);
188 var y = parseInt(this.film.style.top, 10);
189 var p = new Point(x, y);
190 return p;
191 };
192
193 FilmSlider.prototype.loadSibling = function(previous) {
194 var slide = null;
195 if (previous) {
196 slide = this.selectedSlide.parentNode.previousSibling;
197 if (slide && slide.nodeType===3) { slide = slide.previousSibling; }
198 }
199 else {
200 slide = this.selectedSlide.parentNode.nextSibling;
201 if (slide && slide.nodeType===3) { slide = slide.nextSibling; }
202 }
203
204 if (!slide) { return; }
205 else {
206 var target = slide.getElementsByTagName('a')[0];
207 raiseMouseEvent(target, 'click');
208 var index = parseInt(target.getAttribute('portfolio:position'), 10);
209 this.centerSlide(index);
210 }
211 };
212
213 FilmSlider.prototype.addEventListeners = function() {
214 var thisSlider = this;
215 addListener(window, 'resize', function(evt){thisSlider.resizeSlider(evt);});
216 addListener(this.filmBar, 'click', function(evt){thisSlider.thumbnailClickHandler(evt);});
217 addListener(this.toolbar, 'click', function(evt){thisSlider.toolbarClickHandler(evt);});
218 addListener(window, 'load', function(evt){thisSlider.fitToScreen(evt);});
219
220 // dd listeners
221 addListener(this.slider, 'mousedown', this.ddHandlers.down);
222 if(browser.isDOM2Event){
223 if (browser.isAppleWebKit) {
224 this.filmBar.addEventListener('mousewheel', function(evt){thisSlider.mouseWheelHandler(evt);}, false);
225 }
226 else {
227 addListener(this.filmBar, 'DOMMouseScroll', function(evt){thisSlider.mouseWheelHandler(evt);});
228 }
229 }
230 else if (browser.isIE6up) {
231 addListener(this.filmBar, 'mousewheel', function(evt){thisSlider.mouseWheelHandler(evt);});
232 }
233
234 addListener(document, 'keydown', function(evt){thisSlider.keyDownHandler(evt);});
235 addListener(document, 'keypress', function(evt){thisSlider.keyPressHandler(evt);});
236 };
237
238
239 FilmSlider.prototype.mouseDownHandler = function(evt) {
240 this.initialClickPoint = new Point(evt.clientX, evt.clientY);
241 this.initialPosition = this.getSliderPosition();
242 this.dragInProgress = true;
243 addListener(document, 'mousemove', this.ddHandlers.move);
244 addListener(document, 'mouseup', this.ddHandlers.up);
245 addListener(document.body, 'mouseout', this.ddHandlers.out);
246 };
247
248
249 FilmSlider.prototype.mouseMoveHandler = function(evt) {
250 if(!this.dragInProgress) { return; }
251
252 clearSelection();
253 evt = getEventObject(evt);
254 var currentPoint = new Point(evt.clientX, evt.clientY);
255 var displacement = currentPoint.diff(this.initialClickPoint);
256 this.setSliderPosition(this.initialPosition.add(displacement));
257 };
258
259 FilmSlider.prototype.mouseUpHandler = function(evt) {
260 this.dragInProgress = false;
261 evt = getEventObject(evt);
262 this.mouseMoveHandler(evt);
263 };
264
265
266 FilmSlider.prototype.mouseOutHandler = function(evt) {
267 evt = getEventObject(evt);
268 var x = evt.clientX;
269 var y = evt.clientY;
270 if (x < 0 ||
271 x > this.winSize.width ||
272 y < 0 ||
273 y > this.winSize.height
274 ) {
275 this.mouseUpHandler(evt);
276 }
277 };
278
279 FilmSlider.prototype.thumbnailClickHandler = function(evt) {
280 var target = getTargetedObject(evt);
281 while (target.tagName !== 'A' && target !== this.filmBar) { target = target.parentNode; }
282 if (target.tagName !== 'A') { return; }
283 else {
284 if (this.viewMode === 'full') {
285 this.mosaique.unload();
286 this.mosaique = null;
287 this.viewMode = 'medium';
288 }
289 disableDefault(evt);
290 disablePropagation(evt);
291 target.blur();
292 history.pushState(target.href, '', target.href);
293
294 var imgBaseUrl = target.href;
295 var canonicalImgUrl;
296 if (this.ctxUrlTranslation[0]) {
297 canonicalImgUrl = imgBaseUrl.replace(this.ctxUrlTranslation[0],
298 this.ctxUrlTranslation[1]);
299 }
300 else { canonicalImgUrl = imgBaseUrl; }
301
302 var ajaxUrl = imgBaseUrl + '/photo_view_ajax';
303 var thisFS = this;
304
305 //this.pendingImage.src = canonicalImgUrl + '/getResizedImage?size=600';
306 var thumbnail = target.getElementsByTagName('IMG')[0];
307 var bestFitSize = this.getBestFitSize(thumbnail.height/thumbnail.width);
308 this.pendingImage.src = canonicalImgUrl + '/getResizedImage?size=' + bestFitSize;
309
310 // update buttons
311 var fullScreenLink = this.buttons.full_screen.parentNode;
312 fullScreenLink.href = canonicalImgUrl + '/zoom_view';
313
314 var toggleSelectionBtn = this.buttons.toggle_selection;
315 var toggleSelectionLink = toggleSelectionBtn.parentNode;
316 this.selectedSlideInSelection = (target.className==='selected');
317 if (this.selectedSlideInSelection) {
318 toggleSelectionBtn.src = portal_url() + '/unselect_flag_btn.gif';
319 toggleSelectionBtn.alt = toggleSelectionLink.title = 'Retirer de la sélection';
320 toggleSelectionLink.href = canonicalImgUrl + '/remove_to_selection';
321 }
322 else {
323 toggleSelectionBtn.src = portal_url() + '/select_flag_btn.gif';
324 toggleSelectionBtn.alt = toggleSelectionLink.title = 'Ajouter à la sélection';
325 toggleSelectionLink.href = canonicalImgUrl + '/add_to_selection';
326 }
327
328 var showBuyableButtonLink = this.buttons.show_buyable.parentNode;
329 showBuyableButtonLink.href = canonicalImgUrl + '/get_slide_buyable_items';
330 this.cartSlide.innerHTML = '';
331 this.cartSlide.style.visibility='hidden';
332
333
334 var metadataButton = this.buttons.edit_metadata;
335 if (metadataButton) {
336 var metadataEditLink = metadataButton.parentNode;
337 metadataEditLink.href = canonicalImgUrl + '/photo_edit_form';
338 }
339
340
341 var req = new XMLHttpRequest();
342 req.onreadystatechange = function() {
343 switch (req.readyState) {
344 case 1 :
345 showProgressImage();
346 break;
347 case 2 :
348 try {
349 if (! isTextMime.exec(req.getResponseHeader('Content-Type'))) {
350 req.onreadystatechange = null;
351 req.abort();
352 hideProgressImage();
353 window.location.href = thisFS._fallBackUrl;
354 }
355 }
356 catch(e){}
357 break;
358 case 4 :
359 hideProgressImage();
360 if (req.status === 200) { thisFS.populateViewer(req); }
361 break;
362 }
363 };
364
365 req.open("GET", ajaxUrl, true);
366 req.send(null);
367
368 // update old displayed slide className
369 var className = this.selectedSlide.className;
370 var classes = className.split(' ');
371 var newClasses = [];
372 var name, i;
373
374 for (i=0 ; i<classes.length ; i++) {
375 name = classes[i];
376 if (name !== 'displayed') {
377 newClasses.push(name);
378 }
379 }
380
381 this.selectedSlide.className = newClasses.join(' ');
382
383 // hightlight new displayed slide
384 this.selectedSlide = target;
385 className = this.selectedSlide.className;
386 classes = className.split(' ');
387 classes.push('displayed');
388 this.selectedSlide.className = classes.join(' ');
389 }
390 };
391
392 FilmSlider.prototype.toolbarClickHandler = function(evt) {
393 var target = getTargetedObject(evt);
394 var button, link, url;
395 if(target.tagName === 'IMG' && target.getAttribute('name')) {
396 switch(target.getAttribute('name')) {
397 case 'previous' :
398 disableDefault(evt);
399 disablePropagation(evt);
400 button = target;
401 link = button.parentNode;
402 link.blur();
403 this.loadSibling(true);
404 break;
405 case 'next' :
406 disableDefault(evt);
407 disablePropagation(evt);
408 button = target;
409 link = button.parentNode;
410 link.blur();
411 this.loadSibling(false);
412 break;
413 case 'full_screen':
414 disableDefault(evt);
415 disablePropagation(evt);
416 target.parentNode.blur();
417 if (this.viewMode === 'full') {
418 this.mosaique.unload();
419 this.mosaique = null;
420 this.viewMode = 'medium';
421 return;
422 }
423 var main = document.getElementById('photo_viewer');
424 url = target.parentNode.href;
425 url = url.substring(0, url.length - '/zoom_view'.length);
426 var margins = {'top':0, 'right':-1, 'bottom':0, 'left':0};
427 this.mosaique = new Mosaique(main, url, margins);
428 this.viewMode = 'full';
429 break;
430
431 case 'toggle_selection':
432 disableDefault(evt);
433 disablePropagation(evt);
434 button = target;
435 link = button.parentNode;
436 link.blur();
437
438 var req = new XMLHttpRequest();
439 url = link.href;
440 req.open("POST", url, true);
441 req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
442 req.send("ajax=1");
443
444 // toggle button
445 var parts = url.split('/');
446 var canonicalImgUrl = parts.slice(0, parts.length-1).join('/');
447
448 if (isAddToSelection.test(url)) {
449 button.src = portal_url() + '/unselect_flag_btn.gif';
450 button.alt = link.title = 'Retirer de la sélection';
451 link.href = canonicalImgUrl + '/remove_to_selection';
452 this.selectedSlide.className = 'selected displayed';
453 this.image.parentNode.className = 'selected';
454 this.selectedSlideInSelection = true;
455 }
456 else {
457 button.src = portal_url() + '/select_flag_btn.gif';
458 button.alt = link.title = 'Ajouter à la sélection';
459 link.href = canonicalImgUrl + '/add_to_selection';
460 this.selectedSlide.className = 'displayed';
461 this.image.parentNode.className = '';
462 this.selectedSlideInSelection = false;
463 }
464 break;
465
466 case 'show_buyable':
467 disableDefault(evt);
468 disablePropagation(evt);
469 button = target;
470 link = button.parentNode;
471 link.blur();
472 var slide = this.cartSlide;
473 slide.innerHTML = '';
474 slide.style.visibility = 'visible';
475 var cw = new CartWidget(slide, link.href);
476 cw.onCancel = function() {
477 CartWidget.prototype.onCancel.apply(this);
478 slide.style.visibility = 'hidden';
479 };
480 cw.onAfterConfirm = function() {
481 slide.style.visibility = 'hidden';
482 };
483 break;
484
485
486
487
488 /*
489 case 'edit_metadata' :
490 disableDefault(evt);
491 disablePropagation(evt);
492 target.blur();
493 if (this.viewMode === 'full') {
494 this.mosaique.unload();
495 this.mosaique = null;
496 this.viewMode = 'medium';
497 return;
498 }
499 var fi = new FragmentImporter(absolute_url());
500 fi.useMacro('metadata_edit_form_macros', 'iptc', 'image_metadata');
501 break;
502 */
503 }
504 }
505 };
506
507
508 if(browser.isDOM2Event) {
509 if (browser.isAppleWebKit) {
510 FilmSlider.prototype.mouseWheelHandler = function(evt) {
511 disableDefault(evt);
512 var pos = this.getSliderPosition();
513 pos.x -= evt.wheelDelta / 40;
514 this.setSliderPosition(pos);
515 };
516 }
517 else {
518 FilmSlider.prototype.mouseWheelHandler = function(evt) {
519 disableDefault(evt);
520 var pos = this.getSliderPosition();
521 pos.x += evt.detail * 3;
522 this.setSliderPosition(pos);
523 };
524 }
525 }
526 else if (browser.isIE6up) {
527 FilmSlider.prototype.mouseWheelHandler = function() {
528 var evt = window.event;
529 evt.returnValue = false;
530 var pos = this.getSliderPosition();
531 pos.x -= evt.wheelDelta / 40;
532 this.setSliderPosition(pos);
533 };
534 }
535
536 FilmSlider.prototype.keyDownHandler = function(evt) {
537 evt = getEventObject(evt);
538 switch (evt.keyCode) {
539 case keyLeft :
540 this.loadSibling(true);
541 break;
542 case keyRight :
543 this.loadSibling(false);
544 break;
545 default:
546 return;
547 }
548 };
549
550
551 FilmSlider.prototype.keyPressHandler = function(evt) {
552 var target = getTargetedObject(evt);
553 if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') { return; }
554 evt = getEventObject(evt);
555 var charPress = String.fromCharCode((evt.keyCode) ? evt.keyCode : evt.which);
556 switch(charPress) {
557 case 'f':
558 case 'F':
559 raiseMouseEvent(this.buttons.full_screen, 'click');
560 break;
561 }
562 };
563
564 FilmSlider.prototype.populateViewer = function(req) {
565 var elements = req.responseXML.documentElement.childNodes;
566 var i;
567 for(i=0 ; i < elements.length ; i++ ) {
568 element = elements[i];
569 switch (element.nodeName) {
570 case 'fragment' :
571 var dest = document.getElementById(element.getAttribute('id'));
572 if (dest) { dest.innerHTML = element.firstChild.nodeValue; }
573 break;
574 case 'imageattributes' :
575 var link = this.buttons.back_to_portfolio.parentNode;
576 link.href = element.getAttribute('backToContextUrl');
577 link = this.buttons.show_buyable.parentNode;
578 var buyable = element.getAttribute('buyable');
579 if(buyable === 'True') { link.className = null; }
580 else if(buyable === 'False') { link.className = 'hidden'; }
581 this.image.alt = element.getAttribute('alt');
582 this.updateBreadcrumbs(element.getAttribute('lastBcUrl'),
583 element.getAttribute('img_id'));
584 break;
585 }
586 }
587 };
588
589 FilmSlider.prototype.refreshImage = function() {
590 this.image.style.visibility = 'hidden';
591 this.image.src = this.pendingImage.src;
592 this.image.width = this.pendingImage.width;
593 this.image.height = this.pendingImage.height;
594 this.image.style.visibility = 'visible';
595 if (this.selectedSlideInSelection) { this.image.parentNode.className = 'selected'; }
596 else { this.image.parentNode.className = ''; }
597 };
598
599 FilmSlider.prototype.updateBreadcrumbs = function(url, title) {
600 if (this.hasBreadcrumbs) {
601 this.lastBCElement.href = element.getAttribute('lastBcUrl');
602 this.lastBCElement.innerHTML = element.getAttribute('img_id');
603 }
604 };
605
606 FilmSlider.prototype.startSlideShow = function() {
607 this.slideShowSlide = this.pendingSlideShowSlide = this.selectedSlide;
608 return this.slideShowSlide.href;
609 };
610
611 FilmSlider.prototype.slideShowNext = function() {
612 var nextSlide = this.slideShowSlide.parentNode.nextSibling;
613 if (nextSlide && nextSlide.nodeType===3) { nextSlide = nextSlide.nextSibling; }
614
615 if (nextSlide) {
616 nextSlide = nextSlide.getElementsByTagName('a')[0];
617 this.pendingSlideShowSlide = nextSlide;
618 return this.pendingSlideShowSlide.href;
619 }
620 else {
621 var row = this.slideShowSlide.parentNode.parentNode;
622 var first = row.firstChild;
623 if (first.nodeType===3) { first = first.nextSibling; }
624 this.pendingSlideShowSlide = first.getElementsByTagName('a')[0];
625 return this.pendingSlideShowSlide.href;
626 }
627 };
628
629 FilmSlider.prototype.slideShowPrevious = function() {
630 var previousSlide = this.slideShowSlide.parentNode.previousSibling;
631 if (previousSlide && previousSlide.nodeType===3) { previousSlide = previousSlide.previousSibling; }
632
633 if (previousSlide) {
634 previousSlide = previousSlide.getElementsByTagName('a')[0];
635 this.pendingSlideShowSlide = previousSlide;
636 return this.pendingSlideShowSlide.href;
637 }
638 else {
639 var row = this.slideShowSlide.parentNode.parentNode;
640 var last = row.lastChild;
641 if (last.nodeType===3) { last = last.previousSibling; }
642 this.pendingSlideShowSlide = last.getElementsByTagName('a')[0];
643 return this.pendingSlideShowSlide.href;
644 }
645 };
646
647 FilmSlider.prototype.slideShowImageLoaded = function() {
648 this.slideShowSlide = this.pendingSlideShowSlide;
649 };
650
651 FilmSlider.prototype.stopSlideShow = function() {
652 raiseMouseEvent(this.slideShowSlide, 'click');
653 var index = parseInt(this.selectedSlide.getAttribute('portfolio:position'), 10);
654 this.centerSlide(index);
655 };
656
657
658 /* UTILS */
659 function Point(x, y) {
660 this.x = Math.round(x);
661 this.y = Math.round(y);
662 }
663 Point.prototype.diff = function(point) { return new Point(this.x - point.x, this.y - point.y); };
664 Point.prototype.add = function(point) { return new Point(this.x + point.x, this.y + point.y); };
665 Point.prototype.mul = function(k) { return new Point(this.x * k, this.y *k); };
666 Point.prototype.toString = function() { return "(" + String(this.x) + ", " + String(this.y) + ")"; };
667
668 }());