mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 23:08:11 +08:00
Polish and add aborting to several input widgets: no Esc closing parent menus; color picker axis align; repeat on arrow buttons (#2276)
* Remove color input outline; reduce antialiasing compositing artifacts in color widgets * Rename ColorButton to ColorInput * Add features and aborting to several other widgets - Prevent Esc from closing parent floating menus when aborting - Fix missing icon regression - Gutter resizing abort - Color picker aborts, Shift axis alignment, improve click/drag behavior for gradient spectrum - Scrollbar abort, repeat when held, fix directional arrows when viewport is zoomed - Number input abort, repeat when held * Move ColorInput into the inputs folder * Fix tiny logo
This commit is contained in:
@@ -138,6 +138,7 @@
|
||||
linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(#ffffff, #ffffff);
|
||||
--color-transparent-checkered-background-size: 16px 16px, 16px 16px, 16px 16px;
|
||||
--color-transparent-checkered-background-position: 0 0, 8px 8px, 8px 8px;
|
||||
--color-transparent-checkered-background-position-plus-one: 1px 1px, 9px 9px, 9px 9px;
|
||||
--color-transparent-checkered-background-size-mini: 8px 8px, 8px 8px, 8px 8px;
|
||||
--color-transparent-checkered-background-position-mini: 0 0, 4px 4px, 4px 4px;
|
||||
--color-transparent-checkered-background-repeat: repeat, repeat, repeat;
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { HSV, RGB, FillChoice } from "@graphite/messages";
|
||||
import { Color, Gradient } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
@@ -65,7 +66,19 @@
|
||||
// Transient state
|
||||
let draggingPickerTrack: HTMLDivElement | undefined = undefined;
|
||||
let strayCloses = true;
|
||||
let gradientSpectrumDragging = false;
|
||||
let shiftPressed = false;
|
||||
let alignedAxis: "saturation" | "value" | undefined = undefined;
|
||||
let hueBeforeDrag = 0;
|
||||
let saturationBeforeDrag = 0;
|
||||
let valueBeforeDrag = 0;
|
||||
let alphaBeforeDrag = 0;
|
||||
let saturationStartOfAxisAlign: number | undefined = undefined;
|
||||
let valueStartOfAxisAlign: number | undefined = undefined;
|
||||
let saturationRestoreWhenShiftReleased: number | undefined = undefined;
|
||||
let valueRestoreWhenShiftReleased: number | undefined = undefined;
|
||||
|
||||
let self: FloatingMenu | undefined;
|
||||
let hexCodeInputWidget: TextInput | undefined;
|
||||
let gradientSpectrumInputWidget: SpectrumInput | undefined;
|
||||
|
||||
@@ -77,6 +90,9 @@
|
||||
$: rgbChannels = Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][];
|
||||
$: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][];
|
||||
$: opaqueHueColor = new Color({ h: hue, s: 1, v: 1, a: 1 });
|
||||
$: outlineFactor = Math.max(contrastingOutlineFactor(newColor, "--color-2-mildblack", 0.01), contrastingOutlineFactor(oldColor, "--color-2-mildblack", 0.01));
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: transparency = newColor.alpha < 1 || oldColor.alpha < 1;
|
||||
|
||||
function generateColor(h: number, s: number, v: number, a: number, none: boolean) {
|
||||
if (none) return new Color("none");
|
||||
@@ -119,6 +135,14 @@
|
||||
const target = (e.target || undefined) as HTMLElement | undefined;
|
||||
draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
|
||||
|
||||
hueBeforeDrag = hue;
|
||||
saturationBeforeDrag = saturation;
|
||||
valueBeforeDrag = value;
|
||||
alphaBeforeDrag = alpha;
|
||||
|
||||
saturationStartOfAxisAlign = undefined;
|
||||
valueStartOfAxisAlign = undefined;
|
||||
|
||||
addEvents();
|
||||
|
||||
onPointerMove(e);
|
||||
@@ -134,6 +158,8 @@
|
||||
saturation = clamp((e.clientX - rectangle.left) / rectangle.width, 0, 1);
|
||||
value = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
|
||||
strayCloses = false;
|
||||
|
||||
if (shiftPressed) updateAxisLock();
|
||||
} else if (draggingPickerTrack?.hasAttribute("data-hue-picker")) {
|
||||
const rectangle = draggingPickerTrack.getBoundingClientRect();
|
||||
|
||||
@@ -148,25 +174,102 @@
|
||||
|
||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
setColor(color);
|
||||
|
||||
if (!e.shiftKey) {
|
||||
shiftPressed = false;
|
||||
alignedAxis = undefined;
|
||||
} else if (!shiftPressed && draggingPickerTrack) {
|
||||
shiftPressed = true;
|
||||
saturationStartOfAxisAlign = saturation;
|
||||
valueStartOfAxisAlign = value;
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortDrag();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
const element = self?.div();
|
||||
if (element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
||||
abortDrag();
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
if (e.key === "Shift") {
|
||||
shiftPressed = false;
|
||||
alignedAxis = undefined;
|
||||
|
||||
if (saturationRestoreWhenShiftReleased !== undefined && valueRestoreWhenShiftReleased !== undefined) {
|
||||
saturation = saturationRestoreWhenShiftReleased;
|
||||
value = valueRestoreWhenShiftReleased;
|
||||
|
||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
setColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addEvents() {
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("keyup", onKeyUp);
|
||||
|
||||
dispatch("startHistoryTransaction");
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
draggingPickerTrack = undefined;
|
||||
strayCloses = true;
|
||||
// The setTimeout is necessary to prevent the FloatingMenu's `escapeCloses` from becoming true immediately upon pressing the Escape key, and thus closing
|
||||
setTimeout(() => (strayCloses = true), 0);
|
||||
shiftPressed = false;
|
||||
alignedAxis = undefined;
|
||||
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("keyup", onKeyUp);
|
||||
}
|
||||
|
||||
function updateAxisLock() {
|
||||
if (!saturationStartOfAxisAlign || !valueStartOfAxisAlign) return;
|
||||
|
||||
const deltaSaturation = saturation - saturationStartOfAxisAlign;
|
||||
const deltaValue = value - valueStartOfAxisAlign;
|
||||
|
||||
saturationRestoreWhenShiftReleased = saturation;
|
||||
valueRestoreWhenShiftReleased = value;
|
||||
|
||||
if (Math.abs(deltaSaturation) < Math.abs(deltaValue)) {
|
||||
alignedAxis = "saturation";
|
||||
saturation = saturationStartOfAxisAlign;
|
||||
} else {
|
||||
alignedAxis = "value";
|
||||
value = valueStartOfAxisAlign;
|
||||
}
|
||||
}
|
||||
|
||||
function abortDrag() {
|
||||
removeEvents();
|
||||
|
||||
hue = hueBeforeDrag;
|
||||
saturation = saturationBeforeDrag;
|
||||
value = valueBeforeDrag;
|
||||
alpha = alphaBeforeDrag;
|
||||
|
||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
setColor(color);
|
||||
}
|
||||
|
||||
function setColor(color?: Color) {
|
||||
@@ -299,7 +402,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<FloatingMenu class="color-picker" {open} on:open {strayCloses} {direction} type="Popover">
|
||||
<FloatingMenu class="color-picker" {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
|
||||
<LayoutRow
|
||||
styles={{
|
||||
"--new-color": newColor.toHexOptionalAlpha(),
|
||||
@@ -318,6 +421,15 @@
|
||||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
|
||||
{/if}
|
||||
{#if alignedAxis}
|
||||
<div
|
||||
class="selection-circle-alignment"
|
||||
class:saturation={alignedAxis === "saturation"}
|
||||
class:value={alignedAxis === "value"}
|
||||
style:top={`${(1 - value) * 100}%`}
|
||||
style:left={`${saturation * 100}%`}
|
||||
/>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
|
||||
{#if !isNone}
|
||||
@@ -340,6 +452,7 @@
|
||||
}}
|
||||
on:activeMarkerIndexChange={gradientActiveMarkerIndexChange}
|
||||
activeMarkerIndex={activeIndex}
|
||||
on:dragging={({ detail }) => (gradientSpectrumDragging = detail)}
|
||||
bind:this={gradientSpectrumInputWidget}
|
||||
/>
|
||||
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
|
||||
@@ -360,6 +473,8 @@
|
||||
<LayoutCol class="details">
|
||||
<LayoutRow
|
||||
class="choice-preview"
|
||||
classes={{ outlined, transparency }}
|
||||
styles={{ "--outline-amount": outlineFactor }}
|
||||
tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"}
|
||||
>
|
||||
{#if !newColor.equals(oldColor)}
|
||||
@@ -453,7 +568,7 @@
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
<LayoutRow>
|
||||
<TextLabel tooltip="Scale from transparent (0%) to opaque (100%) for the color's alpha channel">Alpha</TextLabel>
|
||||
<TextLabel tooltip="Scale of translucency, from transparent (0%) to opaque (100%), for the color's alpha channel">Alpha</TextLabel>
|
||||
<Separator type="Related" />
|
||||
<NumberInput
|
||||
value={!isNone ? alpha * 100 : undefined}
|
||||
@@ -471,18 +586,18 @@
|
||||
unit="%"
|
||||
mode="Range"
|
||||
displayDecimalPlaces={1}
|
||||
tooltip={`Scale from transparent (0%) to opaque (100%) for the color's alpha channel`}
|
||||
tooltip={`Scale of translucency, from transparent (0%) to opaque (100%), for the color's alpha channel`}
|
||||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="leftover-space" />
|
||||
<LayoutRow>
|
||||
{#if allowNone && !gradient}
|
||||
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0" />
|
||||
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
{/if}
|
||||
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0" />
|
||||
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0" />
|
||||
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1">
|
||||
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" />
|
||||
@@ -501,10 +616,13 @@
|
||||
|
||||
<style lang="scss" global>
|
||||
.color-picker {
|
||||
--picker-size: 256px;
|
||||
--picker-circle-radius: 6px;
|
||||
|
||||
.pickers-and-gradient {
|
||||
.pickers {
|
||||
.saturation-value-picker {
|
||||
width: 256px;
|
||||
width: var(--picker-size);
|
||||
background-blend-mode: multiply;
|
||||
background: linear-gradient(to bottom, #ffffff, #000000), linear-gradient(to right, #ffffff, var(--hue-color));
|
||||
position: relative;
|
||||
@@ -513,7 +631,7 @@
|
||||
.saturation-value-picker,
|
||||
.hue-picker,
|
||||
.alpha-picker {
|
||||
height: 256px;
|
||||
height: var(--picker-size);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -552,8 +670,8 @@
|
||||
|
||||
.selection-circle {
|
||||
position: absolute;
|
||||
left: 0%;
|
||||
top: 0%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
@@ -562,19 +680,59 @@
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
left: -6px;
|
||||
top: -6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
left: calc(-1 * var(--picker-circle-radius));
|
||||
top: calc(-1 * var(--picker-circle-radius));
|
||||
width: calc(var(--picker-circle-radius) * 2 + 1px);
|
||||
height: calc(var(--picker-circle-radius) * 2 + 1px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--opaque-color-contrasting);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-circle-alignment {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
|
||||
&.saturation::before,
|
||||
&.saturation::after,
|
||||
&.value::before,
|
||||
&.value::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--opaque-color-contrasting);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&.saturation {
|
||||
&::before {
|
||||
height: var(--picker-size);
|
||||
margin-top: calc(-1 * var(--picker-size) - var(--picker-circle-radius));
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: var(--picker-size);
|
||||
margin-top: var(--picker-circle-radius);
|
||||
}
|
||||
}
|
||||
|
||||
&.value {
|
||||
&::before {
|
||||
width: var(--picker-size);
|
||||
margin-left: var(--picker-circle-radius);
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: var(--picker-size);
|
||||
margin-left: calc(-1 * var(--picker-size) - var(--picker-circle-radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selection-needle {
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
@@ -642,14 +800,27 @@
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--color-1-nearblack);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size);
|
||||
background-position: var(--color-transparent-checkered-background-position);
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
|
||||
&.outlined::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--color-0-black-rgb), var(--outline-amount));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.transparency {
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size);
|
||||
background-position: var(--color-transparent-checkered-background-position);
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
}
|
||||
|
||||
.swap-button-background {
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
<script lang="ts" context="module">
|
||||
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
|
||||
export type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
|
||||
|
||||
/// Prevents the escape key from closing the parent floating menu of the given element.
|
||||
/// This works by momentarily setting the `data-escape-does-not-close` attribute on the parent floating menu element.
|
||||
/// After checking for the Escape key, it checks (in one `setTimeout`) for the attribute and ignores the key if it's present.
|
||||
/// Then after two calls of `setTimeout`, we can safely remove the attribute here.
|
||||
export function preventEscapeClosingParentFloatingMenu(element: HTMLElement) {
|
||||
const floatingMenuParent = element.closest("[data-floating-menu-content]") || undefined;
|
||||
if (floatingMenuParent instanceof HTMLElement) {
|
||||
floatingMenuParent.setAttribute("data-escape-does-not-close", "");
|
||||
setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
floatingMenuParent.removeAttribute("data-escape-does-not-close");
|
||||
}, 0);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -10,6 +26,7 @@
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
||||
const BUTTON_LEFT = 0;
|
||||
const POINTER_STRAY_DISTANCE = 100;
|
||||
|
||||
const dispatch = createEventDispatcher<{ open: boolean; naturalWidth: number }>();
|
||||
@@ -302,7 +319,8 @@
|
||||
}
|
||||
|
||||
// Clean up any messes from lost pointerup events
|
||||
const eventIncludesLmb = Boolean(e.buttons & 1);
|
||||
const BUTTONS_LEFT = 0b0000_0001;
|
||||
const eventIncludesLmb = Boolean(e.buttons & BUTTONS_LEFT);
|
||||
if (!open && !eventIncludesLmb) {
|
||||
pointerStillDown = false;
|
||||
window.removeEventListener("pointerup", pointerUpHandler);
|
||||
@@ -377,8 +395,15 @@
|
||||
}
|
||||
|
||||
function keyDownHandler(e: KeyboardEvent) {
|
||||
if (escapeCloses && e.key.toLowerCase() === "escape") {
|
||||
dispatch("open", false);
|
||||
if (escapeCloses && e.key === "Escape") {
|
||||
setTimeout(() => {
|
||||
if (!floatingMenuContainer?.querySelector("[data-floating-menu-content][data-escape-does-not-close]")) {
|
||||
dispatch("open", false);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Find the parent floating menu and prevent it from also closing with the escape key when this floating menu does
|
||||
if (self) preventEscapeClosingParentFloatingMenu(self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,13 +413,13 @@
|
||||
dispatch("open", false);
|
||||
|
||||
// Track if the left pointer button is now down so its later click event can be canceled
|
||||
const eventIsForLmb = e.button === 0;
|
||||
const eventIsForLmb = e.button === BUTTON_LEFT;
|
||||
if (eventIsForLmb) pointerStillDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
function pointerUpHandler(e: PointerEvent) {
|
||||
const eventIsForLmb = e.button === 0;
|
||||
const eventIsForLmb = e.button === BUTTON_LEFT;
|
||||
if (pointerStillDown && eventIsForLmb) {
|
||||
// Clean up self
|
||||
pointerStillDown = false;
|
||||
|
||||
@@ -169,16 +169,6 @@
|
||||
editor.handle.panCanvas(0, -delta * scrollbarMultiplier.y);
|
||||
}
|
||||
|
||||
function pageX(delta: number) {
|
||||
const move = delta < 0 ? 1 : -1;
|
||||
editor.handle.panCanvasByFraction(move, 0);
|
||||
}
|
||||
|
||||
function pageY(delta: number) {
|
||||
const move = delta < 0 ? 1 : -1;
|
||||
editor.handle.panCanvasByFraction(0, move);
|
||||
}
|
||||
|
||||
function canvasPointerDown(e: PointerEvent) {
|
||||
const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable;
|
||||
|
||||
@@ -549,21 +539,25 @@
|
||||
<LayoutCol class="ruler-or-scrollbar right-scrollbar">
|
||||
<ScrollbarInput
|
||||
direction="Vertical"
|
||||
handleLength={scrollbarSize.y}
|
||||
handlePosition={scrollbarPos.y}
|
||||
on:handlePosition={({ detail }) => panCanvasY(detail)}
|
||||
on:pressTrack={({ detail }) => pageY(detail)}
|
||||
thumbLength={scrollbarSize.y}
|
||||
thumbPosition={scrollbarPos.y}
|
||||
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(0, detail)}
|
||||
on:thumbPosition={({ detail }) => panCanvasY(detail)}
|
||||
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(false)}
|
||||
on:thumbDragAbort={() => editor.handle.panCanvasAbort(false)}
|
||||
/>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="ruler-or-scrollbar bottom-scrollbar">
|
||||
<ScrollbarInput
|
||||
direction="Horizontal"
|
||||
handleLength={scrollbarSize.x}
|
||||
handlePosition={scrollbarPos.x}
|
||||
on:handlePosition={({ detail }) => panCanvasX(detail)}
|
||||
on:pressTrack={({ detail }) => pageX(detail)}
|
||||
on:pointerup={() => editor.handle.setGridAlignedEdges()}
|
||||
thumbLength={scrollbarSize.x}
|
||||
thumbPosition={scrollbarPos.x}
|
||||
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(detail, 0)}
|
||||
on:thumbPosition={({ detail }) => panCanvasX(detail)}
|
||||
on:thumbDragEnd={() => editor.handle.setGridAlignedEdges()}
|
||||
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(true)}
|
||||
on:thumbDragAbort={() => editor.handle.panCanvasAbort(true)}
|
||||
/>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
||||
@@ -435,7 +435,7 @@
|
||||
title={listing.entry.expanded ? "Collapse" : `Expand${listing.entry.ancestorOfSelected ? "\n(A selected layer is contained within)" : ""}`}
|
||||
on:click|stopPropagation={() => handleExpandArrowClick(listing.entry.id)}
|
||||
tabindex="0"
|
||||
/>
|
||||
></button>
|
||||
{/if}
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
|
||||
import ColorButton from "@graphite/components/widgets/buttons/ColorButton.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import ImageButton from "@graphite/components/widgets/buttons/ImageButton.svelte";
|
||||
import ParameterExposeButton from "@graphite/components/widgets/buttons/ParameterExposeButton.svelte";
|
||||
import PopoverButton from "@graphite/components/widgets/buttons/PopoverButton.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import CheckboxInput from "@graphite/components/widgets/inputs/CheckboxInput.svelte";
|
||||
import ColorInput from "@graphite/components/widgets/inputs/ColorInput.svelte";
|
||||
import CurveInput from "@graphite/components/widgets/inputs/CurveInput.svelte";
|
||||
import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte";
|
||||
import FontInput from "@graphite/components/widgets/inputs/FontInput.svelte";
|
||||
@@ -87,9 +87,9 @@
|
||||
{#if checkboxInput}
|
||||
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const colorInput = narrowWidgetProps(component.props, "ColorButton")}
|
||||
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
|
||||
{#if colorInput}
|
||||
<ColorButton {...exclude(colorInput)} on:value={({ detail }) => widgetValueUpdate(index, detail)} on:startHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
|
||||
<ColorInput {...exclude(colorInput)} on:value={({ detail }) => widgetValueUpdate(index, detail)} on:startHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
|
||||
{/if}
|
||||
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
|
||||
{#if curvesInput}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { FillChoice } from "@graphite/messages";
|
||||
import { Color, Gradient } from "@graphite/messages";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
|
||||
|
||||
let open = false;
|
||||
|
||||
export let value: FillChoice;
|
||||
export let disabled = false;
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
$: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ disabled, none: value instanceof Color ? value.none : false, open }} {tooltip}>
|
||||
<button {disabled} style:--chosen-gradient={chosenGradient} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner></button>
|
||||
{#if disabled && value instanceof Color && !value.none}
|
||||
<TextLabel>sRGB</TextLabel>
|
||||
{/if}
|
||||
<ColorPicker
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
colorOrGradient={value}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
value = detail;
|
||||
dispatch("value", detail);
|
||||
}}
|
||||
on:startHistoryTransaction={() => {
|
||||
// This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/1584#discussion_r1477592483>
|
||||
dispatch("startHistoryTransaction");
|
||||
}}
|
||||
{allowNone}
|
||||
/>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
.color-button {
|
||||
position: relative;
|
||||
min-width: 80px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-5-dullgray);
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
&,
|
||||
> .text-label {
|
||||
background: rgba(var(--color-6-lowergray-rgb), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
&,
|
||||
> .text-label {
|
||||
background: var(--color-4-dimgray);
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
|
||||
> button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 2px;
|
||||
margin-top: 2px;
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
background-image: var(--chosen-gradient), var(--color-transparent-checkered-background);
|
||||
background-size:
|
||||
100% 100%,
|
||||
var(--color-transparent-checkered-background-size);
|
||||
background-position:
|
||||
0 0,
|
||||
var(--color-transparent-checkered-background-position);
|
||||
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||
}
|
||||
|
||||
&.none {
|
||||
> button {
|
||||
background: var(--color-none);
|
||||
background-repeat: var(--color-none-repeat);
|
||||
background-position: var(--color-none-position);
|
||||
background-size: var(--color-none-size-24px);
|
||||
background-image: var(--color-none-image-24px);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
> button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(var(--color-4-dimgray-rgb), 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .text-label {
|
||||
background: rgba(var(--color-5-dullgray-rgb), 0.5);
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px 0 0 6px;
|
||||
padding-right: 2px;
|
||||
padding-left: 4px;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
> .floating-menu {
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { FillChoice } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
|
||||
|
||||
let open = false;
|
||||
|
||||
export let value: FillChoice;
|
||||
export let disabled = false;
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`;
|
||||
$: none = value instanceof Color ? value.none : false;
|
||||
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined }} {tooltip}>
|
||||
<button {disabled} style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
|
||||
{#if disabled && value instanceof Color && !value.none}
|
||||
<TextLabel>sRGB</TextLabel>
|
||||
{/if}
|
||||
</button>
|
||||
<ColorPicker
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
colorOrGradient={value}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
value = detail;
|
||||
dispatch("value", detail);
|
||||
}}
|
||||
on:startHistoryTransaction={() => {
|
||||
// This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/1584#discussion_r1477592483>
|
||||
dispatch("startHistoryTransaction");
|
||||
}}
|
||||
{allowNone}
|
||||
/>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
.color-button {
|
||||
position: relative;
|
||||
min-width: 80px;
|
||||
|
||||
> button {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--chosen-gradient);
|
||||
}
|
||||
|
||||
.text-label {
|
||||
background: var(--color-5-dullgray);
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
height: 12px;
|
||||
border-radius: 0 0 0 2px;
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.outlined > button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--color-5-dullgray-rgb), var(--outline-amount));
|
||||
}
|
||||
|
||||
&.transparency > button {
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size);
|
||||
background-position: var(--color-transparent-checkered-background-position);
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
}
|
||||
|
||||
&:not(.disabled).none > button {
|
||||
background: var(--color-none);
|
||||
background-repeat: var(--color-none-repeat);
|
||||
background-position: var(--color-none-position);
|
||||
background-size: var(--color-none-size-24px);
|
||||
background-image: var(--color-none-image-24px);
|
||||
}
|
||||
|
||||
&.disabled.none > button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-4-dimgray);
|
||||
}
|
||||
|
||||
&:not(.disabled):hover > button .text-label,
|
||||
&:not(.disabled).open > button .text-label {
|
||||
background: var(--color-6-lowergray);
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
|
||||
&.disabled > button .text-label {
|
||||
background: var(--color-4-dimgray);
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
|
||||
> .floating-menu {
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -65,6 +66,12 @@
|
||||
export function element(): HTMLInputElement | HTMLTextAreaElement | undefined {
|
||||
return inputOrTextarea;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
dispatch("textChangeCanceled");
|
||||
|
||||
if (inputOrTextarea) preventEscapeClosingParentFloatingMenu(inputOrTextarea);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
|
||||
@@ -83,7 +90,7 @@
|
||||
on:blur={() => dispatch("textChanged")}
|
||||
on:change={() => dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Enter" && dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Escape" && dispatch("textChangeCanceled")}
|
||||
on:keydown={(e) => e.key === "Escape" && cancel()}
|
||||
on:pointerdown
|
||||
on:contextmenu={(e) => hideContextMenu && e.preventDefault()}
|
||||
data-input-element
|
||||
@@ -102,7 +109,7 @@
|
||||
on:blur={() => dispatch("textChanged")}
|
||||
on:change={() => dispatch("textChanged")}
|
||||
on:keydown={(e) => (macKeyboardLayout ? e.metaKey : e.ctrlKey) && e.key === "Enter" && dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Escape" && dispatch("textChangeCanceled")}
|
||||
on:keydown={(e) => e.key === "Escape" && cancel()}
|
||||
on:pointerdown
|
||||
on:contextmenu={(e) => hideContextMenu && e.preventDefault()}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input";
|
||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@graphite/messages";
|
||||
import { evaluateMathExpression } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
|
||||
const BUTTONS_LEFT = 0b0000_0001;
|
||||
@@ -59,8 +61,10 @@
|
||||
let self: FieldInput | undefined;
|
||||
let inputRangeElement: HTMLInputElement | undefined;
|
||||
let text = displayText(value, unit);
|
||||
let isDragging = false;
|
||||
let editing = false;
|
||||
let isDragging = false;
|
||||
let pressingArrow = false;
|
||||
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
// Stays in sync with a binding to the actual input range slider element.
|
||||
let rangeSliderValue = value !== undefined ? value : 0;
|
||||
// Value used to render the position of the fake slider when applicable, and length of the progress colored region to the slider's left.
|
||||
@@ -230,8 +234,8 @@
|
||||
// INCREMENT MODE: ARROW BUTTONS
|
||||
// =============================
|
||||
|
||||
function onIncrement(direction: "Decrease" | "Increase") {
|
||||
if (value === undefined) return;
|
||||
function onIncrementPointerDown(e: PointerEvent, direction: "Decrease" | "Increase") {
|
||||
if (value === undefined || e.button !== BUTTON_LEFT) return;
|
||||
|
||||
const actions: Record<NumberInputIncrementBehavior, () => void> = {
|
||||
Add: () => {
|
||||
@@ -250,7 +254,41 @@
|
||||
},
|
||||
None: () => {},
|
||||
};
|
||||
actions[incrementBehavior]();
|
||||
|
||||
const sendAction = () => {
|
||||
if (!pressingArrow) return;
|
||||
|
||||
actions[incrementBehavior]();
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendAction, PRESS_REPEAT_INTERVAL_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
pressingArrow = true;
|
||||
initialValueBeforeDragging = value;
|
||||
let afterInitialDelay = false;
|
||||
sendAction();
|
||||
repeatTimeout = setTimeout(sendAction, PRESS_REPEAT_DELAY_MS);
|
||||
addEventListener("keydown", incrementPressAbort);
|
||||
}
|
||||
|
||||
function onIncrementPointerUp() {
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
}
|
||||
|
||||
function onIncrementMouseDown(e: MouseEvent) {
|
||||
if (e.button === BUTTON_RIGHT) incrementPressAbort();
|
||||
}
|
||||
|
||||
function incrementPressAbort() {
|
||||
const element = self?.element() || undefined;
|
||||
if (element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
updateValue(initialValueBeforeDragging);
|
||||
removeEventListener("keydown", onIncrementPointerUp);
|
||||
}
|
||||
|
||||
// =======================================
|
||||
@@ -399,8 +437,8 @@
|
||||
if (rangeSliderClickDragState === "Aborted") {
|
||||
// If we've just aborted the drag by right clicking, but the user hasn't yet released the left mouse button, Firefox treats
|
||||
// some subsequent interactions with the slider (like that right mouse button release, or maybe mouse movement in some cases)
|
||||
// as input changes to the slider position. Thus, until we leave the "Aborted" state by releasing all mouse buttons,
|
||||
// we have to set the slider position back to currently intended value to fight against Firefox's attempts to let the user move it.
|
||||
// as input changes to the slider position. Thus, until we leave the "Aborted" state by releasing all mouse buttons, we have
|
||||
// to set the slider position back to the currently intended value to fight against Firefox's attempts to let the user move it.
|
||||
updateValue(rangeSliderValueAsRendered);
|
||||
|
||||
// Now we exit early because we're ignoring further user input until the user releases all mouse buttons, which gets us back to the "Ready" state.
|
||||
@@ -511,10 +549,7 @@
|
||||
// Logic for aborting from pressing Escape.
|
||||
if (e instanceof KeyboardEvent) {
|
||||
// Detect if the user pressed Escape and abort the slider drag.
|
||||
if (e.key === "Escape") {
|
||||
// Call the abort helper function.
|
||||
sliderAbort();
|
||||
}
|
||||
if (e.key === "Escape") sliderAbort(true);
|
||||
}
|
||||
|
||||
// Logic for aborting from a right click.
|
||||
@@ -522,7 +557,7 @@
|
||||
// This handler's "pointermove" event will be fired upon right click even if the cursor didn't move, which is why it's okay to check in this event handler.
|
||||
if (e instanceof PointerEvent && e.buttons & BUTTONS_RIGHT) {
|
||||
// Call the abort helper function
|
||||
sliderAbort();
|
||||
sliderAbort(false);
|
||||
}
|
||||
|
||||
// Recovery from the window losing focus while dragging the slider.
|
||||
@@ -543,9 +578,11 @@
|
||||
// During this momentary step, the slider hasn't moved yet but we want to allow aborting from this limbo state.
|
||||
function sliderAbortFromMousedown(e: MouseEvent | KeyboardEvent) {
|
||||
// Logic for aborting from a right click or pressing Escape.
|
||||
if ((e instanceof KeyboardEvent && e.key === "Escape") || (e instanceof MouseEvent && e.button === BUTTON_RIGHT)) {
|
||||
const abortWithEscape = e instanceof KeyboardEvent && e.key === "Escape";
|
||||
const abortWithRightClick = e instanceof MouseEvent && e.button === BUTTON_RIGHT;
|
||||
if (abortWithEscape || abortWithRightClick) {
|
||||
// Call the abort helper function
|
||||
sliderAbort();
|
||||
sliderAbort(abortWithEscape);
|
||||
|
||||
// Clean up these event listeners because they were for getting us into this function and now we're done with them.
|
||||
removeEventListener("mousedown", sliderAbortFromMousedown);
|
||||
@@ -554,7 +591,10 @@
|
||||
}
|
||||
|
||||
// Helper function that performs the state management and cleanup for aborting the slider drag.
|
||||
function sliderAbort() {
|
||||
function sliderAbort(abortWithEscape: boolean) {
|
||||
const element = self?.element() || undefined;
|
||||
if (abortWithEscape && element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
||||
// End the user's drag by instantaneously disabling and re-enabling the range input element
|
||||
if (inputRangeElement) inputRangeElement.disabled = true;
|
||||
setTimeout(() => {
|
||||
@@ -580,9 +620,7 @@
|
||||
// dragging the slider, hitting Escape, then releasing the mouse button. This results in being transferred by `onSliderInput()` to the
|
||||
// "Deciding" state when we should remain in the "Ready" state as set here. (For debugging, this can be visualized in CSS by
|
||||
// recoloring the fake slider handle, which is shown in the "Deciding" state.)
|
||||
setTimeout(() => {
|
||||
rangeSliderClickDragState = "Ready";
|
||||
}, 0);
|
||||
setTimeout(() => (rangeSliderClickDragState = "Ready"), 0);
|
||||
|
||||
// Clean up the event listener that was used to call this function.
|
||||
removeEventListener("pointerup", sliderResetAbort);
|
||||
@@ -617,8 +655,22 @@
|
||||
>
|
||||
{#if value !== undefined}
|
||||
{#if mode === "Increment" && incrementBehavior !== "None"}
|
||||
<button class="arrow left" on:click={() => onIncrement("Decrease")} tabindex="-1" />
|
||||
<button class="arrow right" on:click={() => onIncrement("Increase")} tabindex="-1" />
|
||||
<button
|
||||
class="arrow left"
|
||||
on:pointerdown={(e) => onIncrementPointerDown(e, "Decrease")}
|
||||
on:mousedown={onIncrementMouseDown}
|
||||
on:pointerup={onIncrementPointerUp}
|
||||
on:pointerleave={onIncrementPointerUp}
|
||||
tabindex="-1"
|
||||
></button>
|
||||
<button
|
||||
class="arrow right"
|
||||
on:pointerdown={(e) => onIncrementPointerDown(e, "Increase")}
|
||||
on:mousedown={onIncrementMouseDown}
|
||||
on:pointerup={onIncrementPointerUp}
|
||||
on:pointerleave={onIncrementPointerUp}
|
||||
tabindex="-1"
|
||||
></button>
|
||||
{/if}
|
||||
{#if mode === "Range"}
|
||||
<input
|
||||
@@ -684,6 +736,8 @@
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: rgba(var(--color-1-nearblack-rgb), 0.5);
|
||||
// An outline can appear when pressing the arrow button with left click then hitting Escape, so this stops that from showing
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-4-dimgray);
|
||||
|
||||
@@ -3,38 +3,46 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
// Linear Interpolation
|
||||
const lerp = (x: number, y: number, a: number): number => x * (1 - a) + y * a;
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS, PRESS_REPEAT_INTERVAL_RAPID_MS } from "@graphite/io-managers/input";
|
||||
|
||||
// Convert the position of the handle (0-1) to the position on the track (0-1).
|
||||
// This includes the 1/2 handle length gap of the possible handle positionson each side so the end of the handle doesn't go off the track.
|
||||
const handleToTrack = (handleLen: number, handlePos: number): number => lerp(handleLen / 2, 1 - handleLen / 2, handlePos);
|
||||
const ARROW_CLICK_DISTANCE = 0.05;
|
||||
const ARROW_REPEAT_DISTANCE = 0.01;
|
||||
|
||||
const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
|
||||
// Convert the position of the thumb (0-1) to the position on the track (0-1).
|
||||
// This includes the 1/2 thumb length gap of the possible thumb position each side so the end of the thumb doesn't go off the track.
|
||||
const lerp = (a: number, b: number, t: number): number => a * (1 - t) + b * t;
|
||||
const thumbToTrack = (thumbLength: number, thumbPosition: number): number => lerp(thumbLength / 2, 1 - thumbLength / 2, thumbPosition);
|
||||
|
||||
const dispatch = createEventDispatcher<{ handlePosition: number; pressTrack: number; pointerup: undefined }>();
|
||||
const pointerPosition = (e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
|
||||
|
||||
const clamp01 = (value: number): number => Math.min(Math.max(value, 0), 1);
|
||||
|
||||
const dispatch = createEventDispatcher<{ trackShift: number; thumbPosition: number; thumbDragStart: undefined; thumbDragEnd: undefined; thumbDragAbort: undefined }>();
|
||||
|
||||
export let direction: ScrollbarDirection = "Vertical";
|
||||
export let handlePosition = 0.5;
|
||||
export let handleLength = 0.5;
|
||||
export let thumbPosition = 0.5;
|
||||
export let thumbLength = 0.5;
|
||||
|
||||
let scrollTrack: HTMLDivElement | undefined;
|
||||
let dragging = false;
|
||||
let pointerPos = 0;
|
||||
let pressingTrack = false;
|
||||
let pressingArrow = false;
|
||||
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
let pointerPositionLastFrame = 0;
|
||||
let thumbTop: string | undefined = undefined;
|
||||
let thumbBottom: string | undefined = undefined;
|
||||
let thumbLeft: string | undefined = undefined;
|
||||
let thumbRight: string | undefined = undefined;
|
||||
|
||||
$: start = handleToTrack(handleLength, handlePosition) - handleLength / 2;
|
||||
$: end = 1 - handleToTrack(handleLength, handlePosition) - handleLength / 2;
|
||||
$: start = thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2;
|
||||
$: end = 1 - thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2;
|
||||
$: [thumbTop, thumbBottom, thumbLeft, thumbRight] = direction === "Vertical" ? [`${start * 100}%`, `${end * 100}%`, "0%", "0%"] : ["0%", "0%", `${start * 100}%`, `${end * 100}%`];
|
||||
|
||||
function trackLength(): number | undefined {
|
||||
if (scrollTrack === undefined) return undefined;
|
||||
return direction === "Vertical" ? scrollTrack.clientHeight - handleLength : scrollTrack.clientWidth;
|
||||
return direction === "Vertical" ? scrollTrack.clientHeight - thumbLength : scrollTrack.clientWidth;
|
||||
}
|
||||
|
||||
function trackOffset(): number | undefined {
|
||||
@@ -42,72 +50,168 @@
|
||||
return direction === "Vertical" ? scrollTrack.getBoundingClientRect().top : scrollTrack.getBoundingClientRect().left;
|
||||
}
|
||||
|
||||
function clampHandlePosition(newPos: number) {
|
||||
const clampedPosition = Math.min(Math.max(newPos, 0), 1);
|
||||
dispatch("handlePosition", clampedPosition);
|
||||
function dragThumb(e: PointerEvent) {
|
||||
if (dragging) return;
|
||||
|
||||
dragging = true;
|
||||
dispatch("thumbDragStart");
|
||||
pointerPositionLastFrame = pointerPosition(e);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function updateHandlePosition(e: PointerEvent) {
|
||||
function pressArrow(direction: number) {
|
||||
const sendMove = () => {
|
||||
if (!pressingArrow) return;
|
||||
|
||||
const distance = afterInitialDelay ? ARROW_REPEAT_DISTANCE : ARROW_CLICK_DISTANCE;
|
||||
dispatch("trackShift", -direction * distance);
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_RAPID_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
pressingArrow = true;
|
||||
dispatch("thumbDragStart");
|
||||
let afterInitialDelay = false;
|
||||
sendMove();
|
||||
repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function pressTrack(e: PointerEvent) {
|
||||
if (dragging) return;
|
||||
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
const offset = trackOffset();
|
||||
if (length === undefined || offset === undefined) return;
|
||||
|
||||
const position = pointerPosition(direction, e);
|
||||
const sendMove = () => {
|
||||
if (!pressingTrack) return;
|
||||
|
||||
clampHandlePosition(handlePosition + (position - pointerPos) / (length * (1 - handleLength)));
|
||||
pointerPos = position;
|
||||
const oldPointer = thumbToTrack(thumbLength, thumbPosition) * length + offset;
|
||||
const newPointer = pointerPosition(e);
|
||||
|
||||
// Check if the thumb has reached the cursor position
|
||||
const proposedThumbPosition = (newPointer - offset) / length;
|
||||
if (proposedThumbPosition >= start && proposedThumbPosition <= 1 - end) {
|
||||
// End pressing the track
|
||||
pressingTrack = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
|
||||
// Begin dragging the thumb
|
||||
dragging = true;
|
||||
pointerPositionLastFrame = newPointer;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const move = newPointer - oldPointer < 0 ? 1 : -1;
|
||||
dispatch("trackShift", move);
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
dispatch("thumbDragStart");
|
||||
pressingTrack = true;
|
||||
let afterInitialDelay = false;
|
||||
sendMove();
|
||||
repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function grabHandle(e: PointerEvent) {
|
||||
if (!dragging) {
|
||||
dragging = true;
|
||||
pointerPos = pointerPosition(direction, e);
|
||||
function abortInteraction() {
|
||||
if (pressingTrack || pressingArrow) {
|
||||
pressingTrack = false;
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
dispatch("thumbDragAbort");
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
dispatch("thumbDragAbort");
|
||||
}
|
||||
}
|
||||
|
||||
function grabArea(e: PointerEvent) {
|
||||
if (!dragging) {
|
||||
const length = trackLength();
|
||||
const offset = trackOffset();
|
||||
if (length === undefined || offset === undefined) return;
|
||||
function onPointerUp() {
|
||||
if (dragging) dispatch("thumbDragEnd");
|
||||
|
||||
const oldPointer = handleToTrack(handleLength, handlePosition) * length + offset;
|
||||
const pointerPos = pointerPosition(direction, e);
|
||||
dispatch("pressTrack", pointerPos - oldPointer);
|
||||
}
|
||||
}
|
||||
|
||||
function pointerUp() {
|
||||
dragging = false;
|
||||
pressingTrack = false;
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
function pointerMove(e: PointerEvent) {
|
||||
if (dragging) updateHandlePosition(e);
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (pressingTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pressingArrow) {
|
||||
const target = e.target || undefined;
|
||||
if (!target || !(target instanceof Element)) return;
|
||||
if (!target?.closest?.("[data-scrollbar-arrow]")) {
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
|
||||
const positionPositionThisFrame = pointerPosition(e);
|
||||
const dragDelta = positionPositionThisFrame - pointerPositionLastFrame;
|
||||
const movement = dragDelta / (length * (1 - thumbLength));
|
||||
const newThumbPosition = clamp01(thumbPosition + movement);
|
||||
dispatch("thumbPosition", newThumbPosition);
|
||||
|
||||
pointerPositionLastFrame = positionPositionThisFrame;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
function changePosition(difference: number) {
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
|
||||
clampHandlePosition(handlePosition + difference / length);
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortInteraction();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("pointerup", pointerUp);
|
||||
window.addEventListener("pointermove", pointerMove);
|
||||
});
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") abortInteraction();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("pointerup", pointerUp);
|
||||
window.removeEventListener("pointermove", pointerMove);
|
||||
});
|
||||
function addEvents() {
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onPointerMove);
|
||||
window.addEventListener("mousedown", onMouseDown);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onPointerMove);
|
||||
window.removeEventListener("mousedown", onMouseDown);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`} on:pointerup={() => dispatch("pointerup")}>
|
||||
<button class="arrow decrease" on:pointerdown={() => changePosition(-50)} tabindex="-1" />
|
||||
<div class="scroll-track" bind:this={scrollTrack} on:pointerdown={grabArea}>
|
||||
<div class="scroll-thumb" on:pointerdown={grabHandle} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`}>
|
||||
<button class="arrow decrease" on:pointerdown={() => pressArrow(-1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
<div class="scroll-track" on:pointerdown={pressTrack} bind:this={scrollTrack}>
|
||||
<div class="scroll-thumb" on:pointerdown={dragThumb} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
|
||||
</div>
|
||||
<button class="arrow increase" on:click={() => changePosition(50)} tabindex="-1" />
|
||||
<button class="arrow increase" on:pointerdown={() => pressArrow(1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
@@ -115,20 +219,66 @@
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
--arrow-color: var(--color-5-dullgray);
|
||||
flex: 0 0 auto;
|
||||
background: none;
|
||||
border: none;
|
||||
border-style: solid;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:hover {
|
||||
--arrow-color: var(--color-6-lowergray);
|
||||
}
|
||||
|
||||
&:hover:active {
|
||||
--arrow-color: var(--color-c-brightgray);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical .arrow.decrease::after {
|
||||
margin: 4px 3px;
|
||||
border-width: 0 5px 8px 5px;
|
||||
border-color: transparent transparent var(--arrow-color) transparent;
|
||||
}
|
||||
|
||||
&.vertical .arrow.increase::after {
|
||||
margin: 4px 3px;
|
||||
border-width: 8px 5px 0 5px;
|
||||
border-color: var(--arrow-color) transparent transparent transparent;
|
||||
}
|
||||
|
||||
&.horizontal .arrow.decrease::after {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 8px 5px 0;
|
||||
border-color: transparent var(--arrow-color) transparent transparent;
|
||||
}
|
||||
|
||||
&.horizontal .arrow.increase::after {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 0 5px 8px;
|
||||
border-color: transparent transparent transparent var(--arrow-color);
|
||||
}
|
||||
|
||||
.scroll-track {
|
||||
flex: 1 1 100%;
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
|
||||
.scroll-thumb {
|
||||
position: absolute;
|
||||
@@ -140,70 +290,6 @@
|
||||
background: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-click-area {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
|
||||
.arrow.decrease {
|
||||
margin: 4px 3px;
|
||||
border-width: 0 5px 8px 5px;
|
||||
border-color: transparent transparent var(--color-5-dullgray) transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent transparent var(--color-6-lowergray) transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent transparent var(--color-c-brightgray) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow.increase {
|
||||
margin: 4px 3px;
|
||||
border-width: 8px 5px 0 5px;
|
||||
border-color: var(--color-5-dullgray) transparent transparent transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-6-lowergray) transparent transparent transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: var(--color-c-brightgray) transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
|
||||
.arrow.decrease {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 8px 5px 0;
|
||||
border-color: transparent var(--color-5-dullgray) transparent transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent var(--color-6-lowergray) transparent transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent var(--color-c-brightgray) transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow.increase {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 0 5px 8px;
|
||||
border-color: transparent transparent transparent var(--color-5-dullgray);
|
||||
|
||||
&:hover {
|
||||
border-color: transparent transparent transparent var(--color-6-lowergray);
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent transparent transparent var(--color-c-brightgray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,21 +3,27 @@
|
||||
|
||||
import { Color, type Gradient } from "@graphite/messages";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient }>();
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
|
||||
|
||||
export let gradient: Gradient;
|
||||
export let activeMarkerIndex = 0 as number | undefined;
|
||||
// export let disabled = false;
|
||||
// export let tooltip: string | undefined = undefined;
|
||||
|
||||
let markerTrack: LayoutRow | undefined;
|
||||
let markerTrack: LayoutRow | undefined = undefined;
|
||||
let positionRestore: number | undefined = undefined;
|
||||
let deletionRestore: boolean | undefined = undefined;
|
||||
|
||||
function markerPointerDown(e: PointerEvent, index: number) {
|
||||
// Left-click to select and begin potentially dragging
|
||||
if (e.button === 0) {
|
||||
if (e.button === BUTTON_LEFT) {
|
||||
activeMarkerIndex = index;
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
addEvents();
|
||||
@@ -25,7 +31,7 @@
|
||||
}
|
||||
|
||||
// Right-click to delete
|
||||
if (e.button === 2) {
|
||||
if (e.button === BUTTON_RIGHT && deletionRestore === undefined) {
|
||||
deleteStopByIndex(index);
|
||||
return;
|
||||
}
|
||||
@@ -41,6 +47,8 @@
|
||||
}
|
||||
|
||||
function insertStop(e: MouseEvent) {
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
@@ -62,14 +70,20 @@
|
||||
|
||||
gradient.stops.splice(index, 0, { position, color });
|
||||
activeMarkerIndex = index;
|
||||
deletionRestore = true;
|
||||
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
dispatch("gradient", gradient);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function deleteStop(e: KeyboardEvent) {
|
||||
if (e.key.toLowerCase() !== "delete" && e.key.toLowerCase() !== "backspace") return;
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
if (positionRestore !== undefined) stopDrag();
|
||||
|
||||
deleteStopByIndex(activeMarkerIndex);
|
||||
}
|
||||
|
||||
@@ -82,6 +96,7 @@
|
||||
} else {
|
||||
activeMarkerIndex = Math.max(0, Math.min(gradient.stops.length - 1, index));
|
||||
}
|
||||
deletionRestore = undefined;
|
||||
|
||||
dispatch("activeMarkerIndexChange", activeMarkerIndex);
|
||||
dispatch("gradient", gradient);
|
||||
@@ -89,10 +104,18 @@
|
||||
|
||||
function moveMarker(e: PointerEvent, index: number) {
|
||||
// Just in case the mouseup event is lost
|
||||
if (e.buttons === 0) removeEvents();
|
||||
if (e.buttons === 0) stopDrag();
|
||||
|
||||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
if (positionRestore === undefined) positionRestore = position;
|
||||
if (deletionRestore === undefined) {
|
||||
deletionRestore = false;
|
||||
|
||||
dispatch("dragging", true);
|
||||
}
|
||||
|
||||
setPosition(index, position);
|
||||
}
|
||||
|
||||
@@ -107,24 +130,61 @@
|
||||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (activeMarkerIndex !== undefined) {
|
||||
moveMarker(e, activeMarkerIndex);
|
||||
function abortDrag() {
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
if (deletionRestore) {
|
||||
deleteStopByIndex(activeMarkerIndex);
|
||||
} else if (positionRestore !== undefined) {
|
||||
setPosition(activeMarkerIndex, positionRestore);
|
||||
}
|
||||
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
removeEvents();
|
||||
|
||||
positionRestore = undefined;
|
||||
deletionRestore = undefined;
|
||||
|
||||
dispatch("dragging", false);
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
removeEvents();
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortDrag();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
const element = markerTrack?.div();
|
||||
if (element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
||||
abortDrag();
|
||||
}
|
||||
}
|
||||
|
||||
function addEvents() {
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", deleteStop);
|
||||
@@ -158,24 +218,22 @@
|
||||
"--gradient-stops": gradient.toLinearGradientCSS(),
|
||||
}}
|
||||
>
|
||||
<LayoutRow class="gradient-strip" on:click={insertStop}></LayoutRow>
|
||||
<LayoutRow class="gradient-strip" on:pointerdown={insertStop}></LayoutRow>
|
||||
<LayoutRow class="marker-track" bind:this={markerTrack}>
|
||||
{#each gradient.stops as marker, index}
|
||||
<svg
|
||||
style:--marker-position={marker.position}
|
||||
style:--marker-color={marker.color.toRgbCSS()}
|
||||
class="marker"
|
||||
class:active={index === activeMarkerIndex}
|
||||
style:--marker-position={marker.position}
|
||||
style:--marker-color={marker.color.toRgbCSS()}
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
data-gradient-marker
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path class="inner-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" />
|
||||
<path
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z"
|
||||
/>
|
||||
<path
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
class="outer-border"
|
||||
d="M6,1.4L1.3,6.1C1.1,6.3,1,6.6,1,6.8V10c0,0.6,0.4,1,1,1h8c0.6,0,1-0.4,1-1V6.8c0-0.3-0.1-0.5-0.3-0.7L6,1.4M6,0l5.4,5.4C11.8,5.8,12,6.3,12,6.8V10c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V6.8c0-0.5,0.2-1,0.6-1.4L6,0z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -216,6 +274,7 @@
|
||||
margin-left: var(--marker-half-width);
|
||||
width: calc(100% - 2 * var(--marker-half-width));
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
|
||||
.marker {
|
||||
position: absolute;
|
||||
@@ -223,30 +282,40 @@
|
||||
left: calc(var(--marker-position) * 100%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
pointer-events: auto;
|
||||
overflow: visible;
|
||||
padding-top: 12px;
|
||||
margin-top: -12px;
|
||||
|
||||
// Inner fill
|
||||
path:first-child {
|
||||
.inner-fill {
|
||||
fill: var(--marker-color);
|
||||
}
|
||||
|
||||
// Outer border
|
||||
path:last-child {
|
||||
.outer-border {
|
||||
fill: var(--color-5-dullgray);
|
||||
}
|
||||
|
||||
&:not(.active) path:first-child:hover + path:last-child,
|
||||
&:not(.active) path:last-child:hover {
|
||||
fill: var(--color-6-lowergray);
|
||||
&:not(.active) {
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
// Outer border when active
|
||||
&.active path:last-child {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
&.active {
|
||||
.inner-fill {
|
||||
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
|
||||
}
|
||||
|
||||
&.active path:first-child:hover + path:last-child,
|
||||
&.active path:last-child:hover {
|
||||
fill: var(--color-f-white);
|
||||
// Outer border when active
|
||||
.outer-border {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<LayoutCol class="working-colors-button">
|
||||
<LayoutRow class="primary swatch">
|
||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
|
||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<ColorPicker
|
||||
open={primaryOpen}
|
||||
on:open={({ detail }) => (primaryOpen = detail)}
|
||||
@@ -47,7 +47,7 @@
|
||||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="secondary swatch">
|
||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
|
||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<ColorPicker
|
||||
open={secondaryOpen}
|
||||
on:open={({ detail }) => (secondaryOpen = detail)}
|
||||
@@ -70,27 +70,55 @@
|
||||
|
||||
> button {
|
||||
--swatch-color: #ffffff;
|
||||
--ring-color: var(--color-5-dullgray);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px var(--color-5-dullgray) solid;
|
||||
box-shadow: 0 0 0 2px var(--color-3-darkgray);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(var(--swatch-color), var(--swatch-color)), var(--color-transparent-checkered-background);
|
||||
background-size:
|
||||
100% 100%,
|
||||
var(--color-transparent-checkered-background-size);
|
||||
background-position:
|
||||
0 0,
|
||||
var(--color-transparent-checkered-background-position);
|
||||
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
// Color of the panel background, used to extend outside the ring and appear to cut out a crescent from the lower circle (by covering it up with the panel background color)
|
||||
box-shadow: 0 0 0 2px var(--color-3-darkgray);
|
||||
background: var(--color-3-darkgray);
|
||||
|
||||
// Main color and checked transparency pattern (inset by 1px to begin inside/below the ring to avoid antialiasing artifacts)
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(var(--swatch-color), var(--swatch-color)), var(--color-transparent-checkered-background);
|
||||
background-size:
|
||||
100% 100%,
|
||||
var(--color-transparent-checkered-background-size);
|
||||
background-position:
|
||||
0 0,
|
||||
var(--color-transparent-checkered-background-position-plus-one);
|
||||
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||
}
|
||||
|
||||
// Gray ring outline
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 2px var(--ring-color);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
border-color: var(--color-6-lowergray);
|
||||
--ring-color: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,14 @@
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
$: iconSizeClass = ((icon: IconName) => {
|
||||
return `size-${iconSizeOverride || ICONS[icon].size}`;
|
||||
const iconData = ICONS[icon];
|
||||
// eslint-disable-next-line no-console
|
||||
if (!iconData) {
|
||||
console.warn(`Icon "${icon}" does not exist.`);
|
||||
return "size-24";
|
||||
}
|
||||
if (iconData.size === undefined) return "";
|
||||
return `size-${iconSizeOverride || iconData.size}`;
|
||||
})(icon);
|
||||
$: extraClasses = Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
@@ -20,7 +27,7 @@
|
||||
</script>
|
||||
|
||||
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltip}>
|
||||
{@html ICON_SVG_STRINGS[icon]}
|
||||
{@html ICON_SVG_STRINGS[icon] || "�"}
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
|
||||
|
||||
const BUTTON_MIDDLE = 1;
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
export let tabMinWidths = false;
|
||||
@@ -100,7 +102,7 @@
|
||||
}}
|
||||
on:auxclick={(e) => {
|
||||
// Middle mouse button click
|
||||
if (e.button === 1) {
|
||||
if (e.button === BUTTON_MIDDLE) {
|
||||
e.stopPropagation();
|
||||
closeAction?.(tabIndex);
|
||||
}
|
||||
@@ -110,7 +112,7 @@
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility
|
||||
// The downside of using mouseup is that the mousedown didn't have to originate in the same element.
|
||||
// A possible future improvement could save the target element during mousedown and check if it's the same here.
|
||||
if (!isEventSupported("auxclick") && e.button === 1) {
|
||||
if (!isEventSupported("auxclick") && e.button === BUTTON_MIDDLE) {
|
||||
e.stopPropagation();
|
||||
closeAction?.(tabIndex);
|
||||
}
|
||||
|
||||
@@ -14,15 +14,17 @@
|
||||
const MIN_PANEL_SIZE = 100;
|
||||
const PANEL_SIZES = {
|
||||
/**/ root: 100,
|
||||
/* ├── */ content: 80,
|
||||
/* │ ├── */ document: 100,
|
||||
/* └── */ details: 20,
|
||||
/* ├── */ properties: 45,
|
||||
/* └── */ layers: 55,
|
||||
/* ├─ */ content: 80,
|
||||
/* │ └─ */ document: 100,
|
||||
/* └─ */ details: 20,
|
||||
/* ├─ */ properties: 45,
|
||||
/* └─ */ layers: 55,
|
||||
};
|
||||
|
||||
let panelSizes = PANEL_SIZES;
|
||||
let documentPanel: Panel | undefined;
|
||||
let gutterResizeRestore: [number, number] | undefined = undefined;
|
||||
let pointerCaptureId: number | undefined = undefined;
|
||||
|
||||
$: documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex);
|
||||
|
||||
@@ -64,32 +66,68 @@
|
||||
const proportionBeingResized = totalResizingSpaceOccupied / parentElementSize;
|
||||
|
||||
// Prevent cursor flicker as mouse temporarily leaves the gutter
|
||||
gutter.setPointerCapture(e.pointerId);
|
||||
pointerCaptureId = e.pointerId;
|
||||
gutter.setPointerCapture(pointerCaptureId);
|
||||
|
||||
const mouseStart = isHorizontal ? e.clientX : e.clientY;
|
||||
|
||||
const updatePosition = (e: PointerEvent) => {
|
||||
const abortResize = () => {
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
|
||||
pointerCaptureId = e.pointerId;
|
||||
gutter.setPointerCapture(pointerCaptureId);
|
||||
|
||||
if (gutterResizeRestore !== undefined) {
|
||||
panelSizes[nextSiblingName] = gutterResizeRestore[0];
|
||||
panelSizes[prevSiblingName] = gutterResizeRestore[1];
|
||||
gutterResizeRestore = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const mouseCurrent = isHorizontal ? e.clientX : e.clientY;
|
||||
let mouseDelta = mouseStart - mouseCurrent;
|
||||
|
||||
mouseDelta = Math.max(nextSiblingSize + mouseDelta, MIN_PANEL_SIZE) - nextSiblingSize;
|
||||
mouseDelta = prevSiblingSize - Math.max(prevSiblingSize - mouseDelta, MIN_PANEL_SIZE);
|
||||
|
||||
if (gutterResizeRestore === undefined) gutterResizeRestore = [panelSizes[nextSiblingName], panelSizes[prevSiblingName]];
|
||||
|
||||
panelSizes[nextSiblingName] = ((nextSiblingSize + mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
|
||||
panelSizes[prevSiblingName] = ((prevSiblingSize - mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
|
||||
};
|
||||
|
||||
const cleanup = (e: PointerEvent) => {
|
||||
gutter.releasePointerCapture(e.pointerId);
|
||||
|
||||
document.removeEventListener("pointermove", updatePosition);
|
||||
document.removeEventListener("pointerleave", cleanup);
|
||||
document.removeEventListener("pointerup", cleanup);
|
||||
const onPointerUp = () => {
|
||||
gutterResizeRestore = undefined;
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", updatePosition);
|
||||
document.addEventListener("pointerleave", cleanup);
|
||||
document.addEventListener("pointerup", cleanup);
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortResize();
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") abortResize();
|
||||
};
|
||||
|
||||
const addListeners = () => {
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
const removeListeners = () => {
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
addListeners();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,6 +12,16 @@ import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||
import { updateBoundsOfViewports } from "@graphite/utility-functions/viewports";
|
||||
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_MIDDLE = 1;
|
||||
const BUTTON_RIGHT = 2;
|
||||
const BUTTON_BACK = 3;
|
||||
const BUTTON_FORWARD = 4;
|
||||
|
||||
export const PRESS_REPEAT_DELAY_MS = 400;
|
||||
export const PRESS_REPEAT_INTERVAL_MS = 72;
|
||||
export const PRESS_REPEAT_INTERVAL_RAPID_MS = 10;
|
||||
|
||||
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap | "modifyinputfield";
|
||||
type EventListenerTarget = {
|
||||
addEventListener: typeof window.addEventListener;
|
||||
@@ -155,7 +165,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
// Block middle mouse button auto-scroll mode (the circlar gizmo that appears and allows quick scrolling by moving the cursor above or below it)
|
||||
if (e.button === 1) e.preventDefault();
|
||||
if (e.button === BUTTON_MIDDLE) e.preventDefault();
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
@@ -172,7 +182,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
}
|
||||
|
||||
if (!inTextInput && !inContextMenu) {
|
||||
const isLeftOrRightClick = e.button === 2 || e.button === 0;
|
||||
const isLeftOrRightClick = e.button === BUTTON_RIGHT || e.button === BUTTON_LEFT;
|
||||
if (textToolInteractiveInputElement) editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText), isLeftOrRightClick);
|
||||
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
|
||||
}
|
||||
@@ -188,7 +198,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
// TODO: This works in Chrome but not in Firefox
|
||||
// TODO: Possible workaround: use the browser's history API to block navigation:
|
||||
// TODO: <https://stackoverflow.com/questions/57102502/preventing-mouse-fourth-and-fifth-buttons-from-navigating-back-forward-in-browse>
|
||||
if (e.button === 3 || e.button === 4) e.preventDefault();
|
||||
if (e.button === BUTTON_BACK || e.button === BUTTON_FORWARD) e.preventDefault();
|
||||
|
||||
if (!e.buttons) viewportPointerInteractionOngoing = false;
|
||||
|
||||
@@ -206,11 +216,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
|
||||
// `e.buttons` is always 0 in the `mouseup` event, so we have to convert from `e.button` instead
|
||||
let buttons = 1;
|
||||
if (e.button === 0) buttons = 1; // Left
|
||||
if (e.button === 2) buttons = 2; // Right
|
||||
if (e.button === 1) buttons = 4; // Middle
|
||||
if (e.button === 3) buttons = 8; // Back
|
||||
if (e.button === 4) buttons = 16; // Forward
|
||||
if (e.button === BUTTON_LEFT) buttons = 1; // Left
|
||||
if (e.button === BUTTON_RIGHT) buttons = 2; // Right
|
||||
if (e.button === BUTTON_MIDDLE) buttons = 4; // Middle
|
||||
if (e.button === BUTTON_BACK) buttons = 8; // Back
|
||||
if (e.button === BUTTON_FORWARD) buttons = 16; // Forward
|
||||
|
||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
||||
editor.handle.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
|
||||
|
||||
@@ -667,8 +667,8 @@ export class Color {
|
||||
return new Color(this.red, this.green, this.blue, 1);
|
||||
}
|
||||
|
||||
contrastingColor(): "black" | "white" {
|
||||
if (this.none) return "black";
|
||||
luminance(): number | undefined {
|
||||
if (this.none) return undefined;
|
||||
|
||||
// Convert alpha into white
|
||||
const r = this.red * this.alpha + (1 - this.alpha);
|
||||
@@ -681,9 +681,15 @@ export class Color {
|
||||
const linearG = g <= 0.04045 ? g / 12.92 : ((g + 0.055) / 1.055) ** 2.4;
|
||||
const linearB = b <= 0.04045 ? b / 12.92 : ((b + 0.055) / 1.055) ** 2.4;
|
||||
|
||||
const linear = linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
||||
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
||||
}
|
||||
|
||||
return linear > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||
contrastingColor(): "black" | "white" {
|
||||
if (this.none) return "black";
|
||||
|
||||
const luminance = this.luminance();
|
||||
|
||||
return luminance && luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,7 +950,7 @@ export class CheckboxInput extends WidgetProps {
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class ColorButton extends WidgetProps {
|
||||
export class ColorInput extends WidgetProps {
|
||||
@Transform(({ value }) => {
|
||||
if (value instanceof Gradient) return value;
|
||||
const gradient = value["Gradient"];
|
||||
@@ -978,6 +984,37 @@ export class ColorButton extends WidgetProps {
|
||||
|
||||
export type FillChoice = Color | Gradient;
|
||||
|
||||
export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number {
|
||||
const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor];
|
||||
const [range1, range2] = pair.map((color) => Color.fromCSS(window.getComputedStyle(document.body).getPropertyValue(color)) || new Color("none"));
|
||||
|
||||
const contrast = (color: Color): number => {
|
||||
const colorLuminance = color.luminance() || 0;
|
||||
let rangeLuminance1 = range1.luminance() || 0;
|
||||
let rangeLuminance2 = range2.luminance() || 0;
|
||||
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)];
|
||||
|
||||
const distance = (() => {
|
||||
if (colorLuminance < rangeLuminance1) return rangeLuminance1 - colorLuminance;
|
||||
if (colorLuminance > rangeLuminance2) return colorLuminance - rangeLuminance2;
|
||||
return 0;
|
||||
})();
|
||||
|
||||
return (1 - Math.min(distance / proximityRange, 1)) * (1 - (color.toHSV()?.s || 0));
|
||||
};
|
||||
|
||||
if (value instanceof Gradient) {
|
||||
if (value.stops.length === 0) return 0;
|
||||
|
||||
const first = contrast(value.stops[0].color);
|
||||
const last = contrast(value.stops[value.stops.length - 1].color);
|
||||
|
||||
return Math.min(first, last);
|
||||
}
|
||||
|
||||
return contrast(value);
|
||||
}
|
||||
|
||||
type MenuEntryCommon = {
|
||||
label: string;
|
||||
icon?: IconName;
|
||||
@@ -1314,7 +1351,7 @@ export class PivotInput extends WidgetProps {
|
||||
const widgetSubTypes = [
|
||||
{ value: BreadcrumbTrailButtons, name: "BreadcrumbTrailButtons" },
|
||||
{ value: CheckboxInput, name: "CheckboxInput" },
|
||||
{ value: ColorButton, name: "ColorButton" },
|
||||
{ value: ColorInput, name: "ColorInput" },
|
||||
{ value: CurveInput, name: "CurveInput" },
|
||||
{ value: DropdownInput, name: "DropdownInput" },
|
||||
{ value: FontInput, name: "FontInput" },
|
||||
|
||||
Reference in New Issue
Block a user