Source: core/textures/Spritesheet.js

core/textures/Spritesheet.js

  1. import { Rectangle, Texture } from '../';
  2. import { getResolutionOfUrl } from '../utils';
  3. /**
  4. * Utility class for maintaining reference to a collection
  5. * of Textures on a single Spritesheet.
  6. *
  7. * To access a sprite sheet from your code pass its JSON data file to Pixi's loader:
  8. *
  9. * ```js
  10. * PIXI.loader.add("images/spritesheet.json").load(setup);
  11. *
  12. * function setup() {
  13. * let sheet = PIXI.loader.resources["images/spritesheet.json"].spritesheet;
  14. * ...
  15. * }
  16. * ```
  17. * With the `sheet.textures` you can create Sprite objects,`sheet.animations` can be used to create an AnimatedSprite.
  18. *
  19. * Sprite sheets can be packed using tools like {@link https://codeandweb.com/texturepacker|TexturePacker},
  20. * {@link https://renderhjs.net/shoebox/|Shoebox} or {@link https://github.com/krzysztof-o/spritesheet.js|Spritesheet.js}.
  21. * Default anchor points (see {@link PIXI.Texture#defaultAnchor}) and grouping of animation sprites are currently only
  22. * supported by TexturePacker.
  23. *
  24. * @class
  25. * @memberof PIXI
  26. */
  27. export default class Spritesheet
  28. {
  29. /**
  30. * The maximum number of Textures to build per process.
  31. *
  32. * @type {number}
  33. * @default 1000
  34. */
  35. static get BATCH_SIZE()
  36. {
  37. return 1000;
  38. }
  39. /**
  40. * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object.
  41. * @param {Object} data - Spritesheet image data.
  42. * @param {string} [resolutionFilename] - The filename to consider when determining
  43. * the resolution of the spritesheet. If not provided, the imageUrl will
  44. * be used on the BaseTexture.
  45. */
  46. constructor(baseTexture, data, resolutionFilename = null)
  47. {
  48. /**
  49. * Reference to ths source texture
  50. * @type {PIXI.BaseTexture}
  51. */
  52. this.baseTexture = baseTexture;
  53. /**
  54. * A map containing all textures of the sprite sheet.
  55. * Can be used to create a {@link PIXI.Sprite|Sprite}:
  56. * ```js
  57. * new PIXI.Sprite(sheet.textures["image.png"]);
  58. * ```
  59. * @member {Object}
  60. */
  61. this.textures = {};
  62. /**
  63. * A map containing the textures for each animation.
  64. * Can be used to create an {@link PIXI.extras.AnimatedSprite|AnimatedSprite}:
  65. * ```js
  66. * new PIXI.extras.AnimatedSprite(sheet.animations["anim_name"])
  67. * ```
  68. * @member {Object}
  69. */
  70. this.animations = {};
  71. /**
  72. * Reference to the original JSON data.
  73. * @type {Object}
  74. */
  75. this.data = data;
  76. /**
  77. * The resolution of the spritesheet.
  78. * @type {number}
  79. */
  80. this.resolution = this._updateResolution(
  81. resolutionFilename || this.baseTexture.imageUrl
  82. );
  83. /**
  84. * Map of spritesheet frames.
  85. * @type {Object}
  86. * @private
  87. */
  88. this._frames = this.data.frames;
  89. /**
  90. * Collection of frame names.
  91. * @type {string[]}
  92. * @private
  93. */
  94. this._frameKeys = Object.keys(this._frames);
  95. /**
  96. * Current batch index being processed.
  97. * @type {number}
  98. * @private
  99. */
  100. this._batchIndex = 0;
  101. /**
  102. * Callback when parse is completed.
  103. * @type {Function}
  104. * @private
  105. */
  106. this._callback = null;
  107. }
  108. /**
  109. * Generate the resolution from the filename or fallback
  110. * to the meta.scale field of the JSON data.
  111. *
  112. * @private
  113. * @param {string} resolutionFilename - The filename to use for resolving
  114. * the default resolution.
  115. * @return {number} Resolution to use for spritesheet.
  116. */
  117. _updateResolution(resolutionFilename)
  118. {
  119. const scale = this.data.meta.scale;
  120. // Use a defaultValue of `null` to check if a url-based resolution is set
  121. let resolution = getResolutionOfUrl(resolutionFilename, null);
  122. // No resolution found via URL
  123. if (resolution === null)
  124. {
  125. // Use the scale value or default to 1
  126. resolution = scale !== undefined ? parseFloat(scale) : 1;
  127. }
  128. // For non-1 resolutions, update baseTexture
  129. if (resolution !== 1)
  130. {
  131. this.baseTexture.resolution = resolution;
  132. this.baseTexture.update();
  133. }
  134. return resolution;
  135. }
  136. /**
  137. * Parser spritesheet from loaded data. This is done asynchronously
  138. * to prevent creating too many Texture within a single process.
  139. *
  140. * @param {Function} callback - Callback when complete returns
  141. * a map of the Textures for this spritesheet.
  142. */
  143. parse(callback)
  144. {
  145. this._batchIndex = 0;
  146. this._callback = callback;
  147. if (this._frameKeys.length <= Spritesheet.BATCH_SIZE)
  148. {
  149. this._processFrames(0);
  150. this._processAnimations();
  151. this._parseComplete();
  152. }
  153. else
  154. {
  155. this._nextBatch();
  156. }
  157. }
  158. /**
  159. * Process a batch of frames
  160. *
  161. * @private
  162. * @param {number} initialFrameIndex - The index of frame to start.
  163. */
  164. _processFrames(initialFrameIndex)
  165. {
  166. let frameIndex = initialFrameIndex;
  167. const maxFrames = Spritesheet.BATCH_SIZE;
  168. const sourceScale = this.baseTexture.sourceScale;
  169. while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length)
  170. {
  171. const i = this._frameKeys[frameIndex];
  172. const data = this._frames[i];
  173. const rect = data.frame;
  174. if (rect)
  175. {
  176. let frame = null;
  177. let trim = null;
  178. const sourceSize = data.trimmed !== false && data.sourceSize
  179. ? data.sourceSize : data.frame;
  180. const orig = new Rectangle(
  181. 0,
  182. 0,
  183. Math.floor(sourceSize.w * sourceScale) / this.resolution,
  184. Math.floor(sourceSize.h * sourceScale) / this.resolution
  185. );
  186. if (data.rotated)
  187. {
  188. frame = new Rectangle(
  189. Math.floor(rect.x * sourceScale) / this.resolution,
  190. Math.floor(rect.y * sourceScale) / this.resolution,
  191. Math.floor(rect.h * sourceScale) / this.resolution,
  192. Math.floor(rect.w * sourceScale) / this.resolution
  193. );
  194. }
  195. else
  196. {
  197. frame = new Rectangle(
  198. Math.floor(rect.x * sourceScale) / this.resolution,
  199. Math.floor(rect.y * sourceScale) / this.resolution,
  200. Math.floor(rect.w * sourceScale) / this.resolution,
  201. Math.floor(rect.h * sourceScale) / this.resolution
  202. );
  203. }
  204. // Check to see if the sprite is trimmed
  205. if (data.trimmed !== false && data.spriteSourceSize)
  206. {
  207. trim = new Rectangle(
  208. Math.floor(data.spriteSourceSize.x * sourceScale) / this.resolution,
  209. Math.floor(data.spriteSourceSize.y * sourceScale) / this.resolution,
  210. Math.floor(rect.w * sourceScale) / this.resolution,
  211. Math.floor(rect.h * sourceScale) / this.resolution
  212. );
  213. }
  214. this.textures[i] = new Texture(
  215. this.baseTexture,
  216. frame,
  217. orig,
  218. trim,
  219. data.rotated ? 2 : 0,
  220. data.anchor
  221. );
  222. // lets also add the frame to pixi's global cache for fromFrame and fromImage functions
  223. Texture.addToCache(this.textures[i], i);
  224. }
  225. frameIndex++;
  226. }
  227. }
  228. /**
  229. * Parse animations config
  230. *
  231. * @private
  232. */
  233. _processAnimations()
  234. {
  235. const animations = this.data.animations || {};
  236. for (const animName in animations)
  237. {
  238. this.animations[animName] = [];
  239. for (const frameName of animations[animName])
  240. {
  241. this.animations[animName].push(this.textures[frameName]);
  242. }
  243. }
  244. }
  245. /**
  246. * The parse has completed.
  247. *
  248. * @private
  249. */
  250. _parseComplete()
  251. {
  252. const callback = this._callback;
  253. this._callback = null;
  254. this._batchIndex = 0;
  255. callback.call(this, this.textures);
  256. }
  257. /**
  258. * Begin the next batch of textures.
  259. *
  260. * @private
  261. */
  262. _nextBatch()
  263. {
  264. this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE);
  265. this._batchIndex++;
  266. setTimeout(() =>
  267. {
  268. if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length)
  269. {
  270. this._nextBatch();
  271. }
  272. else
  273. {
  274. this._processAnimations();
  275. this._parseComplete();
  276. }
  277. }, 0);
  278. }
  279. /**
  280. * Destroy Spritesheet and don't use after this.
  281. *
  282. * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well
  283. */
  284. destroy(destroyBase = false)
  285. {
  286. for (const i in this.textures)
  287. {
  288. this.textures[i].destroy();
  289. }
  290. this._frames = null;
  291. this._frameKeys = null;
  292. this.data = null;
  293. this.textures = null;
  294. if (destroyBase)
  295. {
  296. this.baseTexture.destroy();
  297. }
  298. this.baseTexture = null;
  299. }
  300. }