Implement color picker for primary/secondary working colors (#70)
* feat/color-picker: rework * feat(161) lint * feat(161) Remove response handlers * feat(161) fix rgb <-> hsv conversion * feat(161) inverse swatchs and add checkered bg * feat(161) remove temporary color assignment * feat(161) move cursor outside of the box * feat(161) @Keavon feedbacks * feat(161) lint * feat(161) fix opacity-picker color * feat(161) --saturation-picker-color
This commit is contained in:
parent
a70605b514
commit
b56dfd746f
|
|
@ -154,6 +154,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
background: #ffffff;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -222,10 +223,6 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
todo(toolIndex);
|
todo(toolIndex);
|
||||||
},
|
},
|
||||||
async updatePrimaryColor(c: { r: number; g: number; b: number; a: number }) {
|
|
||||||
const { update_primary_color, Color } = await wasm;
|
|
||||||
update_primary_color(new Color(c.r, c.g, c.b, c.a));
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
registerResponseHandler(ResponseType.UpdateCanvas, (responseData: Response) => {
|
registerResponseHandler(ResponseType.UpdateCanvas, (responseData: Response) => {
|
||||||
|
|
@ -239,9 +236,6 @@ export default defineComponent({
|
||||||
|
|
||||||
window.addEventListener("keyup", (e: KeyboardEvent) => this.keyUp(e));
|
window.addEventListener("keyup", (e: KeyboardEvent) => this.keyUp(e));
|
||||||
window.addEventListener("keydown", (e: KeyboardEvent) => this.keyDown(e));
|
window.addEventListener("keydown", (e: KeyboardEvent) => this.keyDown(e));
|
||||||
|
|
||||||
// TODO: Implement an actual UI for chosing colors (this is completely temporary)
|
|
||||||
this.updatePrimaryColor({ r: 247 / 255, g: 76 / 255, b: 0 / 255, a: 0.6 });
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,31 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="popover-color-picker" @click="notImplemented">
|
<div class="popover-color-picker">
|
||||||
<div class="color-picker">
|
<div class="saturation-picker" ref="saturationPicker" data-picker-action="MoveSaturation" @pointerdown="onPointerDown">
|
||||||
<div class="selection-circle"></div>
|
<div ref="saturationCursor" class="selection-circle"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hue-picker">
|
<div class="hue-picker" ref="huePicker" data-picker-action="MoveHue" @pointerdown="onPointerDown">
|
||||||
<div class="selection-pincers"></div>
|
<div ref="hueCursor" class="selection-pincers"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="opacity-picker">
|
<div class="opacity-picker" ref="opacityPicker" data-picker-action="MoveOpacity" @pointerdown="onPointerDown">
|
||||||
<div class="selection-pincers"></div>
|
<div ref="opacityCursor" class="selection-pincers"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.popover-color-picker {
|
.popover-color-picker {
|
||||||
|
--saturation-picker-color: #ff0000;
|
||||||
|
--opacity-picker-color: #ff0000;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.color-picker {
|
.saturation-picker {
|
||||||
--hue: #ff0000;
|
|
||||||
width: 256px;
|
width: 256px;
|
||||||
background-blend-mode: multiply;
|
background-blend-mode: multiply;
|
||||||
background: linear-gradient(to bottom, #ffffff, #000000), linear-gradient(to right, #ffffff, var(--hue));
|
background: linear-gradient(to bottom, #ffffff, #000000), linear-gradient(to right, #ffffff, var(--saturation-picker-color));
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker,
|
.saturation-picker,
|
||||||
.hue-picker,
|
.hue-picker,
|
||||||
.opacity-picker {
|
.opacity-picker {
|
||||||
height: 256px;
|
height: 256px;
|
||||||
|
|
@ -46,7 +47,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.opacity-picker {
|
.opacity-picker {
|
||||||
background: linear-gradient(to bottom, #ff0000, transparent);
|
background: linear-gradient(to bottom, var(--opacity-picker-color), transparent);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -64,10 +65,11 @@
|
||||||
|
|
||||||
.selection-circle {
|
.selection-circle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 100%;
|
left: 0%;
|
||||||
top: 0%;
|
top: 0%;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -89,6 +91,7 @@
|
||||||
top: 0%;
|
top: 0%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -115,13 +118,181 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
import { clamp, hsvToRgb, rgbToHsv, isRGB } from "../../lib/utils";
|
||||||
|
|
||||||
|
const enum ColorPickerState {
|
||||||
|
Idle = "Idle",
|
||||||
|
MoveHue = "MoveHue",
|
||||||
|
MoveOpacity = "MoveOpacity",
|
||||||
|
MoveSaturation = "MoveSaturation",
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {},
|
components: {},
|
||||||
props: {},
|
props: {
|
||||||
|
color: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
state: ColorPickerState.Idle,
|
||||||
|
// Disable proxy on this object
|
||||||
|
// https://v3.vuejs.org/api/options-data.html#data-2
|
||||||
|
// eslint-disable-next-line vue/no-reserved-keys
|
||||||
|
_: {
|
||||||
|
colorPicker: {
|
||||||
|
color: { h: 0, s: 0, v: 0, a: 1 },
|
||||||
|
hue: {
|
||||||
|
rect: { width: 0, height: 0, top: 0, left: 0 },
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
rect: { width: 0, height: 0, top: 0, left: 0 },
|
||||||
|
},
|
||||||
|
saturation: {
|
||||||
|
rect: { width: 0, height: 0, top: 0, left: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$watch("color", this.updateColor, { immediate: true });
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.removeEvents();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
notImplemented() {
|
addEvents() {
|
||||||
alert("Color picker is not functional yet");
|
document.addEventListener("pointermove", this.onPointerMove);
|
||||||
|
document.addEventListener("pointerup", this.onPointerUp);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeEvents() {
|
||||||
|
document.removeEventListener("pointermove", this.onPointerMove);
|
||||||
|
document.removeEventListener("pointerup", this.onPointerUp);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRef<T>(name: string) {
|
||||||
|
return this.$refs[name] as T;
|
||||||
|
},
|
||||||
|
|
||||||
|
onPointerDown(e: PointerEvent) {
|
||||||
|
if (!(e.currentTarget instanceof Element)) return;
|
||||||
|
const picker = e.currentTarget.getAttribute("data-picker-action");
|
||||||
|
this.state = (() => {
|
||||||
|
switch (picker) {
|
||||||
|
case "MoveHue":
|
||||||
|
return ColorPickerState.MoveHue;
|
||||||
|
case "MoveOpacity":
|
||||||
|
return ColorPickerState.MoveOpacity;
|
||||||
|
case "MoveSaturation":
|
||||||
|
return ColorPickerState.MoveSaturation;
|
||||||
|
default:
|
||||||
|
return ColorPickerState.Idle;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (this.state !== ColorPickerState.Idle) {
|
||||||
|
this.addEvents();
|
||||||
|
this.updateRects();
|
||||||
|
this.onPointerMove(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onPointerMove(e: PointerEvent) {
|
||||||
|
const { colorPicker } = this.$data._;
|
||||||
|
|
||||||
|
if (this.state === ColorPickerState.MoveHue) {
|
||||||
|
this.setHuePosition(e.clientY - colorPicker.hue.rect.top);
|
||||||
|
} else if (this.state === ColorPickerState.MoveOpacity) {
|
||||||
|
this.setOpacityPosition(e.clientY - colorPicker.opacity.rect.top);
|
||||||
|
} else if (this.state === ColorPickerState.MoveSaturation) {
|
||||||
|
this.setSaturationPosition(e.clientX - colorPicker.saturation.rect.left, e.clientY - colorPicker.saturation.rect.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state !== ColorPickerState.Idle) {
|
||||||
|
this.updateHue();
|
||||||
|
this.$emit("update:color", hsvToRgb(colorPicker.color));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onPointerUp() {
|
||||||
|
if (this.state !== ColorPickerState.Idle) {
|
||||||
|
this.state = ColorPickerState.Idle;
|
||||||
|
this.removeEvents();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRects() {
|
||||||
|
const { colorPicker } = this.$data._;
|
||||||
|
|
||||||
|
const saturationPicker = this.getRef<HTMLDivElement>("saturationPicker");
|
||||||
|
const saturation = saturationPicker.getBoundingClientRect();
|
||||||
|
colorPicker.saturation.rect.width = saturation.width;
|
||||||
|
colorPicker.saturation.rect.height = saturation.height;
|
||||||
|
colorPicker.saturation.rect.left = saturation.left;
|
||||||
|
colorPicker.saturation.rect.top = saturation.top;
|
||||||
|
|
||||||
|
const huePicker = this.getRef<HTMLDivElement>("huePicker");
|
||||||
|
const hue = huePicker.getBoundingClientRect();
|
||||||
|
colorPicker.hue.rect.width = hue.width;
|
||||||
|
colorPicker.hue.rect.height = hue.height;
|
||||||
|
colorPicker.hue.rect.left = hue.left;
|
||||||
|
colorPicker.hue.rect.top = hue.top;
|
||||||
|
|
||||||
|
const opacityPicker = this.getRef<HTMLDivElement>("opacityPicker");
|
||||||
|
const opacity = opacityPicker.getBoundingClientRect();
|
||||||
|
colorPicker.opacity.rect.width = opacity.width;
|
||||||
|
colorPicker.opacity.rect.height = opacity.height;
|
||||||
|
colorPicker.opacity.rect.left = opacity.left;
|
||||||
|
colorPicker.opacity.rect.top = opacity.top;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSaturationPosition(x: number, y: number) {
|
||||||
|
const { colorPicker } = this.$data._;
|
||||||
|
const saturationCursor = this.getRef<HTMLDivElement>("saturationCursor");
|
||||||
|
const saturationPosition = [clamp(x, 0, colorPicker.saturation.rect.width), clamp(y, 0, colorPicker.saturation.rect.height)];
|
||||||
|
saturationCursor.style.transform = `translate(${saturationPosition[0]}px, ${saturationPosition[1]}px)`;
|
||||||
|
colorPicker.color.s = saturationPosition[0] / colorPicker.saturation.rect.width;
|
||||||
|
colorPicker.color.v = (1 - saturationPosition[1] / colorPicker.saturation.rect.height) * 255;
|
||||||
|
},
|
||||||
|
|
||||||
|
setHuePosition(y: number) {
|
||||||
|
const { colorPicker } = this.$data._;
|
||||||
|
const hueCursor = this.getRef<HTMLDivElement>("hueCursor");
|
||||||
|
const huePosition = clamp(y, 0, colorPicker.hue.rect.height);
|
||||||
|
hueCursor.style.transform = `translateY(${huePosition}px)`;
|
||||||
|
colorPicker.color.h = clamp(1 - huePosition / colorPicker.hue.rect.height);
|
||||||
|
},
|
||||||
|
|
||||||
|
setOpacityPosition(y: number) {
|
||||||
|
const { colorPicker } = this.$data._;
|
||||||
|
const opacityCursor = this.getRef<HTMLDivElement>("opacityCursor");
|
||||||
|
const opacityPosition = clamp(y, 0, colorPicker.opacity.rect.height);
|
||||||
|
opacityCursor.style.transform = `translateY(${opacityPosition}px)`;
|
||||||
|
colorPicker.color.a = clamp(1 - opacityPosition / colorPicker.opacity.rect.height);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHue() {
|
||||||
|
const { colorPicker } = this.$data._;
|
||||||
|
let color = hsvToRgb({ h: colorPicker.color.h, s: 1, v: 255, a: 1 });
|
||||||
|
this.$el.style.setProperty("--saturation-picker-color", `rgb(${color.r}, ${color.g}, ${color.b})`);
|
||||||
|
color = hsvToRgb(colorPicker.color);
|
||||||
|
this.$el.style.setProperty("--opacity-picker-color", `rgb(${color.r}, ${color.g}, ${color.b})`);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateColor() {
|
||||||
|
if (this.state !== ColorPickerState.Idle) return;
|
||||||
|
const { color } = this;
|
||||||
|
if (!isRGB(color)) return;
|
||||||
|
const { colorPicker } = this.$data._;
|
||||||
|
colorPicker.color = rgbToHsv(color);
|
||||||
|
this.updateRects();
|
||||||
|
this.setSaturationPosition(colorPicker.color.s * colorPicker.saturation.rect.width, (1 - colorPicker.color.v / 255) * colorPicker.saturation.rect.height);
|
||||||
|
this.setOpacityPosition((1 - colorPicker.color.a) * colorPicker.opacity.rect.height);
|
||||||
|
this.setHuePosition((1 - colorPicker.color.h) * colorPicker.hue.rect.height);
|
||||||
|
this.updateHue();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="swatch-pair">
|
<div class="swatch-pair">
|
||||||
<div class="secondary swatch">
|
<div class="secondary swatch">
|
||||||
<button @click="clickSecondarySwatch" style="background: #ffffff"></button>
|
<button @click="clickSecondarySwatch" ref="secondaryButton"></button>
|
||||||
<Popover :direction="PopoverDirection.Right" horizontal ref="secondarySwatchPopover">
|
<Popover :direction="PopoverDirection.Right" horizontal ref="secondarySwatchPopover">
|
||||||
<ColorPicker />
|
<ColorPicker v-model:color="secondaryColor" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div class="primary swatch">
|
<div class="primary swatch">
|
||||||
<button @click="clickPrimarySwatch" style="background: #000000"></button>
|
<button @click="clickPrimarySwatch" ref="primaryButton"></button>
|
||||||
<Popover :direction="PopoverDirection.Right" horizontal ref="primarySwatchPopover">
|
<Popover :direction="PopoverDirection.Right" horizontal ref="primarySwatchPopover">
|
||||||
<ColorPicker />
|
<ColorPicker v-model:color="primaryColor" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
--swatch-color: #ffffff;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
@ -37,6 +38,19 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
background: linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%),
|
||||||
|
linear-gradient(#ffffff, #ffffff);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-position: 0 0, 8px 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--swatch-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
|
|
@ -52,10 +66,13 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { rgbToDecimalRgb } from "@/lib/utils";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import ColorPicker from "../../popovers/ColorPicker.vue";
|
import ColorPicker from "../../popovers/ColorPicker.vue";
|
||||||
import Popover, { PopoverDirection } from "../overlays/Popover.vue";
|
import Popover, { PopoverDirection } from "../overlays/Popover.vue";
|
||||||
|
|
||||||
|
const wasm = import("../../../../wasm/pkg");
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Popover,
|
Popover,
|
||||||
|
|
@ -64,18 +81,51 @@ export default defineComponent({
|
||||||
props: {},
|
props: {},
|
||||||
methods: {
|
methods: {
|
||||||
clickPrimarySwatch() {
|
clickPrimarySwatch() {
|
||||||
(this.$refs.primarySwatchPopover as typeof Popover).setOpen();
|
this.getRef<typeof Popover>("primarySwatchPopover").setOpen();
|
||||||
(this.$refs.secondarySwatchPopover as typeof Popover).setClosed();
|
this.getRef<typeof Popover>("secondarySwatchPopover").setClosed();
|
||||||
},
|
},
|
||||||
|
|
||||||
clickSecondarySwatch() {
|
clickSecondarySwatch() {
|
||||||
(this.$refs.secondarySwatchPopover as typeof Popover).setOpen();
|
this.getRef<typeof Popover>("secondarySwatchPopover").setOpen();
|
||||||
(this.$refs.primarySwatchPopover as typeof Popover).setClosed();
|
this.getRef<typeof Popover>("primarySwatchPopover").setClosed();
|
||||||
|
},
|
||||||
|
|
||||||
|
getRef<T>(name: string) {
|
||||||
|
return this.$refs[name] as T;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updatePrimaryColor() {
|
||||||
|
const { update_primary_color, Color } = await wasm;
|
||||||
|
|
||||||
|
let color = this.primaryColor;
|
||||||
|
const button = this.getRef<HTMLButtonElement>("primaryButton");
|
||||||
|
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
|
||||||
|
|
||||||
|
color = rgbToDecimalRgb(this.primaryColor);
|
||||||
|
update_primary_color(new Color(color.r, color.g, color.b, color.a));
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateSecondaryColor() {
|
||||||
|
const { update_secondary_color, Color } = await wasm;
|
||||||
|
|
||||||
|
let color = this.secondaryColor;
|
||||||
|
const button = this.getRef<HTMLButtonElement>("secondaryButton");
|
||||||
|
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
|
||||||
|
|
||||||
|
color = rgbToDecimalRgb(this.secondaryColor);
|
||||||
|
update_secondary_color(new Color(color.r, color.g, color.b, color.a));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
PopoverDirection,
|
PopoverDirection,
|
||||||
|
primaryColor: { r: 0, g: 0, b: 0, a: 1 },
|
||||||
|
secondaryColor: { r: 255, g: 255, b: 255, a: 1 },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$watch("primaryColor", this.updatePrimaryColor, { immediate: true });
|
||||||
|
this.$watch("secondaryColor", this.updateSecondaryColor, { immediate: true });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
export interface RGB {
|
||||||
|
r: number;
|
||||||
|
g: number;
|
||||||
|
b: number;
|
||||||
|
a: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HSV {
|
||||||
|
h: number;
|
||||||
|
s: number;
|
||||||
|
v: number;
|
||||||
|
a: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hsvToRgb(hsv: HSV): RGB {
|
||||||
|
let { h } = hsv;
|
||||||
|
const { s, v } = hsv;
|
||||||
|
h *= 6;
|
||||||
|
const i = Math.floor(h);
|
||||||
|
const f = h - i;
|
||||||
|
const p = v * (1 - s);
|
||||||
|
const q = v * (1 - f * s);
|
||||||
|
const t = v * (1 - (1 - f) * s);
|
||||||
|
const mod = i % 6;
|
||||||
|
const r = Math.round([v, q, p, p, t, v][mod]);
|
||||||
|
const g = Math.round([t, v, v, q, p, p][mod]);
|
||||||
|
const b = Math.round([p, p, t, v, v, q][mod]);
|
||||||
|
return { r, g, b, a: hsv.a };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbToHsv(rgb: RGB) {
|
||||||
|
const { r, g, b } = rgb;
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
const d = max - min;
|
||||||
|
const s = max === 0 ? 0 : d / max;
|
||||||
|
const v = max;
|
||||||
|
let h = 0;
|
||||||
|
if (max === min) {
|
||||||
|
h = 0;
|
||||||
|
} else {
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
return { h, s, v, a: rgb.a };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbToDecimalRgb(rgb: RGB) {
|
||||||
|
const r = rgb.r / 255;
|
||||||
|
const g = rgb.g / 255;
|
||||||
|
const b = rgb.b / 255;
|
||||||
|
return { r, g, b, a: rgb.a };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clamp(value: number, min = 0, max = 1) {
|
||||||
|
return Math.max(min, Math.min(value, max));
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function isRGB(data: any): data is RGB {
|
||||||
|
if (typeof data !== "object" || data === null) return false;
|
||||||
|
return (
|
||||||
|
typeof data.r === "number" &&
|
||||||
|
!Number.isNaN(data.r) &&
|
||||||
|
typeof data.g === "number" &&
|
||||||
|
!Number.isNaN(data.g) &&
|
||||||
|
typeof data.b === "number" &&
|
||||||
|
!Number.isNaN(data.b) &&
|
||||||
|
typeof data.a === "number" &&
|
||||||
|
!Number.isNaN(data.a)
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue