├── assets └── graphics │ └── ui │ ├── color-wheel.png │ ├── black-gradient.png │ └── black-gradient-rotated.png ├── package.json ├── types.d.ts ├── README.md └── ColorPicker.js /assets/graphics/ui/color-wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naeemur/react-native-wheel-color-picker/HEAD/assets/graphics/ui/color-wheel.png -------------------------------------------------------------------------------- /assets/graphics/ui/black-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naeemur/react-native-wheel-color-picker/HEAD/assets/graphics/ui/black-gradient.png -------------------------------------------------------------------------------- /assets/graphics/ui/black-gradient-rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naeemur/react-native-wheel-color-picker/HEAD/assets/graphics/ui/black-gradient-rotated.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-wheel-color-picker", 3 | "version": "1.3.1", 4 | "description": "A color picker component for react native", 5 | "main": "ColorPicker.js", 6 | "homepage": "https://github.com/naeemur/react-native-wheel-color-picker#readme", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/naeemur/react-native-wheel-color-picker.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/naeemur/react-native-wheel-color-picker/issues" 14 | }, 15 | "dependencies": { 16 | "react-native-elevation": "^1.0.0" 17 | }, 18 | "keywords": [ 19 | "react-native", 20 | "react native", 21 | "material design", 22 | "wheel", 23 | "color", 24 | "picker" 25 | ], 26 | "typings": "types.d.ts" 27 | } 28 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface ColorPickerProps extends React.Props { 4 | /** Use row or vertical layout */ 5 | row?: boolean, 6 | /** Enables snapping on the center of wheel and edges of wheel and slider */ 7 | noSnap?: boolean, 8 | /** Wheel color thumb size */ 9 | thumbSize?: number, 10 | /** Slider and slider color thumb size */ 11 | sliderSize?: number, 12 | /** Gap size between wheel & slider */ 13 | gapSize?: number, 14 | /** Use swatches of shades instead of slider */ 15 | discrete?: boolean, 16 | /** Number of swatches of shades, should be > 1 */ 17 | discreteLength?: number, 18 | /** If true the slider is hidden */ 19 | sliderHidden?: boolean, 20 | /** Show color swatches */ 21 | swatches?: boolean, 22 | /** If false swatches are shown before wheel */ 23 | swatchesLast?: boolean, 24 | /** Show swatch only and hide wheel and slider */ 25 | swatchesOnly?: boolean, 26 | /** Defines how far the touch event can start away from the swatch */ 27 | swatchesHitSlop?: {top: number, left: number, bottom: number, right: number}, 28 | /** Color of the color picker */ 29 | color?: string, 30 | /** Palette colors of swatches */ 31 | palette?: string[], 32 | /** If true the wheel thumb color is shaded */ 33 | shadeWheelThumb?: boolean, 34 | /** If true the slider thumb color is shaded */ 35 | shadeSliderThumb?: boolean, 36 | /** If true the slider thumb is reset to 0 value when wheel thumb is moved */ 37 | autoResetSlider?: boolean, 38 | /** Callback function triggered when user begins dragging slider/wheel */ 39 | onInteractionStart?: () => void, 40 | /** Callback function providing current color while user is actively dragging slider/wheel */ 41 | onColorChange?: (color: string) => void, 42 | /** Callback function providing final color when user stops dragging slider/wheel */ 43 | onColorChangeComplete?: (color: string) => void, 44 | /** Wheel image loading component eg: */ 45 | wheelLoadingIndicator?: React.ReactNode, 46 | /** Slider image loading component eg: */ 47 | sliderLoadingIndicator?: React.ReactNode, 48 | /** To use useNativeDriver for animations if possible */ 49 | useNativeDriver?: boolean, 50 | /** To use onLayoutEvent.nativeEvent.layout instead of measureInWindow for x, y, width, height values for wheel and slider measurements which may be useful to prevent some layout problems */ 51 | useNativeLayout?: boolean, 52 | /** Disable all interactions */ 53 | disabled?: boolean, 54 | /** Flip touch positioning on X axis, might be useful in UI with RTL support */ 55 | flipTouchX?: boolean, 56 | /** Flip touch positioning on Y axis, might be useful in UI with RTL support */ 57 | flipTouchY?: boolean, 58 | /** If true the wheel is hidden, does not work with sliderHidden = true */ 59 | wheelHidden?: boolean, 60 | } 61 | 62 | declare class ColorPicker extends React.Component { 63 | revert(): void; 64 | } 65 | 66 | declare module 'react-native-wheel-color-picker-master' { 67 | } 68 | 69 | export default ColorPicker; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Wheel Color Picker 2 | 3 | A color picker component for react native. 4 | 5 | ## Features 6 | - Pure JS, lightweight, works on Android, iOS and Web 7 | - Uses hue-saturation color wheel and lightness slider 8 | - Selectable from color swatchs 9 | - Smooth and discrete color slider 10 | - Color change animations on wheel, slider and swatches 11 | 12 | ![Demo Image](https://naeemur.github.io/asset-bucket/rn-wheel-color-picker.gif) 13 | 14 | (This demo uses my [react-native-advanced-ripple](https://github.com/Naeemur/react-native-advanced-ripple) and [react-native-elevation](https://github.com/Naeemur/react-native-elevation) modules.) 15 | 16 | ## Installation 17 | 18 | ``` 19 | npm install react-native-wheel-color-picker 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```js 25 | import { Component } from 'react' 26 | import { View, Text, ActivityIndicator } from 'react-native' 27 | 28 | import ColorPicker from 'react-native-wheel-color-picker' 29 | 30 | class App extends Component { 31 | render() { 32 | return ( 33 | 34 | { this.picker = r }} 36 | color={this.state.currentColor} 37 | swatchesOnly={this.state.swatchesOnly} 38 | onColorChange={this.onColorChange} 39 | onColorChangeComplete={this.onColorChangeComplete} 40 | thumbSize={40} 41 | sliderSize={40} 42 | noSnap={true} 43 | row={false} 44 | swatchesLast={this.state.swatchesLast} 45 | swatches={this.state.swatchesEnabled} 46 | discrete={this.state.disc} 47 | wheelLodingIndicator={} 48 | sliderLodingIndicator={} 49 | useNativeDriver={false} 50 | useNativeLayout={false} 51 | /> 52 | this.picker.revert()} /> 53 | 54 | ) 55 | } 56 | } 57 | 58 | export default App 59 | ``` 60 | 61 | ## Changelog 62 | 63 | ### 1.3.1 64 | - fixed a bug related to `useNativeDriver` 65 | - no breaking changes 66 | 67 | ### 1.3.0 68 | - added changelog section to README.md 69 | - prop added: `wheelLoadingIndicator` 70 | - prop added: `sliderLoadingIndicator` 71 | - prop added: `useNativeDriver` 72 | - prop added: `useNativeLayout` 73 | - prop added: `disabled` 74 | - prop added: `flipTouchX` 75 | - prop added: `flipTouchY` 76 | - prop added: `wheelHidden` 77 | - fixed a bug related to `discreteLength` prop 78 | - no breaking changes 79 | 80 | ### 1.2.0 81 | - prop added: `gapSize` 82 | - prop added: `discreteLength` 83 | - prop added: `swatchesHitSlop` 84 | - prop added: `palette` 85 | - prop added: `onInteractionStart` 86 | - no breaking changes 87 | 88 | ### 1.1.0 89 | - prop added: `shadeWheelThumb` 90 | - prop added: `shadeSliderThumb` 91 | - prop added: `autoResetSlider` 92 | - no breaking changes 93 | 94 | ## API 95 | 96 | ### ***ColorPicker*** 97 | 98 | ### Component props and default values 99 | `row: false` use row or vertical layout 100 | 101 | `noSnap: false` enables snapping on the center of wheel and edges of wheel and slider 102 | 103 | `thumbSize: 50` wheel color thumb size 104 | 105 | `sliderSize: 20` slider and slider color thumb size 106 | 107 | `gapSize: 16` size of gap between slider and wheel 108 | 109 | `discrete: false` use swatches of shades instead of slider 110 | 111 | `discreteLength: 10` number of swatches of shades, should be > 1 112 | 113 | `sliderHidden: false` if true the slider is hidden 114 | 115 | `swatches: true` show color swatches 116 | 117 | `swatchesLast: true` if false swatches are shown before wheel 118 | 119 | `swatchesOnly: false` show swatch only and hide wheel and slider 120 | 121 | `swatchesHitSlop: undefined` defines how far the touch event can start away from the swatch 122 | 123 | `color: '#ffffff'` color of the color picker 124 | 125 | `palette: ['#000000','#888888','#ed1c24','#d11cd5','#1633e6','#00aeef','#00c85d','#57ff0a','#ffde17','#f26522']` palette colors of swatches 126 | 127 | `shadeWheelThumb: true` if true the wheel thumb color is shaded 128 | 129 | `shadeSliderThumb: false` if true the slider thumb color is shaded 130 | 131 | `autoResetSlider: false` if true the slider thumb is reset to 0 value when wheel thumb is moved 132 | 133 | `onInteractionStart: () => {}` callback function triggered when user begins dragging slider/wheel 134 | 135 | `onColorChange: (color) => {}` callback function providing current color while user is actively dragging slider/wheel 136 | 137 | `onColorChangeComplete: (color) => {}` callback function providing final color when user stops dragging slider/wheel 138 | 139 | `wheelLoadingIndicator: null` wheel image loading component eg: 140 | 141 | `sliderLoadingIndicator: null` slider image loading component eg: 142 | 143 | `useNativeDriver: false` to use useNativeDriver for animations if possible 144 | 145 | `useNativeLayout: false` to use onLayoutEvent.nativeEvent.layout instead of measureInWindow for x, y, width, height values for wheel and slider measurements which may be useful to prevent some layout problems 146 | 147 | `disabled: false` disable all interactions 148 | 149 | `flipTouchX: false` flip touch positioning on X axis, might be useful in UI with RTL support 150 | 151 | `flipTouchY: false` flip touch positioning on Y axis, might be useful in UI with RTL support 152 | 153 | `wheelHidden: false` if true the wheel is hidden, does not work with sliderHidden = true 154 | 155 | ### Instance methods 156 | `revert()` reverts the color to the one provided in the color prop 157 | 158 | ## License 159 | The MIT License (MIT) 160 | 161 | Copyright (c) 2020-2024 Md. Naeemur Rahman (https://naeemur.github.io) 162 | 163 | Permission is hereby granted, free of charge, to any person obtaining a copy 164 | of this software and associated documentation files (the "Software"), to deal 165 | in the Software without restriction, including without limitation the rights 166 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 167 | copies of the Software, and to permit persons to whom the Software is 168 | furnished to do so, subject to the following conditions: 169 | 170 | The above copyright notice and this permission notice shall be included in 171 | all copies or substantial portions of the Software. 172 | 173 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 174 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 175 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 176 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 177 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 178 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 179 | THE SOFTWARE. 180 | -------------------------------------------------------------------------------- /ColorPicker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const React = require('react'), { Component } = React 4 | 5 | const { 6 | Animated, 7 | Image, 8 | Dimensions, 9 | PanResponder, 10 | StyleSheet, 11 | TouchableWithoutFeedback, 12 | View, 13 | Text, 14 | ActivityIndicator, 15 | } = require('react-native') 16 | 17 | const Elevations = require('react-native-elevation') 18 | const srcWheel = require('./assets/graphics/ui/color-wheel.png') 19 | const srcSlider = require('./assets/graphics/ui/black-gradient.png') 20 | const srcSliderRotated = require('./assets/graphics/ui/black-gradient-rotated.png') 21 | 22 | const PALETTE = [ 23 | '#000000', 24 | '#888888', 25 | '#ed1c24', 26 | '#d11cd5', 27 | '#1633e6', 28 | '#00aeef', 29 | '#00c85d', 30 | '#57ff0a', 31 | '#ffde17', 32 | '#f26522', 33 | ] 34 | 35 | const RGB_MAX = 255 36 | const HUE_MAX = 360 37 | const SV_MAX = 100 38 | 39 | const normalize = (degrees) => ((degrees % 360 + 360) % 360) 40 | 41 | const rgb2Hsv = (r, g, b) => { 42 | if (typeof r === 'object') { 43 | const args = r 44 | r = args.r; g = args.g; b = args.b; 45 | } 46 | 47 | // It converts [0,255] format, to [0,1] 48 | r = (r === RGB_MAX) ? 1 : (r % RGB_MAX / parseFloat(RGB_MAX)) 49 | g = (g === RGB_MAX) ? 1 : (g % RGB_MAX / parseFloat(RGB_MAX)) 50 | b = (b === RGB_MAX) ? 1 : (b % RGB_MAX / parseFloat(RGB_MAX)) 51 | 52 | let max = Math.max(r, g, b) 53 | let min = Math.min(r, g, b) 54 | let h, s, v = max 55 | 56 | let d = max - min 57 | 58 | s = max === 0 ? 0 : d / max 59 | 60 | if (max === min) { 61 | h = 0 // achromatic 62 | } else { 63 | switch (max) { 64 | case r: 65 | h = (g - b) / d + (g < b ? 6 : 0) 66 | break 67 | case g: 68 | h = (b - r) / d + 2 69 | break 70 | case b: 71 | h = (r - g) / d + 4 72 | break 73 | } 74 | h /= 6 75 | } 76 | 77 | return { 78 | h: Math.round(h * HUE_MAX), 79 | s: Math.round(s * SV_MAX), 80 | v: Math.round(v * SV_MAX) 81 | } 82 | } 83 | 84 | const hsv2Rgb = (h, s, v) => { 85 | if (typeof h === 'object') { 86 | const args = h 87 | h = args.h; s = args.s; v = args.v; 88 | } 89 | 90 | h = normalize(h) 91 | h = (h === HUE_MAX) ? 1 : (h % HUE_MAX / parseFloat(HUE_MAX) * 6) 92 | s = (s === SV_MAX) ? 1 : (s % SV_MAX / parseFloat(SV_MAX)) 93 | v = (v === SV_MAX) ? 1 : (v % SV_MAX / parseFloat(SV_MAX)) 94 | 95 | let i = Math.floor(h) 96 | let f = h - i 97 | let p = v * (1 - s) 98 | let q = v * (1 - f * s) 99 | let t = v * (1 - (1 - f) * s) 100 | let mod = i % 6 101 | let r = [v, q, p, p, t, v][mod] 102 | let g = [t, v, v, q, p, p][mod] 103 | let b = [p, p, t, v, v, q][mod] 104 | 105 | return { 106 | r: Math.floor(r * RGB_MAX), 107 | g: Math.floor(g * RGB_MAX), 108 | b: Math.floor(b * RGB_MAX), 109 | } 110 | } 111 | 112 | const rgb2Hex = (r, g, b) => { 113 | if (typeof r === 'object') { 114 | const args = r 115 | r = args.r; g = args.g; b = args.b; 116 | } 117 | r = Math.round(r).toString(16) 118 | g = Math.round(g).toString(16) 119 | b = Math.round(b).toString(16) 120 | 121 | r = r.length === 1 ? '0' + r : r 122 | g = g.length === 1 ? '0' + g : g 123 | b = b.length === 1 ? '0' + b : b 124 | 125 | return '#' + r + g + b 126 | } 127 | 128 | const hex2Rgb = (hex) => { 129 | let result = (/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i).exec(hex) 130 | return result ? { 131 | r: parseInt(result[1], 16), 132 | g: parseInt(result[2], 16), 133 | b: parseInt(result[3], 16) 134 | } : null 135 | } 136 | 137 | const hsv2Hex = (h, s, v) => { 138 | let rgb = hsv2Rgb(h, s, v) 139 | return rgb2Hex(rgb.r, rgb.g, rgb.b) 140 | } 141 | 142 | const hex2Hsv = (hex) => { 143 | let rgb = hex2Rgb(hex) 144 | return rgb2Hsv(rgb.r, rgb.g, rgb.b) 145 | } 146 | 147 | // expands hex to full 6 chars (#fff -> #ffffff) if necessary 148 | const expandColor = color => typeof color == 'string' && color.length === 4 149 | ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` 150 | : color; 151 | 152 | const flipInterpolationConfig = { 153 | inputRange: [1, 10], 154 | outputRange: [-1, -10], 155 | extrapolate: 'extend', // a string such as 'extend', 'identity', or 'clamp' 156 | } 157 | 158 | 159 | module.exports = class ColorPicker extends Component { 160 | // testData = {} 161 | // testView = {forceUpdate(){}} 162 | color = { h: 0, s: 0, v: 100 } 163 | slideX = new Animated.Value(0) 164 | slideY = new Animated.Value(0) 165 | panX = new Animated.Value(30) 166 | panY = new Animated.Value(30) 167 | sliderLength = 0 168 | wheelSize = 0 169 | sliderMeasure = {} 170 | wheelMeasure = {} 171 | wheelWidth = 0 172 | static defaultProps = { 173 | row: false, // use row or vertical layout 174 | noSnap: false, // enables snapping on the center of wheel and edges of wheel and slider 175 | thumbSize: 50, // wheel color thumb size 176 | sliderSize: 20, // slider and slider color thumb size 177 | gapSize: 16, // size of gap between slider and wheel 178 | discrete: false, // use swatches of shades instead of slider 179 | discreteLength: 10, // number of swatches of shades, should be > 1 180 | sliderHidden: false, // if true the slider is hidden 181 | swatches: true, // show color swatches 182 | swatchesLast: true, // if false swatches are shown before wheel 183 | swatchesOnly: false, // show swatch only and hide wheel and slider 184 | swatchesHitSlop: undefined, // defines how far the touch event can start away from the swatch 185 | color: '#ffffff', // color of the color picker 186 | palette: PALETTE, // palette colors of swatches 187 | shadeWheelThumb: true, // if true the wheel thumb color is shaded 188 | shadeSliderThumb: false, // if true the slider thumb color is shaded 189 | autoResetSlider: false, // if true the slider thumb is reset to 0 value when wheel thumb is moved 190 | onInteractionStart: () => { }, // callback function triggered when user begins dragging slider/wheel 191 | onColorChange: () => { }, // callback function providing current color while user is actively dragging slider/wheel 192 | onColorChangeComplete: () => { }, // callback function providing final color when user stops dragging slider/wheel 193 | wheelLoadingIndicator: null, // wheel image loading component eg: 194 | sliderLoadingIndicator: null, // slider image loading component eg: 195 | useNativeDriver: false, // to use useNativeDriver for animations if possible 196 | useNativeLayout: false, // to use onLayoutEvent.nativeEvent.layout instead of measureInWindow for x, y, width, height values for wheel and slider measurements which may be useful to prevent some layout problems 197 | disabled: false, // disable all interactions 198 | flipTouchX: false, // flip touch positioning on X axis, might be useful in UI with RTL support 199 | flipTouchY: false, // flip touch positioning on Y axis, might be useful in UI with RTL support 200 | wheelHidden: false, // if true the wheel is hidden, does not work with sliderHidden = true 201 | } 202 | wheelPanResponder = PanResponder.create({ 203 | onStartShouldSetPanResponderCapture: (event, gestureState) => { 204 | if (this.props.disabled) return false; 205 | const { nativeEvent } = event 206 | if (this.outOfWheel(nativeEvent)) return 207 | this.wheelMovement(event, gestureState) 208 | this.updateHueSaturation({ nativeEvent }) 209 | return true 210 | }, 211 | onStartShouldSetPanResponder: () => true, 212 | onMoveShouldSetPanResponderCapture: () => true, 213 | onPanResponderGrant: (event, gestureState) => { 214 | if (this.props.disabled) return; 215 | const { locationX, locationY } = event.nativeEvent 216 | const { moveX, moveY, x0, y0 } = gestureState 217 | const x = x0 - locationX, y = y0 - locationY 218 | this.wheelMeasure.x = x 219 | this.wheelMeasure.y = y 220 | this.props.onInteractionStart(); 221 | return true 222 | }, 223 | onPanResponderMove: (event, gestureState) => { 224 | if (this.props.disabled) return; 225 | if (event && event.nativeEvent && typeof event.nativeEvent.preventDefault == 'function') event.nativeEvent.preventDefault() 226 | if (event && event.nativeEvent && typeof event.nativeEvent.stopPropagation == 'function') event.nativeEvent.stopPropagation() 227 | if (this.outOfWheel(event.nativeEvent) || this.outOfBox(this.wheelMeasure, gestureState)) return; 228 | this.wheelMovement(event, gestureState) 229 | }, 230 | onMoveShouldSetPanResponder: () => true, 231 | onPanResponderRelease: (event, gestureState) => { 232 | if (this.props.disabled) return; 233 | const { nativeEvent } = event 234 | const { radius } = this.polar(nativeEvent) 235 | const { hsv } = this.state 236 | const { h, s, v } = hsv 237 | if (!this.props.noSnap && radius <= 0.10 && radius >= 0) this.animate('#ffffff', 'hs', false, true) 238 | if (!this.props.noSnap && radius >= 0.95 && radius <= 1) this.animate(this.state.currentColor, 'hs', true) 239 | if (this.props.onColorChangeComplete) this.props.onColorChangeComplete(hsv2Hex(hsv)) 240 | this.setState({ currentColor: this.state.currentColor }, x => this.renderDiscs()) 241 | }, 242 | }) 243 | sliderPanResponder = PanResponder.create({ 244 | onStartShouldSetPanResponderCapture: (event, gestureState) => { 245 | if (this.props.disabled) return false; 246 | const { nativeEvent } = event 247 | if (this.outOfSlider(nativeEvent)) return 248 | this.sliderMovement(event, gestureState) 249 | this.updateValue({ nativeEvent }) 250 | return true 251 | }, 252 | onStartShouldSetPanResponder: () => true, 253 | onMoveShouldSetPanResponderCapture: () => true, 254 | onPanResponderGrant: (event, gestureState) => { 255 | if (this.props.disabled) return; 256 | const { locationX, locationY } = event.nativeEvent 257 | const { moveX, moveY, x0, y0 } = gestureState 258 | const x = x0 - locationX, y = y0 - locationY 259 | this.sliderMeasure.x = x 260 | this.sliderMeasure.y = y 261 | this.props.onInteractionStart(); 262 | return true 263 | }, 264 | onPanResponderMove: (event, gestureState) => { 265 | if (this.props.disabled) return; 266 | if (event && event.nativeEvent && typeof event.nativeEvent.preventDefault == 'function') event.nativeEvent.preventDefault() 267 | if (event && event.nativeEvent && typeof event.nativeEvent.stopPropagation == 'function') event.nativeEvent.stopPropagation() 268 | if (this.outOfSlider(event.nativeEvent) || this.outOfBox(this.sliderMeasure, gestureState)) return; 269 | this.sliderMovement(event, gestureState) 270 | }, 271 | onMoveShouldSetPanResponder: () => true, 272 | onPanResponderRelease: (event, gestureState) => { 273 | if (this.props.disabled) return; 274 | const { nativeEvent } = event 275 | const { hsv } = this.state 276 | const { h, s, v } = hsv 277 | const ratio = this.ratio(nativeEvent) 278 | if (!this.props.noSnap && ratio <= 0.05 && ratio >= 0) this.animate(this.state.currentColor, 'v', false) 279 | if (!this.props.noSnap && ratio >= 0.95 && ratio <= 1) this.animate(this.state.currentColor, 'v', true) 280 | if (this.props.onColorChangeComplete) this.props.onColorChangeComplete(hsv2Hex(hsv)) 281 | }, 282 | }) 283 | constructor(props) { 284 | super(props) 285 | this.mounted = false 286 | this.swatchesUpdatedAt = 0 287 | this.discsUpdatedAt = 0 288 | this.state = { 289 | wheelOpacity: 0, 290 | sliderOpacity: 0, 291 | hueSaturation: hsv2Hex(this.color.h, this.color.s, 100), 292 | currentColor: props.color, 293 | hsv: { h: 0, s: 0, v: 100 }, 294 | wheelImageLoaded: false, 295 | sliderImageLoaded: false, 296 | palette: props.palette, 297 | discreteLength: props.discreteLength, 298 | swatchesHitSlop: props.swatchesHitSlop, 299 | swatchesUpdatedAt: 0, 300 | discsUpdatedAt: 0, 301 | } 302 | this.wheelMovement = new Animated.event( 303 | [ 304 | { 305 | nativeEvent: { 306 | locationX: this.panX, 307 | locationY: this.panY, 308 | } 309 | }, 310 | null, 311 | ], 312 | { 313 | useNativeDriver: false, 314 | listener: this.updateHueSaturation 315 | } 316 | ) 317 | this.sliderMovement = new Animated.event( 318 | [ 319 | { 320 | nativeEvent: { 321 | locationX: this.slideX, 322 | locationY: this.slideY, 323 | } 324 | }, 325 | null, 326 | ], 327 | { 328 | useNativeDriver: false, 329 | listener: this.updateValue 330 | } 331 | ) 332 | this.initSwatchAnimatedValues(props) 333 | this.initDiscAnimatedValues(props) 334 | this.renderSwatches() 335 | this.renderDiscs() 336 | } 337 | componentDidMount() { 338 | this.mounted = true; 339 | } 340 | componentWillUnmount() { 341 | this.mounted = false; 342 | } 343 | onSwatchPress = (c, i) => { 344 | if (this.props.disabled) return; 345 | this.swatchAnim[i].stopAnimation() 346 | Animated.timing(this.swatchAnim[i], { 347 | toValue: 1, 348 | useNativeDriver: false, 349 | duration: 500, 350 | }).start(x => { 351 | this.swatchAnim[i].setValue(0) 352 | }) 353 | this.animate(c) 354 | } 355 | onDiscPress = (c, i) => { 356 | if (this.props.disabled) return; 357 | this.discAnim[i].stopAnimation() 358 | Animated.timing(this.discAnim[i], { 359 | toValue: 1, 360 | useNativeDriver: false, 361 | duration: 500, 362 | }).start(x => { 363 | this.discAnim[i].setValue(0) 364 | }) 365 | const val = i >= 9 ? 100 : 11 * i 366 | this.updateValue({ nativeEvent: null }, val) 367 | this.animate({ h: this.color.h, s: this.color.s, v: val }, 'v') 368 | } 369 | onSquareLayout = (e) => { 370 | let { x, y, width, height } = e.nativeEvent.layout 371 | this.wheelWidth = Math.min(width, height) 372 | this.tryForceUpdate() 373 | } 374 | onWheelImageLoad = (e) => { 375 | this.setState({ wheelImageLoaded: true }) 376 | } 377 | onSliderImageLoad = (e) => { 378 | this.setState({ sliderImageLoaded: true }) 379 | } 380 | onWheelLayout = (e) => { 381 | /* 382 | * const {x, y, width, height} = nativeEvent.layout 383 | * onLayout values are different than measureInWindow 384 | * x and y are the distances to its previous element 385 | * but in measureInWindow they are relative to the window 386 | */ 387 | if (!!this.props.useNativeLayout) { 388 | let {x, y, width, height} = e.nativeEvent.layout 389 | this.setWheelMeasure(x, y, width, height) 390 | } else { 391 | this.wheel.measureInWindow(this.setWheelMeasure) 392 | } 393 | } 394 | onSliderLayout = (e) => { 395 | if (!!this.props.useNativeLayout) { 396 | let {x, y, width, height} = e.nativeEvent.layout 397 | this.setSliderMeasure(x, y, width, height) 398 | } else { 399 | this.slider.measureInWindow(this.setSliderMeasure) 400 | } 401 | } 402 | setWheelMeasure = (x, y, width, height) => { 403 | this.wheelMeasure = { x, y, width, height } 404 | this.wheelSize = width 405 | // this.panX.setOffset(-width/2) 406 | // this.panY.setOffset(-width/2) 407 | this.update(this.state.currentColor) 408 | this.setState({ wheelOpacity: 1 }) 409 | } 410 | setSliderMeasure = (x, y, width, height) => { 411 | this.sliderMeasure = { x, y, width, height } 412 | this.sliderLength = this.props.row ? height - width : width - height 413 | // this.slideX.setOffset(-width/2) 414 | // this.slideY.setOffset(-width/2) 415 | this.update(this.state.currentColor) 416 | this.setState({ sliderOpacity: 1 }) 417 | } 418 | outOfBox(measure, gestureState) { 419 | const { x, y, width, height } = measure 420 | const { moveX, moveY, x0, y0 } = gestureState 421 | // console.log(`${moveX} , ${moveY} / ${x} , ${y} / ${locationX} , ${locationY}`); 422 | return !(moveX >= x && moveX <= x + width && moveY >= y && moveY <= y + height) 423 | } 424 | outOfWheel(nativeEvent) { 425 | const { radius } = this.polar(nativeEvent) 426 | return radius > 1 427 | } 428 | outOfSlider(nativeEvent) { 429 | const row = this.props.row 430 | const loc = row ? nativeEvent.locationY : nativeEvent.locationX 431 | const { width, height } = this.sliderMeasure 432 | return (loc > (row ? height - width : width - height)) 433 | } 434 | val(v) { 435 | const d = this.props.discrete, r = 11 * Math.round(v / 11) 436 | return d ? (r >= 99 ? 100 : r) : v 437 | } 438 | ratio(nativeEvent) { 439 | const row = this.props.row 440 | const loc = row ? nativeEvent.locationY : nativeEvent.locationX 441 | const { width, height } = this.sliderMeasure 442 | return 1 - (loc / (row ? height - width : width - height)) 443 | } 444 | polar(nativeEvent) { 445 | const lx = nativeEvent.locationX, ly = nativeEvent.locationY 446 | const [x, y] = [lx - this.wheelSize / 2, ly - this.wheelSize / 2] 447 | return { 448 | deg: Math.atan2(y, x) * (-180 / Math.PI), 449 | radius: Math.sqrt(y * y + x * x) / (this.wheelSize / 2), 450 | // radius: Math.min(1, Math.max(0, Math.sqrt(y * y + x * x) / (this.wheelSize / 2))), // not working well 451 | } 452 | } 453 | cartesian(deg, radius) { 454 | const r = radius * this.wheelSize / 2 // was normalized 455 | const rad = Math.PI * deg / 180 456 | const x = r * Math.cos(rad) 457 | const y = r * Math.sin(rad) 458 | return { 459 | left: this.wheelSize / 2 + x, 460 | top: this.wheelSize / 2 - y, 461 | } 462 | } 463 | updateHueSaturation = ({ nativeEvent }) => { 464 | const { deg, radius } = this.polar(nativeEvent), h = deg, s = Math.max(0, Math.min(100, 100 * radius)), v = this.color.v 465 | // if(radius > 1 ) return 466 | const hsv = { h, s, v }// v: 100} // causes bug 467 | if (this.props.autoResetSlider === true) { 468 | this.slideX.setValue(0) 469 | this.slideY.setValue(0) 470 | hsv.v = 100 471 | } 472 | const currentColor = hsv2Hex(hsv) 473 | this.color = hsv 474 | this.setState({ hsv, currentColor, hueSaturation: hsv2Hex(this.color.h, this.color.s, 100) }) 475 | this.props.onColorChange(hsv2Hex(hsv)) 476 | // this.testData.deg = deg 477 | // this.testData.radius = radius 478 | // this.testData.pan = JSON.stringify({x:this.panX,y:this.panY}) 479 | // this.testData.pan = JSON.stringify(this.state.pan.getTranslateTransform()) 480 | // this.testView.forceUpdate() 481 | } 482 | updateValue = ({ nativeEvent }, val) => { 483 | const { h, s } = this.color, v = (typeof val == 'number') ? val : 100 * this.ratio(nativeEvent) 484 | const hsv = { h, s, v } 485 | const currentColor = hsv2Hex(hsv) 486 | this.color = hsv 487 | this.setState({ hsv, currentColor, hueSaturation: hsv2Hex(this.color.h, this.color.s, 100) }) 488 | this.props.onColorChange(hsv2Hex(hsv)) 489 | } 490 | update = (color, who, max, force) => { 491 | const isHex = /^#(([0-9a-f]{2}){3}|([0-9a-f]){3})$/i 492 | if (!isHex.test(color)) color = '#ffffff' 493 | color = expandColor(color); 494 | const specific = (typeof who == 'string'), who_hs = (who == 'hs'), who_v = (who == 'v') 495 | let { h, s, v } = (typeof color == 'string') ? hex2Hsv(color) : color, stt = {} 496 | h = (who_hs || !specific) ? h : this.color.h 497 | s = (who_hs && max) ? 100 : (who_hs && max === false) ? 0 : (who_hs || !specific) ? s : this.color.s 498 | v = (who_v && max) ? 100 : (who_v && max === false) ? 0 : (who_v || !specific) ? v : this.color.v 499 | const range = (100 - v) / 100 * this.sliderLength 500 | const { left, top } = this.cartesian(h, s / 100) 501 | const hsv = { h, s, v } 502 | if (!specific || force) { 503 | this.color = hsv 504 | stt.hueSaturation = hsv2Hex(this.color.h, this.color.s, 100) 505 | // this.setState({hueSaturation: hsv2Hex(this.color.h,this.color.s,100)}) 506 | } 507 | stt.currentColor = hsv2Hex(hsv) 508 | this.setState(stt, x => { this.tryForceUpdate(); this.renderDiscs(); }) 509 | // this.setState({currentColor:hsv2Hex(hsv)}, x=>this.tryForceUpdate()) 510 | this.props.onColorChange(hsv2Hex(hsv)) 511 | if (this.props.onColorChangeComplete) this.props.onColorChangeComplete(hsv2Hex(hsv)) 512 | if (who_hs || !specific) { 513 | this.panY.setValue(top)// - this.props.thumbSize / 2) 514 | this.panX.setValue(left)// - this.props.thumbSize / 2) 515 | } 516 | if (who_v || !specific) { 517 | this.slideX.setValue(range) 518 | this.slideY.setValue(range) 519 | } 520 | } 521 | animate = (color, who, max, force) => { 522 | const isHex = /^#(([0-9a-f]{2}){3}|([0-9a-f]){3})$/i 523 | if (!isHex.test(color)) color = '#ffffff' 524 | color = expandColor(color); 525 | const specific = (typeof who == 'string'), who_hs = (who == 'hs'), who_v = (who == 'v') 526 | let { h, s, v } = (typeof color == 'string') ? hex2Hsv(color) : color, stt = {} 527 | h = (who_hs || !specific) ? h : this.color.h 528 | s = (who_hs && max) ? 100 : (who_hs && max === false) ? 0 : (who_hs || !specific) ? s : this.color.s 529 | v = (who_v && max) ? 100 : (who_v && max === false) ? 0 : (who_v || !specific) ? v : this.color.v 530 | const range = (100 - v) / 100 * this.sliderLength 531 | const { left, top } = this.cartesian(h, s / 100) 532 | const hsv = { h, s, v } 533 | // console.log(hsv); 534 | if (!specific || force) { 535 | this.color = hsv 536 | stt.hueSaturation = hsv2Hex(this.color.h, this.color.s, 100) 537 | // this.setState({hueSaturation: hsv2Hex(this.color.h,this.color.s,100)}) 538 | } 539 | stt.currentColor = hsv2Hex(hsv) 540 | this.setState(stt, x => { this.tryForceUpdate(); this.renderDiscs(); }) 541 | // this.setState({currentColor:hsv2Hex(hsv)}, x=>this.tryForceUpdate()) 542 | this.props.onColorChange(hsv2Hex(hsv)) 543 | if (this.props.onColorChangeComplete) this.props.onColorChangeComplete(hsv2Hex(hsv)) 544 | let anims = [] 545 | if (who_hs || !specific) anims.push(//{// 546 | Animated.spring(this.panX, { toValue: left, useNativeDriver: false, friction: 90 }),//.start()// 547 | Animated.spring(this.panY, { toValue: top, useNativeDriver: false, friction: 90 }),//.start()// 548 | )//}// 549 | if (who_v || !specific) anims.push(//{// 550 | Animated.spring(this.slideX, { toValue: range, useNativeDriver: false, friction: 90 }),//.start()// 551 | Animated.spring(this.slideY, { toValue: range, useNativeDriver: false, friction: 90 }),//.start()// 552 | )//}// 553 | Animated.parallel(anims).start() 554 | } 555 | // componentWillReceiveProps(nextProps) { // DEPRICATED 556 | // const { color } = nextProps 557 | // if(color !== this.props.color) this.animate(color) 558 | // } 559 | static getDerivedStateFromProps(nextProps, prevState) { 560 | const { palette, discreteLength, swatchesHitSlop } = nextProps 561 | const now = Date.now() 562 | const payload = {} 563 | if ( 564 | (Array.isArray(palette) && Array.isArray(prevState.palette) && palette.join('-') !== prevState.palette.join('-')) 565 | || swatchesHitSlop !== prevState.swatchesHitSlop 566 | ) { 567 | payload.palette = palette 568 | payload.swatchesHitSlop = swatchesHitSlop 569 | payload.swatchesUpdatedAt = now 570 | } 571 | if (discreteLength !== prevState.discreteLength || swatchesHitSlop !== prevState.swatchesHitSlop) { 572 | payload.discreteLength = discreteLength 573 | payload.swatchesHitSlop = swatchesHitSlop 574 | payload.discsUpdatedAt = now 575 | } 576 | return Object.keys(payload).length > 0 ? payload : null 577 | } 578 | componentDidUpdate(prevProps) { 579 | const { color } = this.props 580 | if (color !== prevProps.color) this.animate(color) 581 | } 582 | revert() { 583 | if (this.mounted) this.animate(this.props.color) 584 | } 585 | tryForceUpdate() { 586 | if (this.mounted) this.forceUpdate() 587 | } 588 | initSwatchAnimatedValues(props) { 589 | this.swatchAnim = props.palette.map((c, i) => (new Animated.Value(0))) 590 | } 591 | initDiscAnimatedValues(props) { 592 | const length = Math.max(props.discreteLength, 2) 593 | this.discAnim = (`1`).repeat(length).split('').map((c, i) => (new Animated.Value(0))) 594 | } 595 | renderSwatches() { 596 | // console.log('RENDER SWATCHES >>', this.props.palette) 597 | this.swatchesUpdatedAt = this.state.swatchesUpdatedAt 598 | this.swatches = this.props.palette.map((c, i) => ( 599 | 600 | this.onSwatchPress(c, i)} hitSlop={this.props.swatchesHitSlop}> 601 | 602 | 603 | 604 | )) 605 | } 606 | renderDiscs() { 607 | this.discsUpdatedAt = this.state.discsUpdatedAt 608 | const length = Math.max(this.props.discreteLength, 2) 609 | this.disc = (`1`).repeat(length).split('').map((c, i) => ( 610 | 611 | this.onDiscPress(c, i)} hitSlop={this.props.swatchesHitSlop}> 612 | 613 | = (length-1) ? 1 : (i * 1/(length-1))) }]}> 614 | 615 | 616 | 617 | )).reverse() 618 | this.tryForceUpdate() 619 | } 620 | render() { 621 | const { 622 | style, 623 | thumbSize, 624 | sliderSize, 625 | gapSize, 626 | swatchesLast, 627 | swatchesOnly, 628 | sliderHidden, 629 | discrete, 630 | row, 631 | wheelHidden, 632 | } = this.props 633 | const swatches = !!(this.props.swatches || swatchesOnly) 634 | const hsv = hsv2Hex(this.color), hex = hsv2Hex(this.color.h, this.color.s, 100) 635 | const wheelPanHandlers = this.wheelPanResponder && this.wheelPanResponder.panHandlers || {} 636 | const sliderPanHandlers = this.sliderPanResponder && this.sliderPanResponder.panHandlers || {} 637 | const opacity = this.state.wheelOpacity// * this.state.sliderOpacity 638 | const margin = swatchesOnly ? 0 : gapSize 639 | const wheelThumbStyle = { 640 | width: thumbSize, 641 | height: thumbSize, 642 | borderRadius: thumbSize / 2, 643 | backgroundColor: this.props.shadeWheelThumb === true ? hsv : hex, 644 | transform: [{ translateX: (!!this.props.flipTouchX ? 1 : -1) * thumbSize / 2 }, { translateY: (!!this.props.flipTouchY ? 1 : -1) * thumbSize / 2 }], 645 | [!!this.props.flipTouchX ? 'right' : 'left']: this.panX, 646 | [!!this.props.flipTouchY ? 'bottom' : 'top']: this.panY, 647 | opacity, 648 | //// 649 | // transform: [{translateX:this.panX},{translateY:this.panY}], 650 | // left: -this.props.thumbSize/2, 651 | // top: -this.props.thumbSize/2, 652 | // zIndex: 2, 653 | } 654 | const sliderThumbStyle = { 655 | [!!this.props.flipTouchX ? 'right' : 'left']: row ? 0 : this.slideX, 656 | [!!this.props.flipTouchY ? 'bottom' : 'top']: row ? this.slideY : 0, 657 | // transform: [row?{translateX:8}:{translateY:8}], 658 | backgroundColor: this.props.shadeSliderThumb === true ? hsv : hex, 659 | borderRadius: sliderSize / 2, 660 | height: sliderSize, 661 | width: sliderSize, 662 | opacity, 663 | } 664 | const sliderStyle = { 665 | width: row ? sliderSize : '100%', 666 | height: row ? '100%' : sliderSize, 667 | marginLeft: row ? gapSize : 0, 668 | marginTop: row ? 0 : gapSize, 669 | borderRadius: sliderSize / 2, 670 | } 671 | const swatchStyle = { 672 | flexDirection: row ? 'column' : 'row', 673 | width: row ? 20 : '100%', 674 | height: row ? '100%' : 20, 675 | marginLeft: row ? margin : 0, 676 | marginTop: row ? 0 : margin, 677 | } 678 | const swatchFirstStyle = { 679 | marginTop: 0, 680 | marginLeft: 0, 681 | marginRight: row ? margin : 0, 682 | marginBottom: row ? 0 : margin, 683 | } 684 | if (this.state.swatchesUpdatedAt !== this.swatchesUpdatedAt) { 685 | this.initSwatchAnimatedValues(this.props) 686 | this.renderSwatches() 687 | } 688 | if (this.state.discsUpdatedAt !== this.discsUpdatedAt) { 689 | this.initDiscAnimatedValues(this.props) 690 | this.renderDiscs() 691 | } 692 | // console.log('RENDER >>',row,thumbSize,sliderSize) 693 | return ( 694 | 695 | {swatches && !swatchesLast && {this.swatches}} 696 | {!swatchesOnly && !(wheelHidden && !sliderHidden) && 697 | {this.wheelWidth > 0 && 698 | 699 | 700 | {(this.props.wheelLoadingIndicator ? this.state.wheelImageLoaded : true) && } 701 | { this.wheel = r }}> 702 | {!!this.props.wheelLoadingIndicator && !this.state.wheelImageLoaded && this.props.wheelLoadingIndicator} 703 | 704 | 705 | } 706 | } 707 | {!swatchesOnly && !sliderHidden && (discrete 708 | ? {this.disc} 709 | : 710 | 711 | 712 | 713 | {(this.props.sliderLoadingIndicator ? this.state.sliderImageLoaded : true) && } 714 | { this.slider = r }}> 715 | {!!this.props.sliderLoadingIndicator && !this.state.sliderImageLoaded && this.props.sliderLoadingIndicator} 716 | 717 | 718 | )} 719 | {swatches && swatchesLast && {this.swatches}} 720 | 721 | ) 722 | } 723 | } 724 | 725 | const ss = StyleSheet.create({ 726 | root: { 727 | flex: 1, 728 | flexDirection: 'column', 729 | alignItems: 'center', 730 | justifyContent: 'space-between', 731 | overflow: 'visible', 732 | // aspectRatio: 1, 733 | // backgroundColor: '#ffcccc', 734 | }, 735 | wheel: { 736 | flex: 1, 737 | justifyContent: 'center', 738 | alignItems: 'center', 739 | position: 'relative', 740 | overflow: 'visible', 741 | width: '100%', 742 | minWidth: 200, 743 | minHeight: 200, 744 | // aspectRatio: 1, 745 | // backgroundColor: '#ffccff', 746 | }, 747 | wheelWrap: { 748 | width: '100%', 749 | height: '100%', 750 | // backgroundColor: '#ffffcc', 751 | }, 752 | wheelImg: { 753 | width: '100%', 754 | height: '100%', 755 | // backgroundColor: '#ffffcc', 756 | }, 757 | wheelThumb: { 758 | position: 'absolute', 759 | backgroundColor: '#EEEEEE', 760 | borderWidth: 3, 761 | borderColor: '#EEEEEE', 762 | elevation: 4, 763 | shadowColor: 'rgb(46, 48, 58)', 764 | shadowOffset: { width: 0, height: 2 }, 765 | shadowOpacity: 0.8, 766 | shadowRadius: 2, 767 | }, 768 | cover: { 769 | position: 'absolute', 770 | top: 0, 771 | left: 0, 772 | width: '100%', 773 | height: '100%', 774 | // backgroundColor: '#ccccff88', 775 | alignItems: 'center', 776 | justifyContent: 'center', 777 | }, 778 | slider: { 779 | width: '100%', 780 | // height: 32, 781 | marginTop: 16, 782 | // overflow: 'hidden', 783 | flexDirection: 'column-reverse', 784 | // elevation: 4, 785 | // backgroundColor: '#ccccff', 786 | }, 787 | sliderImg: { 788 | width: '100%', 789 | height: '100%', 790 | }, 791 | sliderThumb: { 792 | position: 'absolute', 793 | borderWidth: 2, 794 | borderColor: '#EEEEEE', 795 | elevation: 4, 796 | // backgroundColor: '#f00', 797 | }, 798 | grad: { 799 | borderRadius: 100, 800 | overflow: "hidden", 801 | height: '100%', 802 | }, 803 | swatches: { 804 | width: '100%', 805 | flexDirection: 'row', 806 | justifyContent: 'space-between', 807 | marginTop: 16, 808 | // padding: 16, 809 | }, 810 | swatch: { 811 | width: 20, 812 | height: 20, 813 | borderRadius: 10, 814 | // borderWidth: 1, 815 | borderColor: '#8884', 816 | alignItems: 'center', 817 | justifyContent: 'center', 818 | overflow: 'visible', 819 | }, 820 | swatchTouch: { 821 | width: 30, 822 | height: 30, 823 | borderRadius: 15, 824 | backgroundColor: '#f004', 825 | overflow: 'hidden', 826 | }, 827 | }) 828 | --------------------------------------------------------------------------------