├── 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 |

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 | 
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 |
--------------------------------------------------------------------------------