Initial commit: TX Products puzzle games

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mckinleyandClaude Sonnet 4.6 committed 2026-06-17 09:13:06 +02:00
commit 356ed4ea72
9 files changed
+4754

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+606
View File
@@ -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>
+168
View File
@@ -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>
+923
View File
@@ -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>
+746
View File
@@ -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>
+572
View File
@@ -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>
+914
View File
@@ -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>
+657
View File
@@ -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
View File
@@ -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>