-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ffeeef6
commit e9f21b5
Showing
5 changed files
with
321 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
.fp-ColorPicker { | ||
width: 240px; | ||
height: 240px; | ||
outline-width: 1px; | ||
outline-style: solid; | ||
outline-color: #0000001a; /*TODO: color-bordertranslucent*/ | ||
outline-offset: -1px; | ||
--hue-color: red; | ||
--gradient-step-black: rgb(0 0 0); | ||
--gradient-step-black-transparent: rgb(0 0 0 / 0); | ||
--gradient-step-white: rgb(255 255 255); | ||
--gradient-step-white-transparent: rgb(255 255 255 / 0); | ||
--gradient-step-gray: rgb(128 128 128); | ||
--gradient-step-gray-transparent: rgb(128 128 128 / 0); | ||
|
||
@supports (color: color(display-p3 1 1 1)) { | ||
@media (color-gamut: p3) { | ||
--hue-color: color(display-p3 1 0 0); | ||
--gradient-step-black: color(display-p3 0 0 0); | ||
--gradient-step-black-transparent: color(display-p3 0 0 0 / 0); | ||
--gradient-step-white: color(display-p3 1 1 1); | ||
--gradient-step-white-transparent: color(display-p3 1 1 1 / 0); | ||
--gradient-step-gray: color(display-p3 0.5 0.5 0.5); | ||
--gradient-step-gray-transparent: color(display-p3 0.5 0.5 0.5 / 0); | ||
} | ||
} | ||
} | ||
.fp-ColorPicker:where(.fp-model-hsva) { | ||
background: linear-gradient(to bottom, var(--gradient-step-black-transparent) 0%, var(--gradient-step-black) 100%), | ||
linear-gradient(to right, var(--gradient-step-white) 0%, var(--hue-color) 100%); | ||
} | ||
|
||
.fp-ColorPicker:where(.fp-model-hsla) { | ||
background: linear-gradient( | ||
to bottom, | ||
var(--gradient-step-white) 0%, | ||
var(--gradient-step-white-transparent) 49.99%, | ||
var(--gradient-step-black-transparent) 50.01%, | ||
var(--gradient-step-black) 100% | ||
), | ||
linear-gradient(to right, var(--gradient-step-gray) 0%, var(--gradient-step-gray-transparent) 100%), | ||
var(--hue-color); | ||
} | ||
|
||
.fp-ColorPickerThumb { | ||
position: absolute; | ||
box-sizing: border-box; | ||
box-shadow: var(--elevation-200); | ||
left: 0; | ||
transform: translate(-50%, -50%); | ||
width: 16px; | ||
height: 16px; | ||
border-radius: 8px; | ||
border-style: solid; | ||
border-width: 4px; | ||
border-color: white; | ||
background: black; | ||
|
||
&:focus-visible { | ||
outline: 1px solid var(--figma-color-border-selected); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState } from 'react'; | ||
import { Flex } from '@components/flex'; | ||
import * as ValueField from '@components/value-field'; | ||
import * as Select from '@components/select'; | ||
import { Slider } from '@components/slider'; | ||
import { Picker } from './picker'; | ||
import type { ColorModel, ColorSpace } from './picker'; | ||
|
||
type Story = StoryObj<typeof Picker>; | ||
|
||
const meta: Meta<typeof Picker> = { | ||
title: 'Components/Color', | ||
component: Picker, | ||
}; | ||
|
||
export default meta; | ||
|
||
export const Story = () => { | ||
const [color, setColor] = useState({ r: 0, g: 0, b: 0, a: 1 }); | ||
const [model, setModel] = useState<ColorModel>('hsva'); | ||
const [colorSpace, setColorSpace] = useState<ColorSpace>('srgb'); | ||
|
||
const handleAlphaChange = (value: number) => { | ||
setColor((color) => ({ | ||
...color, | ||
a: value, | ||
})); | ||
}; | ||
|
||
const handleModelChange = (model: 'hsva' | 'hsla') => { | ||
setModel(model); | ||
}; | ||
|
||
const handleColorSpaceChange = (colorSpace: 'srgb' | 'display-p3') => { | ||
setColorSpace(colorSpace); | ||
}; | ||
|
||
return ( | ||
<> | ||
<Select.Root value={colorSpace} onValueChange={handleColorSpaceChange}> | ||
<Select.Trigger style={{ width: 128, position: 'fixed', top: 24, left: 24 }} /> | ||
<Select.Content> | ||
<Select.Item value="srgb">sRGB</Select.Item> | ||
<Select.Item value="display-p3">Display P3</Select.Item> | ||
</Select.Content> | ||
</Select.Root> | ||
<Flex direction="column" gap="3" style={{ width: 240 }}> | ||
<Picker color={color} model={model} colorSpace="srgb" onColorChange={setColor} /> | ||
<Flex direction="column" gap="3" style={{ padding: '0 16px' }}> | ||
<Slider min={0} max={360} range={false} /> | ||
<Flex gap="2"> | ||
<Select.Root value={model} onValueChange={handleModelChange}> | ||
<Select.Trigger style={{ width: 64 }} /> | ||
<Select.Content> | ||
<Select.Item value="hsva">HSB</Select.Item> | ||
<Select.Item value="hsla">HSL</Select.Item> | ||
</Select.Content> | ||
</Select.Root> | ||
|
||
<ValueField.Multi style={{ width: 168 }}> | ||
<ValueField.Root> | ||
{/* <div | ||
style={{ | ||
width: 14, | ||
height: 14, | ||
margin: '5px 0 5px 4px', | ||
borderRadius: '2px', | ||
backgroundColor: `rgb(${color.r * 255} ${color.g * 255} ${color.b * 255} / ${Math.round(color.a * 100)}%)`, | ||
}} | ||
/>*/} | ||
<ValueField.Hex value={color} onChange={setColor} /> | ||
</ValueField.Root> | ||
<ValueField.Root> | ||
<ValueField.Numeric | ||
style={{ width: 32 }} | ||
value={color.a} | ||
onChange={handleAlphaChange} | ||
min={0} | ||
max={1} | ||
targetRange={[0, 100]} | ||
precision={2} | ||
/> | ||
<ValueField.Label>%</ValueField.Label> | ||
</ValueField.Root> | ||
</ValueField.Multi> | ||
</Flex> | ||
</Flex> | ||
</Flex> | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './picker'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
import React, { useState } from 'react'; | ||
import { hslaToRgba, hsvaToRgba, RGBA, rgbaToHsla, rgbaToHsva } from '@lib/color'; | ||
import { normalize } from '@lib/number/normalize'; | ||
import { clamp, pipe } from 'remeda'; | ||
|
||
const CURSOR_VERTICAL_OFFSET = 4; | ||
const PICKER_SIZE = 240; | ||
|
||
type HXYA = { | ||
h: number; | ||
x: number; | ||
y: number; | ||
a: number; | ||
}; | ||
|
||
type ThumbPosition = { | ||
x: number; | ||
y: number; | ||
}; | ||
|
||
type ColorModel = 'hsva' | 'hsla'; | ||
type ColorSpace = 'srgb' | 'display-p3'; | ||
|
||
type PickerProps = { | ||
color: RGBA; | ||
model: ColorModel; | ||
colorSpace: ColorSpace; | ||
onColorChange: (color: RGBA) => void; | ||
}; | ||
|
||
const Picker = (props: PickerProps) => { | ||
const { model, color, colorSpace, onColorChange } = props; | ||
const [editingHxya, setEditingHxya] = useState<HXYA | null>(null); | ||
const hxya = editingHxya ?? formatter[model](color); | ||
|
||
const handlePointerDown = (event: React.PointerEvent) => { | ||
const targetRect = event.currentTarget.getBoundingClientRect(); | ||
event.currentTarget.setPointerCapture(event.pointerId); | ||
const newHxya = { | ||
h: hxya.h, | ||
x: pipe(event.clientX - targetRect.x, normalize([0, PICKER_SIZE], [0, 100]), clamp({ min: 0, max: 100 })), | ||
y: pipe( | ||
event.clientY - targetRect.y, | ||
normalize([0, PICKER_SIZE], [0, 100]), | ||
(value) => value - CURSOR_VERTICAL_OFFSET, | ||
clamp({ min: 0, max: 100 }) | ||
), | ||
a: hxya.a, | ||
}; | ||
setEditingHxya(newHxya); | ||
onColorChange(parser[model](newHxya)); | ||
}; | ||
|
||
const handlePointerMove = (event: React.PointerEvent) => { | ||
if (!event.currentTarget.hasPointerCapture(event.pointerId)) { | ||
return; | ||
} | ||
const targetRect = event.currentTarget.getBoundingClientRect(); | ||
const newHxya = { | ||
h: hxya.h, | ||
x: pipe(event.clientX - targetRect.x, normalize([0, PICKER_SIZE], [0, 100]), clamp({ min: 0, max: 100 })), | ||
y: pipe( | ||
event.clientY - targetRect.y, | ||
normalize([0, 240], [0, 100]), | ||
(value) => value - CURSOR_VERTICAL_OFFSET, | ||
clamp({ min: 0, max: 100 }) | ||
), | ||
a: hxya.a, | ||
}; | ||
setEditingHxya(newHxya); | ||
onColorChange(parser[model](newHxya)); | ||
}; | ||
|
||
const handlePointerUp = (event: React.PointerEvent) => { | ||
setEditingHxya(null); | ||
}; | ||
|
||
return ( | ||
<div | ||
onPointerDown={handlePointerDown} | ||
onPointerMove={handlePointerMove} | ||
onPointerUp={handlePointerUp} | ||
style={{ position: 'relative' }} | ||
> | ||
<Thumb color={color} position={hxya} /> | ||
<Canvas color={color} model={model} colorSpace={colorSpace} /> | ||
</div> | ||
); | ||
}; | ||
|
||
type CanvasProps = { | ||
color: RGBA; | ||
model: ColorModel; | ||
colorSpace: ColorSpace; | ||
}; | ||
|
||
const Canvas = (props: CanvasProps) => { | ||
const { color, model, colorSpace } = props; | ||
return <div className={`fp-ColorPicker fp-model-${model}`} />; | ||
}; | ||
|
||
type ThumbProps = { | ||
color: RGBA; | ||
position: ThumbPosition; | ||
}; | ||
|
||
const Thumb = (props: ThumbProps) => { | ||
const { color, position } = props; | ||
|
||
return ( | ||
<div | ||
tabIndex={0} | ||
className="fp-ColorPickerThumb" | ||
style={{ | ||
left: `${position.x}%`, | ||
top: `${position.y}%`, | ||
}} | ||
/> | ||
); | ||
}; | ||
|
||
type Formatter = Record<ColorModel, (color: RGBA) => HXYA>; | ||
|
||
const formatter: Formatter = { | ||
hsva: (color: RGBA) => { | ||
const { h, s, v, a } = rgbaToHsva(color); | ||
return { | ||
h, | ||
x: s, | ||
y: 100 - v, | ||
a, | ||
}; | ||
}, | ||
hsla: (color: RGBA) => { | ||
const { h, s, l, a } = rgbaToHsla(color); | ||
return { | ||
h, | ||
x: s, | ||
y: 100 - l, | ||
a, | ||
}; | ||
}, | ||
}; | ||
|
||
type Parser = Record<ColorModel, (hxya: HXYA) => RGBA>; | ||
|
||
const parser: Parser = { | ||
hsva: (hxya) => | ||
hsvaToRgba({ | ||
h: hxya.h, | ||
s: hxya.x, | ||
v: 100 - hxya.y, | ||
a: hxya.a, | ||
}), | ||
hsla: (hxya) => | ||
hslaToRgba({ | ||
h: hxya.h, | ||
s: hxya.x, | ||
l: 100 - hxya.y, | ||
a: hxya.a, | ||
}), | ||
}; | ||
|
||
export type { PickerProps, ColorSpace, ColorModel }; | ||
export { Picker }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters