使用手势在 Figma 中构建 UI
帖子最初在我的博客上分享。
自从最新版本的MediaPipe 手势检测机器学习模型发布以来,该模型可以检测多只手,我一直想尝试使用它来创建 UI,以下是几个小时内构建的快速原型的结果!
在开始这个项目之前,我还遇到了两个混合 TensorFlow.js 和 Figma 的项目,一个由Anthony DiSpezio 开发,用于将手势转换为表情符号,另一个由Siddharth Ahuja 开发,用于通过手势移动 Figma 的画布。
我之前从未制作过 Figma 插件,但决定研究一下,看看是否可以构建一个使用手部动作来设计 UI 的插件。
首先要知道的是,您无法在网络版本中测试您的插件,因此您需要在开发时安装桌面版本。
然后,即使您可以访问插件中的某些 Web API,出于安全原因,也不允许访问摄像头和麦克风,所以我必须弄清楚如何将手部数据发送到插件。
我的做法是使用Socket.io运行一个单独的 Web 应用程序来处理手部检测并通过 websockets 将特定事件发送到我的 Figma 插件。
以下是该架构的快速可视化:
使用 TensorFlow.js 进行手势检测
在我单独的网络应用程序中,我正在运行TensorFlow.js和手势检测模型来获取我的手和手指在屏幕上的坐标并创建一些自定义手势。
无需过多细节,以下是“缩放”手势的代码示例:
let leftThumbTip,
rightThumbTip,
leftIndexTip,
rightIndexTip,
leftIndexFingerDip,
rightIndexFingerDip,
rightMiddleFingerDip,
rightRingFingerDip,
rightMiddleFingerTip,
leftMiddleFingerTip,
leftMiddleFingerDip,
leftRingFingerTip,
leftRingFingerDip,
rightRingFingerTip;
if (hands && hands.length > 0) {
hands.map((hand) => {
if (hand.handedness === "Left") {
//---------------
// DETECT PALM
//---------------
leftMiddleFingerTip = hand.keypoints.find(
(p) => p.name === "middle_finger_tip"
);
leftRingFingerTip = hand.keypoints.find(
(p) => p.name === "ring_finger_tip"
);
leftIndexFingerDip = hand.keypoints.find(
(p) => p.name === "index_finger_dip"
);
leftMiddleFingerDip = hand.keypoints.find(
(p) => p.name === "middle_finger_dip"
);
leftRingFingerDip = hand.keypoints.find(
(p) => p.name === "ring_finger_dip"
);
if (
leftIndexTip.y < leftIndexFingerDip.y &&
leftMiddleFingerTip.y < leftMiddleFingerDip.y &&
leftRingFingerTip.y < leftRingFingerDip.y
) {
palmLeft = true;
} else {
palmLeft = false;
}
} else {
//---------------
// DETECT PALM
//---------------
rightMiddleFingerTip = hand.keypoints.find(
(p) => p.name === "middle_finger_tip"
);
rightRingFingerTip = hand.keypoints.find(
(p) => p.name === "ring_finger_tip"
);
rightIndexFingerDip = hand.keypoints.find(
(p) => p.name === "index_finger_dip"
);
rightMiddleFingerDip = hand.keypoints.find(
(p) => p.name === "middle_finger_dip"
);
rightRingFingerDip = hand.keypoints.find(
(p) => p.name === "ring_finger_dip"
);
if (
rightIndexTip.y < rightIndexFingerDip.y &&
rightMiddleFingerTip.y < rightMiddleFingerDip.y &&
rightRingFingerTip.y < rightRingFingerDip.y
) {
palmRight = true;
} else {
palmRight = false;
}
if (palmRight && palmLeft) {
// zoom
socket.emit("zoom", rightMiddleFingerTip.x - leftMiddleFingerTip.x);
}
}
});
}
}
这段代码看起来有点乱,但这是故意的。目的是在花时间改进之前,先验证一下这个解决方案是否有效。
在这个示例中,我检查了食指、中指和无名指指尖的 y 坐标是否小于它们指尖的下沉 y 坐标,因为这意味着我的手指是伸直的,所以我做了某种“手掌”手势。一旦检测到
,我就会发出一个“缩放”事件,并发送右手中指和左手中指之间的 x 坐标差值来表示某种宽度。
带有 socket.io 的 Express 服务器
服务器端用于express
提供我的前端文件并socket.io
接收和发送消息。
以下是服务器监听事件zoom
并将其发送给其他应用程序的代码示例。
const express = require("express");
const app = express();
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);
app.use("/", express.static("public"));
io.on("connection", (socket) => {
console.log("a user connected");
socket.on("zoom", (e) => {
io.emit("zoom", e);
});
});
server.listen(8080, () => {
console.log("listening on *:8080");
});
Figma 插件
在 Figma 端,有两个部分。一个ui.html
文件通常负责显示插件的 UI,另一个code.js
文件负责逻辑。
我的 html 文件通过监听与 Express 服务器相同的端口来启动套接字连接,并将事件发送到我的 JavaScript 文件。
例如,这里有一个实现“缩放”功能的示例:
在ui.html
:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.4.1/socket.io.js"></script>
<script>
var socket = io("ws://localhost:8080", { transports: ["websocket"] });
</script>
<script>
// Zoom zoom
socket.on("zoom", (msg) => {
parent.postMessage({ pluginMessage: { type: "zoom", msg } }, "*");
});
</script>
在code.js
:
figma.showUI(__html__);
figma.ui.hide();
figma.ui.onmessage = (msg) => {
// Messages sent from ui.html
if (msg.type === "zoom") {
const normalizedZoom = normalize(msg.msg, 1200, 0);
figma.viewport.zoom = normalizedZoom;
}
};
const normalize = (val, max, min) =>
Math.max(0, Math.min(1, (val - min) / (max - min)));
根据 Figma 文档,缩放级别需要是 0 到 1 之间的数字,因此我将从手部检测应用程序获取的坐标标准化为 0 到 1 之间的值。
因此,当我将双手靠近或远离时,我会放大或缩小设计。
这是一个非常快速的演练,但从那里开始,任何来自前端的自定义手势都可以发送到 Figma 并用于触发图层、创建形状、更改颜色等!
必须运行单独的应用程序才能执行此操作并不是最佳选择,但我怀疑 Figma 是否会getUserMedia
在插件中启用对 Web API 的访问,因此与此同时,这是一个有趣的解决方法!