@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/.cursor/skills/fig-lab/SKILL.md +1 -1
- package/dist/fig-editor.js +3 -3
- package/dist/fig-lab.js +1 -1
- package/dist/fig.js +2 -2
- package/fig-editor.js +93 -12
- package/fig-lab.js +20 -0
- package/fig.js +18 -1
- package/package.json +1 -1
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
|
|
4487
|
-
|
|
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