Skip to content

Commit e678009

Browse files
committed
feat(web): render live LiDAR point cloud
1 parent ecf347e commit e678009

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

  • integrations/iphone-lidar/web
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { decodeLiDARPacket, depthToPointCloud } from './codec.mjs';
2+
3+
const canvas = document.querySelector('#view');
4+
const ctx = canvas.getContext('2d');
5+
const status = document.querySelector('#status');
6+
const fpsEl = document.querySelector('#fps');
7+
const pointsEl = document.querySelector('#points');
8+
const seqEl = document.querySelector('#seq');
9+
const latencyEl = document.querySelector('#latency');
10+
const sensorEl = document.querySelector('#sensor');
11+
12+
let lastFrameAt = performance.now();
13+
let yaw = 0.3;
14+
let pitch = -0.15;
15+
let scale = 120;
16+
17+
function connect() {
18+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
19+
const socket = new WebSocket(`${protocol}//${location.host}/ws/lidar`);
20+
21+
socket.addEventListener('open', () => {
22+
status.textContent = 'LIVE';
23+
status.dataset.state = 'live';
24+
});
25+
26+
socket.addEventListener('close', () => {
27+
status.textContent = 'RECONNECTING';
28+
status.dataset.state = 'warn';
29+
setTimeout(connect, 1000);
30+
});
31+
32+
socket.addEventListener('message', (event) => {
33+
try {
34+
const raw = JSON.parse(event.data);
35+
const frame = decodeLiDARPacket(raw);
36+
const points = depthToPointCloud(frame, 1);
37+
render(points);
38+
39+
const now = performance.now();
40+
const delta = now - lastFrameAt;
41+
lastFrameAt = now;
42+
fpsEl.textContent = delta > 0 ? (1000 / delta).toFixed(1) : '0.0';
43+
pointsEl.textContent = points.length.toLocaleString();
44+
seqEl.textContent = frame.provenance.sequence;
45+
latencyEl.textContent = Math.max(0, Date.now() - Number(frame.provenance.timestampNs / 1_000_000)).toFixed(0);
46+
sensorEl.textContent = frame.provenance.sensor;
47+
} catch (error) {
48+
console.error(error);
49+
status.textContent = 'FRAME ERROR';
50+
status.dataset.state = 'warn';
51+
}
52+
});
53+
}
54+
55+
function resize() {
56+
const dpr = Math.min(devicePixelRatio || 1, 2);
57+
const rect = canvas.getBoundingClientRect();
58+
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
59+
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
60+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
61+
}
62+
63+
function render(points) {
64+
resize();
65+
const w = canvas.clientWidth;
66+
const h = canvas.clientHeight;
67+
ctx.clearRect(0, 0, w, h);
68+
ctx.fillStyle = '#091018';
69+
ctx.fillRect(0, 0, w, h);
70+
71+
const cy = Math.cos(yaw);
72+
const sy = Math.sin(yaw);
73+
const cp = Math.cos(pitch);
74+
const sp = Math.sin(pitch);
75+
76+
ctx.fillStyle = '#58e0d2';
77+
for (let i = 0; i < points.length; i += 1) {
78+
let [x, y, z] = points[i];
79+
const rx = x * cy - z * sy;
80+
const rz = x * sy + z * cy;
81+
const ry = y * cp - rz * sp;
82+
const rz2 = y * sp + rz * cp;
83+
const perspective = 1 / Math.max(0.35, 1.8 - rz2 * 0.12);
84+
const px = w / 2 + rx * scale * perspective;
85+
const py = h / 2 + ry * scale * perspective;
86+
if (px >= 0 && px < w && py >= 0 && py < h) ctx.fillRect(px, py, 1.4, 1.4);
87+
}
88+
}
89+
90+
canvas.addEventListener('pointermove', (event) => {
91+
if (!event.buttons) return;
92+
yaw += event.movementX * 0.006;
93+
pitch += event.movementY * 0.006;
94+
});
95+
96+
canvas.addEventListener('wheel', (event) => {
97+
event.preventDefault();
98+
scale = Math.max(40, Math.min(400, scale - event.deltaY * 0.2));
99+
}, { passive: false });
100+
101+
connect();

0 commit comments

Comments
 (0)