@react-stately/color 3.0.0-beta.1 → 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';
@@ -29,10 +29,18 @@ export function parseColor(value: string): IColor {
29
29
  throw new Error('Invalid color value: ' + value);
30
30
  }
31
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
+
32
40
  abstract class Color implements IColor {
33
41
  abstract toFormat(format: ColorFormat): IColor;
34
42
  abstract toString(format: ColorFormat | 'css'): string;
35
- abstract clone(): Color;
43
+ abstract clone(): IColor;
36
44
  abstract getChannelRange(channel: ColorChannel): ColorChannelRange;
37
45
  abstract formatChannelValue(channel: ColorChannel, locale: string): string;
38
46
 
@@ -61,6 +69,17 @@ abstract class Color implements IColor {
61
69
  getChannelName(channel: ColorChannel, locale: string) {
62
70
  return messages.getStringForLocale(channel, locale);
63
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]
64
83
  }
65
84
 
66
85
  const HEX_REGEX = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i;
@@ -90,7 +109,9 @@ class RGBColor extends Color {
90
109
  let b = parseInt(m[2][4] + m[2][5], 16);
91
110
  return new RGBColor(r, g, b, 1);
92
111
  }
93
- } if ((m = value.match(RGB_REGEX))) {
112
+ }
113
+
114
+ if ((m = value.match(RGB_REGEX))) {
94
115
  const [r, g, b, a] = (m[1] ?? m[2]).split(',').map(n => Number(n.trim()));
95
116
  return new RGBColor(clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), clamp(a ?? 1, 0, 1));
96
117
  }
@@ -119,6 +140,12 @@ class RGBColor extends Color {
119
140
  case 'rgb':
120
141
  case 'rgba':
121
142
  return this;
143
+ case 'hsb':
144
+ case 'hsba':
145
+ return this.toHSB();
146
+ case 'hsl':
147
+ case 'hsla':
148
+ return this.toHSL();
122
149
  default:
123
150
  throw new Error('Unsupported color conversion: rgb -> ' + format);
124
151
  }
@@ -128,7 +155,89 @@ class RGBColor extends Color {
128
155
  return this.red << 16 | this.green << 8 | this.blue;
129
156
  }
130
157
 
131
- 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 {
132
241
  return new RGBColor(this.red, this.green, this.blue, this.alpha);
133
242
  }
134
243
 
@@ -137,9 +246,9 @@ class RGBColor extends Color {
137
246
  case 'red':
138
247
  case 'green':
139
248
  case 'blue':
140
- return {minValue: 0, maxValue: 255, step: 1};
249
+ return {minValue: 0x0, maxValue: 0xFF, step: 0x1, pageSize: 0x11};
141
250
  case 'alpha':
142
- return {minValue: 0, maxValue: 1, step: 0.01};
251
+ return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1};
143
252
  default:
144
253
  throw new Error('Unknown color channel: ' + channel);
145
254
  }
@@ -162,6 +271,15 @@ class RGBColor extends Color {
162
271
  }
163
272
  return new NumberFormatter(locale, options).format(value);
164
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
+ }
165
283
  }
166
284
 
167
285
  // X = <negative/positive number with/without decimal places>
