mirror of
https://github.com/directus/directus.git
synced 2026-02-19 10:14:33 -05:00
Color Interface (#531)
* styling for color inputs....messy * values line up * color swatch * color picker working html * hsl * hsl works! * added preset option kinda (needs repeater) * added color preset colors pretty * Fix input up/down * Remove empty test file * Remove deprecated prop on v-menu * fixed portal and click outside * Add color placeholder text * Emit color value on every type of change * Rename middleware * Remove empty readme Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -27,16 +27,16 @@
|
||||
/>
|
||||
</slot>
|
||||
<span v-if="suffix" class="suffix">{{ suffix }}</span>
|
||||
<span v-if="(type === 'number')">
|
||||
<span v-if="type === 'number' && !hideArrows">
|
||||
<v-icon
|
||||
:class="{ disabled: value >= max }"
|
||||
:class="{ disabled: max !== null && parseInt(value, 10) >= max }"
|
||||
name="keyboard_arrow_up"
|
||||
class="step-up"
|
||||
@click="stepUp"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<v-icon
|
||||
:class="{ disabled: value <= min }"
|
||||
:class="{ disabled: min !== null && parseInt(value, 10) <= min }"
|
||||
name="keyboard_arrow_down"
|
||||
class="step-down"
|
||||
@click="stepDown"
|
||||
@@ -97,6 +97,10 @@ export default defineComponent({
|
||||
default: 'text',
|
||||
},
|
||||
// For number inputs only
|
||||
hideArrows: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null,
|
||||
@@ -190,20 +194,26 @@ export default defineComponent({
|
||||
function stepUp() {
|
||||
if (!input.value) return;
|
||||
if (props.disabled === true) return;
|
||||
if (props.max !== null && props.value >= props.max) return;
|
||||
|
||||
if (props.value < props.max) {
|
||||
input.value.stepUp();
|
||||
emit('input', input.value.value ?? props.min ?? 0);
|
||||
input.value.stepUp();
|
||||
|
||||
if (input.value.value) {
|
||||
return emit('input', input.value.value);
|
||||
}
|
||||
}
|
||||
|
||||
function stepDown() {
|
||||
if (!input.value) return;
|
||||
if (props.disabled === true) return;
|
||||
if (props.min !== null && props.value <= props.min) return;
|
||||
|
||||
if (props.value > props.min) {
|
||||
input.value.stepDown();
|
||||
emit('input', input.value.value);
|
||||
input.value.stepDown();
|
||||
|
||||
if (input.value.value) {
|
||||
return emit('input', input.value.value);
|
||||
} else {
|
||||
return emit('input', props.min || 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
v-if="isActive"
|
||||
v-click-outside="{
|
||||
handler: deactivate,
|
||||
middleware: onClickOutsideMiddleware,
|
||||
disabled: isActive === false || closeOnClick === false,
|
||||
events: ['click'],
|
||||
}"
|
||||
@@ -129,6 +130,7 @@ export default defineComponent({
|
||||
toggle,
|
||||
deactivate,
|
||||
onContentClick,
|
||||
onClickOutsideMiddleware,
|
||||
styles,
|
||||
arrowStyles,
|
||||
popperPlacement,
|
||||
@@ -176,6 +178,10 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
function onClickOutsideMiddleware(e: Event) {
|
||||
return !activator.value?.contains(e.target as Node);
|
||||
}
|
||||
|
||||
function onContentClick() {
|
||||
if (props.closeOnContentClick === true) {
|
||||
deactivate();
|
||||
|
||||
54
src/interfaces/color/color.story.ts
Normal file
54
src/interfaces/color/color.story.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import withPadding from '../../../.storybook/decorators/with-padding';
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import { withKnobs, array, boolean } from '@storybook/addon-knobs';
|
||||
import readme from './readme.md';
|
||||
import i18n from '@/lang';
|
||||
import RawValue from '../../../.storybook/raw-value.vue';
|
||||
|
||||
export default {
|
||||
title: 'Interfaces / Color',
|
||||
decorators: [withPadding, withKnobs],
|
||||
parameters: {
|
||||
notes: readme,
|
||||
},
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
i18n,
|
||||
components: { RawValue },
|
||||
props: {
|
||||
disabled: {
|
||||
default: boolean('Disabled', false),
|
||||
},
|
||||
presets: {
|
||||
default: array('Preset Colors', [
|
||||
'#EB5757',
|
||||
'#F2994A',
|
||||
'#F2C94C',
|
||||
'#6FCF97',
|
||||
'#27AE60',
|
||||
'#56CCF2',
|
||||
'#2F80ED',
|
||||
'#9B51E0',
|
||||
'#BB6BD9',
|
||||
'#607D8B',
|
||||
]),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const value = ref('');
|
||||
return { value };
|
||||
},
|
||||
template: `
|
||||
<div style="max-width: 300px;">
|
||||
<interface-color
|
||||
v-model="value"
|
||||
:presets="presets"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<portal-target multiple name="outlet" />
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
307
src/interfaces/color/color.vue
Normal file
307
src/interfaces/color/color.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<v-menu attached :disabled="disabled">
|
||||
<template #activator="{ toggle, active, activate }">
|
||||
<v-input
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('choose_a_color')"
|
||||
v-model="hexValue"
|
||||
@focus="activate"
|
||||
:pattern="/#([a-f\d]{2}){3}/i"
|
||||
class="color-input"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-input
|
||||
type="color"
|
||||
class="html-color-select"
|
||||
v-model="hexValue"
|
||||
ref="htmlColorInput"
|
||||
/>
|
||||
<v-button
|
||||
@click="activateColorPicker"
|
||||
class="swatch"
|
||||
:icon="true"
|
||||
:style="{
|
||||
'--v-button-background-color': isValidColor ? hexValue : 'transparent',
|
||||
border: isValidColor
|
||||
? 'none'
|
||||
: 'var(--border-width) solid var(--border-normal)',
|
||||
}"
|
||||
>
|
||||
<v-icon v-if="!isValidColor" name="colorize" />
|
||||
</v-button>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-icon name="palette" />
|
||||
</template>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<div class="color-data-inputs">
|
||||
<div class="color-data-input color-type">
|
||||
<v-select :items="colorTypes" v-model="colorType" />
|
||||
</div>
|
||||
<template v-if="colorType === 'RGB'">
|
||||
<v-input
|
||||
:value="rgb.r"
|
||||
@input="rgb = { ...rgb, r: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
:min="0"
|
||||
:max="255"
|
||||
:step="1"
|
||||
/>
|
||||
<v-input
|
||||
:value="rgb.g"
|
||||
@input="rgb = { ...rgb, g: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
:min="0"
|
||||
:max="255"
|
||||
:step="1"
|
||||
/>
|
||||
<v-input
|
||||
:value="rgb.b"
|
||||
@input="rgb = { ...rgb, b: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
:min="0"
|
||||
:max="255"
|
||||
:step="1"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="colorType === 'HSL'">
|
||||
<v-input
|
||||
:value="hsl.h"
|
||||
@input="hsl = { ...hsl, h: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
:min="0"
|
||||
:max="360"
|
||||
:step="1"
|
||||
/>
|
||||
<v-input
|
||||
:value="hsl.s"
|
||||
@input="hsl = { ...hsl, s: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
/>
|
||||
<v-input
|
||||
:value="hsl.l"
|
||||
@input="hsl = { ...hsl, l: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="presets" v-if="presets">
|
||||
<v-button
|
||||
v-for="preset in presets"
|
||||
:key="preset"
|
||||
class="preset"
|
||||
rounded
|
||||
icon
|
||||
:style="{ '--v-button-background-color': preset }"
|
||||
@click="() => (hexValue = preset)"
|
||||
/>
|
||||
</div>
|
||||
</v-menu>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, PropType, watch } from '@vue/composition-api';
|
||||
import color, { RGB, HSL } from '@/utils/color';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator: (val: string) => val === null || val === '' || color.isHex(val),
|
||||
},
|
||||
presets: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: [
|
||||
'#EB5757',
|
||||
'#F2994A',
|
||||
'#F2C94C',
|
||||
'#6FCF97',
|
||||
'#27AE60',
|
||||
'#56CCF2',
|
||||
'#2F80ED',
|
||||
'#9B51E0',
|
||||
'#BB6BD9',
|
||||
'#607D8B',
|
||||
],
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const htmlColorInput = ref<Vue>(null);
|
||||
type ColorType = 'RGB' | 'HSL';
|
||||
|
||||
const colorTypes = ['RGB', 'HSL'] as ColorType[];
|
||||
const colorType = ref<ColorType>('RGB');
|
||||
|
||||
function activateColorPicker() {
|
||||
(htmlColorInput.value?.$el as HTMLElement).getElementsByTagName('input')[0].click();
|
||||
}
|
||||
|
||||
const isValidColor = computed<boolean>(
|
||||
() => hexValue.value != null && color.isHex(hexValue.value as string)
|
||||
);
|
||||
|
||||
const { rgb, hsl, hexValue } = useColor(props.value);
|
||||
|
||||
return {
|
||||
colorTypes,
|
||||
colorType,
|
||||
rgb,
|
||||
hsl,
|
||||
hexValue,
|
||||
htmlColorInput,
|
||||
activateColorPicker,
|
||||
isValidColor,
|
||||
};
|
||||
|
||||
function useColor(hex: string) {
|
||||
const hexValue = ref<string | null>(hex);
|
||||
|
||||
watch(hexValue, (newHex) => {
|
||||
if (!newHex) emit('input', null);
|
||||
else if (newHex.length === 0) emit('input', null);
|
||||
else if (newHex.length === 7) emit('input', newHex);
|
||||
else emit('input', null);
|
||||
});
|
||||
|
||||
const hsl = computed<HSL<string | null>>({
|
||||
get() {
|
||||
return color.hexToHsl(hexValue.value);
|
||||
},
|
||||
set(newHSL) {
|
||||
hexValue.value = color.hslToHex(newHSL);
|
||||
},
|
||||
});
|
||||
|
||||
const rgb = computed<RGB<string | null>>({
|
||||
get() {
|
||||
return color.hexToRgb(hexValue.value);
|
||||
},
|
||||
set(newRGB) {
|
||||
hexValue.value = color.rgbToHex(newRGB);
|
||||
},
|
||||
});
|
||||
|
||||
return { rgb, hsl, hexValue };
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.swatch {
|
||||
/* --v-button-height: calc(var(--input-height) - 12px);
|
||||
--v-button-width: calc(var(--input-height) - 12px);
|
||||
--v-button-min-height: none;
|
||||
--v-button-max-height: calc(var(--input-height) - 12px); */
|
||||
--v-button-padding: 6px;
|
||||
--v-button-background-color: transparent;
|
||||
--v-button-background-color-hover: var(--v-button-background-color);
|
||||
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: calc(var(--input-height) - 12px);
|
||||
max-height: calc(var(--input-height) - 12px);
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0px 8px 14px 8px;
|
||||
|
||||
.preset {
|
||||
--v-button-background-color-hover: var(--v-button-background-color);
|
||||
--v-button-height: 20px;
|
||||
--v-button-width: 20px;
|
||||
|
||||
padding: 0px 4px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0px;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-input.html-color-select {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
--input-padding: 12px 12px 12px 4px;
|
||||
}
|
||||
|
||||
.color-data-inputs {
|
||||
display: grid;
|
||||
grid-gap: 0px;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
width: 100%;
|
||||
padding: 12px 10px;
|
||||
|
||||
.color-type {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
.color-data-input {
|
||||
--border-radius: 0px;
|
||||
|
||||
&::v-deep .input:focus-within,
|
||||
&::v-deep .input:active,
|
||||
&::v-deep .input:focus,
|
||||
&::v-deep .input:hover,
|
||||
&::v-deep .input.active {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:not(.color-type) {
|
||||
--input-padding: 12px 8px;
|
||||
}
|
||||
|
||||
&:not(:first-child)::v-deep .input {
|
||||
margin-left: calc(-1 * var(--border-width));
|
||||
}
|
||||
|
||||
/* stylelint-disable indentation */
|
||||
&:not(:last-child)::v-deep
|
||||
.input:not(.active):not(:focus-within):not(:hover):not(:active):not(:focus) {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
--border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
--border-radius: 0px 4px 4px 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
17
src/interfaces/color/index.ts
Normal file
17
src/interfaces/color/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineInterface } from '@/interfaces/define';
|
||||
import InterfaceColor from './color.vue';
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'color',
|
||||
name: i18n.t('color'),
|
||||
icon: 'palette',
|
||||
component: InterfaceColor,
|
||||
options: [
|
||||
{
|
||||
field: 'presets',
|
||||
name: 'Preset Colors',
|
||||
width: 'full',
|
||||
interface: 'repeater<color>',
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -47,6 +47,7 @@ export const basic = () =>
|
||||
v-model="value"
|
||||
:type="type"
|
||||
/>
|
||||
<portal-target multiple name="outlet" />
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:placeholder="$t('enter_a_value')"
|
||||
>
|
||||
<template #append>
|
||||
<v-icon name="todate" :class="{ active }" />
|
||||
<v-icon name="today" :class="{ active }" />
|
||||
</template>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
@@ -56,6 +56,7 @@ trim :: Option D
|
||||
:choices="choices"
|
||||
:icon="icon"
|
||||
/>
|
||||
<portal-target multiple name="outlet" />
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -56,6 +56,7 @@ trim :: Option D
|
||||
:choices="choices"
|
||||
:icon="icon"
|
||||
/>
|
||||
<portal-target name="outlet" />
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -36,6 +36,7 @@ export const basic = () =>
|
||||
template: `
|
||||
<div style="width: 300px">
|
||||
<interface-icon v-model="value" :disabled="disabled" />
|
||||
<portal-target name="outlet" />
|
||||
<raw-value>{{value}}</raw-value>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -15,6 +15,7 @@ import InterfaceImage from './image';
|
||||
import InterfaceIcon from './icon';
|
||||
import InterfaceManyToOne from './many-to-one';
|
||||
import InterfaceOneToMany from './one-to-many';
|
||||
import InterfaceColor from './color';
|
||||
import InterfaceHash from './hash';
|
||||
import InterfaceSlug from './slug';
|
||||
import InterfaceUser from './user';
|
||||
@@ -38,6 +39,7 @@ export const interfaces = [
|
||||
InterfaceIcon,
|
||||
InterfaceManyToOne,
|
||||
InterfaceOneToMany,
|
||||
InterfaceColor,
|
||||
InterfaceHash,
|
||||
InterfaceSlug,
|
||||
InterfaceUser,
|
||||
|
||||
@@ -15,19 +15,19 @@ export default defineInterface(({ i18n }) => ({
|
||||
interface: 'text-input',
|
||||
},
|
||||
{
|
||||
field: 'minValue',
|
||||
field: 'min',
|
||||
name: i18n.t('minimum_value'),
|
||||
width: 'half',
|
||||
interface: 'numeric',
|
||||
},
|
||||
{
|
||||
field: 'maxValue',
|
||||
field: 'max',
|
||||
name: i18n.t('maximum_value'),
|
||||
width: 'half',
|
||||
interface: 'numeric',
|
||||
},
|
||||
{
|
||||
field: 'stepInterval',
|
||||
field: 'step',
|
||||
name: i18n.t('step_interval'),
|
||||
width: 'half',
|
||||
interface: 'numeric',
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:min="minValue"
|
||||
:max="maxValue"
|
||||
:step="stepInterval"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
@input="$listeners.input"
|
||||
>
|
||||
<template v-if="iconLeft" #prepend>
|
||||
@@ -32,11 +32,11 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
minValue: {
|
||||
min: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
maxValue: {
|
||||
max: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
@@ -48,7 +48,7 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
stepInterval: {
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
|
||||
@@ -380,6 +380,8 @@
|
||||
|
||||
"none": "None",
|
||||
|
||||
"choose_a_color": "Choose a Color...",
|
||||
|
||||
"add_new_item": "Add New Item",
|
||||
|
||||
"many-to-one": "Many to One",
|
||||
|
||||
122
src/utils/color/index.ts
Normal file
122
src/utils/color/index.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
export interface RGB<T> {
|
||||
r: T;
|
||||
g: T;
|
||||
b: T;
|
||||
}
|
||||
|
||||
export interface HSL<T> {
|
||||
h: T;
|
||||
s: T;
|
||||
l: T;
|
||||
}
|
||||
|
||||
export function isNullish(obj: RGB<string | null> | HSL<string | null>): boolean {
|
||||
return Object.values(obj).every((e) => e === null);
|
||||
}
|
||||
|
||||
export function isEmptyStringIsh(obj: RGB<string | null> | HSL<string | null>): boolean {
|
||||
return Object.values(obj).every((e) => e === '');
|
||||
}
|
||||
|
||||
export function componentToHex(c: number): string {
|
||||
if (c > 255) return 'ff';
|
||||
return c.toString(16).padStart(2, '0').toUpperCase();
|
||||
}
|
||||
|
||||
export function toNum(x: string | number | null): number {
|
||||
if (typeof x === 'string') {
|
||||
const res = parseInt(x, 10);
|
||||
return isNaN(res) ? 0 : res;
|
||||
} else return x || 0;
|
||||
}
|
||||
|
||||
export function rgbToHex(rgb: RGB<string | null>): string | null {
|
||||
if (isNullish(rgb)) return null;
|
||||
if (isEmptyStringIsh(rgb)) return '';
|
||||
const r = componentToHex(toNum(rgb.r));
|
||||
const g = componentToHex(toNum(rgb.g));
|
||||
const b = componentToHex(toNum(rgb.b));
|
||||
return `#${r}${g}${b}`;
|
||||
}
|
||||
|
||||
export function isHex(hex: string): boolean {
|
||||
return /^#(([a-f\d]{2}){3,4})$/i.test(hex);
|
||||
}
|
||||
|
||||
export function hexToRgb(hex: string | null): RGB<string | null> {
|
||||
if (hex === null) return { r: null, g: null, b: null };
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex);
|
||||
if (!result) return { r: '', g: '', b: '' };
|
||||
const r = parseInt(result[1], 16);
|
||||
const g = parseInt(result[2], 16);
|
||||
const b = parseInt(result[3], 16);
|
||||
const a = result[4] ? parseInt(result[4], 16) / 255 : 1;
|
||||
return { r: r.toString(), g: g.toString(), b: b.toString() };
|
||||
}
|
||||
|
||||
export function hexToHsl(hex: string | null): HSL<string | null> {
|
||||
return rgbToHsl(hexToRgb(hex));
|
||||
}
|
||||
|
||||
export function hslToHex(hsl: HSL<string | null>): string | null {
|
||||
return rgbToHex(hslToRgb(hsl));
|
||||
}
|
||||
|
||||
// r,g,b in [0,255]; h in [0,360); s,l in [0,100]
|
||||
export function rgbToHsl(rgb: RGB<string | null>): HSL<string | null> {
|
||||
if (isNullish(rgb)) return { h: null, s: null, l: null };
|
||||
if (isEmptyStringIsh(rgb)) return { h: '', s: '', l: '' };
|
||||
let r = toNum(rgb.r);
|
||||
let g = toNum(rgb.g);
|
||||
let b = toNum(rgb.b);
|
||||
// Make r, g, and b fractions of 1
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const a = Math.max(r, g, b),
|
||||
n = a - Math.min(r, g, b),
|
||||
f = 1 - Math.abs(a + a - n - 1);
|
||||
let h = n && (a == r ? (g - b) / n : a == g ? 2 + (b - r) / n : 4 + (r - g) / n);
|
||||
h = 60 * (h < 0 ? h + 6 : h);
|
||||
let s = f ? n / f : 0;
|
||||
let l = (a + a - n) / 2;
|
||||
s *= 100;
|
||||
l *= 100;
|
||||
|
||||
return {
|
||||
h: Math.round(h).toString(),
|
||||
s: Math.round(s).toString(),
|
||||
l: Math.round(l).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
// h in [0, 360); s,l in [0,100]
|
||||
export function hslToRgb(hsl: HSL<string | null>): RGB<string | null> {
|
||||
if (isNullish(hsl)) return { r: null, g: null, b: null };
|
||||
if (isEmptyStringIsh(hsl)) return { r: '', g: '', b: '' };
|
||||
const h = toNum(hsl.h);
|
||||
let s = toNum(hsl.s);
|
||||
let l = toNum(hsl.l);
|
||||
// Must be fractions of 1
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = (n: number, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
const r = Math.round(f(0) * 255);
|
||||
const g = Math.round(f(8) * 255);
|
||||
const b = Math.round(f(4) * 255);
|
||||
return { r: r.toString(), g: g.toString(), b: b.toString() };
|
||||
}
|
||||
|
||||
export default {
|
||||
componentToHex,
|
||||
isHex,
|
||||
rgbToHex,
|
||||
hexToRgb,
|
||||
rgbToHsl,
|
||||
hslToRgb,
|
||||
hexToHsl,
|
||||
hslToHex,
|
||||
};
|
||||
Reference in New Issue
Block a user