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:
parent
1219e26d17
commit
7f9c59dd99
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,30 +1,29 @@
|
|||
<template>
|
||||
<LayoutRow class="color-picker">
|
||||
<LayoutCol class="saturation-picker" ref="saturationPicker" @pointerdown="(e: PointerEvent) => onPointerDown(e)">
|
||||
<div ref="saturationCursor" class="selection-circle"></div>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="hue-picker" ref="huePicker" @pointerdown="(e: PointerEvent) => onPointerDown(e)">
|
||||
<div ref="hueCursor" class="selection-pincers"></div>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="opacity-picker" ref="opacityPicker" @pointerdown="(e: PointerEvent) => onPointerDown(e)">
|
||||
<div ref="opacityCursor" class="selection-pincers"></div>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
<FloatingMenu :open="open" @update:open="(isOpen) => emitOpenState(isOpen)" :direction="direction" :type="'Popover'">
|
||||
<LayoutRow class="color-picker">
|
||||
<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" @pointerdown="(e: PointerEvent) => beginDrag(e)" data-hue-picker>
|
||||
<div class="selection-pincers" :style="{ top: `${(1 - hue) * 100}%` }"></div>
|
||||
</LayoutCol>
|
||||
<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 });
|
||||
},
|
||||
unmounted() {
|
||||
this.removeEvents();
|
||||
computed: {
|
||||
hueColorCSS() {
|
||||
return new Color({ h: this.hue, s: 1, v: 1, a: 1 }).toRgbCSS();
|
||||
},
|
||||
},
|
||||
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";
|
||||
|
||||
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();
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
this.removeEvents();
|
||||
},
|
||||
components: {
|
||||
FloatingMenu,
|
||||
LayoutCol,
|
||||
LayoutRow,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue