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

915 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>WaveGate Pulse Tailor</title>
<style>
:root {
color-scheme: dark;
--panel: rgba(12, 18, 27, 0.9);
--line: rgba(255, 255, 255, 0.16);
--text: #f7f3ea;
--muted: #adb7c6;
--cyan: #65d8e8;
--gold: #f2c96b;
--bad: #ef735f;
--good: #88d76f;
}
* { box-sizing: border-box; }
html, body {
height: 100%;
margin: 0;
}
body {
overflow: hidden;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
background:
radial-gradient(circle at 16% 12%, rgba(101, 216, 232, 0.24), transparent 28%),
radial-gradient(circle at 84% 72%, rgba(242, 201, 107, 0.11), transparent 30%),
linear-gradient(140deg, #101926 0%, #1b242d 48%, #090c10 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(--cyan);
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 36px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(12px);
}
.stat {
min-width: 98px;
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: #101821;
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: none;
cursor: crosshair;
}
.overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 10px;
pointer-events: none;
z-index: 4;
transition: opacity 170ms ease, transform 170ms ease;
}
.overlay.hidden {
opacity: 0;
transform: scale(0.98);
}
.modal {
width: min(840px, calc(100vw - 20px));
border: 1px solid var(--line);
border-radius: 8px;
padding: 22px;
background: rgba(8, 12, 18, 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 12px;
color: var(--muted);
line-height: 1.5;
}
.how-it-works {
display: grid;
gap: 8px;
margin: 0 0 18px;
padding: 0;
list-style: none;
}
.how-it-works[hidden] {
display: none;
}
.how-it-works li {
display: grid;
grid-template-columns: 24px 1fr;
gap: 9px;
align-items: start;
color: var(--muted);
line-height: 1.38;
}
.how-it-works b {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border: 1px solid rgba(101, 216, 232, 0.45);
border-radius: 50%;
color: var(--cyan);
font-size: 0.74rem;
}
.result-report {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin: 8px 0 22px;
}
.result-report[hidden] {
display: none;
}
.report-item {
border: 1px solid var(--line);
border-radius: 8px;
padding: 20px 16px;
background: rgba(255, 255, 255, 0.06);
text-align: center;
}
.report-item strong {
display: block;
margin-bottom: 9px;
color: var(--text);
font-size: clamp(3.3rem, 8vw, 6rem);
line-height: 0.95;
font-variant-numeric: tabular-nums;
}
.report-item span {
display: block;
color: var(--gold);
font-weight: 900;
text-transform: uppercase;
font-size: 0.86rem;
}
.report-item p {
display: block;
margin: 12px 0 0;
color: var(--muted);
font-size: clamp(1rem, 2.2vw, 1.18rem);
line-height: 1.38;
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(242, 201, 107, 0.6);
background: linear-gradient(180deg, #f3d16e, #d89a37);
color: #211609;
}
.tip {
border-radius: 8px;
padding: 8px 10px;
color: var(--muted);
font-size: 0.86rem;
}
.controls { justify-content: flex-end; }
.touch-gate {
min-height: 58px;
border-color: rgba(101, 216, 232, 0.52);
background: linear-gradient(180deg, rgba(101, 216, 232, 0.2), rgba(101, 216, 232, 0.08));
color: var(--text);
touch-action: none;
min-width: 230px;
}
.touch-gate.is-open,
.touch-gate:active {
border-color: rgba(242, 201, 107, 0.78);
background: linear-gradient(180deg, #f3d16e, #d89a37);
color: #211609;
}
.floating-gate {
position: absolute;
left: 50%;
bottom: 14px;
z-index: 3;
width: min(430px, calc(100% - 28px));
transform: translateX(-50%);
}
.floating-gate.is-open,
.floating-gate:active {
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 98px; }
.result-report { grid-template-columns: 1fr; }
.touch-gate {
width: 100%;
min-height: 72px;
font-size: 1.08rem;
}
}
</style>
</head>
<body>
<div class="shell">
<header>
<div class="brand">
<small>txproducts.de presents</small>
<h1>WaveGate Pulse Tailor</h1>
</div>
<div class="stats">
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
<div class="stat"><span>Timescale</span><strong id="scale">ns</strong></div>
<div class="stat"><span>Contrast</span><strong id="contrast">100%</strong></div>
<div class="stat"><span>Round</span><strong id="round">1 / 8</strong></div>
</div>
</header>
<main>
<canvas id="game" aria-label="WaveGate temporal synchrotron pulse tailoring game"></canvas>
<div class="watermark" aria-hidden="true"><img src="assets/txproducts-logo-watermark.png" alt=""></div>
<button class="touch-gate floating-gate" id="gateButton" type="button">Press + hold to open WaveGate</button>
<div class="overlay" id="overlay">
<section class="modal">
<h2 id="modalTitle">Are you ready?</h2>
<p id="modalText">You are about to control WaveGate: decide exactly when synchrotron pulses are allowed through.</p>
<ul class="how-it-works" id="howItWorks">
<li><b>1</b><span>Watch pulses move along the timeline.</span></li>
<li><b>2</b><span>The gold window is the structure to create.</span></li>
<li><b>3</b><span>Hold Space or press the screen to open WaveGate.</span></li>
<li><b>4</b><span>Let gold pulses through. Block the rest.</span></li>
</ul>
<div class="result-report" id="resultReport" hidden>
<div class="report-item">
<strong id="tailoredCount">0</strong>
<span>Tailored</span>
<p>Excellent capture of the target window with almost no leakage.</p>
</div>
<div class="report-item">
<strong id="shapedCount">0</strong>
<span>Shaped</span>
<p>Useful pulse structure, but some target pulses or blockers were missed.</p>
</div>
<div class="report-item">
<strong id="leakageCount">0</strong>
<span>Leakage</span>
<p>Too many unwanted pulses passed through the gate.</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">Transmit the highlighted pulse window. Block everything else.</div>
<div class="controls">
<span><kbd>Hold Space</kbd> open gate</span>
<span><kbd>Press</kbd> open gate</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 scaleEl = document.querySelector("#scale");
const contrastEl = document.querySelector("#contrast");
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 howItWorks = document.querySelector("#howItWorks");
const resultReport = document.querySelector("#resultReport");
const tailoredCount = document.querySelector("#tailoredCount");
const shapedCount = document.querySelector("#shapedCount");
const leakageCount = document.querySelector("#leakageCount");
const startButton = document.querySelector("#startButton");
const stopButton = document.querySelector("#stopButton");
const gateButton = document.querySelector("#gateButton");
const scales = ["300 ns", "800 ns", "2 us", "20 us", "1 ms", "100 ms", "1 s", "seconds"];
const state = {
mode: "menu",
score: 0,
contrast: 100,
round: 1,
totalRounds: 8,
practice: false,
gateOpen: false,
time: 0,
duration: 5.2,
lastTime: 0,
pulses: [],
transmitted: [],
report: { tailored: 0, shaped: 0, leakage: 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);
}
function css(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function startGame(practice = false) {
state.mode = "running";
state.practice = practice;
state.score = 0;
state.contrast = 100;
state.round = 1;
state.report = { tailored: 0, shaped: 0, leakage: 0 };
overlay.classList.add("hidden");
resultReport.hidden = true;
gateButton.hidden = false;
nextRound();
}
function showIntro() {
state.mode = "menu";
state.gateOpen = false;
gateButton.classList.remove("is-open");
gateButton.hidden = true;
modalTitle.textContent = "Are you ready?";
modalText.textContent = "You are about to control WaveGate: decide exactly when synchrotron pulses are allowed through.";
howItWorks.hidden = false;
resultReport.hidden = true;
startButton.textContent = "I'm ready";
overlay.classList.remove("hidden");
messageEl.textContent = "Transmit the highlighted pulse window. Block everything else.";
updateHud();
}
function nextRound() {
state.mode = "running";
state.time = 0;
state.duration = Math.max(3.4, 5.3 - state.round * 0.18);
state.gateOpen = false;
gateButton.classList.remove("is-open");
gateButton.hidden = false;
state.transmitted = [];
state.result = null;
buildPulses();
messageEl.textContent = state.round < 5
? "Fast structure: catch only the highlighted burst window."
: "Long structure: hold the gate through the requested macro window.";
updateHud();
}
function buildPulses() {
const count = 18 + state.round * 2;
const start = 0.45;
const end = state.duration - 0.35;
const step = (end - start) / count;
const windowWidth = state.round < 5 ? step * (2.3 + state.round * 0.12) : step * (4.2 + state.round * 0.45);
const windowStart = start + step * (4 + Math.floor(Math.random() * Math.max(1, count - 9)));
const windowEnd = Math.min(end - step, windowStart + windowWidth);
state.targetStart = windowStart;
state.targetEnd = windowEnd;
state.pulses = [];
for (let i = 0; i < count; i += 1) {
const t = start + i * step;
state.pulses.push({
t,
strength: 0.72 + Math.random() * 0.28,
desired: t >= windowStart && t <= windowEnd,
judged: false,
passed: false
});
}
}
function updateHud() {
scoreEl.textContent = state.score;
scaleEl.textContent = scales[Math.min(scales.length - 1, state.round - 1)];
contrastEl.textContent = `${Math.max(0, Math.round(state.contrast))}%`;
roundEl.textContent = state.practice ? `${state.round}` : `${state.round} / ${state.totalRounds}`;
}
function setGate(open) {
if (state.mode !== "running") {
state.gateOpen = false;
gateButton.classList.remove("is-open");
return;
}
state.gateOpen = open;
gateButton.classList.toggle("is-open", open);
}
function scorePulse(pulse) {
pulse.judged = true;
pulse.passed = state.gateOpen;
const correct = pulse.desired === pulse.passed;
if (correct) {
state.score += pulse.desired ? 95 : 22;
} else {
state.contrast -= pulse.desired ? 12 : 8;
}
if (pulse.passed) {
state.transmitted.push({ t: pulse.t, desired: pulse.desired, age: 0 });
}
updateHud();
}
function finishRound() {
if (state.mode !== "running") return;
const desired = state.pulses.filter((p) => p.desired);
const passedDesired = desired.filter((p) => p.passed).length;
const leaks = state.pulses.filter((p) => !p.desired && p.passed).length;
const capture = passedDesired / Math.max(1, desired.length);
const purity = 1 - leaks / Math.max(1, state.pulses.length - desired.length);
const quality = Math.max(0, capture * 0.62 + purity * 0.38);
if (quality > 0.92) {
state.report.tailored += 1;
state.result = { text: "TAILORED", color: css("--gold"), age: 0 };
state.score += 350;
messageEl.textContent = `Temporal structure tailored on the ${scaleEl.textContent} scale.`;
} else if (quality > 0.68) {
state.report.shaped += 1;
state.result = { text: "SHAPED", color: css("--good"), age: 0 };
state.score += 150;
messageEl.textContent = "Useful pulse structure, with some leakage.";
} else {
state.report.leakage += 1;
state.result = { text: "LEAKAGE", color: css("--bad"), age: 0 };
messageEl.textContent = "Too much unwanted pulse transmission.";
}
state.contrast = Math.max(0, state.contrast * (0.76 + quality * 0.24));
if (state.report.leakage >= 3) {
failRun("Leakage threshold reached", "Three leakage rounds means too many unwanted pulses passed through the gate.");
return;
}
state.mode = "result";
state.gateOpen = false;
gateButton.classList.remove("is-open");
gateButton.hidden = true;
updateHud();
window.setTimeout(() => {
if (state.mode !== "result") return;
if (!state.practice && state.round >= state.totalRounds) {
endGame();
return;
}
state.round += 1;
nextRound();
}, 900);
}
function endGame() {
state.mode = "over";
state.gateOpen = false;
gateButton.classList.remove("is-open");
gateButton.hidden = true;
modalTitle.textContent = "Pulse program complete";
modalText.textContent = `Final score: ${state.score}. Contrast: ${Math.round(state.contrast)}%.`;
howItWorks.hidden = true;
showReport();
startButton.textContent = "Run again";
overlay.classList.remove("hidden");
}
function failRun(title, text) {
state.mode = "over";
state.gateOpen = false;
gateButton.classList.remove("is-open");
gateButton.hidden = true;
state.result = { text: "CONTRAST LOST", color: css("--bad"), age: 0 };
modalTitle.textContent = title;
modalText.textContent = `${text} Final score: ${state.score}.`;
howItWorks.hidden = true;
showReport();
startButton.textContent = "Try again";
overlay.classList.remove("hidden");
messageEl.textContent = "Run stopped: contrast dropped below the threshold.";
updateHud();
}
function stopGame() {
state.mode = "over";
state.gateOpen = false;
gateButton.classList.remove("is-open");
gateButton.hidden = true;
modalTitle.textContent = "Demo paused";
modalText.textContent = "Take a breath. When you are ready, open WaveGate only during the gold time window and block everything else.";
howItWorks.hidden = false;
resultReport.hidden = true;
startButton.textContent = "I'm ready";
overlay.classList.remove("hidden");
messageEl.textContent = "Stopped. Ready for the next booth visitor.";
}
function showReport() {
tailoredCount.textContent = state.report.tailored;
shapedCount.textContent = state.report.shaped;
leakageCount.textContent = state.report.leakage;
resultReport.hidden = false;
}
function restart() {
startGame(state.practice);
}
function update(dt) {
if (state.mode === "running") {
state.time += dt;
for (const pulse of state.pulses) {
if (!pulse.judged && state.time >= pulse.t) scorePulse(pulse);
if (state.mode !== "running") break;
}
if (state.time >= state.duration) finishRound();
}
for (const pulse of state.transmitted) pulse.age += dt;
if (state.result) state.result.age += dt;
}
function draw() {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
ctx.clearRect(0, 0, w, h);
drawBackground(w, h);
drawTimeline(w, h);
drawTargetWindow(w, h);
drawGate(w, h);
drawPulses(w, h);
drawOutput(w, h);
drawResult(w, h);
}
function drawBackground(w, h) {
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, "#202a35");
grad.addColorStop(0.62, "#141d27");
grad.addColorStop(1, "#080b0d");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
ctx.save();
ctx.globalAlpha = 0.16;
ctx.strokeStyle = "#e9f7ff";
ctx.lineWidth = 1;
for (let x = -20; x < w + 80; x += 42) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x + 70, h);
ctx.stroke();
}
for (let y = 70; y < h; y += 74) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
ctx.restore();
}
function timeToX(t, w) {
return 52 + (t / state.duration) * (w - 104);
}
function drawTimeline(w, h) {
const y = h * 0.48;
ctx.save();
ctx.strokeStyle = "rgba(101, 216, 232, 0.32)";
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(36, y);
ctx.lineTo(w - 36, y);
ctx.stroke();
ctx.strokeStyle = "rgba(101, 216, 232, 0.08)";
ctx.lineWidth = 24;
ctx.beginPath();
ctx.moveTo(36, y);
ctx.lineTo(w - 36, y);
ctx.stroke();
const nowX = timeToX(state.time, w);
ctx.strokeStyle = css("--gold");
ctx.lineWidth = 2;
ctx.setLineDash([7, 8]);
ctx.beginPath();
ctx.moveTo(nowX, y - 155);
ctx.lineTo(nowX, y + 155);
ctx.stroke();
ctx.restore();
}
function drawTargetWindow(w, h) {
const y = h * 0.48;
const x1 = timeToX(state.targetStart || 0, w);
const x2 = timeToX(state.targetEnd || 0, w);
ctx.save();
ctx.fillStyle = "rgba(242, 201, 107, 0.16)";
ctx.strokeStyle = "rgba(242, 201, 107, 0.76)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x1, y - 92, x2 - x1, 184, 8);
ctx.fill();
ctx.stroke();
ctx.fillStyle = css("--gold");
ctx.font = "900 12px Inter, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillText("TRANSMIT WINDOW", (x1 + x2) / 2, y - 108);
ctx.restore();
}
function drawGate(w, h) {
const y = h * 0.48;
const x = timeToX(state.time, w);
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = state.gateOpen ? "rgba(101, 216, 232, 0.24)" : "rgba(0, 0, 0, 0.55)";
ctx.strokeStyle = state.gateOpen ? css("--cyan") : "rgba(255, 255, 255, 0.32)";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.roundRect(-34, -74, 68, 148, 10);
ctx.fill();
ctx.stroke();
ctx.fillStyle = state.gateOpen ? css("--cyan") : "rgba(255,255,255,0.25)";
ctx.fillRect(-7, -82, 14, 164);
ctx.font = "900 12px Inter, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillText(state.gateOpen ? "OPEN" : "CLOSED", 0, 100);
ctx.restore();
}
function drawPulses(w, h) {
const y = h * 0.48;
ctx.save();
for (const pulse of state.pulses) {
const x = timeToX(pulse.t, w);
if (x < 24 || x > w - 24) continue;
ctx.globalAlpha = pulse.judged ? 0.32 : 0.95;
ctx.fillStyle = pulse.desired ? css("--gold") : css("--cyan");
ctx.shadowColor = ctx.fillStyle;
ctx.shadowBlur = pulse.desired ? 13 : 8;
ctx.beginPath();
ctx.arc(x, y, pulse.desired ? 8 : 5, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawOutput(w, h) {
const outY = h * 0.73;
ctx.save();
ctx.strokeStyle = "rgba(255,255,255,0.18)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(36, outY);
ctx.lineTo(w - 36, outY);
ctx.stroke();
ctx.fillStyle = "rgba(255,255,255,0.65)";
ctx.font = "900 12px Inter, system-ui, sans-serif";
ctx.fillText("OUTPUT STRUCTURE", 42, outY - 16);
for (const pulse of state.transmitted) {
const x = timeToX(pulse.t, w);
ctx.globalAlpha = pulse.desired ? 0.95 : 0.55;
ctx.fillStyle = pulse.desired ? css("--good") : css("--bad");
ctx.beginPath();
ctx.roundRect(x - 5, outY - 26, 10, 52, 4);
ctx.fill();
}
ctx.restore();
}
function drawResult(w, h) {
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.text, w / 2, Math.max(52, h * 0.26 - age * 24));
ctx.restore();
}
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();
setGate(true);
}
if (event.key.toLowerCase() === "r") restart();
if (event.key === "Escape") stopGame();
});
window.addEventListener("keyup", (event) => {
if (event.code === "Space") setGate(false);
});
window.addEventListener("blur", () => setGate(false));
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
canvas.addEventListener("pointerdown", (event) => {
event.preventDefault();
setGate(true);
});
canvas.addEventListener("pointerup", () => setGate(false));
canvas.addEventListener("pointerleave", () => setGate(false));
canvas.addEventListener("pointercancel", () => setGate(false));
gateButton.addEventListener("contextmenu", (event) => event.preventDefault());
gateButton.addEventListener("pointerdown", (event) => {
event.preventDefault();
gateButton.setPointerCapture(event.pointerId);
setGate(true);
});
gateButton.addEventListener("pointerup", (event) => {
if (gateButton.hasPointerCapture(event.pointerId)) {
gateButton.releasePointerCapture(event.pointerId);
}
setGate(false);
});
gateButton.addEventListener("pointerleave", () => setGate(false));
gateButton.addEventListener("pointercancel", () => setGate(false));
gateButton.addEventListener("lostpointercapture", () => setGate(false));
onTap(startButton, () => startGame(false));
onTap(stopButton, stopGame);
fitCanvas();
showIntro();
requestAnimationFrame(loop);
</script>
</body>
</html>