Completed color picker, value editor, desktop layout.

This commit is contained in:
Jay
2025-08-13 18:05:29 -04:00
parent ae02e49ce2
commit 7a2e4cf2ae
22 changed files with 533 additions and 339 deletions
+2 -2
View File
@@ -55,9 +55,9 @@
"^react$", "^react$",
"^react-dom(.*)$", "^react-dom(.*)$",
"^react(.*)$", "^react(.*)$",
"^@(?!(components|hooks|providers|/))(.*)$", "^@(?!(/))(.*)$",
"^(?!@|[.])(.*)$", "^(?!@|[.])(.*)$",
"^@(/|components|hooks|providers)(.*)$", "^@(/)(.*)$",
"^[./]" "^[./]"
], ],
"importOrderSeparation": true, "importOrderSeparation": true,
+48 -107
View File
@@ -1,7 +1,42 @@
.appWrapper { .appWrapper {
background-color: white;
height: 100%; height: 100%;
width: 100%; width: 100%;
max-width: 1200px;
overflow: hidden; overflow: hidden;
margin: 0 auto;
box-shadow: 0 0 40px #7a7a7a;
border-left: 2px solid #7a7a7a;
border-right: 2px solid #7a7a7a;
}
.appHeader {
height: 40px;
display: flex;
align-items: baseline;
border-bottom: 2px solid #7a7a7a;
padding: 20px 30px 12px;
}
.appHeader .title {
/* Typography */
font-family: "Inter", "Roboto", sans-serif;
font-size: 2rem;
font-weight: 400;
letter-spacing: 0.4rem;
text-transform: uppercase;
line-height: 1;
}
.appHeader .subtitle {
margin-left: 1.5rem;
font-family: "Inter", "Roboto", sans-serif;
font-style: italic;
font-size: 1rem;
font-weight: 300;
letter-spacing: 0.08rem;
line-height: 1;
color: #7a7a7a;
} }
.mobileContent, .mobileContent,
@@ -17,12 +52,7 @@
scrollbar-width: none; scrollbar-width: none;
} }
/* Large */
@media (min-width: 992px),
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) {
.appWrapper { .appWrapper {
max-width: 1200px;
margin: 0 auto;
} }
.mobileContent { .mobileContent {
@@ -30,18 +60,24 @@
} }
.mainContent { .mainContent {
display: flex; display: grid;
grid-template-columns: 1fr 2fr;
} }
.firstZone { .firstZone {
flex: 1; display: flex;
flex-direction: column;
border-right: 2px solid #7a7a7a;
} }
.secondZone { .secondZone {
flex: 1; padding: 40px;
color: #555;
font-style: italic;
} }
.colorPickerWrapper { .colorPickerWrapper {
border-bottom: 2px solid #7a7a7a;
} }
.colorValuesWrapper { .colorValuesWrapper {
@@ -52,111 +88,16 @@
.paletteLibraryWrapper { .paletteLibraryWrapper {
} }
/* Large */
@media (min-width: 992px),
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) {
} }
/* Landscape Phone */ /* Landscape Phone */
@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) { @media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) {
.appWrapper {
}
.mainContent,
.mobileTopNav {
display: none;
}
.mobileContent {
display: flex;
}
.mobileLeftNav {
}
.mobileRightNav {
}
.mobileFirstZone {
flex: 1;
}
.mobileSecondZone {
flex: 1;
}
.tabWrapper {
max-height: 100%;
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
.tabWrapper .tab {
scroll-snap-align: start;
}
.colorPickerWrapper {
}
.colorValuesWrapper {
}
.paletteEditorWrapper {
}
.paletteLibraryWrapper {
}
} }
/* Portrait Phone */ /* Portrait Phone */
@media (max-width: 567px) { @media (max-width: 567px) {
.appWrapper {
}
.mainContent,
.mobileRightNav,
.mobileLeftNav {
display: none;
}
.mobileContent {
}
.mobileTopNav {
display: flex;
}
.leftMenuButton {
margin-right: auto;
}
.rightMenuButton {
margin-left: auto;
}
.mobileFirstZone {
}
.mobileSecondZone {
}
.tabWrapper {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
}
.tabWrapper .tab {
min-width: 100vw;
scroll-snap-align: start;
}
.colorPickerWrapper {
}
.colorValuesWrapper {
}
.paletteEditorWrapper {
}
.paletteLibraryWrapper {
}
} }
+49 -9
View File
@@ -1,14 +1,16 @@
import { useReducer, useState } from "react"; import { useState } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { Color } from "colorlib";
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";
import { useMediaQuery } from "@providers/hooks"; import { useMediaQuery } from "@/providers/hooks";
import { useSelectedColor } from "@providers/hooks"; import { useSelectedColor } from "@/providers/hooks";
import styles from "./App.module.css"; import styles from "./App.module.css";
import { formatCssRgb } from "./util";
// Menu Button Components // Menu Button Components
@@ -106,7 +108,7 @@ function MobileFirstZone() {
aria-roledescription="slide" aria-roledescription="slide"
aria-label="Color Picker" aria-label="Color Picker"
> >
<ColorPicker /> <ColorPicker color={selectedColor} actions={selectedColorActions} />
</div> </div>
<div <div
className={clsx(styles.tab, styles.colorValuesWrapper)} className={clsx(styles.tab, styles.colorValuesWrapper)}
@@ -191,13 +193,17 @@ 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();
return ( return (
<section className={styles.firstZone} aria-label="Color tools"> <section className={styles.firstZone} aria-label="Color tools">
<div className={styles.colorPickerWrapper} aria-label="Color picker"> <div className={styles.colorPickerWrapper} aria-label="Color picker">
<ColorPicker /> <ColorPicker color={selectedColor} actions={selectedColorActions} />
</div> </div>
<div className={styles.colorValuesWrapper} aria-label="Color values"> <div className={styles.colorValuesWrapper} aria-label="Color values">
@@ -210,6 +216,7 @@ function FirstZone() {
function SecondZone() { function SecondZone() {
return ( return (
<section className={styles.secondZone} aria-label="Palette tools"> <section className={styles.secondZone} aria-label="Palette tools">
Palette Creator Coming Soon.
<div <div
className={styles.paletteEditorWrapper} className={styles.paletteEditorWrapper}
aria-label="Palette editor" aria-label="Palette editor"
@@ -224,10 +231,16 @@ function SecondZone() {
function DesktopContent() { function DesktopContent() {
return ( return (
<>
<header className={styles.appHeader}>
<span className={styles.title}>LUMINANCE</span>
<span className={styles.subtitle}>A color picker for humans.</span>
</header>
<main className={styles.mainContent}> <main className={styles.mainContent}>
<FirstZone /> <FirstZone />
<SecondZone /> <SecondZone />
</main> </main>
</>
); );
} }
@@ -236,9 +249,35 @@ function DesktopContent() {
function App() { function App() {
const [isRightMenuOpen, setIsRightMenuOpen] = useState(false); const [isRightMenuOpen, setIsRightMenuOpen] = useState(false);
const [isLeftMenuOpen, setIsLeftMenuOpen] = useState(false); const [isLeftMenuOpen, setIsLeftMenuOpen] = useState(false);
const { isDesktop } = useMediaQuery(); // const { isDesktop } = useMediaQuery();
const isDesktop = true;
const lum = 0.75;
const chr = 0.8;
const steps = 8;
const colors = Array.from({ length: steps }, (_, index) => {
const hue = (index * 360) / (steps - 1);
return Color.from_hcl(hue, chr, lum);
});
const colorGradient = colors
.map((color, index) => {
const colorString = formatCssRgb(color.hex);
const percentage = (index / (colors.length - 1)) * 100;
return `${colorString} ${percentage}%`;
})
.join(", ");
return ( return (
<div
className={styles.background}
style={{
width: "100%",
height: "100%",
background: `linear-gradient(180deg, ${colorGradient})`,
}}
>
<div className={styles.appWrapper} role="application"> <div className={styles.appWrapper} role="application">
{!isDesktop && ( {!isDesktop && (
<MobileContent <MobileContent
@@ -251,6 +290,7 @@ function App() {
{isDesktop && <DesktopContent />} {isDesktop && <DesktopContent />}
</div> </div>
</div>
); );
} }
@@ -1,36 +1,110 @@
.container { .container {
width: 100%; padding: 40px;
height: 100%; display: grid;
display: flex; grid-template-columns: 25px 1fr 25px;
align-items: center; grid-template-rows: 50px 1fr 25px;
justify-content: center; grid-template-areas:
flex-direction: column; ". preview ."
"leftGrip square rightGrip"
". bottomGrip ."
". bar .";
} }
.pickerSquare { .pickerSquare {
flex: 1; grid-area: square;
position: relative;
aspect-ratio: 1/1;
border: 2px solid #7a7a7a;
} }
.pickerBar { .pickerBar {
flex: 1; grid-area: bar;
position: relative;
height: 25px;
margin-top: 15px;
border: 2px solid #7a7a7a;
} }
/* Large - Landscape Tablets / Desktops */ .verticalGripLeft {
/* Medium - Portrait Tablets */ grid-area: leftGrip;
/* Horizontal layout, vertically scrolling picker and palette content */
@media (min-width: 992px),
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) {
} }
/* Medium - Landscape Phones */ .verticalGripRight {
/* Horizontal layout, side menu, vertical tabbed picker */ grid-area: rightGrip;
@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) {
.container {
flex-direction: row;
}
} }
/* Small - Portrait Phones*/ .horizontalGrip {
/* Vertical layout, side menu, horizontal tabbed picker */ grid-area: bottomGrip;
@media (max-width: 567px) { }
/* Color Square */
.colorSquareWrapper {
height: 100%;
width: 100%;
}
.colorSquare {
cursor: crosshair;
}
/* Color Bar */
.colorBarWrapper {
height: 100%;
width: 100%;
}
.colorBar {
cursor: crosshair;
}
/* Crosshairs */
.crosshairWrapper {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
pointer-events: none;
}
.crosshair {
position: absolute;
transition:
background-color 400ms,
box-shadow 400ms;
pointer-events: none;
}
.crossEye {
position: absolute;
width: 9px;
height: 9px;
z-index: 2;
border-radius: 9px;
border-width: 2px;
border-style: solid;
transition:
border-color 350ms,
box-shadow 400ms;
pointer-events: none;
}
/* Square Grips */
.gripSlider {
position: relative;
width: 100%;
height: 100%;
}
.grip {
position: absolute;
width: 0;
height: 0;
}
/* Preview */
.preview {
grid-area: preview;
margin-bottom: 15px;
border: 2px solid #7a7a7a;
} }
+102 -5
View File
@@ -1,10 +1,107 @@
import styles from "./ColorPicker.module.css"; import { useEffect, useRef, useState } from "react";
import * as colorlib from "colorlib";
import type { ColorActions } from "@/hooks/color";
import { useResize } from "@/hooks/window";
import { Direction } from "@/types";
import type { CartesianSpace } from "@/types";
import { formatCssRgb, setMeasurements } from "@/util";
import ColorBar from "./ColorBar";
import styles from "./ColorPicker.module.css";
import ColorSquare from "./ColorSquare";
import { BarCrosshair, SquareCrosshair } from "./Crosshair";
import GripSlider from "./GripSlider";
function ColorPicker({
color,
actions,
}: {
color: colorlib.Color;
actions: ColorActions;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const hueRange = { min: 0, max: 359 };
const lumRange = { min: 0, max: 1 };
const [_origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
// Get measurements
useEffect(() => {
if (containerRef.current) {
setMeasurements(containerRef, setOrigin, setDimensions);
}
return useResize(() => {
setMeasurements(containerRef, setOrigin, setDimensions);
});
}, [containerRef.current]);
function ColorPicker() {
return ( return (
<div className={styles.container}> <div className={styles.container} ref={containerRef}>
<div className={styles.pickerSquare}>Square</div> <div
<div className={styles.pickerBar}>Bar</div> className={styles.preview}
style={{
backgroundColor: formatCssRgb(color.hex),
}}
></div>
<div className={styles.verticalGripLeft}>
<GripSlider
direction={Direction.VERTICAL}
value={color.hcl.l}
setValue={actions.hcl.setL}
valueRange={lumRange}
arrowDirection="right"
invert={true}
/>
</div>
<div className={styles.pickerSquare}>
<SquareCrosshair
hue={color.hcl.h}
luminance={color.hcl.l}
hex={color.hex}
/>
<ColorSquare
chroma={color.hcl.c}
actions={actions.hcl}
parentDimensions={dimensions}
/>
</div>
<div className={styles.verticalGripRight}>
<GripSlider
direction={Direction.VERTICAL}
value={color.hcl.l}
setValue={actions.hcl.setL}
valueRange={lumRange}
arrowDirection="left"
invert={true}
/>
</div>
<div className={styles.horizontalGrip}>
<GripSlider
direction={Direction.HORIZONTAL}
value={color.hcl.h}
setValue={actions.hcl.setH}
valueRange={hueRange}
arrowDirection="up"
/>
</div>
<div className={styles.pickerBar}>
<ColorBar
hue={color.hcl.h}
chroma={color.hcl.c}
luminance={color.hcl.l}
setChroma={actions.hcl.setC}
parentDimensions={dimensions}
/>
<BarCrosshair
chroma={color.hcl.c}
luminance={color.hcl.l}
hex={color.hex}
/>
</div>
</div> </div>
); );
} }
@@ -1,6 +1,137 @@
.colorValuesWrapper { .colorValuesWrapper {
width: 100%; padding: 40px;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px;
}
.spaceWrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border: 2px solid #7a7a7a;
}
.componentWrapper {
display: flex;
align-items: stretch;
width: 100%;
height: 25px;
font-family: monospace;
border-top: 1px solid #7a7a7a;
border-bottom: 1px solid #7a7a7a;
}
.componentWrapper:first-of-type {
border-top: none;
}
.componentWrapper:last-of-type {
border-bottom: none;
}
.section {
display: flex;
align-items: center;
justify-content: center;
border-right: 2px solid #7a7a7a;
}
.section:last-of-type {
border-right: none;
}
.symbol {
font-family: monospace;
font-size: 14px;
aspect-ratio: 1 / 1;
}
.sliderSection {
position: relative;
flex-grow: 1;
}
.sliderWrapper {
width: 100%;
height: 100%;
}
.sliderBar {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #aaa;
pointer-events: none;
}
.buttonWrapper {
aspect-ratio: 1 / 1;
}
.button {
height: 100%;
width: 100%;
padding: 0;
cursor: pointer;
user-select: none;
background: none;
border: none;
}
.button:hover {
background-color: #ddd;
}
.button:active {
background-color: #bbb;
}
.valueWrapper {
aspect-ratio: 1.5 / 1;
min-width: 40px;
}
.value {
height: 100%;
width: 100%;
background: none;
border: none;
padding: 0 5px;
font-family: monospace;
font-size: 14px;
text-align: right;
}
.hexEditor {
display: flex;
align-items: stretch;
font-family: monospace;
border: 2px solid #7a7a7a;
height: 25px;
max-width: 150px;
}
.hexLabel {
font-family: monospace;
font-size: 14px;
padding: 0 10px;
}
.hexValueWrapper {
flex: 1;
}
.hexValueWrapper input {
height: 100%;
width: 100%;
background: none;
border: none;
padding: 0 5px;
font-family: monospace;
font-size: 14px;
letter-spacing: 0.1em;
text-align: center;
} }
+1 -1
View File
@@ -3,7 +3,7 @@ import type { KeyboardEvent } from "react";
import * as colorlib from "colorlib"; import * as colorlib from "colorlib";
import type { ColorActions } from "@hooks/color"; import type { ColorActions } from "@/hooks/color";
import styles from "./ColorValues.module.css"; import styles from "./ColorValues.module.css";
import SpaceEditor from "./SpaceEditor"; import SpaceEditor from "./SpaceEditor";
@@ -2,7 +2,7 @@ import { useReducer } from "react";
import { Color } from "colorlib"; import { Color } from "colorlib";
import { colorReducer, createColorActions } from "@hooks/color"; import { colorReducer, createColorActions } from "@/hooks/color";
import ColorValues from "./ColorValues"; import ColorValues from "./ColorValues";
@@ -2,7 +2,7 @@ import { useReducer } from "react";
import { Color } from "colorlib"; import { Color } from "colorlib";
import { colorReducer, createColorActions } from "@hooks/color"; import { colorReducer, createColorActions } from "@/hooks/color";
import { HexEditor } from "./ValueEditor"; import { HexEditor } from "./ValueEditor";
@@ -1,6 +0,0 @@
.spaceWrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
+2 -2
View File
@@ -4,9 +4,9 @@ import type {
HCLColorActions, HCLColorActions,
HSVColorActions, HSVColorActions,
RGBColorActions, RGBColorActions,
} from "@hooks/color"; } from "@/hooks/color";
import styles from "./SpaceEditor.module.css"; import styles from "./ColorValues.module.css";
import { ValueEditor } from "./ValueEditor"; import { ValueEditor } from "./ValueEditor";
type ColorSpaceProps = type ColorSpaceProps =
@@ -2,8 +2,8 @@ import { useReducer } from "react";
import { Color } from "colorlib"; import { Color } from "colorlib";
import { colorReducer, createColorActions } from "@/hooks/color";
import { roundTo } from "@/util"; import { roundTo } from "@/util";
import { colorReducer, createColorActions } from "@hooks/color";
import SpaceEditor from "./SpaceEditor"; import SpaceEditor from "./SpaceEditor";
@@ -1,118 +0,0 @@
.componentWrapper {
display: flex;
align-items: stretch;
width: 100%;
height: 25px;
font-family: monospace;
border: 1px solid black;
border-top: none;
}
.componentWrapper:first-of-type {
border-top: 1px solid black;
}
.section {
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid black;
}
.section:last-of-type {
border-right: none;
}
.symbol {
font-family: monospace;
font-size: 14px;
aspect-ratio: 1 / 1;
}
.sliderSection {
position: relative;
flex-grow: 1;
}
.sliderWrapper {
width: 100%;
height: 100%;
}
.sliderBar {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #aaa;
pointer-events: none;
}
.buttonWrapper {
aspect-ratio: 1 / 1;
}
.button {
height: 100%;
width: 100%;
padding: 0;
cursor: pointer;
user-select: none;
background: none;
border: none;
}
.button:hover {
background-color: #ddd;
}
.button:active {
background-color: #bbb;
}
.valueWrapper {
aspect-ratio: 1.5 / 1;
min-width: 40px;
}
.value {
height: 100%;
width: 100%;
background: none;
border: none;
padding: 0 5px;
font-family: monospace;
font-size: 14px;
text-align: right;
}
.hexEditor {
display: flex;
align-items: stretch;
font-family: monospace;
border: 1px solid black;
height: 25px;
max-width: 150px;
}
.hexLabel {
font-family: monospace;
font-size: 14px;
padding: 0 10px;
}
.hexValueWrapper {
flex: 1;
}
.hexValueWrapper input {
height: 100%;
width: 100%;
background: none;
border: none;
padding: 0 5px;
font-family: monospace;
font-size: 14px;
letter-spacing: 0.1em;
text-align: center;
}
+7 -7
View File
@@ -10,15 +10,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import clsx from "clsx"; import clsx from "clsx";
import * as colorlib from "colorlib"; import * as colorlib from "colorlib";
import type { HexColorActions } from "@/hooks/color";
import { useScroll } from "@/hooks/scroll";
import { useSlider } from "@/hooks/slider";
import { useResize } from "@/hooks/window";
import type { CartesianSpace, Range, Setter, Timeout } from "@/types"; import type { CartesianSpace, Range, Setter, Timeout } from "@/types";
import { Direction } from "@/types"; import { Direction } from "@/types";
import { minmax, setMeasurements, valueToPosition } from "@/util"; import { minmax, roundTo, setMeasurements, valueToPosition } from "@/util";
import type { HexColorActions } from "@hooks/color";
import { useScroll } from "@hooks/scroll";
import { useSlider } from "@hooks/slider";
import { useResize } from "@hooks/window";
import styles from "./ValueEditor.module.css"; import styles from "./ColorValues.module.css";
// ------------ // // ------------ //
// Value Editor // // Value Editor //
@@ -68,7 +68,7 @@ export function ValueEditor({
setValue((prev) => { setValue((prev) => {
const scaledStep = step / scale; const scaledStep = step / scale;
const newValue = minmax( const newValue = minmax(
Math.floor(prev * scale) / scale + scaledStep, roundTo(Math.floor(roundTo(prev * scale, 6)) / scale + scaledStep, 6),
valueRange.min, valueRange.min,
valueRange.max, valueRange.max,
); );
@@ -2,7 +2,7 @@ import { useReducer } from "react";
import { Color } from "colorlib"; import { Color } from "colorlib";
import { colorReducer, createColorActions } from "@hooks/color"; import { colorReducer, createColorActions } from "@/hooks/color";
import { ValueEditor } from "./ValueEditor"; import { ValueEditor } from "./ValueEditor";
+1 -1
View File
@@ -62,7 +62,7 @@ export function colorReducer(
} }
} }
type Setter = (valOrCallback: SetterValueOrCallback<number>) => void; export type Setter = (valOrCallback: SetterValueOrCallback<number>) => void;
export interface CommonColorActions { export interface CommonColorActions {
setColor: (color: colorlib.Color) => void; setColor: (color: colorlib.Color) => void;
+22 -3
View File
@@ -9,6 +9,8 @@ import {
positionToValue, positionToValue,
} from "@/util"; } from "@/util";
import { useSmoothAnimation } from "./animation";
if (typeof TouchEvent === "undefined") { if (typeof TouchEvent === "undefined") {
// @ts-ignore - intentionally creating global // @ts-ignore - intentionally creating global
window.TouchEvent = window.MouseEvent; window.TouchEvent = window.MouseEvent;
@@ -21,6 +23,8 @@ export function useCrosshair({
setYValue, setYValue,
xValueRange, xValueRange,
yValueRange, yValueRange,
invertX,
invertY,
}: { }: {
origin: CartesianSpace; origin: CartesianSpace;
dimensions: CartesianSpace; dimensions: CartesianSpace;
@@ -28,6 +32,8 @@ export function useCrosshair({
setYValue: Setter<number>; setYValue: Setter<number>;
xValueRange: Range; xValueRange: Range;
yValueRange: Range; yValueRange: Range;
invertX?: boolean;
invertY?: boolean;
}) { }) {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const crosshairRef = useRef<HTMLDivElement>(null); const crosshairRef = useRef<HTMLDivElement>(null);
@@ -42,6 +48,9 @@ export function useCrosshair({
const xValueRangeRef = useRef(xValueRange); const xValueRangeRef = useRef(xValueRange);
const yValueRangeRef = useRef(yValueRange); const yValueRangeRef = useRef(yValueRange);
// Hooks
const smoothAnimation = useSmoothAnimation();
useEffect(() => { useEffect(() => {
originRef.current = origin; originRef.current = origin;
dimensionsRef.current = dimensions; dimensionsRef.current = dimensions;
@@ -55,13 +64,23 @@ export function useCrosshair({
const calculatePositions = useCallback((event: MouseEvent | TouchEvent) => { const calculatePositions = useCallback((event: MouseEvent | TouchEvent) => {
const orig = originRef.current; const orig = originRef.current;
const dims = dimensionsRef.current; const dims = dimensionsRef.current;
const xRange = xValueRangeRef.current;
const yRange = yValueRangeRef.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);
const newXValue = positionToValue(xPos, dims.x - 1, xValueRangeRef.current); let newXValue = positionToValue(xPos, dims.x - 1, xRange);
const newYValue = positionToValue(yPos, dims.y - 1, yValueRangeRef.current); let newYValue = positionToValue(yPos, dims.y - 1, yRange);
if (invertX) {
newXValue = xRange.max - newXValue;
}
if (invertY) {
newYValue = yRange.max - newYValue;
}
setXValueRef.current(newXValue); setXValueRef.current(newXValue);
setYValueRef.current(newYValue); setYValueRef.current(newYValue);
@@ -70,7 +89,7 @@ export function useCrosshair({
const handleMove = useCallback( const handleMove = useCallback(
(event: MouseEvent | TouchEvent) => { (event: MouseEvent | TouchEvent) => {
event.preventDefault(); event.preventDefault();
calculatePositions(event); smoothAnimation(() => calculatePositions(event));
}, },
[calculatePositions], [calculatePositions],
); );
+18 -2
View File
@@ -12,6 +12,7 @@ import {
valueToPosition, valueToPosition,
} from "@/util"; } from "@/util";
import { useSmoothAnimation } from "./animation";
import { useScroll } from "./scroll"; import { useScroll } from "./scroll";
if (typeof TouchEvent === "undefined") { if (typeof TouchEvent === "undefined") {
@@ -34,6 +35,7 @@ export function useSlider({
valueRange, valueRange,
value, value,
setValue, setValue,
invert = false,
}: { }: {
direction: Direction; direction: Direction;
origin: CartesianSpace; origin: CartesianSpace;
@@ -41,6 +43,7 @@ export function useSlider({
valueRange: Range; valueRange: Range;
value: number; value: number;
setValue: Setter<number>; setValue: Setter<number>;
invert?: boolean;
}) { }) {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null); const sliderRef = useRef<HTMLDivElement>(null);
@@ -59,6 +62,9 @@ export function useSlider({
const [position, setPosition] = useState(0); const [position, setPosition] = useState(0);
const positionRef = useRef(position); const positionRef = useRef(position);
// Hooks
const smoothAnimation = useSmoothAnimation();
useEffect(() => { useEffect(() => {
directionRef.current = direction; directionRef.current = direction;
originRef.current = origin; originRef.current = origin;
@@ -68,6 +74,11 @@ export function useSlider({
dimensions.x, dimensions.x,
dimensions.y, dimensions.y,
); );
positionRef.current = valueToPosition(
value,
maxPosition.current,
valueRangeRef.current,
);
}, [direction, origin, dimensions]); }, [direction, origin, dimensions]);
useEffect(() => { useEffect(() => {
@@ -94,19 +105,23 @@ export function useSlider({
0, 0,
chooseValueByDirection(dir, dims.x, dims.y), chooseValueByDirection(dir, dims.x, dims.y),
); );
const newValue = positionToValue( let newValue = positionToValue(
newPosition, newPosition,
maxPosition.current, maxPosition.current,
valueRangeRef.current, valueRangeRef.current,
); );
if (invert) {
newValue = valueRangeRef.current.max - newValue;
}
setValueRef.current(newValue); setValueRef.current(newValue);
}, []); }, []);
const handleMove = useCallback( const handleMove = useCallback(
(event: MouseEvent | TouchEvent) => { (event: MouseEvent | TouchEvent) => {
event.preventDefault(); event.preventDefault();
calculatePosition(event); smoothAnimation(() => calculatePosition(event));
}, },
[calculatePosition], [calculatePosition],
); );
@@ -154,6 +169,7 @@ export function useSlider({
maxPosition.current, maxPosition.current,
valueRangeRef.current, valueRangeRef.current,
); );
setValueRef.current(newValue); setValueRef.current(newValue);
}, []); }, []);
+3 -3
View File
@@ -1,10 +1,10 @@
import { createContext, useContext, useReducer } from "react"; import { createContext, useReducer } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import * as colorlib from "colorlib"; import * as colorlib from "colorlib";
import { colorReducer, createColorActions } from "@hooks/color"; import { colorReducer, createColorActions } from "@/hooks/color";
import type { ColorActions } from "@hooks/color"; import type { ColorActions } from "@/hooks/color";
interface SelectedColorContextType { interface SelectedColorContextType {
selectedColor: colorlib.Color; selectedColor: colorlib.Color;
+6
View File
@@ -1,5 +1,7 @@
import type { RefObject } from "react"; import type { RefObject } from "react";
import { Hex } from "colorlib";
import type { CartesianSpace, Range } from "./types"; import type { CartesianSpace, Range } from "./types";
import { Direction } from "./types"; import { Direction } from "./types";
@@ -94,3 +96,7 @@ export function roundTo(value: number, decimals: number = 0) {
const factor = Math.pow(10, decimals); const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor; return Math.round(value * factor) / factor;
} }
export function formatCssRgb(hex: Hex) {
return `rgb(${hex.r},${hex.g},${hex.b})`;
}
-3
View File
@@ -26,9 +26,6 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@hooks/*": ["./src/hooks/*"],
"@providers/*": ["./src/providers/*"]
} }
}, },
"include": ["src", "cypress"] "include": ["src", "cypress"]
-3
View File
@@ -11,9 +11,6 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
"@components": path.resolve(__dirname, "./src/components"),
"@hooks": path.resolve(__dirname, "./src/hooks"),
"@providers": path.resolve(__dirname, "./src/providers"),
}, },
}, },
server: { server: {