@rogieking/figui3 8.9.21 → 8.9.23
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 +97 -15
- package/fig-lab.js +20 -0
- package/fig.js +34 -3
- 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",
|
|
@@ -4451,7 +4475,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
4451
4475
|
#applyDefaultVideo({ emit = true } = {}) {
|
|
4452
4476
|
if (this.#video.url) {
|
|
4453
4477
|
this.#video.missing = false;
|
|
4454
|
-
if (!this.#video.poster) this.#captureVideoPoster(this.#video.url
|
|
4478
|
+
if (!this.#video.poster) this.#captureVideoPoster(this.#video.url);
|
|
4455
4479
|
return;
|
|
4456
4480
|
}
|
|
4457
4481
|
const fallback = this.getAttribute("default-video");
|
|
@@ -4465,11 +4489,71 @@ class FigFillPicker extends HTMLElement {
|
|
|
4465
4489
|
".fig-fill-picker-video-preview",
|
|
4466
4490
|
);
|
|
4467
4491
|
if (preview) this.#updateVideoPreviewStyle(preview);
|
|
4468
|
-
this.#captureVideoPoster(fallback
|
|
4492
|
+
this.#captureVideoPoster(fallback);
|
|
4493
|
+
if (emit) this.#emitInput();
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4496
|
+
#ensureVideoPoster() {
|
|
4497
|
+
if (this.#fillType !== "video") return;
|
|
4498
|
+
const src = this.#video.url;
|
|
4499
|
+
if (!src || this.#video.poster) return;
|
|
4500
|
+
this.#captureVideoPoster(src);
|
|
4469
4501
|
}
|
|
4470
4502
|
|
|
4471
|
-
async #
|
|
4472
|
-
if (
|
|
4503
|
+
async #videoFrameBitmap(video) {
|
|
4504
|
+
if (typeof video.requestVideoFrameCallback === "function") {
|
|
4505
|
+
await Promise.race([
|
|
4506
|
+
new Promise((resolve) => video.requestVideoFrameCallback(() => resolve())),
|
|
4507
|
+
new Promise((resolve) => setTimeout(resolve, 80)),
|
|
4508
|
+
]);
|
|
4509
|
+
} else {
|
|
4510
|
+
try {
|
|
4511
|
+
await video.play();
|
|
4512
|
+
video.pause();
|
|
4513
|
+
} catch {
|
|
4514
|
+
/* use the current decoded frame */
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
if (typeof createImageBitmap === "function") {
|
|
4518
|
+
try {
|
|
4519
|
+
return await createImageBitmap(video);
|
|
4520
|
+
} catch {
|
|
4521
|
+
/* tainted or undecoded — fall back to drawImage(video) */
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
return null;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
async #blobFromVideoFrame(video) {
|
|
4528
|
+
const bitmap = await this.#videoFrameBitmap(video);
|
|
4529
|
+
const width = bitmap?.width || video.videoWidth;
|
|
4530
|
+
const height = bitmap?.height || video.videoHeight;
|
|
4531
|
+
if (!width || !height) {
|
|
4532
|
+
bitmap?.close?.();
|
|
4533
|
+
return null;
|
|
4534
|
+
}
|
|
4535
|
+
const canvas =
|
|
4536
|
+
typeof OffscreenCanvas === "function"
|
|
4537
|
+
? new OffscreenCanvas(width, height)
|
|
4538
|
+
: Object.assign(document.createElement("canvas"), { width, height });
|
|
4539
|
+
const ctx = canvas.getContext("2d");
|
|
4540
|
+
if (!ctx) {
|
|
4541
|
+
bitmap?.close?.();
|
|
4542
|
+
return null;
|
|
4543
|
+
}
|
|
4544
|
+
try {
|
|
4545
|
+
ctx.drawImage(bitmap || video, 0, 0, width, height);
|
|
4546
|
+
} finally {
|
|
4547
|
+
bitmap?.close?.();
|
|
4548
|
+
}
|
|
4549
|
+
return canvas.convertToBlob
|
|
4550
|
+
? canvas.convertToBlob({ type: "image/jpeg", quality: 0.85 })
|
|
4551
|
+
: new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.85));
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
async #captureVideoPoster(src, { emit = false } = {}) {
|
|
4555
|
+
if (!src || this.#videoPosterCapturing === src) return;
|
|
4556
|
+
this.#videoPosterCapturing = src;
|
|
4473
4557
|
const video = document.createElement("video");
|
|
4474
4558
|
video.muted = true;
|
|
4475
4559
|
video.playsInline = true;
|
|
@@ -4483,14 +4567,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
4483
4567
|
video.src = src;
|
|
4484
4568
|
});
|
|
4485
4569
|
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;
|
|
4570
|
+
const blob = await this.#blobFromVideoFrame(video);
|
|
4571
|
+
if (!blob || this.#video.url !== src) return;
|
|
4494
4572
|
this.#revokeVideoPoster();
|
|
4495
4573
|
this.#video.poster = URL.createObjectURL(blob);
|
|
4496
4574
|
this.#ownedBlobUrls.add(this.#video.poster);
|
|
@@ -4498,6 +4576,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
4498
4576
|
if (emit) this.#emitInput();
|
|
4499
4577
|
} catch {
|
|
4500
4578
|
// Cross-origin or decode failures leave the swatch empty until a poster exists.
|
|
4579
|
+
} finally {
|
|
4580
|
+
video.removeAttribute("src");
|
|
4581
|
+
video.load();
|
|
4582
|
+
if (this.#videoPosterCapturing === src) this.#videoPosterCapturing = null;
|
|
4501
4583
|
}
|
|
4502
4584
|
}
|
|
4503
4585
|
|
|
@@ -4523,7 +4605,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
4523
4605
|
this.#video.url = src;
|
|
4524
4606
|
this.#video.missing = false;
|
|
4525
4607
|
this.#updateVideoPreviewStyle(preview);
|
|
4526
|
-
preview.play?.();
|
|
4527
4608
|
this.#captureVideoPoster(src);
|
|
4528
4609
|
this.#updateSwatch();
|
|
4529
4610
|
this.#emitInput();
|
|
@@ -5186,6 +5267,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
5186
5267
|
case "value":
|
|
5187
5268
|
this.#parseValue();
|
|
5188
5269
|
this.#updateSwatch();
|
|
5270
|
+
this.#ensureVideoPoster();
|
|
5189
5271
|
if (this.#dialog?.open && !this.#isDraggingColor) {
|
|
5190
5272
|
this.#refreshDialogUI();
|
|
5191
5273
|
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
|
}
|
|
@@ -10631,8 +10648,17 @@ class FigInputGradient extends HTMLElement {
|
|
|
10631
10648
|
.join(", ");
|
|
10632
10649
|
const interp = gradientInterpolationClause(gradient);
|
|
10633
10650
|
const interpolation = interp ? ` ${interp}` : "";
|
|
10634
|
-
|
|
10635
|
-
|
|
10651
|
+
if (this.#editMode === "picker") {
|
|
10652
|
+
switch (gradient.type) {
|
|
10653
|
+
case "radial":
|
|
10654
|
+
return `radial-gradient(circle at ${gradient.centerX}% ${gradient.centerY}%${interpolation}, ${stops})`;
|
|
10655
|
+
case "angular":
|
|
10656
|
+
return `conic-gradient(from ${gradient.angle}deg${interpolation}, ${stops})`;
|
|
10657
|
+
default:
|
|
10658
|
+
return `linear-gradient(${gradient.angle}deg${interpolation}, ${stops})`;
|
|
10659
|
+
}
|
|
10660
|
+
}
|
|
10661
|
+
// Inline editor swatch previews L→R so stop handles share that axis.
|
|
10636
10662
|
return `linear-gradient(to right${interpolation}, ${stops})`;
|
|
10637
10663
|
}
|
|
10638
10664
|
|
|
@@ -10744,6 +10770,11 @@ class FigInputGradient extends HTMLElement {
|
|
|
10744
10770
|
...this.#gradient,
|
|
10745
10771
|
...detail.gradient,
|
|
10746
10772
|
});
|
|
10773
|
+
if (this.#editMode === "picker" && typeof detail.css === "string" && detail.css) {
|
|
10774
|
+
this.#swatch?.setAttribute("background", detail.css);
|
|
10775
|
+
this.#syncSwatchSize();
|
|
10776
|
+
return;
|
|
10777
|
+
}
|
|
10747
10778
|
this.#syncSwatch();
|
|
10748
10779
|
};
|
|
10749
10780
|
|
package/package.json
CHANGED