Skip to content

Commit

Permalink
feat: Add alpha channel (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hawkbat authored Jul 22, 2021
1 parent 7122bd8 commit aa7c289
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 38 deletions.
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ If the default colors don't fit your project, you can always change them.
| hideHEX | `bool` | false | Hide HEX input. |
| hideRGB | `bool` | false | Hide RGB input. |
| hideHSV | `bool` | false | Hide HSV input. |
| alpha | `bool` | false | Enable alpha channel. |
| dark | `bool` | false | Color theme. |

[1]: #color
Expand Down Expand Up @@ -148,19 +149,21 @@ If the default colors don't fit your project, you can always change them.

### `ColorRGB`

| Field | Type |
| ----- | -------- |
| r | `number` |
| g | `number` |
| b | `number` |
| Field | Type |
| ----- | ----------------------- |
| r | `number` |
| g | `number` |
| b | `number` |
| a | `number` \| `undefined` |

### `ColorHSV`

| Field | Type |
| ----- | -------- |
| h | `number` |
| s | `number` |
| v | `number` |
| Field | Type |
| ----- | ----------------------- |
| h | `number` |
| s | `number` |
| v | `number` |
| a | `number` \| `undefined` |

<hr />

Expand Down
43 changes: 43 additions & 0 deletions src/components/Alpha.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useMemo } from "react";
import { AlphaProps } from "../interfaces/Alpha.interface";
import { getAlphaCoordinates } from "../utils/coordinates.util";
import { toColor } from "../utils/toColor.util";
import { Interactive } from "./Interactive.component";

export const Alpha = ({ width, color, onChange }: AlphaProps): JSX.Element => {
const position = useMemo(() => {
const x = getAlphaCoordinates(color.hsv.a ?? 1, width);

return x;
}, [color.hsv.a, width]);

const updateColor = (x: number): void => {
onChange(toColor("hsv", { ...color.hsv, a: x / width }));
};

return (
<Interactive
className="rcp-alpha"
onChange={updateColor}
style={{
background: `linear-gradient(to right, rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, 0), rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, 1)) top left / auto auto,
conic-gradient(#666 0.25turn, #999 0.25turn 0.5turn, #666 0.5turn 0.75turn, #999 0.75turn) top left / 12px 12px
repeat`,
}}
>
<div
className="rcp-alpha-cursor"
style={{
left: position,
background: `linear-gradient(to right, rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${
color.rgb.a ?? 1
}), rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a ?? 1})) top left / auto auto,
conic-gradient(#666 0.25turn, #999 0.25turn 0.5turn, #666 0.5turn 0.75turn, #999 0.75turn) ${
-position - 2
}px 2px / 12px 12px
repeat`,
}}
/>
</Interactive>
);
};
5 changes: 4 additions & 1 deletion src/components/ColorPicker.component.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { ColorPickerProps } from "../interfaces/ColorPicker.interface";
import { Alpha } from "./Alpha.component";
import { Saturation } from "./Saturation.component";
import { Hue } from "./Hue.component";
import { Fields } from "./Fields.component";
Expand All @@ -12,13 +13,15 @@ export const ColorPicker = ({
hideHEX = false,
hideRGB = false,
hideHSV = false,
alpha = false,
dark = false,
}: ColorPickerProps): JSX.Element => (
<div className={`rcp ${dark ? "rcp-dark" : "rcp-light"}`} style={{ width }}>
<Saturation width={width} height={height} color={color} onChange={onChange} />
<div className="rcp-body">
<Hue width={width - 40} color={color} onChange={onChange} />
<Fields color={color} hideHEX={hideHEX} hideRGB={hideRGB} hideHSV={hideHSV} onChange={onChange} />
{alpha && <Alpha width={width - 40} color={color} onChange={onChange} />}
<Fields color={color} hideHEX={hideHEX} hideRGB={hideRGB} hideHSV={hideHSV} alpha={alpha} onChange={onChange} />
</div>
</div>
);
34 changes: 21 additions & 13 deletions src/components/Fields.component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback, useState, useEffect } from "react";
import { UpperFloorProps, LowerFloorProps, FieldsProps } from "../interfaces/Fields.interface";
import { toHsv, toRgb } from "../utils/convert.util";
import { roundFloat } from "../utils/roundFloat.util";
import { toColor } from "../utils/toColor.util";
import { validHex } from "../utils/validate.util";

Expand Down Expand Up @@ -42,17 +43,24 @@ const UpperFloor = ({ color, hideHEX, onChange }: UpperFloorProps): JSX.Element
);
};

