@rogieking/figui3 8.9.19 → 8.9.21
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 +4 -3
- package/.cursor/skills/propkit/SKILL.md +1 -1
- package/README.md +26 -4
- package/components.css +40 -10
- package/dist/components.css +1 -1
- package/dist/fig-editor.js +3 -3
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +2 -2
- package/dist/fig.css +1 -1
- package/dist/fig.js +4 -4
- package/fig-editor.js +125 -5
- package/fig-lab.css +46 -8
- package/fig-lab.js +497 -2
- package/fig.js +29 -8
- package/package.json +1 -1
package/fig-editor.js
CHANGED
|
@@ -1668,6 +1668,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
1668
1668
|
opacity: 1,
|
|
1669
1669
|
};
|
|
1670
1670
|
#webcamPosterStream = null;
|
|
1671
|
+
#webcamSnapshotting = false;
|
|
1672
|
+
#webcamImageCapture = null;
|
|
1671
1673
|
|
|
1672
1674
|
// Custom mode slots and data
|
|
1673
1675
|
#customSlots = {};
|
|
@@ -1779,8 +1781,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
1779
1781
|
|
|
1780
1782
|
#applyParsedWebcam(parsed) {
|
|
1781
1783
|
if (parsed.webcam && typeof parsed.webcam === "object") {
|
|
1782
|
-
const { stream: _ignored, ...rest } = parsed.webcam;
|
|
1784
|
+
const { stream: _ignored, snapshot, ...rest } = parsed.webcam;
|
|
1783
1785
|
Object.assign(this.#webcam, rest);
|
|
1786
|
+
// Host writes can arrive as `{ snapshot: null }` while a live frame is
|
|
1787
|
+
// in flight — keep the captured still so the closed swatch can paint it.
|
|
1788
|
+
if (typeof snapshot === "string" && snapshot) {
|
|
1789
|
+
this.#webcam.snapshot = snapshot;
|
|
1790
|
+
}
|
|
1784
1791
|
return;
|
|
1785
1792
|
}
|
|
1786
1793
|
if (parsed.type === "webcam" && parsed.image) {
|
|
@@ -1822,12 +1829,96 @@ class FigFillPicker extends HTMLElement {
|
|
|
1822
1829
|
return this.#webcam.snapshot;
|
|
1823
1830
|
}
|
|
1824
1831
|
|
|
1832
|
+
#webcamFrameIsBlank(ctx, width, height) {
|
|
1833
|
+
if (typeof ctx.getImageData !== "function") return false;
|
|
1834
|
+
let data;
|
|
1835
|
+
try {
|
|
1836
|
+
data = ctx.getImageData(0, 0, width, height).data;
|
|
1837
|
+
} catch {
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
const step = Math.max(1, Math.floor((width * height) / 256));
|
|
1841
|
+
let max = 0;
|
|
1842
|
+
let lit = 0;
|
|
1843
|
+
let samples = 0;
|
|
1844
|
+
for (let i = 0; i < data.length; i += step * 4) {
|
|
1845
|
+
const peak = Math.max(data[i], data[i + 1], data[i + 2]);
|
|
1846
|
+
max = Math.max(max, peak);
|
|
1847
|
+
samples += 1;
|
|
1848
|
+
if (peak > 24) lit += 1;
|
|
1849
|
+
}
|
|
1850
|
+
if (!samples) return true;
|
|
1851
|
+
// Built-in cams often emit near-black boot frames with a bit of noise.
|
|
1852
|
+
return max <= 40 || lit / samples < 0.04;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
async #webcamFrameSources(video) {
|
|
1856
|
+
const sources = [];
|
|
1857
|
+
const track = this.#webcam.stream?.getVideoTracks?.()?.[0];
|
|
1858
|
+
if (track && typeof ImageCapture === "function") {
|
|
1859
|
+
try {
|
|
1860
|
+
if (this.#webcamImageCapture?.track !== track) {
|
|
1861
|
+
this.#webcamImageCapture = new ImageCapture(track);
|
|
1862
|
+
}
|
|
1863
|
+
sources.push(await this.#webcamImageCapture.grabFrame());
|
|
1864
|
+
} catch {
|
|
1865
|
+
this.#webcamImageCapture = null;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if (typeof createImageBitmap === "function") {
|
|
1869
|
+
try {
|
|
1870
|
+
sources.push(await createImageBitmap(video));
|
|
1871
|
+
} catch {
|
|
1872
|
+
/* video not readable yet */
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
sources.push(video);
|
|
1876
|
+
return sources.filter(Boolean);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
async #paintWebcamFrame(ctx, video, width, height) {
|
|
1880
|
+
const sources = await this.#webcamFrameSources(video);
|
|
1881
|
+
for (const source of sources) {
|
|
1882
|
+
try {
|
|
1883
|
+
ctx.drawImage(source, 0, 0, width, height);
|
|
1884
|
+
} catch {
|
|
1885
|
+
continue;
|
|
1886
|
+
} finally {
|
|
1887
|
+
source.close?.();
|
|
1888
|
+
}
|
|
1889
|
+
if (!this.#webcamFrameIsBlank(ctx, width, height)) return true;
|
|
1890
|
+
}
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
#waitForNextVideoFrame(video) {
|
|
1895
|
+
return new Promise((resolve) => {
|
|
1896
|
+
if (typeof video.requestVideoFrameCallback === "function") {
|
|
1897
|
+
const timer = setTimeout(() => resolve(false), 400);
|
|
1898
|
+
video.requestVideoFrameCallback(() => {
|
|
1899
|
+
clearTimeout(timer);
|
|
1900
|
+
resolve(true);
|
|
1901
|
+
});
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
setTimeout(() => resolve(false), 200);
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1825
1908
|
async #snapshotWebcamVideo(video) {
|
|
1826
1909
|
if (!video?.videoWidth || !video.videoHeight) return null;
|
|
1827
1910
|
const canvas = document.createElement("canvas");
|
|
1828
1911
|
canvas.width = video.videoWidth;
|
|
1829
1912
|
canvas.height = video.videoHeight;
|
|
1830
|
-
canvas.getContext("2d"
|
|
1913
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
1914
|
+
if (!ctx) return null;
|
|
1915
|
+
const painted = await this.#paintWebcamFrame(
|
|
1916
|
+
ctx,
|
|
1917
|
+
video,
|
|
1918
|
+
canvas.width,
|
|
1919
|
+
canvas.height,
|
|
1920
|
+
);
|
|
1921
|
+
if (!painted) return null;
|
|
1831
1922
|
const blob = await new Promise((resolve) =>
|
|
1832
1923
|
canvas.toBlob(resolve, "image/png"),
|
|
1833
1924
|
);
|
|
@@ -1842,14 +1933,34 @@ class FigFillPicker extends HTMLElement {
|
|
|
1842
1933
|
this.#updateSwatch();
|
|
1843
1934
|
return;
|
|
1844
1935
|
}
|
|
1845
|
-
this.#
|
|
1846
|
-
this.#
|
|
1936
|
+
if (this.#webcamSnapshotting) return;
|
|
1937
|
+
this.#webcamSnapshotting = true;
|
|
1938
|
+
|
|
1939
|
+
const finish = (url) => {
|
|
1940
|
+
this.#webcamSnapshotting = false;
|
|
1847
1941
|
if (!url || this.#fillType !== "webcam" || this.#webcam.stream !== stream) {
|
|
1848
1942
|
return;
|
|
1849
1943
|
}
|
|
1944
|
+
this.#webcamPosterStream = stream;
|
|
1850
1945
|
this.#updateSwatch();
|
|
1851
1946
|
this.#emitInput();
|
|
1852
|
-
}
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
const attempt = async () => {
|
|
1950
|
+
const url = await this.#snapshotWebcamVideo(video);
|
|
1951
|
+
if (url) {
|
|
1952
|
+
finish(url);
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
if (this.#fillType !== "webcam" || this.#webcam.stream !== stream) {
|
|
1956
|
+
this.#webcamSnapshotting = false;
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
await this.#waitForNextVideoFrame(video);
|
|
1960
|
+
return attempt();
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
attempt();
|
|
1853
1964
|
}
|
|
1854
1965
|
|
|
1855
1966
|
#scheduleFrame(callback) {
|
|
@@ -2117,6 +2228,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
2117
2228
|
if (this.#isDisabled()) return;
|
|
2118
2229
|
if (!this.#dialog) {
|
|
2119
2230
|
this.#createDialog();
|
|
2231
|
+
} else {
|
|
2232
|
+
this.#dialog.anchor = this.anchorElement || this.#trigger;
|
|
2120
2233
|
}
|
|
2121
2234
|
|
|
2122
2235
|
this.#valueAtOpen = JSON.stringify(this.value);
|
|
@@ -2202,6 +2315,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
2202
2315
|
this.#webcam.stream = null;
|
|
2203
2316
|
}
|
|
2204
2317
|
this.#webcamPosterStream = null;
|
|
2318
|
+
this.#webcamSnapshotting = false;
|
|
2319
|
+
this.#webcamImageCapture = null;
|
|
2205
2320
|
const video = this.#dialog?.querySelector(
|
|
2206
2321
|
".fig-fill-picker-webcam-video",
|
|
2207
2322
|
);
|
|
@@ -4524,11 +4639,16 @@ class FigFillPicker extends HTMLElement {
|
|
|
4524
4639
|
};
|
|
4525
4640
|
video.addEventListener("loadedmetadata", updateFrameReadiness);
|
|
4526
4641
|
video.addEventListener("canplay", updateFrameReadiness);
|
|
4642
|
+
video.addEventListener("playing", updateFrameReadiness);
|
|
4527
4643
|
|
|
4528
4644
|
const attachPreview = (stream) => {
|
|
4529
4645
|
this.#webcam.stream = stream;
|
|
4530
4646
|
video.srcObject = stream;
|
|
4647
|
+
video.muted = true;
|
|
4648
|
+
video.playsInline = true;
|
|
4531
4649
|
video.style.display = "block";
|
|
4650
|
+
const play = video.play?.();
|
|
4651
|
+
if (play?.catch) play.catch(() => {});
|
|
4532
4652
|
updateFrameReadiness();
|
|
4533
4653
|
};
|
|
4534
4654
|
|
package/fig-lab.css
CHANGED
|
@@ -349,6 +349,7 @@ fig-content:has(> propskit-group[name]:last-child) {
|
|
|
349
349
|
:is(
|
|
350
350
|
propskit-switch,
|
|
351
351
|
propskit-color,
|
|
352
|
+
propskit-fill,
|
|
352
353
|
propskit-gradient,
|
|
353
354
|
propskit-select,
|
|
354
355
|
propskit-text,
|
|
@@ -372,6 +373,7 @@ fig-content:has(> propskit-group[name]:last-child) {
|
|
|
372
373
|
:is(
|
|
373
374
|
propskit-switch,
|
|
374
375
|
propskit-color,
|
|
376
|
+
propskit-fill,
|
|
375
377
|
propskit-gradient,
|
|
376
378
|
propskit-text,
|
|
377
379
|
propskit-number,
|
|
@@ -472,7 +474,8 @@ propskit-switch {
|
|
|
472
474
|
}
|
|
473
475
|
}
|
|
474
476
|
|
|
475
|
-
propskit-color
|
|
477
|
+
propskit-color,
|
|
478
|
+
propskit-fill {
|
|
476
479
|
--propskit-color-height: 1.5rem;
|
|
477
480
|
--propskit-color-vertical-padding: var(--spacer-1);
|
|
478
481
|
--propskit-color-horizontal-padding: var(--spacer-3);
|
|
@@ -551,16 +554,14 @@ propskit-color {
|
|
|
551
554
|
width: 33%;
|
|
552
555
|
min-width: 0;
|
|
553
556
|
margin-left: auto;
|
|
557
|
+
overflow: hidden;
|
|
558
|
+
contain: paint;
|
|
554
559
|
--width: 100%;
|
|
555
560
|
--height: var(--propskit-color-height);
|
|
556
561
|
--size: var(--propskit-color-height);
|
|
557
562
|
--border-radius: var(--radius-medium);
|
|
558
563
|
height: var(--propskit-color-height);
|
|
559
564
|
border-radius: var(--radius-medium);
|
|
560
|
-
|
|
561
|
-
&:hover {
|
|
562
|
-
outline: none !important;
|
|
563
|
-
}
|
|
564
565
|
}
|
|
565
566
|
}
|
|
566
567
|
}
|
|
@@ -652,11 +653,46 @@ propskit-gradient {
|
|
|
652
653
|
min-width: 0;
|
|
653
654
|
margin-left: auto;
|
|
654
655
|
border-radius: var(--radius-medium);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
655
659
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
660
|
+
:is(propskit-color, propskit-fill, propskit-gradient) {
|
|
661
|
+
outline: none;
|
|
662
|
+
|
|
663
|
+
fig-field:not([direction="vertical"]) {
|
|
664
|
+
outline: none;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
&:focus-within fig-field:not([direction="vertical"]),
|
|
668
|
+
&.has-popup-open fig-field:not([direction="vertical"]) {
|
|
669
|
+
outline: var(--figma-focus-outline);
|
|
670
|
+
outline-offset: var(--figma-focus-outline-offset);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/* Same split as fig-input-gradient: chrome outlines, inner focus does not. */
|
|
674
|
+
:is(fig-swatch, fig-chit, fig-input-gradient, fig-input-fill, fig-fill-picker) {
|
|
675
|
+
--selected: 0 !important;
|
|
676
|
+
|
|
677
|
+
&:hover,
|
|
678
|
+
&:focus,
|
|
679
|
+
&:active {
|
|
680
|
+
outline: 0 !important;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
&:focus-within:not(:focus) {
|
|
684
|
+
outline: 0 !important;
|
|
659
685
|
}
|
|
686
|
+
|
|
687
|
+
&:focus-within:not(:focus) :is(input, fig-swatch, fig-chit) {
|
|
688
|
+
outline: 0 !important;
|
|
689
|
+
outline-offset: 0 !important;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
input[type="color"]:is(:focus, :focus-visible) {
|
|
694
|
+
outline: 0 !important;
|
|
695
|
+
outline-offset: 0 !important;
|
|
660
696
|
}
|
|
661
697
|
}
|
|
662
698
|
|
|
@@ -1341,6 +1377,7 @@ propskit-slider {
|
|
|
1341
1377
|
:is(
|
|
1342
1378
|
propskit-switch,
|
|
1343
1379
|
propskit-color,
|
|
1380
|
+
propskit-fill,
|
|
1344
1381
|
propskit-gradient,
|
|
1345
1382
|
propskit-select,
|
|
1346
1383
|
propskit-text,
|
|
@@ -1365,6 +1402,7 @@ propskit-slider {
|
|
|
1365
1402
|
:is(
|
|
1366
1403
|
propskit-switch,
|
|
1367
1404
|
propskit-color,
|
|
1405
|
+
propskit-fill,
|
|
1368
1406
|
propskit-gradient,
|
|
1369
1407
|
propskit-select,
|
|
1370
1408
|
propskit-text,
|