@rogieking/figui3 3.9.3 → 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/components.css +598 -299
- package/dist/fig.js +133 -76
- package/fig.js +1830 -280
- 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,391 @@ 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
|
+
|
|
5769
|
+
const ghost = document.createElement("fig-handle");
|
|
5770
|
+
ghost.classList.add("fig-input-gradient-ghost");
|
|
5771
|
+
ghost.setAttribute("add", "");
|
|
5772
|
+
ghost.style.position = "absolute";
|
|
5773
|
+
ghost.style.top = "50%";
|
|
5774
|
+
ghost.style.transform = "translate(-50%, -50%)";
|
|
5775
|
+
ghost.style.pointerEvents = "none";
|
|
5776
|
+
ghost.style.opacity = "0";
|
|
5777
|
+
ghost.style.transition = "opacity 0.15s";
|
|
5778
|
+
|
|
5779
|
+
this.#track.appendChild(ghost);
|
|
5780
|
+
this.#ghostHandle = ghost;
|
|
5781
|
+
this.#ghostTooltip = null;
|
|
5782
|
+
|
|
5783
|
+
this.addEventListener("pointerenter", this.#onTrackEnter);
|
|
5784
|
+
this.addEventListener("pointermove", this.#onTrackMove);
|
|
5785
|
+
this.addEventListener("pointerleave", this.#onTrackLeave);
|
|
5786
|
+
this.addEventListener("click", this.#onTrackClick);
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
#showGhost() {
|
|
5790
|
+
if (!this.#ghostHandle) return;
|
|
5791
|
+
this.#ghostHandle.style.opacity = "1";
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
#hideGhost() {
|
|
5795
|
+
if (!this.#ghostHandle) return;
|
|
5796
|
+
this.#ghostHandle.style.opacity = "0";
|
|
5797
|
+
}
|
|
5798
|
+
|
|
5799
|
+
#onTrackEnter = () => {
|
|
5800
|
+
this.#showGhost();
|
|
5801
|
+
};
|
|
5802
|
+
|
|
5803
|
+
#onTrackLeave = () => {
|
|
5804
|
+
this.#hideGhost();
|
|
5805
|
+
};
|
|
5806
|
+
|
|
5807
|
+
#onTrackMove = (e) => {
|
|
5808
|
+
if (!this.#ghostHandle || !this.#track) return;
|
|
5809
|
+
if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) {
|
|
5810
|
+
this.#hideGhost();
|
|
5811
|
+
return;
|
|
5812
|
+
}
|
|
5813
|
+
const trackRect = this.#track.getBoundingClientRect();
|
|
5814
|
+
const pct = Math.max(
|
|
5815
|
+
0,
|
|
5816
|
+
Math.min(1, (e.clientX - trackRect.left) / trackRect.width),
|
|
5817
|
+
);
|
|
5818
|
+
this.#ghostHandle.style.left = `${pct * 100}%`;
|
|
5819
|
+
const color = this.#sampleGradientColor(pct);
|
|
5820
|
+
this.#ghostHandle.setAttribute("color", color);
|
|
5821
|
+
this.#showGhost();
|
|
5822
|
+
};
|
|
5823
|
+
|
|
5824
|
+
#onTrackClick = (e) => {
|
|
5825
|
+
if (!this.#track) return;
|
|
5826
|
+
if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
|
|
5827
|
+
const trackRect = this.#track.getBoundingClientRect();
|
|
5828
|
+
const pct = Math.max(
|
|
5829
|
+
0,
|
|
5830
|
+
Math.min(1, (e.clientX - trackRect.left) / trackRect.width),
|
|
5831
|
+
);
|
|
5832
|
+
const position = Math.round(pct * 100);
|
|
5833
|
+
const color = this.#sampleGradientColor(pct);
|
|
5834
|
+
this.#gradient.stops.push({ position, color, opacity: 100 });
|
|
5835
|
+
this.#gradient.stops.sort((a, b) => a.position - b.position);
|
|
5836
|
+
const newIndex = this.#gradient.stops.findIndex(
|
|
5837
|
+
(s) => s.position === position && s.color === color,
|
|
5838
|
+
);
|
|
5839
|
+
this.#syncHandles();
|
|
5840
|
+
this.#syncFillPicker();
|
|
5841
|
+
this.#emitInput();
|
|
5842
|
+
this.#emitChange();
|
|
5843
|
+
|
|
5844
|
+
requestAnimationFrame(() => {
|
|
5845
|
+
const handles = this.#track.querySelectorAll(
|
|
5846
|
+
"fig-handle:not(.fig-input-gradient-ghost)",
|
|
5847
|
+
);
|
|
5848
|
+
const newHandle = handles[newIndex];
|
|
5849
|
+
if (newHandle) newHandle.click();
|
|
5850
|
+
});
|
|
5851
|
+
};
|
|
5852
|
+
|
|
5853
|
+
#syncHandles() {
|
|
5854
|
+
if (!this.#track) return;
|
|
5855
|
+
const handles = this.#track.querySelectorAll(
|
|
5856
|
+
"fig-handle:not(.fig-input-gradient-ghost)",
|
|
5857
|
+
);
|
|
5858
|
+
const stops = this.#gradient.stops;
|
|
5859
|
+
|
|
5860
|
+
if (handles.length !== stops.length) {
|
|
5861
|
+
const ghost = this.#ghostHandle;
|
|
5862
|
+
this.#track.innerHTML = this.#buildStopHandles();
|
|
5863
|
+
if (ghost) this.#track.appendChild(ghost);
|
|
5864
|
+
this.#reobserveHandleColors();
|
|
5865
|
+
return;
|
|
5866
|
+
}
|
|
5867
|
+
|
|
5868
|
+
for (let i = 0; i < stops.length; i++) {
|
|
5869
|
+
const h = handles[i];
|
|
5870
|
+
const stop = stops[i];
|
|
5871
|
+
h.setAttribute("value", `${stop.position}% 50%`);
|
|
5872
|
+
h.setAttribute("color", stop.color);
|
|
5873
|
+
}
|
|
5874
|
+
}
|
|
5875
|
+
|
|
5876
|
+
#reobserveHandleColors() {
|
|
5877
|
+
if (!this.#colorObserver || !this.#track) return;
|
|
5878
|
+
this.#colorObserver.disconnect();
|
|
5879
|
+
this.#track
|
|
5880
|
+
.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
|
|
5881
|
+
.forEach((h) => {
|
|
5882
|
+
this.#colorObserver.observe(h, {
|
|
5883
|
+
attributes: true,
|
|
5884
|
+
attributeFilter: ["color"],
|
|
5885
|
+
});
|
|
5886
|
+
});
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
#syncFillPicker() {
|
|
5890
|
+
if (!this.#fillPicker) return;
|
|
5891
|
+
this.#fillPicker.setAttribute("value", JSON.stringify(this.value));
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
#setupEventListeners() {
|
|
5895
|
+
requestAnimationFrame(() => {
|
|
5896
|
+
this.#fillPicker = this.querySelector("fig-fill-picker");
|
|
5897
|
+
if (!this.#fillPicker) return;
|
|
5898
|
+
|
|
5899
|
+
const anchor = this.getAttribute("picker-anchor");
|
|
5900
|
+
if (!anchor || anchor === "self") {
|
|
5901
|
+
this.#fillPicker.anchorElement = this;
|
|
5902
|
+
} else {
|
|
5903
|
+
const el = document.querySelector(anchor);
|
|
5904
|
+
if (el) this.#fillPicker.anchorElement = el;
|
|
5905
|
+
}
|
|
5906
|
+
|
|
5907
|
+
this.#fillPicker.addEventListener("input", (e) => {
|
|
5908
|
+
e.stopPropagation();
|
|
5909
|
+
const detail = e.detail;
|
|
5910
|
+
if (detail?.gradient) {
|
|
5911
|
+
this.#gradient = normalizeGradientConfig({
|
|
5912
|
+
...this.#gradient,
|
|
5913
|
+
...detail.gradient,
|
|
5914
|
+
});
|
|
5915
|
+
this.#syncHandles();
|
|
5916
|
+
this.#emitInput();
|
|
5917
|
+
}
|
|
5918
|
+
});
|
|
5919
|
+
|
|
5920
|
+
this.#fillPicker.addEventListener("change", (e) => {
|
|
5921
|
+
e.stopPropagation();
|
|
5922
|
+
this.#emitChange();
|
|
5923
|
+
});
|
|
5924
|
+
|
|
5925
|
+
if (this.#track) {
|
|
5926
|
+
this.#track.addEventListener("input", (e) => {
|
|
5927
|
+
const handle = e.target.closest("fig-handle");
|
|
5928
|
+
if (!handle) return;
|
|
5929
|
+
e.stopPropagation();
|
|
5930
|
+
const idx = parseInt(handle.dataset.stopIndex, 10);
|
|
5931
|
+
if (isNaN(idx) || !this.#gradient.stops[idx]) return;
|
|
5932
|
+
const px = e.detail?.px ?? 0;
|
|
5933
|
+
this.#gradient.stops[idx].position = Math.round(px * 100);
|
|
5934
|
+
this.#syncFillPicker();
|
|
5935
|
+
this.#emitInput();
|
|
5936
|
+
});
|
|
5937
|
+
|
|
5938
|
+
this.#track.addEventListener("change", (e) => {
|
|
5939
|
+
const handle = e.target.closest("fig-handle");
|
|
5940
|
+
if (!handle) return;
|
|
5941
|
+
e.stopPropagation();
|
|
5942
|
+
const idx = parseInt(handle.dataset.stopIndex, 10);
|
|
5943
|
+
if (isNaN(idx) || !this.#gradient.stops[idx]) return;
|
|
5944
|
+
const px = e.detail?.px ?? 0;
|
|
5945
|
+
this.#gradient.stops[idx].position = Math.round(px * 100);
|
|
5946
|
+
this.#syncFillPicker();
|
|
5947
|
+
this.#emitChange();
|
|
5948
|
+
});
|
|
5949
|
+
|
|
5950
|
+
this.#colorObserver = new MutationObserver((mutations) => {
|
|
5951
|
+
for (const m of mutations) {
|
|
5952
|
+
if (m.attributeName !== "color") continue;
|
|
5953
|
+
const handle = m.target;
|
|
5954
|
+
if (handle.classList.contains("fig-input-gradient-ghost")) continue;
|
|
5955
|
+
const idx = parseInt(handle.dataset.stopIndex, 10);
|
|
5956
|
+
if (isNaN(idx) || !this.#gradient.stops[idx]) continue;
|
|
5957
|
+
const newColor = handle.getAttribute("color");
|
|
5958
|
+
if (newColor && newColor !== this.#gradient.stops[idx].color) {
|
|
5959
|
+
this.#gradient.stops[idx].color = newColor;
|
|
5960
|
+
this.#syncFillPicker();
|
|
5961
|
+
this.#emitInput();
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
});
|
|
5965
|
+
this.#track
|
|
5966
|
+
.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
|
|
5967
|
+
.forEach((h) => {
|
|
5968
|
+
this.#colorObserver.observe(h, {
|
|
5969
|
+
attributes: true,
|
|
5970
|
+
attributeFilter: ["color"],
|
|
5971
|
+
});
|
|
5972
|
+
});
|
|
5973
|
+
}
|
|
5974
|
+
});
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
#emitInput() {
|
|
5978
|
+
this.dispatchEvent(
|
|
5979
|
+
new CustomEvent("input", {
|
|
5980
|
+
bubbles: true,
|
|
5981
|
+
detail: this.value,
|
|
5982
|
+
}),
|
|
5983
|
+
);
|
|
5984
|
+
}
|
|
5985
|
+
|
|
5986
|
+
#emitChange() {
|
|
5987
|
+
this.dispatchEvent(
|
|
5988
|
+
new CustomEvent("change", {
|
|
5989
|
+
bubbles: true,
|
|
5990
|
+
detail: this.value,
|
|
5991
|
+
}),
|
|
5992
|
+
);
|
|
5993
|
+
}
|
|
5994
|
+
|
|
5995
|
+
get value() {
|
|
5996
|
+
return {
|
|
5997
|
+
type: "gradient",
|
|
5998
|
+
gradient: gradientToValueShape(this.#gradient),
|
|
5999
|
+
};
|
|
6000
|
+
}
|
|
6001
|
+
|
|
6002
|
+
set value(val) {
|
|
6003
|
+
if (typeof val === "string") {
|
|
6004
|
+
this.setAttribute("value", val);
|
|
6005
|
+
} else {
|
|
6006
|
+
this.setAttribute("value", JSON.stringify(val));
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
6011
|
+
if (oldValue === newValue) return;
|
|
6012
|
+
switch (name) {
|
|
6013
|
+
case "value":
|
|
6014
|
+
this.#parseValue();
|
|
6015
|
+
if (this.#fillPicker) {
|
|
6016
|
+
this.#syncFillPicker();
|
|
6017
|
+
}
|
|
6018
|
+
this.#syncHandles();
|
|
6019
|
+
break;
|
|
6020
|
+
case "disabled":
|
|
6021
|
+
case "experimental":
|
|
6022
|
+
case "picker-anchor":
|
|
6023
|
+
if (this.#fillPicker) this.#render();
|
|
6024
|
+
break;
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
customElements.define("fig-input-gradient", FigInputGradient);
|
|
6029
|
+
|
|
5485
6030
|
/* Checkbox */
|
|
5486
6031
|
/**
|
|
5487
6032
|
* A custom checkbox input element.
|
|
@@ -7150,7 +7695,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
7150
7695
|
);
|
|
7151
7696
|
if (bezierSurface) {
|
|
7152
7697
|
bezierSurface.addEventListener("pointerdown", (e) => {
|
|
7153
|
-
if (e.target?.closest?.(".fig-easing-curve-handle, fig-handle"))
|
|
7698
|
+
if (e.target?.closest?.(".fig-easing-curve-handle, fig-handle"))
|
|
7699
|
+
return;
|
|
7154
7700
|
this.#startBezierDrag(e, this.#bezierHandleForClientHalf(e));
|
|
7155
7701
|
});
|
|
7156
7702
|
}
|
|
@@ -7169,7 +7715,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
7169
7715
|
);
|
|
7170
7716
|
if (springSurface) {
|
|
7171
7717
|
springSurface.addEventListener("pointerdown", (e) => {
|
|
7172
|
-
if (e.target?.closest?.(".fig-easing-curve-handle, fig-handle"))
|
|
7718
|
+
if (e.target?.closest?.(".fig-easing-curve-handle, fig-handle"))
|
|
7719
|
+
return;
|
|
7173
7720
|
this.#startSpringDrag(e, "duration");
|
|
7174
7721
|
});
|
|
7175
7722
|
}
|
|
@@ -8146,10 +8693,10 @@ customElements.define("fig-origin-grid", FigOriginGrid);
|
|
|
8146
8693
|
* @attr {string} axis-labels - Space-delimited labels. 1 token: top. 2 tokens: x y. 4 tokens: left right top bottom.
|
|
8147
8694
|
*/
|
|
8148
8695
|
class FigInputJoystick extends HTMLElement {
|
|
8149
|
-
#
|
|
8150
|
-
#
|
|
8151
|
-
#
|
|
8152
|
-
#
|
|
8696
|
+
#boundPlanePointerDown = null;
|
|
8697
|
+
#boundHandlePointerDown = null;
|
|
8698
|
+
#boundHandleInput = null;
|
|
8699
|
+
#boundHandleChange = null;
|
|
8153
8700
|
#boundXInput = null;
|
|
8154
8701
|
#boundYInput = null;
|
|
8155
8702
|
#boundXFocusOut = null;
|
|
@@ -8162,17 +8709,19 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8162
8709
|
|
|
8163
8710
|
this.position = { x: 0.5, y: 0.5 };
|
|
8164
8711
|
this.isDragging = false;
|
|
8165
|
-
this.isShiftHeld = false;
|
|
8166
8712
|
this.plane = null;
|
|
8167
8713
|
this.cursor = null;
|
|
8168
8714
|
this.xInput = null;
|
|
8169
8715
|
this.yInput = null;
|
|
8170
8716
|
this.coordinates = "screen";
|
|
8171
8717
|
this.#initialized = false;
|
|
8172
|
-
this.#
|
|
8173
|
-
this.#
|
|
8174
|
-
|
|
8175
|
-
|
|
8718
|
+
this.#boundPlanePointerDown = (e) => this.#handlePlanePointerDown(e);
|
|
8719
|
+
this.#boundHandlePointerDown = () => {
|
|
8720
|
+
this.isDragging = true;
|
|
8721
|
+
this.plane?.classList.add("dragging");
|
|
8722
|
+
};
|
|
8723
|
+
this.#boundHandleInput = (e) => this.#handleHandleInput(e);
|
|
8724
|
+
this.#boundHandleChange = () => this.#handleHandleChange();
|
|
8176
8725
|
this.#boundXInput = (e) => this.#handleXInput(e);
|
|
8177
8726
|
this.#boundYInput = (e) => this.#handleYInput(e);
|
|
8178
8727
|
this.#boundXFocusOut = () => this.#handleFieldFocusOut();
|
|
@@ -8278,7 +8827,7 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8278
8827
|
${labelsMarkup}
|
|
8279
8828
|
<div class="fig-input-joystick-plane">
|
|
8280
8829
|
<div class="fig-input-joystick-guides"></div>
|
|
8281
|
-
<fig-handle></fig-handle>
|
|
8830
|
+
<fig-handle drag drag-surface=".fig-input-joystick-plane" drag-axes="x,y" drag-snapping="modifier"></fig-handle>
|
|
8282
8831
|
</div>
|
|
8283
8832
|
<fig-tooltip text="Reset">
|
|
8284
8833
|
<fig-button variant="ghost" icon="true" class="fig-joystick-reset" aria-label="Reset to default">
|
|
@@ -8318,10 +8867,10 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8318
8867
|
this.cursor = this.querySelector("fig-handle");
|
|
8319
8868
|
this.xInput = this.querySelector("fig-input-number[name='x']");
|
|
8320
8869
|
this.yInput = this.querySelector("fig-input-number[name='y']");
|
|
8321
|
-
this.plane
|
|
8322
|
-
this.
|
|
8323
|
-
|
|
8324
|
-
|
|
8870
|
+
this.plane?.addEventListener("pointerdown", this.#boundPlanePointerDown);
|
|
8871
|
+
this.cursor?.addEventListener("pointerdown", this.#boundHandlePointerDown);
|
|
8872
|
+
this.cursor?.addEventListener("input", this.#boundHandleInput);
|
|
8873
|
+
this.cursor?.addEventListener("change", this.#boundHandleChange);
|
|
8325
8874
|
const resetBtn = this.querySelector(".fig-joystick-reset");
|
|
8326
8875
|
if (resetBtn) {
|
|
8327
8876
|
resetBtn.addEventListener("click", () => this.#resetToDefault());
|
|
@@ -8337,12 +8886,15 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8337
8886
|
}
|
|
8338
8887
|
|
|
8339
8888
|
#cleanupListeners() {
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8889
|
+
this.plane?.removeEventListener("pointerdown", this.#boundPlanePointerDown);
|
|
8890
|
+
this.cursor?.removeEventListener(
|
|
8891
|
+
"pointerdown",
|
|
8892
|
+
this.#boundHandlePointerDown,
|
|
8893
|
+
);
|
|
8894
|
+
this.cursor?.removeEventListener("input", this.#boundHandleInput);
|
|
8895
|
+
this.cursor?.removeEventListener("change", this.#boundHandleChange);
|
|
8896
|
+
this.plane?.classList.remove("dragging");
|
|
8897
|
+
this.isDragging = false;
|
|
8346
8898
|
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
8347
8899
|
this.xInput.removeEventListener("input", this.#boundXInput);
|
|
8348
8900
|
this.xInput.removeEventListener("change", this.#boundXInput);
|
|
@@ -8376,49 +8928,41 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8376
8928
|
this.#emitChangeEvent();
|
|
8377
8929
|
}
|
|
8378
8930
|
|
|
8379
|
-
#
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
#snapToDiagonal(x, y) {
|
|
8388
|
-
if (!this.isShiftHeld) return { x, y };
|
|
8389
|
-
const diff = Math.abs(x - y);
|
|
8390
|
-
if (diff < 0.1) return { x: (x + y) / 2, y: (x + y) / 2 };
|
|
8391
|
-
if (Math.abs(1 - x - y) < 0.1) return { x, y: 1 - x };
|
|
8392
|
-
return { x, y };
|
|
8931
|
+
#applyScreenPosition(screenX, screenY, { syncHandle = true } = {}) {
|
|
8932
|
+
const x = Math.max(0, Math.min(1, screenX));
|
|
8933
|
+
const yScreen = Math.max(0, Math.min(1, screenY));
|
|
8934
|
+
const y = this.coordinates === "math" ? 1 - yScreen : yScreen;
|
|
8935
|
+
this.position = { x, y };
|
|
8936
|
+
if (syncHandle) this.#syncHandlePosition();
|
|
8937
|
+
this.#syncValueAttribute();
|
|
8393
8938
|
}
|
|
8394
8939
|
|
|
8395
|
-
#
|
|
8940
|
+
#handlePlanePointerDown(e) {
|
|
8941
|
+
if (!this.plane || !this.cursor) return;
|
|
8942
|
+
if (e.target?.closest?.(".fig-joystick-reset, fig-tooltip, fig-handle"))
|
|
8943
|
+
return;
|
|
8396
8944
|
const rect = this.plane.getBoundingClientRect();
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
0
|
|
8400
|
-
|
|
8401
|
-
);
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
x = this.#snapToGuide(x);
|
|
8407
|
-
y = this.#snapToGuide(y);
|
|
8408
|
-
|
|
8409
|
-
const snapped = this.#snapToDiagonal(x, y);
|
|
8410
|
-
this.position = snapped;
|
|
8945
|
+
const screenX = rect.width > 0 ? (e.clientX - rect.left) / rect.width : 0.5;
|
|
8946
|
+
const screenY =
|
|
8947
|
+
rect.height > 0 ? (e.clientY - rect.top) / rect.height : 0.5;
|
|
8948
|
+
this.cursor.value = `${Math.round(screenX * 100)}% ${Math.round(screenY * 100)}%`;
|
|
8949
|
+
this.#applyScreenPosition(screenX, screenY, { syncHandle: false });
|
|
8950
|
+
this.#emitInputEvent();
|
|
8951
|
+
this.#emitChangeEvent();
|
|
8952
|
+
}
|
|
8411
8953
|
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
}
|
|
8954
|
+
#handleHandleInput(e) {
|
|
8955
|
+
const detail = e.detail ?? {};
|
|
8956
|
+
if (typeof detail.px !== "number" || typeof detail.py !== "number") return;
|
|
8957
|
+
this.#applyScreenPosition(detail.px, detail.py, { syncHandle: false });
|
|
8958
|
+
this.#emitInputEvent();
|
|
8959
|
+
}
|
|
8419
8960
|
|
|
8961
|
+
#handleHandleChange() {
|
|
8962
|
+
this.isDragging = false;
|
|
8963
|
+
this.plane?.classList.remove("dragging");
|
|
8420
8964
|
this.#syncValueAttribute();
|
|
8421
|
-
this.#
|
|
8965
|
+
this.#emitChangeEvent();
|
|
8422
8966
|
}
|
|
8423
8967
|
|
|
8424
8968
|
#emitInputEvent() {
|
|
@@ -8442,8 +8986,7 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8442
8986
|
#syncHandlePosition() {
|
|
8443
8987
|
const displayY = this.#displayY(this.position.y);
|
|
8444
8988
|
if (this.cursor) {
|
|
8445
|
-
this.cursor.
|
|
8446
|
-
this.cursor.style.top = `${displayY * 100}%`;
|
|
8989
|
+
this.cursor.value = `${this.position.x * 100}% ${displayY * 100}%`;
|
|
8447
8990
|
}
|
|
8448
8991
|
// Also sync text inputs if they exist (convert to percentage 0-100)
|
|
8449
8992
|
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
@@ -8479,64 +9022,6 @@ class FigInputJoystick extends HTMLElement {
|
|
|
8479
9022
|
this.#emitChangeEvent();
|
|
8480
9023
|
}
|
|
8481
9024
|
|
|
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
9025
|
focus() {
|
|
8541
9026
|
const container = this.querySelector(".fig-input-joystick-plane-container");
|
|
8542
9027
|
container?.focus();
|
|
@@ -9186,6 +9671,10 @@ class FigShimmer extends HTMLElement {
|
|
|
9186
9671
|
}
|
|
9187
9672
|
customElements.define("fig-shimmer", FigShimmer);
|
|
9188
9673
|
|
|
9674
|
+
// FigSkeleton
|
|
9675
|
+
class FigSkeleton extends FigShimmer {}
|
|
9676
|
+
customElements.define("fig-skeleton", FigSkeleton);
|
|
9677
|
+
|
|
9189
9678
|
// FigLayer
|
|
9190
9679
|
class FigLayer extends HTMLElement {
|
|
9191
9680
|
static get observedAttributes() {
|
|
@@ -9319,12 +9808,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
9319
9808
|
|
|
9320
9809
|
// Fill state
|
|
9321
9810
|
#fillType = "solid";
|
|
9811
|
+
#gamut = "srgb"; // "srgb" or "display-p3"
|
|
9322
9812
|
#color = { h: 0, s: 0, v: 85, a: 1 }; // Default gray #D9D9D9
|
|
9813
|
+
#colorInputMode = "hex";
|
|
9323
9814
|
#gradient = {
|
|
9324
9815
|
type: "linear",
|
|
9325
9816
|
angle: 0,
|
|
9326
9817
|
centerX: 50,
|
|
9327
9818
|
centerY: 50,
|
|
9819
|
+
interpolationSpace: "oklab",
|
|
9820
|
+
hueInterpolation: "shorter",
|
|
9328
9821
|
stops: [
|
|
9329
9822
|
{ position: 0, color: "#D9D9D9", opacity: 100 },
|
|
9330
9823
|
{ position: 100, color: "#737373", opacity: 100 },
|
|
@@ -9344,6 +9837,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
9344
9837
|
#hueSlider = null;
|
|
9345
9838
|
#opacitySlider = null;
|
|
9346
9839
|
#isDraggingColor = false;
|
|
9840
|
+
#teardownColorAreaEvents = null;
|
|
9347
9841
|
|
|
9348
9842
|
constructor() {
|
|
9349
9843
|
super();
|
|
@@ -9365,6 +9859,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
9365
9859
|
}
|
|
9366
9860
|
|
|
9367
9861
|
disconnectedCallback() {
|
|
9862
|
+
if (this.#teardownColorAreaEvents) {
|
|
9863
|
+
this.#teardownColorAreaEvents();
|
|
9864
|
+
this.#teardownColorAreaEvents = null;
|
|
9865
|
+
}
|
|
9368
9866
|
if (this.#dialog) {
|
|
9369
9867
|
this.#dialog.close();
|
|
9370
9868
|
this.#dialog.remove();
|
|
@@ -9434,8 +9932,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
9434
9932
|
if (parsed.opacity !== undefined) {
|
|
9435
9933
|
this.#color.a = parsed.opacity / 100;
|
|
9436
9934
|
}
|
|
9437
|
-
if (parsed.
|
|
9438
|
-
this.#
|
|
9935
|
+
if (parsed.colorSpace === "display-p3" || parsed.colorSpace === "srgb") {
|
|
9936
|
+
this.#gamut = parsed.colorSpace;
|
|
9937
|
+
}
|
|
9938
|
+
if (parsed.gradient) {
|
|
9939
|
+
this.#gradient = normalizeGradientConfig({
|
|
9940
|
+
...this.#gradient,
|
|
9941
|
+
...parsed.gradient,
|
|
9942
|
+
});
|
|
9943
|
+
}
|
|
9439
9944
|
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
9440
9945
|
if (parsed.video) this.#video = { ...this.#video, ...parsed.video };
|
|
9441
9946
|
|
|
@@ -9531,6 +10036,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
9531
10036
|
}
|
|
9532
10037
|
|
|
9533
10038
|
this.#switchTab(this.#fillType);
|
|
10039
|
+
|
|
10040
|
+
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
10041
|
+
if (gamutEl) gamutEl.value = this.#gamut;
|
|
10042
|
+
|
|
9534
10043
|
this.#dialog.open = true;
|
|
9535
10044
|
|
|
9536
10045
|
requestAnimationFrame(() => {
|
|
@@ -9618,16 +10127,22 @@ class FigFillPicker extends HTMLElement {
|
|
|
9618
10127
|
.map((m) => `<div class="fig-fill-picker-tab" data-tab="${m}"></div>`)
|
|
9619
10128
|
.join("\n ");
|
|
9620
10129
|
|
|
10130
|
+
const gamutDropdown = `<fig-dropdown class="fig-fill-picker-gamut" ${expAttr} value="${this.#gamut}">
|
|
10131
|
+
<option value="srgb">sRGB</option>
|
|
10132
|
+
<option value="display-p3">Display P3</option>
|
|
10133
|
+
</fig-dropdown>`;
|
|
10134
|
+
|
|
9621
10135
|
this.#dialog.innerHTML = `
|
|
9622
10136
|
<fig-header>
|
|
9623
10137
|
${headerContent}
|
|
10138
|
+
${gamutDropdown}
|
|
9624
10139
|
<fig-button icon variant="ghost" class="fig-fill-picker-close">
|
|
9625
10140
|
<span class="fig-mask-icon" style="--icon: var(--icon-close)"></span>
|
|
9626
10141
|
</fig-button>
|
|
9627
10142
|
</fig-header>
|
|
9628
|
-
<
|
|
10143
|
+
<fig-content>
|
|
9629
10144
|
${tabDivs}
|
|
9630
|
-
</
|
|
10145
|
+
</fig-content>
|
|
9631
10146
|
`;
|
|
9632
10147
|
|
|
9633
10148
|
document.body.appendChild(this.#dialog);
|
|
@@ -9659,6 +10174,20 @@ class FigFillPicker extends HTMLElement {
|
|
|
9659
10174
|
});
|
|
9660
10175
|
}
|
|
9661
10176
|
|
|
10177
|
+
// Setup gamut dropdown
|
|
10178
|
+
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
10179
|
+
if (gamutEl) {
|
|
10180
|
+
const handleGamutChange = (e) => {
|
|
10181
|
+
const val = e.currentTarget?.value ?? e.target?.value ?? e.detail;
|
|
10182
|
+
if (val && val !== this.#gamut) {
|
|
10183
|
+
this.#gamut = val;
|
|
10184
|
+
this.#onGamutChange();
|
|
10185
|
+
}
|
|
10186
|
+
};
|
|
10187
|
+
gamutEl.addEventListener("input", handleGamutChange);
|
|
10188
|
+
gamutEl.addEventListener("change", handleGamutChange);
|
|
10189
|
+
}
|
|
10190
|
+
|
|
9662
10191
|
this.#dialog
|
|
9663
10192
|
.querySelector(".fig-fill-picker-close")
|
|
9664
10193
|
.addEventListener("click", () => {
|
|
@@ -9728,7 +10257,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
9728
10257
|
});
|
|
9729
10258
|
|
|
9730
10259
|
// Zero out content padding for custom mode tabs
|
|
9731
|
-
const contentEl = this.#dialog.querySelector("
|
|
10260
|
+
const contentEl = this.#dialog.querySelector("fig-content");
|
|
9732
10261
|
if (contentEl) {
|
|
9733
10262
|
contentEl.style.padding = this.#customSlots[tabName] ? "0" : "";
|
|
9734
10263
|
}
|
|
@@ -9749,13 +10278,27 @@ class FigFillPicker extends HTMLElement {
|
|
|
9749
10278
|
#initSolidTab() {
|
|
9750
10279
|
const container = this.#dialog.querySelector('[data-tab="solid"]');
|
|
9751
10280
|
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
10281
|
+
const experimental = this.getAttribute("experimental");
|
|
10282
|
+
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
10283
|
+
const savedMode = localStorage.getItem("figui-color-input-mode");
|
|
10284
|
+
if (
|
|
10285
|
+
savedMode &&
|
|
10286
|
+
["hex", "rgb", "hsl", "hsb", "lab", "lch"].includes(savedMode)
|
|
10287
|
+
) {
|
|
10288
|
+
this.#colorInputMode = savedMode;
|
|
10289
|
+
}
|
|
9752
10290
|
|
|
9753
10291
|
container.innerHTML = `
|
|
9754
|
-
<
|
|
10292
|
+
<fig-preview class="fig-fill-picker-color-area">
|
|
9755
10293
|
<canvas width="200" height="200"></canvas>
|
|
9756
|
-
<
|
|
9757
|
-
|
|
10294
|
+
<fig-handle
|
|
10295
|
+
drag
|
|
10296
|
+
drag-surface=".fig-fill-picker-color-area"
|
|
10297
|
+
drag-axes="x,y"
|
|
10298
|
+
></fig-handle>
|
|
10299
|
+
</fig-preview>
|
|
9758
10300
|
<div class="fig-fill-picker-sliders">
|
|
10301
|
+
<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>
|
|
9759
10302
|
<fig-slider type="hue" variant="neue" min="0" max="360" value="${
|
|
9760
10303
|
this.#color.h
|
|
9761
10304
|
}"></fig-slider>
|
|
@@ -9767,17 +10310,22 @@ class FigFillPicker extends HTMLElement {
|
|
|
9767
10310
|
: ""
|
|
9768
10311
|
}
|
|
9769
10312
|
</div>
|
|
9770
|
-
<
|
|
9771
|
-
<fig-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
10313
|
+
<fig-field class="fig-fill-picker-inputs" direction="horizontal">
|
|
10314
|
+
<fig-dropdown class="fig-fill-picker-input-mode" ${expAttr} value="${this.#colorInputMode}">
|
|
10315
|
+
<option value="hex">Hex</option>
|
|
10316
|
+
<option value="rgb">RGB</option>
|
|
10317
|
+
<option value="hsl">HSL</option>
|
|
10318
|
+
<option value="hsb">HSB</option>
|
|
10319
|
+
<option value="lab">LAB</option>
|
|
10320
|
+
<option value="lch">LCH</option>
|
|
10321
|
+
</fig-dropdown>
|
|
10322
|
+
<span class="fig-fill-picker-input-fields"></span>
|
|
10323
|
+
</fig-field>
|
|
9776
10324
|
`;
|
|
9777
10325
|
|
|
9778
10326
|
// Setup color area
|
|
9779
10327
|
this.#colorArea = container.querySelector("canvas");
|
|
9780
|
-
this.#colorAreaHandle = container.querySelector("
|
|
10328
|
+
this.#colorAreaHandle = container.querySelector("fig-handle");
|
|
9781
10329
|
this.#drawColorArea();
|
|
9782
10330
|
this.#updateHandlePosition();
|
|
9783
10331
|
this.#setupColorAreaEvents();
|
|
@@ -9810,25 +10358,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
9810
10358
|
});
|
|
9811
10359
|
}
|
|
9812
10360
|
|
|
9813
|
-
// Setup color input
|
|
9814
|
-
const
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
const hex = e.target.value;
|
|
9820
|
-
this.#color = { ...this.#hexToHSV(hex), a: this.#color.a };
|
|
9821
|
-
this.#drawColorArea();
|
|
9822
|
-
this.#updateHandlePosition();
|
|
9823
|
-
if (this.#hueSlider) {
|
|
9824
|
-
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
9825
|
-
}
|
|
9826
|
-
this.#emitInput();
|
|
9827
|
-
});
|
|
9828
|
-
colorInput.addEventListener("change", () => {
|
|
9829
|
-
this.#emitChange();
|
|
10361
|
+
// Setup color input mode dropdown
|
|
10362
|
+
const modeDropdown = container.querySelector(".fig-fill-picker-input-mode");
|
|
10363
|
+
modeDropdown.addEventListener("input", (e) => {
|
|
10364
|
+
this.#colorInputMode = e.target.value;
|
|
10365
|
+
localStorage.setItem("figui-color-input-mode", this.#colorInputMode);
|
|
10366
|
+
this.#rebuildColorInputFields();
|
|
9830
10367
|
});
|
|
9831
10368
|
|
|
10369
|
+
// Build initial color input fields
|
|
10370
|
+
this.#rebuildColorInputFields();
|
|
10371
|
+
|
|
9832
10372
|
// Setup eyedropper
|
|
9833
10373
|
const eyedropper = container.querySelector(".fig-fill-picker-eyedropper");
|
|
9834
10374
|
if ("EyeDropper" in window) {
|
|
@@ -9851,6 +10391,27 @@ class FigFillPicker extends HTMLElement {
|
|
|
9851
10391
|
}
|
|
9852
10392
|
}
|
|
9853
10393
|
|
|
10394
|
+
#onGamutChange() {
|
|
10395
|
+
// Recreate the solid canvas with the new color space
|
|
10396
|
+
const solidContainer = this.#dialog?.querySelector('[data-tab="solid"]');
|
|
10397
|
+
if (solidContainer) {
|
|
10398
|
+
const oldCanvas = solidContainer.querySelector("canvas");
|
|
10399
|
+
if (oldCanvas) {
|
|
10400
|
+
const newCanvas = document.createElement("canvas");
|
|
10401
|
+
newCanvas.width = oldCanvas.width;
|
|
10402
|
+
newCanvas.height = oldCanvas.height;
|
|
10403
|
+
oldCanvas.replaceWith(newCanvas);
|
|
10404
|
+
this.#colorArea = newCanvas;
|
|
10405
|
+
this.#setupColorAreaEvents();
|
|
10406
|
+
}
|
|
10407
|
+
this.#drawColorArea();
|
|
10408
|
+
this.#updateHandlePosition();
|
|
10409
|
+
}
|
|
10410
|
+
// Refresh gradient preview if gradient tab exists
|
|
10411
|
+
this.#updateGradientPreview();
|
|
10412
|
+
this.#emitInput();
|
|
10413
|
+
}
|
|
10414
|
+
|
|
9854
10415
|
#drawColorArea() {
|
|
9855
10416
|
// Refresh canvas reference in case DOM changed
|
|
9856
10417
|
if (!this.#colorArea && this.#dialog) {
|
|
@@ -9858,27 +10419,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
9858
10419
|
}
|
|
9859
10420
|
if (!this.#colorArea) return;
|
|
9860
10421
|
|
|
9861
|
-
const
|
|
10422
|
+
const colorSpace = this.#gamut === "display-p3" ? "display-p3" : "srgb";
|
|
10423
|
+
const ctx = this.#colorArea.getContext("2d", { colorSpace });
|
|
9862
10424
|
if (!ctx) return;
|
|
9863
10425
|
|
|
9864
10426
|
const width = this.#colorArea.width;
|
|
9865
10427
|
const height = this.#colorArea.height;
|
|
9866
10428
|
|
|
9867
|
-
// Clear canvas first
|
|
9868
10429
|
ctx.clearRect(0, 0, width, height);
|
|
9869
10430
|
|
|
9870
|
-
// Draw saturation-value gradient
|
|
9871
10431
|
const hue = this.#color.h;
|
|
10432
|
+
const isP3 = this.#gamut === "display-p3";
|
|
9872
10433
|
|
|
9873
|
-
// Create horizontal gradient (white to hue color)
|
|
9874
10434
|
const gradH = ctx.createLinearGradient(0, 0, width, 0);
|
|
9875
|
-
|
|
9876
|
-
|
|
10435
|
+
if (isP3) {
|
|
10436
|
+
gradH.addColorStop(0, "color(display-p3 1 1 1)");
|
|
10437
|
+
const [r, g, b] = hslToP3(hue, 100, 50);
|
|
10438
|
+
gradH.addColorStop(1, `color(display-p3 ${r} ${g} ${b})`);
|
|
10439
|
+
} else {
|
|
10440
|
+
gradH.addColorStop(0, "#FFFFFF");
|
|
10441
|
+
gradH.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
|
|
10442
|
+
}
|
|
9877
10443
|
|
|
9878
10444
|
ctx.fillStyle = gradH;
|
|
9879
10445
|
ctx.fillRect(0, 0, width, height);
|
|
9880
10446
|
|
|
9881
|
-
// Create vertical gradient (transparent to black)
|
|
9882
10447
|
const gradV = ctx.createLinearGradient(0, 0, 0, height);
|
|
9883
10448
|
gradV.addColorStop(0, "rgba(0,0,0,0)");
|
|
9884
10449
|
gradV.addColorStop(1, "rgba(0,0,0,1)");
|
|
@@ -9898,90 +10463,319 @@ class FigFillPicker extends HTMLElement {
|
|
|
9898
10463
|
return;
|
|
9899
10464
|
}
|
|
9900
10465
|
|
|
9901
|
-
const
|
|
9902
|
-
const
|
|
10466
|
+
const xPct = Math.max(0, Math.min(100, this.#color.s));
|
|
10467
|
+
const yPct = Math.max(0, Math.min(100, 100 - this.#color.v));
|
|
9903
10468
|
|
|
9904
|
-
this.#colorAreaHandle.
|
|
9905
|
-
this.#colorAreaHandle.
|
|
9906
|
-
|
|
9907
|
-
"--picker-color",
|
|
10469
|
+
this.#colorAreaHandle.setAttribute("value", `${xPct}% ${yPct}%`);
|
|
10470
|
+
this.#colorAreaHandle.setAttribute(
|
|
10471
|
+
"color",
|
|
9908
10472
|
this.#hsvToHex({ ...this.#color, a: 1 }),
|
|
9909
10473
|
);
|
|
9910
10474
|
}
|
|
9911
10475
|
|
|
10476
|
+
#updateColorFromAreaPosition(x, y, opts = {}) {
|
|
10477
|
+
const { updateHandle = true, emitInput = true, emitChange = false } = opts;
|
|
10478
|
+
this.#color.s = Math.max(0, Math.min(100, x * 100));
|
|
10479
|
+
this.#color.v = Math.max(0, Math.min(100, (1 - y) * 100));
|
|
10480
|
+
if (this.#colorAreaHandle) {
|
|
10481
|
+
this.#colorAreaHandle.setAttribute(
|
|
10482
|
+
"color",
|
|
10483
|
+
this.#hsvToHex({ ...this.#color, a: 1 }),
|
|
10484
|
+
);
|
|
10485
|
+
}
|
|
10486
|
+
if (updateHandle) this.#updateHandlePosition();
|
|
10487
|
+
this.#updateColorInputs();
|
|
10488
|
+
if (emitInput) this.#emitInput();
|
|
10489
|
+
if (emitChange) this.#emitChange();
|
|
10490
|
+
}
|
|
10491
|
+
|
|
9912
10492
|
#setupColorAreaEvents() {
|
|
10493
|
+
if (this.#teardownColorAreaEvents) {
|
|
10494
|
+
this.#teardownColorAreaEvents();
|
|
10495
|
+
this.#teardownColorAreaEvents = null;
|
|
10496
|
+
}
|
|
9913
10497
|
if (!this.#colorArea || !this.#colorAreaHandle) return;
|
|
9914
10498
|
|
|
9915
|
-
const
|
|
9916
|
-
|
|
9917
|
-
this.#isDraggingColor = false;
|
|
9918
|
-
this.#emitChange();
|
|
9919
|
-
};
|
|
10499
|
+
const colorAreaEl = this.#colorArea.parentElement || this.#colorArea;
|
|
10500
|
+
const colorAreaHandleEl = this.#colorAreaHandle;
|
|
9920
10501
|
|
|
9921
|
-
|
|
9922
|
-
|
|
10502
|
+
let isPlaneDragging = false;
|
|
10503
|
+
|
|
10504
|
+
const updatePlaneFromEvent = (e, opts = {}) => {
|
|
10505
|
+
const rect = colorAreaEl.getBoundingClientRect();
|
|
10506
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
9923
10507
|
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
|
9924
10508
|
const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
|
9925
|
-
|
|
9926
|
-
this.#color.s = (x / rect.width) * 100;
|
|
9927
|
-
this.#color.v = 100 - (y / rect.height) * 100;
|
|
9928
|
-
|
|
9929
|
-
this.#updateHandlePosition();
|
|
9930
|
-
this.#updateColorInputs();
|
|
9931
|
-
this.#emitInput();
|
|
10509
|
+
this.#updateColorFromAreaPosition(x / rect.width, y / rect.height, opts);
|
|
9932
10510
|
};
|
|
9933
10511
|
|
|
9934
|
-
|
|
9935
|
-
|
|
10512
|
+
const onPlanePointerDown = (e) => {
|
|
10513
|
+
if (e.button !== 0) return;
|
|
10514
|
+
if (
|
|
10515
|
+
e.target === colorAreaHandleEl ||
|
|
10516
|
+
colorAreaHandleEl.contains(e.target)
|
|
10517
|
+
)
|
|
10518
|
+
return;
|
|
10519
|
+
isPlaneDragging = true;
|
|
9936
10520
|
this.#isDraggingColor = true;
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
}
|
|
10521
|
+
colorAreaEl.setPointerCapture(e.pointerId);
|
|
10522
|
+
updatePlaneFromEvent(e, { updateHandle: true, emitInput: true });
|
|
10523
|
+
};
|
|
9940
10524
|
|
|
9941
|
-
|
|
9942
|
-
if (!
|
|
10525
|
+
const onPlanePointerMove = (e) => {
|
|
10526
|
+
if (!isPlaneDragging) return;
|
|
9943
10527
|
if (e.buttons === 0) {
|
|
9944
|
-
|
|
10528
|
+
onPlaneDragEnd();
|
|
9945
10529
|
return;
|
|
9946
10530
|
}
|
|
9947
|
-
|
|
9948
|
-
}
|
|
10531
|
+
updatePlaneFromEvent(e, { updateHandle: true, emitInput: true });
|
|
10532
|
+
};
|
|
9949
10533
|
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
10534
|
+
const onPlaneDragEnd = () => {
|
|
10535
|
+
if (!isPlaneDragging) return;
|
|
10536
|
+
isPlaneDragging = false;
|
|
10537
|
+
this.#isDraggingColor = false;
|
|
10538
|
+
this.#emitChange();
|
|
10539
|
+
};
|
|
9953
10540
|
|
|
9954
|
-
|
|
9955
|
-
this.#colorAreaHandle.addEventListener("pointerdown", (e) => {
|
|
9956
|
-
e.stopPropagation(); // Prevent canvas from also capturing
|
|
10541
|
+
const onHandleInput = (e) => {
|
|
9957
10542
|
this.#isDraggingColor = true;
|
|
9958
|
-
|
|
9959
|
-
|
|
10543
|
+
const px = e.detail?.px;
|
|
10544
|
+
const py = e.detail?.py;
|
|
10545
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) return;
|
|
10546
|
+
colorAreaHandleEl.setAttribute("value", `${px * 100}% ${py * 100}%`);
|
|
10547
|
+
this.#updateColorFromAreaPosition(px, py, {
|
|
10548
|
+
updateHandle: false,
|
|
10549
|
+
emitInput: true,
|
|
10550
|
+
});
|
|
10551
|
+
};
|
|
9960
10552
|
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
10553
|
+
const onHandleChange = (e) => {
|
|
10554
|
+
const px = e.detail?.px;
|
|
10555
|
+
const py = e.detail?.py;
|
|
10556
|
+
if (Number.isFinite(px) && Number.isFinite(py)) {
|
|
10557
|
+
colorAreaHandleEl.setAttribute("value", `${px * 100}% ${py * 100}%`);
|
|
10558
|
+
this.#updateColorFromAreaPosition(px, py, {
|
|
10559
|
+
updateHandle: false,
|
|
10560
|
+
emitInput: false,
|
|
10561
|
+
});
|
|
10562
|
+
}
|
|
10563
|
+
this.#isDraggingColor = false;
|
|
10564
|
+
this.#emitChange();
|
|
10565
|
+
};
|
|
10566
|
+
|
|
10567
|
+
colorAreaEl.addEventListener("pointerdown", onPlanePointerDown);
|
|
10568
|
+
colorAreaEl.addEventListener("pointermove", onPlanePointerMove);
|
|
10569
|
+
colorAreaEl.addEventListener("pointerup", onPlaneDragEnd);
|
|
10570
|
+
colorAreaEl.addEventListener("pointercancel", onPlaneDragEnd);
|
|
10571
|
+
colorAreaEl.addEventListener("lostpointercapture", onPlaneDragEnd);
|
|
10572
|
+
|
|
10573
|
+
colorAreaHandleEl.addEventListener("input", onHandleInput);
|
|
10574
|
+
colorAreaHandleEl.addEventListener("change", onHandleChange);
|
|
10575
|
+
|
|
10576
|
+
this.#teardownColorAreaEvents = () => {
|
|
10577
|
+
colorAreaEl.removeEventListener("pointerdown", onPlanePointerDown);
|
|
10578
|
+
colorAreaEl.removeEventListener("pointermove", onPlanePointerMove);
|
|
10579
|
+
colorAreaEl.removeEventListener("pointerup", onPlaneDragEnd);
|
|
10580
|
+
colorAreaEl.removeEventListener("pointercancel", onPlaneDragEnd);
|
|
10581
|
+
colorAreaEl.removeEventListener("lostpointercapture", onPlaneDragEnd);
|
|
10582
|
+
|
|
10583
|
+
colorAreaHandleEl.removeEventListener("input", onHandleInput);
|
|
10584
|
+
colorAreaHandleEl.removeEventListener("change", onHandleChange);
|
|
10585
|
+
this.#isDraggingColor = false;
|
|
10586
|
+
};
|
|
10587
|
+
}
|
|
10588
|
+
|
|
10589
|
+
#rebuildColorInputFields() {
|
|
10590
|
+
const container = this.#dialog?.querySelector(
|
|
10591
|
+
".fig-fill-picker-input-fields",
|
|
10592
|
+
);
|
|
10593
|
+
if (!container) return;
|
|
10594
|
+
|
|
10595
|
+
const wrap = (tooltip, html) =>
|
|
10596
|
+
`<fig-tooltip text="${tooltip}">${html}</fig-tooltip>`;
|
|
10597
|
+
|
|
10598
|
+
const num = (cls, min, max, step) =>
|
|
10599
|
+
`<fig-input-number class="${cls}" min="${min}" max="${max}"${step != null ? ` step="${step}"` : ""}></fig-input-number>`;
|
|
10600
|
+
|
|
10601
|
+
let html;
|
|
10602
|
+
switch (this.#colorInputMode) {
|
|
10603
|
+
case "rgb":
|
|
10604
|
+
html = `<div class="input-combo">
|
|
10605
|
+
${wrap("Red", num("fig-fill-picker-ci-r", 0, 255))}
|
|
10606
|
+
${wrap("Green", num("fig-fill-picker-ci-g", 0, 255))}
|
|
10607
|
+
${wrap("Blue", num("fig-fill-picker-ci-b", 0, 255))}
|
|
10608
|
+
</div>`;
|
|
10609
|
+
break;
|
|
10610
|
+
case "hsl":
|
|
10611
|
+
html = `<div class="input-combo">
|
|
10612
|
+
${wrap("Hue", num("fig-fill-picker-ci-h", 0, 360))}
|
|
10613
|
+
${wrap("Saturation", num("fig-fill-picker-ci-s", 0, 100))}
|
|
10614
|
+
${wrap("Lightness", num("fig-fill-picker-ci-l", 0, 100))}
|
|
10615
|
+
</div>`;
|
|
10616
|
+
break;
|
|
10617
|
+
case "hsb":
|
|
10618
|
+
html = `<div class="input-combo">
|
|
10619
|
+
${wrap("Hue", num("fig-fill-picker-ci-h", 0, 360))}
|
|
10620
|
+
${wrap("Saturation", num("fig-fill-picker-ci-s", 0, 100))}
|
|
10621
|
+
${wrap("Brightness", num("fig-fill-picker-ci-v", 0, 100))}
|
|
10622
|
+
</div>`;
|
|
10623
|
+
break;
|
|
10624
|
+
case "lab":
|
|
10625
|
+
html = `<div class="input-combo">
|
|
10626
|
+
${wrap("Lightness", num("fig-fill-picker-ci-okl", 0, 100))}
|
|
10627
|
+
${wrap("Green-Red axis", num("fig-fill-picker-ci-oka", -0.4, 0.4, 0.001))}
|
|
10628
|
+
${wrap("Blue-Yellow axis", num("fig-fill-picker-ci-okb", -0.4, 0.4, 0.001))}
|
|
10629
|
+
</div>`;
|
|
10630
|
+
break;
|
|
10631
|
+
case "lch":
|
|
10632
|
+
html = `<div class="input-combo">
|
|
10633
|
+
${wrap("Lightness", num("fig-fill-picker-ci-okl", 0, 100))}
|
|
10634
|
+
${wrap("Chroma", num("fig-fill-picker-ci-okc", 0, 0.4, 0.001))}
|
|
10635
|
+
${wrap("Hue", num("fig-fill-picker-ci-okh", 0, 360))}
|
|
10636
|
+
</div>`;
|
|
10637
|
+
break;
|
|
10638
|
+
default: // hex
|
|
10639
|
+
html = `<fig-input-text class="fig-fill-picker-ci-hex" placeholder="FFFFFF"></fig-input-text>`;
|
|
10640
|
+
break;
|
|
10641
|
+
}
|
|
10642
|
+
|
|
10643
|
+
container.innerHTML = html;
|
|
10644
|
+
this.#wireColorInputEvents();
|
|
10645
|
+
requestAnimationFrame(() => this.#updateColorInputs());
|
|
10646
|
+
}
|
|
10647
|
+
|
|
10648
|
+
#wireColorInputEvents() {
|
|
10649
|
+
const container = this.#dialog?.querySelector(
|
|
10650
|
+
".fig-fill-picker-input-fields",
|
|
10651
|
+
);
|
|
10652
|
+
if (!container) return;
|
|
10653
|
+
|
|
10654
|
+
const onInput = () => {
|
|
10655
|
+
if (this.#isDraggingColor) return;
|
|
10656
|
+
const color = this.#readColorFromInputs();
|
|
10657
|
+
if (!color) return;
|
|
10658
|
+
this.#color = { ...color, a: this.#color.a };
|
|
10659
|
+
this.#drawColorArea();
|
|
10660
|
+
this.#updateHandlePosition();
|
|
10661
|
+
if (this.#hueSlider) {
|
|
10662
|
+
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
9966
10663
|
}
|
|
9967
|
-
|
|
10664
|
+
this.#emitInput();
|
|
10665
|
+
};
|
|
10666
|
+
|
|
10667
|
+
const onChange = () => this.#emitChange();
|
|
10668
|
+
|
|
10669
|
+
const inputs = container.querySelectorAll(
|
|
10670
|
+
"fig-input-number, fig-input-text",
|
|
10671
|
+
);
|
|
10672
|
+
inputs.forEach((el) => {
|
|
10673
|
+
el.addEventListener("input", onInput);
|
|
10674
|
+
el.addEventListener("change", onChange);
|
|
9968
10675
|
});
|
|
10676
|
+
}
|
|
9969
10677
|
|
|
9970
|
-
|
|
9971
|
-
this.#
|
|
9972
|
-
|
|
10678
|
+
#readColorFromInputs() {
|
|
10679
|
+
const q = (cls) => this.#dialog?.querySelector(`.${cls}`);
|
|
10680
|
+
const val = (cls) => parseFloat(q(cls)?.value ?? 0);
|
|
10681
|
+
|
|
10682
|
+
switch (this.#colorInputMode) {
|
|
10683
|
+
case "rgb":
|
|
10684
|
+
return this.#rgbToHSV({
|
|
10685
|
+
r: val("fig-fill-picker-ci-r"),
|
|
10686
|
+
g: val("fig-fill-picker-ci-g"),
|
|
10687
|
+
b: val("fig-fill-picker-ci-b"),
|
|
10688
|
+
});
|
|
10689
|
+
case "hsl": {
|
|
10690
|
+
const rgb = this.#hslToRGB({
|
|
10691
|
+
h: val("fig-fill-picker-ci-h"),
|
|
10692
|
+
s: val("fig-fill-picker-ci-s"),
|
|
10693
|
+
l: val("fig-fill-picker-ci-l"),
|
|
10694
|
+
});
|
|
10695
|
+
return this.#rgbToHSV(rgb);
|
|
10696
|
+
}
|
|
10697
|
+
case "hsb":
|
|
10698
|
+
return {
|
|
10699
|
+
h: val("fig-fill-picker-ci-h"),
|
|
10700
|
+
s: val("fig-fill-picker-ci-s"),
|
|
10701
|
+
v: val("fig-fill-picker-ci-v"),
|
|
10702
|
+
a: 1,
|
|
10703
|
+
};
|
|
10704
|
+
case "lab": {
|
|
10705
|
+
const rgb = this.#oklabToRGB({
|
|
10706
|
+
l: val("fig-fill-picker-ci-okl") / 100,
|
|
10707
|
+
a: val("fig-fill-picker-ci-oka"),
|
|
10708
|
+
b: val("fig-fill-picker-ci-okb"),
|
|
10709
|
+
});
|
|
10710
|
+
return this.#rgbToHSV(rgb);
|
|
10711
|
+
}
|
|
10712
|
+
case "lch": {
|
|
10713
|
+
const rgb = this.#oklchToRGB({
|
|
10714
|
+
l: val("fig-fill-picker-ci-okl") / 100,
|
|
10715
|
+
c: val("fig-fill-picker-ci-okc"),
|
|
10716
|
+
h: val("fig-fill-picker-ci-okh"),
|
|
10717
|
+
});
|
|
10718
|
+
return this.#rgbToHSV(rgb);
|
|
10719
|
+
}
|
|
10720
|
+
default: {
|
|
10721
|
+
// hex
|
|
10722
|
+
const hexEl = q("fig-fill-picker-ci-hex");
|
|
10723
|
+
if (!hexEl) return null;
|
|
10724
|
+
let hex = hexEl.value.replace(/^#/, "");
|
|
10725
|
+
if (hex.length === 3)
|
|
10726
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
10727
|
+
if (hex.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
10728
|
+
return this.#hexToHSV(`#${hex}`);
|
|
10729
|
+
}
|
|
10730
|
+
}
|
|
9973
10731
|
}
|
|
9974
10732
|
|
|
9975
10733
|
#updateColorInputs() {
|
|
9976
10734
|
if (!this.#dialog) return;
|
|
9977
10735
|
|
|
9978
10736
|
const hex = this.#hsvToHex(this.#color);
|
|
10737
|
+
const rgb = this.#hsvToRGB(this.#color);
|
|
10738
|
+
const q = (cls) => this.#dialog.querySelector(`.${cls}`);
|
|
10739
|
+
const set = (cls, v) => {
|
|
10740
|
+
const el = q(cls);
|
|
10741
|
+
if (el) el.setAttribute("value", v);
|
|
10742
|
+
};
|
|
9979
10743
|
|
|
9980
|
-
|
|
9981
|
-
"
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
10744
|
+
switch (this.#colorInputMode) {
|
|
10745
|
+
case "rgb":
|
|
10746
|
+
set("fig-fill-picker-ci-r", rgb.r);
|
|
10747
|
+
set("fig-fill-picker-ci-g", rgb.g);
|
|
10748
|
+
set("fig-fill-picker-ci-b", rgb.b);
|
|
10749
|
+
break;
|
|
10750
|
+
case "hsl": {
|
|
10751
|
+
const hsl = this.#rgbToHSL(rgb);
|
|
10752
|
+
set("fig-fill-picker-ci-h", Math.round(hsl.h));
|
|
10753
|
+
set("fig-fill-picker-ci-s", Math.round(hsl.s));
|
|
10754
|
+
set("fig-fill-picker-ci-l", Math.round(hsl.l));
|
|
10755
|
+
break;
|
|
10756
|
+
}
|
|
10757
|
+
case "hsb":
|
|
10758
|
+
set("fig-fill-picker-ci-h", Math.round(this.#color.h));
|
|
10759
|
+
set("fig-fill-picker-ci-s", Math.round(this.#color.s));
|
|
10760
|
+
set("fig-fill-picker-ci-v", Math.round(this.#color.v));
|
|
10761
|
+
break;
|
|
10762
|
+
case "lab": {
|
|
10763
|
+
const lab = this.#rgbToOKLAB(rgb);
|
|
10764
|
+
set("fig-fill-picker-ci-okl", Math.round(lab.l * 100));
|
|
10765
|
+
set("fig-fill-picker-ci-oka", +lab.a.toFixed(3));
|
|
10766
|
+
set("fig-fill-picker-ci-okb", +lab.b.toFixed(3));
|
|
10767
|
+
break;
|
|
10768
|
+
}
|
|
10769
|
+
case "lch": {
|
|
10770
|
+
const lch = this.#rgbToOKLCH(rgb);
|
|
10771
|
+
set("fig-fill-picker-ci-okl", Math.round(lch.l * 100));
|
|
10772
|
+
set("fig-fill-picker-ci-okc", +lch.c.toFixed(3));
|
|
10773
|
+
set("fig-fill-picker-ci-okh", Math.round(lch.h));
|
|
10774
|
+
break;
|
|
10775
|
+
}
|
|
10776
|
+
default: // hex
|
|
10777
|
+
set("fig-fill-picker-ci-hex", hex.replace(/^#/, "").toUpperCase());
|
|
10778
|
+
break;
|
|
9985
10779
|
}
|
|
9986
10780
|
|
|
9987
10781
|
if (this.#opacitySlider) {
|
|
@@ -9998,7 +10792,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
9998
10792
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
9999
10793
|
|
|
10000
10794
|
container.innerHTML = `
|
|
10001
|
-
<
|
|
10795
|
+
<fig-field class="fig-fill-picker-gradient-header" direction="horizontal">
|
|
10002
10796
|
<fig-dropdown class="fig-fill-picker-gradient-type" ${expAttr} value="${
|
|
10003
10797
|
this.#gradient.type
|
|
10004
10798
|
}">
|
|
@@ -10024,18 +10818,39 @@ class FigFillPicker extends HTMLElement {
|
|
|
10024
10818
|
<span class="fig-mask-icon" style="--icon: var(--icon-swap)"></span>
|
|
10025
10819
|
</fig-button>
|
|
10026
10820
|
</fig-tooltip>
|
|
10027
|
-
</
|
|
10028
|
-
<
|
|
10821
|
+
</fig-field>
|
|
10822
|
+
<fig-preview class="fig-fill-picker-gradient-preview">
|
|
10029
10823
|
<div class="fig-fill-picker-gradient-bar"></div>
|
|
10030
10824
|
<div class="fig-fill-picker-gradient-stops-handles"></div>
|
|
10031
|
-
</
|
|
10825
|
+
</fig-preview>
|
|
10826
|
+
<fig-field class="fig-fill-picker-gradient-interpolation" direction="horizontal">
|
|
10827
|
+
<label>Mixing</label>
|
|
10828
|
+
<fig-dropdown class="fig-fill-picker-gradient-space" full ${expAttr} value="${
|
|
10829
|
+
this.#gradient.interpolationSpace === "oklch"
|
|
10830
|
+
? `oklch-${this.#gradient.hueInterpolation || "shorter"}`
|
|
10831
|
+
: this.#gradient.interpolationSpace
|
|
10832
|
+
}">
|
|
10833
|
+
<optgroup label="sRGB">
|
|
10834
|
+
<option value="srgb-linear">Linear</option>
|
|
10835
|
+
</optgroup>
|
|
10836
|
+
<optgroup label="OKLab">
|
|
10837
|
+
<option value="oklab">Perceptual</option>
|
|
10838
|
+
</optgroup>
|
|
10839
|
+
<optgroup label="OKLCH">
|
|
10840
|
+
<option value="oklch-shorter">Shorter hue</option>
|
|
10841
|
+
<option value="oklch-longer">Longer hue</option>
|
|
10842
|
+
<option value="oklch-increasing">Increasing hue</option>
|
|
10843
|
+
<option value="oklch-decreasing">Decreasing hue</option>
|
|
10844
|
+
</optgroup>
|
|
10845
|
+
</fig-dropdown>
|
|
10846
|
+
</fig-field>
|
|
10032
10847
|
<div class="fig-fill-picker-gradient-stops">
|
|
10033
|
-
<
|
|
10848
|
+
<fig-header class="fig-fill-picker-gradient-stops-header" borderless>
|
|
10034
10849
|
<span>Stops</span>
|
|
10035
10850
|
<fig-button icon variant="ghost" class="fig-fill-picker-gradient-add" title="Add stop">
|
|
10036
10851
|
<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>
|
|
10037
10852
|
</fig-button>
|
|
10038
|
-
</
|
|
10853
|
+
</fig-header>
|
|
10039
10854
|
<div class="fig-fill-picker-gradient-stops-list"></div>
|
|
10040
10855
|
</div>
|
|
10041
10856
|
`;
|
|
@@ -10049,11 +10864,41 @@ class FigFillPicker extends HTMLElement {
|
|
|
10049
10864
|
const typeDropdown = container.querySelector(
|
|
10050
10865
|
".fig-fill-picker-gradient-type",
|
|
10051
10866
|
);
|
|
10052
|
-
|
|
10053
|
-
|
|
10867
|
+
const getDropdownValue = (event) =>
|
|
10868
|
+
event.currentTarget?.value ?? event.target?.value ?? event.detail;
|
|
10869
|
+
|
|
10870
|
+
const handleTypeChange = (e) => {
|
|
10871
|
+
this.#gradient.type = getDropdownValue(e);
|
|
10054
10872
|
this.#updateGradientUI();
|
|
10055
10873
|
this.#emitInput();
|
|
10056
|
-
}
|
|
10874
|
+
};
|
|
10875
|
+
typeDropdown.addEventListener("input", handleTypeChange);
|
|
10876
|
+
typeDropdown.addEventListener("change", handleTypeChange);
|
|
10877
|
+
|
|
10878
|
+
const interpolationDropdown = container.querySelector(
|
|
10879
|
+
".fig-fill-picker-gradient-space",
|
|
10880
|
+
);
|
|
10881
|
+
const handleInterpolationChange = (e) => {
|
|
10882
|
+
const val = getDropdownValue(e);
|
|
10883
|
+
let space = val;
|
|
10884
|
+
let hue = "shorter";
|
|
10885
|
+
if (val.startsWith("oklch-")) {
|
|
10886
|
+
space = "oklch";
|
|
10887
|
+
hue = val.slice(6);
|
|
10888
|
+
}
|
|
10889
|
+
this.#gradient = normalizeGradientConfig({
|
|
10890
|
+
...this.#gradient,
|
|
10891
|
+
interpolationSpace: space,
|
|
10892
|
+
hueInterpolation: hue,
|
|
10893
|
+
});
|
|
10894
|
+
this.#updateGradientUI();
|
|
10895
|
+
this.#emitInput();
|
|
10896
|
+
};
|
|
10897
|
+
interpolationDropdown?.addEventListener("input", handleInterpolationChange);
|
|
10898
|
+
interpolationDropdown?.addEventListener(
|
|
10899
|
+
"change",
|
|
10900
|
+
handleInterpolationChange,
|
|
10901
|
+
);
|
|
10057
10902
|
|
|
10058
10903
|
// Angle input
|
|
10059
10904
|
// Convert from fig-input-angle coordinates (0° = right) to CSS coordinates (0° = up)
|
|
@@ -10114,6 +10959,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10114
10959
|
|
|
10115
10960
|
const container = this.#dialog.querySelector('[data-tab="gradient"]');
|
|
10116
10961
|
if (!container) return;
|
|
10962
|
+
this.#gradient = normalizeGradientConfig(this.#gradient);
|
|
10117
10963
|
|
|
10118
10964
|
// Show/hide angle vs center inputs
|
|
10119
10965
|
const angleInput = container.querySelector(
|
|
@@ -10134,6 +10980,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
10134
10980
|
angleInput.setAttribute("value", pickerAngle);
|
|
10135
10981
|
}
|
|
10136
10982
|
|
|
10983
|
+
const interpolationDropdown = container.querySelector(
|
|
10984
|
+
".fig-fill-picker-gradient-space",
|
|
10985
|
+
);
|
|
10986
|
+
if (interpolationDropdown) {
|
|
10987
|
+
interpolationDropdown.value =
|
|
10988
|
+
this.#gradient.interpolationSpace === "oklch"
|
|
10989
|
+
? `oklch-${this.#gradient.hueInterpolation || "shorter"}`
|
|
10990
|
+
: this.#gradient.interpolationSpace;
|
|
10991
|
+
}
|
|
10992
|
+
|
|
10137
10993
|
this.#updateGradientPreview();
|
|
10138
10994
|
this.#updateGradientStopsList();
|
|
10139
10995
|
}
|
|
@@ -10141,9 +10997,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
10141
10997
|
#updateGradientPreview() {
|
|
10142
10998
|
if (!this.#dialog) return;
|
|
10143
10999
|
|
|
11000
|
+
const preview = this.#dialog.querySelector(
|
|
11001
|
+
".fig-fill-picker-gradient-preview",
|
|
11002
|
+
);
|
|
10144
11003
|
const bar = this.#dialog.querySelector(".fig-fill-picker-gradient-bar");
|
|
10145
|
-
if (bar) {
|
|
10146
|
-
|
|
11004
|
+
if (preview || bar) {
|
|
11005
|
+
const css = this.#getGradientCSS();
|
|
11006
|
+
if (bar) bar.style.background = css;
|
|
11007
|
+
if (preview) preview.style.background = css;
|
|
10147
11008
|
}
|
|
10148
11009
|
|
|
10149
11010
|
this.#updateChit();
|
|
@@ -10160,7 +11021,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10160
11021
|
list.innerHTML = this.#gradient.stops
|
|
10161
11022
|
.map(
|
|
10162
11023
|
(stop, index) => `
|
|
10163
|
-
<
|
|
11024
|
+
<fig-field class="fig-fill-picker-gradient-stop-row" direction="horizontal" data-index="${index}">
|
|
10164
11025
|
<fig-input-number class="fig-fill-picker-stop-position" min="0" max="100" value="${
|
|
10165
11026
|
stop.position
|
|
10166
11027
|
}" units="%"></fig-input-number>
|
|
@@ -10172,7 +11033,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10172
11033
|
}>
|
|
10173
11034
|
<span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>
|
|
10174
11035
|
</fig-button>
|
|
10175
|
-
</
|
|
11036
|
+
</fig-field>
|
|
10176
11037
|
`,
|
|
10177
11038
|
)
|
|
10178
11039
|
.join("");
|
|
@@ -10226,30 +11087,52 @@ class FigFillPicker extends HTMLElement {
|
|
|
10226
11087
|
});
|
|
10227
11088
|
}
|
|
10228
11089
|
|
|
10229
|
-
#
|
|
10230
|
-
const
|
|
11090
|
+
#buildGradientCSS(interpolationSpaceOverride, includeInterpolation = true) {
|
|
11091
|
+
const gradient = normalizeGradientConfig({
|
|
11092
|
+
...this.#gradient,
|
|
11093
|
+
interpolationSpace:
|
|
11094
|
+
interpolationSpaceOverride ?? this.#gradient.interpolationSpace,
|
|
11095
|
+
});
|
|
11096
|
+
const isP3 = this.#gamut === "display-p3";
|
|
11097
|
+
const stops = gradient.stops
|
|
10231
11098
|
.map((s) => {
|
|
10232
|
-
const
|
|
10233
|
-
|
|
11099
|
+
const color = isP3
|
|
11100
|
+
? this.#hexToP3(s.color, s.opacity / 100)
|
|
11101
|
+
: this.#hexToRGBA(s.color, s.opacity / 100);
|
|
11102
|
+
return `${color} ${s.position}%`;
|
|
10234
11103
|
})
|
|
10235
11104
|
.join(", ");
|
|
10236
|
-
|
|
10237
|
-
|
|
11105
|
+
const interpolation = includeInterpolation
|
|
11106
|
+
? ` ${gradientInterpolationClause(gradient)}`
|
|
11107
|
+
: "";
|
|
11108
|
+
switch (gradient.type) {
|
|
10238
11109
|
case "linear":
|
|
10239
|
-
return `linear-gradient(${
|
|
11110
|
+
return `linear-gradient(${gradient.angle}deg${interpolation}, ${stops})`;
|
|
10240
11111
|
case "radial":
|
|
10241
|
-
return `radial-gradient(circle at ${
|
|
10242
|
-
this.#gradient.centerY
|
|
10243
|
-
}%, ${stops})`;
|
|
11112
|
+
return `radial-gradient(circle at ${gradient.centerX}% ${gradient.centerY}%${interpolation}, ${stops})`;
|
|
10244
11113
|
case "angular":
|
|
10245
|
-
|
|
10246
|
-
// because it's converted when reading from fig-input-angle
|
|
10247
|
-
return `conic-gradient(from ${this.#gradient.angle}deg, ${stops})`;
|
|
11114
|
+
return `conic-gradient(from ${gradient.angle}deg${interpolation}, ${stops})`;
|
|
10248
11115
|
default:
|
|
10249
|
-
return `linear-gradient(${
|
|
11116
|
+
return `linear-gradient(${gradient.angle}deg${interpolation}, ${stops})`;
|
|
10250
11117
|
}
|
|
10251
11118
|
}
|
|
10252
11119
|
|
|
11120
|
+
#testGradientSupport(css) {
|
|
11121
|
+
const el = document.createElement("div");
|
|
11122
|
+
el.style.background = css;
|
|
11123
|
+
return !!el.style.background;
|
|
11124
|
+
}
|
|
11125
|
+
|
|
11126
|
+
#getGradientCSS() {
|
|
11127
|
+
const preferred = this.#buildGradientCSS(undefined, true);
|
|
11128
|
+
if (this.#testGradientSupport(preferred)) return preferred;
|
|
11129
|
+
|
|
11130
|
+
const oklabFallback = this.#buildGradientCSS("oklab", true);
|
|
11131
|
+
if (this.#testGradientSupport(oklabFallback)) return oklabFallback;
|
|
11132
|
+
|
|
11133
|
+
return this.#buildGradientCSS("oklab", false);
|
|
11134
|
+
}
|
|
11135
|
+
|
|
10253
11136
|
// ============ IMAGE TAB ============
|
|
10254
11137
|
#initImageTab() {
|
|
10255
11138
|
const container = this.#dialog.querySelector('[data-tab="image"]');
|
|
@@ -10257,7 +11140,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10257
11140
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
10258
11141
|
|
|
10259
11142
|
container.innerHTML = `
|
|
10260
|
-
<
|
|
11143
|
+
<fig-field class="fig-fill-picker-media-header" direction="horizontal">
|
|
10261
11144
|
<fig-dropdown class="fig-fill-picker-scale-mode" ${expAttr} value="${
|
|
10262
11145
|
this.#image.scaleMode
|
|
10263
11146
|
}">
|
|
@@ -10269,7 +11152,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10269
11152
|
<fig-input-number class="fig-fill-picker-scale" min="1" max="200" value="${
|
|
10270
11153
|
this.#image.scale
|
|
10271
11154
|
}" units="%" style="display: none;"></fig-input-number>
|
|
10272
|
-
</
|
|
11155
|
+
</fig-field>
|
|
10273
11156
|
<div class="fig-fill-picker-media-preview">
|
|
10274
11157
|
<div class="fig-fill-picker-checkerboard"></div>
|
|
10275
11158
|
<div class="fig-fill-picker-image-preview"></div>
|
|
@@ -10411,7 +11294,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10411
11294
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
10412
11295
|
|
|
10413
11296
|
container.innerHTML = `
|
|
10414
|
-
<
|
|
11297
|
+
<fig-field class="fig-fill-picker-media-header" direction="horizontal">
|
|
10415
11298
|
<fig-dropdown class="fig-fill-picker-scale-mode" ${expAttr} value="${
|
|
10416
11299
|
this.#video.scaleMode
|
|
10417
11300
|
}">
|
|
@@ -10419,7 +11302,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10419
11302
|
<option value="fit">Fit</option>
|
|
10420
11303
|
<option value="crop">Crop</option>
|
|
10421
11304
|
</fig-dropdown>
|
|
10422
|
-
</
|
|
11305
|
+
</fig-field>
|
|
10423
11306
|
<div class="fig-fill-picker-media-preview">
|
|
10424
11307
|
<div class="fig-fill-picker-checkerboard"></div>
|
|
10425
11308
|
<video class="fig-fill-picker-video-preview" style="display: none;" muted loop></video>
|
|
@@ -10509,13 +11392,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
10509
11392
|
<span>Camera access required</span>
|
|
10510
11393
|
</div>
|
|
10511
11394
|
</div>
|
|
10512
|
-
<
|
|
11395
|
+
<fig-field class="fig-fill-picker-webcam-controls" direction="horizontal">
|
|
10513
11396
|
<fig-dropdown class="fig-fill-picker-camera-select" ${expAttr} style="display: none;">
|
|
10514
11397
|
</fig-dropdown>
|
|
10515
11398
|
<fig-button class="fig-fill-picker-webcam-capture" variant="primary">
|
|
10516
11399
|
Capture
|
|
10517
11400
|
</fig-button>
|
|
10518
|
-
</
|
|
11401
|
+
</fig-field>
|
|
10519
11402
|
`;
|
|
10520
11403
|
|
|
10521
11404
|
this.#setupWebcamEvents(container);
|
|
@@ -10739,6 +11622,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
10739
11622
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
10740
11623
|
}
|
|
10741
11624
|
|
|
11625
|
+
#hexToP3(hex, alpha = 1) {
|
|
11626
|
+
const r = +(parseInt(hex.slice(1, 3), 16) / 255).toFixed(4);
|
|
11627
|
+
const g = +(parseInt(hex.slice(3, 5), 16) / 255).toFixed(4);
|
|
11628
|
+
const b = +(parseInt(hex.slice(5, 7), 16) / 255).toFixed(4);
|
|
11629
|
+
return `color(display-p3 ${r} ${g} ${b} / ${alpha})`;
|
|
11630
|
+
}
|
|
11631
|
+
|
|
10742
11632
|
#rgbToHSL(rgb) {
|
|
10743
11633
|
const r = rgb.r / 255;
|
|
10744
11634
|
const g = rgb.g / 255;
|
|
@@ -10843,6 +11733,37 @@ class FigFillPicker extends HTMLElement {
|
|
|
10843
11733
|
};
|
|
10844
11734
|
}
|
|
10845
11735
|
|
|
11736
|
+
#oklabToRGB(lab) {
|
|
11737
|
+
const l_ = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
|
|
11738
|
+
const m_ = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
|
|
11739
|
+
const s_ = lab.l - 0.0894841775 * lab.a - 1.291485548 * lab.b;
|
|
11740
|
+
|
|
11741
|
+
const l = l_ * l_ * l_;
|
|
11742
|
+
const m = m_ * m_ * m_;
|
|
11743
|
+
const s = s_ * s_ * s_;
|
|
11744
|
+
|
|
11745
|
+
const toSRGB = (c) => {
|
|
11746
|
+
const v =
|
|
11747
|
+
c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
11748
|
+
return Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
11749
|
+
};
|
|
11750
|
+
|
|
11751
|
+
return {
|
|
11752
|
+
r: toSRGB(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
|
11753
|
+
g: toSRGB(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
|
11754
|
+
b: toSRGB(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
|
|
11755
|
+
};
|
|
11756
|
+
}
|
|
11757
|
+
|
|
11758
|
+
#oklchToRGB(lch) {
|
|
11759
|
+
const hRad = (lch.h * Math.PI) / 180;
|
|
11760
|
+
return this.#oklabToRGB({
|
|
11761
|
+
l: lch.l,
|
|
11762
|
+
a: lch.c * Math.cos(hRad),
|
|
11763
|
+
b: lch.c * Math.sin(hRad),
|
|
11764
|
+
});
|
|
11765
|
+
}
|
|
11766
|
+
|
|
10846
11767
|
// ============ EVENT EMITTERS ============
|
|
10847
11768
|
#emitInput() {
|
|
10848
11769
|
this.#updateChit();
|
|
@@ -10865,7 +11786,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10865
11786
|
|
|
10866
11787
|
// ============ PUBLIC API ============
|
|
10867
11788
|
get value() {
|
|
10868
|
-
const base = { type: this.#fillType };
|
|
11789
|
+
const base = { type: this.#fillType, colorSpace: this.#gamut };
|
|
10869
11790
|
|
|
10870
11791
|
switch (this.#fillType) {
|
|
10871
11792
|
case "solid":
|
|
@@ -10878,7 +11799,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
10878
11799
|
case "gradient":
|
|
10879
11800
|
return {
|
|
10880
11801
|
...base,
|
|
10881
|
-
gradient:
|
|
11802
|
+
gradient: gradientToValueShape(this.#gradient),
|
|
10882
11803
|
css: this.#getGradientCSS(),
|
|
10883
11804
|
};
|
|
10884
11805
|
case "image":
|
|
@@ -10945,11 +11866,266 @@ class FigFillPicker extends HTMLElement {
|
|
|
10945
11866
|
}
|
|
10946
11867
|
customElements.define("fig-fill-picker", FigFillPicker);
|
|
10947
11868
|
|
|
10948
|
-
/*
|
|
11869
|
+
/* Color Tip */
|
|
10949
11870
|
/**
|
|
10950
|
-
* A
|
|
10951
|
-
* @attr {string} value -
|
|
10952
|
-
* @attr {boolean} selected - Whether
|
|
11871
|
+
* A compact solid-color tip that wraps fig-fill-picker.
|
|
11872
|
+
* @attr {string} value - Solid color string (hex/rgb/hsl/named)
|
|
11873
|
+
* @attr {boolean} selected - Whether the tip is selected
|
|
11874
|
+
* @attr {boolean} disabled - Whether the tip is disabled
|
|
11875
|
+
* @fires input - While color changes
|
|
11876
|
+
* @fires change - When color is committed
|
|
11877
|
+
*/
|
|
11878
|
+
class FigColorTip extends HTMLElement {
|
|
11879
|
+
#fillPicker = null;
|
|
11880
|
+
#chit = null;
|
|
11881
|
+
#boundHandleInput = this.#handlePickerInput.bind(this);
|
|
11882
|
+
#boundHandleChange = this.#handlePickerChange.bind(this);
|
|
11883
|
+
|
|
11884
|
+
static get observedAttributes() {
|
|
11885
|
+
return ["value", "selected", "disabled", "alpha", "add"];
|
|
11886
|
+
}
|
|
11887
|
+
|
|
11888
|
+
get #isAddMode() {
|
|
11889
|
+
const v = this.getAttribute("add");
|
|
11890
|
+
return v !== null && v !== "false";
|
|
11891
|
+
}
|
|
11892
|
+
|
|
11893
|
+
connectedCallback() {
|
|
11894
|
+
this.#render();
|
|
11895
|
+
this.#syncFromAttributes();
|
|
11896
|
+
}
|
|
11897
|
+
|
|
11898
|
+
disconnectedCallback() {
|
|
11899
|
+
this.#teardownListeners();
|
|
11900
|
+
this.removeEventListener("click", this.#handleAddClick);
|
|
11901
|
+
}
|
|
11902
|
+
|
|
11903
|
+
#teardownListeners() {
|
|
11904
|
+
if (!this.#fillPicker) return;
|
|
11905
|
+
this.#fillPicker.removeEventListener("input", this.#boundHandleInput);
|
|
11906
|
+
this.#fillPicker.removeEventListener("change", this.#boundHandleChange);
|
|
11907
|
+
}
|
|
11908
|
+
|
|
11909
|
+
#watchPickerDialog = () => {
|
|
11910
|
+
requestAnimationFrame(() => {
|
|
11911
|
+
const dialog = document.querySelector(".fig-fill-picker-dialog[open]");
|
|
11912
|
+
if (!dialog) return;
|
|
11913
|
+
dialog.addEventListener("close", () => this.removeAttribute("selected"), {
|
|
11914
|
+
once: true,
|
|
11915
|
+
});
|
|
11916
|
+
});
|
|
11917
|
+
};
|
|
11918
|
+
|
|
11919
|
+
get #alphaEnabled() {
|
|
11920
|
+
const v = this.getAttribute("alpha");
|
|
11921
|
+
return v === null || v !== "false";
|
|
11922
|
+
}
|
|
11923
|
+
|
|
11924
|
+
#render() {
|
|
11925
|
+
if (this.#isAddMode) {
|
|
11926
|
+
this.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>`;
|
|
11927
|
+
this.#fillPicker = null;
|
|
11928
|
+
this.#chit = null;
|
|
11929
|
+
this.addEventListener("click", this.#handleAddClick);
|
|
11930
|
+
return;
|
|
11931
|
+
}
|
|
11932
|
+
this.removeEventListener("click", this.#handleAddClick);
|
|
11933
|
+
|
|
11934
|
+
const color = this.#normalizeColor(this.getAttribute("value"));
|
|
11935
|
+
const alphaAttr = this.#alphaEnabled ? "" : 'alpha="false"';
|
|
11936
|
+
this.innerHTML = `
|
|
11937
|
+
<fig-fill-picker mode="solid" ${alphaAttr} value='${JSON.stringify({ type: "solid", color })}'>
|
|
11938
|
+
<fig-chit background="${color}"></fig-chit>
|
|
11939
|
+
</fig-fill-picker>`;
|
|
11940
|
+
|
|
11941
|
+
this.#fillPicker = this.querySelector("fig-fill-picker");
|
|
11942
|
+
this.#chit = this.querySelector("fig-chit");
|
|
11943
|
+
this.#teardownListeners();
|
|
11944
|
+
this.#fillPicker?.addEventListener("input", this.#boundHandleInput);
|
|
11945
|
+
this.#fillPicker?.addEventListener("change", this.#boundHandleChange);
|
|
11946
|
+
this.#chit?.addEventListener("click", () => {
|
|
11947
|
+
this.setAttribute("selected", "");
|
|
11948
|
+
this.#watchPickerDialog();
|
|
11949
|
+
});
|
|
11950
|
+
}
|
|
11951
|
+
|
|
11952
|
+
#handleAddClick = () => {
|
|
11953
|
+
this.dispatchEvent(
|
|
11954
|
+
new CustomEvent("add", { bubbles: true, composed: true }),
|
|
11955
|
+
);
|
|
11956
|
+
};
|
|
11957
|
+
|
|
11958
|
+
#normalizeHex(hex) {
|
|
11959
|
+
if (!hex) return "#D9D9D9";
|
|
11960
|
+
const raw = hex.replace("#", "").trim();
|
|
11961
|
+
if (raw.length === 3 || raw.length === 4) {
|
|
11962
|
+
const [r, g, b] = raw;
|
|
11963
|
+
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
|
|
11964
|
+
}
|
|
11965
|
+
if (raw.length === 6 || raw.length === 8) {
|
|
11966
|
+
return `#${raw.slice(0, 6)}`.toUpperCase();
|
|
11967
|
+
}
|
|
11968
|
+
return "#D9D9D9";
|
|
11969
|
+
}
|
|
11970
|
+
|
|
11971
|
+
#normalizeColor(colorValue) {
|
|
11972
|
+
if (!colorValue) return "#D9D9D9";
|
|
11973
|
+
const value = String(colorValue).trim();
|
|
11974
|
+
|
|
11975
|
+
if (value.startsWith("{")) {
|
|
11976
|
+
try {
|
|
11977
|
+
const parsed = JSON.parse(value);
|
|
11978
|
+
if (parsed?.color) {
|
|
11979
|
+
return this.#normalizeColor(parsed.color);
|
|
11980
|
+
}
|
|
11981
|
+
} catch {
|
|
11982
|
+
// Ignore parse errors and continue.
|
|
11983
|
+
}
|
|
11984
|
+
}
|
|
11985
|
+
|
|
11986
|
+
if (value.startsWith("#")) {
|
|
11987
|
+
return this.#normalizeHex(value);
|
|
11988
|
+
}
|
|
11989
|
+
|
|
11990
|
+
try {
|
|
11991
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
11992
|
+
if (!ctx) return "#D9D9D9";
|
|
11993
|
+
ctx.fillStyle = value;
|
|
11994
|
+
const resolved = ctx.fillStyle;
|
|
11995
|
+
if (resolved.startsWith("#")) {
|
|
11996
|
+
return this.#normalizeHex(resolved);
|
|
11997
|
+
}
|
|
11998
|
+
const rgb = resolved.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
|
11999
|
+
if (rgb) {
|
|
12000
|
+
const toHex = (v) =>
|
|
12001
|
+
Math.max(0, Math.min(255, Number(v)))
|
|
12002
|
+
.toString(16)
|
|
12003
|
+
.padStart(2, "0");
|
|
12004
|
+
return `#${toHex(rgb[1])}${toHex(rgb[2])}${toHex(rgb[3])}`.toUpperCase();
|
|
12005
|
+
}
|
|
12006
|
+
} catch {
|
|
12007
|
+
// Fall through to default.
|
|
12008
|
+
}
|
|
12009
|
+
|
|
12010
|
+
return "#D9D9D9";
|
|
12011
|
+
}
|
|
12012
|
+
|
|
12013
|
+
#syncFromAttributes() {
|
|
12014
|
+
const color = this.#normalizeColor(this.getAttribute("value"));
|
|
12015
|
+
if (this.getAttribute("value") !== color) {
|
|
12016
|
+
this.setAttribute("value", color);
|
|
12017
|
+
return;
|
|
12018
|
+
}
|
|
12019
|
+
|
|
12020
|
+
if (this.#fillPicker) {
|
|
12021
|
+
this.#fillPicker.setAttribute(
|
|
12022
|
+
"value",
|
|
12023
|
+
JSON.stringify({ type: "solid", color }),
|
|
12024
|
+
);
|
|
12025
|
+
if (this.#alphaEnabled) {
|
|
12026
|
+
this.#fillPicker.removeAttribute("alpha");
|
|
12027
|
+
} else {
|
|
12028
|
+
this.#fillPicker.setAttribute("alpha", "false");
|
|
12029
|
+
}
|
|
12030
|
+
if (this.hasAttribute("disabled")) {
|
|
12031
|
+
this.#fillPicker.setAttribute("disabled", "");
|
|
12032
|
+
} else {
|
|
12033
|
+
this.#fillPicker.removeAttribute("disabled");
|
|
12034
|
+
}
|
|
12035
|
+
}
|
|
12036
|
+
|
|
12037
|
+
if (this.#chit) {
|
|
12038
|
+
this.#chit.setAttribute("background", color);
|
|
12039
|
+
if (this.hasAttribute("disabled")) {
|
|
12040
|
+
this.#chit.setAttribute("disabled", "");
|
|
12041
|
+
} else {
|
|
12042
|
+
this.#chit.removeAttribute("disabled");
|
|
12043
|
+
}
|
|
12044
|
+
}
|
|
12045
|
+
}
|
|
12046
|
+
|
|
12047
|
+
#updateColorFromPicker(detail, type) {
|
|
12048
|
+
const nextColor = this.#normalizeColor(detail?.color);
|
|
12049
|
+
const prevColor = this.#normalizeColor(this.getAttribute("value"));
|
|
12050
|
+
if (nextColor !== prevColor) {
|
|
12051
|
+
this.setAttribute("value", nextColor);
|
|
12052
|
+
} else {
|
|
12053
|
+
this.#syncFromAttributes();
|
|
12054
|
+
}
|
|
12055
|
+
|
|
12056
|
+
const eventDetail = { color: this.value };
|
|
12057
|
+
if (this.#alphaEnabled && detail?.opacity !== undefined) {
|
|
12058
|
+
eventDetail.opacity = detail.opacity;
|
|
12059
|
+
}
|
|
12060
|
+
|
|
12061
|
+
this.dispatchEvent(
|
|
12062
|
+
new CustomEvent(type, {
|
|
12063
|
+
bubbles: true,
|
|
12064
|
+
cancelable: true,
|
|
12065
|
+
composed: true,
|
|
12066
|
+
detail: eventDetail,
|
|
12067
|
+
}),
|
|
12068
|
+
);
|
|
12069
|
+
}
|
|
12070
|
+
|
|
12071
|
+
#handlePickerInput(event) {
|
|
12072
|
+
event.stopPropagation();
|
|
12073
|
+
this.#updateColorFromPicker(event.detail, "input");
|
|
12074
|
+
}
|
|
12075
|
+
|
|
12076
|
+
#handlePickerChange(event) {
|
|
12077
|
+
event.stopPropagation();
|
|
12078
|
+
this.#updateColorFromPicker(event.detail, "change");
|
|
12079
|
+
}
|
|
12080
|
+
|
|
12081
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
12082
|
+
if (oldValue === newValue) return;
|
|
12083
|
+
if (!this.isConnected) return;
|
|
12084
|
+
|
|
12085
|
+
switch (name) {
|
|
12086
|
+
case "add":
|
|
12087
|
+
this.#render();
|
|
12088
|
+
break;
|
|
12089
|
+
case "value":
|
|
12090
|
+
case "selected":
|
|
12091
|
+
case "disabled":
|
|
12092
|
+
this.#syncFromAttributes();
|
|
12093
|
+
break;
|
|
12094
|
+
}
|
|
12095
|
+
}
|
|
12096
|
+
|
|
12097
|
+
get value() {
|
|
12098
|
+
return this.#normalizeColor(this.getAttribute("value"));
|
|
12099
|
+
}
|
|
12100
|
+
set value(value) {
|
|
12101
|
+
if (value === null || value === undefined || value === "") {
|
|
12102
|
+
this.removeAttribute("value");
|
|
12103
|
+
return;
|
|
12104
|
+
}
|
|
12105
|
+
this.setAttribute("value", this.#normalizeColor(value));
|
|
12106
|
+
}
|
|
12107
|
+
|
|
12108
|
+
get selected() {
|
|
12109
|
+
return this.hasAttribute("selected");
|
|
12110
|
+
}
|
|
12111
|
+
set selected(value) {
|
|
12112
|
+
this.toggleAttribute("selected", Boolean(value));
|
|
12113
|
+
}
|
|
12114
|
+
|
|
12115
|
+
get disabled() {
|
|
12116
|
+
return this.hasAttribute("disabled");
|
|
12117
|
+
}
|
|
12118
|
+
set disabled(value) {
|
|
12119
|
+
this.toggleAttribute("disabled", Boolean(value));
|
|
12120
|
+
}
|
|
12121
|
+
}
|
|
12122
|
+
customElements.define("fig-color-tip", FigColorTip);
|
|
12123
|
+
|
|
12124
|
+
/* Choice */
|
|
12125
|
+
/**
|
|
12126
|
+
* A generic choice container for use within FigChooser.
|
|
12127
|
+
* @attr {string} value - Identifier for this choice
|
|
12128
|
+
* @attr {boolean} selected - Whether this choice is currently selected
|
|
10953
12129
|
* @attr {boolean} disabled - Whether this choice is disabled
|
|
10954
12130
|
*/
|
|
10955
12131
|
class FigChoice extends HTMLElement {
|
|
@@ -11509,8 +12685,382 @@ customElements.define("fig-chooser", FigChooser);
|
|
|
11509
12685
|
|
|
11510
12686
|
/* Handle */
|
|
11511
12687
|
class FigHandle extends HTMLElement {
|
|
11512
|
-
|
|
11513
|
-
|
|
12688
|
+
static observedAttributes = [
|
|
12689
|
+
"color",
|
|
12690
|
+
"selected",
|
|
12691
|
+
"disabled",
|
|
12692
|
+
"drag",
|
|
12693
|
+
"drag-surface",
|
|
12694
|
+
"drag-axes",
|
|
12695
|
+
"drag-snapping",
|
|
12696
|
+
"value",
|
|
12697
|
+
"type",
|
|
12698
|
+
"add",
|
|
12699
|
+
];
|
|
12700
|
+
|
|
12701
|
+
#isDragging = false;
|
|
12702
|
+
#didDrag = false;
|
|
12703
|
+
#boundPointerDown = null;
|
|
12704
|
+
#applyingValue = false;
|
|
12705
|
+
#colorTip = null;
|
|
12706
|
+
|
|
12707
|
+
get #isAddMode() {
|
|
12708
|
+
return this.hasAttribute("add") && this.getAttribute("add") !== "false";
|
|
12709
|
+
}
|
|
12710
|
+
|
|
12711
|
+
get #dragEnabled() {
|
|
12712
|
+
const v = this.getAttribute("drag");
|
|
12713
|
+
return v !== null && v !== "false";
|
|
12714
|
+
}
|
|
12715
|
+
|
|
12716
|
+
get #axes() {
|
|
12717
|
+
const v = (this.getAttribute("drag-axes") || "x,y").toLowerCase();
|
|
12718
|
+
return { x: v.includes("x"), y: v.includes("y") };
|
|
12719
|
+
}
|
|
12720
|
+
|
|
12721
|
+
get #dragSnappingMode() {
|
|
12722
|
+
const raw = this.getAttribute("drag-snapping");
|
|
12723
|
+
if (raw === null) return "false";
|
|
12724
|
+
const normalized = raw.trim().toLowerCase();
|
|
12725
|
+
if (normalized === "modifier") return "modifier";
|
|
12726
|
+
if (normalized === "" || normalized === "true") return "true";
|
|
12727
|
+
return "false";
|
|
12728
|
+
}
|
|
12729
|
+
|
|
12730
|
+
#shouldSnap(shiftKey) {
|
|
12731
|
+
const mode = this.#dragSnappingMode;
|
|
12732
|
+
if (mode === "true") return true;
|
|
12733
|
+
if (mode === "modifier") return !!shiftKey;
|
|
12734
|
+
return false;
|
|
12735
|
+
}
|
|
12736
|
+
|
|
12737
|
+
#snapGuide(value) {
|
|
12738
|
+
if (value < 0.1) return 0;
|
|
12739
|
+
if (value > 0.9) return 1;
|
|
12740
|
+
if (value > 0.4 && value < 0.6) return 0.5;
|
|
12741
|
+
return value;
|
|
12742
|
+
}
|
|
12743
|
+
|
|
12744
|
+
#snapDiagonal(x, y) {
|
|
12745
|
+
const diff = Math.abs(x - y);
|
|
12746
|
+
if (diff < 0.1) {
|
|
12747
|
+
const avg = (x + y) / 2;
|
|
12748
|
+
return { x: avg, y: avg };
|
|
12749
|
+
}
|
|
12750
|
+
if (Math.abs(1 - x - y) < 0.1) return { x, y: 1 - x };
|
|
12751
|
+
return { x, y };
|
|
12752
|
+
}
|
|
12753
|
+
|
|
12754
|
+
#getContainer() {
|
|
12755
|
+
const attr = this.getAttribute("drag-surface");
|
|
12756
|
+
if (!attr || attr === "parent") return this.parentElement;
|
|
12757
|
+
return this.closest(attr);
|
|
12758
|
+
}
|
|
12759
|
+
|
|
12760
|
+
get value() {
|
|
12761
|
+
const container = this.#getContainer();
|
|
12762
|
+
if (!container) return "0% 0%";
|
|
12763
|
+
const rect = container.getBoundingClientRect();
|
|
12764
|
+
const hw = this.offsetWidth / 2;
|
|
12765
|
+
const hh = this.offsetHeight / 2;
|
|
12766
|
+
const x = parseFloat(this.style.left) || 0;
|
|
12767
|
+
const y = parseFloat(this.style.top) || 0;
|
|
12768
|
+
const px = rect.width > 0 ? ((x + hw) / rect.width) * 100 : 0;
|
|
12769
|
+
const py = rect.height > 0 ? ((y + hh) / rect.height) * 100 : 0;
|
|
12770
|
+
return `${Math.round(px)}% ${Math.round(py)}%`;
|
|
12771
|
+
}
|
|
12772
|
+
|
|
12773
|
+
set value(v) {
|
|
12774
|
+
this.setAttribute("value", v ?? "0% 0%");
|
|
12775
|
+
}
|
|
12776
|
+
|
|
12777
|
+
#parseValue(str) {
|
|
12778
|
+
const normalized = str == null ? "" : String(str).trim();
|
|
12779
|
+
if (!normalized) return { xPct: 0, yPct: 0 };
|
|
12780
|
+
|
|
12781
|
+
const parts = normalized.split(/[\s,]+/).filter(Boolean);
|
|
12782
|
+
|
|
12783
|
+
const parseToken = (token) => {
|
|
12784
|
+
if (!token) return 0;
|
|
12785
|
+
const hasPx = token.includes("px");
|
|
12786
|
+
const hasPct = token.includes("%");
|
|
12787
|
+
const numeric = parseFloat(token.replace(/[%px]/g, ""));
|
|
12788
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
12789
|
+
if (hasPx) return { px: numeric };
|
|
12790
|
+
if (hasPct || Math.abs(numeric) > 1)
|
|
12791
|
+
return Math.max(0, Math.min(100, numeric));
|
|
12792
|
+
return Math.max(0, Math.min(100, numeric * 100));
|
|
12793
|
+
};
|
|
12794
|
+
|
|
12795
|
+
const xToken = parseToken(parts[0]);
|
|
12796
|
+
const yToken = parseToken(parts[1] ?? parts[0]);
|
|
12797
|
+
return { xToken, yToken };
|
|
12798
|
+
}
|
|
12799
|
+
|
|
12800
|
+
#applyValue(str) {
|
|
12801
|
+
const container = this.#getContainer();
|
|
12802
|
+
if (!container) return;
|
|
12803
|
+
|
|
12804
|
+
const { xToken, yToken } = this.#parseValue(str);
|
|
12805
|
+
const rect = container.getBoundingClientRect();
|
|
12806
|
+
const hw = this.offsetWidth / 2;
|
|
12807
|
+
const hh = this.offsetHeight / 2;
|
|
12808
|
+
|
|
12809
|
+
const resolve = (token, containerDim, halfHandle) => {
|
|
12810
|
+
if (token && typeof token === "object" && "px" in token) {
|
|
12811
|
+
return Math.max(
|
|
12812
|
+
-halfHandle,
|
|
12813
|
+
Math.min(containerDim - halfHandle, token.px - halfHandle),
|
|
12814
|
+
);
|
|
12815
|
+
}
|
|
12816
|
+
const pct = typeof token === "number" ? token : 0;
|
|
12817
|
+
const center = (pct / 100) * containerDim;
|
|
12818
|
+
return Math.max(
|
|
12819
|
+
-halfHandle,
|
|
12820
|
+
Math.min(containerDim - halfHandle, center - halfHandle),
|
|
12821
|
+
);
|
|
12822
|
+
};
|
|
12823
|
+
|
|
12824
|
+
const axes = this.#axes;
|
|
12825
|
+
if (axes.x) this.style.left = `${resolve(xToken, rect.width, hw)}px`;
|
|
12826
|
+
if (axes.y) this.style.top = `${resolve(yToken, rect.height, hh)}px`;
|
|
12827
|
+
}
|
|
12828
|
+
|
|
12829
|
+
#syncValueAttribute() {
|
|
12830
|
+
this.#applyingValue = true;
|
|
12831
|
+
this.setAttribute("value", this.value);
|
|
12832
|
+
this.#applyingValue = false;
|
|
12833
|
+
}
|
|
12834
|
+
|
|
12835
|
+
connectedCallback() {
|
|
12836
|
+
this.#syncDrag();
|
|
12837
|
+
this.addEventListener("click", this.#handleSelect);
|
|
12838
|
+
document.addEventListener("pointerdown", this.#handleDeselect);
|
|
12839
|
+
const initial = this.getAttribute("value");
|
|
12840
|
+
if (initial) this.#applyValue(initial);
|
|
12841
|
+
if (this.#isAddMode) this.#showColorTip();
|
|
12842
|
+
}
|
|
12843
|
+
|
|
12844
|
+
disconnectedCallback() {
|
|
12845
|
+
this.#teardownDrag();
|
|
12846
|
+
this.#hideColorTip();
|
|
12847
|
+
this.removeEventListener("click", this.#handleSelect);
|
|
12848
|
+
document.removeEventListener("pointerdown", this.#handleDeselect);
|
|
12849
|
+
}
|
|
12850
|
+
|
|
12851
|
+
#handleSelect = (e) => {
|
|
12852
|
+
if (this.hasAttribute("disabled")) return;
|
|
12853
|
+
if (this.#isAddMode) return;
|
|
12854
|
+
if (this.#didDrag) {
|
|
12855
|
+
this.#didDrag = false;
|
|
12856
|
+
return;
|
|
12857
|
+
}
|
|
12858
|
+
this.setAttribute("selected", "");
|
|
12859
|
+
if (this.getAttribute("type") === "color") this.#showColorTip();
|
|
12860
|
+
};
|
|
12861
|
+
|
|
12862
|
+
#handleDeselect = (e) => {
|
|
12863
|
+
if (this.#isAddMode) return;
|
|
12864
|
+
if (this.contains(e.target)) return;
|
|
12865
|
+
if (this.#colorTip && e.target.closest?.("dialog, [popover]")) return;
|
|
12866
|
+
this.removeAttribute("selected");
|
|
12867
|
+
this.#hideColorTip();
|
|
12868
|
+
};
|
|
12869
|
+
|
|
12870
|
+
attributeChangedCallback(name, _old, value) {
|
|
12871
|
+
if (name === "color") {
|
|
12872
|
+
if (!value || value === "false") {
|
|
12873
|
+
this.style.removeProperty("--fill");
|
|
12874
|
+
} else {
|
|
12875
|
+
this.style.setProperty("--fill", value);
|
|
12876
|
+
}
|
|
12877
|
+
}
|
|
12878
|
+
if (name === "drag") this.#syncDrag();
|
|
12879
|
+
if (name === "value" && !this.#applyingValue && !this.#isDragging) {
|
|
12880
|
+
this.#applyValue(value);
|
|
12881
|
+
}
|
|
12882
|
+
if (name === "add") {
|
|
12883
|
+
if (this.#isAddMode) {
|
|
12884
|
+
this.#showColorTip();
|
|
12885
|
+
} else {
|
|
12886
|
+
this.#hideColorTip();
|
|
12887
|
+
}
|
|
12888
|
+
}
|
|
12889
|
+
}
|
|
12890
|
+
|
|
12891
|
+
#syncDrag() {
|
|
12892
|
+
if (this.#dragEnabled && !this.#boundPointerDown) {
|
|
12893
|
+
this.#boundPointerDown = (e) => this.#onPointerDown(e);
|
|
12894
|
+
this.addEventListener("pointerdown", this.#boundPointerDown);
|
|
12895
|
+
} else if (!this.#dragEnabled && this.#boundPointerDown) {
|
|
12896
|
+
this.#teardownDrag();
|
|
12897
|
+
}
|
|
12898
|
+
}
|
|
12899
|
+
|
|
12900
|
+
#teardownDrag() {
|
|
12901
|
+
if (this.#boundPointerDown) {
|
|
12902
|
+
this.removeEventListener("pointerdown", this.#boundPointerDown);
|
|
12903
|
+
this.#boundPointerDown = null;
|
|
12904
|
+
}
|
|
12905
|
+
this.#isDragging = false;
|
|
12906
|
+
}
|
|
12907
|
+
|
|
12908
|
+
#onPointerDown(e) {
|
|
12909
|
+
if (!this.#dragEnabled || this.hasAttribute("disabled")) return;
|
|
12910
|
+
e.preventDefault();
|
|
12911
|
+
const container = this.#getContainer();
|
|
12912
|
+
if (!container) return;
|
|
12913
|
+
|
|
12914
|
+
this.#isDragging = true;
|
|
12915
|
+
const axes = this.#axes;
|
|
12916
|
+
const containerRect = container.getBoundingClientRect();
|
|
12917
|
+
const handleW = this.offsetWidth;
|
|
12918
|
+
const handleH = this.offsetHeight;
|
|
12919
|
+
|
|
12920
|
+
const clampAndApply = (clientX, clientY, shiftKey = false) => {
|
|
12921
|
+
const rect = container.getBoundingClientRect();
|
|
12922
|
+
const currentLeft = parseFloat(this.style.left) || 0;
|
|
12923
|
+
const currentTop = parseFloat(this.style.top) || 0;
|
|
12924
|
+
const rawX = clientX - rect.left - handleW / 2;
|
|
12925
|
+
const rawY = clientY - rect.top - handleH / 2;
|
|
12926
|
+
|
|
12927
|
+
const clampedX = Math.max(
|
|
12928
|
+
-handleW / 2,
|
|
12929
|
+
Math.min(rect.width - handleW / 2, rawX),
|
|
12930
|
+
);
|
|
12931
|
+
const clampedY = Math.max(
|
|
12932
|
+
-handleH / 2,
|
|
12933
|
+
Math.min(rect.height - handleH / 2, rawY),
|
|
12934
|
+
);
|
|
12935
|
+
|
|
12936
|
+
let centerX =
|
|
12937
|
+
rect.width > 0
|
|
12938
|
+
? ((axes.x ? clampedX : currentLeft) + handleW / 2) / rect.width
|
|
12939
|
+
: 0.5;
|
|
12940
|
+
let centerY =
|
|
12941
|
+
rect.height > 0
|
|
12942
|
+
? ((axes.y ? clampedY : currentTop) + handleH / 2) / rect.height
|
|
12943
|
+
: 0.5;
|
|
12944
|
+
|
|
12945
|
+
if (this.#shouldSnap(shiftKey)) {
|
|
12946
|
+
if (axes.x) centerX = this.#snapGuide(centerX);
|
|
12947
|
+
if (axes.y) centerY = this.#snapGuide(centerY);
|
|
12948
|
+
if (axes.x && axes.y) {
|
|
12949
|
+
const diagonal = this.#snapDiagonal(centerX, centerY);
|
|
12950
|
+
centerX = diagonal.x;
|
|
12951
|
+
centerY = diagonal.y;
|
|
12952
|
+
}
|
|
12953
|
+
}
|
|
12954
|
+
|
|
12955
|
+
if (axes.x) {
|
|
12956
|
+
const left = centerX * rect.width - handleW / 2;
|
|
12957
|
+
this.style.left = `${Math.max(-handleW / 2, Math.min(rect.width - handleW / 2, left))}px`;
|
|
12958
|
+
}
|
|
12959
|
+
if (axes.y) {
|
|
12960
|
+
const top = centerY * rect.height - handleH / 2;
|
|
12961
|
+
this.style.top = `${Math.max(-handleH / 2, Math.min(rect.height - handleH / 2, top))}px`;
|
|
12962
|
+
}
|
|
12963
|
+
};
|
|
12964
|
+
|
|
12965
|
+
const isColorType = this.getAttribute("type") === "color";
|
|
12966
|
+
if (!isColorType) {
|
|
12967
|
+
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
12968
|
+
}
|
|
12969
|
+
this.style.cursor = "grabbing";
|
|
12970
|
+
if (!isColorType) {
|
|
12971
|
+
this.dispatchEvent(
|
|
12972
|
+
new CustomEvent("input", {
|
|
12973
|
+
bubbles: true,
|
|
12974
|
+
detail: this.#positionDetail(containerRect),
|
|
12975
|
+
}),
|
|
12976
|
+
);
|
|
12977
|
+
}
|
|
12978
|
+
|
|
12979
|
+
const onMove = (e) => {
|
|
12980
|
+
if (!this.#isDragging) return;
|
|
12981
|
+
this.#didDrag = true;
|
|
12982
|
+
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
12983
|
+
this.dispatchEvent(
|
|
12984
|
+
new CustomEvent("input", {
|
|
12985
|
+
bubbles: true,
|
|
12986
|
+
detail: this.#positionDetail(container.getBoundingClientRect()),
|
|
12987
|
+
}),
|
|
12988
|
+
);
|
|
12989
|
+
};
|
|
12990
|
+
|
|
12991
|
+
const onUp = (e) => {
|
|
12992
|
+
this.#isDragging = false;
|
|
12993
|
+
this.style.cursor = "";
|
|
12994
|
+
window.removeEventListener("pointermove", onMove);
|
|
12995
|
+
window.removeEventListener("pointerup", onUp);
|
|
12996
|
+
if (this.#didDrag || !isColorType) {
|
|
12997
|
+
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
12998
|
+
}
|
|
12999
|
+
this.#syncValueAttribute();
|
|
13000
|
+
this.dispatchEvent(
|
|
13001
|
+
new CustomEvent("change", {
|
|
13002
|
+
bubbles: true,
|
|
13003
|
+
detail: this.#positionDetail(container.getBoundingClientRect()),
|
|
13004
|
+
}),
|
|
13005
|
+
);
|
|
13006
|
+
};
|
|
13007
|
+
|
|
13008
|
+
window.addEventListener("pointermove", onMove);
|
|
13009
|
+
window.addEventListener("pointerup", onUp);
|
|
13010
|
+
}
|
|
13011
|
+
|
|
13012
|
+
#showColorTip() {
|
|
13013
|
+
if (this.#colorTip) return;
|
|
13014
|
+
const tip = document.createElement("fig-color-tip");
|
|
13015
|
+
if (this.#isAddMode) {
|
|
13016
|
+
tip.setAttribute("add", "");
|
|
13017
|
+
} else {
|
|
13018
|
+
tip.setAttribute("value", this.getAttribute("color") || "#D9D9D9");
|
|
13019
|
+
tip.setAttribute("alpha", "true");
|
|
13020
|
+
tip.setAttribute("selected", "");
|
|
13021
|
+
}
|
|
13022
|
+
tip.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
13023
|
+
tip.addEventListener("input", this.#handleColorTipInput);
|
|
13024
|
+
tip.addEventListener("change", this.#handleColorTipChange);
|
|
13025
|
+
tip.addEventListener("add", this.#handleColorTipAdd);
|
|
13026
|
+
this.appendChild(tip);
|
|
13027
|
+
this.#colorTip = tip;
|
|
13028
|
+
}
|
|
13029
|
+
|
|
13030
|
+
#hideColorTip() {
|
|
13031
|
+
if (!this.#colorTip) return;
|
|
13032
|
+
this.#colorTip.removeEventListener("input", this.#handleColorTipInput);
|
|
13033
|
+
this.#colorTip.removeEventListener("change", this.#handleColorTipChange);
|
|
13034
|
+
this.#colorTip.removeEventListener("add", this.#handleColorTipAdd);
|
|
13035
|
+
this.#colorTip.remove();
|
|
13036
|
+
this.#colorTip = null;
|
|
13037
|
+
}
|
|
13038
|
+
|
|
13039
|
+
#handleColorTipInput = (e) => {
|
|
13040
|
+
e.stopPropagation();
|
|
13041
|
+
if (e.detail?.color) this.setAttribute("color", e.detail.color);
|
|
13042
|
+
};
|
|
13043
|
+
|
|
13044
|
+
#handleColorTipChange = (e) => {
|
|
13045
|
+
e.stopPropagation();
|
|
13046
|
+
if (e.detail?.color) this.setAttribute("color", e.detail.color);
|
|
13047
|
+
};
|
|
13048
|
+
|
|
13049
|
+
#handleColorTipAdd = (e) => {
|
|
13050
|
+
e.stopPropagation();
|
|
13051
|
+
this.dispatchEvent(
|
|
13052
|
+
new CustomEvent("add", { bubbles: true, composed: true }),
|
|
13053
|
+
);
|
|
13054
|
+
};
|
|
13055
|
+
|
|
13056
|
+
#positionDetail(containerRect) {
|
|
13057
|
+
const hw = this.offsetWidth / 2;
|
|
13058
|
+
const hh = this.offsetHeight / 2;
|
|
13059
|
+
const x = parseFloat(this.style.left) || 0;
|
|
13060
|
+
const y = parseFloat(this.style.top) || 0;
|
|
13061
|
+
const px = containerRect.width > 0 ? (x + hw) / containerRect.width : 0;
|
|
13062
|
+
const py = containerRect.height > 0 ? (y + hh) / containerRect.height : 0;
|
|
13063
|
+
return { x, y, px, py };
|
|
11514
13064
|
}
|
|
11515
13065
|
}
|
|
11516
13066
|
customElements.define("fig-handle", FigHandle);
|