Skip to content

Commit

Permalink
feat: wip add picker
Browse files Browse the repository at this point in the history
  • Loading branch information
tigranpetrossian committed Aug 4, 2024
1 parent ffeeef6 commit e9f21b5
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 0 deletions.
62 changes: 62 additions & 0 deletions figma-kit/src/components/color/color.css
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);
}
}
92 changes: 92 additions & 0 deletions figma-kit/src/components/color/color.stories.tsx
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>
</>
);
};
1 change: 1 addition & 0 deletions figma-kit/src/components/color/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './picker';
165 changes: 165 additions & 0 deletions figma-kit/src/components/color/picker.tsx
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 };
1 change: 1 addition & 0 deletions figma-kit/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@
@import '../components/radio-group/radio-group.css';
@import '../components/segmented-control/segmented-control.css';
@import '../components/collapsible/collapsible.css';
@import '../components/color/color.css';

0 comments on commit e9f21b5

Please sign in to comment.