├── .circleci
└── config.yml
├── .gitignore
├── LICENSE
├── README.md
├── RELEASE_NOTES.md
├── demo.gif
├── package-lock.json
├── package.json
├── patches
└── react-native-gesture-handler+1.10.3.patch
├── src
├── HueSaturationValuePicker.tsx
├── HueSaturationWheel.tsx
├── Slider.tsx
├── ValueSlider.tsx
├── Wheel.tsx
├── colorHSV.ts
├── defaultStyle.ts
├── index.ts
├── size.ts
└── tsconfig.json
└── tslint.json
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/node:15.9.0
6 | working_directory: ~/repo
7 | steps:
8 | - checkout
9 |
10 | - run: npm i && npm run lint && npm run build
11 |
12 | - persist_to_workspace:
13 | root: .
14 | paths:
15 | - dist/*
16 |
17 | publish:
18 | docker:
19 | - image: circleci/node:15.9.0
20 | steps:
21 | - checkout
22 | - attach_workspace:
23 | at: .
24 | - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
25 | - run: npm publish
26 |
27 | workflows:
28 | version: 2
29 | package:
30 | jobs:
31 | - build:
32 | filters:
33 | tags:
34 | only: /.*/
35 | - publish:
36 | context: iyegoroff
37 | requires:
38 | - build
39 | filters:
40 | tags:
41 | only: /^v.*/
42 | branches:
43 | ignore: /.*/
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # parcel-bundler cache (https://parceljs.org/)
61 | .cache
62 |
63 | # next.js build output
64 | .next
65 |
66 | # nuxt.js build output
67 | .nuxt
68 |
69 | # vuepress build output
70 | .vuepress/dist
71 |
72 | # Serverless directories
73 | .serverless
74 |
75 | dist
76 |
77 | .ionide
78 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 iyegoroff
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-reanimated-color-picker
2 | [](https://badge.fury.io/js/react-native-reanimated-color-picker)
3 | [](https://circleci.com/gh/iyegoroff/react-native-reanimated-color-picker)
4 | [](https://github.com/standard/standard)
5 | [](https://david-dm.org/iyegoroff/react-native-reanimated-color-picker)
6 | [](https://david-dm.org/iyegoroff/react-native-reanimated-color-picker?type=dev)
7 | [](package.json)
8 | [](https://www.npmjs.com/package/react-native-reanimated-color-picker)
9 |
10 | Natively animated HSV color picker for iOS & Android
11 |
12 | ## Installation
13 |
14 | - Install peer dependencies: [react-native-image-filter-kit](https://github.com/iyegoroff/react-native-image-filter-kit#react-native-image-filter-kit), [react-native-reanimated](https://github.com/kmagiera/react-native-reanimated#getting-started), [react-native-gesture-handler](https://kmagiera.github.io/react-native-gesture-handler/docs/getting-started.html#installation)
15 |
16 | - `$ npm install react-native-reanimated-color-picker --save`
17 |
18 | ## Demo
19 |
20 |
21 |
22 | ## Example
23 |
24 | ```jsx
25 | import * as React from 'react'
26 | import { HueSaturationValuePicker } from 'react-native-reanimated-color-picker'
27 |
28 | const wheelStyle = { width: '100%' }
29 | const sliderStyle = { height: 50, width: '100%' }
30 |
31 | const colorChanged = ({ h, s, v }) => {
32 | console.warn(h, s, v)
33 | }
34 |
35 | const picker = (
36 |
41 | )
42 | ```
43 |
44 | ## Description
45 |
46 | There are three components:
47 | - `HueSaturationWheel` - a wheel for selecting hue and saturation with constant value = 1
48 | - `ValueSlider` - a slider for selecting value
49 | - `HueSaturationValuePicker` - a composition of two first components
50 |
51 | The library doesn't provide any color conversion functions, so you have to use a third-party module for color conversion
52 |
53 | ## Reference
54 |
55 | ### HueSaturationWheel props
56 |
57 |
58 |
59 | prop |
60 | type |
61 | default |
62 | desc |
63 |
64 |
65 | style |
66 | ViewStyle |
67 | - |
68 | required |
69 |
70 |
71 | snapToCenter |
72 | number |
73 | - |
74 | Thumb will snap to center of the wheel when the distance is less than snapToCenter |
75 |
76 |
77 | onColorChangeComplete |
78 | (color: {
79 | h: number,
80 | s: number,
81 | v: number
82 | }) => void |
83 | - |
84 | Called when touch ended |
85 |
86 |
87 | onColorChange |
88 | (color: {
89 | h: number,
90 | s: number,
91 | v: number
92 | }) => void |
93 | - |
94 | Called when touch moved |
95 |
96 |
97 | value |
98 | Animated.Node<number> |
99 | new Animated.Value(1) |
100 | value node from ValueSlider |
101 |
102 |
103 | valueGestureState |
104 | Animated.Node<number> |
105 | new Animated.Value(State.END) |
106 | ValueSlider gesture state |
107 |
108 |
109 | thumbRadius |
110 | number |
111 | 50 |
112 | |
113 |
114 |
115 | initialHue |
116 | number |
117 | 0 |
118 | hue in degrees |
119 |
120 |
121 | initialSaturation |
122 | number |
123 | 0 |
124 | [0..1] |
125 |
126 |
127 |
128 | ### ValueSlider props
129 |
130 |
131 |
132 |
133 | prop |
134 | type |
135 | default |
136 | desc |
137 |
138 |
139 | style |
140 | ViewStyle |
141 | - |
142 | required |
143 |
144 |
145 | thumbWidth |
146 | number |
147 | 50 |
148 | |
149 |
150 |
151 | initialValue |
152 | number |
153 | 1 |
154 | [0..1] |
155 |
156 |
157 | onValueInit |
158 | (value: Animated.Node<number>,
159 | gestureState: Animated.Node<number>) => void |
160 | - |
161 | used to wire ValudeSlider with HueSaturationWheel |
162 |
163 |
164 |
165 |
166 | ### HueSaturationValuePicker props
167 |
168 |
169 |
170 | prop |
171 | type |
172 | default |
173 | desc |
174 |
175 |
176 | wheelStyle |
177 | ViewStyle |
178 | - |
179 | required |
180 |
181 |
182 | sliderStyle |
183 | ViewStyle |
184 | - |
185 | required |
186 |
187 |
188 | snapToCenter |
189 | number |
190 | - |
191 | Thumb will snap to center of the wheel when the distance is less than snapToCenter |
192 |
193 |
194 | onColorChangeComplete |
195 | (color: {
196 | h: number,
197 | s: number,
198 | v: number
199 | }) => void |
200 | - |
201 | Called when touch ended |
202 |
203 |
204 | onColorChange |
205 | (color: {
206 | h: number,
207 | s: number,
208 | v: number
209 | }) => void |
210 | - |
211 | Called when touch moved |
212 |
213 |
214 | thumbSize |
215 | number |
216 | 50 |
217 | thumbRadius and thumbWidth |
218 |
219 |
220 | initialHue |
221 | number |
222 | 0 |
223 | hue in degrees |
224 |
225 |
226 | initialSaturation |
227 | number |
228 | 0 |
229 | [0..1] |
230 |
231 |
232 | initialValue |
233 | number |
234 | 1 |
235 | [0..1] |
236 |
237 |
238 |
239 | ## Credits
240 |
241 | - `colorHSV` function was taken from `react-native-reanimated` [example](https://github.com/kmagiera/react-native-reanimated/blob/master/Example/colors/index.js) by @kmagiera
242 |
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
1 | [release notes](https://github.com/iyegoroff/react-native-reanimated-color-picker/releases)
2 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iyegoroff/react-native-reanimated-color-picker/424bb160f3e02f02a11a598e78f9aa607ed48e32/demo.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-reanimated-color-picker",
3 | "version": "0.0.11",
4 | "description": "Natively animated HSV color picker for iOS & Android",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "preversion": "npm run build && npm run lint",
9 | "postversion": "git push && git push --tags",
10 | "build": "patch-package && rm -rf dist && mkdir dist && tsc -p src",
11 | "lint": "tslint -p src",
12 | "watch": "npm run build -- --watch"
13 | },
14 | "keywords": [
15 | "react-native",
16 | "reanimated",
17 | "color-picker"
18 | ],
19 | "files": [
20 | "dist",
21 | "src",
22 | "*.md",
23 | "LICENSE"
24 | ],
25 | "author": "iyegoroff ",
26 | "license": "MIT",
27 | "peerDependencies": {
28 | "react": "*",
29 | "react-native": "*",
30 | "react-native-gesture-handler": "*",
31 | "react-native-image-filter-kit": "*",
32 | "react-native-reanimated": "*"
33 | },
34 | "devDependencies": {
35 | "@types/react": "^17.0.6",
36 | "@types/react-native": "^0.64.5",
37 | "patch-package": "^6.4.7",
38 | "react-native-gesture-handler": "^1.10.3",
39 | "react-native-image-filter-kit": "^0.8.0",
40 | "react-native-reanimated": "^2.1.0",
41 | "tslint": "^6.1.3",
42 | "tslint-config-standard": "^9.0.0",
43 | "tslint-react": "^5.0.0",
44 | "typescript": "^4.2.4"
45 | },
46 | "bugs": {
47 | "url": "https://github.com/iyegoroff/react-native-reanimated-color-picker/issues"
48 | },
49 | "homepage": "https://github.com/iyegoroff/react-native-reanimated-color-picker#readme",
50 | "repository": {
51 | "type": "git",
52 | "url": "git+https://github.com/iyegoroff/react-native-reanimated-color-picker.git"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/patches/react-native-gesture-handler+1.10.3.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/react-native-gesture-handler/lib/typescript/components/touchables/GenericTouchable.d.ts b/node_modules/react-native-gesture-handler/lib/typescript/components/touchables/GenericTouchable.d.ts
2 | index 343288f..6c843e7 100644
3 | --- a/node_modules/react-native-gesture-handler/lib/typescript/components/touchables/GenericTouchable.d.ts
4 | +++ b/node_modules/react-native-gesture-handler/lib/typescript/components/touchables/GenericTouchable.d.ts
5 | @@ -1,4 +1,3 @@
6 | -///
7 | import { Component } from 'react';
8 | import { StyleProp, ViewStyle, TouchableWithoutFeedbackProps } from 'react-native';
9 | import { GestureEvent, HandlerStateChangeEvent } from '../../handlers/gestureHandlers';
10 |
--------------------------------------------------------------------------------
/src/HueSaturationValuePicker.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ViewStyle, StyleProp } from 'react-native'
3 | import Animated from 'react-native-reanimated'
4 | import { State as GestureState } from 'react-native-gesture-handler'
5 | import { HSV, HueSaturationWheel } from './HueSaturationWheel'
6 | import { ValueSlider } from './ValueSlider'
7 |
8 | type Props = {
9 | readonly wheelStyle: StyleProp
10 | readonly sliderStyle: StyleProp
11 | readonly snapToCenter?: number
12 | readonly onColorChangeComplete?: (color: HSV) => void
13 | readonly onColorChange?: (color: HSV) => void
14 | readonly thumbSize?: number
15 | readonly initialHue?: number
16 | readonly initialSaturation?: number
17 | readonly initialValue?: number
18 | }
19 |
20 | type State = {
21 | readonly value: Animated.Node
22 | readonly valueGestureState: Animated.Node
23 | }
24 |
25 | export class HueSaturationValuePicker extends React.PureComponent<
26 | Props,
27 | State
28 | > {
29 | state: State = {
30 | value: new Animated.Value(1),
31 | valueGestureState: new Animated.Value(GestureState.UNDETERMINED)
32 | }
33 |
34 | render() {
35 | const {
36 | wheelStyle,
37 | sliderStyle,
38 | thumbSize,
39 | snapToCenter,
40 | initialHue,
41 | initialValue,
42 | initialSaturation,
43 | onColorChange,
44 | onColorChangeComplete
45 | } = this.props
46 | const { value, valueGestureState } = this.state
47 |
48 | return (
49 | <>
50 |
61 |
67 | >
68 | )
69 | }
70 |
71 | private valueInit = (
72 | value: Animated.Node,
73 | valueGestureState: Animated.Node
74 | ) => {
75 | this.setState({ value, valueGestureState })
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/HueSaturationWheel.tsx:
--------------------------------------------------------------------------------
1 | // tslint:disable:max-file-line-count
2 | import React from 'react'
3 | import { View, ViewStyle, StyleSheet, LayoutChangeEvent, StyleProp } from 'react-native'
4 | import {
5 | State as GestureState,
6 | PanGestureHandlerGestureEvent,
7 | TapGestureHandlerGestureEvent
8 | } from 'react-native-gesture-handler'
9 | import Animated from 'react-native-reanimated'
10 | import { Wheel } from './Wheel'
11 | import { colorHSV } from './colorHSV'
12 | import { defaultStyle } from './defaultStyle'
13 |
14 | const {
15 | and,
16 | neq,
17 | set,
18 | block,
19 | Value,
20 | multiply,
21 | divide,
22 | sqrt,
23 | pow,
24 | sub,
25 | add,
26 | greaterThan,
27 | cond,
28 | event,
29 | interpolateNode,
30 | Extrapolate,
31 | lessThan,
32 | acos,
33 | eq,
34 | lessOrEq
35 | } = Animated
36 |
37 | export type HSV = {
38 | readonly h: number
39 | readonly s: number
40 | readonly v: number
41 | }
42 |
43 | type Props = {
44 | readonly style: StyleProp,
45 | readonly snapToCenter?: number
46 | readonly onColorChangeComplete?: (color: HSV) => void
47 | readonly onColorChange?: (color: HSV) => void
48 | readonly value?: Animated.Node
49 | readonly valueGestureState?: Animated.Node
50 | readonly thumbRadius?: number
51 | readonly initialHue?: number
52 | readonly initialSaturation?: number
53 | }
54 |
55 | type State = {
56 | readonly hue?: Animated.Node
57 | readonly saturation?: Animated.Node
58 | readonly side?: number
59 | readonly posX?: Animated.Node
60 | readonly posY?: Animated.Node
61 | readonly startX?: Animated.Node
62 | readonly startY?: Animated.Node
63 | readonly translateX?: Animated.Node
64 | readonly translateY?: Animated.Node
65 | readonly thumbColor?: Animated.Node
66 | readonly wheelOpacity?: Animated.Node
67 | readonly panGestureEvent?: (
68 | event: PanGestureHandlerGestureEvent | TapGestureHandlerGestureEvent
69 | ) => void
70 | readonly gestureState?: Animated.Node
71 | readonly codeKey?: number
72 | }
73 |
74 | export class HueSaturationWheel extends React.PureComponent {
75 |
76 | static defaultProps: Partial = {
77 | value: new Value(1),
78 | valueGestureState: new Animated.Value(GestureState.UNDETERMINED),
79 | thumbRadius: 50
80 | }
81 |
82 | constructor(props: Props) {
83 | super(props)
84 |
85 | const { style, snapToCenter, value, thumbRadius, initialHue, initialSaturation } = props
86 | const side = HueSaturationWheel.side(style)
87 |
88 | this.state = side === undefined || thumbRadius === undefined
89 | ? {}
90 | : (
91 | HueSaturationWheel.state(
92 | side,
93 | thumbRadius,
94 | value!,
95 | 0,
96 | snapToCenter,
97 | initialHue,
98 | initialSaturation
99 | )
100 | )
101 | }
102 |
103 | static isGestureStartedInsideCircle(
104 | gestureState: Animated.Node,
105 | startX: Animated.Node,
106 | startY: Animated.Node,
107 | thumbSize: number,
108 | side: number
109 | ) {
110 | const halfThumbSize = thumbSize / 2
111 | const center = HueSaturationWheel.center(side, thumbSize)
112 | const radius = center
113 |
114 | return (
115 | and(
116 | neq(gestureState, GestureState.UNDETERMINED),
117 | neq(gestureState, GestureState.FAILED),
118 | lessOrEq(
119 | sqrt(
120 | add(
121 | pow(sub(startX, center + halfThumbSize), 2),
122 | pow(sub(startY, center + halfThumbSize), 2)
123 | )
124 | ),
125 | radius + thumbSize / 1.5
126 | )
127 | )
128 | )
129 | }
130 |
131 | private static center(side: number, thumbSize: number) {
132 | return (side - thumbSize) / 2
133 | }
134 |
135 | private static toCartesian(degree: number, radius: number) {
136 | const rad = degree / 180 * Math.PI
137 |
138 | return {
139 | x: Math.cos(rad) * radius,
140 | y: Math.sin(rad) * radius
141 | }
142 | }
143 |
144 | private static state(
145 | side: number,
146 | thumbRadius: number,
147 | value: Animated.Node,
148 | codeKey: number = 0,
149 | snapToCenter: number = 0,
150 | initialHue: number = 0,
151 | initialSaturation: number = 0
152 | ): Required {
153 | const halfThumbSize = thumbRadius / 2
154 | const center = HueSaturationWheel.center(side, thumbRadius)
155 | const radius = center
156 | const startX = new Value(0)
157 | const startY = new Value(0)
158 | const cartesian = HueSaturationWheel.toCartesian(-initialHue, initialSaturation * radius)
159 | const posX = new Value(center + cartesian.x)
160 | const posY = new Value(center + cartesian.y)
161 | const gestureState = new Value(GestureState.UNDETERMINED)
162 | const distance = sqrt(add(pow(sub(posX, center), 2), pow(sub(posY, center), 2)))
163 | const shouldSnapToCenter = lessThan(distance, snapToCenter)
164 | const dist = cond(shouldSnapToCenter, 0, distance)
165 | const isOutsideWheel = greaterThan(dist, radius)
166 | const isInCenter = eq(dist, 0)
167 | const sinT = cond(isInCenter, 0, divide(sub(posY, center), dist))
168 | const cosT = cond(isInCenter, 1, divide(sub(posX, center), dist))
169 |
170 | const translateX: Animated.Node = cond(
171 | shouldSnapToCenter,
172 | center,
173 | cond(isOutsideWheel, add(center, multiply(radius, cosT)), posX)
174 | )
175 |
176 | const translateY: Animated.Node = cond(
177 | shouldSnapToCenter,
178 | center,
179 | cond(isOutsideWheel, add(center, multiply(radius, sinT)), posY)
180 | )
181 |
182 | const angle = divide(multiply(acos(cosT), 180), Math.PI)
183 | const hue = cond(
184 | greaterThan(sub(translateY, center), 0),
185 | sub(360, angle),
186 | angle
187 | )
188 |
189 | const saturation = interpolateNode(dist, {
190 | inputRange: [0, radius],
191 | outputRange: [0, 1],
192 | extrapolate: Extrapolate.CLAMP
193 | })
194 |
195 | return {
196 | codeKey: codeKey + 1,
197 | wheelOpacity: sub(1, value),
198 | gestureState,
199 | side,
200 | translateX,
201 | translateY,
202 | posX,
203 | posY,
204 | startX,
205 | startY,
206 | hue,
207 | saturation,
208 | // tslint:disable-next-line: no-any
209 | thumbColor: colorHSV(hue, saturation, value) as any,
210 | panGestureEvent: event([{
211 | nativeEvent: ({ x, y, state }: { x: number, y: number, state: GestureState }) => (
212 | block([
213 | set(gestureState, state),
214 | cond(
215 | eq(gestureState, GestureState.BEGAN),
216 | [
217 | set(startX, x),
218 | set(startY, y)
219 | ]
220 | ),
221 | cond(
222 | HueSaturationWheel.isGestureStartedInsideCircle(
223 | gestureState,
224 | startX,
225 | startY,
226 | thumbRadius,
227 | side
228 | ),
229 | [
230 | set(posX, sub(x, halfThumbSize)),
231 | set(posY, sub(y, halfThumbSize))
232 | ]
233 | )
234 | ])
235 | )
236 | }])
237 | }
238 | }
239 |
240 | private static side(style?: StyleProp): number | undefined {
241 | if (style !== undefined) {
242 | const { width, height } = StyleSheet.flatten(style)
243 |
244 | if (typeof width === 'number' && typeof height === 'number') {
245 | return Math.min(width, height)
246 | }
247 |
248 | if (typeof width === 'number') {
249 | return width
250 | }
251 |
252 | if (typeof height === 'number') {
253 | return height
254 | }
255 | }
256 |
257 | return undefined
258 | }
259 |
260 | componentDidUpdate(prevProps: Props, prevState: State) {
261 | const { style, snapToCenter, value, thumbRadius, initialHue, initialSaturation } = this.props
262 | const { width, height } = StyleSheet.flatten(style)
263 | const { width: prevWidth, height: prevHeight } = StyleSheet.flatten(prevProps.style)
264 |
265 | if (prevHeight !== height || prevWidth !== width) {
266 | this.setState({ side: HueSaturationWheel.side(style) })
267 |
268 | } else {
269 | const { side = HueSaturationWheel.side(style) } = this.state
270 |
271 | if ((
272 | side !== undefined && thumbRadius !== undefined
273 | ) && (
274 | side !== prevState.side ||
275 | value !== prevProps.value ||
276 | snapToCenter !== prevProps.snapToCenter ||
277 | thumbRadius !== prevProps.thumbRadius
278 | )) {
279 | this.setState(HueSaturationWheel.state(
280 | side,
281 | thumbRadius,
282 | value!,
283 | prevState.codeKey,
284 | snapToCenter,
285 | initialHue,
286 | initialSaturation
287 | ))
288 | }
289 | }
290 | }
291 |
292 | render() {
293 | const { style, valueGestureState, thumbRadius, value } = this.props
294 | const {
295 | side,
296 | codeKey,
297 | thumbColor,
298 | translateX,
299 | translateY,
300 | wheelOpacity,
301 | panGestureEvent,
302 | gestureState,
303 | startX,
304 | startY,
305 | hue,
306 | saturation
307 | } = this.state
308 |
309 | const containerStyle = side === undefined ? {} : { width: side, height: side }
310 |
311 | return (
312 |
317 | {(
318 | side !== undefined &&
319 | thumbColor !== undefined &&
320 | translateX !== undefined &&
321 | translateY !== undefined &&
322 | wheelOpacity !== undefined &&
323 | panGestureEvent !== undefined &&
324 | gestureState !== undefined &&
325 | codeKey !== undefined &&
326 | startX !== undefined &&
327 | startY !== undefined &&
328 | valueGestureState !== undefined &&
329 | thumbRadius !== undefined &&
330 | hue !== undefined &&
331 | saturation !== undefined &&
332 | value !== undefined
333 | ) ? (
334 |
353 | )
354 | : false
355 | }
356 |
357 | )
358 | }
359 |
360 | private colorChangeComplete = ([h, s, v]: readonly number[]) => {
361 | const { onColorChangeComplete = noop } = this.props
362 |
363 | onColorChangeComplete({ h, s, v })
364 | }
365 |
366 | private colorChange = ([h, s, v]: readonly number[]) => {
367 | const { onColorChange = noop } = this.props
368 |
369 | onColorChange({ h, s, v })
370 | }
371 |
372 | private layout = ({ nativeEvent: { layout: { width, height } } }: LayoutChangeEvent) => {
373 | this.setState({ side: Math.min(width, height) })
374 | }
375 | }
376 |
377 | const noop = () => { /* */ }
378 |
--------------------------------------------------------------------------------
/src/Slider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Animated from 'react-native-reanimated'
3 | import {
4 | PanGestureHandler,
5 | TapGestureHandler,
6 | PanGestureHandlerGestureEvent,
7 | TapGestureHandlerGestureEvent
8 | } from 'react-native-gesture-handler'
9 | import { View, ViewStyle, StyleSheet } from 'react-native'
10 | import { LinearGradient, ImagePlaceholder } from 'react-native-image-filter-kit'
11 |
12 | type Props = {
13 | readonly width: number
14 | readonly height: number
15 | readonly thumbWidth: number
16 | readonly translate: Animated.Node
17 | readonly panGestureEvent: (
18 | event: PanGestureHandlerGestureEvent | TapGestureHandlerGestureEvent
19 | ) => void
20 | readonly thumbColor: Animated.Node
21 | }
22 |
23 | type GradientProps = React.ComponentProps
24 |
25 | const colors: GradientProps['colors'] = ['#000000FF', '#00000000']
26 |
27 | export const Slider = React.memo((props: Props) => {
28 | const { panGestureEvent, translate, thumbColor, width, height, thumbWidth } =
29 | props
30 |
31 | const imageWidth = width - thumbWidth
32 | const imageStyle = { width: imageWidth, height, borderRadius: 5 }
33 | const containerStyle = { width, height }
34 | const offset = -thumbWidth / 2
35 |
36 | return (
37 |
38 |
39 |
44 |
45 |
46 | }
49 | />
50 |
65 |
66 |
67 |
68 |
69 |
70 | )
71 | })
72 |
73 | type Styles = {
74 | readonly container: ViewStyle
75 | readonly slider: ViewStyle
76 | readonly thumb: ViewStyle
77 | }
78 |
79 | const styles = StyleSheet.create({
80 | container: {
81 | justifyContent: 'center',
82 | alignItems: 'center'
83 | },
84 | slider: {
85 | width: '100%',
86 | height: '100%',
87 | backgroundColor: 'white'
88 | },
89 | thumb: {
90 | position: 'absolute',
91 | borderColor: 'white',
92 | borderWidth: 2,
93 | borderRadius: 5
94 | }
95 | })
96 |
--------------------------------------------------------------------------------
/src/ValueSlider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Animated from 'react-native-reanimated'
3 | import {
4 | View,
5 | ViewStyle,
6 | StyleSheet,
7 | LayoutChangeEvent,
8 | StyleProp
9 | } from 'react-native'
10 | import {
11 | PanGestureHandlerGestureEvent,
12 | TapGestureHandlerGestureEvent,
13 | State as GestureState
14 | } from 'react-native-gesture-handler'
15 | import { Slider } from './Slider'
16 | import { Size } from './size'
17 | import { defaultStyle } from './defaultStyle'
18 |
19 | const {
20 | Value,
21 | lessThan,
22 | greaterThan,
23 | cond,
24 | divide,
25 | color,
26 | event,
27 | block,
28 | set,
29 | and,
30 | neq,
31 | multiply,
32 | round,
33 | sub
34 | } = Animated
35 |
36 | type Props = {
37 | readonly style: StyleProp
38 | readonly initialValue?: number
39 | readonly onValueInit: (
40 | value: Animated.Node,
41 | gestureState: Animated.Node
42 | ) => void
43 | readonly thumbWidth?: number
44 | }
45 |
46 | type State = {
47 | readonly width?: number
48 | readonly height?: number
49 | readonly pos?: Animated.Node
50 | readonly translate?: Animated.Node
51 | readonly value?: Animated.Node
52 | readonly thumbColor?: Animated.Node
53 | readonly panGestureEvent?: (
54 | event: PanGestureHandlerGestureEvent | TapGestureHandlerGestureEvent
55 | ) => void
56 | readonly gestureState?: Animated.Node
57 | readonly codeKey?: number
58 | }
59 |
60 | export class ValueSlider extends React.PureComponent {
61 | static defaultProps: Partial = {
62 | thumbWidth: 50
63 | }
64 |
65 | constructor(props: Props) {
66 | super(props)
67 |
68 | const { style, thumbWidth, initialValue } = props
69 | const { width, height } = ValueSlider.size(style)
70 |
71 | this.state =
72 | width === undefined || height === undefined || thumbWidth === undefined
73 | ? {}
74 | : ValueSlider.state(width, height, thumbWidth, 0, initialValue)
75 | }
76 |
77 | private static state(
78 | width: number,
79 | height: number,
80 | thumbWidth: number,
81 | codeKey: number = 0,
82 | initialValue: number = 1
83 | ): Required {
84 | const size = width - thumbWidth
85 | const pos = new Value(size * initialValue)
86 | const gestureState = new Value(GestureState.UNDETERMINED)
87 |
88 | const translate: Animated.Node = cond(
89 | greaterThan(pos, size),
90 | size,
91 | cond(lessThan(pos, 0), 0, pos)
92 | )
93 |
94 | const value = divide(translate, size)
95 | const col = round(multiply(value, 255))
96 |
97 | return {
98 | codeKey: codeKey + 1,
99 | width,
100 | height,
101 | gestureState,
102 | pos,
103 | translate,
104 | value,
105 | // tslint:disable-next-line: no-any
106 | thumbColor: color(col, col, col) as any,
107 | panGestureEvent: event([
108 | {
109 | nativeEvent: ({
110 | x,
111 | state
112 | }: {
113 | x: number
114 | y: number
115 | state: GestureState
116 | }) =>
117 | block([
118 | set(gestureState, state),
119 | cond(
120 | and(
121 | neq(gestureState, GestureState.UNDETERMINED),
122 | neq(gestureState, GestureState.FAILED)
123 | ),
124 | set(pos, sub(x, thumbWidth / 2))
125 | )
126 | ])
127 | }
128 | ])
129 | }
130 | }
131 |
132 | private static size(style?: StyleProp): Size {
133 | if (style !== undefined) {
134 | const { width, height } = StyleSheet.flatten(style)
135 |
136 | if (typeof width === 'number' && typeof height === 'number') {
137 | return { width, height }
138 | }
139 | }
140 |
141 | return { width: undefined, height: undefined }
142 | }
143 |
144 | componentDidMount() {
145 | const { width, height, value } = this.state
146 |
147 | if (width !== undefined && height !== undefined && value !== undefined) {
148 | this.props.onValueInit(value, new Animated.Value(GestureState.END))
149 | }
150 | }
151 |
152 | componentDidUpdate(prevProps: Props, prevState: State) {
153 | const { style, onValueInit, thumbWidth, initialValue } = this.props
154 | const { width, height } = StyleSheet.flatten(style)
155 | const { width: prevWidth, height: prevHeight } = StyleSheet.flatten(
156 | prevProps.style
157 | )
158 | const size = ValueSlider.size(style)
159 |
160 | if (prevHeight !== height || prevWidth !== width) {
161 | this.setState(size)
162 | } else {
163 | const { width: curWidth = size.width, height: curHeight = size.height } =
164 | this.state
165 |
166 | if (
167 | curWidth !== undefined &&
168 | curHeight !== undefined &&
169 | thumbWidth !== undefined &&
170 | (curWidth !== prevState.width ||
171 | curHeight !== prevState.height ||
172 | thumbWidth !== prevProps.thumbWidth)
173 | ) {
174 | const state = ValueSlider.state(
175 | curWidth,
176 | curHeight,
177 | thumbWidth,
178 | prevState.codeKey,
179 | initialValue
180 | )
181 |
182 | onValueInit(state.value, state.gestureState)
183 | this.setState(state)
184 | }
185 | }
186 | }
187 |
188 | render() {
189 | const { width, height, translate, thumbColor, panGestureEvent, codeKey } =
190 | this.state
191 | const { style, thumbWidth } = this.props
192 |
193 | return (
194 |
199 | {width !== undefined &&
200 | height !== undefined &&
201 | thumbColor !== undefined &&
202 | translate !== undefined &&
203 | panGestureEvent !== undefined &&
204 | thumbWidth !== undefined ? (
205 |
213 | ) : (
214 | false
215 | )}
216 |
217 | )
218 | }
219 |
220 | private layout = ({
221 | nativeEvent: {
222 | layout: { width, height }
223 | }
224 | }: LayoutChangeEvent) => {
225 | this.setState({ width, height })
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/Wheel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Animated from 'react-native-reanimated'
3 | import {
4 | TapGestureHandlerGestureEvent,
5 | PanGestureHandlerGestureEvent,
6 | TapGestureHandler,
7 | PanGestureHandler,
8 | State as GestureState
9 | } from 'react-native-gesture-handler'
10 | import { View, ViewStyle, StyleSheet } from 'react-native'
11 | import {
12 | SrcInComposition,
13 | SweepGradient,
14 | ImagePlaceholder,
15 | RadialGradient
16 | } from 'react-native-image-filter-kit'
17 | import { HueSaturationWheel } from './HueSaturationWheel'
18 |
19 | const { call, cond, eq, and, or, set } = Animated
20 |
21 | type Props = {
22 | readonly hue: Animated.Node
23 | readonly saturation: Animated.Node
24 | readonly value: Animated.Node
25 | readonly side: number
26 | readonly thumbSize: number
27 | readonly translateX: Animated.Node
28 | readonly translateY: Animated.Node
29 | readonly startX: Animated.Node
30 | readonly startY: Animated.Node
31 | readonly thumbColor: Animated.Node
32 | readonly wheelOpacity: Animated.Node
33 | readonly panGestureEvent: (
34 | event: PanGestureHandlerGestureEvent | TapGestureHandlerGestureEvent
35 | ) => void
36 | readonly gestureState: Animated.Node
37 | readonly valueGestureState: Animated.Node
38 | readonly codeKey: number
39 | readonly onColorChangeComplete?: ([
40 | hue,
41 | saturation,
42 | value
43 | ]: readonly number[]) => void
44 | readonly onColorChange?: ([hue, saturation, value]: readonly number[]) => void
45 | }
46 |
47 | type GradientProps = React.ComponentProps
48 |
49 | const colors: GradientProps['colors'] = [
50 | '#FF0000',
51 | '#FFFF00',
52 | '#00FF00',
53 | '#00FFFF',
54 | '#0000FF',
55 | '#FF00FF',
56 | '#FF0000'
57 | ]
58 | const stops: GradientProps['stops'] = [0, 0.165, 0.33, 0.495, 0.66, 0.825, 1]
59 | const radialColors: GradientProps['colors'] = ['#00000000', '#000000FF']
60 |
61 | export const Wheel = React.memo((props: Props) => {
62 | const {
63 | side,
64 | thumbSize,
65 | translateX,
66 | translateY,
67 | thumbColor,
68 | wheelOpacity,
69 | panGestureEvent,
70 | onColorChange,
71 | onColorChangeComplete,
72 | codeKey,
73 | gestureState,
74 | startX,
75 | startY,
76 | valueGestureState,
77 | hue,
78 | saturation,
79 | value
80 | } = props
81 |
82 | const thumbOffset = -thumbSize / 2
83 | const imageSide = side - thumbSize
84 | const containerStyle = { width: side, height: side }
85 | const imageStyle = {
86 | width: imageSide,
87 | height: imageSide,
88 | borderRadius: imageSide / 2
89 | }
90 | const thumbStyle = {
91 | width: thumbSize,
92 | height: thumbSize,
93 | borderRadius: thumbSize / 2
94 | }
95 |
96 | return (
97 |
98 |
99 |
104 |
105 |
106 | }
112 | />
113 | }
114 | dstImage={
115 | }
118 | />
119 | }
120 | />
121 |
128 |
144 | {onColorChangeComplete !== undefined ? (
145 | (
148 | and(
149 | or(
150 | eq(gestureState, GestureState.END),
151 | eq(gestureState, GestureState.UNDETERMINED)
152 | ),
153 | or(
154 | eq(valueGestureState, GestureState.END),
155 | eq(valueGestureState, GestureState.UNDETERMINED)
156 | ),
157 | or(
158 | HueSaturationWheel.isGestureStartedInsideCircle(
159 | gestureState,
160 | startX,
161 | startY,
162 | thumbSize,
163 | side
164 | ),
165 | eq(valueGestureState, GestureState.END)
166 | )
167 | ),
168 | [
169 | set(
170 | valueGestureState as Animated.Value,
171 | GestureState.UNDETERMINED
172 | ),
173 | call([hue, saturation, value], onColorChangeComplete)
174 | ]
175 | )}
176 | />
177 | ) : (
178 | false
179 | )}
180 | {onColorChange !== undefined ? (
181 |
198 | ) : (
199 | false
200 | )}
201 |
202 |
203 |
204 |
205 |
206 | )
207 | })
208 |
209 | type Styles = {
210 | readonly container: ViewStyle
211 | readonly thumb: ViewStyle
212 | readonly wheel: ViewStyle
213 | readonly wheelOverlay: ViewStyle
214 | }
215 |
216 | const styles = StyleSheet.create({
217 | container: {
218 | justifyContent: 'center',
219 | alignItems: 'center'
220 | },
221 | wheel: {
222 | width: '100%',
223 | height: '100%',
224 | backgroundColor: 'white'
225 | },
226 | wheelOverlay: {
227 | position: 'absolute',
228 | backgroundColor: 'black'
229 | },
230 | thumb: {
231 | position: 'absolute',
232 | borderColor: 'white',
233 | borderWidth: 2
234 | }
235 | })
236 |
--------------------------------------------------------------------------------
/src/colorHSV.ts:
--------------------------------------------------------------------------------
1 | import Animated from 'react-native-reanimated'
2 |
3 | const {
4 | multiply,
5 | divide,
6 | abs,
7 | sub,
8 | modulo,
9 | color,
10 | round,
11 | add,
12 | cond,
13 | lessThan,
14 | proc
15 | } = Animated
16 |
17 | export const colorHSV = proc(
18 | (
19 | h: Animated.Adaptable,
20 | s: Animated.Adaptable,
21 | v: Animated.Adaptable
22 | ): Animated.Node => {
23 | // Converts color from HSV format into RGB
24 | // Formula explained here: https://www.rapidtables.com/convert/color/hsv-to-rgb.html
25 | const c = multiply(v, s)
26 | const hh = divide(h, 60)
27 | const x = multiply(c, sub(1, abs(sub(modulo(hh, 2), 1))))
28 |
29 | const m = sub(v, c)
30 |
31 | const colorRGB = (
32 | r: Animated.Adaptable,
33 | g: Animated.Adaptable,
34 | b: Animated.Adaptable
35 | ) =>
36 | color(
37 | round(multiply(255, add(r, m))),
38 | round(multiply(255, add(g, m))),
39 | round(multiply(255, add(b, m)))
40 | )
41 |
42 | // @ts-expect-error
43 | return cond(
44 | lessThan(h, 60),
45 | colorRGB(c, x, 0),
46 | cond(
47 | lessThan(h, 120),
48 | colorRGB(x, c, 0),
49 | cond(
50 | lessThan(h, 180),
51 | colorRGB(0, c, x),
52 | cond(
53 | lessThan(h, 240),
54 | colorRGB(0, x, c),
55 | cond(lessThan(h, 300), colorRGB(x, 0, c), colorRGB(c, 0, x))
56 | )
57 | )
58 | )
59 | )
60 | }
61 | )
62 |
--------------------------------------------------------------------------------
/src/defaultStyle.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle, StyleSheet } from 'react-native'
2 |
3 | type Styles = {
4 | readonly container: ViewStyle
5 | }
6 |
7 | export const defaultStyle = StyleSheet.create({
8 | container: {
9 | width: '100%',
10 | height: '100%'
11 | }
12 | })
13 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { HueSaturationValuePicker } from './HueSaturationValuePicker'
2 | export { HueSaturationWheel, HSV } from './HueSaturationWheel'
3 | export { ValueSlider } from './ValueSlider'
4 |
--------------------------------------------------------------------------------
/src/size.ts:
--------------------------------------------------------------------------------
1 | export type Size = {
2 | readonly width: number
3 | readonly height: number
4 | } | {
5 | readonly width: undefined
6 | readonly height: undefined
7 | }
8 |
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "target": "es6",
5 | "strict": true,
6 | "outDir": "../dist",
7 | "jsx": "react-native",
8 | "lib": ["es6"],
9 | "moduleResolution": "node",
10 | "allowSyntheticDefaultImports": true,
11 | "declaration": true,
12 | "types": ["react-native"]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-config-standard", "tslint-react"],
3 | "rules": {
4 | "strict-boolean-expressions": true,
5 | "jsx-no-multiline-js": false,
6 | "jsx-space-before-trailing-slash": true,
7 | "no-any": true,
8 | "no-parameter-reassignment": true,
9 | "no-shadowed-variable": true,
10 | "max-line-length": [true, 100],
11 | "max-file-line-count": [true, 300],
12 | "ter-indent": [true, 2],
13 | "space-before-function-paren": [true, {
14 | "anonymous": "always",
15 | "named": "never",
16 | "asyncArrow": "always"
17 | }]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------