@nkzw/palette-swap 3.0.0 → 3.2.0
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/lib/palette-swap.d.ts +12 -9
- package/lib/palette-swap.js +195 -174
- package/package.json +21 -19
- package/pnpm-workspace.yaml +4 -0
package/lib/palette-swap.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { Canvas, Image as
|
|
1
|
+
import { Canvas, Image as Image$1 } from "canvas";
|
|
2
|
+
|
|
3
|
+
//#region src/palette-swap.d.ts
|
|
2
4
|
type Hue = number;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
type HEX = `#${string}`;
|
|
6
|
+
type HSL = [hue: number, saturation: number, lightness: number];
|
|
7
|
+
type VariantName = number | string;
|
|
8
|
+
type Image = Image$1 | HTMLImageElement;
|
|
9
|
+
declare function paletteSwap(image: Image, inputVariants: ReadonlyMap<VariantName, ReadonlyMap<HEX, HEX> | Hue>, staticColors?: Set<HEX> | null, images?: ReadonlyMap<VariantName, Image> | null, options?: {
|
|
10
|
+
imageName?: string;
|
|
11
|
+
ignoreMissing?: boolean;
|
|
10
12
|
}): ReadonlyMap<VariantName, Canvas>;
|
|
11
|
-
|
|
13
|
+
//#endregion
|
|
14
|
+
export { HEX, HSL, Image, VariantName, paletteSwap as default };
|
package/lib/palette-swap.js
CHANGED
|
@@ -1,182 +1,203 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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;
|
|
1
|
+
import { createCanvas, createImageData } from "canvas";
|
|
2
|
+
//#region src/palette-swap.ts
|
|
3
|
+
const hexToUint32LE = (hex) => {
|
|
4
|
+
const [r, g, b] = int32ToRgb(Number.parseInt(hex.slice(1), 16) & 16777215);
|
|
5
|
+
return b << 16 | g << 8 | r;
|
|
10
6
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
const int32ToRgb = (color) => {
|
|
8
|
+
return [
|
|
9
|
+
color >> 16 & 255,
|
|
10
|
+
color >> 8 & 255,
|
|
11
|
+
color & 255
|
|
12
|
+
];
|
|
16
13
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
const hex = (value) => Math.round(value).toString(16).padStart(2, "0");
|
|
15
|
+
const rgbToHex = ([r, g, b]) => `#${hex(r)}${hex(g)}${hex(b)}`;
|
|
16
|
+
const int32ToHex = (color) => {
|
|
17
|
+
const [r, g, b] = int32ToRgb(color);
|
|
18
|
+
return rgbToHex([
|
|
19
|
+
b,
|
|
20
|
+
g,
|
|
21
|
+
r
|
|
22
|
+
]);
|
|
22
23
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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];
|
|
24
|
+
const applyHue = (color, hue) => {
|
|
25
|
+
const h = hue / 360;
|
|
26
|
+
const r = (color & 255) / 255;
|
|
27
|
+
const g = (color >> 8 & 255) / 255;
|
|
28
|
+
const b = (color >> 16 & 255) / 255;
|
|
29
|
+
const min = Math.min(r, g, b);
|
|
30
|
+
const max = Math.max(r, g, b);
|
|
31
|
+
const delta = max - min;
|
|
32
|
+
let s;
|
|
33
|
+
const l = (min + max) / 2;
|
|
34
|
+
if (max === min) s = 0;
|
|
35
|
+
else if (l <= .5) s = delta / (max + min);
|
|
36
|
+
else s = delta / (2 - max - min);
|
|
37
|
+
let t2;
|
|
38
|
+
let t3;
|
|
39
|
+
let val;
|
|
40
|
+
if (s === 0) {
|
|
41
|
+
val = l * 255;
|
|
42
|
+
return val << 16 | val << 8 | val;
|
|
43
|
+
}
|
|
44
|
+
if (l < .5) t2 = l * (1 + s);
|
|
45
|
+
else t2 = l + s - l * s;
|
|
46
|
+
const t1 = 2 * l - t2;
|
|
47
|
+
let result0 = 0;
|
|
48
|
+
let result1 = 0;
|
|
49
|
+
let result2 = 0;
|
|
50
|
+
for (let i = 0; i < 3; i++) {
|
|
51
|
+
t3 = h + 1 / 3 * -(i - 1);
|
|
52
|
+
if (t3 < 0) t3++;
|
|
53
|
+
if (t3 > 1) t3--;
|
|
54
|
+
if (6 * t3 < 1) val = t1 + (t2 - t1) * 6 * t3;
|
|
55
|
+
else if (2 * t3 < 1) val = t2;
|
|
56
|
+
else if (3 * t3 < 2) val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
|
|
57
|
+
else val = t1;
|
|
58
|
+
if (i === 0) result0 = val * 255;
|
|
59
|
+
else if (i === 1) result1 = val * 255;
|
|
60
|
+
else result2 = val * 255;
|
|
61
|
+
}
|
|
62
|
+
return result2 << 16 | result1 << 8 | result0;
|
|
74
63
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return canvas;
|
|
64
|
+
const createCanvasFromImage = (image) => {
|
|
65
|
+
const canvas = createCanvas(image.width, image.height);
|
|
66
|
+
canvas.getContext("2d").drawImage(image, 0, 0);
|
|
67
|
+
return canvas;
|
|
80
68
|
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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;
|
|
69
|
+
const equals = (a, b) => {
|
|
70
|
+
if (a.width !== b.width || a.height !== b.height) return false;
|
|
71
|
+
for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;
|
|
72
|
+
return true;
|
|
91
73
|
};
|
|
74
|
+
const colorCacheSlot = (keys, key, mask) => {
|
|
75
|
+
let slot = Math.imul(key, 2654435761) & mask;
|
|
76
|
+
let cachedKey = keys[slot];
|
|
77
|
+
while (cachedKey !== 0 && cachedKey !== key) {
|
|
78
|
+
slot = slot + 1 & mask;
|
|
79
|
+
cachedKey = keys[slot];
|
|
80
|
+
}
|
|
81
|
+
return slot;
|
|
82
|
+
};
|
|
83
|
+
const maxCachedColors = 4096;
|
|
92
84
|
function paletteSwap(image, inputVariants, staticColors, images, options) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
85
|
+
const canvas = createCanvasFromImage(image);
|
|
86
|
+
const context = canvas.getContext("2d");
|
|
87
|
+
const { height, width } = canvas;
|
|
88
|
+
const size = width * height;
|
|
89
|
+
const imageData = new Uint32Array(context.getImageData(0, 0, width, height).data.buffer);
|
|
90
|
+
const results = /* @__PURE__ */ new Map();
|
|
91
|
+
const variantNames = [];
|
|
92
|
+
const variantImageData = [];
|
|
93
|
+
const variantBuffers = [];
|
|
94
|
+
const variantHues = [];
|
|
95
|
+
let i = 0;
|
|
96
|
+
const palettes = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const [variant, palette] of inputVariants) {
|
|
98
|
+
const newImageData = createImageData(new Uint8ClampedArray(size * 4), width);
|
|
99
|
+
if (typeof palette !== "number") for (const [key, value] of palette) {
|
|
100
|
+
const k = hexToUint32LE(key);
|
|
101
|
+
const v = palettes.get(k) || [];
|
|
102
|
+
v[i] = hexToUint32LE(value);
|
|
103
|
+
palettes.set(k, v);
|
|
104
|
+
}
|
|
105
|
+
variantNames.push(variant);
|
|
106
|
+
variantImageData.push(newImageData);
|
|
107
|
+
variantBuffers.push(new Uint32Array(newImageData.data.buffer));
|
|
108
|
+
variantHues.push(typeof palette === "number" ? palette : null);
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
const variantCount = variantNames.length;
|
|
112
|
+
const staticColorNumbers = staticColors ? new Set([...staticColors].map(hexToUint32LE)) : null;
|
|
113
|
+
const trackMissing = !options?.ignoreMissing;
|
|
114
|
+
const missing = /* @__PURE__ */ new Set();
|
|
115
|
+
const colorCacheKeys = new Uint32Array(maxCachedColors * 2);
|
|
116
|
+
const colorCacheValues = new Uint32Array(colorCacheKeys.length * variantCount);
|
|
117
|
+
const colorCacheMask = colorCacheKeys.length - 1;
|
|
118
|
+
let colorCacheSize = 0;
|
|
119
|
+
const uncachedColors = new Uint32Array(variantCount);
|
|
120
|
+
const buffer0 = variantBuffers[0];
|
|
121
|
+
const buffer1 = variantBuffers[1];
|
|
122
|
+
const buffer2 = variantBuffers[2];
|
|
123
|
+
const buffer3 = variantBuffers[3];
|
|
124
|
+
const buffer4 = variantBuffers[4];
|
|
125
|
+
const buffer5 = variantBuffers[5];
|
|
126
|
+
const buffer6 = variantBuffers[6];
|
|
127
|
+
const buffer7 = variantBuffers[7];
|
|
128
|
+
for (let index = 0; index < size; index++) {
|
|
129
|
+
const originalColor = imageData[index];
|
|
130
|
+
const a = originalColor >> 24 & 255;
|
|
131
|
+
if (a === 0) continue;
|
|
132
|
+
const key = originalColor >>> 0;
|
|
133
|
+
const slot = colorCacheSlot(colorCacheKeys, key, colorCacheMask);
|
|
134
|
+
const cachedKey = colorCacheKeys[slot];
|
|
135
|
+
const canCache = cachedKey === key || colorCacheSize < maxCachedColors;
|
|
136
|
+
const colors = canCache ? colorCacheValues : uncachedColors;
|
|
137
|
+
const colorOffset = canCache ? slot * variantCount : 0;
|
|
138
|
+
if (cachedKey !== key) {
|
|
139
|
+
if (canCache) {
|
|
140
|
+
colorCacheKeys[slot] = key;
|
|
141
|
+
colorCacheSize++;
|
|
142
|
+
}
|
|
143
|
+
const color = originalColor & 16777215;
|
|
144
|
+
const isStatic = staticColorNumbers?.has(color);
|
|
145
|
+
const paletteColors = !isStatic ? palettes.get(color) : null;
|
|
146
|
+
for (let i = 0; i < variantCount; i++) {
|
|
147
|
+
if (isStatic) {
|
|
148
|
+
colors[colorOffset + i] = originalColor;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const hue = variantHues[i];
|
|
152
|
+
if (hue) colors[colorOffset + i] = a << 24 | applyHue(color, hue);
|
|
153
|
+
else if (paletteColors?.[i]) colors[colorOffset + i] = a << 24 | paletteColors[i];
|
|
154
|
+
else if (!staticColorNumbers) colors[colorOffset + i] = originalColor;
|
|
155
|
+
else {
|
|
156
|
+
colors[colorOffset + i] = 0;
|
|
157
|
+
if (trackMissing) missing.add([variantNames[i], color]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (variantCount === 8) {
|
|
162
|
+
const color0 = colors[colorOffset];
|
|
163
|
+
const color1 = colors[colorOffset + 1];
|
|
164
|
+
const color2 = colors[colorOffset + 2];
|
|
165
|
+
const color3 = colors[colorOffset + 3];
|
|
166
|
+
const color4 = colors[colorOffset + 4];
|
|
167
|
+
const color5 = colors[colorOffset + 5];
|
|
168
|
+
const color6 = colors[colorOffset + 6];
|
|
169
|
+
const color7 = colors[colorOffset + 7];
|
|
170
|
+
if (color0) buffer0[index] = color0;
|
|
171
|
+
if (color1) buffer1[index] = color1;
|
|
172
|
+
if (color2) buffer2[index] = color2;
|
|
173
|
+
if (color3) buffer3[index] = color3;
|
|
174
|
+
if (color4) buffer4[index] = color4;
|
|
175
|
+
if (color5) buffer5[index] = color5;
|
|
176
|
+
if (color6) buffer6[index] = color6;
|
|
177
|
+
if (color7) buffer7[index] = color7;
|
|
178
|
+
} else if (variantCount === 2) {
|
|
179
|
+
const color0 = colors[colorOffset];
|
|
180
|
+
const color1 = colors[colorOffset + 1];
|
|
181
|
+
if (color0) buffer0[index] = color0;
|
|
182
|
+
if (color1) buffer1[index] = color1;
|
|
183
|
+
} else for (let i = 0; i < variantCount; i++) {
|
|
184
|
+
const color = colors[colorOffset + i];
|
|
185
|
+
if (color) variantBuffers[i][index] = color;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (trackMissing && missing.size) throw new Error(`${options?.imageName ? `${options.imageName}` : "Palette Swap"}: Missing ${Array.from(new Set([...missing].map(([variant, color]) => `'${int32ToHex(color)}' in '${variant}'`))).join(", ")}.`);
|
|
189
|
+
for (let i = 0; i < variantCount; i++) {
|
|
190
|
+
const name = variantNames[i];
|
|
191
|
+
const existingImage = images?.get(name);
|
|
192
|
+
const existingImageData = existingImage ? createCanvasFromImage(existingImage).getContext("2d").getImageData(0, 0, width, height) : null;
|
|
193
|
+
const newImageData = variantImageData[i];
|
|
194
|
+
if (!existingImageData || !equals(newImageData, existingImageData)) {
|
|
195
|
+
const newCanvas = createCanvas(width, height);
|
|
196
|
+
newCanvas.getContext("2d").putImageData(newImageData, 0, 0);
|
|
197
|
+
results.set(name, newCanvas);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return results;
|
|
179
201
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
};
|
|
202
|
+
//#endregion
|
|
203
|
+
export { paletteSwap as default };
|
package/package.json
CHANGED
|
@@ -1,34 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nkzw/palette-swap",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"author": "Christoph Nakazawa <christoph.pojer@gmail.com>",
|
|
3
|
+
"version": "3.2.0",
|
|
5
4
|
"description": "Image palette swapping and color replacement tool based on canvas for Node.js and browsers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"canvas",
|
|
7
|
+
"color",
|
|
8
|
+
"palette"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "Christoph Nakazawa <christoph.pojer@gmail.com>",
|
|
6
12
|
"repository": {
|
|
7
13
|
"type": "git",
|
|
8
14
|
"url": "https://github.com/cpojer/palette-swap.git"
|
|
9
15
|
},
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"main": "lib/palette-swap.js",
|
|
12
16
|
"type": "module",
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
"palette",
|
|
16
|
-
"canvas"
|
|
17
|
-
],
|
|
18
|
-
"engines": {
|
|
19
|
-
"node": ">=18.0.0"
|
|
20
|
-
},
|
|
17
|
+
"main": "lib/palette-swap.js",
|
|
18
|
+
"types": "lib/palette-swap.d.ts",
|
|
21
19
|
"dependencies": {
|
|
22
|
-
"canvas": "^3.
|
|
20
|
+
"canvas": "^3.2.3"
|
|
23
21
|
},
|
|
24
22
|
"devDependencies": {
|
|
25
|
-
"@types/
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
23
|
+
"@types/node": "^25.9.1",
|
|
24
|
+
"tsdown": "^0.22.0",
|
|
25
|
+
"typescript": "^6.0.3",
|
|
26
|
+
"vitest": "^4.1.7"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.0.0"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
32
|
+
"benchmark": "vitest bench --run",
|
|
33
|
+
"build": "tsdown",
|
|
34
|
+
"test": "tsc && vitest run"
|
|
33
35
|
}
|
|
34
36
|
}
|