├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── scripts ├── jsdoc.conf.json ├── make_dts.js └── reporter.js ├── src ├── core │ ├── AtlasNode.ts │ ├── BaseTexture.ts │ ├── IAtlas.ts │ ├── TextureRegion.ts │ └── TextureResource.ts ├── hacks │ ├── AtlasManager.ts │ ├── BaseTexture.ts │ └── WebGLRenderer.ts ├── loader.ts ├── superAtlas │ ├── AtlasOptions.ts │ └── SuperAtlas.ts ├── utils.ts └── xporter.ts ├── test └── checkpack.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Sublime Text files 2 | *.sublime* 3 | *.*~*.TMP 4 | 5 | # OS temp files 6 | .DS_Store 7 | Thumbs.db 8 | Desktop.ini 9 | 10 | # Tool temp files 11 | npm-debug.log 12 | *.sw* 13 | *~ 14 | \#*# 15 | yarn.lock 16 | 17 | # project ignores 18 | !.gitkeep 19 | node_modules 20 | dist 21 | gh-pages 22 | build-es6 23 | .idea 24 | 25 | # build temp 26 | compilation.ts 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6" 5 | - "7.10" 6 | - "8.4" 7 | 8 | cache: 9 | yarn: true 10 | directories: 11 | - node_modules 12 | 13 | install: 14 | - npm install -g yarn 15 | - yarn 16 | 17 | script: 18 | - yarn build 19 | - yarn checkpack -- vanillajs -e test/checkpack.ts --validate 20 | - yarn checkpack -- browserify -e test/checkpack.ts --validate 21 | - yarn checkpack -- webpack -e test/checkpack.ts --validate 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 PixiJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pixi-super-atlas 2 | 3 | Plugin allows to create atlas consisted of other atlases or small images. 4 | 5 | Consider that we have atlases in PIXI, we add SuperAtlas class that has BaseTexture but is uploading step-by-step, and MegaAtlas that consists of multiple BaseTextures. 6 | 7 | Supported: 8 | 9 | * spritesheets 10 | * spine atlas 11 | 12 | Not supported yet: 13 | 14 | * compressed-textures 15 | * tilemap 16 | 17 | ## Examples 18 | 19 | Work in progress. 20 | 21 | ## Building 22 | 23 | You will need to have [node][node] setup on your machine. 24 | 25 | Make sure you have [yarn][yarn] installed: 26 | 27 | npm install -g yarn 28 | 29 | Then you can install dependencies and build: 30 | 31 | ```bash 32 | yarn 33 | yarn build 34 | ``` 35 | 36 | That will output the built distributables to `./bin`. 37 | 38 | [node]: https://nodejs.org/ 39 | [typescript]: https://www.typescriptlang.org/ 40 | [yarn]: https://yarnpkg.com 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixi-super-atlas", 3 | "version": "0.1.2", 4 | "description": "Runtime atlas for PixiJS v^4", 5 | "author": "Ivan Popelyshev", 6 | "contributors": [ 7 | "Ivan Popelyshev " 8 | ], 9 | "main": "./dist/pixi-projection.js", 10 | "types": "./dist/pixi-projection.d.ts", 11 | "homepage": "https://github.com/gameofbombs/pixi-super-atlas", 12 | "bugs": { 13 | "url": "https://github.com/gameofbombs/pixi-super-atlas/issues" 14 | }, 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/gameofbombs/pixi-super-atlas.git" 19 | }, 20 | "scripts": { 21 | "cleanup": "rimraf bin && mkdirp dist", 22 | "prestart": "yarn cleanup", 23 | "start": "parallelshell \"yarn watch\"", 24 | "watch": "tsc -w", 25 | "prebuild": "yarn cleanup", 26 | "make:dts": "node scripts/make_dts.js", 27 | "build": "tsc && yarn make:dts", 28 | "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", 29 | "check:browserify": "yarn checkpack -- browserify -e test/checkpack.ts", 30 | "check:webpack": "yarn checkpack -- webpack -e test/checkpack.ts", 31 | "check:vanillajs": "yarn checkpack -- vanillajs -e test/checkpack.ts", 32 | "check:all": "yarn build && yarn check:browserify && yarn check:webpack && yarn check:vanillajs" 33 | }, 34 | "files": [ 35 | "dist/", 36 | "src/", 37 | "package.json", 38 | "README.md" 39 | ], 40 | "devDependencies": { 41 | "checkpack": "^0.2.5", 42 | "del": "~2.2.0", 43 | "glob": "~7.1.1", 44 | "jaguarjs-jsdoc": "~1.0.1", 45 | "jsdoc": "~3.4.0", 46 | "mkdirp": "~0.5.1", 47 | "parallelshell": "~2.0.0", 48 | "pixi.js": "~4.5", 49 | "rimraf": "~2.5.3", 50 | "static-html-server": "~0.1.2", 51 | "tmp": "^0.0.33", 52 | "ts-helpers": "~1.1.2", 53 | "typescript": "~2.4" 54 | }, 55 | "dependencies": { 56 | "@types/pixi.js": "^4.5.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": false 4 | }, 5 | "source": { 6 | "include": [ 7 | "./src/" 8 | ], 9 | "exclude": [ 10 | "./src/polyfill/" 11 | ], 12 | "includePattern": ".+\\.js(doc)?$", 13 | "excludePattern": "(^|\\/|\\\\)_" 14 | }, 15 | "plugins": [ 16 | "plugins/markdown" 17 | ], 18 | "templates": { 19 | "cleverLinks" : false, 20 | "monospaceLinks": false, 21 | "default" : { 22 | "outputSourceFiles" : true 23 | }, 24 | "applicationName": "PIXI", 25 | "footer" : "Made with ♥ by Ivan Popelyshev, Mario Zechner", 26 | "copyright" : "PIXI Copyright © 2013-2016 Mat Groves. Spine Copyright © 2013-2016, Esoteric Software", 27 | "disqus": "", 28 | "googleAnalytics": "", 29 | "openGraph": { 30 | "title": "", 31 | "type": "website", 32 | "image": "", 33 | "site_name": "", 34 | "url": "" 35 | }, 36 | "meta": { 37 | "title": "", 38 | "description": "", 39 | "keyword": "" 40 | }, 41 | "linenums" : true 42 | }, 43 | "markdown" : { 44 | "parser" : "gfm", 45 | "hardwrap" : true 46 | }, 47 | "opts": { 48 | "encoding" : "utf8", 49 | "recurse" : true, 50 | "private" : false, 51 | "lenient" : true, 52 | "destination" : "./docs", 53 | "template" : "./node_modules/jaguarjs-jsdoc" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scripts/make_dts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require('fs'); 3 | var glob = require('glob'); 4 | var path = require('path'); 5 | 6 | var sourcePath = path.resolve(__dirname, '../src'); 7 | var files = glob.sync(sourcePath + '/**/*.ts'); 8 | 9 | var filesCompilation = ''; 10 | 11 | for (var i in files) { 12 | var filePath = files[i]; 13 | var fileContents = fs.readFileSync(filePath); 14 | 15 | filesCompilation += fileContents; 16 | } 17 | 18 | var tmp = require('tmp'); 19 | var process = require('child_process'); 20 | 21 | tmp.file(function (err, filename) { 22 | fs.writeFileSync(filename, filesCompilation); 23 | 24 | process.exec('tsc ' + filename + ' -d --removeComments', function (err, stdout, stderr) { 25 | var dtsPath = filename.replace('.ts', '.d.ts'); 26 | var dtsContent = '' + fs.readFileSync(dtsPath); 27 | 28 | fs.writeFileSync( 29 | path.resolve('dist/pixi-super-atlas.d.ts'), 30 | dtsContent.replace(/namespace pixi_atlas/g, 'module PIXI.atlas') 31 | .replace(/pixi_atlas/g, 'PIXI.atlas') 32 | ); 33 | }); 34 | }, {postfix: '.ts'}); 35 | -------------------------------------------------------------------------------- /scripts/reporter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Since jshint's CLI doesn't currently support multiple reporters it is necessary 3 | * to create a wrapper reporter if jshint-stylish-summary should be used in combination 4 | * with another reporter. 5 | */ 6 | module.exports = { 7 | reporter: function(result, config, options) { 8 | require('jshint-stylish').reporter(result, config, options); 9 | require('jshint-stylish-summary').reporter(result, config, options); 10 | } 11 | }; -------------------------------------------------------------------------------- /src/core/AtlasNode.ts: -------------------------------------------------------------------------------- 1 | namespace pixi_atlas { 2 | import Rectangle = PIXI.Rectangle; 3 | 4 | const INF = 1 << 20; 5 | 6 | //TODO: add some padding 7 | 8 | export class AtlasNode { 9 | public childs: Array> = []; 10 | public rect = new Rectangle(0, 0, INF, INF); 11 | public data: T = null; 12 | 13 | public insert(atlasWidth: number, atlasHeight: number, 14 | width: number, height: number, data: T): AtlasNode { 15 | if (this.childs.length > 0) { 16 | const newNode: AtlasNode = this.childs[0].insert( 17 | atlasWidth, atlasHeight, 18 | width, height, data); 19 | if (newNode != null) { 20 | return newNode; 21 | } 22 | return this.childs[1].insert(atlasWidth, atlasHeight, width, height, data); 23 | } else { 24 | let rect: Rectangle = this.rect; 25 | if (this.data != null) return null; 26 | 27 | const w = Math.min(rect.width, atlasWidth - rect.x); 28 | 29 | if (width > rect.width || 30 | width > atlasWidth - rect.x || 31 | height > rect.height || 32 | height > atlasHeight - rect.y) return null; 33 | 34 | if (width == rect.width && height == rect.height) { 35 | this.data = data; 36 | return this; 37 | } 38 | 39 | this.childs.push(new AtlasNode()); 40 | this.childs.push(new AtlasNode()); 41 | 42 | const dw: Number = rect.width - width; 43 | const dh: Number = rect.height - height; 44 | 45 | if (dw > dh) { 46 | this.childs[0].rect = new Rectangle(rect.x, rect.y, width, rect.height); 47 | this.childs[1].rect = new Rectangle(rect.x + width, rect.y, rect.width - width, rect.height); 48 | } else { 49 | this.childs[0].rect = new Rectangle(rect.x, rect.y, rect.width, height); 50 | this.childs[1].rect = new Rectangle(rect.x, rect.y + height, rect.width, rect.height - height); 51 | } 52 | 53 | return this.childs[0].insert(atlasWidth, atlasHeight, width, height, data); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/core/BaseTexture.ts: -------------------------------------------------------------------------------- 1 | namespace pixi_atlas { 2 | // export class BaseTexture { 3 | // } 4 | } -------------------------------------------------------------------------------- /src/core/IAtlas.ts: -------------------------------------------------------------------------------- 1 | namespace pixi_atlas { 2 | import BaseTexture = PIXI.BaseTexture; 3 | import Texture = PIXI.Texture; 4 | import WebGLRenderer = PIXI.WebGLRenderer; 5 | 6 | export class AtlasEntry { 7 | baseTexture: BaseTexture; 8 | atlas: IAtlas; 9 | currentNode: AtlasNode; 10 | currentAtlas: SuperAtlas; 11 | width: number; 12 | height: number; 13 | 14 | nodeUpdateID: number = 0; 15 | 16 | regions: Array = []; 17 | 18 | constructor(atlas: IAtlas, baseTexture: BaseTexture) { 19 | this.baseTexture = baseTexture; 20 | this.width = baseTexture.width; 21 | this.height = baseTexture.height; 22 | this.atlas = atlas; 23 | } 24 | } 25 | 26 | export interface IRepackResult { 27 | // goodMap: { [key: string]: AtlasNode }; 28 | failed: Array; 29 | 30 | apply(): void; 31 | } 32 | 33 | export interface IAtlas { 34 | add(texture: BaseTexture | Texture, swapCache ?: boolean): TextureRegion; 35 | 36 | addHash(textures: { [key: string]: Texture }, swapCache ?: boolean): { [key: string]: TextureRegion }; 37 | 38 | repack(): IRepackResult; 39 | 40 | prepare(renderer: WebGLRenderer): Promise; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/core/TextureRegion.ts: -------------------------------------------------------------------------------- 1 | namespace pixi_atlas { 2 | import Texture = PIXI.Texture; 3 | import Rectangle = PIXI.Rectangle; 4 | 5 | //TODO: support resolution 6 | //TODO: support no-frame 7 | //TODO: support updates 8 | 9 | export class TextureRegion extends PIXI.Texture { 10 | uid = PIXI.utils.uid(); 11 | 12 | proxied: Texture; 13 | entry: AtlasEntry; 14 | 15 | constructor(entry: AtlasEntry, texture: PIXI.Texture = new Texture(entry.baseTexture)) { 16 | super(entry.currentAtlas ? entry.currentAtlas.baseTexture : texture.baseTexture, 17 | entry.currentNode ? new Rectangle(texture.frame.x + entry.currentNode.rect.x, 18 | texture.frame.y + entry.currentNode.rect.y, 19 | texture.frame.width, 20 | texture.frame.height) : texture.frame.clone(), 21 | texture.orig, 22 | texture.trim, 23 | texture.rotate 24 | ); 25 | this.proxied = texture; 26 | this.entry = entry; 27 | } 28 | 29 | updateFrame() { 30 | const texture = this.proxied; 31 | const entry = this.entry; 32 | const frame = this._frame; 33 | if (entry.currentNode) { 34 | this.baseTexture = entry.currentAtlas.baseTexture; 35 | frame.x = texture.frame.x + entry.currentNode.rect.x; 36 | frame.y = texture.frame.y + entry.currentNode.rect.y; 37 | } else { 38 | this.baseTexture = texture.baseTexture; 39 | frame.x = texture.frame.x; 40 | frame.y = texture.frame.y; 41 | } 42 | 43 | frame.width = texture.frame.width; 44 | frame.height = texture.frame.height; 45 | this._updateUvs(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/TextureResource.ts: -------------------------------------------------------------------------------- 1 | namespace pixi_atlas { 2 | export interface ITextureResource { 3 | onTextureUpload(renderer: PIXI.WebGLRenderer, baseTexture: PIXI.BaseTexture, glTexture: PIXI.glCore.GLTexture): boolean; 4 | 5 | onTextureTag?(baseTexture: PIXI.BaseTexture): void; 6 | 7 | onTextureNew?(baseTexture: PIXI.BaseTexture): void; 8 | 9 | onTextureDestroy?(baseTexture: PIXI.BaseTexture): boolean; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/hacks/AtlasManager.ts: -------------------------------------------------------------------------------- 1 | module pixi_atlas { 2 | export class AtlasManager { 3 | /** 4 | * A reference to the current renderer 5 | * 6 | * @member {PIXI.WebGLRenderer} 7 | */ 8 | renderer: PIXI.WebGLRenderer; 9 | 10 | /** 11 | * The current WebGL rendering context 12 | * 13 | * @member {WebGLRenderingContext} 14 | */ 15 | gl: WebGLRenderingContext; 16 | 17 | constructor(renderer: PIXI.WebGLRenderer) { 18 | this.renderer = renderer; 19 | 20 | renderer.on('context', this.onContextChange); 21 | } 22 | 23 | onContextChange = (gl: WebGLRenderingContext) => { 24 | this.gl = gl; 25 | this.renderer.textureManager.updateTexture = this.updateTexture; 26 | }; 27 | 28 | //TODO: make boundTextures faster? 29 | 30 | updateTexture = (texture_: PIXI.BaseTexture | PIXI.Texture, location?: number) => { 31 | const tm = this.renderer.textureManager; 32 | const gl = this.gl; 33 | const anyThis = this as any; 34 | 35 | const texture: any = (texture_ as any).baseTexture || texture_; 36 | const isRenderTexture = !!(texture as any)._glRenderTargets; 37 | 38 | if (!texture.hasLoaded) { 39 | return null; 40 | } 41 | 42 | const boundTextures: Array = this.renderer.boundTextures as any; 43 | 44 | // if the location is undefined then this may have been called by n event. 45 | // this being the cas e the texture may already be bound to a slot. As a texture can only be bound once 46 | // we need to find its current location if it exists. 47 | if (location === undefined) { 48 | location = 0; 49 | 50 | // TODO maybe we can use texture bound ids later on... 51 | // check if texture is already bound.. 52 | for (let i = 0; i < boundTextures.length; ++i) { 53 | if (boundTextures[i] === texture) { 54 | location = i; 55 | break; 56 | } 57 | } 58 | } 59 | 60 | boundTextures[location] = texture; 61 | 62 | gl.activeTexture(gl.TEXTURE0 + location); 63 | 64 | let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; 65 | 66 | if (!glTexture) { 67 | if (isRenderTexture) { 68 | const renderTarget = new PIXI.RenderTarget( 69 | this.gl, 70 | texture.width, 71 | texture.height, 72 | texture.scaleMode, 73 | texture.resolution 74 | ); 75 | 76 | renderTarget.resize(texture.width, texture.height); 77 | texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; 78 | glTexture = renderTarget.texture; 79 | } 80 | else { 81 | glTexture = new PIXI.glCore.GLTexture(this.gl, null, null, null, null); 82 | glTexture.bind(location); 83 | } 84 | texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; 85 | 86 | texture.on('update', tm.updateTexture, tm); 87 | texture.on('dispose', tm.destroyTexture, tm); 88 | } else if (isRenderTexture) { 89 | texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); 90 | } 91 | 92 | glTexture.premultiplyAlpha = texture.premultipliedAlpha; 93 | 94 | if (!isRenderTexture) { 95 | if (!texture.resource) { 96 | glTexture.upload(texture.source); 97 | } else if (!texture.resource.onTextureUpload(this.renderer, texture, glTexture)) { 98 | glTexture.uploadData(null, texture.realWidth, texture.realHeight); 99 | } 100 | } 101 | 102 | // lets only update what changes.. 103 | if (texture.forceUploadStyle) { 104 | this.setStyle(texture, glTexture); 105 | } 106 | glTexture._updateID = texture._updateID; 107 | return glTexture; 108 | }; 109 | 110 | setStyle(texture: PIXI.BaseTexture, 111 | glTexture: PIXI.glCore.GLTexture) { 112 | const gl = this.gl; 113 | 114 | if ((texture as any).isPowerOfTwo) { 115 | if (texture.mipmap) { 116 | glTexture.enableMipmap(); 117 | } 118 | 119 | if (texture.wrapMode === PIXI.WRAP_MODES.CLAMP) { 120 | glTexture.enableWrapClamp(); 121 | } 122 | else if (texture.wrapMode === PIXI.WRAP_MODES.REPEAT) { 123 | glTexture.enableWrapRepeat(); 124 | } 125 | else { 126 | glTexture.enableWrapMirrorRepeat(); 127 | } 128 | } 129 | else { 130 | glTexture.enableWrapClamp(); 131 | } 132 | 133 | if (texture.scaleMode === PIXI.SCALE_MODES.NEAREST) { 134 | glTexture.enableNearestScaling(); 135 | } 136 | else { 137 | glTexture.enableLinearScaling(); 138 | } 139 | } 140 | 141 | destroy() { 142 | this.renderer.off('context', this.onContextChange); 143 | } 144 | } 145 | 146 | PIXI.WebGLRenderer.registerPlugin('atlas', AtlasManager); 147 | } 148 | -------------------------------------------------------------------------------- /src/hacks/BaseTexture.ts: -------------------------------------------------------------------------------- 1 | declare module PIXI { 2 | interface BaseTexture { 3 | uid: number; 4 | _updateID: number; 5 | _mips: Array; 6 | resource: pixi_atlas.ITextureResource 7 | forceUploadStyle: boolean; 8 | 9 | generateMips(levels: number): void; 10 | } 11 | 12 | interface BaseRenderTexture { 13 | uid: number; 14 | 15 | generateMips(levels: number): void; 16 | } 17 | } 18 | 19 | module pixi_atlas { 20 | PIXI.BaseTexture.prototype._updateID = 0; 21 | PIXI.BaseTexture.prototype.resource = null; 22 | PIXI.BaseTexture.prototype.forceUploadStyle = true; 23 | 24 | let tmpCanvas: HTMLCanvasElement; 25 | 26 | PIXI.BaseTexture.prototype.generateMips = function (levels: number) { 27 | if (!levels) return; 28 | let src = this.source; 29 | 30 | if (!tmpCanvas) tmpCanvas = document.createElement("canvas"); 31 | 32 | let sw = ((src.width + 1) >> 1) << 1; 33 | let h = src.height; 34 | let sh = 0; 35 | for (let i = 1; i <= levels; i++) { 36 | sh += h; 37 | h = (h + 1) >> 1; 38 | } 39 | 40 | if (tmpCanvas.width < sw) { 41 | tmpCanvas.width = sw; 42 | } 43 | if (tmpCanvas.height < sh) { 44 | tmpCanvas.height = sh; 45 | } 46 | let context = tmpCanvas.getContext("2d"); 47 | context.clearRect(0, 0, sw, sh); 48 | 49 | this._mips = []; 50 | 51 | let w = src.width; 52 | h = src.height; 53 | context.drawImage(src, 0, 0, w, h, 0, 0, w / 2, h / 2); 54 | let h1 = 0; 55 | for (let i = 1; i <= levels; i++) { 56 | w = (w + 1) >> 1; 57 | h = (h + 1) >> 1; 58 | let data = context.getImageData(0, h1, w, h); 59 | this._mips.push({ 60 | width: data.width, 61 | height: data.height, 62 | data: new Uint8Array(data.data) 63 | }); 64 | if (i < levels) { 65 | context.drawImage(tmpCanvas, 0, h1, w, h, 0, h1 + h, w / 2, h / 2); 66 | h1 += h; 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/hacks/WebGLRenderer.ts: -------------------------------------------------------------------------------- 1 | declare module PIXI { 2 | interface BaseTexture { 3 | uid: number; 4 | _updateID: number; 5 | resource: pixi_atlas.ITextureResource 6 | forceUploadStyle: boolean; 7 | } 8 | 9 | interface BaseRenderTexture { 10 | uid: number; 11 | } 12 | } 13 | 14 | declare module PIXI.glCore { 15 | interface GLTexture { 16 | _updateID: number; 17 | } 18 | } 19 | 20 | module pixi_atlas { 21 | PIXI.glCore.GLTexture.prototype._updateID = -1; 22 | PIXI.BaseTexture.prototype._updateID = 0; 23 | PIXI.BaseTexture.prototype.resource = null; 24 | PIXI.BaseTexture.prototype.forceUploadStyle = true; 25 | 26 | function bindTexture(texture: any, 27 | location?: number, forceLocation?: boolean): number { 28 | texture = texture || this.emptyTextures[location]; 29 | texture = texture.baseTexture || texture; 30 | texture.touched = this.textureGC.count; 31 | 32 | if (!forceLocation) { 33 | // TODO - maybe look into adding boundIds.. save us the loop? 34 | for (let i = 0; i < this.boundTextures.length; i++) { 35 | if (this.boundTextures[i] === texture) { 36 | return i; 37 | } 38 | } 39 | 40 | if (location === undefined) { 41 | this._nextTextureLocation++; 42 | this._nextTextureLocation %= this.boundTextures.length; 43 | location = this.boundTextures.length - this._nextTextureLocation - 1; 44 | } 45 | } 46 | else { 47 | location = location || 0; 48 | } 49 | 50 | const gl = this.gl; 51 | const glTexture = texture._glTextures[this.CONTEXT_UID]; 52 | if (texture === this.emptyTextures[location]) { 53 | glTexture._updateID = 0; 54 | } 55 | 56 | if (!glTexture || glTexture._updateID < texture._updateID) { 57 | // this will also bind the texture.. 58 | this.textureManager.updateTexture(texture, location); 59 | } 60 | else { 61 | // bind the current texture 62 | this.boundTextures[location] = texture; 63 | gl.activeTexture(gl.TEXTURE0 + location); 64 | gl.bindTexture(gl.TEXTURE_2D, glTexture.texture); 65 | } 66 | 67 | return location; 68 | } 69 | 70 | PIXI.WebGLRenderer.prototype.bindTexture = bindTexture; 71 | } 72 | -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | declare module PIXI.loaders { 2 | interface Resource { 3 | spritesheet?: PIXI.Spritesheet; 4 | } 5 | } 6 | 7 | namespace pixi_atlas { 8 | import Resource = PIXI.loaders.Resource; 9 | 10 | export function atlasChecker() { 11 | return function (resource: PIXI.loaders.Resource, next: () => any) { 12 | let atlas = resource.metadata.runtimeAtlas as IAtlas; 13 | if (!atlas) { 14 | return next(); 15 | } 16 | 17 | if (resource.type === Resource.TYPE.IMAGE) { 18 | if (resource.texture) { 19 | resource.texture = atlas.add(resource.texture, true); 20 | } 21 | 22 | return next(); 23 | } 24 | 25 | if (resource.type === Resource.TYPE.JSON && 26 | resource.spritesheet) { 27 | resource.spritesheet.textures = atlas.addHash(resource.spritesheet.textures, true); 28 | resource.textures = resource.spritesheet.textures; 29 | return next(); 30 | } 31 | 32 | //TODO: something about spine 33 | 34 | next(); 35 | }; 36 | } 37 | 38 | PIXI.loaders.Loader.addPixiMiddleware(atlasChecker); 39 | PIXI.loader.use(atlasChecker()); 40 | } -------------------------------------------------------------------------------- /src/superAtlas/AtlasOptions.ts: -------------------------------------------------------------------------------- 1 | namespace pixi_atlas { 2 | export interface IAtlasOptions { 3 | width ?: number; 4 | height ?: number; 5 | loadFactor ?: number; 6 | repackBeforeResize ?: boolean; 7 | repackAfterResize ?: boolean; 8 | algoTreeResize?: boolean; 9 | maxSize ?: number; 10 | format ?: number; 11 | hasAllFields ?: boolean; 12 | mipLevels ?: number; 13 | padding ?: number; 14 | } 15 | 16 | export class AtlasOptions implements IAtlasOptions { 17 | width = 2048; 18 | height = 2048; 19 | loadFactor = 0.95; 20 | repackBeforeResize = true; 21 | repackAfterResize = true; 22 | algoTreeResize = false; 23 | maxSize = 0; 24 | mipLevels = 0; 25 | padding = 0; 26 | 27 | format = WebGLRenderingContext.RGBA; 28 | 29 | static MAX_SIZE = 0; 30 | 31 | constructor(src: IAtlasOptions) { 32 | if (src) { 33 | this.assign(src); 34 | } 35 | } 36 | 37 | assign(src: IAtlasOptions) { 38 | this.width = src.width || this.width; 39 | this.height = src.height || src.width || this.height; 40 | this.maxSize = src.maxSize || AtlasOptions.MAX_SIZE; 41 | this.format = src.format || this.format; 42 | this.loadFactor = src.loadFactor || this.loadFactor; 43 | this.padding = src.padding || this.padding; 44 | this.mipLevels = src.mipLevels || this.mipLevels; 45 | if (src.repackAfterResize !== undefined) { 46 | this.repackAfterResize = src.repackAfterResize; 47 | } 48 | if (src.repackBeforeResize !== undefined) { 49 | this.repackBeforeResize = src.repackBeforeResize; 50 | } 51 | if (src.algoTreeResize !== undefined) { 52 | this.algoTreeResize = src.algoTreeResize; 53 | } 54 | return this; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/superAtlas/SuperAtlas.ts: -------------------------------------------------------------------------------- 1 | namespace pixi_atlas { 2 | const RGBA = WebGLRenderingContext.RGBA; 3 | import BaseTexture = PIXI.BaseTexture; 4 | import Rectangle = PIXI.Rectangle; 5 | 6 | export class SuperAtlasEntry { 7 | baseTexture: BaseTexture; 8 | superAtlas: SuperAtlas; 9 | } 10 | 11 | export class AtlasTree implements IRepackResult { 12 | failed: Array = []; 13 | 14 | root: AtlasNode; 15 | 16 | good: Array = []; 17 | 18 | hash: { [key: number]: AtlasNode } = {}; 19 | 20 | apply() { 21 | throw new Error("Method not implemented."); 22 | } 23 | } 24 | 25 | export class SuperAtlas implements ITextureResource, IAtlas { 26 | static MAX_SIZE = 2048; 27 | 28 | baseTexture: PIXI.BaseTexture = null; 29 | format: number = RGBA; 30 | width: number = 2048; 31 | height: number = 2048; 32 | options: AtlasOptions; 33 | 34 | all: { [key: number]: AtlasEntry } = {}; 35 | 36 | tree: AtlasTree = null; 37 | 38 | onTextureNew(baseTexture: PIXI.BaseTexture) { 39 | this.baseTexture = baseTexture; 40 | baseTexture.resource = this; 41 | baseTexture.width = this.width; 42 | baseTexture.height = this.height; 43 | baseTexture.hasLoaded = true; 44 | baseTexture.height = this.height; 45 | } 46 | 47 | static create(options: IAtlasOptions) { 48 | let opt = options instanceof AtlasOptions ? options : new AtlasOptions(options); 49 | let atlas = new SuperAtlas(); 50 | atlas.options = opt; 51 | atlas.width = opt.width; 52 | atlas.height = opt.height; 53 | atlas.format = opt.format; 54 | atlas.onTextureNew(new PIXI.BaseTexture()); 55 | 56 | atlas.tree = new AtlasTree(); 57 | atlas.tree.root = atlas.createAtlasRoot(); 58 | 59 | return atlas; 60 | } 61 | 62 | destroy() { 63 | if (this.baseTexture) { 64 | this.baseTexture.destroy(); 65 | this.baseTexture = null; 66 | } 67 | } 68 | 69 | add(texture: BaseTexture | PIXI.Texture, swapCache?: boolean): TextureRegion { 70 | let baseTexture: PIXI.BaseTexture; 71 | let arg: PIXI.Texture; 72 | if (texture instanceof BaseTexture) { 73 | baseTexture = texture as BaseTexture; 74 | arg = new PIXI.Texture(baseTexture); 75 | } else { 76 | baseTexture = texture.baseTexture; 77 | arg = texture; 78 | } 79 | 80 | let entry = this.all[baseTexture.uid]; 81 | if (!entry) { 82 | entry = new AtlasEntry(this, baseTexture); 83 | 84 | // pad it 85 | let p1 = this.options.padding, p2 = (1 << this.options.mipLevels); 86 | let w1 = entry.width + p1, h1 = entry.height + p1; 87 | entry.width = w1 + (p2 - entry.width % p2) % p2; 88 | entry.height = h1 + (p2 - entry.height % p2) % p2; 89 | 90 | this.insert(entry); 91 | } 92 | 93 | let region = new TextureRegion(entry, arg); 94 | if (swapCache) { 95 | let ids = texture.textureCacheIds; 96 | for (let i = 0; i < ids.length; i++) { 97 | PIXI.utils.TextureCache[ids[i]] = region; 98 | } 99 | } 100 | 101 | entry.regions.push(region); 102 | return region; 103 | } 104 | 105 | addHash(textures: { [key: string]: PIXI.Texture; }, swapCache?: boolean): { [key: string]: TextureRegion; } { 106 | let hash: { [key: string]: TextureRegion; } = {}; 107 | for (let key in textures) { 108 | hash[key] = this.add(textures[key], swapCache); 109 | } 110 | return hash; 111 | } 112 | 113 | insert(entry: AtlasEntry) { 114 | if (this.tryInsert(entry)) return; 115 | this.tree.failed.push(entry); 116 | this.all[entry.baseTexture.uid] = entry; 117 | } 118 | 119 | remove(entry: AtlasEntry) { 120 | if (entry.currentNode == null) { 121 | let failed = this.tree.failed; 122 | let ind = failed.indexOf(entry); 123 | if (ind >= 0) { 124 | failed.splice(ind, 1); 125 | } 126 | } else { 127 | throw new Error("Cant remove packed texture"); 128 | } 129 | } 130 | 131 | tryInsert(entry: AtlasEntry): boolean { 132 | let node = this.tree.root.insert(this.width, this.height, 133 | entry.width, entry.height, entry); 134 | if (!node) { 135 | return false; 136 | } 137 | entry.nodeUpdateID = ++this.baseTexture._updateID; 138 | entry.currentNode = node; 139 | entry.currentAtlas = this; 140 | this.all[entry.baseTexture.uid] = entry; 141 | this.tree.hash[entry.baseTexture.uid] = node; 142 | this.tree.good.push(entry); 143 | return true; 144 | } 145 | 146 | private createAtlasRoot() { 147 | let res = new AtlasNode(); 148 | if (!this.options.algoTreeResize) { 149 | res.rect.width = this.width; 150 | res.rect.height = this.height; 151 | } 152 | return res; 153 | } 154 | 155 | repack(failOnFirst: boolean = false): IRepackResult { 156 | let pack = new AtlasTree(); 157 | 158 | let all = this.tree.good.slice(0); 159 | let failed = this.tree.failed; 160 | for (let i = 0; i < failed.length; i++) { 161 | all.push(failed[i]); 162 | } 163 | 164 | all.sort((a: AtlasEntry, b: AtlasEntry) => { 165 | if (b.width == a.width) { 166 | return b.height - a.height; 167 | } 168 | return b.width - a.width; 169 | }); 170 | 171 | let root = this.createAtlasRoot(); 172 | pack.root = root; 173 | for (let obj of all) { 174 | let node = root.insert( 175 | this.width, this.height, 176 | obj.width, obj.height, obj); 177 | if (!node) { 178 | pack.failed.push(obj); 179 | if (failOnFirst) { 180 | return pack; 181 | } 182 | } else { 183 | pack.hash[obj.baseTexture.uid] = node; 184 | } 185 | } 186 | 187 | pack.apply = () => { 188 | //TODO: full copy? 189 | this.tree.root = pack.root; 190 | this.tree.failed = pack.failed.slice(0); 191 | this.tree.hash = pack.hash; 192 | 193 | for (let obj of all) { 194 | obj.currentNode = pack.hash[obj.baseTexture.uid] || null; 195 | obj.currentAtlas = obj.currentNode ? this : null; 196 | for (let region of obj.regions) { 197 | region.updateFrame(); 198 | } 199 | } 200 | 201 | this.imageTextureRebuildUpdateID++; 202 | }; 203 | return pack; 204 | } 205 | 206 | prepare(renderer: PIXI.WebGLRenderer): Promise { 207 | //TODO: wait while everything loads 208 | 209 | renderer.textureManager.updateTexture(this.baseTexture); 210 | throw new Error("Method not implemented."); 211 | } 212 | 213 | imageTextureRebuildUpdateID: number = 0; 214 | 215 | onTextureUpload(renderer: PIXI.WebGLRenderer, baseTexture: PIXI.BaseTexture, tex: PIXI.glCore.GLTexture): boolean { 216 | tex.bind(); 217 | const imgTexture = this.baseTexture; 218 | const gl = tex.gl; 219 | const levels = this.options.mipLevels; 220 | 221 | tex.mipmap = levels > 0; 222 | tex.premultiplyAlpha = imgTexture.premultipliedAlpha; 223 | gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, imgTexture.premultipliedAlpha); 224 | 225 | const uploadAll = tex._updateID < this.imageTextureRebuildUpdateID; 226 | if (uploadAll) { 227 | gl.texImage2D( 228 | gl.TEXTURE_2D, //GLenum target 229 | 0, //GLint level 230 | gl.RGBA, //GLint internalformat 231 | imgTexture.width, //GLsizei width 232 | imgTexture.height, //GLsizei height 233 | 0, //GLint border // should be 0, it is borderColor 234 | gl.RGBA, //GLenum format 235 | gl.UNSIGNED_BYTE,//GLenum type 236 | null //ArrayBufferView? pixels 237 | ); 238 | 239 | if (tex.mipmap) { 240 | //testing 241 | for (let lvl = 1; (imgTexture.width >> lvl) > 0; lvl++) { 242 | gl.texImage2D( 243 | gl.TEXTURE_2D, //GLenum target 244 | lvl, //GLint level 245 | gl.RGBA, //GLint internalformat 246 | imgTexture.width >> lvl, //GLsizei width 247 | imgTexture.height >> lvl, //GLsizei height 248 | 0, //GLint border // should be 0, it is borderColor 249 | gl.RGBA, //GLenum format 250 | gl.UNSIGNED_BYTE,//GLenum type 251 | null //ArrayBufferView? pixels 252 | ); 253 | } 254 | } 255 | } 256 | 257 | for (let key in this.tree.hash) { 258 | let node = this.tree.hash[key]; 259 | let entry = node.data; 260 | let entryTex = entry.baseTexture; 261 | // if (!obj.isLoaded) continue; 262 | if (!uploadAll && tex._updateID >= entry.nodeUpdateID) continue; 263 | 264 | let rect: Rectangle = node.rect; 265 | gl.texSubImage2D( 266 | gl.TEXTURE_2D, //GLenum target 267 | 0, //GLint level 268 | rect.left, // GLint xoffset 269 | rect.top, // GLint yoffset 270 | gl.RGBA, //GLenum format 271 | gl.UNSIGNED_BYTE,//GLenum type 272 | entry.baseTexture.source // TexImageSource source 273 | ); 274 | 275 | if (levels > 0) { 276 | if (!entryTex._mips || entryTex._mips.length < levels) { 277 | entryTex.generateMips(levels); 278 | } 279 | const mips = entryTex._mips; 280 | for (let lvl = 1; lvl <= levels; lvl++) { 281 | const mip = mips[lvl - 1]; 282 | gl.texSubImage2D( 283 | gl.TEXTURE_2D, //GLenum target 284 | lvl, //GLint level 285 | rect.left >> lvl, // GLint xoffset 286 | rect.top >> lvl, // GLint yoffset 287 | mip.width, 288 | mip.height, 289 | gl.RGBA, //GLenum format 290 | gl.UNSIGNED_BYTE,//GLenum type 291 | mip.data 292 | ); 293 | } 294 | } 295 | } 296 | return true; 297 | } 298 | } 299 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | namespace pixi_atlas.utils { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/xporter.ts: -------------------------------------------------------------------------------- 1 | /// 2 | (PIXI as any).atlas = pixi_atlas; 3 | -------------------------------------------------------------------------------- /test/checkpack.ts: -------------------------------------------------------------------------------- 1 | import 'pixi.js'; 2 | import '../dist/pixi-super-atlas.js'; 3 | 4 | //@../node_modules/pixi.js/dist/pixi.min.js 5 | //@../dist/pixi-super-atlas.js 6 | 7 | let app = new PIXI.Application({autoStart: false, width: 800, height: 1024}); 8 | document.body.appendChild(app.view); 9 | 10 | let loader = new PIXI.loaders.Loader("https://pixijs.github.io/examples/required/assets/"); 11 | let levels = 4; 12 | let atlas = PIXI.atlas.SuperAtlas.create({width: 1024, height: 1024, mipLevels: levels}); 13 | let options = {metadata: {runtimeAtlas: atlas}}; 14 | 15 | loader.add('spritesheet', 'monsters.json', options) 16 | .add('spinObj_01', 'spinObj_01.png', options) 17 | .add('spinObj_02', 'spinObj_02.png', options) 18 | .add('spinObj_03', 'spinObj_03.png', options) 19 | .add('spinObj_04', 'spinObj_04.png', options) 20 | .add('spinObj_05', 'spinObj_05.png', options) 21 | .add('spinObj_06', 'spinObj_06.png', options) 22 | .add('spinObj_07', 'spinObj_07.png', options) 23 | .add('spinObj_08', 'spinObj_08.png', options) 24 | .add('panda', 'panda.png', options) 25 | .load(() => { 26 | let pack = atlas.repack(); 27 | pack.apply(); 28 | 29 | let y = 0; 30 | for (let i = 1; i <= levels; i++) { 31 | let spr = new PIXI.Sprite(new PIXI.Texture(atlas.baseTexture)); 32 | spr.scale.set(1.0 / (1 << i)); 33 | spr.position.y = y; 34 | app.stage.addChild(spr); 35 | 36 | y += Math.ceil(atlas.height * spr.scale.y); 37 | } 38 | app.start(); 39 | }); 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "none", 5 | "outFile": "dist/pixi-super-atlas.js", 6 | "experimentalDecorators": true, 7 | "sourceMap": true, 8 | "noImplicitAny": true, 9 | "preserveConstEnums": true, 10 | "removeComments": true, 11 | "inlineSources": true, 12 | "declaration": true, 13 | "lib": [ 14 | "DOM", 15 | "ES5", 16 | "ScriptHost", 17 | "es2015.promise" 18 | ] 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ], 23 | "exclude": [ 24 | "node_modules/**/*", 25 | "dist" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------