1 var gamejs = require('gamejs'); 2 var draw=gamejs.draw; 3 4 var EVT_FOCUS = exports.EVT_FOCUS = 'focus'; 5 var EVT_BLUR = exports.EVT_BLUR = 'blur'; 6 var EVT_MOUSE_OVER = exports.EVT_MOUSE_OVER = 'mouse_over'; 7 var EVT_MOUSE_OUT = exports.EVT_MOUSE_OUT = 'mouse_out'; 8 var EVT_KEY_DOWN = exports.EVT_KEY_DOWN= gamejs.event.KEY_DOWN; 9 var EVT_KEY_UP = exports.EVT_KEY_UP = gamejs.event.KEY_UP; 10 var EVT_MOUSE_UP = exports.EVT_MOUSE_UP = gamejs.event.MOUSE_UP; 11 var EVT_MOUSE_DOWN = exports.EVT_MOUSE_DOWN = gamejs.event.MOUSE_DOWN; 12 var EVT_MOUSE_WHEEL = exports.EVT_MOUSE_WHEEL = gamejs.event.MOUSE_WHEEL; 13 var EVT_MOUSE_MOTION = exports.EVT_MOUSE_MOTION = gamejs.event.MOUSE_MOTION; 14 var EVT_BTN_CLICK = exports.EVT_BTN_CLICK = 'btn_click'; 15 var EVT_CLOSE = exports.EVT_CLOSE = 'close'; 16 var EVT_SCROLL = exports.EVT_SCROLL = 'scroll'; 17 var EVT_DRAG = exports.EVT_DRAG = 'drag'; 18 var EVT_MOVE = exports.EVT_MOVE = 'move'; 19 var EVT_RESIZE = exports.EVT_RESIZE = 'resize'; 20 var EVT_PAINT = exports.EVT_PAINT = 'paint'; 21 var EVT_CHANGE = exports.EVT_CHANGE = 'change'; //input change for input elements 22 var DEFAULT_FONT_DESCR='14px Verdana'; 23 var gamejs_ui_next_id=1; 24 25 26 /** 27 *@ignore 28 */ 29 function cloneEvent(evt, offset){ 30 var new_evt={}; 31 for(key in evt){ 32 new_evt[key]=evt[key]; 33 } 34 if(new_evt.pos && offset){ 35 new_evt.pos=[new_evt.pos[0]-offset[0], new_evt.pos[1]-offset[1]]; 36 } 37 return new_evt; 38 } 39 40 /** 41 *@ignore 42 */ 43 function getCenterPos(size1, size2){ 44 return [Math.max(parseInt((size1[0]-size2[0])/2), 0), 45 Math.max(parseInt((size1[1]-size2[1])/2), 0)]; 46 } 47 48 /** 49 *Make a view draggable within it's parent. Dragging generates EVT_DRAG 50 *@function 51 *@param {View} view view to make draggable. 52 */ 53 var draggable=exports.draggable=function(view){ 54 view.grab_pos=null; 55 view.on(EVT_MOUSE_DOWN, function(event){ 56 this.grab_pos=event.global_pos; 57 }, view); 58 view.getGUI().on(EVT_MOUSE_UP, function(event){ 59 this.grab_pos=null; 60 }, view); 61 62 view.getGUI().on(EVT_MOUSE_MOTION, function(event){ 63 if(this.grab_pos){ 64 var old_position=this.position; 65 66 var new_position=[this.position[0]+event.pos[0]-this.grab_pos[0], 67 this.position[1]+event.pos[1]-this.grab_pos[1]]; 68 69 70 this.grab_pos=event.pos; 71 this.move(new_position); 72 this.despatchEvent({'type':EVT_DRAG, 73 'old_pos':old_position, 74 'new_pos':this.position}); 75 } 76 }, view); 77 }; 78 79 /** 80 *implements lazy caching for individual letters 81 *@class cached font 82 *@constructor 83 * 84 *@param {String|Array} font either font description as string, or assoc array character:gamejs.Surface 85 *@param {String} color a valid #RGB String, "#ffcc00" 86 * 87 *@property {Number} space_width space between lettersin pixels. Default 'm' width divided by 3 88 *@property {Number} tab_width tab width in pixels. Default 3*space_width 89 *@property {gamejs.font.Font} font font object used to render characters. Default 14px Verdana 90 */ 91 var CachedFont=exports.CachedFont=function(font, color){ 92 this.space_width=3; 93 this.tab_width=12; 94 this.chars={}; //character:surface; 95 this.font=null; 96 if((typeof font)=='string'){ 97 color = color ? color : '#000'; 98 this.color=color; 99 this.font=new gamejs.font.Font(font); 100 101 }else{ 102 this.chars=font; 103 this.font=new gamejs.font.Font(DEFAULT_FONT_DESCR); 104 this.color='#000'; 105 } 106 //space width - 1/3 of m's width 107 this.space_width=parseInt(Math.ceil(this.getCharSurface('m').getSize()[0]/3)); 108 this.tab_width=3*this.space_width; 109 }; 110 111 112 /** 113 *returns gamejs.Surface for a character. Caches this surface if it is not cached 114 * 115 *@function 116 *@param {String} c single character 117 * 118 *@returns {gamejs.Surface} surface object with the character painted on. Not a copy, don't paint on it! 119 */ 120 CachedFont.prototype.getCharSurface=function(c){ 121 if(!this.chars[c]){ 122 var s=this.font.render(c, this.color); 123 this.chars[c]=s; 124 } 125 return this.chars[c]; 126 }; 127 128 /** 129 *get size text would occupy if it was rendered 130 *@function 131 * 132 *@param {String} text 133 * 134 *@returns {Array} size, eg. [width, height] 135 */ 136 CachedFont.prototype.getTextSize=function(text){ 137 var w=0, h=0, c, l, sz; 138 if(text){ 139 for(var i=0;i<text.length;i++){ 140 c=text[i]; 141 if(c==' ')w+=this.space_width; 142 else if(c=='\t')w+=this.tab_width; 143 else{ 144 l=this.getCharSurface(c); 145 if(l){ 146 sz=l.getSize(); 147 w+=sz[0]; 148 h=Math.max(sz[1], h); 149 } 150 } 151 } 152 if(!h) h=this.getCharSurface('m').getSize()[1]; 153 return [w, h]; 154 }else return [0, 0]; 155 }; 156 157 /** 158 *render text on a surface 159 *@function 160 * 161 *@param {gamejs.Surface} surface surface to render text on 162 *@param {String} text text to render 163 *@param {Array} position position to render the text at 164 *@param {Number} space_width OPTIONAL, override space width 165 */ 166 CachedFont.prototype.render=function(surface, text, position, space_width){ 167 ofst=position[0]; 168 space_width=space_width? space_width : this.space_width; 169 var i, c, s; 170 for(i=0;i<text.length;i++){ 171 c=text[i]; 172 if(c==' ')ofst+=space_width; 173 else if(c=='\t')ofst+=this.tab_width; 174 else{ 175 s=this.getCharSurface(c); 176 r1=[ofst, position[1]]; 177 surface.blit(s, r1); 178 ofst+=s.getSize()[0]; 179 } 180 } 181 }; 182 183 184 exports.DEFAULT_FONT=new CachedFont('12px Verdana', 'black'); 185 186 /** 187 *View 188 *@class base gui object !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 189 *@constructor 190 * 191 *@param {View} parent parent element 192 *@param {Array} size array containing width & height of the element, eg. [width, height] 193 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 194 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 195 *@param {Bool} visible is this view visible? default true 196 * 197 *@property {Array} size view size, [width, length] 198 *@property {Array} position view position relative to parent, [x, y] 199 *@property {gamejs.Surface} surface surface this view is rendered on 200 *@property {View} parent parent view of this view 201 *@property {Array} children array of this views children views 202 */ 203 var View=exports.View=function(pars){ 204 this.type='view'; 205 this.id=gamejs_ui_next_id++; 206 if(!pars.size) throw "View: size must be specified" 207 this.size=pars.size; 208 if(!pars.position) pars.position=[0, 0]; 209 this.position=[parseInt(pars.position[0]), parseInt(pars.position[1])]; 210 this.surface=pars.surface ? pars.surface : new gamejs.Surface(this.size); 211 if(pars.parent===undefined) throw "Element's parent not given." 212 this.parent=pars.parent; 213 if(pars.visible===undefined){ 214 this.visible=true; 215 }else{ 216 this.visible=pars.visible; 217 } 218 if(this.parent) this.parent.addChild(this); 219 220 this.children=[]; 221 this._refresh=true; 222 223 //is the mouse over this view? 224 this.is_hovered=false; 225 226 //is this view focused? 227 this.is_focused=false; 228 229 //evenet type: [{'callback':x, 'scope':y, ...] 230 this.listeners={}; 231 return this; 232 233 }; 234 235 /* 236 *get is focus state 237 *@function 238 *@returns {Bool} is this view focused? 239 */ 240 View.prototype.isFocused=function(){ 241 return this.is_focused; 242 }; 243 244 /** 245 *get hover state 246 *@function 247 *@returns {Bool} is mouse hovering on this element? 248 */ 249 View.prototype.isHovered=function(){ 250 return this.is_hovered; 251 }; 252 /** 253 *get view size 254 *@function 255 *@returns {Array} view size, [width, height] 256 */ 257 View.prototype.getSize=function(){ 258 return [this.size[0], this.size[1]]; 259 }; 260 261 /** 262 *get view position within it's parent element 263 *@function 264 *@returns {Array} view position within it's parent element, [x, y] 265 */ 266 View.prototype.getPosition=function(){ 267 return [this.position[0], this.position[1]]; 268 }; 269 270 /** 271 *get visible state 272 *@function 273 *@returns {Bool} is this view visible? 274 */ 275 276 View.prototype.isVisible=function(){ 277 return this.visible; 278 }; 279 280 /** 281 *detaches a child from this view 282 *@function 283 *@param {View|Number} child View or or child View id. 284 */ 285 View.prototype.removeChild=function(child){ 286 if(typeof(child)!='number')child=child.id; 287 for(var i=0;i<this.children.length;i++){ 288 if(this.children[i].id==child){ 289 child=this.children.splice(i, 1); 290 this.refresh(); 291 return true; 292 } 293 } 294 return false; 295 } 296 297 /** 298 *detaches view from it's parent, effectively destroying it 299 *@function 300 */ 301 View.prototype.destroy=function(){ 302 if(this.parent)this.parent.removeChild(this); 303 } 304 305 /** 306 *get position & size rect 307 *@returns gamejs.Rect instance. Position is relative to parent 308 */ 309 View.prototype.getRect=function(){ 310 return new gamejs.Rect(this.position, this.size); 311 }; 312 313 /** 314 *add child to this view 315 *@param {View} child view to add as a child of this view 316 */ 317 View.prototype.addChild=function(child){ 318 this.children.push(child); 319 }; 320 321 /** 322 *if needed, paints this view, draws children and coposites them on this views surface 323 *@function 324 *@returns {Bool} has this view been repainted 325 */ 326 View.prototype.draw=function(){ 327 if(!this.visible){ 328 if(this._refresh){ 329 this._refresh=false; 330 return true; 331 } 332 return false; 333 } 334 335 var painted=false; //has something been repainted in this view? 336 //does this view need repainting? 337 338 this.children.forEach(function(child){ 339 //draw children if this view has been repainted or child has been repainted 340 if(child.draw() || this._refresh){ 341 painted=true; 342 } 343 }, this); 344 345 if(this._refresh || painted){ 346 this.paint(); 347 this.despatchEvent({'type':EVT_PAINT, 'surface':this.surface}); 348 this.children.forEach(function(child){ 349 if(child.visible) this.blitChild(child); 350 }, this) 351 painted=true; 352 this._refresh=false; 353 } 354 355 return painted; 356 }; 357 /** 358 *blit child's surface on this view's surface 359 *@function 360 *@param {View} child child view to blit 361 */ 362 View.prototype.blitChild=function(child){ 363 this.surface.blit(child.surface, child.position); 364 }; 365 366 /** 367 *paint this view on it's surface. does not repaint/blit children! override this to implement custom drawing of the element itself. by default, only clears the surface 368 *@function 369 */ 370 View.prototype.paint=function(){ 371 this.surface.clear(); 372 }; 373 374 375 /** 376 *update view. does nothing by default 377 *@function 378 *@param {Number} miliseconds since last update 379 */ 380 View.prototype.update=function(msDuration){}; 381 382 /** 383 *recursively calls _update on this views children, then calls update for this view. 384 *@ignore 385 */ 386 View.prototype._update=function(msDuration){ 387 this.children.forEach(function(child){ 388 child._update(msDuration); 389 }); 390 this.update(msDuration); 391 }; 392 393 /** 394 *register a callback for an event. When event is triggered, callback is called with event object as argument 395 *@function 396 *@param {Event ID} event event ID of the event to be registered on, eg gamejs-gui.EVT_BLUR, gamejs-gui.EVT_KEY_DOWN, etc. 397 *@param {Function} callback function to call when event is triggered 398 *@param {Object} scope - this object for the callback 399 */ 400 View.prototype.on=function(event_type, callback, scope){ 401 if(!this.listeners[event_type])this.listeners[event_type]=[]; 402 this.listeners[event_type].push({'callback':callback, 'scope':scope}); 403 }; 404 405 /** 406 *despatches event to all children. internal 407 *@ignore 408 */ 409 View.prototype.despatchEventToChildren= function(event){ 410 this.children.forEach(function(child){child.despatchEvent(event);}); 411 }; 412 413 /** 414 *Move view relative to its parent. Generates EVT_MOVE event 415 *@function 416 *@param {Array} new position relative to parent element, eg. [x, y] 417 */ 418 View.prototype.move=function(position){ 419 var old_position=this.position; 420 this.position=position; 421 this.parent.refresh(); 422 this.despatchEvent({'type':EVT_MOVE, 423 'old_pos':old_position, 424 'new_pos':position}); 425 }; 426 427 /** 428 *Move view relative to its position. Generates EVT_MOVE event 429 *@function 430 *@param {Array} delta coordinates relative to current position ,eg. [delta x, delta y] 431 */ 432 View.prototype.moveRelative=function(position){ 433 this.move([this.position[0]+position[0], this.position[1]+position[1]]); 434 }; 435 436 /** 437 *Resize this view. Generates EVT_RESIZE event 438 *@function 439 *@param {Array} new size, eg. [width, height] 440 */ 441 View.prototype.resize=function(size){ 442 var old_size=this.size; 443 this.size=size; 444 this.surface=new gamejs.Surface([Math.max(size[0], 1), Math.max(size[1], 1)]); 445 this.refresh(); 446 this.despatchEvent({'type':EVT_RESIZE, 447 'old_size':old_size, 448 'new_size':size}); 449 }; 450 451 /** 452 *Redraw this view and its children. 453 *@function 454 */ 455 View.prototype.refresh=function(){ 456 this._refresh=true; 457 }; 458 459 /** 460 *If this view is hidden, make it visible 461 *@function 462 */ 463 View.prototype.show=function(){ 464 if(!this.visible){ 465 this.visible=true; 466 this.refresh(); 467 } 468 }; 469 470 /** 471 *If this view is visible, hide it. This also blurs and mouse-outs the view, if applicable 472 *@function 473 */ 474 View.prototype.hide=function(){ 475 if(this.visible){ 476 this.despatchEvent({'type':EVT_BLUR}); 477 this.despatchEvent({'type':EVT_MOUSE_OUT}); 478 this.visible=false; 479 this.refresh(); 480 } 481 }; 482 483 /** 484 *Despatch event to this view. Event is despatched to children if applicable, then handled by this view. 485 *@function 486 *@param {Event} event event to despatch 487 */ 488 View.prototype.despatchEvent=function(event){ 489 if(!this.visible) return; 490 var inside=false; //event position inside this view 491 492 if(event.type==EVT_BLUR){ 493 if(this.is_focused){ 494 this.is_focused=false; 495 this.refresh(); 496 this.handleEvent(event); 497 } 498 this.despatchEventToChildren(event); 499 } 500 else if(event.type==EVT_MOUSE_OUT){ 501 if(this.is_hovered){ 502 this.is_hovered=false; 503 this.refresh(); 504 this.handleEvent(event); 505 } 506 this.despatchEventToChildren(event); 507 } 508 else if(event.type==EVT_MOUSE_OVER){ 509 this.is_hovered=true; 510 this.refresh(); 511 this.handleEvent(event); 512 } 513 514 else if(event.type==EVT_FOCUS){ 515 this.is_focused=true; 516 this.refresh(); 517 this.handleEvent(event); 518 } 519 520 else if(event.type==EVT_MOUSE_DOWN){ 521 if(!this.isFocused()){ 522 this.despatchEvent({'type':EVT_FOCUS}); 523 } 524 this.children.forEach(function(child){ 525 //click inside child: despatch 526 if(child.getRect().collidePoint(event.pos)){ 527 child.despatchEvent(cloneEvent(event, child.position)); 528 }else{ 529 //not inside, but child is focused: blur 530 if(child.isFocused()) child.despatchEvent({'type':EVT_BLUR}); 531 } 532 }, this); 533 this.handleEvent(event); 534 } 535 536 else if(event.type==EVT_MOUSE_UP){ 537 this.children.forEach(function(child){ 538 //click inside child: despatch 539 if(child.getRect().collidePoint(event.pos)){ 540 child.despatchEvent(cloneEvent(event, child.position)); 541 } 542 }, this); 543 this.handleEvent(event); 544 } 545 546 else if(event.type==EVT_MOUSE_MOTION){ 547 548 //mouse moved onto this view - hover 549 this.children.forEach(function(child){ 550 //click inside child: despatch 551 if(child.getRect().collidePoint(event.pos)){ 552 //inside, not hovering: hover 553 if(!child.isHovered()) child.despatchEvent(cloneEvent({'type':EVT_MOUSE_OVER, 'pos':event.pos}, child.position)); 554 child.despatchEvent(cloneEvent(event, child.position)); 555 }else{ 556 //not inside, but child is focused: blur 557 if(child.isHovered()) child.despatchEvent(cloneEvent({'type':EVT_MOUSE_OUT, 'pos':event.pos}, child.position)); 558 } 559 }, this); 560 this.handleEvent(event); 561 562 } 563 else if(event.type==EVT_KEY_UP || event.type==EVT_KEY_DOWN || event.type==EVT_KEY_UP){ 564 if(this.isFocused()){ 565 this.children.forEach(function(child){ 566 if(child.isFocused()) child.despatchEvent(cloneEvent(event)); 567 }); 568 this.handleEvent(event); 569 } 570 //default 571 }else{ 572 this.handleEvent(event); 573 } 574 575 }; 576 577 /** 578 *returns GUI object at the base of this views branch 579 *@function 580 *@returns {GUI} GUI object at the base if this views branch 581 */ 582 View.prototype.getGUI=function(){ 583 var parent=this.parent; 584 while(parent!=null && parent.type!='gui'){ 585 parent=parent.parent; 586 } 587 return parent; 588 }; 589 590 /** 591 *Center a child view within this view. Must be direct child 592 *@function 593 *@param {View} child child view 594 */ 595 View.prototype.center=function(child){ 596 child.move(getCenterPos(this.size, child.size)); 597 }; 598 599 /** 600 *execute any registered callbacks for this event. Should only be called by despatchEvent! 601 *@ignore 602 */ 603 View.prototype.handleEvent=function(event){ 604 if(this.listeners[event.type]){ 605 this.listeners[event.type].forEach(function(listener){ 606 if(listener.scope) listener.callback.apply(listener.scope, [event, this]); 607 else listener.callback(event, this); 608 }); 609 } 610 }; 611 612 /** 613 *@class single-line text display !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 614 *@augments View 615 *@constructor 616 * 617 *@param {CachedFont} font font to draw text with. OPTIONAL, default gamejs-gui.DEFAULT_FONT 618 *@param {String} text text to draw 619 *@param {View} parent parent element 620 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 621 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 622 *@param {Bool} visible is this view visible? OPTIONAL, EFAULT true 623 * 624 *resized automatically to house text. 625 * 626 *@property {CachedFont} font 627 *@property {String} text 628 629 */ 630 631 var Label=exports.Label=function(pars){ 632 this.font=pars.font ? pars.font : exports.DEFAULT_FONT; 633 pars.size=[1, 1]; 634 Label.superConstructor.apply(this, [pars]); 635 if(!pars.text) throw "Label: label text must be provided!" 636 this.setText(pars.text); 637 this.type='label'; 638 }; 639 640 gamejs.utils.objects.extend(Label, View); 641 642 /** 643 *set new text for this label. Resizes the view automatically. 644 *@function 645 * 646 *@param {String} text new text 647 */ 648 Label.prototype.setText=function(text){ 649 this.text=text ? text : ' '; 650 this.size=this.font.getTextSize(text); 651 this.resize(this.size); 652 }; 653 654 /*** 655 *paint implementation for label. clears surface and renders text 656 *@function 657 */ 658 Label.prototype.paint=function(){ 659 this.surface.clear(); 660 this.font.render(this.surface, this.text, [0, 0]); 661 }; 662 663 /** 664 *@class button !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 665 *@augments View 666 * 667 *@param {gamejs.Surface} image button image, OPTIONAL 668 *@param {gamejs.Surface} image_down button image when pressed down, OPTIONAL 669 *@param {gamejs.Surface} image_hover button image when hovered on, OPTIONAL 670 *@param {String} text text to display on button, OPTIONAL 671 *@param {CachedFont} font to render text with, OPTIONAL 672 *@param {View} parent parent element 673 *@param {Array} size array containing width & height of the element, eg. [width, height] 674 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 675 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 676 *@param {Bool} visible is this view visible? default true 677 * 678 *@property {String} text 679 *@property {CachedFont} font 680 *@property {Label} label Label obejct, created if text was provided. 681 */ 682 683 var Button=exports.Button=function(pars){ 684 Button.superConstructor.apply(this, [pars]); 685 this.type='button'; 686 this.image=null; 687 this.label=null; 688 this.image=pars.image; 689 this.image_down=pars.image_down; 690 this.image_hover=pars.image_hover; 691 692 if(!this.image){ 693 this.image=new gamejs.Surface(this.size); 694 this.image.fill('#FFF'); 695 gamejs.draw.rect(this.image, '#808080', new gamejs.Rect([0, 0], this.size), 1); 696 } 697 698 if(pars.text){ 699 this.label=new Label({'parent':this, 700 'position':[0, 0], 701 'text':pars.text, 702 'font':pars.font}); 703 this.center(this.label); 704 } 705 706 this.pressed_down=false; 707 this.on(EVT_MOUSE_DOWN, function(){ 708 if(!this.pressed_down){ 709 this.pressed_down=true; 710 this.refresh(); 711 } 712 }, this); 713 714 this.on(EVT_MOUSE_UP, function(){ 715 if(this.pressed_down){ 716 this.pressed_down=false; 717 this.despatchEvent({'type':EVT_BTN_CLICK}); 718 this.refresh(); 719 } 720 }, this); 721 722 this.on(EVT_MOUSE_OUT, function(){ 723 if(this.pressed_down){ 724 this.pressed_down=false; 725 this.refresh(); 726 } 727 }, this) 728 }; 729 730 gamejs.utils.objects.extend(Button, View); 731 732 /** 733 *short hand for on(EVT_BTN_CLICK, callback, scope) 734 *@function 735 * 736 *@param {Function} callback function to call when EVT_BTN_CLICK event is triggered 737 *@param {Object} scope this object for callback, OPTIONAL 738 */ 739 Button.prototype.onClick=function(callback, scope){ 740 this.on(EVT_BTN_CLICK, callback, scope); 741 }; 742 743 /** 744 *default button paint implementation paints image, image_down or image_hover based on button sotate 745 *@function 746 */ 747 Button.prototype.paint=function(){ 748 var img; 749 if(this.pressed_down && this.image_down) img=this.image_down; 750 else if(this.isHovered() && this.image_hover) img=this.image_hover; 751 else img=this.image; 752 this.surface.blit(img, new gamejs.Rect([0, 0], this.surface.getSize()), new gamejs.Rect([0, 0], img.getSize())); 753 754 }; 755 756 /** 757 *set button text 758 *@function 759 *@param {String} text 760 */ 761 Button.prototype.setText=function(text){ 762 if(this.label){ 763 this.label.setText(text); 764 this.center(this.label); 765 } 766 }; 767 768 /** 769 *@class image !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 770 *@augments View 771 * 772 *@param {gamejs.Surface} image to paint 773 *@param {View} parent parent element 774 *@param {Array} size array containing width & height of the element, eg. [width, height] 775 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 776 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 777 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 778 * 779 *@property {gamejs.Surface} image 780 */ 781 782 var Image=exports.Image=function(pars){ 783 if(!pars.image) throw 'Image: parameter image is required'; 784 if(!pars.size) pars.size=pars.image.getSize(); 785 this.image=pars.image; 786 Image.superConstructor.apply(this, [pars]); 787 this.type='image'; 788 return this; 789 }; 790 791 gamejs.utils.objects.extend(Image, View); 792 793 /** 794 *set new image 795 *@function 796 *@param {gamejs.Surface} image 797 */ 798 Image.prototype.setImage=function(image){ 799 this.image=image; 800 this.refresh(); 801 }; 802 803 /** 804 *default paint implementation for image. If Image object size!=provided image surface size, image is stretched. 805 *@function 806 */ 807 Image.prototype.paint=function(){ 808 View.prototype.paint.apply(this, []); 809 this.surface.blit(this.image, new gamejs.Rect([0, 0], this.surface.getSize()), new gamejs.Rect([0, 0], this.image.getSize())); 810 }; 811 812 /** 813 *@class draggable frame header with a close button !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 814 *@augments View 815 * 816 *@param {Frame} parent frame object this header is applied to 817 *@param {Number} height frame height, OPTIONAL, DEFAULT 20 818 *@param {String} title frame title OPTIONAL 819 *@param {CachedFont} title_font font for title OPTIONAL 820 *@param {Bool} close_btn show close button? OPTIONAL, DEFAULT false 821 *@param {gamejs.Surface} close_icon image to use for close button, OPTIONAL 822 *@param {gamejs.Surface} close_btn close button image OPTIONAL 823 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 824 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 825 * 826 *@property {CachedFont) title_font 827 *@property {Number} height 828 */ 829 var FrameHeader=exports.FrameHeader=function(pars){ 830 if(!pars.parent) throw 'FrameHeader: parent parameter is required'; 831 this.height=pars.height || 20; 832 pars.width=pars.parent.size[0]; 833 pars.size=[pars.width, this.height]; 834 pars.position=[0, 0]; 835 this.title_font=pars.title_font; 836 837 FrameHeader.superConstructor.apply(this, [pars]); 838 draggable(this); 839 840 if(pars.title){ 841 this.setTitle(pars.title); 842 } 843 844 if(pars.close_btn){ 845 var img; 846 if(pars.close_icon){ 847 img=pars.close_icon; 848 } 849 else{ 850 img=new gamejs.Surface([this.height, this.height]); 851 gamejs.draw.line(img, '#000', [3, 3], [this.height-3, this.height-3], 3); 852 gamejs.draw.line(img, '#000', [3, this.height-3], [this.height-3, 3], 3); 853 } 854 855 img=new Image({'parent':this, 856 'position':[this.size[0]-img.getSize()[0], 0], 857 'image':img}); 858 img.on(EVT_MOUSE_DOWN, function(){ 859 this.close(); 860 this.despatchEvent({'type':EVT_CLOSE}); 861 }, this.parent); 862 } 863 864 this.type='frameheader'; 865 }; 866 867 gamejs.utils.objects.extend(FrameHeader, View); 868 869 /** 870 *moving header moves parent frame too 871 *@function 872 * 873 *@param {Array} pos position ot move header to 874 */ 875 FrameHeader.prototype.move=function(pos){ 876 this.parent.move([this.parent.position[0]+pos[0]-this.position[0], 877 this.parent.position[1]+pos[1]-this.position[1]]); 878 }; 879 880 /** 881 *set header title 882 *@function 883 * 884 *@param {String} text new header title 885 */ 886 FrameHeader.prototype.setTitle=function(text){ 887 if(!this.title_label)this.title_label=new Label({'parent':this, 888 'position':[0, 0], 889 'font':this.title_font, 890 'text':text}); 891 else this.title_label.setText(text); 892 var font=this.title_label.font; 893 var size=font.getTextSize(text); 894 this.title_label.move([font.space_width, Math.max(parseInt(this.height-size[1]))], 0); 895 draggable(this); 896 }; 897 898 /** 899 *default paint implementation: gray background 900 *@function 901 */ 902 FrameHeader.prototype.paint=function(){ 903 gamejs.draw.rect(this.surface, '#C0C0C0', new gamejs.Rect([0, 0], this.size)); 904 }; 905 906 907 908 /** 909 *@class a overlay view with it's own space and hierarchy, a 'window' in OS talk. Hidden by default !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 910 *@augments View 911 * 912 *@param {GUI} parent parent GUI object 913 *@param {Bool} constrain if true, frame cannot be moved out of visible area 914 *@param {Array} size array containing width & height of the element, eg. [width, height] 915 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 916 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 917 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 918 * 919 *@property {Bool} constrain 920 */ 921 var Frame=exports.Frame=function(pars){ 922 if(!pars.parent) throw 'Frame: parent parameter is required'; 923 if(pars.parent.type!='gui') throw 'Frame: parent object must be instance of GUI'; 924 var gui=pars.parent; 925 pars.parent=null; 926 Frame.superConstructor.apply(this, [pars]); 927 this.type='frame'; 928 this.visible=false; 929 gui.frames.push(this); 930 this.parent=gui; 931 //constrain 932 this.constrain=pars.constrain; 933 return this; 934 }; 935 gamejs.utils.objects.extend(Frame, View); 936 937 /** 938 *Default implementation, white fill and gray border. 939 *@function 940 */ 941 Frame.prototype.paint=function(){ 942 //fill 943 gamejs.draw.rect(this.surface, '#FFF', new gamejs.Rect([0, 0], this.size)); 944 945 //draw border 946 gamejs.draw.rect(this.surface, '#404040', new gamejs.Rect([0, 0], this.size), 1); 947 }; 948 949 /** 950 *Show frame, move it to top of the screen 951 *@function 952 */ 953 Frame.prototype.show=function(){ 954 View.prototype.show.apply(this, []); 955 this.parent.moveFrameToTop(this); 956 }; 957 958 /** 959 *Close frame. You propably want to use this instead of hide()! generates EVT_CLOSE 960 *@function 961 */ 962 Frame.prototype.close=function(){ 963 View.prototype.hide.apply(this, []); 964 this.despatchEvent({'type':EVT_CLOSE}); 965 }; 966 967 /** 968 *implements restricting frame to GUI bounds. generates EVT_MOVE 969 *@function 970 *@param {Array} position position to move frame to 971 */ 972 973 Frame.prototype.move=function(position){ 974 if(this.constrain){ 975 if(position[0]<0)position[0]=0; 976 if(position[0]>this.parent.size[0]-this.size[0]) position[0]=this.parent.size[0]-this.size[0]; 977 if(position[1]<0)position[1]=0; 978 if(position[1]>this.parent.size[1]-this.size[1]) position[1]=this.parent.size[1]-this.size[1]; 979 } 980 View.prototype.move.apply(this, [position]); 981 }; 982 983 /** 984 *closes frame, then destroys it 985 *@function 986 */ 987 Frame.prototype.destroy=function(){ 988 if(this.visible) this.close(); 989 if(this.parent) this.parent.removeFrame(this); 990 }; 991 992 /** 993 *@class draggable view: can be dragged within its parent by holding down left mouse btn 994 *@augments View 995 * 996 *@param {Number} min_x minimum x coordinate view can be dragged to, OPTIONAL 997 *@param {Number} max_x maximum x coordinate view can be dragged to, OPTIONAL 998 *@param {Number} min_y minimum y coordinate view can be dragged to, OPTIONAL 999 *@param {Number} max_y maximum y coordinate view can be dragged to, OPTIONAL 1000 *@param {View} parent parent element 1001 *@param {Array} size array containing width & height of the element, eg. [width, height] 1002 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 1003 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 1004 *@param {Bool} visible is this view visible? default true 1005 * 1006 *@property {Number} min_x 1007 *@property {Number} max_x 1008 *@property {Number} min_y 1009 *@property {Number} max_y 1010 */ 1011 1012 var DraggableView=exports.DraggableView=function(pars){ 1013 DraggableView.superConstructor.apply(this, [pars]); 1014 draggable(this); 1015 this.min_x=pars.min_x; 1016 this.max_x=pars.max_x; 1017 this.min_y=pars.min_y; 1018 this.max_y=pars.max_y; 1019 this.type='draggableview'; 1020 }; 1021 1022 gamejs.utils.objects.extend(DraggableView, View); 1023 1024 /** 1025 *implements restricting to coordinates, if applicable 1026 *@function 1027 *@param {Array} pos new position 1028 */ 1029 DraggableView.prototype.move=function(pos){ 1030 var x=pos[0]; 1031 if(this.min_x || (this.min_x==0)) x=Math.max(x, this.min_x); 1032 if(this.max_x || (this.max_x==0)) x=Math.min(x, this.max_x); 1033 1034 var y=pos[1]; 1035 if(this.min_y || (this.min_y==0)) y=Math.max(y, this.min_y); 1036 if(this.max_y || (this.max_y==0)) y=Math.min(y, this.max_y); 1037 1038 View.prototype.move.apply(this, [[x, y]]); 1039 }; 1040 1041 1042 /** 1043 *@class draggable part of the scrollbar !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 1044 *@augments DraggableView 1045 * 1046 *@param {View} parent parent element 1047 *@param {Array} size array containing width & height of the element, eg. [width, height] 1048 *@param {gamejs.Surface} image iamge to use for the scroller, OPTIONAL 1049 *@param {Number} min_x minimum x coordinate view can be dragged to, OPTIONAL 1050 *@param {Number} max_x maximum x coordinate view can be dragged to, OPTIONAL 1051 *@param {Number} min_y minimum y coordinate view can be dragged to, OPTIONAL 1052 *@param {Number} max_y maximum y coordinate view can be dragged to, OPTIONAL 1053 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 1054 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 1055 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 1056 * 1057 *@property {Image} img Image object created if image parameter was provided 1058 */ 1059 var Scroller=exports.Scroller=function(pars){ 1060 Scroller.superConstructor.apply(this, [pars]); 1061 this.img=null; 1062 if(pars.image){ 1063 this.img=new Image({'parent':this, 1064 'position':[0, 0], 1065 'size':this.size, 1066 'image':pars.image}); 1067 } 1068 }; 1069 gamejs.utils.objects.extend(Scroller, DraggableView); 1070 1071 /** 1072 *resizes image along with scroller 1073 *@function 1074 */ 1075 Scroller.prototype.resize=function(size){ 1076 DraggableView.prototype.resize.apply(this,[size]); 1077 if(this.img)this.img.resize(size); 1078 1079 }; 1080 1081 /** 1082 *@class !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 1083 *@augments View 1084 * 1085 *@param {gamejs.Surface} left_btn_image image for left scrollbar button 1086 *@param {gamejs.Surface} scroller_image image for scroller 1087 *@param {gamejs.Surface} right_btn_image image for right scrollbar button 1088 *@param {View} parent parent element 1089 *@param {Array} size array containing width & height of the element, eg. [width, height] 1090 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 1091 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 1092 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 1093 * 1094 *@property {Button} left_btn left scrollbar button 1095 *@property {Button} right_btn right scrollbar button 1096 *@property {Scroller} scroller 1097 *@property {Function} scroller_class scroller class used to create scroller #TODO find better way to implement customization 1098 */ 1099 1100 var HorizontalScrollbar=exports.HorizontalScrollbar=function(pars){ 1101 HorizontalScrollbar.superConstructor.apply(this, [pars]); 1102 this.type='horizontalscrollbar'; 1103 var left_btn_image=pars.left_btn_image; 1104 if(!left_btn_image){ 1105 left_btn_image=new gamejs.Surface([this.size[1], this.size[1]]); 1106 var pts=[[0, this.size[1]/2], 1107 [this.size[1], 0], 1108 [this.size[1], this.size[1]]]; 1109 gamejs.draw.polygon(left_btn_image, '#C0C0C0', pts); 1110 } 1111 this.left_btn=new Button({'parent':this, 1112 'position':[0, 0], 1113 'size':[this.size[1], this.size[1]], 1114 'image':left_btn_image}); 1115 this.left_btn.onClick(this.scrollLeft, this); 1116 1117 var right_btn_image=pars.right_btn_image; 1118 if(!right_btn_image){ 1119 right_btn_image=new gamejs.Surface([this.size[1], this.size[1]]); 1120 var pts=[[0, 0], 1121 [this.size[1], this.size[1]/2], 1122 [0, this.size[1]]]; 1123 gamejs.draw.polygon(right_btn_image, '#C0C0C0', pts); 1124 } 1125 this.right_btn=new Button({'parent':this, 1126 'position':[this.size[0]-this.size[1], 0], 1127 'size':[this.size[1], this.size[1]], 1128 'image':right_btn_image}); 1129 1130 this.right_btn.onClick(this.scrollRight, this); 1131 1132 //scroller track size 1133 this.sts=this.size[0]-this.right_btn.size[0]-this.left_btn.size[0]; 1134 1135 var scroller_image=pars.scroller_image; 1136 if(!scroller_image){ 1137 scroller_image=new gamejs.Surface([this.size[1], this.size[1]]); 1138 var sz=scroller_image.getSize() 1139 gamejs.draw.rect(scroller_image, '#C0C0C0', new gamejs.Rect([0, 0],[sz[0], sz[1]])); 1140 } 1141 var size=[Math.max(parseInt((this.size[0]-2*this.size[1])/2),scroller_image.getSize()[0]), this.size[1]]; 1142 this.scroller=new this.scroller_class({'parent':this, 1143 'position':[this.size[1], 0], 1144 'image':scroller_image, 1145 'size':size, 1146 'min_x':this.size[1], 1147 'max_x':this.size[0]-this.right_btn.size[0]-size[0], 1148 'min_y':0, 1149 'max_y':0}); 1150 1151 1152 this.scroll_pos=0; 1153 this.max_scroll_pos=this.sts-this.scroller.size[0]; 1154 1155 this.scroller.on(EVT_DRAG, function(event){ 1156 this.setScrollPX(event.new_pos[0]-this.size[1]); 1157 }, this); 1158 }; 1159 gamejs.utils.objects.extend(HorizontalScrollbar, View); 1160 1161 HorizontalScrollbar.prototype.scroller_class=Scroller; 1162 1163 /** 1164 *set relative scroller width 1165 *@function 1166 *@param {Number} sz relative scroller width, between 0.1 and 1, 1167 */ 1168 HorizontalScrollbar.prototype.setScrollerSize=function(sz){ 1169 sz=Math.min(Math.max(sz, 0.1), 1); 1170 this.scroller.resize([this.sts*sz, this.scroller.size[1]]); 1171 1172 this.max_scroll_pos=this.sts-this.scroller.size[0]; 1173 this.scroller.max_x=this.size[0]-this.left_btn.size[0]-this.scroller.size[0]; 1174 this.refresh(); 1175 }; 1176 1177 /** 1178 *set scroll amount, px 1179 *@function 1180 *@param {Number} pos scroll amount, px 1181 */ 1182 HorizontalScrollbar.prototype.setScrollPX=function(pos){ 1183 this.scroller.move([pos+this.left_btn.size[0], 0]); 1184 var pos_x=this.scroller.position[0]-this.left_btn.size[0]; 1185 this.scroll_pos=pos_x; 1186 var scroll=0; 1187 if(this.max_scroll_pos>0){ 1188 scroll=pos_x/this.max_scroll_pos; 1189 } 1190 this.despatchEvent({'type':EVT_SCROLL, 1191 'scroll_px':pos_x, 1192 'scroll':scroll}); 1193 this.refresh(); 1194 }; 1195 1196 /** 1197 *set scroll amount, relative 1198 *@function 1199 *@param {Number} pos scroll amount, between 0 and 1 1200 */ 1201 HorizontalScrollbar.prototype.setScroll=function(pos){ 1202 this.setScrollPX(parseInt(this.max_scroll_pos*pos)); 1203 }; 1204 1205 /** 1206 *scroll left by 0.1 of max scrollable amount 1207 *@function 1208 */ 1209 HorizontalScrollbar.prototype.scrollLeft=function(){ 1210 this.setScrollPX(Math.max(0, this.scroll_pos-this.max_scroll_pos*0.1)); 1211 }; 1212 1213 /** 1214 *scroll right by 0.1 of max scrollable amount 1215 *@function 1216 */ 1217 HorizontalScrollbar.prototype.scrollRight=function(){ 1218 this.setScrollPX(Math.min(this.max_scroll_pos, this.scroll_pos+this.max_scroll_pos*0.1)); 1219 }; 1220 1221 /** 1222 *@class !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 1223 *@augments View 1224 * 1225 *@param {gamejs.Surface} top_btn_image image for top scrollbar button 1226 *@param {gamejs.Surface} scroller_image image for scroller 1227 *@param {gamejs.Surface} bot_btn_image image for bottom scrollbar button 1228 *@param {View} parent parent element 1229 *@param {Array} size array containing width & height of the element, eg. [width, height] 1230 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 1231 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 1232 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 1233 * 1234 *@property {Button} top_btn top scrollbar button 1235 *@property {Button} bot_btn bottom scrollbar button 1236 *@property {Scroller} scroller 1237 *@property {Function} scroller_class scroller class used to create scroller #TODO find better way to implement customization 1238 */ 1239 1240 var VerticalScrollbar=exports.VerticalScrollbar=function(pars){ 1241 VerticalScrollbar.superConstructor.apply(this, [pars]); 1242 this.type='verticalscrollbar'; 1243 var top_btn_image=pars.top_btn_image; 1244 if(!top_btn_image){ 1245 top_btn_image=new gamejs.Surface([this.size[0], this.size[0]]); 1246 var pts=[[this.size[0]/2, 0], 1247 [0, this.size[0]], 1248 [this.size[0], this.size[0]]]; 1249 gamejs.draw.polygon(top_btn_image, '#C0C0C0', pts); 1250 } 1251 this.top_btn=new Button({'parent':this, 1252 'position':[0, 0], 1253 'size':[this.size[0], this.size[0]], 1254 'image':top_btn_image}); 1255 this.top_btn.onClick(this.scrollUp, this); 1256 1257 var bot_btn_image=pars.bot_btn_image; 1258 if(!bot_btn_image){ 1259 bot_btn_image=new gamejs.Surface([this.size[0], this.size[0]]); 1260 var pts=[[0, 0], 1261 [this.size[0], 0], 1262 [this.size[0]/2, this.size[0]]]; 1263 gamejs.draw.polygon(bot_btn_image, '#C0C0C0', pts); 1264 } 1265 this.bot_btn=new Button({'parent':this, 1266 'position':[0, this.size[1]-this.size[0]], 1267 'size':[this.size[0], this.size[0]], 1268 'image':bot_btn_image}); 1269 1270 this.bot_btn.onClick(this.scrollDown, this); 1271 1272 //scroller track size 1273 this.sts=this.size[1]-this.bot_btn.size[1]-this.top_btn.size[1]; 1274 1275 var scroller_image=pars.scroller_image; 1276 if(!scroller_image){ 1277 scroller_image=new gamejs.Surface([this.size[0], this.size[0]]); 1278 var sz=scroller_image.getSize() 1279 gamejs.draw.rect(scroller_image, '#C0C0C0', new gamejs.Rect([0, 0],[sz[0], sz[1]])); 1280 } 1281 var size=[this.size[0], Math.max(parseInt((this.size[1]-2*this.size[0])/2),scroller_image.getSize()[1])]; 1282 this.scroller=new this.scroller_class({'parent':this, 1283 'position':[0, this.size[0]], 1284 'image':scroller_image, 1285 'size':size, 1286 'min_x':0, 1287 'max_x':0, 1288 'min_y':this.size[0], 1289 'max_y':this.size[1]-this.bot_btn.size[1]-size[1]}); 1290 1291 1292 this.scroll_pos=0; 1293 this.max_scroll_pos=this.sts-this.scroller.size[1]; 1294 1295 this.scroller.on(EVT_DRAG, function(event){ 1296 this.setScrollPX(event.new_pos[1]-this.size[0]); 1297 }, this); 1298 }; 1299 gamejs.utils.objects.extend(VerticalScrollbar, View); 1300 1301 VerticalScrollbar.prototype.scroller_class=Scroller; 1302 1303 /** 1304 *set relative scroller width 1305 *@function 1306 *@param {Number} sz relative scroller width, between 0.1 and 1, 1307 */ 1308 VerticalScrollbar.prototype.setScrollerSize=function(sz){ 1309 sz=Math.min(Math.max(sz, 0.1), 1); 1310 this.scroller.resize([this.scroller.size[0], this.sts*sz]); 1311 1312 this.max_scroll_pos=this.sts-this.scroller.size[1]; 1313 this.scroller.max_y=this.size[1]-this.bot_btn.size[1]-this.scroller.size[1]; 1314 this.refresh(); 1315 }; 1316 1317 /** 1318 *set scroll amount, px 1319 *@function 1320 *@param {Number} pos scroll amount, px 1321 */ 1322 VerticalScrollbar.prototype.setScrollPX=function(pos){ 1323 this.scroller.move([0, pos+this.top_btn.size[1]]); 1324 var pos_y=this.scroller.position[1]-this.top_btn.size[1]; 1325 this.scroll_pos=pos_y; 1326 var scroll=0; 1327 if(this.max_scroll_pos>0){ 1328 scroll=pos_y/this.max_scroll_pos; 1329 } 1330 this.despatchEvent({'type':EVT_SCROLL, 1331 'scroll_px':pos_y, 1332 'scroll':scroll}); 1333 this.refresh(); 1334 }; 1335 1336 /** 1337 *set scroll amount, relative 1338 *@function 1339 *@param {Number} pos scroll amount, between 0 and 1 1340 */ 1341 VerticalScrollbar.prototype.setScroll=function(pos){ 1342 this.setScrollPX(parseInt(this.max_scroll_pos*pos)); 1343 }; 1344 1345 /** 1346 *@function 1347 *scroll up by 0.1 of max scrollable amount 1348 */ 1349 VerticalScrollbar.prototype.scrollUp=function(){ 1350 this.setScrollPX(Math.max(0, this.scroll_pos-this.max_scroll_pos*0.1)); 1351 }; 1352 1353 /** 1354 *@function 1355 *scroll down by 0.1 of max scrollable amount 1356 */ 1357 VerticalScrollbar.prototype.scrollDown=function(){ 1358 this.setScrollPX(Math.min(this.max_scroll_pos, this.scroll_pos+this.max_scroll_pos*0.1)); 1359 }; 1360 1361 /** 1362 *@class view with scrollable content !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 1363 *@augments View 1364 * 1365 *@param {View} parent parent element 1366 *@param {Array} size array containing width & height of the element, eg. [width, height] 1367 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 1368 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 1369 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 1370 */ 1371 var ScrollableView=exports.ScrollableView=function(pars){ 1372 ScrollableView.superConstructor.apply(this, [pars]); 1373 this.type='scrollableview'; 1374 this.scroll_x=0; 1375 this.scroll_y=0; 1376 this.max_scroll_x=0; 1377 this.max_scroll_y=0; 1378 this.scrollable_area=[0, 0]; 1379 this.setScrollableArea(this.size); 1380 this.vertical_scrollbar=null; 1381 this.horizontal_scrollbar=null; 1382 }; 1383 gamejs.utils.objects.extend(ScrollableView, View); 1384 1385 /** 1386 *set vertical scrollbar for this view 1387 *@function 1388 *@param {VerticalScrollbar} scrollbar 1389 */ 1390 ScrollableView.prototype.setVerticalScrollbar=function(scrollbar){ 1391 this.vertical_scrollbar=scrollbar; 1392 scrollbar.on(EVT_SCROLL, function(event){ 1393 this.setScrollY(Math.ceil(event.scroll*this.max_scroll_y)); 1394 }, this); 1395 }; 1396 1397 /** 1398 *set horizontal scrollbar for this view 1399 *@function 1400 *@param {HorizontalScrollbar} scrollbar 1401 */ 1402 ScrollableView.prototype.setHorizontalScrollbar=function(scrollbar){ 1403 this.horizontal_scrollbar=scrollbar; 1404 scrollbar.on(EVT_SCROLL, function(event){ 1405 this.setScrollX(Math.ceil(event.scroll*this.max_scroll_x)); 1406 }, this); 1407 }; 1408 1409 /** 1410 *manually set size of scrollable area 1411 *@function 1412 *@param {Array} area scrollbale area, [width, height] 1413 */ 1414 ScrollableView.prototype.setScrollableArea=function(area){ 1415 this.scrollable_area=area; 1416 this.max_scroll_y=Math.max(area[1]-this.size[1], 0); 1417 this.max_scroll_x=Math.max(area[0]-this.size[0], 0); 1418 if(this.vertical_scrollbar){ 1419 var sz=Math.max(Math.min(1, this.size[1]/area[1]), 0.1); 1420 this.vertical_scrollbar.setScrollerSize(sz); 1421 } 1422 if(this.horizontal_scrollbar){ 1423 var sz=Math.max(Math.min(1, this.size[0]/area[0]), 0.1); 1424 this.horizontal_scrollbar.setScrollerSize(sz); 1425 } 1426 }; 1427 1428 /** 1429 *automatically set scrollable area based on children positions and sizes 1430 *@function 1431 */ 1432 ScrollableView.prototype.autoSetScrollableArea=function(){ 1433 scrollable_area=[0, 0]; 1434 this.children.forEach(function(child){ 1435 scrollable_area[0]=Math.max(scrollable_area[0], child.position[0]+child.size[0]+20); 1436 scrollable_area[1]=Math.max(scrollable_area[1], child.position[1]+child.size[1]+20); 1437 }, this); 1438 this.setScrollableArea(scrollable_area); 1439 }; 1440 1441 /** 1442 *TODO: implement optional auto setting scrollable area when children are added 1443 *@function 1444 */ 1445 ScrollableView.prototype.addChild=function(child){ 1446 View.prototype.addChild.apply(this, [child]); 1447 this.refresh(); 1448 }; 1449 1450 /** 1451 *implements child blitting adjusted to scroll state 1452 *@function 1453 */ 1454 ScrollableView.prototype.blitChild=function(child){ 1455 this.surface.blit(child.surface, [child.position[0]-this.scroll_x, child.position[1]-this.scroll_y]); 1456 }; 1457 1458 /** 1459 *adjusts event position based on scroll state 1460 *@function 1461 */ 1462 ScrollableView.prototype.despatchEvent=function(event){ 1463 if(event.pos){ 1464 event=cloneEvent(event); 1465 event.pos=[event.pos[0]+this.scroll_x, event.pos[1]+this.scroll_y]; 1466 } 1467 View.prototype.despatchEvent.apply(this, [event]); 1468 }; 1469 1470 /** 1471 *increment horizontal scroll 1472 *@function 1473 *@param {Number} x px to increment horizontal scroll by 1474 */ 1475 ScrollableView.prototype.scrollX=function(x){ 1476 this.setScrollX(this.scroll_x+x); 1477 this.refresh(); 1478 }; 1479 1480 /** 1481 *increment vertical scroll 1482 *@function 1483 *@param {Number} y px to increment vertical scroll by 1484 */ 1485 ScrollableView.prototype.scrollY=function(y){ 1486 this.setScrollY(this.scroll_y+y); 1487 this.refresh(); 1488 }; 1489 1490 /** 1491 *set horizontal scroll 1492 *@function 1493 *@param {Number} x horizontal scroll, px 1494 */ 1495 ScrollableView.prototype.setScrollX=function(x){ 1496 this.scroll_x=Math.min(Math.max(x, 0), this.max_scroll_x); 1497 this.refresh(); 1498 }; 1499 1500 /** 1501 *set vertical scroll 1502 *@function 1503 *@param {Number} y vertical scroll, px 1504 */ 1505 ScrollableView.prototype.setScrollY=function(y){ 1506 this.scroll_y=Math.min(Math.max(y, 0), this.max_scroll_y); 1507 this.refresh(); 1508 }; 1509 1510 /** 1511 *@class text input !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 1512 *@augments View 1513 * 1514 *@param {CachedFont} font 1515 *@param {String} text 1516 *@param {Array} scw_size actual text display size, [width, height]. 1517 *@param {View} parent parent element 1518 *@param {Array} size array containing width & height of the element, eg. [width, height] 1519 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 1520 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 1521 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 1522 * 1523 *@property {CachedFont} font 1524 *@property {String} text 1525 */ 1526 var TextInput=exports.TextInput=function(pars){ 1527 TextInput.superConstructor.apply(this, [pars]); 1528 this.font=pars.font ? pars.font : exports.DEFAULT_FONT; 1529 this.text=pars.text ? pars.text : ''; 1530 this.blip=false; 1531 this.pos=0; 1532 this.ms=500; 1533 1534 this.scw=new ScrollableView({'parent':this, 1535 'position':[0, 0], 1536 'size':pars.scw_size || this.size}); 1537 this.center(this.scw); 1538 this.label=new Label({'parent':this.scw, 1539 'position':[0, 0], 1540 'font':this.font, 1541 'text':this.text}); 1542 this.scw.center(this.label); 1543 this.label.move([3, this.label.position[1]]); 1544 1545 this.bliplabel=new Label({'parent':this.scw, 1546 'position':[0, 0], 1547 'font':this.font, 1548 'visible':false, 1549 'text':'|'}); 1550 1551 this.on(EVT_KEY_DOWN, this.onKeyDown, this); 1552 this.on(EVT_FOCUS, this.blipon, this); 1553 this.on(EVT_BLUR, function(){ 1554 this.blip=false; 1555 }, this); 1556 this.setPos(this.text.length); 1557 }; 1558 gamejs.utils.objects.extend(TextInput, View); 1559 1560 /** 1561 *turn blip on 1562 *@ignore 1563 */ 1564 TextInput.prototype.blipon=function(event){ 1565 this.blip=true; 1566 this.ms=500; 1567 this.refresh(); 1568 }; 1569 1570 /** 1571 *implements blip updating 1572 *@function 1573 *@param {Number} msDuration 1574 */ 1575 TextInput.prototype.update=function(msDuration){ 1576 if(this.isFocused()){ 1577 this.ms-=msDuration; 1578 if(this.ms<0){ 1579 this.ms=500; 1580 this.blip=!this.blip; 1581 }; 1582 if(this.blip){ 1583 this.bliplabel.show(); 1584 }else{ 1585 this.bliplabel.hide(); 1586 } 1587 }else{ 1588 this.bliplabel.hide(); 1589 } 1590 }; 1591 1592 /** 1593 *default implementation: white fill, gray border 1594 *@function 1595 */ 1596 TextInput.prototype.paint=function(){ 1597 this.surface.fill('#FFF'); 1598 gamejs.draw.rect(this.surface, '#COCOCO', new gamejs.Rect([0, 0], this.size), 1); 1599 }; 1600 1601 /** 1602 *set input text. generates EVT_CHANGE 1603 *@function 1604 *@param {String} text 1605 */ 1606 TextInput.prototype.setText=function(text){ 1607 this.setPos(this.text.length); 1608 this._setText(text); 1609 }; 1610 1611 /** 1612 *set blip position 1613 *@ignore 1614 */ 1615 TextInput.prototype.setPos=function(pos){ 1616 this.pos=Math.min(Math.max(pos, 0), this.text.length); 1617 1618 //calc offset for scorllable area 1619 var ofst=0; 1620 var ofst=0; 1621 var tlen=this.font.getTextSize(this.text.substr(0, this.pos))[0]; 1622 ofst=Math.max(tlen-this.scw.size[0]+this.font.getTextSize('s')[0]); 1623 1624 this.scw.setScrollX(ofst); 1625 this.bliplabel.move([Math.max(this.font.getTextSize(this.text.substr(0, this.pos))[0]+this.label.position[0]-2, 0), this.label.position[1]]); 1626 1627 }; 1628 1629 /** 1630 *@ignore 1631 */ 1632 TextInput.prototype._setText=function(text){ 1633 this.text=text; 1634 this.label.setText(text); 1635 this.scw.autoSetScrollableArea(); 1636 this.refresh(); 1637 this.despatchEvent({'type':EVT_CHANGE,'value':text}); 1638 }; 1639 1640 /** 1641 *key down handler 1642 *@ignore 1643 */ 1644 TextInput.prototype.onKeyDown=function(event){ 1645 var charcode=event.key; 1646 if(charcode==13){ 1647 //TODO 1648 } 1649 //BACKSPACE 1650 if(charcode==8){ 1651 if(this.text){ 1652 if(this.pos==this.text.length){ 1653 this._setText(this.text.substr(0,this.text.length-1)); 1654 }else { 1655 this._setText(this.text.substr(0, this.pos-1)+this.text.substr(this.pos, this.text.length)); 1656 } 1657 this.blipon(); 1658 this.setPos(this.pos-1); 1659 } 1660 } 1661 //WRITEABLE SYMBOLS, 0 to z or space 1662 if(((charcode>=48) && (charcode<=90))||(charcode==32)){ 1663 var c=String.fromCharCode(charcode); 1664 if(event.shiftKey)c=c.toUpperCase(); 1665 else c=c.toLowerCase(); 1666 if(this.pos==this.text.length){ 1667 this._setText(this.text+c); 1668 }else{ 1669 this._setText(this.text.substr(0, this.pos)+c+this.text.substr(this.pos, this.text.length)); 1670 } 1671 this.setPos(this.pos+1); 1672 this.blipon(); 1673 } 1674 1675 //LEFT 1676 if(charcode==37){ 1677 this.setPos(this.pos-1); 1678 this.blipon(); 1679 } 1680 //RIGHT 1681 if(charcode==39){ 1682 this.setPos(this.pos+1); 1683 this.blipon(); 1684 } 1685 }; 1686 1687 /** 1688 *@class a centered dialog position at the top of the GUI. disables and grays out rest of the guy 1689 *@augments Frame 1690 * 1691 *@param {GUI} parent parent GUI object 1692 *@param {Bool} constrain if true, frame cannot be moved out of visible area 1693 *@param {Array} size array containing width & height of the element, eg. [width, height] 1694 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 1695 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 1696 *@param {Bool} visible is this view visible? default true 1697 */ 1698 1699 var Dialog=exports.Dialog=function(pars){ 1700 pars.position=getCenterPos(pars.parent.size, pars.size); 1701 Dialog.superConstructor.apply(this, [pars]); 1702 1703 }; 1704 1705 gamejs.utils.objects.extend(Dialog, Frame); 1706 1707 /** 1708 *lock GUI & show dialog 1709 *@function 1710 */ 1711 Dialog.prototype.show=function(){ 1712 this.getGUI().lockFrame(this); 1713 Frame.prototype.show.apply(this, []); 1714 }; 1715 1716 /** 1717 *unlock gui & hide dialog 1718 *@function 1719 */ 1720 Dialog.prototype.close=function(){ 1721 this.getGUI().unlockFrame(); 1722 Frame.prototype.close.apply(this, []); 1723 }; 1724 1725 /** 1726 *@class multi-line, line-wrapped text dislay !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL! 1727 *@augments View 1728 * 1729 *@param {CachedFont} font 1730 *@param {String} text 1731 *@param {Bool} justify if true, text is justified. By default, it's left-aligned 1732 *@param {View} parent parent element 1733 *@param {Array} size array containing width & height of the element, eg. [width, height] 1734 *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0] 1735 *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL 1736 *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true 1737 */ 1738 var Text=exports.Text=function(pars){ 1739 pars.size=[pars.width, 1]; 1740 Text.superConstructor.apply(this, [pars]); 1741 this.width=pars.width; 1742 this.font=pars.font ? pars.font : exports.DEFAULT_FONT; 1743 this.setText(pars.text); 1744 this.justify=pars.justify; 1745 }; 1746 gamejs.utils.objects.extend(Text, View); 1747 1748 /** 1749 *set text 1750 *@function 1751 *@param {String} text 1752 */ 1753 Text.prototype.setText=function(text){ 1754 //wow, is this a hacky mess! 1755 1756 this.text=text; 1757 this.lines=[]; 1758 var nlines=text.split(/\r\n|\r|\n/); 1759 nlines.push(' '); 1760 var line, words; 1761 var i, ci, c, l; 1762 var word=''; 1763 var height=0; 1764 var n_line_length=0; 1765 var n_line=''; 1766 for(i=0;i<nlines.length;i++){ 1767 line=nlines[i]; 1768 line+=' '; 1769 for(ci=0;ci<line.length;ci++){ 1770 c=line[ci]; 1771 if(c==' ' || c=='\t'){ 1772 if(word){ 1773 l=this.font.getTextSize(word)[0]; 1774 if((n_line_length+l>this.size[0]) && (!(n_line==''))){ 1775 this.lines.push({'t':n_line, 'e':false}); 1776 height+=this.font.getTextSize(n_line)[1]; 1777 n_line=''; 1778 n_line_length=0; 1779 if(word[0]==' ' || word[0]=='\t'){ 1780 word=word.substr(1, word.length); 1781 } 1782 } 1783 n_line+=word; 1784 n_line_length+=l; 1785 1786 } 1787 word=''; 1788 word+=c; 1789 }else{ 1790 word+=c; 1791 } 1792 } 1793 if(n_line){ 1794 this.lines.push({'t':n_line, 'e':true}); 1795 height+=this.font.getTextSize(n_line)[1]; 1796 n_line=''; 1797 n_line_length=0; 1798 } 1799 } 1800 this.resize([this.width, height]); 1801 this.refresh(); 1802 }; 1803 1804 /** 1805 *@function 1806 */ 1807 Text.prototype.paint=function(){ 1808 View.prototype.paint.apply(this, []); 1809 var pos=0; 1810 this.lines.forEach(function(line){ 1811 var sz=this.font.getTextSize(line.t); 1812 var extra_w=0; 1813 if(this.justify && (!line.e)){ 1814 var space_count=0; 1815 for(var i=0;i<line.t.length;i++){ 1816 if(line.t[i]==' ') space_count++; 1817 } 1818 extra_w=Math.floor((this.size[0]-sz[0])/space_count); 1819 } 1820 this.font.render(this.surface, line.t, [0, pos], this.font.space_width+extra_w); 1821 pos+=sz[1]; 1822 }, this); 1823 } 1824 1825 /** 1826 *@class root GUI object. Handles gamejs events, frames 1827 *@augments View 1828 * 1829 *@param {gamejs.Surface} surface surface to render GUI on 1830 */ 1831 var GUI=exports.GUI=function(surface){ 1832 GUI.superConstructor.apply(this, [{'position':[0, 0], 1833 'parent':null, 1834 'size':surface.getSize(), 1835 'surface':surface}]); 1836 this.type='gui'; 1837 this.locked_frame=null; 1838 this.frames=[]; 1839 }; 1840 1841 gamejs.utils.objects.extend(GUI, View); 1842 1843 /** 1844 *redraw GUI, if needed 1845 *@function 1846 *@param {Bool} force_redraw if true GUI is redrawn even if tehre are no internal changes 1847 */ 1848 GUI.prototype.draw=function(force_redraw){ 1849 if(force_redraw)this.refresh(); 1850 var painted=View.prototype.draw.apply(this, []); 1851 this.frames.forEach(function(frame){ 1852 if(frame.visible && (frame.draw() || painted)){ 1853 if(this.locked_frame && (this.locked_frame.id==frame.id)){ 1854 this.refresh(); 1855 this.blur_bg(); 1856 } 1857 this.surface.blit(frame.surface, frame.position); 1858 } 1859 }, this); 1860 }; 1861 1862 /** 1863 *Does nothing! don't remove! 1864 *@function 1865 */ 1866 GUI.prototype.paint=function(){}; 1867 1868 GUI.prototype.blur_bg=function(){ 1869 gamejs.draw.rect(this.surface, 'rgba(192,192, 192, 0.5)', new gamejs.Rect([0, 0], this.size),0); 1870 }; 1871 1872 /** 1873 *Remove a frame from GUI 1874 *@function 1875 *@param {Frame|Id} frame frame object or id of frame to remove 1876 */ 1877 1878 GUI.prototype.removeFrame=function(frame){ 1879 if(typeof(frame)!='number')frame=frame.id; 1880 for(var i=0;i<this.frames.length;i++){ 1881 if(this.frames[i].id==frame){ 1882 frame=this.frames.splice(i, 1); 1883 this.refresh(); 1884 return true; 1885 } 1886 } 1887 return false; 1888 }; 1889 1890 /** 1891 *Move a frame to the top 1892 *@function 1893 *@param {Frame} frame 1894 */ 1895 GUI.prototype.moveFrameToTop=function(frame){ 1896 for(var i=0;i<this.frames.length;i++){ 1897 var f=this.frames[i]; 1898 if(f.id==frame.id){ 1899 if(i==this.frames.length-1) return; 1900 this.despatchEvent({'type':EVT_BLUR}); 1901 this.frames.splice(i, 1); 1902 this.frames.push(f); 1903 this.refresh(); 1904 frame.despatchEvent({'type':EVT_FOCUS}); 1905 break; 1906 } 1907 } 1908 }; 1909 1910 /** 1911 *Update GUI and its child objects 1912 *@function 1913 *@param {Number} msDuration miliseconds since last update 1914 */ 1915 GUI.prototype.update=function(msDuration){ 1916 this.children.forEach(function(child){ 1917 child._update(msDuration); 1918 }); 1919 this.frames.forEach(function(frame){ 1920 frame._update(msDuration); 1921 }); 1922 1923 }; 1924 1925 /** 1926 *@ignore 1927 */ 1928 GUI.prototype.lockFrame=function(frame){ 1929 this.locked_frame=frame; 1930 this.refresh(); 1931 }; 1932 1933 /** 1934 *@ignore 1935 */ 1936 GUI.prototype.unlockFrame=function(){ 1937 this.locked_frame=null; 1938 this.refresh(); 1939 }; 1940 1941 /** 1942 *despatch gamejs event 1943 *@function 1944 *@param {gamejs Event| GUI event} event event generated by gamejs, or a GUI event if needed. 1945 */ 1946 GUI.prototype.despatchEvent=function(event){ 1947 if(event.pos)event.global_pos=event.pos; 1948 1949 var i, frame; 1950 //dispatching mouse events to frames: if event is dispatched to a frame, don't dispatch it anywhere else. 1951 if(event.type==EVT_MOUSE_DOWN || event.type==EVT_MOUSE_MOTION || event.type==EVT_MOUSE_UP){ 1952 var frame; 1953 var hit=false; 1954 var clicked_on=null; 1955 var moused_on=null; 1956 var topframe=null; 1957 for(i=this.frames.length-1; i>=0;i--){ 1958 frame=this.frames[i]; 1959 1960 //if frame is locked, dispatch events only to that frame 1961 if(this.locked_frame &&(this.locked_frame.id!=frame.id)) continue; 1962 1963 if(frame.visible && frame.getRect().collidePoint(event.pos)){ 1964 frame.despatchEvent(cloneEvent(event, frame.position)); 1965 if(event.type==EVT_MOUSE_DOWN){ 1966 clicked_on=i; 1967 } 1968 else if(event.type==EVT_MOUSE_MOTION){ 1969 moused_on=i; 1970 } 1971 hit=true; 1972 //mouseout view if mouse is on a frame 1973 if(frame.isFocused())topframe=i; 1974 break; 1975 }else{ 1976 //blur frame if focused but clicked somewhere else 1977 if((event.type==EVT_MOUSE_DOWN) && (frame.isFocused())){ 1978 frame.despatchEvent({'type':EVT_BLUR}); 1979 } 1980 } 1981 } 1982 1983 //blur everything else if clicked on a frame 1984 if(clicked_on!=null){ 1985 View.prototype.despatchEvent.apply(this, [{'type':EVT_BLUR}]); 1986 for(i=0;i<this.frames.length;i++){ 1987 if(i!=clicked_on) this.frames[i].despatchEvent({'type':EVT_BLUR}); 1988 } 1989 } 1990 1991 //mouseout everyhting else if clicked on a frame 1992 if(moused_on!=null){ 1993 View.prototype.despatchEvent.apply(this, [{'type':EVT_MOUSE_OUT}]); 1994 for(i=0;i<this.frames.length;i++){ 1995 if(i!=moused_on) this.frames[i].despatchEvent({'type':EVT_MOUSE_OUT}); 1996 } 1997 } 1998 1999 if(!hit &&(!this.locked_frame)){ 2000 View.prototype.despatchEvent.apply(this, [event]); 2001 } 2002 else{ 2003 View.prototype.handleEvent.apply(this, [event]); 2004 } 2005 2006 if(topframe!=null){ 2007 this.moveFrameToTop(this.frames[topframe]); 2008 } 2009 2010 }else{ 2011 if(event.type==EVT_BLUR || event.type==EVT_MOUSE_OUT || event.type==EVT_KEY_DOWN || event.type==EVT_KEY_UP){ 2012 this.frames.forEach(function(frame){ 2013 if(frame.visible &&(!this.locked_frame || (this.locked_frame.id==frame.id))){ 2014 frame.despatchEvent(cloneEvent(event, frame.position)); 2015 } 2016 }); 2017 } 2018 2019 if(!this.locked_frame) View.prototype.despatchEvent.apply(this, [event]); 2020 else View.prototype.handleEvent.apply(this, [event]); 2021 } 2022 }; 2023 2024 var layout=exports.layout={ 2025 2026 /** 2027 *arranges obejcts vertically 2028 *@function 2029 *@name vertical 2030 *@lends layout 2031 *@param {Array} objects a list of gui objects (with the same parent) 2032 *@param {Number} y starting y coordinate, default 0 2033 *@param {Number} space space between objects px, default 0 2034 */ 2035 'vertical':function(objects, y, space){ 2036 y=y || 0; 2037 space = space || 0; 2038 objects.forEach(function(object){ 2039 object.move([object.position[0], y]); 2040 y+=object.size[1]+space; 2041 }); 2042 2043 }, 2044 2045 /** 2046 *arranges obejcts horizontally 2047 *@function 2048 *@name horizontal 2049 *@lends layout 2050 *@param {Array} objects a list of gui objects (with the same parent) 2051 *@param {Number} x startingx coordinate, default 0 2052 *@param {Number} space space between objects px, default 0 2053 */ 2054 'horizontal':function(objects, x, space){ 2055 x=x || 0; 2056 space = space || 0; 2057 objects.forEach(function(object){ 2058 object.move([x, object.position[1]]); 2059 x+=object.size[0]+space; 2060 }); 2061 } 2062 }; 2063 2064