UNPKG

reanimated-color-picker

Version:
239 lines (232 loc) 8 kB
import React from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, withTiming } from 'react-native-reanimated'; import colorKit from '../../../colorKit/index'; import usePickerContext from '../../../AppContext'; import { SliderCore } from '../SliderCore'; import Thumb from '../../Thumb/Thumb'; import { ConditionalRendering, getStyle, isRtl } from '../../../utils'; /** @see [HSLSaturationSlider](https://alabsi91.github.io/reanimated-color-picker/api/sliders/hsl/saturation-slider/) */ export function HSLSaturationSlider({ gestures = [], style = {}, vertical = false, reverse = false, ...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.thumbScaleAnimationValue ?? ctx.thumbScaleAnimationValue; const thumbScaleAnimationDuration = props.thumbScaleAnimationDuration ?? ctx.thumbScaleAnimationDuration; const adaptSpectrum = props.adaptSpectrum ?? ctx.adaptSpectrum; const sliderThickness = props.sliderThickness ?? ctx.sliderThickness; const borderRadius = getStyle(style, 'borderRadius') ?? 5; const getWidth = getStyle(style, 'width'); const getHeight = getStyle(style, 'height'); const width = useSharedValue(vertical ? sliderThickness : typeof getWidth === 'number' ? getWidth : 0); const height = useSharedValue(!vertical ? sliderThickness : typeof getHeight === 'number' ? getHeight : 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 === 0 || l === 100) { return { h, s: hslSaturationValue.value, l, }; } hslSaturationValue.value = s; return { h, s, l, }; }, [hueValue, saturationValue, brightnessValue]); const thumbAnimatedStyle = useAnimatedStyle(() => { const length = (vertical ? height.value : width.value) - (boundedThumb ? thumbSize : 0); const percent = (hsl.value.s / 100) * length; const pos = (reverse ? length - percent : percent) - (boundedThumb ? 0 : thumbSize / 2); const posY = vertical ? pos : height.value / 2 - thumbSize / 2; const posX = vertical ? width.value / 2 - thumbSize / 2 : pos; return { transform: [ { translateY: posY, }, { translateX: posX, }, { scale: handleScale.value, }, ], }; }, [width, height, hsl, handleScale, vertical, reverse, boundedThumb, thumbSize]); const activeColorStyle = useAnimatedStyle(() => { return { backgroundColor: `hsl(${hsl.value.h}, 100%, 50%)`, }; }, [hsl]); const activeBrightnessStyle = useAnimatedStyle(() => { if (!adaptSpectrum) { return {}; } 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})`, }; }, [hsl, adaptSpectrum]); const imageStyle = useAnimatedStyle(() => { const imageRotate = vertical ? (reverse ? '270deg' : '90deg') : reverse ? '180deg' : '0deg'; const imageTranslateY = ((height.value - width.value) / 2) * ((reverse && isRtl) || (!reverse && !isRtl) ? 1 : -1); return { // Width and height are intentionally swapped to correct dimensions after the rotation width: vertical ? height.value : '100%', height: vertical ? width.value : '100%', transform: [ { rotate: imageRotate, }, { translateX: vertical ? ((height.value - width.value) / 2) * (reverse ? -1 : 1) : 0, }, { translateY: vertical ? imageTranslateY : 0, }, ], }; }, [width, height, vertical, reverse]); const onBegin = () => { 'worklet'; handleScale.value = withTiming(thumbScaleAnimationValue, { duration: thumbScaleAnimationDuration, }); }; const onUpdate = newValue => { 'worklet'; // 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: newValue, l, }) .object(false); if (saturationValue.value === s && brightnessValue.value === v) { return; } saturationValue.value = s; brightnessValue.value = v; onGestureChange(); }; const onEnd = () => { 'worklet'; handleScale.value = withTiming(1, { duration: thumbScaleAnimationDuration, }); onGestureEnd(); }; const getAdaptiveColor = hsva => { 'worklet'; if (adaptSpectrum) { return hsva; } const { h, s } = hsl.value; return { h, s, l: 50, }; }; return /*#__PURE__*/ React.createElement( SliderCore, { style: [ style, { position: 'relative', borderRadius, borderWidth: 0, padding: 0, }, activeColorStyle, ], label: props.accessibilityLabel ?? 'HSL Saturation Slider', hint: props.accessibilityHint, currentValue: hslSaturationValue, width: width, height: height, thumbSize: thumbSize, boundedThumb: boundedThumb, sliderThickness: sliderThickness, gestures: gestures, vertical: vertical, reverse: reverse, onBegin: onBegin, onUpdate: onUpdate, onEnd: onEnd, }, /*#__PURE__*/ React.createElement( View, { style: { flex: 1, borderRadius, overflow: 'hidden', }, 'aria-hidden': true, }, /*#__PURE__*/ React.createElement(Animated.Image, { source: require('../../../assets/blackGradient.png'), style: imageStyle, tintColor: '#888', }), ), /*#__PURE__*/ React.createElement( ConditionalRendering, { if: adaptSpectrum, }, /*#__PURE__*/ React.createElement(Animated.View, { style: [ { borderRadius, }, activeBrightnessStyle, StyleSheet.absoluteFill, ], 'aria-hidden': true, }), ), /*#__PURE__*/ React.createElement(Thumb, { thumbShape: thumbShape, thumbSize: thumbSize, thumbColor: thumbColor, renderThumb: renderThumb, innerStyle: thumbInnerStyle, thumbAnimatedStyle: thumbAnimatedStyle, style: thumbStyle, vertical: vertical, getAdaptiveColor: getAdaptiveColor, }), ); }