wifi-densepose/pointcloud/index.html

786 lines
36 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>
<head>
<title>RuView — Camera + WiFi CSI Point Cloud</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="ruview-viewer-version" content="0.2.0-face-mesh">
<style>
body { margin: 0; background: #0a0a0a; color: #e8a634; font-family: monospace; }
canvas { display: block; }
#info { position: absolute; top: 10px; left: 10px; padding: 12px; background: rgba(0,0,0,0.85); border: 1px solid #e8a634; border-radius: 6px; min-width: 240px; font-size: 13px; line-height: 1.5; z-index: 10; }
#cam-cta { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); padding: 10px 18px; background: #e8a634; color: #0a0a0a; border: none; border-radius: 4px; font-family: monospace; font-size: 14px; font-weight: bold; cursor: pointer; z-index: 10; }
#cam-cta:hover { background: #ffc04d; }
#cam-cta.hidden { display: none; }
.live { color: #4f4; } .demo { color: #f44; }
.face { color: #4cf; }
.section { margin-top: 6px; padding-top: 6px; border-top: 1px solid #333; }
.label { color: #888; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<!-- MediaPipe Face Mesh — runs in demo mode so each visitor sees their own face as a point cloud -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4/face_mesh.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.3/camera_utils.js"></script>
</head>
<body>
<div id="info">
<h3 style="margin:0 0 4px 0">RuView · Seldon Vault</h3>
<div style="font-size: 11px; color: #888; margin-bottom: 8px; max-width: 240px; line-height: 1.4; font-style: italic;">"Psychohistory deals with reactions of human conglomerates to fixed social and economic stimuli." — Hari Seldon</div>
<div id="stats">Loading...</div>
</div>
<button id="cam-cta">▶ Project Subject — render your face into the Vault</button>
<script>
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0a);
var camera = new THREE.PerspectiveCamera(72, window.innerWidth/window.innerHeight, 0.1, 200);
camera.position.set(0, 0.2, -3.5);
camera.lookAt(0, 0, 2);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 2);
var pointsMesh = null;
var lastFrame = -1;
var skeletonGroup = null;
var prevTimestamp = 0;
var frameRateVal = 0;
// COCO skeleton connections: pairs of keypoint indices
// 0=nose 1=leftEye 2=rightEye 3=leftEar 4=rightEar
// 5=leftShoulder 6=rightShoulder 7=leftElbow 8=rightElbow
// 9=leftWrist 10=rightWrist 11=leftHip 12=rightHip
// 13=leftKnee 14=rightKnee 15=leftAnkle 16=rightAnkle
var COCO_BONES = [
[0,1],[0,2],[1,3],[2,4],
[5,6],[5,7],[7,9],[6,8],[8,10],
[5,11],[6,12],[11,12],
[11,13],[13,15],[12,14],[14,16]
];
function clearSkeleton() {
if (skeletonGroup) {
scene.remove(skeletonGroup);
skeletonGroup.traverse(function(obj) {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) obj.material.dispose();
});
skeletonGroup = null;
}
}
function drawSkeleton(keypoints) {
clearSkeleton();
if (!keypoints || keypoints.length < 17) return;
skeletonGroup = new THREE.Group();
// Map keypoints from [0,1] to scene coords
// x: [-2, 2], y: [2, -2] (flip y), z: fixed at 2
var sphereGeo = new THREE.SphereGeometry(0.04, 8, 8);
var sphereMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
var positions3D = [];
var i, kp, sx, sy;
for (i = 0; i < 17; i++) {
kp = keypoints[i];
if (!kp) { positions3D.push(null); continue; }
sx = (kp[0] - 0.5) * 4;
sy = (0.5 - kp[1]) * 4;
positions3D.push([sx, sy, 2]);
var sphere = new THREE.Mesh(sphereGeo, sphereMat);
sphere.position.set(sx, sy, 2);
skeletonGroup.add(sphere);
}
// Draw bones as white lines
var lineMat = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
var b, a, bIdx;
for (b = 0; b < COCO_BONES.length; b++) {
a = COCO_BONES[b][0];
bIdx = COCO_BONES[b][1];
if (!positions3D[a] || !positions3D[bIdx]) continue;
var lineGeo = new THREE.BufferGeometry();
var verts = new Float32Array([
positions3D[a][0], positions3D[a][1], positions3D[a][2],
positions3D[bIdx][0], positions3D[bIdx][1], positions3D[bIdx][2]
]);
lineGeo.setAttribute("position", new THREE.BufferAttribute(verts, 3));
var line = new THREE.Line(lineGeo, lineMat);
skeletonGroup.add(line);
}
scene.add(skeletonGroup);
}
// ----- Transport configuration -----
// ?backend=<url> → fetch splats from <url>/api/splats (CORS-permitting host)
// ?backend=auto → try /api/splats, fall back to synthetic demo on failure (default)
// ?backend=demo → always render synthetic demo (no network)
// ?live=1 → require live; show error instead of demo fallback
var urlParams = new URLSearchParams(window.location.search);
var backendArg = urlParams.get("backend") || "auto";
var requireLive = urlParams.get("live") === "1";
var transportMode = "demo"; // resolved at first fetch: "live" | "remote" | "demo"
var demoStartMs = Date.now();
var demoFrameNum = 0;
var latestFaceLandmarks = null; // populated by MediaPipe when camera enabled
var faceMeshState = "idle"; // "idle" | "starting" | "running" | "denied" | "unavailable"
// ----- Hollywood effect toggles (optional, opt-out) -----
// ?fx=clean — colored points only, no wireframe / no scan
// ?fx=points — original solid amber, no texture / no wireframe / no scan
// ?fx=mesh,texture,scan,halo (default) — full cinematic stack
// Comma-separated; presence of "all" enables every effect.
var fxArg = urlParams.get("fx") || "all";
var fxList = fxArg.split(",").map(function(s) { return s.trim(); });
var fxAll = fxList.indexOf("all") >= 0 || fxArg === "all";
function fxOn(name) {
if (fxArg === "clean") return name === "texture";
if (fxArg === "points") return false;
return fxAll || fxList.indexOf(name) >= 0;
}
var FX_TEXTURE = fxOn("texture"); // sample webcam pixels onto each splat
var FX_MESH = fxOn("mesh"); // translucent amber wireframe over the points
var FX_SCAN = fxOn("scan"); // sweeping scan line that brightens nearby splats
var FX_HALO = fxOn("halo"); // amber halo ring around the face
// Webcam sampler for FX_TEXTURE — a hidden 2D canvas updated each frame.
var sampleCanvas = null;
var sampleCtx = null;
var sampleData = null;
var sampleW = 0, sampleH = 0;
var sampleVideo = null; // populated by startFaceMesh
function refreshSampleData() {
if (!sampleCtx || !sampleVideo) return false;
if (!sampleVideo.videoWidth || !sampleVideo.videoHeight) return false;
if (sampleW !== sampleVideo.videoWidth || sampleH !== sampleVideo.videoHeight) {
sampleW = sampleVideo.videoWidth;
sampleH = sampleVideo.videoHeight;
sampleCanvas.width = sampleW;
sampleCanvas.height = sampleH;
}
sampleCtx.drawImage(sampleVideo, 0, 0, sampleW, sampleH);
try {
sampleData = sampleCtx.getImageData(0, 0, sampleW, sampleH).data;
return true;
} catch (e) {
return false; // tainted canvas (shouldn't happen on same-origin webcam)
}
}
function sampleColorAt(lmx, lmy) {
if (!sampleData) return null;
var px = Math.min(sampleW - 1, Math.max(0, Math.floor(lmx * sampleW)));
var py = Math.min(sampleH - 1, Math.max(0, Math.floor(lmy * sampleH)));
var idx = (py * sampleW + px) * 4;
return [sampleData[idx] / 255, sampleData[idx + 1] / 255, sampleData[idx + 2] / 255];
}
// ----- MediaPipe Face Mesh (browser equivalent of camera-depth backprojection) -----
// Locally, ruview-pointcloud serve fuses real camera depth + WiFi CSI. In the
// browser we don't have depth from a webcam, but Face Mesh produces 468
// 3D landmarks (x,y in [0,1], z roughly in [-0.5,0.5]) at ~30 fps — enough to
// reproduce the "I can see the outline of my face in points" experience. The
// landmarks feed into the same splat render path as live /api/splats data.
async function startFaceMesh() {
if (faceMeshState !== "idle") return;
if (!window.FaceMesh || !window.Camera) {
faceMeshState = "unavailable";
return;
}
faceMeshState = "starting";
try {
var videoEl = document.createElement("video");
videoEl.style.display = "none";
videoEl.autoplay = true;
videoEl.playsInline = true;
videoEl.muted = true;
document.body.appendChild(videoEl);
sampleVideo = videoEl;
// Hidden canvas used for per-pixel webcam sampling (FX_TEXTURE).
sampleCanvas = document.createElement("canvas");
sampleCtx = sampleCanvas.getContext("2d", { willReadFrequently: true });
var fm = new FaceMesh({
locateFile: function(file) {
return "https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4/" + file;
}
});
fm.setOptions({
maxNumFaces: 1,
refineLandmarks: true,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
fm.onResults(function(results) {
if (results.multiFaceLandmarks && results.multiFaceLandmarks[0]) {
latestFaceLandmarks = results.multiFaceLandmarks[0];
}
});
var mpCamera = new Camera(videoEl, {
onFrame: async function() { await fm.send({ image: videoEl }); },
width: 640,
height: 480
});
await mpCamera.start();
faceMeshState = "running";
var btn = document.getElementById("cam-cta");
if (btn) btn.classList.add("hidden");
} catch (err) {
faceMeshState = "denied";
console.warn("Face mesh unavailable:", err);
}
}
// ---- Foundation-inspired galactic context (Asimov / Trantor / Seldon) ----
// Shared between face-mesh and synthetic-fallback paths. The subject (face
// or procedural figure) is the foreground; this function paints the Seldon
// time-vault around it: holographic surveyor grid underfoot, slow galactic
// spiral receding into the distance, distant starfield, and a halo ring.
function pushFoundationContext(splats) {
var t = (Date.now() - demoStartMs) / 1000.0;
// 1. Holographic surveyor grid — amber lattice at y=+1.4 (renders below
// the subject because the renderer flips y to Three.js Y-up).
var gx, gz;
for (gx = -10; gx <= 10; gx++) {
for (gz = 0; gz <= 30; gz++) {
var alpha = 0.35 + 0.15 * Math.sin(t * 0.5 + gz * 0.2);
splats.push({
center: [gx * 0.5, 1.4, gz * 0.4],
color: [0.40 * alpha, 0.28 * alpha, 0.10 * alpha],
opacity: 1.0,
scale: [0.018, 0.018, 0.018]
});
}
}
for (gz = 0; gz <= 30; gz += 2) {
for (gx = -20; gx <= 20; gx++) {
splats.push({
center: [gx * 0.25, 1.4, gz * 0.4 + 0.1],
color: [0.30, 0.22, 0.08],
opacity: 1.0,
scale: [0.014, 0.014, 0.014]
});
}
}
// 2. Galactic spiral — Trantor recedes behind the subject. ~640 stars
// across two logarithmic arms, slowly rotating. Warmer at the core,
// cooler at the edges (Hertzsprung-Russell-ish).
var arm, k, theta_arm, r_arm, sx, sz, twist, arm_color;
for (arm = 0; arm < 2; arm++) {
for (k = 0; k < 320; k++) {
var prog = k / 320;
theta_arm = arm * Math.PI + prog * 6.0 + t * 0.05;
r_arm = 4.0 + prog * 14.0;
twist = Math.sin(prog * 8.0) * 0.4;
sx = Math.cos(theta_arm) * r_arm + twist;
sz = Math.sin(theta_arm) * r_arm + 12.0;
var coreFade = Math.max(0.15, 1.0 - prog);
arm_color = [
coreFade * 0.85 + 0.15 * (1 - prog),
coreFade * 0.70 + 0.20,
coreFade * 0.55 + 0.45 * prog
];
splats.push({
center: [sx, -2.5 + Math.sin(prog * 12) * 0.3, sz],
color: arm_color,
opacity: 1.0,
scale: [0.025, 0.025, 0.025]
});
}
}
// 3. Distant starfield — 800 deterministic stars on a spherical shell.
// Fixed LCG seed so visitors don't see noise flicker between frames.
var seed = 42;
function nextRand() {
seed = (seed * 1664525 + 1013904223) >>> 0;
return seed / 4294967296;
}
var s, r_s, phi, costheta, sinphi;
for (s = 0; s < 800; s++) {
phi = nextRand() * Math.PI * 2;
costheta = nextRand() * 2 - 1;
sinphi = Math.sqrt(1 - costheta * costheta);
r_s = 22 + nextRand() * 8;
var brightness = 0.4 + nextRand() * 0.6;
var hue = nextRand();
splats.push({
center: [
Math.cos(phi) * sinphi * r_s,
costheta * r_s * 0.5 - 1.0,
Math.sin(phi) * sinphi * r_s + 5.0
],
color: hue > 0.85
? [brightness, brightness * 0.85, brightness * 0.6]
: (hue > 0.3
? [brightness * 0.9, brightness * 0.95, brightness]
: [brightness * 0.5, brightness * 0.7, brightness]),
opacity: 1.0,
scale: [0.020, 0.020, 0.020]
});
}
// 4. Holographic projection halo around the subject — Seldon vault
// projections always had a faint encircling ring of particles.
if (FX_HALO) {
var ring;
for (ring = 0; ring < 60; ring++) {
var rt = ring / 60 * Math.PI * 2 + t * 0.3;
splats.push({
center: [
Math.cos(rt) * 1.6,
Math.sin(rt) * 1.2 - 0.2,
2.0 + Math.sin(rt * 3 + t * 0.5) * 0.3
],
color: [0.95, 0.55, 0.15],
opacity: 1.0,
scale: [0.014, 0.014, 0.014]
});
}
}
}
// Map a single landmark to world coords. Coord conventions:
// x: 0.5 - lm.x → mirror so left-of-screen = your left side (selfie)
// y: lm.y - 0.5 → keep MediaPipe's y-DOWN convention; the renderer's
// existing -y flip in updateSplats does the single flip
// to Three.js Y-up. Pre-flipping here would double-flip
// and the face would render upside down.
// z: 2.0 + lm.z*8 → amplify lm.z (~[-0.1,+0.1]) so the nose/eye-socket
// depth is visible from an oblique camera angle.
function lmToCenter(lm) {
return [
(0.5 - lm.x) * 4.0,
(lm.y - 0.5) * 3.0,
2.0 + lm.z * 8.0
];
}
function pushFaceSplat(splats, center, alpha) {
splats.push({
center: center,
color: [0.95 * alpha, 0.65 * alpha, 0.20 * alpha],
opacity: 1.0,
scale: [0.006, 0.006, 0.006]
});
}
// FACEMESH_TESSELATION is a flat array [a0,b0, a1,b1, ...] of vertex indices
// forming edges of the triangulated face mesh. ~1300 edges × 2 = 2600 entries.
// We interpolate 6 splats per edge → ~8000 splats per face vs 478 vertices.
var FACE_EDGES = (typeof FACEMESH_TESSELATION !== "undefined") ? FACEMESH_TESSELATION : null;
// Persistent translucent wireframe overlay (FX_MESH). Reuses one
// LineSegments object — we just rewrite vertex positions each frame.
var faceMesh3D = null;
var faceMeshPositions = null;
function ensureFaceWireframe(edgeCount) {
if (faceMesh3D || !FX_MESH || !FACE_EDGES) return;
var n = edgeCount; // 2 endpoints per line segment
faceMeshPositions = new Float32Array(n * 3);
var geo = new THREE.BufferGeometry();
geo.setAttribute("position", new THREE.BufferAttribute(faceMeshPositions, 3));
var mat = new THREE.LineBasicMaterial({
color: 0xe8a634,
transparent: true,
opacity: 0.35,
blending: THREE.AdditiveBlending,
depthWrite: false
});
faceMesh3D = new THREE.LineSegments(geo, mat);
scene.add(faceMesh3D);
}
function updateFaceWireframe(lms) {
if (!FX_MESH || !FACE_EDGES) return;
ensureFaceWireframe(FACE_EDGES.length);
if (!faceMesh3D || !faceMeshPositions) return;
var arr = faceMeshPositions;
var i, idxA, idxB, posA, posB, w = 0;
for (i = 0; i < FACE_EDGES.length; i += 2) {
idxA = FACE_EDGES[i];
idxB = FACE_EDGES[i + 1];
posA = lmToCenter(lms[idxA]);
posB = lmToCenter(lms[idxB]);
// The renderer's updateSplats() flips y on ColorPoint splats but
// the wireframe renders directly in scene coords, so apply the
// same flip here for consistency.
arr[w++] = posA[0]; arr[w++] = -posA[1]; arr[w++] = posA[2];
arr[w++] = posB[0]; arr[w++] = -posB[1]; arr[w++] = posB[2];
}
faceMesh3D.geometry.attributes.position.needsUpdate = true;
faceMesh3D.visible = true;
}
function hideFaceWireframe() {
if (faceMesh3D) faceMesh3D.visible = false;
}
function faceMeshFrame() {
if (faceMeshState !== "running" || !latestFaceLandmarks) return null;
var lms = latestFaceLandmarks;
var splats = [];
var i, lm;
// FX_TEXTURE: refresh webcam frame buffer once per render call so all
// landmark sampling reads from the same instant.
if (FX_TEXTURE) refreshSampleData();
// FX_SCAN: a vertical scan line sweeping top→bottom every 4 seconds.
// Splats whose y is within +/- band of the scan line get amplified.
var t_now = (Date.now() - demoStartMs) / 1000.0;
var scanY = ((t_now % 4) / 4) * 2.4 - 1.2; // -1.2 → +1.2 over 4s
var scanBand = 0.08;
function scanBoost(y) {
if (!FX_SCAN) return 1.0;
var dist = Math.abs(y - scanY);
if (dist > scanBand) return 1.0;
return 1.0 + (1.0 - dist / scanBand) * 1.6; // up to 2.6x at line center
}
function clampColor(c) { return [Math.min(1, c[0]), Math.min(1, c[1]), Math.min(1, c[2])]; }
// 1. Original 478 vertices — webcam-sampled or amber, scan-modulated.
for (i = 0; i < lms.length; i++) {
lm = lms[i];
var center = lmToCenter(lm);
var col = FX_TEXTURE ? sampleColorAt(1.0 - lm.x, lm.y) : null;
if (!col) col = [1.0, 0.72, 0.25]; // fallback amber
var boost = scanBoost(center[1]);
splats.push({
center: center,
color: clampColor([col[0] * boost, col[1] * boost, col[2] * boost]),
opacity: 1.0,
scale: [0.010, 0.010, 0.010]
});
}
// 2. Edge interpolation — 6 splats per FACEMESH_TESSELATION edge.
// Texture-sample at the interpolated UV so the edge fill matches
// actual skin tone between vertices.
if (FACE_EDGES) {
var edgeCount = FACE_EDGES.length;
var SAMPLES = 6;
var e, a, b, ti, f, lmx, lmy;
for (e = 0; e < edgeCount; e += 2) {
a = lms[FACE_EDGES[e]];
b = lms[FACE_EDGES[e + 1]];
if (!a || !b) continue;
var aPos = lmToCenter(a);
var bPos = lmToCenter(b);
for (ti = 1; ti <= SAMPLES; ti++) {
f = ti / (SAMPLES + 1);
var cx = aPos[0] * (1 - f) + bPos[0] * f;
var cy = aPos[1] * (1 - f) + bPos[1] * f;
var cz = aPos[2] * (1 - f) + bPos[2] * f;
var col2;
if (FX_TEXTURE) {
lmx = a.x * (1 - f) + b.x * f;
lmy = a.y * (1 - f) + b.y * f;
col2 = sampleColorAt(1.0 - lmx, lmy) || [0.85, 0.62, 0.22];
} else {
col2 = [0.85, 0.62, 0.22];
}
var boost2 = scanBoost(cy);
splats.push({
center: [cx, cy, cz],
color: clampColor([col2[0] * boost2, col2[1] * boost2, col2[2] * boost2]),
opacity: 1.0,
scale: [0.006, 0.006, 0.006]
});
}
}
}
// FX_MESH: update the persistent translucent amber wireframe over the face.
updateFaceWireframe(lms);
pushFoundationContext(splats);
demoFrameNum += 1;
return {
splats: splats,
count: splats.length,
frame: demoFrameNum,
live: false,
source: "face-mesh",
pipeline: {
skeleton: null,
vitals: { breathing_rate: 14, motion_score: 0.15 }
}
};
}
function buildSplatsUrl() {
if (backendArg === "demo") return null;
if (backendArg === "auto") return "/api/splats";
// User-supplied URL — strip trailing slash and append /api/splats.
var base = backendArg.replace(/\/+$/, "");
return base + "/api/splats";
}
function syntheticFrame() {
// Used when camera permission is denied / unavailable. Renders a
// procedural standing figure inside the Seldon vault context.
// y-down convention: head at small/negative y, feet at large/positive y;
// the renderer flips y so head ends up at the top of the screen.
var t = (Date.now() - demoStartMs) / 1000.0;
var sway = Math.sin(t * 0.8) * 0.05;
var breath = Math.sin(t * 1.2) * 0.015;
var splats = [];
// Standing figure — 240 points in a vertical cylinder, denser than
// before to feel like a holographic projection.
var ring, k_ring, theta, r, py;
for (ring = 0; ring < 30; ring++) {
py = -1.0 + (ring / 30) * 2.2; // head (-1.0) → feet (+1.2) in y-down
r = 0.20 + breath * (py < 0 ? 1.5 : 0); // chest expands more on inhale
for (k_ring = 0; k_ring < 16; k_ring++) {
theta = (k_ring / 16) * Math.PI * 2;
splats.push({
center: [
sway + Math.cos(theta) * r,
py,
2.3 + Math.sin(theta) * r
],
color: [0.91, 0.65, 0.20],
opacity: 1.0,
scale: [0.018, 0.018, 0.018]
});
}
}
// 17 COCO keypoints in normalized [0,1] image coords (matches live shape)
var headY = 0.18;
var keypoints = [
[0.50 + sway * 0.05, headY, 0.95], // 0 nose
[0.52 + sway * 0.05, headY - 0.01, 0.92], // 1 leftEye
[0.48 + sway * 0.05, headY - 0.01, 0.92], // 2 rightEye
[0.54 + sway * 0.05, headY, 0.85], // 3 leftEar
[0.46 + sway * 0.05, headY, 0.85], // 4 rightEar
[0.60 + sway * 0.04, 0.32, 0.93], // 5 leftShoulder
[0.40 + sway * 0.04, 0.32, 0.93], // 6 rightShoulder
[0.65 + sway * 0.03, 0.46, 0.90], // 7 leftElbow
[0.35 + sway * 0.03, 0.46, 0.90], // 8 rightElbow
[0.68, 0.60 + Math.sin(t * 1.4) * 0.02, 0.86], // 9 leftWrist
[0.32, 0.60 - Math.sin(t * 1.4) * 0.02, 0.86], // 10 rightWrist
[0.57, 0.58, 0.94], // 11 leftHip
[0.43, 0.58, 0.94], // 12 rightHip
[0.58, 0.74, 0.90], // 13 leftKnee
[0.42, 0.74, 0.90], // 14 rightKnee
[0.59, 0.92, 0.88], // 15 leftAnkle
[0.41, 0.92, 0.88] // 16 rightAnkle
];
// No face mesh in synthetic mode — hide the wireframe overlay if it exists.
hideFaceWireframe();
// Wrap the figure in the Seldon-vault context (grid, spiral, starfield, halo)
pushFoundationContext(splats);
demoFrameNum += 1;
return {
splats: splats,
count: splats.length,
frame: demoFrameNum,
live: false,
pipeline: {
skeleton: { keypoints: keypoints, confidence: 0.86 },
vitals: {
breathing_rate: 14 + Math.round(Math.sin(t * 0.05) * 2),
motion_score: 0.18 + Math.abs(sway) * 2
}
}
};
}
function pickDemoFrame() {
// Prefer real face-mesh data when the camera is running; else procedural.
return faceMeshFrame() || syntheticFrame();
}
async function fetchCloud() {
// Demo-only mode: never hit the network.
if (backendArg === "demo") {
transportMode = "demo";
handleData(pickDemoFrame());
return;
}
try {
var resp = await fetch(buildSplatsUrl(), { cache: "no-store" });
if (!resp.ok) throw new Error("HTTP " + resp.status);
var data = await resp.json();
transportMode = (backendArg === "auto") ? "live" : "remote";
handleData(data);
} catch (err) {
if (requireLive) {
document.getElementById("stats").innerHTML =
'<span class="demo">&#9679; OFFLINE</span><br>Live backend required (?live=1) but unreachable.<br><span class="label">' + (err && err.message ? err.message : err) + '</span>';
return;
}
transportMode = "demo";
handleData(pickDemoFrame());
}
}
function handleData(data) {
try {
if (data.splats && data.frame !== lastFrame) {
// Compute CSI frame rate
var now = Date.now();
if (prevTimestamp > 0) {
var dt = (now - prevTimestamp) / 1000.0;
if (dt > 0) frameRateVal = (1.0 / dt).toFixed(1);
}
prevTimestamp = now;
lastFrame = data.frame;
updateSplats(data.splats);
// Draw skeleton if available
var pipe = data.pipeline;
if (pipe && pipe.skeleton && pipe.skeleton.keypoints) {
drawSkeleton(pipe.skeleton.keypoints);
} else {
clearSkeleton();
}
// Build info panel — badge reflects active transport
var mode;
if (transportMode === "live") {
mode = '<span class="live">&#9679; LIVE</span> Local Backend';
} else if (transportMode === "remote") {
mode = '<span class="live">&#9679; REMOTE</span> ' + backendArg;
} else if (data.source === "face-mesh") {
mode = '<span class="face">&#9679; DEMO</span> Your Face (MediaPipe)';
} else {
mode = '<span class="demo">&#9679; DEMO</span> Synthetic';
}
var html = mode + "<br>"
+ "Splats: " + data.count + "<br>"
+ "Frame: " + data.frame;
// FX status — only show in face-mesh mode where the toggles matter
if (data.source === "face-mesh") {
var fxOnList = [];
if (FX_TEXTURE) fxOnList.push("texture");
if (FX_MESH) fxOnList.push("mesh");
if (FX_SCAN) fxOnList.push("scan");
if (FX_HALO) fxOnList.push("halo");
html += '<div class="section">'
+ '<span class="label">FX:</span> '
+ (fxOnList.length ? fxOnList.join(" · ") : "off")
+ '</div>';
}
// CSI frame rate
html += '<div class="section">'
+ '<span class="label">CSI Rate:</span> '
+ frameRateVal + " fps</div>";
// Skeleton confidence
if (pipe && pipe.skeleton && pipe.skeleton.confidence !== undefined) {
var conf = (pipe.skeleton.confidence * 100).toFixed(0);
html += '<div class="section">'
+ '<span class="label">Skeleton:</span> '
+ conf + "% confidence</div>";
}
// Weather data
if (pipe && pipe.weather) {
var w = pipe.weather;
html += '<div class="section">'
+ '<span class="label">Weather:</span> ';
if (w.temperature !== undefined) {
html += w.temperature + "&deg;C";
}
if (w.conditions) {
html += " " + w.conditions;
}
html += "</div>";
}
// Building count from geo
if (pipe && pipe.geo && pipe.geo.building_count !== undefined) {
html += '<div class="section">'
+ '<span class="label">Buildings:</span> '
+ pipe.geo.building_count + "</div>";
}
// Vitals
if (pipe && pipe.vitals) {
var v = pipe.vitals;
html += '<div class="section">'
+ '<span class="label">Vitals:</span> ';
if (v.breathing_rate !== undefined) {
html += "BR " + v.breathing_rate + "/min";
}
if (v.motion_score !== undefined) {
html += " Motion " + (v.motion_score * 100).toFixed(0) + "%";
}
html += "</div>";
}
document.getElementById("stats").innerHTML = html;
}
} catch(e) {}
}
// Wire the camera CTA: shown only when we'll be rendering the demo path
// (auto-with-no-backend or explicit ?backend=demo). Hidden in live/remote.
(function wireCamCta() {
var btn = document.getElementById("cam-cta");
if (!btn) return;
// Hide CTA when user explicitly required live data.
if (requireLive || backendArg.startsWith("http")) {
btn.classList.add("hidden");
return;
}
btn.addEventListener("click", function() {
btn.textContent = "Initializing the Vault…";
startFaceMesh();
});
})();
fetchCloud();
setInterval(fetchCloud, 100); // 10 Hz — denser updates so face mesh feels live and the spiral animates smoothly
function updateSplats(splats) {
if (pointsMesh) scene.remove(pointsMesh);
var geometry = new THREE.BufferGeometry();
var positions = new Float32Array(splats.length * 3);
var colors = new Float32Array(splats.length * 3);
var i, s;
for (i = 0; i < splats.length; i++) {
s = splats[i];
positions[i*3] = s.center[0];
positions[i*3+1] = -s.center[1];
positions[i*3+2] = s.center[2];
colors[i*3] = s.color[0];
colors[i*3+1] = s.color[1];
colors[i*3+2] = s.color[2];
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
pointsMesh = new THREE.Points(geometry, new THREE.PointsMaterial({
size: 0.02, vertexColors: true, sizeAttenuation: true
}));
scene.add(pointsMesh);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener("resize", function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>