Remake Eyedropper tool to sample pixel colors from viewport canvas (#801)
* Remake Eyedropper tool to sample pixel colors from viewport canvas * Bug fixes * Reorder export buttons * Remove the larger primary/secondary ring * Add aborting with Escape
This commit is contained in:
parent
fe1a03fac7
commit
58a53a995d
|
|
@ -63,7 +63,7 @@ impl PropertyHolder for ExportDialogMessageHandler {
|
|||
})),
|
||||
];
|
||||
|
||||
let entries = [(FileType::Svg, "SVG"), (FileType::Png, "PNG"), (FileType::Jpg, "JPG")]
|
||||
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
|
||||
.into_iter()
|
||||
.map(|(val, name)| RadioEntryData {
|
||||
label: name.into(),
|
||||
|
|
|
|||
|
|
@ -157,6 +157,16 @@ pub enum FrontendMessage {
|
|||
size: (f64, f64),
|
||||
multiplier: (f64, f64),
|
||||
},
|
||||
UpdateEyedropperSamplingState {
|
||||
#[serde(rename = "mousePosition")]
|
||||
mouse_position: Option<(f64, f64)>,
|
||||
#[serde(rename = "primaryColor")]
|
||||
primary_color: String,
|
||||
#[serde(rename = "secondaryColor")]
|
||||
secondary_color: String,
|
||||
#[serde(rename = "setColorChoice")]
|
||||
set_color_choice: Option<String>,
|
||||
},
|
||||
UpdateImageData {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: u64,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub struct FrontendImageData {
|
|||
pub enum MouseCursorIcon {
|
||||
#[default]
|
||||
Default,
|
||||
None,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Grabbing,
|
||||
|
|
@ -36,17 +37,17 @@ pub enum MouseCursorIcon {
|
|||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum FileType {
|
||||
#[default]
|
||||
Svg,
|
||||
Png,
|
||||
Jpg,
|
||||
Svg,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub fn to_mime(self) -> &'static str {
|
||||
match self {
|
||||
FileType::Svg => "image/svg+xml",
|
||||
FileType::Png => "image/png",
|
||||
FileType::Jpg => "image/jpeg",
|
||||
FileType::Svg => "image/svg+xml",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,8 +92,12 @@ pub fn default_mapping() -> Mapping {
|
|||
entry!(KeyUp(Mmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
|
||||
//
|
||||
// EyedropperToolMessage
|
||||
entry!(KeyDown(Lmb); action_dispatch=EyedropperToolMessage::LeftMouseDown),
|
||||
entry!(KeyDown(Rmb); action_dispatch=EyedropperToolMessage::RightMouseDown),
|
||||
entry!(PointerMove; action_dispatch=EyedropperToolMessage::PointerMove),
|
||||
entry!(KeyDown(Lmb); action_dispatch=EyedropperToolMessage::LeftPointerDown),
|
||||
entry!(KeyDown(Rmb); action_dispatch=EyedropperToolMessage::RightPointerDown),
|
||||
entry!(KeyUp(Lmb); action_dispatch=EyedropperToolMessage::LeftPointerUp),
|
||||
entry!(KeyUp(Rmb); action_dispatch=EyedropperToolMessage::RightPointerUp),
|
||||
entry!(KeyDown(Escape); action_dispatch=EyedropperToolMessage::Abort),
|
||||
//
|
||||
// TextToolMessage
|
||||
entry!(KeyUp(Lmb); action_dispatch=TextToolMessage::Interact),
|
||||
|
|
@ -170,8 +174,8 @@ pub fn default_mapping() -> Mapping {
|
|||
entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm),
|
||||
//
|
||||
// FillToolMessage
|
||||
entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftMouseDown),
|
||||
entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightMouseDown),
|
||||
entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftPointerDown),
|
||||
entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightPointerDown),
|
||||
//
|
||||
// ToolMessage
|
||||
entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ impl ViewportBounds {
|
|||
pub fn center(&self) -> DVec2 {
|
||||
self.bottom_right.lerp(self.top_left, 0.5)
|
||||
}
|
||||
|
||||
pub fn in_bounds(&self, position: ViewportPosition) -> bool {
|
||||
position.x >= 0. && position.y >= 0. && position.x <= self.bottom_right.x && position.y <= self.bottom_right.y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -910,8 +910,8 @@ 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 the importance of a word or phrase, wrap it in quotes ending with a colon and a multiplier, for example:\n\
|
||||
\"(colorless:0.7) green (ideas sleep:1.3) furiously\"
|
||||
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\
|
||||
\"Colorless green ideas (sleep:1.3) furiously\"
|
||||
"
|
||||
.trim()
|
||||
.into(),
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
|
|||
return;
|
||||
}
|
||||
|
||||
// Send the Abort state transition to the tool
|
||||
// Send the old and new tools a transition to their FSM Abort states
|
||||
let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| {
|
||||
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
|
||||
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
|
||||
|
|
@ -82,8 +82,6 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Send the old and new tools a transition to their FSM Abort states
|
||||
send_abort_to_tool(tool_type, true);
|
||||
send_abort_to_tool(old_tool, false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
use crate::consts::SELECTION_TOLERANCE;
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
|
||||
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
use graphene::intersection::Quad;
|
||||
use graphene::layers::layer_info::LayerDataType;
|
||||
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -27,8 +22,11 @@ pub enum EyedropperToolMessage {
|
|||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
LeftPointerDown,
|
||||
LeftPointerUp,
|
||||
PointerMove,
|
||||
RightPointerDown,
|
||||
RightPointerUp,
|
||||
}
|
||||
|
||||
impl ToolMetadata for EyedropperTool {
|
||||
|
|
@ -67,8 +65,12 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for EyedropperTo
|
|||
}
|
||||
|
||||
advertise_actions!(EyedropperToolMessageDiscriminant;
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
LeftPointerDown,
|
||||
LeftPointerUp,
|
||||
PointerMove,
|
||||
RightPointerDown,
|
||||
RightPointerUp,
|
||||
Abort,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +87,8 @@ impl ToolTransition for EyedropperTool {
|
|||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum EyedropperToolFsmState {
|
||||
Ready,
|
||||
SamplingPrimary,
|
||||
SamplingSecondary,
|
||||
}
|
||||
|
||||
impl Default for EyedropperToolFsmState {
|
||||
|
|
@ -104,7 +108,7 @@ impl Fsm for EyedropperToolFsmState {
|
|||
self,
|
||||
event: ToolMessage,
|
||||
_tool_data: &mut Self::ToolData,
|
||||
(document, _document_id, _global_tool_data, input, font_cache): ToolActionHandlerData,
|
||||
(_document, _document_id, global_tool_data, input, _font_cache): ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
|
|
@ -113,28 +117,41 @@ impl Fsm for EyedropperToolFsmState {
|
|||
|
||||
if let ToolMessage::Eyedropper(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
|
||||
let mouse_pos = input.mouse.position;
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
||||
// Ready -> Sampling
|
||||
(Ready, mouse_down) | (Ready, mouse_down) if mouse_down == LeftPointerDown || mouse_down == RightPointerDown => {
|
||||
update_cursor_preview(responses, input, global_tool_data, None);
|
||||
|
||||
// TODO: Destroy this pyramid
|
||||
if let Some(path) = document.graphene_document.intersects_quad_root(quad, font_cache).last() {
|
||||
if let Ok(layer) = document.graphene_document.layer(path) {
|
||||
if let LayerDataType::Shape(shape) = &layer.data {
|
||||
if shape.style.fill().is_some() {
|
||||
match lmb_or_rmb {
|
||||
EyedropperToolMessage::LeftMouseDown => responses.push_back(ToolMessage::SelectPrimaryColor { color: shape.style.fill().color() }.into()),
|
||||
EyedropperToolMessage::RightMouseDown => responses.push_back(ToolMessage::SelectSecondaryColor { color: shape.style.fill().color() }.into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if mouse_down == LeftPointerDown {
|
||||
SamplingPrimary
|
||||
} else {
|
||||
SamplingSecondary
|
||||
}
|
||||
}
|
||||
// Sampling -> Sampling
|
||||
(SamplingPrimary, PointerMove) | (SamplingSecondary, PointerMove) => {
|
||||
if input.viewport_bounds.in_bounds(input.mouse.position) {
|
||||
update_cursor_preview(responses, input, global_tool_data, None);
|
||||
} else {
|
||||
disable_cursor_preview(responses);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
// Sampling -> Ready
|
||||
(SamplingPrimary, mouse_up) | (SamplingSecondary, mouse_up) if mouse_up == LeftPointerUp || mouse_up == RightPointerUp => {
|
||||
let set_color_choice = if self == SamplingPrimary { "Primary".to_string() } else { "Secondary".to_string() };
|
||||
update_cursor_preview(responses, input, global_tool_data, Some(set_color_choice));
|
||||
disable_cursor_preview(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
// Any -> Ready
|
||||
(_, Abort) => {
|
||||
disable_cursor_preview(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
// Ready -> Ready
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -160,12 +177,43 @@ impl Fsm for EyedropperToolFsmState {
|
|||
plus: false,
|
||||
},
|
||||
])]),
|
||||
EyedropperToolFsmState::SamplingPrimary => HintData(vec![]),
|
||||
EyedropperToolFsmState::SamplingSecondary => HintData(vec![]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
let cursor = match *self {
|
||||
EyedropperToolFsmState::Ready => MouseCursorIcon::Default,
|
||||
EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary => MouseCursorIcon::None,
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn disable_cursor_preview(responses: &mut VecDeque<Message>) {
|
||||
responses.push_back(
|
||||
FrontendMessage::UpdateEyedropperSamplingState {
|
||||
mouse_position: None,
|
||||
primary_color: "".into(),
|
||||
secondary_color: "".into(),
|
||||
set_color_choice: None,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
fn update_cursor_preview(responses: &mut VecDeque<Message>, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option<String>) {
|
||||
responses.push_back(
|
||||
FrontendMessage::UpdateEyedropperSamplingState {
|
||||
mouse_position: Some(input.mouse.position.into()),
|
||||
primary_color: "#".to_string() + global_tool_data.primary_color.rgb_hex().as_str(),
|
||||
secondary_color: "#".to_string() + global_tool_data.secondary_color.rgb_hex().as_str(),
|
||||
set_color_choice,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ pub enum FillToolMessage {
|
|||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
LeftPointerDown,
|
||||
RightPointerDown,
|
||||
}
|
||||
|
||||
impl ToolMetadata for FillTool {
|
||||
|
|
@ -68,8 +68,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for FillTool {
|
|||
}
|
||||
|
||||
advertise_actions!(FillToolMessageDiscriminant;
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
LeftPointerDown,
|
||||
RightPointerDown,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -114,15 +114,15 @@ impl Fsm for FillToolFsmState {
|
|||
|
||||
if let ToolMessage::Fill(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
|
||||
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftPointerDown || lmb_or_rmb == RightPointerDown => {
|
||||
let mouse_pos = input.mouse.position;
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
||||
|
||||
if let Some(path) = document.graphene_document.intersects_quad_root(quad, font_cache).last() {
|
||||
let color = match lmb_or_rmb {
|
||||
LeftMouseDown => global_tool_data.primary_color,
|
||||
RightMouseDown => global_tool_data.secondary_color,
|
||||
LeftPointerDown => global_tool_data.primary_color,
|
||||
RightPointerDown => global_tool_data.secondary_color,
|
||||
Abort => unreachable!(),
|
||||
};
|
||||
let fill = Fill::Solid(color);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ impl DocumentToolData {
|
|||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(EyedropperToolMessage::PointerMove.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<FloatingMenu
|
||||
:open="true"
|
||||
class="eyedropper-preview"
|
||||
:type="'Cursor'"
|
||||
:style="{ '--ring-color-primary': primaryColor, '--ring-color-secondary': secondaryColor, '--ring-color-choice': colorChoice }"
|
||||
>
|
||||
<div class="ring">
|
||||
<div class="canvas-container">
|
||||
<canvas ref="zoomPreviewCanvas"></canvas>
|
||||
<div class="pixel-outline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</FloatingMenu>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.eyedropper-preview {
|
||||
pointer-events: none;
|
||||
|
||||
.ring {
|
||||
transform: translate(0, -50%) rotate(45deg);
|
||||
position: relative;
|
||||
background: var(--ring-color-choice);
|
||||
padding: 16px;
|
||||
border: 8px solid;
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--ring-color-primary);
|
||||
border-left-color: var(--ring-color-primary);
|
||||
border-bottom-color: var(--ring-color-secondary);
|
||||
border-right-color: var(--ring-color-secondary);
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
|
||||
|
||||
.canvas-container {
|
||||
transform: rotate(-45deg);
|
||||
|
||||
canvas {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 50%;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.pixel-outline {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
--outline-width: 2;
|
||||
margin-top: calc(-1px * (var(--outline-width) / 2));
|
||||
width: calc(10px - (var(--outline-width) * 1px));
|
||||
height: calc(10px - var(--outline-width) * 1px);
|
||||
border: calc(var(--outline-width) * 1px) solid var(--color-0-black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
|
||||
|
||||
// Should be equal to the width and height of the canvas in the CSS above
|
||||
const ZOOM_WINDOW_DIMENSIONS_EXPANDED = 110;
|
||||
// SHould be equal to the width and height of the `.pixel-outline` div in the CSS above, and should be evenly divisible into the number above
|
||||
const UPSCALE_FACTOR = 10;
|
||||
|
||||
export const ZOOM_WINDOW_DIMENSIONS = ZOOM_WINDOW_DIMENSIONS_EXPANDED / UPSCALE_FACTOR;
|
||||
|
||||
const temporaryCanvas = document.createElement("canvas");
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
imageData: { type: Object as PropType<ImageData> },
|
||||
colorChoice: { type: String as PropType<string>, required: true },
|
||||
primaryColor: { type: String as PropType<string>, required: true },
|
||||
secondaryColor: { type: String as PropType<string>, required: true },
|
||||
},
|
||||
mounted() {
|
||||
this.displayImageDataPreview(this.imageData);
|
||||
},
|
||||
watch: {
|
||||
imageData(imageData: ImageData | undefined) {
|
||||
this.displayImageDataPreview(imageData);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
displayImageDataPreview(imageData: ImageData | undefined) {
|
||||
const canvas = this.$refs.zoomPreviewCanvas as HTMLCanvasElement;
|
||||
canvas.width = ZOOM_WINDOW_DIMENSIONS;
|
||||
canvas.height = ZOOM_WINDOW_DIMENSIONS;
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
|
||||
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
|
||||
const temporaryContext = temporaryCanvas.getContext("2d");
|
||||
|
||||
if (!imageData || !context || !temporaryContext) return;
|
||||
|
||||
temporaryContext.putImageData(imageData, 0, 0, 0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
|
||||
|
||||
context.fillStyle = "black";
|
||||
context.fillRect(0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
|
||||
|
||||
context.drawImage(temporaryCanvas, 0, 0);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
FloatingMenu,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -110,6 +110,13 @@
|
|||
--floating-menu-content-border-radius: 4px;
|
||||
}
|
||||
|
||||
&.cursor .floating-menu-container .floating-menu-content {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
@ -180,7 +187,7 @@ import { defineComponent, nextTick, type PropType } from "vue";
|
|||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
|
||||
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
|
||||
export type MenuType = "Popover" | "Dropdown" | "Dialog";
|
||||
export type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
|
||||
|
||||
const POINTER_STRAY_DISTANCE = 100;
|
||||
|
||||
|
|
@ -235,6 +242,8 @@ export default defineComponent({
|
|||
this.minWidthParentWidth = entries[0].contentRect.width;
|
||||
},
|
||||
positionAndStyleFloatingMenu() {
|
||||
if (this.type === "Cursor") return;
|
||||
|
||||
const workspace = document.querySelector("[data-workspace]");
|
||||
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
|
||||
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
|
||||
|
|
|
|||
|
|
@ -28,25 +28,25 @@
|
|||
<LayoutCol class="bar-area">
|
||||
<CanvasRuler :origin="rulerOrigin.y" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Vertical'" ref="rulerVertical" />
|
||||
</LayoutCol>
|
||||
<LayoutCol class="canvas-area">
|
||||
<div
|
||||
class="canvas"
|
||||
:style="{ cursor: canvasCursor }"
|
||||
@pointerdown="(e: PointerEvent) => canvasPointerDown(e)"
|
||||
@dragover="(e) => e.preventDefault()"
|
||||
@drop="(e) => pasteFile(e)"
|
||||
ref="canvas"
|
||||
data-canvas
|
||||
>
|
||||
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
|
||||
<LayoutCol class="canvas-area" :style="{ cursor: canvasCursor }">
|
||||
<EyedropperPreview
|
||||
v-if="cursorEyedropper"
|
||||
:colorChoice="cursorEyedropperPreviewColorChoice"
|
||||
:primaryColor="cursorEyedropperPreviewColorPrimary"
|
||||
:secondaryColor="cursorEyedropperPreviewColorSecondary"
|
||||
:imageData="cursorEyedropperPreviewImageData"
|
||||
:style="{ left: cursorLeft + 'px', top: cursorTop + 'px' }"
|
||||
/>
|
||||
<div class="canvas" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)" @dragover="(e) => e.preventDefault()" @drop="(e) => pasteFile(e)" ref="canvas" data-canvas>
|
||||
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasWidthCSS, height: canvasHeightCSS }"></svg>
|
||||
<svg
|
||||
class="artwork"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
v-html="artworkSvg"
|
||||
:style="{ width: canvasSvgWidth, height: canvasSvgHeight }"
|
||||
:style="{ width: canvasWidthCSS, height: canvasHeightCSS }"
|
||||
></svg>
|
||||
<svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
|
||||
<svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasWidthCSS, height: canvasHeightCSS }"></svg>
|
||||
</div>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="bar-area">
|
||||
|
|
@ -149,6 +149,7 @@
|
|||
|
||||
.canvas-area {
|
||||
flex: 1 1 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar-area {
|
||||
|
|
@ -224,6 +225,7 @@
|
|||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
import { textInputCleanup } from "@/utility-functions/keyboard-entry";
|
||||
import { rasterizeSVGCanvas } from "@/utility-functions/rasterization";
|
||||
import {
|
||||
defaultWidgetLayout,
|
||||
type DisplayEditableTextbox,
|
||||
|
|
@ -236,6 +238,7 @@ import {
|
|||
type XY,
|
||||
} from "@/wasm-communication/messages";
|
||||
|
||||
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@/components/floating-menus/EyedropperPreview.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import CanvasRuler from "@/components/widgets/metrics/CanvasRuler.vue";
|
||||
|
|
@ -244,6 +247,64 @@ import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
|||
|
||||
export default defineComponent({
|
||||
inject: ["editor", "panels"],
|
||||
data() {
|
||||
const scrollbarPos: XY = { x: 0.5, y: 0.5 };
|
||||
const scrollbarSize: XY = { x: 0.5, y: 0.5 };
|
||||
const scrollbarMultiplier: XY = { x: 0, y: 0 };
|
||||
|
||||
const rulerOrigin: XY = { x: 0, y: 0 };
|
||||
|
||||
return {
|
||||
// Interactive text editing
|
||||
textInput: undefined as undefined | HTMLDivElement,
|
||||
|
||||
// CSS properties
|
||||
canvasSvgWidth: undefined as number | undefined,
|
||||
canvasSvgHeight: undefined as number | undefined,
|
||||
canvasCursor: "default" as MouseCursorIcon,
|
||||
|
||||
// Scrollbars
|
||||
scrollbarPos,
|
||||
scrollbarSize,
|
||||
scrollbarMultiplier,
|
||||
|
||||
// Rulers
|
||||
rulerOrigin,
|
||||
rulerSpacing: 100 as number,
|
||||
rulerInterval: 100 as number,
|
||||
|
||||
// Rendered SVG viewport data
|
||||
artworkSvg: "" as string,
|
||||
artboardSvg: "" as string,
|
||||
overlaysSvg: "" as string,
|
||||
|
||||
// Rasterized SVG viewport data, or none if it's not up-to-date
|
||||
rasterizedCanvas: undefined as HTMLCanvasElement | undefined,
|
||||
rasterizedContext: undefined as CanvasRenderingContext2D | undefined,
|
||||
|
||||
// Cursor position for cursor floating menus like the Eyedropper tool zoom
|
||||
cursorLeft: 0,
|
||||
cursorTop: 0,
|
||||
cursorEyedropper: false,
|
||||
cursorEyedropperPreviewImageData: undefined as ImageData | undefined,
|
||||
cursorEyedropperPreviewColorChoice: "",
|
||||
cursorEyedropperPreviewColorPrimary: "",
|
||||
cursorEyedropperPreviewColorSecondary: "",
|
||||
|
||||
// Layouts
|
||||
documentModeLayout: defaultWidgetLayout(),
|
||||
toolOptionsLayout: defaultWidgetLayout(),
|
||||
documentBarLayout: defaultWidgetLayout(),
|
||||
toolShelfLayout: defaultWidgetLayout(),
|
||||
workingColorsLayout: defaultWidgetLayout(),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.panels.registerPanel("Document", this);
|
||||
|
||||
// Once this component is mounted, we want to resend the document bounds to the backend via the resize event handler which does that
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
methods: {
|
||||
pasteFile(e: DragEvent) {
|
||||
const { dataTransfer } = e;
|
||||
|
|
@ -288,6 +349,7 @@ export default defineComponent({
|
|||
// Update rendered SVGs
|
||||
async updateDocumentArtwork(svg: string) {
|
||||
this.artworkSvg = svg;
|
||||
this.rasterizedCanvas = undefined;
|
||||
|
||||
await nextTick();
|
||||
|
||||
|
|
@ -321,6 +383,57 @@ export default defineComponent({
|
|||
},
|
||||
updateDocumentArtboards(svg: string) {
|
||||
this.artboardSvg = svg;
|
||||
this.rasterizedCanvas = undefined;
|
||||
},
|
||||
async updateEyedropperSamplingState(mousePosition: XY | undefined, colorPrimary: string, colorSecondary: string): Promise<[number, number, number] | undefined> {
|
||||
if (mousePosition === undefined) {
|
||||
this.cursorEyedropper = false;
|
||||
return undefined;
|
||||
}
|
||||
this.cursorEyedropper = true;
|
||||
|
||||
if (this.canvasSvgWidth === undefined || this.canvasSvgHeight === undefined) return undefined;
|
||||
|
||||
this.cursorLeft = mousePosition.x;
|
||||
this.cursorTop = mousePosition.y;
|
||||
|
||||
// This works nearly perfectly, but sometimes at odd DPI scale factors like 1.25, the anti-aliasing color can yield slightly incorrect colors (potential room for future improvement)
|
||||
const dpiFactor = window.devicePixelRatio;
|
||||
const [width, height] = [this.canvasSvgWidth, this.canvasSvgHeight];
|
||||
|
||||
const outsideArtboardsColor = getComputedStyle(document.documentElement).getPropertyValue("--color-2-mildblack");
|
||||
const outsideArtboards = `<rect x="0" y="0" width="100%" height="100%" fill="${outsideArtboardsColor}" />`;
|
||||
const artboards = this.artboardSvg;
|
||||
const artwork = this.artworkSvg;
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${outsideArtboards}${artboards}${artwork}</svg>
|
||||
`.trim();
|
||||
|
||||
if (!this.rasterizedCanvas) {
|
||||
this.rasterizedCanvas = await rasterizeSVGCanvas(svg, width * dpiFactor, height * dpiFactor, "image/png");
|
||||
this.rasterizedContext = this.rasterizedCanvas.getContext("2d") || undefined;
|
||||
}
|
||||
if (!this.rasterizedContext) return undefined;
|
||||
|
||||
const rgbToHex = (r: number, g: number, b: number): string => `#${[r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
|
||||
|
||||
const pixel = this.rasterizedContext.getImageData(mousePosition.x * dpiFactor, mousePosition.y * dpiFactor, 1, 1).data;
|
||||
const hex = rgbToHex(pixel[0], pixel[1], pixel[2]);
|
||||
const rgb: [number, number, number] = [pixel[0] / 255, pixel[1] / 255, pixel[2] / 255];
|
||||
|
||||
this.cursorEyedropperPreviewColorChoice = hex;
|
||||
this.cursorEyedropperPreviewColorPrimary = colorPrimary;
|
||||
this.cursorEyedropperPreviewColorSecondary = colorSecondary;
|
||||
|
||||
const previewRegion = this.rasterizedContext.getImageData(
|
||||
mousePosition.x * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2,
|
||||
mousePosition.y * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2,
|
||||
ZOOM_WINDOW_DIMENSIONS,
|
||||
ZOOM_WINDOW_DIMENSIONS
|
||||
);
|
||||
this.cursorEyedropperPreviewImageData = previewRegion;
|
||||
|
||||
return rgb;
|
||||
},
|
||||
// Update scrollbars and rulers
|
||||
updateDocumentScrollbars(position: XY, size: XY, multiplier: XY) {
|
||||
|
|
@ -383,12 +496,11 @@ export default defineComponent({
|
|||
// Resize elements to render the new viewport size
|
||||
viewportResize() {
|
||||
// Resize the canvas
|
||||
// Width and height are rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
|
||||
const canvas = this.$refs.canvas as HTMLElement;
|
||||
const width = Math.ceil(parseFloat(getComputedStyle(canvas).width));
|
||||
const height = Math.ceil(parseFloat(getComputedStyle(canvas).height));
|
||||
this.canvasSvgWidth = `${width % 2 === 1 ? width + 1 : width}px`;
|
||||
this.canvasSvgHeight = `${height % 2 === 1 ? height + 1 : height}px`;
|
||||
this.canvasSvgWidth = width;
|
||||
this.canvasSvgHeight = height;
|
||||
|
||||
// Resize the rulers
|
||||
const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler;
|
||||
|
|
@ -396,51 +508,22 @@ export default defineComponent({
|
|||
rulerHorizontal?.resize();
|
||||
rulerVertical?.resize();
|
||||
},
|
||||
canvasDimensionCSS(dimension: number | undefined): string {
|
||||
// Temporary placeholder until the first actual value is populated
|
||||
// This at least gets close to the correct value but an actual number is required to prevent CSS from causing non-integer sizing making the SVG render with anti-aliasing
|
||||
if (dimension === undefined) return "100%";
|
||||
|
||||
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
|
||||
return `${dimension % 2 === 1 ? dimension + 1 : dimension}px`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.panels.registerPanel("Document", this);
|
||||
|
||||
// Once this component is mounted, we want to resend the document bounds to the backend via the resize event handler which does that
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
data() {
|
||||
const scrollbarPos: XY = { x: 0.5, y: 0.5 };
|
||||
const scrollbarSize: XY = { x: 0.5, y: 0.5 };
|
||||
const scrollbarMultiplier: XY = { x: 0, y: 0 };
|
||||
|
||||
const rulerOrigin: XY = { x: 0, y: 0 };
|
||||
|
||||
return {
|
||||
// Interactive text editing
|
||||
textInput: undefined as undefined | HTMLDivElement,
|
||||
|
||||
// CSS properties
|
||||
canvasSvgWidth: "100%" as string,
|
||||
canvasSvgHeight: "100%" as string,
|
||||
canvasCursor: "default" as MouseCursorIcon,
|
||||
|
||||
// Scrollbars
|
||||
scrollbarPos,
|
||||
scrollbarSize,
|
||||
scrollbarMultiplier,
|
||||
|
||||
// Rulers
|
||||
rulerOrigin,
|
||||
rulerSpacing: 100 as number,
|
||||
rulerInterval: 100 as number,
|
||||
|
||||
// Rendered SVG viewport data
|
||||
artworkSvg: "" as string,
|
||||
artboardSvg: "" as string,
|
||||
overlaysSvg: "" as string,
|
||||
|
||||
// Layouts
|
||||
documentModeLayout: defaultWidgetLayout(),
|
||||
toolOptionsLayout: defaultWidgetLayout(),
|
||||
documentBarLayout: defaultWidgetLayout(),
|
||||
toolShelfLayout: defaultWidgetLayout(),
|
||||
workingColorsLayout: defaultWidgetLayout(),
|
||||
};
|
||||
computed: {
|
||||
canvasWidthCSS(): string {
|
||||
return this.canvasDimensionCSS(this.canvasSvgWidth);
|
||||
},
|
||||
canvasHeightCSS(): string {
|
||||
return this.canvasDimensionCSS(this.canvasSvgHeight);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
CanvasRuler,
|
||||
|
|
@ -448,6 +531,7 @@ export default defineComponent({
|
|||
LayoutRow,
|
||||
PersistentScrollbar,
|
||||
WidgetLayout,
|
||||
EyedropperPreview,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
UpdateDocumentOverlays,
|
||||
UpdateDocumentRulers,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateEyedropperSamplingState,
|
||||
UpdateMouseCursor,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
|
|
@ -48,6 +49,16 @@ export function createPanelsState(editor: Editor) {
|
|||
await nextTick();
|
||||
state.documentPanel.updateDocumentArtboards(updateDocumentArtboards.svg);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateEyedropperSamplingState, async (updateEyedropperSamplingState) => {
|
||||
await nextTick();
|
||||
const { mousePosition, primaryColor, secondaryColor, setColorChoice } = updateEyedropperSamplingState;
|
||||
const rgb = (await state.documentPanel.updateEyedropperSamplingState(mousePosition, primaryColor, secondaryColor)) as [number, number, number] | undefined;
|
||||
|
||||
if (setColorChoice && rgb) {
|
||||
if (setColorChoice === "Primary") editor.instance.updatePrimaryColor(...rgb, 1);
|
||||
if (setColorChoice === "Secondary") editor.instance.updateSecondaryColor(...rgb, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Update scrollbars and rulers
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentScrollbars, async (updateDocumentScrollbars) => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { blobToBase64 } from "@/utility-functions/files";
|
|||
import { type RequestResult, requestWithUploadDownloadProgress } from "@/utility-functions/network";
|
||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import type { XY } from "@/wasm-communication/messages";
|
||||
import { type ImaginateGenerationParameters } from "@/wasm-communication/messages";
|
||||
|
||||
const MAX_POLLING_RETRIES = 4;
|
||||
|
|
@ -73,7 +74,7 @@ export async function imaginateGenerate(
|
|||
|
||||
// Send the backend a blob URL for the final image
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, parameters.resolution[0], parameters.resolution[1]);
|
||||
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, parameters.resolution.x, parameters.resolution.y);
|
||||
|
||||
// Send the backend the blob data to be stored persistently in the layer
|
||||
const u8Array = new Uint8Array(await blob.arrayBuffer());
|
||||
|
|
@ -134,7 +135,7 @@ function scheduleNextPollingUpdate(
|
|||
hostname: string,
|
||||
documentId: bigint,
|
||||
layerPath: BigUint64Array,
|
||||
resolution: [number, number]
|
||||
resolution: XY
|
||||
): void {
|
||||
// Pick a future time that keeps to the user-requested interval if possible, but on slower connections will go as fast as possible without overlapping itself
|
||||
const nextPollTimeGoal = timeoutBegan + interval;
|
||||
|
|
@ -148,7 +149,7 @@ function scheduleNextPollingUpdate(
|
|||
if (terminated) return;
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, resolution[0], resolution[1]);
|
||||
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, resolution.x, resolution.y);
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, percentComplete, "Generating");
|
||||
|
||||
scheduleNextPollingUpdate(interval, nextTimeoutBegan, 0, editor, hostname, documentId, layerPath, resolution);
|
||||
|
|
@ -244,8 +245,8 @@ async function generate(
|
|||
0,
|
||||
0,
|
||||
false,
|
||||
${parameters.resolution[1]},
|
||||
${parameters.resolution[0]},
|
||||
${parameters.resolution.y},
|
||||
${parameters.resolution.x},
|
||||
false,
|
||||
0.7,
|
||||
0,
|
||||
|
|
@ -301,8 +302,8 @@ async function generate(
|
|||
0,
|
||||
0,
|
||||
false,
|
||||
${parameters.resolution[1]},
|
||||
${parameters.resolution[0]},
|
||||
${parameters.resolution.y},
|
||||
${parameters.resolution.x},
|
||||
"Just resize",
|
||||
false,
|
||||
32,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { replaceBlobURLsWithBase64 } from "@/utility-functions/files";
|
||||
|
||||
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
||||
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
||||
let promiseResolve: (value: Blob | PromiseLike<Blob>) => void | undefined;
|
||||
let promiseReject: () => void | undefined;
|
||||
const promise = new Promise<Blob>((resolve, reject) => {
|
||||
export async function rasterizeSVGCanvas(svg: string, width: number, height: number, backgroundColor?: string): Promise<HTMLCanvasElement> {
|
||||
let promiseResolve: (value: HTMLCanvasElement | PromiseLike<HTMLCanvasElement>) => void | undefined;
|
||||
const promise = new Promise<HTMLCanvasElement>((resolve) => {
|
||||
promiseResolve = resolve;
|
||||
promiseReject = reject;
|
||||
});
|
||||
|
||||
// A canvas to render our svg to in order to get a raster image
|
||||
|
|
@ -14,8 +12,8 @@ export async function rasterizeSVG(svg: string, width: number, height: number, m
|
|||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return Promise.reject();
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) throw new Error("Can't create 2D context from canvas during SVG rasterization");
|
||||
|
||||
// Apply a background fill color if one is given
|
||||
if (backgroundColor) {
|
||||
|
|
@ -37,13 +35,28 @@ export async function rasterizeSVG(svg: string, width: number, height: number, m
|
|||
// Clean up the SVG blob URL (once the URL is revoked, the SVG blob data itself is garbage collected after `svgBlob` goes out of scope)
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Convert the canvas to an image of the correct MIME type
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob !== null) promiseResolve(blob);
|
||||
else promiseReject();
|
||||
}, mime);
|
||||
promiseResolve(canvas);
|
||||
};
|
||||
image.src = url;
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
||||
let promiseResolve: (value: Blob | PromiseLike<Blob>) => void | undefined;
|
||||
let promiseReject: () => void | undefined;
|
||||
const promise = new Promise<Blob>((resolve, reject) => {
|
||||
promiseResolve = resolve;
|
||||
promiseReject = reject;
|
||||
});
|
||||
|
||||
rasterizeSVGCanvas(svg, width, height, backgroundColor).then((canvas) => {
|
||||
// Convert the canvas to an image of the correct MIME type
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob !== null) promiseResolve(blob);
|
||||
else promiseReject();
|
||||
}, mime);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ export class UpdateDocumentArtboards extends JsMessage {
|
|||
}
|
||||
|
||||
const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] }));
|
||||
const BigIntTupleToNumberTuple = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : [Number(value[0]), Number(value[1])]));
|
||||
const BigIntTupleToVec2 = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : { x: Number(value[0]), y: Number(value[1]) }));
|
||||
|
||||
export type XY = { x: number; y: number };
|
||||
|
||||
|
|
@ -190,7 +190,19 @@ export class UpdateDocumentRulers extends JsMessage {
|
|||
readonly interval!: number;
|
||||
}
|
||||
|
||||
export class UpdateEyedropperSamplingState extends JsMessage {
|
||||
@TupleToVec2
|
||||
readonly mousePosition!: XY | undefined;
|
||||
|
||||
readonly primaryColor!: string;
|
||||
|
||||
readonly secondaryColor!: string;
|
||||
|
||||
readonly setColorChoice!: "Primary" | "Secondary" | undefined;
|
||||
}
|
||||
|
||||
const mouseCursorIconCSSNames = {
|
||||
None: "none",
|
||||
ZoomIn: "zoom-in",
|
||||
ZoomOut: "zoom-out",
|
||||
Grabbing: "grabbing",
|
||||
|
|
@ -278,8 +290,8 @@ export class ImaginateGenerationParameters {
|
|||
|
||||
readonly negativePrompt!: string;
|
||||
|
||||
@BigIntTupleToNumberTuple
|
||||
readonly resolution!: [number, number];
|
||||
@BigIntTupleToVec2
|
||||
readonly resolution!: XY;
|
||||
|
||||
readonly restoreFaces!: boolean;
|
||||
|
||||
|
|
@ -1008,6 +1020,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
UpdateDocumentModeLayout,
|
||||
UpdateDocumentOverlays,
|
||||
UpdateDocumentRulers,
|
||||
UpdateEyedropperSamplingState,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateImageData,
|
||||
UpdateInputHints,
|
||||
|
|
|
|||
Loading…
Reference in New Issue