463 lines
16 KiB
HTML
463 lines
16 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>Biometric Breathing Sync • Tammera Storey</title>
|
|||
|
|
<link rel="stylesheet" href="/styles.css">
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--bg-deep: #0a0a0c;
|
|||
|
|
--bg-card: #121216;
|
|||
|
|
--text-primary: #e0e0e0;
|
|||
|
|
--accent-neon: #00ff9d;
|
|||
|
|
--accent-rust: #d45d00;
|
|||
|
|
--accent-teal: #00bfa5;
|
|||
|
|
--accent-indigo: #4b0082;
|
|||
|
|
--font-mono: 'Courier New', Courier, monospace;
|
|||
|
|
--font-display: 'Helvetica Neue', Arial, sans-serif;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
background-color: var(--bg-deep);
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
font-family: var(--font-mono);
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.canvas-wrap {
|
|||
|
|
min-height: 100vh;
|
|||
|
|
padding: 4rem 2rem;
|
|||
|
|
max-width: 1400px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
header {
|
|||
|
|
text-align: center;
|
|||
|
|
margin-bottom: 4rem;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h1 {
|
|||
|
|
font-family: var(--font-display);
|
|||
|
|
font-size: clamp(2rem, 5vw, 4rem);
|
|||
|
|
font-weight: 900;
|
|||
|
|
letter-spacing: -0.05em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
background: linear-gradient(180deg, var(--accent-neon), var(--accent-teal));
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
background-clip: text;
|
|||
|
|
color: transparent;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.subtitle {
|
|||
|
|
font-size: 1.1rem;
|
|||
|
|
color: var(--accent-teal);
|
|||
|
|
max-width: 700px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
border-left: 3px solid var(--accent-rust);
|
|||
|
|
padding-left: 1.5rem;
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.workbench {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 3rem;
|
|||
|
|
margin-bottom: 4rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 900px) {
|
|||
|
|
.workbench { grid-template-columns: 1fr; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel {
|
|||
|
|
background: var(--bg-card);
|
|||
|
|
border: 1px solid var(--accent-teal);
|
|||
|
|
padding: 2rem;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
height: 3px;
|
|||
|
|
background: linear-gradient(90deg, var(--accent-rust), var(--accent-neon));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel h2 {
|
|||
|
|
font-family: var(--font-display);
|
|||
|
|
font-size: 1.5rem;
|
|||
|
|
color: var(--accent-neon);
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel h2::before {
|
|||
|
|
content: '';
|
|||
|
|
width: 12px;
|
|||
|
|
height: 12px;
|
|||
|
|
background: var(--accent-teal);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
box-shadow: 0 0 20px var(--accent-teal);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.input-group {
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
label {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
color: var(--accent-teal);
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
input[type="number"], select {
|
|||
|
|
width: 100%;
|
|||
|
|
background: rgba(0, 191, 165, 0.1);
|
|||
|
|
border: 1px solid var(--accent-teal);
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
font-family: var(--font-mono);
|
|||
|
|
font-size: 1rem;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
input:focus, select:focus {
|
|||
|
|
outline: none;
|
|||
|
|
border-color: var(--accent-neon);
|
|||
|
|
box-shadow: 0 0 30px rgba(0, 255, 157, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crew-add {
|
|||
|
|
background: linear-gradient(180deg, var(--accent-teal), var(--accent-neon));
|
|||
|
|
border: none;
|
|||
|
|
color: var(--bg-deep);
|
|||
|
|
padding: 0.75rem 1.5rem;
|
|||
|
|
font-family: var(--font-mono);
|
|||
|
|
font-weight: 700;
|
|||
|
|
cursor: pointer;
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crew-add:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 10px 30px rgba(0, 191, 165, 0.4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crew-list {
|
|||
|
|
margin-top: 2rem;
|
|||
|
|
max-height: 300px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crew-member {
|
|||
|
|
background: rgba(0, 191, 165, 0.05);
|
|||
|
|
border-left: 3px solid var(--accent-rust);
|
|||
|
|
padding: 1rem;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crew-member .remove {
|
|||
|
|
color: var(--accent-rust);
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.viz-canvas {
|
|||
|
|
background: radial-gradient(circle at center, rgba(0, 191, 165, 0.1), transparent 70%);
|
|||
|
|
border: 1px dashed var(--accent-teal);
|
|||
|
|
min-height: 500px;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pulse-ring {
|
|||
|
|
position: absolute;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
border: 2px solid var(--accent-teal);
|
|||
|
|
opacity: 0;
|
|||
|
|
animation: pulse-wave 6s ease-out infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes pulse-wave {
|
|||
|
|
0% { transform: scale(1); opacity: 1; }
|
|||
|
|
100% { transform: scale(4); opacity: 0; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.metric-display {
|
|||
|
|
position: absolute;
|
|||
|
|
background: rgba(10, 10, 12, 0.9);
|
|||
|
|
border: 1px solid var(--accent-neon);
|
|||
|
|
padding: 1rem;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.metric-display.bpm { top: 2rem; left: 2rem; }
|
|||
|
|
.metric-display.phase { top: 2rem; right: 2rem; }
|
|||
|
|
.metric-display.stability { bottom: 2rem; left: 2rem; }
|
|||
|
|
.metric-display.co2 { bottom: 2rem; right: 2rem; }
|
|||
|
|
|
|||
|
|
.metric-value {
|
|||
|
|
font-family: var(--font-display);
|
|||
|
|
font-size: 2rem;
|
|||
|
|
font-weight: 900;
|
|||
|
|
color: var(--accent-neon);
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.compute-btn {
|
|||
|
|
width: 100%;
|
|||
|
|
background: linear-gradient(180deg, var(--accent-rust), var(--accent-neon));
|
|||
|
|
border: none;
|
|||
|
|
color: var(--bg-deep);
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
font-family: var(--font-display);
|
|||
|
|
font-size: 1.2rem;
|
|||
|
|
font-weight: 900;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 0.1em;
|
|||
|
|
cursor: pointer;
|
|||
|
|
margin-top: 2rem;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.compute-btn:hover {
|
|||
|
|
transform: scale(1.02);
|
|||
|
|
box-shadow: 0 20px 60px rgba(212, 93, 0, 0.4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.formula-block {
|
|||
|
|
background: rgba(0, 191, 165, 0.05);
|
|||
|
|
border: 1px solid var(--accent-teal);
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
margin-top: 3rem;
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.formula-block pre {
|
|||
|
|
overflow-x: auto;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
word-break: break-all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grounding-ref {
|
|||
|
|
color: var(--accent-teal);
|
|||
|
|
font-style: italic;
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.vinyl-status {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 2rem;
|
|||
|
|
left: 2rem;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
color: var(--accent-teal);
|
|||
|
|
background: rgba(10,10,12,0.9);
|
|||
|
|
padding: 1rem;
|
|||
|
|
border-left: 2px solid var(--accent-neon);
|
|||
|
|
max-width: 300px;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
<script defer src="https://analytics.4ort.xyz/script.js" data-website-id="d3ed927c-888a-4a6c-ae5f-0b1c613ddf5b"></script>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="vinyl-status">
|
|||
|
|
Now spinning: Talk Talk's 'Spirit of Eden' while calibrating crew coherence.<br>
|
|||
|
|
<span style="opacity:0.7">// Target BPM: 6.0 // Phase Lock: Pending</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="canvas-wrap">
|
|||
|
|
<header>
|
|||
|
|
<h1>Biometric<br>Breathing Sync</h1>
|
|||
|
|
<p class="subtitle">
|
|||
|
|
Input crew vitals. Output the exact tempo that locks hearts to lungs.
|
|||
|
|
No metaphors. Only the math of survival.
|
|||
|
|
</p>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<main class="workbench">
|
|||
|
|
<section class="panel">
|
|||
|
|
<h2>Crew Ingest</h2>
|
|||
|
|
|
|||
|
|
<div class="input-group">
|
|||
|
|
<label for="hrv">Heart Rate Variability (ms)</label>
|
|||
|
|
<input type="number" id="hrv" placeholder="e.g., 45" min="20" max="200">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="input-group">
|
|||
|
|
<label for="respRate">Respiration Rate (breaths/min)</label>
|
|||
|
|
<input type="number" id="respRate" placeholder="e.g., 6" step="0.1" min="4" max="20">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="input-group">
|
|||
|
|
<label for="co2">Cabin CO₂ (ppm)</label>
|
|||
|
|
<input type="number" id="co2" placeholder="e.g., 450" min="400" max="10000">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<button class="crew-add" onclick="addCrewMember()">+ Add Crew Member</button>
|
|||
|
|
|
|||
|
|
<div class="crew-list" id="crewList"></div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="panel">
|
|||
|
|
<h2>Coherence Viz</h2>
|
|||
|
|
|
|||
|
|
<div class="viz-canvas" id="vizCanvas">
|
|||
|
|
<div class="pulse-ring" style="top:50%;left:50%;width:100px;height:100px;transform:translate(-50%,-50%);animation-delay:0s;"></div>
|
|||
|
|
<div class="pulse-ring" style="top:50%;left:50%;width:100px;height:100px;transform:translate(-50%,-50%);animation-delay:1s;"></div>
|
|||
|
|
<div class="pulse-ring" style="top:50%;left:50%;width:100px;height:100px;transform:translate(-50%,-50%);animation-delay:2s;"></div>
|
|||
|
|
|
|||
|
|
<div class="metric-display bpm">
|
|||
|
|
<span>TARGET GROUP BPM</span>
|
|||
|
|
<span class="metric-value" id="displayBPM">—</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="metric-display phase">
|
|||
|
|
<span>PHASE OFFSET (°)</span>
|
|||
|
|
<span class="metric-value" id="displayPhase">—</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="metric-display stability">
|
|||
|
|
<span>STABILITY SCORE</span>
|
|||
|
|
<span class="metric-value" id="displayStability">—</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="metric-display co2">
|
|||
|
|
<span>CO₂ STATUS</span>
|
|||
|
|
<span class="metric-value" id="displayCO2">—</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<button class="compute-btn" onclick="computeSync()">LOCK PHASE</button>
|
|||
|
|
</section>
|
|||
|
|
</main>
|
|||
|
|
|
|||
|
|
<section class="formula-block">
|
|||
|
|
<h3 style="color:var(--accent-neon);margin-bottom:1rem;">GROUNDING FORMULAS</h3>
|
|||
|
|
<pre id="formulaDisplay">
|
|||
|
|
// Target Group BPM = argmin_f Σᵢ |HRVᵢ(f) − HRV_opt|²
|
|||
|
|
// subject to f ∈ [5.5, 6.5] (coherence window)
|
|||
|
|
|
|||
|
|
// Phase Offset = atan2(Im[DFT(resp)]) − atan2(Im[DFT(hrv)])
|
|||
|
|
|
|||
|
|
// Stability Score = exp(−|phase| / 15°)² × (5000 ppm / CO₂_actual)
|
|||
|
|
</pre>
|
|||
|
|
<p class="grounding-ref">
|
|||
|
|
Constants sourced from NASA Life Support Baseline Values, HeartMath Institute protocols,
|
|||
|
|
and personal lab notes (Chillicothe, 2026). Full spec: /breathing-sync.json
|
|||
|
|
</p>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
const CONSTANTS = {
|
|||
|
|
COHERENCE_BPM_MIN: 5.5,
|
|||
|
|
COHERENCE_BPM_MAX: 6.5,
|
|||
|
|
OPTIMAL_HRV_MS: 50,
|
|||
|
|
CO2_SAFE_PPM: 5000,
|
|||
|
|
PHASE_TOLERANCE_DEG: 15
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let crewMembers = [];
|
|||
|
|
|
|||
|
|
function addCrewMember() {
|
|||
|
|
const hrv = parseFloat(document.getElementById('hrv').value);
|
|||
|
|
const resp = parseFloat(document.getElementById('respRate').value);
|
|||
|
|
const co2 = parseFloat(document.getElementById('co2').value);
|
|||
|
|
|
|||
|
|
if (!hrv || !resp || !co2) {
|
|||
|
|
alert("Fill all vitals before adding crew member.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const id = Date.now();
|
|||
|
|
crewMembers.push({ id, hrv, resp, co2 });
|
|||
|
|
|
|||
|
|
renderCrewList();
|
|||
|
|
|
|||
|
|
document.getElementById('hrv').value = '';
|
|||
|
|
document.getElementById('respRate').value = '';
|
|||
|
|
document.getElementById('co2').value = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function removeMember(id) {
|
|||
|
|
crewMembers = crewMembers.filter(m => m.id !== id);
|
|||
|
|
renderCrewList();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderCrewList() {
|
|||
|
|
const list = document.getElementById('crewList');
|
|||
|
|
list.innerHTML = crewMembers.map(m => `
|
|||
|
|
<div class="crew-member">
|
|||
|
|
<span>Crew ${m.id.toString().slice(-4)}: HRV=${m.hrv}ms, Resp=${m.resp}bpm, CO₂=${m.co2}ppm</span>
|
|||
|
|
<span class="remove" onclick="removeMember(${m.id})">×</span>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function computeSync() {
|
|||
|
|
if (crewMembers.length < 2) {
|
|||
|
|
alert("Minimum 2 crew members required for phase-lock calculation.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Find optimal group BPM via coherence minimization
|
|||
|
|
let bestBPM = 6.0;
|
|||
|
|
let minError = Infinity;
|
|||
|
|
|
|||
|
|
for (let bpm = CONSTANTS.COHERENCE_BPM_MIN; bpm <= CONSTANTS.COHERENCE_BPM_MAX; bpm += 0.01) {
|
|||
|
|
let error = 0;
|
|||
|
|
crewMembers.forEach(m => {
|
|||
|
|
const predictedHRV = CONSTANTS.OPTIMAL_HRV_MS * (bpm / 6.0);
|
|||
|
|
error += Math.pow(m.hrv - predictedHRV, 2);
|
|||
|
|
});
|
|||
|
|
if (error < minError) {
|
|||
|
|
minError = error;
|
|||
|
|
bestBPM = bpm;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Calculate average phase offset from respiration diversity
|
|||
|
|
const avgResp = crewMembers.reduce((sum, m) => sum + m.resp, 0) / crewMembers.length;
|
|||
|
|
const phaseOffset = Math.abs(avgResp - bestBPM) * 360 / CONSTANTS.COHERENCE_BPM_MIN;
|
|||
|
|
|
|||
|
|
// Stability score
|
|||
|
|
const avgCO2 = crewMembers.reduce((sum, m) => sum + m.co2, 0) / crewMembers.length;
|
|||
|
|
const stability = Math.exp(-phaseOffset / CONSTANTS.PHASE_TOLERANCE_DEG) *
|
|||
|
|
(CONSTANTS.CO2_SAFE_PPM / avgCO2);
|
|||
|
|
|
|||
|
|
// Render
|
|||
|
|
document.getElementById('displayBPM').textContent = bestBPM.toFixed(2);
|
|||
|
|
document.getElementById('displayPhase').textContent = phaseOffset.toFixed(1);
|
|||
|
|
document.getElementById('displayStability').textContent = stability.toFixed(3);
|
|||
|
|
document.getElementById('displayCO2').textContent = `${avgCO2.toFixed(0)} ppm`;
|
|||
|
|
|
|||
|
|
// Animate pulses
|
|||
|
|
const canvas = document.getElementById('vizCanvas');
|
|||
|
|
Array.from(canvas.querySelectorAll('.pulse-ring')).forEach((ring, i) => {
|
|||
|
|
ring.style.animationDuration = `${bestBPM / 10}s`;
|
|||
|
|
ring.style.animationDelay = `${i * 0.5}s`;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|