Adrian Bradford

@username
email@example.com

Mobile App

Mobile App

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Will Marr — jtom.ca" />
<title>WILL MARR // jtom.ca</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=JetBrains+Mono:wght@300;400;600;800&family=VT323&display=swap" rel="stylesheet">
<style>
:root {
--bg: #000000;
--green: #00ff66;
--green-dim: #00b347;
--green-darker: #006628;
--green-glow: rgba(0, 255, 102, 0.6);
--amber: #a7ff3e;
--text: #c8ffd8;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
min-height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'JetBrains Mono', 'Courier New', monospace;
cursor: none;
overflow-x: hidden;
}
body::before {
content: "";
position: fixed; inset: 0;
pointer-events: none; z-index: 1000;
background: repeating-linear-gradient(0deg,
rgba(0,0,0,0) 0px, rgba(0,0,0,0) 2px,
rgba(0,0,0,0.25) 3px, rgba(0,0,0,0) 4px);
mix-blend-mode: multiply;
}
body::after {
content: "";
position: fixed; inset: 0;
pointer-events: none; z-index: 1001;
background: radial-gradient(ellipse at center,
rgba(0,0,0,0) 40%, rgba(0,0,0,0.7) 100%);
animation: flicker 6s infinite;
}
@keyframes flicker {
0%, 100% { opacity: 1; }
50% { opacity: 0.97; }
52% { opacity: 0.92; }
54% { opacity: 1; }
}
#matrix {
position: fixed; inset: 0; z-index: 0; opacity: 0.35;
}
.terminal {
position: relative; z-index: 10;
min-height: 100vh; width: 100%;
display: flex; flex-direction: column;
padding: 1.5rem 2rem;
}
.status-bar {
display: flex; justify-content: space-between; align-items: center;
font-size: 0.75rem; color: var(--green-dim);
border-bottom: 1px solid var(--green-darker);
padding-bottom: 0.5rem;
letter-spacing: 0.15em; text-transform: uppercase;
}
.status-left, .status-right { display: flex; gap: 1.25rem; }
.status-item .dot {
display: inline-block; width: 6px; height: 6px;
background: var(--green); border-radius: 50%;
box-shadow: 0 0 8px var(--green-glow);
margin-right: 0.4rem;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* GEO BANNER */
.geo-banner {
margin-top: 1rem;
text-align: center;
font-family: 'VT323', monospace;
font-size: 1.3rem;
color: var(--green);
letter-spacing: 0.1em;
text-shadow: 0 0 8px var(--green-glow);
min-height: 1.6rem;
opacity: 0;
animation: fadeIn 0.8s 0.6s forwards;
}
.geo-banner .label { color: var(--green-dim); }
.geo-banner .city {
color: var(--amber);
text-shadow: 0 0 10px rgba(167, 255, 62, 0.6);
}
@keyframes fadeIn { to { opacity: 1; } }
.main {
flex: 1;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
text-align: center; gap: 1.5rem;
padding: 2rem 0;
}
.boot-log {
width: 100%; max-width: 900px;
min-height: 130px;
color: var(--green-dim);
text-align: left;
font-family: 'VT323', monospace;
font-size: 1rem; line-height: 1.4;
}
.boot-line { opacity: 0; animation: fadeIn 0.1s forwards; }
.boot-line .ok { color: var(--green); }
.boot-line .warn { color: var(--amber); }
.ascii-name {
font-family: 'JetBrains Mono', monospace;
font-weight: 800;
font-size: clamp(0.45rem, 1.15vw, 0.95rem);
line-height: 1;
color: var(--green);
text-shadow:
0 0 6px var(--green-glow),
0 0 18px rgba(0,255,102,0.35),
0 0 40px rgba(0,255,102,0.18);
white-space: pre;
user-select: none;
opacity: 0;
animation: fadeInName 1.2s 1.5s forwards, glow 3s 2.7s infinite ease-in-out;
}
@keyframes fadeInName {
from { opacity: 0; filter: blur(4px); transform: translateY(10px); }
to { opacity: 1; filter: blur(0); transform: translateY(0); }
}
@keyframes glow {
0%, 100% {
text-shadow:
0 0 6px var(--green-glow),
0 0 18px rgba(0,255,102,0.35),
0 0 40px rgba(0,255,102,0.18);
}
50% {
text-shadow:
0 0 10px var(--green-glow),
0 0 28px rgba(0,255,102,0.55),
0 0 60px rgba(0,255,102,0.3);
}
}
.glitch-wrap { position: relative; cursor: none; }
.glitch-wrap .ascii-name.layer {
position: absolute; top: 0; left: 0;
opacity: 0;
mix-blend-mode: screen;
pointer-events: none;
}
.glitch-wrap .layer-red { color: #ff2a5c; animation: glitchRed 5s infinite; }
.glitch-wrap .layer-blue { color: #2af0ff; animation: glitchBlue 5s infinite; }
@keyframes glitchRed {
0%, 92%, 100% { opacity: 0; transform: translate(0,0); }
93% { opacity: 0.5; transform: translate(-2px, 1px); }
95% { opacity: 0; }
97% { opacity: 0.4; transform: translate(3px, -1px); }
98% { opacity: 0; }
}
@keyframes glitchBlue {
0%, 91%, 100% { opacity: 0; transform: translate(0,0); }
93% { opacity: 0.5; transform: translate(2px, -1px); }
95% { opacity: 0; }
97% { opacity: 0.4; transform: translate(-3px, 1px); }
98% { opacity: 0; }
}
/* CRAZY HOVER MODE */
.glitch-wrap.chaos .ascii-name:not(.layer) {
animation: rainbowCycle 0.4s infinite steps(1), chaosShake 0.08s infinite !important;
}
.glitch-wrap.chaos .layer-red {
animation: chaosRed 0.12s infinite !important;
opacity: 0.8;
}
.glitch-wrap.chaos .layer-blue {
animation: chaosBlue 0.1s infinite !important;
opacity: 0.8;
}
@keyframes rainbowCycle {
0% { color: #ff2a5c; text-shadow: 0 0 8px #ff2a5c, 0 0 24px #ff2a5c, 0 0 50px #ff2a5c; }
12% { color: #ff9500; text-shadow: 0 0 8px #ff9500, 0 0 24px #ff9500, 0 0 50px #ff9500; }
25% { color: #ffeb00; text-shadow: 0 0 8px #ffeb00, 0 0 24px #ffeb00, 0 0 50px #ffeb00; }
37% { color: #00ff66; text-shadow: 0 0 8px #00ff66, 0 0 24px #00ff66, 0 0 50px #00ff66; }
50% { color: #2af0ff; text-shadow: 0 0 8px #2af0ff, 0 0 24px #2af0ff, 0 0 50px #2af0ff; }
62% { color: #4d6bff; text-shadow: 0 0 8px #4d6bff, 0 0 24px #4d6bff, 0 0 50px #4d6bff; }
75% { color: #c54dff; text-shadow: 0 0 8px #c54dff, 0 0 24px #c54dff, 0 0 50px #c54dff; }
87% { color: #ff2aa8; text-shadow: 0 0 8px #ff2aa8, 0 0 24px #ff2aa8, 0 0 50px #ff2aa8; }
100% { color: #ffffff; text-shadow: 0 0 8px #ffffff, 0 0 24px #ffffff, 0 0 50px #ffffff; }
}
@keyframes chaosShake {
0% { transform: translate(0, 0) skewX(0deg); }
20% { transform: translate(-3px, 2px) skewX(-2deg); }
40% { transform: translate(3px, -1px) skewX(1deg); }
60% { transform: translate(-2px, -2px) skewX(2deg); }
80% { transform: translate(2px, 3px) skewX(-1deg); }
100% { transform: translate(-1px, -1px) skewX(0deg); }
}
@keyframes chaosRed {
0% { transform: translate(-6px, 2px); opacity: 0.9; }
50% { transform: translate(4px, -3px); opacity: 0.6; }
100% { transform: translate(-3px, 4px); opacity: 0.9; }
}
@keyframes chaosBlue {
0% { transform: translate(6px, -2px); opacity: 0.9; }
50% { transform: translate(-4px, 3px); opacity: 0.6; }
100% { transform: translate(3px, -4px); opacity: 0.9; }
}
.tagline {
font-family: 'VT323', monospace;
font-size: 1.4rem;
letter-spacing: 0.3em;
text-transform: uppercase;
opacity: 0;
animation: fadeInUp 0.8s 2.5s forwards;
}
.tagline .bracket { color: var(--green-dim); }
.tagline .scramble {
color: var(--green);
text-shadow: 0 0 8px var(--green-glow);
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.contact-btn {
display: inline-block;
padding: 1rem 2.5rem;
font-family: 'JetBrains Mono', monospace;
font-size: 1rem; font-weight: 600;
color: var(--green);
background: transparent;
border: 1px solid var(--green-dim);
text-decoration: none;
letter-spacing: 0.25em;
text-transform: uppercase;
position: relative; overflow: hidden;
transition: all 0.2s ease;
cursor: none;
opacity: 0;
animation: fadeInUp 0.8s 3s forwards;
}
.contact-btn.copied {
color: var(--bg);
background: var(--green);
border-color: var(--green);
box-shadow: 0 0 40px var(--green-glow);
}
.contact-btn::before {
content: ""; position: absolute; inset: 0;
background: var(--green);
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.77, 0, 0.175, 1);
z-index: -1;
}
.contact-btn:hover {
color: var(--bg);
border-color: var(--green);
box-shadow: 0 0 30px var(--green-glow), inset 0 0 10px rgba(0,255,102,0.2);
text-shadow: none;
}
.contact-btn:hover::before { transform: translateX(0); }
.contact-btn .arrow {
display: inline-block;
margin: 0 0.5rem;
animation: blink 1s infinite step-end;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* SOCIALS */
.socials {
display: flex; flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin-top: 0.5rem;
opacity: 0;
animation: fadeInUp 0.8s 3.3s forwards;
}
.social-link {
position: relative;
display: inline-flex; align-items: center;
gap: 0.6rem;
padding: 0.7rem 1.1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem; font-weight: 600;
color: var(--green-dim);
background: rgba(0, 20, 8, 0.4);
border: 1px solid var(--green-darker);
text-decoration: none;
letter-spacing: 0.15em;
text-transform: uppercase;
cursor: none;
transition: all 0.25s ease;
overflow: hidden;
}
.social-link::before {
content: ""; position: absolute;
top: 0; left: 0;
width: 4px; height: 100%;
background: var(--green);
transform: scaleY(0);
transform-origin: bottom;
transition: transform 0.25s ease;
}
.social-link:hover {
color: var(--green);
border-color: var(--green);
background: rgba(0, 255, 102, 0.08);
box-shadow: 0 0 20px rgba(0, 255, 102, 0.3);
transform: translateY(-2px);
}
.social-link:hover::before { transform: scaleY(1); }
.social-link .icon { color: var(--green); font-weight: 800; }
.footer-bar {
display: flex; justify-content: space-between; align-items: center;
font-size: 0.7rem; color: var(--green-darker);
border-top: 1px solid var(--green-darker);
padding-top: 0.5rem;
letter-spacing: 0.2em; text-transform: uppercase;
margin-top: auto;
}
.footer-bar .prompt { color: var(--green); }
.footer-bar .typed-cmd { color: var(--text); }
.cursor-block {
display: inline-block;
width: 8px; height: 14px;
background: var(--green);
vertical-align: middle;
margin-left: 2px;
animation: blink 1s infinite step-end;
box-shadow: 0 0 6px var(--green-glow);
}
.cursor {
position: fixed;
width: 20px; height: 20px;
border: 1px solid var(--green);
pointer-events: none; z-index: 9999;
transform: translate(-50%, -50%);
transition: width 0.15s, height 0.15s, background 0.15s;
mix-blend-mode: difference;
}
.cursor.active {
width: 40px; height: 40px;
background: rgba(0, 255, 102, 0.2);
}
.cursor-dot {
position: fixed;
width: 4px; height: 4px;
background: var(--green); border-radius: 50%;
pointer-events: none; z-index: 9999;
transform: translate(-50%, -50%);
box-shadow: 0 0 8px var(--green-glow);
}
.trail-char {
position: fixed;
color: var(--green);
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
pointer-events: none; z-index: 9998;
text-shadow: 0 0 6px var(--green-glow);
animation: trailFade 0.8s forwards;
}
@keyframes trailFade {
0% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(0.4) translateY(20px); }
}
@media (max-width: 768px) {
html, body { cursor: auto; }
.cursor, .cursor-dot { display: none; }
.terminal { padding: 0.9rem 0.9rem 1rem; }
/* Status/footer bars: stack and shrink */
.status-bar, .footer-bar {
font-size: 0.55rem;
flex-wrap: wrap;
gap: 0.4rem 0.8rem;
letter-spacing: 0.1em;
}
.status-left, .status-right { gap: 0.6rem; flex-wrap: wrap; }
/* Geo banner */
.geo-banner { font-size: 1.1rem; margin-top: 0.8rem; letter-spacing: 0.08em; }
/* Main: less padding, tighter gaps */
.main { gap: 1.1rem; padding: 1.2rem 0; }
/* Boot log tighter */
.boot-log { font-size: 0.82rem; min-height: 90px; line-height: 1.35; }
/* ASCII name: scale to viewport so it always fits */
.ascii-name {
font-size: 1.7vw; /* fallback */
font-size: clamp(0.32rem, 2vw, 0.55rem);
}
/* Tagline */
.tagline { font-size: 0.95rem; letter-spacing: 0.15em; padding: 0 0.5rem; }
/* Email button: full width and touch-friendly */
.contact-btn {
padding: 0.9rem 1rem;
font-size: 0.78rem;
letter-spacing: 0.12em;
width: 100%;
max-width: 360px;
}
.contact-btn .arrow { margin: 0 0.3rem; }
/* Socials: comfortable tap targets, wrap nicely */
.socials { gap: 0.5rem; width: 100%; max-width: 360px; }
.social-link {
flex: 1 1 calc(50% - 0.25rem);
justify-content: center;
padding: 0.75rem 0.5rem;
font-size: 0.72rem;
letter-spacing: 0.08em;
gap: 0.4rem;
min-height: 44px; /* iOS tap target */
}
/* Trim matrix rain density on mobile */
#matrix { opacity: 0.25; }
}
/* Very narrow phones */
@media (max-width: 380px) {
.status-bar { font-size: 0.5rem; }
.footer-bar { font-size: 0.5rem; }
.status-item:nth-child(3) { display: none; } /* hide uptime on tiny screens */
.ascii-name { font-size: clamp(0.28rem, 1.9vw, 0.5rem); }
.tagline { font-size: 0.85rem; }
.boot-log { font-size: 0.75rem; min-height: 80px; }
}
/* Landscape phones / short viewports */
@media (max-height: 600px) and (max-width: 900px) {
.main { gap: 0.8rem; padding: 0.8rem 0; }
.boot-log { min-height: 60px; }
.boot-log .boot-line:nth-child(-n+2) { display: none; } /* trim old lines faster */
.geo-banner { margin-top: 0.4rem; font-size: 1rem; }
}
/* Respect reduced-motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
body::after { animation: none; }
#matrix { display: none; }
}
</style>
</head>
<body>
<canvas id="matrix"></canvas>
<div class="cursor" id="cursor"></div>
<div class="cursor-dot" id="cursorDot"></div>
<div class="terminal">
<div class="status-bar">
<div class="status-left">
<span class="status-item"><span class="dot"></span>SYS://ONLINE</span>
<span class="status-item">NODE: JTOM.CA</span>
<span class="status-item" id="uptime">UPTIME: 00:00:00</span>
</div>
<div class="status-right">
<span class="status-item" id="clock">--:--:--</span>
<span class="status-item">ENC: AES-256</span>
</div>
</div>
<!-- GEO GREETING -->
<div class="geo-banner" id="geoBanner">
<span class="label">&gt;&gt; PINGING CLIENT...</span>
</div>
<div class="main">
<div class="boot-log" id="bootLog"></div>
<div class="glitch-wrap" id="glitchWrap">
<pre class="ascii-name" id="asciiName">
██╗ ██╗██╗██╗ ██╗ ███╗ ███╗ █████╗ ██████╗ ██████╗
██║ ██║██║██║ ██║ ████╗ ████║██╔══██╗██╔══██╗██╔══██╗
██║ █╗ ██║██║██║ ██║ ██╔████╔██║███████║██████╔╝██████╔╝
██║███╗██║██║██║ ██║ ██║╚██╔╝██║██╔══██║██╔══██╗██╔══██╗
╚███╔███╔╝██║███████╗███████╗ ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██║
╚══╝╚══╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
</pre>
<pre class="ascii-name layer layer-red" aria-hidden="true">
██╗ ██╗██╗██╗ ██╗ ███╗ ███╗ █████╗ ██████╗ ██████╗
██║ ██║██║██║ ██║ ████╗ ████║██╔══██╗██╔══██╗██╔══██╗
██║ █╗ ██║██║██║ ██║ ██╔████╔██║███████║██████╔╝██████╔╝
██║███╗██║██║██║ ██║ ██║╚██╔╝██║██╔══██║██╔══██╗██╔══██╗
╚███╔███╔╝██║███████╗███████╗ ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██║
╚══╝╚══╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
</pre>
<pre class="ascii-name layer layer-blue" aria-hidden="true">
██╗ ██╗██╗██╗ ██╗ ███╗ ███╗ █████╗ ██████╗ ██████╗
██║ ██║██║██║ ██║ ████╗ ████║██╔══██╗██╔══██╗██╔══██╗
██║ █╗ ██║██║██║ ██║ ██╔████╔██║███████║██████╔╝██████╔╝
██║███╗██║██║██║ ██║ ██║╚██╔╝██║██╔══██║██╔══██║██╔══██╗
╚███╔███╔╝██║███████╗███████╗ ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██║
╚══╝╚══╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
</pre>
</div>
<div class="tagline" id="tagline">
<span class="bracket">[</span> <span class="scramble" id="scrambleText">ESTABLISHING SECURE CHANNEL</span> <span class="bracket">]</span>
</div>
<button type="button" class="contact-btn" id="contactBtn">
<span class="arrow">&gt;</span>
<span id="contactBtnText">COPY_EMAIL: WILL@JTOM.CA</span>
<span class="arrow">_</span>
</button>
<!-- SOCIALS -->
<div class="socials" id="socials">
<a class="social-link" href="https://www.tiktok.com/@willmawr" target="_blank" rel="noopener">
<span class="icon">[TT]</span><span>@WILLMAWR</span>
</a>
<a class="social-link" href="https://www.instagram.com/will.marr" target="_blank" rel="noopener">
<span class="icon">[IG]</span><span>@WILL.MARR</span>
</a>
<a class="social-link" href="https://open.spotify.com/artist/62Giwxriifay79DmCJoeVP" target="_blank" rel="noopener">
<span class="icon">[SP]</span><span>SPOTIFY</span>
</a>
</div>
</div>
<div class="footer-bar">
<div>
<span class="prompt">root@jtom.ca:~$</span>
<span class="typed-cmd" id="typedCmd"></span><span class="cursor-block"></span>
</div>
<div>© 2026 // ALL SYSTEMS NOMINAL</div>
</div>
</div>
<script>
// MATRIX RAIN
const canvas = document.getElementById('matrix');
const ctx = canvas.getContext('2d');
function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$%#@!*<>?=+-';
const fontSize = window.innerWidth < 480 ? 12 : 14;
let columns = Math.floor(canvas.width / fontSize);
let drops = Array(columns).fill(1).map(() => Math.random() * -100);
const matrixTickMs = window.innerWidth < 480 ? 55 : 40;
function drawMatrix() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.06)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = fontSize + 'px JetBrains Mono, monospace';
for (let i = 0; i < drops.length; i++) {
const char = chars[Math.floor(Math.random() * chars.length)];
const x = i * fontSize;
const y = drops[i] * fontSize;
ctx.fillStyle = Math.random() > 0.975 ? '#c8ffd8' : '#00b347';
ctx.fillText(char, x, y);
if (y > canvas.height && Math.random() > 0.975) drops[i] = 0;
drops[i]++;
}
}
window.addEventListener('resize', () => {
columns = Math.floor(canvas.width / fontSize);
drops = Array(columns).fill(1).map(() => Math.random() * -100);
});
setInterval(drawMatrix, matrixTickMs);
// CLOCK + UPTIME
const startTime = Date.now();
function updateClock() {
const now = new Date();
const pad = n => String(n).padStart(2, '0');
document.getElementById('clock').textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
const diff = Math.floor((Date.now() - startTime) / 1000);
document.getElementById('uptime').textContent =
`UPTIME: ${pad(Math.floor(diff/3600))}:${pad(Math.floor((diff%3600)/60))}:${pad(diff%60)}`;
}
updateClock();
setInterval(updateClock, 1000);
// GEO GREETING
const geoBanner = document.getElementById('geoBanner');
async function fetchGeo() {
// Primary: ipapi.co (free, HTTPS, no key)
try {
const res = await fetch('https://ipapi.co/json/');
if (res.ok) {
const d = await res.json();
if (d && d.city && !d.error) return {
city: d.city, region: d.region,
country: d.country_name, country_code: d.country_code, ip: d.ip
};
}
} catch (e) {}
// Fallback: ipwho.is (free, HTTPS, no key)
try {
const res = await fetch('https://ipwho.is/');
if (res.ok) {
const d = await res.json();
if (d && d.success && d.city) return {
city: d.city, region: d.region,
country: d.country, country_code: d.country_code, ip: d.ip
};
}
} catch (e) {}
return null;
}
function scrambleIntoHTML(el, finalHTML, duration = 1100) {
// Work out the plain-text end state to scramble toward
const tmp = document.createElement('div');
tmp.innerHTML = finalHTML;
const plain = tmp.textContent;
const pool = '!<>-_\\/[]{}=+*^?#01アカサ@$%';
const steps = 30;
const interval = duration / steps;
let step = 0;
const iv = setInterval(() => {
if (step >= steps) {
clearInterval(iv);
el.innerHTML = finalHTML;
return;
}
const reveal = Math.floor((step / steps) * plain.length);
let out = '';
for (let i = 0; i < plain.length; i++) {
if (i < reveal) out += plain[i];
else if (plain[i] === ' ' || plain[i] === '\n') out += plain[i];
else out += pool[Math.floor(Math.random() * pool.length)];
}
el.textContent = out;
step++;
}, interval);
}
(async () => {
const html = `<span class="label">&gt;&gt;</span> <span class="city">HELLO ALL</span>`;
scrambleIntoHTML(geoBanner, html, 1200);
})();
// BOOT LOG
const bootLines = [
{ text: '[BOOT] Initializing jtom.ca kernel v4.2.0...', cls: 'ok', delay: 100 },
{ text: '[NET] Opening port 443 ............... [ OK ]', cls: 'ok', delay: 350 },
{ text: '[GEO] Resolving client coordinates ... [ OK ]', cls: 'ok', delay: 600 },
{ text: '[SEC] Handshake complete ............. [ OK ]', cls: 'ok', delay: 850 },
{ text: '[USR] Loading identity: will_marr .... [ OK ]', cls: 'ok', delay: 1100 },
{ text: '[SYS] All systems nominal. Welcome.', cls: 'ok', delay: 1350 },
];
const bootLog = document.getElementById('bootLog');
bootLines.forEach(line => {
setTimeout(() => {
const div = document.createElement('div');
div.className = 'boot-line';
div.innerHTML = `<span class="${line.cls}">${line.text}</span>`;
bootLog.appendChild(div);
if (bootLog.children.length > 6) bootLog.removeChild(bootLog.firstChild);
}, line.delay);
});
// SCRAMBLE TAGLINE
const scrambleEl = document.getElementById('scrambleText');
const messages = [
'ESTABLISHING SECURE CHANNEL',
'CREATOR / MAKER / HUMAN',
'AVAILABLE FOR TRANSMISSION',
'HELLO FROM ONTARIO, CA',
];
const scrambleCharPool = '!<>-_\\/[]{}—=+*^?#________';
let scrambleIdx = 0;
function scrambleTo(newText) {
const oldText = scrambleEl.textContent;
const length = Math.max(oldText.length, newText.length);
const queue = [];
for (let i = 0; i < length; i++) {
const from = oldText[i] || '';
const to = newText[i] || '';
const start = Math.floor(Math.random() * 20);
const end = start + Math.floor(Math.random() * 20);
queue.push({ from, to, start, end, char: '' });
}
let frame = 0;
function update() {
let output = '';
let complete = 0;
for (let i = 0; i < queue.length; i++) {
let { from, to, start, end, char } = queue[i];
if (frame >= end) { complete++; output += to; }
else if (frame >= start) {
if (!char || Math.random() < 0.28) {
char = scrambleCharPool[Math.floor(Math.random() * scrambleCharPool.length)];
queue[i].char = char;
}
output += char;
} else output += from;
}
scrambleEl.textContent = output;
if (complete !== queue.length) { requestAnimationFrame(update); frame++; }
}
update();
}
setTimeout(() => {
setInterval(() => {
scrambleIdx = (scrambleIdx + 1) % messages.length;
scrambleTo(messages[scrambleIdx]);
}, 3500);
}, 4000);
// TYPED FOOTER
const typedEl = document.getElementById('typedCmd');
const commands = ['ls -la ./contact', 'cat willmarr.pub', 'ssh will@jtom.ca', 'echo "hello world"'];
let cmdIdx = 0;
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function typeCommand() {
while (true) {
const cmd = commands[cmdIdx];
for (let i = 0; i <= cmd.length; i++) {
typedEl.textContent = cmd.slice(0, i);
await sleep(70 + Math.random() * 50);
}
await sleep(2500);
for (let i = cmd.length; i >= 0; i--) {
typedEl.textContent = cmd.slice(0, i);
await sleep(30);
}
cmdIdx = (cmdIdx + 1) % commands.length;
await sleep(500);
}
}
typeCommand();
// CUSTOM CURSOR + TRAIL (desktop only)
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || window.matchMedia('(pointer: coarse)').matches;
const cursor = document.getElementById('cursor');
const cursorDot = document.getElementById('cursorDot');
if (!isTouchDevice) {
let mouseX = 0, mouseY = 0, cursorX = 0, cursorY = 0;
document.addEventListener('mousemove', e => {
mouseX = e.clientX; mouseY = e.clientY;
cursorDot.style.left = mouseX + 'px';
cursorDot.style.top = mouseY + 'px';
});
(function animateCursor() {
cursorX += (mouseX - cursorX) * 0.18;
cursorY += (mouseY - cursorY) * 0.18;
cursor.style.left = cursorX + 'px';
cursor.style.top = cursorY + 'px';
requestAnimationFrame(animateCursor);
})();
document.querySelectorAll('a, button').forEach(el => {
el.addEventListener('mouseenter', () => cursor.classList.add('active'));
el.addEventListener('mouseleave', () => cursor.classList.remove('active'));
});
}
const trailChars = 'アカサタナハマヤラワ0123456789$#%@!';
if (!isTouchDevice) {
let lastTrail = 0;
document.addEventListener('mousemove', e => {
const now = Date.now();
if (now - lastTrail < 35) return;
lastTrail = now;
const c = document.createElement('span');
c.className = 'trail-char';
c.textContent = trailChars[Math.floor(Math.random() * trailChars.length)];
c.style.left = (e.clientX + (Math.random() * 20 - 10)) + 'px';
c.style.top = (e.clientY + (Math.random() * 20 - 10)) + 'px';
document.body.appendChild(c);
setTimeout(() => c.remove(), 800);
});
}
// NAME HOVER / TAP = CHAOS MODE
const glitchWrap = document.getElementById('glitchWrap');
const asciiName = document.getElementById('asciiName');
const originalName = asciiName.textContent;
let chaosInterval = null;
let chaosTouchTimeout = null;
function startChaos() {
glitchWrap.classList.add('chaos');
if (chaosInterval) return;
const pool = '█▓▒░╔╗╚╝║═╬╣╠┌┐└┘│─┼01#@*$%&!?<>/\\+=';
chaosInterval = setInterval(() => {
let out = '';
for (const ch of originalName) {
if (ch === ' ' || ch === '\n') out += ch;
else if (Math.random() < 0.55) out += pool[Math.floor(Math.random() * pool.length)];
else out += ch;
}
asciiName.textContent = out;
document.querySelectorAll('.glitch-wrap .ascii-name.layer').forEach(layer => {
let lout = '';
for (const ch of originalName) {
if (ch === ' ' || ch === '\n') lout += ch;
else if (Math.random() < 0.4) lout += pool[Math.floor(Math.random() * pool.length)];
else lout += ch;
}
layer.textContent = lout;
});
}, 50);
}
function stopChaos() {
glitchWrap.classList.remove('chaos');
clearInterval(chaosInterval);
chaosInterval = null;
asciiName.textContent = originalName;
document.querySelectorAll('.glitch-wrap .ascii-name.layer').forEach(layer => {
layer.textContent = originalName;
});
}
// Desktop: hover
glitchWrap.addEventListener('mouseenter', startChaos);
glitchWrap.addEventListener('mouseleave', stopChaos);
// Mobile: tap name = burst of chaos for 2 seconds
glitchWrap.addEventListener('touchstart', (e) => {
e.preventDefault();
startChaos();
clearTimeout(chaosTouchTimeout);
chaosTouchTimeout = setTimeout(stopChaos, 2000);
}, { passive: false });
// CLICK BURST
document.addEventListener('click', e => {
if (e.target.closest('a, button')) return;
for (let i = 0; i < 15; i++) {
const c = document.createElement('span');
c.className = 'trail-char';
c.textContent = trailChars[Math.floor(Math.random() * trailChars.length)];
const angle = (Math.PI * 2 * i) / 15;
const dist = 40 + Math.random() * 30;
c.style.left = (e.clientX + Math.cos(angle) * dist) + 'px';
c.style.top = (e.clientY + Math.sin(angle) * dist) + 'px';
document.body.appendChild(c);
setTimeout(() => c.remove(), 800);
}
});
// COPY EMAIL TO CLIPBOARD
const contactBtn = document.getElementById('contactBtn');
const contactBtnText = document.getElementById('contactBtnText');
const EMAIL_ADDRESS = 'will@jtom.ca';
let copyResetTimer = null;
async function copyEmail() {
let success = false;
try {
await navigator.clipboard.writeText(EMAIL_ADDRESS);
success = true;
} catch (e) {
// fallback for non-HTTPS / older browsers
try {
const ta = document.createElement('textarea');
ta.value = EMAIL_ADDRESS;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
success = document.execCommand('copy');
document.body.removeChild(ta);
} catch (err) {
success = false;
}
}
if (success) {
contactBtn.classList.add('copied');
contactBtnText.textContent = '✓ COPIED TO CLIPBOARD';
} else {
contactBtnText.textContent = '✗ PRESS CTRL+C — ' + EMAIL_ADDRESS.toUpperCase();
}
clearTimeout(copyResetTimer);
copyResetTimer = setTimeout(() => {
contactBtn.classList.remove('copied');
contactBtnText.textContent = 'COPY_EMAIL: WILL@JTOM.CA';
}, 2200);
// celebration burst from the button
const rect = contactBtn.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
for (let i = 0; i < 20; i++) {
const c = document.createElement('span');
c.className = 'trail-char';
c.textContent = trailChars[Math.floor(Math.random() * trailChars.length)];
const angle = (Math.PI * 2 * i) / 20;
const dist = 60 + Math.random() * 40;
c.style.left = (cx + Math.cos(angle) * dist) + 'px';
c.style.top = (cy + Math.sin(angle) * dist) + 'px';
document.body.appendChild(c);
setTimeout(() => c.remove(), 800);
}
}
contactBtn.addEventListener('click', copyEmail);
</script>
</body>
</html>