@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.
@@ -1,11 +1,14 @@
1
- import { Canvas, Image as CanvasImage } from 'canvas';
1
+ import { Canvas, Image as Image$1 } from "canvas";
2
+
3
+ //#region src/palette-swap.d.ts
2
4
  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;
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
- export {};
13
+ //#endregion
14
+ export { HEX, HSL, Image, VariantName, paletteSwap as default };
@@ -1,182 +1,203 @@
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;
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
- 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];
7
+ const int32ToRgb = (color) => {
8
+ return [
9
+ color >> 16 & 255,
10
+ color >> 8 & 255,
11
+ color & 255
12
+ ];
16
13
  };
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]);
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
- 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];
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
- 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;
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
- 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;
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
- 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
- `${options?.imageName ? `${options.imageName}` : "Pallete Swap"}: Missing ${Array.from(
159
- new Set(
160
- [...missing].map(
161
- ([variant, color]) => `'${int32ToHex(color)}' in '${variant}'`
162
- )
163
- )
164
- ).join(", ")}.`
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;
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
- export {
181
- paletteSwap as default
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.0.0",
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
- "keywords": [
14
- "color",
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.0.0"
20
+ "canvas": "^3.2.3"
23
21
  },
24
22
  "devDependencies": {
25
- "@types/color-convert": "^2.0.4",
26
- "@types/node": "^22.10.2",
27
- "esbuild": "^0.24.2",
28
- "typescript": "^5.7.2"
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
- "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",
32
- "test": "tsc"
32
+ "benchmark": "vitest bench --run",
33
+ "build": "tsdown",
34
+ "test": "tsc && vitest run"
33
35
  }
34
36
  }
@@ -0,0 +1,4 @@
1
+ allowBuilds:
2
+ canvas: true
3
+ onlyBuiltDependencies:
4
+ - canvas