Skip to content

Commit

Permalink
feat: add color picker context and hue slider
Browse files Browse the repository at this point in the history
  • Loading branch information
tigranpetrossian committed Aug 4, 2024
1 parent 5cf379a commit ddf7d59
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 1 deletion.
82 changes: 82 additions & 0 deletions figma-kit/src/components/color-picker/color-picker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
.fp-ColorPickerHueSlider {
}

.fp-ColorPickerHueSlider:where(.fp-color-model-hsv, .fp-color-model-hsl) {
--slider-track-bg: linear-gradient(
90deg,
rgba(255, 0, 0, 1) calc(var(--slider-track-size) / 2),
rgba(255, 48, 0, 1),
rgba(255, 96, 0, 1),
rgba(255, 143, 0, 1),
rgba(255, 191, 0, 1),
rgba(255, 239, 0, 1),
rgba(223, 255, 0, 1),
rgba(175, 255, 0, 1),
rgba(128, 255, 0, 1),
rgba(80, 255, 0, 1),
rgba(32, 255, 0, 1),
rgba(0, 255, 16, 1),
rgba(0, 255, 64, 1),
rgba(0, 255, 112, 1),
rgba(0, 255, 159, 1),
rgba(0, 255, 207, 1),
rgba(0, 255, 255, 1),
rgba(0, 207, 255, 1),
rgba(0, 159, 255, 1),
rgba(0, 112, 255, 1),
rgba(0, 64, 255, 1),
rgba(0, 16, 255, 1),
rgba(32, 0, 255, 1),
rgba(80, 0, 255, 1),
rgba(127, 0, 255, 1),
rgba(175, 0, 255, 1),
rgba(223, 0, 255, 1),
rgba(255, 0, 239, 1),
rgba(255, 0, 191, 1),
rgba(255, 0, 143, 1),
rgba(255, 0, 96, 1),
rgba(255, 0, 48, 1) calc(100% - calc(var(--slider-track-size) / 2))
);
}

@supports (color: color(display-p3 1 1 1)) {
@media (color-gamut: p3) {
.fp-ColorPickerHueSlider:where(.fp-color-model-hsv, .fp-color-model-hsl):where(.fp-color-space-display-p3) {
--slider-track-bg: linear-gradient(
90deg,
color(display-p3 1 0 0) calc(var(--slider-track-size) / 2),
color(display-p3 1 0.1875 0),
color(display-p3 1 0.375 0),
color(display-p3 1 0.5625 0),
color(display-p3 1 0.75 0),
color(display-p3 1 0.9375 0),
color(display-p3 0.875 1 0),
color(display-p3 0.6875 1 0),
color(display-p3 0.5 1 0),
color(display-p3 0.3125 1 0),
color(display-p3 0.125 1 0),
color(display-p3 0 1 0.0625),
color(display-p3 0 1 0.25),
color(display-p3 0 1 0.4375),
color(display-p3 0 1 0.625),
color(display-p3 0 1 0.8125),
color(display-p3 0 1 1),
color(display-p3 0 0.8125 1),
color(display-p3 0 0.625 1),
color(display-p3 0 0.4375 1),
color(display-p3 0 0.25 1),
color(display-p3 0 0.0625 1),
color(display-p3 0.125 0 1),
color(display-p3 0.3125 0 1),
color(display-p3 0.5 0 1),
color(display-p3 0.6875 0 1),
color(display-p3 0.875 0 1),
color(display-p3 1 0 0.9375),
color(display-p3 1 0 0.75),
color(display-p3 1 0 0.5625),
color(display-p3 1 0 0.375),
color(display-p3 1 0 0.1875) calc(100% - calc(var(--slider-track-size) / 2))
);
}
}
}
29 changes: 29 additions & 0 deletions figma-kit/src/components/color-picker/color-picker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import * as ColorPicker from '@components/color-picker';
import { Flex } from '@components/flex';

const Picker = () => {
return <div></div>;
};

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: 1, b: 0, a: 1 });

