├── README.md └── public ├── assets ├── link-palette.png └── link.png ├── index.html └── js └── palette-swap.js /README.md: -------------------------------------------------------------------------------- 1 | # Phaser 3 Palette Swapping Example 2 | 3 | Example of using palette swapping on a spritesheet in Phaser 3. 4 | 5 | ## How It Works 6 | This initial idea came from this article: 7 | http://laxvikinggames.blogspot.com/2015/01/build-dynamic-texture-atlas-in-phaser.html 8 | 9 | The palette swapping in this example is achieved by taking an image that contains palette data then going through a spritesheet and switching the matching pixels from the original palette to the new palette. 10 | 11 | Our "palette data" image is a small image consisting of each unique color we want to replace and their replacements. Each color is a single pixel, and each row represents an individual palette. 12 | 13 |

Palette Example

14 | 15 | We then define a config containing the relevant information for creating the necessary spritesheets and animations after the palette swapping is performed. 16 | 17 | ```js 18 | var animConfig = { 19 | paletteKey: 'link-palette', // Palette file we're referencing. 20 | paletteNames: ['green', 'red', 'blue', 'purple'], // Names for each palette to build out the names for the atlas. 21 | spriteSheet: { // Spritesheet we're manipulating. 22 | key: 'link', 23 | frameWidth: 32, // NOTE: Potential drawback. All frames are the same size. 24 | frameHeight: 32 25 | }, 26 | animations: [ // Animation data. 27 | {key: 'walk-down', frameRate: 15, startFrame: 0, endFrame: 9}, 28 | {key: 'walk-left', frameRate: 15, startFrame: 10, endFrame: 19}, 29 | {key: 'walk-up', frameRate: 15, startFrame: 20, endFrame: 29} 30 | ] 31 | }; 32 | ``` 33 | 34 | Once the spritesheets and animations have been built, we can then use them in our game as we need! 35 | 36 |

Demonstration

