mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 06:28:12 +08:00
Clean up Gradient and Color classes in TS by removing their methods (#3857)
This commit is contained in:
@@ -2,7 +2,25 @@
|
|||||||
import { getContext, onDestroy, createEventDispatcher, tick } from "svelte";
|
import { getContext, onDestroy, createEventDispatcher, tick } from "svelte";
|
||||||
|
|
||||||
import type { HSV, RGB, FillChoice, MenuDirection } from "@graphite/messages";
|
import type { HSV, RGB, FillChoice, MenuDirection } from "@graphite/messages";
|
||||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
import {
|
||||||
|
type Color,
|
||||||
|
contrastingOutlineFactor,
|
||||||
|
isColor,
|
||||||
|
isGradient,
|
||||||
|
createColor,
|
||||||
|
createNoneColor,
|
||||||
|
createColorFromHSVA,
|
||||||
|
colorFromCSS,
|
||||||
|
colorToRgb255,
|
||||||
|
colorToHSVA,
|
||||||
|
colorToHexOptionalAlpha,
|
||||||
|
colorToHexNoAlpha,
|
||||||
|
colorToRgbCSS,
|
||||||
|
colorContrastingColor,
|
||||||
|
colorOpaque,
|
||||||
|
colorEquals,
|
||||||
|
gradientFirstColor,
|
||||||
|
} from "@graphite/messages";
|
||||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||||
import { clamp } from "@graphite/utility-functions/math";
|
import { clamp } from "@graphite/utility-functions/math";
|
||||||
import { isDesktop } from "@graphite/utility-functions/platform";
|
import { isDesktop } from "@graphite/utility-functions/platform";
|
||||||
@@ -51,16 +69,17 @@
|
|||||||
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
|
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
|
||||||
export let open: boolean;
|
export let open: boolean;
|
||||||
|
|
||||||
const hsvaOrNone = colorOrGradient instanceof Color ? colorOrGradient.toHSVA() : colorOrGradient.firstColor()?.toHSVA();
|
const colorForHSVA = isColor(colorOrGradient) ? colorOrGradient : gradientFirstColor(colorOrGradient);
|
||||||
|
const hsvaOrNone = colorForHSVA ? colorToHSVA(colorForHSVA) : undefined;
|
||||||
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
|
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
|
||||||
|
|
||||||
// Gradient color stops
|
// Gradient color stops
|
||||||
$: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined;
|
$: gradient = isGradient(colorOrGradient) ? colorOrGradient : undefined;
|
||||||
let activeIndex = 0 as number | undefined;
|
let activeIndex = 0 as number | undefined;
|
||||||
let activeIndexIsMidpoint = false;
|
let activeIndexIsMidpoint = false;
|
||||||
$: selectedGradientColor = (activeIndex !== undefined && gradient?.color[activeIndex]) || (Color.fromCSS("black") as Color);
|
$: selectedGradientColor = (activeIndex !== undefined && gradient?.color[activeIndex]) || (colorFromCSS("black") as Color);
|
||||||
// Currently viewed color
|
// Currently viewed color
|
||||||
$: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor;
|
$: color = isColor(colorOrGradient) ? colorOrGradient : selectedGradientColor;
|
||||||
// New color components
|
// New color components
|
||||||
let hue = hsva.h;
|
let hue = hsva.h;
|
||||||
let saturation = hsva.s;
|
let saturation = hsva.s;
|
||||||
@@ -97,16 +116,16 @@
|
|||||||
|
|
||||||
$: oldColor = generateColor(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone);
|
$: oldColor = generateColor(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone);
|
||||||
$: newColor = generateColor(hue, saturation, value, alpha, isNone);
|
$: newColor = generateColor(hue, saturation, value, alpha, isNone);
|
||||||
$: rgbChannels = Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][];
|
$: rgbChannels = Object.entries(colorToRgb255(newColor) || { 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][];
|
$: 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 });
|
$: opaqueHueColor = createColorFromHSVA(hue, 1, 1, 1);
|
||||||
$: outlineFactor = Math.max(contrastingOutlineFactor(newColor, "--color-2-mildblack", 0.01), contrastingOutlineFactor(oldColor, "--color-2-mildblack", 0.01));
|
$: outlineFactor = Math.max(contrastingOutlineFactor(newColor, "--color-2-mildblack", 0.01), contrastingOutlineFactor(oldColor, "--color-2-mildblack", 0.01));
|
||||||
$: outlined = outlineFactor > 0.0001;
|
$: outlined = outlineFactor > 0.0001;
|
||||||
$: transparency = newColor.alpha < 1 || oldColor.alpha < 1;
|
$: transparency = newColor.alpha < 1 || oldColor.alpha < 1;
|
||||||
|
|
||||||
function generateColor(h: number, s: number, v: number, a: number, none: boolean) {
|
function generateColor(h: number, s: number, v: number, a: number, none: boolean) {
|
||||||
if (none) return new Color("none");
|
if (none) return createNoneColor();
|
||||||
return new Color({ h, s, v, a });
|
return createColorFromHSVA(h, s, v, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function watchOpen(open: boolean) {
|
async function watchOpen(open: boolean) {
|
||||||
@@ -119,7 +138,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function watchColor(color: Color) {
|
function watchColor(color: Color) {
|
||||||
const hsva = color.toHSVA();
|
const hsva = colorToHSVA(color);
|
||||||
|
|
||||||
if (hsva === undefined) {
|
if (hsva === undefined) {
|
||||||
setNewHSVA(0, 0, 0, 1, true);
|
setNewHSVA(0, 0, 0, 1, true);
|
||||||
@@ -185,7 +204,7 @@
|
|||||||
strayCloses = false;
|
strayCloses = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
const color = createColorFromHSVA(hue, saturation, value, alpha);
|
||||||
setColor(color);
|
setColor(color);
|
||||||
|
|
||||||
if (!e.shiftKey) {
|
if (!e.shiftKey) {
|
||||||
@@ -226,7 +245,7 @@
|
|||||||
saturation = saturationRestoreWhenShiftReleased;
|
saturation = saturationRestoreWhenShiftReleased;
|
||||||
value = valueRestoreWhenShiftReleased;
|
value = valueRestoreWhenShiftReleased;
|
||||||
|
|
||||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
const color = createColorFromHSVA(hue, saturation, value, alpha);
|
||||||
setColor(color);
|
setColor(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,14 +301,14 @@
|
|||||||
value = valueBeforeDrag;
|
value = valueBeforeDrag;
|
||||||
alpha = alphaBeforeDrag;
|
alpha = alphaBeforeDrag;
|
||||||
|
|
||||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
const color = createColorFromHSVA(hue, saturation, value, alpha);
|
||||||
setColor(color);
|
setColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setColor(color?: Color) {
|
function setColor(color?: Color) {
|
||||||
const colorToEmit = color || new Color({ h: hue, s: saturation, v: value, a: alpha });
|
const colorToEmit = color || createColorFromHSVA(hue, saturation, value, alpha);
|
||||||
|
|
||||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.position[activeIndex] !== undefined && colorOrGradient instanceof Gradient) {
|
if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.position[activeIndex] !== undefined && isGradient(colorOrGradient)) {
|
||||||
colorOrGradient.color[activeIndex] = colorToEmit;
|
colorOrGradient.color[activeIndex] = colorToEmit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +331,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setColorCode(colorCode: string) {
|
function setColorCode(colorCode: string) {
|
||||||
const color = Color.fromCSS(colorCode);
|
const color = colorFromCSS(colorCode);
|
||||||
if (color) setColor(color);
|
if (color) setColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,9 +339,9 @@
|
|||||||
// Do nothing if the given value is undefined
|
// Do nothing if the given value is undefined
|
||||||
if (strength === undefined) return undefined;
|
if (strength === undefined) return undefined;
|
||||||
// Set the specified channel to the given value
|
// Set the specified channel to the given value
|
||||||
else if (channel === "r") setColor(new Color(strength / 255, newColor.green, newColor.blue, newColor.alpha));
|
else if (channel === "r") setColor(createColor(strength / 255, newColor.green, newColor.blue, newColor.alpha));
|
||||||
else if (channel === "g") setColor(new Color(newColor.red, strength / 255, newColor.blue, newColor.alpha));
|
else if (channel === "g") setColor(createColor(newColor.red, strength / 255, newColor.blue, newColor.alpha));
|
||||||
else if (channel === "b") setColor(new Color(newColor.red, newColor.green, strength / 255, newColor.alpha));
|
else if (channel === "b") setColor(createColor(newColor.red, newColor.green, strength / 255, newColor.alpha));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setColorHSV(channel: keyof HSV, strength: number | undefined) {
|
function setColorHSV(channel: keyof HSV, strength: number | undefined) {
|
||||||
@@ -353,10 +372,10 @@
|
|||||||
|
|
||||||
if (preset === "none") {
|
if (preset === "none") {
|
||||||
setNewHSVA(0, 0, 0, 1, true);
|
setNewHSVA(0, 0, 0, 1, true);
|
||||||
setColor(new Color("none"));
|
setColor(createNoneColor());
|
||||||
} else {
|
} else {
|
||||||
const presetColor = new Color(...PURE_COLORS[preset], 1);
|
const presetColor = createColor(...PURE_COLORS[preset], 1);
|
||||||
const hsva = presetColor.toHSVA() || { h: 0, s: 0, v: 0, a: 0 };
|
const hsva = colorToHSVA(presetColor) || { h: 0, s: 0, v: 0, a: 0 };
|
||||||
|
|
||||||
setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, false);
|
setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, false);
|
||||||
setColor(presetColor);
|
setColor(presetColor);
|
||||||
@@ -406,7 +425,7 @@
|
|||||||
activeIndexIsMidpoint = activeMarkerIsMidpoint;
|
activeIndexIsMidpoint = activeMarkerIsMidpoint;
|
||||||
|
|
||||||
const color = activeMarkerIndex === undefined ? undefined : gradient?.color[activeMarkerIndex];
|
const color = activeMarkerIndex === undefined ? undefined : gradient?.color[activeMarkerIndex];
|
||||||
const hsva = color?.toHSVA();
|
const hsva = color ? colorToHSVA(color) : undefined;
|
||||||
if (!color || !hsva) return;
|
if (!color || !hsva) return;
|
||||||
|
|
||||||
setColor(color);
|
setColor(color);
|
||||||
@@ -427,14 +446,14 @@
|
|||||||
<FloatingMenu class="color-picker" classes={{ disabled }} {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
|
<FloatingMenu class="color-picker" classes={{ disabled }} {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
styles={{
|
styles={{
|
||||||
"--new-color": newColor.toHexOptionalAlpha(),
|
"--new-color": colorToHexOptionalAlpha(newColor),
|
||||||
"--new-color-contrasting": newColor.contrastingColor(),
|
"--new-color-contrasting": colorContrastingColor(newColor),
|
||||||
"--old-color": oldColor.toHexOptionalAlpha(),
|
"--old-color": colorToHexOptionalAlpha(oldColor),
|
||||||
"--old-color-contrasting": oldColor.contrastingColor(),
|
"--old-color-contrasting": colorContrastingColor(oldColor),
|
||||||
"--hue-color": opaqueHueColor.toRgbCSS(),
|
"--hue-color": colorToRgbCSS(opaqueHueColor),
|
||||||
"--hue-color-contrasting": opaqueHueColor.contrastingColor(),
|
"--hue-color-contrasting": colorContrastingColor(opaqueHueColor),
|
||||||
"--opaque-color": (newColor.opaque() || new Color(0, 0, 0, 1)).toHexNoAlpha(),
|
"--opaque-color": colorToHexNoAlpha(colorOpaque(newColor) || createColor(0, 0, 0, 1)),
|
||||||
"--opaque-color-contrasting": (newColor.opaque() || new Color(0, 0, 0, 1)).contrastingColor(),
|
"--opaque-color-contrasting": colorContrastingColor(colorOpaque(newColor) || createColor(0, 0, 0, 1)),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{@const hueDescription = "The shade along the spectrum of the rainbow."}
|
{@const hueDescription = "The shade along the spectrum of the rainbow."}
|
||||||
@@ -528,18 +547,18 @@
|
|||||||
class="choice-preview"
|
class="choice-preview"
|
||||||
classes={{ outlined, transparency }}
|
classes={{ outlined, transparency }}
|
||||||
styles={{ "--outline-amount": outlineFactor }}
|
styles={{ "--outline-amount": outlineFactor }}
|
||||||
tooltipDescription={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before it was changed (right)." : "The present color choice."}
|
tooltipDescription={!colorEquals(newColor, oldColor) ? "Comparison between the present color choice (left) and the color before it was changed (right)." : "The present color choice."}
|
||||||
>
|
>
|
||||||
{#if !newColor.equals(oldColor) && !disabled}
|
{#if !colorEquals(newColor, oldColor) && !disabled}
|
||||||
<div class="swap-button-background"></div>
|
<div class="swap-button-background"></div>
|
||||||
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltipLabel="Swap" />
|
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltipLabel="Swap" />
|
||||||
{/if}
|
{/if}
|
||||||
<LayoutCol class="new-color" classes={{ none: isNone }}>
|
<LayoutCol class="new-color" classes={{ none: isNone }}>
|
||||||
{#if !newColor.equals(oldColor)}
|
{#if !colorEquals(newColor, oldColor)}
|
||||||
<TextLabel>New</TextLabel>
|
<TextLabel>New</TextLabel>
|
||||||
{/if}
|
{/if}
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
{#if !newColor.equals(oldColor)}
|
{#if !colorEquals(newColor, oldColor)}
|
||||||
<LayoutCol class="old-color" classes={{ none: oldIsNone }}>
|
<LayoutCol class="old-color" classes={{ none: oldIsNone }}>
|
||||||
<TextLabel>Old</TextLabel>
|
<TextLabel>Old</TextLabel>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
@@ -552,7 +571,7 @@
|
|||||||
<Separator style="Related" />
|
<Separator style="Related" />
|
||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={newColor.toHexOptionalAlpha() || "-"}
|
value={colorToHexOptionalAlpha(newColor) || "-"}
|
||||||
{disabled}
|
{disabled}
|
||||||
on:commitText={({ detail }) => {
|
on:commitText={({ detail }) => {
|
||||||
dispatch("startHistoryTransaction");
|
dispatch("startHistoryTransaction");
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
type MenuDirection,
|
type MenuDirection,
|
||||||
type MouseCursorIcon,
|
type MouseCursorIcon,
|
||||||
type XY,
|
type XY,
|
||||||
Color,
|
type Color,
|
||||||
|
isColor,
|
||||||
|
createColor,
|
||||||
|
colorToHexOptionalAlpha,
|
||||||
DisplayEditableTextbox,
|
DisplayEditableTextbox,
|
||||||
DisplayEditableTextboxUpdateFontData,
|
DisplayEditableTextboxUpdateFontData,
|
||||||
DisplayEditableTextboxTransform,
|
DisplayEditableTextboxTransform,
|
||||||
@@ -360,7 +363,7 @@
|
|||||||
textInput.style.height = height;
|
textInput.style.height = height;
|
||||||
textInput.style.lineHeight = `${data.lineHeightRatio}`;
|
textInput.style.lineHeight = `${data.lineHeightRatio}`;
|
||||||
textInput.style.fontSize = `${data.fontSize}px`;
|
textInput.style.fontSize = `${data.fontSize}px`;
|
||||||
textInput.style.color = data.color.toHexOptionalAlpha() || "transparent";
|
textInput.style.color = colorToHexOptionalAlpha(data.color) || "transparent";
|
||||||
textInput.style.textAlign = data.align;
|
textInput.style.textAlign = data.align;
|
||||||
|
|
||||||
textInput.oninput = () => {
|
textInput.oninput = () => {
|
||||||
@@ -609,9 +612,9 @@
|
|||||||
gradientStopPickerColor = undefined;
|
gradientStopPickerColor = undefined;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
colorOrGradient={gradientStopPickerColor || new Color()}
|
colorOrGradient={gradientStopPickerColor || createColor(0, 0, 0, 1)}
|
||||||
on:colorOrGradient={({ detail }) => {
|
on:colorOrGradient={({ detail }) => {
|
||||||
if (detail instanceof Color) {
|
if (isColor(detail)) {
|
||||||
editor.handle.updateGradientStopColor(detail.red, detail.green, detail.blue, detail.alpha);
|
editor.handle.updateGradientStopColor(detail.red, detail.green, detail.blue, detail.alpha);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
import type { FillChoice, MenuDirection, ActionShortcut } from "@graphite/messages";
|
import type { FillChoice, MenuDirection, ActionShortcut } from "@graphite/messages";
|
||||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
import { type Color, contrastingOutlineFactor, isColor, isGradient, colorToHexOptionalAlpha, gradientToLinearGradientCSS } from "@graphite/messages";
|
||||||
|
|
||||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
|
|
||||||
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
|
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
|
||||||
$: outlined = outlineFactor > 0.0001;
|
$: outlined = outlineFactor > 0.0001;
|
||||||
$: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`;
|
$: chosenGradient = isGradient(value) ? gradientToLinearGradientCSS(value) : `linear-gradient(${colorToHexOptionalAlpha(value)}, ${colorToHexOptionalAlpha(value)})`;
|
||||||
$: none = value instanceof Color ? value.none : false;
|
$: none = isColor(value) ? value.none : false;
|
||||||
$: transparency = value instanceof Gradient ? value.color.some((color) => color.alpha < 1) : value.alpha < 1;
|
$: transparency = isGradient(value) ? value.color.some((color: Color) => color.alpha < 1) : value.alpha < 1;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
|
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { createEventDispatcher, onDestroy } from "svelte";
|
import { createEventDispatcher, onDestroy } from "svelte";
|
||||||
|
|
||||||
import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm";
|
import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||||
import { Color, type Gradient } from "@graphite/messages";
|
import { type Color, type Gradient, createColor, colorToHexOptionalAlpha, colorToRgbCSS, gradientFirstColor, gradientLastColor, gradientToLinearGradientCSS } from "@graphite/messages";
|
||||||
|
|
||||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
// Determine the color of the new stop by evaluating the gradient at the position of the new stop
|
// 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 };
|
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 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);
|
const color = createColor(evaluated.red, evaluated.green, evaluated.blue, evaluated.alpha);
|
||||||
|
|
||||||
// Insert the new stop into the gradient
|
// Insert the new stop into the gradient
|
||||||
gradient.position.splice(index, 0, position);
|
gradient.position.splice(index, 0, position);
|
||||||
@@ -368,9 +368,9 @@
|
|||||||
class="spectrum-input"
|
class="spectrum-input"
|
||||||
classes={{ disabled }}
|
classes={{ disabled }}
|
||||||
styles={{
|
styles={{
|
||||||
"--gradient-start": gradient.firstColor()?.toHexOptionalAlpha() || "black",
|
"--gradient-start": ((color) => (color ? colorToHexOptionalAlpha(color) : "black"))(gradientFirstColor(gradient)),
|
||||||
"--gradient-end": gradient.lastColor()?.toHexOptionalAlpha() || "black",
|
"--gradient-end": ((color) => (color ? colorToHexOptionalAlpha(color) : "black"))(gradientLastColor(gradient)),
|
||||||
"--gradient-stops": gradient.toLinearGradientCSS(),
|
"--gradient-stops": gradientToLinearGradientCSS(gradient),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LayoutRow class="gradient-strip" on:pointerdown={insertStop}></LayoutRow>
|
<LayoutRow class="gradient-strip" on:pointerdown={insertStop}></LayoutRow>
|
||||||
@@ -396,7 +396,7 @@
|
|||||||
class="marker"
|
class="marker"
|
||||||
class:active={index === activeMarkerIndex && !activeMarkerIsMidpoint}
|
class:active={index === activeMarkerIndex && !activeMarkerIsMidpoint}
|
||||||
style:--marker-position={marker.position}
|
style:--marker-position={marker.position}
|
||||||
style:--marker-color={marker.color.toRgbCSS()}
|
style:--marker-color={colorToRgbCSS(marker.color)}
|
||||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||||
data-gradient-marker
|
data-gradient-marker
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
import type { Editor } from "@graphite/editor";
|
import type { Editor } from "@graphite/editor";
|
||||||
import { Color } from "@graphite/messages";
|
import { type Color, isColor, colorToRgbaCSS } from "@graphite/messages";
|
||||||
|
|
||||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
@@ -38,22 +38,22 @@
|
|||||||
|
|
||||||
<LayoutCol class="working-colors-button">
|
<LayoutCol class="working-colors-button">
|
||||||
<LayoutRow class="primary swatch">
|
<LayoutRow class="primary swatch">
|
||||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={colorToRgbaCSS(primary)} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
open={primaryOpen}
|
open={primaryOpen}
|
||||||
on:open={({ detail }) => (primaryOpen = detail)}
|
on:open={({ detail }) => (primaryOpen = detail)}
|
||||||
colorOrGradient={primary}
|
colorOrGradient={primary}
|
||||||
on:colorOrGradient={({ detail }) => detail instanceof Color && primaryColorChanged(detail)}
|
on:colorOrGradient={({ detail }) => isColor(detail) && primaryColorChanged(detail)}
|
||||||
direction="Right"
|
direction="Right"
|
||||||
/>
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="secondary swatch">
|
<LayoutRow class="secondary swatch">
|
||||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={colorToRgbaCSS(secondary)} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
open={secondaryOpen}
|
open={secondaryOpen}
|
||||||
on:open={({ detail }) => (secondaryOpen = detail)}
|
on:open={({ detail }) => (secondaryOpen = detail)}
|
||||||
colorOrGradient={secondary}
|
colorOrGradient={secondary}
|
||||||
on:colorOrGradient={({ detail }) => detail instanceof Color && secondaryColorChanged(detail)}
|
on:colorOrGradient={({ detail }) => isColor(detail) && secondaryColorChanged(detail)}
|
||||||
direction="Right"
|
direction="Right"
|
||||||
/>
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
|
|||||||
+197
-259
@@ -365,282 +365,227 @@ export type ActionShortcut = { shortcut: LabeledShortcut };
|
|||||||
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
|
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
|
||||||
export type HSVA = { h: number; s: number; v: number; a: number };
|
export type HSVA = { h: number; s: number; v: number; a: number };
|
||||||
export type HSV = { h: number; s: number; v: number };
|
export type HSV = { h: number; s: number; v: number };
|
||||||
export type RGBA = { r: number; g: number; b: number; a: number };
|
|
||||||
export type RGB = { r: number; g: number; b: number };
|
export type RGB = { r: number; g: number; b: number };
|
||||||
|
|
||||||
export class Gradient {
|
export class Gradient {
|
||||||
position!: number[];
|
position!: number[];
|
||||||
midpoint!: number[];
|
midpoint!: number[];
|
||||||
color!: Color[];
|
color!: Color[];
|
||||||
|
|
||||||
constructor(position: number[], midpoint: number[], color: Color[]) {
|
|
||||||
this.position = position;
|
|
||||||
this.midpoint = midpoint;
|
|
||||||
this.color = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
toLinearGradientCSS(): string {
|
|
||||||
if (this.position.length === 1) {
|
|
||||||
return `linear-gradient(to right, ${this.color[0].toHexOptionalAlpha()} 0%, ${this.color[0].toHexOptionalAlpha()} 100%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pieces = sampleInterpolatedGradient(new Float64Array(this.position), new Float64Array(this.midpoint), this.color, false);
|
|
||||||
return `linear-gradient(to right, ${pieces})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toLinearGradientCSSNoAlpha(): string {
|
|
||||||
if (this.position.length === 1) {
|
|
||||||
return `linear-gradient(to right, ${this.color[0].toHexNoAlpha()} 0%, ${this.color[0].toHexNoAlpha()} 100%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.color[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
lastColor(): Color | undefined {
|
|
||||||
return this.color[this.color.length - 1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All channels range are represented by 0-1, sRGB, gamma.
|
// All channels range are represented by 0-1, sRGB, gamma.
|
||||||
export class Color {
|
export class Color {
|
||||||
readonly red!: number;
|
readonly red!: number;
|
||||||
|
|
||||||
readonly green!: number;
|
readonly green!: number;
|
||||||
|
|
||||||
readonly blue!: number;
|
readonly blue!: number;
|
||||||
|
|
||||||
readonly alpha!: number;
|
readonly alpha!: number;
|
||||||
|
|
||||||
readonly none!: boolean;
|
readonly none!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
constructor();
|
// COLOR FACTORY FUNCTIONS
|
||||||
|
|
||||||
constructor(none: "none");
|
export function createColor(red: number, green: number, blue: number, alpha: number): Color {
|
||||||
|
return { red, green, blue, alpha, none: false };
|
||||||
|
}
|
||||||
|
|
||||||
constructor(hsva: HSVA);
|
export function createNoneColor(): Color {
|
||||||
|
return { red: 0, green: 0, blue: 0, alpha: 1, none: true };
|
||||||
|
}
|
||||||
|
|
||||||
constructor(red: number, green: number, blue: number, alpha: number);
|
export function createColorFromHSVA(h: number, s: number, v: number, a: number): Color {
|
||||||
|
const convert = (n: number): number => {
|
||||||
|
const k = (n + h * 6) % 6;
|
||||||
|
return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0);
|
||||||
|
};
|
||||||
|
|
||||||
constructor(firstArg?: "none" | HSVA | number, green?: number, blue?: number, alpha?: number) {
|
return { red: convert(5), green: convert(3), blue: convert(1), alpha: a, none: false };
|
||||||
// Empty constructor
|
}
|
||||||
if (firstArg === undefined) {
|
|
||||||
this.red = 0;
|
// COLOR UTILITY FUNCTIONS
|
||||||
this.green = 0;
|
|
||||||
this.blue = 0;
|
export function colorFromCSS(colorCode: string): Color | undefined {
|
||||||
this.alpha = 1;
|
// Allow single-digit hex value inputs
|
||||||
this.none = false;
|
let colorValue = colorCode.trim();
|
||||||
} else if (firstArg === "none") {
|
if (colorValue.length === 2 && colorValue.charAt(0) === "#" && /[0-9a-f]/i.test(colorValue.charAt(1))) {
|
||||||
this.red = 0;
|
const digit = colorValue.charAt(1);
|
||||||
this.green = 0;
|
colorValue = `#${digit}${digit}${digit}`;
|
||||||
this.blue = 0;
|
}
|
||||||
this.alpha = 1;
|
|
||||||
this.none = true;
|
const canvas = document.createElement("canvas");
|
||||||
}
|
canvas.width = 1;
|
||||||
// HSVA constructor
|
canvas.height = 1;
|
||||||
else if (typeof firstArg === "object" && green === undefined && blue === undefined && alpha === undefined) {
|
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
const { h, s, v } = firstArg;
|
if (!context) return undefined;
|
||||||
const convert = (n: number): number => {
|
|
||||||
const k = (n + h * 6) % 6;
|
context.clearRect(0, 0, 1, 1);
|
||||||
return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0);
|
|
||||||
};
|
context.fillStyle = "black";
|
||||||
|
context.fillStyle = colorValue;
|
||||||
this.red = convert(5);
|
const comparisonA = context.fillStyle;
|
||||||
this.green = convert(3);
|
|
||||||
this.blue = convert(1);
|
context.fillStyle = "white";
|
||||||
this.alpha = firstArg.a;
|
context.fillStyle = colorValue;
|
||||||
this.none = false;
|
const comparisonB = context.fillStyle;
|
||||||
}
|
|
||||||
// RGBA constructor
|
// Invalid color
|
||||||
else if (typeof firstArg === "number" && typeof green === "number" && typeof blue === "number" && typeof alpha === "number") {
|
if (comparisonA !== comparisonB) {
|
||||||
this.red = firstArg;
|
// If this color code didn't start with a #, add it and try again
|
||||||
this.green = green;
|
if (colorValue.trim().charAt(0) !== "#") return colorFromCSS(`#${colorValue.trim()}`);
|
||||||
this.blue = blue;
|
return undefined;
|
||||||
this.alpha = alpha;
|
}
|
||||||
this.none = false;
|
|
||||||
|
context.fillRect(0, 0, 1, 1);
|
||||||
|
|
||||||
|
const [r, g, b, a] = [...context.getImageData(0, 0, 1, 1).data];
|
||||||
|
return createColor(r / 255, g / 255, b / 255, a / 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorEquals(c1: Color, c2: Color): boolean {
|
||||||
|
if (c1.none && c2.none) return true;
|
||||||
|
return Math.abs(c1.red - c2.red) < 1e-6 && Math.abs(c1.green - c2.green) < 1e-6 && Math.abs(c1.blue - c2.blue) < 1e-6 && Math.abs(c1.alpha - c2.alpha) < 1e-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorToHexNoAlpha(color: Color): string | undefined {
|
||||||
|
if (color.none) return undefined;
|
||||||
|
|
||||||
|
const r = Math.round(color.red * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0");
|
||||||
|
const g = Math.round(color.green * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0");
|
||||||
|
const b = Math.round(color.blue * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0");
|
||||||
|
|
||||||
|
return `#${r}${g}${b}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorToHexOptionalAlpha(color: Color): string | undefined {
|
||||||
|
if (color.none) return undefined;
|
||||||
|
|
||||||
|
const hex = colorToHexNoAlpha(color);
|
||||||
|
const a = Math.round(color.alpha * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0");
|
||||||
|
|
||||||
|
return a === "ff" ? hex : `${hex}${a}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorToRgb255(color: Color): RGB | undefined {
|
||||||
|
if (color.none) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: Math.round(color.red * 255),
|
||||||
|
g: Math.round(color.green * 255),
|
||||||
|
b: Math.round(color.blue * 255),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorToRgbCSS(color: Color): string | undefined {
|
||||||
|
const rgb = colorToRgb255(color);
|
||||||
|
if (!rgb) return undefined;
|
||||||
|
|
||||||
|
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorToRgbaCSS(color: Color): string | undefined {
|
||||||
|
const rgb = colorToRgb255(color);
|
||||||
|
if (!rgb) return undefined;
|
||||||
|
|
||||||
|
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorToHSVA(color: Color): HSVA | undefined {
|
||||||
|
if (color.none) return undefined;
|
||||||
|
|
||||||
|
const { red: r, green: g, blue: b, alpha: a } = color;
|
||||||
|
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
|
||||||
|
const d = max - min;
|
||||||
|
const s = max === 0 ? 0 : d / max;
|
||||||
|
const v = max;
|
||||||
|
|
||||||
|
let h = 0;
|
||||||
|
if (max !== min) {
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
h /= 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromCSS(colorCode: string): Color | undefined {
|
return { h, s, v, a };
|
||||||
// Allow single-digit hex value inputs
|
}
|
||||||
let colorValue = colorCode.trim();
|
|
||||||
if (colorValue.length === 2 && colorValue.charAt(0) === "#" && /[0-9a-f]/i.test(colorValue.charAt(1))) {
|
|
||||||
const digit = colorValue.charAt(1);
|
|
||||||
colorValue = `#${digit}${digit}${digit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
export function colorOpaque(color: Color): Color | undefined {
|
||||||
canvas.width = 1;
|
if (color.none) return undefined;
|
||||||
canvas.height = 1;
|
|
||||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
||||||
if (!context) return undefined;
|
|
||||||
|
|
||||||
context.clearRect(0, 0, 1, 1);
|
return createColor(color.red, color.green, color.blue, 1);
|
||||||
|
}
|
||||||
|
|
||||||
context.fillStyle = "black";
|
export function colorLuminance(color: Color): number | undefined {
|
||||||
context.fillStyle = colorValue;
|
if (color.none) return undefined;
|
||||||
const comparisonA = context.fillStyle;
|
|
||||||
|
|
||||||
context.fillStyle = "white";
|
// Convert alpha into white
|
||||||
context.fillStyle = colorValue;
|
const r = color.red * color.alpha + (1 - color.alpha);
|
||||||
const comparisonB = context.fillStyle;
|
const g = color.green * color.alpha + (1 - color.alpha);
|
||||||
|
const b = color.blue * color.alpha + (1 - color.alpha);
|
||||||
|
|
||||||
// Invalid color
|
// https://stackoverflow.com/a/3943023/775283
|
||||||
if (comparisonA !== comparisonB) {
|
|
||||||
// If this color code didn't start with a #, add it and try again
|
|
||||||
if (colorValue.trim().charAt(0) !== "#") return Color.fromCSS(`#${colorValue.trim()}`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.fillRect(0, 0, 1, 1);
|
const linearR = r <= 0.04045 ? r / 12.92 : ((r + 0.055) / 1.055) ** 2.4;
|
||||||
|
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 [r, g, b, a] = [...context.getImageData(0, 0, 1, 1).data];
|
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
||||||
return new Color(r / 255, g / 255, b / 255, a / 255);
|
}
|
||||||
|
|
||||||
|
export function colorContrastingColor(color: Color): "black" | "white" {
|
||||||
|
if (color.none) return "black";
|
||||||
|
|
||||||
|
const luminance = colorLuminance(color);
|
||||||
|
|
||||||
|
return luminance && luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRADIENT UTILITY FUNCTIONS
|
||||||
|
|
||||||
|
export function gradientToLinearGradientCSS(gradient: Gradient): string {
|
||||||
|
if (gradient.position.length === 1) {
|
||||||
|
return `linear-gradient(to right, ${colorToHexOptionalAlpha(gradient.color[0])} 0%, ${colorToHexOptionalAlpha(gradient.color[0])} 100%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
equals(other: Color): boolean {
|
const pieces = sampleInterpolatedGradient(new Float64Array(gradient.position), new Float64Array(gradient.midpoint), gradient.color, false);
|
||||||
if (this.none && other.none) return true;
|
return `linear-gradient(to right, ${pieces})`;
|
||||||
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;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toHexNoAlpha(): string | undefined {
|
export function gradientFirstColor(gradient: Gradient): Color | undefined {
|
||||||
if (this.none) return undefined;
|
return gradient.color[0];
|
||||||
|
}
|
||||||
|
|
||||||
const r = Math.round(this.red * 255)
|
export function gradientLastColor(gradient: Gradient): Color | undefined {
|
||||||
.toString(16)
|
return gradient.color[gradient.color.length - 1];
|
||||||
.padStart(2, "0");
|
}
|
||||||
const g = Math.round(this.green * 255)
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, "0");
|
|
||||||
const b = Math.round(this.blue * 255)
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, "0");
|
|
||||||
|
|
||||||
return `#${r}${g}${b}`;
|
// COLOR/GRADIENT TYPE GUARDS
|
||||||
}
|
|
||||||
|
|
||||||
toHexOptionalAlpha(): string | undefined {
|
export function isColor(value: unknown): value is Color {
|
||||||
if (this.none) return undefined;
|
return typeof value === "object" && value !== null && "red" in value;
|
||||||
|
}
|
||||||
|
|
||||||
const hex = this.toHexNoAlpha();
|
export function isGradient(value: unknown): value is Gradient {
|
||||||
const a = Math.round(this.alpha * 255)
|
return typeof value === "object" && value !== null && "position" in value && "midpoint" in value;
|
||||||
.toString(16)
|
|
||||||
.padStart(2, "0");
|
|
||||||
|
|
||||||
return a === "ff" ? hex : `${hex}${a}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toRgb255(): RGB | undefined {
|
|
||||||
if (this.none) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
r: Math.round(this.red * 255),
|
|
||||||
g: Math.round(this.green * 255),
|
|
||||||
b: Math.round(this.blue * 255),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toRgbCSS(): string | undefined {
|
|
||||||
const rgb = this.toRgb255();
|
|
||||||
if (!rgb) return undefined;
|
|
||||||
|
|
||||||
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toRgbaCSS(): string | undefined {
|
|
||||||
const rgb = this.toRgb255();
|
|
||||||
if (!rgb) return undefined;
|
|
||||||
|
|
||||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${this.alpha})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toHSV(): HSV | undefined {
|
|
||||||
const hsva = this.toHSVA();
|
|
||||||
if (!hsva) return undefined;
|
|
||||||
|
|
||||||
return { h: hsva.h, s: hsva.s, v: hsva.v };
|
|
||||||
}
|
|
||||||
|
|
||||||
toHSVA(): HSVA | undefined {
|
|
||||||
if (this.none) return undefined;
|
|
||||||
|
|
||||||
const { red: r, green: g, blue: b, alpha: a } = this;
|
|
||||||
|
|
||||||
const max = Math.max(r, g, b);
|
|
||||||
const min = Math.min(r, g, b);
|
|
||||||
|
|
||||||
const d = max - min;
|
|
||||||
const s = max === 0 ? 0 : d / max;
|
|
||||||
const v = max;
|
|
||||||
|
|
||||||
let h = 0;
|
|
||||||
if (max !== min) {
|
|
||||||
switch (max) {
|
|
||||||
case r:
|
|
||||||
h = (g - b) / d + (g < b ? 6 : 0);
|
|
||||||
break;
|
|
||||||
case g:
|
|
||||||
h = (b - r) / d + 2;
|
|
||||||
break;
|
|
||||||
case b:
|
|
||||||
h = (r - g) / d + 4;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
h /= 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { h, s, v, a };
|
|
||||||
}
|
|
||||||
|
|
||||||
toHsvDegreesAndPercent(): HSV | undefined {
|
|
||||||
const hsva = this.toHSVA();
|
|
||||||
if (!hsva) return undefined;
|
|
||||||
|
|
||||||
return { h: hsva.h * 360, s: hsva.s * 100, v: hsva.v * 100 };
|
|
||||||
}
|
|
||||||
|
|
||||||
toHsvaDegreesAndPercent(): HSVA | undefined {
|
|
||||||
const hsva = this.toHSVA();
|
|
||||||
if (!hsva) return undefined;
|
|
||||||
|
|
||||||
return { h: hsva.h * 360, s: hsva.s * 100, v: hsva.v * 100, a: hsva.a * 100 };
|
|
||||||
}
|
|
||||||
|
|
||||||
opaque(): Color | undefined {
|
|
||||||
if (this.none) return undefined;
|
|
||||||
|
|
||||||
return new Color(this.red, this.green, this.blue, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
luminance(): number | undefined {
|
|
||||||
if (this.none) return undefined;
|
|
||||||
|
|
||||||
// Convert alpha into white
|
|
||||||
const r = this.red * this.alpha + (1 - this.alpha);
|
|
||||||
const g = this.green * this.alpha + (1 - this.alpha);
|
|
||||||
const b = this.blue * this.alpha + (1 - this.alpha);
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/3943023/775283
|
|
||||||
|
|
||||||
const linearR = r <= 0.04045 ? r / 12.92 : ((r + 0.055) / 1.055) ** 2.4;
|
|
||||||
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;
|
|
||||||
|
|
||||||
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateActiveDocument extends JsMessage {
|
export class UpdateActiveDocument extends JsMessage {
|
||||||
@@ -963,22 +908,19 @@ export class CheckboxInput extends WidgetProps {
|
|||||||
export class ColorInput extends WidgetProps {
|
export class ColorInput extends WidgetProps {
|
||||||
// Content
|
// Content
|
||||||
@Transform(({ value }) => {
|
@Transform(({ value }) => {
|
||||||
if (value instanceof Gradient) return value;
|
if (isGradient(value)) return value;
|
||||||
const gradient: Gradient | undefined = value["Gradient"];
|
const gradient: Gradient | undefined = value["Gradient"];
|
||||||
if (gradient) {
|
if (gradient) {
|
||||||
return new Gradient(
|
const color = gradient.color.map((color) => createColor(color.red, color.green, color.blue, color.alpha));
|
||||||
gradient.position,
|
return { ...gradient, color };
|
||||||
gradient.midpoint,
|
|
||||||
gradient.color.map((color) => new Color(color.red, color.green, color.blue, color.alpha)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Color) return value;
|
if (isColor(value)) return value;
|
||||||
|
|
||||||
const solid = value["Solid"];
|
const solid = value["Solid"];
|
||||||
if (solid) return new Color(solid.red, solid.green, solid.blue, solid.alpha);
|
if (solid) return createColor(solid.red, solid.green, solid.blue, solid.alpha);
|
||||||
|
|
||||||
return new Color("none");
|
return createNoneColor();
|
||||||
})
|
})
|
||||||
value!: FillChoice;
|
value!: FillChoice;
|
||||||
allowNone!: boolean;
|
allowNone!: boolean;
|
||||||
@@ -1002,24 +944,20 @@ export type FillChoice = Color | Gradient;
|
|||||||
|
|
||||||
export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number {
|
export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number {
|
||||||
const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor];
|
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 [range1, range2] = pair.map((color) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color)) || createNoneColor());
|
||||||
|
|
||||||
const contrast = (color: Color): number => {
|
const contrast = (color: Color): number => {
|
||||||
const colorLuminance = color.luminance() || 0;
|
const lum = colorLuminance(color) || 0;
|
||||||
let rangeLuminance1 = range1.luminance() || 0;
|
let rangeLuminance1 = colorLuminance(range1) || 0;
|
||||||
let rangeLuminance2 = range2.luminance() || 0;
|
let rangeLuminance2 = colorLuminance(range2) || 0;
|
||||||
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)];
|
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)];
|
||||||
|
|
||||||
const distance = (() => {
|
const distance = Math.max(0, rangeLuminance1 - lum, lum - rangeLuminance2);
|
||||||
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));
|
return (1 - Math.min(distance / proximityRange, 1)) * (1 - (colorToHSVA(color)?.s || 0));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (value instanceof Gradient) {
|
if (isGradient(value)) {
|
||||||
if (value.color.length === 0) return 0;
|
if (value.color.length === 0) return 0;
|
||||||
|
|
||||||
const first = contrast(value.color[0]);
|
const first = contrast(value.color[0]);
|
||||||
|
|||||||
Reference in New Issue
Block a user