diff --git a/src/ColorPicker.tsx b/src/ColorPicker.tsx index 8d3de8ca..750a2674 100644 --- a/src/ColorPicker.tsx +++ b/src/ColorPicker.tsx @@ -1,7 +1,7 @@ import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; import { Text } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import { useSharedValue, withTiming } from 'react-native-reanimated'; +import { runOnJS, useSharedValue, withTiming } from 'react-native-reanimated'; import colorKit from '@colorKit'; import { PickerContextProvider } from '@context'; @@ -50,36 +50,79 @@ const ColorPicker = forwardRef( const brightnessValue = useSharedValue(initialColor.v); const alphaValue = useSharedValue(initialColor.a); - const returnedResults = (color?: SupportedColorFormats) => { - color = color ?? { + const returnedResults = (inputColor?: SupportedColorFormats) => { + 'worklet'; + + const color = inputColor ?? { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value, a: alphaValue.value, }; + return { - hex: colorKit.HEX(color), - rgb: colorKit.RGB(color).string(false), - rgba: colorKit.RGB(color).string(true), - hsl: colorKit.HSL(color).string(false), - hsla: colorKit.HSL(color).string(true), - hsv: colorKit.HSV(color).string(false), - hsva: colorKit.HSV(color).string(true), - hwb: colorKit.HWB(color).string(false), - hwba: colorKit.HWB(color).string(true), + get hex() { + return colorKit.runOnUI().HEX(color); + }, + get rgb() { + return colorKit.runOnUI().RGB(color).string(false); + }, + get rgba() { + return colorKit.runOnUI().RGB(color).string(true); + }, + get hsl() { + return colorKit.runOnUI().HSL(color).string(false); + }, + get hsla() { + return colorKit.runOnUI().HSL(color).string(true); + }, + get hsv() { + return colorKit.runOnUI().HSV(color).string(false); + }, + get hsva() { + return colorKit.runOnUI().HSV(color).string(true); + }, + get hwb() { + return colorKit.runOnUI().HWB(color).string(false); + }, + get hwba() { + return colorKit.runOnUI().HWB(color).string(true); + }, }; }; const onGestureEnd = (color?: SupportedColorFormats) => { - onComplete?.(returnedResults(color)); + 'worklet'; + + if (!onComplete) return; + const colorObject = returnedResults(color); + + try { + // run on the UI thread + onComplete(colorObject); + } catch (error) { + // run on the JS thread + runOnJS(onComplete)(colorObject); + } }; const onGestureChange = (color?: SupportedColorFormats) => { - onChange?.(returnedResults(color)); + 'worklet'; + + if (!onChange) return; + const colorObject = returnedResults(color); + + try { + // run on the UI thread + onChange(colorObject); + } catch (error) { + // run on the JS thread + runOnJS(onChange)(colorObject); + } }; - const setColor = (color: string, duration = thumbAnimationDuration) => { - const { h, s, v, a } = colorKit.HSV(color).object(); + const setColor = (color: SupportedColorFormats, duration = thumbAnimationDuration) => { + const { h, s, v, a } = colorKit.HSV(color).object(false); hueValue.value = withTiming(h, { duration }); saturationValue.value = withTiming(s, { duration }); diff --git a/src/components/Panels/Panel1.tsx b/src/components/Panels/Panel1.tsx index 70f234e1..0f30685e 100644 --- a/src/components/Panels/Panel1.tsx +++ b/src/components/Panels/Panel1.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { Image, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import { styles } from '@styles'; @@ -83,7 +83,7 @@ export function Panel1({ saturationValue.value = newSaturationValue; brightnessValue.value = newBrightnessValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -93,7 +93,7 @@ export function Panel1({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Panels/Panel2.tsx b/src/components/Panels/Panel2.tsx index 9b278c15..0ed19991 100644 --- a/src/components/Panels/Panel2.tsx +++ b/src/components/Panels/Panel2.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { ImageBackground, StyleSheet } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import { styles } from '@styles'; @@ -95,7 +95,7 @@ export function Panel2({ hueValue.value = newHueValue; channelValue.value = newChannelValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -105,7 +105,7 @@ export function Panel2({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Panels/Panel3.tsx b/src/components/Panels/Panel3.tsx index 5f154a7f..e30f6c82 100644 --- a/src/components/Panels/Panel3.tsx +++ b/src/components/Panels/Panel3.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { Image, ImageBackground, StyleSheet } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import { styles } from '@styles'; @@ -103,7 +103,7 @@ export function Panel3({ hueValue.value = newHueValue; channelValue.value = newChannelValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -113,7 +113,7 @@ export function Panel3({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Panels/Panel4.tsx b/src/components/Panels/Panel4.tsx index a52dbfc8..adf86740 100644 --- a/src/components/Panels/Panel4.tsx +++ b/src/components/Panels/Panel4.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { Image, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import { styles } from '@styles'; @@ -107,7 +107,7 @@ export function Panel4({ hueValue.value = newHueValue; saturationValue.value = newSaturationValue; brightnessValue.value = newBrightnessValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -117,7 +117,7 @@ export function Panel4({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Preview.tsx b/src/components/Preview.tsx index 01cb4f0c..1c32eb10 100644 --- a/src/components/Preview.tsx +++ b/src/components/Preview.tsx @@ -5,7 +5,7 @@ import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue } from 'rea import colorKit from '@colorKit'; import usePickerContext from '@context'; import { styles } from '@styles'; -import { ConditionalRendering, contrastRatio, getStyle, HSVA2HEX, isWeb } from '@utils'; +import { ConditionalRendering, getStyle, isWeb } from '@utils'; import { PreviewText } from './PreviewText'; import type { PreviewProps } from '@types'; @@ -41,12 +41,14 @@ export function Preview({ useDerivedValue(() => { const currentColor = { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value, a: alphaValue.value }; - previewColor.value = HSVA2HEX(hueValue.value, saturationValue.value, brightnessValue.value, alphaValue.value); + previewColor.value = colorKit + .runOnUI() + .HEX({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value, a: alphaValue.value }); // calculate the contrast ratio const compareColor1 = alphaValue.value > 0.5 ? currentColor : { h: 0, s: 0, v: 70 }; const compareColor2 = textColor.value === '#000000' ? { h: 0, s: 0, v: 0 } : { h: 0, s: 0, v: 100 }; - const contrast = contrastRatio(compareColor1, compareColor2); + const contrast = colorKit.runOnUI().contrastRatio(compareColor1, compareColor2); const reversedColor = textColor.value === '#ffffff' ? '#000000' : '#ffffff'; textColor.value = contrast < 4.5 ? reversedColor : textColor.value; }, [hueValue, saturationValue, brightnessValue, alphaValue]); diff --git a/src/components/Sliders/HSB/BrightnessSlider.tsx b/src/components/Sliders/HSB/BrightnessSlider.tsx index 923fb5a3..945c13ed 100644 --- a/src/components/Sliders/HSB/BrightnessSlider.tsx +++ b/src/components/Sliders/HSB/BrightnessSlider.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import Thumb from '@thumb'; @@ -86,7 +86,7 @@ export function BrightnessSlider({ if (brightnessValue.value === newBrightnessValue) return; brightnessValue.value = newBrightnessValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -96,7 +96,7 @@ export function BrightnessSlider({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Sliders/HSB/HueSlider.tsx b/src/components/Sliders/HSB/HueSlider.tsx index bc445fbf..17c13791 100644 --- a/src/components/Sliders/HSB/HueSlider.tsx +++ b/src/components/Sliders/HSB/HueSlider.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import Thumb from '@thumb'; @@ -93,7 +93,7 @@ export function HueSlider({ if (hueValue.value === newHueValue) return; hueValue.value = newHueValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -103,7 +103,7 @@ export function HueSlider({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Sliders/HSB/SaturationSlider.tsx b/src/components/Sliders/HSB/SaturationSlider.tsx index e317d2c2..4e2e750c 100644 --- a/src/components/Sliders/HSB/SaturationSlider.tsx +++ b/src/components/Sliders/HSB/SaturationSlider.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import Thumb from '@thumb'; @@ -92,7 +92,7 @@ export function SaturationSlider({ if (saturationValue.value === newSaturationValue) return; saturationValue.value = newSaturationValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -102,7 +102,7 @@ export function SaturationSlider({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Sliders/HueCircular.tsx b/src/components/Sliders/HueCircular.tsx index 448b81cd..fd479c3c 100644 --- a/src/components/Sliders/HueCircular.tsx +++ b/src/components/Sliders/HueCircular.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { ImageBackground, StyleSheet } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import { styles } from '@styles'; @@ -105,7 +105,7 @@ export function HueCircular({ if (hueValue.value === newHueValue) return; hueValue.value = newHueValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -122,7 +122,7 @@ export function HueCircular({ isGestureActive.value = false; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Sliders/OpacitySlider.tsx b/src/components/Sliders/OpacitySlider.tsx index 3cbda9b3..c1c3740b 100644 --- a/src/components/Sliders/OpacitySlider.tsx +++ b/src/components/Sliders/OpacitySlider.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Image, StyleSheet } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import usePickerContext from '@context'; import Thumb from '@thumb'; @@ -97,7 +97,7 @@ export function OpacitySlider({ if (alphaValue.value === newOpacityValue) return; alphaValue.value = newOpacityValue; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -107,7 +107,7 @@ export function OpacitySlider({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); diff --git a/src/components/Sliders/RGB/BlueSlider.tsx b/src/components/Sliders/RGB/BlueSlider.tsx index 62cf6d3f..935f5d50 100644 --- a/src/components/Sliders/RGB/BlueSlider.tsx +++ b/src/components/Sliders/RGB/BlueSlider.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import colorKit from '@colorKit'; import usePickerContext from '@context'; import Thumb from '@thumb'; -import { clamp, getStyle, HSVA2RGBA, isRtl, isWeb, RenderNativeOnly, RGBA2HSVA } from '@utils'; +import { clamp, getStyle, isRtl, isWeb, RenderNativeOnly } from '@utils'; import type { RgbSliderProps } from '@types'; import type { LayoutChangeEvent } from 'react-native'; @@ -58,7 +59,7 @@ export function BlueSlider({ const handleScale = useSharedValue(1); const handleStyle = useAnimatedStyle(() => { - const { b } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { b } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(false); const length = (vertical ? height.value : width.value) - (boundedThumb ? thumbSize : 0), percent = (b / 255) * length, @@ -73,7 +74,10 @@ export function BlueSlider({ const onGestureUpdate = ({ x, y }: PanGestureHandlerEventPayload) => { 'worklet'; - const { r, g, b } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { r, g, b } = colorKit + .runOnUI() + .RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }) + .object(false); const length = (vertical ? height.value : width.value) - (boundedThumb ? thumbSize : 0), pos = clamp((vertical ? y : x) - (boundedThumb ? thumbSize / 2 : 0), length), @@ -82,13 +86,13 @@ export function BlueSlider({ if (newBlueValue === b) return; - const { h, s, v } = RGBA2HSVA(r, g, newBlueValue); + const { h, s, v } = colorKit.runOnUI().HSV({ r, g, b: newBlueValue }).object(false); hueValue.value = h; saturationValue.value = s; brightnessValue.value = v; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -98,7 +102,7 @@ export function BlueSlider({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); @@ -112,7 +116,8 @@ export function BlueSlider({ }; const redGreen = useAnimatedStyle(() => { - const { r, g } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { r, g } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(); + if (isWeb) { const deg = vertical ? (reverse ? 180 : 0) : reverse ? 90 : 270; return { background: `linear-gradient(${deg}deg, rgb(${r}, ${g}, 255) 0%, rgb(${r}, ${g}, 0) 100%)` }; @@ -125,7 +130,7 @@ export function BlueSlider({ const imageRotate = vertical ? (reverse ? '90deg' : '270deg') : reverse ? '0deg' : '180deg'; const imageTranslateY = ((height.value - width.value) / 2) * ((reverse && isRtl) || (!reverse && !isRtl) ? -1 : 1); - const { r, g } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { r, g } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(); return { tintColor: `rgb(${r}, ${g}, 255)`, diff --git a/src/components/Sliders/RGB/GreenSlider.tsx b/src/components/Sliders/RGB/GreenSlider.tsx index bea8c8e7..f00f12ae 100644 --- a/src/components/Sliders/RGB/GreenSlider.tsx +++ b/src/components/Sliders/RGB/GreenSlider.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import colorKit from '@colorKit'; import usePickerContext from '@context'; import Thumb from '@thumb'; -import { clamp, getStyle, HSVA2RGBA, isRtl, isWeb, RenderNativeOnly, RGBA2HSVA } from '@utils'; +import { clamp, getStyle, isRtl, isWeb, RenderNativeOnly } from '@utils'; import type { RgbSliderProps } from '@types'; import type { LayoutChangeEvent } from 'react-native'; @@ -58,7 +59,7 @@ export function GreenSlider({ const handleScale = useSharedValue(1); const handleStyle = useAnimatedStyle(() => { - const { g } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { g } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(false); const length = (vertical ? height.value : width.value) - (boundedThumb ? thumbSize : 0), percent = (g / 255) * length, @@ -73,7 +74,10 @@ export function GreenSlider({ const onGestureUpdate = ({ x, y }: PanGestureHandlerEventPayload) => { 'worklet'; - const { r, g, b } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { r, g, b } = colorKit + .runOnUI() + .RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }) + .object(false); const length = (vertical ? height.value : width.value) - (boundedThumb ? thumbSize : 0), pos = clamp((vertical ? y : x) - (boundedThumb ? thumbSize / 2 : 0), length), @@ -82,13 +86,13 @@ export function GreenSlider({ if (newGreenValue === g) return; - const { h, s, v } = RGBA2HSVA(r, newGreenValue, b); + const { h, s, v } = colorKit.runOnUI().HSV({ r, g: newGreenValue, b }).object(false); hueValue.value = h; saturationValue.value = s; brightnessValue.value = v; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -98,7 +102,7 @@ export function GreenSlider({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); @@ -112,7 +116,7 @@ export function GreenSlider({ }; const redBlue = useAnimatedStyle(() => { - const { r, b } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { r, b } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(); if (isWeb) { const deg = vertical ? (reverse ? 180 : 0) : reverse ? 90 : 270; return { background: `linear-gradient(${deg}deg, rgb(${r}, 255, ${b}) 0%, rgb(${r}, 0, ${b}) 100%)` }; @@ -125,7 +129,7 @@ export function GreenSlider({ const imageRotate = vertical ? (reverse ? '90deg' : '270deg') : reverse ? '0deg' : '180deg'; const imageTranslateY = ((height.value - width.value) / 2) * ((reverse && isRtl) || (!reverse && !isRtl) ? -1 : 1); - const { r, b } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { r, b } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(); return { tintColor: `rgb(${r}, 255, ${b})`, diff --git a/src/components/Sliders/RGB/RedSlider.tsx b/src/components/Sliders/RGB/RedSlider.tsx index a7e87b37..49956601 100644 --- a/src/components/Sliders/RGB/RedSlider.tsx +++ b/src/components/Sliders/RGB/RedSlider.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import colorKit from '@colorKit'; import usePickerContext from '@context'; import Thumb from '@thumb'; -import { clamp, getStyle, HSVA2RGBA, isRtl, isWeb, RenderNativeOnly, RGBA2HSVA } from '@utils'; +import { clamp, getStyle, isRtl, isWeb, RenderNativeOnly } from '@utils'; import type { RgbSliderProps } from '@types'; import type { LayoutChangeEvent } from 'react-native'; @@ -58,7 +59,7 @@ export function RedSlider({ const handleScale = useSharedValue(1); const handleStyle = useAnimatedStyle(() => { - const { r } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { r } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(false); const length = (vertical ? height.value : width.value) - (boundedThumb ? thumbSize : 0), percent = (r / 255) * length, @@ -73,7 +74,10 @@ export function RedSlider({ const onGestureUpdate = ({ x, y }: PanGestureHandlerEventPayload) => { 'worklet'; - const { r, g, b } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { r, g, b } = colorKit + .runOnUI() + .RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }) + .object(false); const length = (vertical ? height.value : width.value) - (boundedThumb ? thumbSize : 0), pos = clamp((vertical ? y : x) - (boundedThumb ? thumbSize / 2 : 0), length), @@ -82,13 +86,13 @@ export function RedSlider({ if (newRedValue === r) return; - const { h, s, v } = RGBA2HSVA(newRedValue, g, b); + const { h, s, v } = colorKit.runOnUI().HSV({ r: newRedValue, g, b }).object(false); hueValue.value = h; saturationValue.value = s; brightnessValue.value = v; - runOnJS(onGestureChange)(); + onGestureChange(); }; const onGestureBegin = (event: PanGestureHandlerEventPayload) => { 'worklet'; @@ -98,7 +102,7 @@ export function RedSlider({ const onGestureFinish = () => { 'worklet'; handleScale.value = withTiming(1, { duration: 100 }); - runOnJS(onGestureEnd)(); + onGestureEnd(); }; const pan = Gesture.Pan().onBegin(onGestureBegin).onUpdate(onGestureUpdate).onEnd(onGestureFinish); @@ -112,7 +116,7 @@ export function RedSlider({ }; const greenBlue = useAnimatedStyle(() => { - const { g, b } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { g, b } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(); if (isWeb) { const deg = vertical ? (reverse ? 180 : 0) : reverse ? 90 : 270; return { background: `linear-gradient(${deg}deg, rgb(255, ${g}, ${b}) 0%, rgb(0, ${g}, ${b}) 100%)` }; @@ -125,7 +129,7 @@ export function RedSlider({ const imageRotate = vertical ? (reverse ? '90deg' : '270deg') : reverse ? '0deg' : '180deg'; const imageTranslateY = ((height.value - width.value) / 2) * ((reverse && isRtl) || (!reverse && !isRtl) ? -1 : 1); - const { g, b } = HSVA2RGBA(hueValue.value, saturationValue.value, brightnessValue.value); + const { g, b } = colorKit.runOnUI().RGB({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }).object(); return { tintColor: `rgb(255, ${g}, ${b})`, diff --git a/src/components/Thumb/Thumb.tsx b/src/components/Thumb/Thumb.tsx index 286f432b..4ad96099 100644 --- a/src/components/Thumb/Thumb.tsx +++ b/src/components/Thumb/Thumb.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { useAnimatedStyle, useDerivedValue, useSharedValue } from 'react-native-reanimated'; +import colorKit from '@colorKit'; import usePickerContext from '@context'; import { styles } from '@styles'; -import { contrastRatio, HSVA2HEX } from '@utils'; import BuiltinThumbs from './BuiltinThumbs/index'; import type { BuiltinThumbsProps, ThumbProps } from '@types'; @@ -51,12 +51,12 @@ export default function Thumb({ // When the values of channels change useDerivedValue(() => { alphaValue.value; // to track alpha changes too; - resultColor.value = HSVA2HEX(hueValue.value, saturationValue.value, brightnessValue.value); + resultColor.value = colorKit.runOnUI().HEX({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }); // calculate the contrast ratio const compareColor1 = getColorForAdaptiveColor(); const compareColor2 = adaptiveColor.value === '#000000' ? { h: 0, s: 0, v: 0 } : { h: 0, s: 0, v: 100 }; - const contrast = contrastRatio(compareColor1, compareColor2); + const contrast = colorKit.runOnUI().contrastRatio(compareColor1, compareColor2); const reversedColor = adaptiveColor.value === '#ffffff' ? '#000000' : '#ffffff'; adaptiveColor.value = contrast < 4.5 ? reversedColor : adaptiveColor.value; }, [alphaValue, hueValue, saturationValue, brightnessValue]); diff --git a/src/types.ts b/src/types.ts index 05fe414f..d5114931 100644 --- a/src/types.ts +++ b/src/types.ts @@ -103,7 +103,7 @@ export interface ColorPickerContext { adaptSpectrum: boolean; /** Apply a color to the color picker. */ - setColor: (color: string, duration?: number) => void; + setColor: (color: SupportedColorFormats, duration?: number) => void; /** A global prop for all sliders children. */ sliderThickness: number; @@ -405,7 +405,7 @@ export type InputProps = Omit< >; export type WidgetProps = { - onChange: (color: string) => void; + onChange: (color: SupportedColorFormats) => void; returnedResults: ColorPickerContext['returnedResults']; hueValue: ColorPickerContext['hueValue']; saturationValue: ColorPickerContext['saturationValue']; diff --git a/src/utils.tsx b/src/utils.tsx index a8d00e65..fdceed67 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -42,138 +42,6 @@ export function HSVA2HSLA_string(h: number, s: number, v: number, a = 1) { return `hsla(${h}, ${sln * 100}%, ${l * 100}%, ${a})`; } -/** - Convert `HSV` color to an `RGBA` object representation */ -export function HSVA2RGBA(h: number, s: number, v: number, a = 1) { - 'worklet'; - - h = h / 360; - s = s / 100; - v = v / 100; - - const i = Math.floor(h * 6), - f = h * 6 - i, - p = v * (1 - s), - q = v * (1 - f * s), - t = v * (1 - (1 - f) * s); - - let r = 0, - g = 0, - b = 0; - switch (i % 6) { - case 0: - r = v; - g = t; - b = p; - break; - case 1: - r = q; - g = v; - b = p; - break; - case 2: - r = p; - g = v; - b = t; - break; - case 3: - r = p; - g = q; - b = v; - break; - case 4: - r = t; - g = p; - b = v; - break; - case 5: - r = v; - g = p; - b = q; - break; - } - - return { - r: clamp(r * 255, 255), - g: clamp(g * 255, 255), - b: clamp(b * 255, 255), - a: clamp(a, 1), - }; -} - -/** - Convert `RGBA` color to an `HSVA` object representation */ -export function RGBA2HSVA(r: number, g: number, b: number, a = 1) { - 'worklet'; - - r = r / 255; - g = g / 255; - b = b / 255; - - const max = Math.max(r, g, b), - min = Math.min(r, g, b), - d = max - min, - v = max, - s = max === 0 ? 0 : d / max; - - let h = 0; - - if (max === min) { - h = 0; - } else { - if (max === r) { - h = (g - b) / d + (g < b ? 6 : 0); - } else if (max === g) { - h = (b - r) / d + 2; - } else if (max === b) { - h = (r - g) / d + 4; - } - h = h / 6; - } - - return { - h: clamp(h * 360, 360), - s: clamp(s * 100, 100), - v: clamp(v * 100, 100), - a: clamp(a, 1), - }; -} - -/** - Convert an `RGB` color to its corresponding `Hex` color */ -export function RGB_HEX(r: number, g: number, b: number, a = 1): string { - 'worklet'; - const red = Math.round(r).toString(16).padStart(2, '0'); - const green = Math.round(g).toString(16).padStart(2, '0'); - const blue = Math.round(b).toString(16).padStart(2, '0'); - const alpha = Math.round(clamp(a * 255, 255)) - .toString(16) - .padStart(2, '0'); - - return `#${red + green + blue + alpha}`; -} - -/** - Convert an `HSV` color to its corresponding `Hex` color */ -export function HSVA2HEX(h: number, s: number, v: number, a = 1) { - 'worklet'; - const { r, g, b, a: alpha } = HSVA2RGBA(h, s, v, a); - return RGB_HEX(r, g, b, alpha); -} - -/** - Returns the perceived `luminance` of a color, from `0-1` as defined by Web Content Accessibility Guidelines (Version 2.0). */ -export function getLuminanceWCAG(h: number, s: number, v: number): number { - 'worklet'; - const { r, g, b } = HSVA2RGBA(h, s, v); - const a = [r, g, b].map(val => (val / 255 <= 0.03928 ? val / 255 / 12.92 : Math.pow((val / 255 + 0.055) / 1.055, 2.4))); - return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; -} - -/** - Calculates the contrast ratio between two colors, useful for ensuring accessibility and readability. */ -export function contrastRatio(color1: { h: number; s: number; v: number }, color2: { h: number; s: number; v: number }): number { - 'worklet'; - const luminance1 = getLuminanceWCAG(color1.h, color1.s, color1.v); - const luminance2 = getLuminanceWCAG(color2.h, color2.s, color2.v); - const contrast = (Math.max(luminance1, luminance2) + 0.05) / (Math.min(luminance1, luminance2) + 0.05); - return Math.round(contrast * 100) / 100; -} - /** - Render children only if the `render` property is `true` */ export function ConditionalRendering(props: { children: React.ReactNode; if: boolean }) { if (!props.if) return null;