@rogieking/figui3 9.0.1 → 9.0.2

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/fig.js CHANGED
@@ -14054,15 +14054,18 @@ figDefineElement("fig-input-file", FigInputFile);
14054
14054
 
14055
14055
  /**
14056
14056
  * A bezier / spring easing curve editor with draggable control points.
14057
- * @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
14057
+ * @attr {string} value - Bezier/spring shorthand or a serialized easing object
14058
14058
  * @attr {string} mode - Optional editor constraint: "bezier" or "spring".
14059
14059
  * @attr {number} precision - Decimal places for output values (default 2)
14060
14060
  * @attr {boolean} edit - Show the editor and custom preset options (default true; set "false" for presets only)
14061
+ * @attr {boolean} presets - Show the preset selector (default true)
14062
+ * @attr {boolean} text - Show the manual value input (default true)
14063
+ * @property {object|null} spring - Structured spring configuration when in spring mode
14061
14064
  */
14062
14065
  class FigEasingCurve extends HTMLElement {
14063
14066
  #cp1 = { x: 0.42, y: 0 };
14064
14067
  #cp2 = { x: 0.58, y: 1 };
14065
- #spring = { stiffness: 200, damping: 15, mass: 1 };
14068
+ #spring = { stiffness: 200, damping: 15, mass: 1, initialVelocity: 0 };
14066
14069
  #mode = "bezier";
14067
14070
  #precision = 2;
14068
14071
  #isDragging = null;
@@ -14155,7 +14158,15 @@ class FigEasingCurve extends HTMLElement {
14155
14158
  ];
14156
14159
 
14157
14160
  static get observedAttributes() {
14158
- return ["value", "mode", "precision", "aspect-ratio", "edit"];
14161
+ return [
14162
+ "value",
14163
+ "mode",
14164
+ "precision",
14165
+ "aspect-ratio",
14166
+ "edit",
14167
+ "presets",
14168
+ "text",
14169
+ ];
14159
14170
  }
14160
14171
 
14161
14172
  connectedCallback() {
@@ -14191,12 +14202,15 @@ class FigEasingCurve extends HTMLElement {
14191
14202
  return;
14192
14203
  }
14193
14204
 
14194
- if (name === "edit") {
14205
+ if (name === "edit" || name === "presets" || name === "text") {
14195
14206
  if (this.isConnected) this.#render();
14196
14207
  return;
14197
14208
  }
14198
14209
 
14199
14210
  if (name === "mode") {
14211
+ this.#applyModeConstraint();
14212
+ const value = this.getAttribute("value");
14213
+ if (value) this.#parseValue(value);
14200
14214
  this.#applyModeConstraint();
14201
14215
  this.#presetName = this.#matchPreset();
14202
14216
  if (this.isConnected) this.#render();
@@ -14222,8 +14236,9 @@ class FigEasingCurve extends HTMLElement {
14222
14236
 
14223
14237
  get value() {
14224
14238
  if (this.#mode === "spring") {
14225
- const { stiffness, damping, mass } = this.#spring;
14226
- return `spring(${stiffness}, ${damping}, ${mass})`;
14239
+ const { stiffness, damping, mass, initialVelocity = 0 } = this.#spring;
14240
+ const velocity = initialVelocity ? `, ${initialVelocity}` : "";
14241
+ return `spring(${stiffness}, ${damping}, ${mass}${velocity})`;
14227
14242
  }
14228
14243
  const p = this.#precision;
14229
14244
  return `${this.#cp1.x.toFixed(p)}, ${this.#cp1.y.toFixed(p)}, ${this.#cp2.x.toFixed(p)}, ${this.#cp2.y.toFixed(p)}`;
@@ -14251,9 +14266,39 @@ class FigEasingCurve extends HTMLElement {
14251
14266
  }
14252
14267
 
14253
14268
  set value(v) {
14269
+ if (v && typeof v === "object") {
14270
+ const normalized = this.#normalizeStructuredValue(v);
14271
+ if (!normalized) {
14272
+ throw new TypeError("Invalid easing value or mode/type mismatch");
14273
+ }
14274
+ const serialized =
14275
+ normalized.type === "spring"
14276
+ ? { type: "spring", ...normalized.spring }
14277
+ : {
14278
+ type: "bezier",
14279
+ x1: normalized.bezier[0],
14280
+ y1: normalized.bezier[1],
14281
+ x2: normalized.bezier[2],
14282
+ y2: normalized.bezier[3],
14283
+ };
14284
+ this.setAttribute("value", JSON.stringify(serialized));
14285
+ return;
14286
+ }
14254
14287
  this.setAttribute("value", v);
14255
14288
  }
14256
14289
 
14290
+ get spring() {
14291
+ if (this.#mode !== "spring") return null;
14292
+ return { ...this.#spring, initialVelocity: this.#spring.initialVelocity ?? 0 };
14293
+ }
14294
+
14295
+ set spring(value) {
14296
+ if (!value || typeof value !== "object") {
14297
+ throw new TypeError("spring must be an object");
14298
+ }
14299
+ this.value = { ...value, type: "spring" };
14300
+ }
14301
+
14257
14302
  #constrainedMode() {
14258
14303
  const mode = this.getAttribute("mode")?.trim().toLowerCase();
14259
14304
  return mode === "bezier" || mode === "spring" ? mode : null;
@@ -14269,15 +14314,114 @@ class FigEasingCurve extends HTMLElement {
14269
14314
  return !mode || preset.type === mode;
14270
14315
  }
14271
14316
 
14272
- #parseValue(str) {
14273
- const springMatch = str.match(
14274
- /^spring\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/,
14275
- );
14276
- if (springMatch && this.#constrainedMode() !== "bezier") {
14317
+ #normalizeStructuredValue(value) {
14318
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
14319
+ const constrainedMode = this.#constrainedMode();
14320
+ const declaredType =
14321
+ typeof value.type === "string" ? value.type.trim().toLowerCase() : null;
14322
+ if (declaredType && declaredType !== "bezier" && declaredType !== "spring") {
14323
+ return null;
14324
+ }
14325
+ if (declaredType && constrainedMode && declaredType !== constrainedMode) {
14326
+ return null;
14327
+ }
14328
+ const type = declaredType || constrainedMode;
14329
+ if (!type) return null;
14330
+
14331
+ if (type === "spring") {
14332
+ const spring = {
14333
+ mass: Number(value.mass),
14334
+ stiffness: Number(value.stiffness),
14335
+ damping: Number(value.damping),
14336
+ initialVelocity:
14337
+ value.initialVelocity === undefined
14338
+ ? 0
14339
+ : Number(value.initialVelocity),
14340
+ };
14341
+ if (
14342
+ !Number.isFinite(spring.mass) ||
14343
+ spring.mass <= 0 ||
14344
+ !Number.isFinite(spring.stiffness) ||
14345
+ spring.stiffness <= 0 ||
14346
+ !Number.isFinite(spring.damping) ||
14347
+ spring.damping < 0 ||
14348
+ !Number.isFinite(spring.initialVelocity)
14349
+ ) {
14350
+ return null;
14351
+ }
14352
+ return { type, spring };
14353
+ }
14354
+
14355
+ const bezier = [value.x1, value.y1, value.x2, value.y2].map(Number);
14356
+ if (
14357
+ bezier.some((part) => !Number.isFinite(part)) ||
14358
+ bezier[0] < 0 ||
14359
+ bezier[0] > 1 ||
14360
+ bezier[2] < 0 ||
14361
+ bezier[2] > 1
14362
+ ) {
14363
+ return null;
14364
+ }
14365
+ return { type, bezier };
14366
+ }
14367
+
14368
+ #applyStructuredValue(value) {
14369
+ if (value.type === "spring") {
14370
+ this.#mode = "spring";
14371
+ this.#spring = { ...value.spring };
14372
+ } else {
14373
+ this.#mode = "bezier";
14374
+ [this.#cp1.x, this.#cp1.y, this.#cp2.x, this.#cp2.y] = value.bezier;
14375
+ }
14376
+ }
14377
+
14378
+ #parseSpringString(str) {
14379
+ const match = str.match(/^spring\(\s*(.*?)\s*\)$/i);
14380
+ if (!match) return null;
14381
+ const parts = match[1].split(",").map((part) => Number(part.trim()));
14382
+ if (
14383
+ (parts.length !== 3 && parts.length !== 4) ||
14384
+ parts.some((part) => !Number.isFinite(part))
14385
+ ) {
14386
+ return null;
14387
+ }
14388
+ const spring = {
14389
+ stiffness: parts[0],
14390
+ damping: parts[1],
14391
+ mass: parts[2],
14392
+ initialVelocity: parts[3] ?? 0,
14393
+ };
14394
+ if (
14395
+ spring.stiffness <= 0 ||
14396
+ spring.damping < 0 ||
14397
+ spring.mass <= 0
14398
+ ) {
14399
+ return null;
14400
+ }
14401
+ return spring;
14402
+ }
14403
+
14404
+ #parseValue(input) {
14405
+ if (input && typeof input === "object") {
14406
+ const normalized = this.#normalizeStructuredValue(input);
14407
+ if (!normalized) return false;
14408
+ this.#applyStructuredValue(normalized);
14409
+ return true;
14410
+ }
14411
+ if (typeof input !== "string") return false;
14412
+ const str = input.trim();
14413
+ if (str.startsWith("{")) {
14414
+ try {
14415
+ return this.#parseValue(JSON.parse(str));
14416
+ } catch {
14417
+ return false;
14418
+ }
14419
+ }
14420
+
14421
+ const spring = this.#parseSpringString(str);
14422
+ if (spring && this.#constrainedMode() !== "bezier") {
14277
14423
  this.#mode = "spring";
14278
- this.#spring.stiffness = parseFloat(springMatch[1]);
14279
- this.#spring.damping = parseFloat(springMatch[2]);
14280
- this.#spring.mass = parseFloat(springMatch[3]);
14424
+ this.#spring = spring;
14281
14425
  return true;
14282
14426
  }
14283
14427
  const parts = str.split(",").map((s) => parseFloat(s.trim()));
@@ -14316,7 +14460,8 @@ class FigEasingCurve extends HTMLElement {
14316
14460
  if (
14317
14461
  Math.abs(this.#spring.stiffness - p.spring.stiffness) < ep &&
14318
14462
  Math.abs(this.#spring.damping - p.spring.damping) < ep &&
14319
- Math.abs(this.#spring.mass - p.spring.mass) < ep
14463
+ Math.abs(this.#spring.mass - p.spring.mass) < ep &&
14464
+ Math.abs(this.#spring.initialVelocity ?? 0) < ep
14320
14465
  )
14321
14466
  return p.name;
14322
14467
  }
@@ -14326,12 +14471,12 @@ class FigEasingCurve extends HTMLElement {
14326
14471
  // --- Spring simulation ---
14327
14472
 
14328
14473
  #simulateSpring() {
14329
- const { stiffness, damping, mass } = this.#spring;
14474
+ const { stiffness, damping, mass, initialVelocity = 0 } = this.#spring;
14330
14475
  const dt = 0.004;
14331
14476
  const maxTime = 5;
14332
14477
  const points = [];
14333
14478
  let pos = 0,
14334
- vel = 0;
14479
+ vel = initialVelocity;
14335
14480
  for (let t = 0; t <= maxTime; t += dt) {
14336
14481
  const force = -stiffness * (pos - 1) - damping * vel;
14337
14482
  vel += (force / mass) * dt;
@@ -14344,12 +14489,12 @@ class FigEasingCurve extends HTMLElement {
14344
14489
  }
14345
14490
 
14346
14491
  static #springIcon(spring, size = 24) {
14347
- const { stiffness, damping, mass } = spring;
14492
+ const { stiffness, damping, mass, initialVelocity = 0 } = spring;
14348
14493
  const dt = 0.004;
14349
14494
  const maxTime = 5;
14350
14495
  const pts = [];
14351
14496
  let pos = 0,
14352
- vel = 0;
14497
+ vel = initialVelocity;
14353
14498
  for (let t = 0; t <= maxTime; t += dt) {
14354
14499
  const force = -stiffness * (pos - 1) - damping * vel;
14355
14500
  vel += (force / mass) * dt;
@@ -14467,6 +14612,14 @@ class FigEasingCurve extends HTMLElement {
14467
14612
  return this.getAttribute("edit") !== "false";
14468
14613
  }
14469
14614
 
14615
+ #isPresetsEnabled() {
14616
+ return this.getAttribute("presets") !== "false";
14617
+ }
14618
+
14619
+ #isTextEnabled() {
14620
+ return this.getAttribute("text") !== "false";
14621
+ }
14622
+
14470
14623
  #render() {
14471
14624
  this.classList.toggle("spring-mode", this.#mode === "spring");
14472
14625
  this.classList.toggle("bezier-mode", this.#mode !== "spring");
@@ -14594,13 +14747,26 @@ class FigEasingCurve extends HTMLElement {
14594
14747
 
14595
14748
  #createContent() {
14596
14749
  const size = 200;
14597
- const select = this.#createSelect();
14598
- if (!this.#isEditEnabled()) return [select];
14599
- const valueInput = figCreateElement("fig-input-text", {
14600
- className: "fig-easing-curve-value-input",
14601
- value: this.value,
14602
- full: true,
14603
- });
14750
+ const select = this.#isPresetsEnabled() ? this.#createSelect() : null;
14751
+ if (!this.#isEditEnabled()) return select ? [select] : [];
14752
+ const valueInput = this.#isTextEnabled()
14753
+ ? this.#mode === "spring"
14754
+ ? figCreateElement("fig-input-number", {
14755
+ className: "fig-easing-curve-value-input",
14756
+ value: this.#springBounce().toFixed(2),
14757
+ min: "0",
14758
+ max: "1",
14759
+ step: "0.01",
14760
+ precision: "2",
14761
+ "aria-label": "Spring bounce",
14762
+ full: true,
14763
+ })
14764
+ : figCreateElement("fig-input-text", {
14765
+ className: "fig-easing-curve-value-input",
14766
+ value: this.value,
14767
+ full: true,
14768
+ })
14769
+ : null;
14604
14770
  const svgChildren = [
14605
14771
  figCreateSvgElement("rect", {
14606
14772
  className: "fig-easing-curve-bounds",
@@ -14686,13 +14852,13 @@ class FigEasingCurve extends HTMLElement {
14686
14852
  svgChildren,
14687
14853
  );
14688
14854
  return [
14689
- select,
14855
+ ...(select ? [select] : []),
14690
14856
  figCreateElement(
14691
14857
  "div",
14692
14858
  { className: "fig-easing-curve-svg-container" },
14693
14859
  svg,
14694
14860
  ),
14695
- valueInput,
14861
+ ...(valueInput ? [valueInput] : []),
14696
14862
  ];
14697
14863
  }
14698
14864
 
@@ -14981,7 +15147,43 @@ class FigEasingCurve extends HTMLElement {
14981
15147
 
14982
15148
  #syncValueInput() {
14983
15149
  if (!this.#valueInput) return;
14984
- this.#valueInput.setAttribute("value", this.value);
15150
+ const value =
15151
+ this.#mode === "spring"
15152
+ ? this.#springBounce().toFixed(2)
15153
+ : this.value;
15154
+ this.#valueInput.setAttribute("value", value);
15155
+ }
15156
+
15157
+ #springBounce() {
15158
+ const { stiffness, damping, mass } = this.#spring;
15159
+ const criticalDamping = 2 * Math.sqrt(stiffness * mass);
15160
+ if (!Number.isFinite(criticalDamping) || criticalDamping <= 0) return 0;
15161
+ return Math.max(0, Math.min(1, 1 - damping / criticalDamping));
15162
+ }
15163
+
15164
+ #setSpringBounce(value) {
15165
+ const bounce = Math.max(0, Math.min(1, Number(value)));
15166
+ const criticalDamping = 2 * Math.sqrt(
15167
+ this.#spring.stiffness * this.#spring.mass,
15168
+ );
15169
+ this.#spring.damping = Number(
15170
+ (criticalDamping * (1 - bounce)).toFixed(4),
15171
+ );
15172
+ }
15173
+
15174
+ #applySpringBounce(value, eventType) {
15175
+ const bounce = Number(value);
15176
+ if (!Number.isFinite(bounce)) {
15177
+ if (eventType === "change") this.#syncValueInput();
15178
+ return;
15179
+ }
15180
+
15181
+ this.#setSpringBounce(bounce);
15182
+ this.#presetName = this.#matchPreset();
15183
+ this.#updatePaths();
15184
+ this.#syncSelect();
15185
+ if (eventType === "change") this.#syncValueInput();
15186
+ this.#emit(eventType);
14985
15187
  }
14986
15188
 
14987
15189
  #parseManualBezierValue(value) {
@@ -14996,16 +15198,7 @@ class FigEasingCurve extends HTMLElement {
14996
15198
  }
14997
15199
 
14998
15200
  #parseManualSpringValue(value) {
14999
- const match = value.match(
15000
- /^spring\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/,
15001
- );
15002
- if (!match) return null;
15003
- const spring = {
15004
- stiffness: Number(match[1]),
15005
- damping: Number(match[2]),
15006
- mass: Number(match[3]),
15007
- };
15008
- return Object.values(spring).every((part) => part > 0) ? spring : null;
15201
+ return this.#parseSpringString(value);
15009
15202
  }
15010
15203
 
15011
15204
  #applyManualValue(value, eventType) {
@@ -15115,18 +15308,19 @@ class FigEasingCurve extends HTMLElement {
15115
15308
  const unit = event.shiftKey ? 10 : 1;
15116
15309
 
15117
15310
  if (handleType === "bounce") {
15311
+ const step = unit / 100;
15118
15312
  switch (event.key) {
15119
15313
  case "ArrowUp":
15120
- this.#spring.damping = Math.max(1, Math.round(this.#spring.damping - unit));
15314
+ this.#setSpringBounce(this.#springBounce() + step);
15121
15315
  break;
15122
15316
  case "ArrowDown":
15123
- this.#spring.damping = Math.max(1, Math.round(this.#spring.damping + unit));
15317
+ this.#setSpringBounce(this.#springBounce() - step);
15124
15318
  break;
15125
15319
  case "Home":
15126
- this.#spring.damping = 1;
15320
+ this.#setSpringBounce(0);
15127
15321
  break;
15128
15322
  case "End":
15129
- this.#spring.damping = 50;
15323
+ this.#setSpringBounce(1);
15130
15324
  break;
15131
15325
  default:
15132
15326
  return false;
@@ -15201,6 +15395,7 @@ class FigEasingCurve extends HTMLElement {
15201
15395
  // --- Events ---
15202
15396
 
15203
15397
  #emit(type) {
15398
+ const spring = this.spring;
15204
15399
  this.dispatchEvent(
15205
15400
  new CustomEvent(type, {
15206
15401
  bubbles: true,
@@ -15209,6 +15404,7 @@ class FigEasingCurve extends HTMLElement {
15209
15404
  value: this.value,
15210
15405
  cssValue: this.cssValue,
15211
15406
  preset: this.#presetName,
15407
+ ...(spring ? { spring } : {}),
15212
15408
  },
15213
15409
  }),
15214
15410
  );
@@ -15260,7 +15456,14 @@ class FigEasingCurve extends HTMLElement {
15260
15456
  }
15261
15457
 
15262
15458
  if (this.#select) {
15459
+ this.#select.addEventListener("input", (e) => {
15460
+ e.stopPropagation();
15461
+ });
15462
+ this.#select.addEventListener("optionhover", (e) => {
15463
+ e.stopPropagation();
15464
+ });
15263
15465
  this.#select.addEventListener("change", (e) => {
15466
+ e.stopPropagation();
15264
15467
  const name = e.detail;
15265
15468
  const preset = FigEasingCurve.PRESETS.find((p) => p.name === name);
15266
15469
  if (!preset) return;
@@ -15282,7 +15485,7 @@ class FigEasingCurve extends HTMLElement {
15282
15485
  }
15283
15486
  } else if (preset.type === "spring") {
15284
15487
  if (preset.spring) {
15285
- this.#spring = { ...preset.spring };
15488
+ this.#spring = { ...preset.spring, initialVelocity: 0 };
15286
15489
  }
15287
15490
  this.#presetName = name;
15288
15491
  if (this.#mode !== "spring") {
@@ -15302,14 +15505,20 @@ class FigEasingCurve extends HTMLElement {
15302
15505
  this.#valueInput.addEventListener("input", (e) => {
15303
15506
  e.stopPropagation();
15304
15507
  const value = e.detail ?? e.target?.value;
15305
- if (typeof value !== "string") return;
15306
- this.#applyManualValue(value, "input");
15508
+ if (this.#mode === "spring") {
15509
+ this.#applySpringBounce(value, "input");
15510
+ } else if (typeof value === "string") {
15511
+ this.#applyManualValue(value, "input");
15512
+ }
15307
15513
  });
15308
15514
  this.#valueInput.addEventListener("change", (e) => {
15309
15515
  e.stopPropagation();
15310
15516
  const value = e.detail ?? e.target?.value;
15311
- if (typeof value !== "string") return;
15312
- this.#applyManualValue(value, "change");
15517
+ if (this.#mode === "spring") {
15518
+ this.#applySpringBounce(value, "change");
15519
+ } else if (typeof value === "string") {
15520
+ this.#applyManualValue(value, "change");
15521
+ }
15313
15522
  });
15314
15523
  }
15315
15524
  }
@@ -15397,7 +15606,7 @@ class FigEasingCurve extends HTMLElement {
15397
15606
  this.#isDragging = handleType;
15398
15607
  this.classList.toggle("spring-bounce-dragging", handleType === "bounce");
15399
15608
 
15400
- const startDamping = this.#spring.damping;
15609
+ const startBounce = this.#springBounce();
15401
15610
  const startStiffness = this.#spring.stiffness;
15402
15611
  const startDuration = this.#springDuration;
15403
15612
  const startY = e.clientY;
@@ -15408,10 +15617,7 @@ class FigEasingCurve extends HTMLElement {
15408
15617
 
15409
15618
  if (handleType === "bounce") {
15410
15619
  const dy = e.clientY - startY;
15411
- this.#spring.damping = Math.max(
15412
- 1,
15413
- Math.round(startDamping + dy * 0.15),
15414
- );
15620
+ this.#setSpringBounce(startBounce - dy / 200);
15415
15621
  } else {
15416
15622
  const dx = e.clientX - startX;
15417
15623
  this.#springDuration = Math.max(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "9.0.1",
3
+ "version": "9.0.2",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "SEE LICENSE IN LICENSE",