Refactor color picker floating menu (#809)

* Move FloatingMenu template component

* Rewrite the ColorPicker component so it's not horrifically bad code

* Move FloatingMenu into the ColorPicker component

* Little Imaginate fixes

* Add todo
This commit is contained in:
Keavon Chambers 2022-10-23 17:29:04 -07:00
parent 1219e26d17
commit 7f9c59dd99
14 changed files with 177 additions and 290 deletions

View File

@ -990,7 +990,7 @@ impl DocumentMessageHandler {
DocumentRenderMode::OnlyBelowLayerInFolder(below_layer_path) => self.graphene_document.render_layers_below(below_layer_path, render_data).unwrap(),
};
let artboards = self.artboard_message_handler.artboards_graphene_document.render_root(render_data);
let outside_artboards_color = if self.artboard_message_handler.artboard_ids.is_empty() { "#ffffff" } else { "#000000" };
let outside_artboards_color = if self.artboard_message_handler.artboard_ids.is_empty() { "#ffffff" } else { "#222222" };
let outside_artboards = format!(r#"<rect x="0" y="0" width="100%" height="100%" fill="{}" />"#, outside_artboards_color);
let matrix = transform
.to_cols_array()

View File

@ -914,7 +914,7 @@ fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persi
\n\
Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\
\n\
To boost (or lessen) the importance of a word or phrase, wrap it in quotes ending with a colon and a multiplier, for example:\n\
To boost (or lessen) the importance of a word or phrase, wrap it in parentheses ending with a colon and a multiplier, for example:\n\
\"Colorless green ideas (sleep:1.3) furiously\"
"
.trim()

View File

@ -1,30 +1,29 @@
<template>
<FloatingMenu :open="open" @update:open="(isOpen) => emitOpenState(isOpen)" :direction="direction" :type="'Popover'">
<LayoutRow class="color-picker">
<LayoutCol class="saturation-picker" ref="saturationPicker" @pointerdown="(e: PointerEvent) => onPointerDown(e)">
<div ref="saturationCursor" class="selection-circle"></div>
<LayoutCol class="saturation-value-picker" :style="{ '--saturation-value-picker-hue': hueColorCSS }" @pointerdown="(e: PointerEvent) => beginDrag(e)" data-saturation-value-picker>
<div class="selection-circle" :style="{ top: `${(1 - value) * 100}%`, left: `${saturation * 100}%` }"></div>
</LayoutCol>
<LayoutCol class="hue-picker" ref="huePicker" @pointerdown="(e: PointerEvent) => onPointerDown(e)">
<div ref="hueCursor" class="selection-pincers"></div>
<LayoutCol class="hue-picker" @pointerdown="(e: PointerEvent) => beginDrag(e)" data-hue-picker>
<div class="selection-pincers" :style="{ top: `${(1 - hue) * 100}%` }"></div>
</LayoutCol>
<LayoutCol class="opacity-picker" ref="opacityPicker" @pointerdown="(e: PointerEvent) => onPointerDown(e)">
<div ref="opacityCursor" class="selection-pincers"></div>
<LayoutCol class="opacity-picker" :style="{ '--opacity-picker-color': color.toRgbCSS() }" @pointerdown="(e: PointerEvent) => beginDrag(e)" data-opacity-picker>
<div class="selection-pincers" :style="{ top: `${(1 - opacity) * 100}%` }"></div>
</LayoutCol>
</LayoutRow>
</FloatingMenu>
</template>
<style lang="scss">
.color-picker {
--saturation-picker-hue: #ff0000;
--opacity-picker-color: #ff0000;
.saturation-picker {
.saturation-value-picker {
width: 256px;
background-blend-mode: multiply;
background: linear-gradient(to bottom, #ffffff, #000000), linear-gradient(to right, #ffffff, var(--saturation-picker-hue));
background: linear-gradient(to bottom, #ffffff, #000000), linear-gradient(to right, #ffffff, var(--saturation-value-picker-hue));
position: relative;
}
.saturation-picker,
.saturation-value-picker,
.hue-picker,
.opacity-picker {
height: 256px;
@ -118,39 +117,72 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { hsvaToRgba, rgbaToHsva } from "@/utility-functions/color";
import { clamp } from "@/utility-functions/math";
import { type RGBA } from "@/wasm-communication/messages";
import { Color } from "@/wasm-communication/messages";
import FloatingMenu, { type MenuDirection } from "@/components/layout/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
type ColorPickerState = "Idle" | "MoveHue" | "MoveOpacity" | "MoveSaturation";
// TODO: Clean up the fundamental code design in this file to simplify it and use better practices.
// TODO: Such as removing the `picker*` data variables and reducing the number of functions which call each other in weird, non-obvious ways.
export default defineComponent({
emits: ["update:color"],
emits: ["update:color", "update:open"],
props: {
color: { type: Object as PropType<RGBA>, required: true },
color: { type: Object as PropType<Color>, required: true },
open: { type: Boolean as PropType<boolean>, required: true },
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
},
data() {
const hsva = this.color.toHSVA();
return {
state: "Idle" as ColorPickerState,
pickerHSVA: { h: 0, s: 0, v: 0, a: 1 },
pickerHueRect: { width: 0, height: 0, top: 0, left: 0 },
pickerOpacityRect: { width: 0, height: 0, top: 0, left: 0 },
pickerSaturationRect: { width: 0, height: 0, top: 0, left: 0 },
draggingPickerTrack: undefined as HTMLElement | undefined,
hue: hsva.h,
saturation: hsva.s,
value: hsva.v,
opacity: hsva.a,
};
},
mounted() {
this.$watch("color", this.updateColor, { immediate: true });
computed: {
hueColorCSS() {
return new Color({ h: this.hue, s: 1, v: 1, a: 1 }).toRgbCSS();
},
unmounted() {
this.removeEvents();
},
methods: {
beginDrag(e: PointerEvent) {
const target = (e.target || undefined) as HTMLElement | undefined;
this.draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-opacity-picker]") || undefined;
this.addEvents();
this.onPointerMove(e);
},
onPointerMove(e: PointerEvent) {
if (this.draggingPickerTrack?.hasAttribute("data-saturation-value-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.saturation = clamp((e.clientX - rectangle.left) / rectangle.width, 0, 1);
this.value = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
} else if (this.draggingPickerTrack?.hasAttribute("data-hue-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.hue = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
} else if (this.draggingPickerTrack?.hasAttribute("data-opacity-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.opacity = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
}
// Just in case the mouseup event is lost
if (e.buttons === 0) this.removeEvents();
// The `color` prop's watcher calls `this.updateColor()`
this.$emit("update:color", new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.opacity }));
},
onPointerUp() {
this.removeEvents();
},
emitOpenState(isOpen: boolean) {
this.$emit("update:open", isOpen);
},
addEvents() {
document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
@ -159,145 +191,12 @@ export default defineComponent({
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
},
onPointerDown(e: PointerEvent) {
const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol;
const saturationPickerElement = saturationPicker?.$el as HTMLElement | undefined;
const huePicker = this.$refs.huePicker as typeof LayoutCol;
const huePickerElement = huePicker?.$el as HTMLElement | undefined;
const opacityPicker = this.$refs.opacityPicker as typeof LayoutCol;
const opacityPickerElement = opacityPicker?.$el as HTMLElement | undefined;
if (!(e.currentTarget instanceof HTMLElement) || !saturationPickerElement || !huePickerElement || !opacityPickerElement) return;
if (saturationPickerElement.contains(e.currentTarget)) {
this.state = "MoveSaturation";
} else if (huePickerElement.contains(e.currentTarget)) {
this.state = "MoveHue";
} else if (opacityPickerElement.contains(e.currentTarget)) {
this.state = "MoveOpacity";
} else {
this.state = "Idle";
}
if (this.state === "Idle") return;
this.addEvents();
this.updateRects();
this.onPointerMove(e);
},
onPointerMove(e: PointerEvent) {
switch (this.state) {
case "MoveHue":
this.setHueCursorPosition(e.clientY - this.pickerHueRect.top);
break;
case "MoveOpacity":
this.setOpacityCursorPosition(e.clientY - this.pickerOpacityRect.top);
break;
case "MoveSaturation":
this.setSaturationCursorPosition(e.clientX - this.pickerSaturationRect.left, e.clientY - this.pickerSaturationRect.top);
break;
default:
return;
}
this.updateHue();
// The `color` prop's watcher calls `this.updateColor()`
this.$emit("update:color", hsvaToRgba(this.pickerHSVA));
},
onPointerUp() {
if (this.state === "Idle") return;
this.state = "Idle";
unmounted() {
this.removeEvents();
},
updateRects() {
const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol;
const saturationPickerElement = saturationPicker?.$el as HTMLElement | undefined;
const huePicker = this.$refs.huePicker as typeof LayoutCol;
const huePickerElement = huePicker?.$el as HTMLElement | undefined;
const opacityPicker = this.$refs.opacityPicker as typeof LayoutCol;
const opacityPickerElement = opacityPicker?.$el as HTMLElement | undefined;
if (!saturationPickerElement || !huePickerElement || !opacityPickerElement) return;
// Saturation
const saturation = saturationPickerElement.getBoundingClientRect();
this.pickerSaturationRect.width = saturation.width;
this.pickerSaturationRect.height = saturation.height;
this.pickerSaturationRect.left = saturation.left;
this.pickerSaturationRect.top = saturation.top;
// Hue
const hue = huePickerElement.getBoundingClientRect();
this.pickerHueRect.width = hue.width;
this.pickerHueRect.height = hue.height;
this.pickerHueRect.left = hue.left;
this.pickerHueRect.top = hue.top;
// Opacity
const opacity = opacityPickerElement.getBoundingClientRect();
this.pickerOpacityRect.width = opacity.width;
this.pickerOpacityRect.height = opacity.height;
this.pickerOpacityRect.left = opacity.left;
this.pickerOpacityRect.top = opacity.top;
},
setSaturationCursorPosition(x: number, y: number) {
const saturationPositionX = clamp(x, 0, this.pickerSaturationRect.width);
const saturationPositionY = clamp(y, 0, this.pickerSaturationRect.height);
const saturationCursor = this.$refs.saturationCursor as HTMLElement;
saturationCursor.style.transform = `translate(${saturationPositionX}px, ${saturationPositionY}px)`;
this.pickerHSVA.s = saturationPositionX / this.pickerSaturationRect.width;
this.pickerHSVA.v = (1 - saturationPositionY / this.pickerSaturationRect.height) * 255;
},
setHueCursorPosition(y: number) {
const huePosition = clamp(y, 0, this.pickerHueRect.height);
const hueCursor = this.$refs.hueCursor as HTMLElement;
hueCursor.style.transform = `translateY(${huePosition}px)`;
this.pickerHSVA.h = clamp(1 - huePosition / this.pickerHueRect.height);
},
setOpacityCursorPosition(y: number) {
const opacityPosition = clamp(y, 0, this.pickerOpacityRect.height);
const opacityCursor = this.$refs.opacityCursor as HTMLElement;
opacityCursor.style.transform = `translateY(${opacityPosition}px)`;
this.pickerHSVA.a = clamp(1 - opacityPosition / this.pickerOpacityRect.height);
},
updateHue() {
const hsva = hsvaToRgba({ h: this.pickerHSVA.h, s: 1, v: 255, a: 1 });
const rgba = hsvaToRgba(this.pickerHSVA);
this.$el.style.setProperty("--saturation-picker-hue", `rgb(${hsva.r}, ${hsva.g}, ${hsva.b})`);
this.$el.style.setProperty("--opacity-picker-color", `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})`);
},
updateColor() {
if (this.state !== "Idle") return;
this.pickerHSVA = rgbaToHsva(this.color);
this.updateRects();
this.setSaturationCursorPosition(this.pickerHSVA.s * this.pickerSaturationRect.width, (1 - this.pickerHSVA.v / 255) * this.pickerSaturationRect.height);
this.setOpacityCursorPosition((1 - this.pickerHSVA.a) * this.pickerOpacityRect.height);
this.setHueCursorPosition((1 - this.pickerHSVA.h) * this.pickerHueRect.height);
this.updateHue();
},
},
components: {
FloatingMenu,
LayoutCol,
LayoutRow,
},

View File

@ -1,6 +1,6 @@
<template>
<FloatingMenu :open="true" class="dialog-modal" :type="'Dialog'" :direction="'Center'" data-dialog-modal>
<LayoutRow ref="main">
<LayoutRow>
<LayoutCol class="icon-column">
<!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons -->
<IconLabel :icon="dialog.state.icon" :class="dialog.state.icon.toLowerCase()" />
@ -68,7 +68,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";

View File

@ -85,7 +85,7 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
// Should be equal to the width and height of the canvas in the CSS above
const ZOOM_WINDOW_DIMENSIONS_EXPANDED = 110;

View File

@ -163,7 +163,7 @@ import { defineComponent, type PropType } from "vue";
import { type MenuListEntry } from "@/wasm-communication/messages";
import FloatingMenu, { type MenuDirection } from "@/components/floating-menus/FloatingMenu.vue";
import FloatingMenu, { type MenuDirection } from "@/components/layout/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";

View File

@ -51,7 +51,7 @@ import { defineComponent, type PropType } from "vue";
import { type IconName } from "@/utility-functions/icons";
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";

View File

@ -5,9 +5,7 @@
<Separator :type="'Related'" />
<LayoutRow class="swatch">
<button class="swatch-button" :class="{ 'disabled-swatch': !value }" :style="`--swatch-color: #${value}`" @click="() => $emit('update:open', true)"></button>
<FloatingMenu v-model:open="isOpen" :type="'Popover'" :direction="'Bottom'">
<ColorPicker @update:color="(color: RGBA) => colorPickerUpdated(color)" :color="color" />
</FloatingMenu>
<ColorPicker v-model:open="isOpen" :color="color" @update:color="(color: Color) => colorPickerUpdated(color)" :direction="'Bottom'" />
</LayoutRow>
</LayoutRow>
</template>
@ -70,10 +68,9 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type RGBA } from "@/wasm-communication/messages";
import { Color } from "@/wasm-communication/messages";
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
@ -98,16 +95,18 @@ export default defineComponent({
};
},
computed: {
color() {
if (!this.value) return { r: 0, g: 0, b: 0, a: 1 };
color(): Color {
// TODO: Validate length of value, and allow shorthand versions like single-digit or three-digit hex codes
if (!this.value) return new Color(0, 0, 0, 1);
const r = parseInt(this.value.slice(0, 2), 16);
const g = parseInt(this.value.slice(2, 4), 16);
const b = parseInt(this.value.slice(4, 6), 16);
const a = parseInt(this.value.slice(6, 8), 16);
return { r, g, b, a: a / 255 };
const r = parseInt(this.value.slice(0, 2), 16) / 255;
const g = parseInt(this.value.slice(2, 4), 16) / 255;
const b = parseInt(this.value.slice(4, 6), 16) / 255;
const a = parseInt(this.value.slice(6, 8), 16) / 255;
return new Color(r, g, b, a);
},
displayValue() {
displayValue(): string {
if (!this.value) return "";
const value = this.value.toLowerCase();
@ -125,10 +124,12 @@ export default defineComponent({
},
},
methods: {
colorPickerUpdated(color: RGBA) {
const twoDigitHex = (value: number): string => value.toString(16).padStart(2, "0");
const alphaU8Scale = Math.floor(color.a * 255);
const newValue = `${twoDigitHex(color.r)}${twoDigitHex(color.g)}${twoDigitHex(color.b)}${twoDigitHex(alphaU8Scale)}`;
colorPickerUpdated(color: Color) {
const twoDigitHex = (value: number): string =>
Math.floor(value * 255)
.toString(16)
.padStart(2, "0");
const newValue = `${twoDigitHex(color.red)}${twoDigitHex(color.green)}${twoDigitHex(color.blue)}${twoDigitHex(color.alpha)}`;
this.$emit("update:value", newValue);
},
textInputUpdated(newValue: string) {
@ -160,7 +161,6 @@ export default defineComponent({
},
components: {
ColorPicker,
FloatingMenu,
LayoutRow,
OptionalInput,
Separator,

View File

@ -73,8 +73,8 @@ import { defineComponent, nextTick, type PropType } from "vue";
import { type MenuListEntry } from "@/wasm-communication/messages";
import type FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
import MenuList from "@/components/floating-menus/MenuList.vue";
import type FloatingMenu from "@/components/layout/FloatingMenu.vue";
import type LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";

View File

@ -2,15 +2,11 @@
<LayoutCol class="swatch-pair">
<LayoutRow class="secondary swatch">
<button @click="() => clickSecondarySwatch()" :style="`--swatch-color: ${secondary.toRgbaCSS()}`" data-hover-menu-spawner></button>
<FloatingMenu :type="'Popover'" :direction="'Right'" v-model:open="secondaryOpen">
<ColorPicker @update:color="(color: RGBA) => secondaryColorChanged(color)" :color="secondary.toRgba()" />
</FloatingMenu>
<ColorPicker v-model:open="secondaryOpen" :color="secondary" @update:color="(color: Color) => secondaryColorChanged(color)" :direction="'Right'" />
</LayoutRow>
<LayoutRow class="primary swatch">
<button @click="() => clickPrimarySwatch()" :style="`--swatch-color: ${primary.toRgbaCSS()}`" data-hover-menu-spawner></button>
<FloatingMenu :type="'Popover'" :direction="'Right'" v-model:open="primaryOpen">
<ColorPicker @update:color="(color: RGBA) => primaryColorChanged(color)" :color="primary.toRgba()" />
</FloatingMenu>
<ColorPicker v-model:open="primaryOpen" :color="primary" @update:color="(color: Color) => primaryColorChanged(color)" :direction="'Right'" />
</LayoutRow>
</LayoutCol>
</template>
@ -68,11 +64,9 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { rgbaToDecimalRgba } from "@/utility-functions/color";
import { type RGBA, type Color } from "@/wasm-communication/messages";
import { type Color } from "@/wasm-communication/messages";
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@ -97,18 +91,15 @@ export default defineComponent({
this.primaryOpen = false;
this.secondaryOpen = true;
},
primaryColorChanged(color: RGBA) {
const newColor = rgbaToDecimalRgba(color);
this.editor.instance.updatePrimaryColor(newColor.r, newColor.g, newColor.b, newColor.a);
primaryColorChanged(color: Color) {
this.editor.instance.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
},
secondaryColorChanged(color: RGBA) {
const newColor = rgbaToDecimalRgba(color);
this.editor.instance.updateSecondaryColor(newColor.r, newColor.g, newColor.b, newColor.a);
secondaryColorChanged(color: Color) {
this.editor.instance.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
},
},
components: {
ColorPicker,
FloatingMenu,
LayoutCol,
LayoutRow,
},

View File

@ -1,58 +0,0 @@
import { type HSVA, type RGBA } from "@/wasm-communication/messages";
export function hsvaToRgba(hsva: HSVA): RGBA {
const { h, s, v, a } = hsva;
const hue = h * 6;
const hueIntegerPart = Math.floor(hue);
const hueFractionalPart = hue - hueIntegerPart;
const hueIntegerMod6 = hueIntegerPart % 6;
const p = v * (1 - s);
const q = v * (1 - hueFractionalPart * s);
const t = v * (1 - (1 - hueFractionalPart) * s);
const r = Math.round([v, q, p, p, t, v][hueIntegerMod6]);
const g = Math.round([t, v, v, q, p, p][hueIntegerMod6]);
const b = Math.round([p, p, t, v, v, q][hueIntegerMod6]);
return { r, g, b, a };
}
export function rgbaToHsva(rgba: RGBA): HSVA {
const { r, g, b, a } = rgba;
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) {
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 };
}
export function rgbaToDecimalRgba(rgba: RGBA): RGBA {
const r = rgba.r / 255;
const g = rgba.g / 255;
const b = rgba.b / 255;
return { r, g, b, a: rgba.a };
}

View File

@ -100,13 +100,6 @@ export type ActionKeys = { keys: KeysGroup };
export type MouseMotion = string;
export type RGBA = {
r: number;
g: number;
b: number;
a: number;
};
export type HSVA = {
h: number;
s: number;
@ -114,26 +107,88 @@ export type HSVA = {
a: number;
};
const To255Scale = Transform(({ value }: { value: number }) => value * 255);
// All channels range from 0 to 1
export class Color {
@To255Scale
constructor();
constructor(hsva: HSVA);
constructor(red: number, green: number, blue: number, alpha: number);
constructor(hsvaOrRed?: HSVA | number, green?: number, blue?: number, alpha?: number) {
// Empty constructor
if (hsvaOrRed === undefined) {
this.red = 0;
this.green = 0;
this.blue = 0;
this.alpha = 0;
}
// HSVA constructor
else if (typeof hsvaOrRed !== "number" && green === undefined && blue === undefined && alpha === undefined) {
const { h, s, v } = hsvaOrRed;
const convert = (n: number): number => {
const k = (n + h * 6) % 6;
return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0);
};
this.red = convert(5);
this.green = convert(3);
this.blue = convert(1);
this.alpha = hsvaOrRed.a;
}
// RGBA constructor
else if (typeof hsvaOrRed === "number" && typeof green === "number" && typeof blue === "number" && typeof alpha === "number") {
this.red = hsvaOrRed;
this.green = green;
this.blue = blue;
this.alpha = alpha;
}
}
readonly red!: number;
@To255Scale
readonly green!: number;
@To255Scale
readonly blue!: number;
readonly alpha!: number;
toRgba(): RGBA {
return { r: this.red, g: this.green, b: this.blue, a: this.alpha };
toRgbCSS(): string {
return `rgb(${this.red * 255}, ${this.green * 255}, ${this.blue * 255})`;
}
toRgbaCSS(): string {
const { r, g, b, a } = this.toRgba();
return `rgba(${r}, ${g}, ${b}, ${a})`;
return `rgba(${this.red * 255}, ${this.green * 255}, ${this.blue * 255}, ${this.alpha})`;
}
toHSVA(): HSVA {
const { red: r, green: g, blue: b, alpha: a } = this;
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) {
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 };
}
}

View File

@ -353,7 +353,7 @@ impl JsEditorHandle {
Ok(())
}
/// Update primary color
/// Update primary color with values on a scale from 0 to 1.
#[wasm_bindgen(js_name = updatePrimaryColor)]
pub fn update_primary_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
let primary_color = match Color::from_rgbaf32(red, green, blue, alpha) {
@ -367,7 +367,7 @@ impl JsEditorHandle {
Ok(())
}
/// Update secondary color
/// Update secondary color with values on a scale from 0 to 1.
#[wasm_bindgen(js_name = updateSecondaryColor)]
pub fn update_secondary_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
let secondary_color = match Color::from_rgbaf32(red, green, blue, alpha) {