@rogieking/figui3 2.25.0 → 2.27.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 +97 -8
- package/fig.js +715 -64
- package/index.html +26 -0
- package/package.json +1 -1
package/components.css
CHANGED
|
@@ -1152,7 +1152,7 @@ fig-chit {
|
|
|
1152
1152
|
background: var(--chit-background);
|
|
1153
1153
|
background-size: var(--chit-bg-size);
|
|
1154
1154
|
background-position: var(--chit-bg-position);
|
|
1155
|
-
background-repeat: repeat;
|
|
1155
|
+
background-repeat: no-repeat;
|
|
1156
1156
|
border-radius: 0.125rem;
|
|
1157
1157
|
box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
|
|
1158
1158
|
}
|
|
@@ -1199,16 +1199,21 @@ fig-chit {
|
|
|
1199
1199
|
/* Fig Image */
|
|
1200
1200
|
fig-image {
|
|
1201
1201
|
--image-size: 2rem;
|
|
1202
|
+
--fit: cover;
|
|
1203
|
+
--image-width: var(--image-size);
|
|
1204
|
+
--image-height: var(--image-size);
|
|
1205
|
+
--aspect-ratio: 1 / 1;
|
|
1202
1206
|
display: inline-grid;
|
|
1203
1207
|
place-items: center;
|
|
1204
|
-
width: var(--image-
|
|
1205
|
-
height: var(--image-
|
|
1206
|
-
aspect-ratio:
|
|
1208
|
+
width: var(--image-width);
|
|
1209
|
+
height: var(--image-height);
|
|
1210
|
+
aspect-ratio: var(--aspect-ratio);
|
|
1207
1211
|
> * {
|
|
1208
1212
|
grid-area: 1/1;
|
|
1209
1213
|
}
|
|
1210
1214
|
fig-chit {
|
|
1211
|
-
--size:
|
|
1215
|
+
--size: 100% !important;
|
|
1216
|
+
--chit-bg-size: var(--fit);
|
|
1212
1217
|
&[disabled] {
|
|
1213
1218
|
opacity: 1;
|
|
1214
1219
|
}
|
|
@@ -1224,10 +1229,16 @@ fig-image {
|
|
|
1224
1229
|
&[size="large"] {
|
|
1225
1230
|
--image-size: 8rem;
|
|
1226
1231
|
}
|
|
1227
|
-
&[
|
|
1228
|
-
|
|
1232
|
+
&[size="auto"] {
|
|
1233
|
+
--image-size: 6rem;
|
|
1234
|
+
&[full]:not([full="false"]) {
|
|
1235
|
+
--image-width: 100%;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
&[aspect-ratio] {
|
|
1229
1239
|
height: auto;
|
|
1230
1240
|
fig-chit {
|
|
1241
|
+
--size: 100% !important;
|
|
1231
1242
|
height: auto !important;
|
|
1232
1243
|
aspect-ratio: var(--aspect-ratio) !important;
|
|
1233
1244
|
}
|
|
@@ -1253,6 +1264,84 @@ fig-image {
|
|
|
1253
1264
|
}
|
|
1254
1265
|
}
|
|
1255
1266
|
|
|
1267
|
+
/* Easing Curve */
|
|
1268
|
+
fig-easing-curve {
|
|
1269
|
+
display: flex;
|
|
1270
|
+
flex-direction: column;
|
|
1271
|
+
gap: var(--spacer-2);
|
|
1272
|
+
--stroke-width: 1.25;
|
|
1273
|
+
|
|
1274
|
+
width: 100%;
|
|
1275
|
+
.fig-easing-curve-svg {
|
|
1276
|
+
width: 100%;
|
|
1277
|
+
aspect-ratio: 1 / 1;
|
|
1278
|
+
overflow: visible;
|
|
1279
|
+
}
|
|
1280
|
+
.fig-easing-curve-svg-container {
|
|
1281
|
+
border-radius: var(--radius-medium);
|
|
1282
|
+
background: var(--figma-color-bg-secondary);
|
|
1283
|
+
padding: var(--spacer-2);
|
|
1284
|
+
}
|
|
1285
|
+
.fig-easing-curve-diagonal,
|
|
1286
|
+
.fig-easing-curve-bounds,
|
|
1287
|
+
.fig-easing-curve-arm,
|
|
1288
|
+
.fig-easing-curve-path,
|
|
1289
|
+
.fig-easing-curve-target {
|
|
1290
|
+
pointer-events: none;
|
|
1291
|
+
}
|
|
1292
|
+
.fig-easing-curve-diagonal {
|
|
1293
|
+
stroke: var(--figma-color-bordertranslucent);
|
|
1294
|
+
stroke-width: var(--stroke-width);
|
|
1295
|
+
stroke-linejoin: round;
|
|
1296
|
+
stroke-linecap: round;
|
|
1297
|
+
}
|
|
1298
|
+
.fig-easing-curve-bounds {
|
|
1299
|
+
fill: transparent;
|
|
1300
|
+
}
|
|
1301
|
+
.fig-easing-curve-arm {
|
|
1302
|
+
stroke: var(--figma-color-border-strong);
|
|
1303
|
+
stroke-width: var(--stroke-width);
|
|
1304
|
+
stroke-linejoin: round;
|
|
1305
|
+
stroke-linecap: round;
|
|
1306
|
+
}
|
|
1307
|
+
.fig-easing-curve-path {
|
|
1308
|
+
fill: none;
|
|
1309
|
+
stroke: var(--figma-color-text);
|
|
1310
|
+
stroke-width: var(--stroke-width);
|
|
1311
|
+
stroke-linejoin: round;
|
|
1312
|
+
stroke-linecap: round;
|
|
1313
|
+
}
|
|
1314
|
+
.fig-easing-curve-target {
|
|
1315
|
+
stroke: var(--figma-color-bordertranslucent);
|
|
1316
|
+
stroke-width: var(--stroke-width);
|
|
1317
|
+
stroke-linejoin: round;
|
|
1318
|
+
stroke-linecap: round;
|
|
1319
|
+
}
|
|
1320
|
+
.fig-easing-curve-handle {
|
|
1321
|
+
fill: var(--figma-color-border-strong);
|
|
1322
|
+
cursor: grab;
|
|
1323
|
+
pointer-events: all;
|
|
1324
|
+
stroke: var(--figma-color-bg-secondary);
|
|
1325
|
+
stroke-width: var(--stroke-width);
|
|
1326
|
+
&:active {
|
|
1327
|
+
cursor: grabbing;
|
|
1328
|
+
fill: var(--figma-color-bg-brand);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
.fig-easing-curve-duration-bar {
|
|
1332
|
+
fill: var(--figma-color-border-strong);
|
|
1333
|
+
stroke: var(--figma-color-bg-secondary);
|
|
1334
|
+
stroke-width: var(--stroke-width);
|
|
1335
|
+
cursor: ew-resize;
|
|
1336
|
+
pointer-events: all;
|
|
1337
|
+
}
|
|
1338
|
+
.fig-easing-curve-dropdown {
|
|
1339
|
+
option svg {
|
|
1340
|
+
vertical-align: middle;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1256
1345
|
/* Combo input */
|
|
1257
1346
|
.input-combo {
|
|
1258
1347
|
display: inline-flex;
|
|
@@ -2497,7 +2586,7 @@ fig-input-fill {
|
|
|
2497
2586
|
|
|
2498
2587
|
fig-input-number {
|
|
2499
2588
|
flex: 0;
|
|
2500
|
-
flex-basis:
|
|
2589
|
+
flex-basis: 3rem;
|
|
2501
2590
|
}
|
|
2502
2591
|
|
|
2503
2592
|
&[disabled]:not([disabled="false"]) {
|
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() {
|
|
@@ -1078,7 +1085,9 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1078
1085
|
super();
|
|
1079
1086
|
this.#boundReposition = this.#queueReposition.bind(this);
|
|
1080
1087
|
this.#boundScroll = (e) => {
|
|
1081
|
-
if (this.open && !this.contains(e.target)
|
|
1088
|
+
if (this.open && !this.contains(e.target) && this.#shouldAutoReposition()) {
|
|
1089
|
+
this.#positionPopup();
|
|
1090
|
+
}
|
|
1082
1091
|
};
|
|
1083
1092
|
this.#boundOutsidePointerDown = this.#handleOutsidePointerDown.bind(this);
|
|
1084
1093
|
this.#boundPointerDown = this.#handlePointerDown.bind(this);
|
|
@@ -1215,6 +1224,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1215
1224
|
|
|
1216
1225
|
this.#setupObservers();
|
|
1217
1226
|
document.addEventListener("pointerdown", this.#boundOutsidePointerDown, true);
|
|
1227
|
+
this.#wasDragged = false;
|
|
1218
1228
|
this.#queueReposition();
|
|
1219
1229
|
this.#isPopupActive = true;
|
|
1220
1230
|
|
|
@@ -1227,6 +1237,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1227
1237
|
if (anchor) anchor.classList.remove("has-popup-open");
|
|
1228
1238
|
|
|
1229
1239
|
this.#isPopupActive = false;
|
|
1240
|
+
this.#wasDragged = false;
|
|
1230
1241
|
this.#teardownObservers();
|
|
1231
1242
|
document.removeEventListener(
|
|
1232
1243
|
"pointerdown",
|
|
@@ -1397,6 +1408,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1397
1408
|
if (dx > this.#dragThreshold || dy > this.#dragThreshold) {
|
|
1398
1409
|
this.#isDragging = true;
|
|
1399
1410
|
this.#dragPending = false;
|
|
1411
|
+
this.#wasDragged = true;
|
|
1400
1412
|
this.setPointerCapture(e.pointerId);
|
|
1401
1413
|
this.style.cursor = "grabbing";
|
|
1402
1414
|
|
|
@@ -1801,7 +1813,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1801
1813
|
}
|
|
1802
1814
|
|
|
1803
1815
|
#queueReposition() {
|
|
1804
|
-
if (!this.open) return;
|
|
1816
|
+
if (!this.open || !this.#shouldAutoReposition()) return;
|
|
1805
1817
|
if (this.#rafId !== null) return;
|
|
1806
1818
|
|
|
1807
1819
|
this.#rafId = requestAnimationFrame(() => {
|
|
@@ -1809,6 +1821,11 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1809
1821
|
this.#positionPopup();
|
|
1810
1822
|
});
|
|
1811
1823
|
}
|
|
1824
|
+
|
|
1825
|
+
#shouldAutoReposition() {
|
|
1826
|
+
if (!(this.drag && this.#wasDragged)) return true;
|
|
1827
|
+
return !this.#resolveAnchor();
|
|
1828
|
+
}
|
|
1812
1829
|
}
|
|
1813
1830
|
customElements.define("fig-popup", FigPopup, { extends: "dialog" });
|
|
1814
1831
|
|
|
@@ -3830,7 +3847,7 @@ class FigInputFill extends HTMLElement {
|
|
|
3830
3847
|
}
|
|
3831
3848
|
|
|
3832
3849
|
static get observedAttributes() {
|
|
3833
|
-
return ["value", "disabled", "mode", "experimental"];
|
|
3850
|
+
return ["value", "disabled", "mode", "experimental", "alpha"];
|
|
3834
3851
|
}
|
|
3835
3852
|
|
|
3836
3853
|
connectedCallback() {
|
|
@@ -3890,6 +3907,8 @@ class FigInputFill extends HTMLElement {
|
|
|
3890
3907
|
if (mode) attrs["mode"] = mode;
|
|
3891
3908
|
const experimental = this.getAttribute("experimental");
|
|
3892
3909
|
if (experimental) attrs["experimental"] = experimental;
|
|
3910
|
+
const alpha = this.getAttribute("alpha");
|
|
3911
|
+
if (alpha) attrs["alpha"] = alpha;
|
|
3893
3912
|
// picker-* overrides (except anchor, handled programmatically)
|
|
3894
3913
|
for (const { name, value } of this.attributes) {
|
|
3895
3914
|
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
@@ -3905,6 +3924,22 @@ class FigInputFill extends HTMLElement {
|
|
|
3905
3924
|
#render() {
|
|
3906
3925
|
const disabled = this.hasAttribute("disabled");
|
|
3907
3926
|
const fillPickerValue = JSON.stringify(this.value);
|
|
3927
|
+
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
3928
|
+
|
|
3929
|
+
const opacityHtml = (value) =>
|
|
3930
|
+
showAlpha
|
|
3931
|
+
? `<fig-tooltip text="Opacity">
|
|
3932
|
+
<fig-input-number
|
|
3933
|
+
class="fig-input-fill-opacity"
|
|
3934
|
+
placeholder="##"
|
|
3935
|
+
min="0"
|
|
3936
|
+
max="100"
|
|
3937
|
+
value="${value}"
|
|
3938
|
+
units="%"
|
|
3939
|
+
${disabled ? "disabled" : ""}>
|
|
3940
|
+
</fig-input-number>
|
|
3941
|
+
</fig-tooltip>`
|
|
3942
|
+
: "";
|
|
3908
3943
|
|
|
3909
3944
|
let controlsHtml = "";
|
|
3910
3945
|
|
|
@@ -3918,17 +3953,7 @@ class FigInputFill extends HTMLElement {
|
|
|
3918
3953
|
value="${this.#solid.color.slice(1).toUpperCase()}"
|
|
3919
3954
|
${disabled ? "disabled" : ""}>
|
|
3920
3955
|
</fig-input-text>
|
|
3921
|
-
|
|
3922
|
-
<fig-input-number
|
|
3923
|
-
class="fig-input-fill-opacity"
|
|
3924
|
-
placeholder="##"
|
|
3925
|
-
min="0"
|
|
3926
|
-
max="100"
|
|
3927
|
-
value="${Math.round(this.#solid.alpha * 100)}"
|
|
3928
|
-
units="%"
|
|
3929
|
-
${disabled ? "disabled" : ""}>
|
|
3930
|
-
</fig-input-number>
|
|
3931
|
-
</fig-tooltip>`;
|
|
3956
|
+
${opacityHtml(Math.round(this.#solid.alpha * 100))}`;
|
|
3932
3957
|
break;
|
|
3933
3958
|
|
|
3934
3959
|
case "gradient":
|
|
@@ -3937,65 +3962,25 @@ class FigInputFill extends HTMLElement {
|
|
|
3937
3962
|
this.#gradient.type.slice(1);
|
|
3938
3963
|
controlsHtml = `
|
|
3939
3964
|
<label class="fig-input-fill-label">${gradientLabel}</label>
|
|
3940
|
-
|
|
3941
|
-
<fig-input-number
|
|
3942
|
-
class="fig-input-fill-opacity"
|
|
3943
|
-
placeholder="##"
|
|
3944
|
-
min="0"
|
|
3945
|
-
max="100"
|
|
3946
|
-
value="${this.#gradient.stops[0]?.opacity ?? 100}"
|
|
3947
|
-
units="%"
|
|
3948
|
-
${disabled ? "disabled" : ""}>
|
|
3949
|
-
</fig-input-number>
|
|
3950
|
-
</fig-tooltip>`;
|
|
3965
|
+
${opacityHtml(this.#gradient.stops[0]?.opacity ?? 100)}`;
|
|
3951
3966
|
break;
|
|
3952
3967
|
|
|
3953
3968
|
case "image":
|
|
3954
3969
|
controlsHtml = `
|
|
3955
3970
|
<label class="fig-input-fill-label">Image</label>
|
|
3956
|
-
|
|
3957
|
-
<fig-input-number
|
|
3958
|
-
class="fig-input-fill-opacity"
|
|
3959
|
-
placeholder="##"
|
|
3960
|
-
min="0"
|
|
3961
|
-
max="100"
|
|
3962
|
-
value="${Math.round((this.#image.opacity ?? 1) * 100)}"
|
|
3963
|
-
units="%"
|
|
3964
|
-
${disabled ? "disabled" : ""}>
|
|
3965
|
-
</fig-input-number>
|
|
3966
|
-
</fig-tooltip>`;
|
|
3971
|
+
${opacityHtml(Math.round((this.#image.opacity ?? 1) * 100))}`;
|
|
3967
3972
|
break;
|
|
3968
3973
|
|
|
3969
3974
|
case "video":
|
|
3970
3975
|
controlsHtml = `
|
|
3971
3976
|
<label class="fig-input-fill-label">Video</label>
|
|
3972
|
-
|
|
3973
|
-
<fig-input-number
|
|
3974
|
-
class="fig-input-fill-opacity"
|
|
3975
|
-
placeholder="##"
|
|
3976
|
-
min="0"
|
|
3977
|
-
max="100"
|
|
3978
|
-
value="${Math.round((this.#video.opacity ?? 1) * 100)}"
|
|
3979
|
-
units="%"
|
|
3980
|
-
${disabled ? "disabled" : ""}>
|
|
3981
|
-
</fig-input-number>
|
|
3982
|
-
</fig-tooltip>`;
|
|
3977
|
+
${opacityHtml(Math.round((this.#video.opacity ?? 1) * 100))}`;
|
|
3983
3978
|
break;
|
|
3984
3979
|
|
|
3985
3980
|
case "webcam":
|
|
3986
3981
|
controlsHtml = `
|
|
3987
3982
|
<label class="fig-input-fill-label">Webcam</label>
|
|
3988
|
-
|
|
3989
|
-
<fig-input-number
|
|
3990
|
-
class="fig-input-fill-opacity"
|
|
3991
|
-
placeholder="##"
|
|
3992
|
-
min="0"
|
|
3993
|
-
max="100"
|
|
3994
|
-
value="${Math.round((this.#webcam.opacity ?? 1) * 100)}"
|
|
3995
|
-
units="%"
|
|
3996
|
-
${disabled ? "disabled" : ""}>
|
|
3997
|
-
</fig-input-number>
|
|
3998
|
-
</fig-tooltip>`;
|
|
3983
|
+
${opacityHtml(Math.round((this.#webcam.opacity ?? 1) * 100))}`;
|
|
3999
3984
|
break;
|
|
4000
3985
|
}
|
|
4001
3986
|
|
|
@@ -5154,6 +5139,14 @@ class FigImage extends HTMLElement {
|
|
|
5154
5139
|
this.size = this.getAttribute("size") || "small";
|
|
5155
5140
|
this.innerHTML = this.#getInnerHTML();
|
|
5156
5141
|
this.#updateRefs();
|
|
5142
|
+
const ar = this.getAttribute("aspect-ratio");
|
|
5143
|
+
if (ar && ar !== "auto") {
|
|
5144
|
+
this.style.setProperty("--aspect-ratio", ar);
|
|
5145
|
+
}
|
|
5146
|
+
const fit = this.getAttribute("fit");
|
|
5147
|
+
if (fit) {
|
|
5148
|
+
this.style.setProperty("--fit", fit);
|
|
5149
|
+
}
|
|
5157
5150
|
}
|
|
5158
5151
|
disconnectedCallback() {
|
|
5159
5152
|
this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
|
|
@@ -5190,10 +5183,13 @@ class FigImage extends HTMLElement {
|
|
|
5190
5183
|
this.image.crossOrigin = "Anonymous";
|
|
5191
5184
|
this.image.onload = async () => {
|
|
5192
5185
|
this.aspectRatio = this.image.width / this.image.height;
|
|
5193
|
-
this.
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5186
|
+
const ar = this.getAttribute("aspect-ratio");
|
|
5187
|
+
if (!ar || ar === "auto") {
|
|
5188
|
+
this.style.setProperty(
|
|
5189
|
+
"--aspect-ratio",
|
|
5190
|
+
`${this.image.width}/${this.image.height}`
|
|
5191
|
+
);
|
|
5192
|
+
}
|
|
5197
5193
|
this.dispatchEvent(
|
|
5198
5194
|
new CustomEvent("loaded", {
|
|
5199
5195
|
bubbles: true,
|
|
@@ -5265,7 +5261,7 @@ class FigImage extends HTMLElement {
|
|
|
5265
5261
|
this.setAttribute("src", this.blob);
|
|
5266
5262
|
}
|
|
5267
5263
|
static get observedAttributes() {
|
|
5268
|
-
return ["src", "upload"];
|
|
5264
|
+
return ["src", "upload", "aspect-ratio", "fit"];
|
|
5269
5265
|
}
|
|
5270
5266
|
get src() {
|
|
5271
5267
|
return this.#src;
|
|
@@ -5297,10 +5293,665 @@ class FigImage extends HTMLElement {
|
|
|
5297
5293
|
if (name === "size") {
|
|
5298
5294
|
this.size = newValue;
|
|
5299
5295
|
}
|
|
5296
|
+
if (name === "aspect-ratio") {
|
|
5297
|
+
if (newValue && newValue !== "auto") {
|
|
5298
|
+
this.style.setProperty("--aspect-ratio", newValue);
|
|
5299
|
+
} else if (!newValue) {
|
|
5300
|
+
this.style.removeProperty("--aspect-ratio");
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
if (name === "fit") {
|
|
5304
|
+
if (newValue) {
|
|
5305
|
+
this.style.setProperty("--fit", newValue);
|
|
5306
|
+
} else {
|
|
5307
|
+
this.style.removeProperty("--fit");
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5300
5310
|
}
|
|
5301
5311
|
}
|
|
5302
5312
|
customElements.define("fig-image", FigImage);
|
|
5303
5313
|
|
|
5314
|
+
/**
|
|
5315
|
+
* A bezier / spring easing curve editor with draggable control points.
|
|
5316
|
+
* @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
|
|
5317
|
+
* @attr {number} precision - Decimal places for output values (default 2)
|
|
5318
|
+
* @attr {boolean} dropdown - Show a preset dropdown selector
|
|
5319
|
+
*/
|
|
5320
|
+
class FigEasingCurve extends HTMLElement {
|
|
5321
|
+
#cp1 = { x: 0.42, y: 0 };
|
|
5322
|
+
#cp2 = { x: 0.58, y: 1 };
|
|
5323
|
+
#spring = { stiffness: 200, damping: 15, mass: 1 };
|
|
5324
|
+
#mode = "bezier";
|
|
5325
|
+
#precision = 2;
|
|
5326
|
+
#isDragging = null;
|
|
5327
|
+
#svg = null;
|
|
5328
|
+
#curve = null;
|
|
5329
|
+
#line1 = null;
|
|
5330
|
+
#line2 = null;
|
|
5331
|
+
#handle1 = null;
|
|
5332
|
+
#handle2 = null;
|
|
5333
|
+
#dropdown = null;
|
|
5334
|
+
#presetName = null;
|
|
5335
|
+
#targetLine = null;
|
|
5336
|
+
#springDuration = 0.8;
|
|
5337
|
+
#drawWidth = 200;
|
|
5338
|
+
#drawHeight = 200;
|
|
5339
|
+
#bounds = null;
|
|
5340
|
+
#diagonal = null;
|
|
5341
|
+
#resizeObserver = null;
|
|
5342
|
+
|
|
5343
|
+
static PRESETS = [
|
|
5344
|
+
{ group: null, name: "Linear", type: "bezier", value: [0, 0, 1, 1] },
|
|
5345
|
+
{ group: "Bezier", name: "Ease in", type: "bezier", value: [0.42, 0, 1, 1] },
|
|
5346
|
+
{ group: "Bezier", name: "Ease out", type: "bezier", value: [0, 0, 0.58, 1] },
|
|
5347
|
+
{ group: "Bezier", name: "Ease in and out", type: "bezier", value: [0.42, 0, 0.58, 1] },
|
|
5348
|
+
{ group: "Bezier", name: "Ease in back", type: "bezier", value: [0.6, -0.28, 0.735, 0.045] },
|
|
5349
|
+
{ group: "Bezier", name: "Ease out back", type: "bezier", value: [0.175, 0.885, 0.32, 1.275] },
|
|
5350
|
+
{ group: "Bezier", name: "Ease in and out back", type: "bezier", value: [0.68, -0.55, 0.265, 1.55] },
|
|
5351
|
+
{ group: "Bezier", name: "Custom bezier", type: "bezier", value: null },
|
|
5352
|
+
{ group: "Spring", name: "Gentle", type: "spring", spring: { stiffness: 120, damping: 14, mass: 1 } },
|
|
5353
|
+
{ group: "Spring", name: "Quick", type: "spring", spring: { stiffness: 380, damping: 20, mass: 1 } },
|
|
5354
|
+
{ group: "Spring", name: "Bouncy", type: "spring", spring: { stiffness: 250, damping: 8, mass: 1 } },
|
|
5355
|
+
{ group: "Spring", name: "Slow", type: "spring", spring: { stiffness: 60, damping: 11, mass: 1 } },
|
|
5356
|
+
{ group: "Spring", name: "Custom spring", type: "spring", spring: null },
|
|
5357
|
+
];
|
|
5358
|
+
|
|
5359
|
+
static get observedAttributes() {
|
|
5360
|
+
return ["value", "precision"];
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
connectedCallback() {
|
|
5364
|
+
this.#precision = parseInt(this.getAttribute("precision") || "2");
|
|
5365
|
+
const val = this.getAttribute("value");
|
|
5366
|
+
if (val) this.#parseValue(val);
|
|
5367
|
+
this.#presetName = this.#matchPreset();
|
|
5368
|
+
this.#render();
|
|
5369
|
+
this.#setupResizeObserver();
|
|
5370
|
+
}
|
|
5371
|
+
|
|
5372
|
+
disconnectedCallback() {
|
|
5373
|
+
this.#isDragging = null;
|
|
5374
|
+
if (this.#resizeObserver) {
|
|
5375
|
+
this.#resizeObserver.disconnect();
|
|
5376
|
+
this.#resizeObserver = null;
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5380
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
5381
|
+
if (!this.#svg) return;
|
|
5382
|
+
if (name === "value" && newValue) {
|
|
5383
|
+
const prevMode = this.#mode;
|
|
5384
|
+
this.#parseValue(newValue);
|
|
5385
|
+
this.#presetName = this.#matchPreset();
|
|
5386
|
+
if (prevMode !== this.#mode) {
|
|
5387
|
+
this.#render();
|
|
5388
|
+
} else {
|
|
5389
|
+
this.#updatePaths();
|
|
5390
|
+
this.#syncDropdown();
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
if (name === "precision") {
|
|
5394
|
+
this.#precision = parseInt(newValue || "2");
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5398
|
+
get value() {
|
|
5399
|
+
if (this.#mode === "spring") {
|
|
5400
|
+
const { stiffness, damping, mass } = this.#spring;
|
|
5401
|
+
return `spring(${stiffness}, ${damping}, ${mass})`;
|
|
5402
|
+
}
|
|
5403
|
+
const p = this.#precision;
|
|
5404
|
+
return `${this.#cp1.x.toFixed(p)}, ${this.#cp1.y.toFixed(p)}, ${this.#cp2.x.toFixed(p)}, ${this.#cp2.y.toFixed(p)}`;
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
get cssValue() {
|
|
5408
|
+
if (this.#mode === "spring") {
|
|
5409
|
+
const points = this.#simulateSpring();
|
|
5410
|
+
const samples = 20;
|
|
5411
|
+
const step = Math.max(1, Math.floor(points.length / samples));
|
|
5412
|
+
const vals = [];
|
|
5413
|
+
for (let i = 0; i < points.length; i += step) {
|
|
5414
|
+
vals.push(points[i].value.toFixed(3));
|
|
5415
|
+
}
|
|
5416
|
+
if (points.length > 0) vals.push(points[points.length - 1].value.toFixed(3));
|
|
5417
|
+
return `linear(${vals.join(", ")})`;
|
|
5418
|
+
}
|
|
5419
|
+
const p = this.#precision;
|
|
5420
|
+
return `cubic-bezier(${this.#cp1.x.toFixed(p)}, ${this.#cp1.y.toFixed(p)}, ${this.#cp2.x.toFixed(p)}, ${this.#cp2.y.toFixed(p)})`;
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5423
|
+
get preset() {
|
|
5424
|
+
return this.#presetName;
|
|
5425
|
+
}
|
|
5426
|
+
|
|
5427
|
+
set value(v) {
|
|
5428
|
+
this.setAttribute("value", v);
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5431
|
+
#parseValue(str) {
|
|
5432
|
+
const springMatch = str.match(/^spring\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/);
|
|
5433
|
+
if (springMatch) {
|
|
5434
|
+
this.#mode = "spring";
|
|
5435
|
+
this.#spring.stiffness = parseFloat(springMatch[1]);
|
|
5436
|
+
this.#spring.damping = parseFloat(springMatch[2]);
|
|
5437
|
+
this.#spring.mass = parseFloat(springMatch[3]);
|
|
5438
|
+
return;
|
|
5439
|
+
}
|
|
5440
|
+
const parts = str.split(",").map((s) => parseFloat(s.trim()));
|
|
5441
|
+
if (parts.length >= 4 && parts.every((n) => !isNaN(n))) {
|
|
5442
|
+
this.#mode = "bezier";
|
|
5443
|
+
this.#cp1.x = parts[0];
|
|
5444
|
+
this.#cp1.y = parts[1];
|
|
5445
|
+
this.#cp2.x = parts[2];
|
|
5446
|
+
this.#cp2.y = parts[3];
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5449
|
+
|
|
5450
|
+
#matchPreset() {
|
|
5451
|
+
const ep = 0.001;
|
|
5452
|
+
if (this.#mode === "bezier") {
|
|
5453
|
+
for (const p of FigEasingCurve.PRESETS) {
|
|
5454
|
+
if (p.type !== "bezier" || !p.value) continue;
|
|
5455
|
+
if (
|
|
5456
|
+
Math.abs(this.#cp1.x - p.value[0]) < ep &&
|
|
5457
|
+
Math.abs(this.#cp1.y - p.value[1]) < ep &&
|
|
5458
|
+
Math.abs(this.#cp2.x - p.value[2]) < ep &&
|
|
5459
|
+
Math.abs(this.#cp2.y - p.value[3]) < ep
|
|
5460
|
+
) return p.name;
|
|
5461
|
+
}
|
|
5462
|
+
return "Custom bezier";
|
|
5463
|
+
}
|
|
5464
|
+
for (const p of FigEasingCurve.PRESETS) {
|
|
5465
|
+
if (p.type !== "spring" || !p.spring) continue;
|
|
5466
|
+
if (
|
|
5467
|
+
Math.abs(this.#spring.stiffness - p.spring.stiffness) < ep &&
|
|
5468
|
+
Math.abs(this.#spring.damping - p.spring.damping) < ep &&
|
|
5469
|
+
Math.abs(this.#spring.mass - p.spring.mass) < ep
|
|
5470
|
+
) return p.name;
|
|
5471
|
+
}
|
|
5472
|
+
return "Custom spring";
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
// --- Spring simulation ---
|
|
5476
|
+
|
|
5477
|
+
#simulateSpring() {
|
|
5478
|
+
const { stiffness, damping, mass } = this.#spring;
|
|
5479
|
+
const dt = 0.004;
|
|
5480
|
+
const maxTime = 5;
|
|
5481
|
+
const points = [];
|
|
5482
|
+
let pos = 0, vel = 0;
|
|
5483
|
+
for (let t = 0; t <= maxTime; t += dt) {
|
|
5484
|
+
const force = -stiffness * (pos - 1) - damping * vel;
|
|
5485
|
+
vel += (force / mass) * dt;
|
|
5486
|
+
pos += vel * dt;
|
|
5487
|
+
points.push({ t, value: pos });
|
|
5488
|
+
if (t > 0.1 && Math.abs(pos - 1) < 0.0005 && Math.abs(vel) < 0.0005) break;
|
|
5489
|
+
}
|
|
5490
|
+
return points;
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5493
|
+
static #springIcon(spring, size = 24) {
|
|
5494
|
+
const { stiffness, damping, mass } = spring;
|
|
5495
|
+
const dt = 0.004;
|
|
5496
|
+
const maxTime = 5;
|
|
5497
|
+
const pts = [];
|
|
5498
|
+
let pos = 0, vel = 0;
|
|
5499
|
+
for (let t = 0; t <= maxTime; t += dt) {
|
|
5500
|
+
const force = -stiffness * (pos - 1) - damping * vel;
|
|
5501
|
+
vel += (force / mass) * dt;
|
|
5502
|
+
pos += vel * dt;
|
|
5503
|
+
pts.push({ t, value: pos });
|
|
5504
|
+
if (t > 0.1 && Math.abs(pos - 1) < 0.001 && Math.abs(vel) < 0.001) break;
|
|
5505
|
+
}
|
|
5506
|
+
const totalTime = pts[pts.length - 1].t || 1;
|
|
5507
|
+
let maxVal = 1;
|
|
5508
|
+
for (const p of pts) if (p.value > maxVal) maxVal = p.value;
|
|
5509
|
+
let minVal = 0;
|
|
5510
|
+
for (const p of pts) if (p.value < minVal) minVal = p.value;
|
|
5511
|
+
const range = Math.max(maxVal - minVal, 1);
|
|
5512
|
+
const pad = 6;
|
|
5513
|
+
const s = size - pad * 2;
|
|
5514
|
+
const step = Math.max(1, Math.floor(pts.length / 30));
|
|
5515
|
+
let d = "";
|
|
5516
|
+
for (let i = 0; i < pts.length; i += step) {
|
|
5517
|
+
const x = pad + (pts[i].t / totalTime) * s;
|
|
5518
|
+
const y = pad + (1 - (pts[i].value - minVal) / range) * s;
|
|
5519
|
+
d += (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
|
|
5520
|
+
}
|
|
5521
|
+
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>`;
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
static curveIcon(cp1x, cp1y, cp2x, cp2y, size = 24) {
|
|
5525
|
+
const pad = 6;
|
|
5526
|
+
const s = size - pad * 2;
|
|
5527
|
+
const x = (n) => pad + n * s;
|
|
5528
|
+
const y = (n) => pad + (1 - n) * s;
|
|
5529
|
+
const d = `M${x(0)},${y(0)} C${x(cp1x)},${y(cp1y)} ${x(cp2x)},${y(cp2y)} ${x(1)},${y(1)}`;
|
|
5530
|
+
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>`;
|
|
5531
|
+
}
|
|
5532
|
+
|
|
5533
|
+
// --- Rendering ---
|
|
5534
|
+
|
|
5535
|
+
#render() {
|
|
5536
|
+
this.innerHTML = this.#getInnerHTML();
|
|
5537
|
+
this.#cacheRefs();
|
|
5538
|
+
this.#syncViewportSize();
|
|
5539
|
+
this.#updatePaths();
|
|
5540
|
+
this.#setupEvents();
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
#getDropdownHTML() {
|
|
5544
|
+
if (this.getAttribute("dropdown") !== "true") return "";
|
|
5545
|
+
let optionsHTML = "";
|
|
5546
|
+
let currentGroup = undefined;
|
|
5547
|
+
for (const p of FigEasingCurve.PRESETS) {
|
|
5548
|
+
if (p.group !== currentGroup) {
|
|
5549
|
+
if (currentGroup !== undefined) optionsHTML += `</optgroup>`;
|
|
5550
|
+
if (p.group) optionsHTML += `<optgroup label="${p.group}">`;
|
|
5551
|
+
currentGroup = p.group;
|
|
5552
|
+
}
|
|
5553
|
+
let icon;
|
|
5554
|
+
if (p.type === "spring") {
|
|
5555
|
+
const sp = p.spring || this.#spring;
|
|
5556
|
+
icon = FigEasingCurve.#springIcon(sp);
|
|
5557
|
+
} else {
|
|
5558
|
+
const v = p.value || [this.#cp1.x, this.#cp1.y, this.#cp2.x, this.#cp2.y];
|
|
5559
|
+
icon = FigEasingCurve.curveIcon(...v);
|
|
5560
|
+
}
|
|
5561
|
+
const selected = p.name === this.#presetName ? " selected" : "";
|
|
5562
|
+
optionsHTML += `<option value="${p.name}"${selected}>${icon} ${p.name}</option>`;
|
|
5563
|
+
}
|
|
5564
|
+
if (currentGroup) optionsHTML += `</optgroup>`;
|
|
5565
|
+
return `<fig-dropdown class="fig-easing-curve-dropdown" full experimental="modern">${optionsHTML}</fig-dropdown>`;
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
#getInnerHTML() {
|
|
5569
|
+
const size = 200;
|
|
5570
|
+
const dropdown = this.#getDropdownHTML();
|
|
5571
|
+
|
|
5572
|
+
if (this.#mode === "spring") {
|
|
5573
|
+
const targetY = 40;
|
|
5574
|
+
const startY = 180;
|
|
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-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
|
|
5578
|
+
<line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
|
|
5579
|
+
<path class="fig-easing-curve-path"/>
|
|
5580
|
+
<circle class="fig-easing-curve-handle" data-handle="bounce" r="5.25"/>
|
|
5581
|
+
<rect class="fig-easing-curve-duration-bar" data-handle="duration" width="6" height="16" rx="3" ry="3"/>
|
|
5582
|
+
</svg></div>`;
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5585
|
+
return `${dropdown}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
5586
|
+
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
5587
|
+
<line class="fig-easing-curve-diagonal" x1="0" y1="${size}" x2="${size}" y2="0"/>
|
|
5588
|
+
<line class="fig-easing-curve-arm" data-arm="1"/>
|
|
5589
|
+
<line class="fig-easing-curve-arm" data-arm="2"/>
|
|
5590
|
+
<path class="fig-easing-curve-path"/>
|
|
5591
|
+
<circle class="fig-easing-curve-handle" data-handle="1" r="5.25"/>
|
|
5592
|
+
<circle class="fig-easing-curve-handle" data-handle="2" r="5.25"/>
|
|
5593
|
+
</svg></div>`;
|
|
5594
|
+
}
|
|
5595
|
+
|
|
5596
|
+
#cacheRefs() {
|
|
5597
|
+
this.#svg = this.querySelector(".fig-easing-curve-svg");
|
|
5598
|
+
this.#curve = this.querySelector(".fig-easing-curve-path");
|
|
5599
|
+
this.#line1 = this.querySelector('[data-arm="1"]');
|
|
5600
|
+
this.#line2 = this.querySelector('[data-arm="2"]');
|
|
5601
|
+
this.#handle1 = this.querySelector('[data-handle="1"]') || this.querySelector('[data-handle="bounce"]');
|
|
5602
|
+
this.#handle2 = this.querySelector('[data-handle="2"]') || this.querySelector('[data-handle="duration"]');
|
|
5603
|
+
this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
|
|
5604
|
+
this.#targetLine = this.querySelector(".fig-easing-curve-target");
|
|
5605
|
+
this.#bounds = this.querySelector(".fig-easing-curve-bounds");
|
|
5606
|
+
this.#diagonal = this.querySelector(".fig-easing-curve-diagonal");
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5609
|
+
#setupResizeObserver() {
|
|
5610
|
+
if (this.#resizeObserver || !window.ResizeObserver) return;
|
|
5611
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
5612
|
+
if (this.#syncViewportSize()) {
|
|
5613
|
+
this.#updatePaths();
|
|
5614
|
+
}
|
|
5615
|
+
});
|
|
5616
|
+
this.#resizeObserver.observe(this);
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5619
|
+
#syncViewportSize() {
|
|
5620
|
+
if (!this.#svg) return false;
|
|
5621
|
+
const rect = this.#svg.getBoundingClientRect();
|
|
5622
|
+
const width = Math.max(1, Math.round(rect.width || 200));
|
|
5623
|
+
const height = Math.max(1, Math.round(rect.height || 200));
|
|
5624
|
+
const changed = width !== this.#drawWidth || height !== this.#drawHeight;
|
|
5625
|
+
this.#drawWidth = width;
|
|
5626
|
+
this.#drawHeight = height;
|
|
5627
|
+
this.#svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
5628
|
+
return changed;
|
|
5629
|
+
}
|
|
5630
|
+
|
|
5631
|
+
// --- Coordinate helpers ---
|
|
5632
|
+
|
|
5633
|
+
#toSVG(nx, ny) {
|
|
5634
|
+
return { x: nx * this.#drawWidth, y: (1 - ny) * this.#drawHeight };
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
#fromSVG(sx, sy) {
|
|
5638
|
+
return { x: sx / this.#drawWidth, y: 1 - sy / this.#drawHeight };
|
|
5639
|
+
}
|
|
5640
|
+
|
|
5641
|
+
#springScale = { minVal: 0, maxVal: 1.2, totalTime: 1 };
|
|
5642
|
+
|
|
5643
|
+
#springToSVG(nt, nv) {
|
|
5644
|
+
const pad = 20;
|
|
5645
|
+
const draw = this.#drawHeight - pad * 2;
|
|
5646
|
+
const { minVal, maxVal } = this.#springScale;
|
|
5647
|
+
const range = maxVal - minVal || 1;
|
|
5648
|
+
return {
|
|
5649
|
+
x: nt * this.#drawWidth,
|
|
5650
|
+
y: pad + (1 - (nv - minVal) / range) * draw,
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
|
|
5654
|
+
// --- Path updates ---
|
|
5655
|
+
|
|
5656
|
+
#updatePaths() {
|
|
5657
|
+
this.#syncViewportSize();
|
|
5658
|
+
if (this.#mode === "spring") {
|
|
5659
|
+
this.#updateSpringPaths();
|
|
5660
|
+
} else {
|
|
5661
|
+
this.#updateBezierPaths();
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
|
|
5665
|
+
#updateBezierPaths() {
|
|
5666
|
+
if (this.#bounds) {
|
|
5667
|
+
this.#bounds.setAttribute("x", "0");
|
|
5668
|
+
this.#bounds.setAttribute("y", "0");
|
|
5669
|
+
this.#bounds.setAttribute("width", this.#drawWidth);
|
|
5670
|
+
this.#bounds.setAttribute("height", this.#drawHeight);
|
|
5671
|
+
}
|
|
5672
|
+
if (this.#diagonal) {
|
|
5673
|
+
this.#diagonal.setAttribute("x1", "0");
|
|
5674
|
+
this.#diagonal.setAttribute("y1", this.#drawHeight);
|
|
5675
|
+
this.#diagonal.setAttribute("x2", this.#drawWidth);
|
|
5676
|
+
this.#diagonal.setAttribute("y2", "0");
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
const p0 = this.#toSVG(0, 0);
|
|
5680
|
+
const p1 = this.#toSVG(this.#cp1.x, this.#cp1.y);
|
|
5681
|
+
const p2 = this.#toSVG(this.#cp2.x, this.#cp2.y);
|
|
5682
|
+
const p3 = this.#toSVG(1, 1);
|
|
5683
|
+
|
|
5684
|
+
this.#curve.setAttribute("d", `M${p0.x},${p0.y} C${p1.x},${p1.y} ${p2.x},${p2.y} ${p3.x},${p3.y}`);
|
|
5685
|
+
this.#line1.setAttribute("x1", p0.x);
|
|
5686
|
+
this.#line1.setAttribute("y1", p0.y);
|
|
5687
|
+
this.#line1.setAttribute("x2", p1.x);
|
|
5688
|
+
this.#line1.setAttribute("y2", p1.y);
|
|
5689
|
+
this.#line2.setAttribute("x1", p3.x);
|
|
5690
|
+
this.#line2.setAttribute("y1", p3.y);
|
|
5691
|
+
this.#line2.setAttribute("x2", p2.x);
|
|
5692
|
+
this.#line2.setAttribute("y2", p2.y);
|
|
5693
|
+
this.#handle1.setAttribute("cx", p1.x);
|
|
5694
|
+
this.#handle1.setAttribute("cy", p1.y);
|
|
5695
|
+
this.#handle2.setAttribute("cx", p2.x);
|
|
5696
|
+
this.#handle2.setAttribute("cy", p2.y);
|
|
5697
|
+
}
|
|
5698
|
+
|
|
5699
|
+
#updateSpringPaths() {
|
|
5700
|
+
if (this.#bounds) {
|
|
5701
|
+
this.#bounds.setAttribute("x", "0");
|
|
5702
|
+
this.#bounds.setAttribute("y", "0");
|
|
5703
|
+
this.#bounds.setAttribute("width", this.#drawWidth);
|
|
5704
|
+
this.#bounds.setAttribute("height", this.#drawHeight);
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
const points = this.#simulateSpring();
|
|
5708
|
+
if (!points.length) return;
|
|
5709
|
+
const totalTime = points[points.length - 1].t || 1;
|
|
5710
|
+
|
|
5711
|
+
let minVal = 0, maxVal = 1;
|
|
5712
|
+
for (const p of points) {
|
|
5713
|
+
if (p.value < minVal) minVal = p.value;
|
|
5714
|
+
if (p.value > maxVal) maxVal = p.value;
|
|
5715
|
+
}
|
|
5716
|
+
const maxDistFromCenter = Math.max(Math.abs(minVal - 1), Math.abs(maxVal - 1), 0.01);
|
|
5717
|
+
const valPad = 0;
|
|
5718
|
+
this.#springScale = {
|
|
5719
|
+
minVal: 1 - maxDistFromCenter - valPad,
|
|
5720
|
+
maxVal: 1 + maxDistFromCenter + valPad,
|
|
5721
|
+
totalTime,
|
|
5722
|
+
};
|
|
5723
|
+
|
|
5724
|
+
const durationNorm = Math.max(0.05, Math.min(0.95, this.#springDuration));
|
|
5725
|
+
let d = "";
|
|
5726
|
+
for (let i = 0; i < points.length; i++) {
|
|
5727
|
+
const nt = (points[i].t / totalTime) * durationNorm;
|
|
5728
|
+
const pt = this.#springToSVG(nt, points[i].value);
|
|
5729
|
+
d += (i === 0 ? "M" : "L") + pt.x.toFixed(1) + "," + pt.y.toFixed(1);
|
|
5730
|
+
}
|
|
5731
|
+
const flatStart = this.#springToSVG(durationNorm, 1);
|
|
5732
|
+
const flatEnd = this.#springToSVG(1, 1);
|
|
5733
|
+
d += `L${flatStart.x.toFixed(1)},${flatStart.y.toFixed(1)} L${flatEnd.x.toFixed(1)},${flatEnd.y.toFixed(1)}`;
|
|
5734
|
+
this.#curve.setAttribute("d", d);
|
|
5735
|
+
|
|
5736
|
+
// Update target line position at value=1
|
|
5737
|
+
if (this.#targetLine) {
|
|
5738
|
+
const tl = this.#springToSVG(0, 1);
|
|
5739
|
+
const tr = this.#springToSVG(1, 1);
|
|
5740
|
+
this.#targetLine.setAttribute("x1", tl.x);
|
|
5741
|
+
this.#targetLine.setAttribute("y1", tl.y);
|
|
5742
|
+
this.#targetLine.setAttribute("x2", tr.x);
|
|
5743
|
+
this.#targetLine.setAttribute("y2", tr.y);
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5746
|
+
// Bounce handle: at first overshoot peak
|
|
5747
|
+
const peak = this.#findPeakOvershoot(points);
|
|
5748
|
+
const peakNorm = (peak.t / totalTime) * durationNorm;
|
|
5749
|
+
const peakPt = this.#springToSVG(peakNorm, peak.value);
|
|
5750
|
+
this.#handle1.setAttribute("cx", peakPt.x);
|
|
5751
|
+
this.#handle1.setAttribute("cy", peakPt.y);
|
|
5752
|
+
|
|
5753
|
+
// Duration handle: on the target line
|
|
5754
|
+
const targetPt = this.#springToSVG(durationNorm, 1);
|
|
5755
|
+
this.#handle2.setAttribute("x", targetPt.x - 3);
|
|
5756
|
+
this.#handle2.setAttribute("y", targetPt.y - 8);
|
|
5757
|
+
|
|
5758
|
+
}
|
|
5759
|
+
|
|
5760
|
+
#findPeakOvershoot(points) {
|
|
5761
|
+
let peak = { t: 0, value: 1 };
|
|
5762
|
+
let passedTarget = false;
|
|
5763
|
+
for (const p of points) {
|
|
5764
|
+
if (p.value >= 0.99) passedTarget = true;
|
|
5765
|
+
if (passedTarget && p.value > peak.value) {
|
|
5766
|
+
peak = { t: p.t, value: p.value };
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
return peak;
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5772
|
+
// --- Dropdown ---
|
|
5773
|
+
|
|
5774
|
+
#syncDropdown() {
|
|
5775
|
+
if (!this.#dropdown) return;
|
|
5776
|
+
this.#dropdown.value = this.#presetName;
|
|
5777
|
+
this.#refreshCustomPresetIcons();
|
|
5778
|
+
}
|
|
5779
|
+
|
|
5780
|
+
#setOptionIconByValue(root, optionValue, icon) {
|
|
5781
|
+
if (!root) return;
|
|
5782
|
+
for (const option of root.querySelectorAll("option")) {
|
|
5783
|
+
if (option.value === optionValue) {
|
|
5784
|
+
option.innerHTML = `${icon} ${optionValue}`;
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
#refreshCustomPresetIcons() {
|
|
5790
|
+
if (!this.#dropdown) return;
|
|
5791
|
+
const bezierIcon = FigEasingCurve.curveIcon(
|
|
5792
|
+
this.#cp1.x,
|
|
5793
|
+
this.#cp1.y,
|
|
5794
|
+
this.#cp2.x,
|
|
5795
|
+
this.#cp2.y
|
|
5796
|
+
);
|
|
5797
|
+
const springIcon = FigEasingCurve.#springIcon(this.#spring);
|
|
5798
|
+
|
|
5799
|
+
// Update both slotted options and the cloned native select options.
|
|
5800
|
+
this.#setOptionIconByValue(this.#dropdown, "Custom bezier", bezierIcon);
|
|
5801
|
+
this.#setOptionIconByValue(this.#dropdown, "Custom spring", springIcon);
|
|
5802
|
+
this.#setOptionIconByValue(this.#dropdown.select, "Custom bezier", bezierIcon);
|
|
5803
|
+
this.#setOptionIconByValue(this.#dropdown.select, "Custom spring", springIcon);
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
// --- Events ---
|
|
5807
|
+
|
|
5808
|
+
#emit(type) {
|
|
5809
|
+
this.dispatchEvent(new CustomEvent(type, {
|
|
5810
|
+
bubbles: true,
|
|
5811
|
+
detail: {
|
|
5812
|
+
mode: this.#mode,
|
|
5813
|
+
value: this.value,
|
|
5814
|
+
cssValue: this.cssValue,
|
|
5815
|
+
preset: this.#presetName,
|
|
5816
|
+
},
|
|
5817
|
+
}));
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5820
|
+
#setupEvents() {
|
|
5821
|
+
if (this.#mode === "bezier") {
|
|
5822
|
+
this.#handle1.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 1));
|
|
5823
|
+
this.#handle2.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 2));
|
|
5824
|
+
} else {
|
|
5825
|
+
this.#handle1.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "bounce"));
|
|
5826
|
+
this.#handle2.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "duration"));
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5829
|
+
if (this.#dropdown) {
|
|
5830
|
+
this.#dropdown.addEventListener("change", (e) => {
|
|
5831
|
+
const name = e.detail;
|
|
5832
|
+
const preset = FigEasingCurve.PRESETS.find((p) => p.name === name);
|
|
5833
|
+
if (!preset) return;
|
|
5834
|
+
|
|
5835
|
+
if (preset.type === "bezier") {
|
|
5836
|
+
if (preset.value) {
|
|
5837
|
+
this.#cp1.x = preset.value[0];
|
|
5838
|
+
this.#cp1.y = preset.value[1];
|
|
5839
|
+
this.#cp2.x = preset.value[2];
|
|
5840
|
+
this.#cp2.y = preset.value[3];
|
|
5841
|
+
}
|
|
5842
|
+
this.#presetName = name;
|
|
5843
|
+
if (this.#mode !== "bezier") {
|
|
5844
|
+
this.#mode = "bezier";
|
|
5845
|
+
this.#render();
|
|
5846
|
+
} else {
|
|
5847
|
+
this.#updatePaths();
|
|
5848
|
+
}
|
|
5849
|
+
} else if (preset.type === "spring") {
|
|
5850
|
+
if (preset.spring) {
|
|
5851
|
+
this.#spring = { ...preset.spring };
|
|
5852
|
+
}
|
|
5853
|
+
this.#presetName = name;
|
|
5854
|
+
if (this.#mode !== "spring") {
|
|
5855
|
+
this.#mode = "spring";
|
|
5856
|
+
this.#render();
|
|
5857
|
+
} else {
|
|
5858
|
+
this.#updatePaths();
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
this.#emit("input");
|
|
5862
|
+
this.#emit("change");
|
|
5863
|
+
});
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
#clientToSVG(e) {
|
|
5868
|
+
const ctm = this.#svg.getScreenCTM();
|
|
5869
|
+
if (!ctm) return { x: 0, y: 0 };
|
|
5870
|
+
const inv = ctm.inverse();
|
|
5871
|
+
return {
|
|
5872
|
+
x: inv.a * e.clientX + inv.c * e.clientY + inv.e,
|
|
5873
|
+
y: inv.b * e.clientX + inv.d * e.clientY + inv.f,
|
|
5874
|
+
};
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5877
|
+
#startBezierDrag(e, handle) {
|
|
5878
|
+
e.preventDefault();
|
|
5879
|
+
this.#isDragging = handle;
|
|
5880
|
+
|
|
5881
|
+
const onMove = (e) => {
|
|
5882
|
+
if (!this.#isDragging) return;
|
|
5883
|
+
const svgPt = this.#clientToSVG(e);
|
|
5884
|
+
const norm = this.#fromSVG(svgPt.x, svgPt.y);
|
|
5885
|
+
|
|
5886
|
+
norm.x = Math.round(norm.x * 100) / 100;
|
|
5887
|
+
norm.y = Math.round(norm.y * 100) / 100;
|
|
5888
|
+
norm.x = Math.max(0, Math.min(1, norm.x));
|
|
5889
|
+
|
|
5890
|
+
if (this.#isDragging === 1) {
|
|
5891
|
+
this.#cp1.x = norm.x;
|
|
5892
|
+
this.#cp1.y = norm.y;
|
|
5893
|
+
} else {
|
|
5894
|
+
this.#cp2.x = norm.x;
|
|
5895
|
+
this.#cp2.y = norm.y;
|
|
5896
|
+
}
|
|
5897
|
+
this.#updatePaths();
|
|
5898
|
+
this.#presetName = this.#matchPreset();
|
|
5899
|
+
this.#syncDropdown();
|
|
5900
|
+
this.#emit("input");
|
|
5901
|
+
};
|
|
5902
|
+
|
|
5903
|
+
const onUp = () => {
|
|
5904
|
+
this.#isDragging = null;
|
|
5905
|
+
document.removeEventListener("pointermove", onMove);
|
|
5906
|
+
document.removeEventListener("pointerup", onUp);
|
|
5907
|
+
this.#emit("change");
|
|
5908
|
+
};
|
|
5909
|
+
|
|
5910
|
+
document.addEventListener("pointermove", onMove);
|
|
5911
|
+
document.addEventListener("pointerup", onUp);
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
#startSpringDrag(e, handleType) {
|
|
5915
|
+
e.preventDefault();
|
|
5916
|
+
this.#isDragging = handleType;
|
|
5917
|
+
|
|
5918
|
+
const startDamping = this.#spring.damping;
|
|
5919
|
+
const startStiffness = this.#spring.stiffness;
|
|
5920
|
+
const startDuration = this.#springDuration;
|
|
5921
|
+
const startY = e.clientY;
|
|
5922
|
+
const startX = e.clientX;
|
|
5923
|
+
|
|
5924
|
+
const onMove = (e) => {
|
|
5925
|
+
if (!this.#isDragging) return;
|
|
5926
|
+
|
|
5927
|
+
if (handleType === "bounce") {
|
|
5928
|
+
const dy = e.clientY - startY;
|
|
5929
|
+
this.#spring.damping = Math.max(1, Math.round(startDamping + dy * 0.15));
|
|
5930
|
+
} else {
|
|
5931
|
+
const dx = e.clientX - startX;
|
|
5932
|
+
this.#springDuration = Math.max(0.05, Math.min(0.95, startDuration + dx / 200));
|
|
5933
|
+
this.#spring.stiffness = Math.max(10, Math.round(startStiffness - dx * 1.5));
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5936
|
+
this.#updatePaths();
|
|
5937
|
+
this.#presetName = this.#matchPreset();
|
|
5938
|
+
this.#syncDropdown();
|
|
5939
|
+
this.#emit("input");
|
|
5940
|
+
};
|
|
5941
|
+
|
|
5942
|
+
const onUp = () => {
|
|
5943
|
+
this.#isDragging = null;
|
|
5944
|
+
document.removeEventListener("pointermove", onMove);
|
|
5945
|
+
document.removeEventListener("pointerup", onUp);
|
|
5946
|
+
this.#emit("change");
|
|
5947
|
+
};
|
|
5948
|
+
|
|
5949
|
+
document.addEventListener("pointermove", onMove);
|
|
5950
|
+
document.addEventListener("pointerup", onUp);
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
customElements.define("fig-easing-curve", FigEasingCurve);
|
|
5954
|
+
|
|
5304
5955
|
/**
|
|
5305
5956
|
* A custom joystick input element.
|
|
5306
5957
|
* @attr {string} value - The current position of the joystick (e.g., "0.5,0.5").
|
package/index.html
CHANGED
|
@@ -4339,6 +4339,32 @@
|
|
|
4339
4339
|
<fig-button>No delay</fig-button>
|
|
4340
4340
|
</fig-tooltip>
|
|
4341
4341
|
|
|
4342
|
+
<h3>Warmup (Rapid Hover)</h3>
|
|
4343
|
+
<p class="description">After the first tooltip appears, moving quickly between these buttons shows
|
|
4344
|
+
subsequent tooltips instantly — no repeated delay.</p>
|
|
4345
|
+
<hstack>
|
|
4346
|
+
<fig-tooltip text="Add" delay="500">
|
|
4347
|
+
<fig-button icon variant="ghost">
|
|
4348
|
+
<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>
|
|
4349
|
+
</fig-button>
|
|
4350
|
+
</fig-tooltip>
|
|
4351
|
+
<fig-tooltip text="Remove" delay="500">
|
|
4352
|
+
<fig-button icon variant="ghost">
|
|
4353
|
+
<span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>
|
|
4354
|
+
</fig-button>
|
|
4355
|
+
</fig-tooltip>
|
|
4356
|
+
<fig-tooltip text="Rotate" delay="500">
|
|
4357
|
+
<fig-button icon variant="ghost">
|
|
4358
|
+
<span class="fig-mask-icon" style="--icon: var(--icon-rotate)"></span>
|
|
4359
|
+
</fig-button>
|
|
4360
|
+
</fig-tooltip>
|
|
4361
|
+
<fig-tooltip text="Swap" delay="500">
|
|
4362
|
+
<fig-button icon variant="ghost">
|
|
4363
|
+
<span class="fig-mask-icon" style="--icon: var(--icon-swap)"></span>
|
|
4364
|
+
</fig-button>
|
|
4365
|
+
</fig-tooltip>
|
|
4366
|
+
</hstack>
|
|
4367
|
+
|
|
4342
4368
|
<h3>Long Text</h3>
|
|
4343
4369
|
<fig-tooltip text="This is a much longer tooltip that contains more detailed information about the element">
|
|
4344
4370
|
<fig-button variant="secondary">Long tooltip</fig-button>
|
package/package.json
CHANGED