Initial commit: TX Products puzzle games
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
356ed4ea72
9 files changed
+4754
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,606 @@
|
|||||||
|
<!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>Decode the Synchrotron Signal</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--panel: rgba(10, 16, 25, 0.92);
|
||||||
|
--line: rgba(255, 255, 255, 0.16);
|
||||||
|
--text: #f8f3ea;
|
||||||
|
--muted: #aeb8c8;
|
||||||
|
--cyan: #65d8e8;
|
||||||
|
--gold: #f2c96b;
|
||||||
|
--green: #88d76f;
|
||||||
|
--red: #ef735f;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 16% 12%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||||
|
radial-gradient(circle at 86% 78%, rgba(242, 201, 107, 0.13), transparent 32%),
|
||||||
|
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand img {
|
||||||
|
width: clamp(48px, 8vw, 72px);
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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.45rem, 3.5vw, 2.55rem);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat,
|
||||||
|
.panel,
|
||||||
|
button {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
min-width: 104px;
|
||||||
|
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: 1.12rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(320px, 1.15fr) minmax(280px, 0.85fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 10% 14%;
|
||||||
|
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||||
|
opacity: 0.08;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid rgba(101, 216, 232, 0.32);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--cyan);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: rgba(101, 216, 232, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
min-height: 42px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: var(--gold);
|
||||||
|
font-size: clamp(1rem, 2vw, 1.25rem);
|
||||||
|
font-weight: 850;
|
||||||
|
line-height: 1.32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phrase {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
gap: 12px 18px;
|
||||||
|
min-height: 330px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
width: clamp(32px, 4.7vw, 52px);
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cipher {
|
||||||
|
width: 100%;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 900;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plain {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-bottom: 3px solid rgba(101, 216, 232, 0.65);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
font-size: clamp(1.05rem, 2.8vw, 1.65rem);
|
||||||
|
font-weight: 950;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile.active .plain {
|
||||||
|
border-color: rgba(242, 201, 107, 0.95);
|
||||||
|
box-shadow: 0 0 0 4px rgba(242, 201, 107, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile.correct .plain {
|
||||||
|
color: #061106;
|
||||||
|
background: var(--green);
|
||||||
|
border-color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint h2 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: clamp(1.4rem, 3vw, 2.1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(1rem, 2vw, 1.15rem);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(72px, 100px));
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
|
gap: 7px;
|
||||||
|
align-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 900;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.primary {
|
||||||
|
color: #071014;
|
||||||
|
background: linear-gradient(135deg, var(--gold), #ffdc83);
|
||||||
|
border-color: rgba(255, 220, 131, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active-level {
|
||||||
|
color: #071014;
|
||||||
|
background: var(--green);
|
||||||
|
border-color: rgba(136, 215, 111, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.letter {
|
||||||
|
min-height: 48px;
|
||||||
|
color: #071014;
|
||||||
|
background: var(--cyan);
|
||||||
|
border-color: rgba(101, 216, 232, 0.8);
|
||||||
|
font-size: clamp(1rem, 1.8vw, 1.24rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
min-height: 38px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty button {
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.38;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
color: var(--muted);
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 840px) {
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phrase {
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard {
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header>
|
||||||
|
<div class="brand">
|
||||||
|
<img src="assets/txproducts-logo-watermark.png" alt="">
|
||||||
|
<div>
|
||||||
|
<small>txproducts.de signal puzzle</small>
|
||||||
|
<h1>Decode the Synchrotron Signal</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||||
|
<div class="stat"><span>Signal</span><strong id="signal">1 / 8</strong></div>
|
||||||
|
<div class="stat"><span>Errors</span><strong id="errors">0</strong></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="panel">
|
||||||
|
<div class="category" id="category">Product</div>
|
||||||
|
<p class="message" id="message">Tap an empty tile, then choose the decoded letter.</p>
|
||||||
|
<div class="phrase" id="phrase"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="side">
|
||||||
|
<section class="panel hint">
|
||||||
|
<h2>Are you ready?</h2>
|
||||||
|
<p>Decode the hidden phrase. The small top letters are the encrypted signal; your job is to recover the real synchrotron message below.</p>
|
||||||
|
</section>
|
||||||
|
<section class="difficulty" aria-label="Difficulty">
|
||||||
|
<button class="active-level" data-level="Easy" type="button">Easy</button>
|
||||||
|
<button data-level="Medium" type="button">Medium</button>
|
||||||
|
<button data-level="Hard" type="button">Hard</button>
|
||||||
|
</section>
|
||||||
|
<section class="controls">
|
||||||
|
<button class="primary" id="hintButton">Hint</button>
|
||||||
|
<button id="checkButton">Check</button>
|
||||||
|
<button id="nextButton">Next</button>
|
||||||
|
</section>
|
||||||
|
<section class="keyboard" id="keyboard"></section>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button class="ghost" id="resetButton">Reset</button>
|
||||||
|
<button class="ghost" onclick="window.location.href='index.html'">Back to menu</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
|
const puzzleSets = {
|
||||||
|
Easy: [
|
||||||
|
{ category: "Easy Product", phrase: "WAVEGATE", note: "WaveGate is the pulse timing product visitors should remember." },
|
||||||
|
{ category: "Easy Product", phrase: "PICOSWITCH", note: "PicoSwitch is the fast x-ray pulse shortening product." },
|
||||||
|
{ category: "Easy Data", phrase: "TXDATA", note: "TXData connects measurements with usable insight." },
|
||||||
|
{ category: "Easy Service", phrase: "TXSERVICES", note: "TXServices supports teams around experiments and beamtime." },
|
||||||
|
{ category: "Easy Physics", phrase: "XRAY PULSE", note: "X-ray pulses are central to these timing games." },
|
||||||
|
{ category: "Easy Business", phrase: "BETTER DATA", note: "Better data helps create better decisions." }
|
||||||
|
],
|
||||||
|
Medium: [
|
||||||
|
{ category: "Product", phrase: "WAVEGATE TAILORS PULSE STRUCTURE", note: "WaveGate shapes synchrotron pulse timing across nanoseconds to seconds." },
|
||||||
|
{ category: "Product", phrase: "PICOSWITCH CUTS X RAY PULSES", note: "PicoSwitch uses photo-acoustic Bragg switching for short x-ray pulses." },
|
||||||
|
{ category: "Physics", phrase: "BRAGG DIFFRACTION SELECTS THE BEAM", note: "Bragg diffraction is the physics clue behind precise switching." },
|
||||||
|
{ category: "Beamline", phrase: "SYNCHROTRON TIMING MATTERS", note: "Timing can decide whether a measurement is clean or noisy." },
|
||||||
|
{ category: "Data", phrase: "TXDATA TURNS MEASUREMENTS INTO INSIGHT", note: "TXData connects experimental output with decisions people can use." },
|
||||||
|
{ category: "Service", phrase: "TXSERVICES SUPPORTS BETTER BEAMTIME", note: "Good support helps teams get more value from scarce experiment time." },
|
||||||
|
{ category: "Business", phrase: "BETTER DATA BETTER DECISIONS", note: "The booth story links products, data, and service to business value." },
|
||||||
|
{ category: "Experiment", phrase: "COHERENT LATTICE DYNAMICS", note: "Short pulses can reveal ultrafast motion inside materials." }
|
||||||
|
],
|
||||||
|
Hard: [
|
||||||
|
{ category: "Hard Physics", phrase: "PHOTO ACOUSTIC BRAGG SWITCHING", note: "PicoSwitch uses photo-acoustic Bragg switching to shorten x-ray pulses." },
|
||||||
|
{ category: "Hard Timing", phrase: "TEMPORAL SYNCHROTRON PULSE STRUCTURE", note: "WaveGate tailors synchrotron pulse structure across broad timescales." },
|
||||||
|
{ category: "Hard Experiment", phrase: "COHERENT LATTICE DYNAMICS IN THIN FILMS", note: "Short x-ray pulses can reveal coherent lattice motion in materials." },
|
||||||
|
{ category: "Hard Business", phrase: "MEASUREMENTS BECOME ACTIONABLE INSIGHT", note: "TXData and TXServices help turn complex experimental output into decisions." },
|
||||||
|
{ category: "Hard Beamline", phrase: "DIFFRACTION EFFICIENCY LIMITS REPETITION RATE", note: "Technical constraints shape how a device performs in real experiments." },
|
||||||
|
{ category: "Hard Product", phrase: "NANOSECONDS TO SECONDS PULSE TAILORING", note: "That broad timing span is the memorable WaveGate story." }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let puzzles = puzzleSets.Easy;
|
||||||
|
const shifts = [7, 11, 5, 19, 9, 15, 21, 3];
|
||||||
|
const state = {
|
||||||
|
level: "Easy",
|
||||||
|
index: 0,
|
||||||
|
score: 0,
|
||||||
|
errors: 0,
|
||||||
|
selected: 0,
|
||||||
|
guesses: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const phraseEl = document.getElementById("phrase");
|
||||||
|
const keyboardEl = document.getElementById("keyboard");
|
||||||
|
const messageEl = document.getElementById("message");
|
||||||
|
const scoreEl = document.getElementById("score");
|
||||||
|
const signalEl = document.getElementById("signal");
|
||||||
|
const errorsEl = document.getElementById("errors");
|
||||||
|
const categoryEl = document.getElementById("category");
|
||||||
|
const levelButtons = document.querySelectorAll("[data-level]");
|
||||||
|
|
||||||
|
function encodeLetter(letter) {
|
||||||
|
if (!/[A-Z]/.test(letter)) return letter;
|
||||||
|
const shift = shifts[state.index % shifts.length];
|
||||||
|
const next = (alphabet.indexOf(letter) + shift) % 26;
|
||||||
|
return alphabet[next];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLetters() {
|
||||||
|
return puzzles[state.index].phrase.split("").map((char, index) => ({
|
||||||
|
char,
|
||||||
|
cipher: encodeLetter(char),
|
||||||
|
index,
|
||||||
|
isLetter: /[A-Z]/.test(char)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPuzzle() {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
state.guesses = Array(puzzle.phrase.length).fill("");
|
||||||
|
state.selected = getLetters().find((item) => item.isLetter)?.index || 0;
|
||||||
|
categoryEl.textContent = `${state.level} / ${puzzle.category}`;
|
||||||
|
messageEl.textContent = "Tap an empty tile, then choose the decoded letter.";
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
scoreEl.textContent = state.score;
|
||||||
|
signalEl.textContent = `${state.index + 1} / ${puzzles.length}`;
|
||||||
|
errorsEl.textContent = state.errors;
|
||||||
|
phraseEl.innerHTML = "";
|
||||||
|
const words = puzzle.phrase.split(" ");
|
||||||
|
let offset = 0;
|
||||||
|
words.forEach((word) => {
|
||||||
|
const wordEl = document.createElement("div");
|
||||||
|
wordEl.className = "word";
|
||||||
|
for (const char of word) {
|
||||||
|
const index = offset;
|
||||||
|
const tile = document.createElement("button");
|
||||||
|
tile.className = "tile";
|
||||||
|
tile.type = "button";
|
||||||
|
if (index === state.selected) tile.classList.add("active");
|
||||||
|
if (state.guesses[index] === puzzle.phrase[index]) tile.classList.add("correct");
|
||||||
|
tile.innerHTML = `<span class="cipher">${encodeLetter(char)}</span><span class="plain">${state.guesses[index] || ""}</span>`;
|
||||||
|
tile.addEventListener("click", () => {
|
||||||
|
state.selected = index;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
wordEl.appendChild(tile);
|
||||||
|
offset += 1;
|
||||||
|
}
|
||||||
|
phraseEl.appendChild(wordEl);
|
||||||
|
offset += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKeyboard() {
|
||||||
|
keyboardEl.innerHTML = "";
|
||||||
|
alphabet.forEach((letter) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.className = "letter";
|
||||||
|
button.textContent = letter;
|
||||||
|
button.addEventListener("click", () => guess(letter));
|
||||||
|
keyboardEl.appendChild(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextEmpty(fromIndex = 0) {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
for (let i = fromIndex; i < puzzle.phrase.length; i += 1) {
|
||||||
|
if (/[A-Z]/.test(puzzle.phrase[i]) && !state.guesses[i]) return i;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < puzzle.phrase.length; i += 1) {
|
||||||
|
if (/[A-Z]/.test(puzzle.phrase[i]) && !state.guesses[i]) return i;
|
||||||
|
}
|
||||||
|
return state.selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
function guess(letter) {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
if (!/[A-Z]/.test(puzzle.phrase[state.selected])) {
|
||||||
|
state.selected = nextEmpty();
|
||||||
|
}
|
||||||
|
state.guesses[state.selected] = letter;
|
||||||
|
if (letter === puzzle.phrase[state.selected]) {
|
||||||
|
state.score += 25;
|
||||||
|
messageEl.textContent = "Good decode. Keep going.";
|
||||||
|
} else {
|
||||||
|
state.errors += 1;
|
||||||
|
messageEl.textContent = "That signal is off. Try another tile.";
|
||||||
|
}
|
||||||
|
state.selected = nextEmpty(state.selected + 1);
|
||||||
|
render();
|
||||||
|
if (isSolved()) completePuzzle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSolved() {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
return puzzle.phrase.split("").every((char, index) => !/[A-Z]/.test(char) || state.guesses[index] === char);
|
||||||
|
}
|
||||||
|
|
||||||
|
function completePuzzle() {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
state.score += 150;
|
||||||
|
messageEl.textContent = `Signal decoded: ${puzzle.note}`;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function revealHint() {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
const missing = puzzle.phrase.split("").findIndex((char, index) => /[A-Z]/.test(char) && state.guesses[index] !== char);
|
||||||
|
if (missing >= 0) {
|
||||||
|
state.guesses[missing] = puzzle.phrase[missing];
|
||||||
|
state.selected = nextEmpty(missing + 1);
|
||||||
|
messageEl.textContent = "Hint revealed one correct signal letter.";
|
||||||
|
render();
|
||||||
|
if (isSolved()) completePuzzle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPuzzle() {
|
||||||
|
if (isSolved()) {
|
||||||
|
completePuzzle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
const wrong = puzzle.phrase.split("").filter((char, index) => /[A-Z]/.test(char) && state.guesses[index] && state.guesses[index] !== char).length;
|
||||||
|
messageEl.textContent = wrong ? `${wrong} decoded letters need another look.` : "No wrong letters yet. Keep decoding.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPuzzle() {
|
||||||
|
state.index = (state.index + 1) % puzzles.length;
|
||||||
|
startPuzzle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLevel(level) {
|
||||||
|
state.level = level;
|
||||||
|
puzzles = puzzleSets[level];
|
||||||
|
state.index = 0;
|
||||||
|
state.score = 0;
|
||||||
|
state.errors = 0;
|
||||||
|
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
|
||||||
|
startPuzzle();
|
||||||
|
}
|
||||||
|
|
||||||
|
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
|
||||||
|
document.getElementById("hintButton").addEventListener("click", revealHint);
|
||||||
|
document.getElementById("checkButton").addEventListener("click", checkPuzzle);
|
||||||
|
document.getElementById("nextButton").addEventListener("click", nextPuzzle);
|
||||||
|
document.getElementById("resetButton").addEventListener("click", () => {
|
||||||
|
state.index = 0;
|
||||||
|
state.score = 0;
|
||||||
|
state.errors = 0;
|
||||||
|
startPuzzle();
|
||||||
|
});
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
const letter = event.key.toUpperCase();
|
||||||
|
if (alphabet.includes(letter)) guess(letter);
|
||||||
|
if (event.key === "Enter") checkPuzzle();
|
||||||
|
if (event.key.toLowerCase() === "r") {
|
||||||
|
state.score = 0;
|
||||||
|
state.errors = 0;
|
||||||
|
startPuzzle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderKeyboard();
|
||||||
|
startPuzzle();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>TX Products Puzzle Games</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;
|
||||||
|
--green: #88d76f;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 16%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||||
|
radial-gradient(circle at 82% 78%, rgba(242, 201, 107, 0.12), 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
top: -28px;
|
||||||
|
left: -18px;
|
||||||
|
width: min(50vw, 500px);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||||
|
opacity: 0.16;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(1080px, 100%);
|
||||||
|
padding: 28px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: var(--cyan);
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 4px 0 10px;
|
||||||
|
font-size: clamp(2.1rem, 5vw, 4.2rem);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 1040px;
|
||||||
|
margin: 0 0 24px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(1.18rem, 2.2vw, 1.42rem);
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
min-height: 178px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:nth-child(3n + 1) { border-color: rgba(242, 201, 107, 0.45); }
|
||||||
|
a:nth-child(3n + 2) { border-color: rgba(101, 216, 232, 0.45); }
|
||||||
|
a:nth-child(3n) { border-color: rgba(136, 215, 111, 0.45); }
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
font-size: clamp(1.35rem, 2.6vw, 2.15rem);
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
p {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<small>TX Products Puzzle Games</small>
|
||||||
|
<h1>Choose your Challenge</h1>
|
||||||
|
<p>Fast hands, sharp eyes, clear signals. Which challenge will you take on first?</p>
|
||||||
|
<div class="grid">
|
||||||
|
<a href="wavegate.html">
|
||||||
|
<strong>WaveGate Pulse Tailor</strong>
|
||||||
|
<span>Shape synchrotron pulse timing by opening the gate only during the target window.</span>
|
||||||
|
</a>
|
||||||
|
<a href="picoswitch.html">
|
||||||
|
<strong>PicoSwitch Pulse Cut</strong>
|
||||||
|
<span>Trigger a photo-acoustic Bragg switch to cut x-ray pulses into the picosecond range.</span>
|
||||||
|
</a>
|
||||||
|
<a href="synchrotron-jeopardy.html">
|
||||||
|
<strong>Synchrotron Challenge</strong>
|
||||||
|
<span>A Jeopardy-style quiz covering synchrotrons, TXProducts, TXData, TXServices, and business value.</span>
|
||||||
|
</a>
|
||||||
|
<a href="wheel-of-photons.html">
|
||||||
|
<strong>Wheel of Photons</strong>
|
||||||
|
<span>A phrase puzzle with Easy, Medium, and Hard synchrotron challenges.</span>
|
||||||
|
</a>
|
||||||
|
<a href="cryptogram.html">
|
||||||
|
<strong>Decode the Signal</strong>
|
||||||
|
<span>A cryptogram where visitors recover hidden product, physics, data, and service messages.</span>
|
||||||
|
</a>
|
||||||
|
<a href="synchrotron-crossword.html">
|
||||||
|
<strong>Synchrotron Crossword</strong>
|
||||||
|
<span>Connected crossword challenges with Easy, Medium, and Hard boards.</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,923 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,746 @@
|
|||||||
|
<!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>Synchrotron Mini Crossword</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--panel: rgba(10, 16, 25, 0.92);
|
||||||
|
--line: rgba(255, 255, 255, 0.16);
|
||||||
|
--text: #f8f3ea;
|
||||||
|
--muted: #aeb8c8;
|
||||||
|
--cyan: #65d8e8;
|
||||||
|
--gold: #f2c96b;
|
||||||
|
--green: #88d76f;
|
||||||
|
--red: #ef735f;
|
||||||
|
--cell: #f8f3ea;
|
||||||
|
--ink: #071014;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 14% 14%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||||
|
radial-gradient(circle at 86% 78%, rgba(242, 201, 107, 0.13), transparent 32%),
|
||||||
|
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand img {
|
||||||
|
width: clamp(42px, 6vw, 60px);
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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.45rem, 3.5vw, 2.55rem);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat,
|
||||||
|
.panel,
|
||||||
|
button {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
min-width: 94px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(500px, 1.08fr) minmax(300px, 0.92fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 10% 16%;
|
||||||
|
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||||
|
opacity: 0.07;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-wrap {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
width: min(100%, 540px);
|
||||||
|
max-width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(17, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--cell);
|
||||||
|
font: inherit;
|
||||||
|
font-size: clamp(0.76rem, 1.45vw, 1.08rem);
|
||||||
|
font-weight: 950;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
box-shadow: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.block {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.active {
|
||||||
|
background: var(--gold);
|
||||||
|
box-shadow: 0 0 0 3px rgba(242, 201, 107, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.related {
|
||||||
|
background: #d8f6fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.correct {
|
||||||
|
background: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.num {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 3px;
|
||||||
|
color: rgba(7, 16, 20, 0.68);
|
||||||
|
font-size: 0.42rem;
|
||||||
|
font-weight: 950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: minmax(190px, 1fr) auto auto auto;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: var(--gold);
|
||||||
|
font-size: clamp(1rem, 2vw, 1.25rem);
|
||||||
|
font-weight: 850;
|
||||||
|
line-height: 1.32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clues {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clues h2 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: var(--cyan);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clues ol {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(0.76rem, 1.15vw, 0.9rem);
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clues li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clues li.active {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(72px, 100px));
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 900;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.primary {
|
||||||
|
color: #071014;
|
||||||
|
background: linear-gradient(135deg, var(--gold), #ffdc83);
|
||||||
|
border-color: rgba(255, 220, 131, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active-level {
|
||||||
|
color: #071014;
|
||||||
|
background: var(--green);
|
||||||
|
border-color: rgba(136, 215, 111, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.letter {
|
||||||
|
min-height: 48px;
|
||||||
|
color: #071014;
|
||||||
|
background: var(--cyan);
|
||||||
|
border-color: rgba(101, 216, 232, 0.8);
|
||||||
|
font-size: clamp(1rem, 1.8vw, 1.24rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
min-height: 38px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty button {
|
||||||
|
min-height: 28px;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
color: var(--muted);
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
width: min(100%, 540px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clues {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.board {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clues {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard {
|
||||||
|
grid-template-columns: repeat(9, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header>
|
||||||
|
<div class="brand">
|
||||||
|
<img src="assets/txproducts-logo-watermark.png" alt="">
|
||||||
|
<div>
|
||||||
|
<small>txproducts.de technical crossword</small>
|
||||||
|
<h1>Synchrotron Mini Crossword</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||||
|
<div class="stat"><span>Filled</span><strong id="filled">0%</strong></div>
|
||||||
|
<div class="stat"><span>Mode</span><strong id="mode">Across</strong></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="panel board-wrap">
|
||||||
|
<p class="message" id="message">Tap a square or clue, then fill the answer. Short, technical, and booth-friendly.</p>
|
||||||
|
<div class="board" id="board"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="side">
|
||||||
|
<section class="panel clues">
|
||||||
|
<div>
|
||||||
|
<h2>Across</h2>
|
||||||
|
<ol id="acrossClues"></ol>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Down</h2>
|
||||||
|
<ol id="downClues"></ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="difficulty" aria-label="Difficulty">
|
||||||
|
<button class="active-level" data-level="Easy" type="button">Easy</button>
|
||||||
|
<button data-level="Medium" type="button">Medium</button>
|
||||||
|
<button data-level="Hard" type="button">Hard</button>
|
||||||
|
</section>
|
||||||
|
<section class="keyboard" id="keyboard"></section>
|
||||||
|
<section class="controls">
|
||||||
|
<button class="primary" id="checkButton">Check</button>
|
||||||
|
<button id="revealButton">Reveal</button>
|
||||||
|
<button id="clearButton">Clear</button>
|
||||||
|
<button class="ghost" id="resetButton">Reset</button>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button class="ghost" onclick="window.location.href='index.html'">Back to menu</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
|
const rows = 17;
|
||||||
|
const cols = 17;
|
||||||
|
const puzzleSets = {
|
||||||
|
Easy: [
|
||||||
|
{ num: 1, dir: "across", row: 2, col: 7, answer: "PICOSWITCH", clue: "Product that shortens x-ray pulses." },
|
||||||
|
{ num: 2, dir: "down", row: 2, col: 14, answer: "TXSERVICES", clue: "Support area for customers and experiments." },
|
||||||
|
{ num: 3, dir: "down", row: 4, col: 7, answer: "VALUE", clue: "What the right solution should create for a customer." },
|
||||||
|
{ num: 4, dir: "across", row: 5, col: 0, answer: "DATA", clue: "Measurement output before analysis." },
|
||||||
|
{ num: 5, dir: "across", row: 5, col: 10, answer: "PULSE", clue: "Short burst of x-rays or signal." },
|
||||||
|
{ num: 6, dir: "down", row: 5, col: 2, answer: "TXDATA", clue: "Turns measurements into insight." },
|
||||||
|
{ num: 7, dir: "down", row: 5, col: 10, answer: "PHOTON", clue: "Particle of light." },
|
||||||
|
{ num: 8, dir: "down", row: 6, col: 5, answer: "XRAY", clue: "Radiation used to probe structures." },
|
||||||
|
{ num: 9, dir: "across", row: 8, col: 4, answer: "WAVEGATE", clue: "Product for tailoring pulse timing." },
|
||||||
|
{ num: 10, dir: "across", row: 10, col: 0, answer: "BRAGG", clue: "Diffraction name linked to crystal planes." },
|
||||||
|
{ num: 11, dir: "across", row: 10, col: 7, answer: "SIGNAL", clue: "Useful information separated from noise." },
|
||||||
|
{ num: 12, dir: "down", row: 10, col: 0, answer: "BEAM", clue: "Focused stream used at a beamline." },
|
||||||
|
{ num: 13, dir: "down", row: 10, col: 4, answer: "GATE", clue: "Timing window that lets a pulse through." },
|
||||||
|
{ num: 14, dir: "down", row: 10, col: 8, answer: "ION", clue: "Charged particle." },
|
||||||
|
{ num: 15, dir: "across", row: 12, col: 4, answer: "TIMING", clue: "When pulses arrive." }
|
||||||
|
],
|
||||||
|
Medium: [
|
||||||
|
{ num: 1, dir: "across", row: 1, col: 1, answer: "WAVEGATE", clue: "TXProducts device for tailoring synchrotron pulse timing." },
|
||||||
|
{ num: 2, dir: "down", row: 3, col: 4, answer: "BEAMTIME", clue: "Scarce experiment time that teams want to use well." },
|
||||||
|
{ num: 3, dir: "across", row: 3, col: 6, answer: "TXSERVICES", clue: "Support area that helps teams get more from beamtime and experiments." },
|
||||||
|
{ num: 4, dir: "down", row: 3, col: 12, answer: "INSIGHT", clue: "What useful analysis gives a business after the measurement." },
|
||||||
|
{ num: 5, dir: "across", row: 5, col: 3, answer: "LATTICE", clue: "Ordered atomic structure studied in many materials experiments." },
|
||||||
|
{ num: 6, dir: "down", row: 6, col: 10, answer: "GATE", clue: "A timing window that lets the wanted pulse through." },
|
||||||
|
{ num: 7, dir: "across", row: 6, col: 12, answer: "ION", clue: "Charged particle found throughout accelerator physics vocabulary." },
|
||||||
|
{ num: 8, dir: "down", row: 8, col: 1, answer: "BRAGG", clue: "Diffraction condition central to selecting and switching x-rays." },
|
||||||
|
{ num: 9, dir: "across", row: 8, col: 3, answer: "PICOSWITCH", clue: "Photo-acoustic Bragg switch for shortening x-ray pulses." },
|
||||||
|
{ num: 10, dir: "down", row: 8, col: 7, answer: "SIGNAL", clue: "Useful measurement information that must be separated from noise." },
|
||||||
|
{ num: 11, dir: "across", row: 10, col: 0, answer: "VALUE", clue: "What the right product, data, and service should create for a customer." },
|
||||||
|
{ num: 12, dir: "down", row: 11, col: 11, answer: "DATA", clue: "Raw experiment output before analysis turns it into decisions." },
|
||||||
|
{ num: 13, dir: "across", row: 12, col: 6, answer: "WAVEGATE", clue: "Product that tailors synchrotron pulse structure from nanoseconds to seconds." },
|
||||||
|
{ num: 14, dir: "across", row: 14, col: 9, answer: "XRAY", clue: "Radiation used to probe structure and dynamics at beamlines." },
|
||||||
|
{ num: 15, dir: "down", row: 14, col: 10, answer: "RAY", clue: "A beam, line, or part of the x-ray term." }
|
||||||
|
],
|
||||||
|
Hard: [
|
||||||
|
{ num: 1, dir: "down", row: 0, col: 0, answer: "DETECTOR", clue: "Instrument that records the experimental signal." },
|
||||||
|
{ num: 2, dir: "down", row: 1, col: 6, answer: "SIGNAL", clue: "Measurement information separated from noise." },
|
||||||
|
{ num: 3, dir: "across", row: 2, col: 5, answer: "PICOSWITCH", clue: "Photo-acoustic Bragg switch for shortening x-ray pulses." },
|
||||||
|
{ num: 4, dir: "down", row: 2, col: 10, answer: "WAVEGATE", clue: "Product that tailors temporal pulse structure." },
|
||||||
|
{ num: 5, dir: "down", row: 3, col: 15, answer: "DYNAMICS", clue: "Time-dependent motion studied in materials." },
|
||||||
|
{ num: 6, dir: "across", row: 4, col: 0, answer: "COHERENCE", clue: "Wave property important in many advanced experiments." },
|
||||||
|
{ num: 7, dir: "across", row: 5, col: 9, answer: "BEAMLINE", clue: "Experimental path that delivers synchrotron radiation." },
|
||||||
|
{ num: 8, dir: "down", row: 7, col: 8, answer: "SAMPLE", clue: "Material placed in the beam for measurement." },
|
||||||
|
{ num: 9, dir: "down", row: 7, col: 13, answer: "ANALYSIS", clue: "Process that turns data into conclusions." },
|
||||||
|
{ num: 10, dir: "across", row: 8, col: 3, answer: "DIFFRACTION", clue: "Scattering pattern used to infer structure." },
|
||||||
|
{ num: 11, dir: "down", row: 10, col: 5, answer: "BRAGG", clue: "Diffraction condition named for a father-son team." },
|
||||||
|
{ num: 12, dir: "down", row: 10, col: 11, answer: "PHOTON", clue: "Quantum of light, including x-rays." },
|
||||||
|
{ num: 13, dir: "across", row: 12, col: 4, answer: "VALUE", clue: "Business result created by better data and support." },
|
||||||
|
{ num: 14, dir: "across", row: 13, col: 9, answer: "LATTICE", clue: "Ordered atomic arrangement in a crystal." },
|
||||||
|
{ num: 15, dir: "across", row: 15, col: 7, answer: "TIMING", clue: "Critical control variable for pulses." }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let entries = puzzleSets.Easy;
|
||||||
|
let grid = [];
|
||||||
|
let guesses = [];
|
||||||
|
let cellEntries = {};
|
||||||
|
let numbers = {};
|
||||||
|
const state = {
|
||||||
|
level: "Easy",
|
||||||
|
active: null,
|
||||||
|
row: 0,
|
||||||
|
col: 0,
|
||||||
|
score: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildGrid() {
|
||||||
|
entries = puzzleSets[state.level];
|
||||||
|
grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => null));
|
||||||
|
guesses = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ""));
|
||||||
|
cellEntries = {};
|
||||||
|
numbers = {};
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
numbers[`${entry.row},${entry.col}`] = entry.num;
|
||||||
|
entry.answer.split("").forEach((letter, index) => {
|
||||||
|
const row = entry.row + (entry.dir === "down" ? index : 0);
|
||||||
|
const col = entry.col + (entry.dir === "across" ? index : 0);
|
||||||
|
grid[row][col] = letter;
|
||||||
|
const entryKey = `${row},${col}`;
|
||||||
|
if (!cellEntries[entryKey]) cellEntries[entryKey] = [];
|
||||||
|
cellEntries[entryKey].push(entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
state.active = entries[0];
|
||||||
|
state.row = entries[0].row;
|
||||||
|
state.col = entries[0].col;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boardEl = document.getElementById("board");
|
||||||
|
const keyboardEl = document.getElementById("keyboard");
|
||||||
|
const acrossEl = document.getElementById("acrossClues");
|
||||||
|
const downEl = document.getElementById("downClues");
|
||||||
|
const messageEl = document.getElementById("message");
|
||||||
|
const scoreEl = document.getElementById("score");
|
||||||
|
const filledEl = document.getElementById("filled");
|
||||||
|
const modeEl = document.getElementById("mode");
|
||||||
|
const levelButtons = document.querySelectorAll("[data-level]");
|
||||||
|
|
||||||
|
function key(row, col) {
|
||||||
|
return `${row},${col}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeCells() {
|
||||||
|
const cells = [];
|
||||||
|
state.active.answer.split("").forEach((_, index) => {
|
||||||
|
cells.push({
|
||||||
|
row: state.active.row + (state.active.dir === "down" ? index : 0),
|
||||||
|
col: state.active.col + (state.active.dir === "across" ? index : 0)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseCell(row, col) {
|
||||||
|
if (!grid[row][col]) return;
|
||||||
|
const options = cellEntries[key(row, col)];
|
||||||
|
if (options.includes(state.active) && options.length > 1) {
|
||||||
|
state.active = options.find((entry) => entry !== state.active);
|
||||||
|
} else {
|
||||||
|
state.active = options[0];
|
||||||
|
}
|
||||||
|
state.row = row;
|
||||||
|
state.col = col;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseEntry(entry) {
|
||||||
|
state.active = entry;
|
||||||
|
state.row = entry.row;
|
||||||
|
state.col = entry.col;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBoard() {
|
||||||
|
const related = new Set(activeCells().map((cell) => key(cell.row, cell.col)));
|
||||||
|
boardEl.innerHTML = "";
|
||||||
|
for (let row = 0; row < rows; row += 1) {
|
||||||
|
for (let col = 0; col < cols; col += 1) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "cell";
|
||||||
|
if (!grid[row][col]) {
|
||||||
|
button.classList.add("block");
|
||||||
|
button.disabled = true;
|
||||||
|
} else {
|
||||||
|
if (related.has(key(row, col))) button.classList.add("related");
|
||||||
|
if (row === state.row && col === state.col) button.classList.add("active");
|
||||||
|
if (guesses[row][col] === grid[row][col]) button.classList.add("correct");
|
||||||
|
button.textContent = guesses[row][col];
|
||||||
|
if (numbers[key(row, col)]) {
|
||||||
|
const num = document.createElement("span");
|
||||||
|
num.className = "num";
|
||||||
|
num.textContent = numbers[key(row, col)];
|
||||||
|
button.appendChild(num);
|
||||||
|
}
|
||||||
|
button.addEventListener("click", () => chooseCell(row, col));
|
||||||
|
}
|
||||||
|
boardEl.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderClues() {
|
||||||
|
acrossEl.innerHTML = "";
|
||||||
|
downEl.innerHTML = "";
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.value = entry.num;
|
||||||
|
li.textContent = entry.clue;
|
||||||
|
if (entry === state.active) li.className = "active";
|
||||||
|
li.addEventListener("click", () => chooseEntry(entry));
|
||||||
|
(entry.dir === "across" ? acrossEl : downEl).appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKeyboard() {
|
||||||
|
keyboardEl.innerHTML = "";
|
||||||
|
alphabet.forEach((letter) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.className = "letter";
|
||||||
|
button.textContent = letter;
|
||||||
|
button.addEventListener("click", () => typeLetter(letter));
|
||||||
|
keyboardEl.appendChild(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStats() {
|
||||||
|
let total = 0;
|
||||||
|
let filled = 0;
|
||||||
|
for (let row = 0; row < rows; row += 1) {
|
||||||
|
for (let col = 0; col < cols; col += 1) {
|
||||||
|
if (grid[row][col]) {
|
||||||
|
total += 1;
|
||||||
|
if (guesses[row][col]) filled += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scoreEl.textContent = state.score;
|
||||||
|
filledEl.textContent = `${Math.round((filled / total) * 100)}%`;
|
||||||
|
modeEl.textContent = `${state.level} ${state.active.dir === "across" ? "Across" : "Down"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
renderBoard();
|
||||||
|
renderClues();
|
||||||
|
renderStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextCell() {
|
||||||
|
const cells = activeCells();
|
||||||
|
const current = cells.findIndex((cell) => cell.row === state.row && cell.col === state.col);
|
||||||
|
const next = cells[Math.min(current + 1, cells.length - 1)] || cells[0];
|
||||||
|
state.row = next.row;
|
||||||
|
state.col = next.col;
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousCell() {
|
||||||
|
const cells = activeCells();
|
||||||
|
const current = cells.findIndex((cell) => cell.row === state.row && cell.col === state.col);
|
||||||
|
const previous = cells[Math.max(current - 1, 0)] || cells[0];
|
||||||
|
state.row = previous.row;
|
||||||
|
state.col = previous.col;
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeLetter(letter) {
|
||||||
|
guesses[state.row][state.col] = letter;
|
||||||
|
nextCell();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function backspace() {
|
||||||
|
if (guesses[state.row][state.col]) {
|
||||||
|
guesses[state.row][state.col] = "";
|
||||||
|
} else {
|
||||||
|
previousCell();
|
||||||
|
guesses[state.row][state.col] = "";
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPuzzle() {
|
||||||
|
let wrong = 0;
|
||||||
|
let correct = 0;
|
||||||
|
for (let row = 0; row < rows; row += 1) {
|
||||||
|
for (let col = 0; col < cols; col += 1) {
|
||||||
|
if (grid[row][col] && guesses[row][col]) {
|
||||||
|
if (guesses[row][col] === grid[row][col]) correct += 1;
|
||||||
|
else wrong += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.score = correct * 20;
|
||||||
|
messageEl.textContent = wrong ? `${wrong} squares need another look.` : "Everything filled so far is correct.";
|
||||||
|
if (isComplete() && !wrong) {
|
||||||
|
state.score += 400;
|
||||||
|
messageEl.textContent = "Crossword complete: product, physics, data, and service all connected.";
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isComplete() {
|
||||||
|
for (let row = 0; row < rows; row += 1) {
|
||||||
|
for (let col = 0; col < cols; col += 1) {
|
||||||
|
if (grid[row][col] && guesses[row][col] !== grid[row][col]) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function revealActive() {
|
||||||
|
activeCells().forEach((cell, index) => {
|
||||||
|
guesses[cell.row][cell.col] = state.active.answer[index];
|
||||||
|
});
|
||||||
|
messageEl.textContent = `${state.active.num} ${state.active.dir} revealed.`;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearActive() {
|
||||||
|
activeCells().forEach((cell) => {
|
||||||
|
guesses[cell.row][cell.col] = "";
|
||||||
|
});
|
||||||
|
messageEl.textContent = "Current answer cleared.";
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPuzzle() {
|
||||||
|
buildGrid();
|
||||||
|
state.score = 0;
|
||||||
|
messageEl.textContent = `${state.level} crossword ready. Tap a square or clue, then fill the answer.`;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLevel(level) {
|
||||||
|
state.level = level;
|
||||||
|
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
|
||||||
|
resetPuzzle();
|
||||||
|
}
|
||||||
|
|
||||||
|
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
|
||||||
|
document.getElementById("checkButton").addEventListener("click", checkPuzzle);
|
||||||
|
document.getElementById("revealButton").addEventListener("click", revealActive);
|
||||||
|
document.getElementById("clearButton").addEventListener("click", clearActive);
|
||||||
|
document.getElementById("resetButton").addEventListener("click", resetPuzzle);
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
const letter = event.key.toUpperCase();
|
||||||
|
if (alphabet.includes(letter)) typeLetter(letter);
|
||||||
|
if (event.key === "Backspace") backspace();
|
||||||
|
if (event.key === "Enter") checkPuzzle();
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
event.preventDefault();
|
||||||
|
const index = entries.indexOf(state.active);
|
||||||
|
chooseEntry(entries[(index + 1) % entries.length]);
|
||||||
|
}
|
||||||
|
if (event.key.toLowerCase() === "r") resetPuzzle();
|
||||||
|
});
|
||||||
|
|
||||||
|
buildGrid();
|
||||||
|
renderKeyboard();
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
<!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>Synchrotron Challenge</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--panel: rgba(10, 16, 25, 0.92);
|
||||||
|
--line: rgba(255, 255, 255, 0.16);
|
||||||
|
--text: #f8f3ea;
|
||||||
|
--muted: #aeb8c8;
|
||||||
|
--cyan: #65d8e8;
|
||||||
|
--gold: #f2c96b;
|
||||||
|
--green: #88d76f;
|
||||||
|
--red: #ef735f;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { 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.22), transparent 28%),
|
||||||
|
radial-gradient(circle at 86% 80%, rgba(242, 201, 107, 0.12), transparent 32%),
|
||||||
|
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 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: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: clamp(48px, 8vw, 76px);
|
||||||
|
height: auto;
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
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.45rem, 3.5vw, 2.5rem);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scorebar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score,
|
||||||
|
.tip,
|
||||||
|
button {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score {
|
||||||
|
min-width: 112px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score span {
|
||||||
|
display: block;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watermark {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.05;
|
||||||
|
transform: rotate(-18deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watermark img {
|
||||||
|
width: min(62vmin, 520px);
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
grid-template-rows: auto repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category,
|
||||||
|
.tile {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(9, 14, 22, 0.88);
|
||||||
|
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
min-height: 74px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--cyan);
|
||||||
|
font-size: clamp(0.72rem, 1.45vw, 1rem);
|
||||||
|
font-weight: 900;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
min-height: 72px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--gold);
|
||||||
|
font-size: clamp(1.3rem, 3.4vw, 2.6rem);
|
||||||
|
font-weight: 1000;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile.used {
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(2, 5, 10, 0.62);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
width: min(900px, 100%);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(7, 11, 18, 0.95);
|
||||||
|
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--cyan);
|
||||||
|
font-size: clamp(1rem, 2.2vw, 1.25rem);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--gold);
|
||||||
|
font-size: clamp(1.4rem, 4vw, 2.4rem);
|
||||||
|
font-weight: 1000;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clue {
|
||||||
|
min-height: 145px;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.55rem, 4vw, 3.2rem);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
border-left: 4px solid var(--gold);
|
||||||
|
padding: 12px 0 12px 14px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: clamp(1.2rem, 2.6vw, 1.7rem);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 900;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button,
|
||||||
|
footer button {
|
||||||
|
min-height: 38px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.primary {
|
||||||
|
border-color: rgba(242, 201, 107, 0.62);
|
||||||
|
background: linear-gradient(180deg, #f3d16e, #d89a37);
|
||||||
|
color: #211609;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.good {
|
||||||
|
border-color: rgba(136, 215, 111, 0.58);
|
||||||
|
background: rgba(136, 215, 111, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.bad {
|
||||||
|
border-color: rgba(239, 115, 95, 0.58);
|
||||||
|
background: rgba(239, 115, 95, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
body {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scorebar,
|
||||||
|
.footer-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
grid-template-columns: repeat(5, minmax(92px, 1fr));
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header>
|
||||||
|
<div class="brand">
|
||||||
|
<img class="brand-logo" src="assets/txproducts-logo-watermark.png" alt="">
|
||||||
|
<div class="brand-text">
|
||||||
|
<small>txproducts.de exhibition quiz</small>
|
||||||
|
<h1>Synchrotron Challenge</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="scorebar">
|
||||||
|
<div class="score"><span>Score</span><strong id="score">0</strong></div>
|
||||||
|
<div class="score"><span>Answered</span><strong id="answered">0 / 25</strong></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="watermark" aria-hidden="true"><img src="assets/txproducts-logo-watermark.png" alt=""></div>
|
||||||
|
<section class="board" id="board" aria-label="Synchrotron quiz board"></section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="tip">Tap a value, discuss the clue, reveal the answer, then mark correct or missed.</div>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button id="resetButton" type="button">Reset board</button>
|
||||||
|
<button type="button" onclick="window.location.href='index.html'">Back to menu</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overlay" id="overlay" hidden>
|
||||||
|
<article class="modal">
|
||||||
|
<div class="modal-top">
|
||||||
|
<div>
|
||||||
|
<h2 id="modalCategory">Category</h2>
|
||||||
|
<div id="modalDifficulty">Difficulty</div>
|
||||||
|
</div>
|
||||||
|
<div class="value" id="modalValue">100</div>
|
||||||
|
</div>
|
||||||
|
<p class="clue" id="modalClue"></p>
|
||||||
|
<div class="answer" id="modalAnswer" hidden></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="primary" id="showAnswerButton" type="button">Show answer</button>
|
||||||
|
<button class="good" id="correctButton" type="button" hidden>Correct</button>
|
||||||
|
<button class="bad" id="missedButton" type="button" hidden>Missed</button>
|
||||||
|
<button id="closeButton" type="button">Close</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
title: "Synchrotron Basics",
|
||||||
|
clues: [
|
||||||
|
["A large accelerator facility that produces extremely bright x-rays for experiments.", "What is a synchrotron?"],
|
||||||
|
["These charged particles are accelerated around the storage ring to generate synchrotron radiation.", "What are electrons?"],
|
||||||
|
["Scientists use synchrotron x-rays because they are bright, tunable, and can probe this tiny scale.", "What is the atomic or nanoscale?"],
|
||||||
|
["This property of synchrotron light lets researchers choose x-ray energies for specific materials or edges.", "What is tunability?"],
|
||||||
|
["This repeating time pattern of x-ray bunches is what timing devices can reshape for experiments.", "What is pulse structure?"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "TXProducts",
|
||||||
|
clues: [
|
||||||
|
["This product tailors synchrotron pulse structure from nanoseconds to seconds.", "What is WaveGate?"],
|
||||||
|
["This product cuts x-ray pulses to picosecond duration using photo-acoustic Bragg switching.", "What is PicoSwitch?"],
|
||||||
|
["WaveGate controls when pulses are allowed through, shaping this aspect of the beam.", "What is temporal structure?"],
|
||||||
|
["PicoSwitch uses a photo-acoustic effect together with this diffraction condition.", "What is Bragg diffraction?"],
|
||||||
|
["Together, WaveGate and PicoSwitch help researchers control this dimension of x-ray experiments.", "What is time?"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "TXData",
|
||||||
|
clues: [
|
||||||
|
["A data service helps turn raw detector output into this useful form.", "What is actionable insight?"],
|
||||||
|
["Good experimental data needs metadata, calibration, and this quality check.", "What is validation?"],
|
||||||
|
["TXData can help researchers compare runs, spot trends, and reduce this costly problem.", "What is wasted beamtime or failed experiments?"],
|
||||||
|
["When pulse timing, detector output, and sample conditions are linked, the result is better this.", "What is data context or traceability?"],
|
||||||
|
["A business benefits when scientific data becomes searchable, reusable, and easier to share across this group.", "What is a team or organization?"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "TXServices",
|
||||||
|
clues: [
|
||||||
|
["TXServices can support planning, setup, and optimization so teams make better use of this scarce resource.", "What is beamtime?"],
|
||||||
|
["A service partner can help translate an experimental question into this practical beamline plan.", "What is a measurement strategy?"],
|
||||||
|
["Training and support reduce the gap between advanced instruments and this day-to-day user need.", "What is usability or confidence?"],
|
||||||
|
["When equipment, data, and workflow are aligned, customers can move faster from experiment to this.", "What is decision or result?"],
|
||||||
|
["For a customer, expert service lowers technical risk and protects this project investment.", "What is time, budget, or ROI?"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Business Value",
|
||||||
|
clues: [
|
||||||
|
["Better pulse control can improve experiment quality by reducing unwanted signal known as this.", "What is background or leakage?"],
|
||||||
|
["Reliable timing tools help customers spend less time troubleshooting and more time getting these.", "What are results?"],
|
||||||
|
["TXProducts, TXData, and TXServices together can connect hardware, analysis, and this human support layer.", "What is expertise or service?"],
|
||||||
|
["A business improves when experiments become more repeatable, data becomes clearer, and decisions become this.", "What is faster or more confident?"],
|
||||||
|
["For industrial or research customers, the combined offering can turn advanced synchrotron access into this advantage.", "What is competitive advantage?"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const values = [100, 200, 300, 400, 500];
|
||||||
|
const difficulties = ["Warm-up", "Basic", "Applied", "Technical", "Expert"];
|
||||||
|
const board = document.querySelector("#board");
|
||||||
|
const scoreEl = document.querySelector("#score");
|
||||||
|
const answeredEl = document.querySelector("#answered");
|
||||||
|
const overlay = document.querySelector("#overlay");
|
||||||
|
const modalCategory = document.querySelector("#modalCategory");
|
||||||
|
const modalDifficulty = document.querySelector("#modalDifficulty");
|
||||||
|
const modalValue = document.querySelector("#modalValue");
|
||||||
|
const modalClue = document.querySelector("#modalClue");
|
||||||
|
const modalAnswer = document.querySelector("#modalAnswer");
|
||||||
|
const showAnswerButton = document.querySelector("#showAnswerButton");
|
||||||
|
const correctButton = document.querySelector("#correctButton");
|
||||||
|
const missedButton = document.querySelector("#missedButton");
|
||||||
|
const closeButton = document.querySelector("#closeButton");
|
||||||
|
const resetButton = document.querySelector("#resetButton");
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
score: 0,
|
||||||
|
answered: 0,
|
||||||
|
current: null,
|
||||||
|
used: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderBoard() {
|
||||||
|
board.innerHTML = "";
|
||||||
|
for (const category of categories) {
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
cell.className = "category";
|
||||||
|
cell.textContent = category.title;
|
||||||
|
board.appendChild(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let row = 0; row < values.length; row += 1) {
|
||||||
|
for (let col = 0; col < categories.length; col += 1) {
|
||||||
|
const key = `${col}-${row}`;
|
||||||
|
const tile = document.createElement("button");
|
||||||
|
tile.className = `tile${state.used.has(key) ? " used" : ""}`;
|
||||||
|
tile.type = "button";
|
||||||
|
tile.textContent = state.used.has(key) ? "..." : values[row];
|
||||||
|
tile.disabled = state.used.has(key);
|
||||||
|
tile.addEventListener("click", () => openClue(col, row));
|
||||||
|
tile.addEventListener("touchend", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
openClue(col, row);
|
||||||
|
}, { passive: false });
|
||||||
|
board.appendChild(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openClue(col, row) {
|
||||||
|
const key = `${col}-${row}`;
|
||||||
|
if (state.used.has(key)) return;
|
||||||
|
const category = categories[col];
|
||||||
|
const [clue, answer] = category.clues[row];
|
||||||
|
state.current = { key, value: values[row], clue, answer, category: category.title, difficulty: difficulties[row] };
|
||||||
|
modalCategory.textContent = category.title;
|
||||||
|
modalDifficulty.textContent = difficulties[row];
|
||||||
|
modalValue.textContent = values[row];
|
||||||
|
modalClue.textContent = clue;
|
||||||
|
modalAnswer.hidden = true;
|
||||||
|
modalAnswer.textContent = answer;
|
||||||
|
showAnswerButton.hidden = false;
|
||||||
|
correctButton.hidden = true;
|
||||||
|
missedButton.hidden = true;
|
||||||
|
overlay.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAnswer() {
|
||||||
|
modalAnswer.hidden = false;
|
||||||
|
showAnswerButton.hidden = true;
|
||||||
|
correctButton.hidden = false;
|
||||||
|
missedButton.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreCurrent(correct) {
|
||||||
|
if (!state.current) return;
|
||||||
|
state.used.add(state.current.key);
|
||||||
|
state.answered += 1;
|
||||||
|
if (correct) state.score += state.current.value;
|
||||||
|
else state.score = Math.max(0, state.score - Math.floor(state.current.value / 2));
|
||||||
|
closeModal();
|
||||||
|
updateHud();
|
||||||
|
renderBoard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
overlay.hidden = true;
|
||||||
|
state.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHud() {
|
||||||
|
scoreEl.textContent = state.score;
|
||||||
|
answeredEl.textContent = `${state.answered} / 25`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBoard() {
|
||||||
|
state.score = 0;
|
||||||
|
state.answered = 0;
|
||||||
|
state.current = null;
|
||||||
|
state.used.clear();
|
||||||
|
updateHud();
|
||||||
|
renderBoard();
|
||||||
|
overlay.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
showAnswerButton.addEventListener("click", showAnswer);
|
||||||
|
correctButton.addEventListener("click", () => scoreCurrent(true));
|
||||||
|
missedButton.addEventListener("click", () => scoreCurrent(false));
|
||||||
|
closeButton.addEventListener("click", closeModal);
|
||||||
|
resetButton.addEventListener("click", resetBoard);
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") closeModal();
|
||||||
|
if (event.key.toLowerCase() === "r") resetBoard();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderBoard();
|
||||||
|
updateHud();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,914 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,657 @@
|
|||||||
|
<!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>Wheel of Photons</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--panel: rgba(10, 16, 25, 0.92);
|
||||||
|
--line: rgba(255, 255, 255, 0.16);
|
||||||
|
--text: #f8f3ea;
|
||||||
|
--muted: #aeb8c8;
|
||||||
|
--cyan: #65d8e8;
|
||||||
|
--gold: #f2c96b;
|
||||||
|
--green: #88d76f;
|
||||||
|
--red: #ef735f;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 16% 12%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||||
|
radial-gradient(circle at 86% 80%, rgba(242, 201, 107, 0.12), transparent 32%),
|
||||||
|
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand img {
|
||||||
|
width: clamp(48px, 8vw, 72px);
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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.45rem, 3.5vw, 2.5rem);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat,
|
||||||
|
.tip,
|
||||||
|
button,
|
||||||
|
.panel {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
min-width: 108px;
|
||||||
|
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: 1.1rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(260px, 0.82fr) minmax(340px, 1.18fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel-panel {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-height: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel-panel::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 12%;
|
||||||
|
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||||
|
opacity: 0.1;
|
||||||
|
transform: rotate(-18deg);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel {
|
||||||
|
position: relative;
|
||||||
|
width: min(72vmin, 420px);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 10px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 50%;
|
||||||
|
background:
|
||||||
|
conic-gradient(
|
||||||
|
from 0deg,
|
||||||
|
#f2c96b 0 30deg,
|
||||||
|
#65d8e8 30deg 60deg,
|
||||||
|
#88d76f 60deg 90deg,
|
||||||
|
#293544 90deg 120deg,
|
||||||
|
#f2c96b 120deg 150deg,
|
||||||
|
#ef735f 150deg 180deg,
|
||||||
|
#65d8e8 180deg 210deg,
|
||||||
|
#88d76f 210deg 240deg,
|
||||||
|
#293544 240deg 270deg,
|
||||||
|
#f2c96b 270deg 300deg,
|
||||||
|
#65d8e8 300deg 330deg,
|
||||||
|
#ef735f 330deg 360deg
|
||||||
|
);
|
||||||
|
box-shadow: 0 26px 80px rgba(0, 0, 0, 0.45), inset 0 0 60px rgba(0, 0, 0, 0.42);
|
||||||
|
transition: transform 1100ms cubic-bezier(0.16, 0.82, 0.28, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 28%;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.24);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(6, 10, 15, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - min(36vmin, 210px) - 16px);
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
z-index: 3;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-left: 16px solid transparent;
|
||||||
|
border-right: 16px solid transparent;
|
||||||
|
border-top: 26px solid var(--text);
|
||||||
|
filter: drop-shadow(0 5px 8px rgba(0, 0, 0, 0.35));
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin-readout {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 45%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(8, 13, 20, 0.86);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin-readout span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin-readout strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--gold);
|
||||||
|
font-size: clamp(1.7rem, 5vw, 3rem);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
border: 1px solid rgba(101, 216, 232, 0.34);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
color: var(--cyan);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phrase {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-content: center;
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 5px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: clamp(30px, 5vw, 52px);
|
||||||
|
height: clamp(38px, 6vw, 64px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.24);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: clamp(1.15rem, 3vw, 2rem);
|
||||||
|
font-weight: 1000;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile.space {
|
||||||
|
width: 18px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(13, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(72px, 100px));
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 900;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.primary {
|
||||||
|
border-color: rgba(242, 201, 107, 0.62);
|
||||||
|
background: linear-gradient(180deg, #f3d16e, #d89a37);
|
||||||
|
color: #211609;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active-level {
|
||||||
|
color: #071014;
|
||||||
|
background: var(--green);
|
||||||
|
border-color: rgba(136, 215, 111, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter.used {
|
||||||
|
opacity: 0.32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter.good {
|
||||||
|
border-color: rgba(136, 215, 111, 0.62);
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter.bad {
|
||||||
|
border-color: rgba(239, 115, 95, 0.62);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions,
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty button {
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button {
|
||||||
|
min-height: 38px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard .letter {
|
||||||
|
min-height: 48px;
|
||||||
|
font-size: clamp(1rem, 1.8vw, 1.24rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explain {
|
||||||
|
min-height: 54px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(1rem, 1.7vw, 1.15rem);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
body {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
height: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel-panel {
|
||||||
|
min-height: 330px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats,
|
||||||
|
.footer-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard {
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header>
|
||||||
|
<div class="brand">
|
||||||
|
<img src="assets/txproducts-logo-watermark.png" alt="">
|
||||||
|
<div>
|
||||||
|
<small>txproducts.de photon puzzle</small>
|
||||||
|
<h1>Wheel of Photons</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||||
|
<div class="stat"><span>Beamtime</span><strong id="beamtime">6</strong></div>
|
||||||
|
<div class="stat"><span>Puzzle</span><strong id="round">1 / 6</strong></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="panel wheel-panel">
|
||||||
|
<div class="pointer"></div>
|
||||||
|
<div class="wheel" id="wheel"></div>
|
||||||
|
<div class="spin-readout">
|
||||||
|
<div><span>Spin value</span><strong id="spinValue">Spin</strong></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel game-panel">
|
||||||
|
<div class="category" id="category">Category</div>
|
||||||
|
<div class="difficulty" aria-label="Difficulty">
|
||||||
|
<button class="active-level" data-level="Easy" type="button">Easy</button>
|
||||||
|
<button data-level="Medium" type="button">Medium</button>
|
||||||
|
<button data-level="Hard" type="button">Hard</button>
|
||||||
|
</div>
|
||||||
|
<div class="phrase" id="phrase" aria-label="Hidden phrase"></div>
|
||||||
|
<div class="keyboard" id="keyboard" aria-label="Letter keyboard"></div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" id="spinButton" type="button">Spin</button>
|
||||||
|
<button id="solveButton" type="button">Solve / Reveal</button>
|
||||||
|
<button id="nextButton" type="button">Next phrase</button>
|
||||||
|
</div>
|
||||||
|
<div class="explain" id="explain">Spin, pick letters, then use Next phrase for another puzzle.</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="tip">Passerby mode: one spin, one letter, quick reveal. Good for drawing people into the booth.</div>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button id="resetButton" type="button">Reset</button>
|
||||||
|
<button type="button" onclick="window.location.href='index.html'">Back to menu</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const puzzleSets = {
|
||||||
|
Easy: [
|
||||||
|
{ category: "Easy Product", phrase: "WAVEGATE", explain: "WaveGate tailors synchrotron pulse timing." },
|
||||||
|
{ category: "Easy Product", phrase: "PICOSWITCH", explain: "PicoSwitch shortens x-ray pulses." },
|
||||||
|
{ category: "Easy Data", phrase: "TXDATA", explain: "TXData turns measurements into insight." },
|
||||||
|
{ category: "Easy Service", phrase: "TXSERVICES", explain: "TXServices supports better experiments." },
|
||||||
|
{ category: "Easy Physics", phrase: "XRAY PULSE", explain: "X-ray pulses are the timing target." },
|
||||||
|
{ category: "Easy Business", phrase: "BETTER BEAMTIME", explain: "Good support helps teams get more from beamtime." }
|
||||||
|
],
|
||||||
|
Medium: [
|
||||||
|
{ category: "Product", phrase: "WAVEGATE", explain: "WaveGate tailors synchrotron pulse structure from nanoseconds to seconds." },
|
||||||
|
{ category: "Product", phrase: "PICOSWITCH", explain: "PicoSwitch shortens x-ray pulses into the 5-10 ps range." },
|
||||||
|
{ category: "Physics", phrase: "BRAGG DIFFRACTION", explain: "Bragg diffraction links x-ray scattering to ordered crystal structure." },
|
||||||
|
{ category: "Timing", phrase: "TEMPORAL PULSE STRUCTURE", explain: "Timing structure describes when pulses arrive and which pulses are transmitted." },
|
||||||
|
{ category: "Beamline", phrase: "SYNCHROTRON RADIATION", explain: "Synchrotrons produce bright, tunable radiation for advanced experiments." },
|
||||||
|
{ category: "Experiment", phrase: "COHERENT LATTICE DYNAMICS", explain: "Short x-ray pulses help observe fast collective motion in crystals." },
|
||||||
|
{ category: "Business", phrase: "ACTIONABLE INSIGHT", explain: "TXData and TXServices can help convert complex measurements into useful decisions." },
|
||||||
|
{ category: "Service", phrase: "BETTER BEAMTIME", explain: "Good products, data, and support help teams get more value from scarce beamtime." }
|
||||||
|
],
|
||||||
|
Hard: [
|
||||||
|
{ category: "Hard Product", phrase: "PHOTO ACOUSTIC BRAGG SWITCH", explain: "PicoSwitch uses photo-acoustic Bragg switching to shorten x-ray pulses." },
|
||||||
|
{ category: "Hard Timing", phrase: "NANOSECONDS TO SECONDS", explain: "WaveGate covers broad temporal pulse tailoring." },
|
||||||
|
{ category: "Hard Experiment", phrase: "COHERENT LATTICE DYNAMICS", explain: "Short pulses can reveal ultrafast motion inside materials." },
|
||||||
|
{ category: "Hard Business", phrase: "MEASUREMENTS INTO INSIGHT", explain: "TXData and TXServices help convert complex measurements into decisions." },
|
||||||
|
{ category: "Hard Physics", phrase: "DIFFRACTION EFFICIENCY", explain: "Diffraction efficiency matters when choosing repetition rates and switching conditions." },
|
||||||
|
{ category: "Hard Beamline", phrase: "TEMPORAL SYNCHROTRON PULSE STRUCTURE", explain: "Temporal structure controls which pulses reach the experiment." }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let puzzles = puzzleSets.Easy;
|
||||||
|
|
||||||
|
const wheelValues = [100, 150, 200, 250, 300, 350, 400, 500, 600, 750, "BANKRUPT", "PASS"];
|
||||||
|
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
|
const state = {
|
||||||
|
index: 0,
|
||||||
|
level: "Easy",
|
||||||
|
score: 0,
|
||||||
|
beamtime: 6,
|
||||||
|
currentValue: 0,
|
||||||
|
rotation: 0,
|
||||||
|
guessed: new Map(),
|
||||||
|
solved: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const scoreEl = document.querySelector("#score");
|
||||||
|
const beamtimeEl = document.querySelector("#beamtime");
|
||||||
|
const roundEl = document.querySelector("#round");
|
||||||
|
const spinValueEl = document.querySelector("#spinValue");
|
||||||
|
const wheel = document.querySelector("#wheel");
|
||||||
|
const categoryEl = document.querySelector("#category");
|
||||||
|
const phraseEl = document.querySelector("#phrase");
|
||||||
|
const keyboardEl = document.querySelector("#keyboard");
|
||||||
|
const explainEl = document.querySelector("#explain");
|
||||||
|
const spinButton = document.querySelector("#spinButton");
|
||||||
|
const solveButton = document.querySelector("#solveButton");
|
||||||
|
const nextButton = document.querySelector("#nextButton");
|
||||||
|
const resetButton = document.querySelector("#resetButton");
|
||||||
|
const levelButtons = document.querySelectorAll("[data-level]");
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
categoryEl.textContent = `${state.level} / ${puzzle.category}`;
|
||||||
|
scoreEl.textContent = state.score;
|
||||||
|
beamtimeEl.textContent = state.beamtime;
|
||||||
|
roundEl.textContent = `${state.index + 1} / ${puzzles.length}`;
|
||||||
|
spinValueEl.textContent = state.currentValue || "Spin";
|
||||||
|
renderPhrase(puzzle.phrase);
|
||||||
|
renderKeyboard(puzzle.phrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPhrase(phrase) {
|
||||||
|
phraseEl.innerHTML = "";
|
||||||
|
for (const word of phrase.split(" ")) {
|
||||||
|
const wordEl = document.createElement("span");
|
||||||
|
wordEl.className = "word";
|
||||||
|
for (const char of word) {
|
||||||
|
const tile = document.createElement("span");
|
||||||
|
tile.className = "tile";
|
||||||
|
tile.textContent = state.solved || state.guessed.get(char) ? char : "";
|
||||||
|
wordEl.appendChild(tile);
|
||||||
|
}
|
||||||
|
phraseEl.appendChild(wordEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKeyboard(phrase) {
|
||||||
|
keyboardEl.innerHTML = "";
|
||||||
|
for (const letter of letters) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
const used = state.guessed.has(letter);
|
||||||
|
const good = used && phrase.includes(letter);
|
||||||
|
const bad = used && !phrase.includes(letter);
|
||||||
|
button.type = "button";
|
||||||
|
button.className = `letter${used ? " used" : ""}${good ? " good" : ""}${bad ? " bad" : ""}`;
|
||||||
|
button.textContent = letter;
|
||||||
|
button.disabled = used || state.solved || !state.currentValue || state.currentValue === "BANKRUPT" || state.currentValue === "PASS";
|
||||||
|
button.addEventListener("click", () => guessLetter(letter));
|
||||||
|
button.addEventListener("touchend", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
guessLetter(letter);
|
||||||
|
}, { passive: false });
|
||||||
|
keyboardEl.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function spin() {
|
||||||
|
if (state.solved) return;
|
||||||
|
const value = wheelValues[Math.floor(Math.random() * wheelValues.length)];
|
||||||
|
state.currentValue = value;
|
||||||
|
state.rotation += 720 + Math.floor(Math.random() * 720);
|
||||||
|
wheel.style.transform = `rotate(${state.rotation}deg)`;
|
||||||
|
if (value === "BANKRUPT") {
|
||||||
|
state.score = Math.max(0, Math.floor(state.score * 0.5));
|
||||||
|
state.beamtime = Math.max(0, state.beamtime - 1);
|
||||||
|
explainEl.textContent = "Beamtime penalty: half the score is lost. Spin again.";
|
||||||
|
} else if (value === "PASS") {
|
||||||
|
state.beamtime = Math.max(0, state.beamtime - 1);
|
||||||
|
explainEl.textContent = "No useful pulse this turn. Spin again.";
|
||||||
|
} else {
|
||||||
|
explainEl.textContent = `Spin value ${value}. Pick a consonant or vowel from the board.`;
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
checkBeamtime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessLetter(letter) {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
if (state.guessed.has(letter) || state.solved || typeof state.currentValue !== "number") return;
|
||||||
|
const count = puzzle.phrase.split("").filter((char) => char === letter).length;
|
||||||
|
state.guessed.set(letter, true);
|
||||||
|
if (count > 0) {
|
||||||
|
state.score += count * state.currentValue;
|
||||||
|
explainEl.textContent = `${letter} appears ${count} time${count === 1 ? "" : "s"}.`;
|
||||||
|
} else {
|
||||||
|
state.beamtime = Math.max(0, state.beamtime - 1);
|
||||||
|
explainEl.textContent = `${letter} is not in the phrase. Beamtime drops by one.`;
|
||||||
|
}
|
||||||
|
state.currentValue = 0;
|
||||||
|
if (isSolved(puzzle.phrase)) solve(true);
|
||||||
|
render();
|
||||||
|
checkBeamtime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSolved(phrase) {
|
||||||
|
return phrase.split("").every((char) => char === " " || state.guessed.has(char));
|
||||||
|
}
|
||||||
|
|
||||||
|
function solve(auto = false) {
|
||||||
|
const puzzle = puzzles[state.index];
|
||||||
|
state.solved = true;
|
||||||
|
explainEl.textContent = auto ? `${puzzle.phrase}. ${puzzle.explain}` : `${puzzle.phrase}: ${puzzle.explain}`;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPuzzle() {
|
||||||
|
state.index = (state.index + 1) % puzzles.length;
|
||||||
|
state.beamtime = 6;
|
||||||
|
state.currentValue = 0;
|
||||||
|
state.guessed = new Map();
|
||||||
|
state.solved = false;
|
||||||
|
explainEl.textContent = "Spin, pick letters, then use Next phrase for another puzzle.";
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetGame() {
|
||||||
|
state.index = 0;
|
||||||
|
state.score = 0;
|
||||||
|
state.beamtime = 6;
|
||||||
|
state.currentValue = 0;
|
||||||
|
state.rotation = 0;
|
||||||
|
state.guessed = new Map();
|
||||||
|
state.solved = false;
|
||||||
|
explainEl.textContent = "Spin, pick letters, then use Next phrase for another puzzle.";
|
||||||
|
wheel.style.transform = "rotate(0deg)";
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLevel(level) {
|
||||||
|
state.level = level;
|
||||||
|
puzzles = puzzleSets[level];
|
||||||
|
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
|
||||||
|
resetGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkBeamtime() {
|
||||||
|
if (state.beamtime <= 0 && !state.solved) {
|
||||||
|
solve(false);
|
||||||
|
explainEl.textContent = `Beamtime expired. ${puzzles[state.index].phrase}: ${puzzles[state.index].explain}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spinButton.addEventListener("click", spin);
|
||||||
|
solveButton.addEventListener("click", () => solve(false));
|
||||||
|
nextButton.addEventListener("click", nextPuzzle);
|
||||||
|
resetButton.addEventListener("click", resetGame);
|
||||||
|
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
const letter = event.key.toUpperCase();
|
||||||
|
if (letters.includes(letter)) guessLetter(letter);
|
||||||
|
if (event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
spin();
|
||||||
|
}
|
||||||
|
if (event.key.toLowerCase() === "r") resetGame();
|
||||||
|
});
|
||||||
|
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>TX Products Puzzle Games</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;
|
||||||
|
--green: #88d76f;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 16%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||||
|
radial-gradient(circle at 82% 78%, rgba(242, 201, 107, 0.12), 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
top: -28px;
|
||||||
|
left: -18px;
|
||||||
|
width: min(50vw, 500px);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: url("all-games/assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||||
|
opacity: 0.16;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(1080px, 100%);
|
||||||
|
padding: 28px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: var(--cyan);
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 4px 0 10px;
|
||||||
|
font-size: clamp(2.1rem, 5vw, 4.2rem);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 1040px;
|
||||||
|
margin: 0 0 24px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: clamp(1.18rem, 2.2vw, 1.42rem);
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
min-height: 178px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:nth-child(3n + 1) { border-color: rgba(242, 201, 107, 0.45); }
|
||||||
|
a:nth-child(3n + 2) { border-color: rgba(101, 216, 232, 0.45); }
|
||||||
|
a:nth-child(3n) { border-color: rgba(136, 215, 111, 0.45); }
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
font-size: clamp(1.35rem, 2.6vw, 2.15rem);
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
p {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<small>TX Products Puzzle Games</small>
|
||||||
|
<h1>Choose your Challenge</h1>
|
||||||
|
<p>Fast hands, sharp eyes, clear signals. Which challenge will you take on first?</p>
|
||||||
|
<div class="grid">
|
||||||
|
<a href="all-games/wavegate.html">
|
||||||
|
<strong>WaveGate Pulse Tailor</strong>
|
||||||
|
<span>Shape synchrotron pulse timing by opening the gate only during the target window.</span>
|
||||||
|
</a>
|
||||||
|
<a href="all-games/picoswitch.html">
|
||||||
|
<strong>PicoSwitch Pulse Cut</strong>
|
||||||
|
<span>Trigger a photo-acoustic Bragg switch to cut x-ray pulses into the picosecond range.</span>
|
||||||
|
</a>
|
||||||
|
<a href="all-games/synchrotron-jeopardy.html">
|
||||||
|
<strong>Synchrotron Challenge</strong>
|
||||||
|
<span>A Jeopardy-style quiz covering synchrotrons, TXProducts, TXData, TXServices, and business value.</span>
|
||||||
|
</a>
|
||||||
|
<a href="all-games/wheel-of-photons.html">
|
||||||
|
<strong>Wheel of Photons</strong>
|
||||||
|
<span>A phrase puzzle with Easy, Medium, and Hard synchrotron challenges.</span>
|
||||||
|
</a>
|
||||||
|
<a href="all-games/cryptogram.html">
|
||||||
|
<strong>Decode the Signal</strong>
|
||||||
|
<span>A cryptogram where visitors recover hidden product, physics, data, and service messages.</span>
|
||||||
|
</a>
|
||||||
|
<a href="all-games/synchrotron-crossword.html">
|
||||||
|
<strong>Synchrotron Crossword</strong>
|
||||||
|
<span>Connected crossword challenges with Easy, Medium, and Hard boards.</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user