Added color reducer. Performed state management refactor to prevent circular behavior.
This commit is contained in:
Generated
+1482
-569
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -13,7 +13,8 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:wasm": "cargo test --lib --manifest-path colorlib/Cargo.toml",
|
"test:wasm": "cargo test --lib --manifest-path colorlib/Cargo.toml",
|
||||||
"test:wasmdoc": "cargo test --doc --manifest-path colorlib/Cargo.toml"
|
"test:wasmdoc": "cargo test --doc --manifest-path colorlib/Cargo.toml",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"motion": "^12.23.12",
|
"motion": "^12.23.12",
|
||||||
@@ -35,12 +36,14 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-top-level-await": "^1.5.0",
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-plugin-wasm": "^3.4.1"
|
"vite-plugin-wasm": "^3.4.1",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"importOrder": [
|
"importOrder": [
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import styles from "./ColorValues.module.css";
|
import * as colorlib from "colorlib";
|
||||||
|
|
||||||
function ColorValues() {
|
import styles from "./ColorValues.module.css";
|
||||||
return (
|
import SpaceEditor from "./SpaceEditor";
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.valueItem}>RGB: 255, 0, 0</div>
|
function ColorValues({ selectedColor }: { selectedColor: colorlib.Color }) {
|
||||||
<div className={styles.valueItem}>HEX: #FF0000</div>
|
return <div className={styles.wrapper}></div>;
|
||||||
<div className={styles.valueItem}>HSL: 0, 100%, 50%</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ColorValues;
|
export default ColorValues;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import * as colorlib from "colorlib";
|
||||||
|
|
||||||
|
import styles from "./SpaceEditor.module.css";
|
||||||
|
|
||||||
|
function SpaceEditor({}: {}) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SpaceEditor;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: var(--height, 25px);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -27,11 +27,16 @@
|
|||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sliderWrapper {
|
.sliderSection {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sliderWrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.sliderBar {
|
.sliderBar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -1,91 +1,62 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import type { ChangeEvent, Dispatch, RefObject, SetStateAction } from "react";
|
import type { ChangeEvent, RefObject } from "react";
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import type { CartesianSpace } from "@/types";
|
import type { CartesianSpace, Range, Setter, Timeout } from "@/types";
|
||||||
import { minmax, setMeasurements } from "@/util";
|
import { Direction } from "@/types";
|
||||||
|
import { minmax, setMeasurements, valueToPosition } from "@/util";
|
||||||
import { useScroll } from "@hooks/scroll";
|
import { useScroll } from "@hooks/scroll";
|
||||||
import { Direction, useSlider } from "@hooks/slider";
|
import { useSlider } from "@hooks/slider";
|
||||||
import { useResize } from "@hooks/window";
|
import { useResize } from "@hooks/window";
|
||||||
|
|
||||||
import styles from "./ValueEditor.module.css";
|
import styles from "./ValueEditor.module.css";
|
||||||
|
|
||||||
// Types
|
|
||||||
type Timeout = ReturnType<typeof setTimeout>;
|
|
||||||
interface Range {
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculation functions
|
|
||||||
const getPositionFromValue = (
|
|
||||||
newValue: number,
|
|
||||||
maxValue: number,
|
|
||||||
range: Range,
|
|
||||||
) => {
|
|
||||||
const newPosition = parseFloat(
|
|
||||||
(
|
|
||||||
((newValue + Math.abs(range.min)) /
|
|
||||||
(Math.abs(range.min) + Math.abs(range.max))) *
|
|
||||||
maxValue
|
|
||||||
).toFixed(0),
|
|
||||||
);
|
|
||||||
return newPosition;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getValueFromPosition = (
|
|
||||||
newPosition: number,
|
|
||||||
maxValue: number,
|
|
||||||
range: Range,
|
|
||||||
) => {
|
|
||||||
const newValue = parseFloat(
|
|
||||||
(
|
|
||||||
(newPosition / maxValue) * (Math.abs(range.min) + Math.abs(range.max)) -
|
|
||||||
Math.abs(range.min)
|
|
||||||
).toFixed(0),
|
|
||||||
);
|
|
||||||
return newValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Component
|
// Component
|
||||||
function ValueEditor({
|
function ValueEditor({
|
||||||
componentSymbol,
|
componentSymbol,
|
||||||
range,
|
valueRange,
|
||||||
value,
|
value,
|
||||||
setValue,
|
setValue,
|
||||||
|
scale = 1,
|
||||||
}: {
|
}: {
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
range: Range;
|
valueRange: Range;
|
||||||
value: number;
|
value: number;
|
||||||
setValue: Dispatch<SetStateAction<number>>;
|
setValue: Setter<number>;
|
||||||
|
scale?: number;
|
||||||
}) {
|
}) {
|
||||||
// Set up component state
|
// Set up component state
|
||||||
const direction = Direction.HORIZONTAL;
|
const direction = Direction.HORIZONTAL;
|
||||||
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const [position, setPosition] = useState(0);
|
const position = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
position.current = valueToPosition(value, dimensions.x, valueRange);
|
||||||
|
}, [value, dimensions, valueRange]);
|
||||||
|
|
||||||
// Handler functions
|
// Handler functions
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const inputValue = parseInt(e.target.value, 10);
|
const inputValue = parseInt(e.target.value, 10);
|
||||||
if (!isNaN(inputValue)) {
|
if (!isNaN(inputValue)) {
|
||||||
const newValue = minmax(inputValue, range.min, range.max);
|
const actualValue = inputValue / scale;
|
||||||
const newPosition = getPositionFromValue(newValue, dimensions.x, range);
|
const newValue = minmax(actualValue, valueRange.min, valueRange.max);
|
||||||
|
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
setPosition(newPosition);
|
|
||||||
} else {
|
} else {
|
||||||
setValue(range.min);
|
setValue(valueRange.min);
|
||||||
setPosition(0);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleValueStep = (step: number) => {
|
const handleValueStep = (step: number) => {
|
||||||
setValue((prev) => {
|
setValue((prev) => {
|
||||||
const newValue = minmax(prev + step, range.min, range.max);
|
const scaledStep = step / scale;
|
||||||
const newPosition = getPositionFromValue(newValue, dimensions.x, range);
|
const newValue = minmax(
|
||||||
setPosition(newPosition);
|
prev + scaledStep,
|
||||||
|
valueRange.min,
|
||||||
|
valueRange.max,
|
||||||
|
);
|
||||||
return newValue;
|
return newValue;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -95,7 +66,9 @@ function ValueEditor({
|
|||||||
direction,
|
direction,
|
||||||
origin,
|
origin,
|
||||||
dimensions,
|
dimensions,
|
||||||
setPosition,
|
valueRange,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set component dimensions for slider hook
|
// Set component dimensions for slider hook
|
||||||
@@ -106,17 +79,6 @@ function ValueEditor({
|
|||||||
);
|
);
|
||||||
}, [sliderRef, setOrigin, setDimensions]);
|
}, [sliderRef, setOrigin, setDimensions]);
|
||||||
|
|
||||||
// Update value when position, etc. changes
|
|
||||||
useEffect(() => {
|
|
||||||
const maxValue = dimensions.x;
|
|
||||||
if (maxValue > 0) {
|
|
||||||
const newValue = getValueFromPosition(position, maxValue, range);
|
|
||||||
setValue(newValue);
|
|
||||||
} else {
|
|
||||||
setValue(range.min);
|
|
||||||
}
|
|
||||||
}, [dimensions, position, range, setValue]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.componentWrapper}
|
className={styles.componentWrapper}
|
||||||
@@ -128,9 +90,8 @@ function ValueEditor({
|
|||||||
|
|
||||||
<Slider
|
<Slider
|
||||||
sliderRef={sliderRef}
|
sliderRef={sliderRef}
|
||||||
position={position}
|
position={position.current}
|
||||||
value={value}
|
dimensions={dimensions}
|
||||||
range={range}
|
|
||||||
componentSymbol={componentSymbol}
|
componentSymbol={componentSymbol}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -143,9 +104,10 @@ function ValueEditor({
|
|||||||
<Value
|
<Value
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
range={range}
|
valueRange={valueRange}
|
||||||
componentSymbol={componentSymbol}
|
componentSymbol={componentSymbol}
|
||||||
handleValueStep={handleValueStep}
|
handleValueStep={handleValueStep}
|
||||||
|
scale={scale}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -173,33 +135,33 @@ function Label({ componentSymbol }: { componentSymbol: string }) {
|
|||||||
function Slider({
|
function Slider({
|
||||||
sliderRef,
|
sliderRef,
|
||||||
position,
|
position,
|
||||||
value,
|
dimensions,
|
||||||
range,
|
|
||||||
componentSymbol,
|
componentSymbol,
|
||||||
}: {
|
}: {
|
||||||
sliderRef: RefObject<HTMLDivElement | null>;
|
sliderRef: RefObject<HTMLDivElement | null>;
|
||||||
position: number;
|
position: number;
|
||||||
value: number;
|
dimensions: CartesianSpace;
|
||||||
range: { min: number; max: number };
|
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={clsx(styles.section, styles.sliderSection)}>
|
||||||
className={clsx(styles.section, styles.sliderWrapper)}
|
|
||||||
ref={sliderRef}
|
|
||||||
role="slider"
|
|
||||||
aria-valuemin={range.min}
|
|
||||||
aria-valuemax={range.max}
|
|
||||||
aria-valuenow={value}
|
|
||||||
aria-labelledby={`${componentSymbol}-label`}
|
|
||||||
tabIndex={0}
|
|
||||||
data-cy={`${componentSymbol}-slider`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={styles.sliderBar}
|
className={styles.sliderWrapper}
|
||||||
style={{ width: position }}
|
ref={sliderRef}
|
||||||
data-cy={`${componentSymbol}-slider-bar`}
|
role="slider"
|
||||||
></div>
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={dimensions.x}
|
||||||
|
aria-valuenow={position}
|
||||||
|
aria-labelledby={`${componentSymbol}-label`}
|
||||||
|
tabIndex={0}
|
||||||
|
data-cy={`${componentSymbol}-slider`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.sliderBar}
|
||||||
|
style={{ width: position }}
|
||||||
|
data-cy={`${componentSymbol}-slider-bar`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -240,15 +202,17 @@ function Button({
|
|||||||
function Value({
|
function Value({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
range,
|
valueRange,
|
||||||
componentSymbol,
|
componentSymbol,
|
||||||
handleValueStep,
|
handleValueStep,
|
||||||
|
scale,
|
||||||
}: {
|
}: {
|
||||||
value: number;
|
value: number;
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
range: { min: number; max: number };
|
valueRange: { min: number; max: number };
|
||||||
componentSymbol: string;
|
componentSymbol: string;
|
||||||
handleValueStep: (step: number) => void;
|
handleValueStep: (step: number) => void;
|
||||||
|
scale: number;
|
||||||
}) {
|
}) {
|
||||||
const valueRef = useRef(null);
|
const valueRef = useRef(null);
|
||||||
const valueScroller = useScroll({
|
const valueScroller = useScroll({
|
||||||
@@ -263,9 +227,7 @@ function Value({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (valueRef.current) {
|
valueScroller.removeScrollListener();
|
||||||
valueScroller.removeScrollListener();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [valueScroller]);
|
}, [valueScroller]);
|
||||||
|
|
||||||
@@ -274,13 +236,13 @@ function Value({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
ref={valueRef}
|
ref={valueRef}
|
||||||
value={value}
|
value={Math.round(value * scale)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={styles.value}
|
className={styles.value}
|
||||||
onFocus={(e) => e.target.select()}
|
onFocus={(e) => e.target.select()}
|
||||||
aria-label={`${componentSymbol} value input`}
|
aria-label={`${componentSymbol} value input`}
|
||||||
aria-valuemin={range.min}
|
aria-valuemin={valueRange.min}
|
||||||
aria-valuemax={range.max}
|
aria-valuemax={valueRange.max}
|
||||||
aria-valuenow={value}
|
aria-valuenow={value}
|
||||||
data-cy={`${componentSymbol}-value-input`}
|
data-cy={`${componentSymbol}-value-input`}
|
||||||
/>
|
/>
|
||||||
@@ -294,35 +256,30 @@ function useLongPressRepeat(
|
|||||||
startDelay = 650,
|
startDelay = 650,
|
||||||
repeatInterval = 150,
|
repeatInterval = 150,
|
||||||
) {
|
) {
|
||||||
const [pressing, setPressing] = useState(false);
|
const timeoutRef = useRef<Timeout>(null);
|
||||||
const timerRef = useRef<Timeout>(null);
|
|
||||||
const intervalRef = useRef<Timeout>(null);
|
const intervalRef = useRef<Timeout>(null);
|
||||||
|
|
||||||
const start = useCallback(
|
const cleanup = () => {
|
||||||
(e: Event | any) => {
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
e.preventDefault();
|
|
||||||
setPressing(true);
|
|
||||||
timerRef.current = setTimeout(() => {
|
|
||||||
callback();
|
|
||||||
intervalRef.current = setInterval(callback, repeatInterval);
|
|
||||||
}, startDelay);
|
|
||||||
},
|
|
||||||
[callback, startDelay, repeatInterval],
|
|
||||||
);
|
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
|
||||||
setPressing(false);
|
|
||||||
|
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
|
||||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
}, []);
|
timeoutRef.current = null;
|
||||||
|
intervalRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Intentional 'any' to avoid overly complex typing
|
||||||
return () => {
|
const start = (e: Event | any) => {
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
e.preventDefault();
|
||||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
cleanup();
|
||||||
};
|
|
||||||
}, []);
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
intervalRef.current = setInterval(callback, repeatInterval);
|
||||||
|
}, startDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stop = cleanup;
|
||||||
|
|
||||||
|
useEffect(() => cleanup, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onMouseDown: start,
|
onMouseDown: start,
|
||||||
@@ -330,8 +287,8 @@ function useLongPressRepeat(
|
|||||||
onMouseLeave: stop,
|
onMouseLeave: stop,
|
||||||
onTouchStart: start,
|
onTouchStart: start,
|
||||||
onTouchEnd: stop,
|
onTouchEnd: stop,
|
||||||
|
// Intentional 'any' to avoid overly complex typing
|
||||||
onContextMenu: (e: Event | any) => e.preventDefault(),
|
onContextMenu: (e: Event | any) => e.preventDefault(),
|
||||||
pressing: pressing.toString(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import { useState } from "react";
|
import { useReducer } from "react";
|
||||||
|
|
||||||
|
import { Color } from "colorlib";
|
||||||
|
|
||||||
|
import { colorReducer, createColorActions } from "@hooks/color";
|
||||||
|
|
||||||
import ValueEditor from "./ValueEditor";
|
import ValueEditor from "./ValueEditor";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
color: Color.from_hex("000"),
|
||||||
|
};
|
||||||
|
|
||||||
function TestWrapper() {
|
function TestWrapper() {
|
||||||
const [value, setValue] = useState(0);
|
const [state, dispatch] = useReducer(colorReducer, initialState);
|
||||||
|
const actions = createColorActions(dispatch);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: 400, height: 25 }}>
|
<div style={{ width: 400 }}>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
componentSymbol="R"
|
componentSymbol="R"
|
||||||
range={{ min: 0, max: 255 }}
|
valueRange={{ min: 0, max: 255 }}
|
||||||
value={value}
|
value={state.color.rgb.r}
|
||||||
setValue={setValue}
|
setValue={actions.rgb.setR}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -37,9 +47,9 @@ 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", "141px")
|
.should("have.css", "width", "140px")
|
||||||
.dataCy("R-value-input")
|
.dataCy("R-value-input")
|
||||||
.should("have.value", "128");
|
.should("have.value", "127");
|
||||||
|
|
||||||
// Test slider scroll
|
// Test slider scroll
|
||||||
cy.dataCy("R-slider")
|
cy.dataCy("R-slider")
|
||||||
@@ -47,7 +57,8 @@ describe("component editor tests", () => {
|
|||||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||||
.dataCy("R-value-input")
|
.dataCy("R-value-input")
|
||||||
.should("have.value", "129");
|
.should("have.value", "128")
|
||||||
|
.wait(50);
|
||||||
|
|
||||||
// Input focus should select text
|
// Input focus should select text
|
||||||
cy.dataCy("R-value-input")
|
cy.dataCy("R-value-input")
|
||||||
@@ -61,14 +72,15 @@ 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", "111px");
|
.should("have.css", "width", "110px");
|
||||||
|
|
||||||
// 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", "101")
|
.should("have.value", "100")
|
||||||
.dataCy("R-slider-bar")
|
.dataCy("R-slider-bar")
|
||||||
.should("have.css", "width", "112px");
|
.should("have.css", "width", "111px")
|
||||||
|
.wait(50);
|
||||||
|
|
||||||
// Test increment/decrement buttons
|
// Test increment/decrement buttons
|
||||||
cy.dataCy("R-decrement-button")
|
cy.dataCy("R-decrement-button")
|
||||||
@@ -114,9 +126,9 @@ 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", "141px")
|
.should("have.css", "width", "140px")
|
||||||
.dataCy("R-value-input")
|
.dataCy("R-value-input")
|
||||||
.should("have.value", "128");
|
.should("have.value", "127");
|
||||||
|
|
||||||
// Test button long press repeat
|
// Test button long press repeat
|
||||||
cy.dataCy("R-decrement-button")
|
cy.dataCy("R-decrement-button")
|
||||||
@@ -128,7 +140,7 @@ describe("component editor tests", () => {
|
|||||||
.dataCy("R-decrement-button")
|
.dataCy("R-decrement-button")
|
||||||
.trigger("touchend")
|
.trigger("touchend")
|
||||||
.dataCy("R-value-input")
|
.dataCy("R-value-input")
|
||||||
.should("have.value", "125")
|
.should("have.value", "124")
|
||||||
.dataCy("R-increment-button")
|
.dataCy("R-increment-button")
|
||||||
.trigger("touchstart")
|
.trigger("touchstart")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -138,6 +150,6 @@ describe("component editor tests", () => {
|
|||||||
.dataCy("R-increment-button")
|
.dataCy("R-increment-button")
|
||||||
.trigger("touchend")
|
.trigger("touchend")
|
||||||
.dataCy("R-value-input")
|
.dataCy("R-value-input")
|
||||||
.should("have.value", "128");
|
.should("have.value", "127");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import type { Dispatch } from "react";
|
||||||
|
|
||||||
|
import * as colorlib from "colorlib";
|
||||||
|
|
||||||
|
import type { SetterValueOrCallback } from "@/types";
|
||||||
|
|
||||||
|
export interface ColorState {
|
||||||
|
color: colorlib.Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorAction =
|
||||||
|
| { type: "SET_COLOR"; payload: colorlib.Color }
|
||||||
|
| { type: "SET_RGB"; payload: colorlib.RGB }
|
||||||
|
| { type: "SET_HSV"; payload: colorlib.HSV }
|
||||||
|
| { type: "SET_HCL"; payload: colorlib.HCL }
|
||||||
|
| { type: "SET_HEX"; payload: colorlib.Hex }
|
||||||
|
| {
|
||||||
|
type: "SET_VALUE";
|
||||||
|
component: colorlib.Component;
|
||||||
|
payload: SetterValueOrCallback<number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function colorReducer(
|
||||||
|
state: ColorState,
|
||||||
|
action: ColorAction,
|
||||||
|
): ColorState {
|
||||||
|
let comp;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case "SET_COLOR":
|
||||||
|
return { ...state, color: action.payload };
|
||||||
|
|
||||||
|
case "SET_RGB":
|
||||||
|
let rgb = action.payload;
|
||||||
|
return { ...state, color: colorlib.Color.from_rgb(rgb.r, rgb.g, rgb.b) };
|
||||||
|
|
||||||
|
case "SET_HSV":
|
||||||
|
let hsv = action.payload;
|
||||||
|
return { ...state, color: colorlib.Color.from_hsv(hsv.h, hsv.s, hsv.v) };
|
||||||
|
|
||||||
|
case "SET_HCL":
|
||||||
|
let hcl = action.payload;
|
||||||
|
return { ...state, color: colorlib.Color.from_hcl(hcl.h, hcl.c, hcl.l) };
|
||||||
|
|
||||||
|
case "SET_HEX":
|
||||||
|
let hex = action.payload;
|
||||||
|
return { ...state, color: colorlib.Color.from_hex(hex.to_code()) };
|
||||||
|
|
||||||
|
case "SET_VALUE":
|
||||||
|
comp = action.component;
|
||||||
|
let valOrFn = action.payload;
|
||||||
|
|
||||||
|
if (typeof valOrFn === "function") {
|
||||||
|
let prev = state.color.get(comp);
|
||||||
|
return { ...state, color: state.color.update(comp, valOrFn(prev)) };
|
||||||
|
} else {
|
||||||
|
return { ...state, color: state.color.update(comp, valOrFn) };
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Setter = (valOrCallback: SetterValueOrCallback<number>) => void;
|
||||||
|
|
||||||
|
export interface CommonColorActions {
|
||||||
|
setColor: (color: colorlib.Color) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RGBColorActions {
|
||||||
|
setRGB: (rgb: colorlib.RGB) => void;
|
||||||
|
setR: Setter;
|
||||||
|
setG: Setter;
|
||||||
|
setB: Setter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HSVColorActions {
|
||||||
|
setHSV: (hsv: colorlib.HSV) => void;
|
||||||
|
setH: Setter;
|
||||||
|
setS: Setter;
|
||||||
|
setV: Setter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HCLColorActions {
|
||||||
|
setHCL: (hcl: colorlib.HCL) => void;
|
||||||
|
setH: Setter;
|
||||||
|
setC: Setter;
|
||||||
|
setL: Setter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HexColorActions {
|
||||||
|
setHex: (hex: colorlib.Hex) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColorActions {
|
||||||
|
common: CommonColorActions;
|
||||||
|
rgb: RGBColorActions;
|
||||||
|
hsv: HSVColorActions;
|
||||||
|
hcl: HCLColorActions;
|
||||||
|
hex: HexColorActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createColorActions(
|
||||||
|
dispatch: Dispatch<ColorAction>,
|
||||||
|
): ColorActions {
|
||||||
|
const Comp = colorlib.Component;
|
||||||
|
const setValue = (
|
||||||
|
comp: colorlib.Component,
|
||||||
|
payload: SetterValueOrCallback<number>,
|
||||||
|
) => dispatch({ type: "SET_VALUE", component: comp, payload });
|
||||||
|
|
||||||
|
return {
|
||||||
|
common: {
|
||||||
|
setColor: (payload) => dispatch({ type: "SET_COLOR", payload }),
|
||||||
|
},
|
||||||
|
|
||||||
|
rgb: {
|
||||||
|
setRGB: (rgb) => dispatch({ type: "SET_RGB", payload: rgb }),
|
||||||
|
setR: (val) => setValue(Comp.RGB_R, val),
|
||||||
|
setG: (val) => setValue(Comp.RGB_G, val),
|
||||||
|
setB: (val) => setValue(Comp.RGB_B, val),
|
||||||
|
},
|
||||||
|
|
||||||
|
hsv: {
|
||||||
|
setHSV: (hsv) => dispatch({ type: "SET_HSV", payload: hsv }),
|
||||||
|
setH: (val) => setValue(Comp.HSV_H, val),
|
||||||
|
setS: (val) => setValue(Comp.HSV_S, val),
|
||||||
|
setV: (val) => setValue(Comp.HSV_V, val),
|
||||||
|
},
|
||||||
|
|
||||||
|
hcl: {
|
||||||
|
setHCL: (hcl) => dispatch({ type: "SET_HCL", payload: hcl }),
|
||||||
|
setH: (val) => setValue(Comp.HCL_H, val),
|
||||||
|
setC: (val) => setValue(Comp.HCL_C, val),
|
||||||
|
setL: (val) => setValue(Comp.HCL_L, val),
|
||||||
|
},
|
||||||
|
|
||||||
|
hex: {
|
||||||
|
setHex: (hex) => dispatch({ type: "SET_HEX", payload: hex }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+47
-17
@@ -1,12 +1,13 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
|
||||||
|
|
||||||
import type { CartesianSpace } from "@/types";
|
import type { CartesianSpace, Range, Setter } from "@/types";
|
||||||
import {
|
import {
|
||||||
extractEventCoordinates,
|
extractEventCoordinates,
|
||||||
isLeftMouseButton,
|
isLeftMouseButton,
|
||||||
isTouchEvent,
|
isTouchEvent,
|
||||||
minmax,
|
minmax,
|
||||||
|
positionToValue,
|
||||||
|
valueToPosition,
|
||||||
} from "@/util";
|
} from "@/util";
|
||||||
|
|
||||||
if (typeof TouchEvent === "undefined") {
|
if (typeof TouchEvent === "undefined") {
|
||||||
@@ -19,37 +20,57 @@ export function useCrosshair({
|
|||||||
dimensions,
|
dimensions,
|
||||||
setXPosition,
|
setXPosition,
|
||||||
setYPosition,
|
setYPosition,
|
||||||
|
xValue,
|
||||||
|
yValue,
|
||||||
|
setXValue,
|
||||||
|
setYValue,
|
||||||
|
xValueRange,
|
||||||
|
yValueRange,
|
||||||
}: {
|
}: {
|
||||||
origin: CartesianSpace;
|
origin: CartesianSpace;
|
||||||
dimensions: CartesianSpace;
|
dimensions: CartesianSpace;
|
||||||
setXPosition: Dispatch<SetStateAction<number>>;
|
setXPosition: Setter<number>;
|
||||||
setYPosition: Dispatch<SetStateAction<number>>;
|
setYPosition: Setter<number>;
|
||||||
|
xValue: number;
|
||||||
|
yValue: number;
|
||||||
|
setXValue: Setter<number>;
|
||||||
|
setYValue: Setter<number>;
|
||||||
|
xValueRange: Range;
|
||||||
|
yValueRange: Range;
|
||||||
}) {
|
}) {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const crosshairRef = useRef<HTMLDivElement>(null);
|
const crosshairRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const originRef = useRef(origin);
|
const originRef = useRef(origin);
|
||||||
const dimensionsRef = useRef(dimensions);
|
const dimensionsRef = useRef(dimensions);
|
||||||
|
const setXValueRef = useRef(setXValue);
|
||||||
|
const setYValueRef = useRef(setYValue);
|
||||||
|
const xValueRangeRef = useRef(xValueRange);
|
||||||
|
const yValueRangeRef = useRef(yValueRange);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
originRef.current = origin;
|
originRef.current = origin;
|
||||||
dimensionsRef.current = dimensions;
|
dimensionsRef.current = dimensions;
|
||||||
}, [origin, dimensions]);
|
setXValueRef.current = setXValue;
|
||||||
|
setYValueRef.current = setYValue;
|
||||||
|
xValueRangeRef.current = xValueRange;
|
||||||
|
yValueRangeRef.current = yValueRange;
|
||||||
|
}, [origin, dimensions, setXValue, setYValue, xValueRange, yValueRange]);
|
||||||
|
|
||||||
const calculatePositions = useCallback(
|
const calculatePositions = useCallback((event: MouseEvent | TouchEvent) => {
|
||||||
(event: MouseEvent | TouchEvent) => {
|
const orig = originRef.current;
|
||||||
const orig = originRef.current;
|
const dims = dimensionsRef.current;
|
||||||
const dims = dimensionsRef.current;
|
|
||||||
|
|
||||||
const { clientX, clientY } = extractEventCoordinates(event);
|
const { clientX, clientY } = extractEventCoordinates(event);
|
||||||
|
|
||||||
const xPos = minmax(clientX - orig.x, 0, dims.x - 1);
|
const xPos = minmax(clientX - orig.x, 0, dims.x - 1);
|
||||||
const yPos = minmax(clientY - orig.y, 0, dims.y - 1);
|
const yPos = minmax(clientY - orig.y, 0, dims.y - 1);
|
||||||
setXPosition(xPos);
|
const newXValue = positionToValue(xPos, dims.x - 1, xValueRangeRef.current);
|
||||||
setYPosition(yPos);
|
const newYValue = positionToValue(yPos, dims.y - 1, yValueRangeRef.current);
|
||||||
},
|
|
||||||
[setXPosition, setYPosition],
|
setXValueRef.current(newXValue);
|
||||||
);
|
setYValueRef.current(newYValue);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleMove = useCallback(
|
const handleMove = useCallback(
|
||||||
(event: MouseEvent | TouchEvent) => {
|
(event: MouseEvent | TouchEvent) => {
|
||||||
@@ -101,6 +122,15 @@ export function useCrosshair({
|
|||||||
[calculatePositions, handleMove, handleEnd],
|
[calculatePositions, handleMove, handleEnd],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dims = dimensionsRef.current;
|
||||||
|
const newXPos = valueToPosition(xValue, dims.x - 1, xValueRangeRef.current);
|
||||||
|
const newYPos = valueToPosition(yValue, dims.y - 1, yValueRangeRef.current);
|
||||||
|
|
||||||
|
if (newXPos === newXPos) setXPosition(newXPos);
|
||||||
|
if (newYPos === newYPos) setYPosition(newYPos);
|
||||||
|
}, [xValue, yValue, setXPosition, setYPosition]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentRef = crosshairRef.current;
|
const currentRef = crosshairRef.current;
|
||||||
if (currentRef) {
|
if (currentRef) {
|
||||||
|
|||||||
+7
-9
@@ -35,7 +35,7 @@ export function useScroll<T extends HTMLElement>({
|
|||||||
onScrollDown: ScrollHandler;
|
onScrollDown: ScrollHandler;
|
||||||
deltaYMultiplier?: number;
|
deltaYMultiplier?: number;
|
||||||
}) {
|
}) {
|
||||||
const [_, setScrollLength] = useState(0);
|
const scrollLength = useRef(0);
|
||||||
|
|
||||||
const onScrollUpRef = useRef(onScrollUp);
|
const onScrollUpRef = useRef(onScrollUp);
|
||||||
const onScrollDownRef = useRef(onScrollDown);
|
const onScrollDownRef = useRef(onScrollDown);
|
||||||
@@ -49,16 +49,14 @@ export function useScroll<T extends HTMLElement>({
|
|||||||
|
|
||||||
const handleWheelEvent = useCallback((event: WheelEvent) => {
|
const handleWheelEvent = useCallback((event: WheelEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log("Handling wheel event.");
|
|
||||||
|
|
||||||
setScrollLength((prev) =>
|
const newScrollLength = handleScroll(
|
||||||
handleScroll(
|
scrollLength.current,
|
||||||
prev,
|
event.deltaY * deltaYMultiplierRef.current,
|
||||||
event.deltaY * deltaYMultiplierRef.current,
|
onScrollDownRef.current,
|
||||||
onScrollDownRef.current,
|
onScrollUpRef.current,
|
||||||
onScrollUpRef.current,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
scrollLength.current = newScrollLength;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addScrollListener = useCallback(() => {
|
const addScrollListener = useCallback(() => {
|
||||||
|
|||||||
+86
-39
@@ -1,12 +1,15 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
|
||||||
|
|
||||||
import type { CartesianSpace } from "@/types";
|
import type { CartesianSpace, Range, Setter } from "@/types";
|
||||||
|
import { Direction } from "@/types";
|
||||||
import {
|
import {
|
||||||
|
chooseValueByDirection,
|
||||||
extractEventCoordinates,
|
extractEventCoordinates,
|
||||||
isLeftMouseButton,
|
isLeftMouseButton,
|
||||||
isTouchEvent,
|
isTouchEvent,
|
||||||
minmax,
|
minmax,
|
||||||
|
positionToValue,
|
||||||
|
valueToPosition,
|
||||||
} from "@/util";
|
} from "@/util";
|
||||||
|
|
||||||
import { useScroll } from "./scroll";
|
import { useScroll } from "./scroll";
|
||||||
@@ -16,19 +19,6 @@ if (typeof TouchEvent === "undefined") {
|
|||||||
window.TouchEvent = window.MouseEvent;
|
window.TouchEvent = window.MouseEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Direction {
|
|
||||||
HORIZONTAL = "horizontal",
|
|
||||||
VERTICAL = "vertical",
|
|
||||||
}
|
|
||||||
|
|
||||||
function chooseValueByDirection(
|
|
||||||
direction: Direction,
|
|
||||||
xValue: number,
|
|
||||||
yValue: number,
|
|
||||||
) {
|
|
||||||
return direction === Direction.HORIZONTAL ? xValue : yValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractEventCoordinateByDirection(
|
function extractEventCoordinateByDirection(
|
||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
@@ -41,43 +31,74 @@ export function useSlider({
|
|||||||
direction,
|
direction,
|
||||||
origin,
|
origin,
|
||||||
dimensions,
|
dimensions,
|
||||||
setPosition,
|
valueRange,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
}: {
|
}: {
|
||||||
direction: Direction;
|
direction: Direction;
|
||||||
origin: CartesianSpace;
|
origin: CartesianSpace;
|
||||||
dimensions: CartesianSpace;
|
dimensions: CartesianSpace;
|
||||||
setPosition: Dispatch<SetStateAction<number>>;
|
valueRange: Range;
|
||||||
|
value: number;
|
||||||
|
setValue: Setter<number>;
|
||||||
}) {
|
}) {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const sliderRef = useRef<HTMLDivElement>(null);
|
const sliderRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Slider UI refs
|
||||||
const directionRef = useRef(direction);
|
const directionRef = useRef(direction);
|
||||||
const originRef = useRef(origin);
|
const originRef = useRef(origin);
|
||||||
const dimensionsRef = useRef(dimensions);
|
const dimensionsRef = useRef(dimensions);
|
||||||
|
|
||||||
|
// Slider value refs
|
||||||
|
const setValueRef = useRef(setValue);
|
||||||
|
const valueRangeRef = useRef(valueRange);
|
||||||
|
const maxPosition = useRef(0);
|
||||||
|
|
||||||
|
// Internal position management
|
||||||
|
const [position, setPosition] = useState(0);
|
||||||
|
const positionRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
directionRef.current = direction;
|
directionRef.current = direction;
|
||||||
originRef.current = origin;
|
originRef.current = origin;
|
||||||
dimensionsRef.current = dimensions;
|
dimensionsRef.current = dimensions;
|
||||||
}, [direction, origin, dimensions]);
|
maxPosition.current = chooseValueByDirection(
|
||||||
|
direction,
|
||||||
|
dimensions.x,
|
||||||
|
dimensions.y,
|
||||||
|
);
|
||||||
|
valueRangeRef.current = valueRange;
|
||||||
|
}, [direction, origin, dimensions, valueRangeRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValueRef.current = setValue;
|
||||||
|
}, [setValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
positionRef.current = position;
|
||||||
|
}, [position]);
|
||||||
|
|
||||||
// Setup drag handlers
|
// Setup drag handlers
|
||||||
const calculatePosition = useCallback(
|
const calculatePosition = useCallback((event: MouseEvent | TouchEvent) => {
|
||||||
(event: MouseEvent | TouchEvent) => {
|
const dir = directionRef.current;
|
||||||
const dir = directionRef.current;
|
const orig = originRef.current;
|
||||||
const orig = originRef.current;
|
const dims = dimensionsRef.current;
|
||||||
const dims = dimensionsRef.current;
|
|
||||||
|
|
||||||
const clientCoord = extractEventCoordinateByDirection(event, dir);
|
const clientCoord = extractEventCoordinateByDirection(event, dir);
|
||||||
const positionValue = minmax(
|
const newPosition = minmax(
|
||||||
clientCoord - chooseValueByDirection(dir, orig.x, orig.y),
|
clientCoord - chooseValueByDirection(dir, orig.x, orig.y),
|
||||||
0,
|
0,
|
||||||
chooseValueByDirection(dir, dims.x, dims.y),
|
chooseValueByDirection(dir, dims.x, dims.y),
|
||||||
);
|
);
|
||||||
setPosition(positionValue);
|
const newValue = positionToValue(
|
||||||
},
|
newPosition,
|
||||||
[setPosition],
|
maxPosition.current,
|
||||||
);
|
valueRangeRef.current,
|
||||||
|
);
|
||||||
|
|
||||||
|
setValueRef.current(newValue);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleMove = useCallback(
|
const handleMove = useCallback(
|
||||||
(event: MouseEvent | TouchEvent) => {
|
(event: MouseEvent | TouchEvent) => {
|
||||||
@@ -120,20 +141,37 @@ export function useSlider({
|
|||||||
const dims = dimensionsRef.current;
|
const dims = dimensionsRef.current;
|
||||||
const inc = chooseValueByDirection(dir, 1, -1);
|
const inc = chooseValueByDirection(dir, 1, -1);
|
||||||
|
|
||||||
setPosition((prev: number) =>
|
const newPosition = minmax(
|
||||||
minmax(prev + inc, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
positionRef.current + inc,
|
||||||
|
0,
|
||||||
|
chooseValueByDirection(dir, dims.x, dims.y),
|
||||||
);
|
);
|
||||||
}, [setPosition]);
|
const newValue = positionToValue(
|
||||||
|
newPosition,
|
||||||
|
maxPosition.current,
|
||||||
|
valueRangeRef.current,
|
||||||
|
);
|
||||||
|
setValueRef.current(newValue);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleScrollDown = useCallback(() => {
|
const handleScrollDown = useCallback(() => {
|
||||||
const dir = directionRef.current;
|
const dir = directionRef.current;
|
||||||
const dims = dimensionsRef.current;
|
const dims = dimensionsRef.current;
|
||||||
const inc = chooseValueByDirection(dir, -1, 1);
|
const inc = chooseValueByDirection(dir, -1, 1);
|
||||||
|
|
||||||
setPosition((prev: number) =>
|
const newPosition = minmax(
|
||||||
minmax(prev + inc, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
positionRef.current + inc,
|
||||||
|
0,
|
||||||
|
chooseValueByDirection(dir, dims.x, dims.y),
|
||||||
);
|
);
|
||||||
}, [setPosition]);
|
const newValue = positionToValue(
|
||||||
|
newPosition,
|
||||||
|
maxPosition.current,
|
||||||
|
valueRangeRef.current,
|
||||||
|
);
|
||||||
|
|
||||||
|
setValueRef.current(newValue);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { addScrollListener, removeScrollListener } = useScroll({
|
const { addScrollListener, removeScrollListener } = useScroll({
|
||||||
targetRef: sliderRef,
|
targetRef: sliderRef,
|
||||||
@@ -141,6 +179,15 @@ export function useSlider({
|
|||||||
onScrollDown: handleScrollDown,
|
onScrollDown: handleScrollDown,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newPosition = valueToPosition(
|
||||||
|
value,
|
||||||
|
maxPosition.current,
|
||||||
|
valueRangeRef.current,
|
||||||
|
);
|
||||||
|
setPosition(newPosition);
|
||||||
|
}, [value, setPosition]);
|
||||||
|
|
||||||
// Set up entry listeners
|
// Set up entry listeners
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentRef = sliderRef.current;
|
const currentRef = sliderRef.current;
|
||||||
|
|||||||
@@ -0,0 +1,572 @@
|
|||||||
|
import * as colorlib from "colorlib";
|
||||||
|
import { beforeEach, describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { colorReducer, createColorActions } from "../color";
|
||||||
|
import type { ColorAction, ColorActions, ColorState } from "../color";
|
||||||
|
|
||||||
|
const mockUseReducer = <T extends object, U>(
|
||||||
|
reducer: (state: T, action: U) => T,
|
||||||
|
initialArg: T,
|
||||||
|
): [T, (value: U) => void] => {
|
||||||
|
let currentState = initialArg;
|
||||||
|
|
||||||
|
const state = new Proxy({} as T, {
|
||||||
|
get: (_, prop) => currentState[prop as keyof T],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dispatch = (value: U) => {
|
||||||
|
const nextState = reducer(currentState, value);
|
||||||
|
currentState = nextState;
|
||||||
|
};
|
||||||
|
return [state, dispatch];
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectRGB = (value: colorlib.RGB, expected: colorlib.RGB) => {
|
||||||
|
expect(value.r).toBe(expected.r);
|
||||||
|
expect(value.g).toBe(expected.g);
|
||||||
|
expect(value.b).toBe(expected.b);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectHSV = (value: colorlib.HSV, expected: colorlib.HSV) => {
|
||||||
|
expect(value.h).toBe(expected.h);
|
||||||
|
expect(value.s).toBe(expected.s);
|
||||||
|
expect(value.v).toBe(expected.v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectHCL = (value: colorlib.HCL, expected: colorlib.HCL) => {
|
||||||
|
expect(value.h).toBe(expected.h);
|
||||||
|
expect(value.c).toBe(expected.c);
|
||||||
|
expect(value.l).toBe(expected.l);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("color reducer", () => {
|
||||||
|
const initialState = { color: colorlib.Color.from_hex("000") };
|
||||||
|
|
||||||
|
describe("set by color", () => {
|
||||||
|
test("set color", () => {
|
||||||
|
const nextColor = colorlib.Color.from_hex("F00");
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_COLOR",
|
||||||
|
payload: nextColor,
|
||||||
|
});
|
||||||
|
expect(nextState.color.hex.to_code()).toBe(nextColor.hex.to_code());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("set by color space", () => {
|
||||||
|
test("set rgb", () => {
|
||||||
|
const nextColor = colorlib.RGB.new(1, 2, 3);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_RGB",
|
||||||
|
payload: nextColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hsv", () => {
|
||||||
|
const nextColor = colorlib.HSV.new(1, 2, 3);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_HSV",
|
||||||
|
payload: nextColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hcl", () => {
|
||||||
|
const nextColor = colorlib.HCL.new(1, 2, 3);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_HCL",
|
||||||
|
payload: nextColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hex", () => {
|
||||||
|
const nextColor = colorlib.Hex.new(1, 2, 3);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_HEX",
|
||||||
|
payload: nextColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.color.hex.to_code()).toBe(nextColor.to_code());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("set by component", () => {
|
||||||
|
describe("rgb", () => {
|
||||||
|
test("set rgb r", () => {
|
||||||
|
const nextColor = colorlib.RGB.new(100, 0, 0);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_R,
|
||||||
|
payload: nextColor.r,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set rgb g", () => {
|
||||||
|
const nextColor = colorlib.RGB.new(0, 100, 0);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_G,
|
||||||
|
payload: nextColor.g,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set rgb b", () => {
|
||||||
|
const nextColor = colorlib.RGB.new(0, 0, 100);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_B,
|
||||||
|
payload: nextColor.b,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hsv", () => {
|
||||||
|
test("set hsv h", () => {
|
||||||
|
const nextColor = colorlib.HSV.new(100, 0, 0);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_H,
|
||||||
|
payload: nextColor.h,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hsv s", () => {
|
||||||
|
const nextColor = colorlib.HSV.new(0, 0.5, 0);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_S,
|
||||||
|
payload: nextColor.s,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hsv v", () => {
|
||||||
|
const nextColor = colorlib.HSV.new(0, 0, 0.5);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_V,
|
||||||
|
payload: nextColor.v,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hcl", () => {
|
||||||
|
test("set hcl h", () => {
|
||||||
|
const nextColor = colorlib.HCL.new(100, 0, 0);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_H,
|
||||||
|
payload: nextColor.h,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hcl c", () => {
|
||||||
|
const nextColor = colorlib.HCL.new(0, 0.5, 0);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_C,
|
||||||
|
payload: nextColor.c,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hcl l", () => {
|
||||||
|
const nextColor = colorlib.HCL.new(0, 0, 0.5);
|
||||||
|
const nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_L,
|
||||||
|
payload: nextColor.l,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("adjust by component", () => {
|
||||||
|
describe("rgb", () => {
|
||||||
|
test("adjust rgb r", () => {
|
||||||
|
let nextColor = colorlib.RGB.new(100, 0, 0);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_R,
|
||||||
|
payload: (prev) => prev + 100,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.RGB.new(50, 0, 0);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_R,
|
||||||
|
payload: (prev) => prev - 50,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust rgb g", () => {
|
||||||
|
let nextColor = colorlib.RGB.new(0, 100, 0);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_G,
|
||||||
|
payload: (prev) => prev + 100,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.RGB.new(0, 50, 0);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_G,
|
||||||
|
payload: (prev) => prev - 50,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust rgb b", () => {
|
||||||
|
let nextColor = colorlib.RGB.new(0, 0, 100);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_B,
|
||||||
|
payload: (prev) => prev + 100,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.RGB.new(0, 0, 50);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.RGB_B,
|
||||||
|
payload: (prev) => prev - 50,
|
||||||
|
});
|
||||||
|
expectRGB(nextState.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hsv", () => {
|
||||||
|
test("adjust hsv h", () => {
|
||||||
|
let nextColor = colorlib.HSV.new(100, 0, 0);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_H,
|
||||||
|
payload: (prev) => prev + 100,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HSV.new(50, 0, 0);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_H,
|
||||||
|
payload: (prev) => prev - 50,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust hsv s", () => {
|
||||||
|
let nextColor = colorlib.HSV.new(0, 1, 0);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_S,
|
||||||
|
payload: (prev) => prev + 1,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HSV.new(0, 0.5, 0);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_S,
|
||||||
|
payload: (prev) => prev - 0.5,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust hsv v", () => {
|
||||||
|
let nextColor = colorlib.HSV.new(0, 0, 1);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_V,
|
||||||
|
payload: (prev) => prev + 1,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HSV.new(0, 0, 0.5);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HSV_V,
|
||||||
|
payload: (prev) => prev - 0.5,
|
||||||
|
});
|
||||||
|
expectHSV(nextState.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hcl", () => {
|
||||||
|
test("adjust hcl h", () => {
|
||||||
|
let nextColor = colorlib.HCL.new(100, 0, 0);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_H,
|
||||||
|
payload: (prev) => prev + 100,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HCL.new(50, 0, 0);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_H,
|
||||||
|
payload: (prev) => prev - 50,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust hcl c", () => {
|
||||||
|
let nextColor = colorlib.HCL.new(0, 1, 0);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_C,
|
||||||
|
payload: (prev) => prev + 1,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HCL.new(0, 0.5, 0);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_C,
|
||||||
|
payload: (prev) => prev - 0.5,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust hcl l", () => {
|
||||||
|
let nextColor = colorlib.HCL.new(0, 0, 1);
|
||||||
|
let nextState = colorReducer(initialState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_L,
|
||||||
|
payload: (prev) => prev + 1,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HCL.new(0, 0, 0.5);
|
||||||
|
nextState = colorReducer(nextState, {
|
||||||
|
type: "SET_VALUE",
|
||||||
|
component: colorlib.Component.HCL_L,
|
||||||
|
payload: (prev) => prev - 0.5,
|
||||||
|
});
|
||||||
|
expectHCL(nextState.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("color actions", () => {
|
||||||
|
const initialState = { color: colorlib.Color.from_hex("000") };
|
||||||
|
|
||||||
|
let state: ColorState;
|
||||||
|
let dispatch: (value: ColorAction) => void;
|
||||||
|
let actions: ColorActions;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
[state, dispatch] = mockUseReducer(colorReducer, initialState);
|
||||||
|
actions = createColorActions(dispatch);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("set by color", () => {
|
||||||
|
test("set color", () => {
|
||||||
|
const nextColor = colorlib.Color.from_hex("FF0000");
|
||||||
|
actions.common.setColor(nextColor);
|
||||||
|
expect(state.color.hex.to_code()).toBe(nextColor.hex.to_code());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("set by color space", () => {
|
||||||
|
test("set rgb", () => {
|
||||||
|
const nextColor = colorlib.RGB.new(1, 2, 3);
|
||||||
|
actions.rgb.setRGB(nextColor);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hsv", () => {
|
||||||
|
const nextColor = colorlib.HSV.new(1, 2, 3);
|
||||||
|
actions.hsv.setHSV(nextColor);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hcl", () => {
|
||||||
|
const nextColor = colorlib.HCL.new(1, 2, 3);
|
||||||
|
actions.hcl.setHCL(nextColor);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hex", () => {
|
||||||
|
const nextColor = colorlib.Hex.from_code("FF0000");
|
||||||
|
actions.hex.setHex(nextColor);
|
||||||
|
expect(state.color.hex.to_code()).toBe(nextColor.to_code());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("set by component", () => {
|
||||||
|
describe("rgb", () => {
|
||||||
|
test("set rgb r", () => {
|
||||||
|
const nextColor = colorlib.RGB.new(100, 0, 0);
|
||||||
|
actions.rgb.setR(nextColor.r);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set rgb g", () => {
|
||||||
|
const nextColor = colorlib.RGB.new(0, 100, 0);
|
||||||
|
actions.rgb.setG(nextColor.g);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set rgb b", () => {
|
||||||
|
const nextColor = colorlib.RGB.new(0, 0, 100);
|
||||||
|
actions.rgb.setB(nextColor.b);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hsv", () => {
|
||||||
|
test("set hsv h", () => {
|
||||||
|
const nextColor = colorlib.HSV.new(100, 0, 0);
|
||||||
|
actions.hsv.setH(nextColor.h);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hsv s", () => {
|
||||||
|
const nextColor = colorlib.HSV.new(0, 0.5, 0);
|
||||||
|
actions.hsv.setS(nextColor.s);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hsv v", () => {
|
||||||
|
const nextColor = colorlib.HSV.new(0, 0, 0.5);
|
||||||
|
actions.hsv.setV(nextColor.v);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hcl", () => {
|
||||||
|
test("set hcl h", () => {
|
||||||
|
const nextColor = colorlib.HCL.new(100, 0, 0);
|
||||||
|
actions.hcl.setH(nextColor.h);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hcl c", () => {
|
||||||
|
const nextColor = colorlib.HCL.new(0, 0.5, 0);
|
||||||
|
actions.hcl.setC(nextColor.c);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set hcl l", () => {
|
||||||
|
const nextColor = colorlib.HCL.new(0, 0, 0.5);
|
||||||
|
actions.hcl.setL(nextColor.l);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("adjust by component", () => {
|
||||||
|
describe("rgb", () => {
|
||||||
|
test("adjust rgb r", () => {
|
||||||
|
let nextColor = colorlib.RGB.new(100, 0, 0);
|
||||||
|
actions.rgb.setR((prev) => prev + 100);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.RGB.new(50, 0, 0);
|
||||||
|
actions.rgb.setR((prev) => prev - 50);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust rgb g", () => {
|
||||||
|
let nextColor = colorlib.RGB.new(0, 100, 0);
|
||||||
|
actions.rgb.setG((prev) => prev + 100);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.RGB.new(0, 50, 0);
|
||||||
|
actions.rgb.setG((prev) => prev - 50);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust rgb b", () => {
|
||||||
|
let nextColor = colorlib.RGB.new(0, 0, 100);
|
||||||
|
actions.rgb.setB((prev) => prev + 100);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.RGB.new(0, 0, 50);
|
||||||
|
actions.rgb.setB((prev) => prev - 50);
|
||||||
|
expectRGB(state.color.rgb, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hsv", () => {
|
||||||
|
test("adjust hsv h", () => {
|
||||||
|
let nextColor = colorlib.HSV.new(100, 0, 0);
|
||||||
|
actions.hsv.setH((prev) => prev + 100);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HSV.new(50, 0, 0);
|
||||||
|
actions.hsv.setH((prev) => prev - 50);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust hsv s", () => {
|
||||||
|
let nextColor = colorlib.HSV.new(0, 1, 0);
|
||||||
|
actions.hsv.setS((prev) => prev + 1);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HSV.new(0, 0.5, 0);
|
||||||
|
actions.hsv.setS((prev) => prev - 0.5);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust hsv v", () => {
|
||||||
|
let nextColor = colorlib.HSV.new(0, 0, 1);
|
||||||
|
actions.hsv.setV((prev) => prev + 1);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HSV.new(0, 0, 0.5);
|
||||||
|
actions.hsv.setV((prev) => prev - 0.5);
|
||||||
|
expectHSV(state.color.hsv, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hcl", () => {
|
||||||
|
test("adjust hcl h", () => {
|
||||||
|
let nextColor = colorlib.HCL.new(100, 0, 0);
|
||||||
|
actions.hcl.setH((prev) => prev + 100);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HCL.new(50, 0, 0);
|
||||||
|
actions.hcl.setH((prev) => prev - 50);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust hcl c", () => {
|
||||||
|
let nextColor = colorlib.HCL.new(0, 1, 0);
|
||||||
|
actions.hcl.setC((prev) => prev + 1);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HCL.new(0, 0.5, 0);
|
||||||
|
actions.hcl.setC((prev) => prev - 0.5);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adjust hcl l", () => {
|
||||||
|
let nextColor = colorlib.HCL.new(0, 0, 1);
|
||||||
|
actions.hcl.setL((prev) => prev + 1);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
|
||||||
|
nextColor = colorlib.HCL.new(0, 0, 0.5);
|
||||||
|
actions.hcl.setL((prev) => prev - 0.5);
|
||||||
|
expectHCL(state.color.hcl, nextColor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,12 +11,22 @@ function TestSquare() {
|
|||||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const [xPosition, setXPosition] = useState(0);
|
const [xPosition, setXPosition] = useState(0);
|
||||||
const [yPosition, setYPosition] = useState(0);
|
const [yPosition, setYPosition] = useState(0);
|
||||||
|
const [xValue, setXValue] = useState(0);
|
||||||
|
const [yValue, setYValue] = useState(0);
|
||||||
|
const xValueRange = { min: 0, max: 100 };
|
||||||
|
const yValueRange = { min: 0, max: 100 };
|
||||||
|
|
||||||
const { crosshairRef, isDragging } = useCrosshair({
|
const { crosshairRef, isDragging } = useCrosshair({
|
||||||
origin,
|
origin,
|
||||||
dimensions,
|
dimensions,
|
||||||
setXPosition,
|
setXPosition,
|
||||||
setYPosition,
|
setYPosition,
|
||||||
|
xValue,
|
||||||
|
yValue,
|
||||||
|
setXValue,
|
||||||
|
setYValue,
|
||||||
|
xValueRange,
|
||||||
|
yValueRange,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundaryRef = useRef<HTMLDivElement>(null);
|
const boundaryRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -98,6 +108,10 @@ function TestSquare() {
|
|||||||
{isDragging ? "True" : "False"}
|
{isDragging ? "True" : "False"}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
X Value: <span data-cy="x-value-display">{xValue}</span>
|
||||||
|
<br />Y Value: <span data-cy="y-value-display">{yValue}</span>
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import type { CartesianSpace } from "@/types";
|
import type { CartesianSpace } from "@/types";
|
||||||
|
import { Direction } from "@/types";
|
||||||
|
import { chooseValueByDirection, valueToPosition } from "@/util";
|
||||||
|
|
||||||
import { Direction, useSlider } from "../slider";
|
import { useSlider } from "../slider";
|
||||||
|
|
||||||
// Test Fixtures
|
// Test Fixtures
|
||||||
|
|
||||||
@@ -13,13 +15,16 @@ function TestSlider({
|
|||||||
}) {
|
}) {
|
||||||
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const [position, setPosition] = useState(0);
|
const [value, setValue] = useState(0);
|
||||||
const [sliderValue, setSliderValue] = useState(0);
|
const position = useRef(0);
|
||||||
|
const valueRange = { min: 0, max: 100 };
|
||||||
const { sliderRef, isDragging } = useSlider({
|
const { sliderRef, isDragging } = useSlider({
|
||||||
direction,
|
direction,
|
||||||
origin,
|
origin,
|
||||||
dimensions,
|
dimensions,
|
||||||
setPosition,
|
valueRange,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
const railRef = useRef<HTMLSpanElement>(null);
|
const railRef = useRef<HTMLSpanElement>(null);
|
||||||
@@ -41,12 +46,23 @@ function TestSlider({
|
|||||||
const maxValue =
|
const maxValue =
|
||||||
direction == Direction.HORIZONTAL ? dimensions.x : dimensions.y;
|
direction == Direction.HORIZONTAL ? dimensions.x : dimensions.y;
|
||||||
if (maxValue > 0) {
|
if (maxValue > 0) {
|
||||||
const percentage = parseFloat(((position / maxValue) * 100).toFixed(3));
|
const percentage = parseFloat(
|
||||||
setSliderValue(percentage);
|
((position.current / maxValue) * 100).toFixed(3),
|
||||||
|
);
|
||||||
|
setValue(percentage);
|
||||||
} else {
|
} else {
|
||||||
setSliderValue(0);
|
setValue(0);
|
||||||
}
|
}
|
||||||
}, [dimensions, direction, position]);
|
}, [dimensions, direction]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const maxPosition = chooseValueByDirection(
|
||||||
|
direction,
|
||||||
|
dimensions.x,
|
||||||
|
dimensions.y,
|
||||||
|
);
|
||||||
|
position.current = valueToPosition(value, maxPosition, valueRange);
|
||||||
|
}, [value, direction, dimensions, valueRange]);
|
||||||
|
|
||||||
const isHorizontal = direction === Direction.HORIZONTAL;
|
const isHorizontal = direction === Direction.HORIZONTAL;
|
||||||
|
|
||||||
@@ -82,8 +98,8 @@ function TestSlider({
|
|||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
...(isHorizontal
|
...(isHorizontal
|
||||||
? { left: position, top: 0 }
|
? { left: position.current, top: 0 }
|
||||||
: { left: 0, top: position }),
|
: { left: 0, top: position.current }),
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
background: "rgba(255,0,0,0.5)",
|
background: "rgba(255,0,0,0.5)",
|
||||||
@@ -92,8 +108,9 @@ function TestSlider({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Position: <span data-cy="position-display">{position}</span>px
|
Value: <span data-cy="value-display">{value}</span>
|
||||||
<br /> Value: <span data-cy="value-display">{sliderValue}</span>
|
<br /> Position:{" "}
|
||||||
|
<span data-cy="position-display">{position.current}</span>px
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
|
// Interfaces
|
||||||
export interface CartesianSpace {
|
export interface CartesianSpace {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Range {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export type Setter<T> = (valueOrCallback: SetterValueOrCallback<T>) => void;
|
||||||
|
export type SetterValueOrCallback<T> = T | ((prev: T) => T);
|
||||||
|
export type Timeout = ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
export enum Direction {
|
||||||
|
HORIZONTAL = "horizontal",
|
||||||
|
VERTICAL = "vertical",
|
||||||
|
}
|
||||||
|
|||||||
+46
-1
@@ -1,6 +1,7 @@
|
|||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
|
|
||||||
import type { CartesianSpace } from "./types";
|
import type { CartesianSpace, Range } from "./types";
|
||||||
|
import { Direction } from "./types";
|
||||||
|
|
||||||
export function minmax(number: number, min: number, max: number) {
|
export function minmax(number: number, min: number, max: number) {
|
||||||
return Math.min(max, Math.max(min, number));
|
return Math.min(max, Math.max(min, number));
|
||||||
@@ -44,3 +45,47 @@ export function setMeasurements(
|
|||||||
setDimensions({ x: rect.width, y: rect.height });
|
setDimensions({ x: rect.width, y: rect.height });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function valueToPosition(
|
||||||
|
value: number,
|
||||||
|
maxPosition: number,
|
||||||
|
valueRange: Range,
|
||||||
|
) {
|
||||||
|
if (maxPosition <= 0 || !isFinite(value)) return 0;
|
||||||
|
|
||||||
|
const rangeSpan = Math.abs(valueRange.min) + Math.abs(valueRange.max);
|
||||||
|
if (rangeSpan === 0) return 0;
|
||||||
|
|
||||||
|
const position = Math.round(
|
||||||
|
maxPosition *
|
||||||
|
((value + Math.abs(valueRange.min)) /
|
||||||
|
(Math.abs(valueRange.min) + Math.abs(valueRange.max))),
|
||||||
|
);
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function positionToValue(
|
||||||
|
position: number,
|
||||||
|
maxPosition: number,
|
||||||
|
valueRange: Range,
|
||||||
|
) {
|
||||||
|
if (maxPosition <= 0 || !isFinite(position)) return valueRange.min;
|
||||||
|
|
||||||
|
const rangeSpan = Math.abs(valueRange.min) + Math.abs(valueRange.max);
|
||||||
|
if (rangeSpan === 0) return valueRange.min;
|
||||||
|
|
||||||
|
const value =
|
||||||
|
(position / maxPosition) *
|
||||||
|
(Math.abs(valueRange.min) + Math.abs(valueRange.max)) -
|
||||||
|
Math.abs(valueRange.min);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chooseValueByDirection(
|
||||||
|
direction: Direction,
|
||||||
|
xValue: number,
|
||||||
|
yValue: number,
|
||||||
|
) {
|
||||||
|
return direction === Direction.HORIZONTAL ? xValue : yValue;
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -20,5 +20,5 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts", "cypress.config.ts"]
|
"include": ["vite.config.ts", "cypress.config.ts", "vitest.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,8 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
},
|
},
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import topLevelAwait from "vite-plugin-top-level-await";
|
||||||
|
import wasm from "vite-plugin-wasm";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [wasm(), topLevelAwait()],
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user