@rogieking/figui3 9.0.14 → 9.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/skills/fig-lab/SKILL.md +2 -2
- package/.cursor/skills/fig-lab/components.md +0 -13
- package/.cursor/skills/fig-lab/reference.md +0 -6
- package/.cursor/skills/figui3/SKILL.md +1 -1
- package/.cursor/skills/figui3/components.md +20 -1
- package/.cursor/skills/figui3/react.md +1 -1
- package/.cursor/skills/figui3/reference.md +18 -0
- package/README.md +21 -11
- package/components.css +258 -39
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +3 -3
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +1 -1
- package/dist/fig.css +1 -1
- package/dist/fig.js +6 -6
- package/fig-editor.css +1 -1
- package/fig-editor.js +22 -5
- package/fig-lab.css +0 -71
- package/fig-lab.js +1 -671
- package/fig.js +883 -26
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -7015,6 +7015,7 @@ figDefineElement("fig-input-text", FigInputText);
|
|
|
7015
7015
|
* @attr {string} value - The current numeric value
|
|
7016
7016
|
* @attr {string} placeholder - Placeholder text
|
|
7017
7017
|
* @attr {string} size - Input size: default or "large"
|
|
7018
|
+
* @attr {string} variant - Visual style. Use `ghost` for a borderless control.
|
|
7018
7019
|
* @attr {boolean} disabled - Whether the input is disabled
|
|
7019
7020
|
* @attr {number} min - Minimum value
|
|
7020
7021
|
* @attr {number} max - Maximum value
|
|
@@ -14526,11 +14527,15 @@ class FigEasingCurve extends HTMLElement {
|
|
|
14526
14527
|
const y = pad + (1 - (pts[i].value - minVal) / range) * s;
|
|
14527
14528
|
d += (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
|
|
14528
14529
|
}
|
|
14529
|
-
return FigEasingCurve.#createSvgIcon(
|
|
14530
|
+
return FigEasingCurve.#createSvgIcon(
|
|
14531
|
+
d,
|
|
14532
|
+
size,
|
|
14533
|
+
FigEasingCurve.#createIconGrid(size),
|
|
14534
|
+
);
|
|
14530
14535
|
}
|
|
14531
14536
|
|
|
14532
14537
|
static #curveIconPath(cp1x, cp1y, cp2x, cp2y, size = 24) {
|
|
14533
|
-
const draw =
|
|
14538
|
+
const draw = 11;
|
|
14534
14539
|
const pad = (size - draw) / 2;
|
|
14535
14540
|
const samples = 48;
|
|
14536
14541
|
const points = [];
|
|
@@ -14578,7 +14583,28 @@ class FigEasingCurve extends HTMLElement {
|
|
|
14578
14583
|
return d;
|
|
14579
14584
|
}
|
|
14580
14585
|
|
|
14581
|
-
static #
|
|
14586
|
+
static #iconGridPoints(size = 24) {
|
|
14587
|
+
const draw = 11;
|
|
14588
|
+
const start = (size - draw) / 2;
|
|
14589
|
+
const gap = draw / 3;
|
|
14590
|
+
return Array.from({ length: 16 }, (_, index) => ({
|
|
14591
|
+
x: start + (index % 4) * gap,
|
|
14592
|
+
y: start + Math.floor(index / 4) * gap,
|
|
14593
|
+
}));
|
|
14594
|
+
}
|
|
14595
|
+
|
|
14596
|
+
static #createIconGrid(size = 24) {
|
|
14597
|
+
return FigEasingCurve.#iconGridPoints(size).map(({ x, y }) =>
|
|
14598
|
+
figCreateSvgElement("circle", {
|
|
14599
|
+
cx: x,
|
|
14600
|
+
cy: y,
|
|
14601
|
+
r: 0.5,
|
|
14602
|
+
fill: "var(--figma-color-icon-tertiary)",
|
|
14603
|
+
}),
|
|
14604
|
+
);
|
|
14605
|
+
}
|
|
14606
|
+
|
|
14607
|
+
static #createSvgIcon(d, size = 24, decorations = []) {
|
|
14582
14608
|
return figCreateSvgElement(
|
|
14583
14609
|
"svg",
|
|
14584
14610
|
{
|
|
@@ -14587,13 +14613,16 @@ class FigEasingCurve extends HTMLElement {
|
|
|
14587
14613
|
viewBox: `0 0 ${size} ${size}`,
|
|
14588
14614
|
fill: "none",
|
|
14589
14615
|
},
|
|
14590
|
-
|
|
14591
|
-
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14616
|
+
[
|
|
14617
|
+
...decorations,
|
|
14618
|
+
figCreateSvgElement("path", {
|
|
14619
|
+
d,
|
|
14620
|
+
stroke: "currentColor",
|
|
14621
|
+
"stroke-width": "1",
|
|
14622
|
+
"stroke-linecap": "round",
|
|
14623
|
+
fill: "none",
|
|
14624
|
+
}),
|
|
14625
|
+
],
|
|
14597
14626
|
);
|
|
14598
14627
|
}
|
|
14599
14628
|
|
|
@@ -14601,6 +14630,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
14601
14630
|
return FigEasingCurve.#createSvgIcon(
|
|
14602
14631
|
FigEasingCurve.#curveIconPath(cp1x, cp1y, cp2x, cp2y, size),
|
|
14603
14632
|
size,
|
|
14633
|
+
FigEasingCurve.#createIconGrid(size),
|
|
14604
14634
|
);
|
|
14605
14635
|
}
|
|
14606
14636
|
|
|
@@ -14612,7 +14642,13 @@ class FigEasingCurve extends HTMLElement {
|
|
|
14612
14642
|
cp2y,
|
|
14613
14643
|
size,
|
|
14614
14644
|
);
|
|
14615
|
-
|
|
14645
|
+
const grid = FigEasingCurve.#iconGridPoints(size)
|
|
14646
|
+
.map(
|
|
14647
|
+
({ x, y }) =>
|
|
14648
|
+
`<circle cx="${x}" cy="${y}" r="0.5" fill="var(--figma-color-icon-tertiary)"></circle>`,
|
|
14649
|
+
)
|
|
14650
|
+
.join("");
|
|
14651
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none">${grid}<path d="${d}" stroke="currentColor" stroke-width="1" stroke-linecap="round"/></svg>`;
|
|
14616
14652
|
}
|
|
14617
14653
|
|
|
14618
14654
|
// --- Rendering ---
|
|
@@ -14738,6 +14774,11 @@ class FigEasingCurve extends HTMLElement {
|
|
|
14738
14774
|
}
|
|
14739
14775
|
|
|
14740
14776
|
#createHandle(className, dataHandle, label) {
|
|
14777
|
+
const handle = figCreateElement("fig-handle", {
|
|
14778
|
+
size: "small",
|
|
14779
|
+
type: "minimal",
|
|
14780
|
+
"aria-label": label,
|
|
14781
|
+
});
|
|
14741
14782
|
return figCreateSvgElement(
|
|
14742
14783
|
"foreignObject",
|
|
14743
14784
|
{
|
|
@@ -14746,14 +14787,49 @@ class FigEasingCurve extends HTMLElement {
|
|
|
14746
14787
|
width: "20",
|
|
14747
14788
|
height: "20",
|
|
14748
14789
|
},
|
|
14749
|
-
figCreateElement(
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14790
|
+
figCreateElement(
|
|
14791
|
+
"fig-tooltip",
|
|
14792
|
+
{ text: this.#handleTooltipText(dataHandle) },
|
|
14793
|
+
handle,
|
|
14794
|
+
),
|
|
14754
14795
|
);
|
|
14755
14796
|
}
|
|
14756
14797
|
|
|
14798
|
+
#formatHandleTooltipNumber(value) {
|
|
14799
|
+
if (!Number.isFinite(value)) return "0";
|
|
14800
|
+
return String(Number(value.toFixed(this.#precision)));
|
|
14801
|
+
}
|
|
14802
|
+
|
|
14803
|
+
#handleTooltipText(handle) {
|
|
14804
|
+
if (this.#mode === "spring") {
|
|
14805
|
+
if (handle === "bounce") {
|
|
14806
|
+
return `Bounce ${this.#formatHandleTooltipNumber(this.#springBounce())}`;
|
|
14807
|
+
}
|
|
14808
|
+
return `Duration ${Math.round(this.#springDuration * 100)}%`;
|
|
14809
|
+
}
|
|
14810
|
+
|
|
14811
|
+
const point = handle === "1" ? this.#cp1 : this.#cp2;
|
|
14812
|
+
const label = handle === "1" ? "Start" : "End";
|
|
14813
|
+
return `${label} ${this.#formatHandleTooltipNumber(point.x)}, ${this.#formatHandleTooltipNumber(point.y)}`;
|
|
14814
|
+
}
|
|
14815
|
+
|
|
14816
|
+
#syncHandleTooltips() {
|
|
14817
|
+
for (const [container, handle] of [
|
|
14818
|
+
[this.#handle1, this.#mode === "spring" ? "bounce" : "1"],
|
|
14819
|
+
[this.#handle2, this.#mode === "spring" ? "duration" : "2"],
|
|
14820
|
+
]) {
|
|
14821
|
+
const tooltip = container?.querySelector("fig-tooltip");
|
|
14822
|
+
if (tooltip) tooltip.text = this.#handleTooltipText(handle);
|
|
14823
|
+
}
|
|
14824
|
+
}
|
|
14825
|
+
|
|
14826
|
+
#toggleHandleTooltip(container, show) {
|
|
14827
|
+
const tooltip = container?.querySelector("fig-tooltip");
|
|
14828
|
+
if (!tooltip) return;
|
|
14829
|
+
if (show) tooltip.setAttribute("show", "true");
|
|
14830
|
+
else tooltip.removeAttribute("show");
|
|
14831
|
+
}
|
|
14832
|
+
|
|
14757
14833
|
#createContent() {
|
|
14758
14834
|
const size = 200;
|
|
14759
14835
|
const select = this.#isPresetsEnabled() ? this.#createSelect() : null;
|
|
@@ -15002,6 +15078,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15002
15078
|
} else {
|
|
15003
15079
|
this.#updateBezierPaths();
|
|
15004
15080
|
}
|
|
15081
|
+
this.#syncHandleTooltips();
|
|
15005
15082
|
}
|
|
15006
15083
|
|
|
15007
15084
|
#syncActiveBezierArm() {
|
|
@@ -15098,7 +15175,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15098
15175
|
totalTime,
|
|
15099
15176
|
};
|
|
15100
15177
|
|
|
15101
|
-
const durationNorm = Math.max(0.05, Math.min(
|
|
15178
|
+
const durationNorm = Math.max(0.05, Math.min(1, this.#springDuration));
|
|
15102
15179
|
let d = "";
|
|
15103
15180
|
for (let i = 0; i < points.length; i++) {
|
|
15104
15181
|
const nt = (points[i].t / totalTime) * durationNorm;
|
|
@@ -15137,10 +15214,21 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15137
15214
|
#findPeakOvershoot(points) {
|
|
15138
15215
|
let peak = { t: 0, value: 1 };
|
|
15139
15216
|
let passedTarget = false;
|
|
15140
|
-
for (
|
|
15141
|
-
|
|
15142
|
-
|
|
15143
|
-
|
|
15217
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
15218
|
+
const previous = points[i - 1];
|
|
15219
|
+
const current = points[i];
|
|
15220
|
+
const next = points[i + 1];
|
|
15221
|
+
if (current.value >= 0.99) passedTarget = true;
|
|
15222
|
+
if (passedTarget && current.value > peak.value) {
|
|
15223
|
+
peak = { t: current.t, value: current.value };
|
|
15224
|
+
}
|
|
15225
|
+
if (
|
|
15226
|
+
passedTarget &&
|
|
15227
|
+
current.value > 1 &&
|
|
15228
|
+
current.value >= previous.value &&
|
|
15229
|
+
current.value >= next.value
|
|
15230
|
+
) {
|
|
15231
|
+
return { t: current.t, value: current.value };
|
|
15144
15232
|
}
|
|
15145
15233
|
}
|
|
15146
15234
|
return peak;
|
|
@@ -15342,14 +15430,14 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15342
15430
|
this.#spring.stiffness = Math.max(10, Math.round(this.#spring.stiffness + dx * 1.5));
|
|
15343
15431
|
break;
|
|
15344
15432
|
case "ArrowRight":
|
|
15345
|
-
this.#springDuration = Math.min(
|
|
15433
|
+
this.#springDuration = Math.min(1, this.#springDuration + dx / 200);
|
|
15346
15434
|
this.#spring.stiffness = Math.max(10, Math.round(this.#spring.stiffness - dx * 1.5));
|
|
15347
15435
|
break;
|
|
15348
15436
|
case "Home":
|
|
15349
15437
|
this.#springDuration = 0.05;
|
|
15350
15438
|
break;
|
|
15351
15439
|
case "End":
|
|
15352
|
-
this.#springDuration =
|
|
15440
|
+
this.#springDuration = 1;
|
|
15353
15441
|
break;
|
|
15354
15442
|
default:
|
|
15355
15443
|
return false;
|
|
@@ -15551,6 +15639,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15551
15639
|
e.preventDefault();
|
|
15552
15640
|
this.#isDragging = handle;
|
|
15553
15641
|
this.#syncActiveBezierArm();
|
|
15642
|
+
const activeHandle = handle === 1 ? this.#handle1 : this.#handle2;
|
|
15643
|
+
this.#toggleHandleTooltip(activeHandle, true);
|
|
15554
15644
|
const svgRect = this.#svg.getBoundingClientRect();
|
|
15555
15645
|
const startClientX = e.clientX;
|
|
15556
15646
|
const startClientY = e.clientY;
|
|
@@ -15601,6 +15691,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15601
15691
|
const onUp = () => {
|
|
15602
15692
|
this.#isDragging = null;
|
|
15603
15693
|
this.#syncActiveBezierArm();
|
|
15694
|
+
this.#toggleHandleTooltip(activeHandle, false);
|
|
15604
15695
|
document.removeEventListener("pointermove", onMove);
|
|
15605
15696
|
document.removeEventListener("pointerup", onUp);
|
|
15606
15697
|
this.#emit("change");
|
|
@@ -15614,6 +15705,9 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15614
15705
|
e.preventDefault();
|
|
15615
15706
|
this.#isDragging = handleType;
|
|
15616
15707
|
this.classList.toggle("spring-bounce-dragging", handleType === "bounce");
|
|
15708
|
+
const activeHandle =
|
|
15709
|
+
handleType === "bounce" ? this.#handle1 : this.#handle2;
|
|
15710
|
+
this.#toggleHandleTooltip(activeHandle, true);
|
|
15617
15711
|
|
|
15618
15712
|
const startBounce = this.#springBounce();
|
|
15619
15713
|
const startStiffness = this.#spring.stiffness;
|
|
@@ -15631,7 +15725,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15631
15725
|
const dx = e.clientX - startX;
|
|
15632
15726
|
this.#springDuration = Math.max(
|
|
15633
15727
|
0.05,
|
|
15634
|
-
Math.min(
|
|
15728
|
+
Math.min(1, startDuration + dx / 200),
|
|
15635
15729
|
);
|
|
15636
15730
|
this.#spring.stiffness = Math.max(
|
|
15637
15731
|
10,
|
|
@@ -15649,6 +15743,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15649
15743
|
const onUp = () => {
|
|
15650
15744
|
this.#isDragging = null;
|
|
15651
15745
|
this.classList.remove("spring-bounce-dragging");
|
|
15746
|
+
this.#toggleHandleTooltip(activeHandle, false);
|
|
15652
15747
|
document.removeEventListener("pointermove", onMove);
|
|
15653
15748
|
document.removeEventListener("pointerup", onUp);
|
|
15654
15749
|
this.#emit("change");
|
|
@@ -15660,6 +15755,770 @@ class FigEasingCurve extends HTMLElement {
|
|
|
15660
15755
|
}
|
|
15661
15756
|
figDefineElement("fig-easing-curve", FigEasingCurve);
|
|
15662
15757
|
|
|
15758
|
+
/**
|
|
15759
|
+
* A circular angle control with an instrument-style dial.
|
|
15760
|
+
* @attr {number} value - Angle in the selected unit.
|
|
15761
|
+
* @attr {number} default - Default/reset value in the selected unit.
|
|
15762
|
+
* @attr {number} precision - Decimal places for output (defaults: deg/grad 0, rad 2, turn 3).
|
|
15763
|
+
* @attr {boolean} dial - Show the dial surface (default true).
|
|
15764
|
+
* @attr {number} min - Optional minimum value in the selected unit.
|
|
15765
|
+
* @attr {number} max - Optional maximum value in the selected unit.
|
|
15766
|
+
* @attr {number} step - Keyboard/text step in the selected unit.
|
|
15767
|
+
* @attr {string} units - deg, °, rad, turn, or grad (default deg).
|
|
15768
|
+
* @attr {boolean} rotations - Show completed rotations beside the text input.
|
|
15769
|
+
* @attr {boolean} disabled - Disable interaction.
|
|
15770
|
+
*/
|
|
15771
|
+
class FigAngle extends HTMLElement {
|
|
15772
|
+
#angle = 0;
|
|
15773
|
+
#precision = 0;
|
|
15774
|
+
#units = "deg";
|
|
15775
|
+
#surface = null;
|
|
15776
|
+
#dial = null;
|
|
15777
|
+
#indicator = null;
|
|
15778
|
+
#indicatorHalo = null;
|
|
15779
|
+
#reference = null;
|
|
15780
|
+
#angleInput = null;
|
|
15781
|
+
#rotationSpan = null;
|
|
15782
|
+
#resetButton = null;
|
|
15783
|
+
#eventAbort = null;
|
|
15784
|
+
#activePointerId = null;
|
|
15785
|
+
#previousPointerAngle = null;
|
|
15786
|
+
#reflectingValue = false;
|
|
15787
|
+
#convertingUnits = false;
|
|
15788
|
+
#onPointerDown = (event) => this.#startPointerGesture(event);
|
|
15789
|
+
#onPointerMove = (event) => this.#movePointerGesture(event);
|
|
15790
|
+
#onPointerEnd = (event) => this.#endPointerGesture(event);
|
|
15791
|
+
#onKeyDown = (event) => this.#handleKeyDown(event);
|
|
15792
|
+
#onTextInput = (event) => this.#handleTextInput(event);
|
|
15793
|
+
#onRawTextChange = (event) => this.#normalizeRawTextUnit(event);
|
|
15794
|
+
#onResetClick = () => this.#resetToDefault();
|
|
15795
|
+
|
|
15796
|
+
static #rotationGeometry(
|
|
15797
|
+
angle,
|
|
15798
|
+
centerX = 12,
|
|
15799
|
+
centerY = 12,
|
|
15800
|
+
radius = 7,
|
|
15801
|
+
arrowSize = 3,
|
|
15802
|
+
) {
|
|
15803
|
+
const value = Number(angle);
|
|
15804
|
+
const safeValue = Number.isFinite(value) ? value : 0;
|
|
15805
|
+
const direction = safeValue < 0 ? "counterclockwise" : "clockwise";
|
|
15806
|
+
const absoluteSweep = Math.abs(safeValue);
|
|
15807
|
+
const fullSweep = absoluteSweep >= 360;
|
|
15808
|
+
const sweep = absoluteSweep % 360;
|
|
15809
|
+
if (sweep < 0.001 && !fullSweep) {
|
|
15810
|
+
return { direction, arc: "", arrow: "" };
|
|
15811
|
+
}
|
|
15812
|
+
|
|
15813
|
+
const signedSweep = direction === "clockwise" ? sweep : -sweep;
|
|
15814
|
+
const radians = (signedSweep * Math.PI) / 180;
|
|
15815
|
+
const end = {
|
|
15816
|
+
x: centerX + Math.cos(radians) * radius,
|
|
15817
|
+
y: centerY + Math.sin(radians) * radius,
|
|
15818
|
+
};
|
|
15819
|
+
const tangent =
|
|
15820
|
+
radians + (direction === "clockwise" ? Math.PI / 2 : -Math.PI / 2);
|
|
15821
|
+
const back = tangent + Math.PI;
|
|
15822
|
+
const spread = Math.PI / 4;
|
|
15823
|
+
const arm = (offset) => ({
|
|
15824
|
+
x: end.x + Math.cos(back + offset) * arrowSize,
|
|
15825
|
+
y: end.y + Math.sin(back + offset) * arrowSize,
|
|
15826
|
+
});
|
|
15827
|
+
const firstArm = arm(-spread);
|
|
15828
|
+
const secondArm = arm(spread);
|
|
15829
|
+
const point = (pointValue) =>
|
|
15830
|
+
`${Number(pointValue.x.toFixed(3))} ${Number(pointValue.y.toFixed(3))}`;
|
|
15831
|
+
const sweepFlag = direction === "clockwise" ? 1 : 0;
|
|
15832
|
+
const arc = fullSweep
|
|
15833
|
+
? `M ${centerX + radius} ${centerY} A ${radius} ${radius} 0 0 ${sweepFlag} ${centerX - radius} ${centerY} A ${radius} ${radius} 0 0 ${sweepFlag} ${centerX + radius} ${centerY}`
|
|
15834
|
+
: `M ${centerX + radius} ${centerY} A ${radius} ${radius} 0 ${sweep > 180 ? 1 : 0} ${sweepFlag} ${point(end)}`;
|
|
15835
|
+
return {
|
|
15836
|
+
direction,
|
|
15837
|
+
arc,
|
|
15838
|
+
arrow: `M ${point(firstArm)} L ${point(end)} L ${point(secondArm)}`,
|
|
15839
|
+
};
|
|
15840
|
+
}
|
|
15841
|
+
|
|
15842
|
+
static #createRotationIcon(angle = 0) {
|
|
15843
|
+
const geometry = FigAngle.#rotationGeometry(angle, 50, 50, 47, 4);
|
|
15844
|
+
return figCreateSvgElement(
|
|
15845
|
+
"svg",
|
|
15846
|
+
{
|
|
15847
|
+
class: "fig-angle-reference",
|
|
15848
|
+
x: "3%",
|
|
15849
|
+
y: "3%",
|
|
15850
|
+
width: "94%",
|
|
15851
|
+
height: "94%",
|
|
15852
|
+
viewBox: "3 3 94 94",
|
|
15853
|
+
fill: "none",
|
|
15854
|
+
overflow: "visible",
|
|
15855
|
+
"aria-hidden": "true",
|
|
15856
|
+
"data-direction": geometry.direction,
|
|
15857
|
+
},
|
|
15858
|
+
figCreateSvgElement("path", {
|
|
15859
|
+
class: "fig-angle-reference-path",
|
|
15860
|
+
d: [geometry.arc, geometry.arrow].filter(Boolean).join(" "),
|
|
15861
|
+
hidden: !geometry.arc,
|
|
15862
|
+
fill: "none",
|
|
15863
|
+
stroke: "currentColor",
|
|
15864
|
+
"stroke-width": "var(--stroke-width)",
|
|
15865
|
+
"stroke-linecap": "round",
|
|
15866
|
+
"stroke-linejoin": "round",
|
|
15867
|
+
}),
|
|
15868
|
+
);
|
|
15869
|
+
}
|
|
15870
|
+
|
|
15871
|
+
static rotationIcon(angle = 0, size = 48) {
|
|
15872
|
+
const iconSize = Number.isFinite(Number(size))
|
|
15873
|
+
? Math.max(1, Number(size))
|
|
15874
|
+
: 48;
|
|
15875
|
+
const geometry = FigAngle.#rotationGeometry(angle);
|
|
15876
|
+
const paths = geometry.arc
|
|
15877
|
+
? `<path d="${geometry.arc} ${geometry.arrow}" fill="none" stroke="currentColor" stroke-width="var(--stroke-width, 1)" stroke-linecap="round" stroke-linejoin="round"></path>`
|
|
15878
|
+
: "";
|
|
15879
|
+
return `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" data-direction="${geometry.direction}">${paths}</svg>`;
|
|
15880
|
+
}
|
|
15881
|
+
|
|
15882
|
+
static get observedAttributes() {
|
|
15883
|
+
return [
|
|
15884
|
+
"value",
|
|
15885
|
+
"default",
|
|
15886
|
+
"precision",
|
|
15887
|
+
"dial",
|
|
15888
|
+
"min",
|
|
15889
|
+
"max",
|
|
15890
|
+
"step",
|
|
15891
|
+
"units",
|
|
15892
|
+
"rotations",
|
|
15893
|
+
"disabled",
|
|
15894
|
+
"aria-label",
|
|
15895
|
+
"name",
|
|
15896
|
+
];
|
|
15897
|
+
}
|
|
15898
|
+
|
|
15899
|
+
connectedCallback() {
|
|
15900
|
+
this.#readConfiguration();
|
|
15901
|
+
this.#render();
|
|
15902
|
+
}
|
|
15903
|
+
|
|
15904
|
+
disconnectedCallback() {
|
|
15905
|
+
this.#cancelPointerGesture();
|
|
15906
|
+
this.#teardownEvents();
|
|
15907
|
+
}
|
|
15908
|
+
|
|
15909
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
15910
|
+
if (oldValue === newValue) return;
|
|
15911
|
+
if (this.#convertingUnits) return;
|
|
15912
|
+
if (name === "value") {
|
|
15913
|
+
if (this.#reflectingValue) return;
|
|
15914
|
+
const next = Number(newValue);
|
|
15915
|
+
if (Number.isFinite(next)) {
|
|
15916
|
+
this.#setValue(next, { reflect: false });
|
|
15917
|
+
}
|
|
15918
|
+
return;
|
|
15919
|
+
}
|
|
15920
|
+
|
|
15921
|
+
if (name === "units" && this.isConnected) {
|
|
15922
|
+
const degrees = this.#toDegrees(this.#angle);
|
|
15923
|
+
const previousUnit = this.#units;
|
|
15924
|
+
this.#units = this.#normalizeUnit(newValue);
|
|
15925
|
+
this.#convertUnitAttributes(previousUnit, this.#units);
|
|
15926
|
+
this.#precision = this.#configuredPrecision();
|
|
15927
|
+
this.#setValue(this.#fromDegrees(degrees));
|
|
15928
|
+
this.#render();
|
|
15929
|
+
return;
|
|
15930
|
+
}
|
|
15931
|
+
|
|
15932
|
+
this.#readConfiguration();
|
|
15933
|
+
if (!this.isConnected) return;
|
|
15934
|
+
if (
|
|
15935
|
+
[
|
|
15936
|
+
"dial",
|
|
15937
|
+
"precision",
|
|
15938
|
+
"min",
|
|
15939
|
+
"max",
|
|
15940
|
+
"step",
|
|
15941
|
+
"units",
|
|
15942
|
+
"rotations",
|
|
15943
|
+
"disabled",
|
|
15944
|
+
"aria-label",
|
|
15945
|
+
"name",
|
|
15946
|
+
].includes(name)
|
|
15947
|
+
) {
|
|
15948
|
+
this.#render();
|
|
15949
|
+
return;
|
|
15950
|
+
}
|
|
15951
|
+
this.#setValue(this.#angle);
|
|
15952
|
+
}
|
|
15953
|
+
|
|
15954
|
+
get value() {
|
|
15955
|
+
return this.#angle;
|
|
15956
|
+
}
|
|
15957
|
+
|
|
15958
|
+
set value(value) {
|
|
15959
|
+
const next = Number(value);
|
|
15960
|
+
if (!Number.isFinite(next)) return;
|
|
15961
|
+
this.#setValue(next);
|
|
15962
|
+
}
|
|
15963
|
+
|
|
15964
|
+
get defaultValue() {
|
|
15965
|
+
return this.#resolveDefaultValue();
|
|
15966
|
+
}
|
|
15967
|
+
|
|
15968
|
+
set defaultValue(value) {
|
|
15969
|
+
if (value === null || value === undefined || value === "") {
|
|
15970
|
+
this.removeAttribute("default");
|
|
15971
|
+
return;
|
|
15972
|
+
}
|
|
15973
|
+
const next = Number(value);
|
|
15974
|
+
if (Number.isFinite(next)) this.setAttribute("default", String(next));
|
|
15975
|
+
}
|
|
15976
|
+
|
|
15977
|
+
get adjacent() {
|
|
15978
|
+
return Math.cos((this.#toDegrees(this.#angle) * Math.PI) / 180);
|
|
15979
|
+
}
|
|
15980
|
+
|
|
15981
|
+
get opposite() {
|
|
15982
|
+
return Math.sin((this.#toDegrees(this.#angle) * Math.PI) / 180);
|
|
15983
|
+
}
|
|
15984
|
+
|
|
15985
|
+
focus(options) {
|
|
15986
|
+
this.#dial?.focus(options);
|
|
15987
|
+
}
|
|
15988
|
+
|
|
15989
|
+
#readConfiguration() {
|
|
15990
|
+
this.#units = this.#normalizeUnit(this.getAttribute("units"));
|
|
15991
|
+
this.#precision = this.#configuredPrecision();
|
|
15992
|
+
const valueAttr = this.getAttribute("value");
|
|
15993
|
+
const value =
|
|
15994
|
+
valueAttr === null || valueAttr.trim() === ""
|
|
15995
|
+
? this.#resolveDefaultValue()
|
|
15996
|
+
: Number(valueAttr);
|
|
15997
|
+
if (Number.isFinite(value)) this.#angle = this.#clamp(value);
|
|
15998
|
+
}
|
|
15999
|
+
|
|
16000
|
+
#configuredPrecision() {
|
|
16001
|
+
if (!this.hasAttribute("precision")) {
|
|
16002
|
+
if (this.#units === "turn") return 3;
|
|
16003
|
+
if (this.#units === "rad") return 2;
|
|
16004
|
+
return 0;
|
|
16005
|
+
}
|
|
16006
|
+
const precision = Number.parseInt(this.getAttribute("precision"), 10);
|
|
16007
|
+
return Number.isFinite(precision) ? Math.max(0, precision) : 0;
|
|
16008
|
+
}
|
|
16009
|
+
|
|
16010
|
+
#normalizeUnit(unit) {
|
|
16011
|
+
const normalized = String(unit || "deg")
|
|
16012
|
+
.trim()
|
|
16013
|
+
.toLowerCase();
|
|
16014
|
+
if (normalized === "°") return "°";
|
|
16015
|
+
return ["deg", "rad", "turn", "grad"].includes(normalized)
|
|
16016
|
+
? normalized
|
|
16017
|
+
: "deg";
|
|
16018
|
+
}
|
|
16019
|
+
|
|
16020
|
+
#convertUnitAttributes(fromUnit, toUnit) {
|
|
16021
|
+
if (fromUnit === toUnit) return;
|
|
16022
|
+
this.#convertingUnits = true;
|
|
16023
|
+
for (const name of ["min", "max", "default", "step"]) {
|
|
16024
|
+
const raw = this.getAttribute(name);
|
|
16025
|
+
if (raw === null || raw.trim() === "") continue;
|
|
16026
|
+
const value = Number(raw);
|
|
16027
|
+
if (!Number.isFinite(value)) continue;
|
|
16028
|
+
const converted = this.#fromDegreesForUnit(
|
|
16029
|
+
this.#toDegreesForUnit(value, fromUnit),
|
|
16030
|
+
toUnit,
|
|
16031
|
+
);
|
|
16032
|
+
this.setAttribute(name, String(converted));
|
|
16033
|
+
}
|
|
16034
|
+
this.#convertingUnits = false;
|
|
16035
|
+
}
|
|
16036
|
+
|
|
16037
|
+
#readBoolean(name, defaultValue = false) {
|
|
16038
|
+
if (!this.hasAttribute(name)) return defaultValue;
|
|
16039
|
+
return this.getAttribute(name)?.toLowerCase() !== "false";
|
|
16040
|
+
}
|
|
16041
|
+
|
|
16042
|
+
#isDisabled() {
|
|
16043
|
+
return figBooleanAttribute(this, "disabled");
|
|
16044
|
+
}
|
|
16045
|
+
|
|
16046
|
+
#minimum() {
|
|
16047
|
+
const value = Number(this.getAttribute("min"));
|
|
16048
|
+
return this.hasAttribute("min") && Number.isFinite(value) ? value : null;
|
|
16049
|
+
}
|
|
16050
|
+
|
|
16051
|
+
#maximum() {
|
|
16052
|
+
const value = Number(this.getAttribute("max"));
|
|
16053
|
+
return this.hasAttribute("max") && Number.isFinite(value) ? value : null;
|
|
16054
|
+
}
|
|
16055
|
+
|
|
16056
|
+
#clamp(value) {
|
|
16057
|
+
let next = Number(value);
|
|
16058
|
+
if (!Number.isFinite(next)) return this.#angle;
|
|
16059
|
+
const min = this.#minimum();
|
|
16060
|
+
const max = this.#maximum();
|
|
16061
|
+
if (min !== null) next = Math.max(min, next);
|
|
16062
|
+
if (max !== null) next = Math.min(max, next);
|
|
16063
|
+
return next;
|
|
16064
|
+
}
|
|
16065
|
+
|
|
16066
|
+
#resolveDefaultValue(rawDefault = this.getAttribute("default")) {
|
|
16067
|
+
const parsed =
|
|
16068
|
+
rawDefault === null || String(rawDefault).trim() === ""
|
|
16069
|
+
? 0
|
|
16070
|
+
: Number(rawDefault);
|
|
16071
|
+
return this.#clamp(Number.isFinite(parsed) ? parsed : 0);
|
|
16072
|
+
}
|
|
16073
|
+
|
|
16074
|
+
#toDegreesForUnit(value, unit) {
|
|
16075
|
+
switch (unit) {
|
|
16076
|
+
case "rad":
|
|
16077
|
+
return (value * 180) / Math.PI;
|
|
16078
|
+
case "turn":
|
|
16079
|
+
return value * 360;
|
|
16080
|
+
case "grad":
|
|
16081
|
+
return value * 0.9;
|
|
16082
|
+
default:
|
|
16083
|
+
return value;
|
|
16084
|
+
}
|
|
16085
|
+
}
|
|
16086
|
+
|
|
16087
|
+
#fromDegreesForUnit(degrees, unit) {
|
|
16088
|
+
switch (unit) {
|
|
16089
|
+
case "rad":
|
|
16090
|
+
return (degrees * Math.PI) / 180;
|
|
16091
|
+
case "turn":
|
|
16092
|
+
return degrees / 360;
|
|
16093
|
+
case "grad":
|
|
16094
|
+
return degrees / 0.9;
|
|
16095
|
+
default:
|
|
16096
|
+
return degrees;
|
|
16097
|
+
}
|
|
16098
|
+
}
|
|
16099
|
+
|
|
16100
|
+
#toDegrees(value) {
|
|
16101
|
+
return this.#toDegreesForUnit(value, this.#units);
|
|
16102
|
+
}
|
|
16103
|
+
|
|
16104
|
+
#fromDegrees(degrees) {
|
|
16105
|
+
return this.#fromDegreesForUnit(degrees, this.#units);
|
|
16106
|
+
}
|
|
16107
|
+
|
|
16108
|
+
#unitSuffix() {
|
|
16109
|
+
return this.#units === "deg" ? "°" : this.#units;
|
|
16110
|
+
}
|
|
16111
|
+
|
|
16112
|
+
#defaultStep() {
|
|
16113
|
+
switch (this.#units) {
|
|
16114
|
+
case "rad":
|
|
16115
|
+
return 0.01;
|
|
16116
|
+
case "turn":
|
|
16117
|
+
return 0.001;
|
|
16118
|
+
case "grad":
|
|
16119
|
+
return 0.1;
|
|
16120
|
+
default:
|
|
16121
|
+
return 1;
|
|
16122
|
+
}
|
|
16123
|
+
}
|
|
16124
|
+
|
|
16125
|
+
#step() {
|
|
16126
|
+
const step = Number(this.getAttribute("step"));
|
|
16127
|
+
return this.hasAttribute("step") && Number.isFinite(step) && step > 0
|
|
16128
|
+
? step
|
|
16129
|
+
: this.#defaultStep();
|
|
16130
|
+
}
|
|
16131
|
+
|
|
16132
|
+
#name() {
|
|
16133
|
+
return this.getAttribute("aria-label") || this.getAttribute("name") || "Angle";
|
|
16134
|
+
}
|
|
16135
|
+
|
|
16136
|
+
#render() {
|
|
16137
|
+
this.#cancelPointerGesture();
|
|
16138
|
+
this.#teardownEvents();
|
|
16139
|
+
const children = [];
|
|
16140
|
+
const rotations = this.#readBoolean("rotations")
|
|
16141
|
+
? figCreateElement("span", {
|
|
16142
|
+
slot: "append",
|
|
16143
|
+
className: "fig-angle-rotations",
|
|
16144
|
+
})
|
|
16145
|
+
: null;
|
|
16146
|
+
const showDial = this.#readBoolean("dial", true);
|
|
16147
|
+
const angleInput = figCreateElement(
|
|
16148
|
+
"fig-input-number",
|
|
16149
|
+
{
|
|
16150
|
+
className: "fig-angle-input",
|
|
16151
|
+
variant: showDial ? "ghost" : undefined,
|
|
16152
|
+
tabular: true,
|
|
16153
|
+
name: "angle",
|
|
16154
|
+
value: this.#angle,
|
|
16155
|
+
min: this.#minimum(),
|
|
16156
|
+
max: this.#maximum(),
|
|
16157
|
+
step: this.#step(),
|
|
16158
|
+
precision: this.#precision,
|
|
16159
|
+
units: this.#unitSuffix(),
|
|
16160
|
+
disabled: this.#isDisabled(),
|
|
16161
|
+
"aria-label": this.#name(),
|
|
16162
|
+
},
|
|
16163
|
+
rotations,
|
|
16164
|
+
);
|
|
16165
|
+
|
|
16166
|
+
if (showDial) {
|
|
16167
|
+
const reset = figCreateElement(
|
|
16168
|
+
"fig-tooltip",
|
|
16169
|
+
{ text: "Reset" },
|
|
16170
|
+
figCreateElement(
|
|
16171
|
+
"fig-button",
|
|
16172
|
+
{
|
|
16173
|
+
variant: "ghost",
|
|
16174
|
+
icon: "true",
|
|
16175
|
+
className: "fig-angle-reset",
|
|
16176
|
+
disabled: this.#isDisabled(),
|
|
16177
|
+
"aria-label": "Reset to default",
|
|
16178
|
+
},
|
|
16179
|
+
createFigIcon("reset", { size: "small" }),
|
|
16180
|
+
),
|
|
16181
|
+
);
|
|
16182
|
+
const min = this.#minimum() ?? this.#fromDegrees(0);
|
|
16183
|
+
const max = this.#maximum() ?? this.#fromDegrees(360);
|
|
16184
|
+
const disabled = this.#isDisabled();
|
|
16185
|
+
const ticks = Array.from({ length: 24 }, (_, index) => {
|
|
16186
|
+
const angle = index * 15;
|
|
16187
|
+
const radians = (angle * Math.PI) / 180;
|
|
16188
|
+
const outer = 37;
|
|
16189
|
+
const inner = angle % 45 === 0 ? 31 : 35;
|
|
16190
|
+
return figCreateSvgElement("line", {
|
|
16191
|
+
class:
|
|
16192
|
+
angle % 45 === 0
|
|
16193
|
+
? "fig-angle-tick fig-angle-tick-major"
|
|
16194
|
+
: "fig-angle-tick",
|
|
16195
|
+
x1: 50 + Math.cos(radians) * inner,
|
|
16196
|
+
y1: 50 + Math.sin(radians) * inner,
|
|
16197
|
+
x2: 50 + Math.cos(radians) * outer,
|
|
16198
|
+
y2: 50 + Math.sin(radians) * outer,
|
|
16199
|
+
});
|
|
16200
|
+
});
|
|
16201
|
+
const svg = figCreateSvgElement(
|
|
16202
|
+
"svg",
|
|
16203
|
+
{
|
|
16204
|
+
class: "fig-angle-svg",
|
|
16205
|
+
viewBox: "0 0 100 100",
|
|
16206
|
+
preserveAspectRatio: "xMidYMid meet",
|
|
16207
|
+
role: "slider",
|
|
16208
|
+
tabindex: disabled ? "-1" : "0",
|
|
16209
|
+
"aria-label": this.#name(),
|
|
16210
|
+
"aria-valuemin": min,
|
|
16211
|
+
"aria-valuemax": max,
|
|
16212
|
+
"aria-disabled": disabled ? "true" : "false",
|
|
16213
|
+
},
|
|
16214
|
+
[
|
|
16215
|
+
figCreateSvgElement("rect", {
|
|
16216
|
+
class: "fig-angle-hit-area",
|
|
16217
|
+
x: "0",
|
|
16218
|
+
y: "0",
|
|
16219
|
+
width: "100",
|
|
16220
|
+
height: "100",
|
|
16221
|
+
fill: "transparent",
|
|
16222
|
+
}),
|
|
16223
|
+
figCreateSvgElement("circle", {
|
|
16224
|
+
class: "fig-angle-boundary",
|
|
16225
|
+
cx: "50",
|
|
16226
|
+
cy: "50",
|
|
16227
|
+
r: "47",
|
|
16228
|
+
}),
|
|
16229
|
+
...ticks,
|
|
16230
|
+
FigAngle.#createRotationIcon(this.#toDegrees(this.#angle)),
|
|
16231
|
+
figCreateSvgElement("line", {
|
|
16232
|
+
class: "fig-angle-indicator-halo",
|
|
16233
|
+
x1: "75",
|
|
16234
|
+
y1: "50",
|
|
16235
|
+
x2: "87",
|
|
16236
|
+
y2: "50",
|
|
16237
|
+
}),
|
|
16238
|
+
figCreateSvgElement("line", {
|
|
16239
|
+
class: "fig-angle-indicator",
|
|
16240
|
+
x1: "75",
|
|
16241
|
+
y1: "50",
|
|
16242
|
+
x2: "87",
|
|
16243
|
+
y2: "50",
|
|
16244
|
+
}),
|
|
16245
|
+
],
|
|
16246
|
+
);
|
|
16247
|
+
children.push(
|
|
16248
|
+
figCreateElement(
|
|
16249
|
+
"div",
|
|
16250
|
+
{
|
|
16251
|
+
className: "fig-angle-surface",
|
|
16252
|
+
},
|
|
16253
|
+
[svg, angleInput, reset],
|
|
16254
|
+
),
|
|
16255
|
+
);
|
|
16256
|
+
} else {
|
|
16257
|
+
children.push(angleInput);
|
|
16258
|
+
}
|
|
16259
|
+
|
|
16260
|
+
this.replaceChildren(...children);
|
|
16261
|
+
this.#surface = this.querySelector(".fig-angle-surface");
|
|
16262
|
+
this.#dial = this.querySelector(".fig-angle-svg");
|
|
16263
|
+
this.#indicator = this.querySelector(".fig-angle-indicator");
|
|
16264
|
+
this.#indicatorHalo = this.querySelector(".fig-angle-indicator-halo");
|
|
16265
|
+
this.#reference = this.querySelector(".fig-angle-reference");
|
|
16266
|
+
this.#angleInput = this.querySelector(".fig-angle-input");
|
|
16267
|
+
this.#rotationSpan = this.querySelector(".fig-angle-rotations");
|
|
16268
|
+
this.#resetButton = this.querySelector(".fig-angle-reset");
|
|
16269
|
+
this.#setupEvents();
|
|
16270
|
+
this.#sync();
|
|
16271
|
+
}
|
|
16272
|
+
|
|
16273
|
+
#setupEvents() {
|
|
16274
|
+
this.#eventAbort = new AbortController();
|
|
16275
|
+
const signal = this.#eventAbort.signal;
|
|
16276
|
+
this.#dial?.addEventListener("pointerdown", this.#onPointerDown, { signal });
|
|
16277
|
+
this.#dial?.addEventListener("pointermove", this.#onPointerMove, { signal });
|
|
16278
|
+
this.#dial?.addEventListener("pointerup", this.#onPointerEnd, { signal });
|
|
16279
|
+
this.#dial?.addEventListener("pointercancel", this.#onPointerEnd, { signal });
|
|
16280
|
+
this.#dial?.addEventListener("lostpointercapture", this.#onPointerEnd, {
|
|
16281
|
+
signal,
|
|
16282
|
+
});
|
|
16283
|
+
this.#dial?.addEventListener("keydown", this.#onKeyDown, { signal });
|
|
16284
|
+
this.#angleInput?.addEventListener("input", this.#onTextInput, { signal });
|
|
16285
|
+
this.#angleInput?.addEventListener("change", this.#onTextInput, { signal });
|
|
16286
|
+
this.#resetButton?.addEventListener("click", this.#onResetClick, { signal });
|
|
16287
|
+
this.addEventListener("change", this.#onRawTextChange, {
|
|
16288
|
+
capture: true,
|
|
16289
|
+
signal,
|
|
16290
|
+
});
|
|
16291
|
+
}
|
|
16292
|
+
|
|
16293
|
+
#teardownEvents() {
|
|
16294
|
+
this.#eventAbort?.abort();
|
|
16295
|
+
this.#eventAbort = null;
|
|
16296
|
+
}
|
|
16297
|
+
|
|
16298
|
+
#setValue(value, { reflect = true } = {}) {
|
|
16299
|
+
const next = this.#clamp(value);
|
|
16300
|
+
this.#angle = next;
|
|
16301
|
+
if (reflect) {
|
|
16302
|
+
const serialized = String(next);
|
|
16303
|
+
if (this.getAttribute("value") !== serialized) {
|
|
16304
|
+
this.#reflectingValue = true;
|
|
16305
|
+
this.setAttribute("value", serialized);
|
|
16306
|
+
this.#reflectingValue = false;
|
|
16307
|
+
}
|
|
16308
|
+
}
|
|
16309
|
+
this.#sync();
|
|
16310
|
+
}
|
|
16311
|
+
|
|
16312
|
+
#sync() {
|
|
16313
|
+
const degrees = this.#toDegrees(this.#angle);
|
|
16314
|
+
const normalized = ((degrees % 360) + 360) % 360;
|
|
16315
|
+
const transform = `rotate(${normalized} 50 50)`;
|
|
16316
|
+
this.#indicator?.setAttribute("transform", transform);
|
|
16317
|
+
this.#indicatorHalo?.setAttribute("transform", transform);
|
|
16318
|
+
const referenceGeometry = FigAngle.#rotationGeometry(
|
|
16319
|
+
degrees,
|
|
16320
|
+
50,
|
|
16321
|
+
50,
|
|
16322
|
+
47,
|
|
16323
|
+
4,
|
|
16324
|
+
);
|
|
16325
|
+
this.#reference?.setAttribute(
|
|
16326
|
+
"data-direction",
|
|
16327
|
+
referenceGeometry.direction,
|
|
16328
|
+
);
|
|
16329
|
+
const referencePath = this.#reference?.querySelector(
|
|
16330
|
+
".fig-angle-reference-path",
|
|
16331
|
+
);
|
|
16332
|
+
referencePath?.setAttribute(
|
|
16333
|
+
"d",
|
|
16334
|
+
[referenceGeometry.arc, referenceGeometry.arrow]
|
|
16335
|
+
.filter(Boolean)
|
|
16336
|
+
.join(" "),
|
|
16337
|
+
);
|
|
16338
|
+
referencePath?.toggleAttribute("hidden", !referenceGeometry.arc);
|
|
16339
|
+
if (this.#dial) {
|
|
16340
|
+
const defaultMin = this.#fromDegrees(0);
|
|
16341
|
+
const defaultMax = this.#fromDegrees(360);
|
|
16342
|
+
this.#dial.setAttribute(
|
|
16343
|
+
"aria-valuemin",
|
|
16344
|
+
String(this.#minimum() ?? Math.min(defaultMin, this.#angle)),
|
|
16345
|
+
);
|
|
16346
|
+
this.#dial.setAttribute(
|
|
16347
|
+
"aria-valuemax",
|
|
16348
|
+
String(this.#maximum() ?? Math.max(defaultMax, this.#angle)),
|
|
16349
|
+
);
|
|
16350
|
+
this.#dial.setAttribute("aria-valuenow", String(this.#angle));
|
|
16351
|
+
this.#dial.setAttribute(
|
|
16352
|
+
"aria-valuetext",
|
|
16353
|
+
`${this.#angle.toFixed(this.#precision)}${this.#unitSuffix()}`,
|
|
16354
|
+
);
|
|
16355
|
+
}
|
|
16356
|
+
if (this.#angleInput) {
|
|
16357
|
+
const value = this.#angle.toFixed(this.#precision);
|
|
16358
|
+
if (this.#angleInput.getAttribute("value") !== value) {
|
|
16359
|
+
this.#angleInput.setAttribute("value", value);
|
|
16360
|
+
}
|
|
16361
|
+
}
|
|
16362
|
+
if (this.#rotationSpan) {
|
|
16363
|
+
const rotations = Math.floor(Math.abs(degrees) / 360);
|
|
16364
|
+
this.#rotationSpan.textContent = rotations > 1 ? `×${rotations}` : "";
|
|
16365
|
+
this.#rotationSpan.hidden = rotations <= 1;
|
|
16366
|
+
}
|
|
16367
|
+
if (this.#resetButton) {
|
|
16368
|
+
this.#resetButton.hidden =
|
|
16369
|
+
this.#angle === this.#resolveDefaultValue();
|
|
16370
|
+
}
|
|
16371
|
+
}
|
|
16372
|
+
|
|
16373
|
+
#resetToDefault() {
|
|
16374
|
+
if (this.#isDisabled()) return;
|
|
16375
|
+
this.#setValue(this.#resolveDefaultValue());
|
|
16376
|
+
this.#emit("input");
|
|
16377
|
+
this.#emit("change");
|
|
16378
|
+
}
|
|
16379
|
+
|
|
16380
|
+
#pointerDegrees(event) {
|
|
16381
|
+
const rect = this.#surface.getBoundingClientRect();
|
|
16382
|
+
const centerX = rect.left + rect.width / 2;
|
|
16383
|
+
const centerY = rect.top + rect.height / 2;
|
|
16384
|
+
const dx = event.clientX - centerX;
|
|
16385
|
+
const dy = event.clientY - centerY;
|
|
16386
|
+
if (Math.hypot(dx, dy) < Math.min(rect.width, rect.height) * 0.08) {
|
|
16387
|
+
return null;
|
|
16388
|
+
}
|
|
16389
|
+
let degrees = (Math.atan2(dy, dx) * 180) / Math.PI;
|
|
16390
|
+
degrees = ((degrees % 360) + 360) % 360;
|
|
16391
|
+
if (event.shiftKey) degrees = Math.round(degrees / 15) * 15;
|
|
16392
|
+
return degrees;
|
|
16393
|
+
}
|
|
16394
|
+
|
|
16395
|
+
#updateFromPointer(event) {
|
|
16396
|
+
const pointerDegrees = this.#pointerDegrees(event);
|
|
16397
|
+
if (pointerDegrees === null) return;
|
|
16398
|
+
const currentDegrees = this.#toDegrees(this.#angle);
|
|
16399
|
+
let delta;
|
|
16400
|
+
if (this.#previousPointerAngle === null) {
|
|
16401
|
+
const currentNormalized = ((currentDegrees % 360) + 360) % 360;
|
|
16402
|
+
delta = pointerDegrees - currentNormalized;
|
|
16403
|
+
} else {
|
|
16404
|
+
delta = pointerDegrees - this.#previousPointerAngle;
|
|
16405
|
+
}
|
|
16406
|
+
if (delta > 180) delta -= 360;
|
|
16407
|
+
if (delta < -180) delta += 360;
|
|
16408
|
+
this.#previousPointerAngle = pointerDegrees;
|
|
16409
|
+
this.#setValue(this.#fromDegrees(currentDegrees + delta));
|
|
16410
|
+
this.#emit("input");
|
|
16411
|
+
}
|
|
16412
|
+
|
|
16413
|
+
#startPointerGesture(event) {
|
|
16414
|
+
if (this.#isDisabled() || event.button !== 0) return;
|
|
16415
|
+
event.preventDefault();
|
|
16416
|
+
this.#dial?.focus({ preventScroll: true });
|
|
16417
|
+
this.#activePointerId = event.pointerId;
|
|
16418
|
+
this.#previousPointerAngle = null;
|
|
16419
|
+
this.#surface.classList.add("dragging");
|
|
16420
|
+
try {
|
|
16421
|
+
this.#dial.setPointerCapture?.(event.pointerId);
|
|
16422
|
+
} catch {
|
|
16423
|
+
// Synthetic events and older browsers may not own an active pointer.
|
|
16424
|
+
}
|
|
16425
|
+
this.#updateFromPointer(event);
|
|
16426
|
+
}
|
|
16427
|
+
|
|
16428
|
+
#movePointerGesture(event) {
|
|
16429
|
+
if (event.pointerId !== this.#activePointerId) return;
|
|
16430
|
+
if (event.buttons === 0) {
|
|
16431
|
+
this.#endPointerGesture(event);
|
|
16432
|
+
return;
|
|
16433
|
+
}
|
|
16434
|
+
this.#updateFromPointer(event);
|
|
16435
|
+
}
|
|
16436
|
+
|
|
16437
|
+
#endPointerGesture(event) {
|
|
16438
|
+
if (event.pointerId !== this.#activePointerId) return;
|
|
16439
|
+
const pointerId = this.#activePointerId;
|
|
16440
|
+
this.#activePointerId = null;
|
|
16441
|
+
this.#previousPointerAngle = null;
|
|
16442
|
+
this.#surface?.classList.remove("dragging");
|
|
16443
|
+
if (this.#dial?.hasPointerCapture?.(pointerId)) {
|
|
16444
|
+
this.#dial.releasePointerCapture(pointerId);
|
|
16445
|
+
}
|
|
16446
|
+
this.#emit("change");
|
|
16447
|
+
}
|
|
16448
|
+
|
|
16449
|
+
#cancelPointerGesture() {
|
|
16450
|
+
this.#activePointerId = null;
|
|
16451
|
+
this.#previousPointerAngle = null;
|
|
16452
|
+
this.#surface?.classList.remove("dragging");
|
|
16453
|
+
}
|
|
16454
|
+
|
|
16455
|
+
#handleKeyDown(event) {
|
|
16456
|
+
if (this.#isDisabled()) return;
|
|
16457
|
+
let next = this.#angle;
|
|
16458
|
+
const direction =
|
|
16459
|
+
event.key === "ArrowLeft" || event.key === "ArrowDown"
|
|
16460
|
+
? -1
|
|
16461
|
+
: event.key === "ArrowRight" || event.key === "ArrowUp"
|
|
16462
|
+
? 1
|
|
16463
|
+
: 0;
|
|
16464
|
+
if (direction) {
|
|
16465
|
+
const increment = event.shiftKey ? this.#fromDegrees(15) : this.#step();
|
|
16466
|
+
next += direction * increment;
|
|
16467
|
+
} else if (event.key === "Home") {
|
|
16468
|
+
next = this.#minimum() ?? this.#fromDegrees(0);
|
|
16469
|
+
} else if (event.key === "End") {
|
|
16470
|
+
next = this.#maximum() ?? this.#fromDegrees(360);
|
|
16471
|
+
} else {
|
|
16472
|
+
return;
|
|
16473
|
+
}
|
|
16474
|
+
event.preventDefault();
|
|
16475
|
+
event.stopPropagation();
|
|
16476
|
+
this.#setValue(next);
|
|
16477
|
+
this.#emit("input");
|
|
16478
|
+
this.#emit("change");
|
|
16479
|
+
}
|
|
16480
|
+
|
|
16481
|
+
#handleTextInput(event) {
|
|
16482
|
+
event.stopPropagation();
|
|
16483
|
+
if (this.#isDisabled()) return;
|
|
16484
|
+
const value = Number(event.currentTarget.value);
|
|
16485
|
+
if (!Number.isFinite(value)) return;
|
|
16486
|
+
this.#setValue(value);
|
|
16487
|
+
this.#emit(event.type);
|
|
16488
|
+
}
|
|
16489
|
+
|
|
16490
|
+
#normalizeRawTextUnit(event) {
|
|
16491
|
+
if (!event.target?.matches?.("input")) return;
|
|
16492
|
+
const match = String(event.target.value).match(
|
|
16493
|
+
/^(-?\d*\.?\d+)\s*(deg|°|rad|turn|grad)$/i,
|
|
16494
|
+
);
|
|
16495
|
+
if (!match) return;
|
|
16496
|
+
const value = Number(match[1]);
|
|
16497
|
+
const sourceUnit = this.#normalizeUnit(match[2]);
|
|
16498
|
+
let degrees = value;
|
|
16499
|
+
if (sourceUnit === "rad") degrees = (value * 180) / Math.PI;
|
|
16500
|
+
else if (sourceUnit === "turn") degrees = value * 360;
|
|
16501
|
+
else if (sourceUnit === "grad") degrees = value * 0.9;
|
|
16502
|
+
event.target.value = String(this.#fromDegrees(degrees));
|
|
16503
|
+
}
|
|
16504
|
+
|
|
16505
|
+
#emit(type) {
|
|
16506
|
+
const outputValue = Number(this.value.toFixed(this.#precision));
|
|
16507
|
+
this.dispatchEvent(
|
|
16508
|
+
new CustomEvent(type, {
|
|
16509
|
+
bubbles: true,
|
|
16510
|
+
cancelable: true,
|
|
16511
|
+
detail: {
|
|
16512
|
+
value: outputValue,
|
|
16513
|
+
angle: outputValue,
|
|
16514
|
+
units: this.#units,
|
|
16515
|
+
},
|
|
16516
|
+
}),
|
|
16517
|
+
);
|
|
16518
|
+
}
|
|
16519
|
+
}
|
|
16520
|
+
figDefineElement("fig-angle", FigAngle);
|
|
16521
|
+
|
|
15663
16522
|
/**
|
|
15664
16523
|
* A 3D rotation control with an interactive cube preview.
|
|
15665
16524
|
* @attr {string} value - CSS transform string, e.g. "rotateX(20deg) rotateY(-35deg) rotateZ(0deg)".
|
|
@@ -17074,8 +17933,6 @@ class FigInputJoystick extends HTMLElement {
|
|
|
17074
17933
|
|
|
17075
17934
|
figDefineElement("fig-joystick", FigInputJoystick);
|
|
17076
17935
|
|
|
17077
|
-
|
|
17078
|
-
// FigInputAngle moved to fig-lab.js
|
|
17079
17936
|
// FigShimmer
|
|
17080
17937
|
class FigShimmer extends HTMLElement {
|
|
17081
17938
|
get durationPropertyName() {
|