@rogieking/figui3 2.24.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components.css +94 -8
- package/fig.js +894 -119
- package/index.html +369 -3
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -365,6 +365,9 @@ customElements.define("fig-dropdown", FigDropdown);
|
|
|
365
365
|
* @attr {string} offset - Comma-separated offset values: left,top,right,bottom
|
|
366
366
|
*/
|
|
367
367
|
class FigTooltip extends HTMLElement {
|
|
368
|
+
static #lastShownAt = 0;
|
|
369
|
+
static #warmupWindow = 500;
|
|
370
|
+
|
|
368
371
|
#boundHideOnChromeOpen;
|
|
369
372
|
#boundHidePopupOutsideClick;
|
|
370
373
|
#touchTimeout;
|
|
@@ -532,7 +535,9 @@ class FigTooltip extends HTMLElement {
|
|
|
532
535
|
showDelayedPopup() {
|
|
533
536
|
this.render();
|
|
534
537
|
clearTimeout(this.timeout);
|
|
535
|
-
|
|
538
|
+
const warm = Date.now() - FigTooltip.#lastShownAt < FigTooltip.#warmupWindow;
|
|
539
|
+
const effectiveDelay = warm ? 0 : this.delay;
|
|
540
|
+
this.timeout = setTimeout(this.showPopup.bind(this), effectiveDelay);
|
|
536
541
|
}
|
|
537
542
|
|
|
538
543
|
showPopup() {
|
|
@@ -544,6 +549,7 @@ class FigTooltip extends HTMLElement {
|
|
|
544
549
|
this.popup.style.zIndex = figGetHighestZIndex() + 1;
|
|
545
550
|
|
|
546
551
|
this.isOpen = true;
|
|
552
|
+
FigTooltip.#lastShownAt = Date.now();
|
|
547
553
|
this.#startObserving();
|
|
548
554
|
}
|
|
549
555
|
|
|
@@ -594,6 +600,7 @@ class FigTooltip extends HTMLElement {
|
|
|
594
600
|
}
|
|
595
601
|
|
|
596
602
|
this.isOpen = false;
|
|
603
|
+
FigTooltip.#lastShownAt = Date.now();
|
|
597
604
|
}
|
|
598
605
|
|
|
599
606
|
#startObserving() {
|
|
@@ -3384,16 +3391,29 @@ class FigInputColor extends HTMLElement {
|
|
|
3384
3391
|
return this.getAttribute("picker") || "native";
|
|
3385
3392
|
}
|
|
3386
3393
|
|
|
3394
|
+
#buildFillPickerAttrs() {
|
|
3395
|
+
const attrs = {};
|
|
3396
|
+
const experimental = this.getAttribute("experimental");
|
|
3397
|
+
if (experimental) attrs["experimental"] = experimental;
|
|
3398
|
+
// picker-* attributes forwarded to fill picker (except anchor, handled programmatically)
|
|
3399
|
+
for (const { name, value } of this.attributes) {
|
|
3400
|
+
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
3401
|
+
attrs[name.slice(7)] = value;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
|
|
3405
|
+
return Object.entries(attrs)
|
|
3406
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
3407
|
+
.join(" ");
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3387
3410
|
connectedCallback() {
|
|
3388
3411
|
this.#setValues(this.getAttribute("value"));
|
|
3389
3412
|
|
|
3390
3413
|
const useFigmaPicker = this.picker === "figma";
|
|
3391
3414
|
const hidePicker = this.picker === "false";
|
|
3392
3415
|
const showAlpha = this.getAttribute("alpha") === "true";
|
|
3393
|
-
const
|
|
3394
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
3395
|
-
const dialogPos = this.getAttribute("dialog-position") || "left";
|
|
3396
|
-
const dialogPosAttr = `dialog-position="${dialogPos}"`;
|
|
3416
|
+
const fpAttrs = this.#buildFillPickerAttrs();
|
|
3397
3417
|
|
|
3398
3418
|
let html = ``;
|
|
3399
3419
|
if (this.getAttribute("text")) {
|
|
@@ -3418,7 +3438,7 @@ class FigInputColor extends HTMLElement {
|
|
|
3418
3438
|
let swatchElement = "";
|
|
3419
3439
|
if (!hidePicker) {
|
|
3420
3440
|
swatchElement = useFigmaPicker
|
|
3421
|
-
? `<fig-fill-picker mode="solid" ${
|
|
3441
|
+
? `<fig-fill-picker mode="solid" ${fpAttrs} ${
|
|
3422
3442
|
showAlpha ? "" : 'alpha="false"'
|
|
3423
3443
|
} value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
|
|
3424
3444
|
this.alpha
|
|
@@ -3436,7 +3456,7 @@ class FigInputColor extends HTMLElement {
|
|
|
3436
3456
|
html = ``;
|
|
3437
3457
|
} else {
|
|
3438
3458
|
html = useFigmaPicker
|
|
3439
|
-
? `<fig-fill-picker mode="solid" ${
|
|
3459
|
+
? `<fig-fill-picker mode="solid" ${fpAttrs} ${
|
|
3440
3460
|
showAlpha ? "" : 'alpha="false"'
|
|
3441
3461
|
} value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
|
|
3442
3462
|
this.alpha
|
|
@@ -3460,6 +3480,13 @@ class FigInputColor extends HTMLElement {
|
|
|
3460
3480
|
|
|
3461
3481
|
// Setup fill picker (figma picker)
|
|
3462
3482
|
if (this.#fillPicker) {
|
|
3483
|
+
const anchor = this.getAttribute("picker-anchor");
|
|
3484
|
+
if (anchor === "self") {
|
|
3485
|
+
this.#fillPicker.anchorElement = this;
|
|
3486
|
+
} else if (anchor) {
|
|
3487
|
+
const el = document.querySelector(anchor);
|
|
3488
|
+
if (el) this.#fillPicker.anchorElement = el;
|
|
3489
|
+
}
|
|
3463
3490
|
if (this.hasAttribute("disabled")) {
|
|
3464
3491
|
this.#fillPicker.setAttribute("disabled", "");
|
|
3465
3492
|
}
|
|
@@ -3628,7 +3655,7 @@ class FigInputColor extends HTMLElement {
|
|
|
3628
3655
|
}
|
|
3629
3656
|
|
|
3630
3657
|
static get observedAttributes() {
|
|
3631
|
-
return ["value", "style", "mode", "picker", "experimental"
|
|
3658
|
+
return ["value", "style", "mode", "picker", "experimental"];
|
|
3632
3659
|
}
|
|
3633
3660
|
|
|
3634
3661
|
get mode() {
|
|
@@ -3810,7 +3837,7 @@ class FigInputFill extends HTMLElement {
|
|
|
3810
3837
|
}
|
|
3811
3838
|
|
|
3812
3839
|
static get observedAttributes() {
|
|
3813
|
-
return ["value", "disabled", "mode", "experimental"];
|
|
3840
|
+
return ["value", "disabled", "mode", "experimental", "alpha"];
|
|
3814
3841
|
}
|
|
3815
3842
|
|
|
3816
3843
|
connectedCallback() {
|
|
@@ -3863,9 +3890,46 @@ class FigInputFill extends HTMLElement {
|
|
|
3863
3890
|
}
|
|
3864
3891
|
}
|
|
3865
3892
|
|
|
3893
|
+
#buildFillPickerAttrs() {
|
|
3894
|
+
const attrs = {};
|
|
3895
|
+
// Backward-compat: direct attributes forwarded to fill picker
|
|
3896
|
+
const mode = this.getAttribute("mode");
|
|
3897
|
+
if (mode) attrs["mode"] = mode;
|
|
3898
|
+
const experimental = this.getAttribute("experimental");
|
|
3899
|
+
if (experimental) attrs["experimental"] = experimental;
|
|
3900
|
+
const alpha = this.getAttribute("alpha");
|
|
3901
|
+
if (alpha) attrs["alpha"] = alpha;
|
|
3902
|
+
// picker-* overrides (except anchor, handled programmatically)
|
|
3903
|
+
for (const { name, value } of this.attributes) {
|
|
3904
|
+
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
3905
|
+
attrs[name.slice(7)] = value;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
|
|
3909
|
+
return Object.entries(attrs)
|
|
3910
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
3911
|
+
.join(" ");
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3866
3914
|
#render() {
|
|
3867
3915
|
const disabled = this.hasAttribute("disabled");
|
|
3868
3916
|
const fillPickerValue = JSON.stringify(this.value);
|
|
3917
|
+
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
3918
|
+
|
|
3919
|
+
const opacityHtml = (value) =>
|
|
3920
|
+
showAlpha
|
|
3921
|
+
? `<fig-tooltip text="Opacity">
|
|
3922
|
+
<fig-input-number
|
|
3923
|
+
class="fig-input-fill-opacity"
|
|
3924
|
+
placeholder="##"
|
|
3925
|
+
min="0"
|
|
3926
|
+
max="100"
|
|
3927
|
+
value="${value}"
|
|
3928
|
+
units="%"
|
|
3929
|
+
${disabled ? "disabled" : ""}>
|
|
3930
|
+
</fig-input-number>
|
|
3931
|
+
</fig-tooltip>`
|
|
3932
|
+
: "";
|
|
3869
3933
|
|
|
3870
3934
|
let controlsHtml = "";
|
|
3871
3935
|
|
|
@@ -3879,17 +3943,7 @@ class FigInputFill extends HTMLElement {
|
|
|
3879
3943
|
value="${this.#solid.color.slice(1).toUpperCase()}"
|
|
3880
3944
|
${disabled ? "disabled" : ""}>
|
|
3881
3945
|
</fig-input-text>
|
|
3882
|
-
|
|
3883
|
-
<fig-input-number
|
|
3884
|
-
class="fig-input-fill-opacity"
|
|
3885
|
-
placeholder="##"
|
|
3886
|
-
min="0"
|
|
3887
|
-
max="100"
|
|
3888
|
-
value="${Math.round(this.#solid.alpha * 100)}"
|
|
3889
|
-
units="%"
|
|
3890
|
-
${disabled ? "disabled" : ""}>
|
|
3891
|
-
</fig-input-number>
|
|
3892
|
-
</fig-tooltip>`;
|
|
3946
|
+
${opacityHtml(Math.round(this.#solid.alpha * 100))}`;
|
|
3893
3947
|
break;
|
|
3894
3948
|
|
|
3895
3949
|
case "gradient":
|
|
@@ -3898,75 +3952,34 @@ class FigInputFill extends HTMLElement {
|
|
|
3898
3952
|
this.#gradient.type.slice(1);
|
|
3899
3953
|
controlsHtml = `
|
|
3900
3954
|
<label class="fig-input-fill-label">${gradientLabel}</label>
|
|
3901
|
-
|
|
3902
|
-
<fig-input-number
|
|
3903
|
-
class="fig-input-fill-opacity"
|
|
3904
|
-
placeholder="##"
|
|
3905
|
-
min="0"
|
|
3906
|
-
max="100"
|
|
3907
|
-
value="${this.#gradient.stops[0]?.opacity ?? 100}"
|
|
3908
|
-
units="%"
|
|
3909
|
-
${disabled ? "disabled" : ""}>
|
|
3910
|
-
</fig-input-number>
|
|
3911
|
-
</fig-tooltip>`;
|
|
3955
|
+
${opacityHtml(this.#gradient.stops[0]?.opacity ?? 100)}`;
|
|
3912
3956
|
break;
|
|
3913
3957
|
|
|
3914
3958
|
case "image":
|
|
3915
3959
|
controlsHtml = `
|
|
3916
3960
|
<label class="fig-input-fill-label">Image</label>
|
|
3917
|
-
|
|
3918
|
-
<fig-input-number
|
|
3919
|
-
class="fig-input-fill-opacity"
|
|
3920
|
-
placeholder="##"
|
|
3921
|
-
min="0"
|
|
3922
|
-
max="100"
|
|
3923
|
-
value="${Math.round((this.#image.opacity ?? 1) * 100)}"
|
|
3924
|
-
units="%"
|
|
3925
|
-
${disabled ? "disabled" : ""}>
|
|
3926
|
-
</fig-input-number>
|
|
3927
|
-
</fig-tooltip>`;
|
|
3961
|
+
${opacityHtml(Math.round((this.#image.opacity ?? 1) * 100))}`;
|
|
3928
3962
|
break;
|
|
3929
3963
|
|
|
3930
3964
|
case "video":
|
|
3931
3965
|
controlsHtml = `
|
|
3932
3966
|
<label class="fig-input-fill-label">Video</label>
|
|
3933
|
-
|
|
3934
|
-
<fig-input-number
|
|
3935
|
-
class="fig-input-fill-opacity"
|
|
3936
|
-
placeholder="##"
|
|
3937
|
-
min="0"
|
|
3938
|
-
max="100"
|
|
3939
|
-
value="${Math.round((this.#video.opacity ?? 1) * 100)}"
|
|
3940
|
-
units="%"
|
|
3941
|
-
${disabled ? "disabled" : ""}>
|
|
3942
|
-
</fig-input-number>
|
|
3943
|
-
</fig-tooltip>`;
|
|
3967
|
+
${opacityHtml(Math.round((this.#video.opacity ?? 1) * 100))}`;
|
|
3944
3968
|
break;
|
|
3945
3969
|
|
|
3946
3970
|
case "webcam":
|
|
3947
3971
|
controlsHtml = `
|
|
3948
3972
|
<label class="fig-input-fill-label">Webcam</label>
|
|
3949
|
-
|
|
3950
|
-
<fig-input-number
|
|
3951
|
-
class="fig-input-fill-opacity"
|
|
3952
|
-
placeholder="##"
|
|
3953
|
-
min="0"
|
|
3954
|
-
max="100"
|
|
3955
|
-
value="${Math.round((this.#webcam.opacity ?? 1) * 100)}"
|
|
3956
|
-
units="%"
|
|
3957
|
-
${disabled ? "disabled" : ""}>
|
|
3958
|
-
</fig-input-number>
|
|
3959
|
-
</fig-tooltip>`;
|
|
3973
|
+
${opacityHtml(Math.round((this.#webcam.opacity ?? 1) * 100))}`;
|
|
3960
3974
|
break;
|
|
3961
3975
|
}
|
|
3962
3976
|
|
|
3963
|
-
const
|
|
3964
|
-
const experimentalAttr = this.getAttribute("experimental");
|
|
3977
|
+
const fpAttrs = this.#buildFillPickerAttrs();
|
|
3965
3978
|
this.innerHTML = `
|
|
3966
3979
|
<div class="input-combo">
|
|
3967
|
-
<fig-fill-picker
|
|
3980
|
+
<fig-fill-picker ${fpAttrs} value='${fillPickerValue}' ${
|
|
3968
3981
|
disabled ? "disabled" : ""
|
|
3969
|
-
}
|
|
3982
|
+
}></fig-fill-picker>
|
|
3970
3983
|
${controlsHtml}
|
|
3971
3984
|
</div>`;
|
|
3972
3985
|
|
|
@@ -3991,6 +4004,14 @@ class FigInputFill extends HTMLElement {
|
|
|
3991
4004
|
}
|
|
3992
4005
|
|
|
3993
4006
|
if (this.#fillPicker) {
|
|
4007
|
+
const anchor = this.getAttribute("picker-anchor");
|
|
4008
|
+
if (!anchor || anchor === "self") {
|
|
4009
|
+
this.#fillPicker.anchorElement = this;
|
|
4010
|
+
} else {
|
|
4011
|
+
const el = document.querySelector(anchor);
|
|
4012
|
+
if (el) this.#fillPicker.anchorElement = el;
|
|
4013
|
+
}
|
|
4014
|
+
|
|
3994
4015
|
this.#fillPicker.addEventListener("input", (e) => {
|
|
3995
4016
|
e.stopPropagation();
|
|
3996
4017
|
const detail = e.detail;
|
|
@@ -5108,6 +5129,14 @@ class FigImage extends HTMLElement {
|
|
|
5108
5129
|
this.size = this.getAttribute("size") || "small";
|
|
5109
5130
|
this.innerHTML = this.#getInnerHTML();
|
|
5110
5131
|
this.#updateRefs();
|
|
5132
|
+
const ar = this.getAttribute("aspect-ratio");
|
|
5133
|
+
if (ar && ar !== "auto") {
|
|
5134
|
+
this.style.setProperty("--aspect-ratio", ar);
|
|
5135
|
+
}
|
|
5136
|
+
const fit = this.getAttribute("fit");
|
|
5137
|
+
if (fit) {
|
|
5138
|
+
this.style.setProperty("--fit", fit);
|
|
5139
|
+
}
|
|
5111
5140
|
}
|
|
5112
5141
|
disconnectedCallback() {
|
|
5113
5142
|
this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
|
|
@@ -5144,10 +5173,13 @@ class FigImage extends HTMLElement {
|
|
|
5144
5173
|
this.image.crossOrigin = "Anonymous";
|
|
5145
5174
|
this.image.onload = async () => {
|
|
5146
5175
|
this.aspectRatio = this.image.width / this.image.height;
|
|
5147
|
-
this.
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5176
|
+
const ar = this.getAttribute("aspect-ratio");
|
|
5177
|
+
if (!ar || ar === "auto") {
|
|
5178
|
+
this.style.setProperty(
|
|
5179
|
+
"--aspect-ratio",
|
|
5180
|
+
`${this.image.width}/${this.image.height}`
|
|
5181
|
+
);
|
|
5182
|
+
}
|
|
5151
5183
|
this.dispatchEvent(
|
|
5152
5184
|
new CustomEvent("loaded", {
|
|
5153
5185
|
bubbles: true,
|
|
@@ -5219,7 +5251,7 @@ class FigImage extends HTMLElement {
|
|
|
5219
5251
|
this.setAttribute("src", this.blob);
|
|
5220
5252
|
}
|
|
5221
5253
|
static get observedAttributes() {
|
|
5222
|
-
return ["src", "upload"];
|
|
5254
|
+
return ["src", "upload", "aspect-ratio", "fit"];
|
|
5223
5255
|
}
|
|
5224
5256
|
get src() {
|
|
5225
5257
|
return this.#src;
|
|
@@ -5251,10 +5283,665 @@ class FigImage extends HTMLElement {
|
|
|
5251
5283
|
if (name === "size") {
|
|
5252
5284
|
this.size = newValue;
|
|
5253
5285
|
}
|
|
5286
|
+
if (name === "aspect-ratio") {
|
|
5287
|
+
if (newValue && newValue !== "auto") {
|
|
5288
|
+
this.style.setProperty("--aspect-ratio", newValue);
|
|
5289
|
+
} else if (!newValue) {
|
|
5290
|
+
this.style.removeProperty("--aspect-ratio");
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5293
|
+
if (name === "fit") {
|
|
5294
|
+
if (newValue) {
|
|
5295
|
+
this.style.setProperty("--fit", newValue);
|
|
5296
|
+
} else {
|
|
5297
|
+
this.style.removeProperty("--fit");
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5254
5300
|
}
|
|
5255
5301
|
}
|
|
5256
5302
|
customElements.define("fig-image", FigImage);
|
|
5257
5303
|
|
|
5304
|
+
/**
|
|
5305
|
+
* A bezier / spring easing curve editor with draggable control points.
|
|
5306
|
+
* @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
|
|
5307
|
+
* @attr {number} precision - Decimal places for output values (default 2)
|
|
5308
|
+
* @attr {boolean} dropdown - Show a preset dropdown selector
|
|
5309
|
+
*/
|
|
5310
|
+
class FigEasingCurve extends HTMLElement {
|
|
5311
|
+
#cp1 = { x: 0.42, y: 0 };
|
|
5312
|
+
#cp2 = { x: 0.58, y: 1 };
|
|
5313
|
+
#spring = { stiffness: 200, damping: 15, mass: 1 };
|
|
5314
|
+
#mode = "bezier";
|
|
5315
|
+
#precision = 2;
|
|
5316
|
+
#isDragging = null;
|
|
5317
|
+
#svg = null;
|
|
5318
|
+
#curve = null;
|
|
5319
|
+
#line1 = null;
|
|
5320
|
+
#line2 = null;
|
|
5321
|
+
#handle1 = null;
|
|
5322
|
+
#handle2 = null;
|
|
5323
|
+
#dropdown = null;
|
|
5324
|
+
#presetName = null;
|
|
5325
|
+
#targetLine = null;
|
|
5326
|
+
#springDuration = 0.8;
|
|
5327
|
+
#drawWidth = 200;
|
|
5328
|
+
#drawHeight = 200;
|
|
5329
|
+
#bounds = null;
|
|
5330
|
+
#diagonal = null;
|
|
5331
|
+
#resizeObserver = null;
|
|
5332
|
+
|
|
5333
|
+
static PRESETS = [
|
|
5334
|
+
{ group: null, name: "Linear", type: "bezier", value: [0, 0, 1, 1] },
|
|
5335
|
+
{ group: "Bezier", name: "Ease in", type: "bezier", value: [0.42, 0, 1, 1] },
|
|
5336
|
+
{ group: "Bezier", name: "Ease out", type: "bezier", value: [0, 0, 0.58, 1] },
|
|
5337
|
+
{ group: "Bezier", name: "Ease in and out", type: "bezier", value: [0.42, 0, 0.58, 1] },
|
|
5338
|
+
{ group: "Bezier", name: "Ease in back", type: "bezier", value: [0.6, -0.28, 0.735, 0.045] },
|
|
5339
|
+
{ group: "Bezier", name: "Ease out back", type: "bezier", value: [0.175, 0.885, 0.32, 1.275] },
|
|
5340
|
+
{ group: "Bezier", name: "Ease in and out back", type: "bezier", value: [0.68, -0.55, 0.265, 1.55] },
|
|
5341
|
+
{ group: "Bezier", name: "Custom bezier", type: "bezier", value: null },
|
|
5342
|
+
{ group: "Spring", name: "Gentle", type: "spring", spring: { stiffness: 120, damping: 14, mass: 1 } },
|
|
5343
|
+
{ group: "Spring", name: "Quick", type: "spring", spring: { stiffness: 380, damping: 20, mass: 1 } },
|
|
5344
|
+
{ group: "Spring", name: "Bouncy", type: "spring", spring: { stiffness: 250, damping: 8, mass: 1 } },
|
|
5345
|
+
{ group: "Spring", name: "Slow", type: "spring", spring: { stiffness: 60, damping: 11, mass: 1 } },
|
|
5346
|
+
{ group: "Spring", name: "Custom spring", type: "spring", spring: null },
|
|
5347
|
+
];
|
|
5348
|
+
|
|
5349
|
+
static get observedAttributes() {
|
|
5350
|
+
return ["value", "precision"];
|
|
5351
|
+
}
|
|
5352
|
+
|
|
5353
|
+
connectedCallback() {
|
|
5354
|
+
this.#precision = parseInt(this.getAttribute("precision") || "2");
|
|
5355
|
+
const val = this.getAttribute("value");
|
|
5356
|
+
if (val) this.#parseValue(val);
|
|
5357
|
+
this.#presetName = this.#matchPreset();
|
|
5358
|
+
this.#render();
|
|
5359
|
+
this.#setupResizeObserver();
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5362
|
+
disconnectedCallback() {
|
|
5363
|
+
this.#isDragging = null;
|
|
5364
|
+
if (this.#resizeObserver) {
|
|
5365
|
+
this.#resizeObserver.disconnect();
|
|
5366
|
+
this.#resizeObserver = null;
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
5371
|
+
if (!this.#svg) return;
|
|
5372
|
+
if (name === "value" && newValue) {
|
|
5373
|
+
const prevMode = this.#mode;
|
|
5374
|
+
this.#parseValue(newValue);
|
|
5375
|
+
this.#presetName = this.#matchPreset();
|
|
5376
|
+
if (prevMode !== this.#mode) {
|
|
5377
|
+
this.#render();
|
|
5378
|
+
} else {
|
|
5379
|
+
this.#updatePaths();
|
|
5380
|
+
this.#syncDropdown();
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
if (name === "precision") {
|
|
5384
|
+
this.#precision = parseInt(newValue || "2");
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
|
|
5388
|
+
get value() {
|
|
5389
|
+
if (this.#mode === "spring") {
|
|
5390
|
+
const { stiffness, damping, mass } = this.#spring;
|
|
5391
|
+
return `spring(${stiffness}, ${damping}, ${mass})`;
|
|
5392
|
+
}
|
|
5393
|
+
const p = this.#precision;
|
|
5394
|
+
return `${this.#cp1.x.toFixed(p)}, ${this.#cp1.y.toFixed(p)}, ${this.#cp2.x.toFixed(p)}, ${this.#cp2.y.toFixed(p)}`;
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5397
|
+
get cssValue() {
|
|
5398
|
+
if (this.#mode === "spring") {
|
|
5399
|
+
const points = this.#simulateSpring();
|
|
5400
|
+
const samples = 20;
|
|
5401
|
+
const step = Math.max(1, Math.floor(points.length / samples));
|
|
5402
|
+
const vals = [];
|
|
5403
|
+
for (let i = 0; i < points.length; i += step) {
|
|
5404
|
+
vals.push(points[i].value.toFixed(3));
|
|
5405
|
+
}
|
|
5406
|
+
if (points.length > 0) vals.push(points[points.length - 1].value.toFixed(3));
|
|
5407
|
+
return `linear(${vals.join(", ")})`;
|
|
5408
|
+
}
|
|
5409
|
+
const p = this.#precision;
|
|
5410
|
+
return `cubic-bezier(${this.#cp1.x.toFixed(p)}, ${this.#cp1.y.toFixed(p)}, ${this.#cp2.x.toFixed(p)}, ${this.#cp2.y.toFixed(p)})`;
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
get preset() {
|
|
5414
|
+
return this.#presetName;
|
|
5415
|
+
}
|
|
5416
|
+
|
|
5417
|
+
set value(v) {
|
|
5418
|
+
this.setAttribute("value", v);
|
|
5419
|
+
}
|
|
5420
|
+
|
|
5421
|
+
#parseValue(str) {
|
|
5422
|
+
const springMatch = str.match(/^spring\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/);
|
|
5423
|
+
if (springMatch) {
|
|
5424
|
+
this.#mode = "spring";
|
|
5425
|
+
this.#spring.stiffness = parseFloat(springMatch[1]);
|
|
5426
|
+
this.#spring.damping = parseFloat(springMatch[2]);
|
|
5427
|
+
this.#spring.mass = parseFloat(springMatch[3]);
|
|
5428
|
+
return;
|
|
5429
|
+
}
|
|
5430
|
+
const parts = str.split(",").map((s) => parseFloat(s.trim()));
|
|
5431
|
+
if (parts.length >= 4 && parts.every((n) => !isNaN(n))) {
|
|
5432
|
+
this.#mode = "bezier";
|
|
5433
|
+
this.#cp1.x = parts[0];
|
|
5434
|
+
this.#cp1.y = parts[1];
|
|
5435
|
+
this.#cp2.x = parts[2];
|
|
5436
|
+
this.#cp2.y = parts[3];
|
|
5437
|
+
}
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5440
|
+
#matchPreset() {
|
|
5441
|
+
const ep = 0.001;
|
|
5442
|
+
if (this.#mode === "bezier") {
|
|
5443
|
+
for (const p of FigEasingCurve.PRESETS) {
|
|
5444
|
+
if (p.type !== "bezier" || !p.value) continue;
|
|
5445
|
+
if (
|
|
5446
|
+
Math.abs(this.#cp1.x - p.value[0]) < ep &&
|
|
5447
|
+
Math.abs(this.#cp1.y - p.value[1]) < ep &&
|
|
5448
|
+
Math.abs(this.#cp2.x - p.value[2]) < ep &&
|
|
5449
|
+
Math.abs(this.#cp2.y - p.value[3]) < ep
|
|
5450
|
+
) return p.name;
|
|
5451
|
+
}
|
|
5452
|
+
return "Custom bezier";
|
|
5453
|
+
}
|
|
5454
|
+
for (const p of FigEasingCurve.PRESETS) {
|
|
5455
|
+
if (p.type !== "spring" || !p.spring) continue;
|
|
5456
|
+
if (
|
|
5457
|
+
Math.abs(this.#spring.stiffness - p.spring.stiffness) < ep &&
|
|
5458
|
+
Math.abs(this.#spring.damping - p.spring.damping) < ep &&
|
|
5459
|
+
Math.abs(this.#spring.mass - p.spring.mass) < ep
|
|
5460
|
+
) return p.name;
|
|
5461
|
+
}
|
|
5462
|
+
return "Custom spring";
|
|
5463
|
+
}
|
|
5464
|
+
|
|
5465
|
+
// --- Spring simulation ---
|
|
5466
|
+
|
|
5467
|
+
#simulateSpring() {
|
|
5468
|
+
const { stiffness, damping, mass } = this.#spring;
|
|
5469
|
+
const dt = 0.004;
|
|
5470
|
+
const maxTime = 5;
|
|
5471
|
+
const points = [];
|
|
5472
|
+
let pos = 0, vel = 0;
|
|
5473
|
+
for (let t = 0; t <= maxTime; t += dt) {
|
|
5474
|
+
const force = -stiffness * (pos - 1) - damping * vel;
|
|
5475
|
+
vel += (force / mass) * dt;
|
|
5476
|
+
pos += vel * dt;
|
|
5477
|
+
points.push({ t, value: pos });
|
|
5478
|
+
if (t > 0.1 && Math.abs(pos - 1) < 0.0005 && Math.abs(vel) < 0.0005) break;
|
|
5479
|
+
}
|
|
5480
|
+
return points;
|
|
5481
|
+
}
|
|
5482
|
+
|
|
5483
|
+
static #springIcon(spring, size = 24) {
|
|
5484
|
+
const { stiffness, damping, mass } = spring;
|
|
5485
|
+
const dt = 0.004;
|
|
5486
|
+
const maxTime = 5;
|
|
5487
|
+
const pts = [];
|
|
5488
|
+
let pos = 0, vel = 0;
|
|
5489
|
+
for (let t = 0; t <= maxTime; t += dt) {
|
|
5490
|
+
const force = -stiffness * (pos - 1) - damping * vel;
|
|
5491
|
+
vel += (force / mass) * dt;
|
|
5492
|
+
pos += vel * dt;
|
|
5493
|
+
pts.push({ t, value: pos });
|
|
5494
|
+
if (t > 0.1 && Math.abs(pos - 1) < 0.001 && Math.abs(vel) < 0.001) break;
|
|
5495
|
+
}
|
|
5496
|
+
const totalTime = pts[pts.length - 1].t || 1;
|
|
5497
|
+
let maxVal = 1;
|
|
5498
|
+
for (const p of pts) if (p.value > maxVal) maxVal = p.value;
|
|
5499
|
+
let minVal = 0;
|
|
5500
|
+
for (const p of pts) if (p.value < minVal) minVal = p.value;
|
|
5501
|
+
const range = Math.max(maxVal - minVal, 1);
|
|
5502
|
+
const pad = 6;
|
|
5503
|
+
const s = size - pad * 2;
|
|
5504
|
+
const step = Math.max(1, Math.floor(pts.length / 30));
|
|
5505
|
+
let d = "";
|
|
5506
|
+
for (let i = 0; i < pts.length; i += step) {
|
|
5507
|
+
const x = pad + (pts[i].t / totalTime) * s;
|
|
5508
|
+
const y = pad + (1 - (pts[i].value - minVal) / range) * s;
|
|
5509
|
+
d += (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
|
|
5510
|
+
}
|
|
5511
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none"><path d="${d}" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>`;
|
|
5512
|
+
}
|
|
5513
|
+
|
|
5514
|
+
static curveIcon(cp1x, cp1y, cp2x, cp2y, size = 24) {
|
|
5515
|
+
const pad = 6;
|
|
5516
|
+
const s = size - pad * 2;
|
|
5517
|
+
const x = (n) => pad + n * s;
|
|
5518
|
+
const y = (n) => pad + (1 - n) * s;
|
|
5519
|
+
const d = `M${x(0)},${y(0)} C${x(cp1x)},${y(cp1y)} ${x(cp2x)},${y(cp2y)} ${x(1)},${y(1)}`;
|
|
5520
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none"><path d="${d}" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
// --- Rendering ---
|
|
5524
|
+
|
|
5525
|
+
#render() {
|
|
5526
|
+
this.innerHTML = this.#getInnerHTML();
|
|
5527
|
+
this.#cacheRefs();
|
|
5528
|
+
this.#syncViewportSize();
|
|
5529
|
+
this.#updatePaths();
|
|
5530
|
+
this.#setupEvents();
|
|
5531
|
+
}
|
|
5532
|
+
|
|
5533
|
+
#getDropdownHTML() {
|
|
5534
|
+
if (this.getAttribute("dropdown") !== "true") return "";
|
|
5535
|
+
let optionsHTML = "";
|
|
5536
|
+
let currentGroup = undefined;
|
|
5537
|
+
for (const p of FigEasingCurve.PRESETS) {
|
|
5538
|
+
if (p.group !== currentGroup) {
|
|
5539
|
+
if (currentGroup !== undefined) optionsHTML += `</optgroup>`;
|
|
5540
|
+
if (p.group) optionsHTML += `<optgroup label="${p.group}">`;
|
|
5541
|
+
currentGroup = p.group;
|
|
5542
|
+
}
|
|
5543
|
+
let icon;
|
|
5544
|
+
if (p.type === "spring") {
|
|
5545
|
+
const sp = p.spring || this.#spring;
|
|
5546
|
+
icon = FigEasingCurve.#springIcon(sp);
|
|
5547
|
+
} else {
|
|
5548
|
+
const v = p.value || [this.#cp1.x, this.#cp1.y, this.#cp2.x, this.#cp2.y];
|
|
5549
|
+
icon = FigEasingCurve.curveIcon(...v);
|
|
5550
|
+
}
|
|
5551
|
+
const selected = p.name === this.#presetName ? " selected" : "";
|
|
5552
|
+
optionsHTML += `<option value="${p.name}"${selected}>${icon} ${p.name}</option>`;
|
|
5553
|
+
}
|
|
5554
|
+
if (currentGroup) optionsHTML += `</optgroup>`;
|
|
5555
|
+
return `<fig-dropdown class="fig-easing-curve-dropdown" full experimental="modern">${optionsHTML}</fig-dropdown>`;
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
#getInnerHTML() {
|
|
5559
|
+
const size = 200;
|
|
5560
|
+
const dropdown = this.#getDropdownHTML();
|
|
5561
|
+
|
|
5562
|
+
if (this.#mode === "spring") {
|
|
5563
|
+
const targetY = 40;
|
|
5564
|
+
const startY = 180;
|
|
5565
|
+
return `${dropdown}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
5566
|
+
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
5567
|
+
<line class="fig-easing-curve-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
|
|
5568
|
+
<line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
|
|
5569
|
+
<path class="fig-easing-curve-path"/>
|
|
5570
|
+
<circle class="fig-easing-curve-handle" data-handle="bounce" r="5.25"/>
|
|
5571
|
+
<rect class="fig-easing-curve-duration-bar" data-handle="duration" width="6" height="16" rx="3" ry="3"/>
|
|
5572
|
+
</svg></div>`;
|
|
5573
|
+
}
|
|
5574
|
+
|
|
5575
|
+
return `${dropdown}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
5576
|
+
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
5577
|
+
<line class="fig-easing-curve-diagonal" x1="0" y1="${size}" x2="${size}" y2="0"/>
|
|
5578
|
+
<line class="fig-easing-curve-arm" data-arm="1"/>
|
|
5579
|
+
<line class="fig-easing-curve-arm" data-arm="2"/>
|
|
5580
|
+
<path class="fig-easing-curve-path"/>
|
|
5581
|
+
<circle class="fig-easing-curve-handle" data-handle="1" r="5.25"/>
|
|
5582
|
+
<circle class="fig-easing-curve-handle" data-handle="2" r="5.25"/>
|
|
5583
|
+
</svg></div>`;
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
#cacheRefs() {
|
|
5587
|
+
this.#svg = this.querySelector(".fig-easing-curve-svg");
|
|
5588
|
+
this.#curve = this.querySelector(".fig-easing-curve-path");
|
|
5589
|
+
this.#line1 = this.querySelector('[data-arm="1"]');
|
|
5590
|
+
this.#line2 = this.querySelector('[data-arm="2"]');
|
|
5591
|
+
this.#handle1 = this.querySelector('[data-handle="1"]') || this.querySelector('[data-handle="bounce"]');
|
|
5592
|
+
this.#handle2 = this.querySelector('[data-handle="2"]') || this.querySelector('[data-handle="duration"]');
|
|
5593
|
+
this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
|
|
5594
|
+
this.#targetLine = this.querySelector(".fig-easing-curve-target");
|
|
5595
|
+
this.#bounds = this.querySelector(".fig-easing-curve-bounds");
|
|
5596
|
+
this.#diagonal = this.querySelector(".fig-easing-curve-diagonal");
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
#setupResizeObserver() {
|
|
5600
|
+
if (this.#resizeObserver || !window.ResizeObserver) return;
|
|
5601
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
5602
|
+
if (this.#syncViewportSize()) {
|
|
5603
|
+
this.#updatePaths();
|
|
5604
|
+
}
|
|
5605
|
+
});
|
|
5606
|
+
this.#resizeObserver.observe(this);
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5609
|
+
#syncViewportSize() {
|
|
5610
|
+
if (!this.#svg) return false;
|
|
5611
|
+
const rect = this.#svg.getBoundingClientRect();
|
|
5612
|
+
const width = Math.max(1, Math.round(rect.width || 200));
|
|
5613
|
+
const height = Math.max(1, Math.round(rect.height || 200));
|
|
5614
|
+
const changed = width !== this.#drawWidth || height !== this.#drawHeight;
|
|
5615
|
+
this.#drawWidth = width;
|
|
5616
|
+
this.#drawHeight = height;
|
|
5617
|
+
this.#svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
5618
|
+
return changed;
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
// --- Coordinate helpers ---
|
|
5622
|
+
|
|
5623
|
+
#toSVG(nx, ny) {
|
|
5624
|
+
return { x: nx * this.#drawWidth, y: (1 - ny) * this.#drawHeight };
|
|
5625
|
+
}
|
|
5626
|
+
|
|
5627
|
+
#fromSVG(sx, sy) {
|
|
5628
|
+
return { x: sx / this.#drawWidth, y: 1 - sy / this.#drawHeight };
|
|
5629
|
+
}
|
|
5630
|
+
|
|
5631
|
+
#springScale = { minVal: 0, maxVal: 1.2, totalTime: 1 };
|
|
5632
|
+
|
|
5633
|
+
#springToSVG(nt, nv) {
|
|
5634
|
+
const pad = 20;
|
|
5635
|
+
const draw = this.#drawHeight - pad * 2;
|
|
5636
|
+
const { minVal, maxVal } = this.#springScale;
|
|
5637
|
+
const range = maxVal - minVal || 1;
|
|
5638
|
+
return {
|
|
5639
|
+
x: nt * this.#drawWidth,
|
|
5640
|
+
y: pad + (1 - (nv - minVal) / range) * draw,
|
|
5641
|
+
};
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
// --- Path updates ---
|
|
5645
|
+
|
|
5646
|
+
#updatePaths() {
|
|
5647
|
+
this.#syncViewportSize();
|
|
5648
|
+
if (this.#mode === "spring") {
|
|
5649
|
+
this.#updateSpringPaths();
|
|
5650
|
+
} else {
|
|
5651
|
+
this.#updateBezierPaths();
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
|
|
5655
|
+
#updateBezierPaths() {
|
|
5656
|
+
if (this.#bounds) {
|
|
5657
|
+
this.#bounds.setAttribute("x", "0");
|
|
5658
|
+
this.#bounds.setAttribute("y", "0");
|
|
5659
|
+
this.#bounds.setAttribute("width", this.#drawWidth);
|
|
5660
|
+
this.#bounds.setAttribute("height", this.#drawHeight);
|
|
5661
|
+
}
|
|
5662
|
+
if (this.#diagonal) {
|
|
5663
|
+
this.#diagonal.setAttribute("x1", "0");
|
|
5664
|
+
this.#diagonal.setAttribute("y1", this.#drawHeight);
|
|
5665
|
+
this.#diagonal.setAttribute("x2", this.#drawWidth);
|
|
5666
|
+
this.#diagonal.setAttribute("y2", "0");
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
const p0 = this.#toSVG(0, 0);
|
|
5670
|
+
const p1 = this.#toSVG(this.#cp1.x, this.#cp1.y);
|
|
5671
|
+
const p2 = this.#toSVG(this.#cp2.x, this.#cp2.y);
|
|
5672
|
+
const p3 = this.#toSVG(1, 1);
|
|
5673
|
+
|
|
5674
|
+
this.#curve.setAttribute("d", `M${p0.x},${p0.y} C${p1.x},${p1.y} ${p2.x},${p2.y} ${p3.x},${p3.y}`);
|
|
5675
|
+
this.#line1.setAttribute("x1", p0.x);
|
|
5676
|
+
this.#line1.setAttribute("y1", p0.y);
|
|
5677
|
+
this.#line1.setAttribute("x2", p1.x);
|
|
5678
|
+
this.#line1.setAttribute("y2", p1.y);
|
|
5679
|
+
this.#line2.setAttribute("x1", p3.x);
|
|
5680
|
+
this.#line2.setAttribute("y1", p3.y);
|
|
5681
|
+
this.#line2.setAttribute("x2", p2.x);
|
|
5682
|
+
this.#line2.setAttribute("y2", p2.y);
|
|
5683
|
+
this.#handle1.setAttribute("cx", p1.x);
|
|
5684
|
+
this.#handle1.setAttribute("cy", p1.y);
|
|
5685
|
+
this.#handle2.setAttribute("cx", p2.x);
|
|
5686
|
+
this.#handle2.setAttribute("cy", p2.y);
|
|
5687
|
+
}
|
|
5688
|
+
|
|
5689
|
+
#updateSpringPaths() {
|
|
5690
|
+
if (this.#bounds) {
|
|
5691
|
+
this.#bounds.setAttribute("x", "0");
|
|
5692
|
+
this.#bounds.setAttribute("y", "0");
|
|
5693
|
+
this.#bounds.setAttribute("width", this.#drawWidth);
|
|
5694
|
+
this.#bounds.setAttribute("height", this.#drawHeight);
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
const points = this.#simulateSpring();
|
|
5698
|
+
if (!points.length) return;
|
|
5699
|
+
const totalTime = points[points.length - 1].t || 1;
|
|
5700
|
+
|
|
5701
|
+
let minVal = 0, maxVal = 1;
|
|
5702
|
+
for (const p of points) {
|
|
5703
|
+
if (p.value < minVal) minVal = p.value;
|
|
5704
|
+
if (p.value > maxVal) maxVal = p.value;
|
|
5705
|
+
}
|
|
5706
|
+
const maxDistFromCenter = Math.max(Math.abs(minVal - 1), Math.abs(maxVal - 1), 0.01);
|
|
5707
|
+
const valPad = 0;
|
|
5708
|
+
this.#springScale = {
|
|
5709
|
+
minVal: 1 - maxDistFromCenter - valPad,
|
|
5710
|
+
maxVal: 1 + maxDistFromCenter + valPad,
|
|
5711
|
+
totalTime,
|
|
5712
|
+
};
|
|
5713
|
+
|
|
5714
|
+
const durationNorm = Math.max(0.05, Math.min(0.95, this.#springDuration));
|
|
5715
|
+
let d = "";
|
|
5716
|
+
for (let i = 0; i < points.length; i++) {
|
|
5717
|
+
const nt = (points[i].t / totalTime) * durationNorm;
|
|
5718
|
+
const pt = this.#springToSVG(nt, points[i].value);
|
|
5719
|
+
d += (i === 0 ? "M" : "L") + pt.x.toFixed(1) + "," + pt.y.toFixed(1);
|
|
5720
|
+
}
|
|
5721
|
+
const flatStart = this.#springToSVG(durationNorm, 1);
|
|
5722
|
+
const flatEnd = this.#springToSVG(1, 1);
|
|
5723
|
+
d += `L${flatStart.x.toFixed(1)},${flatStart.y.toFixed(1)} L${flatEnd.x.toFixed(1)},${flatEnd.y.toFixed(1)}`;
|
|
5724
|
+
this.#curve.setAttribute("d", d);
|
|
5725
|
+
|
|
5726
|
+
// Update target line position at value=1
|
|
5727
|
+
if (this.#targetLine) {
|
|
5728
|
+
const tl = this.#springToSVG(0, 1);
|
|
5729
|
+
const tr = this.#springToSVG(1, 1);
|
|
5730
|
+
this.#targetLine.setAttribute("x1", tl.x);
|
|
5731
|
+
this.#targetLine.setAttribute("y1", tl.y);
|
|
5732
|
+
this.#targetLine.setAttribute("x2", tr.x);
|
|
5733
|
+
this.#targetLine.setAttribute("y2", tr.y);
|
|
5734
|
+
}
|
|
5735
|
+
|
|
5736
|
+
// Bounce handle: at first overshoot peak
|
|
5737
|
+
const peak = this.#findPeakOvershoot(points);
|
|
5738
|
+
const peakNorm = (peak.t / totalTime) * durationNorm;
|
|
5739
|
+
const peakPt = this.#springToSVG(peakNorm, peak.value);
|
|
5740
|
+
this.#handle1.setAttribute("cx", peakPt.x);
|
|
5741
|
+
this.#handle1.setAttribute("cy", peakPt.y);
|
|
5742
|
+
|
|
5743
|
+
// Duration handle: on the target line
|
|
5744
|
+
const targetPt = this.#springToSVG(durationNorm, 1);
|
|
5745
|
+
this.#handle2.setAttribute("x", targetPt.x - 3);
|
|
5746
|
+
this.#handle2.setAttribute("y", targetPt.y - 8);
|
|
5747
|
+
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
#findPeakOvershoot(points) {
|
|
5751
|
+
let peak = { t: 0, value: 1 };
|
|
5752
|
+
let passedTarget = false;
|
|
5753
|
+
for (const p of points) {
|
|
5754
|
+
if (p.value >= 0.99) passedTarget = true;
|
|
5755
|
+
if (passedTarget && p.value > peak.value) {
|
|
5756
|
+
peak = { t: p.t, value: p.value };
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
return peak;
|
|
5760
|
+
}
|
|
5761
|
+
|
|
5762
|
+
// --- Dropdown ---
|
|
5763
|
+
|
|
5764
|
+
#syncDropdown() {
|
|
5765
|
+
if (!this.#dropdown) return;
|
|
5766
|
+
this.#dropdown.value = this.#presetName;
|
|
5767
|
+
this.#refreshCustomPresetIcons();
|
|
5768
|
+
}
|
|
5769
|
+
|
|
5770
|
+
#setOptionIconByValue(root, optionValue, icon) {
|
|
5771
|
+
if (!root) return;
|
|
5772
|
+
for (const option of root.querySelectorAll("option")) {
|
|
5773
|
+
if (option.value === optionValue) {
|
|
5774
|
+
option.innerHTML = `${icon} ${optionValue}`;
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5779
|
+
#refreshCustomPresetIcons() {
|
|
5780
|
+
if (!this.#dropdown) return;
|
|
5781
|
+
const bezierIcon = FigEasingCurve.curveIcon(
|
|
5782
|
+
this.#cp1.x,
|
|
5783
|
+
this.#cp1.y,
|
|
5784
|
+
this.#cp2.x,
|
|
5785
|
+
this.#cp2.y
|
|
5786
|
+
);
|
|
5787
|
+
const springIcon = FigEasingCurve.#springIcon(this.#spring);
|
|
5788
|
+
|
|
5789
|
+
// Update both slotted options and the cloned native select options.
|
|
5790
|
+
this.#setOptionIconByValue(this.#dropdown, "Custom bezier", bezierIcon);
|
|
5791
|
+
this.#setOptionIconByValue(this.#dropdown, "Custom spring", springIcon);
|
|
5792
|
+
this.#setOptionIconByValue(this.#dropdown.select, "Custom bezier", bezierIcon);
|
|
5793
|
+
this.#setOptionIconByValue(this.#dropdown.select, "Custom spring", springIcon);
|
|
5794
|
+
}
|
|
5795
|
+
|
|
5796
|
+
// --- Events ---
|
|
5797
|
+
|
|
5798
|
+
#emit(type) {
|
|
5799
|
+
this.dispatchEvent(new CustomEvent(type, {
|
|
5800
|
+
bubbles: true,
|
|
5801
|
+
detail: {
|
|
5802
|
+
mode: this.#mode,
|
|
5803
|
+
value: this.value,
|
|
5804
|
+
cssValue: this.cssValue,
|
|
5805
|
+
preset: this.#presetName,
|
|
5806
|
+
},
|
|
5807
|
+
}));
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5810
|
+
#setupEvents() {
|
|
5811
|
+
if (this.#mode === "bezier") {
|
|
5812
|
+
this.#handle1.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 1));
|
|
5813
|
+
this.#handle2.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 2));
|
|
5814
|
+
} else {
|
|
5815
|
+
this.#handle1.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "bounce"));
|
|
5816
|
+
this.#handle2.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "duration"));
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
if (this.#dropdown) {
|
|
5820
|
+
this.#dropdown.addEventListener("change", (e) => {
|
|
5821
|
+
const name = e.detail;
|
|
5822
|
+
const preset = FigEasingCurve.PRESETS.find((p) => p.name === name);
|
|
5823
|
+
if (!preset) return;
|
|
5824
|
+
|
|
5825
|
+
if (preset.type === "bezier") {
|
|
5826
|
+
if (preset.value) {
|
|
5827
|
+
this.#cp1.x = preset.value[0];
|
|
5828
|
+
this.#cp1.y = preset.value[1];
|
|
5829
|
+
this.#cp2.x = preset.value[2];
|
|
5830
|
+
this.#cp2.y = preset.value[3];
|
|
5831
|
+
}
|
|
5832
|
+
this.#presetName = name;
|
|
5833
|
+
if (this.#mode !== "bezier") {
|
|
5834
|
+
this.#mode = "bezier";
|
|
5835
|
+
this.#render();
|
|
5836
|
+
} else {
|
|
5837
|
+
this.#updatePaths();
|
|
5838
|
+
}
|
|
5839
|
+
} else if (preset.type === "spring") {
|
|
5840
|
+
if (preset.spring) {
|
|
5841
|
+
this.#spring = { ...preset.spring };
|
|
5842
|
+
}
|
|
5843
|
+
this.#presetName = name;
|
|
5844
|
+
if (this.#mode !== "spring") {
|
|
5845
|
+
this.#mode = "spring";
|
|
5846
|
+
this.#render();
|
|
5847
|
+
} else {
|
|
5848
|
+
this.#updatePaths();
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
this.#emit("input");
|
|
5852
|
+
this.#emit("change");
|
|
5853
|
+
});
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
|
|
5857
|
+
#clientToSVG(e) {
|
|
5858
|
+
const ctm = this.#svg.getScreenCTM();
|
|
5859
|
+
if (!ctm) return { x: 0, y: 0 };
|
|
5860
|
+
const inv = ctm.inverse();
|
|
5861
|
+
return {
|
|
5862
|
+
x: inv.a * e.clientX + inv.c * e.clientY + inv.e,
|
|
5863
|
+
y: inv.b * e.clientX + inv.d * e.clientY + inv.f,
|
|
5864
|
+
};
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
#startBezierDrag(e, handle) {
|
|
5868
|
+
e.preventDefault();
|
|
5869
|
+
this.#isDragging = handle;
|
|
5870
|
+
|
|
5871
|
+
const onMove = (e) => {
|
|
5872
|
+
if (!this.#isDragging) return;
|
|
5873
|
+
const svgPt = this.#clientToSVG(e);
|
|
5874
|
+
const norm = this.#fromSVG(svgPt.x, svgPt.y);
|
|
5875
|
+
|
|
5876
|
+
norm.x = Math.round(norm.x * 100) / 100;
|
|
5877
|
+
norm.y = Math.round(norm.y * 100) / 100;
|
|
5878
|
+
norm.x = Math.max(0, Math.min(1, norm.x));
|
|
5879
|
+
|
|
5880
|
+
if (this.#isDragging === 1) {
|
|
5881
|
+
this.#cp1.x = norm.x;
|
|
5882
|
+
this.#cp1.y = norm.y;
|
|
5883
|
+
} else {
|
|
5884
|
+
this.#cp2.x = norm.x;
|
|
5885
|
+
this.#cp2.y = norm.y;
|
|
5886
|
+
}
|
|
5887
|
+
this.#updatePaths();
|
|
5888
|
+
this.#presetName = this.#matchPreset();
|
|
5889
|
+
this.#syncDropdown();
|
|
5890
|
+
this.#emit("input");
|
|
5891
|
+
};
|
|
5892
|
+
|
|
5893
|
+
const onUp = () => {
|
|
5894
|
+
this.#isDragging = null;
|
|
5895
|
+
document.removeEventListener("pointermove", onMove);
|
|
5896
|
+
document.removeEventListener("pointerup", onUp);
|
|
5897
|
+
this.#emit("change");
|
|
5898
|
+
};
|
|
5899
|
+
|
|
5900
|
+
document.addEventListener("pointermove", onMove);
|
|
5901
|
+
document.addEventListener("pointerup", onUp);
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
#startSpringDrag(e, handleType) {
|
|
5905
|
+
e.preventDefault();
|
|
5906
|
+
this.#isDragging = handleType;
|
|
5907
|
+
|
|
5908
|
+
const startDamping = this.#spring.damping;
|
|
5909
|
+
const startStiffness = this.#spring.stiffness;
|
|
5910
|
+
const startDuration = this.#springDuration;
|
|
5911
|
+
const startY = e.clientY;
|
|
5912
|
+
const startX = e.clientX;
|
|
5913
|
+
|
|
5914
|
+
const onMove = (e) => {
|
|
5915
|
+
if (!this.#isDragging) return;
|
|
5916
|
+
|
|
5917
|
+
if (handleType === "bounce") {
|
|
5918
|
+
const dy = e.clientY - startY;
|
|
5919
|
+
this.#spring.damping = Math.max(1, Math.round(startDamping + dy * 0.15));
|
|
5920
|
+
} else {
|
|
5921
|
+
const dx = e.clientX - startX;
|
|
5922
|
+
this.#springDuration = Math.max(0.05, Math.min(0.95, startDuration + dx / 200));
|
|
5923
|
+
this.#spring.stiffness = Math.max(10, Math.round(startStiffness - dx * 1.5));
|
|
5924
|
+
}
|
|
5925
|
+
|
|
5926
|
+
this.#updatePaths();
|
|
5927
|
+
this.#presetName = this.#matchPreset();
|
|
5928
|
+
this.#syncDropdown();
|
|
5929
|
+
this.#emit("input");
|
|
5930
|
+
};
|
|
5931
|
+
|
|
5932
|
+
const onUp = () => {
|
|
5933
|
+
this.#isDragging = null;
|
|
5934
|
+
document.removeEventListener("pointermove", onMove);
|
|
5935
|
+
document.removeEventListener("pointerup", onUp);
|
|
5936
|
+
this.#emit("change");
|
|
5937
|
+
};
|
|
5938
|
+
|
|
5939
|
+
document.addEventListener("pointermove", onMove);
|
|
5940
|
+
document.addEventListener("pointerup", onUp);
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
customElements.define("fig-easing-curve", FigEasingCurve);
|
|
5944
|
+
|
|
5258
5945
|
/**
|
|
5259
5946
|
* A custom joystick input element.
|
|
5260
5947
|
* @attr {string} value - The current position of the joystick (e.g., "0.5,0.5").
|
|
@@ -6223,6 +6910,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
6223
6910
|
#chit = null;
|
|
6224
6911
|
#dialog = null;
|
|
6225
6912
|
#activeTab = "solid";
|
|
6913
|
+
anchorElement = null;
|
|
6226
6914
|
|
|
6227
6915
|
// Fill state
|
|
6228
6916
|
#fillType = "solid";
|
|
@@ -6241,6 +6929,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
6241
6929
|
#video = { url: null, scaleMode: "fill", scale: 50 };
|
|
6242
6930
|
#webcam = { stream: null, snapshot: null };
|
|
6243
6931
|
|
|
6932
|
+
// Custom mode slots and data
|
|
6933
|
+
#customSlots = {};
|
|
6934
|
+
#customData = {};
|
|
6935
|
+
|
|
6244
6936
|
// DOM references for solid tab
|
|
6245
6937
|
#colorArea = null;
|
|
6246
6938
|
#colorAreaHandle = null;
|
|
@@ -6275,7 +6967,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
6275
6967
|
}
|
|
6276
6968
|
|
|
6277
6969
|
#setupTrigger() {
|
|
6278
|
-
const child = this.
|
|
6970
|
+
const child = Array.from(this.children).find(
|
|
6971
|
+
(el) => !el.getAttribute("slot")?.startsWith("mode-")
|
|
6972
|
+
);
|
|
6279
6973
|
|
|
6280
6974
|
if (!child) {
|
|
6281
6975
|
// Scenario 1: Empty - create fig-chit
|
|
@@ -6315,6 +7009,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
6315
7009
|
const valueAttr = this.getAttribute("value");
|
|
6316
7010
|
if (!valueAttr) return;
|
|
6317
7011
|
|
|
7012
|
+
const builtinTypes = ["solid", "gradient", "image", "video", "webcam"];
|
|
7013
|
+
|
|
6318
7014
|
try {
|
|
6319
7015
|
const parsed = JSON.parse(valueAttr);
|
|
6320
7016
|
if (parsed.type) this.#fillType = parsed.type;
|
|
@@ -6337,6 +7033,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6337
7033
|
this.#gradient = { ...this.#gradient, ...parsed.gradient };
|
|
6338
7034
|
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
6339
7035
|
if (parsed.video) this.#video = { ...this.#video, ...parsed.video };
|
|
7036
|
+
|
|
7037
|
+
// Store full parsed data for custom (non-built-in) types
|
|
7038
|
+
if (parsed.type && !builtinTypes.includes(parsed.type)) {
|
|
7039
|
+
const { type, ...rest } = parsed;
|
|
7040
|
+
this.#customData[parsed.type] = rest;
|
|
7041
|
+
}
|
|
6340
7042
|
} catch (e) {
|
|
6341
7043
|
// If not JSON, treat as hex color
|
|
6342
7044
|
if (valueAttr.startsWith("#")) {
|
|
@@ -6387,7 +7089,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
6387
7089
|
}
|
|
6388
7090
|
break;
|
|
6389
7091
|
default:
|
|
6390
|
-
|
|
7092
|
+
const slot = this.#customSlots[this.#fillType];
|
|
7093
|
+
bg = slot?.element?.getAttribute("chit-background") || "#D9D9D9";
|
|
6391
7094
|
}
|
|
6392
7095
|
|
|
6393
7096
|
this.#chit.setAttribute("background", bg);
|
|
@@ -6434,6 +7137,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
6434
7137
|
}
|
|
6435
7138
|
|
|
6436
7139
|
#createDialog() {
|
|
7140
|
+
// Collect slotted custom mode content before any DOM changes
|
|
7141
|
+
this.#customSlots = {};
|
|
7142
|
+
this.querySelectorAll('[slot^="mode-"]').forEach((el) => {
|
|
7143
|
+
const modeName = el.getAttribute("slot").slice(5);
|
|
7144
|
+
this.#customSlots[modeName] = {
|
|
7145
|
+
element: el,
|
|
7146
|
+
label:
|
|
7147
|
+
el.getAttribute("label") ||
|
|
7148
|
+
modeName.charAt(0).toUpperCase() + modeName.slice(1),
|
|
7149
|
+
};
|
|
7150
|
+
});
|
|
7151
|
+
|
|
6437
7152
|
this.#dialog = document.createElement("dialog", { is: "fig-popup" });
|
|
6438
7153
|
this.#dialog.setAttribute("is", "fig-popup");
|
|
6439
7154
|
this.#dialog.setAttribute("drag", "true");
|
|
@@ -6441,14 +7156,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6441
7156
|
this.#dialog.setAttribute("autoresize", "false");
|
|
6442
7157
|
this.#dialog.classList.add("fig-fill-picker-dialog");
|
|
6443
7158
|
|
|
6444
|
-
this.#dialog.anchor = this.#trigger;
|
|
7159
|
+
this.#dialog.anchor = this.anchorElement || this.#trigger;
|
|
6445
7160
|
const dialogPosition = this.getAttribute("dialog-position") || "left";
|
|
6446
7161
|
this.#dialog.setAttribute("position", dialogPosition);
|
|
6447
7162
|
|
|
6448
|
-
|
|
6449
|
-
const
|
|
6450
|
-
const allModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6451
|
-
const modeLabels = {
|
|
7163
|
+
const builtinModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
7164
|
+
const builtinLabels = {
|
|
6452
7165
|
solid: "Solid",
|
|
6453
7166
|
gradient: "Gradient",
|
|
6454
7167
|
image: "Image",
|
|
@@ -6456,24 +7169,33 @@ class FigFillPicker extends HTMLElement {
|
|
|
6456
7169
|
webcam: "Webcam",
|
|
6457
7170
|
};
|
|
6458
7171
|
|
|
6459
|
-
//
|
|
6460
|
-
|
|
7172
|
+
// Build allowed modes: built-ins filtered normally, custom names accepted if slot exists
|
|
7173
|
+
const mode = this.getAttribute("mode");
|
|
7174
|
+
let allowedModes;
|
|
6461
7175
|
if (mode) {
|
|
6462
|
-
const
|
|
6463
|
-
allowedModes =
|
|
6464
|
-
|
|
7176
|
+
const requested = mode.split(",").map((m) => m.trim().toLowerCase());
|
|
7177
|
+
allowedModes = requested.filter(
|
|
7178
|
+
(m) => builtinModes.includes(m) || this.#customSlots[m]
|
|
7179
|
+
);
|
|
7180
|
+
if (allowedModes.length === 0) allowedModes = [...builtinModes];
|
|
7181
|
+
} else {
|
|
7182
|
+
allowedModes = [...builtinModes];
|
|
7183
|
+
}
|
|
7184
|
+
|
|
7185
|
+
// Build labels map: built-in labels + custom slot labels
|
|
7186
|
+
const modeLabels = { ...builtinLabels };
|
|
7187
|
+
for (const [name, { label }] of Object.entries(this.#customSlots)) {
|
|
7188
|
+
modeLabels[name] = label;
|
|
6465
7189
|
}
|
|
6466
7190
|
|
|
6467
|
-
// If current fillType not in allowed modes, switch to first allowed
|
|
6468
7191
|
if (!allowedModes.includes(this.#fillType)) {
|
|
6469
7192
|
this.#fillType = allowedModes[0];
|
|
6470
7193
|
this.#activeTab = allowedModes[0];
|
|
6471
7194
|
}
|
|
6472
7195
|
|
|
6473
|
-
// Build header content - label if single mode, dropdown if multiple
|
|
6474
7196
|
const experimental = this.getAttribute("experimental");
|
|
6475
7197
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
6476
|
-
|
|
7198
|
+
|
|
6477
7199
|
let headerContent;
|
|
6478
7200
|
if (allowedModes.length === 1) {
|
|
6479
7201
|
headerContent = `<h3 class="fig-fill-picker-type-label">${modeLabels[allowedModes[0]]}</h3>`;
|
|
@@ -6486,6 +7208,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
6486
7208
|
</fig-dropdown>`;
|
|
6487
7209
|
}
|
|
6488
7210
|
|
|
7211
|
+
// Generate tab containers for all allowed modes
|
|
7212
|
+
const tabDivs = allowedModes
|
|
7213
|
+
.map((m) => `<div class="fig-fill-picker-tab" data-tab="${m}"></div>`)
|
|
7214
|
+
.join("\n ");
|
|
7215
|
+
|
|
6489
7216
|
this.#dialog.innerHTML = `
|
|
6490
7217
|
<fig-header>
|
|
6491
7218
|
${headerContent}
|
|
@@ -6494,16 +7221,33 @@ class FigFillPicker extends HTMLElement {
|
|
|
6494
7221
|
</fig-button>
|
|
6495
7222
|
</fig-header>
|
|
6496
7223
|
<div class="fig-fill-picker-content">
|
|
6497
|
-
|
|
6498
|
-
<div class="fig-fill-picker-tab" data-tab="gradient"></div>
|
|
6499
|
-
<div class="fig-fill-picker-tab" data-tab="image"></div>
|
|
6500
|
-
<div class="fig-fill-picker-tab" data-tab="video"></div>
|
|
6501
|
-
<div class="fig-fill-picker-tab" data-tab="webcam"></div>
|
|
7224
|
+
${tabDivs}
|
|
6502
7225
|
</div>
|
|
6503
7226
|
`;
|
|
6504
7227
|
|
|
6505
7228
|
document.body.appendChild(this.#dialog);
|
|
6506
7229
|
|
|
7230
|
+
// Populate custom tab containers and emit modeready
|
|
7231
|
+
for (const [modeName, { element }] of Object.entries(this.#customSlots)) {
|
|
7232
|
+
const container = this.#dialog.querySelector(
|
|
7233
|
+
`[data-tab="${modeName}"]`
|
|
7234
|
+
);
|
|
7235
|
+
if (!container) continue;
|
|
7236
|
+
|
|
7237
|
+
// Move children (not the element itself) for vanilla HTML usage
|
|
7238
|
+
while (element.firstChild) {
|
|
7239
|
+
container.appendChild(element.firstChild);
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
// Emit modeready so frameworks can render into the container
|
|
7243
|
+
this.dispatchEvent(
|
|
7244
|
+
new CustomEvent("modeready", {
|
|
7245
|
+
bubbles: true,
|
|
7246
|
+
detail: { mode: modeName, container },
|
|
7247
|
+
})
|
|
7248
|
+
);
|
|
7249
|
+
}
|
|
7250
|
+
|
|
6507
7251
|
// Setup type dropdown switching (only if not locked)
|
|
6508
7252
|
const typeDropdown = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
6509
7253
|
if (typeDropdown) {
|
|
@@ -6518,34 +7262,50 @@ class FigFillPicker extends HTMLElement {
|
|
|
6518
7262
|
this.#dialog.open = false;
|
|
6519
7263
|
});
|
|
6520
7264
|
|
|
6521
|
-
// Emit change on close
|
|
6522
7265
|
this.#dialog.addEventListener("close", () => {
|
|
6523
7266
|
this.#emitChange();
|
|
6524
7267
|
});
|
|
6525
7268
|
|
|
6526
|
-
// Initialize tabs
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
const mode = this.getAttribute("mode");
|
|
6537
|
-
const allModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6538
|
-
|
|
6539
|
-
let allowedModes = allModes;
|
|
6540
|
-
if (mode) {
|
|
6541
|
-
const requestedModes = mode.split(",").map((m) => m.trim().toLowerCase());
|
|
6542
|
-
allowedModes = requestedModes.filter((m) => allModes.includes(m));
|
|
6543
|
-
if (allowedModes.length === 0) allowedModes = allModes;
|
|
7269
|
+
// Initialize built-in tabs (skip any overridden by custom slots)
|
|
7270
|
+
const builtinInits = {
|
|
7271
|
+
solid: () => this.#initSolidTab(),
|
|
7272
|
+
gradient: () => this.#initGradientTab(),
|
|
7273
|
+
image: () => this.#initImageTab(),
|
|
7274
|
+
video: () => this.#initVideoTab(),
|
|
7275
|
+
webcam: () => this.#initWebcamTab(),
|
|
7276
|
+
};
|
|
7277
|
+
for (const [name, init] of Object.entries(builtinInits)) {
|
|
7278
|
+
if (!this.#customSlots[name] && allowedModes.includes(name)) init();
|
|
6544
7279
|
}
|
|
6545
7280
|
|
|
6546
|
-
|
|
6547
|
-
|
|
7281
|
+
// Listen for input/change from custom tab content
|
|
7282
|
+
for (const modeName of Object.keys(this.#customSlots)) {
|
|
7283
|
+
if (builtinModes.includes(modeName)) continue;
|
|
7284
|
+
const container = this.#dialog.querySelector(
|
|
7285
|
+
`[data-tab="${modeName}"]`
|
|
7286
|
+
);
|
|
7287
|
+
if (!container) continue;
|
|
7288
|
+
container.addEventListener("input", (e) => {
|
|
7289
|
+
if (e.target === this) return;
|
|
7290
|
+
e.stopPropagation();
|
|
7291
|
+
if (e.detail) this.#customData[modeName] = e.detail;
|
|
7292
|
+
this.#emitInput();
|
|
7293
|
+
});
|
|
7294
|
+
container.addEventListener("change", (e) => {
|
|
7295
|
+
if (e.target === this) return;
|
|
7296
|
+
e.stopPropagation();
|
|
7297
|
+
if (e.detail) this.#customData[modeName] = e.detail;
|
|
7298
|
+
this.#emitChange();
|
|
7299
|
+
});
|
|
6548
7300
|
}
|
|
7301
|
+
}
|
|
7302
|
+
|
|
7303
|
+
#switchTab(tabName) {
|
|
7304
|
+
// Only allow switching to modes that have a tab container in the dialog
|
|
7305
|
+
const tab = this.#dialog?.querySelector(
|
|
7306
|
+
`.fig-fill-picker-tab[data-tab="${tabName}"]`
|
|
7307
|
+
);
|
|
7308
|
+
if (!tab) return;
|
|
6549
7309
|
|
|
6550
7310
|
this.#activeTab = tabName;
|
|
6551
7311
|
this.#fillType = tabName;
|
|
@@ -6566,6 +7326,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6566
7326
|
}
|
|
6567
7327
|
});
|
|
6568
7328
|
|
|
7329
|
+
// Zero out content padding for custom mode tabs
|
|
7330
|
+
const contentEl = this.#dialog.querySelector(".fig-fill-picker-content");
|
|
7331
|
+
if (contentEl) {
|
|
7332
|
+
contentEl.style.padding = this.#customSlots[tabName] ? "0" : "";
|
|
7333
|
+
}
|
|
7334
|
+
|
|
6569
7335
|
// Update tab-specific UI after visibility change
|
|
6570
7336
|
if (tabName === "gradient") {
|
|
6571
7337
|
// Use RAF to ensure layout is complete before updating angle input
|
|
@@ -6987,7 +7753,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
6987
7753
|
<fig-input-number class="fig-fill-picker-stop-position" min="0" max="100" value="${
|
|
6988
7754
|
stop.position
|
|
6989
7755
|
}" units="%"></fig-input-number>
|
|
6990
|
-
<fig-input-color class="fig-fill-picker-stop-color" text="true" alpha="true" picker="figma" dialog-position="right" value="${
|
|
7756
|
+
<fig-input-color class="fig-fill-picker-stop-color" text="true" alpha="true" picker="figma" picker-dialog-position="right" value="${
|
|
6991
7757
|
stop.color
|
|
6992
7758
|
}"></fig-input-color>
|
|
6993
7759
|
<fig-button icon variant="ghost" class="fig-fill-picker-stop-remove" ${
|
|
@@ -7015,9 +7781,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
7015
7781
|
this.#emitInput();
|
|
7016
7782
|
});
|
|
7017
7783
|
|
|
7018
|
-
row
|
|
7019
|
-
|
|
7020
|
-
|
|
7784
|
+
const stopColor = row.querySelector(".fig-fill-picker-stop-color");
|
|
7785
|
+
const stopFillPicker = stopColor.querySelector("fig-fill-picker");
|
|
7786
|
+
if (stopFillPicker) {
|
|
7787
|
+
stopFillPicker.anchorElement = this.#dialog;
|
|
7788
|
+
} else {
|
|
7789
|
+
requestAnimationFrame(() => {
|
|
7790
|
+
const fp = stopColor.querySelector("fig-fill-picker");
|
|
7791
|
+
if (fp) fp.anchorElement = this.#dialog;
|
|
7792
|
+
});
|
|
7793
|
+
}
|
|
7794
|
+
|
|
7795
|
+
stopColor.addEventListener("input", (e) => {
|
|
7021
7796
|
this.#gradient.stops[index].color =
|
|
7022
7797
|
e.target.hexOpaque || e.target.value;
|
|
7023
7798
|
const parsedAlpha = parseFloat(e.target.alpha);
|
|
@@ -7711,7 +8486,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
7711
8486
|
image: { url: this.#webcam.snapshot, scaleMode: "fill", scale: 50 },
|
|
7712
8487
|
};
|
|
7713
8488
|
default:
|
|
7714
|
-
return base;
|
|
8489
|
+
return { ...base, ...this.#customData[this.#fillType] };
|
|
7715
8490
|
}
|
|
7716
8491
|
}
|
|
7717
8492
|
|