@trackunit/react-map-color-utils 0.0.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/index.cjs.js +139 -0
- package/index.d.ts +1 -0
- package/index.esm.js +133 -0
- package/package.json +12 -0
- package/src/colorUtils.d.ts +45 -0
- package/src/index.d.ts +1 -0
package/index.cjs.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser-backed color mixing via CSS `color-mix()`.
|
|
5
|
+
*
|
|
6
|
+
* Uses a hidden DOM element + `getComputedStyle` to resolve expressions,
|
|
7
|
+
* cached so each unique expression is computed at most once.
|
|
8
|
+
*/
|
|
9
|
+
const colorCache = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Modern browsers return CSS Color Level 4 `color(srgb r g b)` from
|
|
12
|
+
* `getComputedStyle`, but Mapbox GL JS only accepts classic formats
|
|
13
|
+
* (hex, rgb, rgba, hsl, hsla). Convert to `rgb()`/`rgba()`.
|
|
14
|
+
*/
|
|
15
|
+
const normalizeComputedColor = (color) => {
|
|
16
|
+
const m = color.match(/^color\(srgb\s+([\d.e+-]+)\s+([\d.e+-]+)\s+([\d.e+-]+)(?:\s*\/\s*([\d.e+-]+))?\)$/);
|
|
17
|
+
if (!m)
|
|
18
|
+
return color;
|
|
19
|
+
const r = Math.round(parseFloat(m[1] ?? "0") * 255);
|
|
20
|
+
const g = Math.round(parseFloat(m[2] ?? "0") * 255);
|
|
21
|
+
const b = Math.round(parseFloat(m[3] ?? "0") * 255);
|
|
22
|
+
if (m[4] !== undefined) {
|
|
23
|
+
return `rgba(${r}, ${g}, ${b}, ${parseFloat(m[4])})`;
|
|
24
|
+
}
|
|
25
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
26
|
+
};
|
|
27
|
+
let probeElement = null;
|
|
28
|
+
const getProbeElement = () => {
|
|
29
|
+
if (probeElement) {
|
|
30
|
+
return probeElement;
|
|
31
|
+
}
|
|
32
|
+
if (typeof document === "undefined") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const el = document.createElement("span");
|
|
36
|
+
el.style.display = "none";
|
|
37
|
+
document.body.appendChild(el);
|
|
38
|
+
probeElement = el;
|
|
39
|
+
return el;
|
|
40
|
+
};
|
|
41
|
+
const resolveColorMix = (expression, fallback) => {
|
|
42
|
+
const cached = colorCache.get(expression);
|
|
43
|
+
if (cached !== undefined) {
|
|
44
|
+
return cached;
|
|
45
|
+
}
|
|
46
|
+
const el = getProbeElement();
|
|
47
|
+
if (!el) {
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
el.style.color = "";
|
|
51
|
+
el.style.color = expression;
|
|
52
|
+
if (!el.style.color) {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
const computed = getComputedStyle(el).color;
|
|
56
|
+
if (!computed) {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
const normalized = normalizeComputedColor(computed);
|
|
60
|
+
colorCache.set(expression, normalized);
|
|
61
|
+
return normalized;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Mix two CSS colors using the browser's `color-mix(in srgb)` function.
|
|
65
|
+
*
|
|
66
|
+
* @param color1 - First color (any valid CSS color string)
|
|
67
|
+
* @param color2 - Second color (any valid CSS color string)
|
|
68
|
+
* @param percentage - Percentage of `color1` in the mix (0–100)
|
|
69
|
+
* @returns {string} Resolved color as an `rgb()` string, or `color1` if resolution fails
|
|
70
|
+
*/
|
|
71
|
+
const mixColor = (color1, color2, percentage) => {
|
|
72
|
+
const expression = `color-mix(in srgb, ${color1} ${percentage}%, ${color2})`;
|
|
73
|
+
return resolveColorMix(expression, color1);
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Darken a CSS color by mixing it with black.
|
|
77
|
+
*
|
|
78
|
+
* @param color - Any valid CSS color string
|
|
79
|
+
* @param amount - Darkening intensity from 0 (no change) to 100 (pure black)
|
|
80
|
+
*/
|
|
81
|
+
const darkenColor = (color, amount) => mixColor(color, "black", 100 - amount);
|
|
82
|
+
/**
|
|
83
|
+
* Lighten a CSS color by mixing it with white.
|
|
84
|
+
*
|
|
85
|
+
* @param color - Any valid CSS color string
|
|
86
|
+
* @param amount - Lightening intensity from 0 (no change) to 100 (pure white)
|
|
87
|
+
*/
|
|
88
|
+
const lightenColor = (color, amount) => mixColor(color, "white", 100 - amount);
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a CSS color and apply an opacity multiplier to its alpha channel.
|
|
91
|
+
*
|
|
92
|
+
* Unlike setting `element.style.opacity`, this only affects the individual
|
|
93
|
+
* color value — useful when fill and stroke need independent opacity.
|
|
94
|
+
*
|
|
95
|
+
* @param color - Any valid CSS color string
|
|
96
|
+
* @param opacity - Opacity multiplier from 0 to 1 (multiplied with existing alpha)
|
|
97
|
+
* @returns {string} `rgba()` string with the combined alpha, or the original color if resolution fails
|
|
98
|
+
*/
|
|
99
|
+
const colorWithOpacity = (color, opacity) => {
|
|
100
|
+
if (opacity >= 1)
|
|
101
|
+
return color;
|
|
102
|
+
const cacheKey = `${color}@${opacity}`;
|
|
103
|
+
const cached = colorCache.get(cacheKey);
|
|
104
|
+
if (cached !== undefined)
|
|
105
|
+
return cached;
|
|
106
|
+
const el = getProbeElement();
|
|
107
|
+
if (!el)
|
|
108
|
+
return color;
|
|
109
|
+
el.style.color = "";
|
|
110
|
+
el.style.color = color;
|
|
111
|
+
const computed = getComputedStyle(el).color;
|
|
112
|
+
if (!computed)
|
|
113
|
+
return color;
|
|
114
|
+
const normalized = normalizeComputedColor(computed);
|
|
115
|
+
const rgbaMatch = normalized.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/);
|
|
116
|
+
if (!rgbaMatch)
|
|
117
|
+
return color;
|
|
118
|
+
const existingAlpha = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1;
|
|
119
|
+
const result = `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, ${existingAlpha * opacity})`;
|
|
120
|
+
colorCache.set(cacheKey, result);
|
|
121
|
+
return result;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Clear the resolved-color cache and detach the probe element.
|
|
125
|
+
* Exposed for test teardown only — not part of the public API.
|
|
126
|
+
*/
|
|
127
|
+
const resetColorUtilsForTesting = () => {
|
|
128
|
+
colorCache.clear();
|
|
129
|
+
if (probeElement) {
|
|
130
|
+
probeElement.remove();
|
|
131
|
+
probeElement = null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
exports.colorWithOpacity = colorWithOpacity;
|
|
136
|
+
exports.darkenColor = darkenColor;
|
|
137
|
+
exports.lightenColor = lightenColor;
|
|
138
|
+
exports.mixColor = mixColor;
|
|
139
|
+
exports.resetColorUtilsForTesting = resetColorUtilsForTesting;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index";
|
package/index.esm.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-backed color mixing via CSS `color-mix()`.
|
|
3
|
+
*
|
|
4
|
+
* Uses a hidden DOM element + `getComputedStyle` to resolve expressions,
|
|
5
|
+
* cached so each unique expression is computed at most once.
|
|
6
|
+
*/
|
|
7
|
+
const colorCache = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Modern browsers return CSS Color Level 4 `color(srgb r g b)` from
|
|
10
|
+
* `getComputedStyle`, but Mapbox GL JS only accepts classic formats
|
|
11
|
+
* (hex, rgb, rgba, hsl, hsla). Convert to `rgb()`/`rgba()`.
|
|
12
|
+
*/
|
|
13
|
+
const normalizeComputedColor = (color) => {
|
|
14
|
+
const m = color.match(/^color\(srgb\s+([\d.e+-]+)\s+([\d.e+-]+)\s+([\d.e+-]+)(?:\s*\/\s*([\d.e+-]+))?\)$/);
|
|
15
|
+
if (!m)
|
|
16
|
+
return color;
|
|
17
|
+
const r = Math.round(parseFloat(m[1] ?? "0") * 255);
|
|
18
|
+
const g = Math.round(parseFloat(m[2] ?? "0") * 255);
|
|
19
|
+
const b = Math.round(parseFloat(m[3] ?? "0") * 255);
|
|
20
|
+
if (m[4] !== undefined) {
|
|
21
|
+
return `rgba(${r}, ${g}, ${b}, ${parseFloat(m[4])})`;
|
|
22
|
+
}
|
|
23
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
24
|
+
};
|
|
25
|
+
let probeElement = null;
|
|
26
|
+
const getProbeElement = () => {
|
|
27
|
+
if (probeElement) {
|
|
28
|
+
return probeElement;
|
|
29
|
+
}
|
|
30
|
+
if (typeof document === "undefined") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const el = document.createElement("span");
|
|
34
|
+
el.style.display = "none";
|
|
35
|
+
document.body.appendChild(el);
|
|
36
|
+
probeElement = el;
|
|
37
|
+
return el;
|
|
38
|
+
};
|
|
39
|
+
const resolveColorMix = (expression, fallback) => {
|
|
40
|
+
const cached = colorCache.get(expression);
|
|
41
|
+
if (cached !== undefined) {
|
|
42
|
+
return cached;
|
|
43
|
+
}
|
|
44
|
+
const el = getProbeElement();
|
|
45
|
+
if (!el) {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
el.style.color = "";
|
|
49
|
+
el.style.color = expression;
|
|
50
|
+
if (!el.style.color) {
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
const computed = getComputedStyle(el).color;
|
|
54
|
+
if (!computed) {
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
const normalized = normalizeComputedColor(computed);
|
|
58
|
+
colorCache.set(expression, normalized);
|
|
59
|
+
return normalized;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Mix two CSS colors using the browser's `color-mix(in srgb)` function.
|
|
63
|
+
*
|
|
64
|
+
* @param color1 - First color (any valid CSS color string)
|
|
65
|
+
* @param color2 - Second color (any valid CSS color string)
|
|
66
|
+
* @param percentage - Percentage of `color1` in the mix (0–100)
|
|
67
|
+
* @returns {string} Resolved color as an `rgb()` string, or `color1` if resolution fails
|
|
68
|
+
*/
|
|
69
|
+
const mixColor = (color1, color2, percentage) => {
|
|
70
|
+
const expression = `color-mix(in srgb, ${color1} ${percentage}%, ${color2})`;
|
|
71
|
+
return resolveColorMix(expression, color1);
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Darken a CSS color by mixing it with black.
|
|
75
|
+
*
|
|
76
|
+
* @param color - Any valid CSS color string
|
|
77
|
+
* @param amount - Darkening intensity from 0 (no change) to 100 (pure black)
|
|
78
|
+
*/
|
|
79
|
+
const darkenColor = (color, amount) => mixColor(color, "black", 100 - amount);
|
|
80
|
+
/**
|
|
81
|
+
* Lighten a CSS color by mixing it with white.
|
|
82
|
+
*
|
|
83
|
+
* @param color - Any valid CSS color string
|
|
84
|
+
* @param amount - Lightening intensity from 0 (no change) to 100 (pure white)
|
|
85
|
+
*/
|
|
86
|
+
const lightenColor = (color, amount) => mixColor(color, "white", 100 - amount);
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a CSS color and apply an opacity multiplier to its alpha channel.
|
|
89
|
+
*
|
|
90
|
+
* Unlike setting `element.style.opacity`, this only affects the individual
|
|
91
|
+
* color value — useful when fill and stroke need independent opacity.
|
|
92
|
+
*
|
|
93
|
+
* @param color - Any valid CSS color string
|
|
94
|
+
* @param opacity - Opacity multiplier from 0 to 1 (multiplied with existing alpha)
|
|
95
|
+
* @returns {string} `rgba()` string with the combined alpha, or the original color if resolution fails
|
|
96
|
+
*/
|
|
97
|
+
const colorWithOpacity = (color, opacity) => {
|
|
98
|
+
if (opacity >= 1)
|
|
99
|
+
return color;
|
|
100
|
+
const cacheKey = `${color}@${opacity}`;
|
|
101
|
+
const cached = colorCache.get(cacheKey);
|
|
102
|
+
if (cached !== undefined)
|
|
103
|
+
return cached;
|
|
104
|
+
const el = getProbeElement();
|
|
105
|
+
if (!el)
|
|
106
|
+
return color;
|
|
107
|
+
el.style.color = "";
|
|
108
|
+
el.style.color = color;
|
|
109
|
+
const computed = getComputedStyle(el).color;
|
|
110
|
+
if (!computed)
|
|
111
|
+
return color;
|
|
112
|
+
const normalized = normalizeComputedColor(computed);
|
|
113
|
+
const rgbaMatch = normalized.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/);
|
|
114
|
+
if (!rgbaMatch)
|
|
115
|
+
return color;
|
|
116
|
+
const existingAlpha = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1;
|
|
117
|
+
const result = `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, ${existingAlpha * opacity})`;
|
|
118
|
+
colorCache.set(cacheKey, result);
|
|
119
|
+
return result;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Clear the resolved-color cache and detach the probe element.
|
|
123
|
+
* Exposed for test teardown only — not part of the public API.
|
|
124
|
+
*/
|
|
125
|
+
const resetColorUtilsForTesting = () => {
|
|
126
|
+
colorCache.clear();
|
|
127
|
+
if (probeElement) {
|
|
128
|
+
probeElement.remove();
|
|
129
|
+
probeElement = null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export { colorWithOpacity, darkenColor, lightenColor, mixColor, resetColorUtilsForTesting };
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@trackunit/react-map-color-utils",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"repository": "https://github.com/Trackunit/manager",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.x"
|
|
8
|
+
},
|
|
9
|
+
"module": "./index.esm.js",
|
|
10
|
+
"main": "./index.cjs.js",
|
|
11
|
+
"types": "./index.d.ts"
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-backed color mixing via CSS `color-mix()`.
|
|
3
|
+
*
|
|
4
|
+
* Uses a hidden DOM element + `getComputedStyle` to resolve expressions,
|
|
5
|
+
* cached so each unique expression is computed at most once.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Mix two CSS colors using the browser's `color-mix(in srgb)` function.
|
|
9
|
+
*
|
|
10
|
+
* @param color1 - First color (any valid CSS color string)
|
|
11
|
+
* @param color2 - Second color (any valid CSS color string)
|
|
12
|
+
* @param percentage - Percentage of `color1` in the mix (0–100)
|
|
13
|
+
* @returns {string} Resolved color as an `rgb()` string, or `color1` if resolution fails
|
|
14
|
+
*/
|
|
15
|
+
export declare const mixColor: (color1: string, color2: string, percentage: number) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Darken a CSS color by mixing it with black.
|
|
18
|
+
*
|
|
19
|
+
* @param color - Any valid CSS color string
|
|
20
|
+
* @param amount - Darkening intensity from 0 (no change) to 100 (pure black)
|
|
21
|
+
*/
|
|
22
|
+
export declare const darkenColor: (color: string, amount: number) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Lighten a CSS color by mixing it with white.
|
|
25
|
+
*
|
|
26
|
+
* @param color - Any valid CSS color string
|
|
27
|
+
* @param amount - Lightening intensity from 0 (no change) to 100 (pure white)
|
|
28
|
+
*/
|
|
29
|
+
export declare const lightenColor: (color: string, amount: number) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a CSS color and apply an opacity multiplier to its alpha channel.
|
|
32
|
+
*
|
|
33
|
+
* Unlike setting `element.style.opacity`, this only affects the individual
|
|
34
|
+
* color value — useful when fill and stroke need independent opacity.
|
|
35
|
+
*
|
|
36
|
+
* @param color - Any valid CSS color string
|
|
37
|
+
* @param opacity - Opacity multiplier from 0 to 1 (multiplied with existing alpha)
|
|
38
|
+
* @returns {string} `rgba()` string with the combined alpha, or the original color if resolution fails
|
|
39
|
+
*/
|
|
40
|
+
export declare const colorWithOpacity: (color: string, opacity: number) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Clear the resolved-color cache and detach the probe element.
|
|
43
|
+
* Exposed for test teardown only — not part of the public API.
|
|
44
|
+
*/
|
|
45
|
+
export declare const resetColorUtilsForTesting: () => void;
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./colorUtils";
|