From d72565c80556afda20e8f9ca343e0b6e66eefff6 Mon Sep 17 00:00:00 2001 From: Ahmed ALAbsi Date: Wed, 1 Nov 2023 12:39:07 +0300 Subject: [PATCH] Optimize thumb and preview performance --- .../InputWidget/Widgets/HexWidget.tsx | 4 +- .../InputWidget/Widgets/HslWidget.tsx | 4 +- .../InputWidget/Widgets/HsvWidget.tsx | 4 +- .../InputWidget/Widgets/HwbWidget.tsx | 4 +- .../InputWidget/Widgets/RgbWidget.tsx | 4 +- src/components/Preview.tsx | 34 ++++++------- src/components/PreviewText.tsx | 4 +- src/components/Thumb/Thumb.tsx | 49 +++++++------------ src/utils.tsx | 37 ++++++++++++++ 9 files changed, 84 insertions(+), 60 deletions(-) diff --git a/src/components/InputWidget/Widgets/HexWidget.tsx b/src/components/InputWidget/Widgets/HexWidget.tsx index 6ea2211..2f79be1 100644 --- a/src/components/InputWidget/Widgets/HexWidget.tsx +++ b/src/components/InputWidget/Widgets/HexWidget.tsx @@ -26,9 +26,9 @@ export default function HexWidget({ }; useDerivedValue(() => { - [hueValue, saturationValue, brightnessValue, alphaValue]; + [hueValue, saturationValue, brightnessValue, alphaValue]; // track changes on Native runOnJS(updateText)(); - }, [hueValue, saturationValue, brightnessValue, alphaValue]); + }, [hueValue, saturationValue, brightnessValue, alphaValue]); // track changes on WEB const onTextChange = (text: string) => { text = text.startsWith('#') ? text : '#' + text; diff --git a/src/components/InputWidget/Widgets/HslWidget.tsx b/src/components/InputWidget/Widgets/HslWidget.tsx index f431b88..15c50ff 100644 --- a/src/components/InputWidget/Widgets/HslWidget.tsx +++ b/src/components/InputWidget/Widgets/HslWidget.tsx @@ -29,9 +29,9 @@ export default function HslWidget({ }; useDerivedValue(() => { - [hueValue, saturationValue, brightnessValue, alphaValue]; + [hueValue, saturationValue, brightnessValue, alphaValue]; // track changes on Native runOnJS(updateText)(); - }, [hueValue, saturationValue, brightnessValue, alphaValue]); + }, [hueValue, saturationValue, brightnessValue, alphaValue]); // track changes on WEB const onHueChange = (text: string) => { let hue = +text; diff --git a/src/components/InputWidget/Widgets/HsvWidget.tsx b/src/components/InputWidget/Widgets/HsvWidget.tsx index 3038672..adecb4c 100644 --- a/src/components/InputWidget/Widgets/HsvWidget.tsx +++ b/src/components/InputWidget/Widgets/HsvWidget.tsx @@ -29,9 +29,9 @@ export default function HsvWidget({ }; useDerivedValue(() => { - [hueValue, saturationValue, brightnessValue, alphaValue]; + [hueValue, saturationValue, brightnessValue, alphaValue]; // track changes on Native runOnJS(updateText)(); - }, [hueValue, saturationValue, brightnessValue, alphaValue]); + }, [hueValue, saturationValue, brightnessValue, alphaValue]); // track changes on WEB const onHueChange = (text: string) => { let hue = +text; diff --git a/src/components/InputWidget/Widgets/HwbWidget.tsx b/src/components/InputWidget/Widgets/HwbWidget.tsx index 076ba8f..3e36773 100644 --- a/src/components/InputWidget/Widgets/HwbWidget.tsx +++ b/src/components/InputWidget/Widgets/HwbWidget.tsx @@ -29,9 +29,9 @@ export default function HwbWidget({ }; useDerivedValue(() => { - [hueValue, saturationValue, brightnessValue, alphaValue]; + [hueValue, saturationValue, brightnessValue, alphaValue]; // track changes on Native runOnJS(updateText)(); - }, [hueValue, saturationValue, brightnessValue, alphaValue]); + }, [hueValue, saturationValue, brightnessValue, alphaValue]); // track changes on WEB const onHueChange = (text: string) => { let hue = +text; diff --git a/src/components/InputWidget/Widgets/RgbWidget.tsx b/src/components/InputWidget/Widgets/RgbWidget.tsx index 713613e..a394b2a 100644 --- a/src/components/InputWidget/Widgets/RgbWidget.tsx +++ b/src/components/InputWidget/Widgets/RgbWidget.tsx @@ -29,9 +29,9 @@ export default function RgbWidget({ }; useDerivedValue(() => { - [hueValue, saturationValue, brightnessValue, alphaValue]; + [hueValue, saturationValue, brightnessValue, alphaValue]; // track changes on Native runOnJS(updateText)(); - }, [hueValue, saturationValue, brightnessValue, alphaValue]); + }, [hueValue, saturationValue, brightnessValue, alphaValue]); // track changes on WEB const onRedChange = (text: string) => { let red = +text; diff --git a/src/components/Preview.tsx b/src/components/Preview.tsx index c26074b..87b8979 100644 --- a/src/components/Preview.tsx +++ b/src/components/Preview.tsx @@ -5,7 +5,7 @@ import Animated, { runOnJS, useAnimatedStyle, useDerivedValue, useSharedValue } import colorKit from '@colorKit'; import usePickerContext from '@context'; import { styles } from '@styles'; -import { ConditionalRendering, getStyle, isWeb } from '@utils'; +import { ConditionalRendering, contrastRatio, getStyle, HSVA2HEX, isWeb } from '@utils'; import type { PreviewProps } from '@types'; import type { ReactNode } from 'react'; @@ -20,9 +20,9 @@ const ReText = ({ text, style, hash }: { text: () => string; style: StyleProp { - [hash[0], hash[1], hash[2], hash[3]]; + [hash[0], hash[1], hash[2], hash[3]]; // track changes on Native runOnJS(updateText)(); - }, [hash[0], hash[1], hash[2], hash[3]]); + }, [hash[0], hash[1], hash[2], hash[3]]); // track changes on WEB return {color}; }; @@ -44,31 +44,29 @@ export function Preview({ const initialColorText = useMemo(() => { const adaptiveTextColor = alphaValue.value > 0.5 ? value : { h: 0, s: 0, v: 70 }; - const contrast = colorKit.contrastRatio(adaptiveTextColor, '#fff'); - const color = contrast < 4.5 ? '#000' : '#fff'; + const contrast = colorKit.contrastRatio(adaptiveTextColor, '#ffffff'); + const color = contrast < 4.5 ? '#000000' : '#ffffff'; return { formatted: returnedResults()[colorFormat], color }; }, [value, colorFormat]); - const textColor = useSharedValue('#fff'); + const textColor = useSharedValue<'#000000' | '#ffffff'>('#ffffff'); const textColorStyle = useAnimatedStyle(() => ({ color: textColor.value }), [textColor]); - const setTextColor = (color1: { h: number; s: number; v: number; a?: number }) => { - const color = textColor.value === '#ffffff' ? '#000000' : '#ffffff'; - const contrast = colorKit.contrastRatio(color1, textColor.value); - textColor.value = contrast < 4.5 ? color : textColor.value; - }; - const previewColor = useSharedValue('#fff'); + const previewColor = useSharedValue('#ffffff'); const previewColorStyle = useAnimatedStyle(() => ({ backgroundColor: previewColor.value }), [previewColor]); - const setPreviewColor = (color: { h: number; s: number; v: number; a: number }) => { - previewColor.value = colorKit.HEX(color); - }; // When the values of channels change useDerivedValue(() => { const currentColor = { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value, a: alphaValue.value }; - const adaptiveTextColor = alphaValue.value > 0.5 ? currentColor : { h: 0, s: 0, v: 70 }; - runOnJS(setPreviewColor)(currentColor); - runOnJS(setTextColor)(adaptiveTextColor); + + previewColor.value = HSVA2HEX(hueValue.value, saturationValue.value, brightnessValue.value, 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 reversedColor = textColor.value === '#ffffff' ? '#000000' : '#ffffff'; + textColor.value = contrast < 4.5 ? reversedColor : textColor.value; }, [hueValue, saturationValue, brightnessValue, alphaValue]); return ( diff --git a/src/components/PreviewText.tsx b/src/components/PreviewText.tsx index f68cfdd..8dec20a 100644 --- a/src/components/PreviewText.tsx +++ b/src/components/PreviewText.tsx @@ -17,9 +17,9 @@ export function PreviewText({ style = {}, colorFormat = 'hex' }: PreviewTextProp }; useDerivedValue(() => { - [colorFormat, hueValue, saturationValue, brightnessValue, alphaValue]; + [colorFormat, hueValue, saturationValue, brightnessValue, alphaValue]; // track changes on Native runOnJS(updateText)(); - }, [colorFormat, hueValue, saturationValue, brightnessValue, alphaValue]); + }, [colorFormat, hueValue, saturationValue, brightnessValue, alphaValue]); // track changes on WEB return {text}; } diff --git a/src/components/Thumb/Thumb.tsx b/src/components/Thumb/Thumb.tsx index 6bc94a0..286f432 100644 --- a/src/components/Thumb/Thumb.tsx +++ b/src/components/Thumb/Thumb.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { runOnJS, useAnimatedStyle, useDerivedValue, useSharedValue } from 'react-native-reanimated'; +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'; @@ -25,16 +25,7 @@ export default function Thumb({ const resultColor = useSharedValue('#ffffff'); const solidColor = useAnimatedStyle(() => ({ backgroundColor: resultColor.value }), [resultColor]); - const setResultColor = (color: { h: number; s: number; v: number; a?: number }) => { - resultColor.value = colorKit.HEX(color); - }; - - const adaptiveColor = useSharedValue('#ffffff'); - const setAdaptiveColor = (color1: { h: number; s: number; v: number; a?: number }) => { - const color = adaptiveColor.value === '#ffffff' ? '#000000' : '#ffffff'; - const contrast = colorKit.contrastRatio(color1, adaptiveColor.value); - adaptiveColor.value = contrast < 4.5 ? color : adaptiveColor.value; - }; + const adaptiveColor = useSharedValue<'#000000' | '#ffffff'>('#ffffff'); /** * Get the current color and calculate its contrast ratio against white or black, @@ -44,32 +35,30 @@ export default function Thumb({ 'worklet'; if (adaptSpectrum) { if (channel === 'a') { - return alphaValue.value > 0.5 - ? { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value } - : { h: 0, s: 0, v: 70 }; + if (alphaValue.value > 0.5) return { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }; + return { h: 0, s: 0, v: 70 }; } return { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }; } - switch (channel) { - case 'h': - return { h: hueValue.value, s: 100, v: 100 }; - case 'v': - return { h: hueValue.value, s: 100, v: brightnessValue.value }; - case 's': - return { h: hueValue.value, s: saturationValue.value, v: 70 }; - case 'a': - return { h: hueValue.value, s: alphaValue.value * 100, v: 70 }; - default: - return { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }; - } + if (channel === 'h') return { h: hueValue.value, s: 100, v: 100 }; + if (channel === 'v') return { h: hueValue.value, s: 100, v: brightnessValue.value }; + if (channel === 's') return { h: hueValue.value, s: saturationValue.value, v: 70 }; + if (channel === 'a') return { h: hueValue.value, s: alphaValue.value * 100, v: 70 }; + return { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }; }; // When the values of channels change useDerivedValue(() => { - alphaValue; - runOnJS(setAdaptiveColor)(getColorForAdaptiveColor()); - runOnJS(setResultColor)({ h: hueValue.value, s: saturationValue.value, v: brightnessValue.value }); + alphaValue.value; // to track alpha changes too; + resultColor.value = HSVA2HEX(hueValue.value, saturationValue.value, 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 reversedColor = adaptiveColor.value === '#ffffff' ? '#000000' : '#ffffff'; + adaptiveColor.value = contrast < 4.5 ? reversedColor : adaptiveColor.value; }, [alphaValue, hueValue, saturationValue, brightnessValue]); const thumbProps: BuiltinThumbsProps = { diff --git a/src/utils.tsx b/src/utils.tsx index b4d00a0..0d0b6c7 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -137,6 +137,43 @@ export function RGBA2HSVA(r: number, g: number, b: number, 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;