Add Hex prompt

This commit is contained in:
Jeremy Thomas 2024-06-25 16:20:07 +01:00
parent 2cc6593269
commit cb2de3a2a2
5 changed files with 251 additions and 46 deletions

View File

@ -49,15 +49,20 @@ function App() {
getVar: (id) => { getVar: (id) => {
return context.cssvars[id]; return context.cssvars[id];
}, },
updateVar: (id, newValue, unit) => { updateVar: (id, newValue) => {
setContext((context) => {
const { start, unit } = context.cssvars[id];
const computedValue = `${newValue}${unit}`; const computedValue = `${newValue}${unit}`;
if (start === newValue) {
document.documentElement.style.removeProperty(`--bulma-${id}`);
} else {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
`--bulma-${id}`, `--bulma-${id}`,
computedValue, computedValue,
); );
}
setContext((context) => {
return { return {
...context, ...context,
cssvars: { cssvars: {

View File

@ -1,4 +1,4 @@
import { useContext } from "react"; import { useContext, useEffect, useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import Slider from "./Slider"; import Slider from "./Slider";
@ -6,12 +6,103 @@ import Slider from "./Slider";
import cn from "./Color.module.css"; import cn from "./Color.module.css";
import { CustomizerContext } from "../App"; import { CustomizerContext } from "../App";
function Color({ color }) { function hslToHex(h, s, l) {
// const [hue, setHue] = useState(h.start); s /= 100;
// const [saturation, setSaturation] = useState(s.start); l /= 100;
// const [lightness, setLightness] = useState(l.start);
let c = (1 - Math.abs(2 * l - 1)) * s;
let x = c * (1 - Math.abs(((h / 60) % 2) - 1));
let m = l - c / 2;
let r = 0,
g = 0,
b = 0;
if (0 <= h && h < 60) {
r = c;
g = x;
b = 0;
} else if (60 <= h && h < 120) {
r = x;
g = c;
b = 0;
} else if (120 <= h && h < 180) {
r = 0;
g = c;
b = x;
} else if (180 <= h && h < 240) {
r = 0;
g = x;
b = c;
} else if (240 <= h && h < 300) {
r = x;
g = 0;
b = c;
} else if (300 <= h && h < 360) {
r = c;
g = 0;
b = x;
}
r = Math.round((r + m) * 255);
g = Math.round((g + m) * 255);
b = Math.round((b + m) * 255);
return `${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
}
function hexToHsl(hex) {
// Remove the hash at the start if it's there
hex = hex.replace(/^#/, "");
// Parse the hex values
let r = parseInt(hex.slice(0, 2), 16);
let g = parseInt(hex.slice(2, 4), 16);
let b = parseInt(hex.slice(4, 6), 16);
// Convert the RGB values to the range [0, 1]
r /= 255;
g /= 255;
b /= 255;
// Find the maximum and minimum values to get the lightness
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h,
s,
l = (max + min) / 2;
if (max === min) {
h = s = 0; // Achromatic
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h *= 60;
}
return {
hue: Math.round(h),
saturation: Math.round(s * 100),
lightness: Math.round(l * 100),
};
}
function Color({ color }) {
const { cssvars, updateVar } = useContext(CustomizerContext); const { cssvars, updateVar } = useContext(CustomizerContext);
const [hexValue, setHexValue] = useState("");
const hName = `${color}-h`; const hName = `${color}-h`;
const sName = `${color}-s`; const sName = `${color}-s`;
const lName = `${color}-l`; const lName = `${color}-l`;
@ -28,14 +119,74 @@ function Color({ color }) {
const handleReset = (event) => { const handleReset = (event) => {
event.preventDefault(); event.preventDefault();
updateVar(h.id, h.start, h.unit); updateVar(h.id, h.start);
updateVar(s.id, s.start, s.unit); updateVar(s.id, s.start);
updateVar(l.id, l.start, l.unit); updateVar(l.id, l.start);
// document.documentElement.style.removeProperty(`--bulma-${hName}`);
// document.documentElement.style.removeProperty(`--bulma-${sName}`);
// document.documentElement.style.removeProperty(`--bulma-${lName}`);
}; };
const handleHexInput = (event) => {
event.preventDefault();
let value = window.prompt("Enter a Hexadecimal value (e.g. 00d1b2)");
if (value.startsWith("#")) {
value = value.replace(/^#/, "");
}
const hexPattern = /^([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;
if (!hexPattern.test(value) || value.length > 6) {
window.prompt("That is not a valid Hexadecimal value. Please try again.");
return;
}
if (value.length === 3) {
value = value[0] + value[0] + value[1] + value[1] + value[2] + value[2];
}
const { hue, saturation, lightness } = hexToHsl(value);
updateVar(h.id, hue);
updateVar(s.id, saturation);
updateVar(l.id, lightness);
};
const handleHexChange = (event) => {
let value = event.target.value;
if (value.startsWith("#")) {
value = value.replace(/^#/, "");
}
setHexValue(value);
const hexPattern = /^([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;
if (!hexPattern.test(value) || value.length < 6) {
return;
}
const { hue, saturation, lightness } = hexToHsl(value);
updateVar(h.id, hue);
updateVar(s.id, saturation);
updateVar(l.id, lightness);
};
const handleInputChange = (event, cssvar) => {
let value = event.target.value;
updateVar(cssvar.id, value);
};
useEffect(() => {
if (!h) {
return;
}
const hex = hslToHex(h.current, s.current, l.current);
setHexValue(hex);
}, [h, s, l]);
if (!h) { if (!h) {
return; return;
} }
@ -51,52 +202,80 @@ function Color({ color }) {
<p>{name}</p> <p>{name}</p>
</div> </div>
<button <button className="button is-small" onClick={handleHexInput}>
className="button is-small" Enter a Hex code
onClick={handleReset}
disabled={isDisabled}
>
Reset
</button> </button>
<div className="is-hidden field has-addons">
<p className="control">
<span className="button is-static">#</span>
</p>
<p className="control">
<input
className="input"
type="text"
value={hexValue}
onChange={handleHexChange}
/>
</p>
<p className="control">
<span className="button is-icon">Copy</span>
</p>
</div>
</div> </div>
<div className={cn.lines}> <div className={cn.lines}>
<div className={cn.line}> <div className={cn.line}>
<p>Hue</p> <p>Hue</p>
<Slider id={hName} kind="hue" color={color} /> <Slider id={hName} kind="hue" color={color} />
<p> <p className={cn.form}>
<code> <input
{h.current} type="text"
{h.unit} className="input"
</code> value={h.current}
onChange={(e) => handleInputChange(e, h)}
size="3"
/>
<span>{h.unit}</span>
</p> </p>
</div> </div>
<div className={cn.line}> <div className={cn.line}>
<p>Saturation</p> <p>Saturation</p>
<Slider id={sName} kind="saturation" color={color} /> <Slider id={sName} kind="saturation" color={color} />
<p> <p className={cn.form}>
<code> <input
{s.current} type="text"
{s.unit} className="input"
</code> value={s.current}
onChange={(e) => handleInputChange(e, s)}
size="3"
/>
<span>{s.unit}</span>
</p> </p>
</div> </div>
<div className={cn.line}> <div className={cn.line}>
<p>Lightness</p> <p>Lightness</p>
<Slider id={lName} kind="lightness" color={color} /> <Slider id={lName} kind="lightness" color={color} />
<p> <p className={cn.form}>
<code> <input
{l.current} type="text"
{l.unit} className="input"
</code> value={l.current}
onChange={(e) => handleInputChange(e, l)}
size="3"
/>
<span>{l.unit}</span>
</p> </p>
</div> </div>
</div> </div>
<div className={cn.side}> <div className={cn.side}>
<button className={`button is-${color}`}>{name}</button> <button className={`button is-${color}`}>{name}</button>
<button className="button" onClick={handleReset} disabled={isDisabled}>
Reset
</button>
</div> </div>
</div> </div>
); );

View File

@ -47,3 +47,24 @@
color: var(--bulma-text-strong); color: var(--bulma-text-strong);
width: 6rem; width: 6rem;
} }
.form {
display: flex;
align-items: center;
font-family: var(--bulma-family-code);
gap: 0.25em;
}
.form input {
font-family: inherit;
font-size: inherit;
padding: 0.25em;
height: auto;
border-radius: 0.25em;
width: 3em;
padding: 0 0.25em;
}
.form span {
opacity: 0.5;
}

View File

@ -27,14 +27,14 @@ const valueToX = (value, width, min, max) => {
function Slider({ id, color, kind }) { function Slider({ id, color, kind }) {
const { cssvars, updateVar } = useContext(CustomizerContext); const { cssvars, updateVar } = useContext(CustomizerContext);
const { start, unit, current } = cssvars[id]; const { start, current } = cssvars[id];
const [min, max] = RANGES[kind]; const [min, max] = RANGES[kind];
const sliderRef = useRef(null); const sliderRef = useRef(null);
const handleRef = useRef(null); const handleRef = useRef(null);
const [isMoving, setMoving] = useState(false); const [isMoving, setMoving] = useState(false);
const [x, setX] = useState(valueToX(start, 240, min, max)); const [x, setX] = useState(valueToX(start, 360, min, max));
const handleMouseDown = (event) => { const handleMouseDown = (event) => {
setMoving(true); setMoving(true);
@ -69,11 +69,11 @@ function Slider({ id, color, kind }) {
const slider = sliderRef.current; const slider = sliderRef.current;
const sliderRect = slider.getBoundingClientRect(); const sliderRect = slider.getBoundingClientRect();
const final = xToValue(x, sliderRect.width, min, max); const final = xToValue(x, sliderRect.width, min, max);
updateVar(id, final, unit); updateVar(id, final);
}, [id, min, max, updateVar, unit, x]); }, [id, min, max, updateVar, x]);
useEffect(() => { useEffect(() => {
const newX = valueToX(current, 240, min, max); const newX = valueToX(current, 360, min, max);
setX(newX); setX(newX);
}, [min, max, current]); }, [min, max, current]);

View File

@ -1,6 +1,6 @@
.main { .main {
position: relative; position: relative;
width: 15rem; width: 360px;
padding: 0.375rem 0; padding: 0.375rem 0;
cursor: grab; cursor: grab;
} }