From 581a4aab3da143fa8f81b0358a3eede2b4b14df5 Mon Sep 17 00:00:00 2001 From: Sparkenstein Date: Sat, 13 Jul 2024 21:28:57 +0530 Subject: [PATCH 01/24] doc: 3 new features --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f2d1f10..76d8f8f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ This project exists solely because I was fed up switching between different tool #### Checkout [features.md](features.md) for a short video demo on every feature. -DevTools-X has about **37 features** as of now, and growing. +DevTools-X has about **40 features** as of now, and growing. The full list in below, One big selling point of DevTools-X is it uses `monaco-editor`, the editor used by vscode, so tons of editor features are available to you right from the start, as if you are using vscode. @@ -101,6 +101,9 @@ available to you right from the start, as if you are using vscode. 35. Generate mock data with Faker 36. CSS live playground 37. QR Code Reader +38. Image cropper +39. HMAC Generator +40. Color palette generator ## Contributing From 87fdf0432e84c8781e74793b1eec166183a2e334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:12:53 +0000 Subject: [PATCH 02/24] chore(deps): bump openssl in /src-tauri in the cargo group Bumps the cargo group in /src-tauri with 1 update: [openssl](https://github.com/sfackler/rust-openssl). Updates `openssl` from 0.10.64 to 0.10.66 - [Release notes](https://github.com/sfackler/rust-openssl/releases) - [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.64...openssl-v0.10.66) --- updated-dependencies: - dependency-name: openssl dependency-type: indirect dependency-group: cargo ... Signed-off-by: dependabot[bot] --- src-tauri/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5028f0e..ac823e9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2466,9 +2466,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -2498,9 +2498,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", From 8e8634ca88cac9070da8cc32ba6c58fc72f92a0e Mon Sep 17 00:00:00 2001 From: Thijs Zijdel Date: Tue, 6 Aug 2024 13:14:10 +0200 Subject: [PATCH 03/24] Improved color tools As mentioned in: [Feature Request] improve color tools https://github.com/fosslife/devtools-x/issues/36 Will keep improving this module since if still have some left from a previous project. --- package.json | 6 +- src/Features/colors/Colors.tsx | 303 ++++++++++++---- src/Features/colors/CustomPicker.tsx | 251 +++++++++++++ src/Features/colors/blindness.ts | 175 +++++++++ src/Features/colors/contrast.ts | 89 +++++ src/Features/colors/styles.module.css | 55 +++ src/Features/colors/utilities.ts | 494 ++++++++++++++++++++++++++ 7 files changed, 1292 insertions(+), 81 deletions(-) create mode 100644 src/Features/colors/CustomPicker.tsx create mode 100644 src/Features/colors/blindness.ts create mode 100644 src/Features/colors/contrast.ts create mode 100644 src/Features/colors/utilities.ts diff --git a/package.json b/package.json index 7581bfe..1db7c56 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "axios": "^1.6.8", "chroma-js": "^2.4.2", "clsx": "^2.1.0", - "color-convert": "^2.0.1", "convert-units": "^2.3.4", "cron-parser": "^4.9.0", "cronstrue": "^2.48.0", @@ -60,7 +59,7 @@ "quicktype": "^23.0.114", "quicktype-core": "^23.0.114", "react": "^18.2.0", - "react-colorful": "^5.6.1", + "react-color": "^2.19.3", "react-compare-slider": "^3.0.1", "react-cropper": "^2.3.3", "react-dom": "^18.2.0", @@ -74,6 +73,7 @@ "simple-base-converter": "^1.0.19", "tauri-plugin-store-api": "https://github.com/tauri-apps/tauri-plugin-store", "terser": "^5.30.0", + "tinycolor2": "^1.6.0", "typescript-eslint": "^7.4.0", "uuid": "^9.0.1" }, @@ -93,7 +93,9 @@ "@types/prismjs": "^1.26.3", "@types/qrcode": "^1.5.5", "@types/react": "^18.2.73", + "@types/react-color": "^3.0.12", "@types/react-dom": "^18.2.23", + "@types/tinycolor2": "^1.4.6", "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^7.4.0", "@vitejs/plugin-react": "^4.2.1", diff --git a/src/Features/colors/Colors.tsx b/src/Features/colors/Colors.tsx index 9e7b28c..d36ace5 100644 --- a/src/Features/colors/Colors.tsx +++ b/src/Features/colors/Colors.tsx @@ -9,32 +9,36 @@ import { Tooltip, useMantineColorScheme, } from "@mantine/core"; -import cc from "color-convert"; -import { useState } from "react"; -import { RgbaColor, RgbaColorPicker } from "react-colorful"; +import { Fragment, useState } from "react"; +import CustomPicker from "./CustomPicker"; import { BsMoon, BsSun } from "react-icons/bs"; import { OutputBox } from "../../Components/OutputBox"; import { clipboard } from "@tauri-apps/api"; +import { + getInterpolateShades, + renderCmyk, + renderHsl, + Convert, + interpolateColor, + createShading, + hex2cmyk, +} from "./utilities"; +import { + canBeWhite, + checkContrast, + formatRatio, + meetsMinimumRequirements, +} from "./contrast"; const Colors = () => { const { colorScheme, toggleColorScheme } = useMantineColorScheme(); - // maintain 20 history Colors - const [history, setHistory] = useState( - Array(20).fill({ - r: 0, - g: 0, - b: 0, - a: 0, - }) as RgbaColor[] - ); - const [color, setColor] = useState({ - r: 34, - g: 135, - b: 199, - a: 0.5, + const [config, setConfig] = useState({ + steps: 15, }); + const [history, setHistory] = useState(Array(20).fill("#000000")); + const [color, setColor] = useState("#000000"); const onCopy = () => { // fill one color in history @@ -46,87 +50,228 @@ const Colors = () => { }); }; + const copy = (color: string) => { + clipboard.writeText(color.startsWith("#") ? color : `#${color}`); + setHistory((prev) => { + let newHistory = [...prev]; + newHistory.pop(); + newHistory.unshift(color); + return newHistory; + }); + }; + + const [l, c, h] = new Convert().hex2lch(color); + + const spectrum = createShading({ + color: color, + shades: config.steps, + start: 0, + end: 95, + easeMethod: "ease-in-out", + includeBase: true, + space: "full-gamut", + }).shades; + + const shades = interpolateColor([l, c, h], "l", config.steps, 1); // towards black + + const tints = getInterpolateShades(color, "#ffffff", config.steps); // towards white + const tones = getInterpolateShades(color, "#808080", config.steps); // towards grey + + const after = interpolateColor([l, c, h], "h", config.steps / 2, h + 90); // rotating the hue + const before = interpolateColor( + [l, c, h], + "h", + config.steps / 2, + h - 90 + ).reverse(); // rotating the hue + const hues = before.concat(after.slice(1)); + + const temperaturesCool = interpolateColor([l, c, h], "h", config.steps, 240); + const temperaturesWarm = interpolateColor([l, c, h], "h", config.steps, 60); + const temperatures = h > 180 ? temperaturesCool : temperaturesWarm; + + const conv = new Convert(); + const hsv = Object.values(conv.hex2hsv(color)) + .map((v) => v.toFixed()) + .join(", "); return ( - - { - setColor(e); - }} + + setColor(color.hex)} /> - + + - v.toFixed()) + .join(", ")} onCopy={onCopy} /> + + + + + + + + + + + + + + } + offLabel={} + size={"lg"} + onChange={() => toggleColorScheme()} + /> + + History + + + + {history.map((color, i) => ( + + { + clipboard.writeText( + color.startsWith("#") ? color : `#${color}` + ); + }} + style={{ + backgroundColor: color, + }} + > + + ))} + + + + + + + + + + ); +}; + +const RenderShades = ({ + colors, + setColor, + label, +}: { + colors: string[]; + setColor: (color: string) => void; + label: string; +}) => ( +
+ + {label} + + {colors.map((color, i) => ( { + setColor(color); + }} style={{ - background: `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`, - borderRadius: 5, - width: "400px", - height: "30%", + backgroundColor: color, + color: canBeWhite(color) ? "white" : "black", + fontSize: "0.7em", + display: "flex", + justifyContent: "center", + alignItems: "center", + width: "10%", + height: "100%", }} - > - - } - offLabel={} - size={"lg"} - onChange={() => toggleColorScheme()} - /> - - History - - - - {history.map((color, i) => ( - - { - clipboard.writeText( - `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})` - ); - }} - style={{ - backgroundColor: `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`, - }} - > - - ))} + > + {color.toUpperCase()} - + ))} +
+); + +export const ContrastContent = ({ + color, + background = "#000000", + toggle, +}: { + toggle?: () => void; + background: string; + color: string; +}) => { + const contrast = checkContrast(color, background); + const result = meetsMinimumRequirements(contrast); + + return ( +
+ WCAG contrast +
+ (toggle ? toggle() : null)} + className={`${classes.box} ${background === "#ffffff" ? "dark" : "light"}`} + style={{ color: color, background }} + > + Aa + + {formatRatio(contrast)} +
+
+ {result.map(({ level, label, pass }) => ( + +
+ {label} +
+
{level}
+
+ ))} +
+
); }; diff --git a/src/Features/colors/CustomPicker.tsx b/src/Features/colors/CustomPicker.tsx new file mode 100644 index 0000000..7facedd --- /dev/null +++ b/src/Features/colors/CustomPicker.tsx @@ -0,0 +1,251 @@ +import React, { MouseEvent } from "react"; +import { ColorResult, CustomPicker, HSLColor } from "react-color"; +import { + Saturation, + EditableInput, + Hue, +} from "react-color/lib/components/common"; +import { Convert } from "./utilities"; +import tinycolor2 from "tinycolor2"; + +const inputStyles = { + input: { + border: "1px solid black", + padding: "10px", + fontSize: "15px", + // color: "#000", + }, +}; +const inlineStyles = { + container: { + display: "flex", + flexDirection: "column", + height: "auto", + width: "95%", + textAlign: "center", + justifyContent: "center", + // margin: "auto" + }, + pointer: { + transform: "translate(-50%, -50%)", + width: "20px", + height: "20px", + borderRadius: "50%", + backgroundColor: "rgb(248, 248, 248)", + boxShadow: "0 1px 4px 0 rgba(0, 0, 0, 0.37)", + cursor: "pointer", + }, + slider: { + width: "8px", + borderRadius: "1px", + height: "20px", + boxShadow: "0 0 2px rgba(0, 0, 0, .6)", + background: "#fff", + transform: "translateX(-2px)", + }, + saturation: { + width: "100%", + paddingBottom: "15%", + position: "relative", + overflow: "hidden", + cursor: "pointer", + }, + swatchCircle: { + minWidth: 20, + minHeight: 20, + margin: "1px 2px", + cursor: "pointer", + background: "red", + zIndex: 9, + boxShadow: "0 0 2px rgba(0, 0, 0, .6)", + borderRadius: "50%", + }, +}; + +const onPointerMouseDown = (event: MouseEvent) => { + const pointer = document.querySelector(".custom-pointer") as HTMLDivElement; + const pointerContainer = pointer?.parentElement as HTMLDivElement; + if (pointerContainer) { + pointerContainer.style.top = event.clientY + "px"; + pointerContainer.style.left = event.clientX + "px"; + } +}; + +const CustomSlider = () => { + return
; +}; + +const CustomPointer = () => { + return
; +}; + +interface Props { + colors?: string[]; + hexCode: string; + onChange: (color: ColorResult) => void; +} +interface State { + hsl: { h: number; s: number; l: number }; + hsv: { h: number; s: number; v: number }; + hex: string; +} + +// Todo Convert to functional component +// The docs of react-color aren't that good, but the picker is more customizable than the previous one +class CustomColorPicker extends React.Component { + convert = new Convert(); + + constructor(props: Props) { + super(props); + this.state = { + hsl: { + h: 0, + s: 0, + l: 0, + }, + hsv: { + h: 0, + s: 0, + v: 0, + }, + hex: "aaaaaa", + }; + } + + guard = (num: number) => { + if (isNaN(num)) return 0; + return num > 255 ? 255 : num < 0 ? 0 : num; + }; + + componentDidMount() { + const color = tinycolor2(this.props.hexCode); + this.setState({ + hsv: color.toHsv(), + hsl: color.toHsl(), + hex: color.toHex(), + }); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.hexCode !== this.state.hex) { + const color = tinycolor2(nextProps.hexCode); + this.setState({ + hsv: color.toHsv(), + hsl: color.toHsl(), + hex: color.toHex(), + }); + } + } + + handleHueChange = (hue: HSLColor) => { + const color = tinycolor2(hue); + const newState = { + hsl: hue, + hsv: color.toHsv(), + hex: color.toHex(), + rgb: color.toRgb(), + }; + this.setState(newState); + this.props.onChange(newState); + }; + + handleSaturationChange = (hsv: ColorResult) => { + const color = tinycolor2(hsv as any); + + this.props.onChange(hsv); + const input = document.body.getElementsByTagName("input"); + if (input[5]) { + input[5].value = color.toHex(); + } + this.setState({ + hsl: color.toHsl(), + }); + }; + + displayColorSwatches = (colors: string[]) => { + return colors.map((color) => { + return ( +
this.props.onChange(color as any)} + key={color} + style={{ ...inlineStyles.swatchCircle, backgroundColor: color }} + /> + ); + }); + }; + + render() { + return ( +
+
+ +
+
+ +
+
+ + Hex + + +
+ {this.props.colors?.length && ( +
+ {this.displayColorSwatches(this.props.colors!)} +
+ )} +
+ ); + } +} + +// @ts-ignore +export default CustomPicker(CustomColorPicker); diff --git a/src/Features/colors/blindness.ts b/src/Features/colors/blindness.ts new file mode 100644 index 0000000..e47efb5 --- /dev/null +++ b/src/Features/colors/blindness.ts @@ -0,0 +1,175 @@ +import { Convert } from "./utilities"; + +const hexToRgb = (hex: string) => new Convert().hex2rgb(hex); +const rgbToHex = (r: number, g: number, b: number) => + new Convert().rgb2hex(r, g, b); + +const simulateColorBlindness = (color: string, blindnessType: string) => { + const [r, g, b] = hexToRgb(color) ?? [0, 0, 0]; + + let perceivedColor = [r, g, b]; + + if (blindnessType === "Protanopia") { + perceivedColor = [ + 0.567 * r + 0.433 * g + 0.0 * b, + 0.558 * r + 0.442 * g + 0.0 * b, + 0.242 * r - 0.1055 * g + 1.053 * b, + ]; + } + + if (blindnessType === "Deuteranopia") { + perceivedColor = [ + 0.625 * r + 0.375 * g + 0.0 * b, + 0.7 * r + 0.3 * g + 0.0 * b, + 0.15 * r + 0.1 * g + 0.75 * b, + ]; + } + + if (blindnessType === "Tritanopia") { + perceivedColor = [ + 0.95 * r + 0.05 * g + 0.0 * b, + 0.433 * r + 0.567 * g + 0.0 * b, + 0.475 * r + 0.475 * g + 0.05 * b, + ]; + } + + if (blindnessType === "Protanomaly") { + perceivedColor = [ + 0.817 * r + 0.183 * g + 0.0 * b, + 0.333 * r + 0.667 * g + 0.0 * b, + 0.0 * r + 0.125 * g + 0.875 * b, + ]; + } + + if (blindnessType === "Deuteranomaly") { + perceivedColor = [ + 0.8 * r + 0.2 * g + 0.0 * b, + 0.258 * r + 0.742 * g + 0.0 * b, + 0.0 * r + 0.142 * g + 0.858 * b, + ]; + } + + if (blindnessType === "Tritanomaly") { + perceivedColor = [ + 0.967 * r + 0.033 * g + 0.0 * b, + 0.0 * r + 0.733 * g + 0.267 * b, + 0.0 * r + 0.183 * g + 0.817 * b, + ]; + } + + if (blindnessType === "Achromatopsia") { + perceivedColor = [ + 0.299 * r + 0.587 * g + 0.114 * b, + 0.299 * r + 0.587 * g + 0.114 * b, + 0.299 * r + 0.587 * g + 0.114 * b, + ]; + } + + if (blindnessType === "Achromatomaly") { + perceivedColor = [ + 0.618 * r + 0.32 * g + 0.062 * b, + 0.163 * r + 0.775 * g + 0.062 * b, + 0.163 * r + 0.32 * g + 0.516 * b, + ]; + } + + if (blindnessType === "Achromatopsia") { + perceivedColor = [ + 0.299 * r + 0.587 * g + 0.114 * b, + 0.299 * r + 0.587 * g + 0.114 * b, + 0.299 * r + 0.587 * g + 0.114 * b, + ]; + } + + return rgbToHex( + Math.round(perceivedColor[0]), + Math.round(perceivedColor[1]), + Math.round(perceivedColor[2]) + ); +}; + +const colorSimilarityPercentage = (color1: string, color2: string): number => { + const [r1, g1, b1] = hexToRgb(color1) ?? [0, 0, 0]; + const [r2, g2, b2] = hexToRgb(color2) ?? [0, 0, 0]; + + // Compute Euclidean distance + const distance = Math.sqrt( + Math.pow(r2 - r1, 2) + Math.pow(g2 - g1, 2) + Math.pow(b2 - b1, 2) + ); + + // Max possible distance in RGB space is sqrt(3 * 255^2) + const maxDistance = Math.sqrt(3 * Math.pow(255, 2)); + + // Calculate similarity as a percentage + const similarity = 1 - distance / maxDistance; + + return similarity * 100; // Returns similarity percentage +}; + +const getSimilarBlindness = (color: string) => { + const stats: { + name: string; + match: string; + percentage: number; + }[] = blindnessStats.map((blindness) => { + // Hypothetical function to simulate color blindness + const perceivedColor = simulateColorBlindness(color, blindness.name); + + // Hypothetical function to calculate color similarity + const similarityPercentage = colorSimilarityPercentage( + color, + perceivedColor + ); + + return { + name: blindness.name, + match: perceivedColor, + percentage: similarityPercentage, + }; + }); + + return stats; +}; + +export const blindnessStats = [ + { + name: "Deuteranomaly", + description: "5.0% of men, 0.35% of women", + info: "Green-weak type of color blindness. Greens are more muted, and reds may be confused with greens.", + }, + { + name: "Protanopia", + description: "1.3% of men, 0.02% of women", + info: "Red-green color blindness, with a leaning towards difficulties distinguishing red hues.", + }, + { + name: "Protanomaly", + description: "1.3% of men, 0.02% of women", + info: "Reduced sensitivity to red light causing reds to be less bright.", + }, + { + name: "Deuteranopia", + description: "1.2% of men, 0.01% of women", + info: "Red-green color blindness, with a leaning towards difficulties distinguishing green hues.", + }, + { + name: "Tritanopia", + description: "0.001% of men, 0.03% of women", + info: "Blue-yellow color blindness. Blues appear greener and it can be hard to tell yellow and red from pink.", + }, + { + name: "Tritanomaly", + description: "0.0001% of men, 0.0001% of women", + info: "Reduced sensitivity to blue light causing blues to be less bright, and difficulties distinguishing between yellow and red from pink.", + }, + { + name: "Achromatomaly", + description: "0.003% of the population", + info: "Reduced sensitivity to light causing colors to appear less bright and less saturated.", + }, + { + name: "Achromatopsia", + description: "0.0001% of the population", + info: "Total color blindness. Colors are seen as completely neutral.", + }, +]; diff --git a/src/Features/colors/contrast.ts b/src/Features/colors/contrast.ts new file mode 100644 index 0000000..78aaa3c --- /dev/null +++ b/src/Features/colors/contrast.ts @@ -0,0 +1,89 @@ +const canBeWhite = (hex: string) => { + const ratio = checkContrast(hex, "#ffffff"); + return ratio >= 4.5; +}; + +function luminance(r: number, g: number, b: number) { + let [lumR, lumG, lumB] = [r, g, b].map((component) => { + let proportion = component / 255; + + return proportion <= 0.03928 + ? proportion / 12.92 + : Math.pow((proportion + 0.055) / 1.055, 2.4); + }); + + return 0.2126 * lumR + 0.7152 * lumG + 0.0722 * lumB; +} + +function contrastRatio(luminance1: number, luminance2: number) { + let lighterLum = Math.max(luminance1, luminance2); + let darkerLum = Math.min(luminance1, luminance2); + + return (lighterLum + 0.05) / (darkerLum + 0.05); +} + +/** + * Because color inputs format their values as hex strings (ex. + * #000000), we have to do a little parsing to extract the red, + * green, and blue components as numbers before calculating the + * luminance values and contrast ratio. + */ +function checkContrast(color1: any, color2: any) { + let [luminance1, luminance2] = [color1, color2].map((color) => { + /* Remove the leading hash sign if it exists */ + color = color.startsWith("#") ? color.slice(1) : color; + + let r = parseInt(color.slice(0, 2), 16); + let g = parseInt(color.slice(2, 4), 16); + let b = parseInt(color.slice(4, 6), 16); + + return luminance(r, g, b); + }); + + return contrastRatio(luminance1, luminance2); +} + +/** + * A utility to format ratios as nice, human-readable strings with + * up to two digits after the decimal point (ex. "4.3:1" or "17:1") + */ +function formatRatio(ratio: number) { + let ratioAsFloat = ratio.toFixed(2); + let isInteger = Number.isInteger(parseFloat(ratioAsFloat)); + return `${isInteger ? Math.floor(ratio) : ratioAsFloat}:1`; +} + +/** + * Determine whether the given contrast ratio meets WCAG + * requirements at any level (AA Large, AA, or AAA). In the return + * value, `isPass` is true if the ratio meets or exceeds the minimum + * of at least one level, and `maxLevel` is the strictest level that + * the ratio passes. + */ +const WCAG_MINIMUM_RATIOS = [ + ["AA Large", 3], + ["AA", 4.5], + ["AAA", 7], +]; + +const NameMapping = { + "AA Large": "Large text", + AA: "Small text", + AAA: "Graphics", +}; + +function meetsMinimumRequirements(ratio: number) { + let didPass = false; + let maxLevel = null; + + const checks = WCAG_MINIMUM_RATIOS.map(([level, minRatio]) => { + return { + level, + pass: ratio >= (minRatio as number), + label: NameMapping[level as keyof typeof NameMapping], + }; + }); + return checks; +} + +export { checkContrast, formatRatio, meetsMinimumRequirements, canBeWhite }; diff --git a/src/Features/colors/styles.module.css b/src/Features/colors/styles.module.css index 129b0c3..1ed3b8e 100644 --- a/src/Features/colors/styles.module.css +++ b/src/Features/colors/styles.module.css @@ -18,3 +18,58 @@ height: 24px; width: 24px; } + + +.wcagBox { + font-size: 14px; + display: flex; + flex-direction: column; + padding: 10px; + width: 40%; + border: 1px solid var(--mantine-color-gray-8); + border-radius: 5px; + +} + +.box { + display: flex; + margin-right: 0.5rem; + align-items: center; + padding: 0.125rem 0.5rem; + border: 2px; + text-align: center; + font-size: 1rem; + border-radius: 0.25rem; + + &.dark { + background-color: var(--mantine-color-gray-7); + border: 1px solid var(--mantine-color-gray-7); + } + &.light { + background-color: var(--mantine-color-gray-3); + border: 1px solid var(--mantine-color-gray-3); + } +} + +.grid { + margin-top: 10px; + display: grid; + gap: 0.5em; + grid-template-columns: repeat(2, 1fr); +} + +.ok { + color: #0ca678; +} + +.fail { + color: #ff5252; +} + + +.colorPicker { + /*width: 95%!important;*/ + /*height: 35vh!important;*/ + cursor: pointer !important; + +} diff --git a/src/Features/colors/utilities.ts b/src/Features/colors/utilities.ts new file mode 100644 index 0000000..419c1b4 --- /dev/null +++ b/src/Features/colors/utilities.ts @@ -0,0 +1,494 @@ +export const { + abs, + atan2, + cbrt, + cos, + exp, + floor, + max, + min, + PI, + pow, + sin, + sqrt, +} = Math; + +export const epsilon = pow(6, 3) / pow(29, 3); +export const kappa = pow(29, 3) / pow(3, 3); +export const precision = 100000000; +export const [wd50X, wd50Y, wd50Z] = [96.42, 100, 82.49]; + +// Degree and Radian conversion utilities +export const deg2rad = (degrees: number) => (degrees * PI) / 180; +export const rad2deg = (radians: number) => (radians * 180) / PI; +export const atan2d = (y: number, x: number) => rad2deg(atan2(y, x)); +export const cosd = (degrees: number) => cos(deg2rad(degrees)); +export const sind = (degrees: number) => sin(deg2rad(degrees)); + +const matrix = (params: number[], mats: any[]) => { + return mats.map((mat: any[]) => + mat.reduce( + (acc: number, value: number, index: string | number) => + acc + + (params[index as number] * precision * (value * precision)) / + precision / + precision, + 0 + ) + ); +}; + +const hexColorMatch = + /^#?(?:([a-f0-9])([a-f0-9])([a-f0-9])([a-f0-9])?|([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})?)$/i; +const fixFloat = (num: number) => parseFloat((num * 100).toFixed(1)); + +export class Convert { + hex2rgb = (hex: string) => { + // #{3,4,6,8} + const [, r, g, b, a, rr, gg, bb, aa] = hex.match(hexColorMatch) || []; + if (rr !== undefined || r !== undefined) { + const red = rr !== undefined ? parseInt(rr, 16) : parseInt(r + r, 16); + const green = gg !== undefined ? parseInt(gg, 16) : parseInt(g + g, 16); + const blue = bb !== undefined ? parseInt(bb, 16) : parseInt(b + b, 16); + const alpha = + aa !== undefined + ? parseInt(aa, 16) + : a !== undefined + ? parseInt(a + a, 16) + : 255; + return [red, green, blue, alpha].map((c) => (c * 100) / 255); + } + return [0, 0, 0, 1]; + // hex = hex.startsWith("#") ? hex.slice(1) : hex; + // if (hex.length === 3) { + // hex = Array.from(hex).reduce((str, x) => str + x + x, ""); // 123 -> 112233 + // } + // return hex + // .split(/([a-z0-9]{2,2})/) + // .filter(Boolean) + // .map((x) => parseInt(x, 16)); + // return `rgb${values.length == 4 ? "a" : ""}(${values.join(", ")})`; + }; + + hex2hsv = (hex: string) => { + const [h, s, l] = this.hex2hsl(hex) as [number, number, number]; + return this.hsl2hsv(h, s, l); + }; + + hex2hsl = (hex: string, asObj = false) => { + const [r, g, b] = this.hex2rgb(hex); + const max = Math.max(r, g, b), + min = Math.min(r, g, b); + let h: number, + s: number, + l = (max + min) / 2; + + if (max === min) { + h = s = 0; // achromatic + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + h = + max === r + ? (g - b) / d + (g < b ? 6 : 0) + : max === g + ? (b - r) / d + 2 + : (r - g) / d + 4; + h *= 60; + if (h < 0) h += 360; + } + + return [Math.round(h), fixFloat(s), fixFloat(l)]; + }; + + hsl2Object = (hsl: number[]) => { + const [h, s, l] = hsl; + return { h, s, l }; + }; + + hex2lch = (hex: string) => { + const rgb = this.hex2rgb(hex); + if (!rgb) return [0, 0, 0]; + return this.rgb2lch(...(rgb as [number, number, number])); + }; + + rgb2hex = (r: number, g: number, b: number) => { + return `#${((1 << 24) + (Math.round((r * 255) / 100) << 16) + (Math.round((g * 255) / 100) << 8) + Math.round((b * 255) / 100)).toString(16).slice(1)}`; + }; + + rgb2lch = (r: number, g: number, b: number) => { + const [x, y, z] = this.rgb2xyz(r, g, b); + const [_l, _a, _b] = this.xyz2lab(x, y, z); + return this.lab2lch(_l, _a, _b); + }; + + rgb2xyz = (r: number, g: number, b: number) => { + const [lR, lB, lG] = [r, g, b].map((v) => + v > 4.045 ? Math.pow((v + 5.5) / 105.5, 2.4) * 100 : v / 12.92 + ); + return matrix( + [lR, lB, lG], + [ + [0.4124564, 0.3575761, 0.1804375], + [0.2126729, 0.7151522, 0.072175], + [0.0193339, 0.119192, 0.9503041], + ] + ); + }; + + hsv2hsl = (h: number, s: number, v: number) => { + const l = ((200 - s) * v) / 200; + s = l === 0 || l === 100 ? 0 : (s * v) / 100 / (l < 50 ? l : 100 - l); + return [h, s * 100, l / 2]; + }; + + hsl2hsv = (h: number, s: number, l: number) => { + const v = l + (s * Math.min(l, 100 - l)) / 100; + s = v === 0 ? 0 : 2 * (1 - l / v); + return { h, s: s * 100, v }; + }; + + hslToRgb = (h: number, s: number, l: number) => { + let r, g, b; + if (s == 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = function hue2rgb(p: number, q: number, t: number) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + let q = l < 0.5 ? l * (1 + s) : l + s - l * s; + let p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + return [ + Math.min(255, Math.max(0, Math.round(r * 255))), + Math.min(255, Math.max(0, Math.round(g * 255))), + Math.min(255, Math.max(0, Math.round(b * 255))), + ]; + }; + + hsv2rgb = (h: number, s: number, v: number, a: number) => { + const rgbI = floor(h / 60); + // calculate rgb parts + const rgbF = (h / 60 - rgbI) & 1 ? h / 60 - rgbI : 1 - h / 60 - rgbI; + const rgbM = (v * (100 - s)) / 100; + const rgbN = (v * (100 - s * rgbF)) / 100; + const rgbA = a / 100; + + const [rgbR, rgbG, rgbB] = + rgbI === 5 + ? [v, rgbM, rgbN] + : rgbI === 4 + ? [rgbN, rgbM, v] + : rgbI === 3 + ? [rgbM, rgbN, v] + : rgbI === 2 + ? [rgbM, v, rgbN] + : rgbI === 1 + ? [rgbN, v, rgbM] + : [v, rgbN, rgbM]; + return [rgbR, rgbG, rgbB, rgbA]; + }; + + lch2hex = (l: number, c: number, h: number) => { + const [r, g, b] = this.lch2rgb(l, c, h); + return this.rgb2hex(r, g, b); + }; + + lch2rgb = (lchL: number, lchC: number, lchH: number) => { + const [labL, labA, labB] = this.lch2lab(lchL, lchC, lchH); + const [xyzX, xyzY, xyzZ] = this.lab2xyz(labL, labA, labB); + const [rgbR, rgbG, rgbB] = this.xyz2rgb(xyzX, xyzY, xyzZ); + return [rgbR, rgbG, rgbB]; + }; + + lch2lab = (lchL: number, lchC: number, lchH: number) => { + // convert to Lab a and b from the polar form + const [labA, labB] = [lchC * cosd(lchH), lchC * sind(lchH)]; + return [lchL, labA, labB]; + }; + + xyz2rgb = (xyzX: number, xyzY: number, xyzZ: number) => { + const [lrgbR, lrgbB, lrgbG] = matrix( + [xyzX, xyzY, xyzZ], + [ + [3.2404542, -1.5371385, -0.4985314], + [-0.969266, 1.8760108, 0.041556], + [0.0556434, -0.2040259, 1.0572252], + ] + ); + const [rgbR, rgbG, rgbB] = [lrgbR, lrgbB, lrgbG].map((v) => + v > 0.31308 ? 1.055 * pow(v / 100, 1 / 2.4) * 100 - 5.5 : 12.92 * v + ); + return [rgbR, rgbG, rgbB]; + }; + + xyz2lab = (x: number, y: number, z: number) => { + // calculate D50 XYZ from D65 XYZ + const [d50X, d50Y, d50Z] = matrix( + [x, y, z], + [ + [1.0478112, 0.0228866, -0.050127], + [0.0295424, 0.9904844, -0.0170491], + [-0.0092345, 0.0150436, 0.7521316], + ] + ); + // calculate f + const [f1, f2, f3] = [d50X / wd50X, d50Y / wd50Y, d50Z / wd50Z].map( + (value) => (value > epsilon ? cbrt(value) : (kappa * value + 16) / 116) + ); + return [116 * f2 - 16, 500 * (f1 - f2), 200 * (f2 - f3)]; + }; + + lab2xyz = (labL: number, labA: number, labB: number) => { + // compute f, starting with the luminance-related term + const f2 = (labL + 16) / 116; + const f1 = labA / 500 + f2; + const f3 = f2 - labB / 200; + // compute pre-scaled XYZ + const [initX, initY, initZ] = [ + pow(f1, 3) > epsilon ? pow(f1, 3) : (116 * f1 - 16) / kappa, + labL > kappa * epsilon ? pow((labL + 16) / 116, 3) : labL / kappa, + pow(f3, 3) > epsilon ? pow(f3, 3) : (116 * f3 - 16) / kappa, + ]; + const [xyzX, xyzY, xyzZ] = matrix( + // compute XYZ by scaling pre-scaled XYZ by reference white + [initX * wd50X, initY * wd50Y, initZ * wd50Z], + // calculate D65 XYZ from D50 XYZ + [ + [0.9555766, -0.0230393, 0.0631636], + [-0.0282895, 1.0099416, 0.0210077], + [0.0122982, -0.020483, 1.3299098], + ] + ); + return [xyzX, xyzY, xyzZ]; + }; + + lab2lch = (labL: number, labA: number, labB: number) => { + return [ + labL, + sqrt(pow(labA, 2) + pow(labB, 2)), // convert to chroma + rad2deg(atan2(labB, labA)), // convert to hue, in degrees + ]; + }; + + canBeWhite = (hex: string) => { + const [h, s, l] = this.hex2hsl(hex); + return l < 55; + }; +} + +const c = new Convert(); + +export const renderHsl = (hsl: number[]) => + `${hsl[0].toFixed()}, ${(hsl[1] * -1).toFixed()}%, ${(hsl[2] / 100).toFixed()}%`; + +export const hex2cmyk = (hex: string) => { + return ChromaJS(hex).cmyk(); +}; +export const renderCmyk = (cmyk: number[]) => + cmyk.map((v) => (v * 100).toFixed()).join(", "); + +import ChromaJS from "chroma-js"; + +const intLch2hex = (l: number, c: number, h: number) => + ChromaJS.lch(l, c, h).hex(); + +const interpolate = ( + start: number, + end: number, + step: number, + maxStep: number +) => { + let diff = end - start; + if (Math.abs(diff) > 180) { + diff = -(Math.sign(diff) * (360 - Math.abs(diff))); + } + return (start + (diff * step) / maxStep) % 360; +}; + +export const interpolateColor = ( + color: [number, number, number], + interpolateBy: "l" | "c" | "h", + steps: number, + endValue: number +): string[] => { + const [l, c, h] = color; + const interpolatedColors: string[] = []; + + for (let i = 0; i < steps; i++) { + let currentL = l, + currentC = c, + currentH = h; + + switch (interpolateBy) { + case "l": + currentL = interpolate(l, endValue, i, steps - 1); + break; + case "c": + currentC = interpolate(c, endValue, i, steps - 1); // Grey color + break; + case "h": + currentH = interpolate(h, endValue, i, steps - 1); + break; + } + + interpolatedColors.push(render_lch2hex(currentL, currentC, currentH)); + } + + return interpolatedColors; +}; + +const render_lch2hex = (l: number, c: number, h: number) => + ChromaJS.lch(l, c, h).hex(); + +export const interpolateTwoColors = ( + c1: [number, number, number], + c2: [number, number, number], + steps: number +) => { + let interpolatedColorArray = []; + + for (let i = 0; i < steps; i++) { + interpolatedColorArray.push( + render_lch2hex( + interpolate(c1[0], c2[0], i, steps - 1), // interpolate lightness + interpolate(c1[1], c2[1], i, steps - 1), // interpolate chroma + interpolate(c1[2], c2[2], i, steps - 1) // interpolate hue + ) + ); + } + + return interpolatedColorArray; +}; + +export const getInterpolateShades = ( + startColor: string, + endColor: string, + shades: number +) => { + const [l1, c1, h1] = c.hex2lch(startColor) ?? [0, 0, 0]; + const [l2, c2, h2] = c.hex2lch(endColor) ?? [0, 0, 0]; + + return interpolateTwoColors([l1, c1, h1], [l2, c2, h2], shades); +}; + +interface ShadingConfig { + color: string; + shades: number; + start: number; + end: number; + easeMethod: EaseMethod; + includeBase: boolean; + space: "full-gamut" | "other"; // You can define other spaces if needed +} + +type EaseMethod = + | "ease-in" + | "ease-out" + | "ease-in-out" + | "linear" + | "custom" + | "circ-in" + | "circ-out"; + +const getEasing = ( + start: number, + end: number, + shades: number, + easeMethod: EaseMethod, + currentEasing?: number[] +): number[] => { + let _easing: number[] = []; + if (easeMethod === "custom") { + if (!currentEasing || !currentEasing.length) { + _easing = new Array(shades).fill(start); + } else { + _easing = currentEasing; + } + } else { + for (let i = 0; i < shades; i++) { + switch (easeMethod) { + case "ease-in": + _easing.push(start + (end - start) * Math.pow(i / (shades - 1), 2)); + break; + case "ease-out": + _easing.push( + start + (end - start) * (1 - Math.pow(1 - i / (shades - 1), 2)) + ); + break; + case "ease-in-out": + _easing.push( + start + + (end - start) * + (-Math.cos((i / (shades - 1)) * Math.PI) / 2 + 0.5) + ); + break; + case "circ-in": + _easing.push( + start + + (end - start) * + (1 - Math.sqrt(1 - (i / (shades - 1)) * (i / (shades - 1)))) + ); + break; + case "circ-out": + _easing.push( + start + + (end - start) * + Math.sqrt(1 - (i / (shades - 1) - 1) * (i / (shades - 1) - 1)) + ); + break; + case "linear": + default: + _easing.push(start + (end - start) * (i / (shades - 1))); + } + } + } + return _easing; +}; + +export const createShading = (config: ShadingConfig) => { + const { color, shades, start, end, easeMethod, includeBase, space } = config; + + const easingValues = getEasing(start, end, shades, easeMethod); + + const baseColorLCH = c.hex2lch(color); + if (!baseColorLCH) { + throw new Error("Invalid base color provided"); + } + + const baseLightness = baseColorLCH[0]; + + const shadeColors = easingValues.map((easedValue) => { + let newLightness; + if (baseLightness > 50) { + // Color is light, interpolate from baseLightness to 100 + newLightness = baseLightness - (baseLightness - 0) * (easedValue / 100); + } else { + // Color is dark, interpolate from baseLightness to 0 + newLightness = baseLightness + (100 - baseLightness) * (easedValue / 100); + } + + const newColorLCH: [number, number, number] = [ + newLightness, + baseColorLCH[1], + baseColorLCH[2], + ]; + return c.lch2hex(newColorLCH[0], newColorLCH[1], newColorLCH[2]); + }); + + if (includeBase) { + shadeColors.push(color); + } + + return { + shades: shadeColors, + }; +}; From 089bda43dc8336e014d2313c5393e65bf86a5e02 Mon Sep 17 00:00:00 2001 From: Thijs Zijdel Date: Tue, 6 Aug 2024 13:15:49 +0200 Subject: [PATCH 04/24] Improved Ids module & fixed some styling bugs - Fixed playground styling - Extended the ids module - Renamed Yaml folder in preparation for more json tools --- package.json | 1 + src/App.tsx | 2 +- src/Features/ids/Ids.tsx | 74 ++++++++++++------- .../{yaml-json => json-yaml}/Yaml.tsx | 27 +++++-- src/Features/reactPlayground/Playground.tsx | 3 +- 5 files changed, 71 insertions(+), 36 deletions(-) rename src/Features/{yaml-json => json-yaml}/Yaml.tsx (70%) diff --git a/package.json b/package.json index 1db7c56..5a0dce3 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "lesspass": "^9.2.0", "lorem-ipsum": "^2.0.8", "monaco-themes": "^0.4.4", + "nanoid": "^5.0.7", "polished": "^4.3.1", "preact": "^10.20.1", "qrcode": "^1.5.3", diff --git a/src/App.tsx b/src/App.tsx index 8b4287a..e6c64ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,7 +44,7 @@ const Colors = loadable(() => import("./Features/colors/Colors")); const RegexTester = loadable(() => import("./Features/regex/RegexTester")); const TextDiff = loadable(() => import("./Features/text/TextDiff")); const Markdown = loadable(() => import("./Features/markdown/Markdown")); -const YamlJson = loadable(() => import("./Features/yaml-json/Yaml")); +const YamlJson = loadable(() => import("./Features/json-yaml/Yaml")); const Pastebin = loadable(() => import("./Features/pastebin/Pastebin")); const Repl = loadable(() => import("./Features/repl/Repl")); const Image = loadable(() => import("./Features/image/Image")); diff --git a/src/Features/ids/Ids.tsx b/src/Features/ids/Ids.tsx index d61449e..1690d69 100644 --- a/src/Features/ids/Ids.tsx +++ b/src/Features/ids/Ids.tsx @@ -8,33 +8,43 @@ import { Textarea, } from "@mantine/core"; import { useInputState } from "@mantine/hooks"; -import { useState } from "react"; -import { v1, v3, v4, v5 } from "uuid"; +import { useCallback, useEffect, useState } from "react"; +import { v4 } from "uuid"; +import { nanoid, customAlphabet } from "nanoid"; import { Copy } from "../../Components/Copy"; -type Versions = "v1" | "v3" | "v4" | "v5"; +type Generator = "v4" | "nanoid" | "custom"; export default function Ids() { const [ids, setIds] = useState([]); const [count, setCount] = useInputState(5); - const [version, setVersion] = useInputState("v4"); + const [generator, setGenerator] = useInputState("v4"); + const [custom, setCustom] = useInputState<{ + alphabet: string; + length: number; + }>({ + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + length: 16, + }); - const generateIds = () => { - switch (version) { - case "v1": - // setIds(Array.from({ length: count }, () => v1())); - break; - case "v3": - // setIds(Array.from({ length: count }, () => v3())); - break; - case "v4": - setIds(Array.from({ length: count }, () => v4())); - break; - case "v5": - // setIds(Array.from({ length: count }, () => v5())); - break; - } - }; + const generateIds = useCallback(() => { + const newIds = Array.from({ length: count }, () => { + switch (generator) { + case "v4": + return v4(); + case "nanoid": + return nanoid(); + case "custom": + return customAlphabet(custom.alphabet)(custom.length); + } + }); + setIds(newIds); + }, [count, generator, custom.length, custom.alphabet]); + + // Set initial IDs + useEffect(() => { + generateIds(); + }, [generator]); return ( @@ -46,14 +56,26 @@ export default function Ids() { onChange={(e) => setCount(Number(e))} />