Source: core/display/Container.js

core/display/Container.js

  1. import { removeItems } from '../utils';
  2. import DisplayObject from './DisplayObject';
  3. /**
  4. * A Container represents a collection of display objects.
  5. * It is the base class of all display objects that act as a container for other objects.
  6. *
  7. *```js
  8. * let container = new PIXI.Container();
  9. * container.addChild(sprite);
  10. * ```
  11. *
  12. * @class
  13. * @extends PIXI.DisplayObject
  14. * @memberof PIXI
  15. */
  16. export default class Container extends DisplayObject
  17. {
  18. /**
  19. *
  20. */
  21. constructor()
  22. {
  23. super();
  24. /**
  25. * The array of children of this container.
  26. *
  27. * @member {PIXI.DisplayObject[]}
  28. * @readonly
  29. */
  30. this.children = [];
  31. }
  32. /**
  33. * Overridable method that can be used by Container subclasses whenever the children array is modified
  34. *
  35. * @private
  36. */
  37. onChildrenChange()
  38. {
  39. /* empty */
  40. }
  41. /**
  42. * Adds one or more children to the container.
  43. *
  44. * Multiple items can be added like so: `myContainer.addChild(thingOne, thingTwo, thingThree)`
  45. *
  46. * @param {...PIXI.DisplayObject} child - The DisplayObject(s) to add to the container
  47. * @return {PIXI.DisplayObject} The first child that was added.
  48. */
  49. addChild(child)
  50. {
  51. const argumentsLength = arguments.length;
  52. // if there is only one argument we can bypass looping through the them
  53. if (argumentsLength > 1)
  54. {
  55. // loop through the arguments property and add all children
  56. // use it the right way (.length and [i]) so that this function can still be optimised by JS runtimes
  57. for (let i = 0; i < argumentsLength; i++)
  58. {
  59. this.addChild(arguments[i]);
  60. }
  61. }
  62. else
  63. {
  64. // if the child has a parent then lets remove it as PixiJS objects can only exist in one place
  65. if (child.parent)
  66. {
  67. child.parent.removeChild(child);
  68. }
  69. child.parent = this;
  70. // ensure child transform will be recalculated
  71. child.transform._parentID = -1;
  72. this.children.push(child);
  73. // ensure bounds will be recalculated
  74. this._boundsID++;
  75. // TODO - lets either do all callbacks or all events.. not both!
  76. this.onChildrenChange(this.children.length - 1);
  77. child.emit('added', this);
  78. }
  79. return child;
  80. }
  81. /**
  82. * Adds a child to the container at a specified index. If the index is out of bounds an error will be thrown
  83. *
  84. * @param {PIXI.DisplayObject} child - The child to add
  85. * @param {number} index - The index to place the child in
  86. * @return {PIXI.DisplayObject} The child that was added.
  87. */
  88. addChildAt(child, index)
  89. {
  90. if (index < 0 || index > this.children.length)
  91. {
  92. throw new Error(`${child}addChildAt: The index ${index} supplied is out of bounds ${this.children.length}`);
  93. }
  94. if (child.parent)
  95. {
  96. child.parent.removeChild(child);
  97. }
  98. child.parent = this;
  99. // ensure child transform will be recalculated
  100. child.transform._parentID = -1;
  101. this.children.splice(index, 0, child);
  102. // ensure bounds will be recalculated
  103. this._boundsID++;
  104. // TODO - lets either do all callbacks or all events.. not both!
  105. this.onChildrenChange(index);
  106. child.emit('added', this);
  107. return child;
  108. }
  109. /**
  110. * Swaps the position of 2 Display Objects within this container.
  111. *
  112. * @param {PIXI.DisplayObject} child - First display object to swap
  113. * @param {PIXI.DisplayObject} child2 - Second display object to swap
  114. */
  115. swapChildren(child, child2)
  116. {
  117. if (child === child2)
  118. {
  119. return;
  120. }
  121. const index1 = this.getChildIndex(child);
  122. const index2 = this.getChildIndex(child2);
  123. this.children[index1] = child2;
  124. this.children[index2] = child;
  125. this.onChildrenChange(index1 < index2 ? index1 : index2);
  126. }
  127. /**
  128. * Returns the index position of a child DisplayObject instance
  129. *
  130. * @param {PIXI.DisplayObject} child - The DisplayObject instance to identify
  131. * @return {number} The index position of the child display object to identify
  132. */
  133. getChildIndex(child)
  134. {
  135. const index = this.children.indexOf(child);
  136. if (index === -1)
  137. {
  138. throw new Error('The supplied DisplayObject must be a child of the caller');
  139. }
  140. return index;
  141. }
  142. /**
  143. * Changes the position of an existing child in the display object container
  144. *
  145. * @param {PIXI.DisplayObject} child - The child DisplayObject instance for which you want to change the index number
  146. * @param {number} index - The resulting index number for the child display object
  147. */
  148. setChildIndex(child, index)
  149. {
  150. if (index < 0 || index >= this.children.length)
  151. {
  152. throw new Error(`The index ${index} supplied is out of bounds ${this.children.length}`);
  153. }
  154. const currentIndex = this.getChildIndex(child);
  155. removeItems(this.children, currentIndex, 1); // remove from old position
  156. this.children.splice(index, 0, child); // add at new position
  157. this.onChildrenChange(index);
  158. }
  159. /**
  160. * Returns the child at the specified index
  161. *
  162. * @param {number} index - The index to get the child at
  163. * @return {PIXI.DisplayObject} The child at the given index, if any.
  164. */
  165. getChildAt(index)
  166. {
  167. if (index < 0 || index >= this.children.length)
  168. {
  169. throw new Error(`getChildAt: Index (${index}) does not exist.`);
  170. }
  171. return this.children[index];
  172. }
  173. /**
  174. * Removes one or more children from the container.
  175. *
  176. * @param {...PIXI.DisplayObject} child - The DisplayObject(s) to remove
  177. * @return {PIXI.DisplayObject} The first child that was removed.
  178. */
  179. removeChild(child)
  180. {
  181. const argumentsLength = arguments.length;
  182. // if there is only one argument we can bypass looping through the them
  183. if (argumentsLength > 1)
  184. {
  185. // loop through the arguments property and add all children
  186. // use it the right way (.length and [i]) so that this function can still be optimised by JS runtimes
  187. for (let i = 0; i < argumentsLength; i++)
  188. {
  189. this.removeChild(arguments[i]);
  190. }
  191. }
  192. else
  193. {
  194. const index = this.children.indexOf(child);
  195. if (index === -1) return null;
  196. child.parent = null;
  197. // ensure child transform will be recalculated
  198. child.transform._parentID = -1;
  199. removeItems(this.children, index, 1);
  200. // ensure bounds will be recalculated
  201. this._boundsID++;
  202. // TODO - lets either do all callbacks or all events.. not both!
  203. this.onChildrenChange(index);
  204. child.emit('removed', this);
  205. }
  206. return child;
  207. }
  208. /**
  209. * Removes a child from the specified index position.
  210. *
  211. * @param {number} index - The index to get the child from
  212. * @return {PIXI.DisplayObject} The child that was removed.
  213. */
  214. removeChildAt(index)
  215. {
  216. const child = this.getChildAt(index);
  217. // ensure child transform will be recalculated..
  218. child.parent = null;
  219. child.transform._parentID = -1;
  220. removeItems(this.children, index, 1);
  221. // ensure bounds will be recalculated
  222. this._boundsID++;
  223. // TODO - lets either do all callbacks or all events.. not both!
  224. this.onChildrenChange(index);
  225. child.emit('removed', this);
  226. return child;
  227. }
  228. /**
  229. * Removes all children from this container that are within the begin and end indexes.
  230. *
  231. * @param {number} [beginIndex=0] - The beginning position.
  232. * @param {number} [endIndex=this.children.length] - The ending position. Default value is size of the container.
  233. * @returns {DisplayObject[]} List of removed children
  234. */
  235. removeChildren(beginIndex = 0, endIndex)
  236. {
  237. const begin = beginIndex;
  238. const end = typeof endIndex === 'number' ? endIndex : this.children.length;
  239. const range = end - begin;
  240. let removed;
  241. if (range > 0 && range <= end)
  242. {
  243. removed = this.children.splice(begin, range);
  244. for (let i = 0; i < removed.length; ++i)
  245. {
  246. removed[i].parent = null;
  247. if (removed[i].transform)
  248. {
  249. removed[i].transform._parentID = -1;
  250. }
  251. }
  252. this._boundsID++;
  253. this.onChildrenChange(beginIndex);
  254. for (let i = 0; i < removed.length; ++i)
  255. {
  256. removed[i].emit('removed', this);
  257. }
  258. return removed;
  259. }
  260. else if (range === 0 && this.children.length === 0)
  261. {
  262. return [];
  263. }
  264. throw new RangeError('removeChildren: numeric values are outside the acceptable range.');
  265. }
  266. /**
  267. * Updates the transform on all children of this container for rendering
  268. */
  269. updateTransform()
  270. {
  271. this._boundsID++;
  272. this.transform.updateTransform(this.parent.transform);
  273. // TODO: check render flags, how to process stuff here
  274. this.worldAlpha = this.alpha * this.parent.worldAlpha;
  275. for (let i = 0, j = this.children.length; i < j; ++i)
  276. {
  277. const child = this.children[i];
  278. if (child.visible)
  279. {
  280. child.updateTransform();
  281. }
  282. }
  283. }
  284. /**
  285. * Recalculates the bounds of the container.
  286. *
  287. */
  288. calculateBounds()
  289. {
  290. this._bounds.clear();
  291. this._calculateBounds();
  292. for (let i = 0; i < this.children.length; i++)
  293. {
  294. const child = this.children[i];
  295. if (!child.visible || !child.renderable)
  296. {
  297. continue;
  298. }
  299. child.calculateBounds();
  300. // TODO: filter+mask, need to mask both somehow
  301. if (child._mask)
  302. {
  303. child._mask.calculateBounds();
  304. this._bounds.addBoundsMask(child._bounds, child._mask._bounds);
  305. }
  306. else if (child.filterArea)
  307. {
  308. this._bounds.addBoundsArea(child._bounds, child.filterArea);
  309. }
  310. else
  311. {
  312. this._bounds.addBounds(child._bounds);
  313. }
  314. }
  315. this._lastBoundsID = this._boundsID;
  316. }
  317. /**
  318. * Recalculates the bounds of the object. Override this to
  319. * calculate the bounds of the specific object (not including children).
  320. *
  321. */
  322. _calculateBounds()
  323. {
  324. // FILL IN//
  325. }
  326. /**
  327. * Renders the object using the WebGL renderer
  328. *
  329. * @param {PIXI.WebGLRenderer} renderer - The renderer
  330. */
  331. renderWebGL(renderer)
  332. {
  333. // if the object is not visible or the alpha is 0 then no need to render this element
  334. if (!this.visible || this.worldAlpha <= 0 || !this.renderable)
  335. {
  336. return;
  337. }
  338. // do a quick check to see if this element has a mask or a filter.
  339. if (this._mask || (this.filters && this.filters.length))
  340. {
  341. this.renderAdvancedWebGL(renderer);
  342. }
  343. else
  344. {
  345. this._renderWebGL(renderer);
  346. // simple render children!
  347. for (let i = 0, j = this.children.length; i < j; ++i)
  348. {
  349. this.children[i].renderWebGL(renderer);
  350. }
  351. }
  352. }
  353. /**
  354. * Render the object using the WebGL renderer and advanced features.
  355. *
  356. * @private
  357. * @param {PIXI.WebGLRenderer} renderer - The renderer
  358. */
  359. renderAdvancedWebGL(renderer)
  360. {
  361. renderer.flush();
  362. const filters = this._filters;
  363. const mask = this._mask;
  364. // push filter first as we need to ensure the stencil buffer is correct for any masking
  365. if (filters)
  366. {
  367. if (!this._enabledFilters)
  368. {
  369. this._enabledFilters = [];
  370. }
  371. this._enabledFilters.length = 0;
  372. for (let i = 0; i < filters.length; i++)
  373. {
  374. if (filters[i].enabled)
  375. {
  376. this._enabledFilters.push(filters[i]);
  377. }
  378. }
  379. if (this._enabledFilters.length)
  380. {
  381. renderer.filterManager.pushFilter(this, this._enabledFilters);
  382. }
  383. }
  384. if (mask)
  385. {
  386. renderer.maskManager.pushMask(this, this._mask);
  387. }
  388. // add this object to the batch, only rendered if it has a texture.
  389. this._renderWebGL(renderer);
  390. // now loop through the children and make sure they get rendered
  391. for (let i = 0, j = this.children.length; i < j; i++)
  392. {
  393. this.children[i].renderWebGL(renderer);
  394. }
  395. renderer.flush();
  396. if (mask)
  397. {
  398. renderer.maskManager.popMask(this, this._mask);
  399. }
  400. if (filters && this._enabledFilters && this._enabledFilters.length)
  401. {
  402. renderer.filterManager.popFilter();
  403. }
  404. }
  405. /**
  406. * To be overridden by the subclasses.
  407. *
  408. * @private
  409. * @param {PIXI.WebGLRenderer} renderer - The renderer
  410. */
  411. _renderWebGL(renderer) // eslint-disable-line no-unused-vars
  412. {
  413. // this is where content itself gets rendered...
  414. }
  415. /**
  416. * To be overridden by the subclass
  417. *
  418. * @private
  419. * @param {PIXI.CanvasRenderer} renderer - The renderer
  420. */
  421. _renderCanvas(renderer) // eslint-disable-line no-unused-vars
  422. {
  423. // this is where content itself gets rendered...
  424. }
  425. /**
  426. * Renders the object using the Canvas renderer
  427. *
  428. * @param {PIXI.CanvasRenderer} renderer - The renderer
  429. */
  430. renderCanvas(renderer)
  431. {
  432. // if not visible or the alpha is 0 then no need to render this
  433. if (!this.visible || this.worldAlpha <= 0 || !this.renderable)
  434. {
  435. return;
  436. }
  437. if (this._mask)
  438. {
  439. renderer.maskManager.pushMask(this._mask);
  440. }
  441. this._renderCanvas(renderer);
  442. for (let i = 0, j = this.children.length; i < j; ++i)
  443. {
  444. this.children[i].renderCanvas(renderer);
  445. }
  446. if (this._mask)
  447. {
  448. renderer.maskManager.popMask(renderer);
  449. }
  450. }
  451. /**
  452. * Removes all internal references and listeners as well as removes children from the display list.
  453. * Do not use a Container after calling `destroy`.
  454. *
  455. * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options
  456. * have been set to that value
  457. * @param {boolean} [options.children=false] - if set to true, all the children will have their destroy
  458. * method called as well. 'options' will be passed on to those calls.
  459. * @param {boolean} [options.texture=false] - Only used for child Sprites if options.children is set to true
  460. * Should it destroy the texture of the child sprite
  461. * @param {boolean} [options.baseTexture=false] - Only used for child Sprites if options.children is set to true
  462. * Should it destroy the base texture of the child sprite
  463. */
  464. destroy(options)
  465. {
  466. super.destroy();
  467. const destroyChildren = typeof options === 'boolean' ? options : options && options.children;
  468. const oldChildren = this.removeChildren(0, this.children.length);
  469. if (destroyChildren)
  470. {
  471. for (let i = 0; i < oldChildren.length; ++i)
  472. {
  473. oldChildren[i].destroy(options);
  474. }
  475. }
  476. }
  477. /**
  478. * The width of the Container, setting this will actually modify the scale to achieve the value set
  479. *
  480. * @member {number}
  481. */
  482. get width()
  483. {
  484. return this.scale.x * this.getLocalBounds().width;
  485. }
  486. set width(value) // eslint-disable-line require-jsdoc
  487. {
  488. const width = this.getLocalBounds().width;
  489. if (width !== 0)
  490. {
  491. this.scale.x = value / width;
  492. }
  493. else
  494. {
  495. this.scale.x = 1;
  496. }
  497. this._width = value;
  498. }
  499. /**
  500. * The height of the Container, setting this will actually modify the scale to achieve the value set
  501. *
  502. * @member {number}
  503. */
  504. get height()
  505. {
  506. return this.scale.y * this.getLocalBounds().height;
  507. }
  508. set height(value) // eslint-disable-line require-jsdoc
  509. {
  510. const height = this.getLocalBounds().height;
  511. if (height !== 0)
  512. {
  513. this.scale.y = value / height;
  514. }
  515. else
  516. {
  517. this.scale.y = 1;
  518. }
  519. this._height = value;
  520. }
  521. }
  522. // performance increase to avoid using call.. (10x faster)
  523. Container.prototype.containerUpdateTransform = Container.prototype.updateTransform;