panelui-native 0.41.0 → 0.44.0
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/README.md +3 -2
- package/lib/module/components/card/index.js +8 -154
- package/lib/module/components/card/index.js.map +1 -1
- package/lib/module/components/color-picker/index.js +1395 -0
- package/lib/module/components/color-picker/index.js.map +1 -0
- package/lib/module/components/combobox/index.js +87 -19
- package/lib/module/components/combobox/index.js.map +1 -1
- package/lib/module/components/kpi-chart/index.js +648 -0
- package/lib/module/components/kpi-chart/index.js.map +1 -0
- package/lib/module/components/menu/index.js +40 -10
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +704 -0
- package/lib/module/components/radar-chart/index.js.map +1 -0
- package/lib/module/components/section-rail/index.js +32 -7
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/slider/index.js +240 -39
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +194 -10
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/module/utils/chart.js +44 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/module/utils/color.js +268 -0
- package/lib/module/utils/color.js.map +1 -0
- package/lib/typescript/src/components/card/index.d.ts +0 -20
- package/lib/typescript/src/components/card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/color-picker/index.d.ts +258 -0
- package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -0
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/kpi-chart/index.d.ts +490 -0
- package/lib/typescript/src/components/kpi-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radar-chart/index.d.ts +214 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts +19 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +11 -1
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +21 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/lib/typescript/src/utils/color.d.ts +74 -0
- package/lib/typescript/src/utils/color.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/components/card/index.tsx +7 -209
- package/src/components/color-picker/index.tsx +1618 -0
- package/src/components/combobox/index.tsx +89 -19
- package/src/components/kpi-chart/index.tsx +719 -0
- package/src/components/menu/index.tsx +51 -6
- package/src/components/radar-chart/index.tsx +910 -0
- package/src/components/section-rail/index.tsx +42 -8
- package/src/components/slider/index.tsx +283 -41
- package/src/components/tabs/index.tsx +263 -8
- package/src/index.ts +61 -1
- package/src/theme/use-theme.ts +1 -1
- package/src/utils/chart.ts +52 -0
- package/src/utils/color.ts +246 -0
- package/theme.css +125 -112
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ColorPicker — a colour chosen by dragging, not by typing.
|
|
5
|
+
*
|
|
6
|
+
* The controls are the colour model made visible: a square where saturation
|
|
7
|
+
* runs across and brightness runs up, under a hue picked on its own scale.
|
|
8
|
+
* That is why the component stores hue, saturation, value and alpha rather
|
|
9
|
+
* than the string it hands back — a fully black colour is `#000` whatever hue
|
|
10
|
+
* and saturation produced it, so a picker that stored the output would lose
|
|
11
|
+
* the thumb the moment you dragged into a corner and could not put it back.
|
|
12
|
+
*
|
|
13
|
+
* Nothing about a drag crosses to JavaScript. The four channels are shared
|
|
14
|
+
* values; the hue behind the square, both thumb fills and the preview swatch
|
|
15
|
+
* are `backgroundColor`s computed from them on the UI thread, and the
|
|
16
|
+
* translucency ramp on the alpha track is a gradient used as a mask over a
|
|
17
|
+
* solid fill rather than a gradient whose colours have to be re-declared from
|
|
18
|
+
* React on every frame. The picked colour bridges back on change and again on
|
|
19
|
+
* release, the same as any other input here.
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <ColorPicker defaultValue="#22c55e" onValueCommit={setBrand}>
|
|
23
|
+
* <ColorPicker.Area />
|
|
24
|
+
* <ColorPicker.Hue />
|
|
25
|
+
* <ColorPicker.Preview showValue />
|
|
26
|
+
* </ColorPicker>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Composition is the API: a picker is whichever of the parts you put in it, so
|
|
30
|
+
* a control with no alpha is one that has no `ColorPicker.Alpha`, not one with
|
|
31
|
+
* a prop turned off.
|
|
32
|
+
*
|
|
33
|
+
* Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
34
|
+
*/
|
|
35
|
+
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
36
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
37
|
+
import MaskedView from '@react-native-masked-view/masked-view';
|
|
38
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
39
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
40
|
+
import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
41
|
+
import Svg, { Circle, Defs, Path, Pattern, RadialGradient, Rect, Stop } from 'react-native-svg';
|
|
42
|
+
import { tv } from 'tailwind-variants';
|
|
43
|
+
import { useCSSVariable } from 'uniwind';
|
|
44
|
+
import { useDirectionSign } from "../../hooks/use-direction.js";
|
|
45
|
+
import { Text } from "../../primitives/text.js";
|
|
46
|
+
import { formatColor, hsvToCss, hsvToHex, parseColor } from "../../utils/color.js";
|
|
47
|
+
import { selectionTick } from "../../utils/haptics.js";
|
|
48
|
+
|
|
49
|
+
/** Settles a thumb that was moved by something other than a finger. */
|
|
50
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
51
|
+
const TIMING = {
|
|
52
|
+
duration: 140
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The hue scale, drawn once as a gradient rather than sampled per pixel. Six
|
|
57
|
+
* stops plus the wrap back to red — fewer would bend the ramp, more would not
|
|
58
|
+
* change it, because hue is piecewise linear in RGB and these are the corners.
|
|
59
|
+
*/
|
|
60
|
+
const HUE_STOPS = ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#ff00ff', '#ff0000'];
|
|
61
|
+
|
|
62
|
+
/** The same ramp for a right-to-left picker, where hue runs the other way. */
|
|
63
|
+
const HUE_STOPS_FLIPPED = [...HUE_STOPS].reverse();
|
|
64
|
+
|
|
65
|
+
/** How far one screen-reader increment moves a channel. */
|
|
66
|
+
const NUDGE = 0.05;
|
|
67
|
+
/** How far one screen-reader increment moves the hue, in degrees. */
|
|
68
|
+
const HUE_NUDGE = 10;
|
|
69
|
+
const colorPickerVariants = tv({
|
|
70
|
+
slots: {
|
|
71
|
+
root: 'w-full gap-3',
|
|
72
|
+
area: 'overflow-hidden rounded-2xl border border-border',
|
|
73
|
+
wheel: 'overflow-hidden rounded-full border border-border',
|
|
74
|
+
// Both thumbs are a white ring around a fill of the colour under them, so
|
|
75
|
+
// the ring reads against the dark end of the square as well as the light.
|
|
76
|
+
// Two points, not three: the ring is there to separate the fill from what
|
|
77
|
+
// is behind it, and any more of it starts hiding the colour it is showing.
|
|
78
|
+
thumb: 'absolute start-0 top-0 rounded-full border-2 border-white shadow-md',
|
|
79
|
+
thumbFill: 'flex-1 rounded-full',
|
|
80
|
+
track: 'w-full overflow-hidden rounded-full border border-border',
|
|
81
|
+
swatches: 'flex-row flex-wrap items-center gap-2',
|
|
82
|
+
swatch: 'items-center justify-center rounded-full',
|
|
83
|
+
swatchRing: 'absolute rounded-full border-2 border-foreground',
|
|
84
|
+
preview: 'flex-row items-center gap-3',
|
|
85
|
+
previewSwatch: 'overflow-hidden rounded-full border border-border',
|
|
86
|
+
previewValue: 'text-sm font-medium text-foreground',
|
|
87
|
+
// The header strip: a name on the leading edge, the value and a swatch on
|
|
88
|
+
// the trailing one.
|
|
89
|
+
field: 'w-full flex-row items-center gap-3 rounded-2xl bg-surface px-4 py-3',
|
|
90
|
+
fieldLabel: 'flex-1 text-base font-medium text-foreground',
|
|
91
|
+
fieldValue: 'text-base font-normal text-muted-foreground',
|
|
92
|
+
fieldSwatch: 'overflow-hidden rounded-full border border-border',
|
|
93
|
+
// The readout above a track — the number on one side, what it names on the
|
|
94
|
+
// other.
|
|
95
|
+
channel: 'w-full flex-row items-center justify-between gap-3',
|
|
96
|
+
channelValue: 'text-base font-normal text-muted-foreground',
|
|
97
|
+
channelLabel: 'text-base font-medium text-foreground'
|
|
98
|
+
},
|
|
99
|
+
variants: {
|
|
100
|
+
size: {
|
|
101
|
+
sm: {},
|
|
102
|
+
md: {},
|
|
103
|
+
lg: {}
|
|
104
|
+
},
|
|
105
|
+
disabled: {
|
|
106
|
+
true: {
|
|
107
|
+
root: 'opacity-50'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
defaultVariants: {
|
|
112
|
+
size: 'md'
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
/** Height of the saturation/brightness square, per size. */
|
|
116
|
+
const AREA_HEIGHT = {
|
|
117
|
+
sm: 140,
|
|
118
|
+
md: 180,
|
|
119
|
+
lg: 220
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Height of a channel track, per size. Tall enough that the knob sits *in* the
|
|
123
|
+
* track rather than on top of a hairline — a slider whose thumb overhangs a
|
|
124
|
+
* thin rule reads as a control laid over a decoration.
|
|
125
|
+
*/
|
|
126
|
+
const TRACK_HEIGHT = {
|
|
127
|
+
sm: 16,
|
|
128
|
+
md: 20,
|
|
129
|
+
lg: 24
|
|
130
|
+
};
|
|
131
|
+
/** Thumb diameter on a channel track — wider than the track, so it reads as a knob. */
|
|
132
|
+
const TRACK_THUMB = {
|
|
133
|
+
sm: 22,
|
|
134
|
+
md: 26,
|
|
135
|
+
lg: 30
|
|
136
|
+
};
|
|
137
|
+
/** Thumb diameter on the square. */
|
|
138
|
+
const AREA_THUMB = {
|
|
139
|
+
sm: 20,
|
|
140
|
+
md: 24,
|
|
141
|
+
lg: 28
|
|
142
|
+
};
|
|
143
|
+
/** Preset swatch diameter. */
|
|
144
|
+
const SWATCH = {
|
|
145
|
+
sm: 26,
|
|
146
|
+
md: 30,
|
|
147
|
+
lg: 34
|
|
148
|
+
};
|
|
149
|
+
/** The swatch beside the value in `Preview`. */
|
|
150
|
+
const PREVIEW_SWATCH = {
|
|
151
|
+
sm: 28,
|
|
152
|
+
md: 36,
|
|
153
|
+
lg: 44
|
|
154
|
+
};
|
|
155
|
+
/** The smaller swatch on the end of the `Field` strip. */
|
|
156
|
+
const FIELD_SWATCH = {
|
|
157
|
+
sm: 22,
|
|
158
|
+
md: 28,
|
|
159
|
+
lg: 34
|
|
160
|
+
};
|
|
161
|
+
/** Diameter of the wheel, per size. */
|
|
162
|
+
const WHEEL_SIZE = {
|
|
163
|
+
sm: 180,
|
|
164
|
+
md: 240,
|
|
165
|
+
lg: 300
|
|
166
|
+
};
|
|
167
|
+
const ColorPickerContext = /*#__PURE__*/createContext(null);
|
|
168
|
+
function useColorPicker(part) {
|
|
169
|
+
const ctx = useContext(ColorPickerContext);
|
|
170
|
+
if (!ctx) throw new Error(`${part} must be used inside <ColorPicker>.`);
|
|
171
|
+
return ctx;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Where a picker starts when it is handed a value it cannot read. */
|
|
175
|
+
const FALLBACK = {
|
|
176
|
+
h: 0,
|
|
177
|
+
s: 1,
|
|
178
|
+
v: 1,
|
|
179
|
+
a: 1
|
|
180
|
+
};
|
|
181
|
+
const ColorPickerRoot = /*#__PURE__*/forwardRef(({
|
|
182
|
+
className,
|
|
183
|
+
value: valueProp,
|
|
184
|
+
defaultValue = '#ff0000',
|
|
185
|
+
onValueChange,
|
|
186
|
+
onValueCommit,
|
|
187
|
+
format = 'hex',
|
|
188
|
+
disabled = false,
|
|
189
|
+
haptics = false,
|
|
190
|
+
size = 'md',
|
|
191
|
+
children
|
|
192
|
+
}, ref) => {
|
|
193
|
+
const slots = colorPickerVariants({
|
|
194
|
+
size,
|
|
195
|
+
disabled
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Parsed once. After this the channels are the truth and the prop is only
|
|
199
|
+
// consulted when it changes to something we did not ourselves emit.
|
|
200
|
+
const initial = useMemo(() => parseColor(valueProp ?? defaultValue) ?? FALLBACK,
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
[]);
|
|
203
|
+
const hue = useSharedValue(initial.h);
|
|
204
|
+
const saturation = useSharedValue(initial.s);
|
|
205
|
+
const brightness = useSharedValue(initial.v);
|
|
206
|
+
const opacity = useSharedValue(initial.a);
|
|
207
|
+
const changeRef = useRef(onValueChange);
|
|
208
|
+
changeRef.current = onValueChange;
|
|
209
|
+
const commitRef = useRef(onValueCommit);
|
|
210
|
+
commitRef.current = onValueCommit;
|
|
211
|
+
const formatRef = useRef(format);
|
|
212
|
+
formatRef.current = format;
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* The last string this picker produced. A controlled parent hands it
|
|
216
|
+
* straight back, and re-parsing it into the channels mid-drag would fight
|
|
217
|
+
* the finger: the round trip is lossy at the edges of the square, so the
|
|
218
|
+
* thumb would be pulled a pixel back on every frame.
|
|
219
|
+
*/
|
|
220
|
+
const lastEmitted = useRef(null);
|
|
221
|
+
const emit = useCallback((h, s, v, a, commit) => {
|
|
222
|
+
const next = formatColor({
|
|
223
|
+
h,
|
|
224
|
+
s,
|
|
225
|
+
v,
|
|
226
|
+
a
|
|
227
|
+
}, formatRef.current);
|
|
228
|
+
lastEmitted.current = next;
|
|
229
|
+
changeRef.current?.(next);
|
|
230
|
+
if (commit) commitRef.current?.(next);
|
|
231
|
+
}, []);
|
|
232
|
+
|
|
233
|
+
// Keep the channels in step with a controlled value changed elsewhere. Set
|
|
234
|
+
// rather than animated: an external change is not a gesture, and springing
|
|
235
|
+
// the hue from 350° to 10° would run the long way round the wheel.
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (valueProp === undefined) return;
|
|
238
|
+
if (valueProp === lastEmitted.current) return;
|
|
239
|
+
const parsed = parseColor(valueProp);
|
|
240
|
+
if (!parsed) return;
|
|
241
|
+
hue.value = parsed.h;
|
|
242
|
+
saturation.value = parsed.s;
|
|
243
|
+
brightness.value = parsed.v;
|
|
244
|
+
opacity.value = parsed.a;
|
|
245
|
+
}, [valueProp, hue, saturation, brightness, opacity]);
|
|
246
|
+
const context = useMemo(() => ({
|
|
247
|
+
hue,
|
|
248
|
+
saturation,
|
|
249
|
+
brightness,
|
|
250
|
+
opacity,
|
|
251
|
+
emit,
|
|
252
|
+
format,
|
|
253
|
+
disabled,
|
|
254
|
+
haptics,
|
|
255
|
+
size: size ?? 'md'
|
|
256
|
+
}), [hue, saturation, brightness, opacity, emit, format, disabled, haptics, size]);
|
|
257
|
+
return /*#__PURE__*/_jsx(ColorPickerContext.Provider, {
|
|
258
|
+
value: context,
|
|
259
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
260
|
+
ref: ref,
|
|
261
|
+
className: slots.root({
|
|
262
|
+
className
|
|
263
|
+
}),
|
|
264
|
+
collapsable: false,
|
|
265
|
+
children: children
|
|
266
|
+
})
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
ColorPickerRoot.displayName = 'ColorPicker';
|
|
270
|
+
|
|
271
|
+
/* ------------------------------------------------------------------ *
|
|
272
|
+
* The checkerboard behind anything translucent.
|
|
273
|
+
* ------------------------------------------------------------------ */
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The "nothing here" backing. A tiled SVG pattern rather than a grid of views:
|
|
277
|
+
* a 340-point track at eight-point squares is 170 squares, and every one of
|
|
278
|
+
* them would be a view laid out by Yoga on the frame the picker mounts.
|
|
279
|
+
*/
|
|
280
|
+
function Checkerboard({
|
|
281
|
+
square = 7
|
|
282
|
+
}) {
|
|
283
|
+
const id = useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
284
|
+
// The pattern is themed rather than the usual grey-on-white: a checkerboard
|
|
285
|
+
// is a statement that there is nothing there, and "nothing" is the page.
|
|
286
|
+
const light = String(useCSSVariable('--color-background') ?? '#ffffff');
|
|
287
|
+
const dark = String(useCSSVariable('--color-muted') ?? '#e5e5e5');
|
|
288
|
+
return /*#__PURE__*/_jsxs(Svg, {
|
|
289
|
+
style: StyleSheet.absoluteFill,
|
|
290
|
+
pointerEvents: "none",
|
|
291
|
+
children: [/*#__PURE__*/_jsx(Defs, {
|
|
292
|
+
children: /*#__PURE__*/_jsxs(Pattern, {
|
|
293
|
+
id: id,
|
|
294
|
+
width: square * 2,
|
|
295
|
+
height: square * 2,
|
|
296
|
+
patternUnits: "userSpaceOnUse",
|
|
297
|
+
children: [/*#__PURE__*/_jsx(Rect, {
|
|
298
|
+
width: square * 2,
|
|
299
|
+
height: square * 2,
|
|
300
|
+
fill: light
|
|
301
|
+
}), /*#__PURE__*/_jsx(Rect, {
|
|
302
|
+
width: square,
|
|
303
|
+
height: square,
|
|
304
|
+
fill: dark
|
|
305
|
+
}), /*#__PURE__*/_jsx(Rect, {
|
|
306
|
+
x: square,
|
|
307
|
+
y: square,
|
|
308
|
+
width: square,
|
|
309
|
+
height: square,
|
|
310
|
+
fill: dark
|
|
311
|
+
})]
|
|
312
|
+
})
|
|
313
|
+
}), /*#__PURE__*/_jsx(Rect, {
|
|
314
|
+
width: "100%",
|
|
315
|
+
height: "100%",
|
|
316
|
+
fill: `url(#${id})`
|
|
317
|
+
})]
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* ------------------------------------------------------------------ *
|
|
322
|
+
* Area — saturation across, brightness up.
|
|
323
|
+
* ------------------------------------------------------------------ */
|
|
324
|
+
|
|
325
|
+
const ColorPickerArea = /*#__PURE__*/forwardRef(({
|
|
326
|
+
className,
|
|
327
|
+
height,
|
|
328
|
+
thumbClassName
|
|
329
|
+
}, ref) => {
|
|
330
|
+
const ctx = useColorPicker('ColorPicker.Area');
|
|
331
|
+
const slots = colorPickerVariants({
|
|
332
|
+
size: ctx.size
|
|
333
|
+
});
|
|
334
|
+
const sign = useDirectionSign();
|
|
335
|
+
const boxHeight = height ?? AREA_HEIGHT[ctx.size];
|
|
336
|
+
const thumbSize = AREA_THUMB[ctx.size];
|
|
337
|
+
const width = useSharedValue(0);
|
|
338
|
+
|
|
339
|
+
/*
|
|
340
|
+
* The screen reader's copy of the value. Updated when a drag ends rather
|
|
341
|
+
* than while it runs: a `now` that re-rendered on every frame would be a
|
|
342
|
+
* hundred announcements for one gesture, and nobody is listening to the
|
|
343
|
+
* middle of a drag.
|
|
344
|
+
*/
|
|
345
|
+
const [announced, setAnnounced] = useState(() => ({
|
|
346
|
+
s: ctx.saturation.value,
|
|
347
|
+
v: ctx.brightness.value
|
|
348
|
+
}));
|
|
349
|
+
const settle = useCallback((s, v) => {
|
|
350
|
+
setAnnounced({
|
|
351
|
+
s,
|
|
352
|
+
v
|
|
353
|
+
});
|
|
354
|
+
}, []);
|
|
355
|
+
const apply = (x, y, commit) => {
|
|
356
|
+
'worklet';
|
|
357
|
+
|
|
358
|
+
const w = Math.max(width.value, 1);
|
|
359
|
+
// `x` is measured from the physical left edge either way, so a
|
|
360
|
+
// right-to-left picker counts saturation back from the far side.
|
|
361
|
+
const along = sign === 1 ? x : w - x;
|
|
362
|
+
const s = Math.min(Math.max(along / w, 0), 1);
|
|
363
|
+
const v = 1 - Math.min(Math.max(y / boxHeight, 0), 1);
|
|
364
|
+
ctx.saturation.value = s;
|
|
365
|
+
ctx.brightness.value = v;
|
|
366
|
+
runOnJS(ctx.emit)(ctx.hue.value, s, v, ctx.opacity.value, commit);
|
|
367
|
+
if (commit) {
|
|
368
|
+
runOnJS(settle)(s, v);
|
|
369
|
+
if (ctx.haptics) runOnJS(selectionTick)();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Both axes belong to the square, so it takes the gesture on touch-down
|
|
374
|
+
// rather than after a threshold — a pad that needed ten points of travel
|
|
375
|
+
// before it responded would feel broken.
|
|
376
|
+
const pan = Gesture.Pan().enabled(!ctx.disabled).minDistance(0).shouldCancelWhenOutside(false).onBegin(event => apply(event.x, event.y, false)).onUpdate(event => apply(event.x, event.y, false)).onFinalize(event => apply(event.x, event.y, true));
|
|
377
|
+
const hueStyle = useAnimatedStyle(() => ({
|
|
378
|
+
backgroundColor: hsvToCss(ctx.hue.value, 1, 1, 1)
|
|
379
|
+
}));
|
|
380
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
381
|
+
// A transform is not laid out, so Yoga does not mirror it — the thumb
|
|
382
|
+
// has to travel the other way itself.
|
|
383
|
+
const along = ctx.saturation.value * width.value - thumbSize / 2;
|
|
384
|
+
return {
|
|
385
|
+
transform: [{
|
|
386
|
+
translateX: along * sign
|
|
387
|
+
}, {
|
|
388
|
+
translateY: (1 - ctx.brightness.value) * boxHeight - thumbSize / 2
|
|
389
|
+
}]
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
const thumbFillStyle = useAnimatedStyle(() => ({
|
|
393
|
+
backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, 1)
|
|
394
|
+
}));
|
|
395
|
+
const nudge = (channel, dir) => {
|
|
396
|
+
const next = Math.min(Math.max(ctx[channel].value + dir * NUDGE, 0), 1);
|
|
397
|
+
ctx[channel].value = withTiming(next, TIMING);
|
|
398
|
+
const s = channel === 'saturation' ? next : ctx.saturation.value;
|
|
399
|
+
const v = channel === 'brightness' ? next : ctx.brightness.value;
|
|
400
|
+
settle(s, v);
|
|
401
|
+
ctx.emit(ctx.hue.value, s, v, ctx.opacity.value, true);
|
|
402
|
+
};
|
|
403
|
+
const onAccessibilityAction = event => {
|
|
404
|
+
const action = event.nativeEvent.actionName;
|
|
405
|
+
if (action === 'increment') nudge('saturation', 1);else if (action === 'decrement') nudge('saturation', -1);else if (action === 'brighter') nudge('brightness', 1);else if (action === 'darker') nudge('brightness', -1);
|
|
406
|
+
};
|
|
407
|
+
return /*#__PURE__*/_jsx(GestureDetector, {
|
|
408
|
+
gesture: pan,
|
|
409
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
410
|
+
ref: ref,
|
|
411
|
+
style: {
|
|
412
|
+
height: boxHeight
|
|
413
|
+
},
|
|
414
|
+
className: "w-full",
|
|
415
|
+
onLayout: event => {
|
|
416
|
+
width.value = event.nativeEvent.layout.width;
|
|
417
|
+
},
|
|
418
|
+
accessible: true,
|
|
419
|
+
accessibilityRole: "adjustable",
|
|
420
|
+
accessibilityLabel: "Saturation and brightness",
|
|
421
|
+
accessibilityState: {
|
|
422
|
+
disabled: ctx.disabled
|
|
423
|
+
},
|
|
424
|
+
accessibilityValue: {
|
|
425
|
+
min: 0,
|
|
426
|
+
max: 100,
|
|
427
|
+
now: Math.round(announced.s * 100),
|
|
428
|
+
text: `${Math.round(announced.s * 100)}% saturation, ${Math.round(announced.v * 100)}% brightness`
|
|
429
|
+
},
|
|
430
|
+
accessibilityActions: [{
|
|
431
|
+
name: 'increment'
|
|
432
|
+
}, {
|
|
433
|
+
name: 'decrement'
|
|
434
|
+
}, {
|
|
435
|
+
name: 'brighter',
|
|
436
|
+
label: 'Brighter'
|
|
437
|
+
}, {
|
|
438
|
+
name: 'darker',
|
|
439
|
+
label: 'Darker'
|
|
440
|
+
}],
|
|
441
|
+
onAccessibilityAction: onAccessibilityAction,
|
|
442
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
443
|
+
style: StyleSheet.absoluteFill,
|
|
444
|
+
className: slots.area({
|
|
445
|
+
className
|
|
446
|
+
}),
|
|
447
|
+
children: [/*#__PURE__*/_jsx(Animated.View, {
|
|
448
|
+
style: [StyleSheet.absoluteFill, hueStyle]
|
|
449
|
+
}), /*#__PURE__*/_jsx(LinearGradient, {
|
|
450
|
+
colors: ['rgba(255,255,255,1)', 'rgba(255,255,255,0)'],
|
|
451
|
+
start: {
|
|
452
|
+
x: sign === 1 ? 0 : 1,
|
|
453
|
+
y: 0.5
|
|
454
|
+
},
|
|
455
|
+
end: {
|
|
456
|
+
x: sign === 1 ? 1 : 0,
|
|
457
|
+
y: 0.5
|
|
458
|
+
},
|
|
459
|
+
style: StyleSheet.absoluteFill
|
|
460
|
+
}), /*#__PURE__*/_jsx(LinearGradient, {
|
|
461
|
+
colors: ['rgba(0,0,0,0)', 'rgba(0,0,0,1)'],
|
|
462
|
+
start: {
|
|
463
|
+
x: 0.5,
|
|
464
|
+
y: 0
|
|
465
|
+
},
|
|
466
|
+
end: {
|
|
467
|
+
x: 0.5,
|
|
468
|
+
y: 1
|
|
469
|
+
},
|
|
470
|
+
style: StyleSheet.absoluteFill
|
|
471
|
+
})]
|
|
472
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
473
|
+
pointerEvents: "none",
|
|
474
|
+
style: [thumbStyle, {
|
|
475
|
+
width: thumbSize,
|
|
476
|
+
height: thumbSize
|
|
477
|
+
}],
|
|
478
|
+
className: slots.thumb({
|
|
479
|
+
className: thumbClassName
|
|
480
|
+
}),
|
|
481
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
482
|
+
style: thumbFillStyle,
|
|
483
|
+
className: slots.thumbFill()
|
|
484
|
+
})
|
|
485
|
+
})]
|
|
486
|
+
})
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
ColorPickerArea.displayName = 'ColorPicker.Area';
|
|
490
|
+
|
|
491
|
+
/* ------------------------------------------------------------------ *
|
|
492
|
+
* The two channel tracks share everything but what is drawn in them.
|
|
493
|
+
* ------------------------------------------------------------------ */
|
|
494
|
+
|
|
495
|
+
function ChannelTrack({
|
|
496
|
+
children,
|
|
497
|
+
gesture,
|
|
498
|
+
thumbStyle,
|
|
499
|
+
thumbFillStyle,
|
|
500
|
+
onLayout,
|
|
501
|
+
size,
|
|
502
|
+
className,
|
|
503
|
+
thumbClassName,
|
|
504
|
+
label,
|
|
505
|
+
disabled,
|
|
506
|
+
accessibilityValue,
|
|
507
|
+
onAccessibilityAction,
|
|
508
|
+
innerRef
|
|
509
|
+
}) {
|
|
510
|
+
const slots = colorPickerVariants({
|
|
511
|
+
size
|
|
512
|
+
});
|
|
513
|
+
const trackHeight = TRACK_HEIGHT[size];
|
|
514
|
+
const thumbSize = TRACK_THUMB[size];
|
|
515
|
+
return /*#__PURE__*/_jsx(GestureDetector, {
|
|
516
|
+
gesture: gesture,
|
|
517
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
518
|
+
ref: innerRef,
|
|
519
|
+
style: {
|
|
520
|
+
height: thumbSize
|
|
521
|
+
},
|
|
522
|
+
className: "w-full justify-center",
|
|
523
|
+
onLayout: onLayout,
|
|
524
|
+
accessible: true,
|
|
525
|
+
accessibilityRole: "adjustable",
|
|
526
|
+
accessibilityLabel: label,
|
|
527
|
+
accessibilityState: {
|
|
528
|
+
disabled
|
|
529
|
+
},
|
|
530
|
+
accessibilityValue: accessibilityValue,
|
|
531
|
+
accessibilityActions: [{
|
|
532
|
+
name: 'increment'
|
|
533
|
+
}, {
|
|
534
|
+
name: 'decrement'
|
|
535
|
+
}],
|
|
536
|
+
onAccessibilityAction: onAccessibilityAction,
|
|
537
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
538
|
+
style: {
|
|
539
|
+
height: trackHeight
|
|
540
|
+
},
|
|
541
|
+
className: slots.track({
|
|
542
|
+
className
|
|
543
|
+
}),
|
|
544
|
+
children: children
|
|
545
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
546
|
+
pointerEvents: "none",
|
|
547
|
+
style: [thumbStyle, {
|
|
548
|
+
width: thumbSize,
|
|
549
|
+
height: thumbSize
|
|
550
|
+
}],
|
|
551
|
+
className: slots.thumb({
|
|
552
|
+
className: thumbClassName
|
|
553
|
+
}),
|
|
554
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
555
|
+
style: thumbFillStyle,
|
|
556
|
+
className: slots.thumbFill()
|
|
557
|
+
})
|
|
558
|
+
})]
|
|
559
|
+
})
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Maps a touch anywhere on a track to a fraction of it, accounting for the
|
|
565
|
+
* thumb's own width: the thumb travels between the two edges rather than off
|
|
566
|
+
* them, so the usable run is the track minus one thumb.
|
|
567
|
+
*/
|
|
568
|
+
function trackFraction(x, width, thumbSize, sign) {
|
|
569
|
+
'worklet';
|
|
570
|
+
|
|
571
|
+
const along = sign === 1 ? x : width - x;
|
|
572
|
+
const travel = Math.max(width - thumbSize, 1);
|
|
573
|
+
return Math.min(Math.max((along - thumbSize / 2) / travel, 0), 1);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/* ------------------------------------------------------------------ *
|
|
577
|
+
* Hue.
|
|
578
|
+
* ------------------------------------------------------------------ */
|
|
579
|
+
|
|
580
|
+
const ColorPickerHue = /*#__PURE__*/forwardRef(({
|
|
581
|
+
className,
|
|
582
|
+
thumbClassName
|
|
583
|
+
}, ref) => {
|
|
584
|
+
const ctx = useColorPicker('ColorPicker.Hue');
|
|
585
|
+
const sign = useDirectionSign();
|
|
586
|
+
const thumbSize = TRACK_THUMB[ctx.size];
|
|
587
|
+
const width = useSharedValue(0);
|
|
588
|
+
const [announced, setAnnounced] = useState(() => Math.round(ctx.hue.value));
|
|
589
|
+
const settle = useCallback(h => setAnnounced(Math.round(h)), []);
|
|
590
|
+
const apply = (x, commit) => {
|
|
591
|
+
'worklet';
|
|
592
|
+
|
|
593
|
+
const h = trackFraction(x, width.value, thumbSize, sign) * 360;
|
|
594
|
+
ctx.hue.value = h;
|
|
595
|
+
runOnJS(ctx.emit)(h, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value, commit);
|
|
596
|
+
if (commit) {
|
|
597
|
+
runOnJS(settle)(h);
|
|
598
|
+
if (ctx.haptics) runOnJS(selectionTick)();
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
/*
|
|
603
|
+
* A thin horizontal control inside a scroller: the pan waits for six points
|
|
604
|
+
* of sideways travel so a vertical flick starting on the track still
|
|
605
|
+
* scrolls the page, and the tap handles the case where there was no travel
|
|
606
|
+
* at all.
|
|
607
|
+
*/
|
|
608
|
+
const pan = Gesture.Pan().enabled(!ctx.disabled).activeOffsetX([-6, 6]).shouldCancelWhenOutside(false).onUpdate(event => apply(event.x, false)).onFinalize(event => apply(event.x, true));
|
|
609
|
+
const tap = Gesture.Tap().enabled(!ctx.disabled).maxDuration(250).onEnd(event => apply(event.x, true));
|
|
610
|
+
const gesture = Gesture.Race(pan, tap);
|
|
611
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
612
|
+
const travel = Math.max(width.value - thumbSize, 0);
|
|
613
|
+
return {
|
|
614
|
+
transform: [{
|
|
615
|
+
translateX: ctx.hue.value / 360 * travel * sign
|
|
616
|
+
}]
|
|
617
|
+
};
|
|
618
|
+
});
|
|
619
|
+
const thumbFillStyle = useAnimatedStyle(() => ({
|
|
620
|
+
backgroundColor: hsvToCss(ctx.hue.value, 1, 1, 1)
|
|
621
|
+
}));
|
|
622
|
+
const nudge = dir => {
|
|
623
|
+
const next = Math.min(Math.max(ctx.hue.value + dir * HUE_NUDGE, 0), 360);
|
|
624
|
+
ctx.hue.value = withTiming(next, TIMING);
|
|
625
|
+
settle(next);
|
|
626
|
+
ctx.emit(next, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value, true);
|
|
627
|
+
};
|
|
628
|
+
return /*#__PURE__*/_jsx(ChannelTrack, {
|
|
629
|
+
innerRef: ref,
|
|
630
|
+
gesture: gesture,
|
|
631
|
+
thumbStyle: thumbStyle,
|
|
632
|
+
thumbFillStyle: thumbFillStyle,
|
|
633
|
+
onLayout: event => {
|
|
634
|
+
width.value = event.nativeEvent.layout.width;
|
|
635
|
+
},
|
|
636
|
+
size: ctx.size,
|
|
637
|
+
className: className,
|
|
638
|
+
thumbClassName: thumbClassName,
|
|
639
|
+
label: "Hue",
|
|
640
|
+
disabled: ctx.disabled,
|
|
641
|
+
accessibilityValue: {
|
|
642
|
+
min: 0,
|
|
643
|
+
max: 360,
|
|
644
|
+
now: announced,
|
|
645
|
+
text: `${announced}°`
|
|
646
|
+
},
|
|
647
|
+
onAccessibilityAction: event => {
|
|
648
|
+
if (event.nativeEvent.actionName === 'increment') nudge(1);else if (event.nativeEvent.actionName === 'decrement') nudge(-1);
|
|
649
|
+
},
|
|
650
|
+
children: /*#__PURE__*/_jsx(LinearGradient, {
|
|
651
|
+
colors: sign === 1 ? HUE_STOPS : HUE_STOPS_FLIPPED,
|
|
652
|
+
start: {
|
|
653
|
+
x: 0,
|
|
654
|
+
y: 0.5
|
|
655
|
+
},
|
|
656
|
+
end: {
|
|
657
|
+
x: 1,
|
|
658
|
+
y: 0.5
|
|
659
|
+
},
|
|
660
|
+
style: StyleSheet.absoluteFill
|
|
661
|
+
})
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
ColorPickerHue.displayName = 'ColorPicker.Hue';
|
|
665
|
+
|
|
666
|
+
/* ------------------------------------------------------------------ *
|
|
667
|
+
* Alpha.
|
|
668
|
+
* ------------------------------------------------------------------ */
|
|
669
|
+
|
|
670
|
+
const ColorPickerAlpha = /*#__PURE__*/forwardRef(({
|
|
671
|
+
className,
|
|
672
|
+
thumbClassName
|
|
673
|
+
}, ref) => {
|
|
674
|
+
const ctx = useColorPicker('ColorPicker.Alpha');
|
|
675
|
+
const sign = useDirectionSign();
|
|
676
|
+
const thumbSize = TRACK_THUMB[ctx.size];
|
|
677
|
+
const width = useSharedValue(0);
|
|
678
|
+
const [announced, setAnnounced] = useState(() => Math.round(ctx.opacity.value * 100));
|
|
679
|
+
const settle = useCallback(a => setAnnounced(Math.round(a * 100)), []);
|
|
680
|
+
const apply = (x, commit) => {
|
|
681
|
+
'worklet';
|
|
682
|
+
|
|
683
|
+
const a = trackFraction(x, width.value, thumbSize, sign);
|
|
684
|
+
ctx.opacity.value = a;
|
|
685
|
+
runOnJS(ctx.emit)(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, a, commit);
|
|
686
|
+
if (commit) {
|
|
687
|
+
runOnJS(settle)(a);
|
|
688
|
+
if (ctx.haptics) runOnJS(selectionTick)();
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
const pan = Gesture.Pan().enabled(!ctx.disabled).activeOffsetX([-6, 6]).shouldCancelWhenOutside(false).onUpdate(event => apply(event.x, false)).onFinalize(event => apply(event.x, true));
|
|
692
|
+
const tap = Gesture.Tap().enabled(!ctx.disabled).maxDuration(250).onEnd(event => apply(event.x, true));
|
|
693
|
+
const gesture = Gesture.Race(pan, tap);
|
|
694
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
695
|
+
const travel = Math.max(width.value - thumbSize, 0);
|
|
696
|
+
return {
|
|
697
|
+
transform: [{
|
|
698
|
+
translateX: ctx.opacity.value * travel * sign
|
|
699
|
+
}]
|
|
700
|
+
};
|
|
701
|
+
});
|
|
702
|
+
const solidStyle = useAnimatedStyle(() => ({
|
|
703
|
+
backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, 1)
|
|
704
|
+
}));
|
|
705
|
+
const thumbFillStyle = useAnimatedStyle(() => ({
|
|
706
|
+
backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value)
|
|
707
|
+
}));
|
|
708
|
+
const nudge = dir => {
|
|
709
|
+
const next = Math.min(Math.max(ctx.opacity.value + dir * NUDGE, 0), 1);
|
|
710
|
+
ctx.opacity.value = withTiming(next, TIMING);
|
|
711
|
+
settle(next);
|
|
712
|
+
ctx.emit(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, next, true);
|
|
713
|
+
};
|
|
714
|
+
return /*#__PURE__*/_jsxs(ChannelTrack, {
|
|
715
|
+
innerRef: ref,
|
|
716
|
+
gesture: gesture,
|
|
717
|
+
thumbStyle: thumbStyle,
|
|
718
|
+
thumbFillStyle: thumbFillStyle,
|
|
719
|
+
onLayout: event => {
|
|
720
|
+
width.value = event.nativeEvent.layout.width;
|
|
721
|
+
},
|
|
722
|
+
size: ctx.size,
|
|
723
|
+
className: className,
|
|
724
|
+
thumbClassName: thumbClassName,
|
|
725
|
+
label: "Opacity",
|
|
726
|
+
disabled: ctx.disabled,
|
|
727
|
+
accessibilityValue: {
|
|
728
|
+
min: 0,
|
|
729
|
+
max: 100,
|
|
730
|
+
now: announced,
|
|
731
|
+
text: `${announced}%`
|
|
732
|
+
},
|
|
733
|
+
onAccessibilityAction: event => {
|
|
734
|
+
if (event.nativeEvent.actionName === 'increment') nudge(1);else if (event.nativeEvent.actionName === 'decrement') nudge(-1);
|
|
735
|
+
},
|
|
736
|
+
children: [/*#__PURE__*/_jsx(Checkerboard, {}), /*#__PURE__*/_jsx(MaskedView, {
|
|
737
|
+
style: StyleSheet.absoluteFill,
|
|
738
|
+
pointerEvents: "none",
|
|
739
|
+
maskElement: /*#__PURE__*/_jsx(LinearGradient, {
|
|
740
|
+
colors: ['rgba(0,0,0,0)', 'rgba(0,0,0,1)'],
|
|
741
|
+
start: {
|
|
742
|
+
x: sign === 1 ? 0 : 1,
|
|
743
|
+
y: 0.5
|
|
744
|
+
},
|
|
745
|
+
end: {
|
|
746
|
+
x: sign === 1 ? 1 : 0,
|
|
747
|
+
y: 0.5
|
|
748
|
+
},
|
|
749
|
+
style: StyleSheet.absoluteFill
|
|
750
|
+
}),
|
|
751
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
752
|
+
style: [StyleSheet.absoluteFill, solidStyle]
|
|
753
|
+
})
|
|
754
|
+
})]
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
ColorPickerAlpha.displayName = 'ColorPicker.Alpha';
|
|
758
|
+
|
|
759
|
+
/* ------------------------------------------------------------------ *
|
|
760
|
+
* Preview.
|
|
761
|
+
* ------------------------------------------------------------------ */
|
|
762
|
+
|
|
763
|
+
const ColorPickerPreview = /*#__PURE__*/forwardRef(({
|
|
764
|
+
className,
|
|
765
|
+
showValue = false,
|
|
766
|
+
swatchClassName,
|
|
767
|
+
valueClassName,
|
|
768
|
+
children
|
|
769
|
+
}, ref) => {
|
|
770
|
+
const ctx = useColorPicker('ColorPicker.Preview');
|
|
771
|
+
const slots = colorPickerVariants({
|
|
772
|
+
size: ctx.size
|
|
773
|
+
});
|
|
774
|
+
const diameter = PREVIEW_SWATCH[ctx.size];
|
|
775
|
+
const [printed, setPrinted] = useState(() => formatColor({
|
|
776
|
+
h: ctx.hue.value,
|
|
777
|
+
s: ctx.saturation.value,
|
|
778
|
+
v: ctx.brightness.value,
|
|
779
|
+
a: ctx.opacity.value
|
|
780
|
+
}, ctx.format));
|
|
781
|
+
const format = ctx.format;
|
|
782
|
+
useAnimatedReaction(() => formatColor({
|
|
783
|
+
h: ctx.hue.value,
|
|
784
|
+
s: ctx.saturation.value,
|
|
785
|
+
v: ctx.brightness.value,
|
|
786
|
+
a: ctx.opacity.value
|
|
787
|
+
}, format), (current, previous) => {
|
|
788
|
+
if (current !== previous) runOnJS(setPrinted)(current);
|
|
789
|
+
}, [format]);
|
|
790
|
+
const fillStyle = useAnimatedStyle(() => ({
|
|
791
|
+
backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value)
|
|
792
|
+
}));
|
|
793
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
794
|
+
ref: ref,
|
|
795
|
+
className: slots.preview({
|
|
796
|
+
className
|
|
797
|
+
}),
|
|
798
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
799
|
+
style: {
|
|
800
|
+
width: diameter,
|
|
801
|
+
height: diameter
|
|
802
|
+
},
|
|
803
|
+
className: slots.previewSwatch({
|
|
804
|
+
className: swatchClassName
|
|
805
|
+
}),
|
|
806
|
+
children: [/*#__PURE__*/_jsx(Checkerboard, {
|
|
807
|
+
square: 5
|
|
808
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
809
|
+
style: [StyleSheet.absoluteFill, fillStyle]
|
|
810
|
+
})]
|
|
811
|
+
}), showValue ? /*#__PURE__*/_jsx(Text, {
|
|
812
|
+
className: slots.previewValue({
|
|
813
|
+
className: valueClassName
|
|
814
|
+
}),
|
|
815
|
+
children: printed
|
|
816
|
+
}) : null, children]
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
ColorPickerPreview.displayName = 'ColorPicker.Preview';
|
|
820
|
+
|
|
821
|
+
/* ------------------------------------------------------------------ *
|
|
822
|
+
* Swatches.
|
|
823
|
+
* ------------------------------------------------------------------ */
|
|
824
|
+
|
|
825
|
+
const ColorPickerSwatches = /*#__PURE__*/forwardRef(({
|
|
826
|
+
className,
|
|
827
|
+
colors,
|
|
828
|
+
swatchSize,
|
|
829
|
+
swatchClassName
|
|
830
|
+
}, ref) => {
|
|
831
|
+
const ctx = useColorPicker('ColorPicker.Swatches');
|
|
832
|
+
const slots = colorPickerVariants({
|
|
833
|
+
size: ctx.size
|
|
834
|
+
});
|
|
835
|
+
const diameter = swatchSize ?? SWATCH[ctx.size];
|
|
836
|
+
|
|
837
|
+
// Unreadable strings are dropped here rather than rendered as black: a row
|
|
838
|
+
// of presets with a mystery black one in it is worse than a shorter row.
|
|
839
|
+
const parsed = useMemo(() => colors.map(raw => ({
|
|
840
|
+
raw,
|
|
841
|
+
color: parseColor(raw)
|
|
842
|
+
})).filter(entry => entry.color !== null), [colors]);
|
|
843
|
+
return /*#__PURE__*/_jsx(View, {
|
|
844
|
+
ref: ref,
|
|
845
|
+
className: slots.swatches({
|
|
846
|
+
className
|
|
847
|
+
}),
|
|
848
|
+
children: parsed.map((entry, index) => /*#__PURE__*/_jsx(Swatch, {
|
|
849
|
+
color: entry.color,
|
|
850
|
+
diameter: diameter,
|
|
851
|
+
className: swatchClassName,
|
|
852
|
+
ctx: ctx,
|
|
853
|
+
slots: slots
|
|
854
|
+
}, `${entry.raw}-${index}`))
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
ColorPickerSwatches.displayName = 'ColorPicker.Swatches';
|
|
858
|
+
function Swatch({
|
|
859
|
+
color,
|
|
860
|
+
diameter,
|
|
861
|
+
className,
|
|
862
|
+
ctx,
|
|
863
|
+
slots
|
|
864
|
+
}) {
|
|
865
|
+
const hex = hsvToHex(color.h, color.s, color.v, color.a);
|
|
866
|
+
|
|
867
|
+
/*
|
|
868
|
+
* Selected is an exact match on the written colour rather than a distance
|
|
869
|
+
* between two sets of channels. A near-black has an arbitrary hue and a
|
|
870
|
+
* meaningless saturation, so a per-channel comparison marks two visually
|
|
871
|
+
* identical blacks as different and two different hues of black as the same.
|
|
872
|
+
*/
|
|
873
|
+
const ringStyle = useAnimatedStyle(() => {
|
|
874
|
+
const current = hsvToHex(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value);
|
|
875
|
+
return {
|
|
876
|
+
opacity: withTiming(current === hex ? 1 : 0, {
|
|
877
|
+
duration: 120
|
|
878
|
+
})
|
|
879
|
+
};
|
|
880
|
+
});
|
|
881
|
+
const onPress = () => {
|
|
882
|
+
ctx.hue.value = color.h;
|
|
883
|
+
ctx.saturation.value = color.s;
|
|
884
|
+
ctx.brightness.value = color.v;
|
|
885
|
+
ctx.opacity.value = color.a;
|
|
886
|
+
ctx.emit(color.h, color.s, color.v, color.a, true);
|
|
887
|
+
if (ctx.haptics) selectionTick();
|
|
888
|
+
};
|
|
889
|
+
return /*#__PURE__*/_jsxs(Pressable, {
|
|
890
|
+
onPress: onPress,
|
|
891
|
+
disabled: ctx.disabled,
|
|
892
|
+
accessibilityRole: "button",
|
|
893
|
+
accessibilityLabel: hex,
|
|
894
|
+
style: {
|
|
895
|
+
width: diameter,
|
|
896
|
+
height: diameter
|
|
897
|
+
},
|
|
898
|
+
className: slots.swatch({
|
|
899
|
+
className
|
|
900
|
+
}),
|
|
901
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
902
|
+
style: {
|
|
903
|
+
width: diameter,
|
|
904
|
+
height: diameter
|
|
905
|
+
},
|
|
906
|
+
className: "absolute overflow-hidden rounded-full",
|
|
907
|
+
children: [color.a < 1 ? /*#__PURE__*/_jsx(Checkerboard, {
|
|
908
|
+
square: 4
|
|
909
|
+
}) : null, /*#__PURE__*/_jsx(View, {
|
|
910
|
+
style: [StyleSheet.absoluteFill, {
|
|
911
|
+
backgroundColor: hex
|
|
912
|
+
}]
|
|
913
|
+
})]
|
|
914
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
915
|
+
pointerEvents: "none",
|
|
916
|
+
style: [ringStyle, {
|
|
917
|
+
width: diameter + 6,
|
|
918
|
+
height: diameter + 6
|
|
919
|
+
}],
|
|
920
|
+
className: slots.swatchRing()
|
|
921
|
+
})]
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/* ------------------------------------------------------------------ *
|
|
926
|
+
* Field — the strip above the controls: what is being picked, and what
|
|
927
|
+
* it currently is.
|
|
928
|
+
* ------------------------------------------------------------------ */
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* A header for the picker below it.
|
|
932
|
+
*
|
|
933
|
+
* The value it prints is built on the UI thread and only crosses to
|
|
934
|
+
* JavaScript when the rounded string actually changes, the same as `Preview`.
|
|
935
|
+
* A drag through a hundred frames of the same hex costs one render.
|
|
936
|
+
*/
|
|
937
|
+
const ColorPickerField = /*#__PURE__*/forwardRef(({
|
|
938
|
+
className,
|
|
939
|
+
label,
|
|
940
|
+
showValue = true,
|
|
941
|
+
swatchClassName,
|
|
942
|
+
children
|
|
943
|
+
}, ref) => {
|
|
944
|
+
const ctx = useColorPicker('ColorPicker.Field');
|
|
945
|
+
const slots = colorPickerVariants({
|
|
946
|
+
size: ctx.size
|
|
947
|
+
});
|
|
948
|
+
const diameter = FIELD_SWATCH[ctx.size];
|
|
949
|
+
const read = useCallback(() => formatColor({
|
|
950
|
+
h: ctx.hue.value,
|
|
951
|
+
s: ctx.saturation.value,
|
|
952
|
+
v: ctx.brightness.value,
|
|
953
|
+
a: ctx.opacity.value
|
|
954
|
+
}, ctx.format),
|
|
955
|
+
// Shared values are stable objects; `format` is the only real dependency.
|
|
956
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
957
|
+
[ctx.format]);
|
|
958
|
+
const [printed, setPrinted] = useState(read);
|
|
959
|
+
const format = ctx.format;
|
|
960
|
+
useAnimatedReaction(() => formatColor({
|
|
961
|
+
h: ctx.hue.value,
|
|
962
|
+
s: ctx.saturation.value,
|
|
963
|
+
v: ctx.brightness.value,
|
|
964
|
+
a: ctx.opacity.value
|
|
965
|
+
}, format), (current, previous) => {
|
|
966
|
+
if (current !== previous) runOnJS(setPrinted)(current);
|
|
967
|
+
}, [format]);
|
|
968
|
+
const fillStyle = useAnimatedStyle(() => ({
|
|
969
|
+
backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value)
|
|
970
|
+
}));
|
|
971
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
972
|
+
ref: ref,
|
|
973
|
+
className: slots.field({
|
|
974
|
+
className
|
|
975
|
+
})
|
|
976
|
+
// One thing being read out, not three: a label, a value and a swatch
|
|
977
|
+
// announced separately are three stops that each say a third of it.
|
|
978
|
+
,
|
|
979
|
+
accessible: true,
|
|
980
|
+
accessibilityLabel: label ? `${label}, ${printed}` : printed,
|
|
981
|
+
children: [label ? /*#__PURE__*/_jsx(Text, {
|
|
982
|
+
className: slots.fieldLabel(),
|
|
983
|
+
children: label
|
|
984
|
+
}) : null, showValue ? /*#__PURE__*/_jsx(Text, {
|
|
985
|
+
className: slots.fieldValue(),
|
|
986
|
+
children: printed.toUpperCase()
|
|
987
|
+
}) : null, /*#__PURE__*/_jsxs(View, {
|
|
988
|
+
style: {
|
|
989
|
+
width: diameter,
|
|
990
|
+
height: diameter
|
|
991
|
+
},
|
|
992
|
+
className: slots.fieldSwatch({
|
|
993
|
+
className: swatchClassName
|
|
994
|
+
}),
|
|
995
|
+
children: [/*#__PURE__*/_jsx(Checkerboard, {
|
|
996
|
+
square: 4
|
|
997
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
998
|
+
style: [StyleSheet.absoluteFill, fillStyle]
|
|
999
|
+
})]
|
|
1000
|
+
}), children]
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
ColorPickerField.displayName = 'ColorPicker.Field';
|
|
1004
|
+
|
|
1005
|
+
/* ------------------------------------------------------------------ *
|
|
1006
|
+
* Channel — the readout that names the track under it.
|
|
1007
|
+
* ------------------------------------------------------------------ */
|
|
1008
|
+
|
|
1009
|
+
/** Which channel a readout is reading. */
|
|
1010
|
+
|
|
1011
|
+
/** What each channel is called, and how its number is written. */
|
|
1012
|
+
const CHANNEL_LABEL = {
|
|
1013
|
+
hue: 'Hue',
|
|
1014
|
+
saturation: 'Saturation',
|
|
1015
|
+
brightness: 'Brightness',
|
|
1016
|
+
alpha: 'Opacity'
|
|
1017
|
+
};
|
|
1018
|
+
/**
|
|
1019
|
+
* The line above a track: the number on the leading edge, what it names on the
|
|
1020
|
+
* trailing one.
|
|
1021
|
+
*
|
|
1022
|
+
* The number is the one thing in this component that has to reach JavaScript
|
|
1023
|
+
* during a drag, because text cannot be written from a worklet. It is rounded
|
|
1024
|
+
* first and only sent when the rounded value changes, so a slow drag across a
|
|
1025
|
+
* hue track sends 360 updates rather than one per frame.
|
|
1026
|
+
*/
|
|
1027
|
+
const ColorPickerChannelReadout = /*#__PURE__*/forwardRef(({
|
|
1028
|
+
className,
|
|
1029
|
+
channel,
|
|
1030
|
+
label,
|
|
1031
|
+
format
|
|
1032
|
+
}, ref) => {
|
|
1033
|
+
const ctx = useColorPicker('ColorPicker.Channel');
|
|
1034
|
+
const slots = colorPickerVariants({
|
|
1035
|
+
size: ctx.size
|
|
1036
|
+
});
|
|
1037
|
+
const source = channel === 'hue' ? ctx.hue : channel === 'saturation' ? ctx.saturation : channel === 'brightness' ? ctx.brightness : ctx.opacity;
|
|
1038
|
+
const [value, setValue] = useState(() => Math.round(channel === 'hue' ? source.value : source.value * 100));
|
|
1039
|
+
useAnimatedReaction(() => Math.round(channel === 'hue' ? source.value : source.value * 100), (current, previous) => {
|
|
1040
|
+
if (current !== previous) runOnJS(setValue)(current);
|
|
1041
|
+
}, [channel]);
|
|
1042
|
+
const text = format ? format(value) : channel === 'hue' ? `${value}°` : `${value}%`;
|
|
1043
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
1044
|
+
ref: ref,
|
|
1045
|
+
className: slots.channel({
|
|
1046
|
+
className
|
|
1047
|
+
}),
|
|
1048
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
1049
|
+
className: slots.channelValue(),
|
|
1050
|
+
children: text
|
|
1051
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
1052
|
+
className: slots.channelLabel(),
|
|
1053
|
+
children: label ?? CHANNEL_LABEL[channel]
|
|
1054
|
+
})]
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
ColorPickerChannelReadout.displayName = 'ColorPicker.Channel';
|
|
1058
|
+
|
|
1059
|
+
/* ------------------------------------------------------------------ *
|
|
1060
|
+
* Brightness — the third channel, as a track.
|
|
1061
|
+
* ------------------------------------------------------------------ */
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Black to the colour at full brightness.
|
|
1065
|
+
*
|
|
1066
|
+
* The square carries saturation and brightness together, so a picker built
|
|
1067
|
+
* around it never needs this. A wheel carries hue and saturation instead, and
|
|
1068
|
+
* leaves brightness with nowhere to live — this is where it goes.
|
|
1069
|
+
*/
|
|
1070
|
+
const ColorPickerBrightness = /*#__PURE__*/forwardRef(({
|
|
1071
|
+
className,
|
|
1072
|
+
thumbClassName
|
|
1073
|
+
}, ref) => {
|
|
1074
|
+
const ctx = useColorPicker('ColorPicker.Brightness');
|
|
1075
|
+
const sign = useDirectionSign();
|
|
1076
|
+
const thumbSize = TRACK_THUMB[ctx.size];
|
|
1077
|
+
const width = useSharedValue(0);
|
|
1078
|
+
const [announced, setAnnounced] = useState(() => Math.round(ctx.brightness.value * 100));
|
|
1079
|
+
const settle = useCallback(v => setAnnounced(Math.round(v * 100)), []);
|
|
1080
|
+
|
|
1081
|
+
/*
|
|
1082
|
+
* The ramp's far end is the current hue at full brightness, so it has to be
|
|
1083
|
+
* re-declared from React when the hue moves. Unlike the hue ramp — which is
|
|
1084
|
+
* every hue and therefore never changes — this one is only two colours, so
|
|
1085
|
+
* rebuilding it is a two-element array rather than a gradient recompile.
|
|
1086
|
+
*/
|
|
1087
|
+
const [hueEnd, setHueEnd] = useState(() => hsvToCss(ctx.hue.value, ctx.saturation.value, 1, 1));
|
|
1088
|
+
useAnimatedReaction(() => hsvToCss(ctx.hue.value, ctx.saturation.value, 1, 1), (current, previous) => {
|
|
1089
|
+
if (current !== previous) runOnJS(setHueEnd)(current);
|
|
1090
|
+
});
|
|
1091
|
+
const apply = (x, commit) => {
|
|
1092
|
+
'worklet';
|
|
1093
|
+
|
|
1094
|
+
const v = trackFraction(x, width.value, thumbSize, sign);
|
|
1095
|
+
ctx.brightness.value = v;
|
|
1096
|
+
runOnJS(ctx.emit)(ctx.hue.value, ctx.saturation.value, v, ctx.opacity.value, commit);
|
|
1097
|
+
if (commit) {
|
|
1098
|
+
runOnJS(settle)(v);
|
|
1099
|
+
if (ctx.haptics) runOnJS(selectionTick)();
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const pan = Gesture.Pan().enabled(!ctx.disabled).activeOffsetX([-6, 6]).shouldCancelWhenOutside(false).onUpdate(event => apply(event.x, false)).onFinalize(event => apply(event.x, true));
|
|
1103
|
+
const tap = Gesture.Tap().enabled(!ctx.disabled).maxDuration(250).onEnd(event => apply(event.x, true));
|
|
1104
|
+
const gesture = Gesture.Race(pan, tap);
|
|
1105
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
1106
|
+
const travel = Math.max(width.value - thumbSize, 0);
|
|
1107
|
+
return {
|
|
1108
|
+
transform: [{
|
|
1109
|
+
translateX: ctx.brightness.value * travel * sign
|
|
1110
|
+
}]
|
|
1111
|
+
};
|
|
1112
|
+
});
|
|
1113
|
+
const thumbFillStyle = useAnimatedStyle(() => ({
|
|
1114
|
+
backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, 1)
|
|
1115
|
+
}));
|
|
1116
|
+
const nudge = dir => {
|
|
1117
|
+
const next = Math.min(Math.max(ctx.brightness.value + dir * NUDGE, 0), 1);
|
|
1118
|
+
ctx.brightness.value = withTiming(next, TIMING);
|
|
1119
|
+
settle(next);
|
|
1120
|
+
ctx.emit(ctx.hue.value, ctx.saturation.value, next, ctx.opacity.value, true);
|
|
1121
|
+
};
|
|
1122
|
+
return /*#__PURE__*/_jsx(ChannelTrack, {
|
|
1123
|
+
innerRef: ref,
|
|
1124
|
+
gesture: gesture,
|
|
1125
|
+
thumbStyle: thumbStyle,
|
|
1126
|
+
thumbFillStyle: thumbFillStyle,
|
|
1127
|
+
onLayout: event => {
|
|
1128
|
+
width.value = event.nativeEvent.layout.width;
|
|
1129
|
+
},
|
|
1130
|
+
size: ctx.size,
|
|
1131
|
+
className: className,
|
|
1132
|
+
thumbClassName: thumbClassName,
|
|
1133
|
+
label: "Brightness",
|
|
1134
|
+
disabled: ctx.disabled,
|
|
1135
|
+
accessibilityValue: {
|
|
1136
|
+
min: 0,
|
|
1137
|
+
max: 100,
|
|
1138
|
+
now: announced,
|
|
1139
|
+
text: `${announced}%`
|
|
1140
|
+
},
|
|
1141
|
+
onAccessibilityAction: event => {
|
|
1142
|
+
if (event.nativeEvent.actionName === 'increment') nudge(1);else if (event.nativeEvent.actionName === 'decrement') nudge(-1);
|
|
1143
|
+
},
|
|
1144
|
+
children: /*#__PURE__*/_jsx(LinearGradient, {
|
|
1145
|
+
colors: sign === 1 ? ['#000000', hueEnd] : [hueEnd, '#000000'],
|
|
1146
|
+
start: {
|
|
1147
|
+
x: 0,
|
|
1148
|
+
y: 0.5
|
|
1149
|
+
},
|
|
1150
|
+
end: {
|
|
1151
|
+
x: 1,
|
|
1152
|
+
y: 0.5
|
|
1153
|
+
},
|
|
1154
|
+
style: StyleSheet.absoluteFill
|
|
1155
|
+
})
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
ColorPickerBrightness.displayName = 'ColorPicker.Brightness';
|
|
1159
|
+
|
|
1160
|
+
/* ------------------------------------------------------------------ *
|
|
1161
|
+
* Wheel — hue around, saturation out.
|
|
1162
|
+
* ------------------------------------------------------------------ */
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* How many wedges the hue ring is cut into.
|
|
1166
|
+
*
|
|
1167
|
+
* There is no conic gradient here to draw a hue ring with, so it is
|
|
1168
|
+
* approximated by solid wedges. Three degrees each is under a pixel of colour
|
|
1169
|
+
* step at any size this is drawn at, and the wedges are given a hair of
|
|
1170
|
+
* overlap so the seams between them cannot show as lighter lines.
|
|
1171
|
+
*/
|
|
1172
|
+
const WHEEL_WEDGES = 120;
|
|
1173
|
+
const WHEEL_OVERLAP = 0.6;
|
|
1174
|
+
|
|
1175
|
+
/** Radius of the drawing, in the wheel's own viewBox units. */
|
|
1176
|
+
const WHEEL_R = 50;
|
|
1177
|
+
|
|
1178
|
+
/** The hue ring, built once — it is the same wheel in every picker. */
|
|
1179
|
+
const WHEEL_PATHS = Array.from({
|
|
1180
|
+
length: WHEEL_WEDGES
|
|
1181
|
+
}, (_, index) => {
|
|
1182
|
+
const step = 360 / WHEEL_WEDGES;
|
|
1183
|
+
const from = index * step;
|
|
1184
|
+
const to = from + step + WHEEL_OVERLAP;
|
|
1185
|
+
const rad = deg => deg * Math.PI / 180;
|
|
1186
|
+
const x1 = WHEEL_R + WHEEL_R * Math.cos(rad(from));
|
|
1187
|
+
const y1 = WHEEL_R + WHEEL_R * Math.sin(rad(from));
|
|
1188
|
+
const x2 = WHEEL_R + WHEEL_R * Math.cos(rad(to));
|
|
1189
|
+
const y2 = WHEEL_R + WHEEL_R * Math.sin(rad(to));
|
|
1190
|
+
return {
|
|
1191
|
+
d: `M ${WHEEL_R} ${WHEEL_R} L ${x1} ${y1} A ${WHEEL_R} ${WHEEL_R} 0 0 1 ${x2} ${y2} Z`,
|
|
1192
|
+
fill: hsvToCss(from, 1, 1, 1)
|
|
1193
|
+
};
|
|
1194
|
+
});
|
|
1195
|
+
/**
|
|
1196
|
+
* The square, bent into a circle: hue is the angle and saturation is the
|
|
1197
|
+
* distance from the middle.
|
|
1198
|
+
*
|
|
1199
|
+
* It reads the same three shared values as `ColorPicker.Area`, so the two are
|
|
1200
|
+
* interchangeable — a picker is a wheel instead of a square, not a wheel as
|
|
1201
|
+
* well as one. What it does not carry is brightness, which has no third
|
|
1202
|
+
* dimension to occupy here; pair it with `ColorPicker.Brightness`.
|
|
1203
|
+
*
|
|
1204
|
+
* The wheel is deliberately not mirrored under RTL. A track has a start and an
|
|
1205
|
+
* end and so has a direction to read it in; a wheel has neither, and flipping
|
|
1206
|
+
* which way round the spectrum runs would only make the same colour sit
|
|
1207
|
+
* somewhere else.
|
|
1208
|
+
*/
|
|
1209
|
+
const ColorPickerWheel = /*#__PURE__*/forwardRef(({
|
|
1210
|
+
className,
|
|
1211
|
+
size,
|
|
1212
|
+
thumbClassName
|
|
1213
|
+
}, ref) => {
|
|
1214
|
+
const ctx = useColorPicker('ColorPicker.Wheel');
|
|
1215
|
+
const slots = colorPickerVariants({
|
|
1216
|
+
size: ctx.size
|
|
1217
|
+
});
|
|
1218
|
+
const gradientId = useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
1219
|
+
const diameter = size ?? WHEEL_SIZE[ctx.size];
|
|
1220
|
+
const thumbSize = AREA_THUMB[ctx.size];
|
|
1221
|
+
const radius = diameter / 2;
|
|
1222
|
+
const [announced, setAnnounced] = useState(() => ({
|
|
1223
|
+
h: Math.round(ctx.hue.value),
|
|
1224
|
+
s: Math.round(ctx.saturation.value * 100)
|
|
1225
|
+
}));
|
|
1226
|
+
const settle = useCallback((h, s) => {
|
|
1227
|
+
setAnnounced({
|
|
1228
|
+
h: Math.round(h),
|
|
1229
|
+
s: Math.round(s * 100)
|
|
1230
|
+
});
|
|
1231
|
+
}, []);
|
|
1232
|
+
const apply = (x, y, commit) => {
|
|
1233
|
+
'worklet';
|
|
1234
|
+
|
|
1235
|
+
const dx = x - radius;
|
|
1236
|
+
const dy = y - radius;
|
|
1237
|
+
// Clockwise from three o'clock, which is what `atan2` already gives in a
|
|
1238
|
+
// coordinate space whose y runs downwards — so the wedges above and the
|
|
1239
|
+
// touch here agree without either of them correcting for the other.
|
|
1240
|
+
const degrees = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
1241
|
+
const h = (degrees + 360) % 360;
|
|
1242
|
+
// Past the rim the colour is the one on the rim, rather than nothing:
|
|
1243
|
+
// a finger that overshoots the edge of a disc has still chosen a hue.
|
|
1244
|
+
const s = Math.min(Math.sqrt(dx * dx + dy * dy) / radius, 1);
|
|
1245
|
+
ctx.hue.value = h;
|
|
1246
|
+
ctx.saturation.value = s;
|
|
1247
|
+
runOnJS(ctx.emit)(h, s, ctx.brightness.value, ctx.opacity.value, commit);
|
|
1248
|
+
if (commit) {
|
|
1249
|
+
runOnJS(settle)(h, s);
|
|
1250
|
+
if (ctx.haptics) runOnJS(selectionTick)();
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
const pan = Gesture.Pan().enabled(!ctx.disabled).minDistance(0).shouldCancelWhenOutside(false).onBegin(event => apply(event.x, event.y, false)).onUpdate(event => apply(event.x, event.y, false)).onFinalize(event => apply(event.x, event.y, true));
|
|
1254
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
1255
|
+
const angle = ctx.hue.value * Math.PI / 180;
|
|
1256
|
+
const r = ctx.saturation.value * radius;
|
|
1257
|
+
return {
|
|
1258
|
+
transform: [{
|
|
1259
|
+
translateX: radius + r * Math.cos(angle) - thumbSize / 2
|
|
1260
|
+
}, {
|
|
1261
|
+
translateY: radius + r * Math.sin(angle) - thumbSize / 2
|
|
1262
|
+
}]
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
const thumbFillStyle = useAnimatedStyle(() => ({
|
|
1266
|
+
backgroundColor: hsvToCss(ctx.hue.value, ctx.saturation.value, ctx.brightness.value, 1)
|
|
1267
|
+
}));
|
|
1268
|
+
|
|
1269
|
+
// Brightness is not drawn into the wheel — it darkens the whole of it,
|
|
1270
|
+
// which is what brightness does.
|
|
1271
|
+
const dimStyle = useAnimatedStyle(() => ({
|
|
1272
|
+
opacity: 1 - ctx.brightness.value
|
|
1273
|
+
}));
|
|
1274
|
+
const nudge = (channel, dir) => {
|
|
1275
|
+
if (channel === 'hue') {
|
|
1276
|
+
const next = (ctx.hue.value + dir * HUE_NUDGE + 360) % 360;
|
|
1277
|
+
ctx.hue.value = next;
|
|
1278
|
+
settle(next, ctx.saturation.value);
|
|
1279
|
+
ctx.emit(next, ctx.saturation.value, ctx.brightness.value, ctx.opacity.value, true);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const next = Math.min(Math.max(ctx.saturation.value + dir * NUDGE, 0), 1);
|
|
1283
|
+
ctx.saturation.value = withTiming(next, TIMING);
|
|
1284
|
+
settle(ctx.hue.value, next);
|
|
1285
|
+
ctx.emit(ctx.hue.value, next, ctx.brightness.value, ctx.opacity.value, true);
|
|
1286
|
+
};
|
|
1287
|
+
const onAccessibilityAction = event => {
|
|
1288
|
+
const action = event.nativeEvent.actionName;
|
|
1289
|
+
if (action === 'increment') nudge('hue', 1);else if (action === 'decrement') nudge('hue', -1);else if (action === 'saturate') nudge('saturation', 1);else if (action === 'desaturate') nudge('saturation', -1);
|
|
1290
|
+
};
|
|
1291
|
+
return /*#__PURE__*/_jsx(GestureDetector, {
|
|
1292
|
+
gesture: pan,
|
|
1293
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
1294
|
+
ref: ref,
|
|
1295
|
+
style: {
|
|
1296
|
+
width: diameter,
|
|
1297
|
+
height: diameter
|
|
1298
|
+
},
|
|
1299
|
+
className: "self-center",
|
|
1300
|
+
accessible: true,
|
|
1301
|
+
accessibilityRole: "adjustable",
|
|
1302
|
+
accessibilityLabel: "Hue and saturation",
|
|
1303
|
+
accessibilityState: {
|
|
1304
|
+
disabled: ctx.disabled
|
|
1305
|
+
},
|
|
1306
|
+
accessibilityValue: {
|
|
1307
|
+
min: 0,
|
|
1308
|
+
max: 360,
|
|
1309
|
+
now: announced.h,
|
|
1310
|
+
text: `${announced.h}°, ${announced.s}% saturation`
|
|
1311
|
+
},
|
|
1312
|
+
accessibilityActions: [{
|
|
1313
|
+
name: 'increment'
|
|
1314
|
+
}, {
|
|
1315
|
+
name: 'decrement'
|
|
1316
|
+
}, {
|
|
1317
|
+
name: 'saturate',
|
|
1318
|
+
label: 'More saturated'
|
|
1319
|
+
}, {
|
|
1320
|
+
name: 'desaturate',
|
|
1321
|
+
label: 'Less saturated'
|
|
1322
|
+
}],
|
|
1323
|
+
onAccessibilityAction: onAccessibilityAction,
|
|
1324
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
1325
|
+
style: {
|
|
1326
|
+
width: diameter,
|
|
1327
|
+
height: diameter
|
|
1328
|
+
},
|
|
1329
|
+
className: slots.wheel({
|
|
1330
|
+
className
|
|
1331
|
+
}),
|
|
1332
|
+
children: [/*#__PURE__*/_jsxs(Svg, {
|
|
1333
|
+
width: "100%",
|
|
1334
|
+
height: "100%",
|
|
1335
|
+
viewBox: "0 0 100 100",
|
|
1336
|
+
children: [/*#__PURE__*/_jsx(Defs, {
|
|
1337
|
+
children: /*#__PURE__*/_jsxs(RadialGradient, {
|
|
1338
|
+
id: gradientId,
|
|
1339
|
+
cx: "50%",
|
|
1340
|
+
cy: "50%",
|
|
1341
|
+
r: "50%",
|
|
1342
|
+
children: [/*#__PURE__*/_jsx(Stop, {
|
|
1343
|
+
offset: "0%",
|
|
1344
|
+
stopColor: "#ffffff",
|
|
1345
|
+
stopOpacity: 1
|
|
1346
|
+
}), /*#__PURE__*/_jsx(Stop, {
|
|
1347
|
+
offset: "100%",
|
|
1348
|
+
stopColor: "#ffffff",
|
|
1349
|
+
stopOpacity: 0
|
|
1350
|
+
})]
|
|
1351
|
+
})
|
|
1352
|
+
}), WHEEL_PATHS.map((wedge, index) => /*#__PURE__*/_jsx(Path, {
|
|
1353
|
+
d: wedge.d,
|
|
1354
|
+
fill: wedge.fill
|
|
1355
|
+
}, index)), /*#__PURE__*/_jsx(Circle, {
|
|
1356
|
+
cx: WHEEL_R,
|
|
1357
|
+
cy: WHEEL_R,
|
|
1358
|
+
r: WHEEL_R,
|
|
1359
|
+
fill: `url(#${gradientId})`
|
|
1360
|
+
})]
|
|
1361
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
1362
|
+
pointerEvents: "none",
|
|
1363
|
+
style: [StyleSheet.absoluteFill, dimStyle],
|
|
1364
|
+
className: "bg-black"
|
|
1365
|
+
})]
|
|
1366
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
1367
|
+
pointerEvents: "none",
|
|
1368
|
+
style: [thumbStyle, {
|
|
1369
|
+
width: thumbSize,
|
|
1370
|
+
height: thumbSize
|
|
1371
|
+
}],
|
|
1372
|
+
className: slots.thumb({
|
|
1373
|
+
className: thumbClassName
|
|
1374
|
+
}),
|
|
1375
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
1376
|
+
style: thumbFillStyle,
|
|
1377
|
+
className: slots.thumbFill()
|
|
1378
|
+
})
|
|
1379
|
+
})]
|
|
1380
|
+
})
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
1383
|
+
ColorPickerWheel.displayName = 'ColorPicker.Wheel';
|
|
1384
|
+
export const ColorPicker = Object.assign(ColorPickerRoot, {
|
|
1385
|
+
Field: ColorPickerField,
|
|
1386
|
+
Area: ColorPickerArea,
|
|
1387
|
+
Wheel: ColorPickerWheel,
|
|
1388
|
+
Channel: ColorPickerChannelReadout,
|
|
1389
|
+
Hue: ColorPickerHue,
|
|
1390
|
+
Brightness: ColorPickerBrightness,
|
|
1391
|
+
Alpha: ColorPickerAlpha,
|
|
1392
|
+
Preview: ColorPickerPreview,
|
|
1393
|
+
Swatches: ColorPickerSwatches
|
|
1394
|
+
});
|
|
1395
|
+
//# sourceMappingURL=index.js.map
|