@@ -187,10 +305,14 @@ class HSBColor extends Color {
187
305
  switch (format) {
188
306
  case 'css':
189
307
  return this.toHSL().toString('css');
308
+ case 'hex':
309
+ return this.toRGB().toString('hex');
310
+ case 'hexa':
311
+ return this.toRGB().toString('hexa');
190
312
  case 'hsb':
191
- return `hsb(${this.hue}, ${this.saturation}%, ${this.brightness}%)`;
313
+ return `hsb(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.brightness, 2)}%)`;
192
314
  case 'hsba':
193
- 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})`;
194
316
  default:
195
317
  return this.toFormat(format).toString(format);
196
318
  }
@@ -204,41 +326,64 @@ class HSBColor extends Color {
204
326
  case 'hsl':
205
327
  case 'hsla':
206
328
  return this.toHSL();
329
+ case 'rgb':
330
+ case 'rgba':
331
+ return this.toRGB();
207
332
  default:
208
333
  throw new Error('Unsupported color conversion: hsb -> ' + format);
209
334
  }
210
335
  }
211
336
 
212
- private toHSL(): Color {
213
- // determine the lightness in the range [0,100]
214
- 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
+ }
215
355
 
216
- // 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 {
217
362
  let hue = this.hue;
218
- let saturation = this.saturation * this.brightness / (l < 50 ? l * 2 : 200 - l * 2);
219
- let lightness = l;
220
-
221
- // correct a division-by-zero error
222
- if (isNaN(saturation)) {
223
- saturation = 0;
224
- }
225
-
226
- 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
+ );
227
372
  }
228
373
 
229
- clone(): Color {
374
+ clone(): IColor {
230
375
  return new HSBColor(this.hue, this.saturation, this.brightness, this.alpha);
231
376
  }
232
377
 
233
378
  getChannelRange(channel: ColorChannel): ColorChannelRange {
234
379
  switch (channel) {
235
380
  case 'hue':
236
- return {minValue: 0, maxValue: 360, step: 1};
381
+ return {minValue: 0, maxValue: 360, step: 1, pageSize: 15};
237
382
  case 'saturation':
238
383
  case 'brightness':
239
- return {minValue: 0, maxValue: 100, step: 1};
384
+ return {minValue: 0, maxValue: 100, step: 1, pageSize: 10};
240
385
  case 'alpha':
241
- return {minValue: 0, maxValue: 1, step: 0.01};
386
+ return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1};
242
387
  default:
243
388
  throw new Error('Unknown color channel: ' + channel);
244
389
  }
@@ -264,6 +409,15 @@ class HSBColor extends Color {
264
409
  }
265
410
  return new NumberFormatter(locale, options).format(value);
266
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
+ }
267
421
  }
268
422
 
269
423
  // X = <negative/positive number with/without decimal places>
@@ -276,13 +430,11 @@ function mod(n, m) {
276
430
  return ((n % m) + m) % m;
277
431
  }
278
432
 
279
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
280
433
  class HSLColor extends Color {
281
434
  constructor(private hue: number, private saturation: number, private lightness: number, private alpha: number) {
282
435
  super();
283
436
  }
284
437
 
285
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
286
438
  static parse(value: string): HSLColor | void {
287
439
  let m: RegExpMatchArray | void;
288
440
  if ((m = value.match(HSL_REGEX))) {
@@ -293,39 +445,85 @@ class HSLColor extends Color {
293
445
 
294
446
  toString(format: ColorFormat | 'css') {
295
447
  switch (format) {
448
+ case 'hex':
449
+ return this.toRGB().toString('hex');
450
+ case 'hexa':
451
+ return this.toRGB().toString('hexa');
296
452
  case 'hsl':
297
- return `hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`;
453
+ return `hsl(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.lightness, 2)}%)`;
298
454
  case 'css':
299
455
  case 'hsla':
300
- 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})`;
301
457
  default:
302
458
  return this.toFormat(format).toString(format);
303
459
  }
304
460
  }
305
-
306
461
  toFormat(format: ColorFormat): IColor {
307
462
  switch (format) {
308
463
  case 'hsl':
309
464
  case 'hsla':
310
465
  return this;
466
+ case 'hsb':
467
+ case 'hsba':
468
+ return this.toHSB();
469
+ case 'rgb':
470
+ case 'rgba':
471
+ return this.toRGB();
311
472
  default:
312
473
  throw new Error('Unsupported color conversion: hsl -> ' + format);
313
474
  }
314
475
  }
315
476
 
316
- 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 {
317
515
  return new HSLColor(this.hue, this.saturation, this.lightness, this.alpha);
318
516
  }
319
517
 
320
518
  getChannelRange(channel: ColorChannel): ColorChannelRange {
321
519
  switch (channel) {
322
520
  case 'hue':
323
- return {minValue: 0, maxValue: 360, step: 1};
521
+ return {minValue: 0, maxValue: 360, step: 1, pageSize: 15};
324
522
  case 'saturation':
325
523
  case 'lightness':
326
- return {minValue: 0, maxValue: 100, step: 1};
524
+ return {minValue: 0, maxValue: 100, step: 1, pageSize: 10};
327
525
  case 'alpha':
328
- return {minValue: 0, maxValue: 1, step: 0.01};
526
+ return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1};
329
527
  default:
330
528
  throw new Error('Unknown color channel: ' + channel);
331
529
  }
@@ -351,4 +549,13 @@ class HSLColor extends Color {
351
549
  }
352
550
  return new NumberFormatter(locale, options).format(value);
353
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
+ }
354
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
+ }