@xivdyetools/core 1.16.0 → 1.17.2
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 +37 -37
- package/README.md +436 -437
- package/dist/data/locales/de.json +1 -1
- package/dist/data/locales/en.json +1 -1
- package/dist/data/locales/fr.json +1 -1
- package/dist/data/locales/ja.json +1 -1
- package/dist/data/locales/ko.json +1 -1
- package/dist/data/locales/zh.json +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/APIService.d.ts +24 -1
- package/dist/services/APIService.d.ts.map +1 -1
- package/dist/services/APIService.js +38 -41
- package/dist/services/APIService.js.map +1 -1
- package/dist/services/CharacterColorService.js +16 -16
- package/dist/services/CharacterColorService.js.map +1 -1
- package/dist/services/DyeService.js +3 -0
- package/dist/services/DyeService.js.map +1 -1
- package/dist/services/LocalizationService.js +9 -5
- package/dist/services/LocalizationService.js.map +1 -1
- package/dist/services/PaletteService.js +8 -7
- package/dist/services/PaletteService.js.map +1 -1
- package/dist/services/PresetService.js +2 -0
- package/dist/services/PresetService.js.map +1 -1
- package/dist/services/color/ColorConverter.d.ts +3 -0
- package/dist/services/color/ColorConverter.d.ts.map +1 -1
- package/dist/services/color/ColorConverter.js +23 -4
- package/dist/services/color/ColorConverter.js.map +1 -1
- package/dist/services/color/ColorblindnessSimulator.js +2 -2
- package/dist/services/color/ColorblindnessSimulator.js.map +1 -1
- package/dist/services/color/SpectralMixer.d.ts.map +1 -1
- package/dist/services/color/SpectralMixer.js +0 -1
- package/dist/services/color/SpectralMixer.js.map +1 -1
- package/dist/services/dye/DyeDatabase.js +23 -22
- package/dist/services/dye/DyeDatabase.js.map +1 -1
- package/dist/services/dye/DyeSearch.js +2 -0
- package/dist/services/dye/DyeSearch.js.map +1 -1
- package/dist/services/dye/HarmonyGenerator.js +2 -0
- package/dist/services/dye/HarmonyGenerator.js.map +1 -1
- package/dist/services/localization/LocaleRegistry.js +1 -3
- package/dist/services/localization/LocaleRegistry.js.map +1 -1
- package/dist/services/localization/TranslationProvider.js +1 -0
- package/dist/services/localization/TranslationProvider.js.map +1 -1
- package/dist/utils/index.js +6 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/kd-tree.js +6 -4
- package/dist/utils/kd-tree.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +69 -91
package/README.md
CHANGED
|
@@ -1,437 +1,436 @@
|
|
|
1
|
-
# xivdyetools-core
|
|
2
|
-
|
|
3
|
-
> Core color algorithms and dye database for XIV Dye Tools - Environment-agnostic TypeScript library for FFXIV dye color matching, harmony generation, and accessibility checking.
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/xivdyetools-core)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://www.typescriptlang.org/)
|
|
8
|
-
|
|
9
|
-
## Features
|
|
10
|
-
|
|
11
|
-
✨ **Color Conversion** - RGB ↔ HSV ↔ Hex ↔ LAB ↔ RYB ↔ OKLAB ↔ OKLCH ↔ LCH ↔ HSL
|
|
12
|
-
🎨 **136 FFXIV Dyes** - Complete database with RGB/HSV/metadata
|
|
13
|
-
🖌️ **Advanced Color Mixing** - RYB, OKLAB, HSL, and Spectral (Kubelka-Munk)
|
|
14
|
-
🔬 **Spectral.js Integration** - Physics-based paint mixing (Blue + Yellow = Green!)
|
|
15
|
-
🎯 **Dye Matching** - Find closest dyes to any color
|
|
16
|
-
🌈 **Color Harmonies** - Triadic, complementary, analogous, and more
|
|
17
|
-
🖼️ **Palette Extraction** - K-means++ clustering for multi-color extraction from images
|
|
18
|
-
♿ **Accessibility** - Colorblindness simulation (Brettel 1997)
|
|
19
|
-
📡 **Universalis API** - Price data integration with caching
|
|
20
|
-
🔌 **Pluggable Cache** - Memory, localStorage, Redis support
|
|
21
|
-
🌍 **Environment Agnostic** - Works in Node.js, browsers, edge runtimes
|
|
22
|
-
🗣️ **6 Languages** - English, Japanese, German, French, Korean, Chinese
|
|
23
|
-
|
|
24
|
-
## Installation
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm install xivdyetools-core
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Quick Start
|
|
31
|
-
|
|
32
|
-
### Browser (with bundler)
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { ColorService, DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
36
|
-
|
|
37
|
-
// Initialize services
|
|
38
|
-
const dyeService = new DyeService(dyeDatabase);
|
|
39
|
-
|
|
40
|
-
// Find closest dye to a color
|
|
41
|
-
const closestDye = dyeService.findClosestDye('#FF6B6B');
|
|
42
|
-
console.log(closestDye.name); // "Coral Pink"
|
|
43
|
-
|
|
44
|
-
// Generate color harmonies
|
|
45
|
-
const triadicDyes = dyeService.findTriadicDyes('#FF6B6B');
|
|
46
|
-
console.log(triadicDyes.map(d => d.name)); // ["Turquoise Green", "Grape Purple"]
|
|
47
|
-
|
|
48
|
-
// Color conversions
|
|
49
|
-
const rgb = ColorService.hexToRgb('#FF6B6B');
|
|
50
|
-
const hsv = ColorService.rgbToHsv(rgb.r, rgb.g, rgb.b);
|
|
51
|
-
console.log(hsv); // { h: 0, s: 58.04, v: 100 }
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Node.js (Discord bot, API, CLI)
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
import {
|
|
58
|
-
DyeService,
|
|
59
|
-
APIService,
|
|
60
|
-
dyeDatabase
|
|
61
|
-
} from 'xivdyetools-core';
|
|
62
|
-
import Redis from 'ioredis';
|
|
63
|
-
|
|
64
|
-
// Initialize with Redis cache (for Discord bots)
|
|
65
|
-
const redis = new Redis();
|
|
66
|
-
const cacheBackend = new RedisCacheBackend(redis);
|
|
67
|
-
const apiService = new APIService(cacheBackend);
|
|
68
|
-
const dyeService = new DyeService(dyeDatabase);
|
|
69
|
-
|
|
70
|
-
// Fetch live market prices
|
|
71
|
-
const priceData = await apiService.getPriceData(5752); // Jet Black Dye
|
|
72
|
-
console.log(`${priceData.currentMinPrice} Gil`);
|
|
73
|
-
|
|
74
|
-
// Find harmony with pricing
|
|
75
|
-
const baseDye = dyeService.findClosestDye('#000000');
|
|
76
|
-
const harmonyDyes = dyeService.findComplementaryPair(baseDye.hex);
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Core Services
|
|
80
|
-
|
|
81
|
-
### ColorService
|
|
82
|
-
|
|
83
|
-
Pure color conversion and manipulation algorithms.
|
|
84
|
-
|
|
85
|
-
> **Memory Note**: ColorService uses LRU caches (5 caches × 1000 entries each = up to 5000 cached entries) for performance optimization. For long-running applications or memory-constrained environments, call `ColorService.clearCaches()` periodically to free memory. Each cache entry is approximately 50-100 bytes, so maximum memory usage is ~500KB.
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
import { ColorService } from 'xivdyetools-core';
|
|
89
|
-
|
|
90
|
-
// Hex ↔ RGB
|
|
91
|
-
const rgb = ColorService.hexToRgb('#FF6B6B');
|
|
92
|
-
const hex = ColorService.rgbToHex(255, 107, 107);
|
|
93
|
-
|
|
94
|
-
// RGB ↔ HSV
|
|
95
|
-
const hsv = ColorService.rgbToHsv(255, 107, 107);
|
|
96
|
-
const rgbFromHsv = ColorService.hsvToRgb(0, 58.04, 100);
|
|
97
|
-
|
|
98
|
-
// Colorblindness simulation
|
|
99
|
-
const simulated = ColorService.simulateColorblindness(
|
|
100
|
-
{ r: 255, g: 0, b: 0 },
|
|
101
|
-
'deuteranopia'
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// Color distance (Euclidean in RGB space)
|
|
105
|
-
const distance = ColorService.getColorDistance('#FF0000', '#00FF00');
|
|
106
|
-
|
|
107
|
-
// LAB color space and DeltaE (perceptual color difference)
|
|
108
|
-
const lab = ColorService.hexToLab('#FF6B6B');
|
|
109
|
-
const deltaE = ColorService.getDeltaE('#FF0000', '#FF6B6B'); // CIE76 by default
|
|
110
|
-
const deltaE2000 = ColorService.getDeltaE('#FF0000', '#FF6B6B', 'cie2000'); // CIEDE2000
|
|
111
|
-
|
|
112
|
-
// Color inversion
|
|
113
|
-
const inverted = ColorService.invert('#FF6B6B');
|
|
114
|
-
|
|
115
|
-
// Cache management (for memory-constrained environments)
|
|
116
|
-
ColorService.clearCaches();
|
|
117
|
-
const cacheStats = ColorService.getCacheStats();
|
|
118
|
-
|
|
119
|
-
// RYB Subtractive Color Mixing (paint-like mixing)
|
|
120
|
-
// Blue + Yellow = Green (not gray like RGB!)
|
|
121
|
-
const mixed = ColorService.mixColorsRyb('#0000FF', '#FFFF00');
|
|
122
|
-
const partialMix = ColorService.mixColorsRyb('#FF0000', '#FFFF00', 0.3); // 30% yellow
|
|
123
|
-
|
|
124
|
-
// RYB ↔ RGB conversions
|
|
125
|
-
const ryb = ColorService.hexToRyb('#00FF00');
|
|
126
|
-
const rgb = ColorService.rybToRgb(0, 255, 255); // Yellow+Blue = Green
|
|
127
|
-
const hex = ColorService.rybToHex(255, 255, 0); // Red+Yellow = Orange
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### DyeService
|
|
131
|
-
|
|
132
|
-
FFXIV dye database management and color matching.
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
import { DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
136
|
-
|
|
137
|
-
const dyeService = new DyeService(dyeDatabase);
|
|
138
|
-
|
|
139
|
-
// Database access
|
|
140
|
-
const allDyes = dyeService.getAllDyes(); // 136 dyes
|
|
141
|
-
const dyeById = dyeService.getDyeById(5752); // By itemID - Jet Black Dye
|
|
142
|
-
const dyeByStain = dyeService.getByStainId(1); // By stainID - Snow White
|
|
143
|
-
const categories = dyeService.getCategories(); // ['Neutral', 'Red', 'Blue', ...]
|
|
144
|
-
|
|
145
|
-
// Color matching
|
|
146
|
-
const closest = dyeService.findClosestDye('#FF6B6B');
|
|
147
|
-
const nearby = dyeService.findDyesWithinDistance('#FF6B6B', 50, 5);
|
|
148
|
-
|
|
149
|
-
// Harmony generation (default: fast hue-based matching)
|
|
150
|
-
const triadic = dyeService.findTriadicDyes('#FF6B6B');
|
|
151
|
-
const complementary = dyeService.findComplementaryPair('#FF6B6B');
|
|
152
|
-
const analogous = dyeService.findAnalogousDyes('#FF6B6B', 30);
|
|
153
|
-
const monochromatic = dyeService.findMonochromaticDyes('#FF6B6B', 6);
|
|
154
|
-
const splitComplementary = dyeService.findSplitComplementaryDyes('#FF6B6B');
|
|
155
|
-
|
|
156
|
-
// DeltaE-based harmony (perceptually accurate matching)
|
|
157
|
-
const triadicDeltaE = dyeService.findTriadicDyes('#FF6B6B', {
|
|
158
|
-
algorithm: 'deltaE',
|
|
159
|
-
deltaEFormula: 'cie2000', // or 'cie76' (faster, default)
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Color space selection for hue rotation (v1.16.0+)
|
|
163
|
-
// OKLCH produces more perceptually balanced harmonies
|
|
164
|
-
const triadicOklch = dyeService.findTriadicDyes('#FF6B6B', { colorSpace: 'oklch' });
|
|
165
|
-
const compLch = dyeService.findComplementaryPair('#FF6B6B', { colorSpace: 'lch' });
|
|
166
|
-
// Available spaces: 'hsv' (default), 'oklch', 'lch', 'hsl'
|
|
167
|
-
|
|
168
|
-
// Filtering
|
|
169
|
-
const redDyes = dyeService.searchByCategory('Red');
|
|
170
|
-
const searchResults = dyeService.searchByName('black');
|
|
171
|
-
const filtered = dyeService.filterDyes({
|
|
172
|
-
category: 'Special',
|
|
173
|
-
excludeIds: [5752, 5753],
|
|
174
|
-
minPrice: 0,
|
|
175
|
-
maxPrice: 10000
|
|
176
|
-
});
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### PaletteService
|
|
180
|
-
|
|
181
|
-
Multi-color palette extraction from images using K-means++ clustering.
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
import { PaletteService, DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
185
|
-
|
|
186
|
-
const paletteService = new PaletteService();
|
|
187
|
-
const dyeService = new DyeService(dyeDatabase);
|
|
188
|
-
|
|
189
|
-
// Extract from Canvas ImageData
|
|
190
|
-
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
191
|
-
const pixels = PaletteService.pixelDataToRGBFiltered(imageData.data);
|
|
192
|
-
|
|
193
|
-
// Extract dominant colors only
|
|
194
|
-
const palette = paletteService.extractPalette(pixels, { colorCount: 4 });
|
|
195
|
-
// Returns: Array<{ color: RGB, dominance: number }>
|
|
196
|
-
|
|
197
|
-
// Extract and match to FFXIV dyes
|
|
198
|
-
const matches = paletteService.extractAndMatchPalette(pixels, dyeService, {
|
|
199
|
-
colorCount: 4,
|
|
200
|
-
maxIterations: 25,
|
|
201
|
-
convergenceThreshold: 1.0,
|
|
202
|
-
maxSamples: 10000
|
|
203
|
-
});
|
|
204
|
-
// Returns: Array<{ extracted: RGB, matchedDye: Dye, distance: number, dominance: number }>
|
|
205
|
-
|
|
206
|
-
// Helper: Convert raw pixel buffer (RGB, 3 bytes per pixel)
|
|
207
|
-
const pixelsFromBuffer = PaletteService.pixelDataToRGB(buffer);
|
|
208
|
-
|
|
209
|
-
// Helper: Convert RGBA ImageData, filtering transparent pixels
|
|
210
|
-
const pixelsFromCanvas = PaletteService.pixelDataToRGBFiltered(imageData.data);
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### APIService
|
|
214
|
-
|
|
215
|
-
Universalis API integration with pluggable cache backends.
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
import { APIService, MemoryCacheBackend } from 'xivdyetools-core';
|
|
219
|
-
|
|
220
|
-
// With memory cache (default)
|
|
221
|
-
const apiService = new APIService();
|
|
222
|
-
|
|
223
|
-
// With custom cache backend
|
|
224
|
-
const cache = new MemoryCacheBackend();
|
|
225
|
-
const apiService = new APIService(cache);
|
|
226
|
-
|
|
227
|
-
// Fetch price data
|
|
228
|
-
const priceData = await apiService.getPriceData(5752); // itemID
|
|
229
|
-
const pricesWithDC = await apiService.getPriceData(5752, undefined, 'Aether');
|
|
230
|
-
|
|
231
|
-
// Batch operations
|
|
232
|
-
const prices = await apiService.getPricesForItems([5752, 5753, 5754]);
|
|
233
|
-
|
|
234
|
-
// Cache management
|
|
235
|
-
await apiService.clearCache();
|
|
236
|
-
const stats = await apiService.getCacheStats();
|
|
237
|
-
|
|
238
|
-
// API health check
|
|
239
|
-
const { available, latency } = await apiService.getAPIStatus();
|
|
240
|
-
|
|
241
|
-
// Utility methods
|
|
242
|
-
const formatted = APIService.formatPrice(123456); // "123,456G"
|
|
243
|
-
const trend = APIService.getPriceTrend(100, 80); // { trend: 'up', ... }
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
## Custom Cache Backends
|
|
247
|
-
|
|
248
|
-
Implement the `ICacheBackend` interface for custom storage:
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
import { ICacheBackend, CachedData, PriceData } from 'xivdyetools-core';
|
|
252
|
-
import Redis from 'ioredis';
|
|
253
|
-
|
|
254
|
-
class RedisCacheBackend implements ICacheBackend {
|
|
255
|
-
constructor(private redis: Redis) {}
|
|
256
|
-
|
|
257
|
-
async get(key: string): Promise<CachedData<PriceData> | null> {
|
|
258
|
-
const data = await this.redis.get(key);
|
|
259
|
-
return data ? JSON.parse(data) : null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async set(key: string, value: CachedData<PriceData>): Promise<void> {
|
|
263
|
-
await this.redis.set(key, JSON.stringify(value), 'EX', value.ttl / 1000);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async delete(key: string): Promise<void> {
|
|
267
|
-
await this.redis.del(key);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async clear(): Promise<void> {
|
|
271
|
-
await this.redis.flushdb();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async keys(): Promise<string[]> {
|
|
275
|
-
return await this.redis.keys('*');
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Use with APIService
|
|
280
|
-
const redis = new Redis();
|
|
281
|
-
const cache = new RedisCacheBackend(redis);
|
|
282
|
-
const apiService = new APIService(cache);
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
## TypeScript Types
|
|
286
|
-
|
|
287
|
-
All services are fully typed with TypeScript:
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
import type {
|
|
291
|
-
Dye,
|
|
292
|
-
RGB,
|
|
293
|
-
HSV,
|
|
294
|
-
LAB,
|
|
295
|
-
RYB,
|
|
296
|
-
HexColor,
|
|
297
|
-
PriceData,
|
|
298
|
-
CachedData,
|
|
299
|
-
VisionType,
|
|
300
|
-
ErrorSeverity,
|
|
301
|
-
ICacheBackend,
|
|
302
|
-
HarmonyOptions,
|
|
303
|
-
HarmonyMatchingAlgorithm,
|
|
304
|
-
HarmonyColorSpace,
|
|
305
|
-
DeltaEFormula
|
|
306
|
-
} from 'xivdyetools-core';
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
## Constants
|
|
310
|
-
|
|
311
|
-
Access color theory and API configuration constants:
|
|
312
|
-
|
|
313
|
-
```typescript
|
|
314
|
-
import {
|
|
315
|
-
RGB_MAX,
|
|
316
|
-
HUE_MAX,
|
|
317
|
-
VISION_TYPES,
|
|
318
|
-
BRETTEL_MATRICES,
|
|
319
|
-
UNIVERSALIS_API_BASE,
|
|
320
|
-
API_CACHE_TTL
|
|
321
|
-
} from 'xivdyetools-core';
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## Utilities
|
|
325
|
-
|
|
326
|
-
Helper functions for common tasks:
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
import {
|
|
330
|
-
clamp,
|
|
331
|
-
lerp,
|
|
332
|
-
isValidHexColor,
|
|
333
|
-
isValidRGB,
|
|
334
|
-
retry,
|
|
335
|
-
sleep,
|
|
336
|
-
generateChecksum
|
|
337
|
-
} from 'xivdyetools-core';
|
|
338
|
-
|
|
339
|
-
// Validation
|
|
340
|
-
const isValid = isValidHexColor('#FF6B6B'); // true
|
|
341
|
-
|
|
342
|
-
// Math
|
|
343
|
-
const clamped = clamp(150, 0, 100); // 100
|
|
344
|
-
const interpolated = lerp(0, 100, 0.5); // 50
|
|
345
|
-
|
|
346
|
-
// Async utilities
|
|
347
|
-
await sleep(1000); // Wait 1 second
|
|
348
|
-
const result = await retry(() => fetchData(), 3, 1000); // Retry with backoff
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
## Use Cases
|
|
352
|
-
|
|
353
|
-
### Discord Bot
|
|
354
|
-
```typescript
|
|
355
|
-
// Implement /harmony command
|
|
356
|
-
import { DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
357
|
-
|
|
358
|
-
const dyeService = new DyeService(dyeDatabase);
|
|
359
|
-
const baseDye = dyeService.findClosestDye(userColor);
|
|
360
|
-
const harmonyDyes = dyeService.findTriadicDyes(userColor);
|
|
361
|
-
// Render color wheel, send Discord embed
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### Web App
|
|
365
|
-
```typescript
|
|
366
|
-
// Color matcher tool
|
|
367
|
-
import { DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
368
|
-
|
|
369
|
-
const dyeService = new DyeService(dyeDatabase);
|
|
370
|
-
const matchingDyes = dyeService.findDyesWithinDistance(imageColor, 50, 10);
|
|
371
|
-
// Display results in UI
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### CLI Tool
|
|
375
|
-
```typescript
|
|
376
|
-
// Color conversion utility
|
|
377
|
-
import { ColorService } from 'xivdyetools-core';
|
|
378
|
-
|
|
379
|
-
const hex = process.argv[2];
|
|
380
|
-
const rgb = ColorService.hexToRgb(hex);
|
|
381
|
-
console.log(`RGB: ${rgb.r}, ${rgb.g}, ${rgb.b}`);
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
## Requirements
|
|
385
|
-
|
|
386
|
-
- **Node.js** 18.0.0 or higher
|
|
387
|
-
- **TypeScript** 5.3 or higher (for development)
|
|
388
|
-
|
|
389
|
-
## Browser Compatibility
|
|
390
|
-
|
|
391
|
-
Works in all modern browsers with ES6 module support:
|
|
392
|
-
- Chrome/Edge 89+
|
|
393
|
-
- Firefox 88+
|
|
394
|
-
- Safari 15+
|
|
395
|
-
|
|
396
|
-
## License
|
|
397
|
-
|
|
398
|
-
MIT © 2025 Flash Galatine
|
|
399
|
-
|
|
400
|
-
See [LICENSE](./LICENSE) for full details.
|
|
401
|
-
|
|
402
|
-
## Legal Notice
|
|
403
|
-
|
|
404
|
-
**This is a fan-made tool and is not affiliated with or endorsed by Square Enix Co., Ltd. FINAL FANTASY is a registered trademark of Square Enix Holdings Co., Ltd.**
|
|
405
|
-
|
|
406
|
-
## Coming Soon
|
|
407
|
-
|
|
408
|
-
**Budget-Aware Dye Suggestions** - Find affordable alternatives to expensive dyes based on current market prices. See [specification](../xivdyetools-docs/BUDGET_AWARE_SUGGESTIONS.md) for details.
|
|
409
|
-
|
|
410
|
-
## Related Projects
|
|
411
|
-
|
|
412
|
-
- [XIV Dye Tools Web App](https://github.com/FlashGalatine/xivdyetools-web-app) - Interactive color tools for FFXIV
|
|
413
|
-
- [XIV Dye Tools Discord Worker](https://github.com/FlashGalatine/xivdyetools-discord-worker) - Cloudflare Worker Discord bot using this package
|
|
414
|
-
|
|
415
|
-
## Support
|
|
416
|
-
|
|
417
|
-
- **Issues**: [GitHub Issues](https://github.com/FlashGalatine/xivdyetools-core/issues)
|
|
418
|
-
- **NPM Package**: [xivdyetools-core](https://www.npmjs.com/package/xivdyetools-core)
|
|
419
|
-
- **Documentation**: [Full Docs](https://github.com/FlashGalatine/xivdyetools-core#readme)
|
|
420
|
-
|
|
421
|
-
## Connect With Me
|
|
422
|
-
|
|
423
|
-
**Flash Galatine** |
|
|
424
|
-
|
|
425
|
-
🎮 **FFXIV**: [Lodestone Character](https://na.finalfantasyxiv.com/lodestone/character/7677106/)
|
|
426
|
-
📝 **Blog**: [Project Galatine](https://blog.projectgalatine.com/)
|
|
427
|
-
💻 **GitHub**: [@FlashGalatine](https://github.com/FlashGalatine)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
**Made with ❤️ for the FFXIV community**
|
|
1
|
+
# xivdyetools-core
|
|
2
|
+
|
|
3
|
+
> Core color algorithms and dye database for XIV Dye Tools - Environment-agnostic TypeScript library for FFXIV dye color matching, harmony generation, and accessibility checking.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/xivdyetools-core)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
✨ **Color Conversion** - RGB ↔ HSV ↔ Hex ↔ LAB ↔ RYB ↔ OKLAB ↔ OKLCH ↔ LCH ↔ HSL
|
|
12
|
+
🎨 **136 FFXIV Dyes** - Complete database with RGB/HSV/metadata
|
|
13
|
+
🖌️ **Advanced Color Mixing** - RYB, OKLAB, HSL, and Spectral (Kubelka-Munk)
|
|
14
|
+
🔬 **Spectral.js Integration** - Physics-based paint mixing (Blue + Yellow = Green!)
|
|
15
|
+
🎯 **Dye Matching** - Find closest dyes to any color
|
|
16
|
+
🌈 **Color Harmonies** - Triadic, complementary, analogous, and more
|
|
17
|
+
🖼️ **Palette Extraction** - K-means++ clustering for multi-color extraction from images
|
|
18
|
+
♿ **Accessibility** - Colorblindness simulation (Brettel 1997)
|
|
19
|
+
📡 **Universalis API** - Price data integration with caching
|
|
20
|
+
🔌 **Pluggable Cache** - Memory, localStorage, Redis support
|
|
21
|
+
🌍 **Environment Agnostic** - Works in Node.js, browsers, edge runtimes
|
|
22
|
+
🗣️ **6 Languages** - English, Japanese, German, French, Korean, Chinese
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install xivdyetools-core
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### Browser (with bundler)
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { ColorService, DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
36
|
+
|
|
37
|
+
// Initialize services
|
|
38
|
+
const dyeService = new DyeService(dyeDatabase);
|
|
39
|
+
|
|
40
|
+
// Find closest dye to a color
|
|
41
|
+
const closestDye = dyeService.findClosestDye('#FF6B6B');
|
|
42
|
+
console.log(closestDye.name); // "Coral Pink"
|
|
43
|
+
|
|
44
|
+
// Generate color harmonies
|
|
45
|
+
const triadicDyes = dyeService.findTriadicDyes('#FF6B6B');
|
|
46
|
+
console.log(triadicDyes.map(d => d.name)); // ["Turquoise Green", "Grape Purple"]
|
|
47
|
+
|
|
48
|
+
// Color conversions
|
|
49
|
+
const rgb = ColorService.hexToRgb('#FF6B6B');
|
|
50
|
+
const hsv = ColorService.rgbToHsv(rgb.r, rgb.g, rgb.b);
|
|
51
|
+
console.log(hsv); // { h: 0, s: 58.04, v: 100 }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Node.js (Discord bot, API, CLI)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import {
|
|
58
|
+
DyeService,
|
|
59
|
+
APIService,
|
|
60
|
+
dyeDatabase
|
|
61
|
+
} from 'xivdyetools-core';
|
|
62
|
+
import Redis from 'ioredis';
|
|
63
|
+
|
|
64
|
+
// Initialize with Redis cache (for Discord bots)
|
|
65
|
+
const redis = new Redis();
|
|
66
|
+
const cacheBackend = new RedisCacheBackend(redis);
|
|
67
|
+
const apiService = new APIService(cacheBackend);
|
|
68
|
+
const dyeService = new DyeService(dyeDatabase);
|
|
69
|
+
|
|
70
|
+
// Fetch live market prices
|
|
71
|
+
const priceData = await apiService.getPriceData(5752); // Jet Black Dye
|
|
72
|
+
console.log(`${priceData.currentMinPrice} Gil`);
|
|
73
|
+
|
|
74
|
+
// Find harmony with pricing
|
|
75
|
+
const baseDye = dyeService.findClosestDye('#000000');
|
|
76
|
+
const harmonyDyes = dyeService.findComplementaryPair(baseDye.hex);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Core Services
|
|
80
|
+
|
|
81
|
+
### ColorService
|
|
82
|
+
|
|
83
|
+
Pure color conversion and manipulation algorithms.
|
|
84
|
+
|
|
85
|
+
> **Memory Note**: ColorService uses LRU caches (5 caches × 1000 entries each = up to 5000 cached entries) for performance optimization. For long-running applications or memory-constrained environments, call `ColorService.clearCaches()` periodically to free memory. Each cache entry is approximately 50-100 bytes, so maximum memory usage is ~500KB.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { ColorService } from 'xivdyetools-core';
|
|
89
|
+
|
|
90
|
+
// Hex ↔ RGB
|
|
91
|
+
const rgb = ColorService.hexToRgb('#FF6B6B');
|
|
92
|
+
const hex = ColorService.rgbToHex(255, 107, 107);
|
|
93
|
+
|
|
94
|
+
// RGB ↔ HSV
|
|
95
|
+
const hsv = ColorService.rgbToHsv(255, 107, 107);
|
|
96
|
+
const rgbFromHsv = ColorService.hsvToRgb(0, 58.04, 100);
|
|
97
|
+
|
|
98
|
+
// Colorblindness simulation
|
|
99
|
+
const simulated = ColorService.simulateColorblindness(
|
|
100
|
+
{ r: 255, g: 0, b: 0 },
|
|
101
|
+
'deuteranopia'
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Color distance (Euclidean in RGB space)
|
|
105
|
+
const distance = ColorService.getColorDistance('#FF0000', '#00FF00');
|
|
106
|
+
|
|
107
|
+
// LAB color space and DeltaE (perceptual color difference)
|
|
108
|
+
const lab = ColorService.hexToLab('#FF6B6B');
|
|
109
|
+
const deltaE = ColorService.getDeltaE('#FF0000', '#FF6B6B'); // CIE76 by default
|
|
110
|
+
const deltaE2000 = ColorService.getDeltaE('#FF0000', '#FF6B6B', 'cie2000'); // CIEDE2000
|
|
111
|
+
|
|
112
|
+
// Color inversion
|
|
113
|
+
const inverted = ColorService.invert('#FF6B6B');
|
|
114
|
+
|
|
115
|
+
// Cache management (for memory-constrained environments)
|
|
116
|
+
ColorService.clearCaches();
|
|
117
|
+
const cacheStats = ColorService.getCacheStats();
|
|
118
|
+
|
|
119
|
+
// RYB Subtractive Color Mixing (paint-like mixing)
|
|
120
|
+
// Blue + Yellow = Green (not gray like RGB!)
|
|
121
|
+
const mixed = ColorService.mixColorsRyb('#0000FF', '#FFFF00');
|
|
122
|
+
const partialMix = ColorService.mixColorsRyb('#FF0000', '#FFFF00', 0.3); // 30% yellow
|
|
123
|
+
|
|
124
|
+
// RYB ↔ RGB conversions
|
|
125
|
+
const ryb = ColorService.hexToRyb('#00FF00');
|
|
126
|
+
const rgb = ColorService.rybToRgb(0, 255, 255); // Yellow+Blue = Green
|
|
127
|
+
const hex = ColorService.rybToHex(255, 255, 0); // Red+Yellow = Orange
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### DyeService
|
|
131
|
+
|
|
132
|
+
FFXIV dye database management and color matching.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
136
|
+
|
|
137
|
+
const dyeService = new DyeService(dyeDatabase);
|
|
138
|
+
|
|
139
|
+
// Database access
|
|
140
|
+
const allDyes = dyeService.getAllDyes(); // 136 dyes
|
|
141
|
+
const dyeById = dyeService.getDyeById(5752); // By itemID - Jet Black Dye
|
|
142
|
+
const dyeByStain = dyeService.getByStainId(1); // By stainID - Snow White
|
|
143
|
+
const categories = dyeService.getCategories(); // ['Neutral', 'Red', 'Blue', ...]
|
|
144
|
+
|
|
145
|
+
// Color matching
|
|
146
|
+
const closest = dyeService.findClosestDye('#FF6B6B');
|
|
147
|
+
const nearby = dyeService.findDyesWithinDistance('#FF6B6B', 50, 5);
|
|
148
|
+
|
|
149
|
+
// Harmony generation (default: fast hue-based matching)
|
|
150
|
+
const triadic = dyeService.findTriadicDyes('#FF6B6B');
|
|
151
|
+
const complementary = dyeService.findComplementaryPair('#FF6B6B');
|
|
152
|
+
const analogous = dyeService.findAnalogousDyes('#FF6B6B', 30);
|
|
153
|
+
const monochromatic = dyeService.findMonochromaticDyes('#FF6B6B', 6);
|
|
154
|
+
const splitComplementary = dyeService.findSplitComplementaryDyes('#FF6B6B');
|
|
155
|
+
|
|
156
|
+
// DeltaE-based harmony (perceptually accurate matching)
|
|
157
|
+
const triadicDeltaE = dyeService.findTriadicDyes('#FF6B6B', {
|
|
158
|
+
algorithm: 'deltaE',
|
|
159
|
+
deltaEFormula: 'cie2000', // or 'cie76' (faster, default)
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Color space selection for hue rotation (v1.16.0+)
|
|
163
|
+
// OKLCH produces more perceptually balanced harmonies
|
|
164
|
+
const triadicOklch = dyeService.findTriadicDyes('#FF6B6B', { colorSpace: 'oklch' });
|
|
165
|
+
const compLch = dyeService.findComplementaryPair('#FF6B6B', { colorSpace: 'lch' });
|
|
166
|
+
// Available spaces: 'hsv' (default), 'oklch', 'lch', 'hsl'
|
|
167
|
+
|
|
168
|
+
// Filtering
|
|
169
|
+
const redDyes = dyeService.searchByCategory('Red');
|
|
170
|
+
const searchResults = dyeService.searchByName('black');
|
|
171
|
+
const filtered = dyeService.filterDyes({
|
|
172
|
+
category: 'Special',
|
|
173
|
+
excludeIds: [5752, 5753],
|
|
174
|
+
minPrice: 0,
|
|
175
|
+
maxPrice: 10000
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### PaletteService
|
|
180
|
+
|
|
181
|
+
Multi-color palette extraction from images using K-means++ clustering.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { PaletteService, DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
185
|
+
|
|
186
|
+
const paletteService = new PaletteService();
|
|
187
|
+
const dyeService = new DyeService(dyeDatabase);
|
|
188
|
+
|
|
189
|
+
// Extract from Canvas ImageData
|
|
190
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
191
|
+
const pixels = PaletteService.pixelDataToRGBFiltered(imageData.data);
|
|
192
|
+
|
|
193
|
+
// Extract dominant colors only
|
|
194
|
+
const palette = paletteService.extractPalette(pixels, { colorCount: 4 });
|
|
195
|
+
// Returns: Array<{ color: RGB, dominance: number }>
|
|
196
|
+
|
|
197
|
+
// Extract and match to FFXIV dyes
|
|
198
|
+
const matches = paletteService.extractAndMatchPalette(pixels, dyeService, {
|
|
199
|
+
colorCount: 4,
|
|
200
|
+
maxIterations: 25,
|
|
201
|
+
convergenceThreshold: 1.0,
|
|
202
|
+
maxSamples: 10000
|
|
203
|
+
});
|
|
204
|
+
// Returns: Array<{ extracted: RGB, matchedDye: Dye, distance: number, dominance: number }>
|
|
205
|
+
|
|
206
|
+
// Helper: Convert raw pixel buffer (RGB, 3 bytes per pixel)
|
|
207
|
+
const pixelsFromBuffer = PaletteService.pixelDataToRGB(buffer);
|
|
208
|
+
|
|
209
|
+
// Helper: Convert RGBA ImageData, filtering transparent pixels
|
|
210
|
+
const pixelsFromCanvas = PaletteService.pixelDataToRGBFiltered(imageData.data);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### APIService
|
|
214
|
+
|
|
215
|
+
Universalis API integration with pluggable cache backends.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { APIService, MemoryCacheBackend } from 'xivdyetools-core';
|
|
219
|
+
|
|
220
|
+
// With memory cache (default)
|
|
221
|
+
const apiService = new APIService();
|
|
222
|
+
|
|
223
|
+
// With custom cache backend
|
|
224
|
+
const cache = new MemoryCacheBackend();
|
|
225
|
+
const apiService = new APIService(cache);
|
|
226
|
+
|
|
227
|
+
// Fetch price data
|
|
228
|
+
const priceData = await apiService.getPriceData(5752); // itemID
|
|
229
|
+
const pricesWithDC = await apiService.getPriceData(5752, undefined, 'Aether');
|
|
230
|
+
|
|
231
|
+
// Batch operations
|
|
232
|
+
const prices = await apiService.getPricesForItems([5752, 5753, 5754]);
|
|
233
|
+
|
|
234
|
+
// Cache management
|
|
235
|
+
await apiService.clearCache();
|
|
236
|
+
const stats = await apiService.getCacheStats();
|
|
237
|
+
|
|
238
|
+
// API health check
|
|
239
|
+
const { available, latency } = await apiService.getAPIStatus();
|
|
240
|
+
|
|
241
|
+
// Utility methods
|
|
242
|
+
const formatted = APIService.formatPrice(123456); // "123,456G"
|
|
243
|
+
const trend = APIService.getPriceTrend(100, 80); // { trend: 'up', ... }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Custom Cache Backends
|
|
247
|
+
|
|
248
|
+
Implement the `ICacheBackend` interface for custom storage:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { ICacheBackend, CachedData, PriceData } from 'xivdyetools-core';
|
|
252
|
+
import Redis from 'ioredis';
|
|
253
|
+
|
|
254
|
+
class RedisCacheBackend implements ICacheBackend {
|
|
255
|
+
constructor(private redis: Redis) {}
|
|
256
|
+
|
|
257
|
+
async get(key: string): Promise<CachedData<PriceData> | null> {
|
|
258
|
+
const data = await this.redis.get(key);
|
|
259
|
+
return data ? JSON.parse(data) : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async set(key: string, value: CachedData<PriceData>): Promise<void> {
|
|
263
|
+
await this.redis.set(key, JSON.stringify(value), 'EX', value.ttl / 1000);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async delete(key: string): Promise<void> {
|
|
267
|
+
await this.redis.del(key);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async clear(): Promise<void> {
|
|
271
|
+
await this.redis.flushdb();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async keys(): Promise<string[]> {
|
|
275
|
+
return await this.redis.keys('*');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Use with APIService
|
|
280
|
+
const redis = new Redis();
|
|
281
|
+
const cache = new RedisCacheBackend(redis);
|
|
282
|
+
const apiService = new APIService(cache);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## TypeScript Types
|
|
286
|
+
|
|
287
|
+
All services are fully typed with TypeScript:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import type {
|
|
291
|
+
Dye,
|
|
292
|
+
RGB,
|
|
293
|
+
HSV,
|
|
294
|
+
LAB,
|
|
295
|
+
RYB,
|
|
296
|
+
HexColor,
|
|
297
|
+
PriceData,
|
|
298
|
+
CachedData,
|
|
299
|
+
VisionType,
|
|
300
|
+
ErrorSeverity,
|
|
301
|
+
ICacheBackend,
|
|
302
|
+
HarmonyOptions,
|
|
303
|
+
HarmonyMatchingAlgorithm,
|
|
304
|
+
HarmonyColorSpace,
|
|
305
|
+
DeltaEFormula
|
|
306
|
+
} from 'xivdyetools-core';
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Constants
|
|
310
|
+
|
|
311
|
+
Access color theory and API configuration constants:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import {
|
|
315
|
+
RGB_MAX,
|
|
316
|
+
HUE_MAX,
|
|
317
|
+
VISION_TYPES,
|
|
318
|
+
BRETTEL_MATRICES,
|
|
319
|
+
UNIVERSALIS_API_BASE,
|
|
320
|
+
API_CACHE_TTL
|
|
321
|
+
} from 'xivdyetools-core';
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Utilities
|
|
325
|
+
|
|
326
|
+
Helper functions for common tasks:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import {
|
|
330
|
+
clamp,
|
|
331
|
+
lerp,
|
|
332
|
+
isValidHexColor,
|
|
333
|
+
isValidRGB,
|
|
334
|
+
retry,
|
|
335
|
+
sleep,
|
|
336
|
+
generateChecksum
|
|
337
|
+
} from 'xivdyetools-core';
|
|
338
|
+
|
|
339
|
+
// Validation
|
|
340
|
+
const isValid = isValidHexColor('#FF6B6B'); // true
|
|
341
|
+
|
|
342
|
+
// Math
|
|
343
|
+
const clamped = clamp(150, 0, 100); // 100
|
|
344
|
+
const interpolated = lerp(0, 100, 0.5); // 50
|
|
345
|
+
|
|
346
|
+
// Async utilities
|
|
347
|
+
await sleep(1000); // Wait 1 second
|
|
348
|
+
const result = await retry(() => fetchData(), 3, 1000); // Retry with backoff
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Use Cases
|
|
352
|
+
|
|
353
|
+
### Discord Bot
|
|
354
|
+
```typescript
|
|
355
|
+
// Implement /harmony command
|
|
356
|
+
import { DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
357
|
+
|
|
358
|
+
const dyeService = new DyeService(dyeDatabase);
|
|
359
|
+
const baseDye = dyeService.findClosestDye(userColor);
|
|
360
|
+
const harmonyDyes = dyeService.findTriadicDyes(userColor);
|
|
361
|
+
// Render color wheel, send Discord embed
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Web App
|
|
365
|
+
```typescript
|
|
366
|
+
// Color matcher tool
|
|
367
|
+
import { DyeService, dyeDatabase } from 'xivdyetools-core';
|
|
368
|
+
|
|
369
|
+
const dyeService = new DyeService(dyeDatabase);
|
|
370
|
+
const matchingDyes = dyeService.findDyesWithinDistance(imageColor, 50, 10);
|
|
371
|
+
// Display results in UI
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### CLI Tool
|
|
375
|
+
```typescript
|
|
376
|
+
// Color conversion utility
|
|
377
|
+
import { ColorService } from 'xivdyetools-core';
|
|
378
|
+
|
|
379
|
+
const hex = process.argv[2];
|
|
380
|
+
const rgb = ColorService.hexToRgb(hex);
|
|
381
|
+
console.log(`RGB: ${rgb.r}, ${rgb.g}, ${rgb.b}`);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Requirements
|
|
385
|
+
|
|
386
|
+
- **Node.js** 18.0.0 or higher
|
|
387
|
+
- **TypeScript** 5.3 or higher (for development)
|
|
388
|
+
|
|
389
|
+
## Browser Compatibility
|
|
390
|
+
|
|
391
|
+
Works in all modern browsers with ES6 module support:
|
|
392
|
+
- Chrome/Edge 89+
|
|
393
|
+
- Firefox 88+
|
|
394
|
+
- Safari 15+
|
|
395
|
+
|
|
396
|
+
## License
|
|
397
|
+
|
|
398
|
+
MIT © 2025-2026 Flash Galatine
|
|
399
|
+
|
|
400
|
+
See [LICENSE](./LICENSE) for full details.
|
|
401
|
+
|
|
402
|
+
## Legal Notice
|
|
403
|
+
|
|
404
|
+
**This is a fan-made tool and is not affiliated with or endorsed by Square Enix Co., Ltd. FINAL FANTASY is a registered trademark of Square Enix Holdings Co., Ltd.**
|
|
405
|
+
|
|
406
|
+
## Coming Soon
|
|
407
|
+
|
|
408
|
+
**Budget-Aware Dye Suggestions** - Find affordable alternatives to expensive dyes based on current market prices. See [specification](../xivdyetools-docs/BUDGET_AWARE_SUGGESTIONS.md) for details.
|
|
409
|
+
|
|
410
|
+
## Related Projects
|
|
411
|
+
|
|
412
|
+
- [XIV Dye Tools Web App](https://github.com/FlashGalatine/xivdyetools-web-app) - Interactive color tools for FFXIV
|
|
413
|
+
- [XIV Dye Tools Discord Worker](https://github.com/FlashGalatine/xivdyetools-discord-worker) - Cloudflare Worker Discord bot using this package
|
|
414
|
+
|
|
415
|
+
## Support
|
|
416
|
+
|
|
417
|
+
- **Issues**: [GitHub Issues](https://github.com/FlashGalatine/xivdyetools-core/issues)
|
|
418
|
+
- **NPM Package**: [xivdyetools-core](https://www.npmjs.com/package/xivdyetools-core)
|
|
419
|
+
- **Documentation**: [Full Docs](https://github.com/FlashGalatine/xivdyetools-core#readme)
|
|
420
|
+
|
|
421
|
+
## Connect With Me
|
|
422
|
+
|
|
423
|
+
**Flash Galatine** | Midgardsormr (Aether)
|
|
424
|
+
|
|
425
|
+
🎮 **FFXIV**: [Lodestone Character](https://na.finalfantasyxiv.com/lodestone/character/7677106/)
|
|
426
|
+
📝 **Blog**: [Project Galatine](https://blog.projectgalatine.com/)
|
|
427
|
+
💻 **GitHub**: [@FlashGalatine](https://github.com/FlashGalatine)
|
|
428
|
+
📺 **Twitch**: [flashgalatine](https://www.twitch.tv/flashgalatine)
|
|
429
|
+
🌐 **BlueSky**: [projectgalatine.com](https://bsky.app/profile/projectgalatine.com)
|
|
430
|
+
❤️ **Patreon**: [ProjectGalatine](https://patreon.com/ProjectGalatine)
|
|
431
|
+
☕ **Ko-Fi**: [flashgalatine](https://ko-fi.com/flashgalatine)
|
|
432
|
+
💬 **Discord**: [Join Server](https://discord.gg/5VUSKTZCe5)
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
**Made with ❤️ for the FFXIV community**
|