Files
games/all-games/picoswitch.html
T
2026-06-17 09:13:06 +02:00

924 lines
27 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>PicoSwitch Pulse Cut</title>
<style>
:root {
color-scheme: dark;
--bg: #070b10;
--panel: rgba(13, 19, 28, 0.9);
--line: rgba(255, 255, 255, 0.16);
--text: #f5f7fb;
--muted: #a8b3c4;
--xray: #6ce7ff;
--acoustic: #f2d16b;
--good: #8be27e;
--bad: #f06f63;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
}
body {
min-height: 100%;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
background:
radial-gradient(circle at 72% 12%, rgba(108, 231, 255, 0.19), transparent 28%),
radial-gradient(circle at 18% 84%, rgba(242, 209, 107, 0.12), transparent 30%),
linear-gradient(135deg, #071019 0%, #101722 48%, #080b0f 100%);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.shell {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100%;
gap: 14px;
padding: 18px;
}
header,
footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.brand {
display: grid;
gap: 2px;
}
.brand small {
color: var(--xray);
font-size: 0.78rem;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: clamp(1.35rem, 2.4vw, 2.15rem);
font-weight: 900;
letter-spacing: 0;
}
.stats {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.stat,
.tip,
button {
border: 1px solid var(--line);
background: var(--panel);
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(12px);
}
.stat {
min-width: 96px;
border-radius: 8px;
padding: 8px 10px;
}
.stat span {
display: block;
color: var(--muted);
font-size: 0.7rem;
font-weight: 900;
text-transform: uppercase;
}
.stat strong {
display: block;
margin-top: 2px;
font-size: 1rem;
font-variant-numeric: tabular-nums;
}
main {
position: relative;
min-height: 0;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 8px;
background: #08101a;
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.38);
}
.watermark {
position: absolute;
inset: 0;
z-index: 2;
display: grid;
place-items: center;
pointer-events: none;
opacity: 0.34;
transform: rotate(-18deg);
}
.watermark img {
width: min(62vmin, 520px);
height: auto;
}
canvas {
position: relative;
z-index: 1;
display: block;
width: 100%;
height: 100%;
touch-action: manipulation;
cursor: crosshair;
}
.overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 24px;
pointer-events: none;
z-index: 4;
transition: opacity 170ms ease, transform 170ms ease;
}
.overlay.hidden {
opacity: 0;
transform: scale(0.98);
}
.modal {
width: min(820px, calc(100vw - 20px));
border: 1px solid var(--line);
border-radius: 8px;
padding: 22px;
background: rgba(6, 10, 15, 0.9);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
pointer-events: auto;
}
.modal h2 {
margin: 0 0 8px;
font-size: 1.55rem;
letter-spacing: 0;
}
.modal p {
margin: 0 0 18px;
color: var(--muted);
line-height: 1.5;
}
.score-report {
display: grid;
place-items: center;
margin: 6px 0 18px;
border: 1px solid rgba(108, 231, 255, 0.32);
border-radius: 8px;
padding: 16px;
background: rgba(108, 231, 255, 0.08);
}
.score-report[hidden],
.result-report[hidden] {
display: none;
}
.score-report strong {
display: block;
color: var(--text);
font-size: clamp(3.6rem, 9vw, 6.6rem);
line-height: 0.9;
font-variant-numeric: tabular-nums;
}
.score-report span {
margin-top: 8px;
color: var(--xray);
font-size: 0.9rem;
font-weight: 900;
text-transform: uppercase;
}
.result-report {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin: 0 0 18px;
}
.report-item {
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px;
background: rgba(255, 255, 255, 0.06);
text-align: center;
}
.report-item strong {
display: block;
margin-bottom: 7px;
color: var(--text);
font-size: clamp(2.2rem, 5vw, 4.4rem);
line-height: 0.95;
font-variant-numeric: tabular-nums;
}
.report-item span {
display: block;
color: var(--xray);
font-weight: 900;
text-transform: uppercase;
font-size: 0.76rem;
}
.report-item p {
margin: 10px 0 0;
color: var(--muted);
font-size: clamp(0.92rem, 1.6vw, 1.05rem);
line-height: 1.35;
text-align: left;
}
.actions,
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
button {
min-height: 42px;
border-radius: 8px;
padding: 0 15px;
color: var(--text);
font: inherit;
font-weight: 900;
cursor: pointer;
}
button.primary {
border-color: rgba(108, 231, 255, 0.55);
background: linear-gradient(180deg, #8ff0ff, #31a8cb);
color: #031018;
}
button:focus-visible {
outline: 3px solid rgba(242, 209, 107, 0.75);
outline-offset: 2px;
}
.tip {
border-radius: 8px;
padding: 8px 10px;
color: var(--muted);
font-size: 0.86rem;
}
.controls {
justify-content: flex-end;
}
.trigger-button {
position: absolute;
left: 50%;
bottom: 14px;
z-index: 3;
width: min(430px, calc(100% - 28px));
min-height: 62px;
border-color: rgba(108, 231, 255, 0.58);
background: linear-gradient(180deg, rgba(108, 231, 255, 0.24), rgba(108, 231, 255, 0.08));
color: var(--text);
touch-action: none;
}
.trigger-button:active {
border-color: rgba(242, 209, 107, 0.78);
background: linear-gradient(180deg, #8ff0ff, #31a8cb);
color: #031018;
transform: translateX(-50%) translateY(1px);
}
kbd {
min-width: 24px;
border: 1px solid var(--line);
border-bottom-color: rgba(255, 255, 255, 0.34);
border-radius: 6px;
padding: 2px 6px;
background: rgba(255, 255, 255, 0.1);
color: var(--text);
font-family: inherit;
font-size: 0.78rem;
font-weight: 900;
}
@media (max-width: 760px) {
body {
overflow: auto;
}
.shell {
min-height: 100%;
height: auto;
grid-template-rows: auto minmax(560px, 1fr) auto;
padding: 10px;
}
header,
footer {
align-items: stretch;
flex-direction: column;
}
.stats,
.controls {
justify-content: flex-start;
}
.stat {
flex: 1 1 96px;
}
.result-report {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="shell">
<header>
<div class="brand">
<small>txproducts.de presents</small>
<h1>PicoSwitch Pulse Cut</h1>
</div>
<div class="stats" aria-live="polite">
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
<div class="stat"><span>Pulse</span><strong id="pulse">-- ps</strong></div>
<div class="stat"><span>Coherence</span><strong id="coherence">100%</strong></div>
<div class="stat"><span>Round</span><strong id="round">1 / 8</strong></div>
</div>
</header>
<main>
<canvas id="game" aria-label="PicoSwitch photo-acoustic Bragg pulse timing game"></canvas>
<div class="watermark" aria-hidden="true"><img src="assets/txproducts-logo-watermark.png" alt=""></div>
<button class="trigger-button" id="triggerButton" type="button" hidden>Trigger PicoSwitch</button>
<div class="overlay" id="overlay">
<section class="modal">
<h2 id="modalTitle">Are you ready?</h2>
<p id="modalText">You are about to trigger PicoSwitch. Fire when the pulse reaches the Bragg gate and the acoustic bands are aligned.</p>
<div class="score-report" id="scoreReport" hidden>
<strong id="finalScore">0</strong>
<span>Final score</span>
</div>
<div class="result-report" id="resultReport" hidden>
<div class="report-item">
<strong id="fivePsCount">0</strong>
<span>5 ps Cut</span>
<p>Best timing: pulse and acoustic wave aligned for the shortest cut.</p>
</div>
<div class="report-item">
<strong id="picoCutCount">0</strong>
<span>Pico Cut</span>
<p>Successful pulse shortening into the 5-10 ps target range.</p>
</div>
<div class="report-item">
<strong id="weakCount">0</strong>
<span>Weak Diffraction</span>
<p>The pulse hit the gate, but alignment was not clean enough.</p>
</div>
<div class="report-item">
<strong id="missCount">0</strong>
<span>Missed</span>
<p>The trigger fired outside the useful Bragg timing moment.</p>
</div>
</div>
<div class="actions">
<button class="primary" id="startButton" type="button" onclick="startGame(false)" ontouchstart="event.preventDefault(); startGame(false);">I'm ready</button>
</div>
</section>
</div>
</main>
<footer>
<div class="tip" id="message">Tap at the picosecond moment: pulse, acoustic wave, Bragg gate.</div>
<div class="controls" aria-label="Controls">
<span><kbd>Space</kbd> trigger</span>
<span><kbd>Tap</kbd> trigger</span>
<span><kbd>R</kbd> restart</span>
<span><kbd>Esc</kbd> stop</span>
<button id="stopButton" type="button">Stop demo</button>
<button type="button" onclick="window.location.href='index.html'">Back to menu</button>
</div>
</footer>
</div>
<script>
const canvas = document.querySelector("#game");
const ctx = canvas.getContext("2d");
const scoreEl = document.querySelector("#score");
const pulseEl = document.querySelector("#pulse");
const coherenceEl = document.querySelector("#coherence");
const roundEl = document.querySelector("#round");
const messageEl = document.querySelector("#message");
const overlay = document.querySelector("#overlay");
const modalTitle = document.querySelector("#modalTitle");
const modalText = document.querySelector("#modalText");
const scoreReport = document.querySelector("#scoreReport");
const finalScore = document.querySelector("#finalScore");
const resultReport = document.querySelector("#resultReport");
const fivePsCount = document.querySelector("#fivePsCount");
const picoCutCount = document.querySelector("#picoCutCount");
const weakCount = document.querySelector("#weakCount");
const missCount = document.querySelector("#missCount");
const startButton = document.querySelector("#startButton");
const triggerButton = document.querySelector("#triggerButton");
const stopButton = document.querySelector("#stopButton");
const state = {
mode: "menu",
score: 0,
pulsePs: null,
coherence: 100,
round: 1,
totalRounds: 8,
practice: false,
lastTime: 0,
gateX: 0,
beamY: 0,
pulseX: 0,
pulseSpeed: 0,
pulseLength: 170,
acousticPhase: 0,
acousticSpeed: 2.2,
triggerFlash: 0,
report: { five: 0, pico: 0, weak: 0, miss: 0 },
result: null
};
function fitCanvas() {
const rect = canvas.getBoundingClientRect();
const scale = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.floor(rect.width * scale));
canvas.height = Math.max(1, Math.floor(rect.height * scale));
ctx.setTransform(scale, 0, 0, scale, 0, 0);
state.gateX = canvas.clientWidth * 0.56;
state.beamY = canvas.clientHeight * 0.48;
}
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function startGame(practice = false) {
state.mode = "armed";
state.practice = practice;
state.score = 0;
state.pulsePs = null;
state.coherence = 100;
state.round = 1;
state.report = { five: 0, pico: 0, weak: 0, miss: 0 };
overlay.classList.add("hidden");
scoreReport.hidden = true;
resultReport.hidden = true;
triggerButton.hidden = false;
nextRound();
}
function nextRound() {
const w = canvas.clientWidth;
state.mode = "armed";
state.gateX = w * 0.56;
state.beamY = canvas.clientHeight * 0.48;
state.pulseX = -state.pulseLength - rand(0, w * 0.22);
state.pulseSpeed = 260 + state.round * 46 + rand(-15, 45);
state.pulseLength = Math.max(92, 178 - state.round * 8);
state.acousticSpeed = 2.0 + state.round * 0.22;
state.triggerFlash = 0;
state.result = null;
triggerButton.hidden = false;
messageEl.textContent = "Trigger when the x-ray pulse is inside the Bragg gate and the acoustic bands align.";
updateHud();
}
function updateHud() {
scoreEl.textContent = state.score;
pulseEl.textContent = state.pulsePs ? `${state.pulsePs.toFixed(1)} ps` : "-- ps";
coherenceEl.textContent = `${Math.max(0, Math.round(state.coherence))}%`;
roundEl.textContent = state.practice ? `${state.round}` : `${state.round} / ${state.totalRounds}`;
}
function trigger() {
if (state.mode !== "armed") return;
const distance = Math.abs(state.pulseX - state.gateX);
const pulseWindow = Math.max(34, state.pulseLength * 0.34);
const pulseHit = Math.max(0, 1 - distance / pulseWindow);
const phaseHit = acousticAlignment();
const precision = Math.pow(pulseHit, 0.72) * (0.55 + Math.pow(phaseHit, 0.9) * 0.45);
const pulsePs = 32 - precision * 27;
const clampedPulse = Math.max(4.8, pulsePs);
const coherence = Math.max(8, 55 + precision * 45 - Math.max(0, distance - 8) * 0.18);
let points = Math.round(precision * 700 + Math.max(0, 12 - clampedPulse) * 80);
let label = "MISSED";
let color = getCss("--bad");
if (clampedPulse <= 6.2) {
label = "5 ps CUT";
state.report.five += 1;
color = getCss("--good");
messageEl.textContent = `Clean Bragg cut: ${clampedPulse.toFixed(1)} ps pulse. +${points}`;
} else if (clampedPulse <= 10) {
label = "PICO CUT";
state.report.pico += 1;
color = getCss("--xray");
messageEl.textContent = `Pulse shortened to ${clampedPulse.toFixed(1)} ps. +${points}`;
} else if (pulseHit > 0.38 || precision > 0.22) {
label = "WEAK DIFFRACTION";
state.report.weak += 1;
color = getCss("--acoustic");
points = Math.max(points, Math.round(pulseHit * 180));
messageEl.textContent = phaseHit < 0.35
? `Pulse was in the gate, but the acoustic wave was out of phase. +${points}`
: `Partial cut: ${clampedPulse.toFixed(1)} ps. +${points}`;
} else {
state.report.miss += 1;
points = 0;
state.coherence = Math.max(0, state.coherence - 18);
messageEl.textContent = "Timing missed: trigger when the pulse and acoustic bands align.";
}
state.score += points;
state.pulsePs = clampedPulse;
state.coherence = label === "MISSED" ? state.coherence : coherence;
state.triggerFlash = 1;
state.result = { label, color, precision, age: 0 };
state.mode = "result";
triggerButton.hidden = true;
updateHud();
window.setTimeout(() => {
if (state.mode !== "result") return;
if (!state.practice && state.round >= state.totalRounds) {
endGame();
return;
}
state.round += 1;
nextRound();
}, 820);
}
function acousticAlignment() {
const phase = Math.sin(state.acousticPhase);
return 1 - Math.min(1, Math.abs(phase) / 0.92);
}
function triggerReadiness() {
const distance = Math.abs(state.pulseX - state.gateX);
const pulseWindow = Math.max(34, state.pulseLength * 0.34);
const pulseHit = Math.max(0, 1 - distance / pulseWindow);
return {
pulseHit,
phaseHit: acousticAlignment(),
ready: pulseHit > 0.42 && acousticAlignment() > 0.32
};
}
function endGame() {
state.mode = "over";
triggerButton.hidden = true;
modalTitle.textContent = "Pulse run complete";
modalText.textContent = `Coherence: ${Math.round(state.coherence)}%. Best runs hold coherence while cutting x-ray pulses into the 5-10 ps range.`;
showReport();
startButton.textContent = "Run again";
overlay.classList.remove("hidden");
updateHud();
}
function restart() {
startGame(state.practice);
}
function stopGame() {
state.mode = "over";
triggerButton.hidden = true;
modalTitle.textContent = "Demo paused";
modalText.textContent = "Start again when the next visitor is ready. The goal is to trigger PicoSwitch when the pulse reaches the Bragg gate and the acoustic bands align.";
scoreReport.hidden = true;
resultReport.hidden = true;
startButton.textContent = "I'm ready";
overlay.classList.remove("hidden");
messageEl.textContent = "Stopped. Ready for the next booth visitor.";
}
function showReport() {
finalScore.textContent = state.score;
fivePsCount.textContent = state.report.five;
picoCutCount.textContent = state.report.pico;
weakCount.textContent = state.report.weak;
missCount.textContent = state.report.miss;
scoreReport.hidden = false;
resultReport.hidden = false;
}
function update(dt) {
state.acousticPhase += state.acousticSpeed * dt;
if (state.mode === "armed") {
state.pulseX += state.pulseSpeed * dt;
if (state.pulseX - state.pulseLength > canvas.clientWidth + 80) {
state.coherence = Math.max(0, state.coherence - 10);
state.pulsePs = null;
state.result = { label: "NO TRIGGER", color: getCss("--bad"), precision: 0, age: 0 };
state.mode = "result";
triggerButton.hidden = true;
messageEl.textContent = "No trigger: repetition rate outran the switch.";
updateHud();
window.setTimeout(() => {
if (state.mode !== "result") return;
if (!state.practice && state.round >= state.totalRounds) endGame();
else {
state.round += 1;
nextRound();
}
}, 700);
}
}
if (state.triggerFlash > 0) state.triggerFlash = Math.max(0, state.triggerFlash - dt * 2.7);
if (state.result) state.result.age += dt;
}
function draw() {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
ctx.clearRect(0, 0, w, h);
drawInstrumentBackground(w, h);
drawBeamline(w);
drawCrystalGate();
drawAcousticWave();
drawPulse();
drawDetector(w);
drawTimingGuide(w);
drawResult();
}
function drawInstrumentBackground(w, h) {
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, "#0a1420");
grad.addColorStop(0.56, "#111a22");
grad.addColorStop(1, "#070a0d");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
ctx.save();
ctx.globalAlpha = 0.16;
ctx.strokeStyle = "#d7efff";
ctx.lineWidth = 1;
for (let x = -40; x < w + 60; x += 44) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x + 76, h);
ctx.stroke();
}
for (let y = 70; y < h; y += 72) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
ctx.restore();
ctx.fillStyle = "rgba(0, 0, 0, 0.36)";
ctx.fillRect(0, h - 72, w, 72);
}
function drawBeamline(w) {
ctx.save();
ctx.strokeStyle = "rgba(108, 231, 255, 0.3)";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(0, state.beamY);
ctx.lineTo(w, state.beamY);
ctx.stroke();
ctx.strokeStyle = "rgba(108, 231, 255, 0.08)";
ctx.lineWidth = 22;
ctx.beginPath();
ctx.moveTo(0, state.beamY);
ctx.lineTo(w, state.beamY);
ctx.stroke();
ctx.restore();
}
function drawCrystalGate() {
const gateW = 88;
const gateH = 220;
const x = state.gateX - gateW / 2;
const y = state.beamY - gateH / 2;
ctx.save();
ctx.fillStyle = "rgba(108, 231, 255, 0.08)";
ctx.strokeStyle = "rgba(108, 231, 255, 0.65)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x, y, gateW, gateH, 8);
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 0.55;
ctx.strokeStyle = getCss("--acoustic");
ctx.setLineDash([8, 8]);
ctx.beginPath();
ctx.moveTo(state.gateX, y - 20);
ctx.lineTo(state.gateX, y + gateH + 20);
ctx.stroke();
ctx.restore();
}
function drawAcousticWave() {
const gateW = 88;
const gateH = 220;
const left = state.gateX - gateW / 2;
const top = state.beamY - gateH / 2;
ctx.save();
ctx.beginPath();
ctx.rect(left, top, gateW, gateH);
ctx.clip();
for (let i = -5; i < 12; i += 1) {
const y = top + i * 26 + ((state.acousticPhase * 34) % 26);
const alignmentBoost = acousticAlignment();
ctx.strokeStyle = `rgba(242, 209, 107, ${0.16 + alignmentBoost * 0.28})`;
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(left - 12, y);
ctx.lineTo(left + gateW + 12, y - 30);
ctx.stroke();
}
ctx.restore();
}
function drawPulse() {
const y = state.beamY;
const x = state.pulseX;
const length = state.mode === "result" && state.pulsePs && state.pulsePs <= 10 ? 34 : state.pulseLength;
const glow = ctx.createLinearGradient(x - length, 0, x + length, 0);
glow.addColorStop(0, "rgba(108, 231, 255, 0)");
glow.addColorStop(0.48, "rgba(108, 231, 255, 0.85)");
glow.addColorStop(1, "rgba(108, 231, 255, 0)");
ctx.save();
ctx.shadowColor = getCss("--xray");
ctx.shadowBlur = 18 + state.triggerFlash * 30;
ctx.strokeStyle = glow;
ctx.lineWidth = 18;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x - length, y);
ctx.lineTo(x + length * 0.35, y);
ctx.stroke();
ctx.fillStyle = "#f5fbff";
ctx.beginPath();
ctx.arc(x, y, 7 + state.triggerFlash * 8, 0, Math.PI * 2);
ctx.fill();
if (state.triggerFlash > 0) {
ctx.strokeStyle = `rgba(242, 209, 107, ${state.triggerFlash})`;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(state.gateX, y, 42 + (1 - state.triggerFlash) * 70, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore();
}
function drawDetector(w) {
const x = w - 92;
const y = state.beamY;
ctx.save();
ctx.fillStyle = "rgba(255, 255, 255, 0.08)";
ctx.strokeStyle = "rgba(255, 255, 255, 0.28)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x, y - 58, 58, 116, 8);
ctx.fill();
ctx.stroke();
const bars = Math.max(1, Math.round((state.coherence / 100) * 5));
for (let i = 0; i < 5; i += 1) {
ctx.fillStyle = i < bars ? getCss("--good") : "rgba(255, 255, 255, 0.12)";
ctx.fillRect(x + 12, y + 35 - i * 18, 34, 9);
}
ctx.restore();
}
function drawTimingGuide(w) {
if (state.mode !== "armed") return;
const y = state.beamY + 92;
const readiness = triggerReadiness();
const heat = Math.max(readiness.pulseHit, readiness.phaseHit * 0.7);
ctx.save();
ctx.fillStyle = `rgba(108, 231, 255, ${0.14 + heat * 0.22})`;
ctx.fillRect(state.gateX - 72, y, 144, 6);
ctx.fillStyle = getCss("--acoustic");
ctx.fillRect(state.gateX - 72, y, 144 * acousticAlignment(), 6);
ctx.font = "900 13px Inter, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillStyle = readiness.ready ? getCss("--good") : "rgba(245, 247, 251, 0.78)";
ctx.fillText(readiness.ready ? "BEST ALIGNMENT" : "ALIGN PULSE + ACOUSTIC WAVE", state.gateX, y + 24);
ctx.font = "800 11px Inter, system-ui, sans-serif";
ctx.fillStyle = "rgba(245, 247, 251, 0.62)";
ctx.fillText("pulse in gate + acoustic wave aligned", state.gateX, y + 42);
ctx.restore();
}
function drawResult() {
if (!state.result) return;
const age = state.result.age;
ctx.save();
ctx.globalAlpha = Math.max(0, 1 - age * 0.55);
ctx.fillStyle = state.result.color;
ctx.font = "900 30px Inter, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillText(state.result.label, state.gateX, Math.max(48, state.beamY - 150 - age * 28));
if (state.pulsePs) {
ctx.font = "900 18px Inter, system-ui, sans-serif";
ctx.fillText(`${state.pulsePs.toFixed(1)} ps`, state.gateX, Math.max(80, state.beamY - 116 - age * 20));
}
ctx.restore();
}
function getCss(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function loop(time) {
const dt = Math.min(0.033, (time - state.lastTime) / 1000 || 0);
state.lastTime = time;
update(dt);
draw();
requestAnimationFrame(loop);
}
function onTap(element, handler) {
let handledAt = 0;
const run = (event) => {
const now = Date.now();
if (now - handledAt < 450) return;
handledAt = now;
event.preventDefault();
handler();
};
element.addEventListener("click", run);
element.addEventListener("touchend", run, { passive: false });
}
window.addEventListener("resize", fitCanvas);
window.addEventListener("keydown", (event) => {
if (event.code === "Space") {
event.preventDefault();
trigger();
}
if (event.key.toLowerCase() === "r") restart();
if (event.key === "Escape") stopGame();
});
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
triggerButton.addEventListener("contextmenu", (event) => event.preventDefault());
triggerButton.addEventListener("pointerdown", (event) => {
event.preventDefault();
trigger();
});
onTap(startButton, () => startGame(false));
onTap(stopButton, stopGame);
fitCanvas();
requestAnimationFrame(loop);
</script>
</body>
</html>