Skip to content

Commit 95d0541

Browse files
authored
Merge pull request #6 from alphaonelabs/copilot/add-camera-and-humans-sensor
Add wandering humans to simulation with LiDAR detection and camera object-detection overlays
2 parents a2f9230 + d7c6c51 commit 95d0541

1 file changed

Lines changed: 167 additions & 31 deletions

File tree

home.html

Lines changed: 167 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@
246246
const STORAGE_KEY = 'robotics_playground_state';
247247
let lastSaveTime = 0;
248248

249+
// ========== HUMANS ==========
250+
let humans = [];
251+
249252
function generateBlocks() {
250253
blocks = [];
251254
const shapes = ['square', 'circle', 'triangle'];
@@ -263,6 +266,32 @@
263266
document.getElementById('block-count').textContent = blocks.length;
264267
}
265268

269+
function generateHumans() {
270+
humans = [];
271+
for (let i = 0; i < 5; i++) {
272+
humans.push({
273+
id: 'human_' + i,
274+
x: 80 + Math.random() * (canvas.width - 160),
275+
y: 80 + Math.random() * (canvas.height - 160),
276+
angle: Math.random() * Math.PI * 2,
277+
speed: 0.4 + Math.random() * 0.6,
278+
size: 18,
279+
});
280+
}
281+
}
282+
283+
function updateHumans() {
284+
humans.forEach((h) => {
285+
if (Math.random() < 0.02) h.angle += (Math.random() - 0.5) * Math.PI * 0.5;
286+
h.x += Math.sin(h.angle) * h.speed;
287+
h.y -= Math.cos(h.angle) * h.speed;
288+
if (h.x < 30 || h.x > canvas.width - 30) h.angle = Math.PI - h.angle;
289+
if (h.y < 30 || h.y > canvas.height - 30) h.angle = -h.angle;
290+
h.x = Math.max(30, Math.min(canvas.width - 30, h.x));
291+
h.y = Math.max(30, Math.min(canvas.height - 30, h.y));
292+
});
293+
}
294+
266295
// ========== INPUT ==========
267296
let keys = {};
268297
let spaceJustPressed = false;
@@ -349,6 +378,7 @@
349378
robot.y = canvas.height / 2;
350379
document.getElementById('start-prompt').classList.add('hidden');
351380
generateBlocks();
381+
generateHumans();
352382
}
353383
if (type === 'camera') {
354384
document.getElementById('camera-feed').classList.remove('hidden');
@@ -421,6 +451,7 @@
421451
},
422452
};
423453
blocks = [];
454+
humans = [];
424455
document.getElementById('start-prompt').classList.remove('hidden');
425456
document.getElementById('camera-feed').classList.add('hidden');
426457
document.getElementById('block-count').textContent = '0';
@@ -494,6 +525,8 @@
494525
updateDetectionUI(detected);
495526
}
496527
}
528+
529+
updateHumans();
497530
}
498531

