meshwriter-cudu 3.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/LICENSE.md +11 -0
- package/README.md +349 -0
- package/dist/fonts/comic-sans.d.ts +1105 -0
- package/dist/fonts/helvetica.d.ts +1208 -0
- package/dist/fonts/hiruko-pro.d.ts +658 -0
- package/dist/fonts/jura.d.ts +750 -0
- package/dist/fonts/webgl-dings.d.ts +109 -0
- package/dist/index.d.ts +295 -0
- package/dist/meshwriter.cjs.js +2645 -0
- package/dist/meshwriter.cjs.js.map +1 -0
- package/dist/meshwriter.esm.js +2606 -0
- package/dist/meshwriter.esm.js.map +1 -0
- package/dist/meshwriter.min.js +2 -0
- package/dist/meshwriter.min.js.map +1 -0
- package/dist/meshwriter.umd.js +7146 -0
- package/dist/meshwriter.umd.js.map +1 -0
- package/dist/src/babylonImports.d.ts +11 -0
- package/dist/src/bakedFontLoader.d.ts +43 -0
- package/dist/src/colorContrast.d.ts +117 -0
- package/dist/src/csg.d.ts +55 -0
- package/dist/src/curves.d.ts +20 -0
- package/dist/src/fogPlugin.d.ts +32 -0
- package/dist/src/fontCompression.d.ts +12 -0
- package/dist/src/fontRegistry.d.ts +54 -0
- package/dist/src/index.d.ts +47 -0
- package/dist/src/letterMesh.d.ts +46 -0
- package/dist/src/material.d.ts +34 -0
- package/dist/src/meshSplitter.d.ts +10 -0
- package/dist/src/meshwriter.d.ts +46 -0
- package/dist/src/sps.d.ts +27 -0
- package/dist/src/umd-entry.d.ts +3 -0
- package/dist/src/utils.d.ts +12 -0
- package/dist/src/variableFontCache.d.ts +56 -0
- package/dist/src/variableFontConverter.d.ts +21 -0
- package/dist/src/variableFontLoader.d.ts +99 -0
- package/fonts/Figure1.png +0 -0
- package/fonts/LICENSE-OFL.txt +93 -0
- package/fonts/README.md +174 -0
- package/fonts/atkinson-hyperlegible-next.d.ts +8 -0
- package/fonts/atkinson-hyperlegible-next.js +6576 -0
- package/fonts/atkinson-hyperlegible.js +3668 -0
- package/fonts/baked/atkinson-hyperlegible-next-200.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-250.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-300.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-350.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-400.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-450.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-500.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-550.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-600.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-650.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-700.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-750.json +1 -0
- package/fonts/baked/atkinson-hyperlegible-next-800.json +1 -0
- package/fonts/baked/manifest.json +41 -0
- package/fonts/comic-sans.js +1532 -0
- package/fonts/helvetica.js +1695 -0
- package/fonts/hiruko-pro.js +838 -0
- package/fonts/index.js +16 -0
- package/fonts/jura.js +994 -0
- package/fonts/variable/atkinson-hyperlegible-next-variable.ttf +0 -0
- package/fonts/webgl-dings.js +113 -0
- package/package.json +76 -0
- package/src/babylonImports.js +29 -0
- package/src/bakedFontLoader.js +125 -0
- package/src/colorContrast.js +528 -0
- package/src/csg.js +220 -0
- package/src/curves.js +67 -0
- package/src/fogPlugin.js +98 -0
- package/src/fontCompression.js +141 -0
- package/src/fontRegistry.js +98 -0
- package/src/globals.d.ts +20 -0
- package/src/index.js +136 -0
- package/src/letterMesh.js +417 -0
- package/src/material.js +103 -0
- package/src/meshSplitter.js +337 -0
- package/src/meshwriter.js +303 -0
- package/src/sps.js +106 -0
- package/src/types.d.ts +551 -0
- package/src/umd-entry.js +130 -0
- package/src/utils.js +57 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Contrast Utilities for WCAG Compliance
|
|
3
|
+
* Provides color manipulation for dyslexia accessibility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Color Conversion Utilities
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert hex color string to RGB object (0-1 range)
|
|
12
|
+
* @param {string} hex - Hex color string (e.g., "#FF0000" or "FF0000")
|
|
13
|
+
* @returns {{r: number, g: number, b: number}}
|
|
14
|
+
*/
|
|
15
|
+
export function hexToRgb(hex) {
|
|
16
|
+
hex = hex.replace("#", "");
|
|
17
|
+
return {
|
|
18
|
+
r: parseInt(hex.substring(0, 2), 16) / 255,
|
|
19
|
+
g: parseInt(hex.substring(2, 4), 16) / 255,
|
|
20
|
+
b: parseInt(hex.substring(4, 6), 16) / 255
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert RGB object (0-1 range) to hex color string
|
|
26
|
+
* @param {{r: number, g: number, b: number}} rgb
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
export function rgbToHex(rgb) {
|
|
30
|
+
var r = Math.round(Math.max(0, Math.min(1, rgb.r)) * 255);
|
|
31
|
+
var g = Math.round(Math.max(0, Math.min(1, rgb.g)) * 255);
|
|
32
|
+
var b = Math.round(Math.max(0, Math.min(1, rgb.b)) * 255);
|
|
33
|
+
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert RGB to HSL
|
|
38
|
+
* @param {number} r - Red (0-1)
|
|
39
|
+
* @param {number} g - Green (0-1)
|
|
40
|
+
* @param {number} b - Blue (0-1)
|
|
41
|
+
* @returns {{h: number, s: number, l: number}} - h in degrees (0-360), s and l in 0-1
|
|
42
|
+
*/
|
|
43
|
+
export function rgbToHsl(r, g, b) {
|
|
44
|
+
var max = Math.max(r, g, b);
|
|
45
|
+
var min = Math.min(r, g, b);
|
|
46
|
+
var l = (max + min) / 2;
|
|
47
|
+
var h, s;
|
|
48
|
+
|
|
49
|
+
if (max === min) {
|
|
50
|
+
h = s = 0; // achromatic
|
|
51
|
+
} else {
|
|
52
|
+
var d = max - min;
|
|
53
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
54
|
+
|
|
55
|
+
switch (max) {
|
|
56
|
+
case r:
|
|
57
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
58
|
+
break;
|
|
59
|
+
case g:
|
|
60
|
+
h = ((b - r) / d + 2) / 6;
|
|
61
|
+
break;
|
|
62
|
+
case b:
|
|
63
|
+
h = ((r - g) / d + 4) / 6;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
h *= 360;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { h: h, s: s, l: l };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Convert HSL to RGB
|
|
74
|
+
* @param {number} h - Hue in degrees (0-360)
|
|
75
|
+
* @param {number} s - Saturation (0-1)
|
|
76
|
+
* @param {number} l - Lightness (0-1)
|
|
77
|
+
* @returns {{r: number, g: number, b: number}}
|
|
78
|
+
*/
|
|
79
|
+
export function hslToRgb(h, s, l) {
|
|
80
|
+
var r, g, b;
|
|
81
|
+
|
|
82
|
+
if (s === 0) {
|
|
83
|
+
r = g = b = l; // achromatic
|
|
84
|
+
} else {
|
|
85
|
+
function hue2rgb(p, q, t) {
|
|
86
|
+
if (t < 0) t += 1;
|
|
87
|
+
if (t > 1) t -= 1;
|
|
88
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
89
|
+
if (t < 1 / 2) return q;
|
|
90
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
91
|
+
return p;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
95
|
+
var p = 2 * l - q;
|
|
96
|
+
var hNorm = h / 360;
|
|
97
|
+
|
|
98
|
+
r = hue2rgb(p, q, hNorm + 1 / 3);
|
|
99
|
+
g = hue2rgb(p, q, hNorm);
|
|
100
|
+
b = hue2rgb(p, q, hNorm - 1 / 3);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { r: r, g: g, b: b };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================
|
|
107
|
+
// WCAG Luminance Calculations
|
|
108
|
+
// ============================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Linearize an sRGB channel value
|
|
112
|
+
* @param {number} c - Channel value (0-1)
|
|
113
|
+
* @returns {number} - Linearized value
|
|
114
|
+
*/
|
|
115
|
+
function linearize(c) {
|
|
116
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Calculate relative luminance per WCAG 2.1
|
|
121
|
+
* @param {number} r - Red (0-1)
|
|
122
|
+
* @param {number} g - Green (0-1)
|
|
123
|
+
* @param {number} b - Blue (0-1)
|
|
124
|
+
* @returns {number} - Relative luminance (0-1)
|
|
125
|
+
*/
|
|
126
|
+
export function relativeLuminance(r, g, b) {
|
|
127
|
+
var rLin = linearize(r);
|
|
128
|
+
var gLin = linearize(g);
|
|
129
|
+
var bLin = linearize(b);
|
|
130
|
+
return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Calculate WCAG contrast ratio between two luminance values
|
|
135
|
+
* @param {number} L1 - Luminance of first color (0-1)
|
|
136
|
+
* @param {number} L2 - Luminance of second color (0-1)
|
|
137
|
+
* @returns {number} - Contrast ratio (1-21)
|
|
138
|
+
*/
|
|
139
|
+
export function contrastRatio(L1, L2) {
|
|
140
|
+
var lighter = Math.max(L1, L2);
|
|
141
|
+
var darker = Math.min(L1, L2);
|
|
142
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a color is essentially gray (no saturation)
|
|
147
|
+
* @param {{r: number, g: number, b: number}} rgb
|
|
148
|
+
* @param {number} [tolerance=0.02]
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
function isGray(rgb, tolerance) {
|
|
152
|
+
tolerance = tolerance || 0.02;
|
|
153
|
+
var max = Math.max(rgb.r, rgb.g, rgb.b);
|
|
154
|
+
var min = Math.min(rgb.r, rgb.g, rgb.b);
|
|
155
|
+
return (max - min) < tolerance;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================
|
|
159
|
+
// Luminance Adjustment
|
|
160
|
+
// ============================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Adjust color to target luminance while preserving hue
|
|
164
|
+
* Uses binary search in HSL space
|
|
165
|
+
* Desaturates significantly at low lightness for better visual contrast
|
|
166
|
+
* @param {{r: number, g: number, b: number}} rgb
|
|
167
|
+
* @param {number} targetLum - Target relative luminance (0-1)
|
|
168
|
+
* @returns {{r: number, g: number, b: number}}
|
|
169
|
+
*/
|
|
170
|
+
function adjustToLuminance(rgb, targetLum) {
|
|
171
|
+
var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
172
|
+
|
|
173
|
+
// Binary search to find lightness that achieves target luminance
|
|
174
|
+
var minL = 0;
|
|
175
|
+
var maxL = 1;
|
|
176
|
+
var iterations = 20;
|
|
177
|
+
var finalL;
|
|
178
|
+
|
|
179
|
+
for (var i = 0; i < iterations; i++) {
|
|
180
|
+
var midL = (minL + maxL) / 2;
|
|
181
|
+
var testRgb = hslToRgb(hsl.h, hsl.s, midL);
|
|
182
|
+
var testLum = relativeLuminance(testRgb.r, testRgb.g, testRgb.b);
|
|
183
|
+
|
|
184
|
+
if (testLum < targetLum) {
|
|
185
|
+
minL = midL;
|
|
186
|
+
} else {
|
|
187
|
+
maxL = midL;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
finalL = (minL + maxL) / 2;
|
|
192
|
+
|
|
193
|
+
// Desaturate significantly at low lightness for better visual contrast
|
|
194
|
+
// Dark saturated colors (e.g., dark yellow) don't look distinct enough
|
|
195
|
+
// Scale saturation based on lightness: below 0.3 lightness, reduce saturation
|
|
196
|
+
var finalS = hsl.s;
|
|
197
|
+
if (finalL < 0.3) {
|
|
198
|
+
// Linear ramp: at L=0.3, keep 100% saturation; at L=0, keep 20% saturation
|
|
199
|
+
var saturationScale = 0.2 + (finalL / 0.3) * 0.8;
|
|
200
|
+
finalS = hsl.s * saturationScale;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return hslToRgb(hsl.h, finalS, finalL);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================
|
|
207
|
+
// Auto-Derive Edge Colors
|
|
208
|
+
// ============================================
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Auto-derive edge colors (diffuse/ambient) from emissive color
|
|
212
|
+
* Creates high-contrast edges for text legibility
|
|
213
|
+
*
|
|
214
|
+
* INVERTED APPROACH: Since emissive adds to all surfaces equally,
|
|
215
|
+
* we flip the strategy - put bright color in diffuse (shows on lit surfaces)
|
|
216
|
+
* and dark color in emissive (base for unlit surfaces).
|
|
217
|
+
* Returns modified emissive along with diffuse/ambient.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} emissiveHex - Hex color string for desired face color
|
|
220
|
+
* @param {number} [targetContrast=4.5] - Target WCAG contrast ratio
|
|
221
|
+
* @returns {{diffuse: string, ambient: string, emissive: string}}
|
|
222
|
+
*/
|
|
223
|
+
export function deriveEdgeColors(emissiveHex, targetContrast) {
|
|
224
|
+
targetContrast = targetContrast || 4.5;
|
|
225
|
+
|
|
226
|
+
var rgb = hexToRgb(emissiveHex);
|
|
227
|
+
var faceLum = relativeLuminance(rgb.r, rgb.g, rgb.b);
|
|
228
|
+
|
|
229
|
+
// Calculate target luminance for dark areas to achieve contrast
|
|
230
|
+
var darkLum;
|
|
231
|
+
if (faceLum > 0.5) {
|
|
232
|
+
// Bright face needs dark edges
|
|
233
|
+
darkLum = (faceLum + 0.05) / targetContrast - 0.05;
|
|
234
|
+
darkLum = Math.max(darkLum, 0.0);
|
|
235
|
+
} else {
|
|
236
|
+
// Dark face needs light edges (invert the logic)
|
|
237
|
+
darkLum = targetContrast * (faceLum + 0.05) - 0.05;
|
|
238
|
+
darkLum = Math.min(darkLum, 1.0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Handle edge cases
|
|
242
|
+
if (faceLum > 0.95) {
|
|
243
|
+
darkLum = Math.min(0.1, darkLum);
|
|
244
|
+
} else if (faceLum < 0.05) {
|
|
245
|
+
darkLum = Math.max(0.5, darkLum);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Generate dark color (desaturated at low lightness)
|
|
249
|
+
var darkRgb = adjustToLuminance(rgb, darkLum);
|
|
250
|
+
|
|
251
|
+
// INVERTED APPROACH:
|
|
252
|
+
// - diffuse = bright (the user's desired face color) - shows on lit surfaces
|
|
253
|
+
// - emissive = dark - base color for all surfaces (unlit areas show this)
|
|
254
|
+
// - ambient = very dark - shadowed areas
|
|
255
|
+
var ambientLum = darkLum * 0.5;
|
|
256
|
+
var ambientRgb = adjustToLuminance(rgb, Math.max(0, ambientLum));
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
diffuse: emissiveHex, // Bright color for lit surfaces
|
|
260
|
+
ambient: rgbToHex(ambientRgb), // Very dark for shadows
|
|
261
|
+
emissive: rgbToHex(darkRgb) // Dark base for unlit areas
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ============================================
|
|
266
|
+
// High-Contrast Adjustment Algorithm
|
|
267
|
+
// ============================================
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Adjust color by a factor (lightness change with optional hue shift)
|
|
271
|
+
* @param {{r: number, g: number, b: number}} rgb
|
|
272
|
+
* @param {number} factor - Adjustment factor (-1 to 1, negative = darken)
|
|
273
|
+
* @param {boolean} allowHueShift
|
|
274
|
+
* @returns {{r: number, g: number, b: number}}
|
|
275
|
+
*/
|
|
276
|
+
function adjustColorByFactor(rgb, factor, allowHueShift) {
|
|
277
|
+
var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
278
|
+
|
|
279
|
+
// Adjust lightness
|
|
280
|
+
var newL = hsl.l + factor;
|
|
281
|
+
newL = Math.max(0, Math.min(1, newL));
|
|
282
|
+
|
|
283
|
+
// Optionally shift hue for extreme adjustments
|
|
284
|
+
var newH = hsl.h;
|
|
285
|
+
if (allowHueShift && Math.abs(factor) > 0.2) {
|
|
286
|
+
// Slight hue shift toward yellow (high luminance) or blue (low luminance)
|
|
287
|
+
var hueTarget = factor > 0 ? 60 : 240;
|
|
288
|
+
newH = hsl.h + (hueTarget - hsl.h) * Math.abs(factor) * 0.1;
|
|
289
|
+
newH = ((newH % 360) + 360) % 360;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Reduce saturation at extreme lightness for natural look
|
|
293
|
+
var newS = hsl.s;
|
|
294
|
+
if (newL > 0.9 || newL < 0.1) {
|
|
295
|
+
newS *= 0.5;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return hslToRgb(newH, newS, newL);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Oscillate edge colors to find best contrast
|
|
303
|
+
* @param {{r: number, g: number, b: number}} emissive
|
|
304
|
+
* @param {{r: number, g: number, b: number}} diffuse
|
|
305
|
+
* @param {{r: number, g: number, b: number}} ambient
|
|
306
|
+
* @param {object} options
|
|
307
|
+
* @returns {{diffuse: object, ambient: object, achieved: number}}
|
|
308
|
+
*/
|
|
309
|
+
function oscillateEdges(emissive, diffuse, ambient, options) {
|
|
310
|
+
var emissiveLum = relativeLuminance(emissive.r, emissive.g, emissive.b);
|
|
311
|
+
var diffuseLum = relativeLuminance(diffuse.r, diffuse.g, diffuse.b);
|
|
312
|
+
var currentContrast = contrastRatio(emissiveLum, diffuseLum);
|
|
313
|
+
|
|
314
|
+
var bestResult = { diffuse: diffuse, ambient: ambient, achieved: currentContrast };
|
|
315
|
+
|
|
316
|
+
// Determine direction: edges should go opposite to emissive luminance
|
|
317
|
+
var direction = emissiveLum > 0.5 ? -1 : 1;
|
|
318
|
+
|
|
319
|
+
var steps = 10;
|
|
320
|
+
for (var i = 1; i <= steps; i++) {
|
|
321
|
+
var factor = (i / steps) * options.range;
|
|
322
|
+
|
|
323
|
+
// Primary direction
|
|
324
|
+
var testDiffuse = adjustColorByFactor(diffuse, direction * factor, options.allowHueShift);
|
|
325
|
+
var testAmbient = adjustColorByFactor(ambient, direction * factor * 0.8, options.allowHueShift);
|
|
326
|
+
|
|
327
|
+
var testLum = relativeLuminance(testDiffuse.r, testDiffuse.g, testDiffuse.b);
|
|
328
|
+
var contrast = contrastRatio(emissiveLum, testLum);
|
|
329
|
+
|
|
330
|
+
if (contrast > bestResult.achieved) {
|
|
331
|
+
bestResult = { diffuse: testDiffuse, ambient: testAmbient, achieved: contrast };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (contrast >= options.targetContrast) break;
|
|
335
|
+
|
|
336
|
+
// Try opposite direction for edge cases
|
|
337
|
+
testDiffuse = adjustColorByFactor(diffuse, -direction * factor, options.allowHueShift);
|
|
338
|
+
testAmbient = adjustColorByFactor(ambient, -direction * factor * 0.8, options.allowHueShift);
|
|
339
|
+
|
|
340
|
+
testLum = relativeLuminance(testDiffuse.r, testDiffuse.g, testDiffuse.b);
|
|
341
|
+
contrast = contrastRatio(emissiveLum, testLum);
|
|
342
|
+
|
|
343
|
+
if (contrast > bestResult.achieved) {
|
|
344
|
+
bestResult = { diffuse: testDiffuse, ambient: testAmbient, achieved: contrast };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return bestResult;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Oscillate face color to find better contrast
|
|
353
|
+
* @param {{r: number, g: number, b: number}} emissive
|
|
354
|
+
* @param {{r: number, g: number, b: number}} diffuse
|
|
355
|
+
* @param {object} options
|
|
356
|
+
* @returns {{emissive: object, achieved: number}}
|
|
357
|
+
*/
|
|
358
|
+
function oscillateFace(emissive, diffuse, options) {
|
|
359
|
+
var diffuseLum = relativeLuminance(diffuse.r, diffuse.g, diffuse.b);
|
|
360
|
+
var emissiveLum = relativeLuminance(emissive.r, emissive.g, emissive.b);
|
|
361
|
+
var currentContrast = contrastRatio(emissiveLum, diffuseLum);
|
|
362
|
+
|
|
363
|
+
var bestResult = { emissive: emissive, achieved: currentContrast };
|
|
364
|
+
|
|
365
|
+
// Face should move opposite to edges
|
|
366
|
+
var direction = diffuseLum > 0.5 ? -1 : 1;
|
|
367
|
+
|
|
368
|
+
var steps = 10;
|
|
369
|
+
for (var i = 1; i <= steps; i++) {
|
|
370
|
+
var factor = (i / steps) * options.range;
|
|
371
|
+
|
|
372
|
+
var testEmissive = adjustColorByFactor(emissive, direction * factor, options.allowHueShift);
|
|
373
|
+
var testLum = relativeLuminance(testEmissive.r, testEmissive.g, testEmissive.b);
|
|
374
|
+
var contrast = contrastRatio(testLum, diffuseLum);
|
|
375
|
+
|
|
376
|
+
if (contrast > bestResult.achieved) {
|
|
377
|
+
bestResult = { emissive: testEmissive, achieved: contrast };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (contrast >= options.targetContrast) break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return bestResult;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Adjust colors to achieve WCAG contrast while preserving user intent
|
|
388
|
+
* Priority: prefer edge modifications over face modifications
|
|
389
|
+
*
|
|
390
|
+
* @param {object} colors - User-provided colors
|
|
391
|
+
* @param {string} colors.emissive - Face color (hex)
|
|
392
|
+
* @param {string} colors.diffuse - Edge lit color (hex)
|
|
393
|
+
* @param {string} [colors.ambient] - Edge shadow color (hex)
|
|
394
|
+
* @param {object} [options]
|
|
395
|
+
* @param {number} [options.targetContrast=4.5] - Target contrast ratio
|
|
396
|
+
* @param {number} [options.edgeRange=0.4] - Max edge modification (0-1)
|
|
397
|
+
* @param {number} [options.faceRange=0.1] - Max face modification (0-1)
|
|
398
|
+
* @param {boolean} [options.allowHueShift=true] - Allow hue modifications
|
|
399
|
+
* @returns {{emissive: string, diffuse: string, ambient: string, achieved: number}}
|
|
400
|
+
*/
|
|
401
|
+
export function adjustForContrast(colors, options) {
|
|
402
|
+
options = options || {};
|
|
403
|
+
var targetContrast = options.targetContrast || 4.5;
|
|
404
|
+
var edgeRange = options.edgeRange || 0.4;
|
|
405
|
+
var faceRange = options.faceRange || 0.1;
|
|
406
|
+
var allowHueShift = options.allowHueShift !== false;
|
|
407
|
+
|
|
408
|
+
var emissive = hexToRgb(colors.emissive);
|
|
409
|
+
var diffuse = hexToRgb(colors.diffuse);
|
|
410
|
+
var ambient = colors.ambient ? hexToRgb(colors.ambient) : { r: diffuse.r * 0.5, g: diffuse.g * 0.5, b: diffuse.b * 0.5 };
|
|
411
|
+
|
|
412
|
+
var emissiveLum = relativeLuminance(emissive.r, emissive.g, emissive.b);
|
|
413
|
+
var diffuseLum = relativeLuminance(diffuse.r, diffuse.g, diffuse.b);
|
|
414
|
+
var currentContrast = contrastRatio(emissiveLum, diffuseLum);
|
|
415
|
+
|
|
416
|
+
// Already meets target?
|
|
417
|
+
if (currentContrast >= targetContrast) {
|
|
418
|
+
return {
|
|
419
|
+
emissive: colors.emissive,
|
|
420
|
+
diffuse: colors.diffuse,
|
|
421
|
+
ambient: colors.ambient || rgbToHex(ambient),
|
|
422
|
+
achieved: currentContrast
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Phase 1: Try edge modification only
|
|
427
|
+
var edgeResult = oscillateEdges(emissive, diffuse, ambient, {
|
|
428
|
+
targetContrast: targetContrast,
|
|
429
|
+
range: edgeRange,
|
|
430
|
+
allowHueShift: allowHueShift
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (edgeResult.achieved >= targetContrast) {
|
|
434
|
+
return {
|
|
435
|
+
emissive: colors.emissive,
|
|
436
|
+
diffuse: rgbToHex(edgeResult.diffuse),
|
|
437
|
+
ambient: rgbToHex(edgeResult.ambient),
|
|
438
|
+
achieved: edgeResult.achieved
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Phase 2: Try face modification
|
|
443
|
+
var faceResult = oscillateFace(emissive, edgeResult.diffuse, {
|
|
444
|
+
targetContrast: targetContrast,
|
|
445
|
+
range: faceRange,
|
|
446
|
+
allowHueShift: allowHueShift
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (faceResult.achieved >= targetContrast) {
|
|
450
|
+
return {
|
|
451
|
+
emissive: rgbToHex(faceResult.emissive),
|
|
452
|
+
diffuse: rgbToHex(edgeResult.diffuse),
|
|
453
|
+
ambient: rgbToHex(edgeResult.ambient),
|
|
454
|
+
achieved: faceResult.achieved
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Phase 3: Oscillate both until convergence
|
|
459
|
+
var maxIterations = 5;
|
|
460
|
+
var currentEmissive = faceResult.emissive;
|
|
461
|
+
var currentDiffuse = edgeResult.diffuse;
|
|
462
|
+
var currentAmbient = edgeResult.ambient;
|
|
463
|
+
var bestAchieved = faceResult.achieved;
|
|
464
|
+
|
|
465
|
+
for (var iter = 0; iter < maxIterations; iter++) {
|
|
466
|
+
// Try more edge adjustment
|
|
467
|
+
var newEdgeResult = oscillateEdges(currentEmissive, currentDiffuse, currentAmbient, {
|
|
468
|
+
targetContrast: targetContrast,
|
|
469
|
+
range: edgeRange * 0.5,
|
|
470
|
+
allowHueShift: allowHueShift
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (newEdgeResult.achieved >= targetContrast) {
|
|
474
|
+
return {
|
|
475
|
+
emissive: rgbToHex(currentEmissive),
|
|
476
|
+
diffuse: rgbToHex(newEdgeResult.diffuse),
|
|
477
|
+
ambient: rgbToHex(newEdgeResult.ambient),
|
|
478
|
+
achieved: newEdgeResult.achieved
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Try more face adjustment
|
|
483
|
+
var newFaceResult = oscillateFace(currentEmissive, newEdgeResult.diffuse, {
|
|
484
|
+
targetContrast: targetContrast,
|
|
485
|
+
range: faceRange * 0.5,
|
|
486
|
+
allowHueShift: allowHueShift
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
if (newFaceResult.achieved >= targetContrast) {
|
|
490
|
+
return {
|
|
491
|
+
emissive: rgbToHex(newFaceResult.emissive),
|
|
492
|
+
diffuse: rgbToHex(newEdgeResult.diffuse),
|
|
493
|
+
ambient: rgbToHex(newEdgeResult.ambient),
|
|
494
|
+
achieved: newFaceResult.achieved
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Update for next iteration
|
|
499
|
+
if (newFaceResult.achieved > bestAchieved) {
|
|
500
|
+
bestAchieved = newFaceResult.achieved;
|
|
501
|
+
currentEmissive = newFaceResult.emissive;
|
|
502
|
+
currentDiffuse = newEdgeResult.diffuse;
|
|
503
|
+
currentAmbient = newEdgeResult.ambient;
|
|
504
|
+
} else {
|
|
505
|
+
// No improvement, stop
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Return best result even if target not achieved
|
|
511
|
+
return {
|
|
512
|
+
emissive: rgbToHex(currentEmissive),
|
|
513
|
+
diffuse: rgbToHex(currentDiffuse),
|
|
514
|
+
ambient: rgbToHex(currentAmbient),
|
|
515
|
+
achieved: bestAchieved
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ============================================
|
|
520
|
+
// Constants
|
|
521
|
+
// ============================================
|
|
522
|
+
|
|
523
|
+
export var CONTRAST_LEVELS = {
|
|
524
|
+
AA_NORMAL: 4.5,
|
|
525
|
+
AA_LARGE: 3.0,
|
|
526
|
+
AAA_NORMAL: 7.0,
|
|
527
|
+
AAA_LARGE: 4.5
|
|
528
|
+
};
|