const LowerFloor = ({ color, hideRGB, hideHSV, onChange }: LowerFloorProps): JSX.Element => {
const LowerFloor = ({ color, hideRGB, hideHSV, alpha, onChange }: LowerFloorProps): JSX.Element => {
const getValueRGB = useCallback(
() => ({ value: `${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}`, inputted: false }),
[color.rgb]
() => ({
value: `${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}${
alpha && color.rgb.a !== undefined ? `, ${roundFloat(color.rgb.a, 3)}` : ""
}`,
inputted: false,
}),
[color.rgb, alpha]
);
const getValueHSV = useCallback(
() => ({
value: `${Math.round(color.hsv.h)}°, ${Math.round(color.hsv.s)}%, ${Math.round(color.hsv.v)}%`,
value: `${Math.round(color.hsv.h)}°, ${Math.round(color.hsv.s)}%, ${Math.round(color.hsv.v)}%${
alpha && color.hsv.a !== undefined ? `, ${roundFloat(color.hsv.a, 3)}` : ""
}`,
inputted: false,
}),
[color.hsv]
[color.hsv, alpha]
);

const [valueRGB, setValueRGB] = useState(getValueRGB);
Expand All @@ -71,10 +79,10 @@ const LowerFloor = ({ color, hideRGB, hideHSV, onChange }: LowerFloorProps): JSX
}, [valueHSV.inputted, getValueHSV]);

