@react-stately/color 3.0.0-alpha.0 → 3.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Color.ts CHANGED
@@ -10,8 +10,8 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {clamp} from '@react-aria/utils';
14
- import {ColorChannel, ColorChannelRange, ColorFormat, Color as IColor} from '@react-types/color';
13
+ import {clamp, toFixedNumber} from '@react-stately/utils';
14
+ import {ColorAxes, ColorChannel, ColorChannelRange, ColorFormat, Color as IColor} from '@react-types/color';
15
15
  // @ts-ignore
16
16
  import intlMessages from '../intl/*.json';
17
17
  import {MessageDictionary} from '@internationalized/message';
@@ -19,6 +19,7 @@ import {NumberFormatter} from '@internationalized/number';
19
19
 
20
20
  const messages = new MessageDictionary(intlMessages);
21
21
 
22
+ /** Parses a color from a string value. Throws an error if the string could not be parsed. */
22
23
  export function parseColor(value: string): IColor {
23
24
  let res = RGBColor.parse(value) || HSBColor.parse(value) || HSLColor.parse(value);
24
25
  if (res) {
@@ -28,10 +29,18 @@ export function parseColor(value: string): IColor {
28
29
  throw new Error('Invalid color value: ' + value);
29
30
  }
30
31
 
32
+ export function normalizeColor(v: string | IColor) {
33
+ if (typeof v === 'string') {
34
+ return parseColor(v);
35
+ } else {
36
+ return v;
37
+ }
38
+ }
39
+
31
40
  abstract class Color implements IColor {
32
41
  abstract toFormat(format: ColorFormat): IColor;
33
42
  abstract toString(format: ColorFormat | 'css'): string;
34
- abstract clone(): Color;
43
+ abstract clone(): IColor;
35
44
  abstract getChannelRange(channel: ColorChannel): ColorChannelRange;
36
45
  abstract formatChannelValue(channel: ColorChannel, locale: string): string;
37
46
 
@@ -60,6 +69,17 @@ abstract class Color implements IColor {
60
69
  getChannelName(channel: ColorChannel, locale: string) {
61
70
  return messages.getStringForLocale(channel, locale);
62
71
  }
72
+
73
+ abstract getColorSpace(): ColorFormat
74
+ getColorSpaceAxes(xyChannels: {xChannel?: ColorChannel, yChannel?: ColorChannel}): ColorAxes {
75
+ let {xChannel, yChannel} = xyChannels;
76
+ let xCh = xChannel || this.getColorChannels().find(c => c !== yChannel);
77
+ let yCh = yChannel || this.getColorChannels().find(c => c !== xCh);
78
+ let zCh = this.getColorChannels().find(c => c !== xCh && c !== yCh);
79
+
80
+ return {xChannel: xCh, yChannel: yCh, zChannel: zCh};
81
+ }
82
+ abstract getColorChannels(): [ColorChannel, ColorChannel, ColorChannel]
63
83
  }
64
84
 
65
85
  const HEX_REGEX = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i;
@@ -89,7 +109,9 @@ class RGBColor extends Color {
89
109
  let b = parseInt(m[2][4] + m[2][5], 16);
90
110
  return new RGBColor(r, g, b, 1);
91
111
  }
92
- } if ((m = value.match(RGB_REGEX))) {
112
+ }
113
+
114
+ if ((m = value.match(RGB_REGEX))) {
93
115
  const [r, g, b, a] = (m[1] ?? m[2]).split(',').map(n => Number(n.trim()));
94
116
  return new RGBColor(clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), clamp(a ?? 1, 0, 1));
95
117
  }
@@ -118,6 +140,12 @@ class RGBColor extends Color {
118
140
  case 'rgb':
119
141
  case 'rgba':
120
142
  return this;
143
+ case 'hsb':
144
+ case 'hsba':
145
+ return this.toHSB();
146
+ case 'hsl':
147
+ case 'hsla':
148
+ return this.toHSL();
121
149
  default:
122
150
  throw new Error('Unsupported color conversion: rgb -> ' + format);
123
151
  }
@@ -127,7 +155,89 @@ class RGBColor extends Color {
127
155
  return this.red << 16 | this.green << 8 | this.blue;
128
156
  }
129
157
 
130
- clone(): Color {
158
+ /**
159
+ * Converts an RGB color value to HSB.
160
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
161
+ * @returns An HSBColor object.
162
+ */
163
+ private toHSB(): IColor {
164
+ const red = this.red / 255;
165
+ const green = this.green / 255;
166
+ const blue = this.blue / 255;
167
+ const min = Math.min(red, green, blue);
168
+ const brightness = Math.max(red, green, blue);
169
+ const chroma = brightness - min;
170
+ const saturation = brightness === 0 ? 0 : chroma / brightness;
171
+ let hue = 0; // achromatic
172
+
173
+ if (chroma !== 0) {
174
+ switch (brightness) {
175
+ case red:
176
+ hue = (green - blue) / chroma + (green < blue ? 6 : 0);
177
+ break;
178
+ case green:
179
+ hue = (blue - red) / chroma + 2;
180
+ break;
181
+ case blue:
182
+ hue = (red - green) / chroma + 4;
183
+ break;
184
+ }
185
+
186
+ hue /= 6;
187
+ }
188
+
189
+ return new HSBColor(
190
+ toFixedNumber(hue * 360, 2),
191
+ toFixedNumber(saturation * 100, 2),
192
+ toFixedNumber(brightness * 100, 2),
193
+ this.alpha
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Converts an RGB color value to HSL.
199
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
200
+ * @returns An HSLColor object.
201
+ */
202
+ private toHSL(): IColor {
203
+ const red = this.red / 255;
204
+ const green = this.green / 255;
205
+ const blue = this.blue / 255;
206
+ const min = Math.min(red, green, blue);
207
+ const max = Math.max(red, green, blue);
208
+ const lightness = (max + min) / 2;
209
+ const chroma = max - min;
210
+ let hue: number;
211
+ let saturation: number;
212
+
213
+ if (chroma === 0) {
214
+ hue = saturation = 0; // achromatic
215
+ } else {
216
+ saturation = chroma / (lightness < .5 ? max + min : 2 - max - min);
217
+
218
+ switch (max) {
219
+ case red:
220
+ hue = (green - blue) / chroma + (green < blue ? 6 : 0);
221
+ break;
222
+ case green:
223
+ hue = (blue - red) / chroma + 2;
224
+ break;
225
+ case blue:
226
+ hue = (red - green) / chroma + 4;
227
+ break;
228
+ }
229
+
230
+ hue /= 6;
231
+ }
232
+
233
+ return new HSLColor(
234
+ toFixedNumber(hue * 360, 2),
235
+ toFixedNumber(saturation * 100, 2),
236
+ toFixedNumber(lightness * 100, 2),
237
+ this.alpha);
238
+ }
239
+
240
+ clone(): IColor {
131
241
  return new RGBColor(this.red, this.green, this.blue, this.alpha);
132
242
  }
133
243
 
@@ -136,9 +246,9 @@ class RGBColor extends Color {
136
246
  case 'red':
137
247
  case 'green':
138
248
  case 'blue':
139
- return {minValue: 0, maxValue: 255, step: 1};
249
+ return {minValue: 0x0, maxValue: 0xFF, step: 0x1, pageSize: 0x11};
140
250
  case 'alpha':
141
- return {minValue: 0, maxValue: 1, step: 0.01};
251
+ return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1};
142
252
  default:
143
253
  throw new Error('Unknown color channel: ' + channel);
144
254
  }
@@ -161,6 +271,15 @@ class RGBColor extends Color {
161
271
  }
162
272
  return new NumberFormatter(locale, options).format(value);
163
273
  }
274
+
275
+ getColorSpace(): ColorFormat {
276
+ return 'rgb';
277
+ }
278
+
279
+ private static colorChannels: [ColorChannel, ColorChannel, ColorChannel] = ['red', 'green', 'blue'];
280
+ getColorChannels(): [ColorChannel, ColorChannel, ColorChannel] {
281
+ return RGBColor.colorChannels;
282
+ }
164
283
  }
165
284
 
166
285
  // X = <negative/positive number with/without decimal places>
@@ -186,10 +305,14 @@ class HSBColor extends Color {
186
305
  switch (format) {
187
306
  case 'css':
188
307
  return this.toHSL().toString('css');
308
+ case 'hex':
309
+ return this.toRGB().toString('hex');
310
+ case 'hexa':
311
+ return this.toRGB().toString('hexa');
189
312
  case 'hsb':
190
- return `hsb(${this.hue}, ${this.saturation}%, ${this.brightness}%)`;
313
+ return `hsb(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.brightness, 2)}%)`;
191
314
  case 'hsba':
192
- return `hsba(${this.hue}, ${this.saturation}%, ${this.brightness}%, ${this.alpha})`;
315
+ return `hsba(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.brightness, 2)}%, ${this.alpha})`;
193
316
  default:
194
317
  return this.toFormat(format).toString(format);
195
318
  }
@@ -203,41 +326,64 @@ class HSBColor extends Color {
203
326
  case 'hsl':
204
327
  case 'hsla':
205
328
  return this.toHSL();
329
+ case 'rgb':
330
+ case 'rgba':
331
+ return this.toRGB();
206
332
  default:
207
333
  throw new Error('Unsupported color conversion: hsb -> ' + format);
208
334
  }
209
335
  }
210
336
 
211
- private toHSL(): Color {
212
- // determine the lightness in the range [0,100]
213
- var l = (2 - this.saturation / 100) * this.brightness / 2;
337
+ /**
338
+ * Converts a HSB color to HSL.
339
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL.
340
+ * @returns An HSLColor object.
341
+ */
342
+ private toHSL(): IColor {
343
+ let saturation = this.saturation / 100;
344
+ let brightness = this.brightness / 100;
345
+ let lightness = brightness * (1 - saturation / 2);
346
+ saturation = lightness === 0 || lightness === 1 ? 0 : (brightness - lightness) / Math.min(lightness, 1 - lightness);
347
+
348
+ return new HSLColor(
349
+ toFixedNumber(this.hue, 2),
350
+ toFixedNumber(saturation * 100, 2),
351
+ toFixedNumber(lightness * 100, 2),
352
+ this.alpha
353
+ );
354
+ }
214
355
 
215
- // store the HSL components
356
+ /**
357
+ * Converts a HSV color value to RGB.
358
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative.
359
+ * @returns An RGBColor object.
360
+ */
361
+ private toRGB(): IColor {
216
362
  let hue = this.hue;
217
- let saturation = this.saturation * this.brightness / (l < 50 ? l * 2 : 200 - l * 2);
218
- let lightness = l;
219
-
220
- // correct a division-by-zero error
221
- if (isNaN(saturation)) {
222
- saturation = 0;
223
- }
224
-
225
- return new HSLColor(hue, saturation, lightness, this.alpha);
363
+ let saturation = this.saturation / 100;
364
+ let brightness = this.brightness / 100;
365
+ let fn = (n: number, k = (n + hue / 60) % 6) => brightness - saturation * brightness * Math.max(Math.min(k, 4 - k, 1), 0);
366
+ return new RGBColor(
367
+ Math.round(fn(5) * 255),
368
+ Math.round(fn(3) * 255),
369
+ Math.round(fn(1) * 255),
370
+ this.alpha
371
+ );
226
372
  }
227
373
 
228
- clone(): Color {
374
+ clone(): IColor {
229
375
  return new HSBColor(this.hue, this.saturation, this.brightness, this.alpha);
230
376
  }
231
377
 
232
378
  getChannelRange(channel: ColorChannel): ColorChannelRange {
233
379
  switch (channel) {
234
380
  case 'hue':
235
- return {minValue: 0, maxValue: 360, step: 1};
381
+ return {minValue: 0, maxValue: 360, step: 1, pageSize: 15};
236
382
  case 'saturation':
237
383
  case 'brightness':
238
- return {minValue: 0, maxValue: 100, step: 1};
384
+ return {minValue: 0, maxValue: 100, step: 1, pageSize: 10};
239
385
  case 'alpha':
240
- return {minValue: 0, maxValue: 1, step: 0.01};
386
+ return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1};
241
387
  default:
242
388
  throw new Error('Unknown color channel: ' + channel);
243
389
  }
@@ -263,6 +409,15 @@ class HSBColor extends Color {
263
409
  }
264
410
  return new NumberFormatter(locale, options).format(value);
265
411
  }
412
+
413
+ getColorSpace(): ColorFormat {
414
+ return 'hsb';
415
+ }
416
+
417
+ private static colorChannels: [ColorChannel, ColorChannel, ColorChannel] = ['hue', 'saturation', 'brightness'];
418
+ getColorChannels(): [ColorChannel, ColorChannel, ColorChannel] {
419
+ return HSBColor.colorChannels;
420
+ }
266
421
  }
267
422
 
268
423
  // X = <negative/positive number with/without decimal places>
@@ -275,13 +430,11 @@ function mod(n, m) {
275
430
  return ((n % m) + m) % m;
276
431
  }
277
432
 
278
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
279
433
  class HSLColor extends Color {
280
434
  constructor(private hue: number, private saturation: number, private lightness: number, private alpha: number) {
281
435
  super();
282
436
  }
283
437
 
284
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
285
438
  static parse(value: string): HSLColor | void {
286
439
  let m: RegExpMatchArray | void;
287
440
  if ((m = value.match(HSL_REGEX))) {
@@ -292,39 +445,85 @@ class HSLColor extends Color {
292
445
 
293
446
  toString(format: ColorFormat | 'css') {
294
447
  switch (format) {
448
+ case 'hex':
449
+ return this.toRGB().toString('hex');
450
+ case 'hexa':
451
+ return this.toRGB().toString('hexa');
295
452
  case 'hsl':
296
- return `hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`;
453
+ return `hsl(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.lightness, 2)}%)`;
297
454
  case 'css':
298
455
  case 'hsla':
299
- return `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha})`;
456
+ return `hsla(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.lightness, 2)}%, ${this.alpha})`;
300
457
  default:
301
458
  return this.toFormat(format).toString(format);
302
459
  }
303
460
  }
304
-
305
461
  toFormat(format: ColorFormat): IColor {
306
462
  switch (format) {
307
463
  case 'hsl':
308
464
  case 'hsla':
309
465
  return this;
466
+ case 'hsb':
467
+ case 'hsba':
468
+ return this.toHSB();
469
+ case 'rgb':
470
+ case 'rgba':
471
+ return this.toRGB();
310
472
  default:
311
473
  throw new Error('Unsupported color conversion: hsl -> ' + format);
312
474
  }
313
475
  }
314
476
 
315
- clone(): Color {
477
+ /**
478
+ * Converts a HSL color to HSB.
479
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV.
480
+ * @returns An HSBColor object.
481
+ */
482
+ private toHSB(): IColor {
483
+ let saturation = this.saturation / 100;
484
+ let lightness = this.lightness / 100;
485
+ let brightness = lightness + saturation * Math.min(lightness, 1 - lightness);
486
+ saturation = brightness === 0 ? 0 : 2 * (1 - lightness / brightness);
487
+ return new HSBColor(
488
+ toFixedNumber(this.hue, 2),
489
+ toFixedNumber(saturation * 100, 2),
490
+ toFixedNumber(brightness * 100, 2),
491
+ this.alpha
492
+ );
493
+ }
494
+
495
+ /**
496
+ * Converts a HSL color to RGB.
497
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative.
498
+ * @returns An RGBColor object.
499
+ */
500
+ private toRGB(): IColor {
501
+ let hue = this.hue;
502
+ let saturation = this.saturation / 100;
503
+ let lightness = this.lightness / 100;
504
+ let a = saturation * Math.min(lightness, 1 - lightness);
505
+ let fn = (n: number, k = (n + hue / 30) % 12) => lightness - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
506
+ return new RGBColor(
507
+ Math.round(fn(0) * 255),
508
+ Math.round(fn(8) * 255),
509
+ Math.round(fn(4) * 255),
510
+ this.alpha
511
+ );
512
+ }
513
+
514
+ clone(): IColor {
316
515
  return new HSLColor(this.hue, this.saturation, this.lightness, this.alpha);
317
516
  }
318
517
 
319
518
  getChannelRange(channel: ColorChannel): ColorChannelRange {
320
519
  switch (channel) {
321
520
  case 'hue':
322
- return {minValue: 0, maxValue: 360, step: 1};
521
+ return {minValue: 0, maxValue: 360, step: 1, pageSize: 15};
323
522
  case 'saturation':
324
523
  case 'lightness':
325
- return {minValue: 0, maxValue: 100, step: 1};
524
+ return {minValue: 0, maxValue: 100, step: 1, pageSize: 10};
326
525
  case 'alpha':
327
- return {minValue: 0, maxValue: 1, step: 0.01};
526
+ return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1};
328
527
  default:
329
528
  throw new Error('Unknown color channel: ' + channel);
330
529
  }
@@ -350,4 +549,13 @@ class HSLColor extends Color {
350
549
  }
351
550
  return new NumberFormatter(locale, options).format(value);
352
551
  }
552
+
553
+ getColorSpace(): ColorFormat {
554
+ return 'hsl';
555
+ }
556
+
557
+ private static colorChannels: [ColorChannel, ColorChannel, ColorChannel] = ['hue', 'saturation', 'lightness'];
558
+ getColorChannels(): [ColorChannel, ColorChannel, ColorChannel] {
559
+ return HSLColor.colorChannels;
560
+ }
353
561
  }
package/src/index.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- export * from './Color';
13
+ export {parseColor} from './Color';
14
+ export * from './useColorAreaState';
14
15
  export * from './useColorSliderState';
15
16
  export * from './useColorWheelState';
16
17
  export * from './useColorFieldState';
@@ -0,0 +1,184 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {clamp, snapValueToStep, useControlledState} from '@react-stately/utils';
14
+ import {Color, ColorAreaProps, ColorChannel} from '@react-types/color';
15
+ import {normalizeColor, parseColor} from './Color';
16
+ import {useMemo, useRef, useState} from 'react';
17
+
18
+ export interface ColorAreaState {
19
+ /** The current color value displayed by the color area. */
20
+ readonly value: Color,
21
+ /** Sets the current color value. If a string is passed, it will be parsed to a Color. */
22
+ setValue(value: string | Color): void,
23
+
24
+ /** The current value of the horizontal axis channel displayed by the color area. */
25
+ xValue: number,
26
+ /** Sets the value for the horizontal axis channel displayed by the color area, and triggers `onChange`. */
27
+ setXValue(value: number): void,
28
+
29
+ /** The current value of the vertical axis channel displayed by the color area. */
30
+ yValue: number,
31
+ /** Sets the value for the vertical axis channel displayed by the color area, and triggers `onChange`. */
32
+ setYValue(value: number): void,
33
+
34
+ /** Sets the x and y channels of the current color value based on a percentage of the width and height of the color area, and triggers `onChange`. */
35
+ setColorFromPoint(x: number, y: number): void,
36
+ /** Returns the coordinates of the thumb relative to the upper left corner of the color area as a percentage. */
37
+ getThumbPosition(): {x: number, y: number},
38
+
39
+ /** Increments the value of the horizontal axis channel by the channel step or page amount. */
40
+ incrementX(stepSize?: number): void,
41
+ /** Decrements the value of the horizontal axis channel by the channel step or page amount. */
42
+ decrementX(stepSize?: number): void,
43
+
44
+ /** Increments the value of the vertical axis channel by the channel step or page amount. */
45
+ incrementY(stepSize?: number): void,
46
+ /** Decrements the value of the vertical axis channel by the channel step or page amount. */
47
+ decrementY(stepSize?: number): void,
48
+
49
+ /** Whether the color area is currently being dragged. */
50
+ readonly isDragging: boolean,
51
+ /** Sets whether the color area is being dragged. */
52
+ setDragging(value: boolean): void,
53
+
54
+ /** Returns the xChannel, yChannel and zChannel names based on the color value. */
55
+ channels: {xChannel: ColorChannel, yChannel: ColorChannel, zChannel: ColorChannel},
56
+ xChannelStep: number,
57
+ yChannelStep: number,
58
+ xChannelPageStep: number,
59
+ yChannelPageStep: number,
60
+
61
+ /** Returns the color that should be displayed in the color area thumb instead of `value`. */
62
+ getDisplayColor(): Color
63
+ }
64
+
65
+ const DEFAULT_COLOR = parseColor('#ffffff');
66
+ /**
67
+ * Provides state management for a color area component.
68
+ * Color area allows users to adjust two channels of an HSL, HSB or RGB color value against a two-dimensional gradient background.
69
+ */
70
+ export function useColorAreaState(props: ColorAreaProps): ColorAreaState {
71
+ let {
72
+ value,
73
+ defaultValue,
74
+ xChannel,
75
+ yChannel,
76
+ onChange,
77
+ onChangeEnd
78
+ } = props;
79
+
80
+ if (!value && !defaultValue) {
81
+ defaultValue = DEFAULT_COLOR;
82
+ }
83
+
84
+ let [color, setColor] = useControlledState(value && normalizeColor(value), defaultValue && normalizeColor(defaultValue), onChange);
85
+ let valueRef = useRef(color);
86
+ valueRef.current = color;
87
+
88
+ let channels = useMemo(() =>
89
+ valueRef.current.getColorSpaceAxes({xChannel, yChannel}),
90
+ [xChannel, yChannel]
91
+ );
92
+
93
+ let xChannelRange = color.getChannelRange(channels.xChannel);
94
+ let yChannelRange = color.getChannelRange(channels.yChannel);
95
+ let {minValue: minValueX, maxValue: maxValueX, step: stepX, pageSize: pageSizeX} = xChannelRange;
96
+ let {minValue: minValueY, maxValue: maxValueY, step: stepY, pageSize: pageSizeY} = yChannelRange;
97
+
98
+ let [isDragging, setDragging] = useState(false);
99
+ let isDraggingRef = useRef(false).current;
100
+
101
+ let xValue = color.getChannelValue(channels.xChannel);
102
+ let yValue = color.getChannelValue(channels.yChannel);
103
+ let setXValue = (v: number) => {
104
+ if (v === xValue) {
105
+ return;
106
+ }
107
+ valueRef.current = color.withChannelValue(channels.xChannel, v);
108
+ setColor(valueRef.current);
109
+ };
110
+ let setYValue = (v: number) => {
111
+ if (v === yValue) {
112
+ return;
113
+ }
114
+ valueRef.current = color.withChannelValue(channels.yChannel, v);
115
+ setColor(valueRef.current);
116
+ };
117
+
118
+ return {
119
+ channels,
120
+ xChannelStep: stepX,
121
+ yChannelStep: stepY,
122
+ xChannelPageStep: pageSizeX,
123
+ yChannelPageStep: pageSizeY,
124
+ value: color,
125
+ setValue(value) {
126
+ let c = normalizeColor(value);
127
+ valueRef.current = c;
128
+ setColor(c);
129
+ },
130
+ xValue,
131
+ setXValue,
132
+ yValue,
133
+ setYValue,
134
+ setColorFromPoint(x: number, y: number) {
135
+ let newXValue = minValueX + clamp(x, 0, 1) * (maxValueX - minValueX);
136
+ let newYValue = minValueY + (1 - clamp(y, 0, 1)) * (maxValueY - minValueY);
137
+ let newColor:Color;
138
+ if (newXValue !== xValue) {
139
+ // Round new value to multiple of step, clamp value between min and max
140
+ newXValue = snapValueToStep(newXValue, minValueX, maxValueX, stepX);
141
+ newColor = color.withChannelValue(channels.xChannel, newXValue);
142
+ }
143
+ if (newYValue !== yValue) {
144
+ // Round new value to multiple of step, clamp value between min and max
145
+ newYValue = snapValueToStep(newYValue, minValueY, maxValueY, stepY);
146
+ newColor = (newColor || color).withChannelValue(channels.yChannel, newYValue);
147
+ }
148
+ if (newColor) {
149
+ setColor(newColor);
150
+ }
151
+ },
152
+ getThumbPosition() {
153
+ let x = (xValue - minValueX) / (maxValueX - minValueX);
154
+ let y = 1 - (yValue - minValueY) / (maxValueY - minValueY);
155
+ return {x, y};
156
+ },
157
+ incrementX(stepSize) {
158
+ setXValue(xValue + stepSize > maxValueX ? maxValueX : snapValueToStep(xValue + stepSize, minValueX, maxValueX, stepX));
159
+ },
160
+ incrementY(stepSize) {
161
+ setYValue(yValue + stepSize > maxValueY ? maxValueY : snapValueToStep(yValue + stepSize, minValueY, maxValueY, stepY));
162
+ },
163
+ decrementX(stepSize) {
164
+ setXValue(snapValueToStep(xValue - stepSize, minValueX, maxValueX, stepX));
165
+ },
166
+ decrementY(stepSize) {
167
+ setYValue(snapValueToStep(yValue - stepSize, minValueY, maxValueY, stepY));
168
+ },
169
+ setDragging(isDragging) {
170
+ let wasDragging = isDraggingRef;
171
+ isDraggingRef = isDragging;
172
+
173
+ if (onChangeEnd && !isDragging && wasDragging) {
174
+ onChangeEnd(valueRef.current);
175
+ }
176
+
177
+ setDragging(isDragging);
178
+ },
179
+ isDragging,
180
+ getDisplayColor() {
181
+ return color.withChannelValue('alpha', 1);
182
+ }
183
+ };
184
+ }