我在TypeScript中把这个答案https://stackoverflow.com/a/13542669/4537906重写成一对可读的函数。
原因是在现代JavaScript中,我们不再需要关心保存字符。这是由编译器完成的。在我看来,我们的目标应该是编写可读性和可理解的代码。
这是我的方法:
tint.ts
type ColorObject = Record<"r" | "g" | "b" | "a", number>;
const singleColorSpace = 16 * 16; // 256
const blueSpace = singleColorSpace;
const greenSpace = blueSpace * singleColorSpace; // 65536
const redSpace = greenSpace * singleColorSpace; // 16777216
/* eslint-disable regex/invalid */
// adapted to TS from https://github.com/PimpTrizkit/PJs/wiki/12.-Shade,-Blend-and-Convert-a-Web-Color-(pSBC.js)
export const toColorObject = (rgbOrHex: string): ColorObject => {
const { length } = rgbOrHex;
const outputColor = {} as ColorObject;
if (length > 9) {
const rgbaColor = rgbOrHex.split(",");
const [rgbaAndRed, green, blue, alpha] = rgbaColor;
if (rgbaAndRed.slice(0, 3) !== "rgb") {
throw new Error("Invalid color format");
}
const red = rgbaAndRed[3] === "a" ? rgbaAndRed.slice(5) : rgbaAndRed.slice(4);
const rgbaLength = rgbaColor.length;
if (rgbaLength < 3 || rgbaLength > 4) {
return null;
}
outputColor.r = parseInt(red, 10);
outputColor.g = parseInt(green, 10);
outputColor.b = parseInt(blue, 10);
outputColor.a = alpha ? parseFloat(alpha) : -1;
} else {
if (length === 8 || length === 6 || length < 4) {
throw new Error("Invalid hex color format");
}
let HexColor = rgbOrHex;
if (length < 6) {
HexColor = `#${rgbOrHex[1]}${rgbOrHex[1]}${rgbOrHex[2]}${rgbOrHex[2]}${rgbOrHex[3]}${rgbOrHex[3]}${
length > 4 ? rgbOrHex[4] + rgbOrHex[4] : ""
}`;
}
if (length === 9 || length === 5) {
const hexRed = parseInt(HexColor.slice(1, 3), 16);
outputColor.r = hexRed;
const hexGreen = parseInt(HexColor.slice(3, 5), 16);
outputColor.g = hexGreen;
const hexBlue = parseInt(HexColor.slice(5, 7), 16);
outputColor.b = hexBlue;
const hexAlpha = parseInt(HexColor.slice(7, 9), 16);
outputColor.a = Math.round((hexAlpha / 255) * 100) / 100;
} else {
const hexRed = parseInt(HexColor.slice(1, 3), 16);
outputColor.r = hexRed;
const hexGreen = parseInt(HexColor.slice(3, 5), 16);
outputColor.g = hexGreen;
const hexBlue = parseInt(HexColor.slice(5, 7), 16);
outputColor.b = hexBlue;
outputColor.a = -1;
}
}
return outputColor;
};
const black: ColorObject = { r: 0, g: 0, b: 0, a: -1 };
const white: ColorObject = { r: 255, g: 255, b: 255, a: -1 };
export const tint = (
ratio: number,
inputColor: string,
{ toColor, useLinear, reformat }: { toColor?: string; useLinear?: boolean; reformat?: boolean } = {}
) => {
const { round } = Math;
const clampedRatio = Math.min(Math.max(ratio, -1), 1);
if (ratio < -1 || ratio > 1) {
// eslint-disable-next-line no-console
console.info(`Ratio should be between -1 and 1 and it is ${ratio}. It will be clamped to ${clampedRatio}`);
}
let baseColor = inputColor;
if (inputColor[0] !== "r" && inputColor[0] !== "#") {
baseColor = "#000";
// eslint-disable-next-line no-console
console.info(
`Invalid input color format. "${inputColor}" should be rgb(a) or hex. It will fallback to "${baseColor}"`
);
}
let isRGBformat = baseColor.length > 9 || baseColor.includes("rgb(");
isRGBformat = reformat ? !isRGBformat : isRGBformat;
if (toColor) {
const isToColorRgbFormat = (toColor && toColor?.length > 9) || toColor?.includes("rgb(");
isRGBformat = reformat ? !isToColorRgbFormat : isToColorRgbFormat;
}
const formattedBaseColor = toColorObject(baseColor);
const isNegativeRatio = clampedRatio < 0;
const toColorDefault = isNegativeRatio ? black : white;
const formattedToColor = toColor && !reformat ? toColorObject(toColor) : toColorDefault;
const toColorRatio = Math.abs(clampedRatio);
const baseRatio = 1 - toColorRatio;
const outputColor = {} as ColorObject;
if (useLinear) {
outputColor.r = round(baseRatio * formattedBaseColor.r + toColorRatio * formattedToColor.r);
outputColor.g = round(baseRatio * formattedBaseColor.g + toColorRatio * formattedToColor.g);
outputColor.b = round(baseRatio * formattedBaseColor.b + toColorRatio * formattedToColor.b);
} else {
outputColor.r = round((baseRatio * formattedBaseColor.r ** 2 + toColorRatio * formattedToColor.r ** 2) ** 0.5);
outputColor.g = round((baseRatio * formattedBaseColor.g ** 2 + toColorRatio * formattedToColor.g ** 2) ** 0.5);
outputColor.b = round((baseRatio * formattedBaseColor.b ** 2 + toColorRatio * formattedToColor.b ** 2) ** 0.5);
}
const blendedAlpha = formattedBaseColor.a * baseRatio + formattedToColor.a * toColorRatio;
outputColor.a = formattedToColor.a < 0 ? formattedBaseColor.a : blendedAlpha;
const hasAlpha = formattedBaseColor.a >= 0 || formattedToColor.a >= 0;
if (isRGBformat) {
return `rgb${hasAlpha ? "a" : ""}(${outputColor.r},${outputColor.g},${outputColor.b}${
hasAlpha ? `,${round(outputColor.a * 1000) / 1000}` : ""
})`;
}
return `#${(
outputColor.r * redSpace +
outputColor.g * greenSpace +
outputColor.b * blueSpace +
(hasAlpha ? round(outputColor.a * 255) : 0)
)
.toString(16)
// If no Alpha, we remove the last 2 hex digits
.slice(0, hasAlpha ? undefined : -2)}`;
};
还有一个笑话测试的集合
tint.test.ts
import { tint, toColorObject } from "./tint";
const rgbBlue = "rgb(20,60,200)";
const rgbaBlue = "rgba(20,60,200,0.67423)";
const hex6Cyan = "#67DAF0";
const hex3Pink = "#F3A";
const hex4Pink = "#F3A9";
const rbgBrown = "rgb(200,60,20)";
const rgbaBrown = "rgba(200,60,20,0.98631)";
describe("tint", () => {
describe("Logarithmic blending", () => {
describe("Shades", () => {
it("lightens rgb color", () => {
expect(tint(0.42, rgbBlue)).toEqual("rgb(166,171,225)");
});
it("darkens hex color", () => {
expect(tint(-0.4, hex3Pink)).toEqual("#c62884");
});
it("lightens rgba color", () => {
expect(tint(0.42, rgbaBrown)).toEqual("rgba(225,171,166,0.986)");
});
it("returns black with ratio -1", () => {
expect(tint(-1, rgbBlue)).toEqual("rgb(0,0,0)");
});
});
describe("converts color notation", () => {
it("converts from rgba to hexa", () => {
// expect(tint(0.42, color2, "c")).toEqual("#a6abe1ac");
expect(tint(0.42, rgbaBlue, { reformat: true })).toEqual("#a6abe1ac");
});
it("converts from hexa to rgba", () => {
// expect(tint(0, color6, "c", true)).toEqual("rgba(255,51,170,0.6)");
expect(tint(0, hex4Pink, { reformat: true })).toEqual("rgba(255,51,170,0.6)");
});
it("converts and returns white with ratio 1", () => {
expect(tint(1, hex3Pink, { reformat: true })).toEqual("rgb(255,255,255)");
});
});
describe("Blends two colors", () => {
it("blends rgba with rgba", () => {
expect(tint(-0.5, rgbaBlue, { toColor: rgbaBrown })).toEqual("rgba(142,60,142,0.83)");
});
it("blends rgba with rgb", () => {
expect(tint(0.7, rgbaBlue, { toColor: rbgBrown })).toEqual("rgba(168,60,111,0.674)");
});
it("blends hex with rgb", () => {
expect(tint(0.25, hex6Cyan, { toColor: rbgBrown })).toEqual("rgb(134,191,208)");
});
it("blends rgb with hex", () => {
expect(tint(0.75, rbgBrown, { toColor: hex6Cyan })).toEqual("#86bfd0");
});
});
});
describe("Linear Blending", () => {
describe("Shades", () => {
it("lightens rgb color", () => {
expect(tint(0.42, rgbBlue, { useLinear: true })).toEqual("rgb(119,142,223)");
});
it("darkens hex color", () => {
expect(tint(-0.4, hex3Pink, { useLinear: true })).toEqual("#991f66");
});
it("lightens rgba color", () => {
expect(tint(0.42, rgbaBrown, { useLinear: true })).toEqual("rgba(223,142,119,0.986)");
});
it("returns black with ratio -1", () => {
expect(tint(-1, rgbBlue, { useLinear: true })).toEqual("rgb(0,0,0)");
});
});
describe("converts color notation", () => {
it("converts from rgba to hexa", () => {
expect(tint(0.42, rgbaBlue, { reformat: true, useLinear: true })).toEqual("#778edfac");
});
it("converts from hexa to rgba", () => {
expect(tint(0, hex4Pink, { reformat: true, useLinear: true })).toEqual("rgba(255,51,170,0.6)");
});
it("converts and returns white with ratio 1", () => {
expect(tint(1, hex3Pink, { useLinear: true, reformat: true })).toEqual("rgb(255,255,255)");
});
});
describe("Blends two colors", () => {
it("blends rgba with rgba", () => {
expect(tint(-0.5, rgbaBlue, { toColor: rgbaBrown, useLinear: true })).toEqual("rgba(110,60,110,0.83)");
});
it("blends rgba with rgb", () => {
expect(tint(0.7, rgbaBlue, { toColor: rbgBrown, useLinear: true })).toEqual("rgba(146,60,74,0.674)");
});
it("blends hex with rgb", () => {
expect(tint(0.25, hex6Cyan, { toColor: rbgBrown, useLinear: true })).toEqual("rgb(127,179,185)");
});
it("blends rgb with hex", () => {
expect(tint(0.75, rbgBrown, { toColor: hex6Cyan, useLinear: true })).toEqual("#7fb3b9");
});
});
});
describe("Error handling", () => {
describe("When invalid hex color provided", () => {
it.each([1, 2, 5])("throws error if hex color has %s characters", (n) => {
const correlativeNumbers = Array.from(Array(n).keys()).join("");
expect(() => tint(0, `#${correlativeNumbers}`)).toThrow("Invalid hex color format");
});
});
describe("When ratio is not between -1 and 1", () => {
it("clamps ratio to -1", () => {
expect(tint(-43, rgbBlue)).toEqual("rgb(0,0,0)");
});
it("clamps ratio to 1", () => {
expect(tint(42, rgbBlue)).toEqual("rgb(255,255,255)");
});
});
});
});
describe("toColorObject function", () => {
it("should return a color object from hex", () => {
expect(toColorObject("#fff")).toEqual({
r: 255,
g: 255,
b: 255,
a: -1,
});
});
it("should return a color object from hex with alpha", () => {
expect(toColorObject("#fff6")).toEqual({
r: 255,
g: 255,
b: 255,
a: 0.4,
});
});
it("should return a color object from rgb", () => {
expect(toColorObject("rgb(255,255,255)")).toEqual({
r: 255,
g: 255,
b: 255,
a: -1,
});
});
it("should return a color object from rgba", () => {
expect(toColorObject("rgba(255,255,255,1)")).toEqual({
r: 255,
g: 255,
b: 255,
a: 1,
});
});
describe("Error handling", () => {
it("should throw error if invalid color provided", () => {
expect(() => toColorObject("foo")).toThrow("Invalid hex color format");
});
it("should throw error if invalid color provided", () => {
expect(() => toColorObject("invalid color")).toThrow("Invalid color format");
});
});
});
希望你喜欢。这很简单,但效果很好