@rogieking/figui3 3.9.2 → 3.13.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 +36 -3
- package/components.css +618 -300
- package/dist/fig.js +137 -80
- package/fig.js +1824 -291
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -609,6 +609,7 @@ class FigTooltip extends HTMLElement {
|
|
|
609
609
|
}
|
|
610
610
|
|
|
611
611
|
setupEventListeners() {
|
|
612
|
+
if (this.action === "manual") return;
|
|
612
613
|
if (this.action === "hover") {
|
|
613
614
|
if (!this.isTouchDevice()) {
|
|
614
615
|
this.addEventListener("pointerenter", this.showDelayedPopup.bind(this));
|
|
@@ -687,26 +688,34 @@ class FigTooltip extends HTMLElement {
|
|
|
687
688
|
const popupRect = this.popup.getBoundingClientRect();
|
|
688
689
|
const offset = this.getOffset();
|
|
689
690
|
|
|
691
|
+
const container = this.popup.parentElement;
|
|
692
|
+
const containerRect =
|
|
693
|
+
container && container !== document.body
|
|
694
|
+
? container.getBoundingClientRect()
|
|
695
|
+
: { left: 0, top: 0 };
|
|
696
|
+
|
|
690
697
|
// Position the tooltip above the element
|
|
691
|
-
let top = rect.top - popupRect.height - offset.top;
|
|
692
|
-
let left =
|
|
698
|
+
let top = rect.top - popupRect.height - offset.top - containerRect.top;
|
|
699
|
+
let left =
|
|
700
|
+
rect.left + (rect.width - popupRect.width) / 2 - containerRect.left;
|
|
693
701
|
this.popup.setAttribute("position", "top");
|
|
694
702
|
|
|
695
703
|
// Adjust if tooltip would go off-screen
|
|
696
|
-
if (top < 0) {
|
|
704
|
+
if (top + containerRect.top < 0) {
|
|
697
705
|
this.popup.setAttribute("position", "bottom");
|
|
698
|
-
top = rect.bottom + offset.bottom
|
|
706
|
+
top = rect.bottom + offset.bottom - containerRect.top;
|
|
699
707
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
708
|
+
const absLeft = left + containerRect.left;
|
|
709
|
+
if (absLeft < offset.left) {
|
|
710
|
+
left = offset.left - containerRect.left;
|
|
711
|
+
} else if (absLeft + popupRect.width > window.innerWidth - offset.right) {
|
|
712
|
+
left =
|
|
713
|
+
window.innerWidth - popupRect.width - offset.right - containerRect.left;
|
|
704
714
|
}
|
|
705
715
|
|
|
706
716
|
// Calculate the center of the target element relative to the tooltip
|
|
707
|
-
const targetCenter = rect.left + rect.width / 2;
|
|
708
|
-
const
|
|
709
|
-
const beakOffset = targetCenter - tooltipLeft;
|
|
717
|
+
const targetCenter = rect.left - containerRect.left + rect.width / 2;
|
|
718
|
+
const beakOffset = targetCenter - left;
|
|
710
719
|
|
|
711
720
|
// Set the beak offset as a CSS custom property
|
|
712
721
|
this.popup.style.setProperty("--beak-offset", `${beakOffset}px`);
|
|
@@ -1438,6 +1447,13 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1438
1447
|
const val = this.getAttribute("autoresize");
|
|
1439
1448
|
return val === null || val !== "false";
|
|
1440
1449
|
}
|
|
1450
|
+
set autoresize(value) {
|
|
1451
|
+
if (value || value === "") {
|
|
1452
|
+
this.setAttribute("autoresize", value === true ? "" : value);
|
|
1453
|
+
} else {
|
|
1454
|
+
this.removeAttribute("autoresize");
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1441
1457
|
|
|
1442
1458
|
setupObservers() {
|
|
1443
1459
|
this.teardownObservers();
|
|
@@ -2600,7 +2616,8 @@ class FigSegmentedControl extends HTMLElement {
|
|
|
2600
2616
|
|
|
2601
2617
|
#syncIndicator({ forceInstant = false } = {}) {
|
|
2602
2618
|
const isDisabled =
|
|
2603
|
-
this.hasAttribute("disabled") &&
|
|
2619
|
+
this.hasAttribute("disabled") &&
|
|
2620
|
+
this.getAttribute("disabled") !== "false";
|
|
2604
2621
|
const isAnimated = this.#isAnimatedEnabled();
|
|
2605
2622
|
const activeSegment =
|
|
2606
2623
|
this.#selectedSegment && this.contains(this.#selectedSegment)
|
|
@@ -4248,7 +4265,7 @@ class FigFieldSlider extends HTMLElement {
|
|
|
4248
4265
|
this.#label.remove();
|
|
4249
4266
|
}
|
|
4250
4267
|
} else {
|
|
4251
|
-
this.#label.textContent = hasLabelAttr ? rawLabel ?? "" : "Label";
|
|
4268
|
+
this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
|
|
4252
4269
|
if (this.#label.parentElement !== this.#field) {
|
|
4253
4270
|
this.#field.prepend(this.#label);
|
|
4254
4271
|
}
|
|
@@ -4401,7 +4418,7 @@ customElements.define("fig-field-slider", FigFieldSlider);
|
|
|
4401
4418
|
class FigInputColor extends HTMLElement {
|
|
4402
4419
|
rgba;
|
|
4403
4420
|
hex;
|
|
4404
|
-
|
|
4421
|
+
#alphaPercent = 100;
|
|
4405
4422
|
#swatch;
|
|
4406
4423
|
#fillPicker;
|
|
4407
4424
|
#textInput;
|
|
@@ -4413,6 +4430,20 @@ class FigInputColor extends HTMLElement {
|
|
|
4413
4430
|
get picker() {
|
|
4414
4431
|
return this.getAttribute("picker") || "native";
|
|
4415
4432
|
}
|
|
4433
|
+
set picker(value) {
|
|
4434
|
+
this.setAttribute("picker", value);
|
|
4435
|
+
}
|
|
4436
|
+
|
|
4437
|
+
get alpha() {
|
|
4438
|
+
return this.getAttribute("alpha");
|
|
4439
|
+
}
|
|
4440
|
+
set alpha(value) {
|
|
4441
|
+
if (value === null || value === undefined || value === false) {
|
|
4442
|
+
this.removeAttribute("alpha");
|
|
4443
|
+
} else {
|
|
4444
|
+
this.setAttribute("alpha", String(value));
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4416
4447
|
|
|
4417
4448
|
#buildFillPickerAttrs() {
|
|
4418
4449
|
const attrs = {};
|
|
@@ -4431,6 +4462,16 @@ class FigInputColor extends HTMLElement {
|
|
|
4431
4462
|
}
|
|
4432
4463
|
|
|
4433
4464
|
connectedCallback() {
|
|
4465
|
+
if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
|
|
4466
|
+
this.#renderRAF = requestAnimationFrame(() => {
|
|
4467
|
+
this.#renderRAF = null;
|
|
4468
|
+
this.#buildUI();
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
#renderRAF = null;
|
|
4473
|
+
|
|
4474
|
+
#buildUI() {
|
|
4434
4475
|
this.#setValues(this.getAttribute("value"));
|
|
4435
4476
|
|
|
4436
4477
|
const useFigmaPicker = this.picker === "figma";
|
|
@@ -4440,7 +4481,6 @@ class FigInputColor extends HTMLElement {
|
|
|
4440
4481
|
|
|
4441
4482
|
let html = ``;
|
|
4442
4483
|
if (this.getAttribute("text")) {
|
|
4443
|
-
// Display without # prefix
|
|
4444
4484
|
let label = `<fig-input-text
|
|
4445
4485
|
type="text"
|
|
4446
4486
|
placeholder="000000"
|
|
@@ -4452,7 +4492,7 @@ class FigInputColor extends HTMLElement {
|
|
|
4452
4492
|
placeholder="##"
|
|
4453
4493
|
min="0"
|
|
4454
4494
|
max="100"
|
|
4455
|
-
value="${this
|
|
4495
|
+
value="${this.#alphaPercent}"
|
|
4456
4496
|
units="%">
|
|
4457
4497
|
</fig-input-number>
|
|
4458
4498
|
</fig-tooltip>`;
|
|
@@ -4464,7 +4504,7 @@ class FigInputColor extends HTMLElement {
|
|
|
4464
4504
|
? `<fig-fill-picker mode="solid" ${fpAttrs} ${
|
|
4465
4505
|
showAlpha ? "" : 'alpha="false"'
|
|
4466
4506
|
} value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
|
|
4467
|
-
this
|
|
4507
|
+
this.#alphaPercent
|
|
4468
4508
|
}}'></fig-fill-picker>`
|
|
4469
4509
|
: `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"></fig-chit>`;
|
|
4470
4510
|
}
|
|
@@ -4474,7 +4514,6 @@ class FigInputColor extends HTMLElement {
|
|
|
4474
4514
|
${label}
|
|
4475
4515
|
</div>`;
|
|
4476
4516
|
} else {
|
|
4477
|
-
// Without text, if picker is hidden, show nothing
|
|
4478
4517
|
if (hidePicker) {
|
|
4479
4518
|
html = ``;
|
|
4480
4519
|
} else {
|
|
@@ -4482,7 +4521,7 @@ class FigInputColor extends HTMLElement {
|
|
|
4482
4521
|
? `<fig-fill-picker mode="solid" ${fpAttrs} ${
|
|
4483
4522
|
showAlpha ? "" : 'alpha="false"'
|
|
4484
4523
|
} value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
|
|
4485
|
-
this
|
|
4524
|
+
this.#alphaPercent
|
|
4486
4525
|
}}'></fig-fill-picker>`
|
|
4487
4526
|
: `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"></fig-chit>`;
|
|
4488
4527
|
}
|
|
@@ -4565,7 +4604,7 @@ class FigInputColor extends HTMLElement {
|
|
|
4565
4604
|
this.hexWithAlpha = this.value.toUpperCase();
|
|
4566
4605
|
this.hexOpaque = this.hexWithAlpha.slice(0, 7);
|
|
4567
4606
|
if (hexValue.length > 7) {
|
|
4568
|
-
this
|
|
4607
|
+
this.#alphaPercent = (this.rgba.a * 100).toFixed(0);
|
|
4569
4608
|
}
|
|
4570
4609
|
this.style.setProperty("--alpha", this.rgba.a);
|
|
4571
4610
|
}
|
|
@@ -4577,7 +4616,7 @@ class FigInputColor extends HTMLElement {
|
|
|
4577
4616
|
let inputValue = event.target.value.replace("#", "");
|
|
4578
4617
|
this.#setValues("#" + inputValue);
|
|
4579
4618
|
if (this.#alphaInput) {
|
|
4580
|
-
this.#alphaInput.setAttribute("value", this
|
|
4619
|
+
this.#alphaInput.setAttribute("value", this.#alphaPercent);
|
|
4581
4620
|
}
|
|
4582
4621
|
if (this.#swatch) {
|
|
4583
4622
|
this.#swatch.setAttribute("background", this.hexOpaque);
|
|
@@ -4602,7 +4641,7 @@ class FigInputColor extends HTMLElement {
|
|
|
4602
4641
|
JSON.stringify({
|
|
4603
4642
|
type: "solid",
|
|
4604
4643
|
color: this.hexOpaque,
|
|
4605
|
-
opacity: this
|
|
4644
|
+
opacity: this.#alphaPercent,
|
|
4606
4645
|
}),
|
|
4607
4646
|
);
|
|
4608
4647
|
}
|
|
@@ -4678,12 +4717,15 @@ class FigInputColor extends HTMLElement {
|
|
|
4678
4717
|
}
|
|
4679
4718
|
|
|
4680
4719
|
static get observedAttributes() {
|
|
4681
|
-
return ["value", "style", "mode", "picker", "experimental"];
|
|
4720
|
+
return ["value", "style", "mode", "picker", "experimental", "alpha"];
|
|
4682
4721
|
}
|
|
4683
4722
|
|
|
4684
4723
|
get mode() {
|
|
4685
4724
|
return this.getAttribute("mode");
|
|
4686
4725
|
}
|
|
4726
|
+
set mode(value) {
|
|
4727
|
+
this.setAttribute("mode", value);
|
|
4728
|
+
}
|
|
4687
4729
|
|
|
4688
4730
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
4689
4731
|
// Skip if value hasn't actually changed
|
|
@@ -4705,12 +4747,12 @@ class FigInputColor extends HTMLElement {
|
|
|
4705
4747
|
JSON.stringify({
|
|
4706
4748
|
type: "solid",
|
|
4707
4749
|
color: this.hexOpaque,
|
|
4708
|
-
opacity: this
|
|
4750
|
+
opacity: this.#alphaPercent,
|
|
4709
4751
|
}),
|
|
4710
4752
|
);
|
|
4711
4753
|
}
|
|
4712
4754
|
if (this.#alphaInput) {
|
|
4713
|
-
this.#alphaInput.setAttribute("value", this
|
|
4755
|
+
this.#alphaInput.setAttribute("value", this.#alphaPercent);
|
|
4714
4756
|
}
|
|
4715
4757
|
// NOTE: Do NOT emit input events here!
|
|
4716
4758
|
// Input events should only fire from user interactions, not programmatic changes.
|
|
@@ -4725,6 +4767,9 @@ class FigInputColor extends HTMLElement {
|
|
|
4725
4767
|
case "picker":
|
|
4726
4768
|
// Picker type change requires re-render
|
|
4727
4769
|
break;
|
|
4770
|
+
case "alpha":
|
|
4771
|
+
if (this.isConnected) this.#buildUI();
|
|
4772
|
+
break;
|
|
4728
4773
|
}
|
|
4729
4774
|
}
|
|
4730
4775
|
|
|
@@ -4828,6 +4873,110 @@ class FigInputColor extends HTMLElement {
|
|
|
4828
4873
|
customElements.define("fig-input-color", FigInputColor);
|
|
4829
4874
|
|
|
4830
4875
|
/* Input Fill */
|
|
4876
|
+
const GRADIENT_INTERPOLATION_SPACES = [
|
|
4877
|
+
"srgb",
|
|
4878
|
+
"srgb-linear",
|
|
4879
|
+
"display-p3",
|
|
4880
|
+
"oklab",
|
|
4881
|
+
"oklch",
|
|
4882
|
+
];
|
|
4883
|
+
const GRADIENT_HUE_INTERPOLATIONS = [
|
|
4884
|
+
"shorter",
|
|
4885
|
+
"longer",
|
|
4886
|
+
"increasing",
|
|
4887
|
+
"decreasing",
|
|
4888
|
+
];
|
|
4889
|
+
|
|
4890
|
+
const GRADIENT_PICKER_SPACES = ["srgb-linear", "oklab", "oklch"];
|
|
4891
|
+
|
|
4892
|
+
function normalizeGradientConfig(gradient) {
|
|
4893
|
+
const next = { ...(gradient ?? {}) };
|
|
4894
|
+
let interpolationSpace = String(
|
|
4895
|
+
next.interpolationSpace ?? "oklab",
|
|
4896
|
+
).toLowerCase();
|
|
4897
|
+
if (!GRADIENT_INTERPOLATION_SPACES.includes(interpolationSpace)) {
|
|
4898
|
+
interpolationSpace = "oklab";
|
|
4899
|
+
}
|
|
4900
|
+
if (interpolationSpace === "srgb" || interpolationSpace === "display-p3") {
|
|
4901
|
+
interpolationSpace = "oklab";
|
|
4902
|
+
}
|
|
4903
|
+
next.interpolationSpace = interpolationSpace;
|
|
4904
|
+
|
|
4905
|
+
const hueInterpolation = String(
|
|
4906
|
+
next.hueInterpolation ?? "shorter",
|
|
4907
|
+
).toLowerCase();
|
|
4908
|
+
next.hueInterpolation = GRADIENT_HUE_INTERPOLATIONS.includes(hueInterpolation)
|
|
4909
|
+
? hueInterpolation
|
|
4910
|
+
: "shorter";
|
|
4911
|
+
return next;
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
function gradientToValueShape(gradient) {
|
|
4915
|
+
const normalized = normalizeGradientConfig(gradient);
|
|
4916
|
+
const output = {
|
|
4917
|
+
...normalized,
|
|
4918
|
+
interpolationSpace: normalized.interpolationSpace,
|
|
4919
|
+
};
|
|
4920
|
+
if (normalized.interpolationSpace === "oklch") {
|
|
4921
|
+
output.hueInterpolation = normalized.hueInterpolation;
|
|
4922
|
+
} else {
|
|
4923
|
+
delete output.hueInterpolation;
|
|
4924
|
+
}
|
|
4925
|
+
return output;
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
function gradientInterpolationClause(gradient) {
|
|
4929
|
+
const normalized = normalizeGradientConfig(gradient);
|
|
4930
|
+
if (normalized.interpolationSpace === "oklch") {
|
|
4931
|
+
return `in oklch ${normalized.hueInterpolation} hue`;
|
|
4932
|
+
}
|
|
4933
|
+
return `in ${normalized.interpolationSpace}`;
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
function hslToP3(h, s, l) {
|
|
4937
|
+
const sRGB = hslToSRGB(h, s, l);
|
|
4938
|
+
return sRGB.map((c) => +(c / 255).toFixed(4));
|
|
4939
|
+
}
|
|
4940
|
+
|
|
4941
|
+
function hslToSRGB(h, s, l) {
|
|
4942
|
+
s /= 100;
|
|
4943
|
+
l /= 100;
|
|
4944
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
4945
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
4946
|
+
const m = l - c / 2;
|
|
4947
|
+
let r, g, b;
|
|
4948
|
+
if (h < 60) {
|
|
4949
|
+
r = c;
|
|
4950
|
+
g = x;
|
|
4951
|
+
b = 0;
|
|
4952
|
+
} else if (h < 120) {
|
|
4953
|
+
r = x;
|
|
4954
|
+
g = c;
|
|
4955
|
+
b = 0;
|
|
4956
|
+
} else if (h < 180) {
|
|
4957
|
+
r = 0;
|
|
4958
|
+
g = c;
|
|
4959
|
+
b = x;
|
|
4960
|
+
} else if (h < 240) {
|
|
4961
|
+
r = 0;
|
|
4962
|
+
g = x;
|
|
4963
|
+
b = c;
|
|
4964
|
+
} else if (h < 300) {
|
|
4965
|
+
r = x;
|
|
4966
|
+
g = 0;
|
|
4967
|
+
b = c;
|
|
4968
|
+
} else {
|
|
4969
|
+
r = c;
|
|
4970
|
+
g = 0;
|
|
4971
|
+
b = x;
|
|
4972
|
+
}
|
|
4973
|
+
return [
|
|
4974
|
+
Math.round((r + m) * 255),
|
|
4975
|
+
Math.round((g + m) * 255),
|
|
4976
|
+
Math.round((b + m) * 255),
|
|
4977
|
+
];
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4831
4980
|
/**
|
|
4832
4981
|
* A fill input that supports solid colors, gradients, images, and videos.
|
|
4833
4982
|
* @attr {string} value - JSON string with fill data
|
|
@@ -4846,6 +4995,8 @@ class FigInputFill extends HTMLElement {
|
|
|
4846
4995
|
#gradient = {
|
|
4847
4996
|
type: "linear",
|
|
4848
4997
|
angle: 180,
|
|
4998
|
+
interpolationSpace: "oklab",
|
|
4999
|
+
hueInterpolation: "shorter",
|
|
4849
5000
|
stops: [
|
|
4850
5001
|
{ position: 0, color: "#D9D9D9", opacity: 100 },
|
|
4851
5002
|
{ position: 100, color: "#737373", opacity: 100 },
|
|
@@ -4884,8 +5035,12 @@ class FigInputFill extends HTMLElement {
|
|
|
4884
5035
|
this.#solid.alpha = parsed.opacity / 100;
|
|
4885
5036
|
break;
|
|
4886
5037
|
case "gradient":
|
|
4887
|
-
if (parsed.gradient)
|
|
4888
|
-
this.#gradient = {
|
|
5038
|
+
if (parsed.gradient) {
|
|
5039
|
+
this.#gradient = normalizeGradientConfig({
|
|
5040
|
+
...this.#gradient,
|
|
5041
|
+
...parsed.gradient,
|
|
5042
|
+
});
|
|
5043
|
+
}
|
|
4889
5044
|
break;
|
|
4890
5045
|
case "image":
|
|
4891
5046
|
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
@@ -5051,7 +5206,12 @@ class FigInputFill extends HTMLElement {
|
|
|
5051
5206
|
this.#solid.alpha = detail.alpha;
|
|
5052
5207
|
break;
|
|
5053
5208
|
case "gradient":
|
|
5054
|
-
if (detail.gradient)
|
|
5209
|
+
if (detail.gradient) {
|
|
5210
|
+
this.#gradient = normalizeGradientConfig({
|
|
5211
|
+
...this.#gradient,
|
|
5212
|
+
...detail.gradient,
|
|
5213
|
+
});
|
|
5214
|
+
}
|
|
5055
5215
|
break;
|
|
5056
5216
|
case "image":
|
|
5057
5217
|
if (detail.image) this.#image = detail.image;
|
|
@@ -5414,7 +5574,7 @@ class FigInputFill extends HTMLElement {
|
|
|
5414
5574
|
case "gradient":
|
|
5415
5575
|
return {
|
|
5416
5576
|
type: "gradient",
|
|
5417
|
-
gradient:
|
|
5577
|
+
gradient: gradientToValueShape(this.#gradient),
|
|
5418
5578
|
};
|
|
5419
5579
|
case "image":
|
|
5420
5580
|
return {
|
|
@@ -5482,6 +5642,388 @@ class FigInputFill extends HTMLElement {
|
|
|
5482
5642
|
}
|
|
5483
5643
|
customElements.define("fig-input-fill", FigInputFill);
|
|
5484
5644
|
|
|
5645
|
+
/* Input Gradient */
|
|
5646
|
+
/**
|
|
5647
|
+
* A gradient-only fill input built on top of fig-fill-picker.
|
|
5648
|
+
* @attr {string} value - JSON string with gradient fill data
|
|
5649
|
+
* @attr {boolean} disabled - Whether the input is disabled
|
|
5650
|
+
* @fires input - When the gradient value changes
|
|
5651
|
+
* @fires change - When the gradient value is committed
|
|
5652
|
+
*/
|
|
5653
|
+
class FigInputGradient extends HTMLElement {
|
|
5654
|
+
#fillPicker;
|
|
5655
|
+
#track;
|
|
5656
|
+
#colorObserver = null;
|
|
5657
|
+
#gradient = {
|
|
5658
|
+
type: "linear",
|
|
5659
|
+
angle: 180,
|
|
5660
|
+
interpolationSpace: "oklab",
|
|
5661
|
+
hueInterpolation: "shorter",
|
|
5662
|
+
stops: [
|
|
5663
|
+
{ position: 0, color: "#D9D9D9", opacity: 100 },
|
|
5664
|
+
{ position: 100, color: "#737373", opacity: 100 },
|
|
5665
|
+
],
|
|
5666
|
+
};
|
|
5667
|
+
|
|
5668
|
+
constructor() {
|
|
5669
|
+
super();
|
|
5670
|
+
}
|
|
5671
|
+
|
|
5672
|
+
static get observedAttributes() {
|
|
5673
|
+
return ["value", "disabled", "experimental", "picker-anchor"];
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5676
|
+
connectedCallback() {
|
|
5677
|
+
this.#parseValue();
|
|
5678
|
+
this.#render();
|
|
5679
|
+
}
|
|
5680
|
+
|
|
5681
|
+
#parseValue() {
|
|
5682
|
+
const valueAttr = this.getAttribute("value");
|
|
5683
|
+
if (!valueAttr) return;
|
|
5684
|
+
try {
|
|
5685
|
+
const parsed = JSON.parse(valueAttr);
|
|
5686
|
+
if (parsed?.type === "gradient" && parsed.gradient) {
|
|
5687
|
+
this.#gradient = normalizeGradientConfig({
|
|
5688
|
+
...this.#gradient,
|
|
5689
|
+
...parsed.gradient,
|
|
5690
|
+
});
|
|
5691
|
+
return;
|
|
5692
|
+
}
|
|
5693
|
+
if (parsed?.gradient) {
|
|
5694
|
+
this.#gradient = normalizeGradientConfig({
|
|
5695
|
+
...this.#gradient,
|
|
5696
|
+
...parsed.gradient,
|
|
5697
|
+
});
|
|
5698
|
+
}
|
|
5699
|
+
} catch (e) {
|
|
5700
|
+
// Ignore invalid JSON and keep current/default gradient.
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
#buildFillPickerAttrs() {
|
|
5705
|
+
const attrs = {};
|
|
5706
|
+
const experimental = this.getAttribute("experimental");
|
|
5707
|
+
if (experimental) attrs["experimental"] = experimental;
|
|
5708
|
+
for (const { name, value } of this.attributes) {
|
|
5709
|
+
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
5710
|
+
attrs[name.slice(7)] = value;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
|
|
5714
|
+
return Object.entries(attrs)
|
|
5715
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
5716
|
+
.join(" ");
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5719
|
+
#buildStopHandles() {
|
|
5720
|
+
const disabled = this.hasAttribute("disabled");
|
|
5721
|
+
return this.#gradient.stops
|
|
5722
|
+
.map(
|
|
5723
|
+
(stop, i) =>
|
|
5724
|
+
`<fig-handle drag drag-axes="x" type="color" color="${stop.color}" value="${stop.position}% 50%" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle>`,
|
|
5725
|
+
)
|
|
5726
|
+
.join("");
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
#ghostHandle = null;
|
|
5730
|
+
#ghostTooltip = null;
|
|
5731
|
+
|
|
5732
|
+
#render() {
|
|
5733
|
+
const disabled = this.hasAttribute("disabled");
|
|
5734
|
+
const fillPickerValue = JSON.stringify(this.value);
|
|
5735
|
+
const fpAttrs = this.#buildFillPickerAttrs();
|
|
5736
|
+
this.innerHTML = `
|
|
5737
|
+
<fig-fill-picker mode="gradient" ${fpAttrs} value='${fillPickerValue}' ${
|
|
5738
|
+
disabled ? "disabled" : ""
|
|
5739
|
+
}></fig-fill-picker>
|
|
5740
|
+
<div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>`;
|
|
5741
|
+
this.#track = this.querySelector(".fig-input-gradient-track");
|
|
5742
|
+
this.#setupGhostHandle();
|
|
5743
|
+
this.#setupEventListeners();
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5746
|
+
#sampleGradientColor(position) {
|
|
5747
|
+
const canvas = document.createElement("canvas");
|
|
5748
|
+
canvas.width = 256;
|
|
5749
|
+
canvas.height = 1;
|
|
5750
|
+
const ctx = canvas.getContext("2d");
|
|
5751
|
+
const grad = ctx.createLinearGradient(0, 0, 256, 0);
|
|
5752
|
+
for (const stop of this.#gradient.stops) {
|
|
5753
|
+
try {
|
|
5754
|
+
grad.addColorStop(stop.position / 100, stop.color);
|
|
5755
|
+
} catch {
|
|
5756
|
+
/* skip invalid */
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
ctx.fillStyle = grad;
|
|
5760
|
+
ctx.fillRect(0, 0, 256, 1);
|
|
5761
|
+
const px = Math.round(Math.max(0, Math.min(255, position * 255)));
|
|
5762
|
+
const [r, g, b] = ctx.getImageData(px, 0, 1, 1).data;
|
|
5763
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
#setupGhostHandle() {
|
|
5767
|
+
if (!this.#track || this.hasAttribute("disabled")) return;
|
|
5768
|
+
const tooltip = document.createElement("fig-tooltip");
|
|
5769
|
+
tooltip.setAttribute("text", "Add color stop");
|
|
5770
|
+
tooltip.setAttribute("action", "manual");
|
|
5771
|
+
|
|
5772
|
+
const ghost = document.createElement("fig-handle");
|
|
5773
|
+
ghost.classList.add("fig-input-gradient-ghost");
|
|
5774
|
+
ghost.style.position = "absolute";
|
|
5775
|
+
ghost.style.top = "50%";
|
|
5776
|
+
ghost.style.transform = "translate(-50%, -50%)";
|
|
5777
|
+
ghost.style.pointerEvents = "none";
|
|
5778
|
+
ghost.style.opacity = "0";
|
|
5779
|
+
ghost.style.transition = "opacity 0.15s";
|
|
5780
|
+
|
|
5781
|
+
tooltip.appendChild(ghost);
|
|
5782
|
+
this.#track.appendChild(tooltip);
|
|
5783
|
+
this.#ghostHandle = ghost;
|
|
5784
|
+
this.#ghostTooltip = tooltip;
|
|
5785
|
+
|
|
5786
|
+
this.addEventListener("pointerenter", this.#onTrackEnter);
|
|
5787
|
+
this.addEventListener("pointermove", this.#onTrackMove);
|
|
5788
|
+
this.addEventListener("pointerleave", this.#onTrackLeave);
|
|
5789
|
+
this.addEventListener("click", this.#onTrackClick);
|
|
5790
|
+
}
|
|
5791
|
+
|
|
5792
|
+
#showGhost() {
|
|
5793
|
+
if (!this.#ghostHandle) return;
|
|
5794
|
+
this.#ghostHandle.style.opacity = "0.5";
|
|
5795
|
+
if (this.#ghostTooltip) {
|
|
5796
|
+
this.#ghostTooltip.render();
|
|
5797
|
+
this.#ghostTooltip.showPopup();
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
#hideGhost() {
|
|
5802
|
+
if (!this.#ghostHandle) return;
|
|
5803
|
+
this.#ghostHandle.style.opacity = "0";
|
|
5804
|
+
if (this.#ghostTooltip) this.#ghostTooltip.hidePopup();
|
|
5805
|
+
}
|
|
5806
|
+
|
|
5807
|
+
#onTrackEnter = () => {
|
|
5808
|
+
this.#showGhost();
|
|
5809
|
+
};
|
|
5810
|
+
|
|
5811
|
+
#onTrackLeave = () => {
|
|
5812
|
+
this.#hideGhost();
|
|
5813
|
+
};
|
|
5814
|
+
|
|
5815
|
+
#onTrackMove = (e) => {
|
|
5816
|
+
if (!this.#ghostHandle || !this.#track) return;
|
|
5817
|
+
if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) {
|
|
5818
|
+
this.#hideGhost();
|
|
5819
|
+
return;
|
|
5820
|
+
}
|
|
5821
|
+
const trackRect = this.#track.getBoundingClientRect();
|
|
5822
|
+
const pct = Math.max(
|
|
5823
|
+
0,
|
|
5824
|
+
Math.min(1, (e.clientX - trackRect.left) / trackRect.width),
|
|
5825
|
+
);
|
|
5826
|
+
this.#ghostHandle.style.left = `${pct * 100}%`;
|
|
5827
|
+
const color = this.#sampleGradientColor(pct);
|
|
5828
|
+
this.#ghostHandle.setAttribute("color", color);
|
|
5829
|
+
this.#showGhost();
|
|
5830
|
+
};
|
|
5831
|
+
|
|
5832
|
+
#onTrackClick = (e) => {
|
|
5833
|
+
if (!this.#track) return;
|
|
5834
|
+
if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
|
|
5835
|
+
const trackRect = this.#track.getBoundingClientRect();
|
|
5836
|
+
const pct = Math.max(
|
|
5837
|
+
0,
|
|
5838
|
+
Math.min(1, (e.clientX - trackRect.left) / trackRect.width),
|
|
5839
|
+
);
|
|
5840
|
+
const position = Math.round(pct * 100);
|
|
5841
|
+
const color = this.#sampleGradientColor(pct);
|
|
5842
|
+
this.#gradient.stops.push({ position, color, opacity: 100 });
|
|
5843
|
+
this.#gradient.stops.sort((a, b) => a.position - b.position);
|
|
5844
|
+
this.#syncHandles();
|
|
5845
|
+
this.#syncFillPicker();
|
|
5846
|
+
this.#emitInput();
|
|
5847
|
+
this.#emitChange();
|
|
5848
|
+
};
|
|
5849
|
+
|
|
5850
|
+
#syncHandles() {
|
|
5851
|
+
if (!this.#track) return;
|
|
5852
|
+
const handles = this.#track.querySelectorAll(
|
|
5853
|
+
"fig-handle:not(.fig-input-gradient-ghost)",
|
|
5854
|
+
);
|
|
5855
|
+
const stops = this.#gradient.stops;
|
|
5856
|
+
|
|
5857
|
+
if (handles.length !== stops.length) {
|
|
5858
|
+
const wrapper = this.#ghostTooltip;
|
|
5859
|
+
this.#track.innerHTML = this.#buildStopHandles();
|
|
5860
|
+
if (wrapper) this.#track.appendChild(wrapper);
|
|
5861
|
+
this.#reobserveHandleColors();
|
|
5862
|
+
return;
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
for (let i = 0; i < stops.length; i++) {
|
|
5866
|
+
const h = handles[i];
|
|
5867
|
+
const stop = stops[i];
|
|
5868
|
+
h.setAttribute("value", `${stop.position}% 50%`);
|
|
5869
|
+
h.setAttribute("color", stop.color);
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
#reobserveHandleColors() {
|
|
5874
|
+
if (!this.#colorObserver || !this.#track) return;
|
|
5875
|
+
this.#colorObserver.disconnect();
|
|
5876
|
+
this.#track
|
|
5877
|
+
.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
|
|
5878
|
+
.forEach((h) => {
|
|
5879
|
+
this.#colorObserver.observe(h, {
|
|
5880
|
+
attributes: true,
|
|
5881
|
+
attributeFilter: ["color"],
|
|
5882
|
+
});
|
|
5883
|
+
});
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
#syncFillPicker() {
|
|
5887
|
+
if (!this.#fillPicker) return;
|
|
5888
|
+
this.#fillPicker.setAttribute("value", JSON.stringify(this.value));
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
#setupEventListeners() {
|
|
5892
|
+
requestAnimationFrame(() => {
|
|
5893
|
+
this.#fillPicker = this.querySelector("fig-fill-picker");
|
|
5894
|
+
if (!this.#fillPicker) return;
|
|
5895
|
+
|
|
5896
|
+
const anchor = this.getAttribute("picker-anchor");
|
|
5897
|
+
if (!anchor || anchor === "self") {
|
|
5898
|
+
this.#fillPicker.anchorElement = this;
|
|
5899
|
+
} else {
|
|
5900
|
+
const el = document.querySelector(anchor);
|
|
5901
|
+
if (el) this.#fillPicker.anchorElement = el;
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
this.#fillPicker.addEventListener("input", (e) => {
|
|
5905
|
+
e.stopPropagation();
|
|
5906
|
+
const detail = e.detail;
|
|
5907
|
+
if (detail?.gradient) {
|
|
5908
|
+
this.#gradient = normalizeGradientConfig({
|
|
5909
|
+
...this.#gradient,
|
|
5910
|
+
...detail.gradient,
|
|
5911
|
+
});
|
|
5912
|
+
this.#syncHandles();
|
|
5913
|
+
this.#emitInput();
|
|
5914
|
+
}
|
|
5915
|
+
});
|
|
5916
|
+
|
|
5917
|
+
this.#fillPicker.addEventListener("change", (e) => {
|
|
5918
|
+
e.stopPropagation();
|
|
5919
|
+
this.#emitChange();
|
|
5920
|
+
});
|
|
5921
|
+
|
|
5922
|
+
if (this.#track) {
|
|
5923
|
+
this.#track.addEventListener("input", (e) => {
|
|
5924
|
+
const handle = e.target.closest("fig-handle");
|
|
5925
|
+
if (!handle) return;
|
|
5926
|
+
e.stopPropagation();
|
|
5927
|
+
const idx = parseInt(handle.dataset.stopIndex, 10);
|
|
5928
|
+
if (isNaN(idx) || !this.#gradient.stops[idx]) return;
|
|
5929
|
+
const px = e.detail?.px ?? 0;
|
|
5930
|
+
this.#gradient.stops[idx].position = Math.round(px * 100);
|
|
5931
|
+
this.#syncFillPicker();
|
|
5932
|
+
this.#emitInput();
|
|
5933
|
+
});
|
|
5934
|
+
|
|
5935
|
+
this.#track.addEventListener("change", (e) => {
|
|
5936
|
+
const handle = e.target.closest("fig-handle");
|
|
5937
|
+
if (!handle) return;
|
|
5938
|
+
e.stopPropagation();
|
|
5939
|
+
const idx = parseInt(handle.dataset.stopIndex, 10);
|
|
5940
|
+
if (isNaN(idx) || !this.#gradient.stops[idx]) return;
|
|
5941
|
+
const px = e.detail?.px ?? 0;
|
|
5942
|
+
this.#gradient.stops[idx].position = Math.round(px * 100);
|
|
5943
|
+
this.#syncFillPicker();
|
|
5944
|
+
this.#emitChange();
|
|
5945
|
+
});
|
|
5946
|
+
|
|
5947
|
+
this.#colorObserver = new MutationObserver((mutations) => {
|
|
5948
|
+
for (const m of mutations) {
|
|
5949
|
+
if (m.attributeName !== "color") continue;
|
|
5950
|
+
const handle = m.target;
|
|
5951
|
+
if (handle.classList.contains("fig-input-gradient-ghost")) continue;
|
|
5952
|
+
const idx = parseInt(handle.dataset.stopIndex, 10);
|
|
5953
|
+
if (isNaN(idx) || !this.#gradient.stops[idx]) continue;
|
|
5954
|
+
const newColor = handle.getAttribute("color");
|
|
5955
|
+
if (newColor && newColor !== this.#gradient.stops[idx].color) {
|
|
5956
|
+
this.#gradient.stops[idx].color = newColor;
|
|
5957
|
+
this.#syncFillPicker();
|
|
5958
|
+
this.#emitInput();
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
});
|
|
5962
|
+
this.#track
|
|
5963
|
+
.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
|
|
5964
|
+
.forEach((h) => {
|
|
5965
|
+
this.#colorObserver.observe(h, {
|
|
5966
|
+
attributes: true,
|
|
5967
|
+
attributeFilter: ["color"],
|
|
5968
|
+
});
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5973
|
+
|
|
5974
|
+
#emitInput() {
|
|
5975
|
+
this.dispatchEvent(
|
|
5976
|
+
new CustomEvent("input", {
|
|
5977
|
+
bubbles: true,
|
|
5978
|
+
detail: this.value,
|
|
5979
|
+
}),
|
|
5980
|
+
);
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5983
|
+
#emitChange() {
|
|
5984
|
+
this.dispatchEvent(
|
|
5985
|
+
new CustomEvent("change", {
|
|
5986
|
+
bubbles: true,
|
|
5987
|
+
detail: this.value,
|
|
5988
|
+
}),
|
|
5989
|
+
);
|
|
5990
|
+
}
|
|
5991
|
+
|
|
5992
|
+
get value() {
|
|
5993
|
+
return {
|
|
5994
|
+
type: "gradient",
|
|
5995
|
+
gradient: gradientToValueShape(this.#gradient),
|
|
5996
|
+
};
|
|
5997
|
+
}
|
|
5998
|
+
|
|
5999
|
+
set value(val) {
|
|
6000
|
+
if (typeof val === "string") {
|
|
6001
|
+
this.setAttribute("value", val);
|
|
6002
|
+
} else {
|
|
6003
|
+
this.setAttribute("value", JSON.stringify(val));
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
|
|
6007
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
6008
|
+
if (oldValue === newValue) return;
|
|
6009
|
+
switch (name) {
|
|
6010
|
+
case "value":
|
|
6011
|
+
this.#parseValue();
|
|
6012
|
+
if (this.#fillPicker) {
|
|
6013
|
+
this.#syncFillPicker();
|
|
6014
|
+
}
|
|
6015
|
+
this.#syncHandles();
|
|
6016
|
+
break;
|
|
6017
|
+
case "disabled":
|
|
6018
|
+
case "experimental":
|
|
6019
|
+
case "picker-anchor":
|
|
6020
|
+
if (this.#fillPicker) this.#render();
|
|
6021
|
+
break;
|
|
6022
|
+
}
|
|
6023
|
+
}
|
|
6024
|
+
}
|
|
6025
|
+
customElements.define("fig-input-gradient", FigInputGradient);
|
|
6026
|
+
|
|
5485
6027
|
/* Checkbox */
|
|
5486
6028
|
/**
|
|
5487
6029
|
* A custom checkbox input element.
|
|
@@ -7150,7 +7692,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
7150
7692
|
);
|
|
7151
7693
|
if (bezierSurface) {
|
|
7152
7694
|
bezierSurface.addEventListener("pointerdown", (e) => {
|
|
7153
|
-
if (e.target?.closest?.(".fig-easing-curve-handle, fig-handle"))
|
|
7695
|
+
if (e.target?.closest?.(".fig-easing-curve-handle, fig-handle"))
|
|
7696
|
+
return;
|
|
7154
7697
|
this.#startBezierDrag(e, this.#bezierHandleForClientHalf(e));
|
|
7155
7698
|
});
|
|
7156
7699
|
}
|
|
@@ -7169,7 +7712,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
7169
7712
|
);
|
|
7170
7713
|
if (springSurface) {
|
|
7171
7714
|
springSurface.addEventListener("pointerdown", (e) => {
|
|
7172
|
-
if (e.target?.closest?.(".fig-easing-curve-handle, fig-handle"))
|
|
7715
|
+
if (e.target?.closest?.(".fig-easing-curve-handle, fig-handle"))
|
|
7716
|
+
return;
|
|
7173
7717
|
this.#startSpringDrag(e, "duration");
|
|
7174
7718
|
});
|
|
7175
7719
|
}
|
|
@@ -8146,10 +8690,10 @@ customElements.define("fig-origin-grid", FigOriginGrid);
|
|
|
8146
8690
|
* @attr {string} axis-labels - Space-delimited labels. 1 token: top. 2 tokens: x y. 4 tokens: left right top bottom.
|
|
8147
8691
|
*/
|
|
8148
8692
|
class FigInputJoystick extends HTMLElement {
|
|
8149
|
-
#
|
|
8150
|
-
#
|
|
8151
|
-
#
|
|
8152
|
-
#
|
|
8693
|
+
#boundPlanePointerDown = null;
|
|
8694
|
+
#boundHandlePointerDown = null;
|
|
8695
|
+
#boundHandleInput = null;
|
|
8696
|
+
#boundHandleChange = null;
|
|
8153
8697
|
#boundXInput = null;
|
|
8154
8698
|
#boundYInput = null;
|
|
8155
8699
|
#boundXFocusOut = null;
|
|
@@ -8162,17 +8706,19 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8162
8706
|
|
|
8163
8707
|
this.position = { x: 0.5, y: 0.5 };
|
|
8164
8708
|
this.isDragging = false;
|
|
8165
|
-
this.isShiftHeld = false;
|
|
8166
8709
|
this.plane = null;
|
|
8167
8710
|
this.cursor = null;
|
|
8168
8711
|
this.xInput = null;
|
|
8169
8712
|
this.yInput = null;
|
|
8170
8713
|
this.coordinates = "screen";
|
|
8171
8714
|
this.#initialized = false;
|
|
8172
|
-
this.#
|
|
8173
|
-
this.#
|
|
8174
|
-
|
|
8175
|
-
|
|
8715
|
+
this.#boundPlanePointerDown = (e) => this.#handlePlanePointerDown(e);
|
|
8716
|
+
this.#boundHandlePointerDown = () => {
|
|
8717
|
+
this.isDragging = true;
|
|
8718
|
+
this.plane?.classList.add("dragging");
|
|
8719
|
+
};
|
|
8720
|
+
this.#boundHandleInput = (e) => this.#handleHandleInput(e);
|
|
8721
|
+
this.#boundHandleChange = () => this.#handleHandleChange();
|
|
8176
8722
|
this.#boundXInput = (e) => this.#handleXInput(e);
|
|
8177
8723
|
this.#boundYInput = (e) => this.#handleYInput(e);
|
|
8178
8724
|
this.#boundXFocusOut = () => this.#handleFieldFocusOut();
|
|
@@ -8278,7 +8824,7 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8278
8824
|
${labelsMarkup}
|
|
8279
8825
|
<div class="fig-input-joystick-plane">
|
|
8280
8826
|
<div class="fig-input-joystick-guides"></div>
|
|
8281
|
-
<fig-handle></fig-handle>
|
|
8827
|
+
<fig-handle drag drag-surface=".fig-input-joystick-plane" drag-axes="x,y" drag-snapping="modifier"></fig-handle>
|
|
8282
8828
|
</div>
|
|
8283
8829
|
<fig-tooltip text="Reset">
|
|
8284
8830
|
<fig-button variant="ghost" icon="true" class="fig-joystick-reset" aria-label="Reset to default">
|
|
@@ -8318,10 +8864,10 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8318
8864
|
this.cursor = this.querySelector("fig-handle");
|
|
8319
8865
|
this.xInput = this.querySelector("fig-input-number[name='x']");
|
|
8320
8866
|
this.yInput = this.querySelector("fig-input-number[name='y']");
|
|
8321
|
-
this.plane
|
|
8322
|
-
this.
|
|
8323
|
-
|
|
8324
|
-
|
|
8867
|
+
this.plane?.addEventListener("pointerdown", this.#boundPlanePointerDown);
|
|
8868
|
+
this.cursor?.addEventListener("pointerdown", this.#boundHandlePointerDown);
|
|
8869
|
+
this.cursor?.addEventListener("input", this.#boundHandleInput);
|
|
8870
|
+
this.cursor?.addEventListener("change", this.#boundHandleChange);
|
|
8325
8871
|
const resetBtn = this.querySelector(".fig-joystick-reset");
|
|
8326
8872
|
if (resetBtn) {
|
|
8327
8873
|
resetBtn.addEventListener("click", () => this.#resetToDefault());
|
|
@@ -8337,12 +8883,15 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8337
8883
|
}
|
|
8338
8884
|
|
|
8339
8885
|
#cleanupListeners() {
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8886
|
+
this.plane?.removeEventListener("pointerdown", this.#boundPlanePointerDown);
|
|
8887
|
+
this.cursor?.removeEventListener(
|
|
8888
|
+
"pointerdown",
|
|
8889
|
+
this.#boundHandlePointerDown,
|
|
8890
|
+
);
|
|
8891
|
+
this.cursor?.removeEventListener("input", this.#boundHandleInput);
|
|
8892
|
+
this.cursor?.removeEventListener("change", this.#boundHandleChange);
|
|
8893
|
+
this.plane?.classList.remove("dragging");
|
|
8894
|
+
this.isDragging = false;
|
|
8346
8895
|
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
8347
8896
|
this.xInput.removeEventListener("input", this.#boundXInput);
|
|
8348
8897
|
this.xInput.removeEventListener("change", this.#boundXInput);
|
|
@@ -8376,49 +8925,41 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8376
8925
|
this.#emitChangeEvent();
|
|
8377
8926
|
}
|
|
8378
8927
|
|
|
8379
|
-
#
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8928
|
+
#applyScreenPosition(screenX, screenY, { syncHandle = true } = {}) {
|
|
8929
|
+
const x = Math.max(0, Math.min(1, screenX));
|
|
8930
|
+
const yScreen = Math.max(0, Math.min(1, screenY));
|
|
8931
|
+
const y = this.coordinates === "math" ? 1 - yScreen : yScreen;
|
|
8932
|
+
this.position = { x, y };
|
|
8933
|
+
if (syncHandle) this.#syncHandlePosition();
|
|
8934
|
+
this.#syncValueAttribute();
|
|
8385
8935
|
}
|
|
8386
8936
|
|
|
8387
|
-
#
|
|
8388
|
-
if (!this.
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8937
|
+
#handlePlanePointerDown(e) {
|
|
8938
|
+
if (!this.plane || !this.cursor) return;
|
|
8939
|
+
if (e.target?.closest?.(".fig-joystick-reset, fig-tooltip, fig-handle"))
|
|
8940
|
+
return;
|
|
8941
|
+
const rect = this.plane.getBoundingClientRect();
|
|
8942
|
+
const screenX = rect.width > 0 ? (e.clientX - rect.left) / rect.width : 0.5;
|
|
8943
|
+
const screenY =
|
|
8944
|
+
rect.height > 0 ? (e.clientY - rect.top) / rect.height : 0.5;
|
|
8945
|
+
this.cursor.value = `${Math.round(screenX * 100)}% ${Math.round(screenY * 100)}%`;
|
|
8946
|
+
this.#applyScreenPosition(screenX, screenY, { syncHandle: false });
|
|
8947
|
+
this.#emitInputEvent();
|
|
8948
|
+
this.#emitChangeEvent();
|
|
8393
8949
|
}
|
|
8394
8950
|
|
|
8395
|
-
#
|
|
8396
|
-
const
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
);
|
|
8402
|
-
|
|
8403
|
-
// Convert screen Y to internal Y (flip for math coordinates)
|
|
8404
|
-
let y = this.coordinates === "math" ? 1 - screenY : screenY;
|
|
8405
|
-
|
|
8406
|
-
x = this.#snapToGuide(x);
|
|
8407
|
-
y = this.#snapToGuide(y);
|
|
8408
|
-
|
|
8409
|
-
const snapped = this.#snapToDiagonal(x, y);
|
|
8410
|
-
this.position = snapped;
|
|
8411
|
-
|
|
8412
|
-
const displayY = this.#displayY(snapped.y);
|
|
8413
|
-
this.cursor.style.left = `${snapped.x * 100}%`;
|
|
8414
|
-
this.cursor.style.top = `${displayY * 100}%`;
|
|
8415
|
-
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
8416
|
-
this.xInput.setAttribute("value", Math.round(snapped.x * 100));
|
|
8417
|
-
this.yInput.setAttribute("value", Math.round(snapped.y * 100));
|
|
8418
|
-
}
|
|
8951
|
+
#handleHandleInput(e) {
|
|
8952
|
+
const detail = e.detail ?? {};
|
|
8953
|
+
if (typeof detail.px !== "number" || typeof detail.py !== "number") return;
|
|
8954
|
+
this.#applyScreenPosition(detail.px, detail.py, { syncHandle: false });
|
|
8955
|
+
this.#emitInputEvent();
|
|
8956
|
+
}
|
|
8419
8957
|
|
|
8958
|
+
#handleHandleChange() {
|
|
8959
|
+
this.isDragging = false;
|
|
8960
|
+
this.plane?.classList.remove("dragging");
|
|
8420
8961
|
this.#syncValueAttribute();
|
|
8421
|
-
this.#
|
|
8962
|
+
this.#emitChangeEvent();
|
|
8422
8963
|
}
|
|
8423
8964
|
|
|
8424
8965
|
#emitInputEvent() {
|
|
@@ -8442,8 +8983,7 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8442
8983
|
#syncHandlePosition() {
|
|
8443
8984
|
const displayY = this.#displayY(this.position.y);
|
|
8444
8985
|
if (this.cursor) {
|
|
8445
|
-
this.cursor.
|
|
8446
|
-
this.cursor.style.top = `${displayY * 100}%`;
|
|
8986
|
+
this.cursor.value = `${this.position.x * 100}% ${displayY * 100}%`;
|
|
8447
8987
|
}
|
|
8448
8988
|
// Also sync text inputs if they exist (convert to percentage 0-100)
|
|
8449
8989
|
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
@@ -8479,64 +9019,6 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8479
9019
|
this.#emitChangeEvent();
|
|
8480
9020
|
}
|
|
8481
9021
|
|
|
8482
|
-
#handleMouseDown(e) {
|
|
8483
|
-
if (e.target.closest(".fig-joystick-reset, fig-tooltip")) return;
|
|
8484
|
-
this.isDragging = true;
|
|
8485
|
-
|
|
8486
|
-
this.#updatePosition(e);
|
|
8487
|
-
|
|
8488
|
-
this.plane.style.cursor = "grabbing";
|
|
8489
|
-
|
|
8490
|
-
const handleMouseMove = (e) => {
|
|
8491
|
-
this.plane.classList.add("dragging");
|
|
8492
|
-
if (this.isDragging) this.#updatePosition(e);
|
|
8493
|
-
};
|
|
8494
|
-
|
|
8495
|
-
const handleMouseUp = () => {
|
|
8496
|
-
this.isDragging = false;
|
|
8497
|
-
this.plane.classList.remove("dragging");
|
|
8498
|
-
this.plane.style.cursor = "";
|
|
8499
|
-
window.removeEventListener("mousemove", handleMouseMove);
|
|
8500
|
-
window.removeEventListener("mouseup", handleMouseUp);
|
|
8501
|
-
this.#syncValueAttribute();
|
|
8502
|
-
this.#emitChangeEvent();
|
|
8503
|
-
};
|
|
8504
|
-
|
|
8505
|
-
window.addEventListener("mousemove", handleMouseMove);
|
|
8506
|
-
window.addEventListener("mouseup", handleMouseUp);
|
|
8507
|
-
}
|
|
8508
|
-
|
|
8509
|
-
#handleTouchStart(e) {
|
|
8510
|
-
if (e.target.closest(".fig-joystick-reset, fig-tooltip")) return;
|
|
8511
|
-
e.preventDefault();
|
|
8512
|
-
this.isDragging = true;
|
|
8513
|
-
this.#updatePosition(e.touches[0]);
|
|
8514
|
-
|
|
8515
|
-
const handleTouchMove = (e) => {
|
|
8516
|
-
this.plane.classList.add("dragging");
|
|
8517
|
-
if (this.isDragging) this.#updatePosition(e.touches[0]);
|
|
8518
|
-
};
|
|
8519
|
-
|
|
8520
|
-
const handleTouchEnd = () => {
|
|
8521
|
-
this.isDragging = false;
|
|
8522
|
-
this.plane.classList.remove("dragging");
|
|
8523
|
-
window.removeEventListener("touchmove", handleTouchMove);
|
|
8524
|
-
window.removeEventListener("touchend", handleTouchEnd);
|
|
8525
|
-
this.#syncValueAttribute();
|
|
8526
|
-
this.#emitChangeEvent();
|
|
8527
|
-
};
|
|
8528
|
-
|
|
8529
|
-
window.addEventListener("touchmove", handleTouchMove);
|
|
8530
|
-
window.addEventListener("touchend", handleTouchEnd);
|
|
8531
|
-
}
|
|
8532
|
-
|
|
8533
|
-
#handleKeyDown(e) {
|
|
8534
|
-
if (e.key === "Shift") this.isShiftHeld = true;
|
|
8535
|
-
}
|
|
8536
|
-
|
|
8537
|
-
#handleKeyUp(e) {
|
|
8538
|
-
if (e.key === "Shift") this.isShiftHeld = false;
|
|
8539
|
-
}
|
|
8540
9022
|
focus() {
|
|
8541
9023
|
const container = this.querySelector(".fig-input-joystick-plane-container");
|
|
8542
9024
|
container?.focus();
|
|
@@ -8620,9 +9102,10 @@ customElements.define("fig-joystick", FigInputJoystick);
|
|
|
8620
9102
|
* @attr {number} value - The current angle of the handle in degrees.
|
|
8621
9103
|
* @attr {number} precision - The number of decimal places for the output.
|
|
8622
9104
|
* @attr {boolean} text - Whether to display a text input for the angle value.
|
|
9105
|
+
* @attr {boolean} dial - Whether to display the circular dial control. Defaults to true.
|
|
8623
9106
|
* @attr {number} adjacent - The adjacent value of the angle.
|
|
8624
9107
|
* @attr {number} opposite - The opposite value of the angle.
|
|
8625
|
-
* @attr {boolean}
|
|
9108
|
+
* @attr {boolean} rotations - Whether to display a rotation count (×N) when rotations > 1. Defaults to false.
|
|
8626
9109
|
*/
|
|
8627
9110
|
class FigInputAngle extends HTMLElement {
|
|
8628
9111
|
// Private fields
|
|
@@ -8645,6 +9128,7 @@ class FigInputAngle extends HTMLElement {
|
|
|
8645
9128
|
this.units = "°";
|
|
8646
9129
|
this.min = null;
|
|
8647
9130
|
this.max = null;
|
|
9131
|
+
this.dial = true;
|
|
8648
9132
|
this.showRotations = false;
|
|
8649
9133
|
this.rotationSpan = null;
|
|
8650
9134
|
|
|
@@ -8667,7 +9151,8 @@ class FigInputAngle extends HTMLElement {
|
|
|
8667
9151
|
this.max = this.hasAttribute("max")
|
|
8668
9152
|
? Number(this.getAttribute("max"))
|
|
8669
9153
|
: null;
|
|
8670
|
-
this.
|
|
9154
|
+
this.dial = this.#readBooleanAttribute("dial", true);
|
|
9155
|
+
this.showRotations = this.#readRotationsEnabled();
|
|
8671
9156
|
|
|
8672
9157
|
this.#render();
|
|
8673
9158
|
this.#setupListeners();
|
|
@@ -8690,14 +9175,38 @@ class FigInputAngle extends HTMLElement {
|
|
|
8690
9175
|
this.innerHTML = this.#getInnerHTML();
|
|
8691
9176
|
}
|
|
8692
9177
|
|
|
9178
|
+
#readBooleanAttribute(name, defaultValue = false) {
|
|
9179
|
+
const value = this.getAttribute(name);
|
|
9180
|
+
if (value === null) return defaultValue;
|
|
9181
|
+
const normalized = value.trim().toLowerCase();
|
|
9182
|
+
if (normalized === "" || normalized === "true") return true;
|
|
9183
|
+
if (normalized === "false") return false;
|
|
9184
|
+
return true;
|
|
9185
|
+
}
|
|
9186
|
+
|
|
9187
|
+
#readRotationsEnabled() {
|
|
9188
|
+
if (this.hasAttribute("rotations")) {
|
|
9189
|
+
return this.#readBooleanAttribute("rotations", false);
|
|
9190
|
+
}
|
|
9191
|
+
// Backward-compat alias
|
|
9192
|
+
if (this.hasAttribute("show-rotations")) {
|
|
9193
|
+
return this.#readBooleanAttribute("show-rotations", false);
|
|
9194
|
+
}
|
|
9195
|
+
return false;
|
|
9196
|
+
}
|
|
9197
|
+
|
|
8693
9198
|
#getInnerHTML() {
|
|
8694
9199
|
const step = this.#getStepForUnit();
|
|
8695
9200
|
const minAttr = this.min !== null ? `min="${this.min}"` : "";
|
|
8696
9201
|
const maxAttr = this.max !== null ? `max="${this.max}"` : "";
|
|
8697
9202
|
return `
|
|
8698
|
-
|
|
9203
|
+
${
|
|
9204
|
+
this.dial
|
|
9205
|
+
? `<div class="fig-input-angle-plane" tabindex="0">
|
|
8699
9206
|
<div class="fig-input-angle-handle"></div>
|
|
8700
|
-
</div
|
|
9207
|
+
</div>`
|
|
9208
|
+
: ""
|
|
9209
|
+
}
|
|
8701
9210
|
${
|
|
8702
9211
|
this.text
|
|
8703
9212
|
? `<fig-input-number
|
|
@@ -8798,8 +9307,8 @@ class FigInputAngle extends HTMLElement {
|
|
|
8798
9307
|
this.angleInput = this.querySelector("fig-input-number[name='angle']");
|
|
8799
9308
|
this.rotationSpan = this.querySelector(".fig-input-angle-rotations");
|
|
8800
9309
|
this.#updateRotationDisplay();
|
|
8801
|
-
this.plane
|
|
8802
|
-
this.plane
|
|
9310
|
+
this.plane?.addEventListener("mousedown", this.#handleMouseDown.bind(this));
|
|
9311
|
+
this.plane?.addEventListener(
|
|
8803
9312
|
"touchstart",
|
|
8804
9313
|
this.#handleTouchStart.bind(this),
|
|
8805
9314
|
);
|
|
@@ -9025,6 +9534,8 @@ class FigInputAngle extends HTMLElement {
|
|
|
9025
9534
|
"min",
|
|
9026
9535
|
"max",
|
|
9027
9536
|
"units",
|
|
9537
|
+
"dial",
|
|
9538
|
+
"rotations",
|
|
9028
9539
|
"show-rotations",
|
|
9029
9540
|
];
|
|
9030
9541
|
}
|
|
@@ -9067,18 +9578,26 @@ class FigInputAngle extends HTMLElement {
|
|
|
9067
9578
|
case "text":
|
|
9068
9579
|
if (newValue !== oldValue) {
|
|
9069
9580
|
this.text = newValue?.toLowerCase() === "true";
|
|
9070
|
-
if (this.
|
|
9581
|
+
if (this.isConnected) {
|
|
9071
9582
|
this.#render();
|
|
9072
9583
|
this.#setupListeners();
|
|
9073
9584
|
this.#syncHandlePosition();
|
|
9074
9585
|
}
|
|
9075
9586
|
}
|
|
9076
9587
|
break;
|
|
9588
|
+
case "dial":
|
|
9589
|
+
this.dial = this.#readBooleanAttribute("dial", true);
|
|
9590
|
+
if (this.isConnected) {
|
|
9591
|
+
this.#render();
|
|
9592
|
+
this.#setupListeners();
|
|
9593
|
+
this.#syncHandlePosition();
|
|
9594
|
+
}
|
|
9595
|
+
break;
|
|
9077
9596
|
case "units": {
|
|
9078
9597
|
let units = newValue || "°";
|
|
9079
9598
|
if (units === "deg") units = "°";
|
|
9080
9599
|
this.units = units;
|
|
9081
|
-
if (this.
|
|
9600
|
+
if (this.isConnected) {
|
|
9082
9601
|
this.#render();
|
|
9083
9602
|
this.#setupListeners();
|
|
9084
9603
|
this.#syncHandlePosition();
|
|
@@ -9087,7 +9606,7 @@ class FigInputAngle extends HTMLElement {
|
|
|
9087
9606
|
}
|
|
9088
9607
|
case "min":
|
|
9089
9608
|
this.min = newValue !== null ? Number(newValue) : null;
|
|
9090
|
-
if (this.
|
|
9609
|
+
if (this.isConnected) {
|
|
9091
9610
|
this.#render();
|
|
9092
9611
|
this.#setupListeners();
|
|
9093
9612
|
this.#syncHandlePosition();
|
|
@@ -9095,15 +9614,16 @@ class FigInputAngle extends HTMLElement {
|
|
|
9095
9614
|
break;
|
|
9096
9615
|
case "max":
|
|
9097
9616
|
this.max = newValue !== null ? Number(newValue) : null;
|
|
9098
|
-
if (this.
|
|
9617
|
+
if (this.isConnected) {
|
|
9099
9618
|
this.#render();
|
|
9100
9619
|
this.#setupListeners();
|
|
9101
9620
|
this.#syncHandlePosition();
|
|
9102
9621
|
}
|
|
9103
9622
|
break;
|
|
9623
|
+
case "rotations":
|
|
9104
9624
|
case "show-rotations":
|
|
9105
|
-
this.showRotations =
|
|
9106
|
-
if (this.
|
|
9625
|
+
this.showRotations = this.#readRotationsEnabled();
|
|
9626
|
+
if (this.isConnected) {
|
|
9107
9627
|
this.#render();
|
|
9108
9628
|
this.#setupListeners();
|
|
9109
9629
|
this.#syncHandlePosition();
|
|
@@ -9148,6 +9668,10 @@ class FigShimmer extends HTMLElement {
|
|
|
9148
9668
|
}
|
|
9149
9669
|
customElements.define("fig-shimmer", FigShimmer);
|
|
9150
9670
|
|
|
9671
|
+
// FigSkeleton
|
|
9672
|
+
class FigSkeleton extends FigShimmer {}
|
|
9673
|
+
customElements.define("fig-skeleton", FigSkeleton);
|
|
9674
|
+
|
|
9151
9675
|
// FigLayer
|
|
9152
9676
|
class FigLayer extends HTMLElement {
|
|
9153
9677
|
static get observedAttributes() {
|
|
@@ -9281,12 +9805,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
9281
9805
|
|
|
9282
9806
|
// Fill state
|
|
9283
9807
|
#fillType = "solid";
|
|
9808
|
+
#gamut = "srgb"; // "srgb" or "display-p3"
|
|
9284
9809
|
#color = { h: 0, s: 0, v: 85, a: 1 }; // Default gray #D9D9D9
|
|
9810
|
+
#colorInputMode = "hex";
|
|
9285
9811
|
#gradient = {
|
|
9286
9812
|
type: "linear",
|
|
9287
9813
|
angle: 0,
|
|
9288
9814
|
centerX: 50,
|
|
9289
9815
|
centerY: 50,
|
|
9816
|
+
interpolationSpace: "oklab",
|
|
9817
|
+
hueInterpolation: "shorter",
|
|
9290
9818
|
stops: [
|
|
9291
9819
|
{ position: 0, color: "#D9D9D9", opacity: 100 },
|
|
9292
9820
|
{ position: 100, color: "#737373", opacity: 100 },
|
|
@@ -9306,6 +9834,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
9306
9834
|
#hueSlider = null;
|
|
9307
9835
|
#opacitySlider = null;
|
|
9308
9836
|
#isDraggingColor = false;
|
|
9837
|
+
#teardownColorAreaEvents = null;
|
|
9309
9838
|
|
|
9310
9839
|
constructor() {
|
|
9311
9840
|
super();
|
|
@@ -9327,6 +9856,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
9327
9856
|
}
|
|
9328
9857
|
|
|
9329
9858
|
disconnectedCallback() {
|
|
9859
|
+
if (this.#teardownColorAreaEvents) {
|
|
9860
|
+
this.#teardownColorAreaEvents();
|
|
9861
|
+
this.#teardownColorAreaEvents = null;
|
|
9862
|
+
}
|
|
9330
9863
|
if (this.#dialog) {
|
|
9331
9864
|
this.#dialog.close();
|
|
9332
9865
|
this.#dialog.remove();
|
|
@@ -9396,8 +9929,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
9396
9929
|
if (parsed.opacity !== undefined) {
|
|
9397
9930
|
this.#color.a = parsed.opacity / 100;
|
|
9398
9931
|
}
|
|
9399
|
-
if (parsed.
|
|
9400
|
-
this.#
|
|
9932
|
+
if (parsed.colorSpace === "display-p3" || parsed.colorSpace === "srgb") {
|
|
9933
|
+
this.#gamut = parsed.colorSpace;
|
|
9934
|
+
}
|
|
9935
|
+
if (parsed.gradient) {
|
|
9936
|
+
this.#gradient = normalizeGradientConfig({
|
|
9937
|
+
...this.#gradient,
|
|
9938
|
+
...parsed.gradient,
|
|
9939
|
+
});
|
|
9940
|
+
}
|
|
9401
9941
|
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
9402
9942
|
if (parsed.video) this.#video = { ...this.#video, ...parsed.video };
|
|
9403
9943
|
|
|
@@ -9493,6 +10033,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
9493
10033
|
}
|
|
9494
10034
|
|
|
9495
10035
|
this.#switchTab(this.#fillType);
|
|
10036
|
+
|
|
10037
|
+
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
10038
|
+
if (gamutEl) gamutEl.value = this.#gamut;
|
|
10039
|
+
|
|
9496
10040
|
this.#dialog.open = true;
|
|
9497
10041
|
|
|
9498
10042
|
requestAnimationFrame(() => {
|
|
@@ -9580,16 +10124,22 @@ class FigFillPicker extends HTMLElement {
|
|
|
9580
10124
|
.map((m) => `<div class="fig-fill-picker-tab" data-tab="${m}"></div>`)
|
|
9581
10125
|
.join("\n ");
|
|
9582
10126
|
|
|
10127
|
+
const gamutDropdown = `<fig-dropdown class="fig-fill-picker-gamut" ${expAttr} value="${this.#gamut}">
|
|
10128
|
+
<option value="srgb">sRGB</option>
|
|
10129
|
+
<option value="display-p3">Display P3</option>
|
|
10130
|
+
</fig-dropdown>`;
|
|
10131
|
+
|
|
9583
10132
|
this.#dialog.innerHTML = `
|
|
9584
10133
|
<fig-header>
|
|
9585
10134
|
${headerContent}
|
|
10135
|
+
${gamutDropdown}
|
|
9586
10136
|
<fig-button icon variant="ghost" class="fig-fill-picker-close">
|
|
9587
10137
|
<span class="fig-mask-icon" style="--icon: var(--icon-close)"></span>
|
|
9588
10138
|
</fig-button>
|
|
9589
10139
|
</fig-header>
|
|
9590
|
-
<
|
|
10140
|
+
<fig-content>
|
|
9591
10141
|
${tabDivs}
|
|
9592
|
-
</
|
|
10142
|
+
</fig-content>
|
|
9593
10143
|
`;
|
|
9594
10144
|
|
|
9595
10145
|
document.body.appendChild(this.#dialog);
|
|
@@ -9621,6 +10171,20 @@ class FigFillPicker extends HTMLElement {
|
|
|
9621
10171
|
});
|
|
9622
10172
|
}
|
|
9623
10173
|
|
|
10174
|
+
// Setup gamut dropdown
|
|
10175
|
+
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
10176
|
+
if (gamutEl) {
|
|
10177
|
+
const handleGamutChange = (e) => {
|
|
10178
|
+
const val = e.currentTarget?.value ?? e.target?.value ?? e.detail;
|
|
10179
|
+
if (val && val !== this.#gamut) {
|
|
10180
|
+
this.#gamut = val;
|
|
10181
|
+
this.#onGamutChange();
|
|
10182
|
+
}
|
|
10183
|
+
};
|
|
10184
|
+
gamutEl.addEventListener("input", handleGamutChange);
|
|
10185
|
+
gamutEl.addEventListener("change", handleGamutChange);
|
|
10186
|
+
}
|
|
10187
|
+
|
|
9624
10188
|
this.#dialog
|
|
9625
10189
|
.querySelector(".fig-fill-picker-close")
|
|
9626
10190
|
.addEventListener("click", () => {
|
|
@@ -9690,7 +10254,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
9690
10254
|
});
|
|
9691
10255
|
|
|
9692
10256
|
// Zero out content padding for custom mode tabs
|
|
9693
|
-
const contentEl = this.#dialog.querySelector("
|
|
10257
|
+
const contentEl = this.#dialog.querySelector("fig-content");
|
|
9694
10258
|
if (contentEl) {
|
|
9695
10259
|
contentEl.style.padding = this.#customSlots[tabName] ? "0" : "";
|
|
9696
10260
|
}
|
|
@@ -9711,13 +10275,27 @@ class FigFillPicker extends HTMLElement {
|
|
|
9711
10275
|
#initSolidTab() {
|
|
9712
10276
|
const container = this.#dialog.querySelector('[data-tab="solid"]');
|
|
9713
10277
|
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
10278
|
+
const experimental = this.getAttribute("experimental");
|
|
10279
|
+
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
10280
|
+
const savedMode = localStorage.getItem("figui-color-input-mode");
|
|
10281
|
+
if (
|
|
10282
|
+
savedMode &&
|
|
10283
|
+
["hex", "rgb", "hsl", "hsb", "lab", "lch"].includes(savedMode)
|
|
10284
|
+
) {
|
|
10285
|
+
this.#colorInputMode = savedMode;
|
|
10286
|
+
}
|
|
9714
10287
|
|
|
9715
10288
|
container.innerHTML = `
|
|
9716
|
-
<
|
|
10289
|
+
<fig-preview class="fig-fill-picker-color-area">
|
|
9717
10290
|
<canvas width="200" height="200"></canvas>
|
|
9718
|
-
<
|
|
9719
|
-
|
|
10291
|
+
<fig-handle
|
|
10292
|
+
drag
|
|
10293
|
+
drag-surface=".fig-fill-picker-color-area"
|
|
10294
|
+
drag-axes="x,y"
|
|
10295
|
+
></fig-handle>
|
|
10296
|
+
</fig-preview>
|
|
9720
10297
|
<div class="fig-fill-picker-sliders">
|
|
10298
|
+
<fig-tooltip text="Sample color"><fig-button icon variant="ghost" class="fig-fill-picker-eyedropper"><span class="fig-mask-icon" style="--icon: var(--icon-eyedropper)"></span></fig-button></fig-tooltip>
|
|
9721
10299
|
<fig-slider type="hue" variant="neue" min="0" max="360" value="${
|
|
9722
10300
|
this.#color.h
|
|
9723
10301
|
}"></fig-slider>
|
|
@@ -9729,17 +10307,22 @@ class FigFillPicker extends HTMLElement {
|
|
|
9729
10307
|
: ""
|
|
9730
10308
|
}
|
|
9731
10309
|
</div>
|
|
9732
|
-
<
|
|
9733
|
-
<fig-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
10310
|
+
<fig-field class="fig-fill-picker-inputs" direction="horizontal">
|
|
10311
|
+
<fig-dropdown class="fig-fill-picker-input-mode" ${expAttr} value="${this.#colorInputMode}">
|
|
10312
|
+
<option value="hex">Hex</option>
|
|
10313
|
+
<option value="rgb">RGB</option>
|
|
10314
|
+
<option value="hsl">HSL</option>
|
|
10315
|
+
<option value="hsb">HSB</option>
|
|
10316
|
+
<option value="lab">LAB</option>
|
|
10317
|
+
<option value="lch">LCH</option>
|
|
10318
|
+
</fig-dropdown>
|
|
10319
|
+
<span class="fig-fill-picker-input-fields"></span>
|
|
10320
|
+
</fig-field>
|
|
9738
10321
|
`;
|
|
9739
10322
|
|
|
9740
10323
|
// Setup color area
|
|
9741
10324
|
this.#colorArea = container.querySelector("canvas");
|
|
9742
|
-
this.#colorAreaHandle = container.querySelector("
|
|
10325
|
+
this.#colorAreaHandle = container.querySelector("fig-handle");
|
|
9743
10326
|
this.#drawColorArea();
|
|
9744
10327
|
this.#updateHandlePosition();
|
|
9745
10328
|
this.#setupColorAreaEvents();
|
|
@@ -9772,25 +10355,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
9772
10355
|
});
|
|
9773
10356
|
}
|
|
9774
10357
|
|
|
9775
|
-
// Setup color input
|
|
9776
|
-
const
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
const hex = e.target.value;
|
|
9782
|
-
this.#color = { ...this.#hexToHSV(hex), a: this.#color.a };
|
|
9783
|
-
this.#drawColorArea();
|
|
9784
|
-
this.#updateHandlePosition();
|
|
9785
|
-
if (this.#hueSlider) {
|
|
9786
|
-
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
9787
|
-
}
|
|
9788
|
-
this.#emitInput();
|
|
9789
|
-
});
|
|
9790
|
-
colorInput.addEventListener("change", () => {
|
|
9791
|
-
this.#emitChange();
|
|
10358
|
+
// Setup color input mode dropdown
|
|
10359
|
+
const modeDropdown = container.querySelector(".fig-fill-picker-input-mode");
|
|
10360
|
+
modeDropdown.addEventListener("input", (e) => {
|
|
10361
|
+
this.#colorInputMode = e.target.value;
|
|
10362
|
+
localStorage.setItem("figui-color-input-mode", this.#colorInputMode);
|
|
10363
|
+
this.#rebuildColorInputFields();
|
|
9792
10364
|
});
|
|
9793
10365
|
|
|
10366
|
+
// Build initial color input fields
|
|
10367
|
+
this.#rebuildColorInputFields();
|
|
10368
|
+
|
|
9794
10369
|
// Setup eyedropper
|
|
9795
10370
|
const eyedropper = container.querySelector(".fig-fill-picker-eyedropper");
|
|
9796
10371
|
if ("EyeDropper" in window) {
|
|
@@ -9813,6 +10388,27 @@ class FigFillPicker extends HTMLElement {
|
|
|
9813
10388
|
}
|
|
9814
10389
|
}
|
|
9815
10390
|
|
|
10391
|
+
#onGamutChange() {
|
|
10392
|
+
// Recreate the solid canvas with the new color space
|
|
10393
|
+
const solidContainer = this.#dialog?.querySelector('[data-tab="solid"]');
|
|
10394
|
+
if (solidContainer) {
|
|
10395
|
+
const oldCanvas = solidContainer.querySelector("canvas");
|
|
10396
|
+
if (oldCanvas) {
|
|
10397
|
+
const newCanvas = document.createElement("canvas");
|
|
10398
|
+
newCanvas.width = oldCanvas.width;
|
|
10399
|
+
newCanvas.height = oldCanvas.height;
|
|
10400
|
+
oldCanvas.replaceWith(newCanvas);
|
|
10401
|
+
this.#colorArea = newCanvas;
|
|
10402
|
+
this.#setupColorAreaEvents();
|
|
10403
|
+
}
|
|
10404
|
+
this.#drawColorArea();
|
|
10405
|
+
this.#updateHandlePosition();
|
|
10406
|
+
}
|
|
10407
|
+
// Refresh gradient preview if gradient tab exists
|
|
10408
|
+
this.#updateGradientPreview();
|
|
10409
|
+
this.#emitInput();
|
|
10410
|
+
}
|
|
10411
|
+
|
|
9816
10412
|
#drawColorArea() {
|
|
9817
10413
|
// Refresh canvas reference in case DOM changed
|
|
9818
10414
|
if (!this.#colorArea && this.#dialog) {
|
|
@@ -9820,27 +10416,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
9820
10416
|
}
|
|
9821
10417
|
if (!this.#colorArea) return;
|
|
9822
10418
|
|
|
9823
|
-
const
|
|
10419
|
+
const colorSpace = this.#gamut === "display-p3" ? "display-p3" : "srgb";
|
|
10420
|
+
const ctx = this.#colorArea.getContext("2d", { colorSpace });
|
|
9824
10421
|
if (!ctx) return;
|
|
9825
10422
|
|
|
9826
10423
|
const width = this.#colorArea.width;
|
|
9827
10424
|
const height = this.#colorArea.height;
|
|
9828
10425
|
|
|
9829
|
-
// Clear canvas first
|
|
9830
10426
|
ctx.clearRect(0, 0, width, height);
|
|
9831
10427
|
|
|
9832
|
-
// Draw saturation-value gradient
|
|
9833
10428
|
const hue = this.#color.h;
|
|
10429
|
+
const isP3 = this.#gamut === "display-p3";
|
|
9834
10430
|
|
|
9835
|
-
// Create horizontal gradient (white to hue color)
|
|
9836
10431
|
const gradH = ctx.createLinearGradient(0, 0, width, 0);
|
|
9837
|
-
|
|
9838
|
-
|
|
10432
|
+
if (isP3) {
|
|
10433
|
+
gradH.addColorStop(0, "color(display-p3 1 1 1)");
|
|
10434
|
+
const [r, g, b] = hslToP3(hue, 100, 50);
|
|
10435
|
+
gradH.addColorStop(1, `color(display-p3 ${r} ${g} ${b})`);
|
|
10436
|
+
} else {
|
|
10437
|
+
gradH.addColorStop(0, "#FFFFFF");
|
|
10438
|
+
gradH.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
|
|
10439
|
+
}
|
|
9839
10440
|
|
|
9840
10441
|
ctx.fillStyle = gradH;
|
|
9841
10442
|
ctx.fillRect(0, 0, width, height);
|
|
9842
10443
|
|
|
9843
|
-
// Create vertical gradient (transparent to black)
|
|
9844
10444
|
const gradV = ctx.createLinearGradient(0, 0, 0, height);
|
|
9845
10445
|
gradV.addColorStop(0, "rgba(0,0,0,0)");
|
|
9846
10446
|
gradV.addColorStop(1, "rgba(0,0,0,1)");
|
|
@@ -9860,90 +10460,319 @@ class FigFillPicker extends HTMLElement {
|
|
|
9860
10460
|
return;
|
|
9861
10461
|
}
|
|
9862
10462
|
|
|
9863
|
-
const
|
|
9864
|
-
const
|
|
10463
|
+
const xPct = Math.max(0, Math.min(100, this.#color.s));
|
|
10464
|
+
const yPct = Math.max(0, Math.min(100, 100 - this.#color.v));
|
|
9865
10465
|
|
|
9866
|
-
this.#colorAreaHandle.
|
|
9867
|
-
this.#colorAreaHandle.
|
|
9868
|
-
|
|
9869
|
-
"--picker-color",
|
|
10466
|
+
this.#colorAreaHandle.setAttribute("value", `${xPct}% ${yPct}%`);
|
|
10467
|
+
this.#colorAreaHandle.setAttribute(
|
|
10468
|
+
"color",
|
|
9870
10469
|
this.#hsvToHex({ ...this.#color, a: 1 }),
|
|
9871
10470
|
);
|
|
9872
10471
|
}
|
|
9873
10472
|
|
|
10473
|
+
#updateColorFromAreaPosition(x, y, opts = {}) {
|
|
10474
|
+
const { updateHandle = true, emitInput = true, emitChange = false } = opts;
|
|
10475
|
+
this.#color.s = Math.max(0, Math.min(100, x * 100));
|
|
10476
|
+
this.#color.v = Math.max(0, Math.min(100, (1 - y) * 100));
|
|
10477
|
+
if (this.#colorAreaHandle) {
|
|
10478
|
+
this.#colorAreaHandle.setAttribute(
|
|
10479
|
+
"color",
|
|
10480
|
+
this.#hsvToHex({ ...this.#color, a: 1 }),
|
|
10481
|
+
);
|
|
10482
|
+
}
|
|
10483
|
+
if (updateHandle) this.#updateHandlePosition();
|
|
10484
|
+
this.#updateColorInputs();
|
|
10485
|
+
if (emitInput) this.#emitInput();
|
|
10486
|
+
if (emitChange) this.#emitChange();
|
|
10487
|
+
}
|
|
10488
|
+
|
|
9874
10489
|
#setupColorAreaEvents() {
|
|
10490
|
+
if (this.#teardownColorAreaEvents) {
|
|
10491
|
+
this.#teardownColorAreaEvents();
|
|
10492
|
+
this.#teardownColorAreaEvents = null;
|
|
10493
|
+
}
|
|
9875
10494
|
if (!this.#colorArea || !this.#colorAreaHandle) return;
|
|
9876
10495
|
|
|
9877
|
-
const
|
|
9878
|
-
|
|
9879
|
-
this.#isDraggingColor = false;
|
|
9880
|
-
this.#emitChange();
|
|
9881
|
-
};
|
|
10496
|
+
const colorAreaEl = this.#colorArea.parentElement || this.#colorArea;
|
|
10497
|
+
const colorAreaHandleEl = this.#colorAreaHandle;
|
|
9882
10498
|
|
|
9883
|
-
|
|
9884
|
-
|
|
10499
|
+
let isPlaneDragging = false;
|
|
10500
|
+
|
|
10501
|
+
const updatePlaneFromEvent = (e, opts = {}) => {
|
|
10502
|
+
const rect = colorAreaEl.getBoundingClientRect();
|
|
10503
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
9885
10504
|
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
|
9886
10505
|
const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
|
9887
|
-
|
|
9888
|
-
this.#color.s = (x / rect.width) * 100;
|
|
9889
|
-
this.#color.v = 100 - (y / rect.height) * 100;
|
|
9890
|
-
|
|
9891
|
-
this.#updateHandlePosition();
|
|
9892
|
-
this.#updateColorInputs();
|
|
9893
|
-
this.#emitInput();
|
|
10506
|
+
this.#updateColorFromAreaPosition(x / rect.width, y / rect.height, opts);
|
|
9894
10507
|
};
|
|
9895
10508
|
|
|
9896
|
-
|
|
9897
|
-
|
|
10509
|
+
const onPlanePointerDown = (e) => {
|
|
10510
|
+
if (e.button !== 0) return;
|
|
10511
|
+
if (
|
|
10512
|
+
e.target === colorAreaHandleEl ||
|
|
10513
|
+
colorAreaHandleEl.contains(e.target)
|
|
10514
|
+
)
|
|
10515
|
+
return;
|
|
10516
|
+
isPlaneDragging = true;
|
|
9898
10517
|
this.#isDraggingColor = true;
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
}
|
|
10518
|
+
colorAreaEl.setPointerCapture(e.pointerId);
|
|
10519
|
+
updatePlaneFromEvent(e, { updateHandle: true, emitInput: true });
|
|
10520
|
+
};
|
|
9902
10521
|
|
|
9903
|
-
|
|
9904
|
-
if (!
|
|
10522
|
+
const onPlanePointerMove = (e) => {
|
|
10523
|
+
if (!isPlaneDragging) return;
|
|
9905
10524
|
if (e.buttons === 0) {
|
|
9906
|
-
|
|
10525
|
+
onPlaneDragEnd();
|
|
9907
10526
|
return;
|
|
9908
10527
|
}
|
|
9909
|
-
|
|
9910
|
-
}
|
|
10528
|
+
updatePlaneFromEvent(e, { updateHandle: true, emitInput: true });
|
|
10529
|
+
};
|
|
9911
10530
|
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
10531
|
+
const onPlaneDragEnd = () => {
|
|
10532
|
+
if (!isPlaneDragging) return;
|
|
10533
|
+
isPlaneDragging = false;
|
|
10534
|
+
this.#isDraggingColor = false;
|
|
10535
|
+
this.#emitChange();
|
|
10536
|
+
};
|
|
9915
10537
|
|
|
9916
|
-
|
|
9917
|
-
this.#colorAreaHandle.addEventListener("pointerdown", (e) => {
|
|
9918
|
-
e.stopPropagation(); // Prevent canvas from also capturing
|
|
10538
|
+
const onHandleInput = (e) => {
|
|
9919
10539
|
this.#isDraggingColor = true;
|
|
9920
|
-
|
|
9921
|
-
|
|
10540
|
+
const px = e.detail?.px;
|
|
10541
|
+
const py = e.detail?.py;
|
|
10542
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) return;
|
|
10543
|
+
colorAreaHandleEl.setAttribute("value", `${px * 100}% ${py * 100}%`);
|
|
10544
|
+
this.#updateColorFromAreaPosition(px, py, {
|
|
10545
|
+
updateHandle: false,
|
|
10546
|
+
emitInput: true,
|
|
10547
|
+
});
|
|
10548
|
+
};
|
|
9922
10549
|
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
10550
|
+
const onHandleChange = (e) => {
|
|
10551
|
+
const px = e.detail?.px;
|
|
10552
|
+
const py = e.detail?.py;
|
|
10553
|
+
if (Number.isFinite(px) && Number.isFinite(py)) {
|
|
10554
|
+
colorAreaHandleEl.setAttribute("value", `${px * 100}% ${py * 100}%`);
|
|
10555
|
+
this.#updateColorFromAreaPosition(px, py, {
|
|
10556
|
+
updateHandle: false,
|
|
10557
|
+
emitInput: false,
|
|
10558
|
+
});
|
|
10559
|
+
}
|
|
10560
|
+
this.#isDraggingColor = false;
|
|
10561
|
+
this.#emitChange();
|
|
10562
|
+
};
|
|
10563
|
+
|
|
10564
|
+
colorAreaEl.addEventListener("pointerdown", onPlanePointerDown);
|
|
10565
|
+
colorAreaEl.addEventListener("pointermove", onPlanePointerMove);
|
|
10566
|
+
colorAreaEl.addEventListener("pointerup", onPlaneDragEnd);
|
|
10567
|
+
colorAreaEl.addEventListener("pointercancel", onPlaneDragEnd);
|
|
10568
|
+
colorAreaEl.addEventListener("lostpointercapture", onPlaneDragEnd);
|
|
10569
|
+
|
|
10570
|
+
colorAreaHandleEl.addEventListener("input", onHandleInput);
|
|
10571
|
+
colorAreaHandleEl.addEventListener("change", onHandleChange);
|
|
10572
|
+
|
|
10573
|
+
this.#teardownColorAreaEvents = () => {
|
|
10574
|
+
colorAreaEl.removeEventListener("pointerdown", onPlanePointerDown);
|
|
10575
|
+
colorAreaEl.removeEventListener("pointermove", onPlanePointerMove);
|
|
10576
|
+
colorAreaEl.removeEventListener("pointerup", onPlaneDragEnd);
|
|
10577
|
+
colorAreaEl.removeEventListener("pointercancel", onPlaneDragEnd);
|
|
10578
|
+
colorAreaEl.removeEventListener("lostpointercapture", onPlaneDragEnd);
|
|
10579
|
+
|
|
10580
|
+
colorAreaHandleEl.removeEventListener("input", onHandleInput);
|
|
10581
|
+
colorAreaHandleEl.removeEventListener("change", onHandleChange);
|
|
10582
|
+
this.#isDraggingColor = false;
|
|
10583
|
+
};
|
|
10584
|
+
}
|
|
10585
|
+
|
|
10586
|
+
#rebuildColorInputFields() {
|
|
10587
|
+
const container = this.#dialog?.querySelector(
|
|
10588
|
+
".fig-fill-picker-input-fields",
|
|
10589
|
+
);
|
|
10590
|
+
if (!container) return;
|
|
10591
|
+
|
|
10592
|
+
const wrap = (tooltip, html) =>
|
|
10593
|
+
`<fig-tooltip text="${tooltip}">${html}</fig-tooltip>`;
|
|
10594
|
+
|
|
10595
|
+
const num = (cls, min, max, step) =>
|
|
10596
|
+
`<fig-input-number class="${cls}" min="${min}" max="${max}"${step != null ? ` step="${step}"` : ""}></fig-input-number>`;
|
|
10597
|
+
|
|
10598
|
+
let html;
|
|
10599
|
+
switch (this.#colorInputMode) {
|
|
10600
|
+
case "rgb":
|
|
10601
|
+
html = `<div class="input-combo">
|
|
10602
|
+
${wrap("Red", num("fig-fill-picker-ci-r", 0, 255))}
|
|
10603
|
+
${wrap("Green", num("fig-fill-picker-ci-g", 0, 255))}
|
|
10604
|
+
${wrap("Blue", num("fig-fill-picker-ci-b", 0, 255))}
|
|
10605
|
+
</div>`;
|
|
10606
|
+
break;
|
|
10607
|
+
case "hsl":
|
|
10608
|
+
html = `<div class="input-combo">
|
|
10609
|
+
${wrap("Hue", num("fig-fill-picker-ci-h", 0, 360))}
|
|
10610
|
+
${wrap("Saturation", num("fig-fill-picker-ci-s", 0, 100))}
|
|
10611
|
+
${wrap("Lightness", num("fig-fill-picker-ci-l", 0, 100))}
|
|
10612
|
+
</div>`;
|
|
10613
|
+
break;
|
|
10614
|
+
case "hsb":
|
|
10615
|
+
html = `<div class="input-combo">
|
|
10616
|
+
${wrap("Hue", num("fig-fill-picker-ci-h", 0, 360))}
|
|
10617
|
+
${wrap("Saturation", num("fig-fill-picker-ci-s", 0, 100))}
|
|
10618
|
+
${wrap("Brightness", num("fig-fill-picker-ci-v", 0, 100))}
|
|
10619
|
+
</div>`;
|
|
10620
|
+
break;
|
|
10621
|
+
case "lab":
|
|
10622
|
+
html = `<div class="input-combo">
|
|
10623
|
+
${wrap("Lightness", num("fig-fill-picker-ci-okl", 0, 100))}
|
|
10624
|
+
${wrap("Green-Red axis", num("fig-fill-picker-ci-oka", -0.4, 0.4, 0.001))}
|
|
10625
|
+
${wrap("Blue-Yellow axis", num("fig-fill-picker-ci-okb", -0.4, 0.4, 0.001))}
|
|
10626
|
+
</div>`;
|
|
10627
|
+
break;
|
|
10628
|
+
case "lch":
|
|
10629
|
+
html = `<div class="input-combo">
|
|
10630
|
+
${wrap("Lightness", num("fig-fill-picker-ci-okl", 0, 100))}
|
|
10631
|
+
${wrap("Chroma", num("fig-fill-picker-ci-okc", 0, 0.4, 0.001))}
|
|
10632
|
+
${wrap("Hue", num("fig-fill-picker-ci-okh", 0, 360))}
|
|
10633
|
+
</div>`;
|
|
10634
|
+
break;
|
|
10635
|
+
default: // hex
|
|
10636
|
+
html = `<fig-input-text class="fig-fill-picker-ci-hex" placeholder="FFFFFF"></fig-input-text>`;
|
|
10637
|
+
break;
|
|
10638
|
+
}
|
|
10639
|
+
|
|
10640
|
+
container.innerHTML = html;
|
|
10641
|
+
this.#wireColorInputEvents();
|
|
10642
|
+
requestAnimationFrame(() => this.#updateColorInputs());
|
|
10643
|
+
}
|
|
10644
|
+
|
|
10645
|
+
#wireColorInputEvents() {
|
|
10646
|
+
const container = this.#dialog?.querySelector(
|
|
10647
|
+
".fig-fill-picker-input-fields",
|
|
10648
|
+
);
|
|
10649
|
+
if (!container) return;
|
|
10650
|
+
|
|
10651
|
+
const onInput = () => {
|
|
10652
|
+
if (this.#isDraggingColor) return;
|
|
10653
|
+
const color = this.#readColorFromInputs();
|
|
10654
|
+
if (!color) return;
|
|
10655
|
+
this.#color = { ...color, a: this.#color.a };
|
|
10656
|
+
this.#drawColorArea();
|
|
10657
|
+
this.#updateHandlePosition();
|
|
10658
|
+
if (this.#hueSlider) {
|
|
10659
|
+
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
9928
10660
|
}
|
|
9929
|
-
|
|
10661
|
+
this.#emitInput();
|
|
10662
|
+
};
|
|
10663
|
+
|
|
10664
|
+
const onChange = () => this.#emitChange();
|
|
10665
|
+
|
|
10666
|
+
const inputs = container.querySelectorAll(
|
|
10667
|
+
"fig-input-number, fig-input-text",
|
|
10668
|
+
);
|
|
10669
|
+
inputs.forEach((el) => {
|
|
10670
|
+
el.addEventListener("input", onInput);
|
|
10671
|
+
el.addEventListener("change", onChange);
|
|
9930
10672
|
});
|
|
10673
|
+
}
|
|
9931
10674
|
|
|
9932
|
-
|
|
9933
|
-
this.#
|
|
9934
|
-
|
|
10675
|
+
#readColorFromInputs() {
|
|
10676
|
+
const q = (cls) => this.#dialog?.querySelector(`.${cls}`);
|
|
10677
|
+
const val = (cls) => parseFloat(q(cls)?.value ?? 0);
|
|
10678
|
+
|
|
10679
|
+
switch (this.#colorInputMode) {
|
|
10680
|
+
case "rgb":
|
|
10681
|
+
return this.#rgbToHSV({
|
|
10682
|
+
r: val("fig-fill-picker-ci-r"),
|
|
10683
|
+
g: val("fig-fill-picker-ci-g"),
|
|
10684
|
+
b: val("fig-fill-picker-ci-b"),
|
|
10685
|
+
});
|
|
10686
|
+
case "hsl": {
|
|
10687
|
+
const rgb = this.#hslToRGB({
|
|
10688
|
+
h: val("fig-fill-picker-ci-h"),
|
|
10689
|
+
s: val("fig-fill-picker-ci-s"),
|
|
10690
|
+
l: val("fig-fill-picker-ci-l"),
|
|
10691
|
+
});
|
|
10692
|
+
return this.#rgbToHSV(rgb);
|
|
10693
|
+
}
|
|
10694
|
+
case "hsb":
|
|
10695
|
+
return {
|
|
10696
|
+
h: val("fig-fill-picker-ci-h"),
|
|
10697
|
+
s: val("fig-fill-picker-ci-s"),
|
|
10698
|
+
v: val("fig-fill-picker-ci-v"),
|
|
10699
|
+
a: 1,
|
|
10700
|
+
};
|
|
10701
|
+
case "lab": {
|
|
10702
|
+
const rgb = this.#oklabToRGB({
|
|
10703
|
+
l: val("fig-fill-picker-ci-okl") / 100,
|
|
10704
|
+
a: val("fig-fill-picker-ci-oka"),
|
|
10705
|
+
b: val("fig-fill-picker-ci-okb"),
|
|
10706
|
+
});
|
|
10707
|
+
return this.#rgbToHSV(rgb);
|
|
10708
|
+
}
|
|
10709
|
+
case "lch": {
|
|
10710
|
+
const rgb = this.#oklchToRGB({
|
|
10711
|
+
l: val("fig-fill-picker-ci-okl") / 100,
|
|
10712
|
+
c: val("fig-fill-picker-ci-okc"),
|
|
10713
|
+
h: val("fig-fill-picker-ci-okh"),
|
|
10714
|
+
});
|
|
10715
|
+
return this.#rgbToHSV(rgb);
|
|
10716
|
+
}
|
|
10717
|
+
default: {
|
|
10718
|
+
// hex
|
|
10719
|
+
const hexEl = q("fig-fill-picker-ci-hex");
|
|
10720
|
+
if (!hexEl) return null;
|
|
10721
|
+
let hex = hexEl.value.replace(/^#/, "");
|
|
10722
|
+
if (hex.length === 3)
|
|
10723
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
10724
|
+
if (hex.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
10725
|
+
return this.#hexToHSV(`#${hex}`);
|
|
10726
|
+
}
|
|
10727
|
+
}
|
|
9935
10728
|
}
|
|
9936
10729
|
|
|
9937
10730
|
#updateColorInputs() {
|
|
9938
10731
|
if (!this.#dialog) return;
|
|
9939
10732
|
|
|
9940
10733
|
const hex = this.#hsvToHex(this.#color);
|
|
10734
|
+
const rgb = this.#hsvToRGB(this.#color);
|
|
10735
|
+
const q = (cls) => this.#dialog.querySelector(`.${cls}`);
|
|
10736
|
+
const set = (cls, v) => {
|
|
10737
|
+
const el = q(cls);
|
|
10738
|
+
if (el) el.setAttribute("value", v);
|
|
10739
|
+
};
|
|
9941
10740
|
|
|
9942
|
-
|
|
9943
|
-
"
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
10741
|
+
switch (this.#colorInputMode) {
|
|
10742
|
+
case "rgb":
|
|
10743
|
+
set("fig-fill-picker-ci-r", rgb.r);
|
|
10744
|
+
set("fig-fill-picker-ci-g", rgb.g);
|
|
10745
|
+
set("fig-fill-picker-ci-b", rgb.b);
|
|
10746
|
+
break;
|
|
10747
|
+
case "hsl": {
|
|
10748
|
+
const hsl = this.#rgbToHSL(rgb);
|
|
10749
|
+
set("fig-fill-picker-ci-h", Math.round(hsl.h));
|
|
10750
|
+
set("fig-fill-picker-ci-s", Math.round(hsl.s));
|
|
10751
|
+
set("fig-fill-picker-ci-l", Math.round(hsl.l));
|
|
10752
|
+
break;
|
|
10753
|
+
}
|
|
10754
|
+
case "hsb":
|
|
10755
|
+
set("fig-fill-picker-ci-h", Math.round(this.#color.h));
|
|
10756
|
+
set("fig-fill-picker-ci-s", Math.round(this.#color.s));
|
|
10757
|
+
set("fig-fill-picker-ci-v", Math.round(this.#color.v));
|
|
10758
|
+
break;
|
|
10759
|
+
case "lab": {
|
|
10760
|
+
const lab = this.#rgbToOKLAB(rgb);
|
|
10761
|
+
set("fig-fill-picker-ci-okl", Math.round(lab.l * 100));
|
|
10762
|
+
set("fig-fill-picker-ci-oka", +lab.a.toFixed(3));
|
|
10763
|
+
set("fig-fill-picker-ci-okb", +lab.b.toFixed(3));
|
|
10764
|
+
break;
|
|
10765
|
+
}
|
|
10766
|
+
case "lch": {
|
|
10767
|
+
const lch = this.#rgbToOKLCH(rgb);
|
|
10768
|
+
set("fig-fill-picker-ci-okl", Math.round(lch.l * 100));
|
|
10769
|
+
set("fig-fill-picker-ci-okc", +lch.c.toFixed(3));
|
|
10770
|
+
set("fig-fill-picker-ci-okh", Math.round(lch.h));
|
|
10771
|
+
break;
|
|
10772
|
+
}
|
|
10773
|
+
default: // hex
|
|
10774
|
+
set("fig-fill-picker-ci-hex", hex.replace(/^#/, "").toUpperCase());
|
|
10775
|
+
break;
|
|
9947
10776
|
}
|
|
9948
10777
|
|
|
9949
10778
|
if (this.#opacitySlider) {
|
|
@@ -9960,7 +10789,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
9960
10789
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
9961
10790
|
|
|
9962
10791
|
container.innerHTML = `
|
|
9963
|
-
<
|
|
10792
|
+
<fig-field class="fig-fill-picker-gradient-header" direction="horizontal">
|
|
9964
10793
|
<fig-dropdown class="fig-fill-picker-gradient-type" ${expAttr} value="${
|
|
9965
10794
|
this.#gradient.type
|
|
9966
10795
|
}">
|
|
@@ -9986,18 +10815,39 @@ class FigFillPicker extends HTMLElement {
|
|
|
9986
10815
|
<span class="fig-mask-icon" style="--icon: var(--icon-swap)"></span>
|
|
9987
10816
|
</fig-button>
|
|
9988
10817
|
</fig-tooltip>
|
|
9989
|
-
</
|
|
9990
|
-
<
|
|
10818
|
+
</fig-field>
|
|
10819
|
+
<fig-preview class="fig-fill-picker-gradient-preview">
|
|
9991
10820
|
<div class="fig-fill-picker-gradient-bar"></div>
|
|
9992
10821
|
<div class="fig-fill-picker-gradient-stops-handles"></div>
|
|
9993
|
-
</
|
|
10822
|
+
</fig-preview>
|
|
10823
|
+
<fig-field class="fig-fill-picker-gradient-interpolation" direction="horizontal">
|
|
10824
|
+
<label>Mixing</label>
|
|
10825
|
+
<fig-dropdown class="fig-fill-picker-gradient-space" full ${expAttr} value="${
|
|
10826
|
+
this.#gradient.interpolationSpace === "oklch"
|
|
10827
|
+
? `oklch-${this.#gradient.hueInterpolation || "shorter"}`
|
|
10828
|
+
: this.#gradient.interpolationSpace
|
|
10829
|
+
}">
|
|
10830
|
+
<optgroup label="sRGB">
|
|
10831
|
+
<option value="srgb-linear">Linear</option>
|
|
10832
|
+
</optgroup>
|
|
10833
|
+
<optgroup label="OKLab">
|
|
10834
|
+
<option value="oklab">Perceptual</option>
|
|
10835
|
+
</optgroup>
|
|
10836
|
+
<optgroup label="OKLCH">
|
|
10837
|
+
<option value="oklch-shorter">Shorter hue</option>
|
|
10838
|
+
<option value="oklch-longer">Longer hue</option>
|
|
10839
|
+
<option value="oklch-increasing">Increasing hue</option>
|
|
10840
|
+
<option value="oklch-decreasing">Decreasing hue</option>
|
|
10841
|
+
</optgroup>
|
|
10842
|
+
</fig-dropdown>
|
|
10843
|
+
</fig-field>
|
|
9994
10844
|
<div class="fig-fill-picker-gradient-stops">
|
|
9995
|
-
<
|
|
10845
|
+
<fig-header class="fig-fill-picker-gradient-stops-header" borderless>
|
|
9996
10846
|
<span>Stops</span>
|
|
9997
10847
|
<fig-button icon variant="ghost" class="fig-fill-picker-gradient-add" title="Add stop">
|
|
9998
10848
|
<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>
|
|
9999
10849
|
</fig-button>
|
|
10000
|
-
</
|
|
10850
|
+
</fig-header>
|
|
10001
10851
|
<div class="fig-fill-picker-gradient-stops-list"></div>
|
|
10002
10852
|
</div>
|
|
10003
10853
|
`;
|
|
@@ -10011,11 +10861,41 @@ class FigFillPicker extends HTMLElement {
|
|
|
10011
10861
|
const typeDropdown = container.querySelector(
|
|
10012
10862
|
".fig-fill-picker-gradient-type",
|
|
10013
10863
|
);
|
|
10014
|
-
|
|
10015
|
-
|
|
10864
|
+
const getDropdownValue = (event) =>
|
|
10865
|
+
event.currentTarget?.value ?? event.target?.value ?? event.detail;
|
|
10866
|
+
|
|
10867
|
+
const handleTypeChange = (e) => {
|
|
10868
|
+
this.#gradient.type = getDropdownValue(e);
|
|
10016
10869
|
this.#updateGradientUI();
|
|
10017
10870
|
this.#emitInput();
|
|
10018
|
-
}
|
|
10871
|
+
};
|
|
10872
|
+
typeDropdown.addEventListener("input", handleTypeChange);
|
|
10873
|
+
typeDropdown.addEventListener("change", handleTypeChange);
|
|
10874
|
+
|
|
10875
|
+
const interpolationDropdown = container.querySelector(
|
|
10876
|
+
".fig-fill-picker-gradient-space",
|
|
10877
|
+
);
|
|
10878
|
+
const handleInterpolationChange = (e) => {
|
|
10879
|
+
const val = getDropdownValue(e);
|
|
10880
|
+
let space = val;
|
|
10881
|
+
let hue = "shorter";
|
|
10882
|
+
if (val.startsWith("oklch-")) {
|
|
10883
|
+
space = "oklch";
|
|
10884
|
+
hue = val.slice(6);
|
|
10885
|
+
}
|
|
10886
|
+
this.#gradient = normalizeGradientConfig({
|
|
10887
|
+
...this.#gradient,
|
|
10888
|
+
interpolationSpace: space,
|
|
10889
|
+
hueInterpolation: hue,
|
|
10890
|
+
});
|
|
10891
|
+
this.#updateGradientUI();
|
|
10892
|
+
this.#emitInput();
|
|
10893
|
+
};
|
|
10894
|
+
interpolationDropdown?.addEventListener("input", handleInterpolationChange);
|
|
10895
|
+
interpolationDropdown?.addEventListener(
|
|
10896
|
+
"change",
|
|
10897
|
+
handleInterpolationChange,
|
|
10898
|
+
);
|
|
10019
10899
|
|
|
10020
10900
|
// Angle input
|
|
10021
10901
|
// Convert from fig-input-angle coordinates (0° = right) to CSS coordinates (0° = up)
|
|
@@ -10076,6 +10956,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10076
10956
|
|
|
10077
10957
|
const container = this.#dialog.querySelector('[data-tab="gradient"]');
|
|
10078
10958
|
if (!container) return;
|
|
10959
|
+
this.#gradient = normalizeGradientConfig(this.#gradient);
|
|
10079
10960
|
|
|
10080
10961
|
// Show/hide angle vs center inputs
|
|
10081
10962
|
const angleInput = container.querySelector(
|
|
@@ -10096,6 +10977,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
10096
10977
|
angleInput.setAttribute("value", pickerAngle);
|
|
10097
10978
|
}
|
|
10098
10979
|
|
|
10980
|
+
const interpolationDropdown = container.querySelector(
|
|
10981
|
+
".fig-fill-picker-gradient-space",
|
|
10982
|
+
);
|
|
10983
|
+
if (interpolationDropdown) {
|
|
10984
|
+
interpolationDropdown.value =
|
|
10985
|
+
this.#gradient.interpolationSpace === "oklch"
|
|
10986
|
+
? `oklch-${this.#gradient.hueInterpolation || "shorter"}`
|
|
10987
|
+
: this.#gradient.interpolationSpace;
|
|
10988
|
+
}
|
|
10989
|
+
|
|
10099
10990
|
this.#updateGradientPreview();
|
|
10100
10991
|
this.#updateGradientStopsList();
|
|
10101
10992
|
}
|
|
@@ -10103,9 +10994,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
10103
10994
|
#updateGradientPreview() {
|
|
10104
10995
|
if (!this.#dialog) return;
|
|
10105
10996
|
|
|
10997
|
+
const preview = this.#dialog.querySelector(
|
|
10998
|
+
".fig-fill-picker-gradient-preview",
|
|
10999
|
+
);
|
|
10106
11000
|
const bar = this.#dialog.querySelector(".fig-fill-picker-gradient-bar");
|
|
10107
|
-
if (bar) {
|
|
10108
|
-
|
|
11001
|
+
if (preview || bar) {
|
|
11002
|
+
const css = this.#getGradientCSS();
|
|
11003
|
+
if (bar) bar.style.background = css;
|
|
11004
|
+
if (preview) preview.style.background = css;
|
|
10109
11005
|
}
|
|
10110
11006
|
|
|
10111
11007
|
this.#updateChit();
|
|
@@ -10122,7 +11018,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10122
11018
|
list.innerHTML = this.#gradient.stops
|
|
10123
11019
|
.map(
|
|
10124
11020
|
(stop, index) => `
|
|
10125
|
-
<
|
|
11021
|
+
<fig-field class="fig-fill-picker-gradient-stop-row" direction="horizontal" data-index="${index}">
|
|
10126
11022
|
<fig-input-number class="fig-fill-picker-stop-position" min="0" max="100" value="${
|
|
10127
11023
|
stop.position
|
|
10128
11024
|
}" units="%"></fig-input-number>
|
|
@@ -10134,7 +11030,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10134
11030
|
}>
|
|
10135
11031
|
<span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>
|
|
10136
11032
|
</fig-button>
|
|
10137
|
-
</
|
|
11033
|
+
</fig-field>
|
|
10138
11034
|
`,
|
|
10139
11035
|
)
|
|
10140
11036
|
.join("");
|
|
@@ -10188,30 +11084,52 @@ class FigFillPicker extends HTMLElement {
|
|
|
10188
11084
|
});
|
|
10189
11085
|
}
|
|
10190
11086
|
|
|
10191
|
-
#
|
|
10192
|
-
const
|
|
11087
|
+
#buildGradientCSS(interpolationSpaceOverride, includeInterpolation = true) {
|
|
11088
|
+
const gradient = normalizeGradientConfig({
|
|
11089
|
+
...this.#gradient,
|
|
11090
|
+
interpolationSpace:
|
|
11091
|
+
interpolationSpaceOverride ?? this.#gradient.interpolationSpace,
|
|
11092
|
+
});
|
|
11093
|
+
const isP3 = this.#gamut === "display-p3";
|
|
11094
|
+
const stops = gradient.stops
|
|
10193
11095
|
.map((s) => {
|
|
10194
|
-
const
|
|
10195
|
-
|
|
11096
|
+
const color = isP3
|
|
11097
|
+
? this.#hexToP3(s.color, s.opacity / 100)
|
|
11098
|
+
: this.#hexToRGBA(s.color, s.opacity / 100);
|
|
11099
|
+
return `${color} ${s.position}%`;
|
|
10196
11100
|
})
|
|
10197
11101
|
.join(", ");
|
|
10198
|
-
|
|
10199
|
-
|
|
11102
|
+
const interpolation = includeInterpolation
|
|
11103
|
+
? ` ${gradientInterpolationClause(gradient)}`
|
|
11104
|
+
: "";
|
|
11105
|
+
switch (gradient.type) {
|
|
10200
11106
|
case "linear":
|
|
10201
|
-
return `linear-gradient(${
|
|
11107
|
+
return `linear-gradient(${gradient.angle}deg${interpolation}, ${stops})`;
|
|
10202
11108
|
case "radial":
|
|
10203
|
-
return `radial-gradient(circle at ${
|
|
10204
|
-
this.#gradient.centerY
|
|
10205
|
-
}%, ${stops})`;
|
|
11109
|
+
return `radial-gradient(circle at ${gradient.centerX}% ${gradient.centerY}%${interpolation}, ${stops})`;
|
|
10206
11110
|
case "angular":
|
|
10207
|
-
|
|
10208
|
-
// because it's converted when reading from fig-input-angle
|
|
10209
|
-
return `conic-gradient(from ${this.#gradient.angle}deg, ${stops})`;
|
|
11111
|
+
return `conic-gradient(from ${gradient.angle}deg${interpolation}, ${stops})`;
|
|
10210
11112
|
default:
|
|
10211
|
-
return `linear-gradient(${
|
|
11113
|
+
return `linear-gradient(${gradient.angle}deg${interpolation}, ${stops})`;
|
|
10212
11114
|
}
|
|
10213
11115
|
}
|
|
10214
11116
|
|
|
11117
|
+
#testGradientSupport(css) {
|
|
11118
|
+
const el = document.createElement("div");
|
|
11119
|
+
el.style.background = css;
|
|
11120
|
+
return !!el.style.background;
|
|
11121
|
+
}
|
|
11122
|
+
|
|
11123
|
+
#getGradientCSS() {
|
|
11124
|
+
const preferred = this.#buildGradientCSS(undefined, true);
|
|
11125
|
+
if (this.#testGradientSupport(preferred)) return preferred;
|
|
11126
|
+
|
|
11127
|
+
const oklabFallback = this.#buildGradientCSS("oklab", true);
|
|
11128
|
+
if (this.#testGradientSupport(oklabFallback)) return oklabFallback;
|
|
11129
|
+
|
|
11130
|
+
return this.#buildGradientCSS("oklab", false);
|
|
11131
|
+
}
|
|
11132
|
+
|
|
10215
11133
|
// ============ IMAGE TAB ============
|
|
10216
11134
|
#initImageTab() {
|
|
10217
11135
|
const container = this.#dialog.querySelector('[data-tab="image"]');
|
|
@@ -10219,7 +11137,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10219
11137
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
10220
11138
|
|
|
10221
11139
|
container.innerHTML = `
|
|
10222
|
-
<
|
|
11140
|
+
<fig-field class="fig-fill-picker-media-header" direction="horizontal">
|
|
10223
11141
|
<fig-dropdown class="fig-fill-picker-scale-mode" ${expAttr} value="${
|
|
10224
11142
|
this.#image.scaleMode
|
|
10225
11143
|
}">
|
|
@@ -10231,7 +11149,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10231
11149
|
<fig-input-number class="fig-fill-picker-scale" min="1" max="200" value="${
|
|
10232
11150
|
this.#image.scale
|
|
10233
11151
|
}" units="%" style="display: none;"></fig-input-number>
|
|
10234
|
-
</
|
|
11152
|
+
</fig-field>
|
|
10235
11153
|
<div class="fig-fill-picker-media-preview">
|
|
10236
11154
|
<div class="fig-fill-picker-checkerboard"></div>
|
|
10237
11155
|
<div class="fig-fill-picker-image-preview"></div>
|
|
@@ -10373,7 +11291,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10373
11291
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
10374
11292
|
|
|
10375
11293
|
container.innerHTML = `
|
|
10376
|
-
<
|
|
11294
|
+
<fig-field class="fig-fill-picker-media-header" direction="horizontal">
|
|
10377
11295
|
<fig-dropdown class="fig-fill-picker-scale-mode" ${expAttr} value="${
|
|
10378
11296
|
this.#video.scaleMode
|
|
10379
11297
|
}">
|
|
@@ -10381,7 +11299,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10381
11299
|
<option value="fit">Fit</option>
|
|
10382
11300
|
<option value="crop">Crop</option>
|
|
10383
11301
|
</fig-dropdown>
|
|
10384
|
-
</
|
|
11302
|
+
</fig-field>
|
|
10385
11303
|
<div class="fig-fill-picker-media-preview">
|
|
10386
11304
|
<div class="fig-fill-picker-checkerboard"></div>
|
|
10387
11305
|
<video class="fig-fill-picker-video-preview" style="display: none;" muted loop></video>
|
|
@@ -10471,13 +11389,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
10471
11389
|
<span>Camera access required</span>
|
|
10472
11390
|
</div>
|
|
10473
11391
|
</div>
|
|
10474
|
-
<
|
|
11392
|
+
<fig-field class="fig-fill-picker-webcam-controls" direction="horizontal">
|
|
10475
11393
|
<fig-dropdown class="fig-fill-picker-camera-select" ${expAttr} style="display: none;">
|
|
10476
11394
|
</fig-dropdown>
|
|
10477
11395
|
<fig-button class="fig-fill-picker-webcam-capture" variant="primary">
|
|
10478
11396
|
Capture
|
|
10479
11397
|
</fig-button>
|
|
10480
|
-
</
|
|
11398
|
+
</fig-field>
|
|
10481
11399
|
`;
|
|
10482
11400
|
|
|
10483
11401
|
this.#setupWebcamEvents(container);
|
|
@@ -10701,6 +11619,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
10701
11619
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
10702
11620
|
}
|
|
10703
11621
|
|
|
11622
|
+
#hexToP3(hex, alpha = 1) {
|
|
11623
|
+
const r = +(parseInt(hex.slice(1, 3), 16) / 255).toFixed(4);
|
|
11624
|
+
const g = +(parseInt(hex.slice(3, 5), 16) / 255).toFixed(4);
|
|
11625
|
+
const b = +(parseInt(hex.slice(5, 7), 16) / 255).toFixed(4);
|
|
11626
|
+
return `color(display-p3 ${r} ${g} ${b} / ${alpha})`;
|
|
11627
|
+
}
|
|
11628
|
+
|
|
10704
11629
|
#rgbToHSL(rgb) {
|
|
10705
11630
|
const r = rgb.r / 255;
|
|
10706
11631
|
const g = rgb.g / 255;
|
|
@@ -10805,9 +11730,40 @@ class FigFillPicker extends HTMLElement {
|
|
|
10805
11730
|
};
|
|
10806
11731
|
}
|
|
10807
11732
|
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
11733
|
+
#oklabToRGB(lab) {
|
|
11734
|
+
const l_ = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
|
|
11735
|
+
const m_ = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
|
|
11736
|
+
const s_ = lab.l - 0.0894841775 * lab.a - 1.291485548 * lab.b;
|
|
11737
|
+
|
|
11738
|
+
const l = l_ * l_ * l_;
|
|
11739
|
+
const m = m_ * m_ * m_;
|
|
11740
|
+
const s = s_ * s_ * s_;
|
|
11741
|
+
|
|
11742
|
+
const toSRGB = (c) => {
|
|
11743
|
+
const v =
|
|
11744
|
+
c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
11745
|
+
return Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
11746
|
+
};
|
|
11747
|
+
|
|
11748
|
+
return {
|
|
11749
|
+
r: toSRGB(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
|
11750
|
+
g: toSRGB(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
|
11751
|
+
b: toSRGB(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
|
|
11752
|
+
};
|
|
11753
|
+
}
|
|
11754
|
+
|
|
11755
|
+
#oklchToRGB(lch) {
|
|
11756
|
+
const hRad = (lch.h * Math.PI) / 180;
|
|
11757
|
+
return this.#oklabToRGB({
|
|
11758
|
+
l: lch.l,
|
|
11759
|
+
a: lch.c * Math.cos(hRad),
|
|
11760
|
+
b: lch.c * Math.sin(hRad),
|
|
11761
|
+
});
|
|
11762
|
+
}
|
|
11763
|
+
|
|
11764
|
+
// ============ EVENT EMITTERS ============
|
|
11765
|
+
#emitInput() {
|
|
11766
|
+
this.#updateChit();
|
|
10811
11767
|
this.dispatchEvent(
|
|
10812
11768
|
new CustomEvent("input", {
|
|
10813
11769
|
bubbles: true,
|
|
@@ -10827,7 +11783,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10827
11783
|
|
|
10828
11784
|
// ============ PUBLIC API ============
|
|
10829
11785
|
get value() {
|
|
10830
|
-
const base = { type: this.#fillType };
|
|
11786
|
+
const base = { type: this.#fillType, colorSpace: this.#gamut };
|
|
10831
11787
|
|
|
10832
11788
|
switch (this.#fillType) {
|
|
10833
11789
|
case "solid":
|
|
@@ -10840,7 +11796,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10840
11796
|
case "gradient":
|
|
10841
11797
|
return {
|
|
10842
11798
|
...base,
|
|
10843
|
-
gradient:
|
|
11799
|
+
gradient: gradientToValueShape(this.#gradient),
|
|
10844
11800
|
css: this.#getGradientCSS(),
|
|
10845
11801
|
};
|
|
10846
11802
|
case "image":
|
|
@@ -10907,6 +11863,237 @@ class FigFillPicker extends HTMLElement {
|
|
|
10907
11863
|
}
|
|
10908
11864
|
customElements.define("fig-fill-picker", FigFillPicker);
|
|
10909
11865
|
|
|
11866
|
+
/* Color Tip */
|
|
11867
|
+
/**
|
|
11868
|
+
* A compact solid-color tip that wraps fig-fill-picker.
|
|
11869
|
+
* @attr {string} value - Solid color string (hex/rgb/hsl/named)
|
|
11870
|
+
* @attr {boolean} selected - Whether the tip is selected
|
|
11871
|
+
* @attr {boolean} disabled - Whether the tip is disabled
|
|
11872
|
+
* @fires input - While color changes
|
|
11873
|
+
* @fires change - When color is committed
|
|
11874
|
+
*/
|
|
11875
|
+
class FigColorTip extends HTMLElement {
|
|
11876
|
+
#fillPicker = null;
|
|
11877
|
+
#chit = null;
|
|
11878
|
+
#boundHandleInput = this.#handlePickerInput.bind(this);
|
|
11879
|
+
#boundHandleChange = this.#handlePickerChange.bind(this);
|
|
11880
|
+
|
|
11881
|
+
static get observedAttributes() {
|
|
11882
|
+
return ["value", "selected", "disabled", "alpha"];
|
|
11883
|
+
}
|
|
11884
|
+
|
|
11885
|
+
connectedCallback() {
|
|
11886
|
+
this.#render();
|
|
11887
|
+
this.#syncFromAttributes();
|
|
11888
|
+
}
|
|
11889
|
+
|
|
11890
|
+
disconnectedCallback() {
|
|
11891
|
+
this.#teardownListeners();
|
|
11892
|
+
}
|
|
11893
|
+
|
|
11894
|
+
#teardownListeners() {
|
|
11895
|
+
if (!this.#fillPicker) return;
|
|
11896
|
+
this.#fillPicker.removeEventListener("input", this.#boundHandleInput);
|
|
11897
|
+
this.#fillPicker.removeEventListener("change", this.#boundHandleChange);
|
|
11898
|
+
}
|
|
11899
|
+
|
|
11900
|
+
#watchPickerDialog = () => {
|
|
11901
|
+
requestAnimationFrame(() => {
|
|
11902
|
+
const dialog = document.querySelector(".fig-fill-picker-dialog[open]");
|
|
11903
|
+
if (!dialog) return;
|
|
11904
|
+
dialog.addEventListener("close", () => this.removeAttribute("selected"), {
|
|
11905
|
+
once: true,
|
|
11906
|
+
});
|
|
11907
|
+
});
|
|
11908
|
+
};
|
|
11909
|
+
|
|
11910
|
+
get #alphaEnabled() {
|
|
11911
|
+
const v = this.getAttribute("alpha");
|
|
11912
|
+
return v === null || v !== "false";
|
|
11913
|
+
}
|
|
11914
|
+
|
|
11915
|
+
#render() {
|
|
11916
|
+
const color = this.#normalizeColor(this.getAttribute("value"));
|
|
11917
|
+
const alphaAttr = this.#alphaEnabled ? "" : 'alpha="false"';
|
|
11918
|
+
this.innerHTML = `
|
|
11919
|
+
<fig-fill-picker mode="solid" ${alphaAttr} value='${JSON.stringify({ type: "solid", color })}'>
|
|
11920
|
+
<fig-chit background="${color}"></fig-chit>
|
|
11921
|
+
</fig-fill-picker>`;
|
|
11922
|
+
|
|
11923
|
+
this.#fillPicker = this.querySelector("fig-fill-picker");
|
|
11924
|
+
this.#chit = this.querySelector("fig-chit");
|
|
11925
|
+
this.#teardownListeners();
|
|
11926
|
+
this.#fillPicker?.addEventListener("input", this.#boundHandleInput);
|
|
11927
|
+
this.#fillPicker?.addEventListener("change", this.#boundHandleChange);
|
|
11928
|
+
this.#chit?.addEventListener("click", () => {
|
|
11929
|
+
this.setAttribute("selected", "");
|
|
11930
|
+
this.#watchPickerDialog();
|
|
11931
|
+
});
|
|
11932
|
+
}
|
|
11933
|
+
|
|
11934
|
+
#normalizeHex(hex) {
|
|
11935
|
+
if (!hex) return "#D9D9D9";
|
|
11936
|
+
const raw = hex.replace("#", "").trim();
|
|
11937
|
+
if (raw.length === 3 || raw.length === 4) {
|
|
11938
|
+
const [r, g, b] = raw;
|
|
11939
|
+
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
|
|
11940
|
+
}
|
|
11941
|
+
if (raw.length === 6 || raw.length === 8) {
|
|
11942
|
+
return `#${raw.slice(0, 6)}`.toUpperCase();
|
|
11943
|
+
}
|
|
11944
|
+
return "#D9D9D9";
|
|
11945
|
+
}
|
|
11946
|
+
|
|
11947
|
+
#normalizeColor(colorValue) {
|
|
11948
|
+
if (!colorValue) return "#D9D9D9";
|
|
11949
|
+
const value = String(colorValue).trim();
|
|
11950
|
+
|
|
11951
|
+
if (value.startsWith("{")) {
|
|
11952
|
+
try {
|
|
11953
|
+
const parsed = JSON.parse(value);
|
|
11954
|
+
if (parsed?.color) {
|
|
11955
|
+
return this.#normalizeColor(parsed.color);
|
|
11956
|
+
}
|
|
11957
|
+
} catch {
|
|
11958
|
+
// Ignore parse errors and continue.
|
|
11959
|
+
}
|
|
11960
|
+
}
|
|
11961
|
+
|
|
11962
|
+
if (value.startsWith("#")) {
|
|
11963
|
+
return this.#normalizeHex(value);
|
|
11964
|
+
}
|
|
11965
|
+
|
|
11966
|
+
try {
|
|
11967
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
11968
|
+
if (!ctx) return "#D9D9D9";
|
|
11969
|
+
ctx.fillStyle = value;
|
|
11970
|
+
const resolved = ctx.fillStyle;
|
|
11971
|
+
if (resolved.startsWith("#")) {
|
|
11972
|
+
return this.#normalizeHex(resolved);
|
|
11973
|
+
}
|
|
11974
|
+
const rgb = resolved.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
|
11975
|
+
if (rgb) {
|
|
11976
|
+
const toHex = (v) =>
|
|
11977
|
+
Math.max(0, Math.min(255, Number(v)))
|
|
11978
|
+
.toString(16)
|
|
11979
|
+
.padStart(2, "0");
|
|
11980
|
+
return `#${toHex(rgb[1])}${toHex(rgb[2])}${toHex(rgb[3])}`.toUpperCase();
|
|
11981
|
+
}
|
|
11982
|
+
} catch {
|
|
11983
|
+
// Fall through to default.
|
|
11984
|
+
}
|
|
11985
|
+
|
|
11986
|
+
return "#D9D9D9";
|
|
11987
|
+
}
|
|
11988
|
+
|
|
11989
|
+
#syncFromAttributes() {
|
|
11990
|
+
const color = this.#normalizeColor(this.getAttribute("value"));
|
|
11991
|
+
if (this.getAttribute("value") !== color) {
|
|
11992
|
+
this.setAttribute("value", color);
|
|
11993
|
+
return;
|
|
11994
|
+
}
|
|
11995
|
+
|
|
11996
|
+
if (this.#fillPicker) {
|
|
11997
|
+
this.#fillPicker.setAttribute(
|
|
11998
|
+
"value",
|
|
11999
|
+
JSON.stringify({ type: "solid", color }),
|
|
12000
|
+
);
|
|
12001
|
+
if (this.#alphaEnabled) {
|
|
12002
|
+
this.#fillPicker.removeAttribute("alpha");
|
|
12003
|
+
} else {
|
|
12004
|
+
this.#fillPicker.setAttribute("alpha", "false");
|
|
12005
|
+
}
|
|
12006
|
+
if (this.hasAttribute("disabled")) {
|
|
12007
|
+
this.#fillPicker.setAttribute("disabled", "");
|
|
12008
|
+
} else {
|
|
12009
|
+
this.#fillPicker.removeAttribute("disabled");
|
|
12010
|
+
}
|
|
12011
|
+
}
|
|
12012
|
+
|
|
12013
|
+
if (this.#chit) {
|
|
12014
|
+
this.#chit.setAttribute("background", color);
|
|
12015
|
+
if (this.hasAttribute("disabled")) {
|
|
12016
|
+
this.#chit.setAttribute("disabled", "");
|
|
12017
|
+
} else {
|
|
12018
|
+
this.#chit.removeAttribute("disabled");
|
|
12019
|
+
}
|
|
12020
|
+
}
|
|
12021
|
+
}
|
|
12022
|
+
|
|
12023
|
+
#updateColorFromPicker(detail, type) {
|
|
12024
|
+
const nextColor = this.#normalizeColor(detail?.color);
|
|
12025
|
+
const prevColor = this.#normalizeColor(this.getAttribute("value"));
|
|
12026
|
+
if (nextColor !== prevColor) {
|
|
12027
|
+
this.setAttribute("value", nextColor);
|
|
12028
|
+
} else {
|
|
12029
|
+
this.#syncFromAttributes();
|
|
12030
|
+
}
|
|
12031
|
+
|
|
12032
|
+
const eventDetail = { color: this.value };
|
|
12033
|
+
if (this.#alphaEnabled && detail?.opacity !== undefined) {
|
|
12034
|
+
eventDetail.opacity = detail.opacity;
|
|
12035
|
+
}
|
|
12036
|
+
|
|
12037
|
+
this.dispatchEvent(
|
|
12038
|
+
new CustomEvent(type, {
|
|
12039
|
+
bubbles: true,
|
|
12040
|
+
cancelable: true,
|
|
12041
|
+
composed: true,
|
|
12042
|
+
detail: eventDetail,
|
|
12043
|
+
}),
|
|
12044
|
+
);
|
|
12045
|
+
}
|
|
12046
|
+
|
|
12047
|
+
#handlePickerInput(event) {
|
|
12048
|
+
event.stopPropagation();
|
|
12049
|
+
this.#updateColorFromPicker(event.detail, "input");
|
|
12050
|
+
}
|
|
12051
|
+
|
|
12052
|
+
#handlePickerChange(event) {
|
|
12053
|
+
event.stopPropagation();
|
|
12054
|
+
this.#updateColorFromPicker(event.detail, "change");
|
|
12055
|
+
}
|
|
12056
|
+
|
|
12057
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
12058
|
+
if (oldValue === newValue) return;
|
|
12059
|
+
if (!this.isConnected) return;
|
|
12060
|
+
|
|
12061
|
+
switch (name) {
|
|
12062
|
+
case "value":
|
|
12063
|
+
case "selected":
|
|
12064
|
+
case "disabled":
|
|
12065
|
+
this.#syncFromAttributes();
|
|
12066
|
+
break;
|
|
12067
|
+
}
|
|
12068
|
+
}
|
|
12069
|
+
|
|
12070
|
+
get value() {
|
|
12071
|
+
return this.#normalizeColor(this.getAttribute("value"));
|
|
12072
|
+
}
|
|
12073
|
+
set value(value) {
|
|
12074
|
+
if (value === null || value === undefined || value === "") {
|
|
12075
|
+
this.removeAttribute("value");
|
|
12076
|
+
return;
|
|
12077
|
+
}
|
|
12078
|
+
this.setAttribute("value", this.#normalizeColor(value));
|
|
12079
|
+
}
|
|
12080
|
+
|
|
12081
|
+
get selected() {
|
|
12082
|
+
return this.hasAttribute("selected");
|
|
12083
|
+
}
|
|
12084
|
+
set selected(value) {
|
|
12085
|
+
this.toggleAttribute("selected", Boolean(value));
|
|
12086
|
+
}
|
|
12087
|
+
|
|
12088
|
+
get disabled() {
|
|
12089
|
+
return this.hasAttribute("disabled");
|
|
12090
|
+
}
|
|
12091
|
+
set disabled(value) {
|
|
12092
|
+
this.toggleAttribute("disabled", Boolean(value));
|
|
12093
|
+
}
|
|
12094
|
+
}
|
|
12095
|
+
customElements.define("fig-color-tip", FigColorTip);
|
|
12096
|
+
|
|
10910
12097
|
/* Choice */
|
|
10911
12098
|
/**
|
|
10912
12099
|
* A generic choice container for use within FigChooser.
|
|
@@ -11471,8 +12658,354 @@ customElements.define("fig-chooser", FigChooser);
|
|
|
11471
12658
|
|
|
11472
12659
|
/* Handle */
|
|
11473
12660
|
class FigHandle extends HTMLElement {
|
|
11474
|
-
|
|
11475
|
-
|
|
12661
|
+
static observedAttributes = [
|
|
12662
|
+
"color",
|
|
12663
|
+
"selected",
|
|
12664
|
+
"disabled",
|
|
12665
|
+
"drag",
|
|
12666
|
+
"drag-surface",
|
|
12667
|
+
"drag-axes",
|
|
12668
|
+
"drag-snapping",
|
|
12669
|
+
"value",
|
|
12670
|
+
"type",
|
|
12671
|
+
];
|
|
12672
|
+
|
|
12673
|
+
#isDragging = false;
|
|
12674
|
+
#didDrag = false;
|
|
12675
|
+
#boundPointerDown = null;
|
|
12676
|
+
#applyingValue = false;
|
|
12677
|
+
#colorTip = null;
|
|
12678
|
+
|
|
12679
|
+
get #dragEnabled() {
|
|
12680
|
+
const v = this.getAttribute("drag");
|
|
12681
|
+
return v !== null && v !== "false";
|
|
12682
|
+
}
|
|
12683
|
+
|
|
12684
|
+
get #axes() {
|
|
12685
|
+
const v = (this.getAttribute("drag-axes") || "x,y").toLowerCase();
|
|
12686
|
+
return { x: v.includes("x"), y: v.includes("y") };
|
|
12687
|
+
}
|
|
12688
|
+
|
|
12689
|
+
get #dragSnappingMode() {
|
|
12690
|
+
const raw = this.getAttribute("drag-snapping");
|
|
12691
|
+
if (raw === null) return "false";
|
|
12692
|
+
const normalized = raw.trim().toLowerCase();
|
|
12693
|
+
if (normalized === "modifier") return "modifier";
|
|
12694
|
+
if (normalized === "" || normalized === "true") return "true";
|
|
12695
|
+
return "false";
|
|
12696
|
+
}
|
|
12697
|
+
|
|
12698
|
+
#shouldSnap(shiftKey) {
|
|
12699
|
+
const mode = this.#dragSnappingMode;
|
|
12700
|
+
if (mode === "true") return true;
|
|
12701
|
+
if (mode === "modifier") return !!shiftKey;
|
|
12702
|
+
return false;
|
|
12703
|
+
}
|
|
12704
|
+
|
|
12705
|
+
#snapGuide(value) {
|
|
12706
|
+
if (value < 0.1) return 0;
|
|
12707
|
+
if (value > 0.9) return 1;
|
|
12708
|
+
if (value > 0.4 && value < 0.6) return 0.5;
|
|
12709
|
+
return value;
|
|
12710
|
+
}
|
|
12711
|
+
|
|
12712
|
+
#snapDiagonal(x, y) {
|
|
12713
|
+
const diff = Math.abs(x - y);
|
|
12714
|
+
if (diff < 0.1) {
|
|
12715
|
+
const avg = (x + y) / 2;
|
|
12716
|
+
return { x: avg, y: avg };
|
|
12717
|
+
}
|
|
12718
|
+
if (Math.abs(1 - x - y) < 0.1) return { x, y: 1 - x };
|
|
12719
|
+
return { x, y };
|
|
12720
|
+
}
|
|
12721
|
+
|
|
12722
|
+
#getContainer() {
|
|
12723
|
+
const attr = this.getAttribute("drag-surface");
|
|
12724
|
+
if (!attr || attr === "parent") return this.parentElement;
|
|
12725
|
+
return this.closest(attr);
|
|
12726
|
+
}
|
|
12727
|
+
|
|
12728
|
+
get value() {
|
|
12729
|
+
const container = this.#getContainer();
|
|
12730
|
+
if (!container) return "0% 0%";
|
|
12731
|
+
const rect = container.getBoundingClientRect();
|
|
12732
|
+
const hw = this.offsetWidth / 2;
|
|
12733
|
+
const hh = this.offsetHeight / 2;
|
|
12734
|
+
const x = parseFloat(this.style.left) || 0;
|
|
12735
|
+
const y = parseFloat(this.style.top) || 0;
|
|
12736
|
+
const px = rect.width > 0 ? ((x + hw) / rect.width) * 100 : 0;
|
|
12737
|
+
const py = rect.height > 0 ? ((y + hh) / rect.height) * 100 : 0;
|
|
12738
|
+
return `${Math.round(px)}% ${Math.round(py)}%`;
|
|
12739
|
+
}
|
|
12740
|
+
|
|
12741
|
+
set value(v) {
|
|
12742
|
+
this.setAttribute("value", v ?? "0% 0%");
|
|
12743
|
+
}
|
|
12744
|
+
|
|
12745
|
+
#parseValue(str) {
|
|
12746
|
+
const normalized = str == null ? "" : String(str).trim();
|
|
12747
|
+
if (!normalized) return { xPct: 0, yPct: 0 };
|
|
12748
|
+
|
|
12749
|
+
const parts = normalized.split(/[\s,]+/).filter(Boolean);
|
|
12750
|
+
|
|
12751
|
+
const parseToken = (token) => {
|
|
12752
|
+
if (!token) return 0;
|
|
12753
|
+
const hasPx = token.includes("px");
|
|
12754
|
+
const hasPct = token.includes("%");
|
|
12755
|
+
const numeric = parseFloat(token.replace(/[%px]/g, ""));
|
|
12756
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
12757
|
+
if (hasPx) return { px: numeric };
|
|
12758
|
+
if (hasPct || Math.abs(numeric) > 1)
|
|
12759
|
+
return Math.max(0, Math.min(100, numeric));
|
|
12760
|
+
return Math.max(0, Math.min(100, numeric * 100));
|
|
12761
|
+
};
|
|
12762
|
+
|
|
12763
|
+
const xToken = parseToken(parts[0]);
|
|
12764
|
+
const yToken = parseToken(parts[1] ?? parts[0]);
|
|
12765
|
+
return { xToken, yToken };
|
|
12766
|
+
}
|
|
12767
|
+
|
|
12768
|
+
#applyValue(str) {
|
|
12769
|
+
const container = this.#getContainer();
|
|
12770
|
+
if (!container) return;
|
|
12771
|
+
|
|
12772
|
+
const { xToken, yToken } = this.#parseValue(str);
|
|
12773
|
+
const rect = container.getBoundingClientRect();
|
|
12774
|
+
const hw = this.offsetWidth / 2;
|
|
12775
|
+
const hh = this.offsetHeight / 2;
|
|
12776
|
+
|
|
12777
|
+
const resolve = (token, containerDim, halfHandle) => {
|
|
12778
|
+
if (token && typeof token === "object" && "px" in token) {
|
|
12779
|
+
return Math.max(
|
|
12780
|
+
-halfHandle,
|
|
12781
|
+
Math.min(containerDim - halfHandle, token.px - halfHandle),
|
|
12782
|
+
);
|
|
12783
|
+
}
|
|
12784
|
+
const pct = typeof token === "number" ? token : 0;
|
|
12785
|
+
const center = (pct / 100) * containerDim;
|
|
12786
|
+
return Math.max(
|
|
12787
|
+
-halfHandle,
|
|
12788
|
+
Math.min(containerDim - halfHandle, center - halfHandle),
|
|
12789
|
+
);
|
|
12790
|
+
};
|
|
12791
|
+
|
|
12792
|
+
const axes = this.#axes;
|
|
12793
|
+
if (axes.x) this.style.left = `${resolve(xToken, rect.width, hw)}px`;
|
|
12794
|
+
if (axes.y) this.style.top = `${resolve(yToken, rect.height, hh)}px`;
|
|
12795
|
+
}
|
|
12796
|
+
|
|
12797
|
+
#syncValueAttribute() {
|
|
12798
|
+
this.#applyingValue = true;
|
|
12799
|
+
this.setAttribute("value", this.value);
|
|
12800
|
+
this.#applyingValue = false;
|
|
12801
|
+
}
|
|
12802
|
+
|
|
12803
|
+
connectedCallback() {
|
|
12804
|
+
this.#syncDrag();
|
|
12805
|
+
this.addEventListener("click", this.#handleSelect);
|
|
12806
|
+
document.addEventListener("pointerdown", this.#handleDeselect);
|
|
12807
|
+
const initial = this.getAttribute("value");
|
|
12808
|
+
if (initial) this.#applyValue(initial);
|
|
12809
|
+
}
|
|
12810
|
+
|
|
12811
|
+
disconnectedCallback() {
|
|
12812
|
+
this.#teardownDrag();
|
|
12813
|
+
this.#hideColorTip();
|
|
12814
|
+
this.removeEventListener("click", this.#handleSelect);
|
|
12815
|
+
document.removeEventListener("pointerdown", this.#handleDeselect);
|
|
12816
|
+
}
|
|
12817
|
+
|
|
12818
|
+
#handleSelect = (e) => {
|
|
12819
|
+
if (this.hasAttribute("disabled")) return;
|
|
12820
|
+
if (this.#didDrag) {
|
|
12821
|
+
this.#didDrag = false;
|
|
12822
|
+
return;
|
|
12823
|
+
}
|
|
12824
|
+
this.setAttribute("selected", "");
|
|
12825
|
+
if (this.getAttribute("type") === "color") this.#showColorTip();
|
|
12826
|
+
};
|
|
12827
|
+
|
|
12828
|
+
#handleDeselect = (e) => {
|
|
12829
|
+
if (this.contains(e.target)) return;
|
|
12830
|
+
if (this.#colorTip && e.target.closest?.("dialog, [popover]")) return;
|
|
12831
|
+
this.removeAttribute("selected");
|
|
12832
|
+
this.#hideColorTip();
|
|
12833
|
+
};
|
|
12834
|
+
|
|
12835
|
+
attributeChangedCallback(name, _old, value) {
|
|
12836
|
+
if (name === "color") {
|
|
12837
|
+
if (!value || value === "false") {
|
|
12838
|
+
this.style.removeProperty("--fill");
|
|
12839
|
+
} else {
|
|
12840
|
+
this.style.setProperty("--fill", value);
|
|
12841
|
+
}
|
|
12842
|
+
}
|
|
12843
|
+
if (name === "drag") this.#syncDrag();
|
|
12844
|
+
if (name === "value" && !this.#applyingValue && !this.#isDragging) {
|
|
12845
|
+
this.#applyValue(value);
|
|
12846
|
+
}
|
|
12847
|
+
}
|
|
12848
|
+
|
|
12849
|
+
#syncDrag() {
|
|
12850
|
+
if (this.#dragEnabled && !this.#boundPointerDown) {
|
|
12851
|
+
this.#boundPointerDown = (e) => this.#onPointerDown(e);
|
|
12852
|
+
this.addEventListener("pointerdown", this.#boundPointerDown);
|
|
12853
|
+
} else if (!this.#dragEnabled && this.#boundPointerDown) {
|
|
12854
|
+
this.#teardownDrag();
|
|
12855
|
+
}
|
|
12856
|
+
}
|
|
12857
|
+
|
|
12858
|
+
#teardownDrag() {
|
|
12859
|
+
if (this.#boundPointerDown) {
|
|
12860
|
+
this.removeEventListener("pointerdown", this.#boundPointerDown);
|
|
12861
|
+
this.#boundPointerDown = null;
|
|
12862
|
+
}
|
|
12863
|
+
this.#isDragging = false;
|
|
12864
|
+
}
|
|
12865
|
+
|
|
12866
|
+
#onPointerDown(e) {
|
|
12867
|
+
if (!this.#dragEnabled || this.hasAttribute("disabled")) return;
|
|
12868
|
+
e.preventDefault();
|
|
12869
|
+
const container = this.#getContainer();
|
|
12870
|
+
if (!container) return;
|
|
12871
|
+
|
|
12872
|
+
this.#isDragging = true;
|
|
12873
|
+
const axes = this.#axes;
|
|
12874
|
+
const containerRect = container.getBoundingClientRect();
|
|
12875
|
+
const handleW = this.offsetWidth;
|
|
12876
|
+
const handleH = this.offsetHeight;
|
|
12877
|
+
|
|
12878
|
+
const clampAndApply = (clientX, clientY, shiftKey = false) => {
|
|
12879
|
+
const rect = container.getBoundingClientRect();
|
|
12880
|
+
const currentLeft = parseFloat(this.style.left) || 0;
|
|
12881
|
+
const currentTop = parseFloat(this.style.top) || 0;
|
|
12882
|
+
const rawX = clientX - rect.left - handleW / 2;
|
|
12883
|
+
const rawY = clientY - rect.top - handleH / 2;
|
|
12884
|
+
|
|
12885
|
+
const clampedX = Math.max(
|
|
12886
|
+
-handleW / 2,
|
|
12887
|
+
Math.min(rect.width - handleW / 2, rawX),
|
|
12888
|
+
);
|
|
12889
|
+
const clampedY = Math.max(
|
|
12890
|
+
-handleH / 2,
|
|
12891
|
+
Math.min(rect.height - handleH / 2, rawY),
|
|
12892
|
+
);
|
|
12893
|
+
|
|
12894
|
+
let centerX =
|
|
12895
|
+
rect.width > 0
|
|
12896
|
+
? ((axes.x ? clampedX : currentLeft) + handleW / 2) / rect.width
|
|
12897
|
+
: 0.5;
|
|
12898
|
+
let centerY =
|
|
12899
|
+
rect.height > 0
|
|
12900
|
+
? ((axes.y ? clampedY : currentTop) + handleH / 2) / rect.height
|
|
12901
|
+
: 0.5;
|
|
12902
|
+
|
|
12903
|
+
if (this.#shouldSnap(shiftKey)) {
|
|
12904
|
+
if (axes.x) centerX = this.#snapGuide(centerX);
|
|
12905
|
+
if (axes.y) centerY = this.#snapGuide(centerY);
|
|
12906
|
+
if (axes.x && axes.y) {
|
|
12907
|
+
const diagonal = this.#snapDiagonal(centerX, centerY);
|
|
12908
|
+
centerX = diagonal.x;
|
|
12909
|
+
centerY = diagonal.y;
|
|
12910
|
+
}
|
|
12911
|
+
}
|
|
12912
|
+
|
|
12913
|
+
if (axes.x) {
|
|
12914
|
+
const left = centerX * rect.width - handleW / 2;
|
|
12915
|
+
this.style.left = `${Math.max(-handleW / 2, Math.min(rect.width - handleW / 2, left))}px`;
|
|
12916
|
+
}
|
|
12917
|
+
if (axes.y) {
|
|
12918
|
+
const top = centerY * rect.height - handleH / 2;
|
|
12919
|
+
this.style.top = `${Math.max(-handleH / 2, Math.min(rect.height - handleH / 2, top))}px`;
|
|
12920
|
+
}
|
|
12921
|
+
};
|
|
12922
|
+
|
|
12923
|
+
const isColorType = this.getAttribute("type") === "color";
|
|
12924
|
+
if (!isColorType) {
|
|
12925
|
+
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
12926
|
+
}
|
|
12927
|
+
this.style.cursor = "grabbing";
|
|
12928
|
+
if (!isColorType) {
|
|
12929
|
+
this.dispatchEvent(
|
|
12930
|
+
new CustomEvent("input", {
|
|
12931
|
+
bubbles: true,
|
|
12932
|
+
detail: this.#positionDetail(containerRect),
|
|
12933
|
+
}),
|
|
12934
|
+
);
|
|
12935
|
+
}
|
|
12936
|
+
|
|
12937
|
+
const onMove = (e) => {
|
|
12938
|
+
if (!this.#isDragging) return;
|
|
12939
|
+
this.#didDrag = true;
|
|
12940
|
+
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
12941
|
+
this.dispatchEvent(
|
|
12942
|
+
new CustomEvent("input", {
|
|
12943
|
+
bubbles: true,
|
|
12944
|
+
detail: this.#positionDetail(container.getBoundingClientRect()),
|
|
12945
|
+
}),
|
|
12946
|
+
);
|
|
12947
|
+
};
|
|
12948
|
+
|
|
12949
|
+
const onUp = (e) => {
|
|
12950
|
+
this.#isDragging = false;
|
|
12951
|
+
this.style.cursor = "";
|
|
12952
|
+
window.removeEventListener("pointermove", onMove);
|
|
12953
|
+
window.removeEventListener("pointerup", onUp);
|
|
12954
|
+
if (this.#didDrag || !isColorType) {
|
|
12955
|
+
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
12956
|
+
}
|
|
12957
|
+
this.#syncValueAttribute();
|
|
12958
|
+
this.dispatchEvent(
|
|
12959
|
+
new CustomEvent("change", {
|
|
12960
|
+
bubbles: true,
|
|
12961
|
+
detail: this.#positionDetail(container.getBoundingClientRect()),
|
|
12962
|
+
}),
|
|
12963
|
+
);
|
|
12964
|
+
};
|
|
12965
|
+
|
|
12966
|
+
window.addEventListener("pointermove", onMove);
|
|
12967
|
+
window.addEventListener("pointerup", onUp);
|
|
12968
|
+
}
|
|
12969
|
+
|
|
12970
|
+
#showColorTip() {
|
|
12971
|
+
if (this.#colorTip) return;
|
|
12972
|
+
const tip = document.createElement("fig-color-tip");
|
|
12973
|
+
tip.setAttribute("value", this.getAttribute("color") || "#D9D9D9");
|
|
12974
|
+
tip.setAttribute("selected", "");
|
|
12975
|
+
tip.setAttribute("alpha", "true");
|
|
12976
|
+
tip.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
12977
|
+
tip.addEventListener("input", this.#handleColorTipInput);
|
|
12978
|
+
tip.addEventListener("change", this.#handleColorTipChange);
|
|
12979
|
+
this.appendChild(tip);
|
|
12980
|
+
this.#colorTip = tip;
|
|
12981
|
+
}
|
|
12982
|
+
|
|
12983
|
+
#hideColorTip() {
|
|
12984
|
+
if (!this.#colorTip) return;
|
|
12985
|
+
this.#colorTip.removeEventListener("input", this.#handleColorTipInput);
|
|
12986
|
+
this.#colorTip.removeEventListener("change", this.#handleColorTipChange);
|
|
12987
|
+
this.#colorTip.remove();
|
|
12988
|
+
this.#colorTip = null;
|
|
12989
|
+
}
|
|
12990
|
+
|
|
12991
|
+
#handleColorTipInput = (e) => {
|
|
12992
|
+
e.stopPropagation();
|
|
12993
|
+
if (e.detail?.color) this.setAttribute("color", e.detail.color);
|
|
12994
|
+
};
|
|
12995
|
+
|
|
12996
|
+
#handleColorTipChange = (e) => {
|
|
12997
|
+
e.stopPropagation();
|
|
12998
|
+
if (e.detail?.color) this.setAttribute("color", e.detail.color);
|
|
12999
|
+
};
|
|
13000
|
+
|
|
13001
|
+
#positionDetail(containerRect) {
|
|
13002
|
+
const hw = this.offsetWidth / 2;
|
|
13003
|
+
const hh = this.offsetHeight / 2;
|
|
13004
|
+
const x = parseFloat(this.style.left) || 0;
|
|
13005
|
+
const y = parseFloat(this.style.top) || 0;
|
|
13006
|
+
const px = containerRect.width > 0 ? (x + hw) / containerRect.width : 0;
|
|
13007
|
+
const py = containerRect.height > 0 ? (y + hh) / containerRect.height : 0;
|
|
13008
|
+
return { x, y, px, py };
|
|
11476
13009
|
}
|
|
11477
13010
|
}
|
|
11478
13011
|
customElements.define("fig-handle", FigHandle);
|