- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
- 511
- 512
- 513
- 514
- 515
- 516
- 517
- 518
- 519
- 520
- 521
- 522
- 523
- 524
- 525
- 526
- 527
- 528
- 529
- 530
- 531
- 532
- 533
- 534
- 535
- 536
- 537
- 538
- 539
- 540
- 541
- 542
- 543
- 544
- 545
- 546
- 547
- 548
- 549
- 550
- 551
- 552
- 553
- 554
- 555
- 556
- 557
- 558
- 559
- 560
- 561
- 562
- 563
- 564
- 565
- 566
- 567
- 568
- 569
- 570
- 571
- 572
- 573
- 574
- 575
- 576
- 577
- 578
- 579
- 580
- 581
- 582
- 583
- 584
- 585
- 586
- 587
- 588
- 589
- 590
- 591
- 592
- 593
- 594
- 595
- 596
- 597
- 598
- 599
- 600
- 601
- 602
- 603
- 604
- 605
- 606
- 607
- 608
- 609
- 610
- 611
- 612
- 613
- 614
- 615
- 616
- 617
- 618
- 619
- 620
- 621
- 622
- 623
- 624
- 625
- 626
- 627
- 628
- 629
- 630
- 631
- 632
- 633
- 634
- 635
- 636
- 637
- 638
- 639
- 640
- 641
- 642
- 643
- 644
- 645
- 646
- 647
- 648
- 649
- 650
- 651
- 652
- 653
- 654
- 655
- 656
- 657
- 658
- 659
- 660
- 661
- 662
- 663
- 664
- 665
- 666
- 667
- 668
- 669
- 670
- 671
- 672
- 673
- 674
- 675
- 676
- 677
- 678
- 679
- 680
- 681
- 682
- 683
- 684
- 685
- 686
- 687
- 688
- 689
- 690
- 691
- 692
- 693
- 694
- 695
- 696
- 697
- 698
- 699
- 700
- 701
- 702
- 703
- 704
- 705
- 706
- 707
- 708
- 709
- 710
- 711
- 712
- 713
- 714
- 715
- 716
- 717
- 718
- 719
- 720
- 721
- 722
- 723
- 724
- 725
- 726
- 727
- 728
- 729
- 730
- 731
- 732
- 733
- 734
- 735
- 736
- 737
- 738
- 739
- 740
- 741
- 742
- 743
- 744
- 745
- 746
- 747
- 748
- 749
- 750
- 751
- 752
- 753
- 754
- 755
- 756
- 757
- 758
- 759
- 760
- 761
- 762
- 763
- 764
- 765
- 766
- 767
- 768
- 769
- 770
- 771
- 772
- 773
- 774
- 775
- 776
- 777
- 778
- 779
- 780
- 781
- 782
- 783
- 784
- 785
- 786
- 787
- 788
- 789
- 790
- 791
- 792
- 793
- 794
- 795
- 796
- 797
- 798
- 799
- 800
- 801
- 802
- 803
- 804
- 805
- 806
- 807
- 808
- 809
- 810
- 811
- 812
- 813
- 814
- 815
- 816
- 817
- 818
- 819
- 820
- 821
- 822
- 823
- 824
- 825
- 826
- 827
- 828
- 829
- 830
- 831
- 832
- 833
- 834
- 835
- 836
- 837
- 838
- 839
- 840
- 841
- 842
- 843
- 844
- 845
- 846
- 847
- 848
- 849
- 850
- 851
- 852
- 853
- 854
- 855
- 856
- 857
- 858
- 859
- 860
- 861
- 862
- 863
- 864
- 865
- 866
- 867
- 868
- 869
- 870
- 871
- 872
- 873
- 874
- 875
- 876
- 877
- 878
- 879
- 880
- 881
- 882
- 883
- 884
- 885
- 886
- 887
- 888
- 889
- 890
- 891
- 892
- 893
- 894
- 895
- 896
- 897
- 898
- 899
- 900
- 901
- 902
- 903
- 904
- 905
- 906
- 907
- 908
- 909
- 910
- 911
- 912
- 913
- 914
- 915
- 916
- 917
- 918
- 919
- 920
- 921
- 922
- 923
- 924
- 925
- 926
- 927
- 928
- 929
- 930
- 931
- 932
- 933
- 934
- 935
- 936
- 937
- 938
- 939
- 940
- 941
- 942
- 943
- 944
- 945
- 946
- 947
- 948
- 949
- 950
- 951
- 952
- 953
- 954
- 955
- 956
- 957
- 958
- 959
- 960
- 961
- 962
- 963
- 964
- 965
- 966
- 967
- 968
- 969
- 970
- 971
- 972
- 973
- 974
- 975
- 976
- 977
- 978
- 979
- 980
- 981
- 982
- 983
- 984
- 985
- 986
import Renderable from "./renderable.js";
import { createGUID } from "../utils/utils";
import { defer } from "../utils/function";
import { game } from "../index.js";
import * as event from "../system/event.js";
import pool from "../system/pooling.js";
import state from "../state/state.js";
import Body from "../physics/body.js";
/**
* Private function to re-use for object removal in a defer
* @ignore
*/
function deferredRemove(child, keepalive) {
this.removeChildNow(child, keepalive);
}
let globalFloatingCounter = 0;
/**
* @classdesc
* Container represents a collection of child objects
* @augments Renderable
*/
export default class Container extends Renderable {
/**
* @param {number} [x=0] - position of the container (accessible via the inherited pos.x property)
* @param {number} [y=0] - position of the container (accessible via the inherited pos.y property)
* @param {number} [width=game.viewport.width] - width of the container
* @param {number} [height=game.viewport.height] - height of the container
*/
constructor(x = 0, y = 0, width, height, root = false) {
// call the super constructor
super(
x, y,
typeof width === "undefined" ? (typeof game.viewport !== "undefined" ? game.viewport.width : Infinity) : width,
typeof height === "undefined" ? (typeof game.viewport !== "undefined" ? game.viewport.height : Infinity) : height
);
/**
* keep track of pending sort
* @ignore
*/
this.pendingSort = null;
/**
* whether the container is the root of the scene
* @type {boolean}
* @default false
*/
this.root = root;
/**
* The array of children of this container.
* @ignore
*/
this.children = undefined;
/**
* The property of the child object that should be used to sort on this container
* value : "x", "y", "z"
* @type {string}
* @default "z"
*/
this.sortOn = "z";
/**
* Specify if the children list should be automatically sorted when adding a new child
* @type {boolean}
* @default true
*/
this.autoSort = true;
/**
* Specify if the children z index should automatically be managed by the parent container
* @type {boolean}
* @default true
*/
this.autoDepth = true;
/**
* Specify if the container draw operation should clip his children to its own bounds
* @type {boolean}
* @default false
*/
this.clipping = false;
/**
* a callback to be extended, triggered after a child has been added or removed
* @param {number} index - added or removed child index
*/
this.onChildChange = function (index) { // eslint-disable-line no-unused-vars
// to be extended
};
/**
* Specify if the container bounds should automatically take in account
* all child bounds when updated (this is expensive and disabled by default,
* only enable if necessary)
* @type {boolean}
* @default false
*/
this.enableChildBoundsUpdate = false;
/**
* define a background color for this container
* @type {Color}
* @default (0, 0, 0, 0.0)
* @example
* // add a red background color to this container
* this.backgroundColor.setColor(255, 0, 0);
*/
this.backgroundColor = pool.pull("Color", 0, 0, 0, 0.0);
/**
* Used by the debug panel plugin
* @ignore
*/
this.drawCount = 0;
// container self apply any defined transformation
this.autoTransform = true;
// enable collision and event detection
this.isKinematic = false;
this.anchorPoint.set(0, 0);
// subscribe on the canvas resize event
if (this.root === true) {
// Workaround for not updating container child-bounds automatically (it's expensive!)
event.on(event.CANVAS_ONRESIZE, () => {
// temporarly enable the enableChildBoundsUpdate flag
this.enableChildBoundsUpdate === true;
// update bounds
this.updateBounds();
this.enableChildBoundsUpdate === false;
});
}
}
/**
* reset the container, removing all childrens, and reseting transforms.
*/
reset() {
// cancel any sort operation
if (this.pendingSort) {
clearTimeout(this.pendingSort);
this.pendingSort = null;
}
// delete all children
let children = this.getChildren();
for (let i = children.length, child; i >= 0; (child = children[--i])) {
// don't remove it if a persistent object
if (child && child.isPersistent !== true) {
this.removeChildNow(child);
}
}
if (typeof this.currentTransform !== "undefined") {
// just reset some variables
this.currentTransform.identity();
}
this.backgroundColor.setColor(0, 0, 0, 0.0);
}
/**
* Add a child to the container <br>
* if auto-sort is disable, the object will be appended at the bottom of the list.
* Adding a child to the container will automatically remove it from its other container.
* Meaning a child can only have one parent. This is important if you add a renderable
* to a container then add it to the World container it will move it out of the
* orginal container. Then when the World container reset() method is called the renderable
* will not be in any container. <br>
* if the given child implements a onActivateEvent method, that method will be called
* once the child is added to this container.
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be added
* @param {number} [z] - forces the z index of the child to the specified value
* @returns {Renderable} the added child
*/
addChild(child, z) {
if (child.ancestor instanceof Container) {
child.ancestor.removeChildNow(child);
}
else {
// only allocate a GUID if the object has no previous ancestor
// (e.g. move one child from one container to another)
if (child.isRenderable) {
// allocated a GUID value (use child.id as based index if defined)
child.GUID = createGUID(child.id);
}
}
// add the new child
child.ancestor = this;
this.getChildren().push(child);
// update child bounds to reflect the new ancestor
if (typeof child.updateBounds === "function") {
if (this.isFloating === true) {
// only parent container can be floating
child.floating = false;
}
child.updateBounds();
}
// set the child z value if required
if (typeof(child.pos) !== "undefined") {
if (typeof(z) === "number") {
child.pos.z = z;
} else if (this.autoDepth === true) {
child.pos.z = this.getChildren().length;
}
}
if (this.autoSort === true) {
this.sort();
}
if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
child.onActivateEvent();
}
// force container bounds update if required
if (this.enableChildBoundsUpdate === true) {
this.updateBounds();
}
// if a physic body(ies) to the game world
if (this.isAttachedToRoot()) {
let worldContainer = this.getRootAncestor();
if (child.body instanceof Body) {
worldContainer.addBody(child.body);
}
// if the child is a container
if (child instanceof Container) {
// add all container child bodies
// TODO: make it recursive ?
child.forEach((cchild) => {
if (cchild.body instanceof Body) {
worldContainer.addBody(cchild.body);
}
});
}
}
// mark the container for repaint
this.isDirty = true;
// triggered callback if defined
this.onChildChange.call(this, this.getChildren().length - 1);
return child;
}
/**
* Add a child to the container at the specified index<br>
* (the list won't be sorted after insertion)
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be added
* @param {number} index - The index at which to insert the child
* @returns {Renderable} the added child
*/
addChildAt(child, index) {
if (index >= 0 && index < this.getChildren().length) {
if (child.ancestor instanceof Container) {
child.ancestor.removeChildNow(child);
}
else {
// only allocate a GUID if the object has no previous ancestor
// (e.g. move one child from one container to another)
if (child.isRenderable) {
// allocated a GUID value
child.GUID = createGUID();
}
}
// add the new child
child.ancestor = this;
this.getChildren().splice(index, 0, child);
// update child bounds to reflect the new ancestor
if (typeof child.updateBounds === "function") {
if (this.isFloating === true) {
// only parent container can be floating
child.floating = false;
}
child.updateBounds();
}
if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
child.onActivateEvent();
}
// force container bounds update if required
if (this.enableChildBoundsUpdate === true) {
this.updateBounds();
}
// if a physic body(ies) to the game world
if (this.isAttachedToRoot()) {
let worldContainer = this.getRootAncestor();
if (child.body instanceof Body) {
worldContainer.addBody(child.body);
}
// if the child is a container
if (child instanceof Container) {
// add all container child bodies
// TODO: make it recursive ?
child.forEach((cchild) => {
if (cchild.body instanceof Body) {
worldContainer.addBody(cchild.body);
}
});
}
}
// mark the container for repaint
this.isDirty = true;
// triggered callback if defined
this.onChildChange.call(this, index);
return child;
}
else {
throw new Error("Index (" + index + ") Out Of Bounds for addChildAt()");
}
}
/**
* The forEach() method executes a provided function once per child element. <br>
* the callback function is invoked with three arguments: <br>
* - The current element being processed in the array <br>
* - The index of element in the array. <br>
* - The array forEach() was called upon. <br>
* @param {Function} callback - fnction to execute on each element
* @param {object} [thisArg] - value to use as this(i.e reference Object) when executing callback.
* @example
* // iterate through all children of this container
* container.forEach((child) => {
* // do something with the child
* child.doSomething();
* });
* container.forEach((child, index) => { ... });
* container.forEach((child, index, array) => { ... });
* container.forEach((child, index, array) => { ... }, thisArg);
*/
forEach(callback, thisArg) {
let context = this, i = 0;
let children = this.getChildren();
let len = children.length;
if (typeof callback !== "function") {
throw new Error(callback + " is not a function");
}
if (arguments.length > 1) {
context = thisArg;
}
while (i < len) {
callback.call(context, children[i], i, children);
i++;
}
}
/**
* Swaps the position (z-index) of 2 children
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be added
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child2 - Child to be added
*/
swapChildren(child, child2) {
let index = this.getChildIndex(child);
let index2 = this.getChildIndex(child2);
if ((index !== -1) && (index2 !== -1)) {
// swap z index
let _z = child.pos.z;
child.pos.z = child2.pos.z;
child2.pos.z = _z;
// swap the positions..
this.getChildren()[index] = child2;
this.getChildren()[index2] = child;
// mark the container as dirty
this.isDirty = true;
}
else {
throw new Error(child + " Both the supplied childs must be a child of the caller " + this);
}
}
/**
* Returns the Child at the specified index
* @param {number} index - The index of the child
* @returns {Renderable} the child at the specified index
*/
getChildAt(index) {
if (index >= 0 && index < this.getChildren().length) {
return this.getChildren()[index];
}
else {
throw new Error("Index (" + index + ") Out Of Bounds for getChildAt()");
}
}
/**
* Returns the index of the given Child
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - The child object
* @returns {number} index
*/
getChildIndex(child) {
return this.getChildren().indexOf(child);
}
/**
* Returns the next child within the container or undefined if none
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - The child object
* @returns {Renderable} child
*/
getNextChild(child) {
let index = this.getChildren().indexOf(child) - 1;
if (index >= 0 && index < this.getChildren().length) {
return this.getChildAt(index);
}
return undefined;
}
/**
* Returns true if contains the specified Child
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - The child object
* @returns {boolean}
*/
hasChild(child) {
return this === child.ancestor;
}
/**
* return the child corresponding to the given property and value.<br>
* note : avoid calling this function every frame since
* it parses the whole object tree each time
* @param {string} prop - Property name
* @param {string|RegExp|number|boolean} value - Value of the property
* @returns {Renderable[]} Array of childs
* @example
* // get the first child object called "mainPlayer" in a specific container :
* let ent = myContainer.getChildByProp("name", "mainPlayer");
*
* // or query the whole world :
* let ent = container.getChildByProp("name", "mainPlayer");
*
* // partial property matches are also allowed by using a RegExp.
* // the following matches "redCOIN", "bluecoin", "bagOfCoins", etc :
* let allCoins = container.getChildByProp("name", /coin/i);
*
* // searching for numbers or other data types :
* let zIndex10 = container.getChildByProp("z", 10);
* let inViewport = container.getChildByProp("inViewport", true);
*/
getChildByProp(prop, value) {
let objList = [];
/**
* @ignore
*/
function compare(obj, prop) {
let v = obj[prop];
if (value instanceof RegExp && typeof(v) === "string") {
if (value.test(v)) {
objList.push(obj);
}
}
else if (v === value) {
objList.push(obj);
}
}
this.forEach((child) => {
compare(child, prop);
if (child instanceof Container) {
objList = objList.concat(child.getChildByProp(prop, value));
}
});
return objList;
}
/**
* returns the list of childs with the specified class type
* @param {object} classType - Class type
* @returns {Renderable[]} Array of children
*/
getChildByType(classType) {
let objList = [];
this.forEach((child) => {
if (child instanceof classType) {
objList.push(child);
}
if (child instanceof Container) {
objList = objList.concat(child.getChildByType(classType));
}
});
return objList;
}
/**
* returns the list of childs with the specified name<br>
* as defined in Tiled (Name field of the Object Properties)<br>
* note : avoid calling this function every frame since
* it parses the whole object list each time
* @param {string|RegExp|number|boolean} name - child name
* @returns {Renderable[]} Array of children
*/
getChildByName(name) {
return this.getChildByProp("name", name);
}
/**
* return the child corresponding to the specified GUID<br>
* note : avoid calling this function every frame since
* it parses the whole object list each time
* @param {string|RegExp|number|boolean} guid - child GUID
* @returns {Renderable} corresponding child or null
*/
getChildByGUID(guid) {
let obj = this.getChildByProp("GUID", guid);
return (obj.length > 0) ? obj[0] : null;
}
/**
* return all child in this container
* @returns {Renderable[]} an array of renderable object
*/
getChildren() {
if (typeof this.children === "undefined") {
this.children = [];
}
return this.children;
}
/**
* update the bounding box for this container.
* @param {boolean} [absolute=true] - update the bounds size and position in (world) absolute coordinates
* @returns {Bounds} this container bounding box Rectangle object
*/
updateBounds(absolute = true) {
let bounds = this.getBounds();
// call parent method
super.updateBounds(absolute);
if (this.enableChildBoundsUpdate === true) {
this.forEach((child) => {
if (child.isRenderable) {
let childBounds = child.updateBounds(true);
if (childBounds.isFinite()) {
bounds.addBounds(childBounds);
}
}
});
}
return bounds;
}
/**
* Checks if this container is root or if it's attached to the root container.
* @returns {boolean} true if this container is root or if it's attached to the root container
*/
isAttachedToRoot() {
if (this.root === true) {
return true;
} else {
let ancestor = this.ancestor;
while (ancestor) {
if (ancestor.root === true) {
return true;
}
ancestor = ancestor.ancestor;
}
return false;
}
}
/**
* Returns the instance of the root container (i.e. the current application World container).
* @returns {Container} root container
*/
getRootAncestor() {
if (this.root === true) {
return this;
} else {
let ancestor = this.ancestor;
while (ancestor) {
if (ancestor.root === true) {
break;
}
ancestor = ancestor.ancestor;
}
return ancestor;
}
}
/**
* update the cointainer's bounding rect (private)
* @ignore
*/
updateBoundsPos(newX = this.pos.x, newY = this.pos.y) {
// call the parent method
super.updateBoundsPos(newX, newY);
// Notify children that the parent's position has changed
this.forEach((child) => {
if (child.isRenderable) {
child.updateBoundsPos(
child.pos.x + newX - this.pos.x,
child.pos.y + newY - this.pos.y
);
}
});
}
/**
* @ignore
*/
onActivateEvent() {
this.forEach((child) => {
if (typeof child.onActivateEvent === "function") {
child.onActivateEvent();
}
});
}
/**
* Invokes the removeChildNow in a defer, to ensure the child is removed safely after the update & draw stack has completed. <br>
* if the given child implements a onDeactivateEvent() method, that method will be called once the child is removed from this container.
* @param {RendRenderable|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapTexterable} child - Child to be removed
* @param {boolean} [keepalive=false] - true to prevent calling child.destroy()
*/
removeChild(child, keepalive) {
if (this.hasChild(child)) {
defer(deferredRemove, this, child, keepalive);
}
else {
throw new Error("Child is not mine.");
}
}
/**
* Removes (and optionally destroys) a child from the container.<br>
* (removal is immediate and unconditional)<br>
* Never use keepalive=true with objects from {@link pool}. Doing so will create a memory leak.
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be removed
* @param {boolean} [keepalive=False] - True to prevent calling child.destroy()
*/
removeChildNow(child, keepalive) {
if (this.hasChild(child) && (this.getChildIndex(child) >= 0)) {
if (typeof child.onDeactivateEvent === "function") {
child.onDeactivateEvent();
}
// remove the body first to avoid a condition where a body can be detached
// from its parent, before the body is removed from the game world
if (child.body instanceof Body) {
this.getRootAncestor().removeBody(child.body);
}
if (!keepalive) {
// attempt at recycling the object
if (pool.push(child, false) === false ) {
// else just destroy it
if (typeof child.destroy === "function") {
child.destroy();
}
}
}
// Don't cache the child index; another element might have been removed
// by the child's `onDeactivateEvent` or `destroy` methods
let childIndex = this.getChildIndex(child);
if (childIndex >= 0) {
this.getChildren().splice(childIndex, 1);
child.ancestor = undefined;
}
// force bounds update if required
if (this.enableChildBoundsUpdate === true) {
this.updateBounds();
}
// mark the container for repaint
this.isDirty = true;
// triggered callback if defined
this.onChildChange.call(this, childIndex);
}
}
/**
* Automatically set the specified property of all childs to the given value
* @param {string} prop - property name
* @param {object} value - property value
* @param {boolean} [recursive=false] - recursively apply the value to child containers if true
*/
setChildsProperty(prop, value, recursive) {
this.forEach((child) => {
if ((recursive === true) && (child instanceof Container)) {
child.setChildsProperty(prop, value, recursive);
}
child[prop] = value;
});
}
/**
* Move the child in the group one step forward (z depth).
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be moved
*/
moveUp(child) {
let childIndex = this.getChildIndex(child);
if (childIndex - 1 >= 0) {
// note : we use an inverted loop
this.swapChildren(child, this.getChildAt(childIndex - 1));
// mark the container as dirty
this.isDirty = true;
}
}
/**
* Move the child in the group one step backward (z depth).
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be moved
*/
moveDown(child) {
let childIndex = this.getChildIndex(child);
if (childIndex >= 0 && (childIndex + 1) < this.getChildren().length) {
// note : we use an inverted loop
this.swapChildren(child, this.getChildAt(childIndex + 1));
// mark the container as dirty
this.isDirty = true;
}
}
/**
* Move the specified child to the top(z depth).
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be moved
*/
moveToTop(child) {
let childIndex = this.getChildIndex(child);
if (childIndex > 0) {
let children = this.getChildren();
// note : we use an inverted loop
children.splice(0, 0, children.splice(childIndex, 1)[0]);
// increment our child z value based on the previous child depth
child.pos.z = children[1].pos.z + 1;
// mark the container as dirty
this.isDirty = true;
}
}
/**
* Move the specified child the bottom (z depth).
* @param {Renderable|Entity|Sprite|Collectable|Trigger|Draggable|DropTarget|NineSliceSprite|ImageLayer|ColorLayer|Light2d|UIBaseElement|UISpriteElement|UITextButton|Text|BitmapText} child - Child to be moved
*/
moveToBottom(child) {
let childIndex = this.getChildIndex(child);
let children = this.getChildren();
if (childIndex >= 0 && childIndex < (children.length - 1)) {
// note : we use an inverted loop
children.splice((children.length - 1), 0, children.splice(childIndex, 1)[0]);
// increment our child z value based on the next child depth
child.pos.z = children[(children.length - 2)].pos.z - 1;
// mark the container as dirty
this.isDirty = true;
}
}
/**
* Manually trigger the sort of all the childs in the container
* @param {boolean} [recursive=false] - recursively sort all containers if true
*/
sort(recursive) {
// do nothing if there is already a pending sort
if (!this.pendingSort) {
if (recursive === true) {
this.forEach((child) => {
if (child instanceof Container) {
// note : this will generate one defered sorting function
// for each existing containe
child.sort(recursive);
}
});
}
/** @ignore */
this.pendingSort = defer(function () {
// sort everything in this container
this.getChildren().sort(this["_sort" + this.sortOn.toUpperCase()]);
// clear the defer id
this.pendingSort = null;
// make sure we redraw everything
this.isDirty = true;
}, this);
}
}
/**
* @ignore
*/
onDeactivateEvent() {
this.forEach((child) => {
if (typeof child.onDeactivateEvent === "function") {
child.onDeactivateEvent();
}
});
}
/**
* Z Sorting function
* @ignore
*/
_sortZ(a, b) {
return (b.pos && a.pos) ? (b.pos.z - a.pos.z) : (a.pos ? -Infinity : Infinity);
}
/**
* Reverse Z Sorting function
* @ignore
*/
_sortReverseZ(a, b) {
return (a.pos && b.pos) ? (a.pos.z - b.pos.z) : (a.pos ? Infinity : -Infinity);
}
/**
* X Sorting function
* @ignore
*/
_sortX(a, b) {
if (!b.pos || !a.pos) {
return (a.pos ? -Infinity : Infinity);
}
let result = b.pos.z - a.pos.z;
return (result ? result : (b.pos.x - a.pos.x));
}
/**
* Y Sorting function
* @ignore
*/
_sortY(a, b) {
if (!b.pos || !a.pos) {
return (a.pos ? -Infinity : Infinity);
}
let result = b.pos.z - a.pos.z;
return (result ? result : (b.pos.y - a.pos.y));
}
/**
* Destroy function<br>
* @ignore
*/
destroy() {
// empty the container
this.reset();
// call the parent destroy method
super.destroy(arguments);
}
/**
* container update function. <br>
* automatically called by the application update loop {@link Application}
* @protected
* @param {number} dt - time since the last update in milliseconds.
* @returns {boolean} true if the Container is dirty
*/
update(dt) {
let isFloating = false;
let isPaused = state.isPaused();
let children = this.getChildren();
const childrenLength = children.length;
for (let i = childrenLength, obj; i--, (obj = children[i]);) {
if (isPaused && (!obj.updateWhenPaused)) {
// skip this object
continue;
}
if (obj.isRenderable) {
isFloating = (globalFloatingCounter > 0 || obj.floating);
if (isFloating) {
globalFloatingCounter++;
}
// check if object is in any active cameras
obj.inViewport = false;
// iterate through all cameras
state.current().cameras.forEach((camera) => {
if (camera.isVisible(obj, isFloating)) {
obj.inViewport = true;
}
});
// update our object
this.isDirty |= ((obj.inViewport || obj.alwaysUpdate) && obj.update(dt));
if (globalFloatingCounter > 0) {
globalFloatingCounter--;
}
}
else {
// just directly call update() for non renderable object
this.isDirty |= obj.update(dt);
}
}
// call the parent method
return super.update(dt);
}
/**
* draw this renderable (automatically called by melonJS)
* @protected
* @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
* @param {Camera2d} [viewport] - the viewport to (re)draw
*/
draw(renderer, viewport) {
let isFloating = false;
let bounds = this.getBounds();
this.drawCount = 0;
// clip the containter children to the container bounds
if (this.root === false && this.clipping === true && bounds.isFinite() === true) {
renderer.clipRect(
bounds.left,
bounds.top,
bounds.width,
bounds.height
);
}
// adjust position if required (e.g. canvas/window centering)
renderer.translate(this.pos.x, this.pos.y);
// color background if defined
if (this.backgroundColor.alpha > 1 / 255) {
renderer.clearColor(this.backgroundColor);
}
let children = this.getChildren();
for (let i = children.length, obj; i--, (obj = children[i]);) {
if (obj.isRenderable) {
isFloating = obj.floating === true;
if ((obj.inViewport || isFloating)) {
if (isFloating) {
// translate to screen coordinates
renderer.save();
renderer.resetTransform();
}
// predraw (apply transforms)
obj.preDraw(renderer);
// draw the object
obj.draw(renderer, viewport);
// postdraw (clean-up);
obj.postDraw(renderer);
// restore the previous "state"
if (isFloating) {
renderer.restore();
}
this.drawCount++;
}
}
}
}
}