Files
tauri-shadcn-vite-project/picker.html
shenjianZ 927eaa1e03 feat: 实现命令面板、颜色取色、JSON格式化和系统信息功能
- 重构项目架构,采用四层架构模式 (Command → Service → Platform → Utils)
  - 实现命令面板功能,支持快捷搜索和特征分类
  - 添加颜色取色功能,支持屏幕像素颜色获取
  - 添加JSON格式化功能,支持JSON格式化和压缩
  - 添加系统信息功能,显示操作系统和硬件信息
  - 移除旧的状态文档和无用配置文件
2026-02-10 18:46:11 +08:00

258 lines
8.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>取色遮罩</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
background-color: transparent;
cursor: crosshair; /* 关键CSS 十字光标 */
user-select: none;
-webkit-user-select: none;
}
/* 添加一个极其透明的背景,确保能捕获鼠标事件 */
#overlay {
width: 100%;
height: 100%;
/* 保持完全透明,避免影响底层颜色的合成结果 */
background-color: transparent;
position: relative;
}
#magnifier {
position: fixed;
pointer-events: none;
display: block;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 14px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
background: rgba(20, 20, 20, 0.75);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 10px;
width: 260px;
height: 300px;
z-index: 9999;
}
#mag-canvas {
width: 240px;
height: 240px;
border-radius: 10px;
display: block;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.12);
}
#mag-info {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
color: rgba(255, 255, 255, 0.92);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-size: 13px;
}
#mag-swatch {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.18);
}
</style>
</head>
<body>
<div id="overlay"></div>
<div id="magnifier">
<canvas id="mag-canvas" width="240" height="240"></canvas>
<div id="mag-info">
<div id="mag-hex">#------</div>
<div id="mag-swatch"></div>
</div>
</div>
<!--
注意:此页面是 Vite 的独立入口,会被打包到 dist/picker.html。
因此可以直接使用 @tauri-apps/api 的 ESM 导入。
-->
<script type="module">
import { invoke } from "@tauri-apps/api/core";
import { emit } from "@tauri-apps/api/event";
const overlay = document.getElementById("overlay");
const magnifier = document.getElementById("magnifier");
const magCanvas = document.getElementById("mag-canvas");
const magHex = document.getElementById("mag-hex");
const magSwatch = document.getElementById("mag-swatch");
const magCtx = magCanvas.getContext("2d", { willReadFrequently: false });
const tmpCanvas = document.createElement("canvas");
const tmpCtx = tmpCanvas.getContext("2d");
// 初始隐藏到屏幕外,避免一开始挡住视线
magnifier.style.transform = "translate(-9999px, -9999px)";
const CAPTURE_SIZE = 21; // 必须是奇数,中心点用于取色
const SCALE = 10; // 放大倍率21*10=210canvas 240 留边)
const DRAW_SIZE = CAPTURE_SIZE * SCALE;
const OFFSET_X = 26;
const OFFSET_Y = 26;
let lastClientX = 0;
let lastClientY = 0;
let lastScreenX = 0;
let lastScreenY = 0;
let pending = false;
let lastCaptureAt = 0;
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
function positionMagnifier(clientX, clientY) {
const w = magnifier.offsetWidth || 260;
const h = magnifier.offsetHeight || 300;
const maxX = window.innerWidth - w - 8;
const maxY = window.innerHeight - h - 8;
// 放到鼠标右下角,避免把被采样区域遮住(防止递归镜像)
const x = clamp(clientX + OFFSET_X, 8, Math.max(8, maxX));
const y = clamp(clientY + OFFSET_Y, 8, Math.max(8, maxY));
magnifier.style.transform = `translate(${x}px, ${y}px)`;
}
async function captureAndRender() {
pending = false;
if (!magCtx || !tmpCtx) return;
const now = performance.now();
// 节流:最多 30 FPS
if (now - lastCaptureAt < 33) return;
lastCaptureAt = now;
const dpr = window.devicePixelRatio || 1;
const px = Math.round(lastScreenX * dpr);
const py = Math.round(lastScreenY * dpr);
const half = Math.floor(CAPTURE_SIZE / 2);
try {
const res = await invoke("capture_screen_region_rgba", {
x: px - half,
y: py - half,
width: CAPTURE_SIZE,
height: CAPTURE_SIZE,
});
// res.data 是 RGBA Uint8 数组(通过 JSON 传输会变成 number[]
const u8 = new Uint8ClampedArray(res.data);
const imageData = new ImageData(u8, res.width, res.height);
// 先画到临时 canvas原尺寸再按像素风格放大避免每帧创建对象
tmpCanvas.width = res.width;
tmpCanvas.height = res.height;
tmpCtx.putImageData(imageData, 0, 0);
magCtx.clearRect(0, 0, magCanvas.width, magCanvas.height);
magCtx.imageSmoothingEnabled = false;
// 居中绘制
const dx = Math.floor((magCanvas.width - DRAW_SIZE) / 2);
const dy = Math.floor((magCanvas.height - DRAW_SIZE) / 2);
magCtx.drawImage(tmpCanvas, dx, dy, DRAW_SIZE, DRAW_SIZE);
// 网格线(轻微)
magCtx.strokeStyle = "rgba(255,255,255,0.08)";
magCtx.lineWidth = 1;
for (let i = 0; i <= CAPTURE_SIZE; i++) {
const gx = dx + i * SCALE + 0.5;
const gy = dy + i * SCALE + 0.5;
magCtx.beginPath();
magCtx.moveTo(gx, dy);
magCtx.lineTo(gx, dy + DRAW_SIZE);
magCtx.stroke();
magCtx.beginPath();
magCtx.moveTo(dx, gy);
magCtx.lineTo(dx + DRAW_SIZE, gy);
magCtx.stroke();
}
// 中心十字
const cx = dx + half * SCALE;
const cy = dy + half * SCALE;
magCtx.strokeStyle = "rgba(255,255,255,0.9)";
magCtx.lineWidth = 2;
magCtx.strokeRect(cx, cy, SCALE, SCALE);
// 更新颜色信息
magHex.textContent = res.center_hex;
magSwatch.style.backgroundColor = res.center_hex;
} catch (e) {
// 静默失败,避免刷屏
}
}
overlay.addEventListener("mousemove", (e) => {
lastClientX = e.clientX;
lastClientY = e.clientY;
lastScreenX = e.screenX;
lastScreenY = e.screenY;
positionMagnifier(lastClientX, lastClientY);
if (!pending) {
pending = true;
requestAnimationFrame(captureAndRender);
}
});
overlay.addEventListener("click", async (e) => {
try {
// screenX/screenY 通常是 CSS 像素;后端 GetPixel 需要物理像素
const dpr = window.devicePixelRatio || 1;
const x = Math.round(e.screenX * dpr);
const y = Math.round(e.screenY * dpr);
// 取色前先隐藏遮罩窗口,确保拿到“最上层应用”的真实颜色
const color = await invoke("pick_color_at_point_topmost", {
x,
y,
});
await emit("color-picked", color);
await invoke("close_picker_window");
} catch (error) {
console.error("取色失败:", error);
await invoke("close_picker_window");
}
});
document.addEventListener("keydown", async (e) => {
if (e.key === "Escape") {
try {
await invoke("close_picker_window");
await emit("color-picker-cancelled");
} catch (error) {
console.error("关闭窗口失败:", error);
}
}
});
overlay.addEventListener("contextmenu", (e) => {
e.preventDefault();
});
</script>
</body>
</html>