return (
<ColorPicker.Root colorModel="hsv" colorSpace="srgb" color={color} onColorChange={setColor}>
<Flex style={{ width: 240 }}>
<ColorPicker.Hue />
</Flex>
</ColorPicker.Root>
);
};
41 changes: 41 additions & 0 deletions figma-kit/src/components/color-picker/color-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createContext } from '@lib/react/create-context';
import type { RGBA } from '@lib/color';
import { useControllableState } from '@lib/react/use-controllable-state';

type ColorModel = 'hsv' | 'hsl' | 'hex' | 'rgb';
type ColorSpace = 'srgb' | 'display-p3';

const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 };

const [ColorPickerContextProvider, useColorPickerContext] = createContext<{
colorSpace: ColorSpace;
colorModel: ColorModel;
color: RGBA;
onColorChange: (color: RGBA) => void;
}>('ColorPicker');

type RootProps = {
colorSpace: ColorSpace;
colorModel: ColorModel;
color?: RGBA;
onColorChange?: (color: RGBA) => void;
children: React.ReactNode;
};

const Root = (props: RootProps) => {
const { colorModel, colorSpace, children, color: colorProp, onColorChange } = props;
const [color = DEFAULT_COLOR, setColor] = useControllableState({
prop: colorProp,
defaultProp: colorProp,
onChange: onColorChange,
});

return (
<ColorPickerContextProvider colorModel={colorModel} colorSpace={colorSpace} color={color} onColorChange={setColor}>
{children}
</ColorPickerContextProvider>
);
};

export type { RootProps, ColorModel, ColorSpace };
export { Root, useColorPickerContext };
62 changes: 62 additions & 0 deletions figma-kit/src/components/color-picker/hue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { CSSProperties } from 'react';
import { Slider } from '@components/slider';
import type { ColorModel, ColorSpace } from '@components/color-picker/color-picker';
import { useColorPickerContext } from '@components/color-picker/color-picker';
import type { RGBA } from '@lib/color';
import { hsvaToRgba, rgbaToHsva } from '@lib/color';

type HueProps = React.ComponentPropsWithoutRef<'div'>;

const Hue = (props: HueProps) => {
const { className, style } = props;
const { colorModel, colorSpace, color, onColorChange } = useColorPickerContext('Hue');
const strategy = colorModelStrategies[colorModel];
const handleValueChange = (value: number[]) => {
const newColor = strategy.setHue(color, value[0]);
onColorChange(newColor);
};
const hue = strategy.getHue(color);
const thumbColor = strategy.getThumbColor(hue, colorSpace);

return (
<Slider
range={false}
min={0}
max={360}
value={[hue]}
onValueChange={handleValueChange}
className={`fp-ColorPickerHueSlider fp-color-model-${colorModel} fp-color-space-${colorSpace} ${className}`}
style={{ '--slider-thumb-bg': thumbColor, ...style } as CSSProperties}
/>
);
};

type ColorModelStrategy = {
getHue: (color: RGBA) => number;
setHue: (color: RGBA, hue: number) => RGBA;
getThumbColor: (hue: number, colorSpace: ColorSpace) => string;
};

const standardModelStrategy: ColorModelStrategy = {
getHue: (color) => Math.round(rgbaToHsva(color).h),
setHue: (color, hue) => {
const { s, v } = rgbaToHsva(color);
return hsvaToRgba({ h: hue, s, v, a: color.a });
},
getThumbColor: (hue, colorSpace) => {
const { r, g, b } = hsvaToRgba({ h: hue, s: 100, v: 100, a: 1 });
return colorSpace === 'display-p3'
? `color(display-p3 ${+r.toFixed(4)} ${+g.toFixed(4)} ${+b.toFixed(4)})`
: `rgb(${Math.round(r * 255)} ${Math.round(g * 255)} ${Math.round(b * 255)})`;
},
};

const colorModelStrategies: Record<ColorModel, ColorModelStrategy> = {
rgb: standardModelStrategy,
hsl: standardModelStrategy,
hsv: standardModelStrategy,
hex: standardModelStrategy,
};

export type { HueProps };
export { Hue };
2 changes: 2 additions & 0 deletions figma-kit/src/components/color-picker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './color-picker';
export * from './hue';
2 changes: 1 addition & 1 deletion figma-kit/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +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';
@import '../components/color-picker/color-picker.css';

0 comments on commit ddf7d59

Please sign in to comment.