mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 04:58:12 +08:00
Add support for gradients with midpoints and add draggable diamonds to the color picker dialog (#3813)
* Refactor GradientStops to use struct-of-arrays and include midpoint * Implement interaction and rendering * Make color picker saturation-value color picking snap to original position and show both axis lines Make color picker saturation-value color picking snap to original position and show both axis lines * Add graphite:midpoint attribute to SVG exports * Add graphite:midpoint parsing to SVG importer
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte";
|
||||
import SpectrumInput from "@graphite/components/widgets/inputs/SpectrumInput.svelte";
|
||||
import SpectrumInput, { MAX_MIDPOINT, MIN_MIDPOINT } from "@graphite/components/widgets/inputs/SpectrumInput.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
@@ -57,7 +57,8 @@
|
||||
// Gradient color stops
|
||||
$: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined;
|
||||
let activeIndex = 0 as number | undefined;
|
||||
$: selectedGradientColor = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color);
|
||||
let activeIndexIsMidpoint = false;
|
||||
$: selectedGradientColor = (activeIndex !== undefined && gradient?.color[activeIndex]) || (Color.fromCSS("black") as Color);
|
||||
// Currently viewed color
|
||||
$: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor;
|
||||
// New color components
|
||||
@@ -191,8 +192,8 @@
|
||||
alignedAxis = undefined;
|
||||
} else if (!shiftPressed && draggingPickerTrack) {
|
||||
shiftPressed = true;
|
||||
saturationStartOfAxisAlign = saturation;
|
||||
valueStartOfAxisAlign = value;
|
||||
saturationStartOfAxisAlign = saturationBeforeDrag;
|
||||
valueStartOfAxisAlign = valueBeforeDrag;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,8 +287,9 @@
|
||||
function setColor(color?: Color) {
|
||||
const colorToEmit = color || new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
|
||||
const stop = gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.atIndex(activeIndex);
|
||||
if (stop) stop.color = colorToEmit;
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.position[activeIndex] !== undefined && colorOrGradient instanceof Gradient) {
|
||||
colorOrGradient.color[activeIndex] = colorToEmit;
|
||||
}
|
||||
|
||||
dispatch("colorOrGradient", gradient || colorToEmit);
|
||||
}
|
||||
@@ -397,9 +399,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function gradientActiveMarkerIndexChange({ detail: index }: CustomEvent<number | undefined>) {
|
||||
activeIndex = index;
|
||||
const color = index === undefined ? undefined : gradient?.colorAtIndex(index);
|
||||
function gradientActiveMarkerIndexChange({ detail: { activeMarkerIndex, activeMarkerIsMidpoint } }: CustomEvent<{ activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }>) {
|
||||
activeIndex = activeMarkerIndex;
|
||||
activeIndexIsMidpoint = activeMarkerIsMidpoint;
|
||||
|
||||
const color = activeMarkerIndex === undefined ? undefined : gradient?.color[activeMarkerIndex];
|
||||
const hsva = color?.toHSVA();
|
||||
if (!color || !hsva) return;
|
||||
|
||||
@@ -440,17 +444,24 @@
|
||||
on:pointerdown={onPointerDown}
|
||||
data-saturation-value-picker
|
||||
>
|
||||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`}></div>
|
||||
{/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}%`}
|
||||
class="selection-circle-axis-snap-line"
|
||||
style:width={alignedAxis === "value" ? "100%" : undefined}
|
||||
style:height={alignedAxis === "saturation" ? "100%" : undefined}
|
||||
style:top={alignedAxis === "value" ? `${(1 - value) * 100}%` : undefined}
|
||||
style:left={alignedAxis === "saturation" ? `${saturation * 100}%` : undefined}
|
||||
></div>
|
||||
<div
|
||||
class="selection-circle-axis-snap-line"
|
||||
style:width={alignedAxis === "saturation" ? "100%" : undefined}
|
||||
style:height={alignedAxis === "value" ? "100%" : undefined}
|
||||
style:top={alignedAxis === "saturation" ? `${(1 - valueBeforeDrag) * 100}%` : undefined}
|
||||
style:left={alignedAxis === "value" ? `${saturationBeforeDrag * 100}%` : undefined}
|
||||
></div>
|
||||
{/if}
|
||||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`}></div>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol
|
||||
@@ -484,19 +495,22 @@
|
||||
on:gradient={() => dispatch("colorOrGradient", gradient)}
|
||||
on:activeMarkerIndexChange={gradientActiveMarkerIndexChange}
|
||||
activeMarkerIndex={activeIndex}
|
||||
activeMarkerIsMidpoint={activeIndexIsMidpoint}
|
||||
on:dragging={({ detail }) => (gradientSpectrumDragging = detail)}
|
||||
bind:this={gradientSpectrumInputWidget}
|
||||
/>
|
||||
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
|
||||
<NumberInput
|
||||
value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
|
||||
value={(activeIndexIsMidpoint ? gradient.midpoint[activeIndex] : gradient.position[activeIndex] || 0) * 100}
|
||||
{disabled}
|
||||
on:value={({ detail }) => {
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
|
||||
on:value={({ detail: position }) => {
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && position !== undefined) {
|
||||
gradientSpectrumInputWidget.setPosition(activeIndex, position / 100, activeIndexIsMidpoint);
|
||||
}
|
||||
}}
|
||||
displayDecimalPlaces={0}
|
||||
min={0}
|
||||
max={100}
|
||||
min={activeIndexIsMidpoint ? MIN_MIDPOINT * 100 : 0}
|
||||
max={activeIndexIsMidpoint ? MAX_MIDPOINT * 100 : 100}
|
||||
unit="%"
|
||||
/>
|
||||
{/if}
|
||||
@@ -744,12 +758,12 @@
|
||||
}
|
||||
|
||||
.selection-circle {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
@@ -761,56 +775,31 @@
|
||||
height: calc(var(--picker-circle-radius) * 2 + 1px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--opaque-color-contrasting);
|
||||
background: var(--opaque-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-circle-alignment {
|
||||
position: absolute;
|
||||
.selection-circle-axis-snap-line {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: var(--opaque-color-contrasting);
|
||||
|
||||
&.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-circle-axis-snap-line {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-needle {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
@@ -881,13 +870,13 @@
|
||||
|
||||
&.outlined::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
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 {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
$: 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;
|
||||
$: transparency = value instanceof Gradient ? value.color.some((color) => color.alpha < 1) : value.alpha < 1;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts" context="module">
|
||||
export const MIN_MIDPOINT = 0.01;
|
||||
export const MAX_MIDPOINT = 0.99;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
|
||||
import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { Color, type Gradient } from "@graphite/messages";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
@@ -10,27 +16,44 @@
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: { activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }; gradient: Gradient; dragging: boolean }>();
|
||||
|
||||
export let gradient: Gradient;
|
||||
export let disabled = false;
|
||||
export let activeMarkerIndex = 0 as number | undefined;
|
||||
export let activeMarkerIsMidpoint = false;
|
||||
// export let disabled = false;
|
||||
// export let tooltipLabel: string | undefined = undefined;
|
||||
// export let tooltipDescription: string | undefined = undefined;
|
||||
// export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
|
||||
/// Reference to the marker track element so we can access its div.
|
||||
let markerTrack: LayoutRow | undefined = undefined;
|
||||
let positionRestore: number | undefined = undefined;
|
||||
/// When dragging, stores the original value of the marker or midpoint being dragged, so we can restore it if the drag is cancelled.
|
||||
let dragRestore: number | undefined = undefined;
|
||||
/// When dragging, indicates whether this maker was inserted during the drag, so we know whether to remove it again if the drag is cancelled.
|
||||
let deletionRestore: boolean | undefined = undefined;
|
||||
/// When dragging, stores the previous active marker (or its midpoint) index, so we can restore active selection to that one if the drag is cancelled on a different marker.
|
||||
let activeMarkerIndexRestore: number | undefined = undefined;
|
||||
/// When dragging, stores whether the previously active drag item was a midpoint (matching the index kept in `activeMarkerIndexRestore`), so we can restore its active selection if cancelled.
|
||||
let activeMarkerIsMidpointRestore = false;
|
||||
/// When dragging a midpoint, tracks whether the midpoint has actually moved by at least a pixel, to tell between a click-then-click-and-drag (just a drag) or a double-click (reset the midpoint).
|
||||
let midpointDragged = false;
|
||||
|
||||
function markerPointerDown(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
// Left-click to select and begin potentially dragging
|
||||
if (e.button === BUTTON_LEFT) {
|
||||
// Set restore values at this time, so later the user can cancel the drag and restore to these values
|
||||
activeMarkerIndexRestore = activeMarkerIndex;
|
||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||
|
||||
// Update the parent component with the newly activated marker or midpoint drag item
|
||||
activeMarkerIndex = index;
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
activeMarkerIsMidpoint = false;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
|
||||
addEvents();
|
||||
return;
|
||||
}
|
||||
@@ -51,37 +74,68 @@
|
||||
return Math.max(0, Math.min(1, ratio));
|
||||
}
|
||||
|
||||
function insertStop(e: MouseEvent) {
|
||||
function midpointPointerDown(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
// Since we just pressed the mouse button down, the midpoint has not been dragged by any distance
|
||||
midpointDragged = false;
|
||||
|
||||
// Set restore values at this time, so later the user can cancel the drag and restore to these values
|
||||
activeMarkerIndexRestore = activeMarkerIndex;
|
||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||
|
||||
// Update the parent component with the newly activated marker or midpoint drag item
|
||||
activeMarkerIndex = index;
|
||||
activeMarkerIsMidpoint = true;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function resetMidpoint(index: number) {
|
||||
if (disabled || midpointDragged) return;
|
||||
|
||||
gradient.midpoint[index] = 0.5;
|
||||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
function insertStop(e: MouseEvent) {
|
||||
if (disabled) return;
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
// Determine the position along the gradient (0-1) based on the click position in the marker track
|
||||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
let before = gradient.stops.findLast((value) => value.position < position);
|
||||
let after = gradient.stops.find((value) => value.position > position);
|
||||
// Determine which index the new stop should be inserted at based on its position
|
||||
let index = gradient.position.findIndex((item) => item > position);
|
||||
if (index === -1) index = gradient.position.length;
|
||||
|
||||
let color = Color.fromCSS("black") as Color;
|
||||
if (before && after) {
|
||||
let t = (position - before.position) / (after.position - before.position);
|
||||
color = before.color.lerp(after.color, t);
|
||||
} else if (before) {
|
||||
color = before.color;
|
||||
} else if (after) {
|
||||
color = after.color;
|
||||
}
|
||||
// Determine the color of the new stop by evaluating the gradient at the position of the new stop
|
||||
type ReturnedColor = { red: number; green: number; blue: number; alpha: number };
|
||||
const evaluated = evaluateGradientAtPosition(position, new Float64Array(gradient.position), new Float64Array(gradient.midpoint), gradient.color) as ReturnedColor;
|
||||
const color = new Color(evaluated.red, evaluated.green, evaluated.blue, evaluated.alpha);
|
||||
|
||||
let index = gradient.stops.findIndex((value) => value.position > position);
|
||||
if (index === -1) index = gradient.stops.length;
|
||||
|
||||
gradient.stops.splice(index, 0, { position, color });
|
||||
activeMarkerIndex = index;
|
||||
deletionRestore = true;
|
||||
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
// Insert the new stop into the gradient
|
||||
gradient.position.splice(index, 0, position);
|
||||
// Duplicate the midpoint ratio position of the interval we're inserting into, so both new intervals have the same midpoint position ratio
|
||||
gradient.midpoint.splice(index, 0, gradient.midpoint[index - 1] || 0.5);
|
||||
gradient.color.splice(index, 0, color);
|
||||
dispatch("gradient", gradient);
|
||||
|
||||
// Set restore values at this time, so later the user can cancel the drag and restore to these values
|
||||
activeMarkerIndexRestore = activeMarkerIndex;
|
||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||
|
||||
// Update the parent component with the newly activated marker or midpoint drag item
|
||||
activeMarkerIndex = index;
|
||||
activeMarkerIsMidpoint = false;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
|
||||
// Since this stop insertion can happen as part of the beginning of a drag, we set this to indicate that it should be removed again if the drag is cancelled
|
||||
deletionRestore = true;
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
@@ -90,27 +144,35 @@
|
||||
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
if (gradient.position.length <= 2 && !activeMarkerIsMidpoint) return;
|
||||
|
||||
if (positionRestore !== undefined) stopDrag();
|
||||
// Stop dragging the marker or midpoint
|
||||
stopDrag();
|
||||
|
||||
deleteStopByIndex(activeMarkerIndex);
|
||||
// Either reset the midpoint to 50% or delete the marker, based on which type is currently active
|
||||
if (activeMarkerIsMidpoint) resetMidpoint(activeMarkerIndex);
|
||||
else deleteStopByIndex(activeMarkerIndex);
|
||||
}
|
||||
|
||||
function deleteStopByIndex(index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
if (gradient.stops.length <= 2) return;
|
||||
if (gradient.position.length <= 2) return;
|
||||
|
||||
gradient.position.splice(index, 1);
|
||||
gradient.midpoint.splice(index, 1);
|
||||
gradient.color.splice(index, 1);
|
||||
dispatch("gradient", gradient);
|
||||
|
||||
gradient.stops.splice(index, 1);
|
||||
if (gradient.stops.length === 0) {
|
||||
activeMarkerIndex = undefined;
|
||||
} else {
|
||||
activeMarkerIndex = Math.max(0, Math.min(gradient.stops.length - 1, index));
|
||||
}
|
||||
deletionRestore = undefined;
|
||||
|
||||
dispatch("activeMarkerIndexChange", activeMarkerIndex);
|
||||
dispatch("gradient", gradient);
|
||||
if (gradient.position.length === 0) {
|
||||
activeMarkerIndex = undefined;
|
||||
} else {
|
||||
activeMarkerIndex = Math.max(0, Math.min(gradient.position.length - 1, index));
|
||||
}
|
||||
activeMarkerIsMidpoint = false;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
}
|
||||
|
||||
function moveMarker(e: PointerEvent, index: number) {
|
||||
@@ -122,40 +184,99 @@
|
||||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
if (positionRestore === undefined) positionRestore = position;
|
||||
if (dragRestore === undefined) dragRestore = position;
|
||||
if (deletionRestore === undefined) {
|
||||
deletionRestore = false;
|
||||
|
||||
dispatch("dragging", true);
|
||||
}
|
||||
|
||||
setPosition(index, position);
|
||||
setPosition(index, position, false);
|
||||
}
|
||||
|
||||
export function setPosition(index: number, position: number) {
|
||||
function moveMidpoint(e: PointerEvent, index: number) {
|
||||
if (disabled) return;
|
||||
|
||||
const active = gradient.stops[index];
|
||||
active.position = position;
|
||||
gradient.stops.sort((a, b) => a.position - b.position);
|
||||
if (gradient.stops.indexOf(active) !== activeMarkerIndex) {
|
||||
activeMarkerIndex = gradient.stops.indexOf(active);
|
||||
dispatch("activeMarkerIndexChange", gradient.stops.indexOf(active));
|
||||
// Guard in case the mouseup event is lost
|
||||
if (e.buttons === 0) {
|
||||
stopDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
if (dragRestore === undefined) {
|
||||
dragRestore = gradient.midpoint[index];
|
||||
midpointDragged = true;
|
||||
dispatch("dragging", true);
|
||||
}
|
||||
|
||||
const leftStop = gradient.position[index];
|
||||
const rightStop = gradient.position[index + 1];
|
||||
const range = rightStop - leftStop;
|
||||
if (range <= 0) return;
|
||||
|
||||
gradient.midpoint[index] = Math.max(MIN_MIDPOINT, Math.min(MAX_MIDPOINT, (position - leftStop) / range));
|
||||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
export function setPosition(index: number, position: number, isMidpoint: boolean) {
|
||||
if (disabled) return;
|
||||
|
||||
const markers = toMarkers(gradient);
|
||||
const active = markers[index];
|
||||
|
||||
if (isMidpoint) active.midpoint = position;
|
||||
else active.position = position;
|
||||
|
||||
markers.sort((a, b) => a.position - b.position);
|
||||
if (markers.indexOf(active) !== activeMarkerIndex) {
|
||||
activeMarkerIndex = markers.indexOf(active);
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
}
|
||||
|
||||
gradient.position = markers.map((stop) => stop.position);
|
||||
gradient.midpoint = markers.map((stop) => stop.midpoint);
|
||||
gradient.color = markers.map((stop) => stop.color);
|
||||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
function toMarkers(gradient: Gradient): { position: number; midpoint: number; color: Color }[] {
|
||||
return gradient.position.map((position, i) => ({
|
||||
position,
|
||||
midpoint: gradient.midpoint[i],
|
||||
color: gradient.color[i],
|
||||
}));
|
||||
}
|
||||
|
||||
function toMidpoints(gradient: Gradient): number[] {
|
||||
if (gradient.position.length < 2) return [];
|
||||
|
||||
return gradient.midpoint.slice(0, -1).map((midpoint, i) => {
|
||||
const leftMarker = gradient.position[i];
|
||||
const rightMarker = gradient.position[i + 1];
|
||||
return leftMarker + midpoint * (rightMarker - leftMarker);
|
||||
});
|
||||
}
|
||||
|
||||
function abortDrag() {
|
||||
if (disabled) return;
|
||||
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
if (deletionRestore) {
|
||||
deleteStopByIndex(activeMarkerIndex);
|
||||
} else if (positionRestore !== undefined) {
|
||||
setPosition(activeMarkerIndex, positionRestore);
|
||||
if (activeMarkerIndex !== undefined) {
|
||||
if (activeMarkerIsMidpoint && dragRestore !== undefined) {
|
||||
gradient.midpoint[activeMarkerIndex] = dragRestore;
|
||||
dispatch("gradient", gradient);
|
||||
} else {
|
||||
if (deletionRestore) deleteStopByIndex(activeMarkerIndex);
|
||||
else if (dragRestore !== undefined) setPosition(activeMarkerIndex, dragRestore, false);
|
||||
}
|
||||
}
|
||||
|
||||
activeMarkerIndex = activeMarkerIndexRestore;
|
||||
activeMarkerIsMidpoint = activeMarkerIsMidpointRestore;
|
||||
dispatch("activeMarkerIndexChange", { activeMarkerIndex, activeMarkerIsMidpoint });
|
||||
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
@@ -164,8 +285,11 @@
|
||||
|
||||
removeEvents();
|
||||
|
||||
positionRestore = undefined;
|
||||
dragRestore = undefined;
|
||||
deletionRestore = undefined;
|
||||
activeMarkerIndexRestore = undefined;
|
||||
activeMarkerIsMidpointRestore = false;
|
||||
midpointDragged = false;
|
||||
|
||||
dispatch("dragging", false);
|
||||
}
|
||||
@@ -173,7 +297,8 @@
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
|
||||
if (activeMarkerIsMidpoint && activeMarkerIndex !== undefined) moveMidpoint(e, activeMarkerIndex);
|
||||
else if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
@@ -249,11 +374,27 @@
|
||||
}}
|
||||
>
|
||||
<LayoutRow class="gradient-strip" on:pointerdown={insertStop}></LayoutRow>
|
||||
<LayoutRow class="midpoint-track">
|
||||
{#each toMidpoints(gradient) as midpoint, index}
|
||||
<svg
|
||||
class="midpoint"
|
||||
class:active={index === activeMarkerIndex && activeMarkerIsMidpoint}
|
||||
style:--midpoint-position={midpoint}
|
||||
on:pointerdown={(e) => midpointPointerDown(e, index)}
|
||||
on:dblclick={() => resetMidpoint(index)}
|
||||
data-gradient-midpoint
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
>
|
||||
<polygon points="0,4 4,0 8,4 4,8" />
|
||||
</svg>
|
||||
{/each}
|
||||
</LayoutRow>
|
||||
<LayoutRow class="marker-track" bind:this={markerTrack}>
|
||||
{#each gradient.stops as marker, index}
|
||||
{#each toMarkers(gradient) as marker, index}
|
||||
<svg
|
||||
class="marker"
|
||||
class:active={index === activeMarkerIndex}
|
||||
class:active={index === activeMarkerIndex && !activeMarkerIsMidpoint}
|
||||
style:--marker-position={marker.position}
|
||||
style:--marker-color={marker.color.toRgbCSS()}
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
@@ -276,6 +417,7 @@
|
||||
|
||||
<style lang="scss" global>
|
||||
.spectrum-input {
|
||||
position: relative;
|
||||
--marker-half-width: 6px;
|
||||
|
||||
.gradient-strip {
|
||||
@@ -310,6 +452,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
.midpoint-track {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: var(--marker-half-width);
|
||||
right: var(--marker-half-width);
|
||||
|
||||
.midpoint {
|
||||
position: absolute;
|
||||
margin-left: -4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
bottom: 0;
|
||||
left: calc(var(--midpoint-position) * 100%);
|
||||
|
||||
polygon {
|
||||
stroke: var(--color-e-nearwhite);
|
||||
fill: var(--color-2-mildblack);
|
||||
}
|
||||
|
||||
&.active {
|
||||
z-index: 1;
|
||||
|
||||
polygon {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled .midpoint-track .midpoint polygon {
|
||||
stroke: var(--color-4-dimgray);
|
||||
}
|
||||
|
||||
.marker-track {
|
||||
margin-top: calc(24px - 16px - 12px);
|
||||
margin-left: var(--marker-half-width);
|
||||
@@ -357,6 +532,8 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
z-index: 1;
|
||||
|
||||
.inner-fill {
|
||||
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user