mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +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:
@@ -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