@nkzw/palette-swap 3.2.0 → 4.0.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/.gitignore +2 -0
- package/.npmignore +6 -0
- package/README.md +46 -14
- package/lib/browser.d.ts +9 -0
- package/lib/browser.js +24 -0
- package/lib/node.d.ts +10 -0
- package/lib/node.js +17 -0
- package/lib/palette-swap-BQyCssvL.d.ts +6 -0
- package/lib/palette-swap-D-ZznABu.js +199 -0
- package/package.json +24 -9
- package/lib/palette-swap.d.ts +0 -14
- package/lib/palette-swap.js +0 -203
- package/pnpm-workspace.yaml +0 -4
package/.gitignore
ADDED
package/.npmignore
ADDED
package/README.md
CHANGED
|
@@ -9,7 +9,15 @@ Image palette swapping and color replacement tool based on canvas for Node.js an
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
npm install @nkzw/palette-swap
|
|
12
|
+
npm install @nkzw/palette-swap
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package selects the browser or Node.js entry point automatically. Browser bundles use native `HTMLCanvasElement` and `ImageData` and do not include the Node backend. Node.js uses `@napi-rs/canvas`, whose platform binaries are installed as optional dependencies; keep optional dependencies enabled.
|
|
16
|
+
|
|
17
|
+
For Node.js applications that also load images or create canvases, add the backend as a direct dependency:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install @napi-rs/canvas
|
|
13
21
|
```
|
|
14
22
|
|
|
15
23
|
## Usage
|
|
@@ -19,9 +27,9 @@ npm install @nkzw/palette-swap canvas
|
|
|
19
27
|
Here is an example of how to use `paletteSwap()` to create two variants from a base image:
|
|
20
28
|
|
|
21
29
|
```js
|
|
22
|
-
import { loadImage } from 'canvas';
|
|
30
|
+
import { loadImage } from '@napi-rs/canvas';
|
|
23
31
|
import { writeFileSync } from 'node:fs';
|
|
24
|
-
import paletteSwap from '
|
|
32
|
+
import paletteSwap from '@nkzw/palette-swap';
|
|
25
33
|
|
|
26
34
|
const results = paletteSwap(
|
|
27
35
|
await loadImage('./Yoshi.png'),
|
|
@@ -36,23 +44,37 @@ const results = paletteSwap(
|
|
|
36
44
|
],
|
|
37
45
|
[3, 220],
|
|
38
46
|
]),
|
|
39
|
-
new Set([
|
|
40
|
-
'#000000',
|
|
41
|
-
'#903020',
|
|
42
|
-
'#f84020',
|
|
43
|
-
'#f89000',
|
|
44
|
-
'#ff0000',
|
|
45
|
-
'#f8c0a8',
|
|
46
|
-
'#f8e0d0',
|
|
47
|
-
'#f8f8f8',
|
|
48
|
-
]),
|
|
47
|
+
new Set(['#000000', '#903020', '#f84020', '#f89000', '#ff0000', '#f8c0a8', '#f8e0d0', '#f8f8f8']),
|
|
49
48
|
);
|
|
50
49
|
|
|
51
50
|
for (const [variant, canvas] of results) {
|
|
52
|
-
writeFileSync(`./Yoshi-${variant}.png`, canvas.toBuffer());
|
|
51
|
+
writeFileSync(`./Yoshi-${variant}.png`, canvas.toBuffer('image/png'));
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Browser
|
|
56
|
+
|
|
57
|
+
Use the same import and palette definitions with a loaded browser image:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import paletteSwap from '@nkzw/palette-swap';
|
|
61
|
+
|
|
62
|
+
const image = new Image();
|
|
63
|
+
image.src = './Yoshi.png';
|
|
64
|
+
await image.decode();
|
|
65
|
+
|
|
66
|
+
const variants = new Map([[2, new Map([['#006000', '#570061']])]]);
|
|
67
|
+
for (const canvas of paletteSwap(image, variants).values()) {
|
|
68
|
+
document.body.append(canvas);
|
|
53
69
|
}
|
|
54
70
|
```
|
|
55
71
|
|
|
72
|
+
Modern bundlers select the `browser` export automatically. The browser fallback also supports bundlers using the `browser` field. Each entry point has matching TypeScript declarations. With TypeScript's `bundler` module resolution, set `customConditions: ["browser"]` to select the browser declarations explicitly.
|
|
73
|
+
|
|
74
|
+
### Migrating from node-canvas
|
|
75
|
+
|
|
76
|
+
Browser callers keep the same API. Node.js callers should create and load images using `@napi-rs/canvas` instead of `canvas`, and use `canvas.toBuffer('image/png')` to encode results. Returned Node canvases are `@napi-rs/canvas` objects; native objects from the old `canvas` package are not interchangeable.
|
|
77
|
+
|
|
56
78
|
## Parameters
|
|
57
79
|
|
|
58
80
|
### `image: Image`
|
|
@@ -105,3 +127,13 @@ const results = paletteSwap(
|
|
|
105
127
|
### `options?: { imageName?: string; ignoreMissing?: boolean; }`
|
|
106
128
|
|
|
107
129
|
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.
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
pnpm install
|
|
135
|
+
pnpm exec playwright install chromium
|
|
136
|
+
pnpm test
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Tests build the published entry points, verify JavaScript and TypeScript export resolution, reject native dependencies in browser bundles, and compare browser and Node pixels, including transparent edges. The existing example pixel hashes remain regression checks.
|
package/lib/browser.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { n as HSL, r as VariantName, t as HEX } from "./palette-swap-BQyCssvL.js";
|
|
2
|
+
//#region src/browser.d.ts
|
|
3
|
+
export type Image = HTMLImageElement | HTMLCanvasElement | ImageBitmap | OffscreenCanvas;
|
|
4
|
+
declare const _default: (image: Image, inputVariants: ReadonlyMap<VariantName, ReadonlyMap<HEX, HEX> | number>, staticColors?: Set<HEX> | null, images?: ReadonlyMap<VariantName, Image> | null | undefined, options?: {
|
|
5
|
+
imageName?: string;
|
|
6
|
+
ignoreMissing?: boolean;
|
|
7
|
+
} | undefined) => ReadonlyMap<VariantName, HTMLCanvasElement>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { type HEX, type HSL, type VariantName, _default as default };
|
package/lib/browser.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { t as createPaletteSwap } from "./palette-swap-D-ZznABu.js";
|
|
2
|
+
//#region src/browser.ts
|
|
3
|
+
var browser_default = createPaletteSwap({
|
|
4
|
+
readImage(image, width = image.width, height = image.height) {
|
|
5
|
+
const canvas = document.createElement("canvas");
|
|
6
|
+
canvas.width = image.width;
|
|
7
|
+
canvas.height = image.height;
|
|
8
|
+
const context = canvas.getContext("2d");
|
|
9
|
+
if (!context) throw new Error("paletteSwap: Could not create a canvas context.");
|
|
10
|
+
context.drawImage(image, 0, 0);
|
|
11
|
+
return context.getImageData(0, 0, width, height);
|
|
12
|
+
},
|
|
13
|
+
writeImage({ data, height, width }) {
|
|
14
|
+
const canvas = document.createElement("canvas");
|
|
15
|
+
canvas.width = width;
|
|
16
|
+
canvas.height = height;
|
|
17
|
+
const context = canvas.getContext("2d");
|
|
18
|
+
if (!context) throw new Error("paletteSwap: Could not create a canvas context.");
|
|
19
|
+
context.putImageData(new ImageData(data, width, height), 0, 0);
|
|
20
|
+
return canvas;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
//#endregion
|
|
24
|
+
export { browser_default as default };
|
package/lib/node.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { n as HSL, r as VariantName, t as HEX } from "./palette-swap-BQyCssvL.js";
|
|
2
|
+
import { Canvas, Image as Image$1 } from "@napi-rs/canvas";
|
|
3
|
+
//#region src/node.d.ts
|
|
4
|
+
export type Image = Image$1 | Canvas;
|
|
5
|
+
declare const _default: (image: Image, inputVariants: ReadonlyMap<VariantName, ReadonlyMap<HEX, HEX> | number>, staticColors?: Set<HEX> | null, images?: ReadonlyMap<VariantName, Image> | null | undefined, options?: {
|
|
6
|
+
imageName?: string;
|
|
7
|
+
ignoreMissing?: boolean;
|
|
8
|
+
} | undefined) => ReadonlyMap<VariantName, Canvas>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { type HEX, type HSL, type VariantName, _default as default };
|
package/lib/node.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { t as createPaletteSwap } from "./palette-swap-D-ZznABu.js";
|
|
2
|
+
import { ImageData, createCanvas } from "@napi-rs/canvas";
|
|
3
|
+
//#region src/node.ts
|
|
4
|
+
var node_default = createPaletteSwap({
|
|
5
|
+
readImage(image, width = image.width, height = image.height) {
|
|
6
|
+
const context = createCanvas(image.width, image.height).getContext("2d");
|
|
7
|
+
context.drawImage(image, 0, 0);
|
|
8
|
+
return context.getImageData(0, 0, width, height);
|
|
9
|
+
},
|
|
10
|
+
writeImage({ data, height, width }) {
|
|
11
|
+
const canvas = createCanvas(width, height);
|
|
12
|
+
canvas.getContext("2d").putImageData(new ImageData(data, width, height), 0, 0);
|
|
13
|
+
return canvas;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
//#endregion
|
|
17
|
+
export { node_default as default };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
//#region src/palette-swap.ts
|
|
2
|
+
const hexToUint32LE = (hex) => {
|
|
3
|
+
const n = Number.parseInt(hex.slice(1), 16) & 16777215;
|
|
4
|
+
const [r, g, b] = int32ToRgb(n);
|
|
5
|
+
return b << 16 | g << 8 | r;
|
|
6
|
+
};
|
|
7
|
+
const int32ToRgb = (color) => {
|
|
8
|
+
return [
|
|
9
|
+
color >> 16 & 255,
|
|
10
|
+
color >> 8 & 255,
|
|
11
|
+
color & 255
|
|
12
|
+
];
|
|
13
|
+
};
|
|
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
|
+
]);
|
|
23
|
+
};
|
|
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;
|
|
63
|
+
};
|
|
64
|
+
const equals = (a, b) => {
|
|
65
|
+
if (a.width !== b.width || a.height !== b.height) return false;
|
|
66
|
+
for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
const colorCacheSlot = (keys, key, mask) => {
|
|
70
|
+
let slot = Math.imul(key, 2654435761) & mask;
|
|
71
|
+
let cachedKey = keys[slot];
|
|
72
|
+
while (cachedKey !== 0 && cachedKey !== key) {
|
|
73
|
+
slot = slot + 1 & mask;
|
|
74
|
+
cachedKey = keys[slot];
|
|
75
|
+
}
|
|
76
|
+
return slot;
|
|
77
|
+
};
|
|
78
|
+
const maxCachedColors = 4096;
|
|
79
|
+
function createPaletteSwap({ readImage, writeImage }) {
|
|
80
|
+
return function paletteSwap(image, inputVariants, staticColors, images, options) {
|
|
81
|
+
const pixels = readImage(image);
|
|
82
|
+
const { height, width } = pixels;
|
|
83
|
+
const size = width * height;
|
|
84
|
+
const imageData = new Uint32Array(pixels.data.buffer, pixels.data.byteOffset, size);
|
|
85
|
+
const results = /* @__PURE__ */ new Map();
|
|
86
|
+
const variantNames = [];
|
|
87
|
+
const variantImageData = [];
|
|
88
|
+
const variantBuffers = [];
|
|
89
|
+
const variantHues = [];
|
|
90
|
+
let i = 0;
|
|
91
|
+
const palettes = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const [variant, palette] of inputVariants) {
|
|
93
|
+
const newImageData = {
|
|
94
|
+
data: new Uint8ClampedArray(size * 4),
|
|
95
|
+
height,
|
|
96
|
+
width
|
|
97
|
+
};
|
|
98
|
+
if (typeof palette !== "number") for (const [key, value] of palette) {
|
|
99
|
+
const k = hexToUint32LE(key);
|
|
100
|
+
const v = palettes.get(k) || [];
|
|
101
|
+
v[i] = hexToUint32LE(value);
|
|
102
|
+
palettes.set(k, v);
|
|
103
|
+
}
|
|
104
|
+
variantNames.push(variant);
|
|
105
|
+
variantImageData.push(newImageData);
|
|
106
|
+
variantBuffers.push(new Uint32Array(newImageData.data.buffer));
|
|
107
|
+
variantHues.push(typeof palette === "number" ? palette : null);
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
const variantCount = variantNames.length;
|
|
111
|
+
const staticColorNumbers = staticColors ? new Set([...staticColors].map(hexToUint32LE)) : null;
|
|
112
|
+
const trackMissing = !options?.ignoreMissing;
|
|
113
|
+
const missing = /* @__PURE__ */ new Set();
|
|
114
|
+
const colorCacheKeys = new Uint32Array(maxCachedColors * 2);
|
|
115
|
+
const colorCacheValues = new Uint32Array(colorCacheKeys.length * variantCount);
|
|
116
|
+
const colorCacheMask = colorCacheKeys.length - 1;
|
|
117
|
+
let colorCacheSize = 0;
|
|
118
|
+
const uncachedColors = new Uint32Array(variantCount);
|
|
119
|
+
const buffer0 = variantBuffers[0];
|
|
120
|
+
const buffer1 = variantBuffers[1];
|
|
121
|
+
const buffer2 = variantBuffers[2];
|
|
122
|
+
const buffer3 = variantBuffers[3];
|
|
123
|
+
const buffer4 = variantBuffers[4];
|
|
124
|
+
const buffer5 = variantBuffers[5];
|
|
125
|
+
const buffer6 = variantBuffers[6];
|
|
126
|
+
const buffer7 = variantBuffers[7];
|
|
127
|
+
for (let index = 0; index < size; index++) {
|
|
128
|
+
const originalColor = imageData[index];
|
|
129
|
+
const a = originalColor >> 24 & 255;
|
|
130
|
+
if (a === 0) continue;
|
|
131
|
+
const key = originalColor >>> 0;
|
|
132
|
+
const slot = colorCacheSlot(colorCacheKeys, key, colorCacheMask);
|
|
133
|
+
const cachedKey = colorCacheKeys[slot];
|
|
134
|
+
const canCache = cachedKey === key || colorCacheSize < maxCachedColors;
|
|
135
|
+
const colors = canCache ? colorCacheValues : uncachedColors;
|
|
136
|
+
const colorOffset = canCache ? slot * variantCount : 0;
|
|
137
|
+
if (cachedKey !== key) {
|
|
138
|
+
if (canCache) {
|
|
139
|
+
colorCacheKeys[slot] = key;
|
|
140
|
+
colorCacheSize++;
|
|
141
|
+
}
|
|
142
|
+
const color = originalColor & 16777215;
|
|
143
|
+
const isStatic = staticColorNumbers?.has(color);
|
|
144
|
+
const paletteColors = !isStatic ? palettes.get(color) : null;
|
|
145
|
+
for (let i = 0; i < variantCount; i++) {
|
|
146
|
+
if (isStatic) {
|
|
147
|
+
colors[colorOffset + i] = originalColor;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const hue = variantHues[i];
|
|
151
|
+
if (hue) colors[colorOffset + i] = a << 24 | applyHue(color, hue);
|
|
152
|
+
else if (paletteColors?.[i]) colors[colorOffset + i] = a << 24 | paletteColors[i];
|
|
153
|
+
else if (!staticColorNumbers) colors[colorOffset + i] = originalColor;
|
|
154
|
+
else {
|
|
155
|
+
colors[colorOffset + i] = 0;
|
|
156
|
+
if (trackMissing) missing.add([variantNames[i], color]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (variantCount === 8) {
|
|
161
|
+
const color0 = colors[colorOffset];
|
|
162
|
+
const color1 = colors[colorOffset + 1];
|
|
163
|
+
const color2 = colors[colorOffset + 2];
|
|
164
|
+
const color3 = colors[colorOffset + 3];
|
|
165
|
+
const color4 = colors[colorOffset + 4];
|
|
166
|
+
const color5 = colors[colorOffset + 5];
|
|
167
|
+
const color6 = colors[colorOffset + 6];
|
|
168
|
+
const color7 = colors[colorOffset + 7];
|
|
169
|
+
if (color0) buffer0[index] = color0;
|
|
170
|
+
if (color1) buffer1[index] = color1;
|
|
171
|
+
if (color2) buffer2[index] = color2;
|
|
172
|
+
if (color3) buffer3[index] = color3;
|
|
173
|
+
if (color4) buffer4[index] = color4;
|
|
174
|
+
if (color5) buffer5[index] = color5;
|
|
175
|
+
if (color6) buffer6[index] = color6;
|
|
176
|
+
if (color7) buffer7[index] = color7;
|
|
177
|
+
} else if (variantCount === 2) {
|
|
178
|
+
const color0 = colors[colorOffset];
|
|
179
|
+
const color1 = colors[colorOffset + 1];
|
|
180
|
+
if (color0) buffer0[index] = color0;
|
|
181
|
+
if (color1) buffer1[index] = color1;
|
|
182
|
+
} else for (let i = 0; i < variantCount; i++) {
|
|
183
|
+
const color = colors[colorOffset + i];
|
|
184
|
+
if (color) variantBuffers[i][index] = color;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
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(", ")}.`);
|
|
188
|
+
for (let i = 0; i < variantCount; i++) {
|
|
189
|
+
const name = variantNames[i];
|
|
190
|
+
const existingImage = images?.get(name);
|
|
191
|
+
const existingImageData = existingImage ? readImage(existingImage, width, height) : null;
|
|
192
|
+
const newImageData = variantImageData[i];
|
|
193
|
+
if (!existingImageData || !equals(newImageData, existingImageData)) results.set(name, writeImage(newImageData));
|
|
194
|
+
}
|
|
195
|
+
return results;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { createPaletteSwap as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nkzw/palette-swap",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Image palette swapping and color replacement tool based on canvas for Node.js and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"canvas",
|
|
@@ -14,16 +14,31 @@
|
|
|
14
14
|
"url": "https://github.com/cpojer/palette-swap.git"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
|
-
"main": "lib/
|
|
18
|
-
"
|
|
17
|
+
"main": "lib/node.js",
|
|
18
|
+
"browser": "./lib/browser.js",
|
|
19
|
+
"types": "lib/node.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"browser": {
|
|
23
|
+
"types": "./lib/browser.d.ts",
|
|
24
|
+
"default": "./lib/browser.js"
|
|
25
|
+
},
|
|
26
|
+
"node": {
|
|
27
|
+
"types": "./lib/node.d.ts",
|
|
28
|
+
"default": "./lib/node.js"
|
|
29
|
+
},
|
|
30
|
+
"default": "./lib/browser.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
19
33
|
"dependencies": {
|
|
20
|
-
"canvas": "^
|
|
34
|
+
"@napi-rs/canvas": "^1.0.9"
|
|
21
35
|
},
|
|
22
36
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^25.9.
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
37
|
+
"@types/node": "^25.9.7",
|
|
38
|
+
"playwright": "^1.63.0",
|
|
39
|
+
"tsdown": "^0.23.0",
|
|
40
|
+
"typescript": "^7.0.2",
|
|
41
|
+
"vitest": "^5.0.1"
|
|
27
42
|
},
|
|
28
43
|
"engines": {
|
|
29
44
|
"node": ">=22.0.0"
|
|
@@ -31,6 +46,6 @@
|
|
|
31
46
|
"scripts": {
|
|
32
47
|
"benchmark": "vitest bench --run",
|
|
33
48
|
"build": "tsdown",
|
|
34
|
-
"test": "tsc && vitest run"
|
|
49
|
+
"test": "pnpm build && tsc && vitest run"
|
|
35
50
|
}
|
|
36
51
|
}
|
package/lib/palette-swap.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Canvas, Image as Image$1 } from "canvas";
|
|
2
|
-
|
|
3
|
-
//#region src/palette-swap.d.ts
|
|
4
|
-
type Hue = number;
|
|
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;
|
|
12
|
-
}): ReadonlyMap<VariantName, Canvas>;
|
|
13
|
-
//#endregion
|
|
14
|
-
export { HEX, HSL, Image, VariantName, paletteSwap as default };
|
package/lib/palette-swap.js
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
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;
|
|
6
|
-
};
|
|
7
|
-
const int32ToRgb = (color) => {
|
|
8
|
-
return [
|
|
9
|
-
color >> 16 & 255,
|
|
10
|
-
color >> 8 & 255,
|
|
11
|
-
color & 255
|
|
12
|
-
];
|
|
13
|
-
};
|
|
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
|
-
]);
|
|
23
|
-
};
|
|
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;
|
|
63
|
-
};
|
|
64
|
-
const createCanvasFromImage = (image) => {
|
|
65
|
-
const canvas = createCanvas(image.width, image.height);
|
|
66
|
-
canvas.getContext("2d").drawImage(image, 0, 0);
|
|
67
|
-
return canvas;
|
|
68
|
-
};
|
|
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;
|
|
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;
|
|
84
|
-
function paletteSwap(image, inputVariants, staticColors, images, options) {
|
|
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;
|
|
201
|
-
}
|
|
202
|
-
//#endregion
|
|
203
|
-
export { paletteSwap as default };
|
package/pnpm-workspace.yaml
DELETED