reanimated-color-picker
Version:
A Pure JavaScript Color Picker for React Native
232 lines (224 loc) • 7.18 kB
JavaScript
import React from 'react';
import { ImageBackground, StyleSheet } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import usePickerContext from '../../../AppContext';
import { CircularSliderCore } from '../CircularSliderCore';
import { styles } from '../../../styles';
import Thumb from '../../Thumb/Thumb';
import { ConditionalRendering, HSVA2HSLA_string } from '../../../utils';
/** @see [HueCircular](https://alabsi91.github.io/reanimated-color-picker/api/sliders/hue/hue-circular-slider/) */
export function HueCircular({ children, gestures = [], style = {}, containerStyle = {}, rotate = 0, ...props }) {
const { hueValue, saturationValue, brightnessValue, onGestureChange, onGestureEnd, ...ctx } = usePickerContext();
const thumbShape = props.thumbShape ?? ctx.thumbShape;
const thumbSize = props.thumbSize ?? ctx.thumbSize;
const thumbColor = props.thumbColor ?? ctx.thumbColor;
const renderThumb = props.renderThumb ?? ctx.renderThumb;
const thumbStyle = props.thumbStyle ?? ctx.thumbStyle ?? {};
const sliderThickness = props.sliderThickness ?? ctx.sliderThickness;
const thumbScaleAnimationValue = props.thumbScaleAnimationValue ?? ctx.thumbScaleAnimationValue;
const thumbScaleAnimationDuration = props.thumbScaleAnimationDuration ?? ctx.thumbScaleAnimationDuration;
const adaptSpectrum = props.adaptSpectrum ?? ctx.adaptSpectrum;
const thumbInnerStyle = props.thumbInnerStyle ?? ctx.thumbInnerStyle ?? {};
const isGestureActive = useSharedValue(false);
const width = useSharedValue(0);
const borderRadius = useSharedValue(0);
const borderRadiusStyle = useAnimatedStyle(
() => ({
borderRadius: borderRadius.value,
}),
[borderRadius],
);
const handleScale = useSharedValue(1);
const thumbAnimatedStyle = useAnimatedStyle(() => {
const center = width.value / 2;
const rotatedHue = (hueValue.value - rotate) % 360;
const distance = (width.value - sliderThickness) / 2;
const angle = (rotatedHue * Math.PI) / 180;
const posY = width.value - (Math.sin(angle) * distance + center) - thumbSize / 2;
const posX = width.value - (Math.cos(angle) * distance + center) - thumbSize / 2;
return {
transform: [
{
translateX: posX,
},
{
translateY: posY,
},
{
scale: handleScale.value,
},
{
rotate: rotatedHue + 90 + 'deg',
},
],
};
}, [width, hueValue, handleScale, thumbSize, sliderThickness, rotate]);
const activeSaturationStyle = useAnimatedStyle(() => {
if (!adaptSpectrum) {
return {};
}
return {
backgroundColor: HSVA2HSLA_string(0, 0, brightnessValue.value, 1 - saturationValue.value / 100),
};
}, [brightnessValue, saturationValue, adaptSpectrum]);
const activeBrightnessStyle = useAnimatedStyle(() => {
if (!adaptSpectrum) {
return {};
}
return {
backgroundColor: HSVA2HSLA_string(0, 0, 0, 1 - brightnessValue.value / 100),
};
}, [brightnessValue, adaptSpectrum]);
const clipViewStyle = useAnimatedStyle(() => {
return {
position: 'absolute',
width: width.value - sliderThickness * 2,
height: width.value - sliderThickness * 2,
borderRadius: width.value / 2,
};
}, [width, sliderThickness]);
const onBegin = ({ x, y }) => {
'worklet';
const radius = width.value / 2;
const dx = radius - x;
const dy = radius - y;
const pressDistance = Math.sqrt(dx * dx + dy * dy);
// Check if the press is outside the circle
if (pressDistance > radius) {
isGestureActive.value = false;
return;
}
// check if the press inside the circle (not on the actual slider)
const innerR = width.value / 2 - sliderThickness;
if (pressDistance < innerR) {
isGestureActive.value = false;
return;
}
isGestureActive.value = true;
handleScale.value = withTiming(thumbScaleAnimationValue, {
duration: thumbScaleAnimationDuration,
});
};
const onUpdate = newValue => {
'worklet';
if (hueValue.value === newValue) {
return;
}
hueValue.value = newValue;
onGestureChange();
};
const onGestureUpdate = ({ x, y }) => {
'worklet';
if (!isGestureActive.value) return;
const center = width.value / 2;
const dx = center - x;
const dy = center - y;
const theta = Math.atan2(dy, dx) * (180 / Math.PI); // [0 - 180] range
const angle = theta < 0 ? 360 + theta : theta; // [0 - 360] range
const newHueValue = (angle + rotate) % 360;
onUpdate(newHueValue);
};
const onEnd = () => {
'worklet';
isGestureActive.value = false;
handleScale.value = withTiming(1, {
duration: thumbScaleAnimationDuration,
});
onGestureEnd();
};
const getAdaptiveColor = hsva => {
'worklet';
if (adaptSpectrum) {
return hsva;
}
return {
h: hsva.h,
s: 100,
v: 100,
};
};
return /*#__PURE__*/ React.createElement(
CircularSliderCore,
{
style: [
styles.panelContainer,
{
justifyContent: 'center',
alignItems: 'center',
},
style,
{
position: 'relative',
aspectRatio: 1,
borderWidth: 0,
padding: 0,
},
borderRadiusStyle,
],
label: props.accessibilityLabel ?? 'Hue circular slider',
hint: props.accessibilityHint,
currentValue: hueValue,
maxValue: 360,
width: width,
borderRadius: borderRadius,
gestures: gestures,
onGestureUpdate: onGestureUpdate,
onBegin: onBegin,
onUpdate: onUpdate,
onEnd: onEnd,
},
/*#__PURE__*/ React.createElement(
ImageBackground,
{
source: require('../../../assets/circularHue.png'),
style: [
styles.panelImage,
{
transform: [
{
rotate: -rotate + 'deg',
},
],
},
],
resizeMode: 'stretch',
'aria-hidden': true,
},
/*#__PURE__*/ React.createElement(
ConditionalRendering,
{
if: adaptSpectrum,
},
/*#__PURE__*/ React.createElement(Animated.View, {
style: [borderRadiusStyle, activeBrightnessStyle, StyleSheet.absoluteFill],
}),
/*#__PURE__*/ React.createElement(Animated.View, {
style: [borderRadiusStyle, activeSaturationStyle, StyleSheet.absoluteFill],
}),
),
),
/*#__PURE__*/ React.createElement(
Animated.View,
{
style: [
clipViewStyle,
{
backgroundColor: '#fff',
},
containerStyle,
],
},
children,
),
/*#__PURE__*/ React.createElement(Thumb, {
thumbShape: thumbShape,
thumbSize: thumbSize,
thumbColor: thumbColor,
renderThumb: renderThumb,
innerStyle: thumbInnerStyle,
thumbAnimatedStyle: thumbAnimatedStyle,
style: thumbStyle,
getAdaptiveColor: getAdaptiveColor,
}),
);
}