From dc5b12d6b88de2b26f9ad4d5289af9c27535c870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Ramst=C3=B6ck?= Date: Sun, 29 Sep 2024 12:06:58 +0200 Subject: [PATCH 1/8] remove old color picker --- package-lock.json | 13 -- package.json | 1 - src/ScriptEditor/ui/ThemeEditorModal.tsx | 9 +- src/Themes/ui/ColorPicker/ColorPreview.tsx | 17 +++ src/Themes/ui/ColorPicker/HexInput.tsx | 103 ++++++++++++++ src/Themes/ui/ColorPicker/HueSlider.tsx | 74 ++++++++++ src/Themes/ui/ColorPicker/RGBDigit.tsx | 47 +++++++ src/Themes/ui/ColorPicker/SVCanvas.tsx | 110 +++++++++++++++ src/Themes/ui/ColorPicker/index.tsx | 152 +++++++++++++++++++++ src/Themes/ui/ThemeEditorModal.tsx | 10 +- 10 files changed, 506 insertions(+), 30 deletions(-) create mode 100644 src/Themes/ui/ColorPicker/ColorPreview.tsx create mode 100644 src/Themes/ui/ColorPicker/HexInput.tsx create mode 100644 src/Themes/ui/ColorPicker/HueSlider.tsx create mode 100644 src/Themes/ui/ColorPicker/RGBDigit.tsx create mode 100644 src/Themes/ui/ColorPicker/SVCanvas.tsx create mode 100644 src/Themes/ui/ColorPicker/index.tsx diff --git a/package-lock.json b/package-lock.json index 0cd152eb09..8ced9603e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,6 @@ "date-fns": "^2.30.0", "escodegen": "^2.1.0", "jszip": "^3.10.1", - "material-ui-color": "^1.2.0", "material-ui-popup-state": "^1.9.3", "monaco-vim": "^0.3.5", "notistack": "^2.0.8", @@ -13453,18 +13452,6 @@ "node": ">=10" } }, - "node_modules/material-ui-color": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/material-ui-color/-/material-ui-color-1.2.0.tgz", - "integrity": "sha512-bD2Rww+hakJxD2/19uxc280Vh292DnRStLke2LDFavVtGd5fzOz09zIrHO3ZHlMkJFsvwx6IwiB4/932ftv0sQ==", - "peerDependencies": { - "@material-ui/core": "^4.9.5", - "material-ui-popup-state": "^1.5.3", - "prop-types": "^15.7.2", - "react": "^16.0.0 || ^17.0.0", - "react-dom": "^16.0.0 || ^17.0.0" - } - }, "node_modules/material-ui-popup-state": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-1.9.3.tgz", diff --git a/package.json b/package.json index b88fc8e235..6fe5bbd51c 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "date-fns": "^2.30.0", "escodegen": "^2.1.0", "jszip": "^3.10.1", - "material-ui-color": "^1.2.0", "material-ui-popup-state": "^1.9.3", "monaco-vim": "^0.3.5", "notistack": "^2.0.8", diff --git a/src/ScriptEditor/ui/ThemeEditorModal.tsx b/src/ScriptEditor/ui/ThemeEditorModal.tsx index 11363b7495..a3ed93c15b 100644 --- a/src/ScriptEditor/ui/ThemeEditorModal.tsx +++ b/src/ScriptEditor/ui/ThemeEditorModal.tsx @@ -3,7 +3,6 @@ import _ from "lodash"; import { Grid, Box, Button, IconButton, Paper, TextField, Tooltip, Typography } from "@mui/material"; import { History, Reply } from "@mui/icons-material"; -import { Color, ColorPicker } from "material-ui-color"; import { Settings } from "../../Settings/Settings"; import { useRerender } from "../../ui/React/hooks"; @@ -37,13 +36,7 @@ function ColorEditor({ label, themePath, onColorChange, color, defaultColor }: C InputProps={{ readOnly: true, startAdornment: ( - onColorChange(themePath, newColor.hex)} - disableAlpha - /> + <> ), endAdornment: ( onColorChange(themePath, defaultColor)}> diff --git a/src/Themes/ui/ColorPicker/ColorPreview.tsx b/src/Themes/ui/ColorPicker/ColorPreview.tsx new file mode 100644 index 0000000000..59f1b90b3f --- /dev/null +++ b/src/Themes/ui/ColorPicker/ColorPreview.tsx @@ -0,0 +1,17 @@ +import { RGB } from "./"; +import React from "react"; + +type Props = { + rgb: RGB; +}; + +export function ColorPreview({ rgb: { r, g, b } }: Props) { + return
; +} \ No newline at end of file diff --git a/src/Themes/ui/ColorPicker/HexInput.tsx b/src/Themes/ui/ColorPicker/HexInput.tsx new file mode 100644 index 0000000000..7a746582fc --- /dev/null +++ b/src/Themes/ui/ColorPicker/HexInput.tsx @@ -0,0 +1,103 @@ +import { RGB, useSyncState } from "./"; +import React, { ChangeEvent, useMemo, useState } from "react"; + +type Props = { + rgb: RGB, + setRGB: (rgb: RGB) => void; +}; + + +export function formatRGBtoHEX(rgb: RGB) { + const digits = [ + rgb.r.toString(16).padStart(2, '0'), + rgb.g.toString(16).padStart(2, '0'), + rgb.b.toString(16).padStart(2, '0') + ]; + + if (digits.every(d => d[0] == d[1])) return `${digits[0][0]}${digits[1][0]}${digits[2][0]}`; + return digits.join(''); +} + + +export function HexInput({ rgb, setRGB }: Props) { + + const hex = formatRGBtoHEX(rgb); + + const [value, setValue] = useSyncState(hex); + const [error, setError] = useState(false); + + useMemo(() => { + if (value.length != 3 && value.length != 6) return error || setError(true); + + const rgb = value.length == 3 ? + { + r: Number.parseInt(`${value[0]}${value[0]}`, 16), + g: Number.parseInt(`${value[1]}${value[1]}`, 16), + b: Number.parseInt(`${value[2]}${value[2]}`, 16), + } + : { + r: Number.parseInt(value.slice(0, 2), 16), + g: Number.parseInt(value.slice(2, 4), 16), + b: Number.parseInt(value.slice(4, 6), 16), + }; + + if (Object.values(rgb).some(v => isNaN(v))) return error || setError(true); + error && setError(false); + + if (hex == formatRGBtoHEX(rgb)) return; + + setRGB(rgb); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + return value.length <= 6 && setValue(value)} + error={error} + >; +} + + + +type DisplayProps = { + error: boolean; + value: string; + onChange: (e: ChangeEvent) => void; +}; + +function HexInputDisplay({ error, value, onChange }: DisplayProps) { + return
+ HEX + + # + + {error ? 'X' : ''} + +
; +} \ No newline at end of file diff --git a/src/Themes/ui/ColorPicker/HueSlider.tsx b/src/Themes/ui/ColorPicker/HueSlider.tsx new file mode 100644 index 0000000000..6a3dc4db29 --- /dev/null +++ b/src/Themes/ui/ColorPicker/HueSlider.tsx @@ -0,0 +1,74 @@ +import React, { useEffect } from "react"; + +type Props = { + hue: number; + setHue: (hue: number) => void; +}; + +const SLIDER_CLASS = 'color_picker_hue_slider'; + +const GRADIENT_BACKGROUND = `\ +rgba(0, 0, 0, 0) +linear-gradient(to right, + rgb(255, 0, 0) 0%, + rgb(255, 255, 0) 17%, + rgb(0, 255, 0) 33%, + rgb(0, 255, 255) 50%, + rgb(0, 0, 255) 67%, + rgb(255, 0, 255) 83%, + rgb(255, 0, 0) 100% +) +repeat scroll 0% 0%`; + +const THUMB_SELECTORS = [`.${SLIDER_CLASS}::-moz-range-thumb`, `.${SLIDER_CLASS}::-webkit-slider-thumb`]; + +const THUMB_RULES = THUMB_SELECTORS.map((s) => /*css*/` +${s} { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: white; + width: 17px; + height: 17px; + border-radius: 100%; + cursor: pointer; +} +`).join(''); + +const THUMB_HOVER_RULES = THUMB_SELECTORS.map((s) => /*css*/ ` +${s}:hover { + background: lightgrey; +} +`).join(''); + +const CSS = /*css*/` +${THUMB_RULES} +${THUMB_HOVER_RULES} +`; + +export function HueSlider({ hue, setHue }: Props) { + + useEffect(() => { + const styleSheet = new CSSStyleSheet(); + styleSheet.replace(CSS); + document.adoptedStyleSheets.push(styleSheet); + return () => void (document.adoptedStyleSheets = document.adoptedStyleSheets.filter(s => s != styleSheet)); + }, []); + + return setHue(+e.currentTarget.value)} + >; +} \ No newline at end of file diff --git a/src/Themes/ui/ColorPicker/RGBDigit.tsx b/src/Themes/ui/ColorPicker/RGBDigit.tsx new file mode 100644 index 0000000000..af70637273 --- /dev/null +++ b/src/Themes/ui/ColorPicker/RGBDigit.tsx @@ -0,0 +1,47 @@ +import { useSyncState } from "./"; +import React, { useMemo } from "react"; + +type Props = { + digitLabel: 'R' | 'G' | 'B', + digit: [number, (d: number) => void]; +}; + +export function RGBDigit({ digitLabel, digit: [digit, setDigit] }: Props) { + + const [value, setValue] = useSyncState(digit); + + useMemo(() => { + if (digit == value) return; + if (value > 255) return setValue(255); + if (value < 0) return setValue(0); + + setDigit(value); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + return
+ {digitLabel} + setValue(Number.parseInt((value || '0').replaceAll(/[^0-9]/g, '')))} + />
; +} \ No newline at end of file diff --git a/src/Themes/ui/ColorPicker/SVCanvas.tsx b/src/Themes/ui/ColorPicker/SVCanvas.tsx new file mode 100644 index 0000000000..14cd244b86 --- /dev/null +++ b/src/Themes/ui/ColorPicker/SVCanvas.tsx @@ -0,0 +1,110 @@ +import { HSV, useSyncState } from "./"; +import React, { MouseEvent, useMemo } from "react"; + +type Props = { + sat: number; + val: number; + hue: number; + setSV: (sv: Omit) => void; +}; + +const MARKER_RADIUS = 4; + +function constrain(val: number, min: number, max: number) { + return val > min ? (val < max ? val : max) : min; +} + +export function SVCanvas({ hue, sat, val, setSV }: Props) { + + const [s, setS] = useSyncState(sat); + const [v, setV] = useSyncState(val); + + const setSVByEvent = (event: MouseEvent) => { + const {width, height, x:posX, y:posY} = event.currentTarget.getBoundingClientRect(); + + + const x = constrain(event.clientX - posX - MARKER_RADIUS, -MARKER_RADIUS, width - MARKER_RADIUS); + const y = constrain(event.clientY - posY - MARKER_RADIUS, -MARKER_RADIUS, height - MARKER_RADIUS); + + const s = (x + MARKER_RADIUS) / width * 100; + const v = 100 - (y + MARKER_RADIUS) / height * 100; + + setS(s); + setV(v); + }; + + useMemo(() => { + if (sat == s && val == v) return; + + setSV({ sat: s, val: v }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [s, v]); + + return (setSVByEvent(e))} + onMouseMove={(e) => e.buttons == 1 && (setSVByEvent(e))} + > + + ; +} + + +type DisplayProps = { + hue: number; + onMouseDown: (e: MouseEvent) => void; + onMouseMove: (e: MouseEvent) => void; + children: JSX.Element; +}; + +function SVCanvasDisplay({ hue, onMouseDown, onMouseMove, children }: DisplayProps) { + return
+
+
+ {children} +
+
+
; +} + +type MarkerProps = { + s: number, + v: number; +}; + +function Marker({ s, v }: MarkerProps) { + return
; +} \ No newline at end of file diff --git a/src/Themes/ui/ColorPicker/index.tsx b/src/Themes/ui/ColorPicker/index.tsx new file mode 100644 index 0000000000..6b0f76ff62 --- /dev/null +++ b/src/Themes/ui/ColorPicker/index.tsx @@ -0,0 +1,152 @@ +import { ColorPreview } from "./ColorPreview"; +import { HexInput } from "./HexInput"; +import { HueSlider } from "./HueSlider"; +import { RGBDigit } from "./RGBDigit"; +import { SVCanvas } from "./SVCanvas"; +import React, { Dispatch, useMemo, useReducer, useState } from "react"; + +export type HSV = { + hue: number; + sat: number; + val: number; +}; + +export type RGB = { + r: number; + g: number; + b: number; +}; + +type Props = { + initialColor: RGB | HSV; +} & React.DetailedHTMLProps, HTMLDivElement>; + +function normalize(color: HSV | RGB) { + const isRGB = (c: RGB | HSV): c is RGB => Object.hasOwn(color, 'r'); + const rgb: RGB = isRGB(color) ? color : HSVtoRGB(color); + const hsv: HSV = !isRGB(color) ? color : RGBtoHSV(color); + return { rgb, hsv }; +} + +export function ColorPicker({ initialColor, ...attr }: Props) { + const [color, setColor] = useState(initialColor ?? { r: 255, g: 255, b: 255 }); + + const { rgb, hsv } = normalize(color); + + return
{ e.stopPropagation(); }} + > + setColor({ hue: hsv.hue, ...sv })}> + setColor({ ...hsv, hue: h })}> + + + setColor(c)}> + setColor({ ...rgb, r })]}> + setColor({ ...rgb, g })]}> + setColor({ ...rgb, b })]}> + +
; +} + +function useDetachedState(val: T) { + //the useMemo are used like useState that dont trigger a rerender + return useMemo(() => { + const state = [val]; + return [state, (val: T) => { state[0] = val; }] as const; + }, []); +} + +export function useSyncState(val: T): [T, Dispatch] { + + //used to manually trigger a rerender on state change + const [, rerender] = useReducer(() => ({}), {}); + + //this hook internal state is irrelevant for rendering so we dont want to rerender on change + const [[prev], setPrev] = useDetachedState(val); + const [[state], setStateInternal] = useDetachedState(val); + + const setState = (value: T) => (setStateInternal(value), rerender()); + + if (!Object.is(prev, val)) { + setPrev(val); + setStateInternal(val); + return [val, setState]; + } + + return [state, setState]; +} + +function HSVtoRGB({ hue, sat, val }: HSV): RGB { + const h = hue; + const s = sat / 100; + const v = val / 100; + // https://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately + let f = (n: number, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); + return { + r: Math.round(f(5) * 255), + g: Math.round(f(3) * 255), + b: Math.round(f(1) * 255) + }; +} + +function RGBtoHSV(rgb: RGB): HSV { + // https://stackoverflow.com/questions/3018313/algorithm-to-convert-rgb-to-hsv-and-hsv-to-rgb-in-range-0-255-for-both + const hsv: HSV = { hue: 0, sat: 0, val: 0 }; + let min, max, delta; + + const r = rgb.r / 255; + const g = rgb.g / 255; + const b = rgb.b / 255; + + min = r < g ? r : g; + min = min < b ? min : b; + + max = r > g ? r : g; + max = max > b ? max : b; + + hsv.val = Math.floor(max * 100); + delta = max - min; + + + if (delta < 0.00001) { + hsv.sat = 0; + hsv.hue = 0; + return hsv; + } + if (max > 0.0) { + hsv.sat = Math.floor((delta / max) * 100); + } else { + hsv.sat = 0.0; + hsv.hue = 0; + return hsv; + } + if (r >= max) + hsv.hue = (g - b) / delta; + else + if (g >= max) + hsv.hue = 2.0 + (b - r) / delta; + else + hsv.hue = 4.0 + (r - g) / delta; + + hsv.hue *= 60.0; + + if (hsv.hue < 0.0) + hsv.hue += 360.0; + + hsv.hue = Math.floor(hsv.hue); + + return hsv; +} \ No newline at end of file diff --git a/src/Themes/ui/ThemeEditorModal.tsx b/src/Themes/ui/ThemeEditorModal.tsx index 22c1f487bf..e100aaef9c 100644 --- a/src/Themes/ui/ThemeEditorModal.tsx +++ b/src/Themes/ui/ThemeEditorModal.tsx @@ -10,7 +10,6 @@ import IconButton from "@mui/material/IconButton"; import ReplyIcon from "@mui/icons-material/Reply"; import PaletteSharpIcon from "@mui/icons-material/PaletteSharp"; import HistoryIcon from "@mui/icons-material/History"; -import { Color, ColorPicker } from "material-ui-color"; import { ThemeEvents } from "./Theme"; import { Settings } from "../../Settings/Settings"; import { defaultTheme } from "../Themes"; @@ -18,6 +17,7 @@ import { UserInterfaceTheme } from "@nsdefs"; import { Router } from "../../ui/GameRoot"; import { Page } from "../../ui/Router"; import { ThemeCollaborate } from "./ThemeCollaborate"; +import { ColorPicker } from "./ColorPicker"; interface IProps { open: boolean; @@ -46,13 +46,7 @@ function ColorEditor({ name, onColorChange, color, defaultColor }: IColorEditorP InputProps={{ startAdornment: ( <> - onColorChange(name, "#" + newColor.hex)} - disableAlpha - /> + ), endAdornment: ( From a65d127ec797b44651ffeb01cba1bad7638f8181 Mon Sep 17 00:00:00 2001 From: Shy Date: Tue, 15 Oct 2024 22:49:06 +0200 Subject: [PATCH 2/8] apply theme & add `apply` button --- src/ScriptEditor/ui/ThemeEditorModal.tsx | 4 +- src/Themes/ui/ColorPicker/ColorPreview.tsx | 20 ++- src/Themes/ui/ColorPicker/HexInput.tsx | 119 +++++++------- src/Themes/ui/ColorPicker/HueSlider.tsx | 55 ++++--- src/Themes/ui/ColorPicker/RGBDigit.tsx | 56 ++++--- src/Themes/ui/ColorPicker/SVCanvas.tsx | 102 ++++++------ src/Themes/ui/ColorPicker/index.tsx | 182 +++++++++++++++------ src/Themes/ui/ThemeEditorModal.tsx | 14 +- 8 files changed, 323 insertions(+), 229 deletions(-) diff --git a/src/ScriptEditor/ui/ThemeEditorModal.tsx b/src/ScriptEditor/ui/ThemeEditorModal.tsx index a3ed93c15b..b0595af2dc 100644 --- a/src/ScriptEditor/ui/ThemeEditorModal.tsx +++ b/src/ScriptEditor/ui/ThemeEditorModal.tsx @@ -35,9 +35,7 @@ function ColorEditor({ label, themePath, onColorChange, color, defaultColor }: C sx={{ display: "block", my: 1 }} InputProps={{ readOnly: true, - startAdornment: ( - <> - ), + startAdornment: <>, endAdornment: ( onColorChange(themePath, defaultColor)}> diff --git a/src/Themes/ui/ColorPicker/ColorPreview.tsx b/src/Themes/ui/ColorPicker/ColorPreview.tsx index 59f1b90b3f..039e60b6aa 100644 --- a/src/Themes/ui/ColorPicker/ColorPreview.tsx +++ b/src/Themes/ui/ColorPicker/ColorPreview.tsx @@ -6,12 +6,14 @@ type Props = { }; export function ColorPreview({ rgb: { r, g, b } }: Props) { - return
; -} \ No newline at end of file + return ( +
+ ); +} diff --git a/src/Themes/ui/ColorPicker/HexInput.tsx b/src/Themes/ui/ColorPicker/HexInput.tsx index 7a746582fc..d432aba6e1 100644 --- a/src/Themes/ui/ColorPicker/HexInput.tsx +++ b/src/Themes/ui/ColorPicker/HexInput.tsx @@ -2,64 +2,64 @@ import { RGB, useSyncState } from "./"; import React, { ChangeEvent, useMemo, useState } from "react"; type Props = { - rgb: RGB, + rgb: RGB; setRGB: (rgb: RGB) => void; }; - export function formatRGBtoHEX(rgb: RGB) { const digits = [ - rgb.r.toString(16).padStart(2, '0'), - rgb.g.toString(16).padStart(2, '0'), - rgb.b.toString(16).padStart(2, '0') + rgb.r.toString(16).padStart(2, "0"), + rgb.g.toString(16).padStart(2, "0"), + rgb.b.toString(16).padStart(2, "0"), ]; - if (digits.every(d => d[0] == d[1])) return `${digits[0][0]}${digits[1][0]}${digits[2][0]}`; - return digits.join(''); + if (digits.every((d) => d[0] == d[1])) return `${digits[0][0]}${digits[1][0]}${digits[2][0]}`; + return digits.join(""); } - export function HexInput({ rgb, setRGB }: Props) { - const hex = formatRGBtoHEX(rgb); const [value, setValue] = useSyncState(hex); const [error, setError] = useState(false); + //this is a memo instead of an effect to make it run in sync useMemo(() => { if (value.length != 3 && value.length != 6) return error || setError(true); - const rgb = value.length == 3 ? - { - r: Number.parseInt(`${value[0]}${value[0]}`, 16), - g: Number.parseInt(`${value[1]}${value[1]}`, 16), - b: Number.parseInt(`${value[2]}${value[2]}`, 16), - } - : { - r: Number.parseInt(value.slice(0, 2), 16), - g: Number.parseInt(value.slice(2, 4), 16), - b: Number.parseInt(value.slice(4, 6), 16), - }; - - if (Object.values(rgb).some(v => isNaN(v))) return error || setError(true); + const rgb = + value.length == 3 + ? { + r: Number.parseInt(`${value[0]}${value[0]}`, 16), + g: Number.parseInt(`${value[1]}${value[1]}`, 16), + b: Number.parseInt(`${value[2]}${value[2]}`, 16), + } + : { + r: Number.parseInt(value.slice(0, 2), 16), + g: Number.parseInt(value.slice(2, 4), 16), + b: Number.parseInt(value.slice(4, 6), 16), + }; + + if (Object.values(rgb).some((v) => isNaN(v))) return error || setError(true); error && setError(false); if (hex == formatRGBtoHEX(rgb)) return; setRGB(rgb); + //this updates the current color so we only want to run this if `value` changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); - return value.length <= 6 && setValue(value)} - error={error} - >; + return ( + value.length <= 6 && setValue(value)} + error={error} + > + ); } - - type DisplayProps = { error: boolean; value: string; @@ -67,37 +67,40 @@ type DisplayProps = { }; function HexInputDisplay({ error, value, onChange }: DisplayProps) { - return
- HEX - - # - HEX + - {error ? 'X' : ''} - -
; -} \ No newline at end of file + > + # + + {error ? "X" : ""} + + + ); +} diff --git a/src/Themes/ui/ColorPicker/HueSlider.tsx b/src/Themes/ui/ColorPicker/HueSlider.tsx index 6a3dc4db29..b782bbbac5 100644 --- a/src/Themes/ui/ColorPicker/HueSlider.tsx +++ b/src/Themes/ui/ColorPicker/HueSlider.tsx @@ -5,7 +5,7 @@ type Props = { setHue: (hue: number) => void; }; -const SLIDER_CLASS = 'color_picker_hue_slider'; +const SLIDER_CLASS = "color_picker_hue_slider"; const GRADIENT_BACKGROUND = `\ rgba(0, 0, 0, 0) @@ -22,7 +22,8 @@ repeat scroll 0% 0%`; const THUMB_SELECTORS = [`.${SLIDER_CLASS}::-moz-range-thumb`, `.${SLIDER_CLASS}::-webkit-slider-thumb`]; -const THUMB_RULES = THUMB_SELECTORS.map((s) => /*css*/` +const THUMB_RULES = THUMB_SELECTORS.map( + (s) => /*css*/ ` ${s} { -webkit-appearance: none; -moz-appearance: none; @@ -33,42 +34,46 @@ ${s} { border-radius: 100%; cursor: pointer; } -`).join(''); +`, +).join(""); -const THUMB_HOVER_RULES = THUMB_SELECTORS.map((s) => /*css*/ ` +const THUMB_HOVER_RULES = THUMB_SELECTORS.map( + (s) => /*css*/ ` ${s}:hover { background: lightgrey; } -`).join(''); +`, +).join(""); -const CSS = /*css*/` +const CSS = /*css*/ ` ${THUMB_RULES} ${THUMB_HOVER_RULES} `; export function HueSlider({ hue, setHue }: Props) { - useEffect(() => { const styleSheet = new CSSStyleSheet(); styleSheet.replace(CSS); document.adoptedStyleSheets.push(styleSheet); - return () => void (document.adoptedStyleSheets = document.adoptedStyleSheets.filter(s => s != styleSheet)); + return () => void (document.adoptedStyleSheets = document.adoptedStyleSheets.filter((s) => s != styleSheet)); }, []); - return setHue(+e.currentTarget.value)} - >; -} \ No newline at end of file + return ( + setHue(+e.currentTarget.value)} + > + ); +} diff --git a/src/Themes/ui/ColorPicker/RGBDigit.tsx b/src/Themes/ui/ColorPicker/RGBDigit.tsx index af70637273..63929ecd44 100644 --- a/src/Themes/ui/ColorPicker/RGBDigit.tsx +++ b/src/Themes/ui/ColorPicker/RGBDigit.tsx @@ -2,12 +2,11 @@ import { useSyncState } from "./"; import React, { useMemo } from "react"; type Props = { - digitLabel: 'R' | 'G' | 'B', + digitLabel: "R" | "G" | "B"; digit: [number, (d: number) => void]; }; export function RGBDigit({ digitLabel, digit: [digit, setDigit] }: Props) { - const [value, setValue] = useSyncState(digit); useMemo(() => { @@ -16,32 +15,35 @@ export function RGBDigit({ digitLabel, digit: [digit, setDigit] }: Props) { if (value < 0) return setValue(0); setDigit(value); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); - return
- {digitLabel} - setValue(Number.parseInt((value || '0').replaceAll(/[^0-9]/g, '')))} - />
; -} \ No newline at end of file + > + {digitLabel} + setValue(Number.parseInt((value || "0").replaceAll(/[^0-9]/g, "")))} + /> + + ); +} diff --git a/src/Themes/ui/ColorPicker/SVCanvas.tsx b/src/Themes/ui/ColorPicker/SVCanvas.tsx index 14cd244b86..0a4dfdc2b0 100644 --- a/src/Themes/ui/ColorPicker/SVCanvas.tsx +++ b/src/Themes/ui/ColorPicker/SVCanvas.tsx @@ -5,7 +5,7 @@ type Props = { sat: number; val: number; hue: number; - setSV: (sv: Omit) => void; + setSV: (sv: Omit) => void; }; const MARKER_RADIUS = 4; @@ -15,20 +15,18 @@ function constrain(val: number, min: number, max: number) { } export function SVCanvas({ hue, sat, val, setSV }: Props) { - const [s, setS] = useSyncState(sat); const [v, setV] = useSyncState(val); const setSVByEvent = (event: MouseEvent) => { - const {width, height, x:posX, y:posY} = event.currentTarget.getBoundingClientRect(); + const { width, height, x: posX, y: posY } = event.currentTarget.getBoundingClientRect(); - const x = constrain(event.clientX - posX - MARKER_RADIUS, -MARKER_RADIUS, width - MARKER_RADIUS); const y = constrain(event.clientY - posY - MARKER_RADIUS, -MARKER_RADIUS, height - MARKER_RADIUS); - const s = (x + MARKER_RADIUS) / width * 100; - const v = 100 - (y + MARKER_RADIUS) / height * 100; - + const s = ((x + MARKER_RADIUS) / width) * 100; + const v = 100 - ((y + MARKER_RADIUS) / height) * 100; + setS(s); setV(v); }; @@ -37,19 +35,20 @@ export function SVCanvas({ hue, sat, val, setSV }: Props) { if (sat == s && val == v) return; setSV({ sat: s, val: v }); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [s, v]); - return (setSVByEvent(e))} - onMouseMove={(e) => e.buttons == 1 && (setSVByEvent(e))} - > - - ; + return ( + setSVByEvent(e)} + onMouseMove={(e) => e.buttons == 1 && setSVByEvent(e)} + > + + + ); } - type DisplayProps = { hue: number; onMouseDown: (e: MouseEvent) => void; @@ -58,53 +57,58 @@ type DisplayProps = { }; function SVCanvasDisplay({ hue, onMouseDown, onMouseMove, children }: DisplayProps) { - return
+ return (
- {children} +
+ {children} +
-
; + ); } type MarkerProps = { - s: number, + s: number; v: number; }; function Marker({ s, v }: MarkerProps) { - return
; -} \ No newline at end of file + return ( +
+ ); +} diff --git a/src/Themes/ui/ColorPicker/index.tsx b/src/Themes/ui/ColorPicker/index.tsx index 6b0f76ff62..de4755c57e 100644 --- a/src/Themes/ui/ColorPicker/index.tsx +++ b/src/Themes/ui/ColorPicker/index.tsx @@ -1,9 +1,12 @@ import { ColorPreview } from "./ColorPreview"; -import { HexInput } from "./HexInput"; +import { HexInput, formatRGBtoHEX } from "./HexInput"; import { HueSlider } from "./HueSlider"; import { RGBDigit } from "./RGBDigit"; import { SVCanvas } from "./SVCanvas"; import React, { Dispatch, useMemo, useReducer, useState } from "react"; +import { useTheme } from "@mui/material/styles"; +import { Modal } from "../../../ui/React/Modal"; +import Button from "@mui/material/Button"; export type HSV = { hue: number; @@ -17,60 +20,147 @@ export type RGB = { b: number; }; -type Props = { - initialColor: RGB | HSV; -} & React.DetailedHTMLProps, HTMLDivElement>; - function normalize(color: HSV | RGB) { - const isRGB = (c: RGB | HSV): c is RGB => Object.hasOwn(color, 'r'); + const isRGB = (c: RGB | HSV): c is RGB => Object.hasOwn(color, "r"); const rgb: RGB = isRGB(color) ? color : HSVtoRGB(color); const hsv: HSV = !isRGB(color) ? color : RGBtoHSV(color); return { rgb, hsv }; } -export function ColorPicker({ initialColor, ...attr }: Props) { +type OpenColorPickerButtonProps = { + color: string; + title: string; + onColorChange: (color: string) => void; +}; + +function parseHEXtoRGB(hex: string): RGB { + if (hex.length == 4) { + return { + r: Number.parseInt(`${hex[1]}${hex[1]}`, 16), + g: Number.parseInt(`${hex[2]}${hex[2]}`, 16), + b: Number.parseInt(`${hex[3]}${hex[3]}`, 16), + }; + } + + if (hex.length == 7) { + return { + r: Number.parseInt(`${hex[1]}${hex[2]}`, 16), + g: Number.parseInt(`${hex[2]}${hex[3]}`, 16), + b: Number.parseInt(`${hex[3]}${hex[4]}`, 16), + }; + } + + throw new Error("Invalid hex string"); +} + +export function OpenColorPickerButton({ color, onColorChange }: OpenColorPickerButtonProps) { + const [open, setOpen] = useState(false); + + //detached state to prevent rerenders of modal on change. + const [selectedColor, setSelectedColor] = useDetachedState(""); + + return ( + + + + )} + + + ); +} + +type ColorPickerProps = { + initialColor?: RGB | HSV; + setColorString: (color: string) => void; +} & React.DetailedHTMLProps, HTMLDivElement>; + +export function ColorPicker({ initialColor, setColorString, ...attr }: ColorPickerProps) { const [color, setColor] = useState(initialColor ?? { r: 255, g: 255, b: 255 }); const { rgb, hsv } = normalize(color); - return
{ e.stopPropagation(); }} - > - setColor({ hue: hsv.hue, ...sv })}> - setColor({ ...hsv, hue: h })}> - { + setColorString(formatRGBtoHEX(normalize(color).rgb)); + }, [color, setColorString]); + + return ( +
- - setColor(c)}> - setColor({ ...rgb, r })]}> - setColor({ ...rgb, g })]}> - setColor({ ...rgb, b })]}> - -
; + setColor({ hue: hsv.hue, ...sv })}> + setColor({ ...hsv, hue: h })}> + + + setColor(c)}> + setColor({ ...rgb, r })]}> + setColor({ ...rgb, g })]}> + setColor({ ...rgb, b })]}> + +
+ ); } function useDetachedState(val: T) { - //the useMemo are used like useState that dont trigger a rerender + //this useMemo is used like useState that doesnt trigger a rerender return useMemo(() => { - const state = [val]; - return [state, (val: T) => { state[0] = val; }] as const; + const state = [val] as [T]; + return [ + state, + (val: T) => { + state[0] = val; + }, + ] as const; + // this memo should only be run once + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); } export function useSyncState(val: T): [T, Dispatch] { - //used to manually trigger a rerender on state change const [, rerender] = useReducer(() => ({}), {}); @@ -94,18 +184,18 @@ function HSVtoRGB({ hue, sat, val }: HSV): RGB { const s = sat / 100; const v = val / 100; // https://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately - let f = (n: number, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); + const f = (n: number, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); return { r: Math.round(f(5) * 255), g: Math.round(f(3) * 255), - b: Math.round(f(1) * 255) + b: Math.round(f(1) * 255), }; } function RGBtoHSV(rgb: RGB): HSV { // https://stackoverflow.com/questions/3018313/algorithm-to-convert-rgb-to-hsv-and-hsv-to-rgb-in-range-0-255-for-both const hsv: HSV = { hue: 0, sat: 0, val: 0 }; - let min, max, delta; + let min, max; const r = rgb.r / 255; const g = rgb.g / 255; @@ -118,8 +208,7 @@ function RGBtoHSV(rgb: RGB): HSV { max = max > b ? max : b; hsv.val = Math.floor(max * 100); - delta = max - min; - + const delta = max - min; if (delta < 0.00001) { hsv.sat = 0; @@ -133,20 +222,15 @@ function RGBtoHSV(rgb: RGB): HSV { hsv.hue = 0; return hsv; } - if (r >= max) - hsv.hue = (g - b) / delta; - else - if (g >= max) - hsv.hue = 2.0 + (b - r) / delta; - else - hsv.hue = 4.0 + (r - g) / delta; + if (r >= max) hsv.hue = (g - b) / delta; + else if (g >= max) hsv.hue = 2.0 + (b - r) / delta; + else hsv.hue = 4.0 + (r - g) / delta; hsv.hue *= 60.0; - if (hsv.hue < 0.0) - hsv.hue += 360.0; + if (hsv.hue < 0.0) hsv.hue += 360.0; hsv.hue = Math.floor(hsv.hue); return hsv; -} \ No newline at end of file +} diff --git a/src/Themes/ui/ThemeEditorModal.tsx b/src/Themes/ui/ThemeEditorModal.tsx index e100aaef9c..0c350b7662 100644 --- a/src/Themes/ui/ThemeEditorModal.tsx +++ b/src/Themes/ui/ThemeEditorModal.tsx @@ -17,7 +17,7 @@ import { UserInterfaceTheme } from "@nsdefs"; import { Router } from "../../ui/GameRoot"; import { Page } from "../../ui/Router"; import { ThemeCollaborate } from "./ThemeCollaborate"; -import { ColorPicker } from "./ColorPicker"; +import { OpenColorPickerButton } from "./ColorPicker"; interface IProps { open: boolean; @@ -45,16 +45,12 @@ function ColorEditor({ name, onColorChange, color, defaultColor }: IColorEditorP value={color} InputProps={{ startAdornment: ( - <> - - + onColorChange(name, `#${c}`)} /> ), endAdornment: ( - <> - onColorChange(name, defaultColor)}> - - - + onColorChange(name, defaultColor)}> + + ), }} /> From 4b7238b789df5720d1520d1bbb3320e48d76c278 Mon Sep 17 00:00:00 2001 From: Shy Date: Wed, 16 Oct 2024 14:14:51 +0200 Subject: [PATCH 3/8] add colorpicker to themeditor for scripteditor & add alpha support --- src/ScriptEditor/ui/ThemeEditorModal.tsx | 6 ++-- src/Themes/ui/ColorPicker/HexInput.tsx | 35 ++++++++++++++++-------- src/Themes/ui/ColorPicker/index.tsx | 10 +++++-- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/ScriptEditor/ui/ThemeEditorModal.tsx b/src/ScriptEditor/ui/ThemeEditorModal.tsx index b0595af2dc..4cfafa66a0 100644 --- a/src/ScriptEditor/ui/ThemeEditorModal.tsx +++ b/src/ScriptEditor/ui/ThemeEditorModal.tsx @@ -10,6 +10,7 @@ import { Modal } from "../../ui/React/Modal"; import { OptionSwitch } from "../../ui/React/OptionSwitch"; import { defaultMonacoTheme } from "./themes"; +import { OpenColorPickerButton } from "../../Themes/ui/ColorPicker"; type ColorEditorProps = { label: string; @@ -25,7 +26,6 @@ function ColorEditor({ label, themePath, onColorChange, color, defaultColor }: C console.error(`color ${themePath} was undefined, reverting to default`); color = defaultColor; } - return ( @@ -35,7 +35,9 @@ function ColorEditor({ label, themePath, onColorChange, color, defaultColor }: C sx={{ display: "block", my: 1 }} InputProps={{ readOnly: true, - startAdornment: <>, + startAdornment: ( + onColorChange(themePath, c)} /> + ), endAdornment: ( onColorChange(themePath, defaultColor)}> diff --git a/src/Themes/ui/ColorPicker/HexInput.tsx b/src/Themes/ui/ColorPicker/HexInput.tsx index d432aba6e1..32d3e1e2a8 100644 --- a/src/Themes/ui/ColorPicker/HexInput.tsx +++ b/src/Themes/ui/ColorPicker/HexInput.tsx @@ -13,7 +13,11 @@ export function formatRGBtoHEX(rgb: RGB) { rgb.b.toString(16).padStart(2, "0"), ]; - if (digits.every((d) => d[0] == d[1])) return `${digits[0][0]}${digits[1][0]}${digits[2][0]}`; + if (rgb.a) digits.push(rgb.a.toString(16).padStart(2, "0")); + + if (digits.every((d) => d[0] == d[1])) + return digits.reduce((prev, cur) => `${prev}${cur[0]}`, ""); + return digits.join(""); } @@ -25,20 +29,27 @@ export function HexInput({ rgb, setRGB }: Props) { //this is a memo instead of an effect to make it run in sync useMemo(() => { - if (value.length != 3 && value.length != 6) return error || setError(true); + if ( + value.length != 3 && + value.length != 6 && + value.length != 8) + return error || setError(true); - const rgb = + const rgb: RGB = value.length == 3 ? { - r: Number.parseInt(`${value[0]}${value[0]}`, 16), - g: Number.parseInt(`${value[1]}${value[1]}`, 16), - b: Number.parseInt(`${value[2]}${value[2]}`, 16), - } + r: Number.parseInt(`${value[0]}${value[0]}`, 16), + g: Number.parseInt(`${value[1]}${value[1]}`, 16), + b: Number.parseInt(`${value[2]}${value[2]}`, 16), + } : { - r: Number.parseInt(value.slice(0, 2), 16), - g: Number.parseInt(value.slice(2, 4), 16), - b: Number.parseInt(value.slice(4, 6), 16), - }; + r: Number.parseInt(value.slice(0, 2), 16), + g: Number.parseInt(value.slice(2, 4), 16), + b: Number.parseInt(value.slice(4, 6), 16), + }; + + if (value.length == 8) + rgb.a = Number.parseInt(value.slice(6, 8), 16); if (Object.values(rgb).some((v) => isNaN(v))) return error || setError(true); error && setError(false); @@ -54,7 +65,7 @@ export function HexInput({ rgb, setRGB }: Props) { return ( value.length <= 6 && setValue(value)} + onChange={({ currentTarget: { value } }) => value.length <= 8 && setValue(value)} error={error} > ); diff --git a/src/Themes/ui/ColorPicker/index.tsx b/src/Themes/ui/ColorPicker/index.tsx index de4755c57e..979c664def 100644 --- a/src/Themes/ui/ColorPicker/index.tsx +++ b/src/Themes/ui/ColorPicker/index.tsx @@ -18,6 +18,7 @@ export type RGB = { r: number; g: number; b: number; + a?: number; }; function normalize(color: HSV | RGB) { @@ -34,6 +35,8 @@ type OpenColorPickerButtonProps = { }; function parseHEXtoRGB(hex: string): RGB { + console.log(hex); + if (hex.length == 4) { return { r: Number.parseInt(`${hex[1]}${hex[1]}`, 16), @@ -42,11 +45,12 @@ function parseHEXtoRGB(hex: string): RGB { }; } - if (hex.length == 7) { + if (hex.length == 7 || hex.length == 9) { return { r: Number.parseInt(`${hex[1]}${hex[2]}`, 16), - g: Number.parseInt(`${hex[2]}${hex[3]}`, 16), - b: Number.parseInt(`${hex[3]}${hex[4]}`, 16), + g: Number.parseInt(`${hex[3]}${hex[4]}`, 16), + b: Number.parseInt(`${hex[5]}${hex[6]}`, 16), + a: Number.parseInt(`${hex[7]}${hex[8]}`, 16), }; } From 47e744a4d8d0d3b92169245624c780435432c1d1 Mon Sep 17 00:00:00 2001 From: Shy Date: Wed, 16 Oct 2024 14:36:55 +0200 Subject: [PATCH 4/8] run prettier --- src/ScriptEditor/ui/ThemeEditorModal.tsx | 6 ++++- src/Themes/ui/ColorPicker/HexInput.tsx | 28 ++++++++++-------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/ScriptEditor/ui/ThemeEditorModal.tsx b/src/ScriptEditor/ui/ThemeEditorModal.tsx index 4cfafa66a0..e20ee94326 100644 --- a/src/ScriptEditor/ui/ThemeEditorModal.tsx +++ b/src/ScriptEditor/ui/ThemeEditorModal.tsx @@ -36,7 +36,11 @@ function ColorEditor({ label, themePath, onColorChange, color, defaultColor }: C InputProps={{ readOnly: true, startAdornment: ( - onColorChange(themePath, c)} /> + onColorChange(themePath, c)} + /> ), endAdornment: ( onColorChange(themePath, defaultColor)}> diff --git a/src/Themes/ui/ColorPicker/HexInput.tsx b/src/Themes/ui/ColorPicker/HexInput.tsx index 32d3e1e2a8..cc98d5bd1a 100644 --- a/src/Themes/ui/ColorPicker/HexInput.tsx +++ b/src/Themes/ui/ColorPicker/HexInput.tsx @@ -15,8 +15,7 @@ export function formatRGBtoHEX(rgb: RGB) { if (rgb.a) digits.push(rgb.a.toString(16).padStart(2, "0")); - if (digits.every((d) => d[0] == d[1])) - return digits.reduce((prev, cur) => `${prev}${cur[0]}`, ""); + if (digits.every((d) => d[0] == d[1])) return digits.reduce((prev, cur) => `${prev}${cur[0]}`, ""); return digits.join(""); } @@ -29,27 +28,22 @@ export function HexInput({ rgb, setRGB }: Props) { //this is a memo instead of an effect to make it run in sync useMemo(() => { - if ( - value.length != 3 && - value.length != 6 && - value.length != 8) - return error || setError(true); + if (value.length != 3 && value.length != 6 && value.length != 8) return error || setError(true); const rgb: RGB = value.length == 3 ? { - r: Number.parseInt(`${value[0]}${value[0]}`, 16), - g: Number.parseInt(`${value[1]}${value[1]}`, 16), - b: Number.parseInt(`${value[2]}${value[2]}`, 16), - } + r: Number.parseInt(`${value[0]}${value[0]}`, 16), + g: Number.parseInt(`${value[1]}${value[1]}`, 16), + b: Number.parseInt(`${value[2]}${value[2]}`, 16), + } : { - r: Number.parseInt(value.slice(0, 2), 16), - g: Number.parseInt(value.slice(2, 4), 16), - b: Number.parseInt(value.slice(4, 6), 16), - }; + r: Number.parseInt(value.slice(0, 2), 16), + g: Number.parseInt(value.slice(2, 4), 16), + b: Number.parseInt(value.slice(4, 6), 16), + }; - if (value.length == 8) - rgb.a = Number.parseInt(value.slice(6, 8), 16); + if (value.length == 8) rgb.a = Number.parseInt(value.slice(6, 8), 16); if (Object.values(rgb).some((v) => isNaN(v))) return error || setError(true); error && setError(false); From 67e17df20abd715bbf855e823dd0729380dcb716 Mon Sep 17 00:00:00 2001 From: Shy Date: Wed, 16 Oct 2024 15:04:33 +0200 Subject: [PATCH 5/8] remove stray console.log --- src/Themes/ui/ColorPicker/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Themes/ui/ColorPicker/index.tsx b/src/Themes/ui/ColorPicker/index.tsx index 979c664def..29181deac1 100644 --- a/src/Themes/ui/ColorPicker/index.tsx +++ b/src/Themes/ui/ColorPicker/index.tsx @@ -35,7 +35,6 @@ type OpenColorPickerButtonProps = { }; function parseHEXtoRGB(hex: string): RGB { - console.log(hex); if (hex.length == 4) { return { From 7c50c1440a13806f206c52f0df2a95ff535ed0c8 Mon Sep 17 00:00:00 2001 From: Shy Date: Wed, 16 Oct 2024 15:51:19 +0200 Subject: [PATCH 6/8] properly handle hex string with alpha --- src/Themes/ui/ColorPicker/index.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Themes/ui/ColorPicker/index.tsx b/src/Themes/ui/ColorPicker/index.tsx index 29181deac1..5f2ece7270 100644 --- a/src/Themes/ui/ColorPicker/index.tsx +++ b/src/Themes/ui/ColorPicker/index.tsx @@ -35,7 +35,6 @@ type OpenColorPickerButtonProps = { }; function parseHEXtoRGB(hex: string): RGB { - if (hex.length == 4) { return { r: Number.parseInt(`${hex[1]}${hex[1]}`, 16), @@ -44,7 +43,15 @@ function parseHEXtoRGB(hex: string): RGB { }; } - if (hex.length == 7 || hex.length == 9) { + if (hex.length == 7) { + return { + r: Number.parseInt(`${hex[1]}${hex[2]}`, 16), + g: Number.parseInt(`${hex[3]}${hex[4]}`, 16), + b: Number.parseInt(`${hex[5]}${hex[6]}`, 16), + }; + } + + if (hex.length == 9) { return { r: Number.parseInt(`${hex[1]}${hex[2]}`, 16), g: Number.parseInt(`${hex[3]}${hex[4]}`, 16), From 18989172729a1422100edc692a069ca98c9fc43b Mon Sep 17 00:00:00 2001 From: Shy Date: Wed, 16 Oct 2024 17:36:21 +0200 Subject: [PATCH 7/8] various improvments & suggestions --- src/Themes/ui/ColorPicker/HexInput.tsx | 56 +++++++++++++++++-------- src/Themes/ui/ColorPicker/HueSlider.tsx | 5 ++- src/Themes/ui/ColorPicker/RGBDigit.tsx | 20 ++++++++- src/Themes/ui/ColorPicker/SVCanvas.tsx | 4 +- src/Themes/ui/ColorPicker/index.tsx | 4 +- 5 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/Themes/ui/ColorPicker/HexInput.tsx b/src/Themes/ui/ColorPicker/HexInput.tsx index cc98d5bd1a..cff1c174bd 100644 --- a/src/Themes/ui/ColorPicker/HexInput.tsx +++ b/src/Themes/ui/ColorPicker/HexInput.tsx @@ -1,5 +1,6 @@ +import { useTheme } from "@mui/material/styles"; import { RGB, useSyncState } from "./"; -import React, { ChangeEvent, useMemo, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; type Props = { rgb: RGB; @@ -26,27 +27,33 @@ export function HexInput({ rgb, setRGB }: Props) { const [value, setValue] = useSyncState(hex); const [error, setError] = useState(false); - //this is a memo instead of an effect to make it run in sync - useMemo(() => { - if (value.length != 3 && value.length != 6 && value.length != 8) return error || setError(true); + useEffect(() => { + if (value.length != 3 && value.length != 6 && value.length != 8) { + if (!error) setError(true); + return; + } const rgb: RGB = value.length == 3 ? { - r: Number.parseInt(`${value[0]}${value[0]}`, 16), - g: Number.parseInt(`${value[1]}${value[1]}`, 16), - b: Number.parseInt(`${value[2]}${value[2]}`, 16), - } + r: Number.parseInt(`${value[0]}${value[0]}`, 16), + g: Number.parseInt(`${value[1]}${value[1]}`, 16), + b: Number.parseInt(`${value[2]}${value[2]}`, 16), + } : { - r: Number.parseInt(value.slice(0, 2), 16), - g: Number.parseInt(value.slice(2, 4), 16), - b: Number.parseInt(value.slice(4, 6), 16), - }; + r: Number.parseInt(value.slice(0, 2), 16), + g: Number.parseInt(value.slice(2, 4), 16), + b: Number.parseInt(value.slice(4, 6), 16), + }; if (value.length == 8) rgb.a = Number.parseInt(value.slice(6, 8), 16); - if (Object.values(rgb).some((v) => isNaN(v))) return error || setError(true); - error && setError(false); + if (Object.values(rgb).some((v) => isNaN(v))) { + if (!error) setError(true); + return; + } + + if (error) setError(false); if (hex == formatRGBtoHEX(rgb)) return; @@ -72,13 +79,19 @@ type DisplayProps = { }; function HexInputDisplay({ error, value, onChange }: DisplayProps) { + const theme = useTheme(); + return (
HEX @@ -93,7 +106,7 @@ function HexInputDisplay({ error, value, onChange }: DisplayProps) { - {error ? "X" : ""} + + {error ? "X" : ""} +
); diff --git a/src/Themes/ui/ColorPicker/HueSlider.tsx b/src/Themes/ui/ColorPicker/HueSlider.tsx index b782bbbac5..41817faf89 100644 --- a/src/Themes/ui/ColorPicker/HueSlider.tsx +++ b/src/Themes/ui/ColorPicker/HueSlider.tsx @@ -49,11 +49,12 @@ const CSS = /*css*/ ` ${THUMB_RULES} ${THUMB_HOVER_RULES} `; +const styleSheet = new CSSStyleSheet(); +styleSheet.replace(CSS); export function HueSlider({ hue, setHue }: Props) { useEffect(() => { - const styleSheet = new CSSStyleSheet(); - styleSheet.replace(CSS); + if (document.adoptedStyleSheets.includes(styleSheet)) return; document.adoptedStyleSheets.push(styleSheet); return () => void (document.adoptedStyleSheets = document.adoptedStyleSheets.filter((s) => s != styleSheet)); }, []); diff --git a/src/Themes/ui/ColorPicker/RGBDigit.tsx b/src/Themes/ui/ColorPicker/RGBDigit.tsx index 63929ecd44..adc900de98 100644 --- a/src/Themes/ui/ColorPicker/RGBDigit.tsx +++ b/src/Themes/ui/ColorPicker/RGBDigit.tsx @@ -1,15 +1,30 @@ import { useSyncState } from "./"; -import React, { useMemo } from "react"; +import React, { useEffect } from "react"; type Props = { digitLabel: "R" | "G" | "B"; digit: [number, (d: number) => void]; }; +const styleSheet = new CSSStyleSheet(); +styleSheet.replace(` +.color-picker-rgb-digit::-webkit-outer-spin-button, +.color-picker-rgb-digit::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +`); + export function RGBDigit({ digitLabel, digit: [digit, setDigit] }: Props) { const [value, setValue] = useSyncState(digit); - useMemo(() => { + useEffect(() => { + if (document.adoptedStyleSheets.includes(styleSheet)) return; + document.adoptedStyleSheets.push(styleSheet); + return () => void (document.adoptedStyleSheets = document.adoptedStyleSheets.filter((s) => s != styleSheet)); + }, []); + + useEffect(() => { if (digit == value) return; if (value > 255) return setValue(255); if (value < 0) return setValue(0); @@ -29,6 +44,7 @@ export function RGBDigit({ digitLabel, digit: [digit, setDigit] }: Props) { > {digitLabel} { + useEffect(() => { if (sat == s && val == v) return; setSV({ sat: s, val: v }); diff --git a/src/Themes/ui/ColorPicker/index.tsx b/src/Themes/ui/ColorPicker/index.tsx index 5f2ece7270..3f79794291 100644 --- a/src/Themes/ui/ColorPicker/index.tsx +++ b/src/Themes/ui/ColorPicker/index.tsx @@ -3,7 +3,7 @@ import { HexInput, formatRGBtoHEX } from "./HexInput"; import { HueSlider } from "./HueSlider"; import { RGBDigit } from "./RGBDigit"; import { SVCanvas } from "./SVCanvas"; -import React, { Dispatch, useMemo, useReducer, useState } from "react"; +import React, { Dispatch, useEffect, useMemo, useReducer, useState } from "react"; import { useTheme } from "@mui/material/styles"; import { Modal } from "../../../ui/React/Modal"; import Button from "@mui/material/Button"; @@ -118,7 +118,7 @@ export function ColorPicker({ initialColor, setColorString, ...attr }: ColorPick const theme = useTheme(); - useMemo(() => { + useEffect(() => { setColorString(formatRGBtoHEX(normalize(color).rgb)); }, [color, setColorString]); From e027a43936785060d028bec8193e3ec9cfeeca08 Mon Sep 17 00:00:00 2001 From: Shy Date: Wed, 16 Oct 2024 18:17:25 +0200 Subject: [PATCH 8/8] run prettier --- src/Themes/ui/ColorPicker/HexInput.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Themes/ui/ColorPicker/HexInput.tsx b/src/Themes/ui/ColorPicker/HexInput.tsx index cff1c174bd..4a11b2798c 100644 --- a/src/Themes/ui/ColorPicker/HexInput.tsx +++ b/src/Themes/ui/ColorPicker/HexInput.tsx @@ -36,15 +36,15 @@ export function HexInput({ rgb, setRGB }: Props) { const rgb: RGB = value.length == 3 ? { - r: Number.parseInt(`${value[0]}${value[0]}`, 16), - g: Number.parseInt(`${value[1]}${value[1]}`, 16), - b: Number.parseInt(`${value[2]}${value[2]}`, 16), - } + r: Number.parseInt(`${value[0]}${value[0]}`, 16), + g: Number.parseInt(`${value[1]}${value[1]}`, 16), + b: Number.parseInt(`${value[2]}${value[2]}`, 16), + } : { - r: Number.parseInt(value.slice(0, 2), 16), - g: Number.parseInt(value.slice(2, 4), 16), - b: Number.parseInt(value.slice(4, 6), 16), - }; + r: Number.parseInt(value.slice(0, 2), 16), + g: Number.parseInt(value.slice(2, 4), 16), + b: Number.parseInt(value.slice(4, 6), 16), + }; if (value.length == 8) rgb.a = Number.parseInt(value.slice(6, 8), 16);