Completed picker / history. Needs styling refactor.
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
|
|||||||
@@ -53,6 +53,19 @@ Cypress.Commands.add("disableTransitions", () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Flag to disable Framer Motion
|
||||||
|
if (window.framerMotionTestOverride) return;
|
||||||
|
window.framerMotionTestOverride = true;
|
||||||
|
window.originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||||
|
|
||||||
|
window.requestAnimationFrame = (callback) => {
|
||||||
|
return window.setTimeout(() => {
|
||||||
|
if (callback) callback(0);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.setAttribute("data-cy-animations-disabled", "true");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,5 +75,17 @@ Cypress.Commands.add("enableTransitions", () => {
|
|||||||
if (styleElement) {
|
if (styleElement) {
|
||||||
styleElement.remove();
|
styleElement.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove flags for Framer Motion
|
||||||
|
if (window.framerMotionTestOverride) {
|
||||||
|
window.framerMotionTestOverride = false;
|
||||||
|
|
||||||
|
if (window.originalRequestAnimationFrame) {
|
||||||
|
window.requestAnimationFrame = window.originalRequestAnimationFrame;
|
||||||
|
delete window.originalRequestAnimationFrame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeAttribute("data-cy-animations-disabled");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Luminance</title>
|
<title>Luminance</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
+27
-30
@@ -1,17 +1,26 @@
|
|||||||
.appWrapper {
|
.appWrapper {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 1200px;
|
||||||
max-width: 1200px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
box-shadow: 0 0 40px #7a7a7a;
|
box-shadow: 0 0 40px #7a7a7a;
|
||||||
border-left: 2px solid #7a7a7a;
|
border-left: 2px solid #7a7a7a;
|
||||||
border-right: 2px solid #7a7a7a;
|
border-right: 2px solid #7a7a7a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainLayout {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"header header"
|
||||||
|
"picker palette";
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
grid-template-rows: 76px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appHeader {
|
.appHeader {
|
||||||
height: 40px;
|
grid-area: header;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
border-bottom: 2px solid #7a7a7a;
|
border-bottom: 2px solid #7a7a7a;
|
||||||
@@ -39,48 +48,36 @@
|
|||||||
color: #7a7a7a;
|
color: #7a7a7a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileContent,
|
|
||||||
.mainContent,
|
|
||||||
.tabWrapper,
|
|
||||||
.tabWrapper .tab {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabWrapper {
|
|
||||||
/* hide scrollbar */
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appWrapper {
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobileContent {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainContent {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 2fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firstZone {
|
.firstZone {
|
||||||
|
grid-area: picker;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-right: 2px solid #7a7a7a;
|
border-right: 2px solid #7a7a7a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondZone {
|
.secondZone {
|
||||||
padding: 40px;
|
min-width: 0;
|
||||||
|
grid-area: palette;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.colorHistoryWrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 2px solid #7a7a7a;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.colorPickerWrapper {
|
.colorPickerWrapper {
|
||||||
border-bottom: 2px solid #7a7a7a;
|
border-bottom: 2px solid #7a7a7a;
|
||||||
|
padding: 20px 40px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorValuesWrapper {
|
.colorValuesWrapper {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorHistoryWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
.paletteEditorWrapper {
|
.paletteEditorWrapper {
|
||||||
|
|||||||
+14
-12
@@ -3,6 +3,7 @@ import { useState } from "react";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Color } from "colorlib";
|
import { Color } from "colorlib";
|
||||||
|
|
||||||
|
import ColorHistory from "@/components/ColorHistory/ColorHistory";
|
||||||
import ColorPicker from "@/components/ColorPicker/ColorPicker";
|
import ColorPicker from "@/components/ColorPicker/ColorPicker";
|
||||||
import ColorValues from "@/components/ColorValues/ColorValues";
|
import ColorValues from "@/components/ColorValues/ColorValues";
|
||||||
import { LeftMenu, RightMenu } from "@/components/SideMenu";
|
import { LeftMenu, RightMenu } from "@/components/SideMenu";
|
||||||
@@ -193,10 +194,6 @@ function MobileContent({
|
|||||||
|
|
||||||
// Desktop Layout Components
|
// Desktop Layout Components
|
||||||
|
|
||||||
function TitleZone() {
|
|
||||||
return <section className={styles.TitleZone}></section>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FirstZone() {
|
function FirstZone() {
|
||||||
const { selectedColor, selectedColorActions } = useSelectedColor();
|
const { selectedColor, selectedColorActions } = useSelectedColor();
|
||||||
|
|
||||||
@@ -205,7 +202,6 @@ function FirstZone() {
|
|||||||
<div className={styles.colorPickerWrapper} aria-label="Color picker">
|
<div className={styles.colorPickerWrapper} aria-label="Color picker">
|
||||||
<ColorPicker color={selectedColor} actions={selectedColorActions} />
|
<ColorPicker color={selectedColor} actions={selectedColorActions} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.colorValuesWrapper} aria-label="Color values">
|
<div className={styles.colorValuesWrapper} aria-label="Color values">
|
||||||
<ColorValues color={selectedColor} actions={selectedColorActions} />
|
<ColorValues color={selectedColor} actions={selectedColorActions} />
|
||||||
</div>
|
</div>
|
||||||
@@ -214,9 +210,17 @@ function FirstZone() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SecondZone() {
|
function SecondZone() {
|
||||||
|
const { selectedColor, selectedColorActions } = useSelectedColor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.secondZone} aria-label="Palette tools">
|
<section className={styles.secondZone} aria-label="Palette tools">
|
||||||
Palette Creator Coming Soon.
|
<div className={styles.colorHistoryWrapper} aria-label="Color History">
|
||||||
|
<ColorHistory
|
||||||
|
color={selectedColor}
|
||||||
|
setColor={selectedColorActions.common.setColor}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={styles.paletteEditorWrapper}
|
className={styles.paletteEditorWrapper}
|
||||||
aria-label="Palette editor"
|
aria-label="Palette editor"
|
||||||
@@ -231,16 +235,14 @@ function SecondZone() {
|
|||||||
|
|
||||||
function DesktopContent() {
|
function DesktopContent() {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.mainLayout}>
|
||||||
<header className={styles.appHeader}>
|
<header className={styles.appHeader}>
|
||||||
<span className={styles.title}>LUMINANCE</span>
|
<span className={styles.title}>LUMINANCE</span>
|
||||||
<span className={styles.subtitle}>A color picker for humans.</span>
|
<span className={styles.subtitle}>A color picker for humans.</span>
|
||||||
</header>
|
</header>
|
||||||
<main className={styles.mainContent}>
|
<FirstZone />
|
||||||
<FirstZone />
|
<SecondZone />
|
||||||
<SecondZone />
|
</div>
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
.colorHistory {
|
||||||
|
height: 74px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 0px 0px;
|
||||||
|
|
||||||
|
/* Improve scrolling experience */
|
||||||
|
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
|
||||||
|
scrollbar-width: thin; /* For Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyColor {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-block;
|
||||||
|
height: 50px;
|
||||||
|
width: 25px;
|
||||||
|
margin: 5px;
|
||||||
|
border: 2px solid #7a7a7a;
|
||||||
|
transition:
|
||||||
|
margin 200ms,
|
||||||
|
height 200ms,
|
||||||
|
width 200ms;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyColor:hover {
|
||||||
|
height: 56px;
|
||||||
|
width: 31px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyColor:first-of-type:hover {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyColor:first-of-type {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyColor:last-of-type:hover {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyColor:last-of-type {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Color } from "colorlib";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
|
||||||
|
import type { Timeout } from "@/types";
|
||||||
|
import { formatCssRgb } from "@/util";
|
||||||
|
|
||||||
|
import styles from "./ColorHistory.module.css";
|
||||||
|
|
||||||
|
function ColorHistory({
|
||||||
|
color,
|
||||||
|
setColor,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
color: Color;
|
||||||
|
setColor: (newColor: Color) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
}) {
|
||||||
|
const [history, setHistory] = useState<Color[]>([]);
|
||||||
|
const maxItems = 50;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
const timer: Timeout = setTimeout(() => {
|
||||||
|
setHistory((prev) => {
|
||||||
|
if (prev.length > 0 && prev[0].hex.to_code() === color.hex.to_code())
|
||||||
|
return prev;
|
||||||
|
|
||||||
|
const newHistory = [color, ...prev];
|
||||||
|
return newHistory.slice(0, maxItems);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [color, disabled]);
|
||||||
|
|
||||||
|
const handleClick = (historyColor: Color) => {
|
||||||
|
setColor(historyColor);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Manage motion props for testing
|
||||||
|
const isTestEnvironment =
|
||||||
|
typeof window !== "undefined" && window.framerMotionTestOverride === true;
|
||||||
|
|
||||||
|
const getAnimationProps = (isFirst: boolean) => {
|
||||||
|
return isTestEnvironment
|
||||||
|
? {
|
||||||
|
initial: false,
|
||||||
|
animate: {},
|
||||||
|
exit: {},
|
||||||
|
ease: null,
|
||||||
|
transition: { duration: 0 },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
initial: { opacity: isFirst ? 0 : 1, x: -20 },
|
||||||
|
animate: { opacity: 1, x: 0 },
|
||||||
|
exit: { opacity: 0 },
|
||||||
|
ease: "easeInOut",
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div data-cy="color-history" className={styles.colorHistory}>
|
||||||
|
{history.map((historyColor, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={`${historyColor.hex.to_code()}-${index}`}
|
||||||
|
data-cy={`history-color-${index}`}
|
||||||
|
className={styles.historyColor}
|
||||||
|
style={{ backgroundColor: formatCssRgb(historyColor.hex) }}
|
||||||
|
onClick={() => handleClick(historyColor)}
|
||||||
|
{...getAnimationProps(index === 0)}
|
||||||
|
></motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColorHistory;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { useReducer, useState } from "react";
|
||||||
|
import type { ChangeEvent } from "react";
|
||||||
|
|
||||||
|
import { Color } from "colorlib";
|
||||||
|
|
||||||
|
import { colorReducer, createColorActions } from "@/hooks/color";
|
||||||
|
|
||||||
|
import { HexEditor } from "../ColorValues/ValueEditor";
|
||||||
|
import ColorHistory from "./ColorHistory";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
color: Color.from_hex("000"),
|
||||||
|
};
|
||||||
|
|
||||||
|
function TestWrapper() {
|
||||||
|
const [state, dispatch] = useReducer(colorReducer, initialState);
|
||||||
|
const actions = createColorActions(dispatch);
|
||||||
|
|
||||||
|
const [disabled, setDisabled] = useState(false);
|
||||||
|
const handleDisabledChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setDisabled(e.target.checked);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ColorHistory
|
||||||
|
color={state.color}
|
||||||
|
setColor={actions.common.setColor}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<HexEditor color={state.color.hex} actions={actions.hex} />
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
data-cy="disabled-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
checked={disabled}
|
||||||
|
onChange={handleDisabledChange}
|
||||||
|
/>
|
||||||
|
Disabled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("color history", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.disableTransitions();
|
||||||
|
cy.clock();
|
||||||
|
cy.mount(<TestWrapper />);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.clock().then((clock) => clock.restore());
|
||||||
|
cy.enableTransitions();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds stable color values after 1 second", () => {
|
||||||
|
// add stable values to history
|
||||||
|
cy.dataCy("hex-value-input").as("value").clear().type("#00F536");
|
||||||
|
cy.tick(1000);
|
||||||
|
|
||||||
|
cy.dataCy("color-history").children().should("have.length", 1);
|
||||||
|
cy.dataCy("history-color-0").should(
|
||||||
|
"have.css",
|
||||||
|
"background-color",
|
||||||
|
"rgb(0, 245, 54)",
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get("@value").clear().type("#E23AEC");
|
||||||
|
cy.tick(1000);
|
||||||
|
|
||||||
|
cy.dataCy("color-history").children().should("have.length", 2);
|
||||||
|
cy.dataCy("history-color-0").should(
|
||||||
|
"have.css",
|
||||||
|
"background-color",
|
||||||
|
"rgb(226, 58, 236)",
|
||||||
|
);
|
||||||
|
|
||||||
|
// click to restore value
|
||||||
|
cy.dataCy("history-color-1").click();
|
||||||
|
cy.get("@value").should("have.value", "#00F536");
|
||||||
|
|
||||||
|
// disable history
|
||||||
|
cy.dataCy("disabled-checkbox").click();
|
||||||
|
cy.get("@value").clear().type("#00C3EE");
|
||||||
|
cy.tick(1000);
|
||||||
|
|
||||||
|
cy.dataCy("color-history").children().should("have.length", 2);
|
||||||
|
|
||||||
|
// re-enable history
|
||||||
|
cy.dataCy("disabled-checkbox").click();
|
||||||
|
cy.tick(1000);
|
||||||
|
|
||||||
|
cy.dataCy("color-history").children().should("have.length", 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import * as colorlib from "colorlib";
|
||||||
|
import { memory } from "colorlib/colorlib_bg.wasm";
|
||||||
|
|
||||||
|
import { useSmoothAnimation } from "@/hooks/animation";
|
||||||
|
import type { Setter } from "@/hooks/color";
|
||||||
|
import { useSlider } from "@/hooks/slider";
|
||||||
|
import { useResize } from "@/hooks/window";
|
||||||
|
import type { CartesianSpace } from "@/types";
|
||||||
|
import { Direction } from "@/types";
|
||||||
|
import { setMeasurements } from "@/util";
|
||||||
|
|
||||||
|
import styles from "./ColorPicker.module.css";
|
||||||
|
|
||||||
|
function ColorBar({
|
||||||
|
hue,
|
||||||
|
chroma,
|
||||||
|
luminance,
|
||||||
|
setChroma,
|
||||||
|
parentDimensions,
|
||||||
|
}: {
|
||||||
|
hue: number;
|
||||||
|
chroma: number;
|
||||||
|
luminance: number;
|
||||||
|
setChroma: Setter;
|
||||||
|
parentDimensions: CartesianSpace;
|
||||||
|
}) {
|
||||||
|
// State
|
||||||
|
const [colorBar, setColorBar] = useState<colorlib.ColorBar | null>(null);
|
||||||
|
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
const smoothAnimation = useSmoothAnimation();
|
||||||
|
|
||||||
|
// Slider interaction
|
||||||
|
const { sliderRef } = useSlider({
|
||||||
|
direction: Direction.HORIZONTAL,
|
||||||
|
origin,
|
||||||
|
dimensions,
|
||||||
|
valueRange: { min: 0, max: 1 },
|
||||||
|
value: chroma,
|
||||||
|
setValue: setChroma,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update canvas when hue/luminance changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (colorBar && canvasRef.current) {
|
||||||
|
smoothAnimation(() => {
|
||||||
|
colorBar.fill_color(hue, luminance);
|
||||||
|
refreshColorBar(canvasRef.current!, colorBar);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [hue, luminance, colorBar]);
|
||||||
|
|
||||||
|
// Get measurements
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
setMeasurements(containerRef, setOrigin, setDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return useResize(() =>
|
||||||
|
setMeasurements(containerRef, setOrigin, setDimensions),
|
||||||
|
);
|
||||||
|
}, [containerRef.current]);
|
||||||
|
|
||||||
|
// Resize color bar
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current && canvasRef.current && parentDimensions.x > 0) {
|
||||||
|
const newHeight = containerRef.current.clientHeight;
|
||||||
|
const newWidth = parentDimensions.x - 54;
|
||||||
|
const newColorBar = new colorlib.ColorBar(newWidth, newHeight);
|
||||||
|
|
||||||
|
setColorBar(newColorBar);
|
||||||
|
|
||||||
|
if (newColorBar) {
|
||||||
|
smoothAnimation(() => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
newColorBar.fill_color(hue, luminance);
|
||||||
|
refreshColorBar(canvasRef.current!, newColorBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [containerRef.current, canvasRef.current, parentDimensions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.colorBarWrapper} ref={containerRef}>
|
||||||
|
<div
|
||||||
|
className={styles.colorBar}
|
||||||
|
ref={sliderRef}
|
||||||
|
style={{
|
||||||
|
width: colorBar?.get_width(),
|
||||||
|
height: colorBar?.get_height(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={colorBar?.get_width()}
|
||||||
|
height={colorBar?.get_height()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshColorBar(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
colorBar: colorlib.ColorBar,
|
||||||
|
) {
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
const width = colorBar.get_width();
|
||||||
|
const height = colorBar.get_height();
|
||||||
|
const imageData = ctx.createImageData(width, height);
|
||||||
|
const pixelPointer = colorBar.get_pixels_pointer();
|
||||||
|
const pixels = new Uint8Array(
|
||||||
|
memory.buffer,
|
||||||
|
pixelPointer,
|
||||||
|
width * height * 4,
|
||||||
|
);
|
||||||
|
|
||||||
|
imageData.data.set(pixels);
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColorBar;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
.container {
|
.container {
|
||||||
padding: 40px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 25px 1fr 25px;
|
grid-template-columns: 25px 1fr 25px;
|
||||||
grid-template-rows: 50px 1fr 25px;
|
grid-template-rows: 50px 1fr 25px;
|
||||||
@@ -9,6 +8,14 @@
|
|||||||
". bottomGrip ."
|
". bottomGrip ."
|
||||||
". bar .";
|
". bar .";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
grid-area: preview;
|
||||||
|
height: 25px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 2px solid #7a7a7a;
|
||||||
|
}
|
||||||
|
|
||||||
.pickerSquare {
|
.pickerSquare {
|
||||||
grid-area: square;
|
grid-area: square;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -100,11 +107,3 @@
|
|||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Preview */
|
|
||||||
|
|
||||||
.preview {
|
|
||||||
grid-area: preview;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border: 2px solid #7a7a7a;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ function ColorPicker({
|
|||||||
valueRange={lumRange}
|
valueRange={lumRange}
|
||||||
arrowDirection="right"
|
arrowDirection="right"
|
||||||
invert={true}
|
invert={true}
|
||||||
|
parentDimensions={dimensions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.pickerSquare}>
|
<div className={styles.pickerSquare}>
|
||||||
@@ -62,6 +63,7 @@ function ColorPicker({
|
|||||||
hue={color.hcl.h}
|
hue={color.hcl.h}
|
||||||
luminance={color.hcl.l}
|
luminance={color.hcl.l}
|
||||||
hex={color.hex}
|
hex={color.hex}
|
||||||
|
parentDimensions={dimensions}
|
||||||
/>
|
/>
|
||||||
<ColorSquare
|
<ColorSquare
|
||||||
chroma={color.hcl.c}
|
chroma={color.hcl.c}
|
||||||
@@ -77,6 +79,7 @@ function ColorPicker({
|
|||||||
valueRange={lumRange}
|
valueRange={lumRange}
|
||||||
arrowDirection="left"
|
arrowDirection="left"
|
||||||
invert={true}
|
invert={true}
|
||||||
|
parentDimensions={dimensions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.horizontalGrip}>
|
<div className={styles.horizontalGrip}>
|
||||||
@@ -86,6 +89,7 @@ function ColorPicker({
|
|||||||
setValue={actions.hcl.setH}
|
setValue={actions.hcl.setH}
|
||||||
valueRange={hueRange}
|
valueRange={hueRange}
|
||||||
arrowDirection="up"
|
arrowDirection="up"
|
||||||
|
parentDimensions={dimensions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.pickerBar}>
|
<div className={styles.pickerBar}>
|
||||||
@@ -100,6 +104,7 @@ function ColorPicker({
|
|||||||
chroma={color.hcl.c}
|
chroma={color.hcl.c}
|
||||||
luminance={color.hcl.l}
|
luminance={color.hcl.l}
|
||||||
hex={color.hex}
|
hex={color.hex}
|
||||||
|
parentDimensions={dimensions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import * as colorlib from "colorlib";
|
||||||
|
import { memory } from "colorlib/colorlib_bg.wasm";
|
||||||
|
|
||||||
|
import { useSmoothAnimation } from "@/hooks/animation";
|
||||||
|
import type { HCLColorActions } from "@/hooks/color";
|
||||||
|
import { useCrosshair } from "@/hooks/crosshair";
|
||||||
|
import { useScroll } from "@/hooks/scroll";
|
||||||
|
import { useResize } from "@/hooks/window";
|
||||||
|
import type { CartesianSpace } from "@/types";
|
||||||
|
import { setMeasurements } from "@/util";
|
||||||
|
|
||||||
|
import styles from "./ColorPicker.module.css";
|
||||||
|
|
||||||
|
function ColorSquare({
|
||||||
|
chroma,
|
||||||
|
actions,
|
||||||
|
parentDimensions,
|
||||||
|
}: {
|
||||||
|
chroma: number;
|
||||||
|
actions: HCLColorActions;
|
||||||
|
parentDimensions: CartesianSpace;
|
||||||
|
}) {
|
||||||
|
// State
|
||||||
|
const [colorSquare, setColorSquare] = useState<colorlib.ColorSquare | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
const smoothAnimation = useSmoothAnimation();
|
||||||
|
|
||||||
|
// Crosshair interaction
|
||||||
|
const { crosshairRef } = useCrosshair({
|
||||||
|
origin,
|
||||||
|
dimensions,
|
||||||
|
setXValue: actions.setH,
|
||||||
|
setYValue: actions.setL,
|
||||||
|
xValueRange: { min: 0, max: 359 },
|
||||||
|
yValueRange: { min: 0, max: 1 },
|
||||||
|
invertY: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle chroma adjustment with scroll
|
||||||
|
const { addScrollListener } = useScroll({
|
||||||
|
targetRef: canvasRef,
|
||||||
|
onScrollUp: () => actions.setC((prev) => Math.min(1, prev + 0.01)),
|
||||||
|
onScrollDown: () => actions.setC((prev) => Math.max(0, prev - 0.01)),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update canvas when chroma changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (colorSquare && canvasRef.current) {
|
||||||
|
smoothAnimation(() => {
|
||||||
|
colorSquare.fill_chroma(chroma);
|
||||||
|
refreshColorSquare(canvasRef.current!, colorSquare);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [chroma, colorSquare]);
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
useEffect(() => {
|
||||||
|
if (canvasRef.current) addScrollListener();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get measurements
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
setMeasurements(containerRef, setOrigin, setDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return useResize(() =>
|
||||||
|
setMeasurements(containerRef, setOrigin, setDimensions),
|
||||||
|
);
|
||||||
|
}, [containerRef.current, parentDimensions]);
|
||||||
|
|
||||||
|
// Resize square
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current && canvasRef.current && parentDimensions.x > 0) {
|
||||||
|
const newSize = parentDimensions.x - 54;
|
||||||
|
const newColorSquare = new colorlib.ColorSquare(newSize);
|
||||||
|
|
||||||
|
setColorSquare(newColorSquare);
|
||||||
|
|
||||||
|
if (newColorSquare) {
|
||||||
|
smoothAnimation(() => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
newColorSquare.fill_chroma(chroma);
|
||||||
|
refreshColorSquare(canvasRef.current, newColorSquare);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [containerRef.current, canvasRef.current, parentDimensions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.colorSquareWrapper} ref={containerRef}>
|
||||||
|
<div
|
||||||
|
className={styles.colorSquare}
|
||||||
|
ref={crosshairRef}
|
||||||
|
style={{
|
||||||
|
width: colorSquare?.get_size(),
|
||||||
|
height: colorSquare?.get_size(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={colorSquare?.get_size()}
|
||||||
|
height={colorSquare?.get_size()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshColorSquare(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
colorSquare: colorlib.ColorSquare,
|
||||||
|
) {
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
const size = colorSquare.get_size();
|
||||||
|
const imageData = ctx.createImageData(size, size);
|
||||||
|
const pixelPointer = colorSquare.get_pixels_pointer();
|
||||||
|
const pixels = new Uint8Array(memory.buffer, pixelPointer, size * size * 4);
|
||||||
|
imageData.data.set(pixels);
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColorSquare;
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import * as colorlib from "colorlib";
|
||||||
|
|
||||||
|
import { useResize } from "@/hooks/window";
|
||||||
|
import type { CartesianSpace } from "@/types";
|
||||||
|
import { formatCssRgb, setMeasurements, valueToPosition } from "@/util";
|
||||||
|
|
||||||
|
import styles from "./ColorPicker.module.css";
|
||||||
|
|
||||||
|
export function SquareCrosshair({
|
||||||
|
hue,
|
||||||
|
luminance,
|
||||||
|
hex,
|
||||||
|
parentDimensions,
|
||||||
|
isDragging,
|
||||||
|
}: {
|
||||||
|
hue: number;
|
||||||
|
luminance: number;
|
||||||
|
hex: colorlib.Hex;
|
||||||
|
parentDimensions: CartesianSpace;
|
||||||
|
isDragging: boolean;
|
||||||
|
}) {
|
||||||
|
const [_origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [darkCrosshairs, setDarkCrosshairs] = useState(true);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lumRange = { min: 0, max: 1 };
|
||||||
|
const hueRange = { min: 0, max: 359 };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDarkCrosshairs(luminance > 0.5);
|
||||||
|
}, [luminance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMeasurements(containerRef, setOrigin, setDimensions);
|
||||||
|
return useResize(() =>
|
||||||
|
setMeasurements(containerRef, setOrigin, setDimensions),
|
||||||
|
);
|
||||||
|
}, [containerRef.current, parentDimensions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.crosshairWrapper} ref={containerRef}>
|
||||||
|
<div
|
||||||
|
className={styles.crosshair}
|
||||||
|
style={{
|
||||||
|
width: 1,
|
||||||
|
height: dimensions.y,
|
||||||
|
backgroundColor: darkCrosshairs ? "black" : "white",
|
||||||
|
boxShadow: darkCrosshairs ? "0 0 2px black" : "0 0 2px white",
|
||||||
|
left: valueToPosition(hue, dimensions.x - 1, hueRange),
|
||||||
|
top: 0,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={styles.crosshair}
|
||||||
|
style={{
|
||||||
|
width: dimensions.x,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: darkCrosshairs ? "black" : "white",
|
||||||
|
boxShadow: darkCrosshairs ? "0 0 2px black" : "0 0 2px white",
|
||||||
|
left: 0,
|
||||||
|
top: valueToPosition(1 - luminance, dimensions.y - 1, lumRange),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={styles.crossEye}
|
||||||
|
style={{
|
||||||
|
borderColor: darkCrosshairs ? "black" : "white",
|
||||||
|
boxShadow: darkCrosshairs ? "0 0 1px black" : "0 0 1px white",
|
||||||
|
backgroundColor: formatCssRgb(hex),
|
||||||
|
top: valueToPosition(1 - luminance, dimensions.y - 1, lumRange) - 6,
|
||||||
|
left: valueToPosition(hue, dimensions.x - 1, hueRange) - 6,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BarCrosshair({
|
||||||
|
chroma,
|
||||||
|
luminance,
|
||||||
|
hex,
|
||||||
|
parentDimensions,
|
||||||
|
}: {
|
||||||
|
chroma: number;
|
||||||
|
luminance: number;
|
||||||
|
hex: colorlib.Hex;
|
||||||
|
parentDimensions: CartesianSpace;
|
||||||
|
}) {
|
||||||
|
const [_origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [darkCrosshairs, setDarkCrosshairs] = useState(true);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chromaRange = { min: 0, max: 1 };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDarkCrosshairs(luminance > 0.5);
|
||||||
|
}, [luminance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMeasurements(containerRef, setOrigin, setDimensions);
|
||||||
|
return useResize(() =>
|
||||||
|
setMeasurements(containerRef, setOrigin, setDimensions),
|
||||||
|
);
|
||||||
|
}, [containerRef.current, parentDimensions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.crosshairWrapper} ref={containerRef}>
|
||||||
|
<div
|
||||||
|
className={styles.crosshair}
|
||||||
|
style={{
|
||||||
|
width: 1,
|
||||||
|
height: dimensions.y,
|
||||||
|
backgroundColor: darkCrosshairs ? "black" : "white",
|
||||||
|
boxShadow: darkCrosshairs ? "0 0 2px black" : "0 0 2px white",
|
||||||
|
left: valueToPosition(chroma, dimensions.x - 1, chromaRange),
|
||||||
|
top: 0,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={styles.crossEye}
|
||||||
|
style={{
|
||||||
|
borderColor: darkCrosshairs ? "black" : "white",
|
||||||
|
boxShadow: darkCrosshairs ? "0 0 1px black" : "0 0 1px white",
|
||||||
|
backgroundColor: formatCssRgb(hex),
|
||||||
|
top: 6,
|
||||||
|
left: valueToPosition(chroma, dimensions.x - 1, chromaRange) - 6,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import type { Setter } from "@/hooks/color";
|
||||||
|
import { useSlider } from "@/hooks/slider";
|
||||||
|
import { useResize } from "@/hooks/window";
|
||||||
|
import { Direction } from "@/types";
|
||||||
|
import type { CartesianSpace, Range } from "@/types";
|
||||||
|
import {
|
||||||
|
chooseValueByDirection,
|
||||||
|
setMeasurements,
|
||||||
|
valueToPosition,
|
||||||
|
} from "@/util";
|
||||||
|
|
||||||
|
import styles from "./ColorPicker.module.css";
|
||||||
|
|
||||||
|
function GripSlider({
|
||||||
|
direction,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
valueRange,
|
||||||
|
arrowDirection,
|
||||||
|
invert = false,
|
||||||
|
parentDimensions,
|
||||||
|
}: {
|
||||||
|
direction: Direction;
|
||||||
|
value: number;
|
||||||
|
setValue: Setter;
|
||||||
|
valueRange: Range;
|
||||||
|
arrowDirection: "up" | "left" | "right";
|
||||||
|
invert?: boolean;
|
||||||
|
parentDimensions: CartesianSpace;
|
||||||
|
}) {
|
||||||
|
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// Slider interaction
|
||||||
|
const { sliderRef, isDragging } = useSlider({
|
||||||
|
direction,
|
||||||
|
origin,
|
||||||
|
dimensions,
|
||||||
|
valueRange,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
invert,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sliderRef.current) {
|
||||||
|
setMeasurements(sliderRef, setOrigin, setDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return useResize(() =>
|
||||||
|
setMeasurements(sliderRef, setOrigin, setDimensions),
|
||||||
|
);
|
||||||
|
}, [sliderRef.current, parentDimensions]);
|
||||||
|
|
||||||
|
const upArrowStyle = {
|
||||||
|
borderLeft: "12px solid transparent",
|
||||||
|
borderRight: "12px solid transparent",
|
||||||
|
borderBottom: "25px solid black",
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftArrowStyle = {
|
||||||
|
borderTop: "12px solid transparent",
|
||||||
|
borderBottom: "12px solid transparent",
|
||||||
|
borderRight: "25px solid black",
|
||||||
|
};
|
||||||
|
|
||||||
|
const rightArrowStyle = {
|
||||||
|
borderTop: "12px solid transparent",
|
||||||
|
borderBottom: "12px solid transparent",
|
||||||
|
borderLeft: "25px solid black",
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrowStyle = (function () {
|
||||||
|
switch (arrowDirection) {
|
||||||
|
case "up":
|
||||||
|
return upArrowStyle;
|
||||||
|
case "left":
|
||||||
|
return leftArrowStyle;
|
||||||
|
case "right":
|
||||||
|
return rightArrowStyle;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.gripSlider} ref={sliderRef}>
|
||||||
|
<div
|
||||||
|
className={styles.grip}
|
||||||
|
style={{
|
||||||
|
...arrowStyle,
|
||||||
|
cursor: isDragging ? "grabbing" : "grab",
|
||||||
|
top: chooseValueByDirection(
|
||||||
|
direction,
|
||||||
|
0,
|
||||||
|
-12 +
|
||||||
|
valueToPosition(
|
||||||
|
valueRange.max - value,
|
||||||
|
dimensions.y - 1,
|
||||||
|
valueRange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
left: chooseValueByDirection(
|
||||||
|
direction,
|
||||||
|
-12 + valueToPosition(value, dimensions.x - 1, valueRange),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GripSlider;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
.colorValuesWrapper {
|
.colorValuesWrapper {
|
||||||
padding: 40px;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
.spaceWrapper {
|
.spaceWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
max-height: 94px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border: 2px solid #7a7a7a;
|
border: 2px solid #7a7a7a;
|
||||||
@@ -17,7 +18,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 25px;
|
max-height: 25px;
|
||||||
|
min-height: 0;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border-top: 1px solid #7a7a7a;
|
border-top: 1px solid #7a7a7a;
|
||||||
border-bottom: 1px solid #7a7a7a;
|
border-bottom: 1px solid #7a7a7a;
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ function TestWrapper() {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 300,
|
height: 275,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
gap: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SpaceEditor
|
<SpaceEditor
|
||||||
@@ -44,16 +45,19 @@ function TestWrapper() {
|
|||||||
|
|
||||||
<div style={{ fontFamily: "monospace" }}>
|
<div style={{ fontFamily: "monospace" }}>
|
||||||
<p data-cy="hcl-value">
|
<p data-cy="hcl-value">
|
||||||
HCL ({roundTo(state.color.hcl.h, 0)}, {roundTo(state.color.hcl.c, 2)},{" "}
|
HCL ({roundTo(state.color.hcl.h, 0, "down")},{" "}
|
||||||
{roundTo(state.color.hcl.l, 2)})
|
{roundTo(state.color.hcl.c, 2, "down")},{" "}
|
||||||
|
{roundTo(state.color.hcl.l, 2, "down")})
|
||||||
</p>
|
</p>
|
||||||
<p data-cy="hsv-value">
|
<p data-cy="hsv-value">
|
||||||
HSV ({roundTo(state.color.hsv.h, 0)}, {roundTo(state.color.hsv.s, 2)},{" "}
|
HSV ({roundTo(state.color.hsv.h, 0, "down")},{" "}
|
||||||
{roundTo(state.color.hsv.v, 2)})
|
{roundTo(state.color.hsv.s, 2, "down")},{" "}
|
||||||
|
{roundTo(state.color.hsv.v, 2, "down")})
|
||||||
</p>
|
</p>
|
||||||
<p data-cy="rgb-value">
|
<p data-cy="rgb-value">
|
||||||
RGB ({roundTo(state.color.rgb.r, 0)}, {roundTo(state.color.rgb.g, 0)},{" "}
|
RGB ({roundTo(state.color.rgb.r, 0, "down")},{" "}
|
||||||
{roundTo(state.color.rgb.b, 0)})
|
{roundTo(state.color.rgb.g, 0, "down")},{" "}
|
||||||
|
{roundTo(state.color.rgb.b, 0, "down")})
|
||||||
</p>
|
</p>
|
||||||
<p data-cy="hex-value">HEX: #{state.color.hex.to_code()}</p>
|
<p data-cy="hex-value">HEX: #{state.color.hex.to_code()}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +79,7 @@ describe("space editor tests", () => {
|
|||||||
cy.dataCy("HSV-editor").within(() => {
|
cy.dataCy("HSV-editor").within(() => {
|
||||||
cy.dataCy("H-value-input").should("have.value", 158);
|
cy.dataCy("H-value-input").should("have.value", 158);
|
||||||
cy.dataCy("S-value-input").should("have.value", 79);
|
cy.dataCy("S-value-input").should("have.value", 79);
|
||||||
cy.dataCy("V-value-input").should("have.value", 87);
|
cy.dataCy("V-value-input").should("have.value", 86);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.dataCy("HCL-editor").within(() => {
|
cy.dataCy("HCL-editor").within(() => {
|
||||||
@@ -85,7 +89,7 @@ describe("space editor tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cy.dataCy("rgb-value").should("have.text", "RGB (46, 221, 157)");
|
cy.dataCy("rgb-value").should("have.text", "RGB (46, 221, 157)");
|
||||||
cy.dataCy("hsv-value").should("have.text", "HSV (158, 0.79, 0.87)");
|
cy.dataCy("hsv-value").should("have.text", "HSV (158, 0.79, 0.86)");
|
||||||
cy.dataCy("hcl-value").should("have.text", "HCL (158, 0.79, 0.7)");
|
cy.dataCy("hcl-value").should("have.text", "HCL (158, 0.79, 0.7)");
|
||||||
cy.dataCy("hex-value").should("have.text", "HEX: #2EDD9D");
|
cy.dataCy("hex-value").should("have.text", "HEX: #2EDD9D");
|
||||||
|
|
||||||
@@ -97,9 +101,9 @@ describe("space editor tests", () => {
|
|||||||
cy.dataCy("L-value-input").type("25");
|
cy.dataCy("L-value-input").type("25");
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.dataCy("rgb-value").should("have.text", "RGB (17, 76, 75)");
|
cy.dataCy("rgb-value").should("have.text", "RGB (16, 75, 74)");
|
||||||
cy.dataCy("hsv-value").should("have.text", "HSV (179, 0.78, 0.3)");
|
cy.dataCy("hsv-value").should("have.text", "HSV (178, 0.78, 0.29)");
|
||||||
cy.dataCy("hcl-value").should("have.text", "HCL (179, 0.78, 0.25)");
|
cy.dataCy("hcl-value").should("have.text", "HCL (178, 0.78, 0.25)");
|
||||||
cy.dataCy("hex-value").should("have.text", "HEX: #104B4A");
|
cy.dataCy("hex-value").should("have.text", "HEX: #104B4A");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ export function ValueEditor({
|
|||||||
value,
|
value,
|
||||||
setValue,
|
setValue,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
onKeyDown,
|
onKeyDown = null,
|
||||||
}: {
|
}: {
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
valueRange: Range;
|
valueRange: Range;
|
||||||
value: number;
|
value: number;
|
||||||
setValue: Setter<number>;
|
setValue: Setter<number>;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
onKeyDown?: ((e: React.KeyboardEvent) => void) | null;
|
||||||
}) {
|
}) {
|
||||||
// Set up component state
|
// Set up component state
|
||||||
const direction = Direction.HORIZONTAL;
|
const direction = Direction.HORIZONTAL;
|
||||||
@@ -162,7 +162,7 @@ function Slider({
|
|||||||
position: number;
|
position: number;
|
||||||
dimensions: CartesianSpace;
|
dimensions: CartesianSpace;
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
onKeyDown?: ((e: React.KeyboardEvent) => void) | null;
|
||||||
}) {
|
}) {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
@@ -205,7 +205,7 @@ function Button({
|
|||||||
direction: "increase" | "decrease";
|
direction: "increase" | "decrease";
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
handleValueStep: (step: number) => void;
|
handleValueStep: (step: number) => void;
|
||||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
onKeyDown?: ((e: React.KeyboardEvent) => void) | null;
|
||||||
}) {
|
}) {
|
||||||
const isIncrease = direction === "increase";
|
const isIncrease = direction === "increase";
|
||||||
const label = isIncrease ? "Increase" : "Decrease";
|
const label = isIncrease ? "Increase" : "Decrease";
|
||||||
@@ -255,7 +255,7 @@ function Value({
|
|||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
handleValueStep: (step: number) => void;
|
handleValueStep: (step: number) => void;
|
||||||
scale: number;
|
scale: number;
|
||||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
onKeyDown?: ((e: React.KeyboardEvent) => void) | null;
|
||||||
}) {
|
}) {
|
||||||
const valueRef = useRef(null);
|
const valueRef = useRef(null);
|
||||||
const valueScroller = useScroll({
|
const valueScroller = useScroll({
|
||||||
@@ -287,7 +287,7 @@ function Value({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
ref={valueRef}
|
ref={valueRef}
|
||||||
value={Math.round(value * scale)}
|
value={Math.floor(value * scale)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={styles.value}
|
className={styles.value}
|
||||||
onFocus={(e) => e.target.select()}
|
onFocus={(e) => e.target.select()}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ function TestWrapper() {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 400,
|
width: 400,
|
||||||
|
height: 25,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
border: "2px solid #7a7a7a",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
@@ -53,7 +55,7 @@ describe("component editor tests", () => {
|
|||||||
cy.dataCy("R-slider")
|
cy.dataCy("R-slider")
|
||||||
.click()
|
.click()
|
||||||
.dataCy("R-slider-bar")
|
.dataCy("R-slider-bar")
|
||||||
.should("have.css", "width", "140px")
|
.should("have.css", "width", "138px")
|
||||||
.dataCy("R-value-input")
|
.dataCy("R-value-input")
|
||||||
.should("have.value", "127");
|
.should("have.value", "127");
|
||||||
|
|
||||||
@@ -78,14 +80,14 @@ describe("component editor tests", () => {
|
|||||||
.type("100")
|
.type("100")
|
||||||
.should("have.value", "100")
|
.should("have.value", "100")
|
||||||
.dataCy("R-slider-bar")
|
.dataCy("R-slider-bar")
|
||||||
.should("have.css", "width", "110px");
|
.should("have.css", "width", "109px");
|
||||||
|
|
||||||
// Scrolling input should update value
|
// Scrolling input should update value
|
||||||
cy.dataCy("R-value-input")
|
cy.dataCy("R-value-input")
|
||||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||||
.should("have.value", "100")
|
.should("have.value", "100")
|
||||||
.dataCy("R-slider-bar")
|
.dataCy("R-slider-bar")
|
||||||
.should("have.css", "width", "111px")
|
.should("have.css", "width", "110px")
|
||||||
.wait(50);
|
.wait(50);
|
||||||
|
|
||||||
// Test increment/decrement buttons
|
// Test increment/decrement buttons
|
||||||
@@ -132,7 +134,7 @@ describe("component editor tests", () => {
|
|||||||
cy.dataCy("R-slider")
|
cy.dataCy("R-slider")
|
||||||
.click()
|
.click()
|
||||||
.dataCy("R-slider-bar")
|
.dataCy("R-slider-bar")
|
||||||
.should("have.css", "width", "140px")
|
.should("have.css", "width", "138px")
|
||||||
.dataCy("R-value-input")
|
.dataCy("R-value-input")
|
||||||
.should("have.value", "127");
|
.should("have.value", "127");
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -92,9 +92,20 @@ export function chooseValueByDirection(
|
|||||||
return direction === Direction.HORIZONTAL ? xValue : yValue;
|
return direction === Direction.HORIZONTAL ? xValue : yValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function roundTo(value: number, decimals: number = 0) {
|
export function roundTo(
|
||||||
|
value: number,
|
||||||
|
decimals: number = 0,
|
||||||
|
direction: "up" | "down" | null = null,
|
||||||
|
) {
|
||||||
const factor = Math.pow(10, decimals);
|
const factor = Math.pow(10, decimals);
|
||||||
return Math.round(value * factor) / factor;
|
switch (direction) {
|
||||||
|
case "up":
|
||||||
|
return Math.ceil(value * factor) / factor;
|
||||||
|
case "down":
|
||||||
|
return Math.floor(value * factor) / factor;
|
||||||
|
default:
|
||||||
|
return Math.round(value * factor) / factor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCssRgb(hex: Hex) {
|
export function formatCssRgb(hex: Hex) {
|
||||||
|
|||||||
Vendored
+5
@@ -1 +1,6 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
framerMotionTestOverride?: boolean;
|
||||||
|
originalRequestAnimationFrame?: typeof requestAnimationFrame;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user