@nkzw/palette-swap 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/lib/palette-swap.d.ts +11 -0
- package/lib/palette-swap.js +182 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Christoph Nakazawa
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @nkzw/palette-swap
|
|
2
|
+
|
|
3
|
+
Image palette swapping and color replacement tool based on canvas for Node.js and browsers to create variants from a base image.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm install @nkzw/palette-swap canvas
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### `paletteSwap()`
|
|
18
|
+
|
|
19
|
+
Here is an example of how to use `paletteSwap()` to create two variants from a base image:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { loadImage } from 'canvas';
|
|
23
|
+
import { writeFileSync } from 'node:fs';
|
|
24
|
+
import paletteSwap from 'paletteSwap';
|
|
25
|
+
|
|
26
|
+
const results = paletteSwap(
|
|
27
|
+
await loadImage('./Yoshi.png'),
|
|
28
|
+
new Map([
|
|
29
|
+
[
|
|
30
|
+
2,
|
|
31
|
+
new Map([
|
|
32
|
+
['#006000', '#570061'],
|
|
33
|
+
['#00a800', '#8c00a8'],
|
|
34
|
+
['#00f800', '#de00f8'],
|
|
35
|
+
]),
|
|
36
|
+
],
|
|
37
|
+
[3, 220],
|
|
38
|
+
]),
|
|
39
|
+
new Set([
|
|
40
|
+
'#000000',
|
|
41
|
+
'#903020',
|
|
42
|
+
'#f84020',
|
|
43
|
+
'#f89000',
|
|
44
|
+
'#ff0000',
|
|
45
|
+
'#f8c0a8',
|
|
46
|
+
'#f8e0d0',
|
|
47
|
+
'#f8f8f8',
|
|
48
|
+
]),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
for (const [variant, canvas] of results) {
|
|
52
|
+
writeFileSync(`./Yoshi-${variant}.png`, canvas.toBuffer());
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Parameters
|
|
57
|
+
|
|
58
|
+
### `image: Image`
|
|
59
|
+
|
|
60
|
+
The base image that is used for generating each variant.
|
|
61
|
+
|
|
62
|
+
### `variants: ReadonlyMap<VariantName, ReadonlyMap<HEX, HEX> | Hue>`
|
|
63
|
+
|
|
64
|
+
The color definitions for each variant. You can either provide a list of mappings for each color, a hue for all colors or a hue for individual colors:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const variantMap = new Map([
|
|
68
|
+
// Map each color to another hex value.
|
|
69
|
+
[
|
|
70
|
+
'Variant',
|
|
71
|
+
new Map([
|
|
72
|
+
['#ff0000', '#00ff00'],
|
|
73
|
+
['#ffff00', '#0000ff'],
|
|
74
|
+
]),
|
|
75
|
+
],
|
|
76
|
+
// Change the hue of each color.
|
|
77
|
+
['Variant2', 130],
|
|
78
|
+
]);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `staticColors?: Set<HEX>`
|
|
82
|
+
|
|
83
|
+
A `Set` with a list of HEX values (for example `#000000`) to lock certain colors to the same values. Colors in this set will not be replaced when generating variants. For example when you are palette swapping an image of a car you might want to change the color of the chassis but not the color of the tires and the outline. If this parameter is omitted it will copy any colors from the base image that are not explicitly listed in the variants definition.
|
|
84
|
+
|
|
85
|
+
### `images?: ReadonlyMap<VariantName, Image>`
|
|
86
|
+
|
|
87
|
+
If you want to avoid returning variants for images that have not changed, you can add an optional `images` parameter with the current image for each variant. If the image hasn't changed, the variant will not be returned in the results.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
// Optionally you can provide existing variants
|
|
91
|
+
const results = paletteSwap(
|
|
92
|
+
await loadImage('MyImage.png'),
|
|
93
|
+
new Map([
|
|
94
|
+
[2, new Map([])],
|
|
95
|
+
[3, 130],
|
|
96
|
+
]),
|
|
97
|
+
null,
|
|
98
|
+
new Map([
|
|
99
|
+
[2, await loadImage('MyImage-2.png')],
|
|
100
|
+
[3, await loadImage('MyImage-3.png')],
|
|
101
|
+
]),
|
|
102
|
+
);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `options?: { imageName?: string; ignoreMissing?: boolean; }`
|
|
106
|
+
|
|
107
|
+
By default `paletteSwap` will throw an error for any missing color mapping. Options can be specified to ignore missing color mappings or to provide an image name used for the error message when a color mapping is missing.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Canvas, Image as CanvasImage } from 'canvas';
|
|
2
|
+
type Hue = number;
|
|
3
|
+
export type HEX = `#${string}`;
|
|
4
|
+
export type HSL = [hue: number, saturation: number, lightness: number];
|
|
5
|
+
export type VariantName = number | string;
|
|
6
|
+
export type Image = CanvasImage | HTMLImageElement;
|
|
7
|
+
export default function paletteSwap(image: Image, inputVariants: ReadonlyMap<VariantName, ReadonlyMap<HEX, HEX> | Hue>, staticColors?: Set<HEX> | null, images?: ReadonlyMap<VariantName, Image> | null, options?: {
|
|
8
|
+
imageName?: string;
|
|
9
|
+
ignoreMissing?: boolean;
|
|
10
|
+
}): ReadonlyMap<VariantName, Canvas>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/palette-swap.ts
|
|
2
|
+
import {
|
|
3
|
+
createCanvas,
|
|
4
|
+
createImageData
|
|
5
|
+
} from "canvas";
|
|
6
|
+
var hexToInt32 = (hex2) => {
|
|
7
|
+
const n = Number.parseInt(hex2.slice(1), 16) & 16777215;
|
|
8
|
+
const [r, g, b] = int32ToRgb(n);
|
|
9
|
+
return b << 16 | g << 8 | r;
|
|
10
|
+
};
|
|
11
|
+
var int32ToRgb = (color) => {
|
|
12
|
+
const r = color >> 16 & 255;
|
|
13
|
+
const g = color >> 8 & 255;
|
|
14
|
+
const b = color & 255;
|
|
15
|
+
return [r, g, b];
|
|
16
|
+
};
|
|
17
|
+
var hex = (value) => Math.round(value).toString(16).padStart(2, "0");
|
|
18
|
+
var rgbToHex = ([r, g, b]) => `#${hex(r)}${hex(g)}${hex(b)}`;
|
|
19
|
+
var int32ToHex = (color) => {
|
|
20
|
+
const [r, g, b] = int32ToRgb(color);
|
|
21
|
+
return rgbToHex([b, g, r]);
|
|
22
|
+
};
|
|
23
|
+
var applyHue = (rgb, hue) => {
|
|
24
|
+
const h = hue / 360;
|
|
25
|
+
const r = rgb[2] / 255;
|
|
26
|
+
const g = rgb[1] / 255;
|
|
27
|
+
const b = rgb[0] / 255;
|
|
28
|
+
const min = Math.min(r, g, b);
|
|
29
|
+
const max = Math.max(r, g, b);
|
|
30
|
+
const delta = max - min;
|
|
31
|
+
let s;
|
|
32
|
+
const l = (min + max) / 2;
|
|
33
|
+
if (max === min) {
|
|
34
|
+
s = 0;
|
|
35
|
+
} else if (l <= 0.5) {
|
|
36
|
+
s = delta / (max + min);
|
|
37
|
+
} else {
|
|
38
|
+
s = delta / (2 - max - min);
|
|
39
|
+
}
|
|
40
|
+
let t2;
|
|
41
|
+
let t3;
|
|
42
|
+
let val;
|
|
43
|
+
if (s === 0) {
|
|
44
|
+
val = l * 255;
|
|
45
|
+
return val << 16 | val << 8 | val;
|
|
46
|
+
}
|
|
47
|
+
if (l < 0.5) {
|
|
48
|
+
t2 = l * (1 + s);
|
|
49
|
+
} else {
|
|
50
|
+
t2 = l + s - l * s;
|
|
51
|
+
}
|
|
52
|
+
const t1 = 2 * l - t2;
|
|
53
|
+
const result = [0, 0, 0];
|
|
54
|
+
for (let i = 0; i < 3; i++) {
|
|
55
|
+
t3 = h + 1 / 3 * -(i - 1);
|
|
56
|
+
if (t3 < 0) {
|
|
57
|
+
t3++;
|
|
58
|
+
}
|
|
59
|
+
if (t3 > 1) {
|
|
60
|
+
t3--;
|
|
61
|
+
}
|
|
62
|
+
if (6 * t3 < 1) {
|
|
63
|
+
val = t1 + (t2 - t1) * 6 * t3;
|
|
64
|
+
} else if (2 * t3 < 1) {
|
|
65
|
+
val = t2;
|
|
66
|
+
} else if (3 * t3 < 2) {
|
|
67
|
+
val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
|
|
68
|
+
} else {
|
|
69
|
+
val = t1;
|
|
70
|
+
}
|
|
71
|
+
result[i] = val * 255;
|
|
72
|
+
}
|
|
73
|
+
return result[2] << 16 | result[1] << 8 | result[0];
|
|
74
|
+
};
|
|
75
|
+
var createCanvasFromImage = (image) => {
|
|
76
|
+
const canvas = createCanvas(image.width, image.height);
|
|
77
|
+
const context = canvas.getContext("2d");
|
|
78
|
+
context.drawImage(image, 0, 0);
|
|
79
|
+
return canvas;
|
|
80
|
+
};
|
|
81
|
+
var equals = (a, b) => {
|
|
82
|
+
if (a.width !== b.width || a.height !== b.height) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
for (let i = 0; i < a.data.length; i++) {
|
|
86
|
+
if (a.data[i] !== b.data[i]) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
92
|
+
function paletteSwap(image, inputVariants, staticColors, images, options) {
|
|
93
|
+
const canvas = createCanvasFromImage(image);
|
|
94
|
+
const context = canvas.getContext("2d");
|
|
95
|
+
const { height, width } = canvas;
|
|
96
|
+
const imageData = new Uint32Array(
|
|
97
|
+
context.getImageData(0, 0, width, height).data.buffer
|
|
98
|
+
);
|
|
99
|
+
const results = /* @__PURE__ */ new Map();
|
|
100
|
+
const variants = [];
|
|
101
|
+
let i = 0;
|
|
102
|
+
const palettes = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const [variant, palette] of inputVariants) {
|
|
104
|
+
const newImageData = createImageData(
|
|
105
|
+
new Uint8ClampedArray(width * height * 4),
|
|
106
|
+
width
|
|
107
|
+
);
|
|
108
|
+
if (typeof palette !== "number") {
|
|
109
|
+
for (const [key, value] of palette) {
|
|
110
|
+
const k = hexToInt32(key);
|
|
111
|
+
const v = palettes.get(k) || [];
|
|
112
|
+
v[i] = hexToInt32(value);
|
|
113
|
+
palettes.set(k, v);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
variants.push([
|
|
117
|
+
variant,
|
|
118
|
+
newImageData,
|
|
119
|
+
new Uint32Array(newImageData.data.buffer),
|
|
120
|
+
typeof palette === "number" ? palette : null
|
|
121
|
+
]);
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
const staticColorNumbers = staticColors ? new Set([...staticColors].map(hexToInt32)) : null;
|
|
125
|
+
const missing = /* @__PURE__ */ new Set();
|
|
126
|
+
for (var y = 0; y < height; ++y) {
|
|
127
|
+
for (var x = 0; x < width; ++x) {
|
|
128
|
+
const index = y * width + x;
|
|
129
|
+
const originalColor = imageData[index];
|
|
130
|
+
const a = originalColor >> 24 & 255;
|
|
131
|
+
if (a === 0) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const color = originalColor & 16777215;
|
|
135
|
+
const isStatic = staticColorNumbers?.has(color);
|
|
136
|
+
const colors = !isStatic ? palettes.get(color) : null;
|
|
137
|
+
for (let i2 = 0; i2 < variants.length; i2++) {
|
|
138
|
+
const newImageData = variants[i2][2];
|
|
139
|
+
if (isStatic) {
|
|
140
|
+
newImageData[index] = originalColor;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const hue = variants[i2][3];
|
|
144
|
+
if (hue) {
|
|
145
|
+
newImageData[index] = a << 24 | applyHue(int32ToRgb(color), hue);
|
|
146
|
+
} else if (colors?.[i2]) {
|
|
147
|
+
newImageData[index] = a << 24 | colors[i2];
|
|
148
|
+
} else if (!staticColorNumbers) {
|
|
149
|
+
newImageData[index] = originalColor;
|
|
150
|
+
} else {
|
|
151
|
+
missing.add([variants[i2][0], color]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!options?.ignoreMissing && missing.size) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`There is no mapping for ${Array.from(
|
|
159
|
+
new Set(
|
|
160
|
+
[...missing].map(
|
|
161
|
+
([variant, color]) => `'${int32ToHex(color)}' in variant '${variant}'`
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
).join(", ")} in the palette${options?.imageName ? ` for image '${options?.imageName}'` : ""}.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
for (let i2 = 0; i2 < variants.length; i2++) {
|
|
168
|
+
const name = variants[i2][0];
|
|
169
|
+
const existingImage = images?.get(name);
|
|
170
|
+
const existingImageData = existingImage ? createCanvasFromImage(existingImage).getContext("2d").getImageData(0, 0, width, height) : null;
|
|
171
|
+
const newImageData = variants[i2][1];
|
|
172
|
+
if (!existingImageData || !equals(newImageData, existingImageData)) {
|
|
173
|
+
const newCanvas = createCanvasFromImage(image);
|
|
174
|
+
newCanvas.getContext("2d").putImageData(newImageData, 0, 0);
|
|
175
|
+
results.set(name, newCanvas);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
paletteSwap as default
|
|
182
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nkzw/palette-swap",
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"author": "Christoph Nakazawa <christoph.pojer@gmail.com>",
|
|
5
|
+
"description": "Image palette swapping and color replacement tool based on canvas for Node.js and browsers.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/cpojer/palette-swap.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"main": "lib/palette-swap.js",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"color",
|
|
15
|
+
"palette",
|
|
16
|
+
"canvas"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"canvas": "^2.10.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/color-convert": "^2.0.0",
|
|
26
|
+
"@types/node": "^18.11.18",
|
|
27
|
+
"canvas": "^2.11.0",
|
|
28
|
+
"esbuild": "^0.17.2",
|
|
29
|
+
"typescript": "^4.9.4"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "mkdir -p lib && rm lib/*; tsc --declaration --emitDeclarationOnly --noEmit false && esbuild --target=node18 --format=esm --platform=node --outfile=lib/palette-swap.js --external:canvas --bundle ./src/palette-swap.js",
|
|
33
|
+
"test": "tsc"
|
|
34
|
+
}
|
|
35
|
+
}
|