@jjlmoya/utils-games-development 1.55.0 → 1.57.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/package.json +1 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +1 -0
- package/src/tests/contrast_legibility.test.ts +56 -10
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/runtime_registry.test.ts +27 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/localizationSanitizer/localization-sanitizer.css +4 -2
- package/src/tool/pixelArtPaletteSwapper/app-client.ts +234 -0
- package/src/tool/pixelArtPaletteSwapper/bibliography.astro +6 -0
- package/src/tool/pixelArtPaletteSwapper/bibliography.ts +16 -0
- package/src/tool/pixelArtPaletteSwapper/component.astro +153 -0
- package/src/tool/pixelArtPaletteSwapper/entry.ts +28 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/de.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/en.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/es.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/fr.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/id.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/it.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +225 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +225 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/nl.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/pl.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/pt.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/ru.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/sv.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/tr.ts +233 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +225 -0
- package/src/tool/pixelArtPaletteSwapper/index.ts +12 -0
- package/src/tool/pixelArtPaletteSwapper/logic.test.ts +43 -0
- package/src/tool/pixelArtPaletteSwapper/logic.ts +201 -0
- package/src/tool/pixelArtPaletteSwapper/pixel-art-palette-swapper.css +728 -0
- package/src/tool/pixelArtPaletteSwapper/seo.astro +15 -0
- package/src/tool/pixelArtPaletteSwapper/ui.ts +35 -0
- package/src/tool/retroSfxGenerator/client.ts +9 -2
- package/src/tool/retroSfxGenerator/logic.test.ts +21 -0
- package/src/tool/retroSfxGenerator/logic.ts +1 -1
- package/src/tool/retroSfxGenerator/retro-sfx-generator.css +10 -4
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
export interface RGBColor {
|
|
2
|
+
r: number;
|
|
3
|
+
g: number;
|
|
4
|
+
b: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PaletteColor extends RGBColor {
|
|
8
|
+
hex: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PresetPalettes {
|
|
12
|
+
gameBoy: PaletteColor[];
|
|
13
|
+
nes: PaletteColor[];
|
|
14
|
+
pico8: PaletteColor[];
|
|
15
|
+
commodore64: PaletteColor[];
|
|
16
|
+
dawnBringer16: PaletteColor[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface QuantizationResult {
|
|
20
|
+
data: Uint8ClampedArray;
|
|
21
|
+
sourceColors: number;
|
|
22
|
+
mappedColors: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const PRESET_PALETTES: PresetPalettes = {
|
|
26
|
+
gameBoy: ['#0f380f', '#306230', '#8bac0f', '#9bbc0f'].map(createPresetPaletteColor),
|
|
27
|
+
nes: [
|
|
28
|
+
'#000000', '#fcfcfc', '#f8f8f8', '#b8b8b8', '#7c7c7c', '#a80000', '#e45c10', '#f8b800',
|
|
29
|
+
'#f8d878', '#00a800', '#58d854', '#b8f8b8', '#0058f8', '#00a8e8', '#3cbcfc', '#a4e4fc',
|
|
30
|
+
].map(createPresetPaletteColor),
|
|
31
|
+
pico8: [
|
|
32
|
+
'#000000', '#1d2b53', '#7e2553', '#008751', '#ab5236', '#5f574f', '#c2c3c7', '#fff1e8',
|
|
33
|
+
'#ff004d', '#ffa300', '#ffec27', '#00e436', '#29adff', '#83769c', '#ff77a8', '#ffccaa',
|
|
34
|
+
].map(createPresetPaletteColor),
|
|
35
|
+
commodore64: [
|
|
36
|
+
'#000000', '#ffffff', '#813338', '#75cec8', '#8e3c97', '#56ac4d', '#2e2c9b', '#edf171',
|
|
37
|
+
'#8e5029', '#553800', '#c46c71', '#4a4a4a', '#7b7b7b', '#a9ff9f', '#706deb', '#b2b2b2',
|
|
38
|
+
].map(createPresetPaletteColor),
|
|
39
|
+
dawnBringer16: [
|
|
40
|
+
'#140c1c', '#442434', '#30346d', '#4e4a4e', '#854c30', '#346524', '#d04648', '#757161',
|
|
41
|
+
'#597dce', '#d27d2c', '#8595a1', '#6daa2c', '#d2aa99', '#6dc2ca', '#dad45e', '#deeed6',
|
|
42
|
+
].map(createPresetPaletteColor),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function createPaletteColor(hex: string): PaletteColor {
|
|
46
|
+
const rgb = hexToRgb(hex);
|
|
47
|
+
if (!rgb) {
|
|
48
|
+
throw new Error(`Invalid color: ${hex}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { ...rgb, hex: rgbToHex(rgb) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hexToRgb(value: string): RGBColor | null {
|
|
55
|
+
const normalized = value.trim().replace(/^#/, '');
|
|
56
|
+
const expanded = normalized.length === 3
|
|
57
|
+
? normalized.split('').map((channel) => `${channel}${channel}`).join('')
|
|
58
|
+
: normalized;
|
|
59
|
+
|
|
60
|
+
if (!/^[0-9a-f]{6}$/i.test(expanded)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
r: Number.parseInt(expanded.slice(0, 2), 16),
|
|
66
|
+
g: Number.parseInt(expanded.slice(2, 4), 16),
|
|
67
|
+
b: Number.parseInt(expanded.slice(4, 6), 16),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function rgbToHex(color: RGBColor): string {
|
|
72
|
+
return `#${[color.r, color.g, color.b]
|
|
73
|
+
.map((channel) => Math.max(0, Math.min(255, Math.round(channel))).toString(16).padStart(2, '0'))
|
|
74
|
+
.join('')}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parsePaletteInput(value: string): PaletteColor[] {
|
|
78
|
+
const colors = value
|
|
79
|
+
.split(/[\s,;]+/)
|
|
80
|
+
.map((token) => token.trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
const seen = new Set<string>();
|
|
83
|
+
const palette: PaletteColor[] = [];
|
|
84
|
+
|
|
85
|
+
for (const color of colors) {
|
|
86
|
+
const parsed = hexToRgb(color);
|
|
87
|
+
if (!parsed) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const hex = rgbToHex(parsed);
|
|
91
|
+
if (!seen.has(hex)) {
|
|
92
|
+
seen.add(hex);
|
|
93
|
+
palette.push({ ...parsed, hex });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return palette;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function findNearestColor(source: RGBColor, palette: PaletteColor[]): PaletteColor | null {
|
|
101
|
+
if (palette.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const nearestColor = palette[0];
|
|
106
|
+
if (!nearestColor) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let nearest = nearestColor;
|
|
111
|
+
let distance = colorDistanceSquared(source, nearest);
|
|
112
|
+
|
|
113
|
+
for (let index = 1; index < palette.length; index += 1) {
|
|
114
|
+
const candidate = palette[index];
|
|
115
|
+
if (!candidate) continue;
|
|
116
|
+
const candidateDistance = colorDistanceSquared(source, candidate);
|
|
117
|
+
if (candidateDistance < distance) {
|
|
118
|
+
nearest = candidate;
|
|
119
|
+
distance = candidateDistance;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return nearest;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function colorDistanceSquared(first: RGBColor, second: RGBColor): number {
|
|
127
|
+
const red = first.r - second.r;
|
|
128
|
+
const green = first.g - second.g;
|
|
129
|
+
const blue = first.b - second.b;
|
|
130
|
+
return red * red + green * green + blue * blue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface QuantizePixelContext {
|
|
134
|
+
data: Uint8ClampedArray;
|
|
135
|
+
index: number;
|
|
136
|
+
palette: PaletteColor[];
|
|
137
|
+
preserveTransparency: boolean;
|
|
138
|
+
sourceColors: Set<string>;
|
|
139
|
+
mappedColors: Set<string>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface PixelData {
|
|
143
|
+
alpha: number;
|
|
144
|
+
color: RGBColor;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readPixel(data: Uint8ClampedArray, index: number): PixelData | null {
|
|
148
|
+
const alpha = data[index + 3];
|
|
149
|
+
const red = data[index];
|
|
150
|
+
const green = data[index + 1];
|
|
151
|
+
const blue = data[index + 2];
|
|
152
|
+
if (alpha === undefined) return null;
|
|
153
|
+
if (red === undefined) return null;
|
|
154
|
+
if (green === undefined) return null;
|
|
155
|
+
if (blue === undefined) return null;
|
|
156
|
+
return { alpha, color: { r: red, g: green, b: blue } };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function processPixel(ctx: QuantizePixelContext): void {
|
|
160
|
+
const { data, index, palette, preserveTransparency, sourceColors, mappedColors } = ctx;
|
|
161
|
+
const pixel = readPixel(data, index);
|
|
162
|
+
if (!pixel || (pixel.alpha === 0 && preserveTransparency)) return;
|
|
163
|
+
|
|
164
|
+
const mapped = findNearestColor(pixel.color, palette);
|
|
165
|
+
if (!mapped) return;
|
|
166
|
+
|
|
167
|
+
sourceColors.add(rgbToHex(pixel.color));
|
|
168
|
+
mappedColors.add(mapped.hex);
|
|
169
|
+
data[index] = mapped.r;
|
|
170
|
+
data[index + 1] = mapped.g;
|
|
171
|
+
data[index + 2] = mapped.b;
|
|
172
|
+
if (!preserveTransparency) data[index + 3] = 255;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function quantizeImageData(
|
|
176
|
+
source: Uint8ClampedArray,
|
|
177
|
+
palette: PaletteColor[],
|
|
178
|
+
preserveTransparency = true,
|
|
179
|
+
): QuantizationResult {
|
|
180
|
+
const data = new Uint8ClampedArray(source);
|
|
181
|
+
const sourceColors = new Set<string>();
|
|
182
|
+
const mappedColors = new Set<string>();
|
|
183
|
+
|
|
184
|
+
if (palette.length === 0) {
|
|
185
|
+
return { data, sourceColors: 0, mappedColors: 0 };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (let index = 0; index < data.length; index += 4) {
|
|
189
|
+
processPixel({ data, index, palette, preserveTransparency, sourceColors, mappedColors });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { data, sourceColors: sourceColors.size, mappedColors: mappedColors.size };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createPresetPaletteColor(hex: string): PaletteColor {
|
|
196
|
+
const rgb = hexToRgb(hex);
|
|
197
|
+
if (!rgb) {
|
|
198
|
+
throw new Error(`Invalid color: ${hex}`);
|
|
199
|
+
}
|
|
200
|
+
return { ...rgb, hex: rgbToHex(rgb) };
|
|
201
|
+
}
|