37 | -------------------------------------------------------------------------------- /public/assets/link-palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colbydude/phaser-3-palette-swapping-example/a9adaf8e8a8d31cf88f23f08e5f57f350198edc5/public/assets/link-palette.png -------------------------------------------------------------------------------- /public/assets/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colbydude/phaser-3-palette-swapping-example/a9adaf8e8a8d31cf88f23f08e5f57f350198edc5/public/assets/link.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Phaser Palette Swapping Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/js/palette-swap.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | type: Phaser.AUTO, 3 | parent: 'phaser-example', 4 | pixelArt: true, 5 | width: 240, 6 | height: 160, 7 | scene: { 8 | preload: preload, 9 | create: create 10 | } 11 | }; 12 | 13 | var game = new Phaser.Game(config); 14 | 15 | function preload () 16 | { 17 | // Load palette template. 18 | this.load.image('link-palette', '/assets/link-palette.png'); 19 | 20 | // Load spritesheet we'll be manipulating. 21 | // Contains 3 animations with 10 frames each. 3 rows, 10 columns. 30 frames. 22 | this.load.spritesheet('link', '/assets/link.png', { 23 | frameWidth: 32, 24 | frameHeight: 32 25 | }); 26 | } 27 | 28 | function create () 29 | { 30 | var animConfig = { 31 | paletteKey: 'link-palette', // Palette file we're referencing. 32 | paletteNames: ['green', 'red', 'blue', 'purple'], // Names for each palette to build out the names for the atlas. 33 | spriteSheet: { // Spritesheet we're manipulating. 34 | key: 'link', 35 | frameWidth: 32, // NOTE: Potential drawback. All frames are the same size. 36 | frameHeight: 32 37 | }, 38 | animations: [ // Animation data. 39 | {key: 'walk-down', frameRate: 15, startFrame: 0, endFrame: 9}, 40 | {key: 'walk-left', frameRate: 15, startFrame: 10, endFrame: 19}, 41 | {key: 'walk-up', frameRate: 15, startFrame: 20, endFrame: 29} 42 | ] 43 | }; 44 | 45 | // Create the dynamic spritesheets and animations. 46 | createPalettes(animConfig); 47 | 48 | // -- DEMO -- \\ 49 | // Add text. 50 | this.add.text(5, 5, "WASD: Change Animation"); 51 | this.add.text(5, 20, "X: Change Palette"); 52 | 53 | // Add sprite. 54 | var link = this.add.sprite(120, 80, 'link-' + animConfig.paletteNames[0]).setScale(2); 55 | 56 | // Set color and animation. 57 | link.color = animConfig.paletteNames[0]; 58 | link.anims.play('link-' + link.color + '-walk-down'); 59 | 60 | // Handle Input. 61 | this.input.keyboard.on('keydown_W', function (event) { 62 | link.flipX = false; 63 | link.anims.play('link-' + link.color + '-walk-up'); 64 | }); 65 | 66 | this.input.keyboard.on('keydown_A', function (event) { 67 | link.flipX = false; 68 | link.anims.play('link-' + link.color + '-walk-left'); 69 | }); 70 | 71 | this.input.keyboard.on('keydown_S', function (event) { 72 | link.flipX = false; 73 | link.anims.play('link-' + link.color + '-walk-down'); 74 | }); 75 | 76 | this.input.keyboard.on('keydown_D', function (event) { 77 | link.flipX = true; 78 | link.anims.play('link-' + link.color + '-walk-left'); 79 | }); 80 | 81 | this.input.keyboard.on('keydown_X', function (event) { 82 | var index = animConfig.paletteNames.indexOf(link.color); 83 | 84 | index++; 85 | 86 | if (index >= animConfig.paletteNames.length) { 87 | index = 0; 88 | } 89 | 90 | link.color = animConfig.paletteNames[index]; 91 | 92 | link.anims.play('link-' + link.color + '-walk-down'); 93 | }); 94 | } 95 | 96 | /** 97 | * Creates new sprite sheets and animations from the given palette and spritesheet. 98 | * 99 | * @param {object} config - Config schema. 100 | */ 101 | function createPalettes (config) 102 | { 103 | // Create color lookup from palette image. 104 | var colorLookup = {}; 105 | var x, y; 106 | var pixel, palette; 107 | var paletteWidth = game.textures.get(config.paletteKey).getSourceImage().width; 108 | 109 | // Go through each pixel in the palette image and add it to the color lookup. 110 | for (y = 0; y < config.paletteNames.length; y++) { 111 | palette = config.paletteNames[y]; 112 | colorLookup[palette] = []; 113 | 114 | for (x = 0; x < paletteWidth; x++) { 115 | pixel = game.textures.getPixel(x, y, config.paletteKey); 116 | colorLookup[palette].push(pixel); 117 | } 118 | } 119 | 120 | // Create sheets and animations from base sheet. 121 | var sheet = game.textures.get(config.spriteSheet.key).getSourceImage(); 122 | var atlasKey, anim, animKey; 123 | var canvasTexture, canvas, context, imageData, pixelArray; 124 | 125 | // Iterate over each palette. 126 | for (y = 0; y < config.paletteNames.length; y++) { 127 | palette = config.paletteNames[y]; 128 | atlasKey = config.spriteSheet.key + '-' + palette; 129 | 130 | // Create a canvas to draw new image data onto. 131 | canvasTexture = game.textures.createCanvas(config.spriteSheet.key + '-temp', sheet.width, sheet.height); 132 | canvas = canvasTexture.getSourceImage(); 133 | context = canvas.getContext('2d'); 134 | 135 | // Copy the sheet. 136 | context.drawImage(sheet, 0, 0); 137 | 138 | // Get image data from the new sheet. 139 | imageData = context.getImageData(0, 0, sheet.width, sheet.height); 140 | pixelArray = imageData.data; 141 | 142 | // Iterate through every pixel in the image. 143 | for (var p = 0; p < pixelArray.length / 4; p++) { 144 | var index = 4 * p; 145 | 146 | var r = pixelArray[index]; 147 | var g = pixelArray[++index]; 148 | var b = pixelArray[++index]; 149 | var alpha = pixelArray[++index]; 150 | 151 | // If this is a transparent pixel, ignore, move on. 152 | if (alpha === 0) { 153 | continue; 154 | } 155 | 156 | // Iterate through the colors in the palette. 157 | for (var c = 0; c < paletteWidth; c++) { 158 | var oldColor = colorLookup[config.paletteNames[0]][c]; 159 | var newColor = colorLookup[palette][c]; 160 | 161 | // If the color matches, replace the color. 162 | if (r === oldColor.r && g === oldColor.g && b === oldColor.b && alpha === 255) { 163 | pixelArray[--index] = newColor.b; 164 | pixelArray[--index] = newColor.g; 165 | pixelArray[--index] = newColor.r; 166 | } 167 | } 168 | } 169 | 170 | // Put our modified pixel data back into the context. 171 | context.putImageData(imageData, 0, 0); 172 | 173 | // Add the canvas as a sprite sheet to the game. 174 | game.textures.addSpriteSheet(atlasKey, canvasTexture.getSourceImage(), { 175 | frameWidth: config.spriteSheet.frameWidth, 176 | frameHeight: config.spriteSheet.frameHeight, 177 | }); 178 | 179 | // Iterate over each animation. 180 | for (var a = 0; a < config.animations.length; a++) { 181 | anim = config.animations[a]; 182 | animKey = atlasKey + '-' + anim.key; 183 | 184 | // Add the animation to the game. 185 | game.anims.create({ 186 | key: animKey, 187 | frames: game.anims.generateFrameNumbers(atlasKey, {start: anim.startFrame, end: anim.endFrame}), 188 | frameRate: anim.frameRate, 189 | repeat: anim.repeat === undefined ? -1 : anim.repeat 190 | }); 191 | } 192 | 193 | // Destroy temp texture. 194 | game.textures.get(config.spriteSheet.key + '-temp').destroy(); 195 | } 196 | 197 | // Destroy textures that are not longer needed. 198 | // NOTE: This doesn't remove the textures from TextureManager.list. 199 | // However, it does destroy source image data. 200 | game.textures.get(config.spriteSheet.key).destroy(); 201 | game.textures.get(config.paletteKey).destroy(); 202 | } 203 | --------------------------------------------------------------------------------