@rogieking/figui3 8.9.21 → 8.9.22

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-editor.js CHANGED
@@ -29,6 +29,18 @@ function figEditorCssUrl(url) {
29
29
  return `url("${String(url).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
30
30
  }
31
31
 
32
+ function figEditorLooksLikeVideoUrl(raw) {
33
+ const text = String(raw ?? "").trim();
34
+ if (!text || text.startsWith("{") || text.startsWith("#")) return false;
35
+ if (text.startsWith("data:video/")) return true;
36
+ try {
37
+ const path = new URL(text, "https://fig.local").pathname;
38
+ return /\.(mp4|webm|mov|m4v|ogv)$/i.test(path);
39
+ } catch {
40
+ return /\.(mp4|webm|mov|m4v|ogv)(?:[?#]|$)/i.test(text);
41
+ }
42
+ }
43
+
32
44
  function figEditorCreateIcon(name, options = {}) {
33
45
  const icon = document.createElement("fig-icon");
34
46
  if (name) icon.setAttribute("name", name);
@@ -1692,6 +1704,7 @@ class FigFillPicker extends HTMLElement {
1692
1704
  #boundTriggerKeydown = null;
1693
1705
  #rafIds = new Set();
1694
1706
  #ownedBlobUrls = new Set();
1707
+ #videoPosterCapturing = null;
1695
1708
 
1696
1709
  constructor() {
1697
1710
  super();
@@ -1721,6 +1734,7 @@ class FigFillPicker extends HTMLElement {
1721
1734
  this.#setupTrigger();
1722
1735
  this.#parseValue();
1723
1736
  this.#updateSwatch();
1737
+ this.#ensureVideoPoster();
1724
1738
  });
1725
1739
  }
1726
1740
 
@@ -2117,8 +2131,12 @@ class FigFillPicker extends HTMLElement {
2117
2131
  }
2118
2132
  if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
2119
2133
  if (parsed.video) {
2134
+ const prevUrl = this.#video.url;
2120
2135
  this.#video = { ...this.#video, ...parsed.video };
2121
2136
  this.#video.missing = !this.#video.url;
2137
+ if (this.#video.url !== prevUrl && parsed.video.poster == null) {
2138
+ this.#revokeVideoPoster();
2139
+ }
2122
2140
  }
2123
2141
  this.#applyParsedWebcam(parsed);
2124
2142
 
@@ -2128,10 +2146,17 @@ class FigFillPicker extends HTMLElement {
2128
2146
  this.#customData[parsed.type] = rest;
2129
2147
  }
2130
2148
  } catch (e) {
2131
- // If not JSON, treat as hex color
2132
2149
  if (valueAttr.startsWith("#")) {
2133
2150
  this.#fillType = "solid";
2134
2151
  this.#color = this.#hexToHSV(valueAttr);
2152
+ return;
2153
+ }
2154
+ if (figEditorLooksLikeVideoUrl(valueAttr)) {
2155
+ const prevUrl = this.#video.url;
2156
+ this.#fillType = "video";
2157
+ this.#video.url = valueAttr.trim();
2158
+ this.#video.missing = false;
2159
+ if (this.#video.url !== prevUrl) this.#revokeVideoPoster();
2135
2160
  }
2136
2161
  }
2137
2162
  }
@@ -4429,7 +4454,6 @@ class FigFillPicker extends HTMLElement {
4429
4454
  "aspect-ratio": "1/1",
4430
4455
  fit: "cover",
4431
4456
  checkerboard: "true",
4432
- autoplay: "true",
4433
4457
  controls: true,
4434
4458
  muted: "true",
4435
4459
  loop: "true",
@@ -4468,8 +4492,67 @@ class FigFillPicker extends HTMLElement {
4468
4492
  this.#captureVideoPoster(fallback, { emit });
4469
4493
  }
4470
4494
 
4495
+ #ensureVideoPoster({ emit = true } = {}) {
4496
+ if (this.#fillType !== "video") return;
4497
+ const src = this.#video.url;
4498
+ if (!src || this.#video.poster) return;
4499
+ this.#captureVideoPoster(src, { emit });
4500
+ }
4501
+
4502
+ async #videoFrameBitmap(video) {
4503
+ if (typeof video.requestVideoFrameCallback === "function") {
4504
+ await Promise.race([
4505
+ new Promise((resolve) => video.requestVideoFrameCallback(() => resolve())),
4506
+ new Promise((resolve) => setTimeout(resolve, 80)),
4507
+ ]);
4508
+ } else {
4509
+ try {
4510
+ await video.play();
4511
+ video.pause();
4512
+ } catch {
4513
+ /* use the current decoded frame */
4514
+ }
4515
+ }
4516
+ if (typeof createImageBitmap === "function") {
4517
+ try {
4518
+ return await createImageBitmap(video);
4519
+ } catch {
4520
+ /* tainted or undecoded — fall back to drawImage(video) */
4521
+ }
4522
+ }
4523
+ return null;
4524
+ }
4525
+
4526
+ async #blobFromVideoFrame(video) {
4527
+ const bitmap = await this.#videoFrameBitmap(video);
4528
+ const width = bitmap?.width || video.videoWidth;
4529
+ const height = bitmap?.height || video.videoHeight;
4530
+ if (!width || !height) {
4531
+ bitmap?.close?.();
4532
+ return null;
4533
+ }
4534
+ const canvas =
4535
+ typeof OffscreenCanvas === "function"
4536
+ ? new OffscreenCanvas(width, height)
4537
+ : Object.assign(document.createElement("canvas"), { width, height });
4538
+ const ctx = canvas.getContext("2d");
4539
+ if (!ctx) {
4540
+ bitmap?.close?.();
4541
+ return null;
4542
+ }
4543
+ try {
4544
+ ctx.drawImage(bitmap || video, 0, 0, width, height);
4545
+ } finally {
4546
+ bitmap?.close?.();
4547
+ }
4548
+ return canvas.convertToBlob
4549
+ ? canvas.convertToBlob({ type: "image/jpeg", quality: 0.85 })
4550
+ : new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.85));
4551
+ }
4552
+
4471
4553
  async #captureVideoPoster(src, { emit = true } = {}) {
4472
- if (!src) return;
4554
+ if (!src || this.#videoPosterCapturing === src) return;
4555
+ this.#videoPosterCapturing = src;
4473
4556
  const video = document.createElement("video");
4474
4557
  video.muted = true;
4475
4558
  video.playsInline = true;
@@ -4483,14 +4566,8 @@ class FigFillPicker extends HTMLElement {
4483
4566
  video.src = src;
4484
4567
  });
4485
4568
  if (!video.videoWidth || !video.videoHeight) return;
4486
- const canvas = document.createElement("canvas");
4487
- canvas.width = video.videoWidth;
4488
- canvas.height = video.videoHeight;
4489
- canvas.getContext("2d").drawImage(video, 0, 0);
4490
- const blob = await new Promise((resolve) =>
4491
- canvas.toBlob(resolve, "image/jpeg", 0.85),
4492
- );
4493
- if (!blob) return;
4569
+ const blob = await this.#blobFromVideoFrame(video);
4570
+ if (!blob || this.#video.url !== src) return;
4494
4571
  this.#revokeVideoPoster();
4495
4572
  this.#video.poster = URL.createObjectURL(blob);
4496
4573
  this.#ownedBlobUrls.add(this.#video.poster);
@@ -4498,6 +4575,10 @@ class FigFillPicker extends HTMLElement {
4498
4575
  if (emit) this.#emitInput();
4499
4576
  } catch {
4500
4577
  // Cross-origin or decode failures leave the swatch empty until a poster exists.
4578
+ } finally {
4579
+ video.removeAttribute("src");
4580
+ video.load();
4581
+ if (this.#videoPosterCapturing === src) this.#videoPosterCapturing = null;
4501
4582
  }
4502
4583
  }
4503
4584
 
@@ -4523,7 +4604,6 @@ class FigFillPicker extends HTMLElement {
4523
4604
  this.#video.url = src;
4524
4605
  this.#video.missing = false;
4525
4606
  this.#updateVideoPreviewStyle(preview);
4526
- preview.play?.();
4527
4607
  this.#captureVideoPoster(src);
4528
4608
  this.#updateSwatch();
4529
4609
  this.#emitInput();
@@ -5186,6 +5266,7 @@ class FigFillPicker extends HTMLElement {
5186
5266
  case "value":
5187
5267
  this.#parseValue();
5188
5268
  this.#updateSwatch();
5269
+ this.#ensureVideoPoster();
5189
5270
  if (this.#dialog?.open && !this.#isDraggingColor) {
5190
5271
  this.#refreshDialogUI();
5191
5272
  this.#lastChangeValue = JSON.stringify(this.value);
package/fig-lab.js CHANGED
@@ -728,6 +728,25 @@ function figLabCssUrl(url) {
728
728
  return `url("${String(url).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
729
729
  }
730
730
 
731
+ function figLabLooksLikeVideoUrl(raw) {
732
+ const text = String(raw ?? "").trim();
733
+ if (!text || text.startsWith("{") || text.startsWith("#")) return false;
734
+ if (text.startsWith("data:video/")) return true;
735
+ try {
736
+ const path = new URL(text, "https://fig.local").pathname;
737
+ return /\.(mp4|webm|mov|m4v|ogv)$/i.test(path);
738
+ } catch {
739
+ return /\.(mp4|webm|mov|m4v|ogv)(?:[?#]|$)/i.test(text);
740
+ }
741
+ }
742
+
743
+ function figLabVideoFillFromUrl(url) {
744
+ return {
745
+ type: "video",
746
+ video: { url, scaleMode: "fill", scale: 50 },
747
+ };
748
+ }
749
+
731
750
  function figLabParseFillValue(raw) {
732
751
  if (raw && typeof raw === "object") return raw;
733
752
  const text = String(raw ?? "").trim();
@@ -742,6 +761,7 @@ function figLabParseFillValue(raw) {
742
761
  const { color, alpha } = figLabParseSolidColor(text);
743
762
  return { type: "solid", color, alpha };
744
763
  }
764
+ if (figLabLooksLikeVideoUrl(text)) return figLabVideoFillFromUrl(text);
745
765
  return { type: "solid", color: "#D9D9D9", alpha: 1 };
746
766
  }
747
767
 
package/fig.js CHANGED
@@ -8684,6 +8684,18 @@ function figCssUrl(url) {
8684
8684
  return `url("${String(url).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
8685
8685
  }
8686
8686
 
8687
+ function figLooksLikeVideoUrl(raw) {
8688
+ const text = String(raw ?? "").trim();
8689
+ if (!text || text.startsWith("{") || text.startsWith("#")) return false;
8690
+ if (text.startsWith("data:video/")) return true;
8691
+ try {
8692
+ const path = new URL(text, "https://fig.local").pathname;
8693
+ return /\.(mp4|webm|mov|m4v|ogv)$/i.test(path);
8694
+ } catch {
8695
+ return /\.(mp4|webm|mov|m4v|ogv)(?:[?#]|$)/i.test(text);
8696
+ }
8697
+ }
8698
+
8687
8699
  function figHexToRGB(hex) {
8688
8700
  const h = hex.replace(/^#/, "");
8689
8701
  return {
@@ -9077,7 +9089,6 @@ class FigInputFill extends HTMLElement {
9077
9089
  }
9078
9090
  }
9079
9091
  } catch (e) {
9080
- // If not JSON, treat as hex color
9081
9092
  if (valueAttr.startsWith("#")) {
9082
9093
  this.#fillType = "solid";
9083
9094
  this.#solid.color = valueAttr.slice(0, 7);
@@ -9085,6 +9096,12 @@ class FigInputFill extends HTMLElement {
9085
9096
  const alphaHex = valueAttr.slice(7, 9);
9086
9097
  this.#solid.alpha = parseInt(alphaHex, 16) / 255;
9087
9098
  }
9099
+ return;
9100
+ }
9101
+ if (figLooksLikeVideoUrl(valueAttr)) {
9102
+ this.#fillType = "video";
9103
+ this.#video.url = valueAttr.trim();
9104
+ this.#video.missing = false;
9088
9105
  }
9089
9106
  }
9090
9107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.21",
3
+ "version": "8.9.22",
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": "MIT",