const changeRGB = (e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value.match(/\d+/g);
const value = e.target.value.match(/\d+(?:\.\d+)?/g);

if (value && value.length === 3) {
const rgb = toRgb(value.slice(0, 3));
if (value && (value.length === 3 || (alpha && value.length === 4))) {
const rgb = toRgb(value);

onChange(toColor("rgb", rgb));
}
Expand All @@ -83,10 +91,10 @@ const LowerFloor = ({ color, hideRGB, hideHSV, onChange }: LowerFloorProps): JSX
};

const changeHSB = (e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value.match(/\d+/g);
const value = e.target.value.match(/\d+(?:\.\d+)?/g);

if (value && value.length === 3) {
const hsb = toHsv(value.slice(0, 3));
if (value && (value.length === 3 || (alpha && value.length === 4))) {
const hsb = toHsv(value);

onChange(toColor("hsv", hsb));
}
Expand Down Expand Up @@ -128,12 +136,12 @@ const LowerFloor = ({ color, hideRGB, hideHSV, onChange }: LowerFloorProps): JSX
);
};

export const Fields = ({ color, hideHEX, hideRGB, hideHSV, onChange }: FieldsProps): JSX.Element => {
export const Fields = ({ color, hideHEX, hideRGB, hideHSV, alpha, onChange }: FieldsProps): JSX.Element => {
return (
<>
{(!hideHEX || !hideRGB || !hideHSV) && (
<div className="rcp-fields">
<LowerFloor color={color} hideRGB={hideRGB} hideHSV={hideHSV} onChange={onChange} />
<LowerFloor color={color} hideRGB={hideRGB} hideHSV={hideHSV} alpha={alpha} onChange={onChange} />
<UpperFloor color={color} hideHEX={hideHEX} onChange={onChange} />
</div>
)}
Expand Down
25 changes: 25 additions & 0 deletions src/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@
transform: translate(-10px, -4px);
}

.rcp-alpha {
position: relative;

width: 100%;
height: 12px;

border-radius: 10px;

user-select: none;
}

.rcp-alpha-cursor {
position: absolute;

width: 20px;
height: 20px;

border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px;
box-sizing: border-box;

transform: translate(-10px, -4px);
}

.rcp-fields {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useColor.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { toColor } from "../utils/toColor.util";
/**
* Returns a stateful [Color](https://github.com/Wondermarin/react-color-palette#color), and a function to update it.
* @param model HEX.
* @param initColor Color in HEX model (3-6 digit) or [HTML Color Names](https://www.w3.org/wiki/CSS/Properties/color/keywords).
* @param initColor Color in HEX model (3-6 digit or 4-8 digit with alpha) or [HTML Color Names](https://www.w3.org/wiki/CSS/Properties/color/keywords).
*/
export function useColor(model: "hex", initColor: Color["hex"]): [Color, React.Dispatch<React.SetStateAction<Color>>];
/**
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/Alpha.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Color } from "./Color.interface";

export interface AlphaProps {
readonly width: number;
readonly color: Color;
readonly onChange: React.Dispatch<React.SetStateAction<Color>>;
}
2 changes: 2 additions & 0 deletions src/interfaces/Color.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ interface ColorRGB {
readonly r: number;
readonly g: number;
readonly b: number;
readonly a?: number;
}

interface ColorHSV {
readonly h: number;
readonly s: number;
readonly v: number;
readonly a?: number;
}
4 changes: 4 additions & 0 deletions src/interfaces/ColorPicker.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export interface ColorPickerProps {
* Hide HSV field.
*/
readonly hideHSV?: boolean;
/**
* Enable alpha channel.
*/
readonly alpha?: boolean;
/**
* Color theme.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/Fields.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface LowerFloorProps {
readonly color: Color;
readonly hideRGB: boolean;
readonly hideHSV: boolean;
readonly alpha: boolean;
readonly onChange: React.Dispatch<React.SetStateAction<Color>>;
}

Expand All @@ -18,5 +19,6 @@ export interface FieldsProps {
readonly hideHEX: boolean;
readonly hideRGB: boolean;
readonly hideHSV: boolean;
readonly alpha: boolean;
readonly onChange: React.Dispatch<React.SetStateAction<Color>>;
}
38 changes: 25 additions & 13 deletions src/utils/convert.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Color } from "../interfaces/Color.interface";
import { clamp } from "./clamp.util";

export function toHex(value: string): Color["hex"] {
if (!value.startsWith("#") || value.length === 4) {
if (!value.startsWith("#")) {
const ctx = document.createElement("canvas").getContext("2d");

if (!ctx) {
Expand All @@ -12,23 +12,30 @@ export function toHex(value: string): Color["hex"] {
ctx.fillStyle = value;

return ctx.fillStyle;
} else if (value.length === 7) {
} else if (value.length === 4 || value.length === 5) {
value = value
.split("")
.map((v, i) => (i === 0 ? "#" : v + v))
.join("");

return value;
} else if (value.length === 7 || value.length === 9) {
return value;
}

return "#000000";
}

export function toRgb(value: string[]): Color["rgb"] {
const [r, g, b] = value.map((v) => clamp(Number(v), 255, 0));
const [r, g, b, a] = value.map((v, i) => (i < 3 ? clamp(Number(v), 255, 0) : clamp(Number(v), 1, 0)));

return { r, g, b };
return { r, g, b, a };
}

export function toHsv(value: string[]): Color["hsv"] {
const [h, s, v] = value.map((v, i) => clamp(Number(v), i ? 100 : 360, 0));
const [h, s, v, a] = value.map((v, i) => clamp(Number(v), i === 0 ? 360 : i < 3 ? 100 : 1, 0));

return { h, s, v };
return { h, s, v, a };
}

export function hex2rgb(hex: Color["hex"]): Color["rgb"] {
Expand All @@ -37,11 +44,14 @@ export function hex2rgb(hex: Color["hex"]): Color["rgb"] {
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
let a = parseInt(hex.slice(6, 8), 16) || undefined;

if (a) a /= 255;

return { r, g, b };
return { r, g, b, a };
}

export function rgb2hsv({ r, g, b }: Color["rgb"]): Color["hsv"] {
export function rgb2hsv({ r, g, b, a }: Color["rgb"]): Color["hsv"] {
r /= 255;
g /= 255;
b /= 255;
Expand All @@ -53,10 +63,10 @@ export function rgb2hsv({ r, g, b }: Color["rgb"]): Color["hsv"] {
const s = max ? (d / max) * 100 : 0;
const v = max * 100;

return { h, s, v };
return { h, s, v, a };
}

export function hsv2rgb({ h, s, v }: Color["hsv"]): Color["rgb"] {
export function hsv2rgb({ h, s, v, a }: Color["hsv"]): Color["rgb"] {
s /= 100;
v /= 100;

Expand All @@ -71,11 +81,13 @@ export function hsv2rgb({ h, s, v }: Color["hsv"]): Color["rgb"] {
const g = Math.round([t, v, v, q, p, p][index] * 255);
const b = Math.round([p, p, t, v, v, q][index] * 255);

return { r, g, b };
return { r, g, b, a };
}

export function rgb2hex({ r, g, b }: Color["rgb"]): Color["hex"] {
const hex = [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
export function rgb2hex({ r, g, b, a }: Color["rgb"]): Color["hex"] {
const hex = [r, g, b, a]
.map((v, i) => (v !== undefined ? (i < 3 ? v : Math.round(v * 255)).toString(16).padStart(2, "0") : ""))
.join("");

return `#${hex}`;
}
6 changes: 6 additions & 0 deletions src/utils/coordinates.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ export function getHueCoordinates(h: number, width: number): number {

return x;
}

export function getAlphaCoordinates(a: number, width: number): number {
const x = a * width;

return x;
}
3 changes: 3 additions & 0 deletions src/utils/roundFloat.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function roundFloat(n: number, decimalPlaces: number): number {
return Math.round(n * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces);
}

1 comment on commit aa7c289

@vercel
Copy link

@vercel vercel bot commented on aa7c289 Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.