@rogieking/figui3 6.20.2 → 6.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -1
- package/components.css +63 -2
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +240 -98
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +16 -16
- package/dist/fig.css +1 -1
- package/dist/fig.js +60 -43
- package/dist/propskit.css +1 -0
- package/dist/propskit.js +591 -0
- package/fig-editor.css +157 -17
- package/fig-editor.js +457 -187
- package/fig-lab.css +51 -42
- package/fig-lab.js +125 -9
- package/fig.js +519 -16
- package/package.json +33 -4
- package/propskit-core.js +556 -0
- package/propskit.d.ts +98 -0
- package/propskit.js +15 -0
package/fig-editor.js
CHANGED
|
@@ -17,6 +17,7 @@ const GRADIENT_INTERPOLATION_SPACES = [
|
|
|
17
17
|
"display-p3",
|
|
18
18
|
"oklab",
|
|
19
19
|
"oklch",
|
|
20
|
+
"hsl",
|
|
20
21
|
];
|
|
21
22
|
const GRADIENT_HUE_INTERPOLATIONS = [
|
|
22
23
|
"shorter",
|
|
@@ -24,6 +25,7 @@ const GRADIENT_HUE_INTERPOLATIONS = [
|
|
|
24
25
|
"increasing",
|
|
25
26
|
"decreasing",
|
|
26
27
|
];
|
|
28
|
+
const GRADIENT_HUE_SPACES = new Set(["oklch", "hsl"]);
|
|
27
29
|
|
|
28
30
|
function normalizeGradientConfig(gradient) {
|
|
29
31
|
const next = { ...(gradient ?? {}) };
|
|
@@ -50,7 +52,7 @@ function gradientToValueShape(gradient) {
|
|
|
50
52
|
...normalized,
|
|
51
53
|
interpolationSpace: normalized.interpolationSpace,
|
|
52
54
|
};
|
|
53
|
-
if (normalized.interpolationSpace
|
|
55
|
+
if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
|
|
54
56
|
output.hueInterpolation = normalized.hueInterpolation;
|
|
55
57
|
} else {
|
|
56
58
|
delete output.hueInterpolation;
|
|
@@ -63,12 +65,37 @@ function gradientInterpolationClause(gradient) {
|
|
|
63
65
|
if (normalized.interpolationSpace === "srgb") {
|
|
64
66
|
return "";
|
|
65
67
|
}
|
|
66
|
-
if (normalized.interpolationSpace
|
|
67
|
-
return `in
|
|
68
|
+
if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
|
|
69
|
+
return `in ${normalized.interpolationSpace} ${normalized.hueInterpolation} hue`;
|
|
68
70
|
}
|
|
69
71
|
return `in ${normalized.interpolationSpace}`;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
function gradientInterpolationSelectValue(gradient) {
|
|
75
|
+
const normalized = normalizeGradientConfig(gradient);
|
|
76
|
+
if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
|
|
77
|
+
return `${normalized.interpolationSpace}-${normalized.hueInterpolation || "shorter"}`;
|
|
78
|
+
}
|
|
79
|
+
return normalized.interpolationSpace;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseGradientInterpolationSelectValue(val) {
|
|
83
|
+
const raw = String(val ?? "");
|
|
84
|
+
for (const space of GRADIENT_HUE_SPACES) {
|
|
85
|
+
const prefix = `${space}-`;
|
|
86
|
+
if (raw.startsWith(prefix)) {
|
|
87
|
+
return {
|
|
88
|
+
interpolationSpace: space,
|
|
89
|
+
hueInterpolation: raw.slice(prefix.length) || "shorter",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
interpolationSpace: raw || "srgb",
|
|
95
|
+
hueInterpolation: "shorter",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
72
99
|
/**
|
|
73
100
|
* A comprehensive fill picker component supporting solid colors, gradients, images, video, and webcam.
|
|
74
101
|
* Uses display: contents and wraps a trigger element that opens a dialog picker.
|
|
@@ -118,6 +145,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
118
145
|
#isDraggingColor = false;
|
|
119
146
|
#syncingGradientBar = false;
|
|
120
147
|
#teardownColorAreaEvents = null;
|
|
148
|
+
#gradientInterpolationOpenObserver = null;
|
|
121
149
|
#valueAtOpen = null;
|
|
122
150
|
#webcamStart = null;
|
|
123
151
|
#webcamRequestId = 0;
|
|
@@ -285,9 +313,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
285
313
|
if (parsed.opacity !== undefined) {
|
|
286
314
|
this.#color.a = parsed.opacity / 100;
|
|
287
315
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
316
|
+
// Gamut UI hidden for now — lock to sRGB.
|
|
317
|
+
this.#gamut = "srgb";
|
|
291
318
|
if (parsed.gradient) {
|
|
292
319
|
this.#gradient = normalizeGradientConfig({
|
|
293
320
|
...this.#gradient,
|
|
@@ -390,9 +417,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
390
417
|
this.#valueAtOpen = JSON.stringify(this.value);
|
|
391
418
|
this.#switchTab(this.#fillType, { emit: false });
|
|
392
419
|
|
|
393
|
-
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
394
|
-
if (gamutEl) gamutEl.value = this.#gamut;
|
|
395
|
-
|
|
396
420
|
if (this.#swatch) this.#swatch.setAttribute("selected", "true");
|
|
397
421
|
|
|
398
422
|
this.#dialog.open = true;
|
|
@@ -429,6 +453,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
429
453
|
this.#teardownColorAreaEvents();
|
|
430
454
|
this.#teardownColorAreaEvents = null;
|
|
431
455
|
}
|
|
456
|
+
this.#gradientInterpolationOpenObserver?.disconnect();
|
|
457
|
+
this.#gradientInterpolationOpenObserver = null;
|
|
432
458
|
this.#stopWebcam();
|
|
433
459
|
if (!this.#dialog) return;
|
|
434
460
|
this.#restoreCustomSlotContent();
|
|
@@ -508,11 +534,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
508
534
|
this.#activeTab = allowedModes[0];
|
|
509
535
|
}
|
|
510
536
|
|
|
511
|
-
const experimental = this.getAttribute("experimental");
|
|
512
|
-
const expAttr = experimental
|
|
513
|
-
? `experimental="${figEditorEscapeAttribute(experimental)}"`
|
|
514
|
-
: "";
|
|
515
|
-
|
|
516
537
|
let headerContent;
|
|
517
538
|
if (allowedModes.length === 1) {
|
|
518
539
|
headerContent = `<h3 class="fig-fill-picker-type-label">${figEditorEscapeAttribute(modeLabels[allowedModes[0]])}</h3>`;
|
|
@@ -520,12 +541,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
520
541
|
const options = allowedModes
|
|
521
542
|
.map(
|
|
522
543
|
(m) =>
|
|
523
|
-
`<option value="${figEditorEscapeAttribute(m)}">${figEditorEscapeAttribute(modeLabels[m])}</option>`,
|
|
544
|
+
`<fig-select-option value="${figEditorEscapeAttribute(m)}">${figEditorEscapeAttribute(modeLabels[m])}</fig-select-option>`,
|
|
524
545
|
)
|
|
525
|
-
.join("\n
|
|
526
|
-
headerContent = `<fig-
|
|
527
|
-
|
|
528
|
-
|
|
546
|
+
.join("\n ");
|
|
547
|
+
headerContent = `<fig-select class="fig-fill-picker-type" label="Fill type" value="${figEditorEscapeAttribute(this.#fillType)}">
|
|
548
|
+
<fig-select-options>
|
|
549
|
+
${options}
|
|
550
|
+
</fig-select-options>
|
|
551
|
+
</fig-select>`;
|
|
529
552
|
}
|
|
530
553
|
|
|
531
554
|
// Generate tab containers for all allowed modes
|
|
@@ -536,15 +559,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
536
559
|
)
|
|
537
560
|
.join("\n ");
|
|
538
561
|
|
|
539
|
-
const gamutDropdown = `<fig-dropdown class="fig-fill-picker-gamut" label="Color gamut" ${expAttr} value="${this.#gamut}">
|
|
540
|
-
<option value="srgb">sRGB</option>
|
|
541
|
-
<option value="display-p3">Display P3</option>
|
|
542
|
-
</fig-dropdown>`;
|
|
543
|
-
|
|
544
562
|
this.#dialog.innerHTML = `
|
|
545
563
|
<fig-header>
|
|
546
564
|
${headerContent}
|
|
547
|
-
${gamutDropdown}
|
|
548
565
|
<fig-button icon variant="ghost" class="fig-fill-picker-close" aria-label="Close fill picker">
|
|
549
566
|
<fig-icon name="close"></fig-icon>
|
|
550
567
|
</fig-button>
|
|
@@ -577,27 +594,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
577
594
|
);
|
|
578
595
|
}
|
|
579
596
|
|
|
580
|
-
// Setup type
|
|
581
|
-
const
|
|
582
|
-
if (
|
|
583
|
-
|
|
584
|
-
|
|
597
|
+
// Setup type select switching (only if not locked)
|
|
598
|
+
const typeSelect = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
599
|
+
if (typeSelect) {
|
|
600
|
+
typeSelect.addEventListener("change", (e) => {
|
|
601
|
+
const next =
|
|
602
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
603
|
+
if (next) this.#switchTab(next);
|
|
585
604
|
});
|
|
586
605
|
}
|
|
587
606
|
|
|
588
|
-
// Setup gamut dropdown
|
|
589
|
-
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
590
|
-
if (gamutEl) {
|
|
591
|
-
const handleGamutChange = (e) => {
|
|
592
|
-
const val = e.currentTarget?.value ?? e.target?.value ?? e.detail;
|
|
593
|
-
if (val && val !== this.#gamut) {
|
|
594
|
-
this.#gamut = val;
|
|
595
|
-
this.#onGamutChange();
|
|
596
|
-
}
|
|
597
|
-
};
|
|
598
|
-
gamutEl.addEventListener("change", handleGamutChange);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
607
|
this.#dialog
|
|
602
608
|
.querySelector(".fig-fill-picker-close")
|
|
603
609
|
.addEventListener("click", () => {
|
|
@@ -666,10 +672,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
666
672
|
this.#stopWebcam();
|
|
667
673
|
}
|
|
668
674
|
|
|
669
|
-
// Update
|
|
670
|
-
const
|
|
671
|
-
if (
|
|
672
|
-
|
|
675
|
+
// Update type select (only exists if not locked)
|
|
676
|
+
const typeSelect = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
677
|
+
if (typeSelect && typeSelect.value !== tabName) {
|
|
678
|
+
typeSelect.value = tabName;
|
|
673
679
|
}
|
|
674
680
|
|
|
675
681
|
// Show/hide tab content
|
|
@@ -711,8 +717,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
711
717
|
#initSolidTab() {
|
|
712
718
|
const container = this.#dialog.querySelector('[data-tab="solid"]');
|
|
713
719
|
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
714
|
-
const experimental = this.getAttribute("experimental");
|
|
715
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
716
720
|
|
|
717
721
|
container.innerHTML = `
|
|
718
722
|
<fig-preview class="fig-fill-picker-color-area">
|
|
@@ -728,28 +732,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
728
732
|
drag-snapping="modifier"
|
|
729
733
|
></fig-handle>
|
|
730
734
|
</fig-preview>
|
|
731
|
-
<div class="fig-fill-picker-sliders">
|
|
735
|
+
<div class="fig-fill-picker-sliders${showAlpha ? "" : " is-hue-only"}">
|
|
732
736
|
<fig-tooltip text="Sample color"><fig-button icon variant="ghost" class="fig-fill-picker-eyedropper" aria-label="Sample color"><fig-icon name="eyedropper"></fig-icon></fig-button></fig-tooltip>
|
|
733
|
-
<fig-slider type="hue" text="false" min="0" max="360" aria-label="Hue" value="${
|
|
737
|
+
<fig-slider type="hue" variant="classic" text="false" min="0" max="360" aria-label="Hue" value="${
|
|
734
738
|
this.#color.h
|
|
735
739
|
}"></fig-slider>
|
|
736
740
|
${
|
|
737
741
|
showAlpha
|
|
738
|
-
? `<fig-slider type="opacity"
|
|
742
|
+
? `<fig-slider type="opacity" variant="classic" text="false" min="0" max="100" aria-label="Opacity" value="${
|
|
739
743
|
this.#color.a * 100
|
|
740
744
|
}" color="${this.#hsvToHex(this.#color)}"></fig-slider>`
|
|
741
745
|
: ""
|
|
742
746
|
}
|
|
743
747
|
</div>
|
|
744
748
|
<fig-field class="fig-fill-picker-inputs">
|
|
745
|
-
<fig-
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
749
|
+
<fig-select class="fig-fill-picker-input-mode" label="Color value format" value="${figEditorEscapeAttribute(this.#colorInputMode)}">
|
|
750
|
+
<fig-select-options>
|
|
751
|
+
<fig-select-option value="hex">Hex</fig-select-option>
|
|
752
|
+
<fig-select-option value="rgb">RGB</fig-select-option>
|
|
753
|
+
<fig-select-option value="css">CSS</fig-select-option>
|
|
754
|
+
<fig-select-option value="hsl">HSL</fig-select-option>
|
|
755
|
+
<fig-select-option value="hsb">HSB</fig-select-option>
|
|
756
|
+
<fig-select-option value="lab">LAB</fig-select-option>
|
|
757
|
+
<fig-select-option value="lch">LCH</fig-select-option>
|
|
758
|
+
</fig-select-options>
|
|
759
|
+
</fig-select>
|
|
753
760
|
<span class="fig-fill-picker-input-fields"></span>
|
|
754
761
|
</fig-field>
|
|
755
762
|
`;
|
|
@@ -789,10 +796,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
789
796
|
});
|
|
790
797
|
}
|
|
791
798
|
|
|
792
|
-
// Setup color input mode
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
799
|
+
// Setup color input mode select
|
|
800
|
+
const modeSelect = container.querySelector(".fig-fill-picker-input-mode");
|
|
801
|
+
modeSelect.addEventListener("change", (e) => {
|
|
802
|
+
const next =
|
|
803
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
804
|
+
if (!next) return;
|
|
805
|
+
this.#colorInputMode = next;
|
|
796
806
|
this.#rebuildColorInputFields();
|
|
797
807
|
});
|
|
798
808
|
|
|
@@ -1025,8 +1035,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
1025
1035
|
const wrap = (tooltip, html) =>
|
|
1026
1036
|
`<fig-tooltip text="${tooltip}">${html}</fig-tooltip>`;
|
|
1027
1037
|
|
|
1028
|
-
const num = (cls, label, min, max, step) =>
|
|
1029
|
-
`<fig-input-number class="${cls}" aria-label="${label}" min="${min}" max="${max}"${step != null ? ` step="${step}"` : ""}></fig-input-number>`;
|
|
1038
|
+
const num = (cls, label, min, max, step, units) =>
|
|
1039
|
+
`<fig-input-number class="${cls}" aria-label="${label}" min="${min}" max="${max}"${step != null ? ` step="${step}"` : ""}${units ? ` units="${units}"` : ""}></fig-input-number>`;
|
|
1040
|
+
|
|
1041
|
+
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
1042
|
+
const alphaField = () =>
|
|
1043
|
+
showAlpha
|
|
1044
|
+
? wrap(
|
|
1045
|
+
"Alpha",
|
|
1046
|
+
num("fig-fill-picker-ci-a", "Alpha", 0, 100, 0.1, "%"),
|
|
1047
|
+
)
|
|
1048
|
+
: "";
|
|
1030
1049
|
|
|
1031
1050
|
let html;
|
|
1032
1051
|
switch (this.#colorInputMode) {
|
|
@@ -1035,6 +1054,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1035
1054
|
${wrap("Red", num("fig-fill-picker-ci-r", "Red", 0, 255))}
|
|
1036
1055
|
${wrap("Green", num("fig-fill-picker-ci-g", "Green", 0, 255))}
|
|
1037
1056
|
${wrap("Blue", num("fig-fill-picker-ci-b", "Blue", 0, 255))}
|
|
1057
|
+
${alphaField()}
|
|
1038
1058
|
</div>`;
|
|
1039
1059
|
break;
|
|
1040
1060
|
case "hsl":
|
|
@@ -1042,6 +1062,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1042
1062
|
${wrap("Hue", num("fig-fill-picker-ci-h", "Hue", 0, 360))}
|
|
1043
1063
|
${wrap("Saturation", num("fig-fill-picker-ci-s", "Saturation", 0, 100))}
|
|
1044
1064
|
${wrap("Lightness", num("fig-fill-picker-ci-l", "Lightness", 0, 100))}
|
|
1065
|
+
${alphaField()}
|
|
1045
1066
|
</div>`;
|
|
1046
1067
|
break;
|
|
1047
1068
|
case "hsb":
|
|
@@ -1049,6 +1070,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1049
1070
|
${wrap("Hue", num("fig-fill-picker-ci-h", "Hue", 0, 360))}
|
|
1050
1071
|
${wrap("Saturation", num("fig-fill-picker-ci-s", "Saturation", 0, 100))}
|
|
1051
1072
|
${wrap("Brightness", num("fig-fill-picker-ci-v", "Brightness", 0, 100))}
|
|
1073
|
+
${alphaField()}
|
|
1052
1074
|
</div>`;
|
|
1053
1075
|
break;
|
|
1054
1076
|
case "lab":
|
|
@@ -1056,6 +1078,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1056
1078
|
${wrap("Lightness", num("fig-fill-picker-ci-okl", "Lightness", 0, 100))}
|
|
1057
1079
|
${wrap("Green-Red axis", num("fig-fill-picker-ci-oka", "Green-Red axis", -0.4, 0.4, 0.001))}
|
|
1058
1080
|
${wrap("Blue-Yellow axis", num("fig-fill-picker-ci-okb", "Blue-Yellow axis", -0.4, 0.4, 0.001))}
|
|
1081
|
+
${alphaField()}
|
|
1059
1082
|
</div>`;
|
|
1060
1083
|
break;
|
|
1061
1084
|
case "lch":
|
|
@@ -1063,10 +1086,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
1063
1086
|
${wrap("Lightness", num("fig-fill-picker-ci-okl", "Lightness", 0, 100))}
|
|
1064
1087
|
${wrap("Chroma", num("fig-fill-picker-ci-okc", "Chroma", 0, 0.4, 0.001))}
|
|
1065
1088
|
${wrap("Hue", num("fig-fill-picker-ci-okh", "Hue", 0, 360))}
|
|
1089
|
+
${alphaField()}
|
|
1066
1090
|
</div>`;
|
|
1067
1091
|
break;
|
|
1092
|
+
case "css":
|
|
1093
|
+
html = `<fig-input-text class="fig-fill-picker-ci-css" aria-label="CSS color" placeholder="rgba(0, 0, 0, 1)"></fig-input-text>`;
|
|
1094
|
+
break;
|
|
1068
1095
|
default: // hex
|
|
1069
|
-
html =
|
|
1096
|
+
html = showAlpha
|
|
1097
|
+
? `<div class="input-combo fig-fill-picker-ci-hex-row">
|
|
1098
|
+
<fig-input-text class="fig-fill-picker-ci-hex" aria-label="Hex color" placeholder="FFFFFF"></fig-input-text>
|
|
1099
|
+
${alphaField()}
|
|
1100
|
+
</div>`
|
|
1101
|
+
: `<fig-input-text class="fig-fill-picker-ci-hex" aria-label="Hex color" placeholder="FFFFFF"></fig-input-text>`;
|
|
1070
1102
|
break;
|
|
1071
1103
|
}
|
|
1072
1104
|
|
|
@@ -1085,12 +1117,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
1085
1117
|
if (this.#isDraggingColor) return;
|
|
1086
1118
|
const color = this.#readColorFromInputs();
|
|
1087
1119
|
if (!color) return;
|
|
1088
|
-
|
|
1120
|
+
const nextAlpha = Number.isFinite(color.a) ? color.a : this.#color.a;
|
|
1121
|
+
this.#color = { ...color, a: nextAlpha };
|
|
1089
1122
|
this.#drawColorArea();
|
|
1090
1123
|
this.#updateHandlePosition();
|
|
1091
1124
|
if (this.#hueSlider) {
|
|
1092
1125
|
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
1093
1126
|
}
|
|
1127
|
+
if (this.#opacitySlider && Number.isFinite(color.a)) {
|
|
1128
|
+
this.#opacitySlider.setAttribute("value", this.#color.a * 100);
|
|
1129
|
+
}
|
|
1094
1130
|
this.#emitInput();
|
|
1095
1131
|
};
|
|
1096
1132
|
|
|
@@ -1105,39 +1141,53 @@ class FigFillPicker extends HTMLElement {
|
|
|
1105
1141
|
});
|
|
1106
1142
|
}
|
|
1107
1143
|
|
|
1144
|
+
#readAlphaFromInput() {
|
|
1145
|
+
const el = this.#dialog?.querySelector(".fig-fill-picker-ci-a");
|
|
1146
|
+
if (!el) return undefined;
|
|
1147
|
+
const pct = parseFloat(el.value);
|
|
1148
|
+
if (!Number.isFinite(pct)) return undefined;
|
|
1149
|
+
return Math.max(0, Math.min(1, pct / 100));
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1108
1152
|
#readColorFromInputs() {
|
|
1109
1153
|
const q = (cls) => this.#dialog?.querySelector(`.${cls}`);
|
|
1110
1154
|
const val = (cls) => parseFloat(q(cls)?.value ?? 0);
|
|
1155
|
+
const withAlpha = (color) => {
|
|
1156
|
+
if (!color) return null;
|
|
1157
|
+
const a = this.#readAlphaFromInput();
|
|
1158
|
+
return { ...color, a: a ?? color.a ?? this.#color.a };
|
|
1159
|
+
};
|
|
1111
1160
|
|
|
1112
1161
|
switch (this.#colorInputMode) {
|
|
1113
1162
|
case "rgb":
|
|
1114
|
-
return
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1163
|
+
return withAlpha(
|
|
1164
|
+
this.#rgbToHSV({
|
|
1165
|
+
r: val("fig-fill-picker-ci-r"),
|
|
1166
|
+
g: val("fig-fill-picker-ci-g"),
|
|
1167
|
+
b: val("fig-fill-picker-ci-b"),
|
|
1168
|
+
}),
|
|
1169
|
+
);
|
|
1119
1170
|
case "hsl": {
|
|
1120
1171
|
const rgb = this.#hslToRGB({
|
|
1121
1172
|
h: val("fig-fill-picker-ci-h"),
|
|
1122
1173
|
s: val("fig-fill-picker-ci-s"),
|
|
1123
1174
|
l: val("fig-fill-picker-ci-l"),
|
|
1124
1175
|
});
|
|
1125
|
-
return this.#rgbToHSV(rgb);
|
|
1176
|
+
return withAlpha(this.#rgbToHSV(rgb));
|
|
1126
1177
|
}
|
|
1127
1178
|
case "hsb":
|
|
1128
|
-
return {
|
|
1179
|
+
return withAlpha({
|
|
1129
1180
|
h: val("fig-fill-picker-ci-h"),
|
|
1130
1181
|
s: val("fig-fill-picker-ci-s"),
|
|
1131
1182
|
v: val("fig-fill-picker-ci-v"),
|
|
1132
|
-
|
|
1133
|
-
};
|
|
1183
|
+
});
|
|
1134
1184
|
case "lab": {
|
|
1135
1185
|
const rgb = this.#oklabToRGB({
|
|
1136
1186
|
l: val("fig-fill-picker-ci-okl") / 100,
|
|
1137
1187
|
a: val("fig-fill-picker-ci-oka"),
|
|
1138
1188
|
b: val("fig-fill-picker-ci-okb"),
|
|
1139
1189
|
});
|
|
1140
|
-
return this.#rgbToHSV(rgb);
|
|
1190
|
+
return withAlpha(this.#rgbToHSV(rgb));
|
|
1141
1191
|
}
|
|
1142
1192
|
case "lch": {
|
|
1143
1193
|
const rgb = this.#oklchToRGB({
|
|
@@ -1145,7 +1195,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
1145
1195
|
c: val("fig-fill-picker-ci-okc"),
|
|
1146
1196
|
h: val("fig-fill-picker-ci-okh"),
|
|
1147
1197
|
});
|
|
1148
|
-
return this.#rgbToHSV(rgb);
|
|
1198
|
+
return withAlpha(this.#rgbToHSV(rgb));
|
|
1199
|
+
}
|
|
1200
|
+
case "css": {
|
|
1201
|
+
const cssEl = q("fig-fill-picker-ci-css");
|
|
1202
|
+
if (!cssEl) return null;
|
|
1203
|
+
return this.#parseCssColor(cssEl.value);
|
|
1149
1204
|
}
|
|
1150
1205
|
default: {
|
|
1151
1206
|
// hex
|
|
@@ -1154,8 +1209,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
1154
1209
|
let hex = hexEl.value.replace(/^#/, "");
|
|
1155
1210
|
if (hex.length === 3)
|
|
1156
1211
|
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1212
|
+
if (hex.length === 8) {
|
|
1213
|
+
const alpha = parseInt(hex.slice(6, 8), 16) / 255;
|
|
1214
|
+
hex = hex.slice(0, 6);
|
|
1215
|
+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
1216
|
+
return { ...this.#hexToHSV(`#${hex}`), a: alpha };
|
|
1217
|
+
}
|
|
1157
1218
|
if (hex.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
1158
|
-
return this.#hexToHSV(`#${hex}`);
|
|
1219
|
+
return withAlpha(this.#hexToHSV(`#${hex}`));
|
|
1159
1220
|
}
|
|
1160
1221
|
}
|
|
1161
1222
|
}
|
|
@@ -1203,11 +1264,22 @@ class FigFillPicker extends HTMLElement {
|
|
|
1203
1264
|
set("fig-fill-picker-ci-okh", Math.round(lch.h));
|
|
1204
1265
|
break;
|
|
1205
1266
|
}
|
|
1267
|
+
case "css":
|
|
1268
|
+
set("fig-fill-picker-ci-css", this.#formatCssColor(this.#color));
|
|
1269
|
+
break;
|
|
1206
1270
|
default: // hex
|
|
1207
1271
|
set("fig-fill-picker-ci-hex", hex.replace(/^#/, "").toUpperCase());
|
|
1208
1272
|
break;
|
|
1209
1273
|
}
|
|
1210
1274
|
|
|
1275
|
+
if (this.#colorInputMode !== "css") {
|
|
1276
|
+
const alphaPct = Math.round(this.#color.a * 1000) / 10;
|
|
1277
|
+
set(
|
|
1278
|
+
"fig-fill-picker-ci-a",
|
|
1279
|
+
Number.isInteger(alphaPct) ? alphaPct : +alphaPct.toFixed(1),
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1211
1283
|
if (this.#opacitySlider) {
|
|
1212
1284
|
this.#opacitySlider.setAttribute("color", hex);
|
|
1213
1285
|
}
|
|
@@ -1218,19 +1290,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
1218
1290
|
// ============ GRADIENT TAB ============
|
|
1219
1291
|
#initGradientTab() {
|
|
1220
1292
|
const container = this.#dialog.querySelector('[data-tab="gradient"]');
|
|
1221
|
-
const
|
|
1222
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
1293
|
+
const interpolationValue = gradientInterpolationSelectValue(this.#gradient);
|
|
1223
1294
|
|
|
1224
1295
|
container.innerHTML = `
|
|
1225
1296
|
<fig-field class="fig-fill-picker-gradient-header">
|
|
1226
|
-
<fig-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
</fig-
|
|
1233
|
-
<fig-tooltip text="
|
|
1297
|
+
<fig-select class="fig-fill-picker-gradient-type" label="Gradient type" value="${figEditorEscapeAttribute(this.#gradient.type)}">
|
|
1298
|
+
<fig-select-options>
|
|
1299
|
+
<fig-select-option value="linear">Linear</fig-select-option>
|
|
1300
|
+
<fig-select-option value="radial">Radial</fig-select-option>
|
|
1301
|
+
<fig-select-option value="angular">Angular</fig-select-option>
|
|
1302
|
+
</fig-select-options>
|
|
1303
|
+
</fig-select>
|
|
1304
|
+
<fig-tooltip text="Gradient angle">
|
|
1234
1305
|
<fig-input-number class="fig-fill-picker-gradient-angle" aria-label="Gradient angle" value="${
|
|
1235
1306
|
(this.#gradient.angle - 90 + 360) % 360
|
|
1236
1307
|
}" min="0" max="360" units="°" wrap></fig-input-number>
|
|
@@ -1243,11 +1314,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
1243
1314
|
this.#gradient.centerY
|
|
1244
1315
|
}" units="%" class="fig-fill-picker-gradient-cy"></fig-input-number>
|
|
1245
1316
|
</div>
|
|
1246
|
-
<
|
|
1247
|
-
<fig-
|
|
1248
|
-
<fig-icon
|
|
1249
|
-
|
|
1250
|
-
|
|
1317
|
+
<div class="fig-fill-picker-gradient-actions">
|
|
1318
|
+
<fig-tooltip text="Flip gradient">
|
|
1319
|
+
<fig-button icon variant="ghost" class="fig-fill-picker-gradient-flip" aria-label="Flip gradient">
|
|
1320
|
+
<fig-icon name="swap"></fig-icon>
|
|
1321
|
+
</fig-button>
|
|
1322
|
+
</fig-tooltip>
|
|
1323
|
+
<fig-tooltip text="Rotate gradient">
|
|
1324
|
+
<fig-button icon variant="ghost" class="fig-fill-picker-gradient-rotate" aria-label="Rotate gradient">
|
|
1325
|
+
<fig-icon name="rotate"></fig-icon>
|
|
1326
|
+
</fig-button>
|
|
1327
|
+
</fig-tooltip>
|
|
1328
|
+
</div>
|
|
1251
1329
|
</fig-field>
|
|
1252
1330
|
<fig-preview class="fig-fill-picker-gradient-preview">
|
|
1253
1331
|
<fig-input-gradient class="fig-fill-picker-gradient-bar-input" aria-label="Gradient stops" edit="true" mode="tip" size="large" value='${JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) })}'></fig-input-gradient>
|
|
@@ -1268,25 +1346,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
1268
1346
|
<span>Color interpolation</span>
|
|
1269
1347
|
</fig-header>
|
|
1270
1348
|
<fig-field class="fig-fill-picker-gradient-interpolation-field">
|
|
1271
|
-
<fig-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
<optgroup label="sRGB">
|
|
1277
|
-
<option value="srgb">Classic</option>
|
|
1278
|
-
<option value="srgb-linear">Linear</option>
|
|
1279
|
-
</optgroup>
|
|
1280
|
-
<optgroup label="OKLab">
|
|
1281
|
-
<option value="oklab">Perceptual</option>
|
|
1282
|
-
</optgroup>
|
|
1283
|
-
<optgroup label="OKLCH">
|
|
1284
|
-
<option value="oklch-shorter">Shorter hue</option>
|
|
1285
|
-
<option value="oklch-longer">Longer hue</option>
|
|
1286
|
-
<option value="oklch-increasing">Increasing hue</option>
|
|
1287
|
-
<option value="oklch-decreasing">Decreasing hue</option>
|
|
1288
|
-
</optgroup>
|
|
1289
|
-
</fig-dropdown>
|
|
1349
|
+
<fig-select class="fig-fill-picker-gradient-space" label="Color interpolation" full value="${figEditorEscapeAttribute(interpolationValue)}">
|
|
1350
|
+
<fig-select-options>
|
|
1351
|
+
${this.#gradientInterpolationOptionsMarkup()}
|
|
1352
|
+
</fig-select-options>
|
|
1353
|
+
</fig-select>
|
|
1290
1354
|
</fig-field>
|
|
1291
1355
|
</div>
|
|
1292
1356
|
`;
|
|
@@ -1295,44 +1359,141 @@ class FigFillPicker extends HTMLElement {
|
|
|
1295
1359
|
this.#setupGradientEvents(container);
|
|
1296
1360
|
}
|
|
1297
1361
|
|
|
1362
|
+
#gradientInterpolationOptionsMarkup() {
|
|
1363
|
+
const hueMethods = ["shorter", "longer", "increasing", "decreasing"];
|
|
1364
|
+
const groups = [
|
|
1365
|
+
{
|
|
1366
|
+
label: "Linear",
|
|
1367
|
+
options: [
|
|
1368
|
+
{ value: "srgb", label: "sRGB" },
|
|
1369
|
+
],
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
label: "",
|
|
1373
|
+
options: [{ value: "oklab", label: "OKLAB" }],
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
label: "Polar",
|
|
1377
|
+
options: hueMethods.map((method) => ({
|
|
1378
|
+
value: `oklch-${method}`,
|
|
1379
|
+
label: `OKLCH ${method.charAt(0).toUpperCase()}${method.slice(1)}`,
|
|
1380
|
+
})),
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
label: "",
|
|
1384
|
+
separator: true,
|
|
1385
|
+
options: hueMethods.map((method) => ({
|
|
1386
|
+
value: `hsl-${method}`,
|
|
1387
|
+
label: `HSL ${method.charAt(0).toUpperCase()}${method.slice(1)}`,
|
|
1388
|
+
})),
|
|
1389
|
+
},
|
|
1390
|
+
];
|
|
1391
|
+
return groups
|
|
1392
|
+
.map((group) => {
|
|
1393
|
+
const options = group.options
|
|
1394
|
+
.map((opt) => {
|
|
1395
|
+
const methodLabel = opt.method
|
|
1396
|
+
? opt.method.charAt(0).toUpperCase() + opt.method.slice(1)
|
|
1397
|
+
: "";
|
|
1398
|
+
const appendLabel = opt.append || methodLabel;
|
|
1399
|
+
return `<fig-select-option value="${figEditorEscapeAttribute(opt.value)}" label="${figEditorEscapeAttribute(opt.label)}">
|
|
1400
|
+
<fig-interpolation-swatch slot="prepend" size="large" aria-hidden="true"></fig-interpolation-swatch>
|
|
1401
|
+
${figEditorEscapeAttribute(opt.label)}
|
|
1402
|
+
${appendLabel ? `<span slot="append">${figEditorEscapeAttribute(appendLabel)}</span>` : ""}
|
|
1403
|
+
</fig-select-option>`;
|
|
1404
|
+
})
|
|
1405
|
+
.join("");
|
|
1406
|
+
const separator = group.label || group.separator
|
|
1407
|
+
? `<fig-menu-separator${group.label ? ` label="${figEditorEscapeAttribute(group.label)}"` : ""}></fig-menu-separator>`
|
|
1408
|
+
: "";
|
|
1409
|
+
return `${separator}${options}`;
|
|
1410
|
+
})
|
|
1411
|
+
.join("");
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1298
1414
|
#setupGradientEvents(container) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1415
|
+
const getSelectValue = (event) =>
|
|
1416
|
+
typeof event.detail === "string" ? event.detail : event.target?.value;
|
|
1417
|
+
const gradientBarInput = container.querySelector(
|
|
1418
|
+
".fig-fill-picker-gradient-bar-input",
|
|
1302
1419
|
);
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1420
|
+
const setGradientBarPreview = (gradient) => {
|
|
1421
|
+
if (!gradientBarInput) return;
|
|
1422
|
+
this.#syncingGradientBar = true;
|
|
1423
|
+
try {
|
|
1424
|
+
gradientBarInput.setAttribute(
|
|
1425
|
+
"value",
|
|
1426
|
+
JSON.stringify({
|
|
1427
|
+
type: "gradient",
|
|
1428
|
+
gradient: gradientToValueShape(gradient),
|
|
1429
|
+
}),
|
|
1430
|
+
);
|
|
1431
|
+
} finally {
|
|
1432
|
+
this.#syncingGradientBar = false;
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
const restoreGradientBarPreview = () => {
|
|
1436
|
+
setGradientBarPreview(this.#gradient);
|
|
1437
|
+
};
|
|
1305
1438
|
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1439
|
+
const typeSelect = container.querySelector(".fig-fill-picker-gradient-type");
|
|
1440
|
+
typeSelect?.addEventListener("change", (e) => {
|
|
1441
|
+
const next = getSelectValue(e);
|
|
1442
|
+
if (!next) return;
|
|
1443
|
+
this.#gradient.type = next;
|
|
1308
1444
|
this.#updateGradientUI();
|
|
1309
1445
|
this.#emitInput();
|
|
1310
|
-
};
|
|
1311
|
-
typeDropdown.addEventListener("change", handleTypeChange);
|
|
1446
|
+
});
|
|
1312
1447
|
|
|
1313
|
-
const
|
|
1448
|
+
const interpolationSelect = container.querySelector(
|
|
1314
1449
|
".fig-fill-picker-gradient-space",
|
|
1315
1450
|
);
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1451
|
+
interpolationSelect
|
|
1452
|
+
?.querySelectorAll("fig-select-option")
|
|
1453
|
+
.forEach((option) => {
|
|
1454
|
+
const previewOption = () => {
|
|
1455
|
+
const parsed = parseGradientInterpolationSelectValue(
|
|
1456
|
+
option.getAttribute("value") || "srgb",
|
|
1457
|
+
);
|
|
1458
|
+
setGradientBarPreview(
|
|
1459
|
+
normalizeGradientConfig({
|
|
1460
|
+
...this.#gradient,
|
|
1461
|
+
...parsed,
|
|
1462
|
+
}),
|
|
1463
|
+
);
|
|
1464
|
+
};
|
|
1465
|
+
option.addEventListener("pointerenter", previewOption);
|
|
1466
|
+
option.addEventListener("pointerleave", () => {
|
|
1467
|
+
if (document.activeElement !== option) restoreGradientBarPreview();
|
|
1468
|
+
});
|
|
1469
|
+
option.addEventListener("focus", previewOption);
|
|
1470
|
+
option.addEventListener("blur", () => {
|
|
1471
|
+
if (!option.matches(":hover")) restoreGradientBarPreview();
|
|
1472
|
+
});
|
|
1473
|
+
});
|
|
1474
|
+
this.#gradientInterpolationOpenObserver?.disconnect();
|
|
1475
|
+
if (interpolationSelect) {
|
|
1476
|
+
this.#gradientInterpolationOpenObserver = new MutationObserver(() => {
|
|
1477
|
+
if (!interpolationSelect.hasAttribute("open")) {
|
|
1478
|
+
restoreGradientBarPreview();
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
this.#gradientInterpolationOpenObserver.observe(interpolationSelect, {
|
|
1482
|
+
attributes: true,
|
|
1483
|
+
attributeFilter: ["open"],
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
interpolationSelect?.addEventListener("change", (e) => {
|
|
1487
|
+
const val = getSelectValue(e);
|
|
1488
|
+
if (!val) return;
|
|
1489
|
+
const parsed = parseGradientInterpolationSelectValue(val);
|
|
1324
1490
|
this.#gradient = normalizeGradientConfig({
|
|
1325
1491
|
...this.#gradient,
|
|
1326
|
-
|
|
1327
|
-
hueInterpolation: hue,
|
|
1492
|
+
...parsed,
|
|
1328
1493
|
});
|
|
1329
1494
|
this.#updateGradientUI();
|
|
1330
1495
|
this.#emitInput();
|
|
1331
|
-
};
|
|
1332
|
-
interpolationDropdown?.addEventListener(
|
|
1333
|
-
"change",
|
|
1334
|
-
handleInterpolationChange,
|
|
1335
|
-
);
|
|
1496
|
+
});
|
|
1336
1497
|
|
|
1337
1498
|
// Angle input
|
|
1338
1499
|
const angleInput = container.querySelector(
|
|
@@ -1361,6 +1522,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
1361
1522
|
this.#emitInput();
|
|
1362
1523
|
});
|
|
1363
1524
|
|
|
1525
|
+
// Rotate 90° clockwise
|
|
1526
|
+
container
|
|
1527
|
+
.querySelector(".fig-fill-picker-gradient-rotate")
|
|
1528
|
+
?.addEventListener("click", () => {
|
|
1529
|
+
this.#gradient.angle = (Number(this.#gradient.angle) + 90) % 360;
|
|
1530
|
+
this.#updateGradientUI();
|
|
1531
|
+
this.#emitInput();
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1364
1534
|
// Flip button
|
|
1365
1535
|
container
|
|
1366
1536
|
.querySelector(".fig-fill-picker-gradient-flip")
|
|
@@ -1389,9 +1559,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
1389
1559
|
});
|
|
1390
1560
|
|
|
1391
1561
|
// Embedded gradient bar input
|
|
1392
|
-
const gradientBarInput = container.querySelector(
|
|
1393
|
-
".fig-fill-picker-gradient-bar-input",
|
|
1394
|
-
);
|
|
1395
1562
|
if (gradientBarInput) {
|
|
1396
1563
|
const syncFromBarInput = (e) => {
|
|
1397
1564
|
e.stopPropagation();
|
|
@@ -1403,6 +1570,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1403
1570
|
...detail.gradient,
|
|
1404
1571
|
});
|
|
1405
1572
|
this.#updateSwatch();
|
|
1573
|
+
this.#updateGradientInterpolationSwatches();
|
|
1406
1574
|
this.#updateGradientStopsList();
|
|
1407
1575
|
};
|
|
1408
1576
|
gradientBarInput.addEventListener("input", (e) => {
|
|
@@ -1555,6 +1723,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1555
1723
|
|
|
1556
1724
|
this.#syncingGradientBar = true;
|
|
1557
1725
|
try {
|
|
1726
|
+
this.#updateGradientInterpolationSwatches();
|
|
1558
1727
|
this.#updateGradientPreview();
|
|
1559
1728
|
this.#emitInput();
|
|
1560
1729
|
} finally {
|
|
@@ -1573,35 +1742,79 @@ class FigFillPicker extends HTMLElement {
|
|
|
1573
1742
|
const angleInput = container.querySelector(
|
|
1574
1743
|
".fig-fill-picker-gradient-angle",
|
|
1575
1744
|
);
|
|
1745
|
+
const rotateBtn = container.querySelector(
|
|
1746
|
+
".fig-fill-picker-gradient-rotate",
|
|
1747
|
+
);
|
|
1576
1748
|
const centerInputs = container.querySelector(
|
|
1577
1749
|
".fig-fill-picker-gradient-center",
|
|
1578
1750
|
);
|
|
1579
1751
|
|
|
1580
1752
|
if (this.#gradient.type === "radial") {
|
|
1581
1753
|
angleInput.style.display = "none";
|
|
1754
|
+
if (rotateBtn) rotateBtn.style.display = "none";
|
|
1582
1755
|
centerInputs.style.display = "flex";
|
|
1583
1756
|
} else {
|
|
1584
|
-
angleInput.style.display
|
|
1757
|
+
angleInput.style.removeProperty("display");
|
|
1758
|
+
rotateBtn?.style.removeProperty("display");
|
|
1585
1759
|
centerInputs.style.display = "none";
|
|
1586
1760
|
// Sync angle input value (convert CSS angle to picker angle)
|
|
1587
1761
|
const pickerAngle = (this.#gradient.angle - 90 + 360) % 360;
|
|
1588
1762
|
angleInput.setAttribute("value", pickerAngle);
|
|
1589
1763
|
}
|
|
1590
1764
|
|
|
1591
|
-
const
|
|
1765
|
+
const interpolationSelect = container.querySelector(
|
|
1592
1766
|
".fig-fill-picker-gradient-space",
|
|
1593
1767
|
);
|
|
1594
|
-
if (
|
|
1595
|
-
|
|
1596
|
-
this.#gradient
|
|
1597
|
-
|
|
1598
|
-
: this.#gradient.interpolationSpace;
|
|
1768
|
+
if (interpolationSelect) {
|
|
1769
|
+
interpolationSelect.value = gradientInterpolationSelectValue(
|
|
1770
|
+
this.#gradient,
|
|
1771
|
+
);
|
|
1599
1772
|
}
|
|
1600
1773
|
|
|
1774
|
+
this.#updateGradientInterpolationSwatches();
|
|
1601
1775
|
this.#updateGradientPreview();
|
|
1602
1776
|
this.#updateGradientStopsList();
|
|
1603
1777
|
}
|
|
1604
1778
|
|
|
1779
|
+
#interpolationPreviewStops() {
|
|
1780
|
+
const stops = Array.isArray(this.#gradient.stops)
|
|
1781
|
+
? [...this.#gradient.stops].sort(
|
|
1782
|
+
(a, b) => (a.position ?? 0) - (b.position ?? 0),
|
|
1783
|
+
)
|
|
1784
|
+
: [];
|
|
1785
|
+
if (stops.length < 2) {
|
|
1786
|
+
return [
|
|
1787
|
+
{ color: "#FF0000", position: 0 },
|
|
1788
|
+
{ color: "#4F9EFF", position: 100 },
|
|
1789
|
+
];
|
|
1790
|
+
}
|
|
1791
|
+
return stops.map((stop) => ({
|
|
1792
|
+
color: String(stop.color || "#D9D9D9").replace(/^(#(?:[0-9a-f]{6})).*/i, "$1"),
|
|
1793
|
+
position: stop.position ?? 0,
|
|
1794
|
+
}));
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
#updateGradientInterpolationSwatches() {
|
|
1798
|
+
if (!this.#dialog) return;
|
|
1799
|
+
const stops = this.#interpolationPreviewStops();
|
|
1800
|
+
this.#dialog
|
|
1801
|
+
.querySelectorAll("fig-interpolation-swatch")
|
|
1802
|
+
.forEach((swatch) => {
|
|
1803
|
+
const optionVal =
|
|
1804
|
+
swatch.closest("fig-select-option")?.getAttribute("value") || "srgb";
|
|
1805
|
+
const parsed = parseGradientInterpolationSelectValue(optionVal);
|
|
1806
|
+
const gradient = {
|
|
1807
|
+
type: "linear",
|
|
1808
|
+
stops,
|
|
1809
|
+
interpolationSpace: parsed.interpolationSpace,
|
|
1810
|
+
};
|
|
1811
|
+
if (GRADIENT_HUE_SPACES.has(parsed.interpolationSpace)) {
|
|
1812
|
+
gradient.hueInterpolation = parsed.hueInterpolation;
|
|
1813
|
+
}
|
|
1814
|
+
swatch.value = { type: "gradient", gradient };
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1605
1818
|
#updateGradientPreview() {
|
|
1606
1819
|
if (!this.#dialog) return;
|
|
1607
1820
|
|
|
@@ -1690,6 +1903,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1690
1903
|
.querySelector(".fig-fill-picker-stop-position")
|
|
1691
1904
|
.addEventListener("input", () => {
|
|
1692
1905
|
this.#syncGradientStopRow(row);
|
|
1906
|
+
this.#updateGradientInterpolationSwatches();
|
|
1693
1907
|
this.#updateGradientPreview();
|
|
1694
1908
|
this.#emitInput();
|
|
1695
1909
|
});
|
|
@@ -1709,6 +1923,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1709
1923
|
this.#syncGradientStopRow(row);
|
|
1710
1924
|
this.#syncingGradientBar = true;
|
|
1711
1925
|
try {
|
|
1926
|
+
this.#updateGradientInterpolationSwatches();
|
|
1712
1927
|
this.#updateGradientPreview();
|
|
1713
1928
|
this.#emitInput();
|
|
1714
1929
|
} finally {
|
|
@@ -1787,19 +2002,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
1787
2002
|
// ============ IMAGE TAB ============
|
|
1788
2003
|
#initImageTab() {
|
|
1789
2004
|
const container = this.#dialog.querySelector('[data-tab="image"]');
|
|
1790
|
-
const experimental = this.getAttribute("experimental");
|
|
1791
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
1792
2005
|
|
|
1793
2006
|
container.innerHTML = `
|
|
1794
2007
|
<fig-field class="fig-fill-picker-media-header">
|
|
1795
|
-
<fig-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
</fig-
|
|
2008
|
+
<fig-select class="fig-fill-picker-scale-mode" label="Image scale mode" value="${figEditorEscapeAttribute(this.#image.scaleMode)}">
|
|
2009
|
+
<fig-select-options>
|
|
2010
|
+
<fig-select-option value="fill">Fill</fig-select-option>
|
|
2011
|
+
<fig-select-option value="fit">Fit</fig-select-option>
|
|
2012
|
+
<fig-select-option value="crop">Crop</fig-select-option>
|
|
2013
|
+
<fig-select-option value="tile">Tile</fig-select-option>
|
|
2014
|
+
</fig-select-options>
|
|
2015
|
+
</fig-select>
|
|
1803
2016
|
<fig-input-number class="fig-fill-picker-scale" aria-label="Image tile scale" min="1" max="200" value="${
|
|
1804
2017
|
this.#image.scale
|
|
1805
2018
|
}" units="%" ${
|
|
@@ -1816,15 +2029,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
1816
2029
|
}
|
|
1817
2030
|
|
|
1818
2031
|
#setupImageEvents(container) {
|
|
1819
|
-
const
|
|
2032
|
+
const scaleModeSelect = container.querySelector(
|
|
1820
2033
|
".fig-fill-picker-scale-mode",
|
|
1821
2034
|
);
|
|
1822
2035
|
const scaleInput = container.querySelector(".fig-fill-picker-scale");
|
|
1823
2036
|
const preview = container.querySelector(".fig-fill-picker-image-preview");
|
|
1824
2037
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2038
|
+
scaleModeSelect.addEventListener("change", (e) => {
|
|
2039
|
+
const next =
|
|
2040
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
2041
|
+
if (!next) return;
|
|
2042
|
+
this.#image.scaleMode = next;
|
|
2043
|
+
scaleInput.style.display = next === "tile" ? "block" : "none";
|
|
1828
2044
|
this.#updateImagePreview(preview);
|
|
1829
2045
|
this.#updateSwatch();
|
|
1830
2046
|
this.#emitInput();
|
|
@@ -1955,18 +2171,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
1955
2171
|
// ============ VIDEO TAB ============
|
|
1956
2172
|
#initVideoTab() {
|
|
1957
2173
|
const container = this.#dialog.querySelector('[data-tab="video"]');
|
|
1958
|
-
const experimental = this.getAttribute("experimental");
|
|
1959
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
1960
2174
|
|
|
1961
2175
|
container.innerHTML = `
|
|
1962
2176
|
<fig-field class="fig-fill-picker-media-header">
|
|
1963
|
-
<fig-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
</fig-
|
|
2177
|
+
<fig-select class="fig-fill-picker-scale-mode" label="Video scale mode" value="${figEditorEscapeAttribute(this.#video.scaleMode)}">
|
|
2178
|
+
<fig-select-options>
|
|
2179
|
+
<fig-select-option value="fill">Fill</fig-select-option>
|
|
2180
|
+
<fig-select-option value="fit">Fit</fig-select-option>
|
|
2181
|
+
<fig-select-option value="crop">Crop</fig-select-option>
|
|
2182
|
+
</fig-select-options>
|
|
2183
|
+
</fig-select>
|
|
1970
2184
|
<fig-button class="fig-fill-picker-media-rotate" icon variant="ghost" aria-label="Rotate">
|
|
1971
2185
|
<fig-icon name="rotate"></fig-icon>
|
|
1972
2186
|
</fig-button>
|
|
@@ -1978,13 +2192,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
1978
2192
|
}
|
|
1979
2193
|
|
|
1980
2194
|
#setupVideoEvents(container) {
|
|
1981
|
-
const
|
|
2195
|
+
const scaleModeSelect = container.querySelector(
|
|
1982
2196
|
".fig-fill-picker-scale-mode",
|
|
1983
2197
|
);
|
|
1984
2198
|
const preview = container.querySelector(".fig-fill-picker-video-preview");
|
|
1985
2199
|
|
|
1986
|
-
|
|
1987
|
-
|
|
2200
|
+
scaleModeSelect.addEventListener("change", (e) => {
|
|
2201
|
+
const next =
|
|
2202
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
2203
|
+
if (!next) return;
|
|
2204
|
+
this.#video.scaleMode = next;
|
|
1988
2205
|
this.#updateVideoPreviewStyle(preview);
|
|
1989
2206
|
this.#updateSwatch();
|
|
1990
2207
|
this.#emitInput();
|
|
@@ -2014,13 +2231,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
2014
2231
|
// ============ WEBCAM TAB ============
|
|
2015
2232
|
#initWebcamTab() {
|
|
2016
2233
|
const container = this.#dialog.querySelector('[data-tab="webcam"]');
|
|
2017
|
-
const experimental = this.getAttribute("experimental");
|
|
2018
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
2019
2234
|
|
|
2020
2235
|
container.innerHTML = `
|
|
2021
2236
|
<fig-field class="fig-fill-picker-webcam-camera" style="display: none;">
|
|
2022
|
-
<fig-
|
|
2023
|
-
|
|
2237
|
+
<fig-select class="fig-fill-picker-camera-select" label="Camera" full>
|
|
2238
|
+
<fig-select-options></fig-select-options>
|
|
2239
|
+
</fig-select>
|
|
2024
2240
|
</fig-field>
|
|
2025
2241
|
<fig-video class="fig-fill-picker-webcam-preview" aria-label="Webcam preview" aspect-ratio="1/1" fit="cover" checkerboard="true" autoplay="true" muted="true">
|
|
2026
2242
|
<video class="fig-fill-picker-webcam-video" autoplay muted playsinline></video>
|
|
@@ -2081,11 +2297,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
2081
2297
|
|
|
2082
2298
|
if (cameras.length > 1) {
|
|
2083
2299
|
cameraField.style.display = "";
|
|
2084
|
-
cameraSelect
|
|
2085
|
-
|
|
2086
|
-
|
|
2300
|
+
let panel = cameraSelect.querySelector(":scope > fig-select-options");
|
|
2301
|
+
if (!panel) {
|
|
2302
|
+
panel = document.createElement("fig-select-options");
|
|
2303
|
+
cameraSelect.append(panel);
|
|
2304
|
+
}
|
|
2305
|
+
panel.replaceChildren();
|
|
2087
2306
|
cameras.forEach((cam, i) => {
|
|
2088
|
-
const option = document.createElement("option");
|
|
2307
|
+
const option = document.createElement("fig-select-option");
|
|
2089
2308
|
option.value = cam.deviceId;
|
|
2090
2309
|
const label =
|
|
2091
2310
|
cam.label || (cameras.length > 1 ? `Camera ${i + 1}` : "Camera");
|
|
@@ -2098,14 +2317,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
2098
2317
|
return ` ${displayId}`;
|
|
2099
2318
|
},
|
|
2100
2319
|
);
|
|
2101
|
-
|
|
2320
|
+
panel.append(option);
|
|
2102
2321
|
});
|
|
2103
2322
|
if (deviceId) cameraSelect.value = deviceId;
|
|
2104
2323
|
} else {
|
|
2105
2324
|
cameraField.style.display = "none";
|
|
2106
2325
|
cameraSelect
|
|
2107
|
-
.
|
|
2108
|
-
|
|
2326
|
+
.querySelector(":scope > fig-select-options")
|
|
2327
|
+
?.replaceChildren();
|
|
2109
2328
|
}
|
|
2110
2329
|
} catch (err) {
|
|
2111
2330
|
if (requestId !== this.#webcamRequestId) return;
|
|
@@ -2133,7 +2352,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
2133
2352
|
this.#webcamStart = startWebcam;
|
|
2134
2353
|
|
|
2135
2354
|
cameraSelect.addEventListener("change", (e) => {
|
|
2136
|
-
|
|
2355
|
+
const next =
|
|
2356
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
2357
|
+
if (next) startWebcam(next);
|
|
2137
2358
|
});
|
|
2138
2359
|
|
|
2139
2360
|
captureBtn.addEventListener("click", async () => {
|
|
@@ -2287,6 +2508,55 @@ class FigFillPicker extends HTMLElement {
|
|
|
2287
2508
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
2288
2509
|
}
|
|
2289
2510
|
|
|
2511
|
+
#formatCssAlpha(alpha) {
|
|
2512
|
+
const a = Math.max(0, Math.min(1, Number(alpha) || 0));
|
|
2513
|
+
const rounded = Math.round(a * 1000) / 1000;
|
|
2514
|
+
return String(rounded);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
#formatCssColor(color = this.#color) {
|
|
2518
|
+
const rgb = this.#hsvToRGB(color);
|
|
2519
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${this.#formatCssAlpha(color.a)})`;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/** Parse CSS color strings (rgba/rgb/hex) into HSV(+alpha). */
|
|
2523
|
+
#parseCssColor(raw) {
|
|
2524
|
+
const value = String(raw ?? "").trim();
|
|
2525
|
+
if (!value) return null;
|
|
2526
|
+
|
|
2527
|
+
const hexMatch = value.match(/^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
|
|
2528
|
+
if (hexMatch) {
|
|
2529
|
+
let hex = hexMatch[1];
|
|
2530
|
+
if (hex.length === 3) {
|
|
2531
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
2532
|
+
}
|
|
2533
|
+
let alpha = 1;
|
|
2534
|
+
if (hex.length === 8) {
|
|
2535
|
+
alpha = parseInt(hex.slice(6, 8), 16) / 255;
|
|
2536
|
+
hex = hex.slice(0, 6);
|
|
2537
|
+
}
|
|
2538
|
+
const hsv = this.#hexToHSV(`#${hex}`);
|
|
2539
|
+
return { ...hsv, a: alpha };
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
const rgbMatch = value.match(
|
|
2543
|
+
/^rgba?\(\s*([+-]?(?:\d+\.?\d*|\.\d+))\s*,\s*([+-]?(?:\d+\.?\d*|\.\d+))\s*,\s*([+-]?(?:\d+\.?\d*|\.\d+))(?:\s*,\s*([+-]?(?:\d+\.?\d*|\.\d+))\s*)?\)$/i,
|
|
2544
|
+
);
|
|
2545
|
+
if (rgbMatch) {
|
|
2546
|
+
const r = Math.max(0, Math.min(255, Math.round(parseFloat(rgbMatch[1]))));
|
|
2547
|
+
const g = Math.max(0, Math.min(255, Math.round(parseFloat(rgbMatch[2]))));
|
|
2548
|
+
const b = Math.max(0, Math.min(255, Math.round(parseFloat(rgbMatch[3]))));
|
|
2549
|
+
const alpha =
|
|
2550
|
+
rgbMatch[4] !== undefined
|
|
2551
|
+
? Math.max(0, Math.min(1, parseFloat(rgbMatch[4])))
|
|
2552
|
+
: 1;
|
|
2553
|
+
if (![r, g, b, alpha].every(Number.isFinite)) return null;
|
|
2554
|
+
return { ...this.#rgbToHSV({ r, g, b }), a: alpha };
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
return null;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2290
2560
|
#hexToP3(hex, alpha = 1) {
|
|
2291
2561
|
const r = +(parseInt(hex.slice(1, 3), 16) / 255).toFixed(4);
|
|
2292
2562
|
const g = +(parseInt(hex.slice(3, 5), 16) / 255).toFixed(4);
|