@gravity-ui/graph
Version:
Modern graph editor component
118 lines (117 loc) • 4.22 kB
JavaScript
// Cache for parsed colors
const parseColorCache = new Map();
// Cache for colors with applied alpha
const applyAlphaCache = new Map();
// Shared canvas context used only for fillStyle normalization — no drawing, no getImageData.
let colorNormCtx = null;
function getColorNormContext() {
if (typeof document === "undefined") {
return null;
}
if (!colorNormCtx) {
const canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
colorNormCtx = canvas.getContext("2d");
}
return colorNormCtx;
}
/**
* Parses a color string and returns RGBA components.
*
* Normalization works by assigning the color to a canvas `fillStyle` and
* reading the value back — the browser converts any CSS color format
* (named colors, hsl/hsla, hex, rgb/rgba) into either `#rrggbb` or
* `rgba(r, g, b, a)`. No pixels are drawn or read via getImageData,
* so there are no clearRect / compositing issues that affect Safari
* with OffscreenCanvas.
*
* @param color - Color in any valid CSS format
* @returns RGBA components or null if parsing fails
*/
function parseColor(color) {
const cached = parseColorCache.get(color);
if (cached) {
return cached;
}
const ctx = getColorNormContext();
if (!ctx) {
return null;
}
// Assign the color — the browser normalises it immediately.
// Reading fillStyle back gives '#rrggbb' or 'rgba(r, g, b, a)'.
// No pixel is drawn; no getImageData is needed.
ctx.fillStyle = "#000000"; // fallback if user passes invalid color
ctx.fillStyle = color;
const normalized = ctx.fillStyle;
let result = null;
// '#rrggbb' (opaque shorthand browsers emit for solid colors)
const hex = normalized.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
if (hex) {
result = {
r: parseInt(hex[1], 16),
g: parseInt(hex[2], 16),
b: parseInt(hex[3], 16),
a: 1,
};
}
if (!result) {
// 'rgba(r, g, b, a)' or 'rgb(r, g, b)'
const rgba = normalized.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
if (rgba) {
result = {
r: parseInt(rgba[1], 10),
g: parseInt(rgba[2], 10),
b: parseInt(rgba[3], 10),
a: rgba[4] !== undefined ? parseFloat(rgba[4]) : 1,
};
}
}
if (!result) {
return null;
}
parseColorCache.set(color, result);
return result;
}
/**
* Applies alpha (opacity) to a color string.
* This is useful as an alternative to using ctx.globalAlpha, which affects all drawing operations.
* Instead, you can apply opacity directly to specific colors.
*
* @param color - Color in any valid CSS format (hex, rgb, rgba, hsl, hsla, named colors)
* @param alpha - Alpha value between 0 (fully transparent) and 1 (fully opaque)
* @returns Color string in rgba format with applied alpha
*
* @example
* applyAlpha('#ff0000', 0.5) // returns 'rgba(255, 0, 0, 0.5)'
* applyAlpha('rgb(255, 0, 0)', 0.5) // returns 'rgba(255, 0, 0, 0.5)'
* applyAlpha('rgba(255, 0, 0, 0.8)', 0.5) // returns 'rgba(255, 0, 0, 0.4)' - multiplies existing alpha
* applyAlpha('red', 0.5) // returns 'rgba(255, 0, 0, 0.5)'
*/
export function applyAlpha(color, alpha) {
const clampedAlpha = Math.max(0, Math.min(1, alpha));
const cacheKey = `${color}|${clampedAlpha}`;
const cachedResult = applyAlphaCache.get(cacheKey);
if (cachedResult) {
return cachedResult;
}
const parsed = parseColor(color);
if (!parsed) {
// Fallback for SSR or parsing failure
return color;
}
// Multiply existing alpha with new alpha
const finalAlpha = parsed.a * clampedAlpha;
const result = `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${finalAlpha})`;
applyAlphaCache.set(cacheKey, result);
return result;
}
/**
* Clears the color cache to free up memory.
* Use this if you've processed a large number of unique colors and want to reclaim memory.
*/
export function clearColorCache() {
parseColorCache.clear();
applyAlphaCache.clear();
colorNormCtx = null;
}