@rogieking/figui3 6.20.2 → 6.21.1
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 +115 -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 +1 -0
- package/fig-editor.css +157 -17
- package/fig-editor.js +464 -188
- package/fig-lab.css +51 -42
- package/fig-lab.js +145 -23
- package/fig.js +577 -68
- 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
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import "./fig.js";
|
|
2
2
|
import "./fig-lab.js";
|
|
3
3
|
|
|
4
|
+
function figEditorDefineElement(name, constructor) {
|
|
5
|
+
if (!customElements.get(name)) {
|
|
6
|
+
customElements.define(name, constructor);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
function figEditorEscapeAttribute(value) {
|
|
5
11
|
return String(value ?? "")
|
|
6
12
|
.replace(/&/g, "&")
|
|
@@ -17,6 +23,7 @@ const GRADIENT_INTERPOLATION_SPACES = [
|
|
|
17
23
|
"display-p3",
|
|
18
24
|
"oklab",
|
|
19
25
|
"oklch",
|
|
26
|
+
"hsl",
|
|
20
27
|
];
|
|
21
28
|
const GRADIENT_HUE_INTERPOLATIONS = [
|
|
22
29
|
"shorter",
|
|
@@ -24,6 +31,7 @@ const GRADIENT_HUE_INTERPOLATIONS = [
|
|
|
24
31
|
"increasing",
|
|
25
32
|
"decreasing",
|
|
26
33
|
];
|
|
34
|
+
const GRADIENT_HUE_SPACES = new Set(["oklch", "hsl"]);
|
|
27
35
|
|
|
28
36
|
function normalizeGradientConfig(gradient) {
|
|
29
37
|
const next = { ...(gradient ?? {}) };
|
|
@@ -50,7 +58,7 @@ function gradientToValueShape(gradient) {
|
|
|
50
58
|
...normalized,
|
|
51
59
|
interpolationSpace: normalized.interpolationSpace,
|
|
52
60
|
};
|
|
53
|
-
if (normalized.interpolationSpace
|
|
61
|
+
if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
|
|
54
62
|
output.hueInterpolation = normalized.hueInterpolation;
|
|
55
63
|
} else {
|
|
56
64
|
delete output.hueInterpolation;
|
|
@@ -63,12 +71,37 @@ function gradientInterpolationClause(gradient) {
|
|
|
63
71
|
if (normalized.interpolationSpace === "srgb") {
|
|
64
72
|
return "";
|
|
65
73
|
}
|
|
66
|
-
if (normalized.interpolationSpace
|
|
67
|
-
return `in
|
|
74
|
+
if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
|
|
75
|
+
return `in ${normalized.interpolationSpace} ${normalized.hueInterpolation} hue`;
|
|
68
76
|
}
|
|
69
77
|
return `in ${normalized.interpolationSpace}`;
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
function gradientInterpolationSelectValue(gradient) {
|
|
81
|
+
const normalized = normalizeGradientConfig(gradient);
|
|
82
|
+
if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
|
|
83
|
+
return `${normalized.interpolationSpace}-${normalized.hueInterpolation || "shorter"}`;
|
|
84
|
+
}
|
|
85
|
+
return normalized.interpolationSpace;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseGradientInterpolationSelectValue(val) {
|
|
89
|
+
const raw = String(val ?? "");
|
|
90
|
+
for (const space of GRADIENT_HUE_SPACES) {
|
|
91
|
+
const prefix = `${space}-`;
|
|
92
|
+
if (raw.startsWith(prefix)) {
|
|
93
|
+
return {
|
|
94
|
+
interpolationSpace: space,
|
|
95
|
+
hueInterpolation: raw.slice(prefix.length) || "shorter",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
interpolationSpace: raw || "srgb",
|
|
101
|
+
hueInterpolation: "shorter",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
72
105
|
/**
|
|
73
106
|
* A comprehensive fill picker component supporting solid colors, gradients, images, video, and webcam.
|
|
74
107
|
* Uses display: contents and wraps a trigger element that opens a dialog picker.
|
|
@@ -118,6 +151,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
118
151
|
#isDraggingColor = false;
|
|
119
152
|
#syncingGradientBar = false;
|
|
120
153
|
#teardownColorAreaEvents = null;
|
|
154
|
+
#gradientInterpolationOpenObserver = null;
|
|
121
155
|
#valueAtOpen = null;
|
|
122
156
|
#webcamStart = null;
|
|
123
157
|
#webcamRequestId = 0;
|
|
@@ -285,9 +319,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
285
319
|
if (parsed.opacity !== undefined) {
|
|
286
320
|
this.#color.a = parsed.opacity / 100;
|
|
287
321
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
322
|
+
// Gamut UI hidden for now — lock to sRGB.
|
|
323
|
+
this.#gamut = "srgb";
|
|
291
324
|
if (parsed.gradient) {
|
|
292
325
|
this.#gradient = normalizeGradientConfig({
|
|
293
326
|
...this.#gradient,
|
|
@@ -390,9 +423,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
390
423
|
this.#valueAtOpen = JSON.stringify(this.value);
|
|
391
424
|
this.#switchTab(this.#fillType, { emit: false });
|
|
392
425
|
|
|
393
|
-
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
394
|
-
if (gamutEl) gamutEl.value = this.#gamut;
|
|
395
|
-
|
|
396
426
|
if (this.#swatch) this.#swatch.setAttribute("selected", "true");
|
|
397
427
|
|
|
398
428
|
this.#dialog.open = true;
|
|
@@ -429,6 +459,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
429
459
|
this.#teardownColorAreaEvents();
|
|
430
460
|
this.#teardownColorAreaEvents = null;
|
|
431
461
|
}
|
|
462
|
+
this.#gradientInterpolationOpenObserver?.disconnect();
|
|
463
|
+
this.#gradientInterpolationOpenObserver = null;
|
|
432
464
|
this.#stopWebcam();
|
|
433
465
|
if (!this.#dialog) return;
|
|
434
466
|
this.#restoreCustomSlotContent();
|
|
@@ -508,11 +540,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
508
540
|
this.#activeTab = allowedModes[0];
|
|
509
541
|
}
|
|
510
542
|
|
|
511
|
-
const experimental = this.getAttribute("experimental");
|
|
512
|
-
const expAttr = experimental
|
|
513
|
-
? `experimental="${figEditorEscapeAttribute(experimental)}"`
|
|
514
|
-
: "";
|
|
515
|
-
|
|
516
543
|
let headerContent;
|
|
517
544
|
if (allowedModes.length === 1) {
|
|
518
545
|
headerContent = `<h3 class="fig-fill-picker-type-label">${figEditorEscapeAttribute(modeLabels[allowedModes[0]])}</h3>`;
|
|
@@ -520,12 +547,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
520
547
|
const options = allowedModes
|
|
521
548
|
.map(
|
|
522
549
|
(m) =>
|
|
523
|
-
`<option value="${figEditorEscapeAttribute(m)}">${figEditorEscapeAttribute(modeLabels[m])}</option>`,
|
|
550
|
+
`<fig-select-option value="${figEditorEscapeAttribute(m)}">${figEditorEscapeAttribute(modeLabels[m])}</fig-select-option>`,
|
|
524
551
|
)
|
|
525
|
-
.join("\n
|
|
526
|
-
headerContent = `<fig-
|
|
527
|
-
|
|
528
|
-
|
|
552
|
+
.join("\n ");
|
|
553
|
+
headerContent = `<fig-select class="fig-fill-picker-type" label="Fill type" value="${figEditorEscapeAttribute(this.#fillType)}">
|
|
554
|
+
<fig-select-options>
|
|
555
|
+
${options}
|
|
556
|
+
</fig-select-options>
|
|
557
|
+
</fig-select>`;
|
|
529
558
|
}
|
|
530
559
|
|
|
531
560
|
// Generate tab containers for all allowed modes
|
|
@@ -536,15 +565,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
536
565
|
)
|
|
537
566
|
.join("\n ");
|
|
538
567
|
|
|
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
568
|
this.#dialog.innerHTML = `
|
|
545
569
|
<fig-header>
|
|
546
570
|
${headerContent}
|
|
547
|
-
${gamutDropdown}
|
|
548
571
|
<fig-button icon variant="ghost" class="fig-fill-picker-close" aria-label="Close fill picker">
|
|
549
572
|
<fig-icon name="close"></fig-icon>
|
|
550
573
|
</fig-button>
|
|
@@ -577,27 +600,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
577
600
|
);
|
|
578
601
|
}
|
|
579
602
|
|
|
580
|
-
// Setup type
|
|
581
|
-
const
|
|
582
|
-
if (
|
|
583
|
-
|
|
584
|
-
|
|
603
|
+
// Setup type select switching (only if not locked)
|
|
604
|
+
const typeSelect = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
605
|
+
if (typeSelect) {
|
|
606
|
+
typeSelect.addEventListener("change", (e) => {
|
|
607
|
+
const next =
|
|
608
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
609
|
+
if (next) this.#switchTab(next);
|
|
585
610
|
});
|
|
586
611
|
}
|
|
587
612
|
|
|
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
613
|
this.#dialog
|
|
602
614
|
.querySelector(".fig-fill-picker-close")
|
|
603
615
|
.addEventListener("click", () => {
|
|
@@ -666,10 +678,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
666
678
|
this.#stopWebcam();
|
|
667
679
|
}
|
|
668
680
|
|
|
669
|
-
// Update
|
|
670
|
-
const
|
|
671
|
-
if (
|
|
672
|
-
|
|
681
|
+
// Update type select (only exists if not locked)
|
|
682
|
+
const typeSelect = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
683
|
+
if (typeSelect && typeSelect.value !== tabName) {
|
|
684
|
+
typeSelect.value = tabName;
|
|
673
685
|
}
|
|
674
686
|
|
|
675
687
|
// Show/hide tab content
|
|
@@ -711,8 +723,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
711
723
|
#initSolidTab() {
|
|
712
724
|
const container = this.#dialog.querySelector('[data-tab="solid"]');
|
|
713
725
|
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
714
|
-
const experimental = this.getAttribute("experimental");
|
|
715
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
716
726
|
|
|
717
727
|
container.innerHTML = `
|
|
718
728
|
<fig-preview class="fig-fill-picker-color-area">
|
|
@@ -728,28 +738,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
728
738
|
drag-snapping="modifier"
|
|
729
739
|
></fig-handle>
|
|
730
740
|
</fig-preview>
|
|
731
|
-
<div class="fig-fill-picker-sliders">
|
|
741
|
+
<div class="fig-fill-picker-sliders${showAlpha ? "" : " is-hue-only"}">
|
|
732
742
|
<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="${
|
|
743
|
+
<fig-slider type="hue" variant="classic" text="false" min="0" max="360" aria-label="Hue" value="${
|
|
734
744
|
this.#color.h
|
|
735
745
|
}"></fig-slider>
|
|
736
746
|
${
|
|
737
747
|
showAlpha
|
|
738
|
-
? `<fig-slider type="opacity"
|
|
748
|
+
? `<fig-slider type="opacity" variant="classic" text="false" min="0" max="100" aria-label="Opacity" value="${
|
|
739
749
|
this.#color.a * 100
|
|
740
750
|
}" color="${this.#hsvToHex(this.#color)}"></fig-slider>`
|
|
741
751
|
: ""
|
|
742
752
|
}
|
|
743
753
|
</div>
|
|
744
754
|
<fig-field class="fig-fill-picker-inputs">
|
|
745
|
-
<fig-
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
755
|
+
<fig-select class="fig-fill-picker-input-mode" label="Color value format" value="${figEditorEscapeAttribute(this.#colorInputMode)}">
|
|
756
|
+
<fig-select-options>
|
|
757
|
+
<fig-select-option value="hex">Hex</fig-select-option>
|
|
758
|
+
<fig-select-option value="rgb">RGB</fig-select-option>
|
|
759
|
+
<fig-select-option value="css">CSS</fig-select-option>
|
|
760
|
+
<fig-select-option value="hsl">HSL</fig-select-option>
|
|
761
|
+
<fig-select-option value="hsb">HSB</fig-select-option>
|
|
762
|
+
<fig-select-option value="lab">LAB</fig-select-option>
|
|
763
|
+
<fig-select-option value="lch">LCH</fig-select-option>
|
|
764
|
+
</fig-select-options>
|
|
765
|
+
</fig-select>
|
|
753
766
|
<span class="fig-fill-picker-input-fields"></span>
|
|
754
767
|
</fig-field>
|
|
755
768
|
`;
|
|
@@ -789,10 +802,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
789
802
|
});
|
|
790
803
|
}
|
|
791
804
|
|
|
792
|
-
// Setup color input mode
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
805
|
+
// Setup color input mode select
|
|
806
|
+
const modeSelect = container.querySelector(".fig-fill-picker-input-mode");
|
|
807
|
+
modeSelect.addEventListener("change", (e) => {
|
|
808
|
+
const next =
|
|
809
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
810
|
+
if (!next) return;
|
|
811
|
+
this.#colorInputMode = next;
|
|
796
812
|
this.#rebuildColorInputFields();
|
|
797
813
|
});
|
|
798
814
|
|
|
@@ -1025,8 +1041,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
1025
1041
|
const wrap = (tooltip, html) =>
|
|
1026
1042
|
`<fig-tooltip text="${tooltip}">${html}</fig-tooltip>`;
|
|
1027
1043
|
|
|
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>`;
|
|
1044
|
+
const num = (cls, label, min, max, step, units) =>
|
|
1045
|
+
`<fig-input-number class="${cls}" aria-label="${label}" min="${min}" max="${max}"${step != null ? ` step="${step}"` : ""}${units ? ` units="${units}"` : ""}></fig-input-number>`;
|
|
1046
|
+
|
|
1047
|
+
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
1048
|
+
const alphaField = () =>
|
|
1049
|
+
showAlpha
|
|
1050
|
+
? wrap(
|
|
1051
|
+
"Alpha",
|
|
1052
|
+
num("fig-fill-picker-ci-a", "Alpha", 0, 100, 0.1, "%"),
|
|
1053
|
+
)
|
|
1054
|
+
: "";
|
|
1030
1055
|
|
|
1031
1056
|
let html;
|
|
1032
1057
|
switch (this.#colorInputMode) {
|
|
@@ -1035,6 +1060,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1035
1060
|
${wrap("Red", num("fig-fill-picker-ci-r", "Red", 0, 255))}
|
|
1036
1061
|
${wrap("Green", num("fig-fill-picker-ci-g", "Green", 0, 255))}
|
|
1037
1062
|
${wrap("Blue", num("fig-fill-picker-ci-b", "Blue", 0, 255))}
|
|
1063
|
+
${alphaField()}
|
|
1038
1064
|
</div>`;
|
|
1039
1065
|
break;
|
|
1040
1066
|
case "hsl":
|
|
@@ -1042,6 +1068,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1042
1068
|
${wrap("Hue", num("fig-fill-picker-ci-h", "Hue", 0, 360))}
|
|
1043
1069
|
${wrap("Saturation", num("fig-fill-picker-ci-s", "Saturation", 0, 100))}
|
|
1044
1070
|
${wrap("Lightness", num("fig-fill-picker-ci-l", "Lightness", 0, 100))}
|
|
1071
|
+
${alphaField()}
|
|
1045
1072
|
</div>`;
|
|
1046
1073
|
break;
|
|
1047
1074
|
case "hsb":
|
|
@@ -1049,6 +1076,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1049
1076
|
${wrap("Hue", num("fig-fill-picker-ci-h", "Hue", 0, 360))}
|
|
1050
1077
|
${wrap("Saturation", num("fig-fill-picker-ci-s", "Saturation", 0, 100))}
|
|
1051
1078
|
${wrap("Brightness", num("fig-fill-picker-ci-v", "Brightness", 0, 100))}
|
|
1079
|
+
${alphaField()}
|
|
1052
1080
|
</div>`;
|
|
1053
1081
|
break;
|
|
1054
1082
|
case "lab":
|
|
@@ -1056,6 +1084,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1056
1084
|
${wrap("Lightness", num("fig-fill-picker-ci-okl", "Lightness", 0, 100))}
|
|
1057
1085
|
${wrap("Green-Red axis", num("fig-fill-picker-ci-oka", "Green-Red axis", -0.4, 0.4, 0.001))}
|
|
1058
1086
|
${wrap("Blue-Yellow axis", num("fig-fill-picker-ci-okb", "Blue-Yellow axis", -0.4, 0.4, 0.001))}
|
|
1087
|
+
${alphaField()}
|
|
1059
1088
|
</div>`;
|
|
1060
1089
|
break;
|
|
1061
1090
|
case "lch":
|
|
@@ -1063,10 +1092,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
1063
1092
|
${wrap("Lightness", num("fig-fill-picker-ci-okl", "Lightness", 0, 100))}
|
|
1064
1093
|
${wrap("Chroma", num("fig-fill-picker-ci-okc", "Chroma", 0, 0.4, 0.001))}
|
|
1065
1094
|
${wrap("Hue", num("fig-fill-picker-ci-okh", "Hue", 0, 360))}
|
|
1095
|
+
${alphaField()}
|
|
1066
1096
|
</div>`;
|
|
1067
1097
|
break;
|
|
1098
|
+
case "css":
|
|
1099
|
+
html = `<fig-input-text class="fig-fill-picker-ci-css" aria-label="CSS color" placeholder="rgba(0, 0, 0, 1)"></fig-input-text>`;
|
|
1100
|
+
break;
|
|
1068
1101
|
default: // hex
|
|
1069
|
-
html =
|
|
1102
|
+
html = showAlpha
|
|
1103
|
+
? `<div class="input-combo fig-fill-picker-ci-hex-row">
|
|
1104
|
+
<fig-input-text class="fig-fill-picker-ci-hex" aria-label="Hex color" placeholder="FFFFFF"></fig-input-text>
|
|
1105
|
+
${alphaField()}
|
|
1106
|
+
</div>`
|
|
1107
|
+
: `<fig-input-text class="fig-fill-picker-ci-hex" aria-label="Hex color" placeholder="FFFFFF"></fig-input-text>`;
|
|
1070
1108
|
break;
|
|
1071
1109
|
}
|
|
1072
1110
|
|
|
@@ -1085,12 +1123,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
1085
1123
|
if (this.#isDraggingColor) return;
|
|
1086
1124
|
const color = this.#readColorFromInputs();
|
|
1087
1125
|
if (!color) return;
|
|
1088
|
-
|
|
1126
|
+
const nextAlpha = Number.isFinite(color.a) ? color.a : this.#color.a;
|
|
1127
|
+
this.#color = { ...color, a: nextAlpha };
|
|
1089
1128
|
this.#drawColorArea();
|
|
1090
1129
|
this.#updateHandlePosition();
|
|
1091
1130
|
if (this.#hueSlider) {
|
|
1092
1131
|
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
1093
1132
|
}
|
|
1133
|
+
if (this.#opacitySlider && Number.isFinite(color.a)) {
|
|
1134
|
+
this.#opacitySlider.setAttribute("value", this.#color.a * 100);
|
|
1135
|
+
}
|
|
1094
1136
|
this.#emitInput();
|
|
1095
1137
|
};
|
|
1096
1138
|
|
|
@@ -1105,39 +1147,53 @@ class FigFillPicker extends HTMLElement {
|
|
|
1105
1147
|
});
|
|
1106
1148
|
}
|
|
1107
1149
|
|
|
1150
|
+
#readAlphaFromInput() {
|
|
1151
|
+
const el = this.#dialog?.querySelector(".fig-fill-picker-ci-a");
|
|
1152
|
+
if (!el) return undefined;
|
|
1153
|
+
const pct = parseFloat(el.value);
|
|
1154
|
+
if (!Number.isFinite(pct)) return undefined;
|
|
1155
|
+
return Math.max(0, Math.min(1, pct / 100));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1108
1158
|
#readColorFromInputs() {
|
|
1109
1159
|
const q = (cls) => this.#dialog?.querySelector(`.${cls}`);
|
|
1110
1160
|
const val = (cls) => parseFloat(q(cls)?.value ?? 0);
|
|
1161
|
+
const withAlpha = (color) => {
|
|
1162
|
+
if (!color) return null;
|
|
1163
|
+
const a = this.#readAlphaFromInput();
|
|
1164
|
+
return { ...color, a: a ?? color.a ?? this.#color.a };
|
|
1165
|
+
};
|
|
1111
1166
|
|
|
1112
1167
|
switch (this.#colorInputMode) {
|
|
1113
1168
|
case "rgb":
|
|
1114
|
-
return
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1169
|
+
return withAlpha(
|
|
1170
|
+
this.#rgbToHSV({
|
|
1171
|
+
r: val("fig-fill-picker-ci-r"),
|
|
1172
|
+
g: val("fig-fill-picker-ci-g"),
|
|
1173
|
+
b: val("fig-fill-picker-ci-b"),
|
|
1174
|
+
}),
|
|
1175
|
+
);
|
|
1119
1176
|
case "hsl": {
|
|
1120
1177
|
const rgb = this.#hslToRGB({
|
|
1121
1178
|
h: val("fig-fill-picker-ci-h"),
|
|
1122
1179
|
s: val("fig-fill-picker-ci-s"),
|
|
1123
1180
|
l: val("fig-fill-picker-ci-l"),
|
|
1124
1181
|
});
|
|
1125
|
-
return this.#rgbToHSV(rgb);
|
|
1182
|
+
return withAlpha(this.#rgbToHSV(rgb));
|
|
1126
1183
|
}
|
|
1127
1184
|
case "hsb":
|
|
1128
|
-
return {
|
|
1185
|
+
return withAlpha({
|
|
1129
1186
|
h: val("fig-fill-picker-ci-h"),
|
|
1130
1187
|
s: val("fig-fill-picker-ci-s"),
|
|
1131
1188
|
v: val("fig-fill-picker-ci-v"),
|
|
1132
|
-
|
|
1133
|
-
};
|
|
1189
|
+
});
|
|
1134
1190
|
case "lab": {
|
|
1135
1191
|
const rgb = this.#oklabToRGB({
|
|
1136
1192
|
l: val("fig-fill-picker-ci-okl") / 100,
|
|
1137
1193
|
a: val("fig-fill-picker-ci-oka"),
|
|
1138
1194
|
b: val("fig-fill-picker-ci-okb"),
|
|
1139
1195
|
});
|
|
1140
|
-
return this.#rgbToHSV(rgb);
|
|
1196
|
+
return withAlpha(this.#rgbToHSV(rgb));
|
|
1141
1197
|
}
|
|
1142
1198
|
case "lch": {
|
|
1143
1199
|
const rgb = this.#oklchToRGB({
|
|
@@ -1145,7 +1201,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
1145
1201
|
c: val("fig-fill-picker-ci-okc"),
|
|
1146
1202
|
h: val("fig-fill-picker-ci-okh"),
|
|
1147
1203
|
});
|
|
1148
|
-
return this.#rgbToHSV(rgb);
|
|
1204
|
+
return withAlpha(this.#rgbToHSV(rgb));
|
|
1205
|
+
}
|
|
1206
|
+
case "css": {
|
|
1207
|
+
const cssEl = q("fig-fill-picker-ci-css");
|
|
1208
|
+
if (!cssEl) return null;
|
|
1209
|
+
return this.#parseCssColor(cssEl.value);
|
|
1149
1210
|
}
|
|
1150
1211
|
default: {
|
|
1151
1212
|
// hex
|
|
@@ -1154,8 +1215,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
1154
1215
|
let hex = hexEl.value.replace(/^#/, "");
|
|
1155
1216
|
if (hex.length === 3)
|
|
1156
1217
|
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1218
|
+
if (hex.length === 8) {
|
|
1219
|
+
const alpha = parseInt(hex.slice(6, 8), 16) / 255;
|
|
1220
|
+
hex = hex.slice(0, 6);
|
|
1221
|
+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
1222
|
+
return { ...this.#hexToHSV(`#${hex}`), a: alpha };
|
|
1223
|
+
}
|
|
1157
1224
|
if (hex.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
1158
|
-
return this.#hexToHSV(`#${hex}`);
|
|
1225
|
+
return withAlpha(this.#hexToHSV(`#${hex}`));
|
|
1159
1226
|
}
|
|
1160
1227
|
}
|
|
1161
1228
|
}
|
|
@@ -1203,11 +1270,22 @@ class FigFillPicker extends HTMLElement {
|
|
|
1203
1270
|
set("fig-fill-picker-ci-okh", Math.round(lch.h));
|
|
1204
1271
|
break;
|
|
1205
1272
|
}
|
|
1273
|
+
case "css":
|
|
1274
|
+
set("fig-fill-picker-ci-css", this.#formatCssColor(this.#color));
|
|
1275
|
+
break;
|
|
1206
1276
|
default: // hex
|
|
1207
1277
|
set("fig-fill-picker-ci-hex", hex.replace(/^#/, "").toUpperCase());
|
|
1208
1278
|
break;
|
|
1209
1279
|
}
|
|
1210
1280
|
|
|
1281
|
+
if (this.#colorInputMode !== "css") {
|
|
1282
|
+
const alphaPct = Math.round(this.#color.a * 1000) / 10;
|
|
1283
|
+
set(
|
|
1284
|
+
"fig-fill-picker-ci-a",
|
|
1285
|
+
Number.isInteger(alphaPct) ? alphaPct : +alphaPct.toFixed(1),
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1211
1289
|
if (this.#opacitySlider) {
|
|
1212
1290
|
this.#opacitySlider.setAttribute("color", hex);
|
|
1213
1291
|
}
|
|
@@ -1218,19 +1296,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
1218
1296
|
// ============ GRADIENT TAB ============
|
|
1219
1297
|
#initGradientTab() {
|
|
1220
1298
|
const container = this.#dialog.querySelector('[data-tab="gradient"]');
|
|
1221
|
-
const
|
|
1222
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
1299
|
+
const interpolationValue = gradientInterpolationSelectValue(this.#gradient);
|
|
1223
1300
|
|
|
1224
1301
|
container.innerHTML = `
|
|
1225
1302
|
<fig-field class="fig-fill-picker-gradient-header">
|
|
1226
|
-
<fig-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
</fig-
|
|
1233
|
-
<fig-tooltip text="
|
|
1303
|
+
<fig-select class="fig-fill-picker-gradient-type" label="Gradient type" value="${figEditorEscapeAttribute(this.#gradient.type)}">
|
|
1304
|
+
<fig-select-options>
|
|
1305
|
+
<fig-select-option value="linear">Linear</fig-select-option>
|
|
1306
|
+
<fig-select-option value="radial">Radial</fig-select-option>
|
|
1307
|
+
<fig-select-option value="angular">Angular</fig-select-option>
|
|
1308
|
+
</fig-select-options>
|
|
1309
|
+
</fig-select>
|
|
1310
|
+
<fig-tooltip text="Gradient angle">
|
|
1234
1311
|
<fig-input-number class="fig-fill-picker-gradient-angle" aria-label="Gradient angle" value="${
|
|
1235
1312
|
(this.#gradient.angle - 90 + 360) % 360
|
|
1236
1313
|
}" min="0" max="360" units="°" wrap></fig-input-number>
|
|
@@ -1243,11 +1320,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
1243
1320
|
this.#gradient.centerY
|
|
1244
1321
|
}" units="%" class="fig-fill-picker-gradient-cy"></fig-input-number>
|
|
1245
1322
|
</div>
|
|
1246
|
-
<
|
|
1247
|
-
<fig-
|
|
1248
|
-
<fig-icon
|
|
1249
|
-
|
|
1250
|
-
|
|
1323
|
+
<div class="fig-fill-picker-gradient-actions">
|
|
1324
|
+
<fig-tooltip text="Flip gradient">
|
|
1325
|
+
<fig-button icon variant="ghost" class="fig-fill-picker-gradient-flip" aria-label="Flip gradient">
|
|
1326
|
+
<fig-icon name="swap"></fig-icon>
|
|
1327
|
+
</fig-button>
|
|
1328
|
+
</fig-tooltip>
|
|
1329
|
+
<fig-tooltip text="Rotate gradient">
|
|
1330
|
+
<fig-button icon variant="ghost" class="fig-fill-picker-gradient-rotate" aria-label="Rotate gradient">
|
|
1331
|
+
<fig-icon name="rotate"></fig-icon>
|
|
1332
|
+
</fig-button>
|
|
1333
|
+
</fig-tooltip>
|
|
1334
|
+
</div>
|
|
1251
1335
|
</fig-field>
|
|
1252
1336
|
<fig-preview class="fig-fill-picker-gradient-preview">
|
|
1253
1337
|
<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 +1352,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
1268
1352
|
<span>Color interpolation</span>
|
|
1269
1353
|
</fig-header>
|
|
1270
1354
|
<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>
|
|
1355
|
+
<fig-select class="fig-fill-picker-gradient-space" label="Color interpolation" full value="${figEditorEscapeAttribute(interpolationValue)}">
|
|
1356
|
+
<fig-select-options>
|
|
1357
|
+
${this.#gradientInterpolationOptionsMarkup()}
|
|
1358
|
+
</fig-select-options>
|
|
1359
|
+
</fig-select>
|
|
1290
1360
|
</fig-field>
|
|
1291
1361
|
</div>
|
|
1292
1362
|
`;
|
|
@@ -1295,44 +1365,141 @@ class FigFillPicker extends HTMLElement {
|
|
|
1295
1365
|
this.#setupGradientEvents(container);
|
|
1296
1366
|
}
|
|
1297
1367
|
|
|
1368
|
+
#gradientInterpolationOptionsMarkup() {
|
|
1369
|
+
const hueMethods = ["shorter", "longer", "increasing", "decreasing"];
|
|
1370
|
+
const groups = [
|
|
1371
|
+
{
|
|
1372
|
+
label: "Linear",
|
|
1373
|
+
options: [
|
|
1374
|
+
{ value: "srgb", label: "sRGB" },
|
|
1375
|
+
],
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
label: "",
|
|
1379
|
+
options: [{ value: "oklab", label: "OKLAB" }],
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
label: "Polar",
|
|
1383
|
+
options: hueMethods.map((method) => ({
|
|
1384
|
+
value: `oklch-${method}`,
|
|
1385
|
+
label: `OKLCH ${method.charAt(0).toUpperCase()}${method.slice(1)}`,
|
|
1386
|
+
})),
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
label: "",
|
|
1390
|
+
separator: true,
|
|
1391
|
+
options: hueMethods.map((method) => ({
|
|
1392
|
+
value: `hsl-${method}`,
|
|
1393
|
+
label: `HSL ${method.charAt(0).toUpperCase()}${method.slice(1)}`,
|
|
1394
|
+
})),
|
|
1395
|
+
},
|
|
1396
|
+
];
|
|
1397
|
+
return groups
|
|
1398
|
+
.map((group) => {
|
|
1399
|
+
const options = group.options
|
|
1400
|
+
.map((opt) => {
|
|
1401
|
+
const methodLabel = opt.method
|
|
1402
|
+
? opt.method.charAt(0).toUpperCase() + opt.method.slice(1)
|
|
1403
|
+
: "";
|
|
1404
|
+
const appendLabel = opt.append || methodLabel;
|
|
1405
|
+
return `<fig-select-option value="${figEditorEscapeAttribute(opt.value)}" label="${figEditorEscapeAttribute(opt.label)}">
|
|
1406
|
+
<fig-interpolation-swatch slot="prepend" size="large" aria-hidden="true"></fig-interpolation-swatch>
|
|
1407
|
+
${figEditorEscapeAttribute(opt.label)}
|
|
1408
|
+
${appendLabel ? `<span slot="append">${figEditorEscapeAttribute(appendLabel)}</span>` : ""}
|
|
1409
|
+
</fig-select-option>`;
|
|
1410
|
+
})
|
|
1411
|
+
.join("");
|
|
1412
|
+
const separator = group.label || group.separator
|
|
1413
|
+
? `<fig-menu-separator${group.label ? ` label="${figEditorEscapeAttribute(group.label)}"` : ""}></fig-menu-separator>`
|
|
1414
|
+
: "";
|
|
1415
|
+
return `${separator}${options}`;
|
|
1416
|
+
})
|
|
1417
|
+
.join("");
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1298
1420
|
#setupGradientEvents(container) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1421
|
+
const getSelectValue = (event) =>
|
|
1422
|
+
typeof event.detail === "string" ? event.detail : event.target?.value;
|
|
1423
|
+
const gradientBarInput = container.querySelector(
|
|
1424
|
+
".fig-fill-picker-gradient-bar-input",
|
|
1302
1425
|
);
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1426
|
+
const setGradientBarPreview = (gradient) => {
|
|
1427
|
+
if (!gradientBarInput) return;
|
|
1428
|
+
this.#syncingGradientBar = true;
|
|
1429
|
+
try {
|
|
1430
|
+
gradientBarInput.setAttribute(
|
|
1431
|
+
"value",
|
|
1432
|
+
JSON.stringify({
|
|
1433
|
+
type: "gradient",
|
|
1434
|
+
gradient: gradientToValueShape(gradient),
|
|
1435
|
+
}),
|
|
1436
|
+
);
|
|
1437
|
+
} finally {
|
|
1438
|
+
this.#syncingGradientBar = false;
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
const restoreGradientBarPreview = () => {
|
|
1442
|
+
setGradientBarPreview(this.#gradient);
|
|
1443
|
+
};
|
|
1305
1444
|
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1445
|
+
const typeSelect = container.querySelector(".fig-fill-picker-gradient-type");
|
|
1446
|
+
typeSelect?.addEventListener("change", (e) => {
|
|
1447
|
+
const next = getSelectValue(e);
|
|
1448
|
+
if (!next) return;
|
|
1449
|
+
this.#gradient.type = next;
|
|
1308
1450
|
this.#updateGradientUI();
|
|
1309
1451
|
this.#emitInput();
|
|
1310
|
-
};
|
|
1311
|
-
typeDropdown.addEventListener("change", handleTypeChange);
|
|
1452
|
+
});
|
|
1312
1453
|
|
|
1313
|
-
const
|
|
1454
|
+
const interpolationSelect = container.querySelector(
|
|
1314
1455
|
".fig-fill-picker-gradient-space",
|
|
1315
1456
|
);
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1457
|
+
interpolationSelect
|
|
1458
|
+
?.querySelectorAll("fig-select-option")
|
|
1459
|
+
.forEach((option) => {
|
|
1460
|
+
const previewOption = () => {
|
|
1461
|
+
const parsed = parseGradientInterpolationSelectValue(
|
|
1462
|
+
option.getAttribute("value") || "srgb",
|
|
1463
|
+
);
|
|
1464
|
+
setGradientBarPreview(
|
|
1465
|
+
normalizeGradientConfig({
|
|
1466
|
+
...this.#gradient,
|
|
1467
|
+
...parsed,
|
|
1468
|
+
}),
|
|
1469
|
+
);
|
|
1470
|
+
};
|
|
1471
|
+
option.addEventListener("pointerenter", previewOption);
|
|
1472
|
+
option.addEventListener("pointerleave", () => {
|
|
1473
|
+
if (document.activeElement !== option) restoreGradientBarPreview();
|
|
1474
|
+
});
|
|
1475
|
+
option.addEventListener("focus", previewOption);
|
|
1476
|
+
option.addEventListener("blur", () => {
|
|
1477
|
+
if (!option.matches(":hover")) restoreGradientBarPreview();
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
this.#gradientInterpolationOpenObserver?.disconnect();
|
|
1481
|
+
if (interpolationSelect) {
|
|
1482
|
+
this.#gradientInterpolationOpenObserver = new MutationObserver(() => {
|
|
1483
|
+
if (!interpolationSelect.hasAttribute("open")) {
|
|
1484
|
+
restoreGradientBarPreview();
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
this.#gradientInterpolationOpenObserver.observe(interpolationSelect, {
|
|
1488
|
+
attributes: true,
|
|
1489
|
+
attributeFilter: ["open"],
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
interpolationSelect?.addEventListener("change", (e) => {
|
|
1493
|
+
const val = getSelectValue(e);
|
|
1494
|
+
if (!val) return;
|
|
1495
|
+
const parsed = parseGradientInterpolationSelectValue(val);
|
|
1324
1496
|
this.#gradient = normalizeGradientConfig({
|
|
1325
1497
|
...this.#gradient,
|
|
1326
|
-
|
|
1327
|
-
hueInterpolation: hue,
|
|
1498
|
+
...parsed,
|
|
1328
1499
|
});
|
|
1329
1500
|
this.#updateGradientUI();
|
|
1330
1501
|
this.#emitInput();
|
|
1331
|
-
};
|
|
1332
|
-
interpolationDropdown?.addEventListener(
|
|
1333
|
-
"change",
|
|
1334
|
-
handleInterpolationChange,
|
|
1335
|
-
);
|
|
1502
|
+
});
|
|
1336
1503
|
|
|
1337
1504
|
// Angle input
|
|
1338
1505
|
const angleInput = container.querySelector(
|
|
@@ -1361,6 +1528,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
1361
1528
|
this.#emitInput();
|
|
1362
1529
|
});
|
|
1363
1530
|
|
|
1531
|
+
// Rotate 90° clockwise
|
|
1532
|
+
container
|
|
1533
|
+
.querySelector(".fig-fill-picker-gradient-rotate")
|
|
1534
|
+
?.addEventListener("click", () => {
|
|
1535
|
+
this.#gradient.angle = (Number(this.#gradient.angle) + 90) % 360;
|
|
1536
|
+
this.#updateGradientUI();
|
|
1537
|
+
this.#emitInput();
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1364
1540
|
// Flip button
|
|
1365
1541
|
container
|
|
1366
1542
|
.querySelector(".fig-fill-picker-gradient-flip")
|
|
@@ -1389,9 +1565,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
1389
1565
|
});
|
|
1390
1566
|
|
|
1391
1567
|
// Embedded gradient bar input
|
|
1392
|
-
const gradientBarInput = container.querySelector(
|
|
1393
|
-
".fig-fill-picker-gradient-bar-input",
|
|
1394
|
-
);
|
|
1395
1568
|
if (gradientBarInput) {
|
|
1396
1569
|
const syncFromBarInput = (e) => {
|
|
1397
1570
|
e.stopPropagation();
|
|
@@ -1403,6 +1576,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1403
1576
|
...detail.gradient,
|
|
1404
1577
|
});
|
|
1405
1578
|
this.#updateSwatch();
|
|
1579
|
+
this.#updateGradientInterpolationSwatches();
|
|
1406
1580
|
this.#updateGradientStopsList();
|
|
1407
1581
|
};
|
|
1408
1582
|
gradientBarInput.addEventListener("input", (e) => {
|
|
@@ -1555,6 +1729,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1555
1729
|
|
|
1556
1730
|
this.#syncingGradientBar = true;
|
|
1557
1731
|
try {
|
|
1732
|
+
this.#updateGradientInterpolationSwatches();
|
|
1558
1733
|
this.#updateGradientPreview();
|
|
1559
1734
|
this.#emitInput();
|
|
1560
1735
|
} finally {
|
|
@@ -1573,35 +1748,79 @@ class FigFillPicker extends HTMLElement {
|
|
|
1573
1748
|
const angleInput = container.querySelector(
|
|
1574
1749
|
".fig-fill-picker-gradient-angle",
|
|
1575
1750
|
);
|
|
1751
|
+
const rotateBtn = container.querySelector(
|
|
1752
|
+
".fig-fill-picker-gradient-rotate",
|
|
1753
|
+
);
|
|
1576
1754
|
const centerInputs = container.querySelector(
|
|
1577
1755
|
".fig-fill-picker-gradient-center",
|
|
1578
1756
|
);
|
|
1579
1757
|
|
|
1580
1758
|
if (this.#gradient.type === "radial") {
|
|
1581
1759
|
angleInput.style.display = "none";
|
|
1760
|
+
if (rotateBtn) rotateBtn.style.display = "none";
|
|
1582
1761
|
centerInputs.style.display = "flex";
|
|
1583
1762
|
} else {
|
|
1584
|
-
angleInput.style.display
|
|
1763
|
+
angleInput.style.removeProperty("display");
|
|
1764
|
+
rotateBtn?.style.removeProperty("display");
|
|
1585
1765
|
centerInputs.style.display = "none";
|
|
1586
1766
|
// Sync angle input value (convert CSS angle to picker angle)
|
|
1587
1767
|
const pickerAngle = (this.#gradient.angle - 90 + 360) % 360;
|
|
1588
1768
|
angleInput.setAttribute("value", pickerAngle);
|
|
1589
1769
|
}
|
|
1590
1770
|
|
|
1591
|
-
const
|
|
1771
|
+
const interpolationSelect = container.querySelector(
|
|
1592
1772
|
".fig-fill-picker-gradient-space",
|
|
1593
1773
|
);
|
|
1594
|
-
if (
|
|
1595
|
-
|
|
1596
|
-
this.#gradient
|
|
1597
|
-
|
|
1598
|
-
: this.#gradient.interpolationSpace;
|
|
1774
|
+
if (interpolationSelect) {
|
|
1775
|
+
interpolationSelect.value = gradientInterpolationSelectValue(
|
|
1776
|
+
this.#gradient,
|
|
1777
|
+
);
|
|
1599
1778
|
}
|
|
1600
1779
|
|
|
1780
|
+
this.#updateGradientInterpolationSwatches();
|
|
1601
1781
|
this.#updateGradientPreview();
|
|
1602
1782
|
this.#updateGradientStopsList();
|
|
1603
1783
|
}
|
|
1604
1784
|
|
|
1785
|
+
#interpolationPreviewStops() {
|
|
1786
|
+
const stops = Array.isArray(this.#gradient.stops)
|
|
1787
|
+
? [...this.#gradient.stops].sort(
|
|
1788
|
+
(a, b) => (a.position ?? 0) - (b.position ?? 0),
|
|
1789
|
+
)
|
|
1790
|
+
: [];
|
|
1791
|
+
if (stops.length < 2) {
|
|
1792
|
+
return [
|
|
1793
|
+
{ color: "#FF0000", position: 0 },
|
|
1794
|
+
{ color: "#4F9EFF", position: 100 },
|
|
1795
|
+
];
|
|
1796
|
+
}
|
|
1797
|
+
return stops.map((stop) => ({
|
|
1798
|
+
color: String(stop.color || "#D9D9D9").replace(/^(#(?:[0-9a-f]{6})).*/i, "$1"),
|
|
1799
|
+
position: stop.position ?? 0,
|
|
1800
|
+
}));
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
#updateGradientInterpolationSwatches() {
|
|
1804
|
+
if (!this.#dialog) return;
|
|
1805
|
+
const stops = this.#interpolationPreviewStops();
|
|
1806
|
+
this.#dialog
|
|
1807
|
+
.querySelectorAll("fig-interpolation-swatch")
|
|
1808
|
+
.forEach((swatch) => {
|
|
1809
|
+
const optionVal =
|
|
1810
|
+
swatch.closest("fig-select-option")?.getAttribute("value") || "srgb";
|
|
1811
|
+
const parsed = parseGradientInterpolationSelectValue(optionVal);
|
|
1812
|
+
const gradient = {
|
|
1813
|
+
type: "linear",
|
|
1814
|
+
stops,
|
|
1815
|
+
interpolationSpace: parsed.interpolationSpace,
|
|
1816
|
+
};
|
|
1817
|
+
if (GRADIENT_HUE_SPACES.has(parsed.interpolationSpace)) {
|
|
1818
|
+
gradient.hueInterpolation = parsed.hueInterpolation;
|
|
1819
|
+
}
|
|
1820
|
+
swatch.value = { type: "gradient", gradient };
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1605
1824
|
#updateGradientPreview() {
|
|
1606
1825
|
if (!this.#dialog) return;
|
|
1607
1826
|
|
|
@@ -1690,6 +1909,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1690
1909
|
.querySelector(".fig-fill-picker-stop-position")
|
|
1691
1910
|
.addEventListener("input", () => {
|
|
1692
1911
|
this.#syncGradientStopRow(row);
|
|
1912
|
+
this.#updateGradientInterpolationSwatches();
|
|
1693
1913
|
this.#updateGradientPreview();
|
|
1694
1914
|
this.#emitInput();
|
|
1695
1915
|
});
|
|
@@ -1709,6 +1929,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1709
1929
|
this.#syncGradientStopRow(row);
|
|
1710
1930
|
this.#syncingGradientBar = true;
|
|
1711
1931
|
try {
|
|
1932
|
+
this.#updateGradientInterpolationSwatches();
|
|
1712
1933
|
this.#updateGradientPreview();
|
|
1713
1934
|
this.#emitInput();
|
|
1714
1935
|
} finally {
|
|
@@ -1787,19 +2008,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
1787
2008
|
// ============ IMAGE TAB ============
|
|
1788
2009
|
#initImageTab() {
|
|
1789
2010
|
const container = this.#dialog.querySelector('[data-tab="image"]');
|
|
1790
|
-
const experimental = this.getAttribute("experimental");
|
|
1791
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
1792
2011
|
|
|
1793
2012
|
container.innerHTML = `
|
|
1794
2013
|
<fig-field class="fig-fill-picker-media-header">
|
|
1795
|
-
<fig-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
</fig-
|
|
2014
|
+
<fig-select class="fig-fill-picker-scale-mode" label="Image scale mode" value="${figEditorEscapeAttribute(this.#image.scaleMode)}">
|
|
2015
|
+
<fig-select-options>
|
|
2016
|
+
<fig-select-option value="fill">Fill</fig-select-option>
|
|
2017
|
+
<fig-select-option value="fit">Fit</fig-select-option>
|
|
2018
|
+
<fig-select-option value="crop">Crop</fig-select-option>
|
|
2019
|
+
<fig-select-option value="tile">Tile</fig-select-option>
|
|
2020
|
+
</fig-select-options>
|
|
2021
|
+
</fig-select>
|
|
1803
2022
|
<fig-input-number class="fig-fill-picker-scale" aria-label="Image tile scale" min="1" max="200" value="${
|
|
1804
2023
|
this.#image.scale
|
|
1805
2024
|
}" units="%" ${
|
|
@@ -1816,15 +2035,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
1816
2035
|
}
|
|
1817
2036
|
|
|
1818
2037
|
#setupImageEvents(container) {
|
|
1819
|
-
const
|
|
2038
|
+
const scaleModeSelect = container.querySelector(
|
|
1820
2039
|
".fig-fill-picker-scale-mode",
|
|
1821
2040
|
);
|
|
1822
2041
|
const scaleInput = container.querySelector(".fig-fill-picker-scale");
|
|
1823
2042
|
const preview = container.querySelector(".fig-fill-picker-image-preview");
|
|
1824
2043
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2044
|
+
scaleModeSelect.addEventListener("change", (e) => {
|
|
2045
|
+
const next =
|
|
2046
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
2047
|
+
if (!next) return;
|
|
2048
|
+
this.#image.scaleMode = next;
|
|
2049
|
+
scaleInput.style.display = next === "tile" ? "block" : "none";
|
|
1828
2050
|
this.#updateImagePreview(preview);
|
|
1829
2051
|
this.#updateSwatch();
|
|
1830
2052
|
this.#emitInput();
|
|
@@ -1955,18 +2177,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
1955
2177
|
// ============ VIDEO TAB ============
|
|
1956
2178
|
#initVideoTab() {
|
|
1957
2179
|
const container = this.#dialog.querySelector('[data-tab="video"]');
|
|
1958
|
-
const experimental = this.getAttribute("experimental");
|
|
1959
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
1960
2180
|
|
|
1961
2181
|
container.innerHTML = `
|
|
1962
2182
|
<fig-field class="fig-fill-picker-media-header">
|
|
1963
|
-
<fig-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
</fig-
|
|
2183
|
+
<fig-select class="fig-fill-picker-scale-mode" label="Video scale mode" value="${figEditorEscapeAttribute(this.#video.scaleMode)}">
|
|
2184
|
+
<fig-select-options>
|
|
2185
|
+
<fig-select-option value="fill">Fill</fig-select-option>
|
|
2186
|
+
<fig-select-option value="fit">Fit</fig-select-option>
|
|
2187
|
+
<fig-select-option value="crop">Crop</fig-select-option>
|
|
2188
|
+
</fig-select-options>
|
|
2189
|
+
</fig-select>
|
|
1970
2190
|
<fig-button class="fig-fill-picker-media-rotate" icon variant="ghost" aria-label="Rotate">
|
|
1971
2191
|
<fig-icon name="rotate"></fig-icon>
|
|
1972
2192
|
</fig-button>
|
|
@@ -1978,13 +2198,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
1978
2198
|
}
|
|
1979
2199
|
|
|
1980
2200
|
#setupVideoEvents(container) {
|
|
1981
|
-
const
|
|
2201
|
+
const scaleModeSelect = container.querySelector(
|
|
1982
2202
|
".fig-fill-picker-scale-mode",
|
|
1983
2203
|
);
|
|
1984
2204
|
const preview = container.querySelector(".fig-fill-picker-video-preview");
|
|
1985
2205
|
|
|
1986
|
-
|
|
1987
|
-
|
|
2206
|
+
scaleModeSelect.addEventListener("change", (e) => {
|
|
2207
|
+
const next =
|
|
2208
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
2209
|
+
if (!next) return;
|
|
2210
|
+
this.#video.scaleMode = next;
|
|
1988
2211
|
this.#updateVideoPreviewStyle(preview);
|
|
1989
2212
|
this.#updateSwatch();
|
|
1990
2213
|
this.#emitInput();
|
|
@@ -2014,13 +2237,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
2014
2237
|
// ============ WEBCAM TAB ============
|
|
2015
2238
|
#initWebcamTab() {
|
|
2016
2239
|
const container = this.#dialog.querySelector('[data-tab="webcam"]');
|
|
2017
|
-
const experimental = this.getAttribute("experimental");
|
|
2018
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
2019
2240
|
|
|
2020
2241
|
container.innerHTML = `
|
|
2021
2242
|
<fig-field class="fig-fill-picker-webcam-camera" style="display: none;">
|
|
2022
|
-
<fig-
|
|
2023
|
-
|
|
2243
|
+
<fig-select class="fig-fill-picker-camera-select" label="Camera" full>
|
|
2244
|
+
<fig-select-options></fig-select-options>
|
|
2245
|
+
</fig-select>
|
|
2024
2246
|
</fig-field>
|
|
2025
2247
|
<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
2248
|
<video class="fig-fill-picker-webcam-video" autoplay muted playsinline></video>
|
|
@@ -2081,11 +2303,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
2081
2303
|
|
|
2082
2304
|
if (cameras.length > 1) {
|
|
2083
2305
|
cameraField.style.display = "";
|
|
2084
|
-
cameraSelect
|
|
2085
|
-
|
|
2086
|
-
|
|
2306
|
+
let panel = cameraSelect.querySelector(":scope > fig-select-options");
|
|
2307
|
+
if (!panel) {
|
|
2308
|
+
panel = document.createElement("fig-select-options");
|
|
2309
|
+
cameraSelect.append(panel);
|
|
2310
|
+
}
|
|
2311
|
+
panel.replaceChildren();
|
|
2087
2312
|
cameras.forEach((cam, i) => {
|
|
2088
|
-
const option = document.createElement("option");
|
|
2313
|
+
const option = document.createElement("fig-select-option");
|
|
2089
2314
|
option.value = cam.deviceId;
|
|
2090
2315
|
const label =
|
|
2091
2316
|
cam.label || (cameras.length > 1 ? `Camera ${i + 1}` : "Camera");
|
|
@@ -2098,14 +2323,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
2098
2323
|
return ` ${displayId}`;
|
|
2099
2324
|
},
|
|
2100
2325
|
);
|
|
2101
|
-
|
|
2326
|
+
panel.append(option);
|
|
2102
2327
|
});
|
|
2103
2328
|
if (deviceId) cameraSelect.value = deviceId;
|
|
2104
2329
|
} else {
|
|
2105
2330
|
cameraField.style.display = "none";
|
|
2106
2331
|
cameraSelect
|
|
2107
|
-
.
|
|
2108
|
-
|
|
2332
|
+
.querySelector(":scope > fig-select-options")
|
|
2333
|
+
?.replaceChildren();
|
|
2109
2334
|
}
|
|
2110
2335
|
} catch (err) {
|
|
2111
2336
|
if (requestId !== this.#webcamRequestId) return;
|
|
@@ -2133,7 +2358,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
2133
2358
|
this.#webcamStart = startWebcam;
|
|
2134
2359
|
|
|
2135
2360
|
cameraSelect.addEventListener("change", (e) => {
|
|
2136
|
-
|
|
2361
|
+
const next =
|
|
2362
|
+
typeof e.detail === "string" ? e.detail : e.target?.value;
|
|
2363
|
+
if (next) startWebcam(next);
|
|
2137
2364
|
});
|
|
2138
2365
|
|
|
2139
2366
|
captureBtn.addEventListener("click", async () => {
|
|
@@ -2287,6 +2514,55 @@ class FigFillPicker extends HTMLElement {
|
|
|
2287
2514
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
2288
2515
|
}
|
|
2289
2516
|
|
|
2517
|
+
#formatCssAlpha(alpha) {
|
|
2518
|
+
const a = Math.max(0, Math.min(1, Number(alpha) || 0));
|
|
2519
|
+
const rounded = Math.round(a * 1000) / 1000;
|
|
2520
|
+
return String(rounded);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
#formatCssColor(color = this.#color) {
|
|
2524
|
+
const rgb = this.#hsvToRGB(color);
|
|
2525
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${this.#formatCssAlpha(color.a)})`;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
/** Parse CSS color strings (rgba/rgb/hex) into HSV(+alpha). */
|
|
2529
|
+
#parseCssColor(raw) {
|
|
2530
|
+
const value = String(raw ?? "").trim();
|
|
2531
|
+
if (!value) return null;
|
|
2532
|
+
|
|
2533
|
+
const hexMatch = value.match(/^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
|
|
2534
|
+
if (hexMatch) {
|
|
2535
|
+
let hex = hexMatch[1];
|
|
2536
|
+
if (hex.length === 3) {
|
|
2537
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
2538
|
+
}
|
|
2539
|
+
let alpha = 1;
|
|
2540
|
+
if (hex.length === 8) {
|
|
2541
|
+
alpha = parseInt(hex.slice(6, 8), 16) / 255;
|
|
2542
|
+
hex = hex.slice(0, 6);
|
|
2543
|
+
}
|
|
2544
|
+
const hsv = this.#hexToHSV(`#${hex}`);
|
|
2545
|
+
return { ...hsv, a: alpha };
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
const rgbMatch = value.match(
|
|
2549
|
+
/^rgba?\(\s*([+-]?(?:\d+\.?\d*|\.\d+))\s*,\s*([+-]?(?:\d+\.?\d*|\.\d+))\s*,\s*([+-]?(?:\d+\.?\d*|\.\d+))(?:\s*,\s*([+-]?(?:\d+\.?\d*|\.\d+))\s*)?\)$/i,
|
|
2550
|
+
);
|
|
2551
|
+
if (rgbMatch) {
|
|
2552
|
+
const r = Math.max(0, Math.min(255, Math.round(parseFloat(rgbMatch[1]))));
|
|
2553
|
+
const g = Math.max(0, Math.min(255, Math.round(parseFloat(rgbMatch[2]))));
|
|
2554
|
+
const b = Math.max(0, Math.min(255, Math.round(parseFloat(rgbMatch[3]))));
|
|
2555
|
+
const alpha =
|
|
2556
|
+
rgbMatch[4] !== undefined
|
|
2557
|
+
? Math.max(0, Math.min(1, parseFloat(rgbMatch[4])))
|
|
2558
|
+
: 1;
|
|
2559
|
+
if (![r, g, b, alpha].every(Number.isFinite)) return null;
|
|
2560
|
+
return { ...this.#rgbToHSV({ r, g, b }), a: alpha };
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
return null;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2290
2566
|
#hexToP3(hex, alpha = 1) {
|
|
2291
2567
|
const r = +(parseInt(hex.slice(1, 3), 16) / 255).toFixed(4);
|
|
2292
2568
|
const g = +(parseInt(hex.slice(3, 5), 16) / 255).toFixed(4);
|
|
@@ -2543,4 +2819,4 @@ class FigFillPicker extends HTMLElement {
|
|
|
2543
2819
|
}
|
|
2544
2820
|
}
|
|
2545
2821
|
}
|
|
2546
|
-
|
|
2822
|
+
figEditorDefineElement("fig-fill-picker", FigFillPicker);
|