Initial commit: TX Products puzzle games
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
356ed4ea72
9 files changed
+4754
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,606 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>Decode the Synchrotron Signal</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--panel: rgba(10, 16, 25, 0.92);
|
||||
--line: rgba(255, 255, 255, 0.16);
|
||||
--text: #f8f3ea;
|
||||
--muted: #aeb8c8;
|
||||
--cyan: #65d8e8;
|
||||
--gold: #f2c96b;
|
||||
--green: #88d76f;
|
||||
--red: #ef735f;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background:
|
||||
radial-gradient(circle at 16% 12%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 86% 78%, rgba(242, 201, 107, 0.13), transparent 32%),
|
||||
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: clamp(48px, 8vw, 72px);
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.brand small {
|
||||
color: var(--cyan);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.45rem, 3.5vw, 2.55rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat,
|
||||
.panel,
|
||||
button {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.stat {
|
||||
min-width: 104px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.stat span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 1.12rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 1.15fr) minmax(280px, 0.85fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
main::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 10% 14%;
|
||||
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||
opacity: 0.08;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid rgba(101, 216, 232, 0.32);
|
||||
border-radius: 8px;
|
||||
color: var(--cyan);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
background: rgba(101, 216, 232, 0.09);
|
||||
}
|
||||
|
||||
.message {
|
||||
min-height: 42px;
|
||||
margin: 0 0 12px;
|
||||
color: var(--gold);
|
||||
font-size: clamp(1rem, 2vw, 1.25rem);
|
||||
font-weight: 850;
|
||||
line-height: 1.32;
|
||||
}
|
||||
|
||||
.phrase {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 12px 18px;
|
||||
min-height: 330px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.word {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
width: clamp(32px, 4.7vw, 52px);
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cipher {
|
||||
width: 100%;
|
||||
color: var(--muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plain {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-bottom: 3px solid rgba(101, 216, 232, 0.65);
|
||||
border-radius: 7px;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
font-size: clamp(1.05rem, 2.8vw, 1.65rem);
|
||||
font-weight: 950;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tile.active .plain {
|
||||
border-color: rgba(242, 201, 107, 0.95);
|
||||
box-shadow: 0 0 0 4px rgba(242, 201, 107, 0.16);
|
||||
}
|
||||
|
||||
.tile.correct .plain {
|
||||
color: #061106;
|
||||
background: var(--green);
|
||||
border-color: var(--green);
|
||||
}
|
||||
|
||||
.side {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.hint h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: clamp(1.4rem, 3vw, 2.1rem);
|
||||
}
|
||||
|
||||
.hint p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1rem, 2vw, 1.15rem);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.difficulty {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(72px, 100px));
|
||||
gap: 5px;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
align-content: end;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 40px;
|
||||
padding: 0 8px;
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
color: #071014;
|
||||
background: linear-gradient(135deg, var(--gold), #ffdc83);
|
||||
border-color: rgba(255, 220, 131, 0.9);
|
||||
}
|
||||
|
||||
button.active-level {
|
||||
color: #071014;
|
||||
background: var(--green);
|
||||
border-color: rgba(136, 215, 111, 0.85);
|
||||
}
|
||||
|
||||
button.letter {
|
||||
min-height: 48px;
|
||||
color: #071014;
|
||||
background: var(--cyan);
|
||||
border-color: rgba(101, 216, 232, 0.8);
|
||||
font-size: clamp(1rem, 1.8vw, 1.24rem);
|
||||
}
|
||||
|
||||
.controls button {
|
||||
min-height: 38px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.difficulty button {
|
||||
min-height: 28px;
|
||||
padding: 0 6px;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.38;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
header,
|
||||
footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.phrase {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header>
|
||||
<div class="brand">
|
||||
<img src="assets/txproducts-logo-watermark.png" alt="">
|
||||
<div>
|
||||
<small>txproducts.de signal puzzle</small>
|
||||
<h1>Decode the Synchrotron Signal</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||
<div class="stat"><span>Signal</span><strong id="signal">1 / 8</strong></div>
|
||||
<div class="stat"><span>Errors</span><strong id="errors">0</strong></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="panel">
|
||||
<div class="category" id="category">Product</div>
|
||||
<p class="message" id="message">Tap an empty tile, then choose the decoded letter.</p>
|
||||
<div class="phrase" id="phrase"></div>
|
||||
</section>
|
||||
|
||||
<aside class="side">
|
||||
<section class="panel hint">
|
||||
<h2>Are you ready?</h2>
|
||||
<p>Decode the hidden phrase. The small top letters are the encrypted signal; your job is to recover the real synchrotron message below.</p>
|
||||
</section>
|
||||
<section class="difficulty" aria-label="Difficulty">
|
||||
<button class="active-level" data-level="Easy" type="button">Easy</button>
|
||||
<button data-level="Medium" type="button">Medium</button>
|
||||
<button data-level="Hard" type="button">Hard</button>
|
||||
</section>
|
||||
<section class="controls">
|
||||
<button class="primary" id="hintButton">Hint</button>
|
||||
<button id="checkButton">Check</button>
|
||||
<button id="nextButton">Next</button>
|
||||
</section>
|
||||
<section class="keyboard" id="keyboard"></section>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<button class="ghost" id="resetButton">Reset</button>
|
||||
<button class="ghost" onclick="window.location.href='index.html'">Back to menu</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
const puzzleSets = {
|
||||
Easy: [
|
||||
{ category: "Easy Product", phrase: "WAVEGATE", note: "WaveGate is the pulse timing product visitors should remember." },
|
||||
{ category: "Easy Product", phrase: "PICOSWITCH", note: "PicoSwitch is the fast x-ray pulse shortening product." },
|
||||
{ category: "Easy Data", phrase: "TXDATA", note: "TXData connects measurements with usable insight." },
|
||||
{ category: "Easy Service", phrase: "TXSERVICES", note: "TXServices supports teams around experiments and beamtime." },
|
||||
{ category: "Easy Physics", phrase: "XRAY PULSE", note: "X-ray pulses are central to these timing games." },
|
||||
{ category: "Easy Business", phrase: "BETTER DATA", note: "Better data helps create better decisions." }
|
||||
],
|
||||
Medium: [
|
||||
{ category: "Product", phrase: "WAVEGATE TAILORS PULSE STRUCTURE", note: "WaveGate shapes synchrotron pulse timing across nanoseconds to seconds." },
|
||||
{ category: "Product", phrase: "PICOSWITCH CUTS X RAY PULSES", note: "PicoSwitch uses photo-acoustic Bragg switching for short x-ray pulses." },
|
||||
{ category: "Physics", phrase: "BRAGG DIFFRACTION SELECTS THE BEAM", note: "Bragg diffraction is the physics clue behind precise switching." },
|
||||
{ category: "Beamline", phrase: "SYNCHROTRON TIMING MATTERS", note: "Timing can decide whether a measurement is clean or noisy." },
|
||||
{ category: "Data", phrase: "TXDATA TURNS MEASUREMENTS INTO INSIGHT", note: "TXData connects experimental output with decisions people can use." },
|
||||
{ category: "Service", phrase: "TXSERVICES SUPPORTS BETTER BEAMTIME", note: "Good support helps teams get more value from scarce experiment time." },
|
||||
{ category: "Business", phrase: "BETTER DATA BETTER DECISIONS", note: "The booth story links products, data, and service to business value." },
|
||||
{ category: "Experiment", phrase: "COHERENT LATTICE DYNAMICS", note: "Short pulses can reveal ultrafast motion inside materials." }
|
||||
],
|
||||
Hard: [
|
||||
{ category: "Hard Physics", phrase: "PHOTO ACOUSTIC BRAGG SWITCHING", note: "PicoSwitch uses photo-acoustic Bragg switching to shorten x-ray pulses." },
|
||||
{ category: "Hard Timing", phrase: "TEMPORAL SYNCHROTRON PULSE STRUCTURE", note: "WaveGate tailors synchrotron pulse structure across broad timescales." },
|
||||
{ category: "Hard Experiment", phrase: "COHERENT LATTICE DYNAMICS IN THIN FILMS", note: "Short x-ray pulses can reveal coherent lattice motion in materials." },
|
||||
{ category: "Hard Business", phrase: "MEASUREMENTS BECOME ACTIONABLE INSIGHT", note: "TXData and TXServices help turn complex experimental output into decisions." },
|
||||
{ category: "Hard Beamline", phrase: "DIFFRACTION EFFICIENCY LIMITS REPETITION RATE", note: "Technical constraints shape how a device performs in real experiments." },
|
||||
{ category: "Hard Product", phrase: "NANOSECONDS TO SECONDS PULSE TAILORING", note: "That broad timing span is the memorable WaveGate story." }
|
||||
]
|
||||
};
|
||||
let puzzles = puzzleSets.Easy;
|
||||
const shifts = [7, 11, 5, 19, 9, 15, 21, 3];
|
||||
const state = {
|
||||
level: "Easy",
|
||||
index: 0,
|
||||
score: 0,
|
||||
errors: 0,
|
||||
selected: 0,
|
||||
guesses: []
|
||||
};
|
||||
|
||||
const phraseEl = document.getElementById("phrase");
|
||||
const keyboardEl = document.getElementById("keyboard");
|
||||
const messageEl = document.getElementById("message");
|
||||
const scoreEl = document.getElementById("score");
|
||||
const signalEl = document.getElementById("signal");
|
||||
const errorsEl = document.getElementById("errors");
|
||||
const categoryEl = document.getElementById("category");
|
||||
const levelButtons = document.querySelectorAll("[data-level]");
|
||||
|
||||
function encodeLetter(letter) {
|
||||
if (!/[A-Z]/.test(letter)) return letter;
|
||||
const shift = shifts[state.index % shifts.length];
|
||||
const next = (alphabet.indexOf(letter) + shift) % 26;
|
||||
return alphabet[next];
|
||||
}
|
||||
|
||||
function getLetters() {
|
||||
return puzzles[state.index].phrase.split("").map((char, index) => ({
|
||||
char,
|
||||
cipher: encodeLetter(char),
|
||||
index,
|
||||
isLetter: /[A-Z]/.test(char)
|
||||
}));
|
||||
}
|
||||
|
||||
function startPuzzle() {
|
||||
const puzzle = puzzles[state.index];
|
||||
state.guesses = Array(puzzle.phrase.length).fill("");
|
||||
state.selected = getLetters().find((item) => item.isLetter)?.index || 0;
|
||||
categoryEl.textContent = `${state.level} / ${puzzle.category}`;
|
||||
messageEl.textContent = "Tap an empty tile, then choose the decoded letter.";
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
const puzzle = puzzles[state.index];
|
||||
scoreEl.textContent = state.score;
|
||||
signalEl.textContent = `${state.index + 1} / ${puzzles.length}`;
|
||||
errorsEl.textContent = state.errors;
|
||||
phraseEl.innerHTML = "";
|
||||
const words = puzzle.phrase.split(" ");
|
||||
let offset = 0;
|
||||
words.forEach((word) => {
|
||||
const wordEl = document.createElement("div");
|
||||
wordEl.className = "word";
|
||||
for (const char of word) {
|
||||
const index = offset;
|
||||
const tile = document.createElement("button");
|
||||
tile.className = "tile";
|
||||
tile.type = "button";
|
||||
if (index === state.selected) tile.classList.add("active");
|
||||
if (state.guesses[index] === puzzle.phrase[index]) tile.classList.add("correct");
|
||||
tile.innerHTML = `<span class="cipher">${encodeLetter(char)}</span><span class="plain">${state.guesses[index] || ""}</span>`;
|
||||
tile.addEventListener("click", () => {
|
||||
state.selected = index;
|
||||
render();
|
||||
});
|
||||
wordEl.appendChild(tile);
|
||||
offset += 1;
|
||||
}
|
||||
phraseEl.appendChild(wordEl);
|
||||
offset += 1;
|
||||
});
|
||||
}
|
||||
|
||||
function renderKeyboard() {
|
||||
keyboardEl.innerHTML = "";
|
||||
alphabet.forEach((letter) => {
|
||||
const button = document.createElement("button");
|
||||
button.className = "letter";
|
||||
button.textContent = letter;
|
||||
button.addEventListener("click", () => guess(letter));
|
||||
keyboardEl.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function nextEmpty(fromIndex = 0) {
|
||||
const puzzle = puzzles[state.index];
|
||||
for (let i = fromIndex; i < puzzle.phrase.length; i += 1) {
|
||||
if (/[A-Z]/.test(puzzle.phrase[i]) && !state.guesses[i]) return i;
|
||||
}
|
||||
for (let i = 0; i < puzzle.phrase.length; i += 1) {
|
||||
if (/[A-Z]/.test(puzzle.phrase[i]) && !state.guesses[i]) return i;
|
||||
}
|
||||
return state.selected;
|
||||
}
|
||||
|
||||
function guess(letter) {
|
||||
const puzzle = puzzles[state.index];
|
||||
if (!/[A-Z]/.test(puzzle.phrase[state.selected])) {
|
||||
state.selected = nextEmpty();
|
||||
}
|
||||
state.guesses[state.selected] = letter;
|
||||
if (letter === puzzle.phrase[state.selected]) {
|
||||
state.score += 25;
|
||||
messageEl.textContent = "Good decode. Keep going.";
|
||||
} else {
|
||||
state.errors += 1;
|
||||
messageEl.textContent = "That signal is off. Try another tile.";
|
||||
}
|
||||
state.selected = nextEmpty(state.selected + 1);
|
||||
render();
|
||||
if (isSolved()) completePuzzle();
|
||||
}
|
||||
|
||||
function isSolved() {
|
||||
const puzzle = puzzles[state.index];
|
||||
return puzzle.phrase.split("").every((char, index) => !/[A-Z]/.test(char) || state.guesses[index] === char);
|
||||
}
|
||||
|
||||
function completePuzzle() {
|
||||
const puzzle = puzzles[state.index];
|
||||
state.score += 150;
|
||||
messageEl.textContent = `Signal decoded: ${puzzle.note}`;
|
||||
render();
|
||||
}
|
||||
|
||||
function revealHint() {
|
||||
const puzzle = puzzles[state.index];
|
||||
const missing = puzzle.phrase.split("").findIndex((char, index) => /[A-Z]/.test(char) && state.guesses[index] !== char);
|
||||
if (missing >= 0) {
|
||||
state.guesses[missing] = puzzle.phrase[missing];
|
||||
state.selected = nextEmpty(missing + 1);
|
||||
messageEl.textContent = "Hint revealed one correct signal letter.";
|
||||
render();
|
||||
if (isSolved()) completePuzzle();
|
||||
}
|
||||
}
|
||||
|
||||
function checkPuzzle() {
|
||||
if (isSolved()) {
|
||||
completePuzzle();
|
||||
return;
|
||||
}
|
||||
const puzzle = puzzles[state.index];
|
||||
const wrong = puzzle.phrase.split("").filter((char, index) => /[A-Z]/.test(char) && state.guesses[index] && state.guesses[index] !== char).length;
|
||||
messageEl.textContent = wrong ? `${wrong} decoded letters need another look.` : "No wrong letters yet. Keep decoding.";
|
||||
}
|
||||
|
||||
function nextPuzzle() {
|
||||
state.index = (state.index + 1) % puzzles.length;
|
||||
startPuzzle();
|
||||
}
|
||||
|
||||
function setLevel(level) {
|
||||
state.level = level;
|
||||
puzzles = puzzleSets[level];
|
||||
state.index = 0;
|
||||
state.score = 0;
|
||||
state.errors = 0;
|
||||
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
|
||||
startPuzzle();
|
||||
}
|
||||
|
||||
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
|
||||
document.getElementById("hintButton").addEventListener("click", revealHint);
|
||||
document.getElementById("checkButton").addEventListener("click", checkPuzzle);
|
||||
document.getElementById("nextButton").addEventListener("click", nextPuzzle);
|
||||
document.getElementById("resetButton").addEventListener("click", () => {
|
||||
state.index = 0;
|
||||
state.score = 0;
|
||||
state.errors = 0;
|
||||
startPuzzle();
|
||||
});
|
||||
window.addEventListener("keydown", (event) => {
|
||||
const letter = event.key.toUpperCase();
|
||||
if (alphabet.includes(letter)) guess(letter);
|
||||
if (event.key === "Enter") checkPuzzle();
|
||||
if (event.key.toLowerCase() === "r") {
|
||||
state.score = 0;
|
||||
state.errors = 0;
|
||||
startPuzzle();
|
||||
}
|
||||
});
|
||||
|
||||
renderKeyboard();
|
||||
startPuzzle();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,168 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TX Products Puzzle Games</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--panel: rgba(12, 18, 27, 0.9);
|
||||
--line: rgba(255, 255, 255, 0.16);
|
||||
--text: #f7f3ea;
|
||||
--muted: #adb7c6;
|
||||
--cyan: #65d8e8;
|
||||
--gold: #f2c96b;
|
||||
--green: #88d76f;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
background:
|
||||
radial-gradient(circle at 18% 16%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 82% 78%, rgba(242, 201, 107, 0.12), transparent 30%),
|
||||
linear-gradient(140deg, #101926 0%, #1b242d 48%, #090c10 100%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: -28px;
|
||||
left: -18px;
|
||||
width: min(50vw, 500px);
|
||||
aspect-ratio: 1;
|
||||
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(1080px, 100%);
|
||||
padding: 28px 0;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--cyan);
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 4px 0 10px;
|
||||
font-size: clamp(2.1rem, 5vw, 4.2rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 1040px;
|
||||
margin: 0 0 24px;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1.18rem, 2.2vw, 1.42rem);
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
min-height: 178px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.36);
|
||||
}
|
||||
|
||||
a:nth-child(3n + 1) { border-color: rgba(242, 201, 107, 0.45); }
|
||||
a:nth-child(3n + 2) { border-color: rgba(101, 216, 232, 0.45); }
|
||||
a:nth-child(3n) { border-color: rgba(136, 215, 111, 0.45); }
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
font-size: clamp(1.35rem, 2.6vw, 2.15rem);
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
p {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<small>TX Products Puzzle Games</small>
|
||||
<h1>Choose your Challenge</h1>
|
||||
<p>Fast hands, sharp eyes, clear signals. Which challenge will you take on first?</p>
|
||||
<div class="grid">
|
||||
<a href="wavegate.html">
|
||||
<strong>WaveGate Pulse Tailor</strong>
|
||||
<span>Shape synchrotron pulse timing by opening the gate only during the target window.</span>
|
||||
</a>
|
||||
<a href="picoswitch.html">
|
||||
<strong>PicoSwitch Pulse Cut</strong>
|
||||
<span>Trigger a photo-acoustic Bragg switch to cut x-ray pulses into the picosecond range.</span>
|
||||
</a>
|
||||
<a href="synchrotron-jeopardy.html">
|
||||
<strong>Synchrotron Challenge</strong>
|
||||
<span>A Jeopardy-style quiz covering synchrotrons, TXProducts, TXData, TXServices, and business value.</span>
|
||||
</a>
|
||||
<a href="wheel-of-photons.html">
|
||||
<strong>Wheel of Photons</strong>
|
||||
<span>A phrase puzzle with Easy, Medium, and Hard synchrotron challenges.</span>
|
||||
</a>
|
||||
<a href="cryptogram.html">
|
||||
<strong>Decode the Signal</strong>
|
||||
<span>A cryptogram where visitors recover hidden product, physics, data, and service messages.</span>
|
||||
</a>
|
||||
<a href="synchrotron-crossword.html">
|
||||
<strong>Synchrotron Crossword</strong>
|
||||
<span>Connected crossword challenges with Easy, Medium, and Hard boards.</span>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,923 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>PicoSwitch Pulse Cut</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #070b10;
|
||||
--panel: rgba(13, 19, 28, 0.9);
|
||||
--line: rgba(255, 255, 255, 0.16);
|
||||
--text: #f5f7fb;
|
||||
--muted: #a8b3c4;
|
||||
--xray: #6ce7ff;
|
||||
--acoustic: #f2d16b;
|
||||
--good: #8be27e;
|
||||
--bad: #f06f63;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background:
|
||||
radial-gradient(circle at 72% 12%, rgba(108, 231, 255, 0.19), transparent 28%),
|
||||
radial-gradient(circle at 18% 84%, rgba(242, 209, 107, 0.12), transparent 30%),
|
||||
linear-gradient(135deg, #071019 0%, #101722 48%, #080b0f 100%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
height: 100%;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.brand small {
|
||||
color: var(--xray);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.35rem, 2.4vw, 2.15rem);
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat,
|
||||
.tip,
|
||||
button {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.stat {
|
||||
min-width: 96px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.stat span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 8px;
|
||||
background: #08101a;
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.34;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.watermark img {
|
||||
width: min(62vmin, 520px);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: manipulation;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
transition: opacity 170ms ease, transform 170ms ease;
|
||||
}
|
||||
|
||||
.overlay.hidden {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: min(820px, calc(100vw - 20px));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 22px;
|
||||
background: rgba(6, 10, 15, 0.9);
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.55rem;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.modal p {
|
||||
margin: 0 0 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.score-report {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 6px 0 18px;
|
||||
border: 1px solid rgba(108, 231, 255, 0.32);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: rgba(108, 231, 255, 0.08);
|
||||
}
|
||||
|
||||
.score-report[hidden],
|
||||
.result-report[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.score-report strong {
|
||||
display: block;
|
||||
color: var(--text);
|
||||
font-size: clamp(3.6rem, 9vw, 6.6rem);
|
||||
line-height: 0.9;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.score-report span {
|
||||
margin-top: 8px;
|
||||
color: var(--xray);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.result-report {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.report-item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.report-item strong {
|
||||
display: block;
|
||||
margin-bottom: 7px;
|
||||
color: var(--text);
|
||||
font-size: clamp(2.2rem, 5vw, 4.4rem);
|
||||
line-height: 0.95;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.report-item span {
|
||||
display: block;
|
||||
color: var(--xray);
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.report-item p {
|
||||
margin: 10px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: clamp(0.92rem, 1.6vw, 1.05rem);
|
||||
line-height: 1.35;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions,
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 42px;
|
||||
border-radius: 8px;
|
||||
padding: 0 15px;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
border-color: rgba(108, 231, 255, 0.55);
|
||||
background: linear-gradient(180deg, #8ff0ff, #31a8cb);
|
||||
color: #031018;
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: 3px solid rgba(242, 209, 107, 0.75);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.trigger-button {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
z-index: 3;
|
||||
width: min(430px, calc(100% - 28px));
|
||||
min-height: 62px;
|
||||
border-color: rgba(108, 231, 255, 0.58);
|
||||
background: linear-gradient(180deg, rgba(108, 231, 255, 0.24), rgba(108, 231, 255, 0.08));
|
||||
color: var(--text);
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.trigger-button:active {
|
||||
border-color: rgba(242, 209, 107, 0.78);
|
||||
background: linear-gradient(180deg, #8ff0ff, #31a8cb);
|
||||
color: #031018;
|
||||
transform: translateX(-50%) translateY(1px);
|
||||
}
|
||||
|
||||
kbd {
|
||||
min-width: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-bottom-color: rgba(255, 255, 255, 0.34);
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
grid-template-rows: auto minmax(560px, 1fr) auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats,
|
||||
.controls {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.stat {
|
||||
flex: 1 1 96px;
|
||||
}
|
||||
|
||||
.result-report {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header>
|
||||
<div class="brand">
|
||||
<small>txproducts.de presents</small>
|
||||
<h1>PicoSwitch Pulse Cut</h1>
|
||||
</div>
|
||||
<div class="stats" aria-live="polite">
|
||||
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||
<div class="stat"><span>Pulse</span><strong id="pulse">-- ps</strong></div>
|
||||
<div class="stat"><span>Coherence</span><strong id="coherence">100%</strong></div>
|
||||
<div class="stat"><span>Round</span><strong id="round">1 / 8</strong></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<canvas id="game" aria-label="PicoSwitch photo-acoustic Bragg pulse timing game"></canvas>
|
||||
<div class="watermark" aria-hidden="true"><img src="assets/txproducts-logo-watermark.png" alt=""></div>
|
||||
<button class="trigger-button" id="triggerButton" type="button" hidden>Trigger PicoSwitch</button>
|
||||
<div class="overlay" id="overlay">
|
||||
<section class="modal">
|
||||
<h2 id="modalTitle">Are you ready?</h2>
|
||||
<p id="modalText">You are about to trigger PicoSwitch. Fire when the pulse reaches the Bragg gate and the acoustic bands are aligned.</p>
|
||||
<div class="score-report" id="scoreReport" hidden>
|
||||
<strong id="finalScore">0</strong>
|
||||
<span>Final score</span>
|
||||
</div>
|
||||
<div class="result-report" id="resultReport" hidden>
|
||||
<div class="report-item">
|
||||
<strong id="fivePsCount">0</strong>
|
||||
<span>5 ps Cut</span>
|
||||
<p>Best timing: pulse and acoustic wave aligned for the shortest cut.</p>
|
||||
</div>
|
||||
<div class="report-item">
|
||||
<strong id="picoCutCount">0</strong>
|
||||
<span>Pico Cut</span>
|
||||
<p>Successful pulse shortening into the 5-10 ps target range.</p>
|
||||
</div>
|
||||
<div class="report-item">
|
||||
<strong id="weakCount">0</strong>
|
||||
<span>Weak Diffraction</span>
|
||||
<p>The pulse hit the gate, but alignment was not clean enough.</p>
|
||||
</div>
|
||||
<div class="report-item">
|
||||
<strong id="missCount">0</strong>
|
||||
<span>Missed</span>
|
||||
<p>The trigger fired outside the useful Bragg timing moment.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" id="startButton" type="button" onclick="startGame(false)" ontouchstart="event.preventDefault(); startGame(false);">I'm ready</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="tip" id="message">Tap at the picosecond moment: pulse, acoustic wave, Bragg gate.</div>
|
||||
<div class="controls" aria-label="Controls">
|
||||
<span><kbd>Space</kbd> trigger</span>
|
||||
<span><kbd>Tap</kbd> trigger</span>
|
||||
<span><kbd>R</kbd> restart</span>
|
||||
<span><kbd>Esc</kbd> stop</span>
|
||||
<button id="stopButton" type="button">Stop demo</button>
|
||||
<button type="button" onclick="window.location.href='index.html'">Back to menu</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.querySelector("#game");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const scoreEl = document.querySelector("#score");
|
||||
const pulseEl = document.querySelector("#pulse");
|
||||
const coherenceEl = document.querySelector("#coherence");
|
||||
const roundEl = document.querySelector("#round");
|
||||
const messageEl = document.querySelector("#message");
|
||||
const overlay = document.querySelector("#overlay");
|
||||
const modalTitle = document.querySelector("#modalTitle");
|
||||
const modalText = document.querySelector("#modalText");
|
||||
const scoreReport = document.querySelector("#scoreReport");
|
||||
const finalScore = document.querySelector("#finalScore");
|
||||
const resultReport = document.querySelector("#resultReport");
|
||||
const fivePsCount = document.querySelector("#fivePsCount");
|
||||
const picoCutCount = document.querySelector("#picoCutCount");
|
||||
const weakCount = document.querySelector("#weakCount");
|
||||
const missCount = document.querySelector("#missCount");
|
||||
const startButton = document.querySelector("#startButton");
|
||||
const triggerButton = document.querySelector("#triggerButton");
|
||||
const stopButton = document.querySelector("#stopButton");
|
||||
|
||||
const state = {
|
||||
mode: "menu",
|
||||
score: 0,
|
||||
pulsePs: null,
|
||||
coherence: 100,
|
||||
round: 1,
|
||||
totalRounds: 8,
|
||||
practice: false,
|
||||
lastTime: 0,
|
||||
gateX: 0,
|
||||
beamY: 0,
|
||||
pulseX: 0,
|
||||
pulseSpeed: 0,
|
||||
pulseLength: 170,
|
||||
acousticPhase: 0,
|
||||
acousticSpeed: 2.2,
|
||||
triggerFlash: 0,
|
||||
report: { five: 0, pico: 0, weak: 0, miss: 0 },
|
||||
result: null
|
||||
};
|
||||
|
||||
function fitCanvas() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scale = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.max(1, Math.floor(rect.width * scale));
|
||||
canvas.height = Math.max(1, Math.floor(rect.height * scale));
|
||||
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
||||
state.gateX = canvas.clientWidth * 0.56;
|
||||
state.beamY = canvas.clientHeight * 0.48;
|
||||
}
|
||||
|
||||
function rand(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function startGame(practice = false) {
|
||||
state.mode = "armed";
|
||||
state.practice = practice;
|
||||
state.score = 0;
|
||||
state.pulsePs = null;
|
||||
state.coherence = 100;
|
||||
state.round = 1;
|
||||
state.report = { five: 0, pico: 0, weak: 0, miss: 0 };
|
||||
overlay.classList.add("hidden");
|
||||
scoreReport.hidden = true;
|
||||
resultReport.hidden = true;
|
||||
triggerButton.hidden = false;
|
||||
nextRound();
|
||||
}
|
||||
|
||||
function nextRound() {
|
||||
const w = canvas.clientWidth;
|
||||
state.mode = "armed";
|
||||
state.gateX = w * 0.56;
|
||||
state.beamY = canvas.clientHeight * 0.48;
|
||||
state.pulseX = -state.pulseLength - rand(0, w * 0.22);
|
||||
state.pulseSpeed = 260 + state.round * 46 + rand(-15, 45);
|
||||
state.pulseLength = Math.max(92, 178 - state.round * 8);
|
||||
state.acousticSpeed = 2.0 + state.round * 0.22;
|
||||
state.triggerFlash = 0;
|
||||
state.result = null;
|
||||
triggerButton.hidden = false;
|
||||
messageEl.textContent = "Trigger when the x-ray pulse is inside the Bragg gate and the acoustic bands align.";
|
||||
updateHud();
|
||||
}
|
||||
|
||||
function updateHud() {
|
||||
scoreEl.textContent = state.score;
|
||||
pulseEl.textContent = state.pulsePs ? `${state.pulsePs.toFixed(1)} ps` : "-- ps";
|
||||
coherenceEl.textContent = `${Math.max(0, Math.round(state.coherence))}%`;
|
||||
roundEl.textContent = state.practice ? `${state.round}` : `${state.round} / ${state.totalRounds}`;
|
||||
}
|
||||
|
||||
function trigger() {
|
||||
if (state.mode !== "armed") return;
|
||||
|
||||
const distance = Math.abs(state.pulseX - state.gateX);
|
||||
const pulseWindow = Math.max(34, state.pulseLength * 0.34);
|
||||
const pulseHit = Math.max(0, 1 - distance / pulseWindow);
|
||||
const phaseHit = acousticAlignment();
|
||||
const precision = Math.pow(pulseHit, 0.72) * (0.55 + Math.pow(phaseHit, 0.9) * 0.45);
|
||||
const pulsePs = 32 - precision * 27;
|
||||
const clampedPulse = Math.max(4.8, pulsePs);
|
||||
const coherence = Math.max(8, 55 + precision * 45 - Math.max(0, distance - 8) * 0.18);
|
||||
let points = Math.round(precision * 700 + Math.max(0, 12 - clampedPulse) * 80);
|
||||
let label = "MISSED";
|
||||
let color = getCss("--bad");
|
||||
|
||||
if (clampedPulse <= 6.2) {
|
||||
label = "5 ps CUT";
|
||||
state.report.five += 1;
|
||||
color = getCss("--good");
|
||||
messageEl.textContent = `Clean Bragg cut: ${clampedPulse.toFixed(1)} ps pulse. +${points}`;
|
||||
} else if (clampedPulse <= 10) {
|
||||
label = "PICO CUT";
|
||||
state.report.pico += 1;
|
||||
color = getCss("--xray");
|
||||
messageEl.textContent = `Pulse shortened to ${clampedPulse.toFixed(1)} ps. +${points}`;
|
||||
} else if (pulseHit > 0.38 || precision > 0.22) {
|
||||
label = "WEAK DIFFRACTION";
|
||||
state.report.weak += 1;
|
||||
color = getCss("--acoustic");
|
||||
points = Math.max(points, Math.round(pulseHit * 180));
|
||||
messageEl.textContent = phaseHit < 0.35
|
||||
? `Pulse was in the gate, but the acoustic wave was out of phase. +${points}`
|
||||
: `Partial cut: ${clampedPulse.toFixed(1)} ps. +${points}`;
|
||||
} else {
|
||||
state.report.miss += 1;
|
||||
points = 0;
|
||||
state.coherence = Math.max(0, state.coherence - 18);
|
||||
messageEl.textContent = "Timing missed: trigger when the pulse and acoustic bands align.";
|
||||
}
|
||||
|
||||
state.score += points;
|
||||
state.pulsePs = clampedPulse;
|
||||
state.coherence = label === "MISSED" ? state.coherence : coherence;
|
||||
state.triggerFlash = 1;
|
||||
state.result = { label, color, precision, age: 0 };
|
||||
state.mode = "result";
|
||||
triggerButton.hidden = true;
|
||||
updateHud();
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (state.mode !== "result") return;
|
||||
if (!state.practice && state.round >= state.totalRounds) {
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
state.round += 1;
|
||||
nextRound();
|
||||
}, 820);
|
||||
}
|
||||
|
||||
function acousticAlignment() {
|
||||
const phase = Math.sin(state.acousticPhase);
|
||||
return 1 - Math.min(1, Math.abs(phase) / 0.92);
|
||||
}
|
||||
|
||||
function triggerReadiness() {
|
||||
const distance = Math.abs(state.pulseX - state.gateX);
|
||||
const pulseWindow = Math.max(34, state.pulseLength * 0.34);
|
||||
const pulseHit = Math.max(0, 1 - distance / pulseWindow);
|
||||
return {
|
||||
pulseHit,
|
||||
phaseHit: acousticAlignment(),
|
||||
ready: pulseHit > 0.42 && acousticAlignment() > 0.32
|
||||
};
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.mode = "over";
|
||||
triggerButton.hidden = true;
|
||||
modalTitle.textContent = "Pulse run complete";
|
||||
modalText.textContent = `Coherence: ${Math.round(state.coherence)}%. Best runs hold coherence while cutting x-ray pulses into the 5-10 ps range.`;
|
||||
showReport();
|
||||
startButton.textContent = "Run again";
|
||||
overlay.classList.remove("hidden");
|
||||
updateHud();
|
||||
}
|
||||
|
||||
function restart() {
|
||||
startGame(state.practice);
|
||||
}
|
||||
|
||||
function stopGame() {
|
||||
state.mode = "over";
|
||||
triggerButton.hidden = true;
|
||||
modalTitle.textContent = "Demo paused";
|
||||
modalText.textContent = "Start again when the next visitor is ready. The goal is to trigger PicoSwitch when the pulse reaches the Bragg gate and the acoustic bands align.";
|
||||
scoreReport.hidden = true;
|
||||
resultReport.hidden = true;
|
||||
startButton.textContent = "I'm ready";
|
||||
overlay.classList.remove("hidden");
|
||||
messageEl.textContent = "Stopped. Ready for the next booth visitor.";
|
||||
}
|
||||
|
||||
function showReport() {
|
||||
finalScore.textContent = state.score;
|
||||
fivePsCount.textContent = state.report.five;
|
||||
picoCutCount.textContent = state.report.pico;
|
||||
weakCount.textContent = state.report.weak;
|
||||
missCount.textContent = state.report.miss;
|
||||
scoreReport.hidden = false;
|
||||
resultReport.hidden = false;
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
state.acousticPhase += state.acousticSpeed * dt;
|
||||
|
||||
if (state.mode === "armed") {
|
||||
state.pulseX += state.pulseSpeed * dt;
|
||||
if (state.pulseX - state.pulseLength > canvas.clientWidth + 80) {
|
||||
state.coherence = Math.max(0, state.coherence - 10);
|
||||
state.pulsePs = null;
|
||||
state.result = { label: "NO TRIGGER", color: getCss("--bad"), precision: 0, age: 0 };
|
||||
state.mode = "result";
|
||||
triggerButton.hidden = true;
|
||||
messageEl.textContent = "No trigger: repetition rate outran the switch.";
|
||||
updateHud();
|
||||
window.setTimeout(() => {
|
||||
if (state.mode !== "result") return;
|
||||
if (!state.practice && state.round >= state.totalRounds) endGame();
|
||||
else {
|
||||
state.round += 1;
|
||||
nextRound();
|
||||
}
|
||||
}, 700);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.triggerFlash > 0) state.triggerFlash = Math.max(0, state.triggerFlash - dt * 2.7);
|
||||
if (state.result) state.result.age += dt;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const w = canvas.clientWidth;
|
||||
const h = canvas.clientHeight;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
drawInstrumentBackground(w, h);
|
||||
drawBeamline(w);
|
||||
drawCrystalGate();
|
||||
drawAcousticWave();
|
||||
drawPulse();
|
||||
drawDetector(w);
|
||||
drawTimingGuide(w);
|
||||
drawResult();
|
||||
}
|
||||
|
||||
function drawInstrumentBackground(w, h) {
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, h);
|
||||
grad.addColorStop(0, "#0a1420");
|
||||
grad.addColorStop(0.56, "#111a22");
|
||||
grad.addColorStop(1, "#070a0d");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.16;
|
||||
ctx.strokeStyle = "#d7efff";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = -40; x < w + 60; x += 44) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x + 76, h);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 70; y < h; y += 72) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.36)";
|
||||
ctx.fillRect(0, h - 72, w, 72);
|
||||
}
|
||||
|
||||
function drawBeamline(w) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(108, 231, 255, 0.3)";
|
||||
ctx.lineWidth = 4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, state.beamY);
|
||||
ctx.lineTo(w, state.beamY);
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(108, 231, 255, 0.08)";
|
||||
ctx.lineWidth = 22;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, state.beamY);
|
||||
ctx.lineTo(w, state.beamY);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawCrystalGate() {
|
||||
const gateW = 88;
|
||||
const gateH = 220;
|
||||
const x = state.gateX - gateW / 2;
|
||||
const y = state.beamY - gateH / 2;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgba(108, 231, 255, 0.08)";
|
||||
ctx.strokeStyle = "rgba(108, 231, 255, 0.65)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, gateW, gateH, 8);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.globalAlpha = 0.55;
|
||||
ctx.strokeStyle = getCss("--acoustic");
|
||||
ctx.setLineDash([8, 8]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(state.gateX, y - 20);
|
||||
ctx.lineTo(state.gateX, y + gateH + 20);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawAcousticWave() {
|
||||
const gateW = 88;
|
||||
const gateH = 220;
|
||||
const left = state.gateX - gateW / 2;
|
||||
const top = state.beamY - gateH / 2;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(left, top, gateW, gateH);
|
||||
ctx.clip();
|
||||
|
||||
for (let i = -5; i < 12; i += 1) {
|
||||
const y = top + i * 26 + ((state.acousticPhase * 34) % 26);
|
||||
const alignmentBoost = acousticAlignment();
|
||||
ctx.strokeStyle = `rgba(242, 209, 107, ${0.16 + alignmentBoost * 0.28})`;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(left - 12, y);
|
||||
ctx.lineTo(left + gateW + 12, y - 30);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawPulse() {
|
||||
const y = state.beamY;
|
||||
const x = state.pulseX;
|
||||
const length = state.mode === "result" && state.pulsePs && state.pulsePs <= 10 ? 34 : state.pulseLength;
|
||||
const glow = ctx.createLinearGradient(x - length, 0, x + length, 0);
|
||||
glow.addColorStop(0, "rgba(108, 231, 255, 0)");
|
||||
glow.addColorStop(0.48, "rgba(108, 231, 255, 0.85)");
|
||||
glow.addColorStop(1, "rgba(108, 231, 255, 0)");
|
||||
|
||||
ctx.save();
|
||||
ctx.shadowColor = getCss("--xray");
|
||||
ctx.shadowBlur = 18 + state.triggerFlash * 30;
|
||||
ctx.strokeStyle = glow;
|
||||
ctx.lineWidth = 18;
|
||||
ctx.lineCap = "round";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - length, y);
|
||||
ctx.lineTo(x + length * 0.35, y);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = "#f5fbff";
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 7 + state.triggerFlash * 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
if (state.triggerFlash > 0) {
|
||||
ctx.strokeStyle = `rgba(242, 209, 107, ${state.triggerFlash})`;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.arc(state.gateX, y, 42 + (1 - state.triggerFlash) * 70, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawDetector(w) {
|
||||
const x = w - 92;
|
||||
const y = state.beamY;
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 0.08)";
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.28)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y - 58, 58, 116, 8);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
const bars = Math.max(1, Math.round((state.coherence / 100) * 5));
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
ctx.fillStyle = i < bars ? getCss("--good") : "rgba(255, 255, 255, 0.12)";
|
||||
ctx.fillRect(x + 12, y + 35 - i * 18, 34, 9);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawTimingGuide(w) {
|
||||
if (state.mode !== "armed") return;
|
||||
const y = state.beamY + 92;
|
||||
const readiness = triggerReadiness();
|
||||
const heat = Math.max(readiness.pulseHit, readiness.phaseHit * 0.7);
|
||||
ctx.save();
|
||||
ctx.fillStyle = `rgba(108, 231, 255, ${0.14 + heat * 0.22})`;
|
||||
ctx.fillRect(state.gateX - 72, y, 144, 6);
|
||||
ctx.fillStyle = getCss("--acoustic");
|
||||
ctx.fillRect(state.gateX - 72, y, 144 * acousticAlignment(), 6);
|
||||
ctx.font = "900 13px Inter, system-ui, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillStyle = readiness.ready ? getCss("--good") : "rgba(245, 247, 251, 0.78)";
|
||||
ctx.fillText(readiness.ready ? "BEST ALIGNMENT" : "ALIGN PULSE + ACOUSTIC WAVE", state.gateX, y + 24);
|
||||
ctx.font = "800 11px Inter, system-ui, sans-serif";
|
||||
ctx.fillStyle = "rgba(245, 247, 251, 0.62)";
|
||||
ctx.fillText("pulse in gate + acoustic wave aligned", state.gateX, y + 42);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawResult() {
|
||||
if (!state.result) return;
|
||||
const age = state.result.age;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = Math.max(0, 1 - age * 0.55);
|
||||
ctx.fillStyle = state.result.color;
|
||||
ctx.font = "900 30px Inter, system-ui, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(state.result.label, state.gateX, Math.max(48, state.beamY - 150 - age * 28));
|
||||
if (state.pulsePs) {
|
||||
ctx.font = "900 18px Inter, system-ui, sans-serif";
|
||||
ctx.fillText(`${state.pulsePs.toFixed(1)} ps`, state.gateX, Math.max(80, state.beamY - 116 - age * 20));
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function getCss(name) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
function loop(time) {
|
||||
const dt = Math.min(0.033, (time - state.lastTime) / 1000 || 0);
|
||||
state.lastTime = time;
|
||||
update(dt);
|
||||
draw();
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function onTap(element, handler) {
|
||||
let handledAt = 0;
|
||||
const run = (event) => {
|
||||
const now = Date.now();
|
||||
if (now - handledAt < 450) return;
|
||||
handledAt = now;
|
||||
event.preventDefault();
|
||||
handler();
|
||||
};
|
||||
element.addEventListener("click", run);
|
||||
element.addEventListener("touchend", run, { passive: false });
|
||||
}
|
||||
|
||||
window.addEventListener("resize", fitCanvas);
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.code === "Space") {
|
||||
event.preventDefault();
|
||||
trigger();
|
||||
}
|
||||
if (event.key.toLowerCase() === "r") restart();
|
||||
if (event.key === "Escape") stopGame();
|
||||
});
|
||||
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
triggerButton.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
triggerButton.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
trigger();
|
||||
});
|
||||
onTap(startButton, () => startGame(false));
|
||||
onTap(stopButton, stopGame);
|
||||
|
||||
fitCanvas();
|
||||
requestAnimationFrame(loop);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,746 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>Synchrotron Mini Crossword</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--panel: rgba(10, 16, 25, 0.92);
|
||||
--line: rgba(255, 255, 255, 0.16);
|
||||
--text: #f8f3ea;
|
||||
--muted: #aeb8c8;
|
||||
--cyan: #65d8e8;
|
||||
--gold: #f2c96b;
|
||||
--green: #88d76f;
|
||||
--red: #ef735f;
|
||||
--cell: #f8f3ea;
|
||||
--ink: #071014;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background:
|
||||
radial-gradient(circle at 14% 14%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 86% 78%, rgba(242, 201, 107, 0.13), transparent 32%),
|
||||
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: clamp(42px, 6vw, 60px);
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.brand small {
|
||||
color: var(--cyan);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.45rem, 3.5vw, 2.55rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat,
|
||||
.panel,
|
||||
button {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.stat {
|
||||
min-width: 94px;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
|
||||
.stat span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(500px, 1.08fr) minmax(300px, 0.92fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
main::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 10% 16%;
|
||||
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||
opacity: 0.07;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.board-wrap {
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.board {
|
||||
width: min(100%, 540px);
|
||||
max-width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(17, 1fr);
|
||||
gap: 2px;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.cell {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
color: var(--ink);
|
||||
background: var(--cell);
|
||||
font: inherit;
|
||||
font-size: clamp(0.76rem, 1.45vw, 1.08rem);
|
||||
font-weight: 950;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.cell.block {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cell.active {
|
||||
background: var(--gold);
|
||||
box-shadow: 0 0 0 3px rgba(242, 201, 107, 0.2);
|
||||
}
|
||||
|
||||
.cell.related {
|
||||
background: #d8f6fb;
|
||||
}
|
||||
|
||||
.cell.correct {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.num {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 3px;
|
||||
color: rgba(7, 16, 20, 0.68);
|
||||
font-size: 0.42rem;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.side {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(190px, 1fr) auto auto auto;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0 0 12px;
|
||||
color: var(--gold);
|
||||
font-size: clamp(1rem, 2vw, 1.25rem);
|
||||
font-weight: 850;
|
||||
line-height: 1.32;
|
||||
}
|
||||
|
||||
.clues {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
max-height: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.clues h2 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--cyan);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.clues ol {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: var(--muted);
|
||||
font-size: clamp(0.76rem, 1.15vw, 0.9rem);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.clues li {
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clues li.active {
|
||||
color: var(--text);
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.difficulty {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(72px, 100px));
|
||||
gap: 5px;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 40px;
|
||||
padding: 0 8px;
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
color: #071014;
|
||||
background: linear-gradient(135deg, var(--gold), #ffdc83);
|
||||
border-color: rgba(255, 220, 131, 0.9);
|
||||
}
|
||||
|
||||
button.active-level {
|
||||
color: #071014;
|
||||
background: var(--green);
|
||||
border-color: rgba(136, 215, 111, 0.85);
|
||||
}
|
||||
|
||||
button.letter {
|
||||
min-height: 48px;
|
||||
color: #071014;
|
||||
background: var(--cyan);
|
||||
border-color: rgba(101, 216, 232, 0.8);
|
||||
font-size: clamp(1rem, 1.8vw, 1.24rem);
|
||||
}
|
||||
|
||||
.controls button {
|
||||
min-height: 38px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.difficulty button {
|
||||
min-height: 28px;
|
||||
font-size: 0.76rem;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
header,
|
||||
footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.board {
|
||||
width: min(100%, 540px);
|
||||
}
|
||||
|
||||
.clues {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.board {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clues {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
grid-template-columns: repeat(9, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.controls {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header>
|
||||
<div class="brand">
|
||||
<img src="assets/txproducts-logo-watermark.png" alt="">
|
||||
<div>
|
||||
<small>txproducts.de technical crossword</small>
|
||||
<h1>Synchrotron Mini Crossword</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||
<div class="stat"><span>Filled</span><strong id="filled">0%</strong></div>
|
||||
<div class="stat"><span>Mode</span><strong id="mode">Across</strong></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="panel board-wrap">
|
||||
<p class="message" id="message">Tap a square or clue, then fill the answer. Short, technical, and booth-friendly.</p>
|
||||
<div class="board" id="board"></div>
|
||||
</section>
|
||||
|
||||
<aside class="side">
|
||||
<section class="panel clues">
|
||||
<div>
|
||||
<h2>Across</h2>
|
||||
<ol id="acrossClues"></ol>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Down</h2>
|
||||
<ol id="downClues"></ol>
|
||||
</div>
|
||||
</section>
|
||||
<section class="difficulty" aria-label="Difficulty">
|
||||
<button class="active-level" data-level="Easy" type="button">Easy</button>
|
||||
<button data-level="Medium" type="button">Medium</button>
|
||||
<button data-level="Hard" type="button">Hard</button>
|
||||
</section>
|
||||
<section class="keyboard" id="keyboard"></section>
|
||||
<section class="controls">
|
||||
<button class="primary" id="checkButton">Check</button>
|
||||
<button id="revealButton">Reveal</button>
|
||||
<button id="clearButton">Clear</button>
|
||||
<button class="ghost" id="resetButton">Reset</button>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<button class="ghost" onclick="window.location.href='index.html'">Back to menu</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
const rows = 17;
|
||||
const cols = 17;
|
||||
const puzzleSets = {
|
||||
Easy: [
|
||||
{ num: 1, dir: "across", row: 2, col: 7, answer: "PICOSWITCH", clue: "Product that shortens x-ray pulses." },
|
||||
{ num: 2, dir: "down", row: 2, col: 14, answer: "TXSERVICES", clue: "Support area for customers and experiments." },
|
||||
{ num: 3, dir: "down", row: 4, col: 7, answer: "VALUE", clue: "What the right solution should create for a customer." },
|
||||
{ num: 4, dir: "across", row: 5, col: 0, answer: "DATA", clue: "Measurement output before analysis." },
|
||||
{ num: 5, dir: "across", row: 5, col: 10, answer: "PULSE", clue: "Short burst of x-rays or signal." },
|
||||
{ num: 6, dir: "down", row: 5, col: 2, answer: "TXDATA", clue: "Turns measurements into insight." },
|
||||
{ num: 7, dir: "down", row: 5, col: 10, answer: "PHOTON", clue: "Particle of light." },
|
||||
{ num: 8, dir: "down", row: 6, col: 5, answer: "XRAY", clue: "Radiation used to probe structures." },
|
||||
{ num: 9, dir: "across", row: 8, col: 4, answer: "WAVEGATE", clue: "Product for tailoring pulse timing." },
|
||||
{ num: 10, dir: "across", row: 10, col: 0, answer: "BRAGG", clue: "Diffraction name linked to crystal planes." },
|
||||
{ num: 11, dir: "across", row: 10, col: 7, answer: "SIGNAL", clue: "Useful information separated from noise." },
|
||||
{ num: 12, dir: "down", row: 10, col: 0, answer: "BEAM", clue: "Focused stream used at a beamline." },
|
||||
{ num: 13, dir: "down", row: 10, col: 4, answer: "GATE", clue: "Timing window that lets a pulse through." },
|
||||
{ num: 14, dir: "down", row: 10, col: 8, answer: "ION", clue: "Charged particle." },
|
||||
{ num: 15, dir: "across", row: 12, col: 4, answer: "TIMING", clue: "When pulses arrive." }
|
||||
],
|
||||
Medium: [
|
||||
{ num: 1, dir: "across", row: 1, col: 1, answer: "WAVEGATE", clue: "TXProducts device for tailoring synchrotron pulse timing." },
|
||||
{ num: 2, dir: "down", row: 3, col: 4, answer: "BEAMTIME", clue: "Scarce experiment time that teams want to use well." },
|
||||
{ num: 3, dir: "across", row: 3, col: 6, answer: "TXSERVICES", clue: "Support area that helps teams get more from beamtime and experiments." },
|
||||
{ num: 4, dir: "down", row: 3, col: 12, answer: "INSIGHT", clue: "What useful analysis gives a business after the measurement." },
|
||||
{ num: 5, dir: "across", row: 5, col: 3, answer: "LATTICE", clue: "Ordered atomic structure studied in many materials experiments." },
|
||||
{ num: 6, dir: "down", row: 6, col: 10, answer: "GATE", clue: "A timing window that lets the wanted pulse through." },
|
||||
{ num: 7, dir: "across", row: 6, col: 12, answer: "ION", clue: "Charged particle found throughout accelerator physics vocabulary." },
|
||||
{ num: 8, dir: "down", row: 8, col: 1, answer: "BRAGG", clue: "Diffraction condition central to selecting and switching x-rays." },
|
||||
{ num: 9, dir: "across", row: 8, col: 3, answer: "PICOSWITCH", clue: "Photo-acoustic Bragg switch for shortening x-ray pulses." },
|
||||
{ num: 10, dir: "down", row: 8, col: 7, answer: "SIGNAL", clue: "Useful measurement information that must be separated from noise." },
|
||||
{ num: 11, dir: "across", row: 10, col: 0, answer: "VALUE", clue: "What the right product, data, and service should create for a customer." },
|
||||
{ num: 12, dir: "down", row: 11, col: 11, answer: "DATA", clue: "Raw experiment output before analysis turns it into decisions." },
|
||||
{ num: 13, dir: "across", row: 12, col: 6, answer: "WAVEGATE", clue: "Product that tailors synchrotron pulse structure from nanoseconds to seconds." },
|
||||
{ num: 14, dir: "across", row: 14, col: 9, answer: "XRAY", clue: "Radiation used to probe structure and dynamics at beamlines." },
|
||||
{ num: 15, dir: "down", row: 14, col: 10, answer: "RAY", clue: "A beam, line, or part of the x-ray term." }
|
||||
],
|
||||
Hard: [
|
||||
{ num: 1, dir: "down", row: 0, col: 0, answer: "DETECTOR", clue: "Instrument that records the experimental signal." },
|
||||
{ num: 2, dir: "down", row: 1, col: 6, answer: "SIGNAL", clue: "Measurement information separated from noise." },
|
||||
{ num: 3, dir: "across", row: 2, col: 5, answer: "PICOSWITCH", clue: "Photo-acoustic Bragg switch for shortening x-ray pulses." },
|
||||
{ num: 4, dir: "down", row: 2, col: 10, answer: "WAVEGATE", clue: "Product that tailors temporal pulse structure." },
|
||||
{ num: 5, dir: "down", row: 3, col: 15, answer: "DYNAMICS", clue: "Time-dependent motion studied in materials." },
|
||||
{ num: 6, dir: "across", row: 4, col: 0, answer: "COHERENCE", clue: "Wave property important in many advanced experiments." },
|
||||
{ num: 7, dir: "across", row: 5, col: 9, answer: "BEAMLINE", clue: "Experimental path that delivers synchrotron radiation." },
|
||||
{ num: 8, dir: "down", row: 7, col: 8, answer: "SAMPLE", clue: "Material placed in the beam for measurement." },
|
||||
{ num: 9, dir: "down", row: 7, col: 13, answer: "ANALYSIS", clue: "Process that turns data into conclusions." },
|
||||
{ num: 10, dir: "across", row: 8, col: 3, answer: "DIFFRACTION", clue: "Scattering pattern used to infer structure." },
|
||||
{ num: 11, dir: "down", row: 10, col: 5, answer: "BRAGG", clue: "Diffraction condition named for a father-son team." },
|
||||
{ num: 12, dir: "down", row: 10, col: 11, answer: "PHOTON", clue: "Quantum of light, including x-rays." },
|
||||
{ num: 13, dir: "across", row: 12, col: 4, answer: "VALUE", clue: "Business result created by better data and support." },
|
||||
{ num: 14, dir: "across", row: 13, col: 9, answer: "LATTICE", clue: "Ordered atomic arrangement in a crystal." },
|
||||
{ num: 15, dir: "across", row: 15, col: 7, answer: "TIMING", clue: "Critical control variable for pulses." }
|
||||
]
|
||||
};
|
||||
let entries = puzzleSets.Easy;
|
||||
let grid = [];
|
||||
let guesses = [];
|
||||
let cellEntries = {};
|
||||
let numbers = {};
|
||||
const state = {
|
||||
level: "Easy",
|
||||
active: null,
|
||||
row: 0,
|
||||
col: 0,
|
||||
score: 0
|
||||
};
|
||||
|
||||
function buildGrid() {
|
||||
entries = puzzleSets[state.level];
|
||||
grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => null));
|
||||
guesses = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ""));
|
||||
cellEntries = {};
|
||||
numbers = {};
|
||||
entries.forEach((entry) => {
|
||||
numbers[`${entry.row},${entry.col}`] = entry.num;
|
||||
entry.answer.split("").forEach((letter, index) => {
|
||||
const row = entry.row + (entry.dir === "down" ? index : 0);
|
||||
const col = entry.col + (entry.dir === "across" ? index : 0);
|
||||
grid[row][col] = letter;
|
||||
const entryKey = `${row},${col}`;
|
||||
if (!cellEntries[entryKey]) cellEntries[entryKey] = [];
|
||||
cellEntries[entryKey].push(entry);
|
||||
});
|
||||
});
|
||||
state.active = entries[0];
|
||||
state.row = entries[0].row;
|
||||
state.col = entries[0].col;
|
||||
}
|
||||
|
||||
const boardEl = document.getElementById("board");
|
||||
const keyboardEl = document.getElementById("keyboard");
|
||||
const acrossEl = document.getElementById("acrossClues");
|
||||
const downEl = document.getElementById("downClues");
|
||||
const messageEl = document.getElementById("message");
|
||||
const scoreEl = document.getElementById("score");
|
||||
const filledEl = document.getElementById("filled");
|
||||
const modeEl = document.getElementById("mode");
|
||||
const levelButtons = document.querySelectorAll("[data-level]");
|
||||
|
||||
function key(row, col) {
|
||||
return `${row},${col}`;
|
||||
}
|
||||
|
||||
function activeCells() {
|
||||
const cells = [];
|
||||
state.active.answer.split("").forEach((_, index) => {
|
||||
cells.push({
|
||||
row: state.active.row + (state.active.dir === "down" ? index : 0),
|
||||
col: state.active.col + (state.active.dir === "across" ? index : 0)
|
||||
});
|
||||
});
|
||||
return cells;
|
||||
}
|
||||
|
||||
function chooseCell(row, col) {
|
||||
if (!grid[row][col]) return;
|
||||
const options = cellEntries[key(row, col)];
|
||||
if (options.includes(state.active) && options.length > 1) {
|
||||
state.active = options.find((entry) => entry !== state.active);
|
||||
} else {
|
||||
state.active = options[0];
|
||||
}
|
||||
state.row = row;
|
||||
state.col = col;
|
||||
render();
|
||||
}
|
||||
|
||||
function chooseEntry(entry) {
|
||||
state.active = entry;
|
||||
state.row = entry.row;
|
||||
state.col = entry.col;
|
||||
render();
|
||||
}
|
||||
|
||||
function renderBoard() {
|
||||
const related = new Set(activeCells().map((cell) => key(cell.row, cell.col)));
|
||||
boardEl.innerHTML = "";
|
||||
for (let row = 0; row < rows; row += 1) {
|
||||
for (let col = 0; col < cols; col += 1) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "cell";
|
||||
if (!grid[row][col]) {
|
||||
button.classList.add("block");
|
||||
button.disabled = true;
|
||||
} else {
|
||||
if (related.has(key(row, col))) button.classList.add("related");
|
||||
if (row === state.row && col === state.col) button.classList.add("active");
|
||||
if (guesses[row][col] === grid[row][col]) button.classList.add("correct");
|
||||
button.textContent = guesses[row][col];
|
||||
if (numbers[key(row, col)]) {
|
||||
const num = document.createElement("span");
|
||||
num.className = "num";
|
||||
num.textContent = numbers[key(row, col)];
|
||||
button.appendChild(num);
|
||||
}
|
||||
button.addEventListener("click", () => chooseCell(row, col));
|
||||
}
|
||||
boardEl.appendChild(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderClues() {
|
||||
acrossEl.innerHTML = "";
|
||||
downEl.innerHTML = "";
|
||||
entries.forEach((entry) => {
|
||||
const li = document.createElement("li");
|
||||
li.value = entry.num;
|
||||
li.textContent = entry.clue;
|
||||
if (entry === state.active) li.className = "active";
|
||||
li.addEventListener("click", () => chooseEntry(entry));
|
||||
(entry.dir === "across" ? acrossEl : downEl).appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function renderKeyboard() {
|
||||
keyboardEl.innerHTML = "";
|
||||
alphabet.forEach((letter) => {
|
||||
const button = document.createElement("button");
|
||||
button.className = "letter";
|
||||
button.textContent = letter;
|
||||
button.addEventListener("click", () => typeLetter(letter));
|
||||
keyboardEl.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
let total = 0;
|
||||
let filled = 0;
|
||||
for (let row = 0; row < rows; row += 1) {
|
||||
for (let col = 0; col < cols; col += 1) {
|
||||
if (grid[row][col]) {
|
||||
total += 1;
|
||||
if (guesses[row][col]) filled += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
scoreEl.textContent = state.score;
|
||||
filledEl.textContent = `${Math.round((filled / total) * 100)}%`;
|
||||
modeEl.textContent = `${state.level} ${state.active.dir === "across" ? "Across" : "Down"}`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderBoard();
|
||||
renderClues();
|
||||
renderStats();
|
||||
}
|
||||
|
||||
function nextCell() {
|
||||
const cells = activeCells();
|
||||
const current = cells.findIndex((cell) => cell.row === state.row && cell.col === state.col);
|
||||
const next = cells[Math.min(current + 1, cells.length - 1)] || cells[0];
|
||||
state.row = next.row;
|
||||
state.col = next.col;
|
||||
}
|
||||
|
||||
function previousCell() {
|
||||
const cells = activeCells();
|
||||
const current = cells.findIndex((cell) => cell.row === state.row && cell.col === state.col);
|
||||
const previous = cells[Math.max(current - 1, 0)] || cells[0];
|
||||
state.row = previous.row;
|
||||
state.col = previous.col;
|
||||
}
|
||||
|
||||
function typeLetter(letter) {
|
||||
guesses[state.row][state.col] = letter;
|
||||
nextCell();
|
||||
render();
|
||||
}
|
||||
|
||||
function backspace() {
|
||||
if (guesses[state.row][state.col]) {
|
||||
guesses[state.row][state.col] = "";
|
||||
} else {
|
||||
previousCell();
|
||||
guesses[state.row][state.col] = "";
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function checkPuzzle() {
|
||||
let wrong = 0;
|
||||
let correct = 0;
|
||||
for (let row = 0; row < rows; row += 1) {
|
||||
for (let col = 0; col < cols; col += 1) {
|
||||
if (grid[row][col] && guesses[row][col]) {
|
||||
if (guesses[row][col] === grid[row][col]) correct += 1;
|
||||
else wrong += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
state.score = correct * 20;
|
||||
messageEl.textContent = wrong ? `${wrong} squares need another look.` : "Everything filled so far is correct.";
|
||||
if (isComplete() && !wrong) {
|
||||
state.score += 400;
|
||||
messageEl.textContent = "Crossword complete: product, physics, data, and service all connected.";
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function isComplete() {
|
||||
for (let row = 0; row < rows; row += 1) {
|
||||
for (let col = 0; col < cols; col += 1) {
|
||||
if (grid[row][col] && guesses[row][col] !== grid[row][col]) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function revealActive() {
|
||||
activeCells().forEach((cell, index) => {
|
||||
guesses[cell.row][cell.col] = state.active.answer[index];
|
||||
});
|
||||
messageEl.textContent = `${state.active.num} ${state.active.dir} revealed.`;
|
||||
render();
|
||||
}
|
||||
|
||||
function clearActive() {
|
||||
activeCells().forEach((cell) => {
|
||||
guesses[cell.row][cell.col] = "";
|
||||
});
|
||||
messageEl.textContent = "Current answer cleared.";
|
||||
render();
|
||||
}
|
||||
|
||||
function resetPuzzle() {
|
||||
buildGrid();
|
||||
state.score = 0;
|
||||
messageEl.textContent = `${state.level} crossword ready. Tap a square or clue, then fill the answer.`;
|
||||
render();
|
||||
}
|
||||
|
||||
function setLevel(level) {
|
||||
state.level = level;
|
||||
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
|
||||
resetPuzzle();
|
||||
}
|
||||
|
||||
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
|
||||
document.getElementById("checkButton").addEventListener("click", checkPuzzle);
|
||||
document.getElementById("revealButton").addEventListener("click", revealActive);
|
||||
document.getElementById("clearButton").addEventListener("click", clearActive);
|
||||
document.getElementById("resetButton").addEventListener("click", resetPuzzle);
|
||||
window.addEventListener("keydown", (event) => {
|
||||
const letter = event.key.toUpperCase();
|
||||
if (alphabet.includes(letter)) typeLetter(letter);
|
||||
if (event.key === "Backspace") backspace();
|
||||
if (event.key === "Enter") checkPuzzle();
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
const index = entries.indexOf(state.active);
|
||||
chooseEntry(entries[(index + 1) % entries.length]);
|
||||
}
|
||||
if (event.key.toLowerCase() === "r") resetPuzzle();
|
||||
});
|
||||
|
||||
buildGrid();
|
||||
renderKeyboard();
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,572 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>Synchrotron Challenge</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--panel: rgba(10, 16, 25, 0.92);
|
||||
--line: rgba(255, 255, 255, 0.16);
|
||||
--text: #f8f3ea;
|
||||
--muted: #aeb8c8;
|
||||
--cyan: #65d8e8;
|
||||
--gold: #f2c96b;
|
||||
--green: #88d76f;
|
||||
--red: #ef735f;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background:
|
||||
radial-gradient(circle at 16% 12%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 86% 80%, rgba(242, 201, 107, 0.12), transparent 32%),
|
||||
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: clamp(48px, 8vw, 76px);
|
||||
height: auto;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.brand small {
|
||||
color: var(--cyan);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.45rem, 3.5vw, 2.5rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.scorebar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score,
|
||||
.tip,
|
||||
button {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.score {
|
||||
min-width: 112px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.score span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.score strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 1.1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.05;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.watermark img {
|
||||
width: min(62vmin, 520px);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.board {
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-rows: auto repeat(5, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category,
|
||||
.tile {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(9, 14, 22, 0.88);
|
||||
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.category {
|
||||
min-height: 74px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 8px;
|
||||
color: var(--cyan);
|
||||
font-size: clamp(0.72rem, 1.45vw, 1rem);
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tile {
|
||||
min-height: 72px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 8px;
|
||||
color: var(--gold);
|
||||
font-size: clamp(1.3rem, 3.4vw, 2.6rem);
|
||||
font-weight: 1000;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.tile.used {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
background: rgba(2, 5, 10, 0.62);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: min(900px, 100%);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
background: rgba(7, 11, 18, 0.95);
|
||||
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.modal-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0;
|
||||
color: var(--cyan);
|
||||
font-size: clamp(1rem, 2.2vw, 1.25rem);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--gold);
|
||||
font-size: clamp(1.4rem, 4vw, 2.4rem);
|
||||
font-weight: 1000;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.clue {
|
||||
min-height: 145px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
font-size: clamp(1.55rem, 4vw, 3.2rem);
|
||||
font-weight: 900;
|
||||
line-height: 1.12;
|
||||
}
|
||||
|
||||
.answer {
|
||||
margin: 16px 0 0;
|
||||
border-left: 4px solid var(--gold);
|
||||
padding: 12px 0 12px 14px;
|
||||
color: var(--text);
|
||||
font-size: clamp(1.2rem, 2.6vw, 1.7rem);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.answer[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 40px;
|
||||
padding: 0 8px;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.modal-actions button,
|
||||
footer button {
|
||||
min-height: 38px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
border-color: rgba(242, 201, 107, 0.62);
|
||||
background: linear-gradient(180deg, #f3d16e, #d89a37);
|
||||
color: #211609;
|
||||
}
|
||||
|
||||
button.good {
|
||||
border-color: rgba(136, 215, 111, 0.58);
|
||||
background: rgba(136, 215, 111, 0.14);
|
||||
}
|
||||
|
||||
button.bad {
|
||||
border-color: rgba(239, 115, 95, 0.58);
|
||||
background: rgba(239, 115, 95, 0.14);
|
||||
}
|
||||
|
||||
.tip {
|
||||
padding: 8px 10px;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scorebar,
|
||||
.footer-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.board {
|
||||
grid-template-columns: repeat(5, minmax(92px, 1fr));
|
||||
overflow-x: auto;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header>
|
||||
<div class="brand">
|
||||
<img class="brand-logo" src="assets/txproducts-logo-watermark.png" alt="">
|
||||
<div class="brand-text">
|
||||
<small>txproducts.de exhibition quiz</small>
|
||||
<h1>Synchrotron Challenge</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scorebar">
|
||||
<div class="score"><span>Score</span><strong id="score">0</strong></div>
|
||||
<div class="score"><span>Answered</span><strong id="answered">0 / 25</strong></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="watermark" aria-hidden="true"><img src="assets/txproducts-logo-watermark.png" alt=""></div>
|
||||
<section class="board" id="board" aria-label="Synchrotron quiz board"></section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="tip">Tap a value, discuss the clue, reveal the answer, then mark correct or missed.</div>
|
||||
<div class="footer-actions">
|
||||
<button id="resetButton" type="button">Reset board</button>
|
||||
<button type="button" onclick="window.location.href='index.html'">Back to menu</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="overlay" hidden>
|
||||
<article class="modal">
|
||||
<div class="modal-top">
|
||||
<div>
|
||||
<h2 id="modalCategory">Category</h2>
|
||||
<div id="modalDifficulty">Difficulty</div>
|
||||
</div>
|
||||
<div class="value" id="modalValue">100</div>
|
||||
</div>
|
||||
<p class="clue" id="modalClue"></p>
|
||||
<div class="answer" id="modalAnswer" hidden></div>
|
||||
<div class="modal-actions">
|
||||
<button class="primary" id="showAnswerButton" type="button">Show answer</button>
|
||||
<button class="good" id="correctButton" type="button" hidden>Correct</button>
|
||||
<button class="bad" id="missedButton" type="button" hidden>Missed</button>
|
||||
<button id="closeButton" type="button">Close</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const categories = [
|
||||
{
|
||||
title: "Synchrotron Basics",
|
||||
clues: [
|
||||
["A large accelerator facility that produces extremely bright x-rays for experiments.", "What is a synchrotron?"],
|
||||
["These charged particles are accelerated around the storage ring to generate synchrotron radiation.", "What are electrons?"],
|
||||
["Scientists use synchrotron x-rays because they are bright, tunable, and can probe this tiny scale.", "What is the atomic or nanoscale?"],
|
||||
["This property of synchrotron light lets researchers choose x-ray energies for specific materials or edges.", "What is tunability?"],
|
||||
["This repeating time pattern of x-ray bunches is what timing devices can reshape for experiments.", "What is pulse structure?"]
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "TXProducts",
|
||||
clues: [
|
||||
["This product tailors synchrotron pulse structure from nanoseconds to seconds.", "What is WaveGate?"],
|
||||
["This product cuts x-ray pulses to picosecond duration using photo-acoustic Bragg switching.", "What is PicoSwitch?"],
|
||||
["WaveGate controls when pulses are allowed through, shaping this aspect of the beam.", "What is temporal structure?"],
|
||||
["PicoSwitch uses a photo-acoustic effect together with this diffraction condition.", "What is Bragg diffraction?"],
|
||||
["Together, WaveGate and PicoSwitch help researchers control this dimension of x-ray experiments.", "What is time?"]
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "TXData",
|
||||
clues: [
|
||||
["A data service helps turn raw detector output into this useful form.", "What is actionable insight?"],
|
||||
["Good experimental data needs metadata, calibration, and this quality check.", "What is validation?"],
|
||||
["TXData can help researchers compare runs, spot trends, and reduce this costly problem.", "What is wasted beamtime or failed experiments?"],
|
||||
["When pulse timing, detector output, and sample conditions are linked, the result is better this.", "What is data context or traceability?"],
|
||||
["A business benefits when scientific data becomes searchable, reusable, and easier to share across this group.", "What is a team or organization?"]
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "TXServices",
|
||||
clues: [
|
||||
["TXServices can support planning, setup, and optimization so teams make better use of this scarce resource.", "What is beamtime?"],
|
||||
["A service partner can help translate an experimental question into this practical beamline plan.", "What is a measurement strategy?"],
|
||||
["Training and support reduce the gap between advanced instruments and this day-to-day user need.", "What is usability or confidence?"],
|
||||
["When equipment, data, and workflow are aligned, customers can move faster from experiment to this.", "What is decision or result?"],
|
||||
["For a customer, expert service lowers technical risk and protects this project investment.", "What is time, budget, or ROI?"]
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Business Value",
|
||||
clues: [
|
||||
["Better pulse control can improve experiment quality by reducing unwanted signal known as this.", "What is background or leakage?"],
|
||||
["Reliable timing tools help customers spend less time troubleshooting and more time getting these.", "What are results?"],
|
||||
["TXProducts, TXData, and TXServices together can connect hardware, analysis, and this human support layer.", "What is expertise or service?"],
|
||||
["A business improves when experiments become more repeatable, data becomes clearer, and decisions become this.", "What is faster or more confident?"],
|
||||
["For industrial or research customers, the combined offering can turn advanced synchrotron access into this advantage.", "What is competitive advantage?"]
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const values = [100, 200, 300, 400, 500];
|
||||
const difficulties = ["Warm-up", "Basic", "Applied", "Technical", "Expert"];
|
||||
const board = document.querySelector("#board");
|
||||
const scoreEl = document.querySelector("#score");
|
||||
const answeredEl = document.querySelector("#answered");
|
||||
const overlay = document.querySelector("#overlay");
|
||||
const modalCategory = document.querySelector("#modalCategory");
|
||||
const modalDifficulty = document.querySelector("#modalDifficulty");
|
||||
const modalValue = document.querySelector("#modalValue");
|
||||
const modalClue = document.querySelector("#modalClue");
|
||||
const modalAnswer = document.querySelector("#modalAnswer");
|
||||
const showAnswerButton = document.querySelector("#showAnswerButton");
|
||||
const correctButton = document.querySelector("#correctButton");
|
||||
const missedButton = document.querySelector("#missedButton");
|
||||
const closeButton = document.querySelector("#closeButton");
|
||||
const resetButton = document.querySelector("#resetButton");
|
||||
|
||||
const state = {
|
||||
score: 0,
|
||||
answered: 0,
|
||||
current: null,
|
||||
used: new Set()
|
||||
};
|
||||
|
||||
function renderBoard() {
|
||||
board.innerHTML = "";
|
||||
for (const category of categories) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "category";
|
||||
cell.textContent = category.title;
|
||||
board.appendChild(cell);
|
||||
}
|
||||
|
||||
for (let row = 0; row < values.length; row += 1) {
|
||||
for (let col = 0; col < categories.length; col += 1) {
|
||||
const key = `${col}-${row}`;
|
||||
const tile = document.createElement("button");
|
||||
tile.className = `tile${state.used.has(key) ? " used" : ""}`;
|
||||
tile.type = "button";
|
||||
tile.textContent = state.used.has(key) ? "..." : values[row];
|
||||
tile.disabled = state.used.has(key);
|
||||
tile.addEventListener("click", () => openClue(col, row));
|
||||
tile.addEventListener("touchend", (event) => {
|
||||
event.preventDefault();
|
||||
openClue(col, row);
|
||||
}, { passive: false });
|
||||
board.appendChild(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openClue(col, row) {
|
||||
const key = `${col}-${row}`;
|
||||
if (state.used.has(key)) return;
|
||||
const category = categories[col];
|
||||
const [clue, answer] = category.clues[row];
|
||||
state.current = { key, value: values[row], clue, answer, category: category.title, difficulty: difficulties[row] };
|
||||
modalCategory.textContent = category.title;
|
||||
modalDifficulty.textContent = difficulties[row];
|
||||
modalValue.textContent = values[row];
|
||||
modalClue.textContent = clue;
|
||||
modalAnswer.hidden = true;
|
||||
modalAnswer.textContent = answer;
|
||||
showAnswerButton.hidden = false;
|
||||
correctButton.hidden = true;
|
||||
missedButton.hidden = true;
|
||||
overlay.hidden = false;
|
||||
}
|
||||
|
||||
function showAnswer() {
|
||||
modalAnswer.hidden = false;
|
||||
showAnswerButton.hidden = true;
|
||||
correctButton.hidden = false;
|
||||
missedButton.hidden = false;
|
||||
}
|
||||
|
||||
function scoreCurrent(correct) {
|
||||
if (!state.current) return;
|
||||
state.used.add(state.current.key);
|
||||
state.answered += 1;
|
||||
if (correct) state.score += state.current.value;
|
||||
else state.score = Math.max(0, state.score - Math.floor(state.current.value / 2));
|
||||
closeModal();
|
||||
updateHud();
|
||||
renderBoard();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
overlay.hidden = true;
|
||||
state.current = null;
|
||||
}
|
||||
|
||||
function updateHud() {
|
||||
scoreEl.textContent = state.score;
|
||||
answeredEl.textContent = `${state.answered} / 25`;
|
||||
}
|
||||
|
||||
function resetBoard() {
|
||||
state.score = 0;
|
||||
state.answered = 0;
|
||||
state.current = null;
|
||||
state.used.clear();
|
||||
updateHud();
|
||||
renderBoard();
|
||||
overlay.hidden = true;
|
||||
}
|
||||
|
||||
showAnswerButton.addEventListener("click", showAnswer);
|
||||
correctButton.addEventListener("click", () => scoreCurrent(true));
|
||||
missedButton.addEventListener("click", () => scoreCurrent(false));
|
||||
closeButton.addEventListener("click", closeModal);
|
||||
resetButton.addEventListener("click", resetBoard);
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") closeModal();
|
||||
if (event.key.toLowerCase() === "r") resetBoard();
|
||||
});
|
||||
|
||||
renderBoard();
|
||||
updateHud();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,914 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>WaveGate Pulse Tailor</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--panel: rgba(12, 18, 27, 0.9);
|
||||
--line: rgba(255, 255, 255, 0.16);
|
||||
--text: #f7f3ea;
|
||||
--muted: #adb7c6;
|
||||
--cyan: #65d8e8;
|
||||
--gold: #f2c96b;
|
||||
--bad: #ef735f;
|
||||
--good: #88d76f;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background:
|
||||
radial-gradient(circle at 16% 12%, rgba(101, 216, 232, 0.24), transparent 28%),
|
||||
radial-gradient(circle at 84% 72%, rgba(242, 201, 107, 0.11), transparent 30%),
|
||||
linear-gradient(140deg, #101926 0%, #1b242d 48%, #090c10 100%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
height: 100%;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand { display: grid; gap: 2px; }
|
||||
|
||||
.brand small {
|
||||
color: var(--cyan);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.35rem, 2.4vw, 2.15rem);
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat, .tip, button {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.stat {
|
||||
min-width: 98px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.stat span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 8px;
|
||||
background: #101821;
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.34;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.watermark img {
|
||||
width: min(62vmin, 520px);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 10px;
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
transition: opacity 170ms ease, transform 170ms ease;
|
||||
}
|
||||
|
||||
.overlay.hidden {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: min(840px, calc(100vw - 20px));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 22px;
|
||||
background: rgba(8, 12, 18, 0.9);
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.55rem;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.modal p {
|
||||
margin: 0 0 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.how-it-works {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 0 0 18px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.how-it-works[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.how-it-works li {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 1fr;
|
||||
gap: 9px;
|
||||
align-items: start;
|
||||
color: var(--muted);
|
||||
line-height: 1.38;
|
||||
}
|
||||
|
||||
.how-it-works b {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid rgba(101, 216, 232, 0.45);
|
||||
border-radius: 50%;
|
||||
color: var(--cyan);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.result-report {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin: 8px 0 22px;
|
||||
}
|
||||
|
||||
.result-report[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.report-item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 20px 16px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.report-item strong {
|
||||
display: block;
|
||||
margin-bottom: 9px;
|
||||
color: var(--text);
|
||||
font-size: clamp(3.3rem, 8vw, 6rem);
|
||||
line-height: 0.95;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.report-item span {
|
||||
display: block;
|
||||
color: var(--gold);
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.report-item p {
|
||||
display: block;
|
||||
margin: 12px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1rem, 2.2vw, 1.18rem);
|
||||
line-height: 1.38;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions, .controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 42px;
|
||||
border-radius: 8px;
|
||||
padding: 0 15px;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
border-color: rgba(242, 201, 107, 0.6);
|
||||
background: linear-gradient(180deg, #f3d16e, #d89a37);
|
||||
color: #211609;
|
||||
}
|
||||
|
||||
.tip {
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.controls { justify-content: flex-end; }
|
||||
|
||||
.touch-gate {
|
||||
min-height: 58px;
|
||||
border-color: rgba(101, 216, 232, 0.52);
|
||||
background: linear-gradient(180deg, rgba(101, 216, 232, 0.2), rgba(101, 216, 232, 0.08));
|
||||
color: var(--text);
|
||||
touch-action: none;
|
||||
min-width: 230px;
|
||||
}
|
||||
|
||||
.touch-gate.is-open,
|
||||
.touch-gate:active {
|
||||
border-color: rgba(242, 201, 107, 0.78);
|
||||
background: linear-gradient(180deg, #f3d16e, #d89a37);
|
||||
color: #211609;
|
||||
}
|
||||
|
||||
.floating-gate {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
z-index: 3;
|
||||
width: min(430px, calc(100% - 28px));
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.floating-gate.is-open,
|
||||
.floating-gate:active {
|
||||
transform: translateX(-50%) translateY(1px);
|
||||
}
|
||||
|
||||
kbd {
|
||||
min-width: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-bottom-color: rgba(255, 255, 255, 0.34);
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
body { overflow: auto; }
|
||||
.shell {
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
grid-template-rows: auto minmax(560px, 1fr) auto;
|
||||
padding: 10px;
|
||||
}
|
||||
header, footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
.stats, .controls { justify-content: flex-start; }
|
||||
.stat { flex: 1 1 98px; }
|
||||
.result-report { grid-template-columns: 1fr; }
|
||||
.touch-gate {
|
||||
width: 100%;
|
||||
min-height: 72px;
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header>
|
||||
<div class="brand">
|
||||
<small>txproducts.de presents</small>
|
||||
<h1>WaveGate Pulse Tailor</h1>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||
<div class="stat"><span>Timescale</span><strong id="scale">ns</strong></div>
|
||||
<div class="stat"><span>Contrast</span><strong id="contrast">100%</strong></div>
|
||||
<div class="stat"><span>Round</span><strong id="round">1 / 8</strong></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<canvas id="game" aria-label="WaveGate temporal synchrotron pulse tailoring game"></canvas>
|
||||
<div class="watermark" aria-hidden="true"><img src="assets/txproducts-logo-watermark.png" alt=""></div>
|
||||
<button class="touch-gate floating-gate" id="gateButton" type="button">Press + hold to open WaveGate</button>
|
||||
<div class="overlay" id="overlay">
|
||||
<section class="modal">
|
||||
<h2 id="modalTitle">Are you ready?</h2>
|
||||
<p id="modalText">You are about to control WaveGate: decide exactly when synchrotron pulses are allowed through.</p>
|
||||
<ul class="how-it-works" id="howItWorks">
|
||||
<li><b>1</b><span>Watch pulses move along the timeline.</span></li>
|
||||
<li><b>2</b><span>The gold window is the structure to create.</span></li>
|
||||
<li><b>3</b><span>Hold Space or press the screen to open WaveGate.</span></li>
|
||||
<li><b>4</b><span>Let gold pulses through. Block the rest.</span></li>
|
||||
</ul>
|
||||
<div class="result-report" id="resultReport" hidden>
|
||||
<div class="report-item">
|
||||
<strong id="tailoredCount">0</strong>
|
||||
<span>Tailored</span>
|
||||
<p>Excellent capture of the target window with almost no leakage.</p>
|
||||
</div>
|
||||
<div class="report-item">
|
||||
<strong id="shapedCount">0</strong>
|
||||
<span>Shaped</span>
|
||||
<p>Useful pulse structure, but some target pulses or blockers were missed.</p>
|
||||
</div>
|
||||
<div class="report-item">
|
||||
<strong id="leakageCount">0</strong>
|
||||
<span>Leakage</span>
|
||||
<p>Too many unwanted pulses passed through the gate.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" id="startButton" type="button" onclick="startGame(false)" ontouchstart="event.preventDefault(); startGame(false);">I'm ready</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="tip" id="message">Transmit the highlighted pulse window. Block everything else.</div>
|
||||
<div class="controls">
|
||||
<span><kbd>Hold Space</kbd> open gate</span>
|
||||
<span><kbd>Press</kbd> open gate</span>
|
||||
<span><kbd>R</kbd> restart</span>
|
||||
<span><kbd>Esc</kbd> stop</span>
|
||||
<button id="stopButton" type="button">Stop demo</button>
|
||||
<button type="button" onclick="window.location.href='index.html'">Back to menu</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.querySelector("#game");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const scoreEl = document.querySelector("#score");
|
||||
const scaleEl = document.querySelector("#scale");
|
||||
const contrastEl = document.querySelector("#contrast");
|
||||
const roundEl = document.querySelector("#round");
|
||||
const messageEl = document.querySelector("#message");
|
||||
const overlay = document.querySelector("#overlay");
|
||||
const modalTitle = document.querySelector("#modalTitle");
|
||||
const modalText = document.querySelector("#modalText");
|
||||
const howItWorks = document.querySelector("#howItWorks");
|
||||
const resultReport = document.querySelector("#resultReport");
|
||||
const tailoredCount = document.querySelector("#tailoredCount");
|
||||
const shapedCount = document.querySelector("#shapedCount");
|
||||
const leakageCount = document.querySelector("#leakageCount");
|
||||
const startButton = document.querySelector("#startButton");
|
||||
const stopButton = document.querySelector("#stopButton");
|
||||
const gateButton = document.querySelector("#gateButton");
|
||||
|
||||
const scales = ["300 ns", "800 ns", "2 us", "20 us", "1 ms", "100 ms", "1 s", "seconds"];
|
||||
const state = {
|
||||
mode: "menu",
|
||||
score: 0,
|
||||
contrast: 100,
|
||||
round: 1,
|
||||
totalRounds: 8,
|
||||
practice: false,
|
||||
gateOpen: false,
|
||||
time: 0,
|
||||
duration: 5.2,
|
||||
lastTime: 0,
|
||||
pulses: [],
|
||||
transmitted: [],
|
||||
report: { tailored: 0, shaped: 0, leakage: 0 },
|
||||
result: null
|
||||
};
|
||||
|
||||
function fitCanvas() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scale = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.max(1, Math.floor(rect.width * scale));
|
||||
canvas.height = Math.max(1, Math.floor(rect.height * scale));
|
||||
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
||||
}
|
||||
|
||||
function css(name) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
|
||||
function startGame(practice = false) {
|
||||
state.mode = "running";
|
||||
state.practice = practice;
|
||||
state.score = 0;
|
||||
state.contrast = 100;
|
||||
state.round = 1;
|
||||
state.report = { tailored: 0, shaped: 0, leakage: 0 };
|
||||
overlay.classList.add("hidden");
|
||||
resultReport.hidden = true;
|
||||
gateButton.hidden = false;
|
||||
nextRound();
|
||||
}
|
||||
|
||||
function showIntro() {
|
||||
state.mode = "menu";
|
||||
state.gateOpen = false;
|
||||
gateButton.classList.remove("is-open");
|
||||
gateButton.hidden = true;
|
||||
modalTitle.textContent = "Are you ready?";
|
||||
modalText.textContent = "You are about to control WaveGate: decide exactly when synchrotron pulses are allowed through.";
|
||||
howItWorks.hidden = false;
|
||||
resultReport.hidden = true;
|
||||
startButton.textContent = "I'm ready";
|
||||
overlay.classList.remove("hidden");
|
||||
messageEl.textContent = "Transmit the highlighted pulse window. Block everything else.";
|
||||
updateHud();
|
||||
}
|
||||
|
||||
function nextRound() {
|
||||
state.mode = "running";
|
||||
state.time = 0;
|
||||
state.duration = Math.max(3.4, 5.3 - state.round * 0.18);
|
||||
state.gateOpen = false;
|
||||
gateButton.classList.remove("is-open");
|
||||
gateButton.hidden = false;
|
||||
state.transmitted = [];
|
||||
state.result = null;
|
||||
buildPulses();
|
||||
messageEl.textContent = state.round < 5
|
||||
? "Fast structure: catch only the highlighted burst window."
|
||||
: "Long structure: hold the gate through the requested macro window.";
|
||||
updateHud();
|
||||
}
|
||||
|
||||
function buildPulses() {
|
||||
const count = 18 + state.round * 2;
|
||||
const start = 0.45;
|
||||
const end = state.duration - 0.35;
|
||||
const step = (end - start) / count;
|
||||
const windowWidth = state.round < 5 ? step * (2.3 + state.round * 0.12) : step * (4.2 + state.round * 0.45);
|
||||
const windowStart = start + step * (4 + Math.floor(Math.random() * Math.max(1, count - 9)));
|
||||
const windowEnd = Math.min(end - step, windowStart + windowWidth);
|
||||
state.targetStart = windowStart;
|
||||
state.targetEnd = windowEnd;
|
||||
state.pulses = [];
|
||||
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const t = start + i * step;
|
||||
state.pulses.push({
|
||||
t,
|
||||
strength: 0.72 + Math.random() * 0.28,
|
||||
desired: t >= windowStart && t <= windowEnd,
|
||||
judged: false,
|
||||
passed: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateHud() {
|
||||
scoreEl.textContent = state.score;
|
||||
scaleEl.textContent = scales[Math.min(scales.length - 1, state.round - 1)];
|
||||
contrastEl.textContent = `${Math.max(0, Math.round(state.contrast))}%`;
|
||||
roundEl.textContent = state.practice ? `${state.round}` : `${state.round} / ${state.totalRounds}`;
|
||||
}
|
||||
|
||||
function setGate(open) {
|
||||
if (state.mode !== "running") {
|
||||
state.gateOpen = false;
|
||||
gateButton.classList.remove("is-open");
|
||||
return;
|
||||
}
|
||||
state.gateOpen = open;
|
||||
gateButton.classList.toggle("is-open", open);
|
||||
}
|
||||
|
||||
function scorePulse(pulse) {
|
||||
pulse.judged = true;
|
||||
pulse.passed = state.gateOpen;
|
||||
const correct = pulse.desired === pulse.passed;
|
||||
if (correct) {
|
||||
state.score += pulse.desired ? 95 : 22;
|
||||
} else {
|
||||
state.contrast -= pulse.desired ? 12 : 8;
|
||||
}
|
||||
if (pulse.passed) {
|
||||
state.transmitted.push({ t: pulse.t, desired: pulse.desired, age: 0 });
|
||||
}
|
||||
updateHud();
|
||||
}
|
||||
|
||||
function finishRound() {
|
||||
if (state.mode !== "running") return;
|
||||
const desired = state.pulses.filter((p) => p.desired);
|
||||
const passedDesired = desired.filter((p) => p.passed).length;
|
||||
const leaks = state.pulses.filter((p) => !p.desired && p.passed).length;
|
||||
const capture = passedDesired / Math.max(1, desired.length);
|
||||
const purity = 1 - leaks / Math.max(1, state.pulses.length - desired.length);
|
||||
const quality = Math.max(0, capture * 0.62 + purity * 0.38);
|
||||
|
||||
if (quality > 0.92) {
|
||||
state.report.tailored += 1;
|
||||
state.result = { text: "TAILORED", color: css("--gold"), age: 0 };
|
||||
state.score += 350;
|
||||
messageEl.textContent = `Temporal structure tailored on the ${scaleEl.textContent} scale.`;
|
||||
} else if (quality > 0.68) {
|
||||
state.report.shaped += 1;
|
||||
state.result = { text: "SHAPED", color: css("--good"), age: 0 };
|
||||
state.score += 150;
|
||||
messageEl.textContent = "Useful pulse structure, with some leakage.";
|
||||
} else {
|
||||
state.report.leakage += 1;
|
||||
state.result = { text: "LEAKAGE", color: css("--bad"), age: 0 };
|
||||
messageEl.textContent = "Too much unwanted pulse transmission.";
|
||||
}
|
||||
|
||||
state.contrast = Math.max(0, state.contrast * (0.76 + quality * 0.24));
|
||||
if (state.report.leakage >= 3) {
|
||||
failRun("Leakage threshold reached", "Three leakage rounds means too many unwanted pulses passed through the gate.");
|
||||
return;
|
||||
}
|
||||
state.mode = "result";
|
||||
state.gateOpen = false;
|
||||
gateButton.classList.remove("is-open");
|
||||
gateButton.hidden = true;
|
||||
updateHud();
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (state.mode !== "result") return;
|
||||
if (!state.practice && state.round >= state.totalRounds) {
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
state.round += 1;
|
||||
nextRound();
|
||||
}, 900);
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.mode = "over";
|
||||
state.gateOpen = false;
|
||||
gateButton.classList.remove("is-open");
|
||||
gateButton.hidden = true;
|
||||
modalTitle.textContent = "Pulse program complete";
|
||||
modalText.textContent = `Final score: ${state.score}. Contrast: ${Math.round(state.contrast)}%.`;
|
||||
howItWorks.hidden = true;
|
||||
showReport();
|
||||
startButton.textContent = "Run again";
|
||||
overlay.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function failRun(title, text) {
|
||||
state.mode = "over";
|
||||
state.gateOpen = false;
|
||||
gateButton.classList.remove("is-open");
|
||||
gateButton.hidden = true;
|
||||
state.result = { text: "CONTRAST LOST", color: css("--bad"), age: 0 };
|
||||
modalTitle.textContent = title;
|
||||
modalText.textContent = `${text} Final score: ${state.score}.`;
|
||||
howItWorks.hidden = true;
|
||||
showReport();
|
||||
startButton.textContent = "Try again";
|
||||
overlay.classList.remove("hidden");
|
||||
messageEl.textContent = "Run stopped: contrast dropped below the threshold.";
|
||||
updateHud();
|
||||
}
|
||||
|
||||
function stopGame() {
|
||||
state.mode = "over";
|
||||
state.gateOpen = false;
|
||||
gateButton.classList.remove("is-open");
|
||||
gateButton.hidden = true;
|
||||
modalTitle.textContent = "Demo paused";
|
||||
modalText.textContent = "Take a breath. When you are ready, open WaveGate only during the gold time window and block everything else.";
|
||||
howItWorks.hidden = false;
|
||||
resultReport.hidden = true;
|
||||
startButton.textContent = "I'm ready";
|
||||
overlay.classList.remove("hidden");
|
||||
messageEl.textContent = "Stopped. Ready for the next booth visitor.";
|
||||
}
|
||||
|
||||
function showReport() {
|
||||
tailoredCount.textContent = state.report.tailored;
|
||||
shapedCount.textContent = state.report.shaped;
|
||||
leakageCount.textContent = state.report.leakage;
|
||||
resultReport.hidden = false;
|
||||
}
|
||||
|
||||
function restart() {
|
||||
startGame(state.practice);
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
if (state.mode === "running") {
|
||||
state.time += dt;
|
||||
for (const pulse of state.pulses) {
|
||||
if (!pulse.judged && state.time >= pulse.t) scorePulse(pulse);
|
||||
if (state.mode !== "running") break;
|
||||
}
|
||||
if (state.time >= state.duration) finishRound();
|
||||
}
|
||||
for (const pulse of state.transmitted) pulse.age += dt;
|
||||
if (state.result) state.result.age += dt;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const w = canvas.clientWidth;
|
||||
const h = canvas.clientHeight;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
drawBackground(w, h);
|
||||
drawTimeline(w, h);
|
||||
drawTargetWindow(w, h);
|
||||
drawGate(w, h);
|
||||
drawPulses(w, h);
|
||||
drawOutput(w, h);
|
||||
drawResult(w, h);
|
||||
}
|
||||
|
||||
function drawBackground(w, h) {
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, h);
|
||||
grad.addColorStop(0, "#202a35");
|
||||
grad.addColorStop(0.62, "#141d27");
|
||||
grad.addColorStop(1, "#080b0d");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.16;
|
||||
ctx.strokeStyle = "#e9f7ff";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = -20; x < w + 80; x += 42) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x + 70, h);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 70; y < h; y += 74) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function timeToX(t, w) {
|
||||
return 52 + (t / state.duration) * (w - 104);
|
||||
}
|
||||
|
||||
function drawTimeline(w, h) {
|
||||
const y = h * 0.48;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(101, 216, 232, 0.32)";
|
||||
ctx.lineWidth = 5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(36, y);
|
||||
ctx.lineTo(w - 36, y);
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(101, 216, 232, 0.08)";
|
||||
ctx.lineWidth = 24;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(36, y);
|
||||
ctx.lineTo(w - 36, y);
|
||||
ctx.stroke();
|
||||
|
||||
const nowX = timeToX(state.time, w);
|
||||
ctx.strokeStyle = css("--gold");
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([7, 8]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(nowX, y - 155);
|
||||
ctx.lineTo(nowX, y + 155);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawTargetWindow(w, h) {
|
||||
const y = h * 0.48;
|
||||
const x1 = timeToX(state.targetStart || 0, w);
|
||||
const x2 = timeToX(state.targetEnd || 0, w);
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgba(242, 201, 107, 0.16)";
|
||||
ctx.strokeStyle = "rgba(242, 201, 107, 0.76)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x1, y - 92, x2 - x1, 184, 8);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = css("--gold");
|
||||
ctx.font = "900 12px Inter, system-ui, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("TRANSMIT WINDOW", (x1 + x2) / 2, y - 108);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawGate(w, h) {
|
||||
const y = h * 0.48;
|
||||
const x = timeToX(state.time, w);
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.fillStyle = state.gateOpen ? "rgba(101, 216, 232, 0.24)" : "rgba(0, 0, 0, 0.55)";
|
||||
ctx.strokeStyle = state.gateOpen ? css("--cyan") : "rgba(255, 255, 255, 0.32)";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(-34, -74, 68, 148, 10);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = state.gateOpen ? css("--cyan") : "rgba(255,255,255,0.25)";
|
||||
ctx.fillRect(-7, -82, 14, 164);
|
||||
ctx.font = "900 12px Inter, system-ui, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(state.gateOpen ? "OPEN" : "CLOSED", 0, 100);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawPulses(w, h) {
|
||||
const y = h * 0.48;
|
||||
ctx.save();
|
||||
for (const pulse of state.pulses) {
|
||||
const x = timeToX(pulse.t, w);
|
||||
if (x < 24 || x > w - 24) continue;
|
||||
ctx.globalAlpha = pulse.judged ? 0.32 : 0.95;
|
||||
ctx.fillStyle = pulse.desired ? css("--gold") : css("--cyan");
|
||||
ctx.shadowColor = ctx.fillStyle;
|
||||
ctx.shadowBlur = pulse.desired ? 13 : 8;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, pulse.desired ? 8 : 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawOutput(w, h) {
|
||||
const outY = h * 0.73;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.18)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(36, outY);
|
||||
ctx.lineTo(w - 36, outY);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.65)";
|
||||
ctx.font = "900 12px Inter, system-ui, sans-serif";
|
||||
ctx.fillText("OUTPUT STRUCTURE", 42, outY - 16);
|
||||
|
||||
for (const pulse of state.transmitted) {
|
||||
const x = timeToX(pulse.t, w);
|
||||
ctx.globalAlpha = pulse.desired ? 0.95 : 0.55;
|
||||
ctx.fillStyle = pulse.desired ? css("--good") : css("--bad");
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x - 5, outY - 26, 10, 52, 4);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawResult(w, h) {
|
||||
if (!state.result) return;
|
||||
const age = state.result.age;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = Math.max(0, 1 - age * 0.55);
|
||||
ctx.fillStyle = state.result.color;
|
||||
ctx.font = "900 30px Inter, system-ui, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(state.result.text, w / 2, Math.max(52, h * 0.26 - age * 24));
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function loop(time) {
|
||||
const dt = Math.min(0.033, (time - state.lastTime) / 1000 || 0);
|
||||
state.lastTime = time;
|
||||
update(dt);
|
||||
draw();
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function onTap(element, handler) {
|
||||
let handledAt = 0;
|
||||
const run = (event) => {
|
||||
const now = Date.now();
|
||||
if (now - handledAt < 450) return;
|
||||
handledAt = now;
|
||||
event.preventDefault();
|
||||
handler();
|
||||
};
|
||||
element.addEventListener("click", run);
|
||||
element.addEventListener("touchend", run, { passive: false });
|
||||
}
|
||||
|
||||
window.addEventListener("resize", fitCanvas);
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.code === "Space") {
|
||||
event.preventDefault();
|
||||
setGate(true);
|
||||
}
|
||||
if (event.key.toLowerCase() === "r") restart();
|
||||
if (event.key === "Escape") stopGame();
|
||||
});
|
||||
window.addEventListener("keyup", (event) => {
|
||||
if (event.code === "Space") setGate(false);
|
||||
});
|
||||
window.addEventListener("blur", () => setGate(false));
|
||||
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
setGate(true);
|
||||
});
|
||||
canvas.addEventListener("pointerup", () => setGate(false));
|
||||
canvas.addEventListener("pointerleave", () => setGate(false));
|
||||
canvas.addEventListener("pointercancel", () => setGate(false));
|
||||
gateButton.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
gateButton.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
gateButton.setPointerCapture(event.pointerId);
|
||||
setGate(true);
|
||||
});
|
||||
gateButton.addEventListener("pointerup", (event) => {
|
||||
if (gateButton.hasPointerCapture(event.pointerId)) {
|
||||
gateButton.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
setGate(false);
|
||||
});
|
||||
gateButton.addEventListener("pointerleave", () => setGate(false));
|
||||
gateButton.addEventListener("pointercancel", () => setGate(false));
|
||||
gateButton.addEventListener("lostpointercapture", () => setGate(false));
|
||||
onTap(startButton, () => startGame(false));
|
||||
onTap(stopButton, stopGame);
|
||||
|
||||
fitCanvas();
|
||||
showIntro();
|
||||
requestAnimationFrame(loop);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,657 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>Wheel of Photons</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--panel: rgba(10, 16, 25, 0.92);
|
||||
--line: rgba(255, 255, 255, 0.16);
|
||||
--text: #f8f3ea;
|
||||
--muted: #aeb8c8;
|
||||
--cyan: #65d8e8;
|
||||
--gold: #f2c96b;
|
||||
--green: #88d76f;
|
||||
--red: #ef735f;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background:
|
||||
radial-gradient(circle at 16% 12%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 86% 80%, rgba(242, 201, 107, 0.12), transparent 32%),
|
||||
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: clamp(48px, 8vw, 72px);
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.brand small {
|
||||
color: var(--cyan);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.45rem, 3.5vw, 2.5rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat,
|
||||
.tip,
|
||||
button,
|
||||
.panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.stat {
|
||||
min-width: 108px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.stat span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 1.1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 0.82fr) minmax(340px, 1.18fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.wheel-panel {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 420px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wheel-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 12%;
|
||||
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||
opacity: 0.1;
|
||||
transform: rotate(-18deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wheel {
|
||||
position: relative;
|
||||
width: min(72vmin, 420px);
|
||||
aspect-ratio: 1;
|
||||
border: 10px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 50%;
|
||||
background:
|
||||
conic-gradient(
|
||||
from 0deg,
|
||||
#f2c96b 0 30deg,
|
||||
#65d8e8 30deg 60deg,
|
||||
#88d76f 60deg 90deg,
|
||||
#293544 90deg 120deg,
|
||||
#f2c96b 120deg 150deg,
|
||||
#ef735f 150deg 180deg,
|
||||
#65d8e8 180deg 210deg,
|
||||
#88d76f 210deg 240deg,
|
||||
#293544 240deg 270deg,
|
||||
#f2c96b 270deg 300deg,
|
||||
#65d8e8 300deg 330deg,
|
||||
#ef735f 330deg 360deg
|
||||
);
|
||||
box-shadow: 0 26px 80px rgba(0, 0, 0, 0.45), inset 0 0 60px rgba(0, 0, 0, 0.42);
|
||||
transition: transform 1100ms cubic-bezier(0.16, 0.82, 0.28, 1);
|
||||
}
|
||||
|
||||
.wheel::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 28%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.24);
|
||||
border-radius: 50%;
|
||||
background: rgba(6, 10, 15, 0.78);
|
||||
}
|
||||
|
||||
.pointer {
|
||||
position: absolute;
|
||||
top: calc(50% - min(36vmin, 210px) - 16px);
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: 3;
|
||||
transform: translateX(-50%);
|
||||
border-left: 16px solid transparent;
|
||||
border-right: 16px solid transparent;
|
||||
border-top: 26px solid var(--text);
|
||||
filter: drop-shadow(0 5px 8px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
.spin-readout {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 45%;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 50%;
|
||||
background: rgba(8, 13, 20, 0.86);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spin-readout span {
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.spin-readout strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--gold);
|
||||
font-size: clamp(1.7rem, 5vw, 3rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.game-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.category {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
border: 1px solid rgba(101, 216, 232, 0.34);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
color: var(--cyan);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.phrase {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-content: center;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.word {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: clamp(30px, 5vw, 52px);
|
||||
height: clamp(38px, 6vw, 64px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.24);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text);
|
||||
font-size: clamp(1.15rem, 3vw, 2rem);
|
||||
font-weight: 1000;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tile.space {
|
||||
width: 18px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(13, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.difficulty {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(72px, 100px));
|
||||
gap: 5px;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 40px;
|
||||
padding: 0 8px;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
border-color: rgba(242, 201, 107, 0.62);
|
||||
background: linear-gradient(180deg, #f3d16e, #d89a37);
|
||||
color: #211609;
|
||||
}
|
||||
|
||||
button.active-level {
|
||||
color: #071014;
|
||||
background: var(--green);
|
||||
border-color: rgba(136, 215, 111, 0.85);
|
||||
}
|
||||
|
||||
.letter.used {
|
||||
opacity: 0.32;
|
||||
}
|
||||
|
||||
.letter.good {
|
||||
border-color: rgba(136, 215, 111, 0.62);
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.letter.bad {
|
||||
border-color: rgba(239, 115, 95, 0.62);
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.actions,
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.difficulty button {
|
||||
min-height: 28px;
|
||||
padding: 0 6px;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
min-height: 38px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.keyboard .letter {
|
||||
min-height: 48px;
|
||||
font-size: clamp(1rem, 1.8vw, 1.24rem);
|
||||
}
|
||||
|
||||
.explain {
|
||||
min-height: 54px;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1rem, 1.7vw, 1.15rem);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.tip {
|
||||
padding: 8px 10px;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wheel-panel {
|
||||
min-height: 330px;
|
||||
}
|
||||
|
||||
.stats,
|
||||
.footer-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header>
|
||||
<div class="brand">
|
||||
<img src="assets/txproducts-logo-watermark.png" alt="">
|
||||
<div>
|
||||
<small>txproducts.de photon puzzle</small>
|
||||
<h1>Wheel of Photons</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||
<div class="stat"><span>Beamtime</span><strong id="beamtime">6</strong></div>
|
||||
<div class="stat"><span>Puzzle</span><strong id="round">1 / 6</strong></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="panel wheel-panel">
|
||||
<div class="pointer"></div>
|
||||
<div class="wheel" id="wheel"></div>
|
||||
<div class="spin-readout">
|
||||
<div><span>Spin value</span><strong id="spinValue">Spin</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel game-panel">
|
||||
<div class="category" id="category">Category</div>
|
||||
<div class="difficulty" aria-label="Difficulty">
|
||||
<button class="active-level" data-level="Easy" type="button">Easy</button>
|
||||
<button data-level="Medium" type="button">Medium</button>
|
||||
<button data-level="Hard" type="button">Hard</button>
|
||||
</div>
|
||||
<div class="phrase" id="phrase" aria-label="Hidden phrase"></div>
|
||||
<div class="keyboard" id="keyboard" aria-label="Letter keyboard"></div>
|
||||
<div class="actions">
|
||||
<button class="primary" id="spinButton" type="button">Spin</button>
|
||||
<button id="solveButton" type="button">Solve / Reveal</button>
|
||||
<button id="nextButton" type="button">Next phrase</button>
|
||||
</div>
|
||||
<div class="explain" id="explain">Spin, pick letters, then use Next phrase for another puzzle.</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="tip">Passerby mode: one spin, one letter, quick reveal. Good for drawing people into the booth.</div>
|
||||
<div class="footer-actions">
|
||||
<button id="resetButton" type="button">Reset</button>
|
||||
<button type="button" onclick="window.location.href='index.html'">Back to menu</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const puzzleSets = {
|
||||
Easy: [
|
||||
{ category: "Easy Product", phrase: "WAVEGATE", explain: "WaveGate tailors synchrotron pulse timing." },
|
||||
{ category: "Easy Product", phrase: "PICOSWITCH", explain: "PicoSwitch shortens x-ray pulses." },
|
||||
{ category: "Easy Data", phrase: "TXDATA", explain: "TXData turns measurements into insight." },
|
||||
{ category: "Easy Service", phrase: "TXSERVICES", explain: "TXServices supports better experiments." },
|
||||
{ category: "Easy Physics", phrase: "XRAY PULSE", explain: "X-ray pulses are the timing target." },
|
||||
{ category: "Easy Business", phrase: "BETTER BEAMTIME", explain: "Good support helps teams get more from beamtime." }
|
||||
],
|
||||
Medium: [
|
||||
{ category: "Product", phrase: "WAVEGATE", explain: "WaveGate tailors synchrotron pulse structure from nanoseconds to seconds." },
|
||||
{ category: "Product", phrase: "PICOSWITCH", explain: "PicoSwitch shortens x-ray pulses into the 5-10 ps range." },
|
||||
{ category: "Physics", phrase: "BRAGG DIFFRACTION", explain: "Bragg diffraction links x-ray scattering to ordered crystal structure." },
|
||||
{ category: "Timing", phrase: "TEMPORAL PULSE STRUCTURE", explain: "Timing structure describes when pulses arrive and which pulses are transmitted." },
|
||||
{ category: "Beamline", phrase: "SYNCHROTRON RADIATION", explain: "Synchrotrons produce bright, tunable radiation for advanced experiments." },
|
||||
{ category: "Experiment", phrase: "COHERENT LATTICE DYNAMICS", explain: "Short x-ray pulses help observe fast collective motion in crystals." },
|
||||
{ category: "Business", phrase: "ACTIONABLE INSIGHT", explain: "TXData and TXServices can help convert complex measurements into useful decisions." },
|
||||
{ category: "Service", phrase: "BETTER BEAMTIME", explain: "Good products, data, and support help teams get more value from scarce beamtime." }
|
||||
],
|
||||
Hard: [
|
||||
{ category: "Hard Product", phrase: "PHOTO ACOUSTIC BRAGG SWITCH", explain: "PicoSwitch uses photo-acoustic Bragg switching to shorten x-ray pulses." },
|
||||
{ category: "Hard Timing", phrase: "NANOSECONDS TO SECONDS", explain: "WaveGate covers broad temporal pulse tailoring." },
|
||||
{ category: "Hard Experiment", phrase: "COHERENT LATTICE DYNAMICS", explain: "Short pulses can reveal ultrafast motion inside materials." },
|
||||
{ category: "Hard Business", phrase: "MEASUREMENTS INTO INSIGHT", explain: "TXData and TXServices help convert complex measurements into decisions." },
|
||||
{ category: "Hard Physics", phrase: "DIFFRACTION EFFICIENCY", explain: "Diffraction efficiency matters when choosing repetition rates and switching conditions." },
|
||||
{ category: "Hard Beamline", phrase: "TEMPORAL SYNCHROTRON PULSE STRUCTURE", explain: "Temporal structure controls which pulses reach the experiment." }
|
||||
]
|
||||
};
|
||||
let puzzles = puzzleSets.Easy;
|
||||
|
||||
const wheelValues = [100, 150, 200, 250, 300, 350, 400, 500, 600, 750, "BANKRUPT", "PASS"];
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
const state = {
|
||||
index: 0,
|
||||
level: "Easy",
|
||||
score: 0,
|
||||
beamtime: 6,
|
||||
currentValue: 0,
|
||||
rotation: 0,
|
||||
guessed: new Map(),
|
||||
solved: false
|
||||
};
|
||||
|
||||
const scoreEl = document.querySelector("#score");
|
||||
const beamtimeEl = document.querySelector("#beamtime");
|
||||
const roundEl = document.querySelector("#round");
|
||||
const spinValueEl = document.querySelector("#spinValue");
|
||||
const wheel = document.querySelector("#wheel");
|
||||
const categoryEl = document.querySelector("#category");
|
||||
const phraseEl = document.querySelector("#phrase");
|
||||
const keyboardEl = document.querySelector("#keyboard");
|
||||
const explainEl = document.querySelector("#explain");
|
||||
const spinButton = document.querySelector("#spinButton");
|
||||
const solveButton = document.querySelector("#solveButton");
|
||||
const nextButton = document.querySelector("#nextButton");
|
||||
const resetButton = document.querySelector("#resetButton");
|
||||
const levelButtons = document.querySelectorAll("[data-level]");
|
||||
|
||||
function render() {
|
||||
const puzzle = puzzles[state.index];
|
||||
categoryEl.textContent = `${state.level} / ${puzzle.category}`;
|
||||
scoreEl.textContent = state.score;
|
||||
beamtimeEl.textContent = state.beamtime;
|
||||
roundEl.textContent = `${state.index + 1} / ${puzzles.length}`;
|
||||
spinValueEl.textContent = state.currentValue || "Spin";
|
||||
renderPhrase(puzzle.phrase);
|
||||
renderKeyboard(puzzle.phrase);
|
||||
}
|
||||
|
||||
function renderPhrase(phrase) {
|
||||
phraseEl.innerHTML = "";
|
||||
for (const word of phrase.split(" ")) {
|
||||
const wordEl = document.createElement("span");
|
||||
wordEl.className = "word";
|
||||
for (const char of word) {
|
||||
const tile = document.createElement("span");
|
||||
tile.className = "tile";
|
||||
tile.textContent = state.solved || state.guessed.get(char) ? char : "";
|
||||
wordEl.appendChild(tile);
|
||||
}
|
||||
phraseEl.appendChild(wordEl);
|
||||
}
|
||||
}
|
||||
|
||||
function renderKeyboard(phrase) {
|
||||
keyboardEl.innerHTML = "";
|
||||
for (const letter of letters) {
|
||||
const button = document.createElement("button");
|
||||
const used = state.guessed.has(letter);
|
||||
const good = used && phrase.includes(letter);
|
||||
const bad = used && !phrase.includes(letter);
|
||||
button.type = "button";
|
||||
button.className = `letter${used ? " used" : ""}${good ? " good" : ""}${bad ? " bad" : ""}`;
|
||||
button.textContent = letter;
|
||||
button.disabled = used || state.solved || !state.currentValue || state.currentValue === "BANKRUPT" || state.currentValue === "PASS";
|
||||
button.addEventListener("click", () => guessLetter(letter));
|
||||
button.addEventListener("touchend", (event) => {
|
||||
event.preventDefault();
|
||||
guessLetter(letter);
|
||||
}, { passive: false });
|
||||
keyboardEl.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
function spin() {
|
||||
if (state.solved) return;
|
||||
const value = wheelValues[Math.floor(Math.random() * wheelValues.length)];
|
||||
state.currentValue = value;
|
||||
state.rotation += 720 + Math.floor(Math.random() * 720);
|
||||
wheel.style.transform = `rotate(${state.rotation}deg)`;
|
||||
if (value === "BANKRUPT") {
|
||||
state.score = Math.max(0, Math.floor(state.score * 0.5));
|
||||
state.beamtime = Math.max(0, state.beamtime - 1);
|
||||
explainEl.textContent = "Beamtime penalty: half the score is lost. Spin again.";
|
||||
} else if (value === "PASS") {
|
||||
state.beamtime = Math.max(0, state.beamtime - 1);
|
||||
explainEl.textContent = "No useful pulse this turn. Spin again.";
|
||||
} else {
|
||||
explainEl.textContent = `Spin value ${value}. Pick a consonant or vowel from the board.`;
|
||||
}
|
||||
render();
|
||||
checkBeamtime();
|
||||
}
|
||||
|
||||
function guessLetter(letter) {
|
||||
const puzzle = puzzles[state.index];
|
||||
if (state.guessed.has(letter) || state.solved || typeof state.currentValue !== "number") return;
|
||||
const count = puzzle.phrase.split("").filter((char) => char === letter).length;
|
||||
state.guessed.set(letter, true);
|
||||
if (count > 0) {
|
||||
state.score += count * state.currentValue;
|
||||
explainEl.textContent = `${letter} appears ${count} time${count === 1 ? "" : "s"}.`;
|
||||
} else {
|
||||
state.beamtime = Math.max(0, state.beamtime - 1);
|
||||
explainEl.textContent = `${letter} is not in the phrase. Beamtime drops by one.`;
|
||||
}
|
||||
state.currentValue = 0;
|
||||
if (isSolved(puzzle.phrase)) solve(true);
|
||||
render();
|
||||
checkBeamtime();
|
||||
}
|
||||
|
||||
function isSolved(phrase) {
|
||||
return phrase.split("").every((char) => char === " " || state.guessed.has(char));
|
||||
}
|
||||
|
||||
function solve(auto = false) {
|
||||
const puzzle = puzzles[state.index];
|
||||
state.solved = true;
|
||||
explainEl.textContent = auto ? `${puzzle.phrase}. ${puzzle.explain}` : `${puzzle.phrase}: ${puzzle.explain}`;
|
||||
render();
|
||||
}
|
||||
|
||||
function nextPuzzle() {
|
||||
state.index = (state.index + 1) % puzzles.length;
|
||||
state.beamtime = 6;
|
||||
state.currentValue = 0;
|
||||
state.guessed = new Map();
|
||||
state.solved = false;
|
||||
explainEl.textContent = "Spin, pick letters, then use Next phrase for another puzzle.";
|
||||
render();
|
||||
}
|
||||
|
||||
function resetGame() {
|
||||
state.index = 0;
|
||||
state.score = 0;
|
||||
state.beamtime = 6;
|
||||
state.currentValue = 0;
|
||||
state.rotation = 0;
|
||||
state.guessed = new Map();
|
||||
state.solved = false;
|
||||
explainEl.textContent = "Spin, pick letters, then use Next phrase for another puzzle.";
|
||||
wheel.style.transform = "rotate(0deg)";
|
||||
render();
|
||||
}
|
||||
|
||||
function setLevel(level) {
|
||||
state.level = level;
|
||||
puzzles = puzzleSets[level];
|
||||
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
|
||||
resetGame();
|
||||
}
|
||||
|
||||
function checkBeamtime() {
|
||||
if (state.beamtime <= 0 && !state.solved) {
|
||||
solve(false);
|
||||
explainEl.textContent = `Beamtime expired. ${puzzles[state.index].phrase}: ${puzzles[state.index].explain}`;
|
||||
}
|
||||
}
|
||||
|
||||
spinButton.addEventListener("click", spin);
|
||||
solveButton.addEventListener("click", () => solve(false));
|
||||
nextButton.addEventListener("click", nextPuzzle);
|
||||
resetButton.addEventListener("click", resetGame);
|
||||
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
|
||||
window.addEventListener("keydown", (event) => {
|
||||
const letter = event.key.toUpperCase();
|
||||
if (letters.includes(letter)) guessLetter(letter);
|
||||
if (event.key === " ") {
|
||||
event.preventDefault();
|
||||
spin();
|
||||
}
|
||||
if (event.key.toLowerCase() === "r") resetGame();
|
||||
});
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TX Products Puzzle Games</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--panel: rgba(12, 18, 27, 0.9);
|
||||
--line: rgba(255, 255, 255, 0.16);
|
||||
--text: #f7f3ea;
|
||||
--muted: #adb7c6;
|
||||
--cyan: #65d8e8;
|
||||
--gold: #f2c96b;
|
||||
--green: #88d76f;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
background:
|
||||
radial-gradient(circle at 18% 16%, rgba(101, 216, 232, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 82% 78%, rgba(242, 201, 107, 0.12), transparent 30%),
|
||||
linear-gradient(140deg, #101926 0%, #1b242d 48%, #090c10 100%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: -28px;
|
||||
left: -18px;
|
||||
width: min(50vw, 500px);
|
||||
aspect-ratio: 1;
|
||||
background: url("all-games/assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
||||
opacity: 0.16;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(1080px, 100%);
|
||||
padding: 28px 0;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--cyan);
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 4px 0 10px;
|
||||
font-size: clamp(2.1rem, 5vw, 4.2rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 1040px;
|
||||
margin: 0 0 24px;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1.18rem, 2.2vw, 1.42rem);
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
min-height: 178px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.36);
|
||||
}
|
||||
|
||||
a:nth-child(3n + 1) { border-color: rgba(242, 201, 107, 0.45); }
|
||||
a:nth-child(3n + 2) { border-color: rgba(101, 216, 232, 0.45); }
|
||||
a:nth-child(3n) { border-color: rgba(136, 215, 111, 0.45); }
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
font-size: clamp(1.35rem, 2.6vw, 2.15rem);
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
p {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<small>TX Products Puzzle Games</small>
|
||||
<h1>Choose your Challenge</h1>
|
||||
<p>Fast hands, sharp eyes, clear signals. Which challenge will you take on first?</p>
|
||||
<div class="grid">
|
||||
<a href="all-games/wavegate.html">
|
||||
<strong>WaveGate Pulse Tailor</strong>
|
||||
<span>Shape synchrotron pulse timing by opening the gate only during the target window.</span>
|
||||
</a>
|
||||
<a href="all-games/picoswitch.html">
|
||||
<strong>PicoSwitch Pulse Cut</strong>
|
||||
<span>Trigger a photo-acoustic Bragg switch to cut x-ray pulses into the picosecond range.</span>
|
||||
</a>
|
||||
<a href="all-games/synchrotron-jeopardy.html">
|
||||
<strong>Synchrotron Challenge</strong>
|
||||
<span>A Jeopardy-style quiz covering synchrotrons, TXProducts, TXData, TXServices, and business value.</span>
|
||||
</a>
|
||||
<a href="all-games/wheel-of-photons.html">
|
||||
<strong>Wheel of Photons</strong>
|
||||
<span>A phrase puzzle with Easy, Medium, and Hard synchrotron challenges.</span>
|
||||
</a>
|
||||
<a href="all-games/cryptogram.html">
|
||||
<strong>Decode the Signal</strong>
|
||||
<span>A cryptogram where visitors recover hidden product, physics, data, and service messages.</span>
|
||||
</a>
|
||||
<a href="all-games/synchrotron-crossword.html">
|
||||
<strong>Synchrotron Crossword</strong>
|
||||
<span>Connected crossword challenges with Easy, Medium, and Hard boards.</span>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user