camera-trng/src/index.html

814 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Camera TRNG - True random number generator using camera sensor noise. Generate cryptographically random bytes from thermal and shot noise.">
<title>Camera TRNG - True Random Number Generator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #ffffff;
--text-color: #333333;
--accent-color: #0056b3;
--border-color: #e0e0e0;
--hover-color: #003d82;
--theme-bg: #f5f5f5;
--theme-border: #ddd;
--theme-hover: #e0e0e0;
--date-color: #555555;
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--bg-tertiary: #eaeaea;
--bg-hover: #f0f0f0;
--text-primary: #333333;
--focus-outline-color: #0056b3;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
--accent-color: #5fa9ff;
--border-color: #404040;
--hover-color: #8ac2ff;
--theme-bg: #2d2d2d;
--theme-border: #404040;
--theme-hover: #3d3d3d;
--date-color: #a0a0a0;
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3d3d3d;
--bg-hover: #333333;
--text-primary: #e0e0e0;
--focus-outline-color: #5fa9ff;
}
}
html[data-theme='light'] {
--bg-color: #ffffff; --text-color: #333333; --accent-color: #0056b3;
--border-color: #e0e0e0; --hover-color: #003d82; --theme-bg: #f5f5f5;
--theme-border: #ddd; --theme-hover: #e0e0e0; --date-color: #555555;
--bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #eaeaea;
--bg-hover: #f0f0f0; --text-primary: #333333; --focus-outline-color: #0056b3;
}
html[data-theme='dark'] {
--bg-color: #1a1a1a; --text-color: #e0e0e0; --accent-color: #5fa9ff;
--border-color: #404040; --hover-color: #8ac2ff; --theme-bg: #2d2d2d;
--theme-border: #404040; --theme-hover: #3d3d3d; --date-color: #a0a0a0;
--bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #3d3d3d;
--bg-hover: #333333; --text-primary: #e0e0e0; --focus-outline-color: #5fa9ff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
.skip-to-content {
position: absolute; top: -40px; left: 0; background: var(--accent-color);
color: white; padding: 8px 16px; text-decoration: none; z-index: 100;
border-radius: 0 0 4px 0; font-weight: bold;
}
.skip-to-content:focus { top: 0; outline: 3px solid var(--focus-outline-color); outline-offset: 2px; }
body {
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6; color: var(--text-color); background-color: var(--bg-color);
margin: 0 auto; padding: 20px; max-width: 800px;
}
a { color: var(--accent-color); text-decoration: underline; }
a:hover { color: var(--hover-color); }
.main-nav { display: flex; justify-content: center; margin: 1rem 0; }
.main-nav ul {
display: flex; list-style: none; margin: 0; padding: 0.5rem 1rem; gap: 1rem;
border-radius: 4px; background-color: var(--theme-bg); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-nav li { margin: 0; padding: 0; position: relative; }
.main-nav a {
display: block; padding: 0.5rem 1rem; text-decoration: none; color: var(--text-color);
font-weight: 500; border-radius: 4px; transition: background-color 0.3s, color 0.3s;
}
.main-nav a:hover { background-color: var(--theme-hover); color: var(--accent-color); }
.theme-switch { position: fixed; top: 20px; right: 20px; z-index: 1000; }
.theme-switch button {
background: var(--theme-bg); border: 1px solid var(--border-color); font-size: 1.5em;
cursor: pointer; padding: 8px 12px; border-radius: 8px; transition: background-color 0.3s;
}
.theme-switch button:hover { background-color: var(--theme-hover); }
h1 {
font-size: 2em; color: var(--accent-color); border-bottom: 2px solid var(--accent-color);
padding-bottom: 0.3em; margin-top: 1.5em; margin-bottom: 0.5em;
}
h2 { font-size: 1.5em; color: var(--accent-color); margin-top: 1.5em; margin-bottom: 0.5em; }
h3 { font-size: 1.2em; color: var(--text-color); margin-top: 1em; margin-bottom: 0.5em; }
.subtitle { color: var(--date-color); font-style: italic; margin-bottom: 2rem; }
.card {
background: var(--bg-secondary); border: 1px solid var(--border-color);
border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem;
}
.controls { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; }
.field { flex: 1; min-width: 120px; }
label {
display: block; font-size: 0.85rem; color: var(--date-color);
margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em;
}
input, select {
width: 100%; padding: 0.75rem 1rem; background: var(--bg-primary);
border: 1px solid var(--border-color); border-radius: 4px;
color: var(--text-color); font-family: monospace; font-size: 0.9rem;
}
input:focus, select:focus {
outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(0,86,179,0.1);
}
.btn-primary {
padding: 0.75rem 2rem; background: var(--accent-color); border: none; border-radius: 4px;
color: white; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s;
}
.btn-primary:hover { background: var(--hover-color); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.output {
background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px;
padding: 1rem; font-family: monospace; font-size: 0.85rem; word-break: break-all;
line-height: 1.6; min-height: 80px; max-height: 150px; overflow-y: auto; color: var(--accent-color);
}
.output.error { color: #dc3545; }
.output.copyable { cursor: pointer; position: relative; }
.output.copyable:hover { border-color: var(--accent-color); }
.copy-toast {
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
background: var(--accent-color); color: #fff; padding: 0.5rem 1.25rem;
border-radius: 6px; font-size: 0.85rem; font-weight: 500;
opacity: 0; transition: opacity 0.25s; pointer-events: none; z-index: 9999;
}
.copy-toast.show { opacity: 1; }
.stats { display: flex; gap: 2rem; color: var(--date-color); font-size: 0.85rem; margin-top: 1rem; }
.stats span { color: var(--text-color); font-family: monospace; }
pre {
background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px;
padding: 1rem; overflow-x: auto; font-size: 0.85rem; line-height: 1.5; margin: 0.5rem 0;
}
code { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; }
.code-tabs { display: flex; gap: 0; margin-bottom: 0; border-bottom: 1px solid var(--border-color); }
.code-tab {
padding: 0.5rem 1rem; background: var(--bg-secondary); border: 1px solid var(--border-color);
border-bottom: none; border-radius: 4px 4px 0 0; cursor: pointer; font-size: 0.85rem;
margin-right: -1px; color: var(--text-color);
}
.code-tab.active { background: var(--bg-tertiary); font-weight: 600; }
.code-panel { display: none; }
.code-panel.active { display: block; }
.code-panel pre { border-radius: 0 4px 4px 4px; margin-top: 0; }
.mcp-link {
display: inline-block; padding: 0.5rem 1rem; background: var(--bg-tertiary);
border: 1px solid var(--border-color); border-radius: 4px; font-family: monospace;
font-size: 0.85rem; margin: 0.5rem 0;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.loading { animation: pulse 1s infinite; }
@media (max-width: 600px) {
body { padding: 10px; } h1 { font-size: 1.75em; }
.main-nav ul { flex-direction: column; gap: 0.5rem; }
.main-nav a { text-align: center; }
.controls { flex-direction: column; }
.code-tabs { flex-wrap: wrap; }
}
.tools-section { margin-bottom: 1.75rem; }
.tools-section:last-of-type { margin-bottom: 0; }
.tools-section h3 { margin-top: 0; margin-bottom: 0.75rem; font-size: 1.05em; }
#tools-output { margin-top: 1rem; }
.eightball-answer {
font-size: 1.3rem; font-weight: 600; text-align: center; padding: 1.5rem;
border-radius: 50%; width: 180px; height: 180px; margin: 1rem auto;
display: flex; align-items: center; justify-content: center;
background: radial-gradient(circle at 40% 40%, #333, #111);
color: #fff; font-family: serif; line-height: 1.3;
box-shadow: 0 4px 20px rgba(0,0,0,0.3); transition: transform 0.3s;
}
.eightball-answer:hover { transform: scale(1.05); }
.eightball-answer.positive { color: #4cff72; }
.eightball-answer.neutral { color: #ffd644; }
.eightball-answer.negative { color: #ff6b6b; }
@keyframes shake8ball {
0%, 100% { transform: rotate(0deg); }
10% { transform: rotate(-8deg); }
20% { transform: rotate(8deg); }
30% { transform: rotate(-6deg); }
40% { transform: rotate(6deg); }
50% { transform: rotate(-3deg); }
60% { transform: rotate(3deg); }
70% { transform: rotate(-1deg); }
80% { transform: rotate(1deg); }
}
.eightball-shaking { animation: shake8ball 0.6s ease-in-out; }
/* Footer CTA with shimmer flourish */
.footer-cta {
text-align: center; padding: 1.5rem; margin: 1rem 0;
background: var(--bg-secondary); border-radius: 8px;
position: relative; overflow: hidden;
}
.footer-cta::before {
content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
border-radius: 8px; padding: 2px;
background: linear-gradient(90deg,
rgba(0,123,255,0.4), rgba(138,43,226,0.5), rgba(255,0,255,0.5),
rgba(255,20,147,0.5), rgba(0,255,255,0.5), rgba(0,200,255,0.5),
rgba(0,123,255,0.4));
background-size: 300% 100%;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
animation: shimmer 4.5s linear infinite; pointer-events: none;
}
@keyframes shimmer { 0% { background-position: 0% 0%; } 100% { background-position: 300% 0%; } }
.footer-cta > * { position: relative; z-index: 1; }
.footer-cta p { margin: 0; font-size: 1.1rem; }
.cta-link-btn {
background: none; border: none; color: var(--accent-color); font-size: inherit;
font-weight: 600; cursor: pointer; text-decoration: underline; padding: 0;
font-family: 'Montserrat', sans-serif;
}
.cta-link-btn:hover { color: var(--hover-color); }
.accessibility-notice {
font-size: 0.8rem; color: var(--date-color); line-height: 1.5;
margin: 1rem 0; padding: 0.75rem 0;
}
/* Contact Modal */
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6); display: flex; align-items: center;
justify-content: center; z-index: 9999; padding: 1rem;
}
.modal-content {
background: var(--bg-primary); border-radius: 12px; padding: 2rem;
max-width: 500px; width: 100%; max-height: 90vh; overflow-y: auto;
position: relative; box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.modal-close {
position: absolute; top: 1rem; right: 1rem; background: none; border: none;
font-size: 1.5rem; cursor: pointer; color: var(--text-color); line-height: 1;
padding: 0.25rem 0.5rem;
}
.modal-close:hover { color: var(--accent-color); }
#modal-title { margin-top: 0; margin-bottom: 0.5rem; color: var(--accent-color); }
.modal-subtitle { color: var(--date-color); margin-bottom: 1.5rem; }
.modal-content .form-group { margin-bottom: 1rem; }
.modal-content .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
.modal-content .form-group input[type="text"],
.modal-content .form-group input[type="email"],
.modal-content .form-group input[type="tel"],
.modal-content .form-group textarea {
width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 4px;
background: var(--bg-primary); color: var(--text-color); font-size: 1rem;
font-family: 'Montserrat', sans-serif; box-sizing: border-box;
}
.modal-content .form-group input:focus,
.modal-content .form-group textarea:focus {
outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
}
.modal-content .form-group textarea { resize: vertical; min-height: 80px; }
.modal-content .form-hint { font-size: 0.85rem; color: var(--date-color); margin-top: 0.25rem; }
.modal-content .checkbox-group label {
display: flex; align-items: flex-start; gap: 0.5rem; font-weight: normal; cursor: pointer;
}
.modal-content .checkbox-group input[type="checkbox"] { margin-top: 0.25rem; width: auto; }
.modal-content .form-message {
padding: 0.75rem 1rem; border-radius: 4px; margin-bottom: 1rem; font-size: 0.9rem;
}
.modal-content .form-message.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
.modal-content .form-message.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
.modal-content .form-message.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
.modal-content .cta-button {
display: block; width: 100%; padding: 0.75rem 1.5rem; background: var(--accent-color);
color: white; border: none; border-radius: 4px; font-size: 1rem; font-weight: 500;
cursor: pointer; transition: background 0.2s; font-family: 'Montserrat', sans-serif;
}
.modal-content .cta-button:hover { background: var(--hover-color); }
.modal-content .cta-button:disabled { opacity: 0.6; cursor: not-allowed; }
@media (max-width: 480px) { .modal-content { padding: 1.5rem; } }
</style>
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//metrics.nixc.us/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '3']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- PostHog -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog&&window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture identify setPersonProperties group reset get_distinct_id getGroups get_session_id alias set_config opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_3WDvcJlYYXlBVYL8vC1raT0gMfjkMuCyOpXdmgjK0CK',{api_host:'https://eu.i.posthog.com',person_profiles:'identified_only'})
</script>
</head>
<body>
<a href="#main-content" class="skip-to-content">Skip to content</a>
<div class="theme-switch">
<button id="themeToggle" type="button" role="switch" aria-label="Theme mode: Auto">🌓</button>
</div>
<nav class="main-nav">
<ul>
<li><a href="https://colinknapp.com/">Portfolio</a></li>
<li><a href="/">TRNG</a></li>
<li><a href="/tools">Tools</a></li>
</ul>
</nav>
<main id="main-content">
<h1>Camera TRNG</h1>
<p class="subtitle">True random numbers from camera sensor noise</p>
<div class="card">
<div class="controls">
<div class="field">
<label for="bytes">Bytes</label>
<input type="number" id="bytes" value="32" min="1" max="1024">
</div>
<div class="field">
<label for="format">Format</label>
<select id="format">
<option value="hex">Hexadecimal</option>
<option value="base64">Base64</option>
<option value="raw">Raw Binary</option>
</select>
</div>
<div class="field" style="display:flex;align-items:flex-end;">
<button id="generate" class="btn-primary">Generate</button>
</div>
</div>
<div class="output" id="output">Click generate to get random bytes...</div>
<div class="stats">
<div>Bytes: <span id="stat-bytes">-</span></div>
<div>Time: <span id="stat-time">-</span></div>
</div>
</div>
<div class="card">
<h2>QTRNG Tools</h2>
<p style="margin-bottom:1.25rem;color:var(--date-color);">Quantum dice, passwords, coin flips, and a Magic 8 Ball — all powered by quantum camera noise. <a href="/tools" title="Bookmark this page">Standalone tools page &rarr;</a></p>
<div class="tools-section">
<h3>Quantum Dice</h3>
<div class="controls">
<div class="field">
<label for="dice-sides">Sides (d)</label>
<input type="number" id="dice-sides" value="6" min="2" max="256">
</div>
<div class="field">
<label for="dice-count">Count</label>
<input type="number" id="dice-count" value="1" min="1" max="100">
</div>
<button id="btn-dice" class="btn-primary">Roll</button>
</div>
</div>
<div class="tools-section">
<h3>Quantum Password Generator</h3>
<div class="controls">
<div class="field">
<label for="pw-length">Length</label>
<input type="number" id="pw-length" value="20" min="1" max="128">
</div>
<div class="field">
<label for="pw-charset">Charset</label>
<select id="pw-charset">
<option value="alphanumeric">Alphanumeric</option>
<option value="full">Full (+ symbols)</option>
<option value="hex">Hex</option>
</select>
</div>
<button id="btn-password" class="btn-primary">Generate</button>
</div>
<label for="pw-no-ambiguous" style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;color:var(--date-color);cursor:pointer;margin:-0.5rem 0 0;">
<input type="checkbox" id="pw-no-ambiguous" checked style="width:auto;">
Exclude ambiguous <code style="font-size:0.8em;opacity:0.7;">0 O o I l 1 |</code>
</label>
</div>
<div class="tools-section">
<h3>Quantum Coin Flip</h3>
<div class="controls">
<button id="btn-coin" class="btn-primary">Flip</button>
</div>
</div>
<div class="tools-section">
<h3>Quantum 8 Ball</h3>
<div class="controls">
<div class="field" style="flex:3;">
<label for="eightball-question">Ask the Quantum Void</label>
<input type="text" id="eightball-question" placeholder="Will I ever understand quantum mechanics?" maxlength="200">
</div>
<button id="btn-eightball" class="btn-primary">Shake</button>
</div>
<div class="eightball-answer" id="eightball-orb" style="display:none;"></div>
</div>
<div class="output" id="tools-output">Use the buttons above.</div>
</div>
<div class="card">
<h2>API Usage</h2>
<p style="margin-bottom:1rem;color:var(--date-color);">Examples update automatically based on your settings above.</p>
<div class="code-tabs">
<button class="code-tab active" data-tab="curl">cURL</button>
<button class="code-tab" data-tab="python">Python</button>
<button class="code-tab" data-tab="js">JavaScript</button>
<button class="code-tab" data-tab="rust">Rust</button>
<button class="code-tab" data-tab="go">Go</button>
</div>
<div class="code-panel active" id="panel-curl">
<pre><code id="code-curl"></code></pre>
</div>
<div class="code-panel" id="panel-python">
<pre><code id="code-python"></code></pre>
</div>
<div class="code-panel" id="panel-js">
<pre><code id="code-js"></code></pre>
</div>
<div class="code-panel" id="panel-rust">
<pre><code id="code-rust"></code></pre>
</div>
<div class="code-panel" id="panel-go">
<pre><code id="code-go"></code></pre>
</div>
</div>
<div class="card">
<h2>MCP Integration</h2>
<p style="margin-bottom:1rem;">This service exposes a <a href="https://colinknapp.com/stories/well-known-mcp.html">.well-known/mcp.json</a> endpoint for machine-readable service discovery:</p>
<a class="mcp-link" id="mcp-url" href="/.well-known/mcp.json" target="_blank"></a>
<h3>Fetch MCP Manifest</h3>
<pre><code id="code-mcp"></code></pre>
</div>
<div class="card">
<h2>How it works</h2>
<p><strong>Entropy source:</strong> Extracts thermal and shot noise from camera sensor pixels (LSBs), then whitens via SHA-256 hashing for uniform distribution. Each request captures fresh frames ensuring unique entropy.</p>
</div>
</main>
<footer>
<hr>
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
<div id="footer-cta" class="footer-cta">
<p>Interested in working together? <button type="button" id="open-contact-modal" class="cta-link-btn">Get in touch</button></p>
</div>
<!-- Contact Modal -->
<div id="contact-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title" style="display: none;">
<div class="modal-content">
<button type="button" class="modal-close" id="close-contact-modal" aria-label="Close contact form">&times;</button>
<h2 id="modal-title">Let's Work Together</h2>
<p class="modal-subtitle">Have a project in mind? Send me a message.</p>
<form id="contactForm">
<input type="hidden" name="form_id" value="colinknapp_contact">
<div class="form-group">
<label for="contact-name">Name</label>
<input type="text" id="contact-name" name="name" required maxlength="100" placeholder="Your name">
</div>
<div class="form-group">
<label for="contact-email">Email</label>
<input type="email" id="contact-email" name="email" maxlength="254" placeholder="your@email.com">
</div>
<div class="form-group">
<label for="contact-phone">Phone</label>
<input type="tel" id="contact-phone" name="phone" required inputmode="tel" placeholder="(555) 555-5555" maxlength="20">
<p class="form-hint">Required so I can reach you</p>
</div>
<div class="form-group">
<label for="contact-message">Message</label>
<textarea id="contact-message" name="message" rows="4" required maxlength="2000" placeholder="Tell me about your project..."></textarea>
</div>
<!-- Honeypot -->
<div class="form-group" style="display: none;">
<label for="website">Website</label>
<input type="text" id="website" name="website" autocomplete="off">
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" id="contact-accept" name="accept_terms" required>
I agree to the <a href="https://motherboardrepair.ca/privacy.html" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
</label>
</div>
<div id="contact-form-message" class="form-message" style="display: none;"></div>
<button type="submit" class="cta-button" id="contact-submit-btn">Send Message</button>
</form>
</div>
</div>
</footer>
<div class="copy-toast" id="copy-toast">Copied!</div>
<script>
// Theme toggle
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
const savedTheme = localStorage.getItem('theme') || 'auto';
function updateTheme(theme) {
const icons = { light: '🌞', dark: '🌙', auto: '🌓' };
themeToggle.textContent = icons[theme];
themeToggle.setAttribute('aria-label', 'Theme: ' + theme);
if (theme === 'auto') html.removeAttribute('data-theme');
else html.setAttribute('data-theme', theme);
}
updateTheme(savedTheme);
themeToggle.addEventListener('click', () => {
const cur = html.getAttribute('data-theme') || 'auto';
const next = cur === 'light' ? 'dark' : cur === 'dark' ? 'auto' : 'light';
updateTheme(next); localStorage.setItem('theme', next);
});
// Code tabs
document.querySelectorAll('.code-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.code-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.code-panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
});
});
// Dynamic code examples
const origin = window.location.origin;
const bytesInput = document.getElementById('bytes');
const formatSelect = document.getElementById('format');
function updateCodeExamples() {
const bytes = bytesInput.value || 32;
const fmt = formatSelect.value;
const hexParam = fmt === 'hex' ? '&hex=true' : '';
const url = `${origin}/random?bytes=${bytes}${hexParam}`;
document.getElementById('code-curl').textContent = fmt === 'raw'
? `# Get ${bytes} raw random bytes\ncurl -s "${origin}/random?bytes=${bytes}" -o random.bin\n\n# Or pipe to xxd for viewing\ncurl -s "${origin}/random?bytes=${bytes}" | xxd`
: `# Get ${bytes} random bytes as ${fmt}\ncurl -s "${url}"`;
document.getElementById('code-python').textContent = fmt === 'raw'
? `import requests\n\nresp = requests.get("${origin}/random?bytes=${bytes}")\nrandom_bytes = resp.content # ${bytes} bytes\nprint(random_bytes.hex())`
: `import requests\n\nresp = requests.get("${url}")\nrandom_${fmt} = resp.text\nprint(random_${fmt})`;
document.getElementById('code-js').textContent = fmt === 'raw'
? `const resp = await fetch("${origin}/random?bytes=${bytes}");\nconst buffer = await resp.arrayBuffer();\nconst bytes = new Uint8Array(buffer); // ${bytes} bytes\nconsole.log([...bytes].map(b => b.toString(16).padStart(2,'0')).join(''));`
: `const resp = await fetch("${url}");\nconst random = await resp.text();\nconsole.log(random);`;
document.getElementById('code-rust').textContent = `use reqwest;\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n let bytes = reqwest::get("${url}")\n .await?.${fmt === 'raw' ? 'bytes' : 'text'}().await?;\n println!("{:?}", bytes);\n Ok(())\n}`;
document.getElementById('code-go').textContent = `package main\n\nimport (\n "fmt"\n "io"\n "net/http"\n)\n\nfunc main() {\n resp, _ := http.Get("${url}")\n defer resp.Body.Close()\n body, _ := io.ReadAll(resp.Body)\n fmt.Println(string(body))\n}`;
document.getElementById('mcp-url').href = origin + '/.well-known/mcp.json';
document.getElementById('mcp-url').textContent = origin + '/.well-known/mcp.json';
document.getElementById('code-mcp').textContent = `curl -s "${origin}/.well-known/mcp.json" | jq`;
}
bytesInput.addEventListener('input', updateCodeExamples);
formatSelect.addEventListener('change', updateCodeExamples);
updateCodeExamples();
// Click-to-copy for output boxes
const copyToast = document.getElementById('copy-toast');
let copyTimeout;
function copyToClipboard(text) {
if (!text || text.startsWith('Click generate') || text.startsWith('Use the buttons') || text.startsWith('Error:') || text === '...' || text.startsWith('Capturing') || text.startsWith('Rolling') || text.startsWith('Generating') || text.startsWith('Flipping')) return;
navigator.clipboard.writeText(text).then(() => {
clearTimeout(copyTimeout);
copyToast.classList.add('show');
copyTimeout = setTimeout(() => copyToast.classList.remove('show'), 1500);
});
}
// Make both output boxes clickable-to-copy
output.classList.add('copyable');
output.title = 'Click to copy';
output.addEventListener('click', () => copyToClipboard(output.textContent));
toolsOutput.classList.add('copyable');
toolsOutput.title = 'Click to copy';
toolsOutput.addEventListener('click', () => copyToClipboard(toolsOutput.textContent));
// Random generator
const btn = document.getElementById('generate');
const output = document.getElementById('output');
const statBytes = document.getElementById('stat-bytes');
const statTime = document.getElementById('stat-time');
btn.addEventListener('click', async () => {
const bytes = Math.min(1024, Math.max(1, parseInt(bytesInput.value) || 32));
bytesInput.value = bytes;
const fmt = formatSelect.value;
btn.disabled = true; output.className = 'output loading';
output.textContent = 'Capturing camera noise...';
const start = performance.now();
try {
const url = fmt === 'raw'
? `/random?bytes=${bytes}`
: `/random?bytes=${bytes}&hex=true`;
const res = await fetch(url);
const elapsed = (performance.now() - start).toFixed(0);
if (!res.ok) throw new Error(await res.text());
if (fmt === 'raw') {
const buf = await res.arrayBuffer();
const arr = new Uint8Array(buf);
output.textContent = `[${bytes} raw bytes] ` + [...arr].map(b => b.toString(16).padStart(2,'0')).join(' ');
} else if (fmt === 'base64') {
const hex = await res.text();
const arr = new Uint8Array(hex.match(/.{2}/g).map(x => parseInt(x, 16)));
output.textContent = btoa(String.fromCharCode(...arr));
} else {
output.textContent = await res.text();
}
output.className = 'output';
statBytes.textContent = bytes; statTime.textContent = elapsed + 'ms';
} catch (e) { output.className = 'output error'; output.textContent = 'Error: ' + e.message; }
btn.disabled = false;
});
const toolsOutput = document.getElementById('tools-output');
const btnDice = document.getElementById('btn-dice');
const btnPassword = document.getElementById('btn-password');
const btnCoin = document.getElementById('btn-coin');
const btnEightball = document.getElementById('btn-eightball');
const eightballOrb = document.getElementById('eightball-orb');
function setToolsLoading(loading) {
toolsOutput.className = loading ? 'output loading' : 'output';
btnDice.disabled = btnPassword.disabled = btnCoin.disabled = btnEightball.disabled = loading;
}
btnDice.addEventListener('click', async () => {
setToolsLoading(true);
toolsOutput.style.fontFamily = ''; toolsOutput.style.fontSize = ''; toolsOutput.style.letterSpacing = ''; toolsOutput.textContent = 'Rolling...';
try {
const d = document.getElementById('dice-sides').value || 6;
const c = document.getElementById('dice-count').value || 1;
const r = await fetch(origin + '/dice?d=' + d + '&count=' + c);
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
toolsOutput.textContent = 'Rolls: ' + j.rolls.join(', ') + ' Sum: ' + j.sum + ' of Possible: ' + (j.sides * j.count);
} catch (e) { toolsOutput.className = 'output error'; toolsOutput.textContent = 'Error: ' + e.message; }
setToolsLoading(false);
});
btnPassword.addEventListener('click', async () => {
setToolsLoading(true);
toolsOutput.textContent = 'Generating...';
try {
const len = document.getElementById('pw-length').value || 20;
const cs = document.getElementById('pw-charset').value || 'alphanumeric';
const noAmb = document.getElementById('pw-no-ambiguous').checked;
let url = origin + '/password?length=' + len + '&charset=' + cs;
if (noAmb) url += '&exclude_ambiguous=true';
const r = await fetch(url);
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
toolsOutput.style.fontFamily = "'SF Mono', Monaco, 'Cascadia Code', 'Fira Code', monospace";
toolsOutput.style.fontSize = '1.1rem';
toolsOutput.style.letterSpacing = '0.08em';
toolsOutput.textContent = j.password;
} catch (e) { toolsOutput.className = 'output error'; toolsOutput.textContent = 'Error: ' + e.message; }
setToolsLoading(false);
});
btnCoin.addEventListener('click', async () => {
setToolsLoading(true);
toolsOutput.style.fontFamily = ''; toolsOutput.style.fontSize = ''; toolsOutput.style.letterSpacing = ''; toolsOutput.textContent = 'Flipping...';
try {
const r = await fetch(origin + '/coin');
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
toolsOutput.textContent = 'Result: ' + j.result;
} catch (e) { toolsOutput.className = 'output error'; toolsOutput.textContent = 'Error: ' + e.message; }
setToolsLoading(false);
});
</script>
<!-- Contact Modal Logic -->
<script>
(function() {
'use strict';
var modal = document.getElementById('contact-modal');
var openBtn = document.getElementById('open-contact-modal');
var closeBtn = document.getElementById('close-contact-modal');
var form = document.getElementById('contactForm');
var submitBtn = document.getElementById('contact-submit-btn');
var formMessage = document.getElementById('contact-form-message');
var phoneInput = document.getElementById('contact-phone');
if (!modal || !form) return;
function openModal() {
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
var fi = modal.querySelector('input, button');
if (fi) fi.focus();
}
function closeModal() {
modal.style.display = 'none';
document.body.style.overflow = '';
}
if (openBtn) openBtn.addEventListener('click', openModal);
if (closeBtn) closeBtn.addEventListener('click', closeModal);
modal.addEventListener('click', function(e) { if (e.target === modal) closeModal(); });
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal.style.display === 'flex') closeModal();
});
if (phoneInput) {
phoneInput.addEventListener('input', function(e) {
var v = e.target.value.replace(/\D/g, '');
if (v.length > 10) v = v.slice(0, 10);
if (v.length >= 6) v = '(' + v.slice(0,3) + ') ' + v.slice(3,6) + '-' + v.slice(6);
else if (v.length >= 3) v = '(' + v.slice(0,3) + ') ' + v.slice(3);
else if (v.length > 0) v = '(' + v;
e.target.value = v;
});
}
function showMessage(type, message) {
formMessage.textContent = message;
formMessage.className = 'form-message ' + type;
formMessage.style.display = 'block';
if (type === 'success') {
setTimeout(function() { formMessage.style.display = 'none'; closeModal(); }, 3000);
}
}
function sanitizeText(input) {
if (typeof input !== 'string') return '';
return input.replace(/[\u0000-\u001F\u007F]/g, '').trim().replace(/\s+/g, ' ');
}
function sanitizeMultiline(input) {
if (typeof input !== 'string') return '';
return input.replace(/[\u0000-\u0009\u000B-\u001F\u007F]/g, '').replace(/[<>]/g, '');
}
form.addEventListener('submit', function(e) {
e.preventDefault();
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
var fd = new FormData(form);
if (fd.get('website')) {
submitBtn.disabled = false; submitBtn.textContent = 'Send Message';
showMessage('success', 'Thank you! Your message has been received.');
return;
}
var data = {
form_id: 'colinknapp_contact',
name: sanitizeText(fd.get('name')).slice(0, 100),
email: (fd.get('email') || '').trim().slice(0, 254),
phone: (fd.get('phone') || '').replace(/\D/g, '').slice(0, 10),
message: sanitizeMultiline(fd.get('message')).slice(0, 2000)
};
if (!data.name) { showMessage('error', 'Please enter your name.'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; return; }
if (!data.phone || data.phone.length < 10) { showMessage('error', 'Please enter a valid phone number.'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; return; }
if (!data.message) { showMessage('error', 'Please enter a message.'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; return; }
showMessage('info', 'Sending your message...');
fetch('https://forms.motherboardrepair.ca/api/submit', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(function(r) { if (!r.ok) throw new Error('Server error: ' + r.status); return r.json(); })
.then(function() {
showMessage('success', "Thank you! I'll get back to you soon.");
form.reset(); submitBtn.disabled = false; submitBtn.textContent = 'Send Message';
})
.catch(function(err) {
console.error('Form error:', err);
showMessage('error', 'Sorry, there was an issue. Please try again.');
submitBtn.disabled = false; submitBtn.textContent = 'Send Message';
});
});
})();
btnEightball.addEventListener('click', async () => {
setToolsLoading(true);
toolsOutput.style.fontFamily = ''; toolsOutput.style.fontSize = ''; toolsOutput.style.letterSpacing = '';
toolsOutput.textContent = '';
eightballOrb.style.display = 'flex';
eightballOrb.className = 'eightball-answer eightball-shaking';
eightballOrb.textContent = '...';
try {
const q = document.getElementById('eightball-question').value;
let url = origin + '/8ball';
if (q) url += '?question=' + encodeURIComponent(q);
const r = await fetch(url);
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
eightballOrb.className = 'eightball-answer ' + j.sentiment;
eightballOrb.textContent = j.answer;
if (q) {
toolsOutput.textContent = 'Q: ' + q;
}
} catch (e) {
eightballOrb.style.display = 'none';
toolsOutput.className = 'output error';
toolsOutput.textContent = 'Error: ' + e.message;
}
setToolsLoading(false);
});
</script>
</body>
</html>