Files
NaviGlassServer/static/visualizer.js
2025-12-31 15:42:30 +08:00

546 lines
18 KiB
JavaScript

// static/visualizer.js
class TechVisualizer {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.ws = null;
this.data = {};
// 科技感配色方案
this.colors = {
primary: '#00D9FF', // 青蓝色
secondary: '#FF00FF', // 品红/紫色
accent: '#00FF88', // 青绿色
warning: '#FFAA00', // 橙色
background: '#000000', // 黑色
surface: '#0A0A0A', // 深灰
text: '#FFFFFF', // 白色
textMuted: '#888888', // 灰色
grid: '#1A1A1A', // 网格色
glow: '#00D9FF55' // 发光效果
};
// 字体设置
this.fonts = {
title: 'bold 24px "Orbitron", "Microsoft YaHei", sans-serif',
subtitle: 'bold 18px "Rajdhani", "Microsoft YaHei", sans-serif',
body: '16px "Roboto", "Microsoft YaHei", sans-serif',
small: '12px "Roboto", "Microsoft YaHei", sans-serif',
tiny: '10px "Roboto", sans-serif'
};
this.setupCanvas();
this.connectWebSocket();
}
setupCanvas() {
// 设置画布大小
const resizeCanvas = () => {
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * window.devicePixelRatio;
this.canvas.height = rect.height * window.devicePixelRatio;
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
};
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
}
connectWebSocket() {
const wsUrl = `ws://${window.location.host}/ws/visualizer`;
this.ws = new WebSocket(wsUrl);
this.ws.onmessage = (event) => {
try {
this.data = JSON.parse(event.data);
this.render();
} catch (e) {
console.error('Failed to parse visualization data:', e);
}
};
this.ws.onclose = () => {
setTimeout(() => this.connectWebSocket(), 1000);
};
}
render() {
const ctx = this.ctx;
const width = this.canvas.width / window.devicePixelRatio;
const height = this.canvas.height / window.devicePixelRatio;
// 清空画布
ctx.fillStyle = this.colors.background;
ctx.fillRect(0, 0, width, height);
// 绘制网格背景
this.drawGrid(width, height);
// 绘制HUD边框
this.drawHUD(width, height);
// 根据模式绘制内容
if (this.data.mode === 'SEGMENT') {
this.drawSegmentMode(width, height);
} else if (this.data.mode === 'FLASH') {
this.drawFlashMode(width, height);
} else if (this.data.mode === 'TRACK') {
this.drawTrackMode(width, height);
}
// 绘制手部骨骼
if (this.data.hand) {
this.drawHand(this.data.hand, width, height);
}
// 绘制FPS和状态信息
this.drawStats(width, height);
}
drawGrid(width, height) {
const ctx = this.ctx;
ctx.strokeStyle = this.colors.grid;
ctx.lineWidth = 0.5;
const gridSize = 50;
for (let x = 0; x < width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
for (let y = 0; y < height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
}
drawHUD(width, height) {
const ctx = this.ctx;
const margin = 20;
// 四角装饰
ctx.strokeStyle = this.colors.primary;
ctx.lineWidth = 2;
const cornerSize = 40;
// 左上角
ctx.beginPath();
ctx.moveTo(margin, margin + cornerSize);
ctx.lineTo(margin, margin);
ctx.lineTo(margin + cornerSize, margin);
ctx.stroke();
// 右上角
ctx.beginPath();
ctx.moveTo(width - margin - cornerSize, margin);
ctx.lineTo(width - margin, margin);
ctx.lineTo(width - margin, margin + cornerSize);
ctx.stroke();
// 左下角
ctx.beginPath();
ctx.moveTo(margin, height - margin - cornerSize);
ctx.lineTo(margin, height - margin);
ctx.lineTo(margin + cornerSize, height - margin);
ctx.stroke();
// 右下角
ctx.beginPath();
ctx.moveTo(width - margin - cornerSize, height - margin);
ctx.lineTo(width - margin, height - margin);
ctx.lineTo(width - margin, height - margin - cornerSize);
ctx.stroke();
}
drawSegmentMode(width, height) {
const ctx = this.ctx;
// 绘制检测到的物体
if (this.data.segments) {
this.data.segments.forEach((seg, index) => {
if (seg.contour && seg.contour.length > 0) {
// 绘制轮廓
ctx.beginPath();
ctx.strokeStyle = seg.is_target ? this.colors.primary : this.colors.secondary;
ctx.lineWidth = seg.is_target ? 3 : 2;
// 添加发光效果
if (seg.is_target) {
ctx.shadowColor = this.colors.primary;
ctx.shadowBlur = 10;
}
const points = this.scalePoints(seg.contour, width, height);
ctx.moveTo(points[0][0], points[0][1]);
points.forEach(p => ctx.lineTo(p[0], p[1]));
ctx.closePath();
ctx.stroke();
ctx.shadowBlur = 0;
// 如果是目标,绘制中心标记
if (seg.is_target) {
const center = this.getContourCenter(points);
this.drawTargetMarker(center[0], center[1]);
// 绘制面积信息
ctx.font = this.fonts.small;
ctx.fillStyle = this.colors.primary;
ctx.fillText(`Area: ${seg.area}`, center[0] + 20, center[1] - 20);
}
}
});
}
// 绘制状态文字
if (this.data.auto_lock && this.data.auto_lock.active) {
this.drawStatusText(
`目标锁定中 Locking Target`,
`${this.data.auto_lock.remaining.toFixed(1)}s`,
width / 2,
100,
this.colors.warning
);
} else {
this.drawStatusText(
'扫描中 Scanning',
'等待检测目标 Waiting for target',
width / 2,
100,
this.colors.primary
);
}
}
drawFlashMode(width, height) {
const ctx = this.ctx;
if (this.data.flash && this.data.flash.mask_contour) {
const progress = this.data.flash.progress || 0;
const alpha = 0.3 + 0.4 * (0.5 * (1 + Math.sin(progress * 2 * Math.PI - Math.PI/2)));
// 绘制闪烁轮廓
ctx.beginPath();
ctx.strokeStyle = this.colors.accent;
ctx.lineWidth = 4;
ctx.globalAlpha = alpha;
const points = this.scalePoints(this.data.flash.mask_contour, width, height);
ctx.moveTo(points[0][0], points[0][1]);
points.forEach(p => ctx.lineTo(p[0], p[1]));
ctx.closePath();
// 填充
ctx.fillStyle = this.colors.accent + '33';
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 1;
// 绘制锁定动画
const center = this.getContourCenter(points);
this.drawLockAnimation(center[0], center[1], progress);
}
this.drawStatusText(
'正在锁定目标 Locking Target',
'准备追踪 Preparing to track',
width / 2,
100,
this.colors.accent
);
}
drawTrackMode(width, height) {
const ctx = this.ctx;
const tracking = this.data.tracking;
if (!tracking) return;
// 绘制追踪多边形
if (tracking.polygon && tracking.polygon.length > 0) {
ctx.beginPath();
ctx.strokeStyle = this.colors.accent;
ctx.lineWidth = 3;
ctx.shadowColor = this.colors.accent;
ctx.shadowBlur = 15;
const points = this.scalePoints(tracking.polygon, width, height);
ctx.moveTo(points[0][0], points[0][1]);
points.forEach(p => ctx.lineTo(p[0], p[1]));
ctx.closePath();
ctx.stroke();
ctx.shadowBlur = 0;
// 绘制中心点
if (tracking.center) {
const center = this.scalePoint(tracking.center, width, height);
ctx.fillStyle = this.colors.accent;
ctx.beginPath();
ctx.arc(center[0], center[1], 6, 0, Math.PI * 2);
ctx.fill();
}
}
// 绘制进度条
this.drawProgressBars(tracking, width, height);
// 绘制引导文字
if (tracking.guidance) {
const guidanceText = {
'向前靠近': 'Move Closer',
'后退': 'Move Back',
'保持': 'Hold Position'
};
this.drawStatusText(
tracking.guidance,
guidanceText[tracking.guidance] || '',
width / 2,
height - 100,
this.colors.warning
);
}
// 如果触发了重新锁定
if (tracking.relock_triggered) {
this.drawStatusText(
'已根据周边检测刷新追踪',
'Tracking refreshed by peripheral detection',
width / 2,
170,
this.colors.accent
);
}
}
drawHand(handData, width, height) {
const ctx = this.ctx;
if (!handData.landmarks) return;
// 缩放坐标
const landmarks = handData.landmarks.map(p =>
this.scalePoint([p[0], p[1]], width, height)
);
// 绘制手部连接线
ctx.strokeStyle = this.colors.secondary;
ctx.lineWidth = 2;
ctx.globalAlpha = 0.8;
// MediaPipe手部连接定义
const connections = [
[0, 1], [1, 2], [2, 3], [3, 4], // 拇指
[0, 5], [5, 6], [6, 7], [7, 8], // 食指
[5, 9], [9, 10], [10, 11], [11, 12], // 中指
[9, 13], [13, 14], [14, 15], [15, 16], // 无名指
[13, 17], [17, 18], [18, 19], [19, 20], // 小指
[0, 17] // 手腕连接
];
connections.forEach(([start, end]) => {
ctx.beginPath();
ctx.moveTo(landmarks[start][0], landmarks[start][1]);
ctx.lineTo(landmarks[end][0], landmarks[end][1]);
ctx.stroke();
});
// 绘制关键点
landmarks.forEach((point, i) => {
ctx.fillStyle = this.colors.secondary;
ctx.beginPath();
ctx.arc(point[0], point[1], 3, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
// 绘制握持评分
if (handData.grasp_score !== undefined) {
ctx.font = this.fonts.body;
ctx.fillStyle = this.colors.text;
ctx.fillText(
`握持评分 Grasp Score: ${handData.grasp_score.toFixed(2)}`,
20,
80
);
}
}
drawProgressBars(tracking, width, height) {
const ctx = this.ctx;
const barWidth = width * 0.25;
const barHeight = 12;
const x = 20;
const y = height - 80;
// 对齐进度条
this.drawProgressBar(
x, y - 30,
barWidth, barHeight,
tracking.align_score || 0,
'对齐 Alignment',
this.colors.primary
);
// 距离进度条
this.drawProgressBar(
x, y,
barWidth, barHeight,
tracking.range_score || 0,
`距离 Distance (≈1)`,
this.colors.secondary
);
// 显示比率
if (tracking.ratio !== null && tracking.ratio !== undefined) {
ctx.font = this.fonts.small;
ctx.fillStyle = this.colors.text;
ctx.fillText(
`面积比 Ratio: ${tracking.ratio.toFixed(2)}`,
x + barWidth + 20,
y + 8
);
}
}
drawProgressBar(x, y, width, height, value, label, color) {
const ctx = this.ctx;
// 背景
ctx.fillStyle = this.colors.surface;
ctx.fillRect(x, y, width, height);
// 边框
ctx.strokeStyle = color + '44';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
// 填充
const fillWidth = width * Math.max(0, Math.min(1, value));
const gradient = ctx.createLinearGradient(x, y, x + fillWidth, y);
gradient.addColorStop(0, color + 'AA');
gradient.addColorStop(1, color);
ctx.fillStyle = gradient;
ctx.fillRect(x, y, fillWidth, height);
// 标签
ctx.font = this.fonts.small;
ctx.fillStyle = this.colors.textMuted;
ctx.fillText(label, x, y - 5);
}
drawStats(width, height) {
const ctx = this.ctx;
// FPS显示
ctx.font = this.fonts.body;
ctx.fillStyle = this.colors.accent;
ctx.fillText(`FPS: ${(this.data.fps || 0).toFixed(1)}`, 20, 40);
// 模式显示
const modeText = {
'SEGMENT': '分割模式 Segmentation',
'FLASH': '锁定模式 Locking',
'TRACK': '追踪模式 Tracking'
};
ctx.fillStyle = this.colors.text;
ctx.fillText(modeText[this.data.mode] || this.data.mode, width - 200, 40);
}
// 辅助函数
scalePoint(point, width, height) {
if (!this.data.frame_size) return [0, 0];
return [
point[0] * width / this.data.frame_size.width,
point[1] * height / this.data.frame_size.height
];
}
scalePoints(points, width, height) {
return points.map(p => this.scalePoint(p, width, height));
}
getContourCenter(points) {
const sum = points.reduce((acc, p) => [acc[0] + p[0], acc[1] + p[1]], [0, 0]);
return [sum[0] / points.length, sum[1] / points.length];
}
drawTargetMarker(x, y) {
const ctx = this.ctx;
ctx.strokeStyle = this.colors.primary;
ctx.lineWidth = 2;
// 十字准星
const size = 20;
ctx.beginPath();
ctx.moveTo(x - size, y);
ctx.lineTo(x - size/2, y);
ctx.moveTo(x + size/2, y);
ctx.lineTo(x + size, y);
ctx.moveTo(x, y - size);
ctx.lineTo(x, y - size/2);
ctx.moveTo(x, y + size/2);
ctx.lineTo(x, y + size);
ctx.stroke();
// 圆圈
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI * 2);
ctx.stroke();
}
drawLockAnimation(x, y, progress) {
const ctx = this.ctx;
const radius = 30 + 10 * Math.sin(progress * Math.PI * 2);
ctx.strokeStyle = this.colors.accent;
ctx.lineWidth = 3;
ctx.globalAlpha = 0.8;
// 旋转的锁定环
ctx.save();
ctx.translate(x, y);
ctx.rotate(progress * Math.PI * 2);
// 绘制4个弧形
for (let i = 0; i < 4; i++) {
ctx.beginPath();
ctx.arc(0, 0, radius, i * Math.PI/2 + 0.1, i * Math.PI/2 + Math.PI/2 - 0.1);
ctx.stroke();
}
ctx.restore();
ctx.globalAlpha = 1;
}
drawStatusText(mainText, subText, x, y, color) {
const ctx = this.ctx;
// 主文字(中文)
ctx.font = this.fonts.subtitle;
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.fillText(mainText, x, y);
// 副文字(英文)
if (subText) {
ctx.font = this.fonts.small;
ctx.fillStyle = this.colors.textMuted;
ctx.fillText(subText, x, y + 20);
}
ctx.textAlign = 'left';
}
}
// 初始化
window.addEventListener('DOMContentLoaded', () => {
window.visualizer = new TechVisualizer('tech-canvas');
});