diff --git a/package.json b/package.json index 350815b..f9fd244 100644 --- a/package.json +++ b/package.json @@ -55,9 +55,9 @@ "^react$", "^react-dom(.*)$", "^react(.*)$", - "^@(?!(components|hooks|providers|/))(.*)$", + "^@(?!(/))(.*)$", "^(?!@|[.])(.*)$", - "^@(/|components|hooks|providers)(.*)$", + "^@(/)(.*)$", "^[./]" ], "importOrderSeparation": true, diff --git a/src/App.module.css b/src/App.module.css index d83e382..f24bebd 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -1,7 +1,42 @@ .appWrapper { + background-color: white; height: 100%; width: 100%; + max-width: 1200px; 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, @@ -17,146 +52,52 @@ scrollbar-width: none; } +.appWrapper { +} + +.mobileContent { + display: none; +} + +.mainContent { + display: grid; + grid-template-columns: 1fr 2fr; +} + +.firstZone { + display: flex; + flex-direction: column; + border-right: 2px solid #7a7a7a; +} + +.secondZone { + padding: 40px; + color: #555; + font-style: italic; +} + +.colorPickerWrapper { + border-bottom: 2px solid #7a7a7a; +} + +.colorValuesWrapper { +} + +.paletteEditorWrapper { +} + +.paletteLibraryWrapper { +} + /* Large */ @media (min-width: 992px), (min-width: 568px) and (max-width: 991px) and (orientation: portrait) { - .appWrapper { - max-width: 1200px; - margin: 0 auto; - } - - .mobileContent { - display: none; - } - - .mainContent { - display: flex; - } - - .firstZone { - flex: 1; - } - - .secondZone { - flex: 1; - } - - .colorPickerWrapper { - } - - .colorValuesWrapper { - } - - .paletteEditorWrapper { - } - - .paletteLibraryWrapper { - } } /* Landscape Phone */ @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 */ @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 { - } } diff --git a/src/App.tsx b/src/App.tsx index d350bb5..2562728 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,16 @@ -import { useReducer, useState } from "react"; +import { useState } from "react"; import clsx from "clsx"; +import { Color } from "colorlib"; -import ColorPicker from "@components/ColorPicker/ColorPicker"; -import ColorValues from "@components/ColorValues/ColorValues"; -import { LeftMenu, RightMenu } from "@components/SideMenu"; -import { useMediaQuery } from "@providers/hooks"; -import { useSelectedColor } from "@providers/hooks"; +import ColorPicker from "@/components/ColorPicker/ColorPicker"; +import ColorValues from "@/components/ColorValues/ColorValues"; +import { LeftMenu, RightMenu } from "@/components/SideMenu"; +import { useMediaQuery } from "@/providers/hooks"; +import { useSelectedColor } from "@/providers/hooks"; import styles from "./App.module.css"; +import { formatCssRgb } from "./util"; // Menu Button Components @@ -106,7 +108,7 @@ function MobileFirstZone() { aria-roledescription="slide" aria-label="Color Picker" > - +
; +} + function FirstZone() { const { selectedColor, selectedColorActions } = useSelectedColor(); return (
- +
@@ -210,6 +216,7 @@ function FirstZone() { function SecondZone() { return (
+ Palette Creator Coming Soon.
- - - + <> +
+ LUMINANCE + A color picker for humans. +
+
+ + +
+ ); } @@ -236,20 +249,47 @@ function DesktopContent() { function App() { const [isRightMenuOpen, setIsRightMenuOpen] = 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 ( -
- {!isDesktop && ( - - )} +
+
+ {!isDesktop && ( + + )} - {isDesktop && } + {isDesktop && } +
); } diff --git a/src/components/ColorPicker/ColorPicker.module.css b/src/components/ColorPicker/ColorPicker.module.css index 7bac3df..cafc5a4 100644 --- a/src/components/ColorPicker/ColorPicker.module.css +++ b/src/components/ColorPicker/ColorPicker.module.css @@ -1,36 +1,110 @@ .container { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; + padding: 40px; + display: grid; + grid-template-columns: 25px 1fr 25px; + grid-template-rows: 50px 1fr 25px; + grid-template-areas: + ". preview ." + "leftGrip square rightGrip" + ". bottomGrip ." + ". bar ."; } - .pickerSquare { - flex: 1; + grid-area: square; + position: relative; + aspect-ratio: 1/1; + border: 2px solid #7a7a7a; } .pickerBar { - flex: 1; + grid-area: bar; + position: relative; + height: 25px; + margin-top: 15px; + border: 2px solid #7a7a7a; } -/* Large - Landscape Tablets / Desktops */ -/* Medium - Portrait Tablets */ -/* Horizontal layout, vertically scrolling picker and palette content */ -@media (min-width: 992px), - (min-width: 568px) and (max-width: 991px) and (orientation: portrait) { +.verticalGripLeft { + grid-area: leftGrip; } -/* Medium - Landscape Phones */ -/* Horizontal layout, side menu, vertical tabbed picker */ -@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) { - .container { - flex-direction: row; - } +.verticalGripRight { + grid-area: rightGrip; } -/* Small - Portrait Phones*/ -/* Vertical layout, side menu, horizontal tabbed picker */ -@media (max-width: 567px) { +.horizontalGrip { + grid-area: bottomGrip; +} + +/* 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; } diff --git a/src/components/ColorPicker/ColorPicker.tsx b/src/components/ColorPicker/ColorPicker.tsx index e758c8e..c07975b 100644 --- a/src/components/ColorPicker/ColorPicker.tsx +++ b/src/components/ColorPicker/ColorPicker.tsx @@ -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(null); + const hueRange = { min: 0, max: 359 }; + const lumRange = { min: 0, max: 1 }; + + const [_origin, setOrigin] = useState({ x: 0, y: 0 }); + const [dimensions, setDimensions] = useState({ 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 ( -
-
Square
-
Bar
+
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
); } diff --git a/src/components/ColorValues/ColorValues.module.css b/src/components/ColorValues/ColorValues.module.css index 5fbec1b..45bdf14 100644 --- a/src/components/ColorValues/ColorValues.module.css +++ b/src/components/ColorValues/ColorValues.module.css @@ -1,6 +1,137 @@ .colorValuesWrapper { - width: 100%; - height: 100%; + padding: 40px; display: flex; 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; } diff --git a/src/components/ColorValues/ColorValues.tsx b/src/components/ColorValues/ColorValues.tsx index ae4a46f..888f5d2 100644 --- a/src/components/ColorValues/ColorValues.tsx +++ b/src/components/ColorValues/ColorValues.tsx @@ -3,7 +3,7 @@ import type { KeyboardEvent } from "react"; import * as colorlib from "colorlib"; -import type { ColorActions } from "@hooks/color"; +import type { ColorActions } from "@/hooks/color"; import styles from "./ColorValues.module.css"; import SpaceEditor from "./SpaceEditor"; diff --git a/src/components/ColorValues/ColorValuesTest.cy.tsx b/src/components/ColorValues/ColorValuesTest.cy.tsx index a16214b..ebdac5a 100644 --- a/src/components/ColorValues/ColorValuesTest.cy.tsx +++ b/src/components/ColorValues/ColorValuesTest.cy.tsx @@ -2,7 +2,7 @@ import { useReducer } from "react"; import { Color } from "colorlib"; -import { colorReducer, createColorActions } from "@hooks/color"; +import { colorReducer, createColorActions } from "@/hooks/color"; import ColorValues from "./ColorValues"; diff --git a/src/components/ColorValues/HexEditorTest.cy.tsx b/src/components/ColorValues/HexEditorTest.cy.tsx index 7db6f55..e0e88a1 100644 --- a/src/components/ColorValues/HexEditorTest.cy.tsx +++ b/src/components/ColorValues/HexEditorTest.cy.tsx @@ -2,7 +2,7 @@ import { useReducer } from "react"; import { Color } from "colorlib"; -import { colorReducer, createColorActions } from "@hooks/color"; +import { colorReducer, createColorActions } from "@/hooks/color"; import { HexEditor } from "./ValueEditor"; diff --git a/src/components/ColorValues/SpaceEditor.module.css b/src/components/ColorValues/SpaceEditor.module.css deleted file mode 100644 index 616096e..0000000 --- a/src/components/ColorValues/SpaceEditor.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.spaceWrapper { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; -} diff --git a/src/components/ColorValues/SpaceEditor.tsx b/src/components/ColorValues/SpaceEditor.tsx index c4c4034..ac29a34 100644 --- a/src/components/ColorValues/SpaceEditor.tsx +++ b/src/components/ColorValues/SpaceEditor.tsx @@ -4,9 +4,9 @@ import type { HCLColorActions, HSVColorActions, RGBColorActions, -} from "@hooks/color"; +} from "@/hooks/color"; -import styles from "./SpaceEditor.module.css"; +import styles from "./ColorValues.module.css"; import { ValueEditor } from "./ValueEditor"; type ColorSpaceProps = diff --git a/src/components/ColorValues/SpaceEditorTest.cy.tsx b/src/components/ColorValues/SpaceEditorTest.cy.tsx index 166f62f..6c9c0c4 100644 --- a/src/components/ColorValues/SpaceEditorTest.cy.tsx +++ b/src/components/ColorValues/SpaceEditorTest.cy.tsx @@ -2,8 +2,8 @@ import { useReducer } from "react"; import { Color } from "colorlib"; +import { colorReducer, createColorActions } from "@/hooks/color"; import { roundTo } from "@/util"; -import { colorReducer, createColorActions } from "@hooks/color"; import SpaceEditor from "./SpaceEditor"; diff --git a/src/components/ColorValues/ValueEditor.module.css b/src/components/ColorValues/ValueEditor.module.css deleted file mode 100644 index 78142b7..0000000 --- a/src/components/ColorValues/ValueEditor.module.css +++ /dev/null @@ -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; -} diff --git a/src/components/ColorValues/ValueEditor.tsx b/src/components/ColorValues/ValueEditor.tsx index cd88550..8c83bb9 100644 --- a/src/components/ColorValues/ValueEditor.tsx +++ b/src/components/ColorValues/ValueEditor.tsx @@ -10,15 +10,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; 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 { Direction } from "@/types"; -import { minmax, 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 { minmax, roundTo, setMeasurements, valueToPosition } from "@/util"; -import styles from "./ValueEditor.module.css"; +import styles from "./ColorValues.module.css"; // ------------ // // Value Editor // @@ -68,7 +68,7 @@ export function ValueEditor({ setValue((prev) => { const scaledStep = step / scale; const newValue = minmax( - Math.floor(prev * scale) / scale + scaledStep, + roundTo(Math.floor(roundTo(prev * scale, 6)) / scale + scaledStep, 6), valueRange.min, valueRange.max, ); diff --git a/src/components/ColorValues/ValueEditorTest.cy.tsx b/src/components/ColorValues/ValueEditorTest.cy.tsx index f5d886a..f4b7304 100644 --- a/src/components/ColorValues/ValueEditorTest.cy.tsx +++ b/src/components/ColorValues/ValueEditorTest.cy.tsx @@ -2,7 +2,7 @@ import { useReducer } from "react"; import { Color } from "colorlib"; -import { colorReducer, createColorActions } from "@hooks/color"; +import { colorReducer, createColorActions } from "@/hooks/color"; import { ValueEditor } from "./ValueEditor"; diff --git a/src/hooks/color.ts b/src/hooks/color.ts index fa19ba5..767c479 100644 --- a/src/hooks/color.ts +++ b/src/hooks/color.ts @@ -62,7 +62,7 @@ export function colorReducer( } } -type Setter = (valOrCallback: SetterValueOrCallback) => void; +export type Setter = (valOrCallback: SetterValueOrCallback) => void; export interface CommonColorActions { setColor: (color: colorlib.Color) => void; diff --git a/src/hooks/crosshair.tsx b/src/hooks/crosshair.tsx index 39fb975..4d602df 100644 --- a/src/hooks/crosshair.tsx +++ b/src/hooks/crosshair.tsx @@ -9,6 +9,8 @@ import { positionToValue, } from "@/util"; +import { useSmoothAnimation } from "./animation"; + if (typeof TouchEvent === "undefined") { // @ts-ignore - intentionally creating global window.TouchEvent = window.MouseEvent; @@ -21,6 +23,8 @@ export function useCrosshair({ setYValue, xValueRange, yValueRange, + invertX, + invertY, }: { origin: CartesianSpace; dimensions: CartesianSpace; @@ -28,6 +32,8 @@ export function useCrosshair({ setYValue: Setter; xValueRange: Range; yValueRange: Range; + invertX?: boolean; + invertY?: boolean; }) { const [isDragging, setIsDragging] = useState(false); const crosshairRef = useRef(null); @@ -42,6 +48,9 @@ export function useCrosshair({ const xValueRangeRef = useRef(xValueRange); const yValueRangeRef = useRef(yValueRange); + // Hooks + const smoothAnimation = useSmoothAnimation(); + useEffect(() => { originRef.current = origin; dimensionsRef.current = dimensions; @@ -55,13 +64,23 @@ export function useCrosshair({ const calculatePositions = useCallback((event: MouseEvent | TouchEvent) => { const orig = originRef.current; const dims = dimensionsRef.current; + const xRange = xValueRangeRef.current; + const yRange = yValueRangeRef.current; const { clientX, clientY } = extractEventCoordinates(event); const xPos = minmax(clientX - orig.x, 0, dims.x - 1); const yPos = minmax(clientY - orig.y, 0, dims.y - 1); - const newXValue = positionToValue(xPos, dims.x - 1, xValueRangeRef.current); - const newYValue = positionToValue(yPos, dims.y - 1, yValueRangeRef.current); + let newXValue = positionToValue(xPos, dims.x - 1, xRange); + let newYValue = positionToValue(yPos, dims.y - 1, yRange); + + if (invertX) { + newXValue = xRange.max - newXValue; + } + + if (invertY) { + newYValue = yRange.max - newYValue; + } setXValueRef.current(newXValue); setYValueRef.current(newYValue); @@ -70,7 +89,7 @@ export function useCrosshair({ const handleMove = useCallback( (event: MouseEvent | TouchEvent) => { event.preventDefault(); - calculatePositions(event); + smoothAnimation(() => calculatePositions(event)); }, [calculatePositions], ); diff --git a/src/hooks/slider.tsx b/src/hooks/slider.tsx index 65a594f..203b48e 100644 --- a/src/hooks/slider.tsx +++ b/src/hooks/slider.tsx @@ -12,6 +12,7 @@ import { valueToPosition, } from "@/util"; +import { useSmoothAnimation } from "./animation"; import { useScroll } from "./scroll"; if (typeof TouchEvent === "undefined") { @@ -34,6 +35,7 @@ export function useSlider({ valueRange, value, setValue, + invert = false, }: { direction: Direction; origin: CartesianSpace; @@ -41,6 +43,7 @@ export function useSlider({ valueRange: Range; value: number; setValue: Setter; + invert?: boolean; }) { const [isDragging, setIsDragging] = useState(false); const sliderRef = useRef(null); @@ -59,6 +62,9 @@ export function useSlider({ const [position, setPosition] = useState(0); const positionRef = useRef(position); + // Hooks + const smoothAnimation = useSmoothAnimation(); + useEffect(() => { directionRef.current = direction; originRef.current = origin; @@ -68,6 +74,11 @@ export function useSlider({ dimensions.x, dimensions.y, ); + positionRef.current = valueToPosition( + value, + maxPosition.current, + valueRangeRef.current, + ); }, [direction, origin, dimensions]); useEffect(() => { @@ -94,19 +105,23 @@ export function useSlider({ 0, chooseValueByDirection(dir, dims.x, dims.y), ); - const newValue = positionToValue( + let newValue = positionToValue( newPosition, maxPosition.current, valueRangeRef.current, ); + if (invert) { + newValue = valueRangeRef.current.max - newValue; + } + setValueRef.current(newValue); }, []); const handleMove = useCallback( (event: MouseEvent | TouchEvent) => { event.preventDefault(); - calculatePosition(event); + smoothAnimation(() => calculatePosition(event)); }, [calculatePosition], ); @@ -154,6 +169,7 @@ export function useSlider({ maxPosition.current, valueRangeRef.current, ); + setValueRef.current(newValue); }, []); diff --git a/src/providers/SelectedColorProvider.tsx b/src/providers/SelectedColorProvider.tsx index e0b4786..bffd8e7 100644 --- a/src/providers/SelectedColorProvider.tsx +++ b/src/providers/SelectedColorProvider.tsx @@ -1,10 +1,10 @@ -import { createContext, useContext, useReducer } from "react"; +import { createContext, useReducer } from "react"; import type { ReactNode } from "react"; import * as colorlib from "colorlib"; -import { colorReducer, createColorActions } from "@hooks/color"; -import type { ColorActions } from "@hooks/color"; +import { colorReducer, createColorActions } from "@/hooks/color"; +import type { ColorActions } from "@/hooks/color"; interface SelectedColorContextType { selectedColor: colorlib.Color; diff --git a/src/util.ts b/src/util.ts index 2645f39..43a2a8a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,7 @@ import type { RefObject } from "react"; +import { Hex } from "colorlib"; + import type { CartesianSpace, Range } from "./types"; import { Direction } from "./types"; @@ -94,3 +96,7 @@ export function roundTo(value: number, decimals: number = 0) { const factor = Math.pow(10, decimals); return Math.round(value * factor) / factor; } + +export function formatCssRgb(hex: Hex) { + return `rgb(${hex.r},${hex.g},${hex.b})`; +} diff --git a/tsconfig.app.json b/tsconfig.app.json index 3b06989..c5ce3c4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -26,9 +26,6 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], - "@components/*": ["./src/components/*"], - "@hooks/*": ["./src/hooks/*"], - "@providers/*": ["./src/providers/*"] } }, "include": ["src", "cypress"] diff --git a/vite.config.ts b/vite.config.ts index decd6d1..737e56d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,9 +11,6 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), - "@components": path.resolve(__dirname, "./src/components"), - "@hooks": path.resolve(__dirname, "./src/hooks"), - "@providers": path.resolve(__dirname, "./src/providers"), }, }, server: {