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:
Keavon Chambers
2026-02-23 19:21:51 -08:00
committed by GitHub
parent a1c1039ea1
commit 691d965bcf
21 changed files with 842 additions and 322 deletions
+31 -42
View File
@@ -2,7 +2,7 @@
import { Transform, Type, plainToClass } from "class-transformer";
import { type EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import { sampleInterpolatedGradient, type EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import { type PopoverButtonStyle, type IconName, type IconSize } from "@graphite/icons";
export class JsMessage {
@@ -355,46 +355,40 @@ export type RGBA = { r: number; g: number; b: number; a: number };
export type RGB = { r: number; g: number; b: number };
export class Gradient {
readonly stops!: { position: number; color: Color }[];
position!: number[];
midpoint!: number[];
color!: Color[];
constructor(stops: { position: number; color: Color }[]) {
this.stops = stops;
constructor(position: number[], midpoint: number[], color: Color[]) {
this.position = position;
this.midpoint = midpoint;
this.color = color;
}
toLinearGradientCSS(): string {
if (this.stops.length === 1) {
return `linear-gradient(to right, ${this.stops[0].color.toHexOptionalAlpha()} 0%, ${this.stops[0].color.toHexOptionalAlpha()} 100%)`;
if (this.position.length === 1) {
return `linear-gradient(to right, ${this.color[0].toHexOptionalAlpha()} 0%, ${this.color[0].toHexOptionalAlpha()} 100%)`;
}
const pieces = this.stops.map((stop) => `${stop.color.toHexOptionalAlpha()} ${stop.position * 100}%`);
return `linear-gradient(to right, ${pieces.join(", ")})`;
const pieces = sampleInterpolatedGradient(new Float64Array(this.position), new Float64Array(this.midpoint), this.color, false);
return `linear-gradient(to right, ${pieces})`;
}
toLinearGradientCSSNoAlpha(): string {
if (this.stops.length === 1) {
return `linear-gradient(to right, ${this.stops[0].color.toHexNoAlpha()} 0%, ${this.stops[0].color.toHexNoAlpha()} 100%)`;
if (this.position.length === 1) {
return `linear-gradient(to right, ${this.color[0].toHexNoAlpha()} 0%, ${this.color[0].toHexNoAlpha()} 100%)`;
}
const pieces = this.stops.map((stop) => `${stop.color.toHexNoAlpha()} ${stop.position * 100}%`);
return `linear-gradient(to right, ${pieces.join(", ")})`;
const pieces = sampleInterpolatedGradient(new Float64Array(this.position), new Float64Array(this.midpoint), this.color, true);
return `linear-gradient(to right, ${pieces})`;
}
firstColor(): Color | undefined {
return this.stops[0]?.color;
return this.color[0];
}
lastColor(): Color | undefined {
return this.stops[this.stops.length - 1]?.color;
}
atIndex(index: number): { position: number; color: Color } | undefined {
return this.stops[index];
}
colorAtIndex(index: number): Color | undefined {
return this.stops[index]?.color;
}
positionAtIndex(index: number): number | undefined {
return this.stops[index]?.position;
return this.color[this.color.length - 1];
}
}
@@ -499,10 +493,6 @@ export class Color {
return Math.abs(this.red - other.red) < 1e-6 && Math.abs(this.green - other.green) < 1e-6 && Math.abs(this.blue - other.blue) < 1e-6 && Math.abs(this.alpha - other.alpha) < 1e-6;
}
lerp(other: Color, t: number): Color {
return new Color(this.red * (1 - t) + other.red * t, this.green * (1 - t) + other.green * t, this.blue * (1 - t) + other.blue * t, this.alpha * (1 - t) + other.alpha * t);
}
toHexNoAlpha(): string | undefined {
if (this.none) return undefined;
@@ -951,20 +941,19 @@ export class ColorInput extends WidgetProps {
// Content
@Transform(({ value }) => {
if (value instanceof Gradient) return value;
const gradient = value["Gradient"];
const gradient: Gradient | undefined = value["Gradient"];
if (gradient) {
const stops = gradient.map(([position, color]: [number, color: { red: number; green: number; blue: number; alpha: number }]) => ({
position,
color: new Color(color.red, color.green, color.blue, color.alpha),
}));
return new Gradient(stops);
return new Gradient(
gradient.position,
gradient.midpoint,
gradient.color.map((color: any) => new Color(color.red, color.green, color.blue, color.alpha)),
);
}
if (value instanceof Color) return value;
const solid = value["Solid"];
if (solid) {
return new Color(solid.red, solid.green, solid.blue, solid.alpha);
}
if (solid) return new Color(solid.red, solid.green, solid.blue, solid.alpha);
return new Color("none");
})
@@ -1008,10 +997,10 @@ export function contrastingOutlineFactor(value: FillChoice, proximityColor: stri
};
if (value instanceof Gradient) {
if (value.stops.length === 0) return 0;
if (value.color.length === 0) return 0;
const first = contrast(value.stops[0].color);
const last = contrast(value.stops[value.stops.length - 1].color);
const first = contrast(value.color[0]);
const last = contrast(value.color[value.color.length - 1]);
return Math.min(first, last);
}