UNPKG

reanimated-color-picker

Version:
313 lines (305 loc) 10.4 kB
import React from 'react'; import { ImageBackground, StyleSheet } from 'react-native'; import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, withTiming } from 'react-native-reanimated'; import colorKit from '../../colorKit/index'; import usePickerContext from '../../AppContext'; import { PanelCore } from './PanelCore'; import { styles } from '../../styles'; import Thumb from '../Thumb/Thumb'; import { clamp, ConditionalRendering, getStyle, isRtl } from '../../utils'; /** @see [Panel2](https://alabsi91.github.io/reanimated-color-picker/api/panels/panel2/) */ export function Panel2({ verticalChannel = 'saturation', reverseHue = false, reverseVerticalChannel = false, gestures = [], style = {}, ...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 boundedThumb = props.boundedThumb ?? ctx.boundedThumb; const renderThumb = props.renderThumb ?? ctx.renderThumb; const thumbStyle = props.thumbStyle ?? ctx.thumbStyle ?? {}; const thumbInnerStyle = props.thumbInnerStyle ?? ctx.thumbInnerStyle ?? {}; const thumbScaleAnimationValue = props.thumbScaleUpValue ?? ctx.thumbScaleAnimationValue; const thumbScaleAnimationDuration = props.thumbScaleUpDuration ?? ctx.thumbScaleAnimationDuration; const adaptSpectrum = props.adaptSpectrum ?? ctx.adaptSpectrum; const borderRadius = getStyle(style, 'borderRadius') ?? 5; const heightStyle = getStyle(style, 'height') ?? 200; const width = useSharedValue(0); const height = useSharedValue(0); const handleScale = useSharedValue(1); // HSL saturation is mathematically undefined (collapses to 0) when luminance is 0 or 100, // because those represent pure black and white regardless of saturation. // This ref holds the last valid saturation so we can restore it when luminance moves away from the boundary. const hslSaturationValue = useSharedValue(0); const hsl = useDerivedValue(() => { const hsvColor = { h: hueValue.value, s: saturationValue.value, v: brightnessValue.value, }; const { h, s, l } = colorKit.runOnUI().HSL(hsvColor).object(false); // At l=0 (black) or l=100 (white), the conversion loses saturation information. // Substitute the last known saturation so it's restored when luminance changes. if (l === 100 || l === 0) { return { h, s: hslSaturationValue.value, l, }; } hslSaturationValue.value = s; return { h, s, l, }; }, [hueValue, saturationValue, brightnessValue]); const verticalChannelValue = useDerivedValue(() => { if (verticalChannel === 'brightness') { return brightnessValue.value; } if (verticalChannel === 'hsl-saturation') { return hsl.value.s; } return saturationValue.value; }, [brightnessValue, saturationValue, hsl]); const thumbAnimatedStyle = useAnimatedStyle(() => { const length = { x: width.value - (boundedThumb ? thumbSize : 0), y: height.value - (boundedThumb ? thumbSize : 0), }; const percentX = (hueValue.value / 360) * length.x; const posX = (reverseHue ? length.x - percentX : percentX) - (boundedThumb ? 0 : thumbSize / 2); const percentY = (verticalChannelValue.value / 100) * length.y; const posY = (reverseVerticalChannel ? percentY : length.y - percentY) - (boundedThumb ? 0 : thumbSize / 2); return { transform: [ { translateX: posX, }, { translateY: posY, }, { scale: handleScale.value, }, ], }; }, [[width, height, hueValue, verticalChannelValue, handleScale, reverseHue, reverseVerticalChannel, boundedThumb, thumbSize]]); const spectrumStyle = useAnimatedStyle(() => { if (!adaptSpectrum) return {}; if (verticalChannel === 'brightness') { return { backgroundColor: `rgba(255, 255, 255, ${1 - saturationValue.value / 100})`, }; } if (verticalChannel === 'hsl-saturation') { if (hsl.value.l < 50) { return { backgroundColor: `rgba(0, 0, 0, ${1 - hsl.value.l / 50})`, }; } return { backgroundColor: `rgba(255, 255, 255, ${(hsl.value.l - 50) / 50})`, }; } return { backgroundColor: `rgba(0, 0, 0, ${1 - brightnessValue.value / 100})`, }; }, [saturationValue, brightnessValue, hsl, adaptSpectrum, verticalChannel]); const panelImageStyle = useAnimatedStyle(() => { return { // Width and height are intentionally swapped to correct dimensions after the rotation width: height.value, height: width.value, transform: [ { rotate: reverseVerticalChannel ? '90deg' : '270deg', }, { translateX: ((width.value - height.value) / 2) * (reverseVerticalChannel ? -1 : 1), }, { translateY: ((width.value - height.value) / 2) * (isRtl ? -1 : 1) * (reverseVerticalChannel ? -1 : 1), }, ], }; }, [width, height, reverseVerticalChannel]); const onBegin = () => { 'worklet'; handleScale.value = withTiming(thumbScaleAnimationValue, { duration: thumbScaleAnimationDuration, }); }; const onUpdate = (newXValue, newYValue) => { 'worklet'; if (hueValue.value === newXValue && verticalChannelValue.value === newYValue) { return; } hueValue.value = newXValue; if (verticalChannel === 'hsl-saturation') { // Converting back from HSL→HSV at l=0 or l=100 would zero out HSV saturation and brightness, // locking the slider. Nudging l by ±0.01 keeps the conversion well-behaved without any // visible effect on the output color (values are rounded before use). const l = hsl.value.l === 0 ? 0.01 : hsl.value.l === 100 ? 99.99 : hsl.value.l; const { s, v } = colorKit .runOnUI() .HSV({ h: hsl.value.h, s: newYValue, l, }) .object(false); saturationValue.value = s; brightnessValue.value = v; } // Vertical channel is brightness else if (verticalChannel === 'brightness') { brightnessValue.value = newYValue; } // Vertical channel is saturation else { saturationValue.value = newYValue; } onGestureChange(); }; const onGestureUpdate = ({ x, y }) => { 'worklet'; const lengthX = width.value - (boundedThumb ? thumbSize : 0); const lengthY = height.value - (boundedThumb ? thumbSize : 0); const posX = clamp(x - (boundedThumb ? thumbSize / 2 : 0), lengthX); const posY = clamp(y - (boundedThumb ? thumbSize / 2 : 0), lengthY); const valueX = (posX / lengthX) * 360; const valueY = (posY / lengthY) * 100; const newXValue = reverseHue ? 360 - valueX : valueX; const newYValue = reverseVerticalChannel ? valueY : 100 - valueY; onUpdate(newXValue, newYValue); }; const onEnd = () => { 'worklet'; handleScale.value = withTiming(1, { duration: thumbScaleAnimationDuration, }); onGestureEnd(); }; const getAdaptiveColor = hsva => { 'worklet'; if (adaptSpectrum) { return hsva; } switch (verticalChannel) { case 'saturation': return { h: hsva.h, s: hsva.s, v: 100, }; case 'brightness': return { h: hsva.h, s: 100, v: hsva.v, }; case 'hsl-saturation': { const { h, s } = hsl.value; return { h, s, l: 50, }; } default: return hsva; } }; return /*#__PURE__*/ React.createElement( PanelCore, { style: [ styles.panelContainer, style, { position: 'relative', height: heightStyle, borderWidth: 0, padding: 0, }, ], label: props.accessibilityHint ?? `Hue and ${verticalChannel} 2D slider`, hint: props.accessibilityHint ?? `Double tap to switch between hue and ${verticalChannel}`, labelX: 'Hue', currentXValue: hueValue, maxXValue: 360, labelY: verticalChannel, currentYValue: verticalChannelValue, reverseX: reverseHue, reverseY: reverseVerticalChannel, width: width, height: height, gestures: gestures, onGestureUpdate: onGestureUpdate, onBegin: onBegin, onUpdate: onUpdate, onEnd: onEnd, }, /*#__PURE__*/ React.createElement( ImageBackground, { source: require('../../assets/Hue.png'), style: [ styles.panelImage, { position: 'relative', borderRadius, transform: [ { scaleX: reverseHue ? -1 : 1, }, ], }, ], resizeMode: 'stretch', 'aria-hidden': true, }, /*#__PURE__*/ React.createElement( ConditionalRendering, { if: adaptSpectrum && verticalChannel === 'brightness', }, /*#__PURE__*/ React.createElement(Animated.View, { style: [spectrumStyle, StyleSheet.absoluteFill], }), ), /*#__PURE__*/ React.createElement(Animated.Image, { source: require('../../assets/blackGradient.png'), style: [styles.panelImage, panelImageStyle], tintColor: verticalChannel === 'saturation' ? '#fff' : verticalChannel === 'hsl-saturation' ? '#888' : undefined, resizeMode: 'stretch', }), /*#__PURE__*/ React.createElement( ConditionalRendering, { if: adaptSpectrum && (verticalChannel === 'saturation' || verticalChannel === 'hsl-saturation'), }, /*#__PURE__*/ React.createElement(Animated.View, { style: [spectrumStyle, StyleSheet.absoluteFill], }), ), ), /*#__PURE__*/ React.createElement(Thumb, { thumbShape: thumbShape, thumbSize: thumbSize, thumbColor: thumbColor, renderThumb: renderThumb, innerStyle: thumbInnerStyle, thumbAnimatedStyle: thumbAnimatedStyle, style: thumbStyle, getAdaptiveColor: getAdaptiveColor, }), ); }