499532
function drawGrid() {
@@ -677,45 +710,102 @@
677710
function drawCameraView() {
678711
if (!robot.parts.camera) return;
679712

713+
const camW = 176, camH = 112, horizon = 56;
714+
const fov = Math.PI / 2;
715+
const detectionPad = 3;
716+
680717
// Sky gradient
681-
const sky = camCtx.createLinearGradient(0, 0, 0, 56);
718+
const sky = camCtx.createLinearGradient(0, 0, 0, horizon);
682719
sky.addColorStop(0, '#0c1929');
683720
sky.addColorStop(1, '#1e3a5f');
684721
camCtx.fillStyle = sky;
685-
camCtx.fillRect(0, 0, 176, 56);
722+
camCtx.fillRect(0, 0, camW, horizon);
686723

687724
// Ground gradient
688-
const gnd = camCtx.createLinearGradient(0, 56, 0, 112);
725+
const gnd = camCtx.createLinearGradient(0, horizon, 0, camH);
689726
gnd.addColorStop(0, '#374151');
690727
gnd.addColorStop(1, '#1f2937');
691728
camCtx.fillStyle = gnd;
692-
camCtx.fillRect(0, 56, 176, 56);
729+
camCtx.fillRect(0, horizon, camW, horizon);
693730

694-
// Render blocks in view
695-
blocks.forEach((b) => {
696-
const cx = b.x + b.size / 2;
697-
const cy = b.y + b.size / 2;
698-
const dx = cx - robot.x;
699-
const dy = cy - robot.y;
731+
// Helper: project a world position into camera screen coordinates
732+
function project(wx, wy) {
733+
const dx = wx - robot.x;
734+
const dy = wy - robot.y;
700735
const dist = Math.hypot(dx, dy);
701-
702736
let worldAng = Math.atan2(dx, -dy);
703737
let relAng = worldAng - robot.angle;
704738
while (relAng > Math.PI) relAng -= 2 * Math.PI;
705739
while (relAng < -Math.PI) relAng += 2 * Math.PI;
740+
if (Math.abs(relAng) >= fov / 2 || dist > 400 || dist < 20) return null;
741+
const screenX = 88 + (relAng / (fov / 2)) * 88;
742+
const sz = Math.max(6, 600 / dist);
743+
const screenY = horizon - sz / 2 + dist / 30;
744+
return { screenX, screenY, sz, dist };
745+
}
706746

707-
const fov = Math.PI / 2;
708-
if (Math.abs(relAng) < fov / 2 && dist < 400 && dist > 40) {
709-
const screenX = 88 + (relAng / (fov / 2)) * 88;
710-
const sz = Math.max(6, 600 / dist);
711-
const screenY = 56 - sz / 2 + dist / 30;
712-
713-
camCtx.fillStyle = b.color;
714-
camCtx.fillRect(screenX - sz / 2, Math.max(5, screenY), sz, sz);
715-
camCtx.strokeStyle = '#fff';
716-
camCtx.lineWidth = 1;
717-
camCtx.strokeRect(screenX - sz / 2, Math.max(5, screenY), sz, sz);
718-
}
747+
// Render blocks with detection bounding boxes
748+
blocks.forEach((b) => {
749+
if (b.held) return;
750+
const proj = project(b.x + b.size / 2, b.y + b.size / 2);
751+
if (!proj) return;
752+
const { screenX, screenY, sz } = proj;
753+
const sy = Math.max(5, screenY);
754+
755+
camCtx.fillStyle = b.color;
756+
camCtx.fillRect(screenX - sz / 2, sy, sz, sz);
757+
camCtx.strokeStyle = '#fff';
758+
camCtx.lineWidth = 1;
759+
camCtx.strokeRect(screenX - sz / 2, sy, sz, sz);
760+
761+
// Detection bounding box (cyan)
762+
const bx = screenX - sz / 2 - detectionPad;
763+
const by = sy - detectionPad;
764+
const bw = sz + detectionPad * 2;
765+
const bh = sz + detectionPad * 2;
766+
camCtx.strokeStyle = '#22d3ee';
767+
camCtx.lineWidth = 1;
768+
camCtx.strokeRect(bx, by, bw, bh);
769+
770+
// Label
771+
const label = b.shape.charAt(0).toUpperCase() + b.shape.slice(1);
772+
camCtx.fillStyle = '#22d3ee';
773+
camCtx.font = 'bold 6px monospace';
774+
const ly = by - 1;
775+
camCtx.fillText(label, bx, ly > 6 ? ly : by + bh + 7);
776+
});
777+
778+
// Render humans with detection bounding boxes (orange)
779+
humans.forEach((h) => {
780+
const proj = project(h.x, h.y);
781+
if (!proj) return;
782+
const { screenX, screenY, sz } = proj;
783+
const sy = Math.max(5, screenY);
784+
const hw = Math.max(6, sz * 0.7);
785+
const headR = hw / 3;
786+
const bodyH = Math.max(4, sz * 0.6);
787+
788+
// Silhouette: head + body
789+
camCtx.fillStyle = '#94a3b8';
790+
camCtx.beginPath();
791+
camCtx.arc(screenX, sy, headR, 0, Math.PI * 2);
792+
camCtx.fill();
793+
camCtx.fillRect(screenX - hw / 3, sy, (hw * 2) / 3, bodyH);
794+
795+
// Detection bounding box (orange)
796+
const bx = screenX - hw / 2 - detectionPad;
797+
const by = sy - headR - detectionPad;
798+
const bw = hw + detectionPad * 2;
799+
const bh = headR + bodyH + detectionPad * 2;
800+
camCtx.strokeStyle = '#fb923c';
801+
camCtx.lineWidth = 1;
802+
camCtx.strokeRect(bx, by, bw, bh);
803+
804+
// Label
805+
camCtx.fillStyle = '#fb923c';
806+
camCtx.font = 'bold 6px monospace';
807+
const ly = by - 1;
808+
camCtx.fillText('Human', bx, ly > 6 ? ly : by + bh + 7);
719809
});
720810

721811
// Crosshair
@@ -729,11 +819,46 @@
729819
camCtx.stroke();
730820
}
731821

822+
function drawHumans() {
823+
humans.forEach((h) => {
824+
ctx.save();
825+
ctx.translate(h.x, h.y);
826+
ctx.rotate(h.angle);
827+
ctx.strokeStyle = '#e2e8f0';
828+
ctx.fillStyle = '#e2e8f0';
829+
ctx.lineWidth = 2;
830+
ctx.lineCap = 'round';
831+
// Head
832+
ctx.beginPath();
833+
ctx.arc(0, -14, 5, 0, Math.PI * 2);
834+
ctx.fill();
835+
// Body
836+
ctx.beginPath();
837+
ctx.moveTo(0, -9);
838+
ctx.lineTo(0, 4);
839+
ctx.stroke();
840+
// Arms
841+
ctx.beginPath();
842+
ctx.moveTo(-8, -4);
843+
ctx.lineTo(8, -4);
844+
ctx.stroke();
845+
// Legs
846+
ctx.beginPath();
847+
ctx.moveTo(0, 4);
848+
ctx.lineTo(-6, 14);
849+
ctx.moveTo(0, 4);
850+
ctx.lineTo(6, 14);
851+
ctx.stroke();
852+
ctx.restore();
853+
});
854+
}
855+
732856
function render() {
733857
ctx.fillStyle = '#0a0a0a';
734858
ctx.fillRect(0, 0, canvas.width, canvas.height);
735859
drawGrid();
736860
drawBlocks();
861+
drawHumans();
737862
drawRobot();
738863
drawCameraView();
739864
}
@@ -800,7 +925,7 @@
800925
}
801926

802927
function getLidarDetection() {
803-
if (!robot.parts.lidar || blocks.length === 0) return null;
928+
if (!robot.parts.lidar || (blocks.length === 0 && humans.length === 0)) return null;
804929
const sw = (((Date.now() / 15) % 360) * Math.PI) / 180;
805930
// Lidar center in world coordinates (local offset: 0, -32)
806931
const lidarX = robot.x + 32 * Math.sin(robot.angle);
@@ -812,7 +937,12 @@
812937
const beamEndY = lidarY + Math.sin(worldBeamAngle) * beamRange;
813938
for (const b of blocks) {
814939
if (b.held) continue;
815-
if (beamIntersectsBlock(lidarX, lidarY, beamEndX, beamEndY, b)) return b;
940+
if (beamIntersectsBlock(lidarX, lidarY, beamEndX, beamEndY, b)) return { ...b, type: 'block' };
941+
}
942+
for (const h of humans) {
943+
if (pointToSegmentDist(h.x, h.y, lidarX, lidarY, beamEndX, beamEndY) < h.size / 2) {
944+
return { ...h, type: 'human' };
945+
}
816946
}
817947
return null;
818948
}
@@ -836,13 +966,19 @@
836966
const el = document.getElementById('detection-info');
837967
const indicator = document.getElementById('detection-indicator');
838968
if (detected) {
839-
const colorName = getColorName(detected.color);
840-
const shapeName = detected.shape.charAt(0).toUpperCase() + detected.shape.slice(1);
841-
el.innerHTML =
842-
'<div class="text-green-400 font-semibold">● OBJECT DETECTED</div>' +
843-
'<div class="text-gray-300 mt-0.5">Shape: <span class="text-yellow-300">' + shapeName + '</span></div>' +
844-
'<div class="text-gray-300">Color: <span style="color:' + detected.color + ';font-weight:600">' + colorName + '</span></div>';
845969
indicator.classList.remove('hidden');
970+
if (detected.type === 'human') {
971+
el.innerHTML =
972+
'<div class="text-orange-400 font-semibold">● HUMAN DETECTED</div>' +
973+
'<div class="text-gray-300 mt-0.5">Type: <span class="text-orange-300">Human</span></div>';
974+
} else {
975+
const colorName = getColorName(detected.color);
976+
const shapeName = detected.shape.charAt(0).toUpperCase() + detected.shape.slice(1);
977+
el.innerHTML =
978+
'<div class="text-green-400 font-semibold">● OBJECT DETECTED</div>' +
979+
'<div class="text-gray-300 mt-0.5">Shape: <span class="text-yellow-300">' + shapeName + '</span></div>' +
980+
'<div class="text-gray-300">Color: <span style="color:' + detected.color + ';font-weight:600">' + colorName + '</span></div>';
981+
}
846982
} else {
847983
el.innerHTML = '<div class="text-gray-500">No objects detected</div>';
848984
indicator.classList.add('hidden');

0 commit comments

Comments
 (0)