@xivdyetools/svg 1.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/README.md +136 -0
- package/dist/accessibility-comparison.d.ts +66 -0
- package/dist/accessibility-comparison.d.ts.map +1 -0
- package/dist/accessibility-comparison.js +201 -0
- package/dist/accessibility-comparison.js.map +1 -0
- package/dist/base.d.ts +115 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +201 -0
- package/dist/base.js.map +1 -0
- package/dist/budget-comparison.d.ts +121 -0
- package/dist/budget-comparison.d.ts.map +1 -0
- package/dist/budget-comparison.js +331 -0
- package/dist/budget-comparison.js.map +1 -0
- package/dist/comparison-grid.d.ts +61 -0
- package/dist/comparison-grid.d.ts.map +1 -0
- package/dist/comparison-grid.js +366 -0
- package/dist/comparison-grid.js.map +1 -0
- package/dist/contrast-matrix.d.ts +78 -0
- package/dist/contrast-matrix.d.ts.map +1 -0
- package/dist/contrast-matrix.js +303 -0
- package/dist/contrast-matrix.js.map +1 -0
- package/dist/dye-info-card.d.ts +34 -0
- package/dist/dye-info-card.d.ts.map +1 -0
- package/dist/dye-info-card.js +218 -0
- package/dist/dye-info-card.js.map +1 -0
- package/dist/gradient.d.ts +49 -0
- package/dist/gradient.d.ts.map +1 -0
- package/dist/gradient.js +131 -0
- package/dist/gradient.js.map +1 -0
- package/dist/harmony-wheel.d.ts +38 -0
- package/dist/harmony-wheel.d.ts.map +1 -0
- package/dist/harmony-wheel.js +158 -0
- package/dist/harmony-wheel.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/palette-grid.d.ts +105 -0
- package/dist/palette-grid.d.ts.map +1 -0
- package/dist/palette-grid.js +277 -0
- package/dist/palette-grid.js.map +1 -0
- package/dist/preset-swatch.d.ts +76 -0
- package/dist/preset-swatch.d.ts.map +1 -0
- package/dist/preset-swatch.js +226 -0
- package/dist/preset-swatch.js.map +1 -0
- package/dist/random-dyes-grid.d.ts +45 -0
- package/dist/random-dyes-grid.d.ts.map +1 -0
- package/dist/random-dyes-grid.js +143 -0
- package/dist/random-dyes-grid.js.map +1 -0
- package/package.json +56 -0
package/dist/base.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG Base Utilities
|
|
3
|
+
*
|
|
4
|
+
* Core utilities for generating SVG graphics as strings.
|
|
5
|
+
* These SVGs are later converted to PNG using a platform-specific renderer
|
|
6
|
+
* (e.g., resvg-wasm for Cloudflare Workers, @resvg/resvg-js for Node.js).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* XML-escapes a string for safe SVG inclusion
|
|
10
|
+
*/
|
|
11
|
+
export function escapeXml(str) {
|
|
12
|
+
return str
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"')
|
|
17
|
+
.replace(/'/g, ''');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Converts a hex color to RGB components
|
|
21
|
+
*/
|
|
22
|
+
export function hexToRgb(hex) {
|
|
23
|
+
const cleanHex = hex.replace('#', '');
|
|
24
|
+
return {
|
|
25
|
+
r: parseInt(cleanHex.slice(0, 2), 16),
|
|
26
|
+
g: parseInt(cleanHex.slice(2, 4), 16),
|
|
27
|
+
b: parseInt(cleanHex.slice(4, 6), 16),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Converts RGB to hex color string
|
|
32
|
+
*/
|
|
33
|
+
export function rgbToHex(r, g, b) {
|
|
34
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Calculates the luminance of a color (for contrast calculations)
|
|
38
|
+
*/
|
|
39
|
+
export function getLuminance(hex) {
|
|
40
|
+
const { r, g, b } = hexToRgb(hex);
|
|
41
|
+
// Relative luminance formula
|
|
42
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
43
|
+
const s = c / 255;
|
|
44
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
45
|
+
});
|
|
46
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Determines if text should be light or dark based on background color
|
|
50
|
+
*/
|
|
51
|
+
export function getContrastTextColor(bgHex) {
|
|
52
|
+
const luminance = getLuminance(bgHex);
|
|
53
|
+
return luminance > 0.179 ? '#000000' : '#ffffff';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates an SVG document wrapper
|
|
57
|
+
*/
|
|
58
|
+
export function createSvgDocument(width, height, content) {
|
|
59
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
60
|
+
${content}
|
|
61
|
+
</svg>`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates a rectangle element
|
|
65
|
+
*/
|
|
66
|
+
export function rect(x, y, width, height, fill, options = {}) {
|
|
67
|
+
const attrs = [
|
|
68
|
+
`x="${x}"`,
|
|
69
|
+
`y="${y}"`,
|
|
70
|
+
`width="${width}"`,
|
|
71
|
+
`height="${height}"`,
|
|
72
|
+
`fill="${fill}"`,
|
|
73
|
+
];
|
|
74
|
+
if (options.rx)
|
|
75
|
+
attrs.push(`rx="${options.rx}"`);
|
|
76
|
+
if (options.ry)
|
|
77
|
+
attrs.push(`ry="${options.ry}"`);
|
|
78
|
+
if (options.stroke)
|
|
79
|
+
attrs.push(`stroke="${options.stroke}"`);
|
|
80
|
+
if (options.strokeWidth)
|
|
81
|
+
attrs.push(`stroke-width="${options.strokeWidth}"`);
|
|
82
|
+
if (options.opacity !== undefined)
|
|
83
|
+
attrs.push(`opacity="${options.opacity}"`);
|
|
84
|
+
return `<rect ${attrs.join(' ')}/>`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Creates a circle element
|
|
88
|
+
*/
|
|
89
|
+
export function circle(cx, cy, r, fill, options = {}) {
|
|
90
|
+
const attrs = [
|
|
91
|
+
`cx="${cx}"`,
|
|
92
|
+
`cy="${cy}"`,
|
|
93
|
+
`r="${r}"`,
|
|
94
|
+
`fill="${fill}"`,
|
|
95
|
+
];
|
|
96
|
+
if (options.stroke)
|
|
97
|
+
attrs.push(`stroke="${options.stroke}"`);
|
|
98
|
+
if (options.strokeWidth)
|
|
99
|
+
attrs.push(`stroke-width="${options.strokeWidth}"`);
|
|
100
|
+
if (options.opacity !== undefined)
|
|
101
|
+
attrs.push(`opacity="${options.opacity}"`);
|
|
102
|
+
return `<circle ${attrs.join(' ')}/>`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Creates a line element
|
|
106
|
+
*/
|
|
107
|
+
export function line(x1, y1, x2, y2, stroke, strokeWidth = 1, options = {}) {
|
|
108
|
+
const attrs = [
|
|
109
|
+
`x1="${x1}"`,
|
|
110
|
+
`y1="${y1}"`,
|
|
111
|
+
`x2="${x2}"`,
|
|
112
|
+
`y2="${y2}"`,
|
|
113
|
+
`stroke="${stroke}"`,
|
|
114
|
+
`stroke-width="${strokeWidth}"`,
|
|
115
|
+
];
|
|
116
|
+
if (options.opacity !== undefined)
|
|
117
|
+
attrs.push(`opacity="${options.opacity}"`);
|
|
118
|
+
if (options.dashArray)
|
|
119
|
+
attrs.push(`stroke-dasharray="${options.dashArray}"`);
|
|
120
|
+
return `<line ${attrs.join(' ')}/>`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Creates a text element
|
|
124
|
+
*/
|
|
125
|
+
export function text(x, y, content, options = {}) {
|
|
126
|
+
const attrs = [
|
|
127
|
+
`x="${x}"`,
|
|
128
|
+
`y="${y}"`,
|
|
129
|
+
];
|
|
130
|
+
if (options.fill)
|
|
131
|
+
attrs.push(`fill="${options.fill}"`);
|
|
132
|
+
if (options.fontSize)
|
|
133
|
+
attrs.push(`font-size="${options.fontSize}"`);
|
|
134
|
+
if (options.fontFamily)
|
|
135
|
+
attrs.push(`font-family="${options.fontFamily}"`);
|
|
136
|
+
if (options.fontWeight)
|
|
137
|
+
attrs.push(`font-weight="${options.fontWeight}"`);
|
|
138
|
+
if (options.textAnchor)
|
|
139
|
+
attrs.push(`text-anchor="${options.textAnchor}"`);
|
|
140
|
+
if (options.dominantBaseline)
|
|
141
|
+
attrs.push(`dominant-baseline="${options.dominantBaseline}"`);
|
|
142
|
+
return `<text ${attrs.join(' ')}>${escapeXml(content)}</text>`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Creates an arc path for pie/donut charts
|
|
146
|
+
*/
|
|
147
|
+
export function arcPath(cx, cy, radius, startAngle, endAngle) {
|
|
148
|
+
const startRad = (startAngle - 90) * (Math.PI / 180);
|
|
149
|
+
const endRad = (endAngle - 90) * (Math.PI / 180);
|
|
150
|
+
const x1 = cx + radius * Math.cos(startRad);
|
|
151
|
+
const y1 = cy + radius * Math.sin(startRad);
|
|
152
|
+
const x2 = cx + radius * Math.cos(endRad);
|
|
153
|
+
const y2 = cy + radius * Math.sin(endRad);
|
|
154
|
+
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
|
|
155
|
+
return `M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Creates a group element
|
|
159
|
+
*/
|
|
160
|
+
export function group(content, transform) {
|
|
161
|
+
if (transform) {
|
|
162
|
+
return `<g transform="${transform}">${content}</g>`;
|
|
163
|
+
}
|
|
164
|
+
return `<g>${content}</g>`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Theme colors for consistent styling
|
|
168
|
+
*/
|
|
169
|
+
export const THEME = {
|
|
170
|
+
background: '#1a1a2e',
|
|
171
|
+
backgroundLight: '#2d2d3d',
|
|
172
|
+
text: '#ffffff',
|
|
173
|
+
textMuted: '#909090',
|
|
174
|
+
textDim: '#666666',
|
|
175
|
+
accent: '#5865f2', // Discord Blurple
|
|
176
|
+
border: '#404050',
|
|
177
|
+
success: '#57f287',
|
|
178
|
+
warning: '#fee75c',
|
|
179
|
+
error: '#ed4245',
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Font families for consistent typography.
|
|
183
|
+
* These names match the bundled font files loaded by the renderer.
|
|
184
|
+
*
|
|
185
|
+
* - header: Space Grotesk (variable 300-700) - titles, headers
|
|
186
|
+
* - primary: Onest (variable 100-900) - body text, labels
|
|
187
|
+
* - mono: Habibi (regular only) - hex codes, monospace-like text
|
|
188
|
+
* - cjk: Noto Sans SC + Noto Sans KR - Japanese, Korean, Chinese text
|
|
189
|
+
* - primaryCjk: Onest with CJK/KR fallback - for localized text that may contain CJK
|
|
190
|
+
*/
|
|
191
|
+
export const FONTS = {
|
|
192
|
+
header: 'Space Grotesk',
|
|
193
|
+
primary: 'Onest',
|
|
194
|
+
mono: 'Habibi',
|
|
195
|
+
cjk: 'Noto Sans SC, Noto Sans KR',
|
|
196
|
+
/** Use this for headings that may contain CJK characters (e.g., dye names) */
|
|
197
|
+
headerCjk: 'Space Grotesk, Noto Sans SC, Noto Sans KR',
|
|
198
|
+
/** Use this for body text that may contain CJK characters (e.g., dye names) */
|
|
199
|
+
primaryCjk: 'Onest, Noto Sans SC, Noto Sans KR',
|
|
200
|
+
};
|
|
201
|
+
//# sourceMappingURL=base.js.map
|
package/dist/base.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget Comparison SVG Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates a visual comparison of target dye vs budget alternatives.
|
|
5
|
+
* Used by the /budget command to display results.
|
|
6
|
+
*
|
|
7
|
+
* Layout:
|
|
8
|
+
* +----------------------------------------------------------+
|
|
9
|
+
* | BUDGET ALTERNATIVES FOR |
|
|
10
|
+
* | [Target Swatch] Pure White Target Price: 85,000 Gil |
|
|
11
|
+
* +----------------------------------------------------------+
|
|
12
|
+
* | [Alt Swatch] Snow White Price: 5,000 Save: 94% |
|
|
13
|
+
* | #E4DFD0 Distance: 12 (Excellent) |
|
|
14
|
+
* +----------------------------------------------------------+
|
|
15
|
+
* | [Alt Swatch] Ash Grey Price: 2,500 Save: 97% |
|
|
16
|
+
* | #8B8B8B Distance: 28 (Good) |
|
|
17
|
+
* +----------------------------------------------------------+
|
|
18
|
+
*
|
|
19
|
+
* @module svg/budget-comparison
|
|
20
|
+
*/
|
|
21
|
+
import type { Dye } from '@xivdyetools/core';
|
|
22
|
+
/**
|
|
23
|
+
* Price data for a dye on the market board.
|
|
24
|
+
* This interface is structurally compatible with the discord-worker's
|
|
25
|
+
* richer DyePriceData type — only the fields needed for SVG rendering
|
|
26
|
+
* are declared here.
|
|
27
|
+
*/
|
|
28
|
+
export interface DyePriceData {
|
|
29
|
+
currentMinPrice: number;
|
|
30
|
+
world: string;
|
|
31
|
+
listingCount: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A budget alternative suggestion.
|
|
35
|
+
*/
|
|
36
|
+
export interface BudgetSuggestion {
|
|
37
|
+
dye: Dye;
|
|
38
|
+
price: DyePriceData | null;
|
|
39
|
+
colorDistance: number;
|
|
40
|
+
savings: number;
|
|
41
|
+
savingsPercent: number;
|
|
42
|
+
valueScore: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sort option for budget results.
|
|
46
|
+
*/
|
|
47
|
+
export type BudgetSortOption = 'price' | 'color_match' | 'value_score';
|
|
48
|
+
/**
|
|
49
|
+
* Format a Gil amount with locale-appropriate separators.
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatGil(amount: number): string;
|
|
52
|
+
/**
|
|
53
|
+
* Translated labels for the budget comparison SVG
|
|
54
|
+
*
|
|
55
|
+
* Pre-resolved strings are ready to render as-is.
|
|
56
|
+
* Template strings use {var} placeholders for per-row interpolation.
|
|
57
|
+
*/
|
|
58
|
+
export interface BudgetSvgLabels {
|
|
59
|
+
/** Header title (e.g., "BUDGET ALTERNATIVES FOR") */
|
|
60
|
+
headerLabel: string;
|
|
61
|
+
/** Label above target price (e.g., "Target Price") */
|
|
62
|
+
targetPriceLabel: string;
|
|
63
|
+
/** No listings available (e.g., "No listings") */
|
|
64
|
+
noListings: string;
|
|
65
|
+
/** No alternatives found (e.g., "No cheaper alternatives found") */
|
|
66
|
+
noAlternatives: string;
|
|
67
|
+
/** Pre-resolved sort indicator (e.g., "Sorted by: Best Value") */
|
|
68
|
+
sortedBy: string;
|
|
69
|
+
/** Pre-resolved world subtitle (e.g., "on Aether") */
|
|
70
|
+
onWorld: string;
|
|
71
|
+
/** Template: "{amount} Gil" */
|
|
72
|
+
gilAmountTemplate: string;
|
|
73
|
+
/** Template: "Save {amount} ({percent}%)" */
|
|
74
|
+
saveAmountTemplate: string;
|
|
75
|
+
/** Template: "{count} listings" */
|
|
76
|
+
listingCountTemplate: string;
|
|
77
|
+
/** Translated distance quality labels */
|
|
78
|
+
distanceQuality: {
|
|
79
|
+
perfect: string;
|
|
80
|
+
excellent: string;
|
|
81
|
+
good: string;
|
|
82
|
+
fair: string;
|
|
83
|
+
approximate: string;
|
|
84
|
+
};
|
|
85
|
+
/** Localized dye names keyed by itemID (falls back to Dye.name if missing) */
|
|
86
|
+
dyeNames: Record<number, string>;
|
|
87
|
+
/** Localized category names keyed by English category (falls back to raw category if missing) */
|
|
88
|
+
categoryNames: Record<string, string>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Options for generating the budget comparison SVG
|
|
92
|
+
*/
|
|
93
|
+
export interface BudgetComparisonOptions {
|
|
94
|
+
/** The target dye (expensive one) */
|
|
95
|
+
targetDye: Dye;
|
|
96
|
+
/** Target dye's price (null if no listings) */
|
|
97
|
+
targetPrice: DyePriceData | null;
|
|
98
|
+
/** Alternative dye suggestions */
|
|
99
|
+
alternatives: BudgetSuggestion[];
|
|
100
|
+
/** World/datacenter used for prices */
|
|
101
|
+
world: string;
|
|
102
|
+
/** How results are sorted */
|
|
103
|
+
sortBy: BudgetSortOption;
|
|
104
|
+
/** Translated labels for all text in the SVG */
|
|
105
|
+
labels: BudgetSvgLabels;
|
|
106
|
+
/** Canvas width in pixels (default: 800) */
|
|
107
|
+
width?: number;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Generate a budget comparison SVG showing target vs alternatives
|
|
111
|
+
*/
|
|
112
|
+
export declare function generateBudgetComparison(options: BudgetComparisonOptions): string;
|
|
113
|
+
/**
|
|
114
|
+
* Generate an empty state SVG when no world is set
|
|
115
|
+
*/
|
|
116
|
+
export declare function generateNoWorldSetSvg(width?: number): string;
|
|
117
|
+
/**
|
|
118
|
+
* Generate an error state SVG
|
|
119
|
+
*/
|
|
120
|
+
export declare function generateErrorSvg(message: string, width?: number): string;
|
|
121
|
+
//# sourceMappingURL=budget-comparison.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget-comparison.d.ts","sourceRoot":"","sources":["../src/budget-comparison.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAgB7C;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,GAAG,CAAC;IACT,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC;AAEvE;;GAEG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEhD;AAMD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,CAAC;IACzB,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,6CAA6C;IAC7C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mCAAmC;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,yCAAyC;IACzC,eAAe,EAAE;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,iGAAiG;IACjG,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,qCAAqC;IACrC,SAAS,EAAE,GAAG,CAAC;IACf,+CAA+C;IAC/C,WAAW,EAAE,YAAY,GAAG,IAAI,CAAC;IACjC,kCAAkC;IAClC,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,MAAM,EAAE,gBAAgB,CAAC;IACzB,gDAAgD;IAChD,MAAM,EAAE,eAAe,CAAC;IACxB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyCD;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CA4DjF;AA0PD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,CAmC3E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAsB,GAAG,MAAM,CA0BvF"}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget Comparison SVG Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates a visual comparison of target dye vs budget alternatives.
|
|
5
|
+
* Used by the /budget command to display results.
|
|
6
|
+
*
|
|
7
|
+
* Layout:
|
|
8
|
+
* +----------------------------------------------------------+
|
|
9
|
+
* | BUDGET ALTERNATIVES FOR |
|
|
10
|
+
* | [Target Swatch] Pure White Target Price: 85,000 Gil |
|
|
11
|
+
* +----------------------------------------------------------+
|
|
12
|
+
* | [Alt Swatch] Snow White Price: 5,000 Save: 94% |
|
|
13
|
+
* | #E4DFD0 Distance: 12 (Excellent) |
|
|
14
|
+
* +----------------------------------------------------------+
|
|
15
|
+
* | [Alt Swatch] Ash Grey Price: 2,500 Save: 97% |
|
|
16
|
+
* | #8B8B8B Distance: 28 (Good) |
|
|
17
|
+
* +----------------------------------------------------------+
|
|
18
|
+
*
|
|
19
|
+
* @module svg/budget-comparison
|
|
20
|
+
*/
|
|
21
|
+
import { createSvgDocument, rect, text, line, THEME, FONTS, escapeXml, getContrastTextColor, } from './base.js';
|
|
22
|
+
/**
|
|
23
|
+
* Format a Gil amount with locale-appropriate separators.
|
|
24
|
+
*/
|
|
25
|
+
export function formatGil(amount) {
|
|
26
|
+
return amount.toLocaleString('en-US');
|
|
27
|
+
}
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Constants
|
|
30
|
+
// ============================================================================
|
|
31
|
+
const DEFAULT_WIDTH = 800;
|
|
32
|
+
const PADDING = 24;
|
|
33
|
+
const HEADER_HEIGHT = 120;
|
|
34
|
+
const ROW_HEIGHT = 90;
|
|
35
|
+
const SWATCH_SIZE = 56;
|
|
36
|
+
const TARGET_SWATCH_SIZE = 72;
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Helpers
|
|
39
|
+
// ============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Fill a template string with variable values
|
|
42
|
+
*
|
|
43
|
+
* @example fillTemplate("{amount} Gil", { amount: "5,000" }) → "5,000 Gil"
|
|
44
|
+
*/
|
|
45
|
+
function fillTemplate(template, vars) {
|
|
46
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? ''));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Map a color distance value to a quality tier key
|
|
50
|
+
*/
|
|
51
|
+
function getDistanceQualityKey(distance) {
|
|
52
|
+
if (distance === 0)
|
|
53
|
+
return 'perfect';
|
|
54
|
+
if (distance < 10)
|
|
55
|
+
return 'excellent';
|
|
56
|
+
if (distance < 25)
|
|
57
|
+
return 'good';
|
|
58
|
+
if (distance < 50)
|
|
59
|
+
return 'fair';
|
|
60
|
+
return 'approximate';
|
|
61
|
+
}
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// SVG Generation
|
|
64
|
+
// ============================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Generate a budget comparison SVG showing target vs alternatives
|
|
67
|
+
*/
|
|
68
|
+
export function generateBudgetComparison(options) {
|
|
69
|
+
const { targetDye, targetPrice, alternatives, labels, width = DEFAULT_WIDTH, } = options;
|
|
70
|
+
// Calculate dimensions
|
|
71
|
+
const hasAlternatives = alternatives.length > 0;
|
|
72
|
+
const contentHeight = hasAlternatives ? alternatives.length * ROW_HEIGHT : 60;
|
|
73
|
+
const height = PADDING * 2 + HEADER_HEIGHT + contentHeight;
|
|
74
|
+
const elements = [];
|
|
75
|
+
// Background
|
|
76
|
+
elements.push(rect(0, 0, width, height, THEME.background, { rx: 12, ry: 12 }));
|
|
77
|
+
// Header section
|
|
78
|
+
elements.push(generateHeader(targetDye, targetPrice, labels, width));
|
|
79
|
+
// Separator after header
|
|
80
|
+
elements.push(line(PADDING, HEADER_HEIGHT, width - PADDING, HEADER_HEIGHT, THEME.border, 2));
|
|
81
|
+
// Alternatives section
|
|
82
|
+
if (hasAlternatives) {
|
|
83
|
+
alternatives.forEach((alt, index) => {
|
|
84
|
+
const rowY = HEADER_HEIGHT + index * ROW_HEIGHT;
|
|
85
|
+
elements.push(generateAlternativeRow(alt, PADDING, rowY, width - PADDING * 2, targetPrice, labels));
|
|
86
|
+
// Separator line (except after last row)
|
|
87
|
+
if (index < alternatives.length - 1) {
|
|
88
|
+
elements.push(line(PADDING + 20, rowY + ROW_HEIGHT, width - PADDING - 20, rowY + ROW_HEIGHT, THEME.border, 1));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// No alternatives found message
|
|
94
|
+
elements.push(text(width / 2, HEADER_HEIGHT + 30, labels.noAlternatives, {
|
|
95
|
+
fill: THEME.textMuted,
|
|
96
|
+
fontSize: 16,
|
|
97
|
+
fontFamily: FONTS.primaryCjk,
|
|
98
|
+
textAnchor: 'middle',
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
return createSvgDocument(width, height, elements.join('\n'));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Generate the header section with target dye info
|
|
105
|
+
*/
|
|
106
|
+
function generateHeader(targetDye, targetPrice, labels, width) {
|
|
107
|
+
const elements = [];
|
|
108
|
+
// Title
|
|
109
|
+
elements.push(text(PADDING, 35, labels.headerLabel, {
|
|
110
|
+
fill: THEME.textMuted,
|
|
111
|
+
fontSize: 12,
|
|
112
|
+
fontFamily: FONTS.primaryCjk,
|
|
113
|
+
fontWeight: 500,
|
|
114
|
+
}));
|
|
115
|
+
// Target dye swatch
|
|
116
|
+
const swatchY = 50;
|
|
117
|
+
elements.push(rect(PADDING, swatchY, TARGET_SWATCH_SIZE, TARGET_SWATCH_SIZE, targetDye.hex, {
|
|
118
|
+
rx: 8,
|
|
119
|
+
ry: 8,
|
|
120
|
+
stroke: THEME.accent,
|
|
121
|
+
strokeWidth: 3,
|
|
122
|
+
}));
|
|
123
|
+
// Hex value on swatch
|
|
124
|
+
const swatchTextColor = getContrastTextColor(targetDye.hex);
|
|
125
|
+
elements.push(text(PADDING + TARGET_SWATCH_SIZE / 2, swatchY + TARGET_SWATCH_SIZE - 8, targetDye.hex.toUpperCase(), {
|
|
126
|
+
fill: swatchTextColor,
|
|
127
|
+
fontSize: 10,
|
|
128
|
+
fontFamily: FONTS.mono,
|
|
129
|
+
textAnchor: 'middle',
|
|
130
|
+
}));
|
|
131
|
+
// Target dye name (localized)
|
|
132
|
+
const infoX = PADDING + TARGET_SWATCH_SIZE + 16;
|
|
133
|
+
const targetName = labels.dyeNames[targetDye.itemID] ?? targetDye.name;
|
|
134
|
+
elements.push(text(infoX, 70, escapeXml(targetName), {
|
|
135
|
+
fill: THEME.text,
|
|
136
|
+
fontSize: 24,
|
|
137
|
+
fontFamily: FONTS.headerCjk,
|
|
138
|
+
fontWeight: 600,
|
|
139
|
+
}));
|
|
140
|
+
// Category (localized)
|
|
141
|
+
const targetCategory = labels.categoryNames[targetDye.category] ?? targetDye.category;
|
|
142
|
+
elements.push(text(infoX, 92, escapeXml(targetCategory), {
|
|
143
|
+
fill: THEME.textMuted,
|
|
144
|
+
fontSize: 14,
|
|
145
|
+
fontFamily: FONTS.primaryCjk,
|
|
146
|
+
}));
|
|
147
|
+
// Price section (right side)
|
|
148
|
+
const priceX = width - PADDING;
|
|
149
|
+
if (targetPrice) {
|
|
150
|
+
elements.push(text(priceX, 55, labels.targetPriceLabel, {
|
|
151
|
+
fill: THEME.textMuted,
|
|
152
|
+
fontSize: 10,
|
|
153
|
+
fontFamily: FONTS.primaryCjk,
|
|
154
|
+
fontWeight: 500,
|
|
155
|
+
textAnchor: 'end',
|
|
156
|
+
}));
|
|
157
|
+
elements.push(text(priceX, 80, fillTemplate(labels.gilAmountTemplate, { amount: formatGil(targetPrice.currentMinPrice) }), {
|
|
158
|
+
fill: THEME.warning,
|
|
159
|
+
fontSize: 22,
|
|
160
|
+
fontFamily: FONTS.header,
|
|
161
|
+
fontWeight: 600,
|
|
162
|
+
textAnchor: 'end',
|
|
163
|
+
}));
|
|
164
|
+
elements.push(text(priceX, 100, labels.onWorld, {
|
|
165
|
+
fill: THEME.textDim,
|
|
166
|
+
fontSize: 12,
|
|
167
|
+
fontFamily: FONTS.primaryCjk,
|
|
168
|
+
textAnchor: 'end',
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
elements.push(text(priceX, 70, labels.noListings, {
|
|
173
|
+
fill: THEME.textMuted,
|
|
174
|
+
fontSize: 16,
|
|
175
|
+
fontFamily: FONTS.primaryCjk,
|
|
176
|
+
textAnchor: 'end',
|
|
177
|
+
}));
|
|
178
|
+
elements.push(text(priceX, 92, labels.onWorld, {
|
|
179
|
+
fill: THEME.textDim,
|
|
180
|
+
fontSize: 12,
|
|
181
|
+
fontFamily: FONTS.primaryCjk,
|
|
182
|
+
textAnchor: 'end',
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
// Sort indicator
|
|
186
|
+
elements.push(text(width / 2, HEADER_HEIGHT - 8, labels.sortedBy, {
|
|
187
|
+
fill: THEME.textDim,
|
|
188
|
+
fontSize: 11,
|
|
189
|
+
fontFamily: FONTS.primaryCjk,
|
|
190
|
+
textAnchor: 'middle',
|
|
191
|
+
}));
|
|
192
|
+
return elements.join('\n');
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Generate a row for an alternative dye
|
|
196
|
+
*/
|
|
197
|
+
function generateAlternativeRow(alt, x, y, width, targetPrice, labels) {
|
|
198
|
+
const elements = [];
|
|
199
|
+
const rowPadding = 12;
|
|
200
|
+
// Row background
|
|
201
|
+
elements.push(rect(x, y + 6, width, ROW_HEIGHT - 12, THEME.backgroundLight, {
|
|
202
|
+
rx: 8,
|
|
203
|
+
ry: 8,
|
|
204
|
+
opacity: 0.5,
|
|
205
|
+
}));
|
|
206
|
+
// Dye swatch
|
|
207
|
+
const swatchX = x + rowPadding;
|
|
208
|
+
const swatchY = y + (ROW_HEIGHT - SWATCH_SIZE) / 2;
|
|
209
|
+
elements.push(rect(swatchX, swatchY, SWATCH_SIZE, SWATCH_SIZE, alt.dye.hex, {
|
|
210
|
+
rx: 6,
|
|
211
|
+
ry: 6,
|
|
212
|
+
stroke: THEME.border,
|
|
213
|
+
strokeWidth: 2,
|
|
214
|
+
}));
|
|
215
|
+
// Dye name (localized) and hex
|
|
216
|
+
const infoX = swatchX + SWATCH_SIZE + 14;
|
|
217
|
+
const altName = labels.dyeNames[alt.dye.itemID] ?? alt.dye.name;
|
|
218
|
+
elements.push(text(infoX, y + 35, escapeXml(altName), {
|
|
219
|
+
fill: THEME.text,
|
|
220
|
+
fontSize: 16,
|
|
221
|
+
fontFamily: FONTS.primaryCjk,
|
|
222
|
+
fontWeight: 600,
|
|
223
|
+
}));
|
|
224
|
+
elements.push(text(infoX, y + 55, alt.dye.hex.toUpperCase(), {
|
|
225
|
+
fill: THEME.textDim,
|
|
226
|
+
fontSize: 12,
|
|
227
|
+
fontFamily: FONTS.mono,
|
|
228
|
+
}));
|
|
229
|
+
// Color distance badge (no emoji/Δ — fonts lack those glyphs in resvg)
|
|
230
|
+
const qualityKey = getDistanceQualityKey(alt.colorDistance);
|
|
231
|
+
const qualityLabel = labels.distanceQuality[qualityKey];
|
|
232
|
+
const distanceText = `${qualityLabel} (dE ${alt.colorDistance.toFixed(1)})`;
|
|
233
|
+
elements.push(text(infoX, y + 75, distanceText, {
|
|
234
|
+
fill: THEME.textMuted,
|
|
235
|
+
fontSize: 11,
|
|
236
|
+
fontFamily: FONTS.primaryCjk,
|
|
237
|
+
}));
|
|
238
|
+
// Price section (right side)
|
|
239
|
+
const priceX = x + width - rowPadding;
|
|
240
|
+
if (alt.price) {
|
|
241
|
+
// Price
|
|
242
|
+
elements.push(text(priceX, y + 35, fillTemplate(labels.gilAmountTemplate, { amount: formatGil(alt.price.currentMinPrice) }), {
|
|
243
|
+
fill: THEME.success,
|
|
244
|
+
fontSize: 18,
|
|
245
|
+
fontFamily: FONTS.header,
|
|
246
|
+
fontWeight: 600,
|
|
247
|
+
textAnchor: 'end',
|
|
248
|
+
}));
|
|
249
|
+
// Savings
|
|
250
|
+
if (targetPrice && alt.savings > 0) {
|
|
251
|
+
const savingsText = fillTemplate(labels.saveAmountTemplate, {
|
|
252
|
+
amount: formatGil(alt.savings),
|
|
253
|
+
percent: alt.savingsPercent.toFixed(0),
|
|
254
|
+
});
|
|
255
|
+
elements.push(text(priceX, y + 55, savingsText, {
|
|
256
|
+
fill: THEME.accent,
|
|
257
|
+
fontSize: 13,
|
|
258
|
+
fontFamily: FONTS.primaryCjk,
|
|
259
|
+
fontWeight: 500,
|
|
260
|
+
textAnchor: 'end',
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
// Listings count
|
|
264
|
+
elements.push(text(priceX, y + 75, fillTemplate(labels.listingCountTemplate, { count: alt.price.listingCount }), {
|
|
265
|
+
fill: THEME.textDim,
|
|
266
|
+
fontSize: 11,
|
|
267
|
+
fontFamily: FONTS.primaryCjk,
|
|
268
|
+
textAnchor: 'end',
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
elements.push(text(priceX, y + 50, labels.noListings, {
|
|
273
|
+
fill: THEME.textMuted,
|
|
274
|
+
fontSize: 14,
|
|
275
|
+
fontFamily: FONTS.primaryCjk,
|
|
276
|
+
textAnchor: 'end',
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
return elements.join('\n');
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Generate an empty state SVG when no world is set
|
|
283
|
+
*/
|
|
284
|
+
export function generateNoWorldSetSvg(width = DEFAULT_WIDTH) {
|
|
285
|
+
const height = 160;
|
|
286
|
+
const elements = [];
|
|
287
|
+
elements.push(rect(0, 0, width, height, THEME.background, { rx: 12, ry: 12 }));
|
|
288
|
+
elements.push(text(width / 2, 60, 'No World Set', {
|
|
289
|
+
fill: THEME.warning,
|
|
290
|
+
fontSize: 24,
|
|
291
|
+
fontFamily: FONTS.header,
|
|
292
|
+
fontWeight: 600,
|
|
293
|
+
textAnchor: 'middle',
|
|
294
|
+
}));
|
|
295
|
+
elements.push(text(width / 2, 95, 'Use /budget set_world to set your preferred', {
|
|
296
|
+
fill: THEME.textMuted,
|
|
297
|
+
fontSize: 14,
|
|
298
|
+
fontFamily: FONTS.primary,
|
|
299
|
+
textAnchor: 'middle',
|
|
300
|
+
}));
|
|
301
|
+
elements.push(text(width / 2, 115, 'world or datacenter for price lookups.', {
|
|
302
|
+
fill: THEME.textMuted,
|
|
303
|
+
fontSize: 14,
|
|
304
|
+
fontFamily: FONTS.primary,
|
|
305
|
+
textAnchor: 'middle',
|
|
306
|
+
}));
|
|
307
|
+
return createSvgDocument(width, height, elements.join('\n'));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Generate an error state SVG
|
|
311
|
+
*/
|
|
312
|
+
export function generateErrorSvg(message, width = DEFAULT_WIDTH) {
|
|
313
|
+
const height = 120;
|
|
314
|
+
const elements = [];
|
|
315
|
+
elements.push(rect(0, 0, width, height, THEME.background, { rx: 12, ry: 12 }));
|
|
316
|
+
elements.push(text(width / 2, 50, 'Error', {
|
|
317
|
+
fill: THEME.error,
|
|
318
|
+
fontSize: 20,
|
|
319
|
+
fontFamily: FONTS.header,
|
|
320
|
+
fontWeight: 600,
|
|
321
|
+
textAnchor: 'middle',
|
|
322
|
+
}));
|
|
323
|
+
elements.push(text(width / 2, 80, escapeXml(message), {
|
|
324
|
+
fill: THEME.textMuted,
|
|
325
|
+
fontSize: 14,
|
|
326
|
+
fontFamily: FONTS.primary,
|
|
327
|
+
textAnchor: 'middle',
|
|
328
|
+
}));
|
|
329
|
+
return createSvgDocument(width, height, elements.join('\n'));
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=budget-comparison.js.map
|