@rogieking/figui3 2.25.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 +88 -8
- package/fig.js +703 -62
- 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,75 @@ 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
|
+
}
|
|
1284
|
+
.fig-easing-curve-diagonal,
|
|
1285
|
+
.fig-easing-curve-bounds,
|
|
1286
|
+
.fig-easing-curve-arm,
|
|
1287
|
+
.fig-easing-curve-path,
|
|
1288
|
+
.fig-easing-curve-target {
|
|
1289
|
+
pointer-events: none;
|
|
1290
|
+
}
|
|
1291
|
+
.fig-easing-curve-diagonal {
|
|
1292
|
+
stroke: var(--figma-color-bordertranslucent);
|
|
1293
|
+
stroke-width: var(--stroke-width);
|
|
1294
|
+
}
|
|
1295
|
+
.fig-easing-curve-bounds {
|
|
1296
|
+
fill: transparent;
|
|
1297
|
+
}
|
|
1298
|
+
.fig-easing-curve-arm {
|
|
1299
|
+
stroke: var(--figma-color-border-strong);
|
|
1300
|
+
stroke-width: var(--stroke-width);
|
|
1301
|
+
}
|
|
1302
|
+
.fig-easing-curve-path {
|
|
1303
|
+
fill: none;
|
|
1304
|
+
stroke: var(--figma-color-text);
|
|
1305
|
+
stroke-width: var(--stroke-width);
|
|
1306
|
+
}
|
|
1307
|
+
.fig-easing-curve-target {
|
|
1308
|
+
stroke: var(--figma-color-bordertranslucent);
|
|
1309
|
+
stroke-width: var(--stroke-width);
|
|
1310
|
+
}
|
|
1311
|
+
.fig-easing-curve-handle {
|
|
1312
|
+
fill: var(--figma-color-border-strong);
|
|
1313
|
+
cursor: grab;
|
|
1314
|
+
pointer-events: all;
|
|
1315
|
+
stroke: var(--figma-color-bg-secondary);
|
|
1316
|
+
stroke-width: var(--stroke-width);
|
|
1317
|
+
&:active {
|
|
1318
|
+
cursor: grabbing;
|
|
1319
|
+
fill: var(--figma-color-bg-brand);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
.fig-easing-curve-duration-bar {
|
|
1323
|
+
fill: var(--figma-color-border-strong);
|
|
1324
|
+
stroke: var(--figma-color-bg-secondary);
|
|
1325
|
+
stroke-width: var(--stroke-width);
|
|
1326
|
+
cursor: ew-resize;
|
|
1327
|
+
pointer-events: all;
|
|
1328
|
+
}
|
|
1329
|
+
.fig-easing-curve-dropdown {
|
|
1330
|
+
option svg {
|
|
1331
|
+
vertical-align: middle;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1256
1336
|
/* Combo input */
|
|
1257
1337
|
.input-combo {
|
|
1258
1338
|
display: inline-flex;
|
|
@@ -2497,7 +2577,7 @@ fig-input-fill {
|
|
|
2497
2577
|
|
|
2498
2578
|
fig-input-number {
|
|
2499
2579
|
flex: 0;
|
|
2500
|
-
flex-basis:
|
|
2580
|
+
flex-basis: 3rem;
|
|
2501
2581
|
}
|
|
2502
2582
|
|
|
2503
2583
|
&[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() {
|
|
@@ -3830,7 +3837,7 @@ class FigInputFill extends HTMLElement {
|
|
|
3830
3837
|
}
|
|
3831
3838
|
|
|
3832
3839
|
static get observedAttributes() {
|
|
3833
|
-
return ["value", "disabled", "mode", "experimental"];
|
|
3840
|
+
return ["value", "disabled", "mode", "experimental", "alpha"];
|
|
3834
3841
|
}
|
|
3835
3842
|
|
|
3836
3843
|
connectedCallback() {
|
|
@@ -3890,6 +3897,8 @@ class FigInputFill extends HTMLElement {
|
|
|
3890
3897
|
if (mode) attrs["mode"] = mode;
|
|
3891
3898
|
const experimental = this.getAttribute("experimental");
|
|
3892
3899
|
if (experimental) attrs["experimental"] = experimental;
|
|
3900
|
+
const alpha = this.getAttribute("alpha");
|
|
3901
|
+
if (alpha) attrs["alpha"] = alpha;
|
|
3893
3902
|
// picker-* overrides (except anchor, handled programmatically)
|
|
3894
3903
|
for (const { name, value } of this.attributes) {
|
|
3895
3904
|
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
@@ -3905,6 +3914,22 @@ class FigInputFill extends HTMLElement {
|
|
|
3905
3914
|
#render() {
|
|
3906
3915
|
const disabled = this.hasAttribute("disabled");
|
|
3907
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
|
+
: "";
|
|
3908
3933
|
|
|
3909
3934
|
let controlsHtml = "";
|
|
3910
3935
|
|
|
@@ -3918,17 +3943,7 @@ class FigInputFill extends HTMLElement {
|
|
|
3918
3943
|
value="${this.#solid.color.slice(1).toUpperCase()}"
|
|
3919
3944
|
${disabled ? "disabled" : ""}>
|
|
3920
3945
|
</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>`;
|
|
3946
|
+
${opacityHtml(Math.round(this.#solid.alpha * 100))}`;
|
|
3932
3947
|
break;
|
|
3933
3948
|
|
|
3934
3949
|
case "gradient":
|
|
@@ -3937,65 +3952,25 @@ class FigInputFill extends HTMLElement {
|
|
|
3937
3952
|
this.#gradient.type.slice(1);
|
|
3938
3953
|
controlsHtml = `
|
|
3939
3954
|
<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>`;
|
|
3955
|
+
${opacityHtml(this.#gradient.stops[0]?.opacity ?? 100)}`;
|
|
3951
3956
|
break;
|
|
3952
3957
|
|
|
3953
3958
|
case "image":
|
|
3954
3959
|
controlsHtml = `
|
|
3955
3960
|
<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>`;
|
|
3961
|
+
${opacityHtml(Math.round((this.#image.opacity ?? 1) * 100))}`;
|
|
3967
3962
|
break;
|
|
3968
3963
|
|
|
3969
3964
|
case "video":
|
|
3970
3965
|
controlsHtml = `
|
|
3971
3966
|
<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>`;
|
|
3967
|
+
${opacityHtml(Math.round((this.#video.opacity ?? 1) * 100))}`;
|
|
3983
3968
|
break;
|
|
3984
3969
|
|
|
3985
3970
|
case "webcam":
|
|
3986
3971
|
controlsHtml = `
|
|
3987
3972
|
<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>`;
|
|
3973
|
+
${opacityHtml(Math.round((this.#webcam.opacity ?? 1) * 100))}`;
|
|
3999
3974
|
break;
|
|
4000
3975
|
}
|
|
4001
3976
|
|
|
@@ -5154,6 +5129,14 @@ class FigImage extends HTMLElement {
|
|
|
5154
5129
|
this.size = this.getAttribute("size") || "small";
|
|
5155
5130
|
this.innerHTML = this.#getInnerHTML();
|
|
5156
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
|
+
}
|
|
5157
5140
|
}
|
|
5158
5141
|
disconnectedCallback() {
|
|
5159
5142
|
this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
|
|
@@ -5190,10 +5173,13 @@ class FigImage extends HTMLElement {
|
|
|
5190
5173
|
this.image.crossOrigin = "Anonymous";
|
|
5191
5174
|
this.image.onload = async () => {
|
|
5192
5175
|
this.aspectRatio = this.image.width / this.image.height;
|
|
5193
|
-
this.
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
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
|
+
}
|
|
5197
5183
|
this.dispatchEvent(
|
|
5198
5184
|
new CustomEvent("loaded", {
|
|
5199
5185
|
bubbles: true,
|
|
@@ -5265,7 +5251,7 @@ class FigImage extends HTMLElement {
|
|
|
5265
5251
|
this.setAttribute("src", this.blob);
|
|
5266
5252
|
}
|
|
5267
5253
|
static get observedAttributes() {
|
|
5268
|
-
return ["src", "upload"];
|
|
5254
|
+
return ["src", "upload", "aspect-ratio", "fit"];
|
|
5269
5255
|
}
|
|
5270
5256
|
get src() {
|
|
5271
5257
|
return this.#src;
|
|
@@ -5297,10 +5283,665 @@ class FigImage extends HTMLElement {
|
|
|
5297
5283
|
if (name === "size") {
|
|
5298
5284
|
this.size = newValue;
|
|
5299
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
|
+
}
|
|
5300
5300
|
}
|
|
5301
5301
|
}
|
|
5302
5302
|
customElements.define("fig-image", FigImage);
|
|
5303
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
|
+
|
|
5304
5945
|
/**
|
|
5305
5946
|
* A custom joystick input element.
|
|
5306
5947